@defold-typescript/transpiler 0.38.0 → 0.40.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.
@@ -0,0 +1,6 @@
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;
package/dist/index.js CHANGED
@@ -2058,16 +2058,179 @@ function computeOutputRel(rel, config, kind) {
2058
2058
  return baseRel.replace(/\.ts$/, SCRIPT_SUFFIX_BY_KIND[kind]);
2059
2059
  }
2060
2060
  // src/program-diagnostics.ts
2061
- import * as ts8 from "typescript";
2061
+ import * as ts9 from "typescript";
2062
2062
  import * as tstl from "typescript-to-lua";
2063
2063
 
2064
- // src/message-guard-lowering.ts
2064
+ // src/array-index-option-lowering.ts
2065
2065
  import * as ts5 from "typescript";
2066
2066
  import {
2067
2067
  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,
2068
2231
  createCallExpression as createCallExpression3,
2069
2232
  createIdentifier as createIdentifier3,
2070
- SyntaxKind as SyntaxKind7
2233
+ SyntaxKind as SyntaxKind9
2071
2234
  } from "typescript-to-lua";
2072
2235
  var GUARD_MODULE = "@defold-typescript/types";
2073
2236
  var GUARD_NAME = "isMessage";
@@ -2076,7 +2239,7 @@ function resolvesToGuardExport(callee, checker) {
2076
2239
  if (symbol === undefined) {
2077
2240
  return false;
2078
2241
  }
2079
- if (symbol.flags & ts5.SymbolFlags.Alias) {
2242
+ if (symbol.flags & ts6.SymbolFlags.Alias) {
2080
2243
  symbol = checker.getAliasedSymbol(symbol);
2081
2244
  }
2082
2245
  if (symbol.getName() !== GUARD_NAME) {
@@ -2090,12 +2253,12 @@ function resolvesToGuardExport(callee, checker) {
2090
2253
  }
2091
2254
  var messageGuardLoweringPlugin = {
2092
2255
  visitors: {
2093
- [ts5.SyntaxKind.CallExpression]: (node, context) => {
2256
+ [ts6.SyntaxKind.CallExpression]: (node, context) => {
2094
2257
  const [messageIdArg, , expectedArg] = node.arguments;
2095
2258
  if (node.arguments.length === 3 && messageIdArg !== undefined && expectedArg !== undefined && resolvesToGuardExport(node.expression, context.checker)) {
2096
2259
  const messageId = context.transformExpression(messageIdArg);
2097
2260
  const expected = context.transformExpression(expectedArg);
2098
- return createBinaryExpression3(messageId, createCallExpression3(createIdentifier3("hash"), [expected]), SyntaxKind7.EqualityOperator, node);
2261
+ return createBinaryExpression4(messageId, createCallExpression3(createIdentifier3("hash"), [expected]), SyntaxKind9.EqualityOperator, node);
2099
2262
  }
2100
2263
  return context.superTransformExpression(node);
2101
2264
  }
@@ -2103,34 +2266,34 @@ var messageGuardLoweringPlugin = {
2103
2266
  };
2104
2267
 
2105
2268
  // src/timers-lowering.ts
2106
- import * as ts6 from "typescript";
2269
+ import * as ts7 from "typescript";
2107
2270
  import { LuaLibFeature } from "typescript-to-lua";
2108
2271
  function isTimersSpecifier(node) {
2109
- return ts6.isStringLiteral(node) && node.text === TIMERS_MODULE_SPECIFIER;
2272
+ return ts7.isStringLiteral(node) && node.text === TIMERS_MODULE_SPECIFIER;
2110
2273
  }
2111
2274
  function importsTimersModule(sourceFile) {
2112
- return sourceFile.statements.some((statement) => ts6.isImportDeclaration(statement) && isTimersSpecifier(statement.moduleSpecifier));
2275
+ return sourceFile.statements.some((statement) => ts7.isImportDeclaration(statement) && isTimersSpecifier(statement.moduleSpecifier));
2113
2276
  }
2114
2277
  var TIMERS_REQUIRE_PATH = `@NoResolution:${TIMERS_REQUIRE_NAME}`;
2115
2278
  var timersLoweringPlugin = {
2116
2279
  visitors: {
2117
- [ts6.SyntaxKind.ImportDeclaration]: (node, context) => {
2280
+ [ts7.SyntaxKind.ImportDeclaration]: (node, context) => {
2118
2281
  if (!isTimersSpecifier(node.moduleSpecifier)) {
2119
2282
  return context.superTransformStatements(node);
2120
2283
  }
2121
2284
  context.usedLuaLibFeatures.add(LuaLibFeature.Promise);
2122
2285
  context.usedLuaLibFeatures.add(LuaLibFeature.New);
2123
- const rewritten = ts6.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts6.factory.createStringLiteral(TIMERS_REQUIRE_PATH), node.attributes);
2286
+ const rewritten = ts7.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts7.factory.createStringLiteral(TIMERS_REQUIRE_PATH), node.attributes);
2124
2287
  return context.superTransformStatements(rewritten);
2125
2288
  }
2126
2289
  }
2127
2290
  };
2128
2291
 
2129
2292
  // src/window-event-guard-lowering.ts
2130
- import * as ts7 from "typescript";
2293
+ import * as ts8 from "typescript";
2131
2294
  import {
2132
- createBinaryExpression as createBinaryExpression4,
2133
- SyntaxKind as SyntaxKind10
2295
+ createBinaryExpression as createBinaryExpression5,
2296
+ SyntaxKind as SyntaxKind12
2134
2297
  } from "typescript-to-lua";
2135
2298
  var GUARD_MODULE2 = "@defold-typescript/types";
2136
2299
  var GUARD_NAME2 = "isWindowEvent";
@@ -2139,7 +2302,7 @@ function resolvesToGuardExport2(callee, checker) {
2139
2302
  if (symbol === undefined) {
2140
2303
  return false;
2141
2304
  }
2142
- if (symbol.flags & ts7.SymbolFlags.Alias) {
2305
+ if (symbol.flags & ts8.SymbolFlags.Alias) {
2143
2306
  symbol = checker.getAliasedSymbol(symbol);
2144
2307
  }
2145
2308
  if (symbol.getName() !== GUARD_NAME2) {
@@ -2153,12 +2316,12 @@ function resolvesToGuardExport2(callee, checker) {
2153
2316
  }
2154
2317
  var windowEventGuardLoweringPlugin = {
2155
2318
  visitors: {
2156
- [ts7.SyntaxKind.CallExpression]: (node, context) => {
2319
+ [ts8.SyntaxKind.CallExpression]: (node, context) => {
2157
2320
  const [eventArg, , expectedArg] = node.arguments;
2158
2321
  if (node.arguments.length === 3 && eventArg !== undefined && expectedArg !== undefined && resolvesToGuardExport2(node.expression, context.checker)) {
2159
2322
  const event = context.transformExpression(eventArg);
2160
2323
  const expected = context.transformExpression(expectedArg);
2161
- return createBinaryExpression4(event, expected, SyntaxKind10.EqualityOperator, node);
2324
+ return createBinaryExpression5(event, expected, SyntaxKind12.EqualityOperator, node);
2162
2325
  }
2163
2326
  return context.superTransformExpression(node);
2164
2327
  }
@@ -2171,14 +2334,15 @@ var LUA_PLUGINS = [
2171
2334
  messageGuardLoweringPlugin,
2172
2335
  windowEventGuardLoweringPlugin,
2173
2336
  messageDispatchLoweringPlugin,
2174
- timersLoweringPlugin
2337
+ timersLoweringPlugin,
2338
+ arrayIndexOptionLoweringPlugin
2175
2339
  ];
2176
2340
  var noopWriteFile = () => {};
2177
2341
  function isAmbient(fileName) {
2178
2342
  return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
2179
2343
  }
2180
2344
  function getProgramDiagnostics(program, sourceFile) {
2181
- const { diagnostics } = tstl.getProgramTranspileResult(ts8.sys, noopWriteFile, {
2345
+ const { diagnostics } = tstl.getProgramTranspileResult(ts9.sys, noopWriteFile, {
2182
2346
  program,
2183
2347
  plugins: LUA_PLUGINS,
2184
2348
  ...sourceFile ? { sourceFiles: [sourceFile] } : {}
@@ -2749,11 +2913,11 @@ function relativeAddressesFrom(index, context) {
2749
2913
  import { readFileSync as readFileSync2 } from "node:fs";
2750
2914
  import { createRequire as createRequire2 } from "node:module";
2751
2915
  import * as path3 from "node:path";
2752
- import * as ts12 from "typescript";
2916
+ import * as ts13 from "typescript";
2753
2917
  import * as tstl3 from "typescript-to-lua";
2754
2918
 
2755
2919
  // src/companion-emit.ts
2756
- import * as ts9 from "typescript";
2920
+ import * as ts10 from "typescript";
2757
2921
  import {
2758
2922
  createAssignmentStatement as createAssignmentStatement2,
2759
2923
  createCallExpression as createCallExpression4,
@@ -2766,18 +2930,18 @@ import {
2766
2930
  createTableIndexExpression as createTableIndexExpression2,
2767
2931
  createVariableDeclarationStatement as createVariableDeclarationStatement3,
2768
2932
  isBlock as isBlock3,
2769
- isCallExpression as isCallExpression4,
2933
+ isCallExpression as isCallExpression5,
2770
2934
  isDoStatement,
2771
2935
  isForInStatement as isForInStatement2,
2772
2936
  isForStatement,
2773
2937
  isFunctionDefinition,
2774
2938
  isFunctionExpression as isFunctionExpression4,
2775
- isIdentifier as isIdentifier4,
2939
+ isIdentifier as isIdentifier5,
2776
2940
  isMethodCallExpression,
2777
2941
  isRepeatStatement,
2778
2942
  isReturnStatement as isReturnStatement2,
2779
- isStringLiteral as isStringLiteral6,
2780
- isTableExpression,
2943
+ isStringLiteral as isStringLiteral8,
2944
+ isTableExpression as isTableExpression2,
2781
2945
  isVariableDeclarationStatement,
2782
2946
  LuaPrinter
2783
2947
  } from "typescript-to-lua";
@@ -2787,8 +2951,8 @@ function exportsIdentifier() {
2787
2951
  }
2788
2952
  function isRuntimeScriptSource(sourceFile, checker) {
2789
2953
  return sourceFile.statements.some((statement) => {
2790
- const expression = ts9.isExpressionStatement(statement) ? statement.expression : ts9.isExportAssignment(statement) && !statement.isExportEquals ? statement.expression : undefined;
2791
- return expression !== undefined && ts9.isCallExpression(expression) && resolvesToFactoryExport(expression.expression, checker);
2954
+ const expression = ts10.isExpressionStatement(statement) ? statement.expression : ts10.isExportAssignment(statement) && !statement.isExportEquals ? statement.expression : undefined;
2955
+ return expression !== undefined && ts10.isCallExpression(expression) && resolvesToFactoryExport(expression.expression, checker);
2792
2956
  });
2793
2957
  }
2794
2958
  function planFor(sourceFile, checker) {
@@ -2849,7 +3013,7 @@ function freeNames(statements) {
2849
3013
  if (node === undefined) {
2850
3014
  return;
2851
3015
  }
2852
- if (isIdentifier4(node)) {
3016
+ if (isIdentifier5(node)) {
2853
3017
  if (!isBound(node.text)) {
2854
3018
  free.add(node.text);
2855
3019
  }
@@ -2940,11 +3104,11 @@ function freeNames(statements) {
2940
3104
  function resolvedRequirePaths(sourceFile, checker) {
2941
3105
  const paths = new Map;
2942
3106
  for (const statement of sourceFile.statements) {
2943
- const specifier = ts9.isImportDeclaration(statement) || ts9.isExportDeclaration(statement) ? statement.moduleSpecifier : undefined;
2944
- if (specifier === undefined || !ts9.isStringLiteral(specifier)) {
3107
+ const specifier = ts10.isImportDeclaration(statement) || ts10.isExportDeclaration(statement) ? statement.moduleSpecifier : undefined;
3108
+ if (specifier === undefined || !ts10.isStringLiteral(specifier)) {
2945
3109
  continue;
2946
3110
  }
2947
- const target = checker.getSymbolAtLocation(specifier)?.declarations?.find((declaration) => ts9.isSourceFile(declaration));
3111
+ const target = checker.getSymbolAtLocation(specifier)?.declarations?.find((declaration) => ts10.isSourceFile(declaration));
2948
3112
  if (target === undefined || target.isDeclarationFile) {
2949
3113
  continue;
2950
3114
  }
@@ -2955,14 +3119,14 @@ function resolvedRequirePaths(sourceFile, checker) {
2955
3119
  function resolveMovedRequires(statements, paths) {
2956
3120
  for (const statement of statements) {
2957
3121
  forEachLuaNode(statement, (node) => {
2958
- if (!isCallExpression4(node) || !isIdentifier4(node.expression)) {
3122
+ if (!isCallExpression5(node) || !isIdentifier5(node.expression)) {
2959
3123
  return;
2960
3124
  }
2961
3125
  if (node.expression.text !== "require" || node.params.length !== 1) {
2962
3126
  return;
2963
3127
  }
2964
3128
  const [argument] = node.params;
2965
- if (argument === undefined || !isStringLiteral6(argument)) {
3129
+ if (argument === undefined || !isStringLiteral8(argument)) {
2966
3130
  return;
2967
3131
  }
2968
3132
  const resolved = paths.get(argument.value);
@@ -2978,14 +3142,14 @@ function withResolvedRequire(statement, paths) {
2978
3142
  }
2979
3143
  const name = statement.left[0];
2980
3144
  const [value] = statement.right ?? [];
2981
- if (name === undefined || value === undefined || !isCallExpression4(value)) {
3145
+ if (name === undefined || value === undefined || !isCallExpression5(value)) {
2982
3146
  return statement;
2983
3147
  }
2984
- if (!isIdentifier4(value.expression) || value.expression.text !== "require") {
3148
+ if (!isIdentifier5(value.expression) || value.expression.text !== "require") {
2985
3149
  return statement;
2986
3150
  }
2987
3151
  const [argument] = value.params;
2988
- if (argument === undefined || !isStringLiteral6(argument)) {
3152
+ if (argument === undefined || !isStringLiteral8(argument)) {
2989
3153
  return statement;
2990
3154
  }
2991
3155
  const resolved = paths.get(argument.value);
@@ -3021,7 +3185,7 @@ function copiedPrelude(halves, importBindings) {
3021
3185
  function namesIdentifier(statement, names) {
3022
3186
  let found = false;
3023
3187
  forEachLuaNode(statement, (node) => {
3024
- if (isIdentifier4(node) && names.has(node.text)) {
3188
+ if (isIdentifier5(node) && names.has(node.text)) {
3025
3189
  found = true;
3026
3190
  }
3027
3191
  });
@@ -3032,14 +3196,14 @@ function isExportsTableInit(statement) {
3032
3196
  return false;
3033
3197
  }
3034
3198
  const [single] = statement.right ?? [];
3035
- return statement.left[0]?.text === EXPORTS_NAME && single !== undefined && isTableExpression(single);
3199
+ return statement.left[0]?.text === EXPORTS_NAME && single !== undefined && isTableExpression2(single);
3036
3200
  }
3037
3201
  function isExportsReturn(statement) {
3038
3202
  if (!isReturnStatement2(statement) || statement.expressions.length !== 1) {
3039
3203
  return false;
3040
3204
  }
3041
3205
  const [single] = statement.expressions;
3042
- return single !== undefined && isIdentifier4(single) && single.text === EXPORTS_NAME;
3206
+ return single !== undefined && isIdentifier5(single) && single.text === EXPORTS_NAME;
3043
3207
  }
3044
3208
  function isHoistedNameDeclaration(statement) {
3045
3209
  return isVariableDeclarationStatement(statement) && statement.right === undefined;
@@ -3108,11 +3272,11 @@ function createCompanionEmitPlugin() {
3108
3272
  return statements;
3109
3273
  }
3110
3274
  const visitors = {
3111
- [ts9.SyntaxKind.VariableStatement]: capture,
3112
- [ts9.SyntaxKind.FunctionDeclaration]: capture,
3113
- [ts9.SyntaxKind.ClassDeclaration]: capture,
3114
- [ts9.SyntaxKind.EnumDeclaration]: capture,
3115
- [ts9.SyntaxKind.ExportDeclaration]: capture
3275
+ [ts10.SyntaxKind.VariableStatement]: capture,
3276
+ [ts10.SyntaxKind.FunctionDeclaration]: capture,
3277
+ [ts10.SyntaxKind.ClassDeclaration]: capture,
3278
+ [ts10.SyntaxKind.EnumDeclaration]: capture,
3279
+ [ts10.SyntaxKind.ExportDeclaration]: capture
3116
3280
  };
3117
3281
  let configuredNoResolvePaths = [];
3118
3282
  const plugin = {
@@ -3181,7 +3345,7 @@ function emitCompanion(program, emitHost, file, sourceFile, plan, owned, compani
3181
3345
  }
3182
3346
 
3183
3347
  // src/editor-script-erasure.ts
3184
- import * as ts10 from "typescript";
3348
+ import * as ts11 from "typescript";
3185
3349
  import {
3186
3350
  createReturnStatement as createReturnStatement3
3187
3351
  } from "typescript-to-lua";
@@ -3201,7 +3365,7 @@ function resolveEditorFactoryExport(callee, checker) {
3201
3365
  if (symbol === undefined) {
3202
3366
  return;
3203
3367
  }
3204
- if (symbol.flags & ts10.SymbolFlags.Alias) {
3368
+ if (symbol.flags & ts11.SymbolFlags.Alias) {
3205
3369
  symbol = checker.getAliasedSymbol(symbol);
3206
3370
  }
3207
3371
  const name = symbol.getName();
@@ -3215,7 +3379,7 @@ function resolveEditorFactoryExport(callee, checker) {
3215
3379
  return declaration.getSourceFile().fileName.includes(EDITOR_FACTORY_MODULE) ? name : undefined;
3216
3380
  }
3217
3381
  function lowerEditorFactoryCall(expression, context) {
3218
- if (!ts10.isCallExpression(expression)) {
3382
+ if (!ts11.isCallExpression(expression)) {
3219
3383
  return;
3220
3384
  }
3221
3385
  if (resolveEditorFactoryExport(expression.expression, context.checker) !== EDITOR_SCRIPT_FACTORY_NAME) {
@@ -3228,7 +3392,7 @@ function lowerEditorFactoryCall(expression, context) {
3228
3392
  return [createReturnStatement3([context.transformExpression(module)])];
3229
3393
  }
3230
3394
  function isEditorFactoryOnlyImport(node) {
3231
- if (!ts10.isStringLiteral(node.moduleSpecifier) || !EDITOR_FACTORY_SPECIFIERS.has(node.moduleSpecifier.text)) {
3395
+ if (!ts11.isStringLiteral(node.moduleSpecifier) || !EDITOR_FACTORY_SPECIFIERS.has(node.moduleSpecifier.text)) {
3232
3396
  return false;
3233
3397
  }
3234
3398
  const clause = node.importClause;
@@ -3239,7 +3403,7 @@ function isEditorFactoryOnlyImport(node) {
3239
3403
  return false;
3240
3404
  }
3241
3405
  const bindings = clause.namedBindings;
3242
- if (bindings === undefined || !ts10.isNamedImports(bindings)) {
3406
+ if (bindings === undefined || !ts11.isNamedImports(bindings)) {
3243
3407
  return false;
3244
3408
  }
3245
3409
  const runtime = bindings.elements.filter((element) => !element.isTypeOnly);
@@ -3250,7 +3414,7 @@ function isEditorFactoryOnlyImport(node) {
3250
3414
  }
3251
3415
  var editorScriptErasurePlugin = {
3252
3416
  visitors: {
3253
- [ts10.SyntaxKind.ExportAssignment]: (node, context) => {
3417
+ [ts11.SyntaxKind.ExportAssignment]: (node, context) => {
3254
3418
  if (!node.isExportEquals) {
3255
3419
  const lowered = lowerEditorFactoryCall(node.expression, context);
3256
3420
  if (lowered !== undefined) {
@@ -3259,14 +3423,14 @@ var editorScriptErasurePlugin = {
3259
3423
  }
3260
3424
  return context.superTransformStatements(node);
3261
3425
  },
3262
- [ts10.SyntaxKind.CallExpression]: (node, context) => {
3426
+ [ts11.SyntaxKind.CallExpression]: (node, context) => {
3263
3427
  const command = node.arguments[0];
3264
3428
  if (command !== undefined && resolveEditorFactoryExport(node.expression, context.checker) === EDITOR_COMMAND_FACTORY_NAME) {
3265
3429
  return context.transformExpression(command);
3266
3430
  }
3267
3431
  return context.superTransformExpression(node);
3268
3432
  },
3269
- [ts10.SyntaxKind.ImportDeclaration]: (node, context) => isEditorFactoryOnlyImport(node) ? [] : context.superTransformStatements(node)
3433
+ [ts11.SyntaxKind.ImportDeclaration]: (node, context) => isEditorFactoryOnlyImport(node) ? [] : context.superTransformStatements(node)
3270
3434
  }
3271
3435
  };
3272
3436
 
@@ -3277,17 +3441,17 @@ import * as path2 from "node:path";
3277
3441
  import * as tstl2 from "typescript-to-lua";
3278
3442
 
3279
3443
  // src/go-property-direct-call.ts
3280
- import * as ts11 from "typescript";
3444
+ import * as ts12 from "typescript";
3281
3445
  var GO_PROPERTY_DIRECT_CALL_MESSAGE = "`go.property` called directly is deprecated; declare the property in `defineScript({ properties })` so it types onto `self` (the transpiler emits the registration for you).";
3282
3446
  function findDirectGoPropertyCalls(sourceFile) {
3283
3447
  const calls = [];
3284
3448
  const visit = (node) => {
3285
- if (ts11.isCallExpression(node) && ts11.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts11.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
3449
+ if (ts12.isCallExpression(node) && ts12.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts12.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
3286
3450
  calls.push(node);
3287
3451
  }
3288
- ts11.forEachChild(node, visit);
3452
+ ts12.forEachChild(node, visit);
3289
3453
  };
3290
- ts11.forEachChild(sourceFile, visit);
3454
+ ts12.forEachChild(sourceFile, visit);
3291
3455
  return calls;
3292
3456
  }
3293
3457
 
@@ -3467,6 +3631,7 @@ function transpileProject(input) {
3467
3631
  { plugin: windowEventGuardLoweringPlugin },
3468
3632
  { plugin: messageDispatchLoweringPlugin },
3469
3633
  { plugin: timersLoweringPlugin },
3634
+ { plugin: arrayIndexOptionLoweringPlugin },
3470
3635
  { plugin: companionEmit.plugin }
3471
3636
  ]
3472
3637
  });
@@ -3497,6 +3662,7 @@ function compilerOptions(companionEmit) {
3497
3662
  { plugin: windowEventGuardLoweringPlugin },
3498
3663
  { plugin: messageDispatchLoweringPlugin },
3499
3664
  { plugin: timersLoweringPlugin },
3665
+ { plugin: arrayIndexOptionLoweringPlugin },
3500
3666
  { plugin: companionEmit.plugin }
3501
3667
  ]
3502
3668
  };
@@ -3539,7 +3705,7 @@ function createTranspileSession() {
3539
3705
  if (cached) {
3540
3706
  return cached;
3541
3707
  }
3542
- const created = ts12.createSourceFile(fileName, content, ts12.ScriptTarget.Latest, false);
3708
+ const created = ts13.createSourceFile(fileName, content, ts13.ScriptTarget.Latest, false);
3543
3709
  sourceFileCache.set(normalized, created);
3544
3710
  return created;
3545
3711
  }
@@ -3558,7 +3724,7 @@ function createTranspileSession() {
3558
3724
  return cached;
3559
3725
  }
3560
3726
  const fileContent = readFileSync2(filePath, "utf8");
3561
- const created = ts12.createSourceFile(filePath, fileContent, ts12.ScriptTarget.Latest, false);
3727
+ const created = ts13.createSourceFile(filePath, fileContent, ts13.ScriptTarget.Latest, false);
3562
3728
  libCache.set(fileName, created);
3563
3729
  return created;
3564
3730
  }
@@ -3571,10 +3737,10 @@ function createTranspileSession() {
3571
3737
  return AMBIENT_FILES[normalized];
3572
3738
  }
3573
3739
  const host = {
3574
- fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts12.sys.fileExists(fileName),
3740
+ fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts13.sys.fileExists(fileName),
3575
3741
  getCanonicalFileName: (fileName) => fileName,
3576
3742
  getCurrentDirectory: () => "",
3577
- getDefaultLibFileName: ts12.getDefaultLibFileName,
3743
+ getDefaultLibFileName: ts13.getDefaultLibFileName,
3578
3744
  readFile: () => "",
3579
3745
  getNewLine: () => `
3580
3746
  `,
@@ -3596,15 +3762,15 @@ function createTranspileSession() {
3596
3762
  const userKeys = new Set(userFiles.keys());
3597
3763
  const rootNames = [...Object.keys(AMBIENT_FILES).map(normalizeSlashes), ...userKeys];
3598
3764
  const companionEmit = createCompanionEmitPlugin();
3599
- program = ts12.createProgram(rootNames, compilerOptions(companionEmit), host, program);
3600
- const preEmitDiagnostics = ts12.getPreEmitDiagnostics(program);
3765
+ program = ts13.createProgram(rootNames, compilerOptions(companionEmit), host, program);
3766
+ const preEmitDiagnostics = ts13.getPreEmitDiagnostics(program);
3601
3767
  const collector = createOutputCollector();
3602
3768
  const { diagnostics: transpileDiagnostics } = new tstl3.Transpiler().emit({
3603
3769
  program,
3604
3770
  writeFile: collector.writeFile
3605
3771
  });
3606
3772
  const diagnostics = [
3607
- ...ts12.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
3773
+ ...ts13.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
3608
3774
  ];
3609
3775
  return collectOutputs(collector.files, diagnostics, userKeys, companionEmit.companions);
3610
3776
  }
@@ -3614,7 +3780,7 @@ function createTranspileSession() {
3614
3780
  };
3615
3781
  }
3616
3782
  // src/url-address-slots.ts
3617
- import * as ts13 from "typescript";
3783
+ import * as ts14 from "typescript";
3618
3784
  var ADDRESS_CLASSES = {
3619
3785
  "game-object": true,
3620
3786
  component: true,
@@ -3633,7 +3799,7 @@ function isFragmentCaret(slot, position) {
3633
3799
  return slot.fragmentStart !== -1 && position >= slot.fragmentStart;
3634
3800
  }
3635
3801
  function tableKey(checker, callee) {
3636
- const symbol = checker.getSymbolAtLocation(ts13.isIdentifier(callee) ? callee : callee.name);
3802
+ const symbol = checker.getSymbolAtLocation(ts14.isIdentifier(callee) ? callee : callee.name);
3637
3803
  if (!symbol)
3638
3804
  return;
3639
3805
  const fqn = checker.getFullyQualifiedName(symbol);
@@ -3641,43 +3807,43 @@ function tableKey(checker, callee) {
3641
3807
  }
3642
3808
  function parameterName(checker, call, index) {
3643
3809
  const declaration = checker.getResolvedSignature(call)?.declaration;
3644
- if (!declaration || !ts13.isFunctionLike(declaration))
3810
+ if (!declaration || !ts14.isFunctionLike(declaration))
3645
3811
  return;
3646
3812
  const parameter = declaration.parameters[index];
3647
- if (!parameter || parameter.dotDotDotToken || !ts13.isIdentifier(parameter.name))
3813
+ if (!parameter || parameter.dotDotDotToken || !ts14.isIdentifier(parameter.name))
3648
3814
  return;
3649
3815
  return parameter.name.text;
3650
3816
  }
3651
3817
  var EQUALITY_OPERATORS = new Set([
3652
- ts13.SyntaxKind.EqualsEqualsEqualsToken,
3653
- ts13.SyntaxKind.ExclamationEqualsEqualsToken,
3654
- ts13.SyntaxKind.EqualsEqualsToken,
3655
- ts13.SyntaxKind.ExclamationEqualsToken
3818
+ ts14.SyntaxKind.EqualsEqualsEqualsToken,
3819
+ ts14.SyntaxKind.ExclamationEqualsEqualsToken,
3820
+ ts14.SyntaxKind.EqualsEqualsToken,
3821
+ ts14.SyntaxKind.ExclamationEqualsToken
3656
3822
  ]);
3657
3823
  function comparisonSatisfied(checker, entry, call) {
3658
3824
  if (entry.comparedParameter === undefined)
3659
3825
  return true;
3660
3826
  let node = call;
3661
- while (ts13.isParenthesizedExpression(node.parent))
3827
+ while (ts14.isParenthesizedExpression(node.parent))
3662
3828
  node = node.parent;
3663
3829
  const comparison = node.parent;
3664
- if (!ts13.isBinaryExpression(comparison))
3830
+ if (!ts14.isBinaryExpression(comparison))
3665
3831
  return false;
3666
3832
  if (!EQUALITY_OPERATORS.has(comparison.operatorToken.kind))
3667
3833
  return false;
3668
3834
  const other = comparison.left === node ? comparison.right : comparison.left;
3669
3835
  const symbol = checker.getSymbolAtLocation(other);
3670
- return (symbol?.declarations ?? []).some((declaration) => ts13.isParameter(declaration) && ts13.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
3836
+ return (symbol?.declarations ?? []).some((declaration) => ts14.isParameter(declaration) && ts14.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
3671
3837
  }
3672
3838
  function classifiedEntryOfArgument(checker, table, argument) {
3673
3839
  const call = argument.parent;
3674
- if (!ts13.isCallExpression(call))
3840
+ if (!ts14.isCallExpression(call))
3675
3841
  return;
3676
3842
  const argumentIndex = call.arguments.indexOf(argument);
3677
3843
  if (argumentIndex === -1)
3678
3844
  return;
3679
3845
  const callee = call.expression;
3680
- if (!ts13.isPropertyAccessExpression(callee) && !ts13.isIdentifier(callee))
3846
+ if (!ts14.isPropertyAccessExpression(callee) && !ts14.isIdentifier(callee))
3681
3847
  return;
3682
3848
  const fqn = tableKey(checker, callee);
3683
3849
  if (fqn === undefined)
@@ -3694,10 +3860,10 @@ function classifiedEntryOfArgument(checker, table, argument) {
3694
3860
  }
3695
3861
  function slotThroughHashedCall(checker, table, literal) {
3696
3862
  const call = literal.parent;
3697
- if (!ts13.isCallExpression(call))
3863
+ if (!ts14.isCallExpression(call))
3698
3864
  return;
3699
3865
  const callee = call.expression;
3700
- if (!ts13.isPropertyAccessExpression(callee) && !ts13.isIdentifier(callee))
3866
+ if (!ts14.isPropertyAccessExpression(callee) && !ts14.isIdentifier(callee))
3701
3867
  return;
3702
3868
  if (tableKey(checker, callee) !== "hash")
3703
3869
  return;
@@ -3715,24 +3881,24 @@ function addressTextOf(checker, entry, call) {
3715
3881
  if (entry.addressParameter === undefined)
3716
3882
  return;
3717
3883
  const declaration = checker.getResolvedSignature(call)?.declaration;
3718
- if (!declaration || !ts13.isFunctionLike(declaration))
3884
+ if (!declaration || !ts14.isFunctionLike(declaration))
3719
3885
  return;
3720
- const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts13.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
3886
+ const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts14.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
3721
3887
  if (index === -1)
3722
3888
  return;
3723
3889
  const argument = call.arguments[index];
3724
- return argument && ts13.isStringLiteralLike(argument) ? argument.text : undefined;
3890
+ return argument && ts14.isStringLiteralLike(argument) ? argument.text : undefined;
3725
3891
  }
3726
3892
  function innermostLiteralAt(sourceFile, position) {
3727
3893
  let found;
3728
3894
  const visit = (node) => {
3729
3895
  if (position < node.getStart(sourceFile) || position > node.getEnd())
3730
3896
  return;
3731
- if (ts13.isStringLiteralLike(node))
3897
+ if (ts14.isStringLiteralLike(node))
3732
3898
  found = node;
3733
- ts13.forEachChild(node, visit);
3899
+ ts14.forEachChild(node, visit);
3734
3900
  };
3735
- ts13.forEachChild(sourceFile, visit);
3901
+ ts14.forEachChild(sourceFile, visit);
3736
3902
  return found;
3737
3903
  }
3738
3904
  function resolveClassifiedSlotAtPosition(input) {
@@ -3766,23 +3932,23 @@ function resolveClassifiedSlotAtPosition(input) {
3766
3932
  };
3767
3933
  }
3768
3934
  // src/url-cross-world-address.ts
3769
- import * as ts15 from "typescript";
3935
+ import * as ts16 from "typescript";
3770
3936
 
3771
3937
  // src/url-address-literals.ts
3772
- import * as ts14 from "typescript";
3938
+ import * as ts15 from "typescript";
3773
3939
  function isAmbient2(fileName) {
3774
3940
  return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
3775
3941
  }
3776
3942
  function referenceTargetSymbol(type) {
3777
- if ((type.flags & ts14.TypeFlags.Object) === 0)
3943
+ if ((type.flags & ts15.TypeFlags.Object) === 0)
3778
3944
  return;
3779
- if ((type.objectFlags & ts14.ObjectFlags.Reference) === 0)
3945
+ if ((type.objectFlags & ts15.ObjectFlags.Reference) === 0)
3780
3946
  return;
3781
3947
  return type.target.symbol;
3782
3948
  }
3783
3949
  function ambientHashAliasTargets(checker, anchor) {
3784
3950
  const targets = new Set;
3785
- for (const symbol of checker.getSymbolsInScope(anchor, ts14.SymbolFlags.Type)) {
3951
+ for (const symbol of checker.getSymbolsInScope(anchor, ts15.SymbolFlags.Type)) {
3786
3952
  if (symbol.name !== "Hash")
3787
3953
  continue;
3788
3954
  const target = referenceTargetSymbol(checker.getDeclaredTypeOfSymbol(symbol));
@@ -3798,7 +3964,7 @@ function canonicalHashSymbols(checker, program) {
3798
3964
  if (!anchor)
3799
3965
  return canonical;
3800
3966
  const aliased = ambientHashAliasTargets(checker, anchor);
3801
- for (const symbol of checker.getSymbolsInScope(anchor, ts14.SymbolFlags.Function)) {
3967
+ for (const symbol of checker.getSymbolsInScope(anchor, ts15.SymbolFlags.Function)) {
3802
3968
  if (symbol.name !== "hash")
3803
3969
  continue;
3804
3970
  const declaration = symbol.declarations?.[0];
@@ -3820,7 +3986,7 @@ function hashedSourceOfType(checker, type, canonical) {
3820
3986
  return source?.isStringLiteral() ? source.value : undefined;
3821
3987
  }
3822
3988
  function staticAddressTextOf(checker, node, canonical) {
3823
- return ts14.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
3989
+ return ts15.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
3824
3990
  }
3825
3991
 
3826
3992
  // src/url-cross-world-address.ts
@@ -3844,7 +4010,7 @@ function checkCrossWorldAddresses(input) {
3844
4010
  if (worlds.length === 0)
3845
4011
  continue;
3846
4012
  const visit = (node) => {
3847
- if (ts15.isExpression(node)) {
4013
+ if (ts16.isExpression(node)) {
3848
4014
  const entry = addressEntryOfArgument(checker, table, node);
3849
4015
  if (entry !== undefined && slotRejectsForeignSocket(entry)) {
3850
4016
  const address = staticAddressTextOf(checker, node, canonical);
@@ -3861,14 +4027,14 @@ function checkCrossWorldAddresses(input) {
3861
4027
  }
3862
4028
  }
3863
4029
  }
3864
- ts15.forEachChild(node, visit);
4030
+ ts16.forEachChild(node, visit);
3865
4031
  };
3866
- ts15.forEachChild(sourceFile, visit);
4032
+ ts16.forEachChild(sourceFile, visit);
3867
4033
  }
3868
4034
  return findings;
3869
4035
  }
3870
4036
  // src/url-fragment-reachability.ts
3871
- import * as ts16 from "typescript";
4037
+ import * as ts17 from "typescript";
3872
4038
  function pathOfKey(key) {
3873
4039
  const socket = socketOfAddress(key);
3874
4040
  return socket === undefined ? key : key.slice(socket.length + 1);
@@ -3913,7 +4079,7 @@ function checkUrlFragmentReachability(input) {
3913
4079
  if (isAmbient2(sourceFile.fileName))
3914
4080
  continue;
3915
4081
  const visit = (node) => {
3916
- if (ts16.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
4082
+ if (ts17.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
3917
4083
  const text = staticAddressTextOf(checker, node, canonical);
3918
4084
  if (text !== undefined) {
3919
4085
  const hash = text.indexOf("#");
@@ -3932,9 +4098,9 @@ function checkUrlFragmentReachability(input) {
3932
4098
  }
3933
4099
  }
3934
4100
  }
3935
- ts16.forEachChild(node, visit);
4101
+ ts17.forEachChild(node, visit);
3936
4102
  };
3937
- ts16.forEachChild(sourceFile, visit);
4103
+ ts17.forEachChild(sourceFile, visit);
3938
4104
  }
3939
4105
  return { kind: "checked", findings };
3940
4106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defold-typescript/transpiler",
3
- "version": "0.38.0",
3
+ "version": "0.40.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.38.0",
38
+ "@defold-typescript/types": "0.40.0",
39
39
  "@typescript-to-lua/language-extensions": "1.19.0",
40
40
  "typescript-to-lua": "^1.36.0"
41
41
  }
@@ -0,0 +1,212 @@
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
+ };
@@ -1,5 +1,6 @@
1
1
  import * as ts from "typescript";
2
2
  import * as tstl from "typescript-to-lua";
3
+ import { arrayIndexOptionLoweringPlugin } from "./array-index-option-lowering";
3
4
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
4
5
  import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
5
6
  import { messageGuardLoweringPlugin } from "./message-guard-lowering";
@@ -14,6 +15,7 @@ const LUA_PLUGINS: tstl.Plugin[] = [
14
15
  windowEventGuardLoweringPlugin,
15
16
  messageDispatchLoweringPlugin,
16
17
  timersLoweringPlugin,
18
+ arrayIndexOptionLoweringPlugin,
17
19
  ];
18
20
 
19
21
  const noopWriteFile: ts.WriteFileCallback = () => {};
package/src/session.ts CHANGED
@@ -3,6 +3,7 @@ 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";
6
7
  import { type CompanionEmitter, createCompanionEmitPlugin } from "./companion-emit";
7
8
  import { editorScriptErasurePlugin } from "./editor-script-erasure";
8
9
  import { lifecycleErasurePlugin } from "./lifecycle-erasure";
@@ -43,6 +44,7 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
43
44
  { plugin: windowEventGuardLoweringPlugin },
44
45
  { plugin: messageDispatchLoweringPlugin },
45
46
  { plugin: timersLoweringPlugin },
47
+ { plugin: arrayIndexOptionLoweringPlugin },
46
48
  { plugin: companionEmit.plugin },
47
49
  ],
48
50
  };
package/src/transpile.ts CHANGED
@@ -3,6 +3,7 @@ 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";
6
7
  import { createCompanionEmitPlugin } from "./companion-emit";
7
8
  import { editorScriptErasurePlugin } from "./editor-script-erasure";
8
9
  import {
@@ -321,6 +322,7 @@ export function transpileProject(input: TranspileProjectInput): TranspileProject
321
322
  { plugin: windowEventGuardLoweringPlugin },
322
323
  { plugin: messageDispatchLoweringPlugin },
323
324
  { plugin: timersLoweringPlugin },
325
+ { plugin: arrayIndexOptionLoweringPlugin },
324
326
  { plugin: companionEmit.plugin },
325
327
  ],
326
328
  });