Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Preserve quote style in automigrate #17858

Merged
merged 1 commit into from Apr 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 33 additions & 6 deletions lib/csf-tools/src/ConfigFile.test.ts
Expand Up @@ -174,7 +174,7 @@ describe('ConfigFile', () => {
).toMatchInlineSnapshot(`
export const core = {
foo: 'bar',
builder: "webpack5"
builder: 'webpack5'
};
`);
});
Expand All @@ -189,7 +189,7 @@ describe('ConfigFile', () => {
)
).toMatchInlineSnapshot(`
export const core = {
builder: "webpack5"
builder: 'webpack5'
};
`);
});
Expand All @@ -205,7 +205,7 @@ describe('ConfigFile', () => {
).toMatchInlineSnapshot(`
export const core = {
builder: {
"name": "webpack5"
name: 'webpack5'
}
};
`);
Expand All @@ -222,7 +222,7 @@ describe('ConfigFile', () => {
)
).toMatchInlineSnapshot(`
const coreVar = {
builder: "webpack5"
builder: 'webpack5'
};
export const core = coreVar;
`);
Expand Down Expand Up @@ -260,7 +260,7 @@ describe('ConfigFile', () => {
module.exports = {
core: {
foo: 'bar',
builder: "webpack5"
builder: 'webpack5'
}
};
`);
Expand All @@ -277,11 +277,38 @@ describe('ConfigFile', () => {
).toMatchInlineSnapshot(`
module.exports = {
core: {
builder: "webpack5"
builder: 'webpack5'
}
};
`);
});
});
describe('quotes', () => {
it('no quotes', () => {
expect(setField(['foo', 'bar'], 'baz', '')).toMatchInlineSnapshot(`
export const foo = {
bar: "baz"
};
`);
});
it('more single quotes', () => {
expect(setField(['foo', 'bar'], 'baz', `export const stories = ['a', 'b', "c"]`))
.toMatchInlineSnapshot(`
export const stories = ['a', 'b', "c"];
export const foo = {
bar: 'baz'
};
`);
});
it('more double quotes', () => {
expect(setField(['foo', 'bar'], 'baz', `export const stories = ['a', "b", "c"]`))
.toMatchInlineSnapshot(`
export const stories = ['a', "b", "c"];
export const foo = {
bar: "baz"
};
`);
});
});
});
});
64 changes: 47 additions & 17 deletions lib/csf-tools/src/ConfigFile.ts
Expand Up @@ -81,14 +81,19 @@ const _updateExportNode = (path: string[], expr: t.Expression, existing: t.Objec
export class ConfigFile {
_ast: t.File;

_code: string;

_exports: Record<string, t.Expression> = {};

_exportsObject: t.ObjectExpression;

_quotes: 'single' | 'double' | undefined;

fileName?: string;

constructor(ast: t.File, fileName?: string) {
constructor(ast: t.File, code: string, fileName?: string) {
this._ast = ast;
this._code = code;
this.fileName = fileName;
}

Expand Down Expand Up @@ -190,24 +195,49 @@ export class ConfigFile {
}
}

_inferQuotes() {
if (!this._quotes) {
// first 500 tokens for efficiency
const occurrences = (this._ast.tokens || []).slice(0, 500).reduce(
(acc, token) => {
if (token.type.label === 'string') {
acc[this._code[token.start]] += 1;
}
return acc;
},
{ "'": 0, '"': 0 }
);
this._quotes = occurrences["'"] > occurrences['"'] ? 'single' : 'double';
}
return this._quotes;
}

setFieldValue(path: string[], value: any) {
const stringified = JSON.stringify(value);
const program = babelParse(`const __x = ${stringified}`);
const quotes = this._inferQuotes();
let valueNode;
traverse(program, {
VariableDeclaration: {
enter({ node }) {
if (
node.declarations.length === 1 &&
t.isVariableDeclarator(node.declarations[0]) &&
t.isIdentifier(node.declarations[0].id) &&
node.declarations[0].id.name === '__x'
) {
valueNode = node.declarations[0].init;
}
// we do this rather than t.valueToNode because apparently
// babel only preserves quotes if they are parsed from the original code.
if (quotes === 'single') {
const { code } = generate(t.valueToNode(value), { jsescOption: { quotes } });
const program = babelParse(`const __x = ${code}`);
traverse(program, {
VariableDeclaration: {
enter({ node }) {
if (
node.declarations.length === 1 &&
t.isVariableDeclarator(node.declarations[0]) &&
t.isIdentifier(node.declarations[0].id) &&
node.declarations[0].id.name === '__x'
) {
valueNode = node.declarations[0].init;
}
},
},
},
});
});
} else {
// double quotes is the default so we can skip all that
valueNode = t.valueToNode(value);
}
if (!valueNode) {
throw new Error(`Unexpected value ${JSON.stringify(value)}`);
}
Expand All @@ -217,7 +247,7 @@ export class ConfigFile {

export const loadConfig = (code: string, fileName?: string) => {
const ast = babelParse(code);
return new ConfigFile(ast, fileName);
return new ConfigFile(ast, code, fileName);
};

export const formatConfig = (config: ConfigFile) => {
Expand Down
1 change: 1 addition & 0 deletions lib/csf-tools/src/babelParse.ts
Expand Up @@ -10,4 +10,5 @@ export const babelParse = (code: string) =>
['decorators', { decoratorsBeforeExport: true }],
'classProperties',
],
tokens: true,
});