@storybook/codemod 7.0.0-rc.8 → 7.0.0-rc.9
Sign up to get free protection for your applications and to get access to all the features.
@@ -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-rc.
|
3
|
+
"version": "7.0.0-rc.9",
|
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-rc.
|
53
|
-
"@storybook/node-logger": "7.0.0-rc.
|
54
|
-
"@storybook/types": "7.0.0-rc.
|
52
|
+
"@storybook/csf-tools": "7.0.0-rc.9",
|
53
|
+
"@storybook/node-logger": "7.0.0-rc.9",
|
54
|
+
"@storybook/types": "7.0.0-rc.9",
|
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": "618743e115ad0f172537a4b7b6cdfb0beaef52b5"
|
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
|
|