Something went wrong. Try again.
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485import { dirname, isAbsolute, relative, resolve, SEP } from "../shared/posixPath";import { parseScript, type Command, type Script } from "./parser";
export interface IncludeError { /** The file the problem is in, as it should be shown. */ file: string; line: number; message: string;}
export interface IncludeOptions { /** The script being run: its absolute path, and the name to show for it. */ file: string; display: string; /** Included scripts must be inside this folder (the project). */ root: string; readFile(path: string): string; /** The built-in checks, when the script runs where checks are built in: an included script's `call` is held to the same list (E011). */ checks?: readonly string[];}
const MAX_DEPTH = 8;
/** * Replaces each `run PATH` command with the commands of that script's steps, in place, so the run is one flat list of commands that each * know which file and line they came from. The included script's own start, covers, needs and timeout are ignored: the script being run owns the run. * Variables set before the `run` can be used in the included script; ones it sets are not visible to the script that included it. */export function expandIncludes(script: Script, options: IncludeOptions): { ok: true; script: Script } | { ok: false; errors: IncludeError[] } { const errors: IncludeError[] = []; // what the included scripts say they do: a run that posts or spends through an include is a run that posts or spends let posts = script.posts === true; let spends = script.spends === true; const shown = (abs: string) => relative(options.root, abs).split(SEP).join("/");
function expand(commands: Command[], file: string, chain: string[], known: Set<string>): Command[] { const out: Command[] = []; for (const c of commands) { if (c.verb !== "run") { out.push(c); if (c.verb === "set" || c.verb === "capture") known.add(c.name); continue; } const here = c.file ?? (file === options.file ? options.display : shown(file)); const problem = (message: string) => void errors.push({ file: here, line: c.line, message }); const target = resolve(dirname(file), c.path); const inside = relative(options.root, target); if (c.path.includes("\0") || isAbsolute(c.path) || inside.startsWith("..") || isAbsolute(inside) || !target.endsWith(".aite")) { problem(`run ${JSON.stringify(c.path)} must be a .aite file inside the project`); continue; } if (chain.length >= MAX_DEPTH) { problem(`run nests too deep (more than ${MAX_DEPTH} levels): ${[...chain, target].map(shown).join(" -> ")}`); continue; } if (chain.includes(target)) { problem(`run loop: ${[...chain, target].map(shown).join(" -> ")}`); continue; } let text: string; try { text = options.readFile(target); } catch (err) { problem(`run cannot read ${shown(target)}: ${err instanceof Error ? err.message : String(err)}`); continue; } const parsed = parseScript(text, { variables: [...known], profiles: [...(script.needsSession !== undefined ? [script.needsSession] : []), ...(script.alsoNeeds ?? []).map((n) => n.name)], file: shown(target), ...(options.checks !== undefined ? { checks: options.checks } : {}) }); if (!parsed.ok) { for (const e of parsed.errors) errors.push({ file: shown(target), line: e.line, message: e.message }); continue; } if (parsed.script.posts === true) posts = true; if (parsed.script.spends === true) spends = true; const inner = parsed.script.steps.flatMap((s) => s.commands); out.push(...expand(inner, target, [...chain, target], new Set(known))); } return out; }
if (!script.steps.some((s) => s.commands.some((c) => c.verb === "run"))) return { ok: true, script }; const known = new Set<string>(); const steps = script.steps.map((step) => ({ ...step, commands: expand(step.commands, options.file, [options.file], known) })); return errors.length > 0 ? { ok: false, errors } : { ok: true, script: { ...script, ...(posts ? { posts: true as const } : {}), ...(spends ? { spends: true as const } : {}), steps } };}