diff --git a/.nvmrc b/.nvmrc index 2bd5a0a9..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 +24 diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index e51fa2b7..5b9dbfd6 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -16,6 +16,10 @@ import { loadAndParse } from '../utils.mjs'; const availableGenerators = Object.keys(publicGenerators); +// Half of available logical CPUs guarantees in general all physical CPUs are being used +// which in most scenarios is the best way to maximize performance +const optimalThreads = Math.floor(cpus().length / 2) + 1; + /** * @typedef {Object} Options * @property {Array|string} input - Specifies the glob/path for input files. @@ -26,6 +30,7 @@ const availableGenerators = Object.keys(publicGenerators); * @property {string} typeMap - Specifies the path to the Node.js Type Map. * @property {string} [gitRef] - Git ref/commit URL. * @property {number} [threads] - Number of threads to allow. + * @property {number} [chunkSize] - Number of items to process per worker thread. */ /** @@ -61,10 +66,20 @@ export default { }, threads: { flags: ['-p', '--threads '], + desc: 'Number of worker threads to use', prompt: { type: 'text', message: 'How many threads to allow', - initialValue: String(Math.max(cpus().length, 1)), + initialValue: String(Math.max(optimalThreads, 1)), + }, + }, + chunkSize: { + flags: ['--chunk-size '], + desc: 'Number of items to process per worker thread (default: auto)', + prompt: { + type: 'text', + message: 'Items per worker thread', + initialValue: '10', }, }, version: { @@ -149,6 +164,7 @@ export default { releases, gitRef: opts.gitRef, threads: parseInt(opts.threads, 10), + chunkSize: parseInt(opts.chunkSize, 10), index, typeMap, }); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 207228f9..30894767 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -18,7 +18,7 @@ "acorn": "^8.15.0", "commander": "^14.0.2", "dedent": "^1.7.0", - "eslint-plugin-react-x": "^2.3.1", + "eslint-plugin-react-x": "^2.3.12", "estree-util-to-js": "^2.0.0", "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", @@ -26,9 +26,9 @@ "globals": "^16.5.0", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", - "lightningcss": "^1.30.1", + "lightningcss": "^1.30.2", "mdast-util-slice-markdown": "^2.0.1", - "preact": "^10.27.2", + "preact": "^10.28.0", "preact-render-to-string": "^6.6.3", "reading-time": "^1.5.0", "recma-jsx": "^1.0.1", @@ -39,9 +39,9 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", - "rolldown": "^1.0.0-beta.47", - "semver": "^7.7.2", - "shiki": "^3.17.0", + "rolldown": "^1.0.0-beta.53", + "semver": "^7.7.3", + "shiki": "^3.19.0", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-find-after": "^5.0.0", @@ -50,24 +50,24 @@ "unist-util-select": "^5.1.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3", - "yaml": "^2.8.1" + "yaml": "^2.8.2" }, "bin": { "doc-kit": "bin/cli.mjs" }, "devDependencies": { - "@eslint/js": "^9.36.0", + "@eslint/js": "^9.39.1", "@reporters/github": "^1.11.0", "@types/mdast": "^4.0.4", - "@types/node": "^22.18.6", + "@types/node": "^24.10.1", "c8": "^10.1.3", - "eslint": "^9.36.0", + "eslint": "^9.39.1", "eslint-import-resolver-node": "^0.3.9", "eslint-plugin-import-x": "^4.16.1", - "eslint-plugin-jsdoc": "^61.1.12", + "eslint-plugin-jsdoc": "^61.4.1", "husky": "^9.1.7", - "lint-staged": "^16.2.6", - "prettier": "3.6.2" + "lint-staged": "^16.2.7", + "prettier": "3.7.4" } }, "node_modules/@actions/core": { @@ -149,9 +149,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "license": "MIT", "optional": true, "dependencies": { @@ -160,9 +160,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "license": "MIT", "optional": true, "dependencies": { @@ -246,89 +246,105 @@ } }, "node_modules/@eslint-react/ast": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-2.3.1.tgz", - "integrity": "sha512-jB/P72HVbZcC7DtUvjna8tjPSageAS6L9x5muMsBRQxEXkfv2J6CPX47sSpaPu1mMJn1Zzpn9m5z4aTPbfV6Ug==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-2.3.12.tgz", + "integrity": "sha512-2wlRvqS4dxleGlL4Gp3Bh5PNb47wnAEa99CsGppzWCFXSPvm3d/bM5nJPvOwQOF53+PGa6xq1ZqwGh70zL7+zw==", "license": "MIT", "dependencies": { - "@eslint-react/eff": "2.3.1", - "@typescript-eslint/types": "^8.46.2", - "@typescript-eslint/typescript-estree": "^8.46.2", - "@typescript-eslint/utils": "^8.46.2", - "string-ts": "^2.2.1" + "@eslint-react/eff": "2.3.12", + "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/typescript-estree": "^8.48.1", + "@typescript-eslint/utils": "^8.48.1", + "string-ts": "^2.3.1" }, "engines": { "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@eslint-react/core": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-2.3.1.tgz", - "integrity": "sha512-R0gXjIqHqqYSeHxNMbXblnlwzmZ2gD32aVPmrJB+SrLP0rItzo/WgVSvstjOK5+N5KExdM87hopFcqnlZS3ONg==", - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "2.3.1", - "@eslint-react/eff": "2.3.1", - "@eslint-react/shared": "2.3.1", - "@eslint-react/var": "2.3.1", - "@typescript-eslint/scope-manager": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "@typescript-eslint/utils": "^8.46.2", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-2.3.12.tgz", + "integrity": "sha512-Q3w6f0WfVyIJriJa+tYHS4rmVQ3nwnubCH7o/VYlBCR3qczpvpvkCi2XK4clU/7vpVwHbbaXGICAbJu7tNZqoQ==", + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.3.12", + "@eslint-react/eff": "2.3.12", + "@eslint-react/shared": "2.3.12", + "@eslint-react/var": "2.3.12", + "@typescript-eslint/scope-manager": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/utils": "^8.48.1", "birecord": "^0.1.1", "ts-pattern": "^5.9.0" }, "engines": { "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@eslint-react/eff": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-2.3.1.tgz", - "integrity": "sha512-k58lxHmhzatRZXVFzWdLZwfRwPgI5Thhf7BNVJ9F+NI2G1fFypclSVFRPqjGmI5jQ8bqB+5UVt9Rh49rZGZPzw==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-2.3.12.tgz", + "integrity": "sha512-QjFENG1VGVrD67YFc0yiLm9zef2kTeXZGux4hlMjGLnxTHnn0tPx4T/xGzh5C1WRmolcNeIzjVWMqSngFrTphQ==", "license": "MIT", "engines": { "node": ">=20.19.0" } }, "node_modules/@eslint-react/shared": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-2.3.1.tgz", - "integrity": "sha512-UiTbPi1i7UPdsIT2Z7mKZ3zzrgAm1GLeexkKe4QwvZJ1LLeEJmgMwHUw852+VzlDeV8stcQmZ9zWqFX2L0CmGg==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-2.3.12.tgz", + "integrity": "sha512-mIgxjEwKOknJabbQs/bxvkEhKitJnET0QDc0a89pFx36DBLJIEvdcGMCDEXFgtgjDV/WwMxIava/+coE6T3Dyw==", "license": "MIT", "dependencies": { - "@eslint-react/eff": "2.3.1", - "@typescript-eslint/utils": "^8.46.2", + "@eslint-react/eff": "2.3.12", + "@typescript-eslint/utils": "^8.48.1", "ts-pattern": "^5.9.0", - "zod": "^4.1.12" + "zod": "^4.1.13" }, "engines": { "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@eslint-react/shared/node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/@eslint-react/var": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-2.3.1.tgz", - "integrity": "sha512-1rC9dbuKKMq77pPoODGT91VTA3ReivIAfdFJePEjscPSRAUhCy7QPA/yK8MPe9nTsG89IDV+hilCGKiLZW8vNQ==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-2.3.12.tgz", + "integrity": "sha512-jjgeRcop74NTzWzCF8rBN1H5avdSDLEOALJjwmYWOdxoSUNGO7OIeM/pZvHZ7G36kHDuD619P2JauCVM2/c+7A==", "license": "MIT", "dependencies": { - "@eslint-react/ast": "2.3.1", - "@eslint-react/eff": "2.3.1", - "@typescript-eslint/scope-manager": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "@typescript-eslint/utils": "^8.46.2", + "@eslint-react/ast": "2.3.12", + "@eslint-react/eff": "2.3.12", + "@typescript-eslint/scope-manager": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/utils": "^8.48.1", "ts-pattern": "^5.9.0" }, "engines": { "node": ">=20.19.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@eslint/config-array": { @@ -1199,9 +1215,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.96.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.96.0.tgz", - "integrity": "sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw==", + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.101.0.tgz", + "integrity": "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -2084,9 +2100,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-vPP9/MZzESh9QtmvQYojXP/midjgkkc1E4AdnPPAzQXo668ncHJcVLKjJKzoBdsQmaIvNjrMdsCwES8vTQHRQw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==", "cpu": [ "arm64" ], @@ -2100,9 +2116,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-Lc3nrkxeaDVCVl8qR3qoxh6ltDZfkQ98j5vwIr5ALPkgjZtDK4BGCrrBoLpGVMg+csWcaqUbwbKwH5yvVa0oOw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==", "cpu": [ "arm64" ], @@ -2116,9 +2132,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.47.tgz", - "integrity": "sha512-eBYxQDwP0O33plqNVqOtUHqRiSYVneAknviM5XMawke3mwMuVlAsohtOqEjbCEl/Loi/FWdVeks5WkqAkzkYWQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==", "cpu": [ "x64" ], @@ -2132,9 +2148,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.47.tgz", - "integrity": "sha512-Ns+kgp2+1Iq/44bY/Z30DETUSiHY7ZuqaOgD5bHVW++8vme9rdiWsN4yG4rRPXkdgzjvQ9TDHmZZKfY4/G11AA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==", "cpu": [ "x64" ], @@ -2148,9 +2164,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.47.tgz", - "integrity": "sha512-4PecgWCJhTA2EFOlptYJiNyVP2MrVP4cWdndpOu3WmXqWqZUmSubhb4YUAIxAxnXATlGjC1WjxNPhV7ZllNgdA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.53.tgz", + "integrity": "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==", "cpu": [ "arm" ], @@ -2164,9 +2180,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.47.tgz", - "integrity": "sha512-CyIunZ6D9U9Xg94roQI1INt/bLkOpPsZjZZkiaAZ0r6uccQdICmC99M9RUPlMLw/qg4yEWLlQhG73W/mG437NA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==", "cpu": [ "arm64" ], @@ -2180,9 +2196,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.47.tgz", - "integrity": "sha512-doozc/Goe7qRCSnzfJbFINTHsMktqmZQmweull6hsZZ9sjNWQ6BWQnbvOlfZJe4xE5NxM1NhPnY5Giqnl3ZrYQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==", "cpu": [ "arm64" ], @@ -2196,9 +2212,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.47.tgz", - "integrity": "sha512-fodvSMf6Aqwa0wEUSTPewmmZOD44rc5Tpr5p9NkwQ6W1SSpUKzD3SwpJIgANDOhwiYhDuiIaYPGB7Ujkx1q0UQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==", "cpu": [ "x64" ], @@ -2212,9 +2228,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.47.tgz", - "integrity": "sha512-Rxm5hYc0mGjwLh5sjlGmMygxAaV2gnsx7CNm2lsb47oyt5UQyPDZf3GP/ct8BEcwuikdqzsrrlIp8+kCSvMFNQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==", "cpu": [ "x64" ], @@ -2228,9 +2244,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-YakuVe+Gc87jjxazBL34hbr8RJpRuFBhun7NEqoChVDlH5FLhLXjAPHqZd990TVGVNkemourf817Z8u2fONS8w==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==", "cpu": [ "arm64" ], @@ -2244,37 +2260,37 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.47.tgz", - "integrity": "sha512-ak2GvTFQz3UAOw8cuQq8pWE+TNygQB6O47rMhvevvTzETh7VkHRFtRUwJynX5hwzFvQMP6G0az5JrBGuwaMwYQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.53.tgz", + "integrity": "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.7" + "@napi-rs/wasm-runtime": "^1.1.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", + "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-o5BpmBnXU+Cj+9+ndMcdKjhZlPb79dVPBZnWwMnI4RlNSSq5yOvFZqvfPYbyacvnW03Na4n5XXQAPhu3RydZ0w==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==", "cpu": [ "arm64" ], @@ -2287,26 +2303,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-FVOmfyYehNE92IfC9Kgs913UerDog2M1m+FADJypKz0gmRg3UyTt4o1cZMCAl7MiR89JpM9jegNO1nXuP1w1vw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-by/70F13IUE101Bat0oeH8miwWX5mhMFPk1yjCdxoTNHTyTdLgb0THNaebRM6AP7Kz+O3O2qx87sruYuF5UxHg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==", "cpu": [ "x64" ], @@ -2320,9 +2320,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", - "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "license": "MIT" }, "node_modules/@rollup/plugin-virtual": { @@ -2480,18 +2480,18 @@ } }, "node_modules/@shikijs/langs": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.17.0.tgz", - "integrity": "sha512-icmur2n5Ojb+HAiQu6NEcIIJ8oWDFGGEpiqSCe43539Sabpx7Y829WR3QuUW2zjTM4l6V8Sazgb3rrHO2orEAw==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.19.0.tgz", + "integrity": "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.17.0" + "@shikijs/types": "3.19.0" } }, "node_modules/@shikijs/langs/node_modules/@shikijs/types": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.17.0.tgz", - "integrity": "sha512-wjLVfutYWVUnxAjsWEob98xgyaGv0dTEnMZDruU5mRjVN7szcGOfgO+997W2yR6odp+1PtSBNeSITRRTfUzK/g==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz", + "integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -2499,18 +2499,18 @@ } }, "node_modules/@shikijs/themes": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.17.0.tgz", - "integrity": "sha512-/xEizMHLBmMHwtx4JuOkRf3zwhWD2bmG5BRr0IPjpcWpaq4C3mYEuTk/USAEglN0qPrTwEHwKVpSu/y2jhferA==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.19.0.tgz", + "integrity": "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.17.0" + "@shikijs/types": "3.19.0" } }, "node_modules/@shikijs/themes/node_modules/@shikijs/types": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.17.0.tgz", - "integrity": "sha512-wjLVfutYWVUnxAjsWEob98xgyaGv0dTEnMZDruU5mRjVN7szcGOfgO+997W2yR6odp+1PtSBNeSITRRTfUzK/g==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz", + "integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -3076,13 +3076,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", - "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/react": { @@ -3108,13 +3108,13 @@ "license": "MIT" }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", - "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "engines": { @@ -3129,13 +3129,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", - "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3" + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3146,9 +3146,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", - "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3162,14 +3162,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", - "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3186,9 +3186,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", - "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3199,20 +3199,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", - "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.3", - "@typescript-eslint/tsconfig-utils": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -3251,15 +3250,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", - "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3" + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3274,12 +3273,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", - "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3642,6 +3641,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", "peer": true, "bin": { "acorn": "bin/acorn" @@ -4629,9 +4629,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "61.1.12", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.1.12.tgz", - "integrity": "sha512-CGJTnltz7ovwOW33xYhvA4fMuriPZpR5OnJf09SV28iU2IUpJwMd6P7zvUK8Sl56u5YzO+1F9m46wpSs2dufEw==", + "version": "61.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.4.1.tgz", + "integrity": "sha512-3c1QW/bV25sJ1MsIvsvW+EtLtN6yZMduw7LVQNVt72y2/5BbV5Pg5b//TE5T48LRUxoEQGaZJejCmcj3wCxBzw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4658,23 +4658,23 @@ } }, "node_modules/eslint-plugin-react-x": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-2.3.1.tgz", - "integrity": "sha512-7zfi297NfkoEtqaz2W953gdK4J9nJD5okVhJVxgrcrP+9FVertkGqpbWtMZLpQuWJ216FncY8P6t1U+af8KNOA==", - "license": "MIT", - "dependencies": { - "@eslint-react/ast": "2.3.1", - "@eslint-react/core": "2.3.1", - "@eslint-react/eff": "2.3.1", - "@eslint-react/shared": "2.3.1", - "@eslint-react/var": "2.3.1", - "@typescript-eslint/scope-manager": "^8.46.2", - "@typescript-eslint/type-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "@typescript-eslint/utils": "^8.46.2", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-2.3.12.tgz", + "integrity": "sha512-G9ThX5LZQun3243JN/UchMbGPra9ZL1D7Wi4dwaIgqh26nRK8W6LBqRTJC+jlrmOanosg+flcxpUyFS/N+Ch7A==", + "license": "MIT", + "dependencies": { + "@eslint-react/ast": "2.3.12", + "@eslint-react/core": "2.3.12", + "@eslint-react/eff": "2.3.12", + "@eslint-react/shared": "2.3.12", + "@eslint-react/var": "2.3.12", + "@typescript-eslint/scope-manager": "^8.48.1", + "@typescript-eslint/type-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/utils": "^8.48.1", "compare-versions": "^6.1.1", "is-immutable-type": "^5.0.1", - "string-ts": "^2.2.1", + "string-ts": "^2.3.1", "ts-api-utils": "^2.1.0", "ts-pattern": "^5.9.0" }, @@ -4682,8 +4682,8 @@ "node": ">=20.19.0" }, "peerDependencies": { - "eslint": "^9.38.0", - "typescript": "^5.9.3" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/eslint-scope": { @@ -5991,13 +5991,13 @@ } }, "node_modules/lint-staged": { - "version": "16.2.6", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz", - "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", + "version": "16.2.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.7.tgz", + "integrity": "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.1", + "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", @@ -7627,9 +7627,9 @@ "license": "MIT" }, "node_modules/preact": { - "version": "10.27.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", - "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "version": "10.28.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.0.tgz", + "integrity": "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==", "license": "MIT", "peer": true, "funding": { @@ -7656,10 +7656,11 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -8141,13 +8142,13 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.47.tgz", - "integrity": "sha512-Mid74GckX1OeFAOYz9KuXeWYhq3xkXbMziYIC+ULVdUzPTG9y70OBSBQDQn9hQP8u/AfhuYw1R0BSg15nBI4Dg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", + "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.96.0", - "@rolldown/pluginutils": "1.0.0-beta.47" + "@oxc-project/types": "=0.101.0", + "@rolldown/pluginutils": "1.0.0-beta.53" }, "bin": { "rolldown": "bin/cli.mjs" @@ -8156,20 +8157,19 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-beta.47", - "@rolldown/binding-darwin-arm64": "1.0.0-beta.47", - "@rolldown/binding-darwin-x64": "1.0.0-beta.47", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.47", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.47", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.47", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.47", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.47", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.47", - "@rolldown/binding-openharmony-arm64": "1.0.0-beta.47", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.47", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.47", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.47", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.47" + "@rolldown/binding-android-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-x64": "1.0.0-beta.53", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" } }, "node_modules/run-parallel": { @@ -8235,58 +8235,58 @@ } }, "node_modules/shiki": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.17.0.tgz", - "integrity": "sha512-lUZfWsyW7czITYTdo/Tb6ZM4VfyXlzmKYBQBjTz+pBzPPkP08RgIt00Ls1Z50Cl3SfwJsue6WbJeF3UgqLVI9Q==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.19.0.tgz", + "integrity": "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA==", "license": "MIT", "dependencies": { - "@shikijs/core": "3.17.0", - "@shikijs/engine-javascript": "3.17.0", - "@shikijs/engine-oniguruma": "3.17.0", - "@shikijs/langs": "3.17.0", - "@shikijs/themes": "3.17.0", - "@shikijs/types": "3.17.0", + "@shikijs/core": "3.19.0", + "@shikijs/engine-javascript": "3.19.0", + "@shikijs/engine-oniguruma": "3.19.0", + "@shikijs/langs": "3.19.0", + "@shikijs/themes": "3.19.0", + "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "node_modules/shiki/node_modules/@shikijs/core": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.17.0.tgz", - "integrity": "sha512-/HjeOnbc62C+n33QFNFrAhUlIADKwfuoS50Ht0pxujxP4QjZAlFp5Q+OkDo531SCTzivx5T18khwyBdKoPdkuw==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.19.0.tgz", + "integrity": "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.17.0", + "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "node_modules/shiki/node_modules/@shikijs/engine-javascript": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.17.0.tgz", - "integrity": "sha512-WwF99xdP8KfuDrIbT4wxyypfhoIxMeeOCp1AiuvzzZ6JT5B3vIuoclL8xOuuydA6LBeeNXUF/XV5zlwwex1jlA==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.19.0.tgz", + "integrity": "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.17.0", + "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "node_modules/shiki/node_modules/@shikijs/engine-oniguruma": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.17.0.tgz", - "integrity": "sha512-flSbHZAiOZDNTrEbULY8DLWavu/TyVu/E7RChpLB4WvKX4iHMfj80C6Hi3TjIWaQtHOW0KC6kzMcuB5TO1hZ8Q==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.19.0.tgz", + "integrity": "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.17.0", + "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/shiki/node_modules/@shikijs/types": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.17.0.tgz", - "integrity": "sha512-wjLVfutYWVUnxAjsWEob98xgyaGv0dTEnMZDruU5mRjVN7szcGOfgO+997W2yR6odp+1PtSBNeSITRRTfUzK/g==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz", + "integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -8435,9 +8435,9 @@ } }, "node_modules/string-ts": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.2.1.tgz", - "integrity": "sha512-Q2u0gko67PLLhbte5HmPfdOjNvUKbKQM+mCNQae6jE91DmoFHY6HH9GcdqCeNx87DZ2KKjiFxmA0R/42OneGWw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.3.1.tgz", + "integrity": "sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==", "license": "MIT" }, "node_modules/string-width": { @@ -8762,6 +8762,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8951,9 +8997,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -9424,15 +9470,18 @@ } }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 0fad739d..7d9c2970 100644 --- a/package.json +++ b/package.json @@ -24,18 +24,18 @@ "doc-kit": "./bin/cli.mjs" }, "devDependencies": { - "@eslint/js": "^9.36.0", + "@eslint/js": "^9.39.1", "@reporters/github": "^1.11.0", "@types/mdast": "^4.0.4", - "@types/node": "^22.18.6", + "@types/node": "^24.10.1", "c8": "^10.1.3", - "eslint": "^9.36.0", + "eslint": "^9.39.1", "eslint-import-resolver-node": "^0.3.9", "eslint-plugin-import-x": "^4.16.1", - "eslint-plugin-jsdoc": "^61.1.12", + "eslint-plugin-jsdoc": "^61.4.1", "husky": "^9.1.7", - "lint-staged": "^16.2.6", - "prettier": "3.6.2" + "lint-staged": "^16.2.7", + "prettier": "3.7.4" }, "dependencies": { "@actions/core": "^1.11.1", @@ -50,7 +50,7 @@ "acorn": "^8.15.0", "commander": "^14.0.2", "dedent": "^1.7.0", - "eslint-plugin-react-x": "^2.3.1", + "eslint-plugin-react-x": "^2.3.12", "estree-util-to-js": "^2.0.0", "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", @@ -58,9 +58,9 @@ "globals": "^16.5.0", "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", - "lightningcss": "^1.30.1", + "lightningcss": "^1.30.2", "mdast-util-slice-markdown": "^2.0.1", - "preact": "^10.27.2", + "preact": "^10.28.0", "preact-render-to-string": "^6.6.3", "reading-time": "^1.5.0", "recma-jsx": "^1.0.1", @@ -71,9 +71,9 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-stringify": "^11.0.0", - "rolldown": "^1.0.0-beta.47", - "semver": "^7.7.2", - "shiki": "^3.17.0", + "rolldown": "^1.0.0-beta.53", + "semver": "^7.7.3", + "shiki": "^3.19.0", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-find-after": "^5.0.0", @@ -82,6 +82,6 @@ "unist-util-select": "^5.1.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3", - "yaml": "^2.8.1" + "yaml": "^2.8.2" } } diff --git a/src/generators.mjs b/src/generators.mjs index 2b7a3048..3d985b8f 100644 --- a/src/generators.mjs +++ b/src/generators.mjs @@ -2,6 +2,7 @@ import { allGenerators } from './generators/index.mjs'; import WorkerPool from './threading/index.mjs'; +import createParallelWorker from './threading/parallel.mjs'; /** * This method creates a system that allows you to register generators @@ -31,43 +32,51 @@ const createGenerator = input => { */ const cachedGenerators = { ast: Promise.resolve(input) }; - const threadPool = new WorkerPool(); - /** * Runs the Generator engine with the provided top-level input and the given generator options * * @param {GeneratorOptions} options The options for the generator runtime */ - const runGenerators = async ({ generators, threads, ...extra }) => { - // Note that this method is blocking, and will only execute one generator per-time - // but it ensures all dependencies are resolved, and that multiple bottom-level generators - // can reuse the already parsed content from the top-level/dependency generators + const runGenerators = async options => { + const { generators, threads } = options; + + // WorkerPool for chunk-level parallelization within generators + const chunkPool = new WorkerPool('./chunk-worker.mjs', threads); + + // Schedule all generators, allowing independent ones to run in parallel. + // Each generator awaits its own dependency internally, so generators + // with the same dependency (e.g. legacy-html and legacy-json both depend + // on metadata) will run concurrently once metadata resolves. for (const generatorName of generators) { + // Skip if already scheduled + if (generatorName in cachedGenerators) { + continue; + } + const { dependsOn, generate } = allGenerators[generatorName]; - // If the generator dependency has not yet been resolved, we resolve - // the dependency first before running the current generator - if (dependsOn && dependsOn in cachedGenerators === false) { - await runGenerators({ - ...extra, - threads, - generators: [dependsOn], - }); + // Ensure dependency is scheduled (but don't await its result yet) + if (dependsOn && !(dependsOn in cachedGenerators)) { + await runGenerators({ ...options, generators: [dependsOn] }); } - // Ensures that the dependency output gets resolved before we run the current - // generator with its dependency output as the input - const dependencyOutput = await cachedGenerators[dependsOn]; + // Create a ParallelWorker for this generator + const worker = createParallelWorker(generatorName, chunkPool, options); + + /** + * Schedule the generator - it awaits its dependency internally + * his allows multiple generators with the same dependency to run in parallel + */ + const scheduledGenerator = async () => { + const input = await cachedGenerators[dependsOn]; + + return generate(input, { ...options, worker }); + }; - // Adds the current generator execution Promise to the cache - cachedGenerators[generatorName] = - threads < 2 - ? generate(dependencyOutput, extra) // Run in main thread - : threadPool.run(generatorName, dependencyOutput, threads, extra); // Offload to worker thread + cachedGenerators[generatorName] = scheduledGenerator(); } // Returns the value of the last generator of the current pipeline - // Note that dependencies will be awaited (as shown on line 48) return cachedGenerators[generators[generators.length - 1]]; }; diff --git a/src/generators/api-links/__tests__/fixtures.test.mjs b/src/generators/api-links/__tests__/fixtures.test.mjs index 2284249a..fc6b204f 100644 --- a/src/generators/api-links/__tests__/fixtures.test.mjs +++ b/src/generators/api-links/__tests__/fixtures.test.mjs @@ -1,7 +1,10 @@ import { readdir } from 'node:fs/promises'; +import { cpus } from 'node:os'; import { basename, extname, join } from 'node:path'; import { describe, it } from 'node:test'; +import WorkerPool from '../../../threading/index.mjs'; +import createParallelWorker from '../../../threading/parallel.mjs'; import astJs from '../../ast-js/index.mjs'; import apiLinks from '../index.mjs'; @@ -16,8 +19,16 @@ describe('api links', () => { describe('should work correctly for all fixtures', () => { sourceFiles.forEach(sourceFile => { it(`${basename(sourceFile)}`, async t => { + const pool = new WorkerPool('../chunk-worker.mjs', cpus().length); + + const worker = createParallelWorker('ast-js', pool, { + threads: 1, + chunkSize: 10, + }); + const astJsResult = await astJs.generate(undefined, { input: [sourceFile], + worker, }); const actualOutput = await apiLinks.generate(astJsResult, { diff --git a/src/generators/ast-js/index.mjs b/src/generators/ast-js/index.mjs index 15497900..7967a5b3 100644 --- a/src/generators/ast-js/index.mjs +++ b/src/generators/ast-js/index.mjs @@ -1,3 +1,7 @@ +import { extname } from 'node:path'; + +import { globSync } from 'glob'; + import createJsLoader from '../../loaders/javascript.mjs'; import createJsParser from '../../parsers/javascript.mjs'; @@ -23,21 +27,38 @@ export default { dependsOn: 'metadata', /** - * @param {Input} _ + * Process a chunk of JavaScript files in a worker thread. + * @param {unknown} _ + * @param {number[]} itemIndices * @param {Partial} options */ - async generate(_, options) { + async processChunk(_, itemIndices, { input }) { const { loadFiles } = createJsLoader(); + const { parseJsSource } = createJsParser(); - // Load all of the Javascript sources into memory - const sourceFiles = loadFiles(options.input ?? []); + const results = []; - const { parseJsSources } = createJsParser(); + for (const idx of itemIndices) { + const [file] = loadFiles(input[idx]); - // Parse the Javascript sources into ASTs - const parsedJsFiles = await parseJsSources(sourceFiles); + const parsedFile = await parseJsSource(file); + + results.push(parsedFile); + } + + return results; + }, + + /** + * @param {Input} _ + * @param {Partial} options + */ + async generate(_, { input = [], worker }) { + const sourceFiles = globSync(input).filter( + filePath => extname(filePath) === '.js' + ); - // Return the ASTs so they can be used in another generator - return parsedJsFiles; + // Parse the Javascript sources into ASTs in parallel using worker threads + return worker.map(sourceFiles, _, { input: sourceFiles }); }, }; diff --git a/src/generators/jsx-ast/index.mjs b/src/generators/jsx-ast/index.mjs index 3e8e9a3c..629646ea 100644 --- a/src/generators/jsx-ast/index.mjs +++ b/src/generators/jsx-ast/index.mjs @@ -4,67 +4,74 @@ import buildContent from './utils/buildContent.mjs'; import { groupNodesByModule } from '../../utils/generators.mjs'; import { getRemarkRecma } from '../../utils/remark.mjs'; -/** - * Generator for converting MDAST to JSX AST. - * - * @typedef {Array} Input - * @type {GeneratorMetadata} - */ - /** * Sorts entries by OVERRIDDEN_POSITIONS and then heading name. * @param {Array} entries */ const getSortedHeadNodes = entries => { - return entries - .filter(node => node.heading.depth === 1) - .sort((a, b) => { - const ai = OVERRIDDEN_POSITIONS.indexOf(a.api); - const bi = OVERRIDDEN_POSITIONS.indexOf(b.api); - - if (ai !== -1 && bi !== -1) { - return ai - bi; - } - - if (ai !== -1) { - return -1; - } - - if (bi !== -1) { - return 1; - } - - return a.heading.data.name.localeCompare(b.heading.data.name); - }); + /** + * Sorts entries by OVERRIDDEN_POSITIONS and then heading name. + * @param {ApiDocMetadataEntry} a + * @param {ApiDocMetadataEntry} b + * @returns {number} + */ + const headingSortFn = (a, b) => { + const ai = OVERRIDDEN_POSITIONS.indexOf(a.api); + const bi = OVERRIDDEN_POSITIONS.indexOf(b.api); + + if (ai !== -1 && bi !== -1) { + return ai - bi; + } + + if (ai !== -1) { + return -1; + } + + if (bi !== -1) { + return 1; + } + + return a.heading.data.name.localeCompare(b.heading.data.name); + }; + + return entries.filter(node => node.heading.depth === 1).sort(headingSortFn); }; +/** + * Generator for converting MDAST to JSX AST. + * + * @typedef {Array} Input + * @type {GeneratorMetadata} + */ export default { name: 'jsx-ast', + version: '1.0.0', + description: 'Generates JSX AST from the input MDAST', + dependsOn: 'metadata', /** - * Generates a JSX AST - * - * @param {Input} entries + * Process a chunk of items in a worker thread. + * @param {Input} fullInput + * @param {number[]} itemIndices * @param {Partial} options - * @returns {Promise>} Array of generated content */ - async generate(entries, { index, releases, version }) { + async processChunk(fullInput, itemIndices, { index, releases, version }) { const remarkRecma = getRemarkRecma(); - const groupedModules = groupNodesByModule(entries); - const headNodes = getSortedHeadNodes(entries); + const groupedModules = groupNodesByModule(fullInput); + const headNodes = getSortedHeadNodes(fullInput); - // Generate table of contents const docPages = index ? index.map(({ section, api }) => [section, `${api}.html`]) : headNodes.map(node => [node.heading.data.name, `${node.api}.html`]); - // Process each head node and build content const results = []; - for (const entry of headNodes) { + for (const idx of itemIndices) { + const entry = headNodes[idx]; + const sideBarProps = buildSideBarProps( entry, releases, @@ -72,16 +79,29 @@ export default { docPages ); - results.push( - await buildContent( - groupedModules.get(entry.api), - entry, - sideBarProps, - remarkRecma - ) + const content = await buildContent( + groupedModules.get(entry.api), + entry, + sideBarProps, + remarkRecma ); + + results.push(content); } return results; }, + + /** + * Generates a JSX AST + * + * @param {Input} entries + * @param {Partial} options + * @returns {Promise>} Array of generated content + */ + async generate(entries, { index, releases, version, worker }) { + const headNodes = entries.filter(node => node.heading.depth === 1); + + return worker.map(headNodes, entries, { index, releases, version }); + }, }; diff --git a/src/generators/legacy-html-all/index.mjs b/src/generators/legacy-html-all/index.mjs index ad6584d7..15d448a1 100644 --- a/src/generators/legacy-html-all/index.mjs +++ b/src/generators/legacy-html-all/index.mjs @@ -5,7 +5,7 @@ import { join, resolve } from 'node:path'; import HTMLMinifier from '@minify-html/node'; -import { getRemarkRehypeWithShiki } from '../../utils/remark.mjs'; +import { getRemarkRehype } from '../../utils/remark.mjs'; import dropdowns from '../legacy-html/utils/buildDropdowns.mjs'; import tableOfContents from '../legacy-html/utils/tableOfContents.mjs'; @@ -49,7 +49,7 @@ export default { const inputWithoutIndex = input.filter(entry => entry.api !== 'index'); // Gets a Remark Processor that parses Markdown to minified HTML - const remarkWithRehype = getRemarkRehypeWithShiki(); + const remarkWithRehype = getRemarkRehype(); // Current directory path relative to the `index.mjs` file // from the `legacy-html` generator, as all the assets are there diff --git a/src/generators/legacy-html/index.mjs b/src/generators/legacy-html/index.mjs index 123cee57..525c7978 100644 --- a/src/generators/legacy-html/index.mjs +++ b/src/generators/legacy-html/index.mjs @@ -12,6 +12,13 @@ import tableOfContents from './utils/tableOfContents.mjs'; import { groupNodesByModule } from '../../utils/generators.mjs'; import { getRemarkRehypeWithShiki } from '../../utils/remark.mjs'; +/** + * Creates a heading object with the given name. + * @param {string} name - The name of the heading + * @returns {HeadingMetadataEntry} The heading object + */ +const getHeading = name => ({ data: { depth: 1, name } }); + /** * @typedef {{ * api: string; @@ -44,53 +51,31 @@ export default { dependsOn: 'metadata', /** - * Generates the legacy version of the API docs in HTML - * @param {Input} input + * Process a chunk of items in a worker thread. + * @param {Input} fullInput + * @param {number[]} itemIndices * @param {Partial} options */ - async generate(input, { index, releases, version, output }) { - // This array holds all the generated values for each module - const generatedValues = []; - - // Gets a Remark Processor that parses Markdown to minified HTML + async processChunk( + fullInput, + itemIndices, + { releases, version, output, apiTemplate, parsedSideNav } + ) { const remarkRehypeProcessor = getRemarkRehypeWithShiki(); + const groupedModules = groupNodesByModule(fullInput); - const groupedModules = groupNodesByModule(input); - - // Current directory path relative to the `index.mjs` file - const baseDir = import.meta.dirname; - - // Reads the API template.html file to be used as a base for the HTML files - const apiTemplate = await readFile(join(baseDir, 'template.html'), 'utf-8'); - - // Gets the first nodes of each module, which is considered the "head" of the module - // and sorts them alphabetically by the "name" property of the heading - const headNodes = input + const headNodes = fullInput .filter(node => node.heading.depth === 1) .sort((a, b) => a.heading.data.name.localeCompare(b.heading.data.name)); - const indexOfFiles = index - ? index.map(entry => ({ - api: entry.api, - heading: { data: { depth: 1, name: entry.section } }, - })) - : headNodes; - - // Generates the global Table of Contents (Sidebar Navigation) - const parsedSideNav = remarkRehypeProcessor.processSync( - tableOfContents(indexOfFiles, { - maxDepth: 1, - parser: tableOfContents.parseNavigationNode, - }) - ); - /** - * Replaces the aggregated data from a node within a template - * - * @param {TemplateValues} values The values to be replaced in the template + * Replaces the template values in the API template with the given values. + * @param {TemplateValues} values - The values to replace the template values with + * @returns {string} The replaced template values */ const replaceTemplateValues = values => { const { api, added, section, version, toc, nav, content } = values; + return apiTemplate .replace('__ID__', api) .replace(/__FILENAME__/g, api) @@ -105,24 +90,17 @@ export default { .replace('__EDIT_ON_GITHUB__', dropdowns.buildGitHub(api)); }; - /** - * Processes each module node to generate the HTML content - * - * @param {ApiDocMetadataEntry} head The name of the module to be generated - * @param {string} template The template to be used to generate the HTML content - */ - const processModuleNodes = head => { + const results = []; + + for (const idx of itemIndices) { + const head = headNodes[idx]; const nodes = groupedModules.get(head.api); - // Replaces the entry corresponding to the current module - // as an active entry within the side navigation const activeSideNav = String(parsedSideNav).replace( `class="nav-${head.api}`, `class="nav-${head.api} active` ); - // Generates the Table of Contents for the current module, which is appended - // to the top of the page and also to a dropdown const parsedToC = remarkRehypeProcessor.processSync( tableOfContents(nodes, { maxDepth: 4, @@ -130,15 +108,12 @@ export default { }) ); - // Builds the content of the module, including all sections, - // stability indexes, and content for the current file const parsedContent = buildContent( headNodes, nodes, remarkRehypeProcessor ); - // In case there's no Heading, we make a little capitalization of the filename const apiAsHeading = head.api.charAt(0).toUpperCase() + head.api.slice(1); const generatedTemplate = { @@ -151,24 +126,56 @@ export default { content: parsedContent, }; - // Adds the generated template to the list of generated values - generatedValues.push(generatedTemplate); - - // Replaces all the values within the template for the current doc - return replaceTemplateValues(generatedTemplate); - }; - - for (const node of headNodes) { - const result = processModuleNodes(node); - if (output) { // We minify the html result to reduce the file size and keep it "clean" + const result = replaceTemplateValues(generatedTemplate); const minified = HTMLMinifier.minify(Buffer.from(result), {}); - await writeFile(join(output, `${node.api}.html`), minified); + await writeFile(join(output, `${head.api}.html`), minified); } + + results.push(generatedTemplate); } + return results; + }, + + /** + * Generates the legacy version of the API docs in HTML + * @param {Input} input + * @param {Partial} options + */ + async generate(input, { index, releases, version, output, worker }) { + const remarkRehypeProcessor = getRemarkRehypeWithShiki(); + + const baseDir = import.meta.dirname; + + const apiTemplate = await readFile(join(baseDir, 'template.html'), 'utf-8'); + + const headNodes = input + .filter(node => node.heading.depth === 1) + .sort((a, b) => a.heading.data.name.localeCompare(b.heading.data.name)); + + const indexOfFiles = index + ? index.map(({ api, section }) => ({ api, heading: getHeading(section) })) + : headNodes; + + const parsedSideNav = remarkRehypeProcessor.processSync( + tableOfContents(indexOfFiles, { + maxDepth: 1, + parser: tableOfContents.parseNavigationNode, + }) + ); + + const generatedValues = await worker.map(headNodes, input, { + index, + releases, + version, + output, + apiTemplate, + parsedSideNav: String(parsedSideNav), + }); + if (output) { // Define the source folder for API docs assets const srcAssets = join(baseDir, 'assets'); diff --git a/src/generators/legacy-json/index.mjs b/src/generators/legacy-json/index.mjs index 1883b286..9d468760 100644 --- a/src/generators/legacy-json/index.mjs +++ b/src/generators/legacy-json/index.mjs @@ -29,51 +29,46 @@ export default { dependsOn: 'metadata', /** - * Generates a legacy JSON file. - * - * @param {Input} input + * Process a chunk of items in a worker thread. + * @param {Input} fullInput + * @param {number[]} itemIndices * @param {Partial} options */ - async generate(input, { output }) { + async processChunk(fullInput, itemIndices, { output }) { const buildSection = createSectionBuilder(); + const groupedModules = groupNodesByModule(fullInput); - // This array holds all the generated values for each module - const generatedValues = []; + const headNodes = fullInput.filter(node => node.heading.depth === 1); - const groupedModules = groupNodesByModule(input); + const results = []; - // Gets the first nodes of each module, which is considered the "head" - const headNodes = input.filter(node => node.heading.depth === 1); - - /** - * @param {ApiDocMetadataEntry} head - * @returns {import('./types.d.ts').ModuleSection} - */ - const processModuleNodes = head => { + for (const idx of itemIndices) { + const head = headNodes[idx]; const nodes = groupedModules.get(head.api); - const section = buildSection(head, nodes); - generatedValues.push(section); + if (output) { + await writeFile( + join(output, `${head.api}.json`), + JSON.stringify(section) + ); + } - return section; - }; + results.push(section); + } - await Promise.all( - headNodes.map(async node => { - // Get the json for the node's section - const section = processModuleNodes(node); + return results; + }, - // Write it to the output file - if (output) { - await writeFile( - join(output, `${node.api}.json`), - JSON.stringify(section) - ); - } - }) - ); + /** + * Generates a legacy JSON file. + * + * @param {Input} input + * @param {Partial} options + */ + async generate(input, { output, worker }) { + const headNodes = input.filter(node => node.heading.depth === 1); - return generatedValues; + return worker.map(headNodes, input, { output }); }, }; diff --git a/src/generators/metadata/index.mjs b/src/generators/metadata/index.mjs index 37a7d0d0..750dd82c 100644 --- a/src/generators/metadata/index.mjs +++ b/src/generators/metadata/index.mjs @@ -18,12 +18,32 @@ export default { dependsOn: 'ast', + /** + * Process a chunk of API doc files in a worker thread. + * Called by chunk-worker.mjs for parallel processing. + * + * @param {Input} fullInput - Full input array + * @param {number[]} itemIndices - Indices of items to process + * @param {Partial} options + */ + async processChunk(fullInput, itemIndices, { typeMap }) { + const results = []; + + for (const idx of itemIndices) { + results.push(...parseApiDoc(fullInput[idx], typeMap)); + } + + return results; + }, + /** * @param {Input} inputs * @param {GeneratorOptions} options * @returns {Promise>} */ - async generate(inputs, { typeMap }) { - return inputs.flatMap(input => parseApiDoc(input, typeMap)); + async generate(inputs, { typeMap, worker }) { + const results = await worker.map(inputs, inputs, { typeMap }); + + return results.flat(); }, }; diff --git a/src/generators/orama-db/index.mjs b/src/generators/orama-db/index.mjs index 2b91375a..6d6b047a 100644 --- a/src/generators/orama-db/index.mjs +++ b/src/generators/orama-db/index.mjs @@ -65,12 +65,14 @@ export default { } const db = create({ schema: SCHEMA }); + const apiGroups = groupNodesByModule(input); // Process all API groups and flatten into a single document array const documents = Array.from(apiGroups.values()).flatMap(headings => headings.map((entry, index) => { const hierarchicalTitle = buildHierarchicalTitle(headings, index); + const paragraph = entry.content.children.find( child => child.type === 'paragraph' ); diff --git a/src/generators/types.d.ts b/src/generators/types.d.ts index df72fce3..201d8e39 100644 --- a/src/generators/types.d.ts +++ b/src/generators/types.d.ts @@ -1,4 +1,3 @@ -import type { SemVer } from 'semver'; import type { ApiDocReleaseEntry } from '../types'; import type { publicGenerators } from './index.mjs'; @@ -7,6 +6,24 @@ declare global { // to be type complete and runtime friendly within `runGenerators` export type AvailableGenerators = typeof publicGenerators; + // ParallelWorker interface for item-level parallelization using real worker threads + export interface ParallelWorker { + /** + * Process items in parallel using real worker threads. + * Items are split into chunks, each chunk processed by a separate worker. + * + * @param items - Items to process (used to determine indices) + * @param fullInput - Full input data for context rebuilding in workers + * @param opts - Additional options to pass to workers + * @returns Results in same order as input items + */ + map( + items: T[], + fullInput: unknown, + opts?: Record + ): Promise; + } + // This is the runtime config passed to the API doc generators export interface GeneratorOptions { // The path to the input source files. This parameter accepts globs and can @@ -43,8 +60,14 @@ declare global { // The number of threads the process is allowed to use threads: number; + // Number of items to process per worker thread + chunkSize: number; + // The type map typeMap: Record; + + // Parallel worker instance for generators to parallelize work on individual items + worker: ParallelWorker; } export interface GeneratorMetadata { @@ -91,5 +114,23 @@ declare global { * Hence you can combine different generators to achieve different outputs. */ generate: (input: I, options: Partial) => Promise; + + /** + * Optional method for chunk-level parallelization using real worker threads. + * Called by chunk-worker.mjs when processing items in parallel. + * + * Generators that implement this method can have their work distributed + * across multiple worker threads for true parallel processing. + * + * @param fullInput - Full input data (for rebuilding context in workers) + * @param itemIndices - Array of indices of items to process + * @param options - Generator options (without worker, which isn't serializable) + * @returns Array of results for the processed items + */ + processChunk?: ( + fullInput: I, + itemIndices: number[], + options: Partial> + ) => Promise; } } diff --git a/src/generators/web/utils/bundle.mjs b/src/generators/web/utils/bundle.mjs index ce492fc5..441b184d 100644 --- a/src/generators/web/utils/bundle.mjs +++ b/src/generators/web/utils/bundle.mjs @@ -1,9 +1,18 @@ +import { join } from 'node:path'; + import virtual from '@rollup/plugin-virtual'; import { build } from 'rolldown'; import cssLoader from './css.mjs'; import staticData from './data.mjs'; +// Resolve node_modules relative to this package (doc-kit), not cwd. +// This ensures modules are found when running from external directories. +const DOC_KIT_NODE_MODULES = join( + import.meta.dirname, + '../../../../node_modules' +); + /** * Asynchronously bundles JavaScript source code (and its CSS imports), * targeting either browser (client) or server (Node.js) environments. @@ -71,14 +80,16 @@ export default async function bundleCode(codeMap, { server = false } = {}) { jsx: 'react-jsx', }, - // Module resolution aliases. - // This tells the bundler to use `preact/compat` wherever `react` or `react-dom` is imported. - // Allows you to write React-style code but ship much smaller Preact bundles. + // Module resolution configuration. resolve: { - alias: { - react: 'preact/compat', - 'react-dom': 'preact/compat', - }, + // Alias react imports to preact/compat for smaller bundle sizes. + // Explicit jsx-runtime aliases are required for the automatic JSX transform. + alias: { react: 'preact/compat' }, + + // Tell the bundler where to find node_modules. + // This ensures packages are found when running doc-kit from external directories + // (e.g., running from the node repository via tools/doc/node_modules/.bin/doc-kit). + modules: [DOC_KIT_NODE_MODULES, 'node_modules'], }, // Array of plugins to apply during the build. diff --git a/src/generators/web/utils/processing.mjs b/src/generators/web/utils/processing.mjs index eeb5c091..2c61211f 100644 --- a/src/generators/web/utils/processing.mjs +++ b/src/generators/web/utils/processing.mjs @@ -49,8 +49,9 @@ export async function executeServerCode(serverCodeMap, requireFn) { * @param {Array} entries - The JSX AST entry to process. * @param {string} template - The HTML template string that serves as the base for the output page. * @param {ReturnType} astBuilders - The AST generators - * @param {version} version - The version to generator the documentation for * @param {ReturnType} requireFn - A Node.js `require` function. + * @param {Object} options - Processing options + * @param {Object} options.version - Version info */ export async function processJSXEntries( entries, diff --git a/src/logger/__tests__/transports/console.test.mjs b/src/logger/__tests__/transports/console.test.mjs index 71d1268b..cf346707 100644 --- a/src/logger/__tests__/transports/console.test.mjs +++ b/src/logger/__tests__/transports/console.test.mjs @@ -6,6 +6,8 @@ import console from '../../transports/console.mjs'; describe('console', () => { it('should print debug messages', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -33,6 +35,8 @@ describe('console', () => { }); it('should print info messages', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -59,6 +63,8 @@ describe('console', () => { }); it('should print error messages ', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -86,6 +92,8 @@ describe('console', () => { }); it('should print fatal messages', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -113,6 +121,8 @@ describe('console', () => { }); it('should print messages with file', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -151,6 +161,8 @@ describe('console', () => { }); it('should print child logger name', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); diff --git a/src/logger/__tests__/transports/github.test.mjs b/src/logger/__tests__/transports/github.test.mjs index 44674de8..ed7d7dfa 100644 --- a/src/logger/__tests__/transports/github.test.mjs +++ b/src/logger/__tests__/transports/github.test.mjs @@ -6,6 +6,8 @@ import github from '../../transports/github.mjs'; describe('github', () => { it('should print debug messages', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -31,6 +33,8 @@ describe('github', () => { }); it('should print info messages', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -55,6 +59,8 @@ describe('github', () => { }); it('should print error messages ', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -80,6 +86,8 @@ describe('github', () => { }); it('should print fatal messages', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -105,6 +113,8 @@ describe('github', () => { }); it('should print messages with file', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); @@ -139,6 +149,8 @@ describe('github', () => { }); it('should print child logger name', t => { + process.env.FORCE_COLOR = '1'; + t.mock.timers.enable({ apis: ['Date'] }); const fn = t.mock.method(process.stdout, 'write'); diff --git a/src/parsers/javascript.mjs b/src/parsers/javascript.mjs index e68e9d9f..d5a7714d 100644 --- a/src/parsers/javascript.mjs +++ b/src/parsers/javascript.mjs @@ -29,10 +29,7 @@ const createParser = () => { locations: true, }); - return { - ...res, - path: resolvedSourceFile.path, - }; + return { ...res, path: resolvedSourceFile.path }; }; /** diff --git a/src/threading/__tests__/WorkerPool.test.mjs b/src/threading/__tests__/WorkerPool.test.mjs new file mode 100644 index 00000000..c878fe48 --- /dev/null +++ b/src/threading/__tests__/WorkerPool.test.mjs @@ -0,0 +1,90 @@ +import { deepStrictEqual, ok, strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import WorkerPool from '../index.mjs'; + +describe('WorkerPool', () => { + // Use relative path from WorkerPool's location (src/threading/) + const workerPath = './chunk-worker.mjs'; + + it('should create a worker pool with specified thread count', () => { + const pool = new WorkerPool(workerPath, 4); + + strictEqual(pool.threads, 4); + strictEqual(pool.getActiveThreadCount(), 0); + }); + + it('should initialize with zero active threads', () => { + const pool = new WorkerPool(workerPath, 2); + + strictEqual(pool.getActiveThreadCount(), 0); + }); + + it('should change active thread count atomically', () => { + const pool = new WorkerPool(workerPath, 2); + + pool.changeActiveThreadCount(1); + strictEqual(pool.getActiveThreadCount(), 1); + + pool.changeActiveThreadCount(2); + strictEqual(pool.getActiveThreadCount(), 3); + + pool.changeActiveThreadCount(-1); + strictEqual(pool.getActiveThreadCount(), 2); + }); + + it('should queue tasks when thread limit is reached', async () => { + const pool = new WorkerPool(workerPath, 1); + + const task1 = pool.run({ + generatorName: 'ast-js', + fullInput: [], + itemIndices: [], + options: {}, + }); + + const task2 = pool.run({ + generatorName: 'ast-js', + fullInput: [], + itemIndices: [], + options: {}, + }); + + const results = await Promise.all([task1, task2]); + + ok(Array.isArray(results)); + strictEqual(results.length, 2); + }); + + it('should run multiple tasks in parallel with runAll', async () => { + const pool = new WorkerPool(workerPath, 2); + + const tasks = [ + { + generatorName: 'ast-js', + fullInput: [], + itemIndices: [], + options: {}, + }, + { + generatorName: 'ast-js', + fullInput: [], + itemIndices: [], + options: {}, + }, + ]; + + const results = await pool.runAll(tasks); + + ok(Array.isArray(results)); + strictEqual(results.length, 2); + }); + + it('should handle empty task array', async () => { + const pool = new WorkerPool(workerPath, 2); + + const results = await pool.runAll([]); + + deepStrictEqual(results, []); + }); +}); diff --git a/src/threading/__tests__/parallel.test.mjs b/src/threading/__tests__/parallel.test.mjs new file mode 100644 index 00000000..3090e234 --- /dev/null +++ b/src/threading/__tests__/parallel.test.mjs @@ -0,0 +1,85 @@ +import { deepStrictEqual, ok, strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import WorkerPool from '../index.mjs'; +import createParallelWorker from '../parallel.mjs'; + +describe('createParallelWorker', () => { + // Use relative path from WorkerPool's location (src/threading/) + const workerPath = './chunk-worker.mjs'; + + it('should create a ParallelWorker with map method', () => { + const pool = new WorkerPool(workerPath, 2); + + const worker = createParallelWorker('metadata', pool, { threads: 2 }); + + ok(worker); + strictEqual(typeof worker.map, 'function'); + }); + + it('should use main thread for single-threaded execution', async () => { + const pool = new WorkerPool(workerPath, 1); + + const worker = createParallelWorker('ast-js', pool, { threads: 1 }); + const items = []; + const results = await worker.map(items, items, {}); + + ok(Array.isArray(results)); + strictEqual(results.length, 0); + }); + + it('should use main thread for small item counts', async () => { + const pool = new WorkerPool(workerPath, 4); + + const worker = createParallelWorker('ast-js', pool, { threads: 4 }); + const items = []; + const results = await worker.map(items, items, {}); + + ok(Array.isArray(results)); + strictEqual(results.length, 0); + }); + + it('should chunk items for parallel processing', async () => { + const pool = new WorkerPool(workerPath, 2); + + const worker = createParallelWorker('ast-js', pool, { threads: 2 }); + const items = []; + + const results = await worker.map(items, items, {}); + + strictEqual(results.length, 0); + ok(Array.isArray(results)); + }); + + it('should pass extra options to worker', async () => { + const pool = new WorkerPool(workerPath, 1); + + const worker = createParallelWorker('ast-js', pool, { threads: 1 }); + const extra = { gitRef: 'main', customOption: 'value' }; + const items = []; + + const results = await worker.map(items, items, extra); + + ok(Array.isArray(results)); + }); + + it('should serialize and deserialize data correctly', async () => { + const pool = new WorkerPool(workerPath, 2); + + const worker = createParallelWorker('ast-js', pool, { threads: 2 }); + const items = []; + + const results = await worker.map(items, items, {}); + + ok(Array.isArray(results)); + }); + + it('should handle empty items array', async () => { + const pool = new WorkerPool(workerPath, 2); + + const worker = createParallelWorker('ast-js', pool, { threads: 2 }); + const results = await worker.map([], [], {}); + + deepStrictEqual(results, []); + }); +}); diff --git a/src/threading/chunk-worker.mjs b/src/threading/chunk-worker.mjs new file mode 100644 index 00000000..31112109 --- /dev/null +++ b/src/threading/chunk-worker.mjs @@ -0,0 +1,13 @@ +import { parentPort, workerData } from 'node:worker_threads'; + +import { allGenerators } from '../generators/index.mjs'; + +const { generatorName, fullInput, itemIndices, options } = workerData; + +const generator = allGenerators[generatorName]; + +// Generators must implement processChunk for item-level parallelization +generator + .processChunk(fullInput, itemIndices, options) + .then(result => parentPort.postMessage(result)) + .catch(error => parentPort.postMessage({ error: error.message })); diff --git a/src/threading/index.mjs b/src/threading/index.mjs index dc7872a6..a6c19fd2 100644 --- a/src/threading/index.mjs +++ b/src/threading/index.mjs @@ -11,6 +11,19 @@ export default class WorkerPool { /** @private {Array} - Queue of pending tasks */ queue = []; + /** + * @param {string | URL} workerScript - Path to the worker script (relative to this file or absolute URL) + * @param {number} threads - Maximum number of concurrent worker threads + */ + constructor(workerScript = './generator-worker.mjs', threads = 1) { + this.workerScript = + workerScript instanceof URL + ? workerScript + : new URL(workerScript, import.meta.url); + + this.threads = threads; + } + /** * Gets the current active thread count. * @returns {number} The current active thread count. @@ -28,48 +41,40 @@ export default class WorkerPool { } /** - * Runs a generator within a worker thread. - * @param {string} name - The name of the generator to execute - * @param {any} dependencyOutput - Input data for the generator - * @param {number} threads - Maximum number of threads to run concurrently - * @param {Object} extra - Additional options for the generator - * @returns {Promise} Resolves with the generator result, or rejects with an error + * Runs a task in a worker thread with the given data. + * @param {Object} workerData - Data to pass to the worker thread + * @returns {Promise} Resolves with the worker result, or rejects with an error */ - run(name, dependencyOutput, threads, extra) { + run(workerData) { return new Promise((resolve, reject) => { /** - * Function to run the generator in a worker thread + * Runs the worker thread and handles the result or error. + * @private */ const run = () => { this.changeActiveThreadCount(1); - // Create and start the worker thread - const worker = new Worker(new URL('./worker.mjs', import.meta.url), { - workerData: { name, dependencyOutput, extra }, - }); + const worker = new Worker(this.workerScript, { workerData }); - // Handle worker thread messages (result or error) worker.on('message', result => { this.changeActiveThreadCount(-1); - this.processQueue(threads); + this.processQueue(); if (result?.error) { - reject(result.error); + reject(new Error(result.error)); } else { resolve(result); } }); - // Handle worker thread errors worker.on('error', err => { this.changeActiveThreadCount(-1); - this.processQueue(threads); + this.processQueue(); reject(err); }); }; - // If the active thread count exceeds the limit, add the task to the queue - if (this.getActiveThreadCount() >= threads) { + if (this.getActiveThreadCount() >= this.threads) { this.queue.push(run); } else { run(); @@ -78,14 +83,23 @@ export default class WorkerPool { } /** - * Process the worker thread queue to start the next available task - * when there is room for more threads. - * @param {number} threads - Maximum number of threads to run concurrently + * Run multiple tasks in parallel, distributing across worker threads. + * @template T, R + * @param {T[]} tasks - Array of task data to process + * @returns {Promise} Results in same order as input tasks + */ + async runAll(tasks) { + return Promise.all(tasks.map(task => this.run(task))); + } + + /** + * Process the worker thread queue to start the next available task. * @private */ - processQueue(threads) { - if (this.queue.length > 0 && this.getActiveThreadCount() < threads) { + processQueue() { + if (this.queue.length > 0 && this.getActiveThreadCount() < this.threads) { const next = this.queue.shift(); + if (next) { next(); } diff --git a/src/threading/parallel.mjs b/src/threading/parallel.mjs new file mode 100644 index 00000000..25333cb8 --- /dev/null +++ b/src/threading/parallel.mjs @@ -0,0 +1,110 @@ +'use strict'; + +import { allGenerators } from '../generators/index.mjs'; + +/** + * Creates a ParallelWorker that uses real Node.js Worker threads + * for parallel processing of items. + * + * @param {string} generatorName - Name of the generator (for chunk processing) + * @param {import('./index.mjs').default} pool - WorkerPool instance for spawning workers + * @param {object} options - Generator options + * @returns {ParallelWorker} + */ +export default function createParallelWorker(generatorName, pool, options) { + const { threads, chunkSize } = options; + + const generator = allGenerators[generatorName]; + + /** + * Splits items into chunks of specified size. + * @param {number} count - Number of items + * @param {number} size - Items per chunk + * @returns {number[][]} Array of index arrays + */ + const createIndexChunks = (count, size) => { + const chunks = []; + + for (let i = 0; i < count; i += size) { + const end = Math.min(i + size, count); + + const chunk = []; + + for (let j = i; j < end; j++) { + chunk.push(j); + } + + chunks.push(chunk); + } + + return chunks; + }; + + /** + * Strips non-serializable properties from options for worker transfer + * @param {object} extra - Extra options to merge + */ + const serializeOptions = extra => { + const serialized = { ...options, ...extra }; + + delete serialized.worker; + + return serialized; + }; + + return { + /** + * Process items in parallel using real worker threads. + * Items are split into chunks, each chunk processed by a separate worker. + * + * @template T, R + * @param {T[]} items - Items to process (must be serializable) + * @param {T[]} fullInput - Full input data for context rebuilding in workers + * @param {object} extra - Generator-specific context (e.g. apiTemplate, parsedSideNav) + * @returns {Promise} - Results in same order as input items + */ + async map(items, fullInput, extra) { + const itemCount = items.length; + + if (itemCount === 0) { + return []; + } + + if (!generator.processChunk) { + throw new Error( + `Generator "${generatorName}" does not support chunk processing` + ); + } + + // For single thread or small workloads - run in main thread + if (threads <= 1 || itemCount <= 2) { + const indices = []; + + for (let i = 0; i < itemCount; i++) { + indices.push(i); + } + + return generator.processChunk(fullInput, indices, { + ...options, + ...extra, + }); + } + + // Divide items into chunks based on chunkSize + const indexChunks = createIndexChunks(itemCount, chunkSize); + + // Process chunks in parallel using worker threads + const chunkResults = await pool.runAll( + indexChunks.map(indices => ({ + generatorName, + fullInput, + itemIndices: indices, + options: serializeOptions(extra), + })) + ); + + // Flatten results + return chunkResults.flat(); + }, + }; +} diff --git a/src/threading/worker.mjs b/src/threading/worker.mjs deleted file mode 100644 index ab107eac..00000000 --- a/src/threading/worker.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { parentPort, workerData } from 'node:worker_threads'; - -import { allGenerators } from '../generators/index.mjs'; - -const { name, dependencyOutput, extra } = workerData; -const generator = allGenerators[name]; - -// Execute the generator and send the result or error back to the parent thread -generator - .generate(dependencyOutput, extra) - .then(result => parentPort.postMessage(result)) - .catch(error => parentPort.postMessage({ error })); diff --git a/src/types.d.ts b/src/types.d.ts index ca1bfaa6..3e311761 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -42,8 +42,10 @@ declare global { slug: string; } - export interface HeadingMetadataParent - extends NodeWithData {} + export interface HeadingMetadataParent extends NodeWithData< + Heading, + HeadingMetadataEntry + > {} export interface ApiDocMetadataChange { // The Node.js version or versions where said change was introduced simultaneously