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

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