Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 1x 6x 6x 6x 1x 1x 27x 27x 27x 27x 27x 27x 27x 27x 27x 1x 1x 1x 26x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 27x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 27x 1x 1x 1x 1x 1x 25x 25x 25x 27x 5x 5x 5x 5x 5x 20x 20x 20x | import * as vm from 'vm';
import * as fs from 'fs';
// ---------------------------------------------------------------------------
// Public context types
// ---------------------------------------------------------------------------
export interface ScriptRequestContext {
url: string;
method: string;
headers: Record<string, string>;
body: string | undefined;
}
export interface ScriptResponseContext {
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
duration: number;
/** Parse the response body as JSON. Returns null if the body is not valid JSON. */
json(): unknown;
}
export interface PreScriptContext {
/** The outgoing request — all fields are mutable. */
request: ScriptRequestContext;
/** Combined variables map (env merged with file vars). Mutations are persisted back to the .http file. */
variables: Record<string, string>;
/** Read-only snapshot of the active environment variables. */
env: Record<string, string>;
/** Routes to the "Laika Scripts" output channel. */
console: ScriptConsole;
}
export interface PostScriptContext {
/** The request that was sent — read-only. */
request: Readonly<ScriptRequestContext>;
/** The received response. */
response: ScriptResponseContext;
/** Combined variables map. Mutations are persisted back to the .http file. */
variables: Record<string, string>;
/** Active environment variables. Mutations are persisted back to http-client.env.json.user. */
env: Record<string, string>;
/** Routes to the "Laika Scripts" output channel. */
console: ScriptConsole;
}
interface ScriptConsole {
log(...args: unknown[]): void;
warn(...args: unknown[]): void;
error(...args: unknown[]): void;
}
// ---------------------------------------------------------------------------
// OutputChannel interface (subset of vscode.OutputChannel — keeps this file
// free of the vscode dependency so it can be unit-tested without the extension host)
// ---------------------------------------------------------------------------
export interface OutputChannel {
appendLine(value: string): void;
/** Optional — when present, reveals the output panel (e.g. vscode.OutputChannel.show). */
show?(preserveFocus?: boolean): void;
}
// ---------------------------------------------------------------------------
// Runner options
// ---------------------------------------------------------------------------
export interface ScriptRunnerOptions {
/** Seconds before the synchronous portion of a script is timed out. Default: 10. */
timeoutSeconds?: number;
/** Called with a user-visible message when a script error occurs (e.g. show a VS Code notification). */
onError?: (message: string) => void;
}
// ---------------------------------------------------------------------------
// Runner
// ---------------------------------------------------------------------------
/**
* Run a .js script file in a sandboxed Node.js vm context.
*
* The script receives `context` as its global scope. All mutations to
* `context.request`, `context.variables`, etc. are visible to the caller
* after this function returns, because `vm.createContext` contextifies
* the object in-place rather than copying it.
*
* Scripts are wrapped in an async IIFE so top-level `await` is supported.
* `options.timeoutSeconds` (default 10) guards the synchronous portion of
* execution; async continuations run to completion.
*
* @returns `true` if the script ran successfully, `false` if the file was
* not found (the caller may choose to skip rather than abort).
* @throws Re-throws script exceptions after logging them to the output channel.
*/
function formatError(err: unknown): string {
if (!(err instanceof Error)) { return String(err); }
const cause = (err as NodeJS.ErrnoException).cause;
const causeStr = cause instanceof Error ? ` (cause: ${cause.message})` : '';
return err.message + causeStr;
}
export async function runScript(
scriptPath: string,
context: PreScriptContext | PostScriptContext,
outputChannel: OutputChannel,
options?: ScriptRunnerOptions,
): Promise<boolean> {
let code: string;
try {
code = fs.readFileSync(scriptPath, 'utf8');
} catch {
outputChannel.appendLine(`[Laika Scripts] File not found, skipping: ${scriptPath}`);
return false;
}
const timeoutMs = (options?.timeoutSeconds ?? 10) * 1000;
const onError = options?.onError ?? ((_msg: string) => undefined);
// Build the console implementation that routes to the output channel.
const scriptConsole = {
log: (...args: unknown[]) => outputChannel.appendLine('[log] ' + args.map(String).join(' ')),
warn: (...args: unknown[]) => outputChannel.appendLine('[warn] ' + args.map(String).join(' ')),
error: (...args: unknown[]) => outputChannel.appendLine('[error] ' + args.map(String).join(' ')),
};
// Inject globals before contextifying so they exist on the sandbox object.
const sandbox = context as unknown as Record<string, unknown>;
sandbox['console'] = scriptConsole;
// Inject fetch and related Web API globals (available in Node 18+ / Electron 28+).
// vm.createContext does NOT inherit the parent's globalThis, so we must inject explicitly.
if (typeof globalThis.fetch === 'function') {
sandbox['fetch'] = globalThis.fetch.bind(globalThis);
sandbox['Headers'] = globalThis.Headers;
sandbox['Request'] = globalThis.Request;
sandbox['Response'] = globalThis.Response;
}
// vm.createContext contextifies the object in-place — property mutations
// made inside the script are reflected on the original object after execution.
const vmContext = vm.createContext(context as unknown as object);
// Re-assign after createContext: Electron's runtime can set its own console
// on the V8 context during contextification, overwriting what we injected above.
(vmContext as unknown as Record<string, unknown>)['console'] = scriptConsole;
outputChannel.appendLine(`[Laika Scripts] ▶ ${scriptPath}`);
const wrapped = `(async () => { ${code} })()`;
let resultPromise: Promise<unknown>;
try {
const script = new vm.Script(wrapped, { filename: scriptPath });
resultPromise = script.runInContext(vmContext, { timeout: timeoutMs }) as Promise<unknown>;
} catch (err) {
const msg = `[Laika Scripts] Error in ${scriptPath}: ${formatError(err)}`;
outputChannel.appendLine(msg);
onError(msg);
throw err;
}
try {
await resultPromise;
} catch (err) {
const msg = `[Laika Scripts] Error in ${scriptPath}: ${formatError(err)}`;
outputChannel.appendLine(msg);
onError(msg);
throw err;
}
return true;
}
|