X Tutup
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
168 changes: 168 additions & 0 deletions src/transpilation/find-lua-requires.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
export interface LuaRequire {
from: number;
to: number;
requirePath: string;
}

export function findLuaRequires(lua: string): LuaRequire[] {
return findRequire(lua, 0);
}

function findRequire(lua: string, offset: number): LuaRequire[] {
const result = [];

while (offset < lua.length) {
const c = lua[offset];
if (
c === "r" &&
(offset === 0 ||
isWhitespace(lua[offset - 1]) ||
lua[offset - 1] === "]" ||
lua[offset - 1] === "(" ||
lua[offset - 1] === "[")
) {
const m = matchRequire(lua, offset);
if (m.matched) {
offset = m.match.to + 1;
result.push(m.match);
} else {
offset = m.end;
}
} else if (c === '"' || c === "'") {
offset = readString(lua, offset, c).offset; // Skip string and surrounding quotes
} else if (c === "-" && offset + 1 < lua.length && lua[offset + 1] === "-") {
offset = skipComment(lua, offset);
} else {
offset++;
}
}

return result;
}

type MatchResult<T> = { matched: true; match: T } | { matched: false; end: number };

function matchRequire(lua: string, offset: number): MatchResult<LuaRequire> {
const start = offset;
for (const c of "require") {
if (offset > lua.length) {
return { matched: false, end: offset };
}

if (lua[offset] !== c) {
return { matched: false, end: offset };
}
offset++;
}

offset = skipWhitespace(lua, offset);

let hasParentheses = false;

if (offset > lua.length) {
return { matched: false, end: offset };
} else {
if (lua[offset] === "(") {
hasParentheses = true;
offset++;
offset = skipWhitespace(lua, offset);
} else if (lua[offset] === '"' || lua[offset] === "'") {
// require without parentheses
} else {
// otherwise fail match
return { matched: false, end: offset };
}
}

if (offset > lua.length || (lua[offset] !== '"' && lua[offset] !== "'")) {
return { matched: false, end: offset };
}

const { value: requireString, offset: offsetAfterString } = readString(lua, offset, lua[offset]);
offset = offsetAfterString; // Skip string and surrounding quotes

if (hasParentheses) {
offset = skipWhitespace(lua, offset);

if (offset > lua.length || lua[offset] !== ")") {
return { matched: false, end: offset };
}

offset++;
}

return { matched: true, match: { from: start, to: offset - 1, requirePath: requireString } };
}

function readString(lua: string, offset: number, delimiter: string): { value: string; offset: number } {
expect(lua, offset, delimiter);
offset++;

let start = offset;
let result = "";

let escaped = false;
while (offset < lua.length && (lua[offset] !== delimiter || escaped)) {
if (lua[offset] === "\\" && !escaped) {
escaped = true;
} else {
if (lua[offset] === delimiter) {
result += lua.slice(start, offset - 1);
start = offset;
}
escaped = false;
}

offset++;
}

if (offset < lua.length) {
expect(lua, offset, delimiter);
}

result += lua.slice(start, offset);
return { value: result, offset: offset + 1 };
}

function skipWhitespace(lua: string, offset: number): number {
while (offset < lua.length && isWhitespace(lua[offset])) {
offset++;
}
return offset;
}

function isWhitespace(c: string): boolean {
return c === " " || c === "\t" || c === "\r" || c === "\n";
}

function skipComment(lua: string, offset: number): number {
expect(lua, offset, "-");
expect(lua, offset + 1, "-");
offset += 2;

if (offset + 1 < lua.length && lua[offset] === "[" && lua[offset + 1] === "[") {
return skipMultiLineComment(lua, offset);
} else {
return skipSingleLineComment(lua, offset);
}
}

function skipMultiLineComment(lua: string, offset: number): number {
while (offset < lua.length && !(lua[offset] === "]" && lua[offset - 1] === "]")) {
offset++;
}
return offset + 1;
}

function skipSingleLineComment(lua: string, offset: number): number {
while (offset < lua.length && lua[offset] !== "\n") {
offset++;
}
return offset + 1;
}

function expect(lua: string, offset: number, char: string) {
if (lua[offset] !== char) {
throw new Error(`Expected ${char} at position ${offset} but found ${lua[offset]}`);
}
}
66 changes: 29 additions & 37 deletions src/transpilation/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getEmitPathRelativeToOutDir, getProjectRoot, getSourceDir } from "./tra
import { formatPathToLuaPath, normalizeSlashes, trimExtension } from "../utils";
import { couldNotReadDependency, couldNotResolveRequire } from "./diagnostics";
import { BuildMode, CompilerOptions } from "../CompilerOptions";
import { findLuaRequires, LuaRequire } from "./find-lua-requires";

