TypeScript in this repo
New code is .ts, including tests, scripts, and Vite plugins. Do not add new
.js. testem.cjs is the only CommonJS file.
npm run type-check (tsc --noEmit) and npm run lint both gate PRs via the
Lint workflow. Run npm run type-check and npm run lint:fix after any edit
under sources/, scripts/, vite/, or tests/ — all four are in
tsconfig.json include. tsc is the typescript package, which this repo
aliases to @typescript/typescript6. @typescript/native is also in
devDependencies and is unused by workflows.
Erasable syntax only
Node runs first-party .ts by type-stripping it, with no tsx and no compile
step. erasableSyntaxOnly is on, so no enums, no namespaces, no parameter
properties. Anything that would need to emit runtime code is a type error.
Relative imports use the extension of the file on disk, so a .ts file
importing a still-.js module writes .js:
import { markNonExecutableLinesInLcov } from "./mark-non-executable-lines.js";
allowImportingTsExtensions is on, so importing ./foo.ts is correct once the
target is converted. Getting this backwards is the most common failure when
converting a file — every importer has to move with it.
tsconfig deliberately relaxes three strictest flags
tsconfig.json extends @tsconfig/strictest and then
turns off noUncheckedIndexedAccess, exactOptionalPropertyTypes, and
noPropertyAccessFromIndexSignature, each with a comment explaining why. Do not
re-enable them as a drive-by; pixel-index loops and metadata config bags depend
on them being off.
Lint rules that bite
- Unused bindings that must exist need a
_prefix.argsIgnorePattern,varsIgnorePattern, andcaughtErrorsIgnorePatternare all^_. console.*is an error exceptconsole.error. Useconsole.error, ordebugLog/debugWarn/debugGroup/debugGroupEnd/debugTablefromsources/utils/debug.ts, which are gated on localhost or?debug=.eslint.config.jsappliestypescript-eslint's recommended preset to**/*.tsonly, and layers per-directory blocks forsources/,scripts/+vite/,tests/,tests/node/, andtests/visual/. A converted file moves from the.jsblock to the.tsblock and can pick up rules it was never subject to, so lint the file after renaming, not before.
Converting a .js file
Four non-test .js files remain:
scripts/coverage/mark-non-executable-lines.jsvite/vite-plugin-coverage-collect.jseslint.config.jsplaywright.config.js
The rest are under tests/. Ordinary *_spec.js files are not named by
hardcoded path. These harness files are:
tests/tests.js— named intests_run.htmltests/bdd-globals.js— themocha-globalsalias target invite.config.ts, in two HTML import maps (scripts/zip/zip-export-profile-runner.html,tests/fixtures/issue-382/issue382-golden-runner.html), and asserted by path intests/node/scripts/generateSources/vite_config_factory_and_resolve_spec.tstests/node/run-node-tests.js— named intestem.cjsbefore_teststests/vitest-setup.js— imported bytests/tests.jstests/testem-firefox-user.js— named intestem.cjs
Before renaming anything, grep for the filename. Breaking a harness path fails a command rather than the type-check:
scripts/coverage/mark-non-executable-lines.js— named inpackage.jsontest:node:coverage, and imported bymerge-browser-coverage.ts.vite/vite-plugin-coverage-collect.js— imported byvite.config.ts.
Checklist for a conversion:
git mvthe file, then update every importer to the new extension.- Update hardcoded paths in
package.json,testem.cjs, HTML files, and any spec that asserts on the path. npm run type-check— the file is newly checked, so expect real errors.npm run lint:fix— it may have moved into a stricter ESLint block.- Run whatever command names the file, not just the test suite. A broken
before_testspath or import map does not surface as a test failure. - Check
codecov.ymlignore:. If the file is not ignored, it now owes covered lines: coverage.
Adding a .ts browser spec
Canvas already has several TypeScript specs (renderer_spec.ts,
renderer-composite_spec.ts, load-images_spec.ts, and the
palette-recolor-*_spec.ts files). Leftover *_spec.js files under
tests/ stay JS until converted. Two things differ from
those leftover .js specs:
Register it with its real extension, since relative imports use the on-disk extension:
import "./canvas/renderer-composite_spec.ts";It is type-checked under
@tsconfig/strictestas soon as it exists, which leftover.jsspecs are not (checkJsis off). Runnpm run type-check; annotate DOM hosts, sinon sandboxes, and catalog handles the wayrenderer-composite_spec.tsalready does.
Everything else about writing the spec is unchanged: write-spec.