diff --git a/README.md b/README.md index 736219150..6c897330d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,38 @@ # TypescriptToLua -Typescript to lua transpiler. - -[![Build Status](https://travis-ci.org/Perryvw/TypescriptToLua.svg?branch=master)](https://travis-ci.org/Perryvw/TypescriptToLua) -[![Coverage](https://codecov.io/gh/perryvw/typescripttolua/branch/master/graph/badge.svg)](https://codecov.io/gh/perryvw/typescripttolua) +[![Build Status](https://travis-ci.org/Perryvw/TypescriptToLua.svg?branch=master)](https://travis-ci.org/Perryvw/TypescriptToLua) [![Coverage](https://codecov.io/gh/perryvw/typescripttolua/branch/master/graph/badge.svg)](https://codecov.io/gh/perryvw/typescripttolua) +Typescript to lua transpiler. ## Usage Guide +**Install** + `npm install -g typescript-to-lua` -`tstl path/to-my-file.ts` +**Compile Files** + +`tstl path/to-my-file.ts path/to-my-other-file.ts` + +**Compile Projects** + +`tstl -p path/to-my/tsconfig.json` + +**Options** +``` +Syntax: tstl [options] [files...] + +In addtion to the options listed below you can also pass options for the +typescript compiler (For a list of options use tsc -h). NOTE: The tsc options +might have no effect. + +Options: + --version Show version number + --luaTarget, -l Specify Lua target version: 'JIT' (Default), '5.1', '5.2', + '5.3' + --project, -p Compile the project given the path to its configuration file, + or to a folder with a 'tsconfig.json' + --help Show this message +``` **Optionally:** Add the lualib files from dist/ to your project. This helper library unlocks additional typescript functions: @@ -18,11 +41,6 @@ Add the lualib files from dist/ to your project. This helper library unlocks add - Includes lua `Map` and `Set` implementations Add `require("typescript")` in your code code if you want to use the lualib functionality. -### Transpiling a TypeScript project to Lua -The compiler will automatically try to find a typescript configuration file `tsconfig.json` in the files. If found it will transpile all TypeScript files in subdirectories of the project. - -**To prevent accidental compilation to Lua, you are required to add a `"target": "lua"` entry in your tsconfig compilerOptions.** - ## Sublime Text integration This compiler works great in combination with the [Sublime Text Typescript plugin](https://github.com/Microsoft/TypeScript-Sublime-Plugin) (available through the package manager as `TypeScript`). @@ -33,7 +51,7 @@ To add the option to build with the Lua transpiler instead of the regular typesc ``` { - "cmd": ["tstl", "$file"] + "cmd": ["tstl", "-p", "$file"] } ``` Save this in your Sublime settings as a `TypeScriptToLua.sublime-build`. You can now select the TypeScriptToLua build system in `Tools > Build System` to build using the normal hotkey (`ctrl+B`), or if you have multiple TypeScript projects open, you can choose your compiler before building by pressing `ctrl+shift+B`. diff --git a/package-lock.json b/package-lock.json index c90b981cc..c276da76c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -514,6 +514,11 @@ "brace-expansion": "1.1.11" } }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, "nyc": { "version": "11.5.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.5.0.tgz", diff --git a/package.json b/package.json index e0a65fe18..b0e8b2266 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "tstl": "./dist/Compiler.js" }, "dependencies": { + "minimist": "^1.2.0", "typescript": "^2.7.2" }, "devDependencies": { diff --git a/src/Compiler.ts b/src/Compiler.ts index 5b034dd9a..20c16a4cc 100644 --- a/src/Compiler.ts +++ b/src/Compiler.ts @@ -1,25 +1,24 @@ #!/usr/bin/env node import * as ts from "typescript"; -import {readFileSync,writeFileSync} from "fs"; +import * as fs from "fs"; +import * as path from "path"; -import {LuaTranspiler, TranspileError} from "./Transpiler"; -import {TSHelper as tsEx} from "./TSHelper"; +// ES6 syntax broken +import minimist = require("minimist") +import dedent = require("dedent") -function compile(fileNames: string[], options: ts.CompilerOptions, projectRoot: string): void { - // Verify target - if ((options.target) != "lua") { - console.error("Wrong compilation target! Add \"target\": \"lua\" to your tsconfig.json!"); - process.exit(1); - } +import { LuaTranspiler, TranspileError } from "./Transpiler"; +import { TSHelper as tsEx } from "./TSHelper"; +function compile(fileNames: string[], options: ts.CompilerOptions): void { let program = ts.createProgram(fileNames, options); let checker = program.getTypeChecker(); // Get all diagnostics, ignore unsupported extension const diagnostics = ts.getPreEmitDiagnostics(program).filter(diag => diag.code != 6054); diagnostics.forEach(diagnostic => { - if (diagnostic.file) { + if (diagnostic.file) { let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); @@ -37,22 +36,24 @@ function compile(fileNames: string[], options: ts.CompilerOptions, projectRoot: program.getSourceFiles().forEach(sourceFile => { if (!sourceFile.isDeclarationFile) { - // Print AST for debugging - //printAST(sourceFile, 0); - try { + let rootDir = options.rootDir; + if (!rootDir) { + rootDir = process.cwd(); + } + // Transpile AST - const addHeader = options.noHeader === true ? false : true; - let lua = LuaTranspiler.transpileSourceFile(sourceFile, checker, addHeader); - let outPath = sourceFile.fileName.substring(0, sourceFile.fileName.lastIndexOf(".")) + ".lua"; + let lua = LuaTranspiler.transpileSourceFile(sourceFile, checker, options); + let outPath = sourceFile.fileName; if (options.outDir) { - var extension = options.outDir; - if (extension[extension.length - 1] != "/") extension = extension + "/"; - outPath = outPath.replace(projectRoot + "/", projectRoot + "/" + extension); - console.log(outPath); + outPath = path.join(options.outDir, sourceFile.fileName.replace(rootDir, "")); } + // change extension + var fileNameLua = path.basename(outPath, path.extname(outPath)) + '.lua'; + outPath = path.join(path.dirname(outPath), fileNameLua); + // Write output ts.sys.writeFile(outPath, lua); } catch (exception) { @@ -75,32 +76,118 @@ function compile(fileNames: string[], options: ts.CompilerOptions, projectRoot: function printAST(node: ts.Node, indent: number) { let indentStr = ""; - for (let i=0;i printAST(child, indent + 1)); } -// Try to find tsconfig.json -const filename = process.argv[2].split("\\").join("/"); -const filepath = filename.substring(0, filename.lastIndexOf("/")); -let configPath = ts.findConfigFile(filepath, ts.sys.fileExists); - -if (configPath) { - configPath = configPath.split("\\").join("/"); - const projectRoot = configPath.substring(0, configPath.lastIndexOf("/")); - - // Find all files - let files = ts.sys.readDirectory(projectRoot, [".ts"]); - - // Read config - let configFile = ts.readConfigFile(configPath, ts.sys.readFile); - if (configFile.error) { - console.error("Error occured:"); - console.error(configFile.error); - } else { - compile(files, configFile.config.compilerOptions, projectRoot); +// Removes the option and value form argv +function removeInvalidOptionsFromArgv(commandLine: ReadonlyArray, invalidOptions: ReadonlyArray): ReadonlyArray { + let result = commandLine.slice() + for (let opt of invalidOptions) { + let index = result.indexOf('--' + opt); + if (index === -1) { + index = result.indexOf('-' + opt) + } + if (index !== -1) { + result.splice(index, 2); + } } -} else { - console.error("Could not find tsconfig.json, place one in your project root!"); + return result; } + +// Polyfill for report diagnostics +function logError(commandLine: ts.ParsedCommandLine) { + if (commandLine.errors.length !== 0) { + commandLine.errors.forEach((err) => { + console.log(err.messageText); + }); + process.exit(1); + } +} + +function executeCommandLine(args: ReadonlyArray) { + // Right now luaTarget, version and help are the only cli options, if more are added we should + // add a more advanced custom parser + var argv = minimist(args, { + string: ['l'], + alias: { h: 'help', v: 'version', l: 'luaTarget' }, + default: { luaTarget: 'JIT' }, + '--': true, + stopEarly: true, + }); + + let tstlVersion + try { + tstlVersion = require('../package').version; + } catch (e) { + tstlVersion = "0.0.0"; + } + + var helpString = dedent(`Version: ${tstlVersion} + Syntax: tstl [options] [files...] + + In addtion to the options listed below you can also pass options for the + typescript compiler (For a list of options use tsc -h). NOTE: The tsc options + might have no effect. + + Options: + --version Show version number + --luaTarget, -l Specify Lua target version: 'JIT' (Default), '5.1', '5.2', + '5.3' + --project, -p Compile the project given the path to its configuration file, + or to a folder with a 'tsconfig.json' + --help Show this message + `); + if (argv.help) { + console.log(helpString); + process.exit(0); + } + if (argv.version) { + console.log("Version: " + require('../package').version); + } + let validLuaTargets = ['JIT', '5.3', '5.2', '5.1']; + if (!validLuaTargets.some(val => val === argv.luaTarget)) { + console.error(`Invalid lua target valid targets are: ${validLuaTargets.toString()}`); + } + + // Remove tstl options otherwise ts will emit an error + let sanitizedArgs = removeInvalidOptionsFromArgv(args, ['l', 'luaTarget']) + + let commandLine = ts.parseCommandLine(sanitizedArgs); + + logError(commandLine); + + commandLine.options.luaTarget = argv.luaTarget; + let configPath; + if (commandLine.options.project) { + configPath = path.isAbsolute(commandLine.options.project) ? commandLine.options.project : path.join(process.cwd(), commandLine.options.project); + if (fs.statSync(configPath).isDirectory()) { + configPath = path.join(configPath, 'tsconfig.json'); + } else if (fs.statSync(configPath).isFile() && path.extname(configPath) === ".ts") { + // if supplied project points to a .ts fiel we try to find the config + configPath = ts.findConfigFile(configPath, ts.sys.fileExists); + } + commandLine.options.project = configPath; + let configContents = fs.readFileSync(configPath).toString(); + const configJson = ts.parseConfigFileTextToJson(configPath, configContents); + commandLine = ts.parseJsonConfigFileContent(configJson.config, ts.sys, path.dirname(configPath), commandLine.options); + + // Append lua target to options array since its ignored by TS parsers + // option supplied on CLI is prioritized + if (argv.luaTarget === "JIT" && commandLine.raw.luaTarget !== argv.luaTarget) { + commandLine.options.luaTarget = commandLine.raw.luaTarget; + } + } + + if (configPath && !commandLine.options.rootDir) { + commandLine.options.rootDir = path.dirname(configPath); + } + + logError(commandLine); + + compile(commandLine.fileNames, commandLine.options); +} + +executeCommandLine(process.argv.slice(2)); diff --git a/src/Transpiler.ts b/src/Transpiler.ts index c5a9cd5d2..3d9dc5499 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -15,9 +15,9 @@ export class TranspileError extends Error { export class LuaTranspiler { // Transpile a source file - static transpileSourceFile(node: ts.SourceFile, checker: ts.TypeChecker, addHeader: boolean): string { - let transpiler = new LuaTranspiler(checker); - let header = addHeader ? "--=======================================================================================\n" + static transpileSourceFile(node: ts.SourceFile, checker: ts.TypeChecker, options: ts.CompilerOptions): string { + let transpiler = new LuaTranspiler(checker, options); + let header = options.addHeader ? "--=======================================================================================\n" + "-- Generated by TypescriptToLua transpiler https://github.com/Perryvw/TypescriptToLua \n" + "-- Date: " + new Date().toDateString() + "\n" + "--=======================================================================================\n" @@ -37,15 +37,17 @@ export class LuaTranspiler { indent: string; checker: ts.TypeChecker; + options: ts.CompilerOptions genVarCounter: number; transpilingSwitch: boolean; namespace: string[]; importCount: number; isModule: boolean; - constructor(checker: ts.TypeChecker) { + constructor(checker: ts.TypeChecker, options: ts.CompilerOptions) { this.indent = ""; this.checker = checker; + this.options = options; this.genVarCounter = 0; this.transpilingSwitch = false; this.namespace = []; @@ -703,6 +705,10 @@ export class LuaTranspiler { // TODO at check if compiler options is LUA 5.3 // should throw an exception if codepoint is used sub 5.3 + if (identifier.escapedText === "fromCodePoint" && this.options.luaTarget !== '5.3') { + throw new TranspileError(`Unsupported string property ${identifier.escapedText} is only supported for lua 5.3.`, identifier); + } + if (translation[identifier.escapedText]) { return `${translation[identifier.escapedText]}`; } else {