From f2064fbdea162c73b680927c03277a180c2a026d Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 22 Feb 2024 15:44:55 +0100 Subject: [PATCH 1/6] Rename transformed mdx files --- code/lib/codemod/src/index.ts | 5 ----- code/lib/codemod/src/transforms/mdx-to-csf.ts | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/code/lib/codemod/src/index.ts b/code/lib/codemod/src/index.ts index 0bc3f4d1b3db..e458e8ed9906 100644 --- a/code/lib/codemod/src/index.ts +++ b/code/lib/codemod/src/index.ts @@ -93,11 +93,6 @@ export async function runCodemod(codemod: any, { glob, logger, dryRun, rename, p } } - if (!renameParts && codemod === 'mdx-to-csf') { - renameParts = ['.stories.mdx', '.mdx']; - rename = '.stories.mdx:.mdx;'; - } - if (renameParts) { const [from, to] = renameParts; logger.log(`=> Renaming ${rename}: ${files.length} files`); diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index 9c657c822e04..d7751965f673 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -24,6 +24,8 @@ import type { MdxFlowExpression } from 'mdast-util-mdx-expression'; const mdxProcessor = remark().use(remarkMdx) as ReturnType; +const renameList: { original: string; baseName: string }[] = []; + export default async function jscodeshift(info: FileInfo) { const parsed = path.parse(info.path); @@ -39,15 +41,20 @@ export default async function jscodeshift(info: FileInfo) { const result = await transform(info, path.basename(baseName)); - const [mdx, csf] = result; - - if (csf != null) { - fs.writeFileSync(`${baseName}.stories.js`, csf); + if (result[1] != null) { + fs.writeFileSync(`${baseName}.stories.js`, result[1]); + renameList.push({ original: info.path, baseName }); } - return mdx; + return result[0]; } +process.on('exit', () => { + renameList.forEach((file) => { + fs.renameSync(file.original, `${file.baseName}.mdx`); + }); +}); + export async function transform(info: FileInfo, baseName: string): Promise<[string, string]> { const root = mdxProcessor.parse(info.source); const storyNamespaceName = nameToValidExport(`${baseName}Stories`); From 55a7c97bc98e79f8613fccb007b66c47f992f290 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 22 Feb 2024 16:00:52 +0100 Subject: [PATCH 2/6] Fix issues where stories.js files were generated although a story doesn't exist --- .../transforms/__tests__/mdx-to-csf.test.ts | 44 ++++++++ code/lib/codemod/src/transforms/mdx-to-csf.ts | 101 ++++++++++++------ 2 files changed, 115 insertions(+), 30 deletions(-) diff --git a/code/lib/codemod/src/transforms/__tests__/mdx-to-csf.test.ts b/code/lib/codemod/src/transforms/__tests__/mdx-to-csf.test.ts index 1be65c569edc..36db68148a31 100644 --- a/code/lib/codemod/src/transforms/__tests__/mdx-to-csf.test.ts +++ b/code/lib/codemod/src/transforms/__tests__/mdx-to-csf.test.ts @@ -598,6 +598,50 @@ it('story child is identifier', async () => { `); }); +it('should replace ArgsTable by Controls', async () => { + const input = dedent` + import { ArgsTable } from '@storybook/addon-docs/blocks'; + import { Button } from './button'; + + Dummy Code + + + `; + + const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' }); + + expect(mdx).toMatchInlineSnapshot(` + import { Controls } from '@storybook/blocks'; + import { Button } from './button'; + + Dummy Code + + + `); +}); + +it('should not create stories.js file if there are no components', async () => { + const input = dedent` + import { Meta } from '@storybook/addon-docs'; + + + + # Welcome to Storybook + `; + + const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' }); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + + expect(mdx).toMatchInlineSnapshot(` + import { Meta } from '@storybook/blocks'; + + + + # Welcome to Storybook + `); +}); + it('nameToValidExport', () => { expect(nameToValidExport('1 starts with digit')).toMatchInlineSnapshot(`$1StartsWithDigit`); expect(nameToValidExport('name')).toMatchInlineSnapshot(`Name`); diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index d7751965f673..2997a2fa89e0 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -55,7 +55,10 @@ process.on('exit', () => { }); }); -export async function transform(info: FileInfo, baseName: string): Promise<[string, string]> { +export async function transform( + info: FileInfo, + baseName: string +): Promise<[string, string | null]> { const root = mdxProcessor.parse(info.source); const storyNamespaceName = nameToValidExport(`${baseName}Stories`); @@ -81,25 +84,42 @@ export async function transform(info: FileInfo, baseName: string): Promise<[stri node.value = node.value .replaceAll('@storybook/addon-docs/blocks', '@storybook/blocks') .replaceAll('@storybook/addon-docs', '@storybook/blocks'); + + if (node.value.includes('@storybook/blocks')) { + // @ts-ignore + const file: BabelFile = new babel.File( + { filename: 'info.path' }, + { code: node.value, ast: babelParse(node.value) } + ); + + file.path.traverse({ + ImportDeclaration(path) { + if (path.node.source.value === '@storybook/blocks') { + path.get('specifiers').forEach((specifier) => { + if (specifier.isImportSpecifier()) { + const imported = specifier.get('imported'); + if (imported.isIdentifier() && imported.node.name === 'ArgsTable') { + imported.node.name = 'Controls'; + } + } + }); + } + }, + }); + + node.value = recast.print(file.ast).code; + } }); const file = getEsmAst(root); visit(root, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node, index, parent) => { if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') { - if (is(node, { name: 'Meta' })) { - metaAttributes.push(...node.attributes); - node.attributes = [ - { - type: 'mdxJsxAttribute', - name: 'of', - value: { - type: 'mdxJsxAttributeValueExpression', - value: storyNamespaceName, - }, - }, - ]; + if (is(node, { name: 'ArgsTable' })) { + node.name = 'Controls'; + node.attributes = []; } + if (is(node, { name: 'Story' })) { const nameAttribute = node.attributes.find( (it) => it.type === 'mdxJsxAttribute' && it.name === 'name' @@ -174,21 +194,6 @@ export async function transform(info: FileInfo, baseName: string): Promise<[stri return undefined; }); - const metaProperties = metaAttributes.flatMap((attribute) => { - if (attribute.type === 'mdxJsxAttribute') { - if (typeof attribute.value === 'string') { - return [t.objectProperty(t.identifier(attribute.name), t.stringLiteral(attribute.value))]; - } - return [ - t.objectProperty( - t.identifier(attribute.name), - babelParseExpression(attribute.value?.value ?? '') as any as t.Expression - ), - ]; - } - return []; - }); - file.path.traverse({ // remove mdx imports from csf ImportDeclaration(path) { @@ -203,11 +208,47 @@ export async function transform(info: FileInfo, baseName: string): Promise<[stri }, }); - if (storiesMap.size === 0 && metaAttributes.length === 0) { + if (storiesMap.size === 0) { // A CSF file must have at least one story, so skip migrating if this is the case. - return [mdxProcessor.stringify(root), '']; + return [mdxProcessor.stringify(root), null]; } + // Rewrites the Meta tag to use the new story namespace + visit(root, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node, index, parent) => { + if ( + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + is(node, { name: 'Meta' }) + ) { + metaAttributes.push(...node.attributes); + console.log({ storyNamespaceName }); + node.attributes = [ + { + type: 'mdxJsxAttribute', + name: 'of', + value: { + type: 'mdxJsxAttributeValueExpression', + value: storyNamespaceName, + }, + }, + ]; + } + }); + + const metaProperties = metaAttributes.flatMap((attribute) => { + if (attribute.type === 'mdxJsxAttribute') { + if (typeof attribute.value === 'string') { + return [t.objectProperty(t.identifier(attribute.name), t.stringLiteral(attribute.value))]; + } + return [ + t.objectProperty( + t.identifier(attribute.name), + babelParseExpression(attribute.value?.value ?? '') as any as t.Expression + ), + ]; + } + return []; + }); + addStoriesImport(root, baseName, storyNamespaceName); const newStatements: t.Statement[] = [ From f0a899d8c864a79d52d3bbea7944058c759a513e Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 23 Feb 2024 08:50:46 +0100 Subject: [PATCH 3/6] Upgrade migration-guide to mention limitations of mdx-to-csf codemod --- docs/migration-guide.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/migration-guide.md b/docs/migration-guide.md index 1fa7d7c5ad4c..ce5c0d17404c 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -145,6 +145,10 @@ Storybook now requires that MDX pages reference stories written in CSF, rather t You’ll also need to update your stories glob in `.storybook/main.js` to include the newly created .mdx and .stories.js files if it doesn’t already. +#### Known limitations + +- The codemod does not remove the extracted stories from the `.stories.mdx` files. You will need to do this manually. + **Note:** this migration supports the Storybook 6 ["CSF stories with MDX docs"](https://github.com/storybookjs/storybook/blob/6e19f0fe426d58f0f7981a42c3d0b0384fab49b1/code/addons/docs/docs/recipes.md#csf-stories-with-mdx-docs) recipe. ## Troubleshooting From fd8c625521bf9c976a25c6763d824a7b46436cd3 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 23 Feb 2024 10:15:58 +0100 Subject: [PATCH 4/6] Rename broken MDX files to .mdx.broken --- code/lib/codemod/src/index.ts | 7 ++++++- code/lib/codemod/src/transforms/mdx-to-csf.ts | 21 +++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/code/lib/codemod/src/index.ts b/code/lib/codemod/src/index.ts index e458e8ed9906..67eb20799190 100644 --- a/code/lib/codemod/src/index.ts +++ b/code/lib/codemod/src/index.ts @@ -87,7 +87,12 @@ export async function runCodemod(codemod: any, { glob, logger, dryRun, rename, p shell: true, } ); - if (result.status === 1) { + + if (codemod === 'mdx-to-csf' && result.status === 1) { + logger.log( + 'The codemod was not able to transform the files mentioned above. We have renamed the files to .mdx.broken. Please check the files and rename them back to .mdx after you have either manually transformed them to mdx + csf or fixed the issues so that the codemod can transform them.' + ); + } else if (result.status === 1) { logger.log('Skipped renaming because of errors.'); return; } diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index 2997a2fa89e0..eca57e8a8d7e 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -25,6 +25,7 @@ import type { MdxFlowExpression } from 'mdast-util-mdx-expression'; const mdxProcessor = remark().use(remarkMdx) as ReturnType; const renameList: { original: string; baseName: string }[] = []; +const brokenList: { original: string; baseName: string }[] = []; export default async function jscodeshift(info: FileInfo) { const parsed = path.parse(info.path); @@ -39,20 +40,29 @@ export default async function jscodeshift(info: FileInfo) { baseName += '_'; } - const result = await transform(info, path.basename(baseName)); + try { + const result = await transform(info, path.basename(baseName)); + + if (result[1] != null) { + fs.writeFileSync(`${baseName}.stories.js`, result[1]); + } - if (result[1] != null) { - fs.writeFileSync(`${baseName}.stories.js`, result[1]); renameList.push({ original: info.path, baseName }); - } - return result[0]; + return result[0]; + } catch (e) { + brokenList.push({ original: info.path, baseName }); + throw e; + } } process.on('exit', () => { renameList.forEach((file) => { fs.renameSync(file.original, `${file.baseName}.mdx`); }); + brokenList.forEach((file) => { + fs.renameSync(file.original, `${file.original}.broken`); + }); }); export async function transform( @@ -220,7 +230,6 @@ export async function transform( is(node, { name: 'Meta' }) ) { metaAttributes.push(...node.attributes); - console.log({ storyNamespaceName }); node.attributes = [ { type: 'mdxJsxAttribute', From 14440f17926823789d0fdafdc4bc9e377411a27e Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 23 Feb 2024 12:56:29 +0100 Subject: [PATCH 5/6] Refactor MDX to CSF transform function to return an object instead of a tuple --- code/lib/codemod/src/transforms/mdx-to-csf.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index eca57e8a8d7e..ed95f433a2ce 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -41,15 +41,15 @@ export default async function jscodeshift(info: FileInfo) { } try { - const result = await transform(info, path.basename(baseName)); + const { csf, mdx } = await transform(info, path.basename(baseName)); - if (result[1] != null) { - fs.writeFileSync(`${baseName}.stories.js`, result[1]); + if (csf != null) { + fs.writeFileSync(`${baseName}.stories.js`, csf); } renameList.push({ original: info.path, baseName }); - return result[0]; + return mdx; } catch (e) { brokenList.push({ original: info.path, baseName }); throw e; @@ -68,7 +68,7 @@ process.on('exit', () => { export async function transform( info: FileInfo, baseName: string -): Promise<[string, string | null]> { +): Promise<{ mdx: string; csf: string | null }> { const root = mdxProcessor.parse(info.source); const storyNamespaceName = nameToValidExport(`${baseName}Stories`); @@ -220,7 +220,10 @@ export async function transform( if (storiesMap.size === 0) { // A CSF file must have at least one story, so skip migrating if this is the case. - return [mdxProcessor.stringify(root), null]; + return { + csf: null, + mdx: mdxProcessor.stringify(root), + }; } // Rewrites the Meta tag to use the new story namespace @@ -354,7 +357,10 @@ export async function transform( filepath: path, }); - return [newMdx, output]; + return { + csf: output, + mdx: newMdx, + }; } function getEsmAst(root: ReturnType) { From 45caf9f70649144485a1d9e911665ab687ee373f Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 23 Feb 2024 13:57:57 +0100 Subject: [PATCH 6/6] Add todo comment --- code/lib/codemod/src/transforms/mdx-to-csf.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index ed95f433a2ce..765331e40b2e 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -56,6 +56,8 @@ export default async function jscodeshift(info: FileInfo) { } } +// The JSCodeshift CLI doesn't return a list of files that were transformed or skipped. +// This is a workaround to rename the files after the transformation, which we can remove after we switch from jscodeshift to another solution. process.on('exit', () => { renameList.forEach((file) => { fs.renameSync(file.original, `${file.baseName}.mdx`);