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

Sign up to get free protection for your applications and to get access to all the features.
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';