@defold-typescript/transpiler 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,28 +2,143 @@
2
2
  import { readFileSync as readFileSync2 } from "node:fs";
3
3
  import { createRequire as createRequire2 } from "node:module";
4
4
  import * as path2 from "node:path";
5
- import * as ts2 from "typescript";
5
+ import * as ts4 from "typescript";
6
6
  import * as tstl2 from "typescript-to-lua";
7
7
 
8
8
  // src/lifecycle-erasure.ts
9
- import * as ts from "typescript";
9
+ import * as ts2 from "typescript";
10
10
  import {
11
11
  createAssignmentStatement,
12
+ createBinaryExpression as createBinaryExpression2,
13
+ createBlock as createBlock2,
14
+ createCallExpression as createCallExpression2,
15
+ createExpressionStatement,
16
+ createForInStatement,
17
+ createFunctionExpression as createFunctionExpression2,
18
+ createIdentifier as createIdentifier2,
19
+ createIfStatement as createIfStatement2,
20
+ createNilLiteral,
21
+ createReturnStatement,
22
+ createTableIndexExpression,
23
+ createVariableDeclarationStatement as createVariableDeclarationStatement2,
24
+ NodeFlags as NodeFlags2,
25
+ SyntaxKind as SyntaxKind4
26
+ } from "typescript-to-lua";
27
+
28
+ // src/message-dispatch-lowering.ts
29
+ import * as ts from "typescript";
30
+ import {
12
31
  createBinaryExpression,
13
32
  createBlock,
14
33
  createCallExpression,
15
- createExpressionStatement,
16
- createForInStatement,
17
34
  createFunctionExpression,
18
35
  createIdentifier,
19
36
  createIfStatement,
20
- createNilLiteral,
21
- createReturnStatement,
22
- createTableIndexExpression,
37
+ createStringLiteral,
23
38
  createVariableDeclarationStatement,
24
39
  NodeFlags,
25
40
  SyntaxKind as SyntaxKind2
26
41
  } from "typescript-to-lua";
42
+ var DISPATCH_MODULE = "@defold-typescript/types";
43
+ var DISPATCH_NAME = "onMessage";
44
+ var HANDLER_PARAM_TARGETS = ["self", "message", "sender"];
45
+ function resolvesToDispatchExport(callee, checker) {
46
+ let symbol = checker.getSymbolAtLocation(callee);
47
+ if (symbol === undefined) {
48
+ return false;
49
+ }
50
+ if (symbol.flags & ts.SymbolFlags.Alias) {
51
+ symbol = checker.getAliasedSymbol(symbol);
52
+ }
53
+ if (symbol.getName() !== DISPATCH_NAME) {
54
+ return false;
55
+ }
56
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
57
+ if (declaration === undefined) {
58
+ return false;
59
+ }
60
+ return declaration.getSourceFile().fileName.includes(DISPATCH_MODULE);
61
+ }
62
+ function handlerName(property) {
63
+ const name = property.name;
64
+ if (name === undefined) {
65
+ return;
66
+ }
67
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
68
+ return name.text;
69
+ }
70
+ return;
71
+ }
72
+ function handlerFunction(property) {
73
+ if (ts.isMethodDeclaration(property) && property.body !== undefined) {
74
+ return property;
75
+ }
76
+ if (ts.isPropertyAssignment(property)) {
77
+ const initializer = property.initializer;
78
+ if (ts.isFunctionExpression(initializer) || ts.isArrowFunction(initializer)) {
79
+ return initializer;
80
+ }
81
+ }
82
+ return;
83
+ }
84
+ function isThisParameter(param) {
85
+ return ts.isIdentifier(param.name) && ts.identifierToKeywordKind(param.name) === ts.SyntaxKind.ThisKeyword;
86
+ }
87
+ function aliasStatements(fn) {
88
+ const params = fn.parameters.filter((param) => !isThisParameter(param));
89
+ const aliases = [];
90
+ params.forEach((param, index) => {
91
+ const engineName = HANDLER_PARAM_TARGETS[index];
92
+ if (engineName === undefined || !ts.isIdentifier(param.name)) {
93
+ return;
94
+ }
95
+ if (param.name.text !== engineName) {
96
+ aliases.push(createVariableDeclarationStatement(createIdentifier(param.name.text), createIdentifier(engineName)));
97
+ }
98
+ });
99
+ return aliases;
100
+ }
101
+ function handlerBody(fn, context) {
102
+ if (ts.isBlock(fn.body)) {
103
+ return context.transformStatements(fn.body.statements);
104
+ }
105
+ return context.transformStatements([ts.factory.createExpressionStatement(fn.body)]);
106
+ }
107
+ var messageDispatchLoweringPlugin = {
108
+ visitors: {
109
+ [ts.SyntaxKind.CallExpression]: (node, context) => {
110
+ const [handlersArg] = node.arguments;
111
+ if (node.arguments.length === 1 && handlersArg !== undefined && ts.isObjectLiteralExpression(handlersArg) && resolvesToDispatchExport(node.expression, context.checker)) {
112
+ let chain;
113
+ const properties = [...handlersArg.properties];
114
+ for (let i = properties.length - 1;i >= 0; i--) {
115
+ const property = properties[i];
116
+ if (property === undefined) {
117
+ continue;
118
+ }
119
+ const name = handlerName(property);
120
+ const fn = handlerFunction(property);
121
+ if (name === undefined || fn === undefined) {
122
+ continue;
123
+ }
124
+ const condition = createBinaryExpression(createIdentifier("message_id"), createCallExpression(createIdentifier("hash"), [createStringLiteral(name)]), SyntaxKind2.EqualityOperator);
125
+ const body = [...aliasStatements(fn), ...handlerBody(fn, context)];
126
+ chain = createIfStatement(condition, createBlock(body), chain);
127
+ }
128
+ const params = [
129
+ createIdentifier("self"),
130
+ createIdentifier("message_id"),
131
+ createIdentifier("message"),
132
+ createIdentifier("sender")
133
+ ];
134
+ return createFunctionExpression(createBlock(chain === undefined ? [] : [chain]), params, undefined, NodeFlags.Declaration, node);
135
+ }
136
+ return context.superTransformExpression(node);
137
+ }
138
+ }
139
+ };
140
+
141
+ // src/lifecycle-erasure.ts
27
142
  var FACTORY_MODULE = "@defold-typescript/types";
