Skip to content

Commit

Permalink
feat: support react-docgen-typescript in another way (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
fi3ework committed Jul 29, 2024
1 parent f3007e6 commit 06b61eb
Show file tree
Hide file tree
Showing 13 changed files with 457 additions and 39 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export default {
- [x] Support lazy compilation
- [ ] Support virtual modules
- [ ] Support `module.unknownContextCritical`
- [ ] Support `compilation.dependencyTemplates.set` for react-docgen-typescript 🌟
- [x] Support `compilation.dependencyTemplates.set` for react-docgen-typescript (supported in a workaround)
## Credits
Expand Down
32 changes: 0 additions & 32 deletions packages/builder-rsbuild/tests/dummy.test.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/react-rsbuild/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"prepare": "pnpm run build"
},
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"@storybook/react": "^8.2.1",
"@storybook/react-docgen-typescript-plugin": "^1.0.1",
"@types/node": "^18.0.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/react-rsbuild/src/loaders/docgen-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* Code taken from https://github.com/storybookjs/storybook/tree/next/code/presets/react-webpack/src/loaders
*/

import { extname } from 'node:path'
import resolve from 'resolve'

Expand Down
60 changes: 60 additions & 0 deletions packages/react-rsbuild/src/loaders/react-docgen-loader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Code taken from https://github.com/storybookjs/storybook/tree/next/code/presets/react-webpack/src/loaders
*/

import { describe, expect, it, vi } from 'vitest'
import { getReactDocgenImporter } from './react-docgen-loader'

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)
})
})
4 changes: 4 additions & 0 deletions packages/react-rsbuild/src/loaders/react-docgen-loader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* Code taken from https://github.com/storybookjs/storybook/tree/next/code/presets/react-webpack/src/loaders
*/

import findUp from 'find-up'
import MagicString from 'magic-string'
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Code taken from https://github.com/storybookjs/storybook/tree/next/code/frameworks/react-vite/src/plugins
*/

/**
* This is heavily based on the react-docgen `displayNameHandler`
* (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts)
* but instead defines an `actualName` property on the generated docs that is taken first from the component's actual name.
* This addresses an issue where the name that the generated docs are stored under is incorrectly named with the `displayName`
* and not the component's actual name.
*
* This is inspired by `actualNameHandler` from https://github.com/storybookjs/babel-plugin-react-docgen, but is modified
* directly from displayNameHandler, using the same approach as babel-plugin-react-docgen.
*/

import type { Handler, NodePath, babelTypes as t } from 'react-docgen'
import { utils } from 'react-docgen'

const { getNameOrValue, isReactForwardRefCall } = utils

const actualNameHandler: Handler = function actualNameHandler(
documentation,
componentDefinition,
) {
documentation.set('definedInFile', componentDefinition.hub.file.opts.filename)

if (
(componentDefinition.isClassDeclaration() ||
componentDefinition.isFunctionDeclaration()) &&
componentDefinition.has('id')
) {
documentation.set(
'actualName',
getNameOrValue(componentDefinition.get('id') as NodePath<t.Identifier>),
)
} else if (
componentDefinition.isArrowFunctionExpression() ||
componentDefinition.isFunctionExpression() ||
isReactForwardRefCall(componentDefinition)
) {
let currentPath: NodePath = componentDefinition

while (currentPath.parentPath) {
if (currentPath.parentPath.isVariableDeclarator()) {
documentation.set(
'actualName',
getNameOrValue(currentPath.parentPath.get('id')),
)
return
}
if (currentPath.parentPath.isAssignmentExpression()) {
const leftPath = currentPath.parentPath.get('left')

if (leftPath.isIdentifier() || leftPath.isLiteral()) {
documentation.set('actualName', getNameOrValue(leftPath))
return
}
}

currentPath = currentPath.parentPath
}
// Could not find an actual name
documentation.set('actualName', '')
}
}

export default actualNameHandler
78 changes: 78 additions & 0 deletions packages/react-rsbuild/src/plugins/docgen-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Code taken from https://github.com/storybookjs/storybook/tree/next/code/frameworks/react-vite/src/plugins
*/

import { extname } from 'node: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.
* watch out: when updating this code, also update the code in code/presets/react-webpack/src/loaders/docgen-resolver.ts
*/

// These extensions are sorted by priority
// resolve() will check for files in the order these extensions are sorted
export const RESOLVE_EXTENSIONS = [
'.js',
'.cts', // These were originally not in the code, I added them
'.mts', // These were originally not in the code, I added them
'.ctsx', // These were originally not in the code, I added them
'.mtsx', // These were originally not in the code, I added them
'.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: [],
})
}
}
60 changes: 60 additions & 0 deletions packages/react-rsbuild/src/plugins/react-docgen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Code taken from https://github.com/storybookjs/storybook/tree/next/code/frameworks/react-vite/src/plugins
*/

import { describe, expect, it, vi } from 'vitest'
import { getReactDocgenImporter } from './react-docgen'

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)
})
})
Loading

0 comments on commit 06b61eb

Please sign in to comment.