@storybook/codemod 7.0.0-beta.9 → 7.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +0 -39
  2. package/dist/index.js +1 -1
  3. package/dist/transforms/csf-2-to-3.d.ts +5 -4
  4. package/dist/transforms/csf-2-to-3.js +3 -1
  5. package/dist/transforms/mdx-to-csf.d.ts +7 -0
  6. package/dist/transforms/mdx-to-csf.js +55 -0
  7. package/dist/transforms/storiesof-to-csf.js +1 -1
  8. package/dist/transforms/upgrade-deprecated-types.d.ts +10 -0
  9. package/dist/transforms/upgrade-deprecated-types.js +2 -0
  10. package/jest.config.js +2 -0
  11. package/package.json +31 -16
  12. package/project.json +6 -0
  13. package/src/index.js +30 -4
  14. package/src/lib/utils.ts +2 -2
  15. package/src/transforms/__testfixtures__/storiesof-to-csf/decorators.input.js +1 -1
  16. package/src/transforms/__testfixtures__/storiesof-to-csf/export-function.input.js +1 -1
  17. package/src/transforms/__testfixtures__/storiesof-to-csf/story-decorators.input.js +1 -1
  18. package/src/transforms/__testfixtures__/update-addon-info/update-addon-info.input.js +34 -48
  19. package/src/transforms/__testfixtures__/update-addon-info/update-addon-info.output.snapshot +33 -47
  20. package/src/transforms/__tests__/csf-2-to-3.test.ts +170 -57
  21. package/src/transforms/__tests__/mdx-to-csf.test.ts +611 -0
  22. package/src/transforms/__tests__/upgrade-deprecated-types.test.ts +170 -0
  23. package/src/transforms/csf-2-to-3.ts +173 -37
  24. package/src/transforms/mdx-to-csf.ts +342 -0
  25. package/src/transforms/upgrade-deprecated-types.ts +142 -0
  26. package/dist/chunk-3OPQTROG.mjs +0 -1
  27. package/dist/chunk-B5FMQ3BX.mjs +0 -1
  28. package/dist/chunk-CO6EPEMB.mjs +0 -1
  29. package/dist/index.mjs +0 -1
  30. package/dist/transforms/add-component-parameters.mjs +0 -1
  31. package/dist/transforms/csf-2-to-3.mjs +0 -1
  32. package/dist/transforms/csf-hoist-story-annotations.mjs +0 -1
  33. package/dist/transforms/csf-to-mdx.d.ts +0 -29
  34. package/dist/transforms/csf-to-mdx.js +0 -3
  35. package/dist/transforms/csf-to-mdx.mjs +0 -3
  36. package/dist/transforms/move-builtin-addons.mjs +0 -1
  37. package/dist/transforms/storiesof-to-csf.mjs +0 -1
  38. package/dist/transforms/update-addon-info.mjs +0 -1
  39. package/dist/transforms/update-organisation-name.mjs +0 -1
  40. package/dist/transforms/upgrade-hierarchy-separators.mjs +0 -1
  41. package/src/transforms/__testfixtures__/csf-to-mdx/basic.input.js +0 -20
  42. package/src/transforms/__testfixtures__/csf-to-mdx/basic.output.snapshot +0 -18
  43. package/src/transforms/__testfixtures__/csf-to-mdx/component-id.input.js +0 -9
  44. package/src/transforms/__testfixtures__/csf-to-mdx/component-id.output.snapshot +0 -10
  45. package/src/transforms/__testfixtures__/csf-to-mdx/decorators.input.js +0 -13
  46. package/src/transforms/__testfixtures__/csf-to-mdx/decorators.output.snapshot +0 -12
  47. package/src/transforms/__testfixtures__/csf-to-mdx/exclude-stories.input.js +0 -23
  48. package/src/transforms/__testfixtures__/csf-to-mdx/exclude-stories.output.snapshot +0 -22
  49. package/src/transforms/__testfixtures__/csf-to-mdx/parameters.input.js +0 -16
  50. package/src/transforms/__testfixtures__/csf-to-mdx/parameters.output.snapshot +0 -17
  51. package/src/transforms/__testfixtures__/csf-to-mdx/story-function.input.js +0 -19
  52. package/src/transforms/__testfixtures__/csf-to-mdx/story-function.output.snapshot +0 -18
  53. package/src/transforms/__testfixtures__/csf-to-mdx/story-parameters.input.js +0 -24
  54. package/src/transforms/__testfixtures__/csf-to-mdx/story-parameters.output.snapshot +0 -22
  55. package/src/transforms/csf-to-mdx.js +0 -190
