Skip to content

Commit d315218

Browse files
authored
Minimal bundle luaLibImport option (#1383)
* Fix typo * Add "require-minimal" lualib_bundle option that only includes used lualib features * Update snapshot * Increase specificity of search lualib import search pattern * PR feedback * Cache lualib info by luaTarget instead of file paths
1 parent 5a9fdfd commit d315218

File tree

8 files changed

+164
-38
lines changed

8 files changed

+164
-38
lines changed

src/CompilerOptions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ export interface TransformerImport {
1212
after?: boolean;
1313
afterDeclarations?: boolean;
1414
type?: "program" | "config" | "checker" | "raw" | "compilerOptions";
15+
1516
[option: string]: any;
1617
}
1718

1819
export interface LuaPluginImport {
1920
name: string;
2021
import?: string;
22+
2123
[option: string]: any;
2224
}
2325

@@ -49,6 +51,7 @@ export enum LuaLibImportKind {
4951
None = "none",
5052
Inline = "inline",
5153
Require = "require",
54+
RequireMinimal = "require-minimal",
5255
}
5356

5457
export enum LuaTarget {

src/LuaLib.ts

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from "path";
22
import { EmitHost } from "./transpilation";
33
import * as lua from "./LuaAST";
44
import { LuaTarget } from "./CompilerOptions";
5+
import { getOrUpdate } from "./utils";
56

67
export enum LuaLibFeature {
78
ArrayConcat = "ArrayConcat",
@@ -109,6 +110,7 @@ export interface LuaLibFeatureInfo {
109110
dependencies?: LuaLibFeature[];
110111
exports: string[];
111112
}
113+
112114
export type LuaLibModulesInfo = Record<LuaLibFeature, LuaLibFeatureInfo>;
113115

114116
export function resolveLuaLibDir(luaTarget: LuaTarget) {
@@ -117,27 +119,55 @@ export function resolveLuaLibDir(luaTarget: LuaTarget) {
117119
}
118120

119121
export const luaLibModulesInfoFileName = "lualib_module_info.json";
120-
const luaLibModulesInfo = new Map<string, LuaLibModulesInfo>();
122+
const luaLibModulesInfo = new Map<LuaTarget, LuaLibModulesInfo>();
123+
121124
export function getLuaLibModulesInfo(luaTarget: LuaTarget, emitHost: EmitHost): LuaLibModulesInfo {
122-
const lualibPath = path.join(resolveLuaLibDir(luaTarget), luaLibModulesInfoFileName);
123-
if (!luaLibModulesInfo.has(lualibPath)) {
125+
if (!luaLibModulesInfo.has(luaTarget)) {
126+
const lualibPath = path.join(resolveLuaLibDir(luaTarget), luaLibModulesInfoFileName);
124127
const result = emitHost.readFile(lualibPath);
125128
if (result !== undefined) {
126-
luaLibModulesInfo.set(lualibPath, JSON.parse(result) as LuaLibModulesInfo);
129+
luaLibModulesInfo.set(luaTarget, JSON.parse(result) as LuaLibModulesInfo);
127130
} else {
128131
throw new Error(`Could not load lualib dependencies from '${lualibPath}'`);
129132
}
130133
}
131-
return luaLibModulesInfo.get(lualibPath) as LuaLibModulesInfo;
134+
return luaLibModulesInfo.get(luaTarget)!;
135+
}
136+
137+
// This caches the names of lualib exports to their LuaLibFeature, avoiding a linear search for every lookup
138+
const lualibExportToFeature = new Map<LuaTarget, ReadonlyMap<string, LuaLibFeature>>();
139+
140+
export function getLuaLibExportToFeatureMap(
141+
luaTarget: LuaTarget,
142+
emitHost: EmitHost
143+
): ReadonlyMap<string, LuaLibFeature> {
144+
if (!lualibExportToFeature.has(luaTarget)) {
145+
const luaLibModulesInfo = getLuaLibModulesInfo(luaTarget, emitHost);
146+
const map = new Map<string, LuaLibFeature>();
147+
for (const [feature, info] of Object.entries(luaLibModulesInfo)) {
148+
for (const exportName of info.exports) {
149+
map.set(exportName, feature as LuaLibFeature);
150+
}
151+
}
152+
lualibExportToFeature.set(luaTarget, map);
153+
}
154+
155+
return lualibExportToFeature.get(luaTarget)!;
132156
}
133157

158+
const lualibFeatureCache = new Map<LuaTarget, Map<LuaLibFeature, string>>();
159+
134160
export function readLuaLibFeature(feature: LuaLibFeature, luaTarget: LuaTarget, emitHost: EmitHost): string {
135-
const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`);
136-
const luaLibFeature = emitHost.readFile(featurePath);
137-
if (luaLibFeature === undefined) {
138-
throw new Error(`Could not load lualib feature from '${featurePath}'`);
161+
const featureMap = getOrUpdate(lualibFeatureCache, luaTarget, () => new Map());
162+
if (!featureMap.has(feature)) {
163+
const featurePath = path.join(resolveLuaLibDir(luaTarget), `${feature}.lua`);
164+
const luaLibFeature = emitHost.readFile(featurePath);
165+
if (luaLibFeature === undefined) {
166+
throw new Error(`Could not load lualib feature from '${featurePath}'`);
167+
}
168+
featureMap.set(feature, luaLibFeature);
139169
}
140-
return luaLibFeature;
170+
return featureMap.get(feature)!;
141171
}
142172

143173
export function resolveRecursiveLualibFeatures(
@@ -173,14 +203,9 @@ export function loadInlineLualibFeatures(
173203
luaTarget: LuaTarget,
174204
emitHost: EmitHost
175205
): string {
176-
let result = "";
177-
178-
for (const feature of resolveRecursiveLualibFeatures(features, luaTarget, emitHost)) {
179-
const luaLibFeature = readLuaLibFeature(feature, luaTarget, emitHost);
180-
result += luaLibFeature + "\n";
181-
}
182-
183-
return result;
206+
return resolveRecursiveLualibFeatures(features, luaTarget, emitHost)
207+
.map(feature => readLuaLibFeature(feature, luaTarget, emitHost))
208+
.join("\n");
184209
}
185210

186211
export function loadImportedLualibFeatures(
@@ -191,13 +216,13 @@ export function loadImportedLualibFeatures(
191216
const luaLibModuleInfo = getLuaLibModulesInfo(luaTarget, emitHost);
192217

193218
const imports = Array.from(features).flatMap(feature => luaLibModuleInfo[feature].exports);
219+
if (imports.length === 0) {
220+
return [];
221+
}
194222

195223
const requireCall = lua.createCallExpression(lua.createIdentifier("require"), [
196224
lua.createStringLiteral("lualib_bundle"),
197225
]);
198-
if (imports.length === 0) {
199-
return [];
200-
}
201226

202227
const luaLibId = lua.createIdentifier("____lualib");
203228
const importStatement = lua.createVariableDeclarationStatement(luaLibId, requireCall);
@@ -215,6 +240,7 @@ export function loadImportedLualibFeatures(
215240
}
216241

217242
const luaLibBundleContent = new Map<string, string>();
243+
218244
export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost): string {
219245
const lualibPath = path.join(resolveLuaLibDir(luaTarget), "lualib_bundle.lua");
220246
if (!luaLibBundleContent.has(lualibPath)) {
@@ -228,3 +254,42 @@ export function getLuaLibBundle(luaTarget: LuaTarget, emitHost: EmitHost): strin
228254

229255
return luaLibBundleContent.get(lualibPath) as string;
230256
}
257+
258+
export function getLualibBundleReturn(exportedValues: string[]): string {
259+
return `\nreturn {\n${exportedValues.map(exportName => ` ${exportName} = ${exportName}`).join(",\n")}\n}\n`;
260+
}
261+
262+
export function buildMinimalLualibBundle(
263+
features: Iterable<LuaLibFeature>,
264+
luaTarget: LuaTarget,
265+
emitHost: EmitHost
266+
): string {
267+
const code = loadInlineLualibFeatures(features, luaTarget, emitHost);
268+
const moduleInfo = getLuaLibModulesInfo(luaTarget, emitHost);
269+
const exports = Array.from(features).flatMap(feature => moduleInfo[feature].exports);
270+
271+
return code + getLualibBundleReturn(exports);
272+
}
273+
274+
export function findUsedLualibFeatures(
275+
luaTarget: LuaTarget,
276+
emitHost: EmitHost,
277+
luaContents: string[]
278+
): Set<LuaLibFeature> {
279+
const features = new Set<LuaLibFeature>();
280+
const exportToFeatureMap = getLuaLibExportToFeatureMap(luaTarget, emitHost);
281+
282+
for (const lua of luaContents) {
283+
const regex = /^local (\w+) = ____lualib\.(\w+)$/gm;
284+
while (true) {
285+
const match = regex.exec(lua);
286+
if (!match) break;
287+
const [, localName, exportName] = match;
288+
if (localName !== exportName) continue;
289+
const feature = exportToFeatureMap.get(exportName);
290+
if (feature) features.add(feature);
291+
}
292+
}
293+
294+
return features;
295+
}

src/LuaPrinter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ export class LuaPrinter {
235235

236236
const luaTarget = this.options.luaTarget ?? LuaTarget.Universal;
237237
const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require;
238-
if (luaLibImport === LuaLibImportKind.Require && file.luaLibFeatures.size > 0) {
238+
if (
239+
(luaLibImport === LuaLibImportKind.Require || luaLibImport === LuaLibImportKind.RequireMinimal) &&
240+
file.luaLibFeatures.size > 0
241+
) {
239242
// Import lualib features
240243
sourceChunks = this.printStatementArray(
241244
loadImportedLualibFeatures(file.luaLibFeatures, luaTarget, this.emitHost)

src/lualib-build/plugin.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { SourceNode } from "source-map";
22
import * as ts from "typescript";
33
import * as tstl from "..";
44
import * as path from "path";
5-
import { LuaLibFeature, LuaLibModulesInfo, luaLibModulesInfoFileName, resolveRecursiveLualibFeatures } from "../LuaLib";
5+
import {
6+
getLualibBundleReturn,
7+
LuaLibFeature,
8+
LuaLibModulesInfo,
9+
luaLibModulesInfoFileName,
10+
resolveRecursiveLualibFeatures,
11+
} from "../LuaLib";
612
import { EmitHost, ProcessedFile } from "../transpilation/utils";
713
import {
814
isExportAlias,
@@ -63,7 +69,7 @@ class LuaLibPlugin implements tstl.Plugin {
6369
// Concatenate lualib files into bundle with exports table and add lualib_bundle.lua to results
6470
let lualibBundle = orderedFeatures.map(f => exportedLualibFeatures.get(LuaLibFeature[f])).join("\n");
6571
const exports = allFeatures.flatMap(feature => luaLibModuleInfo[feature].exports);
66-
lualibBundle += `\nreturn {\n${exports.map(exportName => ` ${exportName} = ${exportName}`).join(",\n")}\n}\n`;
72+
lualibBundle += getLualibBundleReturn(exports);
6773
result.push({ fileName: "lualib_bundle.lua", code: lualibBundle });
6874

6975
return diagnostics;

src/transpilation/diagnostics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const usingLuaBundleWithInlineMightGenerateDuplicateCode = createSerialDi
5151

5252
export const cannotBundleLibrary = createDiagnosticFactory(
5353
() =>
54-
'Cannot bundle probjects with"buildmode": "library". Projects including the library can still bundle (which will include external library files).'
54+
'Cannot bundle projects with "buildmode": "library". Projects including the library can still bundle (which will include external library files).'
5555
);
5656

5757
export const unsupportedJsxEmit = createDiagnosticFactory(() => 'JSX is only supported with "react" jsx option.');

src/transpilation/transpiler.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as path from "path";
22
import * as ts from "typescript";
3-
import { CompilerOptions, isBundleEnabled, LuaTarget } from "../CompilerOptions";
4-
import { getLuaLibBundle } from "../LuaLib";
3+
import { CompilerOptions, isBundleEnabled, LuaLibImportKind, LuaTarget } from "../CompilerOptions";
4+
import { buildMinimalLualibBundle, findUsedLualibFeatures, getLuaLibBundle } from "../LuaLib";
55
import { normalizeSlashes, trimExtension } from "../utils";
66
import { getBundleResult } from "./bundle";
77
import { getPlugins, Plugin } from "./plugins";
@@ -123,11 +123,10 @@ export class Transpiler {
123123
if (options.tstlVerbose) {
124124
console.log("Including lualib bundle");
125125
}
126-
127126
// Add lualib bundle to source dir 'virtually', will be moved to correct output dir in emitPlan
128127
const fileName = normalizeSlashes(path.resolve(getSourceDir(program), "lualib_bundle.lua"));
129-
const luaTarget = options.luaTarget ?? LuaTarget.Universal;
130-
resolutionResult.resolvedFiles.unshift({ fileName, code: getLuaLibBundle(luaTarget, this.emitHost) });
128+
const code = this.getLuaLibBundleContent(options, resolutionResult.resolvedFiles);
129+
resolutionResult.resolvedFiles.unshift({ fileName, code });
131130
}
132131

133132
let emitPlan: EmitFile[];
@@ -146,6 +145,20 @@ export class Transpiler {
146145

147146
return { emitPlan };
148147
}
148+
149+
private getLuaLibBundleContent(options: CompilerOptions, resolvedFiles: ProcessedFile[]) {
150+
const luaTarget = options.luaTarget ?? LuaTarget.Universal;
151+
if (options.luaLibImport === LuaLibImportKind.RequireMinimal) {
152+
const usedFeatures = findUsedLualibFeatures(
153+
luaTarget,
154+
this.emitHost,
155+
resolvedFiles.map(f => f.code)
156+
);
157+
return buildMinimalLualibBundle(usedFeatures, luaTarget, this.emitHost);
158+
} else {
159+
return getLuaLibBundle(luaTarget, this.emitHost);
160+
}
161+
}
149162
}
150163

151164
export function getEmitPath(file: string, program: ts.Program): string {

test/unit/__snapshots__/bundle.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
exports[`LuaLibImportKind.Inline generates a warning: diagnostics 1`] = `"warning TSTL: Using 'luaBundle' with 'luaLibImport: \\"inline\\"' might generate duplicate code. It is recommended to use 'luaLibImport: \\"require\\"'."`;
44

5-
exports[`bundling not allowed for buildmode library: diagnostics 1`] = `"error TSTL: Cannot bundle probjects with\\"buildmode\\": \\"library\\". Projects including the library can still bundle (which will include external library files)."`;
5+
exports[`bundling not allowed for buildmode library: diagnostics 1`] = `"error TSTL: Cannot bundle projects with \\"buildmode\\": \\"library\\". Projects including the library can still bundle (which will include external library files)."`;
66

77
exports[`luaEntry doesn't exist: diagnostics 1`] = `"error TSTL: Could not find bundle entry point 'entry.ts'. It should be a file in the project."`;
88

test/unit/builtins/loading.spec.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,53 @@ describe("luaLibImport", () => {
1616
.tap(builder => expect(builder.getMainLuaCodeChunk()).toContain('require("lualib_bundle")'))
1717
.expectToMatchJsResult();
1818
});
19-
});
2019

21-
test.each([tstl.LuaLibImportKind.Inline, tstl.LuaLibImportKind.None, tstl.LuaLibImportKind.Require])(
22-
"should not include lualib without code (%p)",
23-
luaLibImport => {
24-
util.testModule``.setOptions({ luaLibImport }).tap(builder => expect(builder.getMainLuaCodeChunk()).toBe(""));
20+
function testLualibOnlyHasArrayIndexOf(builder: util.TestBuilder) {
21+
const lualibBundle = builder.getLuaResult().transpiledFiles.find(f => f.outPath.endsWith("lualib_bundle.lua"))!;
22+
expect(lualibBundle).toBeDefined();
23+
expect(lualibBundle.lua).toEqual(expect.stringMatching("^local function __TS__ArrayIndexOf"));
24+
expect(lualibBundle.lua).not.toContain("__TS__ArrayConcat");
25+
expect(lualibBundle.lua).not.toContain("Error");
2526
}
26-
);
27+
28+
test("require-minimal", () => {
29+
util.testExpression`[0].indexOf(1)`
30+
.setOptions({ luaLibImport: tstl.LuaLibImportKind.RequireMinimal })
31+
.tap(builder => expect(builder.getMainLuaCodeChunk()).toContain('require("lualib_bundle")'))
32+
.tap(testLualibOnlyHasArrayIndexOf)
33+
.expectToMatchJsResult();
34+
});
35+
36+
test("require-minimal with lualib in another file", () => {
37+
util.testModule`
38+
import "./other";
39+
`
40+
.setOptions({ luaLibImport: tstl.LuaLibImportKind.RequireMinimal })
41+
.addExtraFile(
42+
"other.lua",
43+
`
44+
local ____lualib = require("lualib_bundle")
45+
local __TS__ArrayIndexOf = ____lualib.__TS__ArrayIndexOf
46+
__TS__ArrayIndexOf({}, 1)
47+
`
48+
)
49+
// note: indent matters in above code, because searching for lualib checks for start & end of line
50+
.tap(testLualibOnlyHasArrayIndexOf)
51+
.expectNoExecutionError();
52+
});
53+
});
54+
55+
test.each([
56+
tstl.LuaLibImportKind.Inline,
57+
tstl.LuaLibImportKind.None,
58+
tstl.LuaLibImportKind.Require,
59+
tstl.LuaLibImportKind.RequireMinimal,
60+
])("should not include lualib without code (%p)", luaLibImport => {
61+
util.testModule``.setOptions({ luaLibImport }).tap(builder => expect(builder.getMainLuaCodeChunk()).toBe(""));
62+
});
2763

2864
test("lualib should not include tstl header", () => {
29-
util.testExpression`[0].push(1)`.tap(builder =>
65+
util.testExpression`[0].indexOf(1)`.tap(builder =>
3066
expect(builder.getMainLuaCodeChunk()).not.toContain("Generated with")
3167
);
3268
});

0 commit comments

Comments
 (0)