├── .c8rc.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 01-bug-report.yml │ └── 02-feature-request.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ ├── codespell.yml │ ├── compare-builds.yml │ ├── dependency-review.yml │ ├── generate.yml │ └── scorecard.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .vercelignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── cli.mjs ├── commands │ ├── __tests__ │ │ └── index.test.mjs │ ├── generate.mjs │ ├── index.mjs │ ├── interactive.mjs │ └── types.d.ts └── utils.mjs ├── codecov.yml ├── eslint.config.mjs ├── npm-shrinkwrap.json ├── package.json ├── scripts ├── compare-builds │ └── web.mjs ├── vercel-build.sh └── vercel-prepare.sh ├── shiki.config.mjs └── src ├── __tests__ ├── generators.test.mjs ├── metadata.test.mjs └── streaming.test.mjs ├── constants.mjs ├── generators.mjs ├── generators ├── __tests__ │ └── index.test.mjs ├── addon-verify │ ├── constants.mjs │ ├── index.mjs │ └── utils │ │ ├── __tests__ │ │ ├── generateFileList.test.mjs │ │ └── section.test.mjs │ │ ├── generateFileList.mjs │ │ └── section.mjs ├── api-links │ ├── __tests__ │ │ ├── fixtures.test.mjs │ │ ├── fixtures.test.mjs.snapshot │ │ └── fixtures │ │ │ ├── buffer.js │ │ │ ├── class.js │ │ │ ├── exports.js │ │ │ ├── mod.js │ │ │ ├── prototype.js │ │ │ ├── reverse.js │ │ │ └── root.js │ ├── constants.mjs │ ├── index.mjs │ ├── types.d.ts │ └── utils │ │ ├── checkIndirectReferences.mjs │ │ ├── extractExports.mjs │ │ └── findDefinitions.mjs ├── ast-js │ └── index.mjs ├── ast │ └── index.mjs ├── index.mjs ├── json-simple │ └── index.mjs ├── jsx-ast │ ├── constants.mjs │ ├── index.mjs │ └── utils │ │ ├── __tests__ │ │ ├── ast.test.mjs │ │ ├── buildBarProps.test.mjs │ │ └── buildPropertyTable.test.mjs │ │ ├── ast.mjs │ │ ├── buildBarProps.mjs │ │ ├── buildContent.mjs │ │ ├── buildPropertyTable.mjs │ │ ├── buildSignature.mjs │ │ ├── getSortedHeadNodes.mjs │ │ └── transformer.mjs ├── legacy-html-all │ └── index.mjs ├── legacy-html │ ├── assets │ │ ├── api.js │ │ ├── js-flavor-cjs.svg │ │ ├── js-flavor-esm.svg │ │ └── style.css │ ├── index.mjs │ ├── template.html │ ├── types.d.ts │ └── utils │ │ ├── __tests__ │ │ └── safeCopy.test.mjs │ │ ├── buildContent.mjs │ │ ├── buildDropdowns.mjs │ │ ├── buildExtraContent.mjs │ │ ├── replaceTemplateValues.mjs │ │ ├── safeCopy.mjs │ │ └── tableOfContents.mjs ├── legacy-json-all │ ├── index.mjs │ └── types.d.ts ├── legacy-json │ ├── constants.mjs │ ├── index.mjs │ ├── types.d.ts │ └── utils │ │ ├── __tests__ │ │ ├── buildHierarchy.test.mjs │ │ ├── buildSection.test.mjs │ │ ├── parseList.test.mjs │ │ └── parseSignature.test.mjs │ │ ├── buildHierarchy.mjs │ │ ├── buildSection.mjs │ │ ├── parseList.mjs │ │ └── parseSignature.mjs ├── llms-txt │ ├── index.mjs │ ├── template.txt │ └── utils │ │ ├── __tests__ │ │ └── buildApiDocLink.test.mjs │ │ └── buildApiDocLink.mjs ├── man-page │ ├── constants.mjs │ ├── index.mjs │ ├── template.1 │ └── utils │ │ ├── __tests__ │ │ └── converter.test.mjs │ │ └── converter.mjs ├── metadata │ ├── constants.mjs │ ├── index.mjs │ └── utils │ │ └── parse.mjs ├── orama-db │ ├── __tests__ │ │ └── index.test.mjs │ ├── constants.mjs │ ├── index.mjs │ └── types.d.ts ├── types.d.ts └── web │ ├── constants.mjs │ ├── index.mjs │ ├── template.html │ ├── ui │ ├── components │ │ ├── CodeBox.jsx │ │ ├── MetaBar │ │ │ ├── index.jsx │ │ │ └── index.module.css │ │ ├── NavBar.jsx │ │ ├── SearchBox │ │ │ └── index.jsx │ │ └── SideBar │ │ │ ├── index.jsx │ │ │ └── index.module.css │ ├── constants.mjs │ ├── hooks │ │ ├── useOrama.mjs │ │ └── useTheme.mjs │ ├── index.css │ ├── package.json │ └── types.d.ts │ └── utils │ ├── bundle.mjs │ ├── chunks.mjs │ ├── css.mjs │ ├── data.mjs │ ├── generate.mjs │ └── processing.mjs ├── logger ├── __tests__ │ ├── logger.test.mjs │ └── transports │ │ ├── console.test.mjs │ │ └── github.test.mjs ├── constants.mjs ├── index.mjs ├── logger.mjs ├── transports │ ├── console.mjs │ ├── github.mjs │ └── index.mjs ├── types.d.ts └── utils │ ├── colors.mjs │ └── time.mjs ├── metadata.mjs ├── parsers ├── __tests__ │ └── markdown.test.mjs ├── javascript.mjs ├── json.mjs └── markdown.mjs ├── streaming.mjs ├── threading ├── __tests__ │ └── parallel.test.mjs ├── chunk-worker.mjs ├── index.mjs └── parallel.mjs ├── types.d.ts └── utils ├── __tests__ ├── generators.test.mjs ├── parser.test.mjs └── unist.test.mjs ├── array.mjs ├── generators.mjs ├── highlighter.mjs ├── parser.mjs ├── parser ├── __tests__ │ └── index.test.mjs ├── constants.mjs ├── index.mjs ├── slugger.mjs └── typeMap.json ├── queries ├── __tests__ │ └── index.test.mjs ├── constants.mjs └── index.mjs ├── remark.mjs └── unist.mjs /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "exclude": [ 4 | "eslint.config.mjs", 5 | "**/fixtures", 6 | "src/generators/legacy-html/assets", 7 | "src/generators/web/ui", 8 | "**/*.d.ts" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 80 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Report a Technical/Visual Issue on the Node.js API Docs 2 | description: 'Is something not working as expected? Did you encounter a glitch or a bug with the docs?' 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for reporting an issue you've found with Node.js' API docs. 9 | Please fill in the template below. If unsure about something, just do as best 10 | as you're able. If you are reporting a visual glitch, it will be much easier 11 | for us to fix it when you attach a screenshot as well. 12 | - type: input 13 | attributes: 14 | label: 'URL:' 15 | description: The URL of the page you are reporting an issue on. 16 | placeholder: https://nodejs.org/api/ 17 | validations: 18 | required: true 19 | - type: input 20 | attributes: 21 | label: 'Browser Name:' 22 | description: What kind of browser are you using? 23 | placeholder: Chrome 24 | validations: 25 | required: true 26 | - type: input 27 | attributes: 28 | label: 'Browser Version:' 29 | description: What version of browser are you using? 30 | placeholder: '103.0.5060.134' 31 | validations: 32 | required: true 33 | - type: input 34 | attributes: 35 | label: 'Operating System:' 36 | description: What kind of operation system are you using 37 | (Write it in full, with version number)? 38 | placeholder: 'Windows 10, 21H2, 19044.1826' 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: 'How to reproduce the issue:' 44 | placeholder: | 45 | 1. What I did. 46 | 2. What I expected to happen. 47 | 3. What I actually got. 48 | 4. If possible, images or videos are welcome. 49 | validations: 50 | required: true 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Suggest a new feature or improvement for Node.js' docs. 2 | description: 'Do you have an idea or a suggestion and you want to share?' 3 | labels: [feature request] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | You have an idea how to improve the docs? That's awesome! 9 | Before submitting, please have a look at the existing issues if there's already 10 | something related to your suggestion. 11 | - type: textarea 12 | attributes: 13 | label: 'Enter your suggestions in details:' 14 | placeholder: | 15 | 1. What I expected to happen. 16 | 2. Your reason (if possible, images or videos are welcome). 17 | 3. What I plan to do (Optional but better). 18 | validations: 19 | required: true 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## Description 6 | 7 | 8 | 9 | ## Validation 10 | 11 | 12 | 13 | ## Related Issues 14 | 15 | 19 | 20 | ### Check List 21 | 22 | 27 | 28 | - [ ] I have read the [Contributing Guidelines](https://github.com/nodejs/api-docs-tooling/blob/main/CONTRIBUTING.md) and made commit messages that follow the guideline. 29 | - [ ] I have run `node --run test` and all tests passed. 30 | - [ ] I have check code formatting with `node --run format` & `node --run lint`. 31 | - [ ] I've covered new added functionality with unit tests if necessary. 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # We define the interval for the updates to be monthly 2 | # because we don't want to have too many updates 3 | # and the project is currently in a development stage 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: github-actions 8 | directory: '/' 9 | schedule: 10 | interval: monthly 11 | commit-message: 12 | prefix: meta 13 | cooldown: 14 | default-days: 3 15 | open-pull-requests-limit: 10 16 | 17 | - package-ecosystem: npm 18 | directory: '/' 19 | versioning-strategy: increase 20 | schedule: 21 | interval: monthly 22 | commit-message: 23 | prefix: meta 24 | cooldown: 25 | default-days: 3 26 | groups: 27 | orama: 28 | patterns: 29 | - '@orama/*' 30 | cli: 31 | patterns: 32 | - 'commander' 33 | - '@clack/prompts' 34 | lint: 35 | patterns: 36 | - 'prettier' 37 | - 'eslint' 38 | - 'eslint-*' 39 | - 'lint-staged' 40 | - '@eslint/*' 41 | unist: 42 | patterns: 43 | - 'unified' 44 | - 'unist-*' 45 | - 'vfile' 46 | remark: 47 | patterns: 48 | - 'remark-*' 49 | - 'shiki' 50 | rehype: 51 | patterns: 52 | - 'rehype-*' 53 | ast: 54 | patterns: 55 | - 'estree-*' 56 | - 'hast-*' 57 | - 'mdast-*' 58 | - 'hastscript' 59 | - 'acorn' 60 | recma: 61 | patterns: 62 | - 'recma-*' 63 | compiling: 64 | patterns: 65 | - '@minify-html/node' 66 | - '@rollup/*' 67 | - 'rolldown' 68 | - 'lightningcss' 69 | react: 70 | patterns: 71 | - 'preact' 72 | - 'preact-*' 73 | open-pull-requests-limit: 10 74 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | env: 13 | FORCE_COLOR: 1 14 | 15 | jobs: 16 | quality: 17 | name: Lint & Format 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 23 | with: 24 | egress-policy: audit 25 | 26 | - name: Checkout code 27 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 31 | with: 32 | node-version-file: '.nvmrc' 33 | cache: 'npm' 34 | 35 | - name: Install dependencies 36 | run: npm ci 37 | 38 | - name: Run linting 39 | run: node --run lint 40 | 41 | - name: Check formatting 42 | run: node --run format:check 43 | 44 | test: 45 | name: Test & Coverage 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - name: Harden Runner 50 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 51 | with: 52 | egress-policy: audit 53 | 54 | - name: Checkout code 55 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 56 | 57 | - name: Setup Node.js 58 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 59 | with: 60 | node-version-file: '.nvmrc' 61 | cache: 'npm' 62 | 63 | - name: Install dependencies 64 | run: npm ci 65 | 66 | - name: Run tests with coverage 67 | run: node --run test:ci 68 | 69 | - name: Upload coverage to Codecov 70 | if: always() 71 | uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 72 | with: 73 | files: ./coverage/lcov.info 74 | 75 | - name: Upload test results to Codecov 76 | if: always() 77 | uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 78 | with: 79 | files: ./junit.xml 80 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: ['main'] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ['main'] 20 | schedule: 21 | - cron: '0 0 * * 1' 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ['javascript', 'typescript'] 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Harden Runner 43 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 44 | with: 45 | egress-policy: audit 46 | 47 | - name: Checkout repository 48 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 49 | 50 | # Initializes the CodeQL tools for scanning. 51 | - name: Initialize CodeQL 52 | uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 53 | with: 54 | languages: ${{ matrix.language }} 55 | # If you wish to specify custom queries, you can do so here or in a config file. 56 | # By default, queries listed here will override any specified in a config file. 57 | # Prefix the list here with "+" to use these queries and those in the config file. 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 66 | 67 | # If the Autobuild fails above, remove it and uncomment the following three lines. 68 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 69 | 70 | # - run: | 71 | # echo "Run, Build Application using script" 72 | # ./location_of_script_within_repo/buildscript.sh 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 76 | with: 77 | category: '/language:${{matrix.language}}' 78 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/codespell-project/actions-codespell 2 | name: codespell 3 | on: [pull_request, push] 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | codespell: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Harden Runner 12 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 13 | with: 14 | egress-policy: audit 15 | 16 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 17 | - uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 18 | with: 19 | ignore_words_list: crate,raison 20 | exclude_file: .gitignore 21 | skip: package-lock.json, ./src/generators/mandoc/template.1 22 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: Review Dependencies 10 | 11 | on: 12 | pull_request_target: 13 | branches: 14 | - main 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | dependency-review: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Harden Runner 24 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 25 | with: 26 | egress-policy: audit 27 | 28 | - name: Git Checkout 29 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 30 | 31 | - name: Review Dependencies 32 | uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 33 | -------------------------------------------------------------------------------- /.github/workflows/generate.yml: -------------------------------------------------------------------------------- 1 | name: Generate Docs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | generate: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | include: 23 | - target: man-page 24 | input: './node/doc/api/cli.md' 25 | - target: addon-verify 26 | input: './node/doc/api/addons.md' 27 | - target: api-links 28 | input: './node/lib/*.js' 29 | - target: orama-db 30 | input: './node/doc/api/*.md' 31 | - target: json-simple 32 | input: './node/doc/api/*.md' 33 | - target: legacy-json 34 | input: './node/doc/api/*.md' 35 | - target: legacy-html 36 | input: './node/doc/api/*.md' 37 | - target: web 38 | input: './node/doc/api/*.md' 39 | - target: llms-txt 40 | input: './node/doc/api/*.md' 41 | fail-fast: false 42 | 43 | steps: 44 | - name: Harden Runner 45 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 46 | with: 47 | egress-policy: audit 48 | 49 | - name: Git Checkout 50 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 51 | with: 52 | persist-credentials: false 53 | 54 | - name: Git Checkout 55 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 56 | with: 57 | persist-credentials: false 58 | repository: nodejs/node 59 | sparse-checkout: | 60 | doc/api 61 | lib 62 | . 63 | path: node 64 | 65 | - name: Setup Node.js 66 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 67 | with: 68 | node-version-file: '.nvmrc' 69 | cache: 'npm' 70 | 71 | - name: Install dependencies 72 | run: npm ci 73 | 74 | - name: Create output directory 75 | run: mkdir -p out/${{ matrix.target }} 76 | 77 | - name: Generate ${{ matrix.target }} 78 | run: | 79 | node bin/cli.mjs generate \ 80 | -t ${{ matrix.target }} \ 81 | -i "${{ matrix.input }}" \ 82 | -o "out/${{ matrix.target }}" \ 83 | -c ./node/CHANGELOG.md \ 84 | --index ./node/doc/api/index.md \ 85 | --log-level debug 86 | 87 | - name: Upload ${{ matrix.target }} artifacts 88 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 89 | with: 90 | name: ${{ matrix.target }} 91 | path: out/${{ matrix.target }} 92 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third party and are governed by separate terms of service, privacy 3 | # policy and support documentation. 4 | 5 | name: OpenSSF Scorecard Review 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee that the Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: 16 | - main 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | contents: read 31 | actions: read 32 | 33 | steps: 34 | - name: Harden Runner 35 | uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 36 | with: 37 | egress-policy: audit 38 | 39 | - name: Git Checkout 40 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 41 | with: 42 | persist-credentials: false 43 | 44 | - name: Run Scorecard Analysis 45 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 46 | with: 47 | results_file: results.sarif 48 | results_format: sarif 49 | publish_results: true 50 | 51 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 52 | # format to the repository Actions tab. 53 | - name: Upload Artifacts 54 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 55 | with: 56 | name: SARIF file 57 | path: results.sarif 58 | retention-days: 5 59 | 60 | # Upload the results to GitHub's code scanning dashboard. 61 | - name: Upload Scan Results 62 | uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5 63 | with: 64 | sarif_file: results.sarif 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vendor Folders 2 | node_modules 3 | npm-debug.log 4 | 5 | # Default Output Directory 6 | out 7 | 8 | # Tests 9 | coverage 10 | junit.xml 11 | 12 | # Debugging 13 | .clinic 14 | isolate-* 15 | 16 | # Node's Source Folder 17 | node 18 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,mjs,jsx}": ["eslint --fix", "prettier --write"], 3 | "*.{json,yml}": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | npm-shrinkwrap.json 2 | 3 | # Tests files 4 | src/generators/api-links/__tests__/fixtures/ 5 | *.snapshot 6 | 7 | # Templates 8 | src/generators/web/template.html 9 | 10 | # Output 11 | out/ 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": true, 6 | "jsxSingleQuote": false, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | # Ignored the cloned `node` folder 2 | node 3 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nodejs/web-infra 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | - [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md) 4 | - [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/HEAD/Moderation-Policy.md) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Node.js Website WG contributors. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 | 5 | 6 | Node.js Logo 7 | 8 | 9 |

10 | 11 |

12 | @nodejs/doc-kit is a tool to generate API documentation of Node.js. See this issue for more information. 13 |

14 | 15 |

16 | 17 | MIT License 18 | 19 | 20 | Codecov coverage badge 21 | 22 | 23 | doc-kit scorecard badge 24 | 25 | 26 | CII Best Practices badge 27 | 28 |

29 | 30 | ## Usage 31 | 32 | Local invocation: 33 | 34 | ```sh 35 | $ npx doc-kit --help 36 | ``` 37 | 38 | ```sh 39 | $ node bin/cli.mjs --help 40 | ``` 41 | 42 | ``` 43 | Usage: @nodejs/doc-kit [options] [command] 44 | 45 | CLI tool to generate the Node.js API documentation 46 | 47 | Options: 48 | -h, --help display help for command 49 | 50 | Commands: 51 | generate [options] Generate API docs 52 | interactive Launch guided CLI wizard 53 | help [command] display help for command 54 | ``` 55 | 56 | ### `generate` 57 | 58 | ``` 59 | Usage: @nodejs/doc-kit generate [options] 60 | 61 | Generate API docs 62 | 63 | Options: 64 | -i, --input Input file patterns (glob) 65 | --ignore [patterns...] Ignore patterns (comma-separated) 66 | -o, --output Output directory 67 | -p, --threads (default: "12") 68 | -v, --version Target Node.js version (default: "v22.14.0") 69 | -c, --changelog Changelog URL or path (default: "https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md") 70 | --git-ref Git ref/commit URL (default: "https://github.com/nodejs/node/tree/HEAD") 71 | -t, --target [modes...] Target generator modes (choices: "json-simple", "legacy-html", "legacy-html-all", "man-page", "legacy-json", "legacy-json-all", "addon-verify", "api-links", "orama-db", "llms-txt") 72 | -h, --help display help for command 73 | ``` 74 | 75 | ### `interactive` 76 | 77 | ``` 78 | Usage: @nodejs/doc-kit interactive [options] 79 | 80 | Launch guided CLI wizard 81 | 82 | Options: 83 | -h, --help display help for command 84 | ``` 85 | 86 | ## Examples 87 | 88 | ### Legacy 89 | 90 | To generate a 1:1 match with the [legacy tooling](https://github.com/nodejs/node/tree/main/tools/doc), use the `legacy-html`, `legacy-json`, `legacy-html-all`, and `legacy-json-all` generators. 91 | 92 | ```sh 93 | npx doc-kit generate \ 94 | -t legacy-html \ 95 | -t legacy-json \ 96 | -i "path/to/node/doc/api/*.md" \ 97 | -o out \ 98 | --index path/to/node/doc/api/index.md 99 | ``` 100 | 101 | ### Redesigned 102 | 103 | To generate [our redesigned documentation pages](https://nodejs-api-docs-tooling.vercel.app), use the `web` and `orama-db` (for search) generators. 104 | 105 | ```sh 106 | npx doc-kit generate \ 107 | -t web \ 108 | -t orama-db \ 109 | -i "path/to/node/doc/api/*.md" \ 110 | -o out \ 111 | --index path/to/node/doc/api/index.md 112 | ``` 113 | 114 | > [!TIP] 115 | > In order to use the search functionality, you _must_ serve the output directory. 116 | > 117 | > ```sh 118 | > npx serve out 119 | > ``` 120 | -------------------------------------------------------------------------------- /bin/cli.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from 'node:process'; 4 | 5 | import { Command, Option } from 'commander'; 6 | 7 | import commands from './commands/index.mjs'; 8 | import { errorWrap } from './utils.mjs'; 9 | import { LogLevel } from '../src/logger/constants.mjs'; 10 | import logger from '../src/logger/index.mjs'; 11 | 12 | const logLevelOption = new Option('--log-level ', 'Log level') 13 | .choices(Object.keys(LogLevel)) 14 | .default('info'); 15 | 16 | const program = new Command() 17 | .name('@nodejs/doc-kit') 18 | .description('CLI tool to generate the Node.js API documentation') 19 | .addOption(logLevelOption) 20 | .hook('preAction', cmd => logger.setLogLevel(cmd.opts().logLevel)); 21 | 22 | // Registering commands 23 | commands.forEach(({ name, description, options, action }) => { 24 | const cmd = program.command(name).description(description); 25 | 26 | // Add options to the command 27 | Object.values(options).forEach(({ flags, desc, prompt }) => { 28 | const option = new Option(flags.join(', '), desc).default( 29 | prompt.initialValue 30 | ); 31 | 32 | if (prompt.required) { 33 | option.makeOptionMandatory(); 34 | } 35 | 36 | if (prompt.type === 'multiselect') { 37 | option.choices(prompt.options.map(({ value }) => value)); 38 | } 39 | 40 | cmd.addOption(option); 41 | }); 42 | 43 | // Set the action for the command 44 | cmd.action(errorWrap(action)); 45 | }); 46 | 47 | // Parse and execute command-line arguments 48 | program.parse(process.argv); 49 | -------------------------------------------------------------------------------- /bin/commands/__tests__/index.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { Option } from 'commander'; 5 | 6 | import commands from '../index.mjs'; 7 | 8 | describe('Commands', () => { 9 | it('should have unique command names', () => { 10 | const names = new Set(); 11 | 12 | commands.forEach(({ name }) => { 13 | assert.equal(names.has(name), false, `Duplicate command name: "${name}"`); 14 | names.add(name); 15 | }); 16 | }); 17 | 18 | it('should use correct option names', () => { 19 | commands.forEach(({ name: cmdName, options }) => { 20 | Object.entries(options).forEach(([optName, { flags }]) => { 21 | const expectedName = new Option(flags.at(-1)).attributeName(); 22 | assert.equal( 23 | optName, 24 | expectedName, 25 | `In "${cmdName}" command: option "${flags}" should be named "${expectedName}", not "${optName}"` 26 | ); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /bin/commands/index.mjs: -------------------------------------------------------------------------------- 1 | import generate from './generate.mjs'; 2 | import interactive from './interactive.mjs'; 3 | 4 | export default [generate, interactive]; 5 | -------------------------------------------------------------------------------- /bin/commands/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a command-line option for the CLI. 3 | */ 4 | export interface Option { 5 | flags: string[]; 6 | desc: string; 7 | prompt?: { 8 | type: 'text' | 'confirm' | 'select' | 'multiselect'; 9 | message: string; 10 | variadic?: boolean; 11 | required?: boolean; 12 | initialValue?: boolean; 13 | options?: { label: string; value: string }[]; 14 | }; 15 | } 16 | 17 | /** 18 | * Represents a command-line subcommand 19 | */ 20 | export interface Command { 21 | options: { [key: string]: Option }; 22 | name: string; 23 | description: string; 24 | action: Function; 25 | } 26 | -------------------------------------------------------------------------------- /bin/utils.mjs: -------------------------------------------------------------------------------- 1 | import logger from '../src/logger/index.mjs'; 2 | 3 | /** 4 | * Wraps a function to catch both synchronous and asynchronous errors. 5 | * 6 | * @param {Function} fn - The function to wrap. Can be synchronous or return a Promise. 7 | * @returns {Function} A new function that handles errors and logs them. 8 | */ 9 | export const errorWrap = 10 | fn => 11 | async (...args) => { 12 | try { 13 | return await fn(...args); 14 | } catch (err) { 15 | logger.error(err); 16 | process.exit(1); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | # https://docs.codecov.com/docs/commit-status 3 | status: 4 | patch: false 5 | project: 6 | default: 7 | # TODO(@avivkeller): Once our coverage > 80%, 8 | # increase this to 80%, and increase on increments 9 | target: 70% 10 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginJs from '@eslint/js'; 2 | import { defineConfig } from 'eslint/config'; 3 | import importX from 'eslint-plugin-import-x'; 4 | import jsdoc from 'eslint-plugin-jsdoc'; 5 | import react from 'eslint-plugin-react-x'; 6 | import globals from 'globals'; 7 | 8 | export default defineConfig([ 9 | pluginJs.configs.recommended, 10 | importX.flatConfigs.recommended, 11 | react.configs.recommended, 12 | { 13 | ignores: ['out/', 'src/generators/api-links/__tests__/fixtures/'], 14 | }, 15 | { 16 | files: ['**/*.{mjs,jsx}'], 17 | plugins: { jsdoc }, 18 | languageOptions: { 19 | ecmaVersion: 'latest', 20 | parserOptions: { 21 | ecmaFeatures: { 22 | jsx: true, 23 | }, 24 | }, 25 | globals: { ...globals.nodeBuiltin }, 26 | }, 27 | rules: { 28 | 'object-shorthand': 'error', 29 | 'import-x/namespace': 'off', 30 | 'import-x/no-named-as-default': 'off', 31 | 'import-x/no-named-as-default-member': 'off', 32 | 'import-x/no-unresolved': 'off', 33 | 'import-x/order': [ 34 | 'error', 35 | { 36 | groups: [ 37 | 'builtin', 38 | 'external', 39 | 'internal', 40 | ['sibling', 'parent'], 41 | 'index', 42 | 'unknown', 43 | ], 44 | 'newlines-between': 'always', 45 | alphabetize: { 46 | order: 'asc', 47 | caseInsensitive: true, 48 | }, 49 | }, 50 | ], 51 | // We use [] as default props. 52 | 'react-x/no-unstable-default-props': 'off', 53 | curly: ['error', 'all'], 54 | }, 55 | }, 56 | { 57 | files: ['src/**/*.mjs', 'bin/**/*.mjs'], 58 | plugins: { 59 | jsdoc, 60 | }, 61 | languageOptions: { 62 | ecmaVersion: 'latest', 63 | globals: { ...globals.nodeBuiltin }, 64 | }, 65 | rules: { 66 | 'jsdoc/check-alignment': 'error', 67 | 'jsdoc/check-indentation': 'error', 68 | 'jsdoc/require-jsdoc': [ 69 | 'error', 70 | { 71 | require: { 72 | FunctionDeclaration: true, 73 | MethodDefinition: true, 74 | ClassDeclaration: true, 75 | ArrowFunctionExpression: true, 76 | FunctionExpression: true, 77 | }, 78 | }, 79 | ], 80 | 'jsdoc/require-param': 'error', 81 | }, 82 | }, 83 | // Override rules for test files to disable JSDoc rules 84 | { 85 | files: ['**/__tests__/**'], 86 | rules: { 87 | 'jsdoc/check-alignment': 'off', 88 | 'jsdoc/check-indentation': 'off', 89 | 'jsdoc/require-jsdoc': 'off', 90 | 'jsdoc/require-param': 'off', 91 | }, 92 | }, 93 | { 94 | files: [ 95 | 'src/generators/legacy-html/assets/*.js', 96 | 'src/generators/web/ui/**/*', 97 | ], 98 | languageOptions: { 99 | globals: { 100 | ...globals.browser, 101 | // SERVER and CLIENT denote server-only and client-only 102 | // codepaths in our web generator 103 | CLIENT: 'readonly', 104 | SERVER: 'readonly', 105 | }, 106 | ecmaVersion: 'latest', 107 | }, 108 | }, 109 | ]); 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodejs/doc-kit", 3 | "repository": { 4 | "type": "git", 5 | "url": "git+https://github.com/nodejs/api-docs-tooling.git" 6 | }, 7 | "scripts": { 8 | "lint": "eslint . --no-warn-ignored", 9 | "lint:fix": "eslint --fix . --no-warn-ignored", 10 | "format": "prettier .", 11 | "format:write": "prettier --write .", 12 | "format:check": "prettier --check .", 13 | "test": "node --test --experimental-test-module-mocks", 14 | "test:coverage": "c8 npm test", 15 | "test:ci": "c8 --reporter=lcov node --test --experimental-test-module-mocks --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml --test-reporter=spec --test-reporter-destination=stdout", 16 | "test:update-snapshots": "node --test --experimental-test-module-mocks --test-update-snapshots", 17 | "test:watch": "node --test --experimental-test-module-mocks --watch", 18 | "prepare": "husky || exit 0", 19 | "run": "node bin/cli.mjs", 20 | "watch": "node --watch bin/cli.mjs" 21 | }, 22 | "main": "./src/index.mjs", 23 | "bin": { 24 | "doc-kit": "./bin/cli.mjs" 25 | }, 26 | "devDependencies": { 27 | "@eslint/js": "^9.39.1", 28 | "@reporters/github": "^1.11.0", 29 | "@types/mdast": "^4.0.4", 30 | "@types/node": "^24.10.1", 31 | "c8": "^10.1.3", 32 | "eslint": "^9.39.1", 33 | "eslint-import-resolver-node": "^0.3.9", 34 | "eslint-plugin-import-x": "^4.16.1", 35 | "eslint-plugin-jsdoc": "^61.4.1", 36 | "husky": "^9.1.7", 37 | "lint-staged": "^16.2.7", 38 | "prettier": "3.7.4" 39 | }, 40 | "dependencies": { 41 | "@actions/core": "^1.11.1", 42 | "@clack/prompts": "^0.11.0", 43 | "@heroicons/react": "^2.2.0", 44 | "@minify-html/node": "^0.16.4", 45 | "@node-core/rehype-shiki": "1.3.0", 46 | "@node-core/ui-components": "1.4.1", 47 | "@orama/orama": "^3.1.16", 48 | "@orama/ui": "^1.5.3", 49 | "@rollup/plugin-virtual": "^3.0.2", 50 | "acorn": "^8.15.0", 51 | "commander": "^14.0.2", 52 | "dedent": "^1.7.0", 53 | "eslint-plugin-react-x": "^2.3.12", 54 | "estree-util-to-js": "^2.0.0", 55 | "estree-util-visit": "^2.0.0", 56 | "github-slugger": "^2.0.0", 57 | "glob": "^13.0.0", 58 | "globals": "^16.5.0", 59 | "hast-util-to-string": "^3.0.1", 60 | "hastscript": "^9.0.1", 61 | "lightningcss": "^1.30.2", 62 | "mdast-util-slice-markdown": "^2.0.1", 63 | "piscina": "^5.1.4", 64 | "preact": "^11.0.0-beta.0", 65 | "preact-render-to-string": "^6.6.3", 66 | "reading-time": "^1.5.0", 67 | "recma-jsx": "^1.0.1", 68 | "rehype-raw": "^7.0.0", 69 | "rehype-recma": "^1.0.0", 70 | "rehype-stringify": "^10.0.1", 71 | "remark-gfm": "^4.0.1", 72 | "remark-parse": "^11.0.0", 73 | "remark-rehype": "^11.1.2", 74 | "remark-stringify": "^11.0.0", 75 | "rolldown": "^1.0.0-beta.53", 76 | "semver": "^7.7.3", 77 | "shiki": "^3.19.0", 78 | "to-vfile": "^8.0.0", 79 | "unified": "^11.0.5", 80 | "unist-builder": "^4.0.0", 81 | "unist-util-find-after": "^5.0.0", 82 | "unist-util-position": "^5.0.0", 83 | "unist-util-remove": "^4.0.0", 84 | "unist-util-select": "^5.1.0", 85 | "unist-util-visit": "^5.0.0", 86 | "vfile": "^6.0.3", 87 | "yaml": "^2.8.2" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /scripts/vercel-build.sh: -------------------------------------------------------------------------------- 1 | node bin/cli.mjs generate \ 2 | -t orama-db \ 3 | -t legacy-json \ 4 | -t llms-txt \ 5 | -t web \ 6 | -i "./node/doc/api/*.md" \ 7 | -o "./out" \ 8 | -c "./node/CHANGELOG.md" \ 9 | --index "./node/doc/api/index.md" \ 10 | --log-level debug 11 | 12 | rm -rf node/ 13 | -------------------------------------------------------------------------------- /scripts/vercel-prepare.sh: -------------------------------------------------------------------------------- 1 | # Clone the repository with no checkout and shallow history 2 | git clone --depth 1 --filter=blob:none --sparse https://github.com/nodejs/node.git 3 | 4 | # Move into the cloned directory 5 | cd node 6 | 7 | # Enable sparse checkout and specify the folder 8 | git sparse-checkout set lib doc/api . 9 | 10 | # Move back out 11 | cd .. 12 | 13 | # Install npm dependencies 14 | npm ci 15 | 16 | # Create the ./out directory 17 | mkdir -p out 18 | -------------------------------------------------------------------------------- /shiki.config.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import cLanguage from 'shiki/langs/c.mjs'; 4 | import coffeeScriptLanguage from 'shiki/langs/coffeescript.mjs'; 5 | import cPlusPlusLanguage from 'shiki/langs/cpp.mjs'; 6 | import diffLanguage from 'shiki/langs/diff.mjs'; 7 | import dockerLanguage from 'shiki/langs/docker.mjs'; 8 | import httpLanguage from 'shiki/langs/http.mjs'; 9 | import javaScriptLanguage from 'shiki/langs/javascript.mjs'; 10 | import jsonLanguage from 'shiki/langs/json.mjs'; 11 | import powershellLanguage from 'shiki/langs/powershell.mjs'; 12 | import shellScriptLanguage from 'shiki/langs/shellscript.mjs'; 13 | import shellSessionLanguage from 'shiki/langs/shellsession.mjs'; 14 | import typeScriptLanguage from 'shiki/langs/typescript.mjs'; 15 | import lightTheme from 'shiki/themes/catppuccin-latte.mjs'; 16 | import darkTheme from 'shiki/themes/catppuccin-mocha.mjs'; 17 | 18 | /** 19 | * Creates a Shiki configuration for the API Docs tooling 20 | * 21 | * @type {import('@shikijs/core').HighlighterCoreOptions} 22 | */ 23 | export default { 24 | // Only register the themes we need, to support light/dark theme 25 | themes: [lightTheme, darkTheme], 26 | // Only register the languages that the API docs use 27 | // and override the JavaScript language with the aliases 28 | langs: [ 29 | ...httpLanguage, 30 | ...jsonLanguage, 31 | ...typeScriptLanguage, 32 | ...shellScriptLanguage, 33 | ...powershellLanguage, 34 | ...shellSessionLanguage, 35 | ...dockerLanguage, 36 | ...diffLanguage, 37 | ...cLanguage, 38 | ...cPlusPlusLanguage, 39 | ...coffeeScriptLanguage, 40 | { ...javaScriptLanguage[0], aliases: ['mjs', 'cjs', 'js'] }, 41 | ], 42 | }; 43 | -------------------------------------------------------------------------------- /src/__tests__/metadata.test.mjs: -------------------------------------------------------------------------------- 1 | import { strictEqual, deepStrictEqual } from 'node:assert'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import GitHubSlugger from 'github-slugger'; 5 | import { u } from 'unist-builder'; 6 | import { VFile } from 'vfile'; 7 | 8 | import createMetadata from '../metadata.mjs'; 9 | 10 | describe('createMetadata', () => { 11 | it('should set the heading correctly', () => { 12 | const slugger = new GitHubSlugger(); 13 | const metadata = createMetadata(slugger); 14 | const heading = u('heading', { 15 | type: 'heading', 16 | data: { 17 | text: 'Test Heading', 18 | type: 'test', 19 | name: 'test', 20 | depth: 1, 21 | }, 22 | }); 23 | metadata.setHeading(heading); 24 | strictEqual(metadata.create(new VFile(), {}).heading.data, heading.data); 25 | }); 26 | 27 | it('should set the stability correctly', () => { 28 | const slugger = new GitHubSlugger(); 29 | const metadata = createMetadata(slugger); 30 | const stability = { 31 | type: 'root', 32 | data: { index: 2, description: '' }, 33 | children: [], 34 | }; 35 | metadata.addStability(stability); 36 | const actual = metadata.create(new VFile(), {}).stability; 37 | deepStrictEqual(actual, { 38 | children: [stability], 39 | type: 'root', 40 | }); 41 | }); 42 | 43 | it('should create a metadata entry correctly', () => { 44 | const slugger = new GitHubSlugger(); 45 | const metadata = createMetadata(slugger); 46 | const apiDoc = new VFile({ path: 'test.md' }); 47 | const section = { type: 'root', children: [] }; 48 | const heading = { 49 | type: 'heading', 50 | data: { 51 | text: 'Test Heading', 52 | type: 'test', 53 | name: 'test', 54 | depth: 1, 55 | }, 56 | }; 57 | const stability = { 58 | type: 'root', 59 | data: { index: 2, description: '' }, 60 | children: [], 61 | }; 62 | const properties = { source_link: 'test.com' }; 63 | metadata.setHeading(heading); 64 | metadata.addStability(stability); 65 | metadata.updateProperties(properties); 66 | const expected = { 67 | added_in: undefined, 68 | api: 'test', 69 | api_doc_source: 'doc/api/test.md', 70 | changes: [], 71 | content: section, 72 | deprecated_in: undefined, 73 | heading, 74 | n_api_version: undefined, 75 | introduced_in: undefined, 76 | llm_description: undefined, 77 | removed_in: undefined, 78 | slug: 'test-heading', 79 | source_link: 'test.com', 80 | stability: { type: 'root', children: [stability] }, 81 | tags: [], 82 | updates: [], 83 | yaml_position: {}, 84 | }; 85 | const actual = metadata.create(apiDoc, section); 86 | deepStrictEqual(actual, expected); 87 | }); 88 | 89 | it('should be serializable', () => { 90 | const { create } = createMetadata(new GitHubSlugger()); 91 | const actual = create(new VFile({ path: 'test.md' }), { 92 | type: 'root', 93 | children: [], 94 | }); 95 | deepStrictEqual(structuredClone(actual), actual); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/constants.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // The current running version of Node.js (Environment) 4 | export const NODE_VERSION = process.version; 5 | 6 | // This is the Node.js CHANGELOG to be consumed to generate a list of all major Node.js versions 7 | export const NODE_CHANGELOG_URL = 8 | 'https://raw.githubusercontent.com/nodejs/node/HEAD/CHANGELOG.md'; 9 | 10 | // The base URL for the Node.js website 11 | export const BASE_URL = 'https://nodejs.org/'; 12 | 13 | // This is the Node.js Base URL for viewing a file within GitHub UI 14 | export const DOC_NODE_BLOB_BASE_URL = 15 | 'https://github.com/nodejs/node/blob/HEAD/'; 16 | 17 | // This is the Node.js API docs base URL for editing a file on GitHub UI 18 | export const DOC_API_BLOB_EDIT_BASE_URL = 19 | 'https://github.com/nodejs/node/edit/main/doc/api/'; 20 | 21 | // Base URL for a specific Node.js version within the Node.js API docs 22 | export const DOC_API_BASE_URL_VERSION = 'https://nodejs.org/docs/latest-v'; 23 | -------------------------------------------------------------------------------- /src/generators/__tests__/index.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import semver from 'semver'; 5 | 6 | import { allGenerators } from '../index.mjs'; 7 | 8 | const validDependencies = Object.keys(allGenerators); 9 | const generatorEntries = Object.entries(allGenerators); 10 | 11 | describe('All Generators', () => { 12 | it('should have keys matching their name property', () => { 13 | generatorEntries.forEach(([key, generator]) => { 14 | assert.equal( 15 | key, 16 | generator.name, 17 | `Generator key "${key}" does not match its name property "${generator.name}"` 18 | ); 19 | }); 20 | }); 21 | 22 | it('should have valid semver versions', () => { 23 | generatorEntries.forEach(([key, generator]) => { 24 | const isValid = semver.valid(generator.version); 25 | assert.ok( 26 | isValid, 27 | `Generator "${key}" has invalid semver version: "${generator.version}"` 28 | ); 29 | }); 30 | }); 31 | 32 | it('should have valid dependsOn references', () => { 33 | generatorEntries.forEach(([key, generator]) => { 34 | if (generator.dependsOn) { 35 | assert.ok( 36 | validDependencies.includes(generator.dependsOn), 37 | `Generator "${key}" depends on "${generator.dependsOn}" which is not a valid generator` 38 | ); 39 | } 40 | }); 41 | }); 42 | 43 | it('should have ast generator as a top-level generator with no dependencies', () => { 44 | assert.ok(allGenerators.ast, 'ast generator should exist'); 45 | assert.equal( 46 | allGenerators.ast.dependsOn, 47 | undefined, 48 | 'ast generator should have no dependencies' 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/generators/addon-verify/constants.mjs: -------------------------------------------------------------------------------- 1 | export const EXTRACT_CODE_FILENAME_COMMENT = /^\/\/\s+(.*\.(?:cc|h|js))[\r\n]/; 2 | -------------------------------------------------------------------------------- /src/generators/addon-verify/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { mkdir, writeFile } from 'node:fs/promises'; 4 | import { join } from 'node:path'; 5 | 6 | import { visit } from 'unist-util-visit'; 7 | 8 | import { EXTRACT_CODE_FILENAME_COMMENT } from './constants.mjs'; 9 | import { generateFileList } from './utils/generateFileList.mjs'; 10 | import { 11 | generateSectionFolderName, 12 | isBuildableSection, 13 | normalizeSectionName, 14 | } from './utils/section.mjs'; 15 | 16 | /** 17 | * This generator generates a file list from code blocks extracted from 18 | * `doc/api/addons.md` to facilitate C++ compilation and JavaScript runtime 19 | * validations. 20 | * 21 | * @typedef {Array} Input 22 | * 23 | * @type {GeneratorMetadata} 24 | */ 25 | export default { 26 | name: 'addon-verify', 27 | 28 | version: '1.0.0', 29 | 30 | description: 31 | 'Generates a file list from code blocks extracted from `doc/api/addons.md` to facilitate C++ compilation and JavaScript runtime validations', 32 | 33 | dependsOn: 'metadata', 34 | 35 | /** 36 | * Generates a file list from code blocks. 37 | * 38 | * @param {Input} input 39 | * @param {Partial} options 40 | */ 41 | async generate(input, { output }) { 42 | const sectionsCodeBlocks = input.reduce((addons, node) => { 43 | const sectionName = node.heading.data.name; 44 | 45 | const content = node.content; 46 | 47 | visit(content, childNode => { 48 | if (childNode.type === 'code') { 49 | const filename = childNode.value.match(EXTRACT_CODE_FILENAME_COMMENT); 50 | 51 | if (filename === null) { 52 | return; 53 | } 54 | 55 | if (!addons[sectionName]) { 56 | addons[sectionName] = []; 57 | } 58 | 59 | addons[sectionName].push({ 60 | name: filename[1], 61 | content: childNode.value, 62 | }); 63 | } 64 | }); 65 | 66 | return addons; 67 | }, {}); 68 | 69 | const files = await Promise.all( 70 | Object.entries(sectionsCodeBlocks) 71 | .filter(([, codeBlocks]) => isBuildableSection(codeBlocks)) 72 | .flatMap(async ([sectionName, codeBlocks], index) => { 73 | const files = generateFileList(codeBlocks); 74 | 75 | if (output) { 76 | const normalizedSectionName = normalizeSectionName(sectionName); 77 | 78 | const folderName = generateSectionFolderName( 79 | normalizedSectionName, 80 | index 81 | ); 82 | 83 | await mkdir(join(output, folderName), { recursive: true }); 84 | 85 | for (const file of files) { 86 | await writeFile( 87 | join(output, folderName, file.name), 88 | file.content 89 | ); 90 | } 91 | } 92 | 93 | return files; 94 | }) 95 | ); 96 | 97 | return files; 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /src/generators/addon-verify/utils/__tests__/generateFileList.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { generateFileList } from '../generateFileList.mjs'; 5 | 6 | describe('generateFileList', () => { 7 | it('should transform test.js files with updated require paths', () => { 8 | const codeBlocks = [ 9 | { 10 | name: 'test.js', 11 | content: "const addon = require('./build/Release/addon');", 12 | }, 13 | ]; 14 | 15 | const result = generateFileList(codeBlocks); 16 | const testFile = result.find(file => file.name === 'test.js'); 17 | 18 | assert(testFile.content.includes("'use strict';")); 19 | assert(testFile.content.includes('`./build/${common.buildType}/addon`')); 20 | assert(!testFile.content.includes("'./build/Release/addon'")); 21 | }); 22 | 23 | it('should preserve other files unchanged', () => { 24 | const codeBlocks = [{ name: 'addon.cc', content: '#include ' }]; 25 | 26 | const result = generateFileList(codeBlocks); 27 | 28 | assert.equal( 29 | result.find(file => file.name === 'addon.cc').content, 30 | '#include ' 31 | ); 32 | }); 33 | 34 | it('should add binding.gyp file', () => { 35 | const codeBlocks = [{ name: 'addon.cc', content: 'code' }]; 36 | 37 | const result = generateFileList(codeBlocks); 38 | const bindingFile = result.find(file => file.name === 'binding.gyp'); 39 | 40 | assert(bindingFile); 41 | const config = JSON.parse(bindingFile.content); 42 | assert.equal(config.targets[0].target_name, 'addon'); 43 | assert(config.targets[0].sources.includes('addon.cc')); 44 | }); 45 | 46 | it('should handle empty input', () => { 47 | const result = generateFileList([]); 48 | 49 | assert.equal(result.length, 1); 50 | assert.equal(result[0].name, 'binding.gyp'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/generators/addon-verify/utils/__tests__/section.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { 5 | isBuildableSection, 6 | normalizeSectionName, 7 | generateSectionFolderName, 8 | } from '../section.mjs'; 9 | 10 | describe('isBuildableSection', () => { 11 | it('should return true when both .cc and .js files are present', () => { 12 | const codeBlocks = [ 13 | { name: 'addon.cc', content: 'C++ code' }, 14 | { name: 'test.js', content: 'JS code' }, 15 | ]; 16 | 17 | assert.equal(isBuildableSection(codeBlocks), true); 18 | }); 19 | 20 | it('should return false when only .cc file is present', () => { 21 | const codeBlocks = [{ name: 'addon.cc', content: 'C++ code' }]; 22 | 23 | assert.equal(isBuildableSection(codeBlocks), false); 24 | }); 25 | 26 | it('should return false when only .js file is present', () => { 27 | const codeBlocks = [{ name: 'test.js', content: 'JS code' }]; 28 | 29 | assert.equal(isBuildableSection(codeBlocks), false); 30 | }); 31 | 32 | it('should return false for empty array', () => { 33 | assert.equal(isBuildableSection([]), false); 34 | }); 35 | }); 36 | 37 | describe('normalizeSectionName', () => { 38 | it('should convert to lowercase and replace spaces with underscores', () => { 39 | assert.equal(normalizeSectionName('Hello World'), 'hello_world'); 40 | }); 41 | 42 | it('should remove non-word characters', () => { 43 | assert.equal(normalizeSectionName('Test-Section!@#'), 'testsection'); 44 | }); 45 | 46 | it('should handle empty string', () => { 47 | assert.equal(normalizeSectionName(''), ''); 48 | }); 49 | 50 | it('should handle mixed cases and special characters', () => { 51 | assert.equal( 52 | normalizeSectionName('My Test & Example #1'), 53 | 'my_test__example_1' 54 | ); 55 | }); 56 | }); 57 | 58 | describe('generateSectionFolderName', () => { 59 | it('should generate folder name with padded index', () => { 60 | assert.equal(generateSectionFolderName('hello_world', 0), '01_hello_world'); 61 | }); 62 | 63 | it('should pad single digit indices', () => { 64 | assert.equal(generateSectionFolderName('test', 5), '06_test'); 65 | }); 66 | 67 | it('should not pad double digit indices', () => { 68 | assert.equal(generateSectionFolderName('example', 15), '16_example'); 69 | }); 70 | 71 | it('should handle empty section name', () => { 72 | assert.equal(generateSectionFolderName('', 0), '01_'); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/generators/addon-verify/utils/generateFileList.mjs: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent'; 2 | 3 | /** 4 | * Updates JavaScript files with correct require paths for the build tree. 5 | * 6 | * @param {string} content Original code 7 | * @returns {string} 8 | */ 9 | const updateJsRequirePaths = content => { 10 | return dedent` 11 | 'use strict'; 12 | const common = require('../../common'); 13 | ${content.replace( 14 | "'./build/Release/addon'", 15 | '`./build/${common.buildType}/addon`' 16 | )}`; 17 | }; 18 | 19 | /** 20 | * Creates a binding.gyp configuration for C++ addon compilation. 21 | * 22 | * @param {string[]} sourceFiles List of source file names 23 | * @returns {string} 24 | */ 25 | const createBindingGyp = sourceFiles => { 26 | const config = { 27 | targets: [ 28 | { 29 | target_name: 'addon', 30 | sources: sourceFiles, 31 | includes: ['../common.gypi'], 32 | }, 33 | ], 34 | }; 35 | 36 | return JSON.stringify(config); 37 | }; 38 | 39 | /** 40 | * Generates required files list from section's code blocks for C++ addon 41 | * compilation. 42 | * 43 | * @param {{name: string, content: string}[]} codeBlocks Array of code blocks 44 | * @returns {{name: string, content: string}[]} 45 | */ 46 | export const generateFileList = codeBlocks => { 47 | const files = codeBlocks.map(({ name, content }) => { 48 | return { 49 | name, 50 | content: name === 'test.js' ? updateJsRequirePaths(content) : content, 51 | }; 52 | }); 53 | 54 | files.push({ 55 | name: 'binding.gyp', 56 | content: createBindingGyp(files.map(({ name }) => name)), 57 | }); 58 | 59 | return files; 60 | }; 61 | -------------------------------------------------------------------------------- /src/generators/addon-verify/utils/section.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if a section contains the required code blocks for building. 3 | * A buildable section must contain at least one C++ (.cc) file and one 4 | * JavaScript (.js) file. 5 | * 6 | * @param {Array<{name: string; content: string}>} codeBlocks Array of code blocks 7 | * @returns {boolean} 8 | */ 9 | export const isBuildableSection = codeBlocks => { 10 | return ( 11 | codeBlocks.some(codeBlock => codeBlock.name.endsWith('.cc')) && 12 | codeBlocks.some(codeBlock => codeBlock.name.endsWith('.js')) 13 | ); 14 | }; 15 | 16 | /** 17 | * Normalizes a section name. 18 | * 19 | * @param {string} sectionName Original section name 20 | * @returns {string} 21 | */ 22 | export const normalizeSectionName = sectionName => { 23 | return sectionName.toLowerCase().replace(/\s/g, '_').replace(/\W/g, ''); 24 | }; 25 | 26 | /** 27 | * Generates a standardized folder name for a section. 28 | * 29 | * @param {string} sectionName Normalized section name 30 | * @param {number} index Zero-based section index 31 | * @returns {string} 32 | */ 33 | export const generateSectionFolderName = (sectionName, index) => { 34 | const identifier = String(index + 1).padStart(2, '0'); 35 | 36 | return `${identifier}_${sectionName}`; 37 | }; 38 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures.test.mjs: -------------------------------------------------------------------------------- 1 | import { readdir } from 'node:fs/promises'; 2 | import { cpus } from 'node:os'; 3 | import { basename, extname, join } from 'node:path'; 4 | import { after, before, describe, it } from 'node:test'; 5 | 6 | import createWorkerPool from '../../../threading/index.mjs'; 7 | import createParallelWorker from '../../../threading/parallel.mjs'; 8 | import astJs from '../../ast-js/index.mjs'; 9 | import apiLinks from '../index.mjs'; 10 | 11 | const FIXTURES_DIRECTORY = join(import.meta.dirname, 'fixtures'); 12 | const fixtures = await readdir(FIXTURES_DIRECTORY); 13 | 14 | const sourceFiles = fixtures 15 | .filter(fixture => extname(fixture) === '.js') 16 | .map(fixture => join(FIXTURES_DIRECTORY, fixture)); 17 | 18 | describe('api links', () => { 19 | const threads = cpus().length; 20 | let pool; 21 | 22 | before(() => { 23 | pool = createWorkerPool(threads); 24 | }); 25 | 26 | after(async () => { 27 | await pool.destroy(); 28 | }); 29 | 30 | describe('should work correctly for all fixtures', () => { 31 | sourceFiles.forEach(sourceFile => { 32 | it(`${basename(sourceFile)}`, async t => { 33 | const worker = createParallelWorker('ast-js', pool, { 34 | threads, 35 | chunkSize: 10, 36 | }); 37 | 38 | // Collect results from the async generator 39 | const astJsResults = []; 40 | 41 | for await (const chunk of astJs.generate(undefined, { 42 | input: [sourceFile], 43 | worker, 44 | })) { 45 | astJsResults.push(...chunk); 46 | } 47 | 48 | const actualOutput = await apiLinks.generate(astJsResults, { 49 | gitRef: 'https://github.com/nodejs/node/tree/HEAD', 50 | }); 51 | 52 | for (const [k, v] of Object.entries(actualOutput)) { 53 | actualOutput[k] = v.replace(/.*(?=lib\/)/, ''); 54 | } 55 | 56 | t.assert.snapshot(actualOutput); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures.test.mjs.snapshot: -------------------------------------------------------------------------------- 1 | exports[`api links > should work correctly for all fixtures > buffer.js 1`] = ` 2 | { 3 | "buffer.Buffer": "lib/buffer.js#L5", 4 | "buf.instanceMethod": "lib/buffer.js#L8" 5 | } 6 | `; 7 | 8 | exports[`api links > should work correctly for all fixtures > class.js 1`] = ` 9 | { 10 | "Class": "lib/class.js#L5", 11 | "new Class": "lib/class.js#L6", 12 | "class.method": "lib/class.js#L7" 13 | } 14 | `; 15 | 16 | exports[`api links > should work correctly for all fixtures > exports.js 1`] = ` 17 | { 18 | "exports.fn1": "lib/exports.js#L8", 19 | "exports.fn2": "lib/exports.js#L10", 20 | "exports.Buffer": "lib/exports.js#L5", 21 | "exports.fn3": "lib/exports.js#L12" 22 | } 23 | `; 24 | 25 | exports[`api links > should work correctly for all fixtures > mod.js 1`] = ` 26 | { 27 | "mod.foo": "lib/mod.js#L5" 28 | } 29 | `; 30 | 31 | exports[`api links > should work correctly for all fixtures > prototype.js 1`] = ` 32 | { 33 | "prototype.Class": "lib/prototype.js#L5", 34 | "Class.classMethod": "lib/prototype.js#L8", 35 | "class.instanceMethod": "lib/prototype.js#L9" 36 | } 37 | `; 38 | 39 | exports[`api links > should work correctly for all fixtures > reverse.js 1`] = ` 40 | { 41 | "asserts": "lib/reverse.js#L8", 42 | "asserts.ok": "lib/reverse.js#L5", 43 | "asserts.strictEqual": "lib/reverse.js#L12" 44 | } 45 | `; 46 | 47 | exports[`api links > should work correctly for all fixtures > root.js 1`] = ` 48 | {} 49 | `; 50 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures/buffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Buffer instance methods are exported as 'buf'. 4 | 5 | function Buffer() { 6 | } 7 | 8 | Buffer.prototype.instanceMethod = function() {} 9 | 10 | module.exports = { 11 | Buffer 12 | }; 13 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures/class.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // An exported class using ES2015 class syntax. 4 | 5 | class Class { 6 | constructor() {}; 7 | method() {}; 8 | } 9 | 10 | module.exports = { 11 | Class 12 | }; 13 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures/exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Support `exports` as an alternative to `module.exports`. 4 | 5 | function Buffer() {}; 6 | 7 | exports.Buffer = Buffer; 8 | exports.fn1 = function fn1() {}; 9 | 10 | var fn2 = exports.fn2 = function() {}; 11 | 12 | function fn3() {}; 13 | exports.fn3 = fn3; 14 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures/mod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // A module may export one or more methods. 4 | 5 | function foo() { 6 | } 7 | 8 | 9 | module.exports = { 10 | foo 11 | }; 12 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures/prototype.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // An exported class using classic prototype syntax. 4 | 5 | function Class() { 6 | } 7 | 8 | Class.classMethod = function() {} 9 | Class.prototype.instanceMethod = function() {} 10 | 11 | module.exports = { 12 | Class 13 | }; 14 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures/reverse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Parallel assignment to the exported variable and module.exports. 4 | 5 | function ok() { 6 | } 7 | 8 | const asserts = module.exports = ok; 9 | 10 | asserts.ok = ok; 11 | 12 | asserts.strictEqual = function() { 13 | } 14 | -------------------------------------------------------------------------------- /src/generators/api-links/__tests__/fixtures/root.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Set root member 4 | let foo = true; 5 | foo = false; 6 | 7 | // Return outside of function 8 | if (!foo) { 9 | return; 10 | } 11 | -------------------------------------------------------------------------------- /src/generators/api-links/constants.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Checks if a string is a valid name for a constructor in JavaScript 4 | export const CONSTRUCTOR_EXPRESSION = /^[A-Z]/; 5 | -------------------------------------------------------------------------------- /src/generators/api-links/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { writeFile } from 'node:fs/promises'; 4 | import { basename, join } from 'node:path'; 5 | 6 | import { checkIndirectReferences } from './utils/checkIndirectReferences.mjs'; 7 | import { extractExports } from './utils/extractExports.mjs'; 8 | import { findDefinitions } from './utils/findDefinitions.mjs'; 9 | 10 | /** 11 | * This generator is responsible for mapping publicly accessible functions in 12 | * Node.js to their source locations in the Node.js repository. 13 | * 14 | * This is a top-level generator. It takes in the raw AST tree of the JavaScript 15 | * source files. It outputs a `apilinks.json` file into the specified output 16 | * directory. 17 | * 18 | * @typedef {Array} Input 19 | * 20 | * @type {GeneratorMetadata>} 21 | */ 22 | export default { 23 | name: 'api-links', 24 | 25 | version: '1.0.0', 26 | 27 | description: 28 | 'Creates a mapping of publicly accessible functions to their source locations in the Node.js repository.', 29 | 30 | // Unlike the rest of the generators, this utilizes Javascript sources being 31 | // passed into the input field rather than Markdown. 32 | dependsOn: 'ast-js', 33 | 34 | /** 35 | * Generates the `apilinks.json` file. 36 | * 37 | * @param {Input} input 38 | * @param {Partial} options 39 | */ 40 | async generate(input, { output, gitRef }) { 41 | /** 42 | * @type Record 43 | */ 44 | const definitions = {}; 45 | 46 | input.forEach(program => { 47 | /** 48 | * Mapping of definitions to their line number 49 | * 50 | * @type {Record} 51 | * @example { 'someclass.foo': 10 } 52 | */ 53 | const nameToLineNumberMap = {}; 54 | 55 | // `http.js` -> `http` 56 | const baseName = basename(program.path, '.js'); 57 | 58 | const exports = extractExports(program, baseName, nameToLineNumberMap); 59 | 60 | findDefinitions(program, baseName, nameToLineNumberMap, exports); 61 | 62 | checkIndirectReferences(program, exports, nameToLineNumberMap); 63 | 64 | const fullGitUrl = `${gitRef}/lib/${baseName}.js`; 65 | 66 | // Add the exports we found in this program to our output 67 | Object.keys(nameToLineNumberMap).forEach(key => { 68 | const lineNumber = nameToLineNumberMap[key]; 69 | 70 | definitions[key] = `${fullGitUrl}#L${lineNumber}`; 71 | }); 72 | }); 73 | 74 | if (output) { 75 | const out = join(output, 'apilinks.json'); 76 | 77 | await writeFile(out, JSON.stringify(definitions)); 78 | } 79 | 80 | return definitions; 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /src/generators/api-links/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface ProgramExports { 2 | ctors: Array; 3 | identifiers: Array; 4 | indirects: Record; 5 | } 6 | -------------------------------------------------------------------------------- /src/generators/api-links/utils/checkIndirectReferences.mjs: -------------------------------------------------------------------------------- 1 | import { visit } from 'estree-util-visit'; 2 | 3 | /** 4 | * @param {import('acorn').Program} program 5 | * @param {import('../types.d.ts').ProgramExports} exports 6 | * @param {Record} nameToLineNumberMap 7 | */ 8 | export function checkIndirectReferences(program, exports, nameToLineNumberMap) { 9 | if (Object.keys(exports.indirects).length === 0) { 10 | return; 11 | } 12 | 13 | visit(program, node => { 14 | if (!node.loc || node.type !== 'FunctionDeclaration') { 15 | return; 16 | } 17 | 18 | const name = node.id.name; 19 | 20 | if (name in exports.indirects) { 21 | nameToLineNumberMap[exports.indirects[name]] = node.loc.start.line; 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/generators/ast-js/index.mjs: -------------------------------------------------------------------------------- 1 | import { extname } from 'node:path'; 2 | 3 | import { globSync } from 'glob'; 4 | import { read } from 'to-vfile'; 5 | 6 | import { parseJsSource } from '../../parsers/javascript.mjs'; 7 | 8 | /** 9 | * This generator parses Javascript sources passed into the generator's input 10 | * field. This is separate from the Markdown parsing step since it's not as 11 | * commonly used and can take up a significant amount of memory. 12 | * 13 | * Putting this with the rest of the generators allows it to be lazily loaded 14 | * so we're only parsing the Javascript sources when we need to. 15 | * 16 | * @typedef {unknown} Input 17 | * @typedef {Array} Output 18 | * 19 | * @type {GeneratorMetadata} 20 | */ 21 | export default { 22 | name: 'ast-js', 23 | 24 | version: '1.0.0', 25 | 26 | description: 'Parses Javascript source files passed into the input.', 27 | 28 | /** 29 | * Process a chunk of JavaScript files in a worker thread. 30 | * Parses JS source files into AST representations. 31 | * 32 | * @param {string[]} inputSlice - Sliced input paths for this chunk 33 | * @param {number[]} itemIndices - Indices into the sliced array 34 | * @returns {Promise} Parsed JS AST objects for each file 35 | */ 36 | async processChunk(inputSlice, itemIndices) { 37 | const filePaths = itemIndices.map(idx => inputSlice[idx]); 38 | 39 | const results = []; 40 | 41 | for (const path of filePaths) { 42 | const vfile = await read(path, 'utf-8'); 43 | 44 | const parsedJS = await parseJsSource(vfile); 45 | 46 | results.push(parsedJS); 47 | } 48 | 49 | return results; 50 | }, 51 | 52 | /** 53 | * Generates a JavaScript AST from the input files. 54 | * 55 | * @param {Input} _ - Unused (files loaded from input paths) 56 | * @param {Partial} options 57 | * @returns {AsyncGenerator} 58 | */ 59 | async *generate(_, { input = [], ignore, worker }) { 60 | const files = globSync(input, { ignore }).filter(p => extname(p) === '.js'); 61 | 62 | // Parse the Javascript sources into ASTs in parallel using worker threads 63 | // source is both the items list and the fullInput since we use sliceInput 64 | for await (const chunkResult of worker.stream(files, files)) { 65 | yield chunkResult; 66 | } 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /src/generators/ast/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { extname } from 'node:path'; 4 | 5 | import { globSync } from 'glob'; 6 | import { read } from 'to-vfile'; 7 | 8 | import createQueries from '../../utils/queries/index.mjs'; 9 | import { getRemark } from '../../utils/remark.mjs'; 10 | 11 | const { updateStabilityPrefixToLink } = createQueries(); 12 | const remarkProcessor = getRemark(); 13 | 14 | /** 15 | * This generator parses Markdown API doc files into AST trees. 16 | * It parallelizes the parsing across worker threads for better performance. 17 | * 18 | * @typedef {undefined} Input 19 | * @typedef {Array>} Output 20 | * 21 | * @type {GeneratorMetadata} 22 | */ 23 | export default { 24 | name: 'ast', 25 | 26 | version: '1.0.0', 27 | 28 | description: 'Parses Markdown API doc files into AST trees', 29 | 30 | /** 31 | * Process a chunk of markdown files in a worker thread. 32 | * Loads and parses markdown files into AST representations. 33 | * 34 | * @param {string[]} inputSlice - Sliced input paths for this chunk 35 | * @param {number[]} itemIndices - Indices into the sliced array 36 | * @returns {Promise} 37 | */ 38 | async processChunk(inputSlice, itemIndices) { 39 | const filePaths = itemIndices.map(idx => inputSlice[idx]); 40 | 41 | const results = []; 42 | 43 | for (const path of filePaths) { 44 | const vfile = await read(path, 'utf-8'); 45 | 46 | updateStabilityPrefixToLink(vfile); 47 | 48 | results.push({ 49 | tree: remarkProcessor.parse(vfile), 50 | file: { stem: vfile.stem, basename: vfile.basename }, 51 | }); 52 | } 53 | 54 | return results; 55 | }, 56 | 57 | /** 58 | * Generates AST trees from markdown input files. 59 | * 60 | * @param {Input} _ - Unused (top-level generator) 61 | * @param {Partial} options 62 | * @returns {AsyncGenerator} 63 | */ 64 | async *generate(_, { input = [], ignore, worker }) { 65 | const files = globSync(input, { ignore }).filter(p => extname(p) === '.md'); 66 | 67 | // Parse markdown files in parallel using worker threads 68 | for await (const chunkResult of worker.stream(files, files)) { 69 | yield chunkResult; 70 | } 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/generators/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import addonVerify from './addon-verify/index.mjs'; 4 | import apiLinks from './api-links/index.mjs'; 5 | import ast from './ast/index.mjs'; 6 | import astJs from './ast-js/index.mjs'; 7 | import jsonSimple from './json-simple/index.mjs'; 8 | import jsxAst from './jsx-ast/index.mjs'; 9 | import legacyHtml from './legacy-html/index.mjs'; 10 | import legacyHtmlAll from './legacy-html-all/index.mjs'; 11 | import legacyJson from './legacy-json/index.mjs'; 12 | import legacyJsonAll from './legacy-json-all/index.mjs'; 13 | import llmsTxt from './llms-txt/index.mjs'; 14 | import manPage from './man-page/index.mjs'; 15 | import metadata from './metadata/index.mjs'; 16 | import oramaDb from './orama-db/index.mjs'; 17 | import web from './web/index.mjs'; 18 | 19 | export const publicGenerators = { 20 | 'json-simple': jsonSimple, 21 | 'legacy-html': legacyHtml, 22 | 'legacy-html-all': legacyHtmlAll, 23 | 'man-page': manPage, 24 | 'legacy-json': legacyJson, 25 | 'legacy-json-all': legacyJsonAll, 26 | 'addon-verify': addonVerify, 27 | 'api-links': apiLinks, 28 | 'orama-db': oramaDb, 29 | 'llms-txt': llmsTxt, 30 | web, 31 | }; 32 | 33 | // These ones are special since they don't produce standard output, 34 | // and hence, we don't expose them to the CLI. 35 | const internalGenerators = { 36 | ast, 37 | metadata, 38 | 'jsx-ast': jsxAst, 39 | 'ast-js': astJs, 40 | }; 41 | 42 | export const allGenerators = { 43 | ...publicGenerators, 44 | ...internalGenerators, 45 | }; 46 | -------------------------------------------------------------------------------- /src/generators/json-simple/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { writeFile } from 'node:fs/promises'; 4 | import { join } from 'node:path'; 5 | 6 | import { remove } from 'unist-util-remove'; 7 | 8 | import createQueries from '../../utils/queries/index.mjs'; 9 | 10 | /** 11 | * This generator generates a simplified JSON version of the API docs and returns it as a string 12 | * this is not meant to be used for the final API docs, but for debugging and testing purposes 13 | * 14 | * This generator is a top-level generator, and it takes the raw AST tree of the API doc files 15 | * and returns a stringified JSON version of the API docs. 16 | * 17 | * @typedef {Array} Input 18 | * 19 | * @type {GeneratorMetadata} 20 | */ 21 | export default { 22 | name: 'json-simple', 23 | 24 | version: '1.0.0', 25 | 26 | description: 27 | 'Generates the simple JSON version of the API docs, and returns it as a string', 28 | 29 | dependsOn: 'metadata', 30 | 31 | /** 32 | * Generates the simplified JSON version of the API docs 33 | * @param {Input} input 34 | * @param {Partial} options 35 | */ 36 | async generate(input, options) { 37 | // Iterates the input (ApiDocMetadataEntry) and performs a few changes 38 | const mappedInput = input.map(node => { 39 | // Deep clones the content nodes to avoid affecting upstream nodes 40 | const content = JSON.parse(JSON.stringify(node.content)); 41 | 42 | // Removes numerous nodes from the content that should not be on the "body" 43 | // of the JSON version of the API docs as they are already represented in the metadata 44 | remove(content, [ 45 | createQueries.UNIST.isStabilityNode, 46 | createQueries.UNIST.isHeading, 47 | ]); 48 | 49 | return { ...node, content }; 50 | }); 51 | 52 | // This simply grabs all the different files and stringifies them 53 | const stringifiedContent = JSON.stringify(mappedInput); 54 | 55 | if (options.output) { 56 | // Writes all the API docs stringified content into one file 57 | // Note: The full JSON generator in the future will create one JSON file per top-level API doc file 58 | await writeFile( 59 | join(options.output, 'api-docs.json'), 60 | stringifiedContent 61 | ); 62 | } 63 | 64 | return mappedInput; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /src/generators/jsx-ast/index.mjs: -------------------------------------------------------------------------------- 1 | import { buildSideBarProps } from './utils/buildBarProps.mjs'; 2 | import buildContent from './utils/buildContent.mjs'; 3 | import { getSortedHeadNodes } from './utils/getSortedHeadNodes.mjs'; 4 | import { groupNodesByModule } from '../../utils/generators.mjs'; 5 | import { getRemarkRecma } from '../../utils/remark.mjs'; 6 | 7 | const remarkRecma = getRemarkRecma(); 8 | 9 | /** 10 | * Generator for converting MDAST to JSX AST. 11 | * 12 | * @typedef {Array} Input 13 | * @typedef {Array} Output 14 | * 15 | * @type {GeneratorMetadata} 16 | */ 17 | export default { 18 | name: 'jsx-ast', 19 | 20 | version: '1.0.0', 21 | 22 | description: 'Generates JSX AST from the input MDAST', 23 | 24 | dependsOn: 'metadata', 25 | 26 | /** 27 | * Process a chunk of items in a worker thread. 28 | * Transforms metadata entries into JSX AST nodes. 29 | * 30 | * Each item is a SlicedModuleInput containing the head node 31 | * and all entries for that module - no need to recompute grouping. 32 | * 33 | * @param {Array<{head: ApiDocMetadataEntry, entries: Array}>} slicedInput - Pre-sliced module data 34 | * @param {number[]} itemIndices - Indices of items to process 35 | * @param {{ docPages: Array<[string, string]>, releases: Array, version: import('semver').SemVer }} options - Serializable options 36 | * @returns {Promise} JSX AST programs for each module 37 | */ 38 | async processChunk( 39 | slicedInput, 40 | itemIndices, 41 | { docPages, releases, version } 42 | ) { 43 | const results = []; 44 | 45 | for (const idx of itemIndices) { 46 | const { head, entries } = slicedInput[idx]; 47 | 48 | const sideBarProps = buildSideBarProps(head, releases, version, docPages); 49 | 50 | const content = await buildContent( 51 | entries, 52 | head, 53 | sideBarProps, 54 | remarkRecma 55 | ); 56 | 57 | results.push(content); 58 | } 59 | 60 | return results; 61 | }, 62 | 63 | /** 64 | * Generates a JSX AST 65 | * 66 | * @param {Input} input 67 | * @param {Partial} options 68 | */ 69 | async *generate(input, { index, releases, version, worker }) { 70 | const groupedModules = groupNodesByModule(input); 71 | const headNodes = getSortedHeadNodes(input); 72 | 73 | // Pre-compute docPages once in main thread 74 | const docPages = index 75 | ? index.map(({ section, api }) => [section, `${api}.html`]) 76 | : headNodes.map(node => [node.heading.data.name, `${node.api}.html`]); 77 | 78 | // Create sliced input: each item contains head + its module's entries 79 | // This avoids sending all 4700+ entries to every worker 80 | const entries = headNodes.map(head => ({ 81 | head, 82 | entries: groupedModules.get(head.api), 83 | })); 84 | 85 | const deps = { docPages, releases, version }; 86 | 87 | for await (const chunkResult of worker.stream(entries, entries, deps)) { 88 | yield chunkResult; 89 | } 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /src/generators/jsx-ast/utils/__tests__/buildPropertyTable.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import createPropertyTable, { 5 | classifyTypeNode, 6 | extractPropertyName, 7 | extractTypeAnnotations, 8 | } from '../buildPropertyTable.mjs'; 9 | 10 | describe('classifyTypeNode', () => { 11 | it('identifies union separator', () => { 12 | const node = { type: 'text', value: ' | ' }; 13 | assert.equal(classifyTypeNode(node), 2); 14 | }); 15 | 16 | it('identifies type reference', () => { 17 | const node = { 18 | type: 'link', 19 | children: [{ type: 'inlineCode', value: '' }], 20 | }; 21 | assert.equal(classifyTypeNode(node), 1); 22 | }); 23 | 24 | it('returns 0 for non-type nodes', () => { 25 | const node = { type: 'text', value: 'regular text' }; 26 | assert.equal(classifyTypeNode(node), 0); 27 | }); 28 | }); 29 | 30 | describe('extractPropertyName', () => { 31 | it('extracts name from inlineCode', () => { 32 | const children = [{ type: 'inlineCode', value: 'propName ' }]; 33 | const result = extractPropertyName(children); 34 | assert.equal(result.tagName, 'code'); 35 | assert.equal(result.children[0].value, 'propName'); 36 | }); 37 | }); 38 | 39 | describe('extractTypeAnnotations', () => { 40 | it('extracts type nodes until non-type node', () => { 41 | const children = [ 42 | { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, 43 | { type: 'text', value: ' | ' }, 44 | { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, 45 | { type: 'text', value: ' - description' }, 46 | ]; 47 | 48 | const result = extractTypeAnnotations(children); 49 | 50 | assert.equal(result.length, 3); 51 | assert.equal(children.length, 1); // Only non-type node left 52 | assert.equal(children[0].value, ' - description'); 53 | }); 54 | 55 | it('handles single type node', () => { 56 | const children = [ 57 | { type: 'link', children: [{ type: 'inlineCode', value: '' }] }, 58 | { type: 'text', value: ' description' }, 59 | ]; 60 | 61 | const result = extractTypeAnnotations(children); 62 | 63 | assert.equal(result.length, 1); 64 | assert.equal(children.length, 1); 65 | }); 66 | 67 | it('returns empty array for no type nodes', () => { 68 | const children = [{ type: 'text', value: 'just text' }]; 69 | const result = extractTypeAnnotations(children); 70 | assert.equal(result.length, 0); 71 | assert.equal(children.length, 1); 72 | }); 73 | }); 74 | 75 | describe('createPropertyTable', () => { 76 | it('creates a table with headings by default', () => { 77 | const node = { 78 | children: [ 79 | { 80 | children: [ 81 | { 82 | children: [{ type: 'inlineCode', value: 'propName' }], 83 | }, 84 | ], 85 | }, 86 | ], 87 | }; 88 | 89 | const result = createPropertyTable(node); 90 | 91 | assert.equal(result.tagName, 'table'); 92 | assert.ok(result.children.find(child => child.tagName === 'thead')); 93 | assert.ok(result.children.find(child => child.tagName === 'tbody')); 94 | }); 95 | 96 | it('creates a table without headings when specified', () => { 97 | const node = { 98 | children: [ 99 | { 100 | children: [ 101 | { 102 | children: [{ type: 'inlineCode', value: 'propName' }], 103 | }, 104 | ], 105 | }, 106 | ], 107 | }; 108 | 109 | const result = createPropertyTable(node, false); 110 | 111 | assert.equal(result.tagName, 'table'); 112 | assert.ok(!result.children.find(child => child.tagName === 'thead')); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/generators/jsx-ast/utils/ast.mjs: -------------------------------------------------------------------------------- 1 | import { u as createTree } from 'unist-builder'; 2 | 3 | import { AST_NODE_TYPES } from '../constants.mjs'; 4 | 5 | /** 6 | * @typedef {Object} JSXOptions 7 | * @property {boolean} [inline] - Whether the element is inline 8 | * @property {(string | Array)} [children] - Child content or nodes 9 | */ 10 | 11 | /** 12 | * Converts JavaScript values to ESTree AST nodes 13 | * 14 | * @param {*} value - The value to convert to an ESTree node 15 | */ 16 | export const toESTree = value => { 17 | // Preserve existing ESTree nodes, since they 18 | // don't need to be re-processed. 19 | if (value?.type === AST_NODE_TYPES.ESTREE.JSX_FRAGMENT) { 20 | return value; 21 | } 22 | 23 | // Handle undefined 24 | if (value === undefined) { 25 | return { type: AST_NODE_TYPES.ESTREE.IDENTIFIER, name: 'undefined' }; 26 | } 27 | 28 | // Handle primitive values (null, string, number, boolean) 29 | if ( 30 | value == null || 31 | typeof value === 'string' || 32 | typeof value === 'number' || 33 | typeof value === 'boolean' 34 | ) { 35 | return { type: AST_NODE_TYPES.ESTREE.LITERAL, value }; 36 | } 37 | 38 | // Handle arrays 39 | if (Array.isArray(value)) { 40 | return { 41 | type: AST_NODE_TYPES.ESTREE.ARRAY_EXPRESSION, 42 | elements: value.map(toESTree), 43 | }; 44 | } 45 | 46 | // Handle plain objects 47 | if (typeof value === 'object') { 48 | return { 49 | type: AST_NODE_TYPES.ESTREE.OBJECT_EXPRESSION, 50 | properties: Object.entries(value).map(([key, val]) => ({ 51 | type: AST_NODE_TYPES.ESTREE.PROPERTY, 52 | key: { type: AST_NODE_TYPES.ESTREE.IDENTIFIER, name: key }, 53 | value: toESTree(val), 54 | kind: 'init', 55 | method: false, 56 | shorthand: false, 57 | computed: false, 58 | })), 59 | }; 60 | } 61 | 62 | // We only need to convert simple types. This should never be reached. 63 | throw new Error('Unsupported value type for ESTree conversion'); 64 | }; 65 | 66 | /** 67 | * Creates an MDX JSX element. 68 | * 69 | * @param {string} name - The name of the JSX element 70 | * @param {JSXOptions & Record} [options={}] - Options and/or attributes for the JSX node. 71 | * @returns {import('unist').Node} The created MDX JSX element node 72 | */ 73 | export const createJSXElement = ( 74 | name, 75 | { inline = true, children = [], ...attributes } = {} 76 | ) => { 77 | // Convert string children to text node or use array directly 78 | const processedChildren = 79 | typeof children === 'string' 80 | ? [createTree('text', { value: children })] 81 | : children; 82 | 83 | const elementType = inline 84 | ? AST_NODE_TYPES.MDX.JSX_INLINE_ELEMENT 85 | : AST_NODE_TYPES.MDX.JSX_BLOCK_ELEMENT; 86 | 87 | const attrs = Object.entries(attributes).map(([key, value]) => 88 | createAttributeNode(key, value) 89 | ); 90 | 91 | return createTree(elementType, { 92 | name, 93 | attributes: attrs, 94 | children: processedChildren, 95 | }); 96 | }; 97 | 98 | /** 99 | * Creates an MDX JSX attribute node based on the value type. 100 | * 101 | * @param {string} name - The attribute name 102 | * @param {any} value - The attribute value 103 | * @returns {import('unist').Node} The MDX JSX attribute node 104 | */ 105 | export const createAttributeNode = (name, value) => { 106 | // Use expression for objects and arrays 107 | if (value !== null && typeof value === 'object') { 108 | return createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE, { 109 | name, 110 | value: createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE_EXPRESSION, { 111 | data: { 112 | estree: { 113 | type: AST_NODE_TYPES.ESTREE.PROGRAM, 114 | body: [ 115 | { 116 | type: AST_NODE_TYPES.ESTREE.EXPRESSION_STATEMENT, 117 | expression: toESTree(value), 118 | }, 119 | ], 120 | }, 121 | }, 122 | }), 123 | }); 124 | } 125 | 126 | // For primitives, use simple string conversion. 127 | // If nullish, pass nothing. 128 | return createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE, { 129 | name, 130 | value: value == null ? value : String(value), 131 | }); 132 | }; 133 | -------------------------------------------------------------------------------- /src/generators/jsx-ast/utils/getSortedHeadNodes.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { OVERRIDDEN_POSITIONS } from '../constants.mjs'; 4 | 5 | /** 6 | * Sorts entries by OVERRIDDEN_POSITIONS and then heading name. 7 | * @param {ApiDocMetadataEntry} a 8 | * @param {ApiDocMetadataEntry} b 9 | * @returns {number} 10 | */ 11 | const headingSortFn = (a, b) => { 12 | const ai = OVERRIDDEN_POSITIONS.indexOf(a.api); 13 | const bi = OVERRIDDEN_POSITIONS.indexOf(b.api); 14 | 15 | if (ai !== -1 && bi !== -1) { 16 | return ai - bi; 17 | } 18 | 19 | if (ai !== -1) { 20 | return -1; 21 | } 22 | 23 | if (bi !== -1) { 24 | return 1; 25 | } 26 | 27 | return a.heading.data.name.localeCompare(b.heading.data.name); 28 | }; 29 | 30 | /** 31 | * Filters and sorts entries by OVERRIDDEN_POSITIONS and then heading name. 32 | * @param {Array} entries 33 | * @returns {Array} 34 | */ 35 | export const getSortedHeadNodes = entries => 36 | entries.filter(node => node.heading.depth === 1).toSorted(headingSortFn); 37 | -------------------------------------------------------------------------------- /src/generators/jsx-ast/utils/transformer.mjs: -------------------------------------------------------------------------------- 1 | import { toString } from 'hast-util-to-string'; 2 | import { visit } from 'unist-util-visit'; 3 | 4 | import { TAG_TRANSFORMS } from '../constants.mjs'; 5 | 6 | /** 7 | * @template {import('unist').Node} T 8 | * @param {T} tree 9 | * @returns {T} 10 | */ 11 | const transformer = tree => { 12 | visit(tree, 'element', (node, index, parent) => { 13 | node.tagName = TAG_TRANSFORMS[node.tagName] || node.tagName; 14 | 15 | // Wrap in a
, and apply responsive 16 | // data attributes 17 | if (node.tagName === 'table') { 18 | if (parent) { 19 | parent.children[index] = { 20 | type: 'element', 21 | tagName: 'div', 22 | properties: { className: ['overflow-container'] }, 23 | children: [node], 24 | }; 25 | } 26 | 27 | // Not every table will have a header, so only do this on tables 28 | // with them. 29 | const thead = node.children.find(el => el.tagName === 'thead'); 30 | 31 | if (thead) { 32 | const headers = thead.children[0].children.map(toString); 33 | const tbody = node.children.find(el => el.tagName === 'tbody'); 34 | 35 | visit( 36 | tbody, 37 | node => node.tagName === 'td', 38 | (node, index) => (node.properties['data-label'] = headers[index]) 39 | ); 40 | } 41 | } 42 | }); 43 | 44 | // Are there footnotes? 45 | if (tree.children.at(-1).tagName === 'section') { 46 | const section = tree.children.pop(); 47 | // If so, move it into the proper location 48 | // Root -> Article -> Main content 49 | tree.children[2]?.children[1]?.children[0]?.children?.push( 50 | ...section.children 51 | ); 52 | } 53 | }; 54 | 55 | /** 56 | * Transforms elements in a syntax tree by replacing tag names according to the mapping. 57 | * 58 | * Also moves any generated root section into its proper location in the AST. 59 | */ 60 | export default () => transformer; 61 | -------------------------------------------------------------------------------- /src/generators/legacy-html-all/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { readFile, writeFile } from 'node:fs/promises'; 4 | import { join, resolve } from 'node:path'; 5 | 6 | import HTMLMinifier from '@minify-html/node'; 7 | 8 | import { getRemarkRehype } from '../../utils/remark.mjs'; 9 | import { replaceTemplateValues } from '../legacy-html/utils/replaceTemplateValues.mjs'; 10 | import tableOfContents from '../legacy-html/utils/tableOfContents.mjs'; 11 | 12 | /** 13 | * This generator generates the legacy HTML pages of the legacy API docs 14 | * for retro-compatibility and while we are implementing the new 'react' and 'html' generators. 15 | * 16 | * This generator is a top-level generator, and it takes the raw AST tree of the API doc files 17 | * and generates the HTML files to the specified output directory from the configuration settings 18 | * 19 | * @typedef {Array} Input 20 | * 21 | * @type {GeneratorMetadata} 22 | */ 23 | export default { 24 | name: 'legacy-html-all', 25 | 26 | version: '1.0.0', 27 | 28 | description: 29 | 'Generates the `all.html` file from the `legacy-html` generator, which includes all the modules in one single file', 30 | 31 | dependsOn: 'legacy-html', 32 | 33 | /** 34 | * Generates the `all.html` file from the `legacy-html` generator 35 | * @param {Input} input 36 | * @param {Partial} options 37 | * @returns {Promise} 38 | */ 39 | async generate(input, { version, releases, output }) { 40 | // Gets a Remark Processor that parses Markdown to minified HTML 41 | const remarkWithRehype = getRemarkRehype(); 42 | 43 | // Current directory path relative to the `index.mjs` file 44 | // from the `legacy-html` generator, as all the assets are there 45 | const baseDir = resolve(import.meta.dirname, '..', 'legacy-html'); 46 | 47 | // Reads the API template.html file to be used as a base for the HTML files 48 | const apiTemplate = await readFile(join(baseDir, 'template.html'), 'utf-8'); 49 | 50 | // Filter out index entries and extract needed properties 51 | const entries = input.filter(entry => entry.api !== 'index'); 52 | 53 | // Aggregates all individual Table of Contents into one giant string 54 | const aggregatedToC = entries.map(entry => entry.toc).join('\n'); 55 | 56 | // Aggregates all individual content into one giant string 57 | const aggregatedContent = entries.map(entry => entry.content).join('\n'); 58 | 59 | // Creates a "mimic" of an `ApiDocMetadataEntry` which fulfils the requirements 60 | // for generating the `tableOfContents` with the `tableOfContents.parseNavigationNode` parser 61 | const sideNavigationFromValues = entries.map(entry => ({ 62 | api: entry.api, 63 | heading: { data: { depth: 1, name: entry.section } }, 64 | })); 65 | 66 | // Generates the global Table of Contents (Sidebar Navigation) 67 | const parsedSideNav = remarkWithRehype.processSync( 68 | tableOfContents(sideNavigationFromValues, { 69 | maxDepth: 1, 70 | parser: tableOfContents.parseNavigationNode, 71 | }) 72 | ); 73 | 74 | const templateValues = { 75 | api: 'all', 76 | added: '', 77 | section: 'All', 78 | version: `v${version.version}`, 79 | toc: aggregatedToC, 80 | nav: String(parsedSideNav), 81 | content: aggregatedContent, 82 | }; 83 | 84 | const result = replaceTemplateValues( 85 | apiTemplate, 86 | templateValues, 87 | releases, 88 | { skipGitHub: true, skipGtocPicker: true } 89 | ); 90 | 91 | // We minify the html result to reduce the file size and keep it "clean" 92 | const minified = HTMLMinifier.minify(Buffer.from(result), {}); 93 | 94 | if (output) { 95 | await writeFile(join(output, 'all.html'), minified); 96 | } 97 | 98 | return minified; 99 | }, 100 | }; 101 | -------------------------------------------------------------------------------- /src/generators/legacy-html/assets/js-flavor-cjs.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/generators/legacy-html/assets/js-flavor-esm.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/generators/legacy-html/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface TemplateValues { 2 | api: string; 3 | added: string; 4 | section: string; 5 | version: string; 6 | toc: string; 7 | nav: string; 8 | content: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/generators/legacy-html/utils/buildDropdowns.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { DOC_API_BLOB_EDIT_BASE_URL } from '../../../constants.mjs'; 4 | import { 5 | getCompatibleVersions, 6 | getVersionFromSemVer, 7 | getVersionURL, 8 | } from '../../../utils/generators.mjs'; 9 | 10 | /** 11 | * Builds the Dropdown for the current Table of Contents 12 | * 13 | * Note.: We use plain strings here instead of HAST, since these are just 14 | * templates and not actual content that needs to be transformed. 15 | * 16 | * @param {string} tableOfContents The stringified ToC 17 | */ 18 | export const buildToC = tableOfContents => { 19 | if (tableOfContents.length) { 20 | return ( 21 | `
  • ` + 22 | `` + 23 | `Table of contents
    ` + 24 | `
    ${tableOfContents.replace('
  • ` 25 | ); 26 | } 27 | 28 | return ''; 29 | }; 30 | 31 | /** 32 | * Builds the Navigation Dropdown for the current file 33 | * 34 | * Note.: We use plain strings here instead of HAST, since these are just 35 | * templates and not actual content that needs to be transformed. 36 | * 37 | * @param {string} navigationContents The stringified Navigation 38 | */ 39 | export const buildNavigation = navigationContents => 40 | `
  • ` + 41 | `Index` + 42 | `

    ${navigationContents}
  • `; 44 | 45 | /** 46 | * Generates the dropdown for viewing the current API doc in different versions 47 | * 48 | * Note.: We use plain strings here instead of HAST, since these are just 49 | * templates and not actual content that needs to be transformed. 50 | * 51 | * @param {string} api The current API node name 52 | * @param {string} added The version the API was added 53 | * @param {Array} versions All available Node.js releases 54 | */ 55 | export const buildVersions = (api, added, versions) => { 56 | const compatibleVersions = getCompatibleVersions(added, versions); 57 | 58 | // Parses the SemVer version into something we use for URLs and to display the Node.js version 59 | // Then we create a `
  • ` entry for said version, ensuring we link to the correct API doc 60 | const versionsAsList = compatibleVersions.map(({ version, isLts }) => { 61 | const parsedVersion = getVersionFromSemVer(version); 62 | 63 | const ltsLabel = isLts ? 'LTS' : ''; 64 | 65 | return `
  • ${parsedVersion} ${ltsLabel}
  • `; 66 | }); 67 | 68 | return ( 69 | `
  • ` + 70 | `Other versions` + 71 | `
      ${versionsAsList.join('')}
  • ` 72 | ); 73 | }; 74 | 75 | /** 76 | * Builds the "Edit on GitHub" link for the current API doc 77 | * 78 | * Note.: We use plain strings here instead of HAST, since these are just 79 | * templates and not actual content that needs to be transformed. 80 | * 81 | * @param {string} api The current API node name 82 | */ 83 | export const buildGitHub = api => 84 | `
  • ` + 85 | `` + 86 | `Edit on GitHub
  • `; 87 | -------------------------------------------------------------------------------- /src/generators/legacy-html/utils/buildExtraContent.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { h as createElement } from 'hastscript'; 4 | import { u as createTree } from 'unist-builder'; 5 | 6 | /** 7 | * Generates the Stability Overview table based on the API metadata nodes. 8 | * 9 | * @param {Array} headMetadata The API metadata nodes to be used for the Stability Overview 10 | */ 11 | const buildStabilityOverview = headMetadata => { 12 | const headNodesWithStability = headMetadata.filter(entry => 13 | Boolean(entry.stability.children.length) 14 | ); 15 | 16 | const mappedHeadNodesIntoTable = headNodesWithStability.map( 17 | ({ heading, api, stability }) => { 18 | // Retrieves the first Stability Index, as we only want to use the first one 19 | // to generate the Stability Overview 20 | const [{ data }] = stability.children; 21 | 22 | return createElement( 23 | 'tr', 24 | createElement( 25 | 'td.module_stability', 26 | createElement('a', { href: `${api}.html` }, heading.data.name) 27 | ), 28 | createElement( 29 | `td.api_stability.api_stability_${parseInt(data.index)}`, 30 | // Grabs the first sentence of the description 31 | // to be used as a summary of the Stability Index 32 | `(${data.index}) ${data.description.split('. ')[0]}` 33 | ) 34 | ); 35 | } 36 | ); 37 | 38 | return createElement( 39 | 'table', 40 | createElement( 41 | 'thead', 42 | createElement( 43 | 'tr', 44 | createElement('th', 'API'), 45 | createElement('th', 'Stability') 46 | ) 47 | ), 48 | createElement('tbody', mappedHeadNodesIntoTable) 49 | ); 50 | }; 51 | 52 | /** 53 | * Generates extra "special" HTML content based on extra metadata that a node may have. 54 | * 55 | * @param {Array} headNodes The API metadata nodes to be used for the Stability Overview 56 | * @param {ApiDocMetadataEntry} node The current API metadata node to be transformed into HTML content 57 | * @returns {import('unist').Parent} The HTML AST tree for the extra content 58 | */ 59 | export default (headNodes, node) => { 60 | return createTree('root', [ 61 | node.tags.map(tag => { 62 | switch (tag) { 63 | case 'STABILITY_OVERVIEW_SLOT_BEGIN': 64 | return buildStabilityOverview(headNodes); 65 | case 'STABILITY_OVERVIEW_SLOT_END': 66 | return createTree('root'); 67 | default: 68 | return createTree('root'); 69 | } 70 | }), 71 | ]); 72 | }; 73 | -------------------------------------------------------------------------------- /src/generators/legacy-html/utils/replaceTemplateValues.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | buildToC, 5 | buildNavigation, 6 | buildVersions, 7 | buildGitHub, 8 | } from './buildDropdowns.mjs'; 9 | import tableOfContents from './tableOfContents.mjs'; 10 | 11 | /** 12 | * Replaces the template values in the API template with the given values. 13 | * @param {string} apiTemplate - The HTML template string 14 | * @param {import('../types').TemplateValues} values - The values to replace the template values with 15 | * @param {Array} releases - The releases array for version dropdown 16 | * @param {{ skipGitHub?: boolean; skipGtocPicker?: boolean }} [options] - Optional settings 17 | * @returns {string} The replaced template values 18 | */ 19 | export const replaceTemplateValues = ( 20 | apiTemplate, 21 | { api, added, section, version, toc, nav, content }, 22 | releases, 23 | { skipGitHub = false, skipGtocPicker = false } = {} 24 | ) => { 25 | return apiTemplate 26 | .replace('__ID__', api) 27 | .replace(/__FILENAME__/g, api) 28 | .replace('__SECTION__', section) 29 | .replace(/__VERSION__/g, version) 30 | .replace(/__TOC__/g, tableOfContents.wrapToC(toc)) 31 | .replace(/__GTOC__/g, nav) 32 | .replace('__CONTENT__', content) 33 | .replace(/__TOC_PICKER__/g, buildToC(toc)) 34 | .replace(/__GTOC_PICKER__/g, skipGtocPicker ? '' : buildNavigation(nav)) 35 | .replace('__ALTDOCS__', buildVersions(api, added, releases)) 36 | .replace('__EDIT_ON_GITHUB__', skipGitHub ? '' : buildGitHub(api)); 37 | }; 38 | -------------------------------------------------------------------------------- /src/generators/legacy-html/utils/safeCopy.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { statSync, constants } from 'node:fs'; 4 | import { copyFile, readdir } from 'node:fs/promises'; 5 | import { join } from 'node:path'; 6 | 7 | /** 8 | * Copies files from source to target directory, skipping files that haven't changed. 9 | * Uses synchronous stat checks for simplicity and copyFile for atomic operations. 10 | * 11 | * @param {string} srcDir - Source directory path 12 | * @param {string} targetDir - Target directory path 13 | */ 14 | export async function safeCopy(srcDir, targetDir) { 15 | const files = await readdir(srcDir); 16 | 17 | const promises = files.map(file => { 18 | const sourcePath = join(srcDir, file); 19 | const targetPath = join(targetDir, file); 20 | 21 | const tStat = statSync(targetPath, { throwIfNoEntry: false }); 22 | 23 | if (tStat === undefined) { 24 | return copyFile(sourcePath, targetPath, constants.COPYFILE_FICLONE); 25 | } 26 | 27 | const sStat = statSync(sourcePath); 28 | 29 | if (sStat.size !== tStat.size || sStat.mtimeMs > tStat.mtimeMs) { 30 | return copyFile(sourcePath, targetPath, constants.COPYFILE_FICLONE); 31 | } 32 | }); 33 | 34 | await Promise.all(promises); 35 | } 36 | -------------------------------------------------------------------------------- /src/generators/legacy-html/utils/tableOfContents.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Generates the Table of Contents (ToC) based on the API metadata nodes. 5 | * it uses the `node.heading.depth` property to determine the depth of the ToC, 6 | * the `node.heading.text` property to get the label/text of the ToC entry, and 7 | * the `node.slug` property to generate the link to the section. 8 | * 9 | * This generates a Markdown string containing a list as the ToC for the API documentation. 10 | * 11 | * @param {Array} entries The API metadata nodes to be used for the ToC 12 | * @param {{ maxDepth: number; parser: (metadata: ApiDocMetadataEntry) => string }} options The optional ToC options 13 | */ 14 | const tableOfContents = (entries, options) => { 15 | // Filter out the entries that have a name property / or that have empty content 16 | const validEntries = entries.filter(({ heading }) => heading.data.name); 17 | 18 | // Generate the ToC based on the API headings (sections) 19 | return validEntries.reduce((acc, entry) => { 20 | // Check if the depth of the heading is less than or equal to the maximum depth 21 | if (entry.heading.data.depth <= options.maxDepth) { 22 | // Generate the indentation based on the depth of the heading 23 | const indent = ' '.repeat(entry.heading.data.depth - 1); 24 | 25 | // Append the ToC entry to the accumulator 26 | acc += `${indent}- ${options.parser(entry)}\n`; 27 | } 28 | 29 | return acc; 30 | }, ''); 31 | }; 32 | 33 | /** 34 | * Builds the Label with extra metadata to be used in the ToC 35 | * 36 | * @param {ApiDocMetadataEntry} metadata The current node that is being parsed 37 | */ 38 | tableOfContents.parseNavigationNode = ({ api, heading }) => 39 | `${heading.data.name}`; 40 | 41 | /** 42 | * Builds the Label with extra metadata to be used in the ToC 43 | * 44 | * @param {ApiDocMetadataEntry} metadata 45 | */ 46 | tableOfContents.parseToCNode = ({ stability, api, heading }) => { 47 | const fullSlug = `${api}.html#${heading.data.slug}`; 48 | 49 | // If the node has one stability index, we add the stability index class 50 | // into the ToC; Otherwise, we cannot determine which class to add 51 | // which is intentional, as some nodes have multiple stabilities 52 | if (stability.children.length === 1) { 53 | const [firstStability] = stability.children; 54 | 55 | return ( 56 | `` + 57 | `${heading.data.text}` 58 | ); 59 | } 60 | 61 | // Otherwise, just the plain text of the heading with a link 62 | return `${heading.data.text}`; 63 | }; 64 | 65 | /** 66 | * Wraps the Table of Contents (ToC) with a template 67 | * used for rendering within the page template 68 | * 69 | * @param {string} toc 70 | */ 71 | tableOfContents.wrapToC = toc => { 72 | if (toc && toc.length > 0) { 73 | return ( 74 | `` 76 | ); 77 | } 78 | 79 | return ''; 80 | }; 81 | 82 | export default tableOfContents; 83 | -------------------------------------------------------------------------------- /src/generators/legacy-json-all/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { writeFile } from 'node:fs/promises'; 4 | import { join } from 'node:path'; 5 | 6 | /** 7 | * This generator consolidates data from the `legacy-json` generator into a single 8 | * JSON file (`all.json`). 9 | * 10 | * @typedef {Array} Input 11 | * @typedef {import('./types.d.ts').Output} Output 12 | * 13 | * @type {GeneratorMetadata} 14 | */ 15 | export default { 16 | name: 'legacy-json-all', 17 | 18 | version: '1.0.0', 19 | 20 | description: 21 | 'Generates the `all.json` file from the `legacy-json` generator, which includes all the modules in one single file.', 22 | 23 | dependsOn: 'legacy-json', 24 | 25 | /** 26 | * Generates the legacy JSON `all.json` file. 27 | * 28 | * @param {Input} input 29 | * @param {Partial} options 30 | * @returns {Promise} 31 | */ 32 | async generate(input, { output, index }) { 33 | /** 34 | * The consolidated output object that will contain 35 | * combined data from all sections in the input. 36 | * 37 | * @type {import('./types.d.ts').Output} 38 | */ 39 | const generatedValue = { 40 | miscs: [], 41 | modules: [], 42 | classes: [], 43 | globals: [], 44 | methods: [], 45 | }; 46 | 47 | /** 48 | * The properties to copy from each section in the input 49 | */ 50 | const propertiesToCopy = Object.keys(generatedValue); 51 | 52 | // Create a map of api name to index position for sorting 53 | const indexOrder = new Map( 54 | index?.map(({ api }, position) => [`doc/api/${api}.md`, position]) ?? [] 55 | ); 56 | 57 | // Sort input by index order (documents not in index go to the end) 58 | const sortedInput = input.toSorted((a, b) => { 59 | const aOrder = indexOrder.get(a.source) ?? Infinity; 60 | const bOrder = indexOrder.get(b.source) ?? Infinity; 61 | 62 | return aOrder - bOrder; 63 | }); 64 | 65 | // Aggregate all sections into the output 66 | for (const section of sortedInput) { 67 | // Skip index.json - it has no useful content, just navigation 68 | if (section.api === 'index') { 69 | continue; 70 | } 71 | 72 | for (const property of propertiesToCopy) { 73 | const items = section[property]; 74 | 75 | if (Array.isArray(items)) { 76 | const enrichedItems = section.source 77 | ? items.map(item => ({ ...item, source: section.source })) 78 | : items; 79 | 80 | generatedValue[property].push(...enrichedItems); 81 | } 82 | } 83 | } 84 | 85 | if (output) { 86 | await writeFile(join(output, 'all.json'), JSON.stringify(generatedValue)); 87 | } 88 | 89 | return generatedValue; 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /src/generators/legacy-json-all/types.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MiscSection, 3 | Section, 4 | SignatureSection, 5 | ModuleSection, 6 | } from '../legacy-json/types'; 7 | 8 | export interface Output { 9 | miscs: Array; 10 | modules: Array
    ; 11 | classes: Array; 12 | globals: Array; 13 | methods: Array; 14 | } 15 | -------------------------------------------------------------------------------- /src/generators/legacy-json/constants.mjs: -------------------------------------------------------------------------------- 1 | // Grabs a method's name 2 | export const NAME_EXPRESSION = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; 3 | 4 | // Denotes a method's type 5 | export const TYPE_EXPRESSION = /^\{([^}]+)\}\s*/; 6 | 7 | // Checks if there's a leading hyphen 8 | export const LEADING_HYPHEN = /^-\s*/; 9 | 10 | // Grabs the default value if present 11 | export const DEFAULT_EXPRESSION = /\s*\*\*Default:\*\*\s*([^]+)$/i; 12 | 13 | // Grabs the parameters from a method's signature 14 | // ex/ 'new buffer.Blob([sources[, options]])'.match(PARAM_EXPRESSION) === ['([sources[, options]])', '[sources[, options]]'] 15 | export const PARAM_EXPRESSION = /\((.+)\);?$/; 16 | 17 | // The plurals associated with each section type. 18 | export const SECTION_TYPE_PLURALS = { 19 | module: 'modules', 20 | misc: 'miscs', 21 | class: 'classes', 22 | method: 'methods', 23 | property: 'properties', 24 | global: 'globals', 25 | example: 'examples', 26 | ctor: 'signatures', 27 | classMethod: 'classMethods', 28 | event: 'events', 29 | var: 'vars', 30 | }; 31 | 32 | // The keys to not promote when promoting children. 33 | export const UNPROMOTED_KEYS = ['textRaw', 'name', 'type', 'desc', 'miscs']; 34 | -------------------------------------------------------------------------------- /src/generators/legacy-json/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { writeFile } from 'node:fs/promises'; 4 | import { join } from 'node:path'; 5 | 6 | import { createSectionBuilder } from './utils/buildSection.mjs'; 7 | import { groupNodesByModule } from '../../utils/generators.mjs'; 8 | 9 | const buildSection = createSectionBuilder(); 10 | 11 | /** 12 | * This generator is responsible for generating the legacy JSON files for the 13 | * legacy API docs for retro-compatibility. It is to be replaced while we work 14 | * on the new schema for this file. 15 | * 16 | * This is a top-level generator, intaking the raw AST tree of the api docs. 17 | * It generates JSON files to the specified output directory given by the 18 | * config. 19 | * 20 | * @typedef {Array} Input 21 | * @typedef {Array} Output 22 | * 23 | * @type {GeneratorMetadata} 24 | */ 25 | export default { 26 | name: 'legacy-json', 27 | 28 | version: '1.0.0', 29 | 30 | description: 'Generates the legacy version of the JSON API docs.', 31 | 32 | dependsOn: 'metadata', 33 | 34 | /** 35 | * Process a chunk of items in a worker thread. 36 | * Builds JSON sections - FS operations happen in generate(). 37 | * 38 | * Each item is pre-grouped {head, nodes} - no need to 39 | * recompute groupNodesByModule for every chunk. 40 | * 41 | * @param {Array<{ head: ApiDocMetadataEntry, nodes: Array }>} slicedInput - Pre-sliced module data 42 | * @param {number[]} itemIndices - Indices into the sliced array 43 | * @returns {Promise} JSON sections for each processed module 44 | */ 45 | async processChunk(slicedInput, itemIndices) { 46 | const results = []; 47 | 48 | for (const idx of itemIndices) { 49 | const { head, nodes } = slicedInput[idx]; 50 | 51 | results.push(buildSection(head, nodes)); 52 | } 53 | 54 | return results; 55 | }, 56 | 57 | /** 58 | * Generates a legacy JSON file. 59 | * 60 | * @param {Input} input 61 | * @param {Partial} options 62 | * @returns {AsyncGenerator} 63 | */ 64 | async *generate(input, { output, worker }) { 65 | const groupedModules = groupNodesByModule(input); 66 | 67 | const headNodes = input.filter(node => node.heading.depth === 1); 68 | 69 | // Create sliced input: each item contains head + its module's entries 70 | // This avoids sending all 4900+ entries to every worker 71 | const entries = headNodes.map(head => ({ 72 | head, 73 | nodes: groupedModules.get(head.api), 74 | })); 75 | 76 | for await (const chunkResult of worker.stream(entries, entries)) { 77 | if (output) { 78 | for (const section of chunkResult) { 79 | const out = join(output, `${section.api}.json`); 80 | 81 | await writeFile(out, JSON.stringify(section)); 82 | } 83 | } 84 | 85 | yield chunkResult; 86 | } 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /src/generators/legacy-json/utils/__tests__/buildHierarchy.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { findParent, buildHierarchy } from '../buildHierarchy.mjs'; 5 | 6 | describe('findParent', () => { 7 | it('finds parent with lower depth', () => { 8 | const entries = [{ heading: { depth: 1 } }, { heading: { depth: 2 } }]; 9 | const parent = findParent(entries[1], entries, 0); 10 | assert.equal(parent, entries[0]); 11 | }); 12 | 13 | it('throws when no parent exists', () => { 14 | const entries = [{ heading: { depth: 2 } }]; 15 | assert.throws(() => findParent(entries[0], entries, -1)); 16 | }); 17 | }); 18 | 19 | describe('buildHierarchy', () => { 20 | it('returns empty array for empty input', () => { 21 | assert.deepEqual(buildHierarchy([]), []); 22 | }); 23 | 24 | it('keeps root entries at top level', () => { 25 | const entries = [{ heading: { depth: 1 } }, { heading: { depth: 1 } }]; 26 | const result = buildHierarchy(entries); 27 | assert.equal(result.length, 2); 28 | }); 29 | 30 | it('nests children under parents', () => { 31 | const entries = [{ heading: { depth: 1 } }, { heading: { depth: 2 } }]; 32 | const result = buildHierarchy(entries); 33 | 34 | assert.equal(result.length, 1); 35 | assert.equal(result[0].hierarchyChildren.length, 1); 36 | assert.equal(result[0].hierarchyChildren[0], entries[1]); 37 | }); 38 | 39 | it('handles multiple levels', () => { 40 | const entries = [ 41 | { heading: { depth: 1 } }, 42 | { heading: { depth: 2 } }, 43 | { heading: { depth: 3 } }, 44 | ]; 45 | const result = buildHierarchy(entries); 46 | 47 | assert.equal(result.length, 1); 48 | assert.equal(result[0].hierarchyChildren[0].hierarchyChildren.length, 1); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/generators/legacy-json/utils/__tests__/buildSection.test.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import assert from 'node:assert/strict'; 4 | import { describe, test } from 'node:test'; 5 | 6 | import { UNPROMOTED_KEYS } from '../../constants.mjs'; 7 | import { promoteMiscChildren } from '../buildSection.mjs'; 8 | 9 | describe('promoteMiscChildren', () => { 10 | /** 11 | * @template {object} T 12 | * 13 | * @param {T} base 14 | * @param {'section'|'parent'} [type='section'] 15 | * @returns {T} 16 | */ 17 | function buildReadOnlySection(base, type = 'section') { 18 | return new Proxy(base, { 19 | set(_, key) { 20 | throw new Error(`${type} property '${String(key)} modified`); 21 | }, 22 | }); 23 | } 24 | 25 | test('ignores non-misc section', () => { 26 | const section = buildReadOnlySection({ 27 | type: 'text', 28 | }); 29 | 30 | const parent = buildReadOnlySection( 31 | { 32 | type: 'text', 33 | }, 34 | 'parent' 35 | ); 36 | 37 | promoteMiscChildren(section, parent); 38 | }); 39 | 40 | test('ignores misc parent', () => { 41 | const section = buildReadOnlySection({ 42 | type: 'misc', 43 | }); 44 | 45 | const parent = buildReadOnlySection( 46 | { 47 | type: 'misc', 48 | }, 49 | 'parent' 50 | ); 51 | 52 | promoteMiscChildren(section, parent); 53 | }); 54 | 55 | test('ignores keys in UNPROMOTED_KEYS', () => { 56 | const sectionRaw = { 57 | type: 'misc', 58 | promotableKey: 'this should be promoted', 59 | }; 60 | 61 | UNPROMOTED_KEYS.forEach(key => { 62 | if (key === 'type') { 63 | return; 64 | } 65 | 66 | sectionRaw[key] = 'this should be ignored'; 67 | }); 68 | 69 | const section = buildReadOnlySection(sectionRaw); 70 | 71 | const parent = { 72 | type: 'module', 73 | }; 74 | 75 | promoteMiscChildren(section, parent); 76 | 77 | UNPROMOTED_KEYS.forEach(key => { 78 | if (key === 'type') { 79 | return; 80 | } 81 | 82 | if (parent[key]) { 83 | throw new Error(`'${key}' was promoted`); 84 | } 85 | }); 86 | 87 | assert.strictEqual(parent.promotableKey, section.promotableKey); 88 | }); 89 | 90 | describe('merges properties correctly', () => { 91 | test('pushes child property if parent is an array', () => { 92 | const section = buildReadOnlySection({ 93 | type: 'misc', 94 | someValue: 'bar', 95 | }); 96 | 97 | const parent = { 98 | type: 'module', 99 | someValue: ['foo'], 100 | }; 101 | 102 | promoteMiscChildren(section, parent); 103 | 104 | assert.deepStrictEqual(parent.someValue, ['foo', 'bar']); 105 | }); 106 | 107 | test('ignores child property if parent has a value that is not an array', () => { 108 | const section = buildReadOnlySection({ 109 | type: 'misc', 110 | someValue: 'bar', 111 | }); 112 | 113 | const parent = { 114 | type: 'module', 115 | someValue: 'foo', 116 | }; 117 | 118 | promoteMiscChildren(section, parent); 119 | 120 | assert.strictEqual(parent.someValue, 'foo'); 121 | }); 122 | 123 | test('promotes child property if parent does not have the property', () => { 124 | const section = buildReadOnlySection({ 125 | type: 'misc', 126 | someValue: 'bar', 127 | }); 128 | 129 | const parent = { 130 | type: 'module', 131 | }; 132 | 133 | promoteMiscChildren(section, parent); 134 | 135 | assert.deepStrictEqual(parent.someValue, 'bar'); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/generators/legacy-json/utils/buildHierarchy.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Recursively finds the most suitable parent entry for a given `entry` based on heading depth. 3 | * 4 | * @param {ApiDocMetadataEntry} entry 5 | * @param {ApiDocMetadataEntry[]} entries 6 | * @param {number} startIdx 7 | * @returns {import('../types.d.ts').HierarchizedEntry} 8 | */ 9 | export function findParent(entry, entries, startIdx) { 10 | // Base case: if we're at the beginning of the list, no valid parent exists. 11 | if (startIdx < 0) { 12 | throw new Error( 13 | `Cannot find a suitable parent for entry at index ${startIdx + 1}` 14 | ); 15 | } 16 | 17 | const candidateParent = entries[startIdx]; 18 | const candidateDepth = candidateParent.heading.depth; 19 | 20 | // If we find a suitable parent, return it. 21 | if (candidateDepth < entry.heading.depth) { 22 | candidateParent.hierarchyChildren ??= []; 23 | return candidateParent; 24 | } 25 | 26 | // Recurse upwards to find a suitable parent. 27 | return findParent(entry, entries, startIdx - 1); 28 | } 29 | 30 | /** 31 | * We need the files to be in a hierarchy based off of depth, but they're 32 | * given to us flattened. So, let's fix that. 33 | * 34 | * Assuming that {@link entries} is in the same order as the elements are in 35 | * the markdown, we can use the entry's depth property to reassemble the 36 | * hierarchy. 37 | * 38 | * If depth <= 1, it's a top-level element (aka a root). 39 | * 40 | * If it's depth is greater than the previous entry's depth, it's a child of 41 | * the previous entry. Otherwise (if it's less than or equal to the previous 42 | * entry's depth), we need to find the entry that it was the greater than. We 43 | * can do this by just looping through entries in reverse starting at the 44 | * current index - 1. 45 | * 46 | * @param {Array} entries 47 | * @returns {Array} 48 | */ 49 | export function buildHierarchy(entries) { 50 | const roots = []; 51 | 52 | // Main loop to construct the hierarchy. 53 | for (let i = 0; i < entries.length; i++) { 54 | const entry = entries[i]; 55 | const currentDepth = entry.heading.depth; 56 | 57 | // Top-level entries are added directly to roots. 58 | if (currentDepth <= 1) { 59 | roots.push(entry); 60 | continue; 61 | } 62 | 63 | // For non-root entries, find the appropriate parent. 64 | const previousEntry = entries[i - 1]; 65 | const previousDepth = previousEntry.heading.depth; 66 | 67 | if (currentDepth > previousDepth) { 68 | previousEntry.hierarchyChildren ??= []; 69 | previousEntry.hierarchyChildren.push(entry); 70 | } else { 71 | // Use recursive helper to find the nearest valid parent. 72 | const parent = findParent(entry, entries, i - 2); 73 | parent.hierarchyChildren.push(entry); 74 | } 75 | } 76 | 77 | return roots; 78 | } 79 | -------------------------------------------------------------------------------- /src/generators/llms-txt/index.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises'; 2 | import { join } from 'node:path'; 3 | 4 | import { buildApiDocLink } from './utils/buildApiDocLink.mjs'; 5 | 6 | /** 7 | * This generator generates a llms.txt file to provide information to LLMs at 8 | * inference time 9 | * 10 | * @typedef {Array} Input 11 | * 12 | * @type {GeneratorMetadata} 13 | */ 14 | export default { 15 | name: 'llms-txt', 16 | 17 | version: '1.0.0', 18 | 19 | description: 20 | 'Generates a llms.txt file to provide information to LLMs at inference time', 21 | 22 | dependsOn: 'metadata', 23 | 24 | /** 25 | * Generates a llms.txt file 26 | * 27 | * @param {Input} input 28 | * @param {Partial} options 29 | * @returns {Promise} 30 | */ 31 | async generate(input, { output }) { 32 | const template = await readFile( 33 | join(import.meta.dirname, 'template.txt'), 34 | 'utf-8' 35 | ); 36 | 37 | const apiDocsLinks = input 38 | .filter(entry => entry.heading.depth === 1) 39 | .map(entry => `- ${buildApiDocLink(entry)}`) 40 | .join('\n'); 41 | 42 | const filledTemplate = `${template}${apiDocsLinks}`; 43 | 44 | if (output) { 45 | await writeFile(join(output, 'llms.txt'), filledTemplate); 46 | } 47 | 48 | return filledTemplate; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/generators/llms-txt/template.txt: -------------------------------------------------------------------------------- 1 | # Node.js Documentation 2 | 3 | > Node.js is an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside a web browser. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient for building scalable network applications. 4 | 5 | Below are the sections of the API documentation. Look out especially towards the links that point towards guidance/introduction to the structure of this documentation. 6 | 7 | ## API Documentations 8 | -------------------------------------------------------------------------------- /src/generators/llms-txt/utils/__tests__/buildApiDocLink.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { getEntryDescription, buildApiDocLink } from '../buildApiDocLink.mjs'; 5 | 6 | describe('getEntryDescription', () => { 7 | it('returns llm_description when available', () => { 8 | const entry = { 9 | llm_description: 'LLM generated description', 10 | content: { children: [] }, 11 | }; 12 | 13 | const result = getEntryDescription(entry); 14 | assert.equal(result, 'LLM generated description'); 15 | }); 16 | 17 | it('extracts first paragraph when no llm_description', () => { 18 | const entry = { 19 | content: { 20 | children: [ 21 | { 22 | type: 'paragraph', 23 | children: [{ type: 'text', value: 'First paragraph' }], 24 | }, 25 | ], 26 | }, 27 | }; 28 | 29 | const result = getEntryDescription(entry); 30 | assert.ok(result.length > 0); 31 | }); 32 | 33 | it('returns empty string when no paragraph found', () => { 34 | const entry = { 35 | content: { 36 | children: [ 37 | { type: 'heading', children: [{ type: 'text', value: 'Title' }] }, 38 | ], 39 | }, 40 | }; 41 | 42 | const result = getEntryDescription(entry); 43 | assert.equal(result, ''); 44 | }); 45 | 46 | it('removes newlines from description', () => { 47 | const entry = { 48 | content: { 49 | children: [ 50 | { 51 | type: 'paragraph', 52 | children: [{ type: 'text', value: 'Line 1\nLine 2\r\nLine 3' }], 53 | }, 54 | ], 55 | }, 56 | }; 57 | 58 | const result = getEntryDescription(entry); 59 | assert.equal(result.includes('\n'), false); 60 | assert.equal(result.includes('\r'), false); 61 | }); 62 | }); 63 | 64 | describe('buildApiDocLink', () => { 65 | it('builds markdown link with description', () => { 66 | const entry = { 67 | heading: { data: { name: 'Test API' } }, 68 | api_doc_source: 'doc/api/test.md', 69 | llm_description: 'Test description', 70 | }; 71 | 72 | const result = buildApiDocLink(entry); 73 | assert.ok(result.includes('[Test API]')); 74 | assert.ok(result.includes('/docs/latest/api/test.md')); 75 | assert.ok(result.includes('Test description')); 76 | }); 77 | 78 | it('handles doc path replacement', () => { 79 | const entry = { 80 | heading: { data: { name: 'API Method' } }, 81 | api_doc_source: 'doc/some/path.md', 82 | content: { children: [] }, 83 | }; 84 | 85 | const result = buildApiDocLink(entry); 86 | assert.ok(result.includes('/docs/latest/some/path.md')); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/generators/llms-txt/utils/buildApiDocLink.mjs: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from '../../../constants.mjs'; 2 | import { transformNodeToString } from '../../../utils/unist.mjs'; 3 | 4 | /** 5 | * Retrieves the description of a given API doc entry. It first checks whether 6 | * the entry has a llm_description property. If not, it extracts the first 7 | * paragraph from the entry's content. 8 | * 9 | * @param {ApiDocMetadataEntry} entry 10 | * @returns {string} 11 | */ 12 | export const getEntryDescription = entry => { 13 | if (entry.llm_description) { 14 | return entry.llm_description.trim(); 15 | } 16 | 17 | const descriptionNode = entry.content.children.find( 18 | child => child.type === 'paragraph' 19 | ); 20 | 21 | if (!descriptionNode) { 22 | return ''; 23 | } 24 | 25 | return ( 26 | transformNodeToString(descriptionNode) 27 | // Remove newlines and extra spaces 28 | .replace(/[\r\n]+/g, '') 29 | ); 30 | }; 31 | 32 | /** 33 | * Builds a markdown link for an API doc entry 34 | * 35 | * @param {ApiDocMetadataEntry} entry 36 | * @returns {string} 37 | */ 38 | export const buildApiDocLink = entry => { 39 | const title = entry.heading.data.name; 40 | 41 | const path = entry.api_doc_source.replace(/^doc\//, '/docs/latest/'); 42 | const url = new URL(path, BASE_URL); 43 | 44 | const link = `[${title}](${url})`; 45 | 46 | const description = getEntryDescription(entry); 47 | 48 | return `${link}: ${description}`; 49 | }; 50 | -------------------------------------------------------------------------------- /src/generators/man-page/constants.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // https://github.com/nodejs/node/blob/main/doc/api/cli.md#options 4 | // This slug should reference the section where the available 5 | // options are defined. 6 | export const DOC_SLUG_OPTIONS = 'options'; 7 | 8 | // This is the filename of the manpage. 9 | // The format is `.1` (Hence, `node.1`) 10 | export const OUTPUT_FILENAME = 'node.1'; 11 | 12 | // https://github.com/nodejs/node/blob/main/doc/api/cli.md#environment-variables-1 13 | // This slug should reference the section where the available 14 | // environment variables are defined. 15 | export const DOC_SLUG_ENVIRONMENT = 'environment-variables-1'; 16 | -------------------------------------------------------------------------------- /src/generators/man-page/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { writeFile, readFile } from 'node:fs/promises'; 4 | import { join } from 'node:path'; 5 | 6 | import { 7 | DOC_SLUG_ENVIRONMENT, 8 | DOC_SLUG_OPTIONS, 9 | OUTPUT_FILENAME, 10 | } from './constants.mjs'; 11 | import { 12 | convertOptionToMandoc, 13 | convertEnvVarToMandoc, 14 | } from './utils/converter.mjs'; 15 | 16 | /** 17 | * This generator generates a man page version of the CLI.md file. 18 | * See https://man.openbsd.org/mdoc.7 for the formatting. 19 | * 20 | * @typedef {Array} Input 21 | * 22 | * @type {GeneratorMetadata} 23 | */ 24 | export default { 25 | name: 'man-page', 26 | 27 | version: '1.0.0', 28 | 29 | description: 'Generates the Node.js man-page.', 30 | 31 | dependsOn: 'metadata', 32 | 33 | /** 34 | * Generates the Node.js man-page 35 | * 36 | * @param {Input} input 37 | * @param {Partial} options 38 | */ 39 | async generate(input, options) { 40 | // Filter to only 'cli'. 41 | const components = input.filter(({ api }) => api === 'cli'); 42 | 43 | if (!components.length) { 44 | throw new Error('Could not find any `cli` documentation.'); 45 | } 46 | 47 | // Find the appropriate headers 48 | const optionsStart = components.findIndex( 49 | ({ slug }) => slug === DOC_SLUG_OPTIONS 50 | ); 51 | 52 | const environmentStart = components.findIndex( 53 | ({ slug }) => slug === DOC_SLUG_ENVIRONMENT 54 | ); 55 | 56 | // The first header that is <3 in depth after environmentStart 57 | const environmentEnd = components.findIndex( 58 | ({ heading }, index) => heading.depth < 3 && index > environmentStart 59 | ); 60 | 61 | const output = { 62 | // Extract the CLI options. 63 | options: extractMandoc( 64 | components, 65 | optionsStart + 1, 66 | environmentStart, 67 | convertOptionToMandoc 68 | ), 69 | // Extract the environment variables. 70 | env: extractMandoc( 71 | components, 72 | environmentStart + 1, 73 | environmentEnd, 74 | convertEnvVarToMandoc 75 | ), 76 | }; 77 | 78 | const template = await readFile( 79 | join(import.meta.dirname, 'template.1'), 80 | 'utf-8' 81 | ); 82 | 83 | const filledTemplate = template 84 | .replace('__OPTIONS__', output.options) 85 | .replace('__ENVIRONMENT__', output.env); 86 | 87 | if (options.output) { 88 | await writeFile(join(options.output, OUTPUT_FILENAME), filledTemplate); 89 | } 90 | 91 | return filledTemplate; 92 | }, 93 | }; 94 | 95 | /** 96 | * @param {Array} components 97 | * @param {number} start 98 | * @param {number} end 99 | * @param {(element: ApiDocMetadataEntry) => string} convert 100 | * @returns {string} 101 | */ 102 | function extractMandoc(components, start, end, convert) { 103 | return components 104 | .slice(start, end) 105 | .filter(({ heading }) => heading.depth === 3) 106 | .map(convert) 107 | .join(''); 108 | } 109 | -------------------------------------------------------------------------------- /src/generators/man-page/template.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\" This file was generated automatically by the @nodejs/doc-kit tool. 3 | .\" Please do not edit this file manually. Make any updates to cli.md 4 | .\" and regenerate the file afterward. 5 | .\" 6 | .\" To regenerate this file, run `make doc/node.1`. 7 | .\" 8 | .\"====================================================================== 9 | .Dd $Mdocdate$ 10 | .Dt NODE 1 11 | . 12 | .Sh NAME 13 | .Nm node 14 | .Nd server-side JavaScript runtime 15 | . 16 | .Sh SYNOPSIS 17 | .Nm node 18 | .Op Ar options 19 | .Op Ar v8 options 20 | .Op Ar | Fl e Ar string | Fl - 21 | .Op Ar arguments ... 22 | . 23 | .Nm node 24 | .Cm inspect, 25 | .Op Ar | Fl e Ar string | Ar : 26 | .Ar ... 27 | . 28 | .Nm node 29 | .Op Fl -v8-options 30 | . 31 | .Sh DESCRIPTION 32 | Node.js is a set of libraries for JavaScript which allows it to be used outside of the browser. 33 | It is primarily focused on creating simple, easy-to-build network clients and servers. 34 | .Pp 35 | Execute 36 | .Nm 37 | without arguments to start a REPL. 38 | . 39 | .Sh OPTIONS 40 | .Bl -tag -width 6n 41 | __OPTIONS__ 42 | .El 43 | . 44 | .Sh ENVIRONMENT 45 | .Bl -tag -width 6n 46 | __ENVIRONMENT__ 47 | .El 48 | . 49 | .Sh BUGS 50 | Bugs are tracked in GitHub Issues: 51 | .Sy https://github.com/nodejs/node/issues 52 | . 53 | .Sh COPYRIGHT 54 | Copyright Node.js contributors. 55 | Node.js is available under the MIT license. 56 | . 57 | .Pp 58 | Node.js also includes external libraries that are available under a variety of licenses. 59 | See 60 | .Sy https://github.com/nodejs/node/blob/HEAD/LICENSE 61 | for the full license text. 62 | . 63 | .Sh SEE ALSO 64 | Website: 65 | .Sy https://nodejs.org/ 66 | . 67 | .Pp 68 | Documentation: 69 | .Sy https://nodejs.org/api/ 70 | . 71 | .Pp 72 | GitHub repository and issue tracker: 73 | .Sy https://github.com/nodejs/node 74 | -------------------------------------------------------------------------------- /src/generators/metadata/constants.mjs: -------------------------------------------------------------------------------- 1 | // On "About this Documentation", we define the stability indices, and thus 2 | // we don't need to check it for stability references 3 | export const IGNORE_STABILITY_STEMS = ['documentation']; 4 | -------------------------------------------------------------------------------- /src/generators/metadata/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { parseApiDoc } from './utils/parse.mjs'; 4 | 5 | /** 6 | * This generator generates a flattened list of metadata entries from a API doc 7 | * 8 | * @typedef {Array>} Input 9 | * @typedef {Array} Output 10 | * 11 | * @type {GeneratorMetadata} 12 | */ 13 | export default { 14 | name: 'metadata', 15 | 16 | version: '1.0.0', 17 | 18 | description: 'generates a flattened list of API doc metadata entries', 19 | 20 | dependsOn: 'ast', 21 | 22 | /** 23 | * Process a chunk of API doc files in a worker thread. 24 | * Called by chunk-worker.mjs for parallel processing. 25 | * 26 | * @param {Input} fullInput - Full input array (parsed API doc files) 27 | * @param {number[]} itemIndices - Indices of files to process 28 | * @param {Partial} deps - Dependencies passed from generate() 29 | * @returns {Promise} Metadata entries for processed files 30 | */ 31 | async processChunk(fullInput, itemIndices, { typeMap }) { 32 | const results = []; 33 | 34 | for (const idx of itemIndices) { 35 | results.push(...parseApiDoc(fullInput[idx], typeMap)); 36 | } 37 | 38 | return results; 39 | }, 40 | 41 | /** 42 | * @param {Input} inputs 43 | * @param {Partial} options 44 | * @returns {AsyncGenerator} 45 | */ 46 | async *generate(inputs, { typeMap, worker }) { 47 | const deps = { typeMap }; 48 | 49 | // Stream chunks as they complete - allows dependent generators 50 | // to start collecting/preparing while we're still processing 51 | for await (const chunkResult of worker.stream(inputs, inputs, deps)) { 52 | yield chunkResult.flat(); 53 | } 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/generators/orama-db/__tests__/index.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { buildHierarchicalTitle } from '../index.mjs'; 5 | 6 | describe('buildHierarchicalTitle', () => { 7 | const mockHeadings = [ 8 | { heading: { data: { name: 'Module' }, depth: 1 } }, 9 | { heading: { data: { name: 'Class' }, depth: 2 } }, 10 | { heading: { data: { name: 'Method' }, depth: 3 } }, 11 | { heading: { data: { name: 'Parameter' }, depth: 4 } }, 12 | ]; 13 | 14 | it('should build single level title', () => { 15 | const result = buildHierarchicalTitle(mockHeadings, 0); 16 | assert.equal(result, 'Module'); 17 | }); 18 | 19 | it('should build two level hierarchy', () => { 20 | const result = buildHierarchicalTitle(mockHeadings, 1); 21 | assert.equal(result, 'Class'); 22 | }); 23 | 24 | it('should build three level hierarchy', () => { 25 | const result = buildHierarchicalTitle(mockHeadings, 2); 26 | assert.equal(result, 'Class > Method'); 27 | }); 28 | 29 | it('should build full hierarchy', () => { 30 | const result = buildHierarchicalTitle(mockHeadings, 3); 31 | assert.equal(result, 'Class > Method > Parameter'); 32 | }); 33 | 34 | it('should handle non-sequential depths', () => { 35 | const headings = [ 36 | { heading: { data: { name: 'Root' }, depth: 1 } }, 37 | { heading: { data: { name: 'Deep' }, depth: 4 } }, 38 | ]; 39 | 40 | const result = buildHierarchicalTitle(headings, 1); 41 | assert.equal(result, 'Deep'); 42 | }); 43 | 44 | it('should handle same depth headings', () => { 45 | const headings = [ 46 | { heading: { data: { name: 'First' }, depth: 2 } }, 47 | { heading: { data: { name: 'Second' }, depth: 2 } }, 48 | ]; 49 | 50 | const result = buildHierarchicalTitle(headings, 1); 51 | assert.equal(result, 'Second'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/generators/orama-db/constants.mjs: -------------------------------------------------------------------------------- 1 | export const SCHEMA = { 2 | title: 'string', 3 | description: 'string', 4 | href: 'string', 5 | siteSection: 'string', 6 | }; 7 | -------------------------------------------------------------------------------- /src/generators/orama-db/index.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { writeFile } from 'node:fs/promises'; 4 | 5 | import { create, save, insertMultiple } from '@orama/orama'; 6 | 7 | import { SCHEMA } from './constants.mjs'; 8 | import { groupNodesByModule } from '../../utils/generators.mjs'; 9 | import { transformNodeToString } from '../../utils/unist.mjs'; 10 | 11 | /** 12 | * Builds a hierarchical title chain based on heading depths 13 | * 14 | * @param {ApiDocMetadataEntry[]} headings - All headings sorted by order 15 | * @param {number} currentIndex - Index of current heading 16 | * @returns {string} Hierarchical title 17 | */ 18 | export function buildHierarchicalTitle(headings, currentIndex) { 19 | const currentNode = headings[currentIndex]; 20 | const titleChain = [currentNode.heading.data.name]; 21 | let targetDepth = currentNode.heading.depth - 1; 22 | 23 | // Walk backwards through preceding headings to build hierarchy 24 | for (let i = currentIndex - 1; i >= 1 && targetDepth > 0; i--) { 25 | const heading = headings[i]; 26 | const headingDepth = heading.heading.depth; 27 | 28 | if (headingDepth <= targetDepth) { 29 | titleChain.unshift(heading.heading.data.name); 30 | targetDepth = headingDepth - 1; 31 | } 32 | } 33 | 34 | return titleChain.join(' > '); 35 | } 36 | 37 | /** 38 | * This generator is responsible for generating the Orama database for the 39 | * API docs. It is based on the legacy-json generator. 40 | * 41 | * @typedef {Array} Input 42 | * 43 | * @type {GeneratorMetadata} 44 | */ 45 | export default { 46 | name: 'orama-db', 47 | 48 | version: '1.0.0', 49 | 50 | description: 'Generates the Orama database for the API docs.', 51 | 52 | dependsOn: 'metadata', 53 | 54 | /** 55 | * Generates the Orama database. 56 | * 57 | * @param {Input} input 58 | * @param {Partial} options 59 | */ 60 | async generate(input, { output }) { 61 | if (!input?.length) { 62 | throw new Error('Input data is required and must not be empty'); 63 | } 64 | 65 | if (!output) { 66 | throw new Error('Output path is required'); 67 | } 68 | 69 | const db = create({ schema: SCHEMA }); 70 | 71 | const apiGroups = groupNodesByModule(input); 72 | 73 | // Process all API groups and flatten into a single document array 74 | const documents = Array.from(apiGroups.values()).flatMap(headings => 75 | headings.map((entry, index) => { 76 | const hierarchicalTitle = buildHierarchicalTitle(headings, index); 77 | 78 | const paragraph = entry.content.children.find( 79 | child => child.type === 'paragraph' 80 | ); 81 | 82 | return { 83 | title: hierarchicalTitle, 84 | description: paragraph 85 | ? transformNodeToString(paragraph, true) 86 | : undefined, 87 | href: `${entry.api}.html#${entry.slug}`, 88 | siteSection: headings[0].heading.data.name, 89 | }; 90 | }) 91 | ); 92 | 93 | // Insert all documents 94 | await insertMultiple(db, documents); 95 | 96 | // Persist 97 | await writeFile(`${output}/orama-db.json`, JSON.stringify(save(db))); 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /src/generators/orama-db/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Orama } from '@orama/orama'; 2 | 3 | /** 4 | * Schema for the Orama database entry 5 | */ 6 | export interface OramaDbEntry { 7 | name: string; 8 | type: string; 9 | desc: string; 10 | stability: number; 11 | stabilityText: string; 12 | meta: { 13 | changes: string[]; 14 | added: string[]; 15 | napiVersion: string[]; 16 | deprecated: string[]; 17 | removed: string[]; 18 | }; 19 | } 20 | 21 | /** 22 | * Represents the Orama database for API docs 23 | */ 24 | export type OramaDb = Orama; 25 | -------------------------------------------------------------------------------- /src/generators/web/constants.mjs: -------------------------------------------------------------------------------- 1 | import { resolve, dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | export const ROOT = dirname(fileURLToPath(import.meta.url)); 5 | 6 | /** 7 | * @typedef {Object} JSXImportConfig 8 | * @property {string} name - The name of the component to be imported. 9 | * @property {string} source - The path to the component's source file or package. 10 | * @property {boolean} [isDefaultExport=true] - Indicates if it's a default export (true) or named export (false). Defaults to true if not specified. 11 | */ 12 | 13 | /** 14 | * @type {Record} 15 | * An object containing mappings for various JSX components to their import paths. 16 | */ 17 | export const JSX_IMPORTS = { 18 | NavBar: { 19 | name: 'NavBar', 20 | source: resolve(ROOT, './ui/components/NavBar'), 21 | }, 22 | SideBar: { 23 | name: 'SideBar', 24 | source: resolve(ROOT, './ui/components/SideBar'), 25 | }, 26 | MetaBar: { 27 | name: 'MetaBar', 28 | source: resolve(ROOT, './ui/components/MetaBar'), 29 | }, 30 | CodeBox: { 31 | name: 'CodeBox', 32 | source: resolve(ROOT, './ui/components/CodeBox'), 33 | }, 34 | CodeTabs: { 35 | name: 'CodeTabs', 36 | source: '@node-core/ui-components/MDX/CodeTabs', 37 | }, 38 | MDXTooltip: { 39 | name: 'MDXTooltip', 40 | isDefaultExport: false, 41 | source: '@node-core/ui-components/MDX/Tooltip', 42 | }, 43 | MDXTooltipContent: { 44 | name: 'MDXTooltipContent', 45 | isDefaultExport: false, 46 | source: '@node-core/ui-components/MDX/Tooltip', 47 | }, 48 | MDXTooltipTrigger: { 49 | name: 'MDXTooltipTrigger', 50 | isDefaultExport: false, 51 | source: '@node-core/ui-components/MDX/Tooltip', 52 | }, 53 | ChangeHistory: { 54 | name: 'ChangeHistory', 55 | source: '@node-core/ui-components/Common/ChangeHistory', 56 | }, 57 | AlertBox: { 58 | name: 'AlertBox', 59 | source: '@node-core/ui-components/Common/AlertBox', 60 | }, 61 | Article: { 62 | name: 'Article', 63 | source: '@node-core/ui-components/Containers/Article', 64 | }, 65 | Blockquote: { 66 | name: 'Blockquote', 67 | source: '@node-core/ui-components/Common/Blockquote', 68 | }, 69 | DataTag: { 70 | name: 'DataTag', 71 | source: '@node-core/ui-components/Common/DataTag', 72 | }, 73 | ArrowUpRightIcon: { 74 | name: 'ArrowUpRightIcon', 75 | source: '@heroicons/react/24/solid/ArrowUpRightIcon', 76 | }, 77 | NotificationProvider: { 78 | name: 'NotificationProvider', 79 | isDefaultExport: false, 80 | source: '@node-core/ui-components/Providers/NotificationProvider', 81 | }, 82 | }; 83 | 84 | /** 85 | * Specification rules for resource hints like prerendering and prefetching. 86 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API 87 | */ 88 | export const SPECULATION_RULES = JSON.stringify({ 89 | // Eagerly prefetch all links that point to the API docs themselves 90 | // in a moderate eagerness to improve resource loading 91 | prefetch: [{ where: { href_matches: '/*' }, eagerness: 'eager' }], 92 | prerender: [ 93 | // Eagerly prerender Sidebar links for faster navigation 94 | // These will be done in a moderate eagerness (hover, likely next navigation) 95 | { where: { selector_matches: '[rel~=prefetch]' }, eagerness: 'moderate' }, 96 | ], 97 | }); 98 | -------------------------------------------------------------------------------- /src/generators/web/index.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises'; 2 | import { createRequire } from 'node:module'; 3 | import { join } from 'node:path'; 4 | 5 | import createASTBuilder from './utils/generate.mjs'; 6 | import { processJSXEntries } from './utils/processing.mjs'; 7 | 8 | /** 9 | * Web generator - transforms JSX AST entries into complete web bundles. 10 | * 11 | * This generator processes JSX AST entries and produces: 12 | * - Server-side rendered HTML pages 13 | * - Client-side JavaScript with code splitting 14 | * - Bundled CSS styles 15 | * 16 | * Note: This generator does NOT support streaming/chunked processing because 17 | * processJSXEntries needs all entries together to generate code-split bundles. 18 | * 19 | * @typedef {Array} Input 20 | * @typedef {Array<{ html: string, css: string }>} Output 21 | * 22 | * @type {GeneratorMetadata} 23 | */ 24 | export default { 25 | name: 'web', 26 | 27 | version: '1.0.0', 28 | 29 | description: 'Generates HTML/CSS/JS bundles from JSX AST entries', 30 | 31 | dependsOn: 'jsx-ast', 32 | 33 | /** 34 | * Main generation function that processes JSX AST entries into web bundles. 35 | * 36 | * @param {Input} input - JSX AST entries to process. 37 | * @param {Partial} options - Generator options. 38 | * @returns {Promise} Processed HTML/CSS/JS content. 39 | */ 40 | async generate(input, { output, version }) { 41 | const template = await readFile( 42 | new URL('template.html', import.meta.url), 43 | 'utf-8' 44 | ); 45 | 46 | // Create AST builders for server and client programs 47 | const astBuilders = createASTBuilder(); 48 | 49 | // Create require function for resolving external packages in server code 50 | const requireFn = createRequire(import.meta.url); 51 | 52 | // Process all entries: convert JSX to HTML/CSS/JS 53 | const { results, css, chunks } = await processJSXEntries( 54 | input, 55 | template, 56 | astBuilders, 57 | requireFn, 58 | { version } 59 | ); 60 | 61 | // Process all entries together (required for code-split bundles) 62 | if (output) { 63 | // Write HTML files 64 | for (const { html, api } of results) { 65 | await writeFile(join(output, `${api}.html`), html, 'utf-8'); 66 | } 67 | 68 | // Write code-split JavaScript chunks 69 | for (const chunk of chunks) { 70 | await writeFile(join(output, chunk.fileName), chunk.code, 'utf-8'); 71 | } 72 | 73 | // Write CSS bundle 74 | await writeFile(join(output, 'styles.css'), css, 'utf-8'); 75 | } 76 | 77 | return results.map(({ html }) => ({ html: html.toString(), css })); 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /src/generators/web/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{title}} 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
    {{dehydrated}}
    24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/generators/web/ui/components/CodeBox.jsx: -------------------------------------------------------------------------------- 1 | import { CodeBracketIcon } from '@heroicons/react/24/outline'; 2 | import BaseCodeBox from '@node-core/ui-components/Common/BaseCodeBox'; 3 | import styles from '@node-core/ui-components/Common/BaseCodeBox/index.module.css'; 4 | import { useNotification } from '@node-core/ui-components/Providers/NotificationProvider'; 5 | 6 | import { STATIC_DATA } from '../constants.mjs'; 7 | 8 | const languageDisplayNameMap = new Map(STATIC_DATA.shikiDisplayNameMap); 9 | 10 | /** 11 | * Get the display name of a language 12 | * @param {string} language - The language ID 13 | */ 14 | export const getLanguageDisplayName = language => { 15 | const entry = Array.from(languageDisplayNameMap.entries()).find(([aliases]) => 16 | aliases.includes(language.toLowerCase()) 17 | ); 18 | 19 | return entry?.[1] ?? language.toLowerCase(); 20 | }; 21 | 22 | /** @param {import('react').PropsWithChildren<{ className: string }>} props */ 23 | export default ({ className, ...props }) => { 24 | const matches = className?.match(/language-(?[a-zA-Z]+)/); 25 | 26 | const language = matches?.groups?.language ?? ''; 27 | 28 | const notify = useNotification(); 29 | 30 | const onCopy = async text => { 31 | await navigator.clipboard.writeText(text); 32 | 33 | notify({ 34 | duration: 3000, 35 | message: ( 36 |
    37 | 38 | Copied to clipboard 39 |
    40 | ), 41 | }); 42 | }; 43 | 44 | return ( 45 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/generators/web/ui/components/MetaBar/index.jsx: -------------------------------------------------------------------------------- 1 | import { CodeBracketIcon, DocumentIcon } from '@heroicons/react/24/outline'; 2 | import Badge from '@node-core/ui-components/Common/Badge'; 3 | import MetaBar from '@node-core/ui-components/Containers/MetaBar'; 4 | import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub'; 5 | 6 | import styles from './index.module.css'; 7 | 8 | const iconMap = { 9 | JSON: CodeBracketIcon, 10 | MD: DocumentIcon, 11 | }; 12 | 13 | /** 14 | * @typedef MetaBarProps 15 | * @property {Array} headings - Array of page headings for table of contents 16 | * @property {string} addedIn - Version or date when feature was added 17 | * @property {string} readingTime - Estimated reading time for the page 18 | * @property {Array<[string, string]>} viewAs - Array of [title, path] tuples for view options 19 | * @property {string} editThisPage - URL for editing the current page 20 | */ 21 | 22 | const STABILITY_KINDS = ['error', 'warning', null, 'info']; 23 | const STABILITY_LABELS = ['D', 'E', null, 'L']; 24 | 25 | /** 26 | * MetaBar component that displays table of contents and page metadata 27 | * @param {MetaBarProps} props - Component props 28 | */ 29 | export default ({ 30 | headings = [], 31 | addedIn, 32 | readingTime, 33 | viewAs = [], 34 | editThisPage, 35 | }) => ( 36 | ({ 40 | ...heading, 41 | value: 42 | stability !== 2 ? ( 43 | <> 44 | {value} 45 | 50 | {STABILITY_LABELS[stability]} 51 | 52 | 53 | ) : ( 54 | value 55 | ), 56 | data: { id: slug }, 57 | })), 58 | }} 59 | items={{ 60 | 'Reading Time': readingTime, 61 | 'Added In': addedIn, 62 | 'View As': ( 63 |
      64 | {viewAs.map(([title, path]) => { 65 | const Icon = iconMap[title]; 66 | return ( 67 |
    1. 68 | 69 | {Icon && } 70 | {title} 71 | 72 |
    2. 73 | ); 74 | })} 75 |
    76 | ), 77 | Contribute: ( 78 | <> 79 | 80 | Edit this page 81 | 82 | ), 83 | }} 84 | /> 85 | ); 86 | -------------------------------------------------------------------------------- /src/generators/web/ui/components/MetaBar/index.module.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline; 3 | width: 1rem; 4 | height: 1rem; 5 | margin-right: 0.25rem; 6 | } 7 | 8 | .badge { 9 | display: inline-block; 10 | margin-left: 0.25rem; 11 | } 12 | -------------------------------------------------------------------------------- /src/generators/web/ui/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import NodejsLogo from '@node-core/ui-components/Common/NodejsLogo'; 2 | import ThemeToggle from '@node-core/ui-components/Common/ThemeToggle'; 3 | import NavBar from '@node-core/ui-components/Containers/NavBar'; 4 | import styles from '@node-core/ui-components/Containers/NavBar/index.module.css'; 5 | import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub'; 6 | 7 | import SearchBox from './SearchBox'; 8 | import { useTheme } from '../hooks/useTheme.mjs'; 9 | 10 | /** 11 | * NavBar component that displays the headings, search, etc. 12 | */ 13 | export default () => { 14 | const [theme, toggleTheme] = useTheme(); 15 | 16 | return ( 17 | 22 | 23 | 27 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/generators/web/ui/components/SearchBox/index.jsx: -------------------------------------------------------------------------------- 1 | import SearchModal from '@node-core/ui-components/Common/Search/Modal'; 2 | import SearchResults from '@node-core/ui-components/Common/Search/Results'; 3 | import SearchHit from '@node-core/ui-components/Common/Search/Results/Hit'; 4 | 5 | import useOrama from '../../hooks/useOrama.mjs'; 6 | 7 | const SearchBox = () => { 8 | const client = useOrama(); 9 | 10 | return ( 11 | 12 | } 15 | /> 16 | 17 | ); 18 | }; 19 | 20 | export default SearchBox; 21 | -------------------------------------------------------------------------------- /src/generators/web/ui/components/SideBar/index.jsx: -------------------------------------------------------------------------------- 1 | import Select from '@node-core/ui-components/Common/Select'; 2 | import SideBar from '@node-core/ui-components/Containers/Sidebar'; 3 | 4 | import styles from './index.module.css'; 5 | 6 | /** 7 | * @typedef {Object} SideBarProps 8 | * @property {string} pathname - The current document 9 | * @property {Array} versions - Available documentation versions 10 | * @property {string} currentVersion - Currently selected version 11 | * @property {Array<[string, string]>} docPages - [Title, URL] pairs 12 | */ 13 | 14 | /** 15 | * Redirect to a URL 16 | * @param {string} url URL 17 | */ 18 | const redirect = url => (window.location.href = url); 19 | 20 | /** 21 | * Sidebar component for MDX documentation with version selection and page navigation 22 | * @param {SideBarProps} props - Component props 23 | */ 24 | export default ({ versions, pathname, currentVersion, docPages }) => ( 25 | ({ label, link })), 31 | }, 32 | ]} 33 | onSelect={redirect} 34 | as={props => } 35 | > 36 |
    37 |