From 0839c99a9f1ae405f420137d849471d8eba75fe8 Mon Sep 17 00:00:00 2001 From: Brian Beck Date: Wed, 3 Dec 2025 14:32:02 -0800 Subject: [PATCH] add ignoreScripts option to createRuntime (#12) --- src/components/Mission.tsx | 11 ++ src/torqueScript/runtime.spec.ts | 176 +++++++++++++++++++++++++++++++ src/torqueScript/runtime.ts | 13 +++ src/torqueScript/types.ts | 5 + 4 files changed, 205 insertions(+) diff --git a/src/components/Mission.tsx b/src/components/Mission.tsx index 1a53e8c0..ee8727a8 100644 --- a/src/components/Mission.tsx +++ b/src/components/Mission.tsx @@ -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"); diff --git a/src/torqueScript/runtime.spec.ts b/src/torqueScript/runtime.spec.ts index 4c897ba1..845306a4 100644 --- a/src/torqueScript/runtime.spec.ts +++ b/src/torqueScript/runtime.spec.ts @@ -2538,6 +2538,182 @@ describe("TorqueScript Runtime", () => { }); }); + describe("ignoreScripts option", () => { + function createLoader( + files: Record, + ): (path: string) => Promise { + 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(); diff --git a/src/torqueScript/runtime.ts b/src/torqueScript/runtime.ts index ee622cf8..6af6d9ea 100644 --- a/src/torqueScript/runtime.ts +++ b/src/torqueScript/runtime.ts @@ -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(); const failedScripts = new Set(); + // 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)) { diff --git a/src/torqueScript/types.ts b/src/torqueScript/types.ts index 0c802251..957d4839 100644 --- a/src/torqueScript/types.ts +++ b/src/torqueScript/types.ts @@ -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