@storybook/codemod 7.0.0-beta.27 → 7.0.0-beta.29

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 (33) hide show
  1. package/README.md +0 -39
  2. package/dist/chunk-YH46OF24.mjs +2 -0
  3. package/dist/index.mjs +1 -1
  4. package/dist/transforms/csf-2-to-3.d.ts +4 -2
  5. package/dist/transforms/csf-2-to-3.js +3 -1
  6. package/dist/transforms/csf-2-to-3.mjs +2 -1
  7. package/dist/transforms/upgrade-deprecated-types.d.ts +10 -0
  8. package/dist/transforms/upgrade-deprecated-types.js +2 -0
  9. package/dist/transforms/upgrade-deprecated-types.mjs +1 -0
  10. package/jest.config.js +1 -0
  11. package/package.json +10 -9
  12. package/src/transforms/__tests__/csf-2-to-3.test.ts +174 -58
  13. package/src/transforms/__tests__/upgrade-deprecated-types.test.ts +170 -0
  14. package/src/transforms/csf-2-to-3.ts +144 -23
  15. package/src/transforms/upgrade-deprecated-types.ts +142 -0
  16. package/dist/transforms/csf-to-mdx.d.ts +0 -29
  17. package/dist/transforms/csf-to-mdx.js +0 -3
  18. package/dist/transforms/csf-to-mdx.mjs +0 -3
  19. package/src/transforms/__testfixtures__/csf-to-mdx/basic.input.js +0 -20
  20. package/src/transforms/__testfixtures__/csf-to-mdx/basic.output.snapshot +0 -18
  21. package/src/transforms/__testfixtures__/csf-to-mdx/component-id.input.js +0 -9
  22. package/src/transforms/__testfixtures__/csf-to-mdx/component-id.output.snapshot +0 -10
  23. package/src/transforms/__testfixtures__/csf-to-mdx/decorators.input.js +0 -13
  24. package/src/transforms/__testfixtures__/csf-to-mdx/decorators.output.snapshot +0 -12
  25. package/src/transforms/__testfixtures__/csf-to-mdx/exclude-stories.input.js +0 -23
  26. package/src/transforms/__testfixtures__/csf-to-mdx/exclude-stories.output.snapshot +0 -22
  27. package/src/transforms/__testfixtures__/csf-to-mdx/parameters.input.js +0 -16
  28. package/src/transforms/__testfixtures__/csf-to-mdx/parameters.output.snapshot +0 -17
  29. package/src/transforms/__testfixtures__/csf-to-mdx/story-function.input.js +0 -19
  30. package/src/transforms/__testfixtures__/csf-to-mdx/story-function.output.snapshot +0 -18
  31. package/src/transforms/__testfixtures__/csf-to-mdx/story-parameters.input.js +0 -24
  32. package/src/transforms/__testfixtures__/csf-to-mdx/story-parameters.output.snapshot +0 -22
  33. 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 type { API, FileInfo, Options } from 'jscodeshift';
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
- export default function transform({ source, path }: FileInfo, api: API, options: Options) {
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,12 +125,15 @@ export default function transform({ source, path }: FileInfo, api: API, options:
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
 
@@ -128,26 +145,24 @@ export default function transform({ source, path }: FileInfo, api: API, options:
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) => {
151
166
  // remove story annotations & template declarations
152
167
  if (isStoryAnnotation(stmt, objectExports) || isTemplateDeclaration(stmt, csf._templates)) {
153
168
  return acc;
@@ -164,8 +179,10 @@ export default function transform({ source, path }: FileInfo, api: API, options:
164
179
  acc.push(stmt);
165
180
  return acc;
166
181
  }, []);
167
- csf._ast.program.body = updatedBody;
168
- let output = formatCsf(csf);
182
+
183
+ upgradeDeprecatedTypes(file);
184
+
185
+ let output = recast.print(csf._ast, {}).code;
169
186
 
170
187
  try {
171
188
  const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
@@ -179,13 +196,117 @@ export default function transform({ source, path }: FileInfo, api: API, options:
179
196
  output = prettier.format(output, {
180
197
  ...prettierConfig,
181
198
  // This will infer the parser from the filename.
182
- filepath: path,
199
+ filepath: info.path,
183
200
  });
184
201
  } catch (e) {
185
- logger.log(`Failed applying prettier to ${path}.`);
202
+ logger.log(`Failed applying prettier to ${info.path}.`);
186
203
  }
187
204
 
188
205
  return output;
189
206
  }
190
207
 
208
+ class StorybookImportHelper {
209
+ constructor(file: BabelFile, info: FileInfo) {
210
+ this.sbImportDeclarations = this.getAllSbImportDeclarations(file);
211
+ }
212
+
213
+ private sbImportDeclarations: NodePath<t.ImportDeclaration>[];
214
+
215
+ private getAllSbImportDeclarations = (file: BabelFile) => {
216
+ const found: NodePath<t.ImportDeclaration>[] = [];
217
+
218
+ file.path.traverse({
219
+ ImportDeclaration: (path) => {
220
+ const source = path.node.source.value;
221
+ if (source.startsWith('@storybook/csf') || !source.startsWith('@storybook')) return;
222
+ const isRendererImport = path.get('specifiers').some((specifier) => {
223
+ if (specifier.isImportNamespaceSpecifier()) {
224
+ throw path.buildCodeFrameError(
225
+ `This codemod does not support namespace imports for a ${path.node.source.value} package.\n` +
226
+ 'Replace the namespace import with named imports and try again.'
227
+ );
228
+ }
229
+ if (!specifier.isImportSpecifier()) return false;
230
+ const imported = specifier.get('imported');
231
+ if (!imported.isIdentifier()) return false;
232
+
233
+ return [
234
+ 'Story',
235
+ 'StoryFn',
236
+ 'StoryObj',
237
+ 'Meta',
238
+ 'ComponentStory',
239
+ 'ComponentStoryFn',
240
+ 'ComponentStoryObj',
241
+ 'ComponentMeta',
242
+ ].includes(imported.node.name);
243
+ });
244
+
245
+ if (isRendererImport) found.push(path);
246
+ },
247
+ });
248
+ return found;
249
+ };
250
+
251
+ getOrAddImport = (type: string): string | undefined => {
252
+ // prefer type import
253
+ const sbImport =
254
+ this.sbImportDeclarations.find((path) => path.node.importKind === 'type') ??
255
+ this.sbImportDeclarations[0];
256
+ if (sbImport == null) return undefined;
257
+
258
+ const specifiers = sbImport.get('specifiers');
259
+ const importSpecifier = specifiers.find((specifier) => {
260
+ if (!specifier.isImportSpecifier()) return false;
261
+ const imported = specifier.get('imported');
262
+ if (!imported.isIdentifier()) return false;
263
+ return imported.node.name === type;
264
+ });
265
+ if (importSpecifier) return importSpecifier.node.local.name;
266
+ specifiers[0].insertBefore(t.importSpecifier(t.identifier(type), t.identifier(type)));
267
+ return type;
268
+ };
269
+
270
+ removeDeprecatedStoryImport = () => {
271
+ const specifiers = this.sbImportDeclarations.flatMap((it) => it.get('specifiers'));
272
+ const storyImports = specifiers.filter((specifier) => {
273
+ if (!specifier.isImportSpecifier()) return false;
274
+ const imported = specifier.get('imported');
275
+ if (!imported.isIdentifier()) return false;
276
+ return imported.node.name === 'Story';
277
+ });
278
+ storyImports.forEach((path) => path.remove());
279
+ };
280
+
281
+ getAllLocalImports = () => {
282
+ return this.sbImportDeclarations
283
+ .flatMap((it) => it.get('specifiers'))
284
+ .map((it) => it.node.local.name);
285
+ };
286
+
287
+ updateTypeTo = (id: t.LVal, type: string): t.LVal => {
288
+ if (
289
+ isIdentifier(id) &&
290
+ isTSTypeAnnotation(id.typeAnnotation) &&
291
+ isTSTypeReference(id.typeAnnotation.typeAnnotation) &&
292
+ isIdentifier(id.typeAnnotation.typeAnnotation.typeName)
293
+ ) {
294
+ const { name } = id.typeAnnotation.typeAnnotation.typeName;
295
+ if (this.getAllLocalImports().includes(name)) {
296
+ const localTypeImport = this.getOrAddImport(type);
297
+ return {
298
+ ...id,
299
+ typeAnnotation: t.tsTypeAnnotation(
300
+ t.tsTypeReference(
301
+ t.identifier(localTypeImport),
302
+ id.typeAnnotation.typeAnnotation.typeParameters
303
+ )
304
+ ),
305
+ };
306
+ }
307
+ }
308
+ return id;
309
+ };
310
+ }
311
+
191
312
  export const parser = 'tsx';
@@ -0,0 +1,142 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import prettier from 'prettier';
3
+ import type { API, FileInfo } from 'jscodeshift';
4
+ import type { BabelFile, NodePath } from '@babel/core';
5
+ import * as babel from '@babel/core';
6
+ import { loadCsf } from '@storybook/csf-tools';
7
+ import * as recast from 'recast';
8
+ import * as t from '@babel/types';
9
+
10
+ const logger = console;
11
+
12
+ const deprecatedTypes = [
13
+ 'ComponentStory',
14
+ 'ComponentStoryFn',
15
+ 'ComponentStoryObj',
16
+ 'ComponentMeta',
17
+ 'Story',
18
+ ];
19
+
20
+ function migrateType(oldType: string) {
21
+ if (oldType === 'Story' || oldType === 'ComponentStory') return 'StoryFn';
22
+ return oldType.replace('Component', '');
23
+ }
24
+
25
+ export default function transform(info: FileInfo, api: API, options: { parser?: string }) {
26
+ // TODO what do I need to with the title?
27
+ const fileNode = loadCsf(info.source, { makeTitle: (title) => title })._ast;
28
+ // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606
29
+ const file: BabelFile = new babel.File(
30
+ { filename: info.path },
31
+ { code: info.source, ast: fileNode }
32
+ );
33
+
34
+ upgradeDeprecatedTypes(file);
35
+
36
+ let output = recast.print(file.path.node).code;
37
+
38
+ try {
39
+ const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
40
+ printWidth: 100,
41
+ tabWidth: 2,
42
+ bracketSpacing: true,
43
+ trailingComma: 'es5',
44
+ singleQuote: true,
45
+ };
46
+
47
+ output = prettier.format(output, { ...prettierConfig, filepath: info.path });
48
+ } catch (e) {
49
+ logger.log(`Failed applying prettier to ${info.path}.`);
50
+ }
51
+
52
+ return output;
53
+ }
54
+
55
+ export const parser = 'tsx';
56
+
57
+ export function upgradeDeprecatedTypes(file: BabelFile) {
58
+ const importedNamespaces: Set<string> = new Set();
59
+ const typeReferencesToUpdate: Set<string> = new Set();
60
+ const existingImports: { name: string; isAlias: boolean; path: NodePath }[] = [];
61
+
62
+ file.path.traverse({
63
+ ImportDeclaration: (path) => {
64
+ existingImports.push(
65
+ ...path.get('specifiers').map((specifier) => ({
66
+ name: specifier.node.local.name,
67
+ isAlias: !(
68
+ specifier.isImportSpecifier() &&
69
+ t.isIdentifier(specifier.node.imported) &&
70
+ specifier.node.local.name === specifier.node.imported.name
71
+ ),
72
+ path: specifier,
73
+ }))
74
+ );
75
+
76
+ const source = path.node.source.value;
77
+ if (!source.startsWith('@storybook')) return;
78
+
79
+ path.get('specifiers').forEach((specifier) => {
80
+ if (specifier.isImportNamespaceSpecifier()) {
81
+ importedNamespaces.add(specifier.node.local.name);
82
+ }
83
+ if (!specifier.isImportSpecifier()) return;
84
+ const imported = specifier.get('imported');
85
+ if (!imported.isIdentifier()) return;
86
+
87
+ // if we find a deprecated import
88
+ if (deprecatedTypes.includes(imported.node.name)) {
89
+ // we don't have to rewrite type references for aliased imports
90
+ if (imported.node.name === specifier.node.local.name) {
91
+ typeReferencesToUpdate.add(specifier.node.local.name);
92
+ }
93
+
94
+ const newType = migrateType(imported.node.name);
95
+
96
+ // replace the deprecated import type when the new type isn't yet imported
97
+ // note that we don't replace the local name of the specifier
98
+ if (!existingImports.some((it) => it.name === newType)) {
99
+ imported.replaceWith(t.identifier(newType));
100
+ existingImports.push({ name: newType, isAlias: false, path: specifier });
101
+ } else {
102
+ // if the existing import has the same local name but is an alias we throw
103
+ // we could have imported the type with an alias, but seems to much effort
104
+ const existingImport = existingImports.find((it) => it.name === newType && it.isAlias);
105
+ if (existingImport) {
106
+ throw existingImport.path.buildCodeFrameError(
107
+ 'This codemod does not support local imports that are called the same as a storybook import.\n' +
108
+ 'Rename this local import and try again.'
109
+ );
110
+ } else {
111
+ // if the type already exists, without being aliased
112
+ // we can safely remove the deprecated import now
113
+ specifier.remove();
114
+ }
115
+ }
116
+ }
117
+ });
118
+ },
119
+ });
120
+
121
+ file.path.traverse({
122
+ TSTypeReference: (path) => {
123
+ const typeName = path.get('typeName');
124
+ if (typeName.isIdentifier()) {
125
+ if (typeReferencesToUpdate.has(typeName.node.name)) {
126
+ typeName.replaceWith(t.identifier(migrateType(typeName.node.name)));
127
+ }
128
+ } else if (typeName.isTSQualifiedName()) {
129
+ // For example SB.StoryObj
130
+ const namespace = typeName.get('left');
131
+ if (namespace.isIdentifier()) {
132
+ if (importedNamespaces.has(namespace.node.name)) {
133
+ const right = typeName.get('right');
134
+ if (deprecatedTypes.includes(right.node.name)) {
135
+ right.replaceWith(t.identifier(migrateType(right.node.name)));
136
+ }
137
+ }
138
+ }
139
+ }
140
+ },
141
+ });
142
+ }
@@ -1,29 +0,0 @@
1
- /**
2
- * Convert a component's module story file into an MDX file
3
- *
4
- * For example:
5
- *
6
- * ```
7
- * input { Button } from './Button';
8
- * export default {
9
- * title: 'Button'
10
- * }
11
- * export const story = () => <Button label="The Button" />;
12
- * ```
13
- *
14
- * Becomes:
15
- *
16
- * ```
17
- * import { Meta, Story } from '@storybook/addon-docs';
18
- * input { Button } from './Button';
19
- *
20
- * <Meta title='Button' />
21
- *
22
- * <Story name='story'>
23
- * <Button label="The Button" />
24
- * </Story>
25
- * ```
26
- */
27
- declare function transformer(file: any, api: any): any;
28
-
29
- export { transformer as default };
@@ -1,3 +0,0 @@
1
- var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var csf_to_mdx_exports={};__export(csf_to_mdx_exports,{default:()=>transformer});module.exports=__toCommonJS(csf_to_mdx_exports);var import_recast=require("recast"),import_csf=require("@storybook/csf");function exportMdx(root,options){return root.__paths[0].node.program.body.map(n=>{let{code}=(0,import_recast.prettyPrint)(n,options);return n.type==="JSXElement"?`${code}
2
- `:code}).join(`
3
- `)}function parseIncludeExclude(prop){let{code}=(0,import_recast.prettyPrint)(prop,{});return(0,eval)(code)}function transformer(file,api){let j=api.jscodeshift,root=j(file.source),storyKeyToStory={},meta={};function makeAttr(key,val){return j.jsxAttribute(j.jsxIdentifier(key),val.type==="Literal"?val:j.jsxExpressionContainer(val))}function getStoryContents(node){return node.type==="ArrowFunctionExpression"&&node.body.type==="JSXElement"?node.body:j.jsxExpressionContainer(node)}function getName(storyKey){let story=storyKeyToStory[storyKey];if(story){let name=story.properties.find(prop=>prop.key.name==="name");if(name&&name.value.type==="Literal")return name.value.value}return storyKey}function getStoryAttrs(storyKey){let attrs=[],story=storyKeyToStory[storyKey];return story&&story.properties.forEach(prop=>{let{key,value}=prop;key.name!=="name"&&attrs.push(makeAttr(key.name,value))}),attrs}let defaultExportWithTitle=root.find(j.ExportDefaultDeclaration).filter(def=>def.node.declaration.properties.map(p=>p.key.name).includes("title"));if(defaultExportWithTitle.size()===0)return root.toSource();root.find(j.ImportDeclaration).at(-1).insertAfter(j.emptyStatement()).insertAfter(j.importDeclaration([j.importSpecifier(j.identifier("Meta")),j.importSpecifier(j.identifier("Story"))],j.literal("@storybook/addon-docs"))),root.find(j.ImportDeclaration).filter(decl=>decl.node.source.value==="react").remove(),defaultExportWithTitle.forEach(exp=>{exp.node.declaration.properties.forEach(p=>{["includeStories","excludeStories"].includes(p.key.name)&&(meta[p.key.name]=parseIncludeExclude(p.value))})});let namedExports=root.find(j.ExportNamedDeclaration);namedExports.forEach(exp=>{let storyKey=exp.node.declaration.declarations[0].id.name;(0,import_csf.isExportStory)(storyKey,meta)&&(storyKeyToStory[storyKey]=null)});let storyAssignments=root.find(j.AssignmentExpression).filter(exp=>{let{left}=exp.node;return left.type==="MemberExpression"&&left.object.type==="Identifier"&&left.object.name in storyKeyToStory&&left.property.type==="Identifier"&&left.property.name==="story"});return storyAssignments.forEach(exp=>{let{left,right}=exp.node;storyKeyToStory[left.object.name]=right}),storyAssignments.remove(),defaultExportWithTitle.replaceWith(exp=>{let jsxId=j.jsxIdentifier("Meta"),attrs=[];exp.node.declaration.properties.forEach(prop=>{let{key,value}=prop;["includeStories","excludeStories"].includes(key.name)||attrs.push(makeAttr(key.name,value))});let opening=j.jsxOpeningElement(jsxId,attrs);return opening.selfClosing=!0,j.jsxElement(opening)}),namedExports.replaceWith(exp=>{let storyKey=exp.node.declaration.declarations[0].id.name;if(!(0,import_csf.isExportStory)(storyKey,meta))return exp.node;let jsxId=j.jsxIdentifier("Story"),name=getName(storyKey),attributes=[makeAttr("name",j.literal(name)),...getStoryAttrs(storyKey)],opening=j.jsxOpeningElement(jsxId,attributes),closing=j.jsxClosingElement(jsxId),children=[getStoryContents(exp.node.declaration.declarations[0].init)];return j.jsxElement(opening,closing,children)}),exportMdx(root,{quote:"single",trailingComma:"true",tabWidth:2})}0&&(module.exports={});
@@ -1,3 +0,0 @@
1
- import{prettyPrint}from"recast";import{isExportStory}from"@storybook/csf";function exportMdx(root,options){return root.__paths[0].node.program.body.map(n=>{let{code}=prettyPrint(n,options);return n.type==="JSXElement"?`${code}
2
- `:code}).join(`
3
- `)}function parseIncludeExclude(prop){let{code}=prettyPrint(prop,{});return(0,eval)(code)}function transformer(file,api){let j=api.jscodeshift,root=j(file.source),storyKeyToStory={},meta={};function makeAttr(key,val){return j.jsxAttribute(j.jsxIdentifier(key),val.type==="Literal"?val:j.jsxExpressionContainer(val))}function getStoryContents(node){return node.type==="ArrowFunctionExpression"&&node.body.type==="JSXElement"?node.body:j.jsxExpressionContainer(node)}function getName(storyKey){let story=storyKeyToStory[storyKey];if(story){let name=story.properties.find(prop=>prop.key.name==="name");if(name&&name.value.type==="Literal")return name.value.value}return storyKey}function getStoryAttrs(storyKey){let attrs=[],story=storyKeyToStory[storyKey];return story&&story.properties.forEach(prop=>{let{key,value}=prop;key.name!=="name"&&attrs.push(makeAttr(key.name,value))}),attrs}let defaultExportWithTitle=root.find(j.ExportDefaultDeclaration).filter(def=>def.node.declaration.properties.map(p=>p.key.name).includes("title"));if(defaultExportWithTitle.size()===0)return root.toSource();root.find(j.ImportDeclaration).at(-1).insertAfter(j.emptyStatement()).insertAfter(j.importDeclaration([j.importSpecifier(j.identifier("Meta")),j.importSpecifier(j.identifier("Story"))],j.literal("@storybook/addon-docs"))),root.find(j.ImportDeclaration).filter(decl=>decl.node.source.value==="react").remove(),defaultExportWithTitle.forEach(exp=>{exp.node.declaration.properties.forEach(p=>{["includeStories","excludeStories"].includes(p.key.name)&&(meta[p.key.name]=parseIncludeExclude(p.value))})});let namedExports=root.find(j.ExportNamedDeclaration);namedExports.forEach(exp=>{let storyKey=exp.node.declaration.declarations[0].id.name;isExportStory(storyKey,meta)&&(storyKeyToStory[storyKey]=null)});let storyAssignments=root.find(j.AssignmentExpression).filter(exp=>{let{left}=exp.node;return left.type==="MemberExpression"&&left.object.type==="Identifier"&&left.object.name in storyKeyToStory&&left.property.type==="Identifier"&&left.property.name==="story"});return storyAssignments.forEach(exp=>{let{left,right}=exp.node;storyKeyToStory[left.object.name]=right}),storyAssignments.remove(),defaultExportWithTitle.replaceWith(exp=>{let jsxId=j.jsxIdentifier("Meta"),attrs=[];exp.node.declaration.properties.forEach(prop=>{let{key,value}=prop;["includeStories","excludeStories"].includes(key.name)||attrs.push(makeAttr(key.name,value))});let opening=j.jsxOpeningElement(jsxId,attrs);return opening.selfClosing=!0,j.jsxElement(opening)}),namedExports.replaceWith(exp=>{let storyKey=exp.node.declaration.declarations[0].id.name;if(!isExportStory(storyKey,meta))return exp.node;let jsxId=j.jsxIdentifier("Story"),name=getName(storyKey),attributes=[makeAttr("name",j.literal(name)),...getStoryAttrs(storyKey)],opening=j.jsxOpeningElement(jsxId,attributes),closing=j.jsxClosingElement(jsxId),children=[getStoryContents(exp.node.declaration.declarations[0].init)];return j.jsxElement(opening,closing,children)}),exportMdx(root,{quote:"single",trailingComma:"true",tabWidth:2})}export{transformer as default};
@@ -1,20 +0,0 @@
1
- import React from 'react';
2
- import Button from './Button';
3
- import { action } from '@storybook/addon-actions';
4
-
5
- export default {
6
- title: 'Button',
7
- };
8
-
9
- export const story1 = () => <Button label="Story 1" />;
10
-
11
- export const story2 = () => <Button label="Story 2" onClick={action('click')} />;
12
- story2.story = { name: 'second story' };
13
-
14
- export const story3 = () => (
15
- <div>
16
- <Button label="The Button" onClick={action('onClick')} />
17
- <br />
18
- </div>
19
- );
20
- story3.story = { name: 'complex story' };
@@ -1,18 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`csf-to-mdx transforms correctly using "basic.input.js" data 1`] = `
4
- "import Button from './Button';
5
- import { action } from '@storybook/addon-actions';
6
- import { Meta, Story } from '@storybook/addon-docs';
7
-
8
- <Meta title='Button' />
9
-
10
- <Story name='story1'><Button label='Story 1' /></Story>
11
-
12
- <Story name='second story'><Button label='Story 2' onClick={action('click')} /></Story>
13
-
14
- <Story name='complex story'><div>
15
- <Button label='The Button' onClick={action('onClick')} />
16
- <br />
17
- </div></Story>"
18
- `;
@@ -1,9 +0,0 @@
1
- import React from 'react';
2
- import Button from './Button';
3
-
4
- export default {
5
- title: 'Button',
6
- id: 'button-id',
7
- };
8
-
9
- export const someStory = () => <Button label="Story 1" />;
@@ -1,10 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`csf-to-mdx transforms correctly using "component-id.input.js" data 1`] = `
4
- "import Button from './Button';
5
- import { Meta, Story } from '@storybook/addon-docs';
6
-
7
- <Meta title='Button' id='button-id' />
8
-
9
- <Story name='someStory'><Button label='Story 1' /></Story>"
10
- `;
@@ -1,13 +0,0 @@
1
- import React from 'react';
2
- import Button from './Button';
3
-
4
- export default {
5
- title: 'Some.Button',
6
- decorators: [withKnobs, (storyFn) => <div className="foo">{storyFn}</div>],
7
- };
8
-
9
- export const story1 = () => <Button label="The Button" />;
10
- story1.story = {
11
- name: 'with decorator',
12
- decorators: [withKnobs],
13
- };