mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-19 05:29:46 +08:00
- Add the Node-compatible RequestInfo alias without widening supported fetch inputs. - Cover RequestInfo resolution at the shared preflight boundary. Fixes #369 Co-authored-by: Hagege Ruben <20857346+HagegeR@users.noreply.github.com>
202 lines
7.4 KiB
TypeScript
202 lines
7.4 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, join } from "node:path";
|
|
import { describe, expect, test } from "vitest";
|
|
import { checkPreflight, loadProgram } from "../../src/frontend/program.js";
|
|
import { tsgoPath } from "../../src/frontend/dts-paths.js";
|
|
import * as ts from "../../src/frontend/ts7/adapter.js";
|
|
import { CheckerFacade } from "../../src/frontend/ts7/checker.js";
|
|
import type { Checker, Project } from "typescript/unstable/sync";
|
|
|
|
describe("tsgo virtual filesystem paths", () => {
|
|
test("matches slash-normalized Windows callback paths", () => {
|
|
expect(tsgoPath("C:\\Users\\Alice\\project\\tsconfig.json", "win32"))
|
|
.toBe("C:/Users/Alice/project/tsconfig.json");
|
|
expect(tsgoPath("C:/Users/Alice/project/tsconfig.json", "win32"))
|
|
.toBe("C:/Users/Alice/project/tsconfig.json");
|
|
});
|
|
|
|
test("preserves backslashes that are literal POSIX filename characters", () => {
|
|
expect(tsgoPath("/tmp/project\\name/tsconfig.json", "linux"))
|
|
.toBe("/tmp/project\\name/tsconfig.json");
|
|
});
|
|
});
|
|
|
|
test("preserves leading BOMs in TS7 AST and checker string payloads", () => {
|
|
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-bom-"));
|
|
const entry = join(dir, "entry.ts");
|
|
writeFileSync(entry, [
|
|
'const alone = "\\uFEFF";',
|
|
'const doubled = "\\uFEFF\\uFEFF";',
|
|
"const template = `\\uFEFFvalue`;",
|
|
].join("\n"));
|
|
|
|
const load = loadProgram(entry);
|
|
try {
|
|
const literals: ts.StringLiteralLike[] = [];
|
|
ts.walkPreorder(load.entry, (node) => {
|
|
if (ts.isStringLiteralLike(node)) literals.push(node);
|
|
});
|
|
const expected = ["\uFEFF", "\uFEFF\uFEFF", "\uFEFFvalue"];
|
|
expect(literals.map((literal) => literal.text)).toEqual(expected);
|
|
|
|
const checker = load.program.getTypeChecker();
|
|
expect(literals.map((literal) => {
|
|
const type = checker.getTypeAtLocation(literal);
|
|
return type.isStringLiteralType() ? type.value : null;
|
|
})).toEqual(expected);
|
|
} finally {
|
|
load.dispose();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("retains source-file BOM stripping", () => {
|
|
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-source-bom-"));
|
|
const entry = join(dir, "entry.ts");
|
|
writeFileSync(entry, "\uFEFFconst value = 1;\n");
|
|
|
|
const load = loadProgram(entry);
|
|
try {
|
|
expect(load.entry.statements[0]?.getStart(load.entry)).toBe(0);
|
|
expect(load.entry.text).toBe("const value = 1;\n");
|
|
} finally {
|
|
load.dispose();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("fallback ambient resolves RequestInfo during preflight", () => {
|
|
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
const dir = mkdtempSync(join(tempRoot, "scriptc-request-info-"));
|
|
const entry = join(dir, "entry.ts");
|
|
writeFileSync(entry, [
|
|
'const target: RequestInfo = "https://example.invalid/";',
|
|
"console.log(typeof target);",
|
|
].join("\n"));
|
|
|
|
const load = loadProgram(entry);
|
|
try {
|
|
const diagnostics = checkPreflight(load);
|
|
expect(diagnostics, diagnostics.map((diag) => `${diag.code}: ${diag.message}`).join("\n"))
|
|
.toEqual([]);
|
|
} finally {
|
|
load.dispose();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("preflight batches symbols in deferred TDZ-analysis roots", () => {
|
|
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
const dir = mkdtempSync(join(tempRoot, "scriptc-preflight-batch-"));
|
|
const entry = join(dir, "entry.cjs");
|
|
const locals = Array.from({ length: 24 }, (_, index) => ` const local${index} = ${index};`).join("\n");
|
|
const uses = Array.from({ length: 24 }, (_, index) => ` value += local${index};`).join("\n");
|
|
writeFileSync(entry, `
|
|
function before() {
|
|
let value = 0;
|
|
${locals}
|
|
${uses}
|
|
return required.value + value;
|
|
}
|
|
before();
|
|
const required = require("./dep.cjs");
|
|
console.log(required.value);
|
|
`);
|
|
writeFileSync(join(dir, "dep.cjs"), "exports.value = 1;\n");
|
|
|
|
const load = loadProgram(entry);
|
|
try {
|
|
const program = load.program as unknown as {
|
|
project: Project;
|
|
checkerFacade: CheckerFacade | null;
|
|
};
|
|
const calls: unknown[][] = [];
|
|
const proxy = new Proxy(program.project.checker, {
|
|
get(target, prop, receiver) {
|
|
const value = Reflect.get(target, prop, receiver);
|
|
if (prop !== "getSymbolAtLocation" || typeof value !== "function") return value;
|
|
return (...args: unknown[]) => {
|
|
calls.push(args);
|
|
return (value as (...values: unknown[]) => unknown).apply(target, args);
|
|
};
|
|
},
|
|
}) as Checker;
|
|
program.checkerFacade = new CheckerFacade(proxy, { project: program.project });
|
|
|
|
expect(checkPreflight(load).map((diag) => diag.code)).toContain("SC1013");
|
|
expect(calls.some(([arg]) => Array.isArray(arg) && arg.length > 24)).toBe(true);
|
|
expect(calls.some(([arg]) => !Array.isArray(arg))).toBe(false);
|
|
} finally {
|
|
load.dispose();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("project paths resolve identically in the TS 7 program and scriptc module graph", () => {
|
|
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
const dir = mkdtempSync(join(tempRoot, "scriptc-project-paths-"));
|
|
const src = join(dir, "src");
|
|
mkdirSync(src);
|
|
writeFileSync(
|
|
join(dir, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: {
|
|
strictNullChecks: true,
|
|
baseUrl: ".",
|
|
paths: { "@app/*": ["src/*"] },
|
|
},
|
|
}),
|
|
);
|
|
writeFileSync(join(src, "message.ts"), 'export const message = "paths agree";\n');
|
|
const entry = join(dir, "main.ts");
|
|
writeFileSync(entry, 'import { message } from "@app/message";\nconsole.log(message);\n');
|
|
|
|
const load = loadProgram(entry);
|
|
try {
|
|
const diagnostics = checkPreflight(load);
|
|
expect(diagnostics, diagnostics.map((diag) => `${diag.code}: ${diag.message}`).join("\n"))
|
|
.toEqual([]);
|
|
expect(load.moduleOrder.map((file) => basename(file.fileName))).toEqual(["message.ts", "main.ts"]);
|
|
} finally {
|
|
load.dispose();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("inherited project paths stay relative to their declaring config", () => {
|
|
const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
const dir = mkdtempSync(join(tempRoot, "scriptc-inherited-paths-"));
|
|
const base = join(dir, "base");
|
|
const app = join(dir, "app");
|
|
mkdirSync(join(base, "src"), { recursive: true });
|
|
mkdirSync(app);
|
|
writeFileSync(
|
|
join(base, "tsconfig.json"),
|
|
JSON.stringify({
|
|
compilerOptions: {
|
|
strictNullChecks: true,
|
|
baseUrl: ".",
|
|
paths: { "@base/*": ["src/*"] },
|
|
},
|
|
}),
|
|
);
|
|
writeFileSync(join(base, "src/message.ts"), 'export const message = "inherited paths agree";\n');
|
|
writeFileSync(join(app, "tsconfig.json"), JSON.stringify({ extends: "../base/tsconfig.json" }));
|
|
const entry = join(app, "main.ts");
|
|
writeFileSync(entry, 'import { message } from "@base/message";\nconsole.log(message);\n');
|
|
|
|
const load = loadProgram(entry);
|
|
try {
|
|
const diagnostics = checkPreflight(load);
|
|
expect(diagnostics, diagnostics.map((diag) => `${diag.code}: ${diag.message}`).join("\n"))
|
|
.toEqual([]);
|
|
expect(load.moduleOrder.map((file) => basename(file.fileName))).toEqual(["message.ts", "main.ts"]);
|
|
} finally {
|
|
load.dispose();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|