harden v2 custom widgets and workshop
This commit is contained in:
1 parent
e8a2c87c2d
commit
723b46926a
194 files changed
+44862
-1265
No files matched your search
@@ -0,0 +1,144 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const composeArgs = ["compose", "-f", "apps/workshop/docker-compose.yml"];
|
||||
const pocketBaseUrl = "http://127.0.0.1:8090";
|
||||
const requiredCollections = ["submissions", "workshop_listings"];
|
||||
const isWindows = process.platform === "win32";
|
||||
const children = new Set();
|
||||
let shuttingDown = false;
|
||||
|
||||
try {
|
||||
process.loadEnvFile(".env");
|
||||
} catch (error) {
|
||||
if (error?.code !== "ENOENT") throw error;
|
||||
// The Workshop has safe local defaults when no .env file exists. GitHub OAuth remains optional.
|
||||
}
|
||||
|
||||
function start(command, args, options = {}) {
|
||||
const child = spawn(command, args, {
|
||||
stdio: "inherit",
|
||||
shell: isWindows,
|
||||
...options,
|
||||
});
|
||||
child.once("error", (error) => {
|
||||
child.spawnError = error;
|
||||
});
|
||||
children.add(child);
|
||||
child.once("close", () => children.delete(child));
|
||||
return child;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: "inherit",
|
||||
shell: isWindows,
|
||||
...options,
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function endpointIsReady(path) {
|
||||
try {
|
||||
const response = await fetch(`${pocketBaseUrl}${path}`, {
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForWorkshop(compose) {
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (compose.spawnError) throw compose.spawnError;
|
||||
if (compose.exitCode !== null) {
|
||||
throw new Error(`PocketBase stopped before it became ready (exit ${compose.exitCode})`);
|
||||
}
|
||||
|
||||
const checks = [
|
||||
endpointIsReady("/api/health"),
|
||||
...requiredCollections.map((collection) =>
|
||||
endpointIsReady(`/api/collections/${collection}/records?page=1&perPage=1`),
|
||||
),
|
||||
];
|
||||
if ((await Promise.all(checks)).every(Boolean)) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
||||
}
|
||||
|
||||
throw new Error("PocketBase did not expose its health endpoint and required collections within 120 seconds");
|
||||
}
|
||||
|
||||
async function shutdown(exitCode = 0) {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
|
||||
for (const child of children) {
|
||||
if (child.exitCode === null) child.kill("SIGTERM");
|
||||
}
|
||||
|
||||
try {
|
||||
await run("docker", [...composeArgs, "stop", "workshop"], {
|
||||
env: workshopEnvironment,
|
||||
stdio: "ignore",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Could not stop the Workshop container cleanly:", error);
|
||||
exitCode ||= 1;
|
||||
}
|
||||
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
|
||||
const workshopEnvironment = {
|
||||
...process.env,
|
||||
PB_EXPOSE_PORT: "8090",
|
||||
PB_ALLOWED_ORIGINS: process.env.PB_ALLOWED_ORIGINS ?? "*",
|
||||
WORKSHOP_PUBLIC_ORIGIN: process.env.WORKSHOP_PUBLIC_ORIGIN ?? "http://127.0.0.1:3003",
|
||||
WORKSHOP_WEB_URL: process.env.WORKSHOP_WEB_URL ?? "http://127.0.0.1:3003/workshop",
|
||||
};
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.once(signal, () => void shutdown());
|
||||
}
|
||||
|
||||
const compose = start("docker", [...composeArgs, "up", "--build", "workshop"], {
|
||||
env: workshopEnvironment,
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("Waiting for PocketBase health and Workshop collections on http://127.0.0.1:8090 …");
|
||||
await waitForWorkshop(compose);
|
||||
console.log("Workshop is ready. Starting documentation on http://127.0.0.1:3003/workshop …");
|
||||
|
||||
const docs = start("pnpm", ["--filter", "@homarr/docs", "dev"], {
|
||||
env: {
|
||||
...process.env,
|
||||
HOMARR_WEBSITE_URL: process.env.HOMARR_WEBSITE_URL ?? "http://127.0.0.1:3003",
|
||||
WORKSHOP_API_URL: process.env.WORKSHOP_API_URL ?? pocketBaseUrl,
|
||||
WORKSHOP_WEB_URL: process.env.WORKSHOP_WEB_URL ?? "http://127.0.0.1:3003/workshop",
|
||||
DOCS_BASE_URL: process.env.DOCS_BASE_URL ?? "/",
|
||||
},
|
||||
});
|
||||
|
||||
const exitCode = await new Promise((resolve) => {
|
||||
const handleError = (error) => {
|
||||
console.error(error);
|
||||
resolve(1);
|
||||
};
|
||||
compose.once("error", handleError);
|
||||
docs.once("error", handleError);
|
||||
compose.once("exit", (code) => resolve(code ?? 1));
|
||||
docs.once("exit", (code) => resolve(code ?? 1));
|
||||
});
|
||||
await shutdown(exitCode);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await shutdown(1);
|
||||
}
|
||||
Reference in new issue
Block a user