diff --git a/.prettierignore b/.prettierignore index a9b94c668..fef924d04 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,5 @@ /dist /coverage -/test/compiler/testfiles/invalid_syntax.ts /test/translation/transformation/characterEscapeSequence.ts /src diff --git a/build_lualib.ts b/build_lualib.ts index 18fb24bf7..cca39f055 100644 --- a/build_lualib.ts +++ b/build_lualib.ts @@ -1,34 +1,27 @@ import * as fs from "fs"; import * as glob from "glob"; -import { compile } from "./src/Compiler"; -import { LuaLib as luaLib, LuaLibFeature } from "./src/LuaLib"; +import * as path from "path"; +import * as ts from "typescript"; +import * as tstl from "./src"; +import { LuaLib } from "./src/LuaLib"; -const bundlePath = "./dist/lualib/lualib_bundle.lua"; +const options: tstl.CompilerOptions = { + skipLibCheck: true, + types: [], + luaLibImport: tstl.LuaLibImportKind.None, + luaTarget: tstl.LuaTarget.Lua51, + noHeader: true, + outDir: path.join(__dirname, "./dist/lualib"), + rootDir: path.join(__dirname, "./src/lualib"), +}; -compile([ - "--skipLibCheck", - "--types", - "node", - "--luaLibImport", - "none", - "--luaTarget", - "5.1", - "--noHeader", - "--outDir", - "./dist/lualib", - "--rootDir", - "./src/lualib", - "--noHeader", - "true", - ...glob.sync("./src/lualib/**/*.ts"), -]); +// TODO: Check diagnostics +const { emitResult } = tstl.transpileFiles(glob.sync("./src/lualib/**/*.ts"), options); +emitResult.forEach(({ name, text }) => ts.sys.writeFile(name, text)); +const bundlePath = path.join(__dirname, "./dist/lualib/lualib_bundle.lua"); if (fs.existsSync(bundlePath)) { fs.unlinkSync(bundlePath); } -const features = Object.keys(LuaLibFeature).map( - lib => LuaLibFeature[lib as keyof typeof LuaLibFeature], -); -const bundle = luaLib.loadFeatures(features); -fs.writeFileSync(bundlePath, bundle); +fs.writeFileSync(bundlePath, LuaLib.loadFeatures(Object.values(tstl.LuaLibFeature))); diff --git a/jest.config.js b/jest.config.js index adedec88f..5a98e2b3d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,9 +3,15 @@ const isCI = require("is-ci"); /** @type {Partial} */ module.exports = { testMatch: ["**/test/**/*.spec.ts"], - collectCoverageFrom: ["/src/**/*", "!/src/lualib/**/*"], - watchPathIgnorePatterns: ["/watch\\.ts$"], + collectCoverageFrom: [ + "/src/**/*", + "!/src/lualib/**/*", + // https://github.com/facebook/jest/issues/5274 + "!/src/tstl.ts", + ], + watchPathIgnorePatterns: ["cli/watch/[^/]+$"], + setupFilesAfterEnv: ["/test/setup.ts"], testEnvironment: "node", testRunner: "jest-circus/runner", preset: "ts-jest", diff --git a/package-lock.json b/package-lock.json index dda947979..17805d8db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -764,9 +764,9 @@ "dev": true }, "@types/node": { - "version": "9.6.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.23.tgz", - "integrity": "sha512-d2SJJpwkiPudEQ3+9ysANN2Nvz4QJKUPoe/WL5zyQzI0RaEeZWH5K5xjvUIGszTItHQpFPdH+u51f6G/LkS8Cg==", + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.0.tgz", + "integrity": "sha512-rx29MMkRdVmzunmiA4lzBYJNnXsW/PhG4kMBy2ATsYaDjGGR75dCFEVVROKpNwlVdcUX3xxlghKQOeDPBJobng==", "dev": true }, "@types/stack-utils": { diff --git a/package.json b/package.json index 3c063871b..4bb4edff3 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "devDependencies": { "@types/glob": "^5.0.35", "@types/jest": "^24.0.11", - "@types/node": "^9.6.23", + "@types/node": "^11.13.0", "fengari": "^0.1.2", "glob": "^7.1.2", "jest": "^24.5.0", diff --git a/src/CommandLineParser.ts b/src/CommandLineParser.ts index b0b552e15..76d5c12ee 100644 --- a/src/CommandLineParser.ts +++ b/src/CommandLineParser.ts @@ -1,411 +1,242 @@ -import * as fs from "fs"; import * as path from "path"; import * as ts from "typescript"; import { CompilerOptions, LuaLibImportKind, LuaTarget } from "./CompilerOptions"; +import * as diagnostics from "./diagnostics"; -export type CLIParseResult = ParseResult; - -type ParseResult = - { isValid: true; result: T } - | { isValid: false, errorMessage: string}; - -type ArgumentParseResult = - { isValid: true; result: T; increment?: number } - | { isValid: false, errorMessage: string }; - -interface ParsedCommandLine extends ts.ParsedCommandLine { +export interface ParsedCommandLine extends ts.ParsedCommandLine { options: CompilerOptions; } -interface BaseCLIOption { - aliases: string[]; +interface CommandLineOptionBase { + name: string; + aliases?: string[]; describe: string; - type: string; } -interface CLIOption extends BaseCLIOption { - choices: T[]; - default: T; +interface CommandLineOptionOfEnum extends CommandLineOptionBase { + type: "enum"; + choices: string[]; +} + +interface CommandLineOptionOfBoolean extends CommandLineOptionBase { + type: "boolean"; } -const optionDeclarations: {[key: string]: CLIOption} = { - luaLibImport: { - choices: [LuaLibImportKind.Inline, LuaLibImportKind.Require, LuaLibImportKind.Always, LuaLibImportKind.None], - default: LuaLibImportKind.Inline, +type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfBoolean; +const optionDeclarations: CommandLineOption[] = [ + { + name: "luaLibImport", describe: "Specifies how js standard features missing in lua are imported.", type: "enum", - } as CLIOption, - luaTarget: { + choices: Object.values(LuaLibImportKind), + }, + { + name: "luaTarget", aliases: ["lt"], - choices: [LuaTarget.LuaJIT, LuaTarget.Lua53, LuaTarget.Lua52, LuaTarget.Lua51], - default: LuaTarget.LuaJIT, describe: "Specify Lua target version.", type: "enum", - } as CLIOption, - noHeader: { - default: false, + choices: Object.values(LuaTarget), + }, + { + name: "noHeader", describe: "Specify if a header will be added to compiled files.", type: "boolean", - } as CLIOption, - noHoisting: { - default: false, + }, + { + name: "noHoisting", describe: "Disables hoisting.", type: "boolean", - } as CLIOption, - sourceMapTraceback: { - default: false, + }, + { + name: "sourceMapTraceback", describe: "Applies the source map to show source TS files and lines in error tracebacks.", type: "boolean", - } as CLIOption, -}; - -export const { version } = require("../package.json"); - -const helpString = - `Version ${version}\n` + - "Syntax: tstl [options] [files...]\n\n" + - - "Examples: tstl path/to/file.ts [...]\n" + - " tstl -p path/to/tsconfig.json\n\n" + - - "In addition to the options listed below you can also pass options\n" + - "for the typescript compiler (For a list of options use tsc -h).\n" + - "Some tsc options might have no effect."; - -/** - * Parse the supplied arguments. - * The result will include arguments supplied via CLI and arguments from tsconfig. - */ -export function parseCommandLine(args: string[]): CLIParseResult -{ - let commandLine = ts.parseCommandLine(args); - - // Run diagnostics to check for invalid tsc options - const diagnosticsResult = runTsDiagnostics(commandLine); - if (diagnosticsResult.isValid === false) { - return diagnosticsResult; - } + }, +]; - // This will add TS and TSTL options from a tsconfig - const configResult = readTsConfig(commandLine); - if (configResult.isValid === true) { - commandLine = configResult.result; - } else { - return { isValid: false, errorMessage: configResult.errorMessage }; - } +export const version = `Version ${require("../package.json").version}`; - // Run diagnostics to check for invalid tsconfig - const diagnosticsResult2 = runTsDiagnostics(commandLine); - if (diagnosticsResult2.isValid === false) { - return diagnosticsResult2; - } +const helpString = ` +Syntax: tstl [options] [files...] - // Merge TSTL CLI options in (highest priority) will also set defaults if none specified - const tstlCLIResult = parseTSTLOptions(commandLine, args); - if (tstlCLIResult.isValid === true) { - commandLine = tstlCLIResult.result; - } else { - return { isValid: false, errorMessage: tstlCLIResult.errorMessage }; - } +Examples: tstl path/to/file.ts [...] + tstl -p path/to/tsconfig.json - if (commandLine.options.project && !commandLine.options.rootDir) { - commandLine.options.rootDir = path.dirname(commandLine.options.project); - } - - if (!commandLine.options.rootDir) { - commandLine.options.rootDir = process.cwd(); - } - - if (!commandLine.options.outDir) { - commandLine.options.outDir = commandLine.options.rootDir; - } - - return { isValid: true, result: commandLine as ParsedCommandLine }; -} +In addition to the options listed below you can also pass options +for the typescript compiler (For a list of options use tsc -h). +Some tsc options might have no effect. +`.trim(); export function getHelpString(): string { let result = helpString + "\n\n"; result += "Options:\n"; - for (const optionName in optionDeclarations) { - const option = optionDeclarations[optionName]; - const aliasStrings = option.aliases - ? option.aliases.map(a => "-" + a) - : []; - - const optionString = aliasStrings.concat(["--" + optionName]).join("|"); + for (const option of optionDeclarations) { + const aliasStrings = (option.aliases || []).map(a => "-" + a); + const optionString = aliasStrings.concat(["--" + option.name]).join("|"); - const parameterDescribe = option.choices - ? option.choices.join("|") - : option.type; + const valuesHint = option.type === "enum" ? option.choices.join("|") : option.type; + const spacing = " ".repeat(Math.max(1, 45 - optionString.length - valuesHint.length)); - const spacing = " ".repeat(Math.max(1, 45 - optionString.length - parameterDescribe.length)); - - result += `\n ${optionString} <${parameterDescribe}>${spacing}${option.describe}\n`; + result += `\n ${optionString} <${valuesHint}>${spacing}${option.describe}\n`; } return result; } -function readTsConfig(parsedCommandLine: ts.ParsedCommandLine): CLIParseResult -{ - const options = parsedCommandLine.options; - - // Load config - if (options.project) { - const findProjectPathResult = findConfigFile(options); - if (findProjectPathResult.isValid === true) { - options.project = findProjectPathResult.result; - } else { - return { isValid: false, errorMessage: findProjectPathResult.errorMessage }; - } - - const configPath = options.project; - const parsedJsonConfig = parseTsConfigFile(configPath, options); +export function updateParsedConfigFile(parsedConfigFile: ts.ParsedCommandLine): ParsedCommandLine { + let hasRootLevelOptions = false; + for (const key in parsedConfigFile.raw) { + const option = optionDeclarations.find(option => option.name === key); + if (!option) continue; - return parsedJsonConfig; + if (parsedConfigFile.raw.tstl === undefined) parsedConfigFile.raw.tstl = {}; + parsedConfigFile.raw.tstl[key] = parsedConfigFile.raw[key]; + hasRootLevelOptions = true; } - return { isValid: true, result: parsedCommandLine }; -} - -export function parseTsConfigFile(filePath: string, existingOptions?: ts.CompilerOptions): CLIParseResult { - const configContents = fs.readFileSync(filePath).toString(); - return parseTsConfigString(configContents, filePath, existingOptions); -} -export function parseTsConfigString( - tsConfigString: string, - configPath: string, - existingOptions?: ts.CompilerOptions -): CLIParseResult { - const configJson = ts.parseConfigFileTextToJson(configPath, tsConfigString); - const parsedJsonConfig = ts.parseJsonConfigFileContent( - configJson.config, - ts.sys, - path.dirname(configPath), - existingOptions - ); - - for (const key in parsedJsonConfig.raw) { - const option = optionDeclarations[key]; - if (option !== undefined) { - const value = readValue(parsedJsonConfig.raw[key], option.type, key); - if (option.choices) { - if (option.choices.indexOf(value) < 0) { - return { - isValid: false, - errorMessage: `Unknown ${key} value '${value}'.\nAccepted values: ${option.choices}`, - }; - } - } - // console.warn(`[Deprectated] TSTL options are moving to the luaConfig object. Adjust your tsconfig to ` - // + `look like { "compilerOptions": { }, "tstl": { } }`); - parsedJsonConfig.options[key] = value; + if (parsedConfigFile.raw.tstl) { + if (hasRootLevelOptions) { + parsedConfigFile.errors.push( + diagnostics.tstlOptionsAreMovingToTheTstlObject(parsedConfigFile.raw.tstl) + ); } - } - // Eventually we will only look for the tstl object for tstl options - if (parsedJsonConfig.raw.tstl) { - for (const key in parsedJsonConfig.raw.tstl) { - const option = optionDeclarations[key]; - if (option !== undefined) { - const value = readValue(parsedJsonConfig.raw.tstl[key], option.type, key); - if (option.choices) { - if (option.choices.indexOf(value) < 0) { - return { - isValid: false, - errorMessage: `Unknown ${key} value '${value}'.\nAccepted values: ${option.choices}`, - }; - } - } - - parsedJsonConfig.options[key] = value; + for (const key in parsedConfigFile.raw.tstl) { + const option = optionDeclarations.find(option => option.name === key); + if (!option) { + parsedConfigFile.errors.push(diagnostics.unknownCompilerOption(key)); + continue; } + + const { error, value } = readValue(option, parsedConfigFile.raw.tstl[key]); + if (error) parsedConfigFile.errors.push(error); + if (parsedConfigFile.options[key] === undefined) parsedConfigFile.options[key] = value; } } - return { isValid: true, result: parsedJsonConfig }; + return parsedConfigFile; } -function parseTSTLOptions(commandLine: ts.ParsedCommandLine, args: string[]): CLIParseResult { - const result: { [key: string]: string | boolean } = {}; - for (let i = 0; i < args.length; i++) { - if (args[i].startsWith("--")) { - const argumentName = args[i].substr(2); - const option = optionDeclarations[argumentName]; - if (option) { - const argumentResult = getArgumentValue(argumentName, i, args); - if (argumentResult.isValid === true) { - result[argumentName] = argumentResult.result; - // Skip value from being considered as option - i += argumentResult.increment !== undefined ? argumentResult.increment : 1; - } else { - return { isValid: false, errorMessage: argumentResult.errorMessage }; - } - } - } else if (args[i].startsWith("-")) { - const argument = args[i].substr(1); - let argumentName: string | undefined; - for (const key in optionDeclarations) { - if (optionDeclarations[key].aliases && optionDeclarations[key].aliases.indexOf(argument) >= 0) { - argumentName = key; - break; - } - } - - if (argumentName !== undefined) { - const argumentResult = getArgumentValue(argumentName, i, args); - if (argumentResult.isValid === true) { - result[argumentName] = argumentResult.result; - // Skip value from being considered as option - i += argumentResult.increment !== undefined ? argumentResult.increment : 1; - } else { - return { isValid: false, errorMessage: argumentResult.errorMessage }; - } - } - } - } - for (const option in result) { - commandLine.options[option] = result[option]; - } - // Add defaults if not set - const defaultOptions = getDefaultOptions(); - for (const option in defaultOptions) { - if (!commandLine.options[option]) { - commandLine.options[option] = defaultOptions[option]; - } - } - return { isValid: true, result: commandLine }; +export function parseCommandLine(args: string[]): ParsedCommandLine { + return updateParsedCommandLine(ts.parseCommandLine(args), args); } -function getArgumentValue( - argumentName: string, - argumentIndex: number, +function updateParsedCommandLine( + parsedCommandLine: ts.ParsedCommandLine, args: string[] -): ArgumentParseResult -{ - const option = optionDeclarations[argumentName]; - const argument = args[argumentIndex + 1]; - - if (option.type === "boolean" && (argument === undefined || argument.startsWith("-"))) { - // Set boolean arguments without supplied value to true - return { isValid: true, result: true, increment: 0 }; - } - - if (argument === undefined) { - return { isValid: false, errorMessage: `Missing value for parameter ${argumentName}`}; - } +): ParsedCommandLine { + for (let i = 0; i < args.length; i++) { + if (!args[i].startsWith("-")) continue; + + const isShorthand = !args[i].startsWith("--"); + const argumentName = args[i].substr(isShorthand ? 1 : 2); + const option = optionDeclarations.find(option => { + if (option.name.toLowerCase() === argumentName.toLowerCase()) return true; + if (isShorthand && option.aliases) { + return option.aliases.some(a => a.toLowerCase() === argumentName.toLowerCase()); + } - const value = readValue(argument, option.type, argumentName); + return false; + }); - if (option.choices) { - if (option.choices.indexOf(value) < 0) { - return { - isValid: false, - errorMessage: `Unknown ${argumentName} value '${value}'. Accepted values are: ${option.choices}`, - }; + if (option) { + // Ignore errors caused by tstl specific compiler options + const tsInvalidCompilerOptionErrorCode = 5023; + parsedCommandLine.errors = parsedCommandLine.errors.filter(err => { + return !( + err.code === tsInvalidCompilerOptionErrorCode && + String(err.messageText).endsWith(`'${args[i]}'.`) + ); + }); + + const { error, value, increment } = readCommandLineArgument(option, args[i + 1]); + if (error) parsedCommandLine.errors.push(error); + parsedCommandLine.options[option.name] = value; + i += increment; } } - return { isValid: true, result: value }; + return parsedCommandLine; } -function readValue(value: string | boolean, valueType: string, parameterName: string): string | boolean { - if (valueType === "boolean") { - return value === true || value === "true" || value === "t" - ? true - : false; - } else if (valueType === "enum") { - return value.toString().toLowerCase(); - } else { - return value; - } +interface CommandLineArgument extends ReadValueResult { + increment: number; } -function getDefaultOptions(): CompilerOptions { - const options: CompilerOptions = {}; - - for (const optionName in optionDeclarations) { - if (optionDeclarations[optionName].default !== undefined) { - options[optionName] = optionDeclarations[optionName].default; +function readCommandLineArgument(option: CommandLineOption, value: any): CommandLineArgument { + if (option.type === "boolean") { + if (value === "true" || value === "false") { + value = value === "true"; + } else { + // Set boolean arguments without supplied value to true + return { value: true, increment: 0 }; } + } else if (value === undefined) { + return { + error: diagnostics.compilerOptionExpectsAnArgument(option.name), + value: undefined, + increment: 0, + }; } - return options; + return { ...readValue(option, value), increment: 1 }; } -/** Check the current state of the ParsedCommandLine for errors */ -function runTsDiagnostics(commandLine: ts.ParsedCommandLine): ParseResult { - // Remove files that dont exist - commandLine.fileNames = commandLine.fileNames.filter(file => fs.existsSync(file) || fs.existsSync(file + ".ts")); - - const tsInvalidCompilerOptionErrorCode = 5023; - if (commandLine.errors.length !== 0) { - // Generate a list of valid option names and aliases - const optionNames: string[] = []; - for (const key of Object.keys(optionDeclarations)) { - optionNames.push(key); - const alias = optionDeclarations[key].aliases; - if (alias) { - if (typeof alias === "string") { - optionNames.push(alias); - } else { - optionNames.push(...alias); - } - } - } +interface ReadValueResult { + error?: ts.Diagnostic; + value: any; +} - for (const err of commandLine.errors) { - // Ignore errors caused by tstl specific compiler options - if (err.code === tsInvalidCompilerOptionErrorCode) { - let ignore = false; - for (const optionName of optionNames) { - if (err.messageText.toString().indexOf(optionName) !== -1) { - ignore = true; - break; - } - } - - if (!ignore) { - return { isValid: false, errorMessage: `error TS${err.code}: ${err.messageText}`}; - } +function readValue(option: CommandLineOption, value: unknown): ReadValueResult { + if (value === null) return { value }; + + switch (option.type) { + case "boolean": { + if (typeof value !== "boolean") { + return { + value: undefined, + error: diagnostics.compilerOptionRequiresAValueOfType(option.name, "boolean"), + }; } + + return { value }; } - } - return { isValid: true, result: true }; -} + case "enum": { + if (typeof value !== "string") { + return { + value: undefined, + error: diagnostics.compilerOptionRequiresAValueOfType(option.name, "string"), + }; + } -/** Find configFile, function from ts api seems to be broken? */ -export function findConfigFile(options: ts.CompilerOptions): ParseResult { - if (!options.project) { - return { isValid: false, errorMessage: `error no base path provided, could not find config.`}; - } - let configPath = options.project; - // If the project path is wrapped in double quotes, remove them - if (/^".*"$/.test(configPath)) { - configPath = configPath.substring(1, configPath.length - 1); - } - /* istanbul ignore if: Testing else part is not really possible via automated tests */ - if (!path.isAbsolute(configPath)) { - // TODO check if options.project can even contain non absolute paths - configPath = path.join(process.cwd(), configPath); - } - if (fs.statSync(configPath).isDirectory()) { - configPath = path.join(configPath, "tsconfig.json"); - } else if (fs.statSync(configPath).isFile() && path.extname(configPath) === ".ts") { - // Search for tsconfig upwards in directory hierarchy starting from the file path - const dir = path.dirname(configPath).split(path.sep); - for (let i = dir.length; i > 0; i--) { - const searchPath = dir.slice(0, i).join("/") + path.sep + "tsconfig.json"; - - // If tsconfig.json was found, stop searching - if (ts.sys.fileExists(searchPath)) { - configPath = searchPath; - break; + const normalizedValue = value.toLowerCase(); + if (option.choices && !option.choices.includes(normalizedValue)) { + const optionChoices = option.choices.join(", "); + return { + value: undefined, + error: diagnostics.argumentForOptionMustBe(`--${option.name}`, optionChoices), + }; } + + return { value: normalizedValue }; } } +} + +export function parseConfigFileWithSystem( + configFileName: string, + commandLineOptions?: CompilerOptions, + system = ts.sys +): ParsedCommandLine { + const parsedConfigFile = ts.parseJsonSourceFileConfigFileContent( + ts.readJsonConfigFile(configFileName, system.readFile), + system, + path.dirname(configFileName), + commandLineOptions, + configFileName + ); - return { isValid: true, result: configPath }; + return updateParsedConfigFile(parsedConfigFile); } diff --git a/src/Compiler.ts b/src/Compiler.ts deleted file mode 100644 index a053b44e2..000000000 --- a/src/Compiler.ts +++ /dev/null @@ -1,189 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; -import * as CommandLineParser from "./CommandLineParser"; -import { CompilerOptions, LuaLibImportKind, LuaTarget } from "./CompilerOptions"; -import { LuaTranspiler, TranspileResult } from "./LuaTranspiler"; - -export function compile(argv: string[]): void { - const parseResult = CommandLineParser.parseCommandLine(argv); - - if (parseResult.isValid === true) { - - if (parseResult.result.options.help) { - console.log(CommandLineParser.getHelpString()); - return; - } - - if (parseResult.result.options.version) { - console.log(CommandLineParser.version); - return; - } - - /* istanbul ignore if: tested in test/compiler/watchmode.spec with subproccess */ - if (parseResult.result.options.watch) { - watchWithOptions(parseResult.result.fileNames, parseResult.result.options); - } else { - compileFilesWithOptions(parseResult.result.fileNames, parseResult.result.options); - } - } else { - console.error(`Invalid CLI input: ${parseResult.errorMessage}`); - } -} - -/* istanbul ignore next: tested in test/compiler/watchmode.spec with subproccess */ -export function watchWithOptions(fileNames: string[], options: CompilerOptions): void { - const host = options.project !== undefined - ? ts.createWatchCompilerHost(options.project, options, ts.sys, ts.createSemanticDiagnosticsBuilderProgram) - : ts.createWatchCompilerHost(fileNames, options, ts.sys, ts.createSemanticDiagnosticsBuilderProgram); - - let fullRecompile = true; - host.afterProgramCreate = program => { - const transpiler = new LuaTranspiler(program.getProgram()); - let status = transpiler.reportErrors(); - - if (status === 0) { - if (fullRecompile) { - status = transpiler.emitFilesAndReportErrors(); - } else { - while (true) { - const currentFile = program.getSemanticDiagnosticsOfNextAffectedFile(); - if (!currentFile) { break; } - - if ("fileName" in currentFile.affected) { // test if currentFile.affected is `ts.SourceFile` - const fileStatus = transpiler.emitSourceFile(currentFile.affected); - status |= fileStatus; - } else { - for (const sourceFile of currentFile.affected.getSourceFiles()) { - const fileStatus = transpiler.emitSourceFile(sourceFile); - status |= fileStatus; - } - } - } - } - // do a full recompile after transpiler error. - fullRecompile = status !== 0; - } - - const errorDiagnostic: ts.Diagnostic = { - category: ts.DiagnosticCategory.Error, - code: 6194, - file: undefined, - length: 0, - messageText: "Found 0 errors. Watching for file changes.", - start: 0, - }; - if (status !== 0) { - errorDiagnostic.messageText = "Found Errors. Watching for file changes."; - errorDiagnostic.code = 6193; - } - - if (host.onWatchStatusChange) { - host.onWatchStatusChange(errorDiagnostic, host.getNewLine(), program.getCompilerOptions()); - } - }; - - if (options.project !== undefined) { - ts.createWatchProgram( - host as ts.WatchCompilerHostOfConfigFile - ); - } else { - ts.createWatchProgram( - host as ts.WatchCompilerHostOfFilesAndCompilerOptions - ); - } -} - -export function compileFilesWithOptions(fileNames: string[], options: CompilerOptions): void { - const program = ts.createProgram(fileNames, options); - - const transpiler = new LuaTranspiler(program); - - transpiler.emitFilesAndReportErrors(); -} - -const libCache: {[key: string]: ts.SourceFile} = {}; - -const defaultCompilerOptions: CompilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, -}; - -export function createStringCompilerProgram( - input: string | { [filename: string]: string }, - options: CompilerOptions = defaultCompilerOptions, - filePath = "file.ts" -): ts.Program { - const compilerHost = { - directoryExists: () => true, - fileExists: () => true, - getCanonicalFileName: (fileName: string) => fileName, - getCurrentDirectory: () => "", - getDefaultLibFileName: ts.getDefaultLibFileName, - getDirectories: () => [], - getNewLine: () => "\n", - - getSourceFile: (filename: string) => { - switch (typeof input) { - case "string": - if (filename === filePath) { - return ts.createSourceFile(filename, input, ts.ScriptTarget.Latest, false); - } - break; - case "object": - if (filename in input) { - return ts.createSourceFile(filename, input[filename], ts.ScriptTarget.Latest, false); - } - break; - } - - 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 undefined; - }, - - readFile: () => "", - - useCaseSensitiveFileNames: () => false, - // Don't write output - writeFile: () => undefined, - }; - const filePaths = typeof input === "string" ? [filePath] : Object.keys(input); - return ts.createProgram(filePaths, options, compilerHost); -} - -export function transpileString( - input: string | { [filename: string]: string }, - options: CompilerOptions = defaultCompilerOptions, - ignoreDiagnostics = false, - filePath = "file.ts" -): TranspileResult { - const program = createStringCompilerProgram(input, options, filePath); - - if (!ignoreDiagnostics) { - const diagnostics = ts.getPreEmitDiagnostics(program); - const typeScriptErrors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); - - if (typeScriptErrors.length > 0) { - typeScriptErrors.forEach(e => console.warn(e.messageText)); - throw new Error("Encountered invalid TypeScript."); - } - } - - const transpiler = new LuaTranspiler(program); - - const sourceFile = program.getSourceFile(filePath); - if (sourceFile !== undefined) { - return transpiler.transpileSourceFile(sourceFile); - } else { - throw new Error(`Could not find file ${filePath} in created program.`); - } -} diff --git a/src/Emit.ts b/src/Emit.ts new file mode 100644 index 000000000..16af6318b --- /dev/null +++ b/src/Emit.ts @@ -0,0 +1,78 @@ +import * as fs from "fs"; +import * as path from "path"; +import { CompilerOptions, LuaLibImportKind } from "./CompilerOptions"; +import { TranspiledFile } from "./Transpile"; + +const trimExt = (filePath: string) => filePath.slice(0, -path.extname(filePath).length); +const normalizeSlashes = (filePath: string) => filePath.replace(/\\/g, "/"); + +export interface OutputFile { + name: string; + text: string; +} + +let lualibContent: string; +export function emitTranspiledFiles( + options: CompilerOptions, + transpiledFiles: Map +): OutputFile[] { + let { rootDir, outDir, outFile, luaLibImport } = options; + + const configFileName = options.configFilePath as string | undefined; + // TODO: Use getCommonSourceDirectory + const baseDir = configFileName ? path.dirname(configFileName) : process.cwd(); + + rootDir = rootDir || baseDir; + outDir = outDir ? path.resolve(baseDir, outDir) : rootDir; + + const files: OutputFile[] = []; + for (const [fileName, { lua, sourceMap, declaration, declarationMap }] of transpiledFiles) { + let outPath = fileName; + if (outDir !== rootDir) { + outPath = path.resolve(outDir, path.relative(rootDir, fileName)); + } + + // change extension or rename to outFile + if (outFile) { + outPath = path.isAbsolute(outFile) ? outFile : path.resolve(baseDir, outFile); + } else { + outPath = trimExt(outPath) + ".lua"; + } + + outPath = normalizeSlashes(outPath); + + if (lua !== undefined) { + files.push({ name: outPath, text: lua }); + } + + if (sourceMap !== undefined && options.sourceMap) { + files.push({ name: outPath + ".map", text: sourceMap }); + } + + if (declaration !== undefined) { + files.push({ name: trimExt(outPath) + ".d.ts", text: declaration }); + } + + if (declarationMap !== undefined) { + files.push({ name: trimExt(outPath) + ".d.ts.map", text: declarationMap }); + } + } + + if (luaLibImport === LuaLibImportKind.Require || luaLibImport === LuaLibImportKind.Always) { + if (lualibContent === undefined) { + lualibContent = fs.readFileSync( + path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), + "utf8" + ); + } + + let outPath = path.resolve(rootDir, "lualib_bundle.lua"); + if (outDir !== rootDir) { + outPath = path.join(outDir, path.relative(rootDir, outPath)); + } + + files.push({ name: normalizeSlashes(outPath), text: lualibContent }); + } + + return files; +} diff --git a/src/LuaAST.ts b/src/LuaAST.ts index 6925fc885..d9983979e 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -1,8 +1,8 @@ // Simplified Lua AST based roughly on http://lua-users.org/wiki/MetaLuaAbstractSyntaxTree, // https://www.lua.org/manual/5.3/manual.html (9 – The Complete Syntax of Lua) and the TS AST implementation -// We can ellide a lot of nodes especially tokens and keyowords -// becasue we dont create the AST from text +// We can elide a lot of nodes especially tokens and keywords +// because we dont create the AST from text import * as ts from "typescript"; @@ -41,14 +41,14 @@ export enum SyntaxKind { TableIndexExpression, // Operators // Arithmetic - AdditionOperator, // Maybe use abreviations for those add, sub, mul ... - SubractionOperator, + AdditionOperator, // Maybe use abbreviations for those add, sub, mul ... + SubtractionOperator, MultiplicationOperator, DivisionOperator, FloorDivisionOperator, ModuloOperator, PowerOperator, - NegationOperator, // Unaray minus + NegationOperator, // Unary minus // Concat ConcatOperator, // Length @@ -87,7 +87,7 @@ export type BinaryBitwiseOperator = SyntaxKind.BitwiseAndOperator | SyntaxKind.B | SyntaxKind.BitwiseLeftShiftOperator; export type BinaryOperator = - SyntaxKind.AdditionOperator | SyntaxKind.SubractionOperator | SyntaxKind.MultiplicationOperator + SyntaxKind.AdditionOperator | SyntaxKind.SubtractionOperator | SyntaxKind.MultiplicationOperator | SyntaxKind.DivisionOperator | SyntaxKind.FloorDivisionOperator | SyntaxKind.ModuloOperator | SyntaxKind.PowerOperator | SyntaxKind.ConcatOperator | SyntaxKind.EqualityOperator | SyntaxKind.InequalityOperator | SyntaxKind.LessThanOperator | SyntaxKind.LessEqualOperator @@ -288,7 +288,7 @@ export function createAssignmentStatement( export interface IfStatement extends Statement { kind: SyntaxKind.IfStatement; - condtion: Expression; + condition: Expression; ifBlock: Block; elseBlock?: Block | IfStatement; } @@ -298,7 +298,7 @@ export function isIfStatement(node: Node): node is IfStatement { } export function createIfStatement( - condtion: Expression, + condition: Expression, ifBlock: Block, elseBlock?: Block | IfStatement, tsOriginal?: ts.Node, @@ -306,8 +306,8 @@ export function createIfStatement( ): IfStatement { const statement = createNode(SyntaxKind.IfStatement, tsOriginal, parent) as IfStatement; - setParent(condtion, statement); - statement.condtion = condtion; + setParent(condition, statement); + statement.condition = condition; setParent(ifBlock, statement); statement.ifBlock = ifBlock; setParent(ifBlock, statement); @@ -326,7 +326,7 @@ export function isIterationStatement(node: Node): node is IterationStatement { export interface WhileStatement extends IterationStatement { kind: SyntaxKind.WhileStatement; - condtion: Expression; + condition: Expression; } export function isWhileStatement(node: Node): node is WhileStatement { @@ -335,7 +335,7 @@ export function isWhileStatement(node: Node): node is WhileStatement { export function createWhileStatement( body: Block, - condtion: Expression, + condition: Expression, tsOriginal?: ts.Node, parent?: Node ): WhileStatement @@ -343,14 +343,14 @@ export function createWhileStatement( const statement = createNode(SyntaxKind.WhileStatement, tsOriginal, parent) as WhileStatement; setParent(body, statement); statement.body = body; - setParent(condtion, statement); - statement.condtion = condtion; + setParent(condition, statement); + statement.condition = condition; return statement; } export interface RepeatStatement extends IterationStatement { kind: SyntaxKind.RepeatStatement; - condtion: Expression; + condition: Expression; } export function isRepeatStatement(node: Node): node is RepeatStatement { @@ -359,7 +359,7 @@ export function isRepeatStatement(node: Node): node is RepeatStatement { export function createRepeatStatement( body: Block, - condtion: Expression, + condition: Expression, tsOriginal?: ts.Node, parent?: Node ): RepeatStatement @@ -367,8 +367,8 @@ export function createRepeatStatement( const statement = createNode(SyntaxKind.RepeatStatement, tsOriginal, parent) as RepeatStatement; setParent(body, statement); statement.body = body; - setParent(condtion, statement); - statement.condtion = condtion; + setParent(condition, statement); + statement.condition = condition; return statement; } @@ -745,7 +745,7 @@ export function createBinaryExpression( export interface ParenthesizedExpression extends Expression { kind: SyntaxKind.ParenthesizedExpression; - innerEpxression: Expression; + innerExpression: Expression; } export function isParenthesizedExpression(node: Node): node is ParenthesizedExpression { @@ -760,7 +760,7 @@ export function createParenthesizedExpression( { const expression = createNode(SyntaxKind.ParenthesizedExpression, tsOriginal, parent) as ParenthesizedExpression; setParent(innerExpression, expression); - expression.innerEpxression = innerExpression; + expression.innerExpression = innerExpression; return expression; } @@ -845,7 +845,7 @@ export function cloneIdentifier(identifier: Identifier, tsOriginal?: ts.Node): I return createIdentifier(identifier.text, tsOriginal, identifier.symbolId); } -export function createAnnonymousIdentifier(tsOriginal?: ts.Node, parent?: Node): Identifier { +export function createAnonymousIdentifier(tsOriginal?: ts.Node, parent?: Node): Identifier { const expression = createNode(SyntaxKind.Identifier, tsOriginal, parent) as Identifier; expression.text = "____"; return expression; diff --git a/src/LuaPrinter.ts b/src/LuaPrinter.ts index 118ae789b..40b05d8a2 100644 --- a/src/LuaPrinter.ts +++ b/src/LuaPrinter.ts @@ -10,10 +10,9 @@ import { TSHelper as tsHelper } from "./TSHelper"; type SourceChunk = string | SourceNode; export class LuaPrinter { - /* tslint:disable:object-literal-sort-keys */ private static operatorMap: {[key in tstl.Operator]: string} = { [tstl.SyntaxKind.AdditionOperator]: "+", - [tstl.SyntaxKind.SubractionOperator]: "-", + [tstl.SyntaxKind.SubtractionOperator]: "-", [tstl.SyntaxKind.MultiplicationOperator]: "*", [tstl.SyntaxKind.DivisionOperator]: "/", [tstl.SyntaxKind.FloorDivisionOperator]: "//", @@ -38,7 +37,6 @@ export class LuaPrinter { [tstl.SyntaxKind.BitwiseLeftShiftOperator]: "<<", [tstl.SyntaxKind.BitwiseNotOperator]: "~", }; - /* tslint:enable:object-literal-sort-keys */ private options: CompilerOptions; private currentIndent: string; @@ -117,20 +115,20 @@ export class LuaPrinter { let header = ""; - if (this.options.noHeader === undefined || this.options.noHeader === false) { + if (!this.options.noHeader) { header += `--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n`; } if (luaLibFeatures) { + const luaLibImport = this.options.luaLibImport || LuaLibImportKind.Inline; // Require lualib bundle - if ((this.options.luaLibImport === LuaLibImportKind.Require && luaLibFeatures.size > 0) - || this.options.luaLibImport === LuaLibImportKind.Always) + if ((luaLibImport === LuaLibImportKind.Require && luaLibFeatures.size > 0) + || luaLibImport === LuaLibImportKind.Always) { header += `require("lualib_bundle");\n`; } // Inline lualib features - else if (this.options.luaLibImport === LuaLibImportKind.Inline && luaLibFeatures.size > 0) - { + else if (luaLibImport === LuaLibImportKind.Inline && luaLibFeatures.size > 0) { header += "-- Lua Library inline imports\n"; header += LuaLib.loadFeatures(luaLibFeatures); } @@ -312,7 +310,7 @@ export class LuaPrinter { const prefix = isElseIf ? "elseif" : "if"; - chunks.push(this.indent(prefix + " "), this.printExpression(statement.condtion), " then\n"); + chunks.push(this.indent(prefix + " "), this.printExpression(statement.condition), " then\n"); this.pushIndent(); chunks.push(this.printBlock(statement.ifBlock)); @@ -338,7 +336,7 @@ export class LuaPrinter { private printWhileStatement(statement: tstl.WhileStatement): SourceNode { const chunks: SourceChunk[] = []; - chunks.push(this.indent("while "), this.printExpression(statement.condtion), " do\n"); + chunks.push(this.indent("while "), this.printExpression(statement.condition), " do\n"); this.pushIndent(); chunks.push(this.printBlock(statement.body)); @@ -358,7 +356,7 @@ export class LuaPrinter { chunks.push(this.printBlock(statement.body)); this.popIndent(); - chunks.push(this.indent("until "), this.printExpression(statement.condtion)); + chunks.push(this.indent("until "), this.printExpression(statement.condition)); return this.concatNodes(...chunks); } @@ -613,7 +611,7 @@ export class LuaPrinter { } private printParenthesizedExpression(expression: tstl.ParenthesizedExpression): SourceNode { - return this.createSourceNode(expression, ["(", this.printExpression(expression.innerEpxression), ")"]); + return this.createSourceNode(expression, ["(", this.printExpression(expression.innerExpression), ")"]); } private printCallExpression(expression: tstl.CallExpression): SourceNode { diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 4b236a734..c847186d3 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -43,12 +43,11 @@ export class LuaTransformer { "not", "or", "repeat", "return", "self", "then", "until", "while", ]); - private isStrict = true; + private isStrict: boolean; private luaTarget: LuaTarget; private checker: ts.TypeChecker; protected options: CompilerOptions; - protected program: ts.Program; private isModule = false; @@ -69,17 +68,16 @@ export class LuaTransformer { private readonly typeValidationCache: Map> = new Map>(); - public constructor(program: ts.Program, options: CompilerOptions) { + public constructor(protected program: ts.Program) { this.checker = program.getTypeChecker(); - this.options = options; - this.program = program; + this.options = program.getCompilerOptions(); this.isStrict = this.options.alwaysStrict !== undefined || (this.options.strict !== undefined && this.options.alwaysStrict !== false) || (this.isModule && this.options.target !== undefined && this.options.target >= ts.ScriptTarget.ES2015); - this.luaTarget = options.luaTarget || LuaTarget.LuaJIT; + this.luaTarget = this.options.luaTarget || LuaTarget.LuaJIT; this.setupState(); } @@ -945,7 +943,7 @@ export class LuaTransformer { } private transformClassInstanceFields( - classDeclarataion: ts.ClassLikeDeclaration, + classDeclaration: ts.ClassLikeDeclaration, instanceFields: ts.PropertyDeclaration[] ): tstl.Statement[] { @@ -966,8 +964,8 @@ export class LuaTransformer { statements.push(assignClassField); } - const getOverrides = classDeclarataion.members.filter(m => - tsHelper.isGetAccessorOverride(m, classDeclarataion, this.checker) + const getOverrides = classDeclaration.members.filter(m => + tsHelper.isGetAccessorOverride(m, classDeclaration, this.checker) ) as ts.GetAccessorDeclaration[]; for (const getter of getOverrides) { @@ -1020,7 +1018,7 @@ export class LuaTransformer { const declarationName = this.transformIdentifier(declaration.name as ts.Identifier); if (declaration.initializer) { // self.declarationName = declarationName or initializer - const assignement = tstl.createAssignmentStatement( + const assignment = tstl.createAssignmentStatement( tstl.createTableIndexExpression( this.createSelfIdentifier(), tstl.createStringLiteral(declarationName.text) ), @@ -1030,17 +1028,17 @@ export class LuaTransformer { tstl.SyntaxKind.OrOperator ) ); - bodyWithFieldInitializers.push(assignement); + bodyWithFieldInitializers.push(assignment); } else { // self.declarationName = declarationName - const assignement = tstl.createAssignmentStatement( + const assignment = tstl.createAssignmentStatement( tstl.createTableIndexExpression( this.createSelfIdentifier(), tstl.createStringLiteral(declarationName.text) ), declarationName ); - bodyWithFieldInitializers.push(assignement); + bodyWithFieldInitializers.push(assignment); } } @@ -1649,7 +1647,7 @@ export class LuaTransformer { //function(____, ...) const nextFunctionDeclaration = tstl.createFunctionExpression( tstl.createBlock(nextBody), - [tstl.createAnnonymousIdentifier()], + [tstl.createAnonymousIdentifier()], tstl.createDotsLiteral()); //____it = {next = function(____, ...)} @@ -1783,7 +1781,7 @@ export class LuaTransformer { table = this.transformIdentifier(statement.initializer); } else { // Contain the expression in a temporary variable - table = tstl.createAnnonymousIdentifier(); + table = tstl.createAnonymousIdentifier(); if (statement.initializer) { statements.push(tstl.createVariableDeclarationStatement( table, this.transformExpression(statement.initializer))); @@ -1802,7 +1800,7 @@ export class LuaTransformer { ? this.filterUndefinedAndCast( statement.name.elements.map(e => this.transformArrayBindingElement(e)), tstl.isIdentifier) - : tstl.createAnnonymousIdentifier(statement.name); + : tstl.createAnonymousIdentifier(statement.name); // Don't unpack TupleReturn decorated functions if (statement.initializer) { @@ -1904,7 +1902,7 @@ export class LuaTransformer { if (!ts.isCallLikeExpression(expression)) { // Assign expression statements to dummy to make sure they're legal lua return tstl.createVariableDeclarationStatement( - tstl.createAnnonymousIdentifier(), + tstl.createAnonymousIdentifier(), this.transformExpression(expression) ); } @@ -2634,7 +2632,7 @@ export class LuaTransformer { // Destructuring assignment const left = expression.left.elements.length > 0 ? expression.left.elements.map(e => this.transformExpression(e)) - : [tstl.createAnnonymousIdentifier(expression.left)]; + : [tstl.createAnonymousIdentifier(expression.left)]; let right: tstl.Expression[]; if (ts.isArrayLiteralExpression(expression.right)) { if (expression.right.elements.length > 0) { @@ -2682,7 +2680,7 @@ export class LuaTransformer { // (function() local ${tmps} = ${right}; ${left} = ${tmps}; return {${tmps}} end)() const left = expression.left.elements.length > 0 ? expression.left.elements.map(e => this.transformExpression(e)) - : [tstl.createAnnonymousIdentifier(expression.left)]; + : [tstl.createAnonymousIdentifier(expression.left)]; let right: tstl.Expression[]; if (ts.isArrayLiteralExpression(expression.right)) { right = expression.right.elements.length > 0 @@ -2878,7 +2876,7 @@ export class LuaTransformer { case ts.SyntaxKind.BarBarToken: return tstl.SyntaxKind.OrOperator; case ts.SyntaxKind.MinusToken: - return tstl.SyntaxKind.SubractionOperator; + return tstl.SyntaxKind.SubtractionOperator; case ts.SyntaxKind.PlusToken: if (ts.isBinaryExpression(node)) { // Check is we need to use string concat operator @@ -2919,7 +2917,7 @@ export class LuaTransformer { public transformClassExpression(expression: ts.ClassExpression): ExpressionVisitResult { const className = expression.name !== undefined ? this.transformIdentifier(expression.name) - : tstl.createAnnonymousIdentifier(); + : tstl.createAnonymousIdentifier(); const classDeclaration = this.transformClassDeclaration(expression, className); return this.createImmediatelyInvokedFunctionExpression( @@ -3242,7 +3240,7 @@ export class LuaTransformer { if (ts.isArrowFunction(node)) { // dummy context for arrow functions with parameters if (node.parameters.length > 0) { - context = tstl.createAnnonymousIdentifier(); + context = tstl.createAnonymousIdentifier(); } } else { // self context @@ -3871,7 +3869,7 @@ export class LuaTransformer { ) ), tstl.createNumericLiteral(1), - tstl.SyntaxKind.SubractionOperator, + tstl.SyntaxKind.SubtractionOperator, node ) ); @@ -4353,7 +4351,7 @@ export class LuaTransformer { if (expression.originalKeywordKind === ts.SyntaxKind.UndefinedKeyword) { return tstl.createIdentifier("nil"); // TODO this is a hack that allows use to keep Identifier // as return time as changing that would break a lot of stuff. - // But this should be changed to retun tstl.createNilLiteral() + // But this should be changed to return tstl.createNilLiteral() // at some point. } diff --git a/src/LuaTranspiler.ts b/src/LuaTranspiler.ts deleted file mode 100644 index 2f9a23ab6..000000000 --- a/src/LuaTranspiler.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; -import { CompilerOptions, LuaLibImportKind, LuaTarget } from "./CompilerOptions"; -import * as tstl from "./LuaAST"; -import { LuaPrinter } from "./LuaPrinter"; -import { LuaTransformer } from "./LuaTransformer"; - -export interface TranspileResult { - lua: string; - luaAST: tstl.Node; - sourceMap: string; -} - -export class LuaTranspiler { - private program: ts.Program; - - private options: CompilerOptions; - - private luaTransformer: LuaTransformer; - - private luaPrinter: LuaPrinter; - - constructor(program: ts.Program) { - this.program = program; - this.options = this.getOptions(program); - this.luaTransformer = new LuaTransformer(this.program, this.options); - this.luaPrinter = new LuaPrinter(this.options); - } - - private getOptions(program: ts.Program): CompilerOptions { - const options = program.getCompilerOptions() as CompilerOptions; - - // Make options case-insenstive - if (options.luaTarget) { - options.luaTarget = options.luaTarget.toLowerCase() as LuaTarget; - } - if (options.luaLibImport) { - options.luaLibImport = options.luaLibImport.toLocaleLowerCase() as LuaLibImportKind; - } - - return options; - } - - public reportErrors(): number { - // Get all diagnostics, ignore unsupported extension - const diagnostics = ts.getPreEmitDiagnostics(this.program).filter(diag => diag.code !== 6054); - diagnostics.forEach(diag => this.reportDiagnostic(diag)); - - // If there are errors dont emit - if (diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error).length > 0) { - if (!this.options.watch) { - process.exit(1); - } else { - return 1; - } - } - - return 0; - } - - public emitLuaLib(): string { - const outPath = path.join(this.options.outDir || "", "lualib_bundle.lua"); - fs.copyFileSync( - path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), - outPath - ); - return outPath; - } - - public emitFilesAndReportErrors(): number { - let status = this.reportErrors(); - - if (status > 0) { - return status; - } - - this.program.getSourceFiles().forEach(sourceFile => { - const sourceStatus = this.emitSourceFile(sourceFile); - status |= sourceStatus; - }); - - // Copy lualib to target dir - if (this.options.luaLibImport === LuaLibImportKind.Require - || this.options.luaLibImport === LuaLibImportKind.Always - ) { - this.emitLuaLib(); - } - - return status; - } - - public emitSourceFile(sourceFile: ts.SourceFile): number { - if (!sourceFile.isDeclarationFile) { - try { - const rootDir = this.options.rootDir || ""; - - const { lua, luaAST, sourceMap } = this.transpileSourceFile(sourceFile); - - let outPath = sourceFile.fileName; - if (this.options.outDir !== this.options.rootDir) { - const relativeSourcePath = path.resolve(sourceFile.fileName).replace(path.resolve(rootDir), ""); - outPath = path.join(this.options.outDir || "", relativeSourcePath); - } - - // change extension or rename to outFile - if (this.options.outFile) { - if (path.isAbsolute(this.options.outFile)) { - outPath = this.options.outFile; - } else { - // append to workingDir or outDir - outPath = path.resolve(this.options.outDir || "", this.options.outFile); - } - } else { - const fileNameLua = path.basename(outPath, path.extname(outPath)) + ".lua"; - outPath = path.join(path.dirname(outPath), fileNameLua); - } - - // Write output - ts.sys.writeFile(outPath, lua); - if (this.options.sourceMap) { - ts.sys.writeFile(outPath + ".map", sourceMap); - } - } catch (exception) { - /* istanbul ignore else: Testing else part would require to add a bug/exception to our code */ - if (exception.node) { - const pos = ts.getLineAndCharacterOfPosition(sourceFile, exception.node.pos); - // Graciously handle transpilation errors - console.error("Encountered error parsing file: " + exception.message); - console.error(`${sourceFile.fileName} (${1 + pos.line},${pos.character})\n${exception.stack}`); - return 1; - } else { - throw exception; - } - } - } - return 0; - } - - public transpileSourceFile(sourceFile: ts.SourceFile): TranspileResult { - // Transform AST - const [luaAST, lualibFeatureSet] = this.luaTransformer.transformSourceFile(sourceFile); - // Print AST - const [lua, sourceMap] = this.luaPrinter.print(luaAST, lualibFeatureSet, sourceFile.fileName); - - return { lua, luaAST, sourceMap }; - } - - public reportDiagnostic(diagnostic: ts.Diagnostic): void { - if (diagnostic.file) { - const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); - const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); - console.log(`${diagnostic.code}: ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); - } else { - console.log(`${diagnostic.code}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`); - } - } -} diff --git a/src/Transpile.ts b/src/Transpile.ts new file mode 100644 index 000000000..ee59af81d --- /dev/null +++ b/src/Transpile.ts @@ -0,0 +1,179 @@ +import * as ts from "typescript"; +import { CompilerOptions } from "./CompilerOptions"; +import { transpileError } from "./diagnostics"; +import { Block } from './LuaAST'; +import { LuaPrinter } from "./LuaPrinter"; +import { LuaTransformer } from "./LuaTransformer"; +import { TranspileError } from "./TranspileError"; + +function getCustomTransformers( + program: ts.Program, + customTransformers: ts.CustomTransformers, + onSourceFile: (sourceFile: ts.SourceFile) => void +): ts.CustomTransformers { + // TODO: https://github.com/Microsoft/TypeScript/issues/28310 + const forEachSourceFile = ( + node: ts.SourceFile, + callback: (sourceFile: ts.SourceFile) => ts.SourceFile + ) => + ts.isBundle(node) + ? ((ts.updateBundle(node, node.sourceFiles.map(callback)) as unknown) as ts.SourceFile) + : callback(node); + + const luaTransformer: ts.TransformerFactory = () => node => + forEachSourceFile(node, sourceFile => { + onSourceFile(sourceFile); + return ts.createSourceFile(sourceFile.fileName, "", ts.ScriptTarget.ESNext); + }); + + return { + afterDeclarations: customTransformers.afterDeclarations, + before: [ + ...(customTransformers.before || []), + ...(customTransformers.after || []), + luaTransformer, + ], + }; +} + +export interface TranspiledFile { + luaAst?: Block; + lua?: string; + sourceMap?: string; + declaration?: string; + declarationMap?: string; +} + +export interface TranspileResult { + diagnostics: ts.Diagnostic[]; + transpiledFiles: Map; +} + +export interface TranspileOptions { + program: ts.Program; + sourceFiles?: ts.SourceFile[]; + customTransformers?: ts.CustomTransformers; + transformer?: LuaTransformer; + printer?: LuaPrinter; +} + +export function transpile({ + program, + sourceFiles: targetSourceFiles, + customTransformers = {}, + transformer = new LuaTransformer(program), + printer = new LuaPrinter(program.getCompilerOptions()), +}: TranspileOptions): TranspileResult { + const options = program.getCompilerOptions() as CompilerOptions; + + const diagnostics: ts.Diagnostic[] = []; + const transpiledFiles = new Map(); + const updateTranspiledFile = (filePath: string, file: TranspiledFile) => { + if (transpiledFiles.has(filePath)) { + Object.assign(transpiledFiles.get(filePath), file); + } else { + transpiledFiles.set(filePath, file); + } + }; + + if (options.noEmitOnError) { + const preEmitDiagnostics = [ + ...program.getOptionsDiagnostics(), + ...program.getGlobalDiagnostics(), + ]; + + if (targetSourceFiles) { + for (const sourceFile of targetSourceFiles) { + preEmitDiagnostics.push(...program.getSyntacticDiagnostics(sourceFile)); + preEmitDiagnostics.push(...program.getSemanticDiagnostics(sourceFile)); + } + } else { + preEmitDiagnostics.push(...program.getSyntacticDiagnostics()); + preEmitDiagnostics.push(...program.getSemanticDiagnostics()); + } + + if (preEmitDiagnostics.length === 0 && (options.declaration || options.composite)) { + preEmitDiagnostics.push(...program.getDeclarationDiagnostics()); + } + + if (preEmitDiagnostics.length > 0) { + return { diagnostics: preEmitDiagnostics, transpiledFiles }; + } + } + + const processSourceFile = (sourceFile: ts.SourceFile) => { + try { + const [luaAst, lualibFeatureSet] = transformer.transformSourceFile(sourceFile); + if (!options.noEmit && !options.emitDeclarationOnly) { + const [lua, sourceMap] = printer.print( + luaAst, + lualibFeatureSet, + sourceFile.fileName + ); + updateTranspiledFile(sourceFile.fileName, { luaAst, lua, sourceMap }); + } + } catch (err) { + if (!(err instanceof TranspileError)) throw err; + + diagnostics.push(transpileError(err)); + + updateTranspiledFile(sourceFile.fileName, { + lua: `error(${JSON.stringify(err.message)})\n`, + sourceMap: "", + }); + } + }; + + const transformers = getCustomTransformers(program, customTransformers, processSourceFile); + + const writeFile: ts.WriteFileCallback = (fileName, data, _bom, _onError, sourceFiles = []) => { + for (const sourceFile of sourceFiles) { + const isDeclaration = fileName.endsWith(".d.ts"); + const isDeclarationMap = fileName.endsWith(".d.ts.map"); + if (isDeclaration) { + updateTranspiledFile(sourceFile.fileName, { declaration: data }); + } else if (isDeclarationMap) { + updateTranspiledFile(sourceFile.fileName, { declarationMap: data }); + } + } + }; + + const isEmittableJsonFile = (sourceFile: ts.SourceFile) => + sourceFile.flags & ts.NodeFlags.JsonFile && + !options.emitDeclarationOnly && + !program.isSourceFileFromExternalLibrary(sourceFile); + + // We always have to emit to get transformer diagnostics + const oldNoEmit = options.noEmit; + options.noEmit = false; + + if (targetSourceFiles) { + for (const file of targetSourceFiles) { + if (isEmittableJsonFile(file)) { + processSourceFile(file); + } else { + diagnostics.push( + ...program.emit(file, writeFile, undefined, false, transformers).diagnostics + ); + } + } + } else { + diagnostics.push( + ...program.emit(undefined, writeFile, undefined, false, transformers).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.clear(); + } + + return { diagnostics, transpiledFiles }; +} diff --git a/src/diagnostics.ts b/src/diagnostics.ts new file mode 100644 index 000000000..ddc8611c4 --- /dev/null +++ b/src/diagnostics.ts @@ -0,0 +1,88 @@ +import * as ts from "typescript"; +import { TranspileError } from "./TranspileError"; + +export const transpileError = (error: TranspileError) => ({ + file: error.node.getSourceFile(), + start: error.node.getStart(), + length: error.node.getWidth(), + category: ts.DiagnosticCategory.Error, + code: 0, + source: "typescript-to-lua", + messageText: error.message, +}); + +export const tstlOptionsAreMovingToTheTstlObject = (tstl: Record) => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Warning, + code: 0, + source: "typescript-to-lua", + messageText: + 'TSTL options are moving to the "tstl" object. Adjust your tsconfig to look like\n' + + `"tstl": ${JSON.stringify(tstl, undefined, 4)}`, +}); + +export const watchErrorSummary = (errorCount: number): ts.Diagnostic => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Message, + code: errorCount === 1 ? 6193 : 6194, + messageText: + errorCount === 1 + ? "Found 1 error. Watching for file changes." + : `Found ${errorCount} errors. Watching for file changes.`, +}); + +const createCommandLineError = ( + code: number, + getMessage: (...args: Args) => string +) => (...args: Args): ts.Diagnostic => ({ + file: undefined, + start: undefined, + length: undefined, + category: ts.DiagnosticCategory.Error, + code, + messageText: getMessage(...args), +}); + +export const unknownCompilerOption = createCommandLineError( + 5023, + (name: string) => `Unknown compiler option '${name}'.` +); + +export const compilerOptionRequiresAValueOfType = createCommandLineError( + 5024, + (name: string, type: string) => `Compiler option '${name}' requires a value of type ${type}.` +); + +export const optionProjectCannotBeMixedWithSourceFilesOnACommandLine = createCommandLineError( + 5042, + () => "Option 'project' cannot be mixed with source files on a command line." +); + +export const cannotFindATsconfigJsonAtTheSpecifiedDirectory = createCommandLineError( + 5057, + (dir: string) => `Cannot find a tsconfig.json file at the specified directory: '${dir}'.` +); + +export const theSpecifiedPathDoesNotExist = createCommandLineError( + 5058, + (dir: string) => `The specified path does not exist: '${dir}'.` +); + +export const compilerOptionExpectsAnArgument = createCommandLineError( + 6044, + (name: string) => `Compiler option '${name}' expects an argument.` +); + +export const argumentForOptionMustBe = createCommandLineError( + 6046, + (name: string, values: string) => `Argument for '${name}' option must be: ${values}.` +); + +export const optionBuildMustBeFirstCommandLineArgument = createCommandLineError( + 6369, + () => "Option '--build' must be the first command line argument." +); diff --git a/src/index.ts b/src/index.ts index eb153d7bc..283fbee02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,125 @@ -export { parseCommandLine } from "./CommandLineParser"; -export { compile, compileFilesWithOptions, transpileString, watchWithOptions } from "./Compiler"; -export { CompilerOptions, LuaLibImportKind, LuaTarget } from "./CompilerOptions"; +import * as fs from "fs"; +import * as path from "path"; +import * as ts from "typescript"; +import { parseConfigFileWithSystem } from "./CommandLineParser"; +import { CompilerOptions } from "./CompilerOptions"; +import { emitTranspiledFiles, OutputFile } from "./Emit"; +import { transpile, TranspiledFile, TranspileResult } from "./Transpile"; + +export { parseCommandLine, ParsedCommandLine, updateParsedConfigFile } from "./CommandLineParser"; +export * from "./CompilerOptions"; +export * from "./Emit"; +export * from "./LuaAST"; export { LuaLibFeature } from "./LuaLib"; -export { LuaTranspiler } from "./LuaTranspiler"; +export * from "./LuaPrinter"; +export * from "./LuaTransformer"; +export * from "./Transpile"; +export * from "./TranspileError"; + +export interface TranspileFilesResult { + diagnostics: ts.Diagnostic[]; + emitResult: OutputFile[]; +} + +export function transpileFiles( + rootNames: string[], + options: CompilerOptions = {} +): TranspileFilesResult { + const program = ts.createProgram(rootNames, options); + const { transpiledFiles, diagnostics: transpileDiagnostics } = transpile({ program }); + const emitResult = emitTranspiledFiles(program.getCompilerOptions(), transpiledFiles); + + const diagnostics = ts.sortAndDeduplicateDiagnostics([ + ...ts.getPreEmitDiagnostics(program), + ...transpileDiagnostics, + ]); + + return { diagnostics: [...diagnostics], emitResult }; +} + +export function transpileProject( + fileName: string, + optionsToExtend?: CompilerOptions +): TranspileFilesResult { + const parseResult = parseConfigFileWithSystem(fileName, optionsToExtend); + if (parseResult.errors.length > 0) { + return { diagnostics: parseResult.errors, emitResult: [] }; + } + + return transpileFiles(parseResult.fileNames, parseResult.options); +} + +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 function transpileVirtualProject( + files: Record, + options: CompilerOptions = {} +): TranspileResult { + const program = createVirtualProgram(files, options); + const result = transpile({ program }); + const diagnostics = ts.sortAndDeduplicateDiagnostics([ + ...ts.getPreEmitDiagnostics(program), + ...result.diagnostics, + ]); + + return { ...result, diagnostics: [...diagnostics] }; +} + +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.get("main.ts") }; +} diff --git a/src/tstl.ts b/src/tstl.ts index 5055ec8aa..ea6489c3f 100644 --- a/src/tstl.ts +++ b/src/tstl.ts @@ -1,5 +1,296 @@ #!/usr/bin/env node +import * as path from "path"; +import * as ts from "typescript"; +import * as tstl from "."; +import * as CommandLineParser from "./CommandLineParser"; +import * as cliDiagnostics from "./diagnostics"; -import { compile } from "./Compiler"; +function createDiagnosticReporter(pretty: boolean): ts.DiagnosticReporter { + const reporter: ts.DiagnosticReporter = (ts as any).createDiagnosticReporter(ts.sys, pretty); + return diagnostic => { + if (diagnostic.source === "typescript-to-lua") { + diagnostic = { ...diagnostic, code: ("TL" + diagnostic.code) as any }; + } -compile(process.argv.slice(2)); + reporter(diagnostic); + }; +} + +function createWatchStatusReporter(options?: ts.CompilerOptions): ts.WatchStatusReporter { + return (ts as any).createWatchStatusReporter(ts.sys, shouldBePretty(options)); +} + +function shouldBePretty(options?: ts.CompilerOptions): boolean { + return !options || options.pretty === undefined + ? ts.sys.writeOutputIsTTY !== undefined && ts.sys.writeOutputIsTTY() + : Boolean(options.pretty); +} + +let reportDiagnostic = createDiagnosticReporter(false); +function updateReportDiagnostic(options?: ts.CompilerOptions): void { + reportDiagnostic = createDiagnosticReporter(shouldBePretty(options)); +} + +function locateConfigFile(commandLine: tstl.ParsedCommandLine): string | undefined { + const { project } = commandLine.options; + if (!project) { + if (commandLine.fileNames.length === 0) { + const searchPath = path.posix.normalize(ts.sys.getCurrentDirectory()); + return ts.findConfigFile(searchPath, ts.sys.fileExists); + } + return; + } + + if (commandLine.fileNames.length !== 0) { + reportDiagnostic(cliDiagnostics.optionProjectCannotBeMixedWithSourceFilesOnACommandLine()); + ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + + let fileOrDirectory = path.posix.normalize(project); + if (!path.isAbsolute(fileOrDirectory)) { + fileOrDirectory = path.posix.join(ts.sys.getCurrentDirectory(), fileOrDirectory); + } + + if (!fileOrDirectory || ts.sys.directoryExists(fileOrDirectory)) { + const configFileName = path.posix.join(fileOrDirectory, "tsconfig.json"); + if (ts.sys.fileExists(configFileName)) { + return configFileName; + } else { + reportDiagnostic( + cliDiagnostics.cannotFindATsconfigJsonAtTheSpecifiedDirectory(project) + ); + ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } else { + if (ts.sys.fileExists(fileOrDirectory)) { + return fileOrDirectory; + } else { + reportDiagnostic(cliDiagnostics.theSpecifiedPathDoesNotExist(project)); + ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } +} + +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 = CommandLineParser.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(CommandLineParser.version); + return ts.sys.exit(ts.ExitStatus.Success); + } + + if (commandLine.options.help) { + console.log(CommandLineParser.version); + console.log(CommandLineParser.getHelpString()); + return ts.sys.exit(ts.ExitStatus.Success); + } + + const configFileName = locateConfigFile(commandLine); + const commandLineOptions = commandLine.options; + if (configFileName) { + const configParseResult = CommandLineParser.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: ReadonlyArray | undefined, + options: tstl.CompilerOptions, + configFileParsingDiagnostics?: ReadonlyArray +): void { + const program = ts.createProgram({ + rootNames, + options, + projectReferences, + configFileParsingDiagnostics, + }); + + const { transpiledFiles, diagnostics: transpileDiagnostics } = tstl.transpile({ program }); + + const diagnostics = ts.sortAndDeduplicateDiagnostics([ + ...ts.getPreEmitDiagnostics(program), + ...transpileDiagnostics, + ]); + + const emitResult = tstl.emitTranspiledFiles(options, transpiledFiles); + emitResult.forEach(({ name, text }) => ts.sys.writeFile(name, text)); + + diagnostics.forEach(reportDiagnostic); + const exitCode = + diagnostics.length === 0 + ? ts.ExitStatus.Success + : transpiledFiles.size === 0 + ? 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 fullRecompile = true; + const configFileMap = new WeakMap(); + + host.afterProgramCreate = builderProgram => { + const program = builderProgram.getProgram(); + const options = builderProgram.getCompilerOptions() as tstl.CompilerOptions; + + let configFileParsingDiagnostics: ts.Diagnostic[] = []; + const configFile = options.configFile as ts.TsConfigSourceFile | undefined; + const configFilePath = options.configFilePath as string | undefined; + if (configFile && configFilePath) { + if (!configFileMap.has(configFile)) { + const parsedConfigFile = CommandLineParser.updateParsedConfigFile( + ts.parseJsonSourceFileConfigFileContent( + configFile, + ts.sys, + path.dirname(configFilePath), + optionsToExtend, + configFilePath + ) + ); + + configFileMap.set(configFile, parsedConfigFile); + } + + const parsedConfigFile = configFileMap.get(configFile)!; + Object.assign(options, parsedConfigFile.options); + configFileParsingDiagnostics = parsedConfigFile.errors; + } + + let sourceFiles: ts.SourceFile[] | undefined; + if (!fullRecompile) { + 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, transpiledFiles } = tstl.transpile({ + program, + sourceFiles, + }); + + const emitResult = tstl.emitTranspiledFiles(options, transpiledFiles); + emitResult.forEach(({ name, text }) => ts.sys.writeFile(name, text)); + + 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); + // do a full recompile after an error + fullRecompile = errors.length > 0; + + host.onWatchStatusChange!( + cliDiagnostics.watchErrorSummary(errors.length), + host.getNewLine(), + options + ); + }; +} + +if ((ts.sys as any).setBlocking) (ts.sys as any).setBlocking(); + +executeCommandLine(ts.sys.args); diff --git a/test/cli/errors.spec.ts b/test/cli/errors.spec.ts new file mode 100644 index 000000000..8ff45ce14 --- /dev/null +++ b/test/cli/errors.spec.ts @@ -0,0 +1,27 @@ +import * as fs from "fs"; +import * as path from "path"; +import { runCli } from "./run"; + +const srcFilePath = path.resolve(__dirname, "errors", "error.ts"); +const outFilePath = path.resolve(__dirname, "errors", "error.lua"); +const errorMessage = "Unable to convert function with no 'this' parameter to function with 'this'."; + +afterEach(() => { + if (fs.existsSync(outFilePath)) fs.unlinkSync(outFilePath); +}); + +test("should report errors", async () => { + const { exitCode, output } = await runCli([srcFilePath]); + + expect(output).toContain(errorMessage); + expect(exitCode).toBe(2); + expect(fs.existsSync(outFilePath)).toBe(true); +}); + +test("shouldn't emit files with --noEmitOnError", async () => { + const { exitCode, output } = await runCli(["--noEmitOnError", srcFilePath]); + + expect(output).toContain(errorMessage); + expect(exitCode).toBe(1); + expect(fs.existsSync(outFilePath)).toBe(false); +}); diff --git a/test/cli/errors/error.ts b/test/cli/errors/error.ts new file mode 100644 index 000000000..6b4c5320c --- /dev/null +++ b/test/cli/errors/error.ts @@ -0,0 +1,2 @@ +const foo: (this: void) => void = () => {}; +const bar: () => void = foo; diff --git a/test/cli/run.ts b/test/cli/run.ts new file mode 100644 index 000000000..59d2e9263 --- /dev/null +++ b/test/cli/run.ts @@ -0,0 +1,31 @@ +import { ChildProcess, fork } from "child_process"; +import * as path from "path"; + +jest.setTimeout(20000); + +const cliPath = path.join(__dirname, "../../src/tstl.ts"); + +const defaultArgs = ["--skipLibCheck", "--types", "node"]; +export function forkCli(args: string[]): ChildProcess { + return fork(cliPath, [...defaultArgs, ...args], { + stdio: "pipe", + execArgv: ["--require", "ts-node/register/transpile-only"], + }); +} + +export interface CliResult { + exitCode: number; + output: string; +} + +export async function runCli(args: string[]): Promise { + const child = forkCli(args); + + let output = ""; + child.stdout!.on("data", data => (output += data)); + child.stderr!.on("data", data => (output += data)); + + return new Promise(resolve => { + child.on("close", exitCode => resolve({ exitCode, output })); + }); +} diff --git a/test/cli/watch.spec.ts b/test/cli/watch.spec.ts new file mode 100644 index 000000000..d1274f865 --- /dev/null +++ b/test/cli/watch.spec.ts @@ -0,0 +1,66 @@ +import * as fs from "fs"; +import * as path from "path"; +import { forkCli } from "./run"; + +let testsCleanup: Array<() => void> = []; +afterEach(() => { + testsCleanup.forEach(x => x()); + testsCleanup = []; +}); + +function waitForFileExists(filePath: string): Promise { + return new Promise(resolve => { + const intervalTimerId = setInterval(() => { + if (fs.existsSync(filePath)) { + clearInterval(intervalTimerId); + resolve(); + } + }, 100); + + testsCleanup.push(() => clearInterval(intervalTimerId)); + }); +} + +function forkWatchProcess(args: string[]): void { + const child = forkCli(["--watch", ...args]); + testsCleanup.push(() => child.kill()); +} + +const watchedFile = path.join(__dirname, "./watch/watch.ts"); +const watchedFileOut = watchedFile.replace(".ts", ".lua"); + +afterEach(() => { + if (fs.existsSync(watchedFileOut)) fs.unlinkSync(watchedFileOut); +}); + +async function compileChangeAndCompare(filePath: string, content: string): Promise { + 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)); + + await waitForFileExists(watchedFileOut); + const updatedResultLua = fs.readFileSync(watchedFileOut, "utf8"); + + expect(initialResultLua).not.toEqual(updatedResultLua); +} + +test("should watch single file", async () => { + forkWatchProcess([path.join(__dirname, "./watch/watch.ts")]); + await compileChangeAndCompare(watchedFile, "const value = 1;"); +}); + +test("should watch project", async () => { + forkWatchProcess(["--project", path.join(__dirname, "./watch")]); + await compileChangeAndCompare(watchedFile, "const value = 1;"); +}); + +test("should watch config file", async () => { + const configFilePath = path.join(__dirname, "./watch/tsconfig.json"); + forkWatchProcess(["--project", configFilePath]); + await compileChangeAndCompare(configFilePath, '{ "tstl": { "luaTarget": "5.3" } }'); +}); diff --git a/test/cli/watch/tsconfig.json b/test/cli/watch/tsconfig.json new file mode 100644 index 000000000..922b8de4c --- /dev/null +++ b/test/cli/watch/tsconfig.json @@ -0,0 +1 @@ +{ "tstl": { "luaTarget": "JIT" } } diff --git a/test/cli/watch/watch.ts b/test/cli/watch/watch.ts new file mode 100644 index 000000000..45813bdf9 --- /dev/null +++ b/test/cli/watch/watch.ts @@ -0,0 +1 @@ +const value = 0 & 0; diff --git a/test/compiler/errorreport.spec.ts b/test/compiler/errorreport.spec.ts deleted file mode 100644 index 1c588ee04..000000000 --- a/test/compiler/errorreport.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as path from "path"; -import { compileFilesWithOptions } from "../../src/Compiler"; - -test.each([ - { - errorMsg: - "Encountered error parsing file: Default Imports are not supported, please use named imports instead!", - fileName: "default_import.ts", - }, -])("Compile project (%p)", ({ errorMsg, fileName }) => { - jest.spyOn(console, "log").mockReturnValue(undefined); - const errorMock = jest.spyOn(console, "error").mockReturnValue(undefined); - const exitMock = jest.spyOn(process, "exit").mockReturnValue(undefined as never); - - fileName = path.resolve(__dirname, "testfiles", fileName); - compileFilesWithOptions([fileName], { outDir: ".", rootDir: ".", types: [] }); - - jest.restoreAllMocks(); - - expect(exitMock).toHaveBeenCalledWith(1); - expect(errorMock).toHaveBeenCalledTimes(2); - expect(errorMock).toHaveBeenNthCalledWith(1, errorMsg); - expect(errorMock).toHaveBeenNthCalledWith(2, expect.any(String)); -}); diff --git a/test/compiler/outfile.spec.ts b/test/compiler/outfile.spec.ts deleted file mode 100644 index e82e7341b..000000000 --- a/test/compiler/outfile.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { compile } from "../../src/Compiler"; - -let outFileRelPath: string; -let outFileAbsPath: string; - -beforeAll(() => { - outFileRelPath = "./testfiles/out_file.script"; - outFileAbsPath = path.join(__dirname, outFileRelPath); -}); - -afterEach(() => { - fs.unlink(outFileAbsPath, err => { - if (err) { - throw err; - } - }); -}); - -test("Outfile absoulte path", () => { - compile([ - "--types", - "node", - "--skipLibCheck", - "--outFile", - outFileAbsPath, - path.join(__dirname, "./testfiles/out_file.ts"), - ]); - - expect(fs.existsSync(outFileAbsPath)).toBe(true); -}); - -test("Outfile relative path", () => { - compile([ - "--types", - "node", - "--skipLibCheck", - "--outDir", - __dirname, - "--outFile", - outFileRelPath, - path.join(__dirname, "./testfiles/out_file.ts"), - ]); - - expect(fs.existsSync(outFileAbsPath)).toBe(true); -}); diff --git a/test/compiler/project.spec.ts b/test/compiler/project.spec.ts deleted file mode 100644 index a23fe9209..000000000 --- a/test/compiler/project.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { compile } from "../../src/Compiler"; - -/** - * Find all files inside a dir, recursively. - */ -function getAllFiles(dir: string): string[] { - return fs.readdirSync(dir).reduce((files: string[], file) => { - const name = path.join(dir, file); - const isDirectory = fs.statSync(name).isDirectory(); - return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name]; - }, []); -} - -let existingFiles: string[]; -let filesAfterCompile: string[]; - -afterEach(() => { - // Remove files that were created by the test - const createdFiles = filesAfterCompile.filter(v => existingFiles.indexOf(v) < 0); - for (const file of createdFiles) { - fs.unlinkSync(file); - } - existingFiles = []; - filesAfterCompile = []; -}); - -test.each([ - { - projectName: "basic", - tsconfig: "tsconfig.json", - expectedFiles: ["lualib_bundle.lua", "test_src/test_lib/file.lua", "test_src/main.lua"], - }, - { - projectName: "basic", - tsconfig: ".", - expectedFiles: ["lualib_bundle.lua", "test_src/test_lib/file.lua", "test_src/main.lua"], - }, - { - projectName: "basic", - tsconfig: "test_src/main.ts", - expectedFiles: ["lualib_bundle.lua", "test_src/test_lib/file.lua", "test_src/main.lua"], - }, - { - projectName: "basic", - tsconfig: "tsconfig.outDir.json", - expectedFiles: [ - "out_dir/lualib_bundle.lua", - "out_dir/test_src/test_lib/file.lua", - "out_dir/test_src/main.lua", - ], - }, - { - projectName: "basic", - tsconfig: "tsconfig.rootDir.json", - expectedFiles: [ - "test_src/lualib_bundle.lua", - "test_src/test_lib/file.lua", - "test_src/main.lua", - ], - }, - { - projectName: "basic", - tsconfig: "tsconfig.bothDirOptions.json", - expectedFiles: [ - "out_dir/lualib_bundle.lua", - "out_dir/test_lib/file.lua", - "out_dir/main.lua", - ], - }, - { - projectName: "baseurl", - tsconfig: "tsconfig.json", - expectedFiles: [ - "out_dir/lualib_bundle.lua", - "out_dir/test_src/test_lib/nested/lib_file.lua", - "out_dir/test_src/main.lua", - ], - }, -])("Compile project (%p)", ({ projectName, tsconfig, expectedFiles }) => { - const relPathToProject = path.join("projects", projectName); - - // Setup we cant do this in beforeEach because we need the projectname - existingFiles = getAllFiles(path.resolve(__dirname, relPathToProject)); - filesAfterCompile = []; - // Setup End - - const tsconfigPath = path.resolve(__dirname, relPathToProject, tsconfig); - - compile(["-p", tsconfigPath]); - - filesAfterCompile = getAllFiles(path.resolve(__dirname, relPathToProject)); - expectedFiles = expectedFiles.map(relPath => - path.resolve(__dirname, relPathToProject, relPath), - ); - expectedFiles.push(...existingFiles); - - for (const existingFile of filesAfterCompile) { - expect(expectedFiles).toContain(existingFile); - } -}); diff --git a/test/compiler/projects/baseurl/test_src/main.ts b/test/compiler/projects/baseurl/test_src/main.ts deleted file mode 100644 index 62afd2ad1..000000000 --- a/test/compiler/projects/baseurl/test_src/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as test from "nested/lib_file"; - -let hello = "12"; - -hello = "13"; diff --git a/test/compiler/projects/baseurl/tsconfig.json b/test/compiler/projects/baseurl/tsconfig.json deleted file mode 100644 index 299ae7cf8..000000000 --- a/test/compiler/projects/baseurl/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "luaTarget": "JIT", - "compilerOptions": { - "outDir": "./out_dir", - "rootDir": ".", - "baseUrl": "./test_src/test_lib", - "types": [], - "skipLibCheck": true - } -} diff --git a/test/compiler/projects/basic/tsconfig.bothDirOptions.json b/test/compiler/projects/basic/tsconfig.bothDirOptions.json deleted file mode 100644 index b21881ae5..000000000 --- a/test/compiler/projects/basic/tsconfig.bothDirOptions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "out_dir", - "rootDir": "test_src" - } -} diff --git a/test/compiler/projects/basic/tsconfig.json b/test/compiler/projects/basic/tsconfig.json deleted file mode 100644 index ec7262ddf..000000000 --- a/test/compiler/projects/basic/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "luaTarget": "JIT", - "compilerOptions": { - "types": [], - "skipLibCheck": true - } -} diff --git a/test/compiler/projects/basic/tsconfig.outDir.json b/test/compiler/projects/basic/tsconfig.outDir.json deleted file mode 100644 index cd2de0d24..000000000 --- a/test/compiler/projects/basic/tsconfig.outDir.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "out_dir" - } -} diff --git a/test/compiler/projects/basic/tsconfig.rootDir.json b/test/compiler/projects/basic/tsconfig.rootDir.json deleted file mode 100644 index d9506e7ee..000000000 --- a/test/compiler/projects/basic/tsconfig.rootDir.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "test_src" - } -} diff --git a/test/compiler/projects/watchmode/tsconfig.json b/test/compiler/projects/watchmode/tsconfig.json deleted file mode 100644 index ec7262ddf..000000000 --- a/test/compiler/projects/watchmode/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "luaTarget": "JIT", - "compilerOptions": { - "types": [], - "skipLibCheck": true - } -} diff --git a/test/compiler/projects/watchmode/watch.ts b/test/compiler/projects/watchmode/watch.ts deleted file mode 100644 index fa2edc21a..000000000 --- a/test/compiler/projects/watchmode/watch.ts +++ /dev/null @@ -1 +0,0 @@ -class MyTest {} diff --git a/test/compiler/testfiles/default_import.ts b/test/compiler/testfiles/default_import.ts deleted file mode 100644 index 9f80d14ac..000000000 --- a/test/compiler/testfiles/default_import.ts +++ /dev/null @@ -1 +0,0 @@ -import Test from "./default_export"; diff --git a/test/compiler/testfiles/invalid_syntax.ts b/test/compiler/testfiles/invalid_syntax.ts deleted file mode 100644 index 6d38d3866..000000000 --- a/test/compiler/testfiles/invalid_syntax.ts +++ /dev/null @@ -1 +0,0 @@ -const variable = () => {} => {}; diff --git a/test/compiler/testfiles/watch.ts b/test/compiler/testfiles/watch.ts deleted file mode 100644 index fa2edc21a..000000000 --- a/test/compiler/testfiles/watch.ts +++ /dev/null @@ -1 +0,0 @@ -class MyTest {} diff --git a/test/compiler/watcher_proccess.ts b/test/compiler/watcher_proccess.ts deleted file mode 100644 index 25b4bd344..000000000 --- a/test/compiler/watcher_proccess.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { compile } from "../../src/Compiler"; - -process.on("message", args => { - compile(args); -}); diff --git a/test/compiler/watchmode.spec.ts b/test/compiler/watchmode.spec.ts deleted file mode 100644 index f4115dcc3..000000000 --- a/test/compiler/watchmode.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { fork } from "child_process"; -import * as fs from "fs"; -import * as path from "path"; - -let testsCleanup: Array<() => void> = []; -afterEach(() => { - testsCleanup.forEach(x => x()); - testsCleanup = []; -}); - -function waitForFileExists(filePath: string): Promise { - return new Promise(resolve => { - const intervalTimerId = setInterval(() => { - if (fs.existsSync(filePath)) { - clearInterval(intervalTimerId); - resolve(); - } - }, 100); - - testsCleanup.push(() => clearInterval(intervalTimerId)); - }); -} - -test.each([ - { - args: [ - "--types", - "node", - "--skipLibCheck", - "-w", - path.join(__dirname, "./testfiles/watch.ts"), - ], - fileToChange: path.join(__dirname, "./testfiles/watch.ts"), - }, - { - args: ["-w", "-p", path.join(__dirname, "./projects/watchmode/")], - fileToChange: path.join(__dirname, "./projects/watchmode/watch.ts"), - }, -])( - "Watch single File (%p)", - async ({ args, fileToChange }) => { - const fileToChangeOut = fileToChange.replace(".ts", ".lua"); - const originalTS = fs.readFileSync(fileToChange, "utf-8"); - - const child = fork(path.join(__dirname, "watcher_proccess.ts"), [], { - silent: true, - execArgv: ["--require", "ts-node/register/transpile-only"], - }); - - testsCleanup.push(() => { - try { - fs.unlinkSync(fileToChangeOut); - } catch (err) { - if (err.code !== "ENOENT") throw err; - } - fs.writeFileSync(fileToChange, originalTS); - child.kill(); - }); - - child.send(args); - - await waitForFileExists(fileToChangeOut); - const initialResultLua = fs.readFileSync(fileToChangeOut, "utf-8"); - - fs.unlinkSync(fileToChangeOut); - fs.writeFileSync(fileToChange, "class MyTest2 {}"); - - await waitForFileExists(fileToChangeOut); - const updatedResultLua = fs.readFileSync(fileToChangeOut, "utf-8"); - - expect(initialResultLua).not.toEqual(updatedResultLua); - }, - 20000, -); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 000000000..8e4e5fbb5 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,58 @@ +import * as ts from "typescript"; +import * as util from "./util"; + +declare global { + namespace jest { + interface Matchers { + toThrowExactError(error: Error): void; + toHaveDiagnostics(): void; + } + } +} + +expect.extend({ + toThrowExactError(callback: () => void, error: Error): jest.CustomMatcherResult { + if (this.isNot) { + return { pass: true, message: () => "Inverted toThrowExactError is not implemented" }; + } + + let executionError: Error | undefined; + try { + callback(); + } catch (err) { + executionError = err; + } + + // TODO: + if (util.expectToBeDefined(executionError)) { + expect(executionError.message).toContain(error.message); + } + + return { pass: true, message: () => "" }; + }, + toHaveDiagnostics(diagnostics: ts.Diagnostic[]): jest.CustomMatcherResult { + expect(Array.isArray(diagnostics)).toBe(true); + const options = { isNot: this.isNot }; + // @ts-ignore + const matcherHint = this.utils.matcherHint("toHaveDiagnostics", undefined, "", options); + + const diagnosticMessages = ts.formatDiagnosticsWithColorAndContext(diagnostics, { + getCurrentDirectory: () => "", + getCanonicalFileName: fileName => fileName, + getNewLine: () => "\n", + }); + + return { + pass: diagnostics.length > 0, + message: () => { + return ( + matcherHint + + "\n\n" + + (this.isNot + ? diagnosticMessages + : `Received: ${this.utils.printReceived(diagnostics)}\n`) + ); + }, + }; + }, +}); diff --git a/test/translation/transformation.spec.ts b/test/translation/transformation.spec.ts index bd246dddf..d7467a78f 100644 --- a/test/translation/transformation.spec.ts +++ b/test/translation/transformation.spec.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; +import * as tstl from "../../src"; import * as util from "../util"; -import { LuaLibImportKind } from "../../src/CompilerOptions"; const fixturesPath = path.join(__dirname, "./transformation"); const fixtures = fs @@ -11,6 +11,6 @@ const fixtures = fs .map(f => [path.parse(f).name, fs.readFileSync(path.join(fixturesPath, f), "utf8")]); test.each(fixtures)("Transformation (%s)", (_name, content) => { - const result = util.transpileString(content, { luaLibImport: LuaLibImportKind.Require }); + const result = util.transpileString(content, { luaLibImport: tstl.LuaLibImportKind.Require }); expect(result).toMatchSnapshot(); }); diff --git a/test/transpile/__snapshots__/directories.spec.ts.snap b/test/transpile/__snapshots__/directories.spec.ts.snap new file mode 100644 index 000000000..952fd709b --- /dev/null +++ b/test/transpile/__snapshots__/directories.spec.ts.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should be able to resolve ({"name": "baseurl", "options": [Object]}) 1`] = ` +Array [ + "directories/baseurl/out/lualib_bundle.lua", + "directories/baseurl/out/src/lib/nested/file.lua", + "directories/baseurl/out/src/main.lua", +] +`; + +exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 1`] = ` +Array [ + "directories/basic/lualib_bundle.lua", + "directories/basic/src/lib/file.lua", + "directories/basic/src/main.lua", +] +`; + +exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 2`] = ` +Array [ + "directories/basic/out/lualib_bundle.lua", + "directories/basic/out/src/lib/file.lua", + "directories/basic/out/src/main.lua", +] +`; + +exports[`should be able to resolve ({"name": "basic", "options": [Object]}) 3`] = ` +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", +] +`; diff --git a/test/transpile/directories.spec.ts b/test/transpile/directories.spec.ts new file mode 100644 index 000000000..877e58f50 --- /dev/null +++ b/test/transpile/directories.spec.ts @@ -0,0 +1,33 @@ +import * as path from "path"; +import * as ts from "typescript"; +import * as tstl from "../../src"; +import { buildVirtualProject } from "./run"; + +interface DirectoryTestCase { + name: string; + options: tstl.CompilerOptions; +} + +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)", async ({ name, options: compilerOptions }) => { + const projectPath = path.join(__dirname, "directories", name); + jest.spyOn(process, "cwd").mockReturnValue(projectPath); + + 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, projectPath), + ); + + const { diagnostics, emittedFiles } = buildVirtualProject(fileNames, options); + expect(diagnostics).not.toHaveDiagnostics(); + expect(emittedFiles).toMatchSnapshot(); +}); diff --git a/test/compiler/projects/baseurl/test_src/test_lib/nested/lib_file.ts b/test/transpile/directories/baseurl/src/lib/nested/file.ts similarity index 100% rename from test/compiler/projects/baseurl/test_src/test_lib/nested/lib_file.ts rename to test/transpile/directories/baseurl/src/lib/nested/file.ts diff --git a/test/transpile/directories/baseurl/src/main.ts b/test/transpile/directories/baseurl/src/main.ts new file mode 100644 index 000000000..1665f4e08 --- /dev/null +++ b/test/transpile/directories/baseurl/src/main.ts @@ -0,0 +1,3 @@ +import { test } from "nested/file"; + +test(); diff --git a/test/compiler/projects/basic/test_src/test_lib/file.ts b/test/transpile/directories/basic/src/lib/file.ts similarity index 100% rename from test/compiler/projects/basic/test_src/test_lib/file.ts rename to test/transpile/directories/basic/src/lib/file.ts diff --git a/test/compiler/projects/basic/test_src/main.ts b/test/transpile/directories/basic/src/main.ts similarity index 100% rename from test/compiler/projects/basic/test_src/main.ts rename to test/transpile/directories/basic/src/main.ts diff --git a/test/transpile/outFile.spec.ts b/test/transpile/outFile.spec.ts new file mode 100644 index 000000000..5a2e9c399 --- /dev/null +++ b/test/transpile/outFile.spec.ts @@ -0,0 +1,44 @@ +import * as path from "path"; +import { buildVirtualProject } from "./run"; + +const inputFilePath = path.join(__dirname, "outFile/index.ts"); +test("should support absolute outFile", () => { + const { diagnostics, emittedFiles } = buildVirtualProject([inputFilePath], { + outFile: path.join(__dirname, "output.script"), + }); + + expect(diagnostics).not.toHaveDiagnostics(); + expect(emittedFiles).toEqual(["output.script"]); +}); + +test("should support relative outFile", () => { + jest.spyOn(process, "cwd").mockReturnValue(__dirname); + const { diagnostics, emittedFiles } = buildVirtualProject([inputFilePath], { + outFile: "output.script", + }); + + expect(diagnostics).not.toHaveDiagnostics(); + expect(emittedFiles).toEqual(["output.script"]); +}); + +test("should support outFile with declaration", () => { + const { diagnostics, emittedFiles } = buildVirtualProject([inputFilePath], { + outFile: path.join(__dirname, "output.script"), + declaration: true, + }); + + expect(diagnostics).not.toHaveDiagnostics(); + expect(emittedFiles).toEqual(["output.d.ts", "output.script"]); +}); + +test("should resolve outFile relative to base directory", () => { + jest.spyOn(process, "cwd").mockReturnValue(__dirname); + const { diagnostics, emittedFiles, emitResult } = buildVirtualProject([inputFilePath], { + outFile: "output.script", + outDir: "out", + declaration: true, + }); + + expect(diagnostics).not.toHaveDiagnostics(); + expect(emittedFiles).toEqual(["output.d.ts", "output.script"]); +}); diff --git a/test/compiler/testfiles/out_file.ts b/test/transpile/outFile/index.ts similarity index 100% rename from test/compiler/testfiles/out_file.ts rename to test/transpile/outFile/index.ts diff --git a/test/transpile/run.ts b/test/transpile/run.ts new file mode 100644 index 000000000..57bbc8f6b --- /dev/null +++ b/test/transpile/run.ts @@ -0,0 +1,24 @@ +import * as ts from "typescript"; +import * as tstl from "../../src"; +import * as path from "path"; + +interface BuildVirtualProjectResult { + diagnostics: ts.Diagnostic[]; + emitResult: tstl.OutputFile[]; + emittedFiles: string[]; +} + +export function buildVirtualProject( + rootNames: string[], + options: tstl.CompilerOptions, +): BuildVirtualProjectResult { + options.skipLibCheck = true; + options.types = []; + + const { diagnostics, emitResult } = tstl.transpileFiles(rootNames, options); + const emittedFiles = emitResult + .map(result => path.relative(__dirname, result.name).replace(/\\/g, "/")) + .sort(); + + return { diagnostics, emitResult, emittedFiles }; +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 666a6006e..684b6c260 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -9,5 +9,11 @@ "noEmit": true, "module": "commonjs" }, - "exclude": ["translation/transformation", "compiler/projects", "compiler/testfiles"] + "exclude": [ + "translation/transformation", + "cli/errors", + "cli/watch", + "transpile/directories", + "transpile/outFile" + ] } diff --git a/test/tslint.json b/test/tslint.json index e8bb9a2cf..2796d7894 100644 --- a/test/tslint.json +++ b/test/tslint.json @@ -42,7 +42,7 @@ "no-var-keyword": true, "object-literal-shorthand": true, "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], - "prefer-const": true, + "prefer-const": [true, { "destructuring": "all" }], "radix": true, "switch-default": false, "triple-equals": [true, "allow-null-check"], diff --git a/test/unit/assignmentDestructuring.spec.ts b/test/unit/assignmentDestructuring.spec.ts index 34ccacee5..0aeca594d 100644 --- a/test/unit/assignmentDestructuring.spec.ts +++ b/test/unit/assignmentDestructuring.spec.ts @@ -1,4 +1,4 @@ -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; +import * as tstl from "../../src"; import * as util from "../util"; const assignmentDestruturingTs = ` @@ -7,24 +7,24 @@ const assignmentDestruturingTs = ` test("Assignment destructuring [5.1]", () => { const lua = util.transpileString(assignmentDestruturingTs, { - luaTarget: LuaTarget.Lua51, - luaLibImport: LuaLibImportKind.None, + luaTarget: tstl.LuaTarget.Lua51, + luaLibImport: tstl.LuaLibImportKind.None, }); expect(lua).toBe(`local a, b = unpack(myFunc())`); }); test("Assignment destructuring [5.2]", () => { const lua = util.transpileString(assignmentDestruturingTs, { - luaTarget: LuaTarget.Lua52, - luaLibImport: LuaLibImportKind.None, + luaTarget: tstl.LuaTarget.Lua52, + luaLibImport: tstl.LuaLibImportKind.None, }); expect(lua).toBe(`local a, b = table.unpack(myFunc())`); }); test("Assignment destructuring [JIT]", () => { const lua = util.transpileString(assignmentDestruturingTs, { - luaTarget: LuaTarget.LuaJIT, - luaLibImport: LuaLibImportKind.None, + luaTarget: tstl.LuaTarget.LuaJIT, + luaLibImport: tstl.LuaLibImportKind.None, }); expect(lua).toBe(`local a, b = unpack(myFunc())`); }); diff --git a/test/unit/commandLineParser.spec.ts b/test/unit/commandLineParser.spec.ts index 2e4f8d1d2..8572f7c58 100644 --- a/test/unit/commandLineParser.spec.ts +++ b/test/unit/commandLineParser.spec.ts @@ -1,235 +1,206 @@ -import { findConfigFile, parseCommandLine, parseTsConfigString } from "../../src/CommandLineParser"; -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; - -test.each([ - { args: [""], expected: LuaLibImportKind.Inline }, - { args: ["--luaLibImport", "none"], expected: LuaLibImportKind.None }, - { args: ["--luaLibImport", "always"], expected: LuaLibImportKind.Always }, - { args: ["--luaLibImport", "inline"], expected: LuaLibImportKind.Inline }, - { args: ["--luaLibImport", "require"], expected: LuaLibImportKind.Require }, -])("CLI parser luaLibImportKind (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.luaLibImport).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test("CLI parser invalid luaLibImportKind", () => { - const result = parseCommandLine(["--luaLibImport", "invalid"]); - expect(result.isValid).toBe(false); -}); - -test.each([ - { args: [""], expected: LuaTarget.LuaJIT }, - { args: ["--luaTarget", "5.1"], expected: LuaTarget.Lua51 }, - { args: ["--luaTarget", "5.2"], expected: LuaTarget.Lua52 }, - { args: ["--luaTarget", "jit"], expected: LuaTarget.LuaJIT }, - { args: ["--luaTarget", "JIT"], expected: LuaTarget.LuaJIT }, - { args: ["--luaTarget", "5.3"], expected: LuaTarget.Lua53 }, -])("CLI parser luaTarget (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.luaTarget).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test.each([ - { args: ["-lt", "5.1"], expected: LuaTarget.Lua51 }, - { args: ["-lt", "5.2"], expected: LuaTarget.Lua52 }, - { args: ["-lt", "jit"], expected: LuaTarget.LuaJIT }, - { args: ["-lt", "JIT"], expected: LuaTarget.LuaJIT }, - { args: ["-lt", "5.3"], expected: LuaTarget.Lua53 }, -])("CLI parser luaTarget (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.luaTarget).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test("CLI parser invalid luaTarget", () => { - const result = parseCommandLine(["--luatTarget", "invalid"]); - expect(result.isValid).toBe(false); -}); - -test.each([ - { args: [""], expected: false }, - { args: ["--noHeader", "true"], expected: true }, - { args: ["--noHeader", "false"], expected: false }, - { args: ["--noHeader"], expected: true }, - { args: ["--noHeader", "--noHoisting"], expected: true }, -])("CLI parser noHeader (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.noHeader).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test.each([ - { args: [""], expected: false }, - { args: ["--noHoisting", "true"], expected: true }, - { args: ["--noHoisting", "false"], expected: false }, - { args: ["--noHoisting"], expected: true }, - { args: ["--noHoisting", "--noHeader"], expected: true }, -])("CLI parser noHoisting (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.noHoisting).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test.each([ - { args: [""], expected: false }, - { args: ["--project", "tsconfig.json"], expected: true }, - { args: ["-p", "tsconfig.json"], expected: true }, -])("CLI parser project (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.project !== undefined).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test("CLI Parser Multiple Options", () => { - const commandLine = "--project tsconfig.json --noHeader --noHoisting -lt 5.3"; - const result = parseCommandLine(commandLine.split(" ")); - - if (result.isValid === true) { - expect(result.result.options.project).toBeDefined(); - expect(result.result.options.noHeader).toBe(true); - expect(result.result.options.noHoisting).toBe(true); - expect(result.result.options.luaTarget).toBe(LuaTarget.Lua53); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test.each([ - { args: [""], expected: false }, - { args: ["--help"], expected: true }, - { args: ["-h"], expected: true }, -])("CLI parser project (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.help === true).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test.each([ - { args: [""], expected: false }, - { args: ["--version"], expected: true }, - { args: ["-v"], expected: true }, -])("CLI parser project (%p)", ({ args, expected }) => { - const result = parseCommandLine(args); - if (result.isValid === true) { - expect(result.result.options.version === true).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } -}); - -test.each([ - { option: "luaTarget", expected: LuaTarget.LuaJIT }, - { option: "noHeader", expected: false }, - { option: "luaLibImport", expected: "inline" }, - { option: "rootDir", expected: process.cwd() }, - { option: "outDir", expected: process.cwd() }, -])("defaultOption (%p)", ({ option, expected }) => { - const parsedCommandLine = parseCommandLine([]); - if (parsedCommandLine.isValid) { - expect(expected).toBe(parsedCommandLine.result.options[option]); - } else { - expect(parsedCommandLine.isValid).toBeTruthy(); - } -}); - -test("ValidLuaTarget", () => { - const parsedCommandLine = parseCommandLine(["--luaTarget", "5.3"]); - if (parsedCommandLine.isValid) { - expect(parsedCommandLine.result.options["luaTarget"]).toBe("5.3"); - } else { - expect(parsedCommandLine.isValid).toBeTruthy(); - } -}); - -test("InvalidLuaTarget", () => { - // Don't check error message because the yargs library messes the message up. - const result = parseCommandLine(["--luaTarget", "42"]); - expect(result.isValid).toBe(false); -}); - -test("InvalidArgumentTSTL", () => { - // Don't check error message because the yargs library messes the message up. - const result = parseCommandLine(["--invalidTarget", "test"]); - expect(result.isValid).toBe(false); -}); - -test("outDir", () => { - const parsedCommandLine = parseCommandLine(["--outDir", "./test"]); - - if (parsedCommandLine.isValid) { - expect(parsedCommandLine.result.options["outDir"]).toBe("./test"); - } else { - expect(parsedCommandLine.isValid).toBeTruthy(); - } -}); - -test("rootDir", () => { - const parsedCommandLine = parseCommandLine(["--rootDir", "./test"]); - - if (parsedCommandLine.isValid) { - expect(parsedCommandLine.result.options["rootDir"]).toBe("./test"); - expect(parsedCommandLine.result.options["outDir"]).toBe("./test"); - } else { - expect(parsedCommandLine.isValid).toBeTruthy(); - } -}); - -test("outDirAndRooDir", () => { - const parsedCommandLine = parseCommandLine([ - "--outDir", - "./testOut", - "--rootDir", - "./testRoot", - ]); - - if (parsedCommandLine.isValid) { - expect(parsedCommandLine.result.options["outDir"]).toBe("./testOut"); - expect(parsedCommandLine.result.options["rootDir"]).toBe("./testRoot"); - } else { - expect(parsedCommandLine.isValid).toBeTruthy(); - } -}); - -test("Find config no path", () => { - const result = findConfigFile({ options: {}, fileNames: [], errors: [] }); - expect(result.isValid).toBe(false); -}); - -test.each([ - { tsConfig: "{}" }, - { tsConfig: `{ noHeader: true }`, expected: true }, - { tsConfig: `{ noHeader: "true" }`, expected: true }, - { tsConfig: `{ tstl: { noHeader: true } }`, expected: true }, - { tsConfig: `{ tstl: { noHeader: "true" } }`, expected: true }, -])("TsConfig noHeader (%p)", ({ tsConfig, expected }) => { - const result = parseTsConfigString(tsConfig, ""); - - if (result.isValid) { - expect(result.result.options.noHeader).toBe(expected); - } else { - expect(result.isValid).toBeTruthy(); - } +import * as ts from "typescript"; +import * as tstl from "../../src"; + +describe("command line", () => { + test("should support aliases", () => { + const full = tstl.parseCommandLine(["--luaTarget", "5.1"]); + const alias = tstl.parseCommandLine(["-lt", "5.1"]); + expect(full).toEqual(alias); + }); + + test("should support standard typescript options", () => { + const commandLine = "--project tsconfig.json --noHeader -t es3 -lt 5.3"; + const result = tstl.parseCommandLine(commandLine.split(" ")); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options).toEqual({ + project: "tsconfig.json", + noHeader: true, + target: ts.ScriptTarget.ES3, + luaTarget: tstl.LuaTarget.Lua53, + }); + }); + + test("should error on unknown options", () => { + const result = tstl.parseCommandLine(["--unknownOption"]); + + expect(result.errors).toHaveDiagnostics(); + }); + + test("should parse options case-insensitively", () => { + const result = tstl.parseCommandLine(["--NoHeader"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + }); + + describe("enum options", () => { + test("should parse enums", () => { + const result = tstl.parseCommandLine(["--luaTarget", "5.1"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.Lua51); + }); + + test("should be case-insensitive", () => { + for (const value of ["jit", "JiT", "JIT"]) { + const result = tstl.parseCommandLine(["--luaTarget", value]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.LuaJIT); + } + }); + + test("should error on invalid value", () => { + const result = tstl.parseCommandLine(["--luaTarget", "invalid"]); + + expect(result.errors).toHaveDiagnostics(); + }); + }); + + describe("boolean options", () => { + test.each([true, false])("should parse booleans (%p)", value => { + const result = tstl.parseCommandLine(["--noHeader", value.toString()]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(value); + }); + + test("should be case-sensitive", () => { + const result = tstl.parseCommandLine(["--noHeader", "FALSE"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + expect(result.fileNames).toEqual(["FALSE"]); + }); + + test("should be parsed without a value", () => { + const result = tstl.parseCommandLine(["--noHeader"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + }); + + test("shouldn't parse following arguments as values", () => { + const result = tstl.parseCommandLine(["--noHeader", "--noHoisting"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + expect(result.options.noHoisting).toBe(true); + }); + + test("shouldn't parse following files as values", () => { + const result = tstl.parseCommandLine(["--noHeader", "file.ts"]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(true); + }); + }); + + describe("integration", () => { + test.each<[string, string, tstl.CompilerOptions]>([ + ["noHeader", "false", { noHeader: false }], + ["noHeader", "true", { noHeader: true }], + ["noHoisting", "false", { noHoisting: false }], + ["noHoisting", "true", { noHoisting: true }], + ["sourceMapTraceback", "false", { sourceMapTraceback: false }], + ["sourceMapTraceback", "true", { sourceMapTraceback: true }], + + ["luaLibImport", "none", { luaLibImport: tstl.LuaLibImportKind.None }], + ["luaLibImport", "always", { luaLibImport: tstl.LuaLibImportKind.Always }], + ["luaLibImport", "inline", { luaLibImport: tstl.LuaLibImportKind.Inline }], + ["luaLibImport", "require", { luaLibImport: tstl.LuaLibImportKind.Require }], + + ["luaTarget", "5.1", { luaTarget: tstl.LuaTarget.Lua51 }], + ["luaTarget", "5.2", { luaTarget: tstl.LuaTarget.Lua52 }], + ["luaTarget", "5.3", { luaTarget: tstl.LuaTarget.Lua53 }], + ["luaTarget", "jit", { luaTarget: tstl.LuaTarget.LuaJIT }], + ])("--%s %s", (optionName, value, expected) => { + const result = tstl.parseCommandLine([`--${optionName}`, value]); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options).toEqual(expected); + }); + }); +}); + +describe("tsconfig", () => { + const parseConfigFileContent = (config: any) => { + // Specifying `files` option disables automatic file searching, that includes all files in + // the project, making these tests slow. Empty file list is considered as an error. + config.files = ["src/index.ts"]; + return tstl.updateParsedConfigFile(ts.parseJsonConfigFileContent(config, ts.sys, "")); + }; + + test("should support deprecated root-level options", () => { + const rootLevel = parseConfigFileContent({ noHeader: true }); + const namespaced = parseConfigFileContent({ tstl: { noHeader: true } }); + + expect(rootLevel.errors).toEqual([ + expect.objectContaining({ category: ts.DiagnosticCategory.Warning }), + ]); + expect(rootLevel.options).toEqual(namespaced.options); + }); + + test("should allow unknown root-level options", () => { + const result = parseConfigFileContent({ unknownOption: true }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.unknownOption).toBeUndefined(); + }); + + test("should error on unknown namespaced options", () => { + const result = parseConfigFileContent({ tstl: { unknownOption: true } }); + + expect(result.errors).toHaveDiagnostics(); + expect(result.options.unknownOption).toBeUndefined(); + }); + + test("should parse options case-sensitively", () => { + const result = parseConfigFileContent({ tstl: { NoHeader: true } }); + + expect(result.errors).toHaveDiagnostics(); + expect(result.options.NoHeader).toBeUndefined(); + expect(result.options.noHeader).toBeUndefined(); + }); + + describe("enum options", () => { + test("should parse enums", () => { + const result = parseConfigFileContent({ tstl: { luaTarget: "5.1" } }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.Lua51); + }); + + test("should be case-insensitive", () => { + for (const value of ["jit", "JiT", "JIT"]) { + const result = parseConfigFileContent({ tstl: { luaTarget: value } }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.luaTarget).toBe(tstl.LuaTarget.LuaJIT); + } + }); + + test("should error on invalid value", () => { + const result = parseConfigFileContent({ tstl: { luaTarget: "invalid" } }); + + expect(result.errors).toHaveDiagnostics(); + }); + }); + + describe("boolean options", () => { + test.each([true, false])("should parse booleans (%p)", value => { + const result = parseConfigFileContent({ tstl: { noHeader: value } }); + + expect(result.errors).not.toHaveDiagnostics(); + expect(result.options.noHeader).toBe(value); + }); + + test("shouldn't parse strings", () => { + const result = parseConfigFileContent({ tstl: { noHeader: "true" } }); + + expect(result.errors).toHaveDiagnostics(); + expect(result.options.noHeader).toBeUndefined(); + }); + }); }); diff --git a/test/unit/compiler/configuration/mixed/index.spec.ts b/test/unit/compiler/configuration/mixed/index.spec.ts deleted file mode 100644 index cb98a7d45..000000000 --- a/test/unit/compiler/configuration/mixed/index.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as ts from "typescript"; -import { parseCommandLine } from "../../../../../src/CommandLineParser"; -import { CompilerOptions, LuaLibImportKind } from "../../../../../src/CompilerOptions"; - -test("tsconfig.json mixed with cmd line args", () => { - const rootPath = __dirname; - const tsConfigPath = path.join(rootPath, "project-tsconfig.json"); - const expectedTsConfig = ts.parseJsonConfigFileContent( - ts.parseConfigFileTextToJson(tsConfigPath, fs.readFileSync(tsConfigPath).toString()).config, - ts.sys, - path.dirname(tsConfigPath), - ); - - const parsedArgs = parseCommandLine([ - "-p", - `"${tsConfigPath}"`, - "--luaLibImport", - LuaLibImportKind.Inline, - `${path.join(rootPath, "test.ts")}`, - ]); - - if (parsedArgs.isValid === true) { - expect(parsedArgs.result.options).toEqual({ - ...expectedTsConfig.options, - // Overridden by cmd args (set to "none" in project-tsconfig.json) - luaLibImport: LuaLibImportKind.Inline, - // Only set in tsconfig, TSTL default is "JIT" - luaTarget: "5.1", - // Only present in TSTL dfaults - noHeader: false, - project: tsConfigPath, - noHoisting: false, - sourceMapTraceback: false, - } as CompilerOptions); - } else { - expect(parsedArgs.isValid).toBeTruthy(); - } -}); diff --git a/test/unit/compiler/configuration/mixed/project-tsconfig.json b/test/unit/compiler/configuration/mixed/project-tsconfig.json deleted file mode 100644 index 3fe0b7e03..000000000 --- a/test/unit/compiler/configuration/mixed/project-tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist/foo/bar", - "rootDir": "./src/foo/bar" - }, - "luaTarget": "5.1", - "luaLibImport": "none" -} diff --git a/test/unit/compiler/configuration/options.spec.ts b/test/unit/compiler/configuration/options.spec.ts deleted file mode 100644 index 31b9a6fe1..000000000 --- a/test/unit/compiler/configuration/options.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { LuaLibImportKind, LuaTarget } from "../../../../src/CompilerOptions"; -import * as util from "../../../util"; - -test.each([LuaTarget.LuaJIT, "jit", "JiT"])("Options luaTarget case-insensitive (%p)", target => { - const options = { luaTarget: target as LuaTarget }; - const result = util.transpileString("~a", options); - - expect(result).toBe("local ____ = bit.bnot(a)"); -}); - -test.each([LuaLibImportKind.None, "none", "NoNe"])( - "Options luaLibImport case-insensitive (%p)", - importKind => { - const options = { luaLibImport: importKind as LuaLibImportKind }; - const result = util.transpileString("const a = new Map();", options); - - expect(result).toBe("local a = Map.new()"); - }, -); diff --git a/test/unit/conditionals.spec.ts b/test/unit/conditionals.spec.ts index 6d79a37a8..d09d54a7f 100644 --- a/test/unit/conditionals.spec.ts +++ b/test/unit/conditionals.spec.ts @@ -1,4 +1,4 @@ -import { LuaTarget } from "../../src/CompilerOptions"; +import * as tstl from "../../src"; import { TSTLErrors } from "../../src/TSTLErrors"; import * as util from "../util"; @@ -373,8 +373,8 @@ test("switch dead code after return", () => { test("switch not allowed in 5.1", () => { expect(() => - util.transpileString(`switch ("abc") {}`, { luaTarget: LuaTarget.Lua51 }), + util.transpileString(`switch ("abc") {}`, { luaTarget: tstl.LuaTarget.Lua51 }), ).toThrowExactError( - TSTLErrors.UnsupportedForTarget("Switch statements", LuaTarget.Lua51, util.nodeStub), + TSTLErrors.UnsupportedForTarget("Switch statements", tstl.LuaTarget.Lua51, util.nodeStub), ); }); diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 7a2268624..28f758a98 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; +import * as tstl from "../../src"; import { TSTLErrors } from "../../src/TSTLErrors"; import * as util from "../util"; @@ -99,8 +99,8 @@ test.each([ // Bit operations not supported in 5.1, expect an exception expect(() => util.transpileString(input, { - luaTarget: LuaTarget.Lua51, - luaLibImport: LuaLibImportKind.None, + luaTarget: tstl.LuaTarget.Lua51, + luaLibImport: tstl.LuaLibImportKind.None, }), ).toThrow(); }); @@ -120,7 +120,7 @@ test.each([ { input: "a>>>b", lua: "local ____ = bit.rshift(a, b)" }, { input: "a>>>=b", lua: "a = bit.rshift(a, b)" }, ])("Bitop [JIT] (%p)", ({ input, lua }) => { - const options = { luaTarget: LuaTarget.LuaJIT, luaLibImport: LuaLibImportKind.None }; + const options = { luaTarget: tstl.LuaTarget.LuaJIT, luaLibImport: tstl.LuaLibImportKind.None }; expect(util.transpileString(input, options)).toBe(lua); }); @@ -139,7 +139,7 @@ test.each([ { input: "a>>>b", lua: "local ____ = bit32.rshift(a, b)" }, { input: "a>>>=b", lua: "a = bit32.rshift(a, b)" }, ])("Bitop [5.2] (%p)", ({ input, lua }) => { - const options = { luaTarget: LuaTarget.Lua52, luaLibImport: LuaLibImportKind.None }; + const options = { luaTarget: tstl.LuaTarget.Lua52, luaLibImport: tstl.LuaLibImportKind.None }; expect(util.transpileString(input, options)).toBe(lua); }); @@ -156,15 +156,15 @@ test.each([ { input: "a>>>b", lua: "local ____ = a >> b" }, { input: "a>>>=b", lua: "a = a >> b" }, ])("Bitop [5.3] (%p)", ({ input, lua }) => { - const options = { luaTarget: LuaTarget.Lua53, luaLibImport: LuaLibImportKind.None }; + const options = { luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }; expect(util.transpileString(input, options)).toBe(lua); }); test.each(["a>>b", "a>>=b"])("Unsupported bitop 5.3 (%p)", input => { expect(() => util.transpileString(input, { - luaTarget: LuaTarget.Lua53, - luaLibImport: LuaLibImportKind.None, + luaTarget: tstl.LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.None, }), ).toThrowExactError( TSTLErrors.UnsupportedKind( @@ -232,12 +232,20 @@ test.each([ }, { input: "true ? undefined : true", options: { strictNullChecks: true } }, { input: "true ? null : true", options: { strictNullChecks: true } }, - { input: "true ? false : true", expected: false, options: { luaTarget: LuaTarget.Lua51 } }, - { input: "false ? false : true", expected: true, options: { luaTarget: LuaTarget.Lua51 } }, - { input: "true ? undefined : true", options: { luaTarget: LuaTarget.Lua51 } }, - { input: "true ? false : true", expected: false, options: { luaTarget: LuaTarget.LuaJIT } }, - { input: "false ? false : true", expected: true, options: { luaTarget: LuaTarget.LuaJIT } }, - { input: "true ? undefined : true", options: { luaTarget: LuaTarget.LuaJIT } }, + { input: "true ? false : true", expected: false, options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { input: "false ? false : true", expected: true, options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { input: "true ? undefined : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { + input: "true ? false : true", + expected: false, + options: { luaTarget: tstl.LuaTarget.LuaJIT }, + }, + { + input: "false ? false : true", + expected: true, + options: { luaTarget: tstl.LuaTarget.LuaJIT }, + }, + { input: "true ? undefined : true", options: { luaTarget: tstl.LuaTarget.LuaJIT } }, ])("Ternary operator (%p)", ({ input, expected, options }) => { const result = util.transpileAndExecute( `const literalValue = 'literal'; @@ -487,7 +495,7 @@ test("Incompatible fromCodePoint expression error", () => { expect(() => util.transpileString("const abc = String.fromCodePoint(123);")).toThrowExactError( TSTLErrors.UnsupportedForTarget( "string property fromCodePoint", - LuaTarget.Lua53, + tstl.LuaTarget.Lua53, util.nodeStub, ), ); @@ -495,7 +503,11 @@ test("Incompatible fromCodePoint expression error", () => { test("Unknown string expression error", () => { expect(() => util.transpileString("const abc = String.abcd();")).toThrowExactError( - TSTLErrors.UnsupportedForTarget("string property abcd", LuaTarget.Lua53, util.nodeStub), + TSTLErrors.UnsupportedForTarget( + "string property abcd", + tstl.LuaTarget.Lua53, + util.nodeStub, + ), ); }); diff --git a/test/unit/json.spec.ts b/test/unit/json.spec.ts index 079188cba..e651f71d6 100644 --- a/test/unit/json.spec.ts +++ b/test/unit/json.spec.ts @@ -12,7 +12,7 @@ test.each(["0", '""', "[]", '[1, "2", []]', '{ "a": "b" }', '{ "a": { "b": "c" } "JSON (%p)", json => { const lua = util - .transpileString(json, jsonOptions, false, "file.json") + .transpileString({ "main.json": json }, jsonOptions, false) .replace(/^return ([\s\S]+)$/, "return JSONStringify($1)"); const result = util.executeLua(lua); @@ -21,7 +21,7 @@ test.each(["0", '""', "[]", '[1, "2", []]', '{ "a": "b" }', '{ "a": { "b": "c" } ); test("Empty JSON", () => { - expect(() => util.transpileString("", jsonOptions, false, "file.json")).toThrowExactError( + expect(() => util.transpileString({ "main.json": "" }, jsonOptions, false)).toThrowExactError( TSTLErrors.InvalidJsonFileContent(util.nodeStub), ); }); diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index bf1439006..ddf9de97d 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -1,5 +1,5 @@ import * as ts from "typescript"; -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; +import * as tstl from "../../src"; import { TSTLErrors } from "../../src/TSTLErrors"; import * as util from "../util"; @@ -418,8 +418,8 @@ test("forof with iterator", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -444,8 +444,8 @@ test("forof with iterator and existing variable", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -469,8 +469,8 @@ test("forof destructuring with iterator", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -496,8 +496,8 @@ test("forof destructuring with iterator and existing variables", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -518,8 +518,8 @@ test("forof lua iterator", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -540,8 +540,8 @@ test("forof array lua iterator", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -563,8 +563,8 @@ test("forof lua iterator with existing variable", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -585,8 +585,8 @@ test("forof lua iterator destructuring", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -609,8 +609,8 @@ test("forof lua iterator destructuring with existing variables", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -634,8 +634,8 @@ test("forof lua iterator tuple-return", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -661,8 +661,8 @@ test("forof lua iterator tuple-return with existing variables", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -678,8 +678,8 @@ test("forof lua iterator tuple-return single variable", () => { for (let x of luaIter()) {} `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; expect(() => util.transpileString(code, compilerOptions)).toThrowExactError( @@ -697,8 +697,8 @@ test("forof lua iterator tuple-return single existing variable", () => { for (x of luaIter()) {} `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; expect(() => util.transpileString(code, compilerOptions)).toThrowExactError( @@ -725,8 +725,8 @@ test("forof forwarded lua iterator", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -754,8 +754,8 @@ test("forof forwarded lua iterator with tupleReturn", () => { return result; `; const compilerOptions = { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, target: ts.ScriptTarget.ES2015, }; const result = util.transpileAndExecute(code, compilerOptions); @@ -769,13 +769,17 @@ test.each([ "for (let a in b) { continue; }", "for (let a of b) { continue; }", ])("loop continue in different lua versions (%p)", loop => { - const lua51 = { luaTarget: LuaTarget.Lua51 }; - const lua52 = { luaTarget: LuaTarget.Lua52 }; - const lua53 = { luaTarget: LuaTarget.Lua53 }; - const luajit = { luaTarget: LuaTarget.LuaJIT }; + const lua51 = { luaTarget: tstl.LuaTarget.Lua51 }; + const lua52 = { luaTarget: tstl.LuaTarget.Lua52 }; + const lua53 = { luaTarget: tstl.LuaTarget.Lua53 }; + const luajit = { luaTarget: tstl.LuaTarget.LuaJIT }; expect(() => util.transpileString(loop, lua51)).toThrowExactError( - TSTLErrors.UnsupportedForTarget("Continue statement", LuaTarget.Lua51, ts.createContinue()), + TSTLErrors.UnsupportedForTarget( + "Continue statement", + tstl.LuaTarget.Lua51, + ts.createContinue(), + ), ); expect(util.transpileString(loop, lua52).indexOf("::__continue1::") !== -1).toBe(true); expect(util.transpileString(loop, lua53).indexOf("::__continue1::") !== -1).toBe(true); diff --git a/test/unit/lualib/inlining.spec.ts b/test/unit/lualib/inlining.spec.ts index f4a6e2b45..fcb5ba938 100644 --- a/test/unit/lualib/inlining.spec.ts +++ b/test/unit/lualib/inlining.spec.ts @@ -1,4 +1,4 @@ -import { LuaLibImportKind, LuaTarget } from "../../../src/CompilerOptions"; +import { LuaLibImportKind, LuaTarget } from "../../../src"; import * as util from "../../util"; test("map constructor", () => { diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts index 6da420d9e..679e6e615 100644 --- a/test/unit/modules.spec.ts +++ b/test/unit/modules.spec.ts @@ -1,4 +1,4 @@ -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; +import * as tstl from "../../src"; import { TSTLErrors } from "../../src/TSTLErrors"; import * as util from "../util"; @@ -10,8 +10,8 @@ test("defaultImport", () => { test("lualibRequire", () => { const lua = util.transpileString(`let a = b instanceof c;`, { - luaLibImport: LuaLibImportKind.Require, - luaTarget: LuaTarget.LuaJIT, + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.LuaJIT, }); expect(lua.startsWith(`require("lualib_bundle")`)); @@ -19,8 +19,8 @@ test("lualibRequire", () => { test("lualibRequireAlways", () => { const lua = util.transpileString(``, { - luaLibImport: LuaLibImportKind.Always, - luaTarget: LuaTarget.LuaJIT, + luaLibImport: tstl.LuaLibImportKind.Always, + luaTarget: tstl.LuaTarget.LuaJIT, }); expect(lua).toBe(`require("lualib_bundle");`); @@ -37,14 +37,15 @@ test("Non-exported module", () => { expect(result).toBe(3); }); -test.each([LuaLibImportKind.Inline, LuaLibImportKind.None, LuaLibImportKind.Require])( - "LuaLib no uses? No code (%p)", - impKind => { - const lua = util.transpileString(``, { luaLibImport: impKind }); +test.each([ + tstl.LuaLibImportKind.Inline, + tstl.LuaLibImportKind.None, + tstl.LuaLibImportKind.Require, +])("LuaLib no uses? No code (%p)", luaLibImport => { + const lua = util.transpileString(``, { luaLibImport }); - expect(lua).toBe(``); - }, -); + expect(lua).toBe(``); +}); test("Nested module with dot in name", () => { const code = `module a.b { diff --git a/test/unit/require.spec.ts b/test/unit/require.spec.ts index a7e5e378c..f8d6d9a4f 100644 --- a/test/unit/require.spec.ts +++ b/test/unit/require.spec.ts @@ -2,70 +2,70 @@ import * as util from "../util"; test.each([ { - filePath: "file.ts", + filePath: "main.ts", usedPath: "./folder/Module", expectedPath: "folder.Module", options: { rootDir: "." }, throwsError: false, }, { - filePath: "file.ts", + filePath: "main.ts", usedPath: "./folder/Module", expectedPath: "folder.Module", options: { rootDir: "./" }, throwsError: false, }, { - filePath: "src/file.ts", + filePath: "src/main.ts", usedPath: "./folder/Module", expectedPath: "src.folder.Module", options: { rootDir: "." }, throwsError: false, }, { - filePath: "file.ts", + filePath: "main.ts", usedPath: "folder/Module", expectedPath: "folder.Module", options: { rootDir: ".", baseUrl: "." }, throwsError: false, }, { - filePath: "file.ts", + filePath: "main.ts", usedPath: "folder/Module", expectedPath: "folder.Module", options: { rootDir: "./", baseUrl: "." }, throwsError: false, }, { - filePath: "src/file.ts", + filePath: "src/main.ts", usedPath: "./folder/Module", expectedPath: "folder.Module", options: { rootDir: "src" }, throwsError: false, }, { - filePath: "src/file.ts", + filePath: "src/main.ts", usedPath: "./folder/Module", expectedPath: "folder.Module", options: { rootDir: "./src" }, throwsError: false, }, { - filePath: "file.ts", + filePath: "main.ts", usedPath: "../Module", expectedPath: "", options: { rootDir: "./src" }, throwsError: true, }, { - filePath: "src/dir/file.ts", + filePath: "src/dir/main.ts", usedPath: "../Module", expectedPath: "Module", options: { rootDir: "./src" }, throwsError: false, }, { - filePath: "src/dir/dir/file.ts", + filePath: "src/dir/dir/main.ts", usedPath: "../../dir/Module", expectedPath: "dir.Module", options: { rootDir: "./src" }, @@ -74,17 +74,11 @@ test.each([ ])( "require paths root from --baseUrl or --rootDir (%p)", ({ filePath, usedPath, expectedPath, options, throwsError }) => { + const input = { [filePath]: `import * from "${usedPath}";` }; if (throwsError) { - expect(() => - util.transpileString(`import * from "${usedPath}";`, options, true, filePath), - ).toThrow(); + expect(() => util.transpileString(input, options)).toThrow(); } else { - const lua = util.transpileString( - `import * from "${usedPath}";`, - options, - true, - filePath, - ); + const lua = util.transpileString(input, options); const regex = /require\("(.*?)"\)/; const match = regex.exec(lua); @@ -101,15 +95,10 @@ test.each([ ])( "noResolution on ambient modules causes no path alterations (%p)", ({ comment, expectedPath }) => { - const lua = util.transpileString( - { - "src/file.ts": `import * as fake from "fake";`, - "module.d.ts": `${comment} declare module "fake" {}`, - }, - undefined, - true, - "src/file.ts", - ); + const lua = util.transpileString({ + "src/main.ts": `import * as fake from "fake";`, + "module.d.ts": `${comment} declare module "fake" {}`, + }); const regex = /require\("(.*?)"\)/; const match = regex.exec(lua); diff --git a/test/unit/sourcemaps.spec.ts b/test/unit/sourcemaps.spec.ts index fef652744..905e3dc05 100644 --- a/test/unit/sourcemaps.spec.ts +++ b/test/unit/sourcemaps.spec.ts @@ -1,6 +1,6 @@ +import { Position, SourceMapConsumer } from "source-map"; +import * as tstl from "../../src"; import * as util from "../util"; -import { LuaLibImportKind, CompilerOptions } from "../../src/CompilerOptions"; -import { SourceMapConsumer, Position } from "source-map"; test.each([ { @@ -47,13 +47,14 @@ test.each([ }, ])("Source map has correct mapping (%p)", async ({ typeScriptSource, assertPatterns }) => { // Act - const { lua, sourceMap } = util.transpileStringResult(typeScriptSource); + const { file } = util.transpileStringResult(typeScriptSource); // Assert - const consumer = await new SourceMapConsumer(sourceMap); + if (!util.expectToBeDefined(file.lua) || !util.expectToBeDefined(file.sourceMap)) return; + const consumer = await new SourceMapConsumer(file.sourceMap); for (const { luaPattern, typeScriptPattern } of assertPatterns) { - const luaPosition = lineAndColumnOf(lua, luaPattern); + const luaPosition = lineAndColumnOf(file.lua, luaPattern); const mappedPosition = consumer.originalPositionFor(luaPosition); const typescriptPosition = lineAndColumnOf(typeScriptSource, typeScriptPattern); @@ -71,7 +72,10 @@ test("sourceMapTraceback saves sourcemap in _G", () => { } return JSONStringify(_G.__TS__sourcemap);`; - const options = { sourceMapTraceback: true, luaLibImport: LuaLibImportKind.Inline }; + const options: tstl.CompilerOptions = { + sourceMapTraceback: true, + luaLibImport: tstl.LuaLibImportKind.Inline, + }; // Act const transpiledLua = util.transpileString(typeScriptSource, options); @@ -119,21 +123,20 @@ test("Inline sourcemaps", () => { } return abc();`; - const compilerOptions: CompilerOptions = { - inlineSourceMap: true, - }; + const compilerOptions: tstl.CompilerOptions = { inlineSourceMap: true }; - const { lua, sourceMap } = util.transpileStringResult(typeScriptSource, compilerOptions); + const { file } = util.transpileStringResult(typeScriptSource, compilerOptions); + if (!util.expectToBeDefined(file.lua)) return; - const inlineSourceMapMatch = lua.match( + const inlineSourceMapMatch = file.lua.match( /--# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/, ); if (util.expectToBeDefined(inlineSourceMapMatch)) { const inlineSourceMap = Buffer.from(inlineSourceMapMatch[1], "base64").toString(); - expect(sourceMap).toBe(inlineSourceMap); + expect(file.sourceMap).toBe(inlineSourceMap); - expect(util.executeLua(lua)).toBe("foo"); + expect(util.executeLua(file.lua)).toBe("foo"); } }); diff --git a/test/unit/spreadElement.spec.ts b/test/unit/spreadElement.spec.ts index dcd40db5e..dfcb345d1 100644 --- a/test/unit/spreadElement.spec.ts +++ b/test/unit/spreadElement.spec.ts @@ -1,4 +1,4 @@ -import { LuaLibImportKind, LuaTarget } from "../../src/CompilerOptions"; +import * as tstl from "../../src"; import * as util from "../util"; test.each([{ inp: [] }, { inp: [1, 2, 3] }, { inp: [1, "test", 3] }])( @@ -13,25 +13,37 @@ test.each([{ inp: [] }, { inp: [1, 2, 3] }, { inp: [1, "test", 3] }])( test("Spread Element Lua 5.1", () => { // Cant test functional because our VM doesn't run on 5.1 - const options = { luaTarget: LuaTarget.Lua51, luaLibImport: LuaLibImportKind.None }; + const options: tstl.CompilerOptions = { + luaTarget: tstl.LuaTarget.Lua51, + luaLibImport: tstl.LuaLibImportKind.None, + }; const lua = util.transpileString(`[].push(...${JSON.stringify([1, 2, 3])});`, options); expect(lua).toBe("__TS__ArrayPush({}, unpack({\n 1,\n 2,\n 3,\n}))"); }); test("Spread Element Lua 5.2", () => { - const options = { luaTarget: LuaTarget.Lua52, luaLibImport: LuaLibImportKind.None }; + const options: tstl.CompilerOptions = { + luaTarget: tstl.LuaTarget.Lua52, + luaLibImport: tstl.LuaLibImportKind.None, + }; const lua = util.transpileString(`[...[0, 1, 2]]`, options); expect(lua).toBe("local ____ = {table.unpack({\n 0,\n 1,\n 2,\n})}"); }); test("Spread Element Lua 5.3", () => { - const options = { luaTarget: LuaTarget.Lua53, luaLibImport: LuaLibImportKind.None }; + const options: tstl.CompilerOptions = { + luaTarget: tstl.LuaTarget.Lua53, + luaLibImport: tstl.LuaLibImportKind.None, + }; const lua = util.transpileString(`[...[0, 1, 2]]`, options); expect(lua).toBe("local ____ = {table.unpack({\n 0,\n 1,\n 2,\n})}"); }); test("Spread Element Lua JIT", () => { - const options = { luaTarget: "JiT" as LuaTarget, luaLibImport: LuaLibImportKind.None }; + const options: tstl.CompilerOptions = { + luaTarget: tstl.LuaTarget.LuaJIT, + luaLibImport: tstl.LuaLibImportKind.None, + }; const lua = util.transpileString(`[...[0, 1, 2]]`, options); expect(lua).toBe("local ____ = {unpack({\n 0,\n 1,\n 2,\n})}"); }); diff --git a/test/util.ts b/test/util.ts index fd09d9541..b43f09dc5 100644 --- a/test/util.ts +++ b/test/util.ts @@ -2,115 +2,54 @@ import { lauxlib, lua, lualib, to_jsstring, to_luastring } from "fengari"; import * as fs from "fs"; import * as path from "path"; import * as ts from "typescript"; -import { - createStringCompilerProgram, - transpileString as compilerTranspileString, -} from "../src/Compiler"; -import { CompilerOptions, LuaLibImportKind, LuaTarget } from "../src/CompilerOptions"; -import { LuaTransformer } from "../src/LuaTransformer"; -import { TranspileResult } from "../src/LuaTranspiler"; +import * as tstl from "../src"; export const nodeStub = ts.createNode(ts.SyntaxKind.Unknown); -declare global { - namespace jest { - interface Matchers { - toThrowExactError(error: Error): void; - } - } -} - -expect.extend({ - toThrowExactError( - callback: () => void, - error: Error, - ): { pass: boolean; message: () => string } { - if (this.isNot) { - return { pass: true, message: () => "Inverted toThrowExactError is not implemented" }; - } - - let executionError: Error | undefined; - try { - callback(); - } catch (err) { - executionError = err; - } - - expect(() => { - if (executionError) throw executionError; - }).toThrowError(error.constructor as any); - expect(() => { - if (executionError) throw executionError; - }).toThrowError(error); - - return { pass: true, message: () => "" }; - }, -}); - -function compilerTranspile( - str: string | { [filename: string]: string }, - options: CompilerOptions = {}, - ignoreDiagnostics = true, - filePath = "file.ts", -): TranspileResult { - return compilerTranspileString( - str, - { - luaLibImport: LuaLibImportKind.Inline, - luaTarget: LuaTarget.Lua53, - noHeader: true, - skipLibCheck: true, - target: ts.ScriptTarget.ESNext, - lib: [ - "lib.es2015.d.ts", - "lib.es2016.d.ts", - "lib.es2017.d.ts", - "lib.es2018.d.ts", - "lib.esnext.d.ts", - ], - ...options, - }, - ignoreDiagnostics, - filePath, - ); -} - export function transpileString( str: string | { [filename: string]: string }, - options: CompilerOptions = {}, + options: tstl.CompilerOptions = {}, ignoreDiagnostics = true, - filePath = "file.ts", ): string { - const { lua } = transpileStringResult(str, options, ignoreDiagnostics, filePath); - return lua.trim(); + const { diagnostics, file } = transpileStringResult(str, options); + if (!expectToBeDefined(file) || !expectToBeDefined(file.lua)) return ""; + + const errors = diagnostics.filter(d => !ignoreDiagnostics || d.source === "typescript-to-lua"); + expect(errors).not.toHaveDiagnostics(); + + return file.lua.trim(); } export function transpileStringResult( - str: string | { [filename: string]: string }, - options: CompilerOptions = {}, - ignoreDiagnostics = true, - filePath = "file.ts", -): TranspileResult { - return compilerTranspileString( - str, - { - luaLibImport: LuaLibImportKind.Inline, - luaTarget: LuaTarget.Lua53, - noHeader: true, - skipLibCheck: true, - target: ts.ScriptTarget.ESNext, - lib: [ - "lib.es2015.d.ts", - "lib.es2016.d.ts", - "lib.es2017.d.ts", - "lib.es2018.d.ts", - "lib.esnext.d.ts", - ], - ...options, - }, - ignoreDiagnostics, - filePath, + input: string | Record, + options: tstl.CompilerOptions = {}, +): Required { + const optionsWithDefaults = { + luaTarget: tstl.LuaTarget.Lua53, + noHeader: true, + skipLibCheck: true, + target: ts.ScriptTarget.ESNext, + lib: [ + "lib.es2015.d.ts", + "lib.es2016.d.ts", + "lib.es2017.d.ts", + "lib.es2018.d.ts", + "lib.esnext.d.ts", + ], + ...options, + }; + + const { diagnostics, transpiledFiles } = tstl.transpileVirtualProject( + typeof input === "string" ? { "main.ts": input } : input, + optionsWithDefaults, ); + + const mainFileName = [...transpiledFiles.keys()].find(x => /\bmain\.[a-z]+$/.test(x)); + if (mainFileName === undefined) { + throw new Error('Program should have a file named "main"'); + } + + return { diagnostics, file: transpiledFiles.get(mainFileName)! }; } const lualibContent = fs.readFileSync( @@ -154,14 +93,13 @@ export function executeLua(luaStr: string, withLib = true): any { } // Get a mock transformer to use for testing -export function makeTestTransformer(target: LuaTarget = LuaTarget.Lua53): LuaTransformer { - const options = { luaTarget: target }; - return new LuaTransformer(ts.createProgram([], options), options); +export function makeTestTransformer(luaTarget = tstl.LuaTarget.Lua53): tstl.LuaTransformer { + return new tstl.LuaTransformer(ts.createProgram([], { luaTarget })); } export function transpileAndExecute( tsStr: string, - compilerOptions?: CompilerOptions, + compilerOptions?: tstl.CompilerOptions, luaHeader?: string, tsHeader?: string, ): any { @@ -179,7 +117,7 @@ export function transpileAndExecute( export function transpileExecuteAndReturnExport( tsStr: string, returnExport: string, - compilerOptions?: CompilerOptions, + compilerOptions?: tstl.CompilerOptions, luaHeader?: string, ): any { const wrappedTsString = `declare function JSONStringify(this: void, p: any): string; @@ -195,13 +133,13 @@ export function transpileExecuteAndReturnExport( export function parseTypeScript( typescript: string, - target: LuaTarget = LuaTarget.Lua53, + target: tstl.LuaTarget = tstl.LuaTarget.Lua53, ): [ts.SourceFile, ts.TypeChecker] { - const program = createStringCompilerProgram(typescript, { luaTarget: target }); - const sourceFile = program.getSourceFile("file.ts"); + const program = tstl.createVirtualProgram({ "main.ts": typescript }, { luaTarget: target }); + const sourceFile = program.getSourceFile("main.ts"); if (sourceFile === undefined) { - throw new Error("Could not find source file file.ts in program."); + throw new Error("Could not find source file main.ts in program."); } return [sourceFile, program.getTypeChecker()]; diff --git a/tsconfig.json b/tsconfig.json index 59ea10e64..e6ecaae97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "declaration": true, "sourceMap": true, "target": "es2017", - "module": "commonjs" + "module": "commonjs", + "stripInternal": true }, "include": ["src"], "exclude": ["src/lualib"]