diff --git a/package-lock.json b/package-lock.json index 8d5b9e60..7ae117bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,22 @@ "dependencies": { "@actions/core": "^1.11.1", "@clack/prompts": "^0.10.1", + "@node-core/rehype-shiki": "^1.0.1-1815fa769361b836fa52cfab9c5bd4991f571c95", "@orama/orama": "^3.1.6", "@orama/plugin-data-persistence": "^3.1.6", "acorn": "^8.14.1", "commander": "^13.1.0", "dedent": "^1.6.0", + "estree-util-value-to-estree": "^3.4.0", "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "glob": "^11.0.2", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "html-minifier-terser": "^7.2.0", + "reading-time": "^1.5.0", + "recma-jsx": "^1.0.0", + "rehype-recma": "^1.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", @@ -436,6 +441,96 @@ "node": ">= 10" } }, + "node_modules/@node-core/rehype-shiki": { + "version": "1.0.1-1815fa769361b836fa52cfab9c5bd4991f571c95", + "resolved": "https://registry.npmjs.org/@node-core/rehype-shiki/-/rehype-shiki-1.0.1-1815fa769361b836fa52cfab9c5bd4991f571c95.tgz", + "integrity": "sha512-ec4cui06lkYpVDX+QnoOajZ/sicP8cGGoQPdNAoHYspN7HSybC7LwewR3tB4TeLh6o+cdggd5BI08XYsJEPemg==", + "dependencies": { + "@shikijs/core": "^3.3.0", + "@shikijs/engine-javascript": "^3.3.0", + "classnames": "~2.5.1", + "hast-util-to-string": "^3.0.1", + "shiki": "~3.3.0", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/core": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.3.0.tgz", + "integrity": "sha512-CovkFL2WVaHk6PCrwv6ctlmD4SS1qtIfN8yEyDXDYWh4ONvomdM9MaFw20qHuqJOcb8/xrkqoWQRJ//X10phOQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/engine-javascript": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.3.0.tgz", + "integrity": "sha512-XlhnFGv0glq7pfsoN0KyBCz9FJU678LZdQ2LqlIdAj6JKsg5xpYKay3DkazXWExp3DTJJK9rMOuGzU2911pg7Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.2.0" + } + }, + "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/engine-oniguruma": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.3.0.tgz", + "integrity": "sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/langs": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.3.0.tgz", + "integrity": "sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/themes": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.3.0.tgz", + "integrity": "sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.3.0" + } + }, + "node_modules/@node-core/rehype-shiki/node_modules/@shikijs/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.3.0.tgz", + "integrity": "sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@node-core/rehype-shiki/node_modules/shiki": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.3.0.tgz", + "integrity": "sha512-j0Z1tG5vlOFGW8JVj0Cpuatzvshes7VJy5ncDmmMaYcmnGW0Js1N81TOW98ivTFNZfKRn9uwEg/aIm638o368g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.3.0", + "@shikijs/engine-javascript": "3.3.0", + "@shikijs/engine-oniguruma": "3.3.0", + "@shikijs/langs": "3.3.0", + "@shikijs/themes": "3.3.0", + "@shikijs/types": "3.3.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, "node_modules/@orama/orama": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-3.1.6.tgz", @@ -640,7 +735,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -722,6 +816,15 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -851,6 +954,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -1130,6 +1249,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1329,6 +1480,65 @@ "node": ">=4.0" } }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.4.0.tgz", + "integrity": "sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, "node_modules/estree-util-visit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", @@ -1617,6 +1827,34 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -1784,6 +2022,46 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1819,6 +2097,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2380,6 +2668,66 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", @@ -3254,6 +3602,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-imports-exports": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", @@ -3394,6 +3767,61 @@ "node": ">=6" } }, + "node_modules/reading-time": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", + "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", + "license": "MIT" + }, + "node_modules/recma-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz", + "integrity": "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", @@ -3418,6 +3846,21 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-stringify": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", @@ -3881,6 +4324,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4070,6 +4531,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-remove": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", diff --git a/package.json b/package.json index 0491ba00..25906baa 100644 --- a/package.json +++ b/package.json @@ -39,17 +39,22 @@ "dependencies": { "@actions/core": "^1.11.1", "@clack/prompts": "^0.10.1", + "@node-core/rehype-shiki": "^1.0.1-1815fa769361b836fa52cfab9c5bd4991f571c95", "@orama/orama": "^3.1.6", "@orama/plugin-data-persistence": "^3.1.6", "acorn": "^8.14.1", "commander": "^13.1.0", "dedent": "^1.6.0", + "estree-util-value-to-estree": "^3.4.0", "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "glob": "^11.0.2", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "html-minifier-terser": "^7.2.0", + "reading-time": "^1.5.0", + "recma-jsx": "^1.0.0", + "rehype-recma": "^1.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", diff --git a/src/constants.mjs b/src/constants.mjs index 96d08969..30edc199 100644 --- a/src/constants.mjs +++ b/src/constants.mjs @@ -9,3 +9,14 @@ export const DOC_NODE_CHANGELOG_URL = // The base URL for the Node.js website export const BASE_URL = 'https://nodejs.org/'; + +// This is the Node.js Base URL for viewing a file within GitHub UI +export const DOC_NODE_BLOB_BASE_URL = + 'https://github.com/nodejs/node/blob/HEAD/'; + +// This is the Node.js API docs base URL for editing a file on GitHub UI +export const DOC_API_BLOB_EDIT_BASE_URL = + 'https://github.com/nodejs/node/edit/main/doc/api/'; + +// Base URL for a specific Node.js version within the Node.js API docs +export const DOC_API_BASE_URL_VERSION = 'https://nodejs.org/docs/latest-v'; diff --git a/src/generators/index.mjs b/src/generators/index.mjs index 7a9db52b..1012fb46 100644 --- a/src/generators/index.mjs +++ b/src/generators/index.mjs @@ -11,6 +11,7 @@ import apiLinks from './api-links/index.mjs'; import oramaDb from './orama-db/index.mjs'; import astJs from './ast-js/index.mjs'; import llmsTxt from './llms-txt/index.mjs'; +import jsx from './jsx/index.mjs'; export const publicGenerators = { 'json-simple': jsonSimple, @@ -23,6 +24,7 @@ export const publicGenerators = { 'api-links': apiLinks, 'orama-db': oramaDb, 'llms-txt': llmsTxt, + jsx, }; export const allGenerators = { diff --git a/src/generators/jsx/constants.mjs b/src/generators/jsx/constants.mjs new file mode 100644 index 00000000..d890cb99 --- /dev/null +++ b/src/generators/jsx/constants.mjs @@ -0,0 +1,115 @@ +/** + * UI classes for Node.js API stability levels + * + * @see https://nodejs.org/api/documentation.html#stability-index + */ +export const STABILITY_LEVELS = [ + 'danger', // (0) Deprecated + 'warning', // (1) Experimental + 'success', // (2) Stable + 'info', // (3) Legacy +]; + +/** + * HTML tag to UI component mappings + */ +export const TAG_TRANSFORMS = { + pre: 'CodeBox', + blockquote: 'Blockquote', +}; + +/** + * @see transformer.mjs's TODO comment + */ +export const TYPE_TRANSFORMS = { + raw: 'text', +}; + +/** + * API type icon configurations + */ +export const API_ICONS = { + event: { symbol: 'E', color: 'red' }, + method: { symbol: 'M', color: 'red' }, + property: { symbol: 'P', color: 'red' }, + class: { symbol: 'C', color: 'red' }, + module: { symbol: 'M', color: 'red' }, + classMethod: { symbol: 'S', color: 'red' }, + ctor: { symbol: 'C', color: 'red' }, +}; + +/** + * API lifecycle change labels + */ +export const LIFECYCLE_LABELS = { + added_in: 'Added in', + deprecated_in: 'Deprecated in', + removed_in: 'Removed in', + introduced_in: 'Introduced in', +}; + +// TODO(@avivkeller): These should be inherited from @node-core/website-i18n +export const INTERNATIONALIZABLE = { + sourceCode: 'Source Code: ', +}; + +/** + * Abstract Syntax Tree node type constants + */ +export const AST_NODE_TYPES = { + MDX: { + /** + * Text-level JSX element + * + * @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxtextelement + */ + JSX_INLINE_ELEMENT: 'mdxJsxTextElement', + + /** + * Block-level JSX element + * + * @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxflowelement + */ + JSX_BLOCK_ELEMENT: 'mdxJsxFlowElement', + + /** + * JSX attribute + * + * @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxattribute + */ + JSX_ATTRIBUTE: 'mdxJsxAttribute', + + /** + * JSX expression attribute + * + * @see https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxattributevalueexpression + */ + JSX_ATTRIBUTE_EXPRESSION: 'mdxJsxAttributeValueExpression', + }, + ESTREE: { + /** + * AST Program node + * + * @see https://github.com/estree/estree/blob/master/es5.md#programs + */ + PROGRAM: 'Program', + + /** + * Expression statement + * + * @see https://github.com/estree/estree/blob/master/es5.md#expressionstatement + */ + EXPRESSION_STATEMENT: 'ExpressionStatement', + }, + // TODO(@avivkeller): These should be inherited from the elements themselves + JSX: { + ALERT_BOX: 'AlertBox', + CHANGE_HISTORY: 'ChangeHistory', + CIRCULAR_ICON: 'CircularIcon', + NAV_BAR: 'NavBar', + ARTICLE: 'Article', + SIDE_BAR: 'SideBar', + META_BAR: 'MetaBar', + FOOTER: 'Footer', + }, +}; diff --git a/src/generators/jsx/index.mjs b/src/generators/jsx/index.mjs new file mode 100644 index 00000000..28404474 --- /dev/null +++ b/src/generators/jsx/index.mjs @@ -0,0 +1,68 @@ +import { + getCompatibleVersions, + groupNodesByModule, +} from '../../utils/generators.mjs'; +import buildContent from './utils/buildContent.mjs'; +import { getRemarkRecma } from '../../utils/remark.mjs'; +import { buildSideBarDocPages } from './utils/buildBarProps.mjs'; + +/** + * This generator generates a JSX AST from an input MDAST + * + * @typedef {Array} Input + * + * @type {GeneratorMetadata} + */ +export default { + name: 'jsx', + version: '1.0.0', + description: 'Generates JSX from the input AST', + dependsOn: 'ast', + + /** + * Generates a JSX AST + * + * @param {Input} entries + * @param {Partial} options + * @returns {Promise>} Array of generated content + */ + async generate(entries, { releases, version }) { + const remarkRecma = getRemarkRecma(); + const groupedModules = groupNodesByModule(entries); + + // Get sorted primary heading nodes + const headNodes = entries + .filter(node => node.heading.depth === 1) + .sort((a, b) => a.heading.data.name.localeCompare(b.heading.data.name)); + + // Generate table of contents + const docPages = buildSideBarDocPages(groupedModules, headNodes); + + // Process each head node and build content + const results = await Promise.all( + headNodes.map(entry => { + const versions = getCompatibleVersions( + entry.introduced_in, + releases, + true + ); + + const sideBarProps = { + versions: versions.map(({ version }) => `v${version.version}`), + currentVersion: `v${version.version}`, + currentPage: `${entry.api}.html`, + docPages, + }; + + return buildContent( + groupedModules.get(entry.api), + entry, + sideBarProps, + remarkRecma + ); + }) + ); + + return results; + }, +}; diff --git a/src/generators/jsx/test/utils.test.mjs b/src/generators/jsx/test/utils.test.mjs new file mode 100644 index 00000000..f98c0a4d --- /dev/null +++ b/src/generators/jsx/test/utils.test.mjs @@ -0,0 +1,80 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + buildSideBarDocPages, + buildMetaBarProps, +} from '../utils/buildBarProps.mjs'; +import buildContent from '../utils/buildContent.mjs'; +import { createJSXElement } from '../utils/ast.mjs'; +import { AST_NODE_TYPES } from '../constants.mjs'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkStringify from 'remark-stringify'; + +const sampleEntry = { + api: 'sample-api', + heading: { + depth: 2, + data: { name: 'SampleFunc', slug: 'sample-func', type: 'function' }, + }, + content: { + type: 'root', + children: [ + { type: 'text', value: 'Example text for testing reading time.' }, + ], + }, + added_in: 'v1.0.0', + source_link: '/src/index.js', + changes: [ + { + version: 'v1.1.0', + description: 'Improved performance', + 'pr-url': 'https://github.com/org/repo/pull/123', + }, + ], +}; + +test('buildSideBarDocPages returns expected format', () => { + const grouped = new Map([['sample-api', [sampleEntry]]]); + const result = buildSideBarDocPages(grouped, [sampleEntry]); + + assert.equal(result.length, 1); + assert.equal(result[0].title, 'SampleFunc'); + assert.equal(result[0].doc, 'sample-api.html'); + assert.deepEqual(result[0].headings, [['SampleFunc', '#sample-func']]); +}); + +test('buildMetaBarProps includes expected fields', () => { + const result = buildMetaBarProps(sampleEntry, [sampleEntry]); + + assert.equal(result.addedIn, 'v1.0.0'); + assert.deepEqual(result.viewAs, [['JSON', 'sample-api.json']]); + assert.ok(result.readingTime.startsWith('1 min')); + assert.ok(result.editThisPage.endsWith('sample-api.md')); + assert.deepEqual(result.headings, [{ depth: 2, value: 'SampleFunc' }]); +}); + +test('createJSXElement builds correct JSX tree', () => { + const el = createJSXElement('TestComponent', { + inline: false, + children: 'Some content', + dataAttr: { test: true }, + }); + + assert.equal(el.type, AST_NODE_TYPES.MDX.JSX_BLOCK_ELEMENT); + assert.equal(el.name, 'TestComponent'); + assert.ok(Array.isArray(el.children)); + assert.ok(el.attributes.some(attr => attr.name === 'dataAttr')); +}); + +test('buildContent processes entries and includes JSX wrapper elements', () => { + const processor = unified().use(remarkParse).use(remarkStringify); + const tree = buildContent([sampleEntry], sampleEntry, {}, processor); + + const article = tree.children.find( + child => child.name === AST_NODE_TYPES.JSX.ARTICLE + ); + assert.ok(article); + assert.ok(article.children.some(c => c.name === AST_NODE_TYPES.JSX.SIDE_BAR)); + assert.ok(article.children.some(c => c.name === AST_NODE_TYPES.JSX.FOOTER)); +}); diff --git a/src/generators/jsx/utils/ast.mjs b/src/generators/jsx/utils/ast.mjs new file mode 100644 index 00000000..e144186d --- /dev/null +++ b/src/generators/jsx/utils/ast.mjs @@ -0,0 +1,79 @@ +'use strict'; + +import { u as createTree } from 'unist-builder'; +import { valueToEstree } from 'estree-util-value-to-estree'; +import { AST_NODE_TYPES } from '../constants.mjs'; + +/** + * @typedef {Object} JSXOptions + * @property {boolean} [inline] - Whether the element is inline + * @property {(string | Array)} [children] - Child content or nodes + */ + +/** + * Creates an MDX JSX element with support for complex attribute values. + * + * @param {string} name - The name of the JSX element + * @param {JSXOptions & Record} [options={}] - Options including type, children, and JSX attributes + * @returns {import('unist').Node} The created MDX JSX element node + */ +export const createJSXElement = ( + name, + { inline = true, children = [], ...attributes } = {} +) => { + // Convert string children to text node or use array directly + const processedChildren = + typeof children === 'string' + ? [createTree('text', { value: children })] + : children; + + const elementType = inline + ? AST_NODE_TYPES.MDX.JSX_INLINE_ELEMENT + : AST_NODE_TYPES.MDX.JSX_BLOCK_ELEMENT; + + const attrs = Object.entries(attributes).map(([key, value]) => + createAttributeNode(key, value) + ); + + return createTree(elementType, { + name, + attributes: attrs, + children: processedChildren, + }); +}; + +/** + * Creates an MDX JSX attribute node based on the value type. + * + * @param {string} name - The attribute name + * @param {any} value - The attribute value + * @returns {import('unist').Node} The MDX JSX attribute node + */ +function createAttributeNode(name, value) { + // Use expression for objects and arrays + if (value !== null && typeof value === 'object') { + return createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE, { + name, + value: createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE_EXPRESSION, { + data: { + estree: { + type: AST_NODE_TYPES.ESTREE.PROGRAM, + body: [ + { + type: AST_NODE_TYPES.ESTREE.EXPRESSION_STATEMENT, + expression: valueToEstree(value), + }, + ], + }, + }, + }), + }); + } + + // For primitives, use simple string conversion. + // If undefined, pass nothing. + return createTree(AST_NODE_TYPES.MDX.JSX_ATTRIBUTE, { + name, + value: value == null ? value : String(value), + }); +} diff --git a/src/generators/jsx/utils/buildBarProps.mjs b/src/generators/jsx/utils/buildBarProps.mjs new file mode 100644 index 00000000..0238f381 --- /dev/null +++ b/src/generators/jsx/utils/buildBarProps.mjs @@ -0,0 +1,53 @@ +import readingTime from 'reading-time'; +import { visit } from 'unist-util-visit'; +import { DOC_API_BLOB_EDIT_BASE_URL } from '../../../constants.mjs'; + +/** + * Builds sidebar navigation for API documentation pages + * + * @param {Map>} groupedModules - Modules grouped by API + * @param {Array} headNodes - Main entry nodes for each API + */ +export const buildSideBarDocPages = (groupedModules, headNodes) => + headNodes.map(node => { + const moduleEntries = groupedModules.get(node.api); + + return { + title: node.heading.data.name, + doc: `${node.api}.html`, + headings: moduleEntries + .filter(entry => entry.heading?.data?.name && entry.heading.depth === 2) + .map(entry => [entry.heading.data.name, `#${entry.heading.data.slug}`]), + }; + }); + +/** + * Builds metadata for the sidebar and meta bar + * + * @param {ApiDocMetadataEntry} head - Main API metadata entry + * @param {Array} entries - All API metadata entries + */ +export const buildMetaBarProps = (head, entries) => { + // Extract text content for reading time calculation + const textContent = entries.reduce((acc, entry) => { + visit(entry.content, ['text', 'code'], node => { + acc += node.value || ''; + }); + return acc; + }, ''); + + const headings = entries + .filter(entry => entry.heading?.data?.name) + .map(entry => ({ + depth: entry.heading.depth, + value: entry.heading.data.name, + })); + + return { + headings, + addedIn: head.introduced_in || head.added_in || '', + readingTime: readingTime(textContent).text, + viewAs: [['JSON', `${head.api}.json`]], + editThisPage: `${DOC_API_BLOB_EDIT_BASE_URL}${head.api}.md`, + }; +}; diff --git a/src/generators/jsx/utils/buildContent.mjs b/src/generators/jsx/utils/buildContent.mjs new file mode 100644 index 00000000..478d39d4 --- /dev/null +++ b/src/generators/jsx/utils/buildContent.mjs @@ -0,0 +1,198 @@ +'use strict'; + +import { h as createElement } from 'hastscript'; +import { u as createTree } from 'unist-builder'; +import { SKIP, visit } from 'unist-util-visit'; + +import createQueries from '../../../utils/queries/index.mjs'; +import { createJSXElement } from './ast.mjs'; +import { + API_ICONS, + AST_NODE_TYPES, + STABILITY_LEVELS, + LIFECYCLE_LABELS, + INTERNATIONALIZABLE, +} from '../constants.mjs'; +import { DOC_NODE_BLOB_BASE_URL } from '../../../constants.mjs'; +import { enforceArray, sortChanges } from '../../../utils/generators.mjs'; +import { buildMetaBarProps } from './buildBarProps.mjs'; + +/** + * Creates a history of changes for an API element + * @param {ApiDocMetadataEntry} entry - The metadata entry containing change information + * @returns {import('unist').Node|null} JSX element representing change history or null if no changes + */ +const createChangeElement = entry => { + // Collect lifecycle changes (added, deprecated, etc.) + const changeEntries = Object.entries(LIFECYCLE_LABELS) + // Do we have this field? + .filter(([field]) => entry[field]) + // Get the versions as an array + .map(([field, label]) => [enforceArray(entry[field]), label]) + // Create the change entry + .map(([versions, label]) => ({ + versions, + label: `${label}: ${versions.join(', ')}`, + })); + + // Add explicit changes if they exist + if (entry.changes?.length) { + const explicitChanges = entry.changes.map(change => ({ + versions: enforceArray(change.version), + label: change.description, + url: change['pr-url'], + })); + + changeEntries.push(...explicitChanges); + } + + if (!changeEntries.length) { + return null; + } + + // Sort by version, newest first and create the JSX element + return createJSXElement(AST_NODE_TYPES.JSX.CHANGE_HISTORY, { + changes: sortChanges(changeEntries, 'versions'), + }); +}; + +/** + * Creates a source link element if a source link is available + * @param {string|undefined} sourceLink - The source link path + * @returns {import('hastscript').Element|null} The source link element or null if no source link + */ +const createSourceLink = sourceLink => { + if (!sourceLink) { + return null; + } + + return createElement('span', [ + INTERNATIONALIZABLE.sourceCode, + createElement( + 'a', + { href: `${DOC_NODE_BLOB_BASE_URL}${sourceLink}` }, + sourceLink + ), + ]); +}; + +/** + * Transforms a stability node into an AlertBox JSX element + * @param {import('mdast').Blockquote} node - The stability node to transform + * @param {number} index - The index of the node in its parent's children array + * @param {import('unist').Parent} parent - The parent node containing the stability node + * @returns {[typeof SKIP]} Visitor instruction to skip the node + */ +const transformStabilityNode = ({ data }, index, parent) => { + parent.children[index] = createJSXElement(AST_NODE_TYPES.JSX.ALERT_BOX, { + children: data.description, + level: STABILITY_LEVELS[data.index], + title: data.index, + }); + + return [SKIP]; +}; + +/** + * Enhances a heading node with metadata, source links, and styling + * @param {ApiDocMetadataEntry} entry - The API metadata entry + * @param {import('mdast').Heading} node - The heading node to transform + * @param {number} index - The index of the node in its parent's children array + * @param {import('unist').Parent} parent - The parent node containing the heading + * @returns {[typeof SKIP]} Visitor instruction to skip the node + */ +const transformHeadingNode = (entry, node, index, parent) => { + const { data, children } = node; + const headerChildren = [ + createElement(`h${data.depth + 1}`, [ + createElement(`a#${data.slug}`, { href: `#${data.slug}` }, children), + ]), + ]; + + // Add type icon if available + if (data.type in API_ICONS) { + headerChildren.unshift( + createJSXElement(AST_NODE_TYPES.JSX.CIRCULAR_ICON, API_ICONS[data.type]) + ); + } + + // Add change history if available + const changeElement = createChangeElement(entry); + if (changeElement) { + headerChildren.push(changeElement); + } + + // Replace node with new heading and anchor + parent.children[index] = createElement('div', headerChildren); + + // Add source link if available + const sourceLink = createSourceLink(entry.source_link); + if (sourceLink) { + parent.children.splice(index + 1, 0, sourceLink); + } + + return [SKIP]; +}; + +/** + * Processes an API documentation entry by applying transformations to its content + * @param {ApiDocMetadataEntry} entry - The API metadata entry to process + * @returns {import('unist').Node} The processed content + */ +const processEntry = entry => { + // Create a copy to avoid modifying the original + const content = structuredClone(entry.content); + + // Apply transformations + visit(content, createQueries.UNIST.isStabilityNode, transformStabilityNode); + visit(content, createQueries.UNIST.isHeading, (node, idx, parent) => + transformHeadingNode(entry, node, idx, parent) + ); + + return content; +}; + +/** + * Creates the overall content structure with processed entries + * @param {Array} entries - API documentation metadata entries + * @param {Record} sideBarProps - Props for the sidebar component + * @param {Record} metaBarProps - Props for the meta bar component + * @returns {import('unist').Node} The root node of the content tree + */ +const createContentStructure = (entries, sideBarProps, metaBarProps) => { + return createTree('root', [ + createJSXElement(AST_NODE_TYPES.JSX.NAV_BAR), + createJSXElement(AST_NODE_TYPES.JSX.ARTICLE, { + children: [ + createJSXElement(AST_NODE_TYPES.JSX.SIDE_BAR, sideBarProps), + createElement('div', [ + createElement('main', entries.map(processEntry)), + createJSXElement(AST_NODE_TYPES.JSX.META_BAR, metaBarProps), + ]), + createJSXElement(AST_NODE_TYPES.JSX.FOOTER), + ], + }), + ]); +}; + +/** + * Transforms API metadata entries into processed MDX content + * @param {Array} metadataEntries - API documentation metadata entries + * @param {ApiDocMetadataEntry} head - Main API metadata entry with version information + * @param {Object} sideBarProps - Props for the sidebar component + * @param {import('unified').Processor} remark - Remark processor instance for markdown processing + * @returns {string} The stringified MDX content + */ +const buildContent = (metadataEntries, head, sideBarProps, remark) => { + const metaBarProps = buildMetaBarProps(head, metadataEntries); + + const root = createContentStructure( + metadataEntries, + sideBarProps, + metaBarProps + ); + + return remark.runSync(root); +}; + +export default buildContent; diff --git a/src/generators/jsx/utils/transformer.mjs b/src/generators/jsx/utils/transformer.mjs new file mode 100644 index 00000000..8f3df55a --- /dev/null +++ b/src/generators/jsx/utils/transformer.mjs @@ -0,0 +1,26 @@ +import { visit } from 'unist-util-visit'; +import { TYPE_TRANSFORMS, TAG_TRANSFORMS } from '../constants.mjs'; + +/** + * @template {import('unist').Node} T + * @param {T} tree + * @returns {T} + */ +const transformer = tree => { + visit(tree, ['raw', 'element'], node => { + // TODO(@avivkeller): Our parsers shouldn't return raw nodes + // when they mistake "" for an HTML node, rather, they + // should return the string type that it is. + node.type = + node.type in TYPE_TRANSFORMS ? TYPE_TRANSFORMS[node.type] : node.type; + node.tagName = + node.tagName in TAG_TRANSFORMS + ? TAG_TRANSFORMS[node.tagName] + : node.tagName; + }); +}; + +/** + * Transforms elements in a syntax tree by replacing tag names according to the mapping. + */ +export default () => transformer; diff --git a/src/generators/legacy-html/constants.mjs b/src/generators/legacy-html/constants.mjs deleted file mode 100644 index a6e6c6cd..00000000 --- a/src/generators/legacy-html/constants.mjs +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -// This is the Node.js Base URL for viewing a file within GitHub UI -export const DOC_NODE_BLOB_BASE_URL = - 'https://github.com/nodejs/node/blob/HEAD/'; - -// This is the Node.js API docs base URL for editing a file on GitHub UI -export const DOC_API_BLOB_EDIT_BASE_URL = - 'https://github.com/nodejs/node/edit/main/doc/api/'; - -// Base URL for a specific Node.js version within the Node.js API docs -export const DOC_API_BASE_URL_VERSION = 'https://nodejs.org/docs/latest-v'; diff --git a/src/generators/legacy-html/utils/buildContent.mjs b/src/generators/legacy-html/utils/buildContent.mjs index 15d7b57a..aed07aac 100644 --- a/src/generators/legacy-html/utils/buildContent.mjs +++ b/src/generators/legacy-html/utils/buildContent.mjs @@ -8,7 +8,7 @@ import buildExtraContent from './buildExtraContent.mjs'; import createQueries from '../../../utils/queries/index.mjs'; -import { DOC_NODE_BLOB_BASE_URL } from '../constants.mjs'; +import { DOC_NODE_BLOB_BASE_URL } from '../../../constants.mjs'; /** * Builds a Markdown heading for a given node @@ -209,7 +209,7 @@ export default (headNodes, metadataEntries, remark) => { // Parses the metadata pieces of each node and the content metadataEntries.map(entry => { // Deep clones the content nodes to avoid affecting upstream nodes - const content = JSON.parse(JSON.stringify(entry.content)); + const content = structuredClone(entry.content); // Parses the Heading nodes into Heading elements visit(content, createQueries.UNIST.isHeading, buildHeading); diff --git a/src/generators/legacy-html/utils/buildDropdowns.mjs b/src/generators/legacy-html/utils/buildDropdowns.mjs index cefa0f15..0a45873f 100644 --- a/src/generators/legacy-html/utils/buildDropdowns.mjs +++ b/src/generators/legacy-html/utils/buildDropdowns.mjs @@ -1,16 +1,14 @@ 'use strict'; -import { major } from 'semver'; - import { - coerceSemVer, + getCompatibleVersions, getVersionFromSemVer, } from '../../../utils/generators.mjs'; import { DOC_API_BASE_URL_VERSION, DOC_API_BLOB_EDIT_BASE_URL, -} from '../constants.mjs'; +} from '../../../constants.mjs'; /** * Builds the Dropdown for the current Table of Contents @@ -58,12 +56,7 @@ const buildNavigation = navigationContents => * @param {Array} versions All available Node.js releases */ const buildVersions = (api, added, versions) => { - // All Node.js versions that support the current API; If there's no "introduced_at" field, - // we simply show all versions, as we cannot pinpoint the exact version - const coercedMajor = major(coerceSemVer(added)); - const compatibleVersions = versions.filter(({ version }) => - added ? version.major >= coercedMajor : true - ); + const compatibleVersions = getCompatibleVersions(added, versions); // Parses the SemVer version into something we use for URLs and to display the Node.js version // Then we create a `
  • ` entry for said version, ensuring we link to the correct API doc diff --git a/src/generators/legacy-json/utils/buildSection.mjs b/src/generators/legacy-json/utils/buildSection.mjs index 51dea85d..4846333e 100644 --- a/src/generators/legacy-json/utils/buildSection.mjs +++ b/src/generators/legacy-json/utils/buildSection.mjs @@ -3,15 +3,7 @@ import { getRemarkRehype } from '../../../utils/remark.mjs'; import { transformNodesToString } from '../../../utils/unist.mjs'; import { parseList } from './parseList.mjs'; import { SECTION_TYPE_PLURALS, UNPROMOTED_KEYS } from '../constants.mjs'; - -/** - * Converts a value to an array. - * @template T - * @param {T | T[]} val - The value to convert. - * @returns {T[]} The value as an array. - */ -const enforceArray = val => (Array.isArray(val) ? val : [val]); - +import { enforceArray } from '../../../utils/generators.mjs'; /** * */ diff --git a/src/linter/rules/invalid-change-version.mjs b/src/linter/rules/invalid-change-version.mjs index 0e69a5c0..fe6beb64 100644 --- a/src/linter/rules/invalid-change-version.mjs +++ b/src/linter/rules/invalid-change-version.mjs @@ -1,6 +1,7 @@ import { LINT_MESSAGES } from '../constants.mjs'; import { valid, parse } from 'semver'; import { env } from 'node:process'; +import { enforceArray } from '../../utils/generators.mjs'; const NODE_RELEASED_VERSIONS = env.NODE_RELEASED_VERSIONS?.split(','); @@ -56,7 +57,7 @@ const isInvalid = NODE_RELEASED_VERSIONS export const invalidChangeVersion = entries => entries.flatMap(({ changes, api_doc_source, yaml_position }) => changes.flatMap(({ version }) => - (Array.isArray(version) ? version : [version]) + enforceArray(version) .filter(isInvalid) .map(version => ({ level: 'error', diff --git a/src/metadata.mjs b/src/metadata.mjs index 6b65df94..472267c0 100644 --- a/src/metadata.mjs +++ b/src/metadata.mjs @@ -1,10 +1,7 @@ 'use strict'; import { u as createTree } from 'unist-builder'; - -import { compare } from 'semver'; - -import { coerceSemVer } from './utils/generators.mjs'; +import { sortChanges } from './utils/generators.mjs'; /** * This method allows us to handle creation of Metadata entries @@ -16,22 +13,6 @@ import { coerceSemVer } from './utils/generators.mjs'; * @param {InstanceType} slugger A GitHub Slugger */ const createMetadata = slugger => { - /** - * Maps `updates` into `changes` format, merges them and sorts them by version - * ç - * @param {Array} changes Changes to be merged into updates - * @returns {Array} Mapped, merged and sorted changes - */ - const sortChanges = changes => { - // Sorts the updates and changes by the first version on a given entry - return changes.sort((a, b) => { - const aVersion = Array.isArray(a.version) ? a.version[0] : a.version; - const bVersion = Array.isArray(b.version) ? b.version[0] : b.version; - - return compare(coerceSemVer(aVersion), coerceSemVer(bVersion)); - }); - }; - /** * This holds a temporary buffer of raw metadata before being * transformed into NavigationEntries and MetadataEntries diff --git a/src/threading/index.mjs b/src/threading/index.mjs index 488fa8e3..dd600418 100644 --- a/src/threading/index.mjs +++ b/src/threading/index.mjs @@ -44,12 +44,9 @@ export default class WorkerPool { this.changeActiveThreadCount(1); // Create and start the worker thread - const worker = new Worker( - new URL(import.meta.resolve('./worker.mjs')), - { - workerData: { name, dependencyOutput, extra }, - } - ); + const worker = new Worker(new URL('./worker.mjs', import.meta.url), { + workerData: { name, dependencyOutput, extra }, + }); // Handle worker thread messages (result or error) worker.on('message', result => { diff --git a/src/utils/generators.mjs b/src/utils/generators.mjs index c2df3d4c..f3bb217a 100644 --- a/src/utils/generators.mjs +++ b/src/utils/generators.mjs @@ -1,6 +1,6 @@ 'use strict'; -import { coerce } from 'semver'; +import { coerce, compare, major } from 'semver'; /** * Groups all the API metadata nodes by module (`api` property) so that we can process each different file @@ -53,3 +53,43 @@ export const coerceSemVer = version => { return coercedVersion; }; + +/** + * Gets compatible versions for an entry + * + * @param {string | import('semver').SemVer} introduced + * @param {Array} releases + * @param {Boolean} [includeNonMajor=false] + * @returns {Array} + */ +export const getCompatibleVersions = (introduced, releases) => { + const coercedMajor = major(coerceSemVer(introduced)); + // All Node.js versions that support the current API; If there's no "introduced_at" field, + // we simply show all versions, as we cannot pinpoint the exact version + return releases.filter(release => release.version.major >= coercedMajor); +}; + +/** + * Maps `updates` into `changes` format, merges them and sorts them by version + * ç + * @param {Array} changes Changes to be merged into updates + * @param {[string='version']} key The key where versions are stored + * @returns {Array} Mapped, merged and sorted changes + */ +export const sortChanges = (changes, key = 'version') => { + // Sorts the updates and changes by the first version on a given entry + return changes.sort((a, b) => { + const aVersion = Array.isArray(a[key]) ? a[key][0] : a[key]; + const bVersion = Array.isArray(b[key]) ? b[key][0] : b[key]; + + return compare(coerceSemVer(aVersion), coerceSemVer(bVersion)); + }); +}; + +/** + * Converts a value to an array. + * @template T + * @param {T | Array} val - The value to convert. + * @returns {Array} The value as an array. + */ +export const enforceArray = val => (Array.isArray(val) ? val : [val]); diff --git a/src/utils/remark.mjs b/src/utils/remark.mjs index 0fc4dda2..979b4671 100644 --- a/src/utils/remark.mjs +++ b/src/utils/remark.mjs @@ -7,9 +7,15 @@ import remarkParse from 'remark-parse'; import remarkRehype from 'remark-rehype'; import remarkStringify from 'remark-stringify'; +import rehypeRecma from 'rehype-recma'; import rehypeStringify from 'rehype-stringify'; +import recmaJsx from 'recma-jsx'; + import syntaxHighlighter from './highlighter.mjs'; +import transformElements from '../generators/jsx/utils/transformer.mjs'; +import { AST_NODE_TYPES } from '../generators/jsx/constants.mjs'; +import rehypeShikiji from '@node-core/rehype-shiki'; /** * Retrieves an instance of Remark configured to parse GFM (GitHub Flavored Markdown) @@ -37,3 +43,23 @@ export const getRemarkRehype = () => // We allow dangerous HTML to be passed through, since we have HTML within our Markdown // and we trust the sources of the Markdown files .use(rehypeStringify, { allowDangerousHtml: true }); + +/** + * Retrieves an instance of Remark configured to output JSX code. + * including parsing Code Boxes with syntax highlighting + */ +export const getRemarkRecma = () => + unified() + .use(remarkParse) + // We make Rehype ignore existing HTML nodes, and JSX nodes + // as these are nodes we manually created during the generation process + // We also allow dangerous HTML to be passed through, since we have HTML within our Markdown + // and we trust the sources of the Markdown files + .use(remarkRehype, { + allowDangerousHtml: true, + passThrough: ['element', ...Object.values(AST_NODE_TYPES.MDX)], + }) + .use(rehypeShikiji) + .use(transformElements) + .use(rehypeRecma) + .use(recmaJsx);