chore: compile first-party packages with TypeScript 7 (#3311)
Some checks are pending
CodeQL / Analyze (python) (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* 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 <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Gergő Magyar 2026-09-17 22:16:00 +01:00 • committed by GitHub
parent a2e1710ac0
commit 56feb85c97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1548 additions and 347 deletions

View file

@ -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).

View file

@ -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

View file

@ -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

View file

@ -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.

6
DoD.md
View file

@ -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

View file

@ -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
```

View file

@ -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
```

View file

@ -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": {

View file

@ -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"

View file

@ -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'");
}
});
});

View file

@ -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"]
}

View file

@ -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"]

View file

@ -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"
}

View file

@ -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, '')),
},

View file

@ -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, '')),
},

View file

@ -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": {

View file

@ -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": {

View file

@ -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(

View file

@ -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<ts.Expression> | 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);
};
/**

View file

@ -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/<lang>/captures.ts`, and
* `export-detection.ts`. Collected BY CONSUMPTION SITE: `<n>.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/<lang>/`
* tree-sitter SyntaxNode (via the TypeScript 7 Checker). Per-`languages/<lang>/`
* files tag to that one grammar; shared (non-`languages/<lang>/`) 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<string, ts.SourceFile | undefined>();
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<string, string[]>();

View file

@ -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<Parameters<typeof parse>[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<string, unknown>)[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);
}

View file

@ -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);
}

View file

@ -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<string, string> };
@ -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);
});

View file

@ -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<string>();
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();

View file

@ -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 <name>: 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:

View file

@ -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<string> {
function collectOrderingNames(source: string, ast: t.Node): Set<string> {
const initializers = new Map<string, string>();
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<string>();
const referencesOrdering = (text: string): boolean =>
@ -166,38 +177,38 @@ function collectOrderingNames(sf: ts.SourceFile): Set<string> {
// ---------------------------------------------------------------------------
// 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<number, ts.Node>();
const visit = (node: ts.Node): void => {
function collectQueryUnits(source: string, ast: t.Node): AstNode[] {
const byPosition = new Map<number, AstNode>();
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<number>();
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<number, number>();
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 };

View file

@ -39,6 +39,7 @@ const workflowDocument = load(workflow) as {
'timeout-minutes'?: unknown;
with?: Record<string, unknown>;
env?: Record<string, string>;
'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', () => {

362
package-lock.json generated
View file

@ -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"

View file

@ -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}": [