diff --git a/.eslintignore b/.eslintignore index 707cb8140..f5c92cd2a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,4 @@ /dist -/test/translation/transformation -/test/cli/errors -/test/cli/watch -/test/transpile/directories -/test/transpile/outFile +/test/translation/__fixtures__ +/test/cli/__fixtures__ +/test/transpile/__fixtures__/directories diff --git a/.prettierignore b/.prettierignore index cbfe6aa04..f38b564c0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ /dist /coverage -/test/translation/transformation/characterEscapeSequence.ts +/test/translation/__fixtures__/characterEscapeSequence.ts diff --git a/jest.config.js b/jest.config.js index 09d82545f..b7c677eee 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,8 @@ module.exports = { // https://github.com/facebook/jest/issues/5274 "!/src/tstl.ts", ], - watchPathIgnorePatterns: ["cli/watch/[^/]+$", "src/lualib"], + watchPathIgnorePatterns: ["cli/__fixtures__", "src/lualib"], + watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"], setupFilesAfterEnv: ["/test/setup.ts"], testEnvironment: "node", diff --git a/package-lock.json b/package-lock.json index dbe13db2e..310985709 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1862,15 +1862,6 @@ "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", "dev": true }, - "@types/resolve": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.14.0.tgz", - "integrity": "sha512-bmjNBW6tok+67iOsASeYSJxSgY++BIR35nGyGLORTDirhra9reJ0shgGL3U7KPDUbOBCx8JrlCjd4d/y5uiMRQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -2944,6 +2935,22 @@ "once": "^1.4.0" } }, + "enhanced-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.2.0.tgz", + "integrity": "sha512-NZlGLl8DxmZoq0uqPPtJfsCAir68uR047+Udsh1FH4+5ydGQdMurn/A430A1BtxASVmMEuS7/XiJ5OxJ9apAzQ==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + } + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3831,6 +3838,12 @@ } } }, + "fs-monkey": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.1.tgz", + "integrity": "sha512-fcSa+wyTqZa46iWweI7/ZiUfegOZl0SG8+dltIwFXo7+zYU9J9kpS3NB6pZcSlJdhvIwp81Adx2XhZorncxiaA==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6901,6 +6914,174 @@ } } }, + "jest-watch-typeahead": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-0.6.1.tgz", + "integrity": "sha512-ITVnHhj3Jd/QkqQcTqZfRgjfyRhDFM/auzgVo2RKvSwi18YMvh0WvXDJFoFED6c7jd/5jxtu4kSOb9PTu2cPVg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.1", + "chalk": "^4.0.0", + "jest-regex-util": "^26.0.0", + "jest-watcher": "^26.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "@jest/console": { + "version": "26.5.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.5.2.tgz", + "integrity": "sha512-lJELzKINpF1v74DXHbCRIkQ/+nUV1M+ntj+X1J8LxCgpmJZjfLmhFejiMSbjjD66fayxl5Z06tbs3HMyuik6rw==", + "dev": true, + "requires": { + "@jest/types": "^26.5.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^26.5.2", + "jest-util": "^26.5.2", + "slash": "^3.0.0" + } + }, + "@jest/test-result": { + "version": "26.5.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.5.2.tgz", + "integrity": "sha512-E/Zp6LURJEGSCWpoMGmCFuuEI1OWuI3hmZwmULV0GsgJBh7u0rwqioxhRU95euUuviqBDN8ruX/vP/4bwYolXw==", + "dev": true, + "requires": { + "@jest/console": "^26.5.2", + "@jest/types": "^26.5.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/types": { + "version": "26.5.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.5.2.tgz", + "integrity": "sha512-QDs5d0gYiyetI8q+2xWdkixVQMklReZr4ltw7GFDtb4fuJIBCE6mzj2LnitGqCuAlLap6wPyb8fpoHgwZz5fdg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "jest-message-util": { + "version": "26.5.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.5.2.tgz", + "integrity": "sha512-Ocp9UYZ5Jl15C5PNsoDiGEk14A4NG0zZKknpWdZGoMzJuGAkVt10e97tnEVMYpk7LnQHZOfuK2j/izLBMcuCZw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.5.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-util": { + "version": "26.5.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.5.2.tgz", + "integrity": "sha512-WTL675bK+GSSAYgS8z9FWdCT2nccO1yTIplNLPlP0OD8tUk/H5IrWKMMRudIQQ0qp8bb4k+1Qa8CxGKq9qnYdg==", + "dev": true, + "requires": { + "@jest/types": "^26.5.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-watcher": { + "version": "26.5.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.5.2.tgz", + "integrity": "sha512-i3m1NtWzF+FXfJ3ljLBB/WQEp4uaNhX7QcQUWMokcifFTUQBDFyUMEwk0JkJ1kopHbx7Een3KX0Q7+9koGM/Pw==", + "dev": true, + "requires": { + "@jest/test-result": "^26.5.2", + "@jest/types": "^26.5.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^26.5.2", + "string-length": "^4.0.1" + } + }, + "stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, "jest-watcher": { "version": "26.0.1", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.0.1.tgz", @@ -7289,6 +7470,15 @@ "object-visit": "^1.0.0" } }, + "memfs": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.0.tgz", + "integrity": "sha512-f/xxz2TpdKv6uDn6GtHee8ivFyxwxmPuXatBb1FBwxYNuVpbM3k/Y1Z+vC0mH/dIXXrukYfe3qe5J32Dfjg93A==", + "dev": true, + "requires": { + "fs-monkey": "1.0.1" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -7715,7 +7905,8 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true }, "path-type": { "version": "2.0.0", @@ -8054,6 +8245,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -8847,6 +9039,11 @@ } } }, + "tapable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.0.0.tgz", + "integrity": "sha512-bjzn0C0RWoffnNdTzNi7rNDhs1Zlwk2tRXgk8EiHKAOX1Mag3d6T0Y5zNa7l9CJ+EoUne/0UHdwS8tMbkh9zDg==" + }, "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 a2623d785..5795872f7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "node": ">=12.13.0" }, "dependencies": { - "resolve": "^1.15.1", + "enhanced-resolve": "^5.2.0", "source-map": "^0.7.3", "typescript": ">=4.0.2" }, @@ -45,7 +45,6 @@ "@types/glob": "^7.1.1", "@types/jest": "^25.1.3", "@types/node": "^13.7.7", - "@types/resolve": "1.14.0", "@typescript-eslint/eslint-plugin": "^2.31.0", "@typescript-eslint/parser": "^4.1.0", "eslint": "^6.8.0", @@ -56,9 +55,19 @@ "javascript-stringify": "^2.0.1", "jest": "^26.0.1", "jest-circus": "^25.1.0", + "jest-watch-typeahead": "^0.6.1", "lua-types": "^2.8.0", + "memfs": "^3.2.0", "prettier": "^2.0.5", "ts-jest": "^26.3.0", "ts-node": "^8.6.2" + }, + "peerDependencies": { + "ts-node": "*" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } } } diff --git a/src/CompilerOptions.ts b/src/CompilerOptions.ts index 889a5837a..7ff53746e 100644 --- a/src/CompilerOptions.ts +++ b/src/CompilerOptions.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import * as diagnosticFactories from "./transpilation/diagnostics"; +import * as diagnosticFactories from "./compiler/diagnostics"; type KnownKeys = { [K in keyof T]: string extends K ? never : number extends K ? never : K } extends { [K in keyof T]: infer U; @@ -25,6 +25,7 @@ export interface LuaPluginImport { } export type CompilerOptions = OmitIndexSignature & { + mode?: CompilerMode; noImplicitSelf?: boolean; noHeader?: boolean; luaBundle?: string; @@ -37,6 +38,11 @@ export type CompilerOptions = OmitIndexSignature & { [option: string]: any; }; +export enum CompilerMode { + App = "app", + Lib = "lib", +} + export enum LuaLibImportKind { None = "none", Always = "always", @@ -52,7 +58,9 @@ export enum LuaTarget { LuaJIT = "JIT", } -export const isBundleEnabled = (options: CompilerOptions) => +export const isBundleEnabled = ( + options: CompilerOptions +): options is CompilerOptions & Required> => options.luaBundle !== undefined && options.luaBundleEntry !== undefined; export function validateOptions(options: CompilerOptions): ts.Diagnostic[] { diff --git a/src/LuaLib.ts b/src/LuaLib.ts index fde43c604..4f5d1fda9 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { EmitHost } from "./transpilation"; +import { CompilerHost } from "./compiler"; export enum LuaLibFeature { ArrayConcat = "ArrayConcat", @@ -105,7 +105,7 @@ const luaLibDependencies: Partial> = { SymbolRegistry: [LuaLibFeature.Symbol], }; -export function loadLuaLibFeatures(features: Iterable, emitHost: EmitHost): string { +export function loadLuaLibFeatures(features: Iterable, host: CompilerHost): string { let result = ""; const loadedFeatures = new Set(); @@ -120,7 +120,7 @@ export function loadLuaLibFeatures(features: Iterable, emitHost: } const featurePath = path.resolve(__dirname, `../dist/lualib/${feature}.lua`); - const luaLibFeature = emitHost.readFile(featurePath); + const luaLibFeature = host.readFile(featurePath); if (luaLibFeature !== undefined) { result += luaLibFeature + "\n"; } else { @@ -136,10 +136,10 @@ export function loadLuaLibFeatures(features: Iterable, emitHost: } let luaLibBundleContent: string; -export function getLuaLibBundle(emitHost: EmitHost): string { +export function getLuaLibBundle(host: CompilerHost): string { if (luaLibBundleContent === undefined) { const lualibPath = path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"); - const result = emitHost.readFile(lualibPath); + const result = host.readFile(lualibPath); if (result !== undefined) { luaLibBundleContent = result; } else { diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index a021ae7d5..c8f82ee52 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -1,12 +1,12 @@ import * as path from "path"; -import { Mapping, SourceMapGenerator, SourceNode } from "source-map"; +import { SourceNode } from "source-map"; import * as ts from "typescript"; +import { CompilerHost } from "./compiler"; import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions"; 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 { assert, intersperse, invertObject, normalizeSlashes } 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 +25,13 @@ const escapeStringMap: Record = { export const escapeString = (value: string) => `"${value.replace(escapeStringRegExp, char => escapeStringMap[char])}"`; +const unescapeStringRegExp = new RegExp(`\\\\${escapeStringRegExp.source}`, "g"); +const unescapeStingMap = invertObject(escapeStringMap); +export const unescapeLuaString = (value: string) => { + assert(value.startsWith('"') && value.endsWith('"'), 'Only strings generated by "escapeString" can be unescaped'); + return value.slice(1, -1).replace(unescapeStringRegExp, char => unescapeStingMap[char]); +}; + /** * Checks that a name is valid for use in lua function declaration syntax: * @@ -39,6 +46,13 @@ const isValidLuaFunctionDeclarationName = (str: string) => /^[a-zA-Z0-9_.]+$/.te function isSimpleExpression(expression: lua.Expression): boolean { switch (expression.kind) { case lua.SyntaxKind.CallExpression: + const calledExpression = (expression as lua.CallExpression).expression; + // __TS__Resolve macro is guaranteed to be pure + if (lua.isIdentifier(calledExpression) && calledExpression.text === "__TS__Resolve") { + return true; + } + + return false; case lua.SyntaxKind.MethodCallExpression: case lua.SyntaxKind.FunctionExpression: return false; @@ -71,23 +85,7 @@ function isSimpleExpression(expression: lua.Expression): boolean { type SourceChunk = string | SourceNode; -export type Printer = (program: ts.Program, emitHost: EmitHost, fileName: string, file: lua.File) => PrintResult; - -export interface PrintResult { - code: string; - sourceMap: string; - sourceMapNode: SourceNode; -} - -export function createPrinter(printers: Printer[]): Printer { - if (printers.length === 0) { - return (program, emitHost, fileName, file) => new LuaPrinter(emitHost, program, fileName).print(file); - } else if (printers.length === 1) { - return printers[0]; - } else { - throw new Error("Only one plugin can specify 'printer'"); - } -} +export type Printer = (program: ts.Program, host: CompilerHost, fileName: string, file: lua.File) => SourceNode; export class LuaPrinter { private static operatorMap: Record = { @@ -119,109 +117,62 @@ export class LuaPrinter { }; private currentIndent = ""; - private sourceFile: string; + private fileName: string; private options: CompilerOptions; - constructor(private emitHost: EmitHost, program: ts.Program, fileName: string) { + constructor(private host: CompilerHost, 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; + this.fileName = 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); + this.fileName = path.relative(path.dirname(outputPath), fileName); } // We want forward slashes, even in windows - this.sourceFile = normalizeSlashes(this.sourceFile); + this.fileName = normalizeSlashes(this.fileName); } else { - this.sourceFile = path.basename(fileName); // File will be in same dir as source - } - } - - public print(file: lua.File): PrintResult { - // Add traceback lualib if sourcemap traceback option is enabled - if (this.options.sourceMapTraceback) { - file.luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack); - } - - const sourceRoot = this.options.sourceRoot - ? // According to spec, sourceRoot is simply prepended to the source name, so the slash should be included - this.options.sourceRoot.replace(/[\\/]+$/, "") + "/" - : ""; - const rootSourceNode = this.printFile(file); - const sourceMap = this.buildSourceMap(sourceRoot, rootSourceNode); - - let code = rootSourceNode.toString(); - - if (this.options.inlineSourceMap) { - code += "\n" + this.printInlineSourceMap(sourceMap); + this.fileName = path.basename(fileName); // File will be in same dir as source } - - if (this.options.sourceMapTraceback) { - const stackTraceOverride = this.printStackTraceOverride(rootSourceNode); - code = code.replace("{#SourceMapTraceback}", stackTraceOverride); - } - - return { code, sourceMap: sourceMap.toString(), sourceMapNode: rootSourceNode }; - } - - private printInlineSourceMap(sourceMap: SourceMapGenerator): string { - const map = sourceMap.toString(); - const base64Map = Buffer.from(map).toString("base64"); - - return `--# sourceMappingURL=data:application/json;base64,${base64Map}\n`; - } - - private printStackTraceOverride(rootNode: SourceNode): string { - let currentLine = 1; - const map: Record = {}; - rootNode.walk((chunk, mappedPosition) => { - if (mappedPosition.line !== undefined && mappedPosition.line > 0) { - if (map[currentLine] === undefined) { - map[currentLine] = mappedPosition.line; - } else { - map[currentLine] = Math.min(map[currentLine], mappedPosition.line); - } - } - - currentLine += chunk.split("\n").length - 1; - }); - - const mapItems = Object.entries(map).map(([line, original]) => `["${line}"] = ${original}`); - const mapString = "{" + mapItems.join(",") + "}"; - - return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`; } - private printFile(file: lua.File): SourceNode { + public print(file: lua.File): SourceNode { let header = file.trivia; if (!this.options.noHeader) { header += "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n"; } + // Add traceback lualib if sourcemap traceback option is enabled + if (this.options.sourceMapTraceback) { + file.luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack); + } + const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require; if ( luaLibImport === LuaLibImportKind.Always || (luaLibImport === LuaLibImportKind.Require && file.luaLibFeatures.size > 0) ) { // Require lualib bundle - header += 'require("lualib_bundle");\n'; + header += 'require(__TS__Resolve("/lualib_bundle"));\n'; } else if (luaLibImport === LuaLibImportKind.Inline && file.luaLibFeatures.size > 0) { // Inline lualib features header += "-- Lua Library inline imports\n"; - header += loadLuaLibFeatures(file.luaLibFeatures, this.emitHost); + header += loadLuaLibFeatures(file.luaLibFeatures, this.host); } if (this.options.sourceMapTraceback) { header += "{#SourceMapTraceback}\n"; } - return this.concatNodes(header, ...this.printStatementArray(file.statements)); + // Hack: __TS__Resolve macro resolution destroys mappings of macro's node grand-parent + const headerNode = new SourceNode(null, null, null, new SourceNode(null, null, null, header)); + + return this.concatNodes(headerNode, ...this.printStatementArray(file.statements)); } protected pushIndent(): void { @@ -240,12 +191,12 @@ export class LuaPrinter { const { line, column } = lua.getOriginalPos(node); return line !== undefined && column !== undefined - ? new SourceNode(line + 1, column, this.sourceFile, chunks, name) - : new SourceNode(null, null, this.sourceFile, chunks, name); + ? new SourceNode(line + 1, column, this.fileName, chunks, name) + : new SourceNode(null, null, this.fileName, chunks, name); } protected concatNodes(...chunks: SourceChunk[]): SourceNode { - return new SourceNode(null, null, this.sourceFile, chunks); + return new SourceNode(null, null, this.fileName, chunks); } protected printBlock(block: lua.Block): SourceNode { @@ -731,7 +682,7 @@ export class LuaPrinter { } public printOperator(kind: lua.Operator): SourceNode { - return new SourceNode(null, null, this.sourceFile, LuaPrinter.operatorMap[kind]); + return new SourceNode(null, null, this.fileName, LuaPrinter.operatorMap[kind]); } protected joinChunksWithComma(chunks: SourceChunk[]): SourceChunk[] { @@ -756,67 +707,4 @@ export class LuaPrinter { return chunks; } - - // The key difference between this and SourceNode.toStringWithSourceMap() is that SourceNodes with null line/column - // will not generate 'empty' mappings in the source map that point to nothing in the original TS. - private buildSourceMap(sourceRoot: string, rootSourceNode: SourceNode): SourceMapGenerator { - const map = new SourceMapGenerator({ - file: trimExtension(this.sourceFile) + ".lua", - sourceRoot, - }); - - let generatedLine = 1; - let generatedColumn = 0; - let currentMapping: Mapping | undefined; - - const isNewMapping = (sourceNode: SourceNode) => { - if (sourceNode.line === null) { - return false; - } - if (currentMapping === undefined) { - return true; - } - if ( - currentMapping.generated.line === generatedLine && - currentMapping.generated.column === generatedColumn && - currentMapping.name === sourceNode.name - ) { - return false; - } - return ( - currentMapping.original.line !== sourceNode.line || - currentMapping.original.column !== sourceNode.column || - currentMapping.name !== sourceNode.name - ); - }; - - const build = (sourceNode: SourceNode) => { - if (isNewMapping(sourceNode)) { - currentMapping = { - source: sourceNode.source, - original: { line: sourceNode.line, column: sourceNode.column }, - generated: { line: generatedLine, column: generatedColumn }, - name: sourceNode.name, - }; - map.addMapping(currentMapping); - } - - for (const chunk of sourceNode.children) { - if (typeof chunk === "string") { - const lines = (chunk as string).split("\n"); - if (lines.length > 1) { - generatedLine += lines.length - 1; - generatedColumn = 0; - currentMapping = undefined; // Mappings end at newlines - } - generatedColumn += lines[lines.length - 1].length; - } else { - build(chunk); - } - } - }; - build(rootSourceNode); - - return map; - } } diff --git a/src/cli/execute.ts b/src/cli/execute.ts new file mode 100644 index 000000000..dbb9d12f6 --- /dev/null +++ b/src/cli/execute.ts @@ -0,0 +1,191 @@ +import * as ts from "typescript"; +import * as tstl from ".."; +import * as cliDiagnostics from "./diagnostics"; +import { getHelpString, versionString } from "./information"; +import { parseCommandLine } from "./parse"; +import { createDiagnosticReporter } from "./report"; +import { createConfigFileUpdater, locateConfigFile, parseConfigFileWithSystem } from "./tsconfig"; + +const shouldBePretty = ({ pretty }: ts.CompilerOptions = {}) => + pretty !== undefined ? (pretty as boolean) : ts.sys.writeOutputIsTTY?.() ?? false; + +let reportDiagnostic = createDiagnosticReporter(false); +function updateReportDiagnostic(options?: ts.CompilerOptions): void { + reportDiagnostic = createDiagnosticReporter(shouldBePretty(options)); +} + +function createWatchStatusReporter(options?: ts.CompilerOptions): ts.WatchStatusReporter { + return ts.createWatchStatusReporter(ts.sys, shouldBePretty(options)); +} + +export function executeCommandLine(args: string[]): void { + if (args.length > 0 && args[0].startsWith("-")) { + const firstOption = args[0].slice(args[0].startsWith("--") ? 2 : 1).toLowerCase(); + if (firstOption === "build" || firstOption === "b") { + return performBuild(args.slice(1)); + } + } + + const commandLine = parseCommandLine(args); + + if (commandLine.options.build) { + reportDiagnostic(cliDiagnostics.optionBuildMustBeFirstCommandLineArgument()); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + // TODO: ParsedCommandLine.errors isn't meant to contain warnings. Once root-level options + // support would be dropped it should be changed to `commandLine.errors.length > 0`. + if (commandLine.errors.some(e => e.category === ts.DiagnosticCategory.Error)) { + commandLine.errors.forEach(reportDiagnostic); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + if (commandLine.options.version) { + console.log(versionString); + return ts.sys.exit(ts.ExitStatus.Success); + } + + if (commandLine.options.help) { + console.log(versionString); + console.log(getHelpString()); + return ts.sys.exit(ts.ExitStatus.Success); + } + + const configFileName = locateConfigFile(commandLine); + if (typeof configFileName === "object") { + reportDiagnostic(configFileName); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + const commandLineOptions = commandLine.options; + if (configFileName) { + const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions); + + updateReportDiagnostic(configParseResult.options); + if (configParseResult.options.watch) { + createWatchOfConfigFile(configFileName, commandLineOptions); + } else { + performCompilation( + configParseResult.fileNames, + configParseResult.projectReferences, + configParseResult.options, + ts.getConfigFileParsingDiagnostics(configParseResult) + ); + } + } else { + updateReportDiagnostic(commandLineOptions); + if (commandLineOptions.watch) { + createWatchOfFilesAndCompilerOptions(commandLine.fileNames, commandLineOptions); + } else { + performCompilation(commandLine.fileNames, commandLine.projectReferences, commandLineOptions); + } + } +} + +function performBuild(_args: string[]): void { + console.log("Option '--build' is not supported."); + return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); +} + +function performCompilation( + rootNames: string[], + projectReferences: readonly ts.ProjectReference[] | undefined, + options: tstl.CompilerOptions, + configFileParsingDiagnostics?: readonly ts.Diagnostic[] +): void { + const program = ts.createProgram({ + rootNames, + options, + projectReferences, + configFileParsingDiagnostics, + }); + + const { diagnostics: emitDiagnostics, emitSkipped } = new tstl.Compiler().emit({ program }); + const diagnostics = ts.sortAndDeduplicateDiagnostics([...ts.getPreEmitDiagnostics(program), ...emitDiagnostics]); + + diagnostics.forEach(reportDiagnostic); + const exitCode = + diagnostics.length === 0 + ? ts.ExitStatus.Success + : emitSkipped + ? ts.ExitStatus.DiagnosticsPresent_OutputsSkipped + : ts.ExitStatus.DiagnosticsPresent_OutputsGenerated; + + return ts.sys.exit(exitCode); +} + +function createWatchOfConfigFile(configFileName: string, optionsToExtend: tstl.CompilerOptions): void { + const watchCompilerHost = ts.createWatchCompilerHost( + configFileName, + optionsToExtend, + ts.sys, + ts.createSemanticDiagnosticsBuilderProgram, + undefined, + createWatchStatusReporter(optionsToExtend) + ); + + updateWatchCompilationHost(watchCompilerHost, optionsToExtend); + ts.createWatchProgram(watchCompilerHost); +} + +function createWatchOfFilesAndCompilerOptions(rootFiles: string[], options: tstl.CompilerOptions): void { + const watchCompilerHost = ts.createWatchCompilerHost( + rootFiles, + options, + ts.sys, + ts.createSemanticDiagnosticsBuilderProgram, + undefined, + createWatchStatusReporter(options) + ); + + updateWatchCompilationHost(watchCompilerHost, options); + ts.createWatchProgram(watchCompilerHost); +} + +function updateWatchCompilationHost( + host: ts.WatchCompilerHost, + optionsToExtend: tstl.CompilerOptions +): void { + let hadErrorLastTime = true; + const updateConfigFile = createConfigFileUpdater(optionsToExtend); + + const compiler = new tstl.Compiler(); + host.afterProgramCreate = builderProgram => { + const program = builderProgram.getProgram(); + const options: tstl.CompilerOptions = builderProgram.getCompilerOptions(); + const configFileParsingDiagnostics: ts.Diagnostic[] = updateConfigFile(options); + + let sourceFiles: ts.SourceFile[] | undefined; + if (!tstl.isBundleEnabled(options) && !hadErrorLastTime) { + sourceFiles = []; + while (true) { + const currentFile = builderProgram.getSemanticDiagnosticsOfNextAffectedFile(); + if (!currentFile) break; + + if ("fileName" in currentFile.affected) { + sourceFiles.push(currentFile.affected); + } else { + sourceFiles.push(...currentFile.affected.getSourceFiles()); + } + } + } + + const { diagnostics: emitDiagnostics } = compiler.emit({ program, sourceFiles }); + + const diagnostics = ts.sortAndDeduplicateDiagnostics([ + ...configFileParsingDiagnostics, + ...program.getOptionsDiagnostics(), + ...program.getSyntacticDiagnostics(), + ...program.getGlobalDiagnostics(), + ...program.getSemanticDiagnostics(), + ...emitDiagnostics, + ]); + + diagnostics.forEach(reportDiagnostic); + + const errors = diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); + hadErrorLastTime = errors.length > 0; + + host.onWatchStatusChange!(cliDiagnostics.watchErrorSummary(errors.length), host.getNewLine(), options); + }; +} diff --git a/src/cli/parse.ts b/src/cli/parse.ts index fff0ef0d0..3bf4b27c9 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 { CompilerMode, CompilerOptions, LuaLibImportKind, LuaTarget } from "../CompilerOptions"; import * as cliDiagnostics from "./diagnostics"; export interface ParsedCommandLine extends ts.ParsedCommandLine { @@ -24,6 +24,13 @@ interface CommandLineOptionOfPrimitive extends CommandLineOptionBase { type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfPrimitive; export const optionDeclarations: CommandLineOption[] = [ + { + name: "mode", + description: + 'In "lib" mode, imported modules are emitted without being resolved. Visit https://typescripttolua.github.io/docs/TODO for more details.', + type: "enum", + choices: Object.values(CompilerMode), + }, { name: "luaBundle", description: "The name of the lua file to bundle output lua to. Requires luaBundleEntry.", diff --git a/src/compiler/chunk/assets.ts b/src/compiler/chunk/assets.ts new file mode 100644 index 000000000..a436df773 --- /dev/null +++ b/src/compiler/chunk/assets.ts @@ -0,0 +1,118 @@ +import * as path from "path"; +import { Mapping, SourceMapGenerator, SourceNode, StartOfSourceMap } from "source-map"; +import * as ts from "typescript"; +import { Chunk } from "."; +import { CompilerOptions } from "../../CompilerOptions"; + +export function chunkToAssets(chunk: Chunk, options: CompilerOptions) { + const writeByteOrderMark = options.emitBOM ?? false; + const sourceRoot = options.sourceRoot + ? // According to spec, sourceRoot is simply prepended to the source name, so the slash should be included + options.sourceRoot.replace(/[\\/]+$/, "") + "/" + : ""; + + const map = buildSourceMap(chunk.source, { file: path.basename(chunk.outputPath), sourceRoot }); + const sourceMap = options.sourceMap ? map.toString() : undefined; + + let code = chunk.source.toString(); + + if (options.inlineSourceMap) { + code += printInlineSourceMap(map); + } + + if (options.sourceMapTraceback) { + code = code.replace("{#SourceMapTraceback}", printStackTraceOverride(chunk.source)); + } + + const assets: ts.OutputFile[] = []; + assets.push({ name: chunk.outputPath, text: code, writeByteOrderMark }); + if (sourceMap !== undefined) { + assets.push({ name: chunk.outputPath + ".map", text: sourceMap, writeByteOrderMark }); + } + + return assets; +} + +function printInlineSourceMap(sourceMap: SourceMapGenerator): string { + const base64Map = Buffer.from(sourceMap.toString()).toString("base64"); + return `--# sourceMappingURL=data:application/json;base64,${base64Map}\n`; +} + +function printStackTraceOverride(rootNode: SourceNode): string { + let currentLine = 1; + const lineMap: Record = {}; + rootNode.walk((chunk, mappedPosition) => { + if (mappedPosition.line !== undefined && mappedPosition.line > 0) { + if (lineMap[currentLine] === undefined) { + lineMap[currentLine] = mappedPosition.line; + } else { + lineMap[currentLine] = Math.min(lineMap[currentLine], mappedPosition.line); + } + } + + currentLine += chunk.split("\n").length - 1; + }); + + const mappings = Object.entries(lineMap).map(([line, original]) => `["${line}"] = ${original}`); + return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, {${mappings.join(",")}});`; +} + +// The key difference between this and SourceNode.toStringWithSourceMap() is that SourceNodes with null line/column +// will not generate 'empty' mappings in the source map that point to nothing in the original TS. +function buildSourceMap(sourceNode: SourceNode, startOfSourceMap: StartOfSourceMap): SourceMapGenerator { + const map = new SourceMapGenerator(startOfSourceMap); + + let generatedLine = 1; + let generatedColumn = 0; + let currentMapping: Mapping | undefined; + + const isNewMapping = (sourceNode: SourceNode) => { + if (sourceNode.line === null) { + return false; + } + if (currentMapping === undefined) { + return true; + } + if ( + currentMapping.generated.line === generatedLine && + currentMapping.generated.column === generatedColumn && + currentMapping.name === sourceNode.name + ) { + return false; + } + return ( + currentMapping.original.line !== sourceNode.line || + currentMapping.original.column !== sourceNode.column || + currentMapping.name !== sourceNode.name + ); + }; + + const build = (sourceNode: SourceNode) => { + if (isNewMapping(sourceNode)) { + currentMapping = { + source: sourceNode.source, + original: { line: sourceNode.line, column: sourceNode.column }, + generated: { line: generatedLine, column: generatedColumn }, + name: sourceNode.name, + }; + map.addMapping(currentMapping); + } + + for (const chunk of sourceNode.children) { + if (typeof chunk === "string") { + const lines = (chunk as string).split("\n"); + if (lines.length > 1) { + generatedLine += lines.length - 1; + generatedColumn = 0; + currentMapping = undefined; // Mappings end at newlines + } + generatedColumn += lines[lines.length - 1].length; + } else { + build(chunk); + } + } + }; + build(sourceNode); + + return map; +} diff --git a/src/compiler/chunk/bundle.ts b/src/compiler/chunk/bundle.ts new file mode 100644 index 000000000..497b374e4 --- /dev/null +++ b/src/compiler/chunk/bundle.ts @@ -0,0 +1,71 @@ +import { SourceNode } from "source-map"; +import * as ts from "typescript"; +import { Chunk } from "."; +import { isBundleEnabled } from "../../CompilerOptions"; +import { escapeString } from "../../LuaPrinter"; +import { assert } from "../../utils"; +import { Compilation } from "../compilation"; +import { couldNotFindBundleEntryPoint } from "../diagnostics"; +import { Module } from "../module"; + +// Override `require` to read from ____modules table. +const requireOverride = ` +local ____modules = {} +local ____moduleCache = {} +local ____originalRequire = require +local function require(file) + if ____moduleCache[file] then + return ____moduleCache[file] + end + if ____modules[file] then + ____moduleCache[file] = ____modules[file]() + return ____moduleCache[file] + else + if ____originalRequire then + return ____originalRequire(file) + else + error("module '" .. file .. "' not found") + end + end +end +`; + +export function modulesToBundleChunks(compilation: Compilation, modules: Module[]): Chunk[] { + const { options } = compilation; + assert(isBundleEnabled(options)); + + const outputPath = ts.getNormalizedAbsolutePath(options.luaBundle, compilation.projectDir); + const entryFileName = ts.getNormalizedAbsolutePath(options.luaBundleEntry, compilation.projectDir); + + const entryModule = modules.find(m => m.fileName === entryFileName); + if (entryModule === undefined) { + compilation.diagnostics.push(couldNotFindBundleEntryPoint(options.luaBundleEntry)); + return [{ outputPath, source: new SourceNode() }]; + } + + // For each file: [""] = function() end, + const moduleTableEntries = modules.map(m => moduleSourceNode(m, compilation.getModuleId(m))); + + // Create ____modules table containing all entries from moduleTableEntries + const moduleTable = createModuleTableNode(moduleTableEntries); + + // return require("") + const bootstrap = `return require(${escapeString(compilation.getModuleId(entryModule))})\n`; + + const bundleNode = joinSourceChunks([requireOverride, moduleTable, bootstrap]); + const sourceFiles = modules.flatMap(x => x.sourceFiles ?? []); + return [{ outputPath, source: bundleNode, sourceFiles }]; +} + +function moduleSourceNode(module: Module, moduleId: string): SourceNode { + return joinSourceChunks([`[${escapeString(moduleId)}] = function()\n`, module.source, "\nend,\n"]); +} + +function createModuleTableNode(fileChunks: SourceChunk[]): SourceNode { + return joinSourceChunks(["____modules = {\n", ...fileChunks, "}\n"]); +} + +type SourceChunk = string | SourceNode; +function joinSourceChunks(chunks: SourceChunk[]): SourceNode { + return new SourceNode(null, null, null, chunks); +} diff --git a/src/compiler/chunk/index.ts b/src/compiler/chunk/index.ts new file mode 100644 index 000000000..d371a4deb --- /dev/null +++ b/src/compiler/chunk/index.ts @@ -0,0 +1,27 @@ +import * as path from "path"; +import { SourceNode } from "source-map"; +import * as ts from "typescript"; +import { normalizeSlashes } from "../../utils"; +import { Compilation } from "../compilation"; +import { Module } from "../module"; + +export * from "./assets"; +export * from "./bundle"; + +/** + * A chunk of Lua code to be emitted. + * Usually composed of one or multiple `Module` instances. + */ +export interface Chunk { + outputPath: string; + source: SourceNode; + sourceFiles?: ts.SourceFile[]; +} + +export function modulesToChunks(compilation: Compilation, modules: Module[]): Chunk[] { + return modules.map(module => { + const moduleId = compilation.getModuleId(module); + const outputPath = normalizeSlashes(path.resolve(compilation.outDir, `${moduleId.replace(/\./g, "/")}.lua`)); + return { outputPath, source: module.source, sourceFiles: module.sourceFiles }; + }); +} diff --git a/src/compiler/compilation.ts b/src/compiler/compilation.ts new file mode 100644 index 000000000..2de08af13 --- /dev/null +++ b/src/compiler/compilation.ts @@ -0,0 +1,172 @@ +import { Resolver, ResolverFactory } from "enhanced-resolve"; +import * as fs from "fs"; +import * as path from "path"; +import { SourceNode } from "source-map"; +import * as ts from "typescript"; +import { CompilerMode, CompilerOptions, isBundleEnabled, LuaTarget } from "../CompilerOptions"; +import { getLuaLibBundle } from "../LuaLib"; +import { assert, cast, isNonNull, mapAndFind, normalizeSlashes, trimExtension } from "../utils"; +import { Chunk, chunkToAssets, modulesToBundleChunks, modulesToChunks } from "./chunk"; +import { Compiler, CompilerHost } from "./compiler"; +import { createResolutionErrorDiagnostic } from "./diagnostics"; +import { buildModule, Module } from "./module"; +import { getPlugins, getUniquePluginProperty, Plugin } from "./plugins"; +import { isResolveError } from "./utils"; + +export class Compilation { + public readonly diagnostics: ts.Diagnostic[] = []; + public modules: Module[] = []; + + public host: CompilerHost; + public options: CompilerOptions = this.program.getCompilerOptions(); + public rootDir: string; + public outDir: string; + public projectDir: string; + + public plugins: Plugin[]; + protected tsResolver: Resolver; + protected luaResolver: Resolver; + + constructor(public compiler: Compiler, public program: ts.Program, extraPlugins: Plugin[]) { + this.host = compiler.host; + + this.rootDir = + // getCommonSourceDirectory ignores provided rootDir when TS6059 is emitted + this.options.rootDir == null + ? program.getCommonSourceDirectory() + : ts.getNormalizedAbsolutePath(this.options.rootDir, this.host.getCurrentDirectory()); + + this.outDir = this.options.outDir ?? this.rootDir; + + this.projectDir = + this.options.configFilePath !== undefined + ? ts.getDirectoryPath(this.options.configFilePath) + : this.host.getCurrentDirectory(); + + this.plugins = getPlugins(this, extraPlugins); + + const createResolver = (extensions: string[]) => + ResolverFactory.createResolver({ + extensions, + conditionNames: ["lua", `lua:${this.options.luaTarget ?? LuaTarget.Universal}`], + fileSystem: this.host.resolutionFileSystem ?? fs, + useSyncFileSystemCalls: true, + plugins: this.plugins.flatMap(p => p.getResolvePlugins?.(this) ?? []), + }); + + // We want to prioritize .ts files from current program, but .lua files otherwise + // TODO: It's not very efficient, maybe it could be solved with a plugin? + this.tsResolver = createResolver([".ts", ".tsx", ".js", ".jsx"]); + this.luaResolver = createResolver([".lua"]); + } + + public emit(writeFile: ts.WriteFileCallback) { + this.modules.forEach(module => this.compiler.addModuleToCache(module)); + this.modules.forEach(module => this.buildModule(module)); + + for (const chunk of this.mapModulesToChunks(this.modules)) { + for (const asset of chunkToAssets(chunk, this.options)) { + writeFile(asset.name, asset.text, asset.writeByteOrderMark, undefined, chunk.sourceFiles); + } + } + } + + private buildModule(module: Module) { + if (this.options.mode === CompilerMode.Lib) return; + + buildModule(module, (request, position) => { + const result = this.resolveRequestToModule(module.fileName, request); + if ("error" in result) { + const diagnostic = createResolutionErrorDiagnostic(result.error, module, position); + this.diagnostics.push(diagnostic); + return result; + } + + return this.getModuleId(result); + }); + } + + private resolveRequestToModule(issuer: string, request: string) { + if (request === "/lualib_bundle") { + let module = this.modules.find(m => m.fileName === request); + if (!module) { + const source = new SourceNode(null, null, null, getLuaLibBundle(this.host)); + module = { fileName: request, isBuilt: true, source }; + this.modules.push(module); + } + + return module; + } + + function resolveUsingResolver(resolver: Resolver) { + const result = resolver.resolveSync({}, ts.getDirectoryPath(issuer), request); + assert(typeof result === "string", `Invalid resolution result: ${result}`); + // https://github.com/webpack/enhanced-resolve#escaping + return normalizeSlashes(result.replace(/\0#/g, "#")); + } + + let resolvedPath: string | undefined; + let resolvedTsPath: string | undefined; + + try { + resolvedTsPath = resolveUsingResolver(this.tsResolver); + if (this.compiler.findModuleInCache(resolvedTsPath)) { + resolvedPath = resolvedTsPath; + } + } catch (error) { + if (!isResolveError(error)) throw error; + } + + if (resolvedPath === undefined) { + try { + resolvedPath = resolveUsingResolver(this.luaResolver); + } catch (error) { + if (!isResolveError(error)) throw error; + + if (resolvedTsPath !== undefined) { + return { error: `Resolved source file '${resolvedTsPath}' is not a part of the project.` }; + } else { + return { error: error.message }; + } + } + } + + let module = this.compiler.findModuleInCache(resolvedPath); + if (!module) { + // TODO: Load source map files + const code = cast(this.host.readFile(resolvedPath), isNonNull); + const source = new SourceNode(null, null, null, code); + module = { fileName: resolvedPath, isBuilt: false, source }; + + this.modules.push(module); + this.compiler.addModuleToCache(module); + this.buildModule(module); + } + + return module; + } + + public getModuleId(module: Module) { + const pluginResult = mapAndFind(this.plugins, p => p.getModuleId?.(module, this)); + if (pluginResult !== undefined) return pluginResult; + + if (module.fileName.startsWith("/")) { + return module.fileName.replace("/", ""); + } + + const result = path.relative(this.rootDir, trimExtension(module.fileName)); + // TODO: handle files on other drives + assert(!path.isAbsolute(result), `Invalid path: ${result}`); + return result + .replace(/\.\.[/\\]/g, "_/") + .replace(/\./g, "__") + .replace(/[/\\]/g, "."); + } + + private mapModulesToChunks(modules: Module[]): Chunk[] { + return ( + getUniquePluginProperty(this.plugins, "mapModulesToChunks")?.(modules, this) ?? + (isBundleEnabled(this.options) ? modulesToBundleChunks(this, modules) : modulesToChunks(this, modules)) + ); + } +} diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts new file mode 100644 index 000000000..9f0ec0c2a --- /dev/null +++ b/src/compiler/compiler.ts @@ -0,0 +1,62 @@ +import { FileSystem } from "enhanced-resolve"; +import * as ts from "typescript"; +import { Compilation } from "./compilation"; +import { Module } from "./module"; +import { Plugin } from "./plugins"; +import { emitProgramModules, TranspileOptions } from "./transpile"; + +export interface CompilerHost extends Pick { + resolutionFileSystem?: FileSystem; +} + +export interface EmitOptions extends TranspileOptions { + writeFile?: ts.WriteFileCallback; + plugins?: Plugin[]; +} + +export interface EmitResult { + emitSkipped: boolean; + diagnostics: readonly ts.Diagnostic[]; +} + +export class Compiler { + public host: CompilerHost; + constructor({ host = ts.sys }: { host?: CompilerHost } = {}) { + this.host = host; + } + + public emit(emitOptions: EmitOptions): EmitResult { + const { program, writeFile = this.host.writeFile } = emitOptions; + const compilation = new Compilation(this, program, emitOptions.plugins ?? []); + const { options } = compilation; + + // Clean-up module cache from modules created from deleted source files, so they won't be hit during resolution + const programSourceFiles = program.getSourceFiles(); + const validModulesInCache = [...this.moduleCache.values()].filter( + module => !module.sourceFiles?.some(sourceFile => !programSourceFiles.includes(sourceFile)) + ); + this.moduleCache.clear(); + validModulesInCache.forEach(m => this.addModuleToCache(m)); + + emitProgramModules(compilation, writeFile, emitOptions); + if (options.noEmit || (options.noEmitOnError && compilation.diagnostics.length > 0)) { + return { diagnostics: compilation.diagnostics, emitSkipped: true }; + } + + compilation.emit(writeFile); + + return { diagnostics: compilation.diagnostics, emitSkipped: false }; + } + + // A module cache that persists between compilations, so it can generate ids for these modules, + // without re-reading files every time + private moduleCache = new Map(); + + public addModuleToCache(module: Module) { + this.moduleCache.set(module.fileName, module); + } + + public findModuleInCache(fileName: string) { + return this.moduleCache.get(fileName); + } +} diff --git a/src/transpilation/diagnostics.ts b/src/compiler/diagnostics.ts similarity index 76% rename from src/transpilation/diagnostics.ts rename to src/compiler/diagnostics.ts index d0e609677..1cba59d1b 100644 --- a/src/transpilation/diagnostics.ts +++ b/src/compiler/diagnostics.ts @@ -1,5 +1,6 @@ import * as ts from "typescript"; import { createSerialDiagnosticFactory } from "../utils"; +import { Module } from "./module"; const createDiagnosticFactory = (getMessage: (...args: TArgs) => string) => createSerialDiagnosticFactory((...args: TArgs) => ({ messageText: getMessage(...args) })); @@ -37,3 +38,14 @@ export const usingLuaBundleWithInlineMightGenerateDuplicateCode = createSerialDi "Using 'luaBundle' with 'luaLibImport: \"inline\"' might generate duplicate code. " + "It is recommended to use 'luaLibImport: \"require\"'.", })); + +const endOfFileToken = ts.factory.createToken(ts.SyntaxKind.EndOfFileToken); +export const createResolutionErrorDiagnostic = createSerialDiagnosticFactory( + (messageText: string, module: Module, position: ts.ReadonlyTextRange) => { + const file = ts.factory.createSourceFile([], endOfFileToken, ts.NodeFlags.None); + file.fileName = module.fileName; + file.text = module.source.toString(); + + return { messageText, file, start: position.pos, length: position.end - position.pos }; + } +); diff --git a/src/compiler/index.ts b/src/compiler/index.ts new file mode 100644 index 000000000..3e88465cc --- /dev/null +++ b/src/compiler/index.ts @@ -0,0 +1,6 @@ +export { Chunk } from "./chunk"; +export * from "./compilation"; +export * from "./compiler"; +export { Module } from "./module"; +export { Plugin } from "./plugins"; +export * from "./transpile"; diff --git a/src/compiler/module.ts b/src/compiler/module.ts new file mode 100644 index 000000000..b576b90f6 --- /dev/null +++ b/src/compiler/module.ts @@ -0,0 +1,60 @@ +import { SourceNode } from "source-map"; +import * as ts from "typescript"; +import { escapeString, unescapeLuaString } from "../LuaPrinter"; + +/** + * Source code of a single input Lua file. + * May be constructed from transpiled `.ts` source files, or from real `.lua` files. + */ +export interface Module { + fileName: string; + isBuilt: boolean; + source: SourceNode; + sourceFiles?: ts.SourceFile[]; +} + +export type ModuleDependencyResolver = (request: string, position: ts.ReadonlyTextRange) => string | { error: string }; + +export function buildModule(module: Module, dependencyResolver: ModuleDependencyResolver) { + if (module.isBuilt) return; + module.isBuilt = true; + replaceResolveRequests(module.source, dependencyResolver); +} + +function replaceResolveRequests(rootNode: SourceNode, dependencyResolver: ModuleDependencyResolver) { + let currentPosition = 0; + + function replaceInString(source: string) { + const matches = source.matchAll(/__TS__Resolve\((".*?")\)/g); + for (const match of [...matches].reverse()) { + const request = unescapeLuaString(match[1]); + const pos = currentPosition + match.index!; + const end = pos + match[0].length; + const result = dependencyResolver(request, { pos, end }); + const replacement = + typeof result === "string" + ? escapeString(result) + : `--[[ ${request} ]] error(${escapeString(result.error)})`; + + source = source.slice(0, match.index) + replacement + source.slice(end); + } + + return source; + } + + function walkSourceNode(node: SourceNode, parent: SourceNode) { + for (const child of node.children as Array) { + if (typeof child === "object") { + walkSourceNode(child, node); + } else { + if (child.includes("__TS__Resolve")) { + parent.children = [replaceInString(parent.toString()) as any]; + } + + currentPosition += child.length; + } + } + } + + walkSourceNode(rootNode, rootNode); +} diff --git a/src/compiler/plugins.ts b/src/compiler/plugins.ts new file mode 100644 index 000000000..4052182b3 --- /dev/null +++ b/src/compiler/plugins.ts @@ -0,0 +1,73 @@ +import { Plugin as ResolvePlugin } from "enhanced-resolve"; +import { Printer } from "../LuaPrinter"; +import { Visitors } from "../transformation/context"; +import { Chunk } from "./chunk"; +import { Compilation } from "./compilation"; +import { Module } from "./module"; +import { loadConfigImport } from "./utils"; + +export interface Plugin { + /** + * An augmentation to the map of visitors that transform TypeScript AST to Lua AST. + * + * Key is a `SyntaxKind` of a processed node. + */ + visitors?: Visitors; + + /** + * A function that converts Lua AST to a string. + * + * At most one custom printer can be provided across all plugins. + */ + printer?: Printer; + + /** + * Provide extra [enhanced-resolve](https://github.com/webpack/enhanced-resolve) plugins, + * used for `.lua` module resolution. + */ + getResolvePlugins?(compilation: Compilation): ResolvePlugin[]; + + /** + * Transform modules into chunks. + */ + mapModulesToChunks?(modules: Module[], compilation: Compilation): Chunk[]; + + /** + * Produce a unique identifier for a module, which would be used as `require` call parameter, + * and may be used for chunk naming. + */ + getModuleId?(module: Module, compilation: Compilation): string | undefined; +} + +export function getPlugins(compilation: Compilation, customPlugins: Plugin[]): Plugin[] { + const pluginsFromOptions: Plugin[] = []; + + for (const [index, pluginOption] of (compilation.options.luaPlugins ?? []).entries()) { + const optionName = `tstl.luaPlugins[${index}]`; + + const { error: resolveError, result: factory } = loadConfigImport( + "plugin", + `${optionName}.name`, + compilation.projectDir, + pluginOption.name, + pluginOption.import + ); + + if (resolveError) compilation.diagnostics.push(resolveError); + if (factory === undefined) continue; + + const plugin = typeof factory === "function" ? factory(pluginOption) : factory; + pluginsFromOptions.push(plugin); + } + + return [...customPlugins, ...pluginsFromOptions]; +} + +export function getUniquePluginProperty

(plugins: Plugin[], property: P): Plugin[P] | undefined { + const applicablePlugins = plugins.filter(p => p[property] !== undefined); + if (applicablePlugins.length === 1) { + return applicablePlugins[0][property]; + } else if (applicablePlugins.length > 1) { + throw new Error(`Only one plugin can specify '${property}'`); + } +} diff --git a/src/transpilation/transpile.ts b/src/compiler/transpile/index.ts similarity index 52% rename from src/transpilation/transpile.ts rename to src/compiler/transpile/index.ts index cbc77c13c..5440edfcf 100644 --- a/src/transpilation/transpile.ts +++ b/src/compiler/transpile/index.ts @@ -1,41 +1,28 @@ -import * as path from "path"; import * as ts from "typescript"; -import { CompilerOptions, validateOptions } from "../CompilerOptions"; -import { createPrinter } from "../LuaPrinter"; -import { createVisitorMap, transformSourceFile } from "../transformation"; -import { isNonNull } from "../utils"; -import { getPlugins, Plugin } from "./plugins"; +import { validateOptions } from "../../CompilerOptions"; +import { LuaPrinter } from "../../LuaPrinter"; +import { createVisitorMap, transformSourceFile } from "../../transformation/transform"; +import { isNonNull } from "../../utils"; +import { Compilation } from "../compilation"; +import { getUniquePluginProperty } from "../plugins"; import { getTransformers } from "./transformers"; -import { EmitHost, ProcessedFile } from "./utils"; export interface TranspileOptions { program: ts.Program; sourceFiles?: ts.SourceFile[]; customTransformers?: ts.CustomTransformers; - plugins?: Plugin[]; } -export interface TranspileResult { - diagnostics: ts.Diagnostic[]; - transpiledFiles: ProcessedFile[]; -} - -export function getProgramTranspileResult( - emitHost: EmitHost, +export function emitProgramModules( + compilation: Compilation, writeFileResult: ts.WriteFileCallback, - { program, sourceFiles: targetSourceFiles, customTransformers = {}, plugins: customPlugins = [] }: TranspileOptions -): TranspileResult { - const options = program.getCompilerOptions() as CompilerOptions; - - const diagnostics = validateOptions(options); - let transpiledFiles: ProcessedFile[] = []; + { program, sourceFiles: targetSourceFiles, customTransformers = {} }: TranspileOptions +) { + const { options } = compilation; + compilation.diagnostics.push(...validateOptions(options)); if (options.noEmitOnError) { - const preEmitDiagnostics = [ - ...diagnostics, - ...program.getOptionsDiagnostics(), - ...program.getGlobalDiagnostics(), - ]; + const preEmitDiagnostics = [...program.getOptionsDiagnostics(), ...program.getGlobalDiagnostics()]; if (targetSourceFiles) { for (const sourceFile of targetSourceFiles) { @@ -52,26 +39,28 @@ export function getProgramTranspileResult( } if (preEmitDiagnostics.length > 0) { - return { diagnostics: preEmitDiagnostics, transpiledFiles }; + compilation.diagnostics.push(...preEmitDiagnostics); + return; } } - const plugins = getPlugins(program, diagnostics, customPlugins); - const visitorMap = createVisitorMap(plugins.map(p => p.visitors).filter(isNonNull)); - const printer = createPrinter(plugins.map(p => p.printer).filter(isNonNull)); + const visitorMap = createVisitorMap(compilation.plugins.map(p => p.visitors).filter(isNonNull)); + const printer = + getUniquePluginProperty(compilation.plugins, "printer") ?? + ((program, host, fileName, file) => new LuaPrinter(host, program, fileName).print(file)); + const processSourceFile = (sourceFile: ts.SourceFile) => { const { file, diagnostics: transformDiagnostics } = transformSourceFile(program, sourceFile, visitorMap); - diagnostics.push(...transformDiagnostics); + compilation.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 }); + const fileName = ts.getNormalizedAbsolutePath(sourceFile.fileName, compilation.projectDir); + const source = printer(program, compilation.host, sourceFile.fileName, file); + compilation.modules.push({ fileName, isBuilt: false, source, sourceFiles: [sourceFile] }); } }; - const transformers = getTransformers(program, diagnostics, customTransformers, processSourceFile); + const transformers = getTransformers(compilation, customTransformers, processSourceFile); const isEmittableJsonFile = (sourceFile: ts.SourceFile) => sourceFile.flags & ts.NodeFlags.JsonFile && @@ -93,21 +82,17 @@ export function getProgramTranspileResult( if (isEmittableJsonFile(file)) { processSourceFile(file); } else { - diagnostics.push(...program.emit(file, writeFile, undefined, false, transformers).diagnostics); + const { diagnostics } = program.emit(file, writeFile, undefined, false, transformers); + compilation.diagnostics.push(...diagnostics); } } } else { - diagnostics.push(...program.emit(undefined, writeFile, undefined, false, transformers).diagnostics); + const { diagnostics } = program.emit(undefined, writeFile, undefined, false, transformers); + compilation.diagnostics.push(...diagnostics); // JSON files don't get through transformers and aren't written when outDir is the same as rootDir program.getSourceFiles().filter(isEmittableJsonFile).forEach(processSourceFile); } options.noEmit = oldNoEmit; - - if (options.noEmit || (options.noEmitOnError && diagnostics.length > 0)) { - transpiledFiles = []; - } - - return { diagnostics, transpiledFiles }; } diff --git a/src/transpilation/transformers.ts b/src/compiler/transpile/transformers.ts similarity index 85% rename from src/transpilation/transformers.ts rename to src/compiler/transpile/transformers.ts index 470e7ad13..f71a26e55 100644 --- a/src/transpilation/transformers.ts +++ b/src/compiler/transpile/transformers.ts @@ -1,13 +1,13 @@ import * as ts from "typescript"; // TODO: Don't depend on CLI? -import * as cliDiagnostics from "../cli/diagnostics"; -import { CompilerOptions, TransformerImport } from "../CompilerOptions"; -import * as diagnosticFactories from "./diagnostics"; -import { getConfigDirectory, resolvePlugin } from "./utils"; +import * as cliDiagnostics from "../../cli/diagnostics"; +import { CompilerOptions, TransformerImport } from "../../CompilerOptions"; +import { Compilation } from "../compilation"; +import * as diagnosticFactories from "../diagnostics"; +import { loadConfigImport } from "../utils"; export function getTransformers( - program: ts.Program, - diagnostics: ts.Diagnostic[], + compilation: Compilation, customTransformers: ts.CustomTransformers, onSourceFile: (sourceFile: ts.SourceFile) => void ): ts.CustomTransformers { @@ -16,15 +16,14 @@ export function getTransformers( return ts.createSourceFile(sourceFile.fileName, "", ts.ScriptTarget.ESNext); }; - const transformersFromOptions = loadTransformersFromOptions(program, diagnostics); + const transformersFromOptions = loadTransformersFromOptions(compilation); const afterDeclarations = [ ...(transformersFromOptions.afterDeclarations ?? []), ...(customTransformers.afterDeclarations ?? []), ]; - const options = program.getCompilerOptions() as CompilerOptions; - if (options.noImplicitSelf) { + if (compilation.options.noImplicitSelf) { afterDeclarations.unshift(noImplicitSelfTransformer); } @@ -53,33 +52,37 @@ export const noImplicitSelfTransformer: ts.TransformerFactory = { before: [], after: [], afterDeclarations: [], }; - const options = program.getCompilerOptions() as CompilerOptions; - if (!options.plugins) return customTransformers; + if (!compilation.options.plugins) return customTransformers; - for (const [index, transformerImport] of options.plugins.entries()) { + for (const [index, transformerImport] of compilation.options.plugins.entries()) { if (!("transform" in transformerImport)) continue; const optionName = `compilerOptions.plugins[${index}]`; - const { error: resolveError, result: factory } = resolvePlugin( + const { error: resolveError, result: factory } = loadConfigImport( "transformer", `${optionName}.transform`, - getConfigDirectory(options), + compilation.projectDir, transformerImport.transform, transformerImport.import ); - if (resolveError) diagnostics.push(resolveError); + if (resolveError) compilation.diagnostics.push(resolveError); if (factory === undefined) continue; - const { error: loadError, transformer } = loadTransformer(optionName, program, factory, transformerImport); - if (loadError) diagnostics.push(loadError); + const { error: loadError, transformer } = loadTransformer( + optionName, + compilation.program, + factory, + transformerImport + ); + if (loadError) compilation.diagnostics.push(loadError); if (transformer === undefined) continue; if (transformer.before) { diff --git a/src/transpilation/utils.ts b/src/compiler/utils.ts similarity index 53% rename from src/transpilation/utils.ts rename to src/compiler/utils.ts index 099dfd400..db9229666 100644 --- a/src/transpilation/utils.ts +++ b/src/compiler/utils.ts @@ -1,39 +1,17 @@ -import * as path from "path"; -import * as resolve from "resolve"; -import { SourceNode } from "source-map"; +import { create as createResolve } from "enhanced-resolve"; import * as ts from "typescript"; // TODO: Don't depend on CLI? import * as cliDiagnostics from "../cli/diagnostics"; -import * as lua from "../LuaAST"; +import { assert } from "../utils"; import * as diagnosticFactories from "./diagnostics"; -export interface EmitHost { - getCurrentDirectory(): string; - readFile(path: string): string | undefined; - writeFile: ts.WriteFileCallback; -} - -interface BaseFile { - code: string; - sourceMap?: string; - sourceFiles?: ts.SourceFile[]; -} - -export interface ProcessedFile extends BaseFile { - fileName: string; - luaAst?: lua.File; - /** @internal */ - sourceMapNode?: SourceNode; -} - -export interface EmitFile extends BaseFile { - outputPath: string; -} +// https://github.com/webpack/enhanced-resolve/blob/0001f80dacf033ac4a0e690b2766e0965c458266/lib/Resolver.js#L280-L288 +export const isResolveError = (error: unknown): error is Error & { details: string } => + error instanceof Error && "details" in error; -export const getConfigDirectory = (options: ts.CompilerOptions) => - options.configFilePath ? path.dirname(options.configFilePath) : process.cwd(); +const resolveConfigImport = createResolve.sync({ extensions: [".js", ".ts", ".tsx"] }); -export function resolvePlugin( +export function loadConfigImport( kind: string, optionName: string, basedir: string, @@ -46,18 +24,18 @@ export function resolvePlugin( let resolved: string; try { - resolved = resolve.sync(query, { basedir, extensions: [".js", ".ts", ".tsx"] }); - } catch (err) { - if (err.code !== "MODULE_NOT_FOUND") throw err; + const result = resolveConfigImport({}, basedir, query); + assert(typeof result === "string"); + resolved = result; + } catch (error) { + if (!isResolveError(error)) throw error; return { error: diagnosticFactories.couldNotResolveFrom(kind, query, basedir) }; } const hasNoRequireHook = require.extensions[".ts"] === undefined; if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) { try { - const tsNodePath = resolve.sync("ts-node", { basedir }); - const tsNode: typeof import("ts-node") = require(tsNodePath); - tsNode.register({ transpileOnly: true }); + require("ts-node/register/transpile-only"); } catch (err) { if (err.code !== "MODULE_NOT_FOUND") throw err; return { error: diagnosticFactories.toLoadItShouldBeTranspiled(kind, query) }; diff --git a/src/index.ts b/src/index.ts index 3d060a994..0566dfaf9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,10 @@ export { version } from "./cli/information"; export { parseCommandLine, ParsedCommandLine, updateParsedConfigFile } from "./cli/parse"; export * from "./cli/report"; export { parseConfigFileWithSystem } from "./cli/tsconfig"; +export * from "./compiler"; export * from "./CompilerOptions"; export * from "./LuaAST"; export { LuaLibFeature } from "./LuaLib"; export * from "./LuaPrinter"; +export * from "./managed-api"; export * from "./transformation/context"; -export * from "./transpilation"; diff --git a/src/managed-api/index.ts b/src/managed-api/index.ts new file mode 100644 index 000000000..fea25729c --- /dev/null +++ b/src/managed-api/index.ts @@ -0,0 +1,63 @@ +import * as ts from "typescript"; +import { parseConfigFileWithSystem } from "../cli/tsconfig"; +import { Compiler, EmitResult } from "../compiler"; +import { CompilerOptions } from "../CompilerOptions"; +import { assert } from "../utils"; +import { createEmitOutputCollector, createVirtualProgram, TranspiledFile } from "./utils"; + +export { TranspiledFile }; + +export function transpileFiles( + rootNames: string[], + options: CompilerOptions = {}, + writeFile?: ts.WriteFileCallback +): EmitResult { + const program = ts.createProgram(rootNames, options); + + const { diagnostics: emitDiagnostics, emitSkipped } = new Compiler().emit({ program, writeFile }); + const diagnostics = ts.sortAndDeduplicateDiagnostics([...ts.getPreEmitDiagnostics(program), ...emitDiagnostics]); + + return { diagnostics: [...diagnostics], emitSkipped }; +} + +export function transpileProject( + configFileName: string, + optionsToExtend?: CompilerOptions, + writeFile?: ts.WriteFileCallback +): EmitResult { + const parseResult = parseConfigFileWithSystem(configFileName, optionsToExtend); + if (parseResult.errors.length > 0) { + return { diagnostics: parseResult.errors, emitSkipped: true }; + } + + return transpileFiles(parseResult.fileNames, parseResult.options, writeFile); +} + +export interface TranspileVirtualProjectResult { + diagnostics: ts.Diagnostic[]; + transpiledFiles: TranspiledFile[]; +} + +export function transpileVirtualProject( + files: Record, + options: CompilerOptions = {} +): TranspileVirtualProjectResult { + const program = createVirtualProgram(files, options); + const collector = createEmitOutputCollector(); + + const { diagnostics: emitDiagnostics } = new Compiler().emit({ program, writeFile: collector.writeFile }); + const diagnostics = ts.sortAndDeduplicateDiagnostics([...ts.getPreEmitDiagnostics(program), ...emitDiagnostics]); + + return { diagnostics: [...diagnostics], transpiledFiles: collector.files }; +} + +export interface TranspileStringResult extends TranspiledFile { + diagnostics: ts.Diagnostic[]; +} + +export function transpileString(main: string, options: CompilerOptions = {}): TranspileStringResult { + const { diagnostics, transpiledFiles } = transpileVirtualProject({ "main.ts": main }, options); + const file = transpiledFiles.find(({ sourceFiles }) => sourceFiles.some(f => f.fileName === "main.ts")); + assert(file !== undefined); + return { ...file, diagnostics }; +} diff --git a/src/managed-api/utils.ts b/src/managed-api/utils.ts new file mode 100644 index 000000000..90fa50f70 --- /dev/null +++ b/src/managed-api/utils.ts @@ -0,0 +1,88 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as ts from "typescript"; +import { CompilerOptions } from "../CompilerOptions"; +import { intersection, union } from "../utils"; + +const libCache = new Map(); +export function createVirtualProgram(input: Record, options: CompilerOptions = {}): ts.Program { + function notImplemented(): never { + throw new Error("Not implemented"); + } + + const getFileFromInput = (fileName: string) => + input[fileName] ?? (fileName.startsWith("/") ? input[fileName.slice(1)] : undefined); + + const compilerHost: ts.CompilerHost = { + useCaseSensitiveFileNames: () => false, + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: () => "/", + fileExists: fileName => fileName.startsWith("lib.") || getFileFromInput(fileName) !== undefined, + readFile: notImplemented, + writeFile: notImplemented, + getDefaultLibFileName: ts.getDefaultLibFileName, + getNewLine: () => "\n", + + getSourceFile(fileName) { + const fileFromInput = getFileFromInput(fileName); + if (fileFromInput !== undefined) { + return ts.createSourceFile(fileName, fileFromInput, ts.ScriptTarget.Latest, false); + } + + if (libCache.has(fileName)) return libCache.get(fileName)!; + + if (fileName.startsWith("lib.")) { + const typeScriptDir = path.dirname(require.resolve("typescript")); + const filePath = path.join(typeScriptDir, fileName); + const content = fs.readFileSync(filePath, "utf8"); + + const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, false); + libCache.set(fileName, sourceFile); + return sourceFile; + } + }, + }; + + return ts.createProgram(Object.keys(input), options, compilerHost); +} + +export interface TranspiledFile { + sourceFiles: ts.SourceFile[]; + lua?: string; + luaSourceMap?: string; + declaration?: string; + declarationMap?: string; + /** @internal */ + js?: string; + /** @internal */ + jsSourceMap?: string; +} + +export function createEmitOutputCollector() { + const files: TranspiledFile[] = []; + 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] }; + files.push(file); + } else { + file.sourceFiles = union(file.sourceFiles, sourceFiles); + } + + if (fileName.endsWith(".lua")) { + file.lua = data; + } else if (fileName.endsWith(".lua.map")) { + file.luaSourceMap = data; + } else if (fileName.endsWith(".js")) { + file.js = data; + } else if (fileName.endsWith(".js.map")) { + file.jsSourceMap = data; + } else if (fileName.endsWith(".d.ts")) { + file.declaration = data; + } else if (fileName.endsWith(".d.ts.map")) { + file.declarationMap = data; + } + }; + + return { writeFile, files }; +} diff --git a/src/transformation/index.ts b/src/transformation/transform.ts similarity index 99% rename from src/transformation/index.ts rename to src/transformation/transform.ts index 5b6fca647..58f500e36 100644 --- a/src/transformation/index.ts +++ b/src/transformation/transform.ts @@ -30,6 +30,5 @@ export function createVisitorMap(customVisitors: Visitors[]): VisitorMap { export function transformSourceFile(program: ts.Program, sourceFile: ts.SourceFile, visitorMap: VisitorMap) { const context = new TransformationContext(program, sourceFile, visitorMap); const [file] = context.transformNode(sourceFile) as [lua.File]; - return { file, diagnostics: context.diagnostics }; } diff --git a/src/transformation/utils/diagnostics.ts b/src/transformation/utils/diagnostics.ts index a5bdb35db..5e3f78361 100644 --- a/src/transformation/utils/diagnostics.ts +++ b/src/transformation/utils/diagnostics.ts @@ -119,10 +119,6 @@ export const invalidAmbientIdentifierName = createDiagnosticFactory( (text: string) => `Invalid ambient identifier name '${text}'. Ambient identifiers must be valid lua identifiers.` ); -export const unresolvableRequirePath = createDiagnosticFactory( - (path: string) => `Cannot create require path. Module '${path}' does not exist within --rootDir.` -); - export const unsupportedVarDeclaration = createDiagnosticFactory( "`var` declarations are not supported. Use `let` or `const` instead." ); diff --git a/src/transformation/utils/function-context.ts b/src/transformation/utils/function-context.ts index 8bf05255c..d2a603139 100644 --- a/src/transformation/utils/function-context.ts +++ b/src/transformation/utils/function-context.ts @@ -1,5 +1,4 @@ import * as ts from "typescript"; -import { CompilerOptions } from "../../CompilerOptions"; import { TransformationContext } from "../context"; import { AnnotationKind, getFileAnnotations, getNodeAnnotations } from "./annotations"; import { findFirstNodeAbove, getAllCallSignatures, inferAssignedType } from "./typescript"; @@ -35,7 +34,7 @@ function getExplicitThisParameter(signatureDeclaration: ts.SignatureDeclaration) } export function getDeclarationContextType( - { program }: TransformationContext, + { program, options }: TransformationContext, signatureDeclaration: ts.SignatureDeclaration ): ContextType { const thisParameter = getExplicitThisParameter(signatureDeclaration); @@ -78,7 +77,6 @@ export function getDeclarationContextType( } // When using --noImplicitSelf and the signature is defined in a file targeted by the program apply the @noSelf rule. - const options = program.getCompilerOptions() as CompilerOptions; if (options.noImplicitSelf && program.getRootFileNames().includes(signatureDeclaration.getSourceFile().fileName)) { return ContextType.Void; } diff --git a/src/transformation/visitors/modules/import.ts b/src/transformation/visitors/modules/import.ts index 34fa216d6..07ceda820 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, getTypeAnnotations } from "../../utils/annotations"; import { createDefaultExportStringLiteral } from "../../utils/export"; @@ -10,29 +9,6 @@ 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 { const moduleOwnerSymbol = context.checker.getSymbolAtLocation(moduleSpecifier); @@ -49,11 +25,12 @@ 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) - : moduleSpecifier.text; - - params.push(lua.createStringLiteral(modulePath)); + const module = lua.createStringLiteral(moduleSpecifier.text); + params.push( + shouldResolveModulePath(context, moduleSpecifier) + ? lua.createCallExpression(lua.createIdentifier("__TS__Resolve"), [module]) + : module + ); } return lua.createCallExpression(lua.createIdentifier("require"), params, tsOriginal); @@ -110,9 +87,7 @@ export const transformImportDeclaration: FunctionVisitor = } } - const importPath = ts.isStringLiteral(statement.moduleSpecifier) - ? statement.moduleSpecifier.text.replace(/"/g, "") - : "module"; + const importPath = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "module"; // Create the require statement to extract values. // local ____module = require("module") diff --git a/src/transformation/visitors/namespace.ts b/src/transformation/visitors/namespace.ts index 35190705f..7ce953a44 100644 --- a/src/transformation/visitors/namespace.ts +++ b/src/transformation/visitors/namespace.ts @@ -113,7 +113,7 @@ export const transformModuleDeclaration: FunctionVisitor = } // Set current namespace for nested NS - // Keep previous namespace to reset after block transpilation + // Keep previous namespace to reset after block transformation currentNamespaces.set(context, node); // Transform moduleblock to block and visit it diff --git a/src/transpilation/bundle.ts b/src/transpilation/bundle.ts deleted file mode 100644 index c696c6900..000000000 --- a/src/transpilation/bundle.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as path from "path"; -import { SourceNode } from "source-map"; -import * as ts from "typescript"; -import { CompilerOptions } from "../CompilerOptions"; -import { escapeString } from "../LuaPrinter"; -import { cast, formatPathToLuaPath, isNonNull, normalizeSlashes, trimExtension } from "../utils"; -import { couldNotFindBundleEntryPoint } from "./diagnostics"; -import { EmitFile, EmitHost, ProcessedFile } from "./utils"; - -const createModulePath = (baseDir: string, pathToResolve: string) => - escapeString(formatPathToLuaPath(trimExtension(path.relative(baseDir, pathToResolve)))); - -// Override `require` to read from ____modules table. -const requireOverride = ` -local ____modules = {} -local ____moduleCache = {} -local ____originalRequire = require -local function require(file) - if ____moduleCache[file] then - return ____moduleCache[file] - end - if ____modules[file] then - ____moduleCache[file] = ____modules[file]() - return ____moduleCache[file] - else - if ____originalRequire then - return ____originalRequire(file) - else - error("module '" .. file .. "' not found") - end - end -end -`; - -export function getBundleResult( - program: ts.Program, - emitHost: EmitHost, - 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)); - - if (!files.some(f => f.fileName === resolvedEntryModule)) { - diagnostics.push(couldNotFindBundleEntryPoint(entryModule)); - return [diagnostics, { outputPath, code: "" }]; - } - - // For each file: [""] = function() end, - const moduleTableEntries = files.map(f => moduleSourceNode(f, createModulePath(outDir, f.fileName))); - - // Create ____modules table containing all entries from moduleTableEntries - const moduleTable = createModuleTableNode(moduleTableEntries); - - // return require("") - const entryPoint = `return require(${createModulePath(outDir, resolvedEntryModule)})\n`; - - const bundleNode = joinSourceChunks([requireOverride, moduleTable, entryPoint]); - const { code, map } = bundleNode.toStringWithSourceMap(); - - return [ - diagnostics, - { - outputPath, - code, - sourceMap: map.toString(), - sourceFiles: files.flatMap(x => x.sourceFiles ?? []), - }, - ]; -} - -function moduleSourceNode({ code, sourceMapNode }: ProcessedFile, modulePath: string): SourceNode { - const tableEntryHead = `[${modulePath}] = function() `; - const tableEntryTail = "end,\n"; - - return joinSourceChunks([tableEntryHead, sourceMapNode ?? code, tableEntryTail]); -} - -function createModuleTableNode(fileChunks: SourceChunk[]): SourceNode { - const tableHead = "____modules = {\n"; - const tableEnd = "}\n"; - - return joinSourceChunks([tableHead, ...fileChunks, tableEnd]); -} - -type SourceChunk = string | SourceNode; -function joinSourceChunks(chunks: SourceChunk[]): SourceNode { - return new SourceNode(null, null, null, chunks); -} diff --git a/src/transpilation/index.ts b/src/transpilation/index.ts deleted file mode 100644 index 628246ec9..000000000 --- a/src/transpilation/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; -import { parseConfigFileWithSystem } from "../cli/tsconfig"; -import { CompilerOptions } from "../CompilerOptions"; -import { createEmitOutputCollector, TranspiledFile } from "./output-collector"; -import { EmitResult, Transpiler } from "./transpiler"; - -export { Plugin } from "./plugins"; -export * from "./transpile"; -export * from "./transpiler"; -export { EmitHost } from "./utils"; -export { TranspiledFile }; - -export function transpileFiles( - rootNames: string[], - options: CompilerOptions = {}, - writeFile?: ts.WriteFileCallback -): EmitResult { - const program = ts.createProgram(rootNames, options); - const { diagnostics: transpileDiagnostics, emitSkipped } = new Transpiler().emit({ program, writeFile }); - const diagnostics = ts.sortAndDeduplicateDiagnostics([ - ...ts.getPreEmitDiagnostics(program), - ...transpileDiagnostics, - ]); - - return { diagnostics: [...diagnostics], emitSkipped }; -} - -export function transpileProject( - configFileName: string, - optionsToExtend?: CompilerOptions, - writeFile?: ts.WriteFileCallback -): EmitResult { - const parseResult = parseConfigFileWithSystem(configFileName, optionsToExtend); - if (parseResult.errors.length > 0) { - return { diagnostics: parseResult.errors, emitSkipped: true }; - } - - return transpileFiles(parseResult.fileNames, parseResult.options, writeFile); -} - -const libCache: { [key: string]: ts.SourceFile } = {}; - -/** @internal */ -export function createVirtualProgram(input: Record, options: CompilerOptions = {}): ts.Program { - const compilerHost: ts.CompilerHost = { - fileExists: () => true, - getCanonicalFileName: fileName => fileName, - getCurrentDirectory: () => "", - getDefaultLibFileName: ts.getDefaultLibFileName, - readFile: () => "", - getNewLine: () => "\n", - useCaseSensitiveFileNames: () => false, - writeFile() {}, - - getSourceFile(fileName) { - if (fileName in input) { - return ts.createSourceFile(fileName, input[fileName], ts.ScriptTarget.Latest, false); - } - - if (fileName.startsWith("lib.")) { - if (libCache[fileName]) return libCache[fileName]; - const typeScriptDir = path.dirname(require.resolve("typescript")); - const filePath = path.join(typeScriptDir, fileName); - const content = fs.readFileSync(filePath, "utf8"); - - libCache[fileName] = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, false); - - return libCache[fileName]; - } - }, - }; - - return ts.createProgram(Object.keys(input), options, compilerHost); -} - -export interface TranspileVirtualProjectResult { - diagnostics: ts.Diagnostic[]; - transpiledFiles: TranspiledFile[]; -} - -export function transpileVirtualProject( - files: Record, - options: CompilerOptions = {} -): TranspileVirtualProjectResult { - const program = createVirtualProgram(files, options); - const collector = createEmitOutputCollector(); - const { diagnostics: transpileDiagnostics } = new Transpiler().emit({ program, writeFile: collector.writeFile }); - const diagnostics = ts.sortAndDeduplicateDiagnostics([ - ...ts.getPreEmitDiagnostics(program), - ...transpileDiagnostics, - ]); - - return { diagnostics: [...diagnostics], transpiledFiles: collector.files }; -} - -export interface TranspileStringResult { - diagnostics: ts.Diagnostic[]; - file?: TranspiledFile; -} - -export function transpileString(main: string, options: CompilerOptions = {}): TranspileStringResult { - const { diagnostics, transpiledFiles } = transpileVirtualProject({ "main.ts": main }, options); - return { - diagnostics, - file: transpiledFiles.find(({ sourceFiles }) => sourceFiles.some(f => f.fileName === "main.ts")), - }; -} diff --git a/src/transpilation/output-collector.ts b/src/transpilation/output-collector.ts deleted file mode 100644 index d18137c5a..000000000 --- a/src/transpilation/output-collector.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as ts from "typescript"; -import { intersection, union } from "../utils"; - -export interface TranspiledFile { - sourceFiles: ts.SourceFile[]; - lua?: string; - luaSourceMap?: string; - declaration?: string; - declarationMap?: string; - /** @internal */ - js?: string; - /** @internal */ - jsSourceMap?: string; -} - -export function createEmitOutputCollector() { - const files: TranspiledFile[] = []; - 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] }; - files.push(file); - } else { - file.sourceFiles = union(file.sourceFiles, sourceFiles); - } - - if (fileName.endsWith(".lua")) { - file.lua = data; - } else if (fileName.endsWith(".lua.map")) { - file.luaSourceMap = data; - } else if (fileName.endsWith(".js")) { - file.js = data; - } else if (fileName.endsWith(".js.map")) { - file.jsSourceMap = data; - } else if (fileName.endsWith(".d.ts")) { - file.declaration = data; - } else if (fileName.endsWith(".d.ts.map")) { - file.declarationMap = data; - } - }; - - return { writeFile, files }; -} diff --git a/src/transpilation/plugins.ts b/src/transpilation/plugins.ts deleted file mode 100644 index 0311efbbd..000000000 --- a/src/transpilation/plugins.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as ts from "typescript"; -import { CompilerOptions } from "../CompilerOptions"; -import { Printer } from "../LuaPrinter"; -import { Visitors } from "../transformation/context"; -import { getConfigDirectory, resolvePlugin } from "./utils"; - -export interface Plugin { - /** - * An augmentation to the map of visitors that transform TypeScript AST to Lua AST. - * - * Key is a `SyntaxKind` of a processed node. - */ - visitors?: Visitors; - - /** - * A function that converts Lua AST to a string. - * - * At most one custom printer can be provided across all plugins. - */ - printer?: Printer; -} - -export function getPlugins(program: ts.Program, diagnostics: ts.Diagnostic[], customPlugins: Plugin[]): Plugin[] { - const pluginsFromOptions: Plugin[] = []; - const options = program.getCompilerOptions() as CompilerOptions; - - for (const [index, pluginOption] of (options.luaPlugins ?? []).entries()) { - const optionName = `tstl.luaPlugins[${index}]`; - - const { error: resolveError, result: factory } = resolvePlugin( - "plugin", - `${optionName}.name`, - getConfigDirectory(options), - pluginOption.name, - pluginOption.import - ); - - if (resolveError) diagnostics.push(resolveError); - if (factory === undefined) continue; - - const plugin = typeof factory === "function" ? factory(pluginOption) : factory; - pluginsFromOptions.push(plugin); - } - - return [...customPlugins, ...pluginsFromOptions]; -} diff --git a/src/transpilation/transpiler.ts b/src/transpilation/transpiler.ts deleted file mode 100644 index 205b6b2db..000000000 --- a/src/transpilation/transpiler.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as path from "path"; -import * as ts from "typescript"; -import { isBundleEnabled } from "../CompilerOptions"; -import { getLuaLibBundle } from "../LuaLib"; -import { normalizeSlashes, trimExtension } from "../utils"; -import { getBundleResult } from "./bundle"; -import { getProgramTranspileResult, TranspileOptions } from "./transpile"; -import { EmitFile, EmitHost, ProcessedFile } from "./utils"; - -export interface TranspilerOptions { - emitHost?: EmitHost; -} - -export interface EmitOptions extends TranspileOptions { - writeFile?: ts.WriteFileCallback; -} - -export interface EmitResult { - emitSkipped: boolean; - diagnostics: readonly ts.Diagnostic[]; -} - -export class Transpiler { - protected emitHost: EmitHost; - constructor({ emitHost = ts.sys }: TranspilerOptions = {}) { - this.emitHost = emitHost; - } - - public emit(emitOptions: EmitOptions): EmitResult { - const { program, writeFile = this.emitHost.writeFile } = emitOptions; - const { diagnostics, transpiledFiles: freshFiles } = getProgramTranspileResult( - this.emitHost, - writeFile, - emitOptions - ); - const { emitPlan } = this.getEmitPlan(program, diagnostics, freshFiles); - - const options = program.getCompilerOptions(); - const emitBOM = options.emitBOM ?? false; - for (const { outputPath, code, sourceMap, sourceFiles } of emitPlan) { - writeFile(outputPath, code, emitBOM, undefined, sourceFiles); - if (options.sourceMap && sourceMap !== undefined) { - writeFile(outputPath + ".map", sourceMap, emitBOM, undefined, sourceFiles); - } - } - - return { diagnostics, emitSkipped: emitPlan.length === 0 }; - } - - protected getEmitPlan( - program: ts.Program, - diagnostics: ts.Diagnostic[], - 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")); - files.unshift({ fileName, code: getLuaLibBundle(this.emitHost) }); - } - - let emitPlan: EmitFile[]; - if (isBundleEnabled(options)) { - const [bundleDiagnostics, bundleFile] = getBundleResult(program, this.emitHost, files); - 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 }; - }); - } - - return { emitPlan }; - } -} diff --git a/src/tstl.ts b/src/tstl.ts index fb011be3c..f4abd8306 100644 --- a/src/tstl.ts +++ b/src/tstl.ts @@ -1,200 +1,6 @@ #!/usr/bin/env node import * as ts from "typescript"; -import * as tstl from "."; -import * as cliDiagnostics from "./cli/diagnostics"; -import { getHelpString, versionString } from "./cli/information"; -import { parseCommandLine } from "./cli/parse"; -import { createDiagnosticReporter } from "./cli/report"; -import { createConfigFileUpdater, locateConfigFile, parseConfigFileWithSystem } from "./cli/tsconfig"; -import { isBundleEnabled } from "./CompilerOptions"; - -const shouldBePretty = ({ pretty }: ts.CompilerOptions = {}) => - pretty !== undefined ? (pretty as boolean) : ts.sys.writeOutputIsTTY?.() ?? false; - -let reportDiagnostic = createDiagnosticReporter(false); -function updateReportDiagnostic(options?: ts.CompilerOptions): void { - reportDiagnostic = createDiagnosticReporter(shouldBePretty(options)); -} - -function createWatchStatusReporter(options?: ts.CompilerOptions): ts.WatchStatusReporter { - return ts.createWatchStatusReporter(ts.sys, shouldBePretty(options)); -} - -function executeCommandLine(args: string[]): void { - if (args.length > 0 && args[0].startsWith("-")) { - const firstOption = args[0].slice(args[0].startsWith("--") ? 2 : 1).toLowerCase(); - if (firstOption === "build" || firstOption === "b") { - return performBuild(args.slice(1)); - } - } - - const commandLine = parseCommandLine(args); - - if (commandLine.options.build) { - reportDiagnostic(cliDiagnostics.optionBuildMustBeFirstCommandLineArgument()); - return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - // TODO: ParsedCommandLine.errors isn't meant to contain warnings. Once root-level options - // support would be dropped it should be changed to `commandLine.errors.length > 0`. - if (commandLine.errors.some(e => e.category === ts.DiagnosticCategory.Error)) { - commandLine.errors.forEach(reportDiagnostic); - return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - if (commandLine.options.version) { - console.log(versionString); - return ts.sys.exit(ts.ExitStatus.Success); - } - - if (commandLine.options.help) { - console.log(versionString); - console.log(getHelpString()); - return ts.sys.exit(ts.ExitStatus.Success); - } - - const configFileName = locateConfigFile(commandLine); - if (typeof configFileName === "object") { - reportDiagnostic(configFileName); - return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - const commandLineOptions = commandLine.options; - if (configFileName) { - const configParseResult = parseConfigFileWithSystem(configFileName, commandLineOptions); - - updateReportDiagnostic(configParseResult.options); - if (configParseResult.options.watch) { - createWatchOfConfigFile(configFileName, commandLineOptions); - } else { - performCompilation( - configParseResult.fileNames, - configParseResult.projectReferences, - configParseResult.options, - ts.getConfigFileParsingDiagnostics(configParseResult) - ); - } - } else { - updateReportDiagnostic(commandLineOptions); - if (commandLineOptions.watch) { - createWatchOfFilesAndCompilerOptions(commandLine.fileNames, commandLineOptions); - } else { - performCompilation(commandLine.fileNames, commandLine.projectReferences, commandLineOptions); - } - } -} - -function performBuild(_args: string[]): void { - console.log("Option '--build' is not supported."); - return ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); -} - -function performCompilation( - rootNames: string[], - projectReferences: readonly ts.ProjectReference[] | undefined, - options: tstl.CompilerOptions, - configFileParsingDiagnostics?: readonly ts.Diagnostic[] -): void { - const program = ts.createProgram({ - rootNames, - options, - projectReferences, - configFileParsingDiagnostics, - }); - - const { diagnostics: transpileDiagnostics, emitSkipped } = new tstl.Transpiler().emit({ program }); - - const diagnostics = ts.sortAndDeduplicateDiagnostics([ - ...ts.getPreEmitDiagnostics(program), - ...transpileDiagnostics, - ]); - - diagnostics.forEach(reportDiagnostic); - const exitCode = - diagnostics.length === 0 - ? ts.ExitStatus.Success - : emitSkipped - ? ts.ExitStatus.DiagnosticsPresent_OutputsSkipped - : ts.ExitStatus.DiagnosticsPresent_OutputsGenerated; - - return ts.sys.exit(exitCode); -} - -function createWatchOfConfigFile(configFileName: string, optionsToExtend: tstl.CompilerOptions): void { - const watchCompilerHost = ts.createWatchCompilerHost( - configFileName, - optionsToExtend, - ts.sys, - ts.createSemanticDiagnosticsBuilderProgram, - undefined, - createWatchStatusReporter(optionsToExtend) - ); - - updateWatchCompilationHost(watchCompilerHost, optionsToExtend); - ts.createWatchProgram(watchCompilerHost); -} - -function createWatchOfFilesAndCompilerOptions(rootFiles: string[], options: tstl.CompilerOptions): void { - const watchCompilerHost = ts.createWatchCompilerHost( - rootFiles, - options, - ts.sys, - ts.createSemanticDiagnosticsBuilderProgram, - undefined, - createWatchStatusReporter(options) - ); - - updateWatchCompilationHost(watchCompilerHost, options); - ts.createWatchProgram(watchCompilerHost); -} - -function updateWatchCompilationHost( - host: ts.WatchCompilerHost, - optionsToExtend: tstl.CompilerOptions -): void { - let hadErrorLastTime = true; - const updateConfigFile = createConfigFileUpdater(optionsToExtend); - - const transpiler = new tstl.Transpiler(); - host.afterProgramCreate = builderProgram => { - const program = builderProgram.getProgram(); - const options = builderProgram.getCompilerOptions() as tstl.CompilerOptions; - const configFileParsingDiagnostics: ts.Diagnostic[] = updateConfigFile(options); - - let sourceFiles: ts.SourceFile[] | undefined; - if (!isBundleEnabled(options) && !hadErrorLastTime) { - sourceFiles = []; - while (true) { - const currentFile = builderProgram.getSemanticDiagnosticsOfNextAffectedFile(); - if (!currentFile) break; - - if ("fileName" in currentFile.affected) { - sourceFiles.push(currentFile.affected); - } else { - sourceFiles.push(...currentFile.affected.getSourceFiles()); - } - } - } - - const { diagnostics: emitDiagnostics } = transpiler.emit({ program, sourceFiles }); - - const diagnostics = ts.sortAndDeduplicateDiagnostics([ - ...configFileParsingDiagnostics, - ...program.getOptionsDiagnostics(), - ...program.getSyntacticDiagnostics(), - ...program.getGlobalDiagnostics(), - ...program.getSemanticDiagnostics(), - ...emitDiagnostics, - ]); - - diagnostics.forEach(reportDiagnostic); - - const errors = diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); - hadErrorLastTime = errors.length > 0; - - host.onWatchStatusChange!(cliDiagnostics.watchErrorSummary(errors.length), host.getNewLine(), options); - }; -} +import { executeCommandLine } from "./cli/execute"; function checkNodeVersion(): void { const [major, minor] = process.version.slice(1).split(".").map(Number); diff --git a/src/typescript-internal.d.ts b/src/typescript-internal.d.ts index 7444d89d7..01e7e4b5c 100644 --- a/src/typescript-internal.d.ts +++ b/src/typescript-internal.d.ts @@ -4,6 +4,12 @@ declare module "typescript" { function createDiagnosticReporter(system: System, pretty?: boolean): DiagnosticReporter; function createWatchStatusReporter(system: System, pretty?: boolean): WatchStatusReporter; + // https://github.com/microsoft/TypeScript/blob/master/src/compiler/path.ts + // Prefer to use these methods over "path" module, because they don't depend on runtime platform, + // preserving input path style, which works better with tests that always use "/" as cwd. + function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined): string; + function getDirectoryPath(path: string): string; + interface System { setBlocking?(): void; } diff --git a/src/utils.ts b/src/utils.ts index 1244068a4..7e4966888 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ -import * as ts from "typescript"; import * as nativeAssert from "assert"; import * as path from "path"; +import * as ts from "typescript"; export function castArray(value: T | T[]): T[]; export function castArray(value: T | readonly T[]): readonly T[]; @@ -16,6 +16,18 @@ export const union = (...values: Array>): T[] => [...new Set(...v export const intersection = (first: readonly T[], ...rest: Array): T[] => union(first).filter(x => rest.every(r => r.includes(x))); +export const invertObject = (record: Record): Record => + Object.fromEntries(Object.entries(record).map(([key, value]) => [value, key])); + +export function mapAndFind(elements: T[], callback: (plugin: T) => U | undefined): U | undefined { + for (const element of elements) { + const result = callback(element); + if (result !== undefined) { + return result; + } + } +} + type DiagnosticFactory = (...args: any) => Partial & Pick; export const createDiagnosticFactoryWithCode = (code: number, create: T) => Object.assign( @@ -38,15 +50,6 @@ export const createSerialDiagnosticFactory = (creat export const normalizeSlashes = (filePath: string) => filePath.replace(/\\/g, "/"); export const trimExtension = (filePath: string) => filePath.slice(0, -path.extname(filePath).length); -export function formatPathToLuaPath(filePath: string): string { - filePath = filePath.replace(/\.json$/, ""); - if (process.platform === "win32") { - // Windows can use backslashes - filePath = filePath.replace(/\.\\/g, "").replace(/\\/g, "."); - } - return filePath.replace(/\.\//g, "").replace(/\//g, "."); -} - type NoInfer = [T][T extends any ? 0 : never]; export function getOrUpdate( diff --git a/test/cli/errors/error.ts b/test/cli/__fixtures__/errors/error.ts similarity index 100% rename from test/cli/errors/error.ts rename to test/cli/__fixtures__/errors/error.ts diff --git a/test/cli/watch/tsconfig.json b/test/cli/__fixtures__/watch/basic/tsconfig.json similarity index 100% rename from test/cli/watch/tsconfig.json rename to test/cli/__fixtures__/watch/basic/tsconfig.json diff --git a/test/cli/watch/watch.ts b/test/cli/__fixtures__/watch/basic/watch.ts similarity index 100% rename from test/cli/watch/watch.ts rename to test/cli/__fixtures__/watch/basic/watch.ts diff --git a/test/cli/__fixtures__/watch/multiple-files/a.ts b/test/cli/__fixtures__/watch/multiple-files/a.ts new file mode 100644 index 000000000..e6da568dd --- /dev/null +++ b/test/cli/__fixtures__/watch/multiple-files/a.ts @@ -0,0 +1 @@ +import "./b"; diff --git a/test/cli/__fixtures__/watch/multiple-files/b.ts b/test/cli/__fixtures__/watch/multiple-files/b.ts new file mode 100644 index 000000000..2e583315c --- /dev/null +++ b/test/cli/__fixtures__/watch/multiple-files/b.ts @@ -0,0 +1 @@ +export const result = true; diff --git a/test/cli/__fixtures__/watch/multiple-files/tsconfig.json b/test/cli/__fixtures__/watch/multiple-files/tsconfig.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/cli/__fixtures__/watch/multiple-files/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/test/cli/errors.spec.ts b/test/cli/errors.spec.ts index 8ff45ce14..8f5324cb8 100644 --- a/test/cli/errors.spec.ts +++ b/test/cli/errors.spec.ts @@ -1,9 +1,8 @@ import * as fs from "fs"; -import * as path from "path"; -import { runCli } from "./run"; +import { resolveFixture, runCli } from "./run"; -const srcFilePath = path.resolve(__dirname, "errors", "error.ts"); -const outFilePath = path.resolve(__dirname, "errors", "error.lua"); +const srcFilePath = resolveFixture("errors/error.ts"); +const outFilePath = resolveFixture("errors/error.lua"); const errorMessage = "Unable to convert function with no 'this' parameter to function with 'this'."; afterEach(() => { diff --git a/test/cli/run.ts b/test/cli/run.ts index cee7e19b2..54e96f7c2 100644 --- a/test/cli/run.ts +++ b/test/cli/run.ts @@ -3,8 +3,9 @@ import * as path from "path"; jest.setTimeout(20000); -const cliPath = path.join(__dirname, "../../src/tstl.ts"); +export const resolveFixture = (name: string) => path.resolve(__dirname, "__fixtures__", name); +const cliPath = path.join(__dirname, "../../src/tstl.ts"); const defaultArgs = ["--skipLibCheck", "--types", "node"]; export function forkCli(args: string[]): ChildProcess { return fork(cliPath, [...defaultArgs, ...args], { @@ -13,19 +14,21 @@ export function forkCli(args: string[]): ChildProcess { }); } -export interface CliResult { +export interface CliOutput { exitCode: number; output: string; } -export async function runCli(args: string[]): Promise { - const child = forkCli(args); - +export async function collectCliOutput(child: ChildProcess) { let output = ""; child.stdout!.on("data", (data: Buffer) => (output += data.toString())); child.stderr!.on("data", (data: Buffer) => (output += data.toString())); - return new Promise(resolve => { + return new Promise(resolve => { child.on("close", exitCode => resolve({ exitCode, output })); }); } + +export async function runCli(args: string[]) { + return collectCliOutput(forkCli(args)); +} diff --git a/test/cli/watch.spec.ts b/test/cli/watch.spec.ts index 35d2f2f59..9372c7fa5 100644 --- a/test/cli/watch.spec.ts +++ b/test/cli/watch.spec.ts @@ -1,6 +1,8 @@ import * as fs from "fs-extra"; -import * as path from "path"; -import { forkCli } from "./run"; +import { promisify } from "util"; +import { collectCliOutput, forkCli, resolveFixture } from "./run"; + +const delay = promisify(setTimeout); let testsCleanup: Array<() => void> = []; afterEach(() => { @@ -21,25 +23,36 @@ async function waitForFileExists(filePath: string): Promise { }); } -function forkWatchProcess(args: string[]): void { +function forkWatchProcess(args: string[]) { const child = forkCli(["--watch", ...args]); testsCleanup.push(() => child.kill()); + return child; } -const watchedFile = path.join(__dirname, "./watch/watch.ts"); -const watchedFileOut = watchedFile.replace(".ts", ".lua"); +const changedTestFiles = new Set(); +function changeTestFile(fileName: string, content: string) { + if (!changedTestFiles.has(fileName)) { + changedTestFiles.add(fileName); + const originalContent = fs.readFileSync(fileName); + testsCleanup.push(() => { + changedTestFiles.delete(fileName); + fs.writeFileSync(fileName, originalContent); + }); + } + + fs.writeFileSync(fileName, content); +} -afterEach(() => fs.removeSync(watchedFileOut)); +const watchedFile = resolveFixture("watch/basic/watch.ts"); +const watchedFileOut = resolveFixture("watch/basic/watch.lua"); async function compileChangeAndCompare(filePath: string, content: string): Promise { + testsCleanup.push(() => fs.removeSync(watchedFileOut)); await waitForFileExists(watchedFileOut); const initialResultLua = fs.readFileSync(watchedFileOut, "utf8"); fs.unlinkSync(watchedFileOut); - - const originalContent = fs.readFileSync(filePath, "utf8"); - fs.writeFileSync(filePath, content); - testsCleanup.push(() => fs.writeFileSync(filePath, originalContent)); + changeTestFile(filePath, content); await waitForFileExists(watchedFileOut); const updatedResultLua = fs.readFileSync(watchedFileOut, "utf8"); @@ -48,17 +61,60 @@ async function compileChangeAndCompare(filePath: string, content: string): Promi } test("should watch single file", async () => { - forkWatchProcess([path.join(__dirname, "./watch/watch.ts")]); + forkWatchProcess([watchedFile]); await compileChangeAndCompare(watchedFile, "const value = 1;"); }); test("should watch project", async () => { - forkWatchProcess(["--project", path.join(__dirname, "./watch")]); + forkWatchProcess(["--project", resolveFixture("watch/basic")]); await compileChangeAndCompare(watchedFile, "const value = 1;"); }); test("should watch config file", async () => { - const configFilePath = path.join(__dirname, "./watch/tsconfig.json"); + const configFilePath = resolveFixture("watch/basic/tsconfig.json"); forkWatchProcess(["--project", configFilePath]); await compileChangeAndCompare(configFilePath, '{ "tstl": { "luaTarget": "5.3" } }'); }); + +test("should watch multiple files", async () => { + const mtimeSnapshots: Array<{ a: number; b: number }> = []; + const takeMtimeSnapshot = () => { + mtimeSnapshots.push({ + a: fs.statSync(resolveFixture("watch/multiple-files/a.lua")).mtimeMs, + b: fs.statSync(resolveFixture("watch/multiple-files/b.lua")).mtimeMs, + }); + }; + + testsCleanup.push(() => fs.unlinkSync(resolveFixture("watch/multiple-files/b.lua"))); + testsCleanup.push(() => fs.unlinkSync(resolveFixture("watch/multiple-files/a.lua"))); + + const child = forkWatchProcess(["--project", resolveFixture("watch/multiple-files")]); + const childOutputPromise = collectCliOutput(child); + + await waitForFileExists(resolveFixture("watch/multiple-files/a.lua")); + await waitForFileExists(resolveFixture("watch/multiple-files/b.lua")); + await delay(300); + takeMtimeSnapshot(); + + for (let index = 0; index < 3; index++) { + fs.unlinkSync(resolveFixture("watch/multiple-files/a.lua")); + changeTestFile(resolveFixture("watch/multiple-files/a.ts"), `import "./b"; // ${index}`); + + await waitForFileExists(resolveFixture("watch/multiple-files/a.lua")); + await delay(300); + takeMtimeSnapshot(); + } + + child.kill(); + const { output } = await childOutputPromise; + expect(output.match(/Found 0 errors\. Watching for file changes\./g)).toHaveLength(4); + + // TODO: First watch event re-writes all files + expect(mtimeSnapshots[0].b).not.toEqual(mtimeSnapshots[1].b); + expect(mtimeSnapshots[1].b).toEqual(mtimeSnapshots[2].b); + expect(mtimeSnapshots[1].b).toEqual(mtimeSnapshots[3].b); + + expect(mtimeSnapshots[0].a).not.toEqual(mtimeSnapshots[1].a); + expect(mtimeSnapshots[1].a).not.toEqual(mtimeSnapshots[2].a); + expect(mtimeSnapshots[2].a).not.toEqual(mtimeSnapshots[3].a); +}); diff --git a/test/legacy-utils.ts b/test/legacy-utils.ts index de781d954..4cdf0b82a 100644 --- a/test/legacy-utils.ts +++ b/test/legacy-utils.ts @@ -3,20 +3,20 @@ import * as fs from "fs"; import * as path from "path"; import * as ts from "typescript"; import * as tstl from "../src"; -import { formatPathToLuaPath } from "../src/utils"; +import { assert } from "./util"; export function transpileString( str: string | { [filename: string]: string }, options: tstl.CompilerOptions = {}, ignoreDiagnostics = true ): string { - const { diagnostics, file } = transpileStringResult(str, options); - expect(file.lua).toBeDefined(); + const { diagnostics, lua } = transpileStringResult(str, options); + assert(lua !== undefined); const errors = diagnostics.filter(d => !ignoreDiagnostics || d.source === "typescript-to-lua"); expect(errors).not.toHaveDiagnostics(); - return file.lua!.trim(); + return lua.trim(); } function transpileStringsAsProject(input: Record, options: tstl.CompilerOptions = {}) { @@ -34,7 +34,7 @@ function transpileStringsAsProject(input: Record, options: tstl. function transpileStringResult( input: string | Record, options: tstl.CompilerOptions = {} -): Required { +): tstl.TranspileStringResult { const { diagnostics, transpiledFiles } = transpileStringsAsProject( typeof input === "string" ? { "main.ts": input } : input, options @@ -45,7 +45,7 @@ function transpileStringResult( throw new Error('Program should have a file named "main"'); } - return { diagnostics, file }; + return { ...file, diagnostics }; } const lualibContent = fs.readFileSync(path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), "utf8"); @@ -99,54 +99,6 @@ export function transpileAndExecute( return executeLua(lua); } -function getExportPath(fileName: string, options: ts.CompilerOptions): string { - const rootDir = options.rootDir ? path.resolve(options.rootDir) : path.resolve("."); - - const absolutePath = path.resolve(fileName.replace(/.ts$/, "")); - const absoluteRootDirPath = path.format(path.parse(rootDir)); - return formatPathToLuaPath(absolutePath.replace(absoluteRootDirPath, "").slice(1)); -} - -export function transpileAndExecuteProjectReturningMainExport( - typeScriptFiles: Record, - exportName: string, - options: tstl.CompilerOptions = {} -): [any, string] { - const mainFile = Object.keys(typeScriptFiles).find(typeScriptFileName => typeScriptFileName === "main.ts"); - if (!mainFile) { - throw new Error("An entry point file needs to be specified. This should be called main.ts"); - } - - const joinedTranspiledFiles = Object.keys(typeScriptFiles) - .filter(typeScriptFileName => typeScriptFileName !== "main.ts") - .map(typeScriptFileName => { - const modulePath = getExportPath(typeScriptFileName, options); - const tsCode = typeScriptFiles[typeScriptFileName]; - const luaCode = transpileString(tsCode, options); - return `package.preload["${modulePath}"] = function() - ${luaCode} - end`; - }) - .join("\n"); - - const luaCode = `return (function() - ${joinedTranspiledFiles} - ${transpileString(typeScriptFiles[mainFile])} - end)().${exportName}`; - - try { - return [executeLua(luaCode), luaCode]; - } catch (err) { - throw new Error(` - Encountered an error when executing the following Lua code: - - ${luaCode} - - ${err} - `); - } -} - export function transpileExecuteAndReturnExport( tsStr: string, returnExport: string, diff --git a/test/translation/transformation/blockScopeVariables.ts b/test/translation/__fixtures__/blockScopeVariables.ts similarity index 100% rename from test/translation/transformation/blockScopeVariables.ts rename to test/translation/__fixtures__/blockScopeVariables.ts diff --git a/test/translation/transformation/characterEscapeSequence.ts b/test/translation/__fixtures__/characterEscapeSequence.ts similarity index 100% rename from test/translation/transformation/characterEscapeSequence.ts rename to test/translation/__fixtures__/characterEscapeSequence.ts diff --git a/test/translation/transformation/classExtension1.ts b/test/translation/__fixtures__/classExtension1.ts similarity index 100% rename from test/translation/transformation/classExtension1.ts rename to test/translation/__fixtures__/classExtension1.ts diff --git a/test/translation/transformation/classExtension2.ts b/test/translation/__fixtures__/classExtension2.ts similarity index 100% rename from test/translation/transformation/classExtension2.ts rename to test/translation/__fixtures__/classExtension2.ts diff --git a/test/translation/transformation/classExtension3.ts b/test/translation/__fixtures__/classExtension3.ts similarity index 100% rename from test/translation/transformation/classExtension3.ts rename to test/translation/__fixtures__/classExtension3.ts diff --git a/test/translation/transformation/classExtension4.ts b/test/translation/__fixtures__/classExtension4.ts similarity index 100% rename from test/translation/transformation/classExtension4.ts rename to test/translation/__fixtures__/classExtension4.ts diff --git a/test/translation/transformation/classPureAbstract.ts b/test/translation/__fixtures__/classPureAbstract.ts similarity index 100% rename from test/translation/transformation/classPureAbstract.ts rename to test/translation/__fixtures__/classPureAbstract.ts diff --git a/test/translation/transformation/globalAugmentation.ts b/test/translation/__fixtures__/globalAugmentation.ts similarity index 100% rename from test/translation/transformation/globalAugmentation.ts rename to test/translation/__fixtures__/globalAugmentation.ts diff --git a/test/translation/transformation/methodRestArguments.ts b/test/translation/__fixtures__/methodRestArguments.ts similarity index 100% rename from test/translation/transformation/methodRestArguments.ts rename to test/translation/__fixtures__/methodRestArguments.ts diff --git a/test/translation/transformation/modulesClassExport.ts b/test/translation/__fixtures__/modulesClassExport.ts similarity index 100% rename from test/translation/transformation/modulesClassExport.ts rename to test/translation/__fixtures__/modulesClassExport.ts diff --git a/test/translation/transformation/modulesFunctionExport.ts b/test/translation/__fixtures__/modulesFunctionExport.ts similarity index 100% rename from test/translation/transformation/modulesFunctionExport.ts rename to test/translation/__fixtures__/modulesFunctionExport.ts diff --git a/test/translation/transformation/modulesFunctionNoExport.ts b/test/translation/__fixtures__/modulesFunctionNoExport.ts similarity index 100% rename from test/translation/transformation/modulesFunctionNoExport.ts rename to test/translation/__fixtures__/modulesFunctionNoExport.ts diff --git a/test/translation/transformation/modulesNamespaceExport.ts b/test/translation/__fixtures__/modulesNamespaceExport.ts similarity index 100% rename from test/translation/transformation/modulesNamespaceExport.ts rename to test/translation/__fixtures__/modulesNamespaceExport.ts diff --git a/test/translation/transformation/modulesNamespaceNestedWithMemberExport.ts b/test/translation/__fixtures__/modulesNamespaceNestedWithMemberExport.ts similarity index 100% rename from test/translation/transformation/modulesNamespaceNestedWithMemberExport.ts rename to test/translation/__fixtures__/modulesNamespaceNestedWithMemberExport.ts diff --git a/test/translation/transformation/modulesNamespaceNoExport.ts b/test/translation/__fixtures__/modulesNamespaceNoExport.ts similarity index 100% rename from test/translation/transformation/modulesNamespaceNoExport.ts rename to test/translation/__fixtures__/modulesNamespaceNoExport.ts diff --git a/test/translation/transformation/modulesNamespaceWithMemberExport.ts b/test/translation/__fixtures__/modulesNamespaceWithMemberExport.ts similarity index 100% rename from test/translation/transformation/modulesNamespaceWithMemberExport.ts rename to test/translation/__fixtures__/modulesNamespaceWithMemberExport.ts diff --git a/test/translation/transformation/modulesNamespaceWithMemberNoExport.ts b/test/translation/__fixtures__/modulesNamespaceWithMemberNoExport.ts similarity index 100% rename from test/translation/transformation/modulesNamespaceWithMemberNoExport.ts rename to test/translation/__fixtures__/modulesNamespaceWithMemberNoExport.ts diff --git a/test/translation/transformation/namespacePhantom.ts b/test/translation/__fixtures__/namespacePhantom.ts similarity index 100% rename from test/translation/transformation/namespacePhantom.ts rename to test/translation/__fixtures__/namespacePhantom.ts diff --git a/test/translation/transformation/returnDefault.ts b/test/translation/__fixtures__/returnDefault.ts similarity index 100% rename from test/translation/transformation/returnDefault.ts rename to test/translation/__fixtures__/returnDefault.ts diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap deleted file mode 100644 index 942f17aa2..000000000 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ /dev/null @@ -1,292 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transformation (blockScopeVariables) 1`] = ` -"do - local a = 1 - local b = 1 - local ____ = {c = 1} - local c = ____.c -end" -`; - -exports[`Transformation (characterEscapeSequence) 1`] = ` -"quoteInDoubleQuotes = \\"' ' '\\" -quoteInTemplateString = \\"' ' '\\" -doubleQuoteInQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\" -doubleQuoteInDoubleQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\" -doubleQuoteInTemplateString = \\"\\\\\\" \\\\\\" \\\\\\"\\" -backQuoteInQuotes = \\"\` \` \`\\" -backQuoteInDoubleQuotes = \\"\` \` \`\\" -backQuoteInTemplateString = \\"\` \` \`\\" -escapedCharsInQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" -escapedCharsInDoubleQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" -escapedCharsInTemplateString = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" -nonEmptyTemplateString = (\\"Level 0: \\\\n\\\\t \\" .. ((\\"Level 1: \\\\n\\\\t\\\\t \\" .. ((\\"Level 3: \\\\n\\\\t\\\\t\\\\t \\" .. \\"Last level \\\\n --\\") .. \\" \\\\n --\\")) .. \\" \\\\n --\\")) .. \\" \\\\n --\\"" -`; - -exports[`Transformation (classExtension1) 1`] = ` -"function MyClass.myFunction(self) -end" -`; - -exports[`Transformation (classExtension2) 1`] = ` -"function TestClass.myFunction(self) -end" -`; - -exports[`Transformation (classExtension3) 1`] = ` -"function RenamedTestClass.myFunction(self) -end -function RenamedMyClass.myFunction(self) -end" -`; - -exports[`Transformation (classExtension4) 1`] = ` -"MyClass.test = \\"test\\" -MyClass.testP = \\"testP\\" -function MyClass.myFunction(self) -end" -`; - -exports[`Transformation (classPureAbstract) 1`] = ` -"require(\\"lualib_bundle\\"); -ClassB = __TS__Class() -ClassB.name = \\"ClassB\\" -function ClassB.prototype.____constructor(self) -end" -`; - -exports[`Transformation (exportStatement) 1`] = ` -"local ____exports = {} -local xyz = 4 -____exports.xyz = xyz -____exports.uwv = xyz -do - local ____export = require(\\"xyz\\") - for ____exportKey, ____exportValue in pairs(____export) do - ____exports[____exportKey] = ____exportValue - end -end -do - local ____xyz = require(\\"xyz\\") - local abc = ____xyz.abc - local def = ____xyz.def - ____exports.abc = abc - ____exports.def = def -end -do - local ____xyz = require(\\"xyz\\") - local def = ____xyz.abc - ____exports.def = def -end -return ____exports" -`; - -exports[`Transformation (globalAugmentation) 1`] = ` -"local ____exports = globalVariable -return ____exports" -`; - -exports[`Transformation (methodRestArguments) 1`] = ` -"require(\\"lualib_bundle\\"); -MyClass = __TS__Class() -MyClass.name = \\"MyClass\\" -function MyClass.prototype.____constructor(self) -end -function MyClass.prototype.varargsFunction(self, a, ...) -end" -`; - -exports[`Transformation (modulesChangedVariableExport) 1`] = ` -"local ____exports = {} -____exports.foo = 1 -return ____exports" -`; - -exports[`Transformation (modulesClassExport) 1`] = ` -"require(\\"lualib_bundle\\"); -local ____exports = {} -____exports.TestClass = __TS__Class() -local TestClass = ____exports.TestClass -TestClass.name = \\"TestClass\\" -function TestClass.prototype.____constructor(self) -end -return ____exports" -`; - -exports[`Transformation (modulesClassWithMemberExport) 1`] = ` -"require(\\"lualib_bundle\\"); -local ____exports = {} -____exports.TestClass = __TS__Class() -local TestClass = ____exports.TestClass -TestClass.name = \\"TestClass\\" -function TestClass.prototype.____constructor(self) -end -function TestClass.prototype.memberFunc(self) -end -return ____exports" -`; - -exports[`Transformation (modulesFunctionExport) 1`] = ` -"local ____exports = {} -function ____exports.publicFunc(self) -end -return ____exports" -`; - -exports[`Transformation (modulesFunctionNoExport) 1`] = ` -"function publicFunc(self) -end" -`; - -exports[`Transformation (modulesImportAll) 1`] = ` -"local ____exports = {} -local Test = require(\\"test\\") -local ____ = Test -return ____exports" -`; - -exports[`Transformation (modulesImportNamed) 1`] = ` -"local ____exports = {} -local ____test = require(\\"test\\") -local TestClass = ____test.TestClass -local ____ = TestClass -return ____exports" -`; - -exports[`Transformation (modulesImportNamedSpecialChars) 1`] = ` -"local ____exports = {} -local ____kebab_2Dmodule = require(\\"kebab-module\\") -local TestClass1 = ____kebab_2Dmodule.TestClass1 -local ____dollar_24module = require(\\"dollar$module\\") -local TestClass2 = ____dollar_24module.TestClass2 -local ____singlequote_27module = require(\\"singlequote'module\\") -local TestClass3 = ____singlequote_27module.TestClass3 -local ____hash_23module = require(\\"hash#module\\") -local TestClass4 = ____hash_23module.TestClass4 -local ____space_20module = require(\\"space module\\") -local TestClass5 = ____space_20module.TestClass5 -local ____ = TestClass1 -local ____ = TestClass2 -local ____ = TestClass3 -local ____ = TestClass4 -local ____ = TestClass5 -return ____exports" -`; - -exports[`Transformation (modulesImportRenamed) 1`] = ` -"local ____exports = {} -local ____test = require(\\"test\\") -local RenamedClass = ____test.TestClass -local ____ = RenamedClass -return ____exports" -`; - -exports[`Transformation (modulesImportRenamedSpecialChars) 1`] = ` -"local ____exports = {} -local ____kebab_2Dmodule = require(\\"kebab-module\\") -local RenamedClass1 = ____kebab_2Dmodule.TestClass -local ____dollar_24module = require(\\"dollar$module\\") -local RenamedClass2 = ____dollar_24module.TestClass -local ____singlequote_27module = require(\\"singlequote'module\\") -local RenamedClass3 = ____singlequote_27module.TestClass -local ____hash_23module = require(\\"hash#module\\") -local RenamedClass4 = ____hash_23module.TestClass -local ____space_20module = require(\\"space module\\") -local RenamedClass5 = ____space_20module.TestClass -local ____ = RenamedClass1 -local ____ = RenamedClass2 -local ____ = RenamedClass3 -local ____ = RenamedClass4 -local ____ = RenamedClass5 -return ____exports" -`; - -exports[`Transformation (modulesImportWithoutFromClause) 1`] = ` -"local ____exports = {} -require(\\"test\\") -return ____exports" -`; - -exports[`Transformation (modulesNamespaceExport) 1`] = ` -"local ____exports = {} -____exports.TestSpace = {} -return ____exports" -`; - -exports[`Transformation (modulesNamespaceNestedWithMemberExport) 1`] = ` -"local ____exports = {} -____exports.TestSpace = {} -local TestSpace = ____exports.TestSpace -do - TestSpace.TestNestedSpace = {} - local TestNestedSpace = TestSpace.TestNestedSpace - do - function TestNestedSpace.innerFunc(self) - end - end -end -return ____exports" -`; - -exports[`Transformation (modulesNamespaceNoExport) 1`] = `"TestSpace = TestSpace or ({})"`; - -exports[`Transformation (modulesNamespaceWithMemberExport) 1`] = ` -"local ____exports = {} -____exports.TestSpace = {} -local TestSpace = ____exports.TestSpace -do - function TestSpace.innerFunc(self) - end -end -return ____exports" -`; - -exports[`Transformation (modulesNamespaceWithMemberNoExport) 1`] = ` -"local ____exports = {} -____exports.TestSpace = {} -do - local function innerFunc(self) - end -end -return ____exports" -`; - -exports[`Transformation (modulesVariableExport) 1`] = ` -"local ____exports = {} -____exports.foo = \\"bar\\" -return ____exports" -`; - -exports[`Transformation (modulesVariableNoExport) 1`] = `"foo = \\"bar\\""`; - -exports[`Transformation (namespacePhantom) 1`] = ` -"function nsMember(self) -end" -`; - -exports[`Transformation (returnDefault) 1`] = ` -"function myFunc(self) - return -end" -`; - -exports[`Transformation (topLevelVariables) 1`] = ` -"obj = {value1 = 1, value2 = 2} -value1 = obj.value1 -value2 = obj.value2 -obj2 = {value3 = 1, value4 = 2} -value3 = obj2.value3 -value4 = obj2.value4 -function fun1(self) -end -fun2 = function() -end" -`; - -exports[`Transformation (unusedDefaultWithNamespaceImport) 1`] = ` -"local ____exports = {} -local x = require(\\"module\\") -local ____ = x -return ____exports" -`; diff --git a/test/translation/__snapshots__/translation.spec.ts.snap b/test/translation/__snapshots__/translation.spec.ts.snap new file mode 100644 index 000000000..d1891cebe --- /dev/null +++ b/test/translation/__snapshots__/translation.spec.ts.snap @@ -0,0 +1,150 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transformation (blockScopeVariables) 1`] = ` +"do + local a = 1 + local b = 1 + local ____ = {c = 1} + local c = ____.c +end" +`; + +exports[`Transformation (characterEscapeSequence) 1`] = ` +"quoteInDoubleQuotes = \\"' ' '\\" +quoteInTemplateString = \\"' ' '\\" +doubleQuoteInQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\" +doubleQuoteInDoubleQuotes = \\"\\\\\\" \\\\\\" \\\\\\"\\" +doubleQuoteInTemplateString = \\"\\\\\\" \\\\\\" \\\\\\"\\" +backQuoteInQuotes = \\"\` \` \`\\" +backQuoteInDoubleQuotes = \\"\` \` \`\\" +backQuoteInTemplateString = \\"\` \` \`\\" +escapedCharsInQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" +escapedCharsInDoubleQuotes = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" +escapedCharsInTemplateString = \\"\\\\\\\\ \\\\0 \\\\b \\\\t \\\\n \\\\v \\\\f \\\\\\" ' \`\\" +nonEmptyTemplateString = (\\"Level 0: \\\\n\\\\t \\" .. ((\\"Level 1: \\\\n\\\\t\\\\t \\" .. ((\\"Level 3: \\\\n\\\\t\\\\t\\\\t \\" .. \\"Last level \\\\n --\\") .. \\" \\\\n --\\")) .. \\" \\\\n --\\")) .. \\" \\\\n --\\"" +`; + +exports[`Transformation (classExtension1) 1`] = ` +"function MyClass.myFunction(self) +end" +`; + +exports[`Transformation (classExtension2) 1`] = ` +"function TestClass.myFunction(self) +end" +`; + +exports[`Transformation (classExtension3) 1`] = ` +"function RenamedTestClass.myFunction(self) +end +function RenamedMyClass.myFunction(self) +end" +`; + +exports[`Transformation (classExtension4) 1`] = ` +"MyClass.test = \\"test\\" +MyClass.testP = \\"testP\\" +function MyClass.myFunction(self) +end" +`; + +exports[`Transformation (classPureAbstract) 1`] = ` +"require(\\"lualib_bundle\\"); +ClassB = __TS__Class() +ClassB.name = \\"ClassB\\" +function ClassB.prototype.____constructor(self) +end" +`; + +exports[`Transformation (globalAugmentation) 1`] = ` +"local ____exports = globalVariable +return ____exports" +`; + +exports[`Transformation (methodRestArguments) 1`] = ` +"require(\\"lualib_bundle\\"); +MyClass = __TS__Class() +MyClass.name = \\"MyClass\\" +function MyClass.prototype.____constructor(self) +end +function MyClass.prototype.varargsFunction(self, a, ...) +end" +`; + +exports[`Transformation (modulesClassExport) 1`] = ` +"require(\\"lualib_bundle\\"); +local ____exports = {} +____exports.TestClass = __TS__Class() +local TestClass = ____exports.TestClass +TestClass.name = \\"TestClass\\" +function TestClass.prototype.____constructor(self) +end +return ____exports" +`; + +exports[`Transformation (modulesFunctionExport) 1`] = ` +"local ____exports = {} +function ____exports.publicFunc(self) +end +return ____exports" +`; + +exports[`Transformation (modulesFunctionNoExport) 1`] = ` +"function publicFunc(self) +end" +`; + +exports[`Transformation (modulesNamespaceExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +return ____exports" +`; + +exports[`Transformation (modulesNamespaceNestedWithMemberExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +local TestSpace = ____exports.TestSpace +do + TestSpace.TestNestedSpace = {} + local TestNestedSpace = TestSpace.TestNestedSpace + do + function TestNestedSpace.innerFunc(self) + end + end +end +return ____exports" +`; + +exports[`Transformation (modulesNamespaceNoExport) 1`] = `"TestSpace = TestSpace or ({})"`; + +exports[`Transformation (modulesNamespaceWithMemberExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +local TestSpace = ____exports.TestSpace +do + function TestSpace.innerFunc(self) + end +end +return ____exports" +`; + +exports[`Transformation (modulesNamespaceWithMemberNoExport) 1`] = ` +"local ____exports = {} +____exports.TestSpace = {} +do + local function innerFunc(self) + end +end +return ____exports" +`; + +exports[`Transformation (namespacePhantom) 1`] = ` +"function nsMember(self) +end" +`; + +exports[`Transformation (returnDefault) 1`] = ` +"function myFunc(self) + return +end" +`; diff --git a/test/translation/transformation/exportStatement.ts b/test/translation/transformation/exportStatement.ts deleted file mode 100644 index 7aa965422..000000000 --- a/test/translation/transformation/exportStatement.ts +++ /dev/null @@ -1,6 +0,0 @@ -const xyz = 4; -export { xyz }; -export { xyz as uwv }; -export * from "xyz"; -export { abc, def } from "xyz"; -export { abc as def } from "xyz"; diff --git a/test/translation/transformation/modulesChangedVariableExport.ts b/test/translation/transformation/modulesChangedVariableExport.ts deleted file mode 100644 index 009c4cd06..000000000 --- a/test/translation/transformation/modulesChangedVariableExport.ts +++ /dev/null @@ -1,2 +0,0 @@ -export let foo; -foo = 1; diff --git a/test/translation/transformation/modulesClassWithMemberExport.ts b/test/translation/transformation/modulesClassWithMemberExport.ts deleted file mode 100644 index 73f226b68..000000000 --- a/test/translation/transformation/modulesClassWithMemberExport.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class TestClass { - memberFunc() {} -} diff --git a/test/translation/transformation/modulesImportAll.ts b/test/translation/transformation/modulesImportAll.ts deleted file mode 100644 index 28b470640..000000000 --- a/test/translation/transformation/modulesImportAll.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Test from "test"; - -Test; diff --git a/test/translation/transformation/modulesImportNamed.ts b/test/translation/transformation/modulesImportNamed.ts deleted file mode 100644 index d147081ff..000000000 --- a/test/translation/transformation/modulesImportNamed.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { TestClass } from "test"; - -TestClass; diff --git a/test/translation/transformation/modulesImportNamedSpecialChars.ts b/test/translation/transformation/modulesImportNamedSpecialChars.ts deleted file mode 100644 index dbb54990c..000000000 --- a/test/translation/transformation/modulesImportNamedSpecialChars.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TestClass1 } from "kebab-module"; -import { TestClass2 } from "dollar$module"; -import { TestClass3 } from "singlequote'module"; -import { TestClass4 } from "hash#module"; -import { TestClass5 } from "space module"; - -TestClass1; -TestClass2; -TestClass3; -TestClass4; -TestClass5; diff --git a/test/translation/transformation/modulesImportRenamed.ts b/test/translation/transformation/modulesImportRenamed.ts deleted file mode 100644 index 74c82598b..000000000 --- a/test/translation/transformation/modulesImportRenamed.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { TestClass as RenamedClass } from "test"; - -RenamedClass; diff --git a/test/translation/transformation/modulesImportRenamedSpecialChars.ts b/test/translation/transformation/modulesImportRenamedSpecialChars.ts deleted file mode 100644 index a200c07e9..000000000 --- a/test/translation/transformation/modulesImportRenamedSpecialChars.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TestClass as RenamedClass1 } from "kebab-module"; -import { TestClass as RenamedClass2 } from "dollar$module"; -import { TestClass as RenamedClass3 } from "singlequote'module"; -import { TestClass as RenamedClass4 } from "hash#module"; -import { TestClass as RenamedClass5 } from "space module"; - -RenamedClass1; -RenamedClass2; -RenamedClass3; -RenamedClass4; -RenamedClass5; diff --git a/test/translation/transformation/modulesImportWithoutFromClause.ts b/test/translation/transformation/modulesImportWithoutFromClause.ts deleted file mode 100644 index 08fabf934..000000000 --- a/test/translation/transformation/modulesImportWithoutFromClause.ts +++ /dev/null @@ -1 +0,0 @@ -import "test"; diff --git a/test/translation/transformation/modulesVariableExport.ts b/test/translation/transformation/modulesVariableExport.ts deleted file mode 100644 index d407b0602..000000000 --- a/test/translation/transformation/modulesVariableExport.ts +++ /dev/null @@ -1 +0,0 @@ -export const foo = "bar"; diff --git a/test/translation/transformation/modulesVariableNoExport.ts b/test/translation/transformation/modulesVariableNoExport.ts deleted file mode 100644 index 4f4b4c843..000000000 --- a/test/translation/transformation/modulesVariableNoExport.ts +++ /dev/null @@ -1 +0,0 @@ -const foo = "bar"; diff --git a/test/translation/transformation/topLevelVariables.ts b/test/translation/transformation/topLevelVariables.ts deleted file mode 100644 index 5782c4cf4..000000000 --- a/test/translation/transformation/topLevelVariables.ts +++ /dev/null @@ -1,11 +0,0 @@ -const obj = { value1: 1, value2: 2 }; -const value1 = obj.value1; -const { value2 } = obj; - -let noValueLet; -let obj2 = { value3: 1, value4: 2 }; -let value3 = obj2.value3; -let { value4 } = obj2; - -function fun1(): void {} -const fun2 = () => {}; diff --git a/test/translation/transformation/unusedDefaultWithNamespaceImport.ts b/test/translation/transformation/unusedDefaultWithNamespaceImport.ts deleted file mode 100644 index 5b66a4b22..000000000 --- a/test/translation/transformation/unusedDefaultWithNamespaceImport.ts +++ /dev/null @@ -1,2 +0,0 @@ -import def, * as x from "module"; -x; diff --git a/test/translation/transformation.spec.ts b/test/translation/translation.spec.ts similarity index 89% rename from test/translation/transformation.spec.ts rename to test/translation/translation.spec.ts index 7a5088fcb..e91a5c2b8 100644 --- a/test/translation/transformation.spec.ts +++ b/test/translation/translation.spec.ts @@ -3,7 +3,7 @@ import * as path from "path"; import * as tstl from "../../src"; import * as util from "../util"; -const fixturesPath = path.join(__dirname, "./transformation"); +const fixturesPath = path.join(__dirname, "__fixtures__"); const fixtures = fs .readdirSync(fixturesPath) .filter(f => path.extname(f) === ".ts") diff --git a/test/transpile/bundle/index.ts b/test/transpile/__fixtures__/bundle/index.ts similarity index 100% rename from test/transpile/bundle/index.ts rename to test/transpile/__fixtures__/bundle/index.ts diff --git a/test/transpile/bundle/otherFile.ts b/test/transpile/__fixtures__/bundle/otherFile.ts similarity index 100% rename from test/transpile/bundle/otherFile.ts rename to test/transpile/__fixtures__/bundle/otherFile.ts diff --git a/test/transpile/bundle/tsconfig.json b/test/transpile/__fixtures__/bundle/tsconfig.json similarity index 100% rename from test/transpile/bundle/tsconfig.json rename to test/transpile/__fixtures__/bundle/tsconfig.json diff --git a/test/transpile/directories/basic/src/lib/file.ts b/test/transpile/__fixtures__/directories/src/lib/file.ts similarity index 100% rename from test/transpile/directories/basic/src/lib/file.ts rename to test/transpile/__fixtures__/directories/src/lib/file.ts diff --git a/test/transpile/directories/basic/src/main.ts b/test/transpile/__fixtures__/directories/src/main.ts similarity index 100% rename from test/transpile/directories/basic/src/main.ts rename to test/transpile/__fixtures__/directories/src/main.ts diff --git a/test/transpile/resolve-plugin/cjs.js b/test/transpile/__fixtures__/load-config-import/cjs.js similarity index 100% rename from test/transpile/resolve-plugin/cjs.js rename to test/transpile/__fixtures__/load-config-import/cjs.js diff --git a/test/transpile/resolve-plugin/import.ts b/test/transpile/__fixtures__/load-config-import/import.ts similarity index 100% rename from test/transpile/resolve-plugin/import.ts rename to test/transpile/__fixtures__/load-config-import/import.ts diff --git a/test/transpile/resolve-plugin/transpiled-esm.js b/test/transpile/__fixtures__/load-config-import/transpiled-esm.js similarity index 100% rename from test/transpile/resolve-plugin/transpiled-esm.js rename to test/transpile/__fixtures__/load-config-import/transpiled-esm.js diff --git a/test/transpile/resolve-plugin/ts.ts b/test/transpile/__fixtures__/load-config-import/ts.ts similarity index 100% rename from test/transpile/resolve-plugin/ts.ts rename to test/transpile/__fixtures__/load-config-import/ts.ts diff --git a/test/transpile/plugins/arguments.ts b/test/transpile/__fixtures__/plugins/arguments.ts similarity index 95% rename from test/transpile/plugins/arguments.ts rename to test/transpile/__fixtures__/plugins/arguments.ts index f6aa2ba6e..d0553c7ea 100644 --- a/test/transpile/plugins/arguments.ts +++ b/test/transpile/__fixtures__/plugins/arguments.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import * as tstl from "../../../src"; +import * as tstl from "../../../../src"; interface Options { name: string; diff --git a/test/transpile/__fixtures__/plugins/getModuleId.ts b/test/transpile/__fixtures__/plugins/getModuleId.ts new file mode 100644 index 000000000..e01410355 --- /dev/null +++ b/test/transpile/__fixtures__/plugins/getModuleId.ts @@ -0,0 +1,14 @@ +import { createHash } from "crypto"; +import * as path from "path"; +import * as tstl from "../../../../src"; + +const plugin: tstl.Plugin = { + getModuleId: (module, compilation) => + createHash("sha1") + .update(module.source.toString()) + .update(path.relative(compilation.rootDir, module.fileName)) + .digest("hex"), +}; + +// eslint-disable-next-line import/no-default-export +export default plugin; diff --git a/test/transpile/__fixtures__/plugins/getResolvePlugins.ts b/test/transpile/__fixtures__/plugins/getResolvePlugins.ts new file mode 100644 index 000000000..cb58b4b89 --- /dev/null +++ b/test/transpile/__fixtures__/plugins/getResolvePlugins.ts @@ -0,0 +1,12 @@ +// @ts-expect-error Could not find a declaration file for module 'enhanced-resolve/lib/AliasPlugin'. +import * as AliasPlugin from "enhanced-resolve/lib/AliasPlugin"; +import * as tstl from "../../../../src"; + +const plugin: tstl.Plugin = { + getResolvePlugins: () => [ + new AliasPlugin("described-resolve", { name: "foo", alias: "/bar.ts" }, "internal-resolve"), + ], +}; + +// eslint-disable-next-line import/no-default-export +export default plugin; diff --git a/test/transpile/__fixtures__/plugins/printer.ts b/test/transpile/__fixtures__/plugins/printer.ts new file mode 100644 index 000000000..c0489acf8 --- /dev/null +++ b/test/transpile/__fixtures__/plugins/printer.ts @@ -0,0 +1,12 @@ +import { SourceNode } from "source-map"; +import * as tstl from "../../../../src"; + +const plugin: tstl.Plugin = { + printer(program, host, fileName, ...args) { + const result = new tstl.LuaPrinter(host, program, fileName).print(...args); + return new SourceNode(null, null, null, ["-- Plugin\n", result.toString()]); + }, +}; + +// eslint-disable-next-line import/no-default-export +export default plugin; diff --git a/test/transpile/plugins/visitor-super.ts b/test/transpile/__fixtures__/plugins/visitor-super.ts similarity index 91% rename from test/transpile/plugins/visitor-super.ts rename to test/transpile/__fixtures__/plugins/visitor-super.ts index 9a8bee5f1..42847562b 100644 --- a/test/transpile/plugins/visitor-super.ts +++ b/test/transpile/__fixtures__/plugins/visitor-super.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import * as tstl from "../../../src"; +import * as tstl from "../../../../src"; const plugin: tstl.Plugin = { visitors: { diff --git a/test/transpile/plugins/visitor.ts b/test/transpile/__fixtures__/plugins/visitor.ts similarity index 87% rename from test/transpile/plugins/visitor.ts rename to test/transpile/__fixtures__/plugins/visitor.ts index 507b9fa3d..f74610a74 100644 --- a/test/transpile/plugins/visitor.ts +++ b/test/transpile/__fixtures__/plugins/visitor.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import * as tstl from "../../../src"; +import * as tstl from "../../../../src"; const plugin: tstl.Plugin = { visitors: { diff --git a/test/transpile/project/api.d.ts b/test/transpile/__fixtures__/project/api.d.ts similarity index 100% rename from test/transpile/project/api.d.ts rename to test/transpile/__fixtures__/project/api.d.ts diff --git a/test/transpile/project/index.ts b/test/transpile/__fixtures__/project/index.ts similarity index 100% rename from test/transpile/project/index.ts rename to test/transpile/__fixtures__/project/index.ts diff --git a/test/transpile/project/otherFile.ts b/test/transpile/__fixtures__/project/otherFile.ts similarity index 100% rename from test/transpile/project/otherFile.ts rename to test/transpile/__fixtures__/project/otherFile.ts diff --git a/test/transpile/project/tsconfig.json b/test/transpile/__fixtures__/project/tsconfig.json similarity index 100% rename from test/transpile/project/tsconfig.json rename to test/transpile/__fixtures__/project/tsconfig.json diff --git a/test/transpile/transformers/fixtures.ts b/test/transpile/__fixtures__/transformers.ts similarity index 86% rename from test/transpile/transformers/fixtures.ts rename to test/transpile/__fixtures__/transformers.ts index c998d6efc..80c3a1ab7 100644 --- a/test/transpile/transformers/fixtures.ts +++ b/test/transpile/__fixtures__/transformers.ts @@ -1,7 +1,11 @@ import * as assert from "assert"; import * as ts from "typescript"; import * as tstl from "../../../src"; -import { visitAndReplace } from "./utils"; + +function visitAndReplace(context: ts.TransformationContext, node: T, visitor: ts.Visitor): T { + const visit: ts.Visitor = node => visitor(node) ?? ts.visitEachChild(node, visit, context); + return ts.visitNode(node, visit); +} export const program = (program: ts.Program, options: { value: any }): ts.TransformerFactory => checker(program.getTypeChecker(), options); diff --git a/test/transpile/__snapshots__/directories.spec.ts.snap b/test/transpile/__snapshots__/directories.spec.ts.snap index 901d885f4..3bf126239 100644 --- a/test/transpile/__snapshots__/directories.spec.ts.snap +++ b/test/transpile/__snapshots__/directories.spec.ts.snap @@ -1,41 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should be able to resolve ({"name": "baseurl", "options": [Object]}) 1`] = ` +exports[`should be able to resolve ({"outDir": "out", "rootDir": "src"}) 1`] = ` Array [ - "directories/baseurl/out/lualib_bundle.lua", - "directories/baseurl/out/src/lib/nested/file.lua", - "directories/baseurl/out/src/main.lua", + "directories/out/lib/file.lua", + "directories/out/lualib_bundle.lua", + "directories/out/main.lua", ] `; -exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 1`] = ` +exports[`should be able to resolve ({"outDir": "out"}) 1`] = ` Array [ - "directories/basic/src/lib/file.lua", - "directories/basic/src/lualib_bundle.lua", - "directories/basic/src/main.lua", + "directories/out/lib/file.lua", + "directories/out/lualib_bundle.lua", + "directories/out/main.lua", ] `; -exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 2`] = ` +exports[`should be able to resolve ({"rootDir": "src"}) 1`] = ` Array [ - "directories/basic/out/lib/file.lua", - "directories/basic/out/lualib_bundle.lua", - "directories/basic/out/main.lua", + "directories/src/lib/file.lua", + "directories/src/lualib_bundle.lua", + "directories/src/main.lua", ] `; -exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 3`] = ` +exports[`should be able to resolve ({}) 1`] = ` Array [ - "directories/basic/src/lib/file.lua", - "directories/basic/src/lualib_bundle.lua", - "directories/basic/src/main.lua", -] -`; - -exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 4`] = ` -Array [ - "directories/basic/out/lib/file.lua", - "directories/basic/out/lualib_bundle.lua", - "directories/basic/out/main.lua", + "directories/src/lib/file.lua", + "directories/src/lualib_bundle.lua", + "directories/src/main.lua", ] `; diff --git a/test/transpile/__snapshots__/plugins.spec.ts.snap b/test/transpile/__snapshots__/plugins.spec.ts.snap new file mode 100644 index 000000000..f3e6fe0c2 --- /dev/null +++ b/test/transpile/__snapshots__/plugins.spec.ts.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getModuleId 1`] = ` +" +local ____modules = {} +local ____moduleCache = {} +local ____originalRequire = require +local function require(file) + if ____moduleCache[file] then + return ____moduleCache[file] + end + if ____modules[file] then + ____moduleCache[file] = ____modules[file]() + return ____moduleCache[file] + else + if ____originalRequire then + return ____originalRequire(file) + else + error(\\"module '\\" .. file .. \\"' not found\\") + end + end +end +____modules = { +[\\"5d05566c99dac259f6ff5742a268d405157a0d7c\\"] = function() +local ____exports = {} +____exports.value = true +return ____exports + +end, +[\\"9124a2db32045398a4f097c567d1fe6fcb7ab900\\"] = function() +local ____exports = {} +do + local ____foo = require(\\"5d05566c99dac259f6ff5742a268d405157a0d7c\\") + local value = ____foo.value + ____exports.value = value +end +return ____exports + +end, +} +return require(\\"9124a2db32045398a4f097c567d1fe6fcb7ab900\\")" +`; + +exports[`printer 1`] = `"-- Plugin"`; diff --git a/test/transpile/__snapshots__/resolution.spec.ts.snap b/test/transpile/__snapshots__/resolution.spec.ts.snap new file mode 100644 index 000000000..da53915dc --- /dev/null +++ b/test/transpile/__snapshots__/resolution.spec.ts.snap @@ -0,0 +1,187 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`current directory index: modules 1`] = ` +"[\\"index\\"] = function() + +end, +[\\"main\\"] = function() +local ____exports = {} +require(\\"index\\") +return ____exports + +end," +`; + +exports[`entry point in nested directory: modules 1`] = ` +"[\\"module\\"] = function() + +end, +[\\"main\\"] = function() +local ____exports = {} +require(\\"module\\") +return ____exports + +end," +`; + +exports[`file in a sibling directory: modules 1`] = ` +"[\\"foo.bar\\"] = function() + +end, +[\\"main\\"] = function() +local ____exports = {} +require(\\"foo.bar\\") +return ____exports + +end," +`; + +exports[`mode: "lib" doesn't fail on unresolvable requests: modules 1`] = ` +"[\\"main\\"] = function() +local ____exports = {} +require(__TS__Resolve(\\"./module\\")) +return ____exports + +end," +`; + +exports[`not transpiled script file error: diagnostics 1`] = `"/main.ts(2,22): error TSTL: Resolved source file '/module.ts' is not a part of the project."`; + +exports[`not transpiled script file error: modules 1`] = ` +"[\\"main\\"] = function() +local ____exports = {} +local ____ = require(--[[ ./module ]] error(\\"Resolved source file '/module.ts' is not a part of the project.\\")) +____exports.value = ____.value +return ____exports + +end," +`; + +exports[`package resolution package with dependencies: modules 1`] = ` +"[\\"main\\"] = function() +local ____exports = {} +do + local ____lib = require(\\"node_modules.lib.index\\") + local value = ____lib.value + ____exports.value = value +end +return ____exports + +end, +[\\"node_modules.lib.index\\"] = function() +return require(\\"node_modules.lib2.index\\") +end, +[\\"node_modules.lib2.index\\"] = function() +return { value = true } +end," +`; + +exports[`package resolution package.json with exports: modules 1`] = ` +"[\\"main\\"] = function() +local ____exports = {} +do + local ____lib = require(\\"node_modules.lib.dist.index\\") + local value = ____lib.value + ____exports.value = value +end +return ____exports + +end, +[\\"node_modules.lib.dist.index\\"] = function() +return { value = true } +end," +`; + +exports[`package resolution package.json with versioned exports: modules 1`] = ` +"[\\"main\\"] = function() +local ____exports = {} +do + local ____lib = require(\\"node_modules.lib.dist.5__3\\") + local value = ____lib.value + ____exports.value = value +end +return ____exports + +end, +[\\"node_modules.lib.dist.5__3\\"] = function() +return { value = \\"5.3\\" } +end," +`; + +exports[`package resolution without package.json: modules 1`] = ` +"[\\"main\\"] = function() +local ____exports = {} +do + local ____lib = require(\\"node_modules.lib.index\\") + local value = ____lib.value + ____exports.value = value +end +return ____exports + +end, +[\\"node_modules.lib.index\\"] = function() +return { value = true } +end," +`; + +exports[`prioritize .lua files over external .ts: modules 1`] = ` +"[\\"main\\"] = function() +local ____exports = {} +require(\\"foo\\") +return ____exports + +end, +[\\"foo\\"] = function() + +end," +`; + +exports[`prioritize internal .ts files over .lua: modules 1`] = ` +"[\\"foo\\"] = function() + +end, +[\\"main\\"] = function() +local ____exports = {} +require(\\"foo\\") +return ____exports + +end," +`; + +exports[`resolution out of rootDir .ts file: diagnostics 1`] = `"src/main.ts(2,35): error TS6059: File 'module.ts' is not under 'rootDir' 'src'. 'rootDir' is expected to contain all source files."`; + +exports[`rootDir inference: modules 1`] = ` +"[\\"module\\"] = function() + +end, +[\\"main\\"] = function() +local ____exports = {} +require(\\"module\\") +return ____exports + +end," +`; + +exports[`sibling directory index: modules 1`] = ` +"[\\"foo.index\\"] = function() + +end, +[\\"main\\"] = function() +local ____exports = {} +require(\\"foo.index\\") +return ____exports + +end," +`; + +exports[`sibling file: modules 1`] = ` +"[\\"foo\\"] = function() + +end, +[\\"main\\"] = function() +local ____exports = {} +require(\\"foo\\") +return ____exports + +end," +`; diff --git a/test/transpile/bundle.spec.ts b/test/transpile/bundle.spec.ts index 21e9f153a..85b2554d5 100644 --- a/test/transpile/bundle.spec.ts +++ b/test/transpile/bundle.spec.ts @@ -1,12 +1,8 @@ -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"); +import { resolveFixture, transpileProjectResult } from "./run"; test("should transpile into one file", () => { - const { diagnostics, emittedFiles } = transpileProjectResult(inputProject); + const { diagnostics, emittedFiles } = transpileProjectResult(resolveFixture("bundle/tsconfig.json")); expect(diagnostics).not.toHaveDiagnostics(); expect(emittedFiles).toHaveLength(1); diff --git a/test/transpile/directories.spec.ts b/test/transpile/directories.spec.ts index 9141199e5..80d8e9d26 100644 --- a/test/transpile/directories.spec.ts +++ b/test/transpile/directories.spec.ts @@ -1,33 +1,24 @@ -import * as path from "path"; import * as ts from "typescript"; import * as tstl from "../../src"; -import { transpileFilesResult } from "./run"; +import { resolveFixture, transpileFilesResult } from "./run"; -interface DirectoryTestCase { - name: string; - options: tstl.CompilerOptions; -} +const projectRoot = resolveFixture("directories"); +test.each([{}, { outDir: "out" }, { rootDir: "src" }, { rootDir: "src", outDir: "out" }])( + "should be able to resolve (%p)", + tsconfigOptions => { + jest.spyOn(process, "cwd").mockReturnValue(projectRoot); -test.each([ - { name: "basic", options: {} }, - { 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); + const config = { + compilerOptions: { ...tsconfigOptions, types: [], skipLibCheck: true }, + tstl: { luaTarget: tstl.LuaTarget.LuaJIT, luaLibImport: tstl.LuaLibImportKind.Always }, + }; - const config = { - compilerOptions: { ...compilerOptions, types: [], skipLibCheck: true }, - tstl: { luaTarget: tstl.LuaTarget.LuaJIT, luaLibImport: tstl.LuaLibImportKind.Always }, - }; + const { fileNames, options } = tstl.updateParsedConfigFile( + ts.parseJsonConfigFileContent(config, ts.sys, projectRoot) + ); - const { fileNames, options } = tstl.updateParsedConfigFile( - ts.parseJsonConfigFileContent(config, ts.sys, projectPath) - ); - - const { diagnostics, emittedFiles } = transpileFilesResult(fileNames, options); - expect(diagnostics).not.toHaveDiagnostics(); - expect(emittedFiles.map(f => f.name).sort()).toMatchSnapshot(); -}); + const { diagnostics, emittedFiles } = transpileFilesResult(fileNames, options); + expect(diagnostics).not.toHaveDiagnostics(); + expect(emittedFiles.map(f => f.name).sort()).toMatchSnapshot(); + } +); 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/load-config-import.spec.ts b/test/transpile/load-config-import.spec.ts new file mode 100644 index 000000000..52df3cbd8 --- /dev/null +++ b/test/transpile/load-config-import.spec.ts @@ -0,0 +1,27 @@ +import { loadConfigImport } from "../../src/compiler/utils"; +import { resolveFixture } from "./run"; + +test("resolve relative module paths", () => { + const result = loadConfigImport("test", "test", resolveFixture("load-config-import"), "./ts.ts"); + expect(result).toMatchObject({ result: true }); +}); + +test("load .ts modules", () => { + const result = loadConfigImport("test", "test", __dirname, resolveFixture("load-config-import/ts.ts")); + expect(result).toMatchObject({ result: true }); +}); + +test("load CJS .js modules", () => { + const result = loadConfigImport("test", "test", __dirname, resolveFixture("load-config-import/cjs.js")); + expect(result).toMatchObject({ result: true }); +}); + +test("load transpiled ESM .js modules", () => { + const result = loadConfigImport("test", "test", __dirname, resolveFixture("load-config-import/transpiled-esm.js")); + expect(result).toMatchObject({ result: true }); +}); + +test('"import" option', () => { + const result = loadConfigImport("test", "test", __dirname, resolveFixture("load-config-import/import.ts"), "named"); + expect(result).toMatchObject({ result: true }); +}); diff --git a/test/transpile/plugins.spec.ts b/test/transpile/plugins.spec.ts new file mode 100644 index 000000000..1aa45e86f --- /dev/null +++ b/test/transpile/plugins.spec.ts @@ -0,0 +1,55 @@ +import * as util from "../util"; +import { resolveFixture } from "./run"; + +test("printer", () => { + util.testModule`` + .setOptions({ luaPlugins: [{ name: resolveFixture("plugins/printer.ts") }] }) + .expectLuaToMatchSnapshot(); +}); + +test("visitor", () => { + util.testFunction` + return false; + ` + .setOptions({ luaPlugins: [{ name: resolveFixture("plugins/visitor.ts") }] }) + .expectToEqual(true); +}); + +test("visitor using super", () => { + util.testFunction` + return "foo"; + ` + .setOptions({ luaPlugins: [{ name: resolveFixture("plugins/visitor-super.ts") }] }) + .expectToEqual("bar"); +}); + +test("getModuleId", () => { + util.testBundle` + export { value } from "./foo"; + ` + .addExtraFile("foo.ts", "export const value = true;") + .setOptions({ luaPlugins: [{ name: resolveFixture("plugins/getModuleId.ts") }] }) + .expectToEqual({ value: true }) + .expectLuaToMatchSnapshot(); +}); + +test("getResolvePlugins", () => { + util.testBundle` + export { value } from "foo"; + ` + .addExtraFile("bar.ts", "export const value = true;") + .setOptions({ + luaPlugins: [{ name: resolveFixture("plugins/getResolvePlugins.ts") }], + baseUrl: ".", + paths: { foo: ["bar"] }, + }) + .expectToEqual({ value: true }); +}); + +test("passing arguments", () => { + util.testFunction` + return {}; + ` + .setOptions({ luaPlugins: [{ name: resolveFixture("plugins/arguments.ts"), option: true }] }) + .expectToEqual({ name: resolveFixture("plugins/arguments.ts"), option: true }); +}); diff --git a/test/transpile/plugins/plugins.spec.ts b/test/transpile/plugins/plugins.spec.ts deleted file mode 100644 index e78cc4201..000000000 --- a/test/transpile/plugins/plugins.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as path from "path"; -import * as util from "../../util"; - -test("printer", () => { - util.testModule`` - .setOptions({ luaPlugins: [{ name: path.join(__dirname, "printer.ts") }] }) - .tap(builder => expect(builder.getMainLuaCodeChunk()).toMatch("Plugin")); -}); - -test("visitor", () => { - util.testFunction` - return false; - ` - .setOptions({ luaPlugins: [{ name: path.join(__dirname, "visitor.ts") }] }) - .expectToEqual(true); -}); - -test("visitor using super", () => { - util.testFunction` - return "foo"; - ` - .setOptions({ luaPlugins: [{ name: path.join(__dirname, "visitor-super.ts") }] }) - .expectToEqual("bar"); -}); - -test("passing arguments", () => { - util.testFunction` - return {}; - ` - .setOptions({ luaPlugins: [{ name: path.join(__dirname, "arguments.ts"), option: true }] }) - .expectToEqual({ name: path.join(__dirname, "arguments.ts"), option: true }); -}); diff --git a/test/transpile/plugins/printer.ts b/test/transpile/plugins/printer.ts deleted file mode 100644 index 7ae55ca01..000000000 --- a/test/transpile/plugins/printer.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as tstl from "../../../src"; - -const plugin: tstl.Plugin = { - printer(program, emitHost, fileName, ...args) { - const result = new tstl.LuaPrinter(emitHost, program, fileName).print(...args); - result.code = `-- Plugin\n${result.code}`; - return result; - }, -}; - -// eslint-disable-next-line import/no-default-export -export default plugin; diff --git a/test/transpile/project.spec.ts b/test/transpile/project.spec.ts index 6f262dada..2abbe4dd7 100644 --- a/test/transpile/project.spec.ts +++ b/test/transpile/project.spec.ts @@ -1,8 +1,7 @@ -import * as path from "path"; -import { transpileProjectResult } from "./run"; +import { resolveFixture, transpileProjectResult } from "./run"; test("should transpile", () => { - const { diagnostics, emittedFiles } = transpileProjectResult(path.join(__dirname, "project", "tsconfig.json")); + const { diagnostics, emittedFiles } = transpileProjectResult(resolveFixture("project/tsconfig.json")); expect(diagnostics).not.toHaveDiagnostics(); expect(emittedFiles).toMatchSnapshot(); }); diff --git a/test/transpile/resolution.spec.ts b/test/transpile/resolution.spec.ts new file mode 100644 index 000000000..126a90b28 --- /dev/null +++ b/test/transpile/resolution.spec.ts @@ -0,0 +1,247 @@ +import * as tstl from "../../src"; +import { createResolutionErrorDiagnostic } from "../../src/compiler/diagnostics"; +import * as util from "../util"; + +const requireRegex = /require\("(.*?)"\)/; +const expectToRequire = (expected: string): util.TapCallback => builder => { + const [, requiredPath] = builder.getMainLuaCodeChunk().match(requireRegex) ?? []; + expect(requiredPath).toBe(expected); +}; + +const expectModuleTableToMatchSnapshot: util.TapCallback = builder => { + builder.expectToHaveNoDiagnostics(); + const moduleTable = builder.getMainLuaCodeChunk().match(/(?<=\n)____modules = {\n(.+)\n}\nreturn [^\n]+$/s)?.[1]; + expect(moduleTable).not.toBeUndefined(); + expect(moduleTable).toMatchSnapshot("modules"); +}; + +test("sibling file", () => { + util.testBundle` + import "./foo"; + ` + .addExtraFile("foo.ts", "") + .tap(expectModuleTableToMatchSnapshot); +}); + +test("file in a sibling directory", () => { + util.testBundle` + import "./foo/bar.ts"; + ` + .addExtraFile("foo/bar.ts", "") + .tap(expectModuleTableToMatchSnapshot); +}); + +test("sibling directory index", () => { + util.testBundle` + import "./foo"; + ` + .addExtraFile("foo/index.ts", "") + .tap(expectModuleTableToMatchSnapshot); +}); + +test("current directory index", () => { + util.testBundle` + import "."; + ` + .addExtraFile("index.ts", "") + .tap(expectModuleTableToMatchSnapshot); +}); + +test("rootDir inference", () => { + util.testBundle` + import "./module"; + ` + .setMainFileName("src/main.ts") + .addExtraFile("src/module.ts", "") + .tap(expectModuleTableToMatchSnapshot); +}); + +test("entry point in nested directory", () => { + util.testBundle` + import "./module"; + ` + .setMainFileName("src/main.ts") + .addExtraFile("src/module.ts", "") + .setOptions({ rootDir: "src" }) + .tap(expectModuleTableToMatchSnapshot); +}); + +test("prioritize internal .ts files over .lua", () => { + util.testBundle` + import "./foo"; + ` + .addExtraFile("foo.ts", "") + .addRawFile("foo.lua", "") + .tap(expectModuleTableToMatchSnapshot); +}); + +test("prioritize .lua files over external .ts", () => { + util.testBundle` + import "./foo"; + ` + .addRawFile("foo.ts", "") + .addRawFile("foo.lua", "") + .tap(expectModuleTableToMatchSnapshot); +}); + +describe("resolution out of rootDir", () => { + test(".lua file", () => { + util.testBundle` + export { value } from "../module"; + ` + .setOptions({ rootDir: "src" }) + .setMainFileName("src/main.ts") + .addExtraFile("module.d.ts", "export declare const value: boolean;") + .addRawFile("module.lua", "return { value = true }") + + .tap(expectToRequire("_.module")) + .expectToEqual({ value: true }); + }); + + test(".ts file", () => { + util.testBundle` + export { value } from "../module"; + ` + .setOptions({ rootDir: "src" }) + .setMainFileName("src/main.ts") + .addExtraFile("module.ts", "export const value = true;") + + .tap(expectToRequire("_.module")) + .expectDiagnosticsToMatchSnapshot([6059], true) + .expectToEqual({ value: true }); + }); +}); + +describe("package resolution", () => { + test("without package.json", () => { + util.testBundle` + export { value } from 'lib'; + ` + .addExtraFile("node_modules/lib/index.d.ts", "export const value: boolean;") + .addRawFile("node_modules/lib/index.lua", "return { value = true }") + .tap(expectModuleTableToMatchSnapshot) + .expectToEqual({ value: true }); + }); + + test("package.json with exports", () => { + util.testBundle` + export { value } from 'lib'; + ` + .addExtraFile("node_modules/lib/index.d.ts", "export const value: boolean;") + .addRawFile("node_modules/lib/dist/index.lua", "return { value = true }") + .addRawFile("node_modules/lib/package.json", JSON.stringify({ exports: { lua: "./dist/index.lua" } })) + .tap(expectModuleTableToMatchSnapshot) + .expectToEqual({ value: true }); + }); + + // https://github.com/webpack/enhanced-resolve/issues/256 + test.skip("package.json with directory exports", () => { + util.testBundle` + export { value } from 'lib/foo'; + ` + .addExtraFile("node_modules/lib/index.d.ts", 'declare module "lib/foo" { export const value: boolean; }') + .addRawFile("node_modules/lib/dist/foo.lua", "return { value = true }") + .addRawFile("node_modules/lib/package.json", JSON.stringify({ exports: { "./": { lua: "./dist/" } } })) + .tap(expectModuleTableToMatchSnapshot) + .expectToEqual({ value: true }); + }); + + test("package.json with versioned exports", () => { + util.testBundle` + export { value } from 'lib'; + ` + .addExtraFile("node_modules/lib/index.d.ts", "export const value: boolean;") + .addRawFile("node_modules/lib/dist/5.3.lua", 'return { value = "5.3" }') + .addRawFile("node_modules/lib/dist/jit.lua", 'return { value = "jit" }') + .addRawFile( + "node_modules/lib/package.json", + JSON.stringify({ exports: { "lua:5.3": "./dist/5.3.lua", "lua:jit": "./dist/jit.lua" } }) + ) + .tap(expectModuleTableToMatchSnapshot) + .expectToEqual({ value: "5.3" }); + }); + + test("package with dependencies", () => { + util.testBundle` + export { value } from 'lib'; + ` + .addExtraFile("node_modules/lib/index.d.ts", "export const value: boolean;") + .addRawFile("node_modules/lib/index.lua", 'return require(__TS__Resolve("lib2"))') + .addRawFile("node_modules/lib2/index.lua", "return { value = true }") + .tap(expectModuleTableToMatchSnapshot) + .expectToEqual({ value: true }); + }); + + test.todo("symlink"); +}); + +test("not transpiled script file error", () => { + util.testBundle` + declare function require(this: void, path: string): any; + declare function __TS__Resolve(this: void, request: string): string; + + export const { value } = require(__TS__Resolve("./module")); + ` + .addRawFile("module.ts", "export const value = true;") + .expectDiagnosticsToMatchSnapshot([createResolutionErrorDiagnostic.code], true) + .tap(expectModuleTableToMatchSnapshot) + .expectToEqual(new util.ExecutionError("Resolved source file '/module.ts' is not a part of the project.")); +}); + +test.each([ + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" {} + `, + mainCode: 'import "fake";', + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" {} + `, + mainCode: 'import * as fake from "fake"; fake;', + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" { + export const x: number; + } + `, + mainCode: 'import { x } from "fake"; x;', + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" { + export const x: number; + } + + declare module "fake" { + export const y: number; + } + `, + mainCode: 'import { y } from "fake"; y;', + }, +])("@noResolution annotation (%p)", ({ declarationStatement, mainCode }) => { + util.testModule(mainCode) + .setMainFileName("src/main.ts") + .addExtraFile("module.d.ts", declarationStatement) + .tap(expectToRequire("fake")); +}); + +describe('mode: "lib"', () => { + test("doesn't fail on unresolvable requests", () => { + util.testBundle` + import "./module"; + ` + .addExtraFile("module.d.ts", "export const foo = true;") + .setOptions({ mode: tstl.CompilerMode.Lib }) + .expectToHaveNoDiagnostics() + .tap(expectModuleTableToMatchSnapshot); + }); + + test.todo('gets resolved with mode: "app" from different compilation'); +}); diff --git a/test/transpile/resolve-plugin/resolve-plugin.spec.ts b/test/transpile/resolve-plugin/resolve-plugin.spec.ts deleted file mode 100644 index 7b48bad8f..000000000 --- a/test/transpile/resolve-plugin/resolve-plugin.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as path from "path"; -import { resolvePlugin } from "../../../src/transpilation/utils"; - -test("resolve relative module paths", () => { - const result = resolvePlugin("test", "test", __dirname, "./ts.ts"); - expect(result).toMatchObject({ result: true }); -}); - -test("load .ts modules", () => { - const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "ts.ts")); - expect(result).toMatchObject({ result: true }); -}); - -test("load CJS .js modules", () => { - const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "cjs.js")); - expect(result).toMatchObject({ result: true }); -}); - -test("load transpiled ESM .js modules", () => { - const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "transpiled-esm.js")); - expect(result).toMatchObject({ result: true }); -}); - -test('"import" option', () => { - const result = resolvePlugin("test", "test", __dirname, path.join(__dirname, "import.ts"), "named"); - expect(result).toMatchObject({ result: true }); -}); diff --git a/test/transpile/run.ts b/test/transpile/run.ts index 8890d7a18..5e4d1aaeb 100644 --- a/test/transpile/run.ts +++ b/test/transpile/run.ts @@ -4,13 +4,15 @@ import * as tstl from "../../src"; import { parseConfigFileWithSystem } from "../../src/cli/tsconfig"; import { normalizeSlashes } from "../../src/utils"; +export const resolveFixture = (name: string) => path.resolve(__dirname, "__fixtures__", name); + export function transpileFilesResult(rootNames: string[], options: tstl.CompilerOptions) { options.skipLibCheck = true; options.types = []; const emittedFiles: ts.OutputFile[] = []; const { diagnostics } = tstl.transpileFiles(rootNames, options, (fileName, text, writeByteOrderMark) => { - const name = normalizeSlashes(path.relative(__dirname, fileName)); + const name = normalizeSlashes(path.relative(resolveFixture(""), fileName)); emittedFiles.push({ name, text, writeByteOrderMark }); }); diff --git a/test/transpile/transformers/transformers.spec.ts b/test/transpile/transformers.spec.ts similarity index 54% rename from test/transpile/transformers/transformers.spec.ts rename to test/transpile/transformers.spec.ts index e166198bd..254541a79 100644 --- a/test/transpile/transformers/transformers.spec.ts +++ b/test/transpile/transformers.spec.ts @@ -1,11 +1,13 @@ -import * as path from "path"; -import * as util from "../../util"; +import * as util from "../util"; +import { resolveFixture } from "./run"; + +const transformersFixture = resolveFixture("transformers.ts"); test("ignore language service plugins", () => { util.testFunction` return false; ` - .setOptions({ plugins: [{ name: path.join(__dirname, "types.ts") }] }) + .setOptions({ plugins: [{ name: transformersFixture }] }) .expectToEqual(false); }); @@ -13,13 +15,13 @@ test("default type", () => { util.testFunction` return false; ` - .setOptions({ plugins: [{ transform: path.join(__dirname, "fixtures.ts"), import: "program", value: true }] }) + .setOptions({ plugins: [{ transform: transformersFixture, import: "program", value: true }] }) .expectToEqual(true); }); test("transformer resolution error", () => { util.testModule`` - .setOptions({ plugins: [{ transform: path.join(__dirname, "error.ts") }] }) + .setOptions({ plugins: [{ transform: resolveFixture("transformers/error.ts") }] }) .expectToHaveDiagnostics(); }); @@ -28,9 +30,7 @@ describe("factory types", () => { util.testFunction` return false; ` - .setOptions({ - plugins: [{ transform: path.join(__dirname, "fixtures.ts"), type, import: type, value: true }], - }) + .setOptions({ plugins: [{ transform: transformersFixture, type, import: type, value: true }] }) .expectToEqual(true); }); }); diff --git a/test/transpile/transformers/utils.ts b/test/transpile/transformers/utils.ts deleted file mode 100644 index 3e1503ba0..000000000 --- a/test/transpile/transformers/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as ts from "typescript"; - -export function visitAndReplace(context: ts.TransformationContext, node: T, visitor: ts.Visitor): T { - const visit: ts.Visitor = node => visitor(node) ?? ts.visitEachChild(node, visit, context); - return ts.visitNode(node, visit); -} diff --git a/test/tsconfig.json b/test/tsconfig.json index de0b21bdd..77f05ec58 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -7,12 +7,5 @@ "paths": { "*": ["types/*"] } }, "include": [".", "../src"], - "exclude": [ - "translation/transformation", - "cli/errors", - "cli/watch", - "transpile/directories", - "transpile/outFile", - "../src/lualib" - ] + "exclude": ["translation/__fixtures__", "cli/__fixtures__", "transpile/__fixtures__/directories", "../src/lualib"] } diff --git a/test/unit/__snapshots__/modules.spec.ts.snap b/test/unit/__snapshots__/modules.spec.ts.snap new file mode 100644 index 000000000..766491de9 --- /dev/null +++ b/test/unit/__snapshots__/modules.spec.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`import local identifier generation (".dot"): local identifier 1`] = `"_____2Edot"`; + +exports[`import local identifier generation ("_̀ः٠‿"): local identifier 1`] = `"______300_903_660_203F"`; + +exports[`import local identifier generation ("dollar$"): local identifier 1`] = `"____dollar_24"`; + +exports[`import local identifier generation ("hash#"): local identifier 1`] = `"____hash_23"`; + +exports[`import local identifier generation ("ke-bab"): local identifier 1`] = `"____ke_2Dbab"`; + +exports[`import local identifier generation ("s p a c e"): local identifier 1`] = `"____s_20p_20a_20c_20e"`; + +exports[`import local identifier generation ("singlequote'"): local identifier 1`] = `"____singlequote_27"`; + +exports[`import local identifier generation ("ɥɣɎɌͼƛಠ"): local identifier 1`] = `"_____265_263_24E_24C_37C_19B_CA0"`; diff --git a/test/unit/assignments.spec.ts b/test/unit/assignments.spec.ts index d2d80f08b..d8d360517 100644 --- a/test/unit/assignments.spec.ts +++ b/test/unit/assignments.spec.ts @@ -1,23 +1,32 @@ import { unsupportedVarDeclaration } from "../../src/transformation/utils/diagnostics"; import * as util from "../util"; -test.each(["const", "let"])("%s declaration not top-level is not global", declarationKind => { - util.testModule` - { - ${declarationKind} foo = true; - } - // @ts-ignore - return "foo" in globalThis; - `.expectToEqual(false); -}); +describe("global scoping", () => { + test.each(["const", "let"])("top-level %s declaration in module is not global", declarationKind => { + util.testModule` + ${declarationKind} foo = true + export const result = "foo" in globalThis; + `.expectToMatchJsResult(); + }); -test.each(["const", "let"])("top-level %s declaration is global", declarationKind => { - util.testBundle` - import './a'; - export const result = foo; - ` - .addExtraFile("a.ts", `${declarationKind} foo = true;`) - .expectToEqual({ result: true }); + test.each(["const", "let"])("top-level %s declaration in script is global", declarationKind => { + util.testBundle` + import './script'; + export const result = foo; + ` + .addExtraFile("script.ts", `${declarationKind} foo = true;`) + .expectToEqual({ result: true }); + }); + + test.each(["const", "let"])("%s declaration not top-level is not global", declarationKind => { + util.testModule` + { + ${declarationKind} foo = true; + } + // @ts-ignore + return "foo" in globalThis; + `.expectToEqual(false); + }); }); describe("var is disallowed", () => { @@ -415,7 +424,7 @@ test.each([ * x.y ||= z is translated to x.y || (x.y = z). * x.y &&= z is translated to x.y && (x.y = z). * x.y ||= z is translated to x.y !== undefined && (x.y = z). - + Test if setter in Lua is called same nr of times as in JS. */ util.testModule` @@ -447,7 +456,7 @@ test.each([ * x.y ||= z is translated to x.y || (x.y = z). * x.y &&= z is translated to x.y && (x.y = z). * x.y ||= z is translated to x.y !== undefined && (x.y = z). - + Test if setter in Lua is called same nr of times as in JS. */ util.testModule` diff --git a/test/unit/bundle.spec.ts b/test/unit/bundle.spec.ts index d28f4bf25..515e2578a 100644 --- a/test/unit/bundle.spec.ts +++ b/test/unit/bundle.spec.ts @@ -1,5 +1,5 @@ import { LuaLibImportKind } from "../../src"; -import * as diagnosticFactories from "../../src/transpilation/diagnostics"; +import * as diagnosticFactories from "../../src/compiler/diagnostics"; import * as util from "../util"; test("import module -> main", () => { diff --git a/test/unit/functions/noImplicitSelfOption.spec.ts b/test/unit/functions/noImplicitSelfOption.spec.ts index f61e37cb2..52f5ae35a 100644 --- a/test/unit/functions/noImplicitSelfOption.spec.ts +++ b/test/unit/functions/noImplicitSelfOption.spec.ts @@ -32,8 +32,8 @@ test("generates declaration files with @noSelfInFile", () => { util.assert(fooDeclaration !== undefined); util.testModule` - import { bar } from "./foo.d"; - const test: (this: void) => void = bar; + import type { bar } from "./foo"; + const test: (this: void) => void = undefined as typeof bar; ` .addExtraFile("foo.d.ts", fooDeclaration) .expectToHaveNoDiagnostics(); diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts index ba2bcd8a0..a5aee7a93 100644 --- a/test/unit/identifiers.spec.ts +++ b/test/unit/identifiers.spec.ts @@ -455,18 +455,13 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => { }); test("variable (require)", () => { - const code = ` - const require = "foobar"; - export { foo } from "someModule"; - export const result = require;`; - - const lua = ` - package.loaded.someModule = {foo = "bar"} - return (function() - ${util.transpileString(code, undefined, true)} - end)().result`; - - expect(util.executeLua(lua)).toBe("foobar"); + util.testBundle` + const require = true; + import "./module"; + export const result = require; + ` + .addExtraFile("module.ts", "") + .expectToEqual({ result: true }); }); test("variable (tostring)", () => { @@ -575,19 +570,14 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => { expect(util.transpileAndExecute(code)).toBe("foobar|string"); }); - test.each(["type", "type as type"])("imported variable (%p)", importName => { - const luaHeader = ` - package.loaded.someModule = {type = "foobar"}`; - - const code = ` - import {${importName}} from "someModule"; - export const result = typeof 7 + "|" + type; - `; - - const lua = util.transpileString(code); - const result = util.executeLua(`${luaHeader} return (function() ${lua} end)().result`); - - expect(result).toBe("number|foobar"); + test.each(["type", "type as type"])("imported variable (%p)", importSpecifier => { + util.testBundle` + import { ${importSpecifier} } from "./module"; + typeof 0; + export const result = type; + ` + .addExtraFile("module.ts", "export const type = true;") + .expectToEqual({ result: true }); }); test.each([ @@ -605,16 +595,12 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => { }); test.each(["type", "type as type"])("re-exported variable with lua keyword as name (%p)", importName => { - const code = ` - export { ${importName} } from "someModule"`; - - const lua = ` - package.loaded.someModule = {type = "foobar"} - return (function() - ${util.transpileString(code)} - end)().type`; - - expect(util.executeLua(lua)).toBe("foobar"); + util.testBundle` + export { ${importName} } from "./module"; + typeof 0; + ` + .addExtraFile("module.ts", "export const type = true") + .expectToEqual({ type: true }); }); test("class", () => { diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts new file mode 100644 index 000000000..8909a8143 --- /dev/null +++ b/test/unit/modules.spec.ts @@ -0,0 +1,348 @@ +import * as ts from "typescript"; +import * as util from "../util"; + +test("export const value", () => { + util.testModule` + export const value = true; + `.expectToMatchJsResult(); +}); + +describe("export default", () => { + test("literal", () => { + util.testModule` + export default true; + `.expectToMatchJsResult(); + }); + + test("class", () => { + util.testModule` + export default class Default {} + const d = new Default(); + export const result = d.constructor.name; + ` + .setReturnExport("result") + .expectToMatchJsResult(); + }); + + test("function", () => { + util.testModule` + export default function defaultFunction() { + return true; + } + export const result = defaultFunction(); + ` + .setReturnExport("result") + .expectToMatchJsResult(); + }); +}); + +describe("export { ... }", () => { + test("export { value }", () => { + util.testModule` + const value = true; + export { value }; + `.expectToMatchJsResult(); + }); + + test("export { value as result }", () => { + util.testModule` + const value = true; + export { value as result }; + `.expectToMatchJsResult(); + }); + + test("export { value as default }", () => { + util.testModule` + const value = true; + export { value as default }; + `.expectToMatchJsResult(); + }); +}); + +describe("export ... from", () => { + test("export { value } from '...'", () => { + util.testBundle` + export { value } from "./module"; + ` + .addExtraFile("module.ts", "export const value = true;") + .expectToEqual({ value: true }); + }); + + test("export { value as result } from '...'", () => { + util.testBundle` + export { value as result } from "./module"; + ` + .addExtraFile("module.ts", "export const value = true;") + .expectToEqual({ result: true }); + }); + + test("export { value as result1, value as result2 } from '...'", () => { + util.testBundle` + export { value as result1, value as result2 } from "./module"; + ` + .addExtraFile("module.ts", "export const value = true;") + .expectToEqual({ result1: true, result2: true }); + }); + + test("export { default } from '...'", () => { + util.testBundle` + export { default } from "./module"; + ` + .addExtraFile("module.ts", "export default true;") + .expectToEqual({ default: true }); + }); + + test("export * from '...'", () => { + util.testBundle` + export * from "./module"; + ` + .addExtraFile( + "module.ts", + ` + export const a = "a"; + export const b = "b"; + export default "default"; + ` + ) + // TODO: Doesn't match JS + .expectToEqual({ a: "a", b: "b", default: "default" }); + }); +}); + +describe("export live bindings", () => { + const reassignmentTestCases = [ + "x = 1", + "x++", + "(x = 1)", + "[x] = [1]", + "[[x]] = [[1]]", + "({ x } = { x: 1 })", + "({ y: x } = { y: 1 })", + "({ x = 1 } = { x: undefined })", + ]; + + // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/926 + test.each(reassignmentTestCases.filter(c => !c.includes(" } = { x: ")))("export variable (%p)", reassignment => { + util.testModule` + export let x = 0; + ${reassignment}; + `.expectToMatchJsResult(); + }); + + test.each(reassignmentTestCases)("export variable as a binding (%p)", reassignment => { + util.testModule` + let x = 0; + export { x }; + ${reassignment}; + `.expectToMatchJsResult(); + }); + + test.each(reassignmentTestCases)("export variable with multiple bindings (%p)", reassignment => { + util.testModule` + let x = 0; + export { x as a }; + export { x as b }; + ${reassignment}; + `.expectToMatchJsResult(); + }); + + // Can't be added to reassignmentTestCases because of https://github.com/microsoft/TypeScript/issues/35881 + test("export variable (for in loop)", () => { + util.testModule` + export let foo = ''; + for (foo in { x: true }) {} + ` + .setReturnExport("foo") + .expectToMatchJsResult(); + }); + + test("export variable as a binding (for in loop)", () => { + util.testModule` + let foo = ''; + export { foo as bar }; + for (foo in { x: true }) {} + ` + .setReturnExport("bar") + .expectToEqual("x"); + }); + + test("does not update shadowed names", () => { + util.testModule` + export let a = 1; + { let a = 2; a = 3 }; + `.expectToMatchJsResult(); + }); + + test("renamed export specifier shouldn't effect local vars", () => { + util.testModule` + let x = false; + export { x as a }; + let a = 5; + a = 6; + `.expectToMatchJsResult(); + }); +}); + +describe("import ...", () => { + test("import { value }", () => { + util.testBundle` + import { value } from "./module"; + export const result = value; + ` + .addExtraFile("module.ts", "export const value = true;") + .expectToEqual({ result: true }); + }); + + test("import { value as x }", () => { + util.testBundle` + import { value as x } from "./module"; + export const result = x; + ` + .addExtraFile("module.ts", "export const value = true;") + .expectToEqual({ result: true }); + }); + + test("import * as ns", () => { + util.testBundle` + import * as ns from "./module"; + export { ns } + ` + .addExtraFile( + "module.ts", + ` + export const a = "a"; + export const b = "b"; + export default "default"; + ` + ) + .expectToEqual({ ns: { a: "a", b: "b", default: "default" } }); + }); + + test("import defaultValue", () => { + util.testBundle` + import defaultValue from "./module"; + export const result = defaultValue; + ` + .addExtraFile("module.ts", "export default true;") + .expectToEqual({ result: true }); + }); + + test('import "..."', () => { + util.testBundle` + import { state } from "./state"; + import "./module"; + export const result = state.loaded; + ` + .addExtraFile( + "module.ts", + ` + import { state } from "./state"; + state.loaded = true; + ` + ) + .addExtraFile("state.ts", "export const state = { loaded: false };") + .expectToEqual({ result: true }); + }); +}); + +test("export =", () => { + util.testModule` + export = true; + ` + .setOptions({ module: ts.ModuleKind.CommonJS }) + .expectToMatchJsResult(); +}); + +test("import =", () => { + util.testBundle` + import module = require("./module"); + export const result = module; + ` + .addRawFile("module.lua", "return true") + .addExtraFile("module.d.ts", "export = true;") + .setOptions({ module: ts.ModuleKind.CommonJS }) + .expectToEqual({ result: true }); +}); + +describe("import and export elision", () => { + const moduleDeclaration = ` + export type Type = string; + export const value: string; + export default value; + `; + + const expectToElideImport: util.TapCallback = builder => { + builder.addExtraFile("module.d.ts", moduleDeclaration).expectToHaveNoDiagnostics().expectNoExecutionError(); + }; + + test("should elide named type imports", () => { + util.testModule` + import { Type } from "./module"; + const foo: Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide named value imports used only as a type", () => { + util.testModule` + import { value } from "./module"; + const foo: typeof value = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide unused default imports", () => { + util.testModule` + import defaultValue from "./module"; + `.tap(expectToElideImport); + }); + + test("should elide mixed imports", () => { + util.testBundle` + import defaultValue, { value } from "./module"; + export const result = defaultValue; + ` + .addExtraFile("module.d.ts", moduleDeclaration) + .addRawFile("module.lua", "return { default = true }") + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("a = ")) + .expectToEqual({ result: true }); + }); + + test("should elide namespace imports with unused values", () => { + util.testModule` + import * as module from "./module"; + const foo: module.Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide `import =` declarations", () => { + util.testModule` + import module = require("./module"); + const foo: module.Type = "bar"; + ` + .setOptions({ module: ts.ModuleKind.CommonJS }) + .tap(expectToElideImport); + }); + + test("should elide type exports", () => { + util.testModule` + (globalThis as any).foo = true; + type foo = boolean; + export { foo }; + `.expectToEqual([]); + }); +}); + +test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌͼƛಠ", "_̀ः٠‿", ".dot"])( + "import local identifier generation (%p)", + name => { + util.testBundle` + import { foo } from "./${name}"; + export { foo }; + ` + .addExtraFile(`${name}.ts`, "export const foo = true;") + .expectToEqual({ foo: true }) + .tap(builder => { + const identifier = builder.getMainLuaCodeChunk().match(/local (.+) = require\(/)?.[1]; + expect(identifier).toMatchSnapshot("local identifier"); + }); + } +); diff --git a/test/unit/modules/__snapshots__/resolution.spec.ts.snap b/test/unit/modules/__snapshots__/resolution.spec.ts.snap deleted file mode 100644 index d4228141d..000000000 --- a/test/unit/modules/__snapshots__/resolution.spec.ts.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`doesn't resolve paths out of root dir: code 1`] = ` -"local ____exports = {} -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."`; diff --git a/test/unit/modules/modules.spec.ts b/test/unit/modules/modules.spec.ts deleted file mode 100644 index 885f24541..000000000 --- a/test/unit/modules/modules.spec.ts +++ /dev/null @@ -1,277 +0,0 @@ -import * as ts from "typescript"; -import * as util from "../../util"; - -describe("module import/export elision", () => { - const moduleDeclaration = ` - declare module "module" { - export type Type = string; - export declare const value: string; - } - `; - - const expectToElideImport: util.TapCallback = builder => { - builder.addExtraFile("module.d.ts", moduleDeclaration).setOptions({ module: ts.ModuleKind.CommonJS }); - expect(builder.getLuaExecutionResult()).not.toBeInstanceOf(util.ExecutionError); - }; - - test("should elide named type imports", () => { - util.testModule` - import { Type } from "module"; - const foo: Type = "bar"; - `.tap(expectToElideImport); - }); - - test("should elide named value imports used only as a type", () => { - util.testModule` - import { value } from "module"; - const foo: typeof value = "bar"; - `.tap(expectToElideImport); - }); - - test("should elide namespace imports with unused values", () => { - util.testModule` - import * as module from "module"; - const foo: module.Type = "bar"; - `.tap(expectToElideImport); - }); - - test("should elide `import =` declarations", () => { - util.testModule` - import module = require("module"); - const foo: module.Type = "bar"; - `.tap(expectToElideImport); - }); - - test("should elide type exports", () => { - util.testModule` - (globalThis as any).foo = true; - type foo = boolean; - export { foo }; - `.expectToEqual([]); - }); -}); - -test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])( - "Import module names with invalid lua identifier characters (%p)", - name => { - util.testModule` - import { foo } from "./${name}"; - export { foo }; - ` - .disableSemanticCheck() - .setLuaHeader('setmetatable(package.loaded, { __index = function() return { foo = "bar" } end })') - .setReturnExport("foo") - .expectToEqual("bar"); - } -); - -test.each(["export default value;", "export { value as default };"])("Export Default From (%p)", exportStatement => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - export { default } from "./module"; - `, - "module.ts": ` - export const value = true; - ${exportStatement}; - `, - }, - "default" - ); - - expect(result).toBe(true); -}); - -test("Default Import and Export Expression", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport from "./module"; - export const value = defaultExport; - `, - "module.ts": ` - export default 1 + 2 + 3; - `, - }, - "value" - ); - - expect(result).toBe(6); -}); - -test("Import and Export Assignment", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import * as m from "./module"; - export const value = m; - `, - "module.ts": ` - export = true; - `, - }, - "value" - ); - - expect(result).toBe(true); -}); - -test("Mixed Exports, Default and Named Imports", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport, { a, b, c } from "./module"; - export const value = defaultExport + b + c; - `, - "module.ts": ` - export const a = 1; - export const b = 2; - export const c = 3; - export default a; - `, - }, - "value" - ); - - expect(result).toBe(6); -}); - -test("Mixed Exports, Default and Namespace Import", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport, * as ns from "./module"; - export const value = defaultExport + ns.b + ns.c; - `, - "module.ts": ` - export const a = 1; - export const b = 2; - export const c = 3; - export default a; - `, - }, - "value" - ); - - expect(result).toBe(6); -}); - -test("Export Default Function", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport from "./module"; - export const value = defaultExport(); - `, - "module.ts": ` - export default function() { - return true; - } - `, - }, - "value" - ); - - expect(result).toBe(true); -}); - -test("Export Equals", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import * as module from "./module"; - export const value = module; - `, - "module.ts": ` - export = true; - `, - }, - "value" - ); - - expect(result).toBe(true); -}); - -const reassignmentTestCases = [ - "x = 1", - "x++", - "(x = 1)", - "[x] = [1]", - "[[x]] = [[1]]", - "({ x } = { x: 1 })", - "({ y: x } = { y: 1 })", - "({ x = 1 } = { x: undefined })", -]; - -test.each(reassignmentTestCases)("export specifier with reassignment afterwards (%p)", reassignment => { - util.testModule` - let x = 0; - export { x }; - ${reassignment}; - `.expectToMatchJsResult(); -}); - -test.each(reassignmentTestCases)("export specifier fork (%p)", reassignment => { - util.testModule` - let x = 0; - export { x as a }; - export { x as b }; - ${reassignment}; - `.expectToMatchJsResult(); -}); - -test("does not export shadowed identifiers", () => { - util.testModule` - export let a = 1; - { let a = 2; a = 3 }; - `.expectToMatchJsResult(); -}); - -test("export as specifier shouldn't effect local vars", () => { - util.testModule` - let x = false; - export { x as a }; - let a = 5; - a = 6; - `.expectToMatchJsResult(); -}); - -test("export modified in for in loop", () => { - util.testModule` - export let foo = ''; - for (foo in { x: true }) {} - ` - .setReturnExport("foo") - .expectToMatchJsResult(); -}); - -test("export dependency modified in for in loop", () => { - util.testModule` - let foo = ''; - export { foo as bar }; - for (foo in { x: true }) {} - ` - .setReturnExport("bar") - .expectToEqual("x"); -}); - -test("export default class with future reference", () => { - util.testModule` - export default class Default {} - const d = new Default(); - export const result = d.constructor.name; - ` - .setReturnExport("result") - .expectToMatchJsResult(); -}); - -test("export default function with future reference", () => { - util.testModule` - export default function defaultFunction() { - return true; - } - export const result = defaultFunction(); - ` - .setReturnExport("result") - .expectToMatchJsResult(); -}); diff --git a/test/unit/modules/resolution.spec.ts b/test/unit/modules/resolution.spec.ts deleted file mode 100644 index 541c28c56..000000000 --- a/test/unit/modules/resolution.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import * as ts from "typescript"; -import { unresolvableRequirePath } from "../../../src/transformation/utils/diagnostics"; -import * as util from "../../util"; - -const requireRegex = /require\("(.*?)"\)/; -const expectToRequire = (expected: string): util.TapCallback => builder => { - const [, requiredPath] = builder.getMainLuaCodeChunk().match(requireRegex) ?? []; - expect(requiredPath).toBe(expected); -}; - -test.each([ - { - filePath: "main.ts", - usedPath: "./folder/Module", - expected: "folder.Module", - options: { rootDir: "." }, - }, - { - filePath: "main.ts", - usedPath: "./folder/Module", - expected: "folder.Module", - options: { rootDir: "./" }, - }, - { - filePath: "src/main.ts", - usedPath: "./folder/Module", - expected: "src.folder.Module", - options: { rootDir: "." }, - }, - { - filePath: "main.ts", - usedPath: "folder/Module", - expected: "folder.Module", - options: { rootDir: ".", baseUrl: "." }, - }, - { - filePath: "main.ts", - usedPath: "folder/Module", - expected: "folder.Module", - options: { rootDir: "./", baseUrl: "." }, - }, - { - filePath: "src/main.ts", - usedPath: "./folder/Module", - expected: "folder.Module", - options: { rootDir: "src" }, - }, - { - filePath: "src/main.ts", - usedPath: "./folder/Module", - expected: "folder.Module", - options: { rootDir: "./src" }, - }, - { - filePath: "src/dir/main.ts", - usedPath: "../Module", - expected: "Module", - options: { rootDir: "./src" }, - }, - { - filePath: "src/dir/dir/main.ts", - usedPath: "../../dir/Module", - expected: "dir.Module", - options: { rootDir: "./src" }, - }, -])("resolve paths with baseUrl or rootDir (%p)", ({ filePath, usedPath, expected, options }) => { - util.testModule` - import * as module from "${usedPath}"; - module; - ` - .setMainFileName(filePath) - .setOptions(options) - .tap(expectToRequire(expected)); -}); - -test("doesn't resolve paths out of root dir", () => { - util.testModule` - import * as module from "../module"; - module; - ` - .setMainFileName("src/main.ts") - .setOptions({ rootDir: "./src" }) - .disableSemanticCheck() - .expectDiagnosticsToMatchSnapshot([unresolvableRequirePath.code]); -}); - -test.each([ - { - declarationStatement: ` - /** @noResolution */ - declare module "fake" {} - `, - mainCode: 'import "fake";', - expectedPath: "fake", - }, - { - declarationStatement: ` - /** @noResolution */ - declare module "fake" {} - `, - mainCode: 'import * as fake from "fake"; fake;', - expectedPath: "fake", - }, - { - declarationStatement: ` - /** @noResolution */ - declare module "fake" { - export const x: number; - } - `, - mainCode: 'import { x } from "fake"; x;', - expectedPath: "fake", - }, - { - declarationStatement: ` - /** @noResolution */ - declare module "fake" { - export const x: number; - } - - declare module "fake" { - export const y: number; - } - `, - mainCode: 'import { y } from "fake"; y;', - expectedPath: "fake", - }, -])("noResolution prevents any module path resolution behavior", ({ declarationStatement, mainCode, expectedPath }) => { - util.testModule(mainCode) - .setMainFileName("src/main.ts") - .addExtraFile("module.d.ts", declarationStatement) - .tap(expectToRequire(expectedPath)); -}); - -test("import = require", () => { - util.testModule` - import foo = require("./foo/bar"); - foo; - ` - .setOptions({ module: ts.ModuleKind.CommonJS }) - .tap(expectToRequire("foo.bar")); -}); diff --git a/test/unit/printer/sourcemaps.spec.ts b/test/unit/printer/sourcemaps.spec.ts index 84a6596a6..83dbaed8d 100644 --- a/test/unit/printer/sourcemaps.spec.ts +++ b/test/unit/printer/sourcemaps.spec.ts @@ -51,25 +51,23 @@ test.each([ }, { code: ` - // @ts-ignore - import { Foo } from "foo"; + import { Foo } from "./module"; Foo; `, assertPatterns: [ - { luaPattern: 'require("foo")', typeScriptPattern: '"foo"' }, + { luaPattern: 'require("module")', typeScriptPattern: '"./module"' }, { luaPattern: "Foo", typeScriptPattern: "Foo" }, ], }, { code: ` - // @ts-ignore - import * as Foo from "foo"; + import * as Foo from "./module"; Foo; `, assertPatterns: [ - { luaPattern: 'require("foo")', typeScriptPattern: '"foo"' }, + { luaPattern: 'require("module")', typeScriptPattern: '"./module"' }, { luaPattern: "Foo", typeScriptPattern: "Foo" }, ], }, @@ -144,7 +142,11 @@ test.each([ ], }, ])("Source map has correct mapping (%p)", async ({ code, assertPatterns }) => { - const file = util.testModule(code).expectToHaveNoDiagnostics().getMainLuaFileResult(); + const file = util + .testModule(code) + .addExtraFile("module.ts", "export const Foo = true;") + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); const consumer = await new SourceMapConsumer(file.luaSourceMap); for (const { luaPattern, typeScriptPattern } of assertPatterns) { diff --git a/test/util.ts b/test/util.ts index 560b0102f..91d6738f3 100644 --- a/test/util.ts +++ b/test/util.ts @@ -3,12 +3,13 @@ import * as nativeAssert from "assert"; import { lauxlib, lua, lualib, to_jsstring, to_luastring } from "fengari"; import * as fs from "fs"; import { stringify } from "javascript-stringify"; +import { Volume } from "memfs"; import * as path from "path"; import * as prettyFormat from "pretty-format"; import * as ts from "typescript"; import * as vm from "vm"; import * as tstl from "../src"; -import { createEmitOutputCollector } from "../src/transpilation/output-collector"; +import { createEmitOutputCollector, createVirtualProgram } from "../src/managed-api/utils"; export * from "./legacy-utils"; @@ -176,6 +177,13 @@ export abstract class TestBuilder { return this; } + private nativeFileSystem = false; + public useNativeFileSystem() { + expect(this.hasProgram).toBe(false); + this.nativeFileSystem = true; + return this; + } + private options: tstl.CompilerOptions = { luaTarget: tstl.LuaTarget.Lua53, noHeader: true, @@ -207,6 +215,17 @@ export abstract class TestBuilder { return this; } + protected getSourceFiles() { + return { ...this.extraFiles, [this.mainFileName]: this.getTsCode() }; + } + + private extraRawFiles: Record = {}; + public addRawFile(fileName: string, content: string): this { + expect(this.hasProgram).toBe(false); + this.extraRawFiles[fileName] = content; + return this; + } + private customTransformers?: ts.CustomTransformers; public setCustomTransformers(customTransformers?: ts.CustomTransformers): this { expect(this.hasProgram).toBe(false); @@ -224,14 +243,29 @@ export abstract class TestBuilder { @memoize public getProgram(): ts.Program { this.hasProgram = true; - return tstl.createVirtualProgram({ ...this.extraFiles, [this.mainFileName]: this.getTsCode() }, this.options); + const program = createVirtualProgram(this.getSourceFiles(), this.options); + // https://github.com/microsoft/TypeScript/issues/41020 + program.getCommonSourceDirectory(); + return program; } @memoize public getLuaResult(): tstl.TranspileVirtualProjectResult { - const program = this.getProgram(); const collector = createEmitOutputCollector(); - const { diagnostics: transpileDiagnostics } = new tstl.Transpiler().emit({ + const program = this.getProgram(); + + const host: tstl.CompilerHost = { ...ts.sys }; + if (!this.nativeFileSystem) { + const virtualFS = Volume.fromJSON({ ...this.extraRawFiles, ...this.getSourceFiles() }, "/"); + host.resolutionFileSystem = virtualFS; + host.getCurrentDirectory = () => "/"; + host.readFile = (fileName, encoding = "utf8") => + /[\\/]lualib[\\/]/.test(fileName) + ? ts.sys.readFile(fileName, encoding) + : (virtualFS.readFileSync(fileName, encoding) as string); + } + + const { diagnostics: emitDiagnostics } = new tstl.Compiler({ host }).emit({ program, customTransformers: this.customTransformers, writeFile: collector.writeFile, @@ -239,7 +273,7 @@ export abstract class TestBuilder { const diagnostics = ts.sortAndDeduplicateDiagnostics([ ...ts.getPreEmitDiagnostics(program), - ...transpileDiagnostics, + ...emitDiagnostics, ]); return { diagnostics: [...diagnostics], transpiledFiles: collector.files }; @@ -400,6 +434,12 @@ class BundleTestBuilder extends AccessorTestBuilder { this.setOptions({ luaBundle: "main.lua", luaBundleEntry: this.mainFileName }); } + public setMainFileName(mainFileName: string) { + super.setMainFileName(mainFileName); + this.setOptions({ luaBundleEntry: mainFileName }); + return this; + } + public setEntryPoint(fileName: string): this { return this.setOptions({ luaBundleEntry: fileName }); } diff --git a/tsconfig.json b/tsconfig.json index bf121bd20..3c41ff823 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2019", - "lib": ["es2019"], + "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"], "types": ["node"], "module": "commonjs", "experimentalDecorators": true,