@defold-typescript/cli 0.5.1 → 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 +8 -3
- package/dist/bin.js +344 -8
- package/dist/debug-launcher.d.ts +43 -0
- package/dist/index.js +344 -8
- package/dist/mise-scaffold.d.ts +2 -0
- package/package.json +3 -3
- package/src/debug-launcher.ts +165 -0
- package/src/init.ts +209 -1
- package/src/materialize.ts +17 -8
- package/src/mise-scaffold.ts +62 -0
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
|
|
|
@@ -81,6 +84,144 @@ const VSCODE_SETTINGS_CONTENT = {
|
|
|
81
84
|
"Lua.workspace.ignoreDir": ["src"],
|
|
82
85
|
};
|
|
83
86
|
|
|
87
|
+
interface VscodeSnippet {
|
|
88
|
+
scope: string;
|
|
89
|
+
prefix: string;
|
|
90
|
+
body: string[];
|
|
91
|
+
description: string;
|
|
92
|
+
}
|
|
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
|
+
|
|
148
|
+
// Whole-file TS scaffolds mirroring the Defold editor's empty script/gui/render
|
|
149
|
+
// templates over the lifecycle factories. Two self-typing variants per kind:
|
|
150
|
+
// inline-self (TSelf inferred from `init`'s return) and typed-self (an explicit
|
|
151
|
+
// dummy `Self` placeholder). Hook order mirrors the Lua templates; render omits
|
|
152
|
+
// `on_input` because `RenderScriptHooks` does. The final `$0` lands inside `init`.
|
|
153
|
+
function inlineSnippetBody(factory: string, includeOnInput: boolean): string[] {
|
|
154
|
+
return [
|
|
155
|
+
`import { ${factory} } from "@defold-typescript/types";`,
|
|
156
|
+
"",
|
|
157
|
+
`export const script = ${factory}({`,
|
|
158
|
+
` // ${HOOK_COMMENTS.init}`,
|
|
159
|
+
" init() {",
|
|
160
|
+
" return { $0 };",
|
|
161
|
+
" },",
|
|
162
|
+
...hookLines(includeOnInput, 1),
|
|
163
|
+
"});",
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function typedSnippetBody(factory: string, includeOnInput: boolean): string[] {
|
|
168
|
+
return [
|
|
169
|
+
`import { ${factory} } from "@defold-typescript/types";`,
|
|
170
|
+
"",
|
|
171
|
+
"type Self = {",
|
|
172
|
+
" // Your script's state goes here.",
|
|
173
|
+
" $1",
|
|
174
|
+
"};",
|
|
175
|
+
"",
|
|
176
|
+
`export const script = ${factory}<Self>({`,
|
|
177
|
+
` // ${HOOK_COMMENTS.init}`,
|
|
178
|
+
" init(): Self {",
|
|
179
|
+
" return { $0 };",
|
|
180
|
+
" },",
|
|
181
|
+
...hookLines(includeOnInput, 2),
|
|
182
|
+
"});",
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const VSCODE_SNIPPETS_CONTENT: Record<string, VscodeSnippet> = {
|
|
187
|
+
"Defold script (inferred self)": {
|
|
188
|
+
scope: "typescript",
|
|
189
|
+
prefix: "defold-script",
|
|
190
|
+
body: inlineSnippetBody("defineScript", true),
|
|
191
|
+
description: "Empty Defold script; state inferred from init's return.",
|
|
192
|
+
},
|
|
193
|
+
"Defold script (typed self)": {
|
|
194
|
+
scope: "typescript",
|
|
195
|
+
prefix: "defold-script-typed",
|
|
196
|
+
body: typedSnippetBody("defineScript", true),
|
|
197
|
+
description: "Empty Defold script with an explicit Self type.",
|
|
198
|
+
},
|
|
199
|
+
"Defold GUI script (inferred self)": {
|
|
200
|
+
scope: "typescript",
|
|
201
|
+
prefix: "defold-gui",
|
|
202
|
+
body: inlineSnippetBody("defineGuiScript", true),
|
|
203
|
+
description: "Empty Defold GUI script; state inferred from init's return.",
|
|
204
|
+
},
|
|
205
|
+
"Defold GUI script (typed self)": {
|
|
206
|
+
scope: "typescript",
|
|
207
|
+
prefix: "defold-gui-typed",
|
|
208
|
+
body: typedSnippetBody("defineGuiScript", true),
|
|
209
|
+
description: "Empty Defold GUI script with an explicit Self type.",
|
|
210
|
+
},
|
|
211
|
+
"Defold render script (inferred self)": {
|
|
212
|
+
scope: "typescript",
|
|
213
|
+
prefix: "defold-render",
|
|
214
|
+
body: inlineSnippetBody("defineRenderScript", false),
|
|
215
|
+
description: "Empty Defold render script; state inferred from init's return.",
|
|
216
|
+
},
|
|
217
|
+
"Defold render script (typed self)": {
|
|
218
|
+
scope: "typescript",
|
|
219
|
+
prefix: "defold-render-typed",
|
|
220
|
+
body: typedSnippetBody("defineRenderScript", false),
|
|
221
|
+
description: "Empty Defold render script with an explicit Self type.",
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
84
225
|
const MAIN_TS_CONTENT = `export function init(): void {
|
|
85
226
|
const start = vmath.vector3(0, 0, 0);
|
|
86
227
|
msg.post("main:/hero", "spawn", { start });
|
|
@@ -181,6 +322,13 @@ function writeBiome(cwd: string, written: string[]): void {
|
|
|
181
322
|
written.push("biome.json");
|
|
182
323
|
}
|
|
183
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
|
+
|
|
184
332
|
// Strip `//` line comments, `/* */` block comments, and trailing commas so a
|
|
185
333
|
// hand-edited JSONC `.vscode` file parses with `JSON.parse`. The walk tracks
|
|
186
334
|
// string state so a `//` or comma inside a value (e.g. a URL) is preserved.
|
|
@@ -301,6 +449,62 @@ function writeVscodeSettings(cwd: string, written: string[]): void {
|
|
|
301
449
|
written.push(".vscode/settings.json");
|
|
302
450
|
}
|
|
303
451
|
|
|
452
|
+
function writeVscodeSnippets(cwd: string, written: string[]): void {
|
|
453
|
+
const dir = path.join(cwd, ".vscode");
|
|
454
|
+
const filePath = path.join(dir, "defold-typescript.code-snippets");
|
|
455
|
+
if (existsSync(filePath)) {
|
|
456
|
+
const existing = readVscodeJson(filePath);
|
|
457
|
+
if (existing === null) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (const [key, snippet] of Object.entries(VSCODE_SNIPPETS_CONTENT)) {
|
|
461
|
+
if (!(key in existing)) {
|
|
462
|
+
existing[key] = snippet;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
writeJson(filePath, existing);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
mkdirSync(dir, { recursive: true });
|
|
469
|
+
writeJson(filePath, VSCODE_SNIPPETS_CONTENT);
|
|
470
|
+
written.push(".vscode/defold-typescript.code-snippets");
|
|
471
|
+
}
|
|
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
|
+
|
|
304
508
|
function writeTsSurface(cwd: string, written: string[], force = false): ScriptKind | null {
|
|
305
509
|
mkdirSync(path.join(cwd, "src"), { recursive: true });
|
|
306
510
|
writeFileSync(path.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
|
|
@@ -346,9 +550,13 @@ function writeTsSurface(cwd: string, written: string[], force = false): ScriptKi
|
|
|
346
550
|
written.push(".gitignore");
|
|
347
551
|
|
|
348
552
|
writeBiome(cwd, written);
|
|
553
|
+
writeMiseTasks(cwd, written);
|
|
349
554
|
|
|
350
555
|
writeVscodeExtensions(cwd, written);
|
|
351
556
|
writeVscodeSettings(cwd, written);
|
|
557
|
+
writeVscodeSnippets(cwd, written);
|
|
558
|
+
writeVscodeLaunch(cwd, written);
|
|
559
|
+
writeVscodeDebugLauncher(cwd, written);
|
|
352
560
|
|
|
353
561
|
return selectScriptKind(kinds);
|
|
354
562
|
}
|
package/src/materialize.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|