Skip to content

Commit 6b34308

Browse files
committed
Node-like module resolution
1 parent 110b56f commit 6b34308

14 files changed

Lines changed: 219 additions & 117 deletions

File tree

src/CompilerOptions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ export enum LuaTarget {
5151
LuaJIT = "JIT",
5252
}
5353

54-
export const isBundleEnabled = (options: CompilerOptions) =>
54+
export const isBundleEnabled = (
55+
options: CompilerOptions
56+
): options is CompilerOptions & Required<Pick<CompilerOptions, "luaBundle" | "luaBundleEntry">> =>
5557
options.luaBundle !== undefined && options.luaBundleEntry !== undefined;
5658

5759
export function validateOptions(options: CompilerOptions): ts.Diagnostic[] {

src/LuaPrinter.ts

Lines changed: 14 additions & 1 deletion
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 { assert, 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
@@ -25,6 +25,12 @@ const escapeStringMap: Record<string, string> = {
2525

2626
export const escapeString = (value: string) => `"${value.replace(escapeStringRegExp, char => escapeStringMap[char])}"`;
2727

28+
export const unescapeLuaString = (value: string) => {
29+
assert(value.startsWith('"') && value.endsWith(""), 'Only strings generated by "escapeString" can be unescaped');
30+
// TODO: Unescape
31+
return value.slice(1, -1);
32+
};
33+
2834
/**
2935
* Checks that a name is valid for use in lua function declaration syntax:
3036
*
@@ -39,6 +45,13 @@ const isValidLuaFunctionDeclarationName = (str: string) => /^[a-zA-Z0-9_.]+$/.te
3945
function isSimpleExpression(expression: lua.Expression): boolean {
4046
switch (expression.kind) {
4147
case lua.SyntaxKind.CallExpression:
48+
const calledExpression = (expression as lua.CallExpression).expression;
49+
// __TS__Resolve macro is guaranteed to be pure
50+
if (lua.isIdentifier(calledExpression) && calledExpression.text === "__TS__Resolve") {
51+
return true;
52+
}
53+
54+
return false;
4255
case lua.SyntaxKind.MethodCallExpression:
4356
case lua.SyntaxKind.FunctionExpression:
4457
return false;

src/transformation/utils/diagnostics.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,6 @@ export const invalidAmbientIdentifierName = createDiagnosticFactory(
119119
(text: string) => `Invalid ambient identifier name '${text}'. Ambient identifiers must be valid lua identifiers.`
120120
);
121121

122-
export const unresolvableRequirePath = createDiagnosticFactory(
123-
(path: string) => `Cannot create require path. Module '${path}' does not exist within --rootDir.`
124-
);
125-
126122
export const unsupportedVarDeclaration = createDiagnosticFactory(
127123
"`var` declarations are not supported. Use `let` or `const` instead."
128124
);

src/transformation/visitors/modules/import.ts

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as path from "path";
22
import * as ts from "typescript";
33
import * as lua from "../../../LuaAST";
4-
import { formatPathToLuaPath } from "../../../utils";
54
import { FunctionVisitor, TransformationContext } from "../../context";
65
import { AnnotationKind, getSymbolAnnotations, getTypeAnnotations } from "../../utils/annotations";
76
import { createDefaultExportStringLiteral } from "../../utils/export";
@@ -10,29 +9,6 @@ import { createSafeName } from "../../utils/safe-names";
109
import { peekScope } from "../../utils/scope";
1110
import { transformIdentifier } from "../identifier";
1211
import { transformPropertyName } from "../literal";
13-
import { unresolvableRequirePath } from "../../utils/diagnostics";
14-
15-
const getAbsoluteImportPath = (relativePath: string, directoryPath: string, options: ts.CompilerOptions): string =>
16-
!relativePath.startsWith(".") && options.baseUrl
17-
? path.resolve(options.baseUrl, relativePath)
18-
: path.resolve(directoryPath, relativePath);
19-
20-
function getImportPath(context: TransformationContext, relativePath: string, node: ts.Node): string {
21-
const { options, sourceFile } = context;
22-
const { fileName } = sourceFile;
23-
const rootDir = options.rootDir ? path.resolve(options.rootDir) : path.resolve(".");
24-
25-
const absoluteImportPath = path.format(
26-
path.parse(getAbsoluteImportPath(relativePath, path.dirname(fileName), options))
27-
);
28-
const absoluteRootDirPath = path.format(path.parse(rootDir));
29-
if (absoluteImportPath.includes(absoluteRootDirPath)) {
30-
return formatPathToLuaPath(absoluteImportPath.replace(absoluteRootDirPath, "").slice(1));
31-
} else {
32-
context.diagnostics.push(unresolvableRequirePath(node, relativePath));
33-
return relativePath;
34-
}
35-
}
3612

3713
function shouldResolveModulePath(context: TransformationContext, moduleSpecifier: ts.Expression): boolean {
3814
const moduleOwnerSymbol = context.checker.getSymbolAtLocation(moduleSpecifier);
@@ -49,11 +25,12 @@ export function createModuleRequire(
4925
): lua.CallExpression {
5026
const params: lua.Expression[] = [];
5127
if (ts.isStringLiteral(moduleSpecifier)) {
52-
const modulePath = shouldResolveModulePath(context, moduleSpecifier)
53-
? getImportPath(context, moduleSpecifier.text.replace(/"/g, ""), moduleSpecifier)
54-
: moduleSpecifier.text;
55-
56-
params.push(lua.createStringLiteral(modulePath));
28+
const module = lua.createStringLiteral(moduleSpecifier.text);
29+
params.push(
30+
shouldResolveModulePath(context, moduleSpecifier)
31+
? lua.createCallExpression(lua.createIdentifier("__TS__Resolve"), [module])
32+
: module
33+
);
5734
}
5835

5936
return lua.createCallExpression(lua.createIdentifier("require"), params, tsOriginal);
@@ -110,9 +87,7 @@ export const transformImportDeclaration: FunctionVisitor<ts.ImportDeclaration> =
11087
}
11188
}
11289

113-
const importPath = ts.isStringLiteral(statement.moduleSpecifier)
114-
? statement.moduleSpecifier.text.replace(/"/g, "")
115-
: "module";
90+
const importPath = ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "module";
11691

11792
// Create the require statement to extract values.
11893
// local ____module = require("module")

src/transpilation/bundle.ts

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import * as path from "path";
22
import { SourceNode } from "source-map";
33
import * as ts from "typescript";
4-
import { CompilerOptions } from "../CompilerOptions";
4+
import { CompilerOptions, isBundleEnabled } from "../CompilerOptions";
55
import { escapeString } from "../LuaPrinter";
6-
import { cast, formatPathToLuaPath, isNonNull, normalizeSlashes, trimExtension } from "../utils";
6+
import { assert, normalizeSlashes } from "../utils";
77
import { couldNotFindBundleEntryPoint } from "./diagnostics";
88
import { EmitFile, EmitHost, ProcessedFile } from "./utils";
99

10-
const createModulePath = (baseDir: string, pathToResolve: string) =>
11-
escapeString(formatPathToLuaPath(trimExtension(path.relative(baseDir, pathToResolve))));
12-
1310
// Override `require` to read from ____modules table.
1411
const requireOverride = `
1512
local ____modules = {}
@@ -35,37 +32,36 @@ end
3532
export function getBundleResult(
3633
program: ts.Program,
3734
emitHost: EmitHost,
38-
files: ProcessedFile[]
35+
files: ProcessedFile[],
36+
getRequirePath: (file: ProcessedFile) => string
3937
): [ts.Diagnostic[], EmitFile] {
4038
const diagnostics: ts.Diagnostic[] = [];
4139

4240
const options = program.getCompilerOptions() as CompilerOptions;
43-
const bundleFile = cast(options.luaBundle, isNonNull);
44-
const entryModule = cast(options.luaBundleEntry, isNonNull);
45-
46-
const rootDir = program.getCommonSourceDirectory();
47-
const outDir = options.outDir ?? rootDir;
41+
assert(isBundleEnabled(options));
42+
const bundleFile = options.luaBundle;
43+
const entryModule = options.luaBundleEntry;
4844
const projectRootDir = options.configFilePath
4945
? path.dirname(options.configFilePath)
5046
: emitHost.getCurrentDirectory();
47+
const outputPath = normalizeSlashes(path.resolve(projectRootDir, bundleFile));
5148

5249
// Resolve project settings relative to project file.
5350
const resolvedEntryModule = path.resolve(projectRootDir, entryModule);
54-
const outputPath = normalizeSlashes(path.resolve(projectRootDir, bundleFile));
55-
56-
if (!files.some(f => f.fileName === resolvedEntryModule)) {
51+
const entryFile = files.find(f => f.fileName === resolvedEntryModule);
52+
if (entryFile === undefined) {
5753
diagnostics.push(couldNotFindBundleEntryPoint(entryModule));
5854
return [diagnostics, { outputPath, code: "" }];
5955
}
6056

6157
// For each file: ["<module path>"] = function() <lua content> end,
62-
const moduleTableEntries = files.map(f => moduleSourceNode(f, createModulePath(outDir, f.fileName)));
58+
const moduleTableEntries = files.map(f => moduleSourceNode(f, escapeString(getRequirePath(f))));
6359

6460
// Create ____modules table containing all entries from moduleTableEntries
6561
const moduleTable = createModuleTableNode(moduleTableEntries);
6662

6763
// return require("<entry module path>")
68-
const entryPoint = `return require(${createModulePath(outDir, resolvedEntryModule)})\n`;
64+
const entryPoint = `return require(${escapeString(getRequirePath(entryFile))})\n`;
6965

7066
const bundleNode = joinSourceChunks([requireOverride, moduleTable, entryPoint]);
7167
const { code, map } = bundleNode.toStringWithSourceMap();
@@ -82,17 +78,11 @@ export function getBundleResult(
8278
}
8379

8480
function moduleSourceNode({ code, sourceMapNode }: ProcessedFile, modulePath: string): SourceNode {
85-
const tableEntryHead = `[${modulePath}] = function() `;
86-
const tableEntryTail = "end,\n";
87-
88-
return joinSourceChunks([tableEntryHead, sourceMapNode ?? code, tableEntryTail]);
81+
return joinSourceChunks([`[${modulePath}] = function()\n`, sourceMapNode ?? code, "\nend,\n"]);
8982
}
9083

9184
function createModuleTableNode(fileChunks: SourceChunk[]): SourceNode {
92-
const tableHead = "____modules = {\n";
93-
const tableEnd = "}\n";
94-
95-
return joinSourceChunks([tableHead, ...fileChunks, tableEnd]);
85+
return joinSourceChunks(["____modules = {\n", ...fileChunks, "}\n"]);
9686
}
9787

9888
type SourceChunk = string | SourceNode;

src/transpilation/diagnostics.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,7 @@ export const usingLuaBundleWithInlineMightGenerateDuplicateCode = createSerialDi
3737
"Using 'luaBundle' with 'luaLibImport: \"inline\"' might generate duplicate code. " +
3838
"It is recommended to use 'luaLibImport: \"require\"'.",
3939
}));
40+
41+
export const unresolvableRequirePath = createDiagnosticFactory(
42+
(path: string) => `Cannot create require path. Module '${path}' does not exist within --rootDir.`
43+
);

src/transpilation/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { CompilerOptions } from "../CompilerOptions";
66
import { createEmitOutputCollector, TranspiledFile } from "./output-collector";
77
import { EmitResult, Transpiler } from "./transpiler";
88

9-
export { Plugin } from "./plugins";
109
export * from "./transpile";
1110
export * from "./transpiler";
1211
export { EmitHost } from "./utils";

src/transpilation/macro.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { SourceNode } from "source-map";
2+
import { escapeString, unescapeLuaString } from "../LuaPrinter";
3+
4+
export type ResolveMacroReplacer = (request: string) => string | { error: string };
5+
6+
export function replaceResolveMacroSourceNodes(rootNode: SourceNode, replacer: ResolveMacroReplacer) {
7+
function walkSourceNode(node: SourceNode, parent: SourceNode) {
8+
for (const child of node.children) {
9+
if ((child as any) === "__TS__Resolve") {
10+
parent.children = [replaceResolveMacroInSource(parent.toString(), replacer) as any];
11+
} else if (typeof child === "object") {
12+
walkSourceNode(child, node);
13+
}
14+
}
15+
}
16+
17+
walkSourceNode(rootNode, rootNode);
18+
}
19+
20+
export function replaceResolveMacroInSource(source: string, replacer: ResolveMacroReplacer) {
21+
return source.replace(/__TS__Resolve\((".*?")\)/, (_, match) => {
22+
const request = unescapeLuaString(match);
23+
const replacement = replacer(request);
24+
return typeof replacement === "string"
25+
? escapeString(replacement)
26+
: `--[[ ${request} ]] error(${escapeString(replacement.error)})`;
27+
});
28+
}
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as path from "path";
22
import * as ts from "typescript";
3-
import { CompilerOptions, validateOptions } from "../CompilerOptions";
4-
import { createPrinter } from "../LuaPrinter";
5-
import { createVisitorMap, transformSourceFile } from "../transformation";
6-
import { isNonNull } from "../utils";
3+
import { CompilerOptions, validateOptions } from "../../CompilerOptions";
4+
import { createPrinter } from "../../LuaPrinter";
5+
import { createVisitorMap, transformSourceFile } from "../../transformation";
6+
import { isNonNull } from "../../utils";
77
import { getPlugins, Plugin } from "./plugins";
88
import { getTransformers } from "./transformers";
9-
import { EmitHost, ProcessedFile } from "./utils";
9+
import { EmitHost, ProcessedFile } from "../utils";
10+
11+
export { Plugin };
1012

1113
export interface TranspileOptions {
1214
program: ts.Program;
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as ts from "typescript";
2-
import { CompilerOptions } from "../CompilerOptions";
3-
import { Printer } from "../LuaPrinter";
4-
import { Visitors } from "../transformation/context";
5-
import { getConfigDirectory, resolvePlugin } from "./utils";
2+
import { CompilerOptions } from "../../CompilerOptions";
3+
import { Printer } from "../../LuaPrinter";
4+
import { Visitors } from "../../transformation/context";
5+
import { getConfigDirectory, resolvePlugin } from "../utils";
66

77
export interface Plugin {
88
/**

0 commit comments

Comments
 (0)