Skip to main content

The bundle

There are no imports​

Every .ts file under src/ is concatenated, in the order listed in tsconfig.json, into a single build/index.js that the host loads. All files share one global scope: a function or var declared in data.ts is simply visible in play.ts, with no import anywhere.

Two consequences to internalise:

  • Order in tsconfig.json matters — for load-time work only. Anything that executes as the bundle evaluates (a top-level table, the registerSkill(...) calls in every skill file, var gPages = new PageRouter()) must be listed after what it depends on. Function declarations hoist across the whole bundle, so calls made at runtime are order-independent.
  • Name collisions are silent. Two files declaring the same symbol will not error; the later one wins. index.ts is concatenated near the end and is deliberately kept thin for that reason.

The file list carries a comment per slot saying why it is where it is:

app.gap.Tsum/tsconfig.json
loading...

Tsum is typed by declaration merging​

Tsum is a class in tsum.ts, but every one of its methods is attached from outside the class body as Tsum.prototype.name = function ..., spread over twenty-odd files. With no modules a class cannot be reopened, and splitting the object across files is the whole reason the package is not one enormous file.

The two halves are joined by declaration merging: interface Tsum in globals.d.ts declares every method, and TypeScript merges the interface into the class of the same name. That is what makes ts.foo() checked and find-referenceable across the bundle, lets each Tsum.prototype.name = function (...) take its this and parameter types from the interface rather than annotating them again, and turns a method defined under a name nothing declares into an error.

Adding a method to Tsum therefore means adding its signature to interface Tsum as well. The interface is grouped by the file that implements each member:

app.gap.Tsum/src/globals.d.ts
loading...

The same idea one level down is why Button, Page and the log tables carry no type annotation: a broad index signature would erase the key set, and with it both go-to-definition and any chance of catching a misspelt key. Page uses satisfies PageMap, which validates each entry without losing its keys.

The string vocabularies are const enums​

Several values are passed around as bare strings and are what most branching tests. Each set is a const enum, and code refers to members, never to the string:

EnumDeclared inNames
PageNamedata.ts, above the Page tableevery screen the router can report
SkillTypeshared.d.tsevery entry in the Skill Type dropdown
SettingKeyshared.d.tsevery setting; interface Settings is keyed from it, so the enum and the object that crosses the bridge are one list
RecordKey, Localeshared.d.tsthe keys of hearts.json; the language tags
LoglogEvents.tsevery log event name, one enum per component inside a namespace
EmitscriptEvents.tsevery event broadcast to outside tooling
SkillReadiness, KeyCodeglobals.d.tsthe gauge read's answer; the host's key codes

A const enum is erased at compile time, so it costs nothing at runtime, gives each name one definition to jump to and rename, and — the reason it matters here — is the only kind of shared constant that can span the three compilations, since they share no memory.

A misspelt member is an error everywhere. A correctly spelled raw string still compiles, so the rule is a convention reviewers hold: use the member.

Three compilations​

ConfigOutputTargetShares
tsconfig.jsonbuild/index.js — the game bundleES2023, strictshared.d.ts, logEvents.ts
tsconfig.settings.jsonbuild/settings.js — the settings pageES5 (the WebView), looserthe above plus the page-side files: the string catalogues, the option lists, presets.ts, runPlan.ts
tsconfig.quickbar.jsonbuild/quickbar.js — the Quick Bar pageES5, strictthe same page-side files

Only the first has a name an editor discovers automatically, so settings.ts opens with /// <reference> lines that exist purely so a TypeScript language server checks it against the right files. npm run typecheck runs these three and a fourth, tsconfig.workflow.json, which proves gapWorkflow.ts compiles on its own.

What ships is compacted, and only in ways that cannot change it​

tools/minify/minify.js runs terser over both outputs with compress: false and mangle: false: it parses and reprints without the formatting. Nothing is renamed, inlined, folded or dropped. A script that runs unattended for hours on a phone, with no source map and an error handler that logs String(e), cannot pay for a smaller file with a behaviour change. build/index.js is left alone entirely so the offline tools and a stack trace stay readable.

TypeScript is pinned to 6.x on purpose: 7 removes outFile and module: none, and the no-imports design depends on both. Moving would mean a bundler that emits a single IIFE and preserves global scope — a build migration, not a config edit.