28
143
  var FACTORY_NAMES = new Set(["defineScript", "defineGuiScript", "defineRenderScript"]);
29
144
  function resolvesToFactoryExport(callee, checker) {
@@ -31,7 +146,7 @@ function resolvesToFactoryExport(callee, checker) {
31
146
  if (symbol === undefined) {
32
147
  return false;
33
148
  }
34
- if (symbol.flags & ts.SymbolFlags.Alias) {
149
+ if (symbol.flags & ts2.SymbolFlags.Alias) {
35
150
  symbol = checker.getAliasedSymbol(symbol);
36
151
  }
37
152
  if (!FACTORY_NAMES.has(symbol.getName())) {
@@ -43,42 +158,52 @@ function resolvesToFactoryExport(callee, checker) {
43
158
  }
44
159
  return declaration.getSourceFile().fileName.includes(FACTORY_MODULE);
45
160
  }
46
- function isThisParameter(param) {
47
- return ts.isIdentifier(param.name) && ts.identifierToKeywordKind(param.name) === ts.SyntaxKind.ThisKeyword;
161
+ function isThisParameter2(param) {
162
+ return ts2.isIdentifier(param.name) && ts2.identifierToKeywordKind(param.name) === ts2.SyntaxKind.ThisKeyword;
48
163
  }
49
164
  function hookName(property) {
50
165
  const name = property.name;
51
166
  if (name === undefined) {
52
167
  return;
53
168
  }
54
- if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
169
+ if (ts2.isIdentifier(name) || ts2.isStringLiteral(name)) {
55
170
  return name.text;
56
171
  }
57
172
  return;
58
173
  }
59
174
  function hookFunction(property) {
60
- if (ts.isMethodDeclaration(property) && property.body !== undefined) {
175
+ if (ts2.isMethodDeclaration(property) && property.body !== undefined) {
61
176
  return property;
62
177
  }
63
- if (ts.isPropertyAssignment(property)) {
178
+ if (ts2.isPropertyAssignment(property)) {
64
179
  const initializer = property.initializer;
65
- if (ts.isFunctionExpression(initializer) || ts.isArrowFunction(initializer)) {
180
+ if (ts2.isFunctionExpression(initializer) || ts2.isArrowFunction(initializer)) {
66
181
  return initializer;
67
182
  }
68
183
  }
69
184
  return;
70
185
  }
186
+ function dispatcherInitializer(property, checker) {
187
+ if (!ts2.isPropertyAssignment(property)) {
188
+ return;
189
+ }
190
+ const initializer = property.initializer;
191
+ if (!ts2.isCallExpression(initializer)) {
192
+ return;
193
+ }
194
+ return resolvesToDispatchExport(initializer.expression, checker) ? initializer : undefined;
195
+ }
71
196
  function transformHookBody(fn, context) {
72
- if (ts.isBlock(fn.body)) {
197
+ if (ts2.isBlock(fn.body)) {
73
198
  return context.transformStatements(fn.body.statements);
74
199
  }
75
200
  return [createExpressionStatement(context.transformExpression(fn.body))];
76
201
  }
77
202
  function crossesFunctionBoundary(node) {
78
- return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
203
+ return ts2.isFunctionDeclaration(node) || ts2.isFunctionExpression(node) || ts2.isArrowFunction(node) || ts2.isMethodDeclaration(node) || ts2.isGetAccessorDeclaration(node) || ts2.isSetAccessorDeclaration(node);
79
204
  }
80
205
  function initReturnsValue(fn) {
81
- if (!ts.isBlock(fn.body)) {
206
+ if (!ts2.isBlock(fn.body)) {
82
207
  return true;
83
208
  }
84
209
  let found = false;
@@ -86,61 +211,68 @@ function initReturnsValue(fn) {
86
211
  if (found || crossesFunctionBoundary(node)) {
87
212
  return;
88
213
  }
89
- if (ts.isReturnStatement(node) && node.expression !== undefined) {
214
+ if (ts2.isReturnStatement(node) && node.expression !== undefined) {
90
215
  found = true;
91
216
  return;
92
217
  }
93
- ts.forEachChild(node, visit);
218
+ ts2.forEachChild(node, visit);
94
219
  };
95
- ts.forEachChild(fn.body, visit);
220
+ ts2.forEachChild(fn.body, visit);
96
221
  return found;
97
222
  }
98
223
  function initBuilderBody(fn, context) {
99
- if (ts.isBlock(fn.body)) {
224
+ if (ts2.isBlock(fn.body)) {
100
225
  return context.transformStatements(fn.body.statements);
101
226
  }
102
227
  return [createReturnStatement([context.transformExpression(fn.body)])];
103
228
  }
104
229
  function emitInitMerge(fn, context, property) {
105
- const builder = createFunctionExpression(createBlock(initBuilderBody(fn, context)), [], undefined, NodeFlags.Declaration, fn);
106
- const builderDecl = createVariableDeclarationStatement(createIdentifier("____init"), builder);
107
- const stateDecl = createVariableDeclarationStatement(createIdentifier("____s"), createCallExpression(createIdentifier("____init"), []));
108
- const mergeAssignment = createAssignmentStatement(createTableIndexExpression(createIdentifier("self"), createIdentifier("____k")), createIdentifier("____v"));
109
- const forIn = createForInStatement(createBlock([mergeAssignment]), [createIdentifier("____k"), createIdentifier("____v")], [createCallExpression(createIdentifier("pairs"), [createIdentifier("____s")])]);
110
- const guard = createIfStatement(createBinaryExpression(createIdentifier("____s"), createNilLiteral(), SyntaxKind2.InequalityOperator), createBlock([forIn]));
111
- const initFn = createFunctionExpression(createBlock([stateDecl, guard]), [createIdentifier("self")], undefined, NodeFlags.Declaration, fn);
112
- return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
230
+ const builder = createFunctionExpression2(createBlock2(initBuilderBody(fn, context)), [], undefined, NodeFlags2.Declaration, fn);
231
+ const builderDecl = createVariableDeclarationStatement2(createIdentifier2("____init"), builder);
232
+ const stateDecl = createVariableDeclarationStatement2(createIdentifier2("____s"), createCallExpression2(createIdentifier2("____init"), []));
233
+ const mergeAssignment = createAssignmentStatement(createTableIndexExpression(createIdentifier2("self"), createIdentifier2("____k")), createIdentifier2("____v"));
234
+ const forIn = createForInStatement(createBlock2([mergeAssignment]), [createIdentifier2("____k"), createIdentifier2("____v")], [createCallExpression2(createIdentifier2("pairs"), [createIdentifier2("____s")])]);
235
+ const guard = createIfStatement2(createBinaryExpression2(createIdentifier2("____s"), createNilLiteral(), SyntaxKind4.InequalityOperator), createBlock2([forIn]));
236
+ const initFn = createFunctionExpression2(createBlock2([stateDecl, guard]), [createIdentifier2("self")], undefined, NodeFlags2.Declaration, fn);
237
+ return [builderDecl, createAssignmentStatement(createIdentifier2("init"), initFn, property)];
113
238
  }
114
239
  function eraseFactoryCall(expression, context) {
115
- if (!ts.isCallExpression(expression)) {
240
+ if (!ts2.isCallExpression(expression)) {
116
241
  return;
117
242
  }
118
243
  if (!resolvesToFactoryExport(expression.expression, context.checker)) {
119
244
  return;
120
245
  }
121
246
  const hooks = expression.arguments[0];
122
- if (hooks === undefined || !ts.isObjectLiteralExpression(hooks)) {
247
+ if (hooks === undefined || !ts2.isObjectLiteralExpression(hooks)) {
123
248
  return;
124
249
  }
125
250
  const statements = [];
126
251
  for (const property of hooks.properties) {
127
252
  const name = hookName(property);
253
+ if (name === undefined) {
254
+ continue;
255
+ }
128
256
  const fn = hookFunction(property);
129
- if (name === undefined || fn === undefined) {
257
+ if (fn === undefined) {
258
+ const dispatch = dispatcherInitializer(property, context.checker);
259
+ if (dispatch !== undefined) {
260
+ statements.push(createAssignmentStatement(createIdentifier2(name), context.transformExpression(dispatch), property));
261
+ }
130
262
  continue;
131
263
  }
132
264
  if (name === "init" && initReturnsValue(fn)) {
133
265
  statements.push(...emitInitMerge(fn, context, property));
134
266
  continue;
135
267
  }
136
- const params = fn.parameters.filter((param) => !isThisParameter(param) && ts.isIdentifier(param.name)).map((param) => context.transformExpression(param.name));
137
- const fnExpression = createFunctionExpression(createBlock(transformHookBody(fn, context)), params, undefined, NodeFlags.Declaration, fn);
138
- statements.push(createAssignmentStatement(createIdentifier(name), fnExpression, property));
268
+ const params = fn.parameters.filter((param) => !isThisParameter2(param) && ts2.isIdentifier(param.name)).map((param) => context.transformExpression(param.name));
269
+ const fnExpression = createFunctionExpression2(createBlock2(transformHookBody(fn, context)), params, undefined, NodeFlags2.Declaration, fn);
270
+ statements.push(createAssignmentStatement(createIdentifier2(name), fnExpression, property));
139
271
  }
140
272
  return statements;
141
273
  }
142
274
  function isFactoryOnlyImport(node) {
143
- if (!ts.isStringLiteral(node.moduleSpecifier) || node.moduleSpecifier.text !== FACTORY_MODULE) {
275
+ if (!ts2.isStringLiteral(node.moduleSpecifier) || node.moduleSpecifier.text !== FACTORY_MODULE) {
144
276
  return false;
145
277
  }
146
278
  const clause = node.importClause;
@@ -148,15 +280,15 @@ function isFactoryOnlyImport(node) {
148
280
  return false;
149
281
  }
150
282
  const bindings = clause.namedBindings;
151
- if (bindings === undefined || !ts.isNamedImports(bindings)) {
283
+ if (bindings === undefined || !ts2.isNamedImports(bindings)) {
152
284
  return false;
153
285
  }
154
286
  return bindings.elements.every((element) => FACTORY_NAMES.has((element.propertyName ?? element.name).text));
155
287
  }
156
288
  var lifecycleErasurePlugin = {
157
289
  visitors: {
158
- [ts.SyntaxKind.ExpressionStatement]: (node, context) => eraseFactoryCall(node.expression, context) ?? context.superTransformStatements(node),
159
- [ts.SyntaxKind.ExportAssignment]: (node, context) => {
290
+ [ts2.SyntaxKind.ExpressionStatement]: (node, context) => eraseFactoryCall(node.expression, context) ?? context.superTransformStatements(node),
291
+ [ts2.SyntaxKind.ExportAssignment]: (node, context) => {
160
292
  if (!node.isExportEquals) {
161
293
  const erased = eraseFactoryCall(node.expression, context);
162
294
  if (erased !== undefined) {
@@ -165,7 +297,48 @@ var lifecycleErasurePlugin = {
165
297
  }
166
298
  return context.superTransformStatements(node);
167
299
  },
168
- [ts.SyntaxKind.ImportDeclaration]: (node, context) => isFactoryOnlyImport(node) ? [] : context.superTransformStatements(node)
300
+ [ts2.SyntaxKind.ImportDeclaration]: (node, context) => isFactoryOnlyImport(node) ? [] : context.superTransformStatements(node)
301
+ }
302
+ };
303
+
304
+ // src/message-guard-lowering.ts
305
+ import * as ts3 from "typescript";
306
+ import {
307
+ createBinaryExpression as createBinaryExpression3,
308
+ createCallExpression as createCallExpression3,
309
+ createIdentifier as createIdentifier3,
310
+ SyntaxKind as SyntaxKind6
311
+ } from "typescript-to-lua";
312
+ var GUARD_MODULE = "@defold-typescript/types";
313
+ var GUARD_NAME = "isMessage";
314
+ function resolvesToGuardExport(callee, checker) {
315
+ let symbol = checker.getSymbolAtLocation(callee);
316
+ if (symbol === undefined) {
317
+ return false;
318
+ }
319
+ if (symbol.flags & ts3.SymbolFlags.Alias) {
320
+ symbol = checker.getAliasedSymbol(symbol);
321
+ }
322
+ if (symbol.getName() !== GUARD_NAME) {
323
+ return false;
324
+ }
325
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
326
+ if (declaration === undefined) {
327
+ return false;
328
+ }
329
+ return declaration.getSourceFile().fileName.includes(GUARD_MODULE);
330
+ }
331
+ var messageGuardLoweringPlugin = {
332
+ visitors: {
333
+ [ts3.SyntaxKind.CallExpression]: (node, context) => {
334
+ const [messageIdArg, , expectedArg] = node.arguments;
335
+ if (node.arguments.length === 3 && messageIdArg !== undefined && expectedArg !== undefined && resolvesToGuardExport(node.expression, context.checker)) {
336
+ const messageId = context.transformExpression(messageIdArg);
337
+ const expected = context.transformExpression(expectedArg);
338
+ return createBinaryExpression3(messageId, createCallExpression3(createIdentifier3("hash"), [expected]), SyntaxKind6.EqualityOperator, node);
339
+ }
340
+ return context.superTransformExpression(node);
341
+ }
169
342
  }
170
343
  };
171
344
 
@@ -191,6 +364,8 @@ function buildAmbientFiles() {
191
364
  "node_modules/@typescript-to-lua/language-extensions/index.d.ts": readFileSync(path.join(TSTL_LANG_EXT_ROOT, "index.d.ts"), "utf8"),
192
365
  "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
193
366
  "node_modules/@defold-typescript/types/src/msg-overloads.d.ts": readAmbient("src/msg-overloads.d.ts"),
367
+ "node_modules/@defold-typescript/types/src/message-guard.d.ts": readAmbient("src/message-guard.d.ts"),
368
+ "node_modules/@defold-typescript/types/src/message-dispatch.d.ts": readAmbient("src/message-dispatch.d.ts"),
194
369
  "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
195
370
  "node_modules/@defold-typescript/types/index.ts": [
196
371
  'export { defineGuiScript, defineRenderScript, defineScript } from "./src/lifecycle";',
@@ -241,7 +416,12 @@ function transpileProject(input) {
241
416
  luaTarget: tstl.LuaTarget.Lua54,
242
417
  sourceMap: true,
243
418
  skipLibCheck: true,
244
- luaPlugins: [{ plugin: lifecycleErasurePlugin }]
419
+ noImplicitSelf: true,
420
+ luaPlugins: [
421
+ { plugin: lifecycleErasurePlugin },
422
+ { plugin: messageGuardLoweringPlugin },
423
+ { plugin: messageDispatchLoweringPlugin }
424
+ ]
245
425
  });
246
426
  return collectOutputs(result.transpiledFiles, result.diagnostics, userKeys);
247
427
  }
@@ -260,7 +440,12 @@ var COMPILER_OPTIONS = {
260
440
  luaTarget: tstl2.LuaTarget.Lua54,
261
441
  sourceMap: true,
262
442
  skipLibCheck: true,
263
- luaPlugins: [{ plugin: lifecycleErasurePlugin }]
443
+ noImplicitSelf: true,
444
+ luaPlugins: [
445
+ { plugin: lifecycleErasurePlugin },
446
+ { plugin: messageGuardLoweringPlugin },
447
+ { plugin: messageDispatchLoweringPlugin }
448
+ ]
264
449
  };
265
450
  function normalizeSlashes(p) {
266
451
  return p.replace(/\\/g, "/");
@@ -300,7 +485,7 @@ function createTranspileSession() {
300
485
  if (cached) {
301
486
  return cached;
302
487
  }
303
- const created = ts2.createSourceFile(fileName, content, ts2.ScriptTarget.Latest, false);
488
+ const created = ts4.createSourceFile(fileName, content, ts4.ScriptTarget.Latest, false);
304
489
  sourceFileCache.set(normalized, created);
305
490
  return created;
306
491
  }
@@ -319,7 +504,7 @@ function createTranspileSession() {
319
504
  return cached;
320
505
  }
321
506
  const fileContent = readFileSync2(filePath, "utf8");
322
- const created = ts2.createSourceFile(filePath, fileContent, ts2.ScriptTarget.Latest, false);
507
+ const created = ts4.createSourceFile(filePath, fileContent, ts4.ScriptTarget.Latest, false);
323
508
  libCache.set(fileName, created);
324
509
  return created;
325
510
  }
@@ -332,10 +517,10 @@ function createTranspileSession() {
332
517
  return AMBIENT_FILES[normalized];
333
518
  }
334
519
  const host = {
335
- fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts2.sys.fileExists(fileName),
520
+ fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts4.sys.fileExists(fileName),
336
521
  getCanonicalFileName: (fileName) => fileName,
337
522
  getCurrentDirectory: () => "",
338
- getDefaultLibFileName: ts2.getDefaultLibFileName,
523
+ getDefaultLibFileName: ts4.getDefaultLibFileName,
339
524
  readFile: () => "",
340
525
  getNewLine: () => `
341
526
  `,
@@ -356,15 +541,15 @@ function createTranspileSession() {
356
541
  }
357
542
  const userKeys = new Set(userFiles.keys());
358
543
  const rootNames = [...Object.keys(AMBIENT_FILES).map(normalizeSlashes), ...userKeys];
359
- program = ts2.createProgram(rootNames, COMPILER_OPTIONS, host, program);
360
- const preEmitDiagnostics = ts2.getPreEmitDiagnostics(program);
544
+ program = ts4.createProgram(rootNames, COMPILER_OPTIONS, host, program);
545
+ const preEmitDiagnostics = ts4.getPreEmitDiagnostics(program);
361
546
  const collector = createOutputCollector();
362
547
  const { diagnostics: transpileDiagnostics } = new tstl2.Transpiler().emit({
363
548
  program,
364
549
  writeFile: collector.writeFile
365
550
  });
366
551
  const diagnostics = [
367
- ...ts2.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
552
+ ...ts4.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
368
553
  ];
369
554
  return collectOutputs(collector.files, diagnostics, userKeys);
370
555
  }
@@ -0,0 +1,4 @@
1
+ import * as ts from "typescript";
2
+ import { type Plugin } from "typescript-to-lua";
3
+ export declare function resolvesToDispatchExport(callee: ts.Expression, checker: ts.TypeChecker): boolean;
4
+ export declare const messageDispatchLoweringPlugin: Plugin;
@@ -0,0 +1,2 @@
1
+ import { type Plugin } from "typescript-to-lua";
2
+ export declare const messageGuardLoweringPlugin: Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "TypeScript-to-Lua build pipeline tuned for Defold's runtime.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "test": "bun test"
29
29
  },
30
30
  "dependencies": {
31
- "@defold-typescript/types": "0.5.2",
31
+ "@defold-typescript/types": "0.5.3",
32
32
  "@typescript-to-lua/language-extensions": "1.19.0",
33
33
  "typescript-to-lua": "^1.36.0"
34
34
  }
@@ -20,6 +20,7 @@ import {
20
20
  SyntaxKind,
21
21
  type TransformationContext,
22
22
  } from "typescript-to-lua";
23
+ import { resolvesToDispatchExport } from "./message-dispatch-lowering";
23
24
 
24
25
  const FACTORY_MODULE = "@defold-typescript/types";
25
26
  const FACTORY_NAMES = new Set(["defineScript", "defineGuiScript", "defineRenderScript"]);
@@ -77,6 +78,20 @@ function hookFunction(property: ts.ObjectLiteralElementLike): HookFunction | und
77
78
  return undefined;
78
79
  }
79
80
 
81
+ function dispatcherInitializer(
82
+ property: ts.ObjectLiteralElementLike,
83
+ checker: ts.TypeChecker,
84
+ ): ts.CallExpression | undefined {
85
+ if (!ts.isPropertyAssignment(property)) {
86
+ return undefined;
87
+ }
88
+ const initializer = property.initializer;
89
+ if (!ts.isCallExpression(initializer)) {
90
+ return undefined;
91
+ }
92
+ return resolvesToDispatchExport(initializer.expression, checker) ? initializer : undefined;
93
+ }
94
+
80
95
  function transformHookBody(fn: HookFunction, context: TransformationContext): Statement[] {
81
96
  if (ts.isBlock(fn.body)) {
82
97
  return context.transformStatements(fn.body.statements);
@@ -187,8 +202,24 @@ function eraseFactoryCall(
187
202
  const statements: Statement[] = [];
188
203
  for (const property of hooks.properties) {
189
204
  const name = hookName(property);
205
+ if (name === undefined) {
206
+ continue;
207
+ }
190
208
  const fn = hookFunction(property);
191
- if (name === undefined || fn === undefined) {
209
+ if (fn === undefined) {
210
+ // A hook whose initializer is a recognized `onMessage({...})` call: let it
211
+ // route through the message-dispatch lowering, which yields the flat
212
+ // `function(self, message_id, message, sender)` chunk, then bind it.
213
+ const dispatch = dispatcherInitializer(property, context.checker);
214
+ if (dispatch !== undefined) {
215
+ statements.push(
216
+ createAssignmentStatement(
217
+ createIdentifier(name),
218
+ context.transformExpression(dispatch),
219
+ property,
220
+ ),
221
+ );
222
+ }
192
223
  continue;
193
224
  }
194
225
  if (name === "init" && initReturnsValue(fn)) {
@@ -0,0 +1,155 @@
1
+ import * as ts from "typescript";
2
+ import {
3
+ createBinaryExpression,
4
+ createBlock,
5
+ createCallExpression,
6
+ createFunctionExpression,
7
+ createIdentifier,
8
+ createIfStatement,
9
+ createStringLiteral,
10
+ createVariableDeclarationStatement,
11
+ type Expression,
12
+ type Identifier,
13
+ type IfStatement,
14
+ NodeFlags,
15
+ type Plugin,
16
+ type Statement,
17
+ SyntaxKind,
18
+ type TransformationContext,
19
+ } from "typescript-to-lua";
20
+
21
+ const DISPATCH_MODULE = "@defold-typescript/types";
22
+ const DISPATCH_NAME = "onMessage";
23
+
24
+ // The engine calls `on_message(self, message_id, message, sender)`; a handler's
25
+ // own params (in order) alias onto these three value-bearing names.
26
+ const HANDLER_PARAM_TARGETS = ["self", "message", "sender"] as const;
27
+
28
+ export function resolvesToDispatchExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
29
+ let symbol = checker.getSymbolAtLocation(callee);
30
+ if (symbol === undefined) {
31
+ return false;
32
+ }
33
+ if (symbol.flags & ts.SymbolFlags.Alias) {
34
+ symbol = checker.getAliasedSymbol(symbol);
35
+ }
36
+ if (symbol.getName() !== DISPATCH_NAME) {
37
+ return false;
38
+ }
39
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
40
+ if (declaration === undefined) {
41
+ return false;
42
+ }
43
+ return declaration.getSourceFile().fileName.includes(DISPATCH_MODULE);
44
+ }
45
+
46
+ type HandlerFunction = (ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction) & {
47
+ body: ts.ConciseBody;
48
+ };
49
+
50
+ function handlerName(property: ts.ObjectLiteralElementLike): string | undefined {
51
+ const name = property.name;
52
+ if (name === undefined) {
53
+ return undefined;
54
+ }
55
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
56
+ return name.text;
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ function handlerFunction(property: ts.ObjectLiteralElementLike): HandlerFunction | undefined {
62
+ if (ts.isMethodDeclaration(property) && property.body !== undefined) {
63
+ return property as HandlerFunction;
64
+ }
65
+ if (ts.isPropertyAssignment(property)) {
66
+ const initializer = property.initializer;
67
+ if (ts.isFunctionExpression(initializer) || ts.isArrowFunction(initializer)) {
68
+ return initializer as HandlerFunction;
69
+ }
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ function isThisParameter(param: ts.ParameterDeclaration): boolean {
75
+ return (
76
+ ts.isIdentifier(param.name) &&
77
+ ts.identifierToKeywordKind(param.name) === ts.SyntaxKind.ThisKeyword
78
+ );
79
+ }
80
+
81
+ function aliasStatements(fn: HandlerFunction): Statement[] {
82
+ const params = fn.parameters.filter((param) => !isThisParameter(param));
83
+ const aliases: Statement[] = [];
84
+ params.forEach((param, index) => {
85
+ const engineName = HANDLER_PARAM_TARGETS[index];
86
+ if (engineName === undefined || !ts.isIdentifier(param.name)) {
87
+ return;
88
+ }
89
+ if (param.name.text !== engineName) {
90
+ aliases.push(
91
+ createVariableDeclarationStatement(
92
+ createIdentifier(param.name.text),
93
+ createIdentifier(engineName),
94
+ ),
95
+ );
96
+ }
97
+ });
98
+ return aliases;
99
+ }
100
+
101
+ function handlerBody(fn: HandlerFunction, context: TransformationContext): Statement[] {
102
+ if (ts.isBlock(fn.body)) {
103
+ return context.transformStatements(fn.body.statements);
104
+ }
105
+ return context.transformStatements([ts.factory.createExpressionStatement(fn.body)]);
106
+ }
107
+
108
+ export const messageDispatchLoweringPlugin: Plugin = {
109
+ visitors: {
110
+ [ts.SyntaxKind.CallExpression]: (node, context): Expression => {
111
+ const [handlersArg] = node.arguments;
112
+ if (
113
+ node.arguments.length === 1 &&
114
+ handlersArg !== undefined &&
115
+ ts.isObjectLiteralExpression(handlersArg) &&
116
+ resolvesToDispatchExport(node.expression, context.checker)
117
+ ) {
118
+ let chain: IfStatement | undefined;
119
+ const properties = [...handlersArg.properties];
120
+ for (let i = properties.length - 1; i >= 0; i--) {
121
+ const property = properties[i];
122
+ if (property === undefined) {
123
+ continue;
124
+ }
125
+ const name = handlerName(property);
126
+ const fn = handlerFunction(property);
127
+ if (name === undefined || fn === undefined) {
128
+ continue;
129
+ }
130
+ const condition = createBinaryExpression(
131
+ createIdentifier("message_id"),
132
+ createCallExpression(createIdentifier("hash"), [createStringLiteral(name)]),
133
+ SyntaxKind.EqualityOperator,
134
+ );
135
+ const body = [...aliasStatements(fn), ...handlerBody(fn, context)];
136
+ chain = createIfStatement(condition, createBlock(body), chain);
137
+ }
138
+ const params: Identifier[] = [
139
+ createIdentifier("self"),
140
+ createIdentifier("message_id"),
141
+ createIdentifier("message"),
142
+ createIdentifier("sender"),
143
+ ];
144
+ return createFunctionExpression(
145
+ createBlock(chain === undefined ? [] : [chain]),
146
+ params,
147
+ undefined,
148
+ NodeFlags.Declaration,
149
+ node,
150
+ );
151
+ }
152
+ return context.superTransformExpression(node);
153
+ },
154
+ },
155
+ };
@@ -0,0 +1,54 @@
1
+ import * as ts from "typescript";
2
+ import {
3
+ createBinaryExpression,
4
+ createCallExpression,
5
+ createIdentifier,
6
+ type Expression,
7
+ type Plugin,
8
+ SyntaxKind,
9
+ } from "typescript-to-lua";
10
+
11
+ const GUARD_MODULE = "@defold-typescript/types";
12
+ const GUARD_NAME = "isMessage";
13
+
14
+ function resolvesToGuardExport(callee: ts.Expression, checker: ts.TypeChecker): boolean {
15
+ let symbol = checker.getSymbolAtLocation(callee);
16
+ if (symbol === undefined) {
17
+ return false;
18
+ }
19
+ if (symbol.flags & ts.SymbolFlags.Alias) {
20
+ symbol = checker.getAliasedSymbol(symbol);
21
+ }
22
+ if (symbol.getName() !== GUARD_NAME) {
23
+ return false;
24
+ }
25
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
26
+ if (declaration === undefined) {
27
+ return false;
28
+ }
29
+ return declaration.getSourceFile().fileName.includes(GUARD_MODULE);
30
+ }
31
+
32
+ export const messageGuardLoweringPlugin: Plugin = {
33
+ visitors: {
34
+ [ts.SyntaxKind.CallExpression]: (node, context): Expression => {
35
+ const [messageIdArg, , expectedArg] = node.arguments;
36
+ if (
37
+ node.arguments.length === 3 &&
38
+ messageIdArg !== undefined &&
39
+ expectedArg !== undefined &&
40
+ resolvesToGuardExport(node.expression, context.checker)
41
+ ) {
42
+ const messageId = context.transformExpression(messageIdArg);
43
+ const expected = context.transformExpression(expectedArg);
44
+ return createBinaryExpression(
45
+ messageId,
46
+ createCallExpression(createIdentifier("hash"), [expected]),
47
+ SyntaxKind.EqualityOperator,
48
+ node,
49
+ );
50
+ }
51
+ return context.superTransformExpression(node);
52
+ },
53
+ },
54
+ };
package/src/session.ts CHANGED
@@ -4,6 +4,8 @@ import * as path from "node:path";
4
4
  import * as ts from "typescript";
5
5
  import * as tstl from "typescript-to-lua";
6
6
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
7
+ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
8
+ import { messageGuardLoweringPlugin } from "./message-guard-lowering";
7
9
  import { AMBIENT_FILES, collectOutputs, type TranspileProjectResult } from "./transpile";
8
10
 
9
11
  export interface TranspileSession {
@@ -19,7 +21,14 @@ const COMPILER_OPTIONS: tstl.CompilerOptions = {
19
21
  // Don't cross-check the seeded ambient .d.ts surface against itself; only user
20
22
  // files matter (mirrors transpileProject and the editor's skipLibCheck).
21
23
  skipLibCheck: true,
22
- luaPlugins: [{ plugin: lifecycleErasurePlugin }],
24
+ // Defold scripts are not OO: free helper functions never receive a context,
25
+ // so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
26
+ noImplicitSelf: true,
27
+ luaPlugins: [
28
+ { plugin: lifecycleErasurePlugin },
29
+ { plugin: messageGuardLoweringPlugin },
30
+ { plugin: messageDispatchLoweringPlugin },
31
+ ],
23
32
  };
24
33
 
25
34
  function normalizeSlashes(p: string): string {
package/src/transpile.ts CHANGED
@@ -4,6 +4,8 @@ import * as path from "node:path";
4
4
  import type * as ts from "typescript";
5
5
  import * as tstl from "typescript-to-lua";
6
6
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
7
+ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
8
+ import { messageGuardLoweringPlugin } from "./message-guard-lowering";
7
9
 
8
10
  export interface TranspileResult {
9
11
  readonly lua: string;
@@ -56,6 +58,11 @@ function buildAmbientFiles(): Record<string, string> {
56
58
  "node_modules/@defold-typescript/types/src/core-types.ts": readAmbient("src/core-types.ts"),
57
59
  "node_modules/@defold-typescript/types/src/msg-overloads.d.ts":
58
60
  readAmbient("src/msg-overloads.d.ts"),
61
+ "node_modules/@defold-typescript/types/src/message-guard.d.ts":
62
+ readAmbient("src/message-guard.d.ts"),
63
+ "node_modules/@defold-typescript/types/src/message-dispatch.d.ts": readAmbient(
64
+ "src/message-dispatch.d.ts",
65
+ ),
59
66
  "node_modules/@defold-typescript/types/src/lifecycle.ts": readAmbient("src/lifecycle.ts"),
60
67
  // Mirror the consumer-facing re-exports of the real package index so a user
61
68
  // file can `import { defineScript }` and `import type { Hash, Vector3 }`.
@@ -131,7 +138,14 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
131
138
  // Don't cross-check the seeded ambient .d.ts surface against itself; we only
132
139
  // care about diagnostics on user files (mirrors the editor's skipLibCheck).
133
140
  skipLibCheck: true,
134
- luaPlugins: [{ plugin: lifecycleErasurePlugin }],
141
+ // Defold scripts are not OO: free helper functions never receive a context,
142
+ // so suppress TSTL's implicit `self` parameter and the `_G` call-site filler.
143
+ noImplicitSelf: true,
144
+ luaPlugins: [
145
+ { plugin: lifecycleErasurePlugin },
146
+ { plugin: messageGuardLoweringPlugin },
147
+ { plugin: messageDispatchLoweringPlugin },
148
+ ],
135
149
  });
136
150
 
137
151
  return collectOutputs(result.transpiledFiles, result.diagnostics, userKeys);