diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46fae80..f794535 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 @@ -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" 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 961b108..c0e63b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,26 +9,19 @@ "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", - "typedoc-github-theme": "^0.4.0", - "typescript": "~6.0.2" + "minimatch": "^10.2.6", + "type-fest": "^5.8.0", + "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 +30,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", @@ -499,303 +446,308 @@ "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==", + "node_modules/@types/node": { + "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": { - "@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" + "undici-types": "~8.3.0" } }, - "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/@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": ">=16.20.0" } }, - "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/@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": ">=16.20.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/@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": ">=16.20.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/@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": ">=16.20.0" } }, - "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.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" + "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": ">=16.20.0" } }, - "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/@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", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" } }, - "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" + "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": [ + "linux" + ], + "engines": { + "node": ">=16.20.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-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-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": [ + "loong64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "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-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": [ + "mips64el" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "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-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": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "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-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, + "os": [ + "linux" + ], "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-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": "^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-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" + ], "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-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": [ - "darwin" + "netbsd" ], "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-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": [ - "darwin" + "netbsd" ], "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-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": [ - "arm" + "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { "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-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": [ - "arm64" + "x64" ], "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { "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-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": [ - "linux" + "sunos" ], "engines": { "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-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 +760,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" ], @@ -833,21 +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/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 +795,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,52 +815,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", - "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", @@ -965,96 +856,13 @@ "@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==", - "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.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "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.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "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,57 +871,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", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "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 +883,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" @@ -1169,81 +898,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typedoc": { - "version": "0.28.19", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.19.tgz", - "integrity": "sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==", - "license": "Apache-2.0", - "dependencies": { - "@gerrit0/mini-shiki": "^3.23.0", - "lunr": "^2.3.9", - "markdown-it": "^14.1.1", - "minimatch": "^10.2.5", - "yaml": "^2.8.3" - }, - "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==", + "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", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "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", - "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.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "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 c25678f..2cf9805 100644 --- a/package.json +++ b/package.json @@ -71,31 +71,23 @@ }, "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", - "typedoc-github-theme": "^0.4.0", - "typescript": "~6.0.2" + "minimatch": "^10.2.6", + "type-fest": "^5.8.0", + "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/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 72f16bc..07267a4 100644 --- a/src/building/cycles.ts +++ b/src/building/cycles.ts @@ -6,34 +6,56 @@ const MAX_CYCLES = 1000; -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 { + ExportDeclaration, + Expression, + ImportDeclaration, + isExportDeclaration, + isExternalModuleReference, + isImportDeclaration, + isImportEqualsDeclaration, + isNamedExports, + isNamedImports, + isStringLiteral, + Node, + 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"); - const fileDeps = detective(contents, { - skipTypeImports: true, - skipAsyncImports: true, - }); - deps[filename] = resolveDeps(pkg, filename, fileDeps); + { + 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, []); @@ -99,6 +121,82 @@ function printCycles(pkg: Package, cycles: string[][]) { } } +/** + * 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(source: SourceFile) { + const specifiers = Array(); + + function addSpecifier(node?: Expression) { + if (node !== undefined && isStringLiteral(node)) { + specifiers.push(node.text); + } + } + + function visit(node: Node) { + if (isImportDeclaration(node)) { + if (importsAtLoadTime(node)) { + addSpecifier(node.moduleSpecifier); + } + } else if (isExportDeclaration(node)) { + if (exportsAtLoadTime(node)) { + addSpecifier(node.moduleSpecifier); + } + } else if ( + isImportEqualsDeclaration(node) && + !node.isTypeOnly && + isExternalModuleReference(node.moduleReference) + ) { + addSpecifier(node.moduleReference.expression); + } + + node.forEachChild(visit); + } + + source.forEachChild(visit); + + return specifiers; +} + +function importsAtLoadTime(node: ImportDeclaration) { + const clause = node.importClause; + if (clause === undefined) { + // Bare `import "x"` exists only for its side effects + return true; + } + + // `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 true; + } + + // esbuild erases empty and all-type binding lists alike, so neither is a load-time edge + return !bindings.elements.every(element => element.isTypeOnly); +} + +function exportsAtLoadTime(node: ExportDeclaration) { + if (node.isTypeOnly) { + return false; + } + + const clause = node.exportClause; + if (clause === undefined || !isNamedExports(clause)) { + return true; + } + + return !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/docs.ts b/src/building/docs.ts deleted file mode 100644 index cf8a839..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 } 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 = 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 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 = pkg.root.path; - - for (const name in modules) { - const path = modules[name]; - if (path.endsWith(".json")) { - continue; - } - - const naiveName = 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..3cece0c 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); @@ -183,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; @@ -245,7 +217,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 6c9fb92..446233a 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 @@ -49,19 +45,32 @@ 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 = {}) { 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 +191,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; } @@ -250,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; + } } } } 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/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 72d29e6..f1ab3a2 100644 --- a/src/building/typescript/tsgo.ts +++ b/src/building/typescript/tsgo.ts @@ -6,14 +6,17 @@ 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"; -import { TypescriptContext } from "./context.js"; + +export interface TypescriptContext { + build(pkg: Package, path: string, refreshCallback: () => void, emit?: boolean): Promise; +} function tsgoBin(_workspace: Package) { - return Package.tools.findPackage("@typescript/native-preview").resolve("bin/tsgo.js"); + return Package.tools.findPackage("typescript").resolve("lib/tsc.js"); } export function createTsgoContext(workspace: Package): TypescriptContext { @@ -95,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) => { @@ -138,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); 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/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 80a478c..370937a 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"; @@ -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,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 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/"); } @@ -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("*")) { 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..6e6a7f1 --- /dev/null +++ b/tests/consumer-ts6/verify.sh @@ -0,0 +1,29 @@ +#!/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 one that pulls in the TypeScript API. +nacho-build cycles >/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..7025e1e --- /dev/null +++ b/tests/consumer-ts7/verify.sh @@ -0,0 +1,29 @@ +#!/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 one that pulls in the TypeScript API. +nacho-build cycles >/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..d9e1690 --- /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 +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 +# `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 +done