@@ -0,0 +1,170 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { dedent } from 'ts-dedent';
3
+ import type { API } from 'jscodeshift';
4
+ import ansiRegex from 'ansi-regex';
5
+ import _transform from '../upgrade-deprecated-types';
6
+
7
+ expect.addSnapshotSerializer({
8
+ print: (val: any) => val,
9
+ test: () => true,
10
+ });
11
+
12
+ const tsTransform = (source: string) =>
13
+ _transform({ source, path: 'Component.stories.ts' }, {} as API, { parser: 'tsx' }).trim();
14
+
15
+ describe('upgrade-deprecated-types', () => {
16
+ describe('typescript', () => {
17
+ it('upgrade regular imports', () => {
18
+ expect(
19
+ tsTransform(dedent`
20
+ import { Story, ComponentMeta, Meta, ComponentStory, ComponentStoryObj, ComponentStoryFn } from '@storybook/react';
21
+ import { Cat, CatProps } from './Cat';
22
+
23
+ const meta = { title: 'Cat', component: Cat } satisfies ComponentMeta<typeof Cat>
24
+ const meta2: Meta<CatProps> = { title: 'Cat', component: Cat };
25
+ export default meta;
26
+
27
+ export const A: ComponentStory<typeof Cat> = (args) => <Cat {...args} />;
28
+ export const B: any = (args) => <Button {...args} />;
29
+ export const C: ComponentStoryFn<typeof Cat> = (args) => <Cat {...args} />;
30
+ export const D: ComponentStoryObj<typeof Cat> = {
31
+ args: {
32
+ name: 'Fluffy',
33
+ },
34
+ };
35
+ export const E: Story<CatProps> = (args) => <Cat {...args} />;
36
+ `)
37
+ ).toMatchInlineSnapshot(`
38
+ import { StoryFn, Meta, StoryObj } from '@storybook/react';
39
+ import { Cat, CatProps } from './Cat';
40
+
41
+ const meta = { title: 'Cat', component: Cat } satisfies Meta<typeof Cat>;
42
+ const meta2: Meta<CatProps> = { title: 'Cat', component: Cat };
43
+ export default meta;
44
+
45
+ export const A: StoryFn<typeof Cat> = (args) => <Cat {...args} />;
46
+ export const B: any = (args) => <Button {...args} />;
47
+ export const C: StoryFn<typeof Cat> = (args) => <Cat {...args} />;
48
+ export const D: StoryObj<typeof Cat> = {
49
+ args: {
50
+ name: 'Fluffy',
51
+ },
52
+ };
53
+ export const E: StoryFn<CatProps> = (args) => <Cat {...args} />;
54
+ `);
55
+ });
56
+
57
+ it('upgrade imports with local names', () => {
58
+ expect(
59
+ tsTransform(dedent`
60
+ import { Story as Story_, ComponentMeta as ComponentMeta_, ComponentStory as Story__, ComponentStoryObj as ComponentStoryObj_, ComponentStoryFn as StoryFn_ } from '@storybook/react';
61
+ import { Cat } from './Cat';
62
+
63
+ const meta = { title: 'Cat', component: Cat } satisfies ComponentMeta_<typeof Cat>
64
+ const meta2: ComponentMeta_<typeof Cat> = { title: 'Cat', component: Cat };
65
+ export default meta;
66
+
67
+ export const A: Story__<typeof Cat> = (args) => <Cat {...args} />;
68
+ export const B: any = (args) => <Button {...args} />;
69
+ export const C: StoryFn_<typeof Cat> = (args) => <Cat {...args} />;
70
+ export const D: ComponentStoryObj_<typeof Cat> = {
71
+ args: {
72
+ name: 'Fluffy',
73
+ },
74
+ };
75
+ export const E: Story_<CatProps> = (args) => <Cat {...args} />;
76
+ `)
77
+ ).toMatchInlineSnapshot(`
78
+ import {
79
+ StoryFn as Story_,
80
+ Meta as ComponentMeta_,
81
+ StoryObj as ComponentStoryObj_,
82
+ } from '@storybook/react';
83
+ import { Cat } from './Cat';
84
+
85
+ const meta = { title: 'Cat', component: Cat } satisfies ComponentMeta_<typeof Cat>;
86
+ const meta2: ComponentMeta_<typeof Cat> = { title: 'Cat', component: Cat };
87
+ export default meta;
88
+
89
+ export const A: Story__<typeof Cat> = (args) => <Cat {...args} />;
90
+ export const B: any = (args) => <Button {...args} />;
91
+ export const C: StoryFn_<typeof Cat> = (args) => <Cat {...args} />;
92
+ export const D: ComponentStoryObj_<typeof Cat> = {
93
+ args: {
94
+ name: 'Fluffy',
95
+ },
96
+ };
97
+ export const E: Story_<CatProps> = (args) => <Cat {...args} />;
98
+ `);
99
+ });
100
+
101
+ it('upgrade imports with conflicting local names', () => {
102
+ expect.addSnapshotSerializer({
103
+ serialize: (value) => value.replace(ansiRegex(), ''),
104
+ test: () => true,
105
+ });
106
+
107
+ expect(() =>
108
+ tsTransform(dedent`
109
+ import { ComponentMeta as Meta, ComponentStory as StoryFn } from '@storybook/react';
110
+ import { Cat } from './Cat';
111
+
112
+ const meta = { title: 'Cat', component: Cat } satisfies Meta<typeof Cat>
113
+ export default meta;
114
+
115
+ export const A: StoryFn<typeof Cat> = (args) => <Cat {...args} />;
116
+
117
+ `)
118
+ ).toThrowErrorMatchingInlineSnapshot(`
119
+ This codemod does not support local imports that are called the same as a storybook import.
120
+ Rename this local import and try again.
121
+ > 1 | import { ComponentMeta as Meta, ComponentStory as StoryFn } from '@storybook/react';
122
+ | ^^^^^^^^^^^^^^^^^^^^^
123
+ 2 | import { Cat } from './Cat';
124
+ 3 |
125
+ 4 | const meta = { title: 'Cat', component: Cat } satisfies Meta<typeof Cat>
126
+ `);
127
+ });
128
+
129
+ it('upgrade namespaces', () => {
130
+ expect(
131
+ tsTransform(dedent`
132
+ import * as SB from '@storybook/react';
133
+ import { Cat, CatProps } from './Cat';
134
+
135
+ const meta = { title: 'Cat', component: Cat } satisfies SB.ComponentMeta<typeof Cat>;
136
+ const meta2: SB.ComponentMeta<typeof Cat> = { title: 'Cat', component: Cat };
137
+ export default meta;
138
+
139
+ export const A: SB.ComponentStory<typeof Cat> = (args) => <Cat {...args} />;
140
+ export const B: any = (args) => <Button {...args} />;
141
+ export const C: SB.ComponentStoryFn<typeof Cat> = (args) => <Cat {...args} />;
142
+ export const D: SB.ComponentStoryObj<typeof Cat> = {
143
+ args: {
144
+ name: 'Fluffy',
145
+ },
146
+ };
147
+ export const E: SB.Story<CatProps> = (args) => <Cat {...args} />;
148
+
149
+ `)
150
+ ).toMatchInlineSnapshot(`
151
+ import * as SB from '@storybook/react';
152
+ import { Cat, CatProps } from './Cat';
153
+
154
+ const meta = { title: 'Cat', component: Cat } satisfies SB.Meta<typeof Cat>;
155
+ const meta2: SB.Meta<typeof Cat> = { title: 'Cat', component: Cat };
156
+ export default meta;
157
+
158
+ export const A: SB.StoryFn<typeof Cat> = (args) => <Cat {...args} />;
159
+ export const B: any = (args) => <Button {...args} />;
160
+ export const C: SB.StoryFn<typeof Cat> = (args) => <Cat {...args} />;
161
+ export const D: SB.StoryObj<typeof Cat> = {
162
+ args: {
163
+ name: 'Fluffy',
164
+ },
165
+ };
166
+ export const E: SB.StoryFn<CatProps> = (args) => <Cat {...args} />;
167
+ `);
168
+ });
169
+ });
170
+ });
@@ -1,9 +1,14 @@
1
1
  /* eslint-disable no-underscore-dangle */
