@storybook/codemod 7.0.0-rc.8 → 7.0.0
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.
@@ -1,3 +1,3 @@
|
|
1
1
|
var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf,__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 __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:!0}):target,mod)),__toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var csf_2_to_3_exports={};__export(csf_2_to_3_exports,{default:()=>transform,parser:()=>parser});module.exports=__toCommonJS(csf_2_to_3_exports);var import_prettier2=__toESM(require("prettier")),t2=__toESM(require("@babel/types")),import_types=require("@babel/types"),import_csf_tools2=require("@storybook/csf-tools"),babel2=__toESM(require("@babel/core")),recast2=__toESM(require("recast"));var import_prettier=__toESM(require("prettier")),babel=__toESM(require("@babel/core")),import_csf_tools=require("@storybook/csf-tools"),recast=__toESM(require("recast")),t=__toESM(require("@babel/types"));var deprecatedTypes=["ComponentStory","ComponentStoryFn","ComponentStoryObj","ComponentMeta","Story"];function migrateType(oldType){return oldType==="Story"||oldType==="ComponentStory"?"StoryFn":oldType.replace("Component","")}function upgradeDeprecatedTypes(file){let importedNamespaces=new Set,typeReferencesToUpdate=new Set,existingImports=[];file.path.traverse({ImportDeclaration:path=>{existingImports.push(...path.get("specifiers").map(specifier=>({name:specifier.node.local.name,isAlias:!(specifier.isImportSpecifier()&&t.isIdentifier(specifier.node.imported)&&specifier.node.local.name===specifier.node.imported.name),path:specifier}))),path.node.source.value.startsWith("@storybook")&&path.get("specifiers").forEach(specifier=>{if(specifier.isImportNamespaceSpecifier()&&importedNamespaces.add(specifier.node.local.name),!specifier.isImportSpecifier())return;let imported=specifier.get("imported");if(imported.isIdentifier()&&deprecatedTypes.includes(imported.node.name)){imported.node.name===specifier.node.local.name&&typeReferencesToUpdate.add(specifier.node.local.name);let newType=migrateType(imported.node.name);if(!existingImports.some(it=>it.name===newType))imported.replaceWith(t.identifier(newType)),existingImports.push({name:newType,isAlias:!1,path:specifier});else{let existingImport=existingImports.find(it=>it.name===newType&&it.isAlias);if(existingImport)throw existingImport.path.buildCodeFrameError(`This codemod does not support local imports that are called the same as a storybook import.
|
2
|
-
Rename this local import and try again.`);specifier.remove()}}})}}),file.path.traverse({TSTypeReference:path=>{let typeName=path.get("typeName");if(typeName.isIdentifier())typeReferencesToUpdate.has(typeName.node.name)&&typeName.replaceWith(t.identifier(migrateType(typeName.node.name)));else if(typeName.isTSQualifiedName()){let namespace=typeName.get("left");if(namespace.isIdentifier()&&importedNamespaces.has(namespace.node.name)){let right=typeName.get("right");deprecatedTypes.includes(right.node.name)&&right.replaceWith(t.identifier(migrateType(right.node.name)))}}}})}var logger=console,renameAnnotation=annotation=>annotation==="storyName"?"name":annotation,getTemplateBindVariable=init=>t2.isCallExpression(init)&&t2.isMemberExpression(init.callee)&&t2.isIdentifier(init.callee.object)&&t2.isIdentifier(init.callee.property)&&init.callee.property.name==="bind"&&(init.arguments.length===0||init.arguments.length===1&&t2.isObjectExpression(init.arguments[0])&&init.arguments[0].properties.length===0)?init.callee.object.name:null,isStoryAnnotation=(stmt,objectExports)=>t2.isExpressionStatement(stmt)&&t2.isAssignmentExpression(stmt.expression)&&t2.isMemberExpression(stmt.expression.left)&&t2.isIdentifier(stmt.expression.left.object)&&objectExports[stmt.expression.left.object.name],
|
2
|
+
Rename this local import and try again.`);specifier.remove()}}})}}),file.path.traverse({TSTypeReference:path=>{let typeName=path.get("typeName");if(typeName.isIdentifier())typeReferencesToUpdate.has(typeName.node.name)&&typeName.replaceWith(t.identifier(migrateType(typeName.node.name)));else if(typeName.isTSQualifiedName()){let namespace=typeName.get("left");if(namespace.isIdentifier()&&importedNamespaces.has(namespace.node.name)){let right=typeName.get("right");deprecatedTypes.includes(right.node.name)&&right.replaceWith(t.identifier(migrateType(right.node.name)))}}}})}var logger=console,renameAnnotation=annotation=>annotation==="storyName"?"name":annotation,getTemplateBindVariable=init=>t2.isCallExpression(init)&&t2.isMemberExpression(init.callee)&&t2.isIdentifier(init.callee.object)&&t2.isIdentifier(init.callee.property)&&init.callee.property.name==="bind"&&(init.arguments.length===0||init.arguments.length===1&&t2.isObjectExpression(init.arguments[0])&&init.arguments[0].properties.length===0)?init.callee.object.name:null,isStoryAnnotation=(stmt,objectExports)=>t2.isExpressionStatement(stmt)&&t2.isAssignmentExpression(stmt.expression)&&t2.isMemberExpression(stmt.expression.left)&&t2.isIdentifier(stmt.expression.left.object)&&objectExports[stmt.expression.left.object.name],getNewExport=(stmt,objectExports)=>{if(t2.isExportNamedDeclaration(stmt)&&t2.isVariableDeclaration(stmt.declaration)&&stmt.declaration.declarations.length===1){let decl=stmt.declaration.declarations[0];if(t2.isVariableDeclarator(decl)&&t2.isIdentifier(decl.id))return objectExports[decl.id.name]}return null},isReactGlobalRenderFn=(csf,storyFn)=>{var _a;if((_a=csf._meta)!=null&&_a.component&&t2.isArrowFunctionExpression(storyFn)&&storyFn.params.length===1&&t2.isJSXElement(storyFn.body)){let{openingElement}=storyFn.body;if(openingElement.selfClosing&&t2.isJSXIdentifier(openingElement.name)&&openingElement.attributes.length===1){let attr=openingElement.attributes[0],param=storyFn.params[0];if(t2.isJSXSpreadAttribute(attr)&&t2.isIdentifier(attr.argument)&&t2.isIdentifier(param)&¶m.name===attr.argument.name&&csf._meta.component===openingElement.name.name)return!0}}return!1},isSimpleCSFStory=(init,annotations)=>annotations.length===0&&t2.isArrowFunctionExpression(init)&&init.params.length===0;function removeUnusedTemplates(csf){Object.entries(csf._templates).forEach(([template,templateExpression])=>{let references=[];if(babel2.traverse(csf._ast,{Identifier:path=>{path.node.name===template&&references.push(path)}}),references.length===1){let reference=references[0];reference.parentPath.isVariableDeclarator()&&reference.parentPath.node.init===templateExpression&&reference.parentPath.remove()}})}function transform(info,api,options){let makeTitle=userTitle=>userTitle||"FIXME",csf=(0,import_csf_tools2.loadCsf)(info.source,{makeTitle});try{csf.parse()}catch(err){return logger.log(`Error ${err}, skipping`),info.source}let file=new babel2.File({filename:info.path},{code:info.source,ast:csf._ast}),importHelper=new StorybookImportHelper(file,info),objectExports={};Object.entries(csf._storyExports).forEach(([key,decl])=>{let annotations=Object.entries(csf._storyAnnotations[key]).map(([annotation,val])=>t2.objectProperty(t2.identifier(renameAnnotation(annotation)),val));if(t2.isVariableDeclarator(decl)){let{init,id}=decl,template=getTemplateBindVariable(init);if(!t2.isArrowFunctionExpression(init)&&!template)return;if(isSimpleCSFStory(init,annotations)){objectExports[key]=t2.exportNamedDeclaration(t2.variableDeclaration("const",[t2.variableDeclarator(importHelper.updateTypeTo(id,"StoryFn"),init)]));return}let storyFn=template&&t2.identifier(template);storyFn||(storyFn=init);let renderAnnotation=isReactGlobalRenderFn(csf,template?csf._templates[template]:storyFn)?[]:[t2.objectProperty(t2.identifier("render"),storyFn)];objectExports[key]=t2.exportNamedDeclaration(t2.variableDeclaration("const",[t2.variableDeclarator(importHelper.updateTypeTo(id,"StoryObj"),t2.objectExpression([...renderAnnotation,...annotations]))]))}}),csf._ast.program.body=csf._ast.program.body.reduce((acc,stmt)=>{let statement=stmt;if(isStoryAnnotation(statement,objectExports))return acc;let newExport=getNewExport(statement,objectExports);return newExport?(acc.push(newExport),acc):(acc.push(statement),acc)},[]),upgradeDeprecatedTypes(file),importHelper.removeDeprecatedStoryImport(),removeUnusedTemplates(csf);let output=recast2.print(csf._ast,{}).code;try{let prettierConfig=import_prettier2.default.resolveConfig.sync(".",{editorconfig:!0})||{printWidth:100,tabWidth:2,bracketSpacing:!0,trailingComma:"es5",singleQuote:!0};output=import_prettier2.default.format(output,{...prettierConfig,filepath:info.path})}catch{logger.log(`Failed applying prettier to ${info.path}.`)}return output}var StorybookImportHelper=class{constructor(file,info){this.getAllSbImportDeclarations=file=>{let found=[];return file.path.traverse({ImportDeclaration:path=>{let source=path.node.source.value;if(source.startsWith("@storybook/csf")||!source.startsWith("@storybook"))return;path.get("specifiers").some(specifier=>{if(specifier.isImportNamespaceSpecifier())throw new Error(`This codemod does not support namespace imports for a ${path.node.source.value} package.
|
3
3
|
Replace the namespace import with named imports and try again.`);if(!specifier.isImportSpecifier())return!1;let imported=specifier.get("imported");return imported.isIdentifier()?["Story","StoryFn","StoryObj","Meta","ComponentStory","ComponentStoryFn","ComponentStoryObj","ComponentMeta"].includes(imported.node.name):!1})&&found.push(path)}}),found};this.getOrAddImport=type=>{let sbImport=this.sbImportDeclarations.find(path=>path.node.importKind==="type")??this.sbImportDeclarations[0];if(sbImport==null)return;let specifiers=sbImport.get("specifiers"),importSpecifier2=specifiers.find(specifier=>{if(!specifier.isImportSpecifier())return!1;let imported=specifier.get("imported");return imported.isIdentifier()?imported.node.name===type:!1});return importSpecifier2?importSpecifier2.node.local.name:(specifiers[0].insertBefore(t2.importSpecifier(t2.identifier(type),t2.identifier(type))),type)};this.removeDeprecatedStoryImport=()=>{this.sbImportDeclarations.flatMap(it=>it.get("specifiers")).filter(specifier=>{if(!specifier.isImportSpecifier())return!1;let imported=specifier.get("imported");return imported.isIdentifier()?imported.node.name==="Story":!1}).forEach(path=>path.remove())};this.getAllLocalImports=()=>this.sbImportDeclarations.flatMap(it=>it.get("specifiers")).map(it=>it.node.local.name);this.updateTypeTo=(id,type)=>{if((0,import_types.isIdentifier)(id)&&(0,import_types.isTSTypeAnnotation)(id.typeAnnotation)&&(0,import_types.isTSTypeReference)(id.typeAnnotation.typeAnnotation)&&(0,import_types.isIdentifier)(id.typeAnnotation.typeAnnotation.typeName)){let{name}=id.typeAnnotation.typeAnnotation.typeName;if(this.getAllLocalImports().includes(name)){let localTypeImport=this.getOrAddImport(type);return{...id,typeAnnotation:t2.tsTypeAnnotation(t2.tsTypeReference(t2.identifier(localTypeImport),id.typeAnnotation.typeAnnotation.typeParameters))}}}return id};this.sbImportDeclarations=this.getAllSbImportDeclarations(file)}},parser="tsx";0&&(module.exports={parser});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@storybook/codemod",
|
3
|
-
"version": "7.0.0
|
3
|
+
"version": "7.0.0",
|
4
4
|
"description": "A collection of codemod scripts written with JSCodeshift",
|
5
5
|
"keywords": [
|
6
6
|
"storybook"
|
@@ -49,9 +49,9 @@
|
|
49
49
|
"@babel/preset-env": "~7.20.2",
|
50
50
|
"@babel/types": "~7.21.2",
|
51
51
|
"@storybook/csf": "next",
|
52
|
-
"@storybook/csf-tools": "7.0.0
|
53
|
-
"@storybook/node-logger": "7.0.0
|
54
|
-
"@storybook/types": "7.0.0
|
52
|
+
"@storybook/csf-tools": "7.0.0",
|
53
|
+
"@storybook/node-logger": "7.0.0",
|
54
|
+
"@storybook/types": "7.0.0",
|
55
55
|
"cross-spawn": "^7.0.3",
|
56
56
|
"globby": "^11.0.2",
|
57
57
|
"jscodeshift": "^0.14.0",
|
@@ -97,5 +97,5 @@
|
|
97
97
|
"cjs"
|
98
98
|
]
|
99
99
|
},
|
100
|
-
"gitHead": "
|
100
|
+
"gitHead": "4f2afa644d7f2833181fc03187f5597d442285a6"
|
101
101
|
}
|
@@ -136,14 +136,60 @@ describe('csf-2-to-3', () => {
|
|
136
136
|
`)
|
137
137
|
).toMatchInlineSnapshot(`
|
138
138
|
export default { title: 'Cat' };
|
139
|
+
const Template = (args) => <Cat {...args} />;
|
139
140
|
|
140
141
|
export const A = {
|
141
|
-
render:
|
142
|
+
render: Template,
|
142
143
|
args: { isPrimary: false },
|
143
144
|
};
|
144
145
|
`);
|
145
146
|
});
|
146
147
|
|
148
|
+
it('should reuse the template when there are multiple Template.bind references but no component defined', () => {
|
149
|
+
expect(
|
150
|
+
jsTransform(dedent`
|
151
|
+
export default { title: 'Cat' };
|
152
|
+
const Template = (args) => <Cat {...args} />;
|
153
|
+
|
154
|
+
export const A = Template.bind({});
|
155
|
+
A.args = { isPrimary: false };
|
156
|
+
|
157
|
+
export const B = Template.bind({});
|
158
|
+
B.args = { isPrimary: true };
|
159
|
+
|
160
|
+
|
161
|
+
export const C = Template.bind({});
|
162
|
+
C.args = { bla: true };
|
163
|
+
|
164
|
+
export const D = Template.bind({});
|
165
|
+
D.args = { bla: false };
|
166
|
+
`)
|
167
|
+
).toMatchInlineSnapshot(`
|
168
|
+
export default { title: 'Cat' };
|
169
|
+
const Template = (args) => <Cat {...args} />;
|
170
|
+
|
171
|
+
export const A = {
|
172
|
+
render: Template,
|
173
|
+
args: { isPrimary: false },
|
174
|
+
};
|
175
|
+
|
176
|
+
export const B = {
|
177
|
+
render: Template,
|
178
|
+
args: { isPrimary: true },
|
179
|
+
};
|
180
|
+
|
181
|
+
export const C = {
|
182
|
+
render: Template,
|
183
|
+
args: { bla: true },
|
184
|
+
};
|
185
|
+
|
186
|
+
export const D = {
|
187
|
+
render: Template,
|
188
|
+
args: { bla: false },
|
189
|
+
};
|
190
|
+
`);
|
191
|
+
});
|
192
|
+
|
147
193
|
it('should remove implicit global render for template.bind', () => {
|
148
194
|
expect(
|
149
195
|
jsTransform(dedent`
|
@@ -166,8 +212,10 @@ describe('csf-2-to-3', () => {
|
|
166
212
|
args: { isPrimary: false },
|
167
213
|
};
|
168
214
|
|
215
|
+
const Template2 = (args) => <Banana {...args} />;
|
216
|
+
|
169
217
|
export const B = {
|
170
|
-
render:
|
218
|
+
render: Template2,
|
171
219
|
args: { isPrimary: true },
|
172
220
|
};
|
173
221
|
`);
|
@@ -359,5 +407,33 @@ describe('csf-2-to-3', () => {
|
|
359
407
|
};
|
360
408
|
`);
|
361
409
|
});
|
410
|
+
|
411
|
+
it('migrate Story type to StoryFn when used in an not exported Template function', () => {
|
412
|
+
expect(
|
413
|
+
tsTransform(dedent`
|
414
|
+
import { Story, Meta } from '@storybook/react'
|
415
|
+
|
416
|
+
export default {
|
417
|
+
component: Cat,
|
418
|
+
} satisfies Meta
|
419
|
+
|
420
|
+
const Template: Story = () => <div>Hello World</div>;
|
421
|
+
|
422
|
+
export const Default = Template.bind({})
|
423
|
+
`)
|
424
|
+
).toMatchInlineSnapshot(`
|
425
|
+
import { StoryFn, Meta } from '@storybook/react';
|
426
|
+
|
427
|
+
export default {
|
428
|
+
component: Cat,
|
429
|
+
} satisfies Meta;
|
430
|
+
|
431
|
+
const Template: StoryFn = () => <div>Hello World</div>;
|
432
|
+
|
433
|
+
export const Default = {
|
434
|
+
render: Template,
|
435
|
+
};
|
436
|
+
`);
|
437
|
+
});
|
362
438
|
});
|
363
439
|
});
|
@@ -38,12 +38,6 @@ const isStoryAnnotation = (stmt: t.Statement, objectExports: Record<string, any>
|
|
38
38
|
t.isIdentifier(stmt.expression.left.object) &&
|
39
39
|
objectExports[stmt.expression.left.object.name];
|
40
40
|
|
41
|
-
const isTemplateDeclaration = (stmt: t.Statement, templates: Record<string, any>) =>
|
42
|
-
t.isVariableDeclaration(stmt) &&
|
43
|
-
stmt.declarations.length === 1 &&
|
44
|
-
t.isIdentifier(stmt.declarations[0].id) &&
|
45
|
-
templates[stmt.declarations[0].id.name];
|
46
|
-
|
47
41
|
const getNewExport = (stmt: t.Statement, objectExports: Record<string, any>) => {
|
48
42
|
if (
|
49
43
|
t.isExportNamedDeclaration(stmt) &&
|
@@ -94,6 +88,28 @@ const isReactGlobalRenderFn = (csf: CsfFile, storyFn: t.Expression) => {
|
|
94
88
|
const isSimpleCSFStory = (init: t.Expression, annotations: t.ObjectProperty[]) =>
|
95
89
|
annotations.length === 0 && t.isArrowFunctionExpression(init) && init.params.length === 0;
|
96
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
|
+
|
97
113
|
export default function transform(info: FileInfo, api: API, options: { parser?: string }) {
|
98
114
|
const makeTitle = (userTitle?: string) => {
|
99
115
|
return userTitle || 'FIXME';
|
@@ -137,15 +153,18 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
|
|
137
153
|
return;
|
138
154
|
}
|
139
155
|
|
140
|
-
|
141
|
-
// const Template = (args) => <Cat {...args} />;
|
142
|
-
// export const A = Template.bind({});
|
143
|
-
let storyFn: t.Expression = template && (csf._templates[template] as any as t.Expression);
|
156
|
+
let storyFn: t.Expression = template && t.identifier(template);
|
144
157
|
if (!storyFn) {
|
145
158
|
storyFn = init;
|
146
159
|
}
|
147
160
|
|
148
|
-
|
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
|
+
)
|
149
168
|
? []
|
150
169
|
: [t.objectProperty(t.identifier('render'), storyFn)];
|
151
170
|
|
@@ -160,15 +179,10 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
|
|
160
179
|
}
|
161
180
|
});
|
162
181
|
|
163
|
-
importHelper.removeDeprecatedStoryImport();
|
164
|
-
|
165
182
|
csf._ast.program.body = csf._ast.program.body.reduce((acc, stmt) => {
|
166
183
|
const statement = stmt as t.Statement;
|
167
184
|
// remove story annotations & template declarations
|
168
|
-
if (
|
169
|
-
isStoryAnnotation(statement, objectExports) ||
|
170
|
-
isTemplateDeclaration(statement, csf._templates)
|
171
|
-
) {
|
185
|
+
if (isStoryAnnotation(statement, objectExports)) {
|
172
186
|
return acc;
|
173
187
|
}
|
174
188
|
|
@@ -185,6 +199,8 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
|
|
185
199
|
}, []);
|
186
200
|
|
187
201
|
upgradeDeprecatedTypes(file);
|
202
|
+
importHelper.removeDeprecatedStoryImport();
|
203
|
+
removeUnusedTemplates(csf);
|
188
204
|
|
189
205
|
let output = recast.print(csf._ast, {}).code;
|
190
206
|
|