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
8 changes: 7 additions & 1 deletion src/CompilerOptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ts from "typescript";
import { JsxEmit } from "typescript";
import * as diagnosticFactories from "./transpilation/diagnostics";
import { Plugin } from "./transpilation/plugins";

type OmitIndexSignature<T> = {
[K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
Expand All @@ -23,14 +24,19 @@ export interface LuaPluginImport {
[option: string]: any;
}

export interface InMemoryLuaPlugin {
plugin: Plugin | ((options: Record<string, any>) => Plugin);
[option: string]: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these options and the ((options: Record<string, any>) => Plugin) really add anything? Since the plugin is in-memory, it can just capture options from its scope, I don't see what first passing the options into tstl and then back via a factory function actually adds.

Copy link
Contributor Author

@iced-wav iced-wav Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's mostly useful for plugin compatibility, this way you can load third-party plugins directly in memory even if they were written for tsconfig the same way and without manually loading it separately first.

It also makes internal representation slightly simpler by separating the responsibility of loading a plugin from importing it, and don't end up with two different kinds of plugin factories.

If removing it, I think the best approach is to remove the callback version (from InMemoryLuaPlugin) entirely, without the options parameter it just needlessly adds another plugin factory type to handle. (see next post regarding lifetime management)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess a big plus for the first point is also that it would simplify re-using the same config object between runs (such as in unit tests) and having tstl itself manage its lifetime the same way it would with a tsconfig plugin.

If tstl only accepts a plugin object directly, you'd need to rebuild the config object with a fresh Plugin object each time you use it in case the factory function keeps internal state intended for compiling a single project.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, that makes sense

}

export interface TypeScriptToLuaOptions {
buildMode?: BuildMode;
extension?: string;
luaBundle?: string;
luaBundleEntry?: string;
luaTarget?: LuaTarget;
luaLibImport?: LuaLibImportKind;
luaPlugins?: LuaPluginImport[];
luaPlugins?: Array<LuaPluginImport | InMemoryLuaPlugin>;
noImplicitGlobalVariables?: boolean;
noImplicitSelf?: boolean;
noHeader?: boolean;
Expand Down
26 changes: 17 additions & 9 deletions src/transpilation/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,23 @@ export function getPlugins(program: ts.Program): { diagnostics: ts.Diagnostic[];
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);
const factory = (() => {
if ("plugin" in pluginOption) {
return pluginOption.plugin;
} else {
const { error: resolveError, result: factory } = resolvePlugin(
"plugin",
`${optionName}.name`,
getConfigDirectory(options),
pluginOption.name,
pluginOption.import
);

if (resolveError) diagnostics.push(resolveError);
return factory;
}
})();

if (factory === undefined) continue;

const plugin = typeof factory === "function" ? factory(pluginOption) : factory;
Expand Down
59 changes: 59 additions & 0 deletions test/transpile/plugins/plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as path from "path";
import * as util from "../../util";
import { Plugin } from "../../../src/transpilation/plugins";
import * as ts from "typescript";

test("printer", () => {
Expand Down Expand Up @@ -182,3 +183,61 @@ test("afterEmit plugin", () => {
expect(diagnostic?.category).toBe(ts.DiagnosticCategory.Message);
expect(diagnostic?.messageText).toContain("After emit");
});

test("in memory plugin", () => {
const { diagnostics } = util.testModule``
.setOptions({
luaPlugins: [
{
plugin: {
afterEmit(program: ts.Program) {
return [
{
category: ts.DiagnosticCategory.Message,
messageText: "In memory plugin diagnostic message!",
code: 1234,
file: program.getSourceFiles()[0],
start: undefined,
length: undefined,
} satisfies ts.Diagnostic,
];
},
} satisfies Plugin,
},
],
})
.getLuaResult();

expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].code).toBe(1234);
});

test("in memory plugin with factory", () => {
const { diagnostics } = util.testModule``
.setOptions({
luaPlugins: [
{
code: 1234,
plugin: options =>
({
afterEmit(program: ts.Program) {
return [
{
category: ts.DiagnosticCategory.Message,
messageText: "In memory plugin diagnostic message!",
code: options.code,
file: program.getSourceFiles()[0],
start: undefined,
length: undefined,
} satisfies ts.Diagnostic,
];
},
} satisfies Plugin),
},
],
})
.getLuaResult();

expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].code).toBe(1234);
});
Loading