Repository navigation
[DevEx]: Add TypeScript tooling, ESLint TS parser, typecheck scripts, and CI jobs #1400
Description
Part of #1399.
Summary
Add the type-checking tools, the npm run typecheck:* scripts, and both CI jobs. checkJs stays off — flipping it is #1404 and #1409.
Important
Scope widened 2026-10-08. This issue originally deferred all CI wiring to #1404 / #1409. That was wrong once the project adopted TS for frontend code: checkJs governs only .js files — .ts files and <script setup lang="ts"> are always checked. So the gates can land here, green, and start protecting TS code immediately.
Measured on main (71a0f2d) with checkJs: false:
frontend 0 errors backend 0 errors
and with the same checkJs: false config, deliberate errors in TS sources are still caught:
SpectatorListMenu.vue(54,7) error TS2322 (<script setup lang="ts">)
src/util/probe.ts(1,14) error TS2322
api/helpers/probe.ts(1,53) error TS2322
Without the jobs here, every file converted by #1422–#1427 and #1428 would land unchecked until #1409 — which sits at the very end of the plan, behind the whole Options API migration.
Dependencies — needs approval
This adds three devDependencies, none currently in package-lock.json:
typescript— the compiler/language servicevue-tsc— the SFC-aware wrapper; plaintsccannot parse.vuefiles at alltypescript-eslint— required, not optional (see below)
ESLint blocks all TypeScript until this lands
npm run lint is a blocking CI job, and this repo has no TypeScript-aware ESLint parser — eslint.config.js uses vue-eslint-parser, which delegates <script> to espree. Every form of TS syntax currently fails to parse, measured on main:
const x: string = 'a'; -> Parsing error: Unexpected token : (.vue, lang="ts")
defineProps<{ n: number }>(); -> Parsing error: Unexpected token ) (.vue, lang="ts")
export const y: string = 'b'; -> Parsing error: Unexpected token : (plain .ts)
This is a hard blocker on every TS issue in the plan — the stores (#1422–#1427), #1428, and the lang="ts" targets in #1405–#1408 and #1412. None can merge before this one does. It already cost a round trip on #1415, where a lang="ts" change was requested and had to be withdrawn.
Verified fix — [email protected] with the parser wired for both file types, which makes all four probes parse cleanly:
const tseslint = require('typescript-eslint');
{ files: ['**/*.ts'], languageOptions: { parser: tseslint.parser } },
{ files: ['**/*.vue'], languageOptions: {
parser: vueParser,
parserOptions: { parser: tseslint.parser, ecmaVersion: 'latest', sourceType: 'module' },
} },Note typescript-eslint declares typescript: ">=4.8.4 <6.1.0" — independent confirmation that TypeScript must be pinned to 5.x, since the auto-resolved 7.0.2 falls outside its supported range too.
While in eslint.config.js, import/no-unresolved: off on line 22 references eslint-plugin-import, which is not installed and never has been. Worth deleting.
Per AGENTS.md ("Do not add, update, or remove dependencies without explicit approval") this needs a maintainer's sign-off before the PR is opened. Both are build-time only: nothing ships to users, the bundle is unchanged, and Vite's resolution is untouched.
Pin both, exactly — vue-tsc alone is not enough and ^ ranges break it
vue-tsc ships only a vue-tsc binary. The backend script runs tsc, which comes from the typescript package, so typescript is required outright — not merely as a transitive dependency.
It is also a peerDependency of vue-tsc (typescript: ">=5.0.0"), not a regular one. npm 7+ auto-installs peers, so npm i vue-tsc does put a working tsc in node_modules/.bin — but it resolves that loose range to the latest TypeScript, and both current vue-tsc majors crash against it. Tested in a clean directory:
npm i [email protected] -> typescript 7.0.2 auto-installed
vue-tsc -> Error [ERR_PACKAGE_PATH_NOT_EXPORTED]:
Package subpath './lib/tsc' is not defined by "exports" in typescript/package.json
npm i [email protected] -> typescript 7.0.2 auto-installed
vue-tsc -> throws in vue-tsc/index.js:68
+ npm i [email protected] -> both majors work correctly (exit 2, error reported)
TypeScript 7 removed the ./lib/tsc subpath vue-tsc reaches into, and the >=5.0.0 peer range does not exclude it. So:
"devDependencies": {
"typescript": "5.6.3",
"vue-tsc": "3.3.12",
"typescript-eslint": "8.71.1"
}Exact versions, no ^. With a caret this breaks the next time anyone regenerates the lockfile, and it fails as a stack trace rather than a type error — confusing to debug.
Verified on Node 22.21 with [email protected]. Either vue-tsc major works once typescript is pinned; 3.3.12 is the current one. Measured on this repo they are equivalent — 420 errors under 2.2.10, 421 under 3.3.12, the sole difference being src/routes/home/components/GameListItem.vue (2 → 3). Every per-file count quoted in the sibling issues holds under both.
Note
Because the loose peer range is the actual hazard, consider whether a CI step should assert the resolved version — otherwise this failure mode returns silently via a lockfile refresh.
Two tsconfigs, not one
The backend is CommonJS and the frontend is ESM + SFCs, so a single config cannot describe both. These two were used for the measurements in #1399 and are known to work against this codebase. Both start with checkJs: false — flipping it is #1404 and #1409.
tsconfig.backend.json:
{
"compilerOptions": {
"allowJs": true, "checkJs": false, "noEmit": true,
"module": "commonjs", "moduleResolution": "node", "target": "es2022",
"resolveJsonModule": true, "baseUrl": ".",
"paths": { "_/*": ["./*"], "@/*": ["./src/*"] },
"skipLibCheck": true
},
"include": ["api/**/*.js", "utils/**/*.js", "types/**/*.js"]
}tsconfig.frontend.json:
{
"compilerOptions": {
"allowJs": true, "checkJs": false, "noEmit": true,
"module": "esnext", "moduleResolution": "bundler", "target": "es2022",
"resolveJsonModule": true, "baseUrl": ".",
"paths": { "_/*": ["./*"], "@/*": ["./src/*"] },
"skipLibCheck": true, "jsx": "preserve",
"types": ["vite/client"],
"lib": ["es2022", "dom", "dom.iterable"]
},
"include": ["src/**/*.js", "src/**/*.vue"]
}types: ["vite/client"] matters — without it every import.meta.env access errors. With it, that entire category drops to zero (verified).
Scripts
"typecheck:backend": "tsc -p tsconfig.backend.json",
"typecheck:frontend": "vue-tsc --noEmit -p tsconfig.frontend.json",
"typecheck": "npm run typecheck:backend && npm run typecheck:frontend"CI jobs
.github/workflows/run-npm-script.yml is already a reusable job taking a label and a script — the same mechanism lint and test-unit use — so each gate is five lines in .github/workflows/pr-actions.yml:
typecheck-backend:
uses: ./.github/workflows/run-npm-script.yml
needs: draft
with:
label: 'Type Check: Backend'
script: 'npm run typecheck:backend'
typecheck-frontend:
uses: ./.github/workflows/run-npm-script.yml
needs: draft
with:
label: 'Type Check: Frontend'
script: 'npm run typecheck:frontend'Both are green on main today, so they can be blocking from the start — there is no baseline to carry and no ratchet to maintain.
The frontend job must run vue-tsc, never tsc. Measured on the same config and file set:
| tool | .js errors |
.ts errors |
.vue errors |
|---|---|---|---|
tsc |
27 | 2 | 0 |
vue-tsc |
5 | 2 | 423 |
vue-tsc is a strict superset on non-component files — byte-identical output to tsc on a plain src/**/*.ts probe — and additionally resolves and checks .vue, which tsc cannot see at all.
Worse, tsc on frontend code does not merely under-report: it emits false positives, 12 × TS2307: Cannot find module '@/routes/.../*.vue' in src/router.js alone, because it cannot resolve SFC imports. The inflated .js count above is mostly that.
Rule of thumb: if the import graph reaches a .vue, it needs vue-tsc. The backend has none, so tsc is correct (and faster) there.
Confirm a deliberate error inside a lang="ts" SFC actually turns the job red, not just one in a .ts file — a config mistake here fails open.
One thing to settle while implementing
CI can point at any filename, but editors pick the nearest tsconfig.json / jsconfig.json walking up from the file being edited — they will not find tsconfig.backend.json. So the red-squiggle half of #1399 depends on where these live.
The likely answer is config files at api/tsconfig.json and src/tsconfig.json (each extends a shared base), leaving the root jsconfig.json to cover utils/, tests/, and config/. That is standard resolution behavior, but the exact placement was not verified during measurement — worth confirming in the editor as part of this PR, since every later issue inherits the choice.
Acceptance
npm run typecheckruns and exits 0 onmain- Both CI jobs are present, blocking, and green
- A deliberate error in a
.tsfile fails the backend job - A deliberate error inside a
<script setup lang="ts">block fails the frontend job npm run lintpasses on a.tsfile and on a<script setup lang="ts">component — without this, no TS can merge at allnpm run lintandnpm run test:unitunaffected- No change to
npm run buildoutput
Activity
[-][DevEx]: Add TypeScript and vue-tsc tooling with typecheck scripts[/-][+][DevEx]: Add TypeScript tooling, typecheck scripts, and both CI jobs[/+][-][DevEx]: Add TypeScript tooling, typecheck scripts, and both CI jobs[/-][+][DevEx]: Add TypeScript tooling, ESLint TS parser, typecheck scripts, and CI jobs[/+]