@defold-typescript/cli 0.1.0 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 king8fisher (Anton Veretennikov)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # defold-typescript
2
+
3
+ The end-user CLI for scaffolding and building [Defold](https://defold.com/)
4
+ projects written in TypeScript, transpiled to Lua.
5
+
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
10
+ ```
11
+
12
+ The package name is scoped (`@defold-typescript/cli`); `bunx defold-typescript`
13
+ without the scope resolves a different, nonexistent package. Install it to get
14
+ the shorter `defold-typescript` binary on your `PATH` and pin the version:
15
+
16
+ ```sh
17
+ npm i -D @defold-typescript/cli # then: bunx defold-typescript <command>
18
+ ```
19
+
20
+ See the repository [README](https://github.com/king8fisher/defold-typescript#readme)
21
+ and [`docs/guide/`](https://github.com/king8fisher/defold-typescript/tree/main/docs/guide)
22
+ for the full workflow.
23
+
24
+ ## License
25
+
26
+ MIT
package/dist/bin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/dispatch.ts
4
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
5
- import * as path9 from "node:path";
4
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "node:fs";
5
+ import * as path10 from "node:path";
6
6
 
7
7
  // src/api-registry.ts
8
8
  import { existsSync, readFileSync } from "node:fs";
@@ -94,6 +94,10 @@ var PROJECT_BUCKET = "<project>";
94
94
  function toPosix(p, sep2 = path2.sep) {
95
95
  return p.split(sep2).join("/");
96
96
  }
97
+ var TRANSPILER_SOURCE_RE = /\.(ts|tsx|cts|mts)$/;
98
+ function isTranspilerSource(rel) {
99
+ return TRANSPILER_SOURCE_RE.test(toPosix(rel));
100
+ }
97
101
  function readBuildConfig(cwd) {
98
102
  const tsconfigPath = path2.join(cwd, "tsconfig.json");
99
103
  let raw;
@@ -205,9 +209,27 @@ function runBuild(opts) {
205
209
  return { written };
206
210
  }
207
211
 
208
- // src/init.ts
209
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
212
+ // src/cli-version.ts
213
+ import { readFileSync as readFileSync4 } from "node:fs";
210
214
  import * as path5 from "node:path";
215
+ import { fileURLToPath } from "node:url";
216
+ var VERSION_FALLBACK = "0.0.0";
217
+ function defaultPackageRoot() {
218
+ return path5.join(path5.dirname(fileURLToPath(import.meta.url)), "..");
219
+ }
220
+ function readCliVersion(packageRoot = defaultPackageRoot()) {
221
+ try {
222
+ const pkg = JSON.parse(readFileSync4(path5.join(packageRoot, "package.json"), "utf8"));
223
+ return typeof pkg.version === "string" ? pkg.version : VERSION_FALLBACK;
224
+ } catch {
225
+ return VERSION_FALLBACK;
226
+ }
227
+ }
228
+
229
+ // src/init.ts
230
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
231
+ import * as path6 from "node:path";
232
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
211
233
 
212
234
  // src/script-kind.ts
213
235
  var DEFAULT_TYPES_ENTRYPOINT = "@defold-typescript/types";
@@ -332,19 +354,33 @@ embedded_instances {
332
354
  scale3 { x: 1.0 y: 1.0 z: 1.0 }
333
355
  }
334
356
  `;
357
+ function typesVersionSpec() {
358
+ try {
359
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
360
+ const pkg = JSON.parse(readFileSync5(path6.join(here, "..", "package.json"), "utf8"));
361
+ return pkg.version ? `^${pkg.version}` : "latest";
362
+ } catch {
363
+ return "latest";
364
+ }
365
+ }
335
366
  var SCAFFOLD_DEV_DEPS = {
336
- "@defold-typescript/transpiler": "workspace:*",
337
- "@defold-typescript/types": "workspace:*",
367
+ "@defold-typescript/types": typesVersionSpec(),
338
368
  "@biomejs/biome": "^2.2.0"
339
369
  };
370
+ function repairManagedDevDeps(devDeps) {
371
+ delete devDeps["@defold-typescript/transpiler"];
372
+ if (devDeps["@defold-typescript/types"]?.startsWith("workspace:")) {
373
+ devDeps["@defold-typescript/types"] = typesVersionSpec();
374
+ }
375
+ }
340
376
  function writeJson(filePath, value) {
341
377
  writeFileSync2(filePath, `${JSON.stringify(value, null, 2)}
342
378
  `);
343
379
  }
344
380
  function writeGitignore(cwd) {
345
- const gitignorePath = path5.join(cwd, ".gitignore");
381
+ const gitignorePath = path6.join(cwd, ".gitignore");
346
382
  if (existsSync2(gitignorePath)) {
347
- const existing = readFileSync4(gitignorePath, "utf8");
383
+ const existing = readFileSync5(gitignorePath, "utf8");
348
384
  const present = new Set(existing.split(`
349
385
  `).map((line) => line.trim()));
350
386
  const missing = GITIGNORE_LINES.filter((line) => !present.has(line));
@@ -364,7 +400,7 @@ function writeGitignore(cwd) {
364
400
  }
365
401
  }
366
402
  function writeBiome(cwd, written) {
367
- const biomePath = path5.join(cwd, "biome.json");
403
+ const biomePath = path6.join(cwd, "biome.json");
368
404
  if (existsSync2(biomePath)) {
369
405
  return;
370
406
  }
@@ -372,8 +408,8 @@ function writeBiome(cwd, written) {
372
408
  written.push("biome.json");
373
409
  }
374
410
  function writeTsSurface(cwd, written) {
375
- mkdirSync2(path5.join(cwd, "src"), { recursive: true });
376
- writeFileSync2(path5.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
411
+ mkdirSync2(path6.join(cwd, "src"), { recursive: true });
412
+ writeFileSync2(path6.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
377
413
  written.push("src/main.ts");
378
414
  const kinds = detectScriptKinds(cwd);
379
415
  const tsconfig = {
@@ -383,23 +419,24 @@ function writeTsSurface(cwd, written) {
383
419
  },
384
420
  include: ["src/**/*.ts"]
385
421
  };
386
- writeJson(path5.join(cwd, "tsconfig.json"), tsconfig);
422
+ writeJson(path6.join(cwd, "tsconfig.json"), tsconfig);
387
423
  written.push("tsconfig.json");
388
- const pkgPath = path5.join(cwd, "package.json");
424
+ const pkgPath = path6.join(cwd, "package.json");
389
425
  if (existsSync2(pkgPath)) {
390
- const existing = JSON.parse(readFileSync4(pkgPath, "utf8"));
426
+ const existing = JSON.parse(readFileSync5(pkgPath, "utf8"));
391
427
  const devDeps = { ...existing.devDependencies ?? {} };
392
428
  for (const [name, version] of Object.entries(SCAFFOLD_DEV_DEPS)) {
393
429
  if (!(name in devDeps)) {
394
430
  devDeps[name] = version;
395
431
  }
396
432
  }
433
+ repairManagedDevDeps(devDeps);
397
434
  existing.devDependencies = devDeps;
398
435
  existing["defold-typescript"] ??= { "defold-version": CURRENT_STABLE_DEFOLD_VERSION };
399
436
  writeJson(pkgPath, existing);
400
437
  } else {
401
438
  const fresh = {
402
- name: path5.basename(cwd),
439
+ name: path6.basename(cwd),
403
440
  version: "0.0.0",
404
441
  type: "module",
405
442
  devDependencies: { ...SCAFFOLD_DEV_DEPS },
@@ -420,27 +457,27 @@ function runNewProjectInit(cwd, force = false) {
420
457
  throw new Error(`defold-typescript init: refusing to synthesize a new Defold project into non-empty directory ${cwd}. Pass --force to proceed.`);
421
458
  }
422
459
  const written = [];
423
- writeFileSync2(path5.join(cwd, "game.project"), `[project]
424
- title = ${path5.basename(cwd)}
460
+ writeFileSync2(path6.join(cwd, "game.project"), `[project]
461
+ title = ${path6.basename(cwd)}
425
462
  main_collection = /main/main.collectionc
426
463
  `);
427
464
  written.push("game.project");
428
- mkdirSync2(path5.join(cwd, "main"), { recursive: true });
429
- writeFileSync2(path5.join(cwd, "main", "main.collection"), MAIN_COLLECTION_CONTENT);
465
+ mkdirSync2(path6.join(cwd, "main"), { recursive: true });
466
+ writeFileSync2(path6.join(cwd, "main", "main.collection"), MAIN_COLLECTION_CONTENT);
430
467
  written.push("main/main.collection");
431
- writeFileSync2(path5.join(cwd, "main", "main.script"), MAIN_SCRIPT_CONTENT);
468
+ writeFileSync2(path6.join(cwd, "main", "main.script"), MAIN_SCRIPT_CONTENT);
432
469
  written.push("main/main.script");
433
470
  const scriptKind = writeTsSurface(cwd, written);
434
471
  return { written, scriptKind };
435
472
  }
436
473
  function runInit(opts) {
437
474
  const { cwd, force = false } = opts;
438
- if (!existsSync2(path5.join(cwd, "game.project"))) {
475
+ if (!existsSync2(path6.join(cwd, "game.project"))) {
439
476
  return runNewProjectInit(cwd, force);
440
477
  }
441
478
  if (!force) {
442
479
  for (const rel of CONFLICTING_TS_CONFIGS) {
443
- if (existsSync2(path5.join(cwd, rel))) {
480
+ if (existsSync2(path6.join(cwd, rel))) {
444
481
  throw new Error(`defold-typescript init: refusing to overwrite existing TS config: ${rel}. Pass --force to overwrite.`);
445
482
  }
446
483
  }
@@ -468,11 +505,11 @@ import {
468
505
  existsSync as existsSync3,
469
506
  mkdirSync as mkdirSync3,
470
507
  readdirSync as readdirSync2,
471
- readFileSync as readFileSync5,
508
+ readFileSync as readFileSync6,
472
509
  rmSync,
473
510
  writeFileSync as writeFileSync3
474
511
  } from "node:fs";
475
- import * as path6 from "node:path";
512
+ import * as path7 from "node:path";
476
513
  var MATERIALIZED_ROOT = ".defold-types";
477
514
  function writeJson2(filePath, value) {
478
515
  writeFileSync3(filePath, `${JSON.stringify(value, null, 2)}
@@ -487,14 +524,14 @@ function materializeApiSurface(opts) {
487
524
  return { materializedDir: null, active: null };
488
525
  }
489
526
  const { surfaceId } = surface;
490
- const relDir = path6.posix.join(MATERIALIZED_ROOT, surfaceId);
491
- const absDir = path6.join(cwd, MATERIALIZED_ROOT, surfaceId);
527
+ const relDir = path7.posix.join(MATERIALIZED_ROOT, surfaceId);
528
+ const absDir = path7.join(cwd, MATERIALIZED_ROOT, surfaceId);
492
529
  mkdirSync3(absDir, { recursive: true });
493
530
  const excluded = excludedModulesForKind(opts.scriptKind ?? null);
494
531
  const sources = listDts(sourceGeneratedDir).filter((file) => file !== "index.d.ts").filter((file) => !excluded.has(file.replace(/\.d\.ts$/, "")));
495
- const srcDir = path6.resolve(sourceGeneratedDir, "..", "src");
496
- const overloads = ["msg-overloads.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path6.join(srcDir, file)));
497
- const coreTypesSrc = path6.join(srcDir, "core-types.ts");
532
+ const srcDir = path7.resolve(sourceGeneratedDir, "..", "src");
533
+ const overloads = ["msg-overloads.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path7.join(srcDir, file)));
534
+ const coreTypesSrc = path7.join(srcDir, "core-types.ts");
498
535
  const includeCoreTypes = overloads.length > 0 && existsSync3(coreTypesSrc);
499
536
  const wanted = new Set(sources);
500
537
  for (const file of overloads) {
@@ -505,39 +542,39 @@ function materializeApiSurface(opts) {
505
542
  }
506
543
  for (const existing of readdirSync2(absDir)) {
507
544
  if (existing.endsWith(".d.ts") && existing !== "index.d.ts" && !wanted.has(existing)) {
508
- rmSync(path6.join(absDir, existing));
545
+ rmSync(path7.join(absDir, existing));
509
546
  }
510
547
  }
511
548
  for (const file of sources) {
512
- writeFileSync3(path6.join(absDir, file), readFileSync5(path6.join(sourceGeneratedDir, file), "utf8"));
549
+ writeFileSync3(path7.join(absDir, file), readFileSync6(path7.join(sourceGeneratedDir, file), "utf8"));
513
550
  }
514
551
  if (includeCoreTypes) {
515
- writeFileSync3(path6.join(absDir, "core-types.d.ts"), readFileSync5(coreTypesSrc, "utf8"));
552
+ writeFileSync3(path7.join(absDir, "core-types.d.ts"), readFileSync6(coreTypesSrc, "utf8"));
516
553
  }
517
554
  for (const file of overloads) {
518
- writeFileSync3(path6.join(absDir, file), readFileSync5(path6.join(srcDir, file), "utf8"));
555
+ writeFileSync3(path7.join(absDir, file), readFileSync6(path7.join(srcDir, file), "utf8"));
519
556
  }
520
557
  const modules = [...sources, ...overloads].map((file) => file.replace(/\.d\.ts$/, ""));
521
558
  const imports = modules.map((mod) => `import "./${mod}";`).join(`
522
559
  `);
523
- writeFileSync3(path6.join(absDir, "index.d.ts"), `${imports}
560
+ writeFileSync3(path7.join(absDir, "index.d.ts"), `${imports}
524
561
 
525
562
  export {};
526
563
  `);
527
- writeJson2(path6.join(absDir, "package.json"), {
564
+ writeJson2(path7.join(absDir, "package.json"), {
528
565
  name: `@defold-typescript/materialized-${surfaceId}`,
529
566
  types: "index.d.ts"
530
567
  });
531
568
  return { materializedDir: relDir, active: surfaceId };
532
569
  }
533
570
  function ensureGitignoreLine(cwd, line) {
534
- const gitignorePath = path6.join(cwd, ".gitignore");
571
+ const gitignorePath = path7.join(cwd, ".gitignore");
535
572
  if (!existsSync3(gitignorePath)) {
536
573
  writeFileSync3(gitignorePath, `${line}
537
574
  `);
538
575
  return;
539
576
  }
540
- const existing = readFileSync5(gitignorePath, "utf8");
577
+ const existing = readFileSync6(gitignorePath, "utf8");
541
578
  const present = new Set(existing.split(`
542
579
  `).map((entry) => entry.trim()));
543
580
  if (present.has(line)) {
@@ -553,10 +590,10 @@ function ensureMaterializedReference(cwd, materializedDir) {
553
590
  if (materializedDir === null) {
554
591
  return;
555
592
  }
556
- const surfaceId = path6.posix.basename(materializedDir);
557
- const tsconfigPath = path6.join(cwd, "tsconfig.json");
593
+ const surfaceId = path7.posix.basename(materializedDir);
594
+ const tsconfigPath = path7.join(cwd, "tsconfig.json");
558
595
  if (existsSync3(tsconfigPath)) {
559
- const tsconfig = JSON.parse(readFileSync5(tsconfigPath, "utf8"));
596
+ const tsconfig = JSON.parse(readFileSync6(tsconfigPath, "utf8"));
560
597
  tsconfig.compilerOptions = {
561
598
  ...tsconfig.compilerOptions ?? {},
562
599
  typeRoots: [MATERIALIZED_ROOT],
@@ -568,7 +605,7 @@ function ensureMaterializedReference(cwd, materializedDir) {
568
605
  }
569
606
  function resolveCurrentSurfaceGeneratedDir() {
570
607
  const root = resolveTypesPackageRoot();
571
- return root === null ? null : path6.join(root, "generated");
608
+ return root === null ? null : path7.join(root, "generated");
572
609
  }
573
610
  async function materializeRefDocSurface(opts) {
574
611
  const { cwd, surfaceId, resolveOpts } = opts;
@@ -578,21 +615,21 @@ async function materializeRefDocSurface(opts) {
578
615
  }
579
616
  const registry = opts.registry ?? loadApiTargetsRegistry();
580
617
  const target = registry.find((t) => t.id === surfaceId);
581
- if (!target || target.source?.kind !== "ref-doc") {
618
+ if (target?.source?.kind !== "ref-doc") {
582
619
  return { materializedDir: null, active: null };
583
620
  }
584
621
  const excludeModules = [...excludedModulesForKind(opts.scriptKind ?? null)];
585
- const relDir = path6.posix.join(MATERIALIZED_ROOT, surfaceId);
586
- const absDir = path6.join(cwd, MATERIALIZED_ROOT, surfaceId);
622
+ const relDir = path7.posix.join(MATERIALIZED_ROOT, surfaceId);
623
+ const absDir = path7.join(cwd, MATERIALIZED_ROOT, surfaceId);
587
624
  try {
588
- const mod = await import(path6.join(root, "scripts", "materialize-version.ts"));
625
+ const mod = await import(path7.join(root, "scripts", "materialize-version.ts"));
589
626
  const selfContained = { ...target, coreTypesImport: "./core-types" };
590
627
  await mod.materializeVersionedSurface(selfContained, {
591
628
  destDir: absDir,
592
629
  ...resolveOpts ? { resolveOpts } : {},
593
630
  ...excludeModules.length > 0 ? { excludeModules } : {}
594
631
  });
595
- copyFileSync(path6.join(root, "src", "core-types.ts"), path6.join(absDir, "core-types.d.ts"));
632
+ copyFileSync(path7.join(root, "src", "core-types.ts"), path7.join(absDir, "core-types.d.ts"));
596
633
  } catch {
597
634
  rmSync(absDir, { recursive: true, force: true });
598
635
  return { materializedDir: null, active: null };
@@ -602,11 +639,11 @@ async function materializeRefDocSurface(opts) {
602
639
 
603
640
  // src/watch.ts
604
641
  import { existsSync as existsSync4, watch as fsWatch } from "node:fs";
605
- import * as path8 from "node:path";
642
+ import * as path9 from "node:path";
606
643
 
607
644
  // src/build-session.ts
608
- import { readFileSync as readFileSync6, rmSync as rmSync2 } from "node:fs";
609
- import * as path7 from "node:path";
645
+ import { readFileSync as readFileSync7, rmSync as rmSync2 } from "node:fs";
646
+ import * as path8 from "node:path";
610
647
  import {
611
648
  createTranspileSession
612
649
  } from "@defold-typescript/transpiler";
@@ -645,26 +682,28 @@ function createBuildSession(opts) {
645
682
  }
646
683
  const files = {};
647
684
  for (const rel of sources) {
648
- files[rel] = readFileSync6(path7.join(cwd, rel), "utf8");
685
+ files[rel] = readFileSync7(path8.join(cwd, rel), "utf8");
649
686
  }
650
687
  const result = session.update(files);
651
688
  return writeOutputs(result, sources);
652
689
  }
653
690
  function applyEvents(changed, removed) {
691
+ const sourceChanged = changed.filter(isTranspilerSource);
692
+ const sourceRemoved = removed.filter(isTranspilerSource);
654
693
  const changes = {};
655
- for (const rel of changed) {
656
- changes[rel] = readFileSync6(path7.join(cwd, rel), "utf8");
694
+ for (const rel of sourceChanged) {
695
+ changes[rel] = readFileSync7(path8.join(cwd, rel), "utf8");
657
696
  }
658
- for (const rel of removed) {
697
+ for (const rel of sourceRemoved) {
659
698
  changes[rel] = null;
660
699
  }
661
700
  const result = session.update(changes);
662
- for (const rel of removed) {
663
- const luaAbs = path7.join(cwd, computeLuaRel(rel, config));
701
+ for (const rel of sourceRemoved) {
702
+ const luaAbs = path8.join(cwd, computeLuaRel(rel, config));
664
703
  rmSync2(luaAbs, { force: true });
665
704
  rmSync2(`${luaAbs}.map`, { force: true });
666
705
  }
667
- return writeOutputs(result, changed);
706
+ return writeOutputs(result, sourceChanged);
668
707
  }
669
708
  return { buildAll, applyEvents };
670
709
  }
@@ -712,7 +751,7 @@ function runWatch(opts) {
712
751
  waitForIdle: () => Promise.resolve()
713
752
  };
714
753
  }
715
- const srcDir = path8.join(cwd, "src");
754
+ const srcDir = path9.join(cwd, "src");
716
755
  let scheduled = null;
717
756
  let syncScheduled = null;
718
757
  let rebuildBusy = false;
@@ -736,7 +775,7 @@ function runWatch(opts) {
736
775
  const removed = [];
737
776
  for (const rel of drained) {
738
777
  const key = `src/${toPosix(rel)}`;
739
- if (existsSync4(path8.join(srcDir, rel))) {
778
+ if (existsSync4(path9.join(srcDir, rel))) {
740
779
  changed.push(key);
741
780
  } else {
742
781
  removed.push(key);
@@ -756,9 +795,10 @@ function runWatch(opts) {
756
795
  function onEvent(e) {
757
796
  if (stopped)
758
797
  return;
798
+ if (!e.path || !isTranspilerSource(e.path))
799
+ return;
759
800
  rebuildBusy = true;
760
- if (e.path)
761
- pending.add(e.path);
801
+ pending.add(e.path);
762
802
  if (scheduled)
763
803
  clearTimeout(scheduled);
764
804
  scheduled = setTimeout(rebuild, debounceMs);
@@ -836,23 +876,30 @@ function parseDefoldVersionFlag(argv) {
836
876
  return { flag, rest };
837
877
  }
838
878
  function readProjectPin(cwd) {
839
- const pkgPath = path9.join(cwd, "package.json");
879
+ const pkgPath = path10.join(cwd, "package.json");
840
880
  if (!existsSync5(pkgPath)) {
841
881
  return;
842
882
  }
843
883
  try {
844
- return readDefoldVersionPin(JSON.parse(readFileSync7(pkgPath, "utf8")));
884
+ return readDefoldVersionPin(JSON.parse(readFileSync8(pkgPath, "utf8")));
845
885
  } catch {
846
886
  return;
847
887
  }
848
888
  }
849
889
  function dispatch(argv, io, internals) {
850
890
  const json = argv.includes("--json");
891
+ if (argv.includes("--version") || argv.includes("-v")) {
892
+ const version = internals?.cliVersion ?? readCliVersion();
893
+ io.stdout.write(json ? `{"command":"version","ok":true,"version":${JSON.stringify(version)}}
894
+ ` : `defold-typescript ${version}
895
+ `);
896
+ return 0;
897
+ }
851
898
  const force = argv.includes("--force");
852
899
  const { flag: defoldVersionFlag, rest: nonFlagArgs } = parseDefoldVersionFlag(argv);
853
900
  const positional = nonFlagArgs.filter((a) => a !== "--json" && a !== "--force");
854
901
  const [command, ...rest] = positional;
855
- const cwd = rest[0] ? path9.resolve(rest[0]) : process.cwd();
902
+ const cwd = rest[0] ? path10.resolve(rest[0]) : process.cwd();
856
903
  const pin = readProjectPin(cwd);
857
904
  const resolvedVersion = resolveDefoldVersion({
858
905
  ...defoldVersionFlag !== undefined ? { flag: defoldVersionFlag } : {},
@@ -4,6 +4,7 @@ export interface BuildConfig {
4
4
  readonly include: string[];
5
5
  }
6
6
  export declare function toPosix(p: string, sep?: string): string;
7
+ export declare function isTranspilerSource(rel: string): boolean;
7
8
  export declare function readBuildConfig(cwd: string): BuildConfig;
8
9
  export declare function computeLuaRel(rel: string, config: BuildConfig): string;
9
10
  export declare function collectFailures(diagnostics: readonly TranspileDiagnostic[]): Map<string, string[]>;
@@ -0,0 +1 @@
1
+ export declare function readCliVersion(packageRoot?: string): string;
@@ -13,5 +13,6 @@ export interface DispatchInternals {
13
13
  readonly sourceGeneratedDir?: string;
14
14
  readonly resolveOpts?: RefDocResolveOptions;
15
15
  readonly refDocRegistry?: readonly RegistryTarget[];
16
+ readonly cliVersion?: string;
16
17
  }
17
18
  export declare function dispatch(argv: string[], io: DispatchIo, internals?: DispatchInternals): number | Promise<number>;
package/dist/index.js CHANGED
@@ -13,6 +13,10 @@ var PROJECT_BUCKET = "<project>";
13
13
  function toPosix(p, sep2 = path.sep) {
14
14
  return p.split(sep2).join("/");
15
15
  }
16
+ var TRANSPILER_SOURCE_RE = /\.(ts|tsx|cts|mts)$/;
17
+ function isTranspilerSource(rel) {
18
+ return TRANSPILER_SOURCE_RE.test(toPosix(rel));
19
+ }
16
20
  function readBuildConfig(cwd) {
17
21
  const tsconfigPath = path.join(cwd, "tsconfig.json");
18
22
  let raw;
@@ -129,26 +133,28 @@ function createBuildSession(opts) {
129
133
  return writeOutputs(result, sources);
130
134
  }
131
135
  function applyEvents(changed, removed) {
136
+ const sourceChanged = changed.filter(isTranspilerSource);
137
+ const sourceRemoved = removed.filter(isTranspilerSource);
132
138
  const changes = {};
133
- for (const rel of changed) {
139
+ for (const rel of sourceChanged) {
134
140
  changes[rel] = readFileSync2(path3.join(cwd, rel), "utf8");
135
141
  }
136
- for (const rel of removed) {
142
+ for (const rel of sourceRemoved) {
137
143
  changes[rel] = null;
138
144
  }
139
145
  const result = session.update(changes);
140
- for (const rel of removed) {
146
+ for (const rel of sourceRemoved) {
141
147
  const luaAbs = path3.join(cwd, computeLuaRel(rel, config));
142
148
  rmSync(luaAbs, { force: true });
143
149
  rmSync(`${luaAbs}.map`, { force: true });
144
150
  }
145
- return writeOutputs(result, changed);
151
+ return writeOutputs(result, sourceChanged);
146
152
  }
147
153
  return { buildAll, applyEvents };
148
154
  }
149
155
  // src/dispatch.ts
150
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
151
- import * as path9 from "node:path";
156
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "node:fs";
157
+ import * as path10 from "node:path";
152
158
 
153
159
  // src/api-registry.ts
154
160
  import { existsSync, readFileSync as readFileSync3 } from "node:fs";
@@ -267,9 +273,27 @@ function runBuild(opts) {
267
273
  return { written };
268
274
  }
269
275
 
270
- // src/init.ts
271
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
276
+ // src/cli-version.ts
277
+ import { readFileSync as readFileSync5 } from "node:fs";
272
278
  import * as path6 from "node:path";
279
+ import { fileURLToPath } from "node:url";
280
+ var VERSION_FALLBACK = "0.0.0";
281
+ function defaultPackageRoot() {
282
+ return path6.join(path6.dirname(fileURLToPath(import.meta.url)), "..");
283
+ }
284
+ function readCliVersion(packageRoot = defaultPackageRoot()) {
285
+ try {
286
+ const pkg = JSON.parse(readFileSync5(path6.join(packageRoot, "package.json"), "utf8"));
287
+ return typeof pkg.version === "string" ? pkg.version : VERSION_FALLBACK;
288
+ } catch {
289
+ return VERSION_FALLBACK;
290
+ }
291
+ }
292
+
293
+ // src/init.ts
294
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
295
+ import * as path7 from "node:path";
296
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
273
297
 
274
298
  // src/script-kind.ts
275
299
  var DEFAULT_TYPES_ENTRYPOINT = "@defold-typescript/types";
@@ -394,19 +418,33 @@ embedded_instances {
394
418
  scale3 { x: 1.0 y: 1.0 z: 1.0 }
395
419
  }
396
420
  `;
421
+ function typesVersionSpec() {
422
+ try {
423
+ const here = path7.dirname(fileURLToPath2(import.meta.url));
424
+ const pkg = JSON.parse(readFileSync6(path7.join(here, "..", "package.json"), "utf8"));
425
+ return pkg.version ? `^${pkg.version}` : "latest";
426
+ } catch {
427
+ return "latest";
428
+ }
429
+ }
397
430
  var SCAFFOLD_DEV_DEPS = {
398
- "@defold-typescript/transpiler": "workspace:*",
399
- "@defold-typescript/types": "workspace:*",
431
+ "@defold-typescript/types": typesVersionSpec(),
400
432
  "@biomejs/biome": "^2.2.0"
401
433
  };
434
+ function repairManagedDevDeps(devDeps) {
435
+ delete devDeps["@defold-typescript/transpiler"];
436
+ if (devDeps["@defold-typescript/types"]?.startsWith("workspace:")) {
437
+ devDeps["@defold-typescript/types"] = typesVersionSpec();
438
+ }
439
+ }
402
440
  function writeJson(filePath, value) {
403
441
  writeFileSync2(filePath, `${JSON.stringify(value, null, 2)}
404
442
  `);
405
443
  }
406
444
  function writeGitignore(cwd) {
407
- const gitignorePath = path6.join(cwd, ".gitignore");
445
+ const gitignorePath = path7.join(cwd, ".gitignore");
408
446
  if (existsSync2(gitignorePath)) {
409
- const existing = readFileSync5(gitignorePath, "utf8");
447
+ const existing = readFileSync6(gitignorePath, "utf8");
410
448
  const present = new Set(existing.split(`
411
449
  `).map((line) => line.trim()));
412
450
  const missing = GITIGNORE_LINES.filter((line) => !present.has(line));
@@ -426,7 +464,7 @@ function writeGitignore(cwd) {
426
464
  }
427
465
  }
428
466
  function writeBiome(cwd, written) {
429
- const biomePath = path6.join(cwd, "biome.json");
467
+ const biomePath = path7.join(cwd, "biome.json");
430
468
  if (existsSync2(biomePath)) {
431
469
  return;
432
470
  }
@@ -434,8 +472,8 @@ function writeBiome(cwd, written) {
434
472
  written.push("biome.json");
435
473
  }
436
474
  function writeTsSurface(cwd, written) {
437
- mkdirSync2(path6.join(cwd, "src"), { recursive: true });
438
- writeFileSync2(path6.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
475
+ mkdirSync2(path7.join(cwd, "src"), { recursive: true });
476
+ writeFileSync2(path7.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
439
477
  written.push("src/main.ts");
440
478
  const kinds = detectScriptKinds(cwd);
441
479
  const tsconfig = {
@@ -445,23 +483,24 @@ function writeTsSurface(cwd, written) {
445
483
  },
446
484
  include: ["src/**/*.ts"]
447
485
  };
448
- writeJson(path6.join(cwd, "tsconfig.json"), tsconfig);
486
+ writeJson(path7.join(cwd, "tsconfig.json"), tsconfig);
449
487
  written.push("tsconfig.json");
450
- const pkgPath = path6.join(cwd, "package.json");
488
+ const pkgPath = path7.join(cwd, "package.json");
451
489
  if (existsSync2(pkgPath)) {
452
- const existing = JSON.parse(readFileSync5(pkgPath, "utf8"));
490
+ const existing = JSON.parse(readFileSync6(pkgPath, "utf8"));
453
491
  const devDeps = { ...existing.devDependencies ?? {} };
454
492
  for (const [name, version] of Object.entries(SCAFFOLD_DEV_DEPS)) {
455
493
  if (!(name in devDeps)) {
456
494
  devDeps[name] = version;
457
495
  }
458
496
  }
497
+ repairManagedDevDeps(devDeps);
459
498
  existing.devDependencies = devDeps;
460
499
  existing["defold-typescript"] ??= { "defold-version": CURRENT_STABLE_DEFOLD_VERSION };
461
500
  writeJson(pkgPath, existing);
462
501
  } else {
463
502
  const fresh = {
464
- name: path6.basename(cwd),
503
+ name: path7.basename(cwd),
465
504
  version: "0.0.0",
466
505
  type: "module",
467
506
  devDependencies: { ...SCAFFOLD_DEV_DEPS },
@@ -482,27 +521,27 @@ function runNewProjectInit(cwd, force = false) {
482
521
  throw new Error(`defold-typescript init: refusing to synthesize a new Defold project into non-empty directory ${cwd}. Pass --force to proceed.`);
483
522
  }
484
523
  const written = [];
485
- writeFileSync2(path6.join(cwd, "game.project"), `[project]
486
- title = ${path6.basename(cwd)}
524
+ writeFileSync2(path7.join(cwd, "game.project"), `[project]
525
+ title = ${path7.basename(cwd)}
487
526
  main_collection = /main/main.collectionc
488
527
  `);
489
528
  written.push("game.project");
490
- mkdirSync2(path6.join(cwd, "main"), { recursive: true });
491
- writeFileSync2(path6.join(cwd, "main", "main.collection"), MAIN_COLLECTION_CONTENT);
529
+ mkdirSync2(path7.join(cwd, "main"), { recursive: true });
530
+ writeFileSync2(path7.join(cwd, "main", "main.collection"), MAIN_COLLECTION_CONTENT);
492
531
  written.push("main/main.collection");
493
- writeFileSync2(path6.join(cwd, "main", "main.script"), MAIN_SCRIPT_CONTENT);
532
+ writeFileSync2(path7.join(cwd, "main", "main.script"), MAIN_SCRIPT_CONTENT);
494
533
  written.push("main/main.script");
495
534
  const scriptKind = writeTsSurface(cwd, written);
496
535
  return { written, scriptKind };
497
536
  }
498
537
  function runInit(opts) {
499
538
  const { cwd, force = false } = opts;
500
- if (!existsSync2(path6.join(cwd, "game.project"))) {
539
+ if (!existsSync2(path7.join(cwd, "game.project"))) {
501
540
  return runNewProjectInit(cwd, force);
502
541
  }
503
542
  if (!force) {
504
543
  for (const rel of CONFLICTING_TS_CONFIGS) {
505
- if (existsSync2(path6.join(cwd, rel))) {
544
+ if (existsSync2(path7.join(cwd, rel))) {
506
545
  throw new Error(`defold-typescript init: refusing to overwrite existing TS config: ${rel}. Pass --force to overwrite.`);
507
546
  }
508
547
  }
@@ -530,11 +569,11 @@ import {
530
569
  existsSync as existsSync3,
531
570
  mkdirSync as mkdirSync3,
532
571
  readdirSync as readdirSync2,
533
- readFileSync as readFileSync6,
572
+ readFileSync as readFileSync7,
534
573
  rmSync as rmSync2,
535
574
  writeFileSync as writeFileSync3
536
575
  } from "node:fs";
537
- import * as path7 from "node:path";
576
+ import * as path8 from "node:path";
538
577
  var MATERIALIZED_ROOT = ".defold-types";
539
578
  function writeJson2(filePath, value) {
540
579
  writeFileSync3(filePath, `${JSON.stringify(value, null, 2)}
@@ -549,14 +588,14 @@ function materializeApiSurface(opts) {
549
588
  return { materializedDir: null, active: null };
550
589
  }
551
590
  const { surfaceId } = surface;
552
- const relDir = path7.posix.join(MATERIALIZED_ROOT, surfaceId);
553
- const absDir = path7.join(cwd, MATERIALIZED_ROOT, surfaceId);
591
+ const relDir = path8.posix.join(MATERIALIZED_ROOT, surfaceId);
592
+ const absDir = path8.join(cwd, MATERIALIZED_ROOT, surfaceId);
554
593
  mkdirSync3(absDir, { recursive: true });
555
594
  const excluded = excludedModulesForKind(opts.scriptKind ?? null);
556
595
  const sources = listDts(sourceGeneratedDir).filter((file) => file !== "index.d.ts").filter((file) => !excluded.has(file.replace(/\.d\.ts$/, "")));
557
- const srcDir = path7.resolve(sourceGeneratedDir, "..", "src");
558
- const overloads = ["msg-overloads.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path7.join(srcDir, file)));
559
- const coreTypesSrc = path7.join(srcDir, "core-types.ts");
596
+ const srcDir = path8.resolve(sourceGeneratedDir, "..", "src");
597
+ const overloads = ["msg-overloads.d.ts", "go-overloads.d.ts"].filter((file) => existsSync3(path8.join(srcDir, file)));
598
+ const coreTypesSrc = path8.join(srcDir, "core-types.ts");
560
599
  const includeCoreTypes = overloads.length > 0 && existsSync3(coreTypesSrc);
561
600
  const wanted = new Set(sources);
562
601
  for (const file of overloads) {
@@ -567,39 +606,39 @@ function materializeApiSurface(opts) {
567
606
  }
568
607
  for (const existing of readdirSync2(absDir)) {
569
608
  if (existing.endsWith(".d.ts") && existing !== "index.d.ts" && !wanted.has(existing)) {
570
- rmSync2(path7.join(absDir, existing));
609
+ rmSync2(path8.join(absDir, existing));
571
610
  }
572
611
  }
573
612
  for (const file of sources) {
574
- writeFileSync3(path7.join(absDir, file), readFileSync6(path7.join(sourceGeneratedDir, file), "utf8"));
613
+ writeFileSync3(path8.join(absDir, file), readFileSync7(path8.join(sourceGeneratedDir, file), "utf8"));
575
614
  }
576
615
  if (includeCoreTypes) {
577
- writeFileSync3(path7.join(absDir, "core-types.d.ts"), readFileSync6(coreTypesSrc, "utf8"));
616
+ writeFileSync3(path8.join(absDir, "core-types.d.ts"), readFileSync7(coreTypesSrc, "utf8"));
578
617
  }
579
618
  for (const file of overloads) {
580
- writeFileSync3(path7.join(absDir, file), readFileSync6(path7.join(srcDir, file), "utf8"));
619
+ writeFileSync3(path8.join(absDir, file), readFileSync7(path8.join(srcDir, file), "utf8"));
581
620
  }
582
621
  const modules = [...sources, ...overloads].map((file) => file.replace(/\.d\.ts$/, ""));
583
622
  const imports = modules.map((mod) => `import "./${mod}";`).join(`
584
623
  `);
585
- writeFileSync3(path7.join(absDir, "index.d.ts"), `${imports}
624
+ writeFileSync3(path8.join(absDir, "index.d.ts"), `${imports}
586
625
 
587
626
  export {};
588
627
  `);
589
- writeJson2(path7.join(absDir, "package.json"), {
628
+ writeJson2(path8.join(absDir, "package.json"), {
590
629
  name: `@defold-typescript/materialized-${surfaceId}`,
591
630
  types: "index.d.ts"
592
631
  });
593
632
  return { materializedDir: relDir, active: surfaceId };
594
633
  }
595
634
  function ensureGitignoreLine(cwd, line) {
596
- const gitignorePath = path7.join(cwd, ".gitignore");
635
+ const gitignorePath = path8.join(cwd, ".gitignore");
597
636
  if (!existsSync3(gitignorePath)) {
598
637
  writeFileSync3(gitignorePath, `${line}
599
638
  `);
600
639
  return;
601
640
  }
602
- const existing = readFileSync6(gitignorePath, "utf8");
641
+ const existing = readFileSync7(gitignorePath, "utf8");
603
642
  const present = new Set(existing.split(`
604
643
  `).map((entry) => entry.trim()));
605
644
  if (present.has(line)) {
@@ -615,10 +654,10 @@ function ensureMaterializedReference(cwd, materializedDir) {
615
654
  if (materializedDir === null) {
616
655
  return;
617
656
  }
618
- const surfaceId = path7.posix.basename(materializedDir);
619
- const tsconfigPath = path7.join(cwd, "tsconfig.json");
657
+ const surfaceId = path8.posix.basename(materializedDir);
658
+ const tsconfigPath = path8.join(cwd, "tsconfig.json");
620
659
  if (existsSync3(tsconfigPath)) {
621
- const tsconfig = JSON.parse(readFileSync6(tsconfigPath, "utf8"));
660
+ const tsconfig = JSON.parse(readFileSync7(tsconfigPath, "utf8"));
622
661
  tsconfig.compilerOptions = {
623
662
  ...tsconfig.compilerOptions ?? {},
624
663
  typeRoots: [MATERIALIZED_ROOT],
@@ -630,7 +669,7 @@ function ensureMaterializedReference(cwd, materializedDir) {
630
669
  }
631
670
  function resolveCurrentSurfaceGeneratedDir() {
632
671
  const root = resolveTypesPackageRoot();
633
- return root === null ? null : path7.join(root, "generated");
672
+ return root === null ? null : path8.join(root, "generated");
634
673
  }
635
674
  async function materializeRefDocSurface(opts) {
636
675
  const { cwd, surfaceId, resolveOpts } = opts;
@@ -640,21 +679,21 @@ async function materializeRefDocSurface(opts) {
640
679
  }
641
680
  const registry = opts.registry ?? loadApiTargetsRegistry();
642
681
  const target = registry.find((t) => t.id === surfaceId);
643
- if (!target || target.source?.kind !== "ref-doc") {
682
+ if (target?.source?.kind !== "ref-doc") {
644
683
  return { materializedDir: null, active: null };
645
684
  }
646
685
  const excludeModules = [...excludedModulesForKind(opts.scriptKind ?? null)];
647
- const relDir = path7.posix.join(MATERIALIZED_ROOT, surfaceId);
648
- const absDir = path7.join(cwd, MATERIALIZED_ROOT, surfaceId);
686
+ const relDir = path8.posix.join(MATERIALIZED_ROOT, surfaceId);
687
+ const absDir = path8.join(cwd, MATERIALIZED_ROOT, surfaceId);
649
688
  try {
650
- const mod = await import(path7.join(root, "scripts", "materialize-version.ts"));
689
+ const mod = await import(path8.join(root, "scripts", "materialize-version.ts"));
651
690
  const selfContained = { ...target, coreTypesImport: "./core-types" };
652
691
  await mod.materializeVersionedSurface(selfContained, {
653
692
  destDir: absDir,
654
693
  ...resolveOpts ? { resolveOpts } : {},
655
694
  ...excludeModules.length > 0 ? { excludeModules } : {}
656
695
  });
657
- copyFileSync(path7.join(root, "src", "core-types.ts"), path7.join(absDir, "core-types.d.ts"));
696
+ copyFileSync(path8.join(root, "src", "core-types.ts"), path8.join(absDir, "core-types.d.ts"));
658
697
  } catch {
659
698
  rmSync2(absDir, { recursive: true, force: true });
660
699
  return { materializedDir: null, active: null };
@@ -664,7 +703,7 @@ async function materializeRefDocSurface(opts) {
664
703
 
665
704
  // src/watch.ts
666
705
  import { existsSync as existsSync4, watch as fsWatch } from "node:fs";
667
- import * as path8 from "node:path";
706
+ import * as path9 from "node:path";
668
707
  var DEFAULT_DEBOUNCE_MS = 50;
669
708
  var recursiveWatcherFactory = (srcDir, onEvent) => {
670
709
  const w = fsWatch(srcDir, { recursive: true }, (eventType, filename) => {
@@ -707,7 +746,7 @@ function runWatch(opts) {
707
746
  waitForIdle: () => Promise.resolve()
708
747
  };
709
748
  }
710
- const srcDir = path8.join(cwd, "src");
749
+ const srcDir = path9.join(cwd, "src");
711
750
  let scheduled = null;
712
751
  let syncScheduled = null;
713
752
  let rebuildBusy = false;
@@ -731,7 +770,7 @@ function runWatch(opts) {
731
770
  const removed = [];
732
771
  for (const rel of drained) {
733
772
  const key = `src/${toPosix(rel)}`;
734
- if (existsSync4(path8.join(srcDir, rel))) {
773
+ if (existsSync4(path9.join(srcDir, rel))) {
735
774
  changed.push(key);
736
775
  } else {
737
776
  removed.push(key);
@@ -751,9 +790,10 @@ function runWatch(opts) {
751
790
  function onEvent(e) {
752
791
  if (stopped)
753
792
  return;
793
+ if (!e.path || !isTranspilerSource(e.path))
794
+ return;
754
795
  rebuildBusy = true;
755
- if (e.path)
756
- pending.add(e.path);
796
+ pending.add(e.path);
757
797
  if (scheduled)
758
798
  clearTimeout(scheduled);
759
799
  scheduled = setTimeout(rebuild, debounceMs);
@@ -831,23 +871,30 @@ function parseDefoldVersionFlag(argv) {
831
871
  return { flag, rest };
832
872
  }
833
873
  function readProjectPin(cwd) {
834
- const pkgPath = path9.join(cwd, "package.json");
874
+ const pkgPath = path10.join(cwd, "package.json");
835
875
  if (!existsSync5(pkgPath)) {
836
876
  return;
837
877
  }
838
878
  try {
839
- return readDefoldVersionPin(JSON.parse(readFileSync7(pkgPath, "utf8")));
879
+ return readDefoldVersionPin(JSON.parse(readFileSync8(pkgPath, "utf8")));
840
880
  } catch {
841
881
  return;
842
882
  }
843
883
  }
844
884
  function dispatch(argv, io, internals) {
845
885
  const json = argv.includes("--json");
886
+ if (argv.includes("--version") || argv.includes("-v")) {
887
+ const version = internals?.cliVersion ?? readCliVersion();
888
+ io.stdout.write(json ? `{"command":"version","ok":true,"version":${JSON.stringify(version)}}
889
+ ` : `defold-typescript ${version}
890
+ `);
891
+ return 0;
892
+ }
846
893
  const force = argv.includes("--force");
847
894
  const { flag: defoldVersionFlag, rest: nonFlagArgs } = parseDefoldVersionFlag(argv);
848
895
  const positional = nonFlagArgs.filter((a) => a !== "--json" && a !== "--force");
849
896
  const [command, ...rest] = positional;
850
- const cwd = rest[0] ? path9.resolve(rest[0]) : process.cwd();
897
+ const cwd = rest[0] ? path10.resolve(rest[0]) : process.cwd();
851
898
  const pin = readProjectPin(cwd);
852
899
  const resolvedVersion = resolveDefoldVersion({
853
900
  ...defoldVersionFlag !== undefined ? { flag: defoldVersionFlag } : {},
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@defold-typescript/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "End-user CLI for scaffolding and building Defold projects written in TypeScript.",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
@@ -30,7 +31,7 @@
30
31
  "test": "bun test"
31
32
  },
32
33
  "dependencies": {
33
- "@defold-typescript/transpiler": "0.1.0",
34
- "@defold-typescript/types": "0.1.0"
34
+ "@defold-typescript/transpiler": "0.3.0",
35
+ "@defold-typescript/types": "0.3.0"
35
36
  }
36
37
  }
@@ -21,6 +21,12 @@ export function toPosix(p: string, sep: string = path.sep): string {
21
21
  return p.split(sep).join("/");
22
22
  }
23
23
 
24
+ const TRANSPILER_SOURCE_RE = /\.(ts|tsx|cts|mts)$/;
25
+
26
+ export function isTranspilerSource(rel: string): boolean {
27
+ return TRANSPILER_SOURCE_RE.test(toPosix(rel));
28
+ }
29
+
24
30
  export function readBuildConfig(cwd: string): BuildConfig {
25
31
  const tsconfigPath = path.join(cwd, "tsconfig.json");
26
32
  let raw: string;
@@ -9,6 +9,7 @@ import {
9
9
  type BuildConfig,
10
10
  collectFailures,
11
11
  computeLuaRel,
12
+ isTranspilerSource,
12
13
  readBuildConfig,
13
14
  throwIfFailures,
14
15
  toPosix,
@@ -75,23 +76,25 @@ export function createBuildSession(opts: CreateBuildSessionOptions): BuildSessio
75
76
  }
76
77
 
77
78
  function applyEvents(changed: string[], removed: string[]): BuildResult {
79
+ const sourceChanged = changed.filter(isTranspilerSource);
80
+ const sourceRemoved = removed.filter(isTranspilerSource);
78
81
  const changes: Record<string, string | null> = {};
79
- for (const rel of changed) {
82
+ for (const rel of sourceChanged) {
80
83
  changes[rel] = readFileSync(path.join(cwd, rel), "utf8");
81
84
  }
82
- for (const rel of removed) {
85
+ for (const rel of sourceRemoved) {
83
86
  changes[rel] = null;
84
87
  }
85
88
 
86
89
  const result = session.update(changes);
87
90
 
88
- for (const rel of removed) {
91
+ for (const rel of sourceRemoved) {
89
92
  const luaAbs = path.join(cwd, computeLuaRel(rel, config));
90
93
  rmSync(luaAbs, { force: true });
91
94
  rmSync(`${luaAbs}.map`, { force: true });
92
95
  }
93
96
 
94
- return writeOutputs(result, changed);
97
+ return writeOutputs(result, sourceChanged);
95
98
  }
96
99
 
97
100
  return { buildAll, applyEvents };
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const VERSION_FALLBACK = "0.0.0";
6
+
7
+ // Anchor on the module URL, not `import.meta.dir` — the latter is a Bun-only
8
+ // property and is undefined when the bundled CLI runs under node (the `npx`
9
+ // path), which would silently anchor at the cwd. The bundled bin (`dist/bin.js`)
10
+ // and the in-repo `src/` loop both sit one level below the package root, so
11
+ // `package.json` is always `../package.json` from this module's directory. A
12
+ // missing or malformed manifest reports the fallback rather than throwing.
13
+ function defaultPackageRoot(): string {
14
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ }
16
+
17
+ export function readCliVersion(packageRoot: string = defaultPackageRoot()): string {
18
+ try {
19
+ const pkg = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8")) as {
20
+ version?: unknown;
21
+ };
22
+ return typeof pkg.version === "string" ? pkg.version : VERSION_FALLBACK;
23
+ } catch {
24
+ return VERSION_FALLBACK;
25
+ }
26
+ }
package/src/dispatch.ts CHANGED
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { RegistryTarget } from "./api-registry";
4
4
  import { CURRENT_STABLE_SURFACE_ID, selectApiSurface } from "./api-surface";
5
5
  import { runBuild } from "./build";
6
+ import { readCliVersion } from "./cli-version";
6
7
  import { readDefoldVersionPin, resolveDefoldVersion } from "./defold-version";
7
8
  import { runInit } from "./init";
8
9
  import { renderResult } from "./json-output";
@@ -35,6 +36,7 @@ export interface DispatchInternals {
35
36
  readonly sourceGeneratedDir?: string;
36
37
  readonly resolveOpts?: RefDocResolveOptions;
37
38
  readonly refDocRegistry?: readonly RegistryTarget[];
39
+ readonly cliVersion?: string;
38
40
  }
39
41
 
40
42
  const USAGE = "Usage: defold-typescript <init|build|watch> [path]\n";
@@ -74,6 +76,17 @@ export function dispatch(
74
76
  internals?: DispatchInternals,
75
77
  ): number | Promise<number> {
76
78
  const json = argv.includes("--json");
79
+
80
+ if (argv.includes("--version") || argv.includes("-v")) {
81
+ const version = internals?.cliVersion ?? readCliVersion();
82
+ io.stdout.write(
83
+ json
84
+ ? `{"command":"version","ok":true,"version":${JSON.stringify(version)}}\n`
85
+ : `defold-typescript ${version}\n`,
86
+ );
87
+ return 0;
88
+ }
89
+
77
90
  const force = argv.includes("--force");
78
91
  const { flag: defoldVersionFlag, rest: nonFlagArgs } = parseDefoldVersionFlag(argv);
79
92
  const positional = nonFlagArgs.filter((a) => a !== "--json" && a !== "--force");
package/src/init.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { CURRENT_STABLE_DEFOLD_VERSION } from "./defold-version";
4
5
  import {
5
6
  detectScriptKinds,
@@ -103,12 +104,44 @@ interface PackageJson {
103
104
  [key: string]: unknown;
104
105
  }
105
106
 
107
+ function typesVersionSpec(): string {
108
+ try {
109
+ // Anchor on the module URL, not `import.meta.dir` — the latter is a
110
+ // Bun-only property and is undefined when the bundled CLI runs under node
111
+ // (the `npx` path), which would silently fall back to "latest".
112
+ const here = path.dirname(fileURLToPath(import.meta.url));
113
+ const pkg = JSON.parse(readFileSync(path.join(here, "..", "package.json"), "utf8")) as {
114
+ version?: string;
115
+ };
116
+ return pkg.version ? `^${pkg.version}` : "latest";
117
+ } catch {
118
+ return "latest";
119
+ }
120
+ }
121
+
122
+ // Only @defold-typescript/types ships into the consumer (type-only, for the
123
+ // editor). The transpiler is a dependency of the CLI itself, pulled in when the
124
+ // user runs `build`/`watch`; the scaffold must not duplicate it. Pin types to
125
+ // this CLI's own version so the coordinated-release set stays in lockstep.
106
126
  const SCAFFOLD_DEV_DEPS: Record<string, string> = {
107
- "@defold-typescript/transpiler": "workspace:*",
108
- "@defold-typescript/types": "workspace:*",
127
+ "@defold-typescript/types": typesVersionSpec(),
109
128
  "@biomejs/biome": "^2.2.0",
110
129
  };
111
130
 
131
+ // Older scaffolds wrote both managed `@defold-typescript/*` devDeps as
132
+ // `workspace:*`, which only resolves inside this monorepo and breaks
133
+ // `bun install` in consumers. The additive merge in `writeTsSurface` never
134
+ // repairs an entry it didn't itself create, so repair them explicitly: the
135
+ // transpiler is CLI-internal and must not be a consumer dep at all, and a
136
+ // `workspace:` types pin must become a concrete published version. A concrete
137
+ // user-chosen types pin is left alone.
138
+ function repairManagedDevDeps(devDeps: Record<string, string>): void {
139
+ delete devDeps["@defold-typescript/transpiler"];
140
+ if (devDeps["@defold-typescript/types"]?.startsWith("workspace:")) {
141
+ devDeps["@defold-typescript/types"] = typesVersionSpec();
142
+ }
143
+ }
144
+
112
145
  function writeJson(filePath: string, value: unknown): void {
113
146
  writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
114
147
  }
@@ -163,6 +196,7 @@ function writeTsSurface(cwd: string, written: string[]): ScriptKind | null {
163
196
  devDeps[name] = version;
164
197
  }
165
198
  }
199
+ repairManagedDevDeps(devDeps);
166
200
  existing.devDependencies = devDeps;
167
201
  existing["defold-typescript"] ??= { "defold-version": CURRENT_STABLE_DEFOLD_VERSION };
168
202
  writeJson(pkgPath, existing);
@@ -198,7 +198,7 @@ export async function materializeRefDocSurface(
198
198
  }
199
199
  const registry = opts.registry ?? loadApiTargetsRegistry();
200
200
  const target = registry.find((t) => t.id === surfaceId);
201
- if (!target || target.source?.kind !== "ref-doc") {
201
+ if (target?.source?.kind !== "ref-doc") {
202
202
  return { materializedDir: null, active: null };
203
203
  }
204
204
  const excludeModules = [...excludedModulesForKind(opts.scriptKind ?? null)];
package/src/watch.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, watch as fsWatch } from "node:fs";
2
2
  import * as path from "node:path";
3
- import { toPosix } from "./build-output";
3
+ import { isTranspilerSource, toPosix } from "./build-output";
4
4
  import { type BuildSession, createBuildSession } from "./build-session";
5
5
  import { isComponentPath, isSkipped } from "./script-kind";
6
6
 
@@ -123,8 +123,9 @@ export function runWatch(opts: RunWatchOptions): RunWatchHandle {
123
123
 
124
124
  function onEvent(e: WatchEvent): void {
125
125
  if (stopped) return;
126
+ if (!e.path || !isTranspilerSource(e.path)) return;
126
127
  rebuildBusy = true;
127
- if (e.path) pending.add(e.path);
128
+ pending.add(e.path);
128
129
  if (scheduled) clearTimeout(scheduled);
129
130
  scheduled = setTimeout(rebuild, debounceMs);
130
131
  }