@defold-typescript/docs 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @defold-typescript/docs
2
+
3
+ This package publishes the human-facing guide for the defold-typescript toolchain. The canonical source is [`guide/`](guide/README.md); consumers (the cli, the docs site) reach it through this package rather than the repo root.
@@ -0,0 +1,52 @@
1
+ ---
2
+ toc-title: Overview
3
+ ---
4
+ # Overview
5
+
6
+ Build your [Defold](https://defold.com/) game in [TypeScript](https://www.typescriptlang.org/) and get VSCode's full editor experience — autocomplete, inline type errors, and safe refactors — across the whole Defold API, while still shipping the plain [Lua](https://www.lua.org/) the engine runs.
7
+
8
+ - **The full Defold API, typed** — every module and namespace is typed from the official reference, so `go`, `gui`, `vmath`, `msg`, and the rest autocomplete and type-check as you write.
9
+ - **Compiles to plain Lua** — your TypeScript runs through the battle-tested [TypeScriptToLua](https://typescripttolua.github.io/) (TSTL) compiler down to the Lua Defold already runs: no engine fork, no proprietary runtime, no lock-in.
10
+ - **Typed scripts end to end** — `self`, `on_message`, and `on_input` payloads are typed through `defineScript`, `defineGuiScript`, and `defineRenderScript`.
11
+ - **Fits new and existing projects** — scaffold from scratch, or add the TypeScript surface to a project that already has `game.project` and adopt type safety gradually, one script at a time alongside your existing Lua.
12
+ - **Built for the real loop** — `watch` recompiles beside the Defold editor, live transpile diagnostics surface errors inline, and source maps let you set breakpoints in your `.ts`.
13
+
14
+ End-user documentation for `defold-typescript`: how to scaffold a project, write TypeScript that the toolchain compiles to Lua, and look up the language-and-toolchain quirks you will hit along the way. The repository's `README.md` covers why this project exists and the planning workflow; this folder covers how to use the toolchain.
15
+
16
+ The sections below mirror the top navigation; each lists the pages in its left-sidebar order.
17
+
18
+ ## Get started
19
+
20
+ - [Getting started](./getting-started.md) — install Bun, scaffold a new project with `bunx @defold-typescript/cli@latest init`, write a one-screen script, and build to Lua with `bunx @defold-typescript/cli build`.
21
+ - [Starter templates](./init-templates.md) — pick a new-project layout with `init --template <name>`: the opinionated `default` or a blank-script `minimal`.
22
+ - [Existing project](./add-typescript.md) — run `bunx @defold-typescript/cli@latest init` in a folder with `game.project` to add the TypeScript surface without replacing the Defold project.
23
+ - [Editor setup](./editor-setup.md) — open the project in VSCode, use the generated `tsconfig.json`, and run `bunx @defold-typescript/cli watch` beside the Defold editor.
24
+ - [Defold editor](./defold-editor.md) — install Defold, open the generated project folder, attach a compiled script (`.ts.script`, `.ts.gui_script`, or `.ts.render_script`) to its game object, GUI scene, or render pipeline, and run the game (TypeScript is transpiled to Lua by the CLI, not the editor).
25
+
26
+ ## Guides
27
+
28
+ - [Transpile diagnostics](./transpile-diagnostics.md) — the scaffolded `@defold-typescript/tstl-plugin` language-service plugin that surfaces TypeScript-to-Lua transpile errors live in the editor, advisory-only and never blocking `tsc --noEmit`.
29
+ - [Debugging](./debugging.md) — step through `.ts` source with breakpoints via the Local Lua Debugger and the scaffolded Bun launch path (no shell, Windows-native), resolving through the emitted `<name>.ts.script.map`.
30
+ - [Pinning the Defold version](./pinning-defold-version.md) — keep the default latest surface, or pin an older Defold version whose API surface is generated on the fly and materialized into a project-local `.defold-types/<version>/`.
31
+ - [Native extensions](./extensions.md) — declare an extension in `game.project` `[dependencies]`, run `defold-typescript resolve` to generate an ambient namespace per `.script_api` into a gitignored `.defold-types/extensions/` surface, and consume it with no import.
32
+ - [Advanced CLI](./advanced-cli.md) — opt-in per-directory API walls with the `wall` command (interactive checkbox and flag forms), the full-surface-by-default policy, and the import-from-subpath rule a wall depends on.
33
+ - [Agent runbooks](./agent-runbooks.md) — harness-neutral procedures for driving the CLI from an automated agent: scaffold a project, [install the agent contract](./agent-runbooks.md#install-the-agent-contract), regenerate extension types, [add and attach a script](./agent-runbooks.md#add-a-script) (build, wire the compiled component, verify), and fix the Lua output over the `--json` envelope, gating on `ok`.
34
+
35
+ ## Language
36
+
37
+ - [TypeScript vs Lua](./typescript-vs-lua.md) — the Lua-developer on-ramp: a cheat sheet that translates syntax, tables, modules, and the standard library from Lua to the TypeScript the toolchain expects.
38
+ - [Script lifecycle](./script-lifecycle.md) — type `self`, `on_message`, and `on_input` payloads with `defineScript`, `defineGuiScript`, and `defineRenderScript`.
39
+ - [Vector math](./vector-math.md) — the method-form arithmetic surface (`add`, `sub`, `mul`, `div`, `unm`) on `Vector3`, `Vector4`, `Quaternion`, and `Matrix4`, plus why you cannot write `v3 + v3`.
40
+ - [TypeScript gotchas](./typescript-gotchas.md) — the canonical catalog of TS / TSTL / Defold sharp edges. Today: the unary-minus quirk that silently produces `number` from a `Vector3`. Future entries land here as the toolchain encounters them.
41
+ - [API docs vs `ts-defold-types`](./api-docs-vs-ts-defold.md) — a factual, dimension-by-dimension comparison of the JSDoc that `@defold-typescript/types` and `ts-defold-types` emit (Markdown conversion, dash params, grid-aligned multi-line docs, branded constants, the `@example` trade-off), with a picker for which surface fits your project.
42
+ - [Migrating from `ts-defold`](./migrating-from-ts-defold.md) — move a project off the `@ts-defold/*` stack: the package/tooling map, the step-by-step port via `init` add-TypeScript mode and a `tsconfig.json` reconcile, and the type-surface differences you will hit.
43
+
44
+ ## Reference
45
+
46
+ - [API](/api) — the generated `@defold-typescript/types` reference: every documented Defold namespace, grouped alphabetically as cards (site-only; built from the typed surface the toolchain ships).
47
+ - [Lua standard library](/api/base) — the pure-Lua / LuaJIT reference category (`base`, `bit`, …). Types are owned by the `lua-types` dependency the `lua-stdlib-globals` goal adopted; `@defold-typescript/types` does not re-emit them as generated namespaces.
48
+
49
+ ## Coming later
50
+
51
+ - Per-module API guide — landing with `builtin-messages-typing` and `script-lifecycle-typing`.
52
+ - Messages guide — `msg.post` payload narrowing, builtin message IDs — landing with `builtin-messages-typing`.
@@ -0,0 +1,57 @@
1
+ ---
2
+ toc-title: Existing project
3
+ ---
4
+ # Add TypeScript to an existing Defold project
5
+
6
+ Use `bunx @defold-typescript/cli@latest init` inside a Defold project that already has `game.project`.
7
+
8
+ ```sh
9
+ cd my-existing-defold-game
10
+ bunx @defold-typescript/cli@latest init
11
+ bun install
12
+ ```
13
+
14
+ Scaffold with the `@latest` tag: `init` writes your `@defold-typescript/types`
15
+ version pin, so a stale `bunx` cache would pin an older release. Then run
16
+ `bun install` once — `init` only declares the dev dependencies below; `install`
17
+ is what puts them in `node_modules` so the editor can resolve the Defold types.
18
+
19
+ The package is scoped — run it as `@defold-typescript/cli`. The bare
20
+ `bunx defold-typescript` resolves a nonexistent unscoped package and 404s unless
21
+ you have already installed `@defold-typescript/cli` locally.
22
+
23
+ When `game.project` exists, `init` does not synthesize a new Defold project. It only writes the TypeScript surface:
24
+
25
+ - `src/main.ts` — a starter entry script, written only when absent. `main.ts` is your source, not managed config, so an existing one is left untouched (and omitted from the reported `written` list) even under `--force`.
26
+ - `tsconfig.json`
27
+ - `package.json`
28
+
29
+ If `package.json` already exists, the command preserves its existing fields and merges these dev dependencies when they are missing:
30
+
31
+ - `@defold-typescript/types` — pinned to the CLI's own published version (the packages release in lockstep). This is the only `@defold-typescript/*` package your project needs; it is type-only and feeds the editor. The transpiler is a dependency of the CLI itself, pulled in when you run `build`/`watch`, so the scaffold does not add it.
32
+ - `@biomejs/biome`
33
+
34
+ If `package.json` does not exist, the command creates one.
35
+
36
+ ## Conflicting config files
37
+
38
+ `init` refuses to overwrite an existing TypeScript or defold-typescript config. Remove or move the conflicting file before running the command.
39
+
40
+ Conflicts today are:
41
+
42
+ - `tsconfig.json`
43
+ - `defold-typescript.config.ts`
44
+ - `defold-typescript.config.mts`
45
+ - `defold-typescript.config.js`
46
+
47
+ Pass `--force` to overwrite a conflicting TS config (in new-project mode, `--force` also lets `init` synthesize into a non-empty directory). `--force` overwrites the config wholesale; it does not merge fields, so any settings you had in `tsconfig.json` are replaced by the scaffold config. `--force` refreshes managed config only — it never overwrites `src/main.ts`, since that file is your source.
48
+
49
+ ## Build the Lua output
50
+
51
+ After initialization, write TypeScript under `src/` and run:
52
+
53
+ ```sh
54
+ bunx @defold-typescript/cli build
55
+ ```
56
+
57
+ The default `tsconfig.json` type-checks against `@defold-typescript/types` and writes generated Lua next to each `.ts` source. Files that call a lifecycle factory become Defold components: `defineScript` writes `<name>.ts.script`, `defineGuiScript` writes `<name>.ts.gui_script`, and `defineRenderScript` writes `<name>.ts.render_script`. Helper modules with no lifecycle factory write plain Lua modules such as `src/util.lua`, matching the `require(...)` path emitted for TypeScript imports. The scaffold also drops a `.gitignore` so generated component files, helper `.lua` modules under `src/`, and their `.map` siblings stay out of version control. Set a concrete `outDir` to collect the outputs under a separate tree instead.
@@ -0,0 +1,101 @@
1
+ ---
2
+ toc-title: Advanced CLI
3
+ ---
4
+ # Advanced CLI
5
+
6
+ This page covers `wall`, the opt-in command for narrowing the API surface of a
7
+ source directory to a single script kind.
8
+
9
+ ## Full surface by default
10
+
11
+ Defold scopes two namespaces to a script kind: `gui.*` resolves only inside a
12
+ `.gui_script`, and `render.*` only inside a `.render_script`. Every other
13
+ namespace (`go`, `msg`, `vmath`, `sys`, `physics`, …) is available in every kind.
14
+
15
+ By default `defold-typescript` gives you the **full** `@defold-typescript/types`
16
+ surface everywhere. `init`, `build`, and `watch` never change this: they scaffold
17
+ and build against whatever entrypoint your `tsconfig` names and never add, remove,
18
+ or prune a wall. The full surface never rejects a call the engine would allow, but
19
+ it also can't catch a `gui.*` use in a plain `.script`. To get the engine's wall
20
+ at compile time, opt in with `wall`.
21
+
22
+ ## What a wall is
23
+
24
+ A wall is a composite `tsconfig.json` written into a single-kind source directory
25
+ that narrows `compilerOptions.types` to that kind's subpath:
26
+
27
+ | Script kind | `types` entrypoint | Namespaces |
28
+ | ----------- | ------------------ | ---------- |
29
+ | `.script` | `@defold-typescript/types/script` | universal only |
30
+ | `.gui_script` | `@defold-typescript/types/gui-script` | universal + `gui` |
31
+ | `.render_script` | `@defold-typescript/types/render-script` | universal + `render` |
32
+
33
+ The root `tsconfig.json` references each walled directory and excludes it from the
34
+ root program, so `tsc -b --noEmit` builds every walled directory against only its
35
+ narrowed surface — a `render.*` use inside a gui-walled directory becomes a compile
36
+ error, while the rest of the project stays full-surface.
37
+
38
+ A directory is **eligible** to be walled only when every `.ts` source in it is one
39
+ kind; a directory mixing kinds cannot be walled, because no single narrowing
40
+ applies. `build` and `watch` never touch walls — they are entirely yours to
41
+ manage.
42
+
43
+ ## Interactive
44
+
45
+ Run `wall` with no arguments in a terminal:
46
+
47
+ ```sh
48
+ bunx @defold-typescript/cli wall
49
+ ```
50
+
51
+ You get a checkbox of every eligible source directory, pre-checked to the
52
+ directories already walled. Checking an unwalled directory walls it; unchecking a
53
+ walled directory removes its wall. Mixed-kind directories appear disabled, with
54
+ their competing kinds shown as the reason. The final selection **is** the desired
55
+ wall set — the command reconciles the project on disk to exactly what you checked.
56
+
57
+ ## Flags
58
+
59
+ For agents, CI, and scripted use, pass directories explicitly (a bare `wall` with
60
+ no TTY errors rather than hanging on an unrenderable prompt):
61
+
62
+ ```sh
63
+ # Wall these directories (added to any already walled)
64
+ bunx @defold-typescript/cli wall src/ui src/rendering
65
+
66
+ # Remove a wall
67
+ bunx @defold-typescript/cli wall --remove src/ui
68
+
69
+ # List current and eligible walls (writes nothing)
70
+ bunx @defold-typescript/cli wall --list
71
+ bunx @defold-typescript/cli wall --list --json
72
+ ```
73
+
74
+ `--json` emits the resulting `directoryWalls` (and, for `--list`, the `eligible`
75
+ set) for machine consumption. `--json` is machine-driven intent, so a bare
76
+ `wall --json` never opens the interactive menu — pass directories or `--list`.
77
+
78
+ ## Import the factory from the kind subpath
79
+
80
+ A wall only holds if a walled source imports its lifecycle factory from the **kind
81
+ subpath**, not the main entry:
82
+
83
+ ```ts
84
+ // src/ui/hud.ts — inside a gui wall
85
+ import { defineGuiScript } from "@defold-typescript/types/gui-script"; // correct
86
+ ```
87
+
88
+ Importing the same factory from the main `@defold-typescript/types` entry pulls
89
+ *every* `declare global` namespace (including `render`) into the wall's program and
90
+ silently defeats the narrowing — `render.*` would type-check inside a gui wall:
91
+
92
+ ```ts
93
+ // Wrong: re-introduces the full surface, defeating the wall
94
+ import { defineGuiScript } from "@defold-typescript/types";
95
+ ```
96
+
97
+ The interactive snippets scaffolded by `init` already import from the kind
98
+ subpaths. `build` enforces this: a walled source that imports a lifecycle factory
99
+ from the main entry fails the build before transpile, naming the file and the kind
100
+ subpath it should import from instead. (`watch` does not enforce it yet — its
101
+ session path is a separate slice.)
@@ -0,0 +1,357 @@
1
+ ---
2
+ toc-title: Agent runbooks
3
+ ---
4
+ # Agent runbooks
5
+
6
+ Harness-neutral procedures for driving `defold-typescript` from an automated
7
+ agent. `defold-typescript` is a CLI published to npm (run with `bunx`) plus a
8
+ types package; it ships no
9
+ harness-specific skill or command assets, so the durable interface for an agent
10
+ is the CLI verbs themselves and their machine-readable `--json` output. Every
11
+ runbook below works from any harness: run the command, read the JSON envelope on
12
+ stdout, gate on `ok`.
13
+
14
+ Each one-shot command (`init`, `build`, `resolve`) prints a single JSON object
15
+ when given `--json`. The envelope is always one of two shapes:
16
+
17
+ ```json
18
+ { "command": "<verb>", "ok": true, "written": ["<path>", "..."] }
19
+ ```
20
+
21
+ ```json
22
+ { "command": "<verb>", "ok": false, "error": "<message>" }
23
+ ```
24
+
25
+ The agent branches on `ok`: on `true`, read `written` for the paths the command
26
+ created or updated; on `false`, read `error` for the failure reason. Some verbs
27
+ add fields to the success envelope (noted per runbook), but `command`, `ok`, and
28
+ either `written` or `error` are always present.
29
+
30
+ ## Scaffold a project
31
+
32
+ **Goal:** create a new TypeScript surface — either a fresh project, or the
33
+ TypeScript layer added to an existing Defold project.
34
+
35
+ **Command (fresh project, empty folder):**
36
+
37
+ ```sh
38
+ bunx @defold-typescript/cli@latest init --json
39
+ ```
40
+
41
+ **Command (existing Defold project — run inside the folder that holds
42
+ `game.project`):**
43
+
44
+ ```sh
45
+ bunx @defold-typescript/cli@latest init --json
46
+ ```
47
+
48
+ `init` detects whether a `game.project` is already present and either scaffolds a
49
+ whole new project or adds the TypeScript surface alongside the existing one.
50
+
51
+ **Returns:**
52
+
53
+ ```json
54
+ { "command": "init", "ok": true, "written": ["tsconfig.json", "src/main.ts", "..."] }
55
+ ```
56
+
57
+ On failure:
58
+
59
+ ```json
60
+ { "command": "init", "ok": false, "error": "<message>" }
61
+ ```
62
+
63
+ **Reading `ok`:** if `ok` is `true`, the scaffold succeeded and `written` lists
64
+ every file created or modified — use it to know what to open next. If `ok` is
65
+ `false`, stop and surface `error`; nothing was scaffolded.
66
+
67
+ ## Install the agent contract
68
+
69
+ **Goal:** drop an agent contract at the project root so any harness (or human)
70
+ opening the repo finds the conventions and a pointer to the installed guide.
71
+
72
+ **Command:**
73
+
74
+ ```sh
75
+ bunx @defold-typescript/cli@latest init-agents --json
76
+ ```
77
+
78
+ This writes two files. `AGENTS.md` carries a managed block delimited by HTML
79
+ comment markers; `CLAUDE.md` is the single line `@AGENTS.md`, re-exporting it.
80
+ Only the content **between** the markers is ever rewritten, so any notes you add
81
+ above or below the block survive re-runs untouched. If `AGENTS.md` already exists
82
+ without the markers, the block is appended after one blank line and your prior
83
+ content is left intact; a `CLAUDE.md` that already equals `@AGENTS.md` is left
84
+ byte-for-byte unchanged. The block is versionless — its guide pointer resolves to
85
+ `node_modules/@defold-typescript/cli/docs/guide/README.md`, which the install
86
+ swaps under the same path — so the verb is safe to re-run any time.
87
+
88
+ **Returns:**
89
+
90
+ ```json
91
+ { "command": "init-agents", "ok": true, "written": ["AGENTS.md", "CLAUDE.md"] }
92
+ ```
93
+
94
+ On failure:
95
+
96
+ ```json
97
+ { "command": "init-agents", "ok": false, "error": "<message>" }
98
+ ```
99
+
100
+ **Reading `ok`:** if `ok` is `true`, `written` lists the files touched in order;
101
+ a re-run that changes nothing omits the untouched file. If `ok` is `false`, stop
102
+ and surface `error`; nothing was written.
103
+
104
+ ## Regenerate extension types
105
+
106
+ **Goal:** refresh the ambient TypeScript surface for native extensions after a
107
+ `game.project` `[dependencies]` change, so extension namespaces stay in sync with
108
+ the declared archives. This automates the workflow described in
109
+ [Typing native extensions](./extensions.md).
110
+
111
+ **Command (run from the project root, after editing `[dependencies]`):**
112
+
113
+ ```sh
114
+ defold-typescript resolve --json
115
+ ```
116
+
117
+ `resolve` reads each declared extension's `.script_api`, regenerates the
118
+ gitignored `.defold-types/extensions/` ambient surface, and rewrites its index
119
+ and `package.json` to exactly the declared set.
120
+
121
+ **Returns** the same one-shot envelope keyed `command: "resolve"`, plus an
122
+ `extensions` array reporting provenance for each resolved dependency:
123
+
124
+ ```json
125
+ {
126
+ "command": "resolve",
127
+ "ok": true,
128
+ "written": [".defold-types/extensions/<namespace>.d.ts", "..."],
129
+ "extensions": [
130
+ {
131
+ "url": "<archive url>",
132
+ "provenance": "<cache | download>",
133
+ "namespaces": ["<namespace>"],
134
+ "scriptApiCount": 1,
135
+ "assetOnly": false
136
+ }
137
+ ]
138
+ }
139
+ ```
140
+
141
+ On failure:
142
+
143
+ ```json
144
+ { "command": "resolve", "ok": false, "error": "<message>" }
145
+ ```
146
+
147
+ **Reading `ok`:** if `ok` is `true`, the extension surface is current — `written`
148
+ lists the regenerated declaration files, and `extensions` records where each one
149
+ came from (`provenance`) and how many `.script_api` files it contributed
150
+ (`scriptApiCount`); an `assetOnly` dependency contributes no types. If `ok` is
151
+ `false`, surface `error`; the existing surface is left untouched.
152
+
153
+ ## Add a script
154
+
155
+ **Goal:** add a new gameplay script to a TypeScript Defold project and attach it
156
+ so it runs. There is no `add` verb — the workflow composes ordinary file
157
+ creation with the shipped `build` verb, then a scene-file edit to wire the
158
+ compiled component.
159
+
160
+ **1. Write the source.** One Defold script per file under `src/`, exporting a
161
+ single lifecycle factory as `default` (never two in one file). The factory
162
+ decides the compiled kind:
163
+
164
+ | Source factory | Compiled artifact | Referenced by |
165
+ | -------------- | ----------------- | ------------- |
166
+ | `defineScript` | `<name>.ts.script` | a game object (`.go` / `.collection`) as a component |
167
+ | `defineGuiScript` | `<name>.ts.gui_script` | a GUI scene (`.gui`), as its **Script** property |
168
+ | `defineRenderScript` | `<name>.ts.render_script` | the render pipeline (a `.render` file, set via `game.project`) |
169
+
170
+ A source that calls no factory emits a plain `<name>.lua` module to `import`
171
+ instead. Which hooks to export (`init`, `update`, `fixed_update`, `on_message`,
172
+ `on_input`, `final`, …) and how `self` is typed are covered in
173
+ [Script lifecycle](./script-lifecycle.md).
174
+
175
+ **2. Build** (from the project root):
176
+
177
+ ```sh
178
+ bunx @defold-typescript/cli@latest build --json
179
+ ```
180
+
181
+ Or, if a [`watch --json`](#fix-the-lua-output) is already running, just save the
182
+ file and read its `rebuild` event instead of invoking `build`.
183
+
184
+ **Returns** the one-shot envelope keyed `command: "build"`, plus the build
185
+ context fields:
186
+
187
+ ```json
188
+ {
189
+ "command": "build",
190
+ "ok": true,
191
+ "written": ["src/<name>.ts.script", "..."],
192
+ "defoldVersion": "<version>",
193
+ "defoldChannel": "<stable | beta | alpha>",
194
+ "apiSurface": "<surface id>",
195
+ "materializedSurface": "<path | null>"
196
+ }
197
+ ```
198
+
199
+ On failure:
200
+
201
+ ```json
202
+ { "command": "build", "ok": false, "error": "<message>" }
203
+ ```
204
+
205
+ **Reading `ok`:** if `ok` is `true`, the script transpiled — `written` lists the
206
+ emitted artifact (`.ts.script`, `.ts.gui_script`, or `.ts.render_script`) to
207
+ attach next; `defoldVersion` and `apiSurface` record which API surface it was
208
+ built against; `defoldChannel` records the resolved release channel (`stable`
209
+ unless pinned or passed via `--channel`; it does not yet change which surface is
210
+ fetched). If `ok` is `false`, the build failed — surface `error` and follow
211
+ [Fix the Lua output](#fix-the-lua-output).
212
+
213
+ **What `build` writes.** Each `.ts` source under `src/` produces exactly one
214
+ output in the Defold project tree — a script component or a `<name>.lua` module,
215
+ per the table above. `import` is rewritten to `require("<module>")` resolving
216
+ against the emitted `.lua`, so a shared module must be built for its require to
217
+ resolve at runtime. Two runtime modules are synthesized at the output root on
218
+ demand: `lualib_bundle.lua` (when a source uses a TS runtime helper like
219
+ `Object.keys` or spread) and `defold_typescript_timers.lua` (when timers are
220
+ used).
221
+
222
+ Who creates these: typescript-to-lua (TSTL) produces the Lua *content* in
223
+ memory; the CLI writes the *files*, choosing the `.ts.script` / `.ts.gui_script`
224
+ / `.ts.render_script` / `.lua` name and location. TSTL never touches disk — that
225
+ is why the outputs carry Defold-correct extensions instead of plain `.lua`.
226
+ Treat the `--json` `written` array as the authoritative list of what landed; do
227
+ not infer paths.
228
+
229
+ **3. Attach the compiled script.** Building only produces the artifact; nothing
230
+ runs until a scene references it as a component. Scene files (`.go`,
231
+ `.collection`, `.gui`, `.render`) reference the **compiled** artifact, never the
232
+ `.ts` source. A game object references a `.ts.script` through a `components { … }`
233
+ entry naming its project-root-absolute path:
234
+
235
+ ```
236
+ components {
237
+ id: "player"
238
+ component: "/src/player.ts.script"
239
+ }
240
+ ```
241
+
242
+ In a standalone `.go` the block appears verbatim. When the game object is
243
+ embedded in a `.collection`, Defold stores the same block as an escaped string
244
+ inside an `embedded_instances { data: "…" }` entry — the form to write when
245
+ editing the file directly. The platformer example shows this in
246
+ [`game/player.collection`](../examples/platformer/game/player.collection):
247
+
248
+ ```
249
+ embedded_instances {
250
+ id: "player"
251
+ data: "components {\n"
252
+ " id: \"player\"\n"
253
+ " component: \"/src/player.ts.script\"\n"
254
+ ...
255
+ "}\n"
256
+ ...
257
+ }
258
+ ```
259
+
260
+ A `.ts.gui_script` is referenced by a `.gui` scene's **Script** property and a
261
+ `.ts.render_script` by the render pipeline, the same compiled-artifact rule.
262
+
263
+ **4. Verify the attachment.** Two checks confirm the script is wired in:
264
+
265
+ - Grep the scene files for the compiled name:
266
+
267
+ ```sh
268
+ grep -rl "player.ts.script" --include="*.go" --include="*.collection" --include="*.gui" .
269
+ ```
270
+
271
+ A hit means a scene references it; no hit means it is orphaned and will not
272
+ run.
273
+
274
+ - Defold compiles only **reachable** resources, so after a Defold build
275
+ (the CLI's `defold build` subcommand) an attached script produces `build/default/src/player.ts.scriptc`
276
+ — the path mirrors the source — while an orphaned script never appears under
277
+ `build/default/` at all. (The `_generated_*` artifacts there are inlined or
278
+ asset-derived resources — embedded game objects, components, textures — never
279
+ referenced script files, which keep their source path.)
280
+
281
+ ## Narrow engine callback payloads
282
+
283
+ **Goal:** type an engine callback's untyped payload without hand-rolling `typeof`/`in`/`as`.
284
+
285
+ **Rule for clankers:** when an engine callback hands back an untyped or `unknown`
286
+ payload that is discriminated by a *separate* id/event argument, reach for the
287
+ **provided declaration-only guard** before reaching for `typeof`/`in`/`as`.
288
+ TypeScript cannot correlate two independent parameters, and Defold's ids/event
289
+ constants are pre-hashed `Hash` values or branded numbers — neither works as a
290
+ discriminated-union tag. Each guard below re-introduces the discriminant at the
291
+ use site. They are all **declaration-only**: the transpiler lowers each call to
292
+ its runtime comparison, so there is no runtime Lua symbol and **no import to
293
+ add**.
294
+
295
+ `on_message` — narrow one message with `isMessage`, or route many with `onMessage`:
296
+
297
+ ```ts
298
+ export default defineScript({
299
+ on_message(self, message_id, message) {
300
+ if (isMessage(message_id, message, "contact_point_response")) {
301
+ // message narrowed to the contact_point_response payload — no cast.
302
+ print(message.distance);
303
+ }
304
+ },
305
+ });
306
+ ```
307
+
308
+ `window.set_listener` — narrow the callback's `data` with `isWindowEvent`:
309
+
310
+ ```ts
311
+ window.set_listener((self, event, data) => {
312
+ if (isWindowEvent(event, data, window.WINDOW_EVENT_RESIZED)) {
313
+ // data narrowed to { width: number; height: number } — no cast.
314
+ print(data.width, data.height);
315
+ }
316
+ });
317
+ ```
318
+
319
+ An unknown id/event constant is a compile error, so the guard also catches typos.
320
+ The full narrowing reference, including `onMessage`'s multi-message dispatcher,
321
+ lives in [Script lifecycle](./script-lifecycle.md#receiving-messages-with-type-narrowing)
322
+ and the [`window.set_listener` gotcha](./typescript-gotchas.md#windowsetlistener-hands-event-and-data-as-separate-params).
323
+
324
+ ## Fix the Lua output
325
+
326
+ **Goal:** recover from a transpile failure reported by `build` or `watch`.
327
+
328
+ **Command (re-run after each source fix):**
329
+
330
+ ```sh
331
+ bunx @defold-typescript/cli@latest build --json
332
+ ```
333
+
334
+ On a transpile failure the one-shot `build --json` envelope carries the message:
335
+
336
+ ```json
337
+ { "command": "build", "ok": false, "error": "<message>" }
338
+ ```
339
+
340
+ Under a long-lived `watch --json`, the same failure arrives as an NDJSON event on
341
+ stdout (one JSON object per line) keyed `command: "watch"`:
342
+
343
+ ```json
344
+ { "command": "watch", "event": "rebuild", "ok": false, "error": "<message>" }
345
+ ```
346
+
347
+ The first build emits `event: "build"`; each later rebuild emits
348
+ `event: "rebuild"`. `build` and the transpile-diagnostics pass share one
349
+ diagnostic run, so the `error` names the offending source span.
350
+
351
+ **Reading `ok`:** while `ok` is `false`, read `error` for the failing span,
352
+ then fix the source and rebuild. Two pages route the fix:
353
+ [TypeScript gotchas](./typescript-gotchas.md) for the runtime-semantics traps
354
+ that compile clean but surprise under Lua, and
355
+ [Transpile diagnostics](./transpile-diagnostics.md) for what the diagnostic pass
356
+ surfaces. Repeat until `ok` is `true`, then read `written` as in
357
+ [Add a script](#add-a-script).