Skip to content

Commit

Permalink
Merge pull request #26273 from storybookjs/valentin/support-typescrip…
Browse files Browse the repository at this point in the history
…t-aliases

React Docgen: Handle TypeScript path aliases in react-docgen loader
  • Loading branch information
valentinpalkovic committed Mar 4, 2024
2 parents f0beb18 + ea40ff8 commit b0f4ab3
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 23 deletions.
5 changes: 4 additions & 1 deletion code/frameworks/react-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@
"@joshwooding/vite-plugin-react-docgen-typescript": "0.3.0",
"@rollup/pluginutils": "^5.0.2",
"@storybook/builder-vite": "workspace:*",
"@storybook/node-logger": "workspace:*",
"@storybook/react": "workspace:*",
"find-up": "^5.0.0",
"magic-string": "^0.30.0",
"react-docgen": "^7.0.0",
"resolve": "^1.22.8"
"resolve": "^1.22.8",
"tsconfig-paths": "^4.2.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
Expand Down
52 changes: 52 additions & 0 deletions code/frameworks/react-vite/src/plugins/react-docgen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { getReactDocgenImporter } from './react-docgen';
import { describe, it, expect, vi } from 'vitest';

const reactDocgenMock = vi.hoisted(() => {
return {
makeFsImporter: vi.fn().mockImplementation((fn) => fn),
};
});

const reactDocgenResolverMock = vi.hoisted(() => {
return {
defaultLookupModule: vi.fn(),
};
});

vi.mock('./docgen-resolver', async (importOriginal) => {
const actual = await importOriginal<typeof import('path')>();
return {
...actual,
defaultLookupModule: reactDocgenResolverMock.defaultLookupModule,
};
});

vi.mock('react-docgen', async (importOriginal) => {
const actual = await importOriginal<typeof import('path')>();
return {
...actual,
makeFsImporter: reactDocgenMock.makeFsImporter,
};
});

describe('getReactDocgenImporter function', () => {
it('should not map the request if a tsconfig path mapping is not available', () => {
const filename = './src/components/Button.tsx';
const basedir = '/src';
const imported = getReactDocgenImporter(undefined);
reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen);
const result = (imported as any)(filename, basedir);
expect(result).toBe(filename);
});

it('should map the request', () => {
const mappedFile = './mapped-file.tsx';
const matchPath = vi.fn().mockReturnValue(mappedFile);
const filename = './src/components/Button.tsx';
const basedir = '/src';
const imported = getReactDocgenImporter(matchPath);
reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen);
const result = (imported as any)(filename, basedir);
expect(result).toBe(mappedFile);
});
});
52 changes: 41 additions & 11 deletions code/frameworks/react-vite/src/plugins/react-docgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import {
} from 'react-docgen';
import MagicString from 'magic-string';
import type { PluginOption } from 'vite';
import * as TsconfigPaths from 'tsconfig-paths';
import findUp from 'find-up';
import actualNameHandler from './docgen-handlers/actualNameHandler';
import {
RESOLVE_EXTENSIONS,
ReactDocgenResolveError,
defaultLookupModule,
} from './docgen-resolver';
import { logger } from '@storybook/node-logger';

type DocObj = Documentation & { actualName: string };

Expand All @@ -29,13 +32,27 @@ type Options = {
exclude?: string | RegExp | (string | RegExp)[];
};

