@defold-typescript/transpiler 0.40.0 → 0.41.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.d.ts CHANGED
@@ -32,6 +32,8 @@ export type { SceneMessage } from "./scene-text-format";
32
32
  export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
33
33
  export type { TranspileSession } from "./session";
34
34
  export { createTranspileSession } from "./session";
35
+ export type { TextScriptPropertyFinding } from "./text-script-property";
36
+ export { findTextScriptProperties } from "./text-script-property";
35
37
  export { TIMERS_REQUIRE_NAME } from "./timers-runtime";
36
38
  export type { TranspileDiagnostic, TranspileProjectInput, TranspileProjectResult, TranspileResult, } from "./transpile";
37
39
  export { transpile, transpileProject } from "./transpile";
package/dist/index.js CHANGED
@@ -270,11 +270,11 @@ function emitInitMerge(fn, context, property) {
270
270
  const initFn = createFunctionExpression2(createBlock2([stateDecl, guard]), [createIdentifier2("self")], undefined, NodeFlags2.Declaration, fn);
271
271
  return [builderDecl, createAssignmentStatement(createIdentifier2("init"), initFn, property)];
272
272
  }
273
- function emitPropertyRegistrations(property, context) {
273
+ function propertyMembers(property) {
274
274
  if (!ts2.isPropertyAssignment(property) || !ts2.isObjectLiteralExpression(property.initializer)) {
275
275
  return [];
276
276
  }
277
- const registrations = [];
277
+ const members = [];
278
278
  for (const member of property.initializer.properties) {
279
279
  if (!ts2.isPropertyAssignment(member)) {
280
280
  continue;
@@ -283,21 +283,34 @@ function emitPropertyRegistrations(property, context) {
283
283
  if (!ts2.isIdentifier(key) && !ts2.isStringLiteral(key)) {
284
284
  continue;
285
285
  }
286
- registrations.push(createExpressionStatement(createCallExpression2(createTableIndexExpression(createIdentifier2("go"), createStringLiteral2("property")), [createStringLiteral2(key.text), context.transformExpression(member.initializer)])));
286
+ members.push({ name: key.text, initializer: member.initializer });
287
287
  }
288
- return registrations;
288
+ return members;
289
289
  }
290
- function eraseFactoryCall(expression, context) {
290
+ function factoryHooks(expression, checker) {
291
291
  if (!ts2.isCallExpression(expression)) {
292
292
  return;
293
293
  }
294
- if (!resolvesToFactoryExport(expression.expression, context.checker)) {
294
+ if (!resolvesToFactoryExport(expression.expression, checker)) {
295
295
  return;
296
296
  }
297
297
  const hooks = expression.arguments[0];
298
298
  if (hooks === undefined || !ts2.isObjectLiteralExpression(hooks)) {
299
299
  return;
300
300
  }
301
+ return hooks;
302
+ }
303
+ function isPropertiesHook(property) {
304
+ return hookName(property) === "properties";
305
+ }
306
+ function emitPropertyRegistrations(property, context) {
307
+ return propertyMembers(property).map(({ name, initializer }) => createExpressionStatement(createCallExpression2(createTableIndexExpression(createIdentifier2("go"), createStringLiteral2("property")), [createStringLiteral2(name), context.transformExpression(initializer)])));
308
+ }
309
+ function eraseFactoryCall(expression, context) {
310
+ const hooks = factoryHooks(expression, context.checker);
311
+ if (hooks === undefined) {
312
+ return;
313
+ }
301
314
  const registrations = [];
302
315
  const statements = [];
303
316
  for (const property of hooks.properties) {
@@ -2061,176 +2074,13 @@ function computeOutputRel(rel, config, kind) {
2061
2074
  import * as ts9 from "typescript";
2062
2075
  import * as tstl from "typescript-to-lua";
2063
2076
 
2064
- // src/array-index-option-lowering.ts
2077
+ // src/message-guard-lowering.ts
2065
2078
  import * as ts5 from "typescript";
2066
2079
  import {
2067
2080
  createBinaryExpression as createBinaryExpression3,
2068
- createNumericLiteral,
2069
- getOriginalPos,
2070
- isCallExpression as isCallExpression3,
2071
- isNumericLiteral as isNumericLiteral3,
2072
- isStringLiteral as isStringLiteral5,
2073
- isTableExpression,
2074
- SyntaxKind as SyntaxKind7,
2075
- setNodePosition
2076
- } from "typescript-to-lua";
2077
- var ONE_BASED_INDEX_OPTION_APIS = new Set([
2078
- "go.get",
2079
- "go.set",
2080
- "gui.get",
2081
- "gui.set"
2082
- ]);
2083
- var INDEX_OPTION_VARIABLE_MESSAGE = "`options.index` is zero-based and is converted to Defold's 1-based index only in an object literal written at the call; pass the options inline.";
2084
- var INDEX_OPTION_SPREAD_MESSAGE = "A spread carrying `index` cannot be converted to Defold's 1-based index; write `index` explicitly after the spread.";
2085
- var INDEX_OPTION_UNDEFINED_MESSAGE = "`options.index` may be undefined here, so it cannot be converted to Defold's 1-based index; pass a number, or leave `index` out.";
2086
- var OPTIONS_PARAMETER = "options";
2087
- var INDEX_KEY = "index";
2088
- var loweredLiterals = new WeakSet;
2089
- function oneBasedApi(declaration, checker) {
2090
- let node = declaration;
2091
- while (node !== undefined && !ts5.isFunctionDeclaration(node))
2092
- node = node.parent;
2093
- if (node?.name === undefined)
2094
- return false;
2095
- const symbol = checker.getSymbolAtLocation(node.name);
2096
- if (symbol === undefined)
2097
- return false;
2098
- const fqn = checker.getFullyQualifiedName(symbol);
2099
- return fqn.startsWith("global.") && ONE_BASED_INDEX_OPTION_APIS.has(fqn.slice("global.".length));
2100
- }
2101
- function optionsArgument(call, checker) {
2102
- const declaration = checker.getResolvedSignature(call)?.declaration;
2103
- if (declaration === undefined || !ts5.isFunctionLike(declaration))
2104
- return;
2105
- if (!oneBasedApi(declaration, checker))
2106
- return;
2107
- const index = declaration.parameters.findIndex((parameter) => parameter.dotDotDotToken === undefined && ts5.isIdentifier(parameter.name) && parameter.name.text === OPTIONS_PARAMETER);
2108
- return index === -1 ? undefined : call.arguments[index];
2109
- }
2110
- function unwrap(expression) {
2111
- let current = expression;
2112
- while (ts5.isParenthesizedExpression(current) || ts5.isAsExpression(current) || ts5.isSatisfiesExpression(current) || ts5.isNonNullExpression(current) || ts5.isTypeAssertionExpression(current)) {
2113
- current = current.expression;
2114
- }
2115
- return current;
2116
- }
2117
- function someConstituent(type, predicate) {
2118
- return type.isUnion() ? type.types.some(predicate) : predicate(type);
2119
- }
2120
- function carriesIndex(type) {
2121
- return someConstituent(type, (t) => t.getProperty(INDEX_KEY) !== undefined);
2122
- }
2123
- function admitsUndefined(type) {
2124
- return someConstituent(type, (t) => (t.flags & (ts5.TypeFlags.Undefined | ts5.TypeFlags.Void)) !== 0);
2125
- }
2126
- function propertyName(property, checker) {
2127
- const name = property.name;
2128
- if (name === undefined)
2129
- return;
2130
- if (ts5.isIdentifier(name) || ts5.isStringLiteral(name) || ts5.isNumericLiteral(name)) {
2131
- return name.text;
2132
- }
2133
- if (ts5.isComputedPropertyName(name)) {
2134
- const type = checker.getTypeAtLocation(name.expression);
2135
- if (type.isStringLiteral())
2136
- return type.value;
2137
- }
2138
- return;
2139
- }
2140
- function indexValueType(property, checker) {
2141
- if (ts5.isPropertyAssignment(property))
2142
- return checker.getTypeAtLocation(property.initializer);
2143
- if (ts5.isShorthandPropertyAssignment(property))
2144
- return checker.getTypeAtLocation(property.name);
2145
- return;
2146
- }
2147
- function rejection(node, messageText) {
2148
- const file = node.getSourceFile();
2149
- const start = node.getStart(file);
2150
- return {
2151
- file,
2152
- start,
2153
- length: node.getEnd() - start,
2154
- category: ts5.DiagnosticCategory.Error,
2155
- code: 0,
2156
- messageText
2157
- };
2158
- }
2159
- function rejectOptions(options, checker) {
2160
- const literal = unwrap(options);
2161
- if (!ts5.isObjectLiteralExpression(literal)) {
2162
- return carriesIndex(checker.getTypeAtLocation(literal)) ? rejection(options, INDEX_OPTION_VARIABLE_MESSAGE) : undefined;
2163
- }
2164
- let uncoveredSpread;
2165
- let explicitIndex;
2166
- for (const property of literal.properties) {
2167
- if (ts5.isSpreadAssignment(property)) {
2168
- if (carriesIndex(checker.getTypeAtLocation(property.expression)))
2169
- uncoveredSpread = property;
2170
- } else if (propertyName(property, checker) === INDEX_KEY) {
2171
- uncoveredSpread = undefined;
2172
- explicitIndex = property;
2173
- }
2174
- }
2175
- if (uncoveredSpread !== undefined)
2176
- return rejection(uncoveredSpread, INDEX_OPTION_SPREAD_MESSAGE);
2177
- const valueType = explicitIndex && indexValueType(explicitIndex, checker);
2178
- if (explicitIndex !== undefined && valueType !== undefined && admitsUndefined(valueType)) {
2179
- return rejection(explicitIndex, INDEX_OPTION_UNDEFINED_MESSAGE);
2180
- }
2181
- return;
2182
- }
2183
- function plusOne(value) {
2184
- if (isNumericLiteral3(value)) {
2185
- return setNodePosition(createNumericLiteral(value.value + 1), getOriginalPos(value));
2186
- }
2187
- return setNodePosition(createBinaryExpression3(value, createNumericLiteral(1), SyntaxKind7.AdditionOperator), getOriginalPos(value));
2188
- }
2189
- function offsetEffectiveIndexField(tables) {
2190
- for (const field of tables.flatMap((table) => table.fields).reverse()) {
2191
- if (field.key !== undefined && isStringLiteral5(field.key) && field.key.value === INDEX_KEY) {
2192
- field.value = plusOne(field.value);
2193
- return;
2194
- }
2195
- }
2196
- }
2197
- var arrayIndexOptionLoweringPlugin = {
2198
- visitors: {
2199
- [ts5.SyntaxKind.CallExpression]: (node, context) => {
2200
- const options = optionsArgument(node, context.checker);
2201
- if (options !== undefined) {
2202
- const diagnostic = rejectOptions(options, context.checker);
2203
- if (diagnostic !== undefined) {
2204
- context.diagnostics.push(diagnostic);
2205
- } else {
2206
- const literal = unwrap(options);
2207
- if (ts5.isObjectLiteralExpression(literal))
2208
- loweredLiterals.add(literal);
2209
- }
2210
- }
2211
- return context.superTransformExpression(node);
2212
- },
2213
- [ts5.SyntaxKind.ObjectLiteralExpression]: (node, context) => {
2214
- const result = context.superTransformExpression(node);
2215
- if (!loweredLiterals.has(node))
2216
- return result;
2217
- if (isTableExpression(result)) {
2218
- offsetEffectiveIndexField([result]);
2219
- } else if (isCallExpression3(result)) {
2220
- offsetEffectiveIndexField(result.params.filter(isTableExpression));
2221
- }
2222
- return result;
2223
- }
2224
- }
2225
- };
2226
-
2227
- // src/message-guard-lowering.ts
2228
- import * as ts6 from "typescript";
2229
- import {
2230
- createBinaryExpression as createBinaryExpression4,
2231
2081
  createCallExpression as createCallExpression3,
2232
2082
  createIdentifier as createIdentifier3,
2233
- SyntaxKind as SyntaxKind9
2083
+ SyntaxKind as SyntaxKind7
2234
2084
  } from "typescript-to-lua";
2235
2085
  var GUARD_MODULE = "@defold-typescript/types";
2236
2086
  var GUARD_NAME = "isMessage";
@@ -2239,7 +2089,7 @@ function resolvesToGuardExport(callee, checker) {
2239
2089
  if (symbol === undefined) {
2240
2090
  return false;
2241
2091
  }
2242
- if (symbol.flags & ts6.SymbolFlags.Alias) {
2092
+ if (symbol.flags & ts5.SymbolFlags.Alias) {
2243
2093
  symbol = checker.getAliasedSymbol(symbol);
2244
2094
  }
2245
2095
  if (symbol.getName() !== GUARD_NAME) {
@@ -2253,12 +2103,12 @@ function resolvesToGuardExport(callee, checker) {
2253
2103
  }
2254
2104
  var messageGuardLoweringPlugin = {
2255
2105
  visitors: {
2256
- [ts6.SyntaxKind.CallExpression]: (node, context) => {
2106
+ [ts5.SyntaxKind.CallExpression]: (node, context) => {
2257
2107
  const [messageIdArg, , expectedArg] = node.arguments;
2258
2108
  if (node.arguments.length === 3 && messageIdArg !== undefined && expectedArg !== undefined && resolvesToGuardExport(node.expression, context.checker)) {
2259
2109
  const messageId = context.transformExpression(messageIdArg);
2260
2110
  const expected = context.transformExpression(expectedArg);
2261
- return createBinaryExpression4(messageId, createCallExpression3(createIdentifier3("hash"), [expected]), SyntaxKind9.EqualityOperator, node);
2111
+ return createBinaryExpression3(messageId, createCallExpression3(createIdentifier3("hash"), [expected]), SyntaxKind7.EqualityOperator, node);
2262
2112
  }
2263
2113
  return context.superTransformExpression(node);
2264
2114
  }
@@ -2266,34 +2116,64 @@ var messageGuardLoweringPlugin = {
2266
2116
  };
2267
2117
 
2268
2118
  // src/timers-lowering.ts
2269
- import * as ts7 from "typescript";
2119
+ import * as ts6 from "typescript";
2270
2120
  import { LuaLibFeature } from "typescript-to-lua";
2271
2121
  function isTimersSpecifier(node) {
2272
- return ts7.isStringLiteral(node) && node.text === TIMERS_MODULE_SPECIFIER;
2122
+ return ts6.isStringLiteral(node) && node.text === TIMERS_MODULE_SPECIFIER;
2273
2123
  }
2274
2124
  function importsTimersModule(sourceFile) {
2275
- return sourceFile.statements.some((statement) => ts7.isImportDeclaration(statement) && isTimersSpecifier(statement.moduleSpecifier));
2125
+ return sourceFile.statements.some((statement) => ts6.isImportDeclaration(statement) && isTimersSpecifier(statement.moduleSpecifier));
2276
2126
  }
2277
2127
  var TIMERS_REQUIRE_PATH = `@NoResolution:${TIMERS_REQUIRE_NAME}`;
2278
2128
  var timersLoweringPlugin = {
2279
2129
  visitors: {
2280
- [ts7.SyntaxKind.ImportDeclaration]: (node, context) => {
2130
+ [ts6.SyntaxKind.ImportDeclaration]: (node, context) => {
2281
2131
  if (!isTimersSpecifier(node.moduleSpecifier)) {
2282
2132
  return context.superTransformStatements(node);
2283
2133
  }
2284
2134
  context.usedLuaLibFeatures.add(LuaLibFeature.Promise);
2285
2135
  context.usedLuaLibFeatures.add(LuaLibFeature.New);
2286
- const rewritten = ts7.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts7.factory.createStringLiteral(TIMERS_REQUIRE_PATH), node.attributes);
2136
+ const rewritten = ts6.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts6.factory.createStringLiteral(TIMERS_REQUIRE_PATH), node.attributes);
2287
2137
  return context.superTransformStatements(rewritten);
2288
2138
  }
2289
2139
  }
2290
2140
  };
2291
2141
 
2142
+ // src/type-application-erasure.ts
2143
+ import * as ts7 from "typescript";
2144
+ var TYPE_APPLICATION_APIS = new Set(["go.get", "go.set"]);
2145
+ function isTypeApplication(call, checker) {
2146
+ if (call.arguments.length !== 0)
2147
+ return false;
2148
+ const declaration = checker.getResolvedSignature(call)?.declaration;
2149
+ if (declaration === undefined || !ts7.isFunctionDeclaration(declaration))
2150
+ return false;
2151
+ if (declaration.parameters.length !== 0 || declaration.typeParameters === undefined)
2152
+ return false;
2153
+ if (declaration.name === undefined)
2154
+ return false;
2155
+ const symbol = checker.getSymbolAtLocation(declaration.name);
2156
+ if (symbol === undefined)
2157
+ return false;
2158
+ const fqn = checker.getFullyQualifiedName(symbol);
2159
+ return fqn.startsWith("global.") && TYPE_APPLICATION_APIS.has(fqn.slice("global.".length));
2160
+ }
2161
+ var typeApplicationErasurePlugin = {
2162
+ visitors: {
2163
+ [ts7.SyntaxKind.CallExpression]: (node, context) => {
2164
+ if (isTypeApplication(node, context.checker)) {
2165
+ return context.transformExpression(node.expression);
2166
+ }
2167
+ return context.superTransformExpression(node);
2168
+ }
2169
+ }
2170
+ };
2171
+
2292
2172
  // src/window-event-guard-lowering.ts
2293
2173
  import * as ts8 from "typescript";
2294
2174
  import {
2295
- createBinaryExpression as createBinaryExpression5,
2296
- SyntaxKind as SyntaxKind12
2175
+ createBinaryExpression as createBinaryExpression4,
2176
+ SyntaxKind as SyntaxKind11
2297
2177
  } from "typescript-to-lua";
2298
2178
  var GUARD_MODULE2 = "@defold-typescript/types";
2299
2179
  var GUARD_NAME2 = "isWindowEvent";
@@ -2321,7 +2201,7 @@ var windowEventGuardLoweringPlugin = {
2321
2201
  if (node.arguments.length === 3 && eventArg !== undefined && expectedArg !== undefined && resolvesToGuardExport2(node.expression, context.checker)) {
2322
2202
  const event = context.transformExpression(eventArg);
2323
2203
  const expected = context.transformExpression(expectedArg);
2324
- return createBinaryExpression5(event, expected, SyntaxKind12.EqualityOperator, node);
2204
+ return createBinaryExpression4(event, expected, SyntaxKind11.EqualityOperator, node);
2325
2205
  }
2326
2206
  return context.superTransformExpression(node);
2327
2207
  }
@@ -2335,7 +2215,7 @@ var LUA_PLUGINS = [
2335
2215
  windowEventGuardLoweringPlugin,
2336
2216
  messageDispatchLoweringPlugin,
2337
2217
  timersLoweringPlugin,
2338
- arrayIndexOptionLoweringPlugin
2218
+ typeApplicationErasurePlugin
2339
2219
  ];
2340
2220
  var noopWriteFile = () => {};
2341
2221
  function isAmbient(fileName) {
@@ -2930,18 +2810,18 @@ import {
2930
2810
  createTableIndexExpression as createTableIndexExpression2,
2931
2811
  createVariableDeclarationStatement as createVariableDeclarationStatement3,
2932
2812
  isBlock as isBlock3,
2933
- isCallExpression as isCallExpression5,
2813
+ isCallExpression as isCallExpression4,
2934
2814
  isDoStatement,
2935
2815
  isForInStatement as isForInStatement2,
2936
2816
  isForStatement,
2937
2817
  isFunctionDefinition,
2938
2818
  isFunctionExpression as isFunctionExpression4,
2939
- isIdentifier as isIdentifier5,
2819
+ isIdentifier as isIdentifier4,
2940
2820
  isMethodCallExpression,
2941
2821
  isRepeatStatement,
2942
2822
  isReturnStatement as isReturnStatement2,
2943
- isStringLiteral as isStringLiteral8,
2944
- isTableExpression as isTableExpression2,
2823
+ isStringLiteral as isStringLiteral6,
2824
+ isTableExpression,
2945
2825
  isVariableDeclarationStatement,
2946
2826
  LuaPrinter
2947
2827
  } from "typescript-to-lua";
@@ -3013,7 +2893,7 @@ function freeNames(statements) {
3013
2893
  if (node === undefined) {
3014
2894
  return;
3015
2895
  }
3016
- if (isIdentifier5(node)) {
2896
+ if (isIdentifier4(node)) {
3017
2897
  if (!isBound(node.text)) {
3018
2898
  free.add(node.text);
3019
2899
  }
@@ -3119,14 +2999,14 @@ function resolvedRequirePaths(sourceFile, checker) {
3119
2999
  function resolveMovedRequires(statements, paths) {
3120
3000
  for (const statement of statements) {
3121
3001
  forEachLuaNode(statement, (node) => {
3122
- if (!isCallExpression5(node) || !isIdentifier5(node.expression)) {
3002
+ if (!isCallExpression4(node) || !isIdentifier4(node.expression)) {
3123
3003
  return;
3124
3004
  }
3125
3005
  if (node.expression.text !== "require" || node.params.length !== 1) {
3126
3006
  return;
3127
3007
  }
3128
3008
  const [argument] = node.params;
3129
- if (argument === undefined || !isStringLiteral8(argument)) {
3009
+ if (argument === undefined || !isStringLiteral6(argument)) {
3130
3010
  return;
3131
3011
  }
3132
3012
  const resolved = paths.get(argument.value);
@@ -3142,14 +3022,14 @@ function withResolvedRequire(statement, paths) {
3142
3022
  }
3143
3023
  const name = statement.left[0];
3144
3024
  const [value] = statement.right ?? [];
3145
- if (name === undefined || value === undefined || !isCallExpression5(value)) {
3025
+ if (name === undefined || value === undefined || !isCallExpression4(value)) {
3146
3026
  return statement;
3147
3027
  }
3148
- if (!isIdentifier5(value.expression) || value.expression.text !== "require") {
3028
+ if (!isIdentifier4(value.expression) || value.expression.text !== "require") {
3149
3029
  return statement;
3150
3030
  }
3151
3031
  const [argument] = value.params;
3152
- if (argument === undefined || !isStringLiteral8(argument)) {
3032
+ if (argument === undefined || !isStringLiteral6(argument)) {
3153
3033
  return statement;
3154
3034
  }
3155
3035
  const resolved = paths.get(argument.value);
@@ -3185,7 +3065,7 @@ function copiedPrelude(halves, importBindings) {
3185
3065
  function namesIdentifier(statement, names) {
3186
3066
  let found = false;
3187
3067
  forEachLuaNode(statement, (node) => {
3188
- if (isIdentifier5(node) && names.has(node.text)) {
3068
+ if (isIdentifier4(node) && names.has(node.text)) {
3189
3069
  found = true;
3190
3070
  }
3191
3071
  });
@@ -3196,14 +3076,14 @@ function isExportsTableInit(statement) {
3196
3076
  return false;
3197
3077
  }
3198
3078
  const [single] = statement.right ?? [];
3199
- return statement.left[0]?.text === EXPORTS_NAME && single !== undefined && isTableExpression2(single);
3079
+ return statement.left[0]?.text === EXPORTS_NAME && single !== undefined && isTableExpression(single);
3200
3080
  }
3201
3081
  function isExportsReturn(statement) {
3202
3082
  if (!isReturnStatement2(statement) || statement.expressions.length !== 1) {
3203
3083
  return false;
3204
3084
  }
3205
3085
  const [single] = statement.expressions;
3206
- return single !== undefined && isIdentifier5(single) && single.text === EXPORTS_NAME;
3086
+ return single !== undefined && isIdentifier4(single) && single.text === EXPORTS_NAME;
3207
3087
  }
3208
3088
  function isHoistedNameDeclaration(statement) {
3209
3089
  return isVariableDeclarationStatement(statement) && statement.right === undefined;
@@ -3631,7 +3511,7 @@ function transpileProject(input) {
3631
3511
  { plugin: windowEventGuardLoweringPlugin },
3632
3512
  { plugin: messageDispatchLoweringPlugin },
3633
3513
  { plugin: timersLoweringPlugin },
3634
- { plugin: arrayIndexOptionLoweringPlugin },
3514
+ { plugin: typeApplicationErasurePlugin },
3635
3515
  { plugin: companionEmit.plugin }
3636
3516
  ]
3637
3517
  });
@@ -3662,7 +3542,7 @@ function compilerOptions(companionEmit) {
3662
3542
  { plugin: windowEventGuardLoweringPlugin },
3663
3543
  { plugin: messageDispatchLoweringPlugin },
3664
3544
  { plugin: timersLoweringPlugin },
3665
- { plugin: arrayIndexOptionLoweringPlugin },
3545
+ { plugin: typeApplicationErasurePlugin },
3666
3546
  { plugin: companionEmit.plugin }
3667
3547
  ]
3668
3548
  };
@@ -3779,8 +3659,71 @@ function createTranspileSession() {
3779
3659
  getProgram: () => program
3780
3660
  };
3781
3661
  }
3782
- // src/url-address-slots.ts
3662
+ // src/text-script-property.ts
3783
3663
  import * as ts14 from "typescript";
3664
+ function findTextScriptProperties(program, fileNames) {
3665
+ const checker = program.getTypeChecker();
3666
+ const findings = [];
3667
+ for (const file of fileNames) {
3668
+ const sourceFile = program.getSourceFile(file);
3669
+ if (sourceFile === undefined) {
3670
+ continue;
3671
+ }
3672
+ const located = [];
3673
+ const visit = (node) => {
3674
+ if (ts14.isCallExpression(node)) {
3675
+ const hooks = factoryHooks(node, checker);
3676
+ for (const property of hooks?.properties ?? []) {
3677
+ if (!isPropertiesHook(property)) {
3678
+ continue;
3679
+ }
3680
+ for (const { name, initializer } of propertyMembers(property)) {
3681
+ if (mayHoldString(checker.getTypeAtLocation(initializer), checker)) {
3682
+ located.push({ name, value: initializer });
3683
+ }
3684
+ }
3685
+ }
3686
+ }
3687
+ ts14.forEachChild(node, visit);
3688
+ };
3689
+ visit(sourceFile);
3690
+ for (const call of findDirectGoPropertyCalls(sourceFile)) {
3691
+ const [nameArgument, value] = call.arguments;
3692
+ if (nameArgument === undefined || value === undefined) {
3693
+ continue;
3694
+ }
3695
+ if (mayHoldString(checker.getTypeAtLocation(value), checker)) {
3696
+ const name = ts14.isStringLiteralLike(nameArgument) ? nameArgument.text : nameArgument.getText(sourceFile);
3697
+ located.push({ name, value });
3698
+ }
3699
+ }
3700
+ located.sort((left, right) => left.value.getStart(sourceFile) - right.value.getStart(sourceFile));
3701
+ for (const { name, value } of located) {
3702
+ const position = sourceFile.getLineAndCharacterOfPosition(value.getStart(sourceFile));
3703
+ findings.push({ name, file, line: position.line + 1, column: position.character + 1 });
3704
+ }
3705
+ }
3706
+ return findings;
3707
+ }
3708
+ function mayHoldString(type, checker, visited = new Set) {
3709
+ if (visited.has(type)) {
3710
+ return false;
3711
+ }
3712
+ visited.add(type);
3713
+ if (type.flags & ts14.TypeFlags.StringLike) {
3714
+ return true;
3715
+ }
3716
+ if (type.isUnionOrIntersection()) {
3717
+ return type.types.some((member) => mayHoldString(member, checker, visited));
3718
+ }
3719
+ if (type.flags & ts14.TypeFlags.Instantiable) {
3720
+ const constraint = checker.getBaseConstraintOfType(type);
3721
+ return constraint !== undefined && constraint !== type ? mayHoldString(constraint, checker, visited) : false;
3722
+ }
3723
+ return false;
3724
+ }
3725
+ // src/url-address-slots.ts
3726
+ import * as ts15 from "typescript";
3784
3727
  var ADDRESS_CLASSES = {
3785
3728
  "game-object": true,
3786
3729
  component: true,
@@ -3799,7 +3742,7 @@ function isFragmentCaret(slot, position) {
3799
3742
  return slot.fragmentStart !== -1 && position >= slot.fragmentStart;
3800
3743
  }
3801
3744
  function tableKey(checker, callee) {
3802
- const symbol = checker.getSymbolAtLocation(ts14.isIdentifier(callee) ? callee : callee.name);
3745
+ const symbol = checker.getSymbolAtLocation(ts15.isIdentifier(callee) ? callee : callee.name);
3803
3746
  if (!symbol)
3804
3747
  return;
3805
3748
  const fqn = checker.getFullyQualifiedName(symbol);
@@ -3807,43 +3750,43 @@ function tableKey(checker, callee) {
3807
3750
  }
3808
3751
  function parameterName(checker, call, index) {
3809
3752
  const declaration = checker.getResolvedSignature(call)?.declaration;
3810
- if (!declaration || !ts14.isFunctionLike(declaration))
3753
+ if (!declaration || !ts15.isFunctionLike(declaration))
3811
3754
  return;
3812
3755
  const parameter = declaration.parameters[index];
3813
- if (!parameter || parameter.dotDotDotToken || !ts14.isIdentifier(parameter.name))
3756
+ if (!parameter || parameter.dotDotDotToken || !ts15.isIdentifier(parameter.name))
3814
3757
  return;
3815
3758
  return parameter.name.text;
3816
3759
  }
3817
3760
  var EQUALITY_OPERATORS = new Set([
3818
- ts14.SyntaxKind.EqualsEqualsEqualsToken,
3819
- ts14.SyntaxKind.ExclamationEqualsEqualsToken,
3820
- ts14.SyntaxKind.EqualsEqualsToken,
3821
- ts14.SyntaxKind.ExclamationEqualsToken
3761
+ ts15.SyntaxKind.EqualsEqualsEqualsToken,
3762
+ ts15.SyntaxKind.ExclamationEqualsEqualsToken,
3763
+ ts15.SyntaxKind.EqualsEqualsToken,
3764
+ ts15.SyntaxKind.ExclamationEqualsToken
3822
3765
  ]);
3823
3766
  function comparisonSatisfied(checker, entry, call) {
3824
3767
  if (entry.comparedParameter === undefined)
3825
3768
  return true;
3826
3769
  let node = call;
3827
- while (ts14.isParenthesizedExpression(node.parent))
3770
+ while (ts15.isParenthesizedExpression(node.parent))
3828
3771
  node = node.parent;
3829
3772
  const comparison = node.parent;
3830
- if (!ts14.isBinaryExpression(comparison))
3773
+ if (!ts15.isBinaryExpression(comparison))
3831
3774
  return false;
3832
3775
  if (!EQUALITY_OPERATORS.has(comparison.operatorToken.kind))
3833
3776
  return false;
3834
3777
  const other = comparison.left === node ? comparison.right : comparison.left;
3835
3778
  const symbol = checker.getSymbolAtLocation(other);
3836
- return (symbol?.declarations ?? []).some((declaration) => ts14.isParameter(declaration) && ts14.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
3779
+ return (symbol?.declarations ?? []).some((declaration) => ts15.isParameter(declaration) && ts15.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
3837
3780
  }
3838
3781
  function classifiedEntryOfArgument(checker, table, argument) {
3839
3782
  const call = argument.parent;
3840
- if (!ts14.isCallExpression(call))
3783
+ if (!ts15.isCallExpression(call))
3841
3784
  return;
3842
3785
  const argumentIndex = call.arguments.indexOf(argument);
3843
3786
  if (argumentIndex === -1)
3844
3787
  return;
3845
3788
  const callee = call.expression;
3846
- if (!ts14.isPropertyAccessExpression(callee) && !ts14.isIdentifier(callee))
3789
+ if (!ts15.isPropertyAccessExpression(callee) && !ts15.isIdentifier(callee))
3847
3790
  return;
3848
3791
  const fqn = tableKey(checker, callee);
3849
3792
  if (fqn === undefined)
@@ -3860,10 +3803,10 @@ function classifiedEntryOfArgument(checker, table, argument) {
3860
3803
  }
3861
3804
  function slotThroughHashedCall(checker, table, literal) {
3862
3805
  const call = literal.parent;
3863
- if (!ts14.isCallExpression(call))
3806
+ if (!ts15.isCallExpression(call))
3864
3807
  return;
3865
3808
  const callee = call.expression;
3866
- if (!ts14.isPropertyAccessExpression(callee) && !ts14.isIdentifier(callee))
3809
+ if (!ts15.isPropertyAccessExpression(callee) && !ts15.isIdentifier(callee))
3867
3810
  return;
3868
3811
  if (tableKey(checker, callee) !== "hash")
3869
3812
  return;
@@ -3881,24 +3824,24 @@ function addressTextOf(checker, entry, call) {
3881
3824
  if (entry.addressParameter === undefined)
3882
3825
  return;
3883
3826
  const declaration = checker.getResolvedSignature(call)?.declaration;
3884
- if (!declaration || !ts14.isFunctionLike(declaration))
3827
+ if (!declaration || !ts15.isFunctionLike(declaration))
3885
3828
  return;
3886
- const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts14.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
3829
+ const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts15.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
3887
3830
  if (index === -1)
3888
3831
  return;
3889
3832
  const argument = call.arguments[index];
3890
- return argument && ts14.isStringLiteralLike(argument) ? argument.text : undefined;
3833
+ return argument && ts15.isStringLiteralLike(argument) ? argument.text : undefined;
3891
3834
  }
3892
3835
  function innermostLiteralAt(sourceFile, position) {
3893
3836
  let found;
3894
3837
  const visit = (node) => {
3895
3838
  if (position < node.getStart(sourceFile) || position > node.getEnd())
3896
3839
  return;
3897
- if (ts14.isStringLiteralLike(node))
3840
+ if (ts15.isStringLiteralLike(node))
3898
3841
  found = node;
3899
- ts14.forEachChild(node, visit);
3842
+ ts15.forEachChild(node, visit);
3900
3843
  };
3901
- ts14.forEachChild(sourceFile, visit);
3844
+ ts15.forEachChild(sourceFile, visit);
3902
3845
  return found;
3903
3846
  }
3904
3847
  function resolveClassifiedSlotAtPosition(input) {
@@ -3932,23 +3875,23 @@ function resolveClassifiedSlotAtPosition(input) {
3932
3875
  };
3933
3876
  }
3934
3877
  // src/url-cross-world-address.ts
3935
- import * as ts16 from "typescript";
3878
+ import * as ts17 from "typescript";
3936
3879
 
3937
3880
  // src/url-address-literals.ts
3938
- import * as ts15 from "typescript";
3881
+ import * as ts16 from "typescript";
3939
3882
  function isAmbient2(fileName) {
3940
3883
  return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
3941
3884
  }
3942
3885
  function referenceTargetSymbol(type) {
3943
- if ((type.flags & ts15.TypeFlags.Object) === 0)
3886
+ if ((type.flags & ts16.TypeFlags.Object) === 0)
3944
3887
  return;
3945
- if ((type.objectFlags & ts15.ObjectFlags.Reference) === 0)
3888
+ if ((type.objectFlags & ts16.ObjectFlags.Reference) === 0)
3946
3889
  return;
3947
3890
  return type.target.symbol;
3948
3891
  }
3949
3892
  function ambientHashAliasTargets(checker, anchor) {
3950
3893
  const targets = new Set;
3951
- for (const symbol of checker.getSymbolsInScope(anchor, ts15.SymbolFlags.Type)) {
3894
+ for (const symbol of checker.getSymbolsInScope(anchor, ts16.SymbolFlags.Type)) {
3952
3895
  if (symbol.name !== "Hash")
3953
3896
  continue;
3954
3897
  const target = referenceTargetSymbol(checker.getDeclaredTypeOfSymbol(symbol));
@@ -3964,7 +3907,7 @@ function canonicalHashSymbols(checker, program) {
3964
3907
  if (!anchor)
3965
3908
  return canonical;
3966
3909
  const aliased = ambientHashAliasTargets(checker, anchor);
3967
- for (const symbol of checker.getSymbolsInScope(anchor, ts15.SymbolFlags.Function)) {
3910
+ for (const symbol of checker.getSymbolsInScope(anchor, ts16.SymbolFlags.Function)) {
3968
3911
  if (symbol.name !== "hash")
3969
3912
  continue;
3970
3913
  const declaration = symbol.declarations?.[0];
@@ -3986,7 +3929,7 @@ function hashedSourceOfType(checker, type, canonical) {
3986
3929
  return source?.isStringLiteral() ? source.value : undefined;
3987
3930
  }
3988
3931
  function staticAddressTextOf(checker, node, canonical) {
3989
- return ts15.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
3932
+ return ts16.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
3990
3933
  }
3991
3934
 
3992
3935
  // src/url-cross-world-address.ts
@@ -4010,7 +3953,7 @@ function checkCrossWorldAddresses(input) {
4010
3953
  if (worlds.length === 0)
4011
3954
  continue;
4012
3955
  const visit = (node) => {
4013
- if (ts16.isExpression(node)) {
3956
+ if (ts17.isExpression(node)) {
4014
3957
  const entry = addressEntryOfArgument(checker, table, node);
4015
3958
  if (entry !== undefined && slotRejectsForeignSocket(entry)) {
4016
3959
  const address = staticAddressTextOf(checker, node, canonical);
@@ -4027,14 +3970,14 @@ function checkCrossWorldAddresses(input) {
4027
3970
  }
4028
3971
  }
4029
3972
  }
4030
- ts16.forEachChild(node, visit);
3973
+ ts17.forEachChild(node, visit);
4031
3974
  };
4032
- ts16.forEachChild(sourceFile, visit);
3975
+ ts17.forEachChild(sourceFile, visit);
4033
3976
  }
4034
3977
  return findings;
4035
3978
  }
4036
3979
  // src/url-fragment-reachability.ts
4037
- import * as ts17 from "typescript";
3980
+ import * as ts18 from "typescript";
4038
3981
  function pathOfKey(key) {
4039
3982
  const socket = socketOfAddress(key);
4040
3983
  return socket === undefined ? key : key.slice(socket.length + 1);
@@ -4079,7 +4022,7 @@ function checkUrlFragmentReachability(input) {
4079
4022
  if (isAmbient2(sourceFile.fileName))
4080
4023
  continue;
4081
4024
  const visit = (node) => {
4082
- if (ts17.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
4025
+ if (ts18.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
4083
4026
  const text = staticAddressTextOf(checker, node, canonical);
4084
4027
  if (text !== undefined) {
4085
4028
  const hash = text.indexOf("#");
@@ -4098,9 +4041,9 @@ function checkUrlFragmentReachability(input) {
4098
4041
  }
4099
4042
  }
4100
4043
  }
4101
- ts17.forEachChild(node, visit);
4044
+ ts18.forEachChild(node, visit);
4102
4045
  };
4103
- ts17.forEachChild(sourceFile, visit);
4046
+ ts18.forEachChild(sourceFile, visit);
4104
4047
  }
4105
4048
  return { kind: "checked", findings };
4106
4049
  }
@@ -4145,6 +4088,7 @@ export {
4145
4088
  displayPathOf,
4146
4089
  findEmittedRequires,
4147
4090
  findMainEntryFactoryImports,
4091
+ findTextScriptProperties,
4148
4092
  getProgramDiagnostics,
4149
4093
  guiScriptResourceOf,
4150
4094
  guiScriptResourcesOf,
@@ -3,5 +3,12 @@ import { type Plugin } from "typescript-to-lua";
3
3
  export declare const FACTORY_MODULE = "@defold-typescript/types";
4
4
  export declare const FACTORY_NAMES: Set<string>;
5
5
  export declare function resolvesToFactoryExport(callee: ts.Expression, checker: ts.TypeChecker): boolean;
6
+ export interface PropertyMember {
7
+ readonly name: string;
8
+ readonly initializer: ts.Expression;
9
+ }
10
+ export declare function propertyMembers(property: ts.ObjectLiteralElementLike): PropertyMember[];
11
+ export declare function factoryHooks(expression: ts.Expression, checker: ts.TypeChecker): ts.ObjectLiteralExpression | undefined;
12
+ export declare function isPropertiesHook(property: ts.ObjectLiteralElementLike): boolean;
6
13
  export declare function isFactoryOnlyImport(node: ts.ImportDeclaration): boolean;
7
14
  export declare const lifecycleErasurePlugin: Plugin;
@@ -0,0 +1,16 @@
1
+ import * as ts from "typescript";
2
+ export interface TextScriptPropertyFinding {
3
+ readonly name: string;
4
+ readonly file: string;
5
+ /** 1-based line of the default's initializer or the direct call's value. */
6
+ readonly line: number;
7
+ /** 1-based column of the default's initializer or the direct call's value. */
8
+ readonly column: number;
9
+ }
10
+ export declare function findTextScriptProperties(program: ts.Program, fileNames: readonly string[]): TextScriptPropertyFinding[];
11
+ /**
12
+ * Whether a value of `type` may be a string at runtime: a string-like type, a
13
+ * union or intersection with such a member, or a type parameter or other
14
+ * instantiable type whose constraint may hold one.
15
+ */
16
+ export declare function mayHoldString(type: ts.Type, checker: ts.TypeChecker, visited?: Set<ts.Type>): boolean;
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "typescript-to-lua";
2
+ export declare const TYPE_APPLICATION_APIS: ReadonlySet<string>;
3
+ export declare const typeApplicationErasurePlugin: Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.40.0",
3
+ "version": "0.41.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.40.0",
38
+ "@defold-typescript/types": "0.41.0",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript-to-lua": "^1.36.0"
41
41
  }
package/src/index.ts CHANGED
@@ -86,6 +86,8 @@ export type { SceneMessage } from "./scene-text-format";
86
86
  export { parseSceneTextFormat, SceneTextFormatError } from "./scene-text-format";
87
87
  export type { TranspileSession } from "./session";
88
88
  export { createTranspileSession } from "./session";
89
+ export type { TextScriptPropertyFinding } from "./text-script-property";
90
+ export { findTextScriptProperties } from "./text-script-property";
89
91
  export { TIMERS_REQUIRE_NAME } from "./timers-runtime";
90
92
  export type {
91
93
  TranspileDiagnostic,
@@ -208,18 +208,19 @@ function emitInitMerge(
208
208
  return [builderDecl, createAssignmentStatement(createIdentifier("init"), initFn, property)];
209
209
  }
210
210
 
211
- // The value-keyed `properties` field maps each `key: default` to a chunk-scope
212
- // `go.property("key", default)` the editor reads. Emitted before the hook
213
- // assignments and after the source's `local` consts, so a default referencing a
214
- // module const resolves.
215
- function emitPropertyRegistrations(
216
- property: ts.ObjectLiteralElementLike,
217
- context: TransformationContext,
218
- ): Statement[] {
211
+ export interface PropertyMember {
212
+ readonly name: string;
213
+ readonly initializer: ts.Expression;
214
+ }
215
+
216
+ // The `key: default` members of a factory's `properties` field that register as
217
+ // script properties: a property assignment keyed by an identifier or a string
218
+ // literal. Shared by the emitter and every analysis of what it registers.
219
+ export function propertyMembers(property: ts.ObjectLiteralElementLike): PropertyMember[] {
219
220
  if (!ts.isPropertyAssignment(property) || !ts.isObjectLiteralExpression(property.initializer)) {
220
221
  return [];
221
222
  }
222
- const registrations: Statement[] = [];
223
+ const members: PropertyMember[] = [];
223
224
  for (const member of property.initializer.properties) {
224
225
  if (!ts.isPropertyAssignment(member)) {
225
226
  continue;
@@ -228,32 +229,60 @@ function emitPropertyRegistrations(
228
229
  if (!ts.isIdentifier(key) && !ts.isStringLiteral(key)) {
229
230
  continue;
230
231
  }
231
- registrations.push(
232
- createExpressionStatement(
233
- createCallExpression(
234
- createTableIndexExpression(createIdentifier("go"), createStringLiteral("property")),
235
- [createStringLiteral(key.text), context.transformExpression(member.initializer)],
236
- ),
237
- ),
238
- );
232
+ members.push({ name: key.text, initializer: member.initializer });
239
233
  }
240
- return registrations;
234
+ return members;
241
235
  }
242
236
 
243
- function eraseFactoryCall(
237
+ // The hooks object literal of a call to a lifecycle factory, or `undefined` when
238
+ // the expression is not one the erasure rewrites.
239
+ export function factoryHooks(
244
240
  expression: ts.Expression,
245
- context: TransformationContext,
246
- ): Statement[] | undefined {
241
+ checker: ts.TypeChecker,
242
+ ): ts.ObjectLiteralExpression | undefined {
247
243
  if (!ts.isCallExpression(expression)) {
248
244
  return undefined;
249
245
  }
250
- if (!resolvesToFactoryExport(expression.expression, context.checker)) {
246
+ if (!resolvesToFactoryExport(expression.expression, checker)) {
251
247
  return undefined;
252
248
  }
253
249
  const hooks = expression.arguments[0];
254
250
  if (hooks === undefined || !ts.isObjectLiteralExpression(hooks)) {
255
251
  return undefined;
256
252
  }
253
+ return hooks;
254
+ }
255
+
256
+ export function isPropertiesHook(property: ts.ObjectLiteralElementLike): boolean {
257
+ return hookName(property) === "properties";
258
+ }
259
+
260
+ // The value-keyed `properties` field maps each `key: default` to a chunk-scope
261
+ // `go.property("key", default)` the editor reads. Emitted before the hook
262
+ // assignments and after the source's `local` consts, so a default referencing a
263
+ // module const resolves.
264
+ function emitPropertyRegistrations(
265
+ property: ts.ObjectLiteralElementLike,
266
+ context: TransformationContext,
267
+ ): Statement[] {
268
+ return propertyMembers(property).map(({ name, initializer }) =>
269
+ createExpressionStatement(
270
+ createCallExpression(
271
+ createTableIndexExpression(createIdentifier("go"), createStringLiteral("property")),
272
+ [createStringLiteral(name), context.transformExpression(initializer)],
273
+ ),
274
+ ),
275
+ );
276
+ }
277
+
278
+ function eraseFactoryCall(
279
+ expression: ts.Expression,
280
+ context: TransformationContext,
281
+ ): Statement[] | undefined {
282
+ const hooks = factoryHooks(expression, context.checker);
283
+ if (hooks === undefined) {
284
+ return undefined;
285
+ }
257
286
  const registrations: Statement[] = [];
258
287
  const statements: Statement[] = [];
259
288
  for (const property of hooks.properties) {
@@ -1,10 +1,10 @@
1
1
  import * as ts from "typescript";
2
2
  import * as tstl from "typescript-to-lua";
3
- import { arrayIndexOptionLoweringPlugin } from "./array-index-option-lowering";
4
3
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
5
4
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
6
5
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
7
6
  import { timersLoweringPlugin } from "./timers-lowering";
7
+ import { typeApplicationErasurePlugin } from "./type-application-erasure";
8
8
  import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
9
9
 
10
10
  // The same lua plugins the build path lists (transpile.ts / session.ts), so the
@@ -15,7 +15,7 @@ const LUA_PLUGINS: tstl.Plugin[] = [
15
15
  windowEventGuardLoweringPlugin,
16
16
  messageDispatchLoweringPlugin,
17
17
  timersLoweringPlugin,
18
- arrayIndexOptionLoweringPlugin,
18
+ typeApplicationErasurePlugin,
19
19
  ];
20
20
 
21
21
  const noopWriteFile: ts.WriteFileCallback = () => {};
package/src/session.ts CHANGED
@@ -3,7 +3,6 @@ import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import * as ts from "typescript";
5
5
  import * as tstl from "typescript-to-lua";
6
- import { arrayIndexOptionLoweringPlugin } from "./array-index-option-lowering";
7
6
  import { type CompanionEmitter, createCompanionEmitPlugin } from "./companion-emit";
8
7
  import { editorScriptErasurePlugin } from "./editor-script-erasure";
9
8
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
@@ -11,6 +10,7 @@ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
11
10
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
12
11
  import { timersLoweringPlugin } from "./timers-lowering";
13
12
  import { AMBIENT_FILES, collectOutputs, type TranspileProjectResult } from "./transpile";
13
+ import { typeApplicationErasurePlugin } from "./type-application-erasure";
14
14
  import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
15
15
 
16
16
  export interface TranspileSession {
@@ -44,7 +44,7 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
44
44
  { plugin: windowEventGuardLoweringPlugin },
45
45
  { plugin: messageDispatchLoweringPlugin },
46
46
  { plugin: timersLoweringPlugin },
47
- { plugin: arrayIndexOptionLoweringPlugin },
47
+ { plugin: typeApplicationErasurePlugin },
48
48
  { plugin: companionEmit.plugin },
49
49
  ],
50
50
  };
@@ -0,0 +1,100 @@
1
+ import * as ts from "typescript";
2
+ import { findDirectGoPropertyCalls } from "./go-property-direct-call";
3
+ import { factoryHooks, isPropertiesHook, propertyMembers } from "./lifecycle-erasure";
4
+
5
+ export interface TextScriptPropertyFinding {
6
+ readonly name: string;
7
+ readonly file: string;
8
+ /** 1-based line of the default's initializer or the direct call's value. */
9
+ readonly line: number;
10
+ /** 1-based column of the default's initializer or the direct call's value. */
11
+ readonly column: number;
12
+ }
13
+
14
+ // The script properties whose default may hold a string, declared either in a
15
+ // `properties` field or by a direct `go.property` call. Defold registers a
16
+ // string `go.property` only from 1.13.2, so a build for an older target must
17
+ // refuse them; typed rather than literal-driven, so a const holding a string
18
+ // counts.
19
+ export function findTextScriptProperties(
20
+ program: ts.Program,
21
+ fileNames: readonly string[],
22
+ ): TextScriptPropertyFinding[] {
23
+ const checker = program.getTypeChecker();
24
+ const findings: TextScriptPropertyFinding[] = [];
25
+
26
+ for (const file of fileNames) {
27
+ const sourceFile = program.getSourceFile(file);
28
+ if (sourceFile === undefined) {
29
+ continue;
30
+ }
31
+ const located: { name: string; value: ts.Node }[] = [];
32
+ const visit = (node: ts.Node): void => {
33
+ if (ts.isCallExpression(node)) {
34
+ const hooks = factoryHooks(node, checker);
35
+ for (const property of hooks?.properties ?? []) {
36
+ if (!isPropertiesHook(property)) {
37
+ continue;
38
+ }
39
+ for (const { name, initializer } of propertyMembers(property)) {
40
+ if (mayHoldString(checker.getTypeAtLocation(initializer), checker)) {
41
+ located.push({ name, value: initializer });
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ts.forEachChild(node, visit);
47
+ };
48
+ visit(sourceFile);
49
+ for (const call of findDirectGoPropertyCalls(sourceFile)) {
50
+ const [nameArgument, value] = call.arguments;
51
+ if (nameArgument === undefined || value === undefined) {
52
+ continue;
53
+ }
54
+ if (mayHoldString(checker.getTypeAtLocation(value), checker)) {
55
+ const name = ts.isStringLiteralLike(nameArgument)
56
+ ? nameArgument.text
57
+ : nameArgument.getText(sourceFile);
58
+ located.push({ name, value });
59
+ }
60
+ }
61
+ located.sort(
62
+ (left, right) => left.value.getStart(sourceFile) - right.value.getStart(sourceFile),
63
+ );
64
+ for (const { name, value } of located) {
65
+ const position = sourceFile.getLineAndCharacterOfPosition(value.getStart(sourceFile));
66
+ findings.push({ name, file, line: position.line + 1, column: position.character + 1 });
67
+ }
68
+ }
69
+
70
+ return findings;
71
+ }
72
+
73
+ /**
74
+ * Whether a value of `type` may be a string at runtime: a string-like type, a
75
+ * union or intersection with such a member, or a type parameter or other
76
+ * instantiable type whose constraint may hold one.
77
+ */
78
+ export function mayHoldString(
79
+ type: ts.Type,
80
+ checker: ts.TypeChecker,
81
+ visited: Set<ts.Type> = new Set(),
82
+ ): boolean {
83
+ if (visited.has(type)) {
84
+ return false;
85
+ }
86
+ visited.add(type);
87
+ if (type.flags & ts.TypeFlags.StringLike) {
88
+ return true;
89
+ }
90
+ if (type.isUnionOrIntersection()) {
91
+ return type.types.some((member) => mayHoldString(member, checker, visited));
92
+ }
93
+ if (type.flags & ts.TypeFlags.Instantiable) {
94
+ const constraint = checker.getBaseConstraintOfType(type);
95
+ return constraint !== undefined && constraint !== type
96
+ ? mayHoldString(constraint, checker, visited)
97
+ : false;
98
+ }
99
+ return false;
100
+ }
package/src/transpile.ts CHANGED
@@ -3,7 +3,6 @@ import { createRequire } from "node:module";
3
3
  import * as path from "node:path";
4
4
  import type * as ts from "typescript";
5
5
  import * as tstl from "typescript-to-lua";
6
- import { arrayIndexOptionLoweringPlugin } from "./array-index-option-lowering";
7
6
  import { createCompanionEmitPlugin } from "./companion-emit";
8
7
  import { editorScriptErasurePlugin } from "./editor-script-erasure";
9
8
  import {
@@ -15,6 +14,7 @@ import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
15
14
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
16
15
  import { importsTimersModule, timersLoweringPlugin } from "./timers-lowering";
17
16
  import { TIMERS_RUNTIME } from "./timers-runtime";
17
+ import { typeApplicationErasurePlugin } from "./type-application-erasure";
18
18
  import { windowEventGuardLoweringPlugin } from "./window-event-guard-lowering";
19
19
 
20
20
  export interface TranspileResult {
@@ -322,7 +322,7 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
322
322
  { plugin: windowEventGuardLoweringPlugin },
323
323
  { plugin: messageDispatchLoweringPlugin },
324
324
  { plugin: timersLoweringPlugin },
325
- { plugin: arrayIndexOptionLoweringPlugin },
325
+ { plugin: typeApplicationErasurePlugin },
326
326
  { plugin: companionEmit.plugin },
327
327
  ],
328
328
  });
@@ -0,0 +1,34 @@
1
+ import * as ts from "typescript";
2
+ import type { Expression, Plugin } from "typescript-to-lua";
3
+
4
+ // The Defold functions declared with a curried type-application overload,
5
+ // `go.get<P>()(url, property)`: the empty call only fixes `P` for the checker.
6
+ export const TYPE_APPLICATION_APIS: ReadonlySet<string> = new Set(["go.get", "go.set"]);
7
+
8
+ // Keyed by the declaration's fully qualified name: only a symbol declared in
9
+ // an ambient `declare global` block carries the `global.` prefix, so a
10
+ // project's own `go` never matches.
11
+ function isTypeApplication(call: ts.CallExpression, checker: ts.TypeChecker): boolean {
12
+ if (call.arguments.length !== 0) return false;
13
+ const declaration = checker.getResolvedSignature(call)?.declaration;
14
+ if (declaration === undefined || !ts.isFunctionDeclaration(declaration)) return false;
15
+ if (declaration.parameters.length !== 0 || declaration.typeParameters === undefined) return false;
16
+ if (declaration.name === undefined) return false;
17
+ const symbol = checker.getSymbolAtLocation(declaration.name);
18
+ if (symbol === undefined) return false;
19
+ const fqn = checker.getFullyQualifiedName(symbol);
20
+ return fqn.startsWith("global.") && TYPE_APPLICATION_APIS.has(fqn.slice("global.".length));
21
+ }
22
+
23
+ // `go.get<P>()` becomes `go.get`, so the curried call reaches the engine as
24
+ // `go.get(url, property)` and a stored `go.get<P>()` is the function itself.
25
+ export const typeApplicationErasurePlugin: Plugin = {
26
+ visitors: {
27
+ [ts.SyntaxKind.CallExpression]: (node, context): Expression => {
28
+ if (isTypeApplication(node, context.checker)) {
29
+ return context.transformExpression(node.expression);
30
+ }
31
+ return context.superTransformExpression(node);
32
+ },
33
+ },
34
+ };
@@ -1,6 +0,0 @@
1
- import { type Plugin } from "typescript-to-lua";
2
- export declare const ONE_BASED_INDEX_OPTION_APIS: ReadonlySet<string>;
3
- export declare const INDEX_OPTION_VARIABLE_MESSAGE = "`options.index` is zero-based and is converted to Defold's 1-based index only in an object literal written at the call; pass the options inline.";
4
- export declare const INDEX_OPTION_SPREAD_MESSAGE = "A spread carrying `index` cannot be converted to Defold's 1-based index; write `index` explicitly after the spread.";
5
- export declare const INDEX_OPTION_UNDEFINED_MESSAGE = "`options.index` may be undefined here, so it cannot be converted to Defold's 1-based index; pass a number, or leave `index` out.";
6
- export declare const arrayIndexOptionLoweringPlugin: Plugin;
@@ -1,212 +0,0 @@
1
- import * as ts from "typescript";
2
- import {
3
- createBinaryExpression,
4
- createNumericLiteral,
5
- type Expression,
6
- getOriginalPos,
7
- isCallExpression,
8
- isNumericLiteral,
9
- isStringLiteral,
10
- isTableExpression,
11
- type Plugin,
12
- SyntaxKind,
13
- setNodePosition,
14
- type TableExpression,
15
- } from "typescript-to-lua";
16
-
17
- // The Defold functions whose `options.index` upstream documents as "index into
18
- // array property (1 based)". TypeScript authors it zero-based; the lowering
19
- // adds the one Lua expects.
20
- export const ONE_BASED_INDEX_OPTION_APIS: ReadonlySet<string> = new Set([
21
- "go.get",
22
- "go.set",
23
- "gui.get",
24
- "gui.set",
25
- ]);
26
-
27
- export const INDEX_OPTION_VARIABLE_MESSAGE =
28
- "`options.index` is zero-based and is converted to Defold's 1-based index only in an object literal written at the call; pass the options inline.";
29
- export const INDEX_OPTION_SPREAD_MESSAGE =
30
- "A spread carrying `index` cannot be converted to Defold's 1-based index; write `index` explicitly after the spread.";
31
- export const INDEX_OPTION_UNDEFINED_MESSAGE =
32
- "`options.index` may be undefined here, so it cannot be converted to Defold's 1-based index; pass a number, or leave `index` out.";
33
-
34
- const OPTIONS_PARAMETER = "options";
35
- const INDEX_KEY = "index";
36
-
37
- const loweredLiterals = new WeakSet<ts.Node>();
38
-
39
- // Keyed like `tableKey` in url-address-slots.ts: only a symbol declared in an
40
- // ambient `declare global` block carries the `global.` prefix, so a project's
41
- // own `go` or `gui` never matches. Walking up from the signature declaration
42
- // reaches `go.get<P>()` from the function type its curried inner call resolves to.
43
- function oneBasedApi(declaration: ts.Declaration, checker: ts.TypeChecker): boolean {
44
- let node: ts.Node | undefined = declaration;
45
- while (node !== undefined && !ts.isFunctionDeclaration(node)) node = node.parent;
46
- if (node?.name === undefined) return false;
47
- const symbol = checker.getSymbolAtLocation(node.name);
48
- if (symbol === undefined) return false;
49
- const fqn = checker.getFullyQualifiedName(symbol);
50
- return fqn.startsWith("global.") && ONE_BASED_INDEX_OPTION_APIS.has(fqn.slice("global.".length));
51
- }
52
-
53
- function optionsArgument(
54
- call: ts.CallExpression,
55
- checker: ts.TypeChecker,
56
- ): ts.Expression | undefined {
57
- const declaration = checker.getResolvedSignature(call)?.declaration;
58
- if (declaration === undefined || !ts.isFunctionLike(declaration)) return undefined;
59
- if (!oneBasedApi(declaration, checker)) return undefined;
60
- const index = declaration.parameters.findIndex(
61
- (parameter) =>
62
- parameter.dotDotDotToken === undefined &&
63
- ts.isIdentifier(parameter.name) &&
64
- parameter.name.text === OPTIONS_PARAMETER,
65
- );
66
- return index === -1 ? undefined : call.arguments[index];
67
- }
68
-
69
- function unwrap(expression: ts.Expression): ts.Expression {
70
- let current = expression;
71
- while (
72
- ts.isParenthesizedExpression(current) ||
73
- ts.isAsExpression(current) ||
74
- ts.isSatisfiesExpression(current) ||
75
- ts.isNonNullExpression(current) ||
76
- ts.isTypeAssertionExpression(current)
77
- ) {
78
- current = current.expression;
79
- }
80
- return current;
81
- }
82
-
83
- function someConstituent(type: ts.Type, predicate: (type: ts.Type) => boolean): boolean {
84
- return type.isUnion() ? type.types.some(predicate) : predicate(type);
85
- }
86
-
87
- function carriesIndex(type: ts.Type): boolean {
88
- return someConstituent(type, (t) => t.getProperty(INDEX_KEY) !== undefined);
89
- }
90
-
91
- function admitsUndefined(type: ts.Type): boolean {
92
- return someConstituent(
93
- type,
94
- (t) => (t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
95
- );
96
- }
97
-
98
- function propertyName(
99
- property: ts.ObjectLiteralElementLike,
100
- checker: ts.TypeChecker,
101
- ): string | undefined {
102
- const name = property.name;
103
- if (name === undefined) return undefined;
104
- if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
105
- return name.text;
106
- }
107
- if (ts.isComputedPropertyName(name)) {
108
- const type = checker.getTypeAtLocation(name.expression);
109
- if (type.isStringLiteral()) return type.value;
110
- }
111
- return undefined;
112
- }
113
-
114
- function indexValueType(
115
- property: ts.ObjectLiteralElementLike,
116
- checker: ts.TypeChecker,
117
- ): ts.Type | undefined {
118
- if (ts.isPropertyAssignment(property)) return checker.getTypeAtLocation(property.initializer);
119
- if (ts.isShorthandPropertyAssignment(property)) return checker.getTypeAtLocation(property.name);
120
- return undefined;
121
- }
122
-
123
- function rejection(node: ts.Node, messageText: string): ts.Diagnostic {
124
- const file = node.getSourceFile();
125
- const start = node.getStart(file);
126
- return {
127
- file,
128
- start,
129
- length: node.getEnd() - start,
130
- category: ts.DiagnosticCategory.Error,
131
- code: 0,
132
- messageText,
133
- };
134
- }
135
-
136
- // The first reason `options` cannot be lowered, or `undefined` when it can.
137
- function rejectOptions(options: ts.Expression, checker: ts.TypeChecker): ts.Diagnostic | undefined {
138
- const literal = unwrap(options);
139
- if (!ts.isObjectLiteralExpression(literal)) {
140
- return carriesIndex(checker.getTypeAtLocation(literal))
141
- ? rejection(options, INDEX_OPTION_VARIABLE_MESSAGE)
142
- : undefined;
143
- }
144
- let uncoveredSpread: ts.SpreadAssignment | undefined;
145
- let explicitIndex: ts.ObjectLiteralElementLike | undefined;
146
- for (const property of literal.properties) {
147
- if (ts.isSpreadAssignment(property)) {
148
- if (carriesIndex(checker.getTypeAtLocation(property.expression))) uncoveredSpread = property;
149
- } else if (propertyName(property, checker) === INDEX_KEY) {
150
- uncoveredSpread = undefined;
151
- explicitIndex = property;
152
- }
153
- }
154
- if (uncoveredSpread !== undefined) return rejection(uncoveredSpread, INDEX_OPTION_SPREAD_MESSAGE);
155
- const valueType = explicitIndex && indexValueType(explicitIndex, checker);
156
- if (explicitIndex !== undefined && valueType !== undefined && admitsUndefined(valueType)) {
157
- return rejection(explicitIndex, INDEX_OPTION_UNDEFINED_MESSAGE);
158
- }
159
- return undefined;
160
- }
161
-
162
- function plusOne(value: Expression): Expression {
163
- if (isNumericLiteral(value)) {
164
- return setNodePosition(createNumericLiteral(value.value + 1), getOriginalPos(value));
165
- }
166
- return setNodePosition(
167
- createBinaryExpression(value, createNumericLiteral(1), SyntaxKind.AdditionOperator),
168
- getOriginalPos(value),
169
- );
170
- }
171
-
172
- // `rejectOptions` guarantees the last `index` field is the effective one: a
173
- // spread carrying `index` after it is rejected. Every earlier one is shadowed
174
- // and keeps its source value.
175
- function offsetEffectiveIndexField(tables: readonly TableExpression[]): void {
176
- for (const field of tables.flatMap((table) => table.fields).reverse()) {
177
- if (field.key !== undefined && isStringLiteral(field.key) && field.key.value === INDEX_KEY) {
178
- field.value = plusOne(field.value);
179
- return;
180
- }
181
- }
182
- }
183
-
184
- export const arrayIndexOptionLoweringPlugin: Plugin = {
185
- visitors: {
186
- [ts.SyntaxKind.CallExpression]: (node, context): Expression => {
187
- const options = optionsArgument(node, context.checker);
188
- if (options !== undefined) {
189
- const diagnostic = rejectOptions(options, context.checker);
190
- if (diagnostic !== undefined) {
191
- context.diagnostics.push(diagnostic);
192
- } else {
193
- const literal = unwrap(options);
194
- if (ts.isObjectLiteralExpression(literal)) loweredLiterals.add(literal);
195
- }
196
- }
197
- return context.superTransformExpression(node);
198
- },
199
- // A spread lowers the literal to `__TS__ObjectAssign({}, base, {index = …})`,
200
- // so the table carrying `index` can sit one call deep.
201
- [ts.SyntaxKind.ObjectLiteralExpression]: (node, context): Expression => {
202
- const result = context.superTransformExpression(node);
203
- if (!loweredLiterals.has(node)) return result;
204
- if (isTableExpression(result)) {
205
- offsetEffectiveIndexField([result]);
206
- } else if (isCallExpression(result)) {
207
- offsetEffectiveIndexField(result.params.filter(isTableExpression));
208
- }
209
- return result;
210
- },
211
- },
212
- };