Skip to content

Commit b1eb041

Browse files
authored
Shebang support (#924)
1 parent 5a6ba56 commit b1eb041

File tree

10 files changed

+87
-60
lines changed

10 files changed

+87
-60
lines changed

src/LuaAST.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
// because we don't create the AST from text
66

77
import * as ts from "typescript";
8+
import { LuaLibFeature } from "./transformation/utils/lualib";
89
import { castArray } from "./utils";
910

1011
export enum SyntaxKind {
12+
File,
1113
Block,
1214

1315
// Statements
@@ -186,6 +188,30 @@ export function getOriginalPos(node: Node): TextRange {
186188
return { line: node.line, column: node.column };
187189
}
188190

191+
export interface File extends Node {
192+
kind: SyntaxKind.File;
193+
statements: Statement[];
194+
luaLibFeatures: Set<LuaLibFeature>;
195+
trivia: string;
196+
}
197+
198+
export function isFile(node: Node): node is File {
199+
return node.kind === SyntaxKind.File;
200+
}
201+
202+
export function createFile(
203+
statements: Statement[],
204+
luaLibFeatures: Set<LuaLibFeature>,
205+
trivia: string,
206+
tsOriginal?: ts.Node
207+
): File {
208+
const file = createNode(SyntaxKind.File, tsOriginal) as File;
209+
file.statements = statements;
210+
file.luaLibFeatures = luaLibFeatures;
211+
file.trivia = trivia;
212+
return file;
213+
}
214+
189215
export interface Block extends Node {
190216
kind: SyntaxKind.Block;
191217
statements: Statement[];

src/LuaPrinter.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as lua from "./LuaAST";
66
import { loadLuaLibFeatures, LuaLibFeature } from "./LuaLib";
77
import { isValidLuaIdentifier } from "./transformation/utils/safe-names";
88
import { EmitHost } from "./transpilation";
9-
import { intersperse, trimExtension, normalizeSlashes } from "./utils";
9+
import { intersperse, normalizeSlashes, trimExtension } from "./utils";
1010

1111
// https://www.lua.org/pil/2.4.html
1212
// https://www.ecma-international.org/ecma-262/10.0/index.html#table-34
@@ -71,13 +71,7 @@ function isSimpleExpression(expression: lua.Expression): boolean {
7171

7272
type SourceChunk = string | SourceNode;
7373

74-
export type Printer = (
75-
program: ts.Program,
76-
emitHost: EmitHost,
77-
fileName: string,
78-
block: lua.Block,
79-
luaLibFeatures: Set<LuaLibFeature>
80-
) => PrintResult;
74+
export type Printer = (program: ts.Program, emitHost: EmitHost, fileName: string, file: lua.File) => PrintResult;
8175

8276
export interface PrintResult {
8377
code: string;
@@ -87,7 +81,7 @@ export interface PrintResult {
8781

8882
export function createPrinter(printers: Printer[]): Printer {
8983
if (printers.length === 0) {
90-
return (program, emitHost, fileName, ...args) => new LuaPrinter(emitHost, program, fileName).print(...args);
84+
return (program, emitHost, fileName, file) => new LuaPrinter(emitHost, program, fileName).print(file);
9185
} else if (printers.length === 1) {
9286
return printers[0];
9387
} else {
@@ -148,17 +142,17 @@ export class LuaPrinter {
148142
}
149143
}
150144

151-
public print(block: lua.Block, luaLibFeatures: Set<LuaLibFeature>): PrintResult {
145+
public print(file: lua.File): PrintResult {
152146
// Add traceback lualib if sourcemap traceback option is enabled
153147
if (this.options.sourceMapTraceback) {
154-
luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack);
148+
file.luaLibFeatures.add(LuaLibFeature.SourceMapTraceBack);
155149
}
156150

157151
const sourceRoot = this.options.sourceRoot
158152
? // According to spec, sourceRoot is simply prepended to the source name, so the slash should be included
159153
this.options.sourceRoot.replace(/[\\/]+$/, "") + "/"
160154
: "";
161-
const rootSourceNode = this.printImplementation(block, luaLibFeatures);
155+
const rootSourceNode = this.printFile(file);
162156
const sourceMap = this.buildSourceMap(sourceRoot, rootSourceNode);
163157

164158
let code = rootSourceNode.toString();
@@ -203,8 +197,8 @@ export class LuaPrinter {
203197
return `__TS__SourceMapTraceBack(debug.getinfo(1).short_src, ${mapString});`;
204198
}
205199

206-
private printImplementation(block: lua.Block, luaLibFeatures: Set<LuaLibFeature>): SourceNode {
207-
let header = "";
200+
private printFile(file: lua.File): SourceNode {
201+
let header = file.trivia;
208202

209203
if (!this.options.noHeader) {
210204
header += "--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]\n";
@@ -213,23 +207,21 @@ export class LuaPrinter {
213207
const luaLibImport = this.options.luaLibImport ?? LuaLibImportKind.Require;
214208
if (
215209
luaLibImport === LuaLibImportKind.Always ||
216-
(luaLibImport === LuaLibImportKind.Require && luaLibFeatures.size > 0)
210+
(luaLibImport === LuaLibImportKind.Require && file.luaLibFeatures.size > 0)
217211
) {
218212
// Require lualib bundle
219213
header += 'require("lualib_bundle");\n';
220-
} else if (luaLibImport === LuaLibImportKind.Inline && luaLibFeatures.size > 0) {
214+
} else if (luaLibImport === LuaLibImportKind.Inline && file.luaLibFeatures.size > 0) {
221215
// Inline lualib features
222216
header += "-- Lua Library inline imports\n";
223-
header += loadLuaLibFeatures(luaLibFeatures, this.emitHost);
217+
header += loadLuaLibFeatures(file.luaLibFeatures, this.emitHost);
224218
}
225219

226220
if (this.options.sourceMapTraceback) {
227221
header += "{#SourceMapTraceback}\n";
228222
}
229223

230-
const fileBlockNode = this.printBlock(block);
231-
232-
return this.concatNodes(header, fileBlockNode);
224+
return this.concatNodes(header, ...this.printStatementArray(file.statements));
233225
}
234226

235227
protected pushIndent(): void {

src/transformation/context/visitors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export type VisitorResult<T extends ts.Node> = T extends ExpressionLikeNode
143143
: T extends StatementLikeNode
144144
? OneToManyVisitorResult<lua.Statement>
145145
: T extends ts.SourceFile
146-
? lua.Block
146+
? lua.File
147147
: OneToManyVisitorResult<lua.Node>;
148148

149149
export type Visitor<T extends ts.Node> = FunctionVisitor<T> | ObjectVisitor<T>;

src/transformation/index.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import * as ts from "typescript";
22
import * as lua from "../LuaAST";
3-
import { LuaLibFeature } from "../LuaLib";
43
import { getOrUpdate } from "../utils";
54
import { ObjectVisitor, TransformationContext, VisitorMap, Visitors } from "./context";
6-
import { getUsedLuaLibFeatures } from "./utils/lualib";
75
import { standardVisitors } from "./visitors";
86

97
export function createVisitorMap(customVisitors: Visitors[]): VisitorMap {
@@ -29,23 +27,9 @@ export function createVisitorMap(customVisitors: Visitors[]): VisitorMap {
2927
return visitorMap;
3028
}
3129

32-
export interface TransformSourceFileResult {
33-
luaAst: lua.Block;
34-
luaLibFeatures: Set<LuaLibFeature>;
35-
diagnostics: ts.Diagnostic[];
36-
}
37-
38-
export function transformSourceFile(
39-
program: ts.Program,
40-
sourceFile: ts.SourceFile,
41-
visitorMap: VisitorMap
42-
): TransformSourceFileResult {
30+
export function transformSourceFile(program: ts.Program, sourceFile: ts.SourceFile, visitorMap: VisitorMap) {
4331
const context = new TransformationContext(program, sourceFile, visitorMap);
44-
const [luaAst] = context.transformNode(sourceFile) as [lua.Block];
32+
const [file] = context.transformNode(sourceFile) as [lua.File];
4533

46-
return {
47-
luaAst,
48-
luaLibFeatures: getUsedLuaLibFeatures(context),
49-
diagnostics: context.diagnostics,
50-
};
34+
return { file, diagnostics: context.diagnostics };
5135
}

src/transformation/visitors/sourceFile.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as lua from "../../LuaAST";
33
import { assert } from "../../utils";
44
import { FunctionVisitor } from "../context";
55
import { createExportsIdentifier } from "../utils/lua-ast";
6+
import { getUsedLuaLibFeatures } from "../utils/lualib";
67
import { performHoisting, popScope, pushScope, ScopeType } from "../utils/scope";
78
import { hasExportEquals } from "../utils/typescript";
89

@@ -39,5 +40,6 @@ export const transformSourceFileNode: FunctionVisitor<ts.SourceFile> = (node, co
3940
}
4041
}
4142

42-
return lua.createBlock(statements, node);
43+
const trivia = node.getFullText().match(/^#!.*\r?\n/)?.[0] ?? "";
44+
return lua.createFile(statements, getUsedLuaLibFeatures(context), trivia, node);
4345
};

src/transpilation/transpile.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,14 @@ export function getProgramTranspileResult(
6060
const visitorMap = createVisitorMap(plugins.map(p => p.visitors).filter(isNonNull));
6161
const printer = createPrinter(plugins.map(p => p.printer).filter(isNonNull));
6262
const processSourceFile = (sourceFile: ts.SourceFile) => {
63-
const { luaAst, luaLibFeatures, diagnostics: transformDiagnostics } = transformSourceFile(
64-
program,
65-
sourceFile,
66-
visitorMap
67-
);
63+
const { file, diagnostics: transformDiagnostics } = transformSourceFile(program, sourceFile, visitorMap);
6864

6965
diagnostics.push(...transformDiagnostics);
7066
if (!options.noEmit && !options.emitDeclarationOnly) {
71-
const printResult = printer(program, emitHost, sourceFile.fileName, luaAst, luaLibFeatures);
67+
const printResult = printer(program, emitHost, sourceFile.fileName, file);
7268
const sourceRootDir = program.getCommonSourceDirectory();
7369
const fileName = path.resolve(sourceRootDir, sourceFile.fileName);
74-
transpiledFiles.push({ sourceFiles: [sourceFile], fileName, luaAst, ...printResult });
70+
transpiledFiles.push({ sourceFiles: [sourceFile], fileName, luaAst: file, ...printResult });
7571
}
7672
};
7773

src/transpilation/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ interface BaseFile {
2121

2222
export interface ProcessedFile extends BaseFile {
2323
fileName: string;
24-
luaAst?: lua.Block;
24+
luaAst?: lua.File;
2525
/** @internal */
2626
sourceMapNode?: SourceNode;
2727
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`shebang CRLF 1`] = `
4+
"#!/usr/bin/env lua
5+
foo = true"
6+
`;
7+
8+
exports[`shebang LF 1`] = `
9+
"#!/usr/bin/env lua
10+
foo = true"
11+
`;

test/unit/file.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as util from "../util";
2+
3+
describe("JSON", () => {
4+
test.each([0, "", [], [1, "2", []], { a: "b" }, { a: { b: "c" } }])("JSON (%p)", json => {
5+
util.testModule(JSON.stringify(json)).setMainFileName("main.json").expectToEqual(json);
6+
});
7+
8+
test("empty file throws runtime error", () => {
9+
util.testModule("")
10+
.setMainFileName("main.json")
11+
.expectToEqual(new util.ExecutionError("Unexpected end of JSON input"));
12+
});
13+
});
14+
15+
describe("shebang", () => {
16+
test("LF", () => {
17+
util.testModule`#!/usr/bin/env lua\n
18+
const foo = true;
19+
`.expectLuaToMatchSnapshot();
20+
});
21+
22+
test("CRLF", () => {
23+
util.testModule`#!/usr/bin/env lua\r\n
24+
const foo = true;
25+
`.expectLuaToMatchSnapshot();
26+
});
27+
});

test/unit/json.spec.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)