diff --git a/package.json b/package.json index 17699d4f2a2..f9b54b89fa0 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,10 @@ "@parcel/packager-react-static": "^2.16.3", "@parcel/workers": "2.16.4", "@sinclair/typebox": "0.27.10", + "@storybook/addon-a11y": "https://pkg.pr.new/@storybook/addon-a11y@b26f35e", + "@storybook/addon-docs": "https://pkg.pr.new/@storybook/addon-docs@b26f35e", + "@storybook/addon-themes": "https://pkg.pr.new/@storybook/addon-themes@b26f35e", + "@storybook/react": "https://pkg.pr.new/@storybook/react@b26f35e", "@testing-library/user-event": "patch:@testing-library/user-event@npm%3A14.6.5#~/.yarn/patches/@testing-library-user-event-npm-14.6.5-1fcf457da8.patch", "@types/babel__traverse": "7.20.7", "@types/node@npm:*": "^24", @@ -257,6 +261,7 @@ "remark-mdx": "patch:remark-mdx@npm%3A2.0.0-rc.2#~/.yarn/patches/remark-mdx-npm-2.0.0-rc.2-7a71234e1f.patch", "remark-parse": "patch:remark-parse@npm%3A10.0.1#~/.yarn/patches/remark-parse-npm-10.0.1-e654d7df78.patch", "rsc-html-stream": "0.0.8", + "storybook": "https://pkg.pr.new/storybook@b26f35e", "svgo": "^3" }, "packageManager": "yarn@4.18.0", diff --git a/packages/dev/parcel-transformer-storybook/StoryTransformer.ts b/packages/dev/parcel-transformer-storybook/StoryTransformer.ts index c7610789ea3..7e6ba822c65 100644 --- a/packages/dev/parcel-transformer-storybook/StoryTransformer.ts +++ b/packages/dev/parcel-transformer-storybook/StoryTransformer.ts @@ -1,14 +1,31 @@ import {Transformer} from '@parcel/plugin'; -import {enrichCsf, formatCsf, loadCsf} from 'storybook/internal/csf-tools'; -import * as t from '@babel/types'; +import { + type CsfFile, + type CsfSourceMap, + type ESTree, + type ESTreeNode, + type FunctionNode, + type SourceEditor, + appendStatement, + appendToList, + enrichCsf, + formatCsf, + isFunction, + loadCsf, + objectList, + objectProperty, + outerSpan, + removeStatement, + statementSpan, + textOf, + wrapExpression +} from 'storybook/internal/csf-tools'; import path from 'path'; import crypto from 'crypto'; import {getClient, getCacheDir} from './react-docgen-typescript'; import {ComponentDoc} from 'react-docgen-typescript'; import SourceMap from '@parcel/source-map'; -// There's a lot of 'any' in this file. This is because the types in storybook/core are a copy and derivative of the equivalent babel types. - module.exports = new Transformer({ async transform({asset, options}) { let docs: ComponentDoc | null = null; @@ -18,12 +35,10 @@ module.exports = new Transformer({ } let code = await asset.getCode(); let name = options.hmrOptions ? `$parcel$ReactRefresh$${asset.id.slice(-4)}` : null; - let {code: compiledCode, rawMappings} = processCsf(code, asset.filePath, docs, name) as any; + let {code: compiledCode, map: compiledMap} = await processCsf(code, asset.filePath, docs, name); let map = new SourceMap(options.projectRoot); - if (rawMappings) { - map.addIndexedMappings(rawMappings); - } + map.addVLQMap(compiledMap); // Invalidate the asset whenever any types change. asset.invalidateOnFileChange(path.join(getCacheDir(options), 'sentinel')); @@ -33,7 +48,7 @@ module.exports = new Transformer({ } }); -function processCsf( +async function processCsf( code: string, filePath: string, docs: ComponentDoc | null, @@ -43,276 +58,306 @@ function processCsf( fileName: filePath, makeTitle: title => title || 'default' }).parse(); - enrichCsf(csf, csf); + let editor = csf.editor; + // The Fast Refresh wrapper goes first, so statements appended below land after the `;` it ends a + // default export with, and not after an import it moves up. + let closeRefresh = refreshName ? wrapRefresh(editor, filePath, refreshName) : null; + await enrichCsf(csf, csf); - // Extract story functions into separate components. This enables React Fast Refresh to work properly. - let count = 0; - let addComponent = (node: t.Function) => { - let name = 'Story' + count++; - csf._ast.program.body.push( - t.functionDeclaration( - t.identifier(name), - node.params.map(p => t.cloneNode(p)), - t.isExpression(node.body) ? t.blockStatement([t.returnStatement(node.body)]) : node.body - ) as any - ); - node.body = t.blockStatement([ - t.returnStatement( - t.jsxElement( - t.jsxOpeningElement( - t.jsxIdentifier(name), - node.params.length && t.isIdentifier(node.params[0]) - ? [t.jsxSpreadAttribute(t.cloneNode(node.params[0]))] - : [], - true - ), - null, - [], - true - ) - ) - ]); + // Story functions become components, so that React Fast Refresh can update them. Their code stays + // where it is, and so does its source map. + let components = new Map(); + // Components named inside an expression, which need a `var`. + let variables: string[] = []; + let addComponent = (fn: FunctionNode) => { + // A template bound by several stories becomes one component. + let name = components.get(fn); + if (name) { + return name; + } + name = 'Story' + components.size; + components.set(fn, name); + let [first] = fn.params; + let params = first ? editor.code.slice(first.start, fn.params.at(-1)!.end) : ''; + let props = first?.type === 'Identifier' ? ` {...${first.name}}` : ''; + let element = `<${name}${props} />`; + let parent = editor.parentOf(fn); + if (fn.type === 'FunctionDeclaration' && fn.id) { + // `function Primary(args) {…}` becomes a wrapper, followed by `function Story0(args) {…}`. + editor.edits.appendRight( + fn.start, + `function ${fn.id.name}(${params}) {\n return ${element};\n}\n` + ); + editor.edits.update(fn.id.start, fn.id.end, name); + } else if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') { + // `const Primary: Story = (args) => …` becomes + // `const Primary = (args) => , Story0: Story = (args) => …`. + let {id} = parent; + editor.edits.update( + id.start, + id.start + id.name.length, + `${id.name} = (${params}) => ${element}, ${name}` + ); + } else { + // `render: (args) => …` becomes `render: (Story0 = (args) => …, (args) => )`. + variables.push(name); + wrapExpression(editor, fn, `(${name} = `, `, (${params}) => ${element})`); + } return name; }; - let handleRenderProperty = (node: t.ObjectExpression) => { - // CSF 3 style object story. Extract render function into a component. - let render = node.properties.find( - p => - (t.isObjectProperty(p) || t.isObjectMethod(p)) && - t.isIdentifier(p.key) && - p.key.name === 'render' - ); - if (render?.type === 'ObjectProperty' && t.isFunction(render.value)) { - let c = addComponent(render.value); - node.properties.push(t.objectProperty(t.identifier('_internalComponent'), t.identifier(c))); - } else if (render?.type === 'ObjectMethod') { - let c = addComponent(render); - node.properties.push(t.objectProperty(t.identifier('_internalComponent'), t.identifier(c))); - } else if (t.isObjectProperty(render) && render.value.type === 'Identifier') { - render.value = t.arrowFunctionExpression( - [t.identifier('args')], - t.jsxElement( - t.jsxOpeningElement( - t.jsxIdentifier(render.value.name), - [t.jsxSpreadAttribute(t.identifier('args'))], - true - ), - null, - [], - true - ) - ); + // The component a CSF 3 `render` function becomes. `render: Button` becomes + // `render: args =>