@defold-typescript/cli 0.5.2 → 0.5.3

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 CHANGED
@@ -4,11 +4,16 @@ The end-user CLI for scaffolding and building [Defold](https://defold.com/)
4
4
  projects written in TypeScript, transpiled to Lua.
5
5
 
6
6
  ```sh
7
- bunx @defold-typescript/cli init # add TypeScript to a Defold project, or scaffold a new one
8
- bunx @defold-typescript/cli build # transpile src/ to Lua alongside your Defold sources
9
- bunx @defold-typescript/cli watch # rebuild on change
7
+ bunx @defold-typescript/cli@latest init # add TypeScript to a Defold project, or scaffold a new one
8
+ bun install # install the scaffolded devDependencies (types, biome)
9
+ bunx @defold-typescript/cli build # transpile src/ to Lua alongside your Defold sources
10
+ bunx @defold-typescript/cli watch # rebuild on change
10
11
  ```
11
12
 
13
+ Scaffold with the `@latest` tag — `init` writes your `@defold-typescript/types`
14
+ version pin, so a stale `bunx` cache would pin an older release. Run `bun install`
15
+ once after `init` to put the scaffolded dev dependencies on disk.
16
+
12
17
  The package name is scoped (`@defold-typescript/cli`); `bunx defold-typescript`
13
18
  without the scope resolves a different, nonexistent package. Install it to get
14
19
  the shorter `defold-typescript` binary on your `PATH` and pin the version:
package/dist/bin.js CHANGED
@@ -231,6 +231,169 @@ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFi
231
231
  import * as path6 from "node:path";
232
232
  import { fileURLToPath as fileURLToPath2 } from "node:url";
233
233
 
234
+ // src/debug-launcher.ts
235
+ var PLATFORM_TARGETS = {
236
+ darwin: { enginePlatform: "x86_64-darwin", buildFolder: "x86_64-osx", executable: "dmengine" },
237
+ linux: { enginePlatform: "x86_64-linux", buildFolder: "x86_64-linux", executable: "dmengine" },
238
+ win32: {
239
+ enginePlatform: "x86_64-win32",
240
+ buildFolder: "x86_64-win32",
241
+ executable: "dmengine.exe"
242
+ }
243
+ };
244
+ var ENGINE_INFO_URL = "https://d.defold.com/stable/info.json";
245
+ var ENGINE_ARCHIVE_BASE = "https://d.defold.com/archive/stable";
246
+ var DEBUG_LAUNCHER_REL = ".vscode/defold-debug.ts";
247
+ function debugLaunchConfig() {
248
+ return {
249
+ name: "Defold: Debug (TypeScript)",
250
+ type: "lua-local",
251
+ request: "launch",
252
+ stopOnEntry: false,
253
+ verbose: false,
254
+ internalConsoleOptions: "openOnSessionStart",
255
+ program: { command: "bun" },
256
+ args: [DEBUG_LAUNCHER_REL]
257
+ };
258
+ }
259
+ var VSCODE_LAUNCH_CONTENT = {
260
+ version: "0.2.0",
261
+ configurations: [debugLaunchConfig()]
262
+ };
263
+ function renderDebugLauncher() {
264
+ const targets = JSON.stringify(PLATFORM_TARGETS, null, 2);
265
+ return `import { chmodSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
266
+ import * as path from "node:path";
267
+
268
+ // Windows only: paths to OpenAL32.dll and wrap_oal.dll from your Defold SDK
269
+ // (defoldsdk/ext/lib/x86_64-win32/). Leave empty on macOS/Linux.
270
+ const WINDOWS_OPENAL32_PATH = "";
271
+ const WINDOWS_WRAPOAL_PATH = "";
272
+
273
+ interface EngineTarget {
274
+ enginePlatform: string;
275
+ buildFolder: string;
276
+ executable: string;
277
+ }
278
+
279
+ const PLATFORM_TARGETS: Record<string, EngineTarget> = ${targets};
280
+
281
+ const ENGINE_INFO_URL = "${ENGINE_INFO_URL}";
282
+ const ENGINE_ARCHIVE_BASE = "${ENGINE_ARCHIVE_BASE}";
283
+
284
+ const target = PLATFORM_TARGETS[process.platform];
285
+ if (!target) {
286
+ console.error(\`Unsupported platform: \${process.platform}\`);
287
+ process.exit(1);
288
+ }
289
+
290
+ const here = path.dirname(new URL(import.meta.url).pathname);
291
+ const stockEnginePath = path.join(here, target.executable);
292
+
293
+ if (!existsSync(stockEnginePath)) {
294
+ const info = (await (await fetch(ENGINE_INFO_URL)).json()) as { sha1: string };
295
+ const url = \`\${ENGINE_ARCHIVE_BASE}/\${info.sha1}/engine/\${target.enginePlatform}/\${target.executable}\`;
296
+ console.log(\`Fetching \${url}\`);
297
+ const res = await fetch(url);
298
+ if (!res.ok) {
299
+ console.error(\`Engine download failed: \${res.status} \${res.statusText}\`);
300
+ process.exit(1);
301
+ }
302
+ await Bun.write(stockEnginePath, res);
303
+ }
304
+
305
+ const buildFolder = path.join("build", target.buildFolder);
306
+ const buildEnginePath = path.join(buildFolder, target.executable);
307
+ let enginePath = existsSync(buildEnginePath) ? buildEnginePath : stockEnginePath;
308
+
309
+ if (process.platform === "win32" && enginePath === buildEnginePath) {
310
+ for (const [src, name] of [
311
+ [WINDOWS_OPENAL32_PATH, "OpenAL32.dll"],
312
+ [WINDOWS_WRAPOAL_PATH, "wrap_oal.dll"],
313
+ ] as const) {
314
+ const dest = path.join(buildFolder, name);
315
+ if (src && !existsSync(dest)) {
316
+ copyFileSync(src, dest);
317
+ }
318
+ }
319
+ }
320
+
321
+ // macOS: a build engine launched in place attaches to the editor process; copy
322
+ // it aside first so it runs standalone.
323
+ if (process.platform === "darwin" && enginePath === buildEnginePath) {
324
+ const tempEngine = path.join(buildFolder, "temp", target.executable);
325
+ mkdirSync(path.dirname(tempEngine), { recursive: true });
326
+ copyFileSync(buildEnginePath, tempEngine);
327
+ enginePath = tempEngine;
328
+ }
329
+
330
+ if (process.platform !== "win32") {
331
+ chmodSync(enginePath, 0o755);
332
+ }
333
+
334
+ const projectc = path.join("build", "default", "game.projectc");
335
+ console.log(\`Launching \${enginePath} \${projectc}\`);
336
+ const proc = Bun.spawn([enginePath, projectc], {
337
+ stdio: ["inherit", "inherit", "inherit"],
338
+ });
339
+ process.exit(await proc.exited);
340
+ `;
341
+ }
342
+ var DEBUG_LAUNCHER_SOURCE = renderDebugLauncher();
343
+
344
+ // src/mise-scaffold.ts
345
+ var MANAGED_MARKER = "# managed by @defold-typescript";
346
+ var MISE_TASKS_TOML = `${MANAGED_MARKER}
347
+ [tasks."defold-typescript:build"]
348
+ description = "Build the TypeScript sources with the installed defold-typescript CLI"
349
+ run = "bunx --no-install defold-typescript build"
350
+
351
+ ${MANAGED_MARKER}
352
+ [tasks."defold-typescript:watch"]
353
+ description = "Watch and rebuild the TypeScript sources with the installed defold-typescript CLI"
354
+ run = "bunx --no-install defold-typescript watch"
355
+
356
+ ${MANAGED_MARKER}
357
+ [tasks."defold-typescript:upgrade"]
358
+ description = "Upgrade the defold-typescript CLI to its latest release and re-pin the types dependency"
359
+ run = ["bunx @defold-typescript/cli@latest init --force", "bun install"]
360
+ `;
361
+ function stripManagedBlocks(text) {
362
+ const lines = text.split(`
363
+ `);
364
+ const out = [];
365
+ let i = 0;
366
+ while (i < lines.length) {
367
+ const line = lines[i] ?? "";
368
+ if (line.trim() === MANAGED_MARKER) {
369
+ i += 1;
370
+ while (i < lines.length && (lines[i] ?? "").trim() !== "") {
371
+ i += 1;
372
+ }
373
+ if (i < lines.length) {
374
+ i += 1;
375
+ }
376
+ continue;
377
+ }
378
+ out.push(line);
379
+ i += 1;
380
+ }
381
+ return out.join(`
382
+ `);
383
+ }
384
+ function mergeMiseToml(existing) {
385
+ if (existing === undefined) {
386
+ return MISE_TASKS_TOML;
387
+ }
388
+ const userContent = stripManagedBlocks(existing).replace(/\s*$/, "");
389
+ if (userContent === "") {
390
+ return MISE_TASKS_TOML;
391
+ }
392
+ return `${userContent}
393
+
394
+ ${MISE_TASKS_TOML}`;
395
+ }
396
+
234
397
  // src/script-kind.ts
235
398
  var DEFAULT_TYPES_ENTRYPOINT = "@defold-typescript/types";
236
399
  var KIND_BY_EXT = {
@@ -342,43 +505,64 @@ var BIOME_JSON_CONTENT = {
342
505
  }
343
506
  };
344
507
  var VSCODE_EXTENSIONS_CONTENT = {
345
- recommendations: ["sumneko.lua", "astronachos.defold"],
508
+ recommendations: ["sumneko.lua", "astronachos.defold", "tomblind.local-lua-debugger-vscode"],
346
509
  unwantedRecommendations: ["johnnymorganz.luau-lsp"]
347
510
  };
348
511
  var VSCODE_SETTINGS_CONTENT = {
349
512
  "Lua.workspace.ignoreDir": ["src"]
350
513
  };
514
+ var HOOK_COMMENTS = {
515
+ init: "Initialize the component and return its state.",
516
+ update: "Update the component every frame; `dt` is the time step.",
517
+ fixed_update: "Update at the fixed physics time step.",
518
+ late_update: "Update every frame after `update`.",
519
+ on_message: "Handle an incoming message.",
520
+ on_input: "Handle input once input focus is acquired.",
521
+ final: "Clean up when the component is deleted.",
522
+ on_reload: "React to a hot reload of this script."
523
+ };
524
+ var HOOK_SIGNATURES = {
525
+ init: "",
526
+ update: "self, dt",
527
+ fixed_update: "self, dt",
528
+ late_update: "self, dt",
529
+ on_message: "self, message_id, message, sender",
530
+ on_input: "self, action_id, action",
531
+ final: "self",
532
+ on_reload: "self"
533
+ };
534
+ var SNIPPET_HOOK_ORDER = Object.keys(HOOK_SIGNATURES);
535
+ function hookLines(includeOnInput, startTabStop) {
536
+ const lines = [];
537
+ let tabStop = startTabStop;
538
+ for (const hook of SNIPPET_HOOK_ORDER) {
539
+ if (hook === "init") {
540
+ continue;
541
+ }
542
+ if (hook === "on_input" && !includeOnInput) {
543
+ continue;
544
+ }
545
+ lines.push(` // ${HOOK_COMMENTS[hook]}`);
546
+ lines.push(` ${hook}(${HOOK_SIGNATURES[hook]}) {$${tabStop}},`);
547
+ tabStop += 1;
548
+ }
549
+ return lines;
550
+ }
351
551
  function inlineSnippetBody(factory, includeOnInput) {
352
- const lines = [
552
+ return [
353
553
  `import { ${factory} } from "@defold-typescript/types";`,
354
554
  "",
355
555
  `export const script = ${factory}({`,
356
- " // Initialize the component and return its state.",
556
+ ` // ${HOOK_COMMENTS.init}`,
357
557
  " init() {",
358
558
  " return { $0 };",
359
559
  " },",
360
- " // Update the component every frame; `dt` is the time step.",
361
- " update(self, dt) {$1},",
362
- " // Update at the fixed physics time step.",
363
- " fixed_update(self, dt) {$2},",
364
- " // Update every frame after `update`.",
365
- " late_update(self, dt) {$3},",
366
- " // Handle an incoming message.",
367
- " on_message(self, message_id, message, sender) {$4},"
560
+ ...hookLines(includeOnInput, 1),
561
+ "});"
368
562
  ];
369
- if (includeOnInput) {
370
- lines.push(" // Handle input once input focus is acquired.");
371
- lines.push(" on_input(self, action_id, action) {$5},");
372
- }
373
- lines.push(" // Clean up when the component is deleted.");
374
- lines.push(" final(self) {$6},");
375
- lines.push(" // React to a hot reload of this script.");
376
- lines.push(" on_reload(self) {$7},");
377
- lines.push("});");
378
- return lines;
379
563
  }
380
564
  function typedSnippetBody(factory, includeOnInput) {
381
- const lines = [
565
+ return [
382
566
  `import { ${factory} } from "@defold-typescript/types";`,
383
567
  "",
384
568
  "type Self = {",
@@ -387,29 +571,13 @@ function typedSnippetBody(factory, includeOnInput) {
387
571
  "};",
388
572
  "",
389
573
  `export const script = ${factory}<Self>({`,
390
- " // Initialize the component and return its state.",
574
+ ` // ${HOOK_COMMENTS.init}`,
391
575
  " init(): Self {",
392
576
  " return { $0 };",
393
577
  " },",
394
- " // Update the component every frame; `dt` is the time step.",
395
- " update(self, dt) {$2},",
396
- " // Update at the fixed physics time step.",
397
- " fixed_update(self, dt) {$3},",
398
- " // Update every frame after `update`.",
399
- " late_update(self, dt) {$4},",
400
- " // Handle an incoming message.",
401
- " on_message(self, message_id, message, sender) {$5},"
578
+ ...hookLines(includeOnInput, 2),
579
+ "});"
402
580
  ];
403
- if (includeOnInput) {
404
- lines.push(" // Handle input once input focus is acquired.");
405
- lines.push(" on_input(self, action_id, action) {$6},");
406
- }
407
- lines.push(" // Clean up when the component is deleted.");
408
- lines.push(" final(self) {$7},");
409
- lines.push(" // React to a hot reload of this script.");
410
- lines.push(" on_reload(self) {$8},");
411
- lines.push("});");
412
- return lines;
413
581
  }
414
582
  var VSCODE_SNIPPETS_CONTENT = {
415
583
  "Defold script (inferred self)": {
@@ -522,6 +690,12 @@ function writeBiome(cwd, written) {
522
690
  writeJson(biomePath, BIOME_JSON_CONTENT);
523
691
  written.push("biome.json");
524
692
  }
693
+ function writeMiseTasks(cwd, written) {
694
+ const misePath = path6.join(cwd, "mise.toml");
695
+ const existing = existsSync2(misePath) ? readFileSync5(misePath, "utf8") : undefined;
696
+ writeFileSync2(misePath, mergeMiseToml(existing));
697
+ written.push("mise.toml");
698
+ }
525
699
  function parseJsonc(text) {
526
700
  let out = "";
527
701
  let inString = false;
@@ -643,6 +817,39 @@ function writeVscodeSnippets(cwd, written) {
643
817
  writeJson(filePath, VSCODE_SNIPPETS_CONTENT);
644
818
  written.push(".vscode/defold-typescript.code-snippets");
645
819
  }
820
+ function writeVscodeLaunch(cwd, written) {
821
+ const dir = path6.join(cwd, ".vscode");
822
+ const filePath = path6.join(dir, "launch.json");
823
+ const ours = debugLaunchConfig();
824
+ if (existsSync2(filePath)) {
825
+ const existing = readVscodeJson(filePath);
826
+ if (existing === null) {
827
+ return;
828
+ }
829
+ const configs = Array.isArray(existing.configurations) ? [...existing.configurations] : [];
830
+ const names = new Set(configs.map((c) => isJsonObject(c) ? c.name : undefined));
831
+ if (!names.has(ours.name)) {
832
+ configs.push(ours);
833
+ }
834
+ existing.configurations = configs;
835
+ existing.version ??= VSCODE_LAUNCH_CONTENT.version;
836
+ writeJson(filePath, existing);
837
+ return;
838
+ }
839
+ mkdirSync2(dir, { recursive: true });
840
+ writeJson(filePath, VSCODE_LAUNCH_CONTENT);
841
+ written.push(".vscode/launch.json");
842
+ }
843
+ function writeVscodeDebugLauncher(cwd, written) {
844
+ const dir = path6.join(cwd, ".vscode");
845
+ const filePath = path6.join(dir, "defold-debug.ts");
846
+ if (existsSync2(filePath)) {
847
+ return;
848
+ }
849
+ mkdirSync2(dir, { recursive: true });
850
+ writeFileSync2(filePath, DEBUG_LAUNCHER_SOURCE);
851
+ written.push(".vscode/defold-debug.ts");
852
+ }
646
853
  function writeTsSurface(cwd, written, force = false) {
647
854
  mkdirSync2(path6.join(cwd, "src"), { recursive: true });
648
855
  writeFileSync2(path6.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
@@ -684,9 +891,12 @@ function writeTsSurface(cwd, written, force = false) {
684
891
  writeGitignore(cwd);
685
892
  written.push(".gitignore");
686
893
  writeBiome(cwd, written);
894
+ writeMiseTasks(cwd, written);
687
895
  writeVscodeExtensions(cwd, written);
688
896
  writeVscodeSettings(cwd, written);
689
897
  writeVscodeSnippets(cwd, written);
898
+ writeVscodeLaunch(cwd, written);
899
+ writeVscodeDebugLauncher(cwd, written);
690
900
  return selectScriptKind(kinds);
691
901
  }
692
902
  function runNewProjectInit(cwd, force = false) {
@@ -769,7 +979,7 @@ function materializeApiSurface(opts) {
769
979
  const excluded = excludedModulesForKind(opts.scriptKind ?? null);
770
980
  const sources = listDts(sourceGeneratedDir).filter((file) => file !== "index.d.ts").filter((file) => !excluded.has(file.replace(/\.d\.ts$/, "")));
771
981
  const srcDir = path7.resolve(sourceGeneratedDir, "..", "src");
772
- const overloads = ["msg-overloads.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path7.join(srcDir, file)));
982
+ const overloads = ["msg-overloads.d.ts", "message-guard.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path7.join(srcDir, file)));
773
983
  const coreTypesSrc = path7.join(srcDir, "core-types.ts");
774
984
  const includeCoreTypes = overloads.length > 0 && existsSync3(coreTypesSrc);
775
985
  const engineGlobalsSrc = path7.join(srcDir, "engine-globals.d.ts");
@@ -844,12 +1054,16 @@ function ensureMaterializedReference(cwd, materializedDir) {
844
1054
  const tsconfigPath = path7.join(cwd, "tsconfig.json");
845
1055
  if (existsSync3(tsconfigPath)) {
846
1056
  const tsconfig = JSON.parse(readFileSync6(tsconfigPath, "utf8"));
847
- tsconfig.compilerOptions = {
848
- ...tsconfig.compilerOptions ?? {},
849
- typeRoots: [MATERIALIZED_ROOT],
850
- types: [surfaceId]
851
- };
852
- writeJson2(tsconfigPath, tsconfig);
1057
+ const current = tsconfig.compilerOptions ?? {};
1058
+ const alreadyRepointed = JSON.stringify(current.typeRoots) === JSON.stringify([MATERIALIZED_ROOT]) && JSON.stringify(current.types) === JSON.stringify([surfaceId]);
1059
+ if (!alreadyRepointed) {
1060
+ tsconfig.compilerOptions = {
1061
+ ...current,
1062
+ typeRoots: [MATERIALIZED_ROOT],
1063
+ types: [surfaceId]
1064
+ };
1065
+ writeJson2(tsconfigPath, tsconfig);
1066
+ }
853
1067
  }
854
1068
  ensureGitignoreLine(cwd, `${MATERIALIZED_ROOT}/`);
855
1069
  }
@@ -0,0 +1,43 @@
1
+ export interface EngineTarget {
2
+ readonly enginePlatform: string;
3
+ readonly buildFolder: string;
4
+ readonly executable: string;
5
+ }
6
+ export declare const DEBUG_LAUNCHER_REL = ".vscode/defold-debug.ts";
7
+ export declare function targetPlatform(platform: NodeJS.Platform): EngineTarget;
8
+ export declare function engineDownloadUrl(sha1: string, enginePlatform: string, executable: string): string;
9
+ export interface ResolveEngineOptions {
10
+ readonly cwd: string;
11
+ readonly target: EngineTarget;
12
+ readonly stockPath: string;
13
+ readonly probe: (candidate: string) => boolean;
14
+ }
15
+ export declare function resolveEnginePath(opts: ResolveEngineOptions): string;
16
+ export declare function debugLaunchConfig(): {
17
+ name: string;
18
+ type: string;
19
+ request: string;
20
+ stopOnEntry: boolean;
21
+ verbose: boolean;
22
+ internalConsoleOptions: string;
23
+ program: {
24
+ command: string;
25
+ };
26
+ args: string[];
27
+ };
28
+ export declare const VSCODE_LAUNCH_CONTENT: {
29
+ version: string;
30
+ configurations: {
31
+ name: string;
32
+ type: string;
33
+ request: string;
34
+ stopOnEntry: boolean;
35
+ verbose: boolean;
36
+ internalConsoleOptions: string;
37
+ program: {
38
+ command: string;
39
+ };
40
+ args: string[];
41
+ }[];
42
+ };
43
+ export declare const DEBUG_LAUNCHER_SOURCE: string;
package/dist/index.js CHANGED
@@ -295,6 +295,169 @@ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFi
295
295
  import * as path7 from "node:path";
296
296
  import { fileURLToPath as fileURLToPath2 } from "node:url";
297
297
 
298
+ // src/debug-launcher.ts
299
+ var PLATFORM_TARGETS = {
300
+ darwin: { enginePlatform: "x86_64-darwin", buildFolder: "x86_64-osx", executable: "dmengine" },
301
+ linux: { enginePlatform: "x86_64-linux", buildFolder: "x86_64-linux", executable: "dmengine" },
302
+ win32: {
303
+ enginePlatform: "x86_64-win32",
304
+ buildFolder: "x86_64-win32",
305
+ executable: "dmengine.exe"
306
+ }
307
+ };
308
+ var ENGINE_INFO_URL = "https://d.defold.com/stable/info.json";
309
+ var ENGINE_ARCHIVE_BASE = "https://d.defold.com/archive/stable";
310
+ var DEBUG_LAUNCHER_REL = ".vscode/defold-debug.ts";
311
+ function debugLaunchConfig() {
312
+ return {
313
+ name: "Defold: Debug (TypeScript)",
314
+ type: "lua-local",
315
+ request: "launch",
316
+ stopOnEntry: false,
317
+ verbose: false,
318
+ internalConsoleOptions: "openOnSessionStart",
319
+ program: { command: "bun" },
320
+ args: [DEBUG_LAUNCHER_REL]
321
+ };
322
+ }
323
+ var VSCODE_LAUNCH_CONTENT = {
324
+ version: "0.2.0",
325
+ configurations: [debugLaunchConfig()]
326
+ };
327
+ function renderDebugLauncher() {
328
+ const targets = JSON.stringify(PLATFORM_TARGETS, null, 2);
329
+ return `import { chmodSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
330
+ import * as path from "node:path";
331
+
332
+ // Windows only: paths to OpenAL32.dll and wrap_oal.dll from your Defold SDK
333
+ // (defoldsdk/ext/lib/x86_64-win32/). Leave empty on macOS/Linux.
334
+ const WINDOWS_OPENAL32_PATH = "";
335
+ const WINDOWS_WRAPOAL_PATH = "";
336
+
337
+ interface EngineTarget {
338
+ enginePlatform: string;
339
+ buildFolder: string;
340
+ executable: string;
341
+ }
342
+
343
+ const PLATFORM_TARGETS: Record<string, EngineTarget> = ${targets};
344
+
345
+ const ENGINE_INFO_URL = "${ENGINE_INFO_URL}";
346
+ const ENGINE_ARCHIVE_BASE = "${ENGINE_ARCHIVE_BASE}";
347
+
348
+ const target = PLATFORM_TARGETS[process.platform];
349
+ if (!target) {
350
+ console.error(\`Unsupported platform: \${process.platform}\`);
351
+ process.exit(1);
352
+ }
353
+
354
+ const here = path.dirname(new URL(import.meta.url).pathname);
355
+ const stockEnginePath = path.join(here, target.executable);
356
+
357
+ if (!existsSync(stockEnginePath)) {
358
+ const info = (await (await fetch(ENGINE_INFO_URL)).json()) as { sha1: string };
359
+ const url = \`\${ENGINE_ARCHIVE_BASE}/\${info.sha1}/engine/\${target.enginePlatform}/\${target.executable}\`;
360
+ console.log(\`Fetching \${url}\`);
361
+ const res = await fetch(url);
362
+ if (!res.ok) {
363
+ console.error(\`Engine download failed: \${res.status} \${res.statusText}\`);
364
+ process.exit(1);
365
+ }
366
+ await Bun.write(stockEnginePath, res);
367
+ }
368
+
369
+ const buildFolder = path.join("build", target.buildFolder);
370
+ const buildEnginePath = path.join(buildFolder, target.executable);
371
+ let enginePath = existsSync(buildEnginePath) ? buildEnginePath : stockEnginePath;
372
+
373
+ if (process.platform === "win32" && enginePath === buildEnginePath) {
374
+ for (const [src, name] of [
375
+ [WINDOWS_OPENAL32_PATH, "OpenAL32.dll"],
376
+ [WINDOWS_WRAPOAL_PATH, "wrap_oal.dll"],
377
+ ] as const) {
378
+ const dest = path.join(buildFolder, name);
379
+ if (src && !existsSync(dest)) {
380
+ copyFileSync(src, dest);
381
+ }
382
+ }
383
+ }
384
+
385
+ // macOS: a build engine launched in place attaches to the editor process; copy
386
+ // it aside first so it runs standalone.
387
+ if (process.platform === "darwin" && enginePath === buildEnginePath) {
388
+ const tempEngine = path.join(buildFolder, "temp", target.executable);
389
+ mkdirSync(path.dirname(tempEngine), { recursive: true });
390
+ copyFileSync(buildEnginePath, tempEngine);
391
+ enginePath = tempEngine;
392
+ }
393
+
394
+ if (process.platform !== "win32") {
395
+ chmodSync(enginePath, 0o755);
396
+ }
397
+
398
+ const projectc = path.join("build", "default", "game.projectc");
399
+ console.log(\`Launching \${enginePath} \${projectc}\`);
400
+ const proc = Bun.spawn([enginePath, projectc], {
401
+ stdio: ["inherit", "inherit", "inherit"],
402
+ });
403
+ process.exit(await proc.exited);
404
+ `;
405
+ }
406
+ var DEBUG_LAUNCHER_SOURCE = renderDebugLauncher();
407
+
408
+ // src/mise-scaffold.ts
409
+ var MANAGED_MARKER = "# managed by @defold-typescript";
410
+ var MISE_TASKS_TOML = `${MANAGED_MARKER}
411
+ [tasks."defold-typescript:build"]
412
+ description = "Build the TypeScript sources with the installed defold-typescript CLI"
413
+ run = "bunx --no-install defold-typescript build"
414
+
415
+ ${MANAGED_MARKER}
416
+ [tasks."defold-typescript:watch"]
417
+ description = "Watch and rebuild the TypeScript sources with the installed defold-typescript CLI"
418
+ run = "bunx --no-install defold-typescript watch"
419
+
420
+ ${MANAGED_MARKER}
421
+ [tasks."defold-typescript:upgrade"]
422
+ description = "Upgrade the defold-typescript CLI to its latest release and re-pin the types dependency"
423
+ run = ["bunx @defold-typescript/cli@latest init --force", "bun install"]
424
+ `;
425
+ function stripManagedBlocks(text) {
426
+ const lines = text.split(`
427
+ `);
428
+ const out = [];
429
+ let i = 0;
430
+ while (i < lines.length) {
431
+ const line = lines[i] ?? "";
432
+ if (line.trim() === MANAGED_MARKER) {
433
+ i += 1;
434
+ while (i < lines.length && (lines[i] ?? "").trim() !== "") {
435
+ i += 1;
436
+ }
437
+ if (i < lines.length) {
438
+ i += 1;
439
+ }
440
+ continue;
441
+ }
442
+ out.push(line);
443
+ i += 1;
444
+ }
445
+ return out.join(`
446
+ `);
447
+ }
448
+ function mergeMiseToml(existing) {
449
+ if (existing === undefined) {
450
+ return MISE_TASKS_TOML;
451
+ }
452
+ const userContent = stripManagedBlocks(existing).replace(/\s*$/, "");
453
+ if (userContent === "") {
454
+ return MISE_TASKS_TOML;
455
+ }
456
+ return `${userContent}
457
+
458
+ ${MISE_TASKS_TOML}`;
459
+ }
460
+
298
461
  // src/script-kind.ts
299
462
  var DEFAULT_TYPES_ENTRYPOINT = "@defold-typescript/types";
300
463
  var KIND_BY_EXT = {
@@ -406,43 +569,64 @@ var BIOME_JSON_CONTENT = {
406
569
  }
407
570
  };
408
571
  var VSCODE_EXTENSIONS_CONTENT = {
409
- recommendations: ["sumneko.lua", "astronachos.defold"],
572
+ recommendations: ["sumneko.lua", "astronachos.defold", "tomblind.local-lua-debugger-vscode"],
410
573
  unwantedRecommendations: ["johnnymorganz.luau-lsp"]
411
574
  };
412
575
  var VSCODE_SETTINGS_CONTENT = {
413
576
  "Lua.workspace.ignoreDir": ["src"]
414
577
  };
578
+ var HOOK_COMMENTS = {
579
+ init: "Initialize the component and return its state.",
580
+ update: "Update the component every frame; `dt` is the time step.",
581
+ fixed_update: "Update at the fixed physics time step.",
582
+ late_update: "Update every frame after `update`.",
583
+ on_message: "Handle an incoming message.",
584
+ on_input: "Handle input once input focus is acquired.",
585
+ final: "Clean up when the component is deleted.",
586
+ on_reload: "React to a hot reload of this script."
587
+ };
588
+ var HOOK_SIGNATURES = {
589
+ init: "",
590
+ update: "self, dt",
591
+ fixed_update: "self, dt",
592
+ late_update: "self, dt",
593
+ on_message: "self, message_id, message, sender",
594
+ on_input: "self, action_id, action",
595
+ final: "self",
596
+ on_reload: "self"
597
+ };
598
+ var SNIPPET_HOOK_ORDER = Object.keys(HOOK_SIGNATURES);
599
+ function hookLines(includeOnInput, startTabStop) {
600
+ const lines = [];
601
+ let tabStop = startTabStop;
602
+ for (const hook of SNIPPET_HOOK_ORDER) {
603
+ if (hook === "init") {
604
+ continue;
605
+ }
606
+ if (hook === "on_input" && !includeOnInput) {
607
+ continue;
608
+ }
609
+ lines.push(` // ${HOOK_COMMENTS[hook]}`);
610
+ lines.push(` ${hook}(${HOOK_SIGNATURES[hook]}) {$${tabStop}},`);
611
+ tabStop += 1;
612
+ }
613
+ return lines;
614
+ }
415
615
  function inlineSnippetBody(factory, includeOnInput) {
416
- const lines = [
616
+ return [
417
617
  `import { ${factory} } from "@defold-typescript/types";`,
418
618
  "",
419
619
  `export const script = ${factory}({`,
420
- " // Initialize the component and return its state.",
620
+ ` // ${HOOK_COMMENTS.init}`,
421
621
  " init() {",
422
622
  " return { $0 };",
423
623
  " },",
424
- " // Update the component every frame; `dt` is the time step.",
425
- " update(self, dt) {$1},",
426
- " // Update at the fixed physics time step.",
427
- " fixed_update(self, dt) {$2},",
428
- " // Update every frame after `update`.",
429
- " late_update(self, dt) {$3},",
430
- " // Handle an incoming message.",
431
- " on_message(self, message_id, message, sender) {$4},"
624
+ ...hookLines(includeOnInput, 1),
625
+ "});"
432
626
  ];
433
- if (includeOnInput) {
434
- lines.push(" // Handle input once input focus is acquired.");
435
- lines.push(" on_input(self, action_id, action) {$5},");
436
- }
437
- lines.push(" // Clean up when the component is deleted.");
438
- lines.push(" final(self) {$6},");
439
- lines.push(" // React to a hot reload of this script.");
440
- lines.push(" on_reload(self) {$7},");
441
- lines.push("});");
442
- return lines;
443
627
  }
444
628
  function typedSnippetBody(factory, includeOnInput) {
445
- const lines = [
629
+ return [
446
630
  `import { ${factory} } from "@defold-typescript/types";`,
447
631
  "",
448
632
  "type Self = {",
@@ -451,29 +635,13 @@ function typedSnippetBody(factory, includeOnInput) {
451
635
  "};",
452
636
  "",
453
637
  `export const script = ${factory}<Self>({`,
454
- " // Initialize the component and return its state.",
638
+ ` // ${HOOK_COMMENTS.init}`,
455
639
  " init(): Self {",
456
640
  " return { $0 };",
457
641
  " },",
458
- " // Update the component every frame; `dt` is the time step.",
459
- " update(self, dt) {$2},",
460
- " // Update at the fixed physics time step.",
461
- " fixed_update(self, dt) {$3},",
462
- " // Update every frame after `update`.",
463
- " late_update(self, dt) {$4},",
464
- " // Handle an incoming message.",
465
- " on_message(self, message_id, message, sender) {$5},"
642
+ ...hookLines(includeOnInput, 2),
643
+ "});"
466
644
  ];
467
- if (includeOnInput) {
468
- lines.push(" // Handle input once input focus is acquired.");
469
- lines.push(" on_input(self, action_id, action) {$6},");
470
- }
471
- lines.push(" // Clean up when the component is deleted.");
472
- lines.push(" final(self) {$7},");
473
- lines.push(" // React to a hot reload of this script.");
474
- lines.push(" on_reload(self) {$8},");
475
- lines.push("});");
476
- return lines;
477
645
  }
478
646
  var VSCODE_SNIPPETS_CONTENT = {
479
647
  "Defold script (inferred self)": {
@@ -586,6 +754,12 @@ function writeBiome(cwd, written) {
586
754
  writeJson(biomePath, BIOME_JSON_CONTENT);
587
755
  written.push("biome.json");
588
756
  }
757
+ function writeMiseTasks(cwd, written) {
758
+ const misePath = path7.join(cwd, "mise.toml");
759
+ const existing = existsSync2(misePath) ? readFileSync6(misePath, "utf8") : undefined;
760
+ writeFileSync2(misePath, mergeMiseToml(existing));
761
+ written.push("mise.toml");
762
+ }
589
763
  function parseJsonc(text) {
590
764
  let out = "";
591
765
  let inString = false;
@@ -707,6 +881,39 @@ function writeVscodeSnippets(cwd, written) {
707
881
  writeJson(filePath, VSCODE_SNIPPETS_CONTENT);
708
882
  written.push(".vscode/defold-typescript.code-snippets");
709
883
  }
884
+ function writeVscodeLaunch(cwd, written) {
885
+ const dir = path7.join(cwd, ".vscode");
886
+ const filePath = path7.join(dir, "launch.json");
887
+ const ours = debugLaunchConfig();
888
+ if (existsSync2(filePath)) {
889
+ const existing = readVscodeJson(filePath);
890
+ if (existing === null) {
891
+ return;
892
+ }
893
+ const configs = Array.isArray(existing.configurations) ? [...existing.configurations] : [];
894
+ const names = new Set(configs.map((c) => isJsonObject(c) ? c.name : undefined));
895
+ if (!names.has(ours.name)) {
896
+ configs.push(ours);
897
+ }
898
+ existing.configurations = configs;
899
+ existing.version ??= VSCODE_LAUNCH_CONTENT.version;
900
+ writeJson(filePath, existing);
901
+ return;
902
+ }
903
+ mkdirSync2(dir, { recursive: true });
904
+ writeJson(filePath, VSCODE_LAUNCH_CONTENT);
905
+ written.push(".vscode/launch.json");
906
+ }
907
+ function writeVscodeDebugLauncher(cwd, written) {
908
+ const dir = path7.join(cwd, ".vscode");
909
+ const filePath = path7.join(dir, "defold-debug.ts");
910
+ if (existsSync2(filePath)) {
911
+ return;
912
+ }
913
+ mkdirSync2(dir, { recursive: true });
914
+ writeFileSync2(filePath, DEBUG_LAUNCHER_SOURCE);
915
+ written.push(".vscode/defold-debug.ts");
916
+ }
710
917
  function writeTsSurface(cwd, written, force = false) {
711
918
  mkdirSync2(path7.join(cwd, "src"), { recursive: true });
712
919
  writeFileSync2(path7.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
@@ -748,9 +955,12 @@ function writeTsSurface(cwd, written, force = false) {
748
955
  writeGitignore(cwd);
749
956
  written.push(".gitignore");
750
957
  writeBiome(cwd, written);
958
+ writeMiseTasks(cwd, written);
751
959
  writeVscodeExtensions(cwd, written);
752
960
  writeVscodeSettings(cwd, written);
753
961
  writeVscodeSnippets(cwd, written);
962
+ writeVscodeLaunch(cwd, written);
963
+ writeVscodeDebugLauncher(cwd, written);
754
964
  return selectScriptKind(kinds);
755
965
  }
756
966
  function runNewProjectInit(cwd, force = false) {
@@ -833,7 +1043,7 @@ function materializeApiSurface(opts) {
833
1043
  const excluded = excludedModulesForKind(opts.scriptKind ?? null);
834
1044
  const sources = listDts(sourceGeneratedDir).filter((file) => file !== "index.d.ts").filter((file) => !excluded.has(file.replace(/\.d\.ts$/, "")));
835
1045
  const srcDir = path8.resolve(sourceGeneratedDir, "..", "src");
836
- const overloads = ["msg-overloads.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path8.join(srcDir, file)));
1046
+ const overloads = ["msg-overloads.d.ts", "message-guard.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path8.join(srcDir, file)));
837
1047
  const coreTypesSrc = path8.join(srcDir, "core-types.ts");
838
1048
  const includeCoreTypes = overloads.length > 0 && existsSync3(coreTypesSrc);
839
1049
  const engineGlobalsSrc = path8.join(srcDir, "engine-globals.d.ts");
@@ -908,12 +1118,16 @@ function ensureMaterializedReference(cwd, materializedDir) {
908
1118
  const tsconfigPath = path8.join(cwd, "tsconfig.json");
909
1119
  if (existsSync3(tsconfigPath)) {
910
1120
  const tsconfig = JSON.parse(readFileSync7(tsconfigPath, "utf8"));
911
- tsconfig.compilerOptions = {
912
- ...tsconfig.compilerOptions ?? {},
913
- typeRoots: [MATERIALIZED_ROOT],
914
- types: [surfaceId]
915
- };
916
- writeJson2(tsconfigPath, tsconfig);
1121
+ const current = tsconfig.compilerOptions ?? {};
1122
+ const alreadyRepointed = JSON.stringify(current.typeRoots) === JSON.stringify([MATERIALIZED_ROOT]) && JSON.stringify(current.types) === JSON.stringify([surfaceId]);
1123
+ if (!alreadyRepointed) {
1124
+ tsconfig.compilerOptions = {
1125
+ ...current,
1126
+ typeRoots: [MATERIALIZED_ROOT],
1127
+ types: [surfaceId]
1128
+ };
1129
+ writeJson2(tsconfigPath, tsconfig);
1130
+ }
917
1131
  }
918
1132
  ensureGitignoreLine(cwd, `${MATERIALIZED_ROOT}/`);
919
1133
  }
@@ -0,0 +1,2 @@
1
+ export declare const MISE_TASKS_TOML = "# managed by @defold-typescript\n[tasks.\"defold-typescript:build\"]\ndescription = \"Build the TypeScript sources with the installed defold-typescript CLI\"\nrun = \"bunx --no-install defold-typescript build\"\n\n# managed by @defold-typescript\n[tasks.\"defold-typescript:watch\"]\ndescription = \"Watch and rebuild the TypeScript sources with the installed defold-typescript CLI\"\nrun = \"bunx --no-install defold-typescript watch\"\n\n# managed by @defold-typescript\n[tasks.\"defold-typescript:upgrade\"]\ndescription = \"Upgrade the defold-typescript CLI to its latest release and re-pin the types dependency\"\nrun = [\"bunx @defold-typescript/cli@latest init --force\", \"bun install\"]\n";
2
+ export declare function mergeMiseToml(existing?: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "End-user CLI for scaffolding and building Defold projects written in TypeScript.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@defold-typescript/transpiler": "0.5.2",
35
- "@defold-typescript/types": "0.5.2"
34
+ "@defold-typescript/transpiler": "0.5.3",
35
+ "@defold-typescript/types": "0.5.3"
36
36
  }
37
37
  }
@@ -0,0 +1,165 @@
1
+ import * as path from "node:path";
2
+
3
+ export interface EngineTarget {
4
+ readonly enginePlatform: string;
5
+ readonly buildFolder: string;
6
+ readonly executable: string;
7
+ }
8
+
9
+ // `enginePlatform` keys the d.defold.com download path; `buildFolder` keys the
10
+ // native-extension build output. They diverge on macOS (`x86_64-darwin` vs
11
+ // `x86_64-osx`), which is why they are tracked separately.
12
+ const PLATFORM_TARGETS: Record<string, EngineTarget> = {
13
+ darwin: { enginePlatform: "x86_64-darwin", buildFolder: "x86_64-osx", executable: "dmengine" },
14
+ linux: { enginePlatform: "x86_64-linux", buildFolder: "x86_64-linux", executable: "dmengine" },
15
+ win32: {
16
+ enginePlatform: "x86_64-win32",
17
+ buildFolder: "x86_64-win32",
18
+ executable: "dmengine.exe",
19
+ },
20
+ };
21
+
22
+ const ENGINE_INFO_URL = "https://d.defold.com/stable/info.json";
23
+ const ENGINE_ARCHIVE_BASE = "https://d.defold.com/archive/stable";
24
+
25
+ export const DEBUG_LAUNCHER_REL = ".vscode/defold-debug.ts";
26
+
27
+ export function targetPlatform(platform: NodeJS.Platform): EngineTarget {
28
+ const target = PLATFORM_TARGETS[platform];
29
+ if (!target) {
30
+ throw new Error(
31
+ `defold-typescript debug: unsupported platform "${platform}"; expected one of ${Object.keys(
32
+ PLATFORM_TARGETS,
33
+ ).join(", ")}.`,
34
+ );
35
+ }
36
+ return target;
37
+ }
38
+
39
+ export function engineDownloadUrl(
40
+ sha1: string,
41
+ enginePlatform: string,
42
+ executable: string,
43
+ ): string {
44
+ return `${ENGINE_ARCHIVE_BASE}/${sha1}/engine/${enginePlatform}/${executable}`;
45
+ }
46
+
47
+ export interface ResolveEngineOptions {
48
+ readonly cwd: string;
49
+ readonly target: EngineTarget;
50
+ readonly stockPath: string;
51
+ readonly probe: (candidate: string) => boolean;
52
+ }
53
+
54
+ // Prefer the native-extension build engine when it exists; the stock engine is
55
+ // the fallback for projects without native extensions.
56
+ export function resolveEnginePath(opts: ResolveEngineOptions): string {
57
+ const { cwd, target, stockPath, probe } = opts;
58
+ const buildEnginePath = path.join(cwd, "build", target.buildFolder, target.executable);
59
+ return probe(buildEnginePath) ? buildEnginePath : stockPath;
60
+ }
61
+
62
+ export function debugLaunchConfig() {
63
+ return {
64
+ name: "Defold: Debug (TypeScript)",
65
+ type: "lua-local",
66
+ request: "launch",
67
+ stopOnEntry: false,
68
+ verbose: false,
69
+ internalConsoleOptions: "openOnSessionStart",
70
+ program: { command: "bun" },
71
+ args: [DEBUG_LAUNCHER_REL],
72
+ };
73
+ }
74
+
75
+ export const VSCODE_LAUNCH_CONTENT = {
76
+ version: "0.2.0",
77
+ configurations: [debugLaunchConfig()],
78
+ };
79
+
80
+ // The scaffolded launcher embeds the same platform table and archive endpoints
81
+ // the helpers above use, so the self-contained `.vscode/defold-debug.ts` and the
82
+ // unit-tested logic stay in lockstep. It is a Bun script: `process.platform` for
83
+ // the OS, `fetch` for the engine download, `Bun.spawn` with inherited stdio for
84
+ // the run (the pipe Local Lua Debugger attaches over). No shell, no Git Bash.
85
+ function renderDebugLauncher(): string {
86
+ const targets = JSON.stringify(PLATFORM_TARGETS, null, 2);
87
+ return `import { chmodSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
88
+ import * as path from "node:path";
89
+
90
+ // Windows only: paths to OpenAL32.dll and wrap_oal.dll from your Defold SDK
91
+ // (defoldsdk/ext/lib/x86_64-win32/). Leave empty on macOS/Linux.
92
+ const WINDOWS_OPENAL32_PATH = "";
93
+ const WINDOWS_WRAPOAL_PATH = "";
94
+
95
+ interface EngineTarget {
96
+ enginePlatform: string;
97
+ buildFolder: string;
98
+ executable: string;
99
+ }
100
+
101
+ const PLATFORM_TARGETS: Record<string, EngineTarget> = ${targets};
102
+
103
+ const ENGINE_INFO_URL = "${ENGINE_INFO_URL}";
104
+ const ENGINE_ARCHIVE_BASE = "${ENGINE_ARCHIVE_BASE}";
105
+
106
+ const target = PLATFORM_TARGETS[process.platform];
107
+ if (!target) {
108
+ console.error(\`Unsupported platform: \${process.platform}\`);
109
+ process.exit(1);
110
+ }
111
+
112
+ const here = path.dirname(new URL(import.meta.url).pathname);
113
+ const stockEnginePath = path.join(here, target.executable);
114
+
115
+ if (!existsSync(stockEnginePath)) {
116
+ const info = (await (await fetch(ENGINE_INFO_URL)).json()) as { sha1: string };
117
+ const url = \`\${ENGINE_ARCHIVE_BASE}/\${info.sha1}/engine/\${target.enginePlatform}/\${target.executable}\`;
118
+ console.log(\`Fetching \${url}\`);
119
+ const res = await fetch(url);
120
+ if (!res.ok) {
121
+ console.error(\`Engine download failed: \${res.status} \${res.statusText}\`);
122
+ process.exit(1);
123
+ }
124
+ await Bun.write(stockEnginePath, res);
125
+ }
126
+
127
+ const buildFolder = path.join("build", target.buildFolder);
128
+ const buildEnginePath = path.join(buildFolder, target.executable);
129
+ let enginePath = existsSync(buildEnginePath) ? buildEnginePath : stockEnginePath;
130
+
131
+ if (process.platform === "win32" && enginePath === buildEnginePath) {
132
+ for (const [src, name] of [
133
+ [WINDOWS_OPENAL32_PATH, "OpenAL32.dll"],
134
+ [WINDOWS_WRAPOAL_PATH, "wrap_oal.dll"],
135
+ ] as const) {
136
+ const dest = path.join(buildFolder, name);
137
+ if (src && !existsSync(dest)) {
138
+ copyFileSync(src, dest);
139
+ }
140
+ }
141
+ }
142
+
143
+ // macOS: a build engine launched in place attaches to the editor process; copy
144
+ // it aside first so it runs standalone.
145
+ if (process.platform === "darwin" && enginePath === buildEnginePath) {
146
+ const tempEngine = path.join(buildFolder, "temp", target.executable);
147
+ mkdirSync(path.dirname(tempEngine), { recursive: true });
148
+ copyFileSync(buildEnginePath, tempEngine);
149
+ enginePath = tempEngine;
150
+ }
151
+
152
+ if (process.platform !== "win32") {
153
+ chmodSync(enginePath, 0o755);
154
+ }
155
+
156
+ const projectc = path.join("build", "default", "game.projectc");
157
+ console.log(\`Launching \${enginePath} \${projectc}\`);
158
+ const proc = Bun.spawn([enginePath, projectc], {
159
+ stdio: ["inherit", "inherit", "inherit"],
160
+ });
161
+ process.exit(await proc.exited);
162
+ `;
163
+ }
164
+
165
+ export const DEBUG_LAUNCHER_SOURCE = renderDebugLauncher();
package/src/init.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import type { ScriptHookName } from "@defold-typescript/types";
5
+ import { DEBUG_LAUNCHER_SOURCE, debugLaunchConfig, VSCODE_LAUNCH_CONTENT } from "./debug-launcher";
4
6
  import { CURRENT_STABLE_DEFOLD_VERSION } from "./defold-version";
7
+ import { mergeMiseToml } from "./mise-scaffold";
5
8
  import {
6
9
  detectScriptKinds,
7
10
  type ScriptKind,
@@ -73,7 +76,7 @@ const BIOME_JSON_CONTENT = {
73
76
  };
74
77
 
75
78
  const VSCODE_EXTENSIONS_CONTENT = {
76
- recommendations: ["sumneko.lua", "astronachos.defold"],
79
+ recommendations: ["sumneko.lua", "astronachos.defold", "tomblind.local-lua-debugger-vscode"],
77
80
  unwantedRecommendations: ["johnnymorganz.luau-lsp"],
78
81
  };
79
82
 
@@ -88,43 +91,81 @@ interface VscodeSnippet {
88
91
  description: string;
89
92
  }
90
93
 
94
+ // One learn-more comment and one parameter list per lifecycle hook, keyed by
95
+ // `ScriptHookName` so a hook added to the types fails to compile here until both
96
+ // maps gain an entry (`satisfies` exhaustiveness — the type is derived from the
97
+ // canonical `SCRIPT_HOOK_NAMES`). The hook list is read off these keys rather
98
+ // than imported as a runtime value: the types package is type-only and not
99
+ // node-ESM-runnable, so the CLI bundle must not resolve it at runtime. `init` is
100
+ // special-cased by the body builders (it carries the return placeholder, not a
101
+ // `self` param), so its signature entry is unused but still required for
102
+ // exhaustiveness.
103
+ const HOOK_COMMENTS = {
104
+ init: "Initialize the component and return its state.",
105
+ update: "Update the component every frame; `dt` is the time step.",
106
+ fixed_update: "Update at the fixed physics time step.",
107
+ late_update: "Update every frame after `update`.",
108
+ on_message: "Handle an incoming message.",
109
+ on_input: "Handle input once input focus is acquired.",
110
+ final: "Clean up when the component is deleted.",
111
+ on_reload: "React to a hot reload of this script.",
112
+ } satisfies Record<ScriptHookName, string>;
113
+
114
+ const HOOK_SIGNATURES = {
115
+ init: "",
116
+ update: "self, dt",
117
+ fixed_update: "self, dt",
118
+ late_update: "self, dt",
119
+ on_message: "self, message_id, message, sender",
120
+ on_input: "self, action_id, action",
121
+ final: "self",
122
+ on_reload: "self",
123
+ } satisfies Record<ScriptHookName, string>;
124
+
125
+ const SNIPPET_HOOK_ORDER = Object.keys(HOOK_SIGNATURES) as ScriptHookName[];
126
+
127
+ // Emit every hook except `init` (the caller writes it with its return
128
+ // placeholder) as a commented `name(sig) {$N},` line. Render scripts pass
129
+ // includeOnInput=false because `RenderScriptHooks` omits `on_input`. Tab stops
130
+ // run sequentially from `startTabStop` across the hooks actually emitted.
131
+ function hookLines(includeOnInput: boolean, startTabStop: number): string[] {
132
+ const lines: string[] = [];
133
+ let tabStop = startTabStop;
134
+ for (const hook of SNIPPET_HOOK_ORDER) {
135
+ if (hook === "init") {
136
+ continue;
137
+ }
138
+ if (hook === "on_input" && !includeOnInput) {
139
+ continue;
140
+ }
141
+ lines.push(` // ${HOOK_COMMENTS[hook]}`);
142
+ lines.push(` ${hook}(${HOOK_SIGNATURES[hook]}) {$${tabStop}},`);
143
+ tabStop += 1;
144
+ }
145
+ return lines;
146
+ }
147
+
91
148
  // Whole-file TS scaffolds mirroring the Defold editor's empty script/gui/render
92
149
  // templates over the lifecycle factories. Two self-typing variants per kind:
93
150
  // inline-self (TSelf inferred from `init`'s return) and typed-self (an explicit
94
151
  // dummy `Self` placeholder). Hook order mirrors the Lua templates; render omits
95
152
  // `on_input` because `RenderScriptHooks` does. The final `$0` lands inside `init`.
96
153
  function inlineSnippetBody(factory: string, includeOnInput: boolean): string[] {
97
- const lines = [
154
+ return [
98
155
  `import { ${factory} } from "@defold-typescript/types";`,
99
156
  "",
100
157
  `export const script = ${factory}({`,
101
- " // Initialize the component and return its state.",
158
+ ` // ${HOOK_COMMENTS.init}`,
102
159
  " init() {",
103
160
  " return { $0 };",
104
161
  " },",
105
- " // Update the component every frame; `dt` is the time step.",
106
- " update(self, dt) {$1},",
107
- " // Update at the fixed physics time step.",
108
- " fixed_update(self, dt) {$2},",
109
- " // Update every frame after `update`.",
110
- " late_update(self, dt) {$3},",
111
- " // Handle an incoming message.",
112
- " on_message(self, message_id, message, sender) {$4},",
162
+ ...hookLines(includeOnInput, 1),
163
+ "});",
113
164
  ];
114
- if (includeOnInput) {
115
- lines.push(" // Handle input once input focus is acquired.");
116
- lines.push(" on_input(self, action_id, action) {$5},");
117
- }
118
- lines.push(" // Clean up when the component is deleted.");
119
- lines.push(" final(self) {$6},");
120
- lines.push(" // React to a hot reload of this script.");
121
- lines.push(" on_reload(self) {$7},");
122
- lines.push("});");
123
- return lines;
124
165
  }
125
166
 
126
167
  function typedSnippetBody(factory: string, includeOnInput: boolean): string[] {
127
- const lines = [
168
+ return [
128
169
  `import { ${factory} } from "@defold-typescript/types";`,
129
170
  "",
130
171
  "type Self = {",
@@ -133,29 +174,13 @@ function typedSnippetBody(factory: string, includeOnInput: boolean): string[] {
133
174
  "};",
134
175
  "",
135
176
  `export const script = ${factory}<Self>({`,
136
- " // Initialize the component and return its state.",
177
+ ` // ${HOOK_COMMENTS.init}`,
137
178
  " init(): Self {",
138
179
  " return { $0 };",
139
180
  " },",
140
- " // Update the component every frame; `dt` is the time step.",
141
- " update(self, dt) {$2},",
142
- " // Update at the fixed physics time step.",
143
- " fixed_update(self, dt) {$3},",
144
- " // Update every frame after `update`.",
145
- " late_update(self, dt) {$4},",
146
- " // Handle an incoming message.",
147
- " on_message(self, message_id, message, sender) {$5},",
181
+ ...hookLines(includeOnInput, 2),
182
+ "});",
148
183
  ];
149
- if (includeOnInput) {
150
- lines.push(" // Handle input once input focus is acquired.");
151
- lines.push(" on_input(self, action_id, action) {$6},");
152
- }
153
- lines.push(" // Clean up when the component is deleted.");
154
- lines.push(" final(self) {$7},");
155
- lines.push(" // React to a hot reload of this script.");
156
- lines.push(" on_reload(self) {$8},");
157
- lines.push("});");
158
- return lines;
159
184
  }
160
185
 
161
186
  const VSCODE_SNIPPETS_CONTENT: Record<string, VscodeSnippet> = {
@@ -297,6 +322,13 @@ function writeBiome(cwd: string, written: string[]): void {
297
322
  written.push("biome.json");
298
323
  }
299
324
 
325
+ function writeMiseTasks(cwd: string, written: string[]): void {
326
+ const misePath = path.join(cwd, "mise.toml");
327
+ const existing = existsSync(misePath) ? readFileSync(misePath, "utf8") : undefined;
328
+ writeFileSync(misePath, mergeMiseToml(existing));
329
+ written.push("mise.toml");
330
+ }
331
+
300
332
  // Strip `//` line comments, `/* */` block comments, and trailing commas so a
301
333
  // hand-edited JSONC `.vscode` file parses with `JSON.parse`. The walk tracks
302
334
  // string state so a `//` or comma inside a value (e.g. a URL) is preserved.
@@ -438,6 +470,41 @@ function writeVscodeSnippets(cwd: string, written: string[]): void {
438
470
  written.push(".vscode/defold-typescript.code-snippets");
439
471
  }
440
472
 
473
+ function writeVscodeLaunch(cwd: string, written: string[]): void {
474
+ const dir = path.join(cwd, ".vscode");
475
+ const filePath = path.join(dir, "launch.json");
476
+ const ours = debugLaunchConfig();
477
+ if (existsSync(filePath)) {
478
+ const existing = readVscodeJson(filePath);
479
+ if (existing === null) {
480
+ return;
481
+ }
482
+ const configs = Array.isArray(existing.configurations) ? [...existing.configurations] : [];
483
+ const names = new Set(configs.map((c) => (isJsonObject(c) ? c.name : undefined)));
484
+ if (!names.has(ours.name)) {
485
+ configs.push(ours);
486
+ }
487
+ existing.configurations = configs;
488
+ existing.version ??= VSCODE_LAUNCH_CONTENT.version;
489
+ writeJson(filePath, existing);
490
+ return;
491
+ }
492
+ mkdirSync(dir, { recursive: true });
493
+ writeJson(filePath, VSCODE_LAUNCH_CONTENT);
494
+ written.push(".vscode/launch.json");
495
+ }
496
+
497
+ function writeVscodeDebugLauncher(cwd: string, written: string[]): void {
498
+ const dir = path.join(cwd, ".vscode");
499
+ const filePath = path.join(dir, "defold-debug.ts");
500
+ if (existsSync(filePath)) {
501
+ return;
502
+ }
503
+ mkdirSync(dir, { recursive: true });
504
+ writeFileSync(filePath, DEBUG_LAUNCHER_SOURCE);
505
+ written.push(".vscode/defold-debug.ts");
506
+ }
507
+
441
508
  function writeTsSurface(cwd: string, written: string[], force = false): ScriptKind | null {
442
509
  mkdirSync(path.join(cwd, "src"), { recursive: true });
443
510
  writeFileSync(path.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
@@ -483,10 +550,13 @@ function writeTsSurface(cwd: string, written: string[], force = false): ScriptKi
483
550
  written.push(".gitignore");
484
551
 
485
552
  writeBiome(cwd, written);
553
+ writeMiseTasks(cwd, written);
486
554
 
487
555
  writeVscodeExtensions(cwd, written);
488
556
  writeVscodeSettings(cwd, written);
489
557
  writeVscodeSnippets(cwd, written);
558
+ writeVscodeLaunch(cwd, written);
559
+ writeVscodeDebugLauncher(cwd, written);
490
560
 
491
561
  return selectScriptKind(kinds);
492
562
  }
@@ -65,8 +65,8 @@ export function materializeApiSurface(
65
65
  // surface silently drops those globals. Skipped when the source has no
66
66
  // sibling `src/` (e.g. synthetic test fixtures).
67
67
  const srcDir = path.resolve(sourceGeneratedDir, "..", "src");
68
- const overloads = ["msg-overloads.d.ts", "go-overloads.d.ts"].filter((file) =>
69
- existsSync(path.join(srcDir, file)),
68
+ const overloads = ["msg-overloads.d.ts", "message-guard.d.ts", "go-overloads.d.ts"].filter(
69
+ (file) => existsSync(path.join(srcDir, file)),
70
70
  );
71
71
  const coreTypesSrc = path.join(srcDir, "core-types.ts");
72
72
  const includeCoreTypes = overloads.length > 0 && existsSync(coreTypesSrc);
@@ -148,12 +148,21 @@ export function ensureMaterializedReference(cwd: string, materializedDir: string
148
148
  compilerOptions?: Record<string, unknown>;
149
149
  [key: string]: unknown;
150
150
  };
151
- tsconfig.compilerOptions = {
152
- ...(tsconfig.compilerOptions ?? {}),
153
- typeRoots: [MATERIALIZED_ROOT],
154
- types: [surfaceId],
155
- };
156
- writeJson(tsconfigPath, tsconfig);
151
+ const current = tsconfig.compilerOptions ?? {};
152
+ // Skip the write when already repointed so the file keeps its existing
153
+ // formatting (a consumer's Biome/Prettier shape) instead of churning to
154
+ // JSON.stringify's layout on every build.
155
+ const alreadyRepointed =
156
+ JSON.stringify(current.typeRoots) === JSON.stringify([MATERIALIZED_ROOT]) &&
157
+ JSON.stringify(current.types) === JSON.stringify([surfaceId]);
158
+ if (!alreadyRepointed) {
159
+ tsconfig.compilerOptions = {
160
+ ...current,
161
+ typeRoots: [MATERIALIZED_ROOT],
162
+ types: [surfaceId],
163
+ };
164
+ writeJson(tsconfigPath, tsconfig);
165
+ }
157
166
  }
158
167
 
159
168
  ensureGitignoreLine(cwd, `${MATERIALIZED_ROOT}/`);
@@ -0,0 +1,62 @@
1
+ // The repo has no TOML parser and must not add one for three tasks, so the
2
+ // managed block is emitted as a literal string and merged line-aware. Every
3
+ // managed task is fronted by this marker so a re-merge can locate and refresh
4
+ // the block without disturbing user-authored `[tools]`/`[tasks.*]` content.
5
+ const MANAGED_MARKER = "# managed by @defold-typescript";
6
+
7
+ // `bunx --no-install` resolves only the locally installed binary and never
8
+ // fetches from the registry — that is the installed-version contract the types
9
+ // pin upholds, while staying cross-platform (the bare `node_modules/.bin` path
10
+ // is `.cmd`-shimmed on Windows). `:upgrade` is the deliberate `@latest` pull
11
+ // that re-pins `@defold-typescript/types` via `init --force` + reinstall.
12
+ export const MISE_TASKS_TOML = `${MANAGED_MARKER}
13
+ [tasks."defold-typescript:build"]
14
+ description = "Build the TypeScript sources with the installed defold-typescript CLI"
15
+ run = "bunx --no-install defold-typescript build"
16
+
17
+ ${MANAGED_MARKER}
18
+ [tasks."defold-typescript:watch"]
19
+ description = "Watch and rebuild the TypeScript sources with the installed defold-typescript CLI"
20
+ run = "bunx --no-install defold-typescript watch"
21
+
22
+ ${MANAGED_MARKER}
23
+ [tasks."defold-typescript:upgrade"]
24
+ description = "Upgrade the defold-typescript CLI to its latest release and re-pin the types dependency"
25
+ run = ["bunx @defold-typescript/cli@latest init --force", "bun install"]
26
+ `;
27
+
28
+ // Drop every managed block (marker line through the next blank line or EOF),
29
+ // leaving all other lines byte-identical, so a refresh strips the stale block
30
+ // before the fresh one is re-appended.
31
+ function stripManagedBlocks(text: string): string {
32
+ const lines = text.split("\n");
33
+ const out: string[] = [];
34
+ let i = 0;
35
+ while (i < lines.length) {
36
+ const line = lines[i] ?? "";
37
+ if (line.trim() === MANAGED_MARKER) {
38
+ i += 1;
39
+ while (i < lines.length && (lines[i] ?? "").trim() !== "") {
40
+ i += 1;
41
+ }
42
+ if (i < lines.length) {
43
+ i += 1;
44
+ }
45
+ continue;
46
+ }
47
+ out.push(line);
48
+ i += 1;
49
+ }
50
+ return out.join("\n");
51
+ }
52
+
53
+ export function mergeMiseToml(existing?: string): string {
54
+ if (existing === undefined) {
55
+ return MISE_TASKS_TOML;
56
+ }
57
+ const userContent = stripManagedBlocks(existing).replace(/\s*$/, "");
58
+ if (userContent === "") {
59
+ return MISE_TASKS_TOML;
60
+ }
61
+ return `${userContent}\n\n${MISE_TASKS_TOML}`;
62
+ }