2
2
  import prettier from 'prettier';
3
3
  import * as t from '@babel/types';
4
+ import { isIdentifier, isTSTypeAnnotation, isTSTypeReference } from '@babel/types';
4
5
  import type { CsfFile } from '@storybook/csf-tools';
5
- import { formatCsf, loadCsf } from '@storybook/csf-tools';
6
- import { jscodeshiftToPrettierParser } from '../lib/utils';
6
+ import { loadCsf } from '@storybook/csf-tools';
7
+ import type { API, FileInfo } from 'jscodeshift';
8
+ import type { BabelFile, NodePath } from '@babel/core';
9
+ import * as babel from '@babel/core';
10
+ import * as recast from 'recast';
11
+ import { upgradeDeprecatedTypes } from './upgrade-deprecated-types';
7
12
 
8
13
  const logger = console;
9
14
 
@@ -89,19 +94,28 @@ const isReactGlobalRenderFn = (csf: CsfFile, storyFn: t.Expression) => {
89
94
  const isSimpleCSFStory = (init: t.Expression, annotations: t.ObjectProperty[]) =>
90
95
  annotations.length === 0 && t.isArrowFunctionExpression(init) && init.params.length === 0;
91
96
 
92
- function transform({ source }: { source: string }, api: any, options: { parser?: string }) {
97
+ export default function transform(info: FileInfo, api: API, options: { parser?: string }) {
93
98
  const makeTitle = (userTitle?: string) => {
94
99
  return userTitle || 'FIXME';
95
100
  };
96
- const csf = loadCsf(source, { makeTitle });
101
+ const csf = loadCsf(info.source, { makeTitle });
97
102
 
98
103
  try {
99
104
  csf.parse();
100
105
  } catch (err) {
101
106
  logger.log(`Error ${err}, skipping`);
102
- return source;
107
+ return info.source;
103
108
  }
104
109
 
110
+ // This allows for showing buildCodeFrameError messages
111
+ // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606
112
+ const file: BabelFile = new babel.File(
113
+ { filename: info.path },
114
+ { code: info.source, ast: csf._ast }
115
+ );
116
+
117
+ const importHelper = new StorybookImportHelper(file, info);
118
+
105
119
  const objectExports: Record<string, t.Statement> = {};
106
120
  Object.entries(csf._storyExports).forEach(([key, decl]) => {
107
121
  const annotations = Object.entries(csf._storyAnnotations[key]).map(([annotation, val]) => {
@@ -111,74 +125,196 @@ function transform({ source }: { source: string }, api: any, options: { parser?:
111
125
  if (t.isVariableDeclarator(decl)) {
112
126
  const { init, id } = decl;
113
127
  // only replace arrow function expressions && template
114
- // ignore no-arg stories without annotations
115
128
  const template = getTemplateBindVariable(init);
116
- if (
117
- (!t.isArrowFunctionExpression(init) && !template) ||
118
- isSimpleCSFStory(init, annotations)
119
- ) {
129
+ if (!t.isArrowFunctionExpression(init) && !template) return;
130
+ // Do change the type of no-arg stories without annotations to StoryFn when applicable
131
+ if (isSimpleCSFStory(init, annotations)) {
132
+ objectExports[key] = t.exportNamedDeclaration(
133
+ t.variableDeclaration('const', [
134
+ t.variableDeclarator(importHelper.updateTypeTo(id, 'StoryFn'), init),
135
+ ])
136
+ );
120
137
  return;
121
138
  }
122
139
 
123
140
  // Remove the render function when we can hoist the template
124
141
  // const Template = (args) => <Cat {...args} />;
125
142
  // export const A = Template.bind({});
126
- let storyFn = template && csf._templates[template];
143
+ let storyFn: t.Expression = template && (csf._templates[template] as any as t.Expression);
127
144
  if (!storyFn) {
128
145
  storyFn = init;
129
146
  }
130
147
 
131
- const keyId = t.identifier(key);
132
- // @ts-expect-error (Converted from ts-ignore)
133
- const { typeAnnotation } = id;
134
- if (typeAnnotation) {
135
- keyId.typeAnnotation = typeAnnotation;
136
- }
137
-
138
148
  const renderAnnotation = isReactGlobalRenderFn(csf, storyFn)
139
149
  ? []
140
150
  : [t.objectProperty(t.identifier('render'), storyFn)];
141
151
 
142
152
  objectExports[key] = t.exportNamedDeclaration(
143
153
  t.variableDeclaration('const', [
144
- t.variableDeclarator(keyId, t.objectExpression([...renderAnnotation, ...annotations])),
154
+ t.variableDeclarator(
155
+ importHelper.updateTypeTo(id, 'StoryObj'),
156
+ t.objectExpression([...renderAnnotation, ...annotations])
157
+ ),
145
158
  ])
146
159
  );
147
160
  }
148
161
  });
149
162
 
150
- const updatedBody = csf._ast.program.body.reduce((acc, stmt) => {
163
+ importHelper.removeDeprecatedStoryImport();
164
+
165
+ csf._ast.program.body = csf._ast.program.body.reduce((acc, stmt) => {
166
+ const statement = stmt as t.Statement;
151
167
  // remove story annotations & template declarations
152
- if (isStoryAnnotation(stmt, objectExports) || isTemplateDeclaration(stmt, csf._templates)) {
168
+ if (
169
+ isStoryAnnotation(statement, objectExports) ||
170
+ isTemplateDeclaration(statement, csf._templates)
171
+ ) {
153
172
  return acc;
154
173
  }
155
174
 
156
175
  // replace story exports with new object exports
157
- const newExport = getNewExport(stmt, objectExports);
176
+ const newExport = getNewExport(statement, objectExports);
158
177
  if (newExport) {
159
178
  acc.push(newExport);
160
179
  return acc;
161
180
  }
162
181
 
163
182
  // include unknown statements
164
- acc.push(stmt);
183
+ acc.push(statement);
165
184
  return acc;
166
185
  }, []);
167
- csf._ast.program.body = updatedBody;
168
- const output = formatCsf(csf);
169
-
170
- const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
171
- printWidth: 100,
172
- tabWidth: 2,
173
- bracketSpacing: true,
174
- trailingComma: 'es5',
175
- singleQuote: true,
186
+
187
+ upgradeDeprecatedTypes(file);
188
+
189
+ let output = recast.print(csf._ast, {}).code;
190
+
191
+ try {
192
+ const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
193
+ printWidth: 100,
194
+ tabWidth: 2,
195
+ bracketSpacing: true,
196
+ trailingComma: 'es5',
197
+ singleQuote: true,
198
+ };
199
+
200
+ output = prettier.format(output, {
201
+ ...prettierConfig,
202
+ // This will infer the parser from the filename.
203
+ filepath: info.path,
204
+ });
205
+ } catch (e) {
206
+ logger.log(`Failed applying prettier to ${info.path}.`);
207
+ }
208
+
209
+ return output;
210
+ }
211
+
212
+ class StorybookImportHelper {
213
+ constructor(file: BabelFile, info: FileInfo) {
214
+ this.sbImportDeclarations = this.getAllSbImportDeclarations(file);
215
+ }
216
+
217
+ private sbImportDeclarations: NodePath<t.ImportDeclaration>[];
218
+
219
+ private getAllSbImportDeclarations = (file: BabelFile) => {
220
+ const found: NodePath<t.ImportDeclaration>[] = [];
221
+
222
+ file.path.traverse({
223
+ ImportDeclaration: (path) => {
224
+ const source = path.node.source.value;
225
+ if (source.startsWith('@storybook/csf') || !source.startsWith('@storybook')) return;
226
+ const isRendererImport = path.get('specifiers').some((specifier) => {
227
+ if (specifier.isImportNamespaceSpecifier()) {
228
+ // throw path.buildCodeFrameError(
229
+ // `This codemod does not support namespace imports for a ${path.node.source.value} package.\n` +
230
+ // 'Replace the namespace import with named imports and try again.'
231
+ // );
232
+ throw new Error(
233
+ `This codemod does not support namespace imports for a ${path.node.source.value} package.\n` +
234
+ 'Replace the namespace import with named imports and try again.'
235
+ );
236
+ }
237
+ if (!specifier.isImportSpecifier()) return false;
238
+ const imported = specifier.get('imported');
239
+ if (!imported.isIdentifier()) return false;
240
+
241
+ return [
242
+ 'Story',
243
+ 'StoryFn',
244
+ 'StoryObj',
245
+ 'Meta',
246
+ 'ComponentStory',
247
+ 'ComponentStoryFn',
248
+ 'ComponentStoryObj',
249
+ 'ComponentMeta',
250
+ ].includes(imported.node.name);
251
+ });
252
+
253
+ if (isRendererImport) found.push(path);
254
+ },
255
+ });
256
+ return found;
176
257
  };
177
258
 
178
- return prettier.format(output, {
179
- ...prettierConfig,
180
- parser: jscodeshiftToPrettierParser(options?.parser),
181
- });
259
+ getOrAddImport = (type: string): string | undefined => {
260
+ // prefer type import
261
+ const sbImport =
262
+ this.sbImportDeclarations.find((path) => path.node.importKind === 'type') ??
263
+ this.sbImportDeclarations[0];
264
+ if (sbImport == null) return undefined;
265
+
266
+ const specifiers = sbImport.get('specifiers');
267
+ const importSpecifier = specifiers.find((specifier) => {
268
+ if (!specifier.isImportSpecifier()) return false;
269
+ const imported = specifier.get('imported');
270
+ if (!imported.isIdentifier()) return false;
271
+ return imported.node.name === type;
272
+ });
273
+ if (importSpecifier) return importSpecifier.node.local.name;
274
+ specifiers[0].insertBefore(t.importSpecifier(t.identifier(type), t.identifier(type)));
275
+ return type;
276
+ };
277
+
278
+ removeDeprecatedStoryImport = () => {
279
+ const specifiers = this.sbImportDeclarations.flatMap((it) => it.get('specifiers'));
280
+ const storyImports = specifiers.filter((specifier) => {
281
+ if (!specifier.isImportSpecifier()) return false;
282
+ const imported = specifier.get('imported');
283
+ if (!imported.isIdentifier()) return false;
284
+ return imported.node.name === 'Story';
285
+ });
286
+ storyImports.forEach((path) => path.remove());
287
+ };
288
+
289
+ getAllLocalImports = () => {
290
+ return this.sbImportDeclarations
291
+ .flatMap((it) => it.get('specifiers'))
292
+ .map((it) => it.node.local.name);
293
+ };
294
+
295
+ updateTypeTo = (id: t.LVal, type: string): t.LVal => {
296
+ if (
297
+ isIdentifier(id) &&
298
+ isTSTypeAnnotation(id.typeAnnotation) &&
299
+ isTSTypeReference(id.typeAnnotation.typeAnnotation) &&
300
+ isIdentifier(id.typeAnnotation.typeAnnotation.typeName)
301
+ ) {
302
+ const { name } = id.typeAnnotation.typeAnnotation.typeName;
303
+ if (this.getAllLocalImports().includes(name)) {
304
+ const localTypeImport = this.getOrAddImport(type);
305
+ return {
306
+ ...id,
307
+ typeAnnotation: t.tsTypeAnnotation(
308
+ t.tsTypeReference(
309
+ t.identifier(localTypeImport),
310
+ id.typeAnnotation.typeAnnotation.typeParameters
311
+ )
312
+ ),
313
+ };
314
+ }
315
+ }
316
+ return id;
317
+ };
182
318
  }
183
319
 
184
- export default transform;
320
+ export const parser = 'tsx';