diff --git a/build-lualib.js b/build-lualib.js index 41ece7e32..f10437547 100644 --- a/build-lualib.js +++ b/build-lualib.js @@ -1,17 +1,16 @@ require("ts-node/register/transpile-only"); -const fs = require("fs"); const path = require("path"); -const ts = require("typescript"); const tstl = require("./src"); -const { loadLuaLibFeatures } = require("./src/LuaLib"); const configFileName = path.resolve(__dirname, "src/lualib/tsconfig.json"); const { diagnostics } = tstl.transpileProject(configFileName); diagnostics.forEach(tstl.createDiagnosticReporter(true)); -const bundlePath = path.join(__dirname, "dist/lualib/lualib_bundle.lua"); -if (fs.existsSync(bundlePath)) { - fs.unlinkSync(bundlePath); -} +const extraDiagnostics = require("./src/lualib-build/build-lualib").writeExtraLualibFiles( + path.resolve(__dirname, "dist/lualib") +); +extraDiagnostics.forEach(tstl.createDiagnosticReporter(true)); -fs.writeFileSync(bundlePath, loadLuaLibFeatures(Object.values(tstl.LuaLibFeature), ts.sys)); +if (diagnostics.length > 0 || extraDiagnostics.length > 0) { + process.exit(1); +} diff --git a/package.json b/package.json index 469f639c7..d1d0ba77c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dist/**/*.js", "dist/**/*.lua", "dist/**/*.ts", + "dist/lualib/*.json", "language-extensions/**/*.ts" ], "main": "dist/index.js", diff --git a/src/LuaLib.ts b/src/LuaLib.ts index e4321d181..3497d6b73 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -1,5 +1,6 @@ import * as path from "path"; import { EmitHost } from "./transpilation"; +import * as lua from "./LuaAST"; export enum LuaLibFeature { ArrayConcat = "ArrayConcat", @@ -99,81 +100,52 @@ export enum LuaLibFeature { Unpack = "Unpack", } -/* eslint-disable @typescript-eslint/naming-convention */ -const luaLibDependencies: Partial> = { - ArrayConcat: [LuaLibFeature.ArrayIsArray], - ArrayFlat: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray], - ArrayFlatMap: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray], - Await: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise], - Decorate: [LuaLibFeature.ObjectGetOwnPropertyDescriptor, LuaLibFeature.SetDescriptor, LuaLibFeature.ObjectAssign], - DelegatedYield: [LuaLibFeature.StringAccess], - Delete: [LuaLibFeature.ObjectGetOwnPropertyDescriptors, LuaLibFeature.Error, LuaLibFeature.New], - Error: [LuaLibFeature.Class, LuaLibFeature.ClassExtends, LuaLibFeature.New], - FunctionBind: [LuaLibFeature.Unpack], - Generator: [LuaLibFeature.Symbol], - InstanceOf: [LuaLibFeature.Symbol], - Iterator: [LuaLibFeature.Symbol], - NumberToString: [LuaLibFeature.StringAccess], - ObjectDefineProperty: [LuaLibFeature.CloneDescriptor, LuaLibFeature.SetDescriptor], - ObjectFromEntries: [LuaLibFeature.Iterator, LuaLibFeature.Symbol], - Promise: [ - LuaLibFeature.ArrayPush, - LuaLibFeature.Class, - LuaLibFeature.FunctionBind, - LuaLibFeature.InstanceOf, - LuaLibFeature.New, - ], - PromiseAll: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator], - PromiseAllSettled: [LuaLibFeature.InstanceOf, LuaLibFeature.New, LuaLibFeature.Promise, LuaLibFeature.Iterator], - PromiseAny: [ - LuaLibFeature.ArrayPush, - LuaLibFeature.InstanceOf, - LuaLibFeature.New, - LuaLibFeature.Promise, - LuaLibFeature.Iterator, - ], - PromiseRace: [ - LuaLibFeature.ArrayPush, - LuaLibFeature.InstanceOf, - LuaLibFeature.New, - LuaLibFeature.Promise, - LuaLibFeature.Iterator, - ], - ParseFloat: [LuaLibFeature.StringAccess], - ParseInt: [LuaLibFeature.StringSubstr, LuaLibFeature.StringSubstring], - SetDescriptor: [LuaLibFeature.CloneDescriptor], - Spread: [LuaLibFeature.Iterator, LuaLibFeature.StringAccess, LuaLibFeature.Unpack], - StringSplit: [LuaLibFeature.StringSubstring, LuaLibFeature.StringAccess], - SymbolRegistry: [LuaLibFeature.Symbol], - - Map: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], - Set: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], - WeakMap: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], - WeakSet: [LuaLibFeature.InstanceOf, LuaLibFeature.Iterator, LuaLibFeature.Symbol, LuaLibFeature.Class], -}; -/* eslint-enable @typescript-eslint/naming-convention */ - -export function loadLuaLibFeatures(features: Iterable, emitHost: EmitHost): string { +export interface LuaLibFeatureInfo { + dependencies?: LuaLibFeature[]; + exports: string[]; +} +export type LuaLibModulesInfo = Record; + +export const luaLibModulesInfoFileName = "lualib_module_info.json"; +let luaLibModulesInfo: LuaLibModulesInfo | undefined; +export function getLuaLibModulesInfo(emitHost: EmitHost): LuaLibModulesInfo { + if (luaLibModulesInfo === undefined) { + const lualibPath = path.resolve(__dirname, `../dist/lualib/${luaLibModulesInfoFileName}`); + const result = emitHost.readFile(lualibPath); + if (result !== undefined) { + luaLibModulesInfo = JSON.parse(result) as LuaLibModulesInfo; + } else { + throw new Error(`Could not load lualib dependencies from '${lualibPath}'`); + } + } + return luaLibModulesInfo; +} + +export function readLuaLibFeature(feature: LuaLibFeature, emitHost: EmitHost): string { + const featurePath = path.resolve(__dirname, `../dist/lualib/${feature}.lua`); + const luaLibFeature = emitHost.readFile(featurePath); + if (luaLibFeature === undefined) { + throw new Error(`Could not load lualib feature from '${featurePath}'`); + } + return luaLibFeature; +} + +export function loadInlineLualibFeatures(features: Iterable, emitHost: EmitHost): string { let result = ""; + const luaLibModulesInfo = getLuaLibModulesInfo(emitHost); const loadedFeatures = new Set(); function load(feature: LuaLibFeature): void { if (loadedFeatures.has(feature)) return; loadedFeatures.add(feature); - const dependencies = luaLibDependencies[feature]; + const dependencies = luaLibModulesInfo[feature]?.dependencies; if (dependencies) { dependencies.forEach(load); } - - const featurePath = path.resolve(__dirname, `../dist/lualib/${feature}.lua`); - const luaLibFeature = emitHost.readFile(featurePath); - if (luaLibFeature !== undefined) { - result += luaLibFeature + "\n"; - } else { - throw new Error(`Could not load lualib feature from '${featurePath}'`); - } + const luaLibFeature = readLuaLibFeature(feature, emitHost); + result += luaLibFeature + "\n"; } for (const feature of features) { @@ -183,6 +155,40 @@ export function loadLuaLibFeatures(features: Iterable, emitHost: return result; } +export function loadImportedLualibFeatures( + features: Iterable, + emitHost: EmitHost, + alwaysRequire = false +): lua.Statement[] { + const luaLibModuleInfo = getLuaLibModulesInfo(emitHost); + + const imports = Array.from(features).flatMap(feature => luaLibModuleInfo[feature].exports); + + const requireCall = lua.createCallExpression(lua.createIdentifier("require"), [ + lua.createStringLiteral("lualib_bundle"), + ]); + if (imports.length === 0) { + if (alwaysRequire) { + return [lua.createExpressionStatement(requireCall)]; + } + return []; + } + + const luaLibId = lua.createIdentifier("____lualib"); + const importStatement = lua.createVariableDeclarationStatement(luaLibId, requireCall); + const statements: lua.Statement[] = [importStatement]; + // local = ____luaLib. + for (const item of imports) { + statements.push( + lua.createVariableDeclarationStatement( + lua.createIdentifier(item), + lua.createTableIndexExpression(luaLibId, lua.createStringLiteral(item)) + ) + ); + } + return statements; +} + let luaLibBundleContent: string; export function getLuaLibBundle(emitHost: EmitHost): string { if (luaLibBundleContent === undefined) { diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index dc75b4a9b..b1d997a6b 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -1,12 +1,11 @@ import * as path from "path"; import { Mapping, SourceMapGenerator, SourceNode } from "source-map"; -import { getEmitPath } from "."; import * as ts from "typescript"; import { CompilerOptions, isBundleEnabled, LuaLibImportKind } from "./CompilerOptions"; import * as lua from "./LuaAST"; -import { loadLuaLibFeatures, LuaLibFeature } from "./LuaLib"; +import { loadInlineLualibFeatures, LuaLibFeature, loadImportedLualibFeatures } from "./LuaLib"; import { isValidLuaIdentifier, shouldAllowUnicode } from "./transformation/utils/safe-names"; -import { EmitHost } from "./transpilation"; +import { EmitHost, getEmitPath } from "./transpilation"; import { intersperse, normalizeSlashes } from "./utils"; // https://www.lua.org/pil/2.4.html @@ -233,18 +232,25 @@ export class LuaPrinter { if (!this.options.noHeader) { header += tstlHeader; } + let statements = file.statements; const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require; if ( luaLibImport === LuaLibImportKind.Always || (luaLibImport === LuaLibImportKind.Require && file.luaLibFeatures.size > 0) ) { - // Require lualib bundle - header += 'require("lualib_bundle");\n'; + // Import lualib features + const importStatements = loadImportedLualibFeatures( + file.luaLibFeatures, + this.emitHost, + luaLibImport === LuaLibImportKind.Always + ); + + statements = importStatements.concat(statements); } else if (luaLibImport === LuaLibImportKind.Inline && file.luaLibFeatures.size > 0) { // Inline lualib features header += "-- Lua Library inline imports\n"; - header += loadLuaLibFeatures(file.luaLibFeatures, this.emitHost); + header += loadInlineLualibFeatures(file.luaLibFeatures, this.emitHost); } if (this.options.sourceMapTraceback && !isBundleEnabled(this.options)) { @@ -253,7 +259,7 @@ export class LuaPrinter { header += `${LuaPrinter.sourceMapTracebackPlaceholder}\n`; } - return this.concatNodes(header, ...this.printStatementArray(file.statements)); + return this.concatNodes(header, ...this.printStatementArray(statements)); } protected pushIndent(): void { diff --git a/src/lualib-build/build-lualib.ts b/src/lualib-build/build-lualib.ts new file mode 100644 index 000000000..61f22093d --- /dev/null +++ b/src/lualib-build/build-lualib.ts @@ -0,0 +1,20 @@ +import pluginInstance from "./plugin"; +import { luaLibModulesInfoFileName, loadInlineLualibFeatures, LuaLibFeature } from "../LuaLib"; +import * as path from "path"; +import * as ts from "typescript"; + +// should be called after lualib modules are built +export function writeExtraLualibFiles(destPath: string): ts.Diagnostic[] { + const { result: luaLibModuleInfo, diagnostics } = pluginInstance.createLuaLibModulesInfo(); + const emitHost = ts.sys; + emitHost.writeFile(path.join(destPath, luaLibModulesInfoFileName), JSON.stringify(luaLibModuleInfo, null, 2)); + + const allFeatures = Object.values(LuaLibFeature) as LuaLibFeature[]; + + let lualibBundle = loadInlineLualibFeatures(allFeatures, emitHost); + const exports = allFeatures.flatMap(feature => luaLibModuleInfo[feature].exports); + lualibBundle += `\nreturn {\n${exports.map(exportName => ` ${exportName} = ${exportName}`).join(",\n")}\n}\n`; + emitHost.writeFile(path.join(destPath, "lualib_bundle.lua"), lualibBundle); + + return diagnostics; +} diff --git a/src/lualib-build/plugin.ts b/src/lualib-build/plugin.ts index c60a8e350..0715c1998 100644 --- a/src/lualib-build/plugin.ts +++ b/src/lualib-build/plugin.ts @@ -1,84 +1,141 @@ import { SourceNode } from "source-map"; import * as ts from "typescript"; import * as tstl from ".."; +import * as path from "path"; +import { createDiagnosticFactoryWithCode } from "../utils"; +import { getUsedLuaLibFeatures } from "../transformation/utils/lualib"; +import { LuaLibModulesInfo } from "../LuaLib"; + +const lualibDiagnostic = createDiagnosticFactoryWithCode(200000, (message: string, file?: ts.SourceFile) => ({ + messageText: message, + file, + start: file && 0, + length: file && 0, +})); + +class LuaLibPlugin implements tstl.Plugin { + public visitors = { + [ts.SyntaxKind.SourceFile]: this.lualibFileVisitor.bind(this), + }; + public printer: tstl.Printer = (program, emitHost, fileName, file) => + new LuaLibPrinter(emitHost, program, fileName).print(file); + + public featureExports: Map> = new Map(); + public featureDependencies: Map> = new Map(); + + public lualibFileVisitor(file: ts.SourceFile, context: tstl.TransformationContext): tstl.File { + const featureName = path.basename(file.fileName, ".ts") as tstl.LuaLibFeature; + if (!(featureName in tstl.LuaLibFeature)) { + context.diagnostics.push(lualibDiagnostic(`File is not a lualib feature: ${featureName}`, file)); + } + + // Transpile file as normal with tstl + const fileResult = context.superTransformNode(file)[0] as tstl.File; -function lualibFileVisitor(file: ts.SourceFile, context: tstl.TransformationContext): tstl.File { - // Get all imports in file - const imports = file.statements.filter(ts.isImportDeclaration); + const usedFeatures = new Set(getUsedLuaLibFeatures(context)); - const importNames = new Set(); - for (const { importClause } of imports) { - if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) { - for (const { name } of importClause.namedBindings.elements) { - importNames.add(name.text); + // Get all imports in file + const importNames = new Set(); + const imports = file.statements.filter(ts.isImportDeclaration); + for (const { importClause, moduleSpecifier } of imports) { + if (importClause?.namedBindings && ts.isNamedImports(importClause.namedBindings)) { + for (const { name } of importClause.namedBindings.elements) { + importNames.add(name.text); + } + } + // track lualib imports + if (ts.isStringLiteral(moduleSpecifier)) { + const featureName = path.basename(moduleSpecifier.text, ".ts") as tstl.LuaLibFeature; + if (featureName in tstl.LuaLibFeature) { + usedFeatures.add(featureName); + } } } - } - - // Transpile file as normal with tstl - const fileResult = context.superTransformNode(file)[0] as tstl.File; - // Find all exports assignments - const exportInitializers = new Map(); - for (const s of fileResult.statements) { - if (tstl.isAssignmentStatement(s) && isExportTableIndex(s.left[0])) { - exportInitializers.set(s.left[0].index.value, s.right[0]); + const filteredStatements = fileResult.statements + .map(statement => { + if ( + isExportTableDeclaration(statement) || + isRequire(statement) || + isImport(statement, importNames) || + isExportsReturn(statement) + ) { + return undefined; + } + if (isExportAlias(statement)) { + const name = statement.left[0]; + const exportName = statement.right[0].index.value; + if (name.text === exportName) return undefined; // Remove "x = x" statements + return tstl.createAssignmentStatement(name, tstl.createIdentifier(exportName)); + } + return statement; + }) + .filter(statement => statement !== undefined) as tstl.Statement[]; + + const exportNames = filteredStatements.filter(isExportAssignment).map(s => s.left[0].index.value); + if (!filteredStatements.every(isExportAssignment)) { + // If there are local statements, wrap them in a do ... end with exports outside + const exports = tstl.createVariableDeclarationStatement(exportNames.map(k => tstl.createIdentifier(k))); + // transform export assignments to local assignments + const bodyStatements = filteredStatements.map(s => + isExportAssignment(s) + ? tstl.createAssignmentStatement(tstl.createIdentifier(s.left[0].index.value), s.right[0]) + : s + ); + + fileResult.statements = [exports, tstl.createDoStatement(bodyStatements)]; + } else { + // transform export assignments to local variable declarations + fileResult.statements = filteredStatements.map(s => + tstl.createVariableDeclarationStatement(tstl.createIdentifier(s.left[0].index.value), s.right[0]) + ); } - } - // Replace export aliases with initializers - for (let i = 0; i < fileResult.statements.length; i++) { - const statement = fileResult.statements[i]; - if (isExportAlias(statement)) { - const name = statement.left[0]; - fileResult.statements[i] = tstl.createAssignmentStatement(name, exportInitializers.get(name.text)); + this.featureExports.set(featureName, new Set(exportNames)); + if (usedFeatures.size > 0) { + this.featureDependencies.set(featureName, usedFeatures); } + + return fileResult; } - // Filter out import/export statements - const shouldIgnoreImportsExports = (node: tstl.Node) => - !isExportTableDeclaration(node) && - !isRequire(node) && - !isImport(node, importNames) && - !isExportAssignment(node) && - !isExportsReturn(node); - const filteredStatements = fileResult.statements.filter(shouldIgnoreImportsExports); - - if (filteredStatements.length > 0) { - // If there are local statements, wrap them in a do ... end with exports outside - const exports = tstl.createVariableDeclarationStatement([...exportInitializers.keys()].map(k => tstl.createIdentifier(k))); - - fileResult.statements = [exports, tstl.createDoStatement(filteredStatements)]; - } else { - const newStatements = []; - for (const [exportName, initializer] of exportInitializers) { - newStatements.push(tstl.createVariableDeclarationStatement(tstl.createIdentifier(exportName), initializer)); + public createLuaLibModulesInfo(): { result: LuaLibModulesInfo; diagnostics: ts.Diagnostic[] } { + const result: Partial = {}; + const diagnostics: ts.Diagnostic[] = []; + for (const feature of Object.values(tstl.LuaLibFeature)) { + const exports = this.featureExports.get(feature); + if (!exports) { + diagnostics.push(lualibDiagnostic(`Missing file for lualib feature: ${feature}`)); + console.error(`Missing file for lualib feature: ${feature}`); + continue; + } + const dependencies = this.featureDependencies.get(feature); + result[feature] = { + exports: Array.from(exports), + dependencies: dependencies ? Array.from(dependencies) : undefined, + }; } - fileResult.statements = newStatements; + return { result: result as LuaLibModulesInfo, diagnostics }; } - - return fileResult; } class LuaLibPrinter extends tstl.LuaPrinter { // Strip all exports during print public printTableIndexExpression(expression: tstl.TableIndexExpression): SourceNode { - if (tstl.isIdentifier(expression.table) && expression.table.text === "____exports" && tstl.isStringLiteral(expression.index)) { + if ( + tstl.isIdentifier(expression.table) && + expression.table.text === "____exports" && + tstl.isStringLiteral(expression.index) + ) { return super.printExpression(tstl.createIdentifier(expression.index.value)); } return super.printTableIndexExpression(expression); } } -const plugin: tstl.Plugin = { - visitors: { - [ts.SyntaxKind.SourceFile]: lualibFileVisitor, - }, - printer: (program, emitHost, fileName, file) => new LuaLibPrinter(emitHost, program, fileName).print(file), -}; - +const pluginInstance = new LuaLibPlugin(); // eslint-disable-next-line import/no-default-export -export default plugin; +export default pluginInstance; function isExportTableDeclaration(node: tstl.Node): node is tstl.VariableDeclarationStatement & { left: [] } { return tstl.isVariableDeclarationStatement(node) && isExportTable(node.left[0]); @@ -88,15 +145,17 @@ function isExportTable(node: tstl.Node): node is tstl.Identifier { return tstl.isIdentifier(node) && node.text === "____exports"; } -function isExportTableIndex(node: tstl.Node): node is tstl.TableIndexExpression & { index: tstl.StringLiteral } { +type ExportTableIndex = tstl.TableIndexExpression & { index: tstl.StringLiteral }; +function isExportTableIndex(node: tstl.Node): node is ExportTableIndex { return tstl.isTableIndexExpression(node) && isExportTable(node.table) && tstl.isStringLiteral(node.index); } -function isExportAlias(node: tstl.Node): node is tstl.VariableDeclarationStatement { +function isExportAlias(node: tstl.Node): node is tstl.VariableDeclarationStatement & { right: [ExportTableIndex] } { return tstl.isVariableDeclarationStatement(node) && node.right !== undefined && isExportTableIndex(node.right[0]); } -function isExportAssignment(node: tstl.Node) { +type ExportAssignment = tstl.AssignmentStatement & { left: [ExportTableIndex] }; +function isExportAssignment(node: tstl.Node): node is ExportAssignment { return tstl.isAssignmentStatement(node) && isExportTableIndex(node.left[0]); } diff --git a/src/lualib/PromiseState.ts b/src/lualib/PromiseState.ts deleted file mode 100644 index 17c7a7559..000000000 --- a/src/lualib/PromiseState.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum __TS__PromiseState { - Pending, - Fulfilled, - Rejected, -} diff --git a/src/lualib/tsconfig.json b/src/lualib/tsconfig.json index 8bd54b70d..beca82ecd 100644 --- a/src/lualib/tsconfig.json +++ b/src/lualib/tsconfig.json @@ -12,8 +12,6 @@ "tstl": { "luaLibImport": "none", "noHeader": true, - "luaPlugins": [ - { "name": "../lualib-build/plugin.ts" } - ] + "luaPlugins": [{ "name": "../lualib-build/plugin.ts" }] } } diff --git a/src/transformation/builtins/index.ts b/src/transformation/builtins/index.ts index b56486425..59c0cf721 100644 --- a/src/transformation/builtins/index.ts +++ b/src/transformation/builtins/index.ts @@ -14,7 +14,6 @@ import { isStringType, } from "../utils/typescript"; import { PropertyCallExpression } from "../visitors/call"; -import { checkForLuaLibType } from "../visitors/class/new"; import { transformArrayConstructorCall, transformArrayProperty, transformArrayPrototypeCall } from "./array"; import { transformConsoleCall } from "./console"; import { transformFunctionPrototypeCall, transformFunctionProperty } from "./function"; @@ -33,6 +32,15 @@ export function transformBuiltinPropertyAccessExpression( ): lua.Expression | undefined { const ownerType = context.checker.getTypeAtLocation(node.expression); + if (ts.isIdentifier(node.expression) && isStandardLibraryType(context, ownerType, undefined)) { + switch (node.expression.text) { + case "Math": + return transformMathProperty(context, node); + case "Symbol": + importLuaLibFeature(context, LuaLibFeature.Symbol); + } + } + if (isStringType(context, ownerType)) { return transformStringProperty(context, node); } @@ -44,15 +52,6 @@ export function transformBuiltinPropertyAccessExpression( if (isFunctionType(ownerType)) { return transformFunctionProperty(context, node); } - - if (ts.isIdentifier(node.expression) && isStandardLibraryType(context, ownerType, undefined)) { - switch (node.expression.text) { - case "Math": - return transformMathProperty(context, node); - case "Symbol": - importLuaLibFeature(context, LuaLibFeature.Symbol); - } - } } export function transformBuiltinCallExpression( @@ -66,7 +65,6 @@ export function transformBuiltinCallExpression( }; const expressionType = context.checker.getTypeAtLocation(node.expression); if (ts.isIdentifier(node.expression) && isStandardLibraryType(context, expressionType, undefined)) { - // TODO: checkForLuaLibType(context, expressionType); const result = transformGlobalCall(context, node); if (result) { @@ -159,3 +157,46 @@ export function transformBuiltinIdentifierExpression( return lua.createIdentifier("_G", node, getIdentifierSymbolId(context, node), "globalThis"); } } + +const builtinErrorTypeNames = new Set([ + "Error", + "ErrorConstructor", + "RangeError", + "RangeErrorConstructor", + "ReferenceError", + "ReferenceErrorConstructor", + "SyntaxError", + "SyntaxErrorConstructor", + "TypeError", + "TypeErrorConstructor", + "URIError", + "URIErrorConstructor", +]); + +export function checkForLuaLibType(context: TransformationContext, type: ts.Type): void { + if (!type.symbol) return; + + const name = context.checker.getFullyQualifiedName(type.symbol); + switch (name) { + case "Map": + case "MapConstructor": + importLuaLibFeature(context, LuaLibFeature.Map); + return; + case "Set": + case "SetConstructor": + importLuaLibFeature(context, LuaLibFeature.Set); + return; + case "WeakMap": + case "WeakMapConstructor": + importLuaLibFeature(context, LuaLibFeature.WeakMap); + return; + case "WeakSet": + case "WeakSetConstructor": + importLuaLibFeature(context, LuaLibFeature.WeakSet); + return; + } + + if (builtinErrorTypeNames.has(name)) { + importLuaLibFeature(context, LuaLibFeature.Error); + } +} diff --git a/src/transformation/visitors/class/index.ts b/src/transformation/visitors/class/index.ts index f43b349e5..5257c2db6 100644 --- a/src/transformation/visitors/class/index.ts +++ b/src/transformation/visitors/class/index.ts @@ -23,9 +23,8 @@ import { transformStaticPropertyDeclaration, } from "./members/fields"; import { createMethodDecoratingExpression, transformMethodDeclaration } from "./members/method"; -import { checkForLuaLibType } from "./new"; -import { createClassSetup } from "./setup"; import { getExtendedNode, getExtendedType, isStaticNode } from "./utils"; +import { createClassSetup } from "./setup"; export const transformClassDeclaration: FunctionVisitor = (declaration, context) => { // If declaration is a default export, transform to export variable assignment instead @@ -86,10 +85,6 @@ function transformClassLikeDeclaration( const superInfo = getOrUpdate(classSuperInfos, context, () => []); superInfo.push({ className, extendedTypeNode }); - if (extendedType) { - checkForLuaLibType(context, extendedType); - } - // Get all properties with value const properties = classDeclaration.members.filter(ts.isPropertyDeclaration).filter(member => member.initializer); diff --git a/src/transformation/visitors/class/new.ts b/src/transformation/visitors/class/new.ts index f7433e7b5..13a4a57a9 100644 --- a/src/transformation/visitors/class/new.ts +++ b/src/transformation/visitors/class/new.ts @@ -1,52 +1,12 @@ import * as ts from "typescript"; import * as lua from "../../../LuaAST"; -import { FunctionVisitor, TransformationContext } from "../../context"; +import { FunctionVisitor } from "../../context"; import { AnnotationKind, getTypeAnnotations } from "../../utils/annotations"; import { annotationInvalidArgumentCount, annotationRemoved } from "../../utils/diagnostics"; -import { importLuaLibFeature, LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; import { transformArguments, transformCallAndArguments } from "../call"; import { isTableNewCall } from "../language-extensions/table"; -const builtinErrorTypeNames = new Set([ - "Error", - "ErrorConstructor", - "RangeError", - "RangeErrorConstructor", - "ReferenceError", - "ReferenceErrorConstructor", - "SyntaxError", - "SyntaxErrorConstructor", - "TypeError", - "TypeErrorConstructor", - "URIError", - "URIErrorConstructor", -]); - -// TODO: Do it in identifier? -export function checkForLuaLibType(context: TransformationContext, type: ts.Type): void { - if (!type.symbol) return; - - const name = context.checker.getFullyQualifiedName(type.symbol); - switch (name) { - case "Map": - importLuaLibFeature(context, LuaLibFeature.Map); - return; - case "Set": - importLuaLibFeature(context, LuaLibFeature.Set); - return; - case "WeakMap": - importLuaLibFeature(context, LuaLibFeature.WeakMap); - return; - case "WeakSet": - importLuaLibFeature(context, LuaLibFeature.WeakSet); - return; - } - - if (builtinErrorTypeNames.has(name)) { - importLuaLibFeature(context, LuaLibFeature.Error); - } -} - export const transformNewExpression: FunctionVisitor = (node, context) => { const type = context.checker.getTypeAtLocation(node); @@ -68,8 +28,6 @@ export const transformNewExpression: FunctionVisitor = (node, signature ); - checkForLuaLibType(context, type); - const customConstructorAnnotation = annotations.get(AnnotationKind.CustomConstructor); if (customConstructorAnnotation) { if (customConstructorAnnotation.args.length === 1) { diff --git a/src/transformation/visitors/identifier.ts b/src/transformation/visitors/identifier.ts index 46c3a8904..6a73bfeb6 100644 --- a/src/transformation/visitors/identifier.ts +++ b/src/transformation/visitors/identifier.ts @@ -1,7 +1,7 @@ import * as ts from "typescript"; import * as lua from "../../LuaAST"; -import { transformBuiltinIdentifierExpression } from "../builtins"; -import { createPromiseIdentifier, isPromiseClass } from "../builtins/promise"; +import { transformBuiltinIdentifierExpression, checkForLuaLibType } from "../builtins"; +import { isPromiseClass, createPromiseIdentifier } from "../builtins/promise"; import { FunctionVisitor, tempSymbolId, TransformationContext } from "../context"; import { AnnotationKind, isForRangeType } from "../utils/annotations"; import { @@ -22,6 +22,7 @@ import { isRangeFunctionNode } from "./language-extensions/range"; import { isTableExtensionIdentifier } from "./language-extensions/table"; import { isVarargConstantNode } from "./language-extensions/vararg"; import { isOptionalContinuation } from "./optional-chaining"; +import { isStandardLibraryType } from "../utils/typescript"; export function transformIdentifier(context: TransformationContext, identifier: ts.Identifier): lua.Identifier { if (isOptionalContinuation(identifier)) { @@ -59,6 +60,10 @@ export function transformIdentifier(context: TransformationContext, identifier: importLuaLibFeature(context, LuaLibFeature.Promise); return createPromiseIdentifier(identifier); } + const type = context.checker.getTypeAtLocation(identifier); + if (isStandardLibraryType(context, type, undefined)) { + checkForLuaLibType(context, type); + } const text = hasUnsafeIdentifierName(context, identifier) ? createSafeName(identifier.text) : identifier.text; diff --git a/src/transformation/visitors/modules/import.ts b/src/transformation/visitors/modules/import.ts index d8daba8c9..a9a53f4e4 100644 --- a/src/transformation/visitors/modules/import.ts +++ b/src/transformation/visitors/modules/import.ts @@ -72,12 +72,8 @@ export const transformImportDeclaration: FunctionVisitor = if (statement.importClause === undefined) { result.push(lua.createExpressionStatement(requireCall)); - if (scope.importStatements) { - scope.importStatements.push(...result); - return undefined; - } else { - return result; - } + scope.importStatements.push(...result); + return undefined; } const importPath = ts.isStringLiteral(statement.moduleSpecifier) @@ -144,12 +140,8 @@ export const transformImportDeclaration: FunctionVisitor = result.unshift(lua.createVariableDeclarationStatement(importUniqueName, requireCall, statement)); } - if (scope.importStatements) { - scope.importStatements.push(...result); - return undefined; - } else { - return result; - } + scope.importStatements.push(...result); + return undefined; }; export const transformExternalModuleReference: FunctionVisitor = (node, context) => diff --git a/src/transpilation/bundle.ts b/src/transpilation/bundle.ts index 45fe6d409..0c039ad4f 100644 --- a/src/transpilation/bundle.ts +++ b/src/transpilation/bundle.ts @@ -104,7 +104,7 @@ export function getBundleResult(program: ts.Program, files: ProcessedFile[]): [t const footers: string[] = []; if (options.sourceMapTraceback) { // Generates SourceMapTraceback for the entire file - footers.push('require("lualib_bundle")\n'); + footers.push('local __TS__SourceMapTraceBack = require("lualib_bundle").__TS__SourceMapTraceBack\n'); footers.push(`${sourceMapTracebackBundlePlaceholder}\n`); } diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 9671f656c..32f48994b 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -58,7 +58,8 @@ return ____exports" `; exports[`Transformation (methodRestArguments) 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class MyClass = __TS__Class() MyClass.name = \\"MyClass\\" function MyClass.prototype.____constructor(self) @@ -74,7 +75,8 @@ return ____exports" `; exports[`Transformation (modulesClassExport) 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class local ____exports = {} ____exports.TestClass = __TS__Class() local TestClass = ____exports.TestClass @@ -85,7 +87,8 @@ return ____exports" `; exports[`Transformation (modulesClassWithMemberExport) 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class local ____exports = {} ____exports.TestClass = __TS__Class() local TestClass = ____exports.TestClass diff --git a/test/transpile/lualib.spec.ts b/test/transpile/lualib.spec.ts index 0af391c46..b35ab996b 100644 --- a/test/transpile/lualib.spec.ts +++ b/test/transpile/lualib.spec.ts @@ -1,26 +1,31 @@ import * as ts from "typescript"; import { LuaLibFeature } from "../../src"; -import { loadLuaLibFeatures } from "../../src/LuaLib"; +import { readLuaLibFeature } from "../../src/LuaLib"; +import * as util from "../util"; -test.each(Object.entries(LuaLibFeature))("Lualib feature has correct dependencies (%p)", (_, feature) => { - const lualibCode = loadLuaLibFeatures([feature], ts.sys); +test.each(Object.entries(LuaLibFeature))("Lualib does not use ____exports (%p)", (_, feature) => { + const lualibCode = readLuaLibFeature(feature, ts.sys); - // Find all used lualib features - const luaLibReferences = lualibCode.match(/__TS__[a-zA-Z_]+\(/g); - - // For every reference lualib function, check if its definition is also included - const missingReferences = []; + const exportsOccurrences = lualibCode.match(/____exports/g); + expect(exportsOccurrences).toBeNull(); +}); - if (luaLibReferences !== null) { - for (const reference of luaLibReferences) { - if ( - !lualibCode.includes(`function ${reference}`) && - !lualibCode.includes(`${reference.substring(0, reference.length - 1)} =`) - ) { - missingReferences.push(reference); +test("Lualib bundle does not assign globals", () => { + // language=TypeScript + util.testModule` + declare const _G: LuaTable; + declare const require: (this: void, module: string) => any; + const globalKeys = new LuaTable(); + for (const [key] of _G) { + globalKeys[key] = true; + } + require("lualib_bundle"); + for (const [key] of _G) { + if (!globalKeys[key]) { + error("Global was assigned: " + key); } } - } - - expect(missingReferences).toHaveLength(0); + ` + .withLanguageExtensions() + .expectNoExecutionError(); }); diff --git a/test/transpile/module-resolution.spec.ts b/test/transpile/module-resolution.spec.ts index c363dbea1..477426c72 100644 --- a/test/transpile/module-resolution.spec.ts +++ b/test/transpile/module-resolution.spec.ts @@ -490,7 +490,8 @@ test("includes lualib_bundle when external lua requests it", () => { .addExtraFile( "lualibuser.lua", ` - require("lualib_bundle") + local ____lualib = require("lualib_bundle") + local __TS__ArrayPush = ____lualib.__TS__ArrayPush local result = {} __TS__ArrayPush(result, "foo") diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap index 91bc2306b..5f58cde14 100644 --- a/test/unit/__snapshots__/expressions.spec.ts.snap +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -503,7 +503,8 @@ return ____exports" `; exports[`Unary expressions basic ("delete tbl.test") 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Delete = ____lualib.__TS__Delete local ____exports = {} function ____exports.__main(self) __TS__Delete(tbl, \\"test\\") @@ -512,7 +513,8 @@ return ____exports" `; exports[`Unary expressions basic ("delete tbl['test']") 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Delete = ____lualib.__TS__Delete local ____exports = {} function ____exports.__main(self) __TS__Delete(tbl, \\"test\\") @@ -537,7 +539,8 @@ return ____exports" `; exports[`Unary expressions basic ("let a = delete tbl.test") 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Delete = ____lualib.__TS__Delete local ____exports = {} function ____exports.__main(self) local a = __TS__Delete(tbl, \\"test\\") @@ -546,7 +549,8 @@ return ____exports" `; exports[`Unary expressions basic ("let a = delete tbl['test']") 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Delete = ____lualib.__TS__Delete local ____exports = {} function ____exports.__main(self) local a = __TS__Delete(tbl, \\"test\\") diff --git a/test/unit/__snapshots__/optionalChaining.spec.ts.snap b/test/unit/__snapshots__/optionalChaining.spec.ts.snap index 46ac47aa8..3d67bf838 100644 --- a/test/unit/__snapshots__/optionalChaining.spec.ts.snap +++ b/test/unit/__snapshots__/optionalChaining.spec.ts.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Unsupported optional chains Builtin global method: code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Number = ____lualib.__TS__Number local ____Number_result_0 = Number if ____Number_result_0 ~= nil then ____Number_result_0 = nil diff --git a/test/unit/__snapshots__/switch.spec.ts.snap b/test/unit/__snapshots__/switch.spec.ts.snap index 45ba87e16..a640878c4 100644 --- a/test/unit/__snapshots__/switch.spec.ts.snap +++ b/test/unit/__snapshots__/switch.spec.ts.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`switch empty fallthrough to default (0) 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__ArrayPush = ____lualib.__TS__ArrayPush local ____exports = {} function ____exports.__main(self) local out = {} @@ -18,7 +19,8 @@ return ____exports" `; exports[`switch empty fallthrough to default (1) 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__ArrayPush = ____lualib.__TS__ArrayPush local ____exports = {} function ____exports.__main(self) local out = {} @@ -111,7 +113,8 @@ return ____exports" `; exports[`switch produces optimal output 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__ArrayPush = ____lualib.__TS__ArrayPush local ____exports = {} function ____exports.__main(self) local x = 0 diff --git a/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap b/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap index c283485bf..ffd43b2b6 100644 --- a/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap +++ b/test/unit/annotations/__snapshots__/customConstructor.spec.ts.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`IncorrectUsage: code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class +local __TS__New = ____lualib.__TS__New local ____exports = {} function ____exports.__main(self) local Point2D = __TS__Class() diff --git a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap index a962a4a30..32c01be9e 100644 --- a/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap +++ b/test/unit/annotations/__snapshots__/deprecated.spec.ts.snap @@ -11,7 +11,8 @@ return ____exports" exports[`LuaTable deprecation warning property access set: diagnostics 1`] = `"main.ts(12,16): error TSTL: '@luaTable' has been removed and will no longer have any effect.See https://typescripttolua.github.io/docs/advanced/compiler-annotations#luatable for more information."`; exports[`LuaTable removed warning constructor: code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__New = ____lualib.__TS__New ____table = __TS__New(Table)" `; @@ -38,14 +39,18 @@ return ____exports" exports[`LuaTable removed warning property access length: diagnostics 1`] = `"main.ts(12,16): error TSTL: '@luaTable' has been removed and will no longer have any effect.See https://typescripttolua.github.io/docs/advanced/compiler-annotations#luatable for more information."`; exports[`extension removed: code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class +local __TS__ClassExtends = ____lualib.__TS__ClassExtends B = __TS__Class() B.name = \\"B\\" __TS__ClassExtends(B, A)" `; exports[`extension removed: code 2`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class +local __TS__ClassExtends = ____lualib.__TS__ClassExtends B = __TS__Class() B.name = \\"B\\" __TS__ClassExtends(B, A)" @@ -90,7 +95,9 @@ end" exports[`phantom removed: diagnostics 1`] = `"main.ts(3,9): error TSTL: '@phantom' has been removed and will no longer have any effect.See https://typescripttolua.github.io/docs/advanced/compiler-annotations#phantom for more information."`; exports[`pureAbstract removed: code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class +local __TS__ClassExtends = ____lualib.__TS__ClassExtends ClassB = __TS__Class() ClassB.name = \\"ClassB\\" __TS__ClassExtends(ClassB, ClassA)" diff --git a/test/unit/builtins/set.spec.ts b/test/unit/builtins/set.spec.ts index 8f81edfe8..93e23e98e 100644 --- a/test/unit/builtins/set.spec.ts +++ b/test/unit/builtins/set.spec.ts @@ -194,3 +194,10 @@ describe.each(iterationMethods)("set.%s() preserves insertion order", iterationM `.expectToMatchJsResult(); }); }); + +test("instanceof Set without creating set", () => { + util.testFunction` + const myset = 3 as any; + return myset instanceof Set; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/classes/__snapshots__/classes.spec.ts.snap b/test/unit/classes/__snapshots__/classes.spec.ts.snap index 6336a53a3..7822867f5 100644 --- a/test/unit/classes/__snapshots__/classes.spec.ts.snap +++ b/test/unit/classes/__snapshots__/classes.spec.ts.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`missing declaration name: code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class ____class_0 = __TS__Class() ____class_0.name = \\"\\" function ____class_0.prototype.____constructor(self) diff --git a/test/unit/classes/__snapshots__/decorators.spec.ts.snap b/test/unit/classes/__snapshots__/decorators.spec.ts.snap index cbc983bd5..cef76f35e 100644 --- a/test/unit/classes/__snapshots__/decorators.spec.ts.snap +++ b/test/unit/classes/__snapshots__/decorators.spec.ts.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Throws error if decorator function has void context: code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Class = ____lualib.__TS__Class +local __TS__Decorate = ____lualib.__TS__Decorate local ____exports = {} function ____exports.__main(self) local function decorator(constructor) diff --git a/test/unit/language-extensions/__snapshots__/range.spec.ts.snap b/test/unit/language-extensions/__snapshots__/range.spec.ts.snap index c1f369cb6..64273b8fe 100644 --- a/test/unit/language-extensions/__snapshots__/range.spec.ts.snap +++ b/test/unit/language-extensions/__snapshots__/range.spec.ts.snap @@ -33,7 +33,8 @@ return ____exports" exports[`$range invalid use ("const x = $range(1, 10);"): diagnostics 1`] = `"main.ts(2,19): error TSTL: $range can only be used in a for...of loop."`; exports[`$range invalid use ("const y = [...$range(1, 10)];"): code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Spread = ____lualib.__TS__Spread local ____exports = {} function ____exports.__main(self) local y = {__TS__Spread(____(nil, 1, 10))} @@ -66,7 +67,8 @@ return ____exports" exports[`$range invalid use ("for (const i of $range(1, 10, 2) as number[]) {}"): diagnostics 1`] = `"main.ts(2,25): error TSTL: $range can only be used in a for...of loop."`; exports[`$range invalid use ("for (const i of ($range(1, 10, 2))) {}"): code 1`] = ` -"require(\\"lualib_bundle\\"); +"local ____lualib = require(\\"lualib_bundle\\") +local __TS__Iterator = ____lualib.__TS__Iterator local ____exports = {} function ____exports.__main(self) for ____, i in __TS__Iterator(____(nil, 1, 10, 2)) do diff --git a/test/unit/printer/sourcemaps.spec.ts b/test/unit/printer/sourcemaps.spec.ts index fdc21a13a..b625f1f5f 100644 --- a/test/unit/printer/sourcemaps.spec.ts +++ b/test/unit/printer/sourcemaps.spec.ts @@ -88,7 +88,7 @@ test.each([ assertPatterns: [ { luaPattern: "Bar = __TS__Class()", typeScriptPattern: "class Bar" }, { luaPattern: "Bar.name =", typeScriptPattern: "class Bar" }, - { luaPattern: "__TS__ClassExtends", typeScriptPattern: "extends" }, + { luaPattern: "__TS__ClassExtends(", typeScriptPattern: "extends" }, // find use of function, not import { luaPattern: "Foo", typeScriptPattern: "Foo" }, { luaPattern: "function Bar.prototype.____constructor", typeScriptPattern: "constructor" }, ],