diff --git a/examples/middleware/open-next.config.ts b/examples/middleware/open-next.config.ts index cb92cf9..0f7794f 100644 --- a/examples/middleware/open-next.config.ts +++ b/examples/middleware/open-next.config.ts @@ -1,13 +1,23 @@ import type { OpenNextConfig } from "@opennextjs/aws/types/open-next"; const config: OpenNextConfig = { - default: {}, + default: { + override: { + wrapper: "cloudflare-node", + converter: "edge", + // Unused implementation + incrementalCache: "dummy", + tagCache: "dummy", + queue: "dummy", + }, + }, middleware: { external: true, override: { - wrapper: "cloudflare", + wrapper: "cloudflare-edge", converter: "edge", + proxyExternalRequest: "fetch", }, }, }; diff --git a/examples/middleware/tsconfig.json b/examples/middleware/tsconfig.json index 5a7b86a..74ea7c8 100755 --- a/examples/middleware/tsconfig.json +++ b/examples/middleware/tsconfig.json @@ -21,5 +21,5 @@ ] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules", "open-next.config.ts"] + "exclude": ["node_modules", "open-next.config.ts", "worker.ts"] } diff --git a/examples/middleware/wrangler.toml b/examples/middleware/wrangler.toml index c57907b..df075da 100644 --- a/examples/middleware/wrangler.toml +++ b/examples/middleware/wrangler.toml @@ -1,7 +1,6 @@ #:schema node_modules/wrangler/config-schema.json name = "middleware" -main = ".open-next/index.mjs" - +main = ".open-next/worker.ts" compatibility_date = "2024-09-23" compatibility_flags = ["nodejs_compat"] diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index 2cb9fdd..deb7671 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -27,7 +27,7 @@ You can use [`create-next-app`](https://nextjs.org/docs/pages/api-reference/cli/ ```toml #:schema node_modules/wrangler/config-schema.json name = "" - main = ".open-next/index.mjs" + main = ".open-next/worker.ts" compatibility_date = "2024-09-23" compatibility_flags = ["nodejs_compat"] @@ -44,52 +44,34 @@ import type { OpenNextConfig } from "open-next/types/open-next"; const config: OpenNextConfig = { default: { override: { - wrapper: "cloudflare", + wrapper: "cloudflare-node", converter: "edge", + // Unused implementation + incrementalCache: "dummy", + tagCache: "dummy", + queue: "dummy", }, }, middleware: { external: true, override: { - wrapper: "cloudflare", + wrapper: "cloudflare-edge", converter: "edge", + proxyExternalRequest: "fetch", }, }, - - dangerous: { - disableTagCache: true, - disableIncrementalCache: true, - }, }; export default config; ``` -You can enable Incremental Static Regeneration ([ISR](https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration)) by adding a KV binding named `NEXT_CACHE_WORKERS_KV` to your `wrangler.toml`: - -- Create the binding - - ```bash - npx wrangler kv namespace create NEXT_CACHE_WORKERS_KV - # or - pnpm wrangler kv namespace create NEXT_CACHE_WORKERS_KV - # or - yarn wrangler kv namespace create NEXT_CACHE_WORKERS_KV - # or - bun wrangler kv namespace create NEXT_CACHE_WORKERS_KV - ``` - -- Paste the snippet to your `wrangler.toml`: - - ```bash - [[kv_namespaces]] - binding = "NEXT_CACHE_WORKERS_KV" - id = "..." - ``` +## Known issues -> [!WARNING] -> The current support for ISR is limited. +- Next cache is not supported in the experimental branch yet +- `▲ [WARNING] Suspicious assignment to defined constant "process.env.NODE_ENV" [assign-to-define]` can safely be ignored +- You should test with cache disabled in the developer tools +- Maybe more, still experimental... ## Local development diff --git a/packages/cloudflare/env.d.ts b/packages/cloudflare/env.d.ts index b5e1c84..81bfb42 100644 --- a/packages/cloudflare/env.d.ts +++ b/packages/cloudflare/env.d.ts @@ -6,9 +6,13 @@ declare global { SKIP_NEXT_APP_BUILD?: string; NEXT_PRIVATE_DEBUG_CACHE?: string; __OPENNEXT_KV_BINDING_NAME: string; - [key: string]: string | Fetcher; + OPEN_NEXT_ORIGIN: string; } } + + interface Window { + [key: string]: string | Fetcher; + } } export {}; diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index ad02e1b..6e831ee 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -59,7 +59,7 @@ "vitest": "catalog:" }, "dependencies": { - "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@5c0e121", + "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@2202f36", "ts-morph": "catalog:" }, "peerDependencies": { diff --git a/packages/cloudflare/src/cli/build/build-worker.ts b/packages/cloudflare/src/cli/build/bundle-server.ts similarity index 66% rename from packages/cloudflare/src/cli/build/build-worker.ts rename to packages/cloudflare/src/cli/build/bundle-server.ts index 739c0a7..514e0a6 100644 --- a/packages/cloudflare/src/cli/build/build-worker.ts +++ b/packages/cloudflare/src/cli/build/bundle-server.ts @@ -1,8 +1,9 @@ -import { readFileSync } from "node:fs"; +import fs from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; +import path from "node:path"; import { fileURLToPath } from "node:url"; +import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; import { build, Plugin } from "esbuild"; import { Config } from "../config"; @@ -20,37 +21,37 @@ import { patchWranglerDeps } from "./patches/to-investigate/wrangler-deps"; import { copyPrerenderedRoutes } from "./utils"; /** The dist directory of the Cloudflare adapter package */ -const packageDistDir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); /** - * Using the Next.js build output in the `.next` directory builds a workerd compatible output - * - * @param outputDir the directory where to save the output - * @param config + * Bundle the Open Next server. */ -export async function buildWorker(config: Config): Promise { +export async function bundleServer(config: Config, openNextOptions: BuildOptions): Promise { // Copy over prerendered assets (e.g. SSG routes) copyPrerenderedRoutes(config); - copyPackageCliFiles(packageDistDir, config); - - const workerEntrypoint = join(config.paths.internal.templates, "worker.ts"); - const workerOutputFile = join(config.paths.output.root, "index.mjs"); + copyPackageCliFiles(packageDistDir, config, openNextOptions); const nextConfigStr = - readFileSync(join(config.paths.output.standaloneApp, "/server.js"), "utf8")?.match( - /const nextConfig = ({.+?})\n/ - )?.[1] ?? {}; + fs + .readFileSync(path.join(config.paths.output.standaloneApp, "server.js"), "utf8") + ?.match(/const nextConfig = ({.+?})\n/)?.[1] ?? {}; - console.log(`\x1b[35m⚙️ Bundling the worker file...\n\x1b[0m`); + console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`); patchWranglerDeps(config); updateWebpackChunksFile(config); + const { appBuildOutputPath, appPath, outputDir, monorepoRoot } = openNextOptions; + const outputPath = path.join(outputDir, "server-functions", "default"); + const packagePath = path.relative(monorepoRoot, appBuildOutputPath); + const openNextServer = path.join(outputPath, packagePath, `index.mjs`); + const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`); + await build({ - entryPoints: [workerEntrypoint], + entryPoints: [openNextServer], bundle: true, - outfile: workerOutputFile, + outfile: openNextServerBundle, format: "esm", target: "esnext", minify: false, @@ -60,15 +61,15 @@ export async function buildWorker(config: Config): Promise { // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s: // eval("require")("bufferutil"); // eval("require")("utf-8-validate"); - "next/dist/compiled/ws": join(config.paths.internal.templates, "shims", "empty.ts"), + "next/dist/compiled/ws": path.join(config.paths.internal.templates, "shims", "empty.ts"), // Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`: // eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext)); // which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63 // QUESTION: Why did I encountered this but mhart didn't? - "next/dist/compiled/edge-runtime": join(config.paths.internal.templates, "shims", "empty.ts"), + "next/dist/compiled/edge-runtime": path.join(config.paths.internal.templates, "shims", "empty.ts"), // `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here // source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env - "@next/env": join(config.paths.internal.templates, "shims", "env.ts"), + "@next/env": path.join(config.paths.internal.templates, "shims", "env.ts"), }, define: { // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139 @@ -87,14 +88,11 @@ export async function buildWorker(config: Config): Promise { platform: "node", banner: { js: ` - ${ - /* - `__dirname` is used by unbundled js files (which don't inherit the `__dirname` present in the `define` field) - so we also need to set it on the global scope - Note: this was hit in the `next/dist/compiled/@opentelemetry/api` module - */ "" - } - globalThis.__dirname ??= ""; +// __dirname is used by unbundled js files (which don't inherit the __dirname present in the define field) +// so we also need to set it on the global scope +// Note: this was hit in the next/dist/compiled/@opentelemetry/api module + +globalThis.__dirname ??= ""; // Do not crash on cache not supported // https://github.com/cloudflare/workerd/pull/2434 @@ -106,7 +104,7 @@ globalThis.fetch = (input, init) => { } return curFetch(input, init); }; -import { Readable } from 'node:stream'; +import __cf_stream from 'node:stream'; fetch = globalThis.fetch; const CustomRequest = class extends globalThis.Request { constructor(input, init) { @@ -114,7 +112,7 @@ const CustomRequest = class extends globalThis.Request { delete init.cache; if (init.body?.__node_stream__ === true) { // https://github.com/cloudflare/workerd/issues/2746 - init.body = Readable.toWeb(init.body); + init.body = __cf_stream.Readable.toWeb(init.body); } } super(input, init); @@ -128,9 +126,18 @@ globalThis.__dangerous_ON_edge_converter_returns_request = true; }, }); - await updateWorkerBundledCode(workerOutputFile, config); + await updateWorkerBundledCode(openNextServerBundle, config, openNextOptions); + + const isMonorepo = monorepoRoot !== appPath; + if (isMonorepo) { + const packagePosixPath = packagePath.split(path.sep).join(path.posix.sep); + fs.writeFileSync( + path.join(outputPath, "handler.mjs"), + `export * from "./${packagePosixPath}/handler.mjs";` + ); + } - console.log(`\x1b[35mWorker saved in \`${workerOutputFile}\` 🚀\n\x1b[0m`); + console.log(`\x1b[35mWorker saved in \`${openNextServerBundle}\` 🚀\n\x1b[0m`); } /** @@ -141,7 +148,11 @@ globalThis.__dangerous_ON_edge_converter_returns_request = true; * @param workerOutputFile * @param config */ -async function updateWorkerBundledCode(workerOutputFile: string, config: Config): Promise { +async function updateWorkerBundledCode( + workerOutputFile: string, + config: Config, + openNextOptions: BuildOptions +): Promise { const originalCode = await readFile(workerOutputFile, "utf8"); let patchedCode = originalCode; @@ -151,10 +162,15 @@ async function updateWorkerBundledCode(workerOutputFile: string, config: Config) patchedCode = inlineNextRequire(patchedCode, config); patchedCode = patchFindDir(patchedCode, config); patchedCode = inlineEvalManifest(patchedCode, config); - patchedCode = await patchCache(patchedCode, config); + patchedCode = await patchCache(patchedCode, openNextOptions); patchedCode = inlineMiddlewareManifestRequire(patchedCode, config); patchedCode = patchExceptionBubbling(patchedCode); + patchedCode = patchedCode + // workers do not support dynamic require nor require.resolve + .replace("patchAsyncStorage();", "//patchAsyncStorage();") + .replace('require.resolve("./cache.cjs")', '"unused"'); + await writeFile(workerOutputFile, patchedCode); } @@ -164,10 +180,10 @@ function createFixRequiresESBuildPlugin(config: Config): Plugin { setup(build) { // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires build.onResolve({ filter: /^\.\/require-hook$/ }, () => ({ - path: join(config.paths.internal.templates, "shims", "empty.ts"), + path: path.join(config.paths.internal.templates, "shims", "empty.ts"), })); build.onResolve({ filter: /\.\/lib\/node-fs-methods$/ }, () => ({ - path: join(config.paths.internal.templates, "shims", "empty.ts"), + path: path.join(config.paths.internal.templates, "shims", "empty.ts"), })); }, }; diff --git a/packages/cloudflare/src/cli/build/index.ts b/packages/cloudflare/src/cli/build/index.ts index ab5b550..6766391 100644 --- a/packages/cloudflare/src/cli/build/index.ts +++ b/packages/cloudflare/src/cli/build/index.ts @@ -3,6 +3,7 @@ import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js"; +import { compileCache } from "@opennextjs/aws/build/compileCache.js"; import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js"; import { createStaticAssets } from "@opennextjs/aws/build/createAssets.js"; import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js"; @@ -13,7 +14,8 @@ import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js"; import type { ProjectOptions } from "../config"; import { containsDotNextDir, getConfig } from "../config"; -import { buildWorker } from "./build-worker"; +import { bundleServer } from "./bundle-server"; +import { createServerBundle } from "./open-next/createServerBundle"; /** * Builds the application in a format that can be passed to workerd @@ -62,17 +64,24 @@ export async function build(projectOpts: ProjectOptions): Promise { printHeader("Generating bundle"); buildHelper.initOutputDir(options); + // Compile cache.ts + compileCache(options); + // Compile middleware await createMiddleware(options, { forceOnlyBuildOnce: true }); createStaticAssets(options); + await createServerBundle(options); + + // TODO: drop this copy. // Copy the .next directory to the output directory so it can be mutated. cpSync(join(projectOpts.sourceDir, ".next"), join(projectOpts.outputDir, ".next"), { recursive: true }); const projConfig = getConfig(projectOpts); - await buildWorker(projConfig); + // TODO: rely on options only. + await bundleServer(projConfig, options); logger.info("OpenNext build complete."); } @@ -84,22 +93,41 @@ export async function build(projectOpts: ProjectOptions): Promise { */ function ensureCloudflareConfig(config: OpenNextConfig) { const requirements = { - isExternal: config.middleware?.external == true, - useCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare", - useEdgeConverter: config.middleware?.override?.converter === "edge", + dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node", + dftUseEdgeConverter: config.default?.override?.converter === "edge", + dftUseDummyCache: + config.default?.override?.incrementalCache === "dummy" && + config.default?.override?.tagCache === "dummy" && + config.default?.override?.queue === "dummy", disableCacheInterception: config.dangerous?.enableCacheInterception !== true, + mwIsMiddlewareExternal: config.middleware?.external == true, + mwUseCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare-edge", + mwUseEdgeConverter: config.middleware?.override?.converter === "edge", + mwUseFetchProxy: config.middleware?.override?.proxyExternalRequest === "fetch", }; if (Object.values(requirements).some((satisfied) => !satisfied)) { throw new Error(`open-next.config.ts should contain: { - "middleware": { - "external": true, - "override": { - "wrapper": "cloudflare", - "converter": "edge" - } + default: { + override: { + wrapper: "cloudflare-node", + converter: "edge", + incrementalCache: "dummy", + tagCache: "dummy", + queue: "dummy", + }, }, + + middleware: { + external: true, + override: { + wrapper: "cloudflare-edge", + converter: "edge", + proxyExternalRequest: "fetch", + }, + }, + "dangerous": { "enableCacheInterception": false } diff --git a/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts b/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts new file mode 100644 index 0000000..7800512 --- /dev/null +++ b/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts @@ -0,0 +1,290 @@ +// Copy-Edit of @opennextjs/aws packages/open-next/src/build/createServerBundle.ts +// Adapted for cloudflare workers + +import fs from "node:fs"; +import path from "node:path"; + +import { bundleNextServer } from "@opennextjs/aws/build/bundleNextServer.js"; +import { compileCache } from "@opennextjs/aws/build/compileCache.js"; +import { copyTracedFiles } from "@opennextjs/aws/build/copyTracedFiles.js"; +import { generateEdgeBundle } from "@opennextjs/aws/build/edge/createEdgeBundle.js"; +import * as buildHelper from "@opennextjs/aws/build/helper.js"; +import { installDependencies } from "@opennextjs/aws/build/installDeps.js"; +import logger from "@opennextjs/aws/logger.js"; +import { minifyAll } from "@opennextjs/aws/minimize-js.js"; +import { openNextEdgePlugins } from "@opennextjs/aws/plugins/edge.js"; +import { openNextReplacementPlugin } from "@opennextjs/aws/plugins/replacement.js"; +import { openNextResolvePlugin } from "@opennextjs/aws/plugins/resolve.js"; +import type { FunctionOptions, SplittedFunctionOptions } from "@opennextjs/aws/types/open-next.js"; + +export async function createServerBundle(options: buildHelper.BuildOptions) { + const { config } = options; + const foundRoutes = new Set(); + // Get all functions to build + const defaultFn = config.default; + const functions = Object.entries(config.functions ?? {}); + + // Recompile cache.ts as ESM if any function is using Deno runtime + if (defaultFn.runtime === "deno" || functions.some(([, fn]) => fn.runtime === "deno")) { + compileCache(options, "esm"); + } + + const promises = functions.map(async ([name, fnOptions]) => { + const routes = fnOptions.routes; + routes.forEach((route) => foundRoutes.add(route)); + if (fnOptions.runtime === "edge") { + await generateEdgeBundle(name, options, fnOptions); + } else { + await generateBundle(name, options, fnOptions); + } + }); + + //TODO: throw an error if not all edge runtime routes has been bundled in a separate function + + // We build every other function than default before so we know which route there is left + await Promise.all(promises); + + const remainingRoutes = new Set(); + + const { appBuildOutputPath, monorepoRoot } = options; + + const packagePath = path.relative(monorepoRoot, appBuildOutputPath); + + // Find remaining routes + const serverPath = path.join(appBuildOutputPath, ".next", "standalone", packagePath, ".next", "server"); + + // Find app dir routes + if (fs.existsSync(path.join(serverPath, "app"))) { + const appPath = path.join(serverPath, "app"); + buildHelper.traverseFiles( + appPath, + ({ relativePath }) => relativePath.endsWith("page.js") || relativePath.endsWith("route.js"), + ({ relativePath }) => { + const route = `app/${relativePath.replace(/\.js$/, "")}`; + if (!foundRoutes.has(route)) { + remainingRoutes.add(route); + } + } + ); + } + + // Find pages dir routes + if (fs.existsSync(path.join(serverPath, "pages"))) { + const pagePath = path.join(serverPath, "pages"); + buildHelper.traverseFiles( + pagePath, + ({ relativePath }) => relativePath.endsWith(".js"), + ({ relativePath }) => { + const route = `pages/${relativePath.replace(/\.js$/, "")}`; + if (!foundRoutes.has(route)) { + remainingRoutes.add(route); + } + } + ); + } + + // Generate default function + await generateBundle("default", options, { + ...defaultFn, + // @ts-expect-error - Those string are RouteTemplate + routes: Array.from(remainingRoutes), + patterns: ["*"], + }); +} + +async function generateBundle( + name: string, + options: buildHelper.BuildOptions, + fnOptions: SplittedFunctionOptions +) { + const { appPath, appBuildOutputPath, config, outputDir, monorepoRoot } = options; + logger.info(`Building server function: ${name}...`); + + // Create output folder + const outputPath = path.join(outputDir, "server-functions", name); + + // Resolve path to the Next.js app if inside the monorepo + // note: if user's app is inside a monorepo, standalone mode places + // `node_modules` inside `.next/standalone`, and others inside + // `.next/standalone/package/path` (ie. `.next`, `server.js`). + // We need to output the handler file inside the package path. + const packagePath = path.relative(monorepoRoot, appBuildOutputPath); + fs.mkdirSync(path.join(outputPath, packagePath), { recursive: true }); + + const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs"; + fs.copyFileSync( + path.join(options.buildDir, `cache.${ext}`), + path.join(outputPath, packagePath, "cache.cjs") + ); + + if (fnOptions.runtime === "deno") { + addDenoJson(outputPath, packagePath); + } + + // Bundle next server if necessary + const isBundled = fnOptions.experimentalBundledNextServer ?? false; + if (isBundled) { + await bundleNextServer(path.join(outputPath, packagePath), appPath, { + debug: options.debug, + }); + } + + // Copy middleware + if (!config.middleware?.external) { + fs.copyFileSync( + path.join(options.buildDir, "middleware.mjs"), + path.join(outputPath, packagePath, "middleware.mjs") + ); + } + + // Copy open-next.config.mjs + buildHelper.copyOpenNextConfig(options.buildDir, path.join(outputPath, packagePath), true); + + // Copy env files + buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); + + // Copy all necessary traced files + await copyTracedFiles( + appBuildOutputPath, + packagePath, + outputPath, + fnOptions.routes ?? ["app/page.tsx"], + isBundled + ); + + // Build Lambda code + // note: bundle in OpenNext package b/c the adapter relies on the + // "serverless-http" package which is not a dependency in user's + // Next.js app. + + const disableNextPrebundledReact = + buildHelper.compareSemver(options.nextVersion, "13.5.1") >= 0 || + buildHelper.compareSemver(options.nextVersion, "13.4.1") <= 0; + + const overrides = fnOptions.override ?? {}; + + const isBefore13413 = buildHelper.compareSemver(options.nextVersion, "13.4.13") <= 0; + const isAfter141 = buildHelper.compareSemver(options.nextVersion, "14.0.4") >= 0; + + const disableRouting = isBefore13413 || config.middleware?.external; + + const plugins = [ + openNextReplacementPlugin({ + name: `requestHandlerOverride ${name}`, + target: /core(\/|\\)requestHandler\.js/g, + deletes: [ + ...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []), + ...(disableRouting ? ["withRouting"] : []), + ], + }), + openNextReplacementPlugin({ + name: `utilOverride ${name}`, + target: /core(\/|\\)util\.js/g, + deletes: [ + ...(disableNextPrebundledReact ? ["requireHooks"] : []), + ...(disableRouting ? ["trustHostHeader"] : []), + ...(!isBefore13413 ? ["requestHandlerHost"] : []), + ...(isAfter141 ? ["experimentalIncrementalCacheHandler"] : ["stableIncrementalCache"]), + ], + }), + + openNextResolvePlugin({ + fnName: name, + overrides, + }), + + openNextEdgePlugins({ + nextDir: path.join(options.appBuildOutputPath, ".next"), + edgeFunctionHandlerPath: path.join(options.openNextDistDir, "core", "edgeFunctionHandler.js"), + isInCloudfare: true, + }), + ]; + + const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; + await buildHelper.esbuildAsync( + { + entryPoints: [path.join(options.openNextDistDir, "adapters", "server-adapter.js")], + outfile: path.join(outputPath, packagePath, `index.${outfileExt}`), + banner: { + js: [ + `globalThis.monorepoPackagePath = "${packagePath}";`, + name === "default" ? "" : `globalThis.fnName = "${name}";`, + ].join(""), + }, + plugins, + alias: { + ...(isBundled + ? { + "next/dist/server/next-server.js": "./next-server.runtime.prod.js", + } + : {}), + }, + }, + options + ); + + const isMonorepo = monorepoRoot !== appPath; + if (isMonorepo) { + addMonorepoEntrypoint(outputPath, packagePath); + } + + installDependencies(outputPath, fnOptions.install); + + if (fnOptions.minify) { + await minifyServerBundle(outputPath); + } + + const shouldGenerateDocker = shouldGenerateDockerfile(fnOptions); + if (shouldGenerateDocker) { + fs.writeFileSync( + path.join(outputPath, "Dockerfile"), + typeof shouldGenerateDocker === "string" + ? shouldGenerateDocker + : ` +FROM node:18-alpine +WORKDIR /app +COPY . /app +EXPOSE 3000 +CMD ["node", "index.mjs"] + ` + ); + } +} + +function shouldGenerateDockerfile(options: FunctionOptions) { + return options.override?.generateDockerfile ?? false; +} + +// Add deno.json file to enable "bring your own node_modules" mode. +// TODO: this won't be necessary in Deno 2. See https://github.com/denoland/deno/issues/23151 +function addDenoJson(outputPath: string, packagePath: string) { + const config = { + // Enable "bring your own node_modules" mode + // and allow `__proto__` + unstable: ["byonm", "fs", "unsafe-proto"], + }; + fs.writeFileSync(path.join(outputPath, packagePath, "deno.json"), JSON.stringify(config, null, 2)); +} + +//TODO: check if this PR is still necessary https://github.com/opennextjs/opennextjs-aws/pull/341 +function addMonorepoEntrypoint(outputPath: string, packagePath: string) { + // Note: in the monorepo case, the handler file is output to + // `.next/standalone/package/path/index.mjs`, but we want + // the Lambda function to be able to find the handler at + // the root of the bundle. We will create a dummy `index.mjs` + // that re-exports the real handler. + + // TOOD: use helper + // Always use posix path for import path + const packagePosixPath = packagePath.split(path.sep).join(path.posix.sep); + fs.writeFileSync(path.join(outputPath, "index.mjs"), `export * from "./${packagePosixPath}/index.mjs";`); +} + +async function minifyServerBundle(outputDir: string) { + logger.info("Minimizing server function..."); + + await minifyAll(outputDir, { + compress_json: true, + mangle: true, + }); +} diff --git a/packages/cloudflare/src/cli/build/patches/investigated/copy-package-cli-files.ts b/packages/cloudflare/src/cli/build/patches/investigated/copy-package-cli-files.ts index fb31c64..c445724 100644 --- a/packages/cloudflare/src/cli/build/patches/investigated/copy-package-cli-files.ts +++ b/packages/cloudflare/src/cli/build/patches/investigated/copy-package-cli-files.ts @@ -1,15 +1,22 @@ -import { cpSync } from "node:fs"; -import { join } from "node:path"; +import fs from "node:fs"; +import path from "node:path"; + +import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; import { Config } from "../../../config"; /** * Copies the template files present in the cloudflare adapter package into the standalone node_modules folder */ -export function copyPackageCliFiles(packageDistDir: string, config: Config) { +export function copyPackageCliFiles(packageDistDir: string, config: Config, openNextConfig: BuildOptions) { console.log("# copyPackageTemplateFiles"); - const sourceDir = join(packageDistDir, "cli"); - const destinationDir = join(config.paths.internal.package, "cli"); + const sourceDir = path.join(packageDistDir, "cli"); + const destinationDir = path.join(config.paths.internal.package, "cli"); + + fs.cpSync(sourceDir, destinationDir, { recursive: true }); - cpSync(sourceDir, destinationDir, { recursive: true }); + fs.copyFileSync( + path.join(packageDistDir, "cli", "templates", "worker.ts"), + path.join(openNextConfig.outputDir, "worker.ts") + ); } diff --git a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts index ef37a81..f71db93 100644 --- a/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts +++ b/packages/cloudflare/src/cli/build/patches/investigated/patch-cache.ts @@ -1,8 +1,6 @@ -import { join } from "node:path"; +import path from "node:path"; -import { build } from "esbuild"; - -import { Config } from "../../../config"; +import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; /** * Sets up the OpenNext cache handler in a Next.js build. @@ -15,29 +13,20 @@ import { Config } from "../../../config"; * build-time. Therefore, we have to manually override the default way that the cache handler is * instantiated with a dynamic require that uses a string literal for the path. */ -export async function patchCache(code: string, config: Config): Promise { +export async function patchCache(code: string, openNextOptions: BuildOptions): Promise { console.log("# patchCache"); - const cacheHandlerFileName = "cache-handler.mjs"; - const cacheHandlerEntrypoint = join(config.paths.internal.templates, "cache-handler", "index.ts"); - const cacheHandlerOutputFile = join(config.paths.output.root, cacheHandlerFileName); + const { appBuildOutputPath, outputDir, monorepoRoot } = openNextOptions; - await build({ - entryPoints: [cacheHandlerEntrypoint], - bundle: true, - outfile: cacheHandlerOutputFile, - format: "esm", - target: "esnext", - minify: config.build.shouldMinify, - define: { - "process.env.__OPENNEXT_KV_BINDING_NAME": `"${config.cache.kvBindingName}"`, - }, - }); + // TODO: switch to cache.mjs + const outputPath = path.join(outputDir, "server-functions", "default"); + const packagePath = path.relative(monorepoRoot, appBuildOutputPath); + const cacheFile = path.join(outputPath, packagePath, "cache.cjs"); const patchedCode = code.replace( "const { cacheHandler } = this.nextConfig;", `const cacheHandler = null; -CacheHandler = (await import('./${cacheHandlerFileName}')).OpenNextCacheHandler; +CacheHandler = require('${cacheFile}').default; ` ); diff --git a/packages/cloudflare/src/cli/templates/cache-handler/index.ts b/packages/cloudflare/src/cli/templates/cache-handler/index.ts deleted file mode 100644 index 46fc26d..0000000 --- a/packages/cloudflare/src/cli/templates/cache-handler/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./open-next-cache-handler"; diff --git a/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts b/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts deleted file mode 100644 index 0f7de47..0000000 --- a/packages/cloudflare/src/cli/templates/cache-handler/open-next-cache-handler.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type { KVNamespace } from "@cloudflare/workers-types"; -import type { - CacheHandler, - CacheHandlerContext, - CacheHandlerValue, -} from "next/dist/server/lib/incremental-cache"; -import type { IncrementalCacheValue } from "next/dist/server/response-cache"; - -import { - NEXT_BODY_SUFFIX, - NEXT_DATA_SUFFIX, - NEXT_HTML_SUFFIX, - RSC_PREFETCH_SUFFIX, - RSC_SUFFIX, - SEED_DATA_DIR, -} from "../../constants/incremental-cache"; -import { getSeedBodyFile, getSeedMetaFile, getSeedTextFile, parseCtx } from "./utils"; - -type CacheEntry = { - lastModified: number; - value: IncrementalCacheValue | null; -}; - -export class OpenNextCacheHandler implements CacheHandler { - protected kv: KVNamespace | undefined; - - protected debug: boolean = !!process.env.NEXT_PRIVATE_DEBUG_CACHE; - - constructor(protected ctx: CacheHandlerContext) { - this.kv = process.env[process.env.__OPENNEXT_KV_BINDING_NAME] as KVNamespace | undefined; - } - - async get(...args: Parameters): Promise { - const [key, _ctx] = args; - const ctx = parseCtx(_ctx); - - if (this.debug) console.log(`cache - get: ${key}, ${ctx?.kind}`); - - if (this.kv !== undefined) { - try { - const value = await this.kv.get(key, "json"); - if (value) return value; - } catch (e) { - console.error(`Failed to get value for key = ${key}: ${e}`); - } - } - - // Check for seed data from the file-system. - - // we don't check for seed data for fetch or image cache entries - if (ctx?.kind === "FETCH" || ctx?.kind === "IMAGE") return null; - - const seedKey = `http://assets.local/${SEED_DATA_DIR}/${key}`.replace(/\/\//g, "/"); - - if (ctx?.kind === "APP" || ctx?.kind === "APP_ROUTE") { - const fallbackBody = await getSeedBodyFile(seedKey, NEXT_BODY_SUFFIX); - if (fallbackBody) { - const meta = await getSeedMetaFile(seedKey); - return { - lastModified: meta?.lastModified, - value: { - kind: (ctx.kind === "APP_ROUTE" ? ctx.kind : "ROUTE") as Extract< - IncrementalCacheValue["kind"], - "ROUTE" - >, - body: fallbackBody, - status: meta?.status ?? 200, - headers: meta?.headers ?? {}, - }, - }; - } - - if (ctx.kind === "APP_ROUTE") { - return null; - } - } - - const seedHtml = await getSeedTextFile(seedKey, NEXT_HTML_SUFFIX); - if (!seedHtml) return null; // we're only checking for prerendered routes at the moment - - if (ctx?.kind === "PAGES" || ctx?.kind === "APP" || ctx?.kind === "APP_PAGE") { - const metaPromise = getSeedMetaFile(seedKey); - - let pageDataPromise: Promise = Promise.resolve(undefined); - if (!ctx.isFallback) { - const rscSuffix = ctx.isRoutePPREnabled ? RSC_PREFETCH_SUFFIX : RSC_SUFFIX; - - if (ctx.kind === "APP_PAGE") { - pageDataPromise = getSeedBodyFile(seedKey, rscSuffix); - } else { - pageDataPromise = getSeedTextFile(seedKey, ctx.kind === "APP" ? rscSuffix : NEXT_DATA_SUFFIX); - } - } - - const [meta, pageData] = await Promise.all([metaPromise, pageDataPromise]); - - return { - lastModified: meta?.lastModified, - value: { - kind: (ctx.kind === "APP_PAGE" ? "APP_PAGE" : "PAGE") as Extract< - IncrementalCacheValue["kind"], - "PAGE" - >, - html: seedHtml, - pageData: pageData ?? "", - ...(ctx.kind === "APP_PAGE" && { rscData: pageData }), - postponed: meta?.postponed, - status: meta?.status, - headers: meta?.headers, - }, - }; - } - - return null; - } - - async set(...args: Parameters) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [key, entry, _ctx] = args; - - if (this.kv === undefined) { - return; - } - - if (this.debug) console.log(`cache - set: ${key}`); - - const data: CacheEntry = { - lastModified: Date.now(), - value: entry, - }; - - try { - await this.kv.put(key, JSON.stringify(data)); - } catch (e) { - console.error(`Failed to set value for key = ${key}: ${e}`); - } - } - - async revalidateTag(...args: Parameters) { - const [tags] = args; - if (this.kv === undefined) { - return; - } - - if (this.debug) console.log(`cache - revalidateTag: ${JSON.stringify([tags].flat())}`); - } - - resetRequestCache(): void {} -} diff --git a/packages/cloudflare/src/cli/templates/cache-handler/utils.ts b/packages/cloudflare/src/cli/templates/cache-handler/utils.ts deleted file mode 100644 index d17084b..0000000 --- a/packages/cloudflare/src/cli/templates/cache-handler/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { IncrementalCache } from "next/dist/server/lib/incremental-cache"; - -import { NEXT_META_SUFFIX } from "../../constants/incremental-cache"; - -type PrerenderedRouteMeta = { - lastModified: number; - status?: number; - headers?: Record; - postponed?: string; -}; - -type EntryKind = - | "APP" // .body, .html - backwards compat - | "PAGES" - | "FETCH" - | "APP_ROUTE" // .body - | "APP_PAGE" // .html - | "IMAGE" - | undefined; - -async function getAsset(key: string, cb: (resp: Response) => T): Promise | undefined> { - const resp = await process.env.ASSETS.fetch(key); - return resp.status === 200 ? await cb(resp) : undefined; -} - -export function getSeedBodyFile(key: string, suffix: string) { - return getAsset(key + suffix, (resp) => resp.arrayBuffer() as Promise); -} - -export function getSeedTextFile(key: string, suffix: string) { - return getAsset(key + suffix, (resp) => resp.text()); -} - -export function getSeedMetaFile(key: string) { - return getAsset(key + NEXT_META_SUFFIX, (resp) => resp.json()); -} - -export function parseCtx(ctx: Parameters[1] = {}) { - return { ...ctx, kind: ctx?.kindHint?.toUpperCase() } as - | (typeof ctx & { kind?: EntryKind; isFallback?: boolean; isRoutePPREnabled?: boolean }) - | undefined; -} diff --git a/packages/cloudflare/src/cli/templates/worker.ts b/packages/cloudflare/src/cli/templates/worker.ts index 3bc66b2..a23da2e 100644 --- a/packages/cloudflare/src/cli/templates/worker.ts +++ b/packages/cloudflare/src/cli/templates/worker.ts @@ -1,17 +1,10 @@ import { AsyncLocalStorage } from "node:async_hooks"; -import type { IncomingMessage } from "node:http"; -import Stream from "node:stream"; - -import type { NextConfig } from "next"; -import { NodeNextRequest, NodeNextResponse } from "next/dist/server/base-http/node"; -import { MockedResponse } from "next/dist/server/lib/mock-request"; -import type { NodeRequestHandler } from "next/dist/server/next-server"; import type { CloudflareContext } from "../../api"; // @ts-expect-error: resolved by wrangler build import { handler as middlewareHandler } from "./middleware/handler.mjs"; - -const NON_BODY_RESPONSES = new Set([101, 204, 205, 304]); +// @ts-expect-error: resolved by wrangler build +import { handler as serverHandler } from "./server-functions/default/handler.mjs"; const cloudflareContextALS = new AsyncLocalStorage(); @@ -28,14 +21,19 @@ const cloudflareContextALS = new AsyncLocalStorage(); } ); -// Injected at build time -const nextConfig: NextConfig = JSON.parse(process.env.__NEXT_PRIVATE_STANDALONE_CONFIG ?? "{}"); - -let requestHandler: NodeRequestHandler | null = null; - export default { async fetch(request, env, ctx) { return cloudflareContextALS.run({ env, ctx, cf: request.cf }, async () => { + // Set the default Origin for the origin resolver. + const url = new URL(request.url); + process.env.OPEN_NEXT_ORIGIN = JSON.stringify({ + default: { + host: url.hostname, + protocol: url.protocol.slice(0, -1), + port: url.port, + }, + }); + // The Middleware handler can return either a `Response` or a `Request`: // - `Response`s should be returned early // - `Request`s are handled by the Next server @@ -45,115 +43,7 @@ export default { return reqOrResp; } - request = reqOrResp; - - if (requestHandler == null) { - globalThis.process.env = { ...globalThis.process.env, ...env }; - // Note: "next/dist/server/next-server" is a cjs module so we have to `require` it not to confuse esbuild - // (since esbuild can run in projects with different module resolutions) - // eslint-disable-next-line @typescript-eslint/no-require-imports - const NextNodeServer = require("next/dist/server/next-server") - .default as typeof import("next/dist/server/next-server").default; - - requestHandler = new NextNodeServer({ - conf: nextConfig, - customServer: false, - dev: false, - dir: "", - minimalMode: false, - }).getRequestHandler(); - } - - const { req, res, webResponse } = getWrappedStreams(request, ctx); - - ctx.waitUntil(Promise.resolve(requestHandler(new NodeNextRequest(req), new NodeNextResponse(res)))); - - return await webResponse(); + return serverHandler(reqOrResp, env, ctx); }); }, } as ExportedHandler<{ ASSETS: Fetcher }>; - -function getWrappedStreams(request: Request, ctx: ExecutionContext) { - const url = new URL(request.url); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const reqBody = request.body && Stream.Readable.fromWeb(request.body as any); - const req = (reqBody ?? Stream.Readable.from([])) as IncomingMessage; - req.httpVersion = "1.0"; - req.httpVersionMajor = 1; - req.httpVersionMinor = 0; - req.url = url.href.slice(url.origin.length); - req.headers = Object.fromEntries([...request.headers]); - req.method = request.method; - Object.defineProperty(req, "__node_stream__", { - value: true, - writable: false, - }); - Object.defineProperty(req, "headersDistinct", { - get() { - const headers: Record = {}; - for (const [key, value] of Object.entries(req.headers)) { - if (!value) continue; - headers[key] = Array.isArray(value) ? value : [value]; - } - return headers; - }, - }); - - const { readable, writable } = new IdentityTransformStream(); - const resBodyWriter = writable.getWriter(); - - const res = new MockedResponse({ - resWriter: (chunk) => { - resBodyWriter.write(typeof chunk === "string" ? Buffer.from(chunk) : chunk).catch((err) => { - if ( - err.message.includes("WritableStream has been closed") || - err.message.includes("Network connection lost") - ) { - // safe to ignore - return; - } - console.error("Error in resBodyWriter.write"); - console.error(err); - }); - return true; - }, - }); - - // It's implemented as a no-op, but really it should mark the headers as done - // eslint-disable-next-line @typescript-eslint/no-explicit-any - res.flushHeaders = () => (res as any).headPromiseResolve(); - - // Only allow statusCode to be modified if not sent - let { statusCode } = res; - Object.defineProperty(res, "statusCode", { - get: function () { - return statusCode; - }, - set: function (val) { - if (this.finished || this.headersSent) { - return; - } - statusCode = val; - }, - }); - - // Make sure the writer is eventually closed - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ctx.waitUntil((res as any).hasStreamed.finally(() => resBodyWriter.close().catch(() => {}))); - - return { - res, - req, - webResponse: async () => { - await res.headPromise; - // TODO: remove this once streaming with compression is working nicely - res.setHeader("content-encoding", "identity"); - return new Response(NON_BODY_RESPONSES.has(res.statusCode) ? null : readable, { - status: res.statusCode, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - headers: (res as any).headers, - }); - }, - }; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f439a53..f7618dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,8 +73,8 @@ catalogs: specifier: ^2.1.1 version: 2.1.1 wrangler: - specifier: ^3.78.10 - version: 3.80.4 + specifier: ^3.87.0 + version: 3.89.0 importers: @@ -131,7 +131,7 @@ importers: version: 22.2.0 wrangler: specifier: 'catalog:' - version: 3.80.4(@cloudflare/workers-types@4.20240925.0) + version: 3.89.0(@cloudflare/workers-types@4.20240925.0) examples/create-next-app: dependencies: @@ -177,7 +177,7 @@ importers: version: 5.5.4 wrangler: specifier: 'catalog:' - version: 3.80.4(@cloudflare/workers-types@4.20240925.0) + version: 3.89.0(@cloudflare/workers-types@4.20240925.0) examples/middleware: dependencies: @@ -214,7 +214,7 @@ importers: version: 5.5.4 wrangler: specifier: 'catalog:' - version: 3.80.4(@cloudflare/workers-types@4.20240925.0) + version: 3.89.0(@cloudflare/workers-types@4.20240925.0) examples/vercel-blog-starter: dependencies: @@ -269,7 +269,7 @@ importers: version: 5.5.4 wrangler: specifier: ^3.80.4 - version: 3.80.4(@cloudflare/workers-types@4.20240925.0) + version: 3.80.4 examples/vercel-commerce: dependencies: @@ -299,7 +299,7 @@ importers: version: 1.5.0(react-dom@19.0.0-rc-3208e73e-20240730(react@19.0.0-rc-3208e73e-20240730))(react@19.0.0-rc-3208e73e-20240730) wrangler: specifier: 'catalog:' - version: 3.80.4(@cloudflare/workers-types@4.20240925.0) + version: 3.89.0(@cloudflare/workers-types@4.20240925.0) devDependencies: '@opennextjs/cloudflare': specifier: workspace:* @@ -341,14 +341,14 @@ importers: packages/cloudflare: dependencies: '@opennextjs/aws': - specifier: https://pkg.pr.new/@opennextjs/aws@5c0e121 - version: https://pkg.pr.new/@opennextjs/aws@5c0e121 + specifier: https://pkg.pr.new/@opennextjs/aws@2202f36 + version: https://pkg.pr.new/@opennextjs/aws@2202f36 ts-morph: specifier: 'catalog:' version: 23.0.0 wrangler: specifier: 'catalog:' - version: 3.80.4(@cloudflare/workers-types@4.20240925.0) + version: 3.89.0(@cloudflare/workers-types@4.20240925.0) devDependencies: '@cloudflare/workers-types': specifier: 'catalog:' @@ -676,34 +676,68 @@ packages: cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20241106.1': + resolution: {integrity: sha512-zxvaToi1m0qzAScrxFt7UvFVqU8DxrCO2CinM1yQkv5no7pA1HolpIrwZ0xOhR3ny64Is2s/J6BrRjpO5dM9Zw==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20241004.0': resolution: {integrity: sha512-siD9fexv5lr2IpBczWV7OPgJvHj8/fJUrRAYCMcBURkfiwssK91coQeZlN1NdQ85aYELVgxDFoG+p86OS+ZzLw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20241106.1': + resolution: {integrity: sha512-j3dg/42D/bPgfNP3cRUBxF+4waCKO/5YKwXNj+lnVOwHxDu+ne5pFw9TIkKYcWTcwn0ZUkbNZNM5rhJqRn4xbg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20241004.0': resolution: {integrity: sha512-EtKGXO5fzRgX6UhDDLhjjEsB1QtliHb12zavZ/S0C8hKPz76II7MQ3Lls9kfB62fbdMP8L6vcqWPObEUcw6GSw==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20241106.1': + resolution: {integrity: sha512-Ih+Ye8E1DMBXcKrJktGfGztFqHKaX1CeByqshmTbODnWKHt6O65ax3oTecUwyC0+abuyraOpAtdhHNpFMhUkmw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20241004.0': resolution: {integrity: sha512-XO7VBE1YaFf/o9tKO1PqDqaxkU2eAR2DLX7R0+R8p+q92sUDXyoxo48T3yJDfxWndnKJ6hSJfvKanw3Mq9Tisw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20241106.1': + resolution: {integrity: sha512-mdQFPk4+14Yywn7n1xIzI+6olWM8Ybz10R7H3h+rk0XulMumCWUCy1CzIDauOx6GyIcSgKIibYMssVHZR30ObA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20241004.0': resolution: {integrity: sha512-o+TmCYGq58jNUDbG73xOvd648XvJ2TicI++2BBoySklJXG6f4But5AwA8TxQgmeujR3vpBjPZKexEzcZSUOTtA==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20241106.1': + resolution: {integrity: sha512-4rtcss31E/Rb/PeFocZfr+B9i1MdrkhsTBWizh8siNR4KMmkslU2xs2wPaH1z8+ErxkOsHrKRa5EPLh5rIiFeg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-shared@0.6.0': resolution: {integrity: sha512-rfUCvb3hx4AsvdUZsxgk9lmgEnQehqV3jdtXLP/Xr0+P56n11T/0nXNMzmn7Nnv+IJFOV6X9NmFhuMz4sBPw7w==} engines: {node: '>=16.7.0'} + '@cloudflare/workers-shared@0.7.1': + resolution: {integrity: sha512-46cP5FCrl3TrvHeoHLb5SRuiDMKH5kc9Yvo36SAfzt8dqJI/qJRoY1GP3ioHn/gP7v2QIoUOTAzIl7Ml7MnfrA==} + engines: {node: '>=16.7.0'} + '@cloudflare/workers-types@4.20240925.0': resolution: {integrity: sha512-KpqyRWvanEuXgBTKYFzRp4NsWOEcswxjsPRSre1zYQcODmc8PUrraVHQUmgvkJgv3FzB+vI9xm7J6oE4MmZHCA==} @@ -1715,9 +1749,9 @@ packages: '@octokit/types@13.6.1': resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==} - '@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@5c0e121': - resolution: {tarball: https://pkg.pr.new/@opennextjs/aws@5c0e121} - version: 3.2.1 + '@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@2202f36': + resolution: {tarball: https://pkg.pr.new/@opennextjs/aws@2202f36} + version: 3.2.2 hasBin: true '@pkgjs/parseargs@0.11.0': @@ -2483,6 +2517,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2599,6 +2637,9 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -3439,6 +3480,9 @@ packages: iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} + itty-time@1.0.6: + resolution: {integrity: sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==} + jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -3698,6 +3742,11 @@ packages: engines: {node: '>=16.13'} hasBin: true + miniflare@3.20241106.1: + resolution: {integrity: sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw==} + engines: {node: '>=16.13'} + hasBin: true + minimatch@10.0.1: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} @@ -4242,6 +4291,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -4796,6 +4849,9 @@ packages: unenv-nightly@2.0.0-20241009-125958-e8ea22f: resolution: {integrity: sha512-hRxmKz1iSVRmuFx/vBdPsx7rX4o7Cas9vdjDNeUeWpQTK2LzU3Xy3Jz0zbo7MJX0bpqo/LEFCA+GPwsbl6zKEQ==} + unenv-nightly@2.0.0-20241111-080453-894aa31: + resolution: {integrity: sha512-0W39QQOQ9VE8kVVUpGwEG+pZcsCXk5wqNG6rDPE6Gr+fiA69LR0qERM61hW5KCOkC1/ArCFrfCGjwHyyv/bI0Q==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -4962,6 +5018,11 @@ packages: engines: {node: '>=16'} hasBin: true + workerd@1.20241106.1: + resolution: {integrity: sha512-1GdKl0kDw8rrirr/ThcK66Kbl4/jd4h8uHx5g7YHBrnenY5SX1UPuop2cnCzYUxlg55kPjzIqqYslz1muRFgFw==} + engines: {node: '>=16'} + hasBin: true + wrangler@3.80.4: resolution: {integrity: sha512-DyNvShtVH3k7ZyBndlIiwyRDXqtHr3g01hxwn4FfwKlAaT6EL0wb3KL3UGbsdpeM/xbJiUQxFQ4WuFBWgZS18Q==} engines: {node: '>=16.17.0'} @@ -4972,6 +5033,16 @@ packages: '@cloudflare/workers-types': optional: true + wrangler@3.89.0: + resolution: {integrity: sha512-ix3Rir/cu9Cn6FklvPDIW1QwOMcRU8iPj3IrkBWGdB66K9z1uqyqoTP64UZZyXrBItfrU7SbQT4L5wJ5y10TPA==} + engines: {node: '>=16.17.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20241106.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5871,23 +5942,43 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20241004.0': optional: true + '@cloudflare/workerd-darwin-64@1.20241106.1': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20241004.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20241106.1': + optional: true + '@cloudflare/workerd-linux-64@1.20241004.0': optional: true + '@cloudflare/workerd-linux-64@1.20241106.1': + optional: true + '@cloudflare/workerd-linux-arm64@1.20241004.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20241106.1': + optional: true + '@cloudflare/workerd-windows-64@1.20241004.0': optional: true + '@cloudflare/workerd-windows-64@1.20241106.1': + optional: true + '@cloudflare/workers-shared@0.6.0': dependencies: mime: 3.0.0 zod: 3.23.8 + '@cloudflare/workers-shared@0.7.1': + dependencies: + mime: 3.0.0 + zod: 3.23.8 + '@cloudflare/workers-types@4.20240925.0': {} '@cspotcode/source-map-support@0.8.1': @@ -6603,7 +6694,7 @@ snapshots: dependencies: '@octokit/openapi-types': 22.2.0 - '@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@5c0e121': + '@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@2202f36': dependencies: '@aws-sdk/client-dynamodb': 3.682.0 '@aws-sdk/client-lambda': 3.682.0 @@ -7601,6 +7692,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + ci-info@3.9.0: {} ci-info@4.0.0: {} @@ -7705,6 +7800,8 @@ snapshots: date-fns@3.6.0: {} + date-fns@4.1.0: {} + debug@3.2.7: dependencies: ms: 2.1.2 @@ -8034,8 +8131,8 @@ snapshots: '@typescript-eslint/parser': 8.7.0(eslint@8.57.1)(typescript@5.5.4) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1) eslint-plugin-react: 7.36.1(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -8054,33 +8151,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.0 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.7.0(eslint@8.57.1)(typescript@5.5.4) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -8094,7 +8191,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -8105,7 +8202,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.7.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -8843,6 +8940,8 @@ snapshots: reflect.getprototypeof: 1.0.6 set-function-name: 2.0.2 + itty-time@1.0.6: {} + jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 @@ -9196,6 +9295,25 @@ snapshots: - supports-color - utf-8-validate + miniflare@3.20241106.1: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.12.1 + acorn-walk: 8.3.3 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.28.4 + workerd: 1.20241106.1 + ws: 8.18.0 + youch: 3.3.3 + zod: 3.23.8 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + minimatch@10.0.1: dependencies: brace-expansion: 2.0.1 @@ -9695,6 +9813,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.2: {} + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -10347,6 +10467,13 @@ snapshots: pathe: 1.1.2 ufo: 1.5.4 + unenv-nightly@2.0.0-20241111-080453-894aa31: + dependencies: + defu: 6.1.4 + ohash: 1.1.4 + pathe: 1.1.2 + ufo: 1.5.4 + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -10549,7 +10676,15 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20241004.0 '@cloudflare/workerd-windows-64': 1.20241004.0 - wrangler@3.80.4(@cloudflare/workers-types@4.20240925.0): + workerd@1.20241106.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20241106.1 + '@cloudflare/workerd-darwin-arm64': 1.20241106.1 + '@cloudflare/workerd-linux-64': 1.20241106.1 + '@cloudflare/workerd-linux-arm64': 1.20241106.1 + '@cloudflare/workerd-windows-64': 1.20241106.1 + + wrangler@3.80.4: dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@cloudflare/workers-shared': 0.6.0 @@ -10568,6 +10703,34 @@ snapshots: unenv: unenv-nightly@2.0.0-20241009-125958-e8ea22f workerd: 1.20241004.0 xxhash-wasm: 1.0.2 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + wrangler@3.89.0(@cloudflare/workers-types@4.20240925.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.3.4 + '@cloudflare/workers-shared': 0.7.1 + '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) + '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) + blake3-wasm: 2.1.5 + chokidar: 4.0.1 + date-fns: 4.1.0 + esbuild: 0.17.19 + itty-time: 1.0.6 + miniflare: 3.20241106.1 + nanoid: 3.3.7 + path-to-regexp: 6.3.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + selfsigned: 2.4.1 + source-map: 0.6.1 + unenv: unenv-nightly@2.0.0-20241111-080453-894aa31 + workerd: 1.20241106.1 + xxhash-wasm: 1.0.2 optionalDependencies: '@cloudflare/workers-types': 4.20240925.0 fsevents: 2.3.3