@storybook/codemod 0.0.0-pr-23609-sha-f47ef339

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 (119) hide show
  1. package/README.md +262 -0
  2. package/dist/index.d.ts +7 -0
  3. package/dist/index.js +1 -0
  4. package/dist/transforms/add-component-parameters.d.ts +22 -0
  5. package/dist/transforms/add-component-parameters.js +1 -0
  6. package/dist/transforms/csf-2-to-3.d.ts +8 -0
  7. package/dist/transforms/csf-2-to-3.js +3 -0
  8. package/dist/transforms/csf-hoist-story-annotations.d.ts +26 -0
  9. package/dist/transforms/csf-hoist-story-annotations.js +1 -0
  10. package/dist/transforms/mdx-to-csf.d.ts +7 -0
  11. package/dist/transforms/mdx-to-csf.js +61 -0
  12. package/dist/transforms/move-builtin-addons.d.ts +3 -0
  13. package/dist/transforms/move-builtin-addons.js +1 -0
  14. package/dist/transforms/storiesof-to-csf.d.ts +24 -0
  15. package/dist/transforms/storiesof-to-csf.js +1 -0
  16. package/dist/transforms/update-addon-info.d.ts +27 -0
  17. package/dist/transforms/update-addon-info.js +1 -0
  18. package/dist/transforms/update-organisation-name.d.ts +25 -0
  19. package/dist/transforms/update-organisation-name.js +1 -0
  20. package/dist/transforms/upgrade-deprecated-types.d.ts +10 -0
  21. package/dist/transforms/upgrade-deprecated-types.js +2 -0
  22. package/dist/transforms/upgrade-hierarchy-separators.d.ts +3 -0
  23. package/dist/transforms/upgrade-hierarchy-separators.js +1 -0
  24. package/jest.config.js +9 -0
  25. package/package.json +102 -0
  26. package/project.json +6 -0
  27. package/src/index.ts +103 -0
  28. package/src/lib/utils.test.js +9 -0
  29. package/src/lib/utils.ts +29 -0
  30. package/src/transforms/__testfixtures__/add-component-parameters/add-component-parameters.input.js +44 -0
  31. package/src/transforms/__testfixtures__/add-component-parameters/add-component-parameters.output.snapshot +68 -0
  32. package/src/transforms/__testfixtures__/csf-hoist-story-annotations/basic.input.js +25 -0
  33. package/src/transforms/__testfixtures__/csf-hoist-story-annotations/basic.output.snapshot +27 -0
  34. package/src/transforms/__testfixtures__/csf-hoist-story-annotations/overrides.input.js +25 -0
  35. package/src/transforms/__testfixtures__/csf-hoist-story-annotations/overrides.output.snapshot +28 -0
  36. package/src/transforms/__testfixtures__/csf-hoist-story-annotations/variable.input.js +13 -0
  37. package/src/transforms/__testfixtures__/csf-hoist-story-annotations/variable.output.snapshot +17 -0
  38. package/src/transforms/__testfixtures__/mdx-to-csf/basic.input.mdx +18 -0
  39. package/src/transforms/__testfixtures__/mdx-to-csf/basic.output.snapshot +40 -0
  40. package/src/transforms/__testfixtures__/mdx-to-csf/component-id.input.mdx +6 -0
  41. package/src/transforms/__testfixtures__/mdx-to-csf/component-id.output.snapshot +17 -0
  42. package/src/transforms/__testfixtures__/mdx-to-csf/decorators.input.mdx +8 -0
  43. package/src/transforms/__testfixtures__/mdx-to-csf/decorators.output.snapshot +18 -0
  44. package/src/transforms/__testfixtures__/mdx-to-csf/exclude-stories.input.mdx +19 -0
  45. package/src/transforms/__testfixtures__/mdx-to-csf/exclude-stories.output.snapshot +39 -0
  46. package/src/transforms/__testfixtures__/mdx-to-csf/parameters.input.mdx +14 -0
  47. package/src/transforms/__testfixtures__/mdx-to-csf/parameters.output.snapshot +23 -0
  48. package/src/transforms/__testfixtures__/mdx-to-csf/plaintext.input.mdx +3 -0
  49. package/src/transforms/__testfixtures__/mdx-to-csf/plaintext.output.snapshot +11 -0
  50. package/src/transforms/__testfixtures__/mdx-to-csf/story-function.input.mdx +10 -0
  51. package/src/transforms/__testfixtures__/mdx-to-csf/story-function.output.snapshot +18 -0
  52. package/src/transforms/__testfixtures__/mdx-to-csf/story-parameters.input.mdx +18 -0
  53. package/src/transforms/__testfixtures__/mdx-to-csf/story-parameters.output.snapshot +32 -0
  54. package/src/transforms/__testfixtures__/mdx-to-csf/story-refs.input.mdx +22 -0
  55. package/src/transforms/__testfixtures__/mdx-to-csf/story-refs.output.snapshot +34 -0
  56. package/src/transforms/__testfixtures__/move-builtin-addons/default.input.js +2 -0
  57. package/src/transforms/__testfixtures__/move-builtin-addons/default.output.snapshot +8 -0
  58. package/src/transforms/__testfixtures__/move-builtin-addons/with-no-change.input.js +3 -0
  59. package/src/transforms/__testfixtures__/move-builtin-addons/with-no-change.output.snapshot +7 -0
  60. package/src/transforms/__testfixtures__/storiesof-to-csf/basic.input.js +18 -0
  61. package/src/transforms/__testfixtures__/storiesof-to-csf/basic.output.snapshot +45 -0
  62. package/src/transforms/__testfixtures__/storiesof-to-csf/collision.input.js +11 -0
  63. package/src/transforms/__testfixtures__/storiesof-to-csf/collision.output.snapshot +38 -0
  64. package/src/transforms/__testfixtures__/storiesof-to-csf/const.input.js +1 -0
  65. package/src/transforms/__testfixtures__/storiesof-to-csf/const.output.snapshot +13 -0
  66. package/src/transforms/__testfixtures__/storiesof-to-csf/decorators.input.js +9 -0
  67. package/src/transforms/__testfixtures__/storiesof-to-csf/decorators.output.snapshot +18 -0
  68. package/src/transforms/__testfixtures__/storiesof-to-csf/default.input.js +7 -0
  69. package/src/transforms/__testfixtures__/storiesof-to-csf/default.output.snapshot +17 -0
  70. package/src/transforms/__testfixtures__/storiesof-to-csf/digit.input.js +1 -0
  71. package/src/transforms/__testfixtures__/storiesof-to-csf/digit.output.js +5 -0
  72. package/src/transforms/__testfixtures__/storiesof-to-csf/digit.output.snapshot +9 -0
  73. package/src/transforms/__testfixtures__/storiesof-to-csf/export-destructuring.input.js +11 -0
  74. package/src/transforms/__testfixtures__/storiesof-to-csf/export-destructuring.output.snapshot +23 -0
  75. package/src/transforms/__testfixtures__/storiesof-to-csf/export-function.input.js +12 -0
  76. package/src/transforms/__testfixtures__/storiesof-to-csf/export-function.output.snapshot +23 -0
  77. package/src/transforms/__testfixtures__/storiesof-to-csf/export-names.input.js +14 -0
  78. package/src/transforms/__testfixtures__/storiesof-to-csf/export-names.output.snapshot +26 -0
  79. package/src/transforms/__testfixtures__/storiesof-to-csf/exports.input.js +2 -0
  80. package/src/transforms/__testfixtures__/storiesof-to-csf/exports.output.snapshot +16 -0
  81. package/src/transforms/__testfixtures__/storiesof-to-csf/module.input.js +12 -0
  82. package/src/transforms/__testfixtures__/storiesof-to-csf/module.output.snapshot +16 -0
  83. package/src/transforms/__testfixtures__/storiesof-to-csf/multi.input.js +14 -0
  84. package/src/transforms/__testfixtures__/storiesof-to-csf/multi.output.snapshot +39 -0
  85. package/src/transforms/__testfixtures__/storiesof-to-csf/parameters-as-var.input.js +8 -0
  86. package/src/transforms/__testfixtures__/storiesof-to-csf/parameters-as-var.output.snapshot +20 -0
  87. package/src/transforms/__testfixtures__/storiesof-to-csf/parameters.input.js +10 -0
  88. package/src/transforms/__testfixtures__/storiesof-to-csf/parameters.output.snapshot +23 -0
  89. package/src/transforms/__testfixtures__/storiesof-to-csf/storiesof-var.input.js +11 -0
  90. package/src/transforms/__testfixtures__/storiesof-to-csf/storiesof-var.output.snapshot +23 -0
  91. package/src/transforms/__testfixtures__/storiesof-to-csf/story-decorators.input.js +11 -0
  92. package/src/transforms/__testfixtures__/storiesof-to-csf/story-decorators.output.snapshot +29 -0
  93. package/src/transforms/__testfixtures__/storiesof-to-csf/story-parameters.input.js +14 -0
  94. package/src/transforms/__testfixtures__/storiesof-to-csf/story-parameters.output.snapshot +32 -0
  95. package/src/transforms/__testfixtures__/update-addon-info/update-addon-info.input.js +184 -0
  96. package/src/transforms/__testfixtures__/update-addon-info/update-addon-info.output.snapshot +184 -0
  97. package/src/transforms/__testfixtures__/update-organisation-name/update-organisation-name.input.js +19 -0
  98. package/src/transforms/__testfixtures__/update-organisation-name/update-organisation-name.output.snapshot +23 -0
  99. package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/csf.input.js +3 -0
  100. package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/csf.output.snapshot +7 -0
  101. package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/dynamic-storiesof.input.js +5 -0
  102. package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/dynamic-storiesof.output.snapshot +9 -0
  103. package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/storiesof.input.js +8 -0
  104. package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/storiesof.output.snapshot +12 -0
  105. package/src/transforms/__tests__/csf-2-to-3.test.ts +439 -0
  106. package/src/transforms/__tests__/mdx-to-csf.test.ts +628 -0
  107. package/src/transforms/__tests__/transforms.tests.js +32 -0
  108. package/src/transforms/__tests__/upgrade-deprecated-types.test.ts +170 -0
  109. package/src/transforms/add-component-parameters.js +62 -0
  110. package/src/transforms/csf-2-to-3.ts +336 -0
  111. package/src/transforms/csf-hoist-story-annotations.js +97 -0
  112. package/src/transforms/mdx-to-csf.ts +340 -0
  113. package/src/transforms/move-builtin-addons.js +32 -0
  114. package/src/transforms/storiesof-to-csf.js +277 -0
  115. package/src/transforms/update-addon-info.js +114 -0
  116. package/src/transforms/update-organisation-name.js +71 -0
  117. package/src/transforms/upgrade-deprecated-types.ts +142 -0
  118. package/src/transforms/upgrade-hierarchy-separators.js +39 -0
  119. package/tsconfig.json +10 -0
