@storybook/codemod 7.0.0-beta.27 → 7.0.0-beta.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -39
- package/dist/chunk-YH46OF24.mjs +2 -0
- package/dist/index.mjs +1 -1
- package/dist/transforms/csf-2-to-3.d.ts +4 -2
- package/dist/transforms/csf-2-to-3.js +3 -1
- package/dist/transforms/csf-2-to-3.mjs +2 -1
- package/dist/transforms/upgrade-deprecated-types.d.ts +10 -0
- package/dist/transforms/upgrade-deprecated-types.js +2 -0
- package/dist/transforms/upgrade-deprecated-types.mjs +1 -0
- package/jest.config.js +1 -0
- package/package.json +10 -9
- package/src/transforms/__tests__/csf-2-to-3.test.ts +174 -58
- package/src/transforms/__tests__/upgrade-deprecated-types.test.ts +170 -0
- package/src/transforms/csf-2-to-3.ts +144 -23
- package/src/transforms/upgrade-deprecated-types.ts +142 -0
- package/dist/transforms/csf-to-mdx.d.ts +0 -29
- package/dist/transforms/csf-to-mdx.js +0 -3
- package/dist/transforms/csf-to-mdx.mjs +0 -3
- package/src/transforms/__testfixtures__/csf-to-mdx/basic.input.js +0 -20
- package/src/transforms/__testfixtures__/csf-to-mdx/basic.output.snapshot +0 -18
- package/src/transforms/__testfixtures__/csf-to-mdx/component-id.input.js +0 -9
- package/src/transforms/__testfixtures__/csf-to-mdx/component-id.output.snapshot +0 -10
- package/src/transforms/__testfixtures__/csf-to-mdx/decorators.input.js +0 -13
- package/src/transforms/__testfixtures__/csf-to-mdx/decorators.output.snapshot +0 -12
- package/src/transforms/__testfixtures__/csf-to-mdx/exclude-stories.input.js +0 -23
- package/src/transforms/__testfixtures__/csf-to-mdx/exclude-stories.output.snapshot +0 -22
- package/src/transforms/__testfixtures__/csf-to-mdx/parameters.input.js +0 -16
- package/src/transforms/__testfixtures__/csf-to-mdx/parameters.output.snapshot +0 -17
- package/src/transforms/__testfixtures__/csf-to-mdx/story-function.input.js +0 -19
- package/src/transforms/__testfixtures__/csf-to-mdx/story-function.output.snapshot +0 -18
- package/src/transforms/__testfixtures__/csf-to-mdx/story-parameters.input.js +0 -24
- package/src/transforms/__testfixtures__/csf-to-mdx/story-parameters.output.snapshot +0 -22
- 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 {
|
6
|
-
import type { API, FileInfo
|
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(
|
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
|
-
|
118
|
-
|
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(
|
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
|
-
|
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
|
-
|
168
|
-
|
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,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
|
-
};
|