From 56feb85c97d3ebc1e15668b18c2cfb243ec01efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Thu, 17 Sep 2026 22:16:00 +0100 Subject: [PATCH] chore: compile first-party packages with TypeScript 7 (#3311) * fix(web): drop TypeScript 7-incompatible tsconfig paths Remove baseUrl and the dead ../shared include so web project references typecheck under TypeScript 7. Co-authored-by: Cursor * test(cli): parse TypeScript with a TypeScript 6 API package Keep AST guards working after the named typescript package becomes 7, which no longer ships the Compiler API. Co-authored-by: Cursor * chore(lint): pin root TypeScript to the 6 API package Give typescript-eslint a TypeScript 6 peer so syntax-only lint still installs after CLI and web move to TypeScript 7. Co-authored-by: Cursor * chore(deps): compile first-party packages with TypeScript 7.0.2 Unify CLI and web on the same native compiler line as gitnexus-shared so typecheck and emit no longer split 5.x versus 7.x. Co-authored-by: Cursor * docs(ci): describe parent TypeScript 7 as the shared compiler Stop saying web compiles shared with TypeScript 5 now that the parent lockfile is 7. Co-authored-by: Cursor * fix(ci): compile shared from parent TypeScript on Vercel and skill-evolution Stop isolated npm installs in gitnexus-shared so those paths do not pull a second TypeScript 7 optional-platform tree. Co-authored-by: Cursor * docs: record TypeScript 7 typecheck and Dependabot major-split policy Keep contributor typecheck commands, and stop Dependabot from bumping shared onto a different TypeScript major than CLI and web. Co-authored-by: Cursor * fix(lint): pin root TypeScript to 5.9 so npm ci satisfies eslint peers typescript-eslint 8 peers typescript below 6.0.0, so the typescript6 alias made quality lint npm ci fail with ERESOLVE. Co-authored-by: Cursor * test(cli): drop the TypeScript 6 Compiler API package TypeScript 7.0 has no classic createProgram surface, so parse-only guards now use Babel and Mode 4 uses the TypeScript 7 Checker. Co-authored-by: Cursor * docs: align contributor setup with parent TypeScript 7 compile Stop telling clones to npm-install gitnexus-shared; CI and Vercel already emit that package from a parent lib/tsc.js shim. Co-authored-by: Cursor * fix(web): typecheck React JSX on TypeScript 7 with explicit DOM libs TypeScript 7 no longer implies DOM or auto-includes @types, so the web app must declare React/JSX settings while Vite keeps plugin-react. Co-authored-by: Cursor * test: pin Vercel --include=dev and share parse-only string helpers Production npm ci omits the web TypeScript unless --include=dev is on that install. Move staticStringValue next to the other Babel walk helpers so CLI help and contract tests share one source. Co-authored-by: Cursor * chore(autofix): apply prettier + eslint fixes via /autofix command --------- Co-authored-by: Gergo Magyar Co-authored-by: Cursor Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/actions/setup-gitnexus-web/action.yml | 4 +- .github/dependabot.yml | 13 + .../workflows/gitnexus-skill-evolution.yml | 18 +- CONTRIBUTING.md | 21 +- DoD.md | 6 +- README.md | 7 +- TESTING.md | 2 + gitnexus-web/package-lock.json | 389 +++++++++++++++++- gitnexus-web/package.json | 2 +- gitnexus-web/test/unit/tsconfig-react.test.ts | 16 + gitnexus-web/tsconfig.app.json | 11 +- gitnexus-web/tsconfig.node.json | 4 + gitnexus-web/vercel.json | 2 +- gitnexus-web/vite.config.ts | 2 +- gitnexus-web/vitest.config.ts | 2 +- gitnexus/package-lock.json | 375 ++++++++++++++++- gitnexus/package.json | 2 +- gitnexus/scripts/build.js | 4 +- gitnexus/test/helpers/ignore-set-source.ts | 64 ++- gitnexus/test/helpers/literal-collectors.ts | 58 ++- .../test/helpers/parse-typescript-source.ts | 162 ++++++++ gitnexus/test/helpers/ts7-ast.ts | 48 +++ gitnexus/test/unit/build-web-optin.test.ts | 44 +- gitnexus/test/unit/cli-index-help.test.ts | 46 +-- .../group/sync-partial-extraction.test.ts | 107 ++--- .../test/unit/query-determinism-guard.test.ts | 98 +++-- .../unit/skill-evolution-workflow.test.ts | 23 +- package-lock.json | 362 +++++++++++----- package.json | 3 +- 29 files changed, 1548 insertions(+), 347 deletions(-) create mode 100644 gitnexus-web/test/unit/tsconfig-react.test.ts create mode 100644 gitnexus/test/helpers/parse-typescript-source.ts create mode 100644 gitnexus/test/helpers/ts7-ast.ts diff --git a/.github/actions/setup-gitnexus-web/action.yml b/.github/actions/setup-gitnexus-web/action.yml index fb734d3ca..36b0b963c 100644 --- a/.github/actions/setup-gitnexus-web/action.yml +++ b/.github/actions/setup-gitnexus-web/action.yml @@ -19,8 +19,8 @@ runs: # Browsers are installed explicitly by e2e. Typecheck only needs types. PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - # Compile shared with the web package's TypeScript 5. Do not npm-ci - # gitnexus-shared (TypeScript 7 optional-platform install, ~7 minutes). + # Compile shared with the web package's TypeScript 7. Do not npm-ci + # gitnexus-shared (a second optional-platform install, ~7 minutes). - name: Build gitnexus-shared # node + lib/tsc.js — same on Windows/macOS/Linux. Do not use .bin/tsc # (tsc.cmd on Windows; execFileSync cannot launch .cmd without a shell). diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 535204a38..9527828ac 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -93,6 +93,9 @@ updates: # The extension version is a separate upstream constant, not derivable # from the core version (see vendor/lbug-fts/manifest.json). - dependency-name: '@ladybugdb/core' + - dependency-name: typescript + update-types: + - version-update:semver-major # gitnexus-web (thin frontend client). - package-ecosystem: npm @@ -111,6 +114,10 @@ updates: labels: - dependencies - frontend + ignore: + - dependency-name: typescript + update-types: + - version-update:semver-major # Shared types package. - package-ecosystem: npm @@ -128,3 +135,9 @@ updates: include: scope labels: - dependencies + ignore: + # Keep shared on the same TypeScript major as CLI/web. A shared-only + # major bump reintroduces a compiler split this repo unified. + - dependency-name: typescript + update-types: + - version-update:semver-major diff --git a/.github/workflows/gitnexus-skill-evolution.yml b/.github/workflows/gitnexus-skill-evolution.yml index 22f0bdfcc..03b3655ca 100644 --- a/.github/workflows/gitnexus-skill-evolution.yml +++ b/.github/workflows/gitnexus-skill-evolution.yml @@ -330,24 +330,20 @@ jobs: set -euo pipefail # The benchmark's task bindings sandbox-copy node_modules from the # monorepo root as well as gitnexus-shared and gitnexus (see the - # sandbox_copy entries in tasks.scenarios.yaml). The two steps below - # install the subpackage trees; the root tree needs its own install - # or capture_task_dependency_binding aborts at task binding on the - # missing root node_modules. + # sandbox_dependencies entries in tasks.scenarios.yaml). gitnexus + # npm ci plus build compiles shared through parent lib/tsc.js; do + # not npm ci gitnexus-shared (second TypeScript 7 optional install). npm ci - - name: Build pinned shared runtime - run: | - set -euo pipefail - npm ci - npm run build - working-directory: gitnexus-shared - - name: Install and build pinned GitNexus runtime run: | set -euo pipefail npm ci npm run build + # Task YAML still mounts gitnexus-shared/node_modules. Keep an empty + # directory so capture_task_dependency_binding does not abort, without + # installing TypeScript 7 inside shared. + mkdir -p ../gitnexus-shared/node_modules working-directory: gitnexus - name: Point the benchmark task repo at the checkout diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23a6ae28c..b2a0c540e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,14 +16,19 @@ This project uses the [PolyForm Noncommercial License 1.0.0](https://polyformpro **Prerequisites:** Node.js — `gitnexus/` requires `^22.18.0 || >=24.11.0` and `gitnexus-web/` requires `^20.19.0 || >=22.12.0` (enforced via the `engines` field in each package). Use `nvm install` to match the local version. 1. Clone the repository. -2. **Shared package:** `cd gitnexus-shared && npm install && npm run build` -3. **CLI / MCP package:** `cd ../gitnexus && npm install && npm run build` -4. **Web UI (if needed):** `cd ../gitnexus-web && npm install` -5. Run tests as described in [TESTING.md](TESTING.md). +2. **CLI / MCP package:** `cd gitnexus && npm install && npm run build` + `prepare` / `scripts/build.js` compiles `gitnexus-shared` with + `node …/typescript/lib/tsc.js` from this package. Do not `npm install` or + `npm ci` inside `gitnexus-shared/` — that is a second TypeScript 7 + optional-platform install and is not what `setup-gitnexus` does. +3. **Web UI (if needed):** `cd gitnexus-web && npm install` + If you skipped step 2, compile shared with the web compiler first: + `cd gitnexus-shared && node ../gitnexus-web/node_modules/typescript/lib/tsc.js` +4. Run tests as described in [TESTING.md](TESTING.md). -The CLI build imports `gitnexus-shared`, so a fresh clone must install and build -the shared package before running `npm install` in `gitnexus/`. This is the same -order used by the repository's `setup-gitnexus` CI action. +The CLI build imports `gitnexus-shared`, so `gitnexus-shared/dist` must exist +before `gitnexus` typecheck. That emit uses a parent package's TypeScript 7 +`lib/tsc.js`, matching `setup-gitnexus`, `setup-gitnexus-web`, and Vercel. ### Containerized development (optional) @@ -70,7 +75,7 @@ Commits within a PR may use any style — only the **merged PR title** shows up ## Before you open a PR - [ ] Tests pass for the packages you touched (`gitnexus` and/or `gitnexus-web`). -- [ ] Typecheck passes: `npx tsc --noEmit` in `gitnexus/` and `npx tsc -b --noEmit` in `gitnexus-web/`. +- [ ] Typecheck passes: `npx tsc --noEmit` in `gitnexus/` and `npx tsc -b --noEmit` in `gitnexus-web/`. Those commands use TypeScript 7. The web app tsconfig lists `lib` `DOM`/`DOM.Iterable` and `jsx: react-jsx` so React/JSX typecheck on 7; Vite/`vitest` keep `@vitejs/plugin-react` with the automatic JSX runtime. Build `gitnexus-shared/dist` first with a parent `lib/tsc.js` (a `gitnexus` install/`npm run build` does this). Repo ESLint stays syntax-only on a TypeScript 5.x peer until typescript-eslint supports 7. - [ ] No secrets, tokens, or machine-specific paths committed. - [ ] Documentation updated if behavior or public CLI/MCP contract changes. - [ ] Every new `GITNEXUS_*` environment variable has a row in the **Environment variables** table in [README.md](README.md) — variable, default, effect, and when to tune it. diff --git a/DoD.md b/DoD.md index a0f6eed08..ff2297903 100644 --- a/DoD.md +++ b/DoD.md @@ -113,7 +113,7 @@ Run the commands relevant to the touched area. If something cannot be run in the ### 4.1 Build ordering -- [ ] `gitnexus-shared/` dist is built before consuming packages are typechecked or tested (CI uses the `setup-gitnexus` action for this — local runs must match). +- [ ] `gitnexus-shared/` dist is built before consuming packages are typechecked or tested (CI uses the `setup-gitnexus` action, which compiles shared with the parent TypeScript 7 `lib/tsc.js` — local runs must match). ### 4.2 If `gitnexus/` changed @@ -123,13 +123,13 @@ Run the commands relevant to the touched area. If something cannot be run in the ### 4.3 If `gitnexus-web/` changed -- [ ] `cd gitnexus-web && npx tsc -b --noEmit` +- [ ] `cd gitnexus-web && npx tsc -b --noEmit` (TypeScript 7 typechecks React/JSX: `jsx: react-jsx`, `lib` includes `DOM`) - [ ] `cd gitnexus-web && npm test` - [ ] `cd gitnexus-web && npm run test:e2e` when browser flows or user-facing UI behavior changed ### 4.4 If `gitnexus-shared/` changed -- [ ] Shared package builds cleanly (`npm run build` in `gitnexus-shared/`) +- [ ] Shared package builds cleanly from a parent TypeScript 7 shim after that parent is installed (`cd gitnexus-shared && node ../gitnexus/node_modules/typescript/lib/tsc.js`, or `node ../gitnexus-web/node_modules/typescript/lib/tsc.js` after a web install). Do not `npm install` / `npm ci` in `gitnexus-shared/` for this check. - [ ] Dependent packages still typecheck and test after the shared change — verify both CLI and web consumers together ### 4.5 If CI workflows or release pipelines changed diff --git a/README.md b/README.md index 80044b999..ae6849672 100644 --- a/README.md +++ b/README.md @@ -902,9 +902,10 @@ The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAs ```bash git clone https://github.com/abhigyanpatwari/gitnexus.git -cd gitnexus/gitnexus-shared && npm install && npm run build -cd ../gitnexus-web && npm install -npm run dev +cd gitnexus/gitnexus-web && npm install +# Compile sibling gitnexus-shared with this package's TypeScript 7 (do not npm ci shared). +cd ../gitnexus-shared && node ../gitnexus-web/node_modules/typescript/lib/tsc.js +cd ../gitnexus-web && npm run dev # Then in another terminal, start the backend the frontend connects to: npx gitnexus@latest serve ``` diff --git a/TESTING.md b/TESTING.md index 72833d3ca..9febb8283 100644 --- a/TESTING.md +++ b/TESTING.md @@ -39,6 +39,8 @@ From `gitnexus-web/`: ### Before opening a PR ```bash +# gitnexus-shared/dist must exist first. `npm install` / `npm run build` in +# gitnexus/ compiles it via parent `lib/tsc.js` (do not npm ci gitnexus-shared). cd gitnexus && npx tsc --noEmit && npm test cd ../gitnexus-web && npx tsc -b --noEmit && npm test ``` diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index 9bff38739..4ec41926d 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -62,7 +62,7 @@ "@vitest/coverage-v8": "^4.1.11", "jsdom": "^30.0.1", "tree-sitter-wasms": "^0.1.13", - "typescript": "^5.4.5", + "typescript": "^7.0.2", "vite": "^8.1.5", "vitest": "^4.1.10", "wait-on": "^9.1.0" @@ -2550,6 +2550,346 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -2659,6 +2999,20 @@ "undici-types": "~5.26.4" } }, + "node_modules/@vercel/node/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@vercel/node/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -8038,17 +8392,38 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "devOptional": true, "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/ufo": { diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 11fc68b1e..a10e13ffa 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -72,7 +72,7 @@ "@vitest/coverage-v8": "^4.1.11", "jsdom": "^30.0.1", "tree-sitter-wasms": "^0.1.13", - "typescript": "^5.4.5", + "typescript": "^7.0.2", "vite": "^8.1.5", "vitest": "^4.1.10", "wait-on": "^9.1.0" diff --git a/gitnexus-web/test/unit/tsconfig-react.test.ts b/gitnexus-web/test/unit/tsconfig-react.test.ts new file mode 100644 index 000000000..d1d805ad8 --- /dev/null +++ b/gitnexus-web/test/unit/tsconfig-react.test.ts @@ -0,0 +1,16 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +describe('TypeScript 7 + React frontend toolchain', () => { + it('keeps Vite and Vitest on the automatic JSX runtime that matches react-jsx', () => { + for (const configFile of ['vite.config.ts', 'vitest.config.ts'] as const) { + const source = readFileSync(path.join(webRoot, configFile), 'utf8'); + expect(source).toContain('@vitejs/plugin-react'); + expect(source).toContain("jsxRuntime: 'automatic'"); + } + }); +}); diff --git a/gitnexus-web/tsconfig.app.json b/gitnexus-web/tsconfig.app.json index 2133377fd..34864f2a0 100644 --- a/gitnexus-web/tsconfig.app.json +++ b/gitnexus-web/tsconfig.app.json @@ -1,26 +1,29 @@ { "compilerOptions": { "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Bundler", + "moduleDetection": "force", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", + "jsxImportSource": "react", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, - "baseUrl": "./", + "noUncheckedSideEffectImports": true, + "rootDir": "./src", "paths": { - "@/*": ["./src/*"], - "@shared/*": ["../shared/*"] + "@/*": ["./src/*"] }, "types": ["vite/client"] }, - "include": ["src", "../shared"], + "include": ["src"], "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] } diff --git a/gitnexus-web/tsconfig.node.json b/gitnexus-web/tsconfig.node.json index 8b6c9f152..cc7fd0674 100644 --- a/gitnexus-web/tsconfig.node.json +++ b/gitnexus-web/tsconfig.node.json @@ -2,11 +2,15 @@ "compilerOptions": { "composite": true, "skipLibCheck": true, + "target": "ESNext", + "lib": ["ESNext"], "module": "ESNext", "moduleResolution": "Bundler", + "moduleDetection": "force", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "noEmit": true, + "rootDir": ".", "types": ["node"] }, "include": ["vite.config.ts"] diff --git a/gitnexus-web/vercel.json b/gitnexus-web/vercel.json index 284b15735..3cd408211 100644 --- a/gitnexus-web/vercel.json +++ b/gitnexus-web/vercel.json @@ -1,3 +1,3 @@ { - "installCommand": "cd ../gitnexus-shared && npm install && npm run build && cd ../gitnexus-web && npm ci --include=dev" + "installCommand": "npm ci --include=dev && cd ../gitnexus-shared && node ../gitnexus-web/node_modules/typescript/lib/tsc.js" } diff --git a/gitnexus-web/vite.config.ts b/gitnexus-web/vite.config.ts index a62b9e586..cdd411b16 100644 --- a/gitnexus-web/vite.config.ts +++ b/gitnexus-web/vite.config.ts @@ -8,7 +8,7 @@ const _require = createRequire(import.meta.url); const gitnexusPkg = _require('../gitnexus/package.json'); export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react({ jsxRuntime: 'automatic' }), tailwindcss()], define: { __REQUIRED_NODE_VERSION__: JSON.stringify(gitnexusPkg.engines.node.replace(/[>=^~\s]/g, '')), }, diff --git a/gitnexus-web/vitest.config.ts b/gitnexus-web/vitest.config.ts index 1de8ef065..2b7196b2e 100644 --- a/gitnexus-web/vitest.config.ts +++ b/gitnexus-web/vitest.config.ts @@ -7,7 +7,7 @@ const _require = createRequire(import.meta.url); const gitnexusPkg = _require('../gitnexus/package.json'); export default defineConfig({ - plugins: [react()], + plugins: [react({ jsxRuntime: 'automatic' })], define: { __REQUIRED_NODE_VERSION__: JSON.stringify(gitnexusPkg.engines.node.replace(/[>=^~\s]/g, '')), }, diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 15047dc21..794d63657 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -68,7 +68,7 @@ "@vitest/coverage-v8": "^4.0.18", "gitnexus-shared": "file:../gitnexus-shared", "tsx": "^4.0.0", - "typescript": "^5.4.5", + "typescript": "^7.0.2", "vitest": "^4.0.18" }, "engines": { @@ -1331,6 +1331,346 @@ "@types/node": "*" } }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "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" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@vitest/coverage-v8": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", @@ -4671,17 +5011,38 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "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/typical": { diff --git a/gitnexus/package.json b/gitnexus/package.json index 1e2bc42ad..c5ab6f3c6 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -129,7 +129,7 @@ "@vitest/coverage-v8": "^4.0.18", "gitnexus-shared": "file:../gitnexus-shared", "tsx": "^4.0.0", - "typescript": "^5.4.5", + "typescript": "^7.0.2", "vitest": "^4.0.18" }, "overrides": { diff --git a/gitnexus/scripts/build.js b/gitnexus/scripts/build.js index 6cead2784..b97f6e3b6 100644 --- a/gitnexus/scripts/build.js +++ b/gitnexus/scripts/build.js @@ -54,8 +54,8 @@ if (!fs.existsSync(SHARED_ROOT)) { // Launch tsc as `node typescript/lib/tsc.js` on every OS. The `.bin/tsc` / // `tsc.cmd` shims are Windows-only wrappers; `execFileSync` cannot spawn a -// `.cmd` without a shell, and a separate `npm ci` in gitnexus-shared pulls -// TypeScript 7 optional platform packages (7+ minutes in CI). +// `.cmd` without a shell, and a separate `npm ci` in gitnexus-shared would +// pull a second TypeScript 7 optional-platform install (7+ minutes in CI). const tscJs = path.join(ROOT, 'node_modules', 'typescript', 'lib', 'tsc.js'); if (!fs.existsSync(tscJs)) { console.error( diff --git a/gitnexus/test/helpers/ignore-set-source.ts b/gitnexus/test/helpers/ignore-set-source.ts index 04db55dd6..584faf839 100644 --- a/gitnexus/test/helpers/ignore-set-source.ts +++ b/gitnexus/test/helpers/ignore-set-source.ts @@ -4,18 +4,17 @@ * Those sets are module-private, and exporting them purely to be testable would * widen a production surface to satisfy a test — the call * `receiver-twin-list-drift.test.ts` documents. So the guards read the source - * instead, through the TypeScript parser the repo already vendors and already - * uses this way (`literal-collectors.ts`, `query-determinism-guard.test.ts`, - * `cli-index-help.test.ts`). + * instead, through `@babel/parser` (`parse-typescript-source.ts`). * - * Using the real parser is what makes the guards trustworthy. A text scanner has - * to decide whether a delimiter opens a comment or sits inside a string, and it - * gets that wrong in both directions here: the ignore-list comments quote paths - * and carry an apostrophe (`Next.js's`), while a glob string such as `'** / *'` - * contains a comment-open sequence. It also has to guess which bracket belongs - * to the declaration rather than to a type annotation. Each of those is a way to - * silently read fewer members — and a guard that quietly stops seeing members is - * the exact defect these guards exist to catch. + * Using an AST parser for TypeScript syntax is what makes the guards + * trustworthy. A text scanner has to decide whether a delimiter opens a comment + * or sits inside a string, and it gets that wrong in both directions here: the + * ignore-list comments quote paths and carry an apostrophe (`Next.js's`), while + * a glob string such as `'** / *'` contains a comment-open sequence. It also + * has to guess which bracket belongs to the declaration rather than to a type + * annotation. Each of those is a way to silently read fewer members — and a + * guard that quietly stops seeing members is the exact defect these guards + * exist to catch. * * `setEntries` therefore refuses anything that is not a plain list of string * literals, rather than skipping the members it cannot resolve. @@ -23,7 +22,8 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; +import * as t from '@babel/types'; +import { forEachChild, nodeText, parseTypeScript } from './parse-typescript-source.js'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); @@ -55,46 +55,44 @@ export const readSource = (file: string): string => readFileSync(file, 'utf8'); * concatenation, a computed value). */ export const setEntries = (source: string, setName: string): string[] => { - const sourceFile = ts.createSourceFile( - 'ignore-set-source.ts', - source, - ts.ScriptTarget.Latest, - true, - ); + const { ast } = parseTypeScript('ignore-set-source.ts', source); - let elements: ts.NodeArray | undefined; - const visit = (node: ts.Node): void => { + let elements: t.ArrayExpression['elements'] | undefined; + const visit = (node: t.Node): void => { if ( elements === undefined && - ts.isVariableDeclaration(node) && - ts.isIdentifier(node.name) && - node.name.text === setName && - node.initializer !== undefined && - ts.isNewExpression(node.initializer) && - node.initializer.arguments?.length === 1 && - ts.isArrayLiteralExpression(node.initializer.arguments[0]) + t.isVariableDeclarator(node) && + t.isIdentifier(node.id) && + node.id.name === setName && + node.init !== undefined && + node.init !== null && + t.isNewExpression(node.init) && + node.init.arguments.length === 1 && + t.isArrayExpression(node.init.arguments[0]) ) { - elements = node.initializer.arguments[0].elements; + elements = node.init.arguments[0].elements; return; } - ts.forEachChild(node, visit); + forEachChild(node, visit); }; - visit(sourceFile); + visit(ast); if (elements === undefined) { throw new Error(`${setName} is not declared as \`new Set([...])\` — update this test`); } - const unresolvable = elements.filter((element) => !ts.isStringLiteral(element)); + const unresolvable = elements.filter((element) => !t.isStringLiteral(element)); if (unresolvable.length > 0) { + const first = unresolvable[0]; + const excerpt = first && typeof first === 'object' ? nodeText(source, first) : String(first); throw new Error( `${setName} holds ${unresolvable.length} member(s) that are not plain string literals ` + - `(first: \`${unresolvable[0].getText(sourceFile)}\`). A source-reading guard cannot resolve ` + + `(first: \`${excerpt}\`). A source-reading guard cannot resolve ` + `those, so switch this set to a runtime assertion rather than letting the guard see fewer members.`, ); } - return elements.map((element) => (element as ts.StringLiteral).text); + return elements.map((element) => (element as t.StringLiteral).value); }; /** diff --git a/gitnexus/test/helpers/literal-collectors.ts b/gitnexus/test/helpers/literal-collectors.ts index a6bb41b29..6172b739b 100644 --- a/gitnexus/test/helpers/literal-collectors.ts +++ b/gitnexus/test/helpers/literal-collectors.ts @@ -9,7 +9,7 @@ * FOUR modes (plan KTD3): * 1. Config reflection — import `*-extractors/configs/*.ts`, read each * config-shaped export's node-type-array keys. Exact `config.language`. - * 2. AST scan (`typescript` parser, no type-checker) over the EXTRACTION + * 2. AST scan (TypeScript 7 program SourceFile; no Checker gating) over the EXTRACTION * surface — `*-extractors/**`, every `languages//captures.ts`, and * `export-detection.ts`. Collected BY CONSUMPTION SITE: `.type === '..'`, * `childForFieldName('..')` (capturing the receiver node type when an @@ -26,7 +26,7 @@ * `ingestion/` (e.g. `type-env.ts`). These files MIX SyntaxNode `.type` with * resolved-symbol `.type` (kinds like 'Class'), so a literal is collected * ONLY when its `.type` / `childForFieldName` receiver resolves to a - * tree-sitter SyntaxNode (via the TS TypeChecker). Per-`languages//` + * tree-sitter SyntaxNode (via the TypeScript 7 Checker). Per-`languages//` * files tag to that one grammar; shared (non-`languages//`) files tag * to the full gated set (valid-if-any). * @@ -35,8 +35,8 @@ * * Test-only file: allowed to name languages. */ -import ts from 'typescript'; -import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'; +import * as ts from './ts7-ast.js'; +import { readdirSync, existsSync, statSync, realpathSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { SupportedLanguages } from '../../src/config/supported-languages.js'; @@ -449,11 +449,16 @@ function receiverNodeTypeOf(call: ts.CallExpression, sf: ts.SourceFile): string return receiverMutatedIn(recvText, scope) ? undefined : found; } -function scanFile(file: string): ScanResult { +function scanFile( + file: string, + built: { program: ts.Program; checker: ts.Checker } | null, +): ScanResult { const relPath = rel(file); const langs = fileLanguages(relPath); - const src = readFileSync(file, 'utf8'); - const sf = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true); + const empty: ScanResult = { nodeTypes: [], fields: [] }; + if (!built) return empty; + const sf = programSourceFile(built.program, file); + if (!sf) return empty; const nodeTypes: CollectedNodeType[] = []; const fields: CollectedField[] = []; @@ -558,8 +563,9 @@ function scanFile(file: string): ScanResult { function collectInCodeLiterals(): ScanResult { const nodeTypes: CollectedNodeType[] = []; const fields: CollectedField[] = []; + const built = buildProgram(); for (const file of mode2Files()) { - const r = scanFile(file); + const r = scanFile(file, built); nodeTypes.push(...r.nodeTypes); fields.push(...r.fields); } @@ -571,7 +577,7 @@ function collectInCodeLiterals(): ScanResult { // binding/interpret/arity/import-decomposer/...), the production path for // migrated languages. These files mix SyntaxNode `.type` (grammar nodes) with // resolved-symbol `.type` (kinds like 'Class'); a naive scan would false- -// positive on the latter. So this mode uses the TS TypeChecker to collect a +// positive on the latter. So this mode uses the TypeScript 7 Checker to collect a // literal ONLY when its `.type` receiver / childForFieldName target resolves to // a tree-sitter SyntaxNode. Per-language dir => grammar (no cross-lang ambiguity). // --------------------------------------------------------------------------- @@ -613,25 +619,35 @@ function resolutionLayerFiles(): { file: string; langs: SupportedLanguages[] }[] } let _program: ts.Program | null = null; -let _checker: ts.TypeChecker | null = null; -function buildProgram( - rootFiles: string[], -): { program: ts.Program; checker: ts.TypeChecker } | null { +let _checker: ts.Checker | null = null; +function buildProgram(): { program: ts.Program; checker: ts.Checker } | null { if (_program && _checker) return { program: _program, checker: _checker }; try { - const cfg = ts.readConfigFile(join(REPO_ROOT, 'tsconfig.json'), ts.sys.readFile); - const parsed = ts.parseJsonConfigFileContent(cfg.config ?? {}, ts.sys, REPO_ROOT); - const options: ts.CompilerOptions = { ...parsed.options, noEmit: true, skipLibCheck: true }; - _program = ts.createProgram(rootFiles, options); - _checker = _program.getTypeChecker(); + const api = new ts.API({ cwd: REPO_ROOT }); + const snapshot = api.updateSnapshot({ + openProjects: [join(REPO_ROOT, 'tsconfig.json')], + }); + const project = snapshot.getProjects()[0]; + if (!project) return null; + _program = project.program; + _checker = project.checker; return { program: _program, checker: _checker }; } catch { return null; } } +const sourceFileByPath = new Map(); + +function programSourceFile(program: ts.Program, file: string): ts.SourceFile | undefined { + if (sourceFileByPath.has(file)) return sourceFileByPath.get(file); + const sf = program.getSourceFile(file) ?? program.getSourceFile(realpathSync(file)); + sourceFileByPath.set(file, sf); + return sf; +} + /** True when `node`'s resolved type is (or includes) a tree-sitter SyntaxNode. */ -function isSyntaxNodeReceiver(checker: ts.TypeChecker, node: ts.Node): boolean { +function isSyntaxNodeReceiver(checker: ts.Checker, node: ts.Node): boolean { try { const s = checker.typeToString(checker.getTypeAtLocation(node)); return /\bSyntaxNode\b/.test(s); @@ -647,7 +663,7 @@ function collectResolutionLayerLiterals(): ScanResult { const nodeTypes: CollectedNodeType[] = []; const fields: CollectedField[] = []; const entries = resolutionLayerFiles(); - const built = buildProgram(entries.map((e) => e.file)); + const built = buildProgram(); if (!built) { resolutionLayerProgramOk = false; return { nodeTypes, fields }; @@ -655,7 +671,7 @@ function collectResolutionLayerLiterals(): ScanResult { const { program, checker } = built; for (const { file, langs } of entries) { - const sf = program.getSourceFile(file); + const sf = programSourceFile(program, file); if (!sf) continue; const relPath = rel(file); const constMembers = new Map(); diff --git a/gitnexus/test/helpers/parse-typescript-source.ts b/gitnexus/test/helpers/parse-typescript-source.ts new file mode 100644 index 000000000..d324d84d8 --- /dev/null +++ b/gitnexus/test/helpers/parse-typescript-source.ts @@ -0,0 +1,162 @@ +/** + * Parse-only TypeScript AST walks for tests. + * + * TypeScript 7.0 does not ship the classic Compiler API (`createSourceFile`). + * These guards only need syntax, so they parse with `@babel/parser` (already a + * CLI dependency) rather than spawning the native TypeScript 7 program API. + */ +import { parse } from '@babel/parser'; +import { + VISITOR_KEYS, + isBinaryExpression, + isNode, + isStringLiteral, + isTemplateLiteral, + type Comment, + type File, + type MemberExpression, + type Node, + type OptionalMemberExpression, + type TemplateLiteral, +} from '@babel/types'; + +export type AstNode = Node & { parent?: AstNode }; + +export interface ParsedSource { + ast: File & { parent?: AstNode }; + source: string; + fileName: string; +} + +const PARSE_PLUGINS: NonNullable[1]>['plugins'] = [ + 'typescript', + 'explicitResourceManagement', + 'importAttributes', + 'decoratorAutoAccessors', + ['decorators', { decoratorsBeforeExport: true }], +]; + +export function forEachChild(node: Node, visit: (child: AstNode) => void): void { + const keys = VISITOR_KEYS[node.type] ?? []; + for (const key of keys) { + const value = (node as unknown as Record)[key]; + if (Array.isArray(value)) { + for (const item of value) { + if (isNode(item)) visit(item); + } + } else if (isNode(value)) { + visit(value); + } + } +} + +/** Direct descendants in source order (does not include `node` itself). */ +export function collectDescendants(node: Node): AstNode[] { + const out: AstNode[] = []; + const visit = (child: AstNode): void => { + out.push(child); + forEachChild(child, visit); + }; + forEachChild(node, visit); + return out; +} + +export function staticMemberName( + node: MemberExpression | OptionalMemberExpression, +): string | undefined { + return node.computed || node.property.type !== 'Identifier' ? undefined : node.property.name; +} + +function templateLiteralText(node: TemplateLiteral): string | undefined { + if (node.expressions.length > 0) return undefined; + return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join(''); +} + +/** Compile-time string from a literal, template without holes, or `+` chain. */ +export function staticStringValue(node: Node | undefined | null): string | undefined { + if (!node) return undefined; + if (isStringLiteral(node)) return node.value; + if (isTemplateLiteral(node)) return templateLiteralText(node); + if (isBinaryExpression(node) && node.operator === '+') { + const left = staticStringValue(node.left); + const right = staticStringValue(node.right); + if (left !== undefined && right !== undefined) return `${left}${right}`; + } + return undefined; +} + +function attachParents(node: AstNode, parent?: AstNode): void { + node.parent = parent; + forEachChild(node, (child) => attachParents(child, node)); +} + +export function parseTypeScript(fileName: string, source: string): ParsedSource { + const ast = parse(source, { + sourceFilename: fileName, + sourceType: 'unambiguous', + plugins: PARSE_PLUGINS, + errorRecovery: true, + attachComment: true, + ranges: true, + }) as File & { parent?: AstNode }; + attachParents(ast); + return { ast, source, fileName }; +} + +export function nodeStart(node: Node): number { + return node.start ?? 0; +} + +export function nodeEnd(node: Node): number { + return node.end ?? 0; +} + +export function nodeText(source: string, node: Node): string { + return source.slice(nodeStart(node), nodeEnd(node)); +} + +let lineAtSource = ''; +let lineAtStarts: number[] = [0]; + +function lineStarts(source: string): number[] { + if (source === lineAtSource) return lineAtStarts; + const starts = [0]; + for (let i = 0; i < source.length; i++) { + if (source[i] === '\n') starts.push(i + 1); + } + lineAtSource = source; + lineAtStarts = starts; + return starts; +} + +export function lineAt(source: string, position: number): number { + if (position <= 0) return 1; + const starts = lineStarts(source); + const pos = Math.min(position, source.length); + let lo = 0; + let hi = starts.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (starts[mid] <= pos) lo = mid; + else hi = mid - 1; + } + return lo + 1; +} + +export interface CommentRange { + pos: number; + end: number; +} + +function toRange(comment: Comment): CommentRange | undefined { + if (comment.start == null || comment.end == null) return undefined; + return { pos: comment.start, end: comment.end }; +} + +export function leadingCommentRanges(node: Node): CommentRange[] { + return (node.leadingComments ?? []).map(toRange).filter((range) => range !== undefined); +} + +export function trailingCommentRanges(node: Node): CommentRange[] { + return (node.trailingComments ?? []).map(toRange).filter((range) => range !== undefined); +} diff --git a/gitnexus/test/helpers/ts7-ast.ts b/gitnexus/test/helpers/ts7-ast.ts new file mode 100644 index 000000000..c2d077042 --- /dev/null +++ b/gitnexus/test/helpers/ts7-ast.ts @@ -0,0 +1,48 @@ +/** + * TypeScript 7 AST + Checker surface for test-only walks that need types. + * + * TypeScript 7.0 dropped the classic Compiler API (`createProgram` / + * `getTypeChecker`). The native `typescript/unstable/sync` client exposes the + * same Checker methods against the Go compiler, and `typescript/unstable/ast` + * is the matching syntax tree. + */ +export { SyntaxKind } from 'typescript/unstable/ast'; +export type { + CallExpression, + Expression, + Node, + PropertyAccessExpression, + SourceFile, + StringLiteral, +} from 'typescript/unstable/ast'; +export { + isArrayLiteralExpression, + isArrowFunction, + isAsExpression, + isBinaryExpression, + isCallExpression, + isCaseClause, + isConstructorDeclaration, + isFunctionDeclaration, + isFunctionExpression, + isGetAccessorDeclaration, + isIdentifier, + isIfStatement, + isMethodDeclaration, + isNewExpression, + isPostfixUnaryExpression, + isPrefixUnaryExpression, + isPropertyAccessExpression, + isSetAccessorDeclaration, + isStringLiteral, + isStringLiteralLikeNode as isStringLiteralLike, + isSwitchStatement, + isVariableDeclaration, +} from 'typescript/unstable/ast/is'; +export { API, type Checker, type Program } from 'typescript/unstable/sync'; + +import type { Node } from 'typescript/unstable/ast'; + +export function forEachChild(node: Node, visitor: (child: Node) => void): void { + node.forEachChild(visitor); +} diff --git a/gitnexus/test/unit/build-web-optin.test.ts b/gitnexus/test/unit/build-web-optin.test.ts index 68e8cd464..efb07190e 100644 --- a/gitnexus/test/unit/build-web-optin.test.ts +++ b/gitnexus/test/unit/build-web-optin.test.ts @@ -8,6 +8,8 @@ import { runWebBuild, shouldBuildWeb, shouldPreserveWebOutput } from '../../scri /** Default prepare/build stay CLI-only; the web UI ships only via prepack --web. */ const REPO_ROOT = path.resolve(__dirname, '../../..'); +const CLI_TSC_JS = 'node ../gitnexus/node_modules/typescript/lib/tsc.js'; +const WEB_TSC_JS = 'node ../gitnexus-web/node_modules/typescript/lib/tsc.js'; const PACKAGE_JSON = JSON.parse( readFileSync(path.join(REPO_ROOT, 'gitnexus/package.json'), 'utf8'), ) as { scripts?: Record }; @@ -283,7 +285,7 @@ describe('workflows that need the web UI install it themselves', () => { describe('setup-gitnexus job budget', () => { it('does not npm-ci gitnexus-shared (TypeScript 7 optional-platform install stalls CI)', () => { const shared = setupGitnexus.runs?.steps?.find((step) => step.name === 'Build gitnexus-shared'); - expect(String(shared?.run)).toBe('node ../gitnexus/node_modules/typescript/lib/tsc.js'); + expect(String(shared?.run)).toBe(CLI_TSC_JS); expect(String(shared?.run)).not.toContain('.bin'); expect(shared?.if).toContain("lifecycle-scripts == 'false'"); expect( @@ -314,12 +316,28 @@ describe('setup-gitnexus job budget', () => { expect(String(setupNode?.with?.['cache-dependency-path'])).toBe( 'gitnexus-web/package-lock.json', ); - expect(String(shared?.run)).toBe('node ../gitnexus-web/node_modules/typescript/lib/tsc.js'); + expect(String(shared?.run)).toBe(WEB_TSC_JS); expect(String(shared?.run)).not.toContain('.bin'); expect(String(shared?.run)).not.toContain('npm ci'); expect(webInstall?.env?.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD).toBe('1'); }); + it('Vercel installs web first and compiles shared with the web TypeScript', () => { + const vercel = JSON.parse( + readFileSync(path.join(REPO_ROOT, 'gitnexus-web/vercel.json'), 'utf8'), + ) as { installCommand?: string }; + const install = String(vercel.installCommand); + // Vercel runs installCommand with NODE_ENV=production, so npm ci drops + // typescript unless --include=dev is on that install (not a later step). + expect(install).toContain('npm ci --include=dev'); + expect(install).toContain('gitnexus-shared'); + expect(install.indexOf('npm ci --include=dev')).toBeLessThan( + install.indexOf('gitnexus-shared'), + ); + expect(install).toContain(WEB_TSC_JS); + expect(install).not.toMatch(/gitnexus-shared[^&]*npm (?:ci|install)/); + }); + it('quality typecheck skips prepare/postinstall so tsc --noEmit fits in 10 minutes', () => { const job = qualityJobs.typecheck; const setup = job?.steps?.find((step) => step.uses === './.github/actions/setup-gitnexus'); @@ -327,6 +345,28 @@ describe('setup-gitnexus job budget', () => { expect(setup?.with?.['lifecycle-scripts']).toBe('false'); }); + it('web app tsconfig typechecks React JSX on TypeScript 7 without baseUrl', () => { + const tsconfig = JSON.parse( + readFileSync(path.join(REPO_ROOT, 'gitnexus-web/tsconfig.app.json'), 'utf8'), + ) as { + compilerOptions?: { + baseUrl?: string; + jsx?: string; + jsxImportSource?: string; + lib?: string[]; + rootDir?: string; + types?: string[]; + }; + }; + const options = tsconfig.compilerOptions ?? {}; + expect(options.baseUrl).toBeUndefined(); + expect(options.jsx).toBe('react-jsx'); + expect(options.jsxImportSource).toBe('react'); + expect(options.lib).toEqual(expect.arrayContaining(['ESNext', 'DOM', 'DOM.Iterable'])); + expect(options.rootDir).toBe('./src'); + expect(options.types).toEqual(['vite/client']); + }); + it('quality typecheck-web can finish a cold web install instead of canceling before cache save', () => { expect(qualityJobs['typecheck-web']?.['timeout-minutes']).toBe(15); }); diff --git a/gitnexus/test/unit/cli-index-help.test.ts b/gitnexus/test/unit/cli-index-help.test.ts index 417ecb53d..9229f5444 100644 --- a/gitnexus/test/unit/cli-index-help.test.ts +++ b/gitnexus/test/unit/cli-index-help.test.ts @@ -4,8 +4,14 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Command, Option } from 'commander'; -import * as ts from 'typescript'; +import * as t from '@babel/types'; import { afterEach, describe, expect, it } from 'vitest'; +import { + forEachChild, + parseTypeScript, + staticMemberName, + staticStringValue, +} from '../helpers/parse-typescript-source.js'; import { CLI_SPAWN_PREFIX } from '../helpers/cli-entry.js'; import { localizeCliHelp } from '../../src/cli/help-i18n.js'; import { setCliLanguage, type SupportedCliLanguage } from '../../src/cli/i18n/index.js'; @@ -69,17 +75,6 @@ const allHelpCommands = [ ['group', 'contracts'], ]; -function staticStringValue(node: ts.Node | undefined): string | undefined { - if (!node) return undefined; - if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text; - if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) { - const left = staticStringValue(node.left); - const right = staticStringValue(node.right); - if (left !== undefined && right !== undefined) return `${left}${right}`; - } - return undefined; -} - function extractRegisteredHelpDescriptions(): string[] { const descriptions = new Set(); const sourceFiles = ['src/cli/index.ts', 'src/cli/group.ts']; @@ -87,27 +82,30 @@ function extractRegisteredHelpDescriptions(): string[] { for (const relativePath of sourceFiles) { const filePath = path.join(repoRoot, relativePath); const source = fs.readFileSync(filePath, 'utf8'); - const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true); + const { ast } = parseTypeScript(filePath, source); - function visit(node: ts.Node): void { - if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { - const method = node.expression.name.text; - const description = - method === 'description' - ? staticStringValue(node.arguments[0]) - : method === 'option' || method === 'requiredOption' - ? staticStringValue(node.arguments[1]) - : undefined; + function visit(node: t.Node): void { + if ( + t.isCallExpression(node) && + (t.isMemberExpression(node.callee) || t.isOptionalMemberExpression(node.callee)) + ) { + const method = staticMemberName(node.callee); + let description: string | undefined; + if (method === 'description') { + description = staticStringValue(node.arguments[0]); + } else if (method === 'option' || method === 'requiredOption') { + description = staticStringValue(node.arguments[1]); + } if (description && /[A-Za-z]/.test(description)) { descriptions.add(description.replace(/\s+/g, ' ').trim()); } } - ts.forEachChild(node, visit); + forEachChild(node, visit); } - visit(sourceFile); + visit(ast); } return [...descriptions].filter((description) => description.length > 0).sort(); diff --git a/gitnexus/test/unit/group/sync-partial-extraction.test.ts b/gitnexus/test/unit/group/sync-partial-extraction.test.ts index 0e7d41228..e1588be1c 100644 --- a/gitnexus/test/unit/group/sync-partial-extraction.test.ts +++ b/gitnexus/test/unit/group/sync-partial-extraction.test.ts @@ -4,7 +4,16 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; +import * as t from '@babel/types'; +import { + type AstNode, + collectDescendants, + lineAt, + nodeStart, + nodeText, + parseTypeScript, + staticMemberName, +} from '../../helpers/parse-typescript-source.js'; import type { ContractRegistry, ExtractedContract, @@ -182,65 +191,63 @@ describe('syncGroup when one extractor fails partway through a repo', () => { */ const SYNC_SOURCE_PATH = fileURLToPath(new URL('../../../src/core/group/sync.ts', import.meta.url)); -/** Every node under `node`, in source order. No branching, so nothing is skippable. */ -function descendants(node: ts.Node): ts.Node[] { - const out: ts.Node[] = []; - const visit = (n: ts.Node): void => { - out.push(n); - n.forEachChild(visit); - }; - node.forEachChild(visit); - return out; -} - /** `const : StoredContract[] = []` — the per-repo staging buffer. */ -function isStagingBufferDeclaration(node: ts.Node): node is ts.VariableDeclaration { +function isStagingBufferDeclaration(node: t.Node): node is t.VariableDeclarator { + if (!t.isVariableDeclarator(node) || !t.isIdentifier(node.id)) return false; + const annotation = node.id.typeAnnotation; + if ( + !annotation || + !t.isTSTypeAnnotation(annotation) || + !t.isTSArrayType(annotation.typeAnnotation) + ) { + return false; + } + const elementType = annotation.typeAnnotation.elementType; return ( - ts.isVariableDeclaration(node) && - node.type !== undefined && - ts.isArrayTypeNode(node.type) && - ts.isTypeReferenceNode(node.type.elementType) && - ts.isIdentifier(node.type.elementType.typeName) && - node.type.elementType.typeName.text === 'StoredContract' && - node.initializer !== undefined && - ts.isArrayLiteralExpression(node.initializer) && - node.initializer.elements.length === 0 && - ts.isVariableDeclarationList(node.parent) && - (node.parent.flags & ts.NodeFlags.Const) !== 0 + t.isTSTypeReference(elementType) && + t.isIdentifier(elementType.typeName) && + elementType.typeName.name === 'StoredContract' && + node.init !== undefined && + node.init !== null && + t.isArrayExpression(node.init) && + node.init.elements.length === 0 && + t.isVariableDeclaration((node as AstNode).parent) && + ((node as AstNode).parent as t.VariableDeclaration).kind === 'const' ); } /** `x.apply(dest, args)` — an argument-limited append in non-spread clothing. */ -function isApplyCall(call: ts.CallExpression): boolean { - return ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === 'apply'; +function isApplyCall(call: t.CallExpression): boolean { + return ( + (t.isMemberExpression(call.callee) || t.isOptionalMemberExpression(call.callee)) && + staticMemberName(call.callee) === 'apply' + ); } -function describeCall(sourceFile: ts.SourceFile, call: ts.CallExpression): string { - const { line } = sourceFile.getLineAndCharacterOfPosition(call.getStart(sourceFile)); - return `${line + 1}: ${call.getText(sourceFile).replace(/\s+/g, ' ')}`; +function describeCall(source: string, call: t.CallExpression): string { + const line = lineAt(source, nodeStart(call)); + return `${line}: ${nodeText(source, call).replace(/\s+/g, ' ')}`; } describe('the per-repo staging append in sync.ts', () => { it('appends the staged contracts without spreading them into a call', () => { const source = fs.readFileSync(SYNC_SOURCE_PATH, 'utf-8'); - const sourceFile = ts.createSourceFile( - SYNC_SOURCE_PATH, - source, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS, - ); + const { ast } = parseTypeScript(SYNC_SOURCE_PATH, source); - const allNodes = descendants(sourceFile); + const allNodes = collectDescendants(ast); const stagingBuffers = allNodes.filter(isStagingBufferDeclaration); // One staging buffer, or this gate no longer knows which code it guards. - expect(stagingBuffers.map((d) => d.name.getText(sourceFile))).toHaveLength(1); - const stagingNames = stagingBuffers.map((d) => d.name.getText(sourceFile)); + const stagingNames = stagingBuffers.map((d) => + t.isIdentifier(d.id) ? d.id.name : nodeText(source, d.id), + ); + expect(stagingNames).toHaveLength(1); // The block the buffer is declared in — the per-repo loop body. + // VariableDeclarator → VariableDeclaration → BlockStatement (Babel has no + // extra VariableStatement wrapper). const declaringBlocks = stagingBuffers - .map((d) => d.parent.parent.parent) // declaration → list → statement → block - .filter(ts.isBlock); + .map((d) => (d as AstNode).parent?.parent) + .filter((node): node is t.BlockStatement => t.isBlockStatement(node)); expect(declaringBlocks).toHaveLength(1); // The extractor try-block: a DIRECT statement of that block whose `try` reads @@ -249,22 +256,22 @@ describe('the per-repo staging append in sync.ts', () => { // ancestor reads the buffer too. Widening to "any try that mentions it" pulls // in the entire function body, manifest-window spreads and all. const extractorTryBlocks = declaringBlocks.flatMap((block) => - block.statements - .filter(ts.isTryStatement) + block.body + .filter((statement): statement is t.TryStatement => t.isTryStatement(statement)) .filter((statement) => - descendants(statement.tryBlock).some( - (n) => ts.isIdentifier(n) && stagingNames.includes(n.text), + collectDescendants(statement.block).some( + (n) => t.isIdentifier(n) && stagingNames.includes(n.name), ), ) - .map((statement) => statement.tryBlock), + .map((statement) => statement.block), ); expect(extractorTryBlocks).toHaveLength(1); const unboundedAppends = extractorTryBlocks.flatMap((block) => - descendants(block) - .filter(ts.isCallExpression) - .filter((call) => call.arguments.some(ts.isSpreadElement) || isApplyCall(call)) - .map((call) => describeCall(sourceFile, call)), + collectDescendants(block) + .filter((node): node is t.CallExpression => t.isCallExpression(node)) + .filter((call) => call.arguments.some((arg) => t.isSpreadElement(arg)) || isApplyCall(call)) + .map((call) => describeCall(source, call)), ); // Every staged contract must reach `autoContracts` through a bounded loop: diff --git a/gitnexus/test/unit/query-determinism-guard.test.ts b/gitnexus/test/unit/query-determinism-guard.test.ts index e6f7acd72..ebad4efb9 100644 --- a/gitnexus/test/unit/query-determinism-guard.test.ts +++ b/gitnexus/test/unit/query-determinism-guard.test.ts @@ -47,10 +47,21 @@ * and `detect-changes-local-id-stability.test.ts`. */ import { describe, expect, it } from 'vitest'; -import ts from 'typescript'; +import * as t from '@babel/types'; import { readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { + type AstNode, + type CommentRange, + forEachChild, + leadingCommentRanges, + lineAt, + nodeStart, + nodeText, + parseTypeScript, + trailingCommentRanges, +} from '../helpers/parse-typescript-source.js'; const SRC_DIR = fileURLToPath(new URL('../../src/', import.meta.url)); @@ -135,18 +146,18 @@ function walkTs(dir: string, relBase: string, out: string[]): string[] { * helpers (`seedBlockQuery`, `buildFtsQueryCypher`), and anything built from * those. Resolved to a fixpoint so an ordering constant can be composed. */ -function collectOrderingNames(sf: ts.SourceFile): Set { +function collectOrderingNames(source: string, ast: t.Node): Set { const initializers = new Map(); - const visit = (node: ts.Node): void => { - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { - initializers.set(node.name.text, node.initializer.getText(sf)); + const visit = (node: t.Node): void => { + if (t.isVariableDeclarator(node) && t.isIdentifier(node.id) && node.init) { + initializers.set(node.id.name, nodeText(source, node.init)); } - if (ts.isFunctionDeclaration(node) && node.name !== undefined) { - initializers.set(node.name.text, node.getText(sf)); + if (t.isFunctionDeclaration(node) && t.isIdentifier(node.id)) { + initializers.set(node.id.name, nodeText(source, node)); } - ts.forEachChild(node, visit); + forEachChild(node, visit); }; - visit(sf); + visit(ast); const ordering = new Set(); const referencesOrdering = (text: string): boolean => @@ -166,38 +177,38 @@ function collectOrderingNames(sf: ts.SourceFile): Set { // --------------------------------------------------------------------------- // Query units // --------------------------------------------------------------------------- -const isLiteralUnit = (node: ts.Node): boolean => - ts.isStringLiteralLike(node) || ts.isTemplateExpression(node); +const isLiteralUnit = (node: t.Node): boolean => + t.isStringLiteral(node) || t.isTemplateLiteral(node); /** Climb `+` concatenation so a query split across fragments is ONE unit. */ -function concatenationRoot(node: ts.Node): ts.Node { - let current = node; +function concatenationRoot(node: AstNode): AstNode { + let current: AstNode = node; while ( current.parent !== undefined && - ts.isBinaryExpression(current.parent) && - current.parent.operatorToken.kind === ts.SyntaxKind.PlusToken + t.isBinaryExpression(current.parent) && + current.parent.operator === '+' ) { current = current.parent; } return current; } -function collectQueryUnits(sf: ts.SourceFile): ts.Node[] { - const byPosition = new Map(); - const visit = (node: ts.Node): void => { +function collectQueryUnits(source: string, ast: t.Node): AstNode[] { + const byPosition = new Map(); + const visit = (node: AstNode): void => { if (!isLiteralUnit(node)) { - ts.forEachChild(node, visit); + forEachChild(node, visit); return; } // Never descend INTO a literal: a nested template inside `${...}` is // already covered by the outer unit's text. const unit = concatenationRoot(node); - const text = unit.getText(sf); + const text = nodeText(source, unit); if (LIMIT_RE.test(text) && (CYPHER_CLAUSE_RE.test(text) || BARE_LIMIT_FRAGMENT_RE.test(text))) { - byPosition.set(unit.getStart(sf), unit); + byPosition.set(nodeStart(unit), unit); } }; - visit(sf); + visit(ast as AstNode); return [...byPosition.entries()].sort((a, b) => a[0] - b[0]).map(([, node]) => node); } @@ -216,14 +227,14 @@ interface CommentBlock { end: number; } -const isFunctionLike = (node: ts.Node): boolean => - ts.isFunctionDeclaration(node) || - ts.isFunctionExpression(node) || - ts.isArrowFunction(node) || - ts.isMethodDeclaration(node) || - ts.isConstructorDeclaration(node) || - ts.isGetAccessorDeclaration(node) || - ts.isSetAccessorDeclaration(node); +const isFunctionLike = (node: t.Node): boolean => + t.isFunctionDeclaration(node) || + t.isFunctionExpression(node) || + t.isArrowFunctionExpression(node) || + t.isClassMethod(node) || + t.isClassPrivateMethod(node) || + t.isObjectMethod(node) || + t.isTSDeclareMethod(node); /** * Comment blocks a marker for `node` may live in, NEAREST FIRST: the query's @@ -239,22 +250,21 @@ const isFunctionLike = (node: ts.Node): boolean => * Consecutive `//` lines arrive as one range EACH, so they are merged back into * a block: a reason may then wrap across lines and still fit `printWidth: 100`. */ -function markerBlocks(source: string, node: ts.Node): CommentBlock[] { - const nearestFirst: ts.CommentRange[] = []; +function markerBlocks(source: string, node: AstNode): CommentBlock[] { + const nearestFirst: CommentRange[] = []; const seen = new Set(); - let current: ts.Node | undefined = node; + let current: AstNode | undefined = node; for (let depth = 0; current !== undefined && depth < 20; depth++) { - for (const range of [ - ...(ts.getLeadingCommentRanges(source, current.getFullStart()) ?? []), - ...(ts.getTrailingCommentRanges(source, current.getEnd()) ?? []), - ]) { + for (const range of [...leadingCommentRanges(current), ...trailingCommentRanges(current)]) { if (!seen.has(range.pos)) { seen.add(range.pos); nearestFirst.push(range); } } - const parent: ts.Node | undefined = current.parent; - if (parent === undefined || isFunctionLike(parent) || ts.isSourceFile(parent)) break; + const parent: AstNode | undefined = current.parent; + if (parent === undefined || isFunctionLike(parent) || t.isFile(parent) || t.isProgram(parent)) { + break; + } current = parent; } @@ -318,15 +328,15 @@ const reasonIsSubstantive = (reason: string): boolean => // --------------------------------------------------------------------------- export function analyzeSource(file: string, source: string): QueryFinding[] { if (!LIMIT_RE.test(source)) return []; - const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true); - const ordering = collectOrderingNames(sf); + const { ast } = parseTypeScript(file, source); + const ordering = collectOrderingNames(source, ast); /** A marker declares ONE query: a claimed range cannot also cover the next. */ const claimed = new Map(); - return collectQueryUnits(sf).map((unit) => { - const text = unit.getText(sf); + return collectQueryUnits(source, ast).map((unit) => { + const text = nodeText(source, unit); const limitOffset = Math.max(text.search(LIMIT_RE), 0); - const line = sf.getLineAndCharacterOfPosition(unit.getStart(sf) + limitOffset).line + 1; + const line = lineAt(source, nodeStart(unit) + limitOffset); const excerpt = text.replace(/\s+/g, ' ').slice(0, 160); const base = { file, line, excerpt }; diff --git a/gitnexus/test/unit/skill-evolution-workflow.test.ts b/gitnexus/test/unit/skill-evolution-workflow.test.ts index 6916c7028..0b46e2736 100644 --- a/gitnexus/test/unit/skill-evolution-workflow.test.ts +++ b/gitnexus/test/unit/skill-evolution-workflow.test.ts @@ -39,6 +39,7 @@ const workflowDocument = load(workflow) as { 'timeout-minutes'?: unknown; with?: Record; env?: Record; + 'working-directory'?: string; }>; } >; @@ -476,16 +477,26 @@ exit 1`); expect(containment).toContain('gitnexus-cursor-integration/skills/gitnexus-review/*'); }); - it('installs node_modules for the monorepo root, gitnexus-shared, and gitnexus', () => { - // The benchmark sandbox-copies node_modules from all three (tasks.scenarios.yaml). - // The root tree was absent on the first real run because only the two subpackage - // steps ran, so capture_task_dependency_binding aborted at task binding. + it('installs node_modules for the monorepo root and gitnexus, and compiles shared from the parent', () => { + // The benchmark sandbox-copies node_modules from root and gitnexus + // (tasks.scenarios.yaml). Shared is compiled by gitnexus `npm run build` + // (scripts/build.js); a dedicated npm ci in gitnexus-shared stalls CI. const rootStep = findStep('Install monorepo root dependencies'); expect(rootStep).toBeDefined(); expect(rootStep).not.toHaveProperty('working-directory'); // installs at the repo root expect(String(rootStep?.run)).toContain('npm ci'); - expect(stepRun('Build pinned shared runtime')).toContain('npm ci'); - expect(stepRun('Install and build pinned GitNexus runtime')).toContain('npm ci'); + expect(findStep('Build pinned shared runtime')).toBeUndefined(); + const gitnexusRun = stepRun('Install and build pinned GitNexus runtime'); + expect(gitnexusRun).toContain('npm ci'); + expect(gitnexusRun).toContain('npm run build'); + expect(gitnexusRun).toContain('mkdir -p ../gitnexus-shared/node_modules'); + expect( + evolveJob?.steps?.some( + (step) => + step['working-directory'] === 'gitnexus-shared' && + String(step.run ?? '').includes('npm ci'), + ), + ).toBe(false); }); it('waits for the runner boot-time package lock before installing containment tools', () => { diff --git a/package-lock.json b/package-lock.json index f3d31e09a..f34ffa491 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "husky": "^9.1.7", "lint-staged": "^15.5.0", "prettier": "^3.8.0", - "prettier-plugin-tailwindcss": "^0.7.0" + "prettier-plugin-tailwindcss": "^0.7.0", + "typescript": "^5.9.3" } }, "node_modules/@babel/code-frame": { @@ -626,6 +627,189 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "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.0.0" + } + }, "node_modules/@typescript-eslint/parser": { "version": "8.57.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", @@ -651,7 +835,35 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/project-service": { + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { "version": "8.57.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", @@ -673,6 +885,23 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "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.0.0" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.57.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", @@ -691,48 +920,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", - "dev": true, - "license": "MIT", - "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.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", - "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/@typescript-eslint/types": { "version": "8.57.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", @@ -747,58 +934,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "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.0.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.57.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", @@ -943,9 +1078,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2154,13 +2289,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -2520,9 +2655,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -2669,14 +2804,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -2704,9 +2839,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -2761,7 +2896,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 8a732cb14..ff2620523 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "husky": "^9.1.7", "lint-staged": "^15.5.0", "prettier": "^3.8.0", - "prettier-plugin-tailwindcss": "^0.7.0" + "prettier-plugin-tailwindcss": "^0.7.0", + "typescript": "^5.9.3" }, "lint-staged": { "*.{ts,tsx}": [