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 + .ts on plain Node with no compile step. Do NOT add a tsc build, do NOT introduce "type": "module", do NOT “fix” a type error with any, and remember a .ts module is only reachable via require('./x.ts') (explicit extension). Root-level *.ts files are invisible to both ESLint and tsconfig — 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 runtimenode 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 supervisor command= passes node flags — landed in 22.18. Verified empirically (2026-07-22, FKITDEV-8387): node:22.6 running a .ts entrypoint dies with SyntaxError: Missing initializer in const declaration (it is parsing TS as JS); node:22.18 runs it.

Where the version actually comes from:

SurfaceNode version
package.json engines (vuer_oss / vuer_css / portal_css)>= 22.18.0
vuer_build, vuer_docker images, CI (NODE_VERSION)24.x
vuer-releasefloating 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.ts rename merge, grep for extensionless require()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 gate n/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 *.ts is unchecked by both ESLint and tsc. To lint a root entrypoint you must add '*.ts' (or the specific path) to the TS block’s files. Doing so on vuer_oss also newly lints playwright.config.ts, which had an extensionless require.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:

RepoTransformer
vuer_oss@swc/jesttransform: { '^.+\\.tsx?$': ['@swc/jest'] }
vuer_cssts-jest
portal_cssts-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.