Skip to content

[DevEx]: Add TypeScript tooling, ESLint TS parser, typecheck scripts, and CI jobs #1400

Open

Description

@itsalaidbacklife

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 service
  • vue-tsc — the SFC-aware wrapper; plain tsc cannot parse .vue files at all
  • typescript-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 typecheck runs and exits 0 on main
  • Both CI jobs are present, blocking, and green
  • A deliberate error in a .ts file fails the backend job
  • A deliberate error inside a <script setup lang="ts"> block fails the frontend job
  • npm run lint passes on a .ts file and on a <script setup lang="ts"> component — without this, no TS can merge at all
  • npm run lint and npm run test:unit unaffected
  • No change to npm run build output

Activity

  1. added
    dev experienceImprovements to the code base that make it easier/better/more enjoyable to contribute to Cuttle
    backendRequires changes to the (node) backend webserver
    frontendRequires changes to the frontend (vue) client
    on Oct 6, 2026
  2. changed the title [-][DevEx]: Add TypeScript and vue-tsc tooling with typecheck scripts[/-] [+][DevEx]: Add TypeScript tooling, typecheck scripts, and both CI jobs[/+] on Oct 8, 2026
  3. changed the title [-][DevEx]: Add TypeScript tooling, typecheck scripts, and both CI jobs[/-] [+][DevEx]: Add TypeScript tooling, ESLint TS parser, typecheck scripts, and CI jobs[/+] on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendRequires changes to the (node) backend webserverdev experienceImprovements to the code base that make it easier/better/more enjoyable to contribute to CuttlefrontendRequires changes to the frontend (vue) client

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions