From 3b28f6ee864fddbd872441035b21ad416ae7f417 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder <231804+danez@users.noreply.github.com> Date: Sat, 10 Dec 2022 20:20:08 +0100 Subject: [PATCH] feat: Introduce new CLI --- .changeset/mighty-shrimps-shout.md | 17 + .changeset/rare-taxis-divide.md | 7 + .gitignore | 5 +- .release-please-manifest.json | 2 +- package.json | 3 + packages/react-docgen-cli/package.json | 15 +- .../src/__tests__/__fixtures__/Component.js | 29 - .../__tests__/__fixtures__/customResolver.js | 17 - .../src/__tests__/react-docgen-test.ts | 424 ------------- packages/react-docgen-cli/src/cli.ts | 13 + .../src/commands/parse/command.ts | 160 +++++ .../src/commands/parse/options/loadOptions.ts | 49 ++ .../parse/options/loadReactDocgenPlugin.ts | 26 + .../src/commands/parse/output/outputError.ts | 50 ++ .../src/commands/parse/output/outputResult.ts | 19 + packages/react-docgen-cli/src/react-docgen.ts | 233 ------- .../react-docgen-cli/src/utils/importFile.ts | 28 + .../__fixtures__/basic/Component.js | 7 + .../__fixtures__/basic/NoComponent.js | 0 .../custom-handler-cjs/Component.js | 10 + .../custom-handler-cjs/handler.cjs | 5 + .../custom-handler-esm/Component.js | 10 + .../custom-handler-esm/handler.mjs | 5 + .../custom-handler-npm/Component.js | 10 + .../test-react-docgen-handler/index.js | 5 + .../test-react-docgen-handler/package.json | 3 + .../custom-importer-cjs/Component.js | 10 + .../custom-importer-esm/Component.js | 10 + .../custom-importer-esm/importer.mjs | 15 + .../custom-importer-npm/Component.js | 10 + .../test-react-docgen-importer/index.js | 15 + .../test-react-docgen-importer/package.json | 3 + .../custom-importer-cjs/importer.cjs | 15 + .../custom-importer-esm/Component.js | 10 + .../custom-importer-esm/importer.mjs | 15 + .../custom-importer-npm/Component.js | 10 + .../test-react-docgen-importer/index.js | 15 + .../test-react-docgen-importer/package.json | 3 + .../custom-resolver-cjs/Component.js | 8 + .../custom-resolver-cjs/resolver.cjs | 23 + .../custom-resolver-esm/Component.js | 8 + .../custom-resolver-esm/resolver.mjs | 23 + .../custom-resolver-npm/Component.js | 8 + .../test-react-docgen-resolver/index.js | 23 + .../test-react-docgen-resolver/package.json | 3 + .../ignore/__mocks__/MockComponent.js | 8 + .../ignore/__tests__/TestComponent.js | 8 + .../__fixtures__/ignore/foo/FooComponent.js | 8 + .../node_modules/NodeModulesComponent.js | 8 + .../multiple}/MultipleComponents.js | 0 .../__fixtures__/syntax-error/SyntaxError.js | 1 + .../tests/integration/cli-test.ts | 585 ++++++++++++++++++ packages/react-docgen/package.json | 4 +- .../__tests__/getMemberValuePath-test.ts | 2 +- vitest.config.ts | 2 + yarn.lock | 265 +++++++- 56 files changed, 1547 insertions(+), 723 deletions(-) create mode 100644 .changeset/mighty-shrimps-shout.md create mode 100644 .changeset/rare-taxis-divide.md delete mode 100644 packages/react-docgen-cli/src/__tests__/__fixtures__/Component.js delete mode 100644 packages/react-docgen-cli/src/__tests__/__fixtures__/customResolver.js delete mode 100644 packages/react-docgen-cli/src/__tests__/react-docgen-test.ts create mode 100755 packages/react-docgen-cli/src/cli.ts create mode 100644 packages/react-docgen-cli/src/commands/parse/command.ts create mode 100644 packages/react-docgen-cli/src/commands/parse/options/loadOptions.ts create mode 100644 packages/react-docgen-cli/src/commands/parse/options/loadReactDocgenPlugin.ts create mode 100644 packages/react-docgen-cli/src/commands/parse/output/outputError.ts create mode 100644 packages/react-docgen-cli/src/commands/parse/output/outputResult.ts delete mode 100755 packages/react-docgen-cli/src/react-docgen.ts create mode 100644 packages/react-docgen-cli/src/utils/importFile.ts create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/basic/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/basic/NoComponent.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/handler.cjs create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/handler.mjs create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/index.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/package.json create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/importer.mjs create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/index.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/package.json create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/importer.cjs create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/importer.mjs create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/index.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/package.json create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/resolver.cjs create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/resolver.mjs create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/Component.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/index.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/package.json create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__mocks__/MockComponent.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__tests__/TestComponent.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/ignore/foo/FooComponent.js create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/ignore/node_modules/NodeModulesComponent.js rename packages/react-docgen-cli/{src/__tests__/__fixtures__ => tests/integration/__fixtures__/multiple}/MultipleComponents.js (100%) create mode 100644 packages/react-docgen-cli/tests/integration/__fixtures__/syntax-error/SyntaxError.js create mode 100644 packages/react-docgen-cli/tests/integration/cli-test.ts diff --git a/.changeset/mighty-shrimps-shout.md b/.changeset/mighty-shrimps-shout.md new file mode 100644 index 00000000000..d89937c4321 --- /dev/null +++ b/.changeset/mighty-shrimps-shout.md @@ -0,0 +1,17 @@ +--- +'@react-docgen/cli': major +--- + +Introducing the new CLI package `@react-docgen/cli` which was extracted from `react-docgen` and is a complete rewrite. +Compared to the old CLI these are some of the major differences: + +- Does not support input via stdin anymore +- The path argument is now a glob +- `-x, --extension` was removed in favor of globs +- `-e, --exclude` was removed +- `-i, --ignore` now accepts a glob +- `--handler` added +- `--importer` added +- `--failOnWarning` added + +Check out https://react-docgen.dev/cli for the documentation. diff --git a/.changeset/rare-taxis-divide.md b/.changeset/rare-taxis-divide.md new file mode 100644 index 00000000000..36d227dd7c8 --- /dev/null +++ b/.changeset/rare-taxis-divide.md @@ -0,0 +1,7 @@ +--- +'react-docgen': major +--- + +The CLi was removed from `react-docgen` into its own package `@react-docgen/cli`. + +Check out https://react-docgen.dev/cli for the documentation. diff --git a/.gitignore b/.gitignore index b0abdf07eb7..c3314df82ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -dist -node_modules/ +dist/ +**/node_modules/* +!**/__fixtures__/**/node_modules/* .idea/ coverage/ yarn-error.log diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d493e2b1bd3..b848bae0795 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,4 @@ { "packages/react-docgen": "6.0.0-alpha.3", - "packages/react-docgen-cli": "6.0.0-alpha.3" + "packages/react-docgen-cli": "1.0.0-alpha.0" } diff --git a/package.json b/package.json index a87ab1e1801..600dc933040 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "fix": "eslint . --ext .js,.ts --fix --report-unused-disable-directives", "test": "yarn build && vitest run", "test:dev": "vitest", + "g:tsc": "cd $INIT_CWD && rimraf dist/ && tsc", "website:build": "yarn workspace website run build", "website:start": "yarn workspace website run start" }, @@ -27,9 +28,11 @@ "@typescript-eslint/eslint-plugin": "5.46.0", "@typescript-eslint/parser": "5.46.0", "@vitest/coverage-c8": "0.25.6", + "cpy": "9.0.1", "eslint": "8.29.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-prettier": "4.2.1", + "execa": "6.1.0", "prettier": "2.8.1", "rimraf": "3.0.2", "tempy": "3.0.0", diff --git a/packages/react-docgen-cli/package.json b/packages/react-docgen-cli/package.json index 64b654c451b..ef7a451faaf 100644 --- a/packages/react-docgen-cli/package.json +++ b/packages/react-docgen-cli/package.json @@ -1,6 +1,6 @@ { "name": "@react-docgen/cli", - "version": "6.0.0-alpha.3", + "version": "1.0.0-alpha.0", "description": "A CLI and toolkit to extract information from React components for documentation generation.", "repository": "reactjs/react-docgen", "type": "module", @@ -14,7 +14,7 @@ "node": ">=14.17.0" }, "scripts": { - "build": "echo 'done'", + "build": "yarn g:tsc", "watch": "echo 'done'" }, "keywords": [ @@ -27,7 +27,14 @@ }, "license": "MIT", "dependencies": { - "commander": "9.4.1", - "react-docgen": "6.0.0-alpha.3" + "chalk": "^5.1.2", + "commander": "^9.4.1", + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "react-docgen": "6.0.0-alpha.3", + "slash": "^5.0.0" + }, + "devDependencies": { + "@types/debug": "4.1.7" } } diff --git a/packages/react-docgen-cli/src/__tests__/__fixtures__/Component.js b/packages/react-docgen-cli/src/__tests__/__fixtures__/Component.js deleted file mode 100644 index e896a124b89..00000000000 --- a/packages/react-docgen-cli/src/__tests__/__fixtures__/Component.js +++ /dev/null @@ -1,29 +0,0 @@ -const React = require('react'); -const Foo = require('Foo'); - -/** - * General component description. - */ -const Component = React.createClass({ - displayName: 'Component', - - propTypes: { - ...Foo.propTypes, - /** - * Prop description - */ - bar: React.PropTypes.number, - }, - - getDefaultProps: function () { - return { - bar: 21, - }; - }, - - render: function () { - // ... - }, -}); - -module.exports = Component; diff --git a/packages/react-docgen-cli/src/__tests__/__fixtures__/customResolver.js b/packages/react-docgen-cli/src/__tests__/__fixtures__/customResolver.js deleted file mode 100644 index c436ef1aa09..00000000000 --- a/packages/react-docgen-cli/src/__tests__/__fixtures__/customResolver.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Dummy resolver that always returns the same AST node - */ - -const code = ` - ({ - displayName: 'Custom', - }) -`; - -const { NodePath } = require('ast-types'); - -module.exports = function (ast, parser) { - const path = new NodePath(parser.parse(code)); - - return path.get('program', 'body', 0, 'expression'); -}; diff --git a/packages/react-docgen-cli/src/__tests__/react-docgen-test.ts b/packages/react-docgen-cli/src/__tests__/react-docgen-test.ts deleted file mode 100644 index 55c0d2df1b6..00000000000 --- a/packages/react-docgen-cli/src/__tests__/react-docgen-test.ts +++ /dev/null @@ -1,424 +0,0 @@ -// NOTE: This test spawns a subprocesses that load the files from dist/, not -// src/. Before running this test run `npm run build` or `npm run watch`. - -const TEST_TIMEOUT = 120000; - -import fs, { promises } from 'fs'; -import path from 'path'; -import rimraf from 'rimraf'; -import { directory as tempDirectory, file as tempFile } from 'tempy'; -import spawn from 'cross-spawn'; -import { afterEach, describe, expect, test } from 'vitest'; - -const { writeFile, mkdir } = promises; - -function run( - args: readonly string[], - stdin: Buffer | string | null = null, -): Promise<[string, string]> { - return new Promise(resolve => { - const docgen = spawn(path.join(__dirname, '../react-docgen.js'), args); - let stdout = ''; - let stderr = ''; - - docgen.stdout?.on('data', data => (stdout += data)); - docgen.stderr?.on('data', data => (stderr += data)); - docgen.on('close', () => resolve([stdout, stderr])); - docgen.on('error', e => { - throw e; - }); - if (stdin) { - docgen.stdin?.write(stdin); - } - docgen.stdin?.end(); - }); -} - -const component = fs.readFileSync( - path.join(__dirname, '__fixtures__/Component.js'), -); - -describe.skip('react-docgen CLI', () => { - let tempDir = ''; - let tempComponents: string[] = []; - let tempNoComponents: string[] = []; - - async function createTempfiles( - extension = 'js', - dir: string | null = null, - ): Promise { - if (!tempDir) { - tempDir = tempDirectory(); - } - - if (!dir) { - dir = tempDir; - } else { - dir = path.join(tempDir, dir); - try { - await mkdir(dir); - } catch (error: any) { - if (error.message.indexOf('EEXIST') === -1) { - throw error; - } - } - } - - const componentPath = path.join(dir, `Component.${extension}`); - - await writeFile(componentPath, component, 'utf-8'); - tempComponents.push(componentPath); - - const noComponentPath = path.join(dir, `NoComponent.${extension}`); - - await writeFile(noComponentPath, '{}', 'utf-8'); - tempNoComponents.push(noComponentPath); - - return dir; - } - - afterEach(() => { - if (tempDir) { - rimraf.sync(tempDir); - } - tempDir = ''; - tempComponents = []; - tempNoComponents = []; - }, TEST_TIMEOUT); - - test( - 'reads from stdin', - async () => { - return run([], component).then(([stdout, stderr]) => { - expect(stdout).not.toBe(''); - expect(stderr).toBe(''); - }); - }, - TEST_TIMEOUT, - ); - - test( - 'reads files provided as command line arguments', - async () => { - await createTempfiles(); - const [stdout, stderr] = await run( - tempComponents.concat(tempNoComponents), - ); - - expect(stdout).toContain('Component'); - expect(stderr).toContain('NoComponent'); - }, - TEST_TIMEOUT, - ); - - test( - 'reads directories provided as command line arguments', - async () => { - await createTempfiles(); - const [stdout, stderr] = await run([tempDir]); - - expect(stderr).toContain('NoComponent'); - expect(stdout).toContain('Component'); - }, - TEST_TIMEOUT, - ); - - test( - 'considers js and jsx by default', - async () => { - await createTempfiles(); - await createTempfiles('jsx'); - await createTempfiles('foo'); - const [stdout, stderr] = await run([tempDir]); - - expect(stdout).toContain('Component.js'); - expect(stdout).toContain('Component.jsx'); - expect(stdout).not.toContain('Component.foo'); - - expect(stderr).toContain('NoComponent.js'); - expect(stderr).toContain('NoComponent.jsx'); - expect(stderr).not.toContain('NoComponent.foo'); - }, - TEST_TIMEOUT, - ); - - test( - 'considers files with the specified extension', - async () => { - await createTempfiles('foo'); - await createTempfiles('bar'); - - const [stdout, stderr] = await run([ - '--extension=foo', - '--extension=bar', - tempDir, - ]); - - expect(stdout).toContain('Component.foo'); - expect(stdout).toContain('Component.bar'); - - expect(stderr).toContain('NoComponent.foo'); - expect(stderr).toContain('NoComponent.bar'); - }, - TEST_TIMEOUT, - ); - - test( - 'considers files with the specified extension shortcut', - async () => { - await createTempfiles('foo'); - await createTempfiles('bar'); - - const [stdout, stderr] = await run(['-x', 'foo', '-x', 'bar', tempDir]); - - expect(stdout).toContain('Component.foo'); - expect(stdout).toContain('Component.bar'); - - expect(stderr).toContain('NoComponent.foo'); - expect(stderr).toContain('NoComponent.bar'); - }, - TEST_TIMEOUT, - ); - - test( - 'ignores files in node_modules, __tests__ and __mocks__ by default', - async () => { - await createTempfiles(undefined, 'node_modules'); - await createTempfiles(undefined, '__tests__'); - await createTempfiles(undefined, '__mocks__'); - - const [stdout, stderr] = await run([tempDir]); - - expect(stdout).toBe(''); - expect(stderr).toBe(''); - }, - TEST_TIMEOUT, - ); - - test( - 'ignores specified folders', - async () => { - await createTempfiles(undefined, 'foo'); - - const [stdout, stderr] = await run(['--ignore=foo', tempDir]); - - expect(stdout).toBe(''); - expect(stderr).toBe(''); - }, - TEST_TIMEOUT, - ); - - test( - 'ignores specified folders shortcut', - async () => { - await createTempfiles(undefined, 'foo'); - - const [stdout, stderr] = await run(['-i', 'foo', tempDir]); - - expect(stdout).toBe(''); - expect(stderr).toBe(''); - }, - TEST_TIMEOUT, - ); - - test( - 'writes to stdout', - async () => { - const [stdout, stderr] = await run([], component); - - expect(stdout.length > 0).toBe(true); - expect(stderr.length).toBe(0); - }, - TEST_TIMEOUT, - ); - - test( - 'writes to stderr', - async () => { - const [stdout, stderr] = await run([], '{}'); - - expect(stderr.length > 0).toBe(true); - expect(stdout.length).toBe(0); - }, - TEST_TIMEOUT, - ); - - test( - 'writes to a file if provided', - async () => { - const outFile = tempFile(); - - await createTempfiles(); - - const [stdout] = await run([`--out=${outFile}`, tempDir]); - - expect(fs.readFileSync(outFile)).not.toBe(''); - expect(stdout).toBe(''); - }, - TEST_TIMEOUT, - ); - - test( - 'writes to a file if provided shortcut', - async () => { - const outFile = tempFile(); - - await createTempfiles(); - - const [stdout] = await run(['-o', outFile, tempDir]); - - expect(fs.readFileSync(outFile)).not.toBe(''); - expect(stdout).toBe(''); - }, - TEST_TIMEOUT, - ); - - describe('--resolver', () => { - describe('accepts the names of built in resolvers', () => { - test( - 'findExportedComponentDefinition (default)', - async () => { - // No option passed: same as --resolver=findExportedComponentDefinition - const [stdout] = await run([ - path.join(__dirname, '__fixtures__/Component.js'), - ]); - - expect(stdout).toContain('Component'); - }, - TEST_TIMEOUT, - ); - - test( - 'findExportedComponentDefinition', - async () => { - const [stdout] = await run([ - '--resolver=findExportedComponentDefinition', - path.join(__dirname, '__fixtures__/Component.js'), - ]); - - expect(stdout).toContain('Component'); - }, - TEST_TIMEOUT, - ); - - test( - 'findAllComponentDefinitions', - async () => { - const [stdout] = await run([ - '--resolver=findAllComponentDefinitions', - path.join(__dirname, '__fixtures__/MultipleComponents.js'), - ]); - - expect(stdout).toContain('ComponentA'); - expect(stdout).toContain('ComponentB'); - }, - TEST_TIMEOUT, - ); - - test( - 'findAllExportedComponentDefinitions', - async () => { - const [stdout] = await run([ - '--resolver=findAllExportedComponentDefinitions', - path.join(__dirname, '__fixtures__/MultipleComponents.js'), - ]); - - expect(stdout).toContain('ComponentA'); - expect(stdout).toContain('ComponentB'); - }, - TEST_TIMEOUT, - ); - }); - - test( - 'accepts a path to a resolver function', - async () => { - const [stdout] = await run([ - `--resolver=${path.join( - __dirname, - '__fixtures__/customResolver.js', - )}`, - path.join(__dirname, '__fixtures__/MultipleComponents.js'), - ]); - - expect(stdout).toContain('Custom'); - }, - TEST_TIMEOUT, - ); - }); - - describe('--exclude/-e', () => { - test( - 'ignores files by name', - async () => { - await createTempfiles(undefined, 'foo'); - await createTempfiles(undefined, 'bar'); - - const [stdout, stderr] = await run([ - '--exclude=Component.js', - '--exclude=NoComponent.js', - tempDir, - ]); - - expect(stdout).toBe(''); - expect(stderr).toBe(''); - }, - TEST_TIMEOUT, - ); - - test( - 'ignores files by name shortcut', - async () => { - await createTempfiles(undefined, 'foo'); - await createTempfiles(undefined, 'bar'); - - const [stdout, stderr] = await run([ - '-e', - 'Component.js', - '-e', - 'NoComponent.js', - tempDir, - ]); - - expect(stdout).toBe(''); - expect(stderr).toBe(''); - }, - TEST_TIMEOUT, - ); - - test( - 'ignores files by regex', - async () => { - await createTempfiles(undefined, 'foo'); - await createTempfiles(undefined, 'bar'); - - const [stdout, stderr] = await run([ - '--exclude=/.*Component\\.js/', - tempDir, - ]); - - expect(stdout).toBe(''); - expect(stderr).toBe(''); - }, - TEST_TIMEOUT, - ); - - test( - 'ignores files by regex shortcut', - async () => { - await createTempfiles(undefined, 'foo'); - await createTempfiles(undefined, 'bar'); - - const [stdout, stderr] = await run([ - '-e', - '/.*Component\\.js/', - tempDir, - ]); - - expect(stdout).toBe(''); - expect(stderr).toBe(''); - }, - TEST_TIMEOUT, - ); - }); -}); diff --git a/packages/react-docgen-cli/src/cli.ts b/packages/react-docgen-cli/src/cli.ts new file mode 100755 index 00000000000..f9ca2b1a5fb --- /dev/null +++ b/packages/react-docgen-cli/src/cli.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env node +import { program } from 'commander'; + +program + .name('react-docgen') + .executableDir('./commands/') + .command('debug', 'Print information for debugging') + .command('parse', 'Extract meta information from React components.', { + isDefault: true, + executableFile: 'parse/command.js', + }); + +program.parse(); diff --git a/packages/react-docgen-cli/src/commands/parse/command.ts b/packages/react-docgen-cli/src/commands/parse/command.ts new file mode 100644 index 00000000000..fe2bdaf3b0e --- /dev/null +++ b/packages/react-docgen-cli/src/commands/parse/command.ts @@ -0,0 +1,160 @@ +#!/usr/bin/env node +import glob from 'fast-glob'; +import debugFactory from 'debug'; +import { program } from 'commander'; +import { builtinHandlers, parse } from 'react-docgen'; +import { readFile } from 'fs/promises'; +import outputResult from './output/outputResult.js'; +import loadOptions from './options/loadOptions.js'; +import outputError from './output/outputError.js'; +import { resolve } from 'path'; +import slash from 'slash'; + +const debug = debugFactory('react-docgen:cli'); + +const defaultIgnoreGlobs = [ + '**/node_modules/**', + '**/__tests__/**', + '**/__mocks__/**', +]; + +const defaultHandlers = Object.keys(builtinHandlers); + +function collect(value: string, previous: string[]) { + if ( + !previous || + previous === defaultIgnoreGlobs || + previous === defaultHandlers + ) { + previous = []; + } + + const values = value.split(','); + + return previous.concat(values); +} + +interface CLIOptions { + defaultIgnores: boolean; + failOnWarning: boolean; + handlers?: string[]; + ignore: string[]; + importer?: string; + out?: string; + pretty: boolean; + resolver?: string; +} + +program + .name('react-docgen-parse') + .description( + 'Extract meta information from React components.\n' + + 'Either specify a paths to files or a glob pattern that matches multiple files.', + ) + .option( + '-o, --out ', + 'Store extracted information in the specified file instead of printing to stdout. If the file exists it will be overwritten.', + ) + .option( + '-i, --ignore ', + 'Comma separated list of glob patterns which will ignore the paths that match. Can also be used multiple times.', + collect, + defaultIgnoreGlobs, + ) + .option( + '--no-default-ignores', + 'Do not ignore the node_modules, __tests__, and __mocks__ directories.', + ) + .option('--pretty', 'Print the output JSON pretty', false) + .option( + '--failOnWarning', + 'Fail with exit code 2 on react-docgen component warnings. This includes "no component found" and "multiple components found" warnings.', + false, + ) + .option( + '--resolver ', + 'Built-in resolver name (findAllComponentDefinitions, findAllExportedComponentDefinitions, findExportedComponentDefinition) or path to a module that exports a resolver.', + 'findExportedComponentDefinition', + ) + .option( + '--importer ', + 'Built-in importer name (fsImport, ignoreImporter) or path to a module that exports an importer.', + 'fsImporter', + ) + .option( + '--handlers ', + 'Comma separated list of handlers to use. Can also be used multiple times. When used no default handlers will be added.', + collect, + defaultHandlers, + ) + .argument('', 'Can be globs or paths to files') + .action(async (globs: string[], input: CLIOptions) => { + const { + defaultIgnores, + failOnWarning, + handlers, + ignore, + importer, + out: output, + pretty, + resolver, + } = input; + + let finalIgnores = ignore; + + // Push the default ignores unless the --no-default-ignore is set + if (defaultIgnores === true && ignore !== defaultIgnoreGlobs) { + finalIgnores.push(...defaultIgnoreGlobs); + } else if (defaultIgnores === false && ignore === defaultIgnoreGlobs) { + finalIgnores = []; + } + + const options = await loadOptions({ + handlers, + importer, + resolver, + }); + // we use slash to convert windows backslashes to unix format so fast-glob works + const files = await glob(globs.map(slash), { + ignore: finalIgnores?.map(ignorePath => { + ignorePath = ignorePath.trim(); + // If the ignore glob starts with a dot we need to resolve the path to an + // absolute path in order for it to work + if (ignorePath.startsWith('.')) { + ignorePath = resolve(process.cwd(), ignorePath); + } + + // we use slash to convert windows backslashes to unix format so fast-glob works + return slash(ignorePath); + }), + }); + const result = {}; + let errorEncountered = false; + + await Promise.all( + files.map(async path => { + debug(`Reading file ${path}`); + const content = await readFile(path, 'utf-8'); + + try { + result[path] = parse(content, { + filename: path, + handlers: options.handlers, + importer: options.importer, + resolver: options.resolver, + }); + } catch (error) { + const isError = outputError(error as Error, path, { failOnWarning }); + + if (isError) { + errorEncountered = true; + } + } + }), + ); + if (!errorEncountered) { + await outputResult(result, { pretty, output }); + } + }); + +program.parse(); diff --git a/packages/react-docgen-cli/src/commands/parse/options/loadOptions.ts b/packages/react-docgen-cli/src/commands/parse/options/loadOptions.ts new file mode 100644 index 00000000000..e17466529d8 --- /dev/null +++ b/packages/react-docgen-cli/src/commands/parse/options/loadOptions.ts @@ -0,0 +1,49 @@ +import type { Handler, Importer, Resolver } from 'react-docgen'; +import { + builtinHandlers, + builtinImporters, + builtinResolvers, +} from 'react-docgen'; +import loadReactDocgenPlugin from './loadReactDocgenPlugin.js'; + +export default async function loadOptions(input: { + handlers: string[] | undefined; + importer: string | undefined; + resolver: string | undefined; +}): Promise<{ + handlers: Handler[] | undefined; + importer: Importer | undefined; + resolver: Resolver | undefined; +}> { + const resolver = + input.resolver && input.resolver.length !== 0 + ? await loadReactDocgenPlugin( + input.resolver, + 'resolver', + builtinResolvers, + ) + : undefined; + + const importer = + input.importer && input.importer.length !== 0 + ? await loadReactDocgenPlugin( + input.importer, + 'importer', + builtinImporters, + ) + : undefined; + + const handlers = input.handlers + ? await Promise.all( + input.handlers.map(async handler => { + return await loadReactDocgenPlugin( + handler, + 'handler', + builtinHandlers, + ); + }), + ) + : undefined; + + return { handlers, importer, resolver }; +} diff --git a/packages/react-docgen-cli/src/commands/parse/options/loadReactDocgenPlugin.ts b/packages/react-docgen-cli/src/commands/parse/options/loadReactDocgenPlugin.ts new file mode 100644 index 00000000000..317f7733fbd --- /dev/null +++ b/packages/react-docgen-cli/src/commands/parse/options/loadReactDocgenPlugin.ts @@ -0,0 +1,26 @@ +import { resolve } from 'path'; +import importFile from '../../../utils/importFile.js'; + +export default async function loadReactDocgenPlugin( + input: string, + name: string, + builtins: Record, +): Promise { + if (builtins[input]) { + return builtins[input]; + } + + const path = resolve(process.cwd(), input); + // Maybe it is local path or a package + const importer: T | undefined = + (await importFile(path)) ?? (await importFile(input)); + + if (importer) { + return importer; + } + + throw new Error( + `Unknown ${name}: "${input}" is not a built-in ${name}, ` + + `not a package, and can not be found locally ("${path}")`, + ); +} diff --git a/packages/react-docgen-cli/src/commands/parse/output/outputError.ts b/packages/react-docgen-cli/src/commands/parse/output/outputError.ts new file mode 100644 index 00000000000..6ae29c40d20 --- /dev/null +++ b/packages/react-docgen-cli/src/commands/parse/output/outputError.ts @@ -0,0 +1,50 @@ +import { relative } from 'path'; +import chalk from 'chalk'; + +function isReactDocgenError(error: NodeJS.ErrnoException) { + return error instanceof Error && error.code?.startsWith('ERR_REACTDOCGEN'); +} + +function outputReactDocgenError( + error: Error, + filePath: string, + { failOnWarning }: { failOnWarning: boolean }, +): boolean { + let label = 'WARNING'; + let color = chalk.yellow; + let log = console.warn; + let isError = false; + + if (failOnWarning && isReactDocgenError(error)) { + process.exitCode = 2; + isError = true; + label = 'ERROR'; + color = chalk.red; + log = console.error; + } + + log( + color( + `▶ ${label}: ${error.message} 👀\n in ${chalk.underline( + relative(process.cwd(), filePath), + )}\n`, + ), + ); + + return isError; +} + +export default function outputError( + error: Error, + filePath: string, + options: { failOnWarning: boolean }, +) { + if (isReactDocgenError(error)) { + return outputReactDocgenError(error, filePath, options); + } else { + process.exitCode = 1; + console.error(error); + + return true; + } +} diff --git a/packages/react-docgen-cli/src/commands/parse/output/outputResult.ts b/packages/react-docgen-cli/src/commands/parse/output/outputResult.ts new file mode 100644 index 00000000000..e6daa8f3841 --- /dev/null +++ b/packages/react-docgen-cli/src/commands/parse/output/outputResult.ts @@ -0,0 +1,19 @@ +import { writeFile } from 'fs/promises'; +import type { Documentation } from 'react-docgen'; + +export default async function outputResult( + documentation: Record, + { pretty = false, output }: { pretty: boolean; output: string | undefined }, +) { + const result = JSON.stringify( + documentation, + undefined, + pretty ? 2 : undefined, + ); + + if (output) { + await writeFile(output, result, 'utf-8'); + } else { + process.stdout.write(result + '\n'); + } +} diff --git a/packages/react-docgen-cli/src/react-docgen.ts b/packages/react-docgen-cli/src/react-docgen.ts deleted file mode 100755 index e72f3aa6651..00000000000 --- a/packages/react-docgen-cli/src/react-docgen.ts +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env node -import argv from 'commander'; - -function collect(val, memo) { - memo.push(val); - - return memo; -} - -const defaultExtensions = ['js', 'jsx']; -const defaultExclude = []; -const defaultIgnore = ['node_modules', '__tests__', '__mocks__']; - -argv - .usage('[path...] [options]') - .description( - 'Extract meta information from React components.\n' + - ' If a directory is passed, it is recursively traversed.', - ) - .option('-o, --out ', 'Store extracted information in the FILE') - .option('--pretty', 'pretty print JSON') - .option( - '-x, --extension ', - 'File extensions to consider. Repeat to define multiple extensions. Default: ' + - JSON.stringify(defaultExtensions), - collect, - ['js', 'jsx'], - ) - .option( - '-e, --exclude ', - 'Filename or regex to exclude. Default: ' + JSON.stringify(defaultExclude), - collect, - [], - ) - .option( - '-i, --ignore ', - 'Folders to ignore. Default: ' + JSON.stringify(defaultIgnore), - collect, - ['node_modules', '__tests__', '__mocks__'], - ) - .option( - '--resolver ', - 'Resolver name (findAllComponentDefinitions, findExportedComponentDefinition) or path to a module that exports ' + - 'a resolver. Default: findExportedComponentDefinition', - 'findExportedComponentDefinition', - ) - .arguments(''); - -argv.parse(process.argv); - -import fs from 'fs'; -import parser from 'react-docgen'; -import path from 'path'; - -const output = argv.out; -const paths = argv.args || []; -const extensions = new RegExp('\\.(?:' + argv.extension.join('|') + ')$'); -const ignoreDir = argv.ignore; -let excludePatterns = argv.exclude; -let resolver; -let errorMessage; -const regexRegex = /^\/(.*)\/([igymu]{0,5})$/; - -if ( - excludePatterns && - excludePatterns.length === 1 && - regexRegex.test(excludePatterns[0]) -) { - const match = excludePatterns[0].match(regexRegex); - - excludePatterns = new RegExp(match[1], match[2]); -} - -if (argv.resolver) { - try { - // Look for built-in resolver - // resolver from `../dist/resolver/${argv.resolver}`).default; - } catch (error) { - if (error.code !== 'MODULE_NOT_FOUND') { - throw error; - } - const resolverPath = path.resolve(process.cwd(), argv.resolver); - - try { - // Look for local resolver - //resolver from resolverPath); - } catch (localError) { - if (localError.code !== 'MODULE_NOT_FOUND') { - throw localError; - } - // Will exit with this error message - errorMessage = - `Unknown resolver: "${argv.resolver}" is neither a built-in resolver ` + - `nor can it be found locally ("${resolverPath}")`; - } - } -} - -function parse(source, filename) { - /*return parser.parse(source, resolver, null, { - filename, - });*/ -} - -function writeError(msg, filePath) { - if (filePath) { - process.stderr.write('Error with path "' + filePath + '": '); - } - process.stderr.write(msg + '\n'); - if (msg instanceof Error) { - process.stderr.write(msg.stack + '\n'); - } -} - -function writeResult(result) { - result = argv.pretty - ? JSON.stringify(result, null, 2) - : JSON.stringify(result); - if (output) { - fs.writeFileSync(output, result); - } else { - process.stdout.write(result + '\n'); - } -} - -function traverseDir(filePath, result, done) { - dir.readFiles( - filePath, - { - match: extensions, - exclude: excludePatterns, - excludeDir: ignoreDir, - }, - function (error, content, filename, next) { - if (error) { - throw error; - } - try { - result[filename] = parse(content, path.join(filePath, filename)); - } catch (parseError) { - writeError(parseError, filename); - } - next(); - }, - function (error) { - if (error) { - throw error; - } - done(); - }, - ); -} - -/** - * 1. An error occurred, so exit - */ -if (errorMessage) { - writeError(errorMessage); - process.exitCode = 1; -} else if (paths.length === 0) { - /** - * 2. No files passed, consume input stream - */ - let source = ''; - - process.stdin.setEncoding('utf8'); - process.stdin.resume(); - const timer = setTimeout(function () { - process.stderr.write('Still waiting for std input...'); - }, 5000); - - process.stdin.on('data', function (chunk) { - clearTimeout(timer); - source += chunk; - }); - process.stdin.on('end', function () { - try { - writeResult(parse(source)); - } catch (error) { - writeError(error); - } - }); -} else { - /** - * 3. Paths are passed - */ - const result = Object.create(null); - - async.eachSeries( - paths, - function (filePath, done) { - fs.stat(filePath, function (error, stats) { - if (error) { - writeError(error, filePath); - done(); - - return; - } - if (stats.isDirectory()) { - try { - traverseDir(filePath, result, done); - } catch (traverseError) { - writeError(traverseError); - done(); - } - } else { - try { - result[filePath] = parse(fs.readFileSync(filePath), filePath); - } catch (parseError) { - writeError(parseError, filePath); - } finally { - done(); - } - } - }); - }, - function () { - const resultsPaths = Object.keys(result); - - if (resultsPaths.length === 0) { - // we must have gotten an error - process.exitCode = 1; - } else if (paths.length === 1) { - // a single path? - fs.stat(paths[0], function (error, stats) { - writeResult(stats.isDirectory() ? result : result[resultsPaths[0]]); - }); - } else { - writeResult(result); - } - }, - ); -} diff --git a/packages/react-docgen-cli/src/utils/importFile.ts b/packages/react-docgen-cli/src/utils/importFile.ts new file mode 100644 index 00000000000..6c40e2df697 --- /dev/null +++ b/packages/react-docgen-cli/src/utils/importFile.ts @@ -0,0 +1,28 @@ +import { createRequire } from 'module'; +import { pathToFileURL } from 'url'; + +const require = createRequire(import.meta.url); +const resolveOptions = { paths: [process.cwd()] }; + +export default async function importFile( + importSpecifier: string, +): Promise { + try { + const importedFile = await import( + // need to convert to file:// url as on windows absolute path strings do not work + pathToFileURL(require.resolve(importSpecifier, resolveOptions)).href + ); + + return importedFile.default ? importedFile.default : importFile; + } catch (error) { + if ( + error instanceof Error && + (error as NodeJS.ErrnoException).code !== 'ERR_MODULE_NOT_FOUND' && + (error as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND' + ) { + throw error; + } + + return undefined; + } +} diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/basic/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/basic/Component.js new file mode 100644 index 00000000000..bde42400052 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/basic/Component.js @@ -0,0 +1,7 @@ +const React = require('react'); + +module.exports = React.createClass({ + displayName: 'Component', + otherMethod: function () {}, + render: function () {}, +}); diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/basic/NoComponent.js b/packages/react-docgen-cli/tests/integration/__fixtures__/basic/NoComponent.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/handler.cjs b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/handler.cjs new file mode 100644 index 00000000000..e9f492ec5cd --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-cjs/handler.cjs @@ -0,0 +1,5 @@ +const testHandler = function (documentation) { + documentation.set('displayName', 'testhandler'); +}; + +module.exports = testHandler; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/handler.mjs b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/handler.mjs new file mode 100644 index 00000000000..4c6b55a5dd6 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-esm/handler.mjs @@ -0,0 +1,5 @@ +const testHandler = function (documentation) { + documentation.set('displayName', 'testhandler'); +}; + +export default testHandler; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/index.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/index.js new file mode 100644 index 00000000000..e9f492ec5cd --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/index.js @@ -0,0 +1,5 @@ +const testHandler = function (documentation) { + documentation.set('displayName', 'testhandler'); +}; + +module.exports = testHandler; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/package.json b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/package.json new file mode 100644 index 00000000000..a92d0ff2c41 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-handler-npm/node_modules/test-react-docgen-handler/package.json @@ -0,0 +1,3 @@ +{ + "name": "test-react-docgen-handler" +} diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/importer.mjs b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/importer.mjs new file mode 100644 index 00000000000..274ce883f7a --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-esm/importer.mjs @@ -0,0 +1,15 @@ +const testImporter = function (pa, n, file) { + const newFile = file.parse('("importer")', 'x.js'); + + let path; + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return path.get('body')[0].get('expression'); +}; + +export default testImporter; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/index.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/index.js new file mode 100644 index 00000000000..d8c52d2585e --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/index.js @@ -0,0 +1,15 @@ +const testImporter = function (pa, n, file) { + const newFile = file.parse('("importer")', 'x.js'); + + let path; + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return path.get('body')[0].get('expression'); +}; + +module.exports = testImporter; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/package.json b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/package.json new file mode 100644 index 00000000000..65df75ff836 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/custom-importer-npm/node_modules/test-react-docgen-importer/package.json @@ -0,0 +1,3 @@ +{ + "name": "test-react-docgen-importer" +} diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/importer.cjs b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/importer.cjs new file mode 100644 index 00000000000..cc56ed37ee4 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-cjs/importer.cjs @@ -0,0 +1,15 @@ +const testImporter = function (pa, n, file) { + const newFile = file.parse('("importer")', 'x.js'); + + let path; + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return path.get('body')[0].get('expression'); +}; + +module.exports = testImporter; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/importer.mjs b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/importer.mjs new file mode 100644 index 00000000000..274ce883f7a --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-esm/importer.mjs @@ -0,0 +1,15 @@ +const testImporter = function (pa, n, file) { + const newFile = file.parse('("importer")', 'x.js'); + + let path; + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return path.get('body')[0].get('expression'); +}; + +export default testImporter; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/Component.js new file mode 100644 index 00000000000..a3c7eb41603 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/Component.js @@ -0,0 +1,10 @@ +const React = require('react'); +import x from './other.cjs'; + +function Component() { + return
; +}; + +Component.displayName = x + +module.exports = Component diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/index.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/index.js new file mode 100644 index 00000000000..d8c52d2585e --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/index.js @@ -0,0 +1,15 @@ +const testImporter = function (pa, n, file) { + const newFile = file.parse('("importer")', 'x.js'); + + let path; + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return path.get('body')[0].get('expression'); +}; + +module.exports = testImporter; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/package.json b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/package.json new file mode 100644 index 00000000000..65df75ff836 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-importer-npm/node_modules/test-react-docgen-importer/package.json @@ -0,0 +1,3 @@ +{ + "name": "test-react-docgen-importer" +} diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/Component.js new file mode 100644 index 00000000000..1a0ceba0d5f --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/Component.js @@ -0,0 +1,8 @@ +const React = require('react'); + +const Component = React.createClass({ + displayName: 'Component', + render: function () {}, +}); + +module.exports = Component; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/resolver.cjs b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/resolver.cjs new file mode 100644 index 00000000000..8fcf0e1763a --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-cjs/resolver.cjs @@ -0,0 +1,23 @@ +const code = ` + ({ + displayName: 'Custom', + }) +`; + +const customResolver = function ( + file, +) { + const newFile = file.parse(code, 'x.js'); + let path; + + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return [path.get('body')[0].get('expression')]; +}; + +module.exports = customResolver; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/Component.js new file mode 100644 index 00000000000..1a0ceba0d5f --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/Component.js @@ -0,0 +1,8 @@ +const React = require('react'); + +const Component = React.createClass({ + displayName: 'Component', + render: function () {}, +}); + +module.exports = Component; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/resolver.mjs b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/resolver.mjs new file mode 100644 index 00000000000..d1d89b17efa --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-esm/resolver.mjs @@ -0,0 +1,23 @@ +const code = ` + ({ + displayName: 'Custom', + }) +`; + +const customResolver = function ( + file, +) { + const newFile = file.parse(code, 'x.js'); + let path; + + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return [path.get('body')[0].get('expression')]; +}; + +export default customResolver; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/Component.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/Component.js new file mode 100644 index 00000000000..1a0ceba0d5f --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/Component.js @@ -0,0 +1,8 @@ +const React = require('react'); + +const Component = React.createClass({ + displayName: 'Component', + render: function () {}, +}); + +module.exports = Component; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/index.js b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/index.js new file mode 100644 index 00000000000..8fcf0e1763a --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/index.js @@ -0,0 +1,23 @@ +const code = ` + ({ + displayName: 'Custom', + }) +`; + +const customResolver = function ( + file, +) { + const newFile = file.parse(code, 'x.js'); + let path; + + newFile.traverse({ + Program(p) { + path = p; + p.stop(); + } + }); + + return [path.get('body')[0].get('expression')]; +}; + +module.exports = customResolver; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/package.json b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/package.json new file mode 100644 index 00000000000..8d3ed80b3d1 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/custom-resolver-npm/node_modules/test-react-docgen-resolver/package.json @@ -0,0 +1,3 @@ +{ + "name": "test-react-docgen-resolver" +} diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__mocks__/MockComponent.js b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__mocks__/MockComponent.js new file mode 100644 index 00000000000..0858c85792a --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__mocks__/MockComponent.js @@ -0,0 +1,8 @@ +const React = require('react'); + +const Component = React.createClass({ + displayName: 'MockComponent', + render: function () {}, +}); + +module.exports = Component; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__tests__/TestComponent.js b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__tests__/TestComponent.js new file mode 100644 index 00000000000..de3324c137a --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/__tests__/TestComponent.js @@ -0,0 +1,8 @@ +const React = require('react'); + +const Component = React.createClass({ + displayName: 'TestComponent', + render: function () {}, +}); + +module.exports = Component; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/foo/FooComponent.js b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/foo/FooComponent.js new file mode 100644 index 00000000000..4843f1e0d63 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/foo/FooComponent.js @@ -0,0 +1,8 @@ +const React = require('react'); + +const Component = React.createClass({ + displayName: 'FooComponent', + render: function () {}, +}); + +module.exports = Component; diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/node_modules/NodeModulesComponent.js b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/node_modules/NodeModulesComponent.js new file mode 100644 index 00000000000..8ee5d6b4f95 --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/ignore/node_modules/NodeModulesComponent.js @@ -0,0 +1,8 @@ +const React = require('react'); + +const Component = React.createClass({ + displayName: 'NodeModulesComponent', + render: function () {}, +}); + +module.exports = Component; diff --git a/packages/react-docgen-cli/src/__tests__/__fixtures__/MultipleComponents.js b/packages/react-docgen-cli/tests/integration/__fixtures__/multiple/MultipleComponents.js similarity index 100% rename from packages/react-docgen-cli/src/__tests__/__fixtures__/MultipleComponents.js rename to packages/react-docgen-cli/tests/integration/__fixtures__/multiple/MultipleComponents.js diff --git a/packages/react-docgen-cli/tests/integration/__fixtures__/syntax-error/SyntaxError.js b/packages/react-docgen-cli/tests/integration/__fixtures__/syntax-error/SyntaxError.js new file mode 100644 index 00000000000..8f7c4dfc85a --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/__fixtures__/syntax-error/SyntaxError.js @@ -0,0 +1 @@ +module.exports {} diff --git a/packages/react-docgen-cli/tests/integration/cli-test.ts b/packages/react-docgen-cli/tests/integration/cli-test.ts new file mode 100644 index 00000000000..91a212d031b --- /dev/null +++ b/packages/react-docgen-cli/tests/integration/cli-test.ts @@ -0,0 +1,585 @@ +// NOTE: This test spawns a subprocesses that load the files from dist/, not +// src/. Before running this test run `build`. + +import { readFile, rm, stat } from 'fs/promises'; +import { dirname, join } from 'path'; +import type { ExecaError } from 'execa'; +import { execaNode } from 'execa'; +import copy from 'cpy'; +import { temporaryDirectory, temporaryFile } from 'tempy'; +import { describe, expect, test } from 'vitest'; +import { fileURLToPath } from 'url'; +import { + builtinHandlers, + builtinImporters, + builtinResolvers, +} from 'react-docgen'; + +const __dir = dirname(fileURLToPath(import.meta.url)); + +const fixtureDir = join(__dir, '__fixtures__'); +const cliBinary = join(__dir, '../../dist/cli.js'); + +describe('cli', () => { + async function withFixture( + fixture: string, + callback: (api: { + dir: string; + run: ( + args: readonly string[], + ) => Promise<{ stdout: string; stderr: string; exitCode: number }>; + }) => Promise, + ): Promise { + const tempDir = temporaryDirectory(); + + async function run(args: readonly string[]) { + try { + return await execaNode(cliBinary, args, { + cwd: tempDir, + }); + } catch (error) { + return error as ExecaError; + } + } + + await stat(join(fixtureDir, fixture)); + + await copy(join(fixtureDir, fixture, '**/*'), tempDir, {}); + await callback({ dir: tempDir, run }); + await rm(tempDir, { force: true, recursive: true }); + } + + describe('glob', () => { + test('reads files provided as command line arguments', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + join(dir, `Component.js`), + join(dir, `NoComponent.js`), + ]); + + expect(stderr).toContain('NoComponent.js'); + expect(stdout).toContain('Component.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('reads absolute globs provided as command line arguments', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([`${dir}/*`]); + + expect(stderr).toContain('NoComponent.js'); + expect(stdout).toContain('Component.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('reads relative globs provided as command line arguments', async () => { + await withFixture('basic', async ({ run }) => { + const { stdout, stderr } = await run(['./*']); + + expect(stderr).toContain('NoComponent.js'); + expect(stdout).toContain('Component.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + }); + + describe('ignore', () => { + test('ignores files in node_modules, __tests__ and __mocks__ by default', async () => { + await withFixture('ignore', async ({ dir, run }) => { + const { stdout, stderr } = await run([`${dir}/**/*`]); + + expect(stderr).toBe(''); + expect(stdout).not.toContain('MockComponent.js'); + expect(stdout).not.toContain('TestComponent.js'); + expect(stdout).not.toContain('NodeModulesComponent.js'); + expect(stdout).toContain('FooComponent.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('can disable default ignores', async () => { + await withFixture('ignore', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--no-default-ignores', + `${dir}/**/*`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('MockComponent.js'); + expect(stdout).toContain('TestComponent.js'); + expect(stdout).toContain('NodeModulesComponent.js'); + expect(stdout).toContain('FooComponent.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('ignores directory defined by glob starting with star', async () => { + await withFixture('ignore', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--ignore=**/foo/**', + `${dir}/**/*`, + ]); + + expect(stderr).toBe(''); + expect(stdout).not.toContain('MockComponent.js'); + expect(stdout).not.toContain('TestComponent.js'); + expect(stdout).not.toContain('NodeModulesComponent.js'); + expect(stdout).not.toContain('FooComponent.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('ignores directory defined by relative path', async () => { + await withFixture('ignore', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--ignore', + './foo/**', + `${dir}/**/*`, + ]); + + expect(stderr).toBe(''); + expect(stdout).not.toContain('MockComponent.js'); + expect(stdout).not.toContain('TestComponent.js'); + expect(stdout).not.toContain('NodeModulesComponent.js'); + expect(stdout).not.toContain('FooComponent.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('ignores directory with shortcut defined by glob starting with star', async () => { + await withFixture('ignore', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '-i', + '**/foo/**', + `${dir}/**/*`, + ]); + + expect(stderr).toBe(''); + expect(stdout).not.toContain('MockComponent.js'); + expect(stdout).not.toContain('TestComponent.js'); + expect(stdout).not.toContain('NodeModulesComponent.js'); + expect(stdout).not.toContain('FooComponent.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('ignores directory with shortcut defined by relative path', async () => { + await withFixture('ignore', async ({ dir, run }) => { + const { stdout, stderr } = await run(['-i', './foo/**', `${dir}/**/*`]); + + expect(stderr).toBe(''); + expect(stdout).not.toContain('MockComponent.js'); + expect(stdout).not.toContain('TestComponent.js'); + expect(stdout).not.toContain('NodeModulesComponent.js'); + expect(stdout).not.toContain('FooComponent.js'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + }); + + describe('out', () => { + test('writes to a file if provided', async () => { + await withFixture('basic', async ({ dir, run }) => { + const outFile = temporaryFile(); + const { stdout, stderr } = await run([ + `--out=${outFile}`, + `${dir}/Component.js`, + ]); + + const writtenResult = await readFile(outFile, 'utf-8'); + + expect(stderr).toBe(''); + expect(writtenResult.length).toBeGreaterThan(4); + expect(() => JSON.parse(writtenResult)).not.toThrowError(); + expect(stdout).toBe(''); + }); + }); + + test('writes to a file if provided shortcut', async () => { + await withFixture('basic', async ({ dir, run }) => { + const outFile = temporaryFile(); + const { stdout, stderr } = await run([ + '-o', + outFile, + `${dir}/Component.js`, + ]); + + const writtenResult = await readFile(outFile, 'utf-8'); + + expect(stderr).toBe(''); + expect(writtenResult.length).toBeGreaterThan(4); + expect(() => JSON.parse(writtenResult)).not.toThrowError(); + expect(stdout).toBe(''); + }); + }); + }); + + describe('resolver', () => { + describe('accepts the names of builtin resolvers', () => { + test.each(Object.keys(builtinResolvers))('%s', async importer => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--resolver=${importer}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Component'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + }); + + describe('custom resolver', () => { + test('accepts an absolute local CommonJS path', async () => { + await withFixture('custom-resolver-cjs', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--resolver=${join(dir, 'resolver.cjs')}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Custom'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a relative local CommonJS path', async () => { + await withFixture('custom-resolver-cjs', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--resolver', + './resolver.cjs', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Custom'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts an absolute local ESM path', async () => { + await withFixture('custom-resolver-esm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--resolver=${join(dir, 'resolver.mjs')}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Custom'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a relative local ESM path', async () => { + await withFixture('custom-resolver-esm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--resolver', + './resolver.mjs', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Custom'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a npm package', async () => { + await withFixture('custom-resolver-npm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--resolver=test-react-docgen-resolver', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Custom'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('throws error when not found', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--resolver=does-not-exist', + `${dir}/Component.js`, + ]); + + expect(stderr).toContain('Unknown resolver: "does-not-exist"'); + expect(stdout).toBe(''); + }); + }); + }); + }); + + describe('importer', () => { + describe('accepts the names of builtin importers', () => { + test.each(Object.keys(builtinImporters))('%s', async importer => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--importer=${importer}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Component'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + }); + + describe('custom importer', () => { + test('accepts an absolute local CommonJS path', async () => { + await withFixture('custom-importer-cjs', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--importer=${join(dir, 'importer.cjs')}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"importer"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a relative local CommonJS path', async () => { + await withFixture('custom-importer-cjs', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--importer', + './importer.cjs', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"importer"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts an absolute local ESM path', async () => { + await withFixture('custom-importer-esm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--importer=${join(dir, 'importer.mjs')}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"importer"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a relative local ESM path', async () => { + await withFixture('custom-importer-esm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--importer', + './importer.mjs', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"importer"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a npm package', async () => { + await withFixture('custom-importer-npm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--importer=test-react-docgen-importer', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"importer"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('throws error when not found', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--importer=does-not-exist', + `${dir}/Component.js`, + ]); + + expect(stderr).toContain('Unknown importer: "does-not-exist"'); + expect(stdout).toBe(''); + }); + }); + }); + }); + + describe('handlers', () => { + describe('accepts the names of builtin handlers', () => { + test.each(Object.keys(builtinHandlers))('%s', async importer => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--handlers=${importer}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('Component'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + }); + + describe('multiple handlers', () => { + test('multiple handlers arguments', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--handlers=displayNameHandler`, + `--handlers=componentDocblockHandler`, + `--handlers=componentMethodsHandler`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"Component"'); + expect(stdout).toContain('"description":""'); + expect(stdout).toContain('"name":"otherMethod"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('multiple handlers comma separated', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--handlers=displayNameHandler,componentDocblockHandler,componentMethodsHandler`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"Component"'); + expect(stdout).toContain('"description":""'); + expect(stdout).toContain('"name":"otherMethod"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + }); + + describe('custom handlers', () => { + test('accepts an absolute local CommonJS path', async () => { + await withFixture('custom-handler-cjs', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--handlers=${join(dir, 'handler.cjs')}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"testhandler"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a relative local CommonJS path', async () => { + await withFixture('custom-handler-cjs', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--handlers', + './handler.cjs', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"testhandler"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts an absolute local ESM path', async () => { + await withFixture('custom-handler-esm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + `--handlers=${join(dir, 'handler.mjs')}`, + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"testhandler"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a relative local ESM path', async () => { + await withFixture('custom-handler-esm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--handlers', + './handler.mjs', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"testhandler"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('accepts a npm package', async () => { + await withFixture('custom-handler-npm', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--handlers=test-react-docgen-handler', + `${dir}/Component.js`, + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('"displayName":"testhandler"'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('throws error when not found', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--handlers=does-not-exist', + `${dir}/Component.js`, + ]); + + expect(stderr).toContain('Unknown handler: "does-not-exist"'); + expect(stdout).toBe(''); + }); + }); + }); + }); + + describe('pretty', () => { + test('by default does not prettify output', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([join(dir, `Component.js`)]); + + expect(stderr).toBe(''); + expect(stdout).not.toContain('\n'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + + test('does prettify output', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr } = await run([ + '--pretty', + join(dir, `Component.js`), + ]); + + expect(stderr).toBe(''); + expect(stdout).toContain('\n'); + expect(() => JSON.parse(stdout)).not.toThrowError(); + }); + }); + }); + + describe('error', () => { + test('does exit with code 2 on warning', async () => { + await withFixture('basic', async ({ dir, run }) => { + const { stdout, stderr, exitCode } = await run([ + '--failOnWarning', + `${dir}/*`, + ]); + + expect(stderr).toContain('NoComponent'); + expect(stdout).toEqual(''); + expect(exitCode).toBe(2); + }); + }); + }); +}); diff --git a/packages/react-docgen/package.json b/packages/react-docgen/package.json index b8dfcae86d6..5a70292f9fd 100644 --- a/packages/react-docgen/package.json +++ b/packages/react-docgen/package.json @@ -17,8 +17,8 @@ "main": "dist/main.js", "typings": "dist/main.d.ts", "scripts": { - "build": "rimraf dist/ && tsc", - "watch": "rimraf dist/ && tsc --watch" + "build": "rimraf dist/ && yarn g:tsc", + "watch": "rimraf dist/ && yarn g:tsc --watch" }, "keywords": [ "react", diff --git a/packages/react-docgen/src/utils/__tests__/getMemberValuePath-test.ts b/packages/react-docgen/src/utils/__tests__/getMemberValuePath-test.ts index 6c97736bb83..a7c48e5e952 100644 --- a/packages/react-docgen/src/utils/__tests__/getMemberValuePath-test.ts +++ b/packages/react-docgen/src/utils/__tests__/getMemberValuePath-test.ts @@ -17,7 +17,7 @@ vi.mock('../getClassMemberValuePath.js'); vi.mock('../getMemberExpressionValuePath.js'); // https://github.com/vitest-dev/vitest/issues/2381 -describe.skip('getMemberValuePath', () => { +describe('getMemberValuePath', () => { test('handles ObjectExpressions', () => { const path = parse.expression('{}'); diff --git a/vitest.config.ts b/vitest.config.ts index 969c4eb21ae..a1c91b6664e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,8 @@ export default defineConfig({ interopDefault: false, }, coverage: { + all: true, + include: ['packages/react-docgen/src/**'], provider: 'c8', reporter: ['text', 'lcov'], }, diff --git a/yarn.lock b/yarn.lock index 7452ce1f8cc..be2bbaf9ae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1614,8 +1614,13 @@ __metadata: version: 0.0.0-use.local resolution: "@react-docgen/cli@workspace:packages/react-docgen-cli" dependencies: - commander: 9.4.1 + "@types/debug": 4.1.7 + chalk: ^5.1.2 + commander: ^9.4.1 + debug: ^4.3.4 + fast-glob: ^3.2.12 react-docgen: 6.0.0-alpha.3 + slash: ^5.0.0 bin: react-docgen: bin/react-docgen.js languageName: unknown @@ -1714,6 +1719,15 @@ __metadata: languageName: node linkType: hard +"@types/debug@npm:4.1.7": + version: 4.1.7 + resolution: "@types/debug@npm:4.1.7" + dependencies: + "@types/ms": "*" + checksum: 0a7b89d8ed72526858f0b61c6fd81f477853e8c4415bb97f48b1b5545248d2ae389931680b94b393b993a7cfe893537a200647d93defe6d87159b96812305adc + languageName: node + linkType: hard + "@types/doctrine@npm:^0.0.5": version: 0.0.5 resolution: "@types/doctrine@npm:0.0.5" @@ -1850,6 +1864,13 @@ __metadata: languageName: node linkType: hard +"@types/ms@npm:*": + version: 0.7.31 + resolution: "@types/ms@npm:0.7.31" + checksum: daadd354aedde024cce6f5aa873fefe7b71b22cd0e28632a69e8b677aeb48ae8caa1c60e5919bb781df040d116b01cb4316335167a3fc0ef6a63fa3614c0f6da + languageName: node + linkType: hard + "@types/node@npm:*": version: 18.11.10 resolution: "@types/node@npm:18.11.10" @@ -2371,6 +2392,16 @@ __metadata: languageName: node linkType: hard +"aggregate-error@npm:^4.0.0": + version: 4.0.1 + resolution: "aggregate-error@npm:4.0.1" + dependencies: + clean-stack: ^4.0.0 + indent-string: ^5.0.0 + checksum: bb3ffdfd13447800fff237c2cba752c59868ee669104bb995dfbbe0b8320e967d679e683dabb640feb32e4882d60258165cde0baafc4cd467cc7d275a13ad6b5 + languageName: node + linkType: hard + "ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" @@ -2518,6 +2549,13 @@ __metadata: languageName: node linkType: hard +"arrify@npm:^3.0.0": + version: 3.0.0 + resolution: "arrify@npm:3.0.0" + checksum: d6c6f3dad9571234f320e130d57fddb2cc283c87f2ac7df6c7005dffc5161b7bb9376f4be655ed257050330336e84afc4f3020d77696ad231ff580a94ae5aba6 + languageName: node + linkType: hard + "assert@npm:2.0.0": version: 2.0.0 resolution: "assert@npm:2.0.0" @@ -2911,6 +2949,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.1.2": + version: 5.2.0 + resolution: "chalk@npm:5.2.0" + checksum: 03d8060277de6cf2fd567dc25fcf770593eb5bb85f460ce443e49255a30ff1242edd0c90a06a03803b0466ff0687a939b41db1757bec987113e83de89a003caa + languageName: node + linkType: hard + "check-error@npm:^1.0.2": version: 1.0.2 resolution: "check-error@npm:1.0.2" @@ -2983,6 +3028,15 @@ __metadata: languageName: node linkType: hard +"clean-stack@npm:^4.0.0": + version: 4.2.0 + resolution: "clean-stack@npm:4.2.0" + dependencies: + escape-string-regexp: 5.0.0 + checksum: 373f656a31face5c615c0839213b9b542a0a48057abfb1df66900eab4dc2a5c6097628e4a0b5aa559cdfc4e66f8a14ea47be9681773165a44470ef5fb8ccc172 + languageName: node + linkType: hard + "cli-table@npm:0.3.11": version: 0.3.11 resolution: "cli-table@npm:0.3.11" @@ -3083,13 +3137,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:9.4.1, commander@npm:^9.4.1": - version: 9.4.1 - resolution: "commander@npm:9.4.1" - checksum: bfb18e325a5bdf772763c2213d5c7d9e77144d944124e988bcd8e5e65fb6d45d5d4e86b09155d0f2556c9a59c31e428720e57968bcd050b2306e910a0bf3cf13 - languageName: node - linkType: hard - "commander@npm:^2.19.0, commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -3111,6 +3158,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^9.4.1": + version: 9.4.1 + resolution: "commander@npm:9.4.1" + checksum: bfb18e325a5bdf772763c2213d5c7d9e77144d944124e988bcd8e5e65fb6d45d5d4e86b09155d0f2556c9a59c31e428720e57968bcd050b2306e910a0bf3cf13 + languageName: node + linkType: hard + "commondir@npm:^1.0.1": version: 1.0.1 resolution: "commondir@npm:1.0.1" @@ -3225,6 +3279,34 @@ __metadata: languageName: node linkType: hard +"cp-file@npm:^9.1.0": + version: 9.1.0 + resolution: "cp-file@npm:9.1.0" + dependencies: + graceful-fs: ^4.1.2 + make-dir: ^3.0.0 + nested-error-stacks: ^2.0.0 + p-event: ^4.1.0 + checksum: 0ba0fb568baf502676fe15d0869f06703fc108d892bc2dd42097f9019c0215b83b4663b0ee4af5c1048c6d52530c67dfcfe855474be3532b559c4e0f549acb7a + languageName: node + linkType: hard + +"cpy@npm:9.0.1": + version: 9.0.1 + resolution: "cpy@npm:9.0.1" + dependencies: + arrify: ^3.0.0 + cp-file: ^9.1.0 + globby: ^13.1.1 + junk: ^4.0.0 + micromatch: ^4.0.4 + nested-error-stacks: ^2.1.0 + p-filter: ^3.0.0 + p-map: ^5.3.0 + checksum: 5139dfc07d181caefe3ec62c956340a1d02c4afeb794f8c199ddfc7e0cb0bdf5f5e4989ec08d6c07984be119bbb07eb323f21e8edb0733051ddf125a1084b565 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -3948,6 +4030,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -4174,6 +4263,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:6.1.0": + version: 6.1.0 + resolution: "execa@npm:6.1.0" + dependencies: + cross-spawn: ^7.0.3 + get-stream: ^6.0.1 + human-signals: ^3.0.1 + is-stream: ^3.0.0 + merge-stream: ^2.0.0 + npm-run-path: ^5.1.0 + onetime: ^6.0.0 + signal-exit: ^3.0.7 + strip-final-newline: ^3.0.0 + checksum: 1a4af799839134f5c72eb63d525b87304c1114a63aa71676c91d57ccef2e26f2f53e14c11384ab11c4ec479be1efa83d11c8190e00040355c2c5c3364327fa8e + languageName: node + linkType: hard + "execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -4244,7 +4350,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.9": +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" dependencies: @@ -4517,7 +4623,7 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^6.0.0": +"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1": version: 6.0.1 resolution: "get-stream@npm:6.0.1" checksum: e04ecece32c92eebf5b8c940f51468cd53554dcbb0ea725b2748be583c9523d00128137966afce410b9b051eb2ef16d657cd2b120ca8edafcf5a65e81af63cad @@ -4606,6 +4712,19 @@ __metadata: languageName: node linkType: hard +"globby@npm:^13.1.1": + version: 13.1.2 + resolution: "globby@npm:13.1.2" + dependencies: + dir-glob: ^3.0.1 + fast-glob: ^3.2.11 + ignore: ^5.2.0 + merge2: ^1.4.1 + slash: ^4.0.0 + checksum: c148fcda0c981f00fb434bb94ca258f0a9d23cedbde6fb3f37098e1abde5b065019e2c63fe2aa2fad4daf2b54bf360b4d0423d85fb3a63d09ed75a2837d4de0f + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -4902,6 +5021,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^3.0.1": + version: 3.0.1 + resolution: "human-signals@npm:3.0.1" + checksum: f252a7769c8094a5c9dc6772816bdb417b188820b04c8b42d0fc468e03a0ba905b1dd07afabe9385cc83504af1ccc2b985cd1e4aeeeb8e0029896c5af2e6f354 + languageName: node + linkType: hard + "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -4997,6 +5123,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:^5.0.0": + version: 5.0.0 + resolution: "indent-string@npm:5.0.0" + checksum: e466c27b6373440e6d84fbc19e750219ce25865cb82d578e41a6053d727e5520dc5725217d6eb1cc76005a1bb1696a0f106d84ce7ebda3033b963a38583fb3b3 + languageName: node + linkType: hard + "infer-owner@npm:^1.0.4": version: 1.0.4 resolution: "infer-owner@npm:1.0.4" @@ -5388,6 +5521,13 @@ __metadata: languageName: node linkType: hard +"junk@npm:^4.0.0": + version: 4.0.0 + resolution: "junk@npm:4.0.0" + checksum: af79841fbdc0f3a8ec328a4bf68381013c7f52a78821184855a4b19ef95713edb3c30cd144c6393e6159e1b7dfb76b3f682dc983aafb54e52ff321ab1b4a9983 + languageName: node + linkType: hard + "kind-of@npm:^6.0.2": version: 6.0.3 resolution: "kind-of@npm:6.0.3" @@ -5725,6 +5865,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 + languageName: node + linkType: hard + "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -5930,6 +6077,13 @@ __metadata: languageName: node linkType: hard +"nested-error-stacks@npm:^2.0.0, nested-error-stacks@npm:^2.1.0": + version: 2.1.1 + resolution: "nested-error-stacks@npm:2.1.1" + checksum: 5f452fad75db8480b4db584e1602894ff5977f8bf3d2822f7ba5cb7be80e89adf1fffa34dada3347ef313a4288850b4486eb0635b315c32bdfb505577e8880e3 + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -6037,6 +6191,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^5.1.0": + version: 5.1.0 + resolution: "npm-run-path@npm:5.1.0" + dependencies: + path-key: ^4.0.0 + checksum: dc184eb5ec239d6a2b990b43236845332ef12f4e0beaa9701de724aa797fe40b6bbd0157fb7639d24d3ab13f5d5cf22d223a19c6300846b8126f335f788bee66 + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -6123,6 +6286,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: ^4.0.0 + checksum: 0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 + languageName: node + linkType: hard + "open@npm:^8.0.9": version: 8.4.0 resolution: "open@npm:8.4.0" @@ -6155,6 +6327,31 @@ __metadata: languageName: node linkType: hard +"p-event@npm:^4.1.0": + version: 4.2.0 + resolution: "p-event@npm:4.2.0" + dependencies: + p-timeout: ^3.1.0 + checksum: 8a3588f7a816a20726a3262dfeee70a631e3997e4773d23219176333eda55cce9a76219e3d2b441b331eb746e14fdb381eb2694ab9ff2fcf87c846462696fe89 + languageName: node + linkType: hard + +"p-filter@npm:^3.0.0": + version: 3.0.0 + resolution: "p-filter@npm:3.0.0" + dependencies: + p-map: ^5.1.0 + checksum: aacc36820f0531c01963334edc6debf5038b47c83a1c2255b7c14f6964a9a5fc1887ce0b93e72d137727403253bcc9bb26eed9bb79896ece1fa9f52d979bb97b + languageName: node + linkType: hard + +"p-finally@npm:^1.0.0": + version: 1.0.0 + resolution: "p-finally@npm:1.0.0" + checksum: 93a654c53dc805dd5b5891bab16eb0ea46db8f66c4bfd99336ae929323b1af2b70a8b0654f8f1eae924b2b73d037031366d645f1fd18b3d30cbd15950cc4b1d4 + languageName: node + linkType: hard + "p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -6200,6 +6397,15 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^5.1.0, p-map@npm:^5.3.0": + version: 5.5.0 + resolution: "p-map@npm:5.5.0" + dependencies: + aggregate-error: ^4.0.0 + checksum: 065cb6fca6b78afbd070dd9224ff160dc23eea96e57863c09a0c8ea7ce921043f76854be7ee0abc295cff1ac9adcf700e79a1fbe3b80b625081087be58e7effb + languageName: node + linkType: hard + "p-retry@npm:^4.5.0": version: 4.6.2 resolution: "p-retry@npm:4.6.2" @@ -6210,6 +6416,15 @@ __metadata: languageName: node linkType: hard +"p-timeout@npm:^3.1.0": + version: 3.2.0 + resolution: "p-timeout@npm:3.2.0" + dependencies: + p-finally: ^1.0.0 + checksum: 3dd0eaa048780a6f23e5855df3dd45c7beacff1f820476c1d0d1bcd6648e3298752ba2c877aa1c92f6453c7dd23faaf13d9f5149fc14c0598a142e2c5e8d649c + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -6297,6 +6512,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 + languageName: node + linkType: hard + "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -7227,9 +7449,11 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.46.0 "@typescript-eslint/parser": 5.46.0 "@vitest/coverage-c8": 0.25.6 + cpy: 9.0.1 eslint: 8.29.0 eslint-config-prettier: 8.5.0 eslint-plugin-prettier: 4.2.1 + execa: 6.1.0 prettier: 2.8.1 rimraf: 3.0.2 tempy: 3.0.0 @@ -7481,6 +7705,20 @@ __metadata: languageName: node linkType: hard +"slash@npm:^4.0.0": + version: 4.0.0 + resolution: "slash@npm:4.0.0" + checksum: da8e4af73712253acd21b7853b7e0dbba776b786e82b010a5bfc8b5051a1db38ed8aba8e1e8f400dd2c9f373be91eb1c42b66e91abb407ff42b10feece5e1d2d + languageName: node + linkType: hard + +"slash@npm:^5.0.0": + version: 5.0.0 + resolution: "slash@npm:5.0.0" + checksum: 1fa799ee165f7eacf0122ea4252bcf44290db402eb9d3058624ff1d421b8dfe262100dffb0b2cc23f36858666bf661476e2a4c40ebaf3e7b61107cad55a1de88 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -7646,6 +7884,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: 23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 + languageName: node + linkType: hard + "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0"