Skip to content

Commit

Permalink
Merge pull request #17858 from storybookjs/tech/automigrate-preserve-…
Browse files Browse the repository at this point in the history
…quotes

CLI: Preserve quote style in automigrate
  • Loading branch information
shilman committed Apr 9, 2022
1 parent b6cb510 commit b4f2056
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 23 deletions.
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,
});

0 comments on commit b4f2056

Please sign in to comment.