@defold-typescript/cli 0.5.5 → 0.7.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.
@@ -8,8 +8,10 @@ import {
8
8
  import {
9
9
  type BuildConfig,
10
10
  collectFailures,
11
- computeScriptRel,
11
+ computeOutputRel,
12
+ detectSourceOutputKind,
12
13
  isTranspilerSource,
14
+ outputRelsForSource,
13
15
  readBuildConfig,
14
16
  throwIfFailures,
15
17
  toPosix,
@@ -35,7 +37,20 @@ export function createBuildSession(opts: CreateBuildSessionOptions): BuildSessio
35
37
  const config: BuildConfig = readBuildConfig(cwd);
36
38
  const session: TranspileSession = createTranspileSession();
37
39
 
38
- function writeOutputs(result: TranspileProjectResult, keys: readonly string[]): BuildResult {
40
+ function pruneOutputs(rel: string, keepRel?: string): void {
41
+ for (const outputRel of outputRelsForSource(rel, config)) {
42
+ if (outputRel !== keepRel && outputRel !== `${keepRel}.map`) {
43
+ rmSync(path.join(cwd, outputRel), { force: true });
44
+ }
45
+ }
46
+ }
47
+
48
+ function writeOutputs(
49
+ result: TranspileProjectResult,
50
+ keys: readonly string[],
51
+ sources: Record<string, string>,
52
+ pruneAlternatives = false,
53
+ ): BuildResult {
39
54
  const failures = collectFailures(result.diagnostics);
40
55
  const written: string[] = [];
41
56
  for (const rel of keys) {
@@ -46,12 +61,15 @@ export function createBuildSession(opts: CreateBuildSessionOptions): BuildSessio
46
61
  if (lua === undefined) {
47
62
  continue;
48
63
  }
49
- const scriptRel = computeScriptRel(rel, config);
50
- writeScriptFile(cwd, scriptRel, lua, result.sourceMaps[rel]);
51
- written.push(scriptRel);
64
+ const outputRel = computeOutputRel(rel, config, detectSourceOutputKind(sources[rel] ?? ""));
65
+ if (pruneAlternatives) {
66
+ pruneOutputs(rel, outputRel);
67
+ }
68
+ writeScriptFile(cwd, outputRel, lua, result.sourceMaps[rel]);
69
+ written.push(outputRel);
52
70
  }
53
71
  throwIfFailures(failures);
54
- return { written };
72
+ return { written: written.sort() };
55
73
  }
56
74
 
57
75
  function buildAll(): BuildResult {
@@ -72,7 +90,7 @@ export function createBuildSession(opts: CreateBuildSessionOptions): BuildSessio
72
90
  }
73
91
 
74
92
  const result = session.update(files);
75
- return writeOutputs(result, sources);
93
+ return writeOutputs(result, sources, files);
76
94
  }
77
95
 
78
96
  function applyEvents(changed: string[], removed: string[]): BuildResult {
@@ -89,12 +107,14 @@ export function createBuildSession(opts: CreateBuildSessionOptions): BuildSessio
89
107
  const result = session.update(changes);
90
108
 
91
109
  for (const rel of sourceRemoved) {
92
- const scriptAbs = path.join(cwd, computeScriptRel(rel, config));
93
- rmSync(scriptAbs, { force: true });
94
- rmSync(`${scriptAbs}.map`, { force: true });
110
+ pruneOutputs(rel);
95
111
  }
96
112
 
97
- return writeOutputs(result, sourceChanged);
113
+ const changedSources: Record<string, string> = {};
114
+ for (const rel of sourceChanged) {
115
+ changedSources[rel] = changes[rel] ?? "";
116
+ }
117
+ return writeOutputs(result, sourceChanged, changedSources, true);
98
118
  }
99
119
 
100
120
  return { buildAll, applyEvents };
package/src/build.ts CHANGED
@@ -3,7 +3,8 @@ import * as path from "node:path";
3
3
  import { transpileProject } from "@defold-typescript/transpiler";
4
4
  import {
5
5
  collectFailures,
6
- computeScriptRel,
6
+ computeOutputRel,
7
+ detectSourceOutputKind,
7
8
  readBuildConfig,
8
9
  throwIfFailures,
9
10
  toPosix,
@@ -52,11 +53,11 @@ export function runBuild(opts: RunBuildOptions): RunBuildResult {
52
53
  if (!lua) {
53
54
  continue;
54
55
  }
55
- const scriptRel = computeScriptRel(rel, config);
56
- writeScriptFile(cwd, scriptRel, lua, result.sourceMaps[rel]);
57
- written.push(scriptRel);
56
+ const outputRel = computeOutputRel(rel, config, detectSourceOutputKind(files[rel] ?? ""));
57
+ writeScriptFile(cwd, outputRel, lua, result.sourceMaps[rel]);
58
+ written.push(outputRel);
58
59
  }
59
60
 
60
61
  throwIfFailures(failures);
61
- return { written };
62
+ return { written: written.sort() };
62
63
  }
@@ -7,28 +7,44 @@ export interface EngineTarget {
7
7
  }
8
8
 
9
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.
10
+ // native-extension build output. Current Defold uses the same `-macos` identifier
11
+ // for both, but they are tracked separately so a future divergence stays local.
12
+ // Keyed by `${process.platform}-${process.arch}` because Apple Silicon and Intel
13
+ // resolve to different engine archives (`arm64-macos` vs `x86_64-macos`).
12
14
  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: {
15
+ "darwin-arm64": {
16
+ enginePlatform: "arm64-macos",
17
+ buildFolder: "arm64-macos",
18
+ executable: "dmengine",
19
+ },
20
+ "darwin-x64": {
21
+ enginePlatform: "x86_64-macos",
22
+ buildFolder: "x86_64-macos",
23
+ executable: "dmengine",
24
+ },
25
+ "linux-x64": {
26
+ enginePlatform: "x86_64-linux",
27
+ buildFolder: "x86_64-linux",
28
+ executable: "dmengine",
29
+ },
30
+ "win32-x64": {
16
31
  enginePlatform: "x86_64-win32",
17
32
  buildFolder: "x86_64-win32",
18
33
  executable: "dmengine.exe",
19
34
  },
20
35
  };
21
36
 
22
- const ENGINE_INFO_URL = "https://d.defold.com/stable/info.json";
23
- const ENGINE_ARCHIVE_BASE = "https://d.defold.com/archive/stable";
37
+ export const ENGINE_INFO_URL = "https://d.defold.com/stable/info.json";
38
+ export const ENGINE_ARCHIVE_BASE = "https://d.defold.com/archive/stable";
24
39
 
25
40
  export const DEBUG_LAUNCHER_REL = ".vscode/defold-debug.ts";
26
41
 
27
- export function targetPlatform(platform: NodeJS.Platform): EngineTarget {
28
- const target = PLATFORM_TARGETS[platform];
42
+ export function targetPlatform(platform: NodeJS.Platform, arch: string): EngineTarget {
43
+ const key = `${platform}-${arch}`;
44
+ const target = PLATFORM_TARGETS[key];
29
45
  if (!target) {
30
46
  throw new Error(
31
- `defold-typescript debug: unsupported platform "${platform}"; expected one of ${Object.keys(
47
+ `defold-typescript debug: unsupported platform "${key}"; expected one of ${Object.keys(
32
48
  PLATFORM_TARGETS,
33
49
  ).join(", ")}.`,
34
50
  );
@@ -69,6 +85,14 @@ export function debugLaunchConfig() {
69
85
  internalConsoleOptions: "openOnSessionStart",
70
86
  program: { command: "bun" },
71
87
  args: [DEBUG_LAUNCHER_REL],
88
+ // Local Lua Debugger (>=0.3.0) pre-scans `scriptFiles` for the emitted
89
+ // `--# sourceMappingURL=` trailers so a breakpoint in a `.ts` resolves
90
+ // ahead of time; without it no source-mapped breakpoint ever binds. Every
91
+ // build emits `<name>.ts.script` under `src/`. `scriptRoots` lets the
92
+ // debugger resolve the running Defold chunk path (`/src/...`) and the map's
93
+ // bare `sources` entry (`player.ts`) back to files on disk.
94
+ scriptFiles: ["src/**/*.ts.script"],
95
+ scriptRoots: [".", "src"],
72
96
  };
73
97
  }
74
98
 
@@ -103,9 +127,9 @@ const PLATFORM_TARGETS: Record<string, EngineTarget> = ${targets};
103
127
  const ENGINE_INFO_URL = "${ENGINE_INFO_URL}";
104
128
  const ENGINE_ARCHIVE_BASE = "${ENGINE_ARCHIVE_BASE}";
105
129
 
106
- const target = PLATFORM_TARGETS[process.platform];
130
+ const target = PLATFORM_TARGETS[\`\${process.platform}-\${process.arch}\`];
107
131
  if (!target) {
108
- console.error(\`Unsupported platform: \${process.platform}\`);
132
+ console.error(\`Unsupported platform: \${process.platform}-\${process.arch}\`);
109
133
  process.exit(1);
110
134
  }
111
135
 
@@ -0,0 +1,214 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { detectSourceOutputKind, isTranspilerSource, readBuildConfig } from "./build-output";
4
+ import { scanFilesSync } from "./scan";
5
+ import {
6
+ isSkipped,
7
+ type ScriptKind,
8
+ selectDirectoryWalls,
9
+ selectScriptKind,
10
+ selectScriptKindEntrypoint,
11
+ } from "./script-kind";
12
+
13
+ export interface DirectoryWall {
14
+ readonly dir: string;
15
+ readonly kind: ScriptKind;
16
+ readonly typesEntrypoint: string;
17
+ }
18
+
19
+ function describeWall(dir: string, kind: ScriptKind): DirectoryWall {
20
+ return {
21
+ dir,
22
+ kind,
23
+ typesEntrypoint: selectScriptKindEntrypoint(new Set([kind])),
24
+ };
25
+ }
26
+
27
+ export function planDirectoryWalls(cwd: string): DirectoryWall[] {
28
+ return [...selectDirectoryWalls(cwd)]
29
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
30
+ .map(([dir, kind]) => describeWall(dir, kind));
31
+ }
32
+
33
+ export function groupSourceScriptKindsByDirectory(cwd: string): Map<string, Set<ScriptKind>> {
34
+ const byDir = new Map<string, Set<ScriptKind>>();
35
+ const seen = new Set<string>();
36
+ for (const pattern of readBuildConfig(cwd).include) {
37
+ for (const match of scanFilesSync(cwd, pattern)) {
38
+ const rel = match.split(path.sep).join("/");
39
+ if (seen.has(rel) || !isTranspilerSource(rel) || isSkipped(rel)) {
40
+ continue;
41
+ }
42
+ seen.add(rel);
43
+ const kind = detectSourceOutputKind(readFileSync(path.join(cwd, match), "utf8"));
44
+ if (kind === "module") {
45
+ continue;
46
+ }
47
+ const dir = path.posix.dirname(rel);
48
+ let set = byDir.get(dir);
49
+ if (set === undefined) {
50
+ set = new Set<ScriptKind>();
51
+ byDir.set(dir, set);
52
+ }
53
+ set.add(kind);
54
+ }
55
+ }
56
+ return byDir;
57
+ }
58
+
59
+ export function planSourceDirectoryWalls(cwd: string): DirectoryWall[] {
60
+ const walls: DirectoryWall[] = [];
61
+ for (const [dir, kinds] of groupSourceScriptKindsByDirectory(cwd)) {
62
+ const kind = selectScriptKind(kinds);
63
+ if (kind !== null) {
64
+ walls.push(describeWall(dir, kind));
65
+ }
66
+ }
67
+ return walls.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
68
+ }
69
+
70
+ interface WallTsconfig {
71
+ readonly extends: string;
72
+ readonly compilerOptions: {
73
+ readonly composite: true;
74
+ readonly typeRoots: null;
75
+ readonly types: string[];
76
+ };
77
+ readonly include: readonly ["**/*.ts"];
78
+ readonly exclude: readonly [];
79
+ }
80
+
81
+ export function directoryWallTsconfig(wall: DirectoryWall): WallTsconfig {
82
+ const depth = wall.dir.split("/").length;
83
+ return {
84
+ extends: `${"../".repeat(depth)}tsconfig.json`,
85
+ compilerOptions: { composite: true, typeRoots: null, types: [wall.typesEntrypoint] },
86
+ include: ["**/*.ts"],
87
+ exclude: [],
88
+ };
89
+ }
90
+
91
+ function writeJson(filePath: string, value: unknown): void {
92
+ mkdirSync(path.dirname(filePath), { recursive: true });
93
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
94
+ }
95
+
96
+ interface RootTsconfig {
97
+ exclude?: string[];
98
+ files?: string[];
99
+ references?: Array<{ path: string }>;
100
+ [key: string]: unknown;
101
+ }
102
+
103
+ function sortedWallDirs(walls: readonly DirectoryWall[]): string[] {
104
+ return walls
105
+ .map((w) => w.dir)
106
+ .filter((dir) => dir !== ".")
107
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
108
+ }
109
+
110
+ function isInsideAnyDir(rel: string, dirs: readonly string[]): boolean {
111
+ return dirs.some((dir) => rel === dir || rel.startsWith(`${dir}/`));
112
+ }
113
+
114
+ function hasRootOwnedTranspilerSources(cwd: string, wallDirs: readonly string[]): boolean {
115
+ const seen = new Set<string>();
116
+ for (const pattern of readBuildConfig(cwd).include) {
117
+ for (const match of scanFilesSync(cwd, pattern)) {
118
+ const rel = match.split(path.sep).join("/");
119
+ if (seen.has(rel) || !isTranspilerSource(rel) || isSkipped(rel)) {
120
+ continue;
121
+ }
122
+ seen.add(rel);
123
+ if (!isInsideAnyDir(rel, wallDirs)) {
124
+ return true;
125
+ }
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+
131
+ export function wireWallReferences(cwd: string, walls: readonly DirectoryWall[]): void {
132
+ const rootPath = path.join(cwd, "tsconfig.json");
133
+ const current = JSON.parse(readFileSync(rootPath, "utf8")) as RootTsconfig;
134
+ const wallDirs = sortedWallDirs(walls);
135
+ const previousReferences = current.references ?? [];
136
+ const previousManaged = new Set(previousReferences.map((ref) => ref.path));
137
+ const nextExclude = [
138
+ ...new Set([
139
+ ...(current.exclude ?? []).filter((entry) => !previousManaged.has(entry)),
140
+ ...wallDirs,
141
+ ]),
142
+ ];
143
+ const next: RootTsconfig = { ...current };
144
+
145
+ if (wallDirs.length > 0) {
146
+ next.references = wallDirs.map((dir) => ({ path: dir }));
147
+ } else {
148
+ delete next.references;
149
+ }
150
+
151
+ if (nextExclude.length > 0) {
152
+ next.exclude = nextExclude;
153
+ } else {
154
+ delete next.exclude;
155
+ }
156
+
157
+ if (wallDirs.length > 0 && !hasRootOwnedTranspilerSources(cwd, wallDirs)) {
158
+ next.files = [];
159
+ } else if (previousReferences.length > 0 && JSON.stringify(next.files) === JSON.stringify([])) {
160
+ delete next.files;
161
+ }
162
+
163
+ if (JSON.stringify(next) !== JSON.stringify(current)) {
164
+ writeJson(rootPath, next);
165
+ }
166
+ }
167
+
168
+ export function writeDirectoryWallTsconfigs(cwd: string, walls: DirectoryWall[]): string[] {
169
+ const written: string[] = [];
170
+ for (const w of walls) {
171
+ if (w.dir === ".") {
172
+ continue;
173
+ }
174
+ const rel = `${w.dir}/tsconfig.json`;
175
+ const target = path.join(cwd, w.dir, "tsconfig.json");
176
+ const desired = directoryWallTsconfig(w);
177
+ if (existsSync(target)) {
178
+ const current = JSON.parse(readFileSync(target, "utf8")) as {
179
+ extends?: string;
180
+ compilerOptions?: Record<string, unknown>;
181
+ [key: string]: unknown;
182
+ };
183
+ const options = current.compilerOptions ?? {};
184
+ // Skip the write when already narrowed so a consumer's formatting is not
185
+ // churned to JSON.stringify's layout on every build.
186
+ const alreadyNarrowed =
187
+ current.extends === desired.extends &&
188
+ options.composite === desired.compilerOptions.composite &&
189
+ options.typeRoots === desired.compilerOptions.typeRoots &&
190
+ JSON.stringify(options.types) === JSON.stringify(desired.compilerOptions.types) &&
191
+ JSON.stringify(current.include) === JSON.stringify(desired.include) &&
192
+ JSON.stringify(current.exclude) === JSON.stringify(desired.exclude);
193
+ if (!alreadyNarrowed) {
194
+ writeJson(target, {
195
+ ...current,
196
+ extends: desired.extends,
197
+ compilerOptions: {
198
+ ...options,
199
+ composite: desired.compilerOptions.composite,
200
+ typeRoots: desired.compilerOptions.typeRoots,
201
+ types: desired.compilerOptions.types,
202
+ },
203
+ include: desired.include,
204
+ exclude: desired.exclude,
205
+ });
206
+ written.push(rel);
207
+ }
208
+ } else {
209
+ writeJson(target, desired);
210
+ written.push(rel);
211
+ }
212
+ }
213
+ return written.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
214
+ }