@defold-typescript/transpiler 0.37.0 → 0.38.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/dist/index.js CHANGED
@@ -56,7 +56,7 @@ function resolvesToDispatchExport(callee, checker) {
56
56
  }
57
57
  return declaration.getSourceFile().fileName.includes(DISPATCH_MODULE);
58
58
  }
59
- function handlerName(property) {
59
+ function handlerName(property, checker) {
60
60
  const name = property.name;
61
61
  if (name === undefined) {
62
62
  return;
@@ -64,8 +64,29 @@ function handlerName(property) {
64
64
  if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
65
65
  return name.text;
66
66
  }
67
+ if (ts.isNumericLiteral(name)) {
68
+ return String(Number(name.text));
69
+ }
70
+ if (ts.isComputedPropertyName(name)) {
71
+ const type = checker.getTypeAtLocation(name.expression);
72
+ if (type.isStringLiteral() || type.isNumberLiteral()) {
73
+ return String(type.value);
74
+ }
75
+ }
67
76
  return;
68
77
  }
78
+ function unlowerableHandler(node, text) {
79
+ const file = node.getSourceFile();
80
+ const start = node.getStart(file);
81
+ return {
82
+ file,
83
+ start,
84
+ length: node.getEnd() - start,
85
+ category: ts.DiagnosticCategory.Error,
86
+ code: 0,
87
+ messageText: `${DISPATCH_NAME} cannot lower \`${text}\`: pass an object literal whose keys are an identifier, a quoted or numeric literal, or a computed literal constant, each with its handler written inline.`
88
+ };
89
+ }
69
90
  function handlerFunction(property) {
70
91
  if (ts.isMethodDeclaration(property) && property.body !== undefined) {
71
92
  return property;
@@ -104,8 +125,11 @@ function handlerBody(fn, context) {
104
125
  var messageDispatchLoweringPlugin = {
105
126
  visitors: {
106
127
  [ts.SyntaxKind.CallExpression]: (node, context) => {
128
+ if (!resolvesToDispatchExport(node.expression, context.checker)) {
129
+ return context.superTransformExpression(node);
130
+ }
107
131
  const [handlersArg] = node.arguments;
108
- if (node.arguments.length === 1 && handlersArg !== undefined && ts.isObjectLiteralExpression(handlersArg) && resolvesToDispatchExport(node.expression, context.checker)) {
132
+ if (node.arguments.length === 1 && handlersArg !== undefined && ts.isObjectLiteralExpression(handlersArg)) {
109
133
  let chain;
110
134
  const properties = [...handlersArg.properties];
111
135
  for (let i = properties.length - 1;i >= 0; i--) {
@@ -113,9 +137,11 @@ var messageDispatchLoweringPlugin = {
113
137
  if (property === undefined) {
114
138
  continue;
115
139
  }
116
- const name = handlerName(property);
140
+ const name = handlerName(property, context.checker);
117
141
  const fn = handlerFunction(property);
118
142
  if (name === undefined || fn === undefined) {
143
+ const offending = property.name ?? property;
144
+ context.diagnostics.push(unlowerableHandler(property, offending.getText()));
119
145
  continue;
120
146
  }
121
147
  const condition = createBinaryExpression(createIdentifier("message_id"), createCallExpression(createIdentifier("hash"), [createStringLiteral(name)]), SyntaxKind2.EqualityOperator);
@@ -130,6 +156,8 @@ var messageDispatchLoweringPlugin = {
130
156
  ];
131
157
  return createFunctionExpression(createBlock(chain === undefined ? [] : [chain]), params, undefined, NodeFlags.Declaration, node);
132
158
  }
159
+ const offending = handlersArg ?? node;
160
+ context.diagnostics.push(unlowerableHandler(offending, offending.getText()));
133
161
  return context.superTransformExpression(node);
134
162
  }
135
163
  }
@@ -2432,20 +2460,71 @@ function componentAddressesOf(documents, paths) {
2432
2460
  }
2433
2461
  return keys;
2434
2462
  }
2435
- function bodyOf(keys) {
2436
- if (keys.length === 0)
2463
+ function bareScriptsOf(documents, paths) {
2464
+ const agreed = new Map;
2465
+ const record = (id, resource) => {
2466
+ if (!agreed.has(id))
2467
+ agreed.set(id, resource);
2468
+ else if (agreed.get(id) !== resource)
2469
+ agreed.set(id, undefined);
2470
+ };
2471
+ for (const [path, ids] of paths.componentsOf) {
2472
+ const resources = paths.componentResourcesOf.get(path);
2473
+ for (const id of ids)
2474
+ record(id, resources?.get(id));
2475
+ }
2476
+ for (const [displayPath, text] of documents) {
2477
+ let declarations;
2478
+ try {
2479
+ declarations = collectComponentDeclarations(parseSceneTextFormat(text), displayPath, []);
2480
+ } catch (error) {
2481
+ if (!(error instanceof SceneTextFormatError))
2482
+ throw error;
2483
+ continue;
2484
+ }
2485
+ for (const id of declarations.ids)
2486
+ record(id, declarations.resources.get(id));
2487
+ }
2488
+ return agreed;
2489
+ }
2490
+ function componentValuesOf(documents, paths, scriptModules) {
2491
+ const values = new Map;
2492
+ const moduleOf = (resource) => resource === undefined ? undefined : scriptModules.get(resource);
2493
+ const keys = componentAddressesOf(documents, paths);
2494
+ if (scriptModules.size === 0) {
2495
+ for (const key of keys)
2496
+ values.set(key, undefined);
2497
+ return values;
2498
+ }
2499
+ const bare = bareScriptsOf(documents, paths);
2500
+ for (const key of keys) {
2501
+ const hash = key.indexOf("#");
2502
+ const path = key.slice(0, hash);
2503
+ const id = key.slice(hash + 1);
2504
+ values.set(key, moduleOf(path === "" ? bare.get(id) : paths.componentResourcesOf.get(path)?.get(id)));
2505
+ }
2506
+ return values;
2507
+ }
2508
+ function bodyOf(values) {
2509
+ if (values.size === 0)
2437
2510
  return "{}";
2438
- const members = keys.map((key) => ` ${JSON.stringify(key)}: true;
2439
- `).sort().join("");
2511
+ const members = [...values].map(([key, specifier]) => {
2512
+ const value = specifier === undefined ? "true" : `typeof import(${JSON.stringify(specifier)}).default`;
2513
+ return ` ${JSON.stringify(key)}: ${value};
2514
+ `;
2515
+ }).sort().join("");
2440
2516
  return `{
2441
2517
  ${members} }`;
2442
2518
  }
2443
- function buildSceneAddressDeclaration(documents, roles) {
2519
+ function trueValues(keys) {
2520
+ return new Map([...keys].map((key) => [key, undefined]));
2521
+ }
2522
+ function buildSceneAddressDeclaration(documents, roles, scriptModules = new Map) {
2444
2523
  const index = buildSceneObjectPathIndex(documents, roles);
2445
- const components = componentAddressesOf(documents, index);
2524
+ const components = componentValuesOf(documents, index, scriptModules);
2446
2525
  return `${BANNER}
2447
2526
  declare global {
2448
- interface SceneGameObjectAddresses ${bodyOf([...index.paths])}
2527
+ interface SceneGameObjectAddresses ${bodyOf(trueValues(index.paths))}
2449
2528
 
2450
2529
  interface SceneComponentAddresses ${bodyOf(components)}
2451
2530
  }
@@ -10,5 +10,11 @@ import type { SceneCollectionRoles } from "./scene-collection-roles";
10
10
  * byte-identical file, and a document map built in a different order emits the
11
11
  * same bytes as well; a `Set` iteration order leaking into the output would
12
12
  * make every regeneration rewrite the file and re-check the whole program.
13
+ *
14
+ * `scriptModules` maps a script resource a scene names to the import specifier
15
+ * of the program source that compiles to it, relative to the declaration file.
16
+ * A component address whose resource maps is written as that module's default
17
+ * export, so `msg.post` can read the script's messages off it; every other
18
+ * address stays `true`. Omitted, every address is `true`.
13
19
  */
14
- export declare function buildSceneAddressDeclaration(documents: ReadonlyMap<string, string>, roles: SceneCollectionRoles): string;
20
+ export declare function buildSceneAddressDeclaration(documents: ReadonlyMap<string, string>, roles: SceneCollectionRoles, scriptModules?: ReadonlyMap<string, string>): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@defold-typescript/types": "0.37.0",
38
+ "@defold-typescript/types": "0.38.0",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript-to-lua": "^1.36.0"
41
41
  }
@@ -47,7 +47,10 @@ type HandlerFunction = (ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowF
47
47
  body: ts.ConciseBody;
48
48
  };
49
49
 
50
- function handlerName(property: ts.ObjectLiteralElementLike): string | undefined {
50
+ function handlerName(
51
+ property: ts.ObjectLiteralElementLike,
52
+ checker: ts.TypeChecker,
53
+ ): string | undefined {
51
54
  const name = property.name;
52
55
  if (name === undefined) {
53
56
  return undefined;
@@ -55,9 +58,31 @@ function handlerName(property: ts.ObjectLiteralElementLike): string | undefined
55
58
  if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
56
59
  return name.text;
57
60
  }
61
+ if (ts.isNumericLiteral(name)) {
62
+ return String(Number(name.text));
63
+ }
64
+ if (ts.isComputedPropertyName(name)) {
65
+ const type = checker.getTypeAtLocation(name.expression);
66
+ if (type.isStringLiteral() || type.isNumberLiteral()) {
67
+ return String(type.value);
68
+ }
69
+ }
58
70
  return undefined;
59
71
  }
60
72
 
73
+ function unlowerableHandler(node: ts.Node, text: string): ts.Diagnostic {
74
+ const file = node.getSourceFile();
75
+ const start = node.getStart(file);
76
+ return {
77
+ file,
78
+ start,
79
+ length: node.getEnd() - start,
80
+ category: ts.DiagnosticCategory.Error,
81
+ code: 0,
82
+ messageText: `${DISPATCH_NAME} cannot lower \`${text}\`: pass an object literal whose keys are an identifier, a quoted or numeric literal, or a computed literal constant, each with its handler written inline.`,
83
+ };
84
+ }
85
+
61
86
  function handlerFunction(property: ts.ObjectLiteralElementLike): HandlerFunction | undefined {
62
87
  if (ts.isMethodDeclaration(property) && property.body !== undefined) {
63
88
  return property as HandlerFunction;
@@ -108,12 +133,14 @@ function handlerBody(fn: HandlerFunction, context: TransformationContext): State
108
133
  export const messageDispatchLoweringPlugin: Plugin = {
109
134
  visitors: {
110
135
  [ts.SyntaxKind.CallExpression]: (node, context): Expression => {
136
+ if (!resolvesToDispatchExport(node.expression, context.checker)) {
137
+ return context.superTransformExpression(node);
138
+ }
111
139
  const [handlersArg] = node.arguments;
112
140
  if (
113
141
  node.arguments.length === 1 &&
114
142
  handlersArg !== undefined &&
115
- ts.isObjectLiteralExpression(handlersArg) &&
116
- resolvesToDispatchExport(node.expression, context.checker)
143
+ ts.isObjectLiteralExpression(handlersArg)
117
144
  ) {
118
145
  let chain: IfStatement | undefined;
119
146
  const properties = [...handlersArg.properties];
@@ -122,9 +149,11 @@ export const messageDispatchLoweringPlugin: Plugin = {
122
149
  if (property === undefined) {
123
150
  continue;
124
151
  }
125
- const name = handlerName(property);
152
+ const name = handlerName(property, context.checker);
126
153
  const fn = handlerFunction(property);
127
154
  if (name === undefined || fn === undefined) {
155
+ const offending = property.name ?? property;
156
+ context.diagnostics.push(unlowerableHandler(property, offending.getText()));
128
157
  continue;
129
158
  }
130
159
  const condition = createBinaryExpression(
@@ -149,6 +178,8 @@ export const messageDispatchLoweringPlugin: Plugin = {
149
178
  node,
150
179
  );
151
180
  }
181
+ const offending = handlersArg ?? node;
182
+ context.diagnostics.push(unlowerableHandler(offending, offending.getText()));
152
183
  return context.superTransformExpression(node);
153
184
  },
154
185
  },
@@ -1,6 +1,7 @@
1
1
  import type { SceneCollectionRoles } from "./scene-collection-roles";
2
- import { buildSceneComponentIndex } from "./scene-component-index";
2
+ import { buildSceneComponentIndex, collectComponentDeclarations } from "./scene-component-index";
3
3
  import { buildSceneObjectPathIndex, type SceneObjectPathIndex } from "./scene-object-path-index";
4
+ import { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
4
5
 
5
6
  const BANNER = `// Generated from this project's scenes by \`defold-typescript scene-types\`.
6
7
  // Do not edit: every run rewrites it from the .go/.collection sources.
@@ -28,19 +29,88 @@ function componentAddressesOf(
28
29
  return keys;
29
30
  }
30
31
 
32
+ // What a component address resolves to: `true` for an address that names no
33
+ // script this program compiles, else the specifier of the script module it hosts.
34
+ type AddressValues = ReadonlyMap<string, string | undefined>;
35
+
36
+ // A bare `#id` is typed only when every declaration of that id anywhere in the
37
+ // project names the same mapped script. The per-object walk covers each object a
38
+ // world reaches; the per-document pass covers what it cannot — a factory
39
+ // prototype with no static path, an embedded component under the same id — so a
40
+ // bare address never borrows one object's script for another's component.
41
+ function bareScriptsOf(
42
+ documents: ReadonlyMap<string, string>,
43
+ paths: SceneObjectPathIndex,
44
+ ): Map<string, string | undefined> {
45
+ const agreed = new Map<string, string | undefined>();
46
+ const record = (id: string, resource: string | undefined): void => {
47
+ if (!agreed.has(id)) agreed.set(id, resource);
48
+ else if (agreed.get(id) !== resource) agreed.set(id, undefined);
49
+ };
50
+ for (const [path, ids] of paths.componentsOf) {
51
+ const resources = paths.componentResourcesOf.get(path);
52
+ for (const id of ids) record(id, resources?.get(id));
53
+ }
54
+ for (const [displayPath, text] of documents) {
55
+ let declarations: ReturnType<typeof collectComponentDeclarations>;
56
+ try {
57
+ declarations = collectComponentDeclarations(parseSceneTextFormat(text), displayPath, []);
58
+ } catch (error) {
59
+ if (!(error instanceof SceneTextFormatError)) throw error;
60
+ continue;
61
+ }
62
+ for (const id of declarations.ids) record(id, declarations.resources.get(id));
63
+ }
64
+ return agreed;
65
+ }
66
+
67
+ function componentValuesOf(
68
+ documents: ReadonlyMap<string, string>,
69
+ paths: SceneObjectPathIndex,
70
+ scriptModules: ReadonlyMap<string, string>,
71
+ ): AddressValues {
72
+ const values = new Map<string, string | undefined>();
73
+ const moduleOf = (resource: string | undefined): string | undefined =>
74
+ resource === undefined ? undefined : scriptModules.get(resource);
75
+ const keys = componentAddressesOf(documents, paths);
76
+ if (scriptModules.size === 0) {
77
+ for (const key of keys) values.set(key, undefined);
78
+ return values;
79
+ }
80
+ const bare = bareScriptsOf(documents, paths);
81
+ for (const key of keys) {
82
+ const hash = key.indexOf("#");
83
+ const path = key.slice(0, hash);
84
+ const id = key.slice(hash + 1);
85
+ values.set(
86
+ key,
87
+ moduleOf(path === "" ? bare.get(id) : paths.componentResourcesOf.get(path)?.get(id)),
88
+ );
89
+ }
90
+ return values;
91
+ }
92
+
31
93
  // An interface with no members closes on the same line: an empty body written
32
94
  // across two lines is what a formatter would collapse, and the generated file
33
95
  // has to survive a consumer's formatter run byte-identical or every regeneration
34
96
  // looks like a change.
35
- function bodyOf(keys: readonly string[]): string {
36
- if (keys.length === 0) return "{}";
37
- const members = keys
38
- .map((key) => ` ${JSON.stringify(key)}: true;\n`)
97
+ function bodyOf(values: AddressValues): string {
98
+ if (values.size === 0) return "{}";
99
+ const members = [...values]
100
+ .map(([key, specifier]) => {
101
+ const value =
102
+ specifier === undefined ? "true" : `typeof import(${JSON.stringify(specifier)}).default`;
103
+ return ` ${JSON.stringify(key)}: ${value};\n`;
104
+ })
39
105
  .sort()
40
106
  .join("");
41
107
  return `{\n${members} }`;
42
108
  }
43
109
 
110
+ function trueValues(keys: Iterable<string>): AddressValues {
111
+ return new Map([...keys].map((key) => [key, undefined]));
112
+ }
113
+
44
114
  /**
45
115
  * The global augmentation that fills `SceneGameObjectAddresses` and
46
116
  * `SceneComponentAddresses` from already-read scene sources — keys are
@@ -52,19 +122,26 @@ function bodyOf(keys: readonly string[]): string {
52
122
  * byte-identical file, and a document map built in a different order emits the
53
123
  * same bytes as well; a `Set` iteration order leaking into the output would
54
124
  * make every regeneration rewrite the file and re-check the whole program.
125
+ *
126
+ * `scriptModules` maps a script resource a scene names to the import specifier
127
+ * of the program source that compiles to it, relative to the declaration file.
128
+ * A component address whose resource maps is written as that module's default
129
+ * export, so `msg.post` can read the script's messages off it; every other
130
+ * address stays `true`. Omitted, every address is `true`.
55
131
  */
56
132
  export function buildSceneAddressDeclaration(
57
133
  documents: ReadonlyMap<string, string>,
58
134
  roles: SceneCollectionRoles,
135
+ scriptModules: ReadonlyMap<string, string> = new Map(),
59
136
  ): string {
60
137
  // One walk for both key sets: the qualified component addresses are the path
61
138
  // universe joined to its own prototypes, not a second index over the same files.
62
139
  const index = buildSceneObjectPathIndex(documents, roles);
63
- const components = componentAddressesOf(documents, index);
140
+ const components = componentValuesOf(documents, index, scriptModules);
64
141
 
65
142
  return `${BANNER}
66
143
  declare global {
67
- interface SceneGameObjectAddresses ${bodyOf([...index.paths])}
144
+ interface SceneGameObjectAddresses ${bodyOf(trueValues(index.paths))}
68
145
 
69
146
  interface SceneComponentAddresses ${bodyOf(components)}
70
147
  }