Skip to content

Commit 9bef35d

Browse files
authored
Load plugins from tsconfig.json (#773)
* Load plugins from `tsconfig.json` * Refactor `transformers.spec.ts` test * Add few basic tests * Resolve `ts-node` on behalf of the root application * Merge plugin and transformer diagnostics * Refactor tests * Fix CJS factory loading * Add description to `luaPlugins` option * Add changelog entry * Rename `luaPlugins.plugin` to `luaPlugins.name`
1 parent 9783568 commit 9bef35d

25 files changed

+276
-210
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@
8181
end
8282
```
8383
84+
- Added `tstl.luaPlugins` option, allowing to specify plugins in a `tsconfig.json` file:
85+
86+
```json
87+
{
88+
"tstl": {
89+
"luaPlugins": [{ "name": "./plugin.ts" }]
90+
}
91+
}
92+
```
93+
8494
## 0.31.0
8595
8696
- **Breaking:** The old annotation syntax (`/* !varArg */`) **no longer works**, the only currently supported syntax is:

src/CompilerOptions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export interface TransformerImport {
1818
[option: string]: any;
1919
}
2020

21+
export interface LuaPluginImport {
22+
name: string;
23+
import?: string;
24+
}
25+
2126
export type CompilerOptions = OmitIndexSignature<ts.CompilerOptions> & {
2227
noImplicitSelf?: boolean;
2328
noHeader?: boolean;
@@ -26,6 +31,7 @@ export type CompilerOptions = OmitIndexSignature<ts.CompilerOptions> & {
2631
luaTarget?: LuaTarget;
2732
luaLibImport?: LuaLibImportKind;
2833
sourceMapTraceback?: boolean;
34+
luaPlugins?: LuaPluginImport[];
2935
plugins?: Array<ts.PluginImport | TransformerImport>;
3036
[option: string]: any;
3137
};

src/cli/parse.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,11 @@ interface CommandLineOptionOfEnum extends CommandLineOptionBase {
1717
choices: string[];
1818
}
1919

20-
interface CommandLineOptionOfBoolean extends CommandLineOptionBase {
21-
type: "boolean";
20+
interface CommandLineOptionOfPrimitive extends CommandLineOptionBase {
21+
type: "boolean" | "string" | "object";
2222
}
2323

24-
interface CommandLineOptionOfString extends CommandLineOptionBase {
25-
type: "string";
26-
}
27-
28-
type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfBoolean | CommandLineOptionOfString;
24+
type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfPrimitive;
2925

3026
export const optionDeclarations: CommandLineOption[] = [
3127
{
@@ -66,6 +62,11 @@ export const optionDeclarations: CommandLineOption[] = [
6662
description: "Applies the source map to show source TS files and lines in error tracebacks.",
6763
type: "boolean",
6864
},
65+
{
66+
name: "luaPlugins",
67+
description: "List of TypeScriptToLua plugins.",
68+
type: "object",
69+
},
6970
];
7071

7172
export function updateParsedConfigFile(parsedConfigFile: ts.ParsedCommandLine): ParsedCommandLine {
@@ -173,8 +174,9 @@ function readValue(option: CommandLineOption, value: unknown): ReadValueResult {
173174
if (value === null) return { value };
174175

175176
switch (option.type) {
177+
case "boolean":
176178
case "string":
177-
case "boolean": {
179+
case "object": {
178180
if (typeof value !== option.type) {
179181
return {
180182
value: undefined,

src/transpilation/diagnostics.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import { createSerialDiagnosticFactory } from "../utils";
44
const createDiagnosticFactory = <TArgs extends any[]>(getMessage: (...args: TArgs) => string) =>
55
createSerialDiagnosticFactory((...args: TArgs) => ({ messageText: getMessage(...args) }));
66

7-
export const toLoadTransformerItShouldBeTranspiled = createDiagnosticFactory(
8-
(transform: string) =>
9-
`To load "${transform}" transformer it should be transpiled or "ts-node" should be installed.`
7+
export const toLoadItShouldBeTranspiled = createDiagnosticFactory(
8+
(kind: string, transform: string) =>
9+
`To load "${transform}" ${kind} it should be transpiled or "ts-node" should be installed.`
1010
);
1111

12-
export const couldNotResolveTransformerFrom = createDiagnosticFactory(
13-
(transform: string, base: string) => `Could not resolve "${transform}" transformer from "${base}".`
12+
export const couldNotResolveFrom = createDiagnosticFactory(
13+
(kind: string, transform: string, base: string) => `Could not resolve "${transform}" ${kind} from "${base}".`
1414
);
1515

16-
export const transformerShouldHaveAExport = createDiagnosticFactory(
17-
(transform: string, importName: string) => `"${transform}" transformer should have a "${importName}" export.`
16+
export const shouldHaveAExport = createDiagnosticFactory(
17+
(kind: string, transform: string, importName: string) =>
18+
`"${transform}" ${kind} should have a "${importName}" export.`
1819
);
1920

2021
export const transformerShouldBeATsTransformerFactory = createDiagnosticFactory(

src/transpilation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { emitTranspiledFiles, OutputFile } from "./emit";
77
import { transpile, TranspiledFile, TranspileResult } from "./transpile";
88

99
export * from "./emit";
10+
export { Plugin } from "./plugins";
1011
export * from "./transpile";
1112

1213
export interface TranspileFilesResult {

src/transpilation/plugins.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as ts from "typescript";
2+
import { CompilerOptions } from "../CompilerOptions";
23
import { Printer } from "../LuaPrinter";
34
import { Visitors } from "../transformation/context";
5+
import { getConfigDirectory, resolvePlugin } from "./utils";
46

57
export interface Plugin {
68
/**
@@ -18,10 +20,27 @@ export interface Plugin {
1820
printer?: Printer;
1921
}
2022

21-
export function getPlugins(
22-
_program: ts.Program,
23-
_diagnostics: ts.Diagnostic[],
24-
pluginsFromOptions: Plugin[]
25-
): Plugin[] {
26-
return pluginsFromOptions;
23+
export function getPlugins(program: ts.Program, diagnostics: ts.Diagnostic[], customPlugins: Plugin[]): Plugin[] {
24+
const pluginsFromOptions: Plugin[] = [];
25+
const options = program.getCompilerOptions() as CompilerOptions;
26+
27+
for (const [index, pluginOption] of (options.luaPlugins ?? []).entries()) {
28+
const optionName = `tstl.luaPlugins[${index}]`;
29+
30+
const { error: resolveError, result: factory } = resolvePlugin(
31+
"plugin",
32+
`${optionName}.name`,
33+
getConfigDirectory(options),
34+
pluginOption.name,
35+
pluginOption.import
36+
);
37+
38+
if (resolveError) diagnostics.push(resolveError);
39+
if (factory === undefined) continue;
40+
41+
const plugin = typeof factory === "function" ? factory(pluginOption) : factory;
42+
pluginsFromOptions.push(plugin);
43+
}
44+
45+
return [...customPlugins, ...pluginsFromOptions];
2746
}

src/transpilation/transformers.ts

Lines changed: 18 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import * as path from "path";
2-
import * as resolve from "resolve";
31
import * as ts from "typescript";
42
// TODO: Don't depend on CLI?
53
import * as cliDiagnostics from "../cli/diagnostics";
64
import { CompilerOptions, TransformerImport } from "../CompilerOptions";
75
import * as diagnosticFactories from "./diagnostics";
6+
import { getConfigDirectory, resolvePlugin } from "./utils";
87

98
export const noImplicitSelfTransformer: ts.TransformerFactory<ts.SourceFile | ts.Bundle> = () => node => {
109
const transformSourceFile: ts.Transformer<ts.SourceFile> = node => {
@@ -18,7 +17,7 @@ export const noImplicitSelfTransformer: ts.TransformerFactory<ts.SourceFile | ts
1817
: transformSourceFile(node);
1918
};
2019

21-
export function getCustomTransformers(
20+
export function getTransformers(
2221
program: ts.Program,
2322
diagnostics: ts.Diagnostic[],
2423
customTransformers: ts.CustomTransformers,
@@ -54,7 +53,7 @@ export function getCustomTransformers(
5453
};
5554
}
5655

57-
function loadTransformersFromOptions(program: ts.Program, allDiagnostics: ts.Diagnostic[]): ts.CustomTransformers {
56+
function loadTransformersFromOptions(program: ts.Program, diagnostics: ts.Diagnostic[]): ts.CustomTransformers {
5857
const customTransformers: Required<ts.CustomTransformers> = {
5958
before: [],
6059
after: [],
@@ -64,19 +63,23 @@ function loadTransformersFromOptions(program: ts.Program, allDiagnostics: ts.Dia
6463
const options = program.getCompilerOptions() as CompilerOptions;
6564
if (!options.plugins) return customTransformers;
6665

67-
const configFileName = options.configFilePath as string | undefined;
68-
const basedir = configFileName ? path.dirname(configFileName) : process.cwd();
69-
7066
for (const [index, transformerImport] of options.plugins.entries()) {
7167
if (!("transform" in transformerImport)) continue;
7268
const optionName = `compilerOptions.plugins[${index}]`;
7369

74-
const { error: resolveError, factory } = resolveTransformerFactory(basedir, optionName, transformerImport);
75-
if (resolveError) allDiagnostics.push(resolveError);
70+
const { error: resolveError, result: factory } = resolvePlugin(
71+
"transformer",
72+
`${optionName}.transform`,
73+
getConfigDirectory(options),
74+
transformerImport.transform,
75+
transformerImport.import
76+
);
77+
78+
if (resolveError) diagnostics.push(resolveError);
7679
if (factory === undefined) continue;
7780

7881
const { error: loadError, transformer } = loadTransformer(optionName, program, factory, transformerImport);
79-
if (loadError) allDiagnostics.push(loadError);
82+
if (loadError) diagnostics.push(loadError);
8083
if (transformer === undefined) continue;
8184

8285
if (transformer.before) {
@@ -103,12 +106,6 @@ type CompilerOptionsTransformerFactory = (
103106
) => Transformer;
104107
type TypeCheckerTransformerFactory = (typeChecker: ts.TypeChecker, options: Record<string, any>) => Transformer;
105108
type RawTransformerFactory = Transformer;
106-
type TransformerFactory =
107-
| ProgramTransformerFactory
108-
| ConfigTransformerFactory
109-
| CompilerOptionsTransformerFactory
110-
| TypeCheckerTransformerFactory
111-
| RawTransformerFactory;
112109

113110
type Transformer = GroupTransformer | ts.TransformerFactory<ts.SourceFile>;
114111
interface GroupTransformer {
@@ -117,48 +114,10 @@ interface GroupTransformer {
117114
afterDeclarations?: ts.TransformerFactory<ts.SourceFile | ts.Bundle>;
118115
}
119116

120-
function resolveTransformerFactory(
121-
basedir: string,
122-
transformerOptionPath: string,
123-
{ transform, import: importName = "default" }: TransformerImport
124-
): { error?: ts.Diagnostic; factory?: TransformerFactory } {
125-
if (typeof transform !== "string") {
126-
const optionName = `${transformerOptionPath}.transform`;
127-
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "string") };
128-
}
129-
130-
let resolved: string;
131-
try {
132-
resolved = resolve.sync(transform, { basedir, extensions: [".js", ".ts", ".tsx"] });
133-
} catch (err) {
134-
if (err.code !== "MODULE_NOT_FOUND") throw err;
135-
return { error: diagnosticFactories.couldNotResolveTransformerFrom(transform, basedir) };
136-
}
137-
138-
// tslint:disable-next-line: deprecation
139-
const hasNoRequireHook = require.extensions[".ts"] === undefined;
140-
if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) {
141-
try {
142-
const tsNode: typeof import("ts-node") = require("ts-node");
143-
tsNode.register({ transpileOnly: true });
144-
} catch (err) {
145-
if (err.code !== "MODULE_NOT_FOUND") throw err;
146-
return { error: diagnosticFactories.toLoadTransformerItShouldBeTranspiled(transform) };
147-
}
148-
}
149-
150-
const factory: TransformerFactory = require(resolved)[importName];
151-
if (factory === undefined) {
152-
return { error: diagnosticFactories.transformerShouldHaveAExport(transform, importName) };
153-
}
154-
155-
return { factory };
156-
}
157-
158117
function loadTransformer(
159-
transformerOptionPath: string,
118+
optionPath: string,
160119
program: ts.Program,
161-
factory: TransformerFactory,
120+
factory: unknown,
162121
{ transform, after = false, afterDeclarations = false, type = "program", ...extraOptions }: TransformerImport
163122
): { error?: ts.Diagnostic; transformer?: GroupTransformer } {
164123
let transformer: Transformer;
@@ -179,18 +138,18 @@ function loadTransformer(
179138
transformer = (factory as CompilerOptionsTransformerFactory)(program.getCompilerOptions(), extraOptions);
180139
break;
181140
default: {
182-
const optionName = `--${transformerOptionPath}.type`;
141+
const optionName = `--${optionPath}.type`;
183142
return { error: cliDiagnostics.argumentForOptionMustBe(optionName, "program") };
184143
}
185144
}
186145

187146
if (typeof after !== "boolean") {
188-
const optionName = `${transformerOptionPath}.after`;
147+
const optionName = `${optionPath}.after`;
189148
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") };
190149
}
191150

192151
if (typeof afterDeclarations !== "boolean") {
193-
const optionName = `${transformerOptionPath}.afterDeclarations`;
152+
const optionName = `${optionPath}.afterDeclarations`;
194153
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") };
195154
}
196155

src/transpilation/transpile.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createVisitorMap, transformSourceFile } from "../transformation";
77
import { isNonNull } from "../utils";
88
import { bundleTranspiledFiles } from "./bundle";
99
import { getPlugins, Plugin } from "./plugins";
10-
import { getCustomTransformers } from "./transformers";
10+
import { getTransformers } from "./transformers";
1111

1212
export interface TranspiledFile {
1313
fileName: string;
@@ -42,7 +42,7 @@ export function transpile({
4242
program,
4343
sourceFiles: targetSourceFiles,
4444
customTransformers = {},
45-
plugins: pluginsFromOptions = [],
45+
plugins: customPlugins = [],
4646
emitHost = ts.sys,
4747
}: TranspileOptions): TranspileResult {
4848
const options = program.getCompilerOptions() as CompilerOptions;
@@ -85,7 +85,7 @@ export function transpile({
8585
}
8686
}
8787

88-
const plugins = getPlugins(program, diagnostics, pluginsFromOptions);
88+
const plugins = getPlugins(program, diagnostics, customPlugins);
8989
const visitorMap = createVisitorMap(plugins.map(p => p.visitors).filter(isNonNull));
9090
const printer = createPrinter(plugins.map(p => p.printer).filter(isNonNull));
9191
const processSourceFile = (sourceFile: ts.SourceFile) => {
@@ -107,7 +107,7 @@ export function transpile({
107107
}
108108
};
109109

110-
const transformers = getCustomTransformers(program, diagnostics, customTransformers, processSourceFile);
110+
const transformers = getTransformers(program, diagnostics, customTransformers, processSourceFile);
111111

112112
const writeFile: ts.WriteFileCallback = (fileName, data, _bom, _onError, sourceFiles = []) => {
113113
for (const sourceFile of sourceFiles) {

src/transpilation/utils.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as path from "path";
2+
import * as resolve from "resolve";
3+
import * as ts from "typescript";
4+
// TODO: Don't depend on CLI?
5+
import * as cliDiagnostics from "../cli/diagnostics";
6+
import * as diagnosticFactories from "./diagnostics";
7+
8+
export const getConfigDirectory = (options: ts.CompilerOptions) =>
9+
options.configFilePath ? path.dirname(options.configFilePath) : process.cwd();
10+
11+
export function resolvePlugin(
12+
kind: string,
13+
optionName: string,
14+
basedir: string,
15+
query: string,
16+
importName = "default"
17+
): { error?: ts.Diagnostic; result?: unknown } {
18+
if (typeof query !== "string") {
19+
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "string") };
20+
}
21+
22+
let resolved: string;
23+
try {
24+
resolved = resolve.sync(query, { basedir, extensions: [".js", ".ts", ".tsx"] });
25+
} catch (err) {
26+
if (err.code !== "MODULE_NOT_FOUND") throw err;
27+
return { error: diagnosticFactories.couldNotResolveFrom(kind, query, basedir) };
28+
}
29+
30+
// tslint:disable-next-line: deprecation
31+
const hasNoRequireHook = require.extensions[".ts"] === undefined;
32+
if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) {
33+
try {
34+
const tsNodePath = resolve.sync("ts-node", { basedir });
35+
const tsNode: typeof import("ts-node") = require(tsNodePath);
36+
tsNode.register({ transpileOnly: true });
37+
} catch (err) {
38+
if (err.code !== "MODULE_NOT_FOUND") throw err;
39+
return { error: diagnosticFactories.toLoadItShouldBeTranspiled(kind, query) };
40+
}
41+
}
42+
43+
const commonjsModule = require(resolved);
44+
const factoryModule = commonjsModule.__esModule ? commonjsModule : { default: commonjsModule };
45+
const result = factoryModule[importName];
46+
if (result === undefined) {
47+
return { error: diagnosticFactories.shouldHaveAExport(kind, query, importName) };
48+
}
49+
50+
return { result };
51+
}

0 commit comments

Comments
 (0)