@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.
- package/README.md +262 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/transforms/add-component-parameters.d.ts +22 -0
- package/dist/transforms/add-component-parameters.js +1 -0
- package/dist/transforms/csf-2-to-3.d.ts +8 -0
- package/dist/transforms/csf-2-to-3.js +3 -0
- package/dist/transforms/csf-hoist-story-annotations.d.ts +26 -0
- package/dist/transforms/csf-hoist-story-annotations.js +1 -0
- package/dist/transforms/mdx-to-csf.d.ts +7 -0
- package/dist/transforms/mdx-to-csf.js +61 -0
- package/dist/transforms/move-builtin-addons.d.ts +3 -0
- package/dist/transforms/move-builtin-addons.js +1 -0
- package/dist/transforms/storiesof-to-csf.d.ts +24 -0
- package/dist/transforms/storiesof-to-csf.js +1 -0
- package/dist/transforms/update-addon-info.d.ts +27 -0
- package/dist/transforms/update-addon-info.js +1 -0
- package/dist/transforms/update-organisation-name.d.ts +25 -0
- package/dist/transforms/update-organisation-name.js +1 -0
- package/dist/transforms/upgrade-deprecated-types.d.ts +10 -0
- package/dist/transforms/upgrade-deprecated-types.js +2 -0
- package/dist/transforms/upgrade-hierarchy-separators.d.ts +3 -0
- package/dist/transforms/upgrade-hierarchy-separators.js +1 -0
- package/jest.config.js +9 -0
- package/package.json +102 -0
- package/project.json +6 -0
- package/src/index.ts +103 -0
- package/src/lib/utils.test.js +9 -0
- package/src/lib/utils.ts +29 -0
- package/src/transforms/__testfixtures__/add-component-parameters/add-component-parameters.input.js +44 -0
- package/src/transforms/__testfixtures__/add-component-parameters/add-component-parameters.output.snapshot +68 -0
- package/src/transforms/__testfixtures__/csf-hoist-story-annotations/basic.input.js +25 -0
- package/src/transforms/__testfixtures__/csf-hoist-story-annotations/basic.output.snapshot +27 -0
- package/src/transforms/__testfixtures__/csf-hoist-story-annotations/overrides.input.js +25 -0
- package/src/transforms/__testfixtures__/csf-hoist-story-annotations/overrides.output.snapshot +28 -0
- package/src/transforms/__testfixtures__/csf-hoist-story-annotations/variable.input.js +13 -0
- package/src/transforms/__testfixtures__/csf-hoist-story-annotations/variable.output.snapshot +17 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/basic.input.mdx +18 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/basic.output.snapshot +40 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/component-id.input.mdx +6 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/component-id.output.snapshot +17 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/decorators.input.mdx +8 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/decorators.output.snapshot +18 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/exclude-stories.input.mdx +19 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/exclude-stories.output.snapshot +39 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/parameters.input.mdx +14 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/parameters.output.snapshot +23 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/plaintext.input.mdx +3 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/plaintext.output.snapshot +11 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/story-function.input.mdx +10 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/story-function.output.snapshot +18 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/story-parameters.input.mdx +18 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/story-parameters.output.snapshot +32 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/story-refs.input.mdx +22 -0
- package/src/transforms/__testfixtures__/mdx-to-csf/story-refs.output.snapshot +34 -0
- package/src/transforms/__testfixtures__/move-builtin-addons/default.input.js +2 -0
- package/src/transforms/__testfixtures__/move-builtin-addons/default.output.snapshot +8 -0
- package/src/transforms/__testfixtures__/move-builtin-addons/with-no-change.input.js +3 -0
- package/src/transforms/__testfixtures__/move-builtin-addons/with-no-change.output.snapshot +7 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/basic.input.js +18 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/basic.output.snapshot +45 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/collision.input.js +11 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/collision.output.snapshot +38 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/const.input.js +1 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/const.output.snapshot +13 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/decorators.input.js +9 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/decorators.output.snapshot +18 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/default.input.js +7 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/default.output.snapshot +17 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/digit.input.js +1 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/digit.output.js +5 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/digit.output.snapshot +9 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/export-destructuring.input.js +11 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/export-destructuring.output.snapshot +23 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/export-function.input.js +12 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/export-function.output.snapshot +23 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/export-names.input.js +14 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/export-names.output.snapshot +26 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/exports.input.js +2 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/exports.output.snapshot +16 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/module.input.js +12 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/module.output.snapshot +16 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/multi.input.js +14 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/multi.output.snapshot +39 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/parameters-as-var.input.js +8 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/parameters-as-var.output.snapshot +20 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/parameters.input.js +10 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/parameters.output.snapshot +23 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/storiesof-var.input.js +11 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/storiesof-var.output.snapshot +23 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/story-decorators.input.js +11 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/story-decorators.output.snapshot +29 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/story-parameters.input.js +14 -0
- package/src/transforms/__testfixtures__/storiesof-to-csf/story-parameters.output.snapshot +32 -0
- package/src/transforms/__testfixtures__/update-addon-info/update-addon-info.input.js +184 -0
- package/src/transforms/__testfixtures__/update-addon-info/update-addon-info.output.snapshot +184 -0
- package/src/transforms/__testfixtures__/update-organisation-name/update-organisation-name.input.js +19 -0
- package/src/transforms/__testfixtures__/update-organisation-name/update-organisation-name.output.snapshot +23 -0
- package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/csf.input.js +3 -0
- package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/csf.output.snapshot +7 -0
- package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/dynamic-storiesof.input.js +5 -0
- package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/dynamic-storiesof.output.snapshot +9 -0
- package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/storiesof.input.js +8 -0
- package/src/transforms/__testfixtures__/upgrade-hierarchy-separators/storiesof.output.snapshot +12 -0
- package/src/transforms/__tests__/csf-2-to-3.test.ts +439 -0
- package/src/transforms/__tests__/mdx-to-csf.test.ts +628 -0
- package/src/transforms/__tests__/transforms.tests.js +32 -0
- package/src/transforms/__tests__/upgrade-deprecated-types.test.ts +170 -0
- package/src/transforms/add-component-parameters.js +62 -0
- package/src/transforms/csf-2-to-3.ts +336 -0
- package/src/transforms/csf-hoist-story-annotations.js +97 -0
- package/src/transforms/mdx-to-csf.ts +340 -0
- package/src/transforms/move-builtin-addons.js +32 -0
- package/src/transforms/storiesof-to-csf.js +277 -0
- package/src/transforms/update-addon-info.js +114 -0
- package/src/transforms/update-organisation-name.js +71 -0
- package/src/transforms/upgrade-deprecated-types.ts +142 -0
- package/src/transforms/upgrade-hierarchy-separators.js +39 -0
- 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
|
+
}
|