@defold-typescript/transpiler 0.53.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +237 -56
- package/dist/lua-require-scan.d.ts +4 -0
- package/dist/native-global-modules.d.ts +20 -0
- package/package.json +2 -2
- package/src/lua-require-scan.ts +0 -0
- package/src/native-global-modules.ts +183 -0
- package/src/session.ts +2 -0
- package/src/transpile.ts +30 -2
package/dist/index.js
CHANGED
|
@@ -1665,6 +1665,41 @@ function mask(lua) {
|
|
|
1665
1665
|
return { text, literals };
|
|
1666
1666
|
}
|
|
1667
1667
|
var REQUIRE_RE = new RegExp(String.raw`(?<![A-Za-z0-9_.])require\s*\(\s*${PLACEHOLDER}(\d+)${PLACEHOLDER}\s*\)`, "g");
|
|
1668
|
+
function isLuaSpace(c) {
|
|
1669
|
+
return c === " " || c === "\t" || c === `
|
|
1670
|
+
` || c === "\r";
|
|
1671
|
+
}
|
|
1672
|
+
var REQUIRE_WORD = "require";
|
|
1673
|
+
function callSpan(lua, literal) {
|
|
1674
|
+
let i = literal.start - 1;
|
|
1675
|
+
while (isLuaSpace(lua[i])) {
|
|
1676
|
+
i--;
|
|
1677
|
+
}
|
|
1678
|
+
if (lua[i] !== "(") {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
i--;
|
|
1682
|
+
while (isLuaSpace(lua[i])) {
|
|
1683
|
+
i--;
|
|
1684
|
+
}
|
|
1685
|
+
const wordEnd = i + 1;
|
|
1686
|
+
const wordStart = wordEnd - REQUIRE_WORD.length;
|
|
1687
|
+
if (wordStart < 0 || lua.slice(wordStart, wordEnd) !== REQUIRE_WORD) {
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
const before = lua[wordStart - 1];
|
|
1691
|
+
if (before !== undefined && /[A-Za-z0-9_.]/.test(before)) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
let j = literal.end;
|
|
1695
|
+
while (isLuaSpace(lua[j])) {
|
|
1696
|
+
j++;
|
|
1697
|
+
}
|
|
1698
|
+
if (lua[j] !== ")") {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
return { callStart: wordStart, callEnd: j + 1 };
|
|
1702
|
+
}
|
|
1668
1703
|
function scanEmittedRequires(lua) {
|
|
1669
1704
|
const { text, literals } = mask(lua);
|
|
1670
1705
|
const sites = [];
|
|
@@ -1673,7 +1708,12 @@ function scanEmittedRequires(lua) {
|
|
|
1673
1708
|
if (literal === undefined) {
|
|
1674
1709
|
continue;
|
|
1675
1710
|
}
|
|
1676
|
-
sites.push({
|
|
1711
|
+
sites.push({
|
|
1712
|
+
path: literal.value,
|
|
1713
|
+
start: literal.start,
|
|
1714
|
+
end: literal.end,
|
|
1715
|
+
...callSpan(lua, literal)
|
|
1716
|
+
});
|
|
1677
1717
|
}
|
|
1678
1718
|
return sites;
|
|
1679
1719
|
}
|
|
@@ -3186,7 +3226,7 @@ function relativeAddressesFrom(index, context) {
|
|
|
3186
3226
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3187
3227
|
import { createRequire as createRequire2 } from "node:module";
|
|
3188
3228
|
import * as path4 from "node:path";
|
|
3189
|
-
import * as
|
|
3229
|
+
import * as ts17 from "typescript";
|
|
3190
3230
|
import * as tstl3 from "typescript-to-lua";
|
|
3191
3231
|
|
|
3192
3232
|
// src/companion-emit.ts
|
|
@@ -3735,6 +3775,128 @@ var editorScriptErasurePlugin = {
|
|
|
3735
3775
|
}
|
|
3736
3776
|
};
|
|
3737
3777
|
|
|
3778
|
+
// src/native-global-modules.ts
|
|
3779
|
+
import * as ts15 from "typescript";
|
|
3780
|
+
var GLOBAL_TABLE_TAG = "globalTable";
|
|
3781
|
+
var GLOBAL_TABLE_REQUIRE_MARKER = "@defold-typescript:global-table:";
|
|
3782
|
+
var NO_RESOLUTION_PREFIX = "@NoResolution:";
|
|
3783
|
+
var LUA_GLOBAL_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
3784
|
+
function resolveGlobalTableTag(specifier, checker) {
|
|
3785
|
+
if (!ts15.isStringLiteral(specifier)) {
|
|
3786
|
+
return { kind: "untagged" };
|
|
3787
|
+
}
|
|
3788
|
+
const symbol = checker.getSymbolAtLocation(specifier);
|
|
3789
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
3790
|
+
if (!ts15.isModuleDeclaration(declaration) || !ts15.isStringLiteral(declaration.name)) {
|
|
3791
|
+
continue;
|
|
3792
|
+
}
|
|
3793
|
+
const tag = ts15.getJSDocTags(declaration).find((t) => t.tagName.text === GLOBAL_TABLE_TAG);
|
|
3794
|
+
if (tag === undefined) {
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
const comment = ts15.getTextOfJSDocComment(tag.comment)?.trim();
|
|
3798
|
+
const globalName = comment === undefined || comment === "" ? specifier.text : comment.split(/\s+/)[0] ?? specifier.text;
|
|
3799
|
+
if (!LUA_GLOBAL_NAME_RE.test(globalName)) {
|
|
3800
|
+
return { kind: "invalid", globalName, moduleId: specifier.text };
|
|
3801
|
+
}
|
|
3802
|
+
return { kind: "tagged", globalName };
|
|
3803
|
+
}
|
|
3804
|
+
return { kind: "untagged" };
|
|
3805
|
+
}
|
|
3806
|
+
function markerRequirePath(globalName) {
|
|
3807
|
+
return `${NO_RESOLUTION_PREFIX}${GLOBAL_TABLE_REQUIRE_MARKER}${globalName}`;
|
|
3808
|
+
}
|
|
3809
|
+
function invalidGlobalName(node, resolution) {
|
|
3810
|
+
const file = node.getSourceFile();
|
|
3811
|
+
const start = node.getStart(file);
|
|
3812
|
+
return {
|
|
3813
|
+
file,
|
|
3814
|
+
start,
|
|
3815
|
+
length: node.getEnd() - start,
|
|
3816
|
+
category: ts15.DiagnosticCategory.Error,
|
|
3817
|
+
code: 0,
|
|
3818
|
+
messageText: `@${GLOBAL_TABLE_TAG} on module "${resolution.moduleId}" names \`${resolution.globalName}\`, which is not a valid Lua global name; use dotted identifiers (\`pixels\`, \`b2d.body\`).`
|
|
3819
|
+
};
|
|
3820
|
+
}
|
|
3821
|
+
var nativeGlobalModulesPlugin = {
|
|
3822
|
+
visitors: {
|
|
3823
|
+
[ts15.SyntaxKind.ImportDeclaration]: (node, context) => {
|
|
3824
|
+
const resolution = resolveGlobalTableTag(node.moduleSpecifier, context.checker);
|
|
3825
|
+
if (resolution.kind === "invalid") {
|
|
3826
|
+
context.diagnostics.push(invalidGlobalName(node.moduleSpecifier, resolution));
|
|
3827
|
+
return context.superTransformStatements(node);
|
|
3828
|
+
}
|
|
3829
|
+
if (resolution.kind === "untagged") {
|
|
3830
|
+
return context.superTransformStatements(node);
|
|
3831
|
+
}
|
|
3832
|
+
if (node.importClause === undefined) {
|
|
3833
|
+
return [];
|
|
3834
|
+
}
|
|
3835
|
+
const rewritten = ts15.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts15.factory.createStringLiteral(markerRequirePath(resolution.globalName)), node.attributes);
|
|
3836
|
+
return context.superTransformStatements(rewritten);
|
|
3837
|
+
},
|
|
3838
|
+
[ts15.SyntaxKind.ExportDeclaration]: (node, context) => {
|
|
3839
|
+
const specifier = node.moduleSpecifier;
|
|
3840
|
+
if (specifier === undefined) {
|
|
3841
|
+
return context.superTransformStatements(node);
|
|
3842
|
+
}
|
|
3843
|
+
const resolution = resolveGlobalTableTag(specifier, context.checker);
|
|
3844
|
+
if (resolution.kind === "invalid") {
|
|
3845
|
+
context.diagnostics.push(invalidGlobalName(specifier, resolution));
|
|
3846
|
+
return context.superTransformStatements(node);
|
|
3847
|
+
}
|
|
3848
|
+
if (resolution.kind === "untagged") {
|
|
3849
|
+
return context.superTransformStatements(node);
|
|
3850
|
+
}
|
|
3851
|
+
const rewritten = ts15.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, ts15.factory.createStringLiteral(markerRequirePath(resolution.globalName)), node.attributes);
|
|
3852
|
+
return context.superTransformStatements(rewritten);
|
|
3853
|
+
},
|
|
3854
|
+
[ts15.SyntaxKind.CallExpression]: (node, context) => {
|
|
3855
|
+
const specifier = node.arguments[0];
|
|
3856
|
+
if (node.expression.kind !== ts15.SyntaxKind.ImportKeyword || specifier === undefined) {
|
|
3857
|
+
return context.superTransformExpression(node);
|
|
3858
|
+
}
|
|
3859
|
+
const resolution = resolveGlobalTableTag(specifier, context.checker);
|
|
3860
|
+
if (resolution.kind === "invalid") {
|
|
3861
|
+
context.diagnostics.push(invalidGlobalName(specifier, resolution));
|
|
3862
|
+
return context.superTransformExpression(node);
|
|
3863
|
+
}
|
|
3864
|
+
if (resolution.kind === "untagged") {
|
|
3865
|
+
return context.superTransformExpression(node);
|
|
3866
|
+
}
|
|
3867
|
+
const rewritten = ts15.factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
3868
|
+
ts15.factory.createStringLiteral(markerRequirePath(resolution.globalName)),
|
|
3869
|
+
...node.arguments.slice(1)
|
|
3870
|
+
]);
|
|
3871
|
+
return context.superTransformExpression(rewritten);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
};
|
|
3875
|
+
function rewriteGlobalAliasRequires(lua) {
|
|
3876
|
+
let out = "";
|
|
3877
|
+
let cursor = 0;
|
|
3878
|
+
for (const site of scanEmittedRequires(lua)) {
|
|
3879
|
+
if (!site.path.startsWith(GLOBAL_TABLE_REQUIRE_MARKER)) {
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
3882
|
+
if (site.callStart === undefined || site.callEnd === undefined) {
|
|
3883
|
+
continue;
|
|
3884
|
+
}
|
|
3885
|
+
out += lua.slice(cursor, site.callStart);
|
|
3886
|
+
out += `_G.${site.path.slice(GLOBAL_TABLE_REQUIRE_MARKER.length)}`;
|
|
3887
|
+
cursor = site.callEnd;
|
|
3888
|
+
}
|
|
3889
|
+
return out + lua.slice(cursor);
|
|
3890
|
+
}
|
|
3891
|
+
function findGlobalAliasMarkerLeak(lua) {
|
|
3892
|
+
for (const site of scanEmittedRequires(lua)) {
|
|
3893
|
+
if (site.path.startsWith(GLOBAL_TABLE_REQUIRE_MARKER)) {
|
|
3894
|
+
return site.path.slice(GLOBAL_TABLE_REQUIRE_MARKER.length);
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3738
3900
|
// src/transpile.ts
|
|
3739
3901
|
import { readdirSync as readdirSync2, readFileSync } from "node:fs";
|
|
3740
3902
|
import { createRequire } from "node:module";
|
|
@@ -3742,17 +3904,17 @@ import * as path3 from "node:path";
|
|
|
3742
3904
|
import * as tstl2 from "typescript-to-lua";
|
|
3743
3905
|
|
|
3744
3906
|
// src/go-property-direct-call.ts
|
|
3745
|
-
import * as
|
|
3907
|
+
import * as ts16 from "typescript";
|
|
3746
3908
|
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).";
|
|
3747
3909
|
function findDirectGoPropertyCalls(sourceFile) {
|
|
3748
3910
|
const calls = [];
|
|
3749
3911
|
const visit = (node) => {
|
|
3750
|
-
if (
|
|
3912
|
+
if (ts16.isCallExpression(node) && ts16.isPropertyAccessExpression(node.expression) && node.expression.name.text === "property" && ts16.isIdentifier(node.expression.expression) && node.expression.expression.text === "go") {
|
|
3751
3913
|
calls.push(node);
|
|
3752
3914
|
}
|
|
3753
|
-
|
|
3915
|
+
ts16.forEachChild(node, visit);
|
|
3754
3916
|
};
|
|
3755
|
-
|
|
3917
|
+
ts16.forEachChild(sourceFile, visit);
|
|
3756
3918
|
return calls;
|
|
3757
3919
|
}
|
|
3758
3920
|
|
|
@@ -3860,6 +4022,7 @@ function collectOutputs(transpiledFiles, diagnostics, userKeys, emittedCompanion
|
|
|
3860
4022
|
const lua = {};
|
|
3861
4023
|
const sourceMaps = {};
|
|
3862
4024
|
const companions = {};
|
|
4025
|
+
const markerLeaks = [];
|
|
3863
4026
|
let lualib;
|
|
3864
4027
|
for (const file of transpiledFiles) {
|
|
3865
4028
|
if (isLualibBundle(file) && typeof file.lua === "string") {
|
|
@@ -3870,13 +4033,23 @@ function collectOutputs(transpiledFiles, diagnostics, userKeys, emittedCompanion
|
|
|
3870
4033
|
if (!userSource || typeof file.lua !== "string") {
|
|
3871
4034
|
continue;
|
|
3872
4035
|
}
|
|
3873
|
-
|
|
4036
|
+
const swapped = rewriteGlobalAliasRequires(file.lua);
|
|
4037
|
+
lua[userSource.fileName] = swapped;
|
|
4038
|
+
const leaked = findGlobalAliasMarkerLeak(swapped);
|
|
4039
|
+
if (leaked !== undefined) {
|
|
4040
|
+
markerLeaks.push({ file: userSource.fileName, globalName: leaked });
|
|
4041
|
+
}
|
|
3874
4042
|
if (typeof file.luaSourceMap === "string") {
|
|
3875
4043
|
sourceMaps[userSource.fileName] = file.luaSourceMap;
|
|
3876
4044
|
}
|
|
3877
4045
|
const companion = emittedCompanions.get(userSource.fileName);
|
|
3878
4046
|
if (companion !== undefined) {
|
|
3879
|
-
|
|
4047
|
+
const swappedCompanion = rewriteGlobalAliasRequires(companion);
|
|
4048
|
+
companions[userSource.fileName] = swappedCompanion;
|
|
4049
|
+
const companionLeak = findGlobalAliasMarkerLeak(swappedCompanion);
|
|
4050
|
+
if (companionLeak !== undefined) {
|
|
4051
|
+
markerLeaks.push({ file: userSource.fileName, globalName: companionLeak });
|
|
4052
|
+
}
|
|
3880
4053
|
}
|
|
3881
4054
|
}
|
|
3882
4055
|
const collectedDiagnostics = diagnostics.map((d) => {
|
|
@@ -3891,6 +4064,12 @@ function collectOutputs(transpiledFiles, diagnostics, userKeys, emittedCompanion
|
|
|
3891
4064
|
const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
|
|
3892
4065
|
return { file: fileName, message, line: line + 1, column: character + 1 };
|
|
3893
4066
|
});
|
|
4067
|
+
for (const leak of markerLeaks) {
|
|
4068
|
+
collectedDiagnostics.push({
|
|
4069
|
+
file: leak.file,
|
|
4070
|
+
message: `@globalTable lowering left an unrewritten require for \`${leak.globalName}\` in the emitted chunk; the output would fail to load in Defold.`
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
3894
4073
|
const scanned = new Set;
|
|
3895
4074
|
let timersImported = false;
|
|
3896
4075
|
for (const file of transpiledFiles) {
|
|
@@ -3951,6 +4130,7 @@ function transpileVirtual(files, companionEmit, overrides = {}) {
|
|
|
3951
4130
|
{ plugin: physicsEventGuardLoweringPlugin },
|
|
3952
4131
|
{ plugin: messageDispatchLoweringPlugin },
|
|
3953
4132
|
{ plugin: timersLoweringPlugin },
|
|
4133
|
+
{ plugin: nativeGlobalModulesPlugin },
|
|
3954
4134
|
{ plugin: hsmLoweringPlugin },
|
|
3955
4135
|
{ plugin: typeApplicationErasurePlugin },
|
|
3956
4136
|
{ plugin: companionEmit.plugin }
|
|
@@ -4042,6 +4222,7 @@ function compilerOptions(companionEmit) {
|
|
|
4042
4222
|
{ plugin: physicsEventGuardLoweringPlugin },
|
|
4043
4223
|
{ plugin: messageDispatchLoweringPlugin },
|
|
4044
4224
|
{ plugin: timersLoweringPlugin },
|
|
4225
|
+
{ plugin: nativeGlobalModulesPlugin },
|
|
4045
4226
|
{ plugin: hsmLoweringPlugin },
|
|
4046
4227
|
{ plugin: typeApplicationErasurePlugin },
|
|
4047
4228
|
{ plugin: companionEmit.plugin }
|
|
@@ -4086,7 +4267,7 @@ function createTranspileSession() {
|
|
|
4086
4267
|
if (cached) {
|
|
4087
4268
|
return cached;
|
|
4088
4269
|
}
|
|
4089
|
-
const created =
|
|
4270
|
+
const created = ts17.createSourceFile(fileName, content, ts17.ScriptTarget.Latest, false);
|
|
4090
4271
|
sourceFileCache.set(normalized, created);
|
|
4091
4272
|
return created;
|
|
4092
4273
|
}
|
|
@@ -4105,7 +4286,7 @@ function createTranspileSession() {
|
|
|
4105
4286
|
return cached;
|
|
4106
4287
|
}
|
|
4107
4288
|
const fileContent = readFileSync2(filePath, "utf8");
|
|
4108
|
-
const created =
|
|
4289
|
+
const created = ts17.createSourceFile(filePath, fileContent, ts17.ScriptTarget.Latest, false);
|
|
4109
4290
|
libCache.set(fileName, created);
|
|
4110
4291
|
return created;
|
|
4111
4292
|
}
|
|
@@ -4118,10 +4299,10 @@ function createTranspileSession() {
|
|
|
4118
4299
|
return AMBIENT_FILES[normalized];
|
|
4119
4300
|
}
|
|
4120
4301
|
const host = {
|
|
4121
|
-
fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined ||
|
|
4302
|
+
fileExists: (fileName) => mergedContent(normalizeSlashes(fileName)) !== undefined || ts17.sys.fileExists(fileName),
|
|
4122
4303
|
getCanonicalFileName: (fileName) => fileName,
|
|
4123
4304
|
getCurrentDirectory: () => "",
|
|
4124
|
-
getDefaultLibFileName:
|
|
4305
|
+
getDefaultLibFileName: ts17.getDefaultLibFileName,
|
|
4125
4306
|
readFile: () => "",
|
|
4126
4307
|
getNewLine: () => `
|
|
4127
4308
|
`,
|
|
@@ -4143,15 +4324,15 @@ function createTranspileSession() {
|
|
|
4143
4324
|
const userKeys = new Set(userFiles.keys());
|
|
4144
4325
|
const rootNames = [...Object.keys(AMBIENT_FILES).map(normalizeSlashes), ...userKeys];
|
|
4145
4326
|
const companionEmit = createCompanionEmitPlugin();
|
|
4146
|
-
program =
|
|
4147
|
-
const preEmitDiagnostics =
|
|
4327
|
+
program = ts17.createProgram(rootNames, compilerOptions(companionEmit), host, program);
|
|
4328
|
+
const preEmitDiagnostics = ts17.getPreEmitDiagnostics(program);
|
|
4148
4329
|
const collector = createOutputCollector();
|
|
4149
4330
|
const { diagnostics: transpileDiagnostics } = new tstl3.Transpiler().emit({
|
|
4150
4331
|
program,
|
|
4151
4332
|
writeFile: collector.writeFile
|
|
4152
4333
|
});
|
|
4153
4334
|
const diagnostics = [
|
|
4154
|
-
...
|
|
4335
|
+
...ts17.sortAndDeduplicateDiagnostics([...preEmitDiagnostics, ...transpileDiagnostics])
|
|
4155
4336
|
];
|
|
4156
4337
|
return collectOutputs(collector.files, diagnostics, userKeys, companionEmit.companions);
|
|
4157
4338
|
}
|
|
@@ -4161,7 +4342,7 @@ function createTranspileSession() {
|
|
|
4161
4342
|
};
|
|
4162
4343
|
}
|
|
4163
4344
|
// src/text-script-property.ts
|
|
4164
|
-
import * as
|
|
4345
|
+
import * as ts18 from "typescript";
|
|
4165
4346
|
function findTextScriptProperties(program, fileNames) {
|
|
4166
4347
|
const checker = program.getTypeChecker();
|
|
4167
4348
|
const findings = [];
|
|
@@ -4172,7 +4353,7 @@ function findTextScriptProperties(program, fileNames) {
|
|
|
4172
4353
|
}
|
|
4173
4354
|
const located = [];
|
|
4174
4355
|
const visit = (node) => {
|
|
4175
|
-
if (
|
|
4356
|
+
if (ts18.isCallExpression(node)) {
|
|
4176
4357
|
const hooks = factoryHooks(node, checker);
|
|
4177
4358
|
for (const property of hooks?.properties ?? []) {
|
|
4178
4359
|
if (!isPropertiesHook(property)) {
|
|
@@ -4185,7 +4366,7 @@ function findTextScriptProperties(program, fileNames) {
|
|
|
4185
4366
|
}
|
|
4186
4367
|
}
|
|
4187
4368
|
}
|
|
4188
|
-
|
|
4369
|
+
ts18.forEachChild(node, visit);
|
|
4189
4370
|
};
|
|
4190
4371
|
visit(sourceFile);
|
|
4191
4372
|
for (const call of findDirectGoPropertyCalls(sourceFile)) {
|
|
@@ -4194,7 +4375,7 @@ function findTextScriptProperties(program, fileNames) {
|
|
|
4194
4375
|
continue;
|
|
4195
4376
|
}
|
|
4196
4377
|
if (mayHoldString(checker.getTypeAtLocation(value), checker)) {
|
|
4197
|
-
const name =
|
|
4378
|
+
const name = ts18.isStringLiteralLike(nameArgument) ? nameArgument.text : nameArgument.getText(sourceFile);
|
|
4198
4379
|
located.push({ name, value });
|
|
4199
4380
|
}
|
|
4200
4381
|
}
|
|
@@ -4211,20 +4392,20 @@ function mayHoldString(type, checker, visited = new Set) {
|
|
|
4211
4392
|
return false;
|
|
4212
4393
|
}
|
|
4213
4394
|
visited.add(type);
|
|
4214
|
-
if (type.flags &
|
|
4395
|
+
if (type.flags & ts18.TypeFlags.StringLike) {
|
|
4215
4396
|
return true;
|
|
4216
4397
|
}
|
|
4217
4398
|
if (type.isUnionOrIntersection()) {
|
|
4218
4399
|
return type.types.some((member) => mayHoldString(member, checker, visited));
|
|
4219
4400
|
}
|
|
4220
|
-
if (type.flags &
|
|
4401
|
+
if (type.flags & ts18.TypeFlags.Instantiable) {
|
|
4221
4402
|
const constraint = checker.getBaseConstraintOfType(type);
|
|
4222
4403
|
return constraint !== undefined && constraint !== type ? mayHoldString(constraint, checker, visited) : false;
|
|
4223
4404
|
}
|
|
4224
4405
|
return false;
|
|
4225
4406
|
}
|
|
4226
4407
|
// src/url-address-slots.ts
|
|
4227
|
-
import * as
|
|
4408
|
+
import * as ts19 from "typescript";
|
|
4228
4409
|
var ADDRESS_CLASSES = {
|
|
4229
4410
|
"game-object": true,
|
|
4230
4411
|
component: true,
|
|
@@ -4243,7 +4424,7 @@ function isFragmentCaret(slot, position) {
|
|
|
4243
4424
|
return slot.fragmentStart !== -1 && position >= slot.fragmentStart;
|
|
4244
4425
|
}
|
|
4245
4426
|
function tableKey(checker, callee) {
|
|
4246
|
-
const symbol = checker.getSymbolAtLocation(
|
|
4427
|
+
const symbol = checker.getSymbolAtLocation(ts19.isIdentifier(callee) ? callee : callee.name);
|
|
4247
4428
|
if (!symbol)
|
|
4248
4429
|
return;
|
|
4249
4430
|
const fqn = checker.getFullyQualifiedName(symbol);
|
|
@@ -4251,43 +4432,43 @@ function tableKey(checker, callee) {
|
|
|
4251
4432
|
}
|
|
4252
4433
|
function parameterName(checker, call, index) {
|
|
4253
4434
|
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
4254
|
-
if (!declaration || !
|
|
4435
|
+
if (!declaration || !ts19.isFunctionLike(declaration))
|
|
4255
4436
|
return;
|
|
4256
4437
|
const parameter = declaration.parameters[index];
|
|
4257
|
-
if (!parameter || parameter.dotDotDotToken || !
|
|
4438
|
+
if (!parameter || parameter.dotDotDotToken || !ts19.isIdentifier(parameter.name))
|
|
4258
4439
|
return;
|
|
4259
4440
|
return parameter.name.text;
|
|
4260
4441
|
}
|
|
4261
4442
|
var EQUALITY_OPERATORS = new Set([
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4443
|
+
ts19.SyntaxKind.EqualsEqualsEqualsToken,
|
|
4444
|
+
ts19.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
4445
|
+
ts19.SyntaxKind.EqualsEqualsToken,
|
|
4446
|
+
ts19.SyntaxKind.ExclamationEqualsToken
|
|
4266
4447
|
]);
|
|
4267
4448
|
function comparisonSatisfied(checker, entry, call) {
|
|
4268
4449
|
if (entry.comparedParameter === undefined)
|
|
4269
4450
|
return true;
|
|
4270
4451
|
let node = call;
|
|
4271
|
-
while (
|
|
4452
|
+
while (ts19.isParenthesizedExpression(node.parent))
|
|
4272
4453
|
node = node.parent;
|
|
4273
4454
|
const comparison = node.parent;
|
|
4274
|
-
if (!
|
|
4455
|
+
if (!ts19.isBinaryExpression(comparison))
|
|
4275
4456
|
return false;
|
|
4276
4457
|
if (!EQUALITY_OPERATORS.has(comparison.operatorToken.kind))
|
|
4277
4458
|
return false;
|
|
4278
4459
|
const other = comparison.left === node ? comparison.right : comparison.left;
|
|
4279
4460
|
const symbol = checker.getSymbolAtLocation(other);
|
|
4280
|
-
return (symbol?.declarations ?? []).some((declaration) =>
|
|
4461
|
+
return (symbol?.declarations ?? []).some((declaration) => ts19.isParameter(declaration) && ts19.isIdentifier(declaration.name) && declaration.name.text === entry.comparedParameter);
|
|
4281
4462
|
}
|
|
4282
4463
|
function classifiedEntryOfArgument(checker, table, argument) {
|
|
4283
4464
|
const call = argument.parent;
|
|
4284
|
-
if (!
|
|
4465
|
+
if (!ts19.isCallExpression(call))
|
|
4285
4466
|
return;
|
|
4286
4467
|
const argumentIndex = call.arguments.indexOf(argument);
|
|
4287
4468
|
if (argumentIndex === -1)
|
|
4288
4469
|
return;
|
|
4289
4470
|
const callee = call.expression;
|
|
4290
|
-
if (!
|
|
4471
|
+
if (!ts19.isPropertyAccessExpression(callee) && !ts19.isIdentifier(callee))
|
|
4291
4472
|
return;
|
|
4292
4473
|
const fqn = tableKey(checker, callee);
|
|
4293
4474
|
if (fqn === undefined)
|
|
@@ -4304,10 +4485,10 @@ function classifiedEntryOfArgument(checker, table, argument) {
|
|
|
4304
4485
|
}
|
|
4305
4486
|
function slotThroughHashedCall(checker, table, literal) {
|
|
4306
4487
|
const call = literal.parent;
|
|
4307
|
-
if (!
|
|
4488
|
+
if (!ts19.isCallExpression(call))
|
|
4308
4489
|
return;
|
|
4309
4490
|
const callee = call.expression;
|
|
4310
|
-
if (!
|
|
4491
|
+
if (!ts19.isPropertyAccessExpression(callee) && !ts19.isIdentifier(callee))
|
|
4311
4492
|
return;
|
|
4312
4493
|
if (tableKey(checker, callee) !== "hash")
|
|
4313
4494
|
return;
|
|
@@ -4325,24 +4506,24 @@ function addressTextOf(checker, entry, call) {
|
|
|
4325
4506
|
if (entry.addressParameter === undefined)
|
|
4326
4507
|
return;
|
|
4327
4508
|
const declaration = checker.getResolvedSignature(call)?.declaration;
|
|
4328
|
-
if (!declaration || !
|
|
4509
|
+
if (!declaration || !ts19.isFunctionLike(declaration))
|
|
4329
4510
|
return;
|
|
4330
|
-
const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken &&
|
|
4511
|
+
const index = declaration.parameters.findIndex((parameter) => !parameter.dotDotDotToken && ts19.isIdentifier(parameter.name) && parameter.name.text === entry.addressParameter);
|
|
4331
4512
|
if (index === -1)
|
|
4332
4513
|
return;
|
|
4333
4514
|
const argument = call.arguments[index];
|
|
4334
|
-
return argument &&
|
|
4515
|
+
return argument && ts19.isStringLiteralLike(argument) ? argument.text : undefined;
|
|
4335
4516
|
}
|
|
4336
4517
|
function innermostLiteralAt(sourceFile, position) {
|
|
4337
4518
|
let found;
|
|
4338
4519
|
const visit = (node) => {
|
|
4339
4520
|
if (position < node.getStart(sourceFile) || position > node.getEnd())
|
|
4340
4521
|
return;
|
|
4341
|
-
if (
|
|
4522
|
+
if (ts19.isStringLiteralLike(node))
|
|
4342
4523
|
found = node;
|
|
4343
|
-
|
|
4524
|
+
ts19.forEachChild(node, visit);
|
|
4344
4525
|
};
|
|
4345
|
-
|
|
4526
|
+
ts19.forEachChild(sourceFile, visit);
|
|
4346
4527
|
return found;
|
|
4347
4528
|
}
|
|
4348
4529
|
function resolveClassifiedSlotAtPosition(input) {
|
|
@@ -4376,23 +4557,23 @@ function resolveClassifiedSlotAtPosition(input) {
|
|
|
4376
4557
|
};
|
|
4377
4558
|
}
|
|
4378
4559
|
// src/url-cross-world-address.ts
|
|
4379
|
-
import * as
|
|
4560
|
+
import * as ts21 from "typescript";
|
|
4380
4561
|
|
|
4381
4562
|
// src/url-address-literals.ts
|
|
4382
|
-
import * as
|
|
4563
|
+
import * as ts20 from "typescript";
|
|
4383
4564
|
function isAmbient2(fileName) {
|
|
4384
4565
|
return /[\\/]node_modules[\\/]/.test(fileName) || /(^|[\\/])lib\.[^\\/]*\.d\.ts$/.test(fileName);
|
|
4385
4566
|
}
|
|
4386
4567
|
function referenceTargetSymbol(type) {
|
|
4387
|
-
if ((type.flags &
|
|
4568
|
+
if ((type.flags & ts20.TypeFlags.Object) === 0)
|
|
4388
4569
|
return;
|
|
4389
|
-
if ((type.objectFlags &
|
|
4570
|
+
if ((type.objectFlags & ts20.ObjectFlags.Reference) === 0)
|
|
4390
4571
|
return;
|
|
4391
4572
|
return type.target.symbol;
|
|
4392
4573
|
}
|
|
4393
4574
|
function ambientHashAliasTargets(checker, anchor) {
|
|
4394
4575
|
const targets = new Set;
|
|
4395
|
-
for (const symbol of checker.getSymbolsInScope(anchor,
|
|
4576
|
+
for (const symbol of checker.getSymbolsInScope(anchor, ts20.SymbolFlags.Type)) {
|
|
4396
4577
|
if (symbol.name !== "Hash")
|
|
4397
4578
|
continue;
|
|
4398
4579
|
const target = referenceTargetSymbol(checker.getDeclaredTypeOfSymbol(symbol));
|
|
@@ -4408,7 +4589,7 @@ function canonicalHashSymbols(checker, program) {
|
|
|
4408
4589
|
if (!anchor)
|
|
4409
4590
|
return canonical;
|
|
4410
4591
|
const aliased = ambientHashAliasTargets(checker, anchor);
|
|
4411
|
-
for (const symbol of checker.getSymbolsInScope(anchor,
|
|
4592
|
+
for (const symbol of checker.getSymbolsInScope(anchor, ts20.SymbolFlags.Function)) {
|
|
4412
4593
|
if (symbol.name !== "hash")
|
|
4413
4594
|
continue;
|
|
4414
4595
|
const declaration = symbol.declarations?.[0];
|
|
@@ -4430,7 +4611,7 @@ function hashedSourceOfType(checker, type, canonical) {
|
|
|
4430
4611
|
return source?.isStringLiteral() ? source.value : undefined;
|
|
4431
4612
|
}
|
|
4432
4613
|
function staticAddressTextOf(checker, node, canonical) {
|
|
4433
|
-
return
|
|
4614
|
+
return ts20.isStringLiteralLike(node) ? node.text : hashedSourceOfType(checker, checker.getTypeAtLocation(node), canonical);
|
|
4434
4615
|
}
|
|
4435
4616
|
|
|
4436
4617
|
// src/url-cross-world-address.ts
|
|
@@ -4454,7 +4635,7 @@ function checkCrossWorldAddresses(input) {
|
|
|
4454
4635
|
if (worlds.length === 0)
|
|
4455
4636
|
continue;
|
|
4456
4637
|
const visit = (node) => {
|
|
4457
|
-
if (
|
|
4638
|
+
if (ts21.isExpression(node)) {
|
|
4458
4639
|
const entry = addressEntryOfArgument(checker, table, node);
|
|
4459
4640
|
if (entry !== undefined && slotRejectsForeignSocket(entry)) {
|
|
4460
4641
|
const address = staticAddressTextOf(checker, node, canonical);
|
|
@@ -4471,14 +4652,14 @@ function checkCrossWorldAddresses(input) {
|
|
|
4471
4652
|
}
|
|
4472
4653
|
}
|
|
4473
4654
|
}
|
|
4474
|
-
|
|
4655
|
+
ts21.forEachChild(node, visit);
|
|
4475
4656
|
};
|
|
4476
|
-
|
|
4657
|
+
ts21.forEachChild(sourceFile, visit);
|
|
4477
4658
|
}
|
|
4478
4659
|
return findings;
|
|
4479
4660
|
}
|
|
4480
4661
|
// src/url-fragment-reachability.ts
|
|
4481
|
-
import * as
|
|
4662
|
+
import * as ts22 from "typescript";
|
|
4482
4663
|
function pathOfKey(key) {
|
|
4483
4664
|
const socket = socketOfAddress(key);
|
|
4484
4665
|
return socket === undefined ? key : key.slice(socket.length + 1);
|
|
@@ -4523,7 +4704,7 @@ function checkUrlFragmentReachability(input) {
|
|
|
4523
4704
|
if (isAmbient2(sourceFile.fileName))
|
|
4524
4705
|
continue;
|
|
4525
4706
|
const visit = (node) => {
|
|
4526
|
-
if (
|
|
4707
|
+
if (ts22.isExpression(node) && isAddressClass(addressClassOfArgument(checker, table, node))) {
|
|
4527
4708
|
const text = staticAddressTextOf(checker, node, canonical);
|
|
4528
4709
|
if (text !== undefined) {
|
|
4529
4710
|
const hash = text.indexOf("#");
|
|
@@ -4542,9 +4723,9 @@ function checkUrlFragmentReachability(input) {
|
|
|
4542
4723
|
}
|
|
4543
4724
|
}
|
|
4544
4725
|
}
|
|
4545
|
-
|
|
4726
|
+
ts22.forEachChild(node, visit);
|
|
4546
4727
|
};
|
|
4547
|
-
|
|
4728
|
+
ts22.forEachChild(sourceFile, visit);
|
|
4548
4729
|
}
|
|
4549
4730
|
return { kind: "checked", findings };
|
|
4550
4731
|
}
|
|
@@ -5,6 +5,10 @@ export interface EmittedRequireSite {
|
|
|
5
5
|
readonly start: number;
|
|
6
6
|
/** Offset one past its closing delimiter. */
|
|
7
7
|
readonly end: number;
|
|
8
|
+
/** Offset of the `require` keyword, when the whole call is recoverable. */
|
|
9
|
+
readonly callStart?: number;
|
|
10
|
+
/** Offset one past the closing paren, when the whole call is recoverable. */
|
|
11
|
+
readonly callEnd?: number;
|
|
8
12
|
}
|
|
9
13
|
/**
|
|
10
14
|
* Every code-position `require` of a string literal in an emitted Lua chunk, in
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from "typescript-to-lua";
|
|
2
|
+
export declare const GLOBAL_TABLE_REQUIRE_MARKER = "@defold-typescript:global-table:";
|
|
3
|
+
export declare const nativeGlobalModulesPlugin: Plugin;
|
|
4
|
+
/**
|
|
5
|
+
* Replace every marker require the scanner can span with the global name it
|
|
6
|
+
* encodes: `local p = require("<marker>pixels")` becomes `local p = _G.pixels`,
|
|
7
|
+
* rooted in `_G` so no local in scope — including one named `pixels` — can
|
|
8
|
+
* shadow the global. A marker site without a recoverable call span is left in
|
|
9
|
+
* place for `findGlobalAliasMarkerLeak` to fail the build on.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rewriteGlobalAliasRequires(lua: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* The global name of the first marker require surviving in a chunk after the
|
|
14
|
+
* swap, or undefined when the chunk is clean. Only require paths the scanner
|
|
15
|
+
* yields count — marker text in an ordinary string or comment is user data,
|
|
16
|
+
* not a leak. A survivor means an emit shape the swap cannot reach; the output
|
|
17
|
+
* would fail to load in Defold, so the caller turns it into a build error
|
|
18
|
+
* rather than shipping it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function findGlobalAliasMarkerLeak(lua: string): string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defold-typescript/transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.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
|
+
"@defold-typescript/types": "0.54.0",
|
|
39
39
|
"@typescript-to-lua/language-extensions": "1.19.0",
|
|
40
40
|
"typescript": "6.0.2",
|
|
41
41
|
"typescript-to-lua": "^1.36.0"
|
package/src/lua-require-scan.ts
CHANGED
|
Binary file
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { Plugin } from "typescript-to-lua";
|
|
3
|
+
import { scanEmittedRequires } from "./lua-require-scan";
|
|
4
|
+
|
|
5
|
+
const GLOBAL_TABLE_TAG = "globalTable";
|
|
6
|
+
|
|
7
|
+
// Survives into the emitted require path, so it must collide with no real
|
|
8
|
+
// module path a build can own: `@` and `:` are impossible in one.
|
|
9
|
+
export const GLOBAL_TABLE_REQUIRE_MARKER = "@defold-typescript:global-table:";
|
|
10
|
+
|
|
11
|
+
// `@NoResolution:` tells TSTL's resolver to leave the path alone (the module
|
|
12
|
+
// has no source file in the program); the resolver strips the prefix, leaving
|
|
13
|
+
// the marker path for the post-pass swap.
|
|
14
|
+
const NO_RESOLUTION_PREFIX = "@NoResolution:";
|
|
15
|
+
|
|
16
|
+
const LUA_GLOBAL_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
17
|
+
|
|
18
|
+
type TagResolution =
|
|
19
|
+
| { readonly kind: "untagged" }
|
|
20
|
+
| { readonly kind: "tagged"; readonly globalName: string }
|
|
21
|
+
| { readonly kind: "invalid"; readonly globalName: string; readonly moduleId: string };
|
|
22
|
+
|
|
23
|
+
// The tag is consulted only when the specifier resolves to an ambient
|
|
24
|
+
// `declare module "<id>"`; a real source file sharing the id resolves to the
|
|
25
|
+
// file and stays on the ordinary require lane.
|
|
26
|
+
function resolveGlobalTableTag(specifier: ts.Expression, checker: ts.TypeChecker): TagResolution {
|
|
27
|
+
if (!ts.isStringLiteral(specifier)) {
|
|
28
|
+
return { kind: "untagged" };
|
|
29
|
+
}
|
|
30
|
+
const symbol = checker.getSymbolAtLocation(specifier);
|
|
31
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
32
|
+
if (!ts.isModuleDeclaration(declaration) || !ts.isStringLiteral(declaration.name)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const tag = ts.getJSDocTags(declaration).find((t) => t.tagName.text === GLOBAL_TABLE_TAG);
|
|
36
|
+
if (tag === undefined) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const comment = ts.getTextOfJSDocComment(tag.comment)?.trim();
|
|
40
|
+
const globalName =
|
|
41
|
+
comment === undefined || comment === ""
|
|
42
|
+
? specifier.text
|
|
43
|
+
: (comment.split(/\s+/)[0] ?? specifier.text);
|
|
44
|
+
if (!LUA_GLOBAL_NAME_RE.test(globalName)) {
|
|
45
|
+
return { kind: "invalid", globalName, moduleId: specifier.text };
|
|
46
|
+
}
|
|
47
|
+
return { kind: "tagged", globalName };
|
|
48
|
+
}
|
|
49
|
+
return { kind: "untagged" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function markerRequirePath(globalName: string): string {
|
|
53
|
+
return `${NO_RESOLUTION_PREFIX}${GLOBAL_TABLE_REQUIRE_MARKER}${globalName}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function invalidGlobalName(
|
|
57
|
+
node: ts.Node,
|
|
58
|
+
resolution: {
|
|
59
|
+
readonly globalName: string;
|
|
60
|
+
readonly moduleId: string;
|
|
61
|
+
},
|
|
62
|
+
): ts.Diagnostic {
|
|
63
|
+
const file = node.getSourceFile();
|
|
64
|
+
const start = node.getStart(file);
|
|
65
|
+
return {
|
|
66
|
+
file,
|
|
67
|
+
start,
|
|
68
|
+
length: node.getEnd() - start,
|
|
69
|
+
category: ts.DiagnosticCategory.Error,
|
|
70
|
+
code: 0,
|
|
71
|
+
messageText: `@${GLOBAL_TABLE_TAG} on module "${resolution.moduleId}" names \`${resolution.globalName}\`, which is not a valid Lua global name; use dotted identifiers (\`pixels\`, \`b2d.body\`).`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const nativeGlobalModulesPlugin: Plugin = {
|
|
76
|
+
visitors: {
|
|
77
|
+
[ts.SyntaxKind.ImportDeclaration]: (node, context) => {
|
|
78
|
+
const resolution = resolveGlobalTableTag(node.moduleSpecifier, context.checker);
|
|
79
|
+
if (resolution.kind === "invalid") {
|
|
80
|
+
context.diagnostics.push(invalidGlobalName(node.moduleSpecifier, resolution));
|
|
81
|
+
return context.superTransformStatements(node);
|
|
82
|
+
}
|
|
83
|
+
if (resolution.kind === "untagged") {
|
|
84
|
+
return context.superTransformStatements(node);
|
|
85
|
+
}
|
|
86
|
+
if (node.importClause === undefined) {
|
|
87
|
+
// A side-effect import of a runtime global loads nothing.
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const rewritten = ts.factory.updateImportDeclaration(
|
|
91
|
+
node,
|
|
92
|
+
node.modifiers,
|
|
93
|
+
node.importClause,
|
|
94
|
+
ts.factory.createStringLiteral(markerRequirePath(resolution.globalName)),
|
|
95
|
+
node.attributes,
|
|
96
|
+
);
|
|
97
|
+
return context.superTransformStatements(rewritten);
|
|
98
|
+
},
|
|
99
|
+
[ts.SyntaxKind.ExportDeclaration]: (node, context) => {
|
|
100
|
+
const specifier = node.moduleSpecifier;
|
|
101
|
+
if (specifier === undefined) {
|
|
102
|
+
return context.superTransformStatements(node);
|
|
103
|
+
}
|
|
104
|
+
const resolution = resolveGlobalTableTag(specifier, context.checker);
|
|
105
|
+
if (resolution.kind === "invalid") {
|
|
106
|
+
context.diagnostics.push(invalidGlobalName(specifier, resolution));
|
|
107
|
+
return context.superTransformStatements(node);
|
|
108
|
+
}
|
|
109
|
+
if (resolution.kind === "untagged") {
|
|
110
|
+
return context.superTransformStatements(node);
|
|
111
|
+
}
|
|
112
|
+
const rewritten = ts.factory.updateExportDeclaration(
|
|
113
|
+
node,
|
|
114
|
+
node.modifiers,
|
|
115
|
+
node.isTypeOnly,
|
|
116
|
+
node.exportClause,
|
|
117
|
+
ts.factory.createStringLiteral(markerRequirePath(resolution.globalName)),
|
|
118
|
+
node.attributes,
|
|
119
|
+
);
|
|
120
|
+
return context.superTransformStatements(rewritten);
|
|
121
|
+
},
|
|
122
|
+
[ts.SyntaxKind.CallExpression]: (node, context) => {
|
|
123
|
+
const specifier = node.arguments[0];
|
|
124
|
+
if (node.expression.kind !== ts.SyntaxKind.ImportKeyword || specifier === undefined) {
|
|
125
|
+
return context.superTransformExpression(node);
|
|
126
|
+
}
|
|
127
|
+
const resolution = resolveGlobalTableTag(specifier, context.checker);
|
|
128
|
+
if (resolution.kind === "invalid") {
|
|
129
|
+
context.diagnostics.push(invalidGlobalName(specifier, resolution));
|
|
130
|
+
return context.superTransformExpression(node);
|
|
131
|
+
}
|
|
132
|
+
if (resolution.kind === "untagged") {
|
|
133
|
+
return context.superTransformExpression(node);
|
|
134
|
+
}
|
|
135
|
+
const rewritten = ts.factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
136
|
+
ts.factory.createStringLiteral(markerRequirePath(resolution.globalName)),
|
|
137
|
+
...node.arguments.slice(1),
|
|
138
|
+
]);
|
|
139
|
+
return context.superTransformExpression(rewritten);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Replace every marker require the scanner can span with the global name it
|
|
146
|
+
* encodes: `local p = require("<marker>pixels")` becomes `local p = _G.pixels`,
|
|
147
|
+
* rooted in `_G` so no local in scope — including one named `pixels` — can
|
|
148
|
+
* shadow the global. A marker site without a recoverable call span is left in
|
|
149
|
+
* place for `findGlobalAliasMarkerLeak` to fail the build on.
|
|
150
|
+
*/
|
|
151
|
+
export function rewriteGlobalAliasRequires(lua: string): string {
|
|
152
|
+
let out = "";
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
for (const site of scanEmittedRequires(lua)) {
|
|
155
|
+
if (!site.path.startsWith(GLOBAL_TABLE_REQUIRE_MARKER)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (site.callStart === undefined || site.callEnd === undefined) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
out += lua.slice(cursor, site.callStart);
|
|
162
|
+
out += `_G.${site.path.slice(GLOBAL_TABLE_REQUIRE_MARKER.length)}`;
|
|
163
|
+
cursor = site.callEnd;
|
|
164
|
+
}
|
|
165
|
+
return out + lua.slice(cursor);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The global name of the first marker require surviving in a chunk after the
|
|
170
|
+
* swap, or undefined when the chunk is clean. Only require paths the scanner
|
|
171
|
+
* yields count — marker text in an ordinary string or comment is user data,
|
|
172
|
+
* not a leak. A survivor means an emit shape the swap cannot reach; the output
|
|
173
|
+
* would fail to load in Defold, so the caller turns it into a build error
|
|
174
|
+
* rather than shipping it.
|
|
175
|
+
*/
|
|
176
|
+
export function findGlobalAliasMarkerLeak(lua: string): string | undefined {
|
|
177
|
+
for (const site of scanEmittedRequires(lua)) {
|
|
178
|
+
if (site.path.startsWith(GLOBAL_TABLE_REQUIRE_MARKER)) {
|
|
179
|
+
return site.path.slice(GLOBAL_TABLE_REQUIRE_MARKER.length);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { hsmLoweringPlugin } from "./hsm-builtin";
|
|
|
9
9
|
import { lifecycleErasurePlugin } from "./lifecycle-erasure";
|
|
10
10
|
import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
|
|
11
11
|
import { messageGuardLoweringPlugin } from "./message-guard-lowering";
|
|
12
|
+
import { nativeGlobalModulesPlugin } from "./native-global-modules";
|
|
12
13
|
import { physicsEventGuardLoweringPlugin } from "./physics-event-guard-lowering";
|
|
13
14
|
import { timersLoweringPlugin } from "./timers-lowering";
|
|
14
15
|
import { AMBIENT_FILES, collectOutputs, type TranspileProjectResult } from "./transpile";
|
|
@@ -54,6 +55,7 @@ function compilerOptions(companionEmit: CompanionEmitter): tstl.CompilerOptions
|
|
|
54
55
|
{ plugin: physicsEventGuardLoweringPlugin },
|
|
55
56
|
{ plugin: messageDispatchLoweringPlugin },
|
|
56
57
|
{ plugin: timersLoweringPlugin },
|
|
58
|
+
{ plugin: nativeGlobalModulesPlugin },
|
|
57
59
|
{ plugin: hsmLoweringPlugin },
|
|
58
60
|
{ plugin: typeApplicationErasurePlugin },
|
|
59
61
|
{ plugin: companionEmit.plugin },
|
package/src/transpile.ts
CHANGED
|
@@ -21,6 +21,11 @@ import { lifecycleErasurePlugin } from "./lifecycle-erasure";
|
|
|
21
21
|
import { scanEmittedRequires } from "./lua-require-scan";
|
|
22
22
|
import { messageDispatchLoweringPlugin } from "./message-dispatch-lowering";
|
|
23
23
|
import { messageGuardLoweringPlugin } from "./message-guard-lowering";
|
|
24
|
+
import {
|
|
25
|
+
findGlobalAliasMarkerLeak,
|
|
26
|
+
nativeGlobalModulesPlugin,
|
|
27
|
+
rewriteGlobalAliasRequires,
|
|
28
|
+
} from "./native-global-modules";
|
|
24
29
|
import { physicsEventGuardLoweringPlugin } from "./physics-event-guard-lowering";
|
|
25
30
|
import { importsTimersModule, timersLoweringPlugin } from "./timers-lowering";
|
|
26
31
|
import { TIMERS_RUNTIME } from "./timers-runtime";
|
|
@@ -258,6 +263,7 @@ export function collectOutputs(
|
|
|
258
263
|
const lua: Record<string, string> = {};
|
|
259
264
|
const sourceMaps: Record<string, string> = {};
|
|
260
265
|
const companions: Record<string, string> = {};
|
|
266
|
+
const markerLeaks: { file: string; globalName: string }[] = [];
|
|
261
267
|
let lualib: string | undefined;
|
|
262
268
|
for (const file of transpiledFiles) {
|
|
263
269
|
if (isLualibBundle(file) && typeof file.lua === "string") {
|
|
@@ -268,13 +274,25 @@ export function collectOutputs(
|
|
|
268
274
|
if (!userSource || typeof file.lua !== "string") {
|
|
269
275
|
continue;
|
|
270
276
|
}
|
|
271
|
-
|
|
277
|
+
// Column offsets after a swap shift, as they already do under the CLI's
|
|
278
|
+
// require respelling — accepted.
|
|
279
|
+
const swapped = rewriteGlobalAliasRequires(file.lua);
|
|
280
|
+
lua[userSource.fileName] = swapped;
|
|
281
|
+
const leaked = findGlobalAliasMarkerLeak(swapped);
|
|
282
|
+
if (leaked !== undefined) {
|
|
283
|
+
markerLeaks.push({ file: userSource.fileName, globalName: leaked });
|
|
284
|
+
}
|
|
272
285
|
if (typeof file.luaSourceMap === "string") {
|
|
273
286
|
sourceMaps[userSource.fileName] = file.luaSourceMap;
|
|
274
287
|
}
|
|
275
288
|
const companion = emittedCompanions.get(userSource.fileName);
|
|
276
289
|
if (companion !== undefined) {
|
|
277
|
-
|
|
290
|
+
const swappedCompanion = rewriteGlobalAliasRequires(companion);
|
|
291
|
+
companions[userSource.fileName] = swappedCompanion;
|
|
292
|
+
const companionLeak = findGlobalAliasMarkerLeak(swappedCompanion);
|
|
293
|
+
if (companionLeak !== undefined) {
|
|
294
|
+
markerLeaks.push({ file: userSource.fileName, globalName: companionLeak });
|
|
295
|
+
}
|
|
278
296
|
}
|
|
279
297
|
}
|
|
280
298
|
|
|
@@ -291,6 +309,15 @@ export function collectOutputs(
|
|
|
291
309
|
return { file: fileName, message, line: line + 1, column: character + 1 };
|
|
292
310
|
});
|
|
293
311
|
|
|
312
|
+
// A surviving marker is a build error, not a fallback: the alternative —
|
|
313
|
+
// leaving the original require — fails silently at load time in Defold.
|
|
314
|
+
for (const leak of markerLeaks) {
|
|
315
|
+
collectedDiagnostics.push({
|
|
316
|
+
file: leak.file,
|
|
317
|
+
message: `@globalTable lowering left an unrewritten require for \`${leak.globalName}\` in the emitted chunk; the output would fail to load in Defold.`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
294
321
|
// Advisory scan of the user TypeScript AST for the deprecated direct
|
|
295
322
|
// `go.property` call, plus detection of the timers polyfill import. Run here
|
|
296
323
|
// (shared by transpileProject and the watch session) so both paths report it
|
|
@@ -378,6 +405,7 @@ function transpileVirtual(
|
|
|
378
405
|
{ plugin: physicsEventGuardLoweringPlugin },
|
|
379
406
|
{ plugin: messageDispatchLoweringPlugin },
|
|
380
407
|
{ plugin: timersLoweringPlugin },
|
|
408
|
+
{ plugin: nativeGlobalModulesPlugin },
|
|
381
409
|
{ plugin: hsmLoweringPlugin },
|
|
382
410
|
{ plugin: typeApplicationErasurePlugin },
|
|
383
411
|
{ plugin: companionEmit.plugin },
|