@@ -0,0 +1,340 @@
1
+ /* eslint-disable no-param-reassign,@typescript-eslint/no-shadow */
2
+ import type { FileInfo } from 'jscodeshift';
3
+ import { babelParse, babelParseExpression } from '@storybook/csf-tools';
4
+ import { remark } from 'remark';
5
+ import type { Root } from 'remark-mdx';
6
+ import remarkMdx from 'remark-mdx';
7
+ import { SKIP, visit } from 'unist-util-visit';
8
+ import { is } from 'unist-util-is';
9
+ import type {
10
+ MdxJsxAttribute,
11
+ MdxJsxExpressionAttribute,
12
+ MdxJsxFlowElement,
13
+ MdxJsxTextElement,
14
+ } from 'mdast-util-mdx-jsx';
15
+ import type { MdxjsEsm } from 'mdast-util-mdxjs-esm';
16
+ import * as t from '@babel/types';
17
+ import type { BabelFile } from '@babel/core';
18
+ import * as babel from '@babel/core';
19
+ import * as recast from 'recast';
20
+ import * as path from 'node:path';
21
+ import prettier from 'prettier';
22
+ import * as fs from 'node:fs';
23
+ import camelCase from 'lodash/camelCase';
24
+ import type { MdxFlowExpression } from 'mdast-util-mdx-expression';
25
+
26
+ const mdxProcessor = remark().use(remarkMdx) as ReturnType<typeof remark>;
27
+
28
+ export default function jscodeshift(info: FileInfo) {
29
+ const parsed = path.parse(info.path);
30
+
31
+ let baseName = path.join(
32
+ parsed.dir,
33
+ parsed.name.replace('.mdx', '').replace('.stories', '').replace('.story', '')
34
+ );
35
+
36
+ // make sure the new csf file we are going to create exists
37
+ while (fs.existsSync(`${baseName}.stories.js`)) {
38
+ baseName += '_';
39
+ }
40
+
41
+ const result = transform(info.source, path.basename(baseName));
42
+
43
+ const [mdx, csf] = result;
44
+
45
+ if (csf != null) {
46
+ fs.writeFileSync(`${baseName}.stories.js`, csf);
47
+ }
48
+
49
+ return mdx;
50
+ }
51
+
52
+ export function transform(source: string, baseName: string): [mdx: string, csf: string] {
53
+ const root = mdxProcessor.parse(source);
54
+ const storyNamespaceName = nameToValidExport(`${baseName}Stories`);
55
+
56
+ const metaAttributes: Array<MdxJsxAttribute | MdxJsxExpressionAttribute> = [];
57
+ const storiesMap = new Map<
58
+ string,
59
+ | {
60
+ type: 'value';
61
+ attributes: Array<MdxJsxAttribute | MdxJsxExpressionAttribute>;
62
+ children: (MdxJsxFlowElement | MdxJsxTextElement)['children'];
63
+ }
64
+ | {
65
+ type: 'reference';
66
+ }
67
+ | {
68
+ type: 'id';
69
+ }
70
+ >();
71
+
72
+ // rewrite addon docs import
73
+ visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => {
74
+ node.value = node.value
75
+ .replaceAll('@storybook/addon-docs/blocks', '@storybook/blocks')
76
+ .replaceAll('@storybook/addon-docs', '@storybook/blocks');
77
+ });
78
+
79
+ const file = getEsmAst(root);
80
+
81
+ visit(
82
+ root,
83
+ ['mdxJsxFlowElement', 'mdxJsxTextElement'],
84
+ (node: MdxJsxFlowElement | MdxJsxTextElement, index, parent) => {
85
+ if (is(node, { name: 'Meta' })) {
86
+ metaAttributes.push(...node.attributes);
87
+ node.attributes = [
88
+ {
89
+ type: 'mdxJsxAttribute',
90
+ name: 'of',
91
+ value: {
92
+ type: 'mdxJsxAttributeValueExpression',
93
+ value: storyNamespaceName,
94
+ },
95
+ },
96
+ ];
97
+ }
98
+ if (is(node, { name: 'Story' })) {
99
+ const nameAttribute = node.attributes.find(
100
+ (it) => it.type === 'mdxJsxAttribute' && it.name === 'name'
101
+ );
102
+ const idAttribute = node.attributes.find(
103
+ (it) => it.type === 'mdxJsxAttribute' && it.name === 'id'
104
+ );
105
+ const storyAttribute = node.attributes.find(
106
+ (it) => it.type === 'mdxJsxAttribute' && it.name === 'story'
107
+ );
108
+ if (typeof nameAttribute?.value === 'string') {
109
+ let name = nameToValidExport(nameAttribute.value);
110
+ while (variableNameExists(name)) name += '_';
111
+
112
+ storiesMap.set(name, {
113
+ type: 'value',
114
+ attributes: node.attributes,
115
+ children: node.children,
116
+ });
117
+ node.attributes = [
118
+ {
119
+ type: 'mdxJsxAttribute',
120
+ name: 'of',
121
+ value: {
122
+ type: 'mdxJsxAttributeValueExpression',
123
+ value: `${storyNamespaceName}.${name}`,
124
+ },
125
+ },
126
+ ];
127
+ node.children = [];
128
+ } else if (idAttribute?.value) {
129
+ // e.g. <Story id="button--primary" />
130
+ // should be migrated manually as it is very hard to find out where the definition of such a string id is located
131
+ const nodeString = mdxProcessor.stringify({ type: 'root', children: [node] }).trim();
132
+ const newNode: MdxFlowExpression = {
133
+ type: 'mdxFlowExpression',
134
+ value: `/* ${nodeString} is deprecated, please migrate it to <Story of={referenceToStory} /> see: https://storybook.js.org/migration-guides/7.0 */`,
135
+ };
136
+ storiesMap.set(idAttribute.value as string, { type: 'id' });
137
+ parent.children.splice(index, 0, newNode);
138
+ // current index is the new comment, and index + 1 is current node
139
+ // SKIP traversing current node, and continue with the node after that
140
+ return [SKIP, index + 2];
141
+ } else if (
142
+ storyAttribute?.type === 'mdxJsxAttribute' &&
143
+ typeof storyAttribute.value === 'object' &&
144
+ storyAttribute.value.type === 'mdxJsxAttributeValueExpression'
145
+ ) {
146
+ // e.g. <Story story={Primary} />
147
+
148
+ const name = storyAttribute.value.value;
149
+ node.attributes = [
150
+ {
151
+ type: 'mdxJsxAttribute',
152
+ name: 'of',
153
+ value: {
154
+ type: 'mdxJsxAttributeValueExpression',
155
+ value: `${storyNamespaceName}.${name}`,
156
+ },
157
+ },
158
+ ];
159
+ node.children = [];
160
+
161
+ storiesMap.set(name, { type: 'reference' });
162
+ } else {
163
+ parent.children.splice(index, 1);
164
+ // Do not traverse `node`, continue at the node *now* at `index`.
165
+ return [SKIP, index];
166
+ }
167
+ }
168
+ return undefined;
169
+ }
170
+ );
171
+
172
+ const metaProperties = metaAttributes.flatMap((attribute) => {
173
+ if (attribute.type === 'mdxJsxAttribute') {
174
+ if (typeof attribute.value === 'string') {
175
+ return [t.objectProperty(t.identifier(attribute.name), t.stringLiteral(attribute.value))];
176
+ }
177
+ return [
178
+ t.objectProperty(
179
+ t.identifier(attribute.name),
180
+ babelParseExpression(attribute.value.value) as any as t.Expression
181
+ ),
182
+ ];
183
+ }
184
+ return [];
185
+ });
186
+
187
+ file.path.traverse({
188
+ // remove mdx imports from csf
189
+ ImportDeclaration(path) {
190
+ if (path.node.source.value === '@storybook/blocks') {
191
+ path.remove();
192
+ }
193
+ },
194
+ // remove exports from csf file
195
+ ExportNamedDeclaration(path) {
196
+ path.replaceWith(path.node.declaration);
197
+ },
198
+ });
199
+
200
+ if (storiesMap.size === 0 && metaAttributes.length === 0) {
201
+ // A CSF file must have at least one story, so skip migrating if this is the case.
202
+ return [mdxProcessor.stringify(root), null];
203
+ }
204
+
205
+ addStoriesImport(root, baseName, storyNamespaceName);
206
+
207
+ const newStatements: t.Statement[] = [
208
+ t.exportDefaultDeclaration(t.objectExpression(metaProperties)),
209
+ ];
210
+
211
+ function mapChildrenToRender(children: (MdxJsxFlowElement | MdxJsxTextElement)['children']) {
212
+ const child = children[0];
213
+
214
+ if (!child) return undefined;
215
+
216
+ if (child.type === 'text') {
217
+ return t.arrowFunctionExpression([], t.stringLiteral(child.value));
218
+ }
219
+ if (child.type === 'mdxFlowExpression' || child.type === 'mdxTextExpression') {
220
+ const expression = babelParseExpression(child.value) as any as t.Expression;
221
+
222
+ // Recreating those lines: https://github.com/storybookjs/mdx1-csf/blob/f408fc97e9a63097ca1ee577df9315a3cccca975/src/sb-mdx-plugin.ts#L185-L198
223
+ const BIND_REGEX = /\.bind\(.*\)/;
224
+ if (BIND_REGEX.test(child.value)) {
225
+ return expression;
226
+ }
227
+ if (t.isIdentifier(expression)) {
228
+ return expression;
229
+ }
230
+ if (t.isArrowFunctionExpression(expression)) {
231
+ return expression;
232
+ }
233
+ return t.arrowFunctionExpression([], expression);
234
+ }
235
+
236
+ const expression = babelParseExpression(
237
+ mdxProcessor.stringify({ type: 'root', children: [child] })
238
+ ) as any as t.Expression;
239
+ return t.arrowFunctionExpression([], expression);
240
+ }
241
+
242
+ function variableNameExists(name: string) {
243
+ let found = false;
244
+ file.path.traverse({
245
+ VariableDeclarator: (path) => {
246
+ const lVal = path.node.id;
247
+ if (t.isIdentifier(lVal) && lVal.name === name) found = true;
248
+ },
249
+ });
250
+ return found;
251
+ }
252
+
253
+ newStatements.push(
254
+ ...[...storiesMap].flatMap(([key, value]) => {
255
+ if (value.type === 'id') return [];
256
+ if (value.type === 'reference') {
257
+ return [
258
+ t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier(key), t.identifier(key))]),
259
+ ];
260
+ }
261
+ const renderProperty = mapChildrenToRender(value.children);
262
+ const newObject = t.objectExpression([
263
+ ...(renderProperty
264
+ ? [t.objectProperty(t.identifier('render'), mapChildrenToRender(value.children))]
265
+ : []),
266
+ ...value.attributes.flatMap((attribute) => {
267
+ if (attribute.type === 'mdxJsxAttribute') {
268
+ if (typeof attribute.value === 'string') {
269
+ return [
270
+ t.objectProperty(t.identifier(attribute.name), t.stringLiteral(attribute.value)),
271
+ ];
272
+ }
273
+ return [
274
+ t.objectProperty(
275
+ t.identifier(attribute.name),
276
+ babelParseExpression(attribute.value.value) as any as t.Expression
277
+ ),
278
+ ];
279
+ }
280
+ return [];
281
+ }),
282
+ ]);
283
+
284
+ return [
285
+ t.exportNamedDeclaration(
286
+ t.variableDeclaration('const', [t.variableDeclarator(t.identifier(key), newObject)])
287
+ ),
288
+ ];
289
+ })
290
+ );
291
+
292
+ file.path.node.body = [...file.path.node.body, ...newStatements];
293
+
294
+ const newMdx = mdxProcessor.stringify(root);
295
+ let output = recast.print(file.path.node).code;
296
+
297
+ const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
298
+ printWidth: 100,
299
+ tabWidth: 2,
300
+ bracketSpacing: true,
301
+ trailingComma: 'es5',
302
+ singleQuote: true,
303
+ };
304
+
305
+ output = prettier.format(output, { ...prettierConfig, filepath: `file.jsx` });
306
+
307
+ return [newMdx, output];
308
+ }
309
+
310
+ function getEsmAst(root: Root) {
311
+ const esm: string[] = [];
312
+ visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => {
313
+ esm.push(node.value);
314
+ });
315
+ const esmSource = `${esm.join('\n\n')}`;
316
+
317
+ // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606
318
+ const file: BabelFile = new babel.File(
319
+ { filename: 'info.path' },
320
+ { code: esmSource, ast: babelParse(esmSource) }
321
+ );
322
+ return file;
323
+ }
324
+
325
+ function addStoriesImport(root: Root, baseName: string, storyNamespaceName: string): void {
326
+ let found = false;
327
+
328
+ visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => {
329
+ if (!found) {
330
+ node.value += `\nimport * as ${storyNamespaceName} from './${baseName}.stories';`;
331
+ found = true;
332
+ }
333
+ });
334
+ }
335
+
336
+ export function nameToValidExport(name: string) {
337
+ const [first, ...rest] = Array.from(camelCase(name));
338
+
339
+ return `${first.match(/[a-zA-Z_$]/) ? first.toUpperCase() : `$${first}`}${rest.join('')}`;
340
+ }
@@ -0,0 +1,32 @@
1
+ export default function transformer(file, api) {
2
+ const j = api.jscodeshift;
3
+
4
+ const createImportDeclaration = (specifiers, source) =>
5
+ j.importDeclaration(
6
+ specifiers.map((s) => j.importSpecifier(j.identifier(s))),
7
+ j.literal(source)
8
+ );
9
+
10
+ const deprecates = {
11
+ action: [['action'], '@storybook/addon-actions'],
12
+ linkTo: [['linkTo'], '@storybook/addon-links'],
13
+ };
14
+
15
+ const transform = j(file.source)
16
+ .find(j.ImportDeclaration)
17
+ .filter((i) => i.value.source.value === '@storybook/react')
18
+ .forEach((i) => {
19
+ const importStatement = i.value;
20
+ importStatement.specifiers = importStatement.specifiers.filter((specifier) => {
21
+ const item = deprecates[specifier.local.name];
22
+ if (item) {
23
+ const [specifiers, moduleName] = item;
24
+ i.insertAfter(createImportDeclaration(specifiers, moduleName));
25
+ return false;
26
+ }
27
+ return specifier;
28
+ });
29
+ });
30
+
31
+ return transform.toSource({ quote: 'single' });
32
+ }
@@ -0,0 +1,277 @@
1
+ import prettier from 'prettier';
2
+ import { logger } from '@storybook/node-logger';
3
+ import { storyNameFromExport } from '@storybook/csf';
4
+ import { sanitizeName, jscodeshiftToPrettierParser } from '../lib/utils';
5
+
6
+ /**
7
+ * Convert a legacy story API to component story format
8
+ *
9
+ * For example:
10
+ *
11
+ * ```
12
+ * input { Button } from './Button';
13
+ * storiesOf('Button', module).add('story', () => <Button label="The Button" />);
14
+ * ```
15
+ *
16
+ * Becomes:
17
+ *
18
+ * ```
19
+ * input { Button } from './Button';
20
+ * export default {
21
+ * title: 'Button'
22
+ * }
23
+ * export const story = () => <Button label="The Button" />;
24
+ *
25
+ * NOTES: only support chained storiesOf() calls
26
+ */
27
+ export default function transformer(file, api, options) {
28
+ const LITERAL = ['ts', 'tsx'].includes(options.parser) ? 'StringLiteral' : 'Literal';
29
+
30
+ const j = api.jscodeshift;
31
+ const root = j(file.source);
32
+
33
+ function extractDecorators(parameters) {
34
+ if (!parameters) {
35
+ return {};
36
+ }
37
+ if (!parameters.properties) {
38
+ return { storyParams: parameters };
39
+ }
40
+ let storyDecorators = parameters.properties.find((p) => p.key.name === 'decorators');
41
+ if (!storyDecorators) {
42
+ return { storyParams: parameters };
43
+ }
44
+ storyDecorators = storyDecorators.value;
45
+ const storyParams = { ...parameters };
46
+ storyParams.properties = storyParams.properties.filter((p) => p.key.name !== 'decorators');
47
+ if (storyParams.properties.length === 0) {
48
+ return { storyDecorators };
49
+ }
50
+ return { storyParams, storyDecorators };
51
+ }
52
+
53
+ function convertToModuleExports(path, originalExports) {
54
+ const base = j(path);
55
+
56
+ const statements = [];
57
+ const extraExports = [];
58
+
59
+ // .addDecorator
60
+ const decorators = [];
61
+ base
62
+ .find(j.CallExpression)
63
+ .filter(
64
+ (call) => call.node.callee.property && call.node.callee.property.name === 'addDecorator'
65
+ )
66
+ .forEach((add) => {
67
+ const decorator = add.node.arguments[0];
68
+ decorators.push(decorator);
69
+ });
70
+ if (decorators.length > 0) {
71
+ decorators.reverse();
72
+ extraExports.push(
73
+ j.property('init', j.identifier('decorators'), j.arrayExpression(decorators))
74
+ );
75
+ }
76
+
77
+ // .addParameters
78
+ const parameters = [];
79
+ base
80
+ .find(j.CallExpression)
81
+ .filter(
82
+ (call) => call.node.callee.property && call.node.callee.property.name === 'addParameters'
83
+ )
84
+ .forEach((add) => {
85
+ // jscodeshift gives us the find results in reverse, but these args come in
86
+ // order, so we double reverse here. ugh.
87
+ const params = [...add.node.arguments[0].properties];
88
+ params.reverse();
89
+ params.forEach((prop) => parameters.push(prop));
90
+ });
91
+ if (parameters.length > 0) {
92
+ parameters.reverse();
93
+ extraExports.push(
94
+ j.property('init', j.identifier('parameters'), j.objectExpression(parameters))
95
+ );
96
+ }
97
+
98
+ if (originalExports.length > 0) {
99
+ extraExports.push(
100
+ j.property(
101
+ 'init',
102
+ j.identifier('excludeStories'),
103
+ j.arrayExpression(originalExports.map((exp) => j.literal(exp)))
104
+ )
105
+ );
106
+ }
107
+
108
+ // storiesOf(...)
109
+ base
110
+ .find(j.CallExpression)
111
+ .filter((call) => call.node.callee.name === 'storiesOf')
112
+ .filter((call) => call.node.arguments.length > 0 && call.node.arguments[0].type === LITERAL)
113
+ .forEach((storiesOf) => {
114
+ const title = storiesOf.node.arguments[0].value;
115
+ statements.push(
116
+ j.exportDefaultDeclaration(
117
+ j.objectExpression([
118
+ j.property('init', j.identifier('title'), j.literal(title)),
119
+ ...extraExports,
120
+ ])
121
+ )
122
+ );
123
+ });
124
+
125
+ // .add(...)
126
+ const adds = [];
127
+ base
128
+ .find(j.CallExpression)
129
+ .filter((add) => add.node.callee.property && add.node.callee.property.name === 'add')
130
+ .filter((add) => add.node.arguments.length >= 2 && add.node.arguments[0].type === LITERAL)
131
+ .forEach((add) => adds.push(add));
132
+
133
+ adds.reverse();
134
+ adds.push(path);
135
+
136
+ const identifiers = new Set();
137
+ root.find(j.Identifier).forEach(({ value }) => identifiers.add(value.name));
138
+ adds.forEach((add) => {
139
+ let name = add.node.arguments[0].value;
140
+ let key = sanitizeName(name);
141
+ while (identifiers.has(key)) {
142
+ key = `_${key}`;
143
+ }
144
+ identifiers.add(key);
145
+ if (storyNameFromExport(key) === name) {
146
+ name = null;
147
+ }
148
+
149
+ const val = add.node.arguments[1];
150
+ statements.push(
151
+ j.exportDeclaration(
152
+ false,
153
+ j.variableDeclaration('const', [j.variableDeclarator(j.identifier(key), val)])
154
+ )
155
+ );
156
+
157
+ const storyAnnotations = [];
158
+
159
+ if (name) {
160
+ storyAnnotations.push(j.property('init', j.identifier('name'), j.literal(name)));
161
+ }
162
+
163
+ if (add.node.arguments.length > 2) {
164
+ const originalStoryParams = add.node.arguments[2];
165
+ const { storyParams, storyDecorators } = extractDecorators(originalStoryParams);
166
+ if (storyParams) {
167
+ storyAnnotations.push(j.property('init', j.identifier('parameters'), storyParams));
168
+ }
169
+ if (storyDecorators) {
170
+ storyAnnotations.push(j.property('init', j.identifier('decorators'), storyDecorators));
171
+ }
172
+ }
173
+
174
+ if (storyAnnotations.length > 0) {
175
+ statements.push(
176
+ j.assignmentStatement(
177
+ '=',
178
+ j.memberExpression(j.identifier(key), j.identifier('story')),
179
+ j.objectExpression(storyAnnotations)
180
+ )
181
+ );
182
+ }
183
+ });
184
+
185
+ const stmt = path.parent.node.type === 'VariableDeclarator' ? path.parent.parent : path.parent;
186
+
187
+ statements.reverse();
188
+ statements.forEach((s) => stmt.insertAfter(s));
189
+ j(stmt).remove();
190
+ }
191
+
192
+ // Save the original storiesOf
193
+ const initialStoriesOf = root
194
+ .find(j.CallExpression)
195
+ .filter((call) => call.node.callee.name === 'storiesOf');
196
+
197
+ const defaultExports = root.find(j.ExportDefaultDeclaration);
198
+ // If there's already a default export
199
+ if (defaultExports.size() > 0) {
200
+ if (initialStoriesOf.size() > 0) {
201
+ logger.warn(
202
+ `Found ${initialStoriesOf.size()} 'storiesOf' calls but existing default export, SKIPPING: '${
203
+ file.path
204
+ }'`
205
+ );
206
+ }
207
+ return root.toSource();
208
+ }
209
+
210
+ // Exclude all the original named exports
211
+ const originalExports = [];
212
+ root.find(j.ExportNamedDeclaration).forEach((exp) => {
213
+ const { declaration, specifiers } = exp.node;
214
+ if (declaration) {
215
+ const { id, declarations } = declaration;
216
+ if (declarations) {
217
+ declarations.forEach((decl) => {
218
+ const { name, properties } = decl.id;
219
+ if (name) {
220
+ originalExports.push(name);
221
+ } else if (properties) {
222
+ properties.forEach((prop) => originalExports.push(prop.key.name));
223
+ }
224
+ });
225
+ } else if (id) {
226
+ originalExports.push(id.name);
227
+ }
228
+ } else if (specifiers) {
229
+ specifiers.forEach((spec) => originalExports.push(spec.exported.name));
230
+ }
231
+ });
232
+
233
+ // each top-level add expression corresponds to the last "add" of the chain.
234
+ // replace it with the entire export statements
235
+ root
236
+ .find(j.CallExpression)
237
+ .filter((add) => add.node.callee.property && add.node.callee.property.name === 'add')
238
+ .filter((add) => add.node.arguments.length >= 2 && add.node.arguments[0].type === LITERAL)
239
+ .filter((add) =>
240
+ ['ExpressionStatement', 'VariableDeclarator'].includes(add.parentPath.node.type)
241
+ )
242
+ .forEach((path) => convertToModuleExports(path, originalExports));
243
+
244
+ // remove storiesOf import
245
+ root
246
+ .find(j.ImportSpecifier)
247
+ .filter(
248
+ (spec) =>
249
+ spec.node.imported.name === 'storiesOf' &&
250
+ spec.parent.node.source.value.startsWith('@storybook/')
251
+ )
252
+ .forEach((spec) => {
253
+ const toRemove = spec.parent.node.specifiers.length > 1 ? spec : spec.parent;
254
+ j(toRemove).remove();
255
+ });
256
+
257
+ const source = root.toSource({ trailingComma: true, quote: 'single', tabWidth: 2 });
258
+ if (initialStoriesOf.size() > 1) {
259
+ logger.warn(
260
+ `Found ${initialStoriesOf.size()} 'storiesOf' calls, PLEASE FIX BY HAND: '${file.path}'`
261
+ );
262
+ return source;
263
+ }
264
+
265
+ const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
266
+ printWidth: 100,
267
+ tabWidth: 2,
268
+ bracketSpacing: true,
269
+ trailingComma: 'es5',
270
+ singleQuote: true,
271
+ };
272
+
273
+ return prettier.format(source, {
274
+ ...prettierConfig,
275
+ parser: jscodeshiftToPrettierParser(options.parser),
276
+ });
277
+ }