From a9078aa0cf6dd9a606cd31cf77472eec5681bbbf Mon Sep 17 00:00:00 2001 From: Max Isom Date: Mon, 22 Jan 2024 16:39:08 -0800 Subject: [PATCH] Allow running in same Node vm --- .gitignore | 1 + package-lock.json | 1 + package.json | 1 + src/bundle/watch.ts | 3 ++ src/cli/commands/dev.ts | 66 ++++++++++++++++++++++++++++++++++------- 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index f1deda3..3205b91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist node_modules bundled.* +.edgespec diff --git a/package-lock.json b/package-lock.json index 290a20c..4eeb43e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@edge-runtime/node-utils": "^2.2.2", "@edge-runtime/primitives": "^4.0.5", + "chalk": "^5.3.0", "clipanion": "^4.0.0-rc.3", "esbuild": "^0.19.11", "human-readable": "^0.2.1", diff --git a/package.json b/package.json index f3048aa..5ca7e6a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "@edge-runtime/node-utils": "^2.2.2", "@edge-runtime/primitives": "^4.0.5", + "chalk": "^5.3.0", "clipanion": "^4.0.0-rc.3", "esbuild": "^0.19.11", "human-readable": "^0.2.1", diff --git a/src/bundle/watch.ts b/src/bundle/watch.ts index 0f637b7..f1dd7d6 100644 --- a/src/bundle/watch.ts +++ b/src/bundle/watch.ts @@ -61,7 +61,9 @@ const constructEntrypoint = async (options: BundleOptions) => { ` } +// todo: combine with bundle.ts export const watchAndBundle = async (options: BundleOptions) => { + // todo: this should watch all relevant paths from a tsconfig.json, not just the ./api directory const watcher = new Watcher(options.directoryPath, { recursive: true, ignoreInitial: true, @@ -80,6 +82,7 @@ export const watchAndBundle = async (options: BundleOptions) => { bundle: true, format: "esm", write: false, + sourcemap: "inline", ...options.esbuild, }) await ctx.rebuild() diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index ce22e7a..1851450 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -5,6 +5,9 @@ import { EdgeRuntime } from "edge-runtime" import { watchAndBundle } from "src/bundle/watch" import { durationFormatter } from "human-readable" import ora from "ora" +import { handleRequestWithEdgeSpec } from "src" +import path from "node:path" +import chalk from "chalk" export class DevCommand extends Command { static paths = [[`dev`]] @@ -21,6 +24,12 @@ export class DevCommand extends Command { description: "The directory of your app", }) + // todo: better syntax for this flag, seems to need --no-emulate-wintercg + emulateWinterCG = Option.Boolean("--emulate-wintercg", true, { + description: + "Emulate the WinterCG runtime. When true, native APIs are unavailable.", + }) + async execute() { const listenSpinner = ora({ text: "Starting server...", @@ -29,14 +38,37 @@ export class DevCommand extends Command { }).start() let runtime: EdgeRuntime + let nonWinterCGHandler: ReturnType const server = createServer( transformToNodeBuilder({ defaultOrigin: `http://localhost:${this.port}`, })(async (req) => { - const response = await runtime.dispatchFetch(req.url, req) - await response.waitUntil() - return response + if (this.emulateWinterCG) { + const response = await runtime.dispatchFetch(req.url, req) + await response.waitUntil() + return response + } + + try { + return await nonWinterCGHandler(req) + } catch (error) { + if (error instanceof Error) { + this.context.stderr.write( + chalk.bgRed("\nUnhandled exception:\n") + + (error.stack ?? error.message) + + "\n" + ) + } else { + this.context.stderr.write( + "Unhandled exception:\n" + JSON.stringify(error) + "\n" + ) + } + + return new Response("Internal server error", { + status: 500, + }) + } }) ) @@ -47,11 +79,14 @@ export class DevCommand extends Command { }) }) + const command = this await watchAndBundle({ directoryPath: this.appDirectory, - // todo: allow running in the same Node.js process/context to access system APIs - bundledAdapter: "wintercg-minimal", + bundledAdapter: this.emulateWinterCG ? "wintercg-minimal" : undefined, esbuild: { + platform: this.emulateWinterCG ? "browser" : "node", + outfile: this.emulateWinterCG ? undefined : ".edgespec/dev-bundle.js", + write: this.emulateWinterCG ? false : true, plugins: [ { name: "watch", @@ -71,15 +106,26 @@ export class DevCommand extends Command { allowMultiples: ["m", "s", "ms"], }) - build.onEnd((result) => { + build.onEnd(async (result) => { const durationMs = performance.now() - buildStartedAt spinner.succeed(`Built in ${timeFormatter(durationMs)}`) - if (!result.outputFiles) throw new Error("no output files") + if (command.emulateWinterCG) { + if (!result.outputFiles) throw new Error("no output files") - runtime = new EdgeRuntime({ - initialCode: result.outputFiles[0].text, - }) + runtime = new EdgeRuntime({ + initialCode: result.outputFiles[0].text, + }) + } else { + const edgeSpecModule = await import( + `file:${path.resolve( + ".edgespec/dev-bundle.js" + )}#${Date.now()}` + ) + nonWinterCGHandler = handleRequestWithEdgeSpec( + edgeSpecModule.default + ) + } }) }, },