1
- import { merge } from "lodash" ;
2
1
import * as path from "path" ;
2
+ import * as ts from "typescript" ;
3
3
4
4
import { parseJson } from "../../../utils" ;
5
5
import { EditorConfigConverter } from "../types" ;
@@ -12,8 +12,19 @@ const knownMissingSettings = [
12
12
"tslint.suppressWhileTypeErrorsPresent" ,
13
13
] ;
14
14
15
+ const getJsonRoot = ( sourceFile : ts . SourceFile ) => {
16
+ const [ rootStatement ] = sourceFile . statements ;
17
+
18
+ return ts . isExpressionStatement ( rootStatement ) && ts . isObjectLiteralExpression ( rootStatement . expression )
19
+ ? rootStatement . expression
20
+ : undefined ;
21
+ }
22
+
23
+
15
24
export const convertVSCodeConfig : EditorConfigConverter = ( rawEditorSettings , settings ) => {
16
25
const editorSettings : Record < string , string | number | symbol > = parseJson ( rawEditorSettings ) ;
26
+ const missing = knownMissingSettings . filter ( ( setting ) => editorSettings [ setting ] ) ;
27
+
17
28
const autoFixOnSave =
18
29
editorSettings [ "editor.codeActionsOnSave" ] &&
19
30
typeof editorSettings [ "editor.codeActionsOnSave" ] === "object" &&
@@ -28,26 +39,97 @@ export const convertVSCodeConfig: EditorConfigConverter = (rawEditorSettings, se
28
39
path . dirname ( settings . config ) ,
29
40
) ;
30
41
31
- const contents = JSON . stringify (
32
- merge (
33
- { } ,
34
- editorSettings ,
35
- autoFixOnSave !== undefined && {
36
- "editor.codeActionsOnSave" : {
37
- "eslint.autoFixOnSave" : autoFixOnSave ,
38
- } ,
39
- } ,
40
- eslintPathMatches && {
41
- "eslint.options" : {
42
- configFile : settings . config ,
43
- } ,
44
- } ,
45
- ) ,
46
- null ,
47
- 4 ,
48
- ) ;
42
+ // We can bail without making changes if there are no changes we need to make...
43
+ if ( ! autoFixOnSave && ! eslintPathMatches ) {
44
+ return { contents : rawEditorSettings , missing } ;
45
+ }
49
46
50
- const missing = knownMissingSettings . filter ( ( setting ) => editorSettings [ setting ] ) ;
47
+ // ...or the JSON file doesn't seem to be a normal {} object root
48
+ const sourceFile = ts . createSourceFile ( "settings.json" , rawEditorSettings , ts . ScriptTarget . Latest , /*setParentNodes*/ true , ts . ScriptKind . JSON ) ;
49
+ const jsonRoot = getJsonRoot ( sourceFile ) ;
50
+ if ( ! jsonRoot ) {
51
+ return { contents : rawEditorSettings , missing } ;
52
+ }
53
+
54
+ const propertyIndexByName = ( properties : ts . NodeArray < ts . ObjectLiteralElementLike > , name : string ) =>
55
+ properties . findIndex ( property => property . name && ts . isStringLiteral ( property . name ) && property . name . text === name ) ;
56
+
57
+ const transformer = ( context : ts . TransformationContext ) => ( rootNode : ts . SourceFile ) : ts . SourceFile => {
58
+ const upsertProperties = ( node : ts . ObjectLiteralExpression , additions : readonly [ string , string , unknown ] [ ] ) => {
59
+ const originalProperties = node . properties ;
60
+
61
+ for ( const [ parent , setting , value ] of additions ) {
62
+ const createNewChild = ( properties ?: ts . NodeArray < ts . ObjectLiteralElementLike > ) => {
63
+ return context . factory . createPropertyAssignment (
64
+ `"${ parent } "` ,
65
+ context . factory . createObjectLiteralExpression (
66
+ [
67
+ ...properties ?? [ ] ,
68
+ context . factory . createPropertyAssignment (
69
+ `"${ setting } "` ,
70
+ typeof value === "string"
71
+ ? context . factory . createStringLiteral ( value )
72
+ : value
73
+ ? context . factory . createTrue ( )
74
+ : context . factory . createFalse ( )
75
+ )
76
+ ] ,
77
+ true
78
+ ) ,
79
+ ) ;
80
+ }
81
+
82
+ const existingIndex = propertyIndexByName ( originalProperties , parent ) ;
83
+
84
+ if ( existingIndex !== - 1 ) {
85
+ const existingProperty = originalProperties [ existingIndex ] ;
86
+ if (
87
+ ! ts . isPropertyAssignment ( existingProperty )
88
+ || ! ts . isObjectLiteralExpression ( existingProperty . initializer )
89
+ || propertyIndexByName ( existingProperty . initializer . properties , `"${ parent } "` ) === - 1 ) {
90
+ return node ;
91
+ }
92
+
93
+ const updatedProperties = [ ...node . properties ] ;
94
+ updatedProperties [ existingIndex ] = createNewChild ( existingProperty . initializer . properties )
95
+ node = context . factory . createObjectLiteralExpression ( updatedProperties , true ) ;
96
+ } else {
97
+ node = context . factory . createObjectLiteralExpression ( [ ...node . properties , createNewChild ( ) ] , true ) ;
98
+ }
99
+ }
100
+
101
+ return node ;
102
+ } ;
103
+
104
+ const visit = ( node : ts . Node ) => {
105
+ node = ts . visitEachChild ( node , visit , context ) ;
106
+
107
+ if ( node !== jsonRoot ) {
108
+ return node ;
109
+ }
110
+
111
+ const additions : [ string , string , unknown ] [ ] = [ ] ;
112
+
113
+ if ( autoFixOnSave !== undefined ) {
114
+ additions . push ( [ "editor.codeActionsOnSave" , "eslint.autoFixOnSave" , autoFixOnSave ] ) ;
115
+ }
116
+
117
+ if ( eslintPathMatches !== undefined ) {
118
+ additions . push ( [ "eslint.options" , "configFile" , settings . config ] ) ;
119
+ }
120
+
121
+ return upsertProperties ( jsonRoot , additions ) ;
122
+ } ;
123
+
124
+ return ts . visitNode ( rootNode , visit )
125
+ } ;
126
+
127
+ const printer = ts . createPrinter ( undefined ) ;
128
+ const result = ts . transform ( sourceFile , [ transformer ] ) ;
129
+ const contents = printer . printFile ( result . transformed [ 0 ] )
130
+ . replace ( / ^ \( / giu, "" )
131
+ . replace ( / \) ; ( \r \n | \r | \n ) * $ / giu, "$1" )
132
+ result . dispose ( ) ;
51
133
52
134
return { contents, missing } ;
53
135
} ;
0 commit comments