faster mock freestyle (#1033)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Test / docker (push) Has been cancelled
Runs E2E API Tests / build (22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E API Tests with external source of truth / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test With Custom Base Port / restart-dev-and-test-with-custom-base-port (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added environment variable handling and logging interception for
improved script execution visibility.

* **Bug Fixes**
  * Enhanced error handling with structured error responses and logs.

* **Chores**
  * Updated dependencies: arktype and react-dom.
  * Improved temporary work directory management and cleanup process.
  * Optimized script execution model for better performance.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
BilalG1 2025-12-01 09:35:31 -08:00 committed by GitHub
parent eb5e1cb28d
commit e8e78cf148
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -8,8 +8,9 @@ RUN cat <<'EOF' > package.json
{
"name": "freestyle-mock",
"dependencies": {
"arktype": "2.1.2",
"arktype": "2.1.20",
"react": "19.1.1",
"react-dom": "19.1.1",
"@react-email/components": "0.1.1"
}
}
@ -26,6 +27,13 @@ import { join } from "path";
import { spawn } from "child_process";
type LogLine = { message: string; type: string };
const preinstalledNodeModules = new Map<string, string>([
["arktype", "2.1.20"],
["react-dom", "19.1.1"],
["react", "19.1.1"],
["@react-email/components", "0.1.1"],
]);
const baseWorkDir = "/app/tmp";
serve({
port: 8080,
@ -38,41 +46,13 @@ serve({
const { script, config = {} } = await req.json();
// 1. temp dir --------------------------------------------------------------
const workDir = join("/tmp", "job-" + crypto.randomUUID());
await mkdir(baseWorkDir, { recursive: true });
const workDir = join(baseWorkDir, "job-" + crypto.randomUUID());
await mkdir(workDir, { recursive: true });
// 2. write user script and runner files -----------------------------------
// Write the user script as-is
// 2. write user script ----------------------------------------------------
const scriptFile = join(workDir, "script.ts");
await writeFile(scriptFile, script);
// Write a runner that imports from the user script
const runnerScript = `
const logs: Array<{ message: string; type: string }> = [];
// Capture console output
const originalConsole = { ...console };
const logMethods = ['log', 'info', 'warn', 'error', 'debug'];
logMethods.forEach(method => {
(console as any)[method] = (...args: any[]) => {
logs.push({ message: args.map(String).join(' '), type: method });
(originalConsole as any)[method](...args);
};
});
// Import the user function
import userFunction from './script.ts';
try {
const result = await (typeof userFunction === 'function' ? userFunction() : userFunction);
console.log(JSON.stringify({ result, logs }));
} catch (error) {
console.error(JSON.stringify({ error: error.message, logs }));
process.exit(1);
}
`;
const runnerFile = join(workDir, "runner.ts");
await writeFile(runnerFile, runnerScript);
// 2.1. create package.json for dependencies -------------------------------
const packageJson = {
@ -83,7 +63,12 @@ try {
await writeFile(packageJsonFile, JSON.stringify(packageJson, null, 2));
// 3. install dependencies -------------------------------------------------
if (config.nodeModules && Object.keys(config.nodeModules).length) {
const requestedNodeModules = config.nodeModules || {};
const needsInstall = Object.entries(requestedNodeModules).some(([name, version]) => {
return preinstalledNodeModules.get(name) !== version;
});
if (needsInstall && Object.keys(requestedNodeModules).length) {
const installProcess = spawn("bun", ["install"], {
cwd: workDir,
stdio: "pipe"
@ -99,67 +84,44 @@ try {
// 4. run user script & capture logs ---------------------------------------
const logs: LogLine[] = [];
let result: unknown = null;
const logMethods = ['log', 'info', 'warn', 'error', 'debug'] as const;
const originalConsole = Object.fromEntries(logMethods.map((method) => [method, console[method]]));
logMethods.forEach(method => {
(console as any)[method] = (...args: any[]) => {
logs.push({ message: args.map(String).join(' '), type: method });
(originalConsole as any)[method](...args);
};
});
const envVars = config.envVars || {};
const previousEnv = new Map<string, string | undefined>();
Object.entries(envVars).forEach(([key, value]) => {
previousEnv.set(key, process.env[key]);
process.env[key] = value;
});
try {
// Set environment variables
const env = { ...process.env, ...(config.envVars ?? {}) };
// ── spawn a new Bun process ──
const bunProcess = spawn("bun", ["run", runnerFile], {
cwd: workDir,
env,
stdio: "pipe"
});
let stdout = "";
let stderr = "";
bunProcess.stdout?.on("data", (data) => {
stdout += data.toString();
});
bunProcess.stderr?.on("data", (data) => {
stderr += data.toString();
});
await new Promise((resolve, reject) => {
bunProcess.on("close", (code) => {
if (code === 0) resolve(void 0);
else reject(new Error(stderr || `Process exited with code ${code}`));
});
});
if (stderr) {
throw new Error(stderr);
}
// Parse the wrapped script output
try {
const lines = stdout.trim().split('\n');
const lastLine = lines[lines.length - 1];
const parsed = JSON.parse(lastLine);
if (parsed && typeof parsed === "object") {
if ("error" in parsed) {
throw new Error(parsed.error);
}
result = parsed.result;
logs.push(...(parsed.logs || []));
} else {
result = parsed;
}
} catch (parseError) {
// If JSON parsing fails, treat stdout as the result
result = stdout.trim();
}
const userModule = await import(`file://${scriptFile}`);
const userFunction = (userModule as any).default ?? userModule;
result = await (typeof userFunction === 'function' ? userFunction() : userFunction);
} catch (err: any) {
return new Response(JSON.stringify({ error: err.message, logs }), {
return new Response(JSON.stringify({ error: err.message || String(err), logs }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
} finally {
previousEnv.forEach((value, key) => {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
});
logMethods.forEach(method => {
(console as any)[method] = originalConsole[method];
});
try { await rm(workDir, { recursive: true }); } catch { /* ignore */ }
}