Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typescript: Strict compile option for codemods package #23373

Merged
3 changes: 2 additions & 1 deletion code/lib/codemod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 28 additions & 13 deletions code/lib/codemod/src/transforms/csf-2-to-3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) &&
Expand Down Expand Up @@ -92,15 +93,15 @@ 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
// then we are sure the template is unused
if (references.length === 1) {
const reference = references[0];
if (
reference.parentPath.isVariableDeclarator() &&
reference.parentPath?.isVariableDeclarator() &&
reference.parentPath.node.init === templateExpression
) {
reference.parentPath.remove();
Expand All @@ -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 }
Expand All @@ -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;
Expand All @@ -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) => <Cat {...args} />;
Expand All @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -321,7 +336,7 @@ class StorybookImportHelper {
...id,
typeAnnotation: t.tsTypeAnnotation(
t.tsTypeReference(
t.identifier(localTypeImport),
t.identifier(localTypeImport ?? ''),
id.typeAnnotation.typeAnnotation.typeParameters
)
),
Expand Down
34 changes: 18 additions & 16 deletions code/lib/codemod/src/transforms/mdx-to-csf.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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`);

Expand All @@ -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')
Expand All @@ -78,6 +79,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf:

const file = getEsmAst(root);

// @ts-ignore
visit(
root,
['mdxJsxFlowElement', 'mdxJsxTextElement'],
Expand Down Expand Up @@ -134,18 +136,18 @@ export function transform(source: string, baseName: string): [mdx: string, csf:
value: `/* ${nodeString} is deprecated, please migrate it to <Story of={referenceToStory} /> 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. <Story story={Primary} />

const name = storyAttribute.value.value;
const name = storyAttribute.value?.value;
node.attributes = [
{
type: 'mdxJsxAttribute',
Expand All @@ -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];
}
Expand All @@ -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
),
];
}
Expand All @@ -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);
Expand Down Expand Up @@ -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') {
Expand All @@ -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
),
];
}
Expand Down Expand Up @@ -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) }
Expand All @@ -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';`;
Expand Down
3 changes: 2 additions & 1 deletion code/lib/codemod/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"compilerOptions": {
"skipLibCheck": true,
"allowJs": true,
"strict": false
"strict": true,
"lib": ["ES2021.String"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "__testfixtures__", "__tests__"]
Expand Down
3 changes: 1 addition & 2 deletions code/lib/react-dom-shim/src/react-16.tsx
Original file line number Diff line number Diff line change
@@ -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<null>((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);
};
1 change: 1 addition & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down