Skip to content

Commit 075f911

Browse files
authored
Merge pull request #26254 from storybookjs/norbert/fix-react-docgen-vite-plugin
ReactVite: Docgen ignore un-parsable files
2 parents c9d7eb1 + 70ee50f commit 075f911

File tree

9 files changed

+197
-11
lines changed

9 files changed

+197
-11
lines changed

code/frameworks/react-vite/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
"@storybook/builder-vite": "workspace:*",
5353
"@storybook/react": "workspace:*",
5454
"magic-string": "^0.30.0",
55-
"react-docgen": "^7.0.0"
55+
"react-docgen": "^7.0.0",
56+
"resolve": "^1.22.8"
5657
},
5758
"devDependencies": {
5859
"@types/node": "^18.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { extname } from 'path';
2+
import resolve from 'resolve';
3+
4+
export class ReactDocgenResolveError extends Error {
5+
// the magic string that react-docgen uses to check if a module is ignored
6+
readonly code = 'MODULE_NOT_FOUND';
7+
8+
constructor(filename: string) {
9+
super(`'${filename}' was ignored by react-docgen.`);
10+
}
11+
}
12+
13+
/* The below code was copied from:
14+
* https://github.com/reactjs/react-docgen/blob/df2daa8b6f0af693ecc3c4dc49f2246f60552bcb/packages/react-docgen/src/importer/makeFsImporter.ts#L14-L63
15+
* because it wasn't exported from the react-docgen package.
16+
* watch out: when updating this code, also update the code in code/presets/react-webpack/src/loaders/docgen-resolver.ts
17+
*/
18+
19+
// These extensions are sorted by priority
20+
// resolve() will check for files in the order these extensions are sorted
21+
export const RESOLVE_EXTENSIONS = [
22+
'.js',
23+
'.cts', // These were originally not in the code, I added them
24+
'.mts', // These were originally not in the code, I added them
25+
'.ctsx', // These were originally not in the code, I added them
26+
'.mtsx', // These were originally not in the code, I added them
27+
'.ts',
28+
'.tsx',
29+
'.mjs',
30+
'.cjs',
31+
'.mts',
32+
'.cts',
33+
'.jsx',
34+
];
35+
36+
export function defaultLookupModule(filename: string, basedir: string): string {
37+
const resolveOptions = {
38+
basedir,
39+
extensions: RESOLVE_EXTENSIONS,
40+
// we do not need to check core modules as we cannot import them anyway
41+
includeCoreModules: false,
42+
};
43+
44+
try {
45+
return resolve.sync(filename, resolveOptions);
46+
} catch (error) {
47+
const ext = extname(filename);
48+
let newFilename: string;
49+
50+
// if we try to import a JavaScript file it might be that we are actually pointing to
51+
// a TypeScript file. This can happen in ES modules as TypeScript requires to import other
52+
// TypeScript files with .js extensions
53+
// https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
54+
switch (ext) {
55+
case '.js':
56+
case '.mjs':
57+
case '.cjs':
58+
newFilename = `${filename.slice(0, -2)}ts`;
59+
break;
60+
61+
case '.jsx':
62+
newFilename = `${filename.slice(0, -3)}tsx`;
63+
break;
64+
default:
65+
throw error;
66+
}
67+
68+
return resolve.sync(newFilename, {
69+
...resolveOptions,
70+
// we already know that there is an extension at this point, so no need to check other extensions
71+
extensions: [],
72+
});
73+
}
74+
}

code/frameworks/react-vite/src/plugins/react-docgen.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ import {
66
parse,
77
builtinHandlers as docgenHandlers,
88
builtinResolvers as docgenResolver,
9-
builtinImporters as docgenImporters,
9+
makeFsImporter,
1010
} from 'react-docgen';
1111
import MagicString from 'magic-string';
1212
import type { PluginOption } from 'vite';
1313
import actualNameHandler from './docgen-handlers/actualNameHandler';
14+
import {
15+
RESOLVE_EXTENSIONS,
16+
ReactDocgenResolveError,
17+
defaultLookupModule,
18+
} from './docgen-resolver';
1419

1520
type DocObj = Documentation & { actualName: string };
1621

1722
// TODO: None of these are able to be overridden, so `default` is aspirational here.
1823
const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler);
1924
const defaultResolver = new docgenResolver.FindExportedDefinitionsResolver();
20-
const defaultImporter = docgenImporters.fsImporter;
2125
const handlers = [...defaultHandlers, actualNameHandler];
2226

