@defold-typescript/types 0.24.0 → 0.26.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.
Files changed (41) hide show
  1. package/api-availability.json +333 -306
  2. package/api-signatures.json +47 -46
  3. package/api-targets.json +66 -2
  4. package/generated/collectionproxy.d.ts +22 -0
  5. package/generated/editor-vm/http.d.ts +56 -0
  6. package/generated/editor-vm/image.d.ts +48 -0
  7. package/generated/editor-vm/json.d.ts +10 -0
  8. package/generated/editor-vm/localization.d.ts +42 -0
  9. package/generated/editor-vm/tilemap_tiles.d.ts +80 -0
  10. package/generated/editor-vm/zip.d.ts +10 -0
  11. package/generated/editor-vm/zlib.d.ts +24 -0
  12. package/generated/editor.d.ts +1111 -0
  13. package/generated/go.d.ts +22 -22
  14. package/generated/gui.d.ts +35 -6
  15. package/generated/kinds/editor-script.d.ts +14 -0
  16. package/generated/kinds/gui-script.d.ts +1 -0
  17. package/generated/kinds/render-script.d.ts +1 -0
  18. package/generated/kinds/script.d.ts +1 -0
  19. package/generated/material.d.ts +4 -0
  20. package/generated/versions/defold-1.12.4/go.d.ts +22 -22
  21. package/generated/versions/defold-1.12.4/gui.d.ts +43 -43
  22. package/index.d.ts +9 -0
  23. package/package.json +9 -1
  24. package/scripts/import-defold-release.ts +10 -0
  25. package/scripts/materialize-version.ts +54 -13
  26. package/scripts/regen.ts +425 -42
  27. package/scripts/sync-api-docs.ts +86 -9
  28. package/src/api-availability.ts +0 -0
  29. package/src/core-types.ts +15 -1
  30. package/src/editor-overloads.d.ts +33 -0
  31. package/src/editor-vm-globals.d.ts +200 -0
  32. package/src/editor-vm-types.ts +44 -0
  33. package/src/editor.ts +117 -16
  34. package/src/emit-dts.ts +298 -79
  35. package/src/engine-globals.d.ts +2 -2
  36. package/src/go-overloads.d.ts +6 -6
  37. package/src/index.ts +15 -0
  38. package/src/msg-overloads.d.ts +3 -3
  39. package/src/scene-addresses.d.ts +62 -0
  40. package/src/url-parameters.ts +174 -0
  41. package/url-parameters.json +285 -0
@@ -11,7 +11,7 @@ import { MODULE_MANIFEST } from "./regen";
11
11
  // model because the per-package `rootDir` boundary forbids a types-package
12
12
  // module importing outside its own tree; `scripts/release-model.test.ts`
13
13
  // correspondence-guards these against the model so drift fails CI.
14
- export const DEFOLD_VERSION = "1.13.0";
14
+ export const DEFOLD_VERSION = "1.13.1";
15
15
  export const refDocUrl = (version = DEFOLD_VERSION): string =>
16
16
  `https://github.com/defold/defold/releases/download/${version}/ref-doc.zip`;
17
17
 
@@ -24,6 +24,10 @@ export interface SyncManifestEntry {
24
24
  // docs (e.g. `sys`) is reassembled before parse, so the emitter still sees one
25
25
  // fixture, one namespace.
26
26
  readonly mergeEntries?: readonly string[];
27
+ // Extract only this namespace's elements from `zipEntry` and rewrite
28
+ // `info.namespace`. One upstream document that carries several top-level
29
+ // namespaces (the editor apidoc) becomes one fixture per namespace.
30
+ readonly split?: true;
27
31
  }
28
32
 
29
33
  // namespace -> ref-doc.zip entry. Entry paths do not match the namespace (gui ->
@@ -122,7 +126,6 @@ const LUA_NAMESPACE = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/;
122
126
  // `EMPTY_BY_UPSTREAM` allowlist shape). The upstream-coverage guard treats these
123
127
  // as covered. Adding a namespace here is a deliberate, reviewed act.
