diff --git a/.eslintignore b/.eslintignore index 707cb8140..492195f96 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ /test/cli/errors /test/cli/watch /test/transpile/directories +/test/transpile/module-resolution/*/node_modules /test/transpile/outFile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13df095ec..968cd9b87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,14 +76,14 @@ jobs: run: rm -rf ./master/benchmark && cp -rf ./commit/benchmark ./master/benchmark # Run master benchmark first and output to commit benchmark data - name: Build benchmark Lua 5.3 master - run: node ../dist/tstl.js -p tsconfig.53.json + run: node ../../commit/dist/tstl.js -p tsconfig.53.json working-directory: master/benchmark - name: Run benchmark Lua 5.3 master id: benchmark-lua-master run: lua5.3 -- run.lua ../../../commit/benchmark/data/benchmark_master_53.json working-directory: master/benchmark/dist - name: Build benchmark LuaJIT master - run: node ../dist/tstl.js -p tsconfig.jit.json + run: node ../../commit/dist/tstl.js -p tsconfig.jit.json working-directory: master/benchmark - name: Run benchmark LuaJIT master id: benchmark-jit-master @@ -91,14 +91,14 @@ jobs: working-directory: master/benchmark/dist # Run commit benchmark and compare with master - name: Build benchmark Lua 5.3 commit - run: node ../dist/tstl.js -p tsconfig.53.json + run: node ../../commit/dist/tstl.js -p tsconfig.53.json working-directory: commit/benchmark - name: Run benchmark Lua 5.3 commit id: benchmark-lua-commit run: lua5.3 -- run.lua ../data/benchmark_master_vs_commit_53.json ../data/benchmark_master_53.json working-directory: commit/benchmark/dist - name: Build benchmark LuaJIT commit - run: node ../dist/tstl.js -p tsconfig.jit.json + run: node ../../commit/dist/tstl.js -p tsconfig.jit.json working-directory: commit/benchmark - name: Run benchmark LuaJIT commit id: benchmark-jit-commit diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..a50a68074 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug current jest test", + "type": "node", + "request": "launch", + "env": { "CI": "true" }, + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest", + "args": ["--runInBand", "--no-cache", "--runTestsByPath", "${relativeFile}"], + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} diff --git a/benchmark/dist/json.lua b/benchmark/src/json.lua similarity index 100% rename from benchmark/dist/json.lua rename to benchmark/src/json.lua diff --git a/package-lock.json b/package-lock.json index 575c0ee9f..6a8e2855a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.39.4", "license": "MIT", "dependencies": { + "enhanced-resolve": "^5.8.2", "resolve": "^1.15.1", "source-map": "^0.7.3", "typescript": "~4.3.2" @@ -3461,6 +3462,18 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz", + "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -4684,8 +4697,7 @@ "node_modules/graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "node_modules/growly": { "version": "1.3.0", @@ -10585,11 +10597,6 @@ "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, "engines": { "node": ">=0.10.0" } @@ -10823,6 +10830,14 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/tapable": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", + "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==", + "engines": { + "node": ">=6" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -14393,6 +14408,15 @@ "once": "^1.4.0" } }, + "enhanced-resolve": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz", + "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -15355,8 +15379,7 @@ "graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "growly": { "version": "1.3.0", @@ -20245,6 +20268,11 @@ } } }, + "tapable": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", + "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==" + }, "terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", diff --git a/package.json b/package.json index 3bbbfefe7..5cbd51f22 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "node": ">=12.13.0" }, "dependencies": { + "enhanced-resolve": "^5.8.2", "resolve": "^1.15.1", "source-map": "^0.7.3", "typescript": "~4.3.2" diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index 439b29652..e06adc1f3 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -25,6 +25,7 @@ export interface LuaPluginImport { } export type CompilerOptions = OmitIndexSignature & { + buildMode?: BuildMode; noImplicitSelf?: boolean; noHeader?: boolean; luaBundle?: string; @@ -53,6 +54,11 @@ export enum LuaTarget { LuaJIT = "JIT", } +export enum BuildMode { + Default = "default", + Library = "library", +} + export const isBundleEnabled = (options: CompilerOptions) => options.luaBundle !== undefined && options.luaBundleEntry !== undefined; diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index e2a6eafe6..390b62977 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -1,4 +1,3 @@ -import * as path from "path"; import { Mapping, SourceMapGenerator, SourceNode } from "source-map"; import * as ts from "typescript"; import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions"; @@ -6,7 +5,7 @@ import * as lua from "./LuaAST"; import { loadLuaLibFeatures, LuaLibFeature } from "./LuaLib"; import { isValidLuaIdentifier } from "./transformation/utils/safe-names"; import { EmitHost } from "./transpilation"; -import { intersperse, normalizeSlashes, trimExtension } from "./utils"; +import { intersperse, trimExtension } from "./utils"; // https://www.lua.org/pil/2.4.html // https://www.ecma-international.org/ecma-262/10.0/index.html#table-34 @@ -25,6 +24,8 @@ const escapeStringMap: Record = { export const escapeString = (value: string) => `"${value.replace(escapeStringRegExp, char => escapeStringMap[char])}"`; +export const tstlHeader = "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n"; + /** * Checks that a name is valid for use in lua function declaration syntax: * @@ -124,22 +125,7 @@ export class LuaPrinter { constructor(private emitHost: EmitHost, program: ts.Program, fileName: string) { this.options = program.getCompilerOptions(); - - if (this.options.outDir) { - const relativeFileName = path.relative(program.getCommonSourceDirectory(), fileName); - if (this.options.sourceRoot) { - // When sourceRoot is specified, just use relative path inside rootDir - this.sourceFile = relativeFileName; - } else { - // Calculate relative path from rootDir to outDir - const outputPath = path.resolve(this.options.outDir, relativeFileName); - this.sourceFile = path.relative(path.dirname(outputPath), fileName); - } - // We want forward slashes, even in windows - this.sourceFile = normalizeSlashes(this.sourceFile); - } else { - this.sourceFile = path.basename(fileName); // File will be in same dir as source - } + this.sourceFile = fileName; } public print(file: lua.File): PrintResult { @@ -201,7 +187,7 @@ export class LuaPrinter { let header = file.trivia; if (!this.options.noHeader) { - header += "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n"; + header += tstlHeader; } const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require; diff --git a/src/cli/parse.ts b/src/cli/parse.ts index fff0ef0d0..d29bf4e83 100644 --- a/src/cli/parse.ts +++ b/src/cli/parse.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import { CompilerOptions, LuaLibImportKind, LuaTarget } from "../CompilerOptions"; +import { BuildMode, CompilerOptions, LuaLibImportKind, LuaTarget } from "../CompilerOptions"; import * as cliDiagnostics from "./diagnostics"; export interface ParsedCommandLine extends ts.ParsedCommandLine { @@ -24,6 +24,12 @@ interface CommandLineOptionOfPrimitive extends CommandLineOptionBase { type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfPrimitive; export const optionDeclarations: CommandLineOption[] = [ + { + name: "buildMode", + description: "'default' or 'library'. Compiling as library will not resolve external dependencies.", + type: "enum", + choices: Object.values(BuildMode), + }, { name: "luaBundle", description: "The name of the lua file to bundle output lua to. Requires luaBundleEntry.", diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts index 422a93cac..22bd5882c 100644 --- a/src/transformation/utils/diagnostics.ts +++ b/src/transformation/utils/diagnostics.ts @@ -124,10 +124,6 @@ export const invalidAmbientIdentifierName = createErrorDiagnosticFactory( (text: string) => `Invalid ambient identifier name '${text}'. Ambient identifiers must be valid lua identifiers.` ); -export const unresolvableRequirePath = createErrorDiagnosticFactory( - (path: string) => `Cannot create require path. Module '${path}' does not exist within --rootDir.` -); - export const unsupportedVarDeclaration = createErrorDiagnosticFactory( "`var` declarations are not supported. Use `let` or `const` instead." ); diff --git a/src/transformation/visitors/modules/import.ts b/src/transformation/visitors/modules/import.ts index d36bb0509..d8daba8c9 100644 --- a/src/transformation/visitors/modules/import.ts +++ b/src/transformation/visitors/modules/import.ts @@ -1,7 +1,6 @@ import * as path from "path"; import * as ts from "typescript"; import * as lua from "../../../LuaAST"; -import { formatPathToLuaPath } from "../../../utils"; import { FunctionVisitor, TransformationContext } from "../../context"; import { AnnotationKind, getSymbolAnnotations } from "../../utils/annotations"; import { createDefaultExportStringLiteral } from "../../utils/export"; @@ -10,36 +9,13 @@ import { createSafeName } from "../../utils/safe-names"; import { peekScope } from "../../utils/scope"; import { transformIdentifier } from "../identifier"; import { transformPropertyName } from "../literal"; -import { unresolvableRequirePath } from "../../utils/diagnostics"; -const getAbsoluteImportPath = (relativePath: string, directoryPath: string, options: ts.CompilerOptions): string => - !relativePath.startsWith(".") && options.baseUrl - ? path.resolve(options.baseUrl, relativePath) - : path.resolve(directoryPath, relativePath); - -function getImportPath(context: TransformationContext, relativePath: string, node: ts.Node): string { - const { options, sourceFile } = context; - const { fileName } = sourceFile; - const rootDir = options.rootDir ? path.resolve(options.rootDir) : path.resolve("."); - - const absoluteImportPath = path.format( - path.parse(getAbsoluteImportPath(relativePath, path.dirname(fileName), options)) - ); - const absoluteRootDirPath = path.format(path.parse(rootDir)); - if (absoluteImportPath.includes(absoluteRootDirPath)) { - return formatPathToLuaPath(absoluteImportPath.replace(absoluteRootDirPath, "").slice(1)); - } else { - context.diagnostics.push(unresolvableRequirePath(node, relativePath)); - return relativePath; - } -} - -function shouldResolveModulePath(context: TransformationContext, moduleSpecifier: ts.Expression): boolean { +function isNoResolutionPath(context: TransformationContext, moduleSpecifier: ts.Expression): boolean { const moduleOwnerSymbol = context.checker.getSymbolAtLocation(moduleSpecifier); - if (!moduleOwnerSymbol) return true; + if (!moduleOwnerSymbol) return false; const annotations = getSymbolAnnotations(moduleOwnerSymbol); - return !annotations.has(AnnotationKind.NoResolution); + return annotations.has(AnnotationKind.NoResolution); } export function createModuleRequire( @@ -49,8 +25,8 @@ export function createModuleRequire( ): lua.CallExpression { const params: lua.Expression[] = []; if (ts.isStringLiteral(moduleSpecifier)) { - const modulePath = shouldResolveModulePath(context, moduleSpecifier) - ? getImportPath(context, moduleSpecifier.text.replace(/"/g, ""), moduleSpecifier) + const modulePath = isNoResolutionPath(context, moduleSpecifier) + ? `@NoResolution:${moduleSpecifier.text}` : moduleSpecifier.text; params.push(lua.createStringLiteral(modulePath)); diff --git a/src/transpilation/bundle.ts b/src/transpilation/bundle.ts index c696c6900..31d58b27b 100644 --- a/src/transpilation/bundle.ts +++ b/src/transpilation/bundle.ts @@ -2,13 +2,14 @@ import * as path from "path"; import { SourceNode } from "source-map"; import * as ts from "typescript"; import { CompilerOptions } from "../CompilerOptions"; -import { escapeString } from "../LuaPrinter"; +import { escapeString, tstlHeader } from "../LuaPrinter"; import { cast, formatPathToLuaPath, isNonNull, normalizeSlashes, trimExtension } from "../utils"; import { couldNotFindBundleEntryPoint } from "./diagnostics"; -import { EmitFile, EmitHost, ProcessedFile } from "./utils"; +import { getEmitOutDir, getEmitPathRelativeToOutDir, getSourceDir } from "./transpiler"; +import { EmitFile, ProcessedFile } from "./utils"; -const createModulePath = (baseDir: string, pathToResolve: string) => - escapeString(formatPathToLuaPath(trimExtension(path.relative(baseDir, pathToResolve)))); +const createModulePath = (pathToResolve: string, program: ts.Program) => + escapeString(formatPathToLuaPath(trimExtension(getEmitPathRelativeToOutDir(pathToResolve, program)))); // Override `require` to read from ____modules table. const requireOverride = ` @@ -32,42 +33,37 @@ local function require(file) end `; -export function getBundleResult( - program: ts.Program, - emitHost: EmitHost, - files: ProcessedFile[] -): [ts.Diagnostic[], EmitFile] { +export function getBundleResult(program: ts.Program, files: ProcessedFile[]): [ts.Diagnostic[], EmitFile] { const diagnostics: ts.Diagnostic[] = []; const options = program.getCompilerOptions() as CompilerOptions; const bundleFile = cast(options.luaBundle, isNonNull); const entryModule = cast(options.luaBundleEntry, isNonNull); - const rootDir = program.getCommonSourceDirectory(); - const outDir = options.outDir ?? rootDir; - const projectRootDir = options.configFilePath - ? path.dirname(options.configFilePath) - : emitHost.getCurrentDirectory(); - // Resolve project settings relative to project file. - const resolvedEntryModule = path.resolve(projectRootDir, entryModule); - const outputPath = normalizeSlashes(path.resolve(projectRootDir, bundleFile)); + const resolvedEntryModule = path.resolve(getSourceDir(program), entryModule); + const outputPath = normalizeSlashes(path.resolve(getEmitOutDir(program), bundleFile)); - if (!files.some(f => f.fileName === resolvedEntryModule)) { + if (program.getSourceFile(resolvedEntryModule) === undefined && program.getSourceFile(entryModule) === undefined) { diagnostics.push(couldNotFindBundleEntryPoint(entryModule)); - return [diagnostics, { outputPath, code: "" }]; } // For each file: [""] = function() end, - const moduleTableEntries = files.map(f => moduleSourceNode(f, createModulePath(outDir, f.fileName))); + const moduleTableEntries = files.map(f => moduleSourceNode(f, createModulePath(f.fileName, program))); // Create ____modules table containing all entries from moduleTableEntries const moduleTable = createModuleTableNode(moduleTableEntries); // return require("") - const entryPoint = `return require(${createModulePath(outDir, resolvedEntryModule)})\n`; + const entryPoint = `return require(${createModulePath(entryModule, program)})\n`; + + const sourceChunks = [requireOverride, moduleTable, entryPoint]; + + if (!options.noHeader) { + sourceChunks.unshift(tstlHeader); + } - const bundleNode = joinSourceChunks([requireOverride, moduleTable, entryPoint]); + const bundleNode = joinSourceChunks(sourceChunks); const { code, map } = bundleNode.toStringWithSourceMap(); return [ @@ -83,7 +79,7 @@ export function getBundleResult( function moduleSourceNode({ code, sourceMapNode }: ProcessedFile, modulePath: string): SourceNode { const tableEntryHead = `[${modulePath}] = function() `; - const tableEntryTail = "end,\n"; + const tableEntryTail = " end,\n"; return joinSourceChunks([tableEntryHead, sourceMapNode ?? code, tableEntryTail]); } diff --git a/src/transpilation/diagnostics.ts b/src/transpilation/diagnostics.ts index d0e609677..bfae6b140 100644 --- a/src/transpilation/diagnostics.ts +++ b/src/transpilation/diagnostics.ts @@ -1,8 +1,19 @@ import * as ts from "typescript"; import { createSerialDiagnosticFactory } from "../utils"; -const createDiagnosticFactory = (getMessage: (...args: TArgs) => string) => - createSerialDiagnosticFactory((...args: TArgs) => ({ messageText: getMessage(...args) })); +const createDiagnosticFactory = ( + getMessage: (...args: TArgs) => string, + category: ts.DiagnosticCategory = ts.DiagnosticCategory.Error +) => createSerialDiagnosticFactory((...args: TArgs) => ({ messageText: getMessage(...args), category })); + +export const couldNotResolveRequire = createDiagnosticFactory( + (requirePath: string, containingFile: string) => + `Could not resolve require path '${requirePath}' in file ${containingFile}.` +); + +export const couldNotReadDependency = createDiagnosticFactory( + (dependency: string) => `Could not read content of resolved dependency ${dependency}.` +); export const toLoadItShouldBeTranspiled = createDiagnosticFactory( (kind: string, transform: string) => diff --git a/src/transpilation/index.ts b/src/transpilation/index.ts index 608b42818..59e83233a 100644 --- a/src/transpilation/index.ts +++ b/src/transpilation/index.ts @@ -45,7 +45,7 @@ const libCache: { [key: string]: ts.SourceFile } = {}; /** @internal */ export function createVirtualProgram(input: Record, options: CompilerOptions = {}): ts.Program { const compilerHost: ts.CompilerHost = { - fileExists: () => true, + fileExists: fileName => fileName in input || ts.sys.fileExists(fileName), getCanonicalFileName: fileName => fileName, getCurrentDirectory: () => "", getDefaultLibFileName: ts.getDefaultLibFileName, diff --git a/src/transpilation/output-collector.ts b/src/transpilation/output-collector.ts index d18137c5a..866c1f79a 100644 --- a/src/transpilation/output-collector.ts +++ b/src/transpilation/output-collector.ts @@ -2,6 +2,7 @@ import * as ts from "typescript"; import { intersection, union } from "../utils"; export interface TranspiledFile { + outPath: string; sourceFiles: ts.SourceFile[]; lua?: string; luaSourceMap?: string; @@ -18,7 +19,7 @@ export function createEmitOutputCollector() { const writeFile: ts.WriteFileCallback = (fileName, data, _bom, _onError, sourceFiles = []) => { let file = files.find(f => intersection(f.sourceFiles, sourceFiles).length > 0); if (!file) { - file = { sourceFiles: [...sourceFiles] }; + file = { outPath: fileName, sourceFiles: [...sourceFiles] }; files.push(file); } else { file.sourceFiles = union(file.sourceFiles, sourceFiles); diff --git a/src/transpilation/resolve.ts b/src/transpilation/resolve.ts new file mode 100644 index 000000000..3eca25e2e --- /dev/null +++ b/src/transpilation/resolve.ts @@ -0,0 +1,231 @@ +import * as path from "path"; +import * as resolve from "enhanced-resolve"; +import * as ts from "typescript"; +import * as fs from "fs"; +import { EmitHost, ProcessedFile } from "./utils"; +import { SourceNode } from "source-map"; +import { getEmitPathRelativeToOutDir, getProjectRoot, getSourceDir } from "./transpiler"; +import { formatPathToLuaPath, trimExtension } from "../utils"; +import { couldNotReadDependency, couldNotResolveRequire } from "./diagnostics"; +import { BuildMode } from "../CompilerOptions"; + +const resolver = resolve.ResolverFactory.createResolver({ + extensions: [".lua"], + enforceExtension: true, // Resolved file must be a lua file + fileSystem: { ...new resolve.CachedInputFileSystem(fs) }, + useSyncFileSystemCalls: true, +}); + +interface ResolutionResult { + resolvedFiles: ProcessedFile[]; + diagnostics: ts.Diagnostic[]; +} + +export function resolveDependencies(program: ts.Program, files: ProcessedFile[], emitHost: EmitHost): ResolutionResult { + const outFiles: ProcessedFile[] = [...files]; + const diagnostics: ts.Diagnostic[] = []; + + // Resolve dependencies for all processed files + for (const file of files) { + const resolutionResult = resolveFileDependencies(file, program, emitHost); + outFiles.push(...resolutionResult.resolvedFiles); + diagnostics.push(...resolutionResult.diagnostics); + } + + return { resolvedFiles: outFiles, diagnostics }; +} + +function resolveFileDependencies(file: ProcessedFile, program: ts.Program, emitHost: EmitHost): ResolutionResult { + const dependencies: ProcessedFile[] = []; + const diagnostics: ts.Diagnostic[] = []; + + for (const required of findRequiredPaths(file.code)) { + // Do no resolve lualib + if (required === "lualib_bundle") { + continue; + } + + // Do not resolve noResolution paths + if (required.startsWith("@NoResolution:")) { + const path = required.replace("@NoResolution:", ""); + replaceRequireInCode(file, required, path); + replaceRequireInSourceMap(file, required, path); + continue; + } + + // Try to resolve the import starting from the directory `file` is in + const fileDir = path.dirname(file.fileName); + const resolvedDependency = resolveDependency(fileDir, required, program, emitHost); + if (resolvedDependency) { + // Figure out resolved require path and dependency output path + const resolvedRequire = getEmitPathRelativeToOutDir(resolvedDependency, program); + + if (shouldRewriteRequires(resolvedDependency, program)) { + replaceRequireInCode(file, required, resolvedRequire); + replaceRequireInSourceMap(file, required, resolvedRequire); + } + + // If dependency is not part of project, add dependency to output and resolve its dependencies recursively + if (shouldIncludeDependency(resolvedDependency, program)) { + // If dependency resolved successfully, read its content + const dependencyContent = emitHost.readFile(resolvedDependency); + if (dependencyContent === undefined) { + diagnostics.push(couldNotReadDependency(resolvedDependency)); + continue; + } + + const dependency = { + fileName: resolvedDependency, + code: dependencyContent, + }; + const nestedDependencies = resolveFileDependencies(dependency, program, emitHost); + dependencies.push(dependency, ...nestedDependencies.resolvedFiles); + diagnostics.push(...nestedDependencies.diagnostics); + } + } else { + // Could not resolve dependency, add a diagnostic and make some fallback path + diagnostics.push(couldNotResolveRequire(required, path.relative(getProjectRoot(program), file.fileName))); + + const fallbackRequire = fallbackResolve(required, getSourceDir(program), fileDir); + replaceRequireInCode(file, required, fallbackRequire); + replaceRequireInSourceMap(file, required, fallbackRequire); + } + } + return { resolvedFiles: dependencies, diagnostics }; +} + +function resolveDependency( + fileDirectory: string, + dependency: string, + program: ts.Program, + emitHost: EmitHost +): string | undefined { + // Check if file is a file in the project + const resolvedPath = path.join(fileDirectory, dependency); + + if (isProjectFile(resolvedPath, program)) { + // JSON files need their extension as part of the import path, caught by this branch + return resolvedPath; + } + + const resolvedFile = resolvedPath + ".ts"; + if (isProjectFile(resolvedFile, program)) { + return resolvedFile; + } + + const projectIndexPath = path.resolve(resolvedPath, "index.ts"); + if (isProjectFile(projectIndexPath, program)) { + return projectIndexPath; + } + + // Check if this is a sibling of a required lua file + const luaFilePath = path.resolve(fileDirectory, dependency + ".lua"); + if (emitHost.fileExists(luaFilePath)) { + return luaFilePath; + } + + // Not a TS file in our project sources, use resolver to check if we can find dependency + try { + const resolveResult = resolver.resolveSync({}, fileDirectory, dependency); + if (resolveResult) { + return resolveResult; + } + } catch (e) { + // resolveSync errors if it fails to resolve + } + + return undefined; +} + +function shouldRewriteRequires(resolvedDependency: string, program: ts.Program) { + return !isNodeModulesFile(resolvedDependency) || !isBuildModeLibrary(program); +} + +function shouldIncludeDependency(resolvedDependency: string, program: ts.Program) { + // Never include lua files (again) that are transpiled from project sources + if (!hasSourceFileInProject(resolvedDependency, program)) { + // Always include lua files not in node_modules (internal lua sources) + if (!isNodeModulesFile(resolvedDependency)) { + return true; + } else { + // Only include node_modules files if not in library mode + return !isBuildModeLibrary(program); + } + } + return false; +} + +function isBuildModeLibrary(program: ts.Program) { + return program.getCompilerOptions().buildMode === BuildMode.Library; +} + +function findRequiredPaths(code: string): string[] { + // Find all require("") paths in a lua code string + const paths: string[] = []; + const pattern = /require\("(.+)"\)/g; + // eslint-disable-next-line @typescript-eslint/ban-types + let match: RegExpExecArray | null; + while ((match = pattern.exec(code))) { + paths.push(match[1]); + } + + return paths; +} + +function replaceRequireInCode(file: ProcessedFile, originalRequire: string, newRequire: string): void { + const requirePath = formatPathToLuaPath(newRequire.replace(".lua", "")); + file.code = file.code.replace(`require("${originalRequire}")`, `require("${requirePath}")`); +} + +function replaceRequireInSourceMap(file: ProcessedFile, originalRequire: string, newRequire: string): void { + const requirePath = formatPathToLuaPath(newRequire.replace(".lua", "")); + if (file.sourceMapNode) { + replaceInSourceMap(file.sourceMapNode, file.sourceMapNode, `"${originalRequire}"`, `"${requirePath}"`); + } +} + +function replaceInSourceMap(node: SourceNode, parent: SourceNode, require: string, resolvedRequire: string): boolean { + if ((!node.children || node.children.length === 0) && node.toString() === require) { + parent.children = [new SourceNode(node.line, node.column, node.source, [resolvedRequire])]; + return true; // Stop after finding the first occurrence + } + + if (node.children) { + for (const c of node.children) { + if (replaceInSourceMap(c, node, require, resolvedRequire)) { + return true; // Occurrence found in one of the children + } + } + } + + return false; // Did not find the require +} + +function isNodeModulesFile(filePath: string): boolean { + return path + .normalize(filePath) + .split(path.sep) + .some(p => p === "node_modules"); +} + +function isProjectFile(file: string, program: ts.Program): boolean { + return program.getSourceFile(file) !== undefined; +} + +function hasSourceFileInProject(filePath: string, program: ts.Program) { + const pathWithoutExtension = trimExtension(filePath); + return ( + isProjectFile(pathWithoutExtension + ".ts", program) || isProjectFile(pathWithoutExtension + ".json", program) + ); +} + +// Transform an import path to a lua require that is probably not correct, but can be used as fallback when regular resolution fails +function fallbackResolve(required: string, sourceRootDir: string, fileDir: string): string { + return formatPathToLuaPath( + path + .normalize(path.join(path.relative(sourceRootDir, fileDir), required)) + .split(path.sep) + .filter(s => s !== "." && s !== "..") + .join(path.sep) + ); +} diff --git a/src/transpilation/transpile.ts b/src/transpilation/transpile.ts index cbc77c13c..0d3071f73 100644 --- a/src/transpilation/transpile.ts +++ b/src/transpilation/transpile.ts @@ -65,9 +65,12 @@ export function getProgramTranspileResult( diagnostics.push(...transformDiagnostics); if (!options.noEmit && !options.emitDeclarationOnly) { const printResult = printer(program, emitHost, sourceFile.fileName, file); - const sourceRootDir = program.getCommonSourceDirectory(); - const fileName = path.resolve(sourceRootDir, sourceFile.fileName); - transpiledFiles.push({ sourceFiles: [sourceFile], fileName, luaAst: file, ...printResult }); + transpiledFiles.push({ + sourceFiles: [sourceFile], + fileName: path.normalize(sourceFile.fileName), + luaAst: file, + ...printResult, + }); } }; diff --git a/src/transpilation/transpiler.ts b/src/transpilation/transpiler.ts index 205b6b2db..7b5eb0f3f 100644 --- a/src/transpilation/transpiler.ts +++ b/src/transpilation/transpiler.ts @@ -4,6 +4,7 @@ import { isBundleEnabled } from "../CompilerOptions"; import { getLuaLibBundle } from "../LuaLib"; import { normalizeSlashes, trimExtension } from "../utils"; import { getBundleResult } from "./bundle"; +import { resolveDependencies } from "./resolve"; import { getProgramTranspileResult, TranspileOptions } from "./transpile"; import { EmitFile, EmitHost, ProcessedFile } from "./utils"; @@ -33,6 +34,7 @@ export class Transpiler { writeFile, emitOptions ); + const { emitPlan } = this.getEmitPlan(program, diagnostics, freshFiles); const options = program.getCompilerOptions(); @@ -53,28 +55,79 @@ export class Transpiler { files: ProcessedFile[] ): { emitPlan: EmitFile[] } { const options = program.getCompilerOptions(); - const rootDir = program.getCommonSourceDirectory(); - const outDir = options.outDir ?? rootDir; const lualibRequired = files.some(f => f.code.includes('require("lualib_bundle")')); if (lualibRequired) { - const fileName = normalizeSlashes(path.resolve(rootDir, "lualib_bundle.lua")); + // Add lualib bundle to source dir 'virtually', will be moved to correct output dir in emitPlan + const fileName = normalizeSlashes(path.resolve(getSourceDir(program), "lualib_bundle.lua")); files.unshift({ fileName, code: getLuaLibBundle(this.emitHost) }); } + // Resolve imported modules and modify output Lua requires + const resolutionResult = resolveDependencies(program, files, this.emitHost); + diagnostics.push(...resolutionResult.diagnostics); + let emitPlan: EmitFile[]; if (isBundleEnabled(options)) { - const [bundleDiagnostics, bundleFile] = getBundleResult(program, this.emitHost, files); + const [bundleDiagnostics, bundleFile] = getBundleResult(program, resolutionResult.resolvedFiles); diagnostics.push(...bundleDiagnostics); emitPlan = [bundleFile]; } else { - emitPlan = files.map(file => { - const pathInOutDir = path.resolve(outDir, path.relative(rootDir, file.fileName)); - const outputPath = normalizeSlashes(trimExtension(pathInOutDir) + ".lua"); - return { ...file, outputPath }; - }); + emitPlan = resolutionResult.resolvedFiles.map(file => ({ + ...file, + outputPath: getEmitPath(file.fileName, program), + })); } return { emitPlan }; } } + +export function getEmitPath(file: string, program: ts.Program): string { + const relativeOutputPath = getEmitPathRelativeToOutDir(file, program); + const outDir = getEmitOutDir(program); + + return path.join(outDir, relativeOutputPath); +} + +export function getEmitPathRelativeToOutDir(fileName: string, program: ts.Program): string { + const sourceDir = getSourceDir(program); + // Default output path is relative path in source dir + let emitPathSplits = path.relative(sourceDir, fileName).split(path.sep); + + // If source is in a parent directory of source dir, move it into the source dir + emitPathSplits = emitPathSplits.filter(s => s !== ".."); + + // To avoid overwriting lua sources in node_modules, emit into lua_modules + if (emitPathSplits[0] === "node_modules") { + emitPathSplits[0] = "lua_modules"; + } + + // Make extension lua + emitPathSplits[emitPathSplits.length - 1] = trimExtension(emitPathSplits[emitPathSplits.length - 1]) + ".lua"; + + return path.join(...emitPathSplits); +} + +export function getSourceDir(program: ts.Program): string { + const rootDir = program.getCompilerOptions().rootDir; + if (rootDir && rootDir.length > 0) { + return path.isAbsolute(rootDir) ? rootDir : path.resolve(getProjectRoot(program), rootDir); + } + return program.getCommonSourceDirectory(); +} + +export function getEmitOutDir(program: ts.Program): string { + const outDir = program.getCompilerOptions().outDir; + if (outDir && outDir.length > 0) { + return path.isAbsolute(outDir) ? outDir : path.resolve(getProjectRoot(program), outDir); + } + return program.getCommonSourceDirectory(); +} + +export function getProjectRoot(program: ts.Program): string { + // Try to get the directory the tsconfig is in + const tsConfigPath = program.getCompilerOptions().configFilePath; + // If no tsconfig is known, use common source directory + return tsConfigPath ? path.dirname(tsConfigPath) : program.getCommonSourceDirectory(); +} diff --git a/src/transpilation/utils.ts b/src/transpilation/utils.ts index 099dfd400..a4acdfbb4 100644 --- a/src/transpilation/utils.ts +++ b/src/transpilation/utils.ts @@ -8,6 +8,7 @@ import * as lua from "../LuaAST"; import * as diagnosticFactories from "./diagnostics"; export interface EmitHost { + fileExists(path: string): boolean; getCurrentDirectory(): string; readFile(path: string): string | undefined; writeFile: ts.WriteFileCallback; diff --git a/test/cli/parse.spec.ts b/test/cli/parse.spec.ts index 06ed05f75..f66b97766 100644 --- a/test/cli/parse.spec.ts +++ b/test/cli/parse.spec.ts @@ -105,6 +105,9 @@ describe("command line", () => { ["sourceMapTraceback", "false", { sourceMapTraceback: false }], ["sourceMapTraceback", "true", { sourceMapTraceback: true }], + ["buildMode", "default", { buildMode: tstl.BuildMode.Default }], + ["buildMode", "library", { buildMode: tstl.BuildMode.Library }], + ["luaLibImport", "none", { luaLibImport: tstl.LuaLibImportKind.None }], ["luaLibImport", "always", { luaLibImport: tstl.LuaLibImportKind.Always }], ["luaLibImport", "inline", { luaLibImport: tstl.LuaLibImportKind.Inline }], @@ -213,6 +216,9 @@ describe("tsconfig", () => { ["sourceMapTraceback", false, { sourceMapTraceback: false }], ["sourceMapTraceback", true, { sourceMapTraceback: true }], + ["buildMode", "default", { buildMode: tstl.BuildMode.Default }], + ["buildMode", "library", { buildMode: tstl.BuildMode.Library }], + ["luaLibImport", "none", { luaLibImport: tstl.LuaLibImportKind.None }], ["luaLibImport", "always", { luaLibImport: tstl.LuaLibImportKind.Always }], ["luaLibImport", "inline", { luaLibImport: tstl.LuaLibImportKind.Inline }], diff --git a/test/translation/transformation.spec.ts b/test/translation/transformation.spec.ts index 4e0d7dd9d..42a4b4355 100644 --- a/test/translation/transformation.spec.ts +++ b/test/translation/transformation.spec.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import * as tstl from "../../src"; import { annotationDeprecated } from "../../src/transformation/utils/diagnostics"; +import { couldNotResolveRequire } from "../../src/transpilation/diagnostics"; import * as util from "../util"; const fixturesPath = path.join(__dirname, "./transformation"); @@ -14,7 +15,7 @@ const fixtures = fs test.each(fixtures)("Transformation (%s)", (_name, content) => { util.testModule(content) .setOptions({ luaLibImport: tstl.LuaLibImportKind.Require }) - .ignoreDiagnostics([annotationDeprecated.code]) + .ignoreDiagnostics([annotationDeprecated.code, couldNotResolveRequire.code]) .disableSemanticCheck() .expectLuaToMatchSnapshot(); }); diff --git a/test/transpile/__snapshots__/directories.spec.ts.snap b/test/transpile/__snapshots__/directories.spec.ts.snap index 901d885f4..eaf179a6c 100644 --- a/test/transpile/__snapshots__/directories.spec.ts.snap +++ b/test/transpile/__snapshots__/directories.spec.ts.snap @@ -1,13 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should be able to resolve ({"name": "baseurl", "options": [Object]}) 1`] = ` -Array [ - "directories/baseurl/out/lualib_bundle.lua", - "directories/baseurl/out/src/lib/nested/file.lua", - "directories/baseurl/out/src/main.lua", -] -`; - exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 1`] = ` Array [ "directories/basic/src/lib/file.lua", diff --git a/test/transpile/__snapshots__/project.spec.ts.snap b/test/transpile/__snapshots__/project.spec.ts.snap index 4db30085f..623480405 100644 --- a/test/transpile/__snapshots__/project.spec.ts.snap +++ b/test/transpile/__snapshots__/project.spec.ts.snap @@ -3,27 +3,23 @@ exports[`should transpile 1`] = ` Array [ Object { - "name": "project/otherFile.lua", - "text": "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] -local ____exports = {} + "filePath": "otherFile.lua", + "lua": "local ____exports = {} function ____exports.getNumber(self) return getAPIValue() end return ____exports ", - "writeByteOrderMark": false, }, Object { - "name": "project/index.lua", - "text": "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] -local ____exports = {} + "filePath": "index.lua", + "lua": "local ____exports = {} local ____otherFile = require(\\"otherFile\\") local getNumber = ____otherFile.getNumber local myNumber = getNumber(nil) setAPIValue(myNumber * 5) return ____exports ", - "writeByteOrderMark": false, }, ] `; diff --git a/test/transpile/bundle.spec.ts b/test/transpile/bundle.spec.ts index 0ccab7c70..69c86406e 100644 --- a/test/transpile/bundle.spec.ts +++ b/test/transpile/bundle.spec.ts @@ -1,20 +1,19 @@ import * as path from "path"; import * as util from "../util"; -import { transpileProjectResult } from "./run"; const projectDir = path.join(__dirname, "bundle"); const inputProject = path.join(projectDir, "tsconfig.json"); test("should transpile into one file", () => { - const { diagnostics, emittedFiles } = transpileProjectResult(inputProject); + const { diagnostics, transpiledFiles } = util.testProject(inputProject).getLuaResult(); expect(diagnostics).not.toHaveDiagnostics(); - expect(emittedFiles).toHaveLength(1); + expect(transpiledFiles).toHaveLength(1); - const { name, text } = emittedFiles[0]; + const { outPath, lua } = transpiledFiles[0]; // Verify the name is as specified in tsconfig - expect(name).toBe("bundle/bundle.lua"); + expect(outPath.endsWith("bundle/bundle.lua")).toBe(true); // Verify exported module by executing // Use an empty TS string because we already transpiled the TS project - util.testModule("").setLuaHeader(text).expectToEqual({ myNumber: 3 }); + util.testModule("").setLuaHeader(lua!).expectToEqual({ myNumber: 3 }); }); diff --git a/test/transpile/directories.spec.ts b/test/transpile/directories.spec.ts index 9141199e5..1c0bba994 100644 --- a/test/transpile/directories.spec.ts +++ b/test/transpile/directories.spec.ts @@ -13,7 +13,6 @@ test.each([ { name: "basic", options: { outDir: "out" } }, { name: "basic", options: { rootDir: "src" } }, { name: "basic", options: { rootDir: "src", outDir: "out" } }, - { name: "baseurl", options: { baseUrl: "./src/lib", rootDir: ".", outDir: "./out" } }, ])("should be able to resolve (%p)", ({ name, options: compilerOptions }) => { const projectPath = path.join(__dirname, "directories", name); jest.spyOn(process, "cwd").mockReturnValue(projectPath); diff --git a/test/transpile/directories/baseurl/src/lib/nested/file.ts b/test/transpile/directories/baseurl/src/lib/nested/file.ts deleted file mode 100644 index 4248a042b..000000000 --- a/test/transpile/directories/baseurl/src/lib/nested/file.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function test() { - return 1; -} diff --git a/test/transpile/directories/baseurl/src/main.ts b/test/transpile/directories/baseurl/src/main.ts deleted file mode 100644 index 1665f4e08..000000000 --- a/test/transpile/directories/baseurl/src/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { test } from "nested/file"; - -test(); diff --git a/test/transpile/module-resolution.spec.ts b/test/transpile/module-resolution.spec.ts new file mode 100644 index 000000000..c386e2d33 --- /dev/null +++ b/test/transpile/module-resolution.spec.ts @@ -0,0 +1,282 @@ +import * as path from "path"; +import * as tstl from "../../src"; +import * as util from "../util"; +import * as ts from "typescript"; +import { transpileProject } from "../../src"; + +describe("basic module resolution", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-node-modules"); + + const projectWithNodeModules = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")); + + test("can resolve global dependencies with declarations", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().globalWithDeclarationsResults).toEqual({ + foo: "foo from lua global with decls", + bar: "bar from lua global with decls: global with declarations!", + baz: "baz from lua global with decls", + }); + }); + + test("can resolve global dependencies with hand-written declarations", () => { + // No declarations in the node_modules directory, but written by hand in project dir + expect(projectWithNodeModules.getLuaExecutionResult().globalWithoutDeclarationsResults).toEqual({ + foo: "foo from lua global without decls", + bar: "bar from lua global without decls: global without declarations!", + baz: "baz from lua global without decls", + }); + }); + + test("can resolve module dependencies with declarations", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDeclarationsResults).toEqual({ + foo: "foo from lua module with decls", + bar: "bar from lua module with decls: module with declarations!", + baz: "baz from lua module with decls", + }); + }); + + test("can resolve module dependencies with hand-written declarations", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().moduleWithoutDeclarationsResults).toEqual({ + foo: "foo from lua module without decls", + bar: "bar from lua module without decls: module without declarations!", + baz: "baz from lua module without decls", + }); + }); + + test("can resolve package depencency with a dependency on another package", () => { + // Declarations in the node_modules directory + expect(projectWithNodeModules.getLuaExecutionResult().moduleWithDependencyResult).toEqual( + "Calling dependency: foo from lua module with decls" + ); + }); + + test("resolved package dependency included in bundle", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual({ + globalWithDeclarationsResults: { + foo: "foo from lua global with decls", + bar: "bar from lua global with decls: global with declarations!", + baz: "baz from lua global with decls", + }, + globalWithoutDeclarationsResults: { + foo: "foo from lua global without decls", + bar: "bar from lua global without decls: global without declarations!", + baz: "baz from lua global without decls", + }, + moduleWithDeclarationsResults: { + foo: "foo from lua module with decls", + bar: "bar from lua module with decls: module with declarations!", + baz: "baz from lua module with decls", + }, + moduleWithDependencyResult: "Calling dependency: foo from lua module with decls", + moduleWithoutDeclarationsResults: { + foo: "foo from lua module without decls", + bar: "bar from lua module without decls: module without declarations!", + baz: "baz from lua module without decls", + }, + }); + }); +}); + +describe("module resolution with chained dependencies", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain"); + const expectedResult = { result: "dependency3", result2: "someFunc from otherfile.lua" }; + + test("can resolve dependencies in chain", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .expectToEqual(expectedResult); + }); + + test("resolved package dependency included in bundle", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); + + test("works with different module setting", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ module: ts.ModuleKind.ESNext }) + .expectToEqual(expectedResult); + }); +}); + +describe("module resolution with outDir", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain"); + const expectedResult = { result: "dependency3", result2: "someFunc from otherfile.lua" }; + + test("emits files in outDir", () => { + const builder = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult); + + // Get the output paths relative to the project path + const outPaths = builder.getLuaResult().transpiledFiles.map(f => path.relative(projectPath, f.outPath)); + expect(outPaths).toHaveLength(5); + expect(outPaths).toContain(path.join("tstl-out", "main.lua")); + // Note: outputs to lua_modules + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency1", "index.lua")); + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency1", "otherfile.lua")); + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency2", "index.lua")); + expect(outPaths).toContain(path.join("tstl-out", "lua_modules", "dependency3", "index.lua")); + }); + + test("emits bundle in outDir", () => { + const mainFile = path.join(projectPath, "main.ts"); + const builder = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ outDir: "tstl-out", luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + + // Get the output paths relative to the project path + const outPaths = builder.getLuaResult().transpiledFiles.map(f => path.relative(projectPath, f.outPath)); + expect(outPaths).toHaveLength(1); + expect(outPaths).toContain(path.join("tstl-out", "bundle.lua")); + }); +}); + +describe("module resolution with sourceDir", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-sourceDir"); + const expectedResult = { + result: "dependency3", + functionInSubDir: "non-node_modules import", + functionReExportedFromSubDir: "nested func result", + nestedFunctionInSubDirOfSubDir: "nested func result", + nestedFunctionUsingFunctionFromParentDir: "nested func: non-node_modules import 2", + }; + + test("can resolve dependencies with sourceDir", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "src", "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult); + }); + + test("can resolve dependencies and bundle files with sourceDir", () => { + const mainFile = path.join(projectPath, "src", "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); +}); + +describe("module resolution project with lua sources", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-lua-sources"); + const expectedResult = { + funcFromLuaFile: "lua file in subdir", + funcFromSubDirLuaFile: "lua file in subdir", + }; + + test("can resolve lua dependencies", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult); + }); + + test("can resolve dependencies and bundle files with sourceDir", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); +}); + +describe("module resolution in library mode", () => { + test("result does not contain resolved paths", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-dependency-chain"); + + const { transpiledFiles } = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ buildMode: tstl.BuildMode.Library }) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + for (const file of transpiledFiles) { + expect(file.lua).not.toContain('require("lua_modules'); + } + }); + + test("project works in library mode because no external dependencies", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-lua-sources"); + + const { transpiledFiles } = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out", buildMode: tstl.BuildMode.Library }) + .expectToEqual({ + funcFromLuaFile: "lua file in subdir", + funcFromSubDirLuaFile: "lua file in subdir", + }) + .getLuaResult(); + + for (const file of transpiledFiles) { + expect(file.lua).not.toContain('require("lua_modules'); + } + }); + + test("bundle works in library mode because no external dependencies", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-lua-sources"); + const mainFile = path.join(projectPath, "main.ts"); + + const { transpiledFiles } = util + .testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ buildMode: tstl.BuildMode.Library, luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual({ + funcFromLuaFile: "lua file in subdir", + funcFromSubDirLuaFile: "lua file in subdir", + }) + .getLuaResult(); + + for (const file of transpiledFiles) { + expect(file.lua).not.toContain('require("lua_modules'); + } + }); +}); + +describe("module resolution project with dependencies built by tstl library mode", () => { + const projectPath = path.resolve(__dirname, "module-resolution", "project-with-tstl-library-modules"); + + // First compile dependencies into node_modules. NOTE: Actually writing to disk, very slow + transpileProject(path.join(projectPath, "dependency1-ts", "tsconfig.json")); + transpileProject(path.join(projectPath, "dependency2-ts", "tsconfig.json")); + + const expectedResult = { + dependency1IndexResult: "function in dependency 1 index: dependency1OtherFileFunc in dependency1/d1otherfile", + dependency1OtherFileFuncResult: "dependency1OtherFileFunc in dependency1/d1otherfile", + dependency2MainResult: "dependency 2 main", + dependency2OtherFileResult: "Dependency 2 func: my string argument", + }; + + test("can resolve lua dependencies", () => { + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(path.join(projectPath, "main.ts")) + .setOptions({ outDir: "tstl-out" }) + .expectToEqual(expectedResult); + }); + + test("can resolve dependencies and bundle", () => { + const mainFile = path.join(projectPath, "main.ts"); + util.testProject(path.join(projectPath, "tsconfig.json")) + .setMainFileName(mainFile) + .setOptions({ luaBundle: "bundle.lua", luaBundleEntry: mainFile }) + .expectToEqual(expectedResult); + }); +}); diff --git a/test/transpile/module-resolution/project-with-dependency-chain/main.ts b/test/transpile/module-resolution/project-with-dependency-chain/main.ts new file mode 100644 index 000000000..cda66cd55 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/main.ts @@ -0,0 +1,4 @@ +import * as dependency1 from "dependency1"; + +export const result = dependency1.f1(); +export const result2 = dependency1.otherFileFromDependency1(); diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.d.ts b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.d.ts new file mode 100644 index 000000000..5155e72c5 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.d.ts @@ -0,0 +1,3 @@ +/** @noSelfInFile */ +export declare function f1(): string; +export declare function otherFileFromDependency1(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.lua new file mode 100644 index 000000000..80cae6fb8 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/index.lua @@ -0,0 +1,7 @@ +local dependency2 = require("dependency2") +local otherfile = require("otherfile") + +return { + f1 = function() return dependency2.f2() end, + otherFileFromDependency1 = otherfile.someFunc +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/otherfile.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/otherfile.lua new file mode 100644 index 000000000..871964bf7 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency1/otherfile.lua @@ -0,0 +1,5 @@ +return { + someFunc = function() + return "someFunc from otherfile.lua" + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency2/index.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency2/index.lua new file mode 100644 index 000000000..10b36647e --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency2/index.lua @@ -0,0 +1,5 @@ +local dependency3 = require("dependency3") + +return { + f2 = dependency3.f3 +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency3/index.lua b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency3/index.lua new file mode 100644 index 000000000..112f9ba7e --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/node_modules/dependency3/index.lua @@ -0,0 +1,3 @@ +return { + f3 = function() return "dependency3" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-dependency-chain/tsconfig.json b/test/transpile/module-resolution/project-with-dependency-chain/tsconfig.json new file mode 100644 index 000000000..b76533290 --- /dev/null +++ b/test/transpile/module-resolution/project-with-dependency-chain/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "Node", + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "." + } +} diff --git a/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.d.ts b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.d.ts new file mode 100644 index 000000000..6166280f2 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function funcFromSubDir(): string; diff --git a/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.lua b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.lua new file mode 100644 index 000000000..a23007078 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/lua_sources/otherluaFile.lua @@ -0,0 +1,3 @@ +return { + funcFromSubDir = function() return "lua file in subdir" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-lua-sources/luafile.d.ts b/test/transpile/module-resolution/project-with-lua-sources/luafile.d.ts new file mode 100644 index 000000000..6294675a1 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/luafile.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function funcInLuaFile(): string; diff --git a/test/transpile/module-resolution/project-with-lua-sources/luafile.lua b/test/transpile/module-resolution/project-with-lua-sources/luafile.lua new file mode 100644 index 000000000..56e61090f --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/luafile.lua @@ -0,0 +1,3 @@ +return { + funcInLuaFile = function() return "lua file in subdir" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-lua-sources/main.ts b/test/transpile/module-resolution/project-with-lua-sources/main.ts new file mode 100644 index 000000000..d4c19b440 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/main.ts @@ -0,0 +1,5 @@ +import { funcInLuaFile } from "./luafile"; +import { funcFromSubDir } from "./lua_sources/otherluaFile"; + +export const funcFromLuaFile = funcInLuaFile(); +export const funcFromSubDirLuaFile = funcFromSubDir(); diff --git a/test/transpile/module-resolution/project-with-lua-sources/tsconfig.json b/test/transpile/module-resolution/project-with-lua-sources/tsconfig.json new file mode 100644 index 000000000..a07455ed7 --- /dev/null +++ b/test/transpile/module-resolution/project-with-lua-sources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + "target": "esnext", + "lib": ["esnext"], + "types": [], + "outDir": "tstl-out" + } +} diff --git a/test/transpile/module-resolution/project-with-node-modules/lua-global-without-decls.d.ts b/test/transpile/module-resolution/project-with-node-modules/lua-global-without-decls.d.ts new file mode 100644 index 000000000..4d1c1db1b --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/lua-global-without-decls.d.ts @@ -0,0 +1,4 @@ +/** @noSelfInFile */ +declare function fooGlobalWithoutDecls(): string; +declare function barGlobalWithoutDecls(param: string): string; +declare function bazGlobalWithoutDecls(): string; diff --git a/test/transpile/module-resolution/project-with-node-modules/lua-module-without-decls.d.ts b/test/transpile/module-resolution/project-with-node-modules/lua-module-without-decls.d.ts new file mode 100644 index 000000000..9ccfc9bfe --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/lua-module-without-decls.d.ts @@ -0,0 +1,9 @@ +/** @noSelfInFile */ +declare module "lua-module-without-decls" { + function foo(this: void): string; + function bar(this: void, param: string): string; +} + +declare module "lua-module-without-decls/baz" { + function baz(this: void): string; +} diff --git a/test/transpile/module-resolution/project-with-node-modules/main.ts b/test/transpile/module-resolution/project-with-node-modules/main.ts new file mode 100644 index 000000000..8d9332190 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/main.ts @@ -0,0 +1,39 @@ +import "lua-global-with-decls"; +import "lua-global-with-decls/baz"; + +import "lua-global-without-decls"; +import "lua-global-without-decls/baz"; + +import * as moduleWithDeclarations from "lua-module-with-decls"; +import * as moduleWithDeclarationsBaz from "lua-module-with-decls/baz"; + +import * as moduleWithoutDeclarations from "lua-module-without-decls"; +import * as moduleWithoutDeclarationsBaz from "lua-module-without-decls/baz"; + +import * as moduleWithDependency from "lua-module-with-dependency"; + +export const globalWithDeclarationsResults = { + foo: fooGlobal(), + bar: barGlobal("global with declarations!"), + baz: bazGlobal(), +}; + +export const globalWithoutDeclarationsResults = { + foo: fooGlobalWithoutDecls(), + bar: barGlobalWithoutDecls("global without declarations!"), + baz: bazGlobalWithoutDecls(), +}; + +export const moduleWithDeclarationsResults = { + foo: moduleWithDeclarations.foo(), + bar: moduleWithDeclarations.bar("module with declarations!"), + baz: moduleWithDeclarationsBaz.baz(), +}; + +export const moduleWithoutDeclarationsResults = { + foo: moduleWithoutDeclarations.foo(), + bar: moduleWithoutDeclarations.bar("module without declarations!"), + baz: moduleWithoutDeclarationsBaz.baz(), +}; + +export const moduleWithDependencyResult = moduleWithDependency.callDependency(); diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.d.ts new file mode 100644 index 000000000..23f55b1ad --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.d.ts @@ -0,0 +1 @@ +declare function bazGlobal(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.lua new file mode 100644 index 000000000..952807f1a --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/baz.lua @@ -0,0 +1,3 @@ +function bazGlobal() + return "baz from lua global with decls" +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.d.ts new file mode 100644 index 000000000..1339056b5 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.d.ts @@ -0,0 +1,3 @@ +/** @noSelfInFile */ +declare function fooGlobal(): string; +declare function barGlobal(param: string): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.lua new file mode 100644 index 000000000..d10edd189 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-with-decls/index.lua @@ -0,0 +1,6 @@ +function fooGlobal() + return "foo from lua global with decls" +end +function barGlobal(param) + return "bar from lua global with decls: " .. param +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/baz.lua new file mode 100644 index 000000000..02980e662 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/baz.lua @@ -0,0 +1,3 @@ +function bazGlobalWithoutDecls() + return "baz from lua global without decls" +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/index.lua new file mode 100644 index 000000000..46dd970a0 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-global-without-decls/index.lua @@ -0,0 +1,6 @@ +function fooGlobalWithoutDecls() + return "foo from lua global without decls" +end +function barGlobalWithoutDecls(param) + return "bar from lua global without decls: " .. param +end \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.d.ts new file mode 100644 index 000000000..5f46a6ce2 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function baz(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.lua new file mode 100644 index 000000000..f4e16a0d7 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/baz.lua @@ -0,0 +1,5 @@ +return { + baz = function() + return "baz from lua module with decls" + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.d.ts new file mode 100644 index 000000000..aa83ce4a6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.d.ts @@ -0,0 +1,3 @@ +/** @noSelfInFile */ +export declare function foo(): string; +export declare function bar(param: string): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.lua new file mode 100644 index 000000000..0c5d47fe6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-decls/index.lua @@ -0,0 +1,8 @@ +return { + foo = function() + return "foo from lua module with decls" + end, + bar = function(param) + return "bar from lua module with decls: " .. param + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.d.ts b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.d.ts new file mode 100644 index 000000000..381596acd --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.d.ts @@ -0,0 +1,2 @@ +/** @noSelf */ +export declare function callDependency(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.lua new file mode 100644 index 000000000..9bc475e5b --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-with-dependency/index.lua @@ -0,0 +1,7 @@ +local dependency = require("lua-module-with-decls") + +return { + callDependency = function() + return "Calling dependency: " .. dependency.foo() + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/baz.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/baz.lua new file mode 100644 index 000000000..7358f341b --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/baz.lua @@ -0,0 +1,5 @@ +return { + baz = function() + return "baz from lua module without decls" + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/index.lua b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/index.lua new file mode 100644 index 000000000..e73581589 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/node_modules/lua-module-without-decls/index.lua @@ -0,0 +1,8 @@ +return { + foo = function() + return "foo from lua module without decls" + end, + bar = function(param) + return "bar from lua module without decls: " .. param + end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-node-modules/tsconfig.json b/test/transpile/module-resolution/project-with-node-modules/tsconfig.json new file mode 100644 index 000000000..935b64af6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-node-modules/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "Node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "." + } +} diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.d.ts b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.d.ts new file mode 100644 index 000000000..761d6bc02 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.d.ts @@ -0,0 +1,2 @@ +/** @noSelfInFile */ +export declare function f1(): string; \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.lua b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.lua new file mode 100644 index 000000000..dcf28d6fd --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency1/index.lua @@ -0,0 +1,5 @@ +local dependency2 = require("dependency2") + +return { + f1 = function() return dependency2.f2() end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency2/index.lua b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency2/index.lua new file mode 100644 index 000000000..10b36647e --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency2/index.lua @@ -0,0 +1,5 @@ +local dependency3 = require("dependency3") + +return { + f2 = dependency3.f3 +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency3/index.lua b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency3/index.lua new file mode 100644 index 000000000..112f9ba7e --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/node_modules/dependency3/index.lua @@ -0,0 +1,3 @@ +return { + f3 = function() return "dependency3" end +} \ No newline at end of file diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/main.ts b/test/transpile/module-resolution/project-with-sourceDir/src/main.ts new file mode 100644 index 000000000..9014f7b62 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/main.ts @@ -0,0 +1,9 @@ +import * as dependency1 from "dependency1"; +import { func, nestedFunc } from "./subdir/otherfile"; +import { nestedFunc as nestedFuncOriginal, nestedFuncUsingParent } from "./subdir/subdirofsubdir/nestedfile"; + +export const result = dependency1.f1(); +export const functionInSubDir = func(); +export const functionReExportedFromSubDir = nestedFunc(); +export const nestedFunctionInSubDirOfSubDir = nestedFuncOriginal(); +export const nestedFunctionUsingFunctionFromParentDir = nestedFuncUsingParent(); diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile.ts b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile.ts new file mode 100644 index 000000000..c12dee955 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile.ts @@ -0,0 +1,5 @@ +export function func() { + return "non-node_modules import"; +} + +export { nestedFunc } from "./subdirofsubdir/nestedfile"; diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile2.ts b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile2.ts new file mode 100644 index 000000000..1132c2ea1 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/otherfile2.ts @@ -0,0 +1,3 @@ +export function func2() { + return "non-node_modules import 2"; +} diff --git a/test/transpile/module-resolution/project-with-sourceDir/src/subdir/subdirofsubdir/nestedfile.ts b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/subdirofsubdir/nestedfile.ts new file mode 100644 index 000000000..d316f023e --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/src/subdir/subdirofsubdir/nestedfile.ts @@ -0,0 +1,9 @@ +import { func2 } from "../otherfile2"; + +export function nestedFunc() { + return "nested func result"; +} + +export function nestedFuncUsingParent() { + return `nested func: ${func2()}`; +} diff --git a/test/transpile/module-resolution/project-with-sourceDir/tsconfig.json b/test/transpile/module-resolution/project-with-sourceDir/tsconfig.json new file mode 100644 index 000000000..200df2468 --- /dev/null +++ b/test/transpile/module-resolution/project-with-sourceDir/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "moduleResolution": "Node", + "target": "esnext", + "lib": ["esnext"], + "types": [], + "rootDir": "src" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/d1otherfile.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/d1otherfile.ts new file mode 100644 index 000000000..3877a1c52 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/d1otherfile.ts @@ -0,0 +1,3 @@ +export function dependency1OtherFileFunc() { + return "dependency1OtherFileFunc in dependency1/d1otherfile"; +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/index.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/index.ts new file mode 100644 index 000000000..fc1cdddec --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/index.ts @@ -0,0 +1,5 @@ +import { dependency1OtherFileFunc } from "./d1otherfile"; + +export function dependency1IndexFunc() { + return "function in dependency 1 index: " + dependency1OtherFileFunc(); +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/tsconfig.json new file mode 100644 index 000000000..5fcb76fbe --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency1-ts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "../node_modules/dependency1", + "declaration": true + }, + "tstl": { + "buildMode": "library" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/d2otherfile.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/d2otherfile.ts new file mode 100644 index 000000000..63e34f8b6 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/d2otherfile.ts @@ -0,0 +1,3 @@ +export function dependency2OtherFileFunc(this: void, arg: string) { + return `Dependency 2 func: ${arg}`; +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/main.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/main.ts new file mode 100644 index 000000000..03b90c231 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/main.ts @@ -0,0 +1,5 @@ +export function dependency2Main() { + return "dependency 2 main"; +} + +export * from "./d2otherfile"; diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/tsconfig.json new file mode 100644 index 000000000..f77b336a2 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/dependency2-ts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "../node_modules/dependency2", + "declaration": true + }, + "tstl": { + "buildMode": "library" + } +} diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/main.ts b/test/transpile/module-resolution/project-with-tstl-library-modules/main.ts new file mode 100644 index 000000000..c1286088a --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/main.ts @@ -0,0 +1,8 @@ +import { dependency1IndexFunc } from "dependency1"; +import { dependency1OtherFileFunc } from "dependency1/d1otherfile"; +import { dependency2Main, dependency2OtherFileFunc } from "dependency2/main"; + +export const dependency1IndexResult = dependency1IndexFunc(); +export const dependency1OtherFileFuncResult = dependency1OtherFileFunc(); +export const dependency2MainResult = dependency2Main(); +export const dependency2OtherFileResult = dependency2OtherFileFunc("my string argument"); diff --git a/test/transpile/module-resolution/project-with-tstl-library-modules/tsconfig.json b/test/transpile/module-resolution/project-with-tstl-library-modules/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/transpile/module-resolution/project-with-tstl-library-modules/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/test/transpile/project.spec.ts b/test/transpile/project.spec.ts index 6f262dada..965c834d7 100644 --- a/test/transpile/project.spec.ts +++ b/test/transpile/project.spec.ts @@ -1,8 +1,15 @@ import * as path from "path"; -import { transpileProjectResult } from "./run"; +import * as util from "../util"; test("should transpile", () => { - const { diagnostics, emittedFiles } = transpileProjectResult(path.join(__dirname, "project", "tsconfig.json")); - expect(diagnostics).not.toHaveDiagnostics(); - expect(emittedFiles).toMatchSnapshot(); + const projectDir = path.join(__dirname, "project"); + const { transpiledFiles } = util + .testProject(path.join(projectDir, "tsconfig.json")) + .setMainFileName(path.join(projectDir, "index.ts")) + .expectToHaveNoDiagnostics() + .getLuaResult(); + + expect( + transpiledFiles.map(f => ({ filePath: path.relative(projectDir, f.outPath), lua: f.lua })) + ).toMatchSnapshot(); }); diff --git a/test/transpile/run.ts b/test/transpile/run.ts index 8890d7a18..a24a76b9f 100644 --- a/test/transpile/run.ts +++ b/test/transpile/run.ts @@ -1,7 +1,6 @@ import * as path from "path"; import * as ts from "typescript"; import * as tstl from "../../src"; -import { parseConfigFileWithSystem } from "../../src/cli/tsconfig"; import { normalizeSlashes } from "../../src/utils"; export function transpileFilesResult(rootNames: string[], options: tstl.CompilerOptions) { @@ -16,12 +15,3 @@ export function transpileFilesResult(rootNames: string[], options: tstl.Compiler return { diagnostics, emittedFiles }; } - -export function transpileProjectResult(configFileName: string) { - const parseResult = parseConfigFileWithSystem(configFileName); - if (parseResult.errors.length > 0) { - return { diagnostics: parseResult.errors, emittedFiles: [] }; - } - - return transpileFilesResult(parseResult.fileNames, parseResult.options); -} diff --git a/test/unit/bundle.spec.ts b/test/unit/bundle.spec.ts index d28f4bf25..f961dddb0 100644 --- a/test/unit/bundle.spec.ts +++ b/test/unit/bundle.spec.ts @@ -59,6 +59,16 @@ test("entry point in directory", () => { .expectToEqual({ value: true }); }); +test("entry point in rootDir", () => { + util.testModule` + export { value } from "./module"; + ` + .setMainFileName("src/main.ts") + .addExtraFile("src/module.ts", "export const value = true") + .setOptions({ rootDir: "src", luaBundle: "bundle.lua", luaBundleEntry: "src/main.ts" }) + .expectToEqual({ value: true }); +}); + test("LuaLibImportKind.Require", () => { util.testBundle` export const result = [1, 2]; diff --git a/test/unit/file.spec.ts b/test/unit/file.spec.ts index 3a810dc60..12a136408 100644 --- a/test/unit/file.spec.ts +++ b/test/unit/file.spec.ts @@ -10,6 +10,20 @@ describe("JSON", () => { .setMainFileName("main.json") .expectToEqual(new util.ExecutionError("Unexpected end of JSON input")); }); + + test("JSON modules can be imported", () => { + util.testModule` + import * as jsonData from "./jsonModule.json"; + export const result = jsonData; + ` + .addExtraFile("jsonModule.json", '{ "jsonField1": "hello, this is JSON", "jsonField2": ["a", "b", "c"] }') + .expectToEqual({ + result: { + jsonField1: "hello, this is JSON", + jsonField2: ["a", "b", "c"], + }, + }); + }); }); describe("shebang", () => { diff --git a/test/unit/functions/noImplicitSelfOption.spec.ts b/test/unit/functions/noImplicitSelfOption.spec.ts index f61e37cb2..2da1d9722 100644 --- a/test/unit/functions/noImplicitSelfOption.spec.ts +++ b/test/unit/functions/noImplicitSelfOption.spec.ts @@ -1,3 +1,4 @@ +import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics"; import * as util from "../../util"; test("enables noSelfInFile behavior for functions", () => { @@ -31,10 +32,13 @@ test("generates declaration files with @noSelfInFile", () => { const fooDeclaration = fooBuilder.getLuaResult().transpiledFiles.find(f => f.declaration)?.declaration; util.assert(fooDeclaration !== undefined); + expect(fooDeclaration).toContain("@noSelfInFile"); + util.testModule` - import { bar } from "./foo.d"; + import { bar } from "./foo"; const test: (this: void) => void = bar; ` .addExtraFile("foo.d.ts", fooDeclaration) + .ignoreDiagnostics([couldNotResolveRequire.code]) // no foo implementation in the project to create foo.lua .expectToHaveNoDiagnostics(); }); diff --git a/test/unit/modules/__snapshots__/resolution.spec.ts.snap b/test/unit/modules/__snapshots__/resolution.spec.ts.snap index d4228141d..4db74c57f 100644 --- a/test/unit/modules/__snapshots__/resolution.spec.ts.snap +++ b/test/unit/modules/__snapshots__/resolution.spec.ts.snap @@ -2,9 +2,9 @@ exports[`doesn't resolve paths out of root dir: code 1`] = ` "local ____exports = {} -local module = require(\\"../module\\") +local module = require(\\"module\\") local ____ = module return ____exports" `; -exports[`doesn't resolve paths out of root dir: diagnostics 1`] = `"src/main.ts(2,33): error TSTL: Cannot create require path. Module '../module' does not exist within --rootDir."`; +exports[`doesn't resolve paths out of root dir: diagnostics 1`] = `"error TSTL: Could not resolve require path '../module' in file main.ts."`; diff --git a/test/unit/modules/modules.spec.ts b/test/unit/modules/modules.spec.ts index dd3d1dd7a..ffdc36ce9 100644 --- a/test/unit/modules/modules.spec.ts +++ b/test/unit/modules/modules.spec.ts @@ -58,8 +58,7 @@ test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌ import { foo } from "./${name}"; export { foo }; ` - .disableSemanticCheck() - .setLuaHeader('setmetatable(package.loaded, { __index = function() return { foo = "bar" } end })') + .addExtraFile(`${name}.ts`, 'export const foo = "bar";') .setReturnExport("foo") .expectToEqual("bar"); } diff --git a/test/unit/modules/resolution.spec.ts b/test/unit/modules/resolution.spec.ts index 541c28c56..791ae06b5 100644 --- a/test/unit/modules/resolution.spec.ts +++ b/test/unit/modules/resolution.spec.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import { unresolvableRequirePath } from "../../../src/transformation/utils/diagnostics"; +import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics"; import * as util from "../../util"; const requireRegex = /require\("(.*?)"\)/; @@ -69,6 +69,7 @@ test.each([ module; ` .setMainFileName(filePath) + .addExtraFile(`${usedPath}.ts`, "") .setOptions(options) .tap(expectToRequire(expected)); }); @@ -81,7 +82,7 @@ test("doesn't resolve paths out of root dir", () => { .setMainFileName("src/main.ts") .setOptions({ rootDir: "./src" }) .disableSemanticCheck() - .expectDiagnosticsToMatchSnapshot([unresolvableRequirePath.code]); + .expectDiagnosticsToMatchSnapshot([couldNotResolveRequire.code]); }); test.each([ diff --git a/test/unit/printer/sourcemaps.spec.ts b/test/unit/printer/sourcemaps.spec.ts index 84a6596a6..371c50df9 100644 --- a/test/unit/printer/sourcemaps.spec.ts +++ b/test/unit/printer/sourcemaps.spec.ts @@ -1,5 +1,6 @@ import { Position, SourceMapConsumer } from "source-map"; import * as tstl from "../../../src"; +import { couldNotResolveRequire } from "../../../src/transpilation/diagnostics"; import * as util from "../../util"; test.each([ @@ -144,7 +145,11 @@ test.each([ ], }, ])("Source map has correct mapping (%p)", async ({ code, assertPatterns }) => { - const file = util.testModule(code).expectToHaveNoDiagnostics().getMainLuaFileResult(); + const file = util + .testModule(code) + .ignoreDiagnostics([couldNotResolveRequire.code]) + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); const consumer = await new SourceMapConsumer(file.luaSourceMap); for (const { luaPattern, typeScriptPattern } of assertPatterns) { @@ -157,32 +162,25 @@ test.each([ }); test.each([ - { fileName: "/proj/foo.ts", config: {}, mapSource: "foo.ts", fullSource: "foo.ts" }, + { fileName: "/proj/foo.ts", config: {} }, { fileName: "/proj/src/foo.ts", config: { outDir: "/proj/dst" }, - mapSource: "../src/foo.ts", - fullSource: "../src/foo.ts", }, { fileName: "/proj/src/foo.ts", config: { rootDir: "/proj/src", outDir: "/proj/dst" }, - mapSource: "../src/foo.ts", - fullSource: "../src/foo.ts", }, { fileName: "/proj/src/sub/foo.ts", config: { rootDir: "/proj/src", outDir: "/proj/dst" }, - mapSource: "../../src/sub/foo.ts", - fullSource: "../../src/sub/foo.ts", }, { fileName: "/proj/src/sub/main.ts", config: { rootDir: "/proj/src", outDir: "/proj/dst", sourceRoot: "bin" }, - mapSource: "sub/main.ts", - fullSource: "bin/sub/main.ts", + fullSource: "bin/proj/src/sub/main.ts", }, -])("Source map has correct sources (%p)", async ({ fileName, config, mapSource, fullSource }) => { +])("Source map has correct sources (%p)", async ({ fileName, config, fullSource }) => { const file = util.testModule` const foo = "foo" ` @@ -192,11 +190,11 @@ test.each([ const sourceMap = JSON.parse(file.luaSourceMap); expect(sourceMap.sources).toHaveLength(1); - expect(sourceMap.sources[0]).toBe(mapSource); + expect(sourceMap.sources[0]).toBe(fileName); const consumer = await new SourceMapConsumer(file.luaSourceMap); expect(consumer.sources).toHaveLength(1); - expect(consumer.sources[0]).toBe(fullSource); + expect(consumer.sources[0]).toBe(fullSource ?? fileName); }); test.each([ diff --git a/test/util.ts b/test/util.ts index b553ef359..39259e784 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,6 +1,6 @@ /* eslint-disable jest/no-standalone-expect */ import * as nativeAssert from "assert"; -import { LauxLib, Lua, LuaLib, LUA_OK } from "lua-wasm-bindings/dist/lua"; +import { LauxLib, Lua, LuaLib, LuaState, LUA_OK } from "lua-wasm-bindings/dist/lua"; import * as fs from "fs"; import { stringify } from "javascript-stringify"; import * as path from "path"; @@ -9,6 +9,8 @@ import * as ts from "typescript"; import * as vm from "vm"; import * as tstl from "../src"; import { createEmitOutputCollector } from "../src/transpilation/output-collector"; +import { getEmitOutDir, transpileProject } from "../src"; +import { formatPathToLuaPath, normalizeSlashes } from "../src/utils"; const jsonLib = fs.readFileSync(path.join(__dirname, "json.lua"), "utf8"); const luaLib = fs.readFileSync(path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), "utf8"); @@ -128,7 +130,7 @@ export abstract class TestBuilder { return this; } - private options: tstl.CompilerOptions = { + protected options: tstl.CompilerOptions = { luaTarget: tstl.LuaTarget.Lua54, noHeader: true, skipLibCheck: true, @@ -148,7 +150,7 @@ export abstract class TestBuilder { protected mainFileName = "main.ts"; public setMainFileName(mainFileName: string): this { expect(this.hasProgram).toBe(false); - this.mainFileName = mainFileName; + this.mainFileName = normalizeSlashes(mainFileName); return this; } @@ -176,7 +178,10 @@ export abstract class TestBuilder { @memoize public getProgram(): ts.Program { this.hasProgram = true; - return tstl.createVirtualProgram({ ...this.extraFiles, [this.mainFileName]: this.getTsCode() }, this.options); + return tstl.createVirtualProgram( + { ...this.extraFiles, [normalizeSlashes(this.mainFileName)]: this.getTsCode() }, + this.options + ); } @memoize @@ -202,7 +207,10 @@ export abstract class TestBuilder { const { transpiledFiles } = this.getLuaResult(); const mainFile = this.options.luaBundle ? transpiledFiles[0] - : transpiledFiles.find(({ sourceFiles }) => sourceFiles.some(f => f.fileName === this.mainFileName)); + : transpiledFiles.find(({ sourceFiles }) => + sourceFiles.some(f => normalizeSlashes(f.fileName) === this.mainFileName) + ); + expect(mainFile).toMatchObject({ lua: expect.any(String), luaSourceMap: expect.any(String) }); return mainFile as ExecutableTranspiledFile; } @@ -259,9 +267,7 @@ export abstract class TestBuilder { public debug(): this { const transpiledFiles = this.getLuaResult().transpiledFiles; - const luaCode = transpiledFiles.map( - f => `[${f.sourceFiles.map(sf => sf.fileName).join(",")}]:\n${f.lua?.replace(/^/gm, " ")}` - ); + const luaCode = transpiledFiles.map(f => `[${f.outPath}]:\n${f.lua?.replace(/^/gm, " ")}`); const value = prettyFormat(this.getLuaExecutionResult()).replace(/^/gm, " "); console.log(`Lua Code:\n${luaCode.join("\n")}\n\nValue:\n${value}`); return this; @@ -366,35 +372,23 @@ export abstract class TestBuilder { // Load modules // Json - lua.lua_getglobal(L, "package"); - lua.lua_getfield(L, -1, "preload"); - lauxlib.luaL_loadstring(L, jsonLib); - lua.lua_setfield(L, -2, "json"); + this.packagePreloadLuaFile(L, lua, lauxlib, "json", jsonLib); // Lua lib if ( this.options.luaLibImport === tstl.LuaLibImportKind.Require || mainFile.includes('require("lualib_bundle")') ) { - lua.lua_getglobal(L, "package"); - lua.lua_getfield(L, -1, "preload"); - lauxlib.luaL_loadstring(L, luaLib); - lua.lua_setfield(L, -2, "lualib_bundle"); + this.packagePreloadLuaFile(L, lua, lauxlib, "lualib_bundle", luaLib); } - // Extra files + // Load all transpiled files into Lua's package cache const { transpiledFiles } = this.getLuaResult(); - - Object.keys(this.extraFiles).forEach(fileName => { - const transpiledExtraFile = transpiledFiles.find(({ sourceFiles }) => - sourceFiles.some(f => f.fileName === fileName) - ); - if (transpiledExtraFile?.lua) { - lua.lua_getglobal(L, "package"); - lua.lua_getfield(L, -1, "preload"); - lauxlib.luaL_loadstring(L, transpiledExtraFile.lua); - lua.lua_setfield(L, -2, fileName.replace(".ts", "")); + for (const transpiledFile of transpiledFiles) { + if (transpiledFile.lua) { + const filePath = path.relative(getEmitOutDir(this.getProgram()), transpiledFile.outPath); + this.packagePreloadLuaFile(L, lua, lauxlib, filePath, transpiledFile.lua); } - }); + } // Execute Main const wrappedMainCode = ` @@ -423,6 +417,14 @@ end)());`; } } + private packagePreloadLuaFile(state: LuaState, lua: Lua, lauxlib: LauxLib, fileName: string, fileContent: string) { + // Adding source Lua to the package.preload cache will allow require to find it + lua.lua_getglobal(state, "package"); + lua.lua_getfield(state, -1, "preload"); + lauxlib.luaL_loadstring(state, fileContent); + lua.lua_setfield(state, -2, formatPathToLuaPath(fileName.replace(".lua", ""))); + } + private executeJs(): any { const { transpiledFiles } = this.getJsResult(); // Custom require for extra files. Really basic. Global support is hacky @@ -539,6 +541,22 @@ class ExpressionTestBuilder extends AccessorTestBuilder { } } +class ProjectTestBuilder extends ModuleTestBuilder { + constructor(private tsConfig: string) { + super(""); + this.setOptions({ configFilePath: this.tsConfig, ...tstl.parseConfigFileWithSystem(this.tsConfig) }); + } + + @memoize + public getLuaResult(): tstl.TranspileVirtualProjectResult { + // Override getLuaResult to use transpileProject with tsconfig.json instead + const collector = createEmitOutputCollector(); + const { diagnostics } = transpileProject(this.tsConfig, this.options, collector.writeFile); + + return { diagnostics: [...diagnostics], transpiledFiles: collector.files }; + } +} + const createTestBuilderFactory = ( builder: new (_tsCode: string) => T, serializeSubstitutions: boolean @@ -566,3 +584,4 @@ export const testFunction = createTestBuilderFactory(FunctionTestBuilder, false) export const testFunctionTemplate = createTestBuilderFactory(FunctionTestBuilder, true); export const testExpression = createTestBuilderFactory(ExpressionTestBuilder, false); export const testExpressionTemplate = createTestBuilderFactory(ExpressionTestBuilder, true); +export const testProject = createTestBuilderFactory(ProjectTestBuilder, false);