export function reactDocgen({
export async function reactDocgen({
include = /\.(mjs|tsx?|jsx?)$/,
exclude = [/node_modules\/.*/],
}: Options = {}): PluginOption {
}: Options = {}): Promise<PluginOption> {
const cwd = process.cwd();
const filter = createFilter(include, exclude);

const tsconfigPath = await findUp('tsconfig.json', { cwd });
const tsconfig = TsconfigPaths.loadConfig(tsconfigPath);

let matchPath: TsconfigPaths.MatchPath | undefined;

if (tsconfig.resultType === 'success') {
logger.info('Using tsconfig paths for react-docgen');
matchPath = TsconfigPaths.createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths, [
'browser',
'module',
'main',
]);
}

return {
name: 'storybook:react-docgen-plugin',
enforce: 'pre',
Expand All @@ -48,15 +65,7 @@ export function reactDocgen({
const docgenResults = parse(src, {
resolver: defaultResolver,
handlers,
importer: makeFsImporter((filename, basedir) => {
const result = defaultLookupModule(filename, basedir);

if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) {
return result;
}

throw new ReactDocgenResolveError(filename);
}),
importer: getReactDocgenImporter(matchPath),
filename: id,
}) as DocObj[];
const s = new MagicString(src);
Expand All @@ -83,3 +92,24 @@ export function reactDocgen({
},
};
}

export function getReactDocgenImporter(matchPath: TsconfigPaths.MatchPath | undefined) {
return makeFsImporter((filename, basedir) => {
const mappedFilenameByPaths = (() => {
if (matchPath) {
const match = matchPath(filename);
return match || filename;
} else {
return filename;
}
})();

const result = defaultLookupModule(mappedFilenameByPaths, basedir);

if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) {
return result;
}

throw new ReactDocgenResolveError(filename);
});
}
2 changes: 1 addition & 1 deletion code/frameworks/react-vite/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config, { presets
// Needs to run before the react plugin, so add to the front
plugins.unshift(
// If react-docgen is specified, use it for everything, otherwise only use it for non-typescript files
reactDocgen({
await reactDocgen({
include: reactDocgenOption === 'react-docgen' ? /\.(mjs|tsx?|jsx?)$/ : /\.(mjs|jsx?)$/,
})
);
Expand Down
2 changes: 2 additions & 0 deletions code/presets/react-webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
"@types/node": "^18.0.0",
"@types/semver": "^7.3.4",
"find-up": "^5.0.0",
"fs-extra": "^11.1.0",
"magic-string": "^0.30.5",
"react-docgen": "^7.0.0",
"resolve": "^1.22.8",
"semver": "^7.3.7",
"tsconfig-paths": "^4.2.0",
"webpack": "5"
},
"devDependencies": {
Expand Down
52 changes: 52 additions & 0 deletions code/presets/react-webpack/src/loaders/react-docgen-loader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { getReactDocgenImporter } from './react-docgen-loader';
import { describe, it, expect, vi } from 'vitest';

const reactDocgenMock = vi.hoisted(() => {
return {
makeFsImporter: vi.fn().mockImplementation((fn) => fn),
};
});

const reactDocgenResolverMock = vi.hoisted(() => {
return {
defaultLookupModule: vi.fn(),
};
});

vi.mock('./docgen-resolver', async (importOriginal) => {
const actual = await importOriginal<typeof import('path')>();
return {
...actual,
defaultLookupModule: reactDocgenResolverMock.defaultLookupModule,
};
});

vi.mock('react-docgen', async (importOriginal) => {
const actual = await importOriginal<typeof import('path')>();
return {
...actual,
makeFsImporter: reactDocgenMock.makeFsImporter,
};
});

describe('getReactDocgenImporter function', () => {
it('should not map the request if a tsconfig path mapping is not available', () => {
const filename = './src/components/Button.tsx';
const basedir = '/src';
const imported = getReactDocgenImporter(undefined);
reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen);
const result = (imported as any)(filename, basedir);
expect(result).toBe(filename);
});

it('should map the request', () => {
const mappedFile = './mapped-file.tsx';
const matchPath = vi.fn().mockReturnValue(mappedFile);
const filename = './src/components/Button.tsx';
const basedir = '/src';
const imported = getReactDocgenImporter(matchPath);
reactDocgenResolverMock.defaultLookupModule.mockImplementation((filen: string) => filen);
const result = (imported as any)(filename, basedir);
expect(result).toBe(mappedFile);
});
});
52 changes: 43 additions & 9 deletions code/presets/react-webpack/src/loaders/react-docgen-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
ERROR_CODES,
utils,
} from 'react-docgen';
import * as TsconfigPaths from 'tsconfig-paths';
import findUp from 'find-up';
import MagicString from 'magic-string';
import type { LoaderContext } from 'webpack';
import type { Handler, NodePath, babelTypes as t, Documentation } from 'react-docgen';
Expand Down Expand Up @@ -62,6 +64,9 @@ const defaultHandlers = Object.values(docgenHandlers).map((handler) => handler);
const defaultResolver = new docgenResolver.FindExportedDefinitionsResolver();
const handlers = [...defaultHandlers, actualNameHandler];

let tsconfigPathsInitialized = false;
let matchPath: TsconfigPaths.MatchPath | undefined;

export default async function reactDocgenLoader(
this: LoaderContext<{ debug: boolean }>,
source: string
Expand All @@ -71,20 +76,28 @@ export default async function reactDocgenLoader(
const options = this.getOptions() || {};
const { debug = false } = options;

if (!tsconfigPathsInitialized) {
const tsconfigPath = await findUp('tsconfig.json', { cwd: process.cwd() });
const tsconfig = TsconfigPaths.loadConfig(tsconfigPath);

if (tsconfig.resultType === 'success') {
logger.info('Using tsconfig paths for react-docgen');
matchPath = TsconfigPaths.createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths, [
'browser',
'module',
'main',
]);
}

tsconfigPathsInitialized = true;
}

try {
const docgenResults = parse(source, {
filename: this.resourcePath,
resolver: defaultResolver,
handlers,
importer: makeFsImporter((filename, basedir) => {
const result = defaultLookupModule(filename, basedir);

if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) {
return result;
}

throw new ReactDocgenResolveError(filename);
}),
importer: getReactDocgenImporter(matchPath),
babelOptions: {
babelrc: false,
configFile: false,
Expand Down Expand Up @@ -122,3 +135,24 @@ export default async function reactDocgenLoader(
}
}
}

export function getReactDocgenImporter(matchingPath: TsconfigPaths.MatchPath | undefined) {
return makeFsImporter((filename, basedir) => {
const mappedFilenameByPaths = (() => {
if (matchingPath) {
const match = matchingPath(filename);
return match || filename;
} else {
return filename;
}
})();

const result = defaultLookupModule(mappedFilenameByPaths, basedir);

if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) {
return result;
}

throw new ReactDocgenResolveError(filename);
});
}
7 changes: 6 additions & 1 deletion code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6340,11 +6340,13 @@ __metadata:
"@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0"
"@types/node": "npm:^18.0.0"
"@types/semver": "npm:^7.3.4"
find-up: "npm:^5.0.0"
fs-extra: "npm:^11.1.0"
magic-string: "npm:^0.30.5"
react-docgen: "npm:^7.0.0"
resolve: "npm:^1.22.8"
semver: "npm:^7.3.7"
tsconfig-paths: "npm:^4.2.0"
typescript: "npm:^5.3.2"
webpack: "npm:5"
peerDependencies:
Expand Down Expand Up @@ -6488,11 +6490,14 @@ __metadata:
"@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.0"
"@rollup/pluginutils": "npm:^5.0.2"
"@storybook/builder-vite": "workspace:*"
"@storybook/node-logger": "workspace:*"
"@storybook/react": "workspace:*"
"@types/node": "npm:^18.0.0"
find-up: "npm:^5.0.0"
magic-string: "npm:^0.30.0"
react-docgen: "npm:^7.0.0"
resolve: "npm:^1.22.8"
tsconfig-paths: "npm:^4.2.0"
typescript: "npm:^5.3.2"
vite: "npm:^4.0.0"
peerDependencies:
Expand Down Expand Up @@ -28381,7 +28386,7 @@ __metadata:
languageName: node
linkType: hard

"tsconfig-paths@npm:^4.0.0, tsconfig-paths@npm:^4.1.2":
"tsconfig-paths@npm:^4.0.0, tsconfig-paths@npm:^4.1.2, tsconfig-paths@npm:^4.2.0":
version: 4.2.0
resolution: "tsconfig-paths@npm:4.2.0"
dependencies:
Expand Down

0 comments on commit b0f4ab3

Please sign in to comment.