From 4435b8c990ba97c3dd5aaf126da28644f1c5122e Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 3 Aug 2026 17:01:17 +0200 Subject: [PATCH 1/8] feat: build with released typescript@7, drop detective-typescript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typescript@7 ships the native compiler and a new unstable API only; the classic API is gone and its root export is just a version string. typedoc and our own JS-API code still need that classic API, so node_modules/typescript stays on 6.x and the native compiler is aliased as "typescript7", replacing the @typescript/native-preview dev preview. detective-typescript pulled in ts-api-utils, whose peer range of typescript >=4.8.4 is satisfied by 7.x. npm therefore hoisted it against a consumer's typescript@7, where it read ts.TypeFlags off the version-string export and threw on load — for every nacho-build command, since cli.ts imports cycles.ts eagerly. cycles.ts now extracts import specifiers with ts.createSourceFile, which drops that subtree along with the overrides block. Behaviour matches the previous detective options: type-only and dynamic imports are excluded, re-exports and mixed value/type bindings are not. Also drops @types/madge, which nothing references. New fixtures: cycles covers the import forms the parser distinguishes, and consumer-ts6/consumer-ts7 pin a consumer's own typescript to each major so CI proves both resolve. Co-Authored-By: Claude Opus 5 (1M context) --- package-lock.json | 709 ++++++++++++--------------- package.json | 24 +- src/building/cycles.ts | 95 +++- src/building/typescript/tsgo.ts | 4 +- tests/consumer-ts6/package.json | 24 + tests/consumer-ts6/src/index.ts | 3 + tests/consumer-ts6/src/tsconfig.json | 8 + tests/consumer-ts6/tsconfig.json | 5 + tests/consumer-ts6/verify.sh | 30 ++ tests/consumer-ts7/package.json | 24 + tests/consumer-ts7/src/index.ts | 3 + tests/consumer-ts7/src/tsconfig.json | 8 + tests/consumer-ts7/tsconfig.json | 5 + tests/consumer-ts7/verify.sh | 30 ++ tests/cycles/package.json | 21 + tests/cycles/src/empty-i.ts | 3 + tests/cycles/src/empty-j.ts | 3 + tests/cycles/src/index.ts | 6 + tests/cycles/src/lazy-e.ts | 5 + tests/cycles/src/lazy-f.ts | 3 + tests/cycles/src/mixed-g.ts | 5 + tests/cycles/src/mixed-h.ts | 7 + tests/cycles/src/texp-k.ts | 9 + tests/cycles/src/texp-l.ts | 5 + tests/cycles/src/texp-m.ts | 3 + tests/cycles/src/texp-n.ts | 5 + tests/cycles/src/tsconfig.json | 8 + tests/cycles/src/type-c.ts | 7 + tests/cycles/src/type-d.ts | 5 + tests/cycles/src/type-e.ts | 1 + tests/cycles/src/value-a.ts | 5 + tests/cycles/src/value-b.ts | 5 + tests/cycles/tsconfig.json | 5 + tests/cycles/verify.sh | 31 ++ 34 files changed, 703 insertions(+), 411 deletions(-) create mode 100644 tests/consumer-ts6/package.json create mode 100644 tests/consumer-ts6/src/index.ts create mode 100644 tests/consumer-ts6/src/tsconfig.json create mode 100644 tests/consumer-ts6/tsconfig.json create mode 100755 tests/consumer-ts6/verify.sh create mode 100644 tests/consumer-ts7/package.json create mode 100644 tests/consumer-ts7/src/index.ts create mode 100644 tests/consumer-ts7/src/tsconfig.json create mode 100644 tests/consumer-ts7/tsconfig.json create mode 100755 tests/consumer-ts7/verify.sh create mode 100644 tests/cycles/package.json create mode 100644 tests/cycles/src/empty-i.ts create mode 100644 tests/cycles/src/empty-j.ts create mode 100644 tests/cycles/src/index.ts create mode 100644 tests/cycles/src/lazy-e.ts create mode 100644 tests/cycles/src/lazy-f.ts create mode 100644 tests/cycles/src/mixed-g.ts create mode 100644 tests/cycles/src/mixed-h.ts create mode 100644 tests/cycles/src/texp-k.ts create mode 100644 tests/cycles/src/texp-l.ts create mode 100644 tests/cycles/src/texp-m.ts create mode 100644 tests/cycles/src/texp-n.ts create mode 100644 tests/cycles/src/tsconfig.json create mode 100644 tests/cycles/src/type-c.ts create mode 100644 tests/cycles/src/type-d.ts create mode 100644 tests/cycles/src/type-e.ts create mode 100644 tests/cycles/src/value-a.ts create mode 100644 tests/cycles/src/value-b.ts create mode 100644 tests/cycles/tsconfig.json create mode 100755 tests/cycles/verify.sh diff --git a/package-lock.json b/package-lock.json index 961b108..812e7f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,26 +9,23 @@ "version": "0.1.7", "license": "Apache-2.0", "dependencies": { - "@esbuild/linux-x64": "0.28.1", "@microsoft/tsdoc": "^0.16.0", - "@typescript/native-preview": "^7.0.0-dev.20260621.1", "ansi-colors": "^4.1.3", "commander": "^15.0.0", - "detective-typescript": "^15.0.0", "esbuild": "^0.28.1", - "minimatch": "^10.2.5", - "type-fest": "^5.7.0", - "typedoc": "^0.28.19", + "minimatch": "^10.2.6", + "type-fest": "^5.8.0", + "typedoc": "^0.28.20", "typedoc-github-theme": "^0.4.0", - "typescript": "~6.0.2" + "typescript": "~6.0.3", + "typescript7": "npm:typescript@~7.0.2" }, "bin": { "nacho-build": "bin/build.js", "nacho-run": "bin/run.js" }, "devDependencies": { - "@types/madge": "^5.0.3", - "@types/node": "^26.0.0" + "@types/node": "^26.1.2" }, "engines": { "node": ">=20" @@ -37,52 +34,6 @@ "@esbuild/linux-x64": "^0.28.1" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -563,28 +514,18 @@ "license": "MIT" }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "license": "MIT", "dependencies": { "@types/unist": "*" } }, - "node_modules/@types/madge": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/madge/-/madge-5.0.3.tgz", - "integrity": "sha512-NlQJd0qRAoyu+pawTDhLxkW940QT2dqASfwd2g/xEZu2F4Xjwa7TVRSPdbmZwUF1ygvAh0/nepeN7JjwEuOXCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "dev": true, "license": "MIT", "dependencies": { @@ -597,159 +538,140 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", - "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.4", - "@typescript-eslint/types": "^8.59.4", - "debug": "^4.4.3" - }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", - "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", - "license": "MIT", + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", - "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", - "license": "MIT", + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", - "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.4", - "@typescript-eslint/tsconfig-utils": "8.59.4", - "@typescript-eslint/types": "8.59.4", - "@typescript-eslint/visitor-keys": "8.59.4", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=16.20.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", - "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.4", - "eslint-visitor-keys": "^5.0.0" - }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-2epYxi5NDRPOvAG8TPEFd43qDVGHhD4IbIHF0uhsy72OSUbVY2rjEAv1DkW1Q2p/YbszXd7ZL/Ulch8VuDDS1g==", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], "license": "Apache-2.0", - "bin": { - "tsgo": "bin/tsgo.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260621.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260621.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20260621.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260621.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20260621.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260621.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20260621.1" - } - }, - "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-E81mNlY2JqZlDqflCvRpt6WhU6ha1SOkRC85o+EmKZJCjBxaaJpBGdwGjIisv2jiEMcHy2OW4ZHcE9GTM1BL2A==", + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", "cpu": [ "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-VS4VF8L0So56tC1jaJ57pMQOGd0Hc1wU0RIMkJi41/dKvcmc/WTRa7hiWJwzZLxXhZoVkdjesT36D9GahoAulg==", + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", "cpu": [ - "x64" + "loong64" ], "license": "Apache-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-2Xp+pu7Xu9EMKxYFiZuyIzd6KUkMn7SvElzgBRwbrhfsk54Jieg2sxyL3aAw9xfzRsEN4m1VrU3Z/iq2gL5OUw==", + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", "cpu": [ - "arm" + "mips64el" ], "license": "Apache-2.0", "optional": true, @@ -760,12 +682,28 @@ "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-Ek1uLWN6VzqwwTQNu1vcnRW4F3ipqvediLEwJP6uR5P6vrXYE7XvcGLEJcXiw31SR40NhxZW/RhLcJrHQcehrA==", + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", "cpu": [ - "arm64" + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" ], "license": "Apache-2.0", "optional": true, @@ -776,10 +714,26 @@ "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-VUhea6+Drw+PHRpsjsY7lNOZtXAlRXLHrpYynJU3jfnSflYgoqkrvlSzWVhcvhkjxGOZsqlIV+5ryy8ANZ6peg==", + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", "cpu": [ "x64" ], @@ -792,10 +746,90 @@ "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-JyxxNWWs2X7WzK90zN8hgGKqP4zo2/y7Z2tlLLaHhSFtEh1AC+tsQun3NBlW9JvF1vHhGI/hPpnoZOxLO0CEWw==", + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", "cpu": [ "arm64" ], @@ -808,10 +842,10 @@ "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20260621.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260621.1.tgz", - "integrity": "sha512-2ijIynqyxK1LkWUpLlmsBKuH7XHk6K4TAwdnfZ7x7BQHF9Z3okIXtKMzOik8fajB3oooVrEvbfOe2Mf+uZ7maQ==", + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", "cpu": [ "x64" ], @@ -839,15 +873,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/ast-module-types": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-7.0.0.tgz", - "integrity": "sha512-WZf/zDJlZYMkn+/MSl1uwJzUnDzTpVitUCcvX2oUZzN5ccYTyiiI05249AYCOqMjvbKuwiielm7GfoQKYpH/7A==", - "license": "MIT", - "engines": { - "node": ">=20.19.0 || >=22.12.0" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -858,15 +883,15 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/commander": { @@ -878,40 +903,6 @@ "node": ">=22.12.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/detective-typescript": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-15.0.0.tgz", - "integrity": "sha512-4V618NRA+WHrnynSKHaPjNszpkWRNm+SLUtzm3SIeSi+9O5veg+oSBkK1AA+dWhRJnS+5V2nt5iFQuEkB3nvBg==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "^8.59.3", - "ast-module-types": "^7.0.0", - "node-source-walk": "^8.0.0" - }, - "engines": { - "node": ">=20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "typescript": "^5.4.4 || ^6.0.2" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -965,55 +956,20 @@ "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/esbuild/node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "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/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -1026,14 +982,24 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -1043,18 +1009,18 @@ } }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "license": "MIT" }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -1063,36 +1029,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/node-source-walk": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-8.0.0.tgz", - "integrity": "sha512-/CsHIdZsVgpGm4A7cD+Vdajv2GyqkVPUNtpChugutV/O9e3j1YWkdOWwhWKo0NfRM4r4XoiKtkb4JoLzsCN5vw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.3" - }, - "engines": { - "node": ">=20.19.0 || >=22.12.0" - } - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -1102,18 +1038,6 @@ "node": ">=6" } }, - "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -1126,38 +1050,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/type-fest": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", - "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -1170,16 +1066,16 @@ } }, "node_modules/typedoc": { - "version": "0.28.19", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.19.tgz", - "integrity": "sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==", + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", "license": "Apache-2.0", "dependencies": { "@gerrit0/mini-shiki": "^3.23.0", "lunr": "^2.3.9", - "markdown-it": "^14.1.1", + "markdown-it": "^14.3.0", "minimatch": "^10.2.5", - "yaml": "^2.8.3" + "yaml": "^2.9.0" }, "bin": { "typedoc": "bin/typedoc" @@ -1217,6 +1113,41 @@ "node": ">=14.17" } }, + "node_modules/typescript7": { + "name": "typescript", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -1231,9 +1162,9 @@ "license": "MIT" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index c25678f..7eb5633 100644 --- a/package.json +++ b/package.json @@ -72,30 +72,26 @@ "homepage": "https://github.com/nacho-iot/js-tools#readme", "dependencies": { "@microsoft/tsdoc": "^0.16.0", - "@typescript/native-preview": "^7.0.0-dev.20260621.1", "ansi-colors": "^4.1.3", "commander": "^15.0.0", - "detective-typescript": "^15.0.0", "esbuild": "^0.28.1", - "minimatch": "^10.2.5", - "type-fest": "^5.7.0", - "typedoc": "^0.28.19", + "minimatch": "^10.2.6", + "type-fest": "^5.8.0", + "typedoc": "^0.28.20", "typedoc-github-theme": "^0.4.0", - "typescript": "~6.0.2" + "typescript": "~6.0.3", + "typescript7": "npm:typescript@~7.0.2" }, "optionalDependencies": { "@esbuild/linux-x64": "^0.28.1" }, "devDependencies": { - "@types/madge": "^5.0.3", - "@types/node": "^26.0.0" - }, - "overrides": { - "detective-typescript": { - "typescript": "$typescript" - } + "@types/node": "^26.1.2" }, "publishConfig": { "access": "public" + }, + "allowScripts": { + "esbuild@0.28.1": true } -} \ No newline at end of file +} diff --git a/src/building/cycles.ts b/src/building/cycles.ts index 72f16bc..2c045c9 100644 --- a/src/building/cycles.ts +++ b/src/building/cycles.ts @@ -10,9 +10,25 @@ import { readFile } from "node:fs/promises"; import { Package } from "../util/package.js"; import { Progress } from "../util/progress.js"; -// @ts-expect-error we don't have types for detective-typescript -import detective from "detective-typescript"; import { dirname, relative, resolve } from "node:path"; +import { + createSourceFile, + ExportDeclaration, + Expression, + forEachChild, + ImportDeclaration, + isExportDeclaration, + isExternalModuleReference, + isImportDeclaration, + isImportEqualsDeclaration, + isNamedExports, + isNamedImports, + isStringLiteral, + Node, + ScriptKind, + ScriptTarget, + SyntaxKind, +} from "typescript"; import { std } from "../ansi-text/std.js"; import { ansi } from "../ansi-text/text-builder.js"; @@ -27,11 +43,7 @@ async function identifyCycles(pkg: Package, progress: Progress) { const deps = {} as Record; for (const filename of await pkg.glob("{src,test}/**/*.ts")) { const contents = await readFile(filename, "utf-8"); - const fileDeps = detective(contents, { - skipTypeImports: true, - skipAsyncImports: true, - }); - deps[filename] = resolveDeps(pkg, filename, fileDeps); + deps[filename] = resolveDeps(pkg, filename, importsOf(filename, contents)); } const cycles = [] as string[][]; @@ -99,6 +111,75 @@ function printCycles(pkg: Package, cycles: string[][]) { } } +/** + * Extract the module specifiers a file depends on at runtime. Type-only and dynamic imports are excluded because + * neither creates a load-time edge, so neither can form a cycle. + */ +function importsOf(filename: string, contents: string) { + const source = createSourceFile(filename, contents, ScriptTarget.Latest, false, ScriptKind.TS); + const specifiers = Array(); + + function addSpecifier(node?: Expression) { + if (node !== undefined && isStringLiteral(node)) { + specifiers.push(node.text); + } + } + + function visit(node: Node) { + if (isImportDeclaration(node)) { + if (!isTypeOnlyImport(node)) { + addSpecifier(node.moduleSpecifier); + } + } else if (isExportDeclaration(node)) { + if (!isTypeOnlyExport(node)) { + addSpecifier(node.moduleSpecifier); + } + } else if (isImportEqualsDeclaration(node) && isExternalModuleReference(node.moduleReference)) { + addSpecifier(node.moduleReference.expression); + } + + forEachChild(node, visit); + } + + forEachChild(source, visit); + + return specifiers; +} + +function isTypeOnlyImport(node: ImportDeclaration) { + const clause = node.importClause; + if (clause === undefined) { + return false; + } + if (clause.phaseModifier === SyntaxKind.TypeKeyword) { + return true; + } + if (clause.name !== undefined) { + return false; + } + + const bindings = clause.namedBindings; + if (bindings === undefined || !isNamedImports(bindings)) { + return false; + } + + // An empty binding list still loads the module, and `every` is vacuously true for it + return bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly); +} + +function isTypeOnlyExport(node: ExportDeclaration) { + if (node.isTypeOnly) { + return true; + } + + const clause = node.exportClause; + if (clause === undefined || !isNamedExports(clause)) { + return false; + } + + return clause.elements.length > 0 && clause.elements.every(element => element.isTypeOnly); +} + function resolveDeps(pkg: Package, sourceFilename: string, deps: string[]) { const dir = dirname(sourceFilename); const aliases = pkg.importAliases; diff --git a/src/building/typescript/tsgo.ts b/src/building/typescript/tsgo.ts index 72d29e6..98df588 100644 --- a/src/building/typescript/tsgo.ts +++ b/src/building/typescript/tsgo.ts @@ -12,8 +12,10 @@ import { Package } from "../../util/package.js"; import { BuildError } from "../error.js"; import { TypescriptContext } from "./context.js"; +// The native compiler ships as typescript@7, aliased to "typescript7" because node_modules/typescript must remain +// typescript@6 — typedoc and our own JS-API code require the classic API that 7.x dropped function tsgoBin(_workspace: Package) { - return Package.tools.findPackage("@typescript/native-preview").resolve("bin/tsgo.js"); + return Package.tools.findPackage("typescript7").resolve("lib/tsc.js"); } export function createTsgoContext(workspace: Package): TypescriptContext { diff --git a/tests/consumer-ts6/package.json b/tests/consumer-ts6/package.json new file mode 100644 index 0000000..d33950a --- /dev/null +++ b/tests/consumer-ts6/package.json @@ -0,0 +1,24 @@ +{ + "name": "nacho-smoke-consumer-ts6", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "devDependencies": { + "typescript": "~6.0.3" + } +} diff --git a/tests/consumer-ts6/src/index.ts b/tests/consumer-ts6/src/index.ts new file mode 100644 index 0000000..2f6ebef --- /dev/null +++ b/tests/consumer-ts6/src/index.ts @@ -0,0 +1,3 @@ +export function greet(name: string): string { + return `hello ${name}`; +} diff --git a/tests/consumer-ts6/src/tsconfig.json b/tests/consumer-ts6/src/tsconfig.json new file mode 100644 index 0000000..0a94243 --- /dev/null +++ b/tests/consumer-ts6/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@nacho-iot/js-tools/tsc/tsconfig.lib.json", + "compilerOptions": { + "types": [] + }, + "include": ["**/*.ts"], + "references": [] +} diff --git a/tests/consumer-ts6/tsconfig.json b/tests/consumer-ts6/tsconfig.json new file mode 100644 index 0000000..60582e0 --- /dev/null +++ b/tests/consumer-ts6/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { "composite": true }, + "files": [], + "references": [{ "path": "src" }] +} diff --git a/tests/consumer-ts6/verify.sh b/tests/consumer-ts6/verify.sh new file mode 100755 index 0000000..46d3a0d --- /dev/null +++ b/tests/consumer-ts6/verify.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +# A consumer pins its own typescript for editors and lint tooling. js-tools type checks with the typescript@7 it +# carries internally, so both TS6 and TS7 consumers must build — see ../consumer-ts7 for the other half. +expected=6 +actual=$(node -p "require('typescript/package.json').version") +case "$actual" in + $expected.*) ;; + *) + echo "ERROR: fixture must exercise root typescript $expected.x but resolved $actual" >&2 + exit 1 + ;; +esac + +nacho-build --clean + +for f in \ + dist/esm/index.js \ + dist/esm/index.d.ts \ + dist/cjs/index.js \ + dist/cjs/index.d.ts +do + test -f "$f" || { echo "missing $f" >&2; exit 1; } +done + +# A transitive package that binds to the consumer's typescript crashes when its module loads, which happens for every +# command regardless of what the command does. Exercise the ones that pull in third-party TypeScript consumers. +nacho-build cycles >/dev/null +nacho-build docs >/dev/null diff --git a/tests/consumer-ts7/package.json b/tests/consumer-ts7/package.json new file mode 100644 index 0000000..48baa24 --- /dev/null +++ b/tests/consumer-ts7/package.json @@ -0,0 +1,24 @@ +{ + "name": "nacho-smoke-consumer-ts7", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "devDependencies": { + "typescript": "~7.0.2" + } +} diff --git a/tests/consumer-ts7/src/index.ts b/tests/consumer-ts7/src/index.ts new file mode 100644 index 0000000..2f6ebef --- /dev/null +++ b/tests/consumer-ts7/src/index.ts @@ -0,0 +1,3 @@ +export function greet(name: string): string { + return `hello ${name}`; +} diff --git a/tests/consumer-ts7/src/tsconfig.json b/tests/consumer-ts7/src/tsconfig.json new file mode 100644 index 0000000..0a94243 --- /dev/null +++ b/tests/consumer-ts7/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@nacho-iot/js-tools/tsc/tsconfig.lib.json", + "compilerOptions": { + "types": [] + }, + "include": ["**/*.ts"], + "references": [] +} diff --git a/tests/consumer-ts7/tsconfig.json b/tests/consumer-ts7/tsconfig.json new file mode 100644 index 0000000..60582e0 --- /dev/null +++ b/tests/consumer-ts7/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { "composite": true }, + "files": [], + "references": [{ "path": "src" }] +} diff --git a/tests/consumer-ts7/verify.sh b/tests/consumer-ts7/verify.sh new file mode 100755 index 0000000..44708cb --- /dev/null +++ b/tests/consumer-ts7/verify.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +# A consumer pins its own typescript for editors and lint tooling. js-tools type checks with the typescript@7 it +# carries internally, so both TS6 and TS7 consumers must build — see ../consumer-ts6 for the other half. +expected=7 +actual=$(node -p "require('typescript/package.json').version") +case "$actual" in + $expected.*) ;; + *) + echo "ERROR: fixture must exercise root typescript $expected.x but resolved $actual" >&2 + exit 1 + ;; +esac + +nacho-build --clean + +for f in \ + dist/esm/index.js \ + dist/esm/index.d.ts \ + dist/cjs/index.js \ + dist/cjs/index.d.ts +do + test -f "$f" || { echo "missing $f" >&2; exit 1; } +done + +# A transitive package that binds to the consumer's typescript crashes when its module loads, which happens for every +# command regardless of what the command does. Exercise the ones that pull in third-party TypeScript consumers. +nacho-build cycles >/dev/null +nacho-build docs >/dev/null diff --git a/tests/cycles/package.json b/tests/cycles/package.json new file mode 100644 index 0000000..60490b6 --- /dev/null +++ b/tests/cycles/package.json @@ -0,0 +1,21 @@ +{ + "name": "nacho-smoke-cycles", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + } +} diff --git a/tests/cycles/src/empty-i.ts b/tests/cycles/src/empty-i.ts new file mode 100644 index 0000000..44c0e12 --- /dev/null +++ b/tests/cycles/src/empty-i.ts @@ -0,0 +1,3 @@ +import {} from "./empty-j.js"; + +export const i = "i"; diff --git a/tests/cycles/src/empty-j.ts b/tests/cycles/src/empty-j.ts new file mode 100644 index 0000000..9ae8c8d --- /dev/null +++ b/tests/cycles/src/empty-j.ts @@ -0,0 +1,3 @@ +import { i } from "./empty-i.js"; + +export const j = `j${i}`; diff --git a/tests/cycles/src/index.ts b/tests/cycles/src/index.ts new file mode 100644 index 0000000..7dd5d37 --- /dev/null +++ b/tests/cycles/src/index.ts @@ -0,0 +1,6 @@ +export * from "./value-a.js"; +export type { TypeC } from "./type-c.js"; +export { lazy } from "./lazy-e.js"; +export { mixed } from "./mixed-g.js"; +export { i } from "./empty-i.js"; +export { j } from "./empty-j.js"; diff --git a/tests/cycles/src/lazy-e.ts b/tests/cycles/src/lazy-e.ts new file mode 100644 index 0000000..cd65724 --- /dev/null +++ b/tests/cycles/src/lazy-e.ts @@ -0,0 +1,5 @@ +import { helper } from "./lazy-f.js"; + +export function lazy() { + return helper(); +} diff --git a/tests/cycles/src/lazy-f.ts b/tests/cycles/src/lazy-f.ts new file mode 100644 index 0000000..b61ab45 --- /dev/null +++ b/tests/cycles/src/lazy-f.ts @@ -0,0 +1,3 @@ +export function helper() { + return import("./lazy-e.js"); +} diff --git a/tests/cycles/src/mixed-g.ts b/tests/cycles/src/mixed-g.ts new file mode 100644 index 0000000..d3c3e68 --- /dev/null +++ b/tests/cycles/src/mixed-g.ts @@ -0,0 +1,5 @@ +import { type Unused, helper } from "./mixed-h.js"; + +export function mixed(): Unused { + return helper(); +} diff --git a/tests/cycles/src/mixed-h.ts b/tests/cycles/src/mixed-h.ts new file mode 100644 index 0000000..3f5d87f --- /dev/null +++ b/tests/cycles/src/mixed-h.ts @@ -0,0 +1,7 @@ +import { mixed } from "./mixed-g.js"; + +export type Unused = string; + +export function helper() { + return typeof mixed === "function" ? "h" : "?"; +} diff --git a/tests/cycles/src/texp-k.ts b/tests/cycles/src/texp-k.ts new file mode 100644 index 0000000..1c4ed57 --- /dev/null +++ b/tests/cycles/src/texp-k.ts @@ -0,0 +1,9 @@ +import { fromL } from "./texp-l.js"; + +export interface KType { + value: string; +} + +export function fromK() { + return fromL(); +} diff --git a/tests/cycles/src/texp-l.ts b/tests/cycles/src/texp-l.ts new file mode 100644 index 0000000..0e6a6f4 --- /dev/null +++ b/tests/cycles/src/texp-l.ts @@ -0,0 +1,5 @@ +export type { KType } from "./texp-k.js"; + +export function fromL() { + return "l"; +} diff --git a/tests/cycles/src/texp-m.ts b/tests/cycles/src/texp-m.ts new file mode 100644 index 0000000..32fad82 --- /dev/null +++ b/tests/cycles/src/texp-m.ts @@ -0,0 +1,3 @@ +export type * from "./texp-n.js"; + +export const m = "m"; diff --git a/tests/cycles/src/texp-n.ts b/tests/cycles/src/texp-n.ts new file mode 100644 index 0000000..1270168 --- /dev/null +++ b/tests/cycles/src/texp-n.ts @@ -0,0 +1,5 @@ +import { m } from "./texp-m.js"; + +export type NType = string; + +export const n = `n${m}`; diff --git a/tests/cycles/src/tsconfig.json b/tests/cycles/src/tsconfig.json new file mode 100644 index 0000000..0a94243 --- /dev/null +++ b/tests/cycles/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@nacho-iot/js-tools/tsc/tsconfig.lib.json", + "compilerOptions": { + "types": [] + }, + "include": ["**/*.ts"], + "references": [] +} diff --git a/tests/cycles/src/type-c.ts b/tests/cycles/src/type-c.ts new file mode 100644 index 0000000..adb6153 --- /dev/null +++ b/tests/cycles/src/type-c.ts @@ -0,0 +1,7 @@ +import type TypeD from "./type-d.js"; +import { type OnlyType } from "./type-e.js"; + +export interface TypeC { + d?: TypeD; + e?: OnlyType; +} diff --git a/tests/cycles/src/type-d.ts b/tests/cycles/src/type-d.ts new file mode 100644 index 0000000..f9da841 --- /dev/null +++ b/tests/cycles/src/type-d.ts @@ -0,0 +1,5 @@ +import type { TypeC } from "./type-c.js"; + +export default interface TypeD { + c?: TypeC; +} diff --git a/tests/cycles/src/type-e.ts b/tests/cycles/src/type-e.ts new file mode 100644 index 0000000..694eb41 --- /dev/null +++ b/tests/cycles/src/type-e.ts @@ -0,0 +1 @@ +export type { TypeC as OnlyType } from "./type-c.js"; diff --git a/tests/cycles/src/value-a.ts b/tests/cycles/src/value-a.ts new file mode 100644 index 0000000..8f3cf5f --- /dev/null +++ b/tests/cycles/src/value-a.ts @@ -0,0 +1,5 @@ +import { fromB } from "./value-b.js"; + +export function fromA() { + return `a:${fromB()}`; +} diff --git a/tests/cycles/src/value-b.ts b/tests/cycles/src/value-b.ts new file mode 100644 index 0000000..6cdeb78 --- /dev/null +++ b/tests/cycles/src/value-b.ts @@ -0,0 +1,5 @@ +export * from "./value-a.js"; + +export function fromB() { + return "b"; +} diff --git a/tests/cycles/tsconfig.json b/tests/cycles/tsconfig.json new file mode 100644 index 0000000..60582e0 --- /dev/null +++ b/tests/cycles/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { "composite": true }, + "files": [], + "references": [{ "path": "src" }] +} diff --git a/tests/cycles/verify.sh b/tests/cycles/verify.sh new file mode 100755 index 0000000..f6f302b --- /dev/null +++ b/tests/cycles/verify.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +nacho-build --clean + +log=$(mktemp) +nacho-build cycles >"$log" 2>&1 + +fail() { + echo "ERROR: $1" >&2 + cat "$log" >&2 + exit 1 +} + +grep -q "Cycles detected:" "$log" || fail "expected a cycle report" + +# value-a ⇄ value-b via a plain import and an `export * from` re-export +# mixed-g ⇄ mixed-h via an import mixing type and value bindings +# empty-i ⇄ empty-j via `import {} from`, which still evaluates the module +for f in value-a value-b mixed-g mixed-h empty-i empty-j; do + grep -q "$f" "$log" || fail "expected $f in the cycle report" +done + +# `import type` edges are not real dependencies, so type-c ⇄ type-d is not a cycle +# `export type ... from` is likewise erased, so texp-k ⇄ texp-l and texp-m ⇄ texp-n are not cycles +# `import()` is lazy, so lazy-e ⇄ lazy-f is not a cycle +for f in type-c type-d texp-k texp-l texp-m texp-n lazy-e lazy-f; do + if grep -q "$f" "$log"; then + fail "$f must not be reported as a cycle" + fi +done From 7c3bfea312a4cfe942235dcbefdd9ee8529ba9c7 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 3 Aug 2026 17:02:31 +0200 Subject: [PATCH 2/8] fix: read BOT_APP_ID from secrets in the release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BOT_APP_ID and BOT_APP_PRIVATE_KEY are both organization secrets visible to this repository, but the workflow read vars.BOT_APP_ID. No variable of that name exists at either level, so app-id was empty and token minting failed before checkout — the only Release run to date never got past its first step. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46fae80..2076ab1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ # Setup required (one-time): # - GitHub App with contents:write installed on this repo # - Org (or repo) secret BOT_APP_PRIVATE_KEY: the App's .pem private key -# - Org (or repo) variable BOT_APP_ID: the App's numeric ID +# - Org (or repo) secret BOT_APP_ID: the App's numeric ID # - The App listed in the "protect main" ruleset's bypass_actors name: Release @@ -39,7 +39,7 @@ jobs: id: app-token uses: actions/create-github-app-token@v3 with: - app-id: ${{ vars.BOT_APP_ID }} + app-id: ${{ secrets.BOT_APP_ID }} private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - uses: actions/checkout@v7 From 7d79e8ed7fd52923dd071c2f7c0809c2c24c47c9 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 3 Aug 2026 17:05:09 +0200 Subject: [PATCH 3/8] fix: keep untrusted values out of run: blocks in the release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub expands ${{ }} as text before the shell parses the line, so a workflow_dispatch version of `1.0.0"; ; "` executed that command with the App token in scope — a token that pushes directly to main past the ruleset. The inputs now travel through env: and stay shell variables. The same value also reached the Commit and Push steps via step outputs, so env: alone would not have closed the path; the applied version is validated against the character set a release tag can contain before it is written to GITHUB_OUTPUT, which also stops a multi-line value forging extra outputs. app-slug moves to env: too. It comes from actions/create-github-app-token and is not attacker controlled, but no interpolation inside any run: block is a rule that can be checked mechanically. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2076ab1..f794535 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,31 +57,48 @@ jobs: - name: Configure git identity env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} run: | - NAME="${{ steps.app-token.outputs.app-slug }}[bot]" + NAME="${APP_SLUG}[bot]" ID=$(gh api "/users/$NAME" --jq .id) git config user.name "$NAME" git config user.email "${ID}+${NAME}@users.noreply.github.com" - name: Apply version id: apply + env: + VERSION: ${{ inputs.version }} + BUMP: ${{ inputs.bump }} run: | - if [ -n "${{ inputs.version }}" ]; then - node bin/build.js version "${{ inputs.version }}" --set --apply + if [ -n "$VERSION" ]; then + node bin/build.js version "$VERSION" --set --apply else - node bin/build.js version --bump "${{ inputs.bump }}" --set --apply + node bin/build.js version --bump "$BUMP" --set --apply fi - echo "version=$(cat version.txt)" >> "$GITHUB_OUTPUT" + applied=$(cat version.txt) + # Expanded into later `run:` blocks before the shell sees it, so anything outside this set injects a + # command or a forged step output + case "$applied" in + "" | *[!0-9A-Za-z.+-]*) + echo "::error::refusing to release unexpected version '$applied'" + exit 1 + ;; + esac + echo "version=$applied" >> "$GITHUB_OUTPUT" - name: Commit + env: + VERSION: ${{ steps.apply.outputs.version }} run: | git add -A - git commit -m "Release ${{ steps.apply.outputs.version }}" + git commit -m "Release $VERSION" - name: Tag run: node bin/build.js version --tag - name: Push + env: + VERSION: ${{ steps.apply.outputs.version }} run: | git push origin HEAD:main - git push origin "v${{ steps.apply.outputs.version }}" + git push origin "v$VERSION" From 23fd79cb0acf2a917d0aeb593109e6418c92453c Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Mon, 3 Aug 2026 17:19:01 +0200 Subject: [PATCH 4/8] fix: build docs with posix paths so the docs commands work on Windows Module paths are assembled with path.join, so on Windows they carry native separators. Three places then matched them against hardcoded "/": - addModuleGlobs rewrote /dist/(esm|cjs)/ to /src/, which never fired, so sourceModules resolved to transpiled output instead of source - typedoc reads entry points as globs, where "\" escapes the next character rather than separating segments, so it rejected the path outright - naiveToRealNamesFor stripped a trailing "/index" to match typedoc's module names, which are posix on every platform The first two together produced "The glob ...\dist\esm\index.js escapes a non-special character" followed by an unmapped-module-name failure. Module paths now stay posix from the point they are assembled, and the local glob already normalises its pattern but resolves results natively, so globbed wildcard paths are normalised too before the prefix arithmetic uses them. sourceModules has no callers outside docs.ts, so this reaches nothing else. Co-Authored-By: Claude Opus 5 (1M context) --- src/building/docs.ts | 10 +++++----- src/util/file.ts | 10 +++++++++- src/util/package.ts | 11 +++++++---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/building/docs.ts b/src/building/docs.ts index cf8a839..19f60df 100644 --- a/src/building/docs.ts +++ b/src/building/docs.ts @@ -16,7 +16,7 @@ import { ReflectionKind, TypeDocOptions, } from "typedoc"; -import { isFile } from "../util/file.js"; +import { isFile, posixPath } from "../util/file.js"; import { Package } from "../util/package.js"; import { Progress } from "../util/progress.js"; import { Graph } from "./graph.js"; @@ -45,7 +45,7 @@ export async function buildDocs(pkg: Package, progress: Progress) { const entryPoints = Array(); // In a workspace typedoc requires files to be relative to the workspace for merge - const basePath = pkg.root.path; + const basePath = posixPath(pkg.root.path); for (const name in modules) { const path = modules[name]; @@ -98,7 +98,7 @@ export async function buildDocs(pkg: Package, progress: Progress) { function reflectionsPath(pkg: Package) { const path = pkg.resolve("build/docs.json"); if (isFile(path)) { - return path; + return posixPath(path); } } @@ -142,7 +142,7 @@ function naiveToRealNamesFor(pkg: Package) { const packagePrefix = `${pkg.name}/`; // In a workspace typedoc requires files to be relative to the workspace for merge - const basePath = pkg.root.path; + const basePath = posixPath(pkg.root.path); for (const name in modules) { const path = modules[name]; @@ -150,7 +150,7 @@ function naiveToRealNamesFor(pkg: Package) { continue; } - const naiveName = relative(basePath, path) + const naiveName = posixPath(relative(basePath, path)) .replace(/.(?:ts|js|mjs|cjs)$/, "") // typedoc strips "/index"; this is hard-coded and doesn't respect exports diff --git a/src/util/file.ts b/src/util/file.ts index b383b04..1c52cba 100644 --- a/src/util/file.ts +++ b/src/util/file.ts @@ -5,7 +5,7 @@ */ import { readdirSync, readFileSync, statSync } from "node:fs"; -import { resolve } from "node:path"; +import { resolve, sep } from "node:path"; import { ignoreErrorSync } from "./errors.js"; function isNotFoundError(e: unknown) { @@ -56,6 +56,14 @@ export function isFile(path: string) { return !!ignoreErrorSync("ENOENT", () => statSync(path).isFile()); } +/** + * Rewrite a native path in posix form. Globs read "\" as an escape rather than a separator, and tooling that names + * modules after their path uses "/" on every platform. + */ +export function posixPath(path: string) { + return sep === "\\" ? path.replaceAll("\\", "/") : path; +} + /** * Tiny virtual filesystem driver. Currently used only for processing globs. */ diff --git a/src/util/package.ts b/src/util/package.ts index 80a478c..e5588d0 100644 --- a/src/util/package.ts +++ b/src/util/package.ts @@ -8,7 +8,7 @@ import { existsSync, readFileSync, statSync } from "node:fs"; import { readdir, readFile, stat, writeFile } from "node:fs/promises"; import { dirname, join, relative, resolve } from "node:path"; import { ignoreError, ignoreErrorSync } from "./errors.js"; -import { isFile, maybeReadJsonSync, maybeStatSync } from "./file.js"; +import { isFile, maybeReadJsonSync, maybeStatSync, posixPath } from "./file.js"; import { globSync } from "./glob.js"; import { ImportAliases } from "./import-aliases.js"; import { Progress } from "./progress.js"; @@ -532,7 +532,9 @@ function findModules( } function addModuleGlobs(source: boolean, target: Record, name: string, base: string, pattern: string) { - let path = join(base, pattern); + // Module paths stay in posix form: they are matched against "/"-separated patterns below and handed to typedoc as + // globs, neither of which tolerates a native Windows separator + let path = posixPath(join(base, pattern)); if (source) { path = path.replace(/\/dist\/(?:esm|cjs)\//, "/src/"); } @@ -553,9 +555,10 @@ function addModuleGlobs(source: boolean, target: Record, name: s const prefixLength = prefix === undefined ? 0 : prefix.length; const suffixLength = suffix === undefined ? 0 : suffix.length; - for (const thisPath of paths) { + for (const globbedPath of paths) { + const thisPath = posixPath(globbedPath); const qualifier = thisPath.substring(prefixLength, thisPath.length - suffixLength); - const thisName = join(name, qualifier); + const thisName = posixPath(join(name, qualifier)); target[thisName] = thisPath; } } else if (path.includes("*")) { From 79ecae35a37e52c660ca235246cec1aa25b3aee4 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Wed, 5 Aug 2026 16:56:46 +0200 Subject: [PATCH 5/8] refactor: rewrite path separators via posixPath everywhere Review feedback on #52: the manual .replace(/\\/g, "/") sites predate the posixPath helper. posixPath also only rewrites on Windows, so a literal backslash in a minimatch pattern is no longer mangled on posix. Co-Authored-By: Claude Fable 5 --- src/building/project.ts | 3 ++- src/building/tsconfig.ts | 7 ++++--- src/util/glob.ts | 4 ++-- src/util/package.ts | 8 ++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/building/project.ts b/src/building/project.ts index 7829ff6..333316f 100644 --- a/src/building/project.ts +++ b/src/building/project.ts @@ -9,6 +9,7 @@ import { cp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; import { platform } from "node:os"; import { dirname, join } from "node:path"; import { ignoreError } from "../util/errors.js"; +import { posixPath } from "../util/file.js"; import { CONFIG_PATH, Package } from "../util/package.js"; export const BUILD_INFO_LOCATION = "build/info.json"; @@ -192,7 +193,7 @@ export class Project { async #targetsOf(indir: string, outdir: string, ...extensions: string[]) { const inputPrefixLength = this.pkg.resolve(indir).length + 1; - outdir = this.pkg.resolve(outdir).replace(/\\/g, "/"); + outdir = posixPath(this.pkg.resolve(outdir)); return (await this.pkg.glob(extensions.map(ext => `${indir}/**/*.${ext}`))).map(file => ({ in: file, diff --git a/src/building/tsconfig.ts b/src/building/tsconfig.ts index 865c2e5..3186e81 100644 --- a/src/building/tsconfig.ts +++ b/src/building/tsconfig.ts @@ -6,6 +6,7 @@ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import { relative, resolve } from "node:path"; +import { posixPath } from "../util/file.js"; import { Package } from "../util/package.js"; import { Graph } from "./graph.js"; @@ -54,7 +55,7 @@ export async function syncAllTsconfigs(graph: Graph, force = false) { for (const node of graph.nodes) { await syncPackageTsconfigs(graph, node, force); rootTsconfig.references.push({ - path: workspace.relative(node.pkg.path).replace(/\\/g, "/"), + path: posixPath(workspace.relative(node.pkg.path)), }); } @@ -108,7 +109,7 @@ async function syncSubproject( // Only rewrite extends when forced (configure command); otherwise preserve user edits if (force) { - tsconfig.extends = relative(path, workspace.resolve(TEMPLATE_DIR, baseConfig)).replace(/\\/g, "/"); + tsconfig.extends = posixPath(relative(path, workspace.resolve(TEMPLATE_DIR, baseConfig))); } const deps = node.dependencies.filter(dep => dep.pkg.isLibrary).map(dep => dep.pkg.resolve("src")); @@ -116,7 +117,7 @@ async function syncSubproject( const desired = [...new Set([...deps, ...extraRefs])]; const newReferences = desired - .map(ref => ({ path: relative(path, ref).replace(/\\/g, "/") })) + .map(ref => ({ path: posixPath(relative(path, ref)) })) .sort((ref1, ref2) => ref1.path.localeCompare(ref2.path)); tsconfig.references = newReferences; diff --git a/src/util/glob.ts b/src/util/glob.ts index eaff33b..5e3d147 100644 --- a/src/util/glob.ts +++ b/src/util/glob.ts @@ -5,7 +5,7 @@ */ import { GLOBSTAR, Minimatch, ParseReturnFiltered } from "minimatch"; -import { FilesystemSync } from "./file.js"; +import { FilesystemSync, posixPath } from "./file.js"; export class GlobError extends Error {} @@ -24,7 +24,7 @@ export function globSync(pattern: string | string[], fs = FilesystemSync()) { function globOneSync(pattern: string, fs: FilesystemSync) { // Parse the glob - const mm = new Minimatch(pattern.replace(/\\/g, "/"), {}); + const mm = new Minimatch(posixPath(pattern), {}); const results = new Set(); for (const part of mm.set) { for (const path of globOnePartSync(mm, part, fs)) { diff --git a/src/util/package.ts b/src/util/package.ts index e5588d0..370937a 100644 --- a/src/util/package.ts +++ b/src/util/package.ts @@ -120,9 +120,9 @@ export class Package { async glob(pattern: string | string[]) { // Glob only understands forward-slash as separator because reasons if (typeof pattern === "string") { - pattern = this.resolve(pattern).replace(/\\/g, "/"); + pattern = posixPath(this.resolve(pattern)); } else { - pattern = pattern.map(s => this.resolve(s).replace(/\\/g, "/")); + pattern = pattern.map(s => posixPath(this.resolve(s))); } // Current glob implementation isn't actually async as this is faster and we only walk small directory trees @@ -532,8 +532,8 @@ function findModules( } function addModuleGlobs(source: boolean, target: Record, name: string, base: string, pattern: string) { - // Module paths stay in posix form: they are matched against "/"-separated patterns below and handed to typedoc as - // globs, neither of which tolerates a native Windows separator + // Module paths stay in posix form: they are matched against "/"-separated patterns below and consumed as globs, + // neither of which tolerates a native Windows separator let path = posixPath(join(base, pattern)); if (source) { path = path.replace(/\/dist\/(?:esm|cjs)\//, "/src/"); From e04984aa90b7440ad3394f76e1427b1f69c5c872 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Wed, 5 Aug 2026 16:58:16 +0200 Subject: [PATCH 6/8] feat: drop typescript@6, typedoc and the ts6 compile path Review feedback on #52: with the native compiler released as typescript@7 there is no reason to keep two compilers around. - typescript dependency is now typescript@7 under its real name; the typescript7 alias and the ts6 pin are gone - docs command and typedoc/tsdoc dependencies removed until typedoc supports typescript@7; resurrect from git history when it does - ts6 solution-builder/incremental-compiler contexts, --tsc flag and NACHO_TSGO env var removed; tsgo is the only type checker - cycles now parses with the typescript@7 API (typescript/unstable/*): one shared Go server per run, per-package openFiles/closeFiles - cycle semantics fixed while porting: import {} from and import type X = require() are erased by the emitter and import defer evaluates lazily, so none of them is a load-time edge; the cycles fixture asserts the corrected behavior Co-Authored-By: Claude Fable 5 --- README.md | 2 +- package-lock.json | 239 +----------- package.json | 6 +- src/building/cli.ts | 51 +-- src/building/cycles.ts | 85 +++-- src/building/docs.ts | 345 ------------------ src/building/graph.ts | 34 +- src/building/project-builder.ts | 14 +- src/building/typescript.ts | 16 - src/building/typescript/context.ts | 85 ----- .../typescript/incremental-compiler.ts | 123 ------- src/building/typescript/solution-builder.ts | 173 --------- src/building/typescript/tsgo.ts | 9 +- tests/consumer-ts6/verify.sh | 3 +- tests/consumer-ts7/verify.sh | 3 +- tests/cycles/verify.sh | 6 +- 16 files changed, 82 insertions(+), 1112 deletions(-) delete mode 100644 src/building/docs.ts delete mode 100644 src/building/typescript.ts delete mode 100644 src/building/typescript/context.ts delete mode 100644 src/building/typescript/incremental-compiler.ts delete mode 100644 src/building/typescript/solution-builder.ts diff --git a/README.md b/README.md index a689e68..2c568a4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Supports both flat single-package repos and multi-package npm workspaces. ## CLI commands * `nacho-build` -- build TypeScript packages (type checking, ESM/CJS transpilation); includes subcommands for - workspace maintenance (`configure`, `relock`, `version`, `cycles`, `docs`, etc.) + workspace maintenance (`configure`, `relock`, `version`, `cycles`, etc.) * `nacho-run` -- run a TypeScript script with automatic transpilation and source maps ## License diff --git a/package-lock.json b/package-lock.json index 812e7f0..c0e63b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,12 @@ "version": "0.1.7", "license": "Apache-2.0", "dependencies": { - "@microsoft/tsdoc": "^0.16.0", "ansi-colors": "^4.1.3", "commander": "^15.0.0", "esbuild": "^0.28.1", "minimatch": "^10.2.6", "type-fest": "^5.8.0", - "typedoc": "^0.28.20", - "typedoc-github-theme": "^0.4.0", - "typescript": "~6.0.3", - "typescript7": "npm:typescript@~7.0.2" + "typescript": "~7.0.2" }, "bin": { "nacho-build": "bin/build.js", @@ -450,78 +446,6 @@ "node": ">=18" } }, - "node_modules/@gerrit0/mini-shiki": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", - "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==", - "license": "MIT", - "dependencies": { - "@shikijs/engine-oniguruma": "^3.23.0", - "@shikijs/langs": "^3.23.0", - "@shikijs/themes": "^3.23.0", - "@shikijs/types": "^3.23.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@microsoft/tsdoc": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", - "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", - "license": "MIT" - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", - "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2" - } - }, - "node_modules/@shikijs/langs": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", - "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0" - } - }, - "node_modules/@shikijs/themes": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", - "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0" - } - }, - "node_modules/@shikijs/types": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", - "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", - "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/@types/node": { "version": "26.1.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", @@ -532,12 +456,6 @@ "undici-types": "~8.3.0" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/@typescript/typescript-aix-ppc64": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", @@ -867,12 +785,6 @@ "node": ">=6" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -903,18 +815,6 @@ "node": ">=22.12.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -956,64 +856,6 @@ "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "license": "MIT" - }, - "node_modules/markdown-it": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", - "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/mdurl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", - "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", - "license": "MIT" - }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -1029,15 +871,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", @@ -1065,56 +898,7 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typedoc": { - "version": "0.28.20", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", - "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", - "license": "Apache-2.0", - "dependencies": { - "@gerrit0/mini-shiki": "^3.23.0", - "lunr": "^2.3.9", - "markdown-it": "^14.3.0", - "minimatch": "^10.2.5", - "yaml": "^2.9.0" - }, - "bin": { - "typedoc": "bin/typedoc" - }, - "engines": { - "node": ">= 18", - "pnpm": ">= 10" - }, - "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" - } - }, - "node_modules/typedoc-github-theme": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/typedoc-github-theme/-/typedoc-github-theme-0.4.0.tgz", - "integrity": "sha512-lo/hr4EFZxq0SsMGeAscKUzljIKFgrJf5fb4nOAJcqaiSShQv7kzwF6M1s2fVRvUyx6UsmD4zEb+MtKkbucYpg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typedoc": "~0.28.0" - } - }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript7": { - "name": "typescript", "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", @@ -1148,33 +932,12 @@ "@typescript/typescript-win32-x64": "7.0.2" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" - }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } } } } diff --git a/package.json b/package.json index 7eb5633..2cf9805 100644 --- a/package.json +++ b/package.json @@ -71,16 +71,12 @@ }, "homepage": "https://github.com/nacho-iot/js-tools#readme", "dependencies": { - "@microsoft/tsdoc": "^0.16.0", "ansi-colors": "^4.1.3", "commander": "^15.0.0", "esbuild": "^0.28.1", "minimatch": "^10.2.6", "type-fest": "^5.8.0", - "typedoc": "^0.28.20", - "typedoc-github-theme": "^0.4.0", - "typescript": "~6.0.3", - "typescript7": "npm:typescript@~7.0.2" + "typescript": "~7.0.2" }, "optionalDependencies": { "@esbuild/linux-x64": "^0.28.1" diff --git a/src/building/cli.ts b/src/building/cli.ts index 33d4575..d610298 100644 --- a/src/building/cli.ts +++ b/src/building/cli.ts @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { API } from "typescript/unstable/sync"; import { commander } from "../util/commander.js"; import { Package } from "../util/package.js"; import { reportCycles } from "./cycles.js"; -import { buildDocs, mergeDocs } from "./docs.js"; import { Graph } from "./graph.js"; import { ProjectBuilder, Target } from "./project-builder.js"; import { Project } from "./project.js"; @@ -21,7 +21,6 @@ enum Mode { BuildWorkspace, DisplayGraph, Configure, - BuildDocs, Relock, SyncTsconfigs, Circular, @@ -42,8 +41,7 @@ export async function main(argv = process.argv) { const program = commander("nacho-build", "Builds TypeScript packages and monorepos.") .option("-p, --prefix ", "specify build directory", ".") .option("-c, --clean", "clean before build", false) - .option("-d, --dependencies", "build dependencies", false) - .option("--tsc", "use tsc instead of tsgo for type checking"); + .option("-d, --dependencies", "build dependencies", false); program .command("build") @@ -92,13 +90,6 @@ export async function main(argv = process.argv) { mode = Mode.SyncTsconfigs; }); - program - .command("docs") - .description("build workspace documentation") - .action(() => { - mode = Mode.BuildDocs; - }); - program .command("configure") .description("refresh tsconfig templates and sync all tsconfigs") @@ -144,8 +135,7 @@ export async function main(argv = process.argv) { } function builder(graph?: Graph) { - const { tsc, ...rest } = args as Args & { tsc?: boolean }; - return new ProjectBuilder({ ...rest, tsgo: tsc ? false : undefined, targets: [...targets], graph }); + return new ProjectBuilder({ ...args, targets: [...targets], graph }); } switch (mode as Mode) { @@ -203,33 +193,22 @@ export async function main(argv = process.argv) { }); break; - case Mode.BuildDocs: { - using progress = pkg.start("Documenting"); - if (pkg.isWorkspace) { - const graph = await Graph.load(); - for (const node of graph.nodes) { - if (node.pkg.isLibrary) { - await progress.run(node.pkg.name, () => buildDocs(node.pkg, progress)); - } - } - await mergeDocs(Package.workspace); - } else { - await progress.run(pkg.name, () => buildDocs(pkg, progress)); - } - break; - } - case Mode.Circular: { using progress = pkg.start("Analyzing dependencies"); - if (pkg.isWorkspace) { - const graph = await Graph.load(); - for (const node of graph.nodes) { - if (node.pkg.isLibrary) { - await reportCycles(node.pkg, progress); + const api = new API(); + try { + if (pkg.isWorkspace) { + const graph = await Graph.load(); + for (const node of graph.nodes) { + if (node.pkg.isLibrary) { + await reportCycles(node.pkg, progress, api); + } } + } else { + await reportCycles(pkg, progress, api); } - } else { - await reportCycles(pkg, progress); + } finally { + api.close(); } break; } diff --git a/src/building/cycles.ts b/src/building/cycles.ts index 2c045c9..07267a4 100644 --- a/src/building/cycles.ts +++ b/src/building/cycles.ts @@ -6,16 +6,13 @@ const MAX_CYCLES = 1000; -import { readFile } from "node:fs/promises"; import { Package } from "../util/package.js"; import { Progress } from "../util/progress.js"; import { dirname, relative, resolve } from "node:path"; import { - createSourceFile, ExportDeclaration, Expression, - forEachChild, ImportDeclaration, isExportDeclaration, isExternalModuleReference, @@ -25,27 +22,40 @@ import { isNamedImports, isStringLiteral, Node, - ScriptKind, - ScriptTarget, - SyntaxKind, -} from "typescript"; + SourceFile, +} from "typescript/unstable/ast"; +import { API } from "typescript/unstable/sync"; import { std } from "../ansi-text/std.js"; import { ansi } from "../ansi-text/text-builder.js"; +import { BuildError } from "./error.js"; -export async function reportCycles(pkg: Package, progress: Progress) { - const cycles = await progress.run(pkg.name, () => identifyCycles(pkg, progress)); +export async function reportCycles(pkg: Package, progress: Progress, api: API) { + const cycles = await progress.run(pkg.name, () => identifyCycles(pkg, progress, api)); if (cycles) { printCycles(pkg, cycles); } } -async function identifyCycles(pkg: Package, progress: Progress) { +async function identifyCycles(pkg: Package, progress: Progress, api: API) { + const files = await pkg.glob("{src,test}/**/*.ts"); + const deps = {} as Record; - for (const filename of await pkg.glob("{src,test}/**/*.ts")) { - const contents = await readFile(filename, "utf-8"); - deps[filename] = resolveDeps(pkg, filename, importsOf(filename, contents)); + { + using snapshot = api.updateSnapshot({ openFiles: files }); + for (const filename of files) { + progress.refresh(); + const source = snapshot.getDefaultProjectForFile(filename)?.program.getSourceFile(filename); + if (source === undefined) { + throw new BuildError(`Cannot parse ${filename} for cycle analysis`); + } + deps[filename] = resolveDeps(pkg, filename, importsOf(source)); + } } + // Opens are ref-counted server-side; without this the shared server accumulates every package's program for the + // life of the run + api.updateSnapshot({ closeFiles: files }).dispose(); + const cycles = [] as string[][]; for (const filename in deps) { visit(filename, []); @@ -112,11 +122,10 @@ function printCycles(pkg: Package, cycles: string[][]) { } /** - * Extract the module specifiers a file depends on at runtime. Type-only and dynamic imports are excluded because - * neither creates a load-time edge, so neither can form a cycle. + * Extract the module specifiers a file depends on at load time. Type-only, deferred, dynamic and fully-elided + * imports are excluded because none of them creates a load-time edge, so none can form a cycle. */ -function importsOf(filename: string, contents: string) { - const source = createSourceFile(filename, contents, ScriptTarget.Latest, false, ScriptKind.TS); +function importsOf(source: SourceFile) { const specifiers = Array(); function addSpecifier(node?: Expression) { @@ -127,57 +136,65 @@ function importsOf(filename: string, contents: string) { function visit(node: Node) { if (isImportDeclaration(node)) { - if (!isTypeOnlyImport(node)) { + if (importsAtLoadTime(node)) { addSpecifier(node.moduleSpecifier); } } else if (isExportDeclaration(node)) { - if (!isTypeOnlyExport(node)) { + if (exportsAtLoadTime(node)) { addSpecifier(node.moduleSpecifier); } - } else if (isImportEqualsDeclaration(node) && isExternalModuleReference(node.moduleReference)) { + } else if ( + isImportEqualsDeclaration(node) && + !node.isTypeOnly && + isExternalModuleReference(node.moduleReference) + ) { addSpecifier(node.moduleReference.expression); } - forEachChild(node, visit); + node.forEachChild(visit); } - forEachChild(source, visit); + source.forEachChild(visit); return specifiers; } -function isTypeOnlyImport(node: ImportDeclaration) { +function importsAtLoadTime(node: ImportDeclaration) { const clause = node.importClause; if (clause === undefined) { - return false; - } - if (clause.phaseModifier === SyntaxKind.TypeKeyword) { + // Bare `import "x"` exists only for its side effects return true; } - if (clause.name !== undefined) { + + // `import type` is erased and `import defer` evaluates lazily like `import()` + if (clause.phaseModifier !== undefined) { return false; } + if (clause.name !== undefined) { + return true; + } + const bindings = clause.namedBindings; if (bindings === undefined || !isNamedImports(bindings)) { - return false; + return true; } - // An empty binding list still loads the module, and `every` is vacuously true for it - return bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly); + // esbuild erases empty and all-type binding lists alike, so neither is a load-time edge + return !bindings.elements.every(element => element.isTypeOnly); } -function isTypeOnlyExport(node: ExportDeclaration) { +function exportsAtLoadTime(node: ExportDeclaration) { if (node.isTypeOnly) { - return true; + return false; } const clause = node.exportClause; if (clause === undefined || !isNamedExports(clause)) { - return false; + return true; } - return clause.elements.length > 0 && clause.elements.every(element => element.isTypeOnly); + return !clause.elements.every(element => element.isTypeOnly); } function resolveDeps(pkg: Package, sourceFilename: string, deps: string[]) { diff --git a/src/building/docs.ts b/src/building/docs.ts deleted file mode 100644 index 19f60df..0000000 --- a/src/building/docs.ts +++ /dev/null @@ -1,345 +0,0 @@ -/** - * @license - * Copyright 2022-2026 Greg Lauckhart - * SPDX-License-Identifier: Apache-2.0 - */ - -import { relative } from "node:path"; -import { - Application, - ContainerReflection, - Converter, - DeclarationReflection, - LogLevel, - Reflection, - ReflectionFlag, - ReflectionKind, - TypeDocOptions, -} from "typedoc"; -import { isFile, posixPath } from "../util/file.js"; -import { Package } from "../util/package.js"; -import { Progress } from "../util/progress.js"; -import { Graph } from "./graph.js"; - -// NOTE - this is a "best attempt" at doc generation via typescript. The result is not all that great; typedoc is too -// limited for our complex API so we're going to need something custom to do it right - -const PLUGINS = ["typedoc-github-theme"] as string[]; - -// Double "docs" directories so top-level directory can be a GH repository with pages configured under "docs" -const OUTPUT_PATH = "build/docs/docs"; - -const THEME_OPTIONS = { - themeColor: "#cb9820", - - // TODO - add service links -}; - -/** - * Generate build/docs.json for a single project. - */ -export async function buildDocs(pkg: Package, progress: Progress) { - const modules = pkg.sourceModules; - - // We point typedoc at each of our public exports - const entryPoints = Array(); - - // In a workspace typedoc requires files to be relative to the workspace for merge - const basePath = posixPath(pkg.root.path); - - for (const name in modules) { - const path = modules[name]; - if (path.endsWith(".json")) { - continue; - } - - if (isFile(path)) { - if (!entryPoints.includes(path)) { - entryPoints.push(path); - } - } else { - throw new Error(`No source found for module ${name} path ${modules[name]}`); - } - } - - const app = await createApplication({ - excludeExternals: true, - entryPoints, - tsconfig: pkg.resolve("tsconfig.json"), - basePath, - skipErrorChecking: true, - - // Removes a couple hundred MB of tabs so worth uncommenting at some point - //pretty: false, - - readme: "none", - }); - - instrumentForEntrypointRepair(app, pkg); - - const analysis = new ReflectionAnalysis(); - - app.converter.on(Converter.EVENT_CREATE_DECLARATION, (/*_context, reflection: DeclarationReflection*/) => { - progress.refresh(); - }); - - app.converter.on(Converter.EVENT_RESOLVE, () => { - progress.refresh(); - }); - - const reflection = await loadReflections(app, pkg); - - analyzeReflections(reflection, analysis); - repairReflections(analysis); - - await app.generateJson(reflection, pkg.resolve("build/docs.json")); -} - -function reflectionsPath(pkg: Package) { - const path = pkg.resolve("build/docs.json"); - if (isFile(path)) { - return posixPath(path); - } -} - -/** - * Typedoc is insufficiently configurable to correctly name modules. The official direction is to rename modules using - * the "module" annotation but seems dumb to have to do that when we can compute the correct name. We'd have to edit - * and maintain many files including codegen. - * - * Instead we create a map of typedoc's naive module name to the correct name so we can update names as reflections - * load. - */ -function instrumentForEntrypointRepair(app: Application, pkg: Package) { - const baseGetEntryPoints = app.getEntryPoints.bind(app); - const mapping = naiveToRealNamesFor(pkg); - app.getEntryPoints = () => { - const list = baseGetEntryPoints(); - if (list === undefined) { - return list; - } - - for (const ep of list) { - const realName = mapping[ep.displayName]; - if (realName === undefined) { - throw new Error(`Typedoc naive module name "${ep.displayName}" is unmapped`); - } - ep.displayName = realName; - } - return list; - }; -} - -/** - * Build the map of typedoc's naive name -> real name. - */ -function naiveToRealNamesFor(pkg: Package) { - const modules = pkg.sourceModules; - - // Map of stupid names that typedoc chooses to actual export names - const mapping = {} as Record; - - const packagePrefix = `${pkg.name}/`; - - // In a workspace typedoc requires files to be relative to the workspace for merge - const basePath = posixPath(pkg.root.path); - - for (const name in modules) { - const path = modules[name]; - if (path.endsWith(".json")) { - continue; - } - - const naiveName = posixPath(relative(basePath, path)) - .replace(/.(?:ts|js|mjs|cjs)$/, "") - - // typedoc strips "/index"; this is hard-coded and doesn't respect exports - .replace(/\/index$/, ""); - - if (name.startsWith(packagePrefix)) { - mapping[naiveName] = name.substring(packagePrefix.length); - } else if (name === pkg.name) { - // Flags for merge with parent - mapping[naiveName] = "."; - } else { - mapping[naiveName] = name; - } - } - - return mapping; -} - -/** - * Generate static documentation website. - * - * Supports individual projects or workspaces but not individual projects within workspaces. - */ -export async function mergeDocs(pkg: Package) { - const entryPoints = Array(); - - const root = pkg.root; - - if (root.isWorkspace) { - // Workspace - const graph = await Graph.load(root); - if (graph === undefined) { - // Shouldn't happen - throw new Error(`Internal: ${pkg.name} has workspaces but no graph`); - } - for (const node of graph.nodes) { - const path = reflectionsPath(node.pkg); - if (path) { - entryPoints.push(path); - } - } - if (!entryPoints.length) { - throw new Error(`No reflections detected in workspace ${pkg.name}`); - } - } else { - // Standalone package - const path = reflectionsPath(pkg); - if (path === undefined) { - throw new Error(`No reflections present for ${pkg.name}`); - } - entryPoints.push(path); - } - - const app = await createApplication({ - entryPoints, - entryPointStrategy: "merge", - ...THEME_OPTIONS, - }); - - const reflection = await loadReflections(app, pkg); - - await app.generateDocs(reflection, pkg.resolve(OUTPUT_PATH)); -} - -/** - * Create a typedoc "application" which is the interface for extracting types and generating docs. - */ -async function createApplication(options: Partial) { - return await Application.bootstrapWithPlugins({ - plugin: PLUGINS, - logLevel: LogLevel.Warn, - ...options, - }); -} - -/** - * Perform "conversion" which is typedoc terminology for executing the operation defined by the application. - */ -async function loadReflections(app: Application, pkg: Package) { - const reflection = await app.convert(); - if (reflection === undefined) { - throw new Error(`Project ${pkg.path} produced no documentation`); - } - return reflection; -} - -class ReflectionAnalysis { - // TODO - need to populate this during analysis so we can warn; will replace typedoc-plugin-missing-exports - unexportedReference = new Set(); - - mergeWithParent = new Set(); - mergeInto = new Map(); -} - -/** - * Transform reflections to improve final output. - */ -function analyzeReflections(node: Reflection, analysis: ReflectionAnalysis) { - visit(node, subject => { - // TODO - populate analysis.unexporedReference (or perhaps just print warning) - - const children = childrenOf(subject); - const mergeable = {} as Record; - const namespaces = new Set(); - for (const child of children) { - if (child.kindOf(ReflectionKind.ClassOrInterface | ReflectionKind.Function)) { - mergeable[child.name] = child; - } else if (child.kindOf(ReflectionKind.Namespace)) { - namespaces.add(child); - } - } - - for (const ns of namespaces) { - const mergeInto = mergeable[ns.name]; - if (mergeInto) { - analysis.mergeInto.set(ns, mergeInto); - } - } - }); -} - -function repairReflections(analysis: ReflectionAnalysis) { - for (const toMerge of analysis.mergeWithParent) { - if (toMerge.parent instanceof DeclarationReflection) { - mergeInto(toMerge, toMerge.parent); - } - } - for (const [toMerge, into] of analysis.mergeInto.entries()) { - mergeInto(toMerge, into); - } -} - -function moveNode(subject: DeclarationReflection, into: ContainerReflection) { - const from = subject.parent as ContainerReflection; - - // No actual movement - if (from === into) { - return; - } - - // Remove from old parent - if (from) { - from.removeChild(subject); - } - - // Add to new parent - into.addChild(subject); - subject.parent = into; - - // When merging a namespace into a type tweak modifier and kind appropriately (a bit of guessing here; might need - // tweaking) - if ( - from.kindOf(ReflectionKind.Namespace) && - into.kindOf(ReflectionKind.ClassOrInterface) && - subject.kindOf(ReflectionKind.SomeValue) - ) { - subject.flags.setFlag(ReflectionFlag.Static, true); - if (subject.kindOf(ReflectionKind.Variable)) { - subject.kind = ReflectionKind.Property; - } - } -} - -function mergeInto(from: DeclarationReflection, into: ContainerReflection) { - const children = childrenOf(from); - while (children.length) { - moveNode(children[0], into); - } - (from.parent as ContainerReflection)?.removeChild(from); - // TODO - remap name -} - -function childrenOf(node: Reflection) { - if (node instanceof ContainerReflection) { - if (node.children === undefined) { - node.children = []; - } - return node.children; - } - return []; -} - -/** - * Visit all nodes in a reflection tree. Reflection has a "visit" method but it's just a branch on type. - */ -function visit(node: Reflection, visitor: (node: Reflection) => void) { - visitor(node); - - for (const child of childrenOf(node)) { - visit(child, visitor); - } -} diff --git a/src/building/graph.ts b/src/building/graph.ts index 80be4e8..5f5529c 100644 --- a/src/building/graph.ts +++ b/src/building/graph.ts @@ -88,12 +88,7 @@ export class Graph { return; } - if (builder.tsgo) { - await this.#buildWithTsgo(builder); - } else { - await this.#prebuild(builder); - await this.#buildSequential(builder); - } + await this.#buildWithTsgo(builder); } async #prebuild(builder: ProjectBuilder, progress?: Progress) { @@ -133,33 +128,6 @@ export class Graph { } } - async #buildSequential(builder: ProjectBuilder) { - const toBuild = new Set(this.nodes); - - while (toBuild.size) { - let node; - - nodes: for (node of toBuild) { - for (const dep of node.dependencies) { - if (dep.isDirty) { - continue nodes; - } - } - break; - } - - if (!node) { - throw new Error("Internal logic error: No unbuilt project has fully built dependencies"); - } - if (node.isDirty || builder.unconditional) { - await builder.build(node.project); - node.info.timestamp = new Date().toISOString(); - } - - toBuild.delete(node); - } - } - async #buildWithTsgo(builder: ProjectBuilder) { const dirtyNodes = this.nodes.filter(node => node.isDirty || builder.unconditional); diff --git a/src/building/project-builder.ts b/src/building/project-builder.ts index 6c9fb92..370d738 100644 --- a/src/building/project-builder.ts +++ b/src/building/project-builder.ts @@ -8,9 +8,7 @@ import { Progress } from "../util/progress.js"; import { BuildError } from "./error.js"; import { Graph } from "./graph.js"; import { BuildInformation, Project } from "./project.js"; -import { createTsgoContext, createTypescriptContext } from "./typescript.js"; -import { TypescriptContext } from "./typescript/context.js"; -import { copyDeclarationsToCjs } from "./typescript/tsgo.js"; +import { copyDeclarationsToCjs, createTsgoContext, TypescriptContext } from "./typescript/tsgo.js"; export enum Target { clean = "clean", @@ -23,7 +21,6 @@ export interface Options { targets?: Target[]; clean?: boolean; graph?: Graph; - tsgo?: boolean; } /** @@ -35,7 +32,6 @@ export class ProjectBuilder { unconditional: boolean; tsContext?: TypescriptContext; graph?: Graph; - tsgo?: boolean; /** * When true, type checking has already been performed by a batched tsgo -b invocation. {@link #doBuild} skips @@ -61,7 +57,6 @@ export class ProjectBuilder { this.graph = options.graph; this.unconditional = options.clean || (options.targets !== undefined && options.targets?.indexOf(Target.clean) !== -1); - this.tsgo = options.tsgo ?? process.env.NACHO_TSGO !== "0"; } get hasClean() { @@ -182,14 +177,9 @@ export class ProjectBuilder { } } else { try { - // Obtain or initialize typescript solution builder let context = this.tsContext; if (context === undefined) { - if (this.tsgo) { - context = createTsgoContext(project.pkg.workspace); - } else { - context = await createTypescriptContext(project.pkg.workspace, graph); - } + context = createTsgoContext(project.pkg.workspace); this.tsContext = context; } diff --git a/src/building/typescript.ts b/src/building/typescript.ts deleted file mode 100644 index 01ca3bb..0000000 --- a/src/building/typescript.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license - * Copyright 2022-2026 Greg Lauckhart - * SPDX-License-Identifier: Apache-2.0 - */ - -import { type TypescriptContext } from "./typescript/context.js"; -import { createSolutionBuilderContext } from "./typescript/solution-builder.js"; -export { copyDeclarationsToCjs, createTsgoContext, tsgoSolutionBuild } from "./typescript/tsgo.js"; - -/** - * Create a {@link TypescriptContext} for validating types and optionally emitting .d.ts file. - * - * We have multiple implementations as we've tried TS API variants. We export the one we currently use here. - */ -export const createTypescriptContext = createSolutionBuilderContext; diff --git a/src/building/typescript/context.ts b/src/building/typescript/context.ts deleted file mode 100644 index 76c2b05..0000000 --- a/src/building/typescript/context.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * Copyright 2022-2026 Greg Lauckhart - * SPDX-License-Identifier: Apache-2.0 - */ - -import { dirname } from "node:path"; -import { - createCompilerHost, - Diagnostic, - formatDiagnosticsWithColorAndContext, - parseJsonConfigFileContent, - readConfigFile, - sys, -} from "typescript"; -import { Package } from "../../util/package.js"; -import { BuildError } from "../error.js"; - -export interface TypescriptContext { - build(pkg: Package, path: string, refreshCallback: () => void, emit?: boolean): Promise; -} - -export namespace TypescriptContext { - export function diagnose(diagnostics?: Diagnostic | Diagnostic[]) { - if (diagnostics === undefined) { - return; - } - if (Array.isArray(diagnostics)) { - if (!diagnostics.length) { - return; - } - } else { - diagnostics = [diagnostics]; - } - - const host = createCompilerHost({}); - - let formatted = formatDiagnosticsWithColorAndContext(diagnostics, host); - - // Strangely there are not newlines between errors in this output like there is when you run tsc from the - // command line. Use the "light blue" ANSI escape code as an injection point for an additional newline - // - // oxlint-disable-next-line no-control-regex - formatted = formatted.replace(/\u001b\[96m/g, "\n\u001b[96m"); - - throw new BuildError(formatted); - } - - export function compilerOptionsFor(path: string, ignoreErrors = false) { - const file = readConfigFile(path, sys.readFile); - diagnose(file.error); - const config = parseJsonConfigFileContent(file.config, sys, dirname(path)); - if (!ignoreErrors) { - diagnose(config.errors); - } - return config.options; - } - - export function instrumentHostForSpinner(host: {}, refreshCallback?: () => void) { - if (!refreshCallback) { - return; - } - - // The refresh callback allows us to make spinner updates even though TS is synchronous. - // - // TODO - add interception points as spinner still hangs - - for (const name of ["readFile", "writeFile", "getSourceFile"]) { - if (!(name in host)) { - continue; - } - - const fn = (host as Record unknown>)[name]; - if (typeof fn !== "function") { - continue; - } - - (host as Record unknown>)[name] = (...args: any) => { - const result = fn.apply(host, args); - refreshCallback(); - return result; - }; - } - } -} diff --git a/src/building/typescript/incremental-compiler.ts b/src/building/typescript/incremental-compiler.ts deleted file mode 100644 index 326eab3..0000000 --- a/src/building/typescript/incremental-compiler.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2022-2026 Greg Lauckhart - * SPDX-License-Identifier: Apache-2.0 - */ - -import { existsSync } from "node:fs"; -import { CompilerOptions, createIncrementalCompilerHost, createIncrementalProgram } from "typescript"; -import { Package } from "../../util/package.js"; -import { InternalBuildError } from "../error.js"; -import { Graph } from "../graph.js"; -import { TypescriptContext } from "./context.js"; - -// TODO - if we ever move back to this we need to copy type files which was previously handled separately -export function createIncrementalCompilerContext(_workspace: Package, _graph: Graph | undefined): TypescriptContext { - const baseOptions = { - ...TypescriptContext.compilerOptionsFor(Package.tools.resolve("tsc/tsconfig.base.json")), - - incremental: true, - isolatedModules: true, - skipLibCheck: true, - }; - - return { build }; - - async function build(pkg: Package, path: string, refreshCallback: () => void, emit?: boolean) { - let options; - if (emit) { - options = { - outDir: pkg.resolve("build/types"), - emitDeclarationOnly: true, - sourceMap: true, - declarationMap: true, - }; - } else { - options = { - noEmit: true, - }; - } - - options = { - ...baseOptions, - tsBuildInfoFile: pkg.resolve("build/tsbuildinfo"), - rootDir: pkg.path, - ...options, - }; - - delete options.composite; - - if (pkg.hasSrc) { - loadConfiguredOptions(pkg.resolve("src/tsconfig.json"), options); - } - - if (pkg.hasTests) { - loadConfiguredOptions(pkg.resolve("src/tsconfig.json"), options); - } - - const host = createIncrementalCompilerHost(options); - - TypescriptContext.instrumentHostForSpinner(host, refreshCallback); - const sources = Array(); - - if (path === "src") { - if (pkg.hasSrc) { - sources.push(...(await pkg.glob("src/**/*.ts"))); - } - } else if (path === "test") { - if (pkg.hasTests) { - sources.push(...(await pkg.glob("test/**/*.ts"))); - } - } else { - throw new InternalBuildError(`Unsupported build path "${path}"`); - } - - const program = createIncrementalProgram({ - rootNames: sources, - options, - host, - }); - - // See https://github.com/microsoft/TypeScript/issues/31849 - const diagnostics = [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getSyntacticDiagnostics(), - ...program.getOptionsDiagnostics(), - ...program.getSemanticDiagnostics(), - ]; - - if (!options.noEmit) { - diagnostics.push(...program.emit().diagnostics); - } - - TypescriptContext.diagnose(diagnostics); - } - - /** - * As we largely configure based on convention, we mostly ignore tsconfig.json files in project directories. The - * limited number of project-specific options we allow load here. - */ - function loadConfiguredOptions(path: string, into: CompilerOptions) { - if (!existsSync(path)) { - // Package tsconfigs are optional - return; - } - - const options = TypescriptContext.compilerOptionsFor(path); - - delete options?.composite; - - const types = options?.types; - if (types) { - if (into.types) { - const merged = new Set(into.types); - for (const type of types) { - merged.add(type); - } - into.types = [...merged]; - } else { - into.types = types; - } - } - } -} diff --git a/src/building/typescript/solution-builder.ts b/src/building/typescript/solution-builder.ts deleted file mode 100644 index 3a97ee1..0000000 --- a/src/building/typescript/solution-builder.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @license - * Copyright 2022-2026 Greg Lauckhart - * SPDX-License-Identifier: Apache-2.0 - */ - -import { existsSync } from "node:fs"; -import { join } from "node:path"; -import { - CompilerHost, - createSolutionBuilder, - createSolutionBuilderHost, - Diagnostic, - EmitAndSemanticDiagnosticsBuilderProgram, - ExitStatus, - SolutionBuilderHost, - sys, -} from "typescript"; -import { Package } from "../../util/package.js"; -import { BuildError, InternalBuildError } from "../error.js"; -import { Graph } from "../graph.js"; -import { TypescriptContext } from "./context.js"; - -const BUILD_DIAGNOSTICS = false; - -interface PackageBuildContext { - fileWrite?: (writeFile: typeof sys.writeFile, filename: string, contents: string, writeBom?: boolean) => void; - refreshCallback(): void; -} - -export async function createSolutionBuilderContext( - workspace: Package, - graph: Graph | undefined, -): Promise { - let diagnostics: undefined | Diagnostic[]; - const host = createSolutionBuilderHost(); - host.reportDiagnostic = diagnostic => (diagnostics ??= []).push(diagnostic); - let context: undefined | PackageBuildContext; - - function currentContext() { - if (context === undefined) { - throw new Error("Build context accessed with no context"); - } - return context; - } - - instrumentHostWithInterceptors(host, currentContext, graph); - - const builder = createSolutionBuilder(host, [workspace.resolve("tsconfig.json")], {}); - - TypescriptContext.diagnose(diagnostics); - - return { - async build(pkg: Package, path: string, refreshCallback: () => void, _emit?: boolean) { - context = { - refreshCallback, - }; - - if (path === "src" && pkg.isLibrary && pkg.supportsEsm && pkg.supportsCjs) { - const esmDeclIdentifier = new RegExp( - `^(${pkg.resolve("dist").replace(/[\\/]/g, "[\\/]")}[\\/](?:.*))esm([\\/].*(?:\\.d\\.ts|\\.d\\.ts\\.map))$`, - ); - context.fileWrite = (writeFile, filename, contents, writeBom) => { - const match = filename.match(esmDeclIdentifier); - if (!match) { - return; - } - writeFile(`${match[1]}cjs${match[2]}`, contents, writeBom); - }; - } - - try { - const status = builder.build(pkg.resolve(join(path, "tsconfig.json"))); - - TypescriptContext.diagnose(diagnostics); - - switch (status) { - case undefined: - case ExitStatus.Success: - break; - - case ExitStatus.InvalidProject_OutputsSkipped: - throw new BuildError( - `Typescript does not think ${path} is part of the project, this is probably a build tooling error`, - ); - - case ExitStatus.ProjectReferenceCycle_OutputsSkipped: - throw new BuildError( - `Typescript identified a cyclical project reference in ${path}, please check package dependencies`, - ); - - default: - throw new InternalBuildError(`Build error ${ExitStatus[status]} without diagnostics`); - } - } finally { - context = undefined; - } - }, - }; -} - -/** - * Adds interception points for mucking around in TS internals. - */ -function instrumentHostWithInterceptors( - host: SolutionBuilderHost, - currentContext: () => PackageBuildContext, - graph?: Graph, -) { - // Matches any file that may be the type definitions for one of our .ts files - const moduleIdentifier = graph - ? new RegExp(`^(${graph.nodes.map(node => node.pkg.path).join("|")})[\\/]dist[\\/](?:esm|cjs)/(.*).d.ts$`) - : undefined; - - // Instrument write to update spinner and bifurcate type definition writes to CJS if project includes both CJS and - // ESM - const writeFile = host.writeFile ?? sys.writeFile; - host.writeFile = (path, data, writeBom) => { - const context = currentContext(); - context.refreshCallback(); - writeFile(path, data, writeBom); - context.fileWrite?.(writeFile, path, data, writeBom); - }; - - // Intercepts for diagnostics and to update spinner - let resolveModuleNameLiterals: CompilerHost["resolveModuleNameLiterals"] | undefined; - const resolveModuleNameLiteralsInterceptor: CompilerHost["resolveModuleNameLiterals"] = (...args) => { - currentContext().refreshCallback(); - - const result = resolveModuleNameLiterals!(...args); - if (!BUILD_DIAGNOSTICS) { - return result; - } - - for (const entry of result) { - const filename = entry.resolvedModule?.resolvedFileName; - const match = moduleIdentifier && filename?.match(moduleIdentifier); - if (match) { - const sourceFilename = `${match[1]}/src/${match[2]}.ts`; - if (existsSync(sourceFilename)) { - console.warn(".d.ts reference", sourceFilename); - } - } - } - - return result; - }; - - // SolutionBuilder design doesn't allow us to access "real" resolveModuleNameLiterals because it's undefined here - // and only created in createSolutionBuilder when it creates the CompilerHost. So instead inject when the - // CompilerHost is passed to SolutionBuilderHost#createProgram - const createProgram = host.createProgram; - host.createProgram = (...args) => { - const compilerHost = args[2]; - - // Instrument compiler host for spinner + diagnostics - if (compilerHost && compilerHost.resolveModuleNameLiterals !== resolveModuleNameLiteralsInterceptor) { - resolveModuleNameLiterals = compilerHost.resolveModuleNameLiterals; - compilerHost.resolveModuleNameLiterals = resolveModuleNameLiteralsInterceptor; - } - - const result = createProgram(...args); - - // TS reads compiler options in a long tight loop where we otherwise never would update the spinner - const getCompilerOptions = result.getCompilerOptions; - result.getCompilerOptions = (...args) => { - currentContext().refreshCallback(); - return getCompilerOptions(...args); - }; - - return result; - }; -} diff --git a/src/building/typescript/tsgo.ts b/src/building/typescript/tsgo.ts index 98df588..545673b 100644 --- a/src/building/typescript/tsgo.ts +++ b/src/building/typescript/tsgo.ts @@ -10,12 +10,13 @@ import { join, resolve } from "path"; import { isDirectory } from "../../util/file.js"; import { Package } from "../../util/package.js"; import { BuildError } from "../error.js"; -import { TypescriptContext } from "./context.js"; -// The native compiler ships as typescript@7, aliased to "typescript7" because node_modules/typescript must remain -// typescript@6 — typedoc and our own JS-API code require the classic API that 7.x dropped +export interface TypescriptContext { + build(pkg: Package, path: string, refreshCallback: () => void, emit?: boolean): Promise; +} + function tsgoBin(_workspace: Package) { - return Package.tools.findPackage("typescript7").resolve("lib/tsc.js"); + return Package.tools.findPackage("typescript").resolve("lib/tsc.js"); } export function createTsgoContext(workspace: Package): TypescriptContext { diff --git a/tests/consumer-ts6/verify.sh b/tests/consumer-ts6/verify.sh index 46d3a0d..6e6a7f1 100755 --- a/tests/consumer-ts6/verify.sh +++ b/tests/consumer-ts6/verify.sh @@ -25,6 +25,5 @@ do done # A transitive package that binds to the consumer's typescript crashes when its module loads, which happens for every -# command regardless of what the command does. Exercise the ones that pull in third-party TypeScript consumers. +# command regardless of what the command does. Exercise the one that pulls in the TypeScript API. nacho-build cycles >/dev/null -nacho-build docs >/dev/null diff --git a/tests/consumer-ts7/verify.sh b/tests/consumer-ts7/verify.sh index 44708cb..7025e1e 100755 --- a/tests/consumer-ts7/verify.sh +++ b/tests/consumer-ts7/verify.sh @@ -25,6 +25,5 @@ do done # A transitive package that binds to the consumer's typescript crashes when its module loads, which happens for every -# command regardless of what the command does. Exercise the ones that pull in third-party TypeScript consumers. +# command regardless of what the command does. Exercise the one that pulls in the TypeScript API. nacho-build cycles >/dev/null -nacho-build docs >/dev/null diff --git a/tests/cycles/verify.sh b/tests/cycles/verify.sh index f6f302b..d9e1690 100755 --- a/tests/cycles/verify.sh +++ b/tests/cycles/verify.sh @@ -16,15 +16,15 @@ grep -q "Cycles detected:" "$log" || fail "expected a cycle report" # value-a ⇄ value-b via a plain import and an `export * from` re-export # mixed-g ⇄ mixed-h via an import mixing type and value bindings -# empty-i ⇄ empty-j via `import {} from`, which still evaluates the module -for f in value-a value-b mixed-g mixed-h empty-i empty-j; do +for f in value-a value-b mixed-g mixed-h; do grep -q "$f" "$log" || fail "expected $f in the cycle report" done # `import type` edges are not real dependencies, so type-c ⇄ type-d is not a cycle # `export type ... from` is likewise erased, so texp-k ⇄ texp-l and texp-m ⇄ texp-n are not cycles # `import()` is lazy, so lazy-e ⇄ lazy-f is not a cycle -for f in type-c type-d texp-k texp-l texp-m texp-n lazy-e lazy-f; do +# `import {} from` is erased by esbuild, so empty-i ⇄ empty-j is not a cycle in the emitted JS +for f in type-c type-d texp-k texp-l texp-m texp-n lazy-e lazy-f empty-i empty-j; do if grep -q "$f" "$log"; then fail "$f must not be reported as a cycle" fi From 11e19f8d69660a700000c45758cd31c3b009a47e Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Wed, 5 Aug 2026 16:59:03 +0200 Subject: [PATCH 7/8] fix: record build info only after deferred transpile work succeeds In the batched tsgo flow #doBuild only enqueues transpile/CJS-copy work, but still wrote build/info.json immediately. When flushWork then failed, the package was already marked built and the next run silently skipped it, leaving broken dist output in place. Build info for deferred work is now persisted by flushWork after all work items complete. Co-Authored-By: Claude Fable 5 --- src/building/graph.ts | 1 - src/building/project-builder.ts | 24 +++++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/building/graph.ts b/src/building/graph.ts index 5f5529c..815331a 100644 --- a/src/building/graph.ts +++ b/src/building/graph.ts @@ -213,7 +213,6 @@ export class Graph { for (const node of dirtyNodes) { await builder.build(node.project, progress); - node.info.timestamp = new Date().toISOString(); } await progress.run("Transpile", () => builder.flushWork()); diff --git a/src/building/project-builder.ts b/src/building/project-builder.ts index 370d738..446233a 100644 --- a/src/building/project-builder.ts +++ b/src/building/project-builder.ts @@ -45,12 +45,26 @@ export class ProjectBuilder { */ #work = Array<() => Promise>(); + /** + * Build info for packages whose enqueued work has not run yet. Persisting it before {@link flushWork} succeeds + * would mark a package as built even when its transpile step then fails, so the next build would skip it. + */ + #pendingInfo = Array<{ project: Project; info: BuildInformation; node?: Graph.Node }>(); + /** * Execute all enqueued work with a concurrency limit and clear the queue. */ async flushWork() { await parallel(this.#work); this.#work = []; + + for (const { project, info, node } of this.#pendingInfo) { + await project.recordBuildInfo(info); + if (node) { + node.info = info; + } + } + this.#pendingInfo = []; } constructor(private options: Options = {}) { @@ -240,9 +254,13 @@ export class ProjectBuilder { // Only update build information when there are no explicit targets so we know it's a full build if (!this.options.targets?.length) { - await project.recordBuildInfo(info); - if (node) { - node.info = info; + if (this.typesPrebuilt) { + this.#pendingInfo.push({ project, info, node }); + } else { + await project.recordBuildInfo(info); + if (node) { + node.info = info; + } } } } From 1d8fc65bf58d619b45eddd0fe572176549d30666 Mon Sep 17 00:00:00 2001 From: Ingo Fischer Date: Wed, 5 Aug 2026 16:59:56 +0200 Subject: [PATCH 8/8] fix: map tsgo diagnostics to packages by path prefix The parser assumed every package sits exactly two path segments below the workspace root, so single-segment or nested workspace entries never matched a real package: their errors were dropped from the per-package report and, on exit code 2, the package was not excluded from transpile and shipped stale declarations. Diagnostics now match the longest actual package path prefix. Co-Authored-By: Claude Fable 5 --- src/building/graph.ts | 6 +++++- src/building/typescript/tsgo.ts | 31 +++++++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/building/graph.ts b/src/building/graph.ts index 815331a..3cece0c 100644 --- a/src/building/graph.ts +++ b/src/building/graph.ts @@ -151,7 +151,11 @@ export class Graph { progress.update("Type check"); let result: TsgoResult; try { - result = await tsgoSolutionBuild(workspace, tsconfigPath); + result = await tsgoSolutionBuild( + workspace, + tsconfigPath, + this.nodes.map(node => node.pkg.path), + ); } catch (e) { progress.failure("Type check"); throw e; diff --git a/src/building/typescript/tsgo.ts b/src/building/typescript/tsgo.ts index 545673b..f1ab3a2 100644 --- a/src/building/typescript/tsgo.ts +++ b/src/building/typescript/tsgo.ts @@ -6,7 +6,7 @@ import { spawn } from "node:child_process"; import { cp } from "node:fs/promises"; -import { join, resolve } from "path"; +import { join, resolve, sep } from "path"; import { isDirectory } from "../../util/file.js"; import { Package } from "../../util/package.js"; import { BuildError } from "../error.js"; @@ -98,13 +98,16 @@ export interface TsgoResult { /** * Run tsgo in solution build mode (-b) for the entire workspace or a scoped set of packages. * - * Captures diagnostics and maps them to packages by file path. Does not throw on type errors — the caller decides how - * to handle failures. + * Captures diagnostics and maps them to the packages in {@link packagePaths} by file path. Does not throw on type + * errors — the caller decides how to handle failures. */ -export async function tsgoSolutionBuild(workspace: Package, tsconfigPath: string): Promise { +export async function tsgoSolutionBuild( + workspace: Package, + tsconfigPath: string, + packagePaths: string[], +): Promise { const bin = tsgoBin(workspace); const args = [bin, "-b", tsconfigPath]; - const workspacePath = workspace.path; // tsgo sends diagnostics to stdout const { code, stdout } = await new Promise<{ code: number; stdout: string }>((resolve, reject) => { @@ -141,21 +144,17 @@ export async function tsgoSolutionBuild(workspace: Package, tsconfigPath: string } const filePath = resolve(match[1]); - const relative = filePath.startsWith(workspacePath + "/") - ? filePath.slice(workspacePath.length + 1) - : undefined; - if (relative === undefined) { - continue; + // Packages may nest, so the longest matching path wins + let pkgPath: string | undefined; + for (const path of packagePaths) { + if (filePath.startsWith(path + sep) && (pkgPath === undefined || path.length > pkgPath.length)) { + pkgPath = path; + } } - - // Extract the package directory — e.g. "packages/protocol/src/foo.ts" → "packages/protocol" - const parts = relative.split("/"); - if (parts.length < 3) { + if (pkgPath === undefined) { continue; } - const pkgDir = `${parts[0]}/${parts[1]}`; - const pkgPath = join(workspacePath, pkgDir); const existing = errorsByPackage.get(pkgPath); errorsByPackage.set(pkgPath, existing ? `${existing}\n${line}` : line);