How TypeScript actually works across vuer_oss, vuer_css and portal_css: there is no build step, Node strips types at runtime, files stay CommonJS, a cross-module require of a .ts module needs an explicit .ts extension, and root-level *.ts is silently unlinted. Established by FKITDEV-8246 “TS Magic” (vuer_oss PR #7645, commit 55035572bb, marked BREAKING CHANGE) — the technical foundation for the FKITDEV-8251 TypeScript-introduction epic.
For Agents
These three repos run mixed
.js+.tson plain Node with no compile step. Do NOT add atscbuild, do NOT introduce"type": "module", do NOT “fix” a type error withany, and remember a.tsmodule is only reachable viarequire('./x.ts')(explicit extension). Root-level*.tsfiles are invisible to both ESLint andtsconfig— assume they are unchecked.
No build step, no typecheck job
tsconfig.json is configured noEmit: true + erasableSyntaxOnly: true. Nothing ever runs tsc. There is no typecheck job in CI in any of the three repos — the PR workflow (.github/workflows/pull-request.yaml) runs Lint / Unit Tests / Audit (+ Sonar / Build) only. TypeScript exists purely for the editor and for ESLint; type errors do not fail CI.
erasableSyntaxOnly forbids any TS construct that emits runtime code — no enums, no parameter properties, no namespaces. Everything must be erasable (type annotations, interface, type, as, generics).
Runtime type-stripping (Node >= 22.18)
engines.node is >= 22.18.0. Node strips the type annotations natively at runtime — node entry.ts just runs the file. There is no transpile artifact and no source map; the running file is the .ts file.
The floor is 22.18, NOT 22.6
Node 22.6 shipped type stripping only behind
--experimental-strip-types. Unflagged stripping — which is what these repos rely on, since no supervisorcommand=passes node flags — landed in 22.18. Verified empirically (2026-07-22, FKITDEV-8387):node:22.6running a.tsentrypoint dies withSyntaxError: Missing initializer in const declaration(it is parsing TS as JS);node:22.18runs it.
Where the version actually comes from:
| Surface | Node version |
|---|---|
package.json engines (vuer_oss / vuer_css / portal_css) | >= 22.18.0 |
vuer_build, vuer_docker images, CI (NODE_VERSION) | 24.x |
vuer-release | floating NODE_VERSION: 22 — above the floor today, but not pinned to >= 22.18 |
Files stay CommonJS
There is no "type": "module" anywhere. TS files keep require() / module.exports. erasableSyntaxOnly guarantees the TS syntax is a pure overlay on the existing CommonJS — no ESM conversion happened.
Cross-module require needs an explicit .ts extension
A require() of a .ts module must spell out the .ts extension. Verified: extensionless require('./dep') throws MODULE_NOT_FOUND when only dep.ts exists — Node does not add .ts to its default resolution probe list. This is why the codebase reads require('../util/magic.ts') at its call sites.
Merge trap
After a cross-side
.js→.tsrename merge, grep for extensionlessrequire()s of the renamed modules. A conflict-free merge can keep an old extensionless caller that no longer resolves. This bit the NÚSZ devel-update (cron.js:46, caught by the lint gaten/no-missing-require) — see nusz-devel-update-2026-06-16-lint-merge-fix.
Root-level *.ts is silently unlinted
ESLint flat config lints .ts only where some config object’s files array names the extension. The TS block is:
files: ['server/**/*.ts', 'customization/**/*.ts', 'client/**/*.ts']A root-level server.ts matches none of these → ESLint reports “File ignored because no matching configuration was supplied” and lints nothing. tsconfig.json’s include has the same blind spot (its globs are server/**, client/**, customization/**) — a root *.ts is outside the type-check surface too.
Silent gap
Anything dropped at the repo root as
*.tsis unchecked by both ESLint andtsc. To lint a root entrypoint you must add'*.ts'(or the specific path) to the TS block’sfiles. Doing so on vuer_oss also newly lintsplaywright.config.ts, which had an extensionlessrequire.resolve('./…/global-teardown')that only works because Playwright patches module resolution — see FKITDEV-8387.
Jest transforms .ts
Tests run TS through a transform, not native Node stripping:
| Repo | Transformer |
|---|---|
| vuer_oss | @swc/jest — transform: { '^.+\\.tsx?$': ['@swc/jest'] } |
| vuer_css | ts-jest |
| portal_css | ts-jest |
no-explicit-any is an ERROR
@typescript-eslint/no-explicit-any is set to error, not warn. Never resolve a type error with any — it fails yarn lint (--max-warnings 0). Narrow the type instead.
Cheap erasable-syntax check
Node exposes
require('node:module').stripTypeScriptTypes(src). Feeding a file’s source to it is a fast way to assert the file is erasable-syntax clean — it throws on enums / parameter properties / namespaces. Used in FKITDEV-8387 to confirm all seven vuer_oss entrypoints strip cleanly.
Related
- FKITDEV-8246 — “TS Magic”, the PR that established this (vuer_oss #7645)
- FKITDEV-8251 — the parent TypeScript-introduction epic
- FKITDEV-8387 — TypeScript-ify the supervisor entrypoints (uses this mechanism; landed as a direct rename, no shims)
- unversioned-partner-supervisor-overlays — why shipping renamed
.tsentrypoints is a release-sequencing problem - nyc-cannot-load-typescript — the one tool that breaks on
.ts(coverage) - entrypoint-rename-blast-radius — why renaming a
.jsentrypoint to.tsis a 4-repo change - nusz-devel-update-2026-06-16-lint-merge-fix — the
.js→.tsmerge trap in practice - vuer_oss · vuer_css · portal_css