diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index d20fa00879b3..1e7650b6b268 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -65,7 +65,8 @@ "jscodeshift": "^0.15.1", "lodash": "^4.17.21", "prettier": "^2.8.0", - "recast": "^0.23.1" + "recast": "^0.23.1", + "tiny-invariant": "^1.3.1" }, "devDependencies": { "@types/jscodeshift": "^0.11.10", diff --git a/code/lib/codemod/src/transforms/csf-2-to-3.ts b/code/lib/codemod/src/transforms/csf-2-to-3.ts index 945f3542b6e8..72f283df95f3 100644 --- a/code/lib/codemod/src/transforms/csf-2-to-3.ts +++ b/code/lib/codemod/src/transforms/csf-2-to-3.ts @@ -7,6 +7,7 @@ import { loadCsf, printCsf } from '@storybook/csf-tools'; import type { API, FileInfo } from 'jscodeshift'; import type { BabelFile, NodePath } from '@babel/core'; import * as babel from '@babel/core'; +import invariant from 'tiny-invariant'; import { upgradeDeprecatedTypes } from './upgrade-deprecated-types'; const logger = console; @@ -15,7 +16,7 @@ const renameAnnotation = (annotation: string) => { return annotation === 'storyName' ? 'name' : annotation; }; -const getTemplateBindVariable = (init: t.Expression) => +const getTemplateBindVariable = (init: t.Expression | undefined) => t.isCallExpression(init) && t.isMemberExpression(init.callee) && t.isIdentifier(init.callee.object) && @@ -92,7 +93,7 @@ function removeUnusedTemplates(csf: CsfFile) { const references: NodePath[] = []; babel.traverse(csf._ast, { Identifier: (path) => { - if (path.node.name === template) references.push(path); + if (path.node.name === template) references.push(path as NodePath); }, }); // if there is only one reference and this reference is the variable declaration initializing the template @@ -100,7 +101,7 @@ function removeUnusedTemplates(csf: CsfFile) { if (references.length === 1) { const reference = references[0]; if ( - reference.parentPath.isVariableDeclarator() && + reference.parentPath?.isVariableDeclarator() && reference.parentPath.node.init === templateExpression ) { reference.parentPath.remove(); @@ -124,6 +125,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?: // This allows for showing buildCodeFrameError messages // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 + const file: BabelFile = new babel.File( { filename: info.path }, { code: info.source, ast: csf._ast } @@ -137,8 +139,9 @@ export default function transform(info: FileInfo, api: API, options: { parser?: return t.objectProperty(t.identifier(renameAnnotation(annotation)), val as t.Expression); }); - if (t.isVariableDeclarator(decl)) { - const { init, id } = decl; + if (t.isVariableDeclarator(decl as t.Node)) { + const { init, id } = decl as any; + invariant(init, 'Inital value should be declared'); // only replace arrow function expressions && template const template = getTemplateBindVariable(init); if (!t.isArrowFunctionExpression(init) && !template) return; @@ -152,10 +155,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?: return; } - let storyFn: t.Expression = template && t.identifier(template); - if (!storyFn) { - storyFn = init; - } + const storyFn: t.Expression = template ? t.identifier(template) : init; // Remove the render function when we can hoist the template // const Template = (args) => ; @@ -178,8 +178,8 @@ export default function transform(info: FileInfo, api: API, options: { parser?: } }); - csf._ast.program.body = csf._ast.program.body.reduce((acc, stmt) => { - const statement = stmt as t.Statement; + csf._ast.program.body = csf._ast.program.body.reduce((acc: t.Statement[], stmt: t.Statement) => { + const statement = stmt; // remove story annotations & template declarations if (isStoryAnnotation(statement, objectExports)) { return acc; @@ -251,8 +251,23 @@ class StorybookImportHelper { } if (!specifier.isImportSpecifier()) return false; const imported = specifier.get('imported'); - if (!imported.isIdentifier()) return false; + if (Array.isArray(imported)) { + return imported.some((importedSpecifier) => { + if (!importedSpecifier.isIdentifier()) return false; + return [ + 'Story', + 'StoryFn', + 'StoryObj', + 'Meta', + 'ComponentStory', + 'ComponentStoryFn', + 'ComponentStoryObj', + 'ComponentMeta', + ].includes(importedSpecifier.node.name); + }); + } + if (!imported.isIdentifier()) return false; return [ 'Story', 'StoryFn', @@ -321,7 +336,7 @@ class StorybookImportHelper { ...id, typeAnnotation: t.tsTypeAnnotation( t.tsTypeReference( - t.identifier(localTypeImport), + t.identifier(localTypeImport ?? ''), id.typeAnnotation.typeAnnotation.typeParameters ) ), diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index 48e527f237f6..b5f68b8a7b50 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-param-reassign,@typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/ban-ts-comment,no-param-reassign,@typescript-eslint/no-shadow */ import type { FileInfo } from 'jscodeshift'; import { babelParse, babelParseExpression } from '@storybook/csf-tools'; import { remark } from 'remark'; @@ -49,7 +49,7 @@ export default function jscodeshift(info: FileInfo) { return mdx; } -export function transform(source: string, baseName: string): [mdx: string, csf: string] { +export function transform(source: string, baseName: string): [string, string] { const root = mdxProcessor.parse(source); const storyNamespaceName = nameToValidExport(`${baseName}Stories`); @@ -70,6 +70,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: >(); // rewrite addon docs import + // @ts-ignore visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => { node.value = node.value .replaceAll('@storybook/addon-docs/blocks', '@storybook/blocks') @@ -78,6 +79,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: const file = getEsmAst(root); + // @ts-ignore visit( root, ['mdxJsxFlowElement', 'mdxJsxTextElement'], @@ -134,18 +136,18 @@ export function transform(source: string, baseName: string): [mdx: string, csf: value: `/* ${nodeString} is deprecated, please migrate it to see: https://storybook.js.org/migration-guides/7.0 */`, }; storiesMap.set(idAttribute.value as string, { type: 'id' }); - parent.children.splice(index, 0, newNode); + parent?.children.splice(index as number, 0, newNode); // current index is the new comment, and index + 1 is current node // SKIP traversing current node, and continue with the node after that - return [SKIP, index + 2]; + return [SKIP, (index as number) + 2]; } else if ( storyAttribute?.type === 'mdxJsxAttribute' && typeof storyAttribute.value === 'object' && - storyAttribute.value.type === 'mdxJsxAttributeValueExpression' + storyAttribute.value?.type === 'mdxJsxAttributeValueExpression' ) { // e.g. - const name = storyAttribute.value.value; + const name = storyAttribute.value?.value; node.attributes = [ { type: 'mdxJsxAttribute', @@ -158,9 +160,9 @@ export function transform(source: string, baseName: string): [mdx: string, csf: ]; node.children = []; - storiesMap.set(name, { type: 'reference' }); + storiesMap.set(name ?? '', { type: 'reference' }); } else { - parent.children.splice(index, 1); + parent?.children.splice(index as number, 1); // Do not traverse `node`, continue at the node *now* at `index`. return [SKIP, index]; } @@ -177,7 +179,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: return [ t.objectProperty( t.identifier(attribute.name), - babelParseExpression(attribute.value.value) as any as t.Expression + babelParseExpression(attribute.value?.value ?? '') as any as t.Expression ), ]; } @@ -193,13 +195,14 @@ export function transform(source: string, baseName: string): [mdx: string, csf: }, // remove exports from csf file ExportNamedDeclaration(path) { + // @ts-ignore path.replaceWith(path.node.declaration); }, }); if (storiesMap.size === 0 && metaAttributes.length === 0) { // A CSF file must have at least one story, so skip migrating if this is the case. - return [mdxProcessor.stringify(root), null]; + return [mdxProcessor.stringify(root), '']; } addStoriesImport(root, baseName, storyNamespaceName); @@ -260,9 +263,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: } const renderProperty = mapChildrenToRender(value.children); const newObject = t.objectExpression([ - ...(renderProperty - ? [t.objectProperty(t.identifier('render'), mapChildrenToRender(value.children))] - : []), + ...(renderProperty ? [t.objectProperty(t.identifier('render'), renderProperty)] : []), ...value.attributes.flatMap((attribute) => { if (attribute.type === 'mdxJsxAttribute') { if (typeof attribute.value === 'string') { @@ -273,7 +274,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: return [ t.objectProperty( t.identifier(attribute.name), - babelParseExpression(attribute.value.value) as any as t.Expression + babelParseExpression(attribute.value?.value ?? '') as any as t.Expression ), ]; } @@ -309,12 +310,13 @@ export function transform(source: string, baseName: string): [mdx: string, csf: function getEsmAst(root: Root) { const esm: string[] = []; + // @ts-expect-error (not valid BuildVisitor) visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => { esm.push(node.value); }); const esmSource = `${esm.join('\n\n')}`; - // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 + // @ts-expect-error (File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606) const file: BabelFile = new babel.File( { filename: 'info.path' }, { code: esmSource, ast: babelParse(esmSource) } @@ -324,7 +326,7 @@ function getEsmAst(root: Root) { function addStoriesImport(root: Root, baseName: string, storyNamespaceName: string): void { let found = false; - + // @ts-expect-error (not valid BuildVisitor) visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => { if (!found) { node.value += `\nimport * as ${storyNamespaceName} from './${baseName}.stories';`; diff --git a/code/lib/codemod/tsconfig.json b/code/lib/codemod/tsconfig.json index 3a9315f11d34..0e884ab34b9d 100644 --- a/code/lib/codemod/tsconfig.json +++ b/code/lib/codemod/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "skipLibCheck": true, "allowJs": true, - "strict": false + "strict": true, + "lib": ["ES2021.String"] }, "include": ["src/**/*"], "exclude": ["node_modules", "__testfixtures__", "__tests__"] diff --git a/code/lib/react-dom-shim/src/react-16.tsx b/code/lib/react-dom-shim/src/react-16.tsx index 8d2e60aea9e5..a519dbd51bb6 100644 --- a/code/lib/react-dom-shim/src/react-16.tsx +++ b/code/lib/react-dom-shim/src/react-16.tsx @@ -1,14 +1,13 @@ +/* eslint-disable react/no-deprecated */ import type { ReactElement } from 'react'; import ReactDOM from 'react-dom'; export const renderElement = async (node: ReactElement, el: Element) => { return new Promise((resolve) => { - // eslint-disable-next-line react/no-deprecated ReactDOM.render(node, el, () => resolve(null)); }); }; export const unmountElement = (el: Element) => { - // eslint-disable-next-line react/no-deprecated ReactDOM.unmountComponentAtNode(el); }; diff --git a/code/yarn.lock b/code/yarn.lock index fe60f7899219..c5118b9cd016 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5491,6 +5491,7 @@ __metadata: recast: "npm:^0.23.1" remark: "npm:^14.0.2" remark-mdx: "npm:^2.3.0" + tiny-invariant: "npm:^1.3.1" ts-dedent: "npm:^2.2.0" typescript: "npm:^5.3.2" unist-util-is: "npm:^5.2.0"