2327
type Options = {
@@ -36,14 +40,23 @@ export function reactDocgen({
3640
name: 'storybook:react-docgen-plugin',
3741
enforce: 'pre',
3842
async transform(src: string, id: string) {
39-
const relPath = path.relative(cwd, id);
40-
if (!filter(relPath)) return;
43+
if (!filter(path.relative(cwd, id))) {
44+
return;
45+
}
4146

4247
try {
4348
const docgenResults = parse(src, {
4449
resolver: defaultResolver,
4550
handlers,
46-
importer: defaultImporter,
51+
importer: makeFsImporter((filename, basedir) => {
52+
const result = defaultLookupModule(filename, basedir);
53+
54+
if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) {
55+
return result;
56+
}
57+
58+
throw new ReactDocgenResolveError(filename);
59+
}),
4760
filename: id,
4861
}) as DocObj[];
4962
const s = new MagicString(src);

code/presets/react-webpack/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"fs-extra": "^11.1.0",
7575
"magic-string": "^0.30.5",
7676
"react-docgen": "^7.0.0",
77+
"resolve": "^1.22.8",
7778
"semver": "^7.3.7",
7879
"webpack": "5"
7980
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { extname } from 'path';
2+
import resolve from 'resolve';
3+
4+
export class ReactDocgenResolveError extends Error {
5+
// the magic string that react-docgen uses to check if a module is ignored
6+
readonly code = 'MODULE_NOT_FOUND';
7+
8+
constructor(filename: string) {
9+
super(`'${filename}' was ignored by react-docgen.`);
10+
}
11+
}
12+
13+
/* The below code was copied from:
14+
* https://github.com/reactjs/react-docgen/blob/df2daa8b6f0af693ecc3c4dc49f2246f60552bcb/packages/react-docgen/src/importer/makeFsImporter.ts#L14-L63
15+
* because it wasn't exported from the react-docgen package.
16+
* watch out: when updating this code, also update the code in code/frameworks/react-vite/src/plugins/docgen-resolver.ts
17+
*/
18+
19+
// These extensions are sorted by priority
20+
// resolve() will check for files in the order these extensions are sorted
21+
export const RESOLVE_EXTENSIONS = [
22+
'.js',
23+
'.cts', // These were originally not in the code, I added them
24+
'.mts', // These were originally not in the code, I added them
25+
'.ctsx', // These were originally not in the code, I added them
26+
'.mtsx', // These were originally not in the code, I added them
27+
'.ts',
28+
'.tsx',
29+
'.mjs',
30+
'.cjs',
31+
'.mts',
32+
'.cts',
33+
'.jsx',
34+
];
35+
36+
export function defaultLookupModule(filename: string, basedir: string): string {
37+
const resolveOptions = {
38+
basedir,
39+
extensions: RESOLVE_EXTENSIONS,
40+
// we do not need to check core modules as we cannot import them anyway
41+
includeCoreModules: false,
42+
};
43+
44+
try {
45+
return resolve.sync(filename, resolveOptions);
46+
} catch (error) {
47+
const ext = extname(filename);
48+
let newFilename: string;
49+
50+
// if we try to import a JavaScript file it might be that we are actually pointing to
51+
// a TypeScript file. This can happen in ES modules as TypeScript requires to import other
52+
// TypeScript files with .js extensions
53+
// https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
54+
switch (ext) {
55+
case '.js':
56+
case '.mjs':
57+
case '.cjs':
58+
newFilename = `${filename.slice(0, -2)}ts`;
59+
break;
60+
61+
case '.jsx':
62+
newFilename = `${filename.slice(0, -3)}tsx`;
63+
break;
64+
default:
65+
throw error;
66+
}
67+
68+
return resolve.sync(newFilename, {
69+
...resolveOptions,
70+
// we already know that there is an extension at this point, so no need to check other extensions
71+
extensions: [],
72+
});
73+
}
74+
}

code/presets/react-webpack/src/loaders/react-docgen-loader.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
parse,
33
builtinResolvers as docgenResolver,
44
builtinHandlers as docgenHandlers,
5-
builtinImporters as docgenImporters,
5+
makeFsImporter,
66
ERROR_CODES,
77
utils,
88
} from 'react-docgen';
@@ -11,6 +11,12 @@ import type { LoaderContext } from 'webpack';
1111
import type { Handler, NodePath, babelTypes as t, Documentation } from 'react-docgen';
1212
import { logger } from '@storybook/node-logger';
1313

14+
import {
15+
RESOLVE_EXTENSIONS,
16+
ReactDocgenResolveError,
17+
defaultLookupModule,
18+
} from './docgen-resolver';
19+
1420
const { getNameOrValue, isReactForwardRefCall } = utils;
1521

1622
const actualNameHandler: Handler = function actualNameHandler(documentation, componentDefinition) {
@@ -54,7 +60,6 @@ type DocObj = Documentation & { actualName: string };
5460

5561
const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler);
5662
const defaultResolver = new docgenResolver.FindExportedDefinitionsResolver();
57-
const defaultImporter = docgenImporters.fsImporter;
5863
const handlers = [...defaultHandlers, actualNameHandler];
5964

6065
export default async function reactDocgenLoader(
@@ -71,7 +76,15 @@ export default async function reactDocgenLoader(
7176
filename: this.resourcePath,
7277
resolver: defaultResolver,
7378
handlers,
74-
importer: defaultImporter,
79+
importer: makeFsImporter((filename, basedir) => {
80+
const result = defaultLookupModule(filename, basedir);
81+
82+
if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) {
83+
return result;
84+
}
85+
86+
throw new ReactDocgenResolveError(filename);
87+
}),
7588
babelOptions: {
7689
babelrc: false,
7790
configFile: false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.foo {
2+
color: red;
3+
}

code/renderers/react/template/stories/docgen-components/ts-function-component/input.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React from 'react';
22

33
import { imported } from '../imported';
4+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+
// @ts-ignore (css import not supported in TS)
6+
import styles from '../imported.module.css';
47

58
const local = 'local-value';
69

@@ -26,6 +29,7 @@ interface PropsWriterProps {
2629
importedReference?: string;
2730
globalReference?: any;
2831
stringGlobalName?: string;
32+
myClass: typeof styles.foo;
2933
}
3034

3135
/**
@@ -47,6 +51,7 @@ PropsWriter.defaultProps = {
4751
importedReference: imported,
4852
globalReference: Date,
4953
stringGlobalName: 'top',
54+
myClass: styles.foo,
5055
};
5156

5257
export const component = PropsWriter;

code/yarn.lock

+4-2
Original file line numberDiff line numberDiff line change
@@ -6343,6 +6343,7 @@ __metadata:
63436343
fs-extra: "npm:^11.1.0"
63446344
magic-string: "npm:^0.30.5"
63456345
react-docgen: "npm:^7.0.0"
6346+
resolve: "npm:^1.22.8"
63466347
semver: "npm:^7.3.7"
63476348
typescript: "npm:^5.3.2"
63486349
webpack: "npm:5"
@@ -6491,6 +6492,7 @@ __metadata:
64916492
"@types/node": "npm:^18.0.0"
64926493
magic-string: "npm:^0.30.0"
64936494
react-docgen: "npm:^7.0.0"
6495+
resolve: "npm:^1.22.8"
64946496
typescript: "npm:^5.3.2"
64956497
vite: "npm:^4.0.0"
64966498
peerDependencies:
@@ -25657,7 +25659,7 @@ __metadata:
2565725659
languageName: node
2565825660
linkType: hard
2565925661

25660-
"resolve@npm:1.22.8, resolve@npm:^1.10.0, resolve@npm:^1.13.1, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.4.0":
25662+
"resolve@npm:1.22.8, resolve@npm:^1.10.0, resolve@npm:^1.13.1, resolve@npm:^1.14.2, resolve@npm:^1.15.1, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:^1.4.0":
2566125663
version: 1.22.8
2566225664
resolution: "resolve@npm:1.22.8"
2566325665
dependencies:
@@ -25683,7 +25685,7 @@ __metadata:
2568325685
languageName: node
2568425686
linkType: hard
2568525687

25686-
"resolve@patch:resolve@npm%3A1.22.8#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.13.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.4.0#optional!builtin<compat/resolve>":
25688+
"resolve@patch:resolve@npm%3A1.22.8#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.13.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin<compat/resolve>, resolve@patch:resolve@npm%3A^1.4.0#optional!builtin<compat/resolve>":
2568725689
version: 1.22.8
2568825690
resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin<compat/resolve>::version=1.22.8&hash=c3c19d"
2568925691
dependencies:

0 commit comments

Comments
 (0)