const resolver = resolve.ResolverFactory.createResolver({
extensions: [".lua"],
Expand Down Expand Up @@ -40,12 +41,13 @@ class ResolutionContext {
if (this.resolvedFiles.has(file.fileName)) return;
this.resolvedFiles.set(file.fileName, file);

for (const required of findRequiredPaths(file.code)) {
// Do this backwards so the replacements do not mess with the positions of the previous requires
for (const required of findLuaRequires(file.code).reverse()) {
// Do not resolve noResolution paths
if (required.startsWith("@NoResolution:")) {
if (required.requirePath.startsWith("@NoResolution:")) {
// Remove @NoResolution prefix if not building in library mode
if (!isBuildModeLibrary(this.program)) {
const path = required.replace("@NoResolution:", "");
const path = required.requirePath.replace("@NoResolution:", "");
replaceRequireInCode(file, required, path);
replaceRequireInSourceMap(file, required, path);
}
Expand All @@ -58,25 +60,27 @@ class ResolutionContext {
}
}

public resolveImport(file: ProcessedFile, required: string): void {
public resolveImport(file: ProcessedFile, required: LuaRequire): void {
// Do no resolve lualib - always use the lualib of the application entry point, not the lualib from external packages
if (required === "lualib_bundle") {
if (required.requirePath === "lualib_bundle") {
this.resolvedFiles.set("lualib_bundle", { fileName: "lualib_bundle", code: "" });
return;
}

if (this.noResolvePaths.has(required)) {
if (this.noResolvePaths.has(required.requirePath)) {
if (this.options.tstlVerbose) {
console.log(`Skipping module resolution of ${required} as it is in the tsconfig noResolvePaths.`);
console.log(
`Skipping module resolution of ${required.requirePath} as it is in the tsconfig noResolvePaths.`
);
}
return;
}

const dependencyPath = this.resolveDependencyPath(file, required);
const dependencyPath = this.resolveDependencyPath(file, required.requirePath);
if (!dependencyPath) return this.couldNotResolveImport(required, file);

if (this.options.tstlVerbose) {
console.log(`Resolved ${required} to ${normalizeSlashes(dependencyPath)}`);
console.log(`Resolved ${required.requirePath} to ${normalizeSlashes(dependencyPath)}`);
}

this.processDependency(dependencyPath);
Expand Down Expand Up @@ -110,13 +114,13 @@ class ResolutionContext {
this.addAndResolveDependencies(dependency);
}

private couldNotResolveImport(required: string, file: ProcessedFile): void {
private couldNotResolveImport(required: LuaRequire, file: ProcessedFile): void {
const fallbackRequire = fallbackResolve(required, getSourceDir(this.program), path.dirname(file.fileName));
replaceRequireInCode(file, required, fallbackRequire);
replaceRequireInSourceMap(file, required, fallbackRequire);

this.diagnostics.push(
couldNotResolveRequire(required, path.relative(getProjectRoot(this.program), file.fileName))
couldNotResolveRequire(required.requirePath, path.relative(getProjectRoot(this.program), file.fileName))
);
}

Expand Down Expand Up @@ -305,35 +309,23 @@ function isBuildModeLibrary(program: ts.Program) {
return program.getCompilerOptions().buildMode === BuildMode.Library;
}

function findRequiredPaths(code: string): string[] {
// Find all require("<path>") paths in a lua code string
const paths: string[] = [];
const pattern = /(^|\s|;|=|\()require\s*\(?(["|'])(.+?)\2\)?/g;
// eslint-disable-next-line @typescript-eslint/ban-types
let match: RegExpExecArray | null;
while ((match = pattern.exec(code))) {
paths.push(match[3]);
}

return paths;
}

function replaceRequireInCode(file: ProcessedFile, originalRequire: string, newRequire: string): void {
function replaceRequireInCode(file: ProcessedFile, originalRequire: LuaRequire, newRequire: string): void {
const requirePath = formatPathToLuaPath(newRequire.replace(".lua", ""));

// Escape special characters to prevent the regex from breaking...
const escapedRequire = originalRequire.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");

file.code = file.code.replace(
new RegExp(`(^|\\s|;|=|\\()require\\s*\\(?["|']${escapedRequire}["|']\\)?`),
`$1require("${requirePath}")`
);
file.code = file.code =
file.code.substring(0, originalRequire.from) +
`require("${requirePath}")` +
file.code.substring(originalRequire.to + 1);
}

function replaceRequireInSourceMap(file: ProcessedFile, originalRequire: string, newRequire: string): void {
function replaceRequireInSourceMap(file: ProcessedFile, originalRequire: LuaRequire, newRequire: string): void {
const requirePath = formatPathToLuaPath(newRequire.replace(".lua", ""));
if (file.sourceMapNode) {
replaceInSourceMap(file.sourceMapNode, file.sourceMapNode, `"${originalRequire}"`, `"${requirePath}"`);
replaceInSourceMap(
file.sourceMapNode,
file.sourceMapNode,
`"${originalRequire.requirePath}"`,
`"${requirePath}"`
);
}
}

Expand Down Expand Up @@ -375,10 +367,10 @@ function hasSourceFileInProject(filePath: string, program: ts.Program) {
}

// Transform an import path to a lua require that is probably not correct, but can be used as fallback when regular resolution fails
function fallbackResolve(required: string, sourceRootDir: string, fileDir: string): string {
function fallbackResolve(required: LuaRequire, sourceRootDir: string, fileDir: string): string {
return formatPathToLuaPath(
path
.normalize(path.join(path.relative(sourceRootDir, fileDir), required))
.normalize(path.join(path.relative(sourceRootDir, fileDir), required.requirePath))
.split(path.sep)
.filter(s => s !== "." && s !== "..")
.join(path.sep)
Expand Down
2 changes: 1 addition & 1 deletion test/transpile/module-resolution.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ test("module resolution should not rewrite @NoResolution requires in library mod
});

// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1050
test("module resolution should not try to resolve resolve-like functions", () => {
test("module resolution should not try to resolve require-like functions", () => {
util.testModule`
function custom_require(this: void, value: string) {
return value;
Expand Down
Loading
X Tutup