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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@
end
```

- Added `tstl.luaPlugins` option, allowing to specify plugins in a `tsconfig.json` file:

```json
{
"tstl": {
"luaPlugins": [{ "name": "./plugin.ts" }]
}
}
```

## 0.31.0

- **Breaking:** The old annotation syntax (`/* !varArg */`) **no longer works**, the only currently supported syntax is:
Expand Down
6 changes: 6 additions & 0 deletions src/CompilerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export interface TransformerImport {
[option: string]: any;
}

export interface LuaPluginImport {
name: string;
import?: string;
}

export type CompilerOptions = OmitIndexSignature<ts.CompilerOptions> & {
noImplicitSelf?: boolean;
noHeader?: boolean;
Expand All @@ -26,6 +31,7 @@ export type CompilerOptions = OmitIndexSignature<ts.CompilerOptions> & {
luaTarget?: LuaTarget;
luaLibImport?: LuaLibImportKind;
sourceMapTraceback?: boolean;
luaPlugins?: LuaPluginImport[];
plugins?: Array<ts.PluginImport | TransformerImport>;
[option: string]: any;
};
Expand Down
18 changes: 10 additions & 8 deletions src/cli/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,11 @@ interface CommandLineOptionOfEnum extends CommandLineOptionBase {
choices: string[];
}

interface CommandLineOptionOfBoolean extends CommandLineOptionBase {
type: "boolean";
interface CommandLineOptionOfPrimitive extends CommandLineOptionBase {
type: "boolean" | "string" | "object";
}

interface CommandLineOptionOfString extends CommandLineOptionBase {
type: "string";
}

type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfBoolean | CommandLineOptionOfString;
type CommandLineOption = CommandLineOptionOfEnum | CommandLineOptionOfPrimitive;

export const optionDeclarations: CommandLineOption[] = [
{
Expand Down Expand Up @@ -66,6 +62,11 @@ export const optionDeclarations: CommandLineOption[] = [
description: "Applies the source map to show source TS files and lines in error tracebacks.",
type: "boolean",
},
{
name: "luaPlugins",
description: "List of TypeScriptToLua plugins.",
type: "object",
},
];

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

switch (option.type) {
case "boolean":
case "string":
case "boolean": {
case "object": {
if (typeof value !== option.type) {
return {
value: undefined,
Expand Down
15 changes: 8 additions & 7 deletions src/transpilation/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import { createSerialDiagnosticFactory } from "../utils";
const createDiagnosticFactory = <TArgs extends any[]>(getMessage: (...args: TArgs) => string) =>
createSerialDiagnosticFactory((...args: TArgs) => ({ messageText: getMessage(...args) }));

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

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

export const transformerShouldHaveAExport = createDiagnosticFactory(
(transform: string, importName: string) => `"${transform}" transformer should have a "${importName}" export.`
export const shouldHaveAExport = createDiagnosticFactory(
(kind: string, transform: string, importName: string) =>
`"${transform}" ${kind} should have a "${importName}" export.`
);

export const transformerShouldBeATsTransformerFactory = createDiagnosticFactory(
Expand Down
1 change: 1 addition & 0 deletions src/transpilation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { emitTranspiledFiles, OutputFile } from "./emit";
import { transpile, TranspiledFile, TranspileResult } from "./transpile";

export * from "./emit";
export { Plugin } from "./plugins";
export * from "./transpile";

export interface TranspileFilesResult {
Expand Down
31 changes: 25 additions & 6 deletions src/transpilation/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as ts from "typescript";
import { CompilerOptions } from "../CompilerOptions";
import { Printer } from "../LuaPrinter";
import { Visitors } from "../transformation/context";
import { getConfigDirectory, resolvePlugin } from "./utils";

export interface Plugin {
/**
Expand All @@ -18,10 +20,27 @@ export interface Plugin {
printer?: Printer;
}

export function getPlugins(
_program: ts.Program,
_diagnostics: ts.Diagnostic[],
pluginsFromOptions: Plugin[]
): Plugin[] {
return pluginsFromOptions;
export function getPlugins(program: ts.Program, diagnostics: ts.Diagnostic[], customPlugins: Plugin[]): Plugin[] {
const pluginsFromOptions: Plugin[] = [];
const options = program.getCompilerOptions() as CompilerOptions;

for (const [index, pluginOption] of (options.luaPlugins ?? []).entries()) {
const optionName = `tstl.luaPlugins[${index}]`;

const { error: resolveError, result: factory } = resolvePlugin(
"plugin",
`${optionName}.name`,
getConfigDirectory(options),
pluginOption.name,
pluginOption.import
);

if (resolveError) diagnostics.push(resolveError);
if (factory === undefined) continue;

const plugin = typeof factory === "function" ? factory(pluginOption) : factory;
pluginsFromOptions.push(plugin);
}

return [...customPlugins, ...pluginsFromOptions];
}
77 changes: 18 additions & 59 deletions src/transpilation/transformers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as path from "path";
import * as resolve from "resolve";
import * as ts from "typescript";
// TODO: Don't depend on CLI?
import * as cliDiagnostics from "../cli/diagnostics";
import { CompilerOptions, TransformerImport } from "../CompilerOptions";
import * as diagnosticFactories from "./diagnostics";
import { getConfigDirectory, resolvePlugin } from "./utils";

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

export function getCustomTransformers(
export function getTransformers(
program: ts.Program,
diagnostics: ts.Diagnostic[],
customTransformers: ts.CustomTransformers,
Expand Down Expand Up @@ -54,7 +53,7 @@ export function getCustomTransformers(
};
}

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

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

for (const [index, transformerImport] of options.plugins.entries()) {
if (!("transform" in transformerImport)) continue;
const optionName = `compilerOptions.plugins[${index}]`;

const { error: resolveError, factory } = resolveTransformerFactory(basedir, optionName, transformerImport);
if (resolveError) allDiagnostics.push(resolveError);
const { error: resolveError, result: factory } = resolvePlugin(
"transformer",
`${optionName}.transform`,
getConfigDirectory(options),
transformerImport.transform,
transformerImport.import
);

if (resolveError) diagnostics.push(resolveError);
if (factory === undefined) continue;

const { error: loadError, transformer } = loadTransformer(optionName, program, factory, transformerImport);
if (loadError) allDiagnostics.push(loadError);
if (loadError) diagnostics.push(loadError);
if (transformer === undefined) continue;

if (transformer.before) {
Expand All @@ -103,12 +106,6 @@ type CompilerOptionsTransformerFactory = (
) => Transformer;
type TypeCheckerTransformerFactory = (typeChecker: ts.TypeChecker, options: Record<string, any>) => Transformer;
type RawTransformerFactory = Transformer;
type TransformerFactory =
| ProgramTransformerFactory
| ConfigTransformerFactory
| CompilerOptionsTransformerFactory
| TypeCheckerTransformerFactory
| RawTransformerFactory;

type Transformer = GroupTransformer | ts.TransformerFactory<ts.SourceFile>;
interface GroupTransformer {
Expand All @@ -117,48 +114,10 @@ interface GroupTransformer {
afterDeclarations?: ts.TransformerFactory<ts.SourceFile | ts.Bundle>;
}

function resolveTransformerFactory(
basedir: string,
transformerOptionPath: string,
{ transform, import: importName = "default" }: TransformerImport
): { error?: ts.Diagnostic; factory?: TransformerFactory } {
if (typeof transform !== "string") {
const optionName = `${transformerOptionPath}.transform`;
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "string") };
}

let resolved: string;
try {
resolved = resolve.sync(transform, { basedir, extensions: [".js", ".ts", ".tsx"] });
} catch (err) {
if (err.code !== "MODULE_NOT_FOUND") throw err;
return { error: diagnosticFactories.couldNotResolveTransformerFrom(transform, basedir) };
}

// tslint:disable-next-line: deprecation
const hasNoRequireHook = require.extensions[".ts"] === undefined;
if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) {
try {
const tsNode: typeof import("ts-node") = require("ts-node");
tsNode.register({ transpileOnly: true });
} catch (err) {
if (err.code !== "MODULE_NOT_FOUND") throw err;
return { error: diagnosticFactories.toLoadTransformerItShouldBeTranspiled(transform) };
}
}

const factory: TransformerFactory = require(resolved)[importName];
if (factory === undefined) {
return { error: diagnosticFactories.transformerShouldHaveAExport(transform, importName) };
}

return { factory };
}

function loadTransformer(
transformerOptionPath: string,
optionPath: string,
program: ts.Program,
factory: TransformerFactory,
factory: unknown,
{ transform, after = false, afterDeclarations = false, type = "program", ...extraOptions }: TransformerImport
): { error?: ts.Diagnostic; transformer?: GroupTransformer } {
let transformer: Transformer;
Expand All @@ -179,18 +138,18 @@ function loadTransformer(
transformer = (factory as CompilerOptionsTransformerFactory)(program.getCompilerOptions(), extraOptions);
break;
default: {
const optionName = `--${transformerOptionPath}.type`;
const optionName = `--${optionPath}.type`;
return { error: cliDiagnostics.argumentForOptionMustBe(optionName, "program") };
}
}

if (typeof after !== "boolean") {
const optionName = `${transformerOptionPath}.after`;
const optionName = `${optionPath}.after`;
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") };
}

if (typeof afterDeclarations !== "boolean") {
const optionName = `${transformerOptionPath}.afterDeclarations`;
const optionName = `${optionPath}.afterDeclarations`;
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "boolean") };
}

Expand Down
8 changes: 4 additions & 4 deletions src/transpilation/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createVisitorMap, transformSourceFile } from "../transformation";
import { isNonNull } from "../utils";
import { bundleTranspiledFiles } from "./bundle";
import { getPlugins, Plugin } from "./plugins";
import { getCustomTransformers } from "./transformers";
import { getTransformers } from "./transformers";

export interface TranspiledFile {
fileName: string;
Expand Down Expand Up @@ -42,7 +42,7 @@ export function transpile({
program,
sourceFiles: targetSourceFiles,
customTransformers = {},
plugins: pluginsFromOptions = [],
plugins: customPlugins = [],
emitHost = ts.sys,
}: TranspileOptions): TranspileResult {
const options = program.getCompilerOptions() as CompilerOptions;
Expand Down Expand Up @@ -85,7 +85,7 @@ export function transpile({
}
}

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

const transformers = getCustomTransformers(program, diagnostics, customTransformers, processSourceFile);
const transformers = getTransformers(program, diagnostics, customTransformers, processSourceFile);

const writeFile: ts.WriteFileCallback = (fileName, data, _bom, _onError, sourceFiles = []) => {
for (const sourceFile of sourceFiles) {
Expand Down
51 changes: 51 additions & 0 deletions src/transpilation/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as path from "path";
import * as resolve from "resolve";
import * as ts from "typescript";
// TODO: Don't depend on CLI?
import * as cliDiagnostics from "../cli/diagnostics";
import * as diagnosticFactories from "./diagnostics";

export const getConfigDirectory = (options: ts.CompilerOptions) =>
options.configFilePath ? path.dirname(options.configFilePath) : process.cwd();

export function resolvePlugin(
kind: string,
optionName: string,
basedir: string,
query: string,
importName = "default"
): { error?: ts.Diagnostic; result?: unknown } {
if (typeof query !== "string") {
return { error: cliDiagnostics.compilerOptionRequiresAValueOfType(optionName, "string") };
}

let resolved: string;
try {
resolved = resolve.sync(query, { basedir, extensions: [".js", ".ts", ".tsx"] });
} catch (err) {
if (err.code !== "MODULE_NOT_FOUND") throw err;
return { error: diagnosticFactories.couldNotResolveFrom(kind, query, basedir) };
}

// tslint:disable-next-line: deprecation
const hasNoRequireHook = require.extensions[".ts"] === undefined;
if (hasNoRequireHook && (resolved.endsWith(".ts") || resolved.endsWith(".tsx"))) {
try {
const tsNodePath = resolve.sync("ts-node", { basedir });
const tsNode: typeof import("ts-node") = require(tsNodePath);
tsNode.register({ transpileOnly: true });
} catch (err) {
if (err.code !== "MODULE_NOT_FOUND") throw err;
return { error: diagnosticFactories.toLoadItShouldBeTranspiled(kind, query) };
}
}

const commonjsModule = require(resolved);
const factoryModule = commonjsModule.__esModule ? commonjsModule : { default: commonjsModule };
const result = factoryModule[importName];
if (result === undefined) {
return { error: diagnosticFactories.shouldHaveAExport(kind, query, importName) };
}

return { result };
}
Loading