Skip to content

Commit ae668e1

Browse files
committed
new option recompileLuaLib: Recompile the Lua standard library with custom plugins.
1 parent 8a43f43 commit ae668e1

9 files changed

Lines changed: 175 additions & 18 deletions

File tree

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
"dist/**/*.js",
1919
"dist/**/*.lua",
2020
"dist/**/*.ts",
21-
"dist/lualib/**/*.json"
21+
"dist/lualib/**/*.json",
22+
"src/lualib/**/*.ts",
23+
"src/lualib/**/*.json",
24+
"language-extensions/index.d.ts"
2225
],
2326
"main": "dist/index.js",
2427
"types": "dist/index.d.ts",

src/CompilerOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface TypeScriptToLuaOptions {
4646
tstlVerbose?: boolean;
4747
lua51AllowTryCatchInAsyncAwait?: boolean;
4848
measurePerformance?: boolean;
49+
recompileLuaLib?: boolean;
4950
}
5051

5152
export type CompilerOptions = OmitIndexSignature<ts.CompilerOptions> &

src/LuaLib.ts

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import * as path from "path";
2-
import { EmitHost } from "./transpilation";
2+
import { EmitHost, transpileProject } from "./transpilation";
33
import * as lua from "./LuaAST";
4-
import { LuaTarget } from "./CompilerOptions";
4+
import { LuaTarget, type CompilerOptions } from "./CompilerOptions";
55
import { getOrUpdate } from "./utils";
6+
import { createEmitOutputCollector, type TranspiledFile } from "./transpilation/output-collector";
7+
import { parseConfigFileWithSystem } from "./cli/tsconfig";
8+
import { createDiagnosticReporter } from "./cli/report";
69

