Skip to content

Commit 3734366

Browse files
WIP: JSON patching
1 parent ed4eaa1 commit 3734366

File tree

1 file changed

+102
-20
lines changed

1 file changed

+102
-20
lines changed
Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { merge } from "lodash";
21
import * as path from "path";
2+
import * as ts from "typescript";
33

44
import { parseJson } from "../../../utils";
55
import { EditorConfigConverter } from "../types";
@@ -12,8 +12,19 @@ const knownMissingSettings = [
1212
"tslint.suppressWhileTypeErrorsPresent",
1313
];
1414

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+
1524
export const convertVSCodeConfig: EditorConfigConverter = (rawEditorSettings, settings) => {
1625
const editorSettings: Record<string, string | number | symbol> = parseJson(rawEditorSettings);
26+
const missing = knownMissingSettings.filter((setting) => editorSettings[setting]);
27+
1728
const autoFixOnSave =
1829
editorSettings["editor.codeActionsOnSave"] &&
1930
typeof editorSettings["editor.codeActionsOnSave"] === "object" &&
@@ -28,26 +39,97 @@ export const convertVSCodeConfig: EditorConfigConverter = (rawEditorSettings, se
2839
path.dirname(settings.config),
2940
);
3041

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+
}
4946

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();
51133

52134
return { contents, missing };
53135
};

0 commit comments

Comments
 (0)