diff --git a/.changeset/purple-planets-join.md b/.changeset/purple-planets-join.md new file mode 100644 index 000000000000..b80c58fda45e --- /dev/null +++ b/.changeset/purple-planets-join.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Expose `wrangler pages functions build` command, which takes the `functions` folder and compiles it into a single Worker. + +This was already done in `wrangler pages dev`, so this change just exposes this build command for use in our build image, or for people who want to do it themselves. diff --git a/packages/wrangler/src/pages.tsx b/packages/wrangler/src/pages.tsx index 12ffb7783c30..fbd11de4354c 100644 --- a/packages/wrangler/src/pages.tsx +++ b/packages/wrangler/src/pages.tsx @@ -643,10 +643,16 @@ const RUNNING_BUILDERS: BuildResult[] = []; async function buildFunctions({ scriptPath, functionsDirectory, + minify = false, + sourcemap = false, + watch = false, onEnd, }: { scriptPath: string; functionsDirectory: string; + minify?: boolean; + sourcemap?: boolean; + watch?: boolean; onEnd?: () => void; }) { RUNNING_BUILDERS.forEach( @@ -670,254 +676,293 @@ async function buildFunctions({ await buildWorker({ routesModule, outfile: scriptPath, - minify: false, // TODO: Expose option to enable - sourcemap: true, - watch: true, + minify, + sourcemap, + watch, onEnd, }) ); } export const pages: BuilderCallback = (yargs) => { - return yargs.command( - "dev [directory] [-- command]", - "🧑‍💻 Develop your full-stack Pages application locally", - (yargs) => { - return yargs - .positional("directory", { - type: "string", - demandOption: undefined, - description: "The directory of static assets to serve", - }) - .positional("command", { - type: "string", - demandOption: undefined, - description: "The proxy command to run", - }) - .options({ - local: { - type: "boolean", - default: true, - description: "Run on my machine", - }, - port: { - type: "number", - default: 8788, - description: "The port to listen on (serve from)", - }, - proxy: { - type: "number", - description: - "The port to proxy (where the static assets are served)", - }, - "script-path": { + return yargs + .command( + "dev [directory] [-- command]", + "🧑‍💻 Develop your full-stack Pages application locally", + (yargs) => { + return yargs + .positional("directory", { type: "string", - default: "_worker.js", - description: - "The location of the single Worker script if not using functions", - }, - binding: { - type: "array", - description: "Bind variable/secret (KEY=VALUE)", - alias: "b", - }, - kv: { - type: "array", - description: "KV namespace to bind", - alias: "k", - }, - do: { - type: "array", - description: "Durable Object to bind (NAME=CLASS)", - alias: "o", - }, - // TODO: Miniflare user options - }); - }, - async ({ - local, - directory, - port, - proxy: requestedProxyPort, - "script-path": singleWorkerScriptPath, - binding: bindings = [], - kv: kvs = [], - do: durableObjects = [], - "--": remaining = [], - }) => { - if (!local) { - console.error("Only local mode is supported at the moment."); - return; - } - - const functionsDirectory = "./functions"; - const usingFunctions = existsSync(functionsDirectory); + demandOption: undefined, + description: "The directory of static assets to serve", + }) + .positional("command", { + type: "string", + demandOption: undefined, + description: "The proxy command to run", + }) + .options({ + local: { + type: "boolean", + default: true, + description: "Run on my machine", + }, + port: { + type: "number", + default: 8788, + description: "The port to listen on (serve from)", + }, + proxy: { + type: "number", + description: + "The port to proxy (where the static assets are served)", + }, + "script-path": { + type: "string", + default: "_worker.js", + description: + "The location of the single Worker script if not using functions", + }, + binding: { + type: "array", + description: "Bind variable/secret (KEY=VALUE)", + alias: "b", + }, + kv: { + type: "array", + description: "KV namespace to bind", + alias: "k", + }, + do: { + type: "array", + description: "Durable Object to bind (NAME=CLASS)", + alias: "o", + }, + // TODO: Miniflare user options + }); + }, + async ({ + local, + directory, + port, + proxy: requestedProxyPort, + "script-path": singleWorkerScriptPath, + binding: bindings = [], + kv: kvs = [], + do: durableObjects = [], + "--": remaining = [], + }) => { + if (!local) { + console.error("Only local mode is supported at the moment."); + return; + } - const command = remaining as (string | number)[]; + const functionsDirectory = "./functions"; + const usingFunctions = existsSync(functionsDirectory); - let proxyPort: number | void; + const command = remaining as (string | number)[]; - if (directory === undefined) { - proxyPort = await spawnProxyProcess({ - port: requestedProxyPort, - command, - }); - if (proxyPort === undefined) return undefined; - } + let proxyPort: number | void; - let miniflareArgs: MiniflareOptions = {}; + if (directory === undefined) { + proxyPort = await spawnProxyProcess({ + port: requestedProxyPort, + command, + }); + if (proxyPort === undefined) return undefined; + } - let scriptReady = true; + let miniflareArgs: MiniflareOptions = {}; - if (usingFunctions) { - const scriptPath = join(tmpdir(), "./functionsWorker.js"); + let scriptReady = true; - console.log(`Compiling worker to "${scriptPath}"...`); + if (usingFunctions) { + const scriptPath = join(tmpdir(), "./functionsWorker.js"); - await buildFunctions({ - scriptPath, - functionsDirectory, - onEnd: () => { - scriptReady = true; - }, - }); + console.log(`Compiling worker to "${scriptPath}"...`); - watch([functionsDirectory], { - persistent: true, - }).on("all", async () => { await buildFunctions({ scriptPath, functionsDirectory, + sourcemap: true, + watch: true, onEnd: () => { scriptReady = true; }, }); - }); - miniflareArgs = { - scriptPath, - }; - } else { - const scriptPath = - directory !== undefined - ? join(directory, singleWorkerScriptPath) - : singleWorkerScriptPath; + watch([functionsDirectory], { + persistent: true, + }).on("all", async () => { + await buildFunctions({ + scriptPath, + functionsDirectory, + sourcemap: true, + watch: true, + onEnd: () => { + scriptReady = true; + }, + }); + }); - if (existsSync(scriptPath)) { miniflareArgs = { scriptPath, }; } else { - console.log("No functions. Shimming..."); - miniflareArgs = { - // TODO: The fact that these request/response hacks are necessary is ridiculous. - // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well) - script: ` + const scriptPath = + directory !== undefined + ? join(directory, singleWorkerScriptPath) + : singleWorkerScriptPath; + + if (existsSync(scriptPath)) { + miniflareArgs = { + scriptPath, + }; + } else { + console.log("No functions. Shimming..."); + miniflareArgs = { + // TODO: The fact that these request/response hacks are necessary is ridiculous. + // We need to eliminate them from env.ASSETS.fetch (not sure if just local or prod as well) + script: ` export default { async fetch(request, env, context) { const response = await env.ASSETS.fetch(request.url, request) return new Response(response.body, response) } }`, - }; + }; + } } - } - // Defer importing miniflare until we really need it - const { Miniflare, Log, LogLevel } = await import("miniflare"); - const { Response, fetch } = await import("@miniflare/core"); - - // Should only be called if no proxyPort, using `assert.fail()` here - // means the type of `assetsFetch` is still `typeof fetch` - const assetsFetch = proxyPort - ? () => assert.fail() - : await generateAssetsFetch(directory); - const miniflare = new Miniflare({ - port, - watch: true, - modules: true, - - log: new Log(LogLevel.ERROR, { prefix: "pages" }), - logUnhandledRejections: true, - sourceMap: true, - - kvNamespaces: kvs.map((kv) => kv.toString()), + // Defer importing miniflare until we really need it + const { Miniflare, Log, LogLevel } = await import("miniflare"); + const { Response, fetch } = await import("@miniflare/core"); + + // Should only be called if no proxyPort, using `assert.fail()` here + // means the type of `assetsFetch` is still `typeof fetch` + const assetsFetch = proxyPort + ? () => assert.fail() + : await generateAssetsFetch(directory); + const miniflare = new Miniflare({ + port, + watch: true, + modules: true, + + log: new Log(LogLevel.ERROR, { prefix: "pages" }), + logUnhandledRejections: true, + sourceMap: true, + + kvNamespaces: kvs.map((kv) => kv.toString()), + + durableObjects: Object.fromEntries( + durableObjects.map((durableObject) => + durableObject.toString().split("=") + ) + ), - durableObjects: Object.fromEntries( - durableObjects.map((durableObject) => - durableObject.toString().split("=") - ) - ), + // User bindings + bindings: { + ...Object.fromEntries( + bindings.map((binding) => binding.toString().split("=")) + ), + }, - // User bindings - bindings: { - ...Object.fromEntries( - bindings.map((binding) => binding.toString().split("=")) - ), - }, - - // env.ASSETS.fetch - serviceBindings: { - async ASSETS(request) { - if (proxyPort) { - try { - const url = new URL(request.url); - url.host = `127.0.0.1:${proxyPort}`; - return await fetch(url, request); - } catch (thrown) { - console.error(`Could not proxy request: ${thrown}`); - - // TODO: Pretty error page - return new Response( - `[wrangler] Could not proxy request: ${thrown}`, - { status: 502 } - ); - } - } else { - try { - return await assetsFetch(request); - } catch (thrown) { - console.error(`Could not serve static asset: ${thrown}`); - - // TODO: Pretty error page - return new Response( - `[wrangler] Could not serve static asset: ${thrown}`, - { status: 502 } - ); + // env.ASSETS.fetch + serviceBindings: { + async ASSETS(request) { + if (proxyPort) { + try { + const url = new URL(request.url); + url.host = `127.0.0.1:${proxyPort}`; + return await fetch(url, request); + } catch (thrown) { + console.error(`Could not proxy request: ${thrown}`); + + // TODO: Pretty error page + return new Response( + `[wrangler] Could not proxy request: ${thrown}`, + { status: 502 } + ); + } + } else { + try { + return await assetsFetch(request); + } catch (thrown) { + console.error(`Could not serve static asset: ${thrown}`); + + // TODO: Pretty error page + return new Response( + `[wrangler] Could not serve static asset: ${thrown}`, + { status: 502 } + ); + } } - } + }, }, - }, - kvPersist: true, - durableObjectsPersist: true, - cachePersist: true, + kvPersist: true, + durableObjectsPersist: true, + cachePersist: true, - ...miniflareArgs, - }); + ...miniflareArgs, + }); - // Wait for esbuild to finish building the script - while (!scriptReady) {} + // Wait for esbuild to finish building the script + while (!scriptReady) {} - try { - // `startServer` might throw if user code contains errors - const server = await miniflare.startServer(); - console.log(`Serving at http://127.0.0.1:${port}/`); - - if (process.env.BROWSER !== "none") { - try { - await open(`http://127.0.0.1:${port}/`); - } catch {} - } + try { + // `startServer` might throw if user code contains errors + const server = await miniflare.startServer(); + console.log(`Serving at http://127.0.0.1:${port}/`); - EXIT_CALLBACKS.push(() => { - server.close(); - miniflare.dispose().catch((err) => miniflare.log.error(err)); - }); - } catch (e) { - miniflare.log.error(e); - EXIT("Could not start Miniflare.", 1); + if (process.env.BROWSER !== "none") { + try { + await open(`http://127.0.0.1:${port}/`); + } catch {} + } + + EXIT_CALLBACKS.push(() => { + server.close(); + miniflare.dispose().catch((err) => miniflare.log.error(err)); + }); + } catch (e) { + miniflare.log.error(e); + EXIT("Could not start Miniflare.", 1); + } } - } - ); + ) + .command("functions", "Cloudflare Pages Functions", (yargs) => + yargs.command( + "build", + "Compile a folder of Cloudflare Pages Functions into a single Worker", + (yargs) => + yargs.options({ + "script-path": { + type: "string", + default: "_worker.js", + description: "The location of the output Worker script", + }, + minify: { + type: "boolean", + default: false, + description: "Minify the output Worker script", + }, + sourcemap: { + type: "boolean", + default: false, + description: "Generate a sourcemap for the output Worker script", + }, + }), + async ({ "script-path": scriptPath, minify, sourcemap }) => { + const functionsDirectory = "./functions"; + + await buildFunctions({ + scriptPath, + functionsDirectory, + minify, + sourcemap, + }); + } + ) + ); };