710
export enum LuaLibFeature {
811
ArrayAt = "ArrayAt",
@@ -139,12 +142,16 @@ export function resolveLuaLibDir(luaTarget: LuaTarget) {
139142
export const luaLibModulesInfoFileName = "lualib_module_info.json";
140143
const luaLibModulesInfo = new Map<LuaTarget, LuaLibModulesInfo>();
141144

142-
export function getLuaLibModulesInfo(luaTarget: LuaTarget, emitHost: EmitHost): LuaLibModulesInfo {
143-
if (!luaLibModulesInfo.has(luaTarget)) {
145+
export function getLuaLibModulesInfo(luaTarget: LuaTarget, emitHost: EmitHost, useCache = true): LuaLibModulesInfo {
146+
if (!useCache || !luaLibModulesInfo.has(luaTarget)) {
144147
const lualibPath = path.join(resolveLuaLibDir(luaTarget), luaLibModulesInfoFileName);
145148
const result = emitHost.readFile(lualibPath);
146149
if (result !== undefined) {
147-
luaLibModulesInfo.set(luaTarget, JSON.parse(result) as LuaLibModulesInfo);
150+
const info = JSON.parse(result) as LuaLibModulesInfo;
151+
if (!useCache) {
152+
return info;
153+
}
154+
luaLibModulesInfo.set(luaTarget, info);
148155
} else {
149156
throw new Error(`Could not load lualib dependencies from '${lualibPath}'`);
150157
}
@@ -175,7 +182,21 @@ export function getLuaLibExportToFeatureMap(
175182

176183
const lualibFeatureCache = new Map<LuaTarget, Map<LuaLibFeature, string>>();
177184

178-
export function readLuaLibFeature(feature: LuaLibFeature, luaTarget: LuaTarget, emitHost: EmitHost): string {
185+
export function readLuaLibFeature(
186+
feature: LuaLibFeature,
187+
luaTarget: LuaTarget,
188+
emitHost: EmitHost,
189+
useCache = true
190+
): string {
191+
if (!useCache) {
192+
const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`);
193+
const luaLibFeature = emitHost.readFile(featurePath);
194+
if (luaLibFeature === undefined) {
195+
throw new Error(`Could not load lualib feature from '${featurePath}'`);
196+
}
197+
return luaLibFeature;
198+
}
199+
179200
const featureMap = getOrUpdate(lualibFeatureCache, luaTarget, () => new Map());
180201
if (!featureMap.has(feature)) {
181202
const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`);
@@ -257,9 +278,68 @@ export function loadImportedLualibFeatures(
257278
return statements;
258279
}
259280

281+
const recompileLualibCache = new WeakMap<EmitHost, TranspiledFile[]>();
282+
283+
function recompileLuaLibFiles(sourceOptions: CompilerOptions, emitHost: EmitHost): TranspiledFile[] {
284+
let transpiledFiles = recompileLualibCache.get(emitHost);
285+
if (!transpiledFiles) {
286+
const tsconfigPath =
287+
sourceOptions.luaTarget === LuaTarget.Lua50
288+
? path.join(__dirname, "./lualib/tsconfig.lua50.json")
289+
: path.join(__dirname, "./lualib/tsconfig.json");
290+
const config = parseConfigFileWithSystem(tsconfigPath);
291+
const options = config.options;
292+
options.luaPlugins = [...(options.luaPlugins ?? []), ...(sourceOptions.luaPlugins ?? [])];
293+
294+
const collector = createEmitOutputCollector(options.extension);
295+
const reportDiagnostic = createDiagnosticReporter(false);
296+
297+
const { diagnostics } = transpileProject(tsconfigPath, options, collector.writeFile);
298+
diagnostics.forEach(reportDiagnostic);
299+
300+
transpiledFiles = collector.files;
301+
recompileLualibCache.set(emitHost, transpiledFiles);
302+
}
303+
304+
return transpiledFiles;
305+
}
306+
307+
function recompileLuaLibBundle(sourceOptions: CompilerOptions, emitHost: EmitHost): string | undefined {
308+
const transpiledFiles = recompileLuaLibFiles(sourceOptions, emitHost);
309+
const lualibBundle = transpiledFiles.find(f => f.outPath.endsWith("lualib_bundle.lua"));
310+
return lualibBundle?.lua;
311+
}
312+
313+
export function recompileInlineLualibFeatures(
314+
features: Iterable<LuaLibFeature>,
315+
options: CompilerOptions,
316+
emitHost: EmitHost
317+
): string {
318+
const luaTarget = options.luaTarget ?? LuaTarget.Universal;
319+
const transpiledFiles = recompileLuaLibFiles(options, emitHost);
320+
emitHost = {
321+
readFile(filePath: string) {
322+
const file = transpiledFiles.find(f => f.outPath === filePath);
323+
return file ? file.text : undefined;
324+
},
325+
} as any as EmitHost;
326+
const moduleInfo = getLuaLibModulesInfo(luaTarget, emitHost, false);
327+
return resolveRecursiveLualibFeatures(features, luaTarget, emitHost, moduleInfo)
328+
.map(feature => readLuaLibFeature(feature, luaTarget, emitHost, false))
329+
.join("\n");
330+
}
331+
260332
const luaLibBundleContent = new Map<string, string>();
261333

262-
export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost): string {
334+
export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost, options: CompilerOptions): string {
335+
if (options.recompileLuaLib) {
336+
const result = recompileLuaLibBundle(options, emitHost);
337+
if (!result) {
338+
throw new Error(`Failed to recompile lualib bundle`);
339+
}
340+
return result;
341+
}
342+
263343
const lualibPath = path.join(resolveLuaLibDir(luaTarget), "lualib_bundle.lua");
264344
if (!luaLibBundleContent.has(lualibPath)) {
265345
const result = emitHost.readFile(lualibPath);
@@ -279,13 +359,26 @@ export function getLualibBundleReturn(exportedValues: string[]): string {
279359

280360
export function buildMinimalLualibBundle(
281361
features: Iterable<LuaLibFeature>,
282-
luaTarget: LuaTarget,
362+
options: CompilerOptions,
283363
emitHost: EmitHost
284364
): string {
285-
const code = loadInlineLualibFeatures(features, luaTarget, emitHost);
286-
const moduleInfo = getLuaLibModulesInfo(luaTarget, emitHost);
287-
const exports = Array.from(features).flatMap(feature => moduleInfo[feature].exports);
365+
const luaTarget = options.luaTarget ?? LuaTarget.Universal;
366+
let code;
367+
if (options.recompileLuaLib) {
368+
code = recompileInlineLualibFeatures(features, options, emitHost);
369+
const transpiledFiles = recompileLuaLibFiles(options, emitHost);
370+
emitHost = {
371+
readFile(filePath: string) {
372+
const file = transpiledFiles.find(f => f.outPath === filePath);
373+
return file ? file.text : undefined;
374+
},
375+
} as any as EmitHost;
376+
} else {
377+
code = loadInlineLualibFeatures(features, luaTarget, emitHost);
378+
}
288379

380+
const moduleInfo = getLuaLibModulesInfo(luaTarget, emitHost, !options.recompileLuaLib);
381+
const exports = Array.from(features).flatMap(feature => moduleInfo[feature].exports);
289382
return code + getLualibBundleReturn(exports);
290383
}
291384

src/LuaPrinter.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { Mapping, SourceMapGenerator, SourceNode } from "source-map";
33
import * as ts from "typescript";
44
import { CompilerOptions, isBundleEnabled, LuaLibImportKind, LuaTarget } from "./CompilerOptions";
55
import * as lua from "./LuaAST";
6-
import { loadImportedLualibFeatures, loadInlineLualibFeatures, LuaLibFeature } from "./LuaLib";
6+
import {
7+
loadImportedLualibFeatures,
8+
loadInlineLualibFeatures,
9+
LuaLibFeature,
10+
recompileInlineLualibFeatures,
11+
} from "./LuaLib";
712
import { isValidLuaIdentifier, shouldAllowUnicode } from "./transformation/utils/safe-names";
813
import { EmitHost, getEmitPath } from "./transpilation";
914
import { intersperse, normalizeSlashes } from "./utils";
@@ -246,7 +251,11 @@ export class LuaPrinter {
246251
} else if (luaLibImport === LuaLibImportKind.Inline && file.luaLibFeatures.size > 0) {
247252
// Inline lualib features
248253
sourceChunks.push("-- Lua Library inline imports\n");
249-
sourceChunks.push(loadInlineLualibFeatures(file.luaLibFeatures, luaTarget, this.emitHost));
254+
if (this.options.recompileLuaLib) {
255+
sourceChunks.push(recompileInlineLualibFeatures(file.luaLibFeatures, this.options, this.emitHost));
256+
} else {
257+
sourceChunks.push(loadInlineLualibFeatures(file.luaLibFeatures, luaTarget, this.emitHost));
258+
}
250259
sourceChunks.push("-- End of Lua Library inline imports\n");
251260
}
252261

src/transpilation/output-collector.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface TranspiledFile {
1212
js?: string;
1313
/** @internal */
1414
jsSourceMap?: string;
15+
/** @internal */
16+
text?: string;
1517
}
1618

1719
export function createEmitOutputCollector(luaExtension = ".lua") {
@@ -38,6 +40,7 @@ export function createEmitOutputCollector(luaExtension = ".lua") {
3840
} else if (fileName.endsWith(".d.ts.map")) {
3941
file.declarationMap = data;
4042
}
43+
file.text = data;
4144
};
4245

4346
return { writeFile, files };

src/transpilation/transpile.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,16 @@ export function getProgramTranspileResult(
145145
transpiledFiles = [];
146146
}
147147

148+
const proxyEmitHost =
149+
writeFileResult !== emitHost.writeFile
150+
? new Proxy(emitHost, {
151+
get: (target, prop) => (prop === "writeFile" ? writeFileResult : target[prop as keyof EmitHost]),
152+
})
153+
: emitHost;
154+
148155
for (const plugin of plugins) {
149156
if (plugin.afterPrint) {
150-
const pluginDiagnostics = plugin.afterPrint(program, options, emitHost, transpiledFiles) ?? [];
157+
const pluginDiagnostics = plugin.afterPrint(program, options, proxyEmitHost, transpiledFiles) ?? [];
151158
diagnostics.push(...pluginDiagnostics);
152159
}
153160
}

src/transpilation/transpiler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ export class Transpiler {
162162
this.emitHost,
163163
resolvedFiles.map(f => f.code)
164164
);
165-
return buildMinimalLualibBundle(usedFeatures, luaTarget, this.emitHost);
165+
return buildMinimalLualibBundle(usedFeatures, options, this.emitHost);
166166
} else {
167-
return getLuaLibBundle(luaTarget, this.emitHost);
167+
return getLuaLibBundle(luaTarget, this.emitHost, options);
168168
}
169169
}
170170
}

test/transpile/lualib.spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as ts from "typescript";
2-
import { LuaLibFeature, LuaTarget } from "../../src";
2+
import { LuaLibFeature, LuaLibImportKind, LuaTarget } from "../../src";
33
import { readLuaLibFeature } from "../../src/LuaLib";
44
import * as util from "../util";
5+
import path = require("path");
56

67
test.each(Object.entries(LuaLibFeature))("Lualib does not use ____exports (%p)", (_, feature) => {
78
const lualibCode = readLuaLibFeature(feature, LuaTarget.Lua54, ts.sys);
@@ -29,3 +30,39 @@ test("Lualib bundle does not assign globals", () => {
2930
.withLanguageExtensions()
3031
.expectNoExecutionError();
3132
});
33+
34+
test("Lualib bundle recompile", () => {
35+
const { transpiledFiles } = util.testExpression`Array.isArray({})`
36+
.setOptions({
37+
recompileLuaLib: true,
38+
luaPlugins: [{ name: path.join(__dirname, "./plugins/beforeEmit.ts") }],
39+
})
40+
.expectToHaveNoDiagnostics()
41+
.getLuaResult();
42+
const lualubBundle = transpiledFiles.find(f => path.basename(f.outPath) === "lualib_bundle.lua");
43+
expect(lualubBundle?.lua).toContain(
44+
"-- Comment added by beforeEmit plugin\n-- Comment added by beforeEmit plugin\n"
45+
);
46+
});
47+
48+
test("Lualib require minimal recompile", () => {
49+
util.testExpression`Array.isArray(123)`
50+
.setOptions({
51+
luaLibImport: LuaLibImportKind.RequireMinimal,
52+
recompileLuaLib: true,
53+
luaPlugins: [{ name: path.join(__dirname, "./plugins/visitor.ts") }],
54+
})
55+
.expectToHaveNoDiagnostics()
56+
.expectToEqual(true);
57+
});
58+
59+
test("Lualib inline recompile", () => {
60+
util.testExpression`Array.isArray(123)`
61+
.setOptions({
62+
luaLibImport: LuaLibImportKind.Inline,
63+
recompileLuaLib: true,
64+
luaPlugins: [{ name: path.join(__dirname, "./plugins/visitor.ts") }],
65+
})
66+
.expectToHaveNoDiagnostics()
67+
.expectToEqual(true);
68+
});

tsconfig-schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
"measurePerformance": {
103103
"description": "Measure and report performance of the tstl compiler.",
104104
"type": "boolean"
105+
},
106+
"recompileLuaLib": {
107+
"description": "Recompile the Lua standard library with custom plugins.",
108+
"type": "boolean"
105109
}
106110
},
107111
"dependencies": {

0 commit comments

Comments
 (0)