124
128
  export const IGNORED_UPSTREAM: ReadonlyMap<string, string> = new Map([
125
- ["editor", "editor-scripting API (editor.apidoc), not a runtime game namespace"],
126
129
  ["engine", "CLI/engine env doc, not a runtime Lua namespace"],
127
130
  [
128
131
  "builtins",
@@ -158,10 +161,51 @@ export const LUA_STDLIB_MANIFEST: readonly SyncManifestEntry[] = [
158
161
  entry("package", "doc/lua_package.doc_h_doc.json"),
159
162
  ];
160
163
 
164
+ // The editor-scripting API. It runs in the Defold *editor's* Lua VM, not the
165
+ // game runtime, so it is deliberately not a `MODULE_MANIFEST` citizen: that
166
+ // manifest drives every runtime kind's universal import set, the per-version
167
+ // targets, `api-availability.json` / `api-signatures.json` and the docs-site API
168
+ // pages, none of which describe the editor. It still syncs and emits through the
169
+ // same machinery (`EDITOR_MODULE_MANIFEST` in regen.ts writes
170
+ // `generated/editor.d.ts`), reached only through the `editor-script` kind index.
171
+ export const EDITOR_MANIFEST: readonly SyncManifestEntry[] = [
172
+ entry("editor", "doc/editor.apidoc_doc.json"),
173
+ ];
174
+
175
+ // The non-`editor.*` namespaces the same editor apidoc carries. Each is split
176
+ // out of that one document into its own fixture; the `editor` entry above keeps
177
+ // vendoring the document whole, so `editor_doc.json` is unaffected.
178
+ //
179
+ // Deliberately absent from `COVERAGE_MANIFEST`, `UPSTREAM_MAPPED_NAMESPACES`
180
+ // and `syncedDocs`: these exist only in the editor VM, so marking them mapped
181
+ // would tell the upstream-coverage guard the *runtime* covers `zip`, `pprint`
182
+ // and `tilemap.tiles` and silently suppress a future real miss. Fixture paths
183
+ // are explicit because the default would collide with the unrelated runtime
184
+ // `http_doc.json` / `json_doc.json` / `zlib_doc.json`.
185
+ export const EDITOR_VM_MANIFEST: readonly SyncManifestEntry[] = [
186
+ editorVm("http"),
187
+ editorVm("image"),
188
+ editorVm("json"),
189
+ editorVm("localization"),
190
+ editorVm("zip"),
191
+ editorVm("zlib"),
192
+ editorVm("pprint"),
193
+ editorVm("tilemap.tiles"),
194
+ ];
195
+
196
+ function editorVm(namespace: string): SyncManifestEntry {
197
+ return {
198
+ namespace,
199
+ zipEntry: "doc/editor.apidoc_doc.json",
200
+ fixture: `fixtures/defold-${DEFOLD_VERSION}/editor_${namespace.replace(/\./g, "_")}_doc.json`,
201
+ split: true,
202
+ };
203
+ }
204
+
161
205
  function entry(
162
206
  namespace: string,
163
207
  zipEntry: string,
164
- fixture: string = `fixtures/defold-1.13.0/${namespace.replace(/\./g, "_")}_doc.json`,
208
+ fixture: string = `fixtures/defold-1.13.1/${namespace.replace(/\./g, "_")}_doc.json`,
165
209
  mergeEntries?: readonly string[],
166
210
  ): SyncManifestEntry {
167
211
  return { namespace, zipEntry, fixture, ...(mergeEntries ? { mergeEntries } : {}) };
@@ -192,10 +236,25 @@ function ext(namespace: string, repo: string, tag: string, path: string): Extens
192
236
  repo,
193
237
  tag,
194
238
  path,
195
- fixture: `fixtures/defold-1.13.0/${namespace}_doc.json`,
239
+ fixture: `fixtures/defold-1.13.1/${namespace}_doc.json`,
196
240
  };
197
241
  }
198
242
 
243
+ // Every vendored fixture set the coverage report audits, and the namespaces it
244
+ // counts as reached from upstream. Composed once here so the `--check` run and
245
+ // the guard over it cannot disagree about which sets are in scope.
246
+ export const COVERAGE_MANIFEST: readonly { readonly namespace: string }[] = [
247
+ ...SYNC_MANIFEST,
248
+ ...EXTENSION_MANIFEST,
249
+ ...EDITOR_MANIFEST,
250
+ ];
251
+
252
+ export const UPSTREAM_MAPPED_NAMESPACES: ReadonlySet<string> = new Set(
253
+ [...SYNC_MANIFEST, ...EXTENSION_MANIFEST, ...LUA_STDLIB_MANIFEST, ...EDITOR_MANIFEST].map(
254
+ (e) => e.namespace,
255
+ ),
256
+ );
257
+
199
258
  export const extensionRawUrl = (e: ExtensionManifestEntry): string =>
200
259
  `https://raw.githubusercontent.com/${e.repo}/${e.tag}/${e.path}`;
201
260
 
@@ -328,6 +387,21 @@ function normalizedArguments(value: unknown): string {
328
387
  );
329
388
  }
330
389
 
390
+ // Segment-wise ownership: `pprint` owns `pprint` and `pprint.x` but never
391
+ // `pprintx`, and `tilemap.tiles` never claims `tilemap.set_tile`.
392
+ function ownsElement(name: string, namespace: string): boolean {
393
+ return name === namespace || name.startsWith(`${namespace}.`);
394
+ }
395
+
396
+ function splitNamespace(contents: string, namespace: string): string {
397
+ const doc = JSON.parse(contents);
398
+ const elements = (Array.isArray(doc.elements) ? doc.elements : []).filter(
399
+ (element: unknown) =>
400
+ isRecord(element) && typeof element.name === "string" && ownsElement(element.name, namespace),
401
+ );
402
+ return JSON.stringify({ ...doc, info: { ...doc.info, namespace }, elements });
403
+ }
404
+
331
405
  export function extractFixtures(
332
406
  zip: ZipAccessor,
333
407
  manifest: readonly SyncManifestEntry[] = SYNC_MANIFEST,
@@ -339,10 +413,11 @@ export function extractFixtures(
339
413
  throw new Error(`zip is missing entry ${source} for namespace ${item.namespace}`);
340
414
  }
341
415
  }
342
- const contents =
416
+ const merged =
343
417
  item.mergeEntries === undefined
344
418
  ? zip.read(item.zipEntry)
345
419
  : JSON.stringify(mergeApiDocs(sources.map((source) => JSON.parse(zip.read(source)))));
420
+ const contents = item.split ? splitNamespace(merged, item.namespace) : merged;
346
421
  return { namespace: item.namespace, fixture: item.fixture, contents };
347
422
  });
