diff --git a/code/lib/codemod/src/transforms/csf-2-to-3.ts b/code/lib/codemod/src/transforms/csf-2-to-3.ts index c0cf079d0235..945f3542b6e8 100644 --- a/code/lib/codemod/src/transforms/csf-2-to-3.ts +++ b/code/lib/codemod/src/transforms/csf-2-to-3.ts @@ -3,11 +3,10 @@ import prettier from 'prettier'; import * as t from '@babel/types'; import { isIdentifier, isTSTypeAnnotation, isTSTypeReference } from '@babel/types'; import type { CsfFile } from '@storybook/csf-tools'; -import { loadCsf } from '@storybook/csf-tools'; +import { loadCsf, printCsf } from '@storybook/csf-tools'; import type { API, FileInfo } from 'jscodeshift'; import type { BabelFile, NodePath } from '@babel/core'; import * as babel from '@babel/core'; -import * as recast from 'recast'; import { upgradeDeprecatedTypes } from './upgrade-deprecated-types'; const logger = console; @@ -202,7 +201,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?: importHelper.removeDeprecatedStoryImport(); removeUnusedTemplates(csf); - let output = recast.print(csf._ast, {}).code; + let output = printCsf(csf).code; try { const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || { diff --git a/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts b/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts index 6b76e75b5aa5..f2b2a97bcd09 100644 --- a/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts +++ b/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts @@ -3,8 +3,7 @@ import prettier from 'prettier'; import type { API, FileInfo } from 'jscodeshift'; import type { BabelFile, NodePath } from '@babel/core'; import * as babel from '@babel/core'; -import { loadCsf } from '@storybook/csf-tools'; -import * as recast from 'recast'; +import { loadCsf, printCsf } from '@storybook/csf-tools'; import * as t from '@babel/types'; const logger = console; @@ -24,7 +23,8 @@ function migrateType(oldType: string) { export default function transform(info: FileInfo, api: API, options: { parser?: string }) { // TODO what do I need to with the title? - const fileNode = loadCsf(info.source, { makeTitle: (title) => title })._ast; + const csf = loadCsf(info.source, { makeTitle: (title) => title }); + const fileNode = csf._ast; // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 const file: BabelFile = new babel.File( { filename: info.path }, @@ -33,7 +33,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?: upgradeDeprecatedTypes(file); - let output = recast.print(file.path.node).code; + let output = printCsf(csf).code; try { const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || { diff --git a/code/lib/core-server/src/presets/common-preset.ts b/code/lib/core-server/src/presets/common-preset.ts index 76240e469f4e..2d0ba9ec61b4 100644 --- a/code/lib/core-server/src/presets/common-preset.ts +++ b/code/lib/core-server/src/presets/common-preset.ts @@ -1,4 +1,4 @@ -import { pathExists, readFile } from 'fs-extra'; +import fs, { pathExists, readFile } from 'fs-extra'; import { deprecate, logger } from '@storybook/node-logger'; import { telemetry } from '@storybook/telemetry'; import { @@ -17,7 +17,7 @@ import type { StorybookConfig, StoryIndexer, } from '@storybook/types'; -import { loadCsf, readConfig, writeConfig } from '@storybook/csf-tools'; +import { loadCsf, printConfig, readConfig } from '@storybook/csf-tools'; import { join } from 'path'; import { dedent } from 'ts-dedent'; import fetch from 'node-fetch'; @@ -29,6 +29,7 @@ import { SET_WHATS_NEW_CACHE, TOGGLE_WHATS_NEW_NOTIFICATIONS, } from '@storybook/core-events'; +import invariant from 'tiny-invariant'; import { parseStaticDir } from '../utils/server-statics'; import { defaultStaticDirs } from '../utils/constants'; import { sendTelemetryError } from '../withTelemetry'; @@ -313,19 +314,18 @@ export const experimental_serverChannel = async ( async ({ disableWhatsNewNotifications }: { disableWhatsNewNotifications: boolean }) => { const isTelemetryEnabled = coreOptions.disableTelemetry !== true; try { - const configFileName = findConfigFile('main', options.configDir); - if (!configFileName) - throw new Error(`unable to find storybook main file in ${options.configDir}`); - const main = await readConfig(configFileName); + const mainPath = findConfigFile('main', options.configDir); + invariant(mainPath, `unable to find storybook main file in ${options.configDir}`); + const main = await readConfig(mainPath); main.setFieldValue(['core', 'disableWhatsNewNotifications'], disableWhatsNewNotifications); - await writeConfig(main); - + await fs.writeFile(mainPath, printConfig(main).code); if (isTelemetryEnabled) { await telemetry('core-config', { disableWhatsNewNotifications }); } } catch (error) { + invariant(error instanceof Error); if (isTelemetryEnabled) { - await sendTelemetryError(error as Error, 'core-config', { + await sendTelemetryError(error, 'core-config', { cliOptions: options, presetOptions: { ...options, corePresets: [], overridePresets: [] }, skipPrompt: true, diff --git a/code/lib/csf-tools/package.json b/code/lib/csf-tools/package.json index 78f16fa1443a..8f9adb1a8191 100644 --- a/code/lib/csf-tools/package.json +++ b/code/lib/csf-tools/package.json @@ -48,6 +48,7 @@ "@storybook/csf": "^0.1.0", "@storybook/types": "workspace:*", "fs-extra": "^11.1.0", + "prettier": "^2.8.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" }, diff --git a/code/lib/csf-tools/src/ConfigFile.test.ts b/code/lib/csf-tools/src/ConfigFile.test.ts index 58df95ba4999..0f2f1d12e1e4 100644 --- a/code/lib/csf-tools/src/ConfigFile.test.ts +++ b/code/lib/csf-tools/src/ConfigFile.test.ts @@ -1,7 +1,7 @@ /// ; import { dedent } from 'ts-dedent'; -import { formatConfig, loadConfig } from './ConfigFile'; +import { loadConfig, printConfig } from './ConfigFile'; import { babelPrint } from './babelParse'; expect.addSnapshotSerializer({ @@ -17,19 +17,19 @@ const getField = (path: string[], source: string) => { const setField = (path: string[], value: any, source: string) => { const config = loadConfig(source).parse(); config.setFieldValue(path, value); - return formatConfig(config); + return printConfig(config).code; }; const appendToArray = (path: string[], value: any, source: string) => { const config = loadConfig(source).parse(); config.appendValueToArray(path, value); - return formatConfig(config); + return printConfig(config).code; }; const removeField = (path: string[], source: string) => { const config = loadConfig(source).parse(); config.removeField(path); - return formatConfig(config); + return printConfig(config).code; }; describe('ConfigFile', () => { @@ -233,9 +233,11 @@ describe('ConfigFile', () => { ) ).toMatchInlineSnapshot(` export const addons = []; + export const core = { - builder: "webpack5" + builder: 'webpack5', }; + `); }); it('missing field', () => { @@ -250,8 +252,9 @@ describe('ConfigFile', () => { ).toMatchInlineSnapshot(` export const core = { foo: 'bar', - builder: 'webpack5' + builder: 'webpack5', }; + `); }); it('found scalar', () => { @@ -264,9 +267,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export const core = { - builder: 'webpack5' - }; + export const core = { builder: 'webpack5' }; + `); }); it('found top-level scalar', () => { @@ -278,7 +280,10 @@ describe('ConfigFile', () => { export const foo = 'bar'; ` ) - ).toMatchInlineSnapshot(`export const foo = 'baz';`); + ).toMatchInlineSnapshot(` + export const foo = 'baz'; + + `); }); it('found object', () => { expect( @@ -292,9 +297,10 @@ describe('ConfigFile', () => { ).toMatchInlineSnapshot(` export const core = { builder: { - name: 'webpack5' - } + name: 'webpack5', + }, }; + `); }); it('variable export', () => { @@ -308,10 +314,9 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - const coreVar = { - builder: 'webpack5' - }; + const coreVar = { builder: 'webpack5' }; export const core = coreVar; + `); }); }); @@ -329,10 +334,12 @@ describe('ConfigFile', () => { ).toMatchInlineSnapshot(` module.exports = { addons: [], + core: { - builder: "webpack5" - } + builder: 'webpack5', + }, }; + `); }); it('missing field', () => { @@ -348,9 +355,10 @@ describe('ConfigFile', () => { module.exports = { core: { foo: 'bar', - builder: 'webpack5' - } + builder: 'webpack5', + }, }; + `); }); it('found scalar', () => { @@ -363,11 +371,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - module.exports = { - core: { - builder: 'webpack5' - } - }; + module.exports = { core: { builder: 'webpack5' } }; + `); }); }); @@ -385,10 +390,12 @@ describe('ConfigFile', () => { ).toMatchInlineSnapshot(` export default { addons: [], + core: { - builder: "webpack5" - } + builder: 'webpack5', + }, }; + `); }); it('missing field', () => { @@ -404,9 +411,10 @@ describe('ConfigFile', () => { export default { core: { foo: 'bar', - builder: 'webpack5' - } + builder: 'webpack5', + }, }; + `); }); it('found scalar', () => { @@ -419,11 +427,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - core: { - builder: 'webpack5' - } - }; + export default { core: { builder: 'webpack5' } }; + `); }); }); @@ -432,26 +437,31 @@ describe('ConfigFile', () => { it('no quotes', () => { expect(setField(['foo', 'bar'], 'baz', '')).toMatchInlineSnapshot(` export const foo = { - bar: "baz" + 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 stories = ['a', 'b', 'c']; + export const foo = { - bar: 'baz' + 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 stories = ['a', 'b', 'c']; + export const foo = { - bar: "baz" + bar: 'baz', }; + `); }); }); @@ -469,11 +479,10 @@ describe('ConfigFile', () => { ) ).toMatchInlineSnapshot(` export default { - core: { - builder: 'webpack5' - }, - addons: ['docs'] + core: { builder: 'webpack5' }, + addons: ['docs'], }; + `); }); it('found scalar', () => { @@ -497,9 +506,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - addons: ['a11y', 'viewport', 'docs'] - }; + export default { addons: ['a11y', 'viewport', 'docs'] }; + `); }); @@ -513,9 +521,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - addons: [require.resolve('a11y'), someVariable, 'docs'] - }; + export default { addons: [require.resolve('a11y'), someVariable, 'docs'] }; + `); }); }); @@ -530,7 +537,10 @@ describe('ConfigFile', () => { export const addons = []; ` ) - ).toMatchInlineSnapshot(`export const addons = [];`); + ).toMatchInlineSnapshot(` + export const addons = []; + + `); }); it('missing field', () => { expect( @@ -541,9 +551,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export const core = { - foo: 'bar' - }; + export const core = { foo: 'bar' }; + `); }); it('found scalar', () => { @@ -554,7 +563,10 @@ describe('ConfigFile', () => { export const core = { builder: 'webpack4' }; ` ) - ).toMatchInlineSnapshot(`export const core = {};`); + ).toMatchInlineSnapshot(` + export const core = {}; + + `); }); it('found object', () => { expect( @@ -564,7 +576,10 @@ describe('ConfigFile', () => { export const core = { builder: { name: 'webpack4' } }; ` ) - ).toMatchInlineSnapshot(`export const core = {};`); + ).toMatchInlineSnapshot(` + export const core = {}; + + `); }); it('nested object', () => { expect( @@ -575,9 +590,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export const core = { - builder: {} - }; + export const core = { builder: {} }; + `); }); it('string literal key', () => { @@ -588,7 +602,10 @@ describe('ConfigFile', () => { export const core = { 'builder': 'webpack4' }; ` ) - ).toMatchInlineSnapshot(`export const core = {};`); + ).toMatchInlineSnapshot(` + export const core = {}; + + `); }); it('variable export', () => { expect( @@ -602,6 +619,7 @@ describe('ConfigFile', () => { ).toMatchInlineSnapshot(` const coreVar = {}; export const core = coreVar; + `); }); it('root export variable', () => { @@ -614,7 +632,10 @@ describe('ConfigFile', () => { export const addons = []; ` ) - ).toMatchInlineSnapshot(`export const addons = [];`); + ).toMatchInlineSnapshot(` + export const addons = []; + + `); }); }); @@ -628,9 +649,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - module.exports = { - addons: [] - }; + module.exports = { addons: [] }; + `); }); it('missing field', () => { @@ -642,11 +662,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - module.exports = { - core: { - foo: 'bar' - } - }; + module.exports = { core: { foo: 'bar' } }; + `); }); it('found scalar', () => { @@ -658,9 +675,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - module.exports = { - core: {} - }; + module.exports = { core: {} }; + `); }); it('nested scalar', () => { @@ -672,11 +688,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - module.exports = { - core: { - builder: {} - } - }; + module.exports = { core: { builder: {} } }; + `); }); it('string literal key', () => { @@ -688,9 +701,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - module.exports = { - 'core': {} - }; + module.exports = { core: {} }; + `); }); it('root property', () => { @@ -703,8 +715,9 @@ describe('ConfigFile', () => { ) ).toMatchInlineSnapshot(` module.exports = { - addons: [] + addons: [], }; + `); }); }); @@ -719,9 +732,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - addons: [] - }; + export default { addons: [] }; + `); }); it('missing field', () => { @@ -733,11 +745,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - core: { - foo: 'bar' - } - }; + export default { core: { foo: 'bar' } }; + `); }); it('found scalar', () => { @@ -749,9 +758,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - core: {} - }; + export default { core: {} }; + `); }); it('nested scalar', () => { @@ -763,11 +771,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - core: { - builder: {} - } - }; + export default { core: { builder: {} } }; + `); }); it('string literal key', () => { @@ -779,9 +784,8 @@ describe('ConfigFile', () => { ` ) ).toMatchInlineSnapshot(` - export default { - 'core': {} - }; + export default { core: {} }; + `); }); it('root property', () => { @@ -794,8 +798,9 @@ describe('ConfigFile', () => { ) ).toMatchInlineSnapshot(` export default { - addons: [] + addons: [], }; + `); }); }); @@ -804,26 +809,31 @@ describe('ConfigFile', () => { it('no quotes', () => { expect(setField(['foo', 'bar'], 'baz', '')).toMatchInlineSnapshot(` export const foo = { - bar: "baz" + 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 stories = ['a', 'b', 'c']; + export const foo = { - bar: 'baz' + 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 stories = ['a', 'b', 'c']; + export const foo = { - bar: "baz" + bar: 'baz', }; + `); }); }); diff --git a/code/lib/csf-tools/src/ConfigFile.ts b/code/lib/csf-tools/src/ConfigFile.ts index 7374d065f0e0..bd66ebb605bb 100644 --- a/code/lib/csf-tools/src/ConfigFile.ts +++ b/code/lib/csf-tools/src/ConfigFile.ts @@ -6,6 +6,10 @@ import * as t from '@babel/types'; import * as generate from '@babel/generator'; import * as traverse from '@babel/traverse'; +import * as recast from 'recast'; +import prettier from 'prettier'; + +import type { Options } from 'recast'; import { babelParse } from './babelParse'; const logger = console; @@ -617,6 +621,25 @@ export const formatConfig = (config: ConfigFile) => { return code; }; +export const printConfig = (config: ConfigFile, options: Options = {}) => { + const result = recast.print(config._ast, options); + const prettierConfig = prettier.resolveConfig.sync('.'); + + if (prettierConfig) { + let pretty: string; + try { + pretty = prettier.format(result.code, { + ...prettierConfig, + filepath: config.fileName ?? 'main.ts', + }); + } catch (_) { + pretty = result.code; + } + return { ...result, code: pretty }; + } + return result; +}; + export const readConfig = async (fileName: string) => { const code = (await fs.readFile(fileName, 'utf-8')).toString(); return loadConfig(code, fileName).parse(); @@ -625,5 +648,5 @@ export const readConfig = async (fileName: string) => { export const writeConfig = async (config: ConfigFile, fileName?: string) => { const fname = fileName || config.fileName; if (!fname) throw new Error('Please specify a fileName for writeConfig'); - await fs.writeFile(fname, await formatConfig(config)); + await fs.writeFile(fname, formatConfig(config)); }; diff --git a/code/lib/csf-tools/src/CsfFile.ts b/code/lib/csf-tools/src/CsfFile.ts index c6e154b43549..5e3a04d5c13a 100644 --- a/code/lib/csf-tools/src/CsfFile.ts +++ b/code/lib/csf-tools/src/CsfFile.ts @@ -5,10 +5,12 @@ import { dedent } from 'ts-dedent'; import * as t from '@babel/types'; import * as generate from '@babel/generator'; +import * as recast from 'recast'; import * as traverse from '@babel/traverse'; import { toId, isExportStory, storyNameFromExport } from '@storybook/csf'; import type { Tag, StoryAnnotations, ComponentAnnotations } from '@storybook/types'; +import type { Options } from 'recast'; import { babelParse } from './babelParse'; import { findVarInitialization } from './findVarInitialization'; @@ -192,7 +194,7 @@ export class CsfFile { } else if (['includeStories', 'excludeStories'].includes(p.key.name)) { (meta as any)[p.key.name] = parseIncludeExclude(p.value); } else if (p.key.name === 'component') { - const { code } = generate.default(p.value, {}); + const { code } = recast.print(p.value, {}); meta.component = code; } else if (p.key.name === 'tags') { let node = p.value; @@ -533,7 +535,9 @@ export const loadCsf = (code: string, options: CsfOptions) => { interface FormatOptions { sourceMaps?: boolean; + preserveStyle?: boolean; } + export const formatCsf = (csf: CsfFile, options: FormatOptions = { sourceMaps: false }) => { const result = generate.default(csf._ast, options); if (options.sourceMaps) { @@ -543,6 +547,13 @@ export const formatCsf = (csf: CsfFile, options: FormatOptions = { sourceMaps: f return code; }; +/** + * Use this function, if you want to preserve styles. Uses recast under the hood. + */ +export const printCsf = (csf: CsfFile, options: Options = {}) => { + return recast.print(csf._ast, options); +}; + export const readCsf = async (fileName: string, options: CsfOptions) => { const code = (await fs.readFile(fileName, 'utf-8')).toString(); return loadCsf(code, { ...options, fileName }); @@ -551,5 +562,5 @@ export const readCsf = async (fileName: string, options: CsfOptions) => { export const writeCsf = async (csf: CsfFile, fileName?: string) => { const fname = fileName || csf._fileName; if (!fname) throw new Error('Please specify a fileName for writeCsf'); - await fs.writeFile(fileName as string, (await formatCsf(csf)) as string); + await fs.writeFile(fileName as string, printCsf(csf).code); }; diff --git a/code/lib/csf-tools/src/enrichCsf.test.ts b/code/lib/csf-tools/src/enrichCsf.test.ts index 1f202bfb5a3a..a8f5f3aaa09c 100644 --- a/code/lib/csf-tools/src/enrichCsf.test.ts +++ b/code/lib/csf-tools/src/enrichCsf.test.ts @@ -2,9 +2,9 @@ /* eslint-disable no-underscore-dangle */ import { dedent } from 'ts-dedent'; -import { loadCsf, formatCsf } from './CsfFile'; -import { enrichCsf, extractSource } from './enrichCsf'; +import { formatCsf, loadCsf } from './CsfFile'; import type { EnrichCsfOptions } from './enrichCsf'; +import { enrichCsf, extractSource } from './enrichCsf'; expect.addSnapshotSerializer({ print: (val: any) => val, diff --git a/code/yarn.lock b/code/yarn.lock index 59939b03ef02..1f98569497a5 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6733,6 +6733,7 @@ __metadata: "@types/js-yaml": ^4.0.5 fs-extra: ^11.1.0 js-yaml: ^4.1.0 + prettier: ^2.8.0 recast: ^0.23.1 ts-dedent: ^2.0.0 typescript: ~4.9.3