Skip to content

Commit 6da44e9

Browse files
committed
Add compatibility with ttypescript and support more transformer types
1 parent f04ac39 commit 6da44e9

File tree

11 files changed

+301
-170
lines changed

11 files changed

+301
-170
lines changed

src/CommandLineParser.ts

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ interface CommandLineOptionBase {
1111
name: string;
1212
aliases?: string[];
1313
description: string;
14-
isTSConfigOnly?: boolean;
1514
}
1615

1716
interface CommandLineOptionOfEnum extends CommandLineOptionBase {
@@ -23,15 +22,7 @@ interface CommandLineOptionOfBoolean extends CommandLineOptionBase {
2322
type: "boolean";
2423
}
2524

26-
interface CommandLineOptionOfListType extends CommandLineOptionBase {
27-
isTSConfigOnly: true;
28-
type: "list";
29-
}
30-
31-
type CommandLineOption =
32-
| CommandLineOptionOfEnum
33-
| CommandLineOptionOfBoolean
34-
| CommandLineOptionOfListType;
25+
type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfBoolean;
3526

3627
const optionDeclarations: CommandLineOption[] = [
3728
{
@@ -63,12 +54,6 @@ const optionDeclarations: CommandLineOption[] = [
6354
"Applies the source map to show source TS files and lines in error tracebacks.",
6455
type: "boolean",
6556
},
66-
{
67-
name: "tsTransformers",
68-
description: "Custom TypeScript transformers.",
69-
isTSConfigOnly: true,
70-
type: "list",
71-
},
7257
];
7358

7459
export const version = `Version ${require("../package.json").version}`;
@@ -89,8 +74,6 @@ export function getHelpString(): string {
8974

9075
result += "Options:\n";
9176
for (const option of optionDeclarations) {
92-
if (option.isTSConfigOnly) continue;
93-
9477
const aliasStrings = (option.aliases || []).map(a => "-" + a);
9578
const optionString = aliasStrings.concat(["--" + option.name]).join("|");
9679

@@ -184,14 +167,6 @@ interface CommandLineArgument extends ReadValueResult {
184167
}
185168

186169
function readCommandLineArgument(option: CommandLineOption, value: any): CommandLineArgument {
187-
if (option.isTSConfigOnly) {
188-
return {
189-
value: undefined,
190-
error: diagnosticFactories.optionCanOnlyBeSpecifiedInTsconfigJsonFile(option.name),
191-
increment: 0,
192-
};
193-
}
194-
195170
if (option.type === "boolean") {
196171
if (value === "true" || value === "false") {
197172
value = value === "true";
@@ -260,20 +235,6 @@ function readValue(option: CommandLineOption, value: unknown): ReadValueResult {
260235

261236
return { value: enumValue };
262237
}
263-
264-
case "list": {
265-
if (!Array.isArray(value)) {
266-
return {
267-
value: undefined,
268-
error: diagnosticFactories.compilerOptionRequiresAValueOfType(
269-
option.name,
270-
"Array"
271-
),
272-
};
273-
}
274-
275-
return { value };
276-
}
277238
}
278239
}
279240

src/CompilerOptions.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ type KnownKeys<T> = {
99
type OmitIndexSignature<T extends Record<any, any>> = Pick<T, KnownKeys<T>>;
1010

1111
export interface TransformerImport {
12-
name: string;
13-
when?: keyof ts.CustomTransformers;
12+
transform: string;
13+
import?: string;
14+
after?: boolean;
15+
afterDeclarations?: boolean;
16+
type?: 'program' | 'config' | 'checker' | 'raw' | 'compilerOptions';
1417
[option: string]: any;
1518
}
1619

@@ -20,8 +23,8 @@ export type CompilerOptions = OmitIndexSignature<ts.CompilerOptions> & {
2023
luaLibImport?: LuaLibImportKind;
2124
noHoisting?: boolean;
2225
sourceMapTraceback?: boolean;
23-
tsTransformers?: TransformerImport[];
24-
[option: string]: ts.CompilerOptions[string] | TransformerImport[];
26+
plugins?: Array<ts.PluginImport | TransformerImport>;
27+
[option: string]: ts.CompilerOptions[string] | Array<ts.PluginImport | TransformerImport>;
2528
};
2629

2730
export enum LuaLibImportKind {

src/Transpile.ts

Lines changed: 140 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,138 @@
11
import * as path from "path";
22
import * as resolve from "resolve";
33
import * as ts from "typescript";
4-
import { CompilerOptions } from "./CompilerOptions";
4+
import { CompilerOptions, TransformerImport } from "./CompilerOptions";
55
import * as diagnosticFactories from "./diagnostics";
66
import { Block } from "./LuaAST";
77
import { LuaPrinter } from "./LuaPrinter";
88
import { LuaTransformer } from "./LuaTransformer";
99
import { TranspileError } from "./TranspileError";
1010

11+
type ProgramTransformerFactory = (program: ts.Program, options: Record<string, any>) => Transformer;
12+
type ConfigTransformerFactory = (options: Record<string, any>) => Transformer;
13+
type CompilerOptionsTransformerFactory =
14+
(compilerOptions: CompilerOptions, options: Record<string, any>) => Transformer;
15+
type TypeCheckerTransformerFactory = (typeChecker: ts.TypeChecker, options: Record<string, any>) => Transformer;
16+
type RawTransformerFactory = Transformer;
17+
type TransformerFactory =
18+
| ProgramTransformerFactory
19+
| ConfigTransformerFactory
20+
| CompilerOptionsTransformerFactory
21+
| TypeCheckerTransformerFactory
22+
| RawTransformerFactory;
23+
24+
type Transformer = GroupTransformer | ts.TransformerFactory<ts.SourceFile>;
25+
interface GroupTransformer {
26+
before?: ts.TransformerFactory<ts.SourceFile>;
27+
after?: ts.TransformerFactory<ts.SourceFile>;
28+
afterDeclarations?: ts.TransformerFactory<ts.SourceFile | ts.Bundle>;
29+
}
30+
31+
function resolveTransformerFactory(
32+
basedir: string,
33+
transformerOptionPath: string,
34+
{ transform, import: importName = 'default' }: TransformerImport
35+
): { error?: ts.Diagnostic; factory?: TransformerFactory } {
36+
if (typeof transform !== "string") {
37+
const optionName = `${transformerOptionPath}.transform`;
38+
return { error: diagnosticFactories.compilerOptionRequiresAValueOfType(optionName, "string") };
39+
}
40+
41+
let resolved: string;
42+
try {
43+
resolved = resolve.sync(transform, { basedir, extensions: [".js", ".ts", ".tsx"] });
44+
} catch (err) {
45+
if (err.code !== "MODULE_NOT_FOUND") throw err;
46+
return { error: diagnosticFactories.couldNotResolveTransformerFrom(transform, basedir) };
47+
}
48+
49+
// tslint:disable-next-line: deprecation
50+
const hasNoRequireHook = require.extensions[".ts"] === undefined;
51+
if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) {
52+
try {
53+
const tsNode: typeof import("ts-node") = require("ts-node");
54+
tsNode.register({ transpileOnly: true });
55+
} catch (err) {
56+
if (err.code !== "MODULE_NOT_FOUND") throw err;
57+
return { error: diagnosticFactories.toLoadTransformerItShouldBeTranspiled(transform) };
58+
}
59+
}
60+
61+
const factory: TransformerFactory = require(resolved)[importName];
62+
if (factory === undefined) {
63+
return { error: diagnosticFactories.transformerShouldHaveAExport(transform, importName) };
64+
}
65+
66+
return { factory };
67+
}
68+
69+
function loadTransformer(
70+
transformerOptionPath: string,
71+
program: ts.Program,
72+
factory: TransformerFactory,
73+
{ transform, after = false, afterDeclarations = false, type = "program", ...extraOptions }: TransformerImport
74+
): { error?: ts.Diagnostic; transformer?: GroupTransformer } {
75+
let transformer: Transformer;
76+
switch (type) {
77+
case 'program':
78+
transformer = (factory as ProgramTransformerFactory)(program, extraOptions);
79+
break;
80+
case 'config':
81+
transformer = (factory as ConfigTransformerFactory)(extraOptions);
82+
break;
83+
case 'checker':
84+
transformer = (factory as TypeCheckerTransformerFactory)(program.getTypeChecker(), extraOptions);
85+
break;
86+
case 'raw':
87+
transformer = factory as RawTransformerFactory;
88+
break;
89+
case 'compilerOptions':
90+
transformer = (factory as CompilerOptionsTransformerFactory)(program.getCompilerOptions(), extraOptions);
91+
break;
92+
default: {
93+
const optionName = `--${transformerOptionPath}.type`;
94+
return { error: diagnosticFactories.argumentForOptionMustBe(optionName, 'program') };
95+
}
96+
}
97+
98+
if (typeof after !== "boolean") {
99+
const optionName = `${transformerOptionPath}.after`;
100+
return { error: diagnosticFactories.compilerOptionRequiresAValueOfType(optionName, "boolean") };
101+
}
102+
103+
if (typeof afterDeclarations !== "boolean") {
104+
const optionName = `${transformerOptionPath}.afterDeclarations`;
105+
return { error: diagnosticFactories.compilerOptionRequiresAValueOfType(optionName, "boolean") };
106+
}
107+
108+
if (typeof transformer === "function") {
109+
let wrappedTransformer: GroupTransformer;
110+
111+
if (after) {
112+
wrappedTransformer = { after: transformer };
113+
} else if (afterDeclarations) {
114+
wrappedTransformer = { afterDeclarations: transformer as ts.TransformerFactory<ts.SourceFile | ts.Bundle> };
115+
} else {
116+
wrappedTransformer = { before: transformer };
117+
}
118+
119+
return { transformer: wrappedTransformer };
120+
} else {
121+
const isValidGroupTransformer =
122+
typeof transformer === "object" &&
123+
(transformer.before || transformer.after || transformer.afterDeclarations);
124+
125+
if (!isValidGroupTransformer) {
126+
return { error: diagnosticFactories.transformerShouldBeATsTransformerFactory(transform) };
127+
}
128+
}
129+
130+
return { transformer };
131+
}
132+
11133
function loadTransformersFromOptions(
12134
program: ts.Program,
13-
diagnostics: ts.Diagnostic[]
135+
allDiagnostics: ts.Diagnostic[]
14136
): ts.CustomTransformers {
15137
const customTransformers: Required<ts.CustomTransformers> = {
16138
before: [],
@@ -19,64 +141,33 @@ function loadTransformersFromOptions(
19141
};
20142

21143
const options = program.getCompilerOptions() as CompilerOptions;
22-
if (!options.tsTransformers) return customTransformers;
144+
if (!options.plugins) return customTransformers;
23145

24146
const configFileName = options.configFilePath as string | undefined;
25147
const basedir = configFileName ? path.dirname(configFileName) : process.cwd();
26148

27-
const extensions = [".js", ".ts", ".tsx"];
28-
for (const [index, transformer] of options.tsTransformers.entries()) {
29-
const transformerOptionPath = `tsTransformers[${index}]`;
30-
const { name, when = "before", ...transformerOptions } = transformer;
31-
32-
if (typeof name !== "string") {
33-
const optionName = `${transformerOptionPath}.name`;
34-
diagnostics.push(
35-
diagnosticFactories.compilerOptionRequiresAValueOfType(optionName, "string")
36-
);
149+
for (const [index, transformerImport] of options.plugins.entries()) {
150+
if ('name' in transformerImport) continue;
151+
const optionName = `compilerOptions.plugins[${index}]`;
37152

38-
continue;
39-
}
153+
const { error: resolveError, factory } = resolveTransformerFactory(basedir, optionName, transformerImport);
154+
if (resolveError) allDiagnostics.push(resolveError);
155+
if (factory === undefined) continue;
40156

41-
const whenValues = ["before", "after", "afterDeclarations"];
42-
if (!whenValues.includes(when)) {
43-
const optionName = `--${transformerOptionPath}.when`;
44-
diagnostics.push(
45-
diagnosticFactories.argumentForOptionMustBe(optionName, whenValues.join(", "))
46-
);
157+
const { error, transformer } = loadTransformer(optionName, program, factory, transformerImport);
158+
if (error) allDiagnostics.push(error);
159+
if (transformer === undefined) continue;
47160

48-
continue;
161+
if (transformer.before) {
162+
customTransformers.before.push(transformer.before);
49163
}
50164

51-
let resolved: string;
52-
try {
53-
resolved = resolve.sync(name, { extensions, basedir });
54-
} catch (err) {
55-
if (err.code !== "MODULE_NOT_FOUND") throw err;
56-
diagnostics.push(diagnosticFactories.couldNotResolveTransformerFrom(name, basedir));
57-
58-
continue;
165+
if (transformer.after) {
166+
customTransformers.after.push(transformer.after);
59167
}
60168

61-
// tslint:disable-next-line: deprecation
62-
const hasNoRequireHook = require.extensions[".ts"] === undefined;
63-
if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) {
64-
try {
65-
const tsNode: typeof import("ts-node") = require("ts-node");
66-
tsNode.register({ transpileOnly: true });
67-
} catch (err) {
68-
if (err.code !== "MODULE_NOT_FOUND") throw err;
69-
diagnostics.push(diagnosticFactories.toLoadTransformerItShouldBeTranspiled(name));
70-
71-
continue;
72-
}
73-
}
74-
75-
const result = require(resolved).default;
76-
if (result !== undefined) {
77-
customTransformers[when].push(result(program, transformerOptions));
78-
} else {
79-
diagnostics.push(diagnosticFactories.transformerShouldHaveADefaultExport(name));
169+
if (transformer.afterDeclarations) {
170+
customTransformers.afterDeclarations.push(transformer.afterDeclarations);
80171
}
81172
}
82173

src/diagnostics.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,25 @@ export const couldNotResolveTransformerFrom = (transform: string, base: string):
4343
messageText: `Could not resolve "${transform}" transformer from "${base}".`,
4444
});
4545

46-
export const transformerShouldHaveADefaultExport = (transform: string): ts.Diagnostic => ({
46+
export const transformerShouldHaveAExport = (transform: string, importName: string): ts.Diagnostic => ({
4747
file: undefined,
4848
start: undefined,
4949
length: undefined,
5050
category: ts.DiagnosticCategory.Error,
5151
code: 0,
5252
source: "typescript-to-lua",
53-
messageText: `"${transform}" transformer should have a default export`,
53+
messageText: `"${transform}" transformer should have a "${importName}" export`,
54+
});
55+
56+
export const transformerShouldBeATsTransformerFactory = (transform: string): ts.Diagnostic => ({
57+
file: undefined,
58+
start: undefined,
59+
length: undefined,
60+
category: ts.DiagnosticCategory.Error,
61+
code: 0,
62+
source: "typescript-to-lua",
63+
messageText:
64+
`"${transform}" transformer should be a ts.TransformerFactory or an object with ts.TransformerFactory values`,
5465
});
5566

5667
export const watchErrorSummary = (errorCount: number): ts.Diagnostic => ({

0 commit comments

Comments
 (0)