Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -18,11 +41,6 @@ Add the lualib files from dist/ to your project. This helper library unlocks add
- Includes lua `Map<S,T>` and `Set<T>` 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`).

Expand All @@ -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`.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"tstl": "./dist/Compiler.js"
},
"dependencies": {
"minimist": "^1.2.0",
"typescript": "^2.7.2"
},
"devDependencies": {
Expand Down
171 changes: 129 additions & 42 deletions src/Compiler.ts
Original file line number Diff line number Diff line change
@@ -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 ((<string><any>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}`);
Expand All @@ -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) {
Expand All @@ -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<indent;i++) indentStr += " ";
for (let i = 0; i < indent; i++) indentStr += " ";

console.log(indentStr + tsEx.enumName(node.kind, ts.SyntaxKind));
node.forEachChild(child => 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<string>, invalidOptions: ReadonlyArray<string>): ReadonlyArray<string> {
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<string>) {
// 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));
14 changes: 10 additions & 4 deletions src/Transpiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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 (<string>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[<string>identifier.escapedText]) {
return `${translation[<string>identifier.escapedText]}`;
} else {
Expand Down