Merge pull request #23373 from aditya1/fix/convert-to-strict-ts-codemod-pkg

Typescript: Strict compile option for `codemods` package
This commit is contained in:
Norbert de Langen 2023-11-29 12:49:22 +01:00 committed by GitHub
commit f998cc921b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 33 deletions

View File

@ -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",

View File

@ -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) => <Cat {...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
)
),

View File

@ -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 <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',
@ -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';`;

View File

@ -3,7 +3,8 @@
"compilerOptions": {
"skipLibCheck": true,
"allowJs": true,
"strict": false
"strict": true,
"lib": ["ES2021.String"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "__testfixtures__", "__tests__"]

View File

@ -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);
};

View File

@ -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"