add ignoreScripts option to createRuntime (#12)

This commit is contained in:
Brian Beck 2025-12-03 14:32:02 -08:00 committed by GitHub
parent 5f48c1c2d2
commit 0839c99a9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 205 additions and 0 deletions

View file

@ -69,6 +69,17 @@ function useExecutedMission(
fileSystem,
cache: scriptCache,
signal: controller.signal,
ignoreScripts: [
"scripts/admin.cs",
"scripts/ai.cs",
"scripts/aiCTF.cs",
"scripts/aiHunters.cs",
"scripts/deathMessages.cs",
"scripts/graphBuild.cs",
"scripts/navGraph.cs",
"scripts/serverTasks.cs",
"scripts/spdialog.cs",
],
},
onMissionLoadDone: () => {
const missionGroup = runtime.getObjectByName("MissionGroup");

View file

@ -2538,6 +2538,182 @@ describe("TorqueScript Runtime", () => {
});
});
describe("ignoreScripts option", () => {
function createLoader(
files: Record<string, string>,
): (path: string) => Promise<string | null> {
return async (path: string) =>
files[path.replace(/\\/g, "/").toLowerCase()] ?? null;
}
it("skips scripts matching glob patterns", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const runtime = createRuntime({
loadScript: createLoader({
"scripts/main.cs": 'exec("scripts/ai/brain.cs"); $Main = 1;',
"scripts/ai/brain.cs": "$AI = 1;",
}),
ignoreScripts: ["**/ai/**"],
});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
expect(runtime.$g.get("Main")).toBe(1);
expect(runtime.$g.get("AI")).toBe(""); // Not loaded
expect(warnSpy).toHaveBeenCalledWith(
"Ignoring script: scripts/ai/brain.cs",
);
warnSpy.mockRestore();
});
it("is case insensitive", async () => {
const runtime = createRuntime({
loadScript: createLoader({
"scripts/main.cs": 'exec("Scripts/AI/Brain.cs"); $Main = 1;',
"scripts/ai/brain.cs": "$AI = 1;",
}),
ignoreScripts: ["**/ai/**"],
});
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
warnSpy.mockRestore();
expect(runtime.$g.get("Main")).toBe(1);
expect(runtime.$g.get("AI")).toBe(""); // Not loaded due to case-insensitive match
});
it("supports multiple patterns", async () => {
const runtime = createRuntime({
loadScript: createLoader({
"scripts/main.cs":
'exec("scripts/ai/brain.cs"); exec("scripts/vehicles/tank.cs"); exec("scripts/utils.cs"); $Main = 1;',
"scripts/ai/brain.cs": "$AI = 1;",
"scripts/vehicles/tank.cs": "$Vehicle = 1;",
"scripts/utils.cs": "$Utils = 1;",
}),
ignoreScripts: ["**/ai/**", "**/vehicles/**"],
});
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
warnSpy.mockRestore();
expect(runtime.$g.get("Main")).toBe(1);
expect(runtime.$g.get("AI")).toBe(""); // Ignored
expect(runtime.$g.get("Vehicle")).toBe(""); // Ignored
expect(runtime.$g.get("Utils")).toBe(1); // Loaded (not in ignore list)
});
it("marks ignored scripts as failed (not retried)", async () => {
let loadCount = 0;
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const runtime = createRuntime({
loadScript: async (path) => {
loadCount++;
if (path.toLowerCase() === "scripts/ignored.cs") {
return "$Ignored = 1;";
}
if (path.toLowerCase() === "scripts/main.cs") {
return 'exec("scripts/ignored.cs"); exec("scripts/ignored.cs"); $Main = 1;';
}
return null;
},
ignoreScripts: ["**/ignored.cs"],
});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
// Should only warn once (second exec sees it's already in failedScripts)
expect(
warnSpy.mock.calls.filter(
(c) => c[0] === "Ignoring script: scripts/ignored.cs",
).length,
).toBe(1);
// Loader should only be called for main.cs, not for ignored.cs
expect(loadCount).toBe(1);
warnSpy.mockRestore();
});
it("matches exact filenames with glob", async () => {
const runtime = createRuntime({
loadScript: createLoader({
"scripts/main.cs":
'exec("scripts/skip-me.cs"); exec("scripts/keep-me.cs"); $Main = 1;',
"scripts/skip-me.cs": "$Skip = 1;",
"scripts/keep-me.cs": "$Keep = 1;",
}),
ignoreScripts: ["**/skip-me.cs"],
});
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
warnSpy.mockRestore();
expect(runtime.$g.get("Main")).toBe(1);
expect(runtime.$g.get("Skip")).toBe(""); // Ignored
expect(runtime.$g.get("Keep")).toBe(1); // Loaded
});
it("does nothing when ignoreScripts is empty array", async () => {
const runtime = createRuntime({
loadScript: createLoader({
"scripts/main.cs": 'exec("scripts/dep.cs"); $Main = 1;',
"scripts/dep.cs": "$Dep = 1;",
}),
ignoreScripts: [],
});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
expect(runtime.$g.get("Main")).toBe(1);
expect(runtime.$g.get("Dep")).toBe(1); // Loaded normally
});
it("does nothing when ignoreScripts is not provided", async () => {
const runtime = createRuntime({
loadScript: createLoader({
"scripts/main.cs": 'exec("scripts/dep.cs"); $Main = 1;',
"scripts/dep.cs": "$Dep = 1;",
}),
// No ignoreScripts option
});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
expect(runtime.$g.get("Main")).toBe(1);
expect(runtime.$g.get("Dep")).toBe(1); // Loaded normally
});
it("exec() returns false for ignored scripts", async () => {
const runtime = createRuntime({
loadScript: createLoader({
"scripts/main.cs": '$Result = exec("scripts/ignored.cs"); $Main = 1;',
"scripts/ignored.cs": "$Ignored = 1;",
}),
ignoreScripts: ["**/ignored.cs"],
});
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const script = await runtime.loadFromPath("scripts/main.cs");
script.execute();
warnSpy.mockRestore();
expect(runtime.$g.get("Main")).toBe(1);
// exec() should return false when the script is ignored (same as failed)
expect(runtime.$g.get("Result")).toBe(false);
});
});
describe("nsRef with execution context", () => {
it("tracks execution context for Parent:: calls via nsRef", () => {
const runtime = createRuntime();

View file

@ -1,3 +1,4 @@
import picomatch from "picomatch";
import { generate } from "./codegen";
import { parse, type Program } from "./index";
import { createBuiltins as defaultCreateBuiltins } from "./builtins";
@ -94,6 +95,11 @@ export function createRuntime(
const executedScripts = new Set<string>();
const failedScripts = new Set<string>();
// Create matcher for ignored scripts (case insensitive)
const isIgnoredScript =
options.ignoreScripts && options.ignoreScripts.length > 0
? picomatch(options.ignoreScripts, { nocase: true })
: null;
// Use cache if provided, otherwise create new maps
const cache = options.cache ?? createScriptCache();
const scripts = cache.scripts;
@ -1093,6 +1099,13 @@ export function createRuntime(
return;
}
// Skip if script matches ignore patterns
if (isIgnoredScript && isIgnoredScript(normalized)) {
console.warn(`Ignoring script: ${ref}`);
state.failedScripts.add(normalized);
return;
}
// If this script is an ancestor in our load chain, it's a cycle - skip
// (awaiting would cause deadlock)
if (ancestors.has(normalized)) {

View file

@ -96,6 +96,11 @@ export interface TorqueRuntimeOptions {
* are exec()'d dynamically and can't be statically analyzed.
*/
preloadScripts?: string[];
/**
* Glob patterns for scripts to ignore during dependency resolution.
* Matched scripts will be skipped and logged as warnings.
*/
ignoreScripts?: string[];
/**
* Cache for parsed scripts and generated code. If provided, the runtime
* will use this cache to store and retrieve parsed ASTs, avoiding redundant