@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,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
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Adds a `component` parameter for each storiesOf(...) call.
3
+ *
4
+ * For example:
5
+ *
6
+ * input { Button } from './Button';
7
+ * storiesOf('Button', module).add('story', () => <Button label="The Button" />);
8
+ *
9
+ * Becomes:
10
+ *
11
+ * input { Button } from './Button';
12
+ * storiesOf('Button', module)
13
+ * .addParameters({ component: Button })
14
+ * .add('story', () => <Button label="The Button" />);
15
+ *
16
+ * Heuristics:
17
+ * - The storiesOf "kind" name must be Button
18
+ * - Button must be imported in the file
19
+ */
20
+ export default function transformer(file, api) {
21
+ const j = api.jscodeshift;
22
+ const root = j(file.source);
23
+
24
+ // Create a dictionary whose keys are all the named imports in the file.
25
+ // For instance:
26
+ //
27
+ // import foo from 'foo';
28
+ // import { bar, baz } from 'zoo';
29
+ //
30
+ // => { foo: true, bar: true, baz: true }
31
+ const importMap = {};
32
+ root.find(j.ImportDeclaration).forEach((imp) =>
33
+ imp.node.specifiers.forEach((spec) => {
34
+ importMap[spec.local.name] = true;
35
+ })
36
+ );
37
+
38
+ function getLeafName(string) {
39
+ const parts = string.split(/\/|\.|\|/);
40
+ return parts[parts.length - 1];
41
+ }
42
+
43
+ function addComponentParameter(call) {
44
+ const { node } = call;
45
+ const leafName = getLeafName(node.arguments[0].value);
46
+ return j.callExpression(j.memberExpression(node, j.identifier('addParameters')), [
47
+ j.objectExpression([j.property('init', j.identifier('component'), j.identifier(leafName))]),
48
+ ]);
49
+ }
50
+
51
+ root
52
+ .find(j.CallExpression)
53
+ .filter((call) => call.node.callee.name === 'storiesOf')
54
+ .filter((call) => call.node.arguments.length > 0 && call.node.arguments[0].type === 'Literal')
55
+ .filter((call) => {
56
+ const leafName = getLeafName(call.node.arguments[0].value);
57
+ return importMap[leafName];
58
+ })
59
+ .replaceWith(addComponentParameter);
60
+
61
+ return root.toSource();
62
+ }
@@ -0,0 +1,336 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import prettier from 'prettier';
3
+ import * as t from '@babel/types';
4
+ import { isIdentifier, isTSTypeAnnotation, isTSTypeReference } from '@babel/types';
5
+ import type { CsfFile } from '@storybook/csf-tools';
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';
12
+
13
+ const logger = console;
14
+
15
+ const renameAnnotation = (annotation: string) => {
16
+ return annotation === 'storyName' ? 'name' : annotation;
17
+ };
18
+
19
+ const getTemplateBindVariable = (init: t.Expression) =>
20
+ t.isCallExpression(init) &&
21
+ t.isMemberExpression(init.callee) &&
22
+ t.isIdentifier(init.callee.object) &&
23
+ t.isIdentifier(init.callee.property) &&
24
+ init.callee.property.name === 'bind' &&
25
+ (init.arguments.length === 0 ||
26
+ (init.arguments.length === 1 &&
27
+ t.isObjectExpression(init.arguments[0]) &&
28
+ init.arguments[0].properties.length === 0))
29
+ ? init.callee.object.name
30
+ : null;
31
+
32
+ // export const A = ...
33
+ // A.parameters = { ... }; <===
34
+ const isStoryAnnotation = (stmt: t.Statement, objectExports: Record<string, any>) =>
35
+ t.isExpressionStatement(stmt) &&
36
+ t.isAssignmentExpression(stmt.expression) &&
37
+ t.isMemberExpression(stmt.expression.left) &&
38
+ t.isIdentifier(stmt.expression.left.object) &&
39
+ objectExports[stmt.expression.left.object.name];
40
+
41
+ const getNewExport = (stmt: t.Statement, objectExports: Record<string, any>) => {
42
+ if (
43
+ t.isExportNamedDeclaration(stmt) &&
44
+ t.isVariableDeclaration(stmt.declaration) &&
45
+ stmt.declaration.declarations.length === 1
46
+ ) {
47
+ const decl = stmt.declaration.declarations[0];
48
+ if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
49
+ return objectExports[decl.id.name];
50
+ }
51
+ }
52
+ return null;
53
+ };
54
+
55
+ // Remove render function when it matches the global render function in react
56
+ // export default { component: Cat };
57
+ // export const A = (args) => <Cat {...args} />;
58
+ const isReactGlobalRenderFn = (csf: CsfFile, storyFn: t.Expression) => {
59
+ if (
60
+ csf._meta?.component &&
61
+ t.isArrowFunctionExpression(storyFn) &&
62
+ storyFn.params.length === 1 &&
63
+ t.isJSXElement(storyFn.body)
64
+ ) {
65
+ const { openingElement } = storyFn.body;
66
+ if (
67
+ openingElement.selfClosing &&
68
+ t.isJSXIdentifier(openingElement.name) &&
69
+ openingElement.attributes.length === 1
70
+ ) {
71
+ const attr = openingElement.attributes[0];
72
+ const param = storyFn.params[0];
73
+ if (
74
+ t.isJSXSpreadAttribute(attr) &&
75
+ t.isIdentifier(attr.argument) &&
76
+ t.isIdentifier(param) &&
77
+ param.name === attr.argument.name &&
78
+ csf._meta.component === openingElement.name.name
79
+ ) {
80
+ return true;
81
+ }
82
+ }
83
+ }
84
+ return false;
85
+ };
86
+
87
+ // A simple CSF story is a no-arg story without any extra annotations (params, args, etc.)
88
+ const isSimpleCSFStory = (init: t.Expression, annotations: t.ObjectProperty[]) =>
89
+ annotations.length === 0 && t.isArrowFunctionExpression(init) && init.params.length === 0;
90
+
91
+ function removeUnusedTemplates(csf: CsfFile) {
92
+ Object.entries(csf._templates).forEach(([template, templateExpression]) => {
93
+ const references: NodePath[] = [];
94
+ babel.traverse(csf._ast, {
95
+ Identifier: (path) => {
96
+ if (path.node.name === template) references.push(path);
97
+ },
98
+ });
99
+ // if there is only one reference and this reference is the variable declaration initializing the template
100
+ // then we are sure the template is unused
101
+ if (references.length === 1) {
102
+ const reference = references[0];
103
+ if (
104
+ reference.parentPath.isVariableDeclarator() &&
105
+ reference.parentPath.node.init === templateExpression
106
+ ) {
107
+ reference.parentPath.remove();
108
+ }
109
+ }
110
+ });
111
+ }
112
+
113
+ export default function transform(info: FileInfo, api: API, options: { parser?: string }) {
114
+ const makeTitle = (userTitle?: string) => {
115
+ return userTitle || 'FIXME';
116
+ };
117
+ const csf = loadCsf(info.source, { makeTitle });
118
+
119
+ try {
120
+ csf.parse();
121
+ } catch (err) {
122
+ logger.log(`Error ${err}, skipping`);
123
+ return info.source;
124
+ }
125
+
126
+ // This allows for showing buildCodeFrameError messages
127
+ // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606
128
+ const file: BabelFile = new babel.File(
129
+ { filename: info.path },
130
+ { code: info.source, ast: csf._ast }
131
+ );
132
+
133
+ const importHelper = new StorybookImportHelper(file, info);
134
+
135
+ const objectExports: Record<string, t.Statement> = {};
136
+ Object.entries(csf._storyExports).forEach(([key, decl]) => {
137
+ const annotations = Object.entries(csf._storyAnnotations[key]).map(([annotation, val]) => {
138
+ return t.objectProperty(t.identifier(renameAnnotation(annotation)), val as t.Expression);
139
+ });
140
+
141
+ if (t.isVariableDeclarator(decl)) {
142
+ const { init, id } = decl;
143
+ // only replace arrow function expressions && template
144
+ const template = getTemplateBindVariable(init);
145
+ if (!t.isArrowFunctionExpression(init) && !template) return;
146
+ // Do change the type of no-arg stories without annotations to StoryFn when applicable
147
+ if (isSimpleCSFStory(init, annotations)) {
148
+ objectExports[key] = t.exportNamedDeclaration(
149
+ t.variableDeclaration('const', [
150
+ t.variableDeclarator(importHelper.updateTypeTo(id, 'StoryFn'), init),
151
+ ])
152
+ );
153
+ return;
154
+ }
155
+
156
+ let storyFn: t.Expression = template && t.identifier(template);
157
+ if (!storyFn) {
158
+ storyFn = init;
159
+ }
160
+
161
+ // Remove the render function when we can hoist the template
162
+ // const Template = (args) => <Cat {...args} />;
163
+ // export const A = Template.bind({});
164
+ const renderAnnotation = isReactGlobalRenderFn(
165
+ csf,
166
+ template ? csf._templates[template] : storyFn
167
+ )
168
+ ? []
169
+ : [t.objectProperty(t.identifier('render'), storyFn)];
170
+
171
+ objectExports[key] = t.exportNamedDeclaration(
172
+ t.variableDeclaration('const', [
173
+ t.variableDeclarator(
174
+ importHelper.updateTypeTo(id, 'StoryObj'),
175
+ t.objectExpression([...renderAnnotation, ...annotations])
176
+ ),
177
+ ])
178
+ );
179
+ }
180
+ });
181
+
182
+ csf._ast.program.body = csf._ast.program.body.reduce((acc, stmt) => {
183
+ const statement = stmt as t.Statement;
184
+ // remove story annotations & template declarations
185
+ if (isStoryAnnotation(statement, objectExports)) {
186
+ return acc;
187
+ }
188
+
189
+ // replace story exports with new object exports
190
+ const newExport = getNewExport(statement, objectExports);
191
+ if (newExport) {
192
+ acc.push(newExport);
193
+ return acc;
194
+ }
195
+
196
+ // include unknown statements
197
+ acc.push(statement);
198
+ return acc;
199
+ }, []);
200
+
201
+ upgradeDeprecatedTypes(file);
202
+ importHelper.removeDeprecatedStoryImport();
203
+ removeUnusedTemplates(csf);
204
+
205
+ let output = recast.print(csf._ast, {}).code;
206
+
207
+ try {
208
+ const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
209
+ printWidth: 100,
210
+ tabWidth: 2,
211
+ bracketSpacing: true,
212
+ trailingComma: 'es5',
213
+ singleQuote: true,
214
+ };
215
+
216
+ output = prettier.format(output, {
217
+ ...prettierConfig,
218
+ // This will infer the parser from the filename.
219
+ filepath: info.path,
220
+ });
221
+ } catch (e) {
222
+ logger.log(`Failed applying prettier to ${info.path}.`);
223
+ }
224
+
225
+ return output;
226
+ }
227
+
228
+ class StorybookImportHelper {
229
+ constructor(file: BabelFile, info: FileInfo) {
230
+ this.sbImportDeclarations = this.getAllSbImportDeclarations(file);
231
+ }
232
+
233
+ private sbImportDeclarations: NodePath<t.ImportDeclaration>[];
234
+
235
+ private getAllSbImportDeclarations = (file: BabelFile) => {
236
+ const found: NodePath<t.ImportDeclaration>[] = [];
237
+
238
+ file.path.traverse({
239
+ ImportDeclaration: (path) => {
240
+ const source = path.node.source.value;
241
+ if (source.startsWith('@storybook/csf') || !source.startsWith('@storybook')) return;
242
+ const isRendererImport = path.get('specifiers').some((specifier) => {
243
+ if (specifier.isImportNamespaceSpecifier()) {
244
+ // throw path.buildCodeFrameError(
245
+ // `This codemod does not support namespace imports for a ${path.node.source.value} package.\n` +
246
+ // 'Replace the namespace import with named imports and try again.'
247
+ // );
248
+ throw new Error(
249
+ `This codemod does not support namespace imports for a ${path.node.source.value} package.\n` +
250
+ 'Replace the namespace import with named imports and try again.'
251
+ );
252
+ }
253
+ if (!specifier.isImportSpecifier()) return false;
254
+ const imported = specifier.get('imported');
255
+ if (!imported.isIdentifier()) return false;
256
+
257
+ return [
258
+ 'Story',
259
+ 'StoryFn',
260
+ 'StoryObj',
261
+ 'Meta',
262
+ 'ComponentStory',
263
+ 'ComponentStoryFn',
264
+ 'ComponentStoryObj',
265
+ 'ComponentMeta',
266
+ ].includes(imported.node.name);
267
+ });
268
+
269
+ if (isRendererImport) found.push(path);
270
+ },
271
+ });
272
+ return found;
273
+ };
274
+
275
+ getOrAddImport = (type: string): string | undefined => {
276
+ // prefer type import
277
+ const sbImport =
278
+ this.sbImportDeclarations.find((path) => path.node.importKind === 'type') ??
279
+ this.sbImportDeclarations[0];
280
+ if (sbImport == null) return undefined;
281
+
282
+ const specifiers = sbImport.get('specifiers');
283
+ const importSpecifier = specifiers.find((specifier) => {
284
+ if (!specifier.isImportSpecifier()) return false;
285
+ const imported = specifier.get('imported');
286
+ if (!imported.isIdentifier()) return false;
287
+ return imported.node.name === type;
288
+ });
289
+ if (importSpecifier) return importSpecifier.node.local.name;
290
+ specifiers[0].insertBefore(t.importSpecifier(t.identifier(type), t.identifier(type)));
291
+ return type;
292
+ };
293
+
294
+ removeDeprecatedStoryImport = () => {
295
+ const specifiers = this.sbImportDeclarations.flatMap((it) => it.get('specifiers'));
296
+ const storyImports = specifiers.filter((specifier) => {
297
+ if (!specifier.isImportSpecifier()) return false;
298
+ const imported = specifier.get('imported');
299
+ if (!imported.isIdentifier()) return false;
300
+ return imported.node.name === 'Story';
301
+ });
302
+ storyImports.forEach((path) => path.remove());
303
+ };
304
+
305
+ getAllLocalImports = () => {
306
+ return this.sbImportDeclarations
307
+ .flatMap((it) => it.get('specifiers'))
308
+ .map((it) => it.node.local.name);
309
+ };
310
+
311
+ updateTypeTo = (id: t.LVal, type: string): t.LVal => {
312
+ if (
313
+ isIdentifier(id) &&
314
+ isTSTypeAnnotation(id.typeAnnotation) &&
315
+ isTSTypeReference(id.typeAnnotation.typeAnnotation) &&
316
+ isIdentifier(id.typeAnnotation.typeAnnotation.typeName)
317
+ ) {
318
+ const { name } = id.typeAnnotation.typeAnnotation.typeName;
319
+ if (this.getAllLocalImports().includes(name)) {
320
+ const localTypeImport = this.getOrAddImport(type);
321
+ return {
322
+ ...id,
323
+ typeAnnotation: t.tsTypeAnnotation(
324
+ t.tsTypeReference(
325
+ t.identifier(localTypeImport),
326
+ id.typeAnnotation.typeAnnotation.typeParameters
327
+ )
328
+ ),
329
+ };
330
+ }
331
+ }
332
+ return id;
333
+ };
334
+ }
335
+
336
+ export const parser = 'tsx';
@@ -0,0 +1,97 @@
1
+ const getContainingStatement = (n) => {
2
+ if (n.node.type.endsWith('Statement')) {
3
+ return n;
4
+ }
5
+ return getContainingStatement(n.parent);
6
+ };
7
+
8
+ /**
9
+ * Hoist CSF .story annotations
10
+ *
11
+ * For example:
12
+ *
13
+ * ```
14
+ * export const Basic = () => <Button />
15
+ * Basic.story = {
16
+ * name: 'foo',
17
+ * parameters: { ... },
18
+ * decorators = [ ... ],
19
+ * };
20
+ * ```
21
+ *
22
+ * Becomes:
23
+ *
24
+ * ```
25
+ * export const Basic = () => <Button />
26
+ * Basic.storyName = 'foo';
27
+ * Basic.parameters = { ... };
28
+ * Basic.decorators = [ ... ];
29
+ * ```
30
+ */
31
+ export default function transformer(file, api) {
32
+ const j = api.jscodeshift;
33
+ const root = j(file.source);
34
+
35
+ const renameKey = (exp) =>
36
+ exp.type === 'Identifier' && exp.name === 'name' ? j.identifier('storyName') : exp;
37
+
38
+ // 1. If the program does not have `export default { title: '....' }, skip it
39
+ const defaultExportWithTitle = root
40
+ .find(j.ExportDefaultDeclaration)
41
+ .filter(
42
+ (def) =>
43
+ def.node.declaration.type === 'ObjectExpression' &&
44
+ def.node.declaration.properties.map((p) => p.key.name).includes('title')
45
+ );
46
+ if (defaultExportWithTitle.size() === 0) {
47
+ return root.toSource();
48
+ }
49
+
50
+ // 2. Replace each Foo.story = { x: xVal } with Foo.x = xVal;
51
+ const storyAssignments = root.find(j.AssignmentExpression).filter((exp) => {
52
+ const { left, right } = exp.node;
53
+ return (
54
+ left.type === 'MemberExpression' &&
55
+ left.object.type === 'Identifier' &&
56
+ left.property.type === 'Identifier' &&
57
+ left.property.name === 'story' &&
58
+ right.type === 'ObjectExpression'
59
+ );
60
+ });
61
+ storyAssignments.forEach((exp) => {
62
+ const { left, right } = exp.node;
63
+ right.properties.forEach((prop) => {
64
+ const stmt = getContainingStatement(exp);
65
+ stmt.insertBefore(
66
+ j.assignmentStatement('=', j.memberExpression(left.object, renameKey(prop.key)), prop.value)
67
+ );
68
+ });
69
+ });
70
+
71
+ // 3. Remove the .story annotations
72
+ storyAssignments.remove();
73
+
74
+ // 4. Replace each Foo.story.x with Foo.x;
75
+ const storyOverrides = root.find(j.AssignmentExpression).filter((exp) => {
76
+ const { left } = exp.node;
77
+ return (
78
+ left.type === 'MemberExpression' &&
79
+ left.object.type === 'MemberExpression' &&
80
+ left.object.property.type === 'Identifier' &&
81
+ left.object.property.name === 'story' &&
82
+ left.property.type === 'Identifier'
83
+ // ?? ANNOTATION_FIELDS.includes(right.property.name)
84
+ );
85
+ });
86
+ storyOverrides.replaceWith((exp) => {
87
+ const { left, right } = exp.node;
88
+ return j.assignmentExpression(
89
+ '=',
90
+ j.memberExpression(left.object.object, renameKey(left.property)),
91
+ right
92
+ );
93
+ });
94
+
95
+ // 4. Render the updated tree
96
+ return root.toSource({ quote: 'single' });
97
+ }