Skip to content

Commit ec7de9c

Browse files
committed
mostly fix the docs pages
1 parent 9f2dd97 commit ec7de9c

File tree

12 files changed

+394
-126
lines changed

12 files changed

+394
-126
lines changed

eslint.config.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ export default [{
5353
"examples/**/*",
5454
"starters/**/*",
5555
"packages/@react-spectrum/s2/icon.d.ts",
56-
"packages/@react-spectrum/s2/spectrum-illustrations"
56+
"packages/@react-spectrum/s2/spectrum-illustrations",
57+
"packages/dev/parcel-config-storybook/*",
58+
"packages/dev/parcel-resolver-storybook/*",
59+
"packages/dev/parcel-transformer-storybook/*",
60+
"packages/dev/storybook-builder-parcel/*",
61+
"packages/dev/storybook-react-parcel/*"
5762
],
5863
}, ...compat.extends("eslint:recommended"), {
5964
plugins: {

packages/dev/parcel-resolver-storybook/StorybookResolver.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
// @ts-ignore
2-
import {glob, normalizeSeparators, relativePath} from '@parcel/utils';
31
import path from 'path';
4-
import {Resolver} from '@parcel/plugin';
5-
const reactVersion = require('react-dom/package.json').version;
2+
import { Resolver } from "@parcel/plugin";
3+
const reactVersion = require("react-dom/package.json").version;
4+
import { default as NodeResolver } from "@parcel/node-resolver-core";
5+
// @ts-ignore
6+
import { isGlob, glob, normalizeSeparators, relativePath } from '@parcel/utils';
67

78
const REACT_MAJOR_VERSION = parseInt(reactVersion.split('.')[0], 10);
89

910
module.exports = new Resolver({
10-
async resolve({dependency, options, specifier, pipeline}) {
11+
async resolve({ dependency, options, specifier, pipeline, logger }) {
1112
// Workaround for interop issue
12-
if (specifier === 'react-dom/client' && REACT_MAJOR_VERSION < 18) {
13+
if (specifier === "react-dom/client" && REACT_MAJOR_VERSION < 18) {
1314
return {
14-
filePath: __dirname + '/react.js',
15+
filePath: __dirname + "/react.js",
1516
code: `
1617
export * from 'react-dom';
1718
export * as default from 'react-dom'
18-
`
19+
`,
1920
};
2021
}
2122

@@ -25,7 +26,7 @@ module.exports = new Resolver({
2526
let sourceFile = dependency.resolveFrom ?? dependency.sourcePath!;
2627
let normalized = normalizeSeparators(path.resolve(path.dirname(sourceFile), atob(specifier)));
2728
let files = await glob(normalized, options.inputFS, {
28-
onlyFiles: true
29+
onlyFiles: true,
2930
});
3031

3132
let cwd = process.cwd();
@@ -46,8 +47,8 @@ module.exports = new Resolver({
4647
{glob: normalized}
4748
],
4849
pipeline: null,
49-
priority: 'sync'
50+
priority: 'sync',
5051
};
5152
}
52-
}
53+
},
5354
});
Lines changed: 253 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
import {enrichCsf, formatCsf, loadCsf} from '@storybook/csf-tools';
2-
import {getCacheDir} from './react-docgen-typescript';
1+
import { Transformer } from '@parcel/plugin';
2+
import { enrichCsf, formatCsf, babelParse, CsfFile } from '@storybook/csf-tools';
3+
import * as t from '@babel/types';
4+
import {parse} from '@babel/parser';
35
import path from 'path';
6+
import crypto from 'crypto';
7+
import { getClient, getCacheDir } from './react-docgen-typescript';
8+
import {ComponentDoc} from 'react-docgen-typescript';
49
import SourceMap from '@parcel/source-map';
5-
import {Transformer} from '@parcel/plugin';
610

711
module.exports = new Transformer({
812
async transform({asset, options}) {
13+
let docs: ComponentDoc | null = null;
14+
if (asset.type === 'ts' || asset.type === 'tsx') {
15+
let client = await getClient(options);
16+
docs = await client.getDocs(asset.filePath);
17+
}
918
let code = await asset.getCode();
10-
let {code: compiledCode, rawMappings} = processCsf(code, asset.filePath) as any;
19+
let name = options.hmrOptions ? `$parcel$ReactRefresh$${asset.id.slice(-4)}` : null;
20+
let {code: compiledCode, rawMappings} = processCsf(code, asset.filePath, docs, name) as any;
1121

1222
let map = new SourceMap(options.projectRoot);
1323
if (rawMappings) {
@@ -22,13 +32,249 @@ module.exports = new Transformer({
2232
}
2333
});
2434

25-
function processCsf(code: string, filePath: string) {
26-
let csf = loadCsf(code, {
35+
function processCsf(code: string, filePath: string, docs: ComponentDoc | null, refreshName: string | null) {
36+
let ast = parse(code, {
37+
sourceFilename: filePath,
38+
sourceType: 'module',
39+
plugins: ['typescript', 'jsx', 'importAttributes', 'classProperties'],
40+
tokens: true
41+
});
42+
43+
let csf = new CsfFile(ast, {
2744
fileName: filePath,
2845
makeTitle: title => title || 'default'
29-
});
46+
}).parse();
3047
enrichCsf(csf, csf);
3148

49+
// Extract story functions into separate components. This enables React Fast Refresh to work properly.
50+
let count = 0;
51+
let addComponent = (node: t.Function) => {
52+
let name = 'Story' + count++;
53+
csf._ast.program.body.push(t.functionDeclaration(
54+
t.identifier(name),
55+
node.params.map(p => t.cloneNode(p)),
56+
t.isExpression(node.body) ? t.blockStatement([t.returnStatement(node.body)]) : node.body
57+
));
58+
node.body = t.blockStatement([
59+
t.returnStatement(
60+
t.jsxElement(
61+
t.jsxOpeningElement(
62+
t.jsxIdentifier(name),
63+
node.params.length && t.isIdentifier(node.params[0]) ? [t.jsxSpreadAttribute(t.cloneNode(node.params[0]))] : [],
64+
true
65+
),
66+
null,
67+
[],
68+
true
69+
)
70+
)
71+
]);
72+
return name;
73+
};
74+
75+
let handleRenderProperty = (node: t.ObjectExpression) => {
76+
// CSF 3 style object story. Extract render function into a component.
77+
let render = node.properties.find(p => (t.isObjectProperty(p) || t.isObjectMethod(p)) && t.isIdentifier(p.key) && p.key.name === 'render');
78+
if (render?.type === 'ObjectProperty' && t.isFunction(render.value)) {
79+
let c = addComponent(render.value);
80+
node.properties.push(t.objectProperty(t.identifier('_internalComponent'), t.identifier(c)));
81+
} else if (render?.type === 'ObjectMethod') {
82+
let c = addComponent(render);
83+
node.properties.push(t.objectProperty(t.identifier('_internalComponent'), t.identifier(c)));
84+
} else if (t.isObjectProperty(render) && render.value.type === 'Identifier') {
85+
render.value = t.arrowFunctionExpression(
86+
[t.identifier('args')],
87+
t.jsxElement(
88+
t.jsxOpeningElement(
89+
t.jsxIdentifier(render.value.name),
90+
[t.jsxSpreadAttribute(t.identifier('args'))],
91+
true
92+
),
93+
null,
94+
[],
95+
true
96+
)
97+
);
98+
}
99+
};
100+
101+
if (refreshName) {
102+
for (let name in csf._storyExports) {
103+
let node = csf.getStoryExport(name);
104+
105+
// Generate a hash of the args and parameters. If this changes, we bail out of Fast Refresh.
106+
let annotations = csf._storyAnnotations[name];
107+
let storyHash = '';
108+
if (annotations) {
109+
let hash = crypto.createHash('md5');
110+
if (annotations.args) {
111+
hash.update(code.slice(annotations.args.start!, annotations.args.end!));
112+
}
113+
if (annotations.parameters) {
114+
hash.update(code.slice(annotations.parameters.start!, annotations.parameters.end!));
115+
}
116+
storyHash = hash.digest('hex');
117+
}
118+
119+
if (t.isFunction(node)) {
120+
// CSF 2 style function story.
121+
let c = addComponent(node);
122+
csf._ast.program.body.push(t.expressionStatement(
123+
t.assignmentExpression(
124+
'=',
125+
t.memberExpression(t.identifier(name), t.identifier('_internalComponent')),
126+
t.identifier(c)
127+
)
128+
));
129+
130+
if (storyHash) {
131+
csf._ast.program.body.push(t.expressionStatement(
132+
t.assignmentExpression(
133+
'=',
134+
t.memberExpression(t.identifier(name), t.identifier('_hash')),
135+
t.stringLiteral(storyHash)
136+
)
137+
));
138+
}
139+
} else if (node.type === 'ObjectExpression') {
140+
handleRenderProperty(node);
141+
if (storyHash) {
142+
node.properties.push(t.objectProperty(t.identifier('_hash'), t.stringLiteral(storyHash)));
143+
}
144+
}
145+
}
146+
}
147+
148+
// Hash the default export to invalidate Fast Refresh.
149+
if (csf._metaNode?.type === 'ObjectExpression') {
150+
if (docs) {
151+
let component = csf._metaNode.properties.find(p => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === 'component');
152+
if (t.isObjectProperty(component) && t.isExpression(component.value)) {
153+
component.value = t.sequenceExpression([
154+
t.assignmentExpression('=', t.memberExpression(component.value, t.identifier('__docgenInfo')), t.valueToNode(docs)),
155+
component.value
156+
]);
157+
}
158+
}
159+
160+
161+
if (refreshName) {
162+
handleRenderProperty(csf._metaNode);
163+
164+
let hash = crypto.createHash('md5');
165+
hash.update(code.slice(csf._metaNode.start!, csf._metaNode.end!));
166+
hash.update(JSON.stringify(docs));
167+
let metaHash = hash.digest('hex');
168+
csf._metaNode.properties.push(t.objectProperty(t.identifier('_hash'), t.stringLiteral(metaHash)));
169+
}
170+
}
171+
172+
if (refreshName) {
173+
wrapRefresh(csf._ast.program, filePath, refreshName);
174+
}
175+
32176
// @ts-ignore
33177
return formatCsf(csf, {sourceFileName: filePath, sourceMaps: true, importAttributesKeyword: 'with'});
34178
}
179+
180+
function wrapRefresh(program: t.Program, filePath: string, refreshName: string) {
181+
let wrapperPath = `${path.relative(
182+
path.dirname(filePath),
183+
__dirname,
184+
)}/csf-hmr.js`;
185+
186+
// Group imports, exports, and body statements which will be wrapped in a try...catch.
187+
let imports: (t.ImportDeclaration | t.ExportDeclaration)[] = [];
188+
let statements: t.Statement[] = [];
189+
let exportVars: t.VariableDeclarator[] = [];
190+
let exports: t.ExportSpecifier[] = [];
191+
192+
for (let statement of program.body) {
193+
if (t.isImportDeclaration(statement) || t.isExportAllDeclaration(statement)) {
194+
imports.push(statement);
195+
} else if (t.isExportNamedDeclaration(statement)) {
196+
if (statement.exportKind === 'type' || statement.source) {
197+
imports.push(statement);
198+
} else if (statement.declaration) {
199+
statements.push(statement.declaration);
200+
for (let id in t.getOuterBindingIdentifiers(statement.declaration)) {
201+
let name = refreshName + '$Export' + exportVars.length;
202+
exportVars.push(t.variableDeclarator(t.identifier(name)));
203+
exports.push(t.exportSpecifier(t.identifier(name), t.identifier(id)));
204+
statements.push(t.expressionStatement(t.assignmentExpression('=', t.identifier(name), t.identifier(id))));
205+
}
206+
} else if (statement.specifiers) {
207+
for (let specifier of statement.specifiers) {
208+
if (t.isExportSpecifier(specifier)) {
209+
let name = refreshName + '$Export' + exportVars.length;
210+
exportVars.push(t.variableDeclarator(t.identifier(name)));
211+
exports.push(t.exportSpecifier(t.identifier(name), specifier.exported));
212+
statements.push(t.expressionStatement(t.assignmentExpression('=', t.identifier(name), specifier.local)));
213+
}
214+
}
215+
}
216+
} else if (t.isExportDefaultDeclaration(statement)) {
217+
if (t.isExpression(statement.declaration)) {
218+
let name = refreshName + '$Export' + exportVars.length;
219+
exportVars.push(t.variableDeclarator(t.identifier(name)));
220+
exports.push(t.exportSpecifier(t.identifier(name), t.identifier('default')));
221+
statements.push(t.expressionStatement(t.assignmentExpression('=', t.identifier(name), statement.declaration)));
222+
} else {
223+
statements.push(statement.declaration);
224+
let name = refreshName + '$Export' + exportVars.length;
225+
exportVars.push(t.variableDeclarator(t.identifier(name)));
226+
exports.push(t.exportSpecifier(t.identifier(name), t.identifier('default')));
227+
}
228+
} else {
229+
statements.push(statement);
230+
}
231+
}
232+
233+
program.body = [
234+
...imports,
235+
t.importDeclaration(
236+
[t.importNamespaceSpecifier(t.identifier(refreshName + '$Helpers'))],
237+
t.stringLiteral(wrapperPath)
238+
),
239+
t.variableDeclaration('var', exportVars),
240+
t.variableDeclaration('var', [
241+
t.variableDeclarator(t.identifier(refreshName + '$PrevRefreshReg'), t.memberExpression(t.identifier('window'), t.identifier('$RefreshReg$'))),
242+
t.variableDeclarator(t.identifier(refreshName + '$PrevRefreshSig'), t.memberExpression(t.identifier('window'), t.identifier('$RefreshSig$')))
243+
]),
244+
t.expressionStatement(
245+
t.callExpression(
246+
t.memberExpression(t.identifier(refreshName + '$Helpers'), t.identifier('prelude')),
247+
[t.identifier('module')]
248+
)
249+
),
250+
t.tryStatement(
251+
t.blockStatement([
252+
...statements,
253+
t.expressionStatement(
254+
t.callExpression(
255+
t.memberExpression(t.identifier(refreshName + '$Helpers'), t.identifier('postlude')),
256+
[t.identifier('module')]
257+
)
258+
),
259+
]),
260+
null,
261+
t.blockStatement([
262+
t.expressionStatement(
263+
t.assignmentExpression(
264+
'=',
265+
t.memberExpression(t.identifier('window'), t.identifier('$RefreshReg$')),
266+
t.identifier(refreshName + '$PrevRefreshReg')
267+
),
268+
),
269+
t.expressionStatement(
270+
t.assignmentExpression(
271+
'=',
272+
t.memberExpression(t.identifier('window'), t.identifier('$RefreshSig$')),
273+
t.identifier(refreshName + '$PrevRefreshSig')
274+
),
275+
),
276+
])
277+
),
278+
t.exportNamedDeclaration(null, exports)
279+
];
280+
}

packages/dev/parcel-transformer-storybook/csf-hmr.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
'use strict';
1+
"use strict";
22

33
var Refresh = require('react-refresh/runtime');
44
function debounce(func, delay) {

packages/dev/parcel-transformer-storybook/react-docgen-typescript.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import path from 'path';
2+
import ts from 'typescript';
13
import {ComponentDoc, Parser} from 'react-docgen-typescript';
2-
import fs from 'fs';
34
import net from 'net';
4-
import path from 'path';
5+
import fs from 'fs';
56
import type {PluginOptions} from '@parcel/types';
6-
import ts from 'typescript';
77

88
export function getCacheDir(options: PluginOptions) {
99
return path.join(options.projectRoot, 'node_modules', '.cache', 'docgen');
@@ -47,7 +47,7 @@ async function createService(options: PluginOptions) {
4747

4848
socket.on('error', () => {});
4949
});
50-
50+
5151
await new Promise<void>((resolve, reject) => {
5252
let sock = getSocketPath(options);
5353
server.listen(sock, () => resolve());
@@ -68,7 +68,7 @@ async function createService(options: PluginOptions) {
6868
module: ts.ModuleKind.CommonJS,
6969
target: ts.ScriptTarget.Latest
7070
};
71-
71+
7272
let program: ts.Program;
7373
if (options.hmrOptions) {
7474
let host = ts.createWatchCompilerHost(
@@ -134,7 +134,7 @@ function parseStory(program: ts.Program, filePath: string) {
134134
'onFocusChange',
135135
'onScroll'
136136
]);
137-
137+
138138
let parser = new Parser(program, {
139139
shouldExtractLiteralValuesFromEnum: true,
140140
shouldRemoveUndefinedFromOptional: true,
@@ -272,7 +272,6 @@ export async function getClient(options: PluginOptions): Promise<Client> {
272272
try {
273273
await createService(options);
274274
} catch (err) {
275-
// @ts-ignore
276275
if (err.code !== 'EADDRINUSE') {
277276
throw err;
278277
}

0 commit comments

Comments
 (0)