rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
134 lines (133 loc) • 5.74 kB
JavaScript
import path from "path";
import { resolve } from "path";
import { writeFile } from "fs/promises";
import { unstable_readConfig } from "wrangler";
import { createServer as createViteServer } from "vite";
import tmp from "tmp-promise";
import baseDebug from "debug";
import enhancedResolve from "enhanced-resolve";
import { readFile } from "fs/promises";
import { Lang, parse } from "@ast-grep/napi";
import { redwood } from "../vite/index.mjs";
import { findWranglerConfig } from "../lib/findWranglerConfig.mjs";
const debug = baseDebug("rwsdk:worker-run");
export const runWorkerScript = async (relativeScriptPath) => {
if (!relativeScriptPath) {
console.error("Error: Script path is required");
console.log("\nUsage:");
console.log(" npm run worker:run <script-path>");
console.log("\nOptions:");
console.log(" RWSDK_WRANGLER_CONFIG Environment variable for config path");
console.log("\nExamples:");
console.log(" npm run worker:run src/scripts/seed.ts");
console.log(" RWSDK_WRANGLER_CONFIG=custom.toml npm run worker:run src/scripts/seed.ts\n");
process.exit(1);
}
const scriptPath = resolve(process.cwd(), relativeScriptPath);
debug("Running worker script: %s", scriptPath);
const workerConfigPath = process.env.RWSDK_WRANGLER_CONFIG
? resolve(process.cwd(), process.env.RWSDK_WRANGLER_CONFIG)
: await findWranglerConfig(process.cwd());
debug("Using wrangler config: %s", workerConfigPath);
const workerConfig = unstable_readConfig({
config: workerConfigPath,
env: "dev",
});
const durableObjectsToExport = workerConfig.durable_objects?.bindings
.filter((binding) => !binding.script_name)
.map((binding) => binding.class_name) ?? [];
const workerEntryRelativePath = workerConfig.main;
const workerEntryPath = workerEntryRelativePath ?? path.join(process.cwd(), "src/worker.tsx");
const durableObjectExports = [];
if (durableObjectsToExport.length > 0) {
const resolver = enhancedResolve.create.sync({
extensions: [".mts", ".ts", ".tsx", ".mjs", ".js", ".jsx", ".json"],
});
const workerEntryContents = await readFile(workerEntryPath, "utf-8");
const workerEntryAst = parse(Lang.Tsx, workerEntryContents);
const exportDeclarations = [
...workerEntryAst.root().findAll('export { $$$EXPORTS } from "$MODULE"'),
...workerEntryAst.root().findAll("export { $$$EXPORTS } from '$MODULE'"),
...workerEntryAst.root().findAll("export { $$$EXPORTS } from '$MODULE'"),
];
for (const exportDeclaration of exportDeclarations) {
const moduleMatch = exportDeclaration.getMatch("MODULE");
const exportsMatch = exportDeclaration.getMultipleMatches("EXPORTS");
if (!moduleMatch || exportsMatch.length === 0) {
continue;
}
const modulePath = moduleMatch.text();
const specifiers = exportsMatch.map((m) => m.text().trim());
for (const specifier of specifiers) {
if (durableObjectsToExport.includes(specifier)) {
const resolvedPath = resolver(path.dirname(workerEntryPath), modulePath);
durableObjectExports.push(`export { ${specifier} } from "${resolvedPath}";`);
}
}
}
}
const tmpDir = await tmp.dir({
prefix: "rw-worker-run-",
unsafeCleanup: true,
});
const relativeTmpWorkerEntryPath = "worker.tsx";
const tmpWorkerPath = path.join(tmpDir.path, "wrangler.json");
const tmpWorkerEntryPath = path.join(tmpDir.path, relativeTmpWorkerEntryPath);
const scriptWorkerConfig = {
...workerConfig,
configPath: tmpWorkerPath,
userConfigPath: tmpWorkerPath,
main: relativeTmpWorkerEntryPath,
};
try {
await writeFile(tmpWorkerPath, JSON.stringify(scriptWorkerConfig, null, 2));
await writeFile(tmpWorkerEntryPath, `
${durableObjectExports.join("\n")}
export { default } from "${scriptPath}";
`);
debug("Worker config written to: %s", tmpWorkerPath);
debug("Worker entry written to: %s", tmpWorkerEntryPath);
process.env.RWSDK_WORKER_RUN = "1";
const server = await createViteServer({
configFile: false,
plugins: [
redwood({
configPath: tmpWorkerPath,
includeCloudflarePlugin: true,
entry: {
worker: tmpWorkerEntryPath,
},
}),
],
server: {
port: 0,
},
});
debug("Vite server created");
try {
await server.listen();
const address = server.httpServer?.address();
debug("Server listening on address: %o", address);
if (!address || typeof address === "string") {
throw new Error("Dev server address is invalid");
}
debug("Fetching worker...");
await fetch(`http://localhost:${address.port}/`);
debug("Worker fetched successfully");
}
finally {
debug("Closing server...");
server.close();
debug("Server closed");
}
}
finally {
debug("Closing inspector servers...");
debug("Temporary files cleaned up");
}
// todo(justinvdm, 01 Apr 2025): Investigate what handles are remaining open
process.exit(0);
};
if (import.meta.url === new URL(process.argv[1], import.meta.url).href) {
runWorkerScript(process.argv[2]);
}