Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactVite: Docgen ignore un-parsable files #26254

Merged
merged 14 commits into from
Mar 1, 2024
3 changes: 2 additions & 1 deletion code/frameworks/react-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
"@storybook/builder-vite": "workspace:*",
"@storybook/react": "workspace:*",
"magic-string": "^0.30.0",
"react-docgen": "^7.0.0"
"react-docgen": "^7.0.0",
"resolve": "^1.22.8"
},
"devDependencies": {
"@types/node": "^18.0.0",
Expand Down
60 changes: 60 additions & 0 deletions code/frameworks/react-vite/src/plugins/docgen-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { extname } from 'path';
import resolve from 'resolve';

export class ReactDocgenResolveError extends Error {
// the magic string that react-docgen uses to check if a module is ignored
readonly code = 'MODULE_NOT_FOUND';

constructor(filename: string) {
super(`'${filename}' was ignored by react-docgen.`);
}
}

/* The below code was copied from:
* https://github.com/reactjs/react-docgen/blob/df2daa8b6f0af693ecc3c4dc49f2246f60552bcb/packages/react-docgen/src/importer/makeFsImporter.ts#L14-L63
* because it wasn't exported from the react-docgen package.
*/

// These extensions are sorted by priority
// resolve() will check for files in the order these extensions are sorted
const RESOLVE_EXTENSIONS = ['.js', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts', '.jsx'];

export function defaultLookupModule(filename: string, basedir: string): string {
const resolveOptions = {
basedir,
extensions: RESOLVE_EXTENSIONS,
// we do not need to check core modules as we cannot import them anyway
includeCoreModules: false,
};

try {
return resolve.sync(filename, resolveOptions);
} catch (error) {
const ext = extname(filename);
let newFilename: string;

// if we try to import a JavaScript file it might be that we are actually pointing to
// a TypeScript file. This can happen in ES modules as TypeScript requires to import other
// TypeScript files with .js extensions
// https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
switch (ext) {
case '.js':
case '.mjs':
case '.cjs':
newFilename = `${filename.slice(0, -2)}ts`;
break;

case '.jsx':
newFilename = `${filename.slice(0, -3)}tsx`;
break;
default:
throw error;
}

return resolve.sync(newFilename, {
...resolveOptions,
// we already know that there is an extension at this point, so no need to check other extensions
extensions: [],
});
}
}
19 changes: 14 additions & 5 deletions code/frameworks/react-vite/src/plugins/react-docgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import {
parse,
builtinHandlers as docgenHandlers,
builtinResolvers as docgenResolver,
builtinImporters as docgenImporters,
makeFsImporter,
} from 'react-docgen';
import MagicString from 'magic-string';
import type { PluginOption } from 'vite';
import actualNameHandler from './docgen-handlers/actualNameHandler';
import { ReactDocgenResolveError, defaultLookupModule } from './docgen-resolver';

type DocObj = Documentation & { actualName: string };

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

type Options = {
Expand All @@ -36,14 +36,23 @@ export function reactDocgen({
name: 'storybook:react-docgen-plugin',
enforce: 'pre',
async transform(src: string, id: string) {
const relPath = path.relative(cwd, id);
if (!filter(relPath)) return;
if (!filter(path.relative(cwd, id))) {
return;
}

try {
const docgenResults = parse(src, {
resolver: defaultResolver,
handlers,
importer: defaultImporter,
importer: makeFsImporter((filename, basedir) => {
const result = defaultLookupModule(filename, basedir);

if (!result.match(/\.(mjs|tsx?|jsx?)$/)) {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
throw new ReactDocgenResolveError(filename);
}

return result;
}),
filename: id,
}) as DocObj[];
const s = new MagicString(src);
Expand Down
18 changes: 15 additions & 3 deletions code/presets/react-webpack/src/loaders/react-docgen-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
parse,
builtinResolvers as docgenResolver,
builtinHandlers as docgenHandlers,
builtinImporters as docgenImporters,
makeFsImporter,
ERROR_CODES,
utils,
} from 'react-docgen';
Expand All @@ -11,6 +11,11 @@ import type { LoaderContext } from 'webpack';
import type { Handler, NodePath, babelTypes as t, Documentation } from 'react-docgen';
import { logger } from '@storybook/node-logger';

import {
ReactDocgenResolveError,
defaultLookupModule,
} from '../../../../frameworks/react-vite/src/plugins/docgen-resolver';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything we can do to clean things up? cc @valentinpalkovic

Copy link
Member Author

@ndelangen ndelangen Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this on purpose.

The only 2 places in our codebase that currently depend on react-docgen are:
Screenshot 2024-02-29 at 17 49 37

So in order to extract this somewhere else, I'd need to introduce the dependency to some shared package.
I understand this might look a bit weird, but it's bundled correctly.

I'm open to suggestions that do not create a new package.

Alternatively (I'm not a fan of this idea either) we could add this code to docs-tools;
but then docs tools gains a dependency to react-docgen, and with it, this:
Screenshot 2024-02-29 at 17 53 04

If you feel like this is so bad we're better off duplicating the code, we could do that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this is kind of ugly (requiring files from the react-vite framework into the react-webpack preset things will compile.

I would rather vote to copy the code from frameworks/react-vite/src/plugins/docgen-resolver into a directory that is correctly accessible by both, the framework react-vite and the preset react-webpack, like in e.g. @storybook/core-common

or

we just duplicate the code in ../../../../frameworks/react-vite/src/plugins/docgen-resolver which I think is okay if we don't want to move it into a sharable core package.

The third option is that things stay as is, but then resolve should be a direct dependency of the react-webpack package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ndelangen Just to clarify. Are you talking about extracting code/frameworks/react-vite/src/plugins/docgen-resolver.ts into a core package? Because this file doesn't have any react-docgen dependencies or am i overlooking something?

Copy link
Member Author

@ndelangen ndelangen Feb 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you define "correctly accessible by both"? @valentinpalkovic
Placing the code in a shared package, like I mentioned that that would cause that package to gain dependencies it currently does not have.
You suggest core-common, I suggested docs-tools.

Are you talking about extracting code/frameworks/react-vite/src/plugins/docgen-resolver.ts into a core package?

Yes we currently do not have a suitable package, hence I decided to just do this way, which minimizes duplicated code, and optimizes for easy of replaceability because I assume that at some point in the future this code will change upstream and updating it in 2 places going to be fragile, right?

But if you both agree that duplicating the code is better than 1 slightly ugly import, I'll change it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve should be a direct dependency of the react-webpack package.

Ah, yes that's right. I missed that. 🙏

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I duplicated the code.


const { getNameOrValue, isReactForwardRefCall } = utils;

const actualNameHandler: Handler = function actualNameHandler(documentation, componentDefinition) {
Expand Down Expand Up @@ -54,7 +59,6 @@ type DocObj = Documentation & { actualName: string };

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

export default async function reactDocgenLoader(
Expand All @@ -71,7 +75,15 @@ export default async function reactDocgenLoader(
filename: this.resourcePath,
resolver: defaultResolver,
handlers,
importer: defaultImporter,
importer: makeFsImporter((filename, basedir) => {
const result = defaultLookupModule(filename, basedir);

if (!result.match(/\.(mjs|tsx?|jsx?)$/)) {
throw new ReactDocgenResolveError(filename);
}

return result;
}),
babelOptions: {
babelrc: false,
configFile: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.foo {
color: red;
}
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react';

import { imported } from '../imported';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore (css import not supported in TS)
import styles from '../imported.module.css';
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

const local = 'local-value';

Expand All @@ -26,6 +29,7 @@ interface PropsWriterProps {
importedReference?: string;
globalReference?: any;
stringGlobalName?: string;
myClass: typeof styles.foo;
}

/**
Expand All @@ -47,6 +51,7 @@ PropsWriter.defaultProps = {
importedReference: imported,
globalReference: Date,
stringGlobalName: 'top',
myClass: styles.foo,
};

export const component = PropsWriter;
5 changes: 3 additions & 2 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6491,6 +6491,7 @@ __metadata:
"@types/node": "npm:^18.0.0"
magic-string: "npm:^0.30.0"
react-docgen: "npm:^7.0.0"
resolve: "npm:^1.22.8"
typescript: "npm:^5.3.2"
vite: "npm:^4.0.0"
peerDependencies:
Expand Down Expand Up @@ -25657,7 +25658,7 @@ __metadata:
languageName: node
linkType: hard

"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":
"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":
version: 1.22.8
resolution: "resolve@npm:1.22.8"
dependencies:
Expand All @@ -25683,7 +25684,7 @@ __metadata:
languageName: node
linkType: hard

"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>":
"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>":
version: 1.22.8
resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin<compat/resolve>::version=1.22.8&hash=c3c19d"
dependencies:
Expand Down
Loading