348
423
  }
@@ -602,24 +677,26 @@ if (import.meta.main) {
602
677
  const coreFixtures = extractFixtures(zip);
603
678
  const extensionFixtures = await downloadExtensionFixtures();
604
679
  const luaStdlibFixtures = extractFixtures(zip, LUA_STDLIB_MANIFEST);
680
+ const editorFixtures = extractFixtures(zip, EDITOR_MANIFEST);
681
+ const editorVmFixtures = extractFixtures(zip, EDITOR_VM_MANIFEST);
605
682
  const results = [
606
683
  ...syncExtractedFixtures(coreFixtures, { check }),
607
684
  ...syncExtractedFixtures(extensionFixtures, { check }),
608
685
  ...syncExtractedFixtures(luaStdlibFixtures, { check }),
686
+ ...syncExtractedFixtures(editorFixtures, { check }),
687
+ ...syncExtractedFixtures(editorVmFixtures, { check }),
609
688
  ];
610
689
  const syncedDocs = [...coreFixtures, ...extensionFixtures].map((f) => ({
611
690
  namespace: f.namespace,
612
691
  doc: JSON.parse(f.contents),
613
692
  }));
614
693
  const report = buildCoverageReport({
615
- manifest: [...SYNC_MANIFEST, ...EXTENSION_MANIFEST],
694
+ manifest: COVERAGE_MANIFEST,
616
695
  moduleManifest: MODULE_MANIFEST,
617
696
  unmapped: UNMAPPED,
618
697
  syncedDocs,
619
698
  upstream: collectUpstreamNamespaces(zip),
620
- upstreamMapped: new Set(
621
- [...SYNC_MANIFEST, ...EXTENSION_MANIFEST, ...LUA_STDLIB_MANIFEST].map((e) => e.namespace),
622
- ),
699
+ upstreamMapped: UPSTREAM_MAPPED_NAMESPACES,
623
700
  ignoredUpstream: IGNORED_UPSTREAM,
624
701
  });
625
702
  printReport(results, report, check);
Binary file
package/src/core-types.ts CHANGED
@@ -88,6 +88,7 @@ export interface Matrix4 {
88
88
  }
89
89
 
90
90
  declare const HashBrand: unique symbol;
91
+ declare const HashSource: unique symbol;
91
92
  /**
92
93
  * An opaque, branded handle to a *hashed name*: hold it and pass it back to the
93
94
  * engine API, but never inspect or construct it. Defold uses it in place of a
@@ -111,9 +112,22 @@ declare const HashBrand: unique symbol;
111
112
  * `hash_to_hex(h)` renders it as a hexadecimal string for logging, and `pprint`
112
113
  * shows it as `hash: [0x…]`. Two hashes are equal exactly when they name the
113
114
  * same thing, so a `Hash` is safe to compare, store, and use as a table key.
115
+ *
116
+ * The `S` parameter records the string `hash()` was called with, so a tool can
117
+ * *read* an address back out of `const SPRITE = hash("#sprite")`. It is not a
118
+ * checked constraint: it defaults to `string`, so every bare `Hash` keeps
119
+ * meaning what it always did, and its phantom member is optional, so `Hash` and
120
+ * `Hash<"#sprite">` stay assignable to each other in both directions. Nothing
121
+ * that compiles without it stops compiling with it.
114
122
  */
115
- export interface Hash {
123
+ export interface Hash<S extends string = string> {
116
124
  readonly [HashBrand]: "Hash";
125
+ // Method syntax, and load-bearing: a method's parameters are compared
126
+ // bivariantly, which is what keeps `Hash` and `Hash<"#sprite">` assignable in
127
+ // *both* directions. Written as a plain optional property (`?: S`) the
128
+ // parameter would be covariant, and passing a bare `Hash` where a sourced one
129
+ // is expected would start failing — the one thing this type must never do.
130
+ [HashSource]?(source: S): void;
117
131
  }
118
132
 
119
133
  declare const OpaqueBrand: unique symbol;
@@ -0,0 +1,33 @@
1
+ /** @noSelfInFile */
2
+
3
+ import type { Opaque } from "./core-types";
4
+ import type { EditorCommand, EditorCommandQuery } from "./editor";
5
+
6
+ declare global {
7
+ namespace editor {
8
+ /**
9
+ * Create an editor command
10
+ *
11
+ * @param opts - A table with the following keys:`label string, message`required, user-visible command name, either a string or a localization message`locations string[]`required, a non-empty list of locations where the command is displayed in the editor, values are either `"Edit"`, `"View"`, `"Project"`, `"Debug"` (the editor menubar), `"Assets"` (the assets pane), or `"Outline"` (the outline pane)`query table`optional, a query that both controls the command availability and provides additional information to the command handler functions; a table with the following keys:`selection table`current selection, a table with the following keys:`type string`either `"resource"` (selected resource) or `"outline"` (selected outline node)`cardinality string`either `"one"` (will use first selected item) or `"many"` (will use all selected items)`argument table`the command argument`id string`optional, keyword identifier that may be used for assigning a shortcut to a command; should be a dot-separated identifier string, e.g. `"my-extension.do-stuff"``active function`optional function that additionally checks if a command is active in the current context; will receive opts table with values populated by the query; should be fast to execute since the editor might invoke it in response to UI interactions (on key typed, mouse clicked)`run function`optional function that is invoked when the user decides to execute the command; will receive opts table with values populated by the query
12
+ * @example
13
+ * ```ts
14
+ * // Print Git history for a file:
15
+ * // (`locations` is added here: upstream's own example omits it, but its
16
+ * // prose declares the key required.)
17
+ * editor.command({
18
+ * label: "Git History",
19
+ * locations: ["Assets"],
20
+ * query: { selection: { type: "resource", cardinality: "one" } },
21
+ * run: (opts) => {
22
+ * editor.execute("git", "log", "--follow", `.${editor.get(opts.selection, "path")}`, {
23
+ * reload_resources: false,
24
+ * });
25
+ * },
26
+ * });
27
+ * ```
28
+ */
29
+ function command<const Q extends EditorCommandQuery = Record<never, never>>(
30
+ opts: EditorCommand<Q>,
31
+ ): Opaque<"command">;
32
+ }
33
+ }
@@ -0,0 +1,200 @@
1
+ /** @noSelfInFile */
2
+
3
+ import type {
4
+ HttpRouteHandler,
5
+ ZipEntries,
6
+ ZipPackOptions,
7
+ ZipUnpackOptions,
8
+ } from "./editor-vm-types";
9
+
10
+ // The editor VM surfaces the emit leaves behind, hand-authored and pinned to
11
+ // their vendored fixtures by `test/editor-vm-globals-parity.test.ts`. Three
12
+ // shapes land here: `pprint` is a bare global function with no namespace to hang
13
+ // off; the two-segment VARIABLEs (`zip.METHOD.*`, `http.server.*`) are reachable
14
+ // by the emitter's nested pass but deliberately withheld, because a VARIABLE
15
+ // carries no type and would emit as `unknown` — useless to `ZipPackOptions`,
16
+ // which types `method` as a string — while its brief is an unreliable literal
17
+ // (upstream's own `zip.ON_CONFLICT.OVERWRITE` reads `"skip"`); and the functions
18
+ // whose *vendored signature* cannot be rendered soundly — an optional parameter
19
+ // sitting before a required one, or an empty `returnvalues` on a function
20
+ // upstream's own prose says returns a value — are written out here as overload
21
+ // sets. All three are withheld from the emit by `EDITOR_VM_SKIP_FUNCTIONS`. The
22
+ // contract is unchanged and still derived: this file declares exactly what the
23
+ // emit leaves behind. The namespaces below merge with the emitted
24
+ // `generated/editor-vm/` bodies rather than replacing them. Upstream records no
25
+ // type for a VARIABLE, so those annotations are read from each member's prose.
26
+
27
+ declare global {
28
+ /**
29
+ * Pretty-print a Lua value
30
+ */
31
+ function pprint(value: unknown): void;
32
+
33
+ namespace http {
34
+ namespace server {
35
+ /**
36
+ * Editor's HTTP server local url
37
+ */
38
+ const local_url: string;
39
+ /**
40
+ * Editor's HTTP server port
41
+ */
42
+ const port: number;
43
+ /**
44
+ * Editor's HTTP server url
45
+ */
46
+ const url: string;
47
+ /**
48
+ * Create route definition for the editor's HTTP server
49
+ *
50
+ * `method`, `as` and `openapi` are optional but sit before the required
51
+ * `handler`, so each documented call shape is its own overload. An optional
52
+ * is supplied only when every earlier optional is: `method` and `as` are
53
+ * both strings, so a form that skipped `method` alone would be
54
+ * indistinguishable from one that supplied it.
55
+ *
56
+ * @param path - HTTP URI path, starts with `/`; may include path patterns (`{name}` for a single segment and `{*name}` for the rest of the request path) that will be extracted from the path and provided to the handler as a part of the request
57
+ * @param method - HTTP request method, default `"GET"`
58
+ * @param as - Request body converter, either `"string"` or `"json"`; the body will be discarded if not specified
59
+ * @param openapi - Optional OpenAPI Operation Object for this route method, exposed from `/openapi.json`
60
+ * @param handler - Request handler function, receives the request table and returns either a single response value, or 0 or more arguments to `http.server.response()`
61
+ * @returns HTTP server route
62
+ * @example
63
+ * ```ts
64
+ * // Receive JSON and respond with JSON:
65
+ * http.server.route("/json", "POST", "json", (request) => {
66
+ * pprint(request);
67
+ * return 200;
68
+ * });
69
+ *
70
+ * // Extract parts of the path:
71
+ * http.server.route("/users/{user}/orders", (request) => {
72
+ * print((request as { user: string }).user);
73
+ * });
74
+ * ```
75
+ */
76
+ function route(path: string, handler: HttpRouteHandler): unknown;
77
+ function route(path: string, method: string, handler: HttpRouteHandler): unknown;
78
+ function route(path: string, method: string, as: string, handler: HttpRouteHandler): unknown;
79
+ function route(
80
+ path: string,
81
+ method: string,
82
+ as: string,
83
+ openapi: Record<string | number, unknown>,
84
+ handler: HttpRouteHandler,
85
+ ): unknown;
86
+ }
87
+ }
88
+
89
+ namespace json {
90
+ /**
91
+ * Decode JSON string to Lua value
92
+ *
93
+ * Upstream records no return value while its own brief names one, so the
94
+ * return is written here. `unknown` is the honest answer: `options.all`
95
+ * makes the result an array, and a JSON document may be a scalar.
96
+ *
97
+ * @param json - json data
98
+ * @param options - A table with the following keys:`all boolean`if `true`, decodes all json values in a string and returns an array
99
+ * @returns the decoded value
100
+ */
101
+ function decode(json: string, options?: Record<string | number, unknown>): unknown;
102
+ /**
103
+ * Encode Lua value to JSON string
104
+ *
105
+ * @param value - any Lua value that may be represented as JSON
106
+ * @returns the encoded document
107
+ */
108
+ function encode(value: unknown): string;
109
+ }
110
+
111
+ namespace zip {
112
+ namespace METHOD {
113
+ /**
114
+ * `"deflated"` compression method
115
+ */
116
+ const DEFLATED: string;
117
+ /**
118
+ * `"stored"` compression method, i.e. no compression
119
+ */
120
+ const STORED: string;
121
+ }
122
+ namespace ON_CONFLICT {
123
+ /**
124
+ * `"error"`, any conflict aborts extraction
125
+ */
126
+ const ERROR: string;
127
+ /**
128
+ * `"skip"`, existing file is preserved
129
+ */
130
+ const SKIP: string;
131
+ /**
132
+ * `"skip"`, existing file is overwritten
133
+ */
134
+ const OVERWRITE: string;
135
+ }
136
+ /**
137
+ * Create a ZIP archive
138
+ *
139
+ * `opts` is optional but sits before the required `entries`, so the two
140
+ * documented call shapes are separate overloads.
141
+ *
142
+ * @param output_path - output zip file path, resolved against project root if relative
143
+ * @param opts - compression options
144
+ * @param entries - entries to compress, either a relative path or a list of entries
145
+ * @example
146
+ * ```ts
147
+ * // Archive a file and a folder:
148
+ * zip.pack("build.zip", ["build", "game.project"]);
149
+ *
150
+ * // Change the location of the files within the archive:
151
+ * zip.pack("build.zip", [
152
+ * ["build/wasm-web", "."],
153
+ * ["configs/prod.json", "config.json"],
154
+ * ]);
155
+ *
156
+ * // Create archive without compression:
157
+ * zip.pack("build.zip", { method: zip.METHOD.STORED }, ["build", "resources"]);
158
+ *
159
+ * // Don't compress one of the folders:
160
+ * zip.pack("build.zip", [{ 1: "assets", method: zip.METHOD.STORED }, "build/wasm-web"]);
161
+ * ```
162
+ */
163
+ function pack(output_path: string, entries: ZipEntries): void;
164
+ function pack(output_path: string, opts: ZipPackOptions, entries: ZipEntries): void;
165
+ /**
166
+ * Extract a ZIP archive
167
+ *
168
+ * `target_path`, `opts` and `paths` are all optional and share the second
169
+ * slot, so the shape-discriminated forms come first: an all-optional
170
+ * `ZipUnpackOptions` cannot absorb a paths array, and vice versa.
171
+ *
172
+ * @param archive_path - zip file path, resolved against project root if relative
173
+ * @param target_path - target path for extraction, defaults to parent of `archive_path` if omitted
174
+ * @param opts - extraction options
175
+ * @param paths - entries to extract, relative string paths
176
+ * @example
177
+ * ```ts
178
+ * // Extract everything next to the archive:
179
+ * zip.unpack("build/dev/resources.zip");
180
+ *
181
+ * // Extract to a different directory:
182
+ * zip.unpack("build/dev/resources.zip", "build/dev/tmp");
183
+ *
184
+ * // Extract while overwriting existing files on conflict:
185
+ * zip.unpack("build/dev/resources.zip", { on_conflict: zip.ON_CONFLICT.OVERWRITE });
186
+ *
187
+ * // Extract a single file:
188
+ * zip.unpack("build/dev/resources.zip", ["config.json"]);
189
+ * ```
190
+ */
191
+ function unpack(archive_path: string, paths: readonly string[]): void;
192
+ function unpack(archive_path: string, opts: ZipUnpackOptions, paths?: readonly string[]): void;
193
+ function unpack(
194
+ archive_path: string,
195
+ target_path?: string,
196
+ opts?: ZipUnpackOptions,
197
+ paths?: readonly string[],
198
+ ): void;
199
+ }
200
+ }
@@ -0,0 +1,44 @@
1
+ /** @noSelfInFile */
2
+
3
+ // The named types the hand-authored editor VM overloads take. They live outside
4
+ // `src/editor-vm-globals.d.ts` on purpose: the parity test compares that file's
5
+ // declared set to the set the emit leaves behind, exactly, and a type alias
6
+ // declared there would count as an extra declaration with no upstream member to
7
+ // answer for it.
8
+
9
+ /**
10
+ * One entry in a `zip.pack` archive: a source path, a `[source, target]` pair
11
+ * that relocates it inside the archive, or a table carrying per-entry
12
+ * compression options alongside those positions.
13
+ */
14
+ export type ZipEntry = string | readonly string[] | Record<string | number, unknown>;
15
+
16
+ /**
17
+ * What `zip.pack` compresses: a single relative path, or a list of entries.
18
+ */
19
+ export type ZipEntries = string | readonly ZipEntry[];
20
+
21
+ /**
22
+ * Archive-wide compression options for `zip.pack`.
23
+ */
24
+ export interface ZipPackOptions {
25
+ /** Compression method, either `zip.METHOD.DEFLATED` (default) or `zip.METHOD.STORED`. */
26
+ readonly method?: string;
27
+ /** Compression level, 0 to 9; only useful with `zip.METHOD.DEFLATED`, defaults to 6. */
28
+ readonly level?: number;
29
+ }
30
+
31
+ /**
32
+ * Extraction options for `zip.unpack`. Every key is optional, which is what lets
33
+ * TypeScript tell this table apart from the paths list that shares its slot.
34
+ */
35
+ export interface ZipUnpackOptions {
36
+ /** Conflict resolution strategy, defaults to `zip.ON_CONFLICT.ERROR`. */
37
+ readonly on_conflict?: string;
38
+ }
39
+
40
+ /**
41
+ * A `http.server.route` handler. Left at the looseness the emit gave it: typing
42
+ * the request table is a separate slice.
43
+ */
44
+ export type HttpRouteHandler = (...args: unknown[]) => unknown;
package/src/editor.ts CHANGED
@@ -3,36 +3,137 @@
3
3
  // makes them a fourth, disjoint script kind — lowered to a chunk-level
4
4
  // `return <hooks table>` rather than the runtime kinds' flat top-level globals.
5
5
  //
6
- // This is the keystone surface only. The full typed `editor.*` global
7
- // (`get`/`transact`/`command` + the editor-VM `http`/`json`/`zip`) and the
8
- // per-kind API walls are a later slice, so a command's `run`/`active` receive a
9
- // loosely-typed opts bag for now.
6
+ // The typed `editor.*` global (`get`/`transact`/`command`) and the walled
7
+ // `@defold-typescript/types/editor-script` entrypoint ship alongside this file,
8
+ // as do the editor VM's own `http`/`json`/`zip`/`zlib`/`tilemap.tiles`
9
+ // libraries and the `editor.ui.*`, `editor.prefs.*`, and `localization.*`
10
+ // surfaces.
11
+
12
+ import type { Opaque } from "./core-types";
13
+
14
+ /**
15
+ * What the editor addresses a node by: either a resource path (e.g.
16
+ * `"/main/game.script"`) or an internal node id the editor hands to the script.
17
+ * This is the argument `editor.get` and the `editor.tx.*` builders take.
18
+ */
19
+ export type EditorNode = string | Opaque<"userdata">;
20
+
21
+ /**
22
+ * Declares a command's context: what the editor resolves before calling the
23
+ * command's hooks, and therefore which members its opts bag carries. Both keys
24
+ * are optional; a command that declares neither receives an empty bag.
25
+ */
26
+ export interface EditorCommandQuery {
27
+ /**
28
+ * The current selection. `type` picks the selection source, `cardinality`
29
+ * picks between the first selected item and all of them.
30
+ */
31
+ selection?: { type: "resource" | "outline"; cardinality: "one" | "many" };
32
+ /** Requests the command argument. Declared as an empty table. */
33
+ argument?: Record<string, never>;
34
+ }
35
+
36
+ /**
37
+ * The opts bag the editor populates for a command whose query is `Q` — exactly
38
+ * the members `Q` declared, and no others.
39
+ *
40
+ * Each half branches on *key presence* (`"selection" extends keyof Q`) rather
41
+ * than on the property type: an optional property is still in `keyof`, so
42
+ * presence is what separates a declared query from the erased one. The
43
+ * cardinality test is wrapped in a one-tuple so a union (or `never`) does not
44
+ * distribute across the conditional.
45
+ */
46
+ export type EditorCommandOpts<Q extends EditorCommandQuery> = ("selection" extends keyof Q
47
+ ? {
48
+ selection: [NonNullable<Q["selection"]>["cardinality"]] extends ["many"]
49
+ ? EditorNode[]
50
+ : EditorNode;
51
+ }
52
+ : Record<never, never>) &
53
+ ("argument" extends keyof Q ? { argument: Record<string, unknown> } : Record<never, never>);
10
54
 
11
55
  /**
12
56
  * A single command an editor script contributes: a label, the editor UI
13
57
  * locations it appears in (e.g. `"Edit"`, `"Assets"`, `"Outline"`, `"View"`),
14
- * and optional `active`/`run` hooks the editor calls with a command-context bag.
58
+ * and optional `active`/`run` hooks the editor calls with the context bag its
59
+ * own `query` declared.
60
+ *
61
+ * The editor calls `active`/`run` as plain functions, so they must emit no
62
+ * leading self parameter. `@noSelf` has to sit here, on the enclosing
63
+ * interface: TSTL resolves a hook's self context from its contextual signature,
64
+ * and the property-signature branch consults only this interface — it returns
65
+ * before `noImplicitSelf` or a file-level `@noSelfInFile` is ever considered.
66
+ *
67
+ * @noSelf
15
68
  */
16
- export interface EditorCommand {
69
+ export interface EditorCommand<Q extends EditorCommandQuery = EditorCommandQuery> {
17
70
  /** Menu/label text shown for the command. */
18
71
  label: string;
19
72
  /** Editor UI locations the command is offered in. */
20
73
  locations: string[];
21
74
  /**
22
75
  * Declares the command's context arguments; the editor passes the resolved
23
- * values to `active`/`run`. Loosely typed until the `editor.*` slice lands.
76
+ * values to `active`/`run` as exactly the matching opts members.
24
77
  */
25
- query?: Record<string, unknown>;
78
+ query?: Q;
26
79
  /**
27
80
  * Called to decide whether the command is currently enabled. Omit to always
28
- * enable. The opts bag is loosely typed until the `editor.*` slice lands.
29
- */
30
- active?: (opts: Record<string, unknown>) => boolean;
31
- /**
32
- * Called when the command is invoked. The opts bag is loosely typed until the
33
- * `editor.*` slice lands.
81
+ * enable. Should be fast the editor may call it on key/mouse events.
34
82
  */
35
- run?: (opts: Record<string, unknown>) => void;
83
+ active?: (opts: EditorCommandOpts<Q>) => boolean;
84
+ /** Called when the user invokes the command. */
85
+ run?: (opts: EditorCommandOpts<Q>) => void;
86
+ }
87
+
88
+ /**
89
+ * A command as it sits in a `get_commands` list: the same shape with its query
90
+ * erased. The hooks take `never`, the one parameter type every concrete opts
91
+ * bag is assignable *from* under parameter contravariance — so a `"one"` and a
92
+ * `"many"` command coexist in a single array while each keeps its own precise
93
+ * typing at its `defineEditorCommand` call site.
94
+ */
95
+ export interface EditorCommandEntry {
96
+ /** Menu/label text shown for the command. */
97
+ label: string;
98
+ /** Editor UI locations the command is offered in. */
99
+ locations: string[];
100
+ /** Declares the command's context arguments. */
101
+ query?: EditorCommandQuery;
102
+ /** Called to decide whether the command is currently enabled. */
103
+ active?: (opts: never) => boolean;
104
+ /** Called when the user invokes the command. */
105
+ run?: (opts: never) => void;
106
+ }
107
+
108
+ /**
109
+ * Type a single editor command so its `active`/`run` hooks receive exactly the
110
+ * opts members its own `query` declared. At runtime this is an identity
111
+ * function — it returns `command` unchanged; its only job is typing, and the
112
+ * transpiler emits the table literal it wraps.
113
+ *
114
+ * @param command - the command to type and return.
115
+ * @returns the same `command`, typed as a query-erased list entry.
116
+ * @example
117
+ * ```ts
118
+ * export default defineEditorScript({
119
+ * get_commands: () => [
120
+ * defineEditorCommand({
121
+ * label: "Git History",
122
+ * locations: ["Assets"],
123
+ * query: { selection: { type: "resource", cardinality: "one" } },
124
+ * run: (opts) => print(editor.get(opts.selection, "path")),
125
+ * }),
126
+ * ],
127
+ * });
128
+ * ```
129
+ */
130
+ // The `Record<never, never>` default (not `Record<string, never>`, whose `keyof`
131
+ // is `string` and would match every presence test) gives a command that declares
132
+ // no query an empty opts bag.
133
+ export function defineEditorCommand<const Q extends EditorCommandQuery = Record<never, never>>(
134
+ command: EditorCommand<Q>,
135
+ ): EditorCommandEntry {
136
+ return command as EditorCommandEntry;
36
137
  }
37
138
 
38
139
  /**
@@ -41,7 +142,7 @@ export interface EditorCommand {
41
142
  */
42
143
  export interface EditorScriptModule {
43
144
  /** Returns the commands this script contributes to the editor. */
44
- get_commands?: () => EditorCommand[];
145
+ get_commands?: () => EditorCommandEntry[];
45
146
  /** Returns language-server descriptors this script contributes. */
46
147
  get_language_servers?: () => unknown[];
47
148
  }