@defold-typescript/types 0.24.0 → 0.25.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/scripts/regen.ts CHANGED
@@ -1,8 +1,13 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { resolve } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
3
  import messagesDoc from "../fixtures/messages_doc.json" with { type: "json" };
4
4
  import { type ApiModule, parseDefoldApiDoc } from "../src/api-doc";
5
- import { emitDeclarations, emitSymbolSignatures, type SymbolSignature } from "../src/emit-dts";
5
+ import {
6
+ defaultMapType,
7
+ emitDeclarations,
8
+ emitSymbolSignatures,
9
+ type SymbolSignature,
10
+ } from "../src/emit-dts";
6
11
  import {
7
12
  applyMessageDeprecations,
8
13
  emitBuiltinMessages,
@@ -10,6 +15,7 @@ import {
10
15
  } from "../src/emit-messages";
11
16
  import type { TranslationStore } from "../src/example-store";
12
17
  import { wrapAsAmbientGlobal, wrapAsModule } from "../src/publish-dts";
18
+ import type { UrlParameterTable } from "../src/url-parameters";
13
19
  import {
14
20
  type DocSourceProvenance,
15
21
  type DownloadRefDoc,
@@ -28,6 +34,16 @@ export interface ApiTargetModule {
28
34
  readonly skipFunctions?: readonly string[];
29
35
  }
30
36
 
37
+ // An editor-VM document declared by a target. It carries two things a runtime
38
+ // module never needs: its own `importsFrom` (the `editor-vm/` subdirectory sits
39
+ // a level below the runtime surface, so the target's `coreTypesImport` does not
40
+ // reach) and a named `mapType` selector, since the editor VM is the only surface
41
+ // with handle tokens no runtime namespace uses.
42
+ export interface ApiTargetEditorModule extends ApiTargetModule {
43
+ readonly importsFrom?: string;
44
+ readonly mapType?: string;
45
+ }
46
+
31
47
  // A target sourced from a resolved ref-doc zip (resolved on demand, never
32
48
  // pre-baked) versus the committed-fixture default (`null`).
33
49
  export type ApiTargetSource = { readonly kind: "ref-doc"; readonly version: string } | null;
@@ -40,6 +56,11 @@ export interface ApiTarget {
40
56
  readonly coreTypesImport: string;
41
57
  readonly source?: ApiTargetSource;
42
58
  readonly modules: readonly ApiTargetModule[];
59
+ // The editor-scripting documents this target ships, or absent when it ships
60
+ // none. Absence is declared, never inferred from a missing file, so a fixture
61
+ // deleted by accident fails loudly instead of silently degrading a pinned
62
+ // project to the default target's editor surface.
63
+ readonly editorModules?: readonly ApiTargetEditorModule[];
43
64
  // Docs-only Lua stdlib pages (no generated `.d.ts`): vendored fixtures the
44
65
  // docs-site pages under the "Lua standard library" category. Never read by
45
66
  // regen/MODULE_MANIFEST; surfaced here so the registry stays type-honest.
@@ -49,7 +70,10 @@ export interface ApiTarget {
49
70
  const REGISTRY_PATH = resolve(import.meta.dir, "..", "api-targets.json");
50
71
  const PACKAGE_ROOT = resolve(import.meta.dir, "..");
51
72
 
52
- export function loadApiTargets(registryPath: string = REGISTRY_PATH): ApiTarget[] {
73
+ export function loadApiTargets(
74
+ registryPath: string = REGISTRY_PATH,
75
+ packageRoot: string = resolve(registryPath, ".."),
76
+ ): ApiTarget[] {
53
77
  const { targets } = JSON.parse(readFileSync(registryPath, "utf8")) as { targets: ApiTarget[] };
54
78
  const defaults = targets.filter((t) => t.default === true);
55
79
  if (defaults.length !== 1) {
@@ -57,6 +81,16 @@ export function loadApiTargets(registryPath: string = REGISTRY_PATH): ApiTarget[
57
81
  `api-targets.json: expected exactly one default target, found ${defaults.length}`,
58
82
  );
59
83
  }
84
+ for (const target of targets) {
85
+ for (const module of target.editorModules ?? []) {
86
+ const path = resolve(packageRoot, target.fixturesDir, module.fixture);
87
+ if (!existsSync(path)) {
88
+ throw new Error(
89
+ `api-targets.json: target "${target.id}" editor module "${module.namespace}" fixture not found: ${path}`,
90
+ );
91
+ }
92
+ }
93
+ }
60
94
  return targets;
61
95
  }
62
96
 
@@ -88,10 +122,18 @@ export interface ModuleManifestEntry {
88
122
  readonly namespace: string;
89
123
  readonly doc: unknown;
90
124
  readonly outFile: string;
125
+ // Each item drops one member of any element kind — a FUNCTION or a VARIABLE —
126
+ // by an exact stripped local (`get`), or — when it ends in `.` — a segment
127
+ // prefix dropping everything beneath it (`ui.`). The field keeps its historic
128
+ // name; its reach is not limited to functions.
91
129
  readonly skipFunctions?: readonly string[];
92
130
  readonly importsFrom?: string;
93
131
  readonly moduleId?: string;
94
132
  readonly sourceProvenance?: DocSourceProvenance;
133
+ // Overrides the shared token -> TS type mapping for this entry alone. Reserved
134
+ // for tokens no runtime namespace uses, so `DEFOLD_TYPE_MAP` keeps describing
135
+ // only the runtime surface.
136
+ readonly mapType?: (token: string) => string;
95
137
  }
96
138
 
97
139
  export interface ResolveTargetOptions {
@@ -157,6 +199,152 @@ const FIDELITY_BASELINE_TARGET = DEFAULT_TARGET;
157
199
  export const FIDELITY_BASELINE_MANIFEST: readonly ModuleManifestEntry[] =
158
200
  loadTargetModules(FIDELITY_BASELINE_TARGET);
159
201
 
202
+ // Handle tokens the editor VM alone exposes. `transaction_step[` is what
203
+ // upstream literally emits for `transaction_step[]` — the ref-doc's own type
204
+ // string loses the closing bracket — so it is repaired here, per entry, rather
205
+ // than in the parser: no runtime namespace carries it, and normalizing a
206
+ // truncated bracket globally would silently reinterpret any future token of that
207
+ // shape. The doc's other truncated token, `string[`, is deliberately left to map
208
+ // to `unknown`: it covers both a plain path list and `create_resources`' mixed
209
+ // path/[path, content] entries, so `string[]` would be wrong at one of its two
210
+ // slots.
211
+ const EDITOR_TYPE_MAP: Readonly<Record<string, string>> = {
212
+ // `editor.command` — the token's only consumer today — is skipped in favour of
213
+ // the hand-authored overload file, which returns the same `Opaque<"command">`.
214
+ // The entry stays so a future `command`-returning function maps identically.
215
+ command: 'Opaque<"command">',
216
+ // The handle every `editor.ui.*` builder returns and `editor.ui.show_dialog`
217
+ // consumes. Branding it keeps the whole component chain one nominal type
218
+ // rather than letting each end fall to the default `unknown` mapping.
219
+ component: 'Opaque<"component">',
220
+ transaction_step: 'Opaque<"transaction_step">',
221
+ "transaction_step[": 'Opaque<"transaction_step">[]',
222
+ // The handle every `localization` function returns: a userdata whose only
223
+ // documented use is being stringified or nested into another pattern.
224
+ message: 'Opaque<"message">',
225
+ // Repaired per entry for the same reason as `transaction_step[` above. Unlike
226
+ // `string[`, this token means one thing everywhere it appears — a list of
227
+ // values to render — so `unknown[]` is sound at all three of its slots.
228
+ "any[": "unknown[]",
229
+ };
230
+
231
+ function mapEditorType(token: string): string {
232
+ return EDITOR_TYPE_MAP[token] ?? defaultMapType(token);
233
+ }
234
+
235
+ // The named type maps an `editorModules` entry may select. A closed set, so an
236
+ // unknown selector fails loudly instead of silently falling back to the runtime
237
+ // token mapping and emitting `unknown` for every editor handle.
238
+ const NAMED_TYPE_MAPS: Readonly<Record<string, (token: string) => string>> = {
239
+ editor: mapEditorType,
240
+ };
241
+
242
+ // The subdirectory the namespace-shaped libraries the editor VM exposes
243
+ // alongside `editor` emit into, rather than beside the runtime modules, for a
244
+ // hard reason: `tsconfig.json` includes the flat glob `generated/*.d.ts`, so an
245
+ // editor `http.d.ts` there would share a program with the runtime one and
246
+ // declare `namespace http` twice. A subdirectory keeps them out of that glob,
247
+ // the same escape `generated/versions/` and `generated/kinds/` already use.
248
+ // Because that boundary is forced, it also discriminates the two halves of a
249
+ // target's declaration: the `editor` namespace a kind index names directly, and
250
+ // the VM libraries it must import by explicit path.
251
+ export const EDITOR_VM_SUBDIR = "editor-vm/";
252
+
253
+ export function isEditorVmModule(module: { readonly outFile: string }): boolean {
254
+ return module.outFile.startsWith(EDITOR_VM_SUBDIR);
255
+ }
256
+
257
+ function oneLevelDeeper(importPath: string): string {
258
+ return importPath.startsWith("./") ? `../${importPath.slice(2)}` : `../${importPath}`;
259
+ }
260
+
261
+ // The editor-scripting surface a target declares. Vendored and emitted through
262
+ // the same pipeline as MODULE_MANIFEST but deliberately separate from it:
263
+ // MODULE_MANIFEST drives every runtime kind's universal imports, the per-version
264
+ // targets and the published API artifacts, none of which describe the editor VM.
265
+ // Reached only through the `editor-script` kind index.
266
+ //
267
+ // The `skipFunctions` rules a target declares in `api-targets.json` withhold
268
+ // members for reasons the registry data alone cannot state. Rules are local
269
+ // names — the `<namespace>.` prefix is stripped before matching — and they
270
+ // withhold VARIABLEs as well as FUNCTIONs.
271
+ //
272
+ // On the `editor` entry, the editor VM's own `http`/`json`/`zip`/`zlib`/
273
+ // `pprint`/`localization`/`tilemap.tiles` sit in that same upstream document
274
+ // under their own top-level namespaces. They are emitted from their own
275
+ // per-namespace fixtures instead, and skipped on `editor` so that entry cannot
276
+ // misname them as `editor.*` — `pprint`, a flat identifier, would otherwise land
277
+ // as `editor.pprint`. `command` is the one rule there that is not a dropped
278
+ // namespace prefix: the hand-authored `src/editor-overloads.d.ts` supplies
279
+ // `editor.command` with a generic signature that couples a command's opts bag to
280
+ // its own query, which the emitter cannot express.
281
+ //
282
+ // On a VM entry, the withheld members are hand-authored in
283
+ // `src/editor-vm-globals.d.ts` instead. The functions are ones whose vendored
284
+ // signature the emitter cannot render soundly, both causes being the fixture's
285
+ // positional model being a lossy description of the Lua function its own
286
+ // `examples` block calls: an optional parameter sitting *before* a required one
287
+ // (TypeScript cannot mark a middle parameter `?`, so the emit renders it
288
+ // `T | undefined` and rejects every documented short form), and an empty
289
+ // `returnvalues` on a function upstream's own prose says returns a value.
290
+ //
291
+ // The constant tables are expressible now that the nested pass reaches
292
+ // variables, but a VARIABLE carries no `types`, so the emit would be `unknown` —
293
+ // and `ZipPackOptions.method?: string` rejects that. Their brief is the literal
294
+ // value, which looks like a string-literal type until you notice upstream's own
295
+ // `zip.ON_CONFLICT.OVERWRITE` reads `"skip"`, so no type is derived from it.
296
+ // The hand-authored declarations stay authoritative and these stay withheld.
297
+ export function loadTargetEditorModules(
298
+ target: ApiTarget,
299
+ packageRoot: string = PACKAGE_ROOT,
300
+ ): ModuleManifestEntry[] {
301
+ return (target.editorModules ?? []).map((module) => {
302
+ const path = resolve(packageRoot, target.fixturesDir, module.fixture);
303
+ let raw: string;
304
+ try {
305
+ raw = readFileSync(path, "utf8");
306
+ } catch {
307
+ throw new Error(
308
+ `api-targets.json: target "${target.id}" editor module "${module.namespace}" fixture not found: ${path}`,
309
+ );
310
+ }
311
+ if (module.mapType !== undefined && NAMED_TYPE_MAPS[module.mapType] === undefined) {
312
+ throw new Error(
313
+ `api-targets.json: target "${target.id}" editor module "${module.namespace}": unknown mapType "${module.mapType}"`,
314
+ );
315
+ }
316
+ const entry: ModuleManifestEntry = {
317
+ namespace: module.namespace,
318
+ doc: JSON.parse(raw),
319
+ outFile: module.outFile,
320
+ // A VM module sits one directory below the rest of the surface, so it
321
+ // cannot ride the target's own `coreTypesImport` unchanged. Deriving the
322
+ // deeper form keeps a relocated surface (a materialized `.defold-types/`
323
+ // copy rewrites `coreTypesImport` to `./core-types`) resolving too.
324
+ importsFrom:
325
+ module.importsFrom ??
326
+ (isEditorVmModule(module)
327
+ ? oneLevelDeeper(target.coreTypesImport)
328
+ : target.coreTypesImport),
329
+ ...(module.skipFunctions ? { skipFunctions: module.skipFunctions } : {}),
330
+ ...(module.mapType ? { mapType: NAMED_TYPE_MAPS[module.mapType] } : {}),
331
+ };
332
+ return entry;
333
+ });
334
+ }
335
+
336
+ const DEFAULT_EDITOR_MODULES = loadTargetEditorModules(DEFAULT_TARGET);
337
+
338
+ export const EDITOR_MODULE_MANIFEST: readonly ModuleManifestEntry[] = DEFAULT_EDITOR_MODULES.filter(
339
+ (entry) => !isEditorVmModule(entry),
340
+ );
341
+
342
+ export const EDITOR_VM_MODULE_MANIFEST: readonly ModuleManifestEntry[] =
343
+ DEFAULT_EDITOR_MODULES.filter(isEditorVmModule);
344
+
345
+ export const EDITOR_SKIP_FUNCTIONS: readonly string[] =
346
+ EDITOR_MODULE_MANIFEST.find((entry) => entry.namespace === "editor")?.skipFunctions ?? [];
347
+
160
348
  export interface MessagesManifestEntry {
161
349
  readonly doc: unknown;
162
350
  readonly outFile: string;
@@ -194,15 +382,34 @@ export function collectConstantFqns(
194
382
  return fqns;
195
383
  }
196
384
 
385
+ const URL_PARAMETERS_PATH = resolve(PACKAGE_ROOT, "url-parameters.json");
386
+
387
+ // The committed classification table, read once. Both the `.d.ts` emit and the
388
+ // authoritative-signature emit default to it, so the two surfaces cannot
389
+ // disagree about which slots address the scene graph.
390
+ export function loadUrlParameters(path: string = URL_PARAMETERS_PATH): UrlParameterTable {
391
+ return JSON.parse(readFileSync(path, "utf8")) as UrlParameterTable;
392
+ }
393
+
394
+ let urlParametersCache: UrlParameterTable | null = null;
395
+
396
+ function committedUrlParameters(): UrlParameterTable {
397
+ urlParametersCache ??= loadUrlParameters();
398
+ return urlParametersCache;
399
+ }
400
+
197
401
  export interface GenerateOptions {
198
402
  knownConstantFqns?: ReadonlySet<string>;
199
403
  translations?: TranslationStore;
404
+ urlParameters?: UrlParameterTable;
200
405
  }
201
406
 
202
407
  interface PreparedGeneratedModule {
203
408
  module: ApiModule;
204
409
  knownConstantFqns: ReadonlySet<string>;
205
410
  translations: TranslationStore;
411
+ urlParameters: UrlParameterTable;
412
+ mapType: ((token: string) => string) | undefined;
206
413
  dropped: string[];
207
414
  }
208
415
 
@@ -217,29 +424,42 @@ function prepareGeneratedModule(
217
424
  const module = parseDefoldApiDoc(entry.doc);
218
425
  const prefix = `${module.namespace}.`;
219
426
  const dropped: string[] = [];
220
- const skip = new Set(entry.skipFunctions ?? []);
221
- module.functions = module.functions.filter((fn) => {
222
- const local = fn.name.startsWith(prefix) ? fn.name.slice(prefix.length) : fn.name;
223
- if (skip.has(local)) {
224
- dropped.push(fn.name);
225
- return false;
226
- }
427
+ const rules = entry.skipFunctions ?? [];
428
+ const exact = new Set(rules.filter((rule) => !rule.endsWith(".")));
429
+ const segments = rules.filter((rule) => rule.endsWith("."));
430
+ const withheld = (name: string): boolean => {
431
+ const local = name.startsWith(prefix) ? name.slice(prefix.length) : name;
432
+ if (!exact.has(local) && !segments.some((segment) => local.startsWith(segment))) return false;
433
+ dropped.push(name);
227
434
  return true;
228
- });
435
+ };
436
+ module.functions = module.functions.filter((fn) => !withheld(fn.name));
437
+ module.variables = module.variables.filter((v) => !withheld(v.name));
229
438
  const knownConstantFqns = options?.knownConstantFqns ?? collectConstantFqns();
230
439
  const translations = options?.translations ?? loadTranslations();
231
- return { module, knownConstantFqns, translations, dropped };
440
+ const urlParameters = options?.urlParameters ?? committedUrlParameters();
441
+ return {
442
+ module,
443
+ knownConstantFqns,
444
+ translations,
445
+ urlParameters,
446
+ mapType: entry.mapType,
447
+ dropped,
448
+ };
232
449
  }
233
450
 
234
451
  export function generateModuleDeclaration(
235
452
  entry: ModuleManifestEntry,
236
453
  options?: GenerateOptions,
237
454
  ): GenerateResult {
238
- const { module, knownConstantFqns, translations, dropped } = prepareGeneratedModule(
239
- entry,
240
- options,
241
- );
242
- const emitted = emitDeclarations(module, { knownConstantFqns, translations });
455
+ const { module, knownConstantFqns, translations, urlParameters, mapType, dropped } =
456
+ prepareGeneratedModule(entry, options);
457
+ const emitted = emitDeclarations(module, {
458
+ knownConstantFqns,
459
+ translations,
460
+ urlParameters,
461
+ ...(mapType ? { mapType } : {}),
462
+ });
243
463
  const importsFrom = entry.importsFrom ?? "../src/core-types";
244
464
  const contents = entry.moduleId
245
465
  ? wrapAsModule({ namespace: module.namespace, emitted, importsFrom, moduleId: entry.moduleId })
@@ -255,23 +475,50 @@ export function generateModuleSignatures(
255
475
  entry: ModuleManifestEntry,
256
476
  options?: GenerateOptions,
257
477
  ): SymbolSignature[] {
258
- const { module, knownConstantFqns } = prepareGeneratedModule(entry, options);
259
- return emitSymbolSignatures(module, { knownConstantFqns });
478
+ const { module, knownConstantFqns, urlParameters, mapType } = prepareGeneratedModule(
479
+ entry,
480
+ options,
481
+ );
482
+ return emitSymbolSignatures(module, {
483
+ knownConstantFqns,
484
+ urlParameters,
485
+ ...(mapType ? { mapType } : {}),
486
+ });
260
487
  }
261
488
 
262
489
  export interface VersionedModuleManifestEntry extends ModuleManifestEntry {
263
490
  readonly versionId: string;
491
+ // Set on an entry derived from the target's `editorModules`. The editor
492
+ // surface rides the same per-version emit as the runtime modules but never the
493
+ // version's aggregate index — importing it there would drag the editor VM into
494
+ // every runtime program pinned to that version.
495
+ readonly editor?: boolean;
264
496
  }
265
497
 
266
498
  // Committed generation covers only filesystem-fixture targets (source == null).
267
499
  // ref-doc targets are resolved on the fly and never pre-baked, so they are
268
500
  // excluded from the committed regen loop and the byte-drift guards.
501
+ export function versionedModuleManifest(
502
+ targets: readonly ApiTarget[],
503
+ packageRoot: string = PACKAGE_ROOT,
504
+ ): VersionedModuleManifestEntry[] {
505
+ return targets
506
+ .filter((target) => target.default !== true && (target.source ?? null) == null)
507
+ .flatMap((target) => [
508
+ ...loadTargetModules(target, packageRoot).map((entry) => ({
509
+ ...entry,
510
+ versionId: target.id,
511
+ })),
512
+ ...loadTargetEditorModules(target, packageRoot).map((entry) => ({
513
+ ...entry,
514
+ versionId: target.id,
515
+ editor: true,
516
+ })),
517
+ ]);
518
+ }
519
+
269
520
  export const VERSIONED_MODULE_MANIFEST: readonly VersionedModuleManifestEntry[] =
270
- API_TARGETS.filter(
271
- (target) => target.default !== true && (target.source ?? null) == null,
272
- ).flatMap((target) =>
273
- loadTargetModules(target).map((entry) => ({ ...entry, versionId: target.id })),
274
- );
521
+ versionedModuleManifest(API_TARGETS);
275
522
 
276
523
  export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
277
524
  gui: "gui_script",
@@ -280,9 +527,18 @@ export const RESTRICTED_NAMESPACES: Readonly<Record<string, string>> = {
280
527
 
281
528
  // The Lua standard library rides every per-kind subpath the same as the full
282
529
  // entrypoint. Triple-slash directives must precede the first statement, so they
283
- // lead the generated kind index.
284
- export const LUA_STDLIB_REFERENCES =
285
- '/// <reference types="lua-types/5.1" />\n/// <reference types="lua-types/special/jit-only" />\n';
530
+ // lead the generated kind index. The two lines select independently: the game
531
+ // runtime is LuaJIT, but the editor VM is plain Lua 5.1 and has no `bit`.
532
+ const LUA_51_REFERENCE = '/// <reference types="lua-types/5.1" />\n';
533
+ const LUA_JIT_ONLY_REFERENCE = '/// <reference types="lua-types/special/jit-only" />\n';
534
+ export const LUA_STDLIB_REFERENCES = `${LUA_51_REFERENCE}${LUA_JIT_ONLY_REFERENCE}`;
535
+
536
+ // The stdlib references a kind's own VM earns. Shared with the materialized
537
+ // renderer so a surface generated on the fly makes the same LuaJIT call the
538
+ // committed emit does.
539
+ export function kindStdlibReferences(entry: KindManifestEntry): string {
540
+ return `${LUA_51_REFERENCE}${entry.jit === false ? "" : LUA_JIT_ONLY_REFERENCE}`;
541
+ }
286
542
 
287
543
  const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
288
544
  "../builtin-messages",
@@ -290,33 +546,109 @@ const UNIVERSAL_EXTRA_IMPORTS: readonly string[] = [
290
546
  "../../src/msg-overloads",
291
547
  "../../src/message-guard",
292
548
  "../../src/window-event-guard",
549
+ "../../src/scene-addresses",
293
550
  "../../src/go-overloads",
294
551
  "../../src/vmath-overloads",
295
552
  ];
296
553
 
554
+ const DEFAULT_FACTORY_MODULE = "../../src/lifecycle";
555
+
297
556
  export interface KindManifestEntry {
298
557
  readonly kind: string;
299
558
  readonly restricted?: string;
300
559
  readonly factory: string;
560
+ // Replaces the universal import set with exactly these generated modules. A
561
+ // kind that names one is disjoint from the runtime surface, not a narrowing
562
+ // of it, so it takes none of the universal extras either.
563
+ readonly only?: readonly string[];
564
+ // Hand-authored ambient modules the kind imports on top of `only`. A kind
565
+ // naming `only` takes none of `UNIVERSAL_EXTRA_IMPORTS`, so it cannot ride
566
+ // that set to reach an overload file.
567
+ readonly extraModules?: readonly string[];
568
+ // Extra value exports the factory module contributes to the kind subpath.
569
+ readonly extraExports?: readonly string[];
570
+ // Extra type-only exports the factory module contributes to the kind subpath.
571
+ readonly extraTypeExports?: readonly string[];
572
+ // Where the factory (and, when emitted, the script-property helper types) is
573
+ // re-exported from. Defaults to the runtime lifecycle module.
574
+ readonly factoryFrom?: string;
575
+ // Whether the kind has script properties at all. Editor scripts do not.
576
+ readonly propertyTypes?: boolean;
577
+ // Whether the kind's VM is LuaJIT. The editor runs plain Lua 5.1.
578
+ readonly jit?: boolean;
301
579
  }
302
580
 
303
581
  export const KIND_MODULE_MANIFEST: readonly KindManifestEntry[] = [
304
582
  { kind: "script", factory: "defineScript" },
305
583
  { kind: "gui-script", restricted: "gui", factory: "defineGuiScript" },
306
584
  { kind: "render-script", restricted: "render", factory: "defineRenderScript" },
585
+ {
586
+ kind: "editor-script",
587
+ // Empty on purpose: the emitted namespaces come from the declaring target's
588
+ // `editorModules` (see `editorKindModules`), and the empty array is what
589
+ // marks the kind as disjoint from the runtime surface rather than a
590
+ // narrowing of it.
591
+ only: [],
592
+ extraModules: ["../../src/editor-overloads", "../../src/editor-vm-globals"],
593
+ factory: "defineEditorScript",
594
+ extraExports: ["defineEditorCommand"],
595
+ extraTypeExports: ["EditorCommandQuery", "EditorNode"],
596
+ factoryFrom: "../../src/editor",
597
+ propertyTypes: false,
598
+ jit: false,
599
+ },
307
600
  ];
308
601
 
309
- export function generateKindIndex(kind: string): string {
602
+ // The kinds every materialized *versioned* surface carries, whatever the target.
603
+ // An `only` kind is built from the target's own `editorModules`, so it has a
604
+ // versioned form only for a target that declares an editor document — see
605
+ // `targetKindManifest`, which is what a per-target emit resolves through.
606
+ export const RUNTIME_KIND_MANIFEST: readonly KindManifestEntry[] = KIND_MODULE_MANIFEST.filter(
607
+ (entry) => entry.only === undefined,
608
+ );
609
+
610
+ // The emitted modules an editor kind index imports, read from the declaring
611
+ // target rather than from a list beside the kind entry, so a pinned surface
612
+ // names its own editor document instead of the default target's. The plain
613
+ // namespace leads and the VM libraries follow by explicit path — `only` holds
614
+ // namespaces and would build `../http` (the runtime module) and
615
+ // `../tilemap.tiles` (not a path) for those.
616
+ function editorKindModules(target: ApiTarget): string[] {
617
+ const declared = target.editorModules ?? [];
618
+ if (declared.length === 0) {
619
+ throw new Error(`api-targets.json: target "${target.id}" declares no editor document`);
620
+ }
621
+ return [
622
+ ...declared.filter((m) => !isEditorVmModule(m)),
623
+ ...declared.filter(isEditorVmModule),
624
+ ].map((m) => `../${m.outFile.replace(/\.d\.ts$/, "")}`);
625
+ }
626
+
627
+ export function generateKindIndex(kind: string, target: ApiTarget = DEFAULT_TARGET): string {
310
628
  const entry = KIND_MODULE_MANIFEST.find((e) => e.kind === kind);
311
629
  if (!entry) throw new Error(`unknown script kind: ${kind}`);
630
+ const srcPrefix = targetSrcPrefix(target);
312
631
  const universalNamespaces = MODULE_MANIFEST.filter(
313
632
  (m) => !Object.hasOwn(RESTRICTED_NAMESPACES, m.namespace),
314
633
  ).map((m) => `../${m.outFile.replace(/\.d\.ts$/, "")}`);
315
- const lines = [
316
- ...new Set([...universalNamespaces.sort(), ...[...UNIVERSAL_EXTRA_IMPORTS].sort()]),
317
- ].map((path) => `import "${path}";`);
634
+ const modules = (
635
+ entry.only === undefined
636
+ ? [...new Set([...universalNamespaces.sort(), ...[...UNIVERSAL_EXTRA_IMPORTS].sort()])]
637
+ : [...editorKindModules(target), ...(entry.extraModules ?? [])]
638
+ ).map((path) => retargetSrcPath(path, srcPrefix));
639
+ const lines = modules.map((path) => `import "${path}";`);
318
640
  if (entry.restricted) lines.push(`import "../${entry.restricted}";`);
319
- return `${LUA_STDLIB_REFERENCES}${lines.join("\n")}\n\nexport { ${entry.factory} } from "../../src/lifecycle";\nexport type { ScriptProperties, ScriptProperty } from "../../src/lifecycle";\n`;
641
+ const references = kindStdlibReferences(entry);
642
+ const from = retargetSrcPath(entry.factoryFrom ?? DEFAULT_FACTORY_MODULE, srcPrefix);
643
+ const values = [entry.factory, ...(entry.extraExports ?? [])].join(", ");
644
+ const typeExports = entry.extraTypeExports?.length
645
+ ? `\nexport type { ${entry.extraTypeExports.join(", ")} } from "${from}";`
646
+ : "";
647
+ const properties =
648
+ entry.propertyTypes === false
649
+ ? ""
650
+ : `\nexport type { ScriptProperties, ScriptProperty } from "${from}";`;
651
+ return `${references}${lines.join("\n")}\n\nexport { ${values} } from "${from}";${typeExports}${properties}\n`;
320
652
  }
321
653
 
322
654
  export function generateVersionIndex(
@@ -324,7 +656,7 @@ export function generateVersionIndex(
324
656
  manifest: readonly VersionedModuleManifestEntry[] = VERSIONED_MODULE_MANIFEST,
325
657
  ): string {
326
658
  const imports = manifest
327
- .filter((entry) => entry.versionId === versionId)
659
+ .filter((entry) => entry.versionId === versionId && entry.editor !== true)
328
660
  .map((entry) => entry.outFile.replace(/\.d\.ts$/, ""))
329
661
  .sort()
330
662
  .map((module) => `import "./${module}";`)
@@ -332,9 +664,40 @@ export function generateVersionIndex(
332
664
  return `${imports}\n\nexport {};\n`;
333
665
  }
334
666
 
667
+ // The `src/`-relative prefix a kind index living in `<generatedDir>/kinds/` needs
668
+ // to reach the package's hand-authored modules. The target's own
669
+ // `coreTypesImport` already measures its generated dir's depth; the kind index
670
+ // sits one level below it.
671
+ function targetSrcPrefix(target: ApiTarget): string {
672
+ return `../${target.coreTypesImport.slice(0, -"core-types".length)}`;
673
+ }
674
+
675
+ const DEFAULT_SRC_PREFIX = "../../src/";
676
+
677
+ function retargetSrcPath(modulePath: string, srcPrefix: string): string {
678
+ return modulePath.startsWith(DEFAULT_SRC_PREFIX)
679
+ ? `${srcPrefix}${modulePath.slice(DEFAULT_SRC_PREFIX.length)}`
680
+ : modulePath;
681
+ }
682
+
683
+ // The kinds a materialized surface for `target` can carry: the runtime trio
684
+ // always, plus the editor kind when — and only when — the target declares an
685
+ // editor document of its own.
686
+ export function targetKindManifest(target: ApiTarget): readonly KindManifestEntry[] {
687
+ if ((target.editorModules?.length ?? 0) === 0) {
688
+ return RUNTIME_KIND_MANIFEST;
689
+ }
690
+ return [...RUNTIME_KIND_MANIFEST, ...KIND_MODULE_MANIFEST.filter((e) => e.only !== undefined)];
691
+ }
692
+
335
693
  if (import.meta.main) {
336
694
  const generated = resolve(import.meta.dir, "..", "generated");
337
- for (const entry of MODULE_MANIFEST) {
695
+ mkdirSync(resolve(generated, "editor-vm"), { recursive: true });
696
+ for (const entry of [
697
+ ...MODULE_MANIFEST,
698
+ ...EDITOR_MODULE_MANIFEST,
699
+ ...EDITOR_VM_MODULE_MANIFEST,
700
+ ]) {
338
701
  const { contents, dropped } = generateModuleDeclaration(entry);
339
702
  if (dropped.length > 0) {
340
703
  console.log(`note: dropped skipped member(s) from ${entry.namespace}: ${dropped.join(", ")}`);
@@ -347,7 +710,17 @@ if (import.meta.main) {
347
710
  writeFileSync(messagesOut, generateBuiltinMessagesDeclaration(MESSAGES_MANIFEST));
348
711
  console.log(`wrote ${messagesOut}`);
349
712
 
350
- const versionIds = new Set(VERSIONED_MODULE_MANIFEST.map((entry) => entry.versionId));
713
+ // The target's own `generatedDir` is where a version lands, so a declaring
714
+ // target's editor surface (and its `editor-vm/` subdirectory) rides the same
715
+ // destination rule as its runtime modules rather than a second convention.
716
+ const versionedTargets = API_TARGETS.filter(
717
+ (target) => target.default !== true && (target.source ?? null) == null,
718
+ );
719
+ const versionDirOf = (versionId: string): string => {
720
+ const target = versionedTargets.find((t) => t.id === versionId);
721
+ if (!target) throw new Error(`no committed target for version ${versionId}`);
722
+ return resolve(PACKAGE_ROOT, target.generatedDir);
723
+ };
351
724
  for (const entry of VERSIONED_MODULE_MANIFEST) {
352
725
  const { contents, dropped } = generateModuleDeclaration(entry);
353
726
  if (dropped.length > 0) {
@@ -355,16 +728,26 @@ if (import.meta.main) {
355
728
  `note: dropped skipped member(s) from ${entry.versionId}/${entry.namespace}: ${dropped.join(", ")}`,
356
729
  );
357
730
  }
358
- const versionDir = resolve(generated, "versions", entry.versionId);
359
- mkdirSync(versionDir, { recursive: true });
360
- const out = resolve(versionDir, entry.outFile);
731
+ const out = resolve(versionDirOf(entry.versionId), entry.outFile);
732
+ mkdirSync(dirname(out), { recursive: true });
361
733
  writeFileSync(out, contents);
362
734
  console.log(`wrote ${out}`);
363
735
  }
364
- for (const versionId of versionIds) {
365
- const indexOut = resolve(generated, "versions", versionId, "index.d.ts");
366
- writeFileSync(indexOut, generateVersionIndex(versionId));
736
+ for (const target of versionedTargets) {
737
+ const indexOut = resolve(versionDirOf(target.id), "index.d.ts");
738
+ mkdirSync(dirname(indexOut), { recursive: true });
739
+ writeFileSync(indexOut, generateVersionIndex(target.id));
367
740
  console.log(`wrote ${indexOut}`);
741
+
742
+ const editorKinds = targetKindManifest(target).filter((entry) => entry.only !== undefined);
743
+ if (editorKinds.length === 0) continue;
744
+ const versionKindsDir = resolve(versionDirOf(target.id), "kinds");
745
+ mkdirSync(versionKindsDir, { recursive: true });
746
+ for (const entry of editorKinds) {
747
+ const out = resolve(versionKindsDir, `${entry.kind}.d.ts`);
748
+ writeFileSync(out, generateKindIndex(entry.kind, target));
749
+ console.log(`wrote ${out}`);
750
+ }
368
751
  }
369
752
 
370
753
  const kindsDir = resolve(generated, "kinds");