UNPKG

typescript-to-lua

Version:

A generic TypeScript to Lua transpiler. Write your code in TypeScript and publish Lua!

379 lines 19.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveDependencies = resolveDependencies; const path = require("path"); const resolve = require("enhanced-resolve"); const fs = require("fs"); const source_map_1 = require("source-map"); const transpiler_1 = require("./transpiler"); const utils_1 = require("../utils"); const diagnostics_1 = require("./diagnostics"); const CompilerOptions_1 = require("../CompilerOptions"); const find_lua_requires_1 = require("./find-lua-requires"); const picomatch = require("picomatch"); const resolver = resolve.ResolverFactory.createResolver({ extensions: [".lua"], enforceExtension: true, // Resolved file must be a lua file fileSystem: { ...new resolve.CachedInputFileSystem(fs, 0) }, useSyncFileSystemCalls: true, conditionNames: ["require", "node", "tstl", "default"], symlinks: false, // Do not resolve symlinks to their original paths (that breaks node_modules detection) }); class ResolutionContext { constructor(program, options, emitHost, plugins) { this.program = program; this.options = options; this.emitHost = emitHost; this.plugins = plugins; this.diagnostics = []; this.resolvedFiles = new Map(); this.processedDependencies = new Set(); // value is false if already searched but not found this.pathToFile = new Map(); const unique = [...new Set(options.noResolvePaths)]; const matchers = unique.map(x => picomatch(x)); this.noResolvePaths = matchers; } addAndResolveDependencies(file) { if (this.resolvedFiles.has(file.fileName)) return; this.resolvedFiles.set(file.fileName, file); // Do this backwards so the replacements do not mess with the positions of the previous requires for (const required of (0, find_lua_requires_1.findLuaRequires)(file.code).reverse()) { // Do not resolve noResolution paths if (required.requirePath.startsWith("@NoResolution:")) { // Remove @NoResolution prefix if not building in library mode if (!isBuildModeLibrary(this.program)) { const path = required.requirePath.replace("@NoResolution:", ""); replaceRequireInCode(file, required, path, this.options.extension); replaceRequireInSourceMap(file, required, path, this.options.extension); } // Skip continue; } // Try to resolve the import starting from the directory `file` is in this.resolveImport(file, required); } } resolveImport(file, required) { var _a; // Do no resolve lualib - always use the lualib of the application entry point, not the lualib from external packages if (required.requirePath === "lualib_bundle") { this.resolvedFiles.set("lualib_bundle", { fileName: "lualib_bundle", code: "" }); return; } if (this.noResolvePaths.find(isMatch => isMatch(required.requirePath))) { if (this.options.tstlVerbose) { console.log(`Skipping module resolution of ${required.requirePath} as it is in the tsconfig noResolvePaths.`); } return; } const dependencyPath = (_a = this.resolveDependencyPathsWithPlugins(file, required.requirePath)) !== null && _a !== void 0 ? _a : this.resolveDependencyPath(file, required.requirePath); if (!dependencyPath) return this.couldNotResolveImport(required, file); if (this.options.tstlVerbose) { console.log(`Resolved ${required.requirePath} to ${(0, utils_1.normalizeSlashes)(dependencyPath)}`); } this.processDependency(dependencyPath); // Figure out resolved require path and dependency output path if (shouldRewriteRequires(dependencyPath, this.program)) { const resolvedRequire = (0, transpiler_1.getEmitPathRelativeToOutDir)(dependencyPath, this.program); replaceRequireInCode(file, required, resolvedRequire, this.options.extension); replaceRequireInSourceMap(file, required, resolvedRequire, this.options.extension); } } resolveDependencyPathsWithPlugins(requiringFile, dependency) { const requiredFromLuaFile = requiringFile.fileName.endsWith(".lua"); for (const plugin of this.plugins) { if (plugin.moduleResolution != null) { const pluginResolvedPath = plugin.moduleResolution(dependency, requiringFile.fileName, this.options, this.emitHost); if (pluginResolvedPath !== undefined) { // If resolved path is absolute no need to further resolve it if (path.isAbsolute(pluginResolvedPath)) { return pluginResolvedPath; } // If lua file is in node_module if (requiredFromLuaFile && isNodeModulesFile(requiringFile.fileName)) { // If requiring file is in lua module, try to resolve sibling in that file first const resolvedNodeModulesFile = this.resolveLuaDependencyPathFromNodeModules(requiringFile, pluginResolvedPath); if (resolvedNodeModulesFile) { if (this.options.tstlVerbose) { console.log(`Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.`); } return resolvedNodeModulesFile; } } const resolvedPath = this.formatPathToFile(pluginResolvedPath, requiringFile); const fileFromPath = this.getFileFromPath(resolvedPath); if (fileFromPath) { if (this.options.tstlVerbose) { console.log(`Resolved file path for module ${dependency} to path ${pluginResolvedPath} using plugin.`); } return fileFromPath; } } } } } formatPathToFile(targetPath, required) { var _a; const isRelative = ["/", "./", "../"].some(p => targetPath.startsWith(p)); // // If the import is relative, always resolve it relative to the requiring file // // If the import is not relative, resolve it relative to options.baseUrl if it is set const fileDirectory = path.dirname(required.fileName); const relativeTo = isRelative ? fileDirectory : (_a = this.options.baseUrl) !== null && _a !== void 0 ? _a : fileDirectory; // // Check if file is a file in the project const resolvedPath = path.join(relativeTo, targetPath); return resolvedPath; } processDependency(dependencyPath) { if (this.processedDependencies.has(dependencyPath)) return; this.processedDependencies.add(dependencyPath); if (!shouldIncludeDependency(dependencyPath, this.program)) return; // If dependency is not part of project, add dependency to output and resolve its dependencies recursively const dependencyContent = this.emitHost.readFile(dependencyPath); if (dependencyContent === undefined) { this.diagnostics.push((0, diagnostics_1.couldNotReadDependency)(dependencyPath)); return; } const dependency = { fileName: dependencyPath, code: dependencyContent, }; this.addAndResolveDependencies(dependency); } couldNotResolveImport(required, file) { const fallbackRequire = fallbackResolve(required, (0, transpiler_1.getSourceDir)(this.program), path.dirname(file.fileName)); replaceRequireInCode(file, required, fallbackRequire, this.options.extension); replaceRequireInSourceMap(file, required, fallbackRequire, this.options.extension); this.diagnostics.push((0, diagnostics_1.couldNotResolveRequire)(required.requirePath, path.relative((0, transpiler_1.getProjectRoot)(this.program), file.fileName))); } resolveDependencyPath(requiringFile, dependency) { const fileDirectory = path.dirname(requiringFile.fileName); if (this.options.tstlVerbose) { console.log(`Resolving "${dependency}" from ${(0, utils_1.normalizeSlashes)(requiringFile.fileName)}`); } const requiredFromLuaFile = requiringFile.fileName.endsWith(".lua"); const dependencyPath = requiredFromLuaFile ? luaRequireToPath(dependency) : dependency; if (requiredFromLuaFile && isNodeModulesFile(requiringFile.fileName)) { // If requiring file is in lua module, try to resolve sibling in that file first const resolvedNodeModulesFile = this.resolveLuaDependencyPathFromNodeModules(requiringFile, dependencyPath); if (resolvedNodeModulesFile) return resolvedNodeModulesFile; } // Check if file is a file in the project const resolvedPath = this.formatPathToFile(dependencyPath, requiringFile); const fileFromPath = this.getFileFromPath(resolvedPath); if (fileFromPath) return fileFromPath; if (this.options.paths && this.options.baseUrl) { // If no file found yet and paths are present, try to find project file via paths mappings const fileFromPaths = this.tryGetModuleNameFromPaths(dependencyPath, this.options.paths, this.options.baseUrl); if (fileFromPaths) return fileFromPaths; } // Not a TS file in our project sources, use resolver to check if we can find dependency try { const resolveResult = resolver.resolveSync({}, fileDirectory, dependencyPath); if (resolveResult) return resolveResult; } catch (e) { // resolveSync errors if it fails to resolve if (this.options.tstlVerbose && e.details) { // Output resolver log console.log(e.details); } } return undefined; } resolveLuaDependencyPathFromNodeModules(requiringFile, dependency) { // We don't know for sure where the lua root is, so guess it is at package root const splitPath = path.normalize(requiringFile.fileName).split(path.sep); let packageRootIndex = splitPath.lastIndexOf("node_modules") + 2; let packageRoot = splitPath.slice(0, packageRootIndex).join(path.sep); while (packageRootIndex < splitPath.length) { // Try to find lua file relative to currently guessed Lua root const resolvedPath = path.join(packageRoot, dependency); const fileFromPath = this.getFileFromPath(resolvedPath); if (fileFromPath) { return fileFromPath; } else { // Did not find file at current root, try again one directory deeper packageRoot = path.join(packageRoot, splitPath[packageRootIndex++]); } } return undefined; } getFileFromPath(resolvedPath) { const existingFile = this.pathToFile.get(resolvedPath); if (existingFile) return existingFile; if (existingFile === false) return undefined; const file = this.searchForFileFromPath(resolvedPath); this.pathToFile.set(resolvedPath, file !== null && file !== void 0 ? file : false); return file; } searchForFileFromPath(resolvedPath) { const possibleProjectFiles = [ resolvedPath, // JSON files need their extension as part of the import path, caught by this branch, resolvedPath + ".ts", // Regular ts file path.join(resolvedPath, "index.ts"), // Index ts file, resolvedPath + ".tsx", // tsx file path.join(resolvedPath, "index.tsx"), // tsx index ]; for (const possibleFile of possibleProjectFiles) { if (isProjectFile(possibleFile, this.program)) { return possibleFile; } } // Check if this is a lua file in the project sources const possibleLuaProjectFiles = [ resolvedPath + ".lua", // lua file in sources path.join(resolvedPath, "index.lua"), // lua index file in sources path.join(resolvedPath, "init.lua"), // lua looks for <require>/init.lua if it cannot find <require>.lua ]; for (const possibleFile of possibleLuaProjectFiles) { if (this.emitHost.fileExists(possibleFile)) { return possibleFile; } } } // Taken from TS and modified: https://github.com/microsoft/TypeScript/blob/88a1e3a1dd8d2d86e844ff1c16d5f041cebcfdb9/src/compiler/moduleSpecifiers.ts#L562 tryGetModuleNameFromPaths(relativeToBaseUrl, paths, baseUrl) { const relativeImport = removeTrailingDirectorySeparator((0, utils_1.normalizeSlashes)(relativeToBaseUrl)); for (const [importPattern, targetPatterns] of Object.entries(paths)) { const pattern = removeFileExtension((0, utils_1.normalizeSlashes)(importPattern)); const indexOfStar = pattern.indexOf("*"); if (indexOfStar !== -1) { // Try to match <prefix>*<suffix> to relativeImport const prefix = pattern.substring(0, indexOfStar); const suffix = pattern.substring(indexOfStar + 1); if ((relativeImport.length >= prefix.length + suffix.length && relativeImport.startsWith(prefix) && relativeImport.endsWith(suffix)) || (!suffix && relativeImport === removeTrailingDirectorySeparator(prefix))) { // If import matches <prefix>*<suffix>, extract the matched * path const matchedStar = relativeImport.substring(prefix.length, relativeImport.length - suffix.length); // Try to resolve to the target patterns with filled in * pattern for (const target of targetPatterns) { const file = this.getFileFromPath(path.join(baseUrl, target.replace("*", matchedStar))); if (file) return file; } } } else if (pattern === relativeImport) { // If there is no * pattern, check for exact matches and try those targets for (const target of targetPatterns) { const file = this.getFileFromPath(path.join(baseUrl, target)); if (file) return file; } } } } } function resolveDependencies(program, files, emitHost, plugins) { const options = program.getCompilerOptions(); const resolutionContext = new ResolutionContext(program, options, emitHost, plugins); // Resolve dependencies for all processed files for (const file of files) { if (options.tstlVerbose) { console.log(`Resolving dependencies for ${(0, utils_1.normalizeSlashes)(file.fileName)}`); } resolutionContext.addAndResolveDependencies(file); } return { resolvedFiles: [...resolutionContext.resolvedFiles.values()], diagnostics: resolutionContext.diagnostics }; } function shouldRewriteRequires(resolvedDependency, program) { return !isBuildModeLibrary(program) || !isNodeModulesFile(resolvedDependency); } function shouldIncludeDependency(resolvedDependency, program) { // Never include lua files (again) that are transpiled from project sources if (hasSourceFileInProject(resolvedDependency, program)) return false; // Always include lua files not in node_modules (internal lua sources) if (!isNodeModulesFile(resolvedDependency)) return true; // Only include node_modules files if not in library mode return !isBuildModeLibrary(program); } function isBuildModeLibrary(program) { return program.getCompilerOptions().buildMode === CompilerOptions_1.BuildMode.Library; } function replaceRequireInCode(file, originalRequire, newRequire, extension) { const requirePath = requirePathForFile(newRequire, extension); file.code = file.code = file.code.substring(0, originalRequire.from) + `require("${requirePath}")` + file.code.substring(originalRequire.to + 1); } function replaceRequireInSourceMap(file, originalRequire, newRequire, extension) { const requirePath = requirePathForFile(newRequire, extension); if (file.sourceMapNode) { replaceInSourceMap(file.sourceMapNode, file.sourceMapNode, `"${originalRequire.requirePath}"`, `"${requirePath}"`); } } function requirePathForFile(filePath, extension = ".lua") { if (!extension.startsWith(".")) { extension = `.${extension}`; } if (filePath.endsWith(extension)) { return (0, utils_1.formatPathToLuaPath)(filePath.substring(0, filePath.length - extension.length)); } else { return (0, utils_1.formatPathToLuaPath)(filePath); } } function replaceInSourceMap(node, parent, require, resolvedRequire) { if ((!node.children || node.children.length === 0) && node.toString() === require) { parent.children = [new source_map_1.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) { return path .normalize(filePath) .split(path.sep) .some(p => p === "node_modules"); } function isProjectFile(file, program) { return program.getSourceFile(file) !== undefined; } function hasSourceFileInProject(filePath, program) { const pathWithoutExtension = (0, utils_1.trimExtension)(filePath); return (isProjectFile(pathWithoutExtension + ".ts", program) || isProjectFile(pathWithoutExtension + ".tsx", 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, sourceRootDir, fileDir) { return (0, utils_1.formatPathToLuaPath)(path .normalize(path.join(path.relative(sourceRootDir, fileDir), required.requirePath)) .split(path.sep) .filter(s => s !== "." && s !== "..") .join(path.sep)); } function luaRequireToPath(requirePath) { return requirePath.replace(/\./g, path.sep); } function removeFileExtension(path) { return path.includes(".") ? (0, utils_1.trimExtension)(path) : path; } function removeTrailingDirectorySeparator(path) { return path.endsWith("/") || path.endsWith("\\") ? path.substring(0, -1) : path; } //# sourceMappingURL=resolve.js.map