@defold-typescript/cli 0.5.2 → 0.5.4
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 +267 -50
- package/dist/debug-launcher.d.ts +43 -0
- package/dist/index.js +267 -50
- package/dist/init.d.ts +1 -0
- package/dist/mise-scaffold.d.ts +2 -0
- package/package.json +3 -3
- package/src/debug-launcher.ts +165 -0
- package/src/init.ts +126 -52
- 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
|
|
|
@@ -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
|
-
|
|
154
|
+
return [
|
|
98
155
|
`import { ${factory} } from "@defold-typescript/types";`,
|
|
99
156
|
"",
|
|
100
157
|
`export const script = ${factory}({`,
|
|
101
|
-
|
|
158
|
+
` // ${HOOK_COMMENTS.init}`,
|
|
102
159
|
" init() {",
|
|
103
160
|
" return { $0 };",
|
|
104
161
|
" },",
|
|
105
|
-
|
|
106
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
177
|
+
` // ${HOOK_COMMENTS.init}`,
|
|
137
178
|
" init(): Self {",
|
|
138
179
|
" return { $0 };",
|
|
139
180
|
" },",
|
|
140
|
-
|
|
141
|
-
"
|
|
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> = {
|
|
@@ -244,27 +269,31 @@ function typesVersionSpec(): string {
|
|
|
244
269
|
}
|
|
245
270
|
}
|
|
246
271
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
272
|
+
// @defold-typescript/types (type-only, for the editor) and @defold-typescript/cli
|
|
273
|
+
// (the local bin the managed `bunx --no-install defold-typescript` mise tasks
|
|
274
|
+
// resolve) both ship into the consumer. The transpiler must NOT be a direct
|
|
275
|
+
// consumer dep — it arrives transitively through the CLI. Pin both managed deps
|
|
276
|
+
// to this CLI's own version so the coordinated-release set stays in lockstep.
|
|
277
|
+
export const SCAFFOLD_DEV_DEPS: Record<string, string> = {
|
|
252
278
|
"@defold-typescript/types": typesVersionSpec(),
|
|
279
|
+
"@defold-typescript/cli": typesVersionSpec(),
|
|
253
280
|
"@biomejs/biome": "^2.2.0",
|
|
254
281
|
};
|
|
255
282
|
|
|
256
|
-
// Older scaffolds wrote
|
|
283
|
+
// Older scaffolds wrote the managed `@defold-typescript/*` devDeps as
|
|
257
284
|
// `workspace:*`, which only resolves inside this monorepo and breaks
|
|
258
285
|
// `bun install` in consumers. The additive merge in `writeTsSurface` never
|
|
259
286
|
// repairs an entry it didn't itself create, so repair them explicitly: the
|
|
260
287
|
// transpiler is CLI-internal and must not be a consumer dep at all, and a
|
|
261
|
-
// `workspace:` types pin must become a concrete published version. A
|
|
262
|
-
// user-chosen
|
|
263
|
-
// opt-in to refresh the managed
|
|
288
|
+
// `workspace:` types/cli pin must become a concrete published version. A
|
|
289
|
+
// concrete user-chosen pin is left alone unless `force` is set, the explicit
|
|
290
|
+
// opt-in to refresh the managed pins (and only those) to the CLI's version.
|
|
264
291
|
function repairManagedDevDeps(devDeps: Record<string, string>, force = false): void {
|
|
265
292
|
delete devDeps["@defold-typescript/transpiler"];
|
|
266
|
-
|
|
267
|
-
devDeps["
|
|
293
|
+
for (const name of ["@defold-typescript/types", "@defold-typescript/cli"]) {
|
|
294
|
+
if (force || devDeps[name]?.startsWith("workspace:")) {
|
|
295
|
+
devDeps[name] = typesVersionSpec();
|
|
296
|
+
}
|
|
268
297
|
}
|
|
269
298
|
}
|
|
270
299
|
|
|
@@ -297,6 +326,13 @@ function writeBiome(cwd: string, written: string[]): void {
|
|
|
297
326
|
written.push("biome.json");
|
|
298
327
|
}
|
|
299
328
|
|
|
329
|
+
function writeMiseTasks(cwd: string, written: string[]): void {
|
|
330
|
+
const misePath = path.join(cwd, "mise.toml");
|
|
331
|
+
const existing = existsSync(misePath) ? readFileSync(misePath, "utf8") : undefined;
|
|
332
|
+
writeFileSync(misePath, mergeMiseToml(existing));
|
|
333
|
+
written.push("mise.toml");
|
|
334
|
+
}
|
|
335
|
+
|
|
300
336
|
// Strip `//` line comments, `/* */` block comments, and trailing commas so a
|
|
301
337
|
// hand-edited JSONC `.vscode` file parses with `JSON.parse`. The walk tracks
|
|
302
338
|
// string state so a `//` or comma inside a value (e.g. a URL) is preserved.
|
|
@@ -438,6 +474,41 @@ function writeVscodeSnippets(cwd: string, written: string[]): void {
|
|
|
438
474
|
written.push(".vscode/defold-typescript.code-snippets");
|
|
439
475
|
}
|
|
440
476
|
|
|
477
|
+
function writeVscodeLaunch(cwd: string, written: string[]): void {
|
|
478
|
+
const dir = path.join(cwd, ".vscode");
|
|
479
|
+
const filePath = path.join(dir, "launch.json");
|
|
480
|
+
const ours = debugLaunchConfig();
|
|
481
|
+
if (existsSync(filePath)) {
|
|
482
|
+
const existing = readVscodeJson(filePath);
|
|
483
|
+
if (existing === null) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const configs = Array.isArray(existing.configurations) ? [...existing.configurations] : [];
|
|
487
|
+
const names = new Set(configs.map((c) => (isJsonObject(c) ? c.name : undefined)));
|
|
488
|
+
if (!names.has(ours.name)) {
|
|
489
|
+
configs.push(ours);
|
|
490
|
+
}
|
|
491
|
+
existing.configurations = configs;
|
|
492
|
+
existing.version ??= VSCODE_LAUNCH_CONTENT.version;
|
|
493
|
+
writeJson(filePath, existing);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
mkdirSync(dir, { recursive: true });
|
|
497
|
+
writeJson(filePath, VSCODE_LAUNCH_CONTENT);
|
|
498
|
+
written.push(".vscode/launch.json");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function writeVscodeDebugLauncher(cwd: string, written: string[]): void {
|
|
502
|
+
const dir = path.join(cwd, ".vscode");
|
|
503
|
+
const filePath = path.join(dir, "defold-debug.ts");
|
|
504
|
+
if (existsSync(filePath)) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
mkdirSync(dir, { recursive: true });
|
|
508
|
+
writeFileSync(filePath, DEBUG_LAUNCHER_SOURCE);
|
|
509
|
+
written.push(".vscode/defold-debug.ts");
|
|
510
|
+
}
|
|
511
|
+
|
|
441
512
|
function writeTsSurface(cwd: string, written: string[], force = false): ScriptKind | null {
|
|
442
513
|
mkdirSync(path.join(cwd, "src"), { recursive: true });
|
|
443
514
|
writeFileSync(path.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
|
|
@@ -483,10 +554,13 @@ function writeTsSurface(cwd: string, written: string[], force = false): ScriptKi
|
|
|
483
554
|
written.push(".gitignore");
|
|
484
555
|
|
|
485
556
|
writeBiome(cwd, written);
|
|
557
|
+
writeMiseTasks(cwd, written);
|
|
486
558
|
|
|
487
559
|
writeVscodeExtensions(cwd, written);
|
|
488
560
|
writeVscodeSettings(cwd, written);
|
|
489
561
|
writeVscodeSnippets(cwd, written);
|
|
562
|
+
writeVscodeLaunch(cwd, written);
|
|
563
|
+
writeVscodeDebugLauncher(cwd, written);
|
|
490
564
|
|
|
491
565
|
return selectScriptKind(kinds);
|
|
492
566
|
}
|
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
|
+
}
|