From 9e468a675ce2cd6bdee1a18dd8054d777c7f3a52 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 25 Jun 2026 08:42:04 +0200 Subject: [PATCH 01/24] wip --- .../ses_11f9e6737ffegwim4y24fFKT76.json | 10 + packages/cloudflare/package.json | 1 + packages/cloudflare/src/api/config.ts | 6 +- .../src/api/durable-objects/queue.ts | 6 +- .../src/api/overrides/asset-resolver/index.ts | 4 +- .../src/api/overrides/cache-purge/index.ts | 4 +- .../incremental-cache/kv-incremental-cache.ts | 6 +- .../incremental-cache/r2-incremental-cache.ts | 6 +- .../incremental-cache/regional-cache.ts | 4 +- .../static-assets-incremental-cache.ts | 6 +- .../cloudflare/src/api/overrides/internal.ts | 4 +- .../src/api/overrides/queue/do-queue.ts | 4 +- .../api/overrides/queue/memory-queue.spec.ts | 2 +- .../src/api/overrides/queue/memory-queue.ts | 6 +- .../api/overrides/queue/queue-cache.spec.ts | 2 +- .../src/api/overrides/queue/queue-cache.ts | 4 +- .../tag-cache/d1-next-tag-cache.spec.ts | 4 +- .../overrides/tag-cache/d1-next-tag-cache.ts | 4 +- .../tag-cache/do-sharded-tag-cache.ts | 8 +- .../tag-cache/kv-next-tag-cache.spec.ts | 4 +- .../overrides/tag-cache/kv-next-tag-cache.ts | 4 +- .../tag-cache/tag-cache-filter.spec.ts | 2 +- .../overrides/tag-cache/tag-cache-filter.ts | 2 +- packages/cloudflare/src/cli/adapter.ts | 22 +- packages/cloudflare/src/cli/build/build.ts | 10 +- .../cloudflare/src/cli/build/bundle-server.ts | 6 +- .../compile-cache-assets-manifest.ts | 4 +- .../cli/build/open-next/compile-env-files.ts | 2 +- .../src/cli/build/open-next/compile-images.ts | 2 +- .../src/cli/build/open-next/compile-init.ts | 2 +- .../open-next/compile-skew-protection.ts | 2 +- .../build/open-next/compileDurableObjects.ts | 4 +- .../cli/build/open-next/createServerBundle.ts | 38 +- .../ast/patch-vercel-og-library.spec.ts | 2 +- .../patches/ast/patch-vercel-og-library.ts | 6 +- .../cli/build/patches/ast/vercel-og.spec.ts | 2 +- .../src/cli/build/patches/ast/vercel-og.ts | 2 +- .../build/patches/ast/webpack-runtime.spec.ts | 2 +- .../cli/build/patches/ast/webpack-runtime.ts | 4 +- .../build/patches/plugins/dynamic-requires.ts | 8 +- .../src/cli/build/patches/plugins/find-dir.ts | 8 +- .../patches/plugins/instrumentation.spec.ts | 2 +- .../build/patches/plugins/instrumentation.ts | 8 +- .../build/patches/plugins/load-manifest.ts | 8 +- .../cli/build/patches/plugins/next-server.ts | 8 +- .../cli/build/patches/plugins/open-next.ts | 8 +- .../patches/plugins/pages-router-context.ts | 2 +- .../plugins/patch-depd-deprecations.spec.ts | 2 +- .../plugins/patch-depd-deprecations.ts | 4 +- .../cli/build/patches/plugins/require-hook.ts | 4 +- .../src/cli/build/patches/plugins/require.ts | 2 +- .../patches/plugins/res-revalidate.spec.ts | 2 +- .../build/patches/plugins/res-revalidate.ts | 6 +- .../cli/build/patches/plugins/route-module.ts | 8 +- .../cli/build/patches/plugins/shim-react.ts | 4 +- .../cli/build/patches/plugins/turbopack.ts | 6 +- .../cli/build/patches/plugins/use-cache.ts | 6 +- .../cli/build/utils/copy-package-cli-files.ts | 2 +- .../src/cli/build/utils/ensure-cf-config.ts | 4 +- .../src/cli/build/utils/middleware.ts | 4 +- .../build/utils/needs-experimental-react.ts | 2 +- .../src/cli/build/utils/test-patch.ts | 2 +- .../cloudflare/src/cli/build/utils/version.ts | 2 +- .../cloudflare/src/cli/build/utils/workerd.ts | 8 +- .../cloudflare/src/cli/commands/build.spec.ts | 4 +- packages/cloudflare/src/cli/commands/build.ts | 2 +- .../cloudflare/src/cli/commands/deploy.ts | 2 +- .../cloudflare/src/cli/commands/migrate.ts | 4 +- .../src/cli/commands/populate-cache.spec.ts | 2 +- .../src/cli/commands/populate-cache.ts | 8 +- .../cloudflare/src/cli/commands/preview.ts | 2 +- .../src/cli/commands/skew-protection.ts | 6 +- .../cloudflare/src/cli/commands/upload.ts | 2 +- .../src/cli/commands/utils/helpers.ts | 2 +- .../src/cli/commands/utils/run-wrangler.ts | 2 +- .../src/cli/commands/utils/utils.spec.ts | 8 +- .../src/cli/commands/utils/utils.ts | 10 +- packages/cloudflare/src/cli/index.ts | 2 +- .../cloudflare/src/cli/templates/images.ts | 2 +- .../cloudflare/src/cli/templates/worker.ts | 2 +- .../src/cli/utils/create-open-next-config.ts | 2 +- .../src/cli/utils/create-wrangler-config.ts | 2 +- .../src/cli/utils/extract-project-env-vars.ts | 2 +- .../src/cli/utils/needs-experimental-react.ts | 2 +- .../src/cli/utils/nextjs-support.ts | 4 +- packages/core/package.json | 60 ++ packages/core/src/adapters/cache.ts | 443 +++++++++++++++ .../core/src/adapters/composable-cache.ts | 135 +++++ packages/core/src/adapters/config/index.ts | 41 ++ packages/core/src/adapters/config/util.ts | 135 +++++ packages/core/src/adapters/edge-adapter.ts | 74 +++ .../adapters/image-optimization-adapter.ts | 256 +++++++++ packages/core/src/adapters/logger.ts | 102 ++++ packages/core/src/adapters/middleware.ts | 127 +++++ .../src/adapters/plugins/README.md | 0 .../image-optimization/image-optimization.ts | 47 ++ packages/core/src/adapters/revalidate.ts | 99 ++++ packages/core/src/adapters/server-adapter.ts | 27 + packages/core/src/adapters/util.ts | 39 ++ packages/core/src/adapters/warmer-function.ts | 34 ++ packages/core/src/build/buildNextApp.ts | 22 + packages/core/src/build/compileCache.ts | 59 ++ packages/core/src/build/compileConfig.ts | 134 +++++ .../core/src/build/compileTagCacheProvider.ts | 34 ++ packages/core/src/build/constant.ts | 3 + packages/core/src/build/copyAdapterFiles.ts | 79 +++ packages/core/src/build/copyTracedFiles.ts | 407 ++++++++++++++ packages/core/src/build/createAssets.ts | 274 +++++++++ .../build/createImageOptimizationBundle.ts | 100 ++++ packages/core/src/build/createMiddleware.ts | 87 +++ .../src/build/createRevalidationBundle.ts | 48 ++ packages/core/src/build/createServerBundle.ts | 324 +++++++++++ packages/core/src/build/createWarmerBundle.ts | 53 ++ .../core/src/build/edge/createEdgeBundle.ts | 233 ++++++++ packages/core/src/build/generateOutput.ts | 340 ++++++++++++ packages/core/src/build/helper.ts | 442 +++++++++++++++ packages/core/src/build/installDeps.ts | 78 +++ .../build/middleware/buildNodeMiddleware.ts | 108 ++++ .../core/src/build/patch/astCodePatcher.ts | 110 ++++ packages/core/src/build/patch/codePatcher.ts | 176 ++++++ .../core/src/build/patch/patches/index.ts | 11 + .../patches/patchBackgroundRevalidation.ts | 29 + .../src/build/patch/patches/patchEnvVar.ts | 54 ++ .../build/patch/patches/patchFetchCacheISR.ts | 149 +++++ .../patch/patches/patchFetchCacheWaitUntil.ts | 41 ++ .../build/patch/patches/patchNextServer.ts | 133 +++++ .../patch/patches/patchNodeEnvironment.ts | 31 ++ .../patch/patches/patchOriginalNextConfig.ts | 87 +++ packages/core/src/build/utils.ts | 30 + packages/core/src/build/validateConfig.ts | 92 +++ .../core/src/core/createGenericHandler.ts | 48 ++ packages/core/src/core/createMainHandler.ts | 57 ++ packages/core/src/core/edgeFunctionHandler.ts | 30 + .../core/src/core/nodeMiddlewareHandler.ts | 40 ++ packages/core/src/core/requestHandler.ts | 242 ++++++++ packages/core/src/core/resolve.ts | 162 ++++++ .../core/src/core/routing/adapterHandler.ts | 120 ++++ .../core/src/core/routing/cacheInterceptor.ts | 418 ++++++++++++++ .../src/core/routing/i18n/accept-header.ts | 136 +++++ packages/core/src/core/routing/i18n/index.ts | 158 ++++++ packages/core/src/core/routing/matcher.ts | 430 ++++++++++++++ packages/core/src/core/routing/middleware.ts | 186 +++++++ packages/core/src/core/routing/queue.ts | 49 ++ .../core/src/core/routing/routeMatcher.ts | 84 +++ packages/core/src/core/routing/util.ts | 448 +++++++++++++++ packages/core/src/core/routingHandler.ts | 298 ++++++++++ packages/core/src/debug.ts | 15 + packages/core/src/helpers/withCloudflare.ts | 102 ++++ packages/core/src/http/index.ts | 4 + packages/core/src/http/openNextResponse.ts | 388 +++++++++++++ packages/core/src/http/request.ts | 54 ++ packages/core/src/http/util.ts | 90 +++ packages/core/src/logger.ts | 18 + packages/core/src/minimize-js.ts | 100 ++++ .../core/src/overrides/assetResolver/dummy.ts | 12 + .../src/overrides/cdnInvalidation/dummy.ts | 8 + .../core/src/overrides/converters/dummy.ts | 24 + .../core/src/overrides/converters/edge.ts | 119 ++++ .../core/src/overrides/converters/node.ts | 58 ++ .../core/src/overrides/converters/utils.ts | 31 ++ .../core/src/overrides/imageLoader/dummy.ts | 11 + .../core/src/overrides/imageLoader/fs-dev.ts | 21 + .../core/src/overrides/imageLoader/host.ts | 33 ++ .../src/overrides/incrementalCache/dummy.ts | 17 + .../src/overrides/incrementalCache/fs-dev.ts | 37 ++ .../src/overrides/originResolver/dummy.ts | 10 + .../overrides/originResolver/pattern-env.ts | 84 +++ .../overrides/proxyExternalRequest/dummy.ts | 11 + .../overrides/proxyExternalRequest/fetch.ts | 33 ++ .../overrides/proxyExternalRequest/node.ts | 83 +++ packages/core/src/overrides/queue/direct.ts | 20 + packages/core/src/overrides/queue/dummy.ts | 11 + packages/core/src/overrides/tagCache/dummy.ts | 21 + .../src/overrides/tagCache/fs-dev-nextMode.ts | 53 ++ .../core/src/overrides/tagCache/fs-dev.ts | 56 ++ packages/core/src/overrides/warmer/dummy.ts | 11 + .../src/overrides/wrappers/cloudflare-edge.ts | 60 ++ .../src/overrides/wrappers/cloudflare-node.ts | 129 +++++ packages/core/src/overrides/wrappers/dummy.ts | 15 + .../src/overrides/wrappers/express-dev.ts | 80 +++ packages/core/src/overrides/wrappers/node.ts | 76 +++ packages/core/src/plugins/content-updater.ts | 97 ++++ packages/core/src/plugins/edge.ts | 205 +++++++ .../core/src/plugins/externalMiddleware.ts | 15 + .../src/plugins/inline-require-resolve.ts | 33 ++ .../core/src/plugins/inlineRouteHandlers.ts | 123 ++++ packages/core/src/plugins/replacement.ts | 110 ++++ packages/core/src/plugins/resolve.ts | 107 ++++ packages/core/src/types/adapter.ts | 17 + packages/core/src/types/cache.ts | 175 ++++++ packages/core/src/types/global.ts | 234 ++++++++ packages/core/src/types/next-types.ts | 211 +++++++ packages/core/src/types/open-next.ts | 524 +++++++++++++++++ packages/core/src/types/overrides.ts | 264 +++++++++ packages/core/src/utils/binary.ts | 67 +++ packages/core/src/utils/cache.ts | 82 +++ packages/core/src/utils/error.ts | 49 ++ packages/core/src/utils/lru.ts | 30 + packages/core/src/utils/normalize-path.ts | 22 + packages/core/src/utils/promise.ts | 137 +++++ packages/core/src/utils/regex.ts | 28 + packages/core/src/utils/safe-json-parse.ts | 10 + packages/core/src/utils/stream.ts | 67 +++ packages/core/tsconfig.json | 16 + packages/open-next/package.json | 1 + packages/open-next/src/adapters/cache.ts | 445 +-------------- .../src/adapters/composable-cache.ts | 137 +---- .../open-next/src/adapters/config/index.ts | 42 +- .../open-next/src/adapters/config/util.ts | 136 +---- .../open-next/src/adapters/edge-adapter.ts | 76 +-- .../adapters/image-optimization-adapter.ts | 257 +-------- packages/open-next/src/adapters/logger.ts | 103 +--- packages/open-next/src/adapters/middleware.ts | 129 +---- .../image-optimization/image-optimization.ts | 49 +- packages/open-next/src/adapters/revalidate.ts | 100 +--- .../open-next/src/adapters/server-adapter.ts | 28 +- packages/open-next/src/adapters/util.ts | 40 +- .../open-next/src/adapters/warmer-function.ts | 35 +- packages/open-next/src/build/buildNextApp.ts | 23 +- packages/open-next/src/build/compileCache.ts | 60 +- packages/open-next/src/build/compileConfig.ts | 135 +---- .../src/build/compileTagCacheProvider.ts | 35 +- packages/open-next/src/build/constant.ts | 4 +- .../open-next/src/build/copyAdapterFiles.ts | 80 +-- .../open-next/src/build/copyTracedFiles.ts | 408 +------------- packages/open-next/src/build/createAssets.ts | 275 +-------- .../build/createImageOptimizationBundle.ts | 101 +--- .../open-next/src/build/createMiddleware.ts | 88 +-- .../src/build/createRevalidationBundle.ts | 49 +- .../open-next/src/build/createServerBundle.ts | 325 +---------- .../open-next/src/build/createWarmerBundle.ts | 54 +- .../src/build/edge/createEdgeBundle.ts | 234 +------- .../open-next/src/build/generateOutput.ts | 341 +----------- packages/open-next/src/build/helper.ts | 443 +-------------- packages/open-next/src/build/installDeps.ts | 79 +-- .../build/middleware/buildNodeMiddleware.ts | 109 +--- .../src/build/patch/astCodePatcher.ts | 111 +--- .../open-next/src/build/patch/codePatcher.ts | 177 +----- .../src/build/patch/patches/index.ts | 12 +- .../patches/patchBackgroundRevalidation.ts | 30 +- .../src/build/patch/patches/patchEnvVar.ts | 55 +- .../build/patch/patches/patchFetchCacheISR.ts | 150 +---- .../patch/patches/patchFetchCacheWaitUntil.ts | 42 +- .../build/patch/patches/patchNextServer.ts | 134 +---- .../patch/patches/patchNodeEnvironment.ts | 32 +- .../patch/patches/patchOriginalNextConfig.ts | 88 +-- packages/open-next/src/build/utils.ts | 31 +- .../open-next/src/build/validateConfig.ts | 93 +--- .../src/core/createGenericHandler.ts | 49 +- .../open-next/src/core/createMainHandler.ts | 58 +- .../open-next/src/core/edgeFunctionHandler.ts | 32 +- .../src/core/nodeMiddlewareHandler.ts | 42 +- packages/open-next/src/core/requestHandler.ts | 243 +------- packages/open-next/src/core/resolve.ts | 160 +----- .../src/core/routing/adapterHandler.ts | 121 +--- .../src/core/routing/cacheInterceptor.ts | 419 +------------- .../src/core/routing/i18n/accept-header.ts | 137 +---- .../open-next/src/core/routing/i18n/index.ts | 159 +----- .../open-next/src/core/routing/matcher.ts | 431 +------------- .../open-next/src/core/routing/middleware.ts | 187 +------ packages/open-next/src/core/routing/queue.ts | 50 +- .../src/core/routing/routeMatcher.ts | 85 +-- packages/open-next/src/core/routing/util.ts | 449 +-------------- packages/open-next/src/core/routingHandler.ts | 300 +--------- packages/open-next/src/debug.ts | 16 +- .../open-next/src/helpers/withCloudflare.ts | 103 +--- packages/open-next/src/http/index.ts | 5 +- .../open-next/src/http/openNextResponse.ts | 389 +------------ packages/open-next/src/http/request.ts | 55 +- packages/open-next/src/http/util.ts | 91 +-- packages/open-next/src/logger.ts | 20 +- packages/open-next/src/minimize-js.ts | 101 +--- .../src/overrides/assetResolver/dummy.ts | 14 +- .../src/overrides/cdnInvalidation/dummy.ts | 10 +- .../src/overrides/converters/dummy.ts | 26 +- .../src/overrides/converters/edge.ts | 121 +--- .../src/overrides/converters/node.ts | 60 +- .../src/overrides/converters/utils.ts | 32 +- .../src/overrides/imageLoader/dummy.ts | 13 +- .../src/overrides/imageLoader/fs-dev.ts | 23 +- .../src/overrides/imageLoader/host.ts | 35 +- .../src/overrides/incrementalCache/dummy.ts | 19 +- .../src/overrides/incrementalCache/fs-dev.ts | 39 +- .../src/overrides/originResolver/dummy.ts | 12 +- .../overrides/originResolver/pattern-env.ts | 86 +-- .../overrides/proxyExternalRequest/dummy.ts | 13 +- .../overrides/proxyExternalRequest/fetch.ts | 35 +- .../overrides/proxyExternalRequest/node.ts | 85 +-- .../open-next/src/overrides/queue/direct.ts | 22 +- .../open-next/src/overrides/queue/dummy.ts | 13 +- .../open-next/src/overrides/tagCache/dummy.ts | 23 +- .../src/overrides/tagCache/fs-dev-nextMode.ts | 55 +- .../src/overrides/tagCache/fs-dev.ts | 58 +- .../open-next/src/overrides/warmer/dummy.ts | 13 +- .../src/overrides/wrappers/cloudflare-edge.ts | 62 +-- .../src/overrides/wrappers/cloudflare-node.ts | 131 +---- .../open-next/src/overrides/wrappers/dummy.ts | 17 +- .../src/overrides/wrappers/express-dev.ts | 82 +-- .../open-next/src/overrides/wrappers/node.ts | 78 +-- .../open-next/src/plugins/content-updater.ts | 98 +--- packages/open-next/src/plugins/edge.ts | 206 +------ .../src/plugins/externalMiddleware.ts | 16 +- .../src/plugins/inline-require-resolve.ts | 34 +- .../src/plugins/inlineRouteHandlers.ts | 124 +---- packages/open-next/src/plugins/replacement.ts | 111 +--- packages/open-next/src/plugins/resolve.ts | 108 +--- packages/open-next/src/types/cache.ts | 176 +----- packages/open-next/src/types/global.ts | 235 +------- packages/open-next/src/types/next-types.ts | 212 +------ packages/open-next/src/types/open-next.ts | 525 +----------------- packages/open-next/src/types/overrides.ts | 265 +-------- packages/open-next/src/utils/binary.ts | 68 +-- packages/open-next/src/utils/cache.ts | 83 +-- packages/open-next/src/utils/error.ts | 50 +- packages/open-next/src/utils/lru.ts | 31 +- .../open-next/src/utils/normalize-path.ts | 23 +- packages/open-next/src/utils/promise.ts | 138 +---- packages/open-next/src/utils/regex.ts | 29 +- .../open-next/src/utils/safe-json-parse.ts | 11 +- packages/open-next/src/utils/stream.ts | 68 +-- packages/tests-unit/package.json | 3 +- .../tests/converters/aws-apigw-v2.test.ts | 2 +- .../tests/converters/aws-cloudfront.test.ts | 2 +- .../core/routing/cacheInterceptor.test.ts | 2 +- .../tests/core/routing/i18n.test.ts | 4 +- .../tests/core/routing/matcher.test.ts | 4 +- .../tests/core/routing/middleware.test.ts | 2 +- .../tests/core/routing/routeMatcher.test.ts | 2 +- .../tests/core/routing/util.test.ts | 6 +- packages/tests-unit/tsconfig.json | 7 +- packages/tests-unit/vitest.config.ts | 8 + pnpm-lock.yaml | 66 ++- 332 files changed, 13262 insertions(+), 12928 deletions(-) create mode 100644 .omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json create mode 100644 packages/core/package.json create mode 100644 packages/core/src/adapters/cache.ts create mode 100644 packages/core/src/adapters/composable-cache.ts create mode 100644 packages/core/src/adapters/config/index.ts create mode 100644 packages/core/src/adapters/config/util.ts create mode 100644 packages/core/src/adapters/edge-adapter.ts create mode 100644 packages/core/src/adapters/image-optimization-adapter.ts create mode 100644 packages/core/src/adapters/logger.ts create mode 100644 packages/core/src/adapters/middleware.ts rename packages/{open-next => core}/src/adapters/plugins/README.md (100%) create mode 100644 packages/core/src/adapters/plugins/image-optimization/image-optimization.ts create mode 100644 packages/core/src/adapters/revalidate.ts create mode 100644 packages/core/src/adapters/server-adapter.ts create mode 100644 packages/core/src/adapters/util.ts create mode 100644 packages/core/src/adapters/warmer-function.ts create mode 100644 packages/core/src/build/buildNextApp.ts create mode 100644 packages/core/src/build/compileCache.ts create mode 100644 packages/core/src/build/compileConfig.ts create mode 100644 packages/core/src/build/compileTagCacheProvider.ts create mode 100644 packages/core/src/build/constant.ts create mode 100644 packages/core/src/build/copyAdapterFiles.ts create mode 100644 packages/core/src/build/copyTracedFiles.ts create mode 100644 packages/core/src/build/createAssets.ts create mode 100644 packages/core/src/build/createImageOptimizationBundle.ts create mode 100644 packages/core/src/build/createMiddleware.ts create mode 100644 packages/core/src/build/createRevalidationBundle.ts create mode 100644 packages/core/src/build/createServerBundle.ts create mode 100644 packages/core/src/build/createWarmerBundle.ts create mode 100644 packages/core/src/build/edge/createEdgeBundle.ts create mode 100644 packages/core/src/build/generateOutput.ts create mode 100644 packages/core/src/build/helper.ts create mode 100644 packages/core/src/build/installDeps.ts create mode 100644 packages/core/src/build/middleware/buildNodeMiddleware.ts create mode 100644 packages/core/src/build/patch/astCodePatcher.ts create mode 100644 packages/core/src/build/patch/codePatcher.ts create mode 100644 packages/core/src/build/patch/patches/index.ts create mode 100644 packages/core/src/build/patch/patches/patchBackgroundRevalidation.ts create mode 100644 packages/core/src/build/patch/patches/patchEnvVar.ts create mode 100644 packages/core/src/build/patch/patches/patchFetchCacheISR.ts create mode 100644 packages/core/src/build/patch/patches/patchFetchCacheWaitUntil.ts create mode 100644 packages/core/src/build/patch/patches/patchNextServer.ts create mode 100644 packages/core/src/build/patch/patches/patchNodeEnvironment.ts create mode 100644 packages/core/src/build/patch/patches/patchOriginalNextConfig.ts create mode 100644 packages/core/src/build/utils.ts create mode 100644 packages/core/src/build/validateConfig.ts create mode 100644 packages/core/src/core/createGenericHandler.ts create mode 100644 packages/core/src/core/createMainHandler.ts create mode 100644 packages/core/src/core/edgeFunctionHandler.ts create mode 100644 packages/core/src/core/nodeMiddlewareHandler.ts create mode 100644 packages/core/src/core/requestHandler.ts create mode 100644 packages/core/src/core/resolve.ts create mode 100644 packages/core/src/core/routing/adapterHandler.ts create mode 100644 packages/core/src/core/routing/cacheInterceptor.ts create mode 100644 packages/core/src/core/routing/i18n/accept-header.ts create mode 100644 packages/core/src/core/routing/i18n/index.ts create mode 100644 packages/core/src/core/routing/matcher.ts create mode 100644 packages/core/src/core/routing/middleware.ts create mode 100644 packages/core/src/core/routing/queue.ts create mode 100644 packages/core/src/core/routing/routeMatcher.ts create mode 100644 packages/core/src/core/routing/util.ts create mode 100644 packages/core/src/core/routingHandler.ts create mode 100644 packages/core/src/debug.ts create mode 100644 packages/core/src/helpers/withCloudflare.ts create mode 100644 packages/core/src/http/index.ts create mode 100644 packages/core/src/http/openNextResponse.ts create mode 100644 packages/core/src/http/request.ts create mode 100644 packages/core/src/http/util.ts create mode 100644 packages/core/src/logger.ts create mode 100644 packages/core/src/minimize-js.ts create mode 100644 packages/core/src/overrides/assetResolver/dummy.ts create mode 100644 packages/core/src/overrides/cdnInvalidation/dummy.ts create mode 100644 packages/core/src/overrides/converters/dummy.ts create mode 100644 packages/core/src/overrides/converters/edge.ts create mode 100644 packages/core/src/overrides/converters/node.ts create mode 100644 packages/core/src/overrides/converters/utils.ts create mode 100644 packages/core/src/overrides/imageLoader/dummy.ts create mode 100644 packages/core/src/overrides/imageLoader/fs-dev.ts create mode 100644 packages/core/src/overrides/imageLoader/host.ts create mode 100644 packages/core/src/overrides/incrementalCache/dummy.ts create mode 100644 packages/core/src/overrides/incrementalCache/fs-dev.ts create mode 100644 packages/core/src/overrides/originResolver/dummy.ts create mode 100644 packages/core/src/overrides/originResolver/pattern-env.ts create mode 100644 packages/core/src/overrides/proxyExternalRequest/dummy.ts create mode 100644 packages/core/src/overrides/proxyExternalRequest/fetch.ts create mode 100644 packages/core/src/overrides/proxyExternalRequest/node.ts create mode 100644 packages/core/src/overrides/queue/direct.ts create mode 100644 packages/core/src/overrides/queue/dummy.ts create mode 100644 packages/core/src/overrides/tagCache/dummy.ts create mode 100644 packages/core/src/overrides/tagCache/fs-dev-nextMode.ts create mode 100644 packages/core/src/overrides/tagCache/fs-dev.ts create mode 100644 packages/core/src/overrides/warmer/dummy.ts create mode 100644 packages/core/src/overrides/wrappers/cloudflare-edge.ts create mode 100644 packages/core/src/overrides/wrappers/cloudflare-node.ts create mode 100644 packages/core/src/overrides/wrappers/dummy.ts create mode 100644 packages/core/src/overrides/wrappers/express-dev.ts create mode 100644 packages/core/src/overrides/wrappers/node.ts create mode 100644 packages/core/src/plugins/content-updater.ts create mode 100644 packages/core/src/plugins/edge.ts create mode 100644 packages/core/src/plugins/externalMiddleware.ts create mode 100644 packages/core/src/plugins/inline-require-resolve.ts create mode 100644 packages/core/src/plugins/inlineRouteHandlers.ts create mode 100644 packages/core/src/plugins/replacement.ts create mode 100644 packages/core/src/plugins/resolve.ts create mode 100644 packages/core/src/types/adapter.ts create mode 100644 packages/core/src/types/cache.ts create mode 100644 packages/core/src/types/global.ts create mode 100644 packages/core/src/types/next-types.ts create mode 100644 packages/core/src/types/open-next.ts create mode 100644 packages/core/src/types/overrides.ts create mode 100644 packages/core/src/utils/binary.ts create mode 100644 packages/core/src/utils/cache.ts create mode 100644 packages/core/src/utils/error.ts create mode 100644 packages/core/src/utils/lru.ts create mode 100644 packages/core/src/utils/normalize-path.ts create mode 100644 packages/core/src/utils/promise.ts create mode 100644 packages/core/src/utils/regex.ts create mode 100644 packages/core/src/utils/safe-json-parse.ts create mode 100644 packages/core/src/utils/stream.ts create mode 100644 packages/core/tsconfig.json diff --git a/.omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json b/.omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json new file mode 100644 index 00000000..38e31e1f --- /dev/null +++ b/.omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_11f9e6737ffegwim4y24fFKT76", + "updatedAt": "2026-06-19T15:09:29.144Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-06-19T15:09:29.144Z" + } + } +} \ No newline at end of file diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 4430bd40..6a39efca 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -53,6 +53,7 @@ "@ast-grep/napi": "0.40.5", "@dotenvx/dotenvx": "catalog:", "@opennextjs/aws": "workspace:*", + "@opennextjs/core": "workspace:*", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", diff --git a/packages/cloudflare/src/api/config.ts b/packages/cloudflare/src/api/config.ts index 13d1ee6b..32d9be73 100644 --- a/packages/cloudflare/src/api/config.ts +++ b/packages/cloudflare/src/api/config.ts @@ -1,16 +1,16 @@ -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; import { BaseOverride, LazyLoadedOverride, OpenNextConfig as AwsOpenNextConfig, type RoutePreloadingBehavior, -} from "@opennextjs/aws/types/open-next.js"; +} from "@opennextjs/core/types/open-next.js"; import type { CDNInvalidationHandler, IncrementalCache, Queue, TagCache, -} from "@opennextjs/aws/types/overrides.js"; +} from "@opennextjs/core/types/overrides.js"; import assetResolver from "./overrides/asset-resolver/index.js"; diff --git a/packages/cloudflare/src/api/durable-objects/queue.ts b/packages/cloudflare/src/api/durable-objects/queue.ts index 4660304a..db5a4abb 100644 --- a/packages/cloudflare/src/api/durable-objects/queue.ts +++ b/packages/cloudflare/src/api/durable-objects/queue.ts @@ -1,11 +1,11 @@ -import { debug, error, warn } from "@opennextjs/aws/adapters/logger.js"; -import type { QueueMessage } from "@opennextjs/aws/types/overrides.js"; +import { debug, error, warn } from "@opennextjs/core/adapters/logger.js"; +import type { QueueMessage } from "@opennextjs/core/types/overrides.js"; import { FatalError, IgnorableError, isOpenNextError, RecoverableError, -} from "@opennextjs/aws/utils/error.js"; +} from "@opennextjs/core/utils/error.js"; import { DurableObject } from "cloudflare:workers"; const DEFAULT_MAX_REVALIDATION = 5; diff --git a/packages/cloudflare/src/api/overrides/asset-resolver/index.ts b/packages/cloudflare/src/api/overrides/asset-resolver/index.ts index e3b17945..2e1a2467 100644 --- a/packages/cloudflare/src/api/overrides/asset-resolver/index.ts +++ b/packages/cloudflare/src/api/overrides/asset-resolver/index.ts @@ -1,5 +1,5 @@ -import type { InternalEvent, InternalResult } from "@opennextjs/aws/types/open-next.js"; -import type { AssetResolver } from "@opennextjs/aws/types/overrides.js"; +import type { InternalEvent, InternalResult } from "@opennextjs/core/types/open-next.js"; +import type { AssetResolver } from "@opennextjs/core/types/overrides.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; diff --git a/packages/cloudflare/src/api/overrides/cache-purge/index.ts b/packages/cloudflare/src/api/overrides/cache-purge/index.ts index f8268361..e34f1281 100644 --- a/packages/cloudflare/src/api/overrides/cache-purge/index.ts +++ b/packages/cloudflare/src/api/overrides/cache-purge/index.ts @@ -1,5 +1,5 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; -import type { CDNInvalidationHandler } from "@opennextjs/aws/types/overrides.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; +import type { CDNInvalidationHandler } from "@opennextjs/core/types/overrides.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { debugCache, internalPurgeCacheByTags } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/incremental-cache/kv-incremental-cache.ts b/packages/cloudflare/src/api/overrides/incremental-cache/kv-incremental-cache.ts index 110f1a3a..c4af0202 100644 --- a/packages/cloudflare/src/api/overrides/incremental-cache/kv-incremental-cache.ts +++ b/packages/cloudflare/src/api/overrides/incremental-cache/kv-incremental-cache.ts @@ -1,11 +1,11 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; import type { CacheEntryType, CacheValue, IncrementalCache, WithLastModified, -} from "@opennextjs/aws/types/overrides.js"; -import { IgnorableError } from "@opennextjs/aws/utils/error.js"; +} from "@opennextjs/core/types/overrides.js"; +import { IgnorableError } from "@opennextjs/core/utils/error.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { computeCacheKey, debugCache, IncrementalCacheEntry } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/incremental-cache/r2-incremental-cache.ts b/packages/cloudflare/src/api/overrides/incremental-cache/r2-incremental-cache.ts index ba7ef4fa..18f804ad 100644 --- a/packages/cloudflare/src/api/overrides/incremental-cache/r2-incremental-cache.ts +++ b/packages/cloudflare/src/api/overrides/incremental-cache/r2-incremental-cache.ts @@ -1,11 +1,11 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; import type { CacheEntryType, CacheValue, IncrementalCache, WithLastModified, -} from "@opennextjs/aws/types/overrides.js"; -import { IgnorableError } from "@opennextjs/aws/utils/error.js"; +} from "@opennextjs/core/types/overrides.js"; +import { IgnorableError } from "@opennextjs/core/utils/error.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { computeCacheKey, debugCache } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts b/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts index d6537c11..41d70238 100644 --- a/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts +++ b/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts @@ -1,10 +1,10 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; import { CacheEntryType, CacheValue, IncrementalCache, WithLastModified, -} from "@opennextjs/aws/types/overrides.js"; +} from "@opennextjs/core/types/overrides.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { debugCache, FALLBACK_BUILD_ID, IncrementalCacheEntry, isPurgeCacheEnabled } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/incremental-cache/static-assets-incremental-cache.ts b/packages/cloudflare/src/api/overrides/incremental-cache/static-assets-incremental-cache.ts index 66952d8f..8610ee76 100644 --- a/packages/cloudflare/src/api/overrides/incremental-cache/static-assets-incremental-cache.ts +++ b/packages/cloudflare/src/api/overrides/incremental-cache/static-assets-incremental-cache.ts @@ -1,11 +1,11 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; import type { CacheEntryType, CacheValue, IncrementalCache, WithLastModified, -} from "@opennextjs/aws/types/overrides.js"; -import { IgnorableError } from "@opennextjs/aws/utils/error.js"; +} from "@opennextjs/core/types/overrides.js"; +import { IgnorableError } from "@opennextjs/core/utils/error.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { debugCache, FALLBACK_BUILD_ID } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/internal.ts b/packages/cloudflare/src/api/overrides/internal.ts index cc7c115d..a4f78b12 100644 --- a/packages/cloudflare/src/api/overrides/internal.ts +++ b/packages/cloudflare/src/api/overrides/internal.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; -import { error } from "@opennextjs/aws/adapters/logger.js"; -import type { CacheEntryType, CacheValue } from "@opennextjs/aws/types/overrides.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; +import type { CacheEntryType, CacheValue } from "@opennextjs/core/types/overrides.js"; import { getCloudflareContext } from "../cloudflare-context.js"; diff --git a/packages/cloudflare/src/api/overrides/queue/do-queue.ts b/packages/cloudflare/src/api/overrides/queue/do-queue.ts index ebb1a272..b50f36aa 100644 --- a/packages/cloudflare/src/api/overrides/queue/do-queue.ts +++ b/packages/cloudflare/src/api/overrides/queue/do-queue.ts @@ -1,5 +1,5 @@ -import type { Queue, QueueMessage } from "@opennextjs/aws/types/overrides.js"; -import { IgnorableError } from "@opennextjs/aws/utils/error.js"; +import type { Queue, QueueMessage } from "@opennextjs/core/types/overrides.js"; +import { IgnorableError } from "@opennextjs/core/utils/error.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; diff --git a/packages/cloudflare/src/api/overrides/queue/memory-queue.spec.ts b/packages/cloudflare/src/api/overrides/queue/memory-queue.spec.ts index 29c2c738..fd963850 100644 --- a/packages/cloudflare/src/api/overrides/queue/memory-queue.spec.ts +++ b/packages/cloudflare/src/api/overrides/queue/memory-queue.spec.ts @@ -1,4 +1,4 @@ -import { generateMessageGroupId } from "@opennextjs/aws/core/routing/queue.js"; +import { generateMessageGroupId } from "@opennextjs/core/core/routing/queue.js"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import cache, { DEFAULT_REVALIDATION_TIMEOUT_MS } from "./memory-queue.js"; diff --git a/packages/cloudflare/src/api/overrides/queue/memory-queue.ts b/packages/cloudflare/src/api/overrides/queue/memory-queue.ts index 980c7723..f4b67293 100644 --- a/packages/cloudflare/src/api/overrides/queue/memory-queue.ts +++ b/packages/cloudflare/src/api/overrides/queue/memory-queue.ts @@ -1,6 +1,6 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; -import type { Queue, QueueMessage } from "@opennextjs/aws/types/overrides.js"; -import { IgnorableError } from "@opennextjs/aws/utils/error.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; +import type { Queue, QueueMessage } from "@opennextjs/core/types/overrides.js"; +import { IgnorableError } from "@opennextjs/core/utils/error.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { debugCache } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/queue/queue-cache.spec.ts b/packages/cloudflare/src/api/overrides/queue/queue-cache.spec.ts index ce28a139..58c57692 100644 --- a/packages/cloudflare/src/api/overrides/queue/queue-cache.spec.ts +++ b/packages/cloudflare/src/api/overrides/queue/queue-cache.spec.ts @@ -1,4 +1,4 @@ -import type { Queue } from "@opennextjs/aws/types/overrides.js"; +import type { Queue } from "@opennextjs/core/types/overrides.js"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import queueCache from "./queue-cache.js"; diff --git a/packages/cloudflare/src/api/overrides/queue/queue-cache.ts b/packages/cloudflare/src/api/overrides/queue/queue-cache.ts index f084907e..81b9dd1f 100644 --- a/packages/cloudflare/src/api/overrides/queue/queue-cache.ts +++ b/packages/cloudflare/src/api/overrides/queue/queue-cache.ts @@ -1,5 +1,5 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; -import type { Queue, QueueMessage } from "@opennextjs/aws/types/overrides.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; +import type { Queue, QueueMessage } from "@opennextjs/core/types/overrides.js"; interface QueueCachingOptions { /** diff --git a/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.spec.ts b/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.spec.ts index 9f531d7c..87aae85f 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.spec.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.spec.ts @@ -1,7 +1,7 @@ /** * Author: Copilot (Claude Sonnet 4) */ -import { error } from "@opennextjs/aws/adapters/logger.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getCloudflareContext } from "../../cloudflare-context.js"; @@ -10,7 +10,7 @@ import { debugCache, FALLBACK_BUILD_ID, purgeCacheByTags } from "../internal.js" import { BINDING_NAME, D1NextModeTagCache, NAME } from "./d1-next-tag-cache.js"; // Mock dependencies -vi.mock("@opennextjs/aws/adapters/logger.js", () => ({ +vi.mock("@opennextjs/core/adapters/logger.js", () => ({ error: vi.fn(), })); diff --git a/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.ts b/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.ts index c13225b7..018ef92e 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.ts @@ -1,5 +1,5 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; -import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; +import type { NextModeTagCache } from "@opennextjs/core/types/overrides.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { debugCache, FALLBACK_BUILD_ID, isPurgeCacheEnabled, purgeCacheByTags } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts b/packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts index 4751d259..cf9462af 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts @@ -1,7 +1,7 @@ -import { debug, error } from "@opennextjs/aws/adapters/logger.js"; -import { generateShardId } from "@opennextjs/aws/core/routing/queue.js"; -import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js"; -import { IgnorableError } from "@opennextjs/aws/utils/error.js"; +import { debug, error } from "@opennextjs/core/adapters/logger.js"; +import { generateShardId } from "@opennextjs/core/core/routing/queue.js"; +import type { NextModeTagCache } from "@opennextjs/core/types/overrides.js"; +import { IgnorableError } from "@opennextjs/core/utils/error.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import type { OpenNextConfig } from "../../config.js"; diff --git a/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.spec.ts b/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.spec.ts index 9d8090f9..7a01d1c6 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.spec.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.spec.ts @@ -1,4 +1,4 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getCloudflareContext } from "../../cloudflare-context.js"; @@ -7,7 +7,7 @@ import { FALLBACK_BUILD_ID, purgeCacheByTags } from "../internal.js"; import { BINDING_NAME, KVNextModeTagCache, NAME } from "./kv-next-tag-cache.js"; // Mock dependencies -vi.mock("@opennextjs/aws/adapters/logger.js", () => ({ +vi.mock("@opennextjs/core/adapters/logger.js", () => ({ error: vi.fn(), })); diff --git a/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.ts b/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.ts index abd64937..6c4a79ad 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/kv-next-tag-cache.ts @@ -1,5 +1,5 @@ -import { error } from "@opennextjs/aws/adapters/logger.js"; -import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; +import type { NextModeTagCache } from "@opennextjs/core/types/overrides.js"; import { getCloudflareContext } from "../../cloudflare-context.js"; import { debugCache, FALLBACK_BUILD_ID, isPurgeCacheEnabled, purgeCacheByTags } from "../internal.js"; diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts index 65f210af..2e6aebd8 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.spec.ts @@ -1,4 +1,4 @@ -import { NextModeTagCache } from "@opennextjs/aws/types/overrides.js"; +import { NextModeTagCache } from "@opennextjs/core/types/overrides.js"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { softTagFilter, withFilter } from "./tag-cache-filter.js"; diff --git a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts index c66f68e0..05c11970 100644 --- a/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts +++ b/packages/cloudflare/src/api/overrides/tag-cache/tag-cache-filter.ts @@ -1,4 +1,4 @@ -import { NextModeTagCache } from "@opennextjs/aws/types/overrides.js"; +import { NextModeTagCache } from "@opennextjs/core/types/overrides.js"; interface WithFilterOptions { /** diff --git a/packages/cloudflare/src/cli/adapter.ts b/packages/cloudflare/src/cli/adapter.ts index 0d4cf8d4..3629f038 100644 --- a/packages/cloudflare/src/cli/adapter.ts +++ b/packages/cloudflare/src/cli/adapter.ts @@ -3,16 +3,16 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; -import { compileCache } from "@opennextjs/aws/build/compileCache.js"; -import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js"; -import { compileTagCacheProvider } from "@opennextjs/aws/build/compileTagCacheProvider.js"; -import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js"; -import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js"; -import * as buildHelper from "@opennextjs/aws/build/helper.js"; -import { addDebugFile } from "@opennextjs/aws/debug.js"; -import type { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js"; -import { inlineRouteHandler } from "@opennextjs/aws/plugins/inlineRouteHandlers.js"; -import type { NextConfig } from "@opennextjs/aws/types/next-types.js"; +import { compileCache } from "@opennextjs/core/build/compileCache.js"; +import { compileOpenNextConfig } from "@opennextjs/core/build/compileConfig.js"; +import { compileTagCacheProvider } from "@opennextjs/core/build/compileTagCacheProvider.js"; +import { createCacheAssets, createStaticAssets } from "@opennextjs/core/build/createAssets.js"; +import { createMiddleware } from "@opennextjs/core/build/createMiddleware.js"; +import * as buildHelper from "@opennextjs/core/build/helper.js"; +import { addDebugFile } from "@opennextjs/core/debug.js"; +import type { ContentUpdater } from "@opennextjs/core/plugins/content-updater.js"; +import { inlineRouteHandler } from "@opennextjs/core/plugins/inlineRouteHandlers.js"; +import type { NextConfig } from "@opennextjs/core/types/next-types.js"; import { bundleServer } from "./build/bundle-server.js"; import { compileEnvFiles } from "./build/open-next/compile-env-files.js"; @@ -59,7 +59,7 @@ export default { }); const require = createRequire(import.meta.url); - const openNextDistDir = path.dirname(require.resolve("@opennextjs/aws/index.js")); + const openNextDistDir = path.dirname(require.resolve("@opennextjs/core/index.js")); buildOpts = buildHelper.normalizeOptions(config, openNextDistDir, buildDir); diff --git a/packages/cloudflare/src/cli/build/build.ts b/packages/cloudflare/src/cli/build/build.ts index a95280b5..5bb61ece 100644 --- a/packages/cloudflare/src/cli/build/build.ts +++ b/packages/cloudflare/src/cli/build/build.ts @@ -1,7 +1,7 @@ -import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js"; -import * as buildHelper from "@opennextjs/aws/build/helper.js"; -import { printHeader } from "@opennextjs/aws/build/utils.js"; -import logger from "@opennextjs/aws/logger.js"; +import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/core/build/buildNextApp.js"; +import * as buildHelper from "@opennextjs/core/build/helper.js"; +import { printHeader } from "@opennextjs/core/build/utils.js"; +import logger from "@opennextjs/core/logger.js"; import type { ProjectOptions } from "../project-options.js"; import { ensureNextjsVersionSupported } from "../utils/nextjs-support.js"; @@ -28,7 +28,7 @@ export async function build(options: buildHelper.BuildOptions, projectOpts: Proj await ensureNextjsVersionSupported(options); const { aws, cloudflare } = getVersion(); logger.info(`@opennextjs/cloudflare version: ${cloudflare}`); - logger.info(`@opennextjs/aws version: ${aws}`); + logger.info(`@opennextjs/core version: ${aws}`); // Clean the output directory before building the Next app. buildHelper.initOutputDir(options); diff --git a/packages/cloudflare/src/cli/build/bundle-server.ts b/packages/cloudflare/src/cli/build/bundle-server.ts index 7aee1a66..59ecacaf 100644 --- a/packages/cloudflare/src/cli/build/bundle-server.ts +++ b/packages/cloudflare/src/cli/build/bundle-server.ts @@ -3,9 +3,9 @@ import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import * as buildHelper from "@opennextjs/aws/build/helper.js"; -import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import * as buildHelper from "@opennextjs/core/build/helper.js"; +import { ContentUpdater } from "@opennextjs/core/plugins/content-updater.js"; import { build, type Plugin } from "esbuild"; import { getOpenNextConfig } from "../../api/config.js"; diff --git a/packages/cloudflare/src/cli/build/open-next/compile-cache-assets-manifest.ts b/packages/cloudflare/src/cli/build/open-next/compile-cache-assets-manifest.ts index 07ea5114..a7d2899d 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-cache-assets-manifest.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-cache-assets-manifest.ts @@ -1,8 +1,8 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; -import type { TagCacheMetaFile } from "@opennextjs/aws/types/cache.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; +import type { TagCacheMetaFile } from "@opennextjs/core/types/cache.js"; /** * Generates SQL statements that can be used to initialize the cache assets manifest in an SQL data store. diff --git a/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts b/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts index 9da91d3e..cb45f7df 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-env-files.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import { BuildOptions } from "@opennextjs/core/build/helper.js"; import { extractProjectEnvVars } from "../../utils/extract-project-env-vars.js"; diff --git a/packages/cloudflare/src/cli/build/open-next/compile-images.ts b/packages/cloudflare/src/cli/build/open-next/compile-images.ts index f3b2525c..0285b170 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-images.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-images.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; import { build } from "esbuild"; /** diff --git a/packages/cloudflare/src/cli/build/open-next/compile-init.ts b/packages/cloudflare/src/cli/build/open-next/compile-init.ts index bae66919..b186d458 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-init.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-init.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; import { build } from "esbuild"; import type { Unstable_Config } from "wrangler"; diff --git a/packages/cloudflare/src/cli/build/open-next/compile-skew-protection.ts b/packages/cloudflare/src/cli/build/open-next/compile-skew-protection.ts index bd70f252..20a05f2e 100644 --- a/packages/cloudflare/src/cli/build/open-next/compile-skew-protection.ts +++ b/packages/cloudflare/src/cli/build/open-next/compile-skew-protection.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; import { build } from "esbuild"; import type { OpenNextConfig } from "../../../api/index.js"; diff --git a/packages/cloudflare/src/cli/build/open-next/compileDurableObjects.ts b/packages/cloudflare/src/cli/build/open-next/compileDurableObjects.ts index ba5fdd01..0784e146 100644 --- a/packages/cloudflare/src/cli/build/open-next/compileDurableObjects.ts +++ b/packages/cloudflare/src/cli/build/open-next/compileDurableObjects.ts @@ -1,8 +1,8 @@ import { createRequire } from "node:module"; import path from "node:path"; -import { loadBuildId, loadPrerenderManifest } from "@opennextjs/aws/adapters/config/util.js"; -import { type BuildOptions, esbuildSync } from "@opennextjs/aws/build/helper.js"; +import { loadBuildId, loadPrerenderManifest } from "@opennextjs/core/adapters/config/util.js"; +import { type BuildOptions, esbuildSync } from "@opennextjs/core/build/helper.js"; export function compileDurableObjects(buildOpts: BuildOptions) { const _require = createRequire(import.meta.url); diff --git a/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts b/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts index 5478411c..d5508952 100644 --- a/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts +++ b/packages/cloudflare/src/cli/build/open-next/createServerBundle.ts @@ -1,27 +1,27 @@ -// Copy-Edit of @opennextjs/aws packages/open-next/src/build/createServerBundle.ts +// Copy-Edit of @opennextjs/core packages/open-next/src/build/createServerBundle.ts // Adapted for cloudflare workers import fs from "node:fs"; import path from "node:path"; -import { loadMiddlewareManifest } from "@opennextjs/aws/adapters/config/util.js"; -import { compileCache } from "@opennextjs/aws/build/compileCache.js"; -import { copyAdapterFiles } from "@opennextjs/aws/build/copyAdapterFiles.js"; -import { copyMiddlewareResources, 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 type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js"; -import { applyCodePatches } from "@opennextjs/aws/build/patch/codePatcher.js"; -import * as awsPatches from "@opennextjs/aws/build/patch/patches/index.js"; -import logger from "@opennextjs/aws/logger.js"; -import { minifyAll } from "@opennextjs/aws/minimize-js.js"; -import { ContentUpdater } from "@opennextjs/aws/plugins/content-updater.js"; -import { openNextEdgePlugins } from "@opennextjs/aws/plugins/edge.js"; -import { openNextExternalMiddlewarePlugin } from "@opennextjs/aws/plugins/externalMiddleware.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"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { loadMiddlewareManifest } from "@opennextjs/core/adapters/config/util.js"; +import { compileCache } from "@opennextjs/core/build/compileCache.js"; +import { copyAdapterFiles } from "@opennextjs/core/build/copyAdapterFiles.js"; +import { copyMiddlewareResources, generateEdgeBundle } from "@opennextjs/core/build/edge/createEdgeBundle.js"; +import * as buildHelper from "@opennextjs/core/build/helper.js"; +import { installDependencies } from "@opennextjs/core/build/installDeps.js"; +import type { CodePatcher } from "@opennextjs/core/build/patch/codePatcher.js"; +import { applyCodePatches } from "@opennextjs/core/build/patch/codePatcher.js"; +import * as awsPatches from "@opennextjs/core/build/patch/patches/index.js"; +import logger from "@opennextjs/core/logger.js"; +import { minifyAll } from "@opennextjs/core/minimize-js.js"; +import { ContentUpdater } from "@opennextjs/core/plugins/content-updater.js"; +import { openNextEdgePlugins } from "@opennextjs/core/plugins/edge.js"; +import { openNextExternalMiddlewarePlugin } from "@opennextjs/core/plugins/externalMiddleware.js"; +import { openNextReplacementPlugin } from "@opennextjs/core/plugins/replacement.js"; +import { openNextResolvePlugin } from "@opennextjs/core/plugins/resolve.js"; +import type { FunctionOptions, SplittedFunctionOptions } from "@opennextjs/core/types/open-next.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import type { Plugin } from "esbuild"; import type { BuildCompleteCtx } from "../../adapter.js"; diff --git a/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.spec.ts b/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.spec.ts index ec6e6492..e1ca9d50 100644 --- a/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.spec.ts +++ b/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.spec.ts @@ -1,7 +1,7 @@ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; -import { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import { BuildOptions } from "@opennextjs/core/build/helper.js"; import mockFs from "mock-fs"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; diff --git a/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.ts b/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.ts index 6683d981..5acc6eb8 100644 --- a/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.ts +++ b/packages/cloudflare/src/cli/build/patches/ast/patch-vercel-og-library.ts @@ -1,9 +1,9 @@ import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import path from "node:path"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; -import { getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { parseFile } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; +import { getPackagePath } from "@opennextjs/core/build/helper.js"; +import { parseFile } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { globSync } from "glob"; import { patchVercelOgFallbackFont, patchVercelOgImport } from "./vercel-og.js"; diff --git a/packages/cloudflare/src/cli/build/patches/ast/vercel-og.spec.ts b/packages/cloudflare/src/cli/build/patches/ast/vercel-og.spec.ts index e40dcb1d..3f4fae8c 100644 --- a/packages/cloudflare/src/cli/build/patches/ast/vercel-og.spec.ts +++ b/packages/cloudflare/src/cli/build/patches/ast/vercel-og.spec.ts @@ -1,4 +1,4 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { describe, expect, it } from "vitest"; import { vercelOgFallbackFontRule, vercelOgImportRule } from "./vercel-og.js"; diff --git a/packages/cloudflare/src/cli/build/patches/ast/vercel-og.ts b/packages/cloudflare/src/cli/build/patches/ast/vercel-og.ts index ca871938..bacab2af 100644 --- a/packages/cloudflare/src/cli/build/patches/ast/vercel-og.ts +++ b/packages/cloudflare/src/cli/build/patches/ast/vercel-og.ts @@ -1,4 +1,4 @@ -import { applyRule, SgNode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { applyRule, SgNode } from "@opennextjs/core/build/patch/astCodePatcher.js"; export const vercelOgImportRule = ` rule: diff --git a/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.spec.ts b/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.spec.ts index 4f87dbf7..263da79d 100644 --- a/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.spec.ts +++ b/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.spec.ts @@ -1,4 +1,4 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { describe, expect, test } from "vitest"; import { buildMultipleChunksRule, singleChunkRule } from "./webpack-runtime.js"; diff --git a/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.ts b/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.ts index d1eda60d..5914e840 100644 --- a/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.ts +++ b/packages/cloudflare/src/cli/build/patches/ast/webpack-runtime.ts @@ -23,8 +23,8 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; // Inline the code when there are multiple chunks export function buildMultipleChunksRule(chunks: number[]) { diff --git a/packages/cloudflare/src/cli/build/patches/plugins/dynamic-requires.ts b/packages/cloudflare/src/cli/build/patches/plugins/dynamic-requires.ts index f4e80b0f..b0947ed4 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/dynamic-requires.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/dynamic-requires.ts @@ -1,10 +1,10 @@ import { readFile } from "node:fs/promises"; import { join, posix, sep } from "node:path"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode, type RuleConfig } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode, type RuleConfig } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import { normalizePath } from "../../../utils/normalize-path.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/find-dir.ts b/packages/cloudflare/src/cli/build/patches/plugins/find-dir.ts index 5f13be5d..7fe2e07a 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/find-dir.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/find-dir.ts @@ -5,10 +5,10 @@ import { existsSync } from "node:fs"; import { join, posix, sep } from "node:path"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; export function inlineFindDir(updater: ContentUpdater, buildOpts: BuildOptions): Plugin { return updater.updateContent("inline-find-dir", [ diff --git a/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.spec.ts b/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.spec.ts index b34930b5..1ea455cc 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.spec.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.spec.ts @@ -1,4 +1,4 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { describe, expect, test } from "vitest"; import { getNext14Rule, getNext15Rule, getNext154Rule } from "./instrumentation.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.ts b/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.ts index 3796699f..a167d6c0 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/instrumentation.ts @@ -1,10 +1,10 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import { normalizePath } from "../../../utils/normalize-path.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts b/packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts index f31c4122..17f68246 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts @@ -7,10 +7,10 @@ import { readFile } from "node:fs/promises"; import { join, posix, relative, sep } from "node:path"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode, type RuleConfig } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode, type RuleConfig } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import { glob } from "glob"; import { normalizePath } from "../../../utils/normalize-path.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/next-server.ts b/packages/cloudflare/src/cli/build/patches/plugins/next-server.ts index 6bac7c11..99aa3bcc 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/next-server.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/next-server.ts @@ -9,10 +9,10 @@ import path from "node:path"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import { normalizePath } from "../../../utils/normalize-path.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/open-next.ts b/packages/cloudflare/src/cli/build/patches/plugins/open-next.ts index 7b643208..219a4cb2 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/open-next.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/open-next.ts @@ -4,10 +4,10 @@ import path from "node:path"; -import { type BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { type BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; export function patchResolveCache(updater: ContentUpdater, buildOpts: BuildOptions): Plugin { const { outputDir } = buildOpts; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/pages-router-context.ts b/packages/cloudflare/src/cli/build/patches/plugins/pages-router-context.ts index aac87ab1..6da1b0a0 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/pages-router-context.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/pages-router-context.ts @@ -4,7 +4,7 @@ * We need to change the import path for the pages router context to use the one provided in `pages-runtime.prod.js` */ -import { BuildOptions, compareSemver } from "@opennextjs/aws/build/helper.js"; +import { BuildOptions, compareSemver } from "@opennextjs/core/build/helper.js"; import type { OnResolveResult, PluginBuild } from "esbuild"; export function patchPagesRouterContext(buildOpts: BuildOptions) { diff --git a/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.spec.ts b/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.spec.ts index 866d1c09..91e0dd32 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.spec.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.spec.ts @@ -1,4 +1,4 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { describe, expect, test } from "vitest"; import { rule } from "./patch-depd-deprecations.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.ts b/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.ts index 3059cd21..7896d1ff 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/patch-depd-deprecations.ts @@ -1,5 +1,5 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; /** * Some dependencies of Next.js use depd to deprecate some of their functions, depd uses `eval` to generate diff --git a/packages/cloudflare/src/cli/build/patches/plugins/require-hook.ts b/packages/cloudflare/src/cli/build/patches/plugins/require-hook.ts index 1dc315a9..50a138d2 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/require-hook.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/require-hook.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import type { Plugin } from "esbuild"; export function shimRequireHook(options: BuildOptions): Plugin { diff --git a/packages/cloudflare/src/cli/build/patches/plugins/require.ts b/packages/cloudflare/src/cli/build/patches/plugins/require.ts index a480c8ab..639d2666 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/require.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/require.ts @@ -1,4 +1,4 @@ -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; export function fixRequire(updater: ContentUpdater): Plugin { return updater.updateContent("fix-require", [ diff --git a/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.spec.ts b/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.spec.ts index 14e26634..8ae9382d 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.spec.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.spec.ts @@ -1,4 +1,4 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { describe, expect, test } from "vitest"; import { computePatchDiff } from "../../utils/test-patch.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.ts b/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.ts index 6d41d25d..8744d9b9 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/res-revalidate.ts @@ -3,9 +3,9 @@ * Without the patch it uses `fetch` to make a call to itself, which doesn't work once deployed in cloudflare workers * This patch will replace this fetch by a call to `WORKER_SELF_REFERENCE` service binding */ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { CodePatcher } from "@opennextjs/core/build/patch/codePatcher.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; export const rule = ` rule: diff --git a/packages/cloudflare/src/cli/build/patches/plugins/route-module.ts b/packages/cloudflare/src/cli/build/patches/plugins/route-module.ts index e4727451..abd76b2b 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/route-module.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/route-module.ts @@ -8,10 +8,10 @@ import path from "node:path"; -import { BuildOptions, getPackagePath } from "@opennextjs/aws/build/helper.js"; -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { BuildOptions, getPackagePath } from "@opennextjs/core/build/helper.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { ContentUpdater, Plugin } from "@opennextjs/core/plugins/content-updater.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import { normalizePath } from "../../../utils/normalize-path.js"; diff --git a/packages/cloudflare/src/cli/build/patches/plugins/shim-react.ts b/packages/cloudflare/src/cli/build/patches/plugins/shim-react.ts index 6351c89a..7322b857 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/shim-react.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/shim-react.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; import type { Plugin } from "esbuild"; /** diff --git a/packages/cloudflare/src/cli/build/patches/plugins/turbopack.ts b/packages/cloudflare/src/cli/build/patches/plugins/turbopack.ts index 715334e4..1a06be36 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/turbopack.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/turbopack.ts @@ -1,6 +1,6 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { CodePatcher } from "@opennextjs/core/build/patch/codePatcher.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; const inlineChunksRule = ` rule: diff --git a/packages/cloudflare/src/cli/build/patches/plugins/use-cache.ts b/packages/cloudflare/src/cli/build/patches/plugins/use-cache.ts index 5587a2ee..798b3a81 100644 --- a/packages/cloudflare/src/cli/build/patches/plugins/use-cache.ts +++ b/packages/cloudflare/src/cli/build/patches/plugins/use-cache.ts @@ -9,9 +9,9 @@ * ALS context from next (i.e. cookies, headers ...) * TODO: Find a better fix for this issue. */ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import type { CodePatcher } from "@opennextjs/core/build/patch/codePatcher.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; export const rule = ` rule: diff --git a/packages/cloudflare/src/cli/build/utils/copy-package-cli-files.ts b/packages/cloudflare/src/cli/build/utils/copy-package-cli-files.ts index 2e417445..10b57618 100644 --- a/packages/cloudflare/src/cli/build/utils/copy-package-cli-files.ts +++ b/packages/cloudflare/src/cli/build/utils/copy-package-cli-files.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; import { getOutputWorkerPath } from "../bundle-server.js"; diff --git a/packages/cloudflare/src/cli/build/utils/ensure-cf-config.ts b/packages/cloudflare/src/cli/build/utils/ensure-cf-config.ts index 426fb218..fb461018 100644 --- a/packages/cloudflare/src/cli/build/utils/ensure-cf-config.ts +++ b/packages/cloudflare/src/cli/build/utils/ensure-cf-config.ts @@ -1,5 +1,5 @@ -import logger from "@opennextjs/aws/logger.js"; -import type { ExternalMiddlewareConfig } from "@opennextjs/aws/types/open-next.js"; +import logger from "@opennextjs/core/logger.js"; +import type { ExternalMiddlewareConfig } from "@opennextjs/core/types/open-next.js"; import type { OpenNextConfig } from "../../../api/config.js"; diff --git a/packages/cloudflare/src/cli/build/utils/middleware.ts b/packages/cloudflare/src/cli/build/utils/middleware.ts index 7b6ff4f1..91822637 100644 --- a/packages/cloudflare/src/cli/build/utils/middleware.ts +++ b/packages/cloudflare/src/cli/build/utils/middleware.ts @@ -1,7 +1,7 @@ import path from "node:path"; -import { loadFunctionsConfigManifest, loadMiddlewareManifest } from "@opennextjs/aws/adapters/config/util.js"; -import * as buildHelper from "@opennextjs/aws/build/helper.js"; +import { loadFunctionsConfigManifest, loadMiddlewareManifest } from "@opennextjs/core/adapters/config/util.js"; +import * as buildHelper from "@opennextjs/core/build/helper.js"; /** * Returns whether the project is using a Node.js middleware. diff --git a/packages/cloudflare/src/cli/build/utils/needs-experimental-react.ts b/packages/cloudflare/src/cli/build/utils/needs-experimental-react.ts index dad47860..af8d0be9 100644 --- a/packages/cloudflare/src/cli/build/utils/needs-experimental-react.ts +++ b/packages/cloudflare/src/cli/build/utils/needs-experimental-react.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "@opennextjs/aws/types/next-types.js"; +import type { NextConfig } from "@opennextjs/core/types/next-types.js"; interface ExtendedNextConfig extends NextConfig { experimental: { diff --git a/packages/cloudflare/src/cli/build/utils/test-patch.ts b/packages/cloudflare/src/cli/build/utils/test-patch.ts index 1a9602fd..c4ad51c2 100644 --- a/packages/cloudflare/src/cli/build/utils/test-patch.ts +++ b/packages/cloudflare/src/cli/build/utils/test-patch.ts @@ -1,4 +1,4 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { createPatch } from "diff"; /** diff --git a/packages/cloudflare/src/cli/build/utils/version.ts b/packages/cloudflare/src/cli/build/utils/version.ts index 55b01246..30d08ef0 100644 --- a/packages/cloudflare/src/cli/build/utils/version.ts +++ b/packages/cloudflare/src/cli/build/utils/version.ts @@ -8,6 +8,6 @@ export function getVersion() { const pkgJson = require(join(__dirname, "../../../../package.json")); return { cloudflare: pkgJson.version, - aws: pkgJson.dependencies["@opennextjs/aws"], + aws: pkgJson.dependencies["@opennextjs/core"], }; } diff --git a/packages/cloudflare/src/cli/build/utils/workerd.ts b/packages/cloudflare/src/cli/build/utils/workerd.ts index 5c2d8a58..a7a12331 100644 --- a/packages/cloudflare/src/cli/build/utils/workerd.ts +++ b/packages/cloudflare/src/cli/build/utils/workerd.ts @@ -1,10 +1,10 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { loadConfig } from "@opennextjs/aws/adapters/config/util.js"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; -import logger from "@opennextjs/aws/logger.js"; -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { loadConfig } from "@opennextjs/core/adapters/config/util.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; +import logger from "@opennextjs/core/logger.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; /** * This function transforms the exports (or imports) object from the package.json diff --git a/packages/cloudflare/src/cli/commands/build.spec.ts b/packages/cloudflare/src/cli/commands/build.spec.ts index 1e1572b9..672d4d32 100644 --- a/packages/cloudflare/src/cli/commands/build.spec.ts +++ b/packages/cloudflare/src/cli/commands/build.spec.ts @@ -1,4 +1,4 @@ -import logger from "@opennextjs/aws/logger.js"; +import logger from "@opennextjs/core/logger.js"; import { afterEach, describe, expect, it, vi } from "vitest"; import { askConfirmation } from "../utils/ask-confirmation.js"; @@ -7,7 +7,7 @@ import { createWranglerConfigFile } from "../utils/create-wrangler-config.js"; import { buildCommand } from "./build.js"; // Mock logger -vi.mock("@opennextjs/aws/logger.js", () => ({ +vi.mock("@opennextjs/core/logger.js", () => ({ default: { info: vi.fn(), warn: vi.fn(), diff --git a/packages/cloudflare/src/cli/commands/build.ts b/packages/cloudflare/src/cli/commands/build.ts index 79fee57b..c7b76fc5 100644 --- a/packages/cloudflare/src/cli/commands/build.ts +++ b/packages/cloudflare/src/cli/commands/build.ts @@ -1,6 +1,6 @@ import { createRequire } from "node:module"; -import logger from "@opennextjs/aws/logger.js"; +import logger from "@opennextjs/core/logger.js"; import type yargs from "yargs"; import { build as buildImpl } from "../build/build.js"; diff --git a/packages/cloudflare/src/cli/commands/deploy.ts b/packages/cloudflare/src/cli/commands/deploy.ts index 0ca121b6..bfa4705f 100644 --- a/packages/cloudflare/src/cli/commands/deploy.ts +++ b/packages/cloudflare/src/cli/commands/deploy.ts @@ -1,4 +1,4 @@ -import logger from "@opennextjs/aws/logger.js"; +import logger from "@opennextjs/core/logger.js"; import type yargs from "yargs"; import { DEPLOYMENT_MAPPING_ENV_NAME } from "../templates/skew-protection.js"; diff --git a/packages/cloudflare/src/cli/commands/migrate.ts b/packages/cloudflare/src/cli/commands/migrate.ts index 40e9110c..c766c831 100644 --- a/packages/cloudflare/src/cli/commands/migrate.ts +++ b/packages/cloudflare/src/cli/commands/migrate.ts @@ -8,8 +8,8 @@ import { findNextConfig, findPackagerAndRoot, getNextVersion, -} from "@opennextjs/aws/build/helper.js"; -import logger from "@opennextjs/aws/logger.js"; +} from "@opennextjs/core/build/helper.js"; +import logger from "@opennextjs/core/logger.js"; import type yargs from "yargs"; import { askConfirmation } from "../utils/ask-confirmation.js"; diff --git a/packages/cloudflare/src/cli/commands/populate-cache.spec.ts b/packages/cloudflare/src/cli/commands/populate-cache.spec.ts index 849f7eae..9127dd1b 100644 --- a/packages/cloudflare/src/cli/commands/populate-cache.spec.ts +++ b/packages/cloudflare/src/cli/commands/populate-cache.spec.ts @@ -1,7 +1,7 @@ import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; import mockFs from "mock-fs"; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest"; diff --git a/packages/cloudflare/src/cli/commands/populate-cache.ts b/packages/cloudflare/src/cli/commands/populate-cache.ts index 496b6e5c..c60aa20c 100644 --- a/packages/cloudflare/src/cli/commands/populate-cache.ts +++ b/packages/cloudflare/src/cli/commands/populate-cache.ts @@ -3,15 +3,15 @@ import fsp from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; -import logger from "@opennextjs/aws/logger.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; +import logger from "@opennextjs/core/logger.js"; import type { IncludedIncrementalCache, IncludedTagCache, LazyLoadedOverride, OpenNextConfig, -} from "@opennextjs/aws/types/open-next.js"; -import type { IncrementalCache, TagCache } from "@opennextjs/aws/types/overrides.js"; +} from "@opennextjs/core/types/open-next.js"; +import type { IncrementalCache, TagCache } from "@opennextjs/core/types/overrides.js"; import { globSync } from "glob"; import { tqdm } from "ts-tqdm"; import type { Unstable_Config as WranglerConfig } from "wrangler"; diff --git a/packages/cloudflare/src/cli/commands/preview.ts b/packages/cloudflare/src/cli/commands/preview.ts index 82502310..67842dd9 100644 --- a/packages/cloudflare/src/cli/commands/preview.ts +++ b/packages/cloudflare/src/cli/commands/preview.ts @@ -1,4 +1,4 @@ -import logger from "@opennextjs/aws/logger.js"; +import logger from "@opennextjs/core/logger.js"; import type yargs from "yargs"; import { populateCache, withPopulateCacheOptions } from "./populate-cache.js"; diff --git a/packages/cloudflare/src/cli/commands/skew-protection.ts b/packages/cloudflare/src/cli/commands/skew-protection.ts index 914fcc66..478d9339 100644 --- a/packages/cloudflare/src/cli/commands/skew-protection.ts +++ b/packages/cloudflare/src/cli/commands/skew-protection.ts @@ -24,9 +24,9 @@ /* oxlint-disable @typescript-eslint/no-explicit-any */ import path from "node:path"; -import { loadConfig } from "@opennextjs/aws/adapters/config/util.js"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; -import logger from "@opennextjs/aws/logger.js"; +import { loadConfig } from "@opennextjs/core/adapters/config/util.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; +import logger from "@opennextjs/core/logger.js"; import { Cloudflare, NotFoundError } from "cloudflare"; import type { VersionGetResponse } from "cloudflare/resources/workers/scripts/versions.js"; diff --git a/packages/cloudflare/src/cli/commands/upload.ts b/packages/cloudflare/src/cli/commands/upload.ts index b94d2616..aa653d65 100644 --- a/packages/cloudflare/src/cli/commands/upload.ts +++ b/packages/cloudflare/src/cli/commands/upload.ts @@ -1,4 +1,4 @@ -import logger from "@opennextjs/aws/logger.js"; +import logger from "@opennextjs/core/logger.js"; import type yargs from "yargs"; import { DEPLOYMENT_MAPPING_ENV_NAME } from "../templates/skew-protection.js"; diff --git a/packages/cloudflare/src/cli/commands/utils/helpers.ts b/packages/cloudflare/src/cli/commands/utils/helpers.ts index 09e35a78..fe3859da 100644 --- a/packages/cloudflare/src/cli/commands/utils/helpers.ts +++ b/packages/cloudflare/src/cli/commands/utils/helpers.ts @@ -1,4 +1,4 @@ -import { type BuildOptions } from "@opennextjs/aws/build/helper.js"; +import { type BuildOptions } from "@opennextjs/core/build/helper.js"; import { getPlatformProxy, type GetPlatformProxyOptions } from "wrangler"; import { extractProjectEnvVars } from "../../utils/extract-project-env-vars.js"; diff --git a/packages/cloudflare/src/cli/commands/utils/run-wrangler.ts b/packages/cloudflare/src/cli/commands/utils/run-wrangler.ts index f062abf0..bebbc87b 100644 --- a/packages/cloudflare/src/cli/commands/utils/run-wrangler.ts +++ b/packages/cloudflare/src/cli/commands/utils/run-wrangler.ts @@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; -import { compareSemver } from "@opennextjs/aws/build/helper.js"; +import { compareSemver } from "@opennextjs/core/build/helper.js"; export type PackagerDetails = { packager: "npm" | "pnpm" | "yarn" | "bun"; diff --git a/packages/cloudflare/src/cli/commands/utils/utils.spec.ts b/packages/cloudflare/src/cli/commands/utils/utils.spec.ts index 3781ee82..e4baa534 100644 --- a/packages/cloudflare/src/cli/commands/utils/utils.spec.ts +++ b/packages/cloudflare/src/cli/commands/utils/utils.spec.ts @@ -16,7 +16,7 @@ vi.mock("node:fs", async (importOriginal) => { }); // Mock logger -vi.mock("@opennextjs/aws/logger.js", () => ({ +vi.mock("@opennextjs/core/logger.js", () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), setLevel: vi.fn() }, })); @@ -25,7 +25,7 @@ const mockCompileOpenNextConfig = vi.fn(async () => ({ config: { default: {} }, buildDir: "/build", })); -vi.mock("@opennextjs/aws/build/compileConfig.js", () => ({ +vi.mock("@opennextjs/core/build/compileConfig.js", () => ({ compileOpenNextConfig: (...args: unknown[]) => mockCompileOpenNextConfig(...args), })); @@ -51,13 +51,13 @@ vi.mock("wrangler", () => ({ })); // Mock build utils -vi.mock("@opennextjs/aws/build/utils.js", () => ({ +vi.mock("@opennextjs/core/build/utils.js", () => ({ printHeader: vi.fn(), showWarningOnWindows: vi.fn(), })); // Mock build helper -vi.mock("@opennextjs/aws/build/helper.js", () => ({ +vi.mock("@opennextjs/core/build/helper.js", () => ({ normalizeOptions: vi.fn(() => ({})), })); diff --git a/packages/cloudflare/src/cli/commands/utils/utils.ts b/packages/cloudflare/src/cli/commands/utils/utils.ts index 0bee952d..50d53ca3 100644 --- a/packages/cloudflare/src/cli/commands/utils/utils.ts +++ b/packages/cloudflare/src/cli/commands/utils/utils.ts @@ -3,10 +3,10 @@ import { createRequire } from "node:module"; import path from "node:path"; import url from "node:url"; -import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js"; -import { normalizeOptions } from "@opennextjs/aws/build/helper.js"; -import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js"; -import logger from "@opennextjs/aws/logger.js"; +import { compileOpenNextConfig } from "@opennextjs/core/build/compileConfig.js"; +import { normalizeOptions } from "@opennextjs/core/build/helper.js"; +import { printHeader, showWarningOnWindows } from "@opennextjs/core/build/utils.js"; +import logger from "@opennextjs/core/logger.js"; import { unstable_readConfig } from "wrangler"; import type yargs from "yargs"; @@ -82,7 +82,7 @@ export async function retrieveCompiledConfig() { export function getNormalizedOptions(config: OpenNextConfig, buildDir = nextAppDir) { const require = createRequire(import.meta.url); - const openNextDistDir = path.dirname(require.resolve("@opennextjs/aws/index.js")); + const openNextDistDir = path.dirname(require.resolve("@opennextjs/core/index.js")); const options = normalizeOptions(config, openNextDistDir, buildDir); logger.setLevel(options.debug ? "debug" : "info"); diff --git a/packages/cloudflare/src/cli/index.ts b/packages/cloudflare/src/cli/index.ts index 3646f316..da33bb5a 100644 --- a/packages/cloudflare/src/cli/index.ts +++ b/packages/cloudflare/src/cli/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import logger from "@opennextjs/aws/logger.js"; +import logger from "@opennextjs/core/logger.js"; import yargs from "yargs"; import { getVersion } from "./build/utils/version.js"; diff --git a/packages/cloudflare/src/cli/templates/images.ts b/packages/cloudflare/src/cli/templates/images.ts index 1dae02f2..888dc741 100644 --- a/packages/cloudflare/src/cli/templates/images.ts +++ b/packages/cloudflare/src/cli/templates/images.ts @@ -1,4 +1,4 @@ -import { error, warn } from "@opennextjs/aws/adapters/logger.js"; +import { error, warn } from "@opennextjs/core/adapters/logger.js"; export type RemotePattern = { protocol?: "http" | "https"; diff --git a/packages/cloudflare/src/cli/templates/worker.ts b/packages/cloudflare/src/cli/templates/worker.ts index b304c8b5..4465dbf6 100644 --- a/packages/cloudflare/src/cli/templates/worker.ts +++ b/packages/cloudflare/src/cli/templates/worker.ts @@ -1,4 +1,4 @@ -import type { InternalResult } from "@opennextjs/aws/types/open-next.js"; +import type { InternalResult } from "@opennextjs/core/types/open-next.js"; //@ts-expect-error: Will be resolved by wrangler build import { handleCdnCgiImageRequest, handleImageRequest } from "./cloudflare/images.js"; diff --git a/packages/cloudflare/src/cli/utils/create-open-next-config.ts b/packages/cloudflare/src/cli/utils/create-open-next-config.ts index 03d576ec..1c383e2d 100644 --- a/packages/cloudflare/src/cli/utils/create-open-next-config.ts +++ b/packages/cloudflare/src/cli/utils/create-open-next-config.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { getPackageTemplatesDirPath } from "../../utils/get-package-templates-dir-path.js"; diff --git a/packages/cloudflare/src/cli/utils/create-wrangler-config.ts b/packages/cloudflare/src/cli/utils/create-wrangler-config.ts index 0dae0398..4b767e21 100644 --- a/packages/cloudflare/src/cli/utils/create-wrangler-config.ts +++ b/packages/cloudflare/src/cli/utils/create-wrangler-config.ts @@ -2,7 +2,7 @@ import assert from "node:assert"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { findPackagerAndRoot } from "@opennextjs/aws/build/helper.js"; +import { findPackagerAndRoot } from "@opennextjs/core/build/helper.js"; import Cloudflare from "cloudflare"; import { type CommentObject, parse, stringify } from "comment-json"; diff --git a/packages/cloudflare/src/cli/utils/extract-project-env-vars.ts b/packages/cloudflare/src/cli/utils/extract-project-env-vars.ts index cc98c07a..36f625db 100644 --- a/packages/cloudflare/src/cli/utils/extract-project-env-vars.ts +++ b/packages/cloudflare/src/cli/utils/extract-project-env-vars.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { parse } from "@dotenvx/dotenvx"; -import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; +import type { BuildOptions } from "@opennextjs/core/build/helper.js"; function readEnvFile(filePath: string) { if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { diff --git a/packages/cloudflare/src/cli/utils/needs-experimental-react.ts b/packages/cloudflare/src/cli/utils/needs-experimental-react.ts index dad47860..af8d0be9 100644 --- a/packages/cloudflare/src/cli/utils/needs-experimental-react.ts +++ b/packages/cloudflare/src/cli/utils/needs-experimental-react.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "@opennextjs/aws/types/next-types.js"; +import type { NextConfig } from "@opennextjs/core/types/next-types.js"; interface ExtendedNextConfig extends NextConfig { experimental: { diff --git a/packages/cloudflare/src/cli/utils/nextjs-support.ts b/packages/cloudflare/src/cli/utils/nextjs-support.ts index ba845fa8..c8e8fed8 100644 --- a/packages/cloudflare/src/cli/utils/nextjs-support.ts +++ b/packages/cloudflare/src/cli/utils/nextjs-support.ts @@ -1,5 +1,5 @@ -import { compareSemver } from "@opennextjs/aws/build/helper.js"; -import logger from "@opennextjs/aws/logger.js"; +import { compareSemver } from "@opennextjs/core/build/helper.js"; +import logger from "@opennextjs/core/logger.js"; export async function ensureNextjsVersionSupported({ nextVersion }: { nextVersion: string }) { if (compareSemver(nextVersion, "<", "14.2.0")) { diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..0bcb561c --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,60 @@ +{ + "name": "@opennextjs/core", + "version": "0.1.0", + "description": "OpenNext core - platform-agnostic Next.js adapter infrastructure", + "keywords": ["next.js", "adapter", "serverless", "opennext"], + "homepage": "https://opennext.js.org", + "bugs": { + "url": "https://github.com/opennextjs/opennextjs-aws/issues" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/opennextjs/opennextjs-aws", + "directory": "packages/core" + }, + "files": ["dist"], + "type": "module", + "typesVersions": { + "*": { + "*": ["dist/*"] + } + }, + "exports": { + "./*": { + "types": "./dist/*.d.ts", + "default": "./dist/*" + } + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc && tsc-alias", + "dev": "concurrently \"tsc -w\" \"tsc-alias -w\"", + "ts:check": "tsc --noEmit" + }, + "dependencies": { + "@ast-grep/napi": "^0.40.5", + "@node-minify/core": "^8.0.6", + "@node-minify/terser": "^8.0.6", + "@tsconfig/node18": "^1.0.3", + "chalk": "^5.6.2", + "cookie": "^1.0.2", + "esbuild": "catalog:aws", + "express": "^5.2.1", + "path-to-regexp": "^6.3.0", + "urlpattern-polyfill": "^10.1.0", + "yaml": "^2.8.1" + }, + "devDependencies": { + "@types/express": "5.0.6", + "@types/node": "catalog:aws", + "concurrently": "^9.2.1", + "tsc-alias": "^1.8.16", + "typescript": "catalog:aws" + }, + "peerDependencies": { + "next": "^16.0.10" + } +} diff --git a/packages/core/src/adapters/cache.ts b/packages/core/src/adapters/cache.ts new file mode 100644 index 00000000..0ac93ae6 --- /dev/null +++ b/packages/core/src/adapters/cache.ts @@ -0,0 +1,443 @@ +import type { CacheHandlerValue, IncrementalCacheContext, IncrementalCacheValue } from "@/types/cache"; +import { getTagsFromValue, hasBeenRevalidated, writeTags } from "@/utils/cache"; + +import { isBinaryContentType } from "../utils/binary"; + +import { debug, error, warn } from "./logger"; + +export const SOFT_TAG_PREFIX = "_N_T_/"; + +function isFetchCache(options?: { kindHint?: "app" | "pages" | "fetch"; kind?: "FETCH" }): boolean { + if (typeof options === "object") { + return options.kindHint === "fetch" || options.kind === "FETCH"; + } + return false; +} +// We need to use globalThis client here as this class can be defined at load time in next 12 but client is not available at load time +export default class Cache { + public async get( + key: string, + // fetchCache is for next 13.5 and above, kindHint is for next 14 and above and boolean is for earlier versions + options?: { + kindHint?: "app" | "pages" | "fetch"; + tags?: string[]; + softTags?: string[]; + kind?: "FETCH"; + } + ) { + if (globalThis.openNextConfig?.dangerous?.disableIncrementalCache) { + return null; + } + + const softTags = typeof options === "object" ? options.softTags : []; + const tags = typeof options === "object" ? options.tags : []; + return isFetchCache(options) ? this.getFetchCache(key, softTags, tags) : this.getIncrementalCache(key); + } + + async getFetchCache(key: string, softTags?: string[], tags?: string[]) { + debug("get fetch cache", { key, softTags, tags }); + try { + const cachedEntry = await globalThis.incrementalCache.get(key, "fetch"); + + if (cachedEntry?.value === undefined) return null; + + const _tags = [...(tags ?? []), ...(softTags ?? [])]; + const _lastModified = cachedEntry.lastModified ?? Date.now(); + const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache + ? false + : await hasBeenRevalidated(key, _tags, cachedEntry); + + if (_hasBeenRevalidated) return null; + + // For cases where we don't have tags, we need to ensure that the soft tags are not being revalidated + // We only need to check for the path as it should already contain all the tags + if ((tags ?? []).length === 0) { + // Then we need to find the path for the given key + const path = softTags?.find( + (tag) => tag.startsWith(SOFT_TAG_PREFIX) && !tag.endsWith("layout") && !tag.endsWith("page") + ); + if (path) { + const hasPathBeenUpdated = cachedEntry.shouldBypassTagCache + ? false + : await hasBeenRevalidated(path.replace(SOFT_TAG_PREFIX, ""), [], cachedEntry); + if (hasPathBeenUpdated) { + // In case the path has been revalidated, we don't want to use the fetch cache + return null; + } + } + } + + return { + lastModified: _lastModified, + value: cachedEntry.value, + } as CacheHandlerValue; + } catch (e) { + // We can usually ignore errors here as they are usually due to cache not being found + debug("Failed to get fetch cache", e); + return null; + } + } + + async getIncrementalCache(key: string): Promise { + try { + const cachedEntry = await globalThis.incrementalCache.get(key, "cache"); + + if (!cachedEntry?.value) { + return null; + } + + const cacheData = cachedEntry.value; + + const meta = cacheData.meta; + const tags = getTagsFromValue(cacheData); + const _lastModified = cachedEntry.lastModified ?? Date.now(); + const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache + ? false + : await hasBeenRevalidated(key, tags, cachedEntry); + if (_hasBeenRevalidated) return null; + + const store = globalThis.__openNextAls.getStore(); + if (store) { + store.lastModified = _lastModified; + } + + if (cacheData?.type === "route") { + return { + lastModified: _lastModified, + value: { + kind: "APP_ROUTE", + body: Buffer.from( + cacheData.body ?? Buffer.alloc(0), + isBinaryContentType(String(meta?.headers?.["content-type"])) ? "base64" : "utf8" + ), + status: meta?.status, + headers: meta?.headers, + }, + } as CacheHandlerValue; + } + if (cacheData?.type === "page" || cacheData?.type === "app") { + if (cacheData?.type === "app") { + const segmentData = new Map(); + if (cacheData.segmentData) { + for (const [segmentPath, segmentContent] of Object.entries(cacheData.segmentData ?? {})) { + segmentData.set(segmentPath, Buffer.from(segmentContent)); + } + } + return { + lastModified: _lastModified, + value: { + kind: "APP_PAGE", + html: cacheData.html, + rscData: Buffer.from(cacheData.rsc), + status: meta?.status, + headers: meta?.headers, + postponed: meta?.postponed, + segmentData, + }, + } as CacheHandlerValue; + } + return { + lastModified: _lastModified, + value: { + kind: "PAGES", + html: cacheData.html, + pageData: cacheData.json, + status: meta?.status, + headers: meta?.headers, + }, + } as CacheHandlerValue; + } + if (cacheData?.type === "redirect") { + return { + lastModified: _lastModified, + value: { + kind: "REDIRECT", + props: cacheData.props, + }, + } as CacheHandlerValue; + } + warn("Unknown cache type", cacheData); + return null; + } catch (e) { + // We can usually ignore errors here as they are usually due to cache not being found + debug("Failed to get body cache", e); + return null; + } + } + + async set(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext): Promise { + if (globalThis.openNextConfig?.dangerous?.disableIncrementalCache) { + return; + } + // This one might not even be necessary anymore + // Better be safe than sorry + const detachedPromise = globalThis.__openNextAls.getStore()?.pendingPromiseRunner.withResolvers(); + try { + if (data === null || data === undefined) { + await globalThis.incrementalCache.delete(key); + } else { + const revalidate = this.extractRevalidateForSet(ctx); + switch (data.kind) { + case "ROUTE": + case "APP_ROUTE": { + const { body, status, headers } = data; + await globalThis.incrementalCache.set( + key, + { + type: "route", + body: body.toString(isBinaryContentType(String(headers["content-type"])) ? "base64" : "utf8"), + meta: { + status, + headers, + }, + revalidate, + }, + "cache" + ); + break; + } + case "PAGE": + case "PAGES": { + const { html, pageData, status, headers } = data; + const isAppPath = typeof pageData === "string"; + if (isAppPath) { + await globalThis.incrementalCache.set( + key, + { + type: "app", + html, + rsc: pageData, + meta: { + status, + headers, + }, + revalidate, + }, + "cache" + ); + } else { + await globalThis.incrementalCache.set( + key, + { + type: "page", + html, + json: pageData, + revalidate, + }, + "cache" + ); + } + break; + } + case "APP_PAGE": { + const { html, rscData, headers, status, segmentData, postponed } = data; + const segmentToWrite: Record = {}; + if (segmentData) { + for (const [segmentPath, segmentContent] of segmentData.entries()) { + segmentToWrite[segmentPath] = segmentContent.toString("utf8"); + } + } + await globalThis.incrementalCache.set( + key, + { + type: "app", + html, + rsc: rscData.toString("utf8"), + meta: { + status, + headers, + postponed, + }, + revalidate, + segmentData: segmentData ? segmentToWrite : undefined, + }, + "cache" + ); + break; + } + case "FETCH": + await globalThis.incrementalCache.set(key, data, "fetch"); + break; + case "REDIRECT": + await globalThis.incrementalCache.set( + key, + { + type: "redirect", + props: data.props, + revalidate, + }, + "cache" + ); + break; + case "IMAGE": + // Not implemented + break; + } + } + + await this.updateTagsOnSet(key, data, ctx); + debug("Finished setting cache"); + } catch (e) { + error("Failed to set cache", e); + } finally { + // We need to resolve the promise even if there was an error + detachedPromise?.resolve(); + } + } + + public async revalidateTag(tags: string | string[]) { + const config = globalThis.openNextConfig.dangerous; + if (config?.disableTagCache || config?.disableIncrementalCache) { + return; + } + const _tags = Array.isArray(tags) ? tags : [tags]; + if (_tags.length === 0) { + return; + } + + try { + if (globalThis.tagCache.mode === "nextMode") { + const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? []; + + await writeTags(_tags); + if (paths.length > 0) { + // TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it + // It also means that we'll need to provide the tags used in every request to the wrapper or converter. + await globalThis.cdnInvalidationHandler.invalidatePaths( + paths.map((path) => ({ + initialPath: path, + rawPath: path, + resolvedRoutes: [ + { + route: path, + // TODO: ideally here we should check if it's an app router page or route + type: "app", + isFallback: false, + }, + ], + })) + ); + } + return; + } + + for (const tag of _tags) { + debug("revalidateTag", tag); + // Find all keys with the given tag + const paths = await globalThis.tagCache.getByTag(tag); + debug("Items", paths); + const toInsert = paths.map((path) => ({ + path, + tag, + })); + + // If the tag is a soft tag, we should also revalidate the hard tags + if (tag.startsWith(SOFT_TAG_PREFIX)) { + for (const path of paths) { + // We need to find all hard tags for a given path + const _tags = await globalThis.tagCache.getByPath(path); + const hardTags = _tags.filter((t) => !t.startsWith(SOFT_TAG_PREFIX)); + // For every hard tag, we need to find all paths and revalidate them + for (const hardTag of hardTags) { + const _paths = await globalThis.tagCache.getByTag(hardTag); + debug({ hardTag, _paths }); + toInsert.push( + ..._paths.map((path) => ({ + path, + tag: hardTag, + })) + ); + } + } + } + + // Update all keys with the given tag with revalidatedAt set to now + await writeTags(toInsert); + + // We can now invalidate all paths in the CDN + // This only applies to `revalidateTag`, not to `res.revalidate()` + const uniquePaths = Array.from( + new Set( + toInsert + // We need to filter fetch cache key as they are not in the CDN + .filter((t) => t.tag.startsWith(SOFT_TAG_PREFIX)) + .map((t) => `/${t.path}`) + ) + ); + if (uniquePaths.length > 0) { + await globalThis.cdnInvalidationHandler.invalidatePaths( + uniquePaths.map((path) => ({ + initialPath: path, + rawPath: path, + resolvedRoutes: [ + { + route: path, + // TODO: ideally here we should check if it's an app router page or route + type: "app", + isFallback: false, + }, + ], + })) + ); + } + } + } catch (e) { + error("Failed to revalidate tag", e); + } + } + + // TODO: We should delete/update tags in this method + // This will require an update to the tag cache interface + private async updateTagsOnSet(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext) { + if ( + globalThis.openNextConfig.dangerous?.disableTagCache || + globalThis.tagCache.mode === "nextMode" || + // Here it means it's a delete + !data + ) { + return; + } + // Write derivedTags to the tag cache + // If we use an in house version of getDerivedTags in build we should use it here instead of next's one + const derivedTags: string[] = + data?.kind === "FETCH" + ? //@ts-expect-error - On older versions of next, ctx was a number, but for these cases we use data?.data?.tags + (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility + : data?.kind === "PAGE" + ? (data.headers?.["x-next-cache-tags"]?.split(",") ?? []) + : []; + debug("derivedTags", derivedTags); + + // Get all tags stored in dynamodb for the given key + // If any of the derived tags are not stored in dynamodb for the given key, write them + const storedTags = await globalThis.tagCache.getByPath(key); + const tagsToWrite = derivedTags.filter((tag) => !storedTags.includes(tag)); + if (tagsToWrite.length > 0) { + await writeTags( + tagsToWrite.map((tag) => ({ + path: key, + tag: tag, + // In case the tags are not there we just need to create them + // but we don't want them to return from `getLastModified` as they are not stale + revalidatedAt: 1, + })) + ); + } + } + + private extractRevalidateForSet(ctx?: IncrementalCacheContext): number | false | undefined { + if (ctx === undefined) { + return undefined; + } + if (typeof ctx === "number" || ctx === false) { + return ctx; + } + if ("revalidate" in ctx) { + return ctx.revalidate; + } + if ("cacheControl" in ctx) { + return ctx.cacheControl?.revalidate; + } + return undefined; + } +} diff --git a/packages/core/src/adapters/composable-cache.ts b/packages/core/src/adapters/composable-cache.ts new file mode 100644 index 00000000..a6fb19c3 --- /dev/null +++ b/packages/core/src/adapters/composable-cache.ts @@ -0,0 +1,135 @@ +import type { ComposableCacheEntry, ComposableCacheHandler } from "@/types/cache"; +import type { CacheValue } from "@/types/overrides"; +import { writeTags } from "@/utils/cache"; +import { fromReadableStream, toReadableStream } from "@/utils/stream"; + +import { debug } from "./logger"; + +const pendingWritePromiseMap = new Map>>(); + +export default { + async get(cacheKey: string) { + try { + // We first check if we have a pending write for this cache key + // If we do, we return the pending promise instead of fetching the cache + if (pendingWritePromiseMap.has(cacheKey)) { + const stored = pendingWritePromiseMap.get(cacheKey); + if (stored) { + return stored.then((entry) => ({ + ...entry, + value: toReadableStream(entry.value), + })); + } + } + const result = await globalThis.incrementalCache.get(cacheKey, "composable"); + if (!result?.value?.value) { + return undefined; + } + + debug("composable cache result", result); + + // We need to check if the tags associated with this entry has been revalidated + if (globalThis.tagCache.mode === "nextMode" && result.value.tags.length > 0) { + const hasBeenRevalidated = result.shouldBypassTagCache + ? false + : await globalThis.tagCache.hasBeenRevalidated(result.value.tags, result.lastModified); + if (hasBeenRevalidated) return undefined; + } else if (globalThis.tagCache.mode === "original" || globalThis.tagCache.mode === undefined) { + const hasBeenRevalidated = result.shouldBypassTagCache + ? false + : (await globalThis.tagCache.getLastModified(cacheKey, result.lastModified)) === -1; + if (hasBeenRevalidated) return undefined; + } + + return { + ...result.value, + value: toReadableStream(result.value.value), + }; + } catch (e) { + debug("Cannot read composable cache entry"); + return undefined; + } + }, + + async set(cacheKey: string, pendingEntry: Promise) { + const promiseEntry = pendingEntry.then(async (entry) => ({ + ...entry, + value: await fromReadableStream(entry.value), + })); + pendingWritePromiseMap.set(cacheKey, promiseEntry); + + const entry = await promiseEntry.finally(() => { + pendingWritePromiseMap.delete(cacheKey); + }); + await globalThis.incrementalCache.set( + cacheKey, + { + ...entry, + value: entry.value, + }, + "composable" + ); + if (globalThis.tagCache.mode === "original") { + const storedTags = await globalThis.tagCache.getByPath(cacheKey); + const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag)); + if (tagsToWrite.length > 0) { + await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey }))); + } + } + }, + + async refreshTags() { + // We don't do anything for now, do we want to do something here ??? + return; + }, + + /** + * The signature has changed in Next.js 16 + * - Before Next.js 16, the method takes `...tags: string[]` + * - From Next.js 16, the method takes `tags: string[]` + */ + async getExpiration(...tags: string[] | string[][]) { + if (globalThis.tagCache.mode === "nextMode") { + // Use `.flat()` to accommodate both signatures + return globalThis.tagCache.getLastRevalidated(tags.flat()); + } + // We always return 0 here, original tag cache are handled directly in the get part + // TODO: We need to test this more, i'm not entirely sure that this is working as expected + return 0; + }, + + /** + * This method is only used before Next.js 16 + */ + async expireTags(...tags: string[]) { + if (globalThis.tagCache.mode === "nextMode") { + return writeTags(tags); + } + const tagCache = globalThis.tagCache; + const revalidatedAt = Date.now(); + // For the original mode, we have more work to do here. + // We need to find all paths linked to to these tags + const pathsToUpdate = await Promise.all( + tags.map(async (tag) => { + const paths = await tagCache.getByTag(tag); + return paths.map((path) => ({ + path, + tag, + revalidatedAt, + })); + }) + ); + // We need to deduplicate paths, we use a set for that + const setToWrite = new Set<{ path: string; tag: string }>(); + for (const entry of pathsToUpdate.flat()) { + setToWrite.add(entry); + } + await writeTags(Array.from(setToWrite)); + }, + + // This one is necessary for older versions of next + async receiveExpiredTags(...tags: string[]) { + // This function does absolutely nothing + return; + }, +} satisfies ComposableCacheHandler; diff --git a/packages/core/src/adapters/config/index.ts b/packages/core/src/adapters/config/index.ts new file mode 100644 index 00000000..15011af5 --- /dev/null +++ b/packages/core/src/adapters/config/index.ts @@ -0,0 +1,41 @@ +import path from "node:path"; + +import { debug } from "../logger"; + +import { + loadAppPathRoutesManifest, + loadAppPathsManifest, + loadAppPathsManifestKeys, + loadBuildId, + loadConfig, + loadConfigHeaders, + loadFunctionsConfigManifest, + loadHtmlPages, + loadMiddlewareManifest, + loadPagesManifest, + loadPrerenderManifest, + loadRoutesManifest, +} from "./util.js"; + +export const NEXT_DIR = path.join(__dirname, ".next"); +export const OPEN_NEXT_DIR = path.join(__dirname, ".open-next"); + +debug({ NEXT_DIR, OPEN_NEXT_DIR }); + +export const NextConfig = /* @__PURE__ */ loadConfig(NEXT_DIR); +export const BuildId = /* @__PURE__ */ loadBuildId(NEXT_DIR); +export const HtmlPages = /* @__PURE__ */ loadHtmlPages(NEXT_DIR); +// export const PublicAssets = loadPublicAssets(OPEN_NEXT_DIR); +export const RoutesManifest = /* @__PURE__ */ loadRoutesManifest(NEXT_DIR); +export const ConfigHeaders = /* @__PURE__ */ loadConfigHeaders(NEXT_DIR); +export const PrerenderManifest = /* @__PURE__ */ loadPrerenderManifest(NEXT_DIR); +export const PagesManifest = /* @__PURE__ */ loadPagesManifest(NEXT_DIR); +export const AppPathsManifestKeys = /* @__PURE__ */ loadAppPathsManifestKeys(NEXT_DIR); +export const MiddlewareManifest = /* @__PURE__ */ loadMiddlewareManifest(NEXT_DIR); +export const AppPathsManifest = /* @__PURE__ */ loadAppPathsManifest(NEXT_DIR); +export const AppPathRoutesManifest = /* @__PURE__ */ loadAppPathRoutesManifest(NEXT_DIR); + +export const FunctionsConfigManifest = /* @__PURE__ */ loadFunctionsConfigManifest(NEXT_DIR); + +process.env.NEXT_BUILD_ID = BuildId; +process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId; diff --git a/packages/core/src/adapters/config/util.ts b/packages/core/src/adapters/config/util.ts new file mode 100644 index 00000000..d19fd06f --- /dev/null +++ b/packages/core/src/adapters/config/util.ts @@ -0,0 +1,135 @@ +import fs from "node:fs"; +import path from "node:path"; + +import type { + FunctionsConfigManifest, + MiddlewareManifest, + NextConfig, + PrerenderManifest, + RoutesManifest, +} from "@/types/next-types"; + +import type { PublicFiles } from "@/types/adapter"; + +export function loadConfig(nextDir: string) { + const filePath = path.join(nextDir, "required-server-files.json"); + const json = fs.readFileSync(filePath, "utf-8"); + const { config } = JSON.parse(json); + return config as NextConfig; +} +export function loadBuildId(nextDir: string) { + const filePath = path.join(nextDir, "BUILD_ID"); + return fs.readFileSync(filePath, "utf-8").trim(); +} + +export function loadPagesManifest(nextDir: string) { + const filePath = path.join(nextDir, "server/pages-manifest.json"); + const json = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(json) as Record; +} + +export function loadHtmlPages(nextDir: string) { + return Object.entries(loadPagesManifest(nextDir)) + .filter(([_, value]) => (value as string).endsWith(".html")) + .map(([key]) => key); +} + +export function loadPublicAssets(openNextDir: string) { + const filePath = path.join(openNextDir, "public-files.json"); + const json = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(json) as PublicFiles; +} + +export function loadRoutesManifest(nextDir: string) { + const filePath = path.join(nextDir, "routes-manifest.json"); + const json = fs.readFileSync(filePath, "utf-8"); + const routesManifest = JSON.parse(json) as RoutesManifest; + + const _dataRoutes = routesManifest.dataRoutes ?? []; + const dataRoutes = { + static: _dataRoutes.filter((r) => r.routeKeys === undefined), + dynamic: _dataRoutes.filter((r) => r.routeKeys !== undefined), + }; + + return { + basePath: routesManifest.basePath, + rewrites: Array.isArray(routesManifest.rewrites) + ? { beforeFiles: [], afterFiles: routesManifest.rewrites, fallback: [] } + : { + beforeFiles: routesManifest.rewrites.beforeFiles ?? [], + afterFiles: routesManifest.rewrites.afterFiles ?? [], + fallback: routesManifest.rewrites.fallback ?? [], + }, + redirects: routesManifest.redirects ?? [], + routes: { + static: routesManifest.staticRoutes ?? [], + dynamic: routesManifest.dynamicRoutes ?? [], + data: dataRoutes, + }, + locales: routesManifest.i18n?.locales ?? [], + }; +} + +export function loadConfigHeaders(nextDir: string) { + const filePath = path.join(nextDir, "routes-manifest.json"); + const json = fs.readFileSync(filePath, "utf-8"); + const routesManifest = JSON.parse(json) as RoutesManifest; + return routesManifest.headers; +} + +export function loadPrerenderManifest(nextDir: string): PrerenderManifest | undefined { + const filePath = path.join(nextDir, "prerender-manifest.json"); + if (!fs.existsSync(filePath)) { + return undefined; + } + const json = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(json); +} + +export function loadAppPathsManifest(nextDir: string) { + const appPathsManifestPath = path.join(nextDir, "server/app-paths-manifest.json"); + const appPathsManifestJson = fs.existsSync(appPathsManifestPath) + ? fs.readFileSync(appPathsManifestPath, "utf-8") + : "{}"; + return JSON.parse(appPathsManifestJson) as Record; +} + +export function loadAppPathRoutesManifest(nextDir: string): Record { + const appPathRoutesManifestPath = path.join(nextDir, "app-path-routes-manifest.json"); + if (fs.existsSync(appPathRoutesManifestPath)) { + return JSON.parse(fs.readFileSync(appPathRoutesManifestPath, "utf-8")); + } + return {}; +} + +export function loadAppPathsManifestKeys(nextDir: string) { + const appPathsManifest = loadAppPathsManifest(nextDir); + return Object.keys(appPathsManifest).map((key) => { + // Remove parallel route + let cleanedKey = key.replace(/\/@[^/]+/g, ""); + + // Remove group routes + cleanedKey = cleanedKey.replace(/\/\((?!\.)[^)]*\)/g, ""); + + // Remove /page suffix + cleanedKey = cleanedKey.replace(/\/page$/g, ""); + // We need to check if the cleaned key is empty because it means it's the root path + return cleanedKey === "" ? "/" : cleanedKey; + }); +} + +export function loadMiddlewareManifest(nextDir: string) { + const filePath = path.join(nextDir, "server/middleware-manifest.json"); + const json = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(json) as MiddlewareManifest; +} + +export function loadFunctionsConfigManifest(nextDir: string) { + const filePath = path.join(nextDir, "server/functions-config-manifest.json"); + try { + const json = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(json) as FunctionsConfigManifest; + } catch (e) { + return { functions: {}, version: 1 }; + } +} diff --git a/packages/core/src/adapters/edge-adapter.ts b/packages/core/src/adapters/edge-adapter.ts new file mode 100644 index 00000000..f6841c6b --- /dev/null +++ b/packages/core/src/adapters/edge-adapter.ts @@ -0,0 +1,74 @@ +import type { ReadableStream } from "node:stream/web"; + +import type { InternalEvent, InternalResult } from "@/types/open-next"; +import type { OpenNextHandlerOptions } from "@/types/overrides"; +import { runWithOpenNextRequestContext } from "@/utils/promise"; +import { emptyReadableStream } from "@/utils/stream"; + +// We import it like that so that the edge plugin can replace it +import { NextConfig } from "../adapters/config"; +import { createGenericHandler } from "../core/createGenericHandler"; +import { convertBodyToReadableStream } from "../core/routing/util"; +import { INTERNAL_EVENT_REQUEST_ID } from "../core/routingHandler"; + +globalThis.__openNextAls = new AsyncLocalStorage(); + +const defaultHandler = async ( + internalEvent: InternalEvent, + options?: OpenNextHandlerOptions +): Promise => { + globalThis.isEdgeRuntime = true; + + const requestId = globalThis.openNextConfig.middleware?.external + ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID] + : Math.random().toString(36); + + // We run everything in the async local storage context so that it is available in edge runtime functions + return runWithOpenNextRequestContext( + { isISRRevalidation: false, waitUntil: options?.waitUntil, requestId }, + async () => { + // @ts-expect-error - This is bundled + const handler = await import("./middleware.mjs"); + + const response: Response = await handler.default({ + headers: internalEvent.headers, + method: internalEvent.method || "GET", + nextConfig: { + basePath: NextConfig.basePath, + i18n: NextConfig.i18n, + trailingSlash: NextConfig.trailingSlash, + }, + url: internalEvent.url, + body: convertBodyToReadableStream(internalEvent.method, internalEvent.body), + }); + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + if (key.toLowerCase() === "set-cookie") { + responseHeaders[key] = responseHeaders[key] ? [...responseHeaders[key], value] : [value]; + } else { + responseHeaders[key] = value; + } + }); + + const body = (response.body as ReadableStream) ?? emptyReadableStream(); + + return { + type: "core", + statusCode: response.status, + headers: responseHeaders, + body: body, + // Do we need to handle base64 encoded response? + isBase64Encoded: false, + }; + } + ); +}; + +export const handler = await createGenericHandler({ + handler: defaultHandler, + type: "middleware", +}); + +export default { + fetch: handler, +}; diff --git a/packages/core/src/adapters/image-optimization-adapter.ts b/packages/core/src/adapters/image-optimization-adapter.ts new file mode 100644 index 00000000..4c648519 --- /dev/null +++ b/packages/core/src/adapters/image-optimization-adapter.ts @@ -0,0 +1,256 @@ +import { createHash } from "node:crypto"; +import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from "node:http"; +import https from "node:https"; +import path from "node:path"; +import type { Writable } from "node:stream"; + +// @ts-ignore +import { defaultConfig } from "next/dist/server/config-shared"; +import { + ImageOptimizerCache, + // @ts-ignore +} from "next/dist/server/image-optimizer"; +// @ts-ignore +import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta"; + +import { loadBuildId, loadConfig } from "@/config/util.js"; +import { OpenNextNodeResponse } from "@/http/openNextResponse.js"; +import type { InternalEvent, InternalResult, StreamCreator } from "@/types/open-next.js"; +import type { OpenNextHandlerOptions } from "@/types/overrides.js"; +import { emptyReadableStream, toReadableStream } from "@/utils/stream.js"; + +import { createGenericHandler } from "../core/createGenericHandler.js"; +import { resolveImageLoader } from "../core/resolve.js"; + +import { debug, error } from "./logger.js"; +import { optimizeImage } from "./plugins/image-optimization/image-optimization.js"; +import { setNodeEnv } from "./util.js"; + +setNodeEnv(); +const nextDir = path.join(__dirname, ".next"); +const config = loadConfig(nextDir); +const buildId = loadBuildId(nextDir); +const nextConfig = { + ...defaultConfig, + images: { + ...defaultConfig.images, + ...config.images, + }, +}; +debug("Init config", { + nextDir, + nextConfig, +}); + +///////////// +// Handler // +///////////// + +export const handler = await createGenericHandler({ + handler: defaultHandler, + type: "imageOptimization", +}); + +export async function defaultHandler( + event: InternalEvent, + options?: OpenNextHandlerOptions +): Promise { + // Images are handled via header and query param information. + debug("handler event", event); + const { headers, query: queryString } = event; + + try { + // Set the HOST environment variable to the host header if it is not set + // If it is set it is assumed to be set by the user and should be used instead + // It might be useful for cases where the user wants to use a different host than the one in the request + // It could even allow to have multiple hosts for the image optimization by setting the HOST environment variable in the wrapper for example + if (!process.env.HOST) { + const headersHost = headers["x-forwarded-host"] || headers.host; + process.env.HOST = headersHost; + } + + const imageParams = validateImageParams(headers, queryString === null ? undefined : queryString); + // We return a 400 here if imageParams returns an errorMessage + // https://github.com/vercel/next.js/blob/512d8283054407ab92b2583ecce3b253c3be7b85/packages/next/src/server/next-server.ts#L937-L941 + if ("errorMessage" in imageParams) { + error("Error during validation of image params", imageParams.errorMessage); + return buildFailureResponse(imageParams.errorMessage, options?.streamCreator, 400); + } + let etag: string | undefined; + // We don't cache any images, so in order to be able to return 304 responses, we compute an ETag from what is assumed to be static + if (process.env.OPENNEXT_STATIC_ETAG) { + etag = computeEtag(imageParams); + } + if (etag && headers["if-none-match"] === etag) { + return { + statusCode: 304, + headers: {}, + body: emptyReadableStream(), + isBase64Encoded: false, + type: "core", + }; + } + const result = await optimizeImage(headers, imageParams, nextConfig, downloadHandler); + return buildSuccessResponse(result, options?.streamCreator, etag); + } catch (e: unknown) { + error("Failed to optimize image", e); + return buildFailureResponse("Internal server error", options?.streamCreator); + } +} + +////////////////////// +// Helper functions // +////////////////////// + +function validateImageParams(headers: OutgoingHttpHeaders, query?: InternalEvent["query"]) { + // Next.js checks if external image URL matches the + // `images.remotePatterns` + const imageParams = ImageOptimizerCache.validateParams( + // @ts-ignore + { headers }, + query, + nextConfig, + false + ); + debug("image params", imageParams); + return imageParams; +} + +function computeEtag(imageParams: { href: string; width: number; quality: number }) { + return createHash("sha1") + .update( + JSON.stringify({ + href: imageParams.href, + width: imageParams.width, + quality: imageParams.quality, + buildId, + }) + ) + .digest("base64"); +} + +type ImageOptimizeResult = { + contentType: string; + buffer: Buffer; + maxAge: number; +}; + +function buildSuccessResponse( + imageOptimizeResult: ImageOptimizeResult, + streamCreator?: StreamCreator, + etag?: string +): InternalResult { + const headers: Record = { + Vary: "Accept", + "Content-Type": imageOptimizeResult.contentType, + "Cache-Control": `public,max-age=${imageOptimizeResult.maxAge},immutable`, + }; + debug("result", imageOptimizeResult); + if (etag) { + headers.ETag = etag; + } + + if (streamCreator) { + const response = new OpenNextNodeResponse( + () => void 0, + async () => void 0, + streamCreator + ); + response.writeHead(200, headers); + response.end(imageOptimizeResult.buffer); + } + + return { + type: "core", + statusCode: 200, + body: toReadableStream(imageOptimizeResult.buffer, true), + isBase64Encoded: true, + headers, + }; +} + +function buildFailureResponse( + errorMessage: string, + streamCreator?: StreamCreator, + statusCode = 500 +): InternalResult { + debug(errorMessage, statusCode); + if (streamCreator) { + const response = new OpenNextNodeResponse( + () => void 0, + async () => void 0, + streamCreator + ); + response.writeHead(statusCode, { + Vary: "Accept", + "Cache-Control": "public,max-age=60,immutable", + }); + response.end(errorMessage); + } + return { + type: "core", + isBase64Encoded: false, + statusCode: statusCode, + headers: { + Vary: "Accept", + // For failed images, allow client to retry after 1 minute. + "Cache-Control": "public,max-age=60,immutable", + }, + body: toReadableStream(errorMessage), + }; +} + +const loader = await resolveImageLoader(globalThis.openNextConfig.imageOptimization?.loader ?? "s3"); + +async function downloadHandler(_req: IncomingMessage, res: ServerResponse, url?: NextUrlWithParsedQuery) { + // downloadHandler is called by Next.js. We don't call this function + // directly. + debug("downloadHandler url", url); + + // Reads the output from the Writable and writes to the response + const pipeRes = (w: Writable, res: ServerResponse) => { + w.pipe(res) + .once("close", () => { + res.statusCode = 200; + res.end(); + }) + .once("error", (err) => { + error("Failed to get image", err); + res.statusCode = 400; + res.end(); + }); + }; + + try { + // Case 1: remote image URL => download the image from the URL + if (url?.href?.toLowerCase().match(/^https?:\/\//)) { + pipeRes(https.get(url), res); + } + // Case 2: local image => download the image from S3 + else { + // Download image from S3 + // note: S3 expects keys without leading `/` + + const response = await loader.load(url?.href ?? ""); + + if (!response.body) { + throw new Error("Empty response body from the S3 request."); + } + + // @ts-ignore + pipeRes(response.body, res); + + // Respect the bucket file's content-type and cache-control + // imageOptimizer will use this to set the results.maxAge + if (response.contentType) { + res.setHeader("Content-Type", response.contentType); + } + if (response.cacheControl) { + res.setHeader("Cache-Control", response.cacheControl); + } + } + } catch (e: unknown) { + error("Failed to download image", e); + throw e; + } +} diff --git a/packages/core/src/adapters/logger.ts b/packages/core/src/adapters/logger.ts new file mode 100644 index 00000000..b79fcb2f --- /dev/null +++ b/packages/core/src/adapters/logger.ts @@ -0,0 +1,102 @@ +import { isOpenNextError } from "@/utils/error.js"; + +export function debug(...args: unknown[]) { + if (globalThis.openNextDebug) { + console.log(...args); + } +} + +export function warn(...args: unknown[]) { + console.warn(...args); +} + +interface AwsSdkClientCommandErrorLog { + clientName: string; + commandName: string; + error: Error & { Code?: string }; +} + +type AwsSdkClientCommandErrorInput = Pick & { + errorName: string; +}; + +const DOWNPLAYED_ERROR_LOGS: AwsSdkClientCommandErrorInput[] = [ + { + clientName: "S3Client", + commandName: "GetObjectCommand", + errorName: "NoSuchKey", + }, +]; + +const isAwsSdkClientCommandErrorLog = (errorLog: unknown): errorLog is AwsSdkClientCommandErrorLog => + errorLog !== null && + typeof errorLog === "object" && + "clientName" in errorLog && + "commandName" in errorLog && + "error" in errorLog; + +const isDownplayedErrorLog = (errorLog: unknown): boolean => { + if (!isAwsSdkClientCommandErrorLog(errorLog)) { + return false; + } + return DOWNPLAYED_ERROR_LOGS.some( + (downplayedInput) => + downplayedInput.clientName === errorLog.clientName && + downplayedInput.commandName === errorLog.commandName && + (downplayedInput.errorName === errorLog.error?.name || + downplayedInput.errorName === errorLog.error?.Code) + ); +}; + +export function error(...args: unknown[]) { + // we try to catch errors from the aws-sdk client and downplay some of them + if (args.some((arg) => isDownplayedErrorLog(arg))) { + return debug(...args); + } + if (args.some((arg) => isOpenNextError(arg))) { + // In case of an internal error, we log it with the appropriate log level + const error = args.find((arg) => isOpenNextError(arg))!; + if (error.logLevel < getOpenNextErrorLogLevel()) { + return; + } + if (error.logLevel === 0) { + // Display the name and the message instead of full Open Next errors. + // console.log is used so that logging does not depend on openNextDebug. + return console.log(...args.map((arg) => (isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg))); + } + if (error.logLevel === 1) { + // Display the name and the message instead of full Open Next errors. + return warn(...args.map((arg) => (isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg))); + } + return console.error(...args); + } + console.error(...args); +} + +export const awsLogger = { + trace: () => {}, + debug: () => {}, + info: debug, + warn, + error, +}; + +/** + * Retrieves the log level for internal errors from the + * OPEN_NEXT_ERROR_LOG_LEVEL environment variable. + * + * @returns The numerical log level 0 (debug), 1 (warn), or 2 (error) + */ +function getOpenNextErrorLogLevel(): number { + const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1"; + switch (strLevel.toLowerCase()) { + case "debug": + case "0": + return 0; + case "error": + case "2": + return 2; + default: + return 1; + } +} diff --git a/packages/core/src/adapters/middleware.ts b/packages/core/src/adapters/middleware.ts new file mode 100644 index 00000000..295ec809 --- /dev/null +++ b/packages/core/src/adapters/middleware.ts @@ -0,0 +1,127 @@ +import type { + ExternalMiddlewareConfig, + InternalEvent, + InternalResult, + MiddlewareResult, +} from "@/types/open-next"; +import type { OpenNextHandlerOptions } from "@/types/overrides"; +import { runWithOpenNextRequestContext } from "@/utils/promise"; + +import { debug, error } from "../adapters/logger"; +import { createGenericHandler } from "../core/createGenericHandler"; +import { + resolveAssetResolver, + resolveIncrementalCache, + resolveOriginResolver, + resolveProxyRequest, + resolveQueue, + resolveTagCache, +} from "../core/resolve"; +import { constructNextUrl } from "../core/routing/util"; +import routingHandler, { + INTERNAL_EVENT_REQUEST_ID, + INTERNAL_HEADER_REWRITE_STATUS_CODE, + INTERNAL_HEADER_INITIAL_URL, + INTERNAL_HEADER_RESOLVED_ROUTES, +} from "../core/routingHandler"; + +globalThis.internalFetch = fetch; +globalThis.__openNextAls = new AsyncLocalStorage(); + +const defaultHandler = async ( + internalEvent: InternalEvent, + options?: OpenNextHandlerOptions +): Promise => { + // We know that the middleware is external when this adapter is used + const middlewareConfig = globalThis.openNextConfig.middleware as ExternalMiddlewareConfig; + const originResolver = await resolveOriginResolver(middlewareConfig?.originResolver); + + const externalRequestProxy = await resolveProxyRequest(middlewareConfig?.override?.proxyExternalRequest); + + const assetResolver = await resolveAssetResolver(middlewareConfig?.assetResolver); + + globalThis.tagCache = await resolveTagCache(middlewareConfig?.override?.tagCache); + + globalThis.queue = await resolveQueue(middlewareConfig?.override?.queue); + + globalThis.incrementalCache = await resolveIncrementalCache(middlewareConfig?.override?.incrementalCache); + + const requestId = Math.random().toString(36); + + // We run everything in the async local storage context so that it is available in the external middleware + return runWithOpenNextRequestContext( + { + isISRRevalidation: internalEvent.headers["x-isr"] === "1", + waitUntil: options?.waitUntil, + requestId, + }, + async () => { + const result = await routingHandler(internalEvent, { assetResolver }); + if ("internalEvent" in result) { + debug("Middleware intercepted event", internalEvent); + if (!result.isExternalRewrite) { + const origin = await originResolver.resolve(result.internalEvent.rawPath); + return { + type: "middleware", + internalEvent: { + ...result.internalEvent, + headers: { + ...result.internalEvent.headers, + [INTERNAL_HEADER_INITIAL_URL]: internalEvent.url, + [INTERNAL_HEADER_RESOLVED_ROUTES]: JSON.stringify(result.resolvedRoutes), + [INTERNAL_EVENT_REQUEST_ID]: requestId, + [INTERNAL_HEADER_REWRITE_STATUS_CODE]: String(result.rewriteStatusCode), + }, + }, + isExternalRewrite: result.isExternalRewrite, + origin, + isISR: result.isISR, + initialURL: result.initialURL, + resolvedRoutes: result.resolvedRoutes, + initialResponse: result.initialResponse, + }; + } + try { + return externalRequestProxy.proxy(result.internalEvent); + } catch (e) { + error("External request failed.", e); + return { + type: "middleware", + internalEvent: { + ...result.internalEvent, + headers: { + ...result.internalEvent.headers, + [INTERNAL_EVENT_REQUEST_ID]: requestId, + }, + rawPath: "/500", + url: constructNextUrl(result.internalEvent.url, "/500"), + method: "GET", + }, + // On error we need to rewrite to the 500 page which is an internal rewrite + isExternalRewrite: false, + origin: false, + isISR: result.isISR, + initialURL: result.internalEvent.url, + resolvedRoutes: [{ route: "/500", type: "page", isFallback: false }], + }; + } + } + + if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) { + result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId; + } + + debug("Middleware response", result); + return result; + } + ); +}; + +export const handler = await createGenericHandler({ + handler: defaultHandler, + type: "middleware", +}); + +export default { + fetch: handler, +}; diff --git a/packages/open-next/src/adapters/plugins/README.md b/packages/core/src/adapters/plugins/README.md similarity index 100% rename from packages/open-next/src/adapters/plugins/README.md rename to packages/core/src/adapters/plugins/README.md diff --git a/packages/core/src/adapters/plugins/image-optimization/image-optimization.ts b/packages/core/src/adapters/plugins/image-optimization/image-optimization.ts new file mode 100644 index 00000000..765bb6e1 --- /dev/null +++ b/packages/core/src/adapters/plugins/image-optimization/image-optimization.ts @@ -0,0 +1,47 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; + +import type { NextConfig } from "next/dist/server/config-shared"; +//#override imports +import { fetchExternalImage, fetchInternalImage, imageOptimizer } from "next/dist/server/image-optimizer"; +//#endOverride +import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta"; + +import { debug } from "../../logger.js"; + +//#override optimizeImage +export async function optimizeImage( + headers: Record, + // oxlint-disable-next-line @typescript-eslint/no-explicit-any - image optimization API varies across Next.js versions + imageParams: any, + nextConfig: NextConfig, + handleRequest: ( + newReq: IncomingMessage, + newRes: ServerResponse, + newParsedUrl?: NextUrlWithParsedQuery + ) => Promise +) { + const { isAbsolute, href } = imageParams; + + const imageUpstream = isAbsolute + ? //@ts-expect-error - fetchExternalImage signature has changed in Next.js 16, it has an extra boolean parameter. + // https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 + await fetchExternalImage(href) + : await fetchInternalImage( + href, + // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. + { headers }, + {}, // res object is not necessary as it's not actually used. + handleRequest + ); + + const result = await imageOptimizer( + imageUpstream, + imageParams, + // @ts-ignore + nextConfig, + false // not in dev mode + ); + debug("optimized result", result); + return result; +} +//#endOverride diff --git a/packages/core/src/adapters/revalidate.ts b/packages/core/src/adapters/revalidate.ts new file mode 100644 index 00000000..d2b1674c --- /dev/null +++ b/packages/core/src/adapters/revalidate.ts @@ -0,0 +1,99 @@ +import fs from "node:fs"; +import type { IncomingMessage } from "node:http"; +import https from "node:https"; +import path from "node:path"; + +import { createGenericHandler } from "../core/createGenericHandler.js"; + +import { debug, error } from "./logger.js"; + +const prerenderManifest = loadPrerenderManifest(); + +interface PrerenderManifest { + preview: { + previewModeId: string; + previewModeSigningKey: string; + previewModeEncryptionKey: string; + }; +} + +export interface RevalidateEvent { + type: "revalidate"; + records: { + host: string; + url: string; + id: string; + }[]; +} + +const defaultHandler = async (event: RevalidateEvent) => { + const failedRecords: RevalidateEvent["records"] = []; + for (const record of event.records) { + const { host, url } = record; + debug("Revalidating stale page", { host, url }); + + // Make a HEAD request to the page to revalidate it. This will trigger + // the page to be re-rendered and cached in S3 + // - HEAD request is used b/c it's not necessary to make a GET request + // and have CloudFront cache the request. This is because the request + // does not have real life headers and the cache won't be used anyway. + // - "previewModeId" is used to ensure the page is revalidated in a + // blocking way in lambda + // https://github.com/vercel/next.js/blob/1088b3f682cbe411be2d1edc502f8a090e36dee4/packages/next/src/server/api-utils/node.ts#L353 + try { + await new Promise((resolve, reject) => { + const req = https.request( + `https://${host}${url}`, + { + method: "HEAD", + headers: { + "x-prerender-revalidate": prerenderManifest.preview.previewModeId, + "x-isr": "1", + }, + }, + (res) => { + debug("revalidating", { + url, + host, + headers: res.headers, + statusCode: res.statusCode, + }); + if (res.statusCode !== 200 || res.headers["x-nextjs-cache"] !== "REVALIDATED") { + failedRecords.push(record); + } + resolve(res); + } + ); + req.on("error", (err) => { + error("Error revalidating page", { host, url }); + reject(err); + }); + req.end(); + }); + } catch (err) { + failedRecords.push(record); + } + } + if (failedRecords.length > 0) { + error(`Failed to revalidate ${failedRecords.length} pages`, { + failedRecords, + }); + } + + return { + type: "revalidate", + // Records returned here are the ones that failed to revalidate + records: failedRecords, + }; +}; + +export const handler = await createGenericHandler({ + handler: defaultHandler, + type: "revalidate", +}); + +function loadPrerenderManifest() { + const filePath = path.join("prerender-manifest.json"); + const json = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(json) as PrerenderManifest; +} diff --git a/packages/core/src/adapters/server-adapter.ts b/packages/core/src/adapters/server-adapter.ts new file mode 100644 index 00000000..4d89758b --- /dev/null +++ b/packages/core/src/adapters/server-adapter.ts @@ -0,0 +1,27 @@ +import { createMainHandler } from "../core/createMainHandler.js"; + +import { setNodeEnv } from "./util.js"; + +// We load every config here so that they are only loaded once +// and during cold starts +setNodeEnv(); +setNextjsServerWorkingDirectory(); + +// Because next is messing with fetch, we have to make sure that we use an untouched version of fetch +globalThis.internalFetch = fetch; + +///////////// +// Handler // +///////////// + +export const handler = await createMainHandler(); + +////////////////////// +// Helper functions // +////////////////////// + +function setNextjsServerWorkingDirectory() { + // WORKAROUND: Set `NextServer` working directory (AWS specific) + // See https://opennext.js.org/aws/v2/advanced/workaround#workaround-set-nextserver-working-directory-aws-specific + process.chdir(__dirname); +} diff --git a/packages/core/src/adapters/util.ts b/packages/core/src/adapters/util.ts new file mode 100644 index 00000000..720c21e2 --- /dev/null +++ b/packages/core/src/adapters/util.ts @@ -0,0 +1,39 @@ +//TODO: We should probably move all the utils to a separate location + +export function setNodeEnv() { + // Note: we create a `processEnv` variable instead of just using `process.env` directly + // because build tools can substitute `process.env.NODE_ENV` on build making + // assignments such as `process.env.NODE_ENV = ...` problematic + const processEnv = process.env; + processEnv.NODE_ENV = process.env.NODE_ENV ?? "production"; +} + +export function generateUniqueId() { + return Math.random().toString(36).slice(2, 8); +} + +/** + * Create an array of arrays of size `chunkSize` from `items` + * @param items Array of T + * @param chunkSize size of each chunk + * @returns T[][] + */ +export function chunk(items: T[], chunkSize: number): T[][] { + const chunked = items.reduce((acc, curr, i) => { + const chunkIndex = Math.floor(i / chunkSize); + acc[chunkIndex] = [...(acc[chunkIndex] ?? []), curr]; + return acc; + }, new Array()); + + return chunked; +} + +export function parseNumberFromEnv(envValue: string | undefined): number | undefined { + if (typeof envValue !== "string") { + return envValue; + } + + const parsedValue = Number.parseInt(envValue); + + return Number.isNaN(parsedValue) ? undefined : parsedValue; +} diff --git a/packages/core/src/adapters/warmer-function.ts b/packages/core/src/adapters/warmer-function.ts new file mode 100644 index 00000000..4b61b068 --- /dev/null +++ b/packages/core/src/adapters/warmer-function.ts @@ -0,0 +1,34 @@ +import { createGenericHandler } from "../core/createGenericHandler.js"; +import { resolveWarmerInvoke } from "../core/resolve.js"; + +import { generateUniqueId } from "./util.js"; + +export interface WarmerEvent { + type: "warmer"; + warmerId: string; + index: number; + concurrency: number; + delay: number; +} + +export interface WarmerResponse { + type: "warmer"; + serverId: string; +} + +export const handler = await createGenericHandler({ + handler: defaultHandler, + type: "warmer", +}); + +async function defaultHandler() { + const warmerId = `warmer-${generateUniqueId()}`; + + const invokeFn = await resolveWarmerInvoke(globalThis.openNextConfig.warmer?.invokeFunction); + + await invokeFn.invoke(warmerId); + + return { + type: "warmer", + }; +} diff --git a/packages/core/src/build/buildNextApp.ts b/packages/core/src/build/buildNextApp.ts new file mode 100644 index 00000000..5142c36b --- /dev/null +++ b/packages/core/src/build/buildNextApp.ts @@ -0,0 +1,22 @@ +import cp from "node:child_process"; +import path from "node:path"; + +import type * as buildHelper from "./helper.js"; + +export function setStandaloneBuildMode(options: buildHelper.BuildOptions) { + // Equivalent to setting `output: "standalone"` in next.config.js + process.env.NEXT_PRIVATE_STANDALONE = "true"; + // Equivalent to setting `experimental.outputFileTracingRoot` in next.config.js + process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = options.monorepoRoot; +} + +export function buildNextjsApp(options: buildHelper.BuildOptions) { + const { config, packager } = options; + const command = + config.buildCommand ?? + (["bun", "npm"].includes(packager) ? `${packager} run build` : `${packager} build`); + cp.execSync(command, { + stdio: "inherit", + cwd: path.dirname(options.appPackageJsonPath), + }); +} diff --git a/packages/core/src/build/compileCache.ts b/packages/core/src/build/compileCache.ts new file mode 100644 index 00000000..553bcbf5 --- /dev/null +++ b/packages/core/src/build/compileCache.ts @@ -0,0 +1,59 @@ +import path from "node:path"; + +import * as buildHelper from "./helper.js"; + +/** + * Compiles the cache adapter. + * + * @param options Build options. + * @param format Output format. + * @returns An object containing the paths to the compiled cache and composable cache files. + */ +export function compileCache(options: buildHelper.BuildOptions, format: "cjs" | "esm" = "cjs") { + const { config } = options; + const ext = format === "cjs" ? "cjs" : "mjs"; + const compiledCacheFile = path.join(options.buildDir, `cache.${ext}`); + + // Normal cache + buildHelper.esbuildSync( + { + external: ["next", "styled-jsx", "react", "@aws-sdk/*"], + entryPoints: [path.join(options.openNextDistDir, "adapters", "cache.js")], + outfile: compiledCacheFile, + target: ["node18"], + format, + banner: { + js: [ + `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`, + `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`, + ].join(""), + }, + }, + options + ); + + const compiledComposableCacheFile = path.join(options.buildDir, `composable-cache.${ext}`); + + // Composable cache + buildHelper.esbuildSync( + { + external: ["next", "styled-jsx", "react", "@aws-sdk/*"], + entryPoints: [path.join(options.openNextDistDir, "adapters", "composable-cache.js")], + outfile: compiledComposableCacheFile, + target: ["node18"], + format, + banner: { + js: [ + `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`, + `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`, + ].join(""), + }, + }, + options + ); + + return { + cache: compiledCacheFile, + composableCache: compiledComposableCacheFile, + }; +} diff --git a/packages/core/src/build/compileConfig.ts b/packages/core/src/build/compileConfig.ts new file mode 100644 index 00000000..d4da6813 --- /dev/null +++ b/packages/core/src/build/compileConfig.ts @@ -0,0 +1,134 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { buildSync } from "esbuild"; + +import type { OpenNextConfig } from "@/types/open-next.js"; + +import logger from "../logger.js"; + +import { validateConfig } from "./validateConfig.js"; + +/** + * Compiles the OpenNext configuration. + * + * The configuration is always compiled for Node.js and for the edge only if needed. + * + * @param openNextConfigPath Path to the configuration file. Absolute or relative to cwd. + * @param nodeExternals Coma separated list of Externals for the Node.js compilation. + * @param compileEdge Force compiling for the edge runtime when true + * @return The configuration and the build directory. + */ +export async function compileOpenNextConfig( + openNextConfigPath: string, + { nodeExternals = "", compileEdge = false } = {} +) { + const buildDir = fs.mkdtempSync(path.join(os.tmpdir(), "open-next-tmp")); + + let configPath = compileOpenNextConfigNode(openNextConfigPath, buildDir, nodeExternals.split(",")); + + // On Windows, we need to use file:// protocol to load the config file using import() + if (process.platform === "win32") configPath = `file://${configPath}`; + const config = (await import(configPath)).default as OpenNextConfig; + if (!config || !config.default) { + logger.error( + "config.default cannot be empty, it should be at least {}, see more info here: https://opennext.js.org/config#configuration-file" + ); + process.exit(1); + } + + validateConfig(config); + + // We need to check if the config uses the edge runtime at any point + // If it does, we need to compile it with the edge runtime + const usesEdgeRuntime = + (config.middleware?.external && config.middleware.runtime !== "node") || + Object.values(config.functions || {}).some((fn) => fn.runtime === "edge"); + if (usesEdgeRuntime || compileEdge) { + compileOpenNextConfigEdge(openNextConfigPath, buildDir, config.edgeExternals ?? []); + } else { + // Skip compiling for the edge runtime. + logger.debug("No edge runtime found in the open-next.config.ts. Using default config."); + } + + return { config, buildDir }; +} + +/** + * Compiles the OpenNext configuration for Node. + * + * @param openNextConfigPath Path to the configuration file. Absolute or relative to cwd. + * @param outputDir Folder where to output the compiled config file (`open-next.config.mjs`). + * @param externals List of packages that should not be bundled. + * @return Path to the compiled config. + */ +export function compileOpenNextConfigNode( + openNextConfigPath: string, + outputDir: string, + externals: string[] +) { + const outputPath = path.join(outputDir, "open-next.config.mjs"); + logger.debug("Compiling open-next.config.ts for Node.", outputPath); + + //Check if open-next.config.ts exists + if (!fs.existsSync(openNextConfigPath)) { + //Create a simple open-next.config.mjs file + logger.debug("Cannot find open-next.config.ts. Using default config."); + fs.writeFileSync(outputPath, "export default { default: { } };"); + } else { + buildSync({ + entryPoints: [openNextConfigPath], + outfile: outputPath, + bundle: true, + format: "esm", + target: ["node18"], + external: externals, + platform: "node", + banner: { + js: [ + "import { createRequire as topLevelCreateRequire } from 'module';", + "const require = topLevelCreateRequire(import.meta.url);", + "import bannerUrl from 'url';", + "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", + ].join(""), + }, + }); + } + + return outputPath; +} + +/** + * Compiles the OpenNext configuration for Edge. + * + * @param openNextConfigPath Path to the configuration file. Absolute or relative to cwd. + * @param outputDir Folder where to output the compiled config file (`open-next.config.edge.mjs`). + * @param externals List of packages that should not be bundled. + * @return Path to the compiled config. + */ +export function compileOpenNextConfigEdge( + openNextConfigPath: string, + outputDir: string, + externals: string[] +) { + const outputPath = path.join(outputDir, "open-next.config.edge.mjs"); + logger.debug("Compiling open-next.config.ts for edge runtime.", outputPath); + + buildSync({ + entryPoints: [openNextConfigPath], + outfile: outputPath, + bundle: true, + format: "esm", + target: ["es2020"], + conditions: ["worker", "browser"], + platform: "browser", + external: externals, + define: { + // with the default esbuild config, the NODE_ENV will be set to "development", we don't want that + "process.env.NODE_ENV": '"production"', + }, + }); + + return outputPath; +} diff --git a/packages/core/src/build/compileTagCacheProvider.ts b/packages/core/src/build/compileTagCacheProvider.ts new file mode 100644 index 00000000..4cddb783 --- /dev/null +++ b/packages/core/src/build/compileTagCacheProvider.ts @@ -0,0 +1,34 @@ +import path from "node:path"; + +import { openNextResolvePlugin } from "../plugins/resolve.js"; + +import * as buildHelper from "./helper.js"; +import { installDependencies } from "./installDeps.js"; + +export async function compileTagCacheProvider(options: buildHelper.BuildOptions) { + const providerPath = path.join(options.outputDir, "dynamodb-provider"); + + const overrides = options.config.initializationFunction?.override; + + await buildHelper.esbuildAsync( + { + external: ["@aws-sdk/client-dynamodb"], + entryPoints: [path.join(options.openNextDistDir, "adapters", "dynamo-provider.js")], + outfile: path.join(providerPath, "index.mjs"), + target: ["node18"], + plugins: [ + openNextResolvePlugin({ + fnName: "initializationFunction", + overrides: { + converter: overrides?.converter ?? "dummy", + wrapper: overrides?.wrapper, + tagCache: options.config.initializationFunction?.tagCache, + }, + }), + ], + }, + options + ); + + installDependencies(providerPath, options.config.initializationFunction?.install); +} diff --git a/packages/core/src/build/constant.ts b/packages/core/src/build/constant.ts new file mode 100644 index 00000000..e32f4074 --- /dev/null +++ b/packages/core/src/build/constant.ts @@ -0,0 +1,3 @@ +//TODO: Move all other manifest path here as well +export const MIDDLEWARE_TRACE_FILE = "server/middleware.js.nft.json"; +export const INSTRUMENTATION_TRACE_FILE = "server/instrumentation.js.nft.json"; diff --git a/packages/core/src/build/copyAdapterFiles.ts b/packages/core/src/build/copyAdapterFiles.ts new file mode 100644 index 00000000..8e1170a7 --- /dev/null +++ b/packages/core/src/build/copyAdapterFiles.ts @@ -0,0 +1,79 @@ +import fs from "node:fs"; +import path from "node:path"; + +import type { NextAdapterOutput, NextAdapterOutputs } from "@/types/adapter"; +import { addDebugFile } from "../debug.js"; + +import type * as buildHelper from "./helper.js"; + +export async function copyAdapterFiles( + options: buildHelper.BuildOptions, + fnName: string, + packagePath: string, + outputs: NextAdapterOutputs +) { + const filesToCopy = new Map(); + + // Copying the files from outputs to the output dir + for (const [key, value] of Object.entries(outputs)) { + if (["pages", "pagesApi", "appPages", "appRoutes", "middleware"].includes(key)) { + const setFileToCopy = (route: NextAdapterOutput) => { + const assets = route.assets; + // We need to copy the filepaths to the output dir + const relativeFilePath = path.join(packagePath, path.relative(options.appPath, route.filePath)); + filesToCopy.set( + route.filePath, + `${options.outputDir}/server-functions/${fnName}/${relativeFilePath}` + ); + + for (const [relative, from] of Object.entries(assets || {})) { + // console.log("route.assets", from, relative, packagePath); + filesToCopy.set(from as string, `${options.outputDir}/server-functions/${fnName}/${relative}`); + } + }; + if (key === "middleware") { + // Middleware is a single object + setFileToCopy(value as NextAdapterOutput); + } else { + // The rest are arrays + for (const route of value as NextAdapterOutput[]) { + setFileToCopy(route); + // copyFileSync(from, `${options.outputDir}/${relative}`); + } + } + } + } + + console.log("\n### Copying adapter files"); + const debugCopiedFiles: Record = {}; + for (const [from, to] of filesToCopy) { + debugCopiedFiles[from] = to; + + //make sure the directory exists first + fs.mkdirSync(path.dirname(to), { recursive: true }); + // For pnpm symlink we need to do that + // see https://github.com/vercel/next.js/blob/498f342b3552d6fc6f1566a1cc5acea324ce0dec/packages/next/src/build/utils.ts#L1932 + let symlink = ""; + try { + symlink = fs.readlinkSync(from); + } catch (e) { + //Ignore + } + if (symlink) { + try { + fs.symlinkSync(symlink, to); + } catch (e: unknown) { + if (e instanceof Error && (e as NodeJS.ErrnoException).code !== "EEXIST") { + throw e; + } + } + } else { + fs.copyFileSync(from, to); + } + } + + // TODO(vicb): debug + addDebugFile(options, "copied_files.json", debugCopiedFiles); + + return Array.from(filesToCopy.values()); +} diff --git a/packages/core/src/build/copyTracedFiles.ts b/packages/core/src/build/copyTracedFiles.ts new file mode 100644 index 00000000..98710f9a --- /dev/null +++ b/packages/core/src/build/copyTracedFiles.ts @@ -0,0 +1,407 @@ +import { + chmodSync, + copyFileSync, + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + readlinkSync, + statSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import url from "node:url"; + +import { + loadAppPathsManifest, + loadBuildId, + loadConfig, + loadFunctionsConfigManifest, + loadMiddlewareManifest, + loadPagesManifest, + loadPrerenderManifest, +} from "@/config/util.js"; +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import logger from "../logger.js"; + +import { INSTRUMENTATION_TRACE_FILE, MIDDLEWARE_TRACE_FILE } from "./constant.js"; + +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); + +/** + * Copies a file and ensures the destination is writable. + * This is necessary because copyFileSync preserves file permissions, + * and source files may be read-only (e.g., in Bazel's node_modules). + * Without this, subsequent patches would fail with EACCES errors. + */ +export function copyFileAndMakeOwnerWritable(src: string, dest: string): void { + copyFileSync(src, dest); + // Ensure the copied file is writable (add owner write permission) + const stats = statSync(dest); + if (!(stats.mode & 0o200)) { + chmodSync(dest, stats.mode | 0o200); + } +} + +//TODO: we need to figure which packages we could safely remove +const EXCLUDED_PACKAGES = [ + "caniuse-lite", + "sharp", + // This seems to be only in Next 15 + // Some of sharp deps are under the @img scope + "@img", + "typescript", + "next/dist/compiled/babel", + "next/dist/compiled/babel-packages", + "next/dist/compiled/amphtml-validator", +]; + +const NON_LINUX_PLATFORMS = ["darwin", "win32", "freebsd"]; +const platformPattern = NON_LINUX_PLATFORMS.join("|"); +const nonLinuxPlatformRegex = getCrossPlatformPathRegex( + `/node_modules/(?:@[^/]+/)?(?:[^/]+-)?(${platformPattern})-[^/]+/`, + { escape: false } +); + +export function isNonLinuxPlatformPackage(srcPath: string): boolean { + return nonLinuxPlatformRegex.test(srcPath); +} + +export function isExcluded(srcPath: string): boolean { + return EXCLUDED_PACKAGES.some((excluded) => + // `pnpm` can create a symbolic link that points to the pnpm store folder + // This will live under `/node_modules/sharp`. We need to handle this in our regex + srcPath.match( + getCrossPlatformPathRegex(`/node_modules/${excluded}(?:/|$)`, { + escape: false, + }) + ) + ); +} + +function copyPatchFile(outputDir: string) { + const patchFile = path.join(__dirname, "patch", "patchedAsyncStorage.js"); + const outputPatchFile = path.join(outputDir, "patchedAsyncStorage.cjs"); + copyFileAndMakeOwnerWritable(patchFile, outputPatchFile); +} + +interface CopyTracedFilesOptions { + buildOutputPath: string; + packagePath: string; + outputDir: string; + routes: string[]; + skipServerFiles?: boolean; +} + +export function getManifests(nextDir: string) { + return { + buildId: loadBuildId(nextDir), + config: loadConfig(nextDir), + prerenderManifest: loadPrerenderManifest(nextDir), + pagesManifest: loadPagesManifest(nextDir), + appPathsManifest: loadAppPathsManifest(nextDir), + middlewareManifest: loadMiddlewareManifest(nextDir), + functionsConfigManifest: loadFunctionsConfigManifest(nextDir), + }; +} + +export async function copyTracedFiles({ + buildOutputPath, + packagePath, + outputDir, + routes, + skipServerFiles, +}: CopyTracedFilesOptions) { + const tsStart = Date.now(); + const dotNextDir = path.join(buildOutputPath, ".next"); + const standaloneDir = path.join(dotNextDir, "standalone"); + const standaloneNextDir = path.join(standaloneDir, packagePath, ".next"); + const standaloneServerDir = path.join(standaloneNextDir, "server"); + const outputNextDir = path.join(outputDir, packagePath, ".next"); + + // Files to copy + // Map from files in the `.next/standalone` to files in the `.open-next` folder + const filesToCopy = new Map(); + + // Node packages + // Map from folders in the project to folders in the `.open-next` folder + // The map might also include the mono-repo path. + const nodePackages = new Map(); + + /** + * Extracts files and node packages from a .nft.json file + * @param nftFile path to the .nft.json file relative to `.next/` + */ + const processNftFile = (nftFile: string) => { + const subDir = path.dirname(nftFile); + const files: string[] = JSON.parse(readFileSync(path.join(dotNextDir, nftFile), "utf8")).files; + + files.forEach((tracedPath: string) => { + const src = path.join(standaloneNextDir, subDir, tracedPath); + const dst = path.join(outputNextDir, subDir, tracedPath); + filesToCopy.set(src, dst); + + const module = path.join(dotNextDir, subDir, tracedPath); + if (module.endsWith("package.json")) { + nodePackages.set(path.dirname(module), path.dirname(dst)); + } + }); + }; + + // Files necessary by the server + if (!skipServerFiles) { + // On next 14+, we might not have to include those files + // For next 13, we need to include them otherwise we get runtime error + const nftFile = "next-server.js.nft.json"; + + processNftFile(nftFile); + } + // create directory for pages + if (existsSync(path.join(standaloneNextDir, "server/pages"))) { + mkdirSync(path.join(outputNextDir, "server/pages"), { + recursive: true, + }); + } + if (existsSync(path.join(standaloneNextDir, "server/app"))) { + mkdirSync(path.join(outputNextDir, "server/app"), { + recursive: true, + }); + } + + mkdirSync(path.join(outputNextDir, "server/chunks"), { + recursive: true, + }); + + const computeCopyFilesForPage = (pagePath: string) => { + const serverPath = `server/${pagePath}.js`; + + try { + processNftFile(`${serverPath}.nft.json`); + } catch (e) { + if (existsSync(path.join(dotNextDir, serverPath))) { + //TODO: add a link to the docs + throw new Error( + ` +-------------------------------------------------------------------------------- +${pagePath} cannot use the edge runtime. +OpenNext requires edge runtime function to be defined in a separate function. +See the docs for more information on how to bundle edge runtime functions. +-------------------------------------------------------------------------------- + ` + ); + } + throw new Error(` +-------------------------------------------------------------------------------- +We cannot find the route for ${pagePath}. +File ${serverPath} does not exist +--------------------------------------------------------------------------------`); + } + + if (!existsSync(path.join(standaloneNextDir, serverPath))) { + throw new Error( + `This error should only happen for static 404 and 500 page from page router. Report this if that's not the case., + File ${serverPath} does not exist` + ); + } + + filesToCopy.set(path.join(standaloneNextDir, serverPath), path.join(outputNextDir, serverPath)); + }; + + const safeComputeCopyFilesForPage = (pagePath: string, alternativePath?: string) => { + try { + computeCopyFilesForPage(pagePath); + } catch (e) { + if (alternativePath) { + safeComputeCopyFilesForPage(alternativePath); + } + } + }; + + // Check for instrumentation trace file + if (existsSync(path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE))) { + // We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw + copyFileAndMakeOwnerWritable( + path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE), + path.join(standaloneNextDir, INSTRUMENTATION_TRACE_FILE) + ); + computeCopyFilesForPage("instrumentation"); + logger.debug("Adding instrumentation trace files"); + } + + if (existsSync(path.join(dotNextDir, MIDDLEWARE_TRACE_FILE))) { + // We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw + copyFileAndMakeOwnerWritable( + path.join(dotNextDir, MIDDLEWARE_TRACE_FILE), + path.join(standaloneNextDir, MIDDLEWARE_TRACE_FILE) + ); + computeCopyFilesForPage("middleware"); + logger.debug("Adding node middleware trace files"); + } + + const hasPageDir = routes.some((route) => route.startsWith("pages/")); + const hasAppDir = routes.some((route) => route.startsWith("app/")); + + // We need to copy all the base files like _app, _document, _error, etc + // One thing to note, is that next try to load every routes that might be needed in advance + // So if you have a [slug].tsx at the root, this route will always be loaded for 1st level request + // along with _app and _document + if (hasPageDir) { + //Page dir + computeCopyFilesForPage("pages/_app"); + computeCopyFilesForPage("pages/_document"); + computeCopyFilesForPage("pages/_error"); + + // These files can be present or not depending on if the user uses getStaticProps + safeComputeCopyFilesForPage("pages/404"); + safeComputeCopyFilesForPage("pages/500"); + } + + if (hasAppDir) { + //App dir + safeComputeCopyFilesForPage("app/_not-found/page"); + } + + //Files we actually want to include + routes.forEach((route) => { + computeCopyFilesForPage(route); + }); + + // Only files that are actually copied + const tracedFiles: string[] = []; + const erroredFiles: string[] = []; + //Actually copy the files + filesToCopy.forEach((to, from) => { + // We don't want to copy excluded packages (e.g. sharp) + if (isExcluded(from)) { + return; + } + // Skip non-Linux platform-specific native binaries (e.g. @swc/core-darwin-arm64) + if (!process.env.OPEN_NEXT_SKIP_PLATFORM_FILTER && isNonLinuxPlatformPackage(from)) { + const match = from.match(/node_modules\/(.+\/)?([^/]+-(?:darwin|win32|freebsd)-[^/]+)/); + if (match) { + logger.debug(`Skipping non-Linux platform package: ${match[2]}`); + } + return; + } + tracedFiles.push(to); + mkdirSync(path.dirname(to), { recursive: true }); + let symlink = null; + // For pnpm symlink we need to do that + // see https://github.com/vercel/next.js/blob/498f342b3552d6fc6f1566a1cc5acea324ce0dec/packages/next/src/build/utils.ts#L1932 + try { + symlink = readlinkSync(from); + } catch (e) { + //Ignore + } + if (symlink) { + try { + symlinkSync(symlink, to); + } catch (e: unknown) { + if (e instanceof Error && (e as NodeJS.ErrnoException).code !== "EEXIST") { + throw e; + } + } + } else { + // Adding this inside a try-catch to handle errors on Next 16+ + // where some files listed in the .nft.json might not be present in the standalone folder + // TODO: investigate that further - is it expected? + try { + copyFileAndMakeOwnerWritable(from, to); + } catch (e) { + logger.debug("Error copying file:", e); + erroredFiles.push(to); + } + } + }); + + readdirSync(standaloneNextDir) + .filter((fileOrDir) => !statSync(path.join(standaloneNextDir, fileOrDir)).isDirectory()) + .forEach((file) => { + copyFileAndMakeOwnerWritable(path.join(standaloneNextDir, file), path.join(outputNextDir, file)); + tracedFiles.push(path.join(outputNextDir, file)); + }); + + // We then need to copy all the files at the root of server + + mkdirSync(path.join(outputNextDir, "server"), { recursive: true }); + + readdirSync(standaloneServerDir) + .filter((fileOrDir) => !statSync(path.join(standaloneServerDir, fileOrDir)).isDirectory()) + .filter((file) => file !== "server.js") + .forEach((file) => { + copyFileAndMakeOwnerWritable( + path.join(standaloneServerDir, file), + path.join(path.join(outputNextDir, "server"), file) + ); + tracedFiles.push(path.join(outputNextDir, "server", file)); + }); + + // Copy patch file + copyPatchFile(path.join(outputDir, packagePath)); + + // TODO: Recompute all the files. + // vercel doesn't seem to do it, but it seems wasteful to have all those files + // we replace the pages-manifest.json with an empty one if we don't have a pages dir so that + // next doesn't try to load _app, _document + if (!hasPageDir) { + writeFileSync(path.join(outputNextDir, "server/pages-manifest.json"), "{}"); + } + + //TODO: Find what else we need to copy + const copyStaticFile = (filePath: string) => { + if (existsSync(path.join(standaloneNextDir, filePath))) { + mkdirSync(path.dirname(path.join(outputNextDir, filePath)), { + recursive: true, + }); + copyFileAndMakeOwnerWritable( + path.join(standaloneNextDir, filePath), + path.join(outputNextDir, filePath) + ); + } + }; + + const manifests = getManifests(standaloneNextDir); + const { config, prerenderManifest, pagesManifest } = manifests; + + // Get all the static files - Should be only for pages dir + // Ideally we would filter only those that might get accessed in this specific functions + // Maybe even move this to s3 directly + if (hasPageDir) { + // First we get truly static files - i.e. pages without getStaticProps + const staticFiles: Array = Object.values(pagesManifest); + // Then we need to get all fallback: true dynamic routes html + const locales = config.i18n?.locales; + Object.values(prerenderManifest?.dynamicRoutes ?? {}).forEach((route) => { + if (typeof route.fallback === "string") { + if (locales) { + locales.forEach((locale) => { + staticFiles.push(`pages/${locale}${route.fallback}`); + }); + } else { + staticFiles.push(`pages${route.fallback}`); + } + } + }); + + staticFiles.filter((file) => file.endsWith(".html")).forEach((file) => copyStaticFile(`server/${file}`)); + } + + // Copy .next/static/css from standalone to output dir + // needed for optimizeCss feature to work + if (config.experimental.optimizeCss) { + cpSync(path.join(standaloneNextDir, "static", "css"), path.join(outputNextDir, "static", "css"), { + recursive: true, + }); + } + + logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms"); + + return { + tracedFiles: tracedFiles.filter((f) => !erroredFiles.includes(f)), + nodePackages, + manifests, + }; +} diff --git a/packages/core/src/build/createAssets.ts b/packages/core/src/build/createAssets.ts new file mode 100644 index 00000000..4ad81bbd --- /dev/null +++ b/packages/core/src/build/createAssets.ts @@ -0,0 +1,274 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { loadConfig } from "@/config/util.js"; +import { safeParseJsonFile } from "@/utils/safe-json-parse.js"; + +import logger from "../logger.js"; +import type { TagCacheMetaFile } from "../types/cache.js"; +import { isBinaryContentType } from "../utils/binary.js"; + +import * as buildHelper from "./helper.js"; + +type CacheFileMeta = { + segmentPaths?: string[]; + headers?: Record; +}; + +type FetchCacheData = { + tags?: string[]; +}; + +/** + * Copy the static assets to the output folder + * + * WARNING: `useBasePath` should be set to `false` when the output file is used. + * + * @param options OpenNext build options + * @param useBasePath whether to copy files into the to Next.js configured basePath + */ +export function createStaticAssets(options: buildHelper.BuildOptions, { useBasePath = false } = {}) { + logger.info("Bundling static assets..."); + + const { appBuildOutputPath, appPublicPath, outputDir, appPath } = options; + + const NextConfig = loadConfig(path.join(appBuildOutputPath, ".next")); + const basePath = useBasePath ? (NextConfig.basePath ?? "") : ""; + + // Create output folder + const outputPath = path.join(outputDir, "assets", basePath); + fs.mkdirSync(outputPath, { recursive: true }); + + /** + * Next.js outputs assets into multiple files. + * + * Copy into the same directory: + * - `.open-next/assets` when `useBasePath` is `false` + * - `.open-next/assets/basePath` when `useBasePath` is `true` + * + * Copy over: + * - .next/BUILD_ID => BUILD_ID + * - .next/static => _next/static + * - public/* => * + * - app/favicon.ico or src/app/favicon.ico => favicon.ico + * + * Note: BUILD_ID is used by the SST infra. + */ + fs.copyFileSync(path.join(appBuildOutputPath, ".next/BUILD_ID"), path.join(outputPath, "BUILD_ID")); + + fs.cpSync(path.join(appBuildOutputPath, ".next/static"), path.join(outputPath, "_next", "static"), { + recursive: true, + }); + if (fs.existsSync(appPublicPath)) { + fs.cpSync(appPublicPath, outputPath, { recursive: true }); + } + + const appSrcPath = fs.existsSync(path.join(appPath, "src")) ? "src/app" : "app"; + + const faviconPath = path.join(appPath, appSrcPath, "favicon.ico"); + + // We need to check if the favicon is either a file or directory. + // If it's a directory, we assume it's a route handler and ignore it. + if (fs.existsSync(faviconPath) && fs.lstatSync(faviconPath).isFile()) { + fs.copyFileSync(faviconPath, path.join(outputPath, "favicon.ico")); + } +} + +/** + * Create the cache assets. + * + * @param options Build options. + * @returns Whether the tag cache is used, and the meta files collected. + */ +export function createCacheAssets(options: buildHelper.BuildOptions) { + logger.info("Bundling cache assets..."); + + const { appBuildOutputPath, outputDir } = options; + const packagePath = buildHelper.getPackagePath(options); + const buildId = buildHelper.getBuildId(options); + let useTagCache = false; + + const dotNextPath = appBuildOutputPath; + + const outputCachePath = path.join(outputDir, "cache", buildId); + fs.mkdirSync(outputCachePath, { recursive: true }); + + const sourceDirs = [".next/server/pages", ".next/server/app"] + .map((dir) => path.join(dotNextPath, dir)) + .filter(fs.existsSync); + + const htmlPages = buildHelper.getHtmlPages(dotNextPath); + + const isFileSkipped = (relativePath: string) => + relativePath.endsWith(".js") || + relativePath.endsWith(".js.nft.json") || + // We skip manifest files as well + relativePath.endsWith("-manifest.json") || + // We skip the segment rsc files as they are treated in a different way + relativePath.endsWith(".segment.rsc") || + (relativePath.endsWith(".html") && htmlPages.has(relativePath)); + + // Merge cache files into a single file + const cacheFilesPath: Record< + string, + { + meta?: string; + html?: string; + json?: string; + rsc?: string; + body?: string; + } + > = {}; + + // Process each source directory + sourceDirs.forEach((sourceDir) => { + buildHelper.traverseFiles( + sourceDir, + ({ relativePath }) => !isFileSkipped(relativePath), + ({ absolutePath, relativePath }) => { + const ext = path.extname(absolutePath); + switch (ext) { + case ".meta": + case ".html": + case ".json": + case ".body": + case ".rsc": { + const newFilePath = path + .join(outputCachePath, relativePath) + .substring(0, path.join(outputCachePath, relativePath).length - ext.length) + .replace(/\.prefetch$/, "") + .concat(".cache"); + + cacheFilesPath[newFilePath] = { + [ext.slice(1)]: absolutePath, + ...cacheFilesPath[newFilePath], + }; + break; + } + case ".map": + break; + default: + logger.warn(`Unknown file extension: ${ext}`); + break; + } + } + ); + }); + + // Generate cache file + Object.entries(cacheFilesPath).forEach(([cacheFilePath, files]) => { + const cacheFileMeta = files.meta + ? safeParseJsonFile(fs.readFileSync(files.meta, "utf8"), cacheFilePath) + : undefined; + const cacheJson = files.json + ? safeParseJsonFile>(fs.readFileSync(files.json, "utf8"), cacheFilePath) + : undefined; + if ((files.meta && !cacheFileMeta) || (files.json && !cacheJson)) { + logger.warn(`Skipping invalid cache file: ${cacheFilePath}`); + return; + } + + // If we have a meta file, and it contains segmentPaths, we need to add them to the cache file + const segments: Record = Array.isArray(cacheFileMeta?.segmentPaths) + ? Object.fromEntries( + cacheFileMeta!.segmentPaths.map((segmentPath: string) => { + const absoluteSegmentPath = path.join( + files.meta!.replace(/\.meta$/, ".segments"), + `${segmentPath}.segment.rsc` + ); + const segmentContent = fs.readFileSync(absoluteSegmentPath, "utf8"); + return [segmentPath, segmentContent]; + }) + ) + : {}; + + const cacheFileContent = { + type: files.body ? "route" : files.json ? "page" : "app", + meta: cacheFileMeta, + html: files.html ? fs.readFileSync(files.html, "utf8") : undefined, + json: cacheJson, + rsc: files.rsc ? fs.readFileSync(files.rsc, "utf8") : undefined, + body: files.body + ? fs + .readFileSync(files.body) + .toString(isBinaryContentType(cacheFileMeta?.headers?.["content-type"]) ? "base64" : "utf8") + : undefined, + segmentData: Object.keys(segments).length > 0 ? segments : undefined, + }; + + // Ensure directory exists before writing + fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true }); + fs.writeFileSync(cacheFilePath, JSON.stringify(cacheFileContent)); + }); + + // We need to traverse the cache to find every .meta file + const metaFiles: TagCacheMetaFile[] = []; + + // Copy fetch-cache to cache folder + const fetchCachePath = path.join(appBuildOutputPath, ".next/cache/fetch-cache"); + if (fs.existsSync(fetchCachePath)) { + const fetchOutputPath = path.join(outputDir, "cache", "__fetch", buildId); + fs.mkdirSync(fetchOutputPath, { recursive: true }); + fs.cpSync(fetchCachePath, fetchOutputPath, { recursive: true }); + + buildHelper.traverseFiles( + fetchCachePath, + () => true, + ({ absolutePath, relativePath }) => { + const fileContent = fs.readFileSync(absolutePath, "utf8"); + const fileData = safeParseJsonFile(fileContent, absolutePath); + fileData?.tags?.forEach((tag: string) => { + metaFiles.push({ + tag: { S: path.posix.join(buildId, tag) }, + path: { + S: path.posix.join(buildId, relativePath), + }, + revalidatedAt: { N: "1" }, + }); + }); + } + ); + } + + if (!options.config.dangerous?.disableTagCache) { + // Compute dynamodb cache data + // Traverse files inside cache to find all meta files and cache tags associated with them + sourceDirs.forEach((sourceDir) => { + buildHelper.traverseFiles( + sourceDir, + ({ absolutePath, relativePath }) => absolutePath.endsWith(".meta") && !isFileSkipped(relativePath), + ({ absolutePath, relativePath }) => { + const fileContent = fs.readFileSync(absolutePath, "utf8"); + const fileData = safeParseJsonFile(fileContent, absolutePath); + if (fileData?.headers?.["x-next-cache-tags"]) { + fileData.headers["x-next-cache-tags"].split(",").forEach((tag: string) => { + // TODO: We should split the tag using getDerivedTags from next.js or maybe use an in house implementation + metaFiles.push({ + tag: { S: path.posix.join(buildId, tag.trim()) }, + path: { + S: path.posix.join(buildId, relativePath.replace(".meta", "")), + }, + // We don't care about the revalidation time here, we just need to make sure it's there + revalidatedAt: { N: "1" }, + }); + }); + } + } + ); + }); + + if (metaFiles.length > 0) { + useTagCache = true; + const providerPath = path.join(outputDir, "dynamodb-provider"); + + // Copy open-next.config.mjs into the bundle + fs.mkdirSync(providerPath, { recursive: true }); + buildHelper.copyOpenNextConfig(options.buildDir, providerPath); + + // TODO: check if metafiles doesn't contain duplicates + fs.writeFileSync(path.join(providerPath, "dynamodb-cache.json"), JSON.stringify(metaFiles)); + } + } + + return { useTagCache, metaFiles }; +} diff --git a/packages/core/src/build/createImageOptimizationBundle.ts b/packages/core/src/build/createImageOptimizationBundle.ts new file mode 100644 index 00000000..f8b1e438 --- /dev/null +++ b/packages/core/src/build/createImageOptimizationBundle.ts @@ -0,0 +1,100 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import logger from "../logger.js"; +import { openNextResolvePlugin } from "../plugins/resolve.js"; + +import * as buildHelper from "./helper.js"; +import { installDependencies } from "./installDeps.js"; + +export async function createImageOptimizationBundle(options: buildHelper.BuildOptions) { + logger.info("Bundling image optimization function..."); + + const { appBuildOutputPath, config, outputDir } = options; + + // Create output folder + const outputPath = path.join(outputDir, "image-optimization-function"); + fs.mkdirSync(outputPath, { recursive: true }); + + // Copy open-next.config.mjs into the bundle + buildHelper.copyOpenNextConfig(options.buildDir, outputPath); + + const plugins = [ + openNextResolvePlugin({ + fnName: "imageOptimization", + overrides: { + converter: config.imageOptimization?.override?.converter, + wrapper: config.imageOptimization?.override?.wrapper, + imageLoader: config.imageOptimization?.loader, + }, + }), + ]; + + // Build Lambda code (1st pass) + // note: bundle in OpenNext package b/c the adapter relies on the + // "@aws-sdk/client-s3" package which is not a dependency in user's + // Next.js app. + await buildHelper.esbuildAsync( + { + entryPoints: [path.join(options.openNextDistDir, "adapters", "image-optimization-adapter.js")], + external: ["sharp", "next"], + outfile: path.join(outputPath, "index.mjs"), + plugins, + }, + options + ); + + // Build Lambda code (2nd pass) + // note: bundle in user's Next.js app again b/c the adapter relies on the + // "next" package. And the "next" package from user's app should + // be used. We also set @opentelemetry/api as external because it seems to be + // required by Next 15 even though it's not used. + buildHelper.esbuildSync( + { + entryPoints: [path.join(outputPath, "index.mjs")], + external: ["sharp", "@opentelemetry/api"], + allowOverwrite: true, + outfile: path.join(outputPath, "index.mjs"), + banner: { + js: [ + "import { createRequire as topLevelCreateRequire } from 'module';", + "const require = topLevelCreateRequire(import.meta.url);", + "import bannerUrl from 'url';", + "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", + ].join("\n"), + }, + }, + options + ); + + // Copy over .next/required-server-files.json file and BUILD_ID + fs.mkdirSync(path.join(outputPath, ".next")); + fs.copyFileSync( + path.join(appBuildOutputPath, ".next/required-server-files.json"), + path.join(outputPath, ".next/required-server-files.json") + ); + fs.copyFileSync(path.join(appBuildOutputPath, ".next/BUILD_ID"), path.join(outputPath, ".next/BUILD_ID")); + + // Sharp provides pre-build binaries for all platforms. https://github.com/lovell/sharp/blob/main/docs/install.md#cross-platform + // Target should be same as used by Lambda, see https://github.com/sst/sst/blob/ca6f763fdfddd099ce2260202d0ce48c72e211ea/packages/sst/src/constructs/NextjsSite.ts#L114 + // For SHARP_IGNORE_GLOBAL_LIBVIPS see: https://github.com/lovell/sharp/blob/main/docs/install.md#aws-lambda + + const sharpVersion = process.env.SHARP_VERSION ?? "0.32.6"; + + // In development, we want to use the local machine settings + const isDev = config.imageOptimization?.loader === "fs-dev"; + + installDependencies( + outputPath, + config.imageOptimization?.install ?? { + packages: [`sharp@${sharpVersion}`], + // By not specifying an arch in dev, `npm install` will choose one for us (i.e. our system one) + arch: isDev ? undefined : "arm64", + // Use the local platform in dev + os: isDev ? os.platform() : "linux", + nodeVersion: "18", + libc: "glibc", + } + ); +} diff --git a/packages/core/src/build/createMiddleware.ts b/packages/core/src/build/createMiddleware.ts new file mode 100644 index 00000000..6c1de5e8 --- /dev/null +++ b/packages/core/src/build/createMiddleware.ts @@ -0,0 +1,87 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { loadFunctionsConfigManifest, loadMiddlewareManifest } from "@/config/util.js"; + +import logger from "../logger.js"; +import type { MiddlewareInfo } from "../types/next-types.js"; + +import { buildEdgeBundle, copyMiddlewareResources } from "./edge/createEdgeBundle.js"; +import * as buildHelper from "./helper.js"; +import { installDependencies } from "./installDeps.js"; +import { buildBundledNodeMiddleware, buildExternalNodeMiddleware } from "./middleware/buildNodeMiddleware.js"; + +/** + * Compiles the middleware bundle. + * + * @param options Build Options. + * @param forceOnlyBuildOnce force to build only once. + */ +export async function createMiddleware( + options: buildHelper.BuildOptions, + { forceOnlyBuildOnce = false } = {} +) { + logger.info("Bundling middleware function..."); + + const { config, outputDir } = options; + const buildOutputDotNextDir = path.join(options.appBuildOutputPath, ".next"); + + // Get middleware manifest + const middlewareManifest = loadMiddlewareManifest(buildOutputDotNextDir); + + const edgeMiddlewareInfo = middlewareManifest.middleware["/"] as MiddlewareInfo | undefined; + + if (!edgeMiddlewareInfo) { + // If there is no middleware info, it might be a node middleware + const functionsConfigManifest = loadFunctionsConfigManifest(buildOutputDotNextDir); + + if (functionsConfigManifest?.functions["/_middleware"]) { + await (config.middleware?.external + ? buildExternalNodeMiddleware(options) + : buildBundledNodeMiddleware(options)); + return; + } + } + + if (config.middleware?.external) { + const outputPath = path.join(outputDir, "middleware"); + copyMiddlewareResources(options, edgeMiddlewareInfo, outputPath); + + fs.mkdirSync(outputPath, { recursive: true }); + + // Copy open-next.config.mjs + buildHelper.copyOpenNextConfig( + options.buildDir, + outputPath, + await buildHelper.isEdgeRuntime(config.middleware.override) + ); + + // Bundle middleware + await buildEdgeBundle({ + entrypoint: path.join(options.openNextDistDir, "adapters", "middleware.js"), + outfile: path.join(outputPath, "handler.mjs"), + middlewareInfo: edgeMiddlewareInfo, + options, + overrides: { + ...config.middleware.override, + originResolver: config.middleware.originResolver, + }, + defaultConverter: "aws-cloudfront", + additionalExternals: config.edgeExternals, + onlyBuildOnce: forceOnlyBuildOnce === true, + name: "middleware", + }); + + installDependencies(outputPath, config.middleware?.install); + } else { + await buildEdgeBundle({ + entrypoint: path.join(options.openNextDistDir, "core", "edgeFunctionHandler.js"), + outfile: path.join(options.buildDir, "middleware.mjs"), + middlewareInfo: edgeMiddlewareInfo, + options, + overrides: config.default.override, + onlyBuildOnce: true, + name: "middleware", + }); + } +} diff --git a/packages/core/src/build/createRevalidationBundle.ts b/packages/core/src/build/createRevalidationBundle.ts new file mode 100644 index 00000000..38dc9991 --- /dev/null +++ b/packages/core/src/build/createRevalidationBundle.ts @@ -0,0 +1,48 @@ +import fs from "node:fs"; +import path from "node:path"; + +import logger from "../logger.js"; +import { openNextResolvePlugin } from "../plugins/resolve.js"; + +import * as buildHelper from "./helper.js"; +import { installDependencies } from "./installDeps.js"; + +export async function createRevalidationBundle(options: buildHelper.BuildOptions) { + logger.info("Bundling revalidation function..."); + + const { appBuildOutputPath, config, outputDir } = options; + + // Create output folder + const outputPath = path.join(outputDir, "revalidation-function"); + fs.mkdirSync(outputPath, { recursive: true }); + + //Copy open-next.config.mjs into the bundle + buildHelper.copyOpenNextConfig(options.buildDir, outputPath); + + // Build Lambda code + await buildHelper.esbuildAsync( + { + external: ["next", "styled-jsx", "react"], + entryPoints: [path.join(options.openNextDistDir, "adapters", "revalidate.js")], + outfile: path.join(outputPath, "index.mjs"), + plugins: [ + openNextResolvePlugin({ + fnName: "revalidate", + overrides: { + converter: config.revalidate?.override?.converter ?? "sqs-revalidate", + wrapper: config.revalidate?.override?.wrapper, + }, + }), + ], + }, + options + ); + + installDependencies(outputPath, config.revalidate?.install); + + // Copy over .next/prerender-manifest.json file + fs.copyFileSync( + path.join(appBuildOutputPath, ".next", "prerender-manifest.json"), + path.join(outputPath, "prerender-manifest.json") + ); +} diff --git a/packages/core/src/build/createServerBundle.ts b/packages/core/src/build/createServerBundle.ts new file mode 100644 index 00000000..17a64d96 --- /dev/null +++ b/packages/core/src/build/createServerBundle.ts @@ -0,0 +1,324 @@ +import fs from "node:fs"; +import path from "node:path"; + +import type { Plugin } from "esbuild"; + +import { loadMiddlewareManifest } from "@/config/util.js"; +import type { FunctionOptions, SplittedFunctionOptions } from "@/types/open-next"; + +import type { NextAdapterOutputs } from "@/types/adapter.js"; +import logger from "../logger.js"; +import { minifyAll } from "../minimize-js.js"; +import { ContentUpdater } from "../plugins/content-updater.js"; +import { openNextReplacementPlugin } from "../plugins/replacement.js"; +import { openNextResolvePlugin } from "../plugins/resolve.js"; +import { getCrossPlatformPathRegex } from "../utils/regex.js"; + +import { compileCache } from "./compileCache.js"; +import { copyAdapterFiles } from "./copyAdapterFiles.js"; +import { getManifests } from "./copyTracedFiles.js"; +import { copyMiddlewareResources, generateEdgeBundle } from "./edge/createEdgeBundle.js"; +import * as buildHelper from "./helper.js"; +import { installDependencies } from "./installDeps.js"; +import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; +import * as patches from "./patch/patches/index.js"; + +interface CodeCustomization { + // These patches are meant to apply on user and next generated code + additionalCodePatches?: CodePatcher[]; + // These plugins are meant to apply during the esbuild bundling process. + // This will only apply to OpenNext code. + additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[]; +} + +export async function createServerBundle( + options: buildHelper.BuildOptions, + codeCustomization?: CodeCustomization, + nextOutputs?: NextAdapterOutputs +) { + 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, codeCustomization, nextOutputs); + } + }); + + //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 } = options; + + // Find remaining routes + const serverPath = path.join( + appBuildOutputPath, + ".next/standalone", + buildHelper.getPackagePath(options), + ".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: ["*"], + }, + codeCustomization, + nextOutputs + ); +} + +async function generateBundle( + name: string, + options: buildHelper.BuildOptions, + fnOptions: SplittedFunctionOptions, + codeCustomization?: CodeCustomization, + nextOutputs?: NextAdapterOutputs +) { + 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 = buildHelper.getPackagePath(options); + const outPackagePath = path.join(outputPath, packagePath); + + fs.mkdirSync(outPackagePath, { recursive: true }); + + const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs"; + // Normal cache + fs.copyFileSync(path.join(options.buildDir, `cache.${ext}`), path.join(outPackagePath, "cache.cjs")); + // Composable cache + fs.copyFileSync( + path.join(options.buildDir, `composable-cache.${ext}`), + path.join(outPackagePath, "composable-cache.cjs") + ); + + if (fnOptions.runtime === "deno") { + addDenoJson(outputPath, packagePath); + } + + // Copy middleware + if (!config.middleware?.external) { + fs.copyFileSync( + path.join(options.buildDir, "middleware.mjs"), + path.join(outPackagePath, "middleware.mjs") + ); + + const middlewareManifest = loadMiddlewareManifest(path.join(options.appBuildOutputPath, ".next")); + + copyMiddlewareResources(options, middlewareManifest.middleware["/"], outPackagePath); + } + + // Copy open-next.config.mjs + buildHelper.copyOpenNextConfig(options.buildDir, outPackagePath); + + // Copy env files + buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); + + let tracedFiles: string[] = []; + let manifests: ReturnType = {} as ReturnType; + + // Copy all necessary traced files + if (!nextOutputs) { + throw new Error( + "createServerBundle was called without adapter outputs. " + + "Please ensure NextAdapterOutputs is provided to createServerBundle." + ); + } + tracedFiles = await copyAdapterFiles(options, name, packagePath, nextOutputs); + //TODO: we should load manifests here + + const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; + + await applyCodePatches(options, tracedFiles, manifests as ReturnType, [ + patches.patchFetchCacheSetMissingWaitUntil, + patches.patchFetchCacheForISR, + patches.patchUnstableCacheForISR, + patches.patchNextServer, + patches.getEnvVarsPatch(options), + patches.patchBackgroundRevalidation, + patches.patchUseCacheForISR, + patches.patchNodeEnvironment, + ...additionalCodePatches, + ]); + + // 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 overrides = fnOptions.override ?? {}; + + const disableRouting = config.middleware?.external; + + const updater = new ContentUpdater(options); + + const additionalPlugins = codeCustomization?.additionalPlugins + ? codeCustomization.additionalPlugins(updater) + : []; + + const plugins = [ + openNextReplacementPlugin({ + name: `requestHandlerOverride ${name}`, + target: getCrossPlatformPathRegex("core/requestHandler.js"), + deletes: disableRouting ? ["withRouting"] : [], + }), + + openNextResolvePlugin({ + fnName: name, + overrides, + }), + ...additionalPlugins, + // The content updater plugin must be the last plugin + updater.plugin, + ]; + + const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; + await buildHelper.esbuildAsync( + { + entryPoints: [path.join(options.openNextDistDir, "adapters", "server-adapter.js")], + external: ["next", "./middleware.mjs", "./next-server.runtime.prod.js"], + outfile: path.join(outputPath, packagePath, `index.${outfileExt}`), + banner: { + js: [ + `globalThis.monorepoPackagePath = "${packagePath}";`, + "import process from 'node:process';", + "import { Buffer } from 'node:buffer';", + "import { createRequire as topLevelCreateRequire } from 'module';", + "const require = topLevelCreateRequire(import.meta.url);", + "import bannerUrl from 'url';", + "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", + "const __filename = bannerUrl.fileURLToPath(import.meta.url);", + name === "default" ? "" : `globalThis.fnName = "${name}";`, + ].join(""), + }, + plugins, + }, + 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. + + // 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 { handler } 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/core/src/build/createWarmerBundle.ts b/packages/core/src/build/createWarmerBundle.ts new file mode 100644 index 00000000..a0ed877a --- /dev/null +++ b/packages/core/src/build/createWarmerBundle.ts @@ -0,0 +1,53 @@ +import fs from "node:fs"; +import path from "node:path"; + +import logger from "../logger.js"; +import { openNextResolvePlugin } from "../plugins/resolve.js"; + +import * as buildHelper from "./helper.js"; +import { installDependencies } from "./installDeps.js"; + +export async function createWarmerBundle(options: buildHelper.BuildOptions) { + logger.info("Bundling warmer function..."); + + const { config, outputDir } = options; + + // Create output folder + const outputPath = path.join(outputDir, "warmer-function"); + fs.mkdirSync(outputPath, { recursive: true }); + + // Copy open-next.config.mjs into the bundle + buildHelper.copyOpenNextConfig(options.buildDir, outputPath); + + // Build Lambda code + // note: bundle in OpenNext package b/c the adatper relys on the + // "serverless-http" package which is not a dependency in user's + // Next.js app. + await buildHelper.esbuildAsync( + { + entryPoints: [path.join(options.openNextDistDir, "adapters", "warmer-function.js")], + external: ["next"], + outfile: path.join(outputPath, "index.mjs"), + plugins: [ + openNextResolvePlugin({ + overrides: { + converter: config.warmer?.override?.converter ?? "dummy", + wrapper: config.warmer?.override?.wrapper, + }, + fnName: "warmer", + }), + ], + banner: { + js: [ + "import { createRequire as topLevelCreateRequire } from 'module';", + "const require = topLevelCreateRequire(import.meta.url);", + "import bannerUrl from 'url';", + "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", + ].join(""), + }, + }, + options + ); + + installDependencies(outputPath, config.warmer?.install); +} diff --git a/packages/core/src/build/edge/createEdgeBundle.ts b/packages/core/src/build/edge/createEdgeBundle.ts new file mode 100644 index 00000000..5d7370aa --- /dev/null +++ b/packages/core/src/build/edge/createEdgeBundle.ts @@ -0,0 +1,233 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { type Plugin, build } from "esbuild"; + +import { loadMiddlewareManifest } from "@/config/util.js"; +import type { MiddlewareInfo } from "@/types/next-types"; +import type { + IncludedConverter, + IncludedOriginResolver, + LazyLoadedOverride, + OverrideOptions, + RouteTemplate, + SplittedFunctionOptions, +} from "@/types/open-next"; +import type { OriginResolver } from "@/types/overrides.js"; + +import logger from "../../logger.js"; +import { ContentUpdater } from "../../plugins/content-updater.js"; +import { openNextEdgePlugins } from "../../plugins/edge.js"; +import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js"; +import { openNextReplacementPlugin } from "../../plugins/replacement.js"; +import { openNextResolvePlugin } from "../../plugins/resolve.js"; +import { getCrossPlatformPathRegex } from "../../utils/regex.js"; +import { type BuildOptions, isEdgeRuntime, copyOpenNextConfig, esbuildAsync } from "../helper.js"; + +type Override = OverrideOptions & { + originResolver?: LazyLoadedOverride | IncludedOriginResolver; +}; +interface BuildEdgeBundleOptions { + middlewareInfo?: MiddlewareInfo; + entrypoint: string; + outfile: string; + options: BuildOptions; + overrides?: Override; + defaultConverter?: IncludedConverter; + additionalInject?: string; + additionalExternals?: string[]; + onlyBuildOnce?: boolean; + name: string; + additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[]; +} + +export async function buildEdgeBundle({ + middlewareInfo, + entrypoint, + outfile, + options, + defaultConverter, + overrides, + additionalInject, + additionalExternals, + onlyBuildOnce, + name, + additionalPlugins: additionalPluginsFn, +}: BuildEdgeBundleOptions) { + const isInCloudflare = await isEdgeRuntime(overrides); + function override(target: T) { + return typeof overrides?.[target] === "string" || typeof overrides?.[target] === "function" + ? overrides[target] + : undefined; + } + const contentUpdater = new ContentUpdater(options); + const additionalPlugins = additionalPluginsFn ? additionalPluginsFn(contentUpdater) : []; + await esbuildAsync( + { + entryPoints: [entrypoint], + bundle: true, + outfile, + external: ["node:*", "next", "@aws-sdk/*"], + target: "es2022", + platform: "neutral", + plugins: [ + openNextResolvePlugin({ + overrides: { + wrapper: override("wrapper") ?? "aws-lambda", + converter: override("converter") ?? defaultConverter, + tagCache: override("tagCache") ?? "dynamodb-lite", + incrementalCache: override("incrementalCache") ?? "s3-lite", + queue: override("queue") ?? "sqs-lite", + originResolver: override("originResolver") ?? "pattern-env", + proxyExternalRequest: override("proxyExternalRequest") ?? "node", + }, + fnName: name, + }), + openNextExternalMiddlewarePlugin(path.join(options.openNextDistDir, "core/edgeFunctionHandler.js")), + openNextEdgePlugins({ + middlewareInfo, + nextDir: path.join(options.appBuildOutputPath, ".next"), + isInCloudflare, + }), + ...additionalPlugins, + // The content updater plugin must be the last plugin + contentUpdater.plugin, + ], + treeShaking: true, + alias: { + path: "node:path", + stream: "node:stream", + fs: "node:fs", + }, + conditions: ["module"], + mainFields: ["module", "main"], + banner: { + js: ` +import {Buffer} from "node:buffer"; +globalThis.Buffer = Buffer; + +import {AsyncLocalStorage} from "node:async_hooks"; +globalThis.AsyncLocalStorage = AsyncLocalStorage; + +${ + "" + /** + * Next.js sets this `__import_unsupported` on `globalThis` (with `configurable: false`): + * https://github.com/vercel/next.js/blob/5b7833e3/packages/next/src/server/web/globals.ts#L94-L98 + * + * It does so in both the middleware and the main server, so if the middleware runs in the same place + * as the main handler this code gets run twice triggering a runtime error. + * + * For this reason we need to patch `Object.defineProperty` to avoid this issue. + */ +} +const defaultDefineProperty = Object.defineProperty; +Object.defineProperty = function(o, p, a) { + if(p=== '__import_unsupported' && Boolean(globalThis.__import_unsupported)) { + return; + } + return defaultDefineProperty(o, p, a); +}; + + ${ + isInCloudflare + ? "" + : ` + const require = (await import("node:module")).createRequire(import.meta.url); + const __filename = (await import("node:url")).fileURLToPath(import.meta.url); + const __dirname = (await import("node:path")).dirname(__filename); + ` + } + ${additionalInject ?? ""} + `, + }, + }, + options + ); + + if (!onlyBuildOnce) { + await build({ + entryPoints: [outfile], + outfile, + allowOverwrite: true, + bundle: true, + minify: options.minify, + platform: "node", + format: "esm", + conditions: ["workerd", "worker", "browser"], + external: ["node:*", ...(additionalExternals ?? [])], + banner: { + js: 'import * as process from "node:process";', + }, + }); + } +} + +export async function generateEdgeBundle( + name: string, + options: BuildOptions, + fnOptions: SplittedFunctionOptions, + additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[] = () => [] +) { + logger.info(`Generating edge bundle for: ${name}`); + + const buildOutputDotNextDir = path.join(options.appBuildOutputPath, ".next"); + + // Create output folder + const outputDir = path.join(options.outputDir, "server-functions", name); + fs.mkdirSync(outputDir, { recursive: true }); + + // Copy open-next.config.mjs + copyOpenNextConfig(options.buildDir, outputDir, true); + + // Load middleware manifest + const middlewareManifest = loadMiddlewareManifest(buildOutputDotNextDir); + + // Find functions + const functions = Object.values(middlewareManifest.functions).filter((fn) => + fnOptions.routes.includes(fn.name as RouteTemplate) + ); + + if (functions.length > 1) { + throw new Error("Only one function is supported for now"); + } + const middlewareInfo = functions[0]; + + copyMiddlewareResources(options, middlewareInfo, outputDir); + + await buildEdgeBundle({ + middlewareInfo, + entrypoint: path.join(options.openNextDistDir, "adapters/edge-adapter.js"), + outfile: path.join(outputDir, "index.mjs"), + options, + overrides: fnOptions.override, + additionalExternals: options.config.edgeExternals, + name, + additionalPlugins, + }); +} + +/** + * Copy wasm files and assets into the destDir. + */ +export function copyMiddlewareResources( + options: BuildOptions, + middlewareInfo: MiddlewareInfo | undefined, + destDir: string +) { + fs.mkdirSync(path.join(destDir, "wasm"), { recursive: true }); + for (const file of middlewareInfo?.wasm ?? []) { + fs.copyFileSync( + path.join(options.appBuildOutputPath, ".next", file.filePath), + path.join(destDir, `wasm/${file.name}.wasm`) + ); + } + + fs.mkdirSync(path.join(destDir, "assets"), { recursive: true }); + for (const file of middlewareInfo?.assets ?? []) { + fs.copyFileSync( + path.join(options.appBuildOutputPath, ".next", file.filePath), + path.join(destDir, `assets/${file.name}`) + ); + } +} diff --git a/packages/core/src/build/generateOutput.ts b/packages/core/src/build/generateOutput.ts new file mode 100644 index 00000000..79716cdc --- /dev/null +++ b/packages/core/src/build/generateOutput.ts @@ -0,0 +1,340 @@ +import * as fs from "node:fs"; +import path from "node:path"; + +import { loadConfig } from "@/config/util.js"; +import type { + BaseOverride, + DefaultOverrideOptions, + ExternalMiddlewareConfig, + FunctionOptions, + LazyLoadedOverride, + OverrideOptions, +} from "@/types/open-next"; + +import { type BuildOptions, getBuildId } from "./helper.js"; + +type BaseFunction = { + handler: string; + bundle: string; +}; + +type OpenNextFunctionOrigin = { + type: "function"; + streaming?: boolean; + wrapper: string; + converter: string; +} & BaseFunction; + +type OpenNextECSOrigin = { + type: "ecs"; + bundle: string; + wrapper: string; + converter: string; + dockerfile: string; +}; + +type CommonOverride = { + queue: string; + incrementalCache: string; + tagCache: string; +}; + +type OpenNextServerFunctionOrigin = OpenNextFunctionOrigin & CommonOverride; +type OpenNextServerECSOrigin = OpenNextECSOrigin & CommonOverride; + +type OpenNextS3Origin = { + type: "s3"; + originPath: string; + copy: { + from: string; + to: string; + cached: boolean; + versionedSubDir?: string; + }[]; +}; + +type OpenNextOrigins = OpenNextServerFunctionOrigin | OpenNextServerECSOrigin | OpenNextS3Origin; + +type ImageFnOrigins = OpenNextFunctionOrigin & { imageLoader: string }; +type ImageECSOrigins = OpenNextECSOrigin & { imageLoader: string }; + +type ImageOrigins = ImageFnOrigins | ImageECSOrigins; + +type DefaultOrigins = { + s3: OpenNextS3Origin; + default: OpenNextServerFunctionOrigin | OpenNextServerECSOrigin; + imageOptimizer: ImageOrigins; +}; + +interface OpenNextOutput { + edgeFunctions: { + [key: string]: BaseFunction; + } & { + middleware?: BaseFunction & { pathResolver: string }; + }; + origins: DefaultOrigins & { + [key: string]: OpenNextOrigins; + }; + behaviors: { + pattern: string; + origin?: string; + edgeFunction?: string; + }[]; + additionalProps?: { + disableIncrementalCache?: boolean; + disableTagCache?: boolean; + initializationFunction?: BaseFunction; + warmer?: BaseFunction; + revalidationFunction?: BaseFunction; + }; +} + +const indexHandler = "index.handler"; + +async function canStream(opts: FunctionOptions) { + if (!opts.override?.wrapper) { + return false; + } + if (typeof opts.override.wrapper === "string") { + return opts.override.wrapper === "aws-lambda-streaming"; + } + const wrapper = await opts.override.wrapper(); + return wrapper.supportStreaming; +} + +async function extractOverrideName( + defaultName: string, + override?: LazyLoadedOverride | string +) { + if (!override) { + return defaultName; + } + if (typeof override === "string") { + return override; + } + const overrideModule = await override(); + return overrideModule.name; +} + +async function extractOverrideFn(override?: DefaultOverrideOptions) { + if (!override) { + return { + wrapper: "aws-lambda", + converter: "aws-apigw-v2", + }; + } + const wrapper = await extractOverrideName("aws-lambda", override.wrapper); + const converter = await extractOverrideName("aws-apigw-v2", override.converter); + return { wrapper, converter }; +} + +async function extractCommonOverride(override?: OverrideOptions) { + if (!override) { + return { + queue: "sqs", + incrementalCache: "s3", + tagCache: "dynamodb", + }; + } + const queue = await extractOverrideName("sqs", override.queue); + const incrementalCache = await extractOverrideName("s3", override.incrementalCache); + const tagCache = await extractOverrideName("dynamodb", override.tagCache); + return { queue, incrementalCache, tagCache }; +} + +function prefixPattern(basePath: string) { + // Prefix CloudFront distribution behavior path patterns with `basePath` if configured + return (pattern: string) => { + return basePath && basePath.length > 0 ? `${basePath.slice(1)}/${pattern}` : pattern; + }; +} + +export async function generateOutput(options: BuildOptions) { + const { appBuildOutputPath, config } = options; + const edgeFunctions: OpenNextOutput["edgeFunctions"] = {}; + const isExternalMiddleware = config.middleware?.external ?? false; + if (isExternalMiddleware) { + const middlewareConfig = options.config.middleware as ExternalMiddlewareConfig; + edgeFunctions.middleware = { + bundle: ".open-next/middleware", + handler: "handler.handler", + pathResolver: await extractOverrideName("pattern-env", middlewareConfig.originResolver), + ...(await extractOverrideFn(middlewareConfig.override)), + }; + } + // Add edge functions + Object.entries(config.functions ?? {}).forEach(async ([key, value]) => { + if (value.placement === "global") { + edgeFunctions[key] = { + bundle: `.open-next/server-functions/${key}`, + handler: indexHandler, + ...(await extractOverrideFn(value.override)), + }; + } + }); + + const defaultOriginCanstream = await canStream(config.default); + + const nextConfig = loadConfig(path.join(appBuildOutputPath, ".next")); + const prefixer = prefixPattern(nextConfig.basePath ?? ""); + + // First add s3 origins and image optimization + + const defaultOrigins: DefaultOrigins = { + s3: { + type: "s3", + originPath: "_assets", + copy: [ + { + from: ".open-next/assets", + to: nextConfig.basePath ? `_assets${nextConfig.basePath}` : "_assets", + cached: true, + versionedSubDir: prefixer("_next"), + }, + ...(config.dangerous?.disableIncrementalCache + ? [] + : [ + { + from: ".open-next/cache", + to: "_cache", + cached: false, + }, + ]), + ], + }, + imageOptimizer: { + type: "function", + handler: indexHandler, + bundle: ".open-next/image-optimization-function", + streaming: false, + imageLoader: await extractOverrideName("s3", config.imageOptimization?.loader), + ...(await extractOverrideFn(config.imageOptimization?.override)), + }, + default: config.default.override?.generateDockerfile + ? { + type: "ecs", + bundle: ".open-next/server-functions/default", + dockerfile: ".open-next/server-functions/default/Dockerfile", + ...(await extractOverrideFn(config.default.override)), + ...(await extractCommonOverride(config.default.override)), + } + : { + type: "function", + handler: indexHandler, + bundle: ".open-next/server-functions/default", + streaming: defaultOriginCanstream, + ...(await extractOverrideFn(config.default.override)), + ...(await extractCommonOverride(config.default.override)), + }, + }; + + //@ts-expect-error - Not sure how to fix typing here, it complains about the type of imageOptimizer and s3 + const origins: OpenNextOutput["origins"] = defaultOrigins; + + // Then add function origins + await Promise.all( + Object.entries(config.functions ?? {}).map(async ([key, value]) => { + if (!value.placement || value.placement === "regional") { + if (value.override?.generateDockerfile) { + origins[key] = { + type: "ecs", + bundle: `.open-next/server-functions/${key}`, + dockerfile: `.open-next/server-functions/${key}/Dockerfile`, + ...(await extractOverrideFn(value.override)), + ...(await extractCommonOverride(value.override)), + }; + } else { + const streaming = await canStream(value); + origins[key] = { + type: "function", + handler: indexHandler, + bundle: `.open-next/server-functions/${key}`, + streaming, + ...(await extractOverrideFn(value.override)), + ...(await extractCommonOverride(value.override)), + }; + } + } + }) + ); + + // Then we need to compute the behaviors + const behaviors: OpenNextOutput["behaviors"] = [ + { pattern: prefixer("_next/image*"), origin: "imageOptimizer" }, + ]; + + // Then we add the routes + Object.entries(config.functions ?? {}).forEach(([key, value]) => { + const patterns = "patterns" in value ? value.patterns : ["*"]; + patterns.forEach((pattern) => { + behaviors.push({ + pattern: prefixer(pattern.replace(/BUILD_ID/, getBuildId(options))), + origin: value.placement === "global" ? undefined : key, + edgeFunction: value.placement === "global" ? key : isExternalMiddleware ? "middleware" : undefined, + }); + }); + }); + + // We finish with the default behavior so that they don't override the others + behaviors.push({ + pattern: prefixer("_next/data/*"), + origin: "default", + edgeFunction: isExternalMiddleware ? "middleware" : undefined, + }); + behaviors.push({ + pattern: "*", // This is the default behavior + origin: "default", + edgeFunction: isExternalMiddleware ? "middleware" : undefined, + }); + + //Compute behaviors for assets files + const assetPath = path.join(appBuildOutputPath, ".open-next", "assets"); + fs.readdirSync(assetPath).forEach((item) => { + if (fs.statSync(path.join(assetPath, item)).isDirectory()) { + behaviors.push({ + pattern: prefixer(`${item}/*`), + origin: "s3", + }); + } else { + behaviors.push({ + pattern: prefixer(item), + origin: "s3", + }); + } + }); + + // Check if we produced a dynamodb provider output + const isTagCacheDisabled = + config.dangerous?.disableTagCache || + !fs.existsSync(path.join(appBuildOutputPath, ".open-next", "dynamodb-provider")); + + const output: OpenNextOutput = { + edgeFunctions, + origins, + behaviors, + additionalProps: { + disableIncrementalCache: config.dangerous?.disableIncrementalCache, + disableTagCache: config.dangerous?.disableTagCache, + warmer: { + handler: indexHandler, + bundle: ".open-next/warmer-function", + }, + initializationFunction: isTagCacheDisabled + ? undefined + : { + handler: indexHandler, + bundle: ".open-next/dynamodb-provider", + }, + revalidationFunction: config.dangerous?.disableIncrementalCache + ? undefined + : { + handler: indexHandler, + bundle: ".open-next/revalidation-function", + }, + }, + }; + fs.writeFileSync( + path.join(appBuildOutputPath, ".open-next", "open-next.output.json"), + JSON.stringify(output) + ); +} diff --git a/packages/core/src/build/helper.ts b/packages/core/src/build/helper.ts new file mode 100644 index 00000000..159495b9 --- /dev/null +++ b/packages/core/src/build/helper.ts @@ -0,0 +1,442 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import url from "node:url"; + +import type { BuildOptions as ESBuildOptions } from "esbuild"; +import { build as buildAsync, buildSync } from "esbuild"; + +import type { DefaultOverrideOptions, OpenNextConfig } from "@/types/open-next.js"; + +import logger from "../logger.js"; + +const require = createRequire(import.meta.url); +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); + +export type BuildOptions = ReturnType; + +export function normalizeOptions(config: OpenNextConfig, distDir: string, tempBuildDir: string) { + const appPath = path.join(process.cwd(), config.appPath || "."); + const buildOutputPath = path.join(process.cwd(), config.buildOutputPath || "."); + const outputDir = path.join(buildOutputPath, ".open-next"); + + const { root: monorepoRoot, packager } = findPackagerAndRoot( + path.join(process.cwd(), config.appPath || ".") + ); + + let appPackageJsonPath: string; + if (config.packageJsonPath) { + const _pkgPath = path.join(process.cwd(), config.packageJsonPath); + appPackageJsonPath = _pkgPath.endsWith("package.json") ? _pkgPath : path.join(_pkgPath, "./package.json"); + } else { + appPackageJsonPath = findNextPackageJsonPath(appPath, monorepoRoot); + } + + const debug = Boolean(process.env.OPEN_NEXT_DEBUG); + + return { + appBuildOutputPath: buildOutputPath, + appPackageJsonPath, + appPath, + appPublicPath: path.join(appPath, "public"), + buildDir: path.join(outputDir, ".build"), + config, + debug, + // Whether ESBuild should minify the code + minify: !debug, + monorepoRoot, + nextVersion: getNextVersion(appPath), + openNextVersion: getOpenNextVersion(), + openNextDistDir: distDir, + outputDir, + packager, + tempBuildDir, + }; +} +/** + * Given the path to a project this function detects the project's repository root (whether the project is in a simple + * repository or a monorepo) as well as the package manager being used. + * + * @param appPath The project's path + * @returns An object containing the root of the project's repo/monorepo as well as the package manager that it uses. + */ +export function findPackagerAndRoot(appPath: string): { + root: string; + packager: "npm" | "pnpm" | "yarn" | "bun"; +} { + let currentPath = appPath; + while (currentPath !== "/") { + const found = [ + // bun can generate yaml lock files (`bun install --yarn`) so bun should be before yarn + { file: "bun.lockb", packager: "bun" as const }, + { file: "bun.lock", packager: "bun" as const }, + { file: "package-lock.json", packager: "npm" as const }, + { file: "yarn.lock", packager: "yarn" as const }, + { file: "pnpm-lock.yaml", packager: "pnpm" as const }, + ].find((f) => fs.existsSync(path.join(currentPath, f.file))); + + if (found) { + if (currentPath !== appPath) { + logger.info("Monorepo detected at", currentPath); + } + return { root: currentPath, packager: found.packager }; + } + currentPath = path.dirname(currentPath); + } + + // note: a lock file (package-lock.json, yarn.lock, or pnpm-lock.yaml) is + // not found in the app's directory or any of its parent directories. + // We are going to assume that the app is not part of a monorepo. + logger.warn("No lockfile found"); + return { root: appPath, packager: "npm" as const }; +} + +function findNextPackageJsonPath(appPath: string, root: string) { + // This is needed for the case where the app is a single-version monorepo and the package.json is in the root of the monorepo + return fs.existsSync(path.join(appPath, "./package.json")) + ? path.join(appPath, "./package.json") + : path.join(root, "./package.json"); +} + +export function esbuildSync(esbuildOptions: ESBuildOptions, options: BuildOptions) { + const { openNextVersion, debug, minify } = options; + const result = buildSync({ + target: "esnext", + format: "esm", + platform: "node", + bundle: true, + minify, + mainFields: ["module", "main"], + sourcemap: debug ? "inline" : false, + sourcesContent: false, + ...esbuildOptions, + external: ["./open-next.config.mjs", ...(esbuildOptions.external ?? [])], + banner: { + ...esbuildOptions.banner, + js: [ + esbuildOptions.banner?.js || "", + `globalThis.openNextDebug = ${debug};`, + `globalThis.openNextVersion = "${openNextVersion}";`, + ].join(""), + }, + }); + + if (result.errors.length > 0) { + result.errors.forEach((error) => logger.error(error)); + throw new Error(`There was a problem bundling ${(esbuildOptions.entryPoints as string[])[0]}.`); + } +} + +export async function esbuildAsync(esbuildOptions: ESBuildOptions, options: BuildOptions) { + const { openNextVersion, debug, minify } = options; + // Dump ESBuild build metadata to file in debug mode + const metafile = debug && esbuildOptions.outfile !== undefined; + const result = await buildAsync({ + target: "esnext", + format: "esm", + platform: "node", + bundle: true, + // TODO(vicb): revert to `minify,` + minify: false, + metafile, + mainFields: ["module", "main"], + sourcemap: debug ? "inline" : false, + sourcesContent: false, + ...esbuildOptions, + external: [...(esbuildOptions.external ?? []), "next", "./open-next.config.mjs"], + banner: { + ...esbuildOptions.banner, + js: [ + esbuildOptions.banner?.js || "", + `globalThis.openNextDebug = ${debug};`, + `globalThis.openNextVersion = "${openNextVersion}";`, + ].join(""), + }, + }); + + if (result.errors.length > 0) { + result.errors.forEach((error) => logger.error(error)); + throw new Error(`There was a problem bundling ${(esbuildOptions.entryPoints as string[])[0]}.`); + } + + if (result.metafile) { + const metaFile = `${esbuildOptions.outfile}.meta.json`; + fs.writeFileSync(metaFile, JSON.stringify(result.metafile, null, 2)); + } +} + +/** + * Type of the parameter of `traverseFiles` callbacks + */ +export type TraversePath = { + absolutePath: string; + relativePath: string; +}; + +/** + * Recursively traverse files in a directory and call `callbackFn` when `conditionFn` returns true + * + * The callbacks are passed both the absolute and relative (to root) path to files. + * + * @param root - Root directory to search + * @param conditionFn - Called to determine if `callbackFn` should be called. + * @param callbackFn - Called when `conditionFn` returns true. + * @param searchingDir - Directory to search (used for recursion) + */ +export function traverseFiles( + root: string, + conditionFn: (paths: TraversePath) => boolean, + callbackFn: (paths: TraversePath) => void, + searchingDir = "" +) { + fs.readdirSync(path.join(root, searchingDir)).forEach((file) => { + const relativePath = path.join(searchingDir, file); + const absolutePath = path.join(root, relativePath); + + if (fs.statSync(absolutePath).isDirectory()) { + traverseFiles(root, conditionFn, callbackFn, relativePath); + return; + } + + if (conditionFn({ absolutePath, relativePath })) { + callbackFn({ absolutePath, relativePath }); + } + }); +} + +/** + * Recursively delete files. + * + * @see `traverseFiles`. + * + * @param root Root directory to search. + * @param conditionFn Predicate used to delete the files. + */ +export function removeFiles(root: string, conditionFn: (paths: TraversePath) => boolean) { + traverseFiles(root, conditionFn, ({ absolutePath }) => fs.rmSync(absolutePath, { force: true })); +} + +export function getHtmlPages(dotNextPath: string) { + // Get a list of HTML pages + // + // sample return value: + // Set([ + // '404.html', + // 'csr.html', + // 'image-html-tag.html', + // ]) + const manifestPath = path.join(dotNextPath, ".next/server/pages-manifest.json"); + const manifest = fs.readFileSync(manifestPath, "utf-8"); + return Object.entries(JSON.parse(manifest)) + .filter(([_, value]) => (value as string).endsWith(".html")) + .map(([_, value]) => (value as string).replace(/^pages\//, "")) + .reduce((acc, page) => acc.add(page), new Set()); +} + +export function getBuildId(options: BuildOptions) { + return fs.readFileSync(path.join(options.appBuildOutputPath, ".next/BUILD_ID"), "utf-8").trim(); +} + +export function getOpenNextVersion(): string { + return require(path.join(__dirname, "../../package.json")).version; +} + +export function getNextVersion(appPath: string): string { + // We cannot just require("next/package.json") because it could be executed in a different directory + const nextPackageJsonPath = require.resolve("next/package.json", { + paths: [appPath], + }); + const version = require(nextPackageJsonPath)?.version; + + if (!version) { + throw new Error("Failed to find Next version"); + } + + // Drop the -canary.n suffix + return version.split("-")[0]; +} + +export type SemverOp = "=" | ">=" | "<=" | ">" | "<"; + +/** + * Compare two semver versions. + * + * @param v1 - First version. Can be "latest", otherwise it should be a valid semver version in the format of `major.minor.patch`. Usually is the next version from the package.json without canary suffix. If minor or patch are missing, they are considered 0. + * @param v2 - Second version. Should not be "latest", it should be a valid semver version in the format of `major.minor.patch`. If minor or patch are missing, they are considered 0. + * @example + * compareSemver("2.0.0", ">=", "1.0.0") === true + */ +export function compareSemver(v1: string, operator: SemverOp, v2: string): boolean { + // - = 0 when versions are equal + // - > 0 if v1 > v2 + // - < 0 if v2 > v1 + let versionDiff = 0; + if (v1 === "latest") { + versionDiff = 1; + } else { + if (/^[^\d]/.test(v1)) { + // oxlint-disable-next-line no-param-reassign + v1 = v1.substring(1); + } + if (/^[^\d]/.test(v2)) { + // oxlint-disable-next-line no-param-reassign + v2 = v2.substring(1); + } + const [major1, minor1 = 0, patch1 = 0] = v1.split(".").map(Number); + const [major2, minor2 = 0, patch2 = 0] = v2.split(".").map(Number); + if (Number.isNaN(major1) || Number.isNaN(major2)) { + throw new Error("The major version is required."); + } + + if (major1 !== major2) { + versionDiff = major1 - major2; + } else if (minor1 !== minor2) { + versionDiff = minor1 - minor2; + } else if (patch1 !== patch2) { + versionDiff = patch1 - patch2; + } + } + + switch (operator) { + case "=": + return versionDiff === 0; + case ">=": + return versionDiff >= 0; + case "<=": + return versionDiff <= 0; + case ">": + return versionDiff > 0; + case "<": + return versionDiff < 0; + default: + throw new Error(`Unsupported operator: ${operator}`); + } +} + +export function copyOpenNextConfig(inputDir: string, outputDir: string, isEdge = false) { + // Copy open-next.config.mjs + fs.copyFileSync( + path.join(inputDir, isEdge ? "open-next.config.edge.mjs" : "open-next.config.mjs"), + path.join(outputDir, "open-next.config.mjs") + ); +} + +export function copyEnvFile(appPath: string, packagePath: string, outputPath: string) { + const baseAppPath = path.join(appPath, ".next/standalone", packagePath); + const baseOutputPath = path.join(outputPath, packagePath); + const envPath = path.join(baseAppPath, ".env"); + if (fs.existsSync(envPath)) { + fs.copyFileSync(envPath, path.join(baseOutputPath, ".env")); + } + const envProdPath = path.join(baseAppPath, ".env.production"); + if (fs.existsSync(envProdPath)) { + fs.copyFileSync(envProdPath, path.join(baseOutputPath, ".env.production")); + } +} + +/** + * Check we are in a Nextjs app by looking for the Nextjs config file. + */ +export function checkRunningInsideNextjsApp({ appPath }: { appPath: string }) { + const extension = ["js", "cjs", "mjs", "ts"].find((ext) => + fs.existsSync(path.join(appPath, `next.config.${ext}`)) + ); + if (!extension) { + logger.error( + "Error: next.config.js not found. Please make sure you are running this command inside a Next.js app." + ); + process.exit(1); + } +} + +export function printNextjsVersion(options: BuildOptions) { + logger.info(`Next.js version : ${options.nextVersion}`); +} + +export function printOpenNextVersion(options: BuildOptions) { + logger.info(`OpenNext v${options.openNextVersion}`); +} + +/** + * Populates the build directory with the compiled configuration files. + * + * We need to get the build relative to the cwd to find the compiled config. + * This is needed for the case where the app is a single-version monorepo + * and the package.json is in the root of the monorepo where the build is in + * the app directory, but the compiled config is in the root of the monorepo. + */ +export function initOutputDir(options: BuildOptions) { + fs.rmSync(options.outputDir, { recursive: true, force: true }); + const { buildDir } = options; + fs.mkdirSync(buildDir, { recursive: true }); + fs.cpSync(options.tempBuildDir, buildDir, { recursive: true }); +} + +/** + * @returns Whether the edge runtime is used + */ +export async function isEdgeRuntime(overrides: DefaultOverrideOptions | undefined) { + if (!overrides?.wrapper) { + return false; + } + if (typeof overrides.wrapper === "string") { + return ["cloudflare-edge", "cloudflare", "cloudflare-node"].includes(overrides.wrapper); + } + return (await overrides?.wrapper?.())?.edgeRuntime; +} + +export function getPackagePath(options: BuildOptions) { + return path.relative(options.monorepoRoot, options.appBuildOutputPath); +} + +/** + * Returns the Next.js runtime used: "webpack" or "turbopack" + * + * Must be called after building the Next.js app. + * + * @param options + * @returns the Next.js runtime used: "webpack" or "turbopack" + */ +export function getBundlerRuntime(options: BuildOptions): "webpack" | "turbopack" { + const dotNextServerPath = path.join(options.appPath, ".next/server"); + if (fs.existsSync(path.join(dotNextServerPath, "webpack-runtime.js"))) { + return "webpack"; + } + if ( + fs.existsSync(path.join(dotNextServerPath, "chunks/[turbopack]_runtime.js")) || + fs.existsSync(path.join(dotNextServerPath, "chunks/ssr/[turbopack]_runtime.js")) + ) { + return "turbopack"; + } + + throw new Error("Unable to determine Next.js runtime (webpack or turbopack)"); +} + +/** + * Finds the path to the Next configuration file if it exists. + * + * @param appPath The directory to check for the Next config file + * @returns An object with the full path to Next config file alongside a flag indicating whether the file is in typescript if it exists, undefined otherwise + */ +export function findNextConfig({ + appPath, +}: Pick): { path: string; isTypescript: boolean } | undefined { + const extensions = [ + { ext: "ts", isTypescript: true }, + { ext: "mts", isTypescript: true }, + { ext: "cts", isTypescript: true }, + { ext: "js", isTypescript: false }, + { ext: "mjs", isTypescript: false }, + { ext: "cjs", isTypescript: false }, + ]; + + for (const { ext, isTypescript } of extensions) { + const configPath = path.join(appPath, `next.config.${ext}`); + if (fs.existsSync(configPath)) { + return { + path: configPath, + isTypescript, + }; + } + } +} diff --git a/packages/core/src/build/installDeps.ts b/packages/core/src/build/installDeps.ts new file mode 100644 index 00000000..490e34cc --- /dev/null +++ b/packages/core/src/build/installDeps.ts @@ -0,0 +1,78 @@ +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import type { InstallOptions } from "@/types/open-next"; + +import logger from "../logger.js"; + +export function installDependencies(outputDir: string, installOptions?: InstallOptions) { + try { + if (!installOptions) { + return; + } + const name = outputDir.split("/").pop(); + // First we create a tempDir + const tempInstallDir = fs.mkdtempSync(path.join(os.tmpdir(), `open-next-install-${name}`)); + logger.info(`Installing dependencies for ${name}...`); + // We then need to run install in the tempDir + // We don't install in the output dir directly because it could contain a package.json, and npm would then try to reinstall not complete deps from tracing the files + const archOption = installOptions.arch ? `--arch=${installOptions.arch}` : ""; + const targetOption = installOptions.nodeVersion ? `--target=${installOptions.nodeVersion}` : ""; + const libcOption = installOptions.libc ? `--libc=${installOptions.libc}` : ""; + const osOption = `--os=${installOptions.os ?? "linux"}`; + + const additionalArgs = installOptions.additionalArgs ?? ""; + const installCommand = `npm install ${osOption} ${archOption} ${targetOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`; + execSync(installCommand, { + stdio: "pipe", + cwd: tempInstallDir, + env: { + ...process.env, + SHARP_IGNORE_GLOBAL_LIBVIPS: "1", + }, + }); + + // Copy the node_modules to the outputDir + fs.cpSync(path.join(tempInstallDir, "node_modules"), path.join(outputDir, "node_modules"), { + recursive: true, + force: true, + dereference: true, + }); + + // https://github.com/nodejs/node/issues/59168 + // This is a workaround for all Node versions. It seems to be an issue continue to affect new versions aswell. + // Therefor I think the most logical solution is to always run this workaround instead of trying to figure out which versions are affected. + const tempBinDir = path.join(tempInstallDir, "node_modules", ".bin"); + const outputBinDir = path.join(outputDir, "node_modules", ".bin"); + + for (const fileName of fs.readdirSync(tempBinDir)) { + const symlinkPath = path.join(tempBinDir, fileName); + const stat = fs.lstatSync(symlinkPath); + + if (stat.isSymbolicLink()) { + const linkTarget = fs.readlinkSync(symlinkPath); + const realFilePath = path.resolve(tempBinDir, linkTarget); + + const outputFilePath = path.join(outputBinDir, fileName); + + if (fs.existsSync(outputFilePath)) { + fs.unlinkSync(outputFilePath); + } + + fs.copyFileSync(realFilePath, outputFilePath); + fs.chmodSync(outputFilePath, "755"); + logger.debug(`Replaced symlink ${fileName} with actual file`); + } + } + // End of Node Workaround + + // Cleanup tempDir + fs.rmSync(tempInstallDir, { recursive: true, force: true }); + logger.info(`Dependencies installed for ${name}`); + } catch (e: unknown) { + logger.error(e instanceof Error ? e.message : String(e)); + logger.error("Could not install dependencies"); + } +} diff --git a/packages/core/src/build/middleware/buildNodeMiddleware.ts b/packages/core/src/build/middleware/buildNodeMiddleware.ts new file mode 100644 index 00000000..8b4a2c74 --- /dev/null +++ b/packages/core/src/build/middleware/buildNodeMiddleware.ts @@ -0,0 +1,108 @@ +import fs from "node:fs"; +import path from "node:path"; + +import type { IncludedOriginResolver, LazyLoadedOverride, OverrideOptions } from "@/types/open-next.js"; +import type { OriginResolver } from "@/types/overrides.js"; +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js"; +import { openNextReplacementPlugin } from "../../plugins/replacement.js"; +import { openNextResolvePlugin } from "../../plugins/resolve.js"; +import { copyTracedFiles } from "../copyTracedFiles.js"; +import * as buildHelper from "../helper.js"; +import { installDependencies } from "../installDeps.js"; + +type Override = OverrideOptions & { + originResolver?: LazyLoadedOverride | IncludedOriginResolver; +}; + +export async function buildExternalNodeMiddleware(options: buildHelper.BuildOptions) { + const { appBuildOutputPath, config, outputDir } = options; + if (!config.middleware?.external) { + throw new Error("This function should only be called for external middleware"); + } + const outputPath = path.join(outputDir, "middleware"); + fs.mkdirSync(outputPath, { recursive: true }); + + // Copy open-next.config.mjs + buildHelper.copyOpenNextConfig( + options.buildDir, + outputPath, + await buildHelper.isEdgeRuntime(config.middleware.override) + ); + const overrides = { + ...config.middleware.override, + originResolver: config.middleware.originResolver, + }; + const packagePath = buildHelper.getPackagePath(options); + + // TODO: change this so that we don't copy unnecessary files + await copyTracedFiles({ + buildOutputPath: appBuildOutputPath, + packagePath, + outputDir: outputPath, + routes: [], + skipServerFiles: true, + }); + + function override(target: T) { + return typeof overrides?.[target] === "string" ? overrides[target] : undefined; + } + + // Bundle middleware + await buildHelper.esbuildAsync( + { + entryPoints: [path.join(options.openNextDistDir, "adapters", "middleware.js")], + outfile: path.join(outputPath, "handler.mjs"), + external: ["./.next/*"], + platform: "node", + plugins: [ + openNextResolvePlugin({ + overrides: { + wrapper: override("wrapper") ?? "aws-lambda", + converter: override("converter") ?? "aws-cloudfront", + tagCache: override("tagCache") ?? "dynamodb-lite", + incrementalCache: override("incrementalCache") ?? "s3-lite", + queue: override("queue") ?? "sqs-lite", + originResolver: override("originResolver") ?? "pattern-env", + proxyExternalRequest: override("proxyExternalRequest") ?? "node", + }, + fnName: "middleware", + }), + openNextExternalMiddlewarePlugin( + path.join(options.openNextDistDir, "core", "nodeMiddlewareHandler.js") + ), + ], + banner: { + js: [ + `globalThis.monorepoPackagePath = '${packagePath}';`, + "import process from 'node:process';", + "import { Buffer } from 'node:buffer';", + "import { AsyncLocalStorage } from 'node:async_hooks';", + "import { createRequire as topLevelCreateRequire } from 'module';", + "const require = topLevelCreateRequire(import.meta.url);", + "import bannerUrl from 'url';", + "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", + ].join(""), + }, + }, + options + ); + + // Do we need to copy or do something with env file here? + + installDependencies(outputPath, config.middleware?.install); +} + +export async function buildBundledNodeMiddleware(options: buildHelper.BuildOptions) { + await buildHelper.esbuildAsync( + { + entryPoints: [path.join(options.openNextDistDir, "core/nodeMiddlewareHandler.js")], + external: ["./.next/*"], + outfile: path.join(options.buildDir, "middleware.mjs"), + bundle: true, + platform: "node", + }, + options + ); +} diff --git a/packages/core/src/build/patch/astCodePatcher.ts b/packages/core/src/build/patch/astCodePatcher.ts new file mode 100644 index 00000000..fc3a1ecb --- /dev/null +++ b/packages/core/src/build/patch/astCodePatcher.ts @@ -0,0 +1,110 @@ +// Mostly copied from the cloudflare adapter +import { readFileSync } from "node:fs"; + +import { type Edit, Lang, type NapiConfig, type SgNode, parse } from "@ast-grep/napi"; +import yaml from "yaml"; + +import type { PatchCodeFn } from "./codePatcher"; + +export type * from "@ast-grep/napi"; + +/** + * fix has the same meaning as in yaml rules + * see https://ast-grep.github.io/guide/rewrite-code.html#using-fix-in-yaml-rule + */ +export type RuleConfig = NapiConfig & { fix?: string }; + +/** + * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format + * + * The rule must have a `fix` to rewrite the matched node. + * + * Tip: use https://ast-grep.github.io/playground.html to create rules. + * + * @param rule The rule. Either a yaml string or an instance of `RuleConfig` + * @param root The root node + * @param once only apply once + * @returns A list of edits and a list of matches. + */ +export function applyRule( + rule: string | RuleConfig, + root: SgNode, + { once = false } = {} +): { + edits: Edit[]; + matches: SgNode[]; +} { + const ruleConfig: RuleConfig = typeof rule === "string" ? yaml.parse(rule) : rule; + if (ruleConfig.transform) { + throw new Error("transform is not supported"); + } + if (!ruleConfig.fix) { + throw new Error("no fix to apply"); + } + + const fix = ruleConfig.fix; + + const matches = once ? [root.find(ruleConfig)].filter((m) => m !== null) : root.findAll(ruleConfig); + + const edits: Edit[] = []; + + matches.forEach((match) => { + edits.push( + match.replace( + // Replace known placeholders by their value + fix + .replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => + match + .getMultipleMatches(name) + .map((n) => n.text()) + .join("") + ) + .replace(/\$([A-Z0-9_]+)/g, (m, name) => match.getMatch(name)?.text() ?? m) + ) + ); + }); + + return { edits, matches }; +} + +/** + * Parse a file and obtain its root. + * + * @param path The file path + * @param lang The language to parse. Defaults to TypeScript. + * @returns The root for the file. + */ +export function parseFile(path: string, lang = Lang.TypeScript): SgNode { + return parse(lang, readFileSync(path, { encoding: "utf-8" })).root(); +} + +/** + * Patches the code from by applying the rule. + * + * This function is mainly for on off edits and tests, + * use `getRuleEdits` to apply multiple rules. + * + * @param code The source code + * @param rule The astgrep rule (yaml or NapiConfig) + * @param lang The language used by the source code + * @param lang Whether to apply the rule only once + * @returns The patched code + */ +export function patchCode( + code: string, + rule: string | RuleConfig, + { lang = Lang.TypeScript, once = false } = {} +): string { + const node = parse(lang, code).root(); + const { edits } = applyRule(rule, node, { once }); + return node.commitEdits(edits); +} + +/** + * @param rule + * @param lang + * @returns a callback applying the the rule. + */ +export function createPatchCode(rule: string | RuleConfig, lang = Lang.TypeScript): PatchCodeFn { + return async ({ code }) => patchCode(code, rule, { lang }); +} diff --git a/packages/core/src/build/patch/codePatcher.ts b/packages/core/src/build/patch/codePatcher.ts new file mode 100644 index 00000000..57bbb311 --- /dev/null +++ b/packages/core/src/build/patch/codePatcher.ts @@ -0,0 +1,176 @@ +import * as fs from "node:fs/promises"; + +import logger from "../../logger.js"; +import type { getManifests } from "../copyTracedFiles.js"; +import * as buildHelper from "../helper.js"; + +/** + * Accepted formats: + * - `">=16.0.0"` + * - `"<=16.0.0"` + * - `">=16.0.0 <=17.0.0"` + * + * **Be careful with spaces** + */ +export type Versions = + | `>=${number}.${number}.${number} <=${number}.${number}.${number}` + | `>=${number}.${number}.${number}` + | `<=${number}.${number}.${number}`; + +export type PatchCodeFn = (args: { + /** + * The code of the file that needs to be patched + */ + code: string; + /** + * The final path of the file that needs to be patched + */ + filePath: string; + /** + * All files that are traced and will be included in the bundle + */ + tracedFiles: string[]; + /** + * Next.js manifests that are used by Next at runtime + */ + manifests: ReturnType; + /** + * OpenNext build options + */ + buildOptions: buildHelper.BuildOptions; +}) => Promise; + +interface IndividualPatch { + pathFilter: RegExp; + contentFilter?: RegExp; + patchCode: PatchCodeFn; + // Only apply the patch to specific versions of Next.js + versions?: Versions; +} + +export interface CodePatcher { + name: string; + patches: IndividualPatch[]; +} + +export function parseVersions(versions?: Versions): { + before?: string; + after?: string; +} { + if (!versions) { + return {}; + } + // We need to use regex to extract the versions + const versionRegex = /([<>]=)(\d+\.\d+\.\d+)/g; + const matches = Array.from(versions.matchAll(versionRegex)); + if (matches.length === 0) { + throw new Error("Invalid version range, no matches found"); + } + if (matches.length > 2) { + throw new Error("Invalid version range, too many matches found"); + } + let after: string | undefined; + let before: string | undefined; + for (const match of matches) { + const [_, operator, version] = match; + if (operator === "<=") { + before = version; + } else { + after = version; + } + } + // Before returning we reconstruct the version string and compare it to the original + // If they don't match we throw an error + // We have to do this because template literal types here seems to allow for extra spaces + // that could easily break the version comparison and allow some patch to be applied on incorrect versions + // This might even go unnoticed + const reconstructedVersion = `${after ? `>=${after}` : ""}${ + before ? `${after ? " " : ""}<=${before}` : "" + }`; + if (reconstructedVersion !== versions) { + throw new Error("Invalid version range, the reconstructed version does not match the original"); + } + return { + before, + after, + }; +} + +/** + * Check whether the version is in the range + * + * @param version A semver version + * @param versionRange A version range + * @returns whether the version satisfies the range + */ +export function isVersionInRange(version: string, versionRange?: Versions): boolean { + const { before, after } = parseVersions(versionRange); + + let inRange = true; + + if (before) { + inRange &&= buildHelper.compareSemver(version, "<=", before); + } + + if (after) { + inRange &&= buildHelper.compareSemver(version, ">=", after); + } + + return inRange; +} + +export async function applyCodePatches( + buildOptions: buildHelper.BuildOptions, + tracedFiles: string[], + manifests: ReturnType, + codePatcher: CodePatcher[] +) { + logger.time("Applying code patches"); + + // We first filter against the version + // We also flatten the array of patches so that we get both the name and all the necessary patches + const patchesToApply = codePatcher.flatMap(({ name, patches }) => { + return patches + .filter(({ versions }) => isVersionInRange(buildOptions.nextVersion, versions)) + .map((patch) => ({ patch, name })); + }); + + await Promise.all( + tracedFiles.map(async (filePath) => { + // We check the filename against the filter to see if we should apply the patch + const patchMatchingPath = patchesToApply.filter(({ patch }) => { + return filePath.match(patch.pathFilter); + }); + if (patchMatchingPath.length === 0) { + return; + } + const content = await fs.readFile(filePath, "utf-8"); + // We filter a last time against the content this time + const patchToApply = patchMatchingPath.filter(({ patch }) => { + if (!patch.contentFilter) { + return true; + } + return content.match(patch.contentFilter); + }); + if (patchToApply.length === 0) { + return; + } + + // We apply the patches + let patchedContent = content; + + for (const { patch, name } of patchToApply) { + logger.debug(`Applying code patch: ${name} to ${filePath}`); + patchedContent = await patch.patchCode({ + code: patchedContent, + filePath, + tracedFiles, + manifests, + buildOptions, + }); + } + await fs.writeFile(filePath, patchedContent); + }) + ); + logger.timeEnd("Applying code patches"); +} diff --git a/packages/core/src/build/patch/patches/index.ts b/packages/core/src/build/patch/patches/index.ts new file mode 100644 index 00000000..a6bb2eba --- /dev/null +++ b/packages/core/src/build/patch/patches/index.ts @@ -0,0 +1,11 @@ +export { getEnvVarsPatch } from "./patchEnvVar.js"; +export { patchNextServer } from "./patchNextServer.js"; +export { + patchFetchCacheForISR, + patchUnstableCacheForISR, + patchUseCacheForISR, +} from "./patchFetchCacheISR.js"; +export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js"; +export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js"; +export { patchNodeEnvironment } from "./patchNodeEnvironment.js"; +export { patchOriginalNextConfig } from "./patchOriginalNextConfig.js"; diff --git a/packages/core/src/build/patch/patches/patchBackgroundRevalidation.ts b/packages/core/src/build/patch/patches/patchBackgroundRevalidation.ts new file mode 100644 index 00000000..9845f774 --- /dev/null +++ b/packages/core/src/build/patch/patches/patchBackgroundRevalidation.ts @@ -0,0 +1,29 @@ +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; + +export const rule = ` +rule: + kind: binary_expression + all: + - has: + kind: unary_expression + regex: "!cachedResponse.isStale" + - has: + kind: member_expression + regex: "context.isPrefetch" +fix: + 'true'`; + +export const patchBackgroundRevalidation = { + name: "patchBackgroundRevalidation", + patches: [ + { + // TODO: test for earlier versions of Next + versions: ">=14.1.0", + pathFilter: getCrossPlatformPathRegex("server/response-cache/index.js"), + patchCode: createPatchCode(rule), + }, + ], +} satisfies CodePatcher; diff --git a/packages/core/src/build/patch/patches/patchEnvVar.ts b/packages/core/src/build/patch/patches/patchEnvVar.ts new file mode 100644 index 00000000..9b33fdec --- /dev/null +++ b/packages/core/src/build/patch/patches/patchEnvVar.ts @@ -0,0 +1,54 @@ +import * as buildHelper from "../../helper.js"; +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher"; + +/** + * Creates a rule to replace `process.env.${envVar}` by `value` in the condition of if statements + * This is used to avoid loading unnecessary deps at runtime + * @param envVar The env var that we want to replace + * @param value The value that we want to replace it with + * @returns + */ +export const envVarRuleCreator = (envVar: string, value: string) => ` +rule: + kind: member_expression + pattern: process.env.${envVar} + inside: + kind: parenthesized_expression + stopBy: end + inside: + kind: if_statement +fix: + '${value}' +`; + +export function getEnvVarsPatch(BuildOptions: buildHelper.BuildOptions): CodePatcher { + const isTurbopack = buildHelper.getBundlerRuntime(BuildOptions) === "turbopack"; + + return { + name: "patch-env-vars", + patches: [ + // This patch will set the `NEXT_RUNTIME` env var to "node" to avoid loading unnecessary edge deps at runtime + { + versions: ">=15.0.0", + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.NEXT_RUNTIME/, + patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), + }, + // This patch will set `NODE_ENV` to production to avoid loading unnecessary dev deps at runtime + { + versions: ">=15.0.0", + pathFilter: /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/, + contentFilter: /process\.env\.NODE_ENV/, + patchCode: createPatchCode(envVarRuleCreator("NODE_ENV", '"production"')), + }, + // This patch will set `TURBOPACK` env to false to avoid loading turbopack related deps at runtime + { + versions: ">=15.0.0", + pathFilter: /module\.compiled\.js$/, + contentFilter: /process\.env\.TURBOPACK/, + patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", JSON.stringify(isTurbopack))), + }, + ], + }; +} diff --git a/packages/core/src/build/patch/patches/patchFetchCacheISR.ts b/packages/core/src/build/patch/patches/patchFetchCacheISR.ts new file mode 100644 index 00000000..38fa023b --- /dev/null +++ b/packages/core/src/build/patch/patches/patchFetchCacheISR.ts @@ -0,0 +1,149 @@ +import { Lang } from "@ast-grep/napi"; + +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; + +export const fetchRule = ` +rule: + kind: member_expression + pattern: $WORK_STORE.isOnDemandRevalidate + inside: + kind: ternary_expression + all: + - has: {kind: 'null'} + - has: + kind: await_expression + has: + kind: call_expression + all: + - has: + kind: member_expression + has: + kind: property_identifier + field: property + regex: get + - has: + kind: arguments + has: + kind: object + has: + kind: pair + all: + - has: + kind: property_identifier + field: key + regex: softTags + inside: + kind: variable_declarator + +fix: + ($WORK_STORE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation) +`; + +export const unstable_cacheRule = ` +rule: + kind: member_expression + pattern: $STORE_OR_CACHE.isOnDemandRevalidate + inside: + kind: if_statement + stopBy: end + has: + kind: statement_block + has: + kind: variable_declarator + has: + kind: await_expression + has: + kind: call_expression + all: + - has: + kind: member_expression + has: + kind: property_identifier + field: property + regex: get + - has: + kind: arguments + has: + kind: object + has: + kind: pair + all: + - has: + kind: property_identifier + field: key + regex: softTags + stopBy: end +fix: + ($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation) +`; + +export const useCacheRule = ` +rule: + kind: member_expression + pattern: $STORE_OR_CACHE.isOnDemandRevalidate + inside: + kind: binary_expression + has: + kind: member_expression + pattern: $STORE_OR_CACHE.isDraftMode + inside: + kind: if_statement + stopBy: end + has: + kind: return_statement + any: + - has: + kind: 'true' + - has: + regex: '!0' + stopBy: end +fix: + '($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)'`; + +export const patchFetchCacheForISR: CodePatcher = { + name: "patch-fetch-cache-for-isr", + patches: [ + { + versions: ">=14.0.0", + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, + { escape: false } + ), + contentFilter: /\.isOnDemandRevalidate/, + patchCode: createPatchCode(fetchRule, Lang.JavaScript), + }, + ], +}; + +export const patchUnstableCacheForISR: CodePatcher = { + name: "patch-unstable-cache-for-isr", + patches: [ + { + versions: ">=14.2.0", + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|spec-extension/unstable-cache\.js)$`, + { escape: false } + ), + contentFilter: /\.isOnDemandRevalidate/, + patchCode: createPatchCode(unstable_cacheRule, Lang.JavaScript), + }, + ], +}; + +export const patchUseCacheForISR: CodePatcher = { + name: "patch-use-cache-for-isr", + patches: [ + { + versions: ">=15.3.0", + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|\.runtime\..*\.js|use-cache/use-cache-wrapper\.js)$`, + { escape: false } + ), + contentFilter: /\.isOnDemandRevalidate/, + patchCode: createPatchCode(useCacheRule, Lang.JavaScript), + }, + ], +}; diff --git a/packages/core/src/build/patch/patches/patchFetchCacheWaitUntil.ts b/packages/core/src/build/patch/patches/patchFetchCacheWaitUntil.ts new file mode 100644 index 00000000..671d58d5 --- /dev/null +++ b/packages/core/src/build/patch/patches/patchFetchCacheWaitUntil.ts @@ -0,0 +1,41 @@ +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; + +export const rule = ` +rule: + kind: call_expression + pattern: $PROMISE + all: + - has: { pattern: $_.arrayBuffer().then, stopBy: end } + - has: { pattern: "Buffer.from", stopBy: end } + - any: + - inside: + kind: sequence_expression + inside: + kind: return_statement + - inside: + kind: expression_statement + precedes: + kind: return_statement + - has: { pattern: $_.FETCH, stopBy: end } + +fix: | + globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE) +`; + +export const patchFetchCacheSetMissingWaitUntil: CodePatcher = { + name: "patch-fetch-cache-set-missing-wait-until", + patches: [ + { + versions: ">=15.0.0", + pathFilter: getCrossPlatformPathRegex( + String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, + { escape: false } + ), + contentFilter: /arrayBuffer\(\)\s*\.then/, + patchCode: createPatchCode(rule), + }, + ], +}; diff --git a/packages/core/src/build/patch/patches/patchNextServer.ts b/packages/core/src/build/patch/patches/patchNextServer.ts new file mode 100644 index 00000000..c3b99d43 --- /dev/null +++ b/packages/core/src/build/patch/patches/patchNextServer.ts @@ -0,0 +1,133 @@ +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; + +// Disable the background preloading of route done by NextServer by default during the creation of NextServer +export const disablePreloadingRule = ` +rule: + kind: statement_block + inside: + kind: if_statement + any: + - has: + kind: member_expression + pattern: this.nextConfig.experimental.preloadEntriesOnStart + stopBy: end + - has: + kind: binary_expression + pattern: appDocumentPreloading === true + stopBy: end +fix: + '{}' +`; + +// Mostly for splitted edge functions so that we don't try to match them on the other non edge functions +export const removeMiddlewareManifestRule = ` +rule: + kind: statement_block + inside: + kind: method_definition + has: + kind: property_identifier + regex: ^getMiddlewareManifest$ +fix: + '{return null;}' +`; + +// Make `handleNextImageRequest` a no-op to avoid pulling `sharp` +// Applies wherever this constructor pattern is matched +export const emptyHandleNextImageRequestRule = ` +rule: + kind: assignment_expression + pattern: this.handleNextImageRequest = $VALUE + inside: + kind: method_definition + stopBy: end + has: + kind: property_identifier + regex: ^constructor$ + inside: + kind: class_body +fix: + this.handleNextImageRequest = async (req, res, parsedUrl) => false +`; + +/** + * Swaps the body for a throwing implementation + * + * @param methodName The name of the method + * @returns A rule to replace the body with a `throw` + */ +export function createEmptyBodyRule(methodName: string) { + return ` +rule: + pattern: + selector: method_definition + context: "class { async ${methodName}($$$PARAMS) { $$$_ } }" +fix: |- + async ${methodName}($$$PARAMS) { + throw new Error("${methodName} should not be called with OpenNext"); + } +`; +} + +const pathFilter = getCrossPlatformPathRegex(String.raw`/next/dist/server/next-server\.js$`, { + escape: false, +}); + +/** + * Patches to avoid pulling babel (~4MB). + * + * Details: + * - empty `NextServer#runMiddleware` and `NextServer#runEdgeFunction` that are not used + * - drop `next/dist/server/node-environment-extensions/error-inspect.js` + */ +const babelPatches = [ + // Empty the body of `NextServer#runMiddleware` + { + pathFilter, + contentFilter: /runMiddleware\(/, + patchCode: createPatchCode(createEmptyBodyRule("runMiddleware")), + }, + // Empty the body of `NextServer#runEdgeFunction` + { + pathFilter, + contentFilter: /runEdgeFunction\(/, + patchCode: createPatchCode(createEmptyBodyRule("runEdgeFunction")), + }, +]; + +export const patchNextServer: CodePatcher = { + name: "patch-next-server", + patches: [ + // Empty the body of `NextServer#imageOptimizer` - unused in OpenNext + { + pathFilter, + contentFilter: /imageOptimizer\(/, + patchCode: createPatchCode(createEmptyBodyRule("imageOptimizer")), + }, + // Make `handleNextImageRequest` a no-op to avoid pulling `sharp` - unused in OpenNext + { + pathFilter, + contentFilter: /handleNextImageRequest/, + patchCode: createPatchCode(emptyHandleNextImageRequestRule), + }, + // Disable Next background preloading done at creation of `NextServer` + { + versions: ">=14.0.0", + pathFilter, + contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/, + patchCode: createPatchCode(disablePreloadingRule), + }, + // Don't match edge functions in `NextServer` + { + // Next 12 and some version of 13 use the bundled middleware/edge function + versions: ">=14.0.0", + pathFilter, + contentFilter: /getMiddlewareManifest/, + patchCode: createPatchCode(removeMiddlewareManifestRule), + }, + ...babelPatches, + ], +}; diff --git a/packages/core/src/build/patch/patches/patchNodeEnvironment.ts b/packages/core/src/build/patch/patches/patchNodeEnvironment.ts new file mode 100644 index 00000000..e864b194 --- /dev/null +++ b/packages/core/src/build/patch/patches/patchNodeEnvironment.ts @@ -0,0 +1,31 @@ +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import { createPatchCode } from "../astCodePatcher.js"; +import type { CodePatcher } from "../codePatcher.js"; + +/** + * Drops `require("./node-environment-extensions/error-inspect");` + * + * This is to avoid pulling babel (~4MB) + */ +export const rule = ` +rule: + pattern: require("./node-environment-extensions/error-inspect"); +fix: |- + // Removed by OpenNext + // require("./node-environment-extensions/error-inspect"); +`; + +export const patchNodeEnvironment: CodePatcher = { + name: "patch-node-environment-error-inspect", + patches: [ + { + pathFilter: getCrossPlatformPathRegex(String.raw`/next/dist/server/node-environment\.js$`, { + escape: false, + }), + contentFilter: /error-inspect/, + patchCode: createPatchCode(rule), + versions: ">=15.0.0", + }, + ], +}; diff --git a/packages/core/src/build/patch/patches/patchOriginalNextConfig.ts b/packages/core/src/build/patch/patches/patchOriginalNextConfig.ts new file mode 100644 index 00000000..ed14902f --- /dev/null +++ b/packages/core/src/build/patch/patches/patchOriginalNextConfig.ts @@ -0,0 +1,87 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { build } from "esbuild"; + +import { inlineRequireResolvePlugin } from "../../../plugins/inline-require-resolve.js"; +import * as buildHelper from "../../helper.js"; + +/** + * Next 16.1.0-16.1.4 has missing fields in `required-server-files.json`: + * - `skipTrailingSlashRedirect` + * - `serverExternalPackages` + * + * This patch adds them back in by compiling and importing the user's `next.config.js` file. + * + * It is a regression in https://github.com/vercel/next.js/pull/86830 (16.1.0) + * Fixed in https://github.com/vercel/next.js/pull/88733 (16.1.4) + */ +export async function patchOriginalNextConfig(options: buildHelper.BuildOptions): Promise { + if ( + buildHelper.compareSemver(options.nextVersion, "<", "16.1.0") || + buildHelper.compareSemver(options.nextVersion, ">=", "16.1.4") + ) { + return; + } + + // The manifests in both `.next` and `.next/standalone` folders + // are patched as Open Next uses either of them. + const manifestPath = path.join(options.appBuildOutputPath, ".next/required-server-files.json"); + + const manifestStandalonePath = path.join( + options.appBuildOutputPath, + ".next/standalone", + buildHelper.getPackagePath(options), + ".next/required-server-files.json" + ); + + if (fs.existsSync(manifestPath)) { + const manifest = JSON.parse(await fs.promises.readFile(manifestPath, "utf-8")); + if (manifest.config.skipTrailingSlashRedirect === undefined) { + const { skipTrailingSlashRedirect, serverExternalPackages } = await importNextConfigFromSource(options); + manifest.config.skipTrailingSlashRedirect = skipTrailingSlashRedirect ?? false; + manifest.config.serverExternalPackages = serverExternalPackages ?? []; + await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); + if (fs.existsSync(manifestStandalonePath)) { + await fs.promises.writeFile(manifestStandalonePath, JSON.stringify(manifest, null, 2), "utf-8"); + } + } + } else { + throw new Error(`Could not find required-server-files.json at path: ${manifestPath}`); + } +} + +/** + * Compile and import the user's `next.config` file + * + * @returns + */ +async function importNextConfigFromSource(buildOptions: buildHelper.BuildOptions) { + const nextConfigDetails = buildHelper.findNextConfig(buildOptions); + + if (!nextConfigDetails) { + throw new Error("Could not find next.config file"); + } + + const { path: configPath, isTypescript: configIsTs } = nextConfigDetails; + + let configToImport: string; + + // Only compile if the extension is a TypeScript extension + if (configIsTs) { + await build({ + entryPoints: [configPath], + outfile: path.join(buildOptions.tempBuildDir, "next.config.mjs"), + bundle: true, + format: "esm", + platform: "node", + plugins: [inlineRequireResolvePlugin], + }); + configToImport = path.join(buildOptions.tempBuildDir, "next.config.mjs"); + } else { + // For .js, .mjs, .cjs, use the file directly + configToImport = configPath; + } + + return (await import(configToImport)).default; +} diff --git a/packages/core/src/build/utils.ts b/packages/core/src/build/utils.ts new file mode 100644 index 00000000..89185130 --- /dev/null +++ b/packages/core/src/build/utils.ts @@ -0,0 +1,30 @@ +import os from "node:os"; + +import logger from "../logger.js"; + +export function printHeader(header: string) { + // oxlint-disable-next-line no-param-reassign + header = `OpenNext — ${header}`; + logger.info( + [ + "", + `┌${"─".repeat(header.length + 2)}┐`, + `│ ${header} │`, + `└${"─".repeat(header.length + 2)}┘`, + "", + ].join("\n") + ); +} + +/** + * Displays a warning on windows platform. + */ +export function showWarningOnWindows() { + if (os.platform() !== "win32") return; + + logger.warn("OpenNext is not fully compatible with Windows."); + logger.warn("For optimal performance, it is recommended to use Windows Subsystem for Linux (WSL)."); + logger.warn( + "While OpenNext may function on Windows, it could encounter unpredictable failures during runtime." + ); +} diff --git a/packages/core/src/build/validateConfig.ts b/packages/core/src/build/validateConfig.ts new file mode 100644 index 00000000..22779d08 --- /dev/null +++ b/packages/core/src/build/validateConfig.ts @@ -0,0 +1,92 @@ +import type { + FunctionOptions, + IncludedConverter, + IncludedWrapper, + OpenNextConfig, + SplittedFunctionOptions, +} from "@/types/open-next"; + +import logger from "../logger.js"; + +const compatibilityMatrix: Record = { + "aws-lambda": ["aws-apigw-v1", "aws-apigw-v2", "aws-cloudfront", "sqs-revalidate"], + "aws-lambda-compressed": ["aws-apigw-v2"], + "aws-lambda-streaming": ["aws-apigw-v2"], + cloudflare: ["edge"], + "cloudflare-edge": ["edge"], + "cloudflare-node": ["edge"], + node: ["node"], + "express-dev": ["node"], + dummy: ["dummy"], +}; + +function validateFunctionOptions(fnOptions: FunctionOptions) { + const wrapper = typeof fnOptions.override?.wrapper === "string" ? fnOptions.override.wrapper : "aws-lambda"; + const converter = + typeof fnOptions.override?.converter === "string" ? fnOptions.override.converter : "aws-apigw-v2"; + if (fnOptions.override?.generateDockerfile && converter !== "node" && wrapper !== "node") { + logger.warn( + "You've specified generateDockerfile without node converter and wrapper. Without custom converter and wrapper the dockerfile will not work" + ); + } + if (converter === "aws-cloudfront" && fnOptions.placement !== "global") { + logger.warn( + "You've specified aws-cloudfront converter without global placement. This may not generate the correct output" + ); + } + const isCustomWrapper = typeof fnOptions.override?.wrapper === "function"; + const isCustomConverter = typeof fnOptions.override?.converter === "function"; + // Check if the wrapper and converter are compatible + // Only check if using one of the included converters or wrapper + if (!compatibilityMatrix[wrapper].includes(converter) && !isCustomWrapper && !isCustomConverter) { + logger.error( + `Wrapper ${wrapper} and converter ${converter} are not compatible. For the wrapper ${wrapper} you should only use the following converters: ${compatibilityMatrix[ + wrapper + ].join(", ")}` + ); + } +} + +function validateSplittedFunctionOptions(fnOptions: SplittedFunctionOptions, name: string) { + validateFunctionOptions(fnOptions); + if (fnOptions.routes.length === 0) { + throw new Error(`Splitted function ${name} must have at least one route`); + } + // Check if the routes are properly formated + fnOptions.routes.forEach((route) => { + if (!route.startsWith("app/") && !route.startsWith("pages/")) { + throw new Error( + `Route ${route} in function ${name} is not a valid route. It should starts with app/ or pages/ depending on if you use page or app router` + ); + } + }); + if (fnOptions.runtime === "edge" && fnOptions.routes.length > 1) { + throw new Error(`Edge function ${name} can only have one route`); + } +} + +export function validateConfig(config: OpenNextConfig) { + validateFunctionOptions(config.default); + Object.entries(config.functions ?? {}).forEach(([name, fnOptions]) => { + validateSplittedFunctionOptions(fnOptions, name); + }); + if (config.dangerous?.disableIncrementalCache) { + logger.warn("You've disabled incremental cache. This means that ISR and SSG will not work."); + } + if (config.dangerous?.disableTagCache) { + logger.warn( + `You've disabled tag cache. + This means that revalidatePath and revalidateTag from next/cache will not work. + It is safe to disable if you only use page router` + ); + } + validateFunctionOptions(config.imageOptimization ?? {}); + if (config.middleware?.external === true) { + validateFunctionOptions(config.middleware ?? {}); + } + //@ts-expect-error - Revalidate custom wrapper type is different + validateFunctionOptions(config.revalidate ?? {}); + //@ts-expect-error - Warmer custom wrapper type is different + validateFunctionOptions(config.warmer ?? {}); + validateFunctionOptions(config.initializationFunction ?? {}); +} diff --git a/packages/core/src/core/createGenericHandler.ts b/packages/core/src/core/createGenericHandler.ts new file mode 100644 index 00000000..a8674936 --- /dev/null +++ b/packages/core/src/core/createGenericHandler.ts @@ -0,0 +1,48 @@ +import type { + BaseEventOrResult, + DefaultOverrideOptions, + InternalEvent, + InternalResult, + OpenNextConfig, +} from "@/types/open-next"; +import type { OpenNextHandler } from "@/types/overrides"; + +import { debug } from "../adapters/logger"; + +import { resolveConverter, resolveWrapper } from "./resolve"; + +type HandlerType = "imageOptimization" | "revalidate" | "warmer" | "middleware" | "initializationFunction"; + +type GenericHandler< + Type extends HandlerType, + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +> = { + handler: OpenNextHandler; + type: Type; +}; + +export async function createGenericHandler< + Type extends HandlerType, + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +>(handler: GenericHandler) { + // @ts-expect-error `./open-next.config.mjs` exists only in the build output + const config: OpenNextConfig = await import("./open-next.config.mjs").then((m) => m.default); + + globalThis.openNextConfig = config; + const handlerConfig = config[handler.type]; + const override = + handlerConfig && "override" in handlerConfig + ? (handlerConfig.override as DefaultOverrideOptions) + : undefined; + + // From the config, we create the converter + const converter = await resolveConverter(override?.converter); + + // Then we create the handler + const { name, wrapper } = await resolveWrapper(override?.wrapper); + debug("Using wrapper", name); + + return wrapper(handler.handler, converter); +} diff --git a/packages/core/src/core/createMainHandler.ts b/packages/core/src/core/createMainHandler.ts new file mode 100644 index 00000000..480aa21b --- /dev/null +++ b/packages/core/src/core/createMainHandler.ts @@ -0,0 +1,57 @@ +import type { OpenNextConfig } from "@/types/open-next"; + +import { debug } from "../adapters/logger"; +import { generateUniqueId } from "../adapters/util"; + +import { openNextHandler } from "./requestHandler"; +import { + resolveAssetResolver, + resolveCdnInvalidation, + resolveConverter, + resolveIncrementalCache, + resolveProxyRequest, + resolveQueue, + resolveTagCache, + resolveWrapper, +} from "./resolve"; + +export async function createMainHandler() { + // @ts-expect-error `./open-next.config.mjs` exists only in the build output + const config: OpenNextConfig = await import("./open-next.config.mjs").then((m) => m.default); + + const thisFunction = globalThis.fnName ? config.functions![globalThis.fnName] : config.default; + + globalThis.serverId = generateUniqueId(); + globalThis.openNextConfig = config; + + // If route preloading behavior is set to start, it will wait for every single route to be preloaded before even creating the main handler. + //TODO: revisit that later + // await globalThis.__next_route_preloader("start"); + + // Default queue + globalThis.queue = await resolveQueue(thisFunction.override?.queue); + + globalThis.incrementalCache = await resolveIncrementalCache(thisFunction.override?.incrementalCache); + + globalThis.tagCache = await resolveTagCache(thisFunction.override?.tagCache); + + if (config.middleware?.external !== true) { + globalThis.assetResolver = await resolveAssetResolver( + globalThis.openNextConfig.middleware?.assetResolver + ); + } + + globalThis.proxyExternalRequest = await resolveProxyRequest(thisFunction.override?.proxyExternalRequest); + + globalThis.cdnInvalidationHandler = await resolveCdnInvalidation(thisFunction.override?.cdnInvalidation); + + // From the config, we create the converter + const converter = await resolveConverter(thisFunction.override?.converter); + + // Then we create the handler + const { wrapper, name } = await resolveWrapper(thisFunction.override?.wrapper); + + debug("Using wrapper", name); + + return wrapper(openNextHandler, converter); +} diff --git a/packages/core/src/core/edgeFunctionHandler.ts b/packages/core/src/core/edgeFunctionHandler.ts new file mode 100644 index 00000000..6f2168a4 --- /dev/null +++ b/packages/core/src/core/edgeFunctionHandler.ts @@ -0,0 +1,30 @@ +// Necessary files will be imported here with banner in esbuild + +import type { RequestData } from "@/types/global"; + +type EdgeRequest = Omit; + +export default async function edgeFunctionHandler(request: EdgeRequest): Promise { + const path = new URL(request.url).pathname; + const routes = globalThis._ROUTES; + const correspondingRoute = routes.find((route) => route.regex.some((r) => new RegExp(r).test(path))); + + if (!correspondingRoute) { + throw new Error(`No route found for ${request.url}`); + } + + const entry = await self._ENTRIES[`middleware_${correspondingRoute.name}`]; + + const result = await entry.default({ + page: correspondingRoute.page, + request: { + ...request, + page: { + name: correspondingRoute.name, + }, + }, + }); + globalThis.__openNextAls.getStore()?.pendingPromiseRunner.add(result.waitUntil); + const response = result.response; + return response; +} diff --git a/packages/core/src/core/nodeMiddlewareHandler.ts b/packages/core/src/core/nodeMiddlewareHandler.ts new file mode 100644 index 00000000..3e8095b0 --- /dev/null +++ b/packages/core/src/core/nodeMiddlewareHandler.ts @@ -0,0 +1,40 @@ +import type { RequestData } from "@/types/global"; + +type EdgeRequest = Omit; + +// Do we need Buffer here? +// oxlint-disable-next-line import/first +import { Buffer } from "node:buffer"; +globalThis.Buffer = Buffer; + +// AsyncLocalStorage is needed to be defined globally +// oxlint-disable-next-line import/first +import { AsyncLocalStorage } from "node:async_hooks"; +globalThis.AsyncLocalStorage = AsyncLocalStorage; + +interface NodeMiddleware { + default: (req: { handler: unknown; request: EdgeRequest; page: "middleware" }) => Promise<{ + response: Response; + waitUntil: Promise; + }>; + middleware: unknown; +} + +let _module: NodeMiddleware | undefined; + +export default async function middlewareHandler(request: EdgeRequest): Promise { + if (!_module) { + // We use await import here so that we are sure that it is loaded after AsyncLocalStorage is defined on globalThis + // We need both await here, same way as in https://github.com/opennextjs/opennextjs-aws/pull/704 + //@ts-expect-error - This file should be bundled with esbuild + _module = await (await import("./.next/server/middleware.js")).default; + } + const adapterFn = _module!.default || _module; + const result = await adapterFn({ + handler: _module!.middleware || _module, + request: request, + page: "middleware", + }); + globalThis.__openNextAls.getStore()?.pendingPromiseRunner.add(result.waitUntil); + return result.response; +} diff --git a/packages/core/src/core/requestHandler.ts b/packages/core/src/core/requestHandler.ts new file mode 100644 index 00000000..aa07f1a5 --- /dev/null +++ b/packages/core/src/core/requestHandler.ts @@ -0,0 +1,242 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { Writable } from "node:stream"; +import { finished } from "node:stream/promises"; + +import { IncomingMessage } from "@/http/request"; +import type { InternalEvent, InternalResult, ResolvedRoute, RoutingResult } from "@/types/open-next"; +import type { OpenNextHandlerOptions } from "@/types/overrides"; +import { runWithOpenNextRequestContext } from "@/utils/promise"; + +import { debug, error } from "../adapters/logger"; + +import { adapterHandler } from "./routing/adapterHandler"; +import { constructNextUrl, convertRes, createServerResponse } from "./routing/util"; +import routingHandler, { + INTERNAL_EVENT_REQUEST_ID, + INTERNAL_HEADER_REWRITE_STATUS_CODE, + INTERNAL_HEADER_INITIAL_URL, + INTERNAL_HEADER_RESOLVED_ROUTES, + MIDDLEWARE_HEADER_PREFIX, + MIDDLEWARE_HEADER_PREFIX_LEN, +} from "./routingHandler"; + +// This is used to identify requests in the cache +globalThis.__openNextAls = new AsyncLocalStorage(); + +export async function openNextHandler( + internalEvent: InternalEvent, + options?: OpenNextHandlerOptions +): Promise { + const initialHeaders = internalEvent.headers; + // We only use the requestId header if we are using an external middleware + // This is to ensure that no one can spoof the requestId + // When using an external middleware, we always assume that headers cannot be spoofed + const requestId = globalThis.openNextConfig.middleware?.external + ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID] + : Math.random().toString(36); + // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer + return runWithOpenNextRequestContext( + { + isISRRevalidation: initialHeaders["x-isr"] === "1", + waitUntil: options?.waitUntil, + requestId, + }, + async () => { + // Disabled for now, we'll need to revisit this later if needed. + //TODO: revisit that later + // await globalThis.__next_route_preloader("waitUntil"); + if (initialHeaders["x-forwarded-host"]) { + initialHeaders.host = initialHeaders["x-forwarded-host"]; + } + debug("internalEvent", internalEvent); + + // These 3 will get overwritten by the routing handler if not using an external middleware + const internalHeaders = { + initialPath: initialHeaders[INTERNAL_HEADER_INITIAL_URL] ?? internalEvent.rawPath, + resolvedRoutes: initialHeaders[INTERNAL_HEADER_RESOLVED_ROUTES] + ? JSON.parse(initialHeaders[INTERNAL_HEADER_RESOLVED_ROUTES]) + : ([] as ResolvedRoute[]), + rewriteStatusCode: Number.parseInt(initialHeaders[INTERNAL_HEADER_REWRITE_STATUS_CODE]), + }; + + let routingResult: InternalResult | RoutingResult = { + internalEvent, + isExternalRewrite: false, + origin: false, + isISR: false, + initialURL: internalEvent.url, + ...internalHeaders, + }; + + //#override withRouting + routingResult = await routingHandler(internalEvent, { + assetResolver: globalThis.assetResolver, + }); + //#endOverride + + const headers = getHeaders(routingResult); + + const overwrittenResponseHeaders: Record = {}; + + for (const [rawKey, value] of Object.entries(headers)) { + if (!rawKey.startsWith(MIDDLEWARE_HEADER_PREFIX)) { + continue; + } + const key = rawKey.slice(MIDDLEWARE_HEADER_PREFIX_LEN); + // We skip this header here since it is used by Next internally and we don't want it on the response headers. + // This header needs to be present in the request headers for processRequest, so cookies().get() from Next will work on initial render. + if (key !== "x-middleware-set-cookie") { + overwrittenResponseHeaders[key] = value as string | string[]; + } + headers[key] = value; + delete headers[rawKey]; + } + + if ("isExternalRewrite" in routingResult && routingResult.isExternalRewrite === true) { + try { + routingResult = await globalThis.proxyExternalRequest.proxy(routingResult.internalEvent); + } catch (e) { + error("External request failed.", e); + routingResult = { + internalEvent: { + type: "core", + rawPath: "/500", + method: "GET", + headers: {}, + url: constructNextUrl(internalEvent.url, "/500"), + query: {}, + cookies: {}, + remoteAddress: "", + }, + // On error we need to rewrite to the 500 page which is an internal rewrite + isExternalRewrite: false, + isISR: false, + origin: false, + initialURL: internalEvent.url, + resolvedRoutes: [{ route: "/500", type: "page", isFallback: false }], + }; + } + } + + if ("type" in routingResult) { + // response is used only in the streaming case + if (options?.streamCreator) { + const response = createServerResponse( + { + internalEvent, + isExternalRewrite: false, + isISR: false, + resolvedRoutes: [], + origin: false, + initialURL: internalEvent.url, + }, + routingResult.headers, + options.streamCreator + ); + response.statusCode = routingResult.statusCode; + response.flushHeaders(); + const [bodyToConsume, bodyToReturn] = routingResult.body.tee(); + for await (const chunk of bodyToConsume) { + response.write(chunk); + } + response.end(); + routingResult.body = bodyToReturn; + } + return routingResult; + } + + const preprocessedEvent = routingResult.internalEvent; + debug("preprocessedEvent", preprocessedEvent); + const { search, pathname, hash } = new URL(preprocessedEvent.url); + const reqProps = { + method: preprocessedEvent.method, + url: `${pathname}${search}${hash}`, + //WORKAROUND: We pass this header to the serverless function to mimic a prefetch request which will not trigger revalidation since we handle revalidation differently + // There is 3 way we can handle revalidation: + // 1. We could just let the revalidation go as normal, but due to race conditions the revalidation will be unreliable + // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh + // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests) + headers: { + ...headers, + }, + body: preprocessedEvent.body, + remoteAddress: preprocessedEvent.remoteAddress, + }; + + const mergeHeadersPriority = globalThis.openNextConfig.dangerous?.headersAndCookiesPriority + ? globalThis.openNextConfig.dangerous.headersAndCookiesPriority(preprocessedEvent) + : "middleware"; + const store = globalThis.__openNextAls.getStore(); + if (store) { + store.mergeHeadersPriority = mergeHeadersPriority; + } + + const req = new IncomingMessage(reqProps); + const res = createServerResponse( + routingResult, + routingResult.initialResponse ? routingResult.initialResponse.headers : overwrittenResponseHeaders, + options?.streamCreator + ); + + if (routingResult.initialResponse) { + res.statusCode = routingResult.initialResponse.statusCode; + res.flushHeaders(); + for await (const chunk of routingResult.initialResponse.body) { + res.write(chunk); + } + + //We create a special response for the PPR resume request + const pprRes = createServerResponse(routingResult, overwrittenResponseHeaders, { + writeHeaders: () => { + return new Writable({ + write(chunk, encoding, callback) { + res.write(chunk, encoding, callback); + }, + }); + }, + }); + await adapterHandler(req, pprRes, routingResult, { + waitUntil: options?.waitUntil, + }); + await finished(pprRes); + res.end(); + + return convertRes(res); + } + + // It seems that Next.js doesn't set the status code for 404 and 500 anymore for us, we have to do it ourselves + // TODO: check security wise if it's ok to do that + if (pathname === "/404") { + res.statusCode = 404; + } else if (pathname === "/500") { + res.statusCode = 500; + } + + //#override useAdapterHandler + await adapterHandler(req, res, routingResult, { + waitUntil: options?.waitUntil, + }); + //#endOverride + + const { statusCode, headers: responseHeaders, isBase64Encoded, body } = convertRes(res); + + const internalResult = { + type: internalEvent.type, + statusCode, + headers: responseHeaders, + body, + isBase64Encoded, + }; + + return internalResult; + } + ); +} + +function getHeaders(routingResult: RoutingResult | InternalResult) { + if ("type" in routingResult) { + return routingResult.headers; + } else { + return routingResult.internalEvent.headers; + } +} diff --git a/packages/core/src/core/resolve.ts b/packages/core/src/core/resolve.ts new file mode 100644 index 00000000..e48b968b --- /dev/null +++ b/packages/core/src/core/resolve.ts @@ -0,0 +1,162 @@ +import type { + BaseEventOrResult, + DefaultOverrideOptions, + ExternalMiddlewareConfig, + InternalEvent, + InternalResult, + OpenNextConfig, + OverrideOptions, +} from "@/types/open-next"; +import type { Converter, TagCache, Wrapper } from "@/types/overrides"; + +// Just a little utility type to remove undefined from a type +type RemoveUndefined = T extends undefined ? never : T; + +export async function resolveConverter< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +>(converter: DefaultOverrideOptions["converter"]): Promise> { + if (typeof converter === "function") { + return converter(); + } + // @ts-expect-error - This will be replaced by the bundler + const m_1 = await import("../overrides/converters/aws-apigw-v2.js"); + return m_1.default; +} + +export async function resolveWrapper< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +>(wrapper: DefaultOverrideOptions["wrapper"]): Promise> { + if (typeof wrapper === "function") { + return wrapper(); + } + // @ts-expect-error - This will be replaced by the bundler + const m_1 = await import("../overrides/wrappers/aws-lambda.js"); + return m_1.default; +} + +/** + * + * @param tagCache + * @returns + * @__PURE__ + */ +export async function resolveTagCache(tagCache: OverrideOptions["tagCache"]): Promise { + if (typeof tagCache === "function") { + return tagCache(); + } + // @ts-expect-error - This will be replaced by the bundler + const m_1 = await import("../overrides/tagCache/dynamodb.js"); + return m_1.default; +} + +/** + * + * @param queue + * @returns + * @__PURE__ + */ +export async function resolveQueue(queue: OverrideOptions["queue"]) { + if (typeof queue === "function") { + return queue(); + } + // @ts-expect-error - This will be replaced by the bundler + const m_1 = await import("../overrides/queue/sqs.js"); + return m_1.default; +} + +/** + * + * @param incrementalCache + * @returns + * @__PURE__ + */ +export async function resolveIncrementalCache(incrementalCache: OverrideOptions["incrementalCache"]) { + if (typeof incrementalCache === "function") { + return incrementalCache(); + } + // @ts-expect-error - This will be replaced by the bundler + const m_1 = await import("../overrides/incrementalCache/s3.js"); + return m_1.default; +} + +/** + * @param imageLoader + * @returns + * @__PURE__ + */ +export async function resolveImageLoader( + imageLoader: RemoveUndefined["loader"] +) { + if (typeof imageLoader === "function") { + return imageLoader(); + } + // @ts-expect-error - This will be replaced by the bundler + const m_1 = await import("../overrides/imageLoader/s3.js"); + return m_1.default; +} + +/** + * @returns + * @__PURE__ + */ +export async function resolveOriginResolver( + originResolver: RemoveUndefined["originResolver"] +) { + if (typeof originResolver === "function") { + return originResolver(); + } + const m_1 = await import("../overrides/originResolver/pattern-env.js"); + return m_1.default; +} + +/** + * @returns + * @__PURE__ + */ +export async function resolveAssetResolver( + assetResolver: RemoveUndefined["assetResolver"] +) { + if (typeof assetResolver === "function") { + return assetResolver(); + } + const m_1 = await import("../overrides/assetResolver/dummy.js"); + return m_1.default; +} + +/** + * @__PURE__ + */ +export async function resolveWarmerInvoke( + warmer: RemoveUndefined["invokeFunction"] +) { + if (typeof warmer === "function") { + return warmer(); + } + // @ts-expect-error - This will be replaced by the bundler + const m_1 = await import("../overrides/warmer/aws-lambda.js"); + return m_1.default; +} + +/** + * @__PURE__ + */ +export async function resolveProxyRequest(proxyRequest: OverrideOptions["proxyExternalRequest"]) { + if (typeof proxyRequest === "function") { + return proxyRequest(); + } + const m_1 = await import("../overrides/proxyExternalRequest/node.js"); + return m_1.default; +} + +/** + * @__PURE__ + */ +export async function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]) { + if (typeof cdnInvalidation === "function") { + return cdnInvalidation(); + } + const m_1 = await import("../overrides/cdnInvalidation/dummy.js"); + return m_1.default; +} diff --git a/packages/core/src/core/routing/adapterHandler.ts b/packages/core/src/core/routing/adapterHandler.ts new file mode 100644 index 00000000..81b7e341 --- /dev/null +++ b/packages/core/src/core/routing/adapterHandler.ts @@ -0,0 +1,120 @@ +import { finished } from "node:stream/promises"; + +import type { OpenNextNodeResponse } from "@/http/index"; +import type { IncomingMessage } from "@/http/request"; +import type { ResolvedRoute, RoutingResult, WaitUntil } from "@/types/open-next"; + +/** + * This function loads the necessary routes, and invoke the expected handler. + * @param routingResult The result of the routing process, containing information about the matched route and any parameters. + */ +export async function adapterHandler( + req: IncomingMessage, + res: OpenNextNodeResponse, + routingResult: RoutingResult, + options: { + waitUntil?: WaitUntil; + } = {} +) { + let resolved = false; + + const pendingPromiseRunner = globalThis.__openNextAls.getStore()?.pendingPromiseRunner; + const waitUntil = options.waitUntil ?? pendingPromiseRunner?.add.bind(pendingPromiseRunner); + + // Our internal routing could return /500 or /404 routes, we first check that + if (routingResult.internalEvent.rawPath === "/404") { + await handle404(req, res, waitUntil); + return; + } + if (routingResult.internalEvent.rawPath === "/500") { + await handle500(req, res, waitUntil); + return; + } + + //TODO: replace this at runtime with a version precompiled for the cloudflare adapter. + for (const route of routingResult.resolvedRoutes) { + const module = getHandler(route); + if (!module || resolved) { + return; + } + + try { + console.log("## adapterHandler trying route", route, req.url); + const result = await module.handler(req, res, { + waitUntil, + }); + console.log("## adapterHandler route succeeded", route); + resolved = true; + return result; + //If it doesn't throw, we are done + } catch (e) { + console.log("## adapterHandler route failed", route, e); + // I'll have to run some more tests, but in theory, we should not have anything special to do here, and we should return the 500 page here. + await handle500(req, res, waitUntil); + return; + } + } + if (!resolved) { + console.log("## adapterHandler no route resolved for", req.url); + await handle404(req, res, waitUntil); + return; + } +} + +async function handle404(req: IncomingMessage, res: OpenNextNodeResponse, waitUntil?: WaitUntil) { + try { + // TODO: find the correct one to use. + const module = getHandler({ + route: "/_not-found", + type: "app", + isFallback: false, + }); + if (module) { + await module.handler(req, res, { + waitUntil, + }); + return; + } + } catch (e2) { + console.log("## adapterHandler not found route also failed", e2); + } + // Ideally we should never reach here as the 404 page should be the Next.js one. + res.statusCode = 404; + res.end("Not Found"); + await finished(res); +} + +async function handle500(req: IncomingMessage, res: OpenNextNodeResponse, waitUntil?: WaitUntil) { + try { + // TODO: find the correct one to use. + const module = getHandler({ + route: "/_global-error", + type: "app", + isFallback: false, + }); + if (module) { + await module.handler(req, res, { + waitUntil, + }); + return; + } + } catch (e2) { + console.log("## adapterHandler global error route also failed", e2); + } + res.statusCode = 500; + res.end("Internal Server Error"); + await finished(res); +} + +// Body replaced at build time +function getHandler(route: ResolvedRoute): + | undefined + | { + handler: ( + req: IncomingMessage, + res: OpenNextNodeResponse, + options: { waitUntil?: (promise: Promise) => void } + ) => Promise; + } { + return undefined; +} diff --git a/packages/core/src/core/routing/cacheInterceptor.ts b/packages/core/src/core/routing/cacheInterceptor.ts new file mode 100644 index 00000000..271b123f --- /dev/null +++ b/packages/core/src/core/routing/cacheInterceptor.ts @@ -0,0 +1,418 @@ +import { createHash } from "node:crypto"; + +import { NextConfig, PrerenderManifest } from "@/config/index"; +import type { InternalEvent, InternalResult, MiddlewareEvent, PartialResult } from "@/types/open-next"; +import type { CacheValue } from "@/types/overrides"; +import { isBinaryContentType } from "@/utils/binary"; +import { getTagsFromValue, hasBeenRevalidated } from "@/utils/cache"; +import { emptyReadableStream, toReadableStream } from "@/utils/stream"; + +import { debug, error } from "../../adapters/logger"; + +import { localizePath } from "./i18n"; +import { generateMessageGroupId } from "./queue"; + +const CACHE_ONE_YEAR = 60 * 60 * 24 * 365; +const CACHE_ONE_MONTH = 60 * 60 * 24 * 30; + +/* + * We use this header to prevent Firefox (and possibly some CDNs) from incorrectly reusing the RSC responses during caching. + * This can especially happen when there's a redirect in the middleware as the `_rsc` query parameter is not visible there. + * So it will get dropped during the redirect, which results in the RSC response being cached instead of the actual HTML on the path `/`. + * This value can be found in the routes manifest, under `rsc.varyHeader`. + * They recompute it here in Next: + * https://github.com/vercel/next.js/blob/c5bf5bb4c8b01b1befbbfa7ad97a97476ee9d0d7/packages/next/src/server/base-server.ts#L2011 + * Also see this PR: https://github.com/vercel/next.js/pull/79426 + */ +const VARY_HEADER = + "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url"; +const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch"; +const NEXT_PRERENDER_HEADER = "x-nextjs-prerender"; +const NEXT_POSTPONED_HEADER = "x-nextjs-postponed"; + +async function computeCacheControl( + path: string, + body: string, + host: string, + revalidate?: number | false, + lastModified?: number +) { + let finalRevalidate = CACHE_ONE_YEAR; + + const existingRoute = Object.entries(PrerenderManifest?.routes ?? {}).find((p) => p[0] === path)?.[1]; + if (revalidate === undefined && existingRoute) { + finalRevalidate = + existingRoute.initialRevalidateSeconds === false + ? CACHE_ONE_YEAR + : existingRoute.initialRevalidateSeconds; + } else if (revalidate !== undefined) { + finalRevalidate = revalidate === false ? CACHE_ONE_YEAR : revalidate; + } + // calculate age + const age = Math.round((Date.now() - (lastModified ?? 0)) / 1000); + const hash = (str: string) => createHash("md5").update(str).digest("hex"); + const etag = hash(body); + if (revalidate === 0) { + // This one should never happen + return { + "cache-control": "private, no-cache, no-store, max-age=0, must-revalidate", + "x-opennext-cache": "ERROR", + etag, + }; + } + if (finalRevalidate !== CACHE_ONE_YEAR) { + const sMaxAge = Math.max(finalRevalidate - age, 1); + debug("sMaxAge", { + finalRevalidate, + age, + lastModified, + revalidate, + }); + const isStale = sMaxAge === 1; + if (isStale) { + let url = NextConfig.trailingSlash ? `${path}/` : path; + if (NextConfig.basePath) { + // We need to add the basePath to the url + url = `${NextConfig.basePath}${url}`; + } + await globalThis.queue.send({ + MessageBody: { + host, + url, + eTag: etag, + lastModified: lastModified ?? Date.now(), + }, + MessageDeduplicationId: hash(`${path}-${lastModified}-${etag}`), + MessageGroupId: generateMessageGroupId(path), + }); + } + return { + "cache-control": `s-maxage=${sMaxAge}, stale-while-revalidate=${CACHE_ONE_MONTH}`, + "x-opennext-cache": isStale ? "STALE" : "HIT", + etag, + }; + } + return { + "cache-control": `s-maxage=${CACHE_ONE_YEAR}, stale-while-revalidate=${CACHE_ONE_MONTH}`, + "x-opennext-cache": "HIT", + etag, + }; +} + +function getBodyForAppRouter( + event: MiddlewareEvent, + cachedValue: CacheValue<"cache"> +): { body: string; additionalHeaders: Record } { + if (cachedValue.type !== "app") { + throw new Error("getBodyForAppRouter called with non-app cache value"); + } + try { + const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`; + const isSegmentResponse = Boolean(segmentHeader) && segmentHeader in (cachedValue.segmentData || {}); + + const body = isSegmentResponse ? cachedValue.segmentData![segmentHeader] : cachedValue.rsc; + return { + body, + additionalHeaders: isSegmentResponse + ? { [NEXT_PRERENDER_HEADER]: "1", [NEXT_POSTPONED_HEADER]: "2" } + : {}, + }; + } catch (e) { + error("Error while getting body for app router from cache:", e); + return { body: cachedValue.rsc, additionalHeaders: {} }; + } +} + +function createPprPartialResult( + event: MiddlewareEvent, + localizedPath: string, + cachedValue: CacheValue<"cache">, + responseBody: string | (() => ReturnType), + contentType: string +): PartialResult { + if (cachedValue.type !== "app") { + throw new Error("createPprPartialResult called with non-app cache value"); + } + + return { + resumeRequest: { + ...event, + method: "POST", + url: `http://${event.headers.host}${NextConfig.basePath || ""}${localizedPath || "/"}`, + headers: { + ...event.headers, + "next-resume": "1", + }, + rawPath: localizedPath, + body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"), + }, + result: { + type: "core", + statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, + body: typeof responseBody === "string" ? toReadableStream(responseBody) : responseBody(), + isBase64Encoded: false, + headers: { + "content-type": contentType, + "x-opennext-ppr": "1", + ...cachedValue.meta?.headers, + vary: VARY_HEADER, + }, + }, + }; +} + +async function generateResult( + event: MiddlewareEvent, + localizedPath: string, + cachedValue: CacheValue<"cache">, + lastModified?: number +): Promise { + debug("Returning result from experimental cache"); + let body = ""; + let type = "application/octet-stream"; + let isDataRequest = false; + let additionalHeaders = {}; + if (cachedValue.type === "app") { + isDataRequest = Boolean(event.headers.rsc); + if (isDataRequest) { + const { body: appRouterBody, additionalHeaders: appHeaders } = getBodyForAppRouter(event, cachedValue); + body = appRouterBody; + additionalHeaders = appHeaders; + + if (cachedValue.meta?.postponed) { + if (event.headers["next-router-prefetch"] === "1") { + debug("Prefetch request detected, returning cached response without postponing"); + // We try to find the corresponding segment for the prefetch request, if it exists. + const segmentToFind = event.headers[NEXT_SEGMENT_PREFETCH_HEADER]; + if (segmentToFind && cachedValue.segmentData?.[segmentToFind]) { + body = cachedValue.segmentData[segmentToFind]; + additionalHeaders = { [NEXT_PRERENDER_HEADER]: "1", [NEXT_POSTPONED_HEADER]: "2" }; + debug("Found segment for prefetch request, returning it"); + } else { + debug("No segment found for prefetch request, returning full response"); + } + } else { + debug("App router postponed request detected", localizedPath); + return createPprPartialResult( + event, + localizedPath, + cachedValue, + () => emptyReadableStream(), + "text/x-component" + ); + } + } + debug("App router data request detected", localizedPath, body); + } else { + if (cachedValue.meta?.postponed) { + debug("Postponed request detected", localizedPath); + return createPprPartialResult( + event, + localizedPath, + cachedValue, + cachedValue.html, + "text/html; charset=utf-8" + ); + } else { + body = cachedValue.html; + } + } + type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8"; + } else if (cachedValue.type === "page") { + isDataRequest = Boolean(event.query.__nextDataReq); + body = isDataRequest ? JSON.stringify(cachedValue.json) : cachedValue.html; + type = isDataRequest ? "application/json" : "text/html; charset=utf-8"; + } else { + throw new Error( + "generateResult called with unsupported cache value type, only 'app' and 'page' are supported" + ); + } + const cacheControl = await computeCacheControl( + localizedPath, + body, + event.headers.host, + cachedValue.revalidate, + lastModified + ); + return { + type: "core", + // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code + // Also set the status code to the rewriteStatusCode if defined + // This can happen in handleMiddleware in routingHandler. + // `NextResponse.rewrite(url, { status: xxx}) + // The rewrite status code should take precedence over the cached one + statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, + body: toReadableStream(body, isBinaryContentType(type)), + isBase64Encoded: false, + headers: { + ...cacheControl, + "content-type": type, + ...cachedValue.meta?.headers, + vary: VARY_HEADER, + ...additionalHeaders, + }, + }; +} + +/** + * + * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts#L2-L10 + */ +function escapePathDelimiters(segment: string, escapeEncoded?: boolean): string { + return segment.replace( + new RegExp(`([/#?]${escapeEncoded ? "|%(2f|23|3f|5c)" : ""})`, "gi"), + (char: string) => encodeURIComponent(char) + ); +} + +/** + * + * SSG cache key needs to be decoded, but some characters needs to be properly escaped + * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26 + */ +function decodePathParams(pathname: string): string { + return pathname + .split("/") + .map((segment) => { + try { + return escapePathDelimiters(decodeURIComponent(segment), true); + } catch (e) { + // If decodeURIComponent fails, we return the original segment + return segment; + } + }) + .join("/"); +} + +export async function cacheInterceptor( + event: MiddlewareEvent +): Promise { + if ( + Boolean(event.headers["next-action"]) || + Boolean(event.headers["x-prerender-revalidate"]) || + Boolean(event.headers["next-resume"]) || + event.method !== "GET" + ) + return event; + + // Check for Next.js preview mode cookies + const cookies = event.headers.cookie || ""; + const hasPreviewData = cookies.includes("__prerender_bypass") || cookies.includes("__next_preview_data"); + + if (hasPreviewData) { + debug("Preview mode detected, passing through to handler"); + return event; + } + // We localize the path in case i18n is enabled + let localizedPath = localizePath(event); + // If using basePath we need to remove it from the path + if (NextConfig.basePath) { + localizedPath = localizedPath.replace(NextConfig.basePath, ""); + } + // We also need to remove trailing slash + localizedPath = localizedPath.replace(/\/$/, ""); + + // Then we decode the path params + localizedPath = decodePathParams(localizedPath); + + debug("Checking cache for", localizedPath, PrerenderManifest); + + const isDynamicISR = Object.values(PrerenderManifest?.dynamicRoutes ?? {}).some((dr) => { + const regex = new RegExp(dr.routeRegex); + return regex.test(localizedPath); + }); + + const isStaticRoute = Object.keys(PrerenderManifest?.routes ?? {}).includes(localizedPath || "/"); + + const isISR = isStaticRoute || isDynamicISR; + debug("isISR", isISR); + if (isISR) { + try { + let pathToUse = localizedPath; + // For PPR, we need to check the fallback value to get the correct cache key + // We don't want to override a static route though + if (isDynamicISR && !isStaticRoute) { + const fallback = Object.entries(PrerenderManifest?.dynamicRoutes ?? {}).find(([, dr]) => { + const regex = new RegExp(dr.routeRegex); + return regex.test(localizedPath); + })?.[1].fallback; + pathToUse = typeof fallback === "string" ? fallback : localizedPath; + } else if (localizedPath === "") { + pathToUse = "/index"; + } + const cachedData = await globalThis.incrementalCache.get(pathToUse); + debug("cached data in interceptor", cachedData); + + if (!cachedData?.value) { + return event; + } + // We need to check the tag cache now + if (cachedData.value?.type === "app" || cachedData.value?.type === "route") { + const tags = getTagsFromValue(cachedData.value); + + const _hasBeenRevalidated = cachedData.shouldBypassTagCache + ? false + : await hasBeenRevalidated(localizedPath, tags, cachedData); + + if (_hasBeenRevalidated) { + return event; + } + } + const host = event.headers.host; + switch (cachedData?.value?.type) { + case "app": + case "page": + return generateResult(event, localizedPath, cachedData.value, cachedData.lastModified); + case "redirect": { + const cacheControl = await computeCacheControl( + localizedPath, + "", + host, + cachedData.value.revalidate, + cachedData.lastModified + ); + return { + type: "core", + statusCode: cachedData.value.meta?.status ?? 307, + body: emptyReadableStream(), + headers: { + ...cachedData.value.meta?.headers, + ...cacheControl, + }, + isBase64Encoded: false, + }; + } + case "route": { + const cacheControl = await computeCacheControl( + localizedPath, + cachedData.value.body, + host, + cachedData.value.revalidate, + cachedData.lastModified + ); + + const isBinary = isBinaryContentType(String(cachedData.value.meta?.headers?.["content-type"])); + + return { + type: "core", + statusCode: event.rewriteStatusCode ?? cachedData.value.meta?.status ?? 200, + body: toReadableStream(cachedData.value.body, isBinary), + headers: { + ...cacheControl, + ...cachedData.value.meta?.headers, + vary: VARY_HEADER, + }, + isBase64Encoded: isBinary, + }; + } + default: + return event; + } + } catch (e) { + debug("Error while fetching cache", e); + // In case of error we fallback to the server + return event; + } + } + return event; +} diff --git a/packages/core/src/core/routing/i18n/accept-header.ts b/packages/core/src/core/routing/i18n/accept-header.ts new file mode 100644 index 00000000..19314a9a --- /dev/null +++ b/packages/core/src/core/routing/i18n/accept-header.ts @@ -0,0 +1,136 @@ +// Copied from Next.js source code +// https://github.com/vercel/next.js/blob/canary/packages/next/src/server/accept-header.ts + +interface Selection { + pos: number; + pref?: number; + q: number; + token: string; +} + +interface Options { + prefixMatch?: boolean; + type: "accept-language"; +} + +function parse(raw: string, preferences: string[] | undefined, options: Options) { + const lowers = new Map(); + const header = raw.replace(/[ \t]/g, ""); + + if (preferences) { + let pos = 0; + for (const preference of preferences) { + const lower = preference.toLowerCase(); + lowers.set(lower, { orig: preference, pos: pos++ }); + if (options.prefixMatch) { + const parts = lower.split("-"); + while ((parts.pop(), parts.length > 0)) { + const joined = parts.join("-"); + if (!lowers.has(joined)) { + lowers.set(joined, { orig: preference, pos: pos++ }); + } + } + } + } + } + + const parts = header.split(","); + const selections: Selection[] = []; + const map = new Set(); + + for (let i = 0; i < parts.length; ++i) { + const part = parts[i]; + if (!part) { + continue; + } + + const params = part.split(";"); + if (params.length > 2) { + throw new Error(`Invalid ${options.type} header`); + } + + const token = params[0].toLowerCase(); + if (!token) { + throw new Error(`Invalid ${options.type} header`); + } + + const selection: Selection = { token, pos: i, q: 1 }; + if (preferences && lowers.has(token)) { + selection.pref = lowers.get(token)!.pos; + } + + map.add(selection.token); + + if (params.length === 2) { + const q = params[1]; + const [key, value] = q.split("="); + + if (!value || (key !== "q" && key !== "Q")) { + throw new Error(`Invalid ${options.type} header`); + } + + const score = Number.parseFloat(value); + if (score === 0) { + continue; + } + + if (Number.isFinite(score) && score <= 1 && score >= 0.001) { + selection.q = score; + } + } + + selections.push(selection); + } + + selections.sort((a, b) => { + if (b.q !== a.q) { + return b.q - a.q; + } + + if (b.pref !== a.pref) { + if (a.pref === undefined) { + return 1; + } + + if (b.pref === undefined) { + return -1; + } + + return a.pref - b.pref; + } + + return a.pos - b.pos; + }); + + const values = selections.map((selection) => selection.token); + if (!preferences || !preferences.length) { + return values; + } + + const preferred: string[] = []; + for (const selection of values) { + if (selection === "*") { + for (const [preference, value] of lowers) { + if (!map.has(preference)) { + preferred.push(value.orig); + } + } + } else { + const lower = selection.toLowerCase(); + if (lowers.has(lower)) { + preferred.push(lowers.get(lower)!.orig); + } + } + } + + return preferred; +} + +export function acceptLanguage(header = "", preferences?: string[]) { + return ( + parse(header, preferences, { + type: "accept-language", + prefixMatch: true, + })[0] || undefined + ); +} diff --git a/packages/core/src/core/routing/i18n/index.ts b/packages/core/src/core/routing/i18n/index.ts new file mode 100644 index 00000000..8bb7c8be --- /dev/null +++ b/packages/core/src/core/routing/i18n/index.ts @@ -0,0 +1,158 @@ +import { NextConfig } from "@/config/index.js"; +import type { DomainLocale, i18nConfig } from "@/types/next-types"; +import type { InternalEvent, InternalResult } from "@/types/open-next"; +import { emptyReadableStream } from "@/utils/stream.js"; + +import { debug } from "../../../adapters/logger.js"; +import { constructNextUrl, convertToQueryString } from "../util.js"; + +import { acceptLanguage } from "./accept-header"; + +function isLocalizedPath(path: string): boolean { + return NextConfig.i18n?.locales.includes(path.split("/")[1].toLowerCase()) ?? false; +} + +// https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/get-locale-redirect.ts +function getLocaleFromCookie(cookies: Record) { + const i18n = NextConfig.i18n; + const nextLocale = cookies.NEXT_LOCALE?.toLowerCase(); + return nextLocale ? i18n?.locales.find((locale) => nextLocale === locale.toLowerCase()) : undefined; +} + +// Inspired by https://github.com/vercel/next.js/blob/6d93d652e0e7ba72d9a3b66e78746dce2069db03/packages/next/src/shared/lib/i18n/detect-domain-locale.ts#L3-L25 +/** + * @param arg an object containing the hostname and detectedLocale + * @returns The `DomainLocale` object if a domain is detected, `undefined` otherwise + */ +export function detectDomainLocale({ + hostname, + detectedLocale, +}: { + hostname?: string; + detectedLocale?: string; +}): DomainLocale | undefined { + const i18n = NextConfig.i18n; + const domains = i18n?.domains; + if (!domains) { + return; + } + const lowercasedLocale = detectedLocale?.toLowerCase(); + for (const domain of domains) { + // We remove the port if present + const domainHostname = domain.domain.split(":", 1)[0].toLowerCase(); + if ( + hostname === domainHostname || + lowercasedLocale === domain.defaultLocale.toLowerCase() || + domain.locales?.some((locale) => lowercasedLocale === locale.toLowerCase()) + ) { + return domain; + } + } +} + +/** + * + * @param internalEvent + * @param i18n + * @returns The detected locale, if `localeDetection` is set to `false` it will return the default locale **or** the domain default locale if a domain is detected. + */ +export function detectLocale(internalEvent: InternalEvent, i18n: i18nConfig): string { + const domainLocale = detectDomainLocale({ + hostname: internalEvent.headers.host, + }); + if (i18n.localeDetection === false) { + return domainLocale?.defaultLocale ?? i18n.defaultLocale; + } + + const cookiesLocale = getLocaleFromCookie(internalEvent.cookies); + const preferredLocale = acceptLanguage(internalEvent.headers["accept-language"], i18n?.locales); + debug({ + cookiesLocale, + preferredLocale, + defaultLocale: i18n.defaultLocale, + domainLocale, + }); + + return domainLocale?.defaultLocale ?? cookiesLocale ?? preferredLocale ?? i18n.defaultLocale; +} + +/** + * This function is used for OpenNext internal routing to localize the path for next config rewrite/redirects/headers and the middleware + * @param internalEvent + * @returns The localized path + */ +export function localizePath(internalEvent: InternalEvent): string { + const i18n = NextConfig.i18n; + if (!i18n) { + return internalEvent.rawPath; + } + // When the path is already localized we don't need to do anything + if (isLocalizedPath(internalEvent.rawPath)) { + return internalEvent.rawPath; + } + + const detectedLocale = detectLocale(internalEvent, i18n); + + return `/${detectedLocale}${internalEvent.rawPath}`; +} + +/** + * + * @param internalEvent + * In this function, for domain locale redirect we need to rely on the host to be present and correct + * @returns `false` if no redirect is needed, `InternalResult` if a redirect is needed + */ +export function handleLocaleRedirect(internalEvent: InternalEvent): false | InternalResult { + const i18n = NextConfig.i18n; + if (!i18n || i18n.localeDetection === false || internalEvent.rawPath !== "/") { + return false; + } + const preferredLocale = acceptLanguage(internalEvent.headers["accept-language"], i18n?.locales); + + const detectedLocale = detectLocale(internalEvent, i18n); + + const domainLocale = detectDomainLocale({ + hostname: internalEvent.headers.host, + }); + const preferredDomain = detectDomainLocale({ + detectedLocale: preferredLocale, + }); + + if (domainLocale && preferredDomain) { + const isPDomain = preferredDomain.domain === domainLocale.domain; + const isPLocale = preferredDomain.defaultLocale === preferredLocale; + if (!isPDomain || !isPLocale) { + const scheme = `http${preferredDomain.http ? "" : "s"}`; + const rlocale = isPLocale ? "" : preferredLocale; + return { + type: "core", + statusCode: 307, + headers: { + Location: `${scheme}://${preferredDomain.domain}/${rlocale}`, + }, + body: emptyReadableStream(), + isBase64Encoded: false, + }; + } + } + + const defaultLocale = domainLocale?.defaultLocale ?? i18n.defaultLocale; + + if (detectedLocale.toLowerCase() !== defaultLocale.toLowerCase()) { + const nextUrl = constructNextUrl( + internalEvent.url, + `/${detectedLocale}${NextConfig.trailingSlash ? "/" : ""}` + ); + const queryString = convertToQueryString(internalEvent.query); + return { + type: "core", + statusCode: 307, + headers: { + Location: `${nextUrl}${queryString}`, + }, + body: emptyReadableStream(), + isBase64Encoded: false, + }; + } + return false; +} diff --git a/packages/core/src/core/routing/matcher.ts b/packages/core/src/core/routing/matcher.ts new file mode 100644 index 00000000..f91816a1 --- /dev/null +++ b/packages/core/src/core/routing/matcher.ts @@ -0,0 +1,430 @@ +import type { Match, MatchFunction, PathFunction } from "path-to-regexp"; +import { compile, match } from "path-to-regexp"; + +import { NextConfig } from "@/config/index"; +import type { + Header, + PrerenderManifest, + RedirectDefinition, + RewriteDefinition, + RouteHas, +} from "@/types/next-types"; +import type { InternalEvent, InternalResult } from "@/types/open-next"; +import { normalizeRepeatedSlashes } from "@/utils/normalize-path"; +import { emptyReadableStream, toReadableStream } from "@/utils/stream"; + +import { debug } from "../../adapters/logger"; + +import { handleLocaleRedirect, localizePath } from "./i18n"; +import { dynamicRouteMatcher, staticRouteMatcher } from "./routeMatcher"; +import { + constructNextUrl, + convertFromQueryString, + convertToQueryString, + escapeRegex, + getUrlParts, + isExternal, + unescapeRegex, +} from "./util"; + +const routeHasMatcher = + ( + headers: Record, + cookies: Record, + query: Record + ) => + (redirect: RouteHas): boolean => { + switch (redirect.type) { + case "header": + return ( + !!headers?.[redirect.key.toLowerCase()] && + new RegExp(redirect.value ?? "").test(headers[redirect.key.toLowerCase()] ?? "") + ); + case "cookie": + return ( + !!cookies?.[redirect.key] && new RegExp(redirect.value ?? "").test(cookies[redirect.key] ?? "") + ); + case "query": + return query[redirect.key] && Array.isArray(redirect.value) + ? redirect.value.reduce( + (prev, current) => prev || new RegExp(current).test(query[redirect.key] as string), + false + ) + : new RegExp(redirect.value ?? "").test((query[redirect.key] as string | undefined) ?? ""); + case "host": + return headers?.host !== "" && new RegExp(redirect.value ?? "").test(headers.host); + default: + return false; + } + }; + +function checkHas(matcher: ReturnType, has?: RouteHas[], inverted = false) { + return has + ? has.reduce((acc, cur) => { + if (acc === false) return false; + return inverted ? !matcher(cur) : matcher(cur); + }, true) + : true; +} + +const getParamsFromSource = (source: MatchFunction) => (value: string) => { + debug("value", value); + const _match = source(value); + return _match ? _match.params : {}; +}; + +const computeParamHas = + ( + headers: Record, + cookies: Record, + query: Record + ) => + (has: RouteHas): object => { + if (!has.value) return {}; + const matcher = new RegExp(`^${has.value}$`); + const fromSource = (value: string) => { + const matches = value.match(matcher); + return matches?.groups ?? {}; + }; + switch (has.type) { + case "header": + return fromSource(headers[has.key.toLowerCase()] ?? ""); + case "cookie": + return fromSource(cookies[has.key] ?? ""); + case "query": + return Array.isArray(query[has.key]) + ? fromSource((query[has.key] as string[]).join(",")) + : fromSource((query[has.key] as string) ?? ""); + case "host": + return fromSource(headers.host ?? ""); + } + }; + +function convertMatch(match: Match, toDestination: PathFunction, destination: string) { + if (!match) { + return destination; + } + + const { params } = match; + const isUsingParams = Object.keys(params).length > 0; + return isUsingParams ? toDestination(params) : destination; +} + +export function getNextConfigHeaders( + event: InternalEvent, + configHeaders?: Header[] | undefined +): Record { + if (!configHeaders) { + return {}; + } + + const matcher = routeHasMatcher(event.headers, event.cookies, event.query); + + const requestHeaders: Record = {}; + const localizedRawPath = localizePath(event); + + for (const { headers, has, missing, regex, source, locale } of configHeaders) { + const path = locale === false ? event.rawPath : localizedRawPath; + if (new RegExp(regex).test(path) && checkHas(matcher, has) && checkHas(matcher, missing, true)) { + const fromSource = match(source); + const _match = fromSource(path); + headers.forEach((h) => { + try { + const key = convertMatch(_match, compile(h.key), h.key); + const value = convertMatch(_match, compile(h.value), h.value); + requestHeaders[key] = value; + } catch { + debug(`Error matching header ${h.key} with value ${h.value}`); + requestHeaders[h.key] = h.value; + } + }); + } + } + return requestHeaders; +} + +/** + * TODO: This method currently only check for the first match. + * It should check for all matches for `beforeFiles` and `afterFiles` rewrite + * See https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites + */ +export function handleRewrites(event: InternalEvent, rewrites: T[]) { + const { rawPath, headers, query, cookies, url } = event; + const localizedRawPath = localizePath(event); + const matcher = routeHasMatcher(headers, cookies, query); + const computeHas = computeParamHas(headers, cookies, query); + const rewrite = rewrites.find((route) => { + const path = route.locale === false ? rawPath : localizedRawPath; + return ( + new RegExp(route.regex).test(path) && + checkHas(matcher, route.has) && + checkHas(matcher, route.missing, true) + ); + }); + let finalQuery = query; + + let rewrittenUrl = url; + const isExternalRewrite = isExternal(rewrite?.destination); + debug("isExternalRewrite", isExternalRewrite); + if (rewrite) { + const { pathname, protocol, hostname, queryString } = getUrlParts(rewrite.destination, isExternalRewrite); + // We need to use a localized path if the rewrite is not locale specific + const pathToUse = rewrite.locale === false ? rawPath : localizedRawPath; + + debug("urlParts", { pathname, protocol, hostname, queryString }); + const toDestinationPath = compile(escapeRegex(pathname, { isPath: true })); + const toDestinationHost = compile(escapeRegex(hostname)); + const toDestinationQuery = compile(escapeRegex(queryString)); + const params = { + // params for the source + ...getParamsFromSource(match(escapeRegex(rewrite.source, { isPath: true })))(pathToUse), + // params for the has + ...rewrite.has?.reduce((acc, cur) => { + return Object.assign(acc, computeHas(cur)); + }, {}), + // params for the missing + ...rewrite.missing?.reduce((acc, cur) => { + return Object.assign(acc, computeHas(cur)); + }, {}), + }; + const isUsingParams = Object.keys(params).length > 0; + let rewrittenQuery = queryString; + let rewrittenHost = hostname; + let rewrittenPath = pathname; + if (isUsingParams) { + rewrittenPath = unescapeRegex(toDestinationPath(params)); + rewrittenHost = unescapeRegex(toDestinationHost(params)); + rewrittenQuery = unescapeRegex(toDestinationQuery(params)); + } + + // We need to strip the locale from the path if it's a local api route + if (NextConfig.i18n && !isExternalRewrite) { + const strippedPathLocale = rewrittenPath.replace( + new RegExp(`^/(${NextConfig.i18n.locales.join("|")})`), + "" + ); + if (strippedPathLocale.startsWith("/api/")) { + rewrittenPath = strippedPathLocale; + } + } + + rewrittenUrl = isExternalRewrite + ? `${protocol}//${rewrittenHost}${rewrittenPath}` + : new URL(rewrittenPath, event.url).href; + + // We merge query params from the source and the destination + finalQuery = { + ...query, + ...convertFromQueryString(rewrittenQuery), + }; + rewrittenUrl += convertToQueryString(finalQuery); + debug("rewrittenUrl", { rewrittenUrl, finalQuery, isUsingParams }); + } + + return { + internalEvent: { + ...event, + query: finalQuery, + rawPath: new URL(rewrittenUrl).pathname, + url: rewrittenUrl, + }, + __rewrite: rewrite, + isExternalRewrite, + }; +} + +// Normalizes repeated slashes in the path e.g. hello//world -> hello/world +// or backslashes to forward slashes. This prevents requests such as //domain +// from invoking the middleware with `request.url === "domain"`. +// See: https://github.com/vercel/next.js/blob/3ecf087f10fdfba4426daa02b459387bc9c3c54f/packages/next/src/server/base-server.ts#L1016-L1020 +function handleRepeatedSlashRedirect(event: InternalEvent): false | InternalResult { + // Redirect `https://example.com//foo` to `https://example.com/foo`. + if (event.rawPath.match(/(\\|\/\/)/)) { + return { + type: event.type, + statusCode: 308, + headers: { + Location: normalizeRepeatedSlashes(new URL(event.url)), + }, + body: emptyReadableStream(), + isBase64Encoded: false, + }; + } + + return false; +} + +function handleTrailingSlashRedirect(event: InternalEvent): false | InternalResult { + // When rawPath is `//domain`, `url.host` would be `domain`. + // https://github.com/opennextjs/opennextjs-aws/issues/355 + const url = new URL(event.rawPath, "http://localhost"); + + if ( + // Someone is trying to redirect to a different origin, let's not do that + url.host !== "localhost" || + NextConfig.skipTrailingSlashRedirect || + // We should not apply trailing slash redirect to API routes + event.rawPath.startsWith("/api/") + ) { + return false; + } + + const emptyBody = emptyReadableStream(); + + if ( + NextConfig.trailingSlash && + !event.headers["x-nextjs-data"] && + !event.rawPath.endsWith("/") && + !event.rawPath.match(/[\w-]+\.[\w]+$/g) + ) { + const headersLocation = event.url.split("?"); + return { + type: event.type, + statusCode: 308, + headers: { + Location: `${headersLocation[0]}/${headersLocation[1] ? `?${headersLocation[1]}` : ""}`, + }, + body: emptyBody, + isBase64Encoded: false, + }; + } + if (!NextConfig.trailingSlash && event.rawPath.endsWith("/") && event.rawPath !== "/") { + const headersLocation = event.url.split("?"); + return { + type: event.type, + statusCode: 308, + headers: { + Location: `${headersLocation[0].replace(/\/$/, "")}${ + headersLocation[1] ? `?${headersLocation[1]}` : "" + }`, + }, + body: emptyBody, + isBase64Encoded: false, + }; + } + return false; +} + +export function handleRedirects( + event: InternalEvent, + redirects: RedirectDefinition[] +): InternalResult | undefined { + const repeatedSlashRedirect = handleRepeatedSlashRedirect(event); + if (repeatedSlashRedirect) return repeatedSlashRedirect; + + const trailingSlashRedirect = handleTrailingSlashRedirect(event); + if (trailingSlashRedirect) return trailingSlashRedirect; + + const localeRedirect = handleLocaleRedirect(event); + if (localeRedirect) return localeRedirect; + + const { internalEvent, __rewrite } = handleRewrites( + event, + redirects.filter((r) => !r.internal) + ); + if (__rewrite && !__rewrite.internal) { + return { + type: event.type, + statusCode: __rewrite.statusCode ?? 308, + headers: { + Location: internalEvent.url, + }, + body: emptyReadableStream(), + isBase64Encoded: false, + }; + } +} + +export function fixDataPage(internalEvent: InternalEvent, buildId: string): InternalEvent | InternalResult { + const { rawPath, query } = internalEvent; + const basePath = NextConfig.basePath ?? ""; + const dataPattern = `${basePath}/_next/data/${buildId}`; + // Return 404 for data requests that don't match the buildId + if (rawPath.startsWith("/_next/data") && !rawPath.startsWith(dataPattern)) { + return { + type: internalEvent.type, + statusCode: 404, + body: toReadableStream("{}"), + headers: { + "Content-Type": "application/json", + }, + isBase64Encoded: false, + }; + } + + if (rawPath.startsWith(dataPattern) && rawPath.endsWith(".json")) { + const newPath = `${basePath}${rawPath + .slice(dataPattern.length, -".json".length) + .replace(/^\/index$/, "/")}`; + query.__nextDataReq = "1"; + + return { + ...internalEvent, + rawPath: newPath, + query, + url: new URL(`${newPath}${convertToQueryString(query)}`, internalEvent.url).href, + }; + } + return internalEvent; +} + +export function handleFallbackFalse( + internalEvent: InternalEvent, + prerenderManifest?: PrerenderManifest +): { event: InternalEvent; isISR: boolean } { + const { rawPath } = internalEvent; + const { dynamicRoutes = {}, routes = {} } = prerenderManifest ?? {}; + const prerenderedFallbackRoutes = Object.entries(dynamicRoutes).filter( + ([, { fallback }]) => fallback === false + ); + const routeFallback = prerenderedFallbackRoutes.some(([, { routeRegex }]) => { + const routeRegexExp = new RegExp(routeRegex); + return routeRegexExp.test(rawPath); + }); + const locales = NextConfig.i18n?.locales; + const routesAlreadyHaveLocale = + locales?.includes(rawPath.split("/")[1]) || + // If we don't use locales, we don't need to add the default locale + locales === undefined; + let localizedPath = routesAlreadyHaveLocale ? rawPath : `/${NextConfig.i18n?.defaultLocale}${rawPath}`; + // We need to remove the trailing slash if it exists + if ( + // Not if localizedPath is "/" tho, because that would not make it find `isPregenerated` below since it would be try to match an empty string. + localizedPath !== "/" && + NextConfig.trailingSlash && + localizedPath.endsWith("/") + ) { + localizedPath = localizedPath.slice(0, -1); + } + const matchedStaticRoute = staticRouteMatcher(localizedPath); + const prerenderedFallbackRoutesName = prerenderedFallbackRoutes.map(([name]) => name); + const matchedDynamicRoute = dynamicRouteMatcher(localizedPath).filter( + ({ route }) => !prerenderedFallbackRoutesName.includes(route) + ); + + const isPregenerated = Object.keys(routes).includes(localizedPath); + if ( + routeFallback && + !isPregenerated && + matchedStaticRoute.length === 0 && + matchedDynamicRoute.length === 0 + ) { + return { + event: { + ...internalEvent, + rawPath: "/404", + url: constructNextUrl(internalEvent.url, "/404"), + headers: { + ...internalEvent.headers, + "x-invoke-status": "404", + }, + }, + isISR: false, + }; + } + + return { + event: internalEvent, + isISR: routeFallback || isPregenerated, + }; +} diff --git a/packages/core/src/core/routing/middleware.ts b/packages/core/src/core/routing/middleware.ts new file mode 100644 index 00000000..78638c95 --- /dev/null +++ b/packages/core/src/core/routing/middleware.ts @@ -0,0 +1,186 @@ +import type { ReadableStream } from "node:stream/web"; + +import { + FunctionsConfigManifest, + MiddlewareManifest, + NextConfig, + PrerenderManifest, +} from "@/config/index.js"; +import type { InternalEvent, InternalResult, MiddlewareEvent } from "@/types/open-next.js"; +import { emptyReadableStream } from "@/utils/stream.js"; + +import { getQueryFromSearchParams } from "../../overrides/converters/utils.js"; + +import { localizePath } from "./i18n/index.js"; +import { + convertBodyToReadableStream, + getMiddlewareMatch, + isExternal, + normalizeLocationHeader, +} from "./util.js"; + +const middlewareManifest = MiddlewareManifest; +const functionsConfigManifest = FunctionsConfigManifest; + +const middleMatch = getMiddlewareMatch(middlewareManifest, functionsConfigManifest); + +const REDIRECTS = new Set([301, 302, 303, 307, 308]); + +type Middleware = (request: Request) => Response | Promise; +type MiddlewareLoader = () => Promise<{ default: Middleware }>; + +function defaultMiddlewareLoader() { + // @ts-expect-error - This is bundled + return import("./middleware.mjs"); +} + +/** + * + * @param internalEvent the internal event + * @param initialSearch the initial query string as it was received in the handler + * @param middlewareLoader Only used for unit test + * @returns `Promise` + */ +export async function handleMiddleware( + internalEvent: InternalEvent, + initialSearch: string, + middlewareLoader: MiddlewareLoader = defaultMiddlewareLoader +): Promise { + const headers = internalEvent.headers; + + // We bypass the middleware if the request is internal + // We should only do that if the request has the correct `x-prerender-revalidate` header + // The `x-prerender-revalidate` header is set at build time and should be safe to trust + if (headers["x-isr"] && headers["x-prerender-revalidate"] === PrerenderManifest?.preview?.previewModeId) + return internalEvent; + + // We only need the normalizedPath to check if the middleware should run + const normalizedPath = localizePath(internalEvent); + const hasMatch = middleMatch.some((r) => r.test(normalizedPath)); + if (!hasMatch) return internalEvent; + + const initialUrl = new URL(normalizedPath, internalEvent.url); + initialUrl.search = initialSearch; + const url = initialUrl.href; + + const middleware = await middlewareLoader(); + + const result: Response = await middleware.default({ + // `geo` is pre Next 15. + geo: { + // The city name is percent-encoded. + // See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37 + city: decodeURIComponent(headers["x-open-next-city"]), + country: headers["x-open-next-country"], + region: headers["x-open-next-region"], + latitude: headers["x-open-next-latitude"], + longitude: headers["x-open-next-longitude"], + }, + headers, + method: internalEvent.method || "GET", + nextConfig: { + basePath: NextConfig.basePath, + i18n: NextConfig.i18n, + trailingSlash: NextConfig.trailingSlash, + }, + url, + body: convertBodyToReadableStream(internalEvent.method, internalEvent.body), + } as unknown as Request); + const statusCode = result.status; + + /* Apply override headers from middleware + NextResponse.next({ + request: { + headers: new Headers(request.headers), + } + }) + Nextjs will set `x-middleware-override-headers` as a comma separated list of keys. + All the keys will be prefixed with `x-middleware-request-` + + We can delete `x-middleware-override-headers` and check if the key starts with + x-middleware-request- to set the req headers + */ + const responseHeaders = result.headers as Headers; + const reqHeaders: Record = {}; + const resHeaders: Record = {}; + + // These are internal headers used by Next.js, we don't want to expose them to the client + const filteredHeaders = [ + "x-middleware-override-headers", + "x-middleware-next", + "x-middleware-rewrite", + // We need to drop `content-encoding` because it will be decoded + "content-encoding", + ]; + + const xMiddlewareKey = "x-middleware-request-"; + responseHeaders.forEach((value, key) => { + if (key.startsWith(xMiddlewareKey)) { + const k = key.substring(xMiddlewareKey.length); + reqHeaders[k] = value; + } else { + if (filteredHeaders.includes(key.toLowerCase())) return; + if (key.toLowerCase() === "set-cookie") { + resHeaders[key] = resHeaders[key] ? [...resHeaders[key], value] : [value]; + } else if (REDIRECTS.has(statusCode) && key.toLowerCase() === "location") { + resHeaders[key] = normalizeLocationHeader(value, internalEvent.url); + } else { + resHeaders[key] = value; + } + } + }); + + // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite + // NOTE: the header was added to `req` from above + const rewriteUrl = responseHeaders.get("x-middleware-rewrite"); + let isExternalRewrite = false; + let middlewareQuery = internalEvent.query; + let newUrl = internalEvent.url; + if (rewriteUrl) { + newUrl = rewriteUrl; + // If not a string, it should probably throw + if (isExternal(newUrl, internalEvent.headers.host as string)) { + isExternalRewrite = true; + } else { + const rewriteUrlObject = new URL(rewriteUrl); + // Search params from the rewritten URL override the original search params + + middlewareQuery = getQueryFromSearchParams(rewriteUrlObject.searchParams); + + // We still need to add internal search params to the query string for pages router on older versions of Next.js + if ("__nextDataReq" in internalEvent.query) { + middlewareQuery.__nextDataReq = internalEvent.query.__nextDataReq; + } + } + } + + // If the middleware wants to directly return a response (i.e. not using `NextResponse.next()` or `NextResponse.rewrite()`) + // we return the response directly + if (!rewriteUrl && !responseHeaders.get("x-middleware-next")) { + // transfer response body to res + const body = (result.body as ReadableStream) ?? emptyReadableStream(); + + return { + type: internalEvent.type, + statusCode: statusCode, + headers: resHeaders, + body, + isBase64Encoded: false, + } satisfies InternalResult; + } + + return { + responseHeaders: resHeaders, + url: newUrl, + rawPath: new URL(newUrl).pathname, + type: internalEvent.type, + headers: { ...internalEvent.headers, ...reqHeaders }, + body: internalEvent.body, + method: internalEvent.method, + query: middlewareQuery, + cookies: internalEvent.cookies, + remoteAddress: internalEvent.remoteAddress, + isExternalRewrite, + rewriteStatusCode: rewriteUrl && !isExternalRewrite ? statusCode : undefined, + } satisfies MiddlewareEvent; +} diff --git a/packages/core/src/core/routing/queue.ts b/packages/core/src/core/routing/queue.ts new file mode 100644 index 00000000..193760f0 --- /dev/null +++ b/packages/core/src/core/routing/queue.ts @@ -0,0 +1,49 @@ +export function generateShardId(rawPath: string, maxConcurrency: number, prefix: string) { + let a = cyrb128(rawPath); + // We use mulberry32 to generate a random int between 0 and MAX_REVALIDATE_CONCURRENCY + let t = (a += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + const randomFloat = ((t ^ (t >>> 14)) >>> 0) / 4294967296; + // This will generate a random int between 0 and maxConcurrency + const randomInt = Math.floor(randomFloat * maxConcurrency); + return `${prefix}-${randomInt}`; +} + +// Since we're using a FIFO queue, every messageGroupId is treated sequentially +// This could cause a backlog of messages in the queue if there is too much page to +// revalidate at once. To avoid this, we generate a random messageGroupId for each +// revalidation request. +// We can't just use a random string because we need to ensure that the same rawPath +// will always have the same messageGroupId. +// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript#answer-47593316 +export function generateMessageGroupId(rawPath: string) { + // This will generate a random int between 0 and MAX_REVALIDATE_CONCURRENCY + // This means that we could have 1000 revalidate request at the same time + const maxConcurrency = Number.parseInt(process.env.MAX_REVALIDATE_CONCURRENCY ?? "10"); + return generateShardId(rawPath, maxConcurrency, "revalidate"); +} + +// Used to generate a hash int from a string +function cyrb128(str: string) { + let h1 = 1779033703; + let h2 = 3144134277; + let h3 = 1013904242; + let h4 = 2773480762; + for (let i = 0, k: number; i < str.length; i++) { + k = str.charCodeAt(i); + h1 = h2 ^ Math.imul(h1 ^ k, 597399067); + h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); + h3 = h4 ^ Math.imul(h3 ^ k, 951274213); + h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); + } + h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); + h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); + h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); + h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); + h1 ^= h2 ^ h3 ^ h4; + h2 ^= h1; + h3 ^= h1; + h4 ^= h1; + return h1 >>> 0; +} diff --git a/packages/core/src/core/routing/routeMatcher.ts b/packages/core/src/core/routing/routeMatcher.ts new file mode 100644 index 00000000..9a4d814e --- /dev/null +++ b/packages/core/src/core/routing/routeMatcher.ts @@ -0,0 +1,84 @@ +import { AppPathRoutesManifest, PagesManifest, PrerenderManifest, RoutesManifest } from "@/config/index"; +import type { RouteDefinition } from "@/types/next-types"; +import type { ResolvedRoute, RouteType } from "@/types/open-next"; + +// Add the locale prefix to the regex so we correctly match the rawPath +const optionalLocalePrefixRegex = `^/(?:${RoutesManifest.locales.map((locale) => `${locale}/?`).join("|")})?`; + +// Add the basepath prefix to the regex so we correctly match the rawPath +const optionalBasepathPrefixRegex = RoutesManifest.basePath ? `^${RoutesManifest.basePath}/?` : "^/"; + +const optionalPrefix = optionalLocalePrefixRegex.replace("^/", optionalBasepathPrefixRegex); + +function routeMatcher(routeDefinitions: RouteDefinition[]) { + const regexp = routeDefinitions.map((route) => ({ + page: route.page, + regexp: new RegExp(route.regex.replace("^/", optionalPrefix)), + })); + + const { dynamicRoutes = {} } = PrerenderManifest ?? {}; + const prerenderedFallbackRoutes = Object.entries(dynamicRoutes) + .filter(([, { fallback }]) => fallback === false) + .map(([route]) => route); + + const appPathsSet = new Set(); + const routePathsSet = new Set(); + // We need to use AppPathRoutesManifest here + for (const [k, v] of Object.entries(AppPathRoutesManifest)) { + if (k.endsWith("page")) { + appPathsSet.add(v); + } else if (k.endsWith("route")) { + routePathsSet.add(v); + } + } + + return function matchRoute(path: string): ResolvedRoute[] { + const foundRoutes = regexp.filter((route) => route.regexp.test(path)); + + return foundRoutes.map((foundRoute) => { + let routeType: RouteType = "page"; + // Check if the route is a prerendered fallback false route + const isFallback = prerenderedFallbackRoutes.includes(foundRoute.page); + + if (appPathsSet.has(foundRoute.page)) { + routeType = "app"; + } else if (routePathsSet.has(foundRoute.page)) { + routeType = "route"; + } + return { + route: foundRoute.page, + type: routeType, + isFallback, + }; + }); + }; +} + +export const staticRouteMatcher = routeMatcher([...RoutesManifest.routes.static, ...getStaticAPIRoutes()]); +export const dynamicRouteMatcher = routeMatcher(RoutesManifest.routes.dynamic); + +/** + * Returns static API routes for both app and pages router cause Next will filter them out in staticRoutes in `routes-manifest.json`. + * We also need to filter out page files that are under `app/api/*` as those would not be present in the routes manifest either. + * This line from Next.js skips it: + * https://github.com/vercel/next.js/blob/ded56f952154a40dcfe53bdb38c73174e9eca9e5/packages/next/src/build/index.ts#L1299 + * + * Without it handleFallbackFalse will 404 on static API routes if there is a catch-all route on root level. + */ +function getStaticAPIRoutes(): RouteDefinition[] { + const createRouteDefinition = (route: string) => ({ + page: route, + regex: `^${route}(?:/)?$`, + }); + const dynamicRoutePages = new Set(RoutesManifest.routes.dynamic.map(({ page }) => page)); + const pagesStaticAPIRoutes = Object.keys(PagesManifest) + .filter((route) => route.startsWith("/api/") && !dynamicRoutePages.has(route)) + .map(createRouteDefinition); + + // We filter out both static API and page routes from the app paths manifest + const appPathsStaticAPIRoutes = Object.values(AppPathRoutesManifest) + .filter((route) => (route.startsWith("/api/") || route === "/api") && !dynamicRoutePages.has(route)) + .map(createRouteDefinition); + + return [...pagesStaticAPIRoutes, ...appPathsStaticAPIRoutes]; +} diff --git a/packages/core/src/core/routing/util.ts b/packages/core/src/core/routing/util.ts new file mode 100644 index 00000000..0d4ead50 --- /dev/null +++ b/packages/core/src/core/routing/util.ts @@ -0,0 +1,448 @@ +import crypto from "node:crypto"; +import type { OutgoingHttpHeaders } from "node:http"; +import { parse as parseQs, stringify as stringifyQs } from "node:querystring"; +import { ReadableStream } from "node:stream/web"; + +import { BuildId, HtmlPages, NextConfig } from "@/config/index.js"; +import type { IncomingMessage } from "@/http/index.js"; +import { OpenNextNodeResponse } from "@/http/openNextResponse.js"; +import { getQueryFromIterator, parseHeaders } from "@/http/util.js"; +import type { FunctionsConfigManifest, MiddlewareManifest } from "@/types/next-types"; +import type { InternalEvent, InternalResult, RoutingResult, StreamCreator } from "@/types/open-next.js"; + +import { debug, error } from "../../adapters/logger.js"; +import { isBinaryContentType } from "../../utils/binary.js"; + +import { localizePath } from "./i18n/index.js"; +import { generateMessageGroupId } from "./queue.js"; + +/** + * + * @__PURE__ + */ +export function isExternal(url?: string, host?: string) { + if (!url) return false; + const pattern = /^https?:\/\//; + if (!pattern.test(url)) return false; + + if (host) { + try { + const parsedUrl = new URL(url); + return parsedUrl.host !== host; + } catch { + // If URL parsing fails, fall back to substring check + return !url.includes(host); + } + } + return true; +} + +export function convertFromQueryString(query: string) { + if (query === "") return {}; + const queryParts = query.split("&"); + return getQueryFromIterator( + queryParts.map((p) => { + const [key, value] = p.split("="); + return [key, value] as const; + }) + ); +} + +/** + * + * @__PURE__ + */ +export function getUrlParts(url: string, isExternal: boolean) { + if (!isExternal) { + const regex = /\/([^?]*)\??(.*)/; + const match = url.match(regex); + return { + hostname: "", + pathname: match?.[1] ? `/${match[1]}` : url, + protocol: "", + queryString: match?.[2] ?? "", + }; + } + + const regex = /^(https?:)\/\/?([^/\s]+)(\/[^?]*)?(\?.*)?/; + const match = url.match(regex); + if (!match) { + throw new Error(`Invalid external URL: ${url}`); + } + return { + protocol: match[1] ?? "https:", + hostname: match[2], + pathname: match[3] ?? "", + queryString: match[4]?.slice(1) ?? "", + }; +} + +/** + * Creates an URL to a Next page + * + * @param baseUrl Used to get the origin + * @param path The pathname + * @returns The Next URL considering the basePath + * + * @__PURE__ + */ +export function constructNextUrl(baseUrl: string, path: string) { + // basePath is generated as "" if not provided on Next.js 15 (not sure about older versions) + const nextBasePath = NextConfig.basePath ?? ""; + const url = new URL(`${nextBasePath}${path}`, baseUrl); + return url.href; +} + +/** + * + * @__PURE__ + */ +export function convertRes(res: OpenNextNodeResponse): InternalResult { + // Format Next.js response to Lambda response + const statusCode = res.statusCode || 200; + // When using HEAD requests, it seems that flushHeaders is not called, not sure why + // Probably some kind of race condition + const headers = parseHeaders(res.getFixedHeaders()); + const isBase64Encoded = isBinaryContentType(headers["content-type"]) || !!headers["content-encoding"]; + const body = new ReadableStream({ + pull(controller) { + if (!res._chunks || res._chunks.length === 0) { + controller.close(); + return; + } + + controller.enqueue(res._chunks.shift()); + }, + }); + return { + type: "core", + statusCode, + headers, + body, + isBase64Encoded, + }; +} + +/** + * Make sure that multi-value query parameters are transformed to + * ?key=value1&key=value2&... so that Next converts those parameters + * to an array when reading the query parameters + * query should be properly encoded before using this function + * @__PURE__ + */ +export function convertToQueryString(query: Record) { + const queryStrings: string[] = []; + Object.entries(query).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((entry) => queryStrings.push(`${key}=${entry}`)); + } else { + queryStrings.push(`${key}=${value}`); + } + }); + + return queryStrings.length > 0 ? `?${queryStrings.join("&")}` : ""; +} + +/** + * Given a raw query string, returns a record with key value-array pairs + * similar to how multiValueQueryStringParameters are structured + * @__PURE__ + */ +export function convertToQuery(querystring: string) { + if (!querystring) return {}; + const query = new URLSearchParams(querystring); + const queryObject: Record = {}; + + for (const key of query.keys()) { + const queries = query.getAll(key); + queryObject[key] = queries.length > 1 ? queries : queries[0]; + } + + return queryObject; +} + +/** + * + * @__PURE__ + */ +export function getMiddlewareMatch( + middlewareManifest: MiddlewareManifest, + functionsManifest?: FunctionsConfigManifest +) { + if (functionsManifest?.functions?.["/_middleware"]) { + return ( + functionsManifest.functions["/_middleware"].matchers?.map(({ regexp }) => new RegExp(regexp)) ?? [/.*/] + ); + } + const rootMiddleware = middlewareManifest.middleware["/"]; + if (!rootMiddleware?.matchers) return []; + return rootMiddleware.matchers.map(({ regexp }) => new RegExp(regexp)); +} + +/** + * + * @__PURE__ + */ +export function escapeRegex(str: string, { isPath }: { isPath?: boolean } = {}) { + const result = str.replaceAll("(.)", "_µ1_").replaceAll("(..)", "_µ2_").replaceAll("(...)", "_µ3_"); + return isPath ? result : result.replaceAll("+", "_µ4_"); +} + +/** + * + * @__PURE__ + */ +export function unescapeRegex(str: string) { + return str + .replaceAll("_µ1_", "(.)") + .replaceAll("_µ2_", "(..)") + .replaceAll("_µ3_", "(...)") + .replaceAll("_µ4_", "+"); +} + +/** + * @__PURE__ + */ +export function convertBodyToReadableStream(method: string, body?: string | Buffer) { + if (method === "GET" || method === "HEAD") return undefined; + if (!body) return undefined; + return new ReadableStream({ + start(controller) { + controller.enqueue(body); + controller.close(); + }, + }); +} + +enum CommonHeaders { + CACHE_CONTROL = "cache-control", + NEXT_CACHE = "x-nextjs-cache", +} + +/** + * + * @__PURE__ + */ +export function fixCacheHeaderForHtmlPages(internalEvent: InternalEvent, headers: OutgoingHttpHeaders) { + // We don't want to cache error pages + if (internalEvent.rawPath === "/404" || internalEvent.rawPath === "/500") { + if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") { + return; + } + headers[CommonHeaders.CACHE_CONTROL] = "private, no-cache, no-store, max-age=0, must-revalidate"; + return; + } + const localizedPath = localizePath(internalEvent); + // WORKAROUND: `NextServer` does not set cache headers for HTML pages + // https://opennext.js.org/aws/v2/advanced/workaround#workaround-nextserver-does-not-set-cache-headers-for-html-pages + // Requests containing an `x-middleware-prefetch` header must not be cached + if (HtmlPages.includes(localizedPath) && !internalEvent.headers["x-middleware-prefetch"]) { + headers[CommonHeaders.CACHE_CONTROL] = "public, max-age=0, s-maxage=31536000, must-revalidate"; + } +} + +/** + * + * @__PURE__ + */ +export function fixSWRCacheHeader(headers: OutgoingHttpHeaders) { + // WORKAROUND: `NextServer` does not set correct SWR cache headers — https://github.com/sst/open-next#workaround-nextserver-does-not-set-correct-swr-cache-headers + let cacheControl = headers[CommonHeaders.CACHE_CONTROL]; + if (!cacheControl) return; + if (Array.isArray(cacheControl)) { + cacheControl = cacheControl.join(","); + } + if (typeof cacheControl !== "string") return; + headers[CommonHeaders.CACHE_CONTROL] = cacheControl.replace( + /\bstale-while-revalidate(?!=)/, + "stale-while-revalidate=2592000" // 30 days + ); +} + +/** + * + * @__PURE__ + */ +export function addOpenNextHeader(headers: OutgoingHttpHeaders) { + if (NextConfig.poweredByHeader) { + headers["X-OpenNext"] = "1"; + } + if (globalThis.openNextDebug) { + headers["X-OpenNext-Version"] = globalThis.openNextVersion; + } + if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) { + headers["X-OpenNext-RequestId"] = globalThis.__openNextAls.getStore()?.requestId; + } +} + +/** + * + * @__PURE__ + */ +export async function revalidateIfRequired( + host: string, + rawPath: string, + headers: OutgoingHttpHeaders, + req?: IncomingMessage +) { + if (headers[CommonHeaders.NEXT_CACHE] === "STALE") { + // If the URL is rewritten, revalidation needs to be done on the rewritten URL. + // - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation + // - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11 + // @ts-ignore + const internalMeta = req?.[Symbol.for("NextInternalRequestMeta")]; + + // When using Pages Router, two requests will be received: + // 1. one for the page: /foo + // 2. one for the json data: /_next/data/BUILD_ID/foo.json + // The rewritten url is correct for 1, but that for the second request + // does not include the "/_next/data/" prefix. Need to add it. + const revalidateUrl = internalMeta?._nextDidRewrite + ? rawPath.startsWith("/_next/data/") + ? `/_next/data/${BuildId}${internalMeta?._nextRewroteUrl}.json` + : internalMeta?._nextRewroteUrl + : rawPath; + + // We need to pass etag to the revalidation queue to try to bypass the default 5 min deduplication window. + // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html + // If you need to have a revalidation happen more frequently than 5 minutes, + // your page will need to have a different etag to bypass the deduplication window. + // If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated. + try { + const hash = (str: string) => crypto.createHash("md5").update(str).digest("hex"); + + const lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0; + + // For some weird cases, lastModified is not set, haven't been able to figure out yet why + // For those cases we add the etag to the deduplication id, it might help + const eTag = `${headers.etag ?? headers.ETag ?? ""}`; + + await globalThis.queue.send({ + MessageBody: { host, url: revalidateUrl, eTag, lastModified }, + MessageDeduplicationId: hash(`${rawPath}-${lastModified}-${eTag}`), + MessageGroupId: generateMessageGroupId(rawPath), + }); + } catch (e) { + error(`Failed to revalidate stale page ${rawPath}`, e); + } + } +} + +/** + * + * @__PURE__ + */ +export function fixISRHeaders(headers: OutgoingHttpHeaders) { + const sMaxAgeRegex = /s-maxage=(\d+)/; + const match = headers[CommonHeaders.CACHE_CONTROL]?.match(sMaxAgeRegex); + const sMaxAge = match ? Number.parseInt(match[1]) : undefined; + // We only apply the fix if the cache-control header contains s-maxage + if (!sMaxAge) { + return; + } + if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") { + headers[CommonHeaders.CACHE_CONTROL] = "private, no-cache, no-store, max-age=0, must-revalidate"; + return; + } + const _lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0; + if (headers[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) { + debug("cache-control", headers[CommonHeaders.CACHE_CONTROL], _lastModified, Date.now()); + + // 31536000 is the default s-maxage value for SSG pages + if (sMaxAge && sMaxAge !== 31536000) { + // calculate age + const age = Math.round((Date.now() - _lastModified) / 1000); + const remainingTtl = Math.max(sMaxAge - age, 1); + headers[CommonHeaders.CACHE_CONTROL] = `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`; + } + } + if (headers[CommonHeaders.NEXT_CACHE] !== "STALE") return; + + // If the cache is stale, we revalidate in the background + // In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds + // This will cause CloudFront to cache the stale data for a short period of time while we revalidate in the background + // Once the revalidation is complete, CloudFront will serve the fresh data + headers[CommonHeaders.CACHE_CONTROL] = "s-maxage=2, stale-while-revalidate=2592000"; +} + +/** + * + * @param internalEvent + * @param headers + * @param responseStream + * @returns + * @__PURE__ + */ +export function createServerResponse( + routingResult: RoutingResult, + headers: Record, + responseStream?: StreamCreator +) { + const internalEvent = routingResult.internalEvent; + return new OpenNextNodeResponse( + (_headers) => { + fixCacheHeaderForHtmlPages(internalEvent, _headers); + fixSWRCacheHeader(_headers); + addOpenNextHeader(_headers); + fixISRHeaders(_headers); + }, + async (_headers) => { + await revalidateIfRequired(internalEvent.headers.host, internalEvent.rawPath, _headers); + await invalidateCDNOnRequest(routingResult, _headers); + }, + responseStream, + headers, + routingResult.rewriteStatusCode + ); +} + +// This function is used only for `res.revalidate()` +export async function invalidateCDNOnRequest(params: RoutingResult, headers: OutgoingHttpHeaders) { + const { internalEvent, resolvedRoutes, initialURL } = params; + const initialPath = new URL(initialURL).pathname; + const isIsrRevalidation = internalEvent.headers["x-isr"] === "1"; + if (!isIsrRevalidation && headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") { + await globalThis.cdnInvalidationHandler.invalidatePaths([ + { + initialPath, + rawPath: internalEvent.rawPath, + resolvedRoutes, + }, + ]); + } +} + +/** + * Normalizes the Location header to either be a relative path or a full URL. + * If the Location header is relative to the origin, it will return a relative path. + * If it is an absolute URL, it will return the full URL. + * Redirects from Next config query parameters are encoded using `stringifyQs` + * Redirects from the middleware the query parameters are not encoded. + * + * @param location The Location header value + * @param baseUrl The base URL to use for relative paths (i.e the original request URL) + * @param encodeQuery Optional flag to indicate if query parameters should be encoded in the Location header + * @returns An absolute or relative Location header value + */ +export function normalizeLocationHeader(location: string, baseUrl: string, encodeQuery = false): string { + if (!URL.canParse(location)) { + // If the location is not a valid URL, return it as-is + return location; + } + + const locationURL = new URL(location); + const origin = new URL(baseUrl).origin; + + let search = locationURL.search; + // If encodeQuery is true, we need to encode the query parameters + // We could have used URLSearchParams, but that doesn't match what Next does. + if (encodeQuery && search) { + search = `?${stringifyQs(parseQs(search.slice(1)))}`; + } + const href = `${locationURL.origin}${locationURL.pathname}${search}${locationURL.hash}`; + // The URL is relative if the origin is the same as the base URL's origin + if (locationURL.origin === origin) { + return href.slice(origin.length); + } + return href; +} diff --git a/packages/core/src/core/routingHandler.ts b/packages/core/src/core/routingHandler.ts new file mode 100644 index 00000000..232f1d67 --- /dev/null +++ b/packages/core/src/core/routingHandler.ts @@ -0,0 +1,298 @@ +import { BuildId, ConfigHeaders, NextConfig, PrerenderManifest, RoutesManifest } from "@/config/index"; +import type { + InternalEvent, + InternalResult, + PartialResult, + ResolvedRoute, + RoutingResult, +} from "@/types/open-next"; +import type { AssetResolver } from "@/types/overrides"; + +import { debug, error } from "../adapters/logger"; + +import { cacheInterceptor } from "./routing/cacheInterceptor"; +import { detectLocale } from "./routing/i18n"; +import { + fixDataPage, + getNextConfigHeaders, + handleFallbackFalse, + handleRedirects, + handleRewrites, +} from "./routing/matcher"; +import { handleMiddleware } from "./routing/middleware"; +import { dynamicRouteMatcher, staticRouteMatcher } from "./routing/routeMatcher"; +import { constructNextUrl, normalizeLocationHeader } from "./routing/util"; + +export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-"; +export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length; +export const INTERNAL_HEADER_PREFIX = "x-opennext-"; +export const INTERNAL_HEADER_INITIAL_URL = `${INTERNAL_HEADER_PREFIX}initial-url`; +export const INTERNAL_HEADER_LOCALE = `${INTERNAL_HEADER_PREFIX}locale`; +export const INTERNAL_HEADER_RESOLVED_ROUTES = `${INTERNAL_HEADER_PREFIX}resolved-routes`; +export const INTERNAL_HEADER_REWRITE_STATUS_CODE = `${INTERNAL_HEADER_PREFIX}rewrite-status-code`; +export const INTERNAL_EVENT_REQUEST_ID = `${INTERNAL_HEADER_PREFIX}request-id`; + +// Geolocation headers starting from Nextjs 15 +// See https://github.com/vercel/vercel/blob/7714b1c/packages/functions/src/headers.ts +const geoHeaderToNextHeader = { + "x-open-next-city": "x-vercel-ip-city", + "x-open-next-country": "x-vercel-ip-country", + "x-open-next-region": "x-vercel-ip-country-region", + "x-open-next-latitude": "x-vercel-ip-latitude", + "x-open-next-longitude": "x-vercel-ip-longitude", +}; + +/** + * Adds the middleware headers to an event or result. + * + * @param eventOrResult + * @param middlewareHeaders + */ +function applyMiddlewareHeaders( + eventOrResult: InternalEvent | InternalResult, + middlewareHeaders: Record +) { + // Use the `MIDDLEWARE_HEADER_PREFIX` prefix for events, they will be processed by the request handler later. + // Results do not go through the request handler and should not be prefixed. + const isResult = isInternalResult(eventOrResult); + const headers = eventOrResult.headers; + const keyPrefix = isResult ? "" : MIDDLEWARE_HEADER_PREFIX; + Object.entries(middlewareHeaders).forEach(([key, value]) => { + if (value) { + headers[keyPrefix + key] = Array.isArray(value) ? value.join(",") : value; + } + }); +} + +export default async function routingHandler( + event: InternalEvent, + { assetResolver }: { assetResolver?: AssetResolver } +): Promise { + try { + // Add Next geo headers + for (const [openNextGeoName, nextGeoName] of Object.entries(geoHeaderToNextHeader)) { + const value = event.headers[openNextGeoName]; + if (value) { + event.headers[nextGeoName] = value; + } + } + + // First we remove internal headers + // We don't want to allow users to set these headers + for (const key of Object.keys(event.headers)) { + if (key.startsWith(INTERNAL_HEADER_PREFIX) || key.startsWith(MIDDLEWARE_HEADER_PREFIX)) { + delete event.headers[key]; + } + } + + // Headers from the Next config and middleware (the later are applied further down). + let headers: Record = getNextConfigHeaders(event, ConfigHeaders); + + let eventOrResult = fixDataPage(event, BuildId); + + if (isInternalResult(eventOrResult)) { + return eventOrResult; + } + + const redirect = handleRedirects(eventOrResult, RoutesManifest.redirects); + if (redirect) { + // We need to encode the value in the Location header to make sure it is valid according to RFC + redirect.headers.Location = normalizeLocationHeader( + redirect.headers.Location as string, + event.url, + true + ); + debug("redirect", redirect); + return redirect; + } + const middlewareEventOrResult = await handleMiddleware( + eventOrResult, + // We need to pass the initial search without any decoding + // TODO: we'd need to refactor InternalEvent to include the initial querystring directly + // Should be done in another PR because it is a breaking change + new URL(event.url).search + ); + if (isInternalResult(middlewareEventOrResult)) { + return middlewareEventOrResult; + } + + const middlewareHeadersPrioritized = + globalThis.openNextConfig.dangerous?.middlewareHeadersOverrideNextConfigHeaders ?? false; + + if (middlewareHeadersPrioritized) { + headers = { + ...headers, + ...middlewareEventOrResult.responseHeaders, + }; + } else { + headers = { + ...middlewareEventOrResult.responseHeaders, + ...headers, + }; + } + + let isExternalRewrite = middlewareEventOrResult.isExternalRewrite ?? false; + eventOrResult = middlewareEventOrResult; + + if (!isExternalRewrite) { + // First rewrite to be applied + const beforeRewrite = handleRewrites(eventOrResult, RoutesManifest.rewrites.beforeFiles); + eventOrResult = beforeRewrite.internalEvent; + isExternalRewrite = beforeRewrite.isExternalRewrite; + // Check for matching public files after `beforeFiles` rewrites + // See: + // - https://nextjs.org/docs/app/api-reference/file-conventions/middleware#execution-order + // - https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites + if (!isExternalRewrite) { + const assetResult = await assetResolver?.maybeGetAssetResult?.(eventOrResult); + if (assetResult) { + applyMiddlewareHeaders(assetResult, headers); + return assetResult; + } + } + } + let foundStaticRoute = staticRouteMatcher(eventOrResult.rawPath); + const isStaticRoute = !isExternalRewrite && foundStaticRoute.length > 0; + + if (!(isStaticRoute || isExternalRewrite)) { + // Second rewrite to be applied + const afterRewrite = handleRewrites(eventOrResult, RoutesManifest.rewrites.afterFiles); + eventOrResult = afterRewrite.internalEvent; + isExternalRewrite = afterRewrite.isExternalRewrite; + } + + let isISR = false; + // We want to run this just before the dynamic route check + // We can skip it if its an external rewrite + if (!isExternalRewrite) { + const fallbackResult = handleFallbackFalse(eventOrResult, PrerenderManifest); + eventOrResult = fallbackResult.event; + isISR = fallbackResult.isISR; + } + + let foundDynamicRoute = dynamicRouteMatcher(eventOrResult.rawPath); + const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0; + + if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) { + // Fallback rewrite to be applied + const fallbackRewrites = handleRewrites(eventOrResult, RoutesManifest.rewrites.fallback); + eventOrResult = fallbackRewrites.internalEvent; + isExternalRewrite = fallbackRewrites.isExternalRewrite; + } + + const isNextImageRoute = eventOrResult.rawPath.startsWith("/_next/image"); + + const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite; + + // We need to ensure that rewrites are applied before showing the 404 page + foundStaticRoute = staticRouteMatcher(eventOrResult.rawPath); + // We also want to remove dynamic routes that are fallback false + foundDynamicRoute = dynamicRouteMatcher(eventOrResult.rawPath).filter((route) => !route.isFallback); + + // If we still haven't found a route, we show the 404 page + if ( + !( + isRouteFoundBeforeAllRewrites || + isNextImageRoute || + // We need to check again once all rewrites have been applied + foundStaticRoute.length > 0 || + foundDynamicRoute.length > 0 + ) + ) { + eventOrResult = { + ...eventOrResult, + rawPath: "/404", + url: constructNextUrl(eventOrResult.url, "/404"), + headers: { + ...eventOrResult.headers, + "x-middleware-response-cache-control": "private, no-cache, no-store, max-age=0, must-revalidate", + }, + }; + } + + const resolvedRoutes: ResolvedRoute[] = [...foundStaticRoute, ...foundDynamicRoute]; + + if (!isInternalResult(eventOrResult)) { + debug("Attempting cache interception"); + const cacheInterceptionResult = await cacheInterceptor(eventOrResult); + if (isInternalResult(cacheInterceptionResult)) { + applyMiddlewareHeaders(cacheInterceptionResult, headers); + return cacheInterceptionResult; + } else if (isPartialResult(cacheInterceptionResult)) { + // We need to apply the headers to both the result (the streamed response) and the resume request + applyMiddlewareHeaders(cacheInterceptionResult.result, headers); + applyMiddlewareHeaders(cacheInterceptionResult.resumeRequest, headers); + return { + internalEvent: cacheInterceptionResult.resumeRequest, + isExternalRewrite: false, + origin: false, + isISR: false, + resolvedRoutes, + initialURL: event.url, + locale: NextConfig.i18n ? detectLocale(eventOrResult, NextConfig.i18n) : undefined, + rewriteStatusCode: middlewareEventOrResult.rewriteStatusCode, + initialResponse: cacheInterceptionResult.result, + }; + } + } + + // We apply the headers from the middleware response last + applyMiddlewareHeaders(eventOrResult, headers); + + debug("resolvedRoutes", resolvedRoutes); + + return { + internalEvent: eventOrResult, + isExternalRewrite, + origin: false, + isISR, + resolvedRoutes, + initialURL: event.url, + locale: NextConfig.i18n ? detectLocale(eventOrResult, NextConfig.i18n) : undefined, + rewriteStatusCode: middlewareEventOrResult.rewriteStatusCode, + }; + } catch (e) { + error("Error in routingHandler", e); + // In case of an error, we want to return the 500 page from Next.js + return { + internalEvent: { + type: "core", + method: "GET", + rawPath: "/500", + url: constructNextUrl(event.url, "/500"), + headers: { + ...event.headers, + }, + query: event.query, + cookies: event.cookies, + remoteAddress: event.remoteAddress, + }, + isExternalRewrite: false, + origin: false, + isISR: false, + resolvedRoutes: [], + initialURL: event.url, + locale: NextConfig.i18n ? detectLocale(event, NextConfig.i18n) : undefined, + }; + } +} + +/** + * @param eventOrResult + * @returns Whether the event is an instance of `InternalResult` + */ +export function isInternalResult( + eventOrResult: InternalEvent | InternalResult | PartialResult +): eventOrResult is InternalResult { + return eventOrResult != null && "statusCode" in eventOrResult; +} + +/** + * @param eventOrResult + * @returns Whether the event is an instance of `PartialResult` (i.e. for PPR responses) + */ +export function isPartialResult( + eventOrResult: InternalEvent | InternalResult | PartialResult +): eventOrResult is PartialResult { + return eventOrResult != null && "resumeRequest" in eventOrResult; +} diff --git a/packages/core/src/debug.ts b/packages/core/src/debug.ts new file mode 100644 index 00000000..be81b9bd --- /dev/null +++ b/packages/core/src/debug.ts @@ -0,0 +1,15 @@ +import fs from "node:fs"; +import path from "node:path"; + +import type { BuildOptions } from "./build/helper"; + +let init = false; + +export function addDebugFile(options: BuildOptions, name: string, content: unknown) { + if (!init) { + fs.mkdirSync(path.join(options.outputDir, ".debug"), { recursive: true }); + init = true; + } + const strContent = typeof content === "string" ? content : JSON.stringify(content, null, 2); + fs.writeFileSync(path.join(options.outputDir, ".debug", name), strContent); +} diff --git a/packages/core/src/helpers/withCloudflare.ts b/packages/core/src/helpers/withCloudflare.ts new file mode 100644 index 00000000..c8e883e6 --- /dev/null +++ b/packages/core/src/helpers/withCloudflare.ts @@ -0,0 +1,102 @@ +import type { + FunctionOptions, + OpenNextConfig, + RouteTemplate, + SplittedFunctionOptions, +} from "@/types/open-next"; + +type CloudflareCompatibleFunction = Placement extends "regional" + ? FunctionOptions & { + placement: "regional"; + } + : { placement: "global" }; + +type CloudflareCompatibleRoutes = Placement extends "regional" + ? { + placement: "regional"; + routes: RouteTemplate[]; + patterns: string[]; + } + : { + placement: "global"; + routes: `app/${string}/route`; + patterns: string; + }; + +type CloudflareCompatibleSplittedFunction = + CloudflareCompatibleRoutes & CloudflareCompatibleFunction; + +type CloudflareConfig< + Fn extends Record>, +> = { + default: CloudflareCompatibleFunction<"regional">; + functions?: Fn; +} & Omit; + +type InterpolatedSplittedFunctionOptions< + Fn extends Record>, +> = { + [K in keyof Fn]: SplittedFunctionOptions; +}; + +/** + * This function makes it easier to use Cloudflare with OpenNext. + * All options are already restricted to Cloudflare compatible options. + * @example + * ```ts + export default withCloudflare({ + default: { + placement: "regional", + runtime: "node", + }, + functions: { + api: { + placement: "regional", + runtime: "node", + routes: ["app/api/test/route", "page/api/otherApi"], + patterns: ["/api/*"], + }, + global: { + placement: "global", + runtime: "edge", + routes: "app/test/page", + patterns: "/page", + }, + }, +}); + * ``` + */ +export function withCloudflare< + Fn extends Record>, + Key extends keyof Fn, +>(config: CloudflareConfig) { + const functions = Object.entries(config.functions ?? {}).reduce((acc, [name, fn]) => { + const _name = name as Key; + acc[_name] = + fn.placement === "global" + ? { + placement: "global", + runtime: "edge", + routes: [fn.routes], + patterns: [fn.patterns], + override: { + wrapper: "cloudflare-edge", + converter: "edge", + }, + } + : { ...fn, placement: "regional" }; + return acc; + }, {} as InterpolatedSplittedFunctionOptions); + return { + default: config.default, + functions: functions, + middleware: { + external: true, + originResolver: "pattern-env", + override: { + wrapper: "cloudflare-edge", + converter: "edge", + }, + }, + } satisfies OpenNextConfig; +} diff --git a/packages/core/src/http/index.ts b/packages/core/src/http/index.ts new file mode 100644 index 00000000..49efb2fe --- /dev/null +++ b/packages/core/src/http/index.ts @@ -0,0 +1,4 @@ +// @__PURE__ +export * from "./openNextResponse.js"; +// @__PURE__ +export * from "./request.js"; diff --git a/packages/core/src/http/openNextResponse.ts b/packages/core/src/http/openNextResponse.ts new file mode 100644 index 00000000..b76cc271 --- /dev/null +++ b/packages/core/src/http/openNextResponse.ts @@ -0,0 +1,388 @@ +import type { IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from "node:http"; +import type { Socket } from "node:net"; +import { Transform } from "node:stream"; +import type { TransformCallback, Writable } from "node:stream"; + +import type { StreamCreator } from "@/types/open-next"; + +import { debug } from "../adapters/logger"; + +import { parseHeaders, parseSetCookieHeader } from "./util"; + +const SET_COOKIE_HEADER = "set-cookie"; +const CANNOT_BE_USED = "This cannot be used in OpenNext"; + +// We only need to implement the methods that are used by next.js +export class OpenNextNodeResponse extends Transform implements ServerResponse { + statusCode!: number; + statusMessage = ""; + headers: OutgoingHttpHeaders = {}; + headersSent = false; + _chunks: Buffer[] = []; + headersAlreadyFixed = false; + + private _cookies: string[] = []; + private responseStream?: Writable; + private bodyLength = 0; + + // To comply with the ServerResponse interface : + strictContentLength = false; + assignSocket(_socket: Socket): void { + throw new Error(CANNOT_BE_USED); + } + detachSocket(_socket: Socket): void { + throw new Error(CANNOT_BE_USED); + } + // We might have to revisit those 3 in the future + writeContinue(_callback?: (() => void) | undefined): void { + throw new Error(CANNOT_BE_USED); + } + writeEarlyHints(_hints: Record, _callback?: (() => void) | undefined): void { + throw new Error(CANNOT_BE_USED); + } + writeProcessing(): void { + throw new Error(CANNOT_BE_USED); + } + /** + * This is a dummy request object to comply with the ServerResponse interface + * It will never be defined + */ + req!: IncomingMessage; + chunkedEncoding = false; + shouldKeepAlive = true; + useChunkedEncodingByDefault = true; + sendDate = false; + connection: Socket | null = null; + socket: Socket | null = null; + setTimeout(_msecs: number, _callback?: (() => void) | undefined): this { + throw new Error(CANNOT_BE_USED); + } + addTrailers(_headers: OutgoingHttpHeaders | readonly [string, string][]): void { + throw new Error(CANNOT_BE_USED); + } + + constructor( + private fixHeadersFn: (headers: OutgoingHttpHeaders) => void, + private onEnd: (headers: OutgoingHttpHeaders) => Promise, + private streamCreator?: StreamCreator, + private initialHeaders?: OutgoingHttpHeaders, + statusCode?: number + ) { + super(); + // We only set the status code if it is not a NaN and it is a number + // Only allow status codes between 100 and 599 https://httpwg.org/specs/rfc9110.html#status.codes + if (statusCode && Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599) { + this.statusCode = statusCode; + } + + // https://github.com/vercel/next.js/blob/ea08bf2/packages/next/src/server/web/spec-extension/adapters/next-request.ts#L46-L54 + // We want to destroy this response when the original response/request is closed. (i.e when the client disconnects) + // This is to support `request.signal.onabort` in route handlers + streamCreator?.abortSignal?.addEventListener("abort", () => { + this.destroy(); + }); + } + + // Necessary for next 12 + // We might have to implement all the methods here + get originalResponse() { + return this; + } + + get finished() { + return this.responseStream ? this.responseStream?.writableFinished : this.writableFinished; + } + + setHeader(name: string, value: string | string[]): this { + const key = name.toLowerCase(); + if (key === SET_COOKIE_HEADER) { + if (Array.isArray(value)) { + this._cookies = value; + } else { + this._cookies = [value]; + } + } + // We should always replace the header + // See https://nodejs.org/docs/latest-v18.x/api/http.html#responsesetheadername-value + this.headers[key] = value; + + return this; + } + + removeHeader(name: string): this { + const key = name.toLowerCase(); + if (key === SET_COOKIE_HEADER) { + this._cookies = []; + } else { + delete this.headers[key]; + } + return this; + } + + hasHeader(name: string): boolean { + const key = name.toLowerCase(); + if (key === SET_COOKIE_HEADER) { + return this._cookies.length > 0; + } + return this.headers[key] !== undefined; + } + + getHeaders(): OutgoingHttpHeaders { + return this.headers; + } + + getHeader(name: string): OutgoingHttpHeader | undefined { + return this.headers[name.toLowerCase()]; + } + + getHeaderNames(): string[] { + return Object.keys(this.headers); + } + + // Only used directly in next@14+ + flushHeaders() { + this.headersSent = true; + // Initial headers should be merged with the new headers + // These initial headers are the one created either in the middleware or in next.config.js + const mergeHeadersPriority = globalThis.__openNextAls?.getStore()?.mergeHeadersPriority ?? "middleware"; + if (this.initialHeaders) { + this.headers = + mergeHeadersPriority === "middleware" + ? { + ...this.headers, + ...this.initialHeaders, + } + : { + ...this.initialHeaders, + ...this.headers, + }; + const initialCookies = parseSetCookieHeader(this.initialHeaders[SET_COOKIE_HEADER]?.toString()); + this._cookies = + mergeHeadersPriority === "middleware" + ? [...this._cookies, ...initialCookies] + : [...initialCookies, ...this._cookies]; + } + this.fixHeaders(this.headers); + this.fixHeadersForError(); + + // We need to fix the set-cookie header here + this.headers[SET_COOKIE_HEADER] = this._cookies; + + const parsedHeaders = parseHeaders(this.headers); + + // We need to remove the set-cookie header from the parsed headers because + // it does not handle multiple set-cookie headers properly + delete parsedHeaders[SET_COOKIE_HEADER]; + + if (this.streamCreator) { + this.responseStream = this.streamCreator?.writeHeaders({ + statusCode: this.statusCode ?? 200, + cookies: this._cookies, + headers: parsedHeaders, + }); + this.pipe(this.responseStream); + } + } + + appendHeader(name: string, value: string | string[]): this { + const key = name.toLowerCase(); + if (!this.hasHeader(key)) { + return this.setHeader(key, value); + } + const existingHeader = this.getHeader(key) as string | string[]; + const toAppend = Array.isArray(value) ? value : [value]; + const newValue = Array.isArray(existingHeader) + ? [...existingHeader, ...toAppend] + : [existingHeader, ...toAppend]; + return this.setHeader(key, newValue); + } + + // Might be used in next page api routes + writeHead( + statusCode: number, + statusMessage?: string | undefined, + headers?: OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined + ): this; + writeHead(statusCode: number, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined): this; + writeHead(statusCode: unknown, statusMessage?: unknown, headers?: unknown): this { + let _headers = headers as OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined; + let _statusMessage: string | undefined; + if (typeof statusMessage === "string") { + _statusMessage = statusMessage; + } else { + _headers = statusMessage as OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined; + } + const finalHeaders: OutgoingHttpHeaders = this.headers; + if (_headers) { + if (Array.isArray(_headers)) { + // headers may be an Array where the keys and values are in the same list. It is not a list of tuples. So, the even-numbered offsets are key values, and the odd-numbered offsets are the associated values. + for (let i = 0; i < _headers.length; i += 2) { + finalHeaders[_headers[i] as string] = _headers[i + 1] as string | string[]; + } + } else { + for (const key of Object.keys(_headers)) { + finalHeaders[key] = _headers[key]; + } + } + } + + this.statusCode = statusCode as number; + if (headers) { + this.headers = finalHeaders; + } + this.flushHeaders(); + return this; + } + + /** + * OpenNext specific method + */ + + fixHeaders(headers: OutgoingHttpHeaders) { + if (this.headersAlreadyFixed) { + return; + } + this.fixHeadersFn(headers); + this.headersAlreadyFixed = true; + } + + getFixedHeaders(): OutgoingHttpHeaders { + // Do we want to apply this on writeHead? + this.fixHeaders(this.headers); + this.fixHeadersForError(); + // This way we ensure that the cookies are correct + this.headers[SET_COOKIE_HEADER] = this._cookies; + return this.headers; + } + + getBody() { + return Buffer.concat(this._chunks); + } + + private _internalWrite(chunk: Buffer | string, encoding: BufferEncoding) { + // When encoding === 'buffer', chunk is already a Buffer + // and does not need to be converted again. + // @ts-expect-error TS2367 'encoding' can be 'buffer', but it's not in the + // official type definition + const buffer = encoding === "buffer" ? (chunk as Buffer) : Buffer.from(chunk, encoding); + this.bodyLength += buffer.length; + if (this.streamCreator?.retainChunks !== false) { + // Avoid keeping chunks around when the `StreamCreator` supports it to save memory + this._chunks.push(buffer); + } + // No need to pass the encoding for buffers + this.push(buffer); + this.streamCreator?.onWrite?.(); + } + + _transform(chunk: Buffer | string, encoding: BufferEncoding, callback: TransformCallback): void { + if (!this.headersSent) { + this.flushHeaders(); + } + + this._internalWrite(chunk, encoding); + callback(); + } + + _flush(callback: TransformCallback): void { + if (!this.headersSent) { + this.flushHeaders(); + } + // In some cases we might not have a store i.e. for example in the image optimization function + // We may want to reconsider this in the future, it might be interesting to have access to this store everywhere + globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add(this.onEnd(this.headers)); + this.streamCreator?.onFinish?.(this.bodyLength); + + //This is only here because of aws broken streaming implementation. + //Hopefully one day they will be able to give us a working streaming implementation in lambda for everyone + //If you're lucky you have a working streaming implementation in your aws account and don't need this + //If not you can set the OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE env variable to true + //BE CAREFUL: Aws keeps rolling out broken streaming implementations even on accounts that had working ones before + //This is not dependent on the node runtime used + if ( + this.bodyLength === 0 && + // We use an env variable here because not all aws account have the same behavior + // On some aws accounts the response will hang if the body is empty + // We are modifying the response body here, this is not a good practice + process.env.OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE === "true" + ) { + debug('Force writing "SOMETHING" to the response body'); + this.push("SOMETHING"); + } + callback(); + } + + /** + * New method in Node 18.15+ + * There are probably not used right now in Next.js, but better be safe than sorry + */ + + setHeaders(headers: Headers | Map): this { + headers.forEach((value, key) => { + this.setHeader(key, Array.isArray(value) ? value : value.toString()); + }); + return this; + } + + /** + * Next specific methods + * On earlier versions of next.js, those methods are mandatory to make everything work + */ + + get sent() { + return this.finished || this.headersSent; + } + + getHeaderValues(name: string): string[] | undefined { + const values = this.getHeader(name); + + if (values === undefined) return undefined; + + return (Array.isArray(values) ? values : [values]).map((value) => value.toString()); + } + + send() { + for (const chunk of this._chunks) { + this.write(chunk); + } + this.end(); + } + + body(value: string) { + this.write(value); + return this; + } + + onClose(callback: () => void) { + this.on("close", callback); + } + + redirect(destination: string, statusCode: number) { + this.setHeader("Location", destination); + this.statusCode = statusCode; + + // Since IE11 doesn't support the 308 header add backwards + // compatibility using refresh header + if (statusCode === 308) { + this.setHeader("Refresh", `0;url=${destination}`); + } + + //TODO: test to see if we need to call end here + return this; + } + + // For some reason, next returns the 500 error page with some cache-control headers + // We need to fix that + private fixHeadersForError() { + if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") { + return; + } + // We only check for 404 and 500 errors + // The rest should be errors that are handled by the user and they should set the cache headers themselves + if (this.statusCode === 404 || this.statusCode === 500) { + // For some reason calling this.setHeader("Cache-Control", "no-cache, no-store, must-revalidate") does not work here + // The function is not even called, i'm probably missing something obvious + this.headers["cache-control"] = "private, no-cache, no-store, max-age=0, must-revalidate"; + } + } +} diff --git a/packages/core/src/http/request.ts b/packages/core/src/http/request.ts new file mode 100644 index 00000000..a70eaa9f --- /dev/null +++ b/packages/core/src/http/request.ts @@ -0,0 +1,54 @@ +// Copied and modified from serverless-http by Doug Moscrop +// https://github.com/dougmoscrop/serverless-http/blob/master/lib/request.js +// Licensed under the MIT License + +// @ts-nocheck +import http from "node:http"; + +export class IncomingMessage extends http.IncomingMessage { + constructor({ + method, + url, + headers, + body, + remoteAddress, + }: { + method: string; + url: string; + headers: Record; + body?: Buffer; + remoteAddress?: string; + }) { + super({ + encrypted: true, + readable: false, + remoteAddress, + address: () => ({ port: 443 }), + end: Function.prototype, + destroy: Function.prototype, + }); + + // Set the content length when there is a body. + // See https://httpwg.org/specs/rfc9110.html#field.content-length + if (body) { + headers["content-length"] ??= String(Buffer.byteLength(body)); + } + + Object.assign(this, { + ip: remoteAddress, + complete: true, + httpVersion: "1.1", + httpVersionMajor: "1", + httpVersionMinor: "1", + method, + headers, + body, + url, + }); + + this._read = () => { + this.push(body); + this.push(null); + }; + } +} diff --git a/packages/core/src/http/util.ts b/packages/core/src/http/util.ts new file mode 100644 index 00000000..a0902d31 --- /dev/null +++ b/packages/core/src/http/util.ts @@ -0,0 +1,90 @@ +import type http from "node:http"; + +import { warn } from "../adapters/logger"; + +export const parseHeaders = (headers?: http.OutgoingHttpHeader[] | http.OutgoingHttpHeaders) => { + const result: Record = {}; + if (!headers) { + return result; + } + + for (const [key, value] of Object.entries(headers)) { + if (value === undefined) { + continue; + } + const keyLower = key.toLowerCase(); + /** + * Next can return an Array for the Location header when you return null from a get in the cacheHandler on a page that has a redirect() + * We dont want to merge that into a comma-separated string + * If they are the same just return one of them + * Otherwise return the last one + * See: https://github.com/opennextjs/opennextjs-cloudflare/issues/875#issuecomment-3258248276 + * and https://github.com/opennextjs/opennextjs-aws/pull/977#issuecomment-3261763114 + */ + if (keyLower === "location" && Array.isArray(value)) { + if (value.length === 1 || value[0] === value[1]) { + result[keyLower] = value[0]; + } else { + warn("Multiple different values for Location header found. Using the last one"); + result[keyLower] = value[value.length - 1]; + } + continue; + } + result[keyLower] = convertHeader(value); + } + + return result; +}; + +export const convertHeader = (header: http.OutgoingHttpHeader) => { + if (typeof header === "string") { + return header; + } + if (Array.isArray(header)) { + return header.join(","); + } + return String(header); +}; + +/** + * Parses a (comma-separated) list of Set-Cookie headers + * + * @param cookies A comma-separated list of Set-Cookie headers or a list of Set-Cookie headers + * @returns A list of Set-Cookie header + */ +export function parseSetCookieHeader(cookies: string | string[] | null | undefined): string[] { + if (!cookies) { + return []; + } + + if (typeof cookies === "string") { + // Split the cookie string on ",". + // Note that "," can also appear in the Expires value (i.e. `Expires=Thu, 01 June`) + // so we have to skip it with a negative lookbehind. + return cookies.split(/(? c.trim()); + } + + return cookies; +} + +/** + * + * Get the query object from an iterable of [key, value] pairs + * @param it - The iterable of [key, value] pairs + * @returns The query object + */ +export function getQueryFromIterator(it: Iterable<[string, string]>) { + const query: Record = {}; + for (const [key, value] of it) { + if (key in query) { + if (Array.isArray(query[key])) { + query[key].push(value); + } else { + query[key] = [query[key], value]; + } + } else { + query[key] = value; + } + } + return query; +} diff --git a/packages/core/src/logger.ts b/packages/core/src/logger.ts new file mode 100644 index 00000000..9ce195f1 --- /dev/null +++ b/packages/core/src/logger.ts @@ -0,0 +1,18 @@ +import chalk from "chalk"; + +type LEVEL = "info" | "debug"; + +let logLevel: LEVEL = "info"; + +export default { + setLevel: (level: LEVEL) => (logLevel = level), + debug: (...args: unknown[]) => { + if (logLevel !== "debug") return; + console.log(chalk.magenta("DEBUG"), ...args); + }, + info: console.log, + warn: (...args: unknown[]) => console.warn(chalk.yellow("WARN"), ...args), + error: (...args: unknown[]) => console.error(chalk.red("ERROR"), ...args), + time: console.time, + timeEnd: console.timeEnd, +}; diff --git a/packages/core/src/minimize-js.ts b/packages/core/src/minimize-js.ts new file mode 100644 index 00000000..2334c0fb --- /dev/null +++ b/packages/core/src/minimize-js.ts @@ -0,0 +1,100 @@ +// Copied and modified from node-minify-all-js by Adones Pitogo +// https://github.com/adonespitogo/node-minify-all-js/blob/master/index.js + +// @ts-nocheck +import fs from "node:fs/promises"; +import path from "node:path"; + +import minify from "@node-minify/core"; +import terser from "@node-minify/terser"; + +const failed_files = []; +let total_files = 0; +const options = {}; + +const promiseSeries = async (tasks, initial) => { + if (!Array.isArray(tasks)) { + return Promise.reject(new TypeError("promise.series only accepts an array of functions")); + } + + return tasks.reduce((current, next) => { + return current.then(next); + }, Promise.resolve(initial)); +}; + +const minifyJS = async (file) => { + total_files++; + try { + await minify({ + compressor: terser, + input: file, + output: file, + options: { + module: options.module, + mangle: options.mangle, + compress: { reduce_vars: false }, + }, + }); + } catch (e) { + failed_files.push(file); + } + //process.stdout.write("."); +}; + +const minifyJSON = async (file) => { + try { + if (options.compress_json || options.packagejson) { + total_files++; + const is_package_json = file.indexOf("package.json") > -1; + const data = await fs.readFile(file, "utf8"); + const json = JSON.parse(data); + let new_json = {}; + if (options.packagejson && is_package_json) { + const { name, version, bin, main, binary, engines } = json; + new_json = { name, version }; + if (bin) new_json.bin = bin; + if (binary) new_json.binary = binary; + if (main) new_json.main = main; + if (engines) new_json.engines = engines; + } else { + new_json = json; + } + await fs.writeFile(file, JSON.stringify(new_json)); + } + } catch (e) {} + //process.stdout.write("."); +}; + +const walk = async (currentDirPath) => { + const js_files = []; + const json_files = []; + const dirs = []; + const current_dirs = await fs.readdir(currentDirPath); + for (const name of current_dirs) { + const filePath = path.join(currentDirPath, name); + const stat = await fs.stat(filePath); + const is_bin = /\.bin$/; + if (stat.isFile()) { + if (filePath.substr(-5) === ".json") json_files.push(filePath); + else if (filePath.substr(-3) === ".js" || options.all_js) js_files.push(filePath); + } else if (stat.isDirectory() && !is_bin.test(filePath)) { + dirs.push(filePath); + } + } + const js_promise = Promise.all(js_files.map((f) => minifyJS(f))); + const json_promise = Promise.all(json_files.map((f) => minifyJSON(f))); + await Promise.all([js_promise, json_promise]); + await promiseSeries(dirs.map((dir) => () => walk(dir))); +}; + +export async function minifyAll(dir, opts) { + Object.assign(options, opts || {}); + //console.log("minify-all-js options:\n", JSON.stringify(options, null, 2)); + await walk(dir); + //process.stdout.write(".\n"); + //console.log("Total found files: " + total_files); + if (failed_files.length) { + console.log("\n\nFailed to minify files:"); + failed_files.forEach((f) => console.log(`\t${f}`)); + } +} diff --git a/packages/core/src/overrides/assetResolver/dummy.ts b/packages/core/src/overrides/assetResolver/dummy.ts new file mode 100644 index 00000000..4de2f2d0 --- /dev/null +++ b/packages/core/src/overrides/assetResolver/dummy.ts @@ -0,0 +1,12 @@ +import type { AssetResolver } from "@/types/overrides"; + +/** + * A dummy asset resolver. + * + * It never overrides the result with an asset. + */ +const resolver: AssetResolver = { + name: "dummy", +}; + +export default resolver; diff --git a/packages/core/src/overrides/cdnInvalidation/dummy.ts b/packages/core/src/overrides/cdnInvalidation/dummy.ts new file mode 100644 index 00000000..5124a1e5 --- /dev/null +++ b/packages/core/src/overrides/cdnInvalidation/dummy.ts @@ -0,0 +1,8 @@ +import type { CDNInvalidationHandler } from "@/types/overrides"; + +export default { + name: "dummy", + invalidatePaths: (_) => { + return Promise.resolve(); + }, +} satisfies CDNInvalidationHandler; diff --git a/packages/core/src/overrides/converters/dummy.ts b/packages/core/src/overrides/converters/dummy.ts new file mode 100644 index 00000000..3d666d99 --- /dev/null +++ b/packages/core/src/overrides/converters/dummy.ts @@ -0,0 +1,24 @@ +import type { Converter } from "@/types/overrides"; + +type DummyEventOrResult = { + type: "dummy"; + original: unknown; +}; + +const converter: Converter = { + convertFrom(event) { + return Promise.resolve({ + type: "dummy", + original: event, + }); + }, + convertTo(internalResult) { + return Promise.resolve({ + type: "dummy", + original: internalResult, + }); + }, + name: "dummy", +}; + +export default converter; diff --git a/packages/core/src/overrides/converters/edge.ts b/packages/core/src/overrides/converters/edge.ts new file mode 100644 index 00000000..5261283d --- /dev/null +++ b/packages/core/src/overrides/converters/edge.ts @@ -0,0 +1,119 @@ +import { Buffer } from "node:buffer"; + +import cookieParser from "cookie"; + +import { parseSetCookieHeader } from "@/http/util"; +import type { InternalEvent, InternalResult, MiddlewareResult } from "@/types/open-next"; +import type { Converter } from "@/types/overrides"; + +import { getQueryFromSearchParams } from "./utils.js"; + +declare global { + // Makes convertTo returns the request instead of fetching it. + var __dangerous_ON_edge_converter_returns_request: boolean | undefined; +} + +// https://fetch.spec.whatwg.org/#statuses +const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]); + +const converter: Converter = { + convertFrom: async (event: unknown) => { + const request = event as Request; + const url = new URL(request.url); + + const searchParams = url.searchParams; + const query = getQueryFromSearchParams(searchParams); + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + const rawPath = url.pathname; + const method = request.method; + const shouldHaveBody = method !== "GET" && method !== "HEAD"; + + // Only read body for methods that should have one + const body = shouldHaveBody ? Buffer.from(await request.arrayBuffer()) : undefined; + + const cookieHeader = request.headers.get("cookie"); + const cookies = cookieHeader ? (cookieParser.parse(cookieHeader) as Record) : {}; + + return { + type: "core", + method, + rawPath, + url: request.url, + body, + headers, + remoteAddress: request.headers.get("x-forwarded-for") ?? "::1", + query, + cookies, + }; + }, + convertTo: async (result) => { + if ("internalEvent" in result) { + const request = new Request(result.internalEvent.url, { + body: result.internalEvent.body as BodyInit | undefined, + method: result.internalEvent.method, + headers: { + ...result.internalEvent.headers, + "x-forwarded-host": result.internalEvent.headers.host, + }, + }); + + if (globalThis.__dangerous_ON_edge_converter_returns_request === true) { + if (result.initialResponse) { + return { + initialResponse: result.initialResponse, + request, + }; + } + return request; + } + + const cfCache = + (result.isISR || result.internalEvent.rawPath.startsWith("/_next/image")) && + process.env.DISABLE_CACHE !== "true" + ? { cacheEverything: true } + : {}; + + //TODO: we need to handle the PPR case here as well. + // We'll revisit this when we'll look at making StreamCreator mandatory. + return fetch(request, { + // This is a hack to make sure that the response is cached by Cloudflare + // See https://developers.cloudflare.com/workers/examples/cache-using-fetch/#caching-html-resources + // @ts-expect-error - This is a Cloudflare specific option + cf: cfCache, + }); + } + const headers = new Headers(); + for (const [key, value] of Object.entries(result.headers)) { + if (key === "set-cookie" && typeof value === "string") { + // If the value is a string, we need to parse it into an array + // This is the case for middleware direct result + const cookies = parseSetCookieHeader(value); + for (const cookie of cookies) { + headers.append(key, cookie); + } + continue; + } + if (Array.isArray(value)) { + for (const v of value) { + headers.append(key, v); + } + } else { + headers.set(key, value); + } + } + + // We should not return a body for statusCode's that doesn't allow bodies + const body = NULL_BODY_STATUSES.has(result.statusCode) ? null : (result.body as ReadableStream); + + return new Response(body, { + status: result.statusCode, + headers, + }); + }, + name: "edge", +}; + +export default converter; diff --git a/packages/core/src/overrides/converters/node.ts b/packages/core/src/overrides/converters/node.ts new file mode 100644 index 00000000..60c5fffa --- /dev/null +++ b/packages/core/src/overrides/converters/node.ts @@ -0,0 +1,58 @@ +import type { IncomingMessage } from "node:http"; + +import cookieParser from "cookie"; + +import type { InternalResult } from "@/types/open-next"; +import type { Converter } from "@/types/overrides"; + +import { extractHostFromHeaders, getQueryFromSearchParams } from "./utils.js"; + +const converter: Converter = { + convertFrom: async (event: unknown) => { + const req = event as IncomingMessage & { protocol?: string }; + const body = await new Promise((resolve) => { + const chunks: Uint8Array[] = []; + req.on("data", (chunk) => { + chunks.push(chunk); + }); + req.on("end", () => { + resolve(Buffer.concat(chunks)); + }); + }); + + const headers = Object.fromEntries( + Object.entries(req.headers ?? {}) + .map(([key, value]) => [key.toLowerCase(), Array.isArray(value) ? value.join(",") : value]) + .filter(([key]) => key) + ); + // https://nodejs.org/api/http.html#messageurl + const url = new URL( + `${req.protocol ? req.protocol : "http"}://${extractHostFromHeaders(headers)}${req.url}` + ); + const query = getQueryFromSearchParams(url.searchParams); + + const cookieHeader = req.headers.cookie; + const cookies = cookieHeader ? (cookieParser.parse(cookieHeader) as Record) : {}; + + return { + type: "core", + method: req.method ?? "GET", + rawPath: url.pathname, + url: url.href, + body, + headers, + remoteAddress: (req.headers["x-forwarded-for"] as string) ?? req.socket.remoteAddress ?? "::1", + query, + cookies, + }; + }, + // Nothing to do here, it's streaming + convertTo: async (internalResult: InternalResult) => ({ + body: internalResult.body, + headers: internalResult.headers, + statusCode: internalResult.statusCode, + }), + name: "node", +}; + +export default converter; diff --git a/packages/core/src/overrides/converters/utils.ts b/packages/core/src/overrides/converters/utils.ts new file mode 100644 index 00000000..0fb60ff1 --- /dev/null +++ b/packages/core/src/overrides/converters/utils.ts @@ -0,0 +1,31 @@ +import { getQueryFromIterator } from "@/http/util.js"; + +export function removeUndefinedFromQuery(query: Record) { + const newQuery: Record = {}; + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) { + newQuery[key] = value; + } + } + return newQuery; +} + +/** + * Extract the host from the headers (default to "on") + * + * @param headers + * @returns The host + */ +export function extractHostFromHeaders(headers: Record): string { + return headers["x-forwarded-host"] ?? headers.host ?? "on"; +} + +/** + * Get the query object from an URLSearchParams + * + * @param searchParams + * @returns + */ +export function getQueryFromSearchParams(searchParams: URLSearchParams) { + return getQueryFromIterator(searchParams.entries()); +} diff --git a/packages/core/src/overrides/imageLoader/dummy.ts b/packages/core/src/overrides/imageLoader/dummy.ts new file mode 100644 index 00000000..b3b2d3a6 --- /dev/null +++ b/packages/core/src/overrides/imageLoader/dummy.ts @@ -0,0 +1,11 @@ +import type { ImageLoader } from "@/types/overrides"; +import { FatalError } from "@/utils/error"; + +const dummyLoader: ImageLoader = { + name: "dummy", + load: async (_: string) => { + throw new FatalError("Dummy loader is not implemented"); + }, +}; + +export default dummyLoader; diff --git a/packages/core/src/overrides/imageLoader/fs-dev.ts b/packages/core/src/overrides/imageLoader/fs-dev.ts new file mode 100644 index 00000000..cdf0b77e --- /dev/null +++ b/packages/core/src/overrides/imageLoader/fs-dev.ts @@ -0,0 +1,21 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { NextConfig } from "@/config/index"; +import type { ImageLoader } from "@/types/overrides"; +import { getMonorepoRelativePath } from "@/utils/normalize-path"; + +export default { + name: "fs-dev", + load: async (url: string) => { + const urlWithoutBasePath = NextConfig.basePath ? url.slice(NextConfig.basePath.length) : url; + const imagePath = path.join(getMonorepoRelativePath(), "assets", urlWithoutBasePath); + const body = fs.createReadStream(imagePath); + const contentType = url.endsWith(".png") ? "image/png" : "image/jpeg"; + return { + body, + contentType, + cacheControl: "public, max-age=31536000, immutable", + }; + }, +} satisfies ImageLoader; diff --git a/packages/core/src/overrides/imageLoader/host.ts b/packages/core/src/overrides/imageLoader/host.ts new file mode 100644 index 00000000..7b2dd215 --- /dev/null +++ b/packages/core/src/overrides/imageLoader/host.ts @@ -0,0 +1,33 @@ +import { Readable } from "node:stream"; +import type { ReadableStream } from "node:stream/web"; + +import type { ImageLoader } from "@/types/overrides"; +import { FatalError } from "@/utils/error"; + +const hostLoader: ImageLoader = { + name: "host", + load: async (key: string) => { + const host = process.env.HOST; + if (!host) { + throw new FatalError("Host must be defined!"); + } + const url = `https://${host}${key}`; + const response = await fetch(url); + if (!response.ok) { + throw new FatalError(`Failed to fetch image from ${url}`); + } + if (!response.body) { + throw new FatalError("No body in response"); + } + const body = Readable.fromWeb(response.body as ReadableStream); + const contentType = response.headers.get("content-type") ?? "image/jpeg"; + const cacheControl = response.headers.get("cache-control") ?? "private, max-age=0, must-revalidate"; + return { + body, + contentType, + cacheControl, + }; + }, +}; + +export default hostLoader; diff --git a/packages/core/src/overrides/incrementalCache/dummy.ts b/packages/core/src/overrides/incrementalCache/dummy.ts new file mode 100644 index 00000000..24639aef --- /dev/null +++ b/packages/core/src/overrides/incrementalCache/dummy.ts @@ -0,0 +1,17 @@ +import type { IncrementalCache } from "@/types/overrides"; +import { IgnorableError } from "@/utils/error"; + +const dummyIncrementalCache: IncrementalCache = { + name: "dummy", + get: async () => { + throw new IgnorableError('"Dummy" cache does not cache anything'); + }, + set: async () => { + throw new IgnorableError('"Dummy" cache does not cache anything'); + }, + delete: async () => { + throw new IgnorableError('"Dummy" cache does not cache anything'); + }, +}; + +export default dummyIncrementalCache; diff --git a/packages/core/src/overrides/incrementalCache/fs-dev.ts b/packages/core/src/overrides/incrementalCache/fs-dev.ts new file mode 100644 index 00000000..9df4f9c2 --- /dev/null +++ b/packages/core/src/overrides/incrementalCache/fs-dev.ts @@ -0,0 +1,37 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import type { IncrementalCache } from "@/types/overrides.js"; +import { getMonorepoRelativePath } from "@/utils/normalize-path"; + +const buildId = process.env.NEXT_BUILD_ID; +const basePath = path.join(getMonorepoRelativePath(), `cache/${buildId}`); + +const getCacheKey = (key: string) => { + return path.join(basePath, `${key}.cache`); +}; + +const cache: IncrementalCache = { + name: "fs-dev", + get: async (key: string) => { + const fileData = await fs.readFile(getCacheKey(key), "utf-8"); + const data = JSON.parse(fileData); + const { mtime } = await fs.stat(getCacheKey(key)); + return { + value: data, + lastModified: mtime.getTime(), + }; + }, + set: async (key, value, isFetch) => { + const data = JSON.stringify(value); + const cacheKey = getCacheKey(key); + // We need to create the directory before writing the file + await fs.mkdir(path.dirname(cacheKey), { recursive: true }); + await fs.writeFile(cacheKey, data); + }, + delete: async (key) => { + await fs.rm(getCacheKey(key)); + }, +}; + +export default cache; diff --git a/packages/core/src/overrides/originResolver/dummy.ts b/packages/core/src/overrides/originResolver/dummy.ts new file mode 100644 index 00000000..b713dedc --- /dev/null +++ b/packages/core/src/overrides/originResolver/dummy.ts @@ -0,0 +1,10 @@ +import type { OriginResolver } from "@/types/overrides"; + +const dummyOriginResolver: OriginResolver = { + name: "dummy", + resolve: async (_path: string) => { + return false as const; + }, +}; + +export default dummyOriginResolver; diff --git a/packages/core/src/overrides/originResolver/pattern-env.ts b/packages/core/src/overrides/originResolver/pattern-env.ts new file mode 100644 index 00000000..3996f02b --- /dev/null +++ b/packages/core/src/overrides/originResolver/pattern-env.ts @@ -0,0 +1,84 @@ +import type { Origin } from "@/types/open-next"; +import type { OriginResolver } from "@/types/overrides"; + +import { debug, error } from "../../adapters/logger"; + +// Cache parsed origins and compiled patterns at module level +let cachedOrigins: Record; +const cachedPatterns: Array<{ + key: string; + patterns: string[]; + regexes: RegExp[]; +}> = []; +let initialized = false; + +/** + * Initializes the cached values on the first execution + */ +function initializeOnce(): void { + if (initialized) return; + + // Parse origin JSON once + cachedOrigins = JSON.parse(process.env.OPEN_NEXT_ORIGIN ?? "{}") as Record; + + // Pre-compile all regex patterns + const functions = globalThis.openNextConfig.functions ?? {}; + for (const key in functions) { + if (key !== "default") { + const value = functions[key]; + const regexes: RegExp[] = []; + + for (const pattern of value.patterns) { + // Convert cloudfront pattern to regex + const regexPattern = `/${pattern + .replace(/\*\*/g, "(.*)") + .replace(/\*/g, "([^/]*)") + .replace(/\//g, "\\/") + .replace(/\?/g, ".")}`; + regexes.push(new RegExp(regexPattern)); + } + + cachedPatterns.push({ + key, + patterns: value.patterns, + regexes, + }); + } + } + + initialized = true; +} + +const envLoader: OriginResolver = { + name: "env", + resolve: async (_path: string) => { + try { + initializeOnce(); + + // Test against pre-compiled patterns + for (const { key, patterns, regexes } of cachedPatterns) { + for (const regex of regexes) { + if (regex.test(_path)) { + debug("Using origin", key, patterns); + return cachedOrigins[key]; + } + } + } + + if (_path.startsWith("/_next/image") && cachedOrigins.imageOptimizer) { + debug("Using origin", "imageOptimizer", _path); + return cachedOrigins.imageOptimizer; + } + if (cachedOrigins.default) { + debug("Using default origin", cachedOrigins.default, _path); + return cachedOrigins.default; + } + return false as const; + } catch (e) { + error("Error while resolving origin", e); + return false as const; + } + }, +}; + +export default envLoader; diff --git a/packages/core/src/overrides/proxyExternalRequest/dummy.ts b/packages/core/src/overrides/proxyExternalRequest/dummy.ts new file mode 100644 index 00000000..ec361260 --- /dev/null +++ b/packages/core/src/overrides/proxyExternalRequest/dummy.ts @@ -0,0 +1,11 @@ +import type { ProxyExternalRequest } from "@/types/overrides"; +import { FatalError } from "@/utils/error"; + +const DummyProxyExternalRequest: ProxyExternalRequest = { + name: "dummy", + proxy: async (_event) => { + throw new FatalError("This is a dummy implementation"); + }, +}; + +export default DummyProxyExternalRequest; diff --git a/packages/core/src/overrides/proxyExternalRequest/fetch.ts b/packages/core/src/overrides/proxyExternalRequest/fetch.ts new file mode 100644 index 00000000..df1dd18c --- /dev/null +++ b/packages/core/src/overrides/proxyExternalRequest/fetch.ts @@ -0,0 +1,33 @@ +import type { ProxyExternalRequest } from "@/types/overrides"; +import { emptyReadableStream } from "@/utils/stream"; + +const fetchProxy: ProxyExternalRequest = { + name: "fetch-proxy", + // @ts-ignore + proxy: async (internalEvent) => { + const { url, headers: eventHeaders, method, body } = internalEvent; + + const headers = Object.fromEntries( + Object.entries(eventHeaders).filter(([key]) => key.toLowerCase() !== "cf-connecting-ip") + ); + + const response = await fetch(url, { + method, + headers, + body: body as BodyInit | undefined, + }); + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + return { + type: "core", + headers: responseHeaders, + statusCode: response.status, + isBase64Encoded: true, + body: response.body ?? emptyReadableStream(), + }; + }, +}; + +export default fetchProxy; diff --git a/packages/core/src/overrides/proxyExternalRequest/node.ts b/packages/core/src/overrides/proxyExternalRequest/node.ts new file mode 100644 index 00000000..97fdef0a --- /dev/null +++ b/packages/core/src/overrides/proxyExternalRequest/node.ts @@ -0,0 +1,83 @@ +import { request } from "node:https"; +import { Readable } from "node:stream"; + +import type { InternalEvent, InternalResult } from "@/types/open-next"; +import type { ProxyExternalRequest } from "@/types/overrides"; + +import { debug, error } from "../../adapters/logger"; +import { isBinaryContentType } from "../../utils/binary"; + +function filterHeadersForProxy(headers: Record) { + const filteredHeaders: Record = {}; + const disallowedHeaders = [ + "host", + "connection", + "via", + "x-cache", + "transfer-encoding", + "content-encoding", + "content-length", + ]; + Object.entries(headers) + .filter(([key, _]) => { + const lowerKey = key.toLowerCase(); + return !(disallowedHeaders.includes(lowerKey) || lowerKey.startsWith("x-amz")); + }) + .forEach(([key, value]) => { + filteredHeaders[key] = value?.toString() ?? ""; + }); + return filteredHeaders; +} + +const nodeProxy: ProxyExternalRequest = { + name: "node-proxy", + proxy: (internalEvent: InternalEvent) => { + const { url, headers, method, body } = internalEvent; + debug("proxyRequest", url); + return new Promise((resolve, reject) => { + const filteredHeaders = filterHeadersForProxy(headers); + debug("filteredHeaders", filteredHeaders); + const req = request( + url, + { + headers: filteredHeaders, + method, + rejectUnauthorized: false, + }, + (_res) => { + const resHeaders = _res.headers; + const nodeReadableStream = + resHeaders["content-encoding"] === "br" + ? _res.pipe(require("node:zlib").createBrotliDecompress()) + : resHeaders["content-encoding"] === "gzip" + ? _res.pipe(require("node:zlib").createGunzip()) + : _res; + const isBase64Encoded = + isBinaryContentType(resHeaders["content-type"]) || !!resHeaders["content-encoding"]; + const result: InternalResult = { + type: "core", + headers: filterHeadersForProxy(resHeaders), + statusCode: _res.statusCode ?? 200, + // TODO: check base64 encoding + isBase64Encoded, + body: Readable.toWeb(nodeReadableStream), + }; + + resolve(result); + + _res.on("error", (e) => { + error("proxyRequest error", e); + reject(e); + }); + } + ); + + if (body && method !== "GET" && method !== "HEAD") { + req.write(body); + } + req.end(); + }); + }, +}; + +export default nodeProxy; diff --git a/packages/core/src/overrides/queue/direct.ts b/packages/core/src/overrides/queue/direct.ts new file mode 100644 index 00000000..0e1f99f9 --- /dev/null +++ b/packages/core/src/overrides/queue/direct.ts @@ -0,0 +1,20 @@ +import type { Queue } from "@/types/overrides.js"; + +const queue: Queue = { + name: "dev-queue", + send: async (message) => { + const prerenderManifest = (await import("../../adapters/config/index.js")).PrerenderManifest; + const { host, url } = message.MessageBody; + const protocol = host.includes("localhost") ? "http" : "https"; + const revalidateId: string = prerenderManifest?.preview?.previewModeId ?? ""; + await globalThis.internalFetch(`${protocol}://${host}${url}`, { + method: "HEAD", + headers: { + "x-prerender-revalidate": revalidateId, + "x-isr": "1", + }, + }); + }, +}; + +export default queue; diff --git a/packages/core/src/overrides/queue/dummy.ts b/packages/core/src/overrides/queue/dummy.ts new file mode 100644 index 00000000..8b5646ee --- /dev/null +++ b/packages/core/src/overrides/queue/dummy.ts @@ -0,0 +1,11 @@ +import type { Queue } from "@/types/overrides"; +import { FatalError } from "@/utils/error"; + +const dummyQueue: Queue = { + name: "dummy", + send: async () => { + throw new FatalError("Dummy queue is not implemented"); + }, +}; + +export default dummyQueue; diff --git a/packages/core/src/overrides/tagCache/dummy.ts b/packages/core/src/overrides/tagCache/dummy.ts new file mode 100644 index 00000000..ac44b532 --- /dev/null +++ b/packages/core/src/overrides/tagCache/dummy.ts @@ -0,0 +1,21 @@ +import type { TagCache } from "@/types/overrides"; + +// We don't want to throw error on this one because we might use it when we don't need tag cache +const dummyTagCache: TagCache = { + name: "dummy", + mode: "original", + getByPath: async () => { + return []; + }, + getByTag: async () => { + return []; + }, + getLastModified: async (_: string, lastModified) => { + return lastModified ?? Date.now(); + }, + writeTags: async () => { + return; + }, +}; + +export default dummyTagCache; diff --git a/packages/core/src/overrides/tagCache/fs-dev-nextMode.ts b/packages/core/src/overrides/tagCache/fs-dev-nextMode.ts new file mode 100644 index 00000000..49ccb498 --- /dev/null +++ b/packages/core/src/overrides/tagCache/fs-dev-nextMode.ts @@ -0,0 +1,53 @@ +import type { NextModeTagCache } from "@/types/overrides"; + +import { debug } from "../../adapters/logger"; + +const tagsMap = new Map(); + +export default { + name: "fs-dev-nextMode", + mode: "nextMode", + getLastRevalidated: async (tags: string[]) => { + if (globalThis.openNextConfig.dangerous?.disableTagCache) { + return 0; + } + + let lastRevalidated = 0; + + tags.forEach((tag) => { + const tagTime = tagsMap.get(tag); + if (tagTime && tagTime > lastRevalidated) { + lastRevalidated = tagTime; + } + }); + + debug("getLastRevalidated result:", lastRevalidated); + return lastRevalidated; + }, + hasBeenRevalidated: async (tags: string[], lastModified?: number) => { + if (globalThis.openNextConfig.dangerous?.disableTagCache) { + return false; + } + + const hasRevalidatedTag = tags.some((tag) => { + const tagRevalidatedAt = tagsMap.get(tag); + return tagRevalidatedAt ? tagRevalidatedAt > (lastModified ?? 0) : false; + }); + + debug("hasBeenRevalidated result:", hasRevalidatedTag); + return hasRevalidatedTag; + }, + writeTags: async (tags: string[]) => { + if (globalThis.openNextConfig.dangerous?.disableTagCache || tags.length === 0) { + return; + } + + debug("writeTags", { tags: tags }); + + tags.forEach((tag) => { + tagsMap.set(tag, Date.now()); + }); + + debug("writeTags completed, written", tags.length, "tags"); + }, +} satisfies NextModeTagCache; diff --git a/packages/core/src/overrides/tagCache/fs-dev.ts b/packages/core/src/overrides/tagCache/fs-dev.ts new file mode 100644 index 00000000..932eb216 --- /dev/null +++ b/packages/core/src/overrides/tagCache/fs-dev.ts @@ -0,0 +1,56 @@ +import fs from "node:fs"; +import path from "node:path"; + +import type { TagCache } from "@/types/overrides"; +import { getMonorepoRelativePath } from "@/utils/normalize-path"; + +const tagFile = path.join(getMonorepoRelativePath(), "dynamodb-provider/dynamodb-cache.json"); +const tagContent = fs.readFileSync(tagFile, "utf-8"); + +let tags = JSON.parse(tagContent) as { + tag: { S: string }; + path: { S: string }; + revalidatedAt: { N: string }; +}[]; + +const { NEXT_BUILD_ID } = process.env; + +function buildKey(key: string) { + return path.posix.join(NEXT_BUILD_ID ?? "", key); +} + +const tagCache: TagCache = { + name: "fs-dev", + mode: "original", + getByPath: async (path: string) => { + return tags + .filter((tagPathMapping) => tagPathMapping.path.S === buildKey(path)) + .map((tag) => tag.tag.S.replace(`${NEXT_BUILD_ID}/`, "")); + }, + getByTag: async (tag: string) => { + return tags + .filter((tagPathMapping) => tagPathMapping.tag.S === buildKey(tag)) + .map((tagEntry) => tagEntry.path.S.replace(`${NEXT_BUILD_ID}/`, "")); + }, + getLastModified: async (path: string, lastModified?: number) => { + const revalidatedTags = tags.filter( + (tagPathMapping) => + tagPathMapping.path.S === buildKey(path) && + Number.parseInt(tagPathMapping.revalidatedAt.N) > (lastModified ?? 0) + ); + return revalidatedTags.length > 0 ? -1 : (lastModified ?? Date.now()); + }, + writeTags: async (newTags) => { + const newTagsSet = new Set(newTags.map(({ tag, path }) => `${buildKey(tag)}-${buildKey(path)}`)); + const unchangedTags = tags.filter(({ tag, path }) => !newTagsSet.has(`${tag.S}-${path.S}`)); + tags = unchangedTags.concat( + newTags.map((item) => ({ + tag: { S: buildKey(item.tag) }, + path: { S: buildKey(item.path) }, + revalidatedAt: { N: `${item.revalidatedAt ?? Date.now()}` }, + })) + ); + }, +}; + +export default tagCache; diff --git a/packages/core/src/overrides/warmer/dummy.ts b/packages/core/src/overrides/warmer/dummy.ts new file mode 100644 index 00000000..92453195 --- /dev/null +++ b/packages/core/src/overrides/warmer/dummy.ts @@ -0,0 +1,11 @@ +import type { Warmer } from "@/types/overrides"; +import { FatalError } from "@/utils/error"; + +const dummyWarmer: Warmer = { + name: "dummy", + invoke: async (_: string) => { + throw new FatalError("Dummy warmer is not implemented"); + }, +}; + +export default dummyWarmer; diff --git a/packages/core/src/overrides/wrappers/cloudflare-edge.ts b/packages/core/src/overrides/wrappers/cloudflare-edge.ts new file mode 100644 index 00000000..60f7e8c6 --- /dev/null +++ b/packages/core/src/overrides/wrappers/cloudflare-edge.ts @@ -0,0 +1,60 @@ +import type { InternalEvent, InternalResult, MiddlewareResult } from "@/types/open-next"; +import type { Wrapper, WrapperHandler } from "@/types/overrides"; + +const cfPropNameMapping: Record string, string]> = { + // The city name is percent-encoded. + // See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37 + city: [encodeURIComponent, "x-open-next-city"], + country: "x-open-next-country", + regionCode: "x-open-next-region", + latitude: "x-open-next-latitude", + longitude: "x-open-next-longitude", +}; + +interface WorkerContext { + waitUntil: (promise: Promise) => void; +} + +const handler: WrapperHandler = + async (handler, converter) => + async (...args: unknown[]): Promise => { + const [request, env, ctx] = args as [Request, Record, WorkerContext]; + globalThis.process = process; + + // Set the environment variables + // Cloudflare suggests to not override the process.env object but instead apply the values to it + for (const [key, value] of Object.entries(env)) { + if (typeof value === "string") { + process.env[key] = value; + } + } + + const internalEvent = await converter.convertFrom(request); + + // Retrieve geo information from the cloudflare request + // See https://developers.cloudflare.com/workers/runtime-apis/request + // Note: This code could be moved to a cloudflare specific converter when one is created. + const cfProperties = (request as Request & { cf?: Record }).cf; + for (const [propName, mapping] of Object.entries(cfPropNameMapping)) { + const propValue = cfProperties?.[propName]; + if (propValue != null) { + const [encode, headerName] = Array.isArray(mapping) ? mapping : [null, mapping]; + internalEvent.headers[headerName] = encode ? encode(propValue) : propValue; + } + } + + const response = await handler(internalEvent, { + waitUntil: ctx.waitUntil.bind(ctx), + }); + + const result = (await converter.convertTo(response)) as Response; + + return result; + }; + +export default { + wrapper: handler, + name: "cloudflare-edge", + supportStreaming: true, + edgeRuntime: true, +} satisfies Wrapper; diff --git a/packages/core/src/overrides/wrappers/cloudflare-node.ts b/packages/core/src/overrides/wrappers/cloudflare-node.ts new file mode 100644 index 00000000..4f90d895 --- /dev/null +++ b/packages/core/src/overrides/wrappers/cloudflare-node.ts @@ -0,0 +1,129 @@ +import { Writable } from "node:stream"; + +import type { InternalEvent, InternalResult, StreamCreator } from "@/types/open-next"; +import type { Wrapper, WrapperHandler } from "@/types/overrides"; + +// Response with null body status (101, 204, 205, or 304) cannot have a body. +const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]); + +const handler: WrapperHandler = + async (handler, converter) => + async (...args: unknown[]): Promise => { + const [request, env, ctx, abortSignal] = args as [ + Request, + Record, + { waitUntil: (promise: Promise) => void }, + AbortSignal, + ]; + globalThis.process = process; + // Set the environment variables + // Cloudflare suggests to not override the process.env object but instead apply the values to it + for (const [key, value] of Object.entries(env)) { + if (typeof value === "string") { + process.env[key] = value; + } + } + + const internalEvent = await converter.convertFrom(request); + const url = new URL(request.url); + + const { promise: promiseResponse, resolve: resolveResponse } = Promise.withResolvers(); + + const streamCreator: StreamCreator = { + writeHeaders(prelude: { + statusCode: number; + cookies: string[]; + headers: Record; + }): Writable { + const { statusCode, cookies, headers } = prelude; + + const responseHeaders = new Headers(headers); + for (const cookie of cookies) { + responseHeaders.append("Set-Cookie", cookie); + } + + // TODO(vicb): this is a workaround to make PPR work with `wrangler dev` + // See https://github.com/cloudflare/workers-sdk/issues/8004 + if (url.hostname === "localhost") { + responseHeaders.set("Content-Encoding", "identity"); + } + + // Optimize: skip ReadableStream creation for null body statuses + if (NULL_BODY_STATUSES.has(statusCode)) { + const response = new Response(null, { + status: statusCode, + headers: responseHeaders, + }); + resolveResponse(response); + + // Return a no-op Writable that discards all data + return new Writable({ + write(chunk, encoding, callback) { + callback(); + }, + }); + } + + let controller: ReadableStreamDefaultController; + const readable = new ReadableStream({ + start(c) { + controller = c; + }, + }); + + const response = new Response(readable, { + status: statusCode, + headers: responseHeaders, + }); + resolveResponse(response); + + return new Writable({ + write(chunk, encoding, callback) { + try { + controller.enqueue(chunk); + } catch (e: unknown) { + return callback(e instanceof Error ? e : new Error(String(e))); + } + callback(); + }, + final(callback) { + controller.close(); + callback(); + }, + destroy(error, callback) { + if (error) { + controller.error(error); + } else { + try { + controller.close(); + } catch { + // Ignore "This ReadableStream is closed" error + } + } + callback(error); + }, + }); + }, + // This is for passing along the original abort signal from the initial Request you retrieve in your worker + // Ensures that the response we pass to NextServer is aborted if the request is aborted + // By doing this `request.signal.onabort` will work in route handlers + abortSignal: abortSignal, + // There is no need to retain the chunks that were pushed to the response stream. + retainChunks: false, + }; + + ctx.waitUntil( + handler(internalEvent, { + streamCreator, + waitUntil: ctx.waitUntil.bind(ctx), + }) + ); + + return promiseResponse; + }; + +export default { + wrapper: handler, + name: "cloudflare-node", + supportStreaming: true, +} satisfies Wrapper; diff --git a/packages/core/src/overrides/wrappers/dummy.ts b/packages/core/src/overrides/wrappers/dummy.ts new file mode 100644 index 00000000..8649a8c8 --- /dev/null +++ b/packages/core/src/overrides/wrappers/dummy.ts @@ -0,0 +1,15 @@ +import type { InternalEvent } from "@/types/open-next"; +import type { OpenNextHandlerOptions, Wrapper, WrapperHandler } from "@/types/overrides"; + +const dummyWrapper: WrapperHandler = + async (handler, _converter) => + async (...args: unknown[]): Promise => { + const [event, options] = args as [InternalEvent, OpenNextHandlerOptions | undefined]; + return await handler(event, options); + }; + +export default { + name: "dummy", + wrapper: dummyWrapper, + supportStreaming: true, +} satisfies Wrapper; diff --git a/packages/core/src/overrides/wrappers/express-dev.ts b/packages/core/src/overrides/wrappers/express-dev.ts new file mode 100644 index 00000000..bcc9e8ef --- /dev/null +++ b/packages/core/src/overrides/wrappers/express-dev.ts @@ -0,0 +1,80 @@ +import path from "node:path"; + +import express from "express"; + +import { NextConfig } from "@/config/index"; +import type { StreamCreator } from "@/types/open-next.js"; +import type { WrapperHandler } from "@/types/overrides.js"; +import { getMonorepoRelativePath } from "@/utils/normalize-path"; + +const wrapper: WrapperHandler = async (handler, converter) => { + const app = express(); + // We disable this cause we wanna use it ourself + // https://stackoverflow.com/a/13055495/16587222 + app.disable("x-powered-by"); + // To serve static assets + const basePath = NextConfig.basePath ?? ""; + app.use(basePath, express.static(path.join(getMonorepoRelativePath(), "assets"))); + + const imageHandlerPath = path.join(getMonorepoRelativePath(), "image-optimization-function/index.mjs"); + + const imageHandler = await import(imageHandlerPath).then((m) => m.handler); + + app.all(`${NextConfig.basePath ?? ""}/_next/image`, async (req, res) => { + const internalEvent = await converter.convertFrom(req); + const streamCreator: StreamCreator = { + writeHeaders: (prelude) => { + res.writeHead(prelude.statusCode, prelude.headers); + return res; + }, + }; + await imageHandler(internalEvent, { streamCreator }); + }); + + app.all(/.*/, async (req, res) => { + if (req.protocol === "http" && req.hostname === "localhost") { + // This is used internally by Next.js during redirects in server actions. We need to set it to the origin of the request. + process.env.__NEXT_PRIVATE_ORIGIN = `${req.protocol}://${req.hostname}`; + // This is to make `next-auth` and other libraries that rely on this header to work locally out of the box. + req.headers["x-forwarded-proto"] = req.protocol; + } + const internalEvent = await converter.convertFrom(req); + + const abortController = new AbortController(); + + const streamCreator: StreamCreator = { + writeHeaders: (prelude) => { + res.setHeader("Set-Cookie", prelude.cookies); + res.writeHead(prelude.statusCode, prelude.headers); + res.flushHeaders(); + return res; + }, + onFinish: () => {}, + abortSignal: abortController.signal, + }; + + res.on("close", () => { + abortController.abort(); + }); + + await handler(internalEvent, { streamCreator }); + }); + + const server = app.listen(Number.parseInt(process.env.PORT ?? "3000", 10), () => { + console.log(`Server running on port ${process.env.PORT ?? 3000}`); + }); + + app.on("error", (err) => { + console.error("error", err); + }); + + return () => { + server.close(); + }; +}; + +export default { + wrapper, + name: "expresss-dev", + supportStreaming: true, +}; diff --git a/packages/core/src/overrides/wrappers/node.ts b/packages/core/src/overrides/wrappers/node.ts new file mode 100644 index 00000000..c1432bf6 --- /dev/null +++ b/packages/core/src/overrides/wrappers/node.ts @@ -0,0 +1,76 @@ +import { createServer } from "node:http"; + +import type { StreamCreator } from "@/types/open-next"; +import type { Wrapper, WrapperHandler } from "@/types/overrides"; + +import { debug, error } from "../../adapters/logger"; + +const wrapper: WrapperHandler = async (handler, converter) => { + const server = createServer(async (req, res) => { + const internalEvent = await converter.convertFrom(req); + + const abortController = new AbortController(); + + const streamCreator: StreamCreator = { + writeHeaders: (prelude) => { + res.setHeader("Set-Cookie", prelude.cookies); + res.writeHead(prelude.statusCode, prelude.headers); + res.flushHeaders(); + return res; + }, + abortSignal: abortController.signal, + }; + + res.on("close", () => { + abortController.abort(); + }); + + if (internalEvent.rawPath === "/__health") { + res.writeHead(200, { + "Content-Type": "text/plain", + }); + res.end("OK"); + } else { + await handler(internalEvent, { + streamCreator, + }); + } + }); + + await new Promise((resolve) => { + server.on("listening", () => { + const cleanup = (code: number) => { + debug("Closing server"); + server.close(() => { + debug("Server closed"); + process.exit(code); + }); + }; + console.log(`Listening on port ${process.env.PORT ?? "3000"}`); + debug(`Open Next version: ${process.env.OPEN_NEXT_VERSION}`); + + process.on("exit", (code) => cleanup(code)); + + process.on("SIGINT", () => cleanup(0)); + process.on("SIGTERM", () => cleanup(0)); + + resolve(); + }); + + server.listen(Number.parseInt(process.env.PORT ?? "3000", 10)); + }); + + server.on("error", (err) => { + error(err); + }); + + return () => { + server.close(); + }; +}; + +export default { + wrapper, + name: "node", + supportStreaming: true, +} satisfies Wrapper; diff --git a/packages/core/src/plugins/content-updater.ts b/packages/core/src/plugins/content-updater.ts new file mode 100644 index 00000000..06c55059 --- /dev/null +++ b/packages/core/src/plugins/content-updater.ts @@ -0,0 +1,97 @@ +/** + * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content. + * + * The updater allows multiple plugins to update the content. + */ + +import { readFile } from "node:fs/promises"; + +import type { OnLoadArgs, OnLoadOptions, Plugin, PluginBuild } from "esbuild"; + +import type { BuildOptions } from "../build/helper"; +import { type Versions, isVersionInRange } from "../build/patch/codePatcher.js"; + +export type * from "esbuild"; + +/** + * The callbacks returns either an updated content or undefined if the content is unchanged. + */ +export type Callback = (args: { + contents: string; + path: string; +}) => string | undefined | Promise; + +/** + * The callback is called only when `contentFilter` matches the content. + * It can be used as a fast heuristic to prevent an expensive update. + */ +export type OnUpdateOptions = OnLoadOptions & { + contentFilter: RegExp; +}; + +export type Updater = OnUpdateOptions & { + callback: Callback; + // Restrict the patch to this Next version range + versions?: Versions; +}; + +export class ContentUpdater { + updaters = new Map(); + + constructor(private buildOptions: BuildOptions) {} + + /** + * Register a callback to update the file content. + * + * The callbacks are called in order of registration. + * + * @param name The name of the plugin (must be unique). + * @param updaters A list of code updaters + * @returns A noop ESBuild plugin. + */ + updateContent(name: string, updaters: Updater[]): Plugin { + if (this.updaters.has(name)) { + throw new Error(`Plugin "${name}" already registered`); + } + this.updaters.set( + name, + updaters.filter(({ versions }) => isVersionInRange(this.buildOptions.nextVersion, versions)) + ); + return { + name, + setup() {}, + }; + } + + /** + * Returns an ESBuild plugin applying the registered updates. + */ + get plugin() { + return { + name: "aggregate-on-load", + + setup: async (build: PluginBuild) => { + build.onLoad({ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, async (args: OnLoadArgs) => { + const updaters = Array.from(this.updaters.values()).flat(); + if (updaters.length === 0) { + return; + } + let contents = await readFile(args.path, "utf-8"); + for (const { filter, namespace, contentFilter, callback } of updaters) { + if (namespace !== undefined && args.namespace !== namespace) { + continue; + } + if (!args.path.match(filter)) { + continue; + } + if (!contents.match(contentFilter)) { + continue; + } + contents = (await callback({ contents, path: args.path })) ?? contents; + } + return { contents }; + }); + }, + }; + } +} diff --git a/packages/core/src/plugins/edge.ts b/packages/core/src/plugins/edge.ts new file mode 100644 index 00000000..41dca4fb --- /dev/null +++ b/packages/core/src/plugins/edge.ts @@ -0,0 +1,205 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; + +import chalk from "chalk"; +import type { Plugin } from "esbuild"; + +import type { MiddlewareInfo } from "@/types/next-types.js"; + +import { + loadAppPathRoutesManifest, + loadAppPathsManifest, + loadAppPathsManifestKeys, + loadBuildId, + loadConfig, + loadConfigHeaders, + loadFunctionsConfigManifest, + loadHtmlPages, + loadMiddlewareManifest, + loadPagesManifest, + loadPrerenderManifest, + loadRoutesManifest, +} from "../adapters/config/util.js"; +import logger from "../logger.js"; +import { normalizePath } from "../utils/normalize-path.js"; +import { getCrossPlatformPathRegex } from "../utils/regex.js"; + +export interface IPluginSettings { + nextDir: string; + middlewareInfo?: MiddlewareInfo; + isInCloudflare?: boolean; +} + +/** + * @param opts.nextDir - The path to the .next directory + * @param opts.middlewareInfo - Information about the middleware + * @param opts.isInCloudflare - Whether the code runs on the cloudflare runtime + * @returns + */ +export function openNextEdgePlugins({ nextDir, middlewareInfo, isInCloudflare }: IPluginSettings): Plugin { + const entryFiles = + middlewareInfo?.files.map((file: string) => normalizePath(path.join(nextDir, file))) ?? []; + const routes = middlewareInfo + ? [ + { + name: middlewareInfo.name || "/", + page: middlewareInfo.page, + regex: middlewareInfo.matchers.map((m) => m.regexp), + }, + ] + : []; + const wasmFiles = middlewareInfo?.wasm ?? []; + + return { + name: "opennext-edge", + setup(build) { + logger.debug(chalk.blue("OpenNext Edge plugin")); + + build.onResolve({ filter: /\.(mjs|wasm)$/ }, () => { + return { + external: true, + }; + }); + + //Copied from https://github.com/cloudflare/next-on-pages/blob/7a18efb5cab4d86c8e3e222fc94ea88ac05baffd/packages/next-on-pages/src/buildApplication/processVercelFunctions/build.ts#L86-L112 + + build.onResolve({ filter: /^node:/ }, ({ kind, path }) => { + // this plugin converts `require("node:*")` calls, those are the only ones that + // need updating (esm imports to "node:*" are totally valid), so here we tag with the + // node-buffer namespace only imports that are require calls + return kind === "require-call" ? { path, namespace: "node-built-in-modules" } : undefined; + }); + + // we convert the imports we tagged with the node-built-in-modules namespace so that instead of `require("node:*")` + // they import from `export * from "node:*";` + build.onLoad({ filter: /.*/, namespace: "node-built-in-modules" }, ({ path }) => ({ + contents: `export * from '${path}'`, + loader: "js", + })); + + // We inject the entry files into the edgeFunctionHandler + build.onLoad({ filter: getCrossPlatformPathRegex("/edgeFunctionHandler.js") }, async (args) => { + let contents = readFileSync(args.path, "utf-8"); + contents = ` +globalThis._ENTRIES = {}; +globalThis.self = globalThis; +globalThis._ROUTES = ${JSON.stringify(routes)}; + +${ + isInCloudflare + ? "" + : ` +import {readFileSync} from "node:fs"; +import path from "node:path"; +function addDuplexToInit(init) { + return typeof init === 'undefined' || + (typeof init === 'object' && init.duplex === undefined) + ? { duplex: 'half', ...init } + : init +} +// We need to override Request to add duplex to the init, it seems Next expects it to work like this +class OverrideRequest extends Request { + constructor(input, init) { + super(input, addDuplexToInit(init)) + } +} +globalThis.Request = OverrideRequest; + +// If we're not in cloudflare, we polyfill crypto +// https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/crypto.js +import { webcrypto } from 'node:crypto' +if(!globalThis.crypto){ + globalThis.crypto = new webcrypto.Crypto() +} +if(!globalThis.CryptoKey){ + globalThis.CryptoKey = webcrypto.CryptoKey +} +function SubtleCrypto() { + if (!(this instanceof SubtleCrypto)) return new SubtleCrypto() + throw TypeError('Illegal constructor') +} +if(!globalThis.SubtleCrypto) { + globalThis.SubtleCrypto = SubtleCrypto +} +if(!globalThis.Crypto) { + globalThis.Crypto = webcrypto.Crypto +} +// We also need to polyfill URLPattern +if (!globalThis.URLPattern) { + await import("urlpattern-polyfill"); +} +` +} +${importWasm(wasmFiles, { isInCloudflare })} +${entryFiles.map((file) => `require("${file}");`).join("\n")} +${contents} + `; + + return { + contents, + }; + }); + + build.onLoad({ filter: getCrossPlatformPathRegex("adapters/config/index") }, async () => { + const NextConfig = loadConfig(nextDir); + const BuildId = loadBuildId(nextDir); + const HtmlPages = loadHtmlPages(nextDir); + const RoutesManifest = loadRoutesManifest(nextDir); + const ConfigHeaders = loadConfigHeaders(nextDir); + const PrerenderManifest = loadPrerenderManifest(nextDir); + const AppPathsManifestKeys = loadAppPathsManifestKeys(nextDir); + const MiddlewareManifest = loadMiddlewareManifest(nextDir); + const AppPathsManifest = loadAppPathsManifest(nextDir); + const AppPathRoutesManifest = loadAppPathRoutesManifest(nextDir); + const FunctionsConfigManifest = loadFunctionsConfigManifest(nextDir); + const PagesManifest = loadPagesManifest(nextDir); + + const contents = ` + import path from "node:path"; + + import { debug } from "../logger"; + + globalThis.__dirname ??= ""; + + export const NEXT_DIR = path.join(__dirname, ".next"); + export const OPEN_NEXT_DIR = path.join(__dirname, ".open-next"); + + debug({ NEXT_DIR, OPEN_NEXT_DIR }); + + export const NextConfig = ${JSON.stringify(NextConfig)}; + export const BuildId = ${JSON.stringify(BuildId)}; + export const HtmlPages = ${JSON.stringify(HtmlPages)}; + export const RoutesManifest = ${JSON.stringify(RoutesManifest)}; + export const ConfigHeaders = ${JSON.stringify(ConfigHeaders)}; + export const PrerenderManifest = ${JSON.stringify(PrerenderManifest)}; + export const AppPathsManifestKeys = ${JSON.stringify(AppPathsManifestKeys)}; + export const MiddlewareManifest = ${JSON.stringify(MiddlewareManifest)}; + export const AppPathsManifest = ${JSON.stringify(AppPathsManifest)}; + export const AppPathRoutesManifest = ${JSON.stringify(AppPathRoutesManifest)}; + export const FunctionsConfigManifest = ${JSON.stringify(FunctionsConfigManifest)}; + export const PagesManifest = ${JSON.stringify(PagesManifest)}; + + process.env.NEXT_BUILD_ID = BuildId; + process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId; +`; + return { contents }; + }); + }, + }; +} + +function importWasm(files: MiddlewareInfo["wasm"], { isInCloudflare }: { isInCloudflare?: boolean }) { + return files + .map(({ name }) => { + if (isInCloudflare) { + // As `.next/server/src/middleware.js` references the name, + // using `import ${name} from '...'` would cause ESBuild to rename the import. + // We use `globalThis.${name}` to make sure `middleware.js` reference name will match. + return `import __onw_${name}__ from './wasm/${name}.wasm' +globalThis.${name} = __onw_${name}__`; + } + + return `const ${name} = readFileSync(path.join(__dirname,'/wasm/${name}.wasm'));`; + }) + .join("\n"); +} diff --git a/packages/core/src/plugins/externalMiddleware.ts b/packages/core/src/plugins/externalMiddleware.ts new file mode 100644 index 00000000..9ad1b97a --- /dev/null +++ b/packages/core/src/plugins/externalMiddleware.ts @@ -0,0 +1,15 @@ +import type { Plugin } from "esbuild"; + +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +export function openNextExternalMiddlewarePlugin(functionPath: string): Plugin { + return { + name: "open-next-external-node-middleware", + setup(build) { + // If we bundle the routing, we need to resolve the middleware + build.onResolve({ filter: getCrossPlatformPathRegex("./middleware.mjs") }, () => ({ + path: functionPath, + })); + }, + }; +} diff --git a/packages/core/src/plugins/inline-require-resolve.ts b/packages/core/src/plugins/inline-require-resolve.ts new file mode 100644 index 00000000..10c56485 --- /dev/null +++ b/packages/core/src/plugins/inline-require-resolve.ts @@ -0,0 +1,33 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; + +import type { Plugin } from "esbuild"; + +/** + * Inlines calls to `require.resolve` in JavaScript files. + * + * esbuild does not statically analyse `require.resolve` calls, and the polyfill + * does not include an implementation to handle them. This can be problematic + * if you attempt to dynamically import a file built by esbuild that unknowingly + * contains `require.resolve` calls, as they will throw an error during import. + */ +export const inlineRequireResolvePlugin: Plugin = { + name: "inline-require-resolve", + setup: (build) => { + build.onLoad({ filter: /\.(js|ts|mjs|cjs)$/ }, async (args) => { + const source = await fs.promises.readFile(args.path, "utf-8"); + const transformed = source.replace( + /require\.resolve\((?['"])((?:\\.|.)*?)\k\)/g, + (_, quote, modulePath) => { + try { + return JSON.stringify(createRequire(args.path).resolve(modulePath)); + } catch { + return `require.resolve(${quote}${modulePath}${quote})`; + } + } + ); + + return { contents: transformed, loader: "default" }; + }); + }, +}; diff --git a/packages/core/src/plugins/inlineRouteHandlers.ts b/packages/core/src/plugins/inlineRouteHandlers.ts new file mode 100644 index 00000000..66ed67e1 --- /dev/null +++ b/packages/core/src/plugins/inlineRouteHandlers.ts @@ -0,0 +1,123 @@ +import { getCrossPlatformPathRegex } from "@/utils/regex.js"; + +import type { NextAdapterOutputs } from "@/types/adapter.js"; +import { patchCode } from "../build/patch/astCodePatcher.js"; + +import type { ContentUpdater, Plugin } from "./content-updater.js"; + +export function inlineRouteHandler( + updater: ContentUpdater, + outputs: NextAdapterOutputs, + packagePath: string +): Plugin { + console.log("## inlineRouteHandler"); + return updater.updateContent("inlineRouteHandler", [ + // This one will inline the route handlers into the adapterHandler's getHandler function. + { + filter: getCrossPlatformPathRegex(String.raw`core/routing/adapterHandler\.js$`, { + escape: false, + }), + contentFilter: /getHandler/, + callback: ({ contents }) => patchCode(contents, inlineRule(outputs)), + }, + // For turbopack, we need to also patch the `[turbopack]_runtime.js` file. + { + filter: getCrossPlatformPathRegex(String.raw`\[turbopack\]_runtime\.js$`, { + escape: false, + }), + contentFilter: /loadRuntimeChunkPath/, + callback: ({ contents }) => { + const result = patchCode(contents, inlineChunksRule); + //TODO: Maybe find another way to do that. + return `${result}\n${inlineChunksFn(outputs, packagePath)}`; + }, + }, + ]); +} + +function inlineRule(outputs: NextAdapterOutputs) { + const routeToHandlerPath: Record = {}; + + for (const type of ["pages", "pagesApi", "appPages", "appRoutes"] as const) { + for (const { pathname, filePath } of outputs[type]) { + routeToHandlerPath[pathname] = filePath; + } + } + + return ` +rule: + pattern: "function getHandler($ROUTE) { $$$BODY }" +fix: |- + function getHandler($ROUTE) { + switch($ROUTE.route) { +${Object.entries(routeToHandlerPath) + .map(([route, file]) => ` case "${route}": return require("${file}");`) + .join("\n")} + default: + throw new Error(\`Not found \${$ROUTE.route}\`); + } + + }`; +} + +//TODO: Make this one more resilient to code changes +const inlineChunksRule = ` +rule: + kind: call_expression + pattern: require(resolved) +fix: + requireChunk(chunkPath) +`; + +function getInlinableChunks(outputs: NextAdapterOutputs, packagePath: string, prefix?: string) { + const chunks = new Set(); + // TODO: handle middleware + for (const type of ["pages", "pagesApi", "appPages", "appRoutes"] as const) { + for (const { assets } of outputs[type]) { + for (let asset of Object.keys(assets)) { + if (asset.includes(".next/server/chunks/") && !asset.includes("[turbopack]_runtime.js")) { + asset = packagePath !== "" ? asset.replace(`${packagePath}/`, "") : asset; + chunks.add(prefix ? `${prefix}${asset}` : asset); + } + } + } + } + return chunks; +} + +function inlineChunksFn(outputs: NextAdapterOutputs, packagePath: string) { + // From the outputs, we extract every chunks + const chunks = getInlinableChunks(outputs, packagePath); + return ` + function requireChunk(chunk) { + const chunkPath = ".next/" + chunk; + switch(chunkPath) { +${Array.from(chunks) + .map((chunk) => ` case "${chunk}": return require("./${chunk}");`) + .join("\n")} + default: + throw new Error(\`Not found \${chunkPath}\`); + } + } +`; +} + +/** + * Esbuild plugin to mark all chunks that we inline as external. + */ +export function externalChunksPlugin(outputs: NextAdapterOutputs, packagePath: string): Plugin { + const chunks = getInlinableChunks(outputs, packagePath, "./"); + return { + name: "external-chunks", + setup(build) { + build.onResolve({ filter: /\/chunks\// }, (args) => { + if (chunks.has(args.path)) { + return { + path: args.path, + external: true, + }; + } + }); + }, + }; +} diff --git a/packages/core/src/plugins/replacement.ts b/packages/core/src/plugins/replacement.ts new file mode 100644 index 00000000..79b01543 --- /dev/null +++ b/packages/core/src/plugins/replacement.ts @@ -0,0 +1,110 @@ +import { readFile } from "node:fs/promises"; + +import chalk from "chalk"; +import type { Plugin } from "esbuild"; + +import logger from "../logger.js"; + +export interface IPluginSettings { + target: RegExp; + replacements?: string[]; + deletes?: string[]; + name?: string; + entireFile?: boolean; +} + +const overridePattern = /\/\/#override (\w+)\n([\s\S]*?)\n\/\/#endOverride/gm; +const importPattern = /\/\/#import([\s\S]*?)\n\/\/#endImport/gm; + +/** + * + * openNextPlugin({ + * target: /plugins(\/|\\)default\.js/g, + * replacements: [require.resolve("./plugins/default.js")], + * deletes: ["id1"], + * }) + * + * To inject arbitrary code by using (import at top of file): + * + * //#import + * + * import data from 'data' + * const datum = data.datum + * + * //#endImport + * + * To replace code: + * + * //#override id1 + * + * export function overrideMe() { + * // I will replace the "id1" block in the target file + * } + * + * //#endOverride + * + * + * @param opts.target - the target file to replace + * @param opts.replacements - list of files used to replace the imports/overrides in the target + * - the path is absolute + * @param opts.deletes - list of ids to delete from the target + * @param opts.entireFile - whether to replace the entire file or just specific blocks. + * @param opts.name - name of the plugin + * @returns + */ +export function openNextReplacementPlugin({ + target, + replacements, + deletes, + name, + entireFile, +}: IPluginSettings): Plugin { + return { + name: name ?? "opennext", + setup(build) { + build.onLoad({ filter: target }, async (args) => { + if (entireFile) { + if (replacements?.length !== 1) { + throw new Error("When using entireFile option, exactly one replacement file must be provided"); + } + const contents = await readFile(replacements[0], "utf-8"); + return { contents }; + } + + let contents = await readFile(args.path, "utf-8"); + + await Promise.all([ + ...(deletes ?? []).map(async (id) => { + const pattern = new RegExp(`//#override (${id})\n([\\s\\S]*?)//#endOverride`); + logger.debug(chalk.blue(`OpenNext Replacement plugin ${name}`), `Delete override ${id}`); + contents = contents.replace(pattern, ""); + }), + ...(replacements ?? []).map(async (filename) => { + const replacementFile = await readFile(filename, "utf-8"); + const matches = replacementFile.matchAll(overridePattern); + + const importMatch = replacementFile.match(importPattern); + const addedImport = importMatch ? importMatch[0] : ""; + + contents = `${addedImport}\n${contents}`; + + for (const match of matches) { + const replacement = match[2]; + const id = match[1]; + const pattern = new RegExp(`//#override (${id})\n([\\s\\S]*?)//#endOverride`, "g"); + logger.debug( + chalk.blue(`Open-next replacement plugin ${name}`), + `Apply override ${id} from ${filename}` + ); + contents = contents.replace(pattern, replacement); + } + }), + ]); + + return { + contents, + }; + }); + }, + }; +} diff --git a/packages/core/src/plugins/resolve.ts b/packages/core/src/plugins/resolve.ts new file mode 100644 index 00000000..717836c8 --- /dev/null +++ b/packages/core/src/plugins/resolve.ts @@ -0,0 +1,107 @@ +import { readFile } from "node:fs/promises"; + +import chalk from "chalk"; +import type { Plugin } from "esbuild"; + +import type { + BaseOverride, + DefaultOverrideOptions, + IncludedImageLoader, + IncludedOriginResolver, + IncludedWarmer, + LazyLoadedOverride, + OverrideOptions, +} from "@/types/open-next"; +import type { ImageLoader, OriginResolver, Warmer } from "@/types/overrides"; + +import logger from "../logger.js"; +import { getCrossPlatformPathRegex } from "../utils/regex.js"; + +export interface IPluginSettings { + overrides?: { + // oxlint-disable-next-line @typescript-eslint/no-explicit-any - generic overrides for flexibility + wrapper?: DefaultOverrideOptions["wrapper"]; + // oxlint-disable-next-line @typescript-eslint/no-explicit-any - generic overrides for flexibility + converter?: DefaultOverrideOptions["converter"]; + tagCache?: OverrideOptions["tagCache"]; + queue?: OverrideOptions["queue"]; + incrementalCache?: OverrideOptions["incrementalCache"]; + imageLoader?: LazyLoadedOverride | IncludedImageLoader; + originResolver?: LazyLoadedOverride | IncludedOriginResolver; + warmer?: LazyLoadedOverride | IncludedWarmer; + proxyExternalRequest?: OverrideOptions["proxyExternalRequest"]; + cdnInvalidation?: OverrideOptions["cdnInvalidation"]; + }; + fnName?: string; +} + +function getOverrideOrDummy>(override: Override) { + if (typeof override === "string") { + return override; + } + // We can return dummy here because if it's not a string, it's a LazyLoadedOverride + return "dummy"; +} + +// This could be useful in the future to map overrides to nested folders +const nameToFolder = { + wrapper: "wrappers", + converter: "converters", + tagCache: "tagCache", + queue: "queue", + incrementalCache: "incrementalCache", + imageLoader: "imageLoader", + originResolver: "originResolver", + warmer: "warmer", + proxyExternalRequest: "proxyExternalRequest", + cdnInvalidation: "cdnInvalidation", +}; + +const defaultOverrides = { + wrapper: "aws-lambda", + converter: "aws-apigw-v2", + tagCache: "dynamodb", + queue: "sqs", + incrementalCache: "s3", + imageLoader: "s3", + originResolver: "pattern-env", + warmer: "aws-lambda", + proxyExternalRequest: "node", + cdnInvalidation: "dummy", +}; + +/** + * @param opts.overrides - The name of the overrides to use + * @returns + */ +export function openNextResolvePlugin({ overrides, fnName }: IPluginSettings): Plugin { + return { + name: "opennext-resolve", + setup(build) { + logger.debug(chalk.blue("OpenNext Resolve plugin"), fnName ? `for ${fnName}` : ""); + build.onLoad({ filter: getCrossPlatformPathRegex("core/resolve.js") }, async (args) => { + let contents = await readFile(args.path, "utf-8"); + const overridesEntries = Object.entries(overrides ?? {}); + for (let [overrideName, overrideValue] of overridesEntries) { + if (!overrideValue) { + continue; + } + if (overrideName === "wrapper" && overrideValue === "cloudflare") { + // "cloudflare" is deprecated and replaced by "cloudflare-edge". + overrideValue = "cloudflare-edge"; + } + const folder = nameToFolder[overrideName as keyof typeof nameToFolder]; + const defaultOverride = defaultOverrides[overrideName as keyof typeof defaultOverrides]; + + contents = contents.replace( + `../overrides/${folder}/${defaultOverride}.js`, + `../overrides/${folder}/${getOverrideOrDummy(overrideValue)}.js` + ); + } + return { + contents, + }; + }); + }, + }; +} diff --git a/packages/core/src/types/adapter.ts b/packages/core/src/types/adapter.ts new file mode 100644 index 00000000..1974e42e --- /dev/null +++ b/packages/core/src/types/adapter.ts @@ -0,0 +1,17 @@ +export type NextAdapterOutput = { + pathname: string; + filePath: string; + assets: Record; +}; + +export type NextAdapterOutputs = { + pages: NextAdapterOutput[]; + pagesApi: NextAdapterOutput[]; + appPages: NextAdapterOutput[]; + appRoutes: NextAdapterOutput[]; + middleware?: NextAdapterOutput; +}; + +export type PublicFiles = { + files: string[]; +}; diff --git a/packages/core/src/types/cache.ts b/packages/core/src/types/cache.ts new file mode 100644 index 00000000..db5b63f1 --- /dev/null +++ b/packages/core/src/types/cache.ts @@ -0,0 +1,175 @@ +import type { ReadableStream } from "node:stream/web"; + +interface CachedFetchValue { + kind: "FETCH"; + data: { + headers: { [k: string]: string }; + body: string; + url: string; + status?: number; + tags?: string[]; + }; + revalidate: number; +} + +interface CachedRedirectValue { + kind: "REDIRECT"; + props: object; +} + +interface CachedRouteValue { + kind: "ROUTE" | "APP_ROUTE"; + // this needs to be a RenderResult so since renderResponse + // expects that type instead of a string + body: Buffer; + status: number; + headers: Record; +} + +interface CachedImageValue { + kind: "IMAGE"; + etag: string; + buffer: Buffer; + extension: string; + isMiss?: boolean; + isStale?: boolean; +} + +interface IncrementalCachedPageValue { + kind: "PAGE" | "PAGES"; + // this needs to be a string since the cache expects to store + // the string value + html: string; + pageData: object; + status?: number; + headers?: Record; +} + +interface IncrementalCachedAppPageValue { + kind: "APP_PAGE"; + // this needs to be a string since the cache expects to store + // the string value + html: string; + rscData: Buffer; + headers?: Record; + postponed?: string; + status?: number; + segmentData?: Map; +} + +export type IncrementalCacheValue = + | CachedRedirectValue + | IncrementalCachedPageValue + | IncrementalCachedAppPageValue + | CachedImageValue + | CachedFetchValue + | CachedRouteValue; + +export interface CacheHandlerContext { + fs?: never; + dev?: boolean; + flushToDisk?: boolean; + serverDistDir?: string; + maxMemoryCacheSize?: number; + _appDir: boolean; + _requestHeaders: never; + fetchCacheKeyPrefix?: string; +} +export interface CacheHandlerValue { + lastModified?: number; + age?: number; + cacheState?: string; + value: IncrementalCacheValue | null; +} + +export type Extension = "cache" | "fetch" | "composable"; + +type MetaHeaders = { + "x-next-cache-tags"?: string; + [k: string]: string | string[] | undefined; +}; + +export interface Meta { + status?: number; + headers?: MetaHeaders; + postponed?: string; +} + +export type TagCacheMetaFile = { + tag: { S: string }; + path: { S: string }; + revalidatedAt: { N: string }; +}; + +// Cache context since vercel/next.js#76207 +interface SetIncrementalFetchCacheContext { + fetchCache: true; + fetchUrl?: string; + fetchIdx?: number; + tags?: string[]; +} + +interface SetIncrementalResponseCacheContext { + fetchCache?: false; + cacheControl?: { + revalidate: number | false; + expire?: number; + }; + + /** + * True if the route is enabled for PPR. + */ + isRoutePPREnabled?: boolean; + + /** + * True if this is a fallback request. + */ + isFallback?: boolean; +} + +// Before vercel/next.js#76207 revalidate was passed this way +interface SetIncrementalCacheContext { + revalidate?: number | false; + isRoutePPREnabled?: boolean; + isFallback?: boolean; +} + +// Before vercel/next.js#53321 context on set was just the revalidate +type OldSetIncrementalCacheContext = number | false | undefined; + +export type IncrementalCacheContext = + | OldSetIncrementalCacheContext + | SetIncrementalCacheContext + | SetIncrementalFetchCacheContext + | SetIncrementalResponseCacheContext; + +export interface ComposableCacheEntry { + value: ReadableStream; + tags: string[]; + stale: number; + timestamp: number; + expire: number; + revalidate: number; +} + +export type StoredComposableCacheEntry = Omit & { + value: string; +}; + +export interface ComposableCacheHandler { + get(cacheKey: string): Promise; + set(cacheKey: string, pendingEntry: Promise): Promise; + refreshTags(): Promise; + /** + * Next 16 takes an array of tags instead of variadic arguments + */ + getExpiration(...tags: string[] | string[][]): Promise; + /** + * Removed from Next.js 16 + */ + expireTags(...tags: string[]): Promise; + /** + * This function is only there for older versions and do nothing + */ + receiveExpiredTags(...tags: string[]): Promise; +} diff --git a/packages/core/src/types/global.ts b/packages/core/src/types/global.ts new file mode 100644 index 00000000..289fe144 --- /dev/null +++ b/packages/core/src/types/global.ts @@ -0,0 +1,234 @@ +import type { AsyncLocalStorage, AsyncLocalStorage as NodeAsyncLocalStorage } from "node:async_hooks"; +import type { OutgoingHttpHeaders } from "node:http"; + +import type { + AssetResolver, + CDNInvalidationHandler, + IncrementalCache, + ProxyExternalRequest, + Queue, + TagCache, +} from "@/types/overrides"; + +import type { DetachedPromiseRunner } from "../utils/promise"; + +import type { i18nConfig } from "./next-types.js"; +import type { OpenNextConfig, WaitUntil } from "./open-next"; + +export interface RequestData { + geo?: { + city?: string; + country?: string; + region?: string; + latitude?: string; + longitude?: string; + }; + headers: OutgoingHttpHeaders; + ip?: string; + method: string; + nextConfig?: { + basePath?: string; + i18n?: i18nConfig; + trailingSlash?: boolean; + }; + page?: { + name?: string; + params?: { [key: string]: string | string[] }; + }; + url: string; + body?: ReadableStream; + signal: AbortSignal; +} + +interface Entry { + default: (props: { page: string; request: RequestData }) => Promise<{ + response: Response; + waitUntil: Promise; + }>; +} + +interface Entries { + [k: string]: Entry | Promise; +} + +export interface EdgeRoute { + name: string; + page: string; + regex: string[]; +} + +interface OpenNextRequestContext { + // Unique ID for the request. + requestId: string; + pendingPromiseRunner: DetachedPromiseRunner; + isISRRevalidation?: boolean; + mergeHeadersPriority?: "middleware" | "handler"; + // Last modified time of the page (used in main functions, only available for ISR/SSG). + lastModified?: number; + waitUntil?: WaitUntil; + /** We use this to deduplicate write of the tags*/ + writtenTags: Set; +} + +declare global { + // Needed in the cache adapter + /** + * The cache adapter for incremental static regeneration. + * Only available in main functions and in the external middleware when `enableCacheInterception` is `true`. + * Defined in `createMainHandler` and in `adapters/middleware.ts`. + */ + var incrementalCache: IncrementalCache; + + /** + * The cache adapter for the tag cache. + * Only available in main functions and in the external middleware when `enableCacheInterception` is `true`. + * Defined in `createMainHandler` and in `adapters/middleware.ts`. + */ + var tagCache: TagCache; + + /** + * The queue that is used to handle ISR revalidation requests. + * Only available in main functions and in the external middleware when `enableCacheInterception` is `true`. + * Defined in `createMainHandler` and in `adapters/middleware.ts`. + */ + var queue: Queue; + + /** + * A boolean that indicates if the DynamoDB cache is disabled. + * @deprecated This will be removed, use `globalThis.openNextConfig.dangerous?.disableTagCache` instead. + * Defined in esbuild banner for the cache adapter. + */ + var disableDynamoDBCache: boolean; + + /** + * A boolean that indicates if the incremental cache is disabled. + * @deprecated This will be removed, use `globalThis.openNextConfig.dangerous?.disableIncrementalCache` instead. + * Defined in esbuild banner for the cache adapter. + */ + var disableIncrementalCache: boolean; + + /** + * A boolean that indicates if the runtime is Edge. + * Only available in `edge` runtime functions (i.e. external middleware or function with edge runtime). + * Defined in `adapters/edge-adapter.ts`. + */ + var isEdgeRuntime: true; + + /** + * A boolean that indicates if we are running in debug mode. + * Available in all functions. + * Defined in the esbuild banner. + */ + var openNextDebug: boolean; + + /** + * The fetch function that should be used to make requests during the execution of the function. + * Used to bypass Next intercepting and caching the fetch calls. Only available in main functions. + * Defined in `adapters/server-adapter.ts` and in `adapters/middleware.ts`. + */ + var internalFetch: typeof fetch; + + /** + * The Open Next configuration object. + * Available in all functions. + * Defined in `createMainHandler` and in the `createGenericHandler`. + */ + var openNextConfig: Partial; + + /** + * The name of the function that is currently being executed. + * Only available in main functions. + * Defined in `createMainHandler`. + */ + var fnName: string | undefined; + /** + * The unique identifier of the server. + * Only available in main functions. + * Defined in `createMainHandler`. + */ + var serverId: string; + + /** + * The AsyncLocalStorage instance that is used to store the request context. + * Only available in main, middleware and edge functions. + * Defined in `requestHandler.ts`, `adapters/middleware.ts` and `adapters/edge-adapter.ts`. + */ + var __openNextAls: AsyncLocalStorage; + + /** + * The entries object that contains the functions that are available in the function. + * Only available in edge runtime functions. + * Defined in the esbuild edge plugin. + */ + var _ENTRIES: Entries; + + /** + * The routes object that contains the routes that are available in the function. + * Only available in edge runtime functions. + * Defined in the esbuild edge plugin. + */ + var _ROUTES: EdgeRoute[]; + + /** + * A map that is used in the edge runtime. + * Only available in edge runtime functions. + */ + var __storage__: Map; + + /** + * AsyncContext available globally in the edge runtime. + * Only available in edge runtime functions. + */ + var AsyncContext: unknown; + + /** + * AsyncLocalStorage available globally in the edge runtime. + * Only available in edge runtime functions. + * Defined in createEdgeBundle. + */ + var AsyncLocalStorage: typeof NodeAsyncLocalStorage; + + /** + * The version of the Open Next runtime. + * Available everywhere. + * Defined in the esbuild banner. + */ + var openNextVersion: string; + + /** + * The function that is used when resolving external rewrite requests. + * Only available in main functions + * Defined in `createMainHandler`. + */ + var proxyExternalRequest: ProxyExternalRequest; + + /** + * The function that will be called when the CDN needs invalidating (either from `revalidateTag` or from `res.revalidate`) + * Available in main functions + * Defined in `createMainHandler` + */ + var cdnInvalidationHandler: CDNInvalidationHandler; + + /** + * The function called to resolve assets. + * Available in main functions + * Defined in `createMainHandler` when the middleware is internal + */ + var assetResolver: AssetResolver | undefined; + + /** + * A function to preload the routes. + * This needs to be defined on globalThis because it can be used by custom overrides. + * Only available in main functions. + * TODO: Disabled for now, we'll need to revisit this later if needed. + */ + // var __next_route_preloader: ( + // stage: "waitUntil" | "start" | "warmerEvent" | "onDemand", + // ) => Promise; + + /** + * This is the relative package path of the monorepo. It will be an empty string "" in normal repos. + * ex. `packages/web` + */ + var monorepoPackagePath: string; +} diff --git a/packages/core/src/types/next-types.ts b/packages/core/src/types/next-types.ts new file mode 100644 index 00000000..3815707b --- /dev/null +++ b/packages/core/src/types/next-types.ts @@ -0,0 +1,211 @@ +// NOTE: add more next config typings as they become relevant + +import type { IncomingMessage, OpenNextNodeResponse } from "@/http/index.js"; + +import type { InternalEvent } from "./open-next"; + +type RemotePattern = { + protocol?: "http" | "https"; + hostname: string; + port?: string; + pathname?: string; +}; +declare type ImageFormat = "image/avif" | "image/webp"; + +type ImageConfigComplete = { + deviceSizes: number[]; + imageSizes: number[]; + path: string; + loaderFile: string; + domains: string[]; + disableStaticImages: boolean; + minimumCacheTTL: number; + formats: ImageFormat[]; + dangerouslyAllowSVG: boolean; + contentSecurityPolicy: string; + contentDispositionType: "inline" | "attachment"; + remotePatterns: RemotePattern[]; + unoptimized: boolean; +}; +type ImageConfig = Partial; + +export type RouteHas = + | { + type: "header" | "query" | "cookie"; + key: string; + value?: string; + } + | { + type: "host"; + key?: undefined; + value: string; + }; +export type Rewrite = { + source: string; + destination: string; + basePath?: false; + locale?: false; + has?: RouteHas[]; + missing?: RouteHas[]; +}; +export type Header = { + source: string; + regex: string; + basePath?: false; + locale?: false; + headers: Array<{ + key: string; + value: string; + }>; + has?: RouteHas[]; + missing?: RouteHas[]; +}; + +export interface DomainLocale { + defaultLocale: string; + domain: string; + http?: true; + locales: readonly string[]; +} + +export interface i18nConfig { + locales: string[]; + defaultLocale: string; + domains?: DomainLocale[]; + localeDetection?: false; +} +export interface NextConfig { + basePath?: string; + trailingSlash?: boolean; + skipTrailingSlashRedirect?: boolean; + i18n?: i18nConfig; + experimental: { + serverActions?: boolean; + appDir?: boolean; + optimizeCss?: boolean; + }; + images: ImageConfig; + poweredByHeader?: boolean; + serverExternalPackages?: string[]; + deploymentId?: string; +} + +export interface RouteDefinition { + page: string; + regex: string; +} + +export interface DataRouteDefinition { + page: string; + dataRouteRegex: string; + routeKeys?: string; +} + +export interface RewriteDefinition { + source: string; + destination: string; + has?: RouteHas[]; + missing?: RouteHas[]; + regex: string; + locale?: false; +} + +export interface RedirectDefinition extends RewriteDefinition { + internal?: boolean; + statusCode?: number; +} + +export interface RoutesManifest { + basePath?: string; + dynamicRoutes: RouteDefinition[]; + staticRoutes: RouteDefinition[]; + dataRoutes: DataRouteDefinition[]; + rewrites: + | { + beforeFiles: RewriteDefinition[]; + afterFiles: RewriteDefinition[]; + fallback: RewriteDefinition[]; + } + | RewriteDefinition[]; + redirects: RedirectDefinition[]; + headers?: Header[]; + i18n?: { + locales: string[]; + }; +} + +export interface MiddlewareInfo { + files: string[]; + paths?: string[]; + name: string; + page: string; + matchers: { + regexp: string; + originalSource: string; + }[]; + wasm: { + filePath: string; + name: string; + }[]; + assets: { + filePath: string; + name: string; + }[]; +} + +export interface MiddlewareManifest { + sortedMiddleware: string[]; + middleware: { + [key: string]: MiddlewareInfo; + }; + functions: { [key: string]: MiddlewareInfo }; + version: number; +} + +export interface PrerenderManifest { + routes: Record< + string, + { + // TODO: add the rest when needed for PPR + initialRevalidateSeconds: number | false; + } + >; + dynamicRoutes: { + [route: string]: { + routeRegex: string; + fallback: string | false | null; + dataRouteRegex: string; + }; + }; + preview: { + previewModeId: string; + }; +} + +export type Options = { + internalEvent: InternalEvent; + buildId: string; + isExternalRewrite?: boolean; +}; +export type PluginHandler = ( + req: IncomingMessage, + res: OpenNextNodeResponse, + options: Options +) => Promise; + +export interface FunctionsConfigManifest { + version: number; + functions: Record< + string, + { + maxDuration?: number | undefined; + runtime?: "nodejs"; + matchers?: Array<{ + regexp: string; + originalSource: string; + has?: Rewrite["has"]; + missing?: Rewrite["has"]; + }>; + } + >; +} diff --git a/packages/core/src/types/open-next.ts b/packages/core/src/types/open-next.ts new file mode 100644 index 00000000..8216e491 --- /dev/null +++ b/packages/core/src/types/open-next.ts @@ -0,0 +1,524 @@ +import type { Writable } from "node:stream"; +import type { ReadableStream } from "node:stream/web"; + +import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function"; + +import type { + AssetResolver, + CDNInvalidationHandler, + Converter, + ImageLoader, + IncrementalCache, + OriginResolver, + ProxyExternalRequest, + Queue, + TagCache, + Warmer, + Wrapper, +} from "./overrides"; + +export type BaseEventOrResult = { + type: T; +}; + +export type InternalEvent = { + readonly method: string; + readonly rawPath: string; + // Full URL - starts with "https://on/" when the host is not available + readonly url: string; + readonly body?: Buffer; + //TODO: change the type of headers to Record + readonly headers: Record; + readonly query: Record; + readonly cookies: Record; + readonly remoteAddress: string; +} & BaseEventOrResult<"core">; + +export type MiddlewareEvent = InternalEvent & { + responseHeaders?: Record; + isExternalRewrite?: boolean; + rewriteStatusCode?: number; +}; + +export type InternalResult = { + statusCode: number; + headers: Record; + body: ReadableStream; + isBase64Encoded: boolean; + rewriteStatusCode?: number; +} & BaseEventOrResult<"core">; + +/** + * This event is returned by the cache interceptor and the routing handler. + * It is then handled by either the external middleware or the classic request handler. + * This is designed for PPR support inside the cache interceptor. + */ +export type PartialResult = { + /** + * Resume request that will be forwarded to the handler + */ + resumeRequest: InternalEvent; + /** + * The result that was generated so far by the cache interceptor + * It contains the first part of the body that we'll need to forward to the client immediately + * As well as the headers and status code + */ + result: InternalResult; +}; + +export interface StreamCreator { + writeHeaders(prelude: { statusCode: number; cookies: string[]; headers: Record }): Writable; + // Just to fix an issue with aws lambda streaming with empty body + onWrite?: () => void; + onFinish?: (length: number) => void; + abortSignal?: AbortSignal; + /** + * Normally there is no need to retain the chunks that have been pushed to the response stream. + * + * However some implementations use a fake `StreamCreator` and expect the chunks to be retained. + * When your stream controller implementation doesn't need to retain the chunk, you can set this + * to `false` to reduce memory usage. + * + * @see https://github.com/opennextjs/opennextjs-aws/blob/main/packages/open-next/src/overrides/wrappers/aws-lambda.ts + * + * @default true for backward compatibility. + */ + retainChunks?: boolean; +} + +export type WaitUntil = (promise: Promise) => void; +export interface DangerousOptions { + /** + * The tag cache is used for revalidateTags and revalidatePath. + * @default false + */ + disableTagCache?: boolean; + /** + * The incremental cache is used for ISR and SSG. + * Disable this only if you use only SSR + * @default false + */ + disableIncrementalCache?: boolean; + /** + * Function to determine which headers or cookies takes precedence. + * By default, the middleware headers and cookies will override the handler headers and cookies. + * This is executed for every request and after next config headers and middleware has executed. + */ + headersAndCookiesPriority?: (event: InternalEvent) => "middleware" | "handler"; + + /** + * Configuration option to prioritize headers set via middleware over headers set via the option in the Next config. + * + * The default will change to 'true' in v4. + * + * See also {@link https://nextjs.org/docs/app/api-reference/file-conventions/middleware#execution-order} + * + * @default false + */ + middlewareHeadersOverrideNextConfigHeaders?: boolean; +} + +export type BaseOverride = { + name: string; +}; +export type LazyLoadedOverride = () => T | Promise; + +export interface Origin { + host: string; + protocol: "http" | "https"; + port?: number; + customHeaders?: Record; +} + +export type IncludedWrapper = + | "aws-lambda" + | "aws-lambda-streaming" + | "aws-lambda-compressed" + | "node" + // @deprecated - use "cloudflare-edge" instead. + | "cloudflare" + | "cloudflare-edge" + | "cloudflare-node" + | "express-dev" + | "dummy"; + +export type IncludedConverter = + | "aws-apigw-v2" + | "aws-apigw-v1" + | "aws-cloudfront" + | "edge" + | "node" + | "sqs-revalidate" + | "dummy"; + +export type RouteType = "route" | "page" | "app"; + +export interface ResolvedRoute { + route: string; + type: RouteType; + /** + * Indicates if the route is a prerendered dynamic fallback route. + * They shouldn't be used to serve the request directly. + */ + isFallback: boolean; +} + +/** + * The route preloading behavior. Only supported in Next 15+. + * Default behavior of Next is disabled. You should do your own testing to choose which one suits you best + * - "none" - No preloading of the route at all. This is the default + * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none". At the moment only cloudflare wrappers provide a `waitUntil` + * - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now + * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation. The handler will only be created after all the routes have been loaded, it may increase the cold start time by a lot in some cases. Useful for long running server or in serverless with some careful testing + * @default "none" + */ +export type RoutePreloadingBehavior = "none" | "withWaitUntil" | "onWarmerEvent" | "onStart"; + +export interface RoutingResult { + internalEvent: InternalEvent; + // If the request is an external rewrite, if used with an external middleware will be false on every server function + isExternalRewrite: boolean; + // Origin is only used in external middleware, will be false on every server function + origin: Origin | false; + // If the request is for an ISR route, will be false on every server function. Only used in external middleware + isISR: boolean; + // The initial URL of the request before applying rewrites, if used with an external middleware will be defined in x-opennext-initial-url header + initialURL: string; + + // The locale of the request, if used with an external middleware will be defined in x-opennext-locale header + locale?: string; + + // The resolved route after applying rewrites, if used with an external middleware will be defined in x-opennext-resolved-routes header as a json encoded array + resolvedRoutes: ResolvedRoute[]; + // The status code applied to a middleware rewrite + rewriteStatusCode?: number; + + /** + * This is the response generated when using PPR in the cache interceptor. + * It contains the initial part of the response that should be sent to the client immediately. + * Can only be present when using cache interception and no external middleware. + */ + initialResponse?: InternalResult; +} + +export interface MiddlewareResult extends RoutingResult, BaseEventOrResult<"middleware"> {} + +export type IncludedQueue = "sqs" | "sqs-lite" | "direct" | "dummy"; + +export type IncludedIncrementalCache = "s3" | "s3-lite" | "multi-tier-ddb-s3" | "fs-dev" | "dummy"; + +export type IncludedTagCache = + | "dynamodb" + | "dynamodb-lite" + | "dynamodb-nextMode" + | "fs-dev" + | "fs-dev-nextMode" + | "dummy"; + +export type IncludedImageLoader = "s3" | "s3-lite" | "host" | "fs-dev" | "dummy"; + +export type IncludedOriginResolver = "pattern-env" | "dummy"; + +export type IncludedWarmer = "aws-lambda" | "dummy"; + +export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy"; + +export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy"; + +export type IncludedAssetResolver = "dummy"; + +export interface DefaultOverrideOptions< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +> { + /** + * This is the main entrypoint of your app. + * @default "aws-lambda" + */ + wrapper?: IncludedWrapper | LazyLoadedOverride>; + + /** + * This code convert the event to InternalEvent and InternalResult to the expected output. + * @default "aws-apigw-v2" + */ + converter?: IncludedConverter | LazyLoadedOverride>; + /** + * Generate a basic dockerfile to deploy the app. + * If a string is provided, it will be used as the base dockerfile. + * @default false + */ + generateDockerfile?: boolean | string; +} + +export interface OverrideOptions extends DefaultOverrideOptions { + /** + * Add possibility to override the default s3 cache. Used for fetch cache and html/rsc/json cache. + * @default "s3" + */ + incrementalCache?: IncludedIncrementalCache | LazyLoadedOverride; + + /** + * Add possibility to override the default tag cache. Used for revalidateTags and revalidatePath. + * @default "dynamodb" + */ + tagCache?: IncludedTagCache | LazyLoadedOverride; + + /** + * Add possibility to override the default queue. Used for isr. + * @default "sqs" + */ + queue?: IncludedQueue | LazyLoadedOverride; + + /** + * Add possibility to override the default proxy for external rewrite + * @default "node" + */ + proxyExternalRequest?: IncludedProxyExternalRequest | LazyLoadedOverride; + + /** + * Add possibility to override the default cdn invalidation for On Demand Revalidation + * @default "dummy" + */ + cdnInvalidation?: IncludedCDNInvalidationHandler | LazyLoadedOverride; +} + +export interface InstallOptions { + /** + * List of packages to install + * @example + * ```ts + * install: { + * packages: ["sharp@0.32"] + * } + * ``` + */ + packages: string[]; + /** @default undefined */ + arch?: "x64" | "arm64"; + /** @default undefined */ + nodeVersion?: string; + /** @default undefined */ + libc?: "glibc" | "musl"; + /** @default "linux" */ + os?: string; + + /** + * @default undefined + * Additional arguments to pass to the install command (i.e. npm install) + */ + additionalArgs?: string; +} + +export interface DefaultFunctionOptions< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +> { + /** + * Minify the server bundle. + * @default false + */ + minify?: boolean; + /** + * Print debug information. + * @default false + */ + debug?: boolean; + /** + * Enable overriding the default lambda. + */ + override?: DefaultOverrideOptions; + + /** + * Install options for the function. + * This is used to install additional packages to this function. + * For image optimization, it will install sharp by default. + * @default undefined + */ + install?: InstallOptions; +} + +export interface FunctionOptions extends DefaultFunctionOptions { + /** + * Runtime used + * @default "node" + */ + runtime?: "node" | "edge" | "deno"; + /** + * @default "regional" + */ + placement?: "regional" | "global"; + /** + * Enable overriding the default lambda. + */ + override?: OverrideOptions; + + routePreloadingBehavior?: RoutePreloadingBehavior; +} + +export type RouteTemplate = + | `app/${string}/route` + | `app/${string}/page` + | `app/page` + | `app/route` + | `pages/${string}`; + +export interface SplittedFunctionOptions extends FunctionOptions { + /** + * Here you should specify all the routes you want to use. + * For app routes, you should use the `app/${name}/route` format or `app/${name}/page` for pages. + * For pages, you should use the `page/${name}` format. + * @example + * ```ts + * routes: ["app/api/test/route", "app/page", "pages/admin"] + * ``` + */ + routes: RouteTemplate[]; + + /** + * Cloudfront compatible patterns. + * i.e. /api/* + * @default [] + */ + patterns: string[]; +} + +/** + * MiddlewareConfig that applies to both external and internal middlewares + * + * Note: this type is internal and included in both `ExternalMiddlewareConfig` and `InternalMiddlewareConfig` + */ +type CommonMiddlewareConfig = { + /** + * The assetResolver is used to resolve assets in the routing layer. + * + * @default "dummy" + */ + assetResolver?: IncludedAssetResolver | LazyLoadedOverride; +}; + +/** MiddlewareConfig that applies to external middlewares only */ +export type ExternalMiddlewareConfig = DefaultFunctionOptions & + CommonMiddlewareConfig & { + external: true; + /** + * The runtime used by next for the middleware. + * @default "edge" + */ + runtime?: "node" | "edge"; + + /** + * The override options for the middleware. + * By default the lite override are used (.i.e. s3-lite, dynamodb-lite, sqs-lite) + * @default undefined + */ + override?: OverrideOptions; + + /** + * Origin resolver is used to resolve the origin for internal rewrite. + * By default, it uses the pattern-env origin resolver. + * Pattern env uses pattern set in split function options and an env variable OPEN_NEXT_ORIGIN + * OPEN_NEXT_ORIGIN should be a json stringified object with the key of the splitted function as key and the origin as value + * @default "pattern-env" + */ + originResolver?: IncludedOriginResolver | LazyLoadedOverride; + }; + +/** MiddlewareConfig that applies to internal middlewares only */ +export type InternalMiddlewareConfig = { + external: false; +} & CommonMiddlewareConfig; + +export interface OpenNextConfig { + default: FunctionOptions; + functions?: Record; + + /** + * Override the default middleware + * When `external` is true, the middleware need to be deployed separately. + * It supports both edge and node runtime. + * @default undefined - Which is equivalent to `external: false` + */ + middleware?: ExternalMiddlewareConfig | InternalMiddlewareConfig; + + /** + * Override the default warmer + * By default, works for lambda only. + * If you override this, you'll need to handle the warmer event in the wrapper + * @default undefined + */ + warmer?: DefaultFunctionOptions & { + invokeFunction?: IncludedWarmer | LazyLoadedOverride; + }; + + /** + * Override the default revalidate function + * By default, works for lambda and on SQS event. + * Supports only node runtime + */ + revalidate?: DefaultFunctionOptions< + { host: string; url: string; type: "revalidate" }, + { type: "revalidate" } + >; + + /** + * Override the default revalidate function + * By default, works on lambda and for S3 key. + * Supports only node runtime + */ + imageOptimization?: DefaultFunctionOptions & { + /** + * The image loader is used to load the image from the source. + * @default "s3" + */ + loader?: IncludedImageLoader | LazyLoadedOverride; + }; + + /** + * Override the default initialization function + * By default, works for lambda and on SQS event. + * Supports only node runtime + */ + initializationFunction?: DefaultFunctionOptions & { + tagCache?: IncludedTagCache | LazyLoadedOverride; + }; + + /** + * Dangerous options. This break some functionnality but can be useful in some cases. + */ + dangerous?: DangerousOptions; + /** + * The command to build the Next.js app. + * @default `npm run build`, `yarn build`, or `pnpm build` based on the lock file found in the app's directory or any of its parent directories. + * @example + * ```ts + * build({ + * buildCommand: "pnpm custom:build", + * }); + * ``` + */ + buildCommand?: string; + /** + * The path to the target folder of build output from the `buildCommand` option (the path which will contain the `.next` and `.open-next` folders). This path is relative from the current process.cwd(). + * @default "." + */ + buildOutputPath?: string; + /** + * The path to the root of the Next.js app's source code. This path is relative from the current process.cwd(). + * @default "." + */ + appPath?: string; + /** + * The path to the package.json file of the Next.js app. This path is relative from the current process.cwd(). + * @default "." + */ + packageJsonPath?: string; + /** + * **Advanced usage** + * If you use the edge runtime somewhere (either in the middleware or in the functions), we compile 2 versions of the open-next.config.ts file. + * One for the node runtime and one for the edge runtime. + * This option allows you to specify the externals for the edge runtime used in esbuild for the compilation of open-next.config.ts + * It is especially useful if you use some custom overrides only in node + * @default [] + */ + edgeExternals?: string[]; +} diff --git a/packages/core/src/types/overrides.ts b/packages/core/src/types/overrides.ts new file mode 100644 index 00000000..84589959 --- /dev/null +++ b/packages/core/src/types/overrides.ts @@ -0,0 +1,264 @@ +import type { Readable } from "node:stream"; + +import type { Extension, Meta, StoredComposableCacheEntry } from "@/types/cache"; + +import type { + BaseEventOrResult, + BaseOverride, + InternalEvent, + InternalResult, + Origin, + ResolvedRoute, + StreamCreator, + WaitUntil, +} from "./open-next"; + +// Queue + +export interface QueueMessage { + MessageDeduplicationId: string; + MessageBody: { + host: string; + url: string; + lastModified: number; + eTag: string; + }; + MessageGroupId: string; +} + +export interface Queue { + send(message: QueueMessage): Promise; + name: string; +} + +/** + * Resolves assets in the routing layer. + */ +export interface AssetResolver { + name: string; + + /** + * Called by the routing layer to check for a matching static asset. + * + * @param event + * @returns an `InternalResult` when an asset is found a the path from the event, undefined otherwise. + */ + maybeGetAssetResult?: (event: InternalEvent) => Promise | undefined; +} + +// Incremental cache + +export type CachedFile = + | { + type: "redirect"; + props?: object; + meta?: Meta; + } + | { + type: "page"; + html: string; + json: object; + meta?: Meta; + } + | { + type: "app"; + html: string; + rsc: string; + meta?: Meta; + segmentData?: Record; + } + | { + type: "route"; + body: string; + meta?: Meta; + }; + +// type taken from: https://github.com/vercel/next.js/blob/9a1cd356/packages/next/src/server/response-cache/types.ts#L26-L38 +export type CachedFetchValue = { + kind: "FETCH"; + data: { + headers: { [k: string]: string }; + body: string; + url: string; + status?: number; + // field used by older versions of Next.js (see: https://github.com/vercel/next.js/blob/fda1ecc/packages/next/src/server/response-cache/types.ts#L23) + tags?: string[]; + }; + // tags are only present with file-system-cache + // fetch cache stores tags outside of cache entry + tags?: string[]; +}; + +export type WithLastModified = { + lastModified?: number; + value?: T; + /** + * If set to true, we will not check the tag cache for this entry. + * `revalidateTag` and `revalidatePath` may not work as expected. + */ + shouldBypassTagCache?: boolean; +}; + +export type CacheEntryType = Extension; + +export type CacheValue = (CacheType extends "fetch" + ? CachedFetchValue + : CacheType extends "cache" + ? CachedFile + : StoredComposableCacheEntry) & { + /** + * This is available for page cache entry, but only at runtime. + */ + revalidate?: number | false; +}; + +export type IncrementalCache = { + get( + key: string, + cacheType?: CacheType + ): Promise> | null>; + set( + key: string, + value: CacheValue, + isFetch?: CacheType + ): Promise; + delete(key: string): Promise; + name: string; +}; + +// Tag cache + +type BaseTagCache = { + name: string; +}; + +/** + * On get : +We have to check for every tag (after reading the incremental cache) that they have not been revalidated. + +In DynamoDB, this would require 1 GetItem per tag (including internal one), more realistically 1 BatchGetItem per get (In terms of pricing, it would be billed as multiple single GetItem) + +On set : +We don't have to do anything here + +On revalidateTag for each tag : +We have to update a single entry for this tag + +Pros : +- No need to prepopulate DDB +- Very little write + +Cons : +- Might be slower on read +- One page request (i.e. GET request) could require to check a lot of tags (And some of them multiple time when used with the fetch cache) +- Almost impossible to do automatic cdn revalidation by itself +*/ +export type NextModeTagCache = BaseTagCache & { + mode: "nextMode"; + // Necessary for the composable cache + getLastRevalidated(tags: string[]): Promise; + hasBeenRevalidated(tags: string[], lastModified?: number): Promise; + writeTags(tags: string[]): Promise; + // Optional method to get paths by tags + // It is used to automatically invalidate paths in the CDN + getPathsByTags?: (tags: string[]) => Promise; +}; + +export interface OriginalTagCacheWriteInput { + tag: string; + path: string; + revalidatedAt?: number; +} + +/** + * On get : +We just check for the cache key in the tag cache. If it has been revalidated we just return null, otherwise we continue + +On set : +We have to write both the incremental cache and check the tag cache for non existing tag/key combination. For non existing tag/key combination, we have to add them + +On revalidateTag for each tag : +We have to update every possible combination for the requested tag + +Pros : +- Very fast on read +- Only one query per get (On DynamoDB it's a lot cheaper) +- Can allow for automatic cdn invalidation on revalidateTag + +Cons : +- Lots of write on set and revalidateTag +- Needs to be prepopulated at build time to work properly + */ +export type OriginalTagCache = BaseTagCache & { + mode?: "original"; + getByTag(tag: string): Promise; + getByPath(path: string): Promise; + getLastModified(path: string, lastModified?: number): Promise; + writeTags(tags: OriginalTagCacheWriteInput[]): Promise; +}; + +export type TagCache = NextModeTagCache | OriginalTagCache; + +export type WrapperHandler< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +> = (handler: OpenNextHandler, converter: Converter) => Promise<(...args: unknown[]) => unknown>; + +export type Wrapper< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +> = BaseOverride & { + wrapper: WrapperHandler; + supportStreaming: boolean; + edgeRuntime?: boolean; +}; + +export type OpenNextHandlerOptions = { + // Create a `Writeable` for streaming responses. + streamCreator?: StreamCreator; + // Extends the liftetime of the runtime after the response is returned. + waitUntil?: WaitUntil; +}; + +export type OpenNextHandler< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +> = (event: E, options?: OpenNextHandlerOptions) => Promise; + +export type Converter< + E extends BaseEventOrResult = InternalEvent, + R extends BaseEventOrResult = InternalResult, +> = BaseOverride & { + convertFrom: (event: unknown) => Promise; + convertTo: (result: R, originalRequest?: unknown) => Promise; +}; + +export type Warmer = BaseOverride & { + invoke: (warmerId: string) => Promise; +}; + +export type ImageLoader = BaseOverride & { + load: (url: string) => Promise<{ + body?: Readable; + contentType?: string; + cacheControl?: string; + }>; +}; + +export type OriginResolver = BaseOverride & { + resolve: (path: string) => Promise; +}; + +export type ProxyExternalRequest = BaseOverride & { + proxy: (event: InternalEvent) => Promise; +}; + +type CDNPath = { + initialPath: string; + rawPath: string; + resolvedRoutes: ResolvedRoute[]; +}; + +export type CDNInvalidationHandler = BaseOverride & { + invalidatePaths: (paths: CDNPath[]) => Promise; +}; diff --git a/packages/core/src/utils/binary.ts b/packages/core/src/utils/binary.ts new file mode 100644 index 00000000..6852e535 --- /dev/null +++ b/packages/core/src/utils/binary.ts @@ -0,0 +1,67 @@ +const commonBinaryMimeTypes = new Set([ + "application/octet-stream", + // Docs + "application/epub+zip", + "application/msword", + "application/pdf", + "application/rtf", + "application/vnd.amazon.ebook", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + // Fonts + "font/otf", + "font/woff", + "font/woff2", + // Images + "image/bmp", + "image/gif", + "image/jpeg", + "image/png", + "image/tiff", + "image/vnd.microsoft.icon", + "image/webp", + // Audio + "audio/3gpp", + "audio/aac", + "audio/basic", + "audio/flac", + "audio/mpeg", + "audio/ogg", + "audio/wavaudio/webm", + "audio/x-aiff", + "audio/x-midi", + "audio/x-wav", + // Video + "video/3gpp", + "video/mp2t", + "video/mpeg", + "video/ogg", + "video/quicktime", + "video/webm", + "video/x-msvideo", + // Archives + "application/java-archive", + "application/vnd.apple.installer+xml", + "application/x-7z-compressed", + "application/x-apple-diskimage", + "application/x-bzip", + "application/x-bzip2", + "application/x-gzip", + "application/x-java-archive", + "application/x-rar-compressed", + "application/x-tar", + "application/x-zip", + "application/zip", + // Serialized data + "application/x-protobuf", +]); + +export function isBinaryContentType(contentType?: string | null) { + if (!contentType) return false; + + const value = contentType.split(";")[0]; + return commonBinaryMimeTypes.has(value); +} diff --git a/packages/core/src/utils/cache.ts b/packages/core/src/utils/cache.ts new file mode 100644 index 00000000..090bc247 --- /dev/null +++ b/packages/core/src/utils/cache.ts @@ -0,0 +1,82 @@ +import type { + CacheEntryType, + CacheValue, + OriginalTagCacheWriteInput, + WithLastModified, +} from "@/types/overrides"; + +import { debug } from "../adapters/logger"; + +export async function hasBeenRevalidated( + key: string, + tags: string[], + cacheEntry: WithLastModified> +): Promise { + if (globalThis.openNextConfig.dangerous?.disableTagCache) { + return false; + } + const value = cacheEntry.value; + if (!value) { + // We should never reach this point + return true; + } + if ("type" in cacheEntry && cacheEntry.type === "page") { + return false; + } + const lastModified = cacheEntry.lastModified ?? Date.now(); + if (globalThis.tagCache.mode === "nextMode") { + return tags.length === 0 ? false : await globalThis.tagCache.hasBeenRevalidated(tags, lastModified); + } + // TODO: refactor this, we should introduce a new method in the tagCache interface so that both implementations use hasBeenRevalidated + const _lastModified = await globalThis.tagCache.getLastModified(key, lastModified); + return _lastModified === -1; +} + +export function getTagsFromValue(value?: CacheValue<"cache">) { + if (!value) { + return []; + } + // The try catch is necessary for older version of next.js that may fail on this + try { + const cacheTags = value.meta?.headers?.["x-next-cache-tags"]?.split(",") ?? []; + delete value.meta?.headers?.["x-next-cache-tags"]; + return cacheTags; + } catch (e) { + return []; + } +} + +function getTagKey(tag: string | OriginalTagCacheWriteInput): string { + if (typeof tag === "string") { + return tag; + } + return JSON.stringify({ + tag: tag.tag, + path: tag.path, + }); +} + +export async function writeTags(tags: (string | OriginalTagCacheWriteInput)[]): Promise { + const store = globalThis.__openNextAls.getStore(); + debug("Writing tags", tags, store); + if (!store || globalThis.openNextConfig.dangerous?.disableTagCache) { + return; + } + const tagsToWrite = tags.filter((t) => { + const tagKey = getTagKey(t); + const shouldWrite = !store.writtenTags.has(tagKey); + // We preemptively add the tag to the writtenTags set + // to avoid writing the same tag multiple times in the same request + if (shouldWrite) { + store.writtenTags.add(tagKey); + } + return shouldWrite; + }); + if (tagsToWrite.length === 0) { + return; + } + + // Here we know that we have the correct type + // oxlint-disable-next-line @typescript-eslint/no-explicit-any - writeTags accepts a union type that typescript cannot infer correctly + await globalThis.tagCache.writeTags(tagsToWrite as any); +} diff --git a/packages/core/src/utils/error.ts b/packages/core/src/utils/error.ts new file mode 100644 index 00000000..da22733b --- /dev/null +++ b/packages/core/src/utils/error.ts @@ -0,0 +1,49 @@ +export interface BaseOpenNextError { + readonly __openNextInternal: true; + readonly canIgnore: boolean; + // 0 - debug, 1 - warn, 2 - error + readonly logLevel: 0 | 1 | 2; +} + +// This is an error that can be totally ignored +// It don't even need to be logged, or only in debug mode +export class IgnorableError extends Error implements BaseOpenNextError { + readonly __openNextInternal = true; + readonly canIgnore = true; + readonly logLevel = 0; + constructor(message: string) { + super(message); + this.name = "IgnorableError"; + } +} + +// This is an error that can be recovered from +// It should be logged but the process can continue +export class RecoverableError extends Error implements BaseOpenNextError { + readonly __openNextInternal = true; + readonly canIgnore = true; + readonly logLevel = 1; + constructor(message: string) { + super(message); + this.name = "RecoverableError"; + } +} + +// We should not continue the process if this error is thrown +export class FatalError extends Error implements BaseOpenNextError { + readonly __openNextInternal = true; + readonly canIgnore = false; + readonly logLevel = 2; + constructor(message: string) { + super(message); + this.name = "FatalError"; + } +} + +export function isOpenNextError(e: unknown): e is BaseOpenNextError & Error { + try { + return e !== null && typeof e === "object" && "__openNextInternal" in e; + } catch { + return false; + } +} diff --git a/packages/core/src/utils/lru.ts b/packages/core/src/utils/lru.ts new file mode 100644 index 00000000..f1bf8f3f --- /dev/null +++ b/packages/core/src/utils/lru.ts @@ -0,0 +1,30 @@ +export class LRUCache { + private cache: Map = new Map(); + + constructor(private maxSize: number) {} + + get(key: string) { + const result = this.cache.get(key); + // We could have used .has to allow for nullish value to be stored but we don't need that right now + if (result) { + // By removing and setting the key again we ensure it's the most recently used + this.cache.delete(key); + this.cache.set(key, result); + } + return result; + } + + set(key: string, value: T) { + if (this.cache.size >= this.maxSize) { + const firstKey = this.cache.keys().next().value; + if (firstKey !== undefined) { + this.cache.delete(firstKey); + } + } + this.cache.set(key, value); + } + + delete(key: string) { + this.cache.delete(key); + } +} diff --git a/packages/core/src/utils/normalize-path.ts b/packages/core/src/utils/normalize-path.ts new file mode 100644 index 00000000..d8d0c5db --- /dev/null +++ b/packages/core/src/utils/normalize-path.ts @@ -0,0 +1,22 @@ +import path from "node:path"; + +export function normalizePath(path: string) { + return path.replace(/\\/g, "/"); +} + +// See: https://github.com/vercel/next.js/blob/3ecf087f10fdfba4426daa02b459387bc9c3c54f/packages/next/src/shared/lib/utils.ts#L348 +export function normalizeRepeatedSlashes(url: URL) { + const urlNoQuery = url.host + url.pathname; + return `${url.protocol}//${urlNoQuery.replace(/\\/g, "/").replace(/\/\/+/g, "/")}${url.search}`; +} + +export function getMonorepoRelativePath(relativePath = "../.."): string { + return path.join( + globalThis.monorepoPackagePath + .split("/") + .filter(Boolean) + .map(() => "..") + .join("/"), + relativePath + ); +} diff --git a/packages/core/src/utils/promise.ts b/packages/core/src/utils/promise.ts new file mode 100644 index 00000000..00602ce0 --- /dev/null +++ b/packages/core/src/utils/promise.ts @@ -0,0 +1,137 @@ +import type { WaitUntil } from "@/types/open-next"; + +import { debug, error } from "../adapters/logger"; + +/** + * A `Promise.withResolvers` implementation that exposes the `resolve` and + * `reject` functions on a `Promise`. + * Copied from next https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/detached-promise.ts + * @see https://tc39.es/proposal-promise-with-resolvers/ + */ +export class DetachedPromise { + public readonly resolve: (value: T | PromiseLike) => void; + public readonly reject: (reason: unknown) => void; + public readonly promise: Promise; + + constructor() { + let resolve: (value: T | PromiseLike) => void; + let reject: (reason: unknown) => void; + + // Create the promise and assign the resolvers to the object. + this.promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + // We know that resolvers is defined because the Promise constructor runs + // synchronously. + this.resolve = resolve!; + this.reject = reject!; + } +} + +export class DetachedPromiseRunner { + private promises: DetachedPromise[] = []; + + public withResolvers(): DetachedPromise { + const detachedPromise = new DetachedPromise(); + this.promises.push(detachedPromise as DetachedPromise); + return detachedPromise; + } + + public add(promise: Promise): void { + const detachedPromise = new DetachedPromise(); + this.promises.push(detachedPromise as DetachedPromise); + promise.then(detachedPromise.resolve).catch((e) => { + // We just want to log the error here to avoid unhandled promise rejections + error("Detached promise rejected:", e); + // @ts-expect-error - We want to resolve to avoid hanging indefinitely, we don't reject to avoid unhandled promise rejections since we already log the error + detachedPromise.resolve(undefined); // Resolve to avoid unhandled promise rejection, we already log the error above + }); + } + + public async await(): Promise { + debug(`Awaiting ${this.promises.length} detached promises`); + const results = await Promise.allSettled(this.promises.map((p) => p.promise)); + const rejectedPromises = results.filter((r) => r.status === "rejected") as PromiseRejectedResult[]; + rejectedPromises.forEach((r) => { + error(r.reason); + }); + } +} + +async function awaitAllDetachedPromise() { + const store = globalThis.__openNextAls.getStore(); + + const promisesToAwait = store?.pendingPromiseRunner.await() ?? Promise.resolve(); + if (store?.waitUntil) { + store.waitUntil(promisesToAwait); + return; + } + await promisesToAwait; +} + +function provideNextAfterProvider() { + const NEXT_REQUEST_CONTEXT_SYMBOL = Symbol.for("@next/request-context"); + + // This is needed by some lib that relies on the vercel request context to properly await stuff. + // Remove this when vercel builder is updated to provide '@next/request-context'. + const VERCEL_REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context"); + + const store = globalThis.__openNextAls.getStore(); + + const waitUntil = + store?.waitUntil ?? ((promise: Promise) => store?.pendingPromiseRunner.add(promise)); + + const nextAfterContext = { + get: () => ({ + waitUntil, + }), + }; + + //@ts-expect-error + globalThis[NEXT_REQUEST_CONTEXT_SYMBOL] = nextAfterContext; + // We probably want to avoid providing this everytime since some lib may incorrectly think they are running in Vercel + // It may break stuff, but at the same time it will allow libs like `@vercel/otel` to work as expected + if (process.env.EMULATE_VERCEL_REQUEST_CONTEXT) { + //@ts-expect-error + globalThis[VERCEL_REQUEST_CONTEXT_SYMBOL] = nextAfterContext; + } +} + +export function runWithOpenNextRequestContext( + { + isISRRevalidation, + waitUntil, + requestId = Math.random().toString(36), + }: { + // Whether we are in ISR revalidation + isISRRevalidation: boolean; + // Extends the liftetime of the runtime after the response is returned. + waitUntil?: WaitUntil; + requestId?: string; + }, + fn: () => Promise +): Promise { + return globalThis.__openNextAls.run( + { + requestId, + pendingPromiseRunner: new DetachedPromiseRunner(), + isISRRevalidation, + waitUntil, + writtenTags: new Set(), + }, + async () => { + provideNextAfterProvider(); + let result: T; + try { + result = await fn(); + // We always await all detached promises before returning the result + // However we don't want to catch errors here, we want to let the parent handle it + } finally { + await awaitAllDetachedPromise(); + } + return result; + } + ); +} diff --git a/packages/core/src/utils/regex.ts b/packages/core/src/utils/regex.ts new file mode 100644 index 00000000..dec0a905 --- /dev/null +++ b/packages/core/src/utils/regex.ts @@ -0,0 +1,28 @@ +type Options = { + escape?: boolean; + flags?: string; +}; + +/** + * Constructs a regular expression for a path that supports separators for multiple platforms + * - Uses posix separators (`/`) as the input that should be made cross-platform. + * - Special characters are escaped by default but can be controlled through opts.escape. + * - Posix separators are always escaped. + * + * @example + * ```ts + * getCrossPlatformPathRegex("./middleware.mjs") + * getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false }) + * ``` + */ +export function getCrossPlatformPathRegex( + regex: string, + { escape: shouldEscape = true, flags = "" }: Options = {} +) { + const newExpr = (shouldEscape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex).replaceAll( + "/", + String.raw`(?:\/|\\)` + ); + + return new RegExp(newExpr, flags); +} diff --git a/packages/core/src/utils/safe-json-parse.ts b/packages/core/src/utils/safe-json-parse.ts new file mode 100644 index 00000000..cd9064ea --- /dev/null +++ b/packages/core/src/utils/safe-json-parse.ts @@ -0,0 +1,10 @@ +import logger from "../logger.js"; + +export function safeParseJsonFile(input: string, filePath: string, fallback?: T): T | undefined { + try { + return JSON.parse(input); + } catch (err) { + logger.warn(`Failed to parse JSON file "${filePath}". Error: ${(err as Error).message}`); + return fallback; + } +} diff --git a/packages/core/src/utils/stream.ts b/packages/core/src/utils/stream.ts new file mode 100644 index 00000000..66bd5234 --- /dev/null +++ b/packages/core/src/utils/stream.ts @@ -0,0 +1,67 @@ +import { ReadableStream } from "node:stream/web"; + +export async function fromReadableStream( + stream: ReadableStream, + base64?: boolean +): Promise { + const chunks: Uint8Array[] = []; + let totalLength = 0; + + for await (const chunk of stream) { + chunks.push(chunk); + totalLength += chunk.length; + } + + if (chunks.length === 0) { + return ""; + } + + if (chunks.length === 1) { + return Buffer.from(chunks[0]).toString(base64 ? "base64" : "utf8"); + } + + // Pre-allocate buffer with exact size to avoid reallocation + const buffer = Buffer.alloc(totalLength); + let offset = 0; + for (const chunk of chunks) { + buffer.set(chunk, offset); + offset += chunk.length; + } + + return buffer.toString(base64 ? "base64" : "utf8"); +} + +export function toReadableStream(value: string | Buffer, isBase64?: boolean): ReadableStream { + return new ReadableStream( + { + pull(controller) { + // Defer the Buffer.from conversion to when the stream is actually read. + controller.enqueue(Buffer.isBuffer(value) ? value : Buffer.from(value, isBase64 ? "base64" : "utf8")); + controller.close(); + }, + }, + { highWaterMark: 0 } + ); +} + +let maybeSomethingBuffer: Buffer | undefined; + +export function emptyReadableStream(): ReadableStream { + if (process.env.OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE === "true") { + return new ReadableStream( + { + pull(controller) { + maybeSomethingBuffer ??= Buffer.from("SOMETHING"); + controller.enqueue(maybeSomethingBuffer); + controller.close(); + }, + }, + { highWaterMark: 0 } + ); + } + return new ReadableStream({ + start(controller) { + controller.close(); + }, + }); +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..67b691f0 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "declaration": true, + "module": "esnext", + "lib": ["DOM", "ESNext"], + "outDir": "./dist", + "allowSyntheticDefaultImports": true, + "paths": { + "@/types/*": ["./src/types/*"], + "@/config/*": ["./src/adapters/config/*"], + "@/http/*": ["./src/http/*"], + "@/utils/*": ["./src/utils/*"] + } + } +} diff --git a/packages/open-next/package.json b/packages/open-next/package.json index 2f2249d6..98e643c8 100644 --- a/packages/open-next/package.json +++ b/packages/open-next/package.json @@ -54,6 +54,7 @@ "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", + "@opennextjs/core": "workspace:*", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts index 0ac93ae6..15ca9d4b 100644 --- a/packages/open-next/src/adapters/cache.ts +++ b/packages/open-next/src/adapters/cache.ts @@ -1,443 +1,2 @@ -import type { CacheHandlerValue, IncrementalCacheContext, IncrementalCacheValue } from "@/types/cache"; -import { getTagsFromValue, hasBeenRevalidated, writeTags } from "@/utils/cache"; - -import { isBinaryContentType } from "../utils/binary"; - -import { debug, error, warn } from "./logger"; - -export const SOFT_TAG_PREFIX = "_N_T_/"; - -function isFetchCache(options?: { kindHint?: "app" | "pages" | "fetch"; kind?: "FETCH" }): boolean { - if (typeof options === "object") { - return options.kindHint === "fetch" || options.kind === "FETCH"; - } - return false; -} -// We need to use globalThis client here as this class can be defined at load time in next 12 but client is not available at load time -export default class Cache { - public async get( - key: string, - // fetchCache is for next 13.5 and above, kindHint is for next 14 and above and boolean is for earlier versions - options?: { - kindHint?: "app" | "pages" | "fetch"; - tags?: string[]; - softTags?: string[]; - kind?: "FETCH"; - } - ) { - if (globalThis.openNextConfig?.dangerous?.disableIncrementalCache) { - return null; - } - - const softTags = typeof options === "object" ? options.softTags : []; - const tags = typeof options === "object" ? options.tags : []; - return isFetchCache(options) ? this.getFetchCache(key, softTags, tags) : this.getIncrementalCache(key); - } - - async getFetchCache(key: string, softTags?: string[], tags?: string[]) { - debug("get fetch cache", { key, softTags, tags }); - try { - const cachedEntry = await globalThis.incrementalCache.get(key, "fetch"); - - if (cachedEntry?.value === undefined) return null; - - const _tags = [...(tags ?? []), ...(softTags ?? [])]; - const _lastModified = cachedEntry.lastModified ?? Date.now(); - const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache - ? false - : await hasBeenRevalidated(key, _tags, cachedEntry); - - if (_hasBeenRevalidated) return null; - - // For cases where we don't have tags, we need to ensure that the soft tags are not being revalidated - // We only need to check for the path as it should already contain all the tags - if ((tags ?? []).length === 0) { - // Then we need to find the path for the given key - const path = softTags?.find( - (tag) => tag.startsWith(SOFT_TAG_PREFIX) && !tag.endsWith("layout") && !tag.endsWith("page") - ); - if (path) { - const hasPathBeenUpdated = cachedEntry.shouldBypassTagCache - ? false - : await hasBeenRevalidated(path.replace(SOFT_TAG_PREFIX, ""), [], cachedEntry); - if (hasPathBeenUpdated) { - // In case the path has been revalidated, we don't want to use the fetch cache - return null; - } - } - } - - return { - lastModified: _lastModified, - value: cachedEntry.value, - } as CacheHandlerValue; - } catch (e) { - // We can usually ignore errors here as they are usually due to cache not being found - debug("Failed to get fetch cache", e); - return null; - } - } - - async getIncrementalCache(key: string): Promise { - try { - const cachedEntry = await globalThis.incrementalCache.get(key, "cache"); - - if (!cachedEntry?.value) { - return null; - } - - const cacheData = cachedEntry.value; - - const meta = cacheData.meta; - const tags = getTagsFromValue(cacheData); - const _lastModified = cachedEntry.lastModified ?? Date.now(); - const _hasBeenRevalidated = cachedEntry.shouldBypassTagCache - ? false - : await hasBeenRevalidated(key, tags, cachedEntry); - if (_hasBeenRevalidated) return null; - - const store = globalThis.__openNextAls.getStore(); - if (store) { - store.lastModified = _lastModified; - } - - if (cacheData?.type === "route") { - return { - lastModified: _lastModified, - value: { - kind: "APP_ROUTE", - body: Buffer.from( - cacheData.body ?? Buffer.alloc(0), - isBinaryContentType(String(meta?.headers?.["content-type"])) ? "base64" : "utf8" - ), - status: meta?.status, - headers: meta?.headers, - }, - } as CacheHandlerValue; - } - if (cacheData?.type === "page" || cacheData?.type === "app") { - if (cacheData?.type === "app") { - const segmentData = new Map(); - if (cacheData.segmentData) { - for (const [segmentPath, segmentContent] of Object.entries(cacheData.segmentData ?? {})) { - segmentData.set(segmentPath, Buffer.from(segmentContent)); - } - } - return { - lastModified: _lastModified, - value: { - kind: "APP_PAGE", - html: cacheData.html, - rscData: Buffer.from(cacheData.rsc), - status: meta?.status, - headers: meta?.headers, - postponed: meta?.postponed, - segmentData, - }, - } as CacheHandlerValue; - } - return { - lastModified: _lastModified, - value: { - kind: "PAGES", - html: cacheData.html, - pageData: cacheData.json, - status: meta?.status, - headers: meta?.headers, - }, - } as CacheHandlerValue; - } - if (cacheData?.type === "redirect") { - return { - lastModified: _lastModified, - value: { - kind: "REDIRECT", - props: cacheData.props, - }, - } as CacheHandlerValue; - } - warn("Unknown cache type", cacheData); - return null; - } catch (e) { - // We can usually ignore errors here as they are usually due to cache not being found - debug("Failed to get body cache", e); - return null; - } - } - - async set(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext): Promise { - if (globalThis.openNextConfig?.dangerous?.disableIncrementalCache) { - return; - } - // This one might not even be necessary anymore - // Better be safe than sorry - const detachedPromise = globalThis.__openNextAls.getStore()?.pendingPromiseRunner.withResolvers(); - try { - if (data === null || data === undefined) { - await globalThis.incrementalCache.delete(key); - } else { - const revalidate = this.extractRevalidateForSet(ctx); - switch (data.kind) { - case "ROUTE": - case "APP_ROUTE": { - const { body, status, headers } = data; - await globalThis.incrementalCache.set( - key, - { - type: "route", - body: body.toString(isBinaryContentType(String(headers["content-type"])) ? "base64" : "utf8"), - meta: { - status, - headers, - }, - revalidate, - }, - "cache" - ); - break; - } - case "PAGE": - case "PAGES": { - const { html, pageData, status, headers } = data; - const isAppPath = typeof pageData === "string"; - if (isAppPath) { - await globalThis.incrementalCache.set( - key, - { - type: "app", - html, - rsc: pageData, - meta: { - status, - headers, - }, - revalidate, - }, - "cache" - ); - } else { - await globalThis.incrementalCache.set( - key, - { - type: "page", - html, - json: pageData, - revalidate, - }, - "cache" - ); - } - break; - } - case "APP_PAGE": { - const { html, rscData, headers, status, segmentData, postponed } = data; - const segmentToWrite: Record = {}; - if (segmentData) { - for (const [segmentPath, segmentContent] of segmentData.entries()) { - segmentToWrite[segmentPath] = segmentContent.toString("utf8"); - } - } - await globalThis.incrementalCache.set( - key, - { - type: "app", - html, - rsc: rscData.toString("utf8"), - meta: { - status, - headers, - postponed, - }, - revalidate, - segmentData: segmentData ? segmentToWrite : undefined, - }, - "cache" - ); - break; - } - case "FETCH": - await globalThis.incrementalCache.set(key, data, "fetch"); - break; - case "REDIRECT": - await globalThis.incrementalCache.set( - key, - { - type: "redirect", - props: data.props, - revalidate, - }, - "cache" - ); - break; - case "IMAGE": - // Not implemented - break; - } - } - - await this.updateTagsOnSet(key, data, ctx); - debug("Finished setting cache"); - } catch (e) { - error("Failed to set cache", e); - } finally { - // We need to resolve the promise even if there was an error - detachedPromise?.resolve(); - } - } - - public async revalidateTag(tags: string | string[]) { - const config = globalThis.openNextConfig.dangerous; - if (config?.disableTagCache || config?.disableIncrementalCache) { - return; - } - const _tags = Array.isArray(tags) ? tags : [tags]; - if (_tags.length === 0) { - return; - } - - try { - if (globalThis.tagCache.mode === "nextMode") { - const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? []; - - await writeTags(_tags); - if (paths.length > 0) { - // TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it - // It also means that we'll need to provide the tags used in every request to the wrapper or converter. - await globalThis.cdnInvalidationHandler.invalidatePaths( - paths.map((path) => ({ - initialPath: path, - rawPath: path, - resolvedRoutes: [ - { - route: path, - // TODO: ideally here we should check if it's an app router page or route - type: "app", - isFallback: false, - }, - ], - })) - ); - } - return; - } - - for (const tag of _tags) { - debug("revalidateTag", tag); - // Find all keys with the given tag - const paths = await globalThis.tagCache.getByTag(tag); - debug("Items", paths); - const toInsert = paths.map((path) => ({ - path, - tag, - })); - - // If the tag is a soft tag, we should also revalidate the hard tags - if (tag.startsWith(SOFT_TAG_PREFIX)) { - for (const path of paths) { - // We need to find all hard tags for a given path - const _tags = await globalThis.tagCache.getByPath(path); - const hardTags = _tags.filter((t) => !t.startsWith(SOFT_TAG_PREFIX)); - // For every hard tag, we need to find all paths and revalidate them - for (const hardTag of hardTags) { - const _paths = await globalThis.tagCache.getByTag(hardTag); - debug({ hardTag, _paths }); - toInsert.push( - ..._paths.map((path) => ({ - path, - tag: hardTag, - })) - ); - } - } - } - - // Update all keys with the given tag with revalidatedAt set to now - await writeTags(toInsert); - - // We can now invalidate all paths in the CDN - // This only applies to `revalidateTag`, not to `res.revalidate()` - const uniquePaths = Array.from( - new Set( - toInsert - // We need to filter fetch cache key as they are not in the CDN - .filter((t) => t.tag.startsWith(SOFT_TAG_PREFIX)) - .map((t) => `/${t.path}`) - ) - ); - if (uniquePaths.length > 0) { - await globalThis.cdnInvalidationHandler.invalidatePaths( - uniquePaths.map((path) => ({ - initialPath: path, - rawPath: path, - resolvedRoutes: [ - { - route: path, - // TODO: ideally here we should check if it's an app router page or route - type: "app", - isFallback: false, - }, - ], - })) - ); - } - } - } catch (e) { - error("Failed to revalidate tag", e); - } - } - - // TODO: We should delete/update tags in this method - // This will require an update to the tag cache interface - private async updateTagsOnSet(key: string, data?: IncrementalCacheValue, ctx?: IncrementalCacheContext) { - if ( - globalThis.openNextConfig.dangerous?.disableTagCache || - globalThis.tagCache.mode === "nextMode" || - // Here it means it's a delete - !data - ) { - return; - } - // Write derivedTags to the tag cache - // If we use an in house version of getDerivedTags in build we should use it here instead of next's one - const derivedTags: string[] = - data?.kind === "FETCH" - ? //@ts-expect-error - On older versions of next, ctx was a number, but for these cases we use data?.data?.tags - (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility - : data?.kind === "PAGE" - ? (data.headers?.["x-next-cache-tags"]?.split(",") ?? []) - : []; - debug("derivedTags", derivedTags); - - // Get all tags stored in dynamodb for the given key - // If any of the derived tags are not stored in dynamodb for the given key, write them - const storedTags = await globalThis.tagCache.getByPath(key); - const tagsToWrite = derivedTags.filter((tag) => !storedTags.includes(tag)); - if (tagsToWrite.length > 0) { - await writeTags( - tagsToWrite.map((tag) => ({ - path: key, - tag: tag, - // In case the tags are not there we just need to create them - // but we don't want them to return from `getLastModified` as they are not stale - revalidatedAt: 1, - })) - ); - } - } - - private extractRevalidateForSet(ctx?: IncrementalCacheContext): number | false | undefined { - if (ctx === undefined) { - return undefined; - } - if (typeof ctx === "number" || ctx === false) { - return ctx; - } - if ("revalidate" in ctx) { - return ctx.revalidate; - } - if ("cacheControl" in ctx) { - return ctx.cacheControl?.revalidate; - } - return undefined; - } -} +export { default } from "@opennextjs/core/adapters/cache.js"; +export * from "@opennextjs/core/adapters/cache.js"; diff --git a/packages/open-next/src/adapters/composable-cache.ts b/packages/open-next/src/adapters/composable-cache.ts index a6fb19c3..281b37dd 100644 --- a/packages/open-next/src/adapters/composable-cache.ts +++ b/packages/open-next/src/adapters/composable-cache.ts @@ -1,135 +1,2 @@ -import type { ComposableCacheEntry, ComposableCacheHandler } from "@/types/cache"; -import type { CacheValue } from "@/types/overrides"; -import { writeTags } from "@/utils/cache"; -import { fromReadableStream, toReadableStream } from "@/utils/stream"; - -import { debug } from "./logger"; - -const pendingWritePromiseMap = new Map>>(); - -export default { - async get(cacheKey: string) { - try { - // We first check if we have a pending write for this cache key - // If we do, we return the pending promise instead of fetching the cache - if (pendingWritePromiseMap.has(cacheKey)) { - const stored = pendingWritePromiseMap.get(cacheKey); - if (stored) { - return stored.then((entry) => ({ - ...entry, - value: toReadableStream(entry.value), - })); - } - } - const result = await globalThis.incrementalCache.get(cacheKey, "composable"); - if (!result?.value?.value) { - return undefined; - } - - debug("composable cache result", result); - - // We need to check if the tags associated with this entry has been revalidated - if (globalThis.tagCache.mode === "nextMode" && result.value.tags.length > 0) { - const hasBeenRevalidated = result.shouldBypassTagCache - ? false - : await globalThis.tagCache.hasBeenRevalidated(result.value.tags, result.lastModified); - if (hasBeenRevalidated) return undefined; - } else if (globalThis.tagCache.mode === "original" || globalThis.tagCache.mode === undefined) { - const hasBeenRevalidated = result.shouldBypassTagCache - ? false - : (await globalThis.tagCache.getLastModified(cacheKey, result.lastModified)) === -1; - if (hasBeenRevalidated) return undefined; - } - - return { - ...result.value, - value: toReadableStream(result.value.value), - }; - } catch (e) { - debug("Cannot read composable cache entry"); - return undefined; - } - }, - - async set(cacheKey: string, pendingEntry: Promise) { - const promiseEntry = pendingEntry.then(async (entry) => ({ - ...entry, - value: await fromReadableStream(entry.value), - })); - pendingWritePromiseMap.set(cacheKey, promiseEntry); - - const entry = await promiseEntry.finally(() => { - pendingWritePromiseMap.delete(cacheKey); - }); - await globalThis.incrementalCache.set( - cacheKey, - { - ...entry, - value: entry.value, - }, - "composable" - ); - if (globalThis.tagCache.mode === "original") { - const storedTags = await globalThis.tagCache.getByPath(cacheKey); - const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag)); - if (tagsToWrite.length > 0) { - await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey }))); - } - } - }, - - async refreshTags() { - // We don't do anything for now, do we want to do something here ??? - return; - }, - - /** - * The signature has changed in Next.js 16 - * - Before Next.js 16, the method takes `...tags: string[]` - * - From Next.js 16, the method takes `tags: string[]` - */ - async getExpiration(...tags: string[] | string[][]) { - if (globalThis.tagCache.mode === "nextMode") { - // Use `.flat()` to accommodate both signatures - return globalThis.tagCache.getLastRevalidated(tags.flat()); - } - // We always return 0 here, original tag cache are handled directly in the get part - // TODO: We need to test this more, i'm not entirely sure that this is working as expected - return 0; - }, - - /** - * This method is only used before Next.js 16 - */ - async expireTags(...tags: string[]) { - if (globalThis.tagCache.mode === "nextMode") { - return writeTags(tags); - } - const tagCache = globalThis.tagCache; - const revalidatedAt = Date.now(); - // For the original mode, we have more work to do here. - // We need to find all paths linked to to these tags - const pathsToUpdate = await Promise.all( - tags.map(async (tag) => { - const paths = await tagCache.getByTag(tag); - return paths.map((path) => ({ - path, - tag, - revalidatedAt, - })); - }) - ); - // We need to deduplicate paths, we use a set for that - const setToWrite = new Set<{ path: string; tag: string }>(); - for (const entry of pathsToUpdate.flat()) { - setToWrite.add(entry); - } - await writeTags(Array.from(setToWrite)); - }, - - // This one is necessary for older versions of next - async receiveExpiredTags(...tags: string[]) { - // This function does absolutely nothing - return; - }, -} satisfies ComposableCacheHandler; +export { default } from "@opennextjs/core/adapters/composable-cache.js"; +export * from "@opennextjs/core/adapters/composable-cache.js"; diff --git a/packages/open-next/src/adapters/config/index.ts b/packages/open-next/src/adapters/config/index.ts index 15011af5..06110ff0 100644 --- a/packages/open-next/src/adapters/config/index.ts +++ b/packages/open-next/src/adapters/config/index.ts @@ -1,41 +1 @@ -import path from "node:path"; - -import { debug } from "../logger"; - -import { - loadAppPathRoutesManifest, - loadAppPathsManifest, - loadAppPathsManifestKeys, - loadBuildId, - loadConfig, - loadConfigHeaders, - loadFunctionsConfigManifest, - loadHtmlPages, - loadMiddlewareManifest, - loadPagesManifest, - loadPrerenderManifest, - loadRoutesManifest, -} from "./util.js"; - -export const NEXT_DIR = path.join(__dirname, ".next"); -export const OPEN_NEXT_DIR = path.join(__dirname, ".open-next"); - -debug({ NEXT_DIR, OPEN_NEXT_DIR }); - -export const NextConfig = /* @__PURE__ */ loadConfig(NEXT_DIR); -export const BuildId = /* @__PURE__ */ loadBuildId(NEXT_DIR); -export const HtmlPages = /* @__PURE__ */ loadHtmlPages(NEXT_DIR); -// export const PublicAssets = loadPublicAssets(OPEN_NEXT_DIR); -export const RoutesManifest = /* @__PURE__ */ loadRoutesManifest(NEXT_DIR); -export const ConfigHeaders = /* @__PURE__ */ loadConfigHeaders(NEXT_DIR); -export const PrerenderManifest = /* @__PURE__ */ loadPrerenderManifest(NEXT_DIR); -export const PagesManifest = /* @__PURE__ */ loadPagesManifest(NEXT_DIR); -export const AppPathsManifestKeys = /* @__PURE__ */ loadAppPathsManifestKeys(NEXT_DIR); -export const MiddlewareManifest = /* @__PURE__ */ loadMiddlewareManifest(NEXT_DIR); -export const AppPathsManifest = /* @__PURE__ */ loadAppPathsManifest(NEXT_DIR); -export const AppPathRoutesManifest = /* @__PURE__ */ loadAppPathRoutesManifest(NEXT_DIR); - -export const FunctionsConfigManifest = /* @__PURE__ */ loadFunctionsConfigManifest(NEXT_DIR); - -process.env.NEXT_BUILD_ID = BuildId; -process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId; +export * from "@opennextjs/core/adapters/config/index.js"; diff --git a/packages/open-next/src/adapters/config/util.ts b/packages/open-next/src/adapters/config/util.ts index 228c0572..d9ce9aa6 100644 --- a/packages/open-next/src/adapters/config/util.ts +++ b/packages/open-next/src/adapters/config/util.ts @@ -1,135 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import type { - FunctionsConfigManifest, - MiddlewareManifest, - NextConfig, - PrerenderManifest, - RoutesManifest, -} from "@/types/next-types"; - -import type { PublicFiles } from "../../build"; - -export function loadConfig(nextDir: string) { - const filePath = path.join(nextDir, "required-server-files.json"); - const json = fs.readFileSync(filePath, "utf-8"); - const { config } = JSON.parse(json); - return config as NextConfig; -} -export function loadBuildId(nextDir: string) { - const filePath = path.join(nextDir, "BUILD_ID"); - return fs.readFileSync(filePath, "utf-8").trim(); -} - -export function loadPagesManifest(nextDir: string) { - const filePath = path.join(nextDir, "server/pages-manifest.json"); - const json = fs.readFileSync(filePath, "utf-8"); - return JSON.parse(json) as Record; -} - -export function loadHtmlPages(nextDir: string) { - return Object.entries(loadPagesManifest(nextDir)) - .filter(([_, value]) => (value as string).endsWith(".html")) - .map(([key]) => key); -} - -export function loadPublicAssets(openNextDir: string) { - const filePath = path.join(openNextDir, "public-files.json"); - const json = fs.readFileSync(filePath, "utf-8"); - return JSON.parse(json) as PublicFiles; -} - -export function loadRoutesManifest(nextDir: string) { - const filePath = path.join(nextDir, "routes-manifest.json"); - const json = fs.readFileSync(filePath, "utf-8"); - const routesManifest = JSON.parse(json) as RoutesManifest; - - const _dataRoutes = routesManifest.dataRoutes ?? []; - const dataRoutes = { - static: _dataRoutes.filter((r) => r.routeKeys === undefined), - dynamic: _dataRoutes.filter((r) => r.routeKeys !== undefined), - }; - - return { - basePath: routesManifest.basePath, - rewrites: Array.isArray(routesManifest.rewrites) - ? { beforeFiles: [], afterFiles: routesManifest.rewrites, fallback: [] } - : { - beforeFiles: routesManifest.rewrites.beforeFiles ?? [], - afterFiles: routesManifest.rewrites.afterFiles ?? [], - fallback: routesManifest.rewrites.fallback ?? [], - }, - redirects: routesManifest.redirects ?? [], - routes: { - static: routesManifest.staticRoutes ?? [], - dynamic: routesManifest.dynamicRoutes ?? [], - data: dataRoutes, - }, - locales: routesManifest.i18n?.locales ?? [], - }; -} - -export function loadConfigHeaders(nextDir: string) { - const filePath = path.join(nextDir, "routes-manifest.json"); - const json = fs.readFileSync(filePath, "utf-8"); - const routesManifest = JSON.parse(json) as RoutesManifest; - return routesManifest.headers; -} - -export function loadPrerenderManifest(nextDir: string): PrerenderManifest | undefined { - const filePath = path.join(nextDir, "prerender-manifest.json"); - if (!fs.existsSync(filePath)) { - return undefined; - } - const json = fs.readFileSync(filePath, "utf-8"); - return JSON.parse(json); -} - -export function loadAppPathsManifest(nextDir: string) { - const appPathsManifestPath = path.join(nextDir, "server/app-paths-manifest.json"); - const appPathsManifestJson = fs.existsSync(appPathsManifestPath) - ? fs.readFileSync(appPathsManifestPath, "utf-8") - : "{}"; - return JSON.parse(appPathsManifestJson) as Record; -} - -export function loadAppPathRoutesManifest(nextDir: string): Record { - const appPathRoutesManifestPath = path.join(nextDir, "app-path-routes-manifest.json"); - if (fs.existsSync(appPathRoutesManifestPath)) { - return JSON.parse(fs.readFileSync(appPathRoutesManifestPath, "utf-8")); - } - return {}; -} - -export function loadAppPathsManifestKeys(nextDir: string) { - const appPathsManifest = loadAppPathsManifest(nextDir); - return Object.keys(appPathsManifest).map((key) => { - // Remove parallel route - let cleanedKey = key.replace(/\/@[^/]+/g, ""); - - // Remove group routes - cleanedKey = cleanedKey.replace(/\/\((?!\.)[^)]*\)/g, ""); - - // Remove /page suffix - cleanedKey = cleanedKey.replace(/\/page$/g, ""); - // We need to check if the cleaned key is empty because it means it's the root path - return cleanedKey === "" ? "/" : cleanedKey; - }); -} - -export function loadMiddlewareManifest(nextDir: string) { - const filePath = path.join(nextDir, "server/middleware-manifest.json"); - const json = fs.readFileSync(filePath, "utf-8"); - return JSON.parse(json) as MiddlewareManifest; -} - -export function loadFunctionsConfigManifest(nextDir: string) { - const filePath = path.join(nextDir, "server/functions-config-manifest.json"); - try { - const json = fs.readFileSync(filePath, "utf-8"); - return JSON.parse(json) as FunctionsConfigManifest; - } catch (e) { - return { functions: {}, version: 1 }; - } -} +export * from "@opennextjs/core/adapters/config/util.js"; diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index f6841c6b..a2ac96ef 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -1,74 +1,2 @@ -import type { ReadableStream } from "node:stream/web"; - -import type { InternalEvent, InternalResult } from "@/types/open-next"; -import type { OpenNextHandlerOptions } from "@/types/overrides"; -import { runWithOpenNextRequestContext } from "@/utils/promise"; -import { emptyReadableStream } from "@/utils/stream"; - -// We import it like that so that the edge plugin can replace it -import { NextConfig } from "../adapters/config"; -import { createGenericHandler } from "../core/createGenericHandler"; -import { convertBodyToReadableStream } from "../core/routing/util"; -import { INTERNAL_EVENT_REQUEST_ID } from "../core/routingHandler"; - -globalThis.__openNextAls = new AsyncLocalStorage(); - -const defaultHandler = async ( - internalEvent: InternalEvent, - options?: OpenNextHandlerOptions -): Promise => { - globalThis.isEdgeRuntime = true; - - const requestId = globalThis.openNextConfig.middleware?.external - ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID] - : Math.random().toString(36); - - // We run everything in the async local storage context so that it is available in edge runtime functions - return runWithOpenNextRequestContext( - { isISRRevalidation: false, waitUntil: options?.waitUntil, requestId }, - async () => { - // @ts-expect-error - This is bundled - const handler = await import("./middleware.mjs"); - - const response: Response = await handler.default({ - headers: internalEvent.headers, - method: internalEvent.method || "GET", - nextConfig: { - basePath: NextConfig.basePath, - i18n: NextConfig.i18n, - trailingSlash: NextConfig.trailingSlash, - }, - url: internalEvent.url, - body: convertBodyToReadableStream(internalEvent.method, internalEvent.body), - }); - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - if (key.toLowerCase() === "set-cookie") { - responseHeaders[key] = responseHeaders[key] ? [...responseHeaders[key], value] : [value]; - } else { - responseHeaders[key] = value; - } - }); - - const body = (response.body as ReadableStream) ?? emptyReadableStream(); - - return { - type: "core", - statusCode: response.status, - headers: responseHeaders, - body: body, - // Do we need to handle base64 encoded response? - isBase64Encoded: false, - }; - } - ); -}; - -export const handler = await createGenericHandler({ - handler: defaultHandler, - type: "middleware", -}); - -export default { - fetch: handler, -}; +export { default } from "@opennextjs/core/adapters/edge-adapter.js"; +export * from "@opennextjs/core/adapters/edge-adapter.js"; diff --git a/packages/open-next/src/adapters/image-optimization-adapter.ts b/packages/open-next/src/adapters/image-optimization-adapter.ts index 4c648519..075f3822 100644 --- a/packages/open-next/src/adapters/image-optimization-adapter.ts +++ b/packages/open-next/src/adapters/image-optimization-adapter.ts @@ -1,256 +1 @@ -import { createHash } from "node:crypto"; -import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from "node:http"; -import https from "node:https"; -import path from "node:path"; -import type { Writable } from "node:stream"; - -// @ts-ignore -import { defaultConfig } from "next/dist/server/config-shared"; -import { - ImageOptimizerCache, - // @ts-ignore -} from "next/dist/server/image-optimizer"; -// @ts-ignore -import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta"; - -import { loadBuildId, loadConfig } from "@/config/util.js"; -import { OpenNextNodeResponse } from "@/http/openNextResponse.js"; -import type { InternalEvent, InternalResult, StreamCreator } from "@/types/open-next.js"; -import type { OpenNextHandlerOptions } from "@/types/overrides.js"; -import { emptyReadableStream, toReadableStream } from "@/utils/stream.js"; - -import { createGenericHandler } from "../core/createGenericHandler.js"; -import { resolveImageLoader } from "../core/resolve.js"; - -import { debug, error } from "./logger.js"; -import { optimizeImage } from "./plugins/image-optimization/image-optimization.js"; -import { setNodeEnv } from "./util.js"; - -setNodeEnv(); -const nextDir = path.join(__dirname, ".next"); -const config = loadConfig(nextDir); -const buildId = loadBuildId(nextDir); -const nextConfig = { - ...defaultConfig, - images: { - ...defaultConfig.images, - ...config.images, - }, -}; -debug("Init config", { - nextDir, - nextConfig, -}); - -///////////// -// Handler // -///////////// - -export const handler = await createGenericHandler({ - handler: defaultHandler, - type: "imageOptimization", -}); - -export async function defaultHandler( - event: InternalEvent, - options?: OpenNextHandlerOptions -): Promise { - // Images are handled via header and query param information. - debug("handler event", event); - const { headers, query: queryString } = event; - - try { - // Set the HOST environment variable to the host header if it is not set - // If it is set it is assumed to be set by the user and should be used instead - // It might be useful for cases where the user wants to use a different host than the one in the request - // It could even allow to have multiple hosts for the image optimization by setting the HOST environment variable in the wrapper for example - if (!process.env.HOST) { - const headersHost = headers["x-forwarded-host"] || headers.host; - process.env.HOST = headersHost; - } - - const imageParams = validateImageParams(headers, queryString === null ? undefined : queryString); - // We return a 400 here if imageParams returns an errorMessage - // https://github.com/vercel/next.js/blob/512d8283054407ab92b2583ecce3b253c3be7b85/packages/next/src/server/next-server.ts#L937-L941 - if ("errorMessage" in imageParams) { - error("Error during validation of image params", imageParams.errorMessage); - return buildFailureResponse(imageParams.errorMessage, options?.streamCreator, 400); - } - let etag: string | undefined; - // We don't cache any images, so in order to be able to return 304 responses, we compute an ETag from what is assumed to be static - if (process.env.OPENNEXT_STATIC_ETAG) { - etag = computeEtag(imageParams); - } - if (etag && headers["if-none-match"] === etag) { - return { - statusCode: 304, - headers: {}, - body: emptyReadableStream(), - isBase64Encoded: false, - type: "core", - }; - } - const result = await optimizeImage(headers, imageParams, nextConfig, downloadHandler); - return buildSuccessResponse(result, options?.streamCreator, etag); - } catch (e: unknown) { - error("Failed to optimize image", e); - return buildFailureResponse("Internal server error", options?.streamCreator); - } -} - -////////////////////// -// Helper functions // -////////////////////// - -function validateImageParams(headers: OutgoingHttpHeaders, query?: InternalEvent["query"]) { - // Next.js checks if external image URL matches the - // `images.remotePatterns` - const imageParams = ImageOptimizerCache.validateParams( - // @ts-ignore - { headers }, - query, - nextConfig, - false - ); - debug("image params", imageParams); - return imageParams; -} - -function computeEtag(imageParams: { href: string; width: number; quality: number }) { - return createHash("sha1") - .update( - JSON.stringify({ - href: imageParams.href, - width: imageParams.width, - quality: imageParams.quality, - buildId, - }) - ) - .digest("base64"); -} - -type ImageOptimizeResult = { - contentType: string; - buffer: Buffer; - maxAge: number; -}; - -function buildSuccessResponse( - imageOptimizeResult: ImageOptimizeResult, - streamCreator?: StreamCreator, - etag?: string -): InternalResult { - const headers: Record = { - Vary: "Accept", - "Content-Type": imageOptimizeResult.contentType, - "Cache-Control": `public,max-age=${imageOptimizeResult.maxAge},immutable`, - }; - debug("result", imageOptimizeResult); - if (etag) { - headers.ETag = etag; - } - - if (streamCreator) { - const response = new OpenNextNodeResponse( - () => void 0, - async () => void 0, - streamCreator - ); - response.writeHead(200, headers); - response.end(imageOptimizeResult.buffer); - } - - return { - type: "core", - statusCode: 200, - body: toReadableStream(imageOptimizeResult.buffer, true), - isBase64Encoded: true, - headers, - }; -} - -function buildFailureResponse( - errorMessage: string, - streamCreator?: StreamCreator, - statusCode = 500 -): InternalResult { - debug(errorMessage, statusCode); - if (streamCreator) { - const response = new OpenNextNodeResponse( - () => void 0, - async () => void 0, - streamCreator - ); - response.writeHead(statusCode, { - Vary: "Accept", - "Cache-Control": "public,max-age=60,immutable", - }); - response.end(errorMessage); - } - return { - type: "core", - isBase64Encoded: false, - statusCode: statusCode, - headers: { - Vary: "Accept", - // For failed images, allow client to retry after 1 minute. - "Cache-Control": "public,max-age=60,immutable", - }, - body: toReadableStream(errorMessage), - }; -} - -const loader = await resolveImageLoader(globalThis.openNextConfig.imageOptimization?.loader ?? "s3"); - -async function downloadHandler(_req: IncomingMessage, res: ServerResponse, url?: NextUrlWithParsedQuery) { - // downloadHandler is called by Next.js. We don't call this function - // directly. - debug("downloadHandler url", url); - - // Reads the output from the Writable and writes to the response - const pipeRes = (w: Writable, res: ServerResponse) => { - w.pipe(res) - .once("close", () => { - res.statusCode = 200; - res.end(); - }) - .once("error", (err) => { - error("Failed to get image", err); - res.statusCode = 400; - res.end(); - }); - }; - - try { - // Case 1: remote image URL => download the image from the URL - if (url?.href?.toLowerCase().match(/^https?:\/\//)) { - pipeRes(https.get(url), res); - } - // Case 2: local image => download the image from S3 - else { - // Download image from S3 - // note: S3 expects keys without leading `/` - - const response = await loader.load(url?.href ?? ""); - - if (!response.body) { - throw new Error("Empty response body from the S3 request."); - } - - // @ts-ignore - pipeRes(response.body, res); - - // Respect the bucket file's content-type and cache-control - // imageOptimizer will use this to set the results.maxAge - if (response.contentType) { - res.setHeader("Content-Type", response.contentType); - } - if (response.cacheControl) { - res.setHeader("Cache-Control", response.cacheControl); - } - } - } catch (e: unknown) { - error("Failed to download image", e); - throw e; - } -} +export * from "@opennextjs/core/adapters/image-optimization-adapter.js"; diff --git a/packages/open-next/src/adapters/logger.ts b/packages/open-next/src/adapters/logger.ts index b79fcb2f..94abb09b 100644 --- a/packages/open-next/src/adapters/logger.ts +++ b/packages/open-next/src/adapters/logger.ts @@ -1,102 +1 @@ -import { isOpenNextError } from "@/utils/error.js"; - -export function debug(...args: unknown[]) { - if (globalThis.openNextDebug) { - console.log(...args); - } -} - -export function warn(...args: unknown[]) { - console.warn(...args); -} - -interface AwsSdkClientCommandErrorLog { - clientName: string; - commandName: string; - error: Error & { Code?: string }; -} - -type AwsSdkClientCommandErrorInput = Pick & { - errorName: string; -}; - -const DOWNPLAYED_ERROR_LOGS: AwsSdkClientCommandErrorInput[] = [ - { - clientName: "S3Client", - commandName: "GetObjectCommand", - errorName: "NoSuchKey", - }, -]; - -const isAwsSdkClientCommandErrorLog = (errorLog: unknown): errorLog is AwsSdkClientCommandErrorLog => - errorLog !== null && - typeof errorLog === "object" && - "clientName" in errorLog && - "commandName" in errorLog && - "error" in errorLog; - -const isDownplayedErrorLog = (errorLog: unknown): boolean => { - if (!isAwsSdkClientCommandErrorLog(errorLog)) { - return false; - } - return DOWNPLAYED_ERROR_LOGS.some( - (downplayedInput) => - downplayedInput.clientName === errorLog.clientName && - downplayedInput.commandName === errorLog.commandName && - (downplayedInput.errorName === errorLog.error?.name || - downplayedInput.errorName === errorLog.error?.Code) - ); -}; - -export function error(...args: unknown[]) { - // we try to catch errors from the aws-sdk client and downplay some of them - if (args.some((arg) => isDownplayedErrorLog(arg))) { - return debug(...args); - } - if (args.some((arg) => isOpenNextError(arg))) { - // In case of an internal error, we log it with the appropriate log level - const error = args.find((arg) => isOpenNextError(arg))!; - if (error.logLevel < getOpenNextErrorLogLevel()) { - return; - } - if (error.logLevel === 0) { - // Display the name and the message instead of full Open Next errors. - // console.log is used so that logging does not depend on openNextDebug. - return console.log(...args.map((arg) => (isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg))); - } - if (error.logLevel === 1) { - // Display the name and the message instead of full Open Next errors. - return warn(...args.map((arg) => (isOpenNextError(arg) ? `${arg.name}: ${arg.message}` : arg))); - } - return console.error(...args); - } - console.error(...args); -} - -export const awsLogger = { - trace: () => {}, - debug: () => {}, - info: debug, - warn, - error, -}; - -/** - * Retrieves the log level for internal errors from the - * OPEN_NEXT_ERROR_LOG_LEVEL environment variable. - * - * @returns The numerical log level 0 (debug), 1 (warn), or 2 (error) - */ -function getOpenNextErrorLogLevel(): number { - const strLevel = process.env.OPEN_NEXT_ERROR_LOG_LEVEL ?? "1"; - switch (strLevel.toLowerCase()) { - case "debug": - case "0": - return 0; - case "error": - case "2": - return 2; - default: - return 1; - } -} +export * from "@opennextjs/core/adapters/logger.js"; diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index 295ec809..01d283da 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -1,127 +1,2 @@ -import type { - ExternalMiddlewareConfig, - InternalEvent, - InternalResult, - MiddlewareResult, -} from "@/types/open-next"; -import type { OpenNextHandlerOptions } from "@/types/overrides"; -import { runWithOpenNextRequestContext } from "@/utils/promise"; - -import { debug, error } from "../adapters/logger"; -import { createGenericHandler } from "../core/createGenericHandler"; -import { - resolveAssetResolver, - resolveIncrementalCache, - resolveOriginResolver, - resolveProxyRequest, - resolveQueue, - resolveTagCache, -} from "../core/resolve"; -import { constructNextUrl } from "../core/routing/util"; -import routingHandler, { - INTERNAL_EVENT_REQUEST_ID, - INTERNAL_HEADER_REWRITE_STATUS_CODE, - INTERNAL_HEADER_INITIAL_URL, - INTERNAL_HEADER_RESOLVED_ROUTES, -} from "../core/routingHandler"; - -globalThis.internalFetch = fetch; -globalThis.__openNextAls = new AsyncLocalStorage(); - -const defaultHandler = async ( - internalEvent: InternalEvent, - options?: OpenNextHandlerOptions -): Promise => { - // We know that the middleware is external when this adapter is used - const middlewareConfig = globalThis.openNextConfig.middleware as ExternalMiddlewareConfig; - const originResolver = await resolveOriginResolver(middlewareConfig?.originResolver); - - const externalRequestProxy = await resolveProxyRequest(middlewareConfig?.override?.proxyExternalRequest); - - const assetResolver = await resolveAssetResolver(middlewareConfig?.assetResolver); - - globalThis.tagCache = await resolveTagCache(middlewareConfig?.override?.tagCache); - - globalThis.queue = await resolveQueue(middlewareConfig?.override?.queue); - - globalThis.incrementalCache = await resolveIncrementalCache(middlewareConfig?.override?.incrementalCache); - - const requestId = Math.random().toString(36); - - // We run everything in the async local storage context so that it is available in the external middleware - return runWithOpenNextRequestContext( - { - isISRRevalidation: internalEvent.headers["x-isr"] === "1", - waitUntil: options?.waitUntil, - requestId, - }, - async () => { - const result = await routingHandler(internalEvent, { assetResolver }); - if ("internalEvent" in result) { - debug("Middleware intercepted event", internalEvent); - if (!result.isExternalRewrite) { - const origin = await originResolver.resolve(result.internalEvent.rawPath); - return { - type: "middleware", - internalEvent: { - ...result.internalEvent, - headers: { - ...result.internalEvent.headers, - [INTERNAL_HEADER_INITIAL_URL]: internalEvent.url, - [INTERNAL_HEADER_RESOLVED_ROUTES]: JSON.stringify(result.resolvedRoutes), - [INTERNAL_EVENT_REQUEST_ID]: requestId, - [INTERNAL_HEADER_REWRITE_STATUS_CODE]: String(result.rewriteStatusCode), - }, - }, - isExternalRewrite: result.isExternalRewrite, - origin, - isISR: result.isISR, - initialURL: result.initialURL, - resolvedRoutes: result.resolvedRoutes, - initialResponse: result.initialResponse, - }; - } - try { - return externalRequestProxy.proxy(result.internalEvent); - } catch (e) { - error("External request failed.", e); - return { - type: "middleware", - internalEvent: { - ...result.internalEvent, - headers: { - ...result.internalEvent.headers, - [INTERNAL_EVENT_REQUEST_ID]: requestId, - }, - rawPath: "/500", - url: constructNextUrl(result.internalEvent.url, "/500"), - method: "GET", - }, - // On error we need to rewrite to the 500 page which is an internal rewrite - isExternalRewrite: false, - origin: false, - isISR: result.isISR, - initialURL: result.internalEvent.url, - resolvedRoutes: [{ route: "/500", type: "page", isFallback: false }], - }; - } - } - - if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) { - result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId; - } - - debug("Middleware response", result); - return result; - } - ); -}; - -export const handler = await createGenericHandler({ - handler: defaultHandler, - type: "middleware", -}); - -export default { - fetch: handler, -}; +export { default } from "@opennextjs/core/adapters/middleware.js"; +export * from "@opennextjs/core/adapters/middleware.js"; diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts index 364e684e..848ffc6e 100644 --- a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts +++ b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts @@ -1,48 +1 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; - -import type { APIGatewayProxyEventHeaders } from "aws-lambda"; -import type { NextConfig } from "next/dist/server/config-shared"; -//#override imports -import { fetchExternalImage, fetchInternalImage, imageOptimizer } from "next/dist/server/image-optimizer"; -//#endOverride -import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta"; - -import { debug } from "../../logger.js"; - -//#override optimizeImage -export async function optimizeImage( - headers: APIGatewayProxyEventHeaders, - // oxlint-disable-next-line @typescript-eslint/no-explicit-any - image optimization API varies across Next.js versions - imageParams: any, - nextConfig: NextConfig, - handleRequest: ( - newReq: IncomingMessage, - newRes: ServerResponse, - newParsedUrl?: NextUrlWithParsedQuery - ) => Promise -) { - const { isAbsolute, href } = imageParams; - - const imageUpstream = isAbsolute - ? //@ts-expect-error - fetchExternalImage signature has changed in Next.js 16, it has an extra boolean parameter. - // https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 - await fetchExternalImage(href) - : await fetchInternalImage( - href, - // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. - { headers }, - {}, // res object is not necessary as it's not actually used. - handleRequest - ); - - const result = await imageOptimizer( - imageUpstream, - imageParams, - // @ts-ignore - nextConfig, - false // not in dev mode - ); - debug("optimized result", result); - return result; -} -//#endOverride +export * from "@opennextjs/core/adapters/plugins/image-optimization/image-optimization.js"; diff --git a/packages/open-next/src/adapters/revalidate.ts b/packages/open-next/src/adapters/revalidate.ts index d2b1674c..05f3e49a 100644 --- a/packages/open-next/src/adapters/revalidate.ts +++ b/packages/open-next/src/adapters/revalidate.ts @@ -1,99 +1 @@ -import fs from "node:fs"; -import type { IncomingMessage } from "node:http"; -import https from "node:https"; -import path from "node:path"; - -import { createGenericHandler } from "../core/createGenericHandler.js"; - -import { debug, error } from "./logger.js"; - -const prerenderManifest = loadPrerenderManifest(); - -interface PrerenderManifest { - preview: { - previewModeId: string; - previewModeSigningKey: string; - previewModeEncryptionKey: string; - }; -} - -export interface RevalidateEvent { - type: "revalidate"; - records: { - host: string; - url: string; - id: string; - }[]; -} - -const defaultHandler = async (event: RevalidateEvent) => { - const failedRecords: RevalidateEvent["records"] = []; - for (const record of event.records) { - const { host, url } = record; - debug("Revalidating stale page", { host, url }); - - // Make a HEAD request to the page to revalidate it. This will trigger - // the page to be re-rendered and cached in S3 - // - HEAD request is used b/c it's not necessary to make a GET request - // and have CloudFront cache the request. This is because the request - // does not have real life headers and the cache won't be used anyway. - // - "previewModeId" is used to ensure the page is revalidated in a - // blocking way in lambda - // https://github.com/vercel/next.js/blob/1088b3f682cbe411be2d1edc502f8a090e36dee4/packages/next/src/server/api-utils/node.ts#L353 - try { - await new Promise((resolve, reject) => { - const req = https.request( - `https://${host}${url}`, - { - method: "HEAD", - headers: { - "x-prerender-revalidate": prerenderManifest.preview.previewModeId, - "x-isr": "1", - }, - }, - (res) => { - debug("revalidating", { - url, - host, - headers: res.headers, - statusCode: res.statusCode, - }); - if (res.statusCode !== 200 || res.headers["x-nextjs-cache"] !== "REVALIDATED") { - failedRecords.push(record); - } - resolve(res); - } - ); - req.on("error", (err) => { - error("Error revalidating page", { host, url }); - reject(err); - }); - req.end(); - }); - } catch (err) { - failedRecords.push(record); - } - } - if (failedRecords.length > 0) { - error(`Failed to revalidate ${failedRecords.length} pages`, { - failedRecords, - }); - } - - return { - type: "revalidate", - // Records returned here are the ones that failed to revalidate - records: failedRecords, - }; -}; - -export const handler = await createGenericHandler({ - handler: defaultHandler, - type: "revalidate", -}); - -function loadPrerenderManifest() { - const filePath = path.join("prerender-manifest.json"); - const json = fs.readFileSync(filePath, "utf-8"); - return JSON.parse(json) as PrerenderManifest; -} +export * from "@opennextjs/core/adapters/revalidate.js"; diff --git a/packages/open-next/src/adapters/server-adapter.ts b/packages/open-next/src/adapters/server-adapter.ts index 4d89758b..2f4f1cb8 100644 --- a/packages/open-next/src/adapters/server-adapter.ts +++ b/packages/open-next/src/adapters/server-adapter.ts @@ -1,27 +1 @@ -import { createMainHandler } from "../core/createMainHandler.js"; - -import { setNodeEnv } from "./util.js"; - -// We load every config here so that they are only loaded once -// and during cold starts -setNodeEnv(); -setNextjsServerWorkingDirectory(); - -// Because next is messing with fetch, we have to make sure that we use an untouched version of fetch -globalThis.internalFetch = fetch; - -///////////// -// Handler // -///////////// - -export const handler = await createMainHandler(); - -////////////////////// -// Helper functions // -////////////////////// - -function setNextjsServerWorkingDirectory() { - // WORKAROUND: Set `NextServer` working directory (AWS specific) - // See https://opennext.js.org/aws/v2/advanced/workaround#workaround-set-nextserver-working-directory-aws-specific - process.chdir(__dirname); -} +export * from "@opennextjs/core/adapters/server-adapter.js"; diff --git a/packages/open-next/src/adapters/util.ts b/packages/open-next/src/adapters/util.ts index 720c21e2..a8b76eef 100644 --- a/packages/open-next/src/adapters/util.ts +++ b/packages/open-next/src/adapters/util.ts @@ -1,39 +1 @@ -//TODO: We should probably move all the utils to a separate location - -export function setNodeEnv() { - // Note: we create a `processEnv` variable instead of just using `process.env` directly - // because build tools can substitute `process.env.NODE_ENV` on build making - // assignments such as `process.env.NODE_ENV = ...` problematic - const processEnv = process.env; - processEnv.NODE_ENV = process.env.NODE_ENV ?? "production"; -} - -export function generateUniqueId() { - return Math.random().toString(36).slice(2, 8); -} - -/** - * Create an array of arrays of size `chunkSize` from `items` - * @param items Array of T - * @param chunkSize size of each chunk - * @returns T[][] - */ -export function chunk(items: T[], chunkSize: number): T[][] { - const chunked = items.reduce((acc, curr, i) => { - const chunkIndex = Math.floor(i / chunkSize); - acc[chunkIndex] = [...(acc[chunkIndex] ?? []), curr]; - return acc; - }, new Array()); - - return chunked; -} - -export function parseNumberFromEnv(envValue: string | undefined): number | undefined { - if (typeof envValue !== "string") { - return envValue; - } - - const parsedValue = Number.parseInt(envValue); - - return Number.isNaN(parsedValue) ? undefined : parsedValue; -} +export * from "@opennextjs/core/adapters/util.js"; diff --git a/packages/open-next/src/adapters/warmer-function.ts b/packages/open-next/src/adapters/warmer-function.ts index 4b61b068..0b5ad303 100644 --- a/packages/open-next/src/adapters/warmer-function.ts +++ b/packages/open-next/src/adapters/warmer-function.ts @@ -1,34 +1 @@ -import { createGenericHandler } from "../core/createGenericHandler.js"; -import { resolveWarmerInvoke } from "../core/resolve.js"; - -import { generateUniqueId } from "./util.js"; - -export interface WarmerEvent { - type: "warmer"; - warmerId: string; - index: number; - concurrency: number; - delay: number; -} - -export interface WarmerResponse { - type: "warmer"; - serverId: string; -} - -export const handler = await createGenericHandler({ - handler: defaultHandler, - type: "warmer", -}); - -async function defaultHandler() { - const warmerId = `warmer-${generateUniqueId()}`; - - const invokeFn = await resolveWarmerInvoke(globalThis.openNextConfig.warmer?.invokeFunction); - - await invokeFn.invoke(warmerId); - - return { - type: "warmer", - }; -} +export * from "@opennextjs/core/adapters/warmer-function.js"; diff --git a/packages/open-next/src/build/buildNextApp.ts b/packages/open-next/src/build/buildNextApp.ts index 5142c36b..61fd9e2e 100644 --- a/packages/open-next/src/build/buildNextApp.ts +++ b/packages/open-next/src/build/buildNextApp.ts @@ -1,22 +1 @@ -import cp from "node:child_process"; -import path from "node:path"; - -import type * as buildHelper from "./helper.js"; - -export function setStandaloneBuildMode(options: buildHelper.BuildOptions) { - // Equivalent to setting `output: "standalone"` in next.config.js - process.env.NEXT_PRIVATE_STANDALONE = "true"; - // Equivalent to setting `experimental.outputFileTracingRoot` in next.config.js - process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = options.monorepoRoot; -} - -export function buildNextjsApp(options: buildHelper.BuildOptions) { - const { config, packager } = options; - const command = - config.buildCommand ?? - (["bun", "npm"].includes(packager) ? `${packager} run build` : `${packager} build`); - cp.execSync(command, { - stdio: "inherit", - cwd: path.dirname(options.appPackageJsonPath), - }); -} +export * from "@opennextjs/core/build/buildNextApp.js"; diff --git a/packages/open-next/src/build/compileCache.ts b/packages/open-next/src/build/compileCache.ts index 553bcbf5..b81d7dcf 100644 --- a/packages/open-next/src/build/compileCache.ts +++ b/packages/open-next/src/build/compileCache.ts @@ -1,59 +1 @@ -import path from "node:path"; - -import * as buildHelper from "./helper.js"; - -/** - * Compiles the cache adapter. - * - * @param options Build options. - * @param format Output format. - * @returns An object containing the paths to the compiled cache and composable cache files. - */ -export function compileCache(options: buildHelper.BuildOptions, format: "cjs" | "esm" = "cjs") { - const { config } = options; - const ext = format === "cjs" ? "cjs" : "mjs"; - const compiledCacheFile = path.join(options.buildDir, `cache.${ext}`); - - // Normal cache - buildHelper.esbuildSync( - { - external: ["next", "styled-jsx", "react", "@aws-sdk/*"], - entryPoints: [path.join(options.openNextDistDir, "adapters", "cache.js")], - outfile: compiledCacheFile, - target: ["node18"], - format, - banner: { - js: [ - `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`, - `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`, - ].join(""), - }, - }, - options - ); - - const compiledComposableCacheFile = path.join(options.buildDir, `composable-cache.${ext}`); - - // Composable cache - buildHelper.esbuildSync( - { - external: ["next", "styled-jsx", "react", "@aws-sdk/*"], - entryPoints: [path.join(options.openNextDistDir, "adapters", "composable-cache.js")], - outfile: compiledComposableCacheFile, - target: ["node18"], - format, - banner: { - js: [ - `globalThis.disableIncrementalCache = ${config.dangerous?.disableIncrementalCache ?? false};`, - `globalThis.disableDynamoDBCache = ${config.dangerous?.disableTagCache ?? false};`, - ].join(""), - }, - }, - options - ); - - return { - cache: compiledCacheFile, - composableCache: compiledComposableCacheFile, - }; -} +export * from "@opennextjs/core/build/compileCache.js"; diff --git a/packages/open-next/src/build/compileConfig.ts b/packages/open-next/src/build/compileConfig.ts index d4da6813..e4b3fcf7 100644 --- a/packages/open-next/src/build/compileConfig.ts +++ b/packages/open-next/src/build/compileConfig.ts @@ -1,134 +1 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -import { buildSync } from "esbuild"; - -import type { OpenNextConfig } from "@/types/open-next.js"; - -import logger from "../logger.js"; - -import { validateConfig } from "./validateConfig.js"; - -/** - * Compiles the OpenNext configuration. - * - * The configuration is always compiled for Node.js and for the edge only if needed. - * - * @param openNextConfigPath Path to the configuration file. Absolute or relative to cwd. - * @param nodeExternals Coma separated list of Externals for the Node.js compilation. - * @param compileEdge Force compiling for the edge runtime when true - * @return The configuration and the build directory. - */ -export async function compileOpenNextConfig( - openNextConfigPath: string, - { nodeExternals = "", compileEdge = false } = {} -) { - const buildDir = fs.mkdtempSync(path.join(os.tmpdir(), "open-next-tmp")); - - let configPath = compileOpenNextConfigNode(openNextConfigPath, buildDir, nodeExternals.split(",")); - - // On Windows, we need to use file:// protocol to load the config file using import() - if (process.platform === "win32") configPath = `file://${configPath}`; - const config = (await import(configPath)).default as OpenNextConfig; - if (!config || !config.default) { - logger.error( - "config.default cannot be empty, it should be at least {}, see more info here: https://opennext.js.org/config#configuration-file" - ); - process.exit(1); - } - - validateConfig(config); - - // We need to check if the config uses the edge runtime at any point - // If it does, we need to compile it with the edge runtime - const usesEdgeRuntime = - (config.middleware?.external && config.middleware.runtime !== "node") || - Object.values(config.functions || {}).some((fn) => fn.runtime === "edge"); - if (usesEdgeRuntime || compileEdge) { - compileOpenNextConfigEdge(openNextConfigPath, buildDir, config.edgeExternals ?? []); - } else { - // Skip compiling for the edge runtime. - logger.debug("No edge runtime found in the open-next.config.ts. Using default config."); - } - - return { config, buildDir }; -} - -/** - * Compiles the OpenNext configuration for Node. - * - * @param openNextConfigPath Path to the configuration file. Absolute or relative to cwd. - * @param outputDir Folder where to output the compiled config file (`open-next.config.mjs`). - * @param externals List of packages that should not be bundled. - * @return Path to the compiled config. - */ -export function compileOpenNextConfigNode( - openNextConfigPath: string, - outputDir: string, - externals: string[] -) { - const outputPath = path.join(outputDir, "open-next.config.mjs"); - logger.debug("Compiling open-next.config.ts for Node.", outputPath); - - //Check if open-next.config.ts exists - if (!fs.existsSync(openNextConfigPath)) { - //Create a simple open-next.config.mjs file - logger.debug("Cannot find open-next.config.ts. Using default config."); - fs.writeFileSync(outputPath, "export default { default: { } };"); - } else { - buildSync({ - entryPoints: [openNextConfigPath], - outfile: outputPath, - bundle: true, - format: "esm", - target: ["node18"], - external: externals, - platform: "node", - banner: { - js: [ - "import { createRequire as topLevelCreateRequire } from 'module';", - "const require = topLevelCreateRequire(import.meta.url);", - "import bannerUrl from 'url';", - "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", - ].join(""), - }, - }); - } - - return outputPath; -} - -/** - * Compiles the OpenNext configuration for Edge. - * - * @param openNextConfigPath Path to the configuration file. Absolute or relative to cwd. - * @param outputDir Folder where to output the compiled config file (`open-next.config.edge.mjs`). - * @param externals List of packages that should not be bundled. - * @return Path to the compiled config. - */ -export function compileOpenNextConfigEdge( - openNextConfigPath: string, - outputDir: string, - externals: string[] -) { - const outputPath = path.join(outputDir, "open-next.config.edge.mjs"); - logger.debug("Compiling open-next.config.ts for edge runtime.", outputPath); - - buildSync({ - entryPoints: [openNextConfigPath], - outfile: outputPath, - bundle: true, - format: "esm", - target: ["es2020"], - conditions: ["worker", "browser"], - platform: "browser", - external: externals, - define: { - // with the default esbuild config, the NODE_ENV will be set to "development", we don't want that - "process.env.NODE_ENV": '"production"', - }, - }); - - return outputPath; -} +export * from "@opennextjs/core/build/compileConfig.js"; diff --git a/packages/open-next/src/build/compileTagCacheProvider.ts b/packages/open-next/src/build/compileTagCacheProvider.ts index 4cddb783..8963fc76 100644 --- a/packages/open-next/src/build/compileTagCacheProvider.ts +++ b/packages/open-next/src/build/compileTagCacheProvider.ts @@ -1,34 +1 @@ -import path from "node:path"; - -import { openNextResolvePlugin } from "../plugins/resolve.js"; - -import * as buildHelper from "./helper.js"; -import { installDependencies } from "./installDeps.js"; - -export async function compileTagCacheProvider(options: buildHelper.BuildOptions) { - const providerPath = path.join(options.outputDir, "dynamodb-provider"); - - const overrides = options.config.initializationFunction?.override; - - await buildHelper.esbuildAsync( - { - external: ["@aws-sdk/client-dynamodb"], - entryPoints: [path.join(options.openNextDistDir, "adapters", "dynamo-provider.js")], - outfile: path.join(providerPath, "index.mjs"), - target: ["node18"], - plugins: [ - openNextResolvePlugin({ - fnName: "initializationFunction", - overrides: { - converter: overrides?.converter ?? "dummy", - wrapper: overrides?.wrapper, - tagCache: options.config.initializationFunction?.tagCache, - }, - }), - ], - }, - options - ); - - installDependencies(providerPath, options.config.initializationFunction?.install); -} +export * from "@opennextjs/core/build/compileTagCacheProvider.js"; diff --git a/packages/open-next/src/build/constant.ts b/packages/open-next/src/build/constant.ts index e32f4074..60719069 100644 --- a/packages/open-next/src/build/constant.ts +++ b/packages/open-next/src/build/constant.ts @@ -1,3 +1 @@ -//TODO: Move all other manifest path here as well -export const MIDDLEWARE_TRACE_FILE = "server/middleware.js.nft.json"; -export const INSTRUMENTATION_TRACE_FILE = "server/instrumentation.js.nft.json"; +export * from "@opennextjs/core/build/constant.js"; diff --git a/packages/open-next/src/build/copyAdapterFiles.ts b/packages/open-next/src/build/copyAdapterFiles.ts index 7dd4e8df..d8cca979 100644 --- a/packages/open-next/src/build/copyAdapterFiles.ts +++ b/packages/open-next/src/build/copyAdapterFiles.ts @@ -1,79 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import type { NextAdapterOutput, NextAdapterOutputs } from "../adapter"; -import { addDebugFile } from "../debug.js"; - -import type * as buildHelper from "./helper.js"; - -export async function copyAdapterFiles( - options: buildHelper.BuildOptions, - fnName: string, - packagePath: string, - outputs: NextAdapterOutputs -) { - const filesToCopy = new Map(); - - // Copying the files from outputs to the output dir - for (const [key, value] of Object.entries(outputs)) { - if (["pages", "pagesApi", "appPages", "appRoutes", "middleware"].includes(key)) { - const setFileToCopy = (route: NextAdapterOutput) => { - const assets = route.assets; - // We need to copy the filepaths to the output dir - const relativeFilePath = path.join(packagePath, path.relative(options.appPath, route.filePath)); - filesToCopy.set( - route.filePath, - `${options.outputDir}/server-functions/${fnName}/${relativeFilePath}` - ); - - for (const [relative, from] of Object.entries(assets || {})) { - // console.log("route.assets", from, relative, packagePath); - filesToCopy.set(from as string, `${options.outputDir}/server-functions/${fnName}/${relative}`); - } - }; - if (key === "middleware") { - // Middleware is a single object - setFileToCopy(value as NextAdapterOutput); - } else { - // The rest are arrays - for (const route of value as NextAdapterOutput[]) { - setFileToCopy(route); - // copyFileSync(from, `${options.outputDir}/${relative}`); - } - } - } - } - - console.log("\n### Copying adapter files"); - const debugCopiedFiles: Record = {}; - for (const [from, to] of filesToCopy) { - debugCopiedFiles[from] = to; - - //make sure the directory exists first - fs.mkdirSync(path.dirname(to), { recursive: true }); - // For pnpm symlink we need to do that - // see https://github.com/vercel/next.js/blob/498f342b3552d6fc6f1566a1cc5acea324ce0dec/packages/next/src/build/utils.ts#L1932 - let symlink = ""; - try { - symlink = fs.readlinkSync(from); - } catch (e) { - //Ignore - } - if (symlink) { - try { - fs.symlinkSync(symlink, to); - } catch (e: unknown) { - if (e instanceof Error && (e as NodeJS.ErrnoException).code !== "EEXIST") { - throw e; - } - } - } else { - fs.copyFileSync(from, to); - } - } - - // TODO(vicb): debug - addDebugFile(options, "copied_files.json", debugCopiedFiles); - - return Array.from(filesToCopy.values()); -} +export * from "@opennextjs/core/build/copyAdapterFiles.js"; diff --git a/packages/open-next/src/build/copyTracedFiles.ts b/packages/open-next/src/build/copyTracedFiles.ts index 98710f9a..624162dc 100644 --- a/packages/open-next/src/build/copyTracedFiles.ts +++ b/packages/open-next/src/build/copyTracedFiles.ts @@ -1,407 +1 @@ -import { - chmodSync, - copyFileSync, - cpSync, - existsSync, - mkdirSync, - readFileSync, - readdirSync, - readlinkSync, - statSync, - symlinkSync, - writeFileSync, -} from "node:fs"; -import path from "node:path"; -import url from "node:url"; - -import { - loadAppPathsManifest, - loadBuildId, - loadConfig, - loadFunctionsConfigManifest, - loadMiddlewareManifest, - loadPagesManifest, - loadPrerenderManifest, -} from "@/config/util.js"; -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import logger from "../logger.js"; - -import { INSTRUMENTATION_TRACE_FILE, MIDDLEWARE_TRACE_FILE } from "./constant.js"; - -const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); - -/** - * Copies a file and ensures the destination is writable. - * This is necessary because copyFileSync preserves file permissions, - * and source files may be read-only (e.g., in Bazel's node_modules). - * Without this, subsequent patches would fail with EACCES errors. - */ -export function copyFileAndMakeOwnerWritable(src: string, dest: string): void { - copyFileSync(src, dest); - // Ensure the copied file is writable (add owner write permission) - const stats = statSync(dest); - if (!(stats.mode & 0o200)) { - chmodSync(dest, stats.mode | 0o200); - } -} - -//TODO: we need to figure which packages we could safely remove -const EXCLUDED_PACKAGES = [ - "caniuse-lite", - "sharp", - // This seems to be only in Next 15 - // Some of sharp deps are under the @img scope - "@img", - "typescript", - "next/dist/compiled/babel", - "next/dist/compiled/babel-packages", - "next/dist/compiled/amphtml-validator", -]; - -const NON_LINUX_PLATFORMS = ["darwin", "win32", "freebsd"]; -const platformPattern = NON_LINUX_PLATFORMS.join("|"); -const nonLinuxPlatformRegex = getCrossPlatformPathRegex( - `/node_modules/(?:@[^/]+/)?(?:[^/]+-)?(${platformPattern})-[^/]+/`, - { escape: false } -); - -export function isNonLinuxPlatformPackage(srcPath: string): boolean { - return nonLinuxPlatformRegex.test(srcPath); -} - -export function isExcluded(srcPath: string): boolean { - return EXCLUDED_PACKAGES.some((excluded) => - // `pnpm` can create a symbolic link that points to the pnpm store folder - // This will live under `/node_modules/sharp`. We need to handle this in our regex - srcPath.match( - getCrossPlatformPathRegex(`/node_modules/${excluded}(?:/|$)`, { - escape: false, - }) - ) - ); -} - -function copyPatchFile(outputDir: string) { - const patchFile = path.join(__dirname, "patch", "patchedAsyncStorage.js"); - const outputPatchFile = path.join(outputDir, "patchedAsyncStorage.cjs"); - copyFileAndMakeOwnerWritable(patchFile, outputPatchFile); -} - -interface CopyTracedFilesOptions { - buildOutputPath: string; - packagePath: string; - outputDir: string; - routes: string[]; - skipServerFiles?: boolean; -} - -export function getManifests(nextDir: string) { - return { - buildId: loadBuildId(nextDir), - config: loadConfig(nextDir), - prerenderManifest: loadPrerenderManifest(nextDir), - pagesManifest: loadPagesManifest(nextDir), - appPathsManifest: loadAppPathsManifest(nextDir), - middlewareManifest: loadMiddlewareManifest(nextDir), - functionsConfigManifest: loadFunctionsConfigManifest(nextDir), - }; -} - -export async function copyTracedFiles({ - buildOutputPath, - packagePath, - outputDir, - routes, - skipServerFiles, -}: CopyTracedFilesOptions) { - const tsStart = Date.now(); - const dotNextDir = path.join(buildOutputPath, ".next"); - const standaloneDir = path.join(dotNextDir, "standalone"); - const standaloneNextDir = path.join(standaloneDir, packagePath, ".next"); - const standaloneServerDir = path.join(standaloneNextDir, "server"); - const outputNextDir = path.join(outputDir, packagePath, ".next"); - - // Files to copy - // Map from files in the `.next/standalone` to files in the `.open-next` folder - const filesToCopy = new Map(); - - // Node packages - // Map from folders in the project to folders in the `.open-next` folder - // The map might also include the mono-repo path. - const nodePackages = new Map(); - - /** - * Extracts files and node packages from a .nft.json file - * @param nftFile path to the .nft.json file relative to `.next/` - */ - const processNftFile = (nftFile: string) => { - const subDir = path.dirname(nftFile); - const files: string[] = JSON.parse(readFileSync(path.join(dotNextDir, nftFile), "utf8")).files; - - files.forEach((tracedPath: string) => { - const src = path.join(standaloneNextDir, subDir, tracedPath); - const dst = path.join(outputNextDir, subDir, tracedPath); - filesToCopy.set(src, dst); - - const module = path.join(dotNextDir, subDir, tracedPath); - if (module.endsWith("package.json")) { - nodePackages.set(path.dirname(module), path.dirname(dst)); - } - }); - }; - - // Files necessary by the server - if (!skipServerFiles) { - // On next 14+, we might not have to include those files - // For next 13, we need to include them otherwise we get runtime error - const nftFile = "next-server.js.nft.json"; - - processNftFile(nftFile); - } - // create directory for pages - if (existsSync(path.join(standaloneNextDir, "server/pages"))) { - mkdirSync(path.join(outputNextDir, "server/pages"), { - recursive: true, - }); - } - if (existsSync(path.join(standaloneNextDir, "server/app"))) { - mkdirSync(path.join(outputNextDir, "server/app"), { - recursive: true, - }); - } - - mkdirSync(path.join(outputNextDir, "server/chunks"), { - recursive: true, - }); - - const computeCopyFilesForPage = (pagePath: string) => { - const serverPath = `server/${pagePath}.js`; - - try { - processNftFile(`${serverPath}.nft.json`); - } catch (e) { - if (existsSync(path.join(dotNextDir, serverPath))) { - //TODO: add a link to the docs - throw new Error( - ` --------------------------------------------------------------------------------- -${pagePath} cannot use the edge runtime. -OpenNext requires edge runtime function to be defined in a separate function. -See the docs for more information on how to bundle edge runtime functions. --------------------------------------------------------------------------------- - ` - ); - } - throw new Error(` --------------------------------------------------------------------------------- -We cannot find the route for ${pagePath}. -File ${serverPath} does not exist ---------------------------------------------------------------------------------`); - } - - if (!existsSync(path.join(standaloneNextDir, serverPath))) { - throw new Error( - `This error should only happen for static 404 and 500 page from page router. Report this if that's not the case., - File ${serverPath} does not exist` - ); - } - - filesToCopy.set(path.join(standaloneNextDir, serverPath), path.join(outputNextDir, serverPath)); - }; - - const safeComputeCopyFilesForPage = (pagePath: string, alternativePath?: string) => { - try { - computeCopyFilesForPage(pagePath); - } catch (e) { - if (alternativePath) { - safeComputeCopyFilesForPage(alternativePath); - } - } - }; - - // Check for instrumentation trace file - if (existsSync(path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE))) { - // We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw - copyFileAndMakeOwnerWritable( - path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE), - path.join(standaloneNextDir, INSTRUMENTATION_TRACE_FILE) - ); - computeCopyFilesForPage("instrumentation"); - logger.debug("Adding instrumentation trace files"); - } - - if (existsSync(path.join(dotNextDir, MIDDLEWARE_TRACE_FILE))) { - // We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw - copyFileAndMakeOwnerWritable( - path.join(dotNextDir, MIDDLEWARE_TRACE_FILE), - path.join(standaloneNextDir, MIDDLEWARE_TRACE_FILE) - ); - computeCopyFilesForPage("middleware"); - logger.debug("Adding node middleware trace files"); - } - - const hasPageDir = routes.some((route) => route.startsWith("pages/")); - const hasAppDir = routes.some((route) => route.startsWith("app/")); - - // We need to copy all the base files like _app, _document, _error, etc - // One thing to note, is that next try to load every routes that might be needed in advance - // So if you have a [slug].tsx at the root, this route will always be loaded for 1st level request - // along with _app and _document - if (hasPageDir) { - //Page dir - computeCopyFilesForPage("pages/_app"); - computeCopyFilesForPage("pages/_document"); - computeCopyFilesForPage("pages/_error"); - - // These files can be present or not depending on if the user uses getStaticProps - safeComputeCopyFilesForPage("pages/404"); - safeComputeCopyFilesForPage("pages/500"); - } - - if (hasAppDir) { - //App dir - safeComputeCopyFilesForPage("app/_not-found/page"); - } - - //Files we actually want to include - routes.forEach((route) => { - computeCopyFilesForPage(route); - }); - - // Only files that are actually copied - const tracedFiles: string[] = []; - const erroredFiles: string[] = []; - //Actually copy the files - filesToCopy.forEach((to, from) => { - // We don't want to copy excluded packages (e.g. sharp) - if (isExcluded(from)) { - return; - } - // Skip non-Linux platform-specific native binaries (e.g. @swc/core-darwin-arm64) - if (!process.env.OPEN_NEXT_SKIP_PLATFORM_FILTER && isNonLinuxPlatformPackage(from)) { - const match = from.match(/node_modules\/(.+\/)?([^/]+-(?:darwin|win32|freebsd)-[^/]+)/); - if (match) { - logger.debug(`Skipping non-Linux platform package: ${match[2]}`); - } - return; - } - tracedFiles.push(to); - mkdirSync(path.dirname(to), { recursive: true }); - let symlink = null; - // For pnpm symlink we need to do that - // see https://github.com/vercel/next.js/blob/498f342b3552d6fc6f1566a1cc5acea324ce0dec/packages/next/src/build/utils.ts#L1932 - try { - symlink = readlinkSync(from); - } catch (e) { - //Ignore - } - if (symlink) { - try { - symlinkSync(symlink, to); - } catch (e: unknown) { - if (e instanceof Error && (e as NodeJS.ErrnoException).code !== "EEXIST") { - throw e; - } - } - } else { - // Adding this inside a try-catch to handle errors on Next 16+ - // where some files listed in the .nft.json might not be present in the standalone folder - // TODO: investigate that further - is it expected? - try { - copyFileAndMakeOwnerWritable(from, to); - } catch (e) { - logger.debug("Error copying file:", e); - erroredFiles.push(to); - } - } - }); - - readdirSync(standaloneNextDir) - .filter((fileOrDir) => !statSync(path.join(standaloneNextDir, fileOrDir)).isDirectory()) - .forEach((file) => { - copyFileAndMakeOwnerWritable(path.join(standaloneNextDir, file), path.join(outputNextDir, file)); - tracedFiles.push(path.join(outputNextDir, file)); - }); - - // We then need to copy all the files at the root of server - - mkdirSync(path.join(outputNextDir, "server"), { recursive: true }); - - readdirSync(standaloneServerDir) - .filter((fileOrDir) => !statSync(path.join(standaloneServerDir, fileOrDir)).isDirectory()) - .filter((file) => file !== "server.js") - .forEach((file) => { - copyFileAndMakeOwnerWritable( - path.join(standaloneServerDir, file), - path.join(path.join(outputNextDir, "server"), file) - ); - tracedFiles.push(path.join(outputNextDir, "server", file)); - }); - - // Copy patch file - copyPatchFile(path.join(outputDir, packagePath)); - - // TODO: Recompute all the files. - // vercel doesn't seem to do it, but it seems wasteful to have all those files - // we replace the pages-manifest.json with an empty one if we don't have a pages dir so that - // next doesn't try to load _app, _document - if (!hasPageDir) { - writeFileSync(path.join(outputNextDir, "server/pages-manifest.json"), "{}"); - } - - //TODO: Find what else we need to copy - const copyStaticFile = (filePath: string) => { - if (existsSync(path.join(standaloneNextDir, filePath))) { - mkdirSync(path.dirname(path.join(outputNextDir, filePath)), { - recursive: true, - }); - copyFileAndMakeOwnerWritable( - path.join(standaloneNextDir, filePath), - path.join(outputNextDir, filePath) - ); - } - }; - - const manifests = getManifests(standaloneNextDir); - const { config, prerenderManifest, pagesManifest } = manifests; - - // Get all the static files - Should be only for pages dir - // Ideally we would filter only those that might get accessed in this specific functions - // Maybe even move this to s3 directly - if (hasPageDir) { - // First we get truly static files - i.e. pages without getStaticProps - const staticFiles: Array = Object.values(pagesManifest); - // Then we need to get all fallback: true dynamic routes html - const locales = config.i18n?.locales; - Object.values(prerenderManifest?.dynamicRoutes ?? {}).forEach((route) => { - if (typeof route.fallback === "string") { - if (locales) { - locales.forEach((locale) => { - staticFiles.push(`pages/${locale}${route.fallback}`); - }); - } else { - staticFiles.push(`pages${route.fallback}`); - } - } - }); - - staticFiles.filter((file) => file.endsWith(".html")).forEach((file) => copyStaticFile(`server/${file}`)); - } - - // Copy .next/static/css from standalone to output dir - // needed for optimizeCss feature to work - if (config.experimental.optimizeCss) { - cpSync(path.join(standaloneNextDir, "static", "css"), path.join(outputNextDir, "static", "css"), { - recursive: true, - }); - } - - logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms"); - - return { - tracedFiles: tracedFiles.filter((f) => !erroredFiles.includes(f)), - nodePackages, - manifests, - }; -} +export * from "@opennextjs/core/build/copyTracedFiles.js"; diff --git a/packages/open-next/src/build/createAssets.ts b/packages/open-next/src/build/createAssets.ts index 4ad81bbd..ad010175 100644 --- a/packages/open-next/src/build/createAssets.ts +++ b/packages/open-next/src/build/createAssets.ts @@ -1,274 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import { loadConfig } from "@/config/util.js"; -import { safeParseJsonFile } from "@/utils/safe-json-parse.js"; - -import logger from "../logger.js"; -import type { TagCacheMetaFile } from "../types/cache.js"; -import { isBinaryContentType } from "../utils/binary.js"; - -import * as buildHelper from "./helper.js"; - -type CacheFileMeta = { - segmentPaths?: string[]; - headers?: Record; -}; - -type FetchCacheData = { - tags?: string[]; -}; - -/** - * Copy the static assets to the output folder - * - * WARNING: `useBasePath` should be set to `false` when the output file is used. - * - * @param options OpenNext build options - * @param useBasePath whether to copy files into the to Next.js configured basePath - */ -export function createStaticAssets(options: buildHelper.BuildOptions, { useBasePath = false } = {}) { - logger.info("Bundling static assets..."); - - const { appBuildOutputPath, appPublicPath, outputDir, appPath } = options; - - const NextConfig = loadConfig(path.join(appBuildOutputPath, ".next")); - const basePath = useBasePath ? (NextConfig.basePath ?? "") : ""; - - // Create output folder - const outputPath = path.join(outputDir, "assets", basePath); - fs.mkdirSync(outputPath, { recursive: true }); - - /** - * Next.js outputs assets into multiple files. - * - * Copy into the same directory: - * - `.open-next/assets` when `useBasePath` is `false` - * - `.open-next/assets/basePath` when `useBasePath` is `true` - * - * Copy over: - * - .next/BUILD_ID => BUILD_ID - * - .next/static => _next/static - * - public/* => * - * - app/favicon.ico or src/app/favicon.ico => favicon.ico - * - * Note: BUILD_ID is used by the SST infra. - */ - fs.copyFileSync(path.join(appBuildOutputPath, ".next/BUILD_ID"), path.join(outputPath, "BUILD_ID")); - - fs.cpSync(path.join(appBuildOutputPath, ".next/static"), path.join(outputPath, "_next", "static"), { - recursive: true, - }); - if (fs.existsSync(appPublicPath)) { - fs.cpSync(appPublicPath, outputPath, { recursive: true }); - } - - const appSrcPath = fs.existsSync(path.join(appPath, "src")) ? "src/app" : "app"; - - const faviconPath = path.join(appPath, appSrcPath, "favicon.ico"); - - // We need to check if the favicon is either a file or directory. - // If it's a directory, we assume it's a route handler and ignore it. - if (fs.existsSync(faviconPath) && fs.lstatSync(faviconPath).isFile()) { - fs.copyFileSync(faviconPath, path.join(outputPath, "favicon.ico")); - } -} - -/** - * Create the cache assets. - * - * @param options Build options. - * @returns Whether the tag cache is used, and the meta files collected. - */ -export function createCacheAssets(options: buildHelper.BuildOptions) { - logger.info("Bundling cache assets..."); - - const { appBuildOutputPath, outputDir } = options; - const packagePath = buildHelper.getPackagePath(options); - const buildId = buildHelper.getBuildId(options); - let useTagCache = false; - - const dotNextPath = appBuildOutputPath; - - const outputCachePath = path.join(outputDir, "cache", buildId); - fs.mkdirSync(outputCachePath, { recursive: true }); - - const sourceDirs = [".next/server/pages", ".next/server/app"] - .map((dir) => path.join(dotNextPath, dir)) - .filter(fs.existsSync); - - const htmlPages = buildHelper.getHtmlPages(dotNextPath); - - const isFileSkipped = (relativePath: string) => - relativePath.endsWith(".js") || - relativePath.endsWith(".js.nft.json") || - // We skip manifest files as well - relativePath.endsWith("-manifest.json") || - // We skip the segment rsc files as they are treated in a different way - relativePath.endsWith(".segment.rsc") || - (relativePath.endsWith(".html") && htmlPages.has(relativePath)); - - // Merge cache files into a single file - const cacheFilesPath: Record< - string, - { - meta?: string; - html?: string; - json?: string; - rsc?: string; - body?: string; - } - > = {}; - - // Process each source directory - sourceDirs.forEach((sourceDir) => { - buildHelper.traverseFiles( - sourceDir, - ({ relativePath }) => !isFileSkipped(relativePath), - ({ absolutePath, relativePath }) => { - const ext = path.extname(absolutePath); - switch (ext) { - case ".meta": - case ".html": - case ".json": - case ".body": - case ".rsc": { - const newFilePath = path - .join(outputCachePath, relativePath) - .substring(0, path.join(outputCachePath, relativePath).length - ext.length) - .replace(/\.prefetch$/, "") - .concat(".cache"); - - cacheFilesPath[newFilePath] = { - [ext.slice(1)]: absolutePath, - ...cacheFilesPath[newFilePath], - }; - break; - } - case ".map": - break; - default: - logger.warn(`Unknown file extension: ${ext}`); - break; - } - } - ); - }); - - // Generate cache file - Object.entries(cacheFilesPath).forEach(([cacheFilePath, files]) => { - const cacheFileMeta = files.meta - ? safeParseJsonFile(fs.readFileSync(files.meta, "utf8"), cacheFilePath) - : undefined; - const cacheJson = files.json - ? safeParseJsonFile>(fs.readFileSync(files.json, "utf8"), cacheFilePath) - : undefined; - if ((files.meta && !cacheFileMeta) || (files.json && !cacheJson)) { - logger.warn(`Skipping invalid cache file: ${cacheFilePath}`); - return; - } - - // If we have a meta file, and it contains segmentPaths, we need to add them to the cache file - const segments: Record = Array.isArray(cacheFileMeta?.segmentPaths) - ? Object.fromEntries( - cacheFileMeta!.segmentPaths.map((segmentPath: string) => { - const absoluteSegmentPath = path.join( - files.meta!.replace(/\.meta$/, ".segments"), - `${segmentPath}.segment.rsc` - ); - const segmentContent = fs.readFileSync(absoluteSegmentPath, "utf8"); - return [segmentPath, segmentContent]; - }) - ) - : {}; - - const cacheFileContent = { - type: files.body ? "route" : files.json ? "page" : "app", - meta: cacheFileMeta, - html: files.html ? fs.readFileSync(files.html, "utf8") : undefined, - json: cacheJson, - rsc: files.rsc ? fs.readFileSync(files.rsc, "utf8") : undefined, - body: files.body - ? fs - .readFileSync(files.body) - .toString(isBinaryContentType(cacheFileMeta?.headers?.["content-type"]) ? "base64" : "utf8") - : undefined, - segmentData: Object.keys(segments).length > 0 ? segments : undefined, - }; - - // Ensure directory exists before writing - fs.mkdirSync(path.dirname(cacheFilePath), { recursive: true }); - fs.writeFileSync(cacheFilePath, JSON.stringify(cacheFileContent)); - }); - - // We need to traverse the cache to find every .meta file - const metaFiles: TagCacheMetaFile[] = []; - - // Copy fetch-cache to cache folder - const fetchCachePath = path.join(appBuildOutputPath, ".next/cache/fetch-cache"); - if (fs.existsSync(fetchCachePath)) { - const fetchOutputPath = path.join(outputDir, "cache", "__fetch", buildId); - fs.mkdirSync(fetchOutputPath, { recursive: true }); - fs.cpSync(fetchCachePath, fetchOutputPath, { recursive: true }); - - buildHelper.traverseFiles( - fetchCachePath, - () => true, - ({ absolutePath, relativePath }) => { - const fileContent = fs.readFileSync(absolutePath, "utf8"); - const fileData = safeParseJsonFile(fileContent, absolutePath); - fileData?.tags?.forEach((tag: string) => { - metaFiles.push({ - tag: { S: path.posix.join(buildId, tag) }, - path: { - S: path.posix.join(buildId, relativePath), - }, - revalidatedAt: { N: "1" }, - }); - }); - } - ); - } - - if (!options.config.dangerous?.disableTagCache) { - // Compute dynamodb cache data - // Traverse files inside cache to find all meta files and cache tags associated with them - sourceDirs.forEach((sourceDir) => { - buildHelper.traverseFiles( - sourceDir, - ({ absolutePath, relativePath }) => absolutePath.endsWith(".meta") && !isFileSkipped(relativePath), - ({ absolutePath, relativePath }) => { - const fileContent = fs.readFileSync(absolutePath, "utf8"); - const fileData = safeParseJsonFile(fileContent, absolutePath); - if (fileData?.headers?.["x-next-cache-tags"]) { - fileData.headers["x-next-cache-tags"].split(",").forEach((tag: string) => { - // TODO: We should split the tag using getDerivedTags from next.js or maybe use an in house implementation - metaFiles.push({ - tag: { S: path.posix.join(buildId, tag.trim()) }, - path: { - S: path.posix.join(buildId, relativePath.replace(".meta", "")), - }, - // We don't care about the revalidation time here, we just need to make sure it's there - revalidatedAt: { N: "1" }, - }); - }); - } - } - ); - }); - - if (metaFiles.length > 0) { - useTagCache = true; - const providerPath = path.join(outputDir, "dynamodb-provider"); - - // Copy open-next.config.mjs into the bundle - fs.mkdirSync(providerPath, { recursive: true }); - buildHelper.copyOpenNextConfig(options.buildDir, providerPath); - - // TODO: check if metafiles doesn't contain duplicates - fs.writeFileSync(path.join(providerPath, "dynamodb-cache.json"), JSON.stringify(metaFiles)); - } - } - - return { useTagCache, metaFiles }; -} +export * from "@opennextjs/core/build/createAssets.js"; diff --git a/packages/open-next/src/build/createImageOptimizationBundle.ts b/packages/open-next/src/build/createImageOptimizationBundle.ts index f8b1e438..a9af6747 100644 --- a/packages/open-next/src/build/createImageOptimizationBundle.ts +++ b/packages/open-next/src/build/createImageOptimizationBundle.ts @@ -1,100 +1 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -import logger from "../logger.js"; -import { openNextResolvePlugin } from "../plugins/resolve.js"; - -import * as buildHelper from "./helper.js"; -import { installDependencies } from "./installDeps.js"; - -export async function createImageOptimizationBundle(options: buildHelper.BuildOptions) { - logger.info("Bundling image optimization function..."); - - const { appBuildOutputPath, config, outputDir } = options; - - // Create output folder - const outputPath = path.join(outputDir, "image-optimization-function"); - fs.mkdirSync(outputPath, { recursive: true }); - - // Copy open-next.config.mjs into the bundle - buildHelper.copyOpenNextConfig(options.buildDir, outputPath); - - const plugins = [ - openNextResolvePlugin({ - fnName: "imageOptimization", - overrides: { - converter: config.imageOptimization?.override?.converter, - wrapper: config.imageOptimization?.override?.wrapper, - imageLoader: config.imageOptimization?.loader, - }, - }), - ]; - - // Build Lambda code (1st pass) - // note: bundle in OpenNext package b/c the adapter relies on the - // "@aws-sdk/client-s3" package which is not a dependency in user's - // Next.js app. - await buildHelper.esbuildAsync( - { - entryPoints: [path.join(options.openNextDistDir, "adapters", "image-optimization-adapter.js")], - external: ["sharp", "next"], - outfile: path.join(outputPath, "index.mjs"), - plugins, - }, - options - ); - - // Build Lambda code (2nd pass) - // note: bundle in user's Next.js app again b/c the adapter relies on the - // "next" package. And the "next" package from user's app should - // be used. We also set @opentelemetry/api as external because it seems to be - // required by Next 15 even though it's not used. - buildHelper.esbuildSync( - { - entryPoints: [path.join(outputPath, "index.mjs")], - external: ["sharp", "@opentelemetry/api"], - allowOverwrite: true, - outfile: path.join(outputPath, "index.mjs"), - banner: { - js: [ - "import { createRequire as topLevelCreateRequire } from 'module';", - "const require = topLevelCreateRequire(import.meta.url);", - "import bannerUrl from 'url';", - "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", - ].join("\n"), - }, - }, - options - ); - - // Copy over .next/required-server-files.json file and BUILD_ID - fs.mkdirSync(path.join(outputPath, ".next")); - fs.copyFileSync( - path.join(appBuildOutputPath, ".next/required-server-files.json"), - path.join(outputPath, ".next/required-server-files.json") - ); - fs.copyFileSync(path.join(appBuildOutputPath, ".next/BUILD_ID"), path.join(outputPath, ".next/BUILD_ID")); - - // Sharp provides pre-build binaries for all platforms. https://github.com/lovell/sharp/blob/main/docs/install.md#cross-platform - // Target should be same as used by Lambda, see https://github.com/sst/sst/blob/ca6f763fdfddd099ce2260202d0ce48c72e211ea/packages/sst/src/constructs/NextjsSite.ts#L114 - // For SHARP_IGNORE_GLOBAL_LIBVIPS see: https://github.com/lovell/sharp/blob/main/docs/install.md#aws-lambda - - const sharpVersion = process.env.SHARP_VERSION ?? "0.32.6"; - - // In development, we want to use the local machine settings - const isDev = config.imageOptimization?.loader === "fs-dev"; - - installDependencies( - outputPath, - config.imageOptimization?.install ?? { - packages: [`sharp@${sharpVersion}`], - // By not specifying an arch in dev, `npm install` will choose one for us (i.e. our system one) - arch: isDev ? undefined : "arm64", - // Use the local platform in dev - os: isDev ? os.platform() : "linux", - nodeVersion: "18", - libc: "glibc", - } - ); -} +export * from "@opennextjs/core/build/createImageOptimizationBundle.js"; diff --git a/packages/open-next/src/build/createMiddleware.ts b/packages/open-next/src/build/createMiddleware.ts index 6c1de5e8..b609f88c 100644 --- a/packages/open-next/src/build/createMiddleware.ts +++ b/packages/open-next/src/build/createMiddleware.ts @@ -1,87 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import { loadFunctionsConfigManifest, loadMiddlewareManifest } from "@/config/util.js"; - -import logger from "../logger.js"; -import type { MiddlewareInfo } from "../types/next-types.js"; - -import { buildEdgeBundle, copyMiddlewareResources } from "./edge/createEdgeBundle.js"; -import * as buildHelper from "./helper.js"; -import { installDependencies } from "./installDeps.js"; -import { buildBundledNodeMiddleware, buildExternalNodeMiddleware } from "./middleware/buildNodeMiddleware.js"; - -/** - * Compiles the middleware bundle. - * - * @param options Build Options. - * @param forceOnlyBuildOnce force to build only once. - */ -export async function createMiddleware( - options: buildHelper.BuildOptions, - { forceOnlyBuildOnce = false } = {} -) { - logger.info("Bundling middleware function..."); - - const { config, outputDir } = options; - const buildOutputDotNextDir = path.join(options.appBuildOutputPath, ".next"); - - // Get middleware manifest - const middlewareManifest = loadMiddlewareManifest(buildOutputDotNextDir); - - const edgeMiddlewareInfo = middlewareManifest.middleware["/"] as MiddlewareInfo | undefined; - - if (!edgeMiddlewareInfo) { - // If there is no middleware info, it might be a node middleware - const functionsConfigManifest = loadFunctionsConfigManifest(buildOutputDotNextDir); - - if (functionsConfigManifest?.functions["/_middleware"]) { - await (config.middleware?.external - ? buildExternalNodeMiddleware(options) - : buildBundledNodeMiddleware(options)); - return; - } - } - - if (config.middleware?.external) { - const outputPath = path.join(outputDir, "middleware"); - copyMiddlewareResources(options, edgeMiddlewareInfo, outputPath); - - fs.mkdirSync(outputPath, { recursive: true }); - - // Copy open-next.config.mjs - buildHelper.copyOpenNextConfig( - options.buildDir, - outputPath, - await buildHelper.isEdgeRuntime(config.middleware.override) - ); - - // Bundle middleware - await buildEdgeBundle({ - entrypoint: path.join(options.openNextDistDir, "adapters", "middleware.js"), - outfile: path.join(outputPath, "handler.mjs"), - middlewareInfo: edgeMiddlewareInfo, - options, - overrides: { - ...config.middleware.override, - originResolver: config.middleware.originResolver, - }, - defaultConverter: "aws-cloudfront", - additionalExternals: config.edgeExternals, - onlyBuildOnce: forceOnlyBuildOnce === true, - name: "middleware", - }); - - installDependencies(outputPath, config.middleware?.install); - } else { - await buildEdgeBundle({ - entrypoint: path.join(options.openNextDistDir, "core", "edgeFunctionHandler.js"), - outfile: path.join(options.buildDir, "middleware.mjs"), - middlewareInfo: edgeMiddlewareInfo, - options, - overrides: config.default.override, - onlyBuildOnce: true, - name: "middleware", - }); - } -} +export * from "@opennextjs/core/build/createMiddleware.js"; diff --git a/packages/open-next/src/build/createRevalidationBundle.ts b/packages/open-next/src/build/createRevalidationBundle.ts index 38dc9991..117c0bb4 100644 --- a/packages/open-next/src/build/createRevalidationBundle.ts +++ b/packages/open-next/src/build/createRevalidationBundle.ts @@ -1,48 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import logger from "../logger.js"; -import { openNextResolvePlugin } from "../plugins/resolve.js"; - -import * as buildHelper from "./helper.js"; -import { installDependencies } from "./installDeps.js"; - -export async function createRevalidationBundle(options: buildHelper.BuildOptions) { - logger.info("Bundling revalidation function..."); - - const { appBuildOutputPath, config, outputDir } = options; - - // Create output folder - const outputPath = path.join(outputDir, "revalidation-function"); - fs.mkdirSync(outputPath, { recursive: true }); - - //Copy open-next.config.mjs into the bundle - buildHelper.copyOpenNextConfig(options.buildDir, outputPath); - - // Build Lambda code - await buildHelper.esbuildAsync( - { - external: ["next", "styled-jsx", "react"], - entryPoints: [path.join(options.openNextDistDir, "adapters", "revalidate.js")], - outfile: path.join(outputPath, "index.mjs"), - plugins: [ - openNextResolvePlugin({ - fnName: "revalidate", - overrides: { - converter: config.revalidate?.override?.converter ?? "sqs-revalidate", - wrapper: config.revalidate?.override?.wrapper, - }, - }), - ], - }, - options - ); - - installDependencies(outputPath, config.revalidate?.install); - - // Copy over .next/prerender-manifest.json file - fs.copyFileSync( - path.join(appBuildOutputPath, ".next", "prerender-manifest.json"), - path.join(outputPath, "prerender-manifest.json") - ); -} +export * from "@opennextjs/core/build/createRevalidationBundle.js"; diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 694015d8..56fa25ca 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -1,324 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import type { Plugin } from "esbuild"; - -import { loadMiddlewareManifest } from "@/config/util.js"; -import type { FunctionOptions, SplittedFunctionOptions } from "@/types/open-next"; - -import type { NextAdapterOutputs } from "../adapter.js"; -import logger from "../logger.js"; -import { minifyAll } from "../minimize-js.js"; -import { ContentUpdater } from "../plugins/content-updater.js"; -import { openNextReplacementPlugin } from "../plugins/replacement.js"; -import { openNextResolvePlugin } from "../plugins/resolve.js"; -import { getCrossPlatformPathRegex } from "../utils/regex.js"; - -import { compileCache } from "./compileCache.js"; -import { copyAdapterFiles } from "./copyAdapterFiles.js"; -import { getManifests } from "./copyTracedFiles.js"; -import { copyMiddlewareResources, generateEdgeBundle } from "./edge/createEdgeBundle.js"; -import * as buildHelper from "./helper.js"; -import { installDependencies } from "./installDeps.js"; -import { type CodePatcher, applyCodePatches } from "./patch/codePatcher.js"; -import * as patches from "./patch/patches/index.js"; - -interface CodeCustomization { - // These patches are meant to apply on user and next generated code - additionalCodePatches?: CodePatcher[]; - // These plugins are meant to apply during the esbuild bundling process. - // This will only apply to OpenNext code. - additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[]; -} - -export async function createServerBundle( - options: buildHelper.BuildOptions, - codeCustomization?: CodeCustomization, - nextOutputs?: NextAdapterOutputs -) { - 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, codeCustomization, nextOutputs); - } - }); - - //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 } = options; - - // Find remaining routes - const serverPath = path.join( - appBuildOutputPath, - ".next/standalone", - buildHelper.getPackagePath(options), - ".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: ["*"], - }, - codeCustomization, - nextOutputs - ); -} - -async function generateBundle( - name: string, - options: buildHelper.BuildOptions, - fnOptions: SplittedFunctionOptions, - codeCustomization?: CodeCustomization, - nextOutputs?: NextAdapterOutputs -) { - 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 = buildHelper.getPackagePath(options); - const outPackagePath = path.join(outputPath, packagePath); - - fs.mkdirSync(outPackagePath, { recursive: true }); - - const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs"; - // Normal cache - fs.copyFileSync(path.join(options.buildDir, `cache.${ext}`), path.join(outPackagePath, "cache.cjs")); - // Composable cache - fs.copyFileSync( - path.join(options.buildDir, `composable-cache.${ext}`), - path.join(outPackagePath, "composable-cache.cjs") - ); - - if (fnOptions.runtime === "deno") { - addDenoJson(outputPath, packagePath); - } - - // Copy middleware - if (!config.middleware?.external) { - fs.copyFileSync( - path.join(options.buildDir, "middleware.mjs"), - path.join(outPackagePath, "middleware.mjs") - ); - - const middlewareManifest = loadMiddlewareManifest(path.join(options.appBuildOutputPath, ".next")); - - copyMiddlewareResources(options, middlewareManifest.middleware["/"], outPackagePath); - } - - // Copy open-next.config.mjs - buildHelper.copyOpenNextConfig(options.buildDir, outPackagePath); - - // Copy env files - buildHelper.copyEnvFile(appBuildOutputPath, packagePath, outputPath); - - let tracedFiles: string[] = []; - let manifests: ReturnType = {} as ReturnType; - - // Copy all necessary traced files - if (!nextOutputs) { - throw new Error( - "createServerBundle was called without adapter outputs. " + - "Please ensure NextAdapterOutputs is provided to createServerBundle." - ); - } - tracedFiles = await copyAdapterFiles(options, name, packagePath, nextOutputs); - //TODO: we should load manifests here - - const additionalCodePatches = codeCustomization?.additionalCodePatches ?? []; - - await applyCodePatches(options, tracedFiles, manifests as ReturnType, [ - patches.patchFetchCacheSetMissingWaitUntil, - patches.patchFetchCacheForISR, - patches.patchUnstableCacheForISR, - patches.patchNextServer, - patches.getEnvVarsPatch(options), - patches.patchBackgroundRevalidation, - patches.patchUseCacheForISR, - patches.patchNodeEnvironment, - ...additionalCodePatches, - ]); - - // 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 overrides = fnOptions.override ?? {}; - - const disableRouting = config.middleware?.external; - - const updater = new ContentUpdater(options); - - const additionalPlugins = codeCustomization?.additionalPlugins - ? codeCustomization.additionalPlugins(updater) - : []; - - const plugins = [ - openNextReplacementPlugin({ - name: `requestHandlerOverride ${name}`, - target: getCrossPlatformPathRegex("core/requestHandler.js"), - deletes: disableRouting ? ["withRouting"] : [], - }), - - openNextResolvePlugin({ - fnName: name, - overrides, - }), - ...additionalPlugins, - // The content updater plugin must be the last plugin - updater.plugin, - ]; - - const outfileExt = fnOptions.runtime === "deno" ? "ts" : "mjs"; - await buildHelper.esbuildAsync( - { - entryPoints: [path.join(options.openNextDistDir, "adapters", "server-adapter.js")], - external: ["next", "./middleware.mjs", "./next-server.runtime.prod.js"], - outfile: path.join(outputPath, packagePath, `index.${outfileExt}`), - banner: { - js: [ - `globalThis.monorepoPackagePath = "${packagePath}";`, - "import process from 'node:process';", - "import { Buffer } from 'node:buffer';", - "import { createRequire as topLevelCreateRequire } from 'module';", - "const require = topLevelCreateRequire(import.meta.url);", - "import bannerUrl from 'url';", - "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", - "const __filename = bannerUrl.fileURLToPath(import.meta.url);", - name === "default" ? "" : `globalThis.fnName = "${name}";`, - ].join(""), - }, - plugins, - }, - 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. - - // 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 { handler } from "./${packagePosixPath}/index.mjs";` - ); -} - -async function minifyServerBundle(outputDir: string) { - logger.info("Minimizing server function..."); - - await minifyAll(outputDir, { - compress_json: true, - mangle: true, - }); -} +export * from "@opennextjs/core/build/createServerBundle.js"; diff --git a/packages/open-next/src/build/createWarmerBundle.ts b/packages/open-next/src/build/createWarmerBundle.ts index a0ed877a..90024cf4 100644 --- a/packages/open-next/src/build/createWarmerBundle.ts +++ b/packages/open-next/src/build/createWarmerBundle.ts @@ -1,53 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import logger from "../logger.js"; -import { openNextResolvePlugin } from "../plugins/resolve.js"; - -import * as buildHelper from "./helper.js"; -import { installDependencies } from "./installDeps.js"; - -export async function createWarmerBundle(options: buildHelper.BuildOptions) { - logger.info("Bundling warmer function..."); - - const { config, outputDir } = options; - - // Create output folder - const outputPath = path.join(outputDir, "warmer-function"); - fs.mkdirSync(outputPath, { recursive: true }); - - // Copy open-next.config.mjs into the bundle - buildHelper.copyOpenNextConfig(options.buildDir, outputPath); - - // Build Lambda code - // note: bundle in OpenNext package b/c the adatper relys on the - // "serverless-http" package which is not a dependency in user's - // Next.js app. - await buildHelper.esbuildAsync( - { - entryPoints: [path.join(options.openNextDistDir, "adapters", "warmer-function.js")], - external: ["next"], - outfile: path.join(outputPath, "index.mjs"), - plugins: [ - openNextResolvePlugin({ - overrides: { - converter: config.warmer?.override?.converter ?? "dummy", - wrapper: config.warmer?.override?.wrapper, - }, - fnName: "warmer", - }), - ], - banner: { - js: [ - "import { createRequire as topLevelCreateRequire } from 'module';", - "const require = topLevelCreateRequire(import.meta.url);", - "import bannerUrl from 'url';", - "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", - ].join(""), - }, - }, - options - ); - - installDependencies(outputPath, config.warmer?.install); -} +export * from "@opennextjs/core/build/createWarmerBundle.js"; diff --git a/packages/open-next/src/build/edge/createEdgeBundle.ts b/packages/open-next/src/build/edge/createEdgeBundle.ts index 5d7370aa..7be2f83d 100644 --- a/packages/open-next/src/build/edge/createEdgeBundle.ts +++ b/packages/open-next/src/build/edge/createEdgeBundle.ts @@ -1,233 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import { type Plugin, build } from "esbuild"; - -import { loadMiddlewareManifest } from "@/config/util.js"; -import type { MiddlewareInfo } from "@/types/next-types"; -import type { - IncludedConverter, - IncludedOriginResolver, - LazyLoadedOverride, - OverrideOptions, - RouteTemplate, - SplittedFunctionOptions, -} from "@/types/open-next"; -import type { OriginResolver } from "@/types/overrides.js"; - -import logger from "../../logger.js"; -import { ContentUpdater } from "../../plugins/content-updater.js"; -import { openNextEdgePlugins } from "../../plugins/edge.js"; -import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js"; -import { openNextReplacementPlugin } from "../../plugins/replacement.js"; -import { openNextResolvePlugin } from "../../plugins/resolve.js"; -import { getCrossPlatformPathRegex } from "../../utils/regex.js"; -import { type BuildOptions, isEdgeRuntime, copyOpenNextConfig, esbuildAsync } from "../helper.js"; - -type Override = OverrideOptions & { - originResolver?: LazyLoadedOverride | IncludedOriginResolver; -}; -interface BuildEdgeBundleOptions { - middlewareInfo?: MiddlewareInfo; - entrypoint: string; - outfile: string; - options: BuildOptions; - overrides?: Override; - defaultConverter?: IncludedConverter; - additionalInject?: string; - additionalExternals?: string[]; - onlyBuildOnce?: boolean; - name: string; - additionalPlugins?: (contentUpdater: ContentUpdater) => Plugin[]; -} - -export async function buildEdgeBundle({ - middlewareInfo, - entrypoint, - outfile, - options, - defaultConverter, - overrides, - additionalInject, - additionalExternals, - onlyBuildOnce, - name, - additionalPlugins: additionalPluginsFn, -}: BuildEdgeBundleOptions) { - const isInCloudflare = await isEdgeRuntime(overrides); - function override(target: T) { - return typeof overrides?.[target] === "string" || typeof overrides?.[target] === "function" - ? overrides[target] - : undefined; - } - const contentUpdater = new ContentUpdater(options); - const additionalPlugins = additionalPluginsFn ? additionalPluginsFn(contentUpdater) : []; - await esbuildAsync( - { - entryPoints: [entrypoint], - bundle: true, - outfile, - external: ["node:*", "next", "@aws-sdk/*"], - target: "es2022", - platform: "neutral", - plugins: [ - openNextResolvePlugin({ - overrides: { - wrapper: override("wrapper") ?? "aws-lambda", - converter: override("converter") ?? defaultConverter, - tagCache: override("tagCache") ?? "dynamodb-lite", - incrementalCache: override("incrementalCache") ?? "s3-lite", - queue: override("queue") ?? "sqs-lite", - originResolver: override("originResolver") ?? "pattern-env", - proxyExternalRequest: override("proxyExternalRequest") ?? "node", - }, - fnName: name, - }), - openNextExternalMiddlewarePlugin(path.join(options.openNextDistDir, "core/edgeFunctionHandler.js")), - openNextEdgePlugins({ - middlewareInfo, - nextDir: path.join(options.appBuildOutputPath, ".next"), - isInCloudflare, - }), - ...additionalPlugins, - // The content updater plugin must be the last plugin - contentUpdater.plugin, - ], - treeShaking: true, - alias: { - path: "node:path", - stream: "node:stream", - fs: "node:fs", - }, - conditions: ["module"], - mainFields: ["module", "main"], - banner: { - js: ` -import {Buffer} from "node:buffer"; -globalThis.Buffer = Buffer; - -import {AsyncLocalStorage} from "node:async_hooks"; -globalThis.AsyncLocalStorage = AsyncLocalStorage; - -${ - "" - /** - * Next.js sets this `__import_unsupported` on `globalThis` (with `configurable: false`): - * https://github.com/vercel/next.js/blob/5b7833e3/packages/next/src/server/web/globals.ts#L94-L98 - * - * It does so in both the middleware and the main server, so if the middleware runs in the same place - * as the main handler this code gets run twice triggering a runtime error. - * - * For this reason we need to patch `Object.defineProperty` to avoid this issue. - */ -} -const defaultDefineProperty = Object.defineProperty; -Object.defineProperty = function(o, p, a) { - if(p=== '__import_unsupported' && Boolean(globalThis.__import_unsupported)) { - return; - } - return defaultDefineProperty(o, p, a); -}; - - ${ - isInCloudflare - ? "" - : ` - const require = (await import("node:module")).createRequire(import.meta.url); - const __filename = (await import("node:url")).fileURLToPath(import.meta.url); - const __dirname = (await import("node:path")).dirname(__filename); - ` - } - ${additionalInject ?? ""} - `, - }, - }, - options - ); - - if (!onlyBuildOnce) { - await build({ - entryPoints: [outfile], - outfile, - allowOverwrite: true, - bundle: true, - minify: options.minify, - platform: "node", - format: "esm", - conditions: ["workerd", "worker", "browser"], - external: ["node:*", ...(additionalExternals ?? [])], - banner: { - js: 'import * as process from "node:process";', - }, - }); - } -} - -export async function generateEdgeBundle( - name: string, - options: BuildOptions, - fnOptions: SplittedFunctionOptions, - additionalPlugins: (contentUpdater: ContentUpdater) => Plugin[] = () => [] -) { - logger.info(`Generating edge bundle for: ${name}`); - - const buildOutputDotNextDir = path.join(options.appBuildOutputPath, ".next"); - - // Create output folder - const outputDir = path.join(options.outputDir, "server-functions", name); - fs.mkdirSync(outputDir, { recursive: true }); - - // Copy open-next.config.mjs - copyOpenNextConfig(options.buildDir, outputDir, true); - - // Load middleware manifest - const middlewareManifest = loadMiddlewareManifest(buildOutputDotNextDir); - - // Find functions - const functions = Object.values(middlewareManifest.functions).filter((fn) => - fnOptions.routes.includes(fn.name as RouteTemplate) - ); - - if (functions.length > 1) { - throw new Error("Only one function is supported for now"); - } - const middlewareInfo = functions[0]; - - copyMiddlewareResources(options, middlewareInfo, outputDir); - - await buildEdgeBundle({ - middlewareInfo, - entrypoint: path.join(options.openNextDistDir, "adapters/edge-adapter.js"), - outfile: path.join(outputDir, "index.mjs"), - options, - overrides: fnOptions.override, - additionalExternals: options.config.edgeExternals, - name, - additionalPlugins, - }); -} - -/** - * Copy wasm files and assets into the destDir. - */ -export function copyMiddlewareResources( - options: BuildOptions, - middlewareInfo: MiddlewareInfo | undefined, - destDir: string -) { - fs.mkdirSync(path.join(destDir, "wasm"), { recursive: true }); - for (const file of middlewareInfo?.wasm ?? []) { - fs.copyFileSync( - path.join(options.appBuildOutputPath, ".next", file.filePath), - path.join(destDir, `wasm/${file.name}.wasm`) - ); - } - - fs.mkdirSync(path.join(destDir, "assets"), { recursive: true }); - for (const file of middlewareInfo?.assets ?? []) { - fs.copyFileSync( - path.join(options.appBuildOutputPath, ".next", file.filePath), - path.join(destDir, `assets/${file.name}`) - ); - } -} +export * from "@opennextjs/core/build/edge/createEdgeBundle.js"; diff --git a/packages/open-next/src/build/generateOutput.ts b/packages/open-next/src/build/generateOutput.ts index 79716cdc..5ff1b4c5 100644 --- a/packages/open-next/src/build/generateOutput.ts +++ b/packages/open-next/src/build/generateOutput.ts @@ -1,340 +1 @@ -import * as fs from "node:fs"; -import path from "node:path"; - -import { loadConfig } from "@/config/util.js"; -import type { - BaseOverride, - DefaultOverrideOptions, - ExternalMiddlewareConfig, - FunctionOptions, - LazyLoadedOverride, - OverrideOptions, -} from "@/types/open-next"; - -import { type BuildOptions, getBuildId } from "./helper.js"; - -type BaseFunction = { - handler: string; - bundle: string; -}; - -type OpenNextFunctionOrigin = { - type: "function"; - streaming?: boolean; - wrapper: string; - converter: string; -} & BaseFunction; - -type OpenNextECSOrigin = { - type: "ecs"; - bundle: string; - wrapper: string; - converter: string; - dockerfile: string; -}; - -type CommonOverride = { - queue: string; - incrementalCache: string; - tagCache: string; -}; - -type OpenNextServerFunctionOrigin = OpenNextFunctionOrigin & CommonOverride; -type OpenNextServerECSOrigin = OpenNextECSOrigin & CommonOverride; - -type OpenNextS3Origin = { - type: "s3"; - originPath: string; - copy: { - from: string; - to: string; - cached: boolean; - versionedSubDir?: string; - }[]; -}; - -type OpenNextOrigins = OpenNextServerFunctionOrigin | OpenNextServerECSOrigin | OpenNextS3Origin; - -type ImageFnOrigins = OpenNextFunctionOrigin & { imageLoader: string }; -type ImageECSOrigins = OpenNextECSOrigin & { imageLoader: string }; - -type ImageOrigins = ImageFnOrigins | ImageECSOrigins; - -type DefaultOrigins = { - s3: OpenNextS3Origin; - default: OpenNextServerFunctionOrigin | OpenNextServerECSOrigin; - imageOptimizer: ImageOrigins; -}; - -interface OpenNextOutput { - edgeFunctions: { - [key: string]: BaseFunction; - } & { - middleware?: BaseFunction & { pathResolver: string }; - }; - origins: DefaultOrigins & { - [key: string]: OpenNextOrigins; - }; - behaviors: { - pattern: string; - origin?: string; - edgeFunction?: string; - }[]; - additionalProps?: { - disableIncrementalCache?: boolean; - disableTagCache?: boolean; - initializationFunction?: BaseFunction; - warmer?: BaseFunction; - revalidationFunction?: BaseFunction; - }; -} - -const indexHandler = "index.handler"; - -async function canStream(opts: FunctionOptions) { - if (!opts.override?.wrapper) { - return false; - } - if (typeof opts.override.wrapper === "string") { - return opts.override.wrapper === "aws-lambda-streaming"; - } - const wrapper = await opts.override.wrapper(); - return wrapper.supportStreaming; -} - -async function extractOverrideName( - defaultName: string, - override?: LazyLoadedOverride | string -) { - if (!override) { - return defaultName; - } - if (typeof override === "string") { - return override; - } - const overrideModule = await override(); - return overrideModule.name; -} - -async function extractOverrideFn(override?: DefaultOverrideOptions) { - if (!override) { - return { - wrapper: "aws-lambda", - converter: "aws-apigw-v2", - }; - } - const wrapper = await extractOverrideName("aws-lambda", override.wrapper); - const converter = await extractOverrideName("aws-apigw-v2", override.converter); - return { wrapper, converter }; -} - -async function extractCommonOverride(override?: OverrideOptions) { - if (!override) { - return { - queue: "sqs", - incrementalCache: "s3", - tagCache: "dynamodb", - }; - } - const queue = await extractOverrideName("sqs", override.queue); - const incrementalCache = await extractOverrideName("s3", override.incrementalCache); - const tagCache = await extractOverrideName("dynamodb", override.tagCache); - return { queue, incrementalCache, tagCache }; -} - -function prefixPattern(basePath: string) { - // Prefix CloudFront distribution behavior path patterns with `basePath` if configured - return (pattern: string) => { - return basePath && basePath.length > 0 ? `${basePath.slice(1)}/${pattern}` : pattern; - }; -} - -export async function generateOutput(options: BuildOptions) { - const { appBuildOutputPath, config } = options; - const edgeFunctions: OpenNextOutput["edgeFunctions"] = {}; - const isExternalMiddleware = config.middleware?.external ?? false; - if (isExternalMiddleware) { - const middlewareConfig = options.config.middleware as ExternalMiddlewareConfig; - edgeFunctions.middleware = { - bundle: ".open-next/middleware", - handler: "handler.handler", - pathResolver: await extractOverrideName("pattern-env", middlewareConfig.originResolver), - ...(await extractOverrideFn(middlewareConfig.override)), - }; - } - // Add edge functions - Object.entries(config.functions ?? {}).forEach(async ([key, value]) => { - if (value.placement === "global") { - edgeFunctions[key] = { - bundle: `.open-next/server-functions/${key}`, - handler: indexHandler, - ...(await extractOverrideFn(value.override)), - }; - } - }); - - const defaultOriginCanstream = await canStream(config.default); - - const nextConfig = loadConfig(path.join(appBuildOutputPath, ".next")); - const prefixer = prefixPattern(nextConfig.basePath ?? ""); - - // First add s3 origins and image optimization - - const defaultOrigins: DefaultOrigins = { - s3: { - type: "s3", - originPath: "_assets", - copy: [ - { - from: ".open-next/assets", - to: nextConfig.basePath ? `_assets${nextConfig.basePath}` : "_assets", - cached: true, - versionedSubDir: prefixer("_next"), - }, - ...(config.dangerous?.disableIncrementalCache - ? [] - : [ - { - from: ".open-next/cache", - to: "_cache", - cached: false, - }, - ]), - ], - }, - imageOptimizer: { - type: "function", - handler: indexHandler, - bundle: ".open-next/image-optimization-function", - streaming: false, - imageLoader: await extractOverrideName("s3", config.imageOptimization?.loader), - ...(await extractOverrideFn(config.imageOptimization?.override)), - }, - default: config.default.override?.generateDockerfile - ? { - type: "ecs", - bundle: ".open-next/server-functions/default", - dockerfile: ".open-next/server-functions/default/Dockerfile", - ...(await extractOverrideFn(config.default.override)), - ...(await extractCommonOverride(config.default.override)), - } - : { - type: "function", - handler: indexHandler, - bundle: ".open-next/server-functions/default", - streaming: defaultOriginCanstream, - ...(await extractOverrideFn(config.default.override)), - ...(await extractCommonOverride(config.default.override)), - }, - }; - - //@ts-expect-error - Not sure how to fix typing here, it complains about the type of imageOptimizer and s3 - const origins: OpenNextOutput["origins"] = defaultOrigins; - - // Then add function origins - await Promise.all( - Object.entries(config.functions ?? {}).map(async ([key, value]) => { - if (!value.placement || value.placement === "regional") { - if (value.override?.generateDockerfile) { - origins[key] = { - type: "ecs", - bundle: `.open-next/server-functions/${key}`, - dockerfile: `.open-next/server-functions/${key}/Dockerfile`, - ...(await extractOverrideFn(value.override)), - ...(await extractCommonOverride(value.override)), - }; - } else { - const streaming = await canStream(value); - origins[key] = { - type: "function", - handler: indexHandler, - bundle: `.open-next/server-functions/${key}`, - streaming, - ...(await extractOverrideFn(value.override)), - ...(await extractCommonOverride(value.override)), - }; - } - } - }) - ); - - // Then we need to compute the behaviors - const behaviors: OpenNextOutput["behaviors"] = [ - { pattern: prefixer("_next/image*"), origin: "imageOptimizer" }, - ]; - - // Then we add the routes - Object.entries(config.functions ?? {}).forEach(([key, value]) => { - const patterns = "patterns" in value ? value.patterns : ["*"]; - patterns.forEach((pattern) => { - behaviors.push({ - pattern: prefixer(pattern.replace(/BUILD_ID/, getBuildId(options))), - origin: value.placement === "global" ? undefined : key, - edgeFunction: value.placement === "global" ? key : isExternalMiddleware ? "middleware" : undefined, - }); - }); - }); - - // We finish with the default behavior so that they don't override the others - behaviors.push({ - pattern: prefixer("_next/data/*"), - origin: "default", - edgeFunction: isExternalMiddleware ? "middleware" : undefined, - }); - behaviors.push({ - pattern: "*", // This is the default behavior - origin: "default", - edgeFunction: isExternalMiddleware ? "middleware" : undefined, - }); - - //Compute behaviors for assets files - const assetPath = path.join(appBuildOutputPath, ".open-next", "assets"); - fs.readdirSync(assetPath).forEach((item) => { - if (fs.statSync(path.join(assetPath, item)).isDirectory()) { - behaviors.push({ - pattern: prefixer(`${item}/*`), - origin: "s3", - }); - } else { - behaviors.push({ - pattern: prefixer(item), - origin: "s3", - }); - } - }); - - // Check if we produced a dynamodb provider output - const isTagCacheDisabled = - config.dangerous?.disableTagCache || - !fs.existsSync(path.join(appBuildOutputPath, ".open-next", "dynamodb-provider")); - - const output: OpenNextOutput = { - edgeFunctions, - origins, - behaviors, - additionalProps: { - disableIncrementalCache: config.dangerous?.disableIncrementalCache, - disableTagCache: config.dangerous?.disableTagCache, - warmer: { - handler: indexHandler, - bundle: ".open-next/warmer-function", - }, - initializationFunction: isTagCacheDisabled - ? undefined - : { - handler: indexHandler, - bundle: ".open-next/dynamodb-provider", - }, - revalidationFunction: config.dangerous?.disableIncrementalCache - ? undefined - : { - handler: indexHandler, - bundle: ".open-next/revalidation-function", - }, - }, - }; - fs.writeFileSync( - path.join(appBuildOutputPath, ".open-next", "open-next.output.json"), - JSON.stringify(output) - ); -} +export * from "@opennextjs/core/build/generateOutput.js"; diff --git a/packages/open-next/src/build/helper.ts b/packages/open-next/src/build/helper.ts index 159495b9..727d3926 100644 --- a/packages/open-next/src/build/helper.ts +++ b/packages/open-next/src/build/helper.ts @@ -1,442 +1 @@ -import fs from "node:fs"; -import { createRequire } from "node:module"; -import path from "node:path"; -import url from "node:url"; - -import type { BuildOptions as ESBuildOptions } from "esbuild"; -import { build as buildAsync, buildSync } from "esbuild"; - -import type { DefaultOverrideOptions, OpenNextConfig } from "@/types/open-next.js"; - -import logger from "../logger.js"; - -const require = createRequire(import.meta.url); -const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); - -export type BuildOptions = ReturnType; - -export function normalizeOptions(config: OpenNextConfig, distDir: string, tempBuildDir: string) { - const appPath = path.join(process.cwd(), config.appPath || "."); - const buildOutputPath = path.join(process.cwd(), config.buildOutputPath || "."); - const outputDir = path.join(buildOutputPath, ".open-next"); - - const { root: monorepoRoot, packager } = findPackagerAndRoot( - path.join(process.cwd(), config.appPath || ".") - ); - - let appPackageJsonPath: string; - if (config.packageJsonPath) { - const _pkgPath = path.join(process.cwd(), config.packageJsonPath); - appPackageJsonPath = _pkgPath.endsWith("package.json") ? _pkgPath : path.join(_pkgPath, "./package.json"); - } else { - appPackageJsonPath = findNextPackageJsonPath(appPath, monorepoRoot); - } - - const debug = Boolean(process.env.OPEN_NEXT_DEBUG); - - return { - appBuildOutputPath: buildOutputPath, - appPackageJsonPath, - appPath, - appPublicPath: path.join(appPath, "public"), - buildDir: path.join(outputDir, ".build"), - config, - debug, - // Whether ESBuild should minify the code - minify: !debug, - monorepoRoot, - nextVersion: getNextVersion(appPath), - openNextVersion: getOpenNextVersion(), - openNextDistDir: distDir, - outputDir, - packager, - tempBuildDir, - }; -} -/** - * Given the path to a project this function detects the project's repository root (whether the project is in a simple - * repository or a monorepo) as well as the package manager being used. - * - * @param appPath The project's path - * @returns An object containing the root of the project's repo/monorepo as well as the package manager that it uses. - */ -export function findPackagerAndRoot(appPath: string): { - root: string; - packager: "npm" | "pnpm" | "yarn" | "bun"; -} { - let currentPath = appPath; - while (currentPath !== "/") { - const found = [ - // bun can generate yaml lock files (`bun install --yarn`) so bun should be before yarn - { file: "bun.lockb", packager: "bun" as const }, - { file: "bun.lock", packager: "bun" as const }, - { file: "package-lock.json", packager: "npm" as const }, - { file: "yarn.lock", packager: "yarn" as const }, - { file: "pnpm-lock.yaml", packager: "pnpm" as const }, - ].find((f) => fs.existsSync(path.join(currentPath, f.file))); - - if (found) { - if (currentPath !== appPath) { - logger.info("Monorepo detected at", currentPath); - } - return { root: currentPath, packager: found.packager }; - } - currentPath = path.dirname(currentPath); - } - - // note: a lock file (package-lock.json, yarn.lock, or pnpm-lock.yaml) is - // not found in the app's directory or any of its parent directories. - // We are going to assume that the app is not part of a monorepo. - logger.warn("No lockfile found"); - return { root: appPath, packager: "npm" as const }; -} - -function findNextPackageJsonPath(appPath: string, root: string) { - // This is needed for the case where the app is a single-version monorepo and the package.json is in the root of the monorepo - return fs.existsSync(path.join(appPath, "./package.json")) - ? path.join(appPath, "./package.json") - : path.join(root, "./package.json"); -} - -export function esbuildSync(esbuildOptions: ESBuildOptions, options: BuildOptions) { - const { openNextVersion, debug, minify } = options; - const result = buildSync({ - target: "esnext", - format: "esm", - platform: "node", - bundle: true, - minify, - mainFields: ["module", "main"], - sourcemap: debug ? "inline" : false, - sourcesContent: false, - ...esbuildOptions, - external: ["./open-next.config.mjs", ...(esbuildOptions.external ?? [])], - banner: { - ...esbuildOptions.banner, - js: [ - esbuildOptions.banner?.js || "", - `globalThis.openNextDebug = ${debug};`, - `globalThis.openNextVersion = "${openNextVersion}";`, - ].join(""), - }, - }); - - if (result.errors.length > 0) { - result.errors.forEach((error) => logger.error(error)); - throw new Error(`There was a problem bundling ${(esbuildOptions.entryPoints as string[])[0]}.`); - } -} - -export async function esbuildAsync(esbuildOptions: ESBuildOptions, options: BuildOptions) { - const { openNextVersion, debug, minify } = options; - // Dump ESBuild build metadata to file in debug mode - const metafile = debug && esbuildOptions.outfile !== undefined; - const result = await buildAsync({ - target: "esnext", - format: "esm", - platform: "node", - bundle: true, - // TODO(vicb): revert to `minify,` - minify: false, - metafile, - mainFields: ["module", "main"], - sourcemap: debug ? "inline" : false, - sourcesContent: false, - ...esbuildOptions, - external: [...(esbuildOptions.external ?? []), "next", "./open-next.config.mjs"], - banner: { - ...esbuildOptions.banner, - js: [ - esbuildOptions.banner?.js || "", - `globalThis.openNextDebug = ${debug};`, - `globalThis.openNextVersion = "${openNextVersion}";`, - ].join(""), - }, - }); - - if (result.errors.length > 0) { - result.errors.forEach((error) => logger.error(error)); - throw new Error(`There was a problem bundling ${(esbuildOptions.entryPoints as string[])[0]}.`); - } - - if (result.metafile) { - const metaFile = `${esbuildOptions.outfile}.meta.json`; - fs.writeFileSync(metaFile, JSON.stringify(result.metafile, null, 2)); - } -} - -/** - * Type of the parameter of `traverseFiles` callbacks - */ -export type TraversePath = { - absolutePath: string; - relativePath: string; -}; - -/** - * Recursively traverse files in a directory and call `callbackFn` when `conditionFn` returns true - * - * The callbacks are passed both the absolute and relative (to root) path to files. - * - * @param root - Root directory to search - * @param conditionFn - Called to determine if `callbackFn` should be called. - * @param callbackFn - Called when `conditionFn` returns true. - * @param searchingDir - Directory to search (used for recursion) - */ -export function traverseFiles( - root: string, - conditionFn: (paths: TraversePath) => boolean, - callbackFn: (paths: TraversePath) => void, - searchingDir = "" -) { - fs.readdirSync(path.join(root, searchingDir)).forEach((file) => { - const relativePath = path.join(searchingDir, file); - const absolutePath = path.join(root, relativePath); - - if (fs.statSync(absolutePath).isDirectory()) { - traverseFiles(root, conditionFn, callbackFn, relativePath); - return; - } - - if (conditionFn({ absolutePath, relativePath })) { - callbackFn({ absolutePath, relativePath }); - } - }); -} - -/** - * Recursively delete files. - * - * @see `traverseFiles`. - * - * @param root Root directory to search. - * @param conditionFn Predicate used to delete the files. - */ -export function removeFiles(root: string, conditionFn: (paths: TraversePath) => boolean) { - traverseFiles(root, conditionFn, ({ absolutePath }) => fs.rmSync(absolutePath, { force: true })); -} - -export function getHtmlPages(dotNextPath: string) { - // Get a list of HTML pages - // - // sample return value: - // Set([ - // '404.html', - // 'csr.html', - // 'image-html-tag.html', - // ]) - const manifestPath = path.join(dotNextPath, ".next/server/pages-manifest.json"); - const manifest = fs.readFileSync(manifestPath, "utf-8"); - return Object.entries(JSON.parse(manifest)) - .filter(([_, value]) => (value as string).endsWith(".html")) - .map(([_, value]) => (value as string).replace(/^pages\//, "")) - .reduce((acc, page) => acc.add(page), new Set()); -} - -export function getBuildId(options: BuildOptions) { - return fs.readFileSync(path.join(options.appBuildOutputPath, ".next/BUILD_ID"), "utf-8").trim(); -} - -export function getOpenNextVersion(): string { - return require(path.join(__dirname, "../../package.json")).version; -} - -export function getNextVersion(appPath: string): string { - // We cannot just require("next/package.json") because it could be executed in a different directory - const nextPackageJsonPath = require.resolve("next/package.json", { - paths: [appPath], - }); - const version = require(nextPackageJsonPath)?.version; - - if (!version) { - throw new Error("Failed to find Next version"); - } - - // Drop the -canary.n suffix - return version.split("-")[0]; -} - -export type SemverOp = "=" | ">=" | "<=" | ">" | "<"; - -/** - * Compare two semver versions. - * - * @param v1 - First version. Can be "latest", otherwise it should be a valid semver version in the format of `major.minor.patch`. Usually is the next version from the package.json without canary suffix. If minor or patch are missing, they are considered 0. - * @param v2 - Second version. Should not be "latest", it should be a valid semver version in the format of `major.minor.patch`. If minor or patch are missing, they are considered 0. - * @example - * compareSemver("2.0.0", ">=", "1.0.0") === true - */ -export function compareSemver(v1: string, operator: SemverOp, v2: string): boolean { - // - = 0 when versions are equal - // - > 0 if v1 > v2 - // - < 0 if v2 > v1 - let versionDiff = 0; - if (v1 === "latest") { - versionDiff = 1; - } else { - if (/^[^\d]/.test(v1)) { - // oxlint-disable-next-line no-param-reassign - v1 = v1.substring(1); - } - if (/^[^\d]/.test(v2)) { - // oxlint-disable-next-line no-param-reassign - v2 = v2.substring(1); - } - const [major1, minor1 = 0, patch1 = 0] = v1.split(".").map(Number); - const [major2, minor2 = 0, patch2 = 0] = v2.split(".").map(Number); - if (Number.isNaN(major1) || Number.isNaN(major2)) { - throw new Error("The major version is required."); - } - - if (major1 !== major2) { - versionDiff = major1 - major2; - } else if (minor1 !== minor2) { - versionDiff = minor1 - minor2; - } else if (patch1 !== patch2) { - versionDiff = patch1 - patch2; - } - } - - switch (operator) { - case "=": - return versionDiff === 0; - case ">=": - return versionDiff >= 0; - case "<=": - return versionDiff <= 0; - case ">": - return versionDiff > 0; - case "<": - return versionDiff < 0; - default: - throw new Error(`Unsupported operator: ${operator}`); - } -} - -export function copyOpenNextConfig(inputDir: string, outputDir: string, isEdge = false) { - // Copy open-next.config.mjs - fs.copyFileSync( - path.join(inputDir, isEdge ? "open-next.config.edge.mjs" : "open-next.config.mjs"), - path.join(outputDir, "open-next.config.mjs") - ); -} - -export function copyEnvFile(appPath: string, packagePath: string, outputPath: string) { - const baseAppPath = path.join(appPath, ".next/standalone", packagePath); - const baseOutputPath = path.join(outputPath, packagePath); - const envPath = path.join(baseAppPath, ".env"); - if (fs.existsSync(envPath)) { - fs.copyFileSync(envPath, path.join(baseOutputPath, ".env")); - } - const envProdPath = path.join(baseAppPath, ".env.production"); - if (fs.existsSync(envProdPath)) { - fs.copyFileSync(envProdPath, path.join(baseOutputPath, ".env.production")); - } -} - -/** - * Check we are in a Nextjs app by looking for the Nextjs config file. - */ -export function checkRunningInsideNextjsApp({ appPath }: { appPath: string }) { - const extension = ["js", "cjs", "mjs", "ts"].find((ext) => - fs.existsSync(path.join(appPath, `next.config.${ext}`)) - ); - if (!extension) { - logger.error( - "Error: next.config.js not found. Please make sure you are running this command inside a Next.js app." - ); - process.exit(1); - } -} - -export function printNextjsVersion(options: BuildOptions) { - logger.info(`Next.js version : ${options.nextVersion}`); -} - -export function printOpenNextVersion(options: BuildOptions) { - logger.info(`OpenNext v${options.openNextVersion}`); -} - -/** - * Populates the build directory with the compiled configuration files. - * - * We need to get the build relative to the cwd to find the compiled config. - * This is needed for the case where the app is a single-version monorepo - * and the package.json is in the root of the monorepo where the build is in - * the app directory, but the compiled config is in the root of the monorepo. - */ -export function initOutputDir(options: BuildOptions) { - fs.rmSync(options.outputDir, { recursive: true, force: true }); - const { buildDir } = options; - fs.mkdirSync(buildDir, { recursive: true }); - fs.cpSync(options.tempBuildDir, buildDir, { recursive: true }); -} - -/** - * @returns Whether the edge runtime is used - */ -export async function isEdgeRuntime(overrides: DefaultOverrideOptions | undefined) { - if (!overrides?.wrapper) { - return false; - } - if (typeof overrides.wrapper === "string") { - return ["cloudflare-edge", "cloudflare", "cloudflare-node"].includes(overrides.wrapper); - } - return (await overrides?.wrapper?.())?.edgeRuntime; -} - -export function getPackagePath(options: BuildOptions) { - return path.relative(options.monorepoRoot, options.appBuildOutputPath); -} - -/** - * Returns the Next.js runtime used: "webpack" or "turbopack" - * - * Must be called after building the Next.js app. - * - * @param options - * @returns the Next.js runtime used: "webpack" or "turbopack" - */ -export function getBundlerRuntime(options: BuildOptions): "webpack" | "turbopack" { - const dotNextServerPath = path.join(options.appPath, ".next/server"); - if (fs.existsSync(path.join(dotNextServerPath, "webpack-runtime.js"))) { - return "webpack"; - } - if ( - fs.existsSync(path.join(dotNextServerPath, "chunks/[turbopack]_runtime.js")) || - fs.existsSync(path.join(dotNextServerPath, "chunks/ssr/[turbopack]_runtime.js")) - ) { - return "turbopack"; - } - - throw new Error("Unable to determine Next.js runtime (webpack or turbopack)"); -} - -/** - * Finds the path to the Next configuration file if it exists. - * - * @param appPath The directory to check for the Next config file - * @returns An object with the full path to Next config file alongside a flag indicating whether the file is in typescript if it exists, undefined otherwise - */ -export function findNextConfig({ - appPath, -}: Pick): { path: string; isTypescript: boolean } | undefined { - const extensions = [ - { ext: "ts", isTypescript: true }, - { ext: "mts", isTypescript: true }, - { ext: "cts", isTypescript: true }, - { ext: "js", isTypescript: false }, - { ext: "mjs", isTypescript: false }, - { ext: "cjs", isTypescript: false }, - ]; - - for (const { ext, isTypescript } of extensions) { - const configPath = path.join(appPath, `next.config.${ext}`); - if (fs.existsSync(configPath)) { - return { - path: configPath, - isTypescript, - }; - } - } -} +export * from "@opennextjs/core/build/helper.js"; diff --git a/packages/open-next/src/build/installDeps.ts b/packages/open-next/src/build/installDeps.ts index 490e34cc..a967c08a 100644 --- a/packages/open-next/src/build/installDeps.ts +++ b/packages/open-next/src/build/installDeps.ts @@ -1,78 +1 @@ -import { execSync } from "node:child_process"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -import type { InstallOptions } from "@/types/open-next"; - -import logger from "../logger.js"; - -export function installDependencies(outputDir: string, installOptions?: InstallOptions) { - try { - if (!installOptions) { - return; - } - const name = outputDir.split("/").pop(); - // First we create a tempDir - const tempInstallDir = fs.mkdtempSync(path.join(os.tmpdir(), `open-next-install-${name}`)); - logger.info(`Installing dependencies for ${name}...`); - // We then need to run install in the tempDir - // We don't install in the output dir directly because it could contain a package.json, and npm would then try to reinstall not complete deps from tracing the files - const archOption = installOptions.arch ? `--arch=${installOptions.arch}` : ""; - const targetOption = installOptions.nodeVersion ? `--target=${installOptions.nodeVersion}` : ""; - const libcOption = installOptions.libc ? `--libc=${installOptions.libc}` : ""; - const osOption = `--os=${installOptions.os ?? "linux"}`; - - const additionalArgs = installOptions.additionalArgs ?? ""; - const installCommand = `npm install ${osOption} ${archOption} ${targetOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`; - execSync(installCommand, { - stdio: "pipe", - cwd: tempInstallDir, - env: { - ...process.env, - SHARP_IGNORE_GLOBAL_LIBVIPS: "1", - }, - }); - - // Copy the node_modules to the outputDir - fs.cpSync(path.join(tempInstallDir, "node_modules"), path.join(outputDir, "node_modules"), { - recursive: true, - force: true, - dereference: true, - }); - - // https://github.com/nodejs/node/issues/59168 - // This is a workaround for all Node versions. It seems to be an issue continue to affect new versions aswell. - // Therefor I think the most logical solution is to always run this workaround instead of trying to figure out which versions are affected. - const tempBinDir = path.join(tempInstallDir, "node_modules", ".bin"); - const outputBinDir = path.join(outputDir, "node_modules", ".bin"); - - for (const fileName of fs.readdirSync(tempBinDir)) { - const symlinkPath = path.join(tempBinDir, fileName); - const stat = fs.lstatSync(symlinkPath); - - if (stat.isSymbolicLink()) { - const linkTarget = fs.readlinkSync(symlinkPath); - const realFilePath = path.resolve(tempBinDir, linkTarget); - - const outputFilePath = path.join(outputBinDir, fileName); - - if (fs.existsSync(outputFilePath)) { - fs.unlinkSync(outputFilePath); - } - - fs.copyFileSync(realFilePath, outputFilePath); - fs.chmodSync(outputFilePath, "755"); - logger.debug(`Replaced symlink ${fileName} with actual file`); - } - } - // End of Node Workaround - - // Cleanup tempDir - fs.rmSync(tempInstallDir, { recursive: true, force: true }); - logger.info(`Dependencies installed for ${name}`); - } catch (e: unknown) { - logger.error(e instanceof Error ? e.message : String(e)); - logger.error("Could not install dependencies"); - } -} +export * from "@opennextjs/core/build/installDeps.js"; diff --git a/packages/open-next/src/build/middleware/buildNodeMiddleware.ts b/packages/open-next/src/build/middleware/buildNodeMiddleware.ts index 8b4a2c74..7cdfdf50 100644 --- a/packages/open-next/src/build/middleware/buildNodeMiddleware.ts +++ b/packages/open-next/src/build/middleware/buildNodeMiddleware.ts @@ -1,108 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import type { IncludedOriginResolver, LazyLoadedOverride, OverrideOptions } from "@/types/open-next.js"; -import type { OriginResolver } from "@/types/overrides.js"; -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import { openNextExternalMiddlewarePlugin } from "../../plugins/externalMiddleware.js"; -import { openNextReplacementPlugin } from "../../plugins/replacement.js"; -import { openNextResolvePlugin } from "../../plugins/resolve.js"; -import { copyTracedFiles } from "../copyTracedFiles.js"; -import * as buildHelper from "../helper.js"; -import { installDependencies } from "../installDeps.js"; - -type Override = OverrideOptions & { - originResolver?: LazyLoadedOverride | IncludedOriginResolver; -}; - -export async function buildExternalNodeMiddleware(options: buildHelper.BuildOptions) { - const { appBuildOutputPath, config, outputDir } = options; - if (!config.middleware?.external) { - throw new Error("This function should only be called for external middleware"); - } - const outputPath = path.join(outputDir, "middleware"); - fs.mkdirSync(outputPath, { recursive: true }); - - // Copy open-next.config.mjs - buildHelper.copyOpenNextConfig( - options.buildDir, - outputPath, - await buildHelper.isEdgeRuntime(config.middleware.override) - ); - const overrides = { - ...config.middleware.override, - originResolver: config.middleware.originResolver, - }; - const packagePath = buildHelper.getPackagePath(options); - - // TODO: change this so that we don't copy unnecessary files - await copyTracedFiles({ - buildOutputPath: appBuildOutputPath, - packagePath, - outputDir: outputPath, - routes: [], - skipServerFiles: true, - }); - - function override(target: T) { - return typeof overrides?.[target] === "string" ? overrides[target] : undefined; - } - - // Bundle middleware - await buildHelper.esbuildAsync( - { - entryPoints: [path.join(options.openNextDistDir, "adapters", "middleware.js")], - outfile: path.join(outputPath, "handler.mjs"), - external: ["./.next/*"], - platform: "node", - plugins: [ - openNextResolvePlugin({ - overrides: { - wrapper: override("wrapper") ?? "aws-lambda", - converter: override("converter") ?? "aws-cloudfront", - tagCache: override("tagCache") ?? "dynamodb-lite", - incrementalCache: override("incrementalCache") ?? "s3-lite", - queue: override("queue") ?? "sqs-lite", - originResolver: override("originResolver") ?? "pattern-env", - proxyExternalRequest: override("proxyExternalRequest") ?? "node", - }, - fnName: "middleware", - }), - openNextExternalMiddlewarePlugin( - path.join(options.openNextDistDir, "core", "nodeMiddlewareHandler.js") - ), - ], - banner: { - js: [ - `globalThis.monorepoPackagePath = '${packagePath}';`, - "import process from 'node:process';", - "import { Buffer } from 'node:buffer';", - "import { AsyncLocalStorage } from 'node:async_hooks';", - "import { createRequire as topLevelCreateRequire } from 'module';", - "const require = topLevelCreateRequire(import.meta.url);", - "import bannerUrl from 'url';", - "const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));", - ].join(""), - }, - }, - options - ); - - // Do we need to copy or do something with env file here? - - installDependencies(outputPath, config.middleware?.install); -} - -export async function buildBundledNodeMiddleware(options: buildHelper.BuildOptions) { - await buildHelper.esbuildAsync( - { - entryPoints: [path.join(options.openNextDistDir, "core/nodeMiddlewareHandler.js")], - external: ["./.next/*"], - outfile: path.join(options.buildDir, "middleware.mjs"), - bundle: true, - platform: "node", - }, - options - ); -} +export * from "@opennextjs/core/build/middleware/buildNodeMiddleware.js"; diff --git a/packages/open-next/src/build/patch/astCodePatcher.ts b/packages/open-next/src/build/patch/astCodePatcher.ts index fc3a1ecb..6fab03b5 100644 --- a/packages/open-next/src/build/patch/astCodePatcher.ts +++ b/packages/open-next/src/build/patch/astCodePatcher.ts @@ -1,110 +1 @@ -// Mostly copied from the cloudflare adapter -import { readFileSync } from "node:fs"; - -import { type Edit, Lang, type NapiConfig, type SgNode, parse } from "@ast-grep/napi"; -import yaml from "yaml"; - -import type { PatchCodeFn } from "./codePatcher"; - -export type * from "@ast-grep/napi"; - -/** - * fix has the same meaning as in yaml rules - * see https://ast-grep.github.io/guide/rewrite-code.html#using-fix-in-yaml-rule - */ -export type RuleConfig = NapiConfig & { fix?: string }; - -/** - * Returns the `Edit`s and `Match`es for an ast-grep rule in yaml format - * - * The rule must have a `fix` to rewrite the matched node. - * - * Tip: use https://ast-grep.github.io/playground.html to create rules. - * - * @param rule The rule. Either a yaml string or an instance of `RuleConfig` - * @param root The root node - * @param once only apply once - * @returns A list of edits and a list of matches. - */ -export function applyRule( - rule: string | RuleConfig, - root: SgNode, - { once = false } = {} -): { - edits: Edit[]; - matches: SgNode[]; -} { - const ruleConfig: RuleConfig = typeof rule === "string" ? yaml.parse(rule) : rule; - if (ruleConfig.transform) { - throw new Error("transform is not supported"); - } - if (!ruleConfig.fix) { - throw new Error("no fix to apply"); - } - - const fix = ruleConfig.fix; - - const matches = once ? [root.find(ruleConfig)].filter((m) => m !== null) : root.findAll(ruleConfig); - - const edits: Edit[] = []; - - matches.forEach((match) => { - edits.push( - match.replace( - // Replace known placeholders by their value - fix - .replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => - match - .getMultipleMatches(name) - .map((n) => n.text()) - .join("") - ) - .replace(/\$([A-Z0-9_]+)/g, (m, name) => match.getMatch(name)?.text() ?? m) - ) - ); - }); - - return { edits, matches }; -} - -/** - * Parse a file and obtain its root. - * - * @param path The file path - * @param lang The language to parse. Defaults to TypeScript. - * @returns The root for the file. - */ -export function parseFile(path: string, lang = Lang.TypeScript): SgNode { - return parse(lang, readFileSync(path, { encoding: "utf-8" })).root(); -} - -/** - * Patches the code from by applying the rule. - * - * This function is mainly for on off edits and tests, - * use `getRuleEdits` to apply multiple rules. - * - * @param code The source code - * @param rule The astgrep rule (yaml or NapiConfig) - * @param lang The language used by the source code - * @param lang Whether to apply the rule only once - * @returns The patched code - */ -export function patchCode( - code: string, - rule: string | RuleConfig, - { lang = Lang.TypeScript, once = false } = {} -): string { - const node = parse(lang, code).root(); - const { edits } = applyRule(rule, node, { once }); - return node.commitEdits(edits); -} - -/** - * @param rule - * @param lang - * @returns a callback applying the the rule. - */ -export function createPatchCode(rule: string | RuleConfig, lang = Lang.TypeScript): PatchCodeFn { - return async ({ code }) => patchCode(code, rule, { lang }); -} +export * from "@opennextjs/core/build/patch/astCodePatcher.js"; diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts index 57bbb311..2cacbab6 100644 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ b/packages/open-next/src/build/patch/codePatcher.ts @@ -1,176 +1 @@ -import * as fs from "node:fs/promises"; - -import logger from "../../logger.js"; -import type { getManifests } from "../copyTracedFiles.js"; -import * as buildHelper from "../helper.js"; - -/** - * Accepted formats: - * - `">=16.0.0"` - * - `"<=16.0.0"` - * - `">=16.0.0 <=17.0.0"` - * - * **Be careful with spaces** - */ -export type Versions = - | `>=${number}.${number}.${number} <=${number}.${number}.${number}` - | `>=${number}.${number}.${number}` - | `<=${number}.${number}.${number}`; - -export type PatchCodeFn = (args: { - /** - * The code of the file that needs to be patched - */ - code: string; - /** - * The final path of the file that needs to be patched - */ - filePath: string; - /** - * All files that are traced and will be included in the bundle - */ - tracedFiles: string[]; - /** - * Next.js manifests that are used by Next at runtime - */ - manifests: ReturnType; - /** - * OpenNext build options - */ - buildOptions: buildHelper.BuildOptions; -}) => Promise; - -interface IndividualPatch { - pathFilter: RegExp; - contentFilter?: RegExp; - patchCode: PatchCodeFn; - // Only apply the patch to specific versions of Next.js - versions?: Versions; -} - -export interface CodePatcher { - name: string; - patches: IndividualPatch[]; -} - -export function parseVersions(versions?: Versions): { - before?: string; - after?: string; -} { - if (!versions) { - return {}; - } - // We need to use regex to extract the versions - const versionRegex = /([<>]=)(\d+\.\d+\.\d+)/g; - const matches = Array.from(versions.matchAll(versionRegex)); - if (matches.length === 0) { - throw new Error("Invalid version range, no matches found"); - } - if (matches.length > 2) { - throw new Error("Invalid version range, too many matches found"); - } - let after: string | undefined; - let before: string | undefined; - for (const match of matches) { - const [_, operator, version] = match; - if (operator === "<=") { - before = version; - } else { - after = version; - } - } - // Before returning we reconstruct the version string and compare it to the original - // If they don't match we throw an error - // We have to do this because template literal types here seems to allow for extra spaces - // that could easily break the version comparison and allow some patch to be applied on incorrect versions - // This might even go unnoticed - const reconstructedVersion = `${after ? `>=${after}` : ""}${ - before ? `${after ? " " : ""}<=${before}` : "" - }`; - if (reconstructedVersion !== versions) { - throw new Error("Invalid version range, the reconstructed version does not match the original"); - } - return { - before, - after, - }; -} - -/** - * Check whether the version is in the range - * - * @param version A semver version - * @param versionRange A version range - * @returns whether the version satisfies the range - */ -export function isVersionInRange(version: string, versionRange?: Versions): boolean { - const { before, after } = parseVersions(versionRange); - - let inRange = true; - - if (before) { - inRange &&= buildHelper.compareSemver(version, "<=", before); - } - - if (after) { - inRange &&= buildHelper.compareSemver(version, ">=", after); - } - - return inRange; -} - -export async function applyCodePatches( - buildOptions: buildHelper.BuildOptions, - tracedFiles: string[], - manifests: ReturnType, - codePatcher: CodePatcher[] -) { - logger.time("Applying code patches"); - - // We first filter against the version - // We also flatten the array of patches so that we get both the name and all the necessary patches - const patchesToApply = codePatcher.flatMap(({ name, patches }) => { - return patches - .filter(({ versions }) => isVersionInRange(buildOptions.nextVersion, versions)) - .map((patch) => ({ patch, name })); - }); - - await Promise.all( - tracedFiles.map(async (filePath) => { - // We check the filename against the filter to see if we should apply the patch - const patchMatchingPath = patchesToApply.filter(({ patch }) => { - return filePath.match(patch.pathFilter); - }); - if (patchMatchingPath.length === 0) { - return; - } - const content = await fs.readFile(filePath, "utf-8"); - // We filter a last time against the content this time - const patchToApply = patchMatchingPath.filter(({ patch }) => { - if (!patch.contentFilter) { - return true; - } - return content.match(patch.contentFilter); - }); - if (patchToApply.length === 0) { - return; - } - - // We apply the patches - let patchedContent = content; - - for (const { patch, name } of patchToApply) { - logger.debug(`Applying code patch: ${name} to ${filePath}`); - patchedContent = await patch.patchCode({ - code: patchedContent, - filePath, - tracedFiles, - manifests, - buildOptions, - }); - } - await fs.writeFile(filePath, patchedContent); - }) - ); - logger.timeEnd("Applying code patches"); -} +export * from "@opennextjs/core/build/patch/codePatcher.js"; diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts index a6bb2eba..a02f8f26 100644 --- a/packages/open-next/src/build/patch/patches/index.ts +++ b/packages/open-next/src/build/patch/patches/index.ts @@ -1,11 +1 @@ -export { getEnvVarsPatch } from "./patchEnvVar.js"; -export { patchNextServer } from "./patchNextServer.js"; -export { - patchFetchCacheForISR, - patchUnstableCacheForISR, - patchUseCacheForISR, -} from "./patchFetchCacheISR.js"; -export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js"; -export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js"; -export { patchNodeEnvironment } from "./patchNodeEnvironment.js"; -export { patchOriginalNextConfig } from "./patchOriginalNextConfig.js"; +export * from "@opennextjs/core/build/patch/patches/index.js"; diff --git a/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts b/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts index 9845f774..2345d463 100644 --- a/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts +++ b/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts @@ -1,29 +1 @@ -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import { createPatchCode } from "../astCodePatcher.js"; -import type { CodePatcher } from "../codePatcher.js"; - -export const rule = ` -rule: - kind: binary_expression - all: - - has: - kind: unary_expression - regex: "!cachedResponse.isStale" - - has: - kind: member_expression - regex: "context.isPrefetch" -fix: - 'true'`; - -export const patchBackgroundRevalidation = { - name: "patchBackgroundRevalidation", - patches: [ - { - // TODO: test for earlier versions of Next - versions: ">=14.1.0", - pathFilter: getCrossPlatformPathRegex("server/response-cache/index.js"), - patchCode: createPatchCode(rule), - }, - ], -} satisfies CodePatcher; +export * from "@opennextjs/core/build/patch/patches/patchBackgroundRevalidation.js"; diff --git a/packages/open-next/src/build/patch/patches/patchEnvVar.ts b/packages/open-next/src/build/patch/patches/patchEnvVar.ts index 9b33fdec..e65cd82b 100644 --- a/packages/open-next/src/build/patch/patches/patchEnvVar.ts +++ b/packages/open-next/src/build/patch/patches/patchEnvVar.ts @@ -1,54 +1 @@ -import * as buildHelper from "../../helper.js"; -import { createPatchCode } from "../astCodePatcher.js"; -import type { CodePatcher } from "../codePatcher"; - -/** - * Creates a rule to replace `process.env.${envVar}` by `value` in the condition of if statements - * This is used to avoid loading unnecessary deps at runtime - * @param envVar The env var that we want to replace - * @param value The value that we want to replace it with - * @returns - */ -export const envVarRuleCreator = (envVar: string, value: string) => ` -rule: - kind: member_expression - pattern: process.env.${envVar} - inside: - kind: parenthesized_expression - stopBy: end - inside: - kind: if_statement -fix: - '${value}' -`; - -export function getEnvVarsPatch(BuildOptions: buildHelper.BuildOptions): CodePatcher { - const isTurbopack = buildHelper.getBundlerRuntime(BuildOptions) === "turbopack"; - - return { - name: "patch-env-vars", - patches: [ - // This patch will set the `NEXT_RUNTIME` env var to "node" to avoid loading unnecessary edge deps at runtime - { - versions: ">=15.0.0", - pathFilter: /module\.compiled\.js$/, - contentFilter: /process\.env\.NEXT_RUNTIME/, - patchCode: createPatchCode(envVarRuleCreator("NEXT_RUNTIME", '"node"')), - }, - // This patch will set `NODE_ENV` to production to avoid loading unnecessary dev deps at runtime - { - versions: ">=15.0.0", - pathFilter: /(module\.compiled|react\/index|react\/jsx-runtime|react-dom\/index)\.js$/, - contentFilter: /process\.env\.NODE_ENV/, - patchCode: createPatchCode(envVarRuleCreator("NODE_ENV", '"production"')), - }, - // This patch will set `TURBOPACK` env to false to avoid loading turbopack related deps at runtime - { - versions: ">=15.0.0", - pathFilter: /module\.compiled\.js$/, - contentFilter: /process\.env\.TURBOPACK/, - patchCode: createPatchCode(envVarRuleCreator("TURBOPACK", JSON.stringify(isTurbopack))), - }, - ], - }; -} +export * from "@opennextjs/core/build/patch/patches/patchEnvVar.js"; diff --git a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts index 38fa023b..8923936b 100644 --- a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts +++ b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts @@ -1,149 +1 @@ -import { Lang } from "@ast-grep/napi"; - -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import { createPatchCode } from "../astCodePatcher.js"; -import type { CodePatcher } from "../codePatcher.js"; - -export const fetchRule = ` -rule: - kind: member_expression - pattern: $WORK_STORE.isOnDemandRevalidate - inside: - kind: ternary_expression - all: - - has: {kind: 'null'} - - has: - kind: await_expression - has: - kind: call_expression - all: - - has: - kind: member_expression - has: - kind: property_identifier - field: property - regex: get - - has: - kind: arguments - has: - kind: object - has: - kind: pair - all: - - has: - kind: property_identifier - field: key - regex: softTags - inside: - kind: variable_declarator - -fix: - ($WORK_STORE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation) -`; - -export const unstable_cacheRule = ` -rule: - kind: member_expression - pattern: $STORE_OR_CACHE.isOnDemandRevalidate - inside: - kind: if_statement - stopBy: end - has: - kind: statement_block - has: - kind: variable_declarator - has: - kind: await_expression - has: - kind: call_expression - all: - - has: - kind: member_expression - has: - kind: property_identifier - field: property - regex: get - - has: - kind: arguments - has: - kind: object - has: - kind: pair - all: - - has: - kind: property_identifier - field: key - regex: softTags - stopBy: end -fix: - ($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation) -`; - -export const useCacheRule = ` -rule: - kind: member_expression - pattern: $STORE_OR_CACHE.isOnDemandRevalidate - inside: - kind: binary_expression - has: - kind: member_expression - pattern: $STORE_OR_CACHE.isDraftMode - inside: - kind: if_statement - stopBy: end - has: - kind: return_statement - any: - - has: - kind: 'true' - - has: - regex: '!0' - stopBy: end -fix: - '($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)'`; - -export const patchFetchCacheForISR: CodePatcher = { - name: "patch-fetch-cache-for-isr", - patches: [ - { - versions: ">=14.0.0", - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, - { escape: false } - ), - contentFilter: /\.isOnDemandRevalidate/, - patchCode: createPatchCode(fetchRule, Lang.JavaScript), - }, - ], -}; - -export const patchUnstableCacheForISR: CodePatcher = { - name: "patch-unstable-cache-for-isr", - patches: [ - { - versions: ">=14.2.0", - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|spec-extension/unstable-cache\.js)$`, - { escape: false } - ), - contentFilter: /\.isOnDemandRevalidate/, - patchCode: createPatchCode(unstable_cacheRule, Lang.JavaScript), - }, - ], -}; - -export const patchUseCacheForISR: CodePatcher = { - name: "patch-use-cache-for-isr", - patches: [ - { - versions: ">=15.3.0", - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|\.runtime\..*\.js|use-cache/use-cache-wrapper\.js)$`, - { escape: false } - ), - contentFilter: /\.isOnDemandRevalidate/, - patchCode: createPatchCode(useCacheRule, Lang.JavaScript), - }, - ], -}; +export * from "@opennextjs/core/build/patch/patches/patchFetchCacheISR.js"; diff --git a/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts index 671d58d5..fe29bab1 100644 --- a/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts +++ b/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts @@ -1,41 +1 @@ -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import { createPatchCode } from "../astCodePatcher.js"; -import type { CodePatcher } from "../codePatcher.js"; - -export const rule = ` -rule: - kind: call_expression - pattern: $PROMISE - all: - - has: { pattern: $_.arrayBuffer().then, stopBy: end } - - has: { pattern: "Buffer.from", stopBy: end } - - any: - - inside: - kind: sequence_expression - inside: - kind: return_statement - - inside: - kind: expression_statement - precedes: - kind: return_statement - - has: { pattern: $_.FETCH, stopBy: end } - -fix: | - globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE) -`; - -export const patchFetchCacheSetMissingWaitUntil: CodePatcher = { - name: "patch-fetch-cache-set-missing-wait-until", - patches: [ - { - versions: ">=15.0.0", - pathFilter: getCrossPlatformPathRegex( - String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`, - { escape: false } - ), - contentFilter: /arrayBuffer\(\)\s*\.then/, - patchCode: createPatchCode(rule), - }, - ], -}; +export * from "@opennextjs/core/build/patch/patches/patchFetchCacheWaitUntil.js"; diff --git a/packages/open-next/src/build/patch/patches/patchNextServer.ts b/packages/open-next/src/build/patch/patches/patchNextServer.ts index c3b99d43..c872cdb3 100644 --- a/packages/open-next/src/build/patch/patches/patchNextServer.ts +++ b/packages/open-next/src/build/patch/patches/patchNextServer.ts @@ -1,133 +1 @@ -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import { createPatchCode } from "../astCodePatcher.js"; -import type { CodePatcher } from "../codePatcher.js"; - -// Disable the background preloading of route done by NextServer by default during the creation of NextServer -export const disablePreloadingRule = ` -rule: - kind: statement_block - inside: - kind: if_statement - any: - - has: - kind: member_expression - pattern: this.nextConfig.experimental.preloadEntriesOnStart - stopBy: end - - has: - kind: binary_expression - pattern: appDocumentPreloading === true - stopBy: end -fix: - '{}' -`; - -// Mostly for splitted edge functions so that we don't try to match them on the other non edge functions -export const removeMiddlewareManifestRule = ` -rule: - kind: statement_block - inside: - kind: method_definition - has: - kind: property_identifier - regex: ^getMiddlewareManifest$ -fix: - '{return null;}' -`; - -// Make `handleNextImageRequest` a no-op to avoid pulling `sharp` -// Applies wherever this constructor pattern is matched -export const emptyHandleNextImageRequestRule = ` -rule: - kind: assignment_expression - pattern: this.handleNextImageRequest = $VALUE - inside: - kind: method_definition - stopBy: end - has: - kind: property_identifier - regex: ^constructor$ - inside: - kind: class_body -fix: - this.handleNextImageRequest = async (req, res, parsedUrl) => false -`; - -/** - * Swaps the body for a throwing implementation - * - * @param methodName The name of the method - * @returns A rule to replace the body with a `throw` - */ -export function createEmptyBodyRule(methodName: string) { - return ` -rule: - pattern: - selector: method_definition - context: "class { async ${methodName}($$$PARAMS) { $$$_ } }" -fix: |- - async ${methodName}($$$PARAMS) { - throw new Error("${methodName} should not be called with OpenNext"); - } -`; -} - -const pathFilter = getCrossPlatformPathRegex(String.raw`/next/dist/server/next-server\.js$`, { - escape: false, -}); - -/** - * Patches to avoid pulling babel (~4MB). - * - * Details: - * - empty `NextServer#runMiddleware` and `NextServer#runEdgeFunction` that are not used - * - drop `next/dist/server/node-environment-extensions/error-inspect.js` - */ -const babelPatches = [ - // Empty the body of `NextServer#runMiddleware` - { - pathFilter, - contentFilter: /runMiddleware\(/, - patchCode: createPatchCode(createEmptyBodyRule("runMiddleware")), - }, - // Empty the body of `NextServer#runEdgeFunction` - { - pathFilter, - contentFilter: /runEdgeFunction\(/, - patchCode: createPatchCode(createEmptyBodyRule("runEdgeFunction")), - }, -]; - -export const patchNextServer: CodePatcher = { - name: "patch-next-server", - patches: [ - // Empty the body of `NextServer#imageOptimizer` - unused in OpenNext - { - pathFilter, - contentFilter: /imageOptimizer\(/, - patchCode: createPatchCode(createEmptyBodyRule("imageOptimizer")), - }, - // Make `handleNextImageRequest` a no-op to avoid pulling `sharp` - unused in OpenNext - { - pathFilter, - contentFilter: /handleNextImageRequest/, - patchCode: createPatchCode(emptyHandleNextImageRequestRule), - }, - // Disable Next background preloading done at creation of `NextServer` - { - versions: ">=14.0.0", - pathFilter, - contentFilter: /this\.nextConfig\.experimental\.preloadEntriesOnStart/, - patchCode: createPatchCode(disablePreloadingRule), - }, - // Don't match edge functions in `NextServer` - { - // Next 12 and some version of 13 use the bundled middleware/edge function - versions: ">=14.0.0", - pathFilter, - contentFilter: /getMiddlewareManifest/, - patchCode: createPatchCode(removeMiddlewareManifestRule), - }, - ...babelPatches, - ], -}; +export * from "@opennextjs/core/build/patch/patches/patchNextServer.js"; diff --git a/packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts b/packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts index e864b194..e6c306f0 100644 --- a/packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts +++ b/packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts @@ -1,31 +1 @@ -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import { createPatchCode } from "../astCodePatcher.js"; -import type { CodePatcher } from "../codePatcher.js"; - -/** - * Drops `require("./node-environment-extensions/error-inspect");` - * - * This is to avoid pulling babel (~4MB) - */ -export const rule = ` -rule: - pattern: require("./node-environment-extensions/error-inspect"); -fix: |- - // Removed by OpenNext - // require("./node-environment-extensions/error-inspect"); -`; - -export const patchNodeEnvironment: CodePatcher = { - name: "patch-node-environment-error-inspect", - patches: [ - { - pathFilter: getCrossPlatformPathRegex(String.raw`/next/dist/server/node-environment\.js$`, { - escape: false, - }), - contentFilter: /error-inspect/, - patchCode: createPatchCode(rule), - versions: ">=15.0.0", - }, - ], -}; +export * from "@opennextjs/core/build/patch/patches/patchNodeEnvironment.js"; diff --git a/packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts b/packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts index ed14902f..a0a128b6 100644 --- a/packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts +++ b/packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts @@ -1,87 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import { build } from "esbuild"; - -import { inlineRequireResolvePlugin } from "../../../plugins/inline-require-resolve.js"; -import * as buildHelper from "../../helper.js"; - -/** - * Next 16.1.0-16.1.4 has missing fields in `required-server-files.json`: - * - `skipTrailingSlashRedirect` - * - `serverExternalPackages` - * - * This patch adds them back in by compiling and importing the user's `next.config.js` file. - * - * It is a regression in https://github.com/vercel/next.js/pull/86830 (16.1.0) - * Fixed in https://github.com/vercel/next.js/pull/88733 (16.1.4) - */ -export async function patchOriginalNextConfig(options: buildHelper.BuildOptions): Promise { - if ( - buildHelper.compareSemver(options.nextVersion, "<", "16.1.0") || - buildHelper.compareSemver(options.nextVersion, ">=", "16.1.4") - ) { - return; - } - - // The manifests in both `.next` and `.next/standalone` folders - // are patched as Open Next uses either of them. - const manifestPath = path.join(options.appBuildOutputPath, ".next/required-server-files.json"); - - const manifestStandalonePath = path.join( - options.appBuildOutputPath, - ".next/standalone", - buildHelper.getPackagePath(options), - ".next/required-server-files.json" - ); - - if (fs.existsSync(manifestPath)) { - const manifest = JSON.parse(await fs.promises.readFile(manifestPath, "utf-8")); - if (manifest.config.skipTrailingSlashRedirect === undefined) { - const { skipTrailingSlashRedirect, serverExternalPackages } = await importNextConfigFromSource(options); - manifest.config.skipTrailingSlashRedirect = skipTrailingSlashRedirect ?? false; - manifest.config.serverExternalPackages = serverExternalPackages ?? []; - await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); - if (fs.existsSync(manifestStandalonePath)) { - await fs.promises.writeFile(manifestStandalonePath, JSON.stringify(manifest, null, 2), "utf-8"); - } - } - } else { - throw new Error(`Could not find required-server-files.json at path: ${manifestPath}`); - } -} - -/** - * Compile and import the user's `next.config` file - * - * @returns - */ -async function importNextConfigFromSource(buildOptions: buildHelper.BuildOptions) { - const nextConfigDetails = buildHelper.findNextConfig(buildOptions); - - if (!nextConfigDetails) { - throw new Error("Could not find next.config file"); - } - - const { path: configPath, isTypescript: configIsTs } = nextConfigDetails; - - let configToImport: string; - - // Only compile if the extension is a TypeScript extension - if (configIsTs) { - await build({ - entryPoints: [configPath], - outfile: path.join(buildOptions.tempBuildDir, "next.config.mjs"), - bundle: true, - format: "esm", - platform: "node", - plugins: [inlineRequireResolvePlugin], - }); - configToImport = path.join(buildOptions.tempBuildDir, "next.config.mjs"); - } else { - // For .js, .mjs, .cjs, use the file directly - configToImport = configPath; - } - - return (await import(configToImport)).default; -} +export * from "@opennextjs/core/build/patch/patches/patchOriginalNextConfig.js"; diff --git a/packages/open-next/src/build/utils.ts b/packages/open-next/src/build/utils.ts index 89185130..70e96a8b 100644 --- a/packages/open-next/src/build/utils.ts +++ b/packages/open-next/src/build/utils.ts @@ -1,30 +1 @@ -import os from "node:os"; - -import logger from "../logger.js"; - -export function printHeader(header: string) { - // oxlint-disable-next-line no-param-reassign - header = `OpenNext — ${header}`; - logger.info( - [ - "", - `┌${"─".repeat(header.length + 2)}┐`, - `│ ${header} │`, - `└${"─".repeat(header.length + 2)}┘`, - "", - ].join("\n") - ); -} - -/** - * Displays a warning on windows platform. - */ -export function showWarningOnWindows() { - if (os.platform() !== "win32") return; - - logger.warn("OpenNext is not fully compatible with Windows."); - logger.warn("For optimal performance, it is recommended to use Windows Subsystem for Linux (WSL)."); - logger.warn( - "While OpenNext may function on Windows, it could encounter unpredictable failures during runtime." - ); -} +export * from "@opennextjs/core/build/utils.js"; diff --git a/packages/open-next/src/build/validateConfig.ts b/packages/open-next/src/build/validateConfig.ts index 22779d08..88be3ba3 100644 --- a/packages/open-next/src/build/validateConfig.ts +++ b/packages/open-next/src/build/validateConfig.ts @@ -1,92 +1 @@ -import type { - FunctionOptions, - IncludedConverter, - IncludedWrapper, - OpenNextConfig, - SplittedFunctionOptions, -} from "@/types/open-next"; - -import logger from "../logger.js"; - -const compatibilityMatrix: Record = { - "aws-lambda": ["aws-apigw-v1", "aws-apigw-v2", "aws-cloudfront", "sqs-revalidate"], - "aws-lambda-compressed": ["aws-apigw-v2"], - "aws-lambda-streaming": ["aws-apigw-v2"], - cloudflare: ["edge"], - "cloudflare-edge": ["edge"], - "cloudflare-node": ["edge"], - node: ["node"], - "express-dev": ["node"], - dummy: ["dummy"], -}; - -function validateFunctionOptions(fnOptions: FunctionOptions) { - const wrapper = typeof fnOptions.override?.wrapper === "string" ? fnOptions.override.wrapper : "aws-lambda"; - const converter = - typeof fnOptions.override?.converter === "string" ? fnOptions.override.converter : "aws-apigw-v2"; - if (fnOptions.override?.generateDockerfile && converter !== "node" && wrapper !== "node") { - logger.warn( - "You've specified generateDockerfile without node converter and wrapper. Without custom converter and wrapper the dockerfile will not work" - ); - } - if (converter === "aws-cloudfront" && fnOptions.placement !== "global") { - logger.warn( - "You've specified aws-cloudfront converter without global placement. This may not generate the correct output" - ); - } - const isCustomWrapper = typeof fnOptions.override?.wrapper === "function"; - const isCustomConverter = typeof fnOptions.override?.converter === "function"; - // Check if the wrapper and converter are compatible - // Only check if using one of the included converters or wrapper - if (!compatibilityMatrix[wrapper].includes(converter) && !isCustomWrapper && !isCustomConverter) { - logger.error( - `Wrapper ${wrapper} and converter ${converter} are not compatible. For the wrapper ${wrapper} you should only use the following converters: ${compatibilityMatrix[ - wrapper - ].join(", ")}` - ); - } -} - -function validateSplittedFunctionOptions(fnOptions: SplittedFunctionOptions, name: string) { - validateFunctionOptions(fnOptions); - if (fnOptions.routes.length === 0) { - throw new Error(`Splitted function ${name} must have at least one route`); - } - // Check if the routes are properly formated - fnOptions.routes.forEach((route) => { - if (!route.startsWith("app/") && !route.startsWith("pages/")) { - throw new Error( - `Route ${route} in function ${name} is not a valid route. It should starts with app/ or pages/ depending on if you use page or app router` - ); - } - }); - if (fnOptions.runtime === "edge" && fnOptions.routes.length > 1) { - throw new Error(`Edge function ${name} can only have one route`); - } -} - -export function validateConfig(config: OpenNextConfig) { - validateFunctionOptions(config.default); - Object.entries(config.functions ?? {}).forEach(([name, fnOptions]) => { - validateSplittedFunctionOptions(fnOptions, name); - }); - if (config.dangerous?.disableIncrementalCache) { - logger.warn("You've disabled incremental cache. This means that ISR and SSG will not work."); - } - if (config.dangerous?.disableTagCache) { - logger.warn( - `You've disabled tag cache. - This means that revalidatePath and revalidateTag from next/cache will not work. - It is safe to disable if you only use page router` - ); - } - validateFunctionOptions(config.imageOptimization ?? {}); - if (config.middleware?.external === true) { - validateFunctionOptions(config.middleware ?? {}); - } - //@ts-expect-error - Revalidate custom wrapper type is different - validateFunctionOptions(config.revalidate ?? {}); - //@ts-expect-error - Warmer custom wrapper type is different - validateFunctionOptions(config.warmer ?? {}); - validateFunctionOptions(config.initializationFunction ?? {}); -} +export * from "@opennextjs/core/build/validateConfig.js"; diff --git a/packages/open-next/src/core/createGenericHandler.ts b/packages/open-next/src/core/createGenericHandler.ts index a8674936..19938c40 100644 --- a/packages/open-next/src/core/createGenericHandler.ts +++ b/packages/open-next/src/core/createGenericHandler.ts @@ -1,48 +1 @@ -import type { - BaseEventOrResult, - DefaultOverrideOptions, - InternalEvent, - InternalResult, - OpenNextConfig, -} from "@/types/open-next"; -import type { OpenNextHandler } from "@/types/overrides"; - -import { debug } from "../adapters/logger"; - -import { resolveConverter, resolveWrapper } from "./resolve"; - -type HandlerType = "imageOptimization" | "revalidate" | "warmer" | "middleware" | "initializationFunction"; - -type GenericHandler< - Type extends HandlerType, - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, -> = { - handler: OpenNextHandler; - type: Type; -}; - -export async function createGenericHandler< - Type extends HandlerType, - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, ->(handler: GenericHandler) { - // @ts-expect-error `./open-next.config.mjs` exists only in the build output - const config: OpenNextConfig = await import("./open-next.config.mjs").then((m) => m.default); - - globalThis.openNextConfig = config; - const handlerConfig = config[handler.type]; - const override = - handlerConfig && "override" in handlerConfig - ? (handlerConfig.override as DefaultOverrideOptions) - : undefined; - - // From the config, we create the converter - const converter = await resolveConverter(override?.converter); - - // Then we create the handler - const { name, wrapper } = await resolveWrapper(override?.wrapper); - debug("Using wrapper", name); - - return wrapper(handler.handler, converter); -} +export * from "@opennextjs/core/core/createGenericHandler.js"; diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts index 480aa21b..793af42b 100644 --- a/packages/open-next/src/core/createMainHandler.ts +++ b/packages/open-next/src/core/createMainHandler.ts @@ -1,57 +1 @@ -import type { OpenNextConfig } from "@/types/open-next"; - -import { debug } from "../adapters/logger"; -import { generateUniqueId } from "../adapters/util"; - -import { openNextHandler } from "./requestHandler"; -import { - resolveAssetResolver, - resolveCdnInvalidation, - resolveConverter, - resolveIncrementalCache, - resolveProxyRequest, - resolveQueue, - resolveTagCache, - resolveWrapper, -} from "./resolve"; - -export async function createMainHandler() { - // @ts-expect-error `./open-next.config.mjs` exists only in the build output - const config: OpenNextConfig = await import("./open-next.config.mjs").then((m) => m.default); - - const thisFunction = globalThis.fnName ? config.functions![globalThis.fnName] : config.default; - - globalThis.serverId = generateUniqueId(); - globalThis.openNextConfig = config; - - // If route preloading behavior is set to start, it will wait for every single route to be preloaded before even creating the main handler. - //TODO: revisit that later - // await globalThis.__next_route_preloader("start"); - - // Default queue - globalThis.queue = await resolveQueue(thisFunction.override?.queue); - - globalThis.incrementalCache = await resolveIncrementalCache(thisFunction.override?.incrementalCache); - - globalThis.tagCache = await resolveTagCache(thisFunction.override?.tagCache); - - if (config.middleware?.external !== true) { - globalThis.assetResolver = await resolveAssetResolver( - globalThis.openNextConfig.middleware?.assetResolver - ); - } - - globalThis.proxyExternalRequest = await resolveProxyRequest(thisFunction.override?.proxyExternalRequest); - - globalThis.cdnInvalidationHandler = await resolveCdnInvalidation(thisFunction.override?.cdnInvalidation); - - // From the config, we create the converter - const converter = await resolveConverter(thisFunction.override?.converter); - - // Then we create the handler - const { wrapper, name } = await resolveWrapper(thisFunction.override?.wrapper); - - debug("Using wrapper", name); - - return wrapper(openNextHandler, converter); -} +export * from "@opennextjs/core/core/createMainHandler.js"; diff --git a/packages/open-next/src/core/edgeFunctionHandler.ts b/packages/open-next/src/core/edgeFunctionHandler.ts index 6f2168a4..b846bf72 100644 --- a/packages/open-next/src/core/edgeFunctionHandler.ts +++ b/packages/open-next/src/core/edgeFunctionHandler.ts @@ -1,30 +1,2 @@ -// Necessary files will be imported here with banner in esbuild - -import type { RequestData } from "@/types/global"; - -type EdgeRequest = Omit; - -export default async function edgeFunctionHandler(request: EdgeRequest): Promise { - const path = new URL(request.url).pathname; - const routes = globalThis._ROUTES; - const correspondingRoute = routes.find((route) => route.regex.some((r) => new RegExp(r).test(path))); - - if (!correspondingRoute) { - throw new Error(`No route found for ${request.url}`); - } - - const entry = await self._ENTRIES[`middleware_${correspondingRoute.name}`]; - - const result = await entry.default({ - page: correspondingRoute.page, - request: { - ...request, - page: { - name: correspondingRoute.name, - }, - }, - }); - globalThis.__openNextAls.getStore()?.pendingPromiseRunner.add(result.waitUntil); - const response = result.response; - return response; -} +export { default } from "@opennextjs/core/core/edgeFunctionHandler.js"; +export * from "@opennextjs/core/core/edgeFunctionHandler.js"; diff --git a/packages/open-next/src/core/nodeMiddlewareHandler.ts b/packages/open-next/src/core/nodeMiddlewareHandler.ts index 3e8095b0..6475ec53 100644 --- a/packages/open-next/src/core/nodeMiddlewareHandler.ts +++ b/packages/open-next/src/core/nodeMiddlewareHandler.ts @@ -1,40 +1,2 @@ -import type { RequestData } from "@/types/global"; - -type EdgeRequest = Omit; - -// Do we need Buffer here? -// oxlint-disable-next-line import/first -import { Buffer } from "node:buffer"; -globalThis.Buffer = Buffer; - -// AsyncLocalStorage is needed to be defined globally -// oxlint-disable-next-line import/first -import { AsyncLocalStorage } from "node:async_hooks"; -globalThis.AsyncLocalStorage = AsyncLocalStorage; - -interface NodeMiddleware { - default: (req: { handler: unknown; request: EdgeRequest; page: "middleware" }) => Promise<{ - response: Response; - waitUntil: Promise; - }>; - middleware: unknown; -} - -let _module: NodeMiddleware | undefined; - -export default async function middlewareHandler(request: EdgeRequest): Promise { - if (!_module) { - // We use await import here so that we are sure that it is loaded after AsyncLocalStorage is defined on globalThis - // We need both await here, same way as in https://github.com/opennextjs/opennextjs-aws/pull/704 - //@ts-expect-error - This file should be bundled with esbuild - _module = await (await import("./.next/server/middleware.js")).default; - } - const adapterFn = _module!.default || _module; - const result = await adapterFn({ - handler: _module!.middleware || _module, - request: request, - page: "middleware", - }); - globalThis.__openNextAls.getStore()?.pendingPromiseRunner.add(result.waitUntil); - return result.response; -} +export { default } from "@opennextjs/core/core/nodeMiddlewareHandler.js"; +export * from "@opennextjs/core/core/nodeMiddlewareHandler.js"; diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index aa07f1a5..50b5bcf0 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -1,242 +1 @@ -import { AsyncLocalStorage } from "node:async_hooks"; -import { Writable } from "node:stream"; -import { finished } from "node:stream/promises"; - -import { IncomingMessage } from "@/http/request"; -import type { InternalEvent, InternalResult, ResolvedRoute, RoutingResult } from "@/types/open-next"; -import type { OpenNextHandlerOptions } from "@/types/overrides"; -import { runWithOpenNextRequestContext } from "@/utils/promise"; - -import { debug, error } from "../adapters/logger"; - -import { adapterHandler } from "./routing/adapterHandler"; -import { constructNextUrl, convertRes, createServerResponse } from "./routing/util"; -import routingHandler, { - INTERNAL_EVENT_REQUEST_ID, - INTERNAL_HEADER_REWRITE_STATUS_CODE, - INTERNAL_HEADER_INITIAL_URL, - INTERNAL_HEADER_RESOLVED_ROUTES, - MIDDLEWARE_HEADER_PREFIX, - MIDDLEWARE_HEADER_PREFIX_LEN, -} from "./routingHandler"; - -// This is used to identify requests in the cache -globalThis.__openNextAls = new AsyncLocalStorage(); - -export async function openNextHandler( - internalEvent: InternalEvent, - options?: OpenNextHandlerOptions -): Promise { - const initialHeaders = internalEvent.headers; - // We only use the requestId header if we are using an external middleware - // This is to ensure that no one can spoof the requestId - // When using an external middleware, we always assume that headers cannot be spoofed - const requestId = globalThis.openNextConfig.middleware?.external - ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID] - : Math.random().toString(36); - // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer - return runWithOpenNextRequestContext( - { - isISRRevalidation: initialHeaders["x-isr"] === "1", - waitUntil: options?.waitUntil, - requestId, - }, - async () => { - // Disabled for now, we'll need to revisit this later if needed. - //TODO: revisit that later - // await globalThis.__next_route_preloader("waitUntil"); - if (initialHeaders["x-forwarded-host"]) { - initialHeaders.host = initialHeaders["x-forwarded-host"]; - } - debug("internalEvent", internalEvent); - - // These 3 will get overwritten by the routing handler if not using an external middleware - const internalHeaders = { - initialPath: initialHeaders[INTERNAL_HEADER_INITIAL_URL] ?? internalEvent.rawPath, - resolvedRoutes: initialHeaders[INTERNAL_HEADER_RESOLVED_ROUTES] - ? JSON.parse(initialHeaders[INTERNAL_HEADER_RESOLVED_ROUTES]) - : ([] as ResolvedRoute[]), - rewriteStatusCode: Number.parseInt(initialHeaders[INTERNAL_HEADER_REWRITE_STATUS_CODE]), - }; - - let routingResult: InternalResult | RoutingResult = { - internalEvent, - isExternalRewrite: false, - origin: false, - isISR: false, - initialURL: internalEvent.url, - ...internalHeaders, - }; - - //#override withRouting - routingResult = await routingHandler(internalEvent, { - assetResolver: globalThis.assetResolver, - }); - //#endOverride - - const headers = getHeaders(routingResult); - - const overwrittenResponseHeaders: Record = {}; - - for (const [rawKey, value] of Object.entries(headers)) { - if (!rawKey.startsWith(MIDDLEWARE_HEADER_PREFIX)) { - continue; - } - const key = rawKey.slice(MIDDLEWARE_HEADER_PREFIX_LEN); - // We skip this header here since it is used by Next internally and we don't want it on the response headers. - // This header needs to be present in the request headers for processRequest, so cookies().get() from Next will work on initial render. - if (key !== "x-middleware-set-cookie") { - overwrittenResponseHeaders[key] = value as string | string[]; - } - headers[key] = value; - delete headers[rawKey]; - } - - if ("isExternalRewrite" in routingResult && routingResult.isExternalRewrite === true) { - try { - routingResult = await globalThis.proxyExternalRequest.proxy(routingResult.internalEvent); - } catch (e) { - error("External request failed.", e); - routingResult = { - internalEvent: { - type: "core", - rawPath: "/500", - method: "GET", - headers: {}, - url: constructNextUrl(internalEvent.url, "/500"), - query: {}, - cookies: {}, - remoteAddress: "", - }, - // On error we need to rewrite to the 500 page which is an internal rewrite - isExternalRewrite: false, - isISR: false, - origin: false, - initialURL: internalEvent.url, - resolvedRoutes: [{ route: "/500", type: "page", isFallback: false }], - }; - } - } - - if ("type" in routingResult) { - // response is used only in the streaming case - if (options?.streamCreator) { - const response = createServerResponse( - { - internalEvent, - isExternalRewrite: false, - isISR: false, - resolvedRoutes: [], - origin: false, - initialURL: internalEvent.url, - }, - routingResult.headers, - options.streamCreator - ); - response.statusCode = routingResult.statusCode; - response.flushHeaders(); - const [bodyToConsume, bodyToReturn] = routingResult.body.tee(); - for await (const chunk of bodyToConsume) { - response.write(chunk); - } - response.end(); - routingResult.body = bodyToReturn; - } - return routingResult; - } - - const preprocessedEvent = routingResult.internalEvent; - debug("preprocessedEvent", preprocessedEvent); - const { search, pathname, hash } = new URL(preprocessedEvent.url); - const reqProps = { - method: preprocessedEvent.method, - url: `${pathname}${search}${hash}`, - //WORKAROUND: We pass this header to the serverless function to mimic a prefetch request which will not trigger revalidation since we handle revalidation differently - // There is 3 way we can handle revalidation: - // 1. We could just let the revalidation go as normal, but due to race conditions the revalidation will be unreliable - // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh - // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests) - headers: { - ...headers, - }, - body: preprocessedEvent.body, - remoteAddress: preprocessedEvent.remoteAddress, - }; - - const mergeHeadersPriority = globalThis.openNextConfig.dangerous?.headersAndCookiesPriority - ? globalThis.openNextConfig.dangerous.headersAndCookiesPriority(preprocessedEvent) - : "middleware"; - const store = globalThis.__openNextAls.getStore(); - if (store) { - store.mergeHeadersPriority = mergeHeadersPriority; - } - - const req = new IncomingMessage(reqProps); - const res = createServerResponse( - routingResult, - routingResult.initialResponse ? routingResult.initialResponse.headers : overwrittenResponseHeaders, - options?.streamCreator - ); - - if (routingResult.initialResponse) { - res.statusCode = routingResult.initialResponse.statusCode; - res.flushHeaders(); - for await (const chunk of routingResult.initialResponse.body) { - res.write(chunk); - } - - //We create a special response for the PPR resume request - const pprRes = createServerResponse(routingResult, overwrittenResponseHeaders, { - writeHeaders: () => { - return new Writable({ - write(chunk, encoding, callback) { - res.write(chunk, encoding, callback); - }, - }); - }, - }); - await adapterHandler(req, pprRes, routingResult, { - waitUntil: options?.waitUntil, - }); - await finished(pprRes); - res.end(); - - return convertRes(res); - } - - // It seems that Next.js doesn't set the status code for 404 and 500 anymore for us, we have to do it ourselves - // TODO: check security wise if it's ok to do that - if (pathname === "/404") { - res.statusCode = 404; - } else if (pathname === "/500") { - res.statusCode = 500; - } - - //#override useAdapterHandler - await adapterHandler(req, res, routingResult, { - waitUntil: options?.waitUntil, - }); - //#endOverride - - const { statusCode, headers: responseHeaders, isBase64Encoded, body } = convertRes(res); - - const internalResult = { - type: internalEvent.type, - statusCode, - headers: responseHeaders, - body, - isBase64Encoded, - }; - - return internalResult; - } - ); -} - -function getHeaders(routingResult: RoutingResult | InternalResult) { - if ("type" in routingResult) { - return routingResult.headers; - } else { - return routingResult.internalEvent.headers; - } -} +export * from "@opennextjs/core/core/requestHandler.js"; diff --git a/packages/open-next/src/core/resolve.ts b/packages/open-next/src/core/resolve.ts index a6d41389..b2eec57a 100644 --- a/packages/open-next/src/core/resolve.ts +++ b/packages/open-next/src/core/resolve.ts @@ -1,159 +1 @@ -import type { - BaseEventOrResult, - DefaultOverrideOptions, - ExternalMiddlewareConfig, - InternalEvent, - InternalResult, - OpenNextConfig, - OverrideOptions, -} from "@/types/open-next"; -import type { Converter, TagCache, Wrapper } from "@/types/overrides"; - -// Just a little utility type to remove undefined from a type -type RemoveUndefined = T extends undefined ? never : T; - -export async function resolveConverter< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, ->(converter: DefaultOverrideOptions["converter"]): Promise> { - if (typeof converter === "function") { - return converter(); - } - const m_1 = await import("../overrides/converters/aws-apigw-v2.js"); - // @ts-expect-error - return m_1.default; -} - -export async function resolveWrapper< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, ->(wrapper: DefaultOverrideOptions["wrapper"]): Promise> { - if (typeof wrapper === "function") { - return wrapper(); - } - // This will be replaced by the bundler - const m_1 = await import("../overrides/wrappers/aws-lambda.js"); - // @ts-expect-error - return m_1.default; -} - -/** - * - * @param tagCache - * @returns - * @__PURE__ - */ -export async function resolveTagCache(tagCache: OverrideOptions["tagCache"]): Promise { - if (typeof tagCache === "function") { - return tagCache(); - } - // This will be replaced by the bundler - const m_1 = await import("../overrides/tagCache/dynamodb.js"); - return m_1.default; -} - -/** - * - * @param queue - * @returns - * @__PURE__ - */ -export async function resolveQueue(queue: OverrideOptions["queue"]) { - if (typeof queue === "function") { - return queue(); - } - const m_1 = await import("../overrides/queue/sqs.js"); - return m_1.default; -} - -/** - * - * @param incrementalCache - * @returns - * @__PURE__ - */ -export async function resolveIncrementalCache(incrementalCache: OverrideOptions["incrementalCache"]) { - if (typeof incrementalCache === "function") { - return incrementalCache(); - } - const m_1 = await import("../overrides/incrementalCache/s3.js"); - return m_1.default; -} - -/** - * @param imageLoader - * @returns - * @__PURE__ - */ -export async function resolveImageLoader( - imageLoader: RemoveUndefined["loader"] -) { - if (typeof imageLoader === "function") { - return imageLoader(); - } - const m_1 = await import("../overrides/imageLoader/s3.js"); - return m_1.default; -} - -/** - * @returns - * @__PURE__ - */ -export async function resolveOriginResolver( - originResolver: RemoveUndefined["originResolver"] -) { - if (typeof originResolver === "function") { - return originResolver(); - } - const m_1 = await import("../overrides/originResolver/pattern-env.js"); - return m_1.default; -} - -/** - * @returns - * @__PURE__ - */ -export async function resolveAssetResolver( - assetResolver: RemoveUndefined["assetResolver"] -) { - if (typeof assetResolver === "function") { - return assetResolver(); - } - const m_1 = await import("../overrides/assetResolver/dummy.js"); - return m_1.default; -} - -/** - * @__PURE__ - */ -export async function resolveWarmerInvoke( - warmer: RemoveUndefined["invokeFunction"] -) { - if (typeof warmer === "function") { - return warmer(); - } - const m_1 = await import("../overrides/warmer/aws-lambda.js"); - return m_1.default; -} - -/** - * @__PURE__ - */ -export async function resolveProxyRequest(proxyRequest: OverrideOptions["proxyExternalRequest"]) { - if (typeof proxyRequest === "function") { - return proxyRequest(); - } - const m_1 = await import("../overrides/proxyExternalRequest/node.js"); - return m_1.default; -} - -/** - * @__PURE__ - */ -export async function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]) { - if (typeof cdnInvalidation === "function") { - return cdnInvalidation(); - } - const m_1 = await import("../overrides/cdnInvalidation/dummy.js"); - return m_1.default; -} +export * from "@opennextjs/core/core/resolve.js"; diff --git a/packages/open-next/src/core/routing/adapterHandler.ts b/packages/open-next/src/core/routing/adapterHandler.ts index 81b7e341..97757fe9 100644 --- a/packages/open-next/src/core/routing/adapterHandler.ts +++ b/packages/open-next/src/core/routing/adapterHandler.ts @@ -1,120 +1 @@ -import { finished } from "node:stream/promises"; - -import type { OpenNextNodeResponse } from "@/http/index"; -import type { IncomingMessage } from "@/http/request"; -import type { ResolvedRoute, RoutingResult, WaitUntil } from "@/types/open-next"; - -/** - * This function loads the necessary routes, and invoke the expected handler. - * @param routingResult The result of the routing process, containing information about the matched route and any parameters. - */ -export async function adapterHandler( - req: IncomingMessage, - res: OpenNextNodeResponse, - routingResult: RoutingResult, - options: { - waitUntil?: WaitUntil; - } = {} -) { - let resolved = false; - - const pendingPromiseRunner = globalThis.__openNextAls.getStore()?.pendingPromiseRunner; - const waitUntil = options.waitUntil ?? pendingPromiseRunner?.add.bind(pendingPromiseRunner); - - // Our internal routing could return /500 or /404 routes, we first check that - if (routingResult.internalEvent.rawPath === "/404") { - await handle404(req, res, waitUntil); - return; - } - if (routingResult.internalEvent.rawPath === "/500") { - await handle500(req, res, waitUntil); - return; - } - - //TODO: replace this at runtime with a version precompiled for the cloudflare adapter. - for (const route of routingResult.resolvedRoutes) { - const module = getHandler(route); - if (!module || resolved) { - return; - } - - try { - console.log("## adapterHandler trying route", route, req.url); - const result = await module.handler(req, res, { - waitUntil, - }); - console.log("## adapterHandler route succeeded", route); - resolved = true; - return result; - //If it doesn't throw, we are done - } catch (e) { - console.log("## adapterHandler route failed", route, e); - // I'll have to run some more tests, but in theory, we should not have anything special to do here, and we should return the 500 page here. - await handle500(req, res, waitUntil); - return; - } - } - if (!resolved) { - console.log("## adapterHandler no route resolved for", req.url); - await handle404(req, res, waitUntil); - return; - } -} - -async function handle404(req: IncomingMessage, res: OpenNextNodeResponse, waitUntil?: WaitUntil) { - try { - // TODO: find the correct one to use. - const module = getHandler({ - route: "/_not-found", - type: "app", - isFallback: false, - }); - if (module) { - await module.handler(req, res, { - waitUntil, - }); - return; - } - } catch (e2) { - console.log("## adapterHandler not found route also failed", e2); - } - // Ideally we should never reach here as the 404 page should be the Next.js one. - res.statusCode = 404; - res.end("Not Found"); - await finished(res); -} - -async function handle500(req: IncomingMessage, res: OpenNextNodeResponse, waitUntil?: WaitUntil) { - try { - // TODO: find the correct one to use. - const module = getHandler({ - route: "/_global-error", - type: "app", - isFallback: false, - }); - if (module) { - await module.handler(req, res, { - waitUntil, - }); - return; - } - } catch (e2) { - console.log("## adapterHandler global error route also failed", e2); - } - res.statusCode = 500; - res.end("Internal Server Error"); - await finished(res); -} - -// Body replaced at build time -function getHandler(route: ResolvedRoute): - | undefined - | { - handler: ( - req: IncomingMessage, - res: OpenNextNodeResponse, - options: { waitUntil?: (promise: Promise) => void } - ) => Promise; - } { - return undefined; -} +export * from "@opennextjs/core/core/routing/adapterHandler.js"; diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts index 271b123f..f519f571 100644 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ b/packages/open-next/src/core/routing/cacheInterceptor.ts @@ -1,418 +1 @@ -import { createHash } from "node:crypto"; - -import { NextConfig, PrerenderManifest } from "@/config/index"; -import type { InternalEvent, InternalResult, MiddlewareEvent, PartialResult } from "@/types/open-next"; -import type { CacheValue } from "@/types/overrides"; -import { isBinaryContentType } from "@/utils/binary"; -import { getTagsFromValue, hasBeenRevalidated } from "@/utils/cache"; -import { emptyReadableStream, toReadableStream } from "@/utils/stream"; - -import { debug, error } from "../../adapters/logger"; - -import { localizePath } from "./i18n"; -import { generateMessageGroupId } from "./queue"; - -const CACHE_ONE_YEAR = 60 * 60 * 24 * 365; -const CACHE_ONE_MONTH = 60 * 60 * 24 * 30; - -/* - * We use this header to prevent Firefox (and possibly some CDNs) from incorrectly reusing the RSC responses during caching. - * This can especially happen when there's a redirect in the middleware as the `_rsc` query parameter is not visible there. - * So it will get dropped during the redirect, which results in the RSC response being cached instead of the actual HTML on the path `/`. - * This value can be found in the routes manifest, under `rsc.varyHeader`. - * They recompute it here in Next: - * https://github.com/vercel/next.js/blob/c5bf5bb4c8b01b1befbbfa7ad97a97476ee9d0d7/packages/next/src/server/base-server.ts#L2011 - * Also see this PR: https://github.com/vercel/next.js/pull/79426 - */ -const VARY_HEADER = - "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url"; -const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch"; -const NEXT_PRERENDER_HEADER = "x-nextjs-prerender"; -const NEXT_POSTPONED_HEADER = "x-nextjs-postponed"; - -async function computeCacheControl( - path: string, - body: string, - host: string, - revalidate?: number | false, - lastModified?: number -) { - let finalRevalidate = CACHE_ONE_YEAR; - - const existingRoute = Object.entries(PrerenderManifest?.routes ?? {}).find((p) => p[0] === path)?.[1]; - if (revalidate === undefined && existingRoute) { - finalRevalidate = - existingRoute.initialRevalidateSeconds === false - ? CACHE_ONE_YEAR - : existingRoute.initialRevalidateSeconds; - } else if (revalidate !== undefined) { - finalRevalidate = revalidate === false ? CACHE_ONE_YEAR : revalidate; - } - // calculate age - const age = Math.round((Date.now() - (lastModified ?? 0)) / 1000); - const hash = (str: string) => createHash("md5").update(str).digest("hex"); - const etag = hash(body); - if (revalidate === 0) { - // This one should never happen - return { - "cache-control": "private, no-cache, no-store, max-age=0, must-revalidate", - "x-opennext-cache": "ERROR", - etag, - }; - } - if (finalRevalidate !== CACHE_ONE_YEAR) { - const sMaxAge = Math.max(finalRevalidate - age, 1); - debug("sMaxAge", { - finalRevalidate, - age, - lastModified, - revalidate, - }); - const isStale = sMaxAge === 1; - if (isStale) { - let url = NextConfig.trailingSlash ? `${path}/` : path; - if (NextConfig.basePath) { - // We need to add the basePath to the url - url = `${NextConfig.basePath}${url}`; - } - await globalThis.queue.send({ - MessageBody: { - host, - url, - eTag: etag, - lastModified: lastModified ?? Date.now(), - }, - MessageDeduplicationId: hash(`${path}-${lastModified}-${etag}`), - MessageGroupId: generateMessageGroupId(path), - }); - } - return { - "cache-control": `s-maxage=${sMaxAge}, stale-while-revalidate=${CACHE_ONE_MONTH}`, - "x-opennext-cache": isStale ? "STALE" : "HIT", - etag, - }; - } - return { - "cache-control": `s-maxage=${CACHE_ONE_YEAR}, stale-while-revalidate=${CACHE_ONE_MONTH}`, - "x-opennext-cache": "HIT", - etag, - }; -} - -function getBodyForAppRouter( - event: MiddlewareEvent, - cachedValue: CacheValue<"cache"> -): { body: string; additionalHeaders: Record } { - if (cachedValue.type !== "app") { - throw new Error("getBodyForAppRouter called with non-app cache value"); - } - try { - const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`; - const isSegmentResponse = Boolean(segmentHeader) && segmentHeader in (cachedValue.segmentData || {}); - - const body = isSegmentResponse ? cachedValue.segmentData![segmentHeader] : cachedValue.rsc; - return { - body, - additionalHeaders: isSegmentResponse - ? { [NEXT_PRERENDER_HEADER]: "1", [NEXT_POSTPONED_HEADER]: "2" } - : {}, - }; - } catch (e) { - error("Error while getting body for app router from cache:", e); - return { body: cachedValue.rsc, additionalHeaders: {} }; - } -} - -function createPprPartialResult( - event: MiddlewareEvent, - localizedPath: string, - cachedValue: CacheValue<"cache">, - responseBody: string | (() => ReturnType), - contentType: string -): PartialResult { - if (cachedValue.type !== "app") { - throw new Error("createPprPartialResult called with non-app cache value"); - } - - return { - resumeRequest: { - ...event, - method: "POST", - url: `http://${event.headers.host}${NextConfig.basePath || ""}${localizedPath || "/"}`, - headers: { - ...event.headers, - "next-resume": "1", - }, - rawPath: localizedPath, - body: Buffer.from(cachedValue.meta?.postponed || "", "utf-8"), - }, - result: { - type: "core", - statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, - body: typeof responseBody === "string" ? toReadableStream(responseBody) : responseBody(), - isBase64Encoded: false, - headers: { - "content-type": contentType, - "x-opennext-ppr": "1", - ...cachedValue.meta?.headers, - vary: VARY_HEADER, - }, - }, - }; -} - -async function generateResult( - event: MiddlewareEvent, - localizedPath: string, - cachedValue: CacheValue<"cache">, - lastModified?: number -): Promise { - debug("Returning result from experimental cache"); - let body = ""; - let type = "application/octet-stream"; - let isDataRequest = false; - let additionalHeaders = {}; - if (cachedValue.type === "app") { - isDataRequest = Boolean(event.headers.rsc); - if (isDataRequest) { - const { body: appRouterBody, additionalHeaders: appHeaders } = getBodyForAppRouter(event, cachedValue); - body = appRouterBody; - additionalHeaders = appHeaders; - - if (cachedValue.meta?.postponed) { - if (event.headers["next-router-prefetch"] === "1") { - debug("Prefetch request detected, returning cached response without postponing"); - // We try to find the corresponding segment for the prefetch request, if it exists. - const segmentToFind = event.headers[NEXT_SEGMENT_PREFETCH_HEADER]; - if (segmentToFind && cachedValue.segmentData?.[segmentToFind]) { - body = cachedValue.segmentData[segmentToFind]; - additionalHeaders = { [NEXT_PRERENDER_HEADER]: "1", [NEXT_POSTPONED_HEADER]: "2" }; - debug("Found segment for prefetch request, returning it"); - } else { - debug("No segment found for prefetch request, returning full response"); - } - } else { - debug("App router postponed request detected", localizedPath); - return createPprPartialResult( - event, - localizedPath, - cachedValue, - () => emptyReadableStream(), - "text/x-component" - ); - } - } - debug("App router data request detected", localizedPath, body); - } else { - if (cachedValue.meta?.postponed) { - debug("Postponed request detected", localizedPath); - return createPprPartialResult( - event, - localizedPath, - cachedValue, - cachedValue.html, - "text/html; charset=utf-8" - ); - } else { - body = cachedValue.html; - } - } - type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8"; - } else if (cachedValue.type === "page") { - isDataRequest = Boolean(event.query.__nextDataReq); - body = isDataRequest ? JSON.stringify(cachedValue.json) : cachedValue.html; - type = isDataRequest ? "application/json" : "text/html; charset=utf-8"; - } else { - throw new Error( - "generateResult called with unsupported cache value type, only 'app' and 'page' are supported" - ); - } - const cacheControl = await computeCacheControl( - localizedPath, - body, - event.headers.host, - cachedValue.revalidate, - lastModified - ); - return { - type: "core", - // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code - // Also set the status code to the rewriteStatusCode if defined - // This can happen in handleMiddleware in routingHandler. - // `NextResponse.rewrite(url, { status: xxx}) - // The rewrite status code should take precedence over the cached one - statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200, - body: toReadableStream(body, isBinaryContentType(type)), - isBase64Encoded: false, - headers: { - ...cacheControl, - "content-type": type, - ...cachedValue.meta?.headers, - vary: VARY_HEADER, - ...additionalHeaders, - }, - }; -} - -/** - * - * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts#L2-L10 - */ -function escapePathDelimiters(segment: string, escapeEncoded?: boolean): string { - return segment.replace( - new RegExp(`([/#?]${escapeEncoded ? "|%(2f|23|3f|5c)" : ""})`, "gi"), - (char: string) => encodeURIComponent(char) - ); -} - -/** - * - * SSG cache key needs to be decoded, but some characters needs to be properly escaped - * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26 - */ -function decodePathParams(pathname: string): string { - return pathname - .split("/") - .map((segment) => { - try { - return escapePathDelimiters(decodeURIComponent(segment), true); - } catch (e) { - // If decodeURIComponent fails, we return the original segment - return segment; - } - }) - .join("/"); -} - -export async function cacheInterceptor( - event: MiddlewareEvent -): Promise { - if ( - Boolean(event.headers["next-action"]) || - Boolean(event.headers["x-prerender-revalidate"]) || - Boolean(event.headers["next-resume"]) || - event.method !== "GET" - ) - return event; - - // Check for Next.js preview mode cookies - const cookies = event.headers.cookie || ""; - const hasPreviewData = cookies.includes("__prerender_bypass") || cookies.includes("__next_preview_data"); - - if (hasPreviewData) { - debug("Preview mode detected, passing through to handler"); - return event; - } - // We localize the path in case i18n is enabled - let localizedPath = localizePath(event); - // If using basePath we need to remove it from the path - if (NextConfig.basePath) { - localizedPath = localizedPath.replace(NextConfig.basePath, ""); - } - // We also need to remove trailing slash - localizedPath = localizedPath.replace(/\/$/, ""); - - // Then we decode the path params - localizedPath = decodePathParams(localizedPath); - - debug("Checking cache for", localizedPath, PrerenderManifest); - - const isDynamicISR = Object.values(PrerenderManifest?.dynamicRoutes ?? {}).some((dr) => { - const regex = new RegExp(dr.routeRegex); - return regex.test(localizedPath); - }); - - const isStaticRoute = Object.keys(PrerenderManifest?.routes ?? {}).includes(localizedPath || "/"); - - const isISR = isStaticRoute || isDynamicISR; - debug("isISR", isISR); - if (isISR) { - try { - let pathToUse = localizedPath; - // For PPR, we need to check the fallback value to get the correct cache key - // We don't want to override a static route though - if (isDynamicISR && !isStaticRoute) { - const fallback = Object.entries(PrerenderManifest?.dynamicRoutes ?? {}).find(([, dr]) => { - const regex = new RegExp(dr.routeRegex); - return regex.test(localizedPath); - })?.[1].fallback; - pathToUse = typeof fallback === "string" ? fallback : localizedPath; - } else if (localizedPath === "") { - pathToUse = "/index"; - } - const cachedData = await globalThis.incrementalCache.get(pathToUse); - debug("cached data in interceptor", cachedData); - - if (!cachedData?.value) { - return event; - } - // We need to check the tag cache now - if (cachedData.value?.type === "app" || cachedData.value?.type === "route") { - const tags = getTagsFromValue(cachedData.value); - - const _hasBeenRevalidated = cachedData.shouldBypassTagCache - ? false - : await hasBeenRevalidated(localizedPath, tags, cachedData); - - if (_hasBeenRevalidated) { - return event; - } - } - const host = event.headers.host; - switch (cachedData?.value?.type) { - case "app": - case "page": - return generateResult(event, localizedPath, cachedData.value, cachedData.lastModified); - case "redirect": { - const cacheControl = await computeCacheControl( - localizedPath, - "", - host, - cachedData.value.revalidate, - cachedData.lastModified - ); - return { - type: "core", - statusCode: cachedData.value.meta?.status ?? 307, - body: emptyReadableStream(), - headers: { - ...cachedData.value.meta?.headers, - ...cacheControl, - }, - isBase64Encoded: false, - }; - } - case "route": { - const cacheControl = await computeCacheControl( - localizedPath, - cachedData.value.body, - host, - cachedData.value.revalidate, - cachedData.lastModified - ); - - const isBinary = isBinaryContentType(String(cachedData.value.meta?.headers?.["content-type"])); - - return { - type: "core", - statusCode: event.rewriteStatusCode ?? cachedData.value.meta?.status ?? 200, - body: toReadableStream(cachedData.value.body, isBinary), - headers: { - ...cacheControl, - ...cachedData.value.meta?.headers, - vary: VARY_HEADER, - }, - isBase64Encoded: isBinary, - }; - } - default: - return event; - } - } catch (e) { - debug("Error while fetching cache", e); - // In case of error we fallback to the server - return event; - } - } - return event; -} +export * from "@opennextjs/core/core/routing/cacheInterceptor.js"; diff --git a/packages/open-next/src/core/routing/i18n/accept-header.ts b/packages/open-next/src/core/routing/i18n/accept-header.ts index 19314a9a..b3eb55b9 100644 --- a/packages/open-next/src/core/routing/i18n/accept-header.ts +++ b/packages/open-next/src/core/routing/i18n/accept-header.ts @@ -1,136 +1 @@ -// Copied from Next.js source code -// https://github.com/vercel/next.js/blob/canary/packages/next/src/server/accept-header.ts - -interface Selection { - pos: number; - pref?: number; - q: number; - token: string; -} - -interface Options { - prefixMatch?: boolean; - type: "accept-language"; -} - -function parse(raw: string, preferences: string[] | undefined, options: Options) { - const lowers = new Map(); - const header = raw.replace(/[ \t]/g, ""); - - if (preferences) { - let pos = 0; - for (const preference of preferences) { - const lower = preference.toLowerCase(); - lowers.set(lower, { orig: preference, pos: pos++ }); - if (options.prefixMatch) { - const parts = lower.split("-"); - while ((parts.pop(), parts.length > 0)) { - const joined = parts.join("-"); - if (!lowers.has(joined)) { - lowers.set(joined, { orig: preference, pos: pos++ }); - } - } - } - } - } - - const parts = header.split(","); - const selections: Selection[] = []; - const map = new Set(); - - for (let i = 0; i < parts.length; ++i) { - const part = parts[i]; - if (!part) { - continue; - } - - const params = part.split(";"); - if (params.length > 2) { - throw new Error(`Invalid ${options.type} header`); - } - - const token = params[0].toLowerCase(); - if (!token) { - throw new Error(`Invalid ${options.type} header`); - } - - const selection: Selection = { token, pos: i, q: 1 }; - if (preferences && lowers.has(token)) { - selection.pref = lowers.get(token)!.pos; - } - - map.add(selection.token); - - if (params.length === 2) { - const q = params[1]; - const [key, value] = q.split("="); - - if (!value || (key !== "q" && key !== "Q")) { - throw new Error(`Invalid ${options.type} header`); - } - - const score = Number.parseFloat(value); - if (score === 0) { - continue; - } - - if (Number.isFinite(score) && score <= 1 && score >= 0.001) { - selection.q = score; - } - } - - selections.push(selection); - } - - selections.sort((a, b) => { - if (b.q !== a.q) { - return b.q - a.q; - } - - if (b.pref !== a.pref) { - if (a.pref === undefined) { - return 1; - } - - if (b.pref === undefined) { - return -1; - } - - return a.pref - b.pref; - } - - return a.pos - b.pos; - }); - - const values = selections.map((selection) => selection.token); - if (!preferences || !preferences.length) { - return values; - } - - const preferred: string[] = []; - for (const selection of values) { - if (selection === "*") { - for (const [preference, value] of lowers) { - if (!map.has(preference)) { - preferred.push(value.orig); - } - } - } else { - const lower = selection.toLowerCase(); - if (lowers.has(lower)) { - preferred.push(lowers.get(lower)!.orig); - } - } - } - - return preferred; -} - -export function acceptLanguage(header = "", preferences?: string[]) { - return ( - parse(header, preferences, { - type: "accept-language", - prefixMatch: true, - })[0] || undefined - ); -} +export * from "@opennextjs/core/core/routing/i18n/accept-header.js"; diff --git a/packages/open-next/src/core/routing/i18n/index.ts b/packages/open-next/src/core/routing/i18n/index.ts index 8bb7c8be..ffc76e3b 100644 --- a/packages/open-next/src/core/routing/i18n/index.ts +++ b/packages/open-next/src/core/routing/i18n/index.ts @@ -1,158 +1 @@ -import { NextConfig } from "@/config/index.js"; -import type { DomainLocale, i18nConfig } from "@/types/next-types"; -import type { InternalEvent, InternalResult } from "@/types/open-next"; -import { emptyReadableStream } from "@/utils/stream.js"; - -import { debug } from "../../../adapters/logger.js"; -import { constructNextUrl, convertToQueryString } from "../util.js"; - -import { acceptLanguage } from "./accept-header"; - -function isLocalizedPath(path: string): boolean { - return NextConfig.i18n?.locales.includes(path.split("/")[1].toLowerCase()) ?? false; -} - -// https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/get-locale-redirect.ts -function getLocaleFromCookie(cookies: Record) { - const i18n = NextConfig.i18n; - const nextLocale = cookies.NEXT_LOCALE?.toLowerCase(); - return nextLocale ? i18n?.locales.find((locale) => nextLocale === locale.toLowerCase()) : undefined; -} - -// Inspired by https://github.com/vercel/next.js/blob/6d93d652e0e7ba72d9a3b66e78746dce2069db03/packages/next/src/shared/lib/i18n/detect-domain-locale.ts#L3-L25 -/** - * @param arg an object containing the hostname and detectedLocale - * @returns The `DomainLocale` object if a domain is detected, `undefined` otherwise - */ -export function detectDomainLocale({ - hostname, - detectedLocale, -}: { - hostname?: string; - detectedLocale?: string; -}): DomainLocale | undefined { - const i18n = NextConfig.i18n; - const domains = i18n?.domains; - if (!domains) { - return; - } - const lowercasedLocale = detectedLocale?.toLowerCase(); - for (const domain of domains) { - // We remove the port if present - const domainHostname = domain.domain.split(":", 1)[0].toLowerCase(); - if ( - hostname === domainHostname || - lowercasedLocale === domain.defaultLocale.toLowerCase() || - domain.locales?.some((locale) => lowercasedLocale === locale.toLowerCase()) - ) { - return domain; - } - } -} - -/** - * - * @param internalEvent - * @param i18n - * @returns The detected locale, if `localeDetection` is set to `false` it will return the default locale **or** the domain default locale if a domain is detected. - */ -export function detectLocale(internalEvent: InternalEvent, i18n: i18nConfig): string { - const domainLocale = detectDomainLocale({ - hostname: internalEvent.headers.host, - }); - if (i18n.localeDetection === false) { - return domainLocale?.defaultLocale ?? i18n.defaultLocale; - } - - const cookiesLocale = getLocaleFromCookie(internalEvent.cookies); - const preferredLocale = acceptLanguage(internalEvent.headers["accept-language"], i18n?.locales); - debug({ - cookiesLocale, - preferredLocale, - defaultLocale: i18n.defaultLocale, - domainLocale, - }); - - return domainLocale?.defaultLocale ?? cookiesLocale ?? preferredLocale ?? i18n.defaultLocale; -} - -/** - * This function is used for OpenNext internal routing to localize the path for next config rewrite/redirects/headers and the middleware - * @param internalEvent - * @returns The localized path - */ -export function localizePath(internalEvent: InternalEvent): string { - const i18n = NextConfig.i18n; - if (!i18n) { - return internalEvent.rawPath; - } - // When the path is already localized we don't need to do anything - if (isLocalizedPath(internalEvent.rawPath)) { - return internalEvent.rawPath; - } - - const detectedLocale = detectLocale(internalEvent, i18n); - - return `/${detectedLocale}${internalEvent.rawPath}`; -} - -/** - * - * @param internalEvent - * In this function, for domain locale redirect we need to rely on the host to be present and correct - * @returns `false` if no redirect is needed, `InternalResult` if a redirect is needed - */ -export function handleLocaleRedirect(internalEvent: InternalEvent): false | InternalResult { - const i18n = NextConfig.i18n; - if (!i18n || i18n.localeDetection === false || internalEvent.rawPath !== "/") { - return false; - } - const preferredLocale = acceptLanguage(internalEvent.headers["accept-language"], i18n?.locales); - - const detectedLocale = detectLocale(internalEvent, i18n); - - const domainLocale = detectDomainLocale({ - hostname: internalEvent.headers.host, - }); - const preferredDomain = detectDomainLocale({ - detectedLocale: preferredLocale, - }); - - if (domainLocale && preferredDomain) { - const isPDomain = preferredDomain.domain === domainLocale.domain; - const isPLocale = preferredDomain.defaultLocale === preferredLocale; - if (!isPDomain || !isPLocale) { - const scheme = `http${preferredDomain.http ? "" : "s"}`; - const rlocale = isPLocale ? "" : preferredLocale; - return { - type: "core", - statusCode: 307, - headers: { - Location: `${scheme}://${preferredDomain.domain}/${rlocale}`, - }, - body: emptyReadableStream(), - isBase64Encoded: false, - }; - } - } - - const defaultLocale = domainLocale?.defaultLocale ?? i18n.defaultLocale; - - if (detectedLocale.toLowerCase() !== defaultLocale.toLowerCase()) { - const nextUrl = constructNextUrl( - internalEvent.url, - `/${detectedLocale}${NextConfig.trailingSlash ? "/" : ""}` - ); - const queryString = convertToQueryString(internalEvent.query); - return { - type: "core", - statusCode: 307, - headers: { - Location: `${nextUrl}${queryString}`, - }, - body: emptyReadableStream(), - isBase64Encoded: false, - }; - } - return false; -} +export * from "@opennextjs/core/core/routing/i18n/index.js"; diff --git a/packages/open-next/src/core/routing/matcher.ts b/packages/open-next/src/core/routing/matcher.ts index f91816a1..93f2d035 100644 --- a/packages/open-next/src/core/routing/matcher.ts +++ b/packages/open-next/src/core/routing/matcher.ts @@ -1,430 +1 @@ -import type { Match, MatchFunction, PathFunction } from "path-to-regexp"; -import { compile, match } from "path-to-regexp"; - -import { NextConfig } from "@/config/index"; -import type { - Header, - PrerenderManifest, - RedirectDefinition, - RewriteDefinition, - RouteHas, -} from "@/types/next-types"; -import type { InternalEvent, InternalResult } from "@/types/open-next"; -import { normalizeRepeatedSlashes } from "@/utils/normalize-path"; -import { emptyReadableStream, toReadableStream } from "@/utils/stream"; - -import { debug } from "../../adapters/logger"; - -import { handleLocaleRedirect, localizePath } from "./i18n"; -import { dynamicRouteMatcher, staticRouteMatcher } from "./routeMatcher"; -import { - constructNextUrl, - convertFromQueryString, - convertToQueryString, - escapeRegex, - getUrlParts, - isExternal, - unescapeRegex, -} from "./util"; - -const routeHasMatcher = - ( - headers: Record, - cookies: Record, - query: Record - ) => - (redirect: RouteHas): boolean => { - switch (redirect.type) { - case "header": - return ( - !!headers?.[redirect.key.toLowerCase()] && - new RegExp(redirect.value ?? "").test(headers[redirect.key.toLowerCase()] ?? "") - ); - case "cookie": - return ( - !!cookies?.[redirect.key] && new RegExp(redirect.value ?? "").test(cookies[redirect.key] ?? "") - ); - case "query": - return query[redirect.key] && Array.isArray(redirect.value) - ? redirect.value.reduce( - (prev, current) => prev || new RegExp(current).test(query[redirect.key] as string), - false - ) - : new RegExp(redirect.value ?? "").test((query[redirect.key] as string | undefined) ?? ""); - case "host": - return headers?.host !== "" && new RegExp(redirect.value ?? "").test(headers.host); - default: - return false; - } - }; - -function checkHas(matcher: ReturnType, has?: RouteHas[], inverted = false) { - return has - ? has.reduce((acc, cur) => { - if (acc === false) return false; - return inverted ? !matcher(cur) : matcher(cur); - }, true) - : true; -} - -const getParamsFromSource = (source: MatchFunction) => (value: string) => { - debug("value", value); - const _match = source(value); - return _match ? _match.params : {}; -}; - -const computeParamHas = - ( - headers: Record, - cookies: Record, - query: Record - ) => - (has: RouteHas): object => { - if (!has.value) return {}; - const matcher = new RegExp(`^${has.value}$`); - const fromSource = (value: string) => { - const matches = value.match(matcher); - return matches?.groups ?? {}; - }; - switch (has.type) { - case "header": - return fromSource(headers[has.key.toLowerCase()] ?? ""); - case "cookie": - return fromSource(cookies[has.key] ?? ""); - case "query": - return Array.isArray(query[has.key]) - ? fromSource((query[has.key] as string[]).join(",")) - : fromSource((query[has.key] as string) ?? ""); - case "host": - return fromSource(headers.host ?? ""); - } - }; - -function convertMatch(match: Match, toDestination: PathFunction, destination: string) { - if (!match) { - return destination; - } - - const { params } = match; - const isUsingParams = Object.keys(params).length > 0; - return isUsingParams ? toDestination(params) : destination; -} - -export function getNextConfigHeaders( - event: InternalEvent, - configHeaders?: Header[] | undefined -): Record { - if (!configHeaders) { - return {}; - } - - const matcher = routeHasMatcher(event.headers, event.cookies, event.query); - - const requestHeaders: Record = {}; - const localizedRawPath = localizePath(event); - - for (const { headers, has, missing, regex, source, locale } of configHeaders) { - const path = locale === false ? event.rawPath : localizedRawPath; - if (new RegExp(regex).test(path) && checkHas(matcher, has) && checkHas(matcher, missing, true)) { - const fromSource = match(source); - const _match = fromSource(path); - headers.forEach((h) => { - try { - const key = convertMatch(_match, compile(h.key), h.key); - const value = convertMatch(_match, compile(h.value), h.value); - requestHeaders[key] = value; - } catch { - debug(`Error matching header ${h.key} with value ${h.value}`); - requestHeaders[h.key] = h.value; - } - }); - } - } - return requestHeaders; -} - -/** - * TODO: This method currently only check for the first match. - * It should check for all matches for `beforeFiles` and `afterFiles` rewrite - * See https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites - */ -export function handleRewrites(event: InternalEvent, rewrites: T[]) { - const { rawPath, headers, query, cookies, url } = event; - const localizedRawPath = localizePath(event); - const matcher = routeHasMatcher(headers, cookies, query); - const computeHas = computeParamHas(headers, cookies, query); - const rewrite = rewrites.find((route) => { - const path = route.locale === false ? rawPath : localizedRawPath; - return ( - new RegExp(route.regex).test(path) && - checkHas(matcher, route.has) && - checkHas(matcher, route.missing, true) - ); - }); - let finalQuery = query; - - let rewrittenUrl = url; - const isExternalRewrite = isExternal(rewrite?.destination); - debug("isExternalRewrite", isExternalRewrite); - if (rewrite) { - const { pathname, protocol, hostname, queryString } = getUrlParts(rewrite.destination, isExternalRewrite); - // We need to use a localized path if the rewrite is not locale specific - const pathToUse = rewrite.locale === false ? rawPath : localizedRawPath; - - debug("urlParts", { pathname, protocol, hostname, queryString }); - const toDestinationPath = compile(escapeRegex(pathname, { isPath: true })); - const toDestinationHost = compile(escapeRegex(hostname)); - const toDestinationQuery = compile(escapeRegex(queryString)); - const params = { - // params for the source - ...getParamsFromSource(match(escapeRegex(rewrite.source, { isPath: true })))(pathToUse), - // params for the has - ...rewrite.has?.reduce((acc, cur) => { - return Object.assign(acc, computeHas(cur)); - }, {}), - // params for the missing - ...rewrite.missing?.reduce((acc, cur) => { - return Object.assign(acc, computeHas(cur)); - }, {}), - }; - const isUsingParams = Object.keys(params).length > 0; - let rewrittenQuery = queryString; - let rewrittenHost = hostname; - let rewrittenPath = pathname; - if (isUsingParams) { - rewrittenPath = unescapeRegex(toDestinationPath(params)); - rewrittenHost = unescapeRegex(toDestinationHost(params)); - rewrittenQuery = unescapeRegex(toDestinationQuery(params)); - } - - // We need to strip the locale from the path if it's a local api route - if (NextConfig.i18n && !isExternalRewrite) { - const strippedPathLocale = rewrittenPath.replace( - new RegExp(`^/(${NextConfig.i18n.locales.join("|")})`), - "" - ); - if (strippedPathLocale.startsWith("/api/")) { - rewrittenPath = strippedPathLocale; - } - } - - rewrittenUrl = isExternalRewrite - ? `${protocol}//${rewrittenHost}${rewrittenPath}` - : new URL(rewrittenPath, event.url).href; - - // We merge query params from the source and the destination - finalQuery = { - ...query, - ...convertFromQueryString(rewrittenQuery), - }; - rewrittenUrl += convertToQueryString(finalQuery); - debug("rewrittenUrl", { rewrittenUrl, finalQuery, isUsingParams }); - } - - return { - internalEvent: { - ...event, - query: finalQuery, - rawPath: new URL(rewrittenUrl).pathname, - url: rewrittenUrl, - }, - __rewrite: rewrite, - isExternalRewrite, - }; -} - -// Normalizes repeated slashes in the path e.g. hello//world -> hello/world -// or backslashes to forward slashes. This prevents requests such as //domain -// from invoking the middleware with `request.url === "domain"`. -// See: https://github.com/vercel/next.js/blob/3ecf087f10fdfba4426daa02b459387bc9c3c54f/packages/next/src/server/base-server.ts#L1016-L1020 -function handleRepeatedSlashRedirect(event: InternalEvent): false | InternalResult { - // Redirect `https://example.com//foo` to `https://example.com/foo`. - if (event.rawPath.match(/(\\|\/\/)/)) { - return { - type: event.type, - statusCode: 308, - headers: { - Location: normalizeRepeatedSlashes(new URL(event.url)), - }, - body: emptyReadableStream(), - isBase64Encoded: false, - }; - } - - return false; -} - -function handleTrailingSlashRedirect(event: InternalEvent): false | InternalResult { - // When rawPath is `//domain`, `url.host` would be `domain`. - // https://github.com/opennextjs/opennextjs-aws/issues/355 - const url = new URL(event.rawPath, "http://localhost"); - - if ( - // Someone is trying to redirect to a different origin, let's not do that - url.host !== "localhost" || - NextConfig.skipTrailingSlashRedirect || - // We should not apply trailing slash redirect to API routes - event.rawPath.startsWith("/api/") - ) { - return false; - } - - const emptyBody = emptyReadableStream(); - - if ( - NextConfig.trailingSlash && - !event.headers["x-nextjs-data"] && - !event.rawPath.endsWith("/") && - !event.rawPath.match(/[\w-]+\.[\w]+$/g) - ) { - const headersLocation = event.url.split("?"); - return { - type: event.type, - statusCode: 308, - headers: { - Location: `${headersLocation[0]}/${headersLocation[1] ? `?${headersLocation[1]}` : ""}`, - }, - body: emptyBody, - isBase64Encoded: false, - }; - } - if (!NextConfig.trailingSlash && event.rawPath.endsWith("/") && event.rawPath !== "/") { - const headersLocation = event.url.split("?"); - return { - type: event.type, - statusCode: 308, - headers: { - Location: `${headersLocation[0].replace(/\/$/, "")}${ - headersLocation[1] ? `?${headersLocation[1]}` : "" - }`, - }, - body: emptyBody, - isBase64Encoded: false, - }; - } - return false; -} - -export function handleRedirects( - event: InternalEvent, - redirects: RedirectDefinition[] -): InternalResult | undefined { - const repeatedSlashRedirect = handleRepeatedSlashRedirect(event); - if (repeatedSlashRedirect) return repeatedSlashRedirect; - - const trailingSlashRedirect = handleTrailingSlashRedirect(event); - if (trailingSlashRedirect) return trailingSlashRedirect; - - const localeRedirect = handleLocaleRedirect(event); - if (localeRedirect) return localeRedirect; - - const { internalEvent, __rewrite } = handleRewrites( - event, - redirects.filter((r) => !r.internal) - ); - if (__rewrite && !__rewrite.internal) { - return { - type: event.type, - statusCode: __rewrite.statusCode ?? 308, - headers: { - Location: internalEvent.url, - }, - body: emptyReadableStream(), - isBase64Encoded: false, - }; - } -} - -export function fixDataPage(internalEvent: InternalEvent, buildId: string): InternalEvent | InternalResult { - const { rawPath, query } = internalEvent; - const basePath = NextConfig.basePath ?? ""; - const dataPattern = `${basePath}/_next/data/${buildId}`; - // Return 404 for data requests that don't match the buildId - if (rawPath.startsWith("/_next/data") && !rawPath.startsWith(dataPattern)) { - return { - type: internalEvent.type, - statusCode: 404, - body: toReadableStream("{}"), - headers: { - "Content-Type": "application/json", - }, - isBase64Encoded: false, - }; - } - - if (rawPath.startsWith(dataPattern) && rawPath.endsWith(".json")) { - const newPath = `${basePath}${rawPath - .slice(dataPattern.length, -".json".length) - .replace(/^\/index$/, "/")}`; - query.__nextDataReq = "1"; - - return { - ...internalEvent, - rawPath: newPath, - query, - url: new URL(`${newPath}${convertToQueryString(query)}`, internalEvent.url).href, - }; - } - return internalEvent; -} - -export function handleFallbackFalse( - internalEvent: InternalEvent, - prerenderManifest?: PrerenderManifest -): { event: InternalEvent; isISR: boolean } { - const { rawPath } = internalEvent; - const { dynamicRoutes = {}, routes = {} } = prerenderManifest ?? {}; - const prerenderedFallbackRoutes = Object.entries(dynamicRoutes).filter( - ([, { fallback }]) => fallback === false - ); - const routeFallback = prerenderedFallbackRoutes.some(([, { routeRegex }]) => { - const routeRegexExp = new RegExp(routeRegex); - return routeRegexExp.test(rawPath); - }); - const locales = NextConfig.i18n?.locales; - const routesAlreadyHaveLocale = - locales?.includes(rawPath.split("/")[1]) || - // If we don't use locales, we don't need to add the default locale - locales === undefined; - let localizedPath = routesAlreadyHaveLocale ? rawPath : `/${NextConfig.i18n?.defaultLocale}${rawPath}`; - // We need to remove the trailing slash if it exists - if ( - // Not if localizedPath is "/" tho, because that would not make it find `isPregenerated` below since it would be try to match an empty string. - localizedPath !== "/" && - NextConfig.trailingSlash && - localizedPath.endsWith("/") - ) { - localizedPath = localizedPath.slice(0, -1); - } - const matchedStaticRoute = staticRouteMatcher(localizedPath); - const prerenderedFallbackRoutesName = prerenderedFallbackRoutes.map(([name]) => name); - const matchedDynamicRoute = dynamicRouteMatcher(localizedPath).filter( - ({ route }) => !prerenderedFallbackRoutesName.includes(route) - ); - - const isPregenerated = Object.keys(routes).includes(localizedPath); - if ( - routeFallback && - !isPregenerated && - matchedStaticRoute.length === 0 && - matchedDynamicRoute.length === 0 - ) { - return { - event: { - ...internalEvent, - rawPath: "/404", - url: constructNextUrl(internalEvent.url, "/404"), - headers: { - ...internalEvent.headers, - "x-invoke-status": "404", - }, - }, - isISR: false, - }; - } - - return { - event: internalEvent, - isISR: routeFallback || isPregenerated, - }; -} +export * from "@opennextjs/core/core/routing/matcher.js"; diff --git a/packages/open-next/src/core/routing/middleware.ts b/packages/open-next/src/core/routing/middleware.ts index 78638c95..9c056129 100644 --- a/packages/open-next/src/core/routing/middleware.ts +++ b/packages/open-next/src/core/routing/middleware.ts @@ -1,186 +1 @@ -import type { ReadableStream } from "node:stream/web"; - -import { - FunctionsConfigManifest, - MiddlewareManifest, - NextConfig, - PrerenderManifest, -} from "@/config/index.js"; -import type { InternalEvent, InternalResult, MiddlewareEvent } from "@/types/open-next.js"; -import { emptyReadableStream } from "@/utils/stream.js"; - -import { getQueryFromSearchParams } from "../../overrides/converters/utils.js"; - -import { localizePath } from "./i18n/index.js"; -import { - convertBodyToReadableStream, - getMiddlewareMatch, - isExternal, - normalizeLocationHeader, -} from "./util.js"; - -const middlewareManifest = MiddlewareManifest; -const functionsConfigManifest = FunctionsConfigManifest; - -const middleMatch = getMiddlewareMatch(middlewareManifest, functionsConfigManifest); - -const REDIRECTS = new Set([301, 302, 303, 307, 308]); - -type Middleware = (request: Request) => Response | Promise; -type MiddlewareLoader = () => Promise<{ default: Middleware }>; - -function defaultMiddlewareLoader() { - // @ts-expect-error - This is bundled - return import("./middleware.mjs"); -} - -/** - * - * @param internalEvent the internal event - * @param initialSearch the initial query string as it was received in the handler - * @param middlewareLoader Only used for unit test - * @returns `Promise` - */ -export async function handleMiddleware( - internalEvent: InternalEvent, - initialSearch: string, - middlewareLoader: MiddlewareLoader = defaultMiddlewareLoader -): Promise { - const headers = internalEvent.headers; - - // We bypass the middleware if the request is internal - // We should only do that if the request has the correct `x-prerender-revalidate` header - // The `x-prerender-revalidate` header is set at build time and should be safe to trust - if (headers["x-isr"] && headers["x-prerender-revalidate"] === PrerenderManifest?.preview?.previewModeId) - return internalEvent; - - // We only need the normalizedPath to check if the middleware should run - const normalizedPath = localizePath(internalEvent); - const hasMatch = middleMatch.some((r) => r.test(normalizedPath)); - if (!hasMatch) return internalEvent; - - const initialUrl = new URL(normalizedPath, internalEvent.url); - initialUrl.search = initialSearch; - const url = initialUrl.href; - - const middleware = await middlewareLoader(); - - const result: Response = await middleware.default({ - // `geo` is pre Next 15. - geo: { - // The city name is percent-encoded. - // See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37 - city: decodeURIComponent(headers["x-open-next-city"]), - country: headers["x-open-next-country"], - region: headers["x-open-next-region"], - latitude: headers["x-open-next-latitude"], - longitude: headers["x-open-next-longitude"], - }, - headers, - method: internalEvent.method || "GET", - nextConfig: { - basePath: NextConfig.basePath, - i18n: NextConfig.i18n, - trailingSlash: NextConfig.trailingSlash, - }, - url, - body: convertBodyToReadableStream(internalEvent.method, internalEvent.body), - } as unknown as Request); - const statusCode = result.status; - - /* Apply override headers from middleware - NextResponse.next({ - request: { - headers: new Headers(request.headers), - } - }) - Nextjs will set `x-middleware-override-headers` as a comma separated list of keys. - All the keys will be prefixed with `x-middleware-request-` - - We can delete `x-middleware-override-headers` and check if the key starts with - x-middleware-request- to set the req headers - */ - const responseHeaders = result.headers as Headers; - const reqHeaders: Record = {}; - const resHeaders: Record = {}; - - // These are internal headers used by Next.js, we don't want to expose them to the client - const filteredHeaders = [ - "x-middleware-override-headers", - "x-middleware-next", - "x-middleware-rewrite", - // We need to drop `content-encoding` because it will be decoded - "content-encoding", - ]; - - const xMiddlewareKey = "x-middleware-request-"; - responseHeaders.forEach((value, key) => { - if (key.startsWith(xMiddlewareKey)) { - const k = key.substring(xMiddlewareKey.length); - reqHeaders[k] = value; - } else { - if (filteredHeaders.includes(key.toLowerCase())) return; - if (key.toLowerCase() === "set-cookie") { - resHeaders[key] = resHeaders[key] ? [...resHeaders[key], value] : [value]; - } else if (REDIRECTS.has(statusCode) && key.toLowerCase() === "location") { - resHeaders[key] = normalizeLocationHeader(value, internalEvent.url); - } else { - resHeaders[key] = value; - } - } - }); - - // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite - // NOTE: the header was added to `req` from above - const rewriteUrl = responseHeaders.get("x-middleware-rewrite"); - let isExternalRewrite = false; - let middlewareQuery = internalEvent.query; - let newUrl = internalEvent.url; - if (rewriteUrl) { - newUrl = rewriteUrl; - // If not a string, it should probably throw - if (isExternal(newUrl, internalEvent.headers.host as string)) { - isExternalRewrite = true; - } else { - const rewriteUrlObject = new URL(rewriteUrl); - // Search params from the rewritten URL override the original search params - - middlewareQuery = getQueryFromSearchParams(rewriteUrlObject.searchParams); - - // We still need to add internal search params to the query string for pages router on older versions of Next.js - if ("__nextDataReq" in internalEvent.query) { - middlewareQuery.__nextDataReq = internalEvent.query.__nextDataReq; - } - } - } - - // If the middleware wants to directly return a response (i.e. not using `NextResponse.next()` or `NextResponse.rewrite()`) - // we return the response directly - if (!rewriteUrl && !responseHeaders.get("x-middleware-next")) { - // transfer response body to res - const body = (result.body as ReadableStream) ?? emptyReadableStream(); - - return { - type: internalEvent.type, - statusCode: statusCode, - headers: resHeaders, - body, - isBase64Encoded: false, - } satisfies InternalResult; - } - - return { - responseHeaders: resHeaders, - url: newUrl, - rawPath: new URL(newUrl).pathname, - type: internalEvent.type, - headers: { ...internalEvent.headers, ...reqHeaders }, - body: internalEvent.body, - method: internalEvent.method, - query: middlewareQuery, - cookies: internalEvent.cookies, - remoteAddress: internalEvent.remoteAddress, - isExternalRewrite, - rewriteStatusCode: rewriteUrl && !isExternalRewrite ? statusCode : undefined, - } satisfies MiddlewareEvent; -} +export * from "@opennextjs/core/core/routing/middleware.js"; diff --git a/packages/open-next/src/core/routing/queue.ts b/packages/open-next/src/core/routing/queue.ts index 193760f0..3c3511cb 100644 --- a/packages/open-next/src/core/routing/queue.ts +++ b/packages/open-next/src/core/routing/queue.ts @@ -1,49 +1 @@ -export function generateShardId(rawPath: string, maxConcurrency: number, prefix: string) { - let a = cyrb128(rawPath); - // We use mulberry32 to generate a random int between 0 and MAX_REVALIDATE_CONCURRENCY - let t = (a += 0x6d2b79f5); - t = Math.imul(t ^ (t >>> 15), t | 1); - t ^= t + Math.imul(t ^ (t >>> 7), t | 61); - const randomFloat = ((t ^ (t >>> 14)) >>> 0) / 4294967296; - // This will generate a random int between 0 and maxConcurrency - const randomInt = Math.floor(randomFloat * maxConcurrency); - return `${prefix}-${randomInt}`; -} - -// Since we're using a FIFO queue, every messageGroupId is treated sequentially -// This could cause a backlog of messages in the queue if there is too much page to -// revalidate at once. To avoid this, we generate a random messageGroupId for each -// revalidation request. -// We can't just use a random string because we need to ensure that the same rawPath -// will always have the same messageGroupId. -// https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript#answer-47593316 -export function generateMessageGroupId(rawPath: string) { - // This will generate a random int between 0 and MAX_REVALIDATE_CONCURRENCY - // This means that we could have 1000 revalidate request at the same time - const maxConcurrency = Number.parseInt(process.env.MAX_REVALIDATE_CONCURRENCY ?? "10"); - return generateShardId(rawPath, maxConcurrency, "revalidate"); -} - -// Used to generate a hash int from a string -function cyrb128(str: string) { - let h1 = 1779033703; - let h2 = 3144134277; - let h3 = 1013904242; - let h4 = 2773480762; - for (let i = 0, k: number; i < str.length; i++) { - k = str.charCodeAt(i); - h1 = h2 ^ Math.imul(h1 ^ k, 597399067); - h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); - h3 = h4 ^ Math.imul(h3 ^ k, 951274213); - h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); - } - h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); - h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); - h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); - h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); - h1 ^= h2 ^ h3 ^ h4; - h2 ^= h1; - h3 ^= h1; - h4 ^= h1; - return h1 >>> 0; -} +export * from "@opennextjs/core/core/routing/queue.js"; diff --git a/packages/open-next/src/core/routing/routeMatcher.ts b/packages/open-next/src/core/routing/routeMatcher.ts index 9a4d814e..ac30f4b9 100644 --- a/packages/open-next/src/core/routing/routeMatcher.ts +++ b/packages/open-next/src/core/routing/routeMatcher.ts @@ -1,84 +1 @@ -import { AppPathRoutesManifest, PagesManifest, PrerenderManifest, RoutesManifest } from "@/config/index"; -import type { RouteDefinition } from "@/types/next-types"; -import type { ResolvedRoute, RouteType } from "@/types/open-next"; - -// Add the locale prefix to the regex so we correctly match the rawPath -const optionalLocalePrefixRegex = `^/(?:${RoutesManifest.locales.map((locale) => `${locale}/?`).join("|")})?`; - -// Add the basepath prefix to the regex so we correctly match the rawPath -const optionalBasepathPrefixRegex = RoutesManifest.basePath ? `^${RoutesManifest.basePath}/?` : "^/"; - -const optionalPrefix = optionalLocalePrefixRegex.replace("^/", optionalBasepathPrefixRegex); - -function routeMatcher(routeDefinitions: RouteDefinition[]) { - const regexp = routeDefinitions.map((route) => ({ - page: route.page, - regexp: new RegExp(route.regex.replace("^/", optionalPrefix)), - })); - - const { dynamicRoutes = {} } = PrerenderManifest ?? {}; - const prerenderedFallbackRoutes = Object.entries(dynamicRoutes) - .filter(([, { fallback }]) => fallback === false) - .map(([route]) => route); - - const appPathsSet = new Set(); - const routePathsSet = new Set(); - // We need to use AppPathRoutesManifest here - for (const [k, v] of Object.entries(AppPathRoutesManifest)) { - if (k.endsWith("page")) { - appPathsSet.add(v); - } else if (k.endsWith("route")) { - routePathsSet.add(v); - } - } - - return function matchRoute(path: string): ResolvedRoute[] { - const foundRoutes = regexp.filter((route) => route.regexp.test(path)); - - return foundRoutes.map((foundRoute) => { - let routeType: RouteType = "page"; - // Check if the route is a prerendered fallback false route - const isFallback = prerenderedFallbackRoutes.includes(foundRoute.page); - - if (appPathsSet.has(foundRoute.page)) { - routeType = "app"; - } else if (routePathsSet.has(foundRoute.page)) { - routeType = "route"; - } - return { - route: foundRoute.page, - type: routeType, - isFallback, - }; - }); - }; -} - -export const staticRouteMatcher = routeMatcher([...RoutesManifest.routes.static, ...getStaticAPIRoutes()]); -export const dynamicRouteMatcher = routeMatcher(RoutesManifest.routes.dynamic); - -/** - * Returns static API routes for both app and pages router cause Next will filter them out in staticRoutes in `routes-manifest.json`. - * We also need to filter out page files that are under `app/api/*` as those would not be present in the routes manifest either. - * This line from Next.js skips it: - * https://github.com/vercel/next.js/blob/ded56f952154a40dcfe53bdb38c73174e9eca9e5/packages/next/src/build/index.ts#L1299 - * - * Without it handleFallbackFalse will 404 on static API routes if there is a catch-all route on root level. - */ -function getStaticAPIRoutes(): RouteDefinition[] { - const createRouteDefinition = (route: string) => ({ - page: route, - regex: `^${route}(?:/)?$`, - }); - const dynamicRoutePages = new Set(RoutesManifest.routes.dynamic.map(({ page }) => page)); - const pagesStaticAPIRoutes = Object.keys(PagesManifest) - .filter((route) => route.startsWith("/api/") && !dynamicRoutePages.has(route)) - .map(createRouteDefinition); - - // We filter out both static API and page routes from the app paths manifest - const appPathsStaticAPIRoutes = Object.values(AppPathRoutesManifest) - .filter((route) => (route.startsWith("/api/") || route === "/api") && !dynamicRoutePages.has(route)) - .map(createRouteDefinition); - - return [...pagesStaticAPIRoutes, ...appPathsStaticAPIRoutes]; -} +export * from "@opennextjs/core/core/routing/routeMatcher.js"; diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index 0d4ead50..9574ddea 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -1,448 +1 @@ -import crypto from "node:crypto"; -import type { OutgoingHttpHeaders } from "node:http"; -import { parse as parseQs, stringify as stringifyQs } from "node:querystring"; -import { ReadableStream } from "node:stream/web"; - -import { BuildId, HtmlPages, NextConfig } from "@/config/index.js"; -import type { IncomingMessage } from "@/http/index.js"; -import { OpenNextNodeResponse } from "@/http/openNextResponse.js"; -import { getQueryFromIterator, parseHeaders } from "@/http/util.js"; -import type { FunctionsConfigManifest, MiddlewareManifest } from "@/types/next-types"; -import type { InternalEvent, InternalResult, RoutingResult, StreamCreator } from "@/types/open-next.js"; - -import { debug, error } from "../../adapters/logger.js"; -import { isBinaryContentType } from "../../utils/binary.js"; - -import { localizePath } from "./i18n/index.js"; -import { generateMessageGroupId } from "./queue.js"; - -/** - * - * @__PURE__ - */ -export function isExternal(url?: string, host?: string) { - if (!url) return false; - const pattern = /^https?:\/\//; - if (!pattern.test(url)) return false; - - if (host) { - try { - const parsedUrl = new URL(url); - return parsedUrl.host !== host; - } catch { - // If URL parsing fails, fall back to substring check - return !url.includes(host); - } - } - return true; -} - -export function convertFromQueryString(query: string) { - if (query === "") return {}; - const queryParts = query.split("&"); - return getQueryFromIterator( - queryParts.map((p) => { - const [key, value] = p.split("="); - return [key, value] as const; - }) - ); -} - -/** - * - * @__PURE__ - */ -export function getUrlParts(url: string, isExternal: boolean) { - if (!isExternal) { - const regex = /\/([^?]*)\??(.*)/; - const match = url.match(regex); - return { - hostname: "", - pathname: match?.[1] ? `/${match[1]}` : url, - protocol: "", - queryString: match?.[2] ?? "", - }; - } - - const regex = /^(https?:)\/\/?([^/\s]+)(\/[^?]*)?(\?.*)?/; - const match = url.match(regex); - if (!match) { - throw new Error(`Invalid external URL: ${url}`); - } - return { - protocol: match[1] ?? "https:", - hostname: match[2], - pathname: match[3] ?? "", - queryString: match[4]?.slice(1) ?? "", - }; -} - -/** - * Creates an URL to a Next page - * - * @param baseUrl Used to get the origin - * @param path The pathname - * @returns The Next URL considering the basePath - * - * @__PURE__ - */ -export function constructNextUrl(baseUrl: string, path: string) { - // basePath is generated as "" if not provided on Next.js 15 (not sure about older versions) - const nextBasePath = NextConfig.basePath ?? ""; - const url = new URL(`${nextBasePath}${path}`, baseUrl); - return url.href; -} - -/** - * - * @__PURE__ - */ -export function convertRes(res: OpenNextNodeResponse): InternalResult { - // Format Next.js response to Lambda response - const statusCode = res.statusCode || 200; - // When using HEAD requests, it seems that flushHeaders is not called, not sure why - // Probably some kind of race condition - const headers = parseHeaders(res.getFixedHeaders()); - const isBase64Encoded = isBinaryContentType(headers["content-type"]) || !!headers["content-encoding"]; - const body = new ReadableStream({ - pull(controller) { - if (!res._chunks || res._chunks.length === 0) { - controller.close(); - return; - } - - controller.enqueue(res._chunks.shift()); - }, - }); - return { - type: "core", - statusCode, - headers, - body, - isBase64Encoded, - }; -} - -/** - * Make sure that multi-value query parameters are transformed to - * ?key=value1&key=value2&... so that Next converts those parameters - * to an array when reading the query parameters - * query should be properly encoded before using this function - * @__PURE__ - */ -export function convertToQueryString(query: Record) { - const queryStrings: string[] = []; - Object.entries(query).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((entry) => queryStrings.push(`${key}=${entry}`)); - } else { - queryStrings.push(`${key}=${value}`); - } - }); - - return queryStrings.length > 0 ? `?${queryStrings.join("&")}` : ""; -} - -/** - * Given a raw query string, returns a record with key value-array pairs - * similar to how multiValueQueryStringParameters are structured - * @__PURE__ - */ -export function convertToQuery(querystring: string) { - if (!querystring) return {}; - const query = new URLSearchParams(querystring); - const queryObject: Record = {}; - - for (const key of query.keys()) { - const queries = query.getAll(key); - queryObject[key] = queries.length > 1 ? queries : queries[0]; - } - - return queryObject; -} - -/** - * - * @__PURE__ - */ -export function getMiddlewareMatch( - middlewareManifest: MiddlewareManifest, - functionsManifest?: FunctionsConfigManifest -) { - if (functionsManifest?.functions?.["/_middleware"]) { - return ( - functionsManifest.functions["/_middleware"].matchers?.map(({ regexp }) => new RegExp(regexp)) ?? [/.*/] - ); - } - const rootMiddleware = middlewareManifest.middleware["/"]; - if (!rootMiddleware?.matchers) return []; - return rootMiddleware.matchers.map(({ regexp }) => new RegExp(regexp)); -} - -/** - * - * @__PURE__ - */ -export function escapeRegex(str: string, { isPath }: { isPath?: boolean } = {}) { - const result = str.replaceAll("(.)", "_µ1_").replaceAll("(..)", "_µ2_").replaceAll("(...)", "_µ3_"); - return isPath ? result : result.replaceAll("+", "_µ4_"); -} - -/** - * - * @__PURE__ - */ -export function unescapeRegex(str: string) { - return str - .replaceAll("_µ1_", "(.)") - .replaceAll("_µ2_", "(..)") - .replaceAll("_µ3_", "(...)") - .replaceAll("_µ4_", "+"); -} - -/** - * @__PURE__ - */ -export function convertBodyToReadableStream(method: string, body?: string | Buffer) { - if (method === "GET" || method === "HEAD") return undefined; - if (!body) return undefined; - return new ReadableStream({ - start(controller) { - controller.enqueue(body); - controller.close(); - }, - }); -} - -enum CommonHeaders { - CACHE_CONTROL = "cache-control", - NEXT_CACHE = "x-nextjs-cache", -} - -/** - * - * @__PURE__ - */ -export function fixCacheHeaderForHtmlPages(internalEvent: InternalEvent, headers: OutgoingHttpHeaders) { - // We don't want to cache error pages - if (internalEvent.rawPath === "/404" || internalEvent.rawPath === "/500") { - if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") { - return; - } - headers[CommonHeaders.CACHE_CONTROL] = "private, no-cache, no-store, max-age=0, must-revalidate"; - return; - } - const localizedPath = localizePath(internalEvent); - // WORKAROUND: `NextServer` does not set cache headers for HTML pages - // https://opennext.js.org/aws/v2/advanced/workaround#workaround-nextserver-does-not-set-cache-headers-for-html-pages - // Requests containing an `x-middleware-prefetch` header must not be cached - if (HtmlPages.includes(localizedPath) && !internalEvent.headers["x-middleware-prefetch"]) { - headers[CommonHeaders.CACHE_CONTROL] = "public, max-age=0, s-maxage=31536000, must-revalidate"; - } -} - -/** - * - * @__PURE__ - */ -export function fixSWRCacheHeader(headers: OutgoingHttpHeaders) { - // WORKAROUND: `NextServer` does not set correct SWR cache headers — https://github.com/sst/open-next#workaround-nextserver-does-not-set-correct-swr-cache-headers - let cacheControl = headers[CommonHeaders.CACHE_CONTROL]; - if (!cacheControl) return; - if (Array.isArray(cacheControl)) { - cacheControl = cacheControl.join(","); - } - if (typeof cacheControl !== "string") return; - headers[CommonHeaders.CACHE_CONTROL] = cacheControl.replace( - /\bstale-while-revalidate(?!=)/, - "stale-while-revalidate=2592000" // 30 days - ); -} - -/** - * - * @__PURE__ - */ -export function addOpenNextHeader(headers: OutgoingHttpHeaders) { - if (NextConfig.poweredByHeader) { - headers["X-OpenNext"] = "1"; - } - if (globalThis.openNextDebug) { - headers["X-OpenNext-Version"] = globalThis.openNextVersion; - } - if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) { - headers["X-OpenNext-RequestId"] = globalThis.__openNextAls.getStore()?.requestId; - } -} - -/** - * - * @__PURE__ - */ -export async function revalidateIfRequired( - host: string, - rawPath: string, - headers: OutgoingHttpHeaders, - req?: IncomingMessage -) { - if (headers[CommonHeaders.NEXT_CACHE] === "STALE") { - // If the URL is rewritten, revalidation needs to be done on the rewritten URL. - // - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation - // - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11 - // @ts-ignore - const internalMeta = req?.[Symbol.for("NextInternalRequestMeta")]; - - // When using Pages Router, two requests will be received: - // 1. one for the page: /foo - // 2. one for the json data: /_next/data/BUILD_ID/foo.json - // The rewritten url is correct for 1, but that for the second request - // does not include the "/_next/data/" prefix. Need to add it. - const revalidateUrl = internalMeta?._nextDidRewrite - ? rawPath.startsWith("/_next/data/") - ? `/_next/data/${BuildId}${internalMeta?._nextRewroteUrl}.json` - : internalMeta?._nextRewroteUrl - : rawPath; - - // We need to pass etag to the revalidation queue to try to bypass the default 5 min deduplication window. - // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html - // If you need to have a revalidation happen more frequently than 5 minutes, - // your page will need to have a different etag to bypass the deduplication window. - // If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated. - try { - const hash = (str: string) => crypto.createHash("md5").update(str).digest("hex"); - - const lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0; - - // For some weird cases, lastModified is not set, haven't been able to figure out yet why - // For those cases we add the etag to the deduplication id, it might help - const eTag = `${headers.etag ?? headers.ETag ?? ""}`; - - await globalThis.queue.send({ - MessageBody: { host, url: revalidateUrl, eTag, lastModified }, - MessageDeduplicationId: hash(`${rawPath}-${lastModified}-${eTag}`), - MessageGroupId: generateMessageGroupId(rawPath), - }); - } catch (e) { - error(`Failed to revalidate stale page ${rawPath}`, e); - } - } -} - -/** - * - * @__PURE__ - */ -export function fixISRHeaders(headers: OutgoingHttpHeaders) { - const sMaxAgeRegex = /s-maxage=(\d+)/; - const match = headers[CommonHeaders.CACHE_CONTROL]?.match(sMaxAgeRegex); - const sMaxAge = match ? Number.parseInt(match[1]) : undefined; - // We only apply the fix if the cache-control header contains s-maxage - if (!sMaxAge) { - return; - } - if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") { - headers[CommonHeaders.CACHE_CONTROL] = "private, no-cache, no-store, max-age=0, must-revalidate"; - return; - } - const _lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0; - if (headers[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) { - debug("cache-control", headers[CommonHeaders.CACHE_CONTROL], _lastModified, Date.now()); - - // 31536000 is the default s-maxage value for SSG pages - if (sMaxAge && sMaxAge !== 31536000) { - // calculate age - const age = Math.round((Date.now() - _lastModified) / 1000); - const remainingTtl = Math.max(sMaxAge - age, 1); - headers[CommonHeaders.CACHE_CONTROL] = `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`; - } - } - if (headers[CommonHeaders.NEXT_CACHE] !== "STALE") return; - - // If the cache is stale, we revalidate in the background - // In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds - // This will cause CloudFront to cache the stale data for a short period of time while we revalidate in the background - // Once the revalidation is complete, CloudFront will serve the fresh data - headers[CommonHeaders.CACHE_CONTROL] = "s-maxage=2, stale-while-revalidate=2592000"; -} - -/** - * - * @param internalEvent - * @param headers - * @param responseStream - * @returns - * @__PURE__ - */ -export function createServerResponse( - routingResult: RoutingResult, - headers: Record, - responseStream?: StreamCreator -) { - const internalEvent = routingResult.internalEvent; - return new OpenNextNodeResponse( - (_headers) => { - fixCacheHeaderForHtmlPages(internalEvent, _headers); - fixSWRCacheHeader(_headers); - addOpenNextHeader(_headers); - fixISRHeaders(_headers); - }, - async (_headers) => { - await revalidateIfRequired(internalEvent.headers.host, internalEvent.rawPath, _headers); - await invalidateCDNOnRequest(routingResult, _headers); - }, - responseStream, - headers, - routingResult.rewriteStatusCode - ); -} - -// This function is used only for `res.revalidate()` -export async function invalidateCDNOnRequest(params: RoutingResult, headers: OutgoingHttpHeaders) { - const { internalEvent, resolvedRoutes, initialURL } = params; - const initialPath = new URL(initialURL).pathname; - const isIsrRevalidation = internalEvent.headers["x-isr"] === "1"; - if (!isIsrRevalidation && headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") { - await globalThis.cdnInvalidationHandler.invalidatePaths([ - { - initialPath, - rawPath: internalEvent.rawPath, - resolvedRoutes, - }, - ]); - } -} - -/** - * Normalizes the Location header to either be a relative path or a full URL. - * If the Location header is relative to the origin, it will return a relative path. - * If it is an absolute URL, it will return the full URL. - * Redirects from Next config query parameters are encoded using `stringifyQs` - * Redirects from the middleware the query parameters are not encoded. - * - * @param location The Location header value - * @param baseUrl The base URL to use for relative paths (i.e the original request URL) - * @param encodeQuery Optional flag to indicate if query parameters should be encoded in the Location header - * @returns An absolute or relative Location header value - */ -export function normalizeLocationHeader(location: string, baseUrl: string, encodeQuery = false): string { - if (!URL.canParse(location)) { - // If the location is not a valid URL, return it as-is - return location; - } - - const locationURL = new URL(location); - const origin = new URL(baseUrl).origin; - - let search = locationURL.search; - // If encodeQuery is true, we need to encode the query parameters - // We could have used URLSearchParams, but that doesn't match what Next does. - if (encodeQuery && search) { - search = `?${stringifyQs(parseQs(search.slice(1)))}`; - } - const href = `${locationURL.origin}${locationURL.pathname}${search}${locationURL.hash}`; - // The URL is relative if the origin is the same as the base URL's origin - if (locationURL.origin === origin) { - return href.slice(origin.length); - } - return href; -} +export * from "@opennextjs/core/core/routing/util.js"; diff --git a/packages/open-next/src/core/routingHandler.ts b/packages/open-next/src/core/routingHandler.ts index 232f1d67..14c9a5c9 100644 --- a/packages/open-next/src/core/routingHandler.ts +++ b/packages/open-next/src/core/routingHandler.ts @@ -1,298 +1,2 @@ -import { BuildId, ConfigHeaders, NextConfig, PrerenderManifest, RoutesManifest } from "@/config/index"; -import type { - InternalEvent, - InternalResult, - PartialResult, - ResolvedRoute, - RoutingResult, -} from "@/types/open-next"; -import type { AssetResolver } from "@/types/overrides"; - -import { debug, error } from "../adapters/logger"; - -import { cacheInterceptor } from "./routing/cacheInterceptor"; -import { detectLocale } from "./routing/i18n"; -import { - fixDataPage, - getNextConfigHeaders, - handleFallbackFalse, - handleRedirects, - handleRewrites, -} from "./routing/matcher"; -import { handleMiddleware } from "./routing/middleware"; -import { dynamicRouteMatcher, staticRouteMatcher } from "./routing/routeMatcher"; -import { constructNextUrl, normalizeLocationHeader } from "./routing/util"; - -export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-"; -export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length; -export const INTERNAL_HEADER_PREFIX = "x-opennext-"; -export const INTERNAL_HEADER_INITIAL_URL = `${INTERNAL_HEADER_PREFIX}initial-url`; -export const INTERNAL_HEADER_LOCALE = `${INTERNAL_HEADER_PREFIX}locale`; -export const INTERNAL_HEADER_RESOLVED_ROUTES = `${INTERNAL_HEADER_PREFIX}resolved-routes`; -export const INTERNAL_HEADER_REWRITE_STATUS_CODE = `${INTERNAL_HEADER_PREFIX}rewrite-status-code`; -export const INTERNAL_EVENT_REQUEST_ID = `${INTERNAL_HEADER_PREFIX}request-id`; - -// Geolocation headers starting from Nextjs 15 -// See https://github.com/vercel/vercel/blob/7714b1c/packages/functions/src/headers.ts -const geoHeaderToNextHeader = { - "x-open-next-city": "x-vercel-ip-city", - "x-open-next-country": "x-vercel-ip-country", - "x-open-next-region": "x-vercel-ip-country-region", - "x-open-next-latitude": "x-vercel-ip-latitude", - "x-open-next-longitude": "x-vercel-ip-longitude", -}; - -/** - * Adds the middleware headers to an event or result. - * - * @param eventOrResult - * @param middlewareHeaders - */ -function applyMiddlewareHeaders( - eventOrResult: InternalEvent | InternalResult, - middlewareHeaders: Record -) { - // Use the `MIDDLEWARE_HEADER_PREFIX` prefix for events, they will be processed by the request handler later. - // Results do not go through the request handler and should not be prefixed. - const isResult = isInternalResult(eventOrResult); - const headers = eventOrResult.headers; - const keyPrefix = isResult ? "" : MIDDLEWARE_HEADER_PREFIX; - Object.entries(middlewareHeaders).forEach(([key, value]) => { - if (value) { - headers[keyPrefix + key] = Array.isArray(value) ? value.join(",") : value; - } - }); -} - -export default async function routingHandler( - event: InternalEvent, - { assetResolver }: { assetResolver?: AssetResolver } -): Promise { - try { - // Add Next geo headers - for (const [openNextGeoName, nextGeoName] of Object.entries(geoHeaderToNextHeader)) { - const value = event.headers[openNextGeoName]; - if (value) { - event.headers[nextGeoName] = value; - } - } - - // First we remove internal headers - // We don't want to allow users to set these headers - for (const key of Object.keys(event.headers)) { - if (key.startsWith(INTERNAL_HEADER_PREFIX) || key.startsWith(MIDDLEWARE_HEADER_PREFIX)) { - delete event.headers[key]; - } - } - - // Headers from the Next config and middleware (the later are applied further down). - let headers: Record = getNextConfigHeaders(event, ConfigHeaders); - - let eventOrResult = fixDataPage(event, BuildId); - - if (isInternalResult(eventOrResult)) { - return eventOrResult; - } - - const redirect = handleRedirects(eventOrResult, RoutesManifest.redirects); - if (redirect) { - // We need to encode the value in the Location header to make sure it is valid according to RFC - redirect.headers.Location = normalizeLocationHeader( - redirect.headers.Location as string, - event.url, - true - ); - debug("redirect", redirect); - return redirect; - } - const middlewareEventOrResult = await handleMiddleware( - eventOrResult, - // We need to pass the initial search without any decoding - // TODO: we'd need to refactor InternalEvent to include the initial querystring directly - // Should be done in another PR because it is a breaking change - new URL(event.url).search - ); - if (isInternalResult(middlewareEventOrResult)) { - return middlewareEventOrResult; - } - - const middlewareHeadersPrioritized = - globalThis.openNextConfig.dangerous?.middlewareHeadersOverrideNextConfigHeaders ?? false; - - if (middlewareHeadersPrioritized) { - headers = { - ...headers, - ...middlewareEventOrResult.responseHeaders, - }; - } else { - headers = { - ...middlewareEventOrResult.responseHeaders, - ...headers, - }; - } - - let isExternalRewrite = middlewareEventOrResult.isExternalRewrite ?? false; - eventOrResult = middlewareEventOrResult; - - if (!isExternalRewrite) { - // First rewrite to be applied - const beforeRewrite = handleRewrites(eventOrResult, RoutesManifest.rewrites.beforeFiles); - eventOrResult = beforeRewrite.internalEvent; - isExternalRewrite = beforeRewrite.isExternalRewrite; - // Check for matching public files after `beforeFiles` rewrites - // See: - // - https://nextjs.org/docs/app/api-reference/file-conventions/middleware#execution-order - // - https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites - if (!isExternalRewrite) { - const assetResult = await assetResolver?.maybeGetAssetResult?.(eventOrResult); - if (assetResult) { - applyMiddlewareHeaders(assetResult, headers); - return assetResult; - } - } - } - let foundStaticRoute = staticRouteMatcher(eventOrResult.rawPath); - const isStaticRoute = !isExternalRewrite && foundStaticRoute.length > 0; - - if (!(isStaticRoute || isExternalRewrite)) { - // Second rewrite to be applied - const afterRewrite = handleRewrites(eventOrResult, RoutesManifest.rewrites.afterFiles); - eventOrResult = afterRewrite.internalEvent; - isExternalRewrite = afterRewrite.isExternalRewrite; - } - - let isISR = false; - // We want to run this just before the dynamic route check - // We can skip it if its an external rewrite - if (!isExternalRewrite) { - const fallbackResult = handleFallbackFalse(eventOrResult, PrerenderManifest); - eventOrResult = fallbackResult.event; - isISR = fallbackResult.isISR; - } - - let foundDynamicRoute = dynamicRouteMatcher(eventOrResult.rawPath); - const isDynamicRoute = !isExternalRewrite && foundDynamicRoute.length > 0; - - if (!(isDynamicRoute || isStaticRoute || isExternalRewrite)) { - // Fallback rewrite to be applied - const fallbackRewrites = handleRewrites(eventOrResult, RoutesManifest.rewrites.fallback); - eventOrResult = fallbackRewrites.internalEvent; - isExternalRewrite = fallbackRewrites.isExternalRewrite; - } - - const isNextImageRoute = eventOrResult.rawPath.startsWith("/_next/image"); - - const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite; - - // We need to ensure that rewrites are applied before showing the 404 page - foundStaticRoute = staticRouteMatcher(eventOrResult.rawPath); - // We also want to remove dynamic routes that are fallback false - foundDynamicRoute = dynamicRouteMatcher(eventOrResult.rawPath).filter((route) => !route.isFallback); - - // If we still haven't found a route, we show the 404 page - if ( - !( - isRouteFoundBeforeAllRewrites || - isNextImageRoute || - // We need to check again once all rewrites have been applied - foundStaticRoute.length > 0 || - foundDynamicRoute.length > 0 - ) - ) { - eventOrResult = { - ...eventOrResult, - rawPath: "/404", - url: constructNextUrl(eventOrResult.url, "/404"), - headers: { - ...eventOrResult.headers, - "x-middleware-response-cache-control": "private, no-cache, no-store, max-age=0, must-revalidate", - }, - }; - } - - const resolvedRoutes: ResolvedRoute[] = [...foundStaticRoute, ...foundDynamicRoute]; - - if (!isInternalResult(eventOrResult)) { - debug("Attempting cache interception"); - const cacheInterceptionResult = await cacheInterceptor(eventOrResult); - if (isInternalResult(cacheInterceptionResult)) { - applyMiddlewareHeaders(cacheInterceptionResult, headers); - return cacheInterceptionResult; - } else if (isPartialResult(cacheInterceptionResult)) { - // We need to apply the headers to both the result (the streamed response) and the resume request - applyMiddlewareHeaders(cacheInterceptionResult.result, headers); - applyMiddlewareHeaders(cacheInterceptionResult.resumeRequest, headers); - return { - internalEvent: cacheInterceptionResult.resumeRequest, - isExternalRewrite: false, - origin: false, - isISR: false, - resolvedRoutes, - initialURL: event.url, - locale: NextConfig.i18n ? detectLocale(eventOrResult, NextConfig.i18n) : undefined, - rewriteStatusCode: middlewareEventOrResult.rewriteStatusCode, - initialResponse: cacheInterceptionResult.result, - }; - } - } - - // We apply the headers from the middleware response last - applyMiddlewareHeaders(eventOrResult, headers); - - debug("resolvedRoutes", resolvedRoutes); - - return { - internalEvent: eventOrResult, - isExternalRewrite, - origin: false, - isISR, - resolvedRoutes, - initialURL: event.url, - locale: NextConfig.i18n ? detectLocale(eventOrResult, NextConfig.i18n) : undefined, - rewriteStatusCode: middlewareEventOrResult.rewriteStatusCode, - }; - } catch (e) { - error("Error in routingHandler", e); - // In case of an error, we want to return the 500 page from Next.js - return { - internalEvent: { - type: "core", - method: "GET", - rawPath: "/500", - url: constructNextUrl(event.url, "/500"), - headers: { - ...event.headers, - }, - query: event.query, - cookies: event.cookies, - remoteAddress: event.remoteAddress, - }, - isExternalRewrite: false, - origin: false, - isISR: false, - resolvedRoutes: [], - initialURL: event.url, - locale: NextConfig.i18n ? detectLocale(event, NextConfig.i18n) : undefined, - }; - } -} - -/** - * @param eventOrResult - * @returns Whether the event is an instance of `InternalResult` - */ -export function isInternalResult( - eventOrResult: InternalEvent | InternalResult | PartialResult -): eventOrResult is InternalResult { - return eventOrResult != null && "statusCode" in eventOrResult; -} - -/** - * @param eventOrResult - * @returns Whether the event is an instance of `PartialResult` (i.e. for PPR responses) - */ -export function isPartialResult( - eventOrResult: InternalEvent | InternalResult | PartialResult -): eventOrResult is PartialResult { - return eventOrResult != null && "resumeRequest" in eventOrResult; -} +export { default } from "@opennextjs/core/core/routingHandler.js"; +export * from "@opennextjs/core/core/routingHandler.js"; diff --git a/packages/open-next/src/debug.ts b/packages/open-next/src/debug.ts index be81b9bd..67457df7 100644 --- a/packages/open-next/src/debug.ts +++ b/packages/open-next/src/debug.ts @@ -1,15 +1 @@ -import fs from "node:fs"; -import path from "node:path"; - -import type { BuildOptions } from "./build/helper"; - -let init = false; - -export function addDebugFile(options: BuildOptions, name: string, content: unknown) { - if (!init) { - fs.mkdirSync(path.join(options.outputDir, ".debug"), { recursive: true }); - init = true; - } - const strContent = typeof content === "string" ? content : JSON.stringify(content, null, 2); - fs.writeFileSync(path.join(options.outputDir, ".debug", name), strContent); -} +export * from "@opennextjs/core/debug.js"; diff --git a/packages/open-next/src/helpers/withCloudflare.ts b/packages/open-next/src/helpers/withCloudflare.ts index c8e883e6..2d4be8e4 100644 --- a/packages/open-next/src/helpers/withCloudflare.ts +++ b/packages/open-next/src/helpers/withCloudflare.ts @@ -1,102 +1 @@ -import type { - FunctionOptions, - OpenNextConfig, - RouteTemplate, - SplittedFunctionOptions, -} from "@/types/open-next"; - -type CloudflareCompatibleFunction = Placement extends "regional" - ? FunctionOptions & { - placement: "regional"; - } - : { placement: "global" }; - -type CloudflareCompatibleRoutes = Placement extends "regional" - ? { - placement: "regional"; - routes: RouteTemplate[]; - patterns: string[]; - } - : { - placement: "global"; - routes: `app/${string}/route`; - patterns: string; - }; - -type CloudflareCompatibleSplittedFunction = - CloudflareCompatibleRoutes & CloudflareCompatibleFunction; - -type CloudflareConfig< - Fn extends Record>, -> = { - default: CloudflareCompatibleFunction<"regional">; - functions?: Fn; -} & Omit; - -type InterpolatedSplittedFunctionOptions< - Fn extends Record>, -> = { - [K in keyof Fn]: SplittedFunctionOptions; -}; - -/** - * This function makes it easier to use Cloudflare with OpenNext. - * All options are already restricted to Cloudflare compatible options. - * @example - * ```ts - export default withCloudflare({ - default: { - placement: "regional", - runtime: "node", - }, - functions: { - api: { - placement: "regional", - runtime: "node", - routes: ["app/api/test/route", "page/api/otherApi"], - patterns: ["/api/*"], - }, - global: { - placement: "global", - runtime: "edge", - routes: "app/test/page", - patterns: "/page", - }, - }, -}); - * ``` - */ -export function withCloudflare< - Fn extends Record>, - Key extends keyof Fn, ->(config: CloudflareConfig) { - const functions = Object.entries(config.functions ?? {}).reduce((acc, [name, fn]) => { - const _name = name as Key; - acc[_name] = - fn.placement === "global" - ? { - placement: "global", - runtime: "edge", - routes: [fn.routes], - patterns: [fn.patterns], - override: { - wrapper: "cloudflare-edge", - converter: "edge", - }, - } - : { ...fn, placement: "regional" }; - return acc; - }, {} as InterpolatedSplittedFunctionOptions); - return { - default: config.default, - functions: functions, - middleware: { - external: true, - originResolver: "pattern-env", - override: { - wrapper: "cloudflare-edge", - converter: "edge", - }, - }, - } satisfies OpenNextConfig; -} +export * from "@opennextjs/core/helpers/withCloudflare.js"; diff --git a/packages/open-next/src/http/index.ts b/packages/open-next/src/http/index.ts index 49efb2fe..9492b4bb 100644 --- a/packages/open-next/src/http/index.ts +++ b/packages/open-next/src/http/index.ts @@ -1,4 +1 @@ -// @__PURE__ -export * from "./openNextResponse.js"; -// @__PURE__ -export * from "./request.js"; +export * from "@opennextjs/core/http/index.js"; diff --git a/packages/open-next/src/http/openNextResponse.ts b/packages/open-next/src/http/openNextResponse.ts index b76cc271..75431c81 100644 --- a/packages/open-next/src/http/openNextResponse.ts +++ b/packages/open-next/src/http/openNextResponse.ts @@ -1,388 +1 @@ -import type { IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from "node:http"; -import type { Socket } from "node:net"; -import { Transform } from "node:stream"; -import type { TransformCallback, Writable } from "node:stream"; - -import type { StreamCreator } from "@/types/open-next"; - -import { debug } from "../adapters/logger"; - -import { parseHeaders, parseSetCookieHeader } from "./util"; - -const SET_COOKIE_HEADER = "set-cookie"; -const CANNOT_BE_USED = "This cannot be used in OpenNext"; - -// We only need to implement the methods that are used by next.js -export class OpenNextNodeResponse extends Transform implements ServerResponse { - statusCode!: number; - statusMessage = ""; - headers: OutgoingHttpHeaders = {}; - headersSent = false; - _chunks: Buffer[] = []; - headersAlreadyFixed = false; - - private _cookies: string[] = []; - private responseStream?: Writable; - private bodyLength = 0; - - // To comply with the ServerResponse interface : - strictContentLength = false; - assignSocket(_socket: Socket): void { - throw new Error(CANNOT_BE_USED); - } - detachSocket(_socket: Socket): void { - throw new Error(CANNOT_BE_USED); - } - // We might have to revisit those 3 in the future - writeContinue(_callback?: (() => void) | undefined): void { - throw new Error(CANNOT_BE_USED); - } - writeEarlyHints(_hints: Record, _callback?: (() => void) | undefined): void { - throw new Error(CANNOT_BE_USED); - } - writeProcessing(): void { - throw new Error(CANNOT_BE_USED); - } - /** - * This is a dummy request object to comply with the ServerResponse interface - * It will never be defined - */ - req!: IncomingMessage; - chunkedEncoding = false; - shouldKeepAlive = true; - useChunkedEncodingByDefault = true; - sendDate = false; - connection: Socket | null = null; - socket: Socket | null = null; - setTimeout(_msecs: number, _callback?: (() => void) | undefined): this { - throw new Error(CANNOT_BE_USED); - } - addTrailers(_headers: OutgoingHttpHeaders | readonly [string, string][]): void { - throw new Error(CANNOT_BE_USED); - } - - constructor( - private fixHeadersFn: (headers: OutgoingHttpHeaders) => void, - private onEnd: (headers: OutgoingHttpHeaders) => Promise, - private streamCreator?: StreamCreator, - private initialHeaders?: OutgoingHttpHeaders, - statusCode?: number - ) { - super(); - // We only set the status code if it is not a NaN and it is a number - // Only allow status codes between 100 and 599 https://httpwg.org/specs/rfc9110.html#status.codes - if (statusCode && Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599) { - this.statusCode = statusCode; - } - - // https://github.com/vercel/next.js/blob/ea08bf2/packages/next/src/server/web/spec-extension/adapters/next-request.ts#L46-L54 - // We want to destroy this response when the original response/request is closed. (i.e when the client disconnects) - // This is to support `request.signal.onabort` in route handlers - streamCreator?.abortSignal?.addEventListener("abort", () => { - this.destroy(); - }); - } - - // Necessary for next 12 - // We might have to implement all the methods here - get originalResponse() { - return this; - } - - get finished() { - return this.responseStream ? this.responseStream?.writableFinished : this.writableFinished; - } - - setHeader(name: string, value: string | string[]): this { - const key = name.toLowerCase(); - if (key === SET_COOKIE_HEADER) { - if (Array.isArray(value)) { - this._cookies = value; - } else { - this._cookies = [value]; - } - } - // We should always replace the header - // See https://nodejs.org/docs/latest-v18.x/api/http.html#responsesetheadername-value - this.headers[key] = value; - - return this; - } - - removeHeader(name: string): this { - const key = name.toLowerCase(); - if (key === SET_COOKIE_HEADER) { - this._cookies = []; - } else { - delete this.headers[key]; - } - return this; - } - - hasHeader(name: string): boolean { - const key = name.toLowerCase(); - if (key === SET_COOKIE_HEADER) { - return this._cookies.length > 0; - } - return this.headers[key] !== undefined; - } - - getHeaders(): OutgoingHttpHeaders { - return this.headers; - } - - getHeader(name: string): OutgoingHttpHeader | undefined { - return this.headers[name.toLowerCase()]; - } - - getHeaderNames(): string[] { - return Object.keys(this.headers); - } - - // Only used directly in next@14+ - flushHeaders() { - this.headersSent = true; - // Initial headers should be merged with the new headers - // These initial headers are the one created either in the middleware or in next.config.js - const mergeHeadersPriority = globalThis.__openNextAls?.getStore()?.mergeHeadersPriority ?? "middleware"; - if (this.initialHeaders) { - this.headers = - mergeHeadersPriority === "middleware" - ? { - ...this.headers, - ...this.initialHeaders, - } - : { - ...this.initialHeaders, - ...this.headers, - }; - const initialCookies = parseSetCookieHeader(this.initialHeaders[SET_COOKIE_HEADER]?.toString()); - this._cookies = - mergeHeadersPriority === "middleware" - ? [...this._cookies, ...initialCookies] - : [...initialCookies, ...this._cookies]; - } - this.fixHeaders(this.headers); - this.fixHeadersForError(); - - // We need to fix the set-cookie header here - this.headers[SET_COOKIE_HEADER] = this._cookies; - - const parsedHeaders = parseHeaders(this.headers); - - // We need to remove the set-cookie header from the parsed headers because - // it does not handle multiple set-cookie headers properly - delete parsedHeaders[SET_COOKIE_HEADER]; - - if (this.streamCreator) { - this.responseStream = this.streamCreator?.writeHeaders({ - statusCode: this.statusCode ?? 200, - cookies: this._cookies, - headers: parsedHeaders, - }); - this.pipe(this.responseStream); - } - } - - appendHeader(name: string, value: string | string[]): this { - const key = name.toLowerCase(); - if (!this.hasHeader(key)) { - return this.setHeader(key, value); - } - const existingHeader = this.getHeader(key) as string | string[]; - const toAppend = Array.isArray(value) ? value : [value]; - const newValue = Array.isArray(existingHeader) - ? [...existingHeader, ...toAppend] - : [existingHeader, ...toAppend]; - return this.setHeader(key, newValue); - } - - // Might be used in next page api routes - writeHead( - statusCode: number, - statusMessage?: string | undefined, - headers?: OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined - ): this; - writeHead(statusCode: number, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined): this; - writeHead(statusCode: unknown, statusMessage?: unknown, headers?: unknown): this { - let _headers = headers as OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined; - let _statusMessage: string | undefined; - if (typeof statusMessage === "string") { - _statusMessage = statusMessage; - } else { - _headers = statusMessage as OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined; - } - const finalHeaders: OutgoingHttpHeaders = this.headers; - if (_headers) { - if (Array.isArray(_headers)) { - // headers may be an Array where the keys and values are in the same list. It is not a list of tuples. So, the even-numbered offsets are key values, and the odd-numbered offsets are the associated values. - for (let i = 0; i < _headers.length; i += 2) { - finalHeaders[_headers[i] as string] = _headers[i + 1] as string | string[]; - } - } else { - for (const key of Object.keys(_headers)) { - finalHeaders[key] = _headers[key]; - } - } - } - - this.statusCode = statusCode as number; - if (headers) { - this.headers = finalHeaders; - } - this.flushHeaders(); - return this; - } - - /** - * OpenNext specific method - */ - - fixHeaders(headers: OutgoingHttpHeaders) { - if (this.headersAlreadyFixed) { - return; - } - this.fixHeadersFn(headers); - this.headersAlreadyFixed = true; - } - - getFixedHeaders(): OutgoingHttpHeaders { - // Do we want to apply this on writeHead? - this.fixHeaders(this.headers); - this.fixHeadersForError(); - // This way we ensure that the cookies are correct - this.headers[SET_COOKIE_HEADER] = this._cookies; - return this.headers; - } - - getBody() { - return Buffer.concat(this._chunks); - } - - private _internalWrite(chunk: Buffer | string, encoding: BufferEncoding) { - // When encoding === 'buffer', chunk is already a Buffer - // and does not need to be converted again. - // @ts-expect-error TS2367 'encoding' can be 'buffer', but it's not in the - // official type definition - const buffer = encoding === "buffer" ? (chunk as Buffer) : Buffer.from(chunk, encoding); - this.bodyLength += buffer.length; - if (this.streamCreator?.retainChunks !== false) { - // Avoid keeping chunks around when the `StreamCreator` supports it to save memory - this._chunks.push(buffer); - } - // No need to pass the encoding for buffers - this.push(buffer); - this.streamCreator?.onWrite?.(); - } - - _transform(chunk: Buffer | string, encoding: BufferEncoding, callback: TransformCallback): void { - if (!this.headersSent) { - this.flushHeaders(); - } - - this._internalWrite(chunk, encoding); - callback(); - } - - _flush(callback: TransformCallback): void { - if (!this.headersSent) { - this.flushHeaders(); - } - // In some cases we might not have a store i.e. for example in the image optimization function - // We may want to reconsider this in the future, it might be interesting to have access to this store everywhere - globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add(this.onEnd(this.headers)); - this.streamCreator?.onFinish?.(this.bodyLength); - - //This is only here because of aws broken streaming implementation. - //Hopefully one day they will be able to give us a working streaming implementation in lambda for everyone - //If you're lucky you have a working streaming implementation in your aws account and don't need this - //If not you can set the OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE env variable to true - //BE CAREFUL: Aws keeps rolling out broken streaming implementations even on accounts that had working ones before - //This is not dependent on the node runtime used - if ( - this.bodyLength === 0 && - // We use an env variable here because not all aws account have the same behavior - // On some aws accounts the response will hang if the body is empty - // We are modifying the response body here, this is not a good practice - process.env.OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE === "true" - ) { - debug('Force writing "SOMETHING" to the response body'); - this.push("SOMETHING"); - } - callback(); - } - - /** - * New method in Node 18.15+ - * There are probably not used right now in Next.js, but better be safe than sorry - */ - - setHeaders(headers: Headers | Map): this { - headers.forEach((value, key) => { - this.setHeader(key, Array.isArray(value) ? value : value.toString()); - }); - return this; - } - - /** - * Next specific methods - * On earlier versions of next.js, those methods are mandatory to make everything work - */ - - get sent() { - return this.finished || this.headersSent; - } - - getHeaderValues(name: string): string[] | undefined { - const values = this.getHeader(name); - - if (values === undefined) return undefined; - - return (Array.isArray(values) ? values : [values]).map((value) => value.toString()); - } - - send() { - for (const chunk of this._chunks) { - this.write(chunk); - } - this.end(); - } - - body(value: string) { - this.write(value); - return this; - } - - onClose(callback: () => void) { - this.on("close", callback); - } - - redirect(destination: string, statusCode: number) { - this.setHeader("Location", destination); - this.statusCode = statusCode; - - // Since IE11 doesn't support the 308 header add backwards - // compatibility using refresh header - if (statusCode === 308) { - this.setHeader("Refresh", `0;url=${destination}`); - } - - //TODO: test to see if we need to call end here - return this; - } - - // For some reason, next returns the 500 error page with some cache-control headers - // We need to fix that - private fixHeadersForError() { - if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") { - return; - } - // We only check for 404 and 500 errors - // The rest should be errors that are handled by the user and they should set the cache headers themselves - if (this.statusCode === 404 || this.statusCode === 500) { - // For some reason calling this.setHeader("Cache-Control", "no-cache, no-store, must-revalidate") does not work here - // The function is not even called, i'm probably missing something obvious - this.headers["cache-control"] = "private, no-cache, no-store, max-age=0, must-revalidate"; - } - } -} +export * from "@opennextjs/core/http/openNextResponse.js"; diff --git a/packages/open-next/src/http/request.ts b/packages/open-next/src/http/request.ts index a70eaa9f..fc6b80fa 100644 --- a/packages/open-next/src/http/request.ts +++ b/packages/open-next/src/http/request.ts @@ -1,54 +1 @@ -// Copied and modified from serverless-http by Doug Moscrop -// https://github.com/dougmoscrop/serverless-http/blob/master/lib/request.js -// Licensed under the MIT License - -// @ts-nocheck -import http from "node:http"; - -export class IncomingMessage extends http.IncomingMessage { - constructor({ - method, - url, - headers, - body, - remoteAddress, - }: { - method: string; - url: string; - headers: Record; - body?: Buffer; - remoteAddress?: string; - }) { - super({ - encrypted: true, - readable: false, - remoteAddress, - address: () => ({ port: 443 }), - end: Function.prototype, - destroy: Function.prototype, - }); - - // Set the content length when there is a body. - // See https://httpwg.org/specs/rfc9110.html#field.content-length - if (body) { - headers["content-length"] ??= String(Buffer.byteLength(body)); - } - - Object.assign(this, { - ip: remoteAddress, - complete: true, - httpVersion: "1.1", - httpVersionMajor: "1", - httpVersionMinor: "1", - method, - headers, - body, - url, - }); - - this._read = () => { - this.push(body); - this.push(null); - }; - } -} +export * from "@opennextjs/core/http/request.js"; diff --git a/packages/open-next/src/http/util.ts b/packages/open-next/src/http/util.ts index a0902d31..604249dc 100644 --- a/packages/open-next/src/http/util.ts +++ b/packages/open-next/src/http/util.ts @@ -1,90 +1 @@ -import type http from "node:http"; - -import { warn } from "../adapters/logger"; - -export const parseHeaders = (headers?: http.OutgoingHttpHeader[] | http.OutgoingHttpHeaders) => { - const result: Record = {}; - if (!headers) { - return result; - } - - for (const [key, value] of Object.entries(headers)) { - if (value === undefined) { - continue; - } - const keyLower = key.toLowerCase(); - /** - * Next can return an Array for the Location header when you return null from a get in the cacheHandler on a page that has a redirect() - * We dont want to merge that into a comma-separated string - * If they are the same just return one of them - * Otherwise return the last one - * See: https://github.com/opennextjs/opennextjs-cloudflare/issues/875#issuecomment-3258248276 - * and https://github.com/opennextjs/opennextjs-aws/pull/977#issuecomment-3261763114 - */ - if (keyLower === "location" && Array.isArray(value)) { - if (value.length === 1 || value[0] === value[1]) { - result[keyLower] = value[0]; - } else { - warn("Multiple different values for Location header found. Using the last one"); - result[keyLower] = value[value.length - 1]; - } - continue; - } - result[keyLower] = convertHeader(value); - } - - return result; -}; - -export const convertHeader = (header: http.OutgoingHttpHeader) => { - if (typeof header === "string") { - return header; - } - if (Array.isArray(header)) { - return header.join(","); - } - return String(header); -}; - -/** - * Parses a (comma-separated) list of Set-Cookie headers - * - * @param cookies A comma-separated list of Set-Cookie headers or a list of Set-Cookie headers - * @returns A list of Set-Cookie header - */ -export function parseSetCookieHeader(cookies: string | string[] | null | undefined): string[] { - if (!cookies) { - return []; - } - - if (typeof cookies === "string") { - // Split the cookie string on ",". - // Note that "," can also appear in the Expires value (i.e. `Expires=Thu, 01 June`) - // so we have to skip it with a negative lookbehind. - return cookies.split(/(? c.trim()); - } - - return cookies; -} - -/** - * - * Get the query object from an iterable of [key, value] pairs - * @param it - The iterable of [key, value] pairs - * @returns The query object - */ -export function getQueryFromIterator(it: Iterable<[string, string]>) { - const query: Record = {}; - for (const [key, value] of it) { - if (key in query) { - if (Array.isArray(query[key])) { - query[key].push(value); - } else { - query[key] = [query[key], value]; - } - } else { - query[key] = value; - } - } - return query; -} +export * from "@opennextjs/core/http/util.js"; diff --git a/packages/open-next/src/logger.ts b/packages/open-next/src/logger.ts index 9ce195f1..4db02f0c 100644 --- a/packages/open-next/src/logger.ts +++ b/packages/open-next/src/logger.ts @@ -1,18 +1,2 @@ -import chalk from "chalk"; - -type LEVEL = "info" | "debug"; - -let logLevel: LEVEL = "info"; - -export default { - setLevel: (level: LEVEL) => (logLevel = level), - debug: (...args: unknown[]) => { - if (logLevel !== "debug") return; - console.log(chalk.magenta("DEBUG"), ...args); - }, - info: console.log, - warn: (...args: unknown[]) => console.warn(chalk.yellow("WARN"), ...args), - error: (...args: unknown[]) => console.error(chalk.red("ERROR"), ...args), - time: console.time, - timeEnd: console.timeEnd, -}; +export { default } from "@opennextjs/core/logger.js"; +export * from "@opennextjs/core/logger.js"; diff --git a/packages/open-next/src/minimize-js.ts b/packages/open-next/src/minimize-js.ts index 2334c0fb..d8ed6424 100644 --- a/packages/open-next/src/minimize-js.ts +++ b/packages/open-next/src/minimize-js.ts @@ -1,100 +1 @@ -// Copied and modified from node-minify-all-js by Adones Pitogo -// https://github.com/adonespitogo/node-minify-all-js/blob/master/index.js - -// @ts-nocheck -import fs from "node:fs/promises"; -import path from "node:path"; - -import minify from "@node-minify/core"; -import terser from "@node-minify/terser"; - -const failed_files = []; -let total_files = 0; -const options = {}; - -const promiseSeries = async (tasks, initial) => { - if (!Array.isArray(tasks)) { - return Promise.reject(new TypeError("promise.series only accepts an array of functions")); - } - - return tasks.reduce((current, next) => { - return current.then(next); - }, Promise.resolve(initial)); -}; - -const minifyJS = async (file) => { - total_files++; - try { - await minify({ - compressor: terser, - input: file, - output: file, - options: { - module: options.module, - mangle: options.mangle, - compress: { reduce_vars: false }, - }, - }); - } catch (e) { - failed_files.push(file); - } - //process.stdout.write("."); -}; - -const minifyJSON = async (file) => { - try { - if (options.compress_json || options.packagejson) { - total_files++; - const is_package_json = file.indexOf("package.json") > -1; - const data = await fs.readFile(file, "utf8"); - const json = JSON.parse(data); - let new_json = {}; - if (options.packagejson && is_package_json) { - const { name, version, bin, main, binary, engines } = json; - new_json = { name, version }; - if (bin) new_json.bin = bin; - if (binary) new_json.binary = binary; - if (main) new_json.main = main; - if (engines) new_json.engines = engines; - } else { - new_json = json; - } - await fs.writeFile(file, JSON.stringify(new_json)); - } - } catch (e) {} - //process.stdout.write("."); -}; - -const walk = async (currentDirPath) => { - const js_files = []; - const json_files = []; - const dirs = []; - const current_dirs = await fs.readdir(currentDirPath); - for (const name of current_dirs) { - const filePath = path.join(currentDirPath, name); - const stat = await fs.stat(filePath); - const is_bin = /\.bin$/; - if (stat.isFile()) { - if (filePath.substr(-5) === ".json") json_files.push(filePath); - else if (filePath.substr(-3) === ".js" || options.all_js) js_files.push(filePath); - } else if (stat.isDirectory() && !is_bin.test(filePath)) { - dirs.push(filePath); - } - } - const js_promise = Promise.all(js_files.map((f) => minifyJS(f))); - const json_promise = Promise.all(json_files.map((f) => minifyJSON(f))); - await Promise.all([js_promise, json_promise]); - await promiseSeries(dirs.map((dir) => () => walk(dir))); -}; - -export async function minifyAll(dir, opts) { - Object.assign(options, opts || {}); - //console.log("minify-all-js options:\n", JSON.stringify(options, null, 2)); - await walk(dir); - //process.stdout.write(".\n"); - //console.log("Total found files: " + total_files); - if (failed_files.length) { - console.log("\n\nFailed to minify files:"); - failed_files.forEach((f) => console.log(`\t${f}`)); - } -} +export * from "@opennextjs/core/minimize-js.js"; diff --git a/packages/open-next/src/overrides/assetResolver/dummy.ts b/packages/open-next/src/overrides/assetResolver/dummy.ts index 4de2f2d0..8f789714 100644 --- a/packages/open-next/src/overrides/assetResolver/dummy.ts +++ b/packages/open-next/src/overrides/assetResolver/dummy.ts @@ -1,12 +1,2 @@ -import type { AssetResolver } from "@/types/overrides"; - -/** - * A dummy asset resolver. - * - * It never overrides the result with an asset. - */ -const resolver: AssetResolver = { - name: "dummy", -}; - -export default resolver; +export { default } from "@opennextjs/core/overrides/assetResolver/dummy.js"; +export * from "@opennextjs/core/overrides/assetResolver/dummy.js"; diff --git a/packages/open-next/src/overrides/cdnInvalidation/dummy.ts b/packages/open-next/src/overrides/cdnInvalidation/dummy.ts index 5124a1e5..12584686 100644 --- a/packages/open-next/src/overrides/cdnInvalidation/dummy.ts +++ b/packages/open-next/src/overrides/cdnInvalidation/dummy.ts @@ -1,8 +1,2 @@ -import type { CDNInvalidationHandler } from "@/types/overrides"; - -export default { - name: "dummy", - invalidatePaths: (_) => { - return Promise.resolve(); - }, -} satisfies CDNInvalidationHandler; +export { default } from "@opennextjs/core/overrides/cdnInvalidation/dummy.js"; +export * from "@opennextjs/core/overrides/cdnInvalidation/dummy.js"; diff --git a/packages/open-next/src/overrides/converters/dummy.ts b/packages/open-next/src/overrides/converters/dummy.ts index 3d666d99..66f6fd35 100644 --- a/packages/open-next/src/overrides/converters/dummy.ts +++ b/packages/open-next/src/overrides/converters/dummy.ts @@ -1,24 +1,2 @@ -import type { Converter } from "@/types/overrides"; - -type DummyEventOrResult = { - type: "dummy"; - original: unknown; -}; - -const converter: Converter = { - convertFrom(event) { - return Promise.resolve({ - type: "dummy", - original: event, - }); - }, - convertTo(internalResult) { - return Promise.resolve({ - type: "dummy", - original: internalResult, - }); - }, - name: "dummy", -}; - -export default converter; +export { default } from "@opennextjs/core/overrides/converters/dummy.js"; +export * from "@opennextjs/core/overrides/converters/dummy.js"; diff --git a/packages/open-next/src/overrides/converters/edge.ts b/packages/open-next/src/overrides/converters/edge.ts index 5261283d..9b230c35 100644 --- a/packages/open-next/src/overrides/converters/edge.ts +++ b/packages/open-next/src/overrides/converters/edge.ts @@ -1,119 +1,2 @@ -import { Buffer } from "node:buffer"; - -import cookieParser from "cookie"; - -import { parseSetCookieHeader } from "@/http/util"; -import type { InternalEvent, InternalResult, MiddlewareResult } from "@/types/open-next"; -import type { Converter } from "@/types/overrides"; - -import { getQueryFromSearchParams } from "./utils.js"; - -declare global { - // Makes convertTo returns the request instead of fetching it. - var __dangerous_ON_edge_converter_returns_request: boolean | undefined; -} - -// https://fetch.spec.whatwg.org/#statuses -const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]); - -const converter: Converter = { - convertFrom: async (event: unknown) => { - const request = event as Request; - const url = new URL(request.url); - - const searchParams = url.searchParams; - const query = getQueryFromSearchParams(searchParams); - const headers: Record = {}; - request.headers.forEach((value, key) => { - headers[key] = value; - }); - const rawPath = url.pathname; - const method = request.method; - const shouldHaveBody = method !== "GET" && method !== "HEAD"; - - // Only read body for methods that should have one - const body = shouldHaveBody ? Buffer.from(await request.arrayBuffer()) : undefined; - - const cookieHeader = request.headers.get("cookie"); - const cookies = cookieHeader ? (cookieParser.parse(cookieHeader) as Record) : {}; - - return { - type: "core", - method, - rawPath, - url: request.url, - body, - headers, - remoteAddress: request.headers.get("x-forwarded-for") ?? "::1", - query, - cookies, - }; - }, - convertTo: async (result) => { - if ("internalEvent" in result) { - const request = new Request(result.internalEvent.url, { - body: result.internalEvent.body as BodyInit | undefined, - method: result.internalEvent.method, - headers: { - ...result.internalEvent.headers, - "x-forwarded-host": result.internalEvent.headers.host, - }, - }); - - if (globalThis.__dangerous_ON_edge_converter_returns_request === true) { - if (result.initialResponse) { - return { - initialResponse: result.initialResponse, - request, - }; - } - return request; - } - - const cfCache = - (result.isISR || result.internalEvent.rawPath.startsWith("/_next/image")) && - process.env.DISABLE_CACHE !== "true" - ? { cacheEverything: true } - : {}; - - //TODO: we need to handle the PPR case here as well. - // We'll revisit this when we'll look at making StreamCreator mandatory. - return fetch(request, { - // This is a hack to make sure that the response is cached by Cloudflare - // See https://developers.cloudflare.com/workers/examples/cache-using-fetch/#caching-html-resources - // @ts-expect-error - This is a Cloudflare specific option - cf: cfCache, - }); - } - const headers = new Headers(); - for (const [key, value] of Object.entries(result.headers)) { - if (key === "set-cookie" && typeof value === "string") { - // If the value is a string, we need to parse it into an array - // This is the case for middleware direct result - const cookies = parseSetCookieHeader(value); - for (const cookie of cookies) { - headers.append(key, cookie); - } - continue; - } - if (Array.isArray(value)) { - for (const v of value) { - headers.append(key, v); - } - } else { - headers.set(key, value); - } - } - - // We should not return a body for statusCode's that doesn't allow bodies - const body = NULL_BODY_STATUSES.has(result.statusCode) ? null : (result.body as ReadableStream); - - return new Response(body, { - status: result.statusCode, - headers, - }); - }, - name: "edge", -}; - -export default converter; +export { default } from "@opennextjs/core/overrides/converters/edge.js"; +export * from "@opennextjs/core/overrides/converters/edge.js"; diff --git a/packages/open-next/src/overrides/converters/node.ts b/packages/open-next/src/overrides/converters/node.ts index 60c5fffa..f89bed98 100644 --- a/packages/open-next/src/overrides/converters/node.ts +++ b/packages/open-next/src/overrides/converters/node.ts @@ -1,58 +1,2 @@ -import type { IncomingMessage } from "node:http"; - -import cookieParser from "cookie"; - -import type { InternalResult } from "@/types/open-next"; -import type { Converter } from "@/types/overrides"; - -import { extractHostFromHeaders, getQueryFromSearchParams } from "./utils.js"; - -const converter: Converter = { - convertFrom: async (event: unknown) => { - const req = event as IncomingMessage & { protocol?: string }; - const body = await new Promise((resolve) => { - const chunks: Uint8Array[] = []; - req.on("data", (chunk) => { - chunks.push(chunk); - }); - req.on("end", () => { - resolve(Buffer.concat(chunks)); - }); - }); - - const headers = Object.fromEntries( - Object.entries(req.headers ?? {}) - .map(([key, value]) => [key.toLowerCase(), Array.isArray(value) ? value.join(",") : value]) - .filter(([key]) => key) - ); - // https://nodejs.org/api/http.html#messageurl - const url = new URL( - `${req.protocol ? req.protocol : "http"}://${extractHostFromHeaders(headers)}${req.url}` - ); - const query = getQueryFromSearchParams(url.searchParams); - - const cookieHeader = req.headers.cookie; - const cookies = cookieHeader ? (cookieParser.parse(cookieHeader) as Record) : {}; - - return { - type: "core", - method: req.method ?? "GET", - rawPath: url.pathname, - url: url.href, - body, - headers, - remoteAddress: (req.headers["x-forwarded-for"] as string) ?? req.socket.remoteAddress ?? "::1", - query, - cookies, - }; - }, - // Nothing to do here, it's streaming - convertTo: async (internalResult: InternalResult) => ({ - body: internalResult.body, - headers: internalResult.headers, - statusCode: internalResult.statusCode, - }), - name: "node", -}; - -export default converter; +export { default } from "@opennextjs/core/overrides/converters/node.js"; +export * from "@opennextjs/core/overrides/converters/node.js"; diff --git a/packages/open-next/src/overrides/converters/utils.ts b/packages/open-next/src/overrides/converters/utils.ts index 0fb60ff1..6991cdb8 100644 --- a/packages/open-next/src/overrides/converters/utils.ts +++ b/packages/open-next/src/overrides/converters/utils.ts @@ -1,31 +1 @@ -import { getQueryFromIterator } from "@/http/util.js"; - -export function removeUndefinedFromQuery(query: Record) { - const newQuery: Record = {}; - for (const [key, value] of Object.entries(query)) { - if (value !== undefined) { - newQuery[key] = value; - } - } - return newQuery; -} - -/** - * Extract the host from the headers (default to "on") - * - * @param headers - * @returns The host - */ -export function extractHostFromHeaders(headers: Record): string { - return headers["x-forwarded-host"] ?? headers.host ?? "on"; -} - -/** - * Get the query object from an URLSearchParams - * - * @param searchParams - * @returns - */ -export function getQueryFromSearchParams(searchParams: URLSearchParams) { - return getQueryFromIterator(searchParams.entries()); -} +export * from "@opennextjs/core/overrides/converters/utils.js"; diff --git a/packages/open-next/src/overrides/imageLoader/dummy.ts b/packages/open-next/src/overrides/imageLoader/dummy.ts index b3b2d3a6..7a776fba 100644 --- a/packages/open-next/src/overrides/imageLoader/dummy.ts +++ b/packages/open-next/src/overrides/imageLoader/dummy.ts @@ -1,11 +1,2 @@ -import type { ImageLoader } from "@/types/overrides"; -import { FatalError } from "@/utils/error"; - -const dummyLoader: ImageLoader = { - name: "dummy", - load: async (_: string) => { - throw new FatalError("Dummy loader is not implemented"); - }, -}; - -export default dummyLoader; +export { default } from "@opennextjs/core/overrides/imageLoader/dummy.js"; +export * from "@opennextjs/core/overrides/imageLoader/dummy.js"; diff --git a/packages/open-next/src/overrides/imageLoader/fs-dev.ts b/packages/open-next/src/overrides/imageLoader/fs-dev.ts index cdf0b77e..42fb4ef0 100644 --- a/packages/open-next/src/overrides/imageLoader/fs-dev.ts +++ b/packages/open-next/src/overrides/imageLoader/fs-dev.ts @@ -1,21 +1,2 @@ -import fs from "node:fs"; -import path from "node:path"; - -import { NextConfig } from "@/config/index"; -import type { ImageLoader } from "@/types/overrides"; -import { getMonorepoRelativePath } from "@/utils/normalize-path"; - -export default { - name: "fs-dev", - load: async (url: string) => { - const urlWithoutBasePath = NextConfig.basePath ? url.slice(NextConfig.basePath.length) : url; - const imagePath = path.join(getMonorepoRelativePath(), "assets", urlWithoutBasePath); - const body = fs.createReadStream(imagePath); - const contentType = url.endsWith(".png") ? "image/png" : "image/jpeg"; - return { - body, - contentType, - cacheControl: "public, max-age=31536000, immutable", - }; - }, -} satisfies ImageLoader; +export { default } from "@opennextjs/core/overrides/imageLoader/fs-dev.js"; +export * from "@opennextjs/core/overrides/imageLoader/fs-dev.js"; diff --git a/packages/open-next/src/overrides/imageLoader/host.ts b/packages/open-next/src/overrides/imageLoader/host.ts index 7b2dd215..9add6dfe 100644 --- a/packages/open-next/src/overrides/imageLoader/host.ts +++ b/packages/open-next/src/overrides/imageLoader/host.ts @@ -1,33 +1,2 @@ -import { Readable } from "node:stream"; -import type { ReadableStream } from "node:stream/web"; - -import type { ImageLoader } from "@/types/overrides"; -import { FatalError } from "@/utils/error"; - -const hostLoader: ImageLoader = { - name: "host", - load: async (key: string) => { - const host = process.env.HOST; - if (!host) { - throw new FatalError("Host must be defined!"); - } - const url = `https://${host}${key}`; - const response = await fetch(url); - if (!response.ok) { - throw new FatalError(`Failed to fetch image from ${url}`); - } - if (!response.body) { - throw new FatalError("No body in response"); - } - const body = Readable.fromWeb(response.body as ReadableStream); - const contentType = response.headers.get("content-type") ?? "image/jpeg"; - const cacheControl = response.headers.get("cache-control") ?? "private, max-age=0, must-revalidate"; - return { - body, - contentType, - cacheControl, - }; - }, -}; - -export default hostLoader; +export { default } from "@opennextjs/core/overrides/imageLoader/host.js"; +export * from "@opennextjs/core/overrides/imageLoader/host.js"; diff --git a/packages/open-next/src/overrides/incrementalCache/dummy.ts b/packages/open-next/src/overrides/incrementalCache/dummy.ts index 24639aef..abf5e4f9 100644 --- a/packages/open-next/src/overrides/incrementalCache/dummy.ts +++ b/packages/open-next/src/overrides/incrementalCache/dummy.ts @@ -1,17 +1,2 @@ -import type { IncrementalCache } from "@/types/overrides"; -import { IgnorableError } from "@/utils/error"; - -const dummyIncrementalCache: IncrementalCache = { - name: "dummy", - get: async () => { - throw new IgnorableError('"Dummy" cache does not cache anything'); - }, - set: async () => { - throw new IgnorableError('"Dummy" cache does not cache anything'); - }, - delete: async () => { - throw new IgnorableError('"Dummy" cache does not cache anything'); - }, -}; - -export default dummyIncrementalCache; +export { default } from "@opennextjs/core/overrides/incrementalCache/dummy.js"; +export * from "@opennextjs/core/overrides/incrementalCache/dummy.js"; diff --git a/packages/open-next/src/overrides/incrementalCache/fs-dev.ts b/packages/open-next/src/overrides/incrementalCache/fs-dev.ts index 9df4f9c2..dd4da4f9 100644 --- a/packages/open-next/src/overrides/incrementalCache/fs-dev.ts +++ b/packages/open-next/src/overrides/incrementalCache/fs-dev.ts @@ -1,37 +1,2 @@ -import fs from "node:fs/promises"; -import path from "node:path"; - -import type { IncrementalCache } from "@/types/overrides.js"; -import { getMonorepoRelativePath } from "@/utils/normalize-path"; - -const buildId = process.env.NEXT_BUILD_ID; -const basePath = path.join(getMonorepoRelativePath(), `cache/${buildId}`); - -const getCacheKey = (key: string) => { - return path.join(basePath, `${key}.cache`); -}; - -const cache: IncrementalCache = { - name: "fs-dev", - get: async (key: string) => { - const fileData = await fs.readFile(getCacheKey(key), "utf-8"); - const data = JSON.parse(fileData); - const { mtime } = await fs.stat(getCacheKey(key)); - return { - value: data, - lastModified: mtime.getTime(), - }; - }, - set: async (key, value, isFetch) => { - const data = JSON.stringify(value); - const cacheKey = getCacheKey(key); - // We need to create the directory before writing the file - await fs.mkdir(path.dirname(cacheKey), { recursive: true }); - await fs.writeFile(cacheKey, data); - }, - delete: async (key) => { - await fs.rm(getCacheKey(key)); - }, -}; - -export default cache; +export { default } from "@opennextjs/core/overrides/incrementalCache/fs-dev.js"; +export * from "@opennextjs/core/overrides/incrementalCache/fs-dev.js"; diff --git a/packages/open-next/src/overrides/originResolver/dummy.ts b/packages/open-next/src/overrides/originResolver/dummy.ts index b713dedc..3cbc9d29 100644 --- a/packages/open-next/src/overrides/originResolver/dummy.ts +++ b/packages/open-next/src/overrides/originResolver/dummy.ts @@ -1,10 +1,2 @@ -import type { OriginResolver } from "@/types/overrides"; - -const dummyOriginResolver: OriginResolver = { - name: "dummy", - resolve: async (_path: string) => { - return false as const; - }, -}; - -export default dummyOriginResolver; +export { default } from "@opennextjs/core/overrides/originResolver/dummy.js"; +export * from "@opennextjs/core/overrides/originResolver/dummy.js"; diff --git a/packages/open-next/src/overrides/originResolver/pattern-env.ts b/packages/open-next/src/overrides/originResolver/pattern-env.ts index 3996f02b..e9644d32 100644 --- a/packages/open-next/src/overrides/originResolver/pattern-env.ts +++ b/packages/open-next/src/overrides/originResolver/pattern-env.ts @@ -1,84 +1,2 @@ -import type { Origin } from "@/types/open-next"; -import type { OriginResolver } from "@/types/overrides"; - -import { debug, error } from "../../adapters/logger"; - -// Cache parsed origins and compiled patterns at module level -let cachedOrigins: Record; -const cachedPatterns: Array<{ - key: string; - patterns: string[]; - regexes: RegExp[]; -}> = []; -let initialized = false; - -/** - * Initializes the cached values on the first execution - */ -function initializeOnce(): void { - if (initialized) return; - - // Parse origin JSON once - cachedOrigins = JSON.parse(process.env.OPEN_NEXT_ORIGIN ?? "{}") as Record; - - // Pre-compile all regex patterns - const functions = globalThis.openNextConfig.functions ?? {}; - for (const key in functions) { - if (key !== "default") { - const value = functions[key]; - const regexes: RegExp[] = []; - - for (const pattern of value.patterns) { - // Convert cloudfront pattern to regex - const regexPattern = `/${pattern - .replace(/\*\*/g, "(.*)") - .replace(/\*/g, "([^/]*)") - .replace(/\//g, "\\/") - .replace(/\?/g, ".")}`; - regexes.push(new RegExp(regexPattern)); - } - - cachedPatterns.push({ - key, - patterns: value.patterns, - regexes, - }); - } - } - - initialized = true; -} - -const envLoader: OriginResolver = { - name: "env", - resolve: async (_path: string) => { - try { - initializeOnce(); - - // Test against pre-compiled patterns - for (const { key, patterns, regexes } of cachedPatterns) { - for (const regex of regexes) { - if (regex.test(_path)) { - debug("Using origin", key, patterns); - return cachedOrigins[key]; - } - } - } - - if (_path.startsWith("/_next/image") && cachedOrigins.imageOptimizer) { - debug("Using origin", "imageOptimizer", _path); - return cachedOrigins.imageOptimizer; - } - if (cachedOrigins.default) { - debug("Using default origin", cachedOrigins.default, _path); - return cachedOrigins.default; - } - return false as const; - } catch (e) { - error("Error while resolving origin", e); - return false as const; - } - }, -}; - -export default envLoader; +export { default } from "@opennextjs/core/overrides/originResolver/pattern-env.js"; +export * from "@opennextjs/core/overrides/originResolver/pattern-env.js"; diff --git a/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts b/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts index ec361260..49bcb948 100644 --- a/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts +++ b/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts @@ -1,11 +1,2 @@ -import type { ProxyExternalRequest } from "@/types/overrides"; -import { FatalError } from "@/utils/error"; - -const DummyProxyExternalRequest: ProxyExternalRequest = { - name: "dummy", - proxy: async (_event) => { - throw new FatalError("This is a dummy implementation"); - }, -}; - -export default DummyProxyExternalRequest; +export { default } from "@opennextjs/core/overrides/proxyExternalRequest/dummy.js"; +export * from "@opennextjs/core/overrides/proxyExternalRequest/dummy.js"; diff --git a/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts b/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts index df1dd18c..d2ff29ad 100644 --- a/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts +++ b/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts @@ -1,33 +1,2 @@ -import type { ProxyExternalRequest } from "@/types/overrides"; -import { emptyReadableStream } from "@/utils/stream"; - -const fetchProxy: ProxyExternalRequest = { - name: "fetch-proxy", - // @ts-ignore - proxy: async (internalEvent) => { - const { url, headers: eventHeaders, method, body } = internalEvent; - - const headers = Object.fromEntries( - Object.entries(eventHeaders).filter(([key]) => key.toLowerCase() !== "cf-connecting-ip") - ); - - const response = await fetch(url, { - method, - headers, - body: body as BodyInit | undefined, - }); - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - return { - type: "core", - headers: responseHeaders, - statusCode: response.status, - isBase64Encoded: true, - body: response.body ?? emptyReadableStream(), - }; - }, -}; - -export default fetchProxy; +export { default } from "@opennextjs/core/overrides/proxyExternalRequest/fetch.js"; +export * from "@opennextjs/core/overrides/proxyExternalRequest/fetch.js"; diff --git a/packages/open-next/src/overrides/proxyExternalRequest/node.ts b/packages/open-next/src/overrides/proxyExternalRequest/node.ts index 97fdef0a..3f8ecdc3 100644 --- a/packages/open-next/src/overrides/proxyExternalRequest/node.ts +++ b/packages/open-next/src/overrides/proxyExternalRequest/node.ts @@ -1,83 +1,2 @@ -import { request } from "node:https"; -import { Readable } from "node:stream"; - -import type { InternalEvent, InternalResult } from "@/types/open-next"; -import type { ProxyExternalRequest } from "@/types/overrides"; - -import { debug, error } from "../../adapters/logger"; -import { isBinaryContentType } from "../../utils/binary"; - -function filterHeadersForProxy(headers: Record) { - const filteredHeaders: Record = {}; - const disallowedHeaders = [ - "host", - "connection", - "via", - "x-cache", - "transfer-encoding", - "content-encoding", - "content-length", - ]; - Object.entries(headers) - .filter(([key, _]) => { - const lowerKey = key.toLowerCase(); - return !(disallowedHeaders.includes(lowerKey) || lowerKey.startsWith("x-amz")); - }) - .forEach(([key, value]) => { - filteredHeaders[key] = value?.toString() ?? ""; - }); - return filteredHeaders; -} - -const nodeProxy: ProxyExternalRequest = { - name: "node-proxy", - proxy: (internalEvent: InternalEvent) => { - const { url, headers, method, body } = internalEvent; - debug("proxyRequest", url); - return new Promise((resolve, reject) => { - const filteredHeaders = filterHeadersForProxy(headers); - debug("filteredHeaders", filteredHeaders); - const req = request( - url, - { - headers: filteredHeaders, - method, - rejectUnauthorized: false, - }, - (_res) => { - const resHeaders = _res.headers; - const nodeReadableStream = - resHeaders["content-encoding"] === "br" - ? _res.pipe(require("node:zlib").createBrotliDecompress()) - : resHeaders["content-encoding"] === "gzip" - ? _res.pipe(require("node:zlib").createGunzip()) - : _res; - const isBase64Encoded = - isBinaryContentType(resHeaders["content-type"]) || !!resHeaders["content-encoding"]; - const result: InternalResult = { - type: "core", - headers: filterHeadersForProxy(resHeaders), - statusCode: _res.statusCode ?? 200, - // TODO: check base64 encoding - isBase64Encoded, - body: Readable.toWeb(nodeReadableStream), - }; - - resolve(result); - - _res.on("error", (e) => { - error("proxyRequest error", e); - reject(e); - }); - } - ); - - if (body && method !== "GET" && method !== "HEAD") { - req.write(body); - } - req.end(); - }); - }, -}; - -export default nodeProxy; +export { default } from "@opennextjs/core/overrides/proxyExternalRequest/node.js"; +export * from "@opennextjs/core/overrides/proxyExternalRequest/node.js"; diff --git a/packages/open-next/src/overrides/queue/direct.ts b/packages/open-next/src/overrides/queue/direct.ts index 0e1f99f9..68470fda 100644 --- a/packages/open-next/src/overrides/queue/direct.ts +++ b/packages/open-next/src/overrides/queue/direct.ts @@ -1,20 +1,2 @@ -import type { Queue } from "@/types/overrides.js"; - -const queue: Queue = { - name: "dev-queue", - send: async (message) => { - const prerenderManifest = (await import("../../adapters/config/index.js")).PrerenderManifest; - const { host, url } = message.MessageBody; - const protocol = host.includes("localhost") ? "http" : "https"; - const revalidateId: string = prerenderManifest?.preview?.previewModeId ?? ""; - await globalThis.internalFetch(`${protocol}://${host}${url}`, { - method: "HEAD", - headers: { - "x-prerender-revalidate": revalidateId, - "x-isr": "1", - }, - }); - }, -}; - -export default queue; +export { default } from "@opennextjs/core/overrides/queue/direct.js"; +export * from "@opennextjs/core/overrides/queue/direct.js"; diff --git a/packages/open-next/src/overrides/queue/dummy.ts b/packages/open-next/src/overrides/queue/dummy.ts index 8b5646ee..5d5d6a41 100644 --- a/packages/open-next/src/overrides/queue/dummy.ts +++ b/packages/open-next/src/overrides/queue/dummy.ts @@ -1,11 +1,2 @@ -import type { Queue } from "@/types/overrides"; -import { FatalError } from "@/utils/error"; - -const dummyQueue: Queue = { - name: "dummy", - send: async () => { - throw new FatalError("Dummy queue is not implemented"); - }, -}; - -export default dummyQueue; +export { default } from "@opennextjs/core/overrides/queue/dummy.js"; +export * from "@opennextjs/core/overrides/queue/dummy.js"; diff --git a/packages/open-next/src/overrides/tagCache/dummy.ts b/packages/open-next/src/overrides/tagCache/dummy.ts index ac44b532..98349d03 100644 --- a/packages/open-next/src/overrides/tagCache/dummy.ts +++ b/packages/open-next/src/overrides/tagCache/dummy.ts @@ -1,21 +1,2 @@ -import type { TagCache } from "@/types/overrides"; - -// We don't want to throw error on this one because we might use it when we don't need tag cache -const dummyTagCache: TagCache = { - name: "dummy", - mode: "original", - getByPath: async () => { - return []; - }, - getByTag: async () => { - return []; - }, - getLastModified: async (_: string, lastModified) => { - return lastModified ?? Date.now(); - }, - writeTags: async () => { - return; - }, -}; - -export default dummyTagCache; +export { default } from "@opennextjs/core/overrides/tagCache/dummy.js"; +export * from "@opennextjs/core/overrides/tagCache/dummy.js"; diff --git a/packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts b/packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts index 49ccb498..f80feb09 100644 --- a/packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts +++ b/packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts @@ -1,53 +1,2 @@ -import type { NextModeTagCache } from "@/types/overrides"; - -import { debug } from "../../adapters/logger"; - -const tagsMap = new Map(); - -export default { - name: "fs-dev-nextMode", - mode: "nextMode", - getLastRevalidated: async (tags: string[]) => { - if (globalThis.openNextConfig.dangerous?.disableTagCache) { - return 0; - } - - let lastRevalidated = 0; - - tags.forEach((tag) => { - const tagTime = tagsMap.get(tag); - if (tagTime && tagTime > lastRevalidated) { - lastRevalidated = tagTime; - } - }); - - debug("getLastRevalidated result:", lastRevalidated); - return lastRevalidated; - }, - hasBeenRevalidated: async (tags: string[], lastModified?: number) => { - if (globalThis.openNextConfig.dangerous?.disableTagCache) { - return false; - } - - const hasRevalidatedTag = tags.some((tag) => { - const tagRevalidatedAt = tagsMap.get(tag); - return tagRevalidatedAt ? tagRevalidatedAt > (lastModified ?? 0) : false; - }); - - debug("hasBeenRevalidated result:", hasRevalidatedTag); - return hasRevalidatedTag; - }, - writeTags: async (tags: string[]) => { - if (globalThis.openNextConfig.dangerous?.disableTagCache || tags.length === 0) { - return; - } - - debug("writeTags", { tags: tags }); - - tags.forEach((tag) => { - tagsMap.set(tag, Date.now()); - }); - - debug("writeTags completed, written", tags.length, "tags"); - }, -} satisfies NextModeTagCache; +export { default } from "@opennextjs/core/overrides/tagCache/fs-dev-nextMode.js"; +export * from "@opennextjs/core/overrides/tagCache/fs-dev-nextMode.js"; diff --git a/packages/open-next/src/overrides/tagCache/fs-dev.ts b/packages/open-next/src/overrides/tagCache/fs-dev.ts index 932eb216..01def372 100644 --- a/packages/open-next/src/overrides/tagCache/fs-dev.ts +++ b/packages/open-next/src/overrides/tagCache/fs-dev.ts @@ -1,56 +1,2 @@ -import fs from "node:fs"; -import path from "node:path"; - -import type { TagCache } from "@/types/overrides"; -import { getMonorepoRelativePath } from "@/utils/normalize-path"; - -const tagFile = path.join(getMonorepoRelativePath(), "dynamodb-provider/dynamodb-cache.json"); -const tagContent = fs.readFileSync(tagFile, "utf-8"); - -let tags = JSON.parse(tagContent) as { - tag: { S: string }; - path: { S: string }; - revalidatedAt: { N: string }; -}[]; - -const { NEXT_BUILD_ID } = process.env; - -function buildKey(key: string) { - return path.posix.join(NEXT_BUILD_ID ?? "", key); -} - -const tagCache: TagCache = { - name: "fs-dev", - mode: "original", - getByPath: async (path: string) => { - return tags - .filter((tagPathMapping) => tagPathMapping.path.S === buildKey(path)) - .map((tag) => tag.tag.S.replace(`${NEXT_BUILD_ID}/`, "")); - }, - getByTag: async (tag: string) => { - return tags - .filter((tagPathMapping) => tagPathMapping.tag.S === buildKey(tag)) - .map((tagEntry) => tagEntry.path.S.replace(`${NEXT_BUILD_ID}/`, "")); - }, - getLastModified: async (path: string, lastModified?: number) => { - const revalidatedTags = tags.filter( - (tagPathMapping) => - tagPathMapping.path.S === buildKey(path) && - Number.parseInt(tagPathMapping.revalidatedAt.N) > (lastModified ?? 0) - ); - return revalidatedTags.length > 0 ? -1 : (lastModified ?? Date.now()); - }, - writeTags: async (newTags) => { - const newTagsSet = new Set(newTags.map(({ tag, path }) => `${buildKey(tag)}-${buildKey(path)}`)); - const unchangedTags = tags.filter(({ tag, path }) => !newTagsSet.has(`${tag.S}-${path.S}`)); - tags = unchangedTags.concat( - newTags.map((item) => ({ - tag: { S: buildKey(item.tag) }, - path: { S: buildKey(item.path) }, - revalidatedAt: { N: `${item.revalidatedAt ?? Date.now()}` }, - })) - ); - }, -}; - -export default tagCache; +export { default } from "@opennextjs/core/overrides/tagCache/fs-dev.js"; +export * from "@opennextjs/core/overrides/tagCache/fs-dev.js"; diff --git a/packages/open-next/src/overrides/warmer/dummy.ts b/packages/open-next/src/overrides/warmer/dummy.ts index 92453195..9dda41e5 100644 --- a/packages/open-next/src/overrides/warmer/dummy.ts +++ b/packages/open-next/src/overrides/warmer/dummy.ts @@ -1,11 +1,2 @@ -import type { Warmer } from "@/types/overrides"; -import { FatalError } from "@/utils/error"; - -const dummyWarmer: Warmer = { - name: "dummy", - invoke: async (_: string) => { - throw new FatalError("Dummy warmer is not implemented"); - }, -}; - -export default dummyWarmer; +export { default } from "@opennextjs/core/overrides/warmer/dummy.js"; +export * from "@opennextjs/core/overrides/warmer/dummy.js"; diff --git a/packages/open-next/src/overrides/wrappers/cloudflare-edge.ts b/packages/open-next/src/overrides/wrappers/cloudflare-edge.ts index 60f7e8c6..98d62b29 100644 --- a/packages/open-next/src/overrides/wrappers/cloudflare-edge.ts +++ b/packages/open-next/src/overrides/wrappers/cloudflare-edge.ts @@ -1,60 +1,2 @@ -import type { InternalEvent, InternalResult, MiddlewareResult } from "@/types/open-next"; -import type { Wrapper, WrapperHandler } from "@/types/overrides"; - -const cfPropNameMapping: Record string, string]> = { - // The city name is percent-encoded. - // See https://github.com/vercel/vercel/blob/4cb6143/packages/functions/src/headers.ts#L94C19-L94C37 - city: [encodeURIComponent, "x-open-next-city"], - country: "x-open-next-country", - regionCode: "x-open-next-region", - latitude: "x-open-next-latitude", - longitude: "x-open-next-longitude", -}; - -interface WorkerContext { - waitUntil: (promise: Promise) => void; -} - -const handler: WrapperHandler = - async (handler, converter) => - async (...args: unknown[]): Promise => { - const [request, env, ctx] = args as [Request, Record, WorkerContext]; - globalThis.process = process; - - // Set the environment variables - // Cloudflare suggests to not override the process.env object but instead apply the values to it - for (const [key, value] of Object.entries(env)) { - if (typeof value === "string") { - process.env[key] = value; - } - } - - const internalEvent = await converter.convertFrom(request); - - // Retrieve geo information from the cloudflare request - // See https://developers.cloudflare.com/workers/runtime-apis/request - // Note: This code could be moved to a cloudflare specific converter when one is created. - const cfProperties = (request as Request & { cf?: Record }).cf; - for (const [propName, mapping] of Object.entries(cfPropNameMapping)) { - const propValue = cfProperties?.[propName]; - if (propValue != null) { - const [encode, headerName] = Array.isArray(mapping) ? mapping : [null, mapping]; - internalEvent.headers[headerName] = encode ? encode(propValue) : propValue; - } - } - - const response = await handler(internalEvent, { - waitUntil: ctx.waitUntil.bind(ctx), - }); - - const result = (await converter.convertTo(response)) as Response; - - return result; - }; - -export default { - wrapper: handler, - name: "cloudflare-edge", - supportStreaming: true, - edgeRuntime: true, -} satisfies Wrapper; +export { default } from "@opennextjs/core/overrides/wrappers/cloudflare-edge.js"; +export * from "@opennextjs/core/overrides/wrappers/cloudflare-edge.js"; diff --git a/packages/open-next/src/overrides/wrappers/cloudflare-node.ts b/packages/open-next/src/overrides/wrappers/cloudflare-node.ts index 4f90d895..cdadcc5b 100644 --- a/packages/open-next/src/overrides/wrappers/cloudflare-node.ts +++ b/packages/open-next/src/overrides/wrappers/cloudflare-node.ts @@ -1,129 +1,2 @@ -import { Writable } from "node:stream"; - -import type { InternalEvent, InternalResult, StreamCreator } from "@/types/open-next"; -import type { Wrapper, WrapperHandler } from "@/types/overrides"; - -// Response with null body status (101, 204, 205, or 304) cannot have a body. -const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]); - -const handler: WrapperHandler = - async (handler, converter) => - async (...args: unknown[]): Promise => { - const [request, env, ctx, abortSignal] = args as [ - Request, - Record, - { waitUntil: (promise: Promise) => void }, - AbortSignal, - ]; - globalThis.process = process; - // Set the environment variables - // Cloudflare suggests to not override the process.env object but instead apply the values to it - for (const [key, value] of Object.entries(env)) { - if (typeof value === "string") { - process.env[key] = value; - } - } - - const internalEvent = await converter.convertFrom(request); - const url = new URL(request.url); - - const { promise: promiseResponse, resolve: resolveResponse } = Promise.withResolvers(); - - const streamCreator: StreamCreator = { - writeHeaders(prelude: { - statusCode: number; - cookies: string[]; - headers: Record; - }): Writable { - const { statusCode, cookies, headers } = prelude; - - const responseHeaders = new Headers(headers); - for (const cookie of cookies) { - responseHeaders.append("Set-Cookie", cookie); - } - - // TODO(vicb): this is a workaround to make PPR work with `wrangler dev` - // See https://github.com/cloudflare/workers-sdk/issues/8004 - if (url.hostname === "localhost") { - responseHeaders.set("Content-Encoding", "identity"); - } - - // Optimize: skip ReadableStream creation for null body statuses - if (NULL_BODY_STATUSES.has(statusCode)) { - const response = new Response(null, { - status: statusCode, - headers: responseHeaders, - }); - resolveResponse(response); - - // Return a no-op Writable that discards all data - return new Writable({ - write(chunk, encoding, callback) { - callback(); - }, - }); - } - - let controller: ReadableStreamDefaultController; - const readable = new ReadableStream({ - start(c) { - controller = c; - }, - }); - - const response = new Response(readable, { - status: statusCode, - headers: responseHeaders, - }); - resolveResponse(response); - - return new Writable({ - write(chunk, encoding, callback) { - try { - controller.enqueue(chunk); - } catch (e: unknown) { - return callback(e instanceof Error ? e : new Error(String(e))); - } - callback(); - }, - final(callback) { - controller.close(); - callback(); - }, - destroy(error, callback) { - if (error) { - controller.error(error); - } else { - try { - controller.close(); - } catch { - // Ignore "This ReadableStream is closed" error - } - } - callback(error); - }, - }); - }, - // This is for passing along the original abort signal from the initial Request you retrieve in your worker - // Ensures that the response we pass to NextServer is aborted if the request is aborted - // By doing this `request.signal.onabort` will work in route handlers - abortSignal: abortSignal, - // There is no need to retain the chunks that were pushed to the response stream. - retainChunks: false, - }; - - ctx.waitUntil( - handler(internalEvent, { - streamCreator, - waitUntil: ctx.waitUntil.bind(ctx), - }) - ); - - return promiseResponse; - }; - -export default { - wrapper: handler, - name: "cloudflare-node", - supportStreaming: true, -} satisfies Wrapper; +export { default } from "@opennextjs/core/overrides/wrappers/cloudflare-node.js"; +export * from "@opennextjs/core/overrides/wrappers/cloudflare-node.js"; diff --git a/packages/open-next/src/overrides/wrappers/dummy.ts b/packages/open-next/src/overrides/wrappers/dummy.ts index 8649a8c8..1178320a 100644 --- a/packages/open-next/src/overrides/wrappers/dummy.ts +++ b/packages/open-next/src/overrides/wrappers/dummy.ts @@ -1,15 +1,2 @@ -import type { InternalEvent } from "@/types/open-next"; -import type { OpenNextHandlerOptions, Wrapper, WrapperHandler } from "@/types/overrides"; - -const dummyWrapper: WrapperHandler = - async (handler, _converter) => - async (...args: unknown[]): Promise => { - const [event, options] = args as [InternalEvent, OpenNextHandlerOptions | undefined]; - return await handler(event, options); - }; - -export default { - name: "dummy", - wrapper: dummyWrapper, - supportStreaming: true, -} satisfies Wrapper; +export { default } from "@opennextjs/core/overrides/wrappers/dummy.js"; +export * from "@opennextjs/core/overrides/wrappers/dummy.js"; diff --git a/packages/open-next/src/overrides/wrappers/express-dev.ts b/packages/open-next/src/overrides/wrappers/express-dev.ts index bcc9e8ef..1238bd8f 100644 --- a/packages/open-next/src/overrides/wrappers/express-dev.ts +++ b/packages/open-next/src/overrides/wrappers/express-dev.ts @@ -1,80 +1,2 @@ -import path from "node:path"; - -import express from "express"; - -import { NextConfig } from "@/config/index"; -import type { StreamCreator } from "@/types/open-next.js"; -import type { WrapperHandler } from "@/types/overrides.js"; -import { getMonorepoRelativePath } from "@/utils/normalize-path"; - -const wrapper: WrapperHandler = async (handler, converter) => { - const app = express(); - // We disable this cause we wanna use it ourself - // https://stackoverflow.com/a/13055495/16587222 - app.disable("x-powered-by"); - // To serve static assets - const basePath = NextConfig.basePath ?? ""; - app.use(basePath, express.static(path.join(getMonorepoRelativePath(), "assets"))); - - const imageHandlerPath = path.join(getMonorepoRelativePath(), "image-optimization-function/index.mjs"); - - const imageHandler = await import(imageHandlerPath).then((m) => m.handler); - - app.all(`${NextConfig.basePath ?? ""}/_next/image`, async (req, res) => { - const internalEvent = await converter.convertFrom(req); - const streamCreator: StreamCreator = { - writeHeaders: (prelude) => { - res.writeHead(prelude.statusCode, prelude.headers); - return res; - }, - }; - await imageHandler(internalEvent, { streamCreator }); - }); - - app.all(/.*/, async (req, res) => { - if (req.protocol === "http" && req.hostname === "localhost") { - // This is used internally by Next.js during redirects in server actions. We need to set it to the origin of the request. - process.env.__NEXT_PRIVATE_ORIGIN = `${req.protocol}://${req.hostname}`; - // This is to make `next-auth` and other libraries that rely on this header to work locally out of the box. - req.headers["x-forwarded-proto"] = req.protocol; - } - const internalEvent = await converter.convertFrom(req); - - const abortController = new AbortController(); - - const streamCreator: StreamCreator = { - writeHeaders: (prelude) => { - res.setHeader("Set-Cookie", prelude.cookies); - res.writeHead(prelude.statusCode, prelude.headers); - res.flushHeaders(); - return res; - }, - onFinish: () => {}, - abortSignal: abortController.signal, - }; - - res.on("close", () => { - abortController.abort(); - }); - - await handler(internalEvent, { streamCreator }); - }); - - const server = app.listen(Number.parseInt(process.env.PORT ?? "3000", 10), () => { - console.log(`Server running on port ${process.env.PORT ?? 3000}`); - }); - - app.on("error", (err) => { - console.error("error", err); - }); - - return () => { - server.close(); - }; -}; - -export default { - wrapper, - name: "expresss-dev", - supportStreaming: true, -}; +export { default } from "@opennextjs/core/overrides/wrappers/express-dev.js"; +export * from "@opennextjs/core/overrides/wrappers/express-dev.js"; diff --git a/packages/open-next/src/overrides/wrappers/node.ts b/packages/open-next/src/overrides/wrappers/node.ts index c1432bf6..13986fd2 100644 --- a/packages/open-next/src/overrides/wrappers/node.ts +++ b/packages/open-next/src/overrides/wrappers/node.ts @@ -1,76 +1,2 @@ -import { createServer } from "node:http"; - -import type { StreamCreator } from "@/types/open-next"; -import type { Wrapper, WrapperHandler } from "@/types/overrides"; - -import { debug, error } from "../../adapters/logger"; - -const wrapper: WrapperHandler = async (handler, converter) => { - const server = createServer(async (req, res) => { - const internalEvent = await converter.convertFrom(req); - - const abortController = new AbortController(); - - const streamCreator: StreamCreator = { - writeHeaders: (prelude) => { - res.setHeader("Set-Cookie", prelude.cookies); - res.writeHead(prelude.statusCode, prelude.headers); - res.flushHeaders(); - return res; - }, - abortSignal: abortController.signal, - }; - - res.on("close", () => { - abortController.abort(); - }); - - if (internalEvent.rawPath === "/__health") { - res.writeHead(200, { - "Content-Type": "text/plain", - }); - res.end("OK"); - } else { - await handler(internalEvent, { - streamCreator, - }); - } - }); - - await new Promise((resolve) => { - server.on("listening", () => { - const cleanup = (code: number) => { - debug("Closing server"); - server.close(() => { - debug("Server closed"); - process.exit(code); - }); - }; - console.log(`Listening on port ${process.env.PORT ?? "3000"}`); - debug(`Open Next version: ${process.env.OPEN_NEXT_VERSION}`); - - process.on("exit", (code) => cleanup(code)); - - process.on("SIGINT", () => cleanup(0)); - process.on("SIGTERM", () => cleanup(0)); - - resolve(); - }); - - server.listen(Number.parseInt(process.env.PORT ?? "3000", 10)); - }); - - server.on("error", (err) => { - error(err); - }); - - return () => { - server.close(); - }; -}; - -export default { - wrapper, - name: "node", - supportStreaming: true, -} satisfies Wrapper; +export { default } from "@opennextjs/core/overrides/wrappers/node.js"; +export * from "@opennextjs/core/overrides/wrappers/node.js"; diff --git a/packages/open-next/src/plugins/content-updater.ts b/packages/open-next/src/plugins/content-updater.ts index 06c55059..1a0aa220 100644 --- a/packages/open-next/src/plugins/content-updater.ts +++ b/packages/open-next/src/plugins/content-updater.ts @@ -1,97 +1 @@ -/** - * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content. - * - * The updater allows multiple plugins to update the content. - */ - -import { readFile } from "node:fs/promises"; - -import type { OnLoadArgs, OnLoadOptions, Plugin, PluginBuild } from "esbuild"; - -import type { BuildOptions } from "../build/helper"; -import { type Versions, isVersionInRange } from "../build/patch/codePatcher.js"; - -export type * from "esbuild"; - -/** - * The callbacks returns either an updated content or undefined if the content is unchanged. - */ -export type Callback = (args: { - contents: string; - path: string; -}) => string | undefined | Promise; - -/** - * The callback is called only when `contentFilter` matches the content. - * It can be used as a fast heuristic to prevent an expensive update. - */ -export type OnUpdateOptions = OnLoadOptions & { - contentFilter: RegExp; -}; - -export type Updater = OnUpdateOptions & { - callback: Callback; - // Restrict the patch to this Next version range - versions?: Versions; -}; - -export class ContentUpdater { - updaters = new Map(); - - constructor(private buildOptions: BuildOptions) {} - - /** - * Register a callback to update the file content. - * - * The callbacks are called in order of registration. - * - * @param name The name of the plugin (must be unique). - * @param updaters A list of code updaters - * @returns A noop ESBuild plugin. - */ - updateContent(name: string, updaters: Updater[]): Plugin { - if (this.updaters.has(name)) { - throw new Error(`Plugin "${name}" already registered`); - } - this.updaters.set( - name, - updaters.filter(({ versions }) => isVersionInRange(this.buildOptions.nextVersion, versions)) - ); - return { - name, - setup() {}, - }; - } - - /** - * Returns an ESBuild plugin applying the registered updates. - */ - get plugin() { - return { - name: "aggregate-on-load", - - setup: async (build: PluginBuild) => { - build.onLoad({ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, async (args: OnLoadArgs) => { - const updaters = Array.from(this.updaters.values()).flat(); - if (updaters.length === 0) { - return; - } - let contents = await readFile(args.path, "utf-8"); - for (const { filter, namespace, contentFilter, callback } of updaters) { - if (namespace !== undefined && args.namespace !== namespace) { - continue; - } - if (!args.path.match(filter)) { - continue; - } - if (!contents.match(contentFilter)) { - continue; - } - contents = (await callback({ contents, path: args.path })) ?? contents; - } - return { contents }; - }); - }, - }; - } -} +export * from "@opennextjs/core/plugins/content-updater.js"; diff --git a/packages/open-next/src/plugins/edge.ts b/packages/open-next/src/plugins/edge.ts index 41dca4fb..ef5e044f 100644 --- a/packages/open-next/src/plugins/edge.ts +++ b/packages/open-next/src/plugins/edge.ts @@ -1,205 +1 @@ -import { readFileSync } from "node:fs"; -import path from "node:path"; - -import chalk from "chalk"; -import type { Plugin } from "esbuild"; - -import type { MiddlewareInfo } from "@/types/next-types.js"; - -import { - loadAppPathRoutesManifest, - loadAppPathsManifest, - loadAppPathsManifestKeys, - loadBuildId, - loadConfig, - loadConfigHeaders, - loadFunctionsConfigManifest, - loadHtmlPages, - loadMiddlewareManifest, - loadPagesManifest, - loadPrerenderManifest, - loadRoutesManifest, -} from "../adapters/config/util.js"; -import logger from "../logger.js"; -import { normalizePath } from "../utils/normalize-path.js"; -import { getCrossPlatformPathRegex } from "../utils/regex.js"; - -export interface IPluginSettings { - nextDir: string; - middlewareInfo?: MiddlewareInfo; - isInCloudflare?: boolean; -} - -/** - * @param opts.nextDir - The path to the .next directory - * @param opts.middlewareInfo - Information about the middleware - * @param opts.isInCloudflare - Whether the code runs on the cloudflare runtime - * @returns - */ -export function openNextEdgePlugins({ nextDir, middlewareInfo, isInCloudflare }: IPluginSettings): Plugin { - const entryFiles = - middlewareInfo?.files.map((file: string) => normalizePath(path.join(nextDir, file))) ?? []; - const routes = middlewareInfo - ? [ - { - name: middlewareInfo.name || "/", - page: middlewareInfo.page, - regex: middlewareInfo.matchers.map((m) => m.regexp), - }, - ] - : []; - const wasmFiles = middlewareInfo?.wasm ?? []; - - return { - name: "opennext-edge", - setup(build) { - logger.debug(chalk.blue("OpenNext Edge plugin")); - - build.onResolve({ filter: /\.(mjs|wasm)$/ }, () => { - return { - external: true, - }; - }); - - //Copied from https://github.com/cloudflare/next-on-pages/blob/7a18efb5cab4d86c8e3e222fc94ea88ac05baffd/packages/next-on-pages/src/buildApplication/processVercelFunctions/build.ts#L86-L112 - - build.onResolve({ filter: /^node:/ }, ({ kind, path }) => { - // this plugin converts `require("node:*")` calls, those are the only ones that - // need updating (esm imports to "node:*" are totally valid), so here we tag with the - // node-buffer namespace only imports that are require calls - return kind === "require-call" ? { path, namespace: "node-built-in-modules" } : undefined; - }); - - // we convert the imports we tagged with the node-built-in-modules namespace so that instead of `require("node:*")` - // they import from `export * from "node:*";` - build.onLoad({ filter: /.*/, namespace: "node-built-in-modules" }, ({ path }) => ({ - contents: `export * from '${path}'`, - loader: "js", - })); - - // We inject the entry files into the edgeFunctionHandler - build.onLoad({ filter: getCrossPlatformPathRegex("/edgeFunctionHandler.js") }, async (args) => { - let contents = readFileSync(args.path, "utf-8"); - contents = ` -globalThis._ENTRIES = {}; -globalThis.self = globalThis; -globalThis._ROUTES = ${JSON.stringify(routes)}; - -${ - isInCloudflare - ? "" - : ` -import {readFileSync} from "node:fs"; -import path from "node:path"; -function addDuplexToInit(init) { - return typeof init === 'undefined' || - (typeof init === 'object' && init.duplex === undefined) - ? { duplex: 'half', ...init } - : init -} -// We need to override Request to add duplex to the init, it seems Next expects it to work like this -class OverrideRequest extends Request { - constructor(input, init) { - super(input, addDuplexToInit(init)) - } -} -globalThis.Request = OverrideRequest; - -// If we're not in cloudflare, we polyfill crypto -// https://github.com/vercel/edge-runtime/blob/main/packages/primitives/src/primitives/crypto.js -import { webcrypto } from 'node:crypto' -if(!globalThis.crypto){ - globalThis.crypto = new webcrypto.Crypto() -} -if(!globalThis.CryptoKey){ - globalThis.CryptoKey = webcrypto.CryptoKey -} -function SubtleCrypto() { - if (!(this instanceof SubtleCrypto)) return new SubtleCrypto() - throw TypeError('Illegal constructor') -} -if(!globalThis.SubtleCrypto) { - globalThis.SubtleCrypto = SubtleCrypto -} -if(!globalThis.Crypto) { - globalThis.Crypto = webcrypto.Crypto -} -// We also need to polyfill URLPattern -if (!globalThis.URLPattern) { - await import("urlpattern-polyfill"); -} -` -} -${importWasm(wasmFiles, { isInCloudflare })} -${entryFiles.map((file) => `require("${file}");`).join("\n")} -${contents} - `; - - return { - contents, - }; - }); - - build.onLoad({ filter: getCrossPlatformPathRegex("adapters/config/index") }, async () => { - const NextConfig = loadConfig(nextDir); - const BuildId = loadBuildId(nextDir); - const HtmlPages = loadHtmlPages(nextDir); - const RoutesManifest = loadRoutesManifest(nextDir); - const ConfigHeaders = loadConfigHeaders(nextDir); - const PrerenderManifest = loadPrerenderManifest(nextDir); - const AppPathsManifestKeys = loadAppPathsManifestKeys(nextDir); - const MiddlewareManifest = loadMiddlewareManifest(nextDir); - const AppPathsManifest = loadAppPathsManifest(nextDir); - const AppPathRoutesManifest = loadAppPathRoutesManifest(nextDir); - const FunctionsConfigManifest = loadFunctionsConfigManifest(nextDir); - const PagesManifest = loadPagesManifest(nextDir); - - const contents = ` - import path from "node:path"; - - import { debug } from "../logger"; - - globalThis.__dirname ??= ""; - - export const NEXT_DIR = path.join(__dirname, ".next"); - export const OPEN_NEXT_DIR = path.join(__dirname, ".open-next"); - - debug({ NEXT_DIR, OPEN_NEXT_DIR }); - - export const NextConfig = ${JSON.stringify(NextConfig)}; - export const BuildId = ${JSON.stringify(BuildId)}; - export const HtmlPages = ${JSON.stringify(HtmlPages)}; - export const RoutesManifest = ${JSON.stringify(RoutesManifest)}; - export const ConfigHeaders = ${JSON.stringify(ConfigHeaders)}; - export const PrerenderManifest = ${JSON.stringify(PrerenderManifest)}; - export const AppPathsManifestKeys = ${JSON.stringify(AppPathsManifestKeys)}; - export const MiddlewareManifest = ${JSON.stringify(MiddlewareManifest)}; - export const AppPathsManifest = ${JSON.stringify(AppPathsManifest)}; - export const AppPathRoutesManifest = ${JSON.stringify(AppPathRoutesManifest)}; - export const FunctionsConfigManifest = ${JSON.stringify(FunctionsConfigManifest)}; - export const PagesManifest = ${JSON.stringify(PagesManifest)}; - - process.env.NEXT_BUILD_ID = BuildId; - process.env.NEXT_PREVIEW_MODE_ID = PrerenderManifest?.preview?.previewModeId; -`; - return { contents }; - }); - }, - }; -} - -function importWasm(files: MiddlewareInfo["wasm"], { isInCloudflare }: { isInCloudflare?: boolean }) { - return files - .map(({ name }) => { - if (isInCloudflare) { - // As `.next/server/src/middleware.js` references the name, - // using `import ${name} from '...'` would cause ESBuild to rename the import. - // We use `globalThis.${name}` to make sure `middleware.js` reference name will match. - return `import __onw_${name}__ from './wasm/${name}.wasm' -globalThis.${name} = __onw_${name}__`; - } - - return `const ${name} = readFileSync(path.join(__dirname,'/wasm/${name}.wasm'));`; - }) - .join("\n"); -} +export * from "@opennextjs/core/plugins/edge.js"; diff --git a/packages/open-next/src/plugins/externalMiddleware.ts b/packages/open-next/src/plugins/externalMiddleware.ts index 9ad1b97a..b76638d1 100644 --- a/packages/open-next/src/plugins/externalMiddleware.ts +++ b/packages/open-next/src/plugins/externalMiddleware.ts @@ -1,15 +1 @@ -import type { Plugin } from "esbuild"; - -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -export function openNextExternalMiddlewarePlugin(functionPath: string): Plugin { - return { - name: "open-next-external-node-middleware", - setup(build) { - // If we bundle the routing, we need to resolve the middleware - build.onResolve({ filter: getCrossPlatformPathRegex("./middleware.mjs") }, () => ({ - path: functionPath, - })); - }, - }; -} +export * from "@opennextjs/core/plugins/externalMiddleware.js"; diff --git a/packages/open-next/src/plugins/inline-require-resolve.ts b/packages/open-next/src/plugins/inline-require-resolve.ts index 10c56485..762f8cad 100644 --- a/packages/open-next/src/plugins/inline-require-resolve.ts +++ b/packages/open-next/src/plugins/inline-require-resolve.ts @@ -1,33 +1 @@ -import fs from "node:fs"; -import { createRequire } from "node:module"; - -import type { Plugin } from "esbuild"; - -/** - * Inlines calls to `require.resolve` in JavaScript files. - * - * esbuild does not statically analyse `require.resolve` calls, and the polyfill - * does not include an implementation to handle them. This can be problematic - * if you attempt to dynamically import a file built by esbuild that unknowingly - * contains `require.resolve` calls, as they will throw an error during import. - */ -export const inlineRequireResolvePlugin: Plugin = { - name: "inline-require-resolve", - setup: (build) => { - build.onLoad({ filter: /\.(js|ts|mjs|cjs)$/ }, async (args) => { - const source = await fs.promises.readFile(args.path, "utf-8"); - const transformed = source.replace( - /require\.resolve\((?['"])((?:\\.|.)*?)\k\)/g, - (_, quote, modulePath) => { - try { - return JSON.stringify(createRequire(args.path).resolve(modulePath)); - } catch { - return `require.resolve(${quote}${modulePath}${quote})`; - } - } - ); - - return { contents: transformed, loader: "default" }; - }); - }, -}; +export * from "@opennextjs/core/plugins/inline-require-resolve.js"; diff --git a/packages/open-next/src/plugins/inlineRouteHandlers.ts b/packages/open-next/src/plugins/inlineRouteHandlers.ts index 4d676b8f..fc21a061 100644 --- a/packages/open-next/src/plugins/inlineRouteHandlers.ts +++ b/packages/open-next/src/plugins/inlineRouteHandlers.ts @@ -1,123 +1 @@ -import { getCrossPlatformPathRegex } from "@/utils/regex.js"; - -import type { NextAdapterOutputs } from "../adapter.js"; -import { patchCode } from "../build/patch/astCodePatcher.js"; - -import type { ContentUpdater, Plugin } from "./content-updater.js"; - -export function inlineRouteHandler( - updater: ContentUpdater, - outputs: NextAdapterOutputs, - packagePath: string -): Plugin { - console.log("## inlineRouteHandler"); - return updater.updateContent("inlineRouteHandler", [ - // This one will inline the route handlers into the adapterHandler's getHandler function. - { - filter: getCrossPlatformPathRegex(String.raw`core/routing/adapterHandler\.js$`, { - escape: false, - }), - contentFilter: /getHandler/, - callback: ({ contents }) => patchCode(contents, inlineRule(outputs)), - }, - // For turbopack, we need to also patch the `[turbopack]_runtime.js` file. - { - filter: getCrossPlatformPathRegex(String.raw`\[turbopack\]_runtime\.js$`, { - escape: false, - }), - contentFilter: /loadRuntimeChunkPath/, - callback: ({ contents }) => { - const result = patchCode(contents, inlineChunksRule); - //TODO: Maybe find another way to do that. - return `${result}\n${inlineChunksFn(outputs, packagePath)}`; - }, - }, - ]); -} - -function inlineRule(outputs: NextAdapterOutputs) { - const routeToHandlerPath: Record = {}; - - for (const type of ["pages", "pagesApi", "appPages", "appRoutes"] as const) { - for (const { pathname, filePath } of outputs[type]) { - routeToHandlerPath[pathname] = filePath; - } - } - - return ` -rule: - pattern: "function getHandler($ROUTE) { $$$BODY }" -fix: |- - function getHandler($ROUTE) { - switch($ROUTE.route) { -${Object.entries(routeToHandlerPath) - .map(([route, file]) => ` case "${route}": return require("${file}");`) - .join("\n")} - default: - throw new Error(\`Not found \${$ROUTE.route}\`); - } - - }`; -} - -//TODO: Make this one more resilient to code changes -const inlineChunksRule = ` -rule: - kind: call_expression - pattern: require(resolved) -fix: - requireChunk(chunkPath) -`; - -function getInlinableChunks(outputs: NextAdapterOutputs, packagePath: string, prefix?: string) { - const chunks = new Set(); - // TODO: handle middleware - for (const type of ["pages", "pagesApi", "appPages", "appRoutes"] as const) { - for (const { assets } of outputs[type]) { - for (let asset of Object.keys(assets)) { - if (asset.includes(".next/server/chunks/") && !asset.includes("[turbopack]_runtime.js")) { - asset = packagePath !== "" ? asset.replace(`${packagePath}/`, "") : asset; - chunks.add(prefix ? `${prefix}${asset}` : asset); - } - } - } - } - return chunks; -} - -function inlineChunksFn(outputs: NextAdapterOutputs, packagePath: string) { - // From the outputs, we extract every chunks - const chunks = getInlinableChunks(outputs, packagePath); - return ` - function requireChunk(chunk) { - const chunkPath = ".next/" + chunk; - switch(chunkPath) { -${Array.from(chunks) - .map((chunk) => ` case "${chunk}": return require("./${chunk}");`) - .join("\n")} - default: - throw new Error(\`Not found \${chunkPath}\`); - } - } -`; -} - -/** - * Esbuild plugin to mark all chunks that we inline as external. - */ -export function externalChunksPlugin(outputs: NextAdapterOutputs, packagePath: string): Plugin { - const chunks = getInlinableChunks(outputs, packagePath, "./"); - return { - name: "external-chunks", - setup(build) { - build.onResolve({ filter: /\/chunks\// }, (args) => { - if (chunks.has(args.path)) { - return { - path: args.path, - external: true, - }; - } - }); - }, - }; -} +export * from "@opennextjs/core/plugins/inlineRouteHandlers.js"; diff --git a/packages/open-next/src/plugins/replacement.ts b/packages/open-next/src/plugins/replacement.ts index 79b01543..c16d8a40 100644 --- a/packages/open-next/src/plugins/replacement.ts +++ b/packages/open-next/src/plugins/replacement.ts @@ -1,110 +1 @@ -import { readFile } from "node:fs/promises"; - -import chalk from "chalk"; -import type { Plugin } from "esbuild"; - -import logger from "../logger.js"; - -export interface IPluginSettings { - target: RegExp; - replacements?: string[]; - deletes?: string[]; - name?: string; - entireFile?: boolean; -} - -const overridePattern = /\/\/#override (\w+)\n([\s\S]*?)\n\/\/#endOverride/gm; -const importPattern = /\/\/#import([\s\S]*?)\n\/\/#endImport/gm; - -/** - * - * openNextPlugin({ - * target: /plugins(\/|\\)default\.js/g, - * replacements: [require.resolve("./plugins/default.js")], - * deletes: ["id1"], - * }) - * - * To inject arbitrary code by using (import at top of file): - * - * //#import - * - * import data from 'data' - * const datum = data.datum - * - * //#endImport - * - * To replace code: - * - * //#override id1 - * - * export function overrideMe() { - * // I will replace the "id1" block in the target file - * } - * - * //#endOverride - * - * - * @param opts.target - the target file to replace - * @param opts.replacements - list of files used to replace the imports/overrides in the target - * - the path is absolute - * @param opts.deletes - list of ids to delete from the target - * @param opts.entireFile - whether to replace the entire file or just specific blocks. - * @param opts.name - name of the plugin - * @returns - */ -export function openNextReplacementPlugin({ - target, - replacements, - deletes, - name, - entireFile, -}: IPluginSettings): Plugin { - return { - name: name ?? "opennext", - setup(build) { - build.onLoad({ filter: target }, async (args) => { - if (entireFile) { - if (replacements?.length !== 1) { - throw new Error("When using entireFile option, exactly one replacement file must be provided"); - } - const contents = await readFile(replacements[0], "utf-8"); - return { contents }; - } - - let contents = await readFile(args.path, "utf-8"); - - await Promise.all([ - ...(deletes ?? []).map(async (id) => { - const pattern = new RegExp(`//#override (${id})\n([\\s\\S]*?)//#endOverride`); - logger.debug(chalk.blue(`OpenNext Replacement plugin ${name}`), `Delete override ${id}`); - contents = contents.replace(pattern, ""); - }), - ...(replacements ?? []).map(async (filename) => { - const replacementFile = await readFile(filename, "utf-8"); - const matches = replacementFile.matchAll(overridePattern); - - const importMatch = replacementFile.match(importPattern); - const addedImport = importMatch ? importMatch[0] : ""; - - contents = `${addedImport}\n${contents}`; - - for (const match of matches) { - const replacement = match[2]; - const id = match[1]; - const pattern = new RegExp(`//#override (${id})\n([\\s\\S]*?)//#endOverride`, "g"); - logger.debug( - chalk.blue(`Open-next replacement plugin ${name}`), - `Apply override ${id} from ${filename}` - ); - contents = contents.replace(pattern, replacement); - } - }), - ]); - - return { - contents, - }; - }); - }, - }; -} +export * from "@opennextjs/core/plugins/replacement.js"; diff --git a/packages/open-next/src/plugins/resolve.ts b/packages/open-next/src/plugins/resolve.ts index 717836c8..a8e4bbaa 100644 --- a/packages/open-next/src/plugins/resolve.ts +++ b/packages/open-next/src/plugins/resolve.ts @@ -1,107 +1 @@ -import { readFile } from "node:fs/promises"; - -import chalk from "chalk"; -import type { Plugin } from "esbuild"; - -import type { - BaseOverride, - DefaultOverrideOptions, - IncludedImageLoader, - IncludedOriginResolver, - IncludedWarmer, - LazyLoadedOverride, - OverrideOptions, -} from "@/types/open-next"; -import type { ImageLoader, OriginResolver, Warmer } from "@/types/overrides"; - -import logger from "../logger.js"; -import { getCrossPlatformPathRegex } from "../utils/regex.js"; - -export interface IPluginSettings { - overrides?: { - // oxlint-disable-next-line @typescript-eslint/no-explicit-any - generic overrides for flexibility - wrapper?: DefaultOverrideOptions["wrapper"]; - // oxlint-disable-next-line @typescript-eslint/no-explicit-any - generic overrides for flexibility - converter?: DefaultOverrideOptions["converter"]; - tagCache?: OverrideOptions["tagCache"]; - queue?: OverrideOptions["queue"]; - incrementalCache?: OverrideOptions["incrementalCache"]; - imageLoader?: LazyLoadedOverride | IncludedImageLoader; - originResolver?: LazyLoadedOverride | IncludedOriginResolver; - warmer?: LazyLoadedOverride | IncludedWarmer; - proxyExternalRequest?: OverrideOptions["proxyExternalRequest"]; - cdnInvalidation?: OverrideOptions["cdnInvalidation"]; - }; - fnName?: string; -} - -function getOverrideOrDummy>(override: Override) { - if (typeof override === "string") { - return override; - } - // We can return dummy here because if it's not a string, it's a LazyLoadedOverride - return "dummy"; -} - -// This could be useful in the future to map overrides to nested folders -const nameToFolder = { - wrapper: "wrappers", - converter: "converters", - tagCache: "tagCache", - queue: "queue", - incrementalCache: "incrementalCache", - imageLoader: "imageLoader", - originResolver: "originResolver", - warmer: "warmer", - proxyExternalRequest: "proxyExternalRequest", - cdnInvalidation: "cdnInvalidation", -}; - -const defaultOverrides = { - wrapper: "aws-lambda", - converter: "aws-apigw-v2", - tagCache: "dynamodb", - queue: "sqs", - incrementalCache: "s3", - imageLoader: "s3", - originResolver: "pattern-env", - warmer: "aws-lambda", - proxyExternalRequest: "node", - cdnInvalidation: "dummy", -}; - -/** - * @param opts.overrides - The name of the overrides to use - * @returns - */ -export function openNextResolvePlugin({ overrides, fnName }: IPluginSettings): Plugin { - return { - name: "opennext-resolve", - setup(build) { - logger.debug(chalk.blue("OpenNext Resolve plugin"), fnName ? `for ${fnName}` : ""); - build.onLoad({ filter: getCrossPlatformPathRegex("core/resolve.js") }, async (args) => { - let contents = await readFile(args.path, "utf-8"); - const overridesEntries = Object.entries(overrides ?? {}); - for (let [overrideName, overrideValue] of overridesEntries) { - if (!overrideValue) { - continue; - } - if (overrideName === "wrapper" && overrideValue === "cloudflare") { - // "cloudflare" is deprecated and replaced by "cloudflare-edge". - overrideValue = "cloudflare-edge"; - } - const folder = nameToFolder[overrideName as keyof typeof nameToFolder]; - const defaultOverride = defaultOverrides[overrideName as keyof typeof defaultOverrides]; - - contents = contents.replace( - `../overrides/${folder}/${defaultOverride}.js`, - `../overrides/${folder}/${getOverrideOrDummy(overrideValue)}.js` - ); - } - return { - contents, - }; - }); - }, - }; -} +export * from "@opennextjs/core/plugins/resolve.js"; diff --git a/packages/open-next/src/types/cache.ts b/packages/open-next/src/types/cache.ts index db5b63f1..2ca89304 100644 --- a/packages/open-next/src/types/cache.ts +++ b/packages/open-next/src/types/cache.ts @@ -1,175 +1 @@ -import type { ReadableStream } from "node:stream/web"; - -interface CachedFetchValue { - kind: "FETCH"; - data: { - headers: { [k: string]: string }; - body: string; - url: string; - status?: number; - tags?: string[]; - }; - revalidate: number; -} - -interface CachedRedirectValue { - kind: "REDIRECT"; - props: object; -} - -interface CachedRouteValue { - kind: "ROUTE" | "APP_ROUTE"; - // this needs to be a RenderResult so since renderResponse - // expects that type instead of a string - body: Buffer; - status: number; - headers: Record; -} - -interface CachedImageValue { - kind: "IMAGE"; - etag: string; - buffer: Buffer; - extension: string; - isMiss?: boolean; - isStale?: boolean; -} - -interface IncrementalCachedPageValue { - kind: "PAGE" | "PAGES"; - // this needs to be a string since the cache expects to store - // the string value - html: string; - pageData: object; - status?: number; - headers?: Record; -} - -interface IncrementalCachedAppPageValue { - kind: "APP_PAGE"; - // this needs to be a string since the cache expects to store - // the string value - html: string; - rscData: Buffer; - headers?: Record; - postponed?: string; - status?: number; - segmentData?: Map; -} - -export type IncrementalCacheValue = - | CachedRedirectValue - | IncrementalCachedPageValue - | IncrementalCachedAppPageValue - | CachedImageValue - | CachedFetchValue - | CachedRouteValue; - -export interface CacheHandlerContext { - fs?: never; - dev?: boolean; - flushToDisk?: boolean; - serverDistDir?: string; - maxMemoryCacheSize?: number; - _appDir: boolean; - _requestHeaders: never; - fetchCacheKeyPrefix?: string; -} -export interface CacheHandlerValue { - lastModified?: number; - age?: number; - cacheState?: string; - value: IncrementalCacheValue | null; -} - -export type Extension = "cache" | "fetch" | "composable"; - -type MetaHeaders = { - "x-next-cache-tags"?: string; - [k: string]: string | string[] | undefined; -}; - -export interface Meta { - status?: number; - headers?: MetaHeaders; - postponed?: string; -} - -export type TagCacheMetaFile = { - tag: { S: string }; - path: { S: string }; - revalidatedAt: { N: string }; -}; - -// Cache context since vercel/next.js#76207 -interface SetIncrementalFetchCacheContext { - fetchCache: true; - fetchUrl?: string; - fetchIdx?: number; - tags?: string[]; -} - -interface SetIncrementalResponseCacheContext { - fetchCache?: false; - cacheControl?: { - revalidate: number | false; - expire?: number; - }; - - /** - * True if the route is enabled for PPR. - */ - isRoutePPREnabled?: boolean; - - /** - * True if this is a fallback request. - */ - isFallback?: boolean; -} - -// Before vercel/next.js#76207 revalidate was passed this way -interface SetIncrementalCacheContext { - revalidate?: number | false; - isRoutePPREnabled?: boolean; - isFallback?: boolean; -} - -// Before vercel/next.js#53321 context on set was just the revalidate -type OldSetIncrementalCacheContext = number | false | undefined; - -export type IncrementalCacheContext = - | OldSetIncrementalCacheContext - | SetIncrementalCacheContext - | SetIncrementalFetchCacheContext - | SetIncrementalResponseCacheContext; - -export interface ComposableCacheEntry { - value: ReadableStream; - tags: string[]; - stale: number; - timestamp: number; - expire: number; - revalidate: number; -} - -export type StoredComposableCacheEntry = Omit & { - value: string; -}; - -export interface ComposableCacheHandler { - get(cacheKey: string): Promise; - set(cacheKey: string, pendingEntry: Promise): Promise; - refreshTags(): Promise; - /** - * Next 16 takes an array of tags instead of variadic arguments - */ - getExpiration(...tags: string[] | string[][]): Promise; - /** - * Removed from Next.js 16 - */ - expireTags(...tags: string[]): Promise; - /** - * This function is only there for older versions and do nothing - */ - receiveExpiredTags(...tags: string[]): Promise; -} +export * from "@opennextjs/core/types/cache.js"; diff --git a/packages/open-next/src/types/global.ts b/packages/open-next/src/types/global.ts index 289fe144..9f91bc12 100644 --- a/packages/open-next/src/types/global.ts +++ b/packages/open-next/src/types/global.ts @@ -1,234 +1 @@ -import type { AsyncLocalStorage, AsyncLocalStorage as NodeAsyncLocalStorage } from "node:async_hooks"; -import type { OutgoingHttpHeaders } from "node:http"; - -import type { - AssetResolver, - CDNInvalidationHandler, - IncrementalCache, - ProxyExternalRequest, - Queue, - TagCache, -} from "@/types/overrides"; - -import type { DetachedPromiseRunner } from "../utils/promise"; - -import type { i18nConfig } from "./next-types.js"; -import type { OpenNextConfig, WaitUntil } from "./open-next"; - -export interface RequestData { - geo?: { - city?: string; - country?: string; - region?: string; - latitude?: string; - longitude?: string; - }; - headers: OutgoingHttpHeaders; - ip?: string; - method: string; - nextConfig?: { - basePath?: string; - i18n?: i18nConfig; - trailingSlash?: boolean; - }; - page?: { - name?: string; - params?: { [key: string]: string | string[] }; - }; - url: string; - body?: ReadableStream; - signal: AbortSignal; -} - -interface Entry { - default: (props: { page: string; request: RequestData }) => Promise<{ - response: Response; - waitUntil: Promise; - }>; -} - -interface Entries { - [k: string]: Entry | Promise; -} - -export interface EdgeRoute { - name: string; - page: string; - regex: string[]; -} - -interface OpenNextRequestContext { - // Unique ID for the request. - requestId: string; - pendingPromiseRunner: DetachedPromiseRunner; - isISRRevalidation?: boolean; - mergeHeadersPriority?: "middleware" | "handler"; - // Last modified time of the page (used in main functions, only available for ISR/SSG). - lastModified?: number; - waitUntil?: WaitUntil; - /** We use this to deduplicate write of the tags*/ - writtenTags: Set; -} - -declare global { - // Needed in the cache adapter - /** - * The cache adapter for incremental static regeneration. - * Only available in main functions and in the external middleware when `enableCacheInterception` is `true`. - * Defined in `createMainHandler` and in `adapters/middleware.ts`. - */ - var incrementalCache: IncrementalCache; - - /** - * The cache adapter for the tag cache. - * Only available in main functions and in the external middleware when `enableCacheInterception` is `true`. - * Defined in `createMainHandler` and in `adapters/middleware.ts`. - */ - var tagCache: TagCache; - - /** - * The queue that is used to handle ISR revalidation requests. - * Only available in main functions and in the external middleware when `enableCacheInterception` is `true`. - * Defined in `createMainHandler` and in `adapters/middleware.ts`. - */ - var queue: Queue; - - /** - * A boolean that indicates if the DynamoDB cache is disabled. - * @deprecated This will be removed, use `globalThis.openNextConfig.dangerous?.disableTagCache` instead. - * Defined in esbuild banner for the cache adapter. - */ - var disableDynamoDBCache: boolean; - - /** - * A boolean that indicates if the incremental cache is disabled. - * @deprecated This will be removed, use `globalThis.openNextConfig.dangerous?.disableIncrementalCache` instead. - * Defined in esbuild banner for the cache adapter. - */ - var disableIncrementalCache: boolean; - - /** - * A boolean that indicates if the runtime is Edge. - * Only available in `edge` runtime functions (i.e. external middleware or function with edge runtime). - * Defined in `adapters/edge-adapter.ts`. - */ - var isEdgeRuntime: true; - - /** - * A boolean that indicates if we are running in debug mode. - * Available in all functions. - * Defined in the esbuild banner. - */ - var openNextDebug: boolean; - - /** - * The fetch function that should be used to make requests during the execution of the function. - * Used to bypass Next intercepting and caching the fetch calls. Only available in main functions. - * Defined in `adapters/server-adapter.ts` and in `adapters/middleware.ts`. - */ - var internalFetch: typeof fetch; - - /** - * The Open Next configuration object. - * Available in all functions. - * Defined in `createMainHandler` and in the `createGenericHandler`. - */ - var openNextConfig: Partial; - - /** - * The name of the function that is currently being executed. - * Only available in main functions. - * Defined in `createMainHandler`. - */ - var fnName: string | undefined; - /** - * The unique identifier of the server. - * Only available in main functions. - * Defined in `createMainHandler`. - */ - var serverId: string; - - /** - * The AsyncLocalStorage instance that is used to store the request context. - * Only available in main, middleware and edge functions. - * Defined in `requestHandler.ts`, `adapters/middleware.ts` and `adapters/edge-adapter.ts`. - */ - var __openNextAls: AsyncLocalStorage; - - /** - * The entries object that contains the functions that are available in the function. - * Only available in edge runtime functions. - * Defined in the esbuild edge plugin. - */ - var _ENTRIES: Entries; - - /** - * The routes object that contains the routes that are available in the function. - * Only available in edge runtime functions. - * Defined in the esbuild edge plugin. - */ - var _ROUTES: EdgeRoute[]; - - /** - * A map that is used in the edge runtime. - * Only available in edge runtime functions. - */ - var __storage__: Map; - - /** - * AsyncContext available globally in the edge runtime. - * Only available in edge runtime functions. - */ - var AsyncContext: unknown; - - /** - * AsyncLocalStorage available globally in the edge runtime. - * Only available in edge runtime functions. - * Defined in createEdgeBundle. - */ - var AsyncLocalStorage: typeof NodeAsyncLocalStorage; - - /** - * The version of the Open Next runtime. - * Available everywhere. - * Defined in the esbuild banner. - */ - var openNextVersion: string; - - /** - * The function that is used when resolving external rewrite requests. - * Only available in main functions - * Defined in `createMainHandler`. - */ - var proxyExternalRequest: ProxyExternalRequest; - - /** - * The function that will be called when the CDN needs invalidating (either from `revalidateTag` or from `res.revalidate`) - * Available in main functions - * Defined in `createMainHandler` - */ - var cdnInvalidationHandler: CDNInvalidationHandler; - - /** - * The function called to resolve assets. - * Available in main functions - * Defined in `createMainHandler` when the middleware is internal - */ - var assetResolver: AssetResolver | undefined; - - /** - * A function to preload the routes. - * This needs to be defined on globalThis because it can be used by custom overrides. - * Only available in main functions. - * TODO: Disabled for now, we'll need to revisit this later if needed. - */ - // var __next_route_preloader: ( - // stage: "waitUntil" | "start" | "warmerEvent" | "onDemand", - // ) => Promise; - - /** - * This is the relative package path of the monorepo. It will be an empty string "" in normal repos. - * ex. `packages/web` - */ - var monorepoPackagePath: string; -} +export * from "@opennextjs/core/types/global.js"; diff --git a/packages/open-next/src/types/next-types.ts b/packages/open-next/src/types/next-types.ts index 3815707b..97b81b80 100644 --- a/packages/open-next/src/types/next-types.ts +++ b/packages/open-next/src/types/next-types.ts @@ -1,211 +1 @@ -// NOTE: add more next config typings as they become relevant - -import type { IncomingMessage, OpenNextNodeResponse } from "@/http/index.js"; - -import type { InternalEvent } from "./open-next"; - -type RemotePattern = { - protocol?: "http" | "https"; - hostname: string; - port?: string; - pathname?: string; -}; -declare type ImageFormat = "image/avif" | "image/webp"; - -type ImageConfigComplete = { - deviceSizes: number[]; - imageSizes: number[]; - path: string; - loaderFile: string; - domains: string[]; - disableStaticImages: boolean; - minimumCacheTTL: number; - formats: ImageFormat[]; - dangerouslyAllowSVG: boolean; - contentSecurityPolicy: string; - contentDispositionType: "inline" | "attachment"; - remotePatterns: RemotePattern[]; - unoptimized: boolean; -}; -type ImageConfig = Partial; - -export type RouteHas = - | { - type: "header" | "query" | "cookie"; - key: string; - value?: string; - } - | { - type: "host"; - key?: undefined; - value: string; - }; -export type Rewrite = { - source: string; - destination: string; - basePath?: false; - locale?: false; - has?: RouteHas[]; - missing?: RouteHas[]; -}; -export type Header = { - source: string; - regex: string; - basePath?: false; - locale?: false; - headers: Array<{ - key: string; - value: string; - }>; - has?: RouteHas[]; - missing?: RouteHas[]; -}; - -export interface DomainLocale { - defaultLocale: string; - domain: string; - http?: true; - locales: readonly string[]; -} - -export interface i18nConfig { - locales: string[]; - defaultLocale: string; - domains?: DomainLocale[]; - localeDetection?: false; -} -export interface NextConfig { - basePath?: string; - trailingSlash?: boolean; - skipTrailingSlashRedirect?: boolean; - i18n?: i18nConfig; - experimental: { - serverActions?: boolean; - appDir?: boolean; - optimizeCss?: boolean; - }; - images: ImageConfig; - poweredByHeader?: boolean; - serverExternalPackages?: string[]; - deploymentId?: string; -} - -export interface RouteDefinition { - page: string; - regex: string; -} - -export interface DataRouteDefinition { - page: string; - dataRouteRegex: string; - routeKeys?: string; -} - -export interface RewriteDefinition { - source: string; - destination: string; - has?: RouteHas[]; - missing?: RouteHas[]; - regex: string; - locale?: false; -} - -export interface RedirectDefinition extends RewriteDefinition { - internal?: boolean; - statusCode?: number; -} - -export interface RoutesManifest { - basePath?: string; - dynamicRoutes: RouteDefinition[]; - staticRoutes: RouteDefinition[]; - dataRoutes: DataRouteDefinition[]; - rewrites: - | { - beforeFiles: RewriteDefinition[]; - afterFiles: RewriteDefinition[]; - fallback: RewriteDefinition[]; - } - | RewriteDefinition[]; - redirects: RedirectDefinition[]; - headers?: Header[]; - i18n?: { - locales: string[]; - }; -} - -export interface MiddlewareInfo { - files: string[]; - paths?: string[]; - name: string; - page: string; - matchers: { - regexp: string; - originalSource: string; - }[]; - wasm: { - filePath: string; - name: string; - }[]; - assets: { - filePath: string; - name: string; - }[]; -} - -export interface MiddlewareManifest { - sortedMiddleware: string[]; - middleware: { - [key: string]: MiddlewareInfo; - }; - functions: { [key: string]: MiddlewareInfo }; - version: number; -} - -export interface PrerenderManifest { - routes: Record< - string, - { - // TODO: add the rest when needed for PPR - initialRevalidateSeconds: number | false; - } - >; - dynamicRoutes: { - [route: string]: { - routeRegex: string; - fallback: string | false | null; - dataRouteRegex: string; - }; - }; - preview: { - previewModeId: string; - }; -} - -export type Options = { - internalEvent: InternalEvent; - buildId: string; - isExternalRewrite?: boolean; -}; -export type PluginHandler = ( - req: IncomingMessage, - res: OpenNextNodeResponse, - options: Options -) => Promise; - -export interface FunctionsConfigManifest { - version: number; - functions: Record< - string, - { - maxDuration?: number | undefined; - runtime?: "nodejs"; - matchers?: Array<{ - regexp: string; - originalSource: string; - has?: Rewrite["has"]; - missing?: Rewrite["has"]; - }>; - } - >; -} +export * from "@opennextjs/core/types/next-types.js"; diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index 8216e491..2f4b49cb 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -1,524 +1 @@ -import type { Writable } from "node:stream"; -import type { ReadableStream } from "node:stream/web"; - -import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function"; - -import type { - AssetResolver, - CDNInvalidationHandler, - Converter, - ImageLoader, - IncrementalCache, - OriginResolver, - ProxyExternalRequest, - Queue, - TagCache, - Warmer, - Wrapper, -} from "./overrides"; - -export type BaseEventOrResult = { - type: T; -}; - -export type InternalEvent = { - readonly method: string; - readonly rawPath: string; - // Full URL - starts with "https://on/" when the host is not available - readonly url: string; - readonly body?: Buffer; - //TODO: change the type of headers to Record - readonly headers: Record; - readonly query: Record; - readonly cookies: Record; - readonly remoteAddress: string; -} & BaseEventOrResult<"core">; - -export type MiddlewareEvent = InternalEvent & { - responseHeaders?: Record; - isExternalRewrite?: boolean; - rewriteStatusCode?: number; -}; - -export type InternalResult = { - statusCode: number; - headers: Record; - body: ReadableStream; - isBase64Encoded: boolean; - rewriteStatusCode?: number; -} & BaseEventOrResult<"core">; - -/** - * This event is returned by the cache interceptor and the routing handler. - * It is then handled by either the external middleware or the classic request handler. - * This is designed for PPR support inside the cache interceptor. - */ -export type PartialResult = { - /** - * Resume request that will be forwarded to the handler - */ - resumeRequest: InternalEvent; - /** - * The result that was generated so far by the cache interceptor - * It contains the first part of the body that we'll need to forward to the client immediately - * As well as the headers and status code - */ - result: InternalResult; -}; - -export interface StreamCreator { - writeHeaders(prelude: { statusCode: number; cookies: string[]; headers: Record }): Writable; - // Just to fix an issue with aws lambda streaming with empty body - onWrite?: () => void; - onFinish?: (length: number) => void; - abortSignal?: AbortSignal; - /** - * Normally there is no need to retain the chunks that have been pushed to the response stream. - * - * However some implementations use a fake `StreamCreator` and expect the chunks to be retained. - * When your stream controller implementation doesn't need to retain the chunk, you can set this - * to `false` to reduce memory usage. - * - * @see https://github.com/opennextjs/opennextjs-aws/blob/main/packages/open-next/src/overrides/wrappers/aws-lambda.ts - * - * @default true for backward compatibility. - */ - retainChunks?: boolean; -} - -export type WaitUntil = (promise: Promise) => void; -export interface DangerousOptions { - /** - * The tag cache is used for revalidateTags and revalidatePath. - * @default false - */ - disableTagCache?: boolean; - /** - * The incremental cache is used for ISR and SSG. - * Disable this only if you use only SSR - * @default false - */ - disableIncrementalCache?: boolean; - /** - * Function to determine which headers or cookies takes precedence. - * By default, the middleware headers and cookies will override the handler headers and cookies. - * This is executed for every request and after next config headers and middleware has executed. - */ - headersAndCookiesPriority?: (event: InternalEvent) => "middleware" | "handler"; - - /** - * Configuration option to prioritize headers set via middleware over headers set via the option in the Next config. - * - * The default will change to 'true' in v4. - * - * See also {@link https://nextjs.org/docs/app/api-reference/file-conventions/middleware#execution-order} - * - * @default false - */ - middlewareHeadersOverrideNextConfigHeaders?: boolean; -} - -export type BaseOverride = { - name: string; -}; -export type LazyLoadedOverride = () => T | Promise; - -export interface Origin { - host: string; - protocol: "http" | "https"; - port?: number; - customHeaders?: Record; -} - -export type IncludedWrapper = - | "aws-lambda" - | "aws-lambda-streaming" - | "aws-lambda-compressed" - | "node" - // @deprecated - use "cloudflare-edge" instead. - | "cloudflare" - | "cloudflare-edge" - | "cloudflare-node" - | "express-dev" - | "dummy"; - -export type IncludedConverter = - | "aws-apigw-v2" - | "aws-apigw-v1" - | "aws-cloudfront" - | "edge" - | "node" - | "sqs-revalidate" - | "dummy"; - -export type RouteType = "route" | "page" | "app"; - -export interface ResolvedRoute { - route: string; - type: RouteType; - /** - * Indicates if the route is a prerendered dynamic fallback route. - * They shouldn't be used to serve the request directly. - */ - isFallback: boolean; -} - -/** - * The route preloading behavior. Only supported in Next 15+. - * Default behavior of Next is disabled. You should do your own testing to choose which one suits you best - * - "none" - No preloading of the route at all. This is the default - * - "withWaitUntil" - Preload the route using the waitUntil provided by the wrapper - If not supported, it will fallback to "none". At the moment only cloudflare wrappers provide a `waitUntil` - * - "onWarmerEvent" - Preload the route on the warmer event - Needs to be implemented by the wrapper. Only supported in `aws-lambda-streaming` wrapper for now - * - "onStart" - Preload the route before even invoking the wrapper - This is a blocking operation. The handler will only be created after all the routes have been loaded, it may increase the cold start time by a lot in some cases. Useful for long running server or in serverless with some careful testing - * @default "none" - */ -export type RoutePreloadingBehavior = "none" | "withWaitUntil" | "onWarmerEvent" | "onStart"; - -export interface RoutingResult { - internalEvent: InternalEvent; - // If the request is an external rewrite, if used with an external middleware will be false on every server function - isExternalRewrite: boolean; - // Origin is only used in external middleware, will be false on every server function - origin: Origin | false; - // If the request is for an ISR route, will be false on every server function. Only used in external middleware - isISR: boolean; - // The initial URL of the request before applying rewrites, if used with an external middleware will be defined in x-opennext-initial-url header - initialURL: string; - - // The locale of the request, if used with an external middleware will be defined in x-opennext-locale header - locale?: string; - - // The resolved route after applying rewrites, if used with an external middleware will be defined in x-opennext-resolved-routes header as a json encoded array - resolvedRoutes: ResolvedRoute[]; - // The status code applied to a middleware rewrite - rewriteStatusCode?: number; - - /** - * This is the response generated when using PPR in the cache interceptor. - * It contains the initial part of the response that should be sent to the client immediately. - * Can only be present when using cache interception and no external middleware. - */ - initialResponse?: InternalResult; -} - -export interface MiddlewareResult extends RoutingResult, BaseEventOrResult<"middleware"> {} - -export type IncludedQueue = "sqs" | "sqs-lite" | "direct" | "dummy"; - -export type IncludedIncrementalCache = "s3" | "s3-lite" | "multi-tier-ddb-s3" | "fs-dev" | "dummy"; - -export type IncludedTagCache = - | "dynamodb" - | "dynamodb-lite" - | "dynamodb-nextMode" - | "fs-dev" - | "fs-dev-nextMode" - | "dummy"; - -export type IncludedImageLoader = "s3" | "s3-lite" | "host" | "fs-dev" | "dummy"; - -export type IncludedOriginResolver = "pattern-env" | "dummy"; - -export type IncludedWarmer = "aws-lambda" | "dummy"; - -export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy"; - -export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy"; - -export type IncludedAssetResolver = "dummy"; - -export interface DefaultOverrideOptions< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, -> { - /** - * This is the main entrypoint of your app. - * @default "aws-lambda" - */ - wrapper?: IncludedWrapper | LazyLoadedOverride>; - - /** - * This code convert the event to InternalEvent and InternalResult to the expected output. - * @default "aws-apigw-v2" - */ - converter?: IncludedConverter | LazyLoadedOverride>; - /** - * Generate a basic dockerfile to deploy the app. - * If a string is provided, it will be used as the base dockerfile. - * @default false - */ - generateDockerfile?: boolean | string; -} - -export interface OverrideOptions extends DefaultOverrideOptions { - /** - * Add possibility to override the default s3 cache. Used for fetch cache and html/rsc/json cache. - * @default "s3" - */ - incrementalCache?: IncludedIncrementalCache | LazyLoadedOverride; - - /** - * Add possibility to override the default tag cache. Used for revalidateTags and revalidatePath. - * @default "dynamodb" - */ - tagCache?: IncludedTagCache | LazyLoadedOverride; - - /** - * Add possibility to override the default queue. Used for isr. - * @default "sqs" - */ - queue?: IncludedQueue | LazyLoadedOverride; - - /** - * Add possibility to override the default proxy for external rewrite - * @default "node" - */ - proxyExternalRequest?: IncludedProxyExternalRequest | LazyLoadedOverride; - - /** - * Add possibility to override the default cdn invalidation for On Demand Revalidation - * @default "dummy" - */ - cdnInvalidation?: IncludedCDNInvalidationHandler | LazyLoadedOverride; -} - -export interface InstallOptions { - /** - * List of packages to install - * @example - * ```ts - * install: { - * packages: ["sharp@0.32"] - * } - * ``` - */ - packages: string[]; - /** @default undefined */ - arch?: "x64" | "arm64"; - /** @default undefined */ - nodeVersion?: string; - /** @default undefined */ - libc?: "glibc" | "musl"; - /** @default "linux" */ - os?: string; - - /** - * @default undefined - * Additional arguments to pass to the install command (i.e. npm install) - */ - additionalArgs?: string; -} - -export interface DefaultFunctionOptions< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, -> { - /** - * Minify the server bundle. - * @default false - */ - minify?: boolean; - /** - * Print debug information. - * @default false - */ - debug?: boolean; - /** - * Enable overriding the default lambda. - */ - override?: DefaultOverrideOptions; - - /** - * Install options for the function. - * This is used to install additional packages to this function. - * For image optimization, it will install sharp by default. - * @default undefined - */ - install?: InstallOptions; -} - -export interface FunctionOptions extends DefaultFunctionOptions { - /** - * Runtime used - * @default "node" - */ - runtime?: "node" | "edge" | "deno"; - /** - * @default "regional" - */ - placement?: "regional" | "global"; - /** - * Enable overriding the default lambda. - */ - override?: OverrideOptions; - - routePreloadingBehavior?: RoutePreloadingBehavior; -} - -export type RouteTemplate = - | `app/${string}/route` - | `app/${string}/page` - | `app/page` - | `app/route` - | `pages/${string}`; - -export interface SplittedFunctionOptions extends FunctionOptions { - /** - * Here you should specify all the routes you want to use. - * For app routes, you should use the `app/${name}/route` format or `app/${name}/page` for pages. - * For pages, you should use the `page/${name}` format. - * @example - * ```ts - * routes: ["app/api/test/route", "app/page", "pages/admin"] - * ``` - */ - routes: RouteTemplate[]; - - /** - * Cloudfront compatible patterns. - * i.e. /api/* - * @default [] - */ - patterns: string[]; -} - -/** - * MiddlewareConfig that applies to both external and internal middlewares - * - * Note: this type is internal and included in both `ExternalMiddlewareConfig` and `InternalMiddlewareConfig` - */ -type CommonMiddlewareConfig = { - /** - * The assetResolver is used to resolve assets in the routing layer. - * - * @default "dummy" - */ - assetResolver?: IncludedAssetResolver | LazyLoadedOverride; -}; - -/** MiddlewareConfig that applies to external middlewares only */ -export type ExternalMiddlewareConfig = DefaultFunctionOptions & - CommonMiddlewareConfig & { - external: true; - /** - * The runtime used by next for the middleware. - * @default "edge" - */ - runtime?: "node" | "edge"; - - /** - * The override options for the middleware. - * By default the lite override are used (.i.e. s3-lite, dynamodb-lite, sqs-lite) - * @default undefined - */ - override?: OverrideOptions; - - /** - * Origin resolver is used to resolve the origin for internal rewrite. - * By default, it uses the pattern-env origin resolver. - * Pattern env uses pattern set in split function options and an env variable OPEN_NEXT_ORIGIN - * OPEN_NEXT_ORIGIN should be a json stringified object with the key of the splitted function as key and the origin as value - * @default "pattern-env" - */ - originResolver?: IncludedOriginResolver | LazyLoadedOverride; - }; - -/** MiddlewareConfig that applies to internal middlewares only */ -export type InternalMiddlewareConfig = { - external: false; -} & CommonMiddlewareConfig; - -export interface OpenNextConfig { - default: FunctionOptions; - functions?: Record; - - /** - * Override the default middleware - * When `external` is true, the middleware need to be deployed separately. - * It supports both edge and node runtime. - * @default undefined - Which is equivalent to `external: false` - */ - middleware?: ExternalMiddlewareConfig | InternalMiddlewareConfig; - - /** - * Override the default warmer - * By default, works for lambda only. - * If you override this, you'll need to handle the warmer event in the wrapper - * @default undefined - */ - warmer?: DefaultFunctionOptions & { - invokeFunction?: IncludedWarmer | LazyLoadedOverride; - }; - - /** - * Override the default revalidate function - * By default, works for lambda and on SQS event. - * Supports only node runtime - */ - revalidate?: DefaultFunctionOptions< - { host: string; url: string; type: "revalidate" }, - { type: "revalidate" } - >; - - /** - * Override the default revalidate function - * By default, works on lambda and for S3 key. - * Supports only node runtime - */ - imageOptimization?: DefaultFunctionOptions & { - /** - * The image loader is used to load the image from the source. - * @default "s3" - */ - loader?: IncludedImageLoader | LazyLoadedOverride; - }; - - /** - * Override the default initialization function - * By default, works for lambda and on SQS event. - * Supports only node runtime - */ - initializationFunction?: DefaultFunctionOptions & { - tagCache?: IncludedTagCache | LazyLoadedOverride; - }; - - /** - * Dangerous options. This break some functionnality but can be useful in some cases. - */ - dangerous?: DangerousOptions; - /** - * The command to build the Next.js app. - * @default `npm run build`, `yarn build`, or `pnpm build` based on the lock file found in the app's directory or any of its parent directories. - * @example - * ```ts - * build({ - * buildCommand: "pnpm custom:build", - * }); - * ``` - */ - buildCommand?: string; - /** - * The path to the target folder of build output from the `buildCommand` option (the path which will contain the `.next` and `.open-next` folders). This path is relative from the current process.cwd(). - * @default "." - */ - buildOutputPath?: string; - /** - * The path to the root of the Next.js app's source code. This path is relative from the current process.cwd(). - * @default "." - */ - appPath?: string; - /** - * The path to the package.json file of the Next.js app. This path is relative from the current process.cwd(). - * @default "." - */ - packageJsonPath?: string; - /** - * **Advanced usage** - * If you use the edge runtime somewhere (either in the middleware or in the functions), we compile 2 versions of the open-next.config.ts file. - * One for the node runtime and one for the edge runtime. - * This option allows you to specify the externals for the edge runtime used in esbuild for the compilation of open-next.config.ts - * It is especially useful if you use some custom overrides only in node - * @default [] - */ - edgeExternals?: string[]; -} +export * from "@opennextjs/core/types/open-next.js"; diff --git a/packages/open-next/src/types/overrides.ts b/packages/open-next/src/types/overrides.ts index 84589959..a6574921 100644 --- a/packages/open-next/src/types/overrides.ts +++ b/packages/open-next/src/types/overrides.ts @@ -1,264 +1 @@ -import type { Readable } from "node:stream"; - -import type { Extension, Meta, StoredComposableCacheEntry } from "@/types/cache"; - -import type { - BaseEventOrResult, - BaseOverride, - InternalEvent, - InternalResult, - Origin, - ResolvedRoute, - StreamCreator, - WaitUntil, -} from "./open-next"; - -// Queue - -export interface QueueMessage { - MessageDeduplicationId: string; - MessageBody: { - host: string; - url: string; - lastModified: number; - eTag: string; - }; - MessageGroupId: string; -} - -export interface Queue { - send(message: QueueMessage): Promise; - name: string; -} - -/** - * Resolves assets in the routing layer. - */ -export interface AssetResolver { - name: string; - - /** - * Called by the routing layer to check for a matching static asset. - * - * @param event - * @returns an `InternalResult` when an asset is found a the path from the event, undefined otherwise. - */ - maybeGetAssetResult?: (event: InternalEvent) => Promise | undefined; -} - -// Incremental cache - -export type CachedFile = - | { - type: "redirect"; - props?: object; - meta?: Meta; - } - | { - type: "page"; - html: string; - json: object; - meta?: Meta; - } - | { - type: "app"; - html: string; - rsc: string; - meta?: Meta; - segmentData?: Record; - } - | { - type: "route"; - body: string; - meta?: Meta; - }; - -// type taken from: https://github.com/vercel/next.js/blob/9a1cd356/packages/next/src/server/response-cache/types.ts#L26-L38 -export type CachedFetchValue = { - kind: "FETCH"; - data: { - headers: { [k: string]: string }; - body: string; - url: string; - status?: number; - // field used by older versions of Next.js (see: https://github.com/vercel/next.js/blob/fda1ecc/packages/next/src/server/response-cache/types.ts#L23) - tags?: string[]; - }; - // tags are only present with file-system-cache - // fetch cache stores tags outside of cache entry - tags?: string[]; -}; - -export type WithLastModified = { - lastModified?: number; - value?: T; - /** - * If set to true, we will not check the tag cache for this entry. - * `revalidateTag` and `revalidatePath` may not work as expected. - */ - shouldBypassTagCache?: boolean; -}; - -export type CacheEntryType = Extension; - -export type CacheValue = (CacheType extends "fetch" - ? CachedFetchValue - : CacheType extends "cache" - ? CachedFile - : StoredComposableCacheEntry) & { - /** - * This is available for page cache entry, but only at runtime. - */ - revalidate?: number | false; -}; - -export type IncrementalCache = { - get( - key: string, - cacheType?: CacheType - ): Promise> | null>; - set( - key: string, - value: CacheValue, - isFetch?: CacheType - ): Promise; - delete(key: string): Promise; - name: string; -}; - -// Tag cache - -type BaseTagCache = { - name: string; -}; - -/** - * On get : -We have to check for every tag (after reading the incremental cache) that they have not been revalidated. - -In DynamoDB, this would require 1 GetItem per tag (including internal one), more realistically 1 BatchGetItem per get (In terms of pricing, it would be billed as multiple single GetItem) - -On set : -We don't have to do anything here - -On revalidateTag for each tag : -We have to update a single entry for this tag - -Pros : -- No need to prepopulate DDB -- Very little write - -Cons : -- Might be slower on read -- One page request (i.e. GET request) could require to check a lot of tags (And some of them multiple time when used with the fetch cache) -- Almost impossible to do automatic cdn revalidation by itself -*/ -export type NextModeTagCache = BaseTagCache & { - mode: "nextMode"; - // Necessary for the composable cache - getLastRevalidated(tags: string[]): Promise; - hasBeenRevalidated(tags: string[], lastModified?: number): Promise; - writeTags(tags: string[]): Promise; - // Optional method to get paths by tags - // It is used to automatically invalidate paths in the CDN - getPathsByTags?: (tags: string[]) => Promise; -}; - -export interface OriginalTagCacheWriteInput { - tag: string; - path: string; - revalidatedAt?: number; -} - -/** - * On get : -We just check for the cache key in the tag cache. If it has been revalidated we just return null, otherwise we continue - -On set : -We have to write both the incremental cache and check the tag cache for non existing tag/key combination. For non existing tag/key combination, we have to add them - -On revalidateTag for each tag : -We have to update every possible combination for the requested tag - -Pros : -- Very fast on read -- Only one query per get (On DynamoDB it's a lot cheaper) -- Can allow for automatic cdn invalidation on revalidateTag - -Cons : -- Lots of write on set and revalidateTag -- Needs to be prepopulated at build time to work properly - */ -export type OriginalTagCache = BaseTagCache & { - mode?: "original"; - getByTag(tag: string): Promise; - getByPath(path: string): Promise; - getLastModified(path: string, lastModified?: number): Promise; - writeTags(tags: OriginalTagCacheWriteInput[]): Promise; -}; - -export type TagCache = NextModeTagCache | OriginalTagCache; - -export type WrapperHandler< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, -> = (handler: OpenNextHandler, converter: Converter) => Promise<(...args: unknown[]) => unknown>; - -export type Wrapper< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, -> = BaseOverride & { - wrapper: WrapperHandler; - supportStreaming: boolean; - edgeRuntime?: boolean; -}; - -export type OpenNextHandlerOptions = { - // Create a `Writeable` for streaming responses. - streamCreator?: StreamCreator; - // Extends the liftetime of the runtime after the response is returned. - waitUntil?: WaitUntil; -}; - -export type OpenNextHandler< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, -> = (event: E, options?: OpenNextHandlerOptions) => Promise; - -export type Converter< - E extends BaseEventOrResult = InternalEvent, - R extends BaseEventOrResult = InternalResult, -> = BaseOverride & { - convertFrom: (event: unknown) => Promise; - convertTo: (result: R, originalRequest?: unknown) => Promise; -}; - -export type Warmer = BaseOverride & { - invoke: (warmerId: string) => Promise; -}; - -export type ImageLoader = BaseOverride & { - load: (url: string) => Promise<{ - body?: Readable; - contentType?: string; - cacheControl?: string; - }>; -}; - -export type OriginResolver = BaseOverride & { - resolve: (path: string) => Promise; -}; - -export type ProxyExternalRequest = BaseOverride & { - proxy: (event: InternalEvent) => Promise; -}; - -type CDNPath = { - initialPath: string; - rawPath: string; - resolvedRoutes: ResolvedRoute[]; -}; - -export type CDNInvalidationHandler = BaseOverride & { - invalidatePaths: (paths: CDNPath[]) => Promise; -}; +export * from "@opennextjs/core/types/overrides.js"; diff --git a/packages/open-next/src/utils/binary.ts b/packages/open-next/src/utils/binary.ts index 6852e535..d06adf6b 100644 --- a/packages/open-next/src/utils/binary.ts +++ b/packages/open-next/src/utils/binary.ts @@ -1,67 +1 @@ -const commonBinaryMimeTypes = new Set([ - "application/octet-stream", - // Docs - "application/epub+zip", - "application/msword", - "application/pdf", - "application/rtf", - "application/vnd.amazon.ebook", - "application/vnd.ms-excel", - "application/vnd.ms-powerpoint", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - // Fonts - "font/otf", - "font/woff", - "font/woff2", - // Images - "image/bmp", - "image/gif", - "image/jpeg", - "image/png", - "image/tiff", - "image/vnd.microsoft.icon", - "image/webp", - // Audio - "audio/3gpp", - "audio/aac", - "audio/basic", - "audio/flac", - "audio/mpeg", - "audio/ogg", - "audio/wavaudio/webm", - "audio/x-aiff", - "audio/x-midi", - "audio/x-wav", - // Video - "video/3gpp", - "video/mp2t", - "video/mpeg", - "video/ogg", - "video/quicktime", - "video/webm", - "video/x-msvideo", - // Archives - "application/java-archive", - "application/vnd.apple.installer+xml", - "application/x-7z-compressed", - "application/x-apple-diskimage", - "application/x-bzip", - "application/x-bzip2", - "application/x-gzip", - "application/x-java-archive", - "application/x-rar-compressed", - "application/x-tar", - "application/x-zip", - "application/zip", - // Serialized data - "application/x-protobuf", -]); - -export function isBinaryContentType(contentType?: string | null) { - if (!contentType) return false; - - const value = contentType.split(";")[0]; - return commonBinaryMimeTypes.has(value); -} +export * from "@opennextjs/core/utils/binary.js"; diff --git a/packages/open-next/src/utils/cache.ts b/packages/open-next/src/utils/cache.ts index 090bc247..3fc74eb0 100644 --- a/packages/open-next/src/utils/cache.ts +++ b/packages/open-next/src/utils/cache.ts @@ -1,82 +1 @@ -import type { - CacheEntryType, - CacheValue, - OriginalTagCacheWriteInput, - WithLastModified, -} from "@/types/overrides"; - -import { debug } from "../adapters/logger"; - -export async function hasBeenRevalidated( - key: string, - tags: string[], - cacheEntry: WithLastModified> -): Promise { - if (globalThis.openNextConfig.dangerous?.disableTagCache) { - return false; - } - const value = cacheEntry.value; - if (!value) { - // We should never reach this point - return true; - } - if ("type" in cacheEntry && cacheEntry.type === "page") { - return false; - } - const lastModified = cacheEntry.lastModified ?? Date.now(); - if (globalThis.tagCache.mode === "nextMode") { - return tags.length === 0 ? false : await globalThis.tagCache.hasBeenRevalidated(tags, lastModified); - } - // TODO: refactor this, we should introduce a new method in the tagCache interface so that both implementations use hasBeenRevalidated - const _lastModified = await globalThis.tagCache.getLastModified(key, lastModified); - return _lastModified === -1; -} - -export function getTagsFromValue(value?: CacheValue<"cache">) { - if (!value) { - return []; - } - // The try catch is necessary for older version of next.js that may fail on this - try { - const cacheTags = value.meta?.headers?.["x-next-cache-tags"]?.split(",") ?? []; - delete value.meta?.headers?.["x-next-cache-tags"]; - return cacheTags; - } catch (e) { - return []; - } -} - -function getTagKey(tag: string | OriginalTagCacheWriteInput): string { - if (typeof tag === "string") { - return tag; - } - return JSON.stringify({ - tag: tag.tag, - path: tag.path, - }); -} - -export async function writeTags(tags: (string | OriginalTagCacheWriteInput)[]): Promise { - const store = globalThis.__openNextAls.getStore(); - debug("Writing tags", tags, store); - if (!store || globalThis.openNextConfig.dangerous?.disableTagCache) { - return; - } - const tagsToWrite = tags.filter((t) => { - const tagKey = getTagKey(t); - const shouldWrite = !store.writtenTags.has(tagKey); - // We preemptively add the tag to the writtenTags set - // to avoid writing the same tag multiple times in the same request - if (shouldWrite) { - store.writtenTags.add(tagKey); - } - return shouldWrite; - }); - if (tagsToWrite.length === 0) { - return; - } - - // Here we know that we have the correct type - // oxlint-disable-next-line @typescript-eslint/no-explicit-any - writeTags accepts a union type that typescript cannot infer correctly - await globalThis.tagCache.writeTags(tagsToWrite as any); -} +export * from "@opennextjs/core/utils/cache.js"; diff --git a/packages/open-next/src/utils/error.ts b/packages/open-next/src/utils/error.ts index da22733b..d10fd395 100644 --- a/packages/open-next/src/utils/error.ts +++ b/packages/open-next/src/utils/error.ts @@ -1,49 +1 @@ -export interface BaseOpenNextError { - readonly __openNextInternal: true; - readonly canIgnore: boolean; - // 0 - debug, 1 - warn, 2 - error - readonly logLevel: 0 | 1 | 2; -} - -// This is an error that can be totally ignored -// It don't even need to be logged, or only in debug mode -export class IgnorableError extends Error implements BaseOpenNextError { - readonly __openNextInternal = true; - readonly canIgnore = true; - readonly logLevel = 0; - constructor(message: string) { - super(message); - this.name = "IgnorableError"; - } -} - -// This is an error that can be recovered from -// It should be logged but the process can continue -export class RecoverableError extends Error implements BaseOpenNextError { - readonly __openNextInternal = true; - readonly canIgnore = true; - readonly logLevel = 1; - constructor(message: string) { - super(message); - this.name = "RecoverableError"; - } -} - -// We should not continue the process if this error is thrown -export class FatalError extends Error implements BaseOpenNextError { - readonly __openNextInternal = true; - readonly canIgnore = false; - readonly logLevel = 2; - constructor(message: string) { - super(message); - this.name = "FatalError"; - } -} - -export function isOpenNextError(e: unknown): e is BaseOpenNextError & Error { - try { - return e !== null && typeof e === "object" && "__openNextInternal" in e; - } catch { - return false; - } -} +export * from "@opennextjs/core/utils/error.js"; diff --git a/packages/open-next/src/utils/lru.ts b/packages/open-next/src/utils/lru.ts index f1bf8f3f..c9e3b215 100644 --- a/packages/open-next/src/utils/lru.ts +++ b/packages/open-next/src/utils/lru.ts @@ -1,30 +1 @@ -export class LRUCache { - private cache: Map = new Map(); - - constructor(private maxSize: number) {} - - get(key: string) { - const result = this.cache.get(key); - // We could have used .has to allow for nullish value to be stored but we don't need that right now - if (result) { - // By removing and setting the key again we ensure it's the most recently used - this.cache.delete(key); - this.cache.set(key, result); - } - return result; - } - - set(key: string, value: T) { - if (this.cache.size >= this.maxSize) { - const firstKey = this.cache.keys().next().value; - if (firstKey !== undefined) { - this.cache.delete(firstKey); - } - } - this.cache.set(key, value); - } - - delete(key: string) { - this.cache.delete(key); - } -} +export * from "@opennextjs/core/utils/lru.js"; diff --git a/packages/open-next/src/utils/normalize-path.ts b/packages/open-next/src/utils/normalize-path.ts index d8d0c5db..faebc369 100644 --- a/packages/open-next/src/utils/normalize-path.ts +++ b/packages/open-next/src/utils/normalize-path.ts @@ -1,22 +1 @@ -import path from "node:path"; - -export function normalizePath(path: string) { - return path.replace(/\\/g, "/"); -} - -// See: https://github.com/vercel/next.js/blob/3ecf087f10fdfba4426daa02b459387bc9c3c54f/packages/next/src/shared/lib/utils.ts#L348 -export function normalizeRepeatedSlashes(url: URL) { - const urlNoQuery = url.host + url.pathname; - return `${url.protocol}//${urlNoQuery.replace(/\\/g, "/").replace(/\/\/+/g, "/")}${url.search}`; -} - -export function getMonorepoRelativePath(relativePath = "../.."): string { - return path.join( - globalThis.monorepoPackagePath - .split("/") - .filter(Boolean) - .map(() => "..") - .join("/"), - relativePath - ); -} +export * from "@opennextjs/core/utils/normalize-path.js"; diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts index 00602ce0..60ddba68 100644 --- a/packages/open-next/src/utils/promise.ts +++ b/packages/open-next/src/utils/promise.ts @@ -1,137 +1 @@ -import type { WaitUntil } from "@/types/open-next"; - -import { debug, error } from "../adapters/logger"; - -/** - * A `Promise.withResolvers` implementation that exposes the `resolve` and - * `reject` functions on a `Promise`. - * Copied from next https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/detached-promise.ts - * @see https://tc39.es/proposal-promise-with-resolvers/ - */ -export class DetachedPromise { - public readonly resolve: (value: T | PromiseLike) => void; - public readonly reject: (reason: unknown) => void; - public readonly promise: Promise; - - constructor() { - let resolve: (value: T | PromiseLike) => void; - let reject: (reason: unknown) => void; - - // Create the promise and assign the resolvers to the object. - this.promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - // We know that resolvers is defined because the Promise constructor runs - // synchronously. - this.resolve = resolve!; - this.reject = reject!; - } -} - -export class DetachedPromiseRunner { - private promises: DetachedPromise[] = []; - - public withResolvers(): DetachedPromise { - const detachedPromise = new DetachedPromise(); - this.promises.push(detachedPromise as DetachedPromise); - return detachedPromise; - } - - public add(promise: Promise): void { - const detachedPromise = new DetachedPromise(); - this.promises.push(detachedPromise as DetachedPromise); - promise.then(detachedPromise.resolve).catch((e) => { - // We just want to log the error here to avoid unhandled promise rejections - error("Detached promise rejected:", e); - // @ts-expect-error - We want to resolve to avoid hanging indefinitely, we don't reject to avoid unhandled promise rejections since we already log the error - detachedPromise.resolve(undefined); // Resolve to avoid unhandled promise rejection, we already log the error above - }); - } - - public async await(): Promise { - debug(`Awaiting ${this.promises.length} detached promises`); - const results = await Promise.allSettled(this.promises.map((p) => p.promise)); - const rejectedPromises = results.filter((r) => r.status === "rejected") as PromiseRejectedResult[]; - rejectedPromises.forEach((r) => { - error(r.reason); - }); - } -} - -async function awaitAllDetachedPromise() { - const store = globalThis.__openNextAls.getStore(); - - const promisesToAwait = store?.pendingPromiseRunner.await() ?? Promise.resolve(); - if (store?.waitUntil) { - store.waitUntil(promisesToAwait); - return; - } - await promisesToAwait; -} - -function provideNextAfterProvider() { - const NEXT_REQUEST_CONTEXT_SYMBOL = Symbol.for("@next/request-context"); - - // This is needed by some lib that relies on the vercel request context to properly await stuff. - // Remove this when vercel builder is updated to provide '@next/request-context'. - const VERCEL_REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context"); - - const store = globalThis.__openNextAls.getStore(); - - const waitUntil = - store?.waitUntil ?? ((promise: Promise) => store?.pendingPromiseRunner.add(promise)); - - const nextAfterContext = { - get: () => ({ - waitUntil, - }), - }; - - //@ts-expect-error - globalThis[NEXT_REQUEST_CONTEXT_SYMBOL] = nextAfterContext; - // We probably want to avoid providing this everytime since some lib may incorrectly think they are running in Vercel - // It may break stuff, but at the same time it will allow libs like `@vercel/otel` to work as expected - if (process.env.EMULATE_VERCEL_REQUEST_CONTEXT) { - //@ts-expect-error - globalThis[VERCEL_REQUEST_CONTEXT_SYMBOL] = nextAfterContext; - } -} - -export function runWithOpenNextRequestContext( - { - isISRRevalidation, - waitUntil, - requestId = Math.random().toString(36), - }: { - // Whether we are in ISR revalidation - isISRRevalidation: boolean; - // Extends the liftetime of the runtime after the response is returned. - waitUntil?: WaitUntil; - requestId?: string; - }, - fn: () => Promise -): Promise { - return globalThis.__openNextAls.run( - { - requestId, - pendingPromiseRunner: new DetachedPromiseRunner(), - isISRRevalidation, - waitUntil, - writtenTags: new Set(), - }, - async () => { - provideNextAfterProvider(); - let result: T; - try { - result = await fn(); - // We always await all detached promises before returning the result - // However we don't want to catch errors here, we want to let the parent handle it - } finally { - await awaitAllDetachedPromise(); - } - return result; - } - ); -} +export * from "@opennextjs/core/utils/promise.js"; diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts index dec0a905..51111cac 100644 --- a/packages/open-next/src/utils/regex.ts +++ b/packages/open-next/src/utils/regex.ts @@ -1,28 +1 @@ -type Options = { - escape?: boolean; - flags?: string; -}; - -/** - * Constructs a regular expression for a path that supports separators for multiple platforms - * - Uses posix separators (`/`) as the input that should be made cross-platform. - * - Special characters are escaped by default but can be controlled through opts.escape. - * - Posix separators are always escaped. - * - * @example - * ```ts - * getCrossPlatformPathRegex("./middleware.mjs") - * getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false }) - * ``` - */ -export function getCrossPlatformPathRegex( - regex: string, - { escape: shouldEscape = true, flags = "" }: Options = {} -) { - const newExpr = (shouldEscape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex).replaceAll( - "/", - String.raw`(?:\/|\\)` - ); - - return new RegExp(newExpr, flags); -} +export * from "@opennextjs/core/utils/regex.js"; diff --git a/packages/open-next/src/utils/safe-json-parse.ts b/packages/open-next/src/utils/safe-json-parse.ts index cd9064ea..2a569ea5 100644 --- a/packages/open-next/src/utils/safe-json-parse.ts +++ b/packages/open-next/src/utils/safe-json-parse.ts @@ -1,10 +1 @@ -import logger from "../logger.js"; - -export function safeParseJsonFile(input: string, filePath: string, fallback?: T): T | undefined { - try { - return JSON.parse(input); - } catch (err) { - logger.warn(`Failed to parse JSON file "${filePath}". Error: ${(err as Error).message}`); - return fallback; - } -} +export * from "@opennextjs/core/utils/safe-json-parse.js"; diff --git a/packages/open-next/src/utils/stream.ts b/packages/open-next/src/utils/stream.ts index 66bd5234..b85355fb 100644 --- a/packages/open-next/src/utils/stream.ts +++ b/packages/open-next/src/utils/stream.ts @@ -1,67 +1 @@ -import { ReadableStream } from "node:stream/web"; - -export async function fromReadableStream( - stream: ReadableStream, - base64?: boolean -): Promise { - const chunks: Uint8Array[] = []; - let totalLength = 0; - - for await (const chunk of stream) { - chunks.push(chunk); - totalLength += chunk.length; - } - - if (chunks.length === 0) { - return ""; - } - - if (chunks.length === 1) { - return Buffer.from(chunks[0]).toString(base64 ? "base64" : "utf8"); - } - - // Pre-allocate buffer with exact size to avoid reallocation - const buffer = Buffer.alloc(totalLength); - let offset = 0; - for (const chunk of chunks) { - buffer.set(chunk, offset); - offset += chunk.length; - } - - return buffer.toString(base64 ? "base64" : "utf8"); -} - -export function toReadableStream(value: string | Buffer, isBase64?: boolean): ReadableStream { - return new ReadableStream( - { - pull(controller) { - // Defer the Buffer.from conversion to when the stream is actually read. - controller.enqueue(Buffer.isBuffer(value) ? value : Buffer.from(value, isBase64 ? "base64" : "utf8")); - controller.close(); - }, - }, - { highWaterMark: 0 } - ); -} - -let maybeSomethingBuffer: Buffer | undefined; - -export function emptyReadableStream(): ReadableStream { - if (process.env.OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE === "true") { - return new ReadableStream( - { - pull(controller) { - maybeSomethingBuffer ??= Buffer.from("SOMETHING"); - controller.enqueue(maybeSomethingBuffer); - controller.close(); - }, - }, - { highWaterMark: 0 } - ); - } - return new ReadableStream({ - start(controller) { - controller.close(); - }, - }); -} +export * from "@opennextjs/core/utils/stream.js"; diff --git a/packages/tests-unit/package.json b/packages/tests-unit/package.json index 10b2d437..58e9d6f9 100644 --- a/packages/tests-unit/package.json +++ b/packages/tests-unit/package.json @@ -9,7 +9,8 @@ "test": "vitest run" }, "dependencies": { - "@opennextjs/aws": "workspace:*" + "@opennextjs/aws": "workspace:*", + "@opennextjs/core": "workspace:*" }, "devDependencies": { "@types/testing-library__jest-dom": "^5.14.9", diff --git a/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts index 3137b000..38298f43 100644 --- a/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts +++ b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts @@ -4,7 +4,7 @@ import converter from "@opennextjs/aws/overrides/converters/aws-apigw-v2.js"; import type { APIGatewayProxyEventV2 } from "aws-lambda"; import { vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({})); +vi.mock("@/config/index.js", () => ({})); describe("convertTo", () => { it("Should parse the headers", async () => { diff --git a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts index 6c18e956..806d0b2a 100644 --- a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts +++ b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts @@ -4,7 +4,7 @@ import converter from "@opennextjs/aws/overrides/converters/aws-cloudfront.js"; import type { CloudFrontRequestEvent, CloudFrontRequestResult } from "aws-lambda"; import { vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({})); +vi.mock("@/config/index.js", () => ({})); describe("convertTo", () => { it("Should parse the headers", async () => { diff --git a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts index 2b79831c..d05749b6 100644 --- a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts +++ b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts @@ -5,7 +5,7 @@ import type { Queue } from "@opennextjs/aws/types/overrides.js"; import { fromReadableStream } from "@opennextjs/aws/utils/stream.js"; import { vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ +vi.mock("@/config/index.js", () => ({ NextConfig: {}, PrerenderManifest: { routes: { diff --git a/packages/tests-unit/tests/core/routing/i18n.test.ts b/packages/tests-unit/tests/core/routing/i18n.test.ts index d9b93083..fc61bd20 100644 --- a/packages/tests-unit/tests/core/routing/i18n.test.ts +++ b/packages/tests-unit/tests/core/routing/i18n.test.ts @@ -1,10 +1,10 @@ -import { NextConfig } from "@opennextjs/aws/adapters/config/index.js"; +import { NextConfig } from "@/config/index.js"; import { handleLocaleRedirect, localizePath } from "@opennextjs/aws/core/routing/i18n/index.js"; import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js"; import type { InternalEvent } from "@opennextjs/aws/types/open-next.js"; import { expect, vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => { +vi.mock("@/config/index.js", () => { return { NextConfig: { i18n: { diff --git a/packages/tests-unit/tests/core/routing/matcher.test.ts b/packages/tests-unit/tests/core/routing/matcher.test.ts index dce38517..afb337eb 100644 --- a/packages/tests-unit/tests/core/routing/matcher.test.ts +++ b/packages/tests-unit/tests/core/routing/matcher.test.ts @@ -1,4 +1,4 @@ -import { NextConfig } from "@opennextjs/aws/adapters/config/index.js"; +import { NextConfig } from "@/config/index.js"; import { fixDataPage, getNextConfigHeaders, @@ -9,7 +9,7 @@ import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js"; import type { InternalEvent } from "@opennextjs/aws/types/open-next.js"; import { vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ +vi.mock("@/config/index.js", () => ({ NextConfig: {}, PrerenderManifest: { routes: {}, diff --git a/packages/tests-unit/tests/core/routing/middleware.test.ts b/packages/tests-unit/tests/core/routing/middleware.test.ts index d389d5bd..1e4edb7e 100644 --- a/packages/tests-unit/tests/core/routing/middleware.test.ts +++ b/packages/tests-unit/tests/core/routing/middleware.test.ts @@ -4,7 +4,7 @@ import type { InternalEvent } from "@opennextjs/aws/types/open-next.js"; import { toReadableStream } from "@opennextjs/aws/utils/stream.js"; import { vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ +vi.mock("@/config/index.js", () => ({ NextConfig: {}, MiddlewareManifest: { sortedMiddleware: ["/"], diff --git a/packages/tests-unit/tests/core/routing/routeMatcher.test.ts b/packages/tests-unit/tests/core/routing/routeMatcher.test.ts index 907afb5f..2c6d8e86 100644 --- a/packages/tests-unit/tests/core/routing/routeMatcher.test.ts +++ b/packages/tests-unit/tests/core/routing/routeMatcher.test.ts @@ -1,7 +1,7 @@ import { dynamicRouteMatcher, staticRouteMatcher } from "@opennextjs/aws/core/routing/routeMatcher.js"; import { vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ +vi.mock("@/config/index.js", () => ({ PrerenderManifest: { routes: {}, dynamicRoutes: { diff --git a/packages/tests-unit/tests/core/routing/util.test.ts b/packages/tests-unit/tests/core/routing/util.test.ts index 910e6a11..d9f73696 100644 --- a/packages/tests-unit/tests/core/routing/util.test.ts +++ b/packages/tests-unit/tests/core/routing/util.test.ts @@ -1,5 +1,5 @@ -import * as config from "@opennextjs/aws/adapters/config/index.js"; -import { NextConfig } from "@opennextjs/aws/adapters/config/index.js"; +import * as config from "@/config/index.js"; +import { NextConfig } from "@/config/index.js"; import { addOpenNextHeader, constructNextUrl, @@ -23,7 +23,7 @@ import { import { fromReadableStream } from "@opennextjs/aws/utils/stream.js"; import { vi } from "vitest"; -vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({ +vi.mock("@/config/index.js", () => ({ NextConfig: { basePath: "", }, diff --git a/packages/tests-unit/tsconfig.json b/packages/tests-unit/tsconfig.json index 40510629..1f6ef435 100644 --- a/packages/tests-unit/tsconfig.json +++ b/packages/tests-unit/tsconfig.json @@ -22,7 +22,12 @@ "target": "ES2022", "sourceMap": true, "paths": { - "@opennextjs/aws/*": ["../open-next/src/*"] + "@opennextjs/aws/*": ["../open-next/src/*"], + "@opennextjs/core/*": ["../core/src/*"], + "@/types/*": ["../core/src/types/*"], + "@/config/*": ["../core/src/adapters/config/*"], + "@/http/*": ["../core/src/http/*"], + "@/utils/*": ["../core/src/utils/*"] } }, "include": ["."], diff --git a/packages/tests-unit/vitest.config.ts b/packages/tests-unit/vitest.config.ts index c84ccfe9..451ae153 100644 --- a/packages/tests-unit/vitest.config.ts +++ b/packages/tests-unit/vitest.config.ts @@ -1,9 +1,17 @@ +import path from "node:path"; + import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vitest/config"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [tsconfigPaths()], + resolve: { + alias: { + "@opennextjs/core": path.resolve(__dirname, "../core/src"), + "@opennextjs/aws": path.resolve(__dirname, "../open-next/src"), + }, + }, test: { globals: true, environment: "node", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 191fab7a..2cb4d3a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1272,6 +1272,9 @@ importers: '@opennextjs/aws': specifier: workspace:* version: link:../open-next + '@opennextjs/core': + specifier: workspace:* + version: link:../core cloudflare: specifier: ^4.4.1 version: 4.5.0 @@ -1343,6 +1346,61 @@ importers: specifier: 'catalog:' version: 2.1.3(@edge-runtime/vm@3.2.0)(@types/node@22.19.7)(jsdom@22.1.0)(lightningcss@1.30.2)(terser@5.16.9) + packages/core: + dependencies: + '@ast-grep/napi': + specifier: ^0.40.5 + version: 0.40.5 + '@node-minify/core': + specifier: ^8.0.6 + version: 8.0.6 + '@node-minify/terser': + specifier: ^8.0.6 + version: 8.0.6 + '@tsconfig/node18': + specifier: ^1.0.3 + version: 1.0.3 + chalk: + specifier: ^5.6.2 + version: 5.6.2 + cookie: + specifier: ^1.0.2 + version: 1.0.2 + esbuild: + specifier: catalog:aws + version: 0.27.0 + express: + specifier: ^5.2.1 + version: 5.2.1 + next: + specifier: ^16.0.10 + version: 16.1.4(@opentelemetry/api@1.9.0)(@playwright/test@1.58.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + path-to-regexp: + specifier: ^6.3.0 + version: 6.3.0 + urlpattern-polyfill: + specifier: ^10.1.0 + version: 10.1.0 + yaml: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@types/express': + specifier: 5.0.6 + version: 5.0.6 + '@types/node': + specifier: catalog:aws + version: 20.17.6 + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + typescript: + specifier: catalog:aws + version: 5.9.3 + packages/open-next: dependencies: '@ast-grep/napi': @@ -1369,6 +1427,9 @@ importers: '@node-minify/terser': specifier: ^8.0.6 version: 8.0.6 + '@opennextjs/core': + specifier: workspace:* + version: link:../core '@tsconfig/node18': specifier: ^1.0.3 version: 1.0.3 @@ -1436,6 +1497,9 @@ importers: '@opennextjs/aws': specifier: workspace:* version: link:../open-next + '@opennextjs/core': + specifier: workspace:* + version: link:../core devDependencies: '@types/testing-library__jest-dom': specifier: ^5.14.9 @@ -20975,7 +21039,7 @@ snapshots: rxjs@7.8.2: dependencies: - tslib: 2.8.0 + tslib: 2.8.1 safe-buffer@5.1.2: {} From d9f326561b44dd537ac7d84c62f937d4d5a9c969 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 08:56:25 +0200 Subject: [PATCH 02/24] remove helpers --- .../open-next/src/helpers/withCloudflare.ts | 1 - packages/open-next/src/helpers/withSST.ts | 52 ------------------- 2 files changed, 53 deletions(-) delete mode 100644 packages/open-next/src/helpers/withCloudflare.ts delete mode 100644 packages/open-next/src/helpers/withSST.ts diff --git a/packages/open-next/src/helpers/withCloudflare.ts b/packages/open-next/src/helpers/withCloudflare.ts deleted file mode 100644 index 2d4be8e4..00000000 --- a/packages/open-next/src/helpers/withCloudflare.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/helpers/withCloudflare.js"; diff --git a/packages/open-next/src/helpers/withSST.ts b/packages/open-next/src/helpers/withSST.ts deleted file mode 100644 index 16a78cae..00000000 --- a/packages/open-next/src/helpers/withSST.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { FunctionOptions, OpenNextConfig, RouteTemplate } from "@/types/open-next"; - -type SSTCompatibleFunction = FunctionOptions & { - override?: { - wrapper?: "aws-lambda-streaming" | "aws-lambda"; - converter?: "aws-apigw-v2" | "aws-apigw-v1" | "aws-cloudfront"; - }; -}; - -type SSTCompatibleSplittedFunction = { - routes: RouteTemplate[]; - patterns: string[]; -} & SSTCompatibleFunction; - -type SSTCompatibleConfig> = { - default: SSTCompatibleFunction; - functions?: Fn; - middleware?: { - external: true; - }; -} & Pick; - -/** - * This function makes it more straightforward to use SST with OpenNext. - * All options are already restricted to SST compatible options only. - * Some options not present here can be used in SST, but it's an advanced use case that - * can easily break the deployment. If you need to use those options, you should just provide a - * compatible OpenNextConfig inside your `open-next.config.ts` file. - * @example - * ```ts - export default withSST({ - default: { - override: { - wrapper: "aws-lambda-streaming", - }, - }, - functions: { - "api/*": { - routes: ["app/api/test/route", "page/api/otherApi"], - patterns: ["/api/*"], - }, - }, - }); - * ``` - */ -export function withSST>( - config: SSTCompatibleConfig -) { - return { - ...config, - } satisfies OpenNextConfig; -} From e42650a6c02afeb85f76d3a990e2c2508f655b96 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 09:17:03 +0200 Subject: [PATCH 03/24] remove useless files from aws --- .../ses_11f9e6737ffegwim4y24fFKT76.json | 10 ------ examples/app-pages-router/open-next.config.ts | 2 +- examples/app-router/open-next.config.ts | 2 +- examples/experimental/open-next.config.ts | 2 +- packages/open-next/src/adapter.ts | 32 +++++++++---------- packages/open-next/src/adapters/cache.ts | 2 -- .../src/adapters/composable-cache.ts | 2 -- .../open-next/src/adapters/config/index.ts | 1 - .../open-next/src/adapters/config/util.ts | 1 - .../open-next/src/adapters/dynamo-provider.ts | 4 +-- .../open-next/src/adapters/edge-adapter.ts | 2 -- .../adapters/image-optimization-adapter.ts | 1 - packages/open-next/src/adapters/logger.ts | 1 - packages/open-next/src/adapters/middleware.ts | 2 -- .../image-optimization/image-optimization.ts | 1 - packages/open-next/src/adapters/revalidate.ts | 1 - .../open-next/src/adapters/server-adapter.ts | 1 - packages/open-next/src/adapters/util.ts | 1 - .../open-next/src/adapters/warmer-function.ts | 1 - packages/open-next/src/build.ts | 30 ++++++++--------- packages/open-next/src/build/buildNextApp.ts | 1 - packages/open-next/src/build/compileCache.ts | 1 - packages/open-next/src/build/compileConfig.ts | 1 - .../src/build/compileTagCacheProvider.ts | 1 - packages/open-next/src/build/constant.ts | 1 - .../open-next/src/build/copyAdapterFiles.ts | 1 - .../open-next/src/build/copyTracedFiles.ts | 1 - packages/open-next/src/build/createAssets.ts | 1 - .../build/createImageOptimizationBundle.ts | 1 - .../open-next/src/build/createMiddleware.ts | 1 - .../src/build/createRevalidationBundle.ts | 1 - .../open-next/src/build/createServerBundle.ts | 1 - .../open-next/src/build/createWarmerBundle.ts | 1 - .../src/build/edge/createEdgeBundle.ts | 1 - .../open-next/src/build/generateOutput.ts | 1 - packages/open-next/src/build/helper.ts | 1 - packages/open-next/src/build/installDeps.ts | 1 - .../build/middleware/buildNodeMiddleware.ts | 1 - .../src/build/patch/astCodePatcher.ts | 1 - .../open-next/src/build/patch/codePatcher.ts | 1 - .../src/build/patch/patches/index.ts | 1 - .../patches/patchBackgroundRevalidation.ts | 1 - .../src/build/patch/patches/patchEnvVar.ts | 1 - .../build/patch/patches/patchFetchCacheISR.ts | 1 - .../patch/patches/patchFetchCacheWaitUntil.ts | 1 - .../build/patch/patches/patchNextServer.ts | 1 - .../patch/patches/patchNodeEnvironment.ts | 1 - .../patch/patches/patchOriginalNextConfig.ts | 1 - packages/open-next/src/build/utils.ts | 1 - .../open-next/src/build/validateConfig.ts | 1 - .../src/core/createGenericHandler.ts | 1 - .../open-next/src/core/createMainHandler.ts | 1 - .../open-next/src/core/edgeFunctionHandler.ts | 2 -- .../src/core/nodeMiddlewareHandler.ts | 2 -- packages/open-next/src/core/requestHandler.ts | 1 - packages/open-next/src/core/resolve.ts | 1 - .../src/core/routing/adapterHandler.ts | 1 - .../src/core/routing/cacheInterceptor.ts | 1 - .../src/core/routing/i18n/accept-header.ts | 1 - .../open-next/src/core/routing/i18n/index.ts | 1 - .../open-next/src/core/routing/matcher.ts | 1 - .../open-next/src/core/routing/middleware.ts | 1 - packages/open-next/src/core/routing/queue.ts | 1 - .../src/core/routing/routeMatcher.ts | 1 - packages/open-next/src/core/routing/util.ts | 1 - packages/open-next/src/core/routingHandler.ts | 2 -- packages/open-next/src/debug.ts | 1 - packages/open-next/src/http/index.ts | 1 - .../open-next/src/http/openNextResponse.ts | 1 - packages/open-next/src/http/request.ts | 1 - packages/open-next/src/http/util.ts | 1 - packages/open-next/src/logger.ts | 2 -- packages/open-next/src/minimize-js.ts | 1 - .../src/overrides/assetResolver/dummy.ts | 2 -- .../overrides/cdnInvalidation/cloudfront.ts | 2 +- .../src/overrides/cdnInvalidation/dummy.ts | 2 -- .../src/overrides/converters/aws-apigw-v1.ts | 10 +++--- .../src/overrides/converters/aws-apigw-v2.ts | 14 ++++---- .../overrides/converters/aws-cloudfront.ts | 14 ++++---- .../src/overrides/converters/dummy.ts | 2 -- .../src/overrides/converters/edge.ts | 2 -- .../src/overrides/converters/node.ts | 2 -- .../overrides/converters/sqs-revalidate.ts | 4 +-- .../src/overrides/converters/utils.ts | 1 - .../src/overrides/imageLoader/dummy.ts | 2 -- .../src/overrides/imageLoader/fs-dev.ts | 2 -- .../src/overrides/imageLoader/host.ts | 2 -- .../src/overrides/imageLoader/s3-lite.ts | 4 +-- .../open-next/src/overrides/imageLoader/s3.ts | 6 ++-- .../src/overrides/incrementalCache/dummy.ts | 2 -- .../src/overrides/incrementalCache/fs-dev.ts | 2 -- .../incrementalCache/multi-tier-ddb-s3.ts | 8 ++--- .../src/overrides/incrementalCache/s3-lite.ts | 10 +++--- .../src/overrides/incrementalCache/s3.ts | 8 ++--- .../src/overrides/originResolver/dummy.ts | 2 -- .../overrides/originResolver/pattern-env.ts | 2 -- .../overrides/proxyExternalRequest/dummy.ts | 2 -- .../overrides/proxyExternalRequest/fetch.ts | 2 -- .../overrides/proxyExternalRequest/node.ts | 2 -- .../open-next/src/overrides/queue/direct.ts | 2 -- .../open-next/src/overrides/queue/dummy.ts | 2 -- .../open-next/src/overrides/queue/sqs-lite.ts | 8 ++--- packages/open-next/src/overrides/queue/sqs.ts | 4 +-- .../open-next/src/overrides/tagCache/dummy.ts | 2 -- .../src/overrides/tagCache/dynamodb-lite.ts | 10 +++--- .../overrides/tagCache/dynamodb-nextMode.ts | 10 +++--- .../src/overrides/tagCache/dynamodb.ts | 6 ++-- .../src/overrides/tagCache/fs-dev-nextMode.ts | 2 -- .../src/overrides/tagCache/fs-dev.ts | 2 -- .../src/overrides/warmer/aws-lambda.ts | 6 ++-- .../open-next/src/overrides/warmer/dummy.ts | 2 -- .../wrappers/aws-lambda-compressed.ts | 8 ++--- .../wrappers/aws-lambda-streaming.ts | 8 ++--- .../src/overrides/wrappers/aws-lambda.ts | 8 ++--- .../src/overrides/wrappers/cloudflare-edge.ts | 2 -- .../src/overrides/wrappers/cloudflare-node.ts | 2 -- .../open-next/src/overrides/wrappers/dummy.ts | 2 -- .../src/overrides/wrappers/express-dev.ts | 2 -- .../open-next/src/overrides/wrappers/node.ts | 2 -- .../open-next/src/plugins/content-updater.ts | 1 - packages/open-next/src/plugins/edge.ts | 1 - .../src/plugins/externalMiddleware.ts | 1 - .../src/plugins/inline-require-resolve.ts | 1 - .../src/plugins/inlineRouteHandlers.ts | 1 - packages/open-next/src/plugins/replacement.ts | 1 - packages/open-next/src/plugins/resolve.ts | 1 - packages/open-next/src/types/aws-lambda.ts | 2 +- packages/open-next/src/types/cache.ts | 1 - packages/open-next/src/types/next-types.ts | 1 - packages/open-next/src/types/open-next.ts | 1 - packages/open-next/src/types/overrides.ts | 1 - packages/open-next/src/utils/binary.ts | 1 - packages/open-next/src/utils/cache.ts | 1 - packages/open-next/src/utils/error.ts | 1 - packages/open-next/src/utils/lru.ts | 1 - .../open-next/src/utils/normalize-path.ts | 1 - packages/open-next/src/utils/promise.ts | 1 - packages/open-next/src/utils/regex.ts | 1 - .../open-next/src/utils/safe-json-parse.ts | 1 - packages/open-next/src/utils/stream.ts | 1 - packages/open-next/tsconfig.json | 7 +--- .../tests-unit/tests/adapters/cache.test.ts | 2 +- .../tests/adapters/composable-cache.test.ts | 4 +-- .../tests-unit/tests/adapters/logger.test.ts | 4 +-- packages/tests-unit/tests/binary.test.ts | 2 +- .../tests/build/copyTracedFiles.test.ts | 2 +- .../tests-unit/tests/build/helper.test.ts | 2 +- .../tests/build/patch/codePatcher.test.ts | 2 +- .../patchBackgroundRevalidation.test.ts | 4 +-- .../build/patch/patches/patchEnvVars.test.ts | 4 +-- .../patch/patches/patchFetchCacheISR.test.ts | 4 +-- .../patches/patchFetchCacheWaitUntil.test.ts | 4 +-- .../patch/patches/patchNextServer.test.ts | 4 +-- .../patches/patchNodeEnvironment.test.ts | 2 +- .../tests/build/patch/patches/util.ts | 2 +- .../tests-unit/tests/converters/node.test.ts | 4 +-- .../tests-unit/tests/converters/utils.test.ts | 2 +- .../core/routing/cacheInterceptor.test.ts | 12 +++---- .../tests/core/routing/i18n.test.ts | 6 ++-- .../tests/core/routing/matcher.test.ts | 8 ++--- .../tests/core/routing/middleware.test.ts | 10 +++--- .../tests/core/routing/routeMatcher.test.ts | 2 +- .../tests/core/routing/util.test.ts | 4 +-- packages/tests-unit/tests/http/utils.test.ts | 2 +- .../proxyExternalRequest/fetch.test.ts | 2 +- packages/tests-unit/tests/utils/regex.test.ts | 2 +- 166 files changed, 160 insertions(+), 322 deletions(-) delete mode 100644 .omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json delete mode 100644 packages/open-next/src/adapters/cache.ts delete mode 100644 packages/open-next/src/adapters/composable-cache.ts delete mode 100644 packages/open-next/src/adapters/config/index.ts delete mode 100644 packages/open-next/src/adapters/config/util.ts delete mode 100644 packages/open-next/src/adapters/edge-adapter.ts delete mode 100644 packages/open-next/src/adapters/image-optimization-adapter.ts delete mode 100644 packages/open-next/src/adapters/logger.ts delete mode 100644 packages/open-next/src/adapters/middleware.ts delete mode 100644 packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts delete mode 100644 packages/open-next/src/adapters/revalidate.ts delete mode 100644 packages/open-next/src/adapters/server-adapter.ts delete mode 100644 packages/open-next/src/adapters/util.ts delete mode 100644 packages/open-next/src/adapters/warmer-function.ts delete mode 100644 packages/open-next/src/build/buildNextApp.ts delete mode 100644 packages/open-next/src/build/compileCache.ts delete mode 100644 packages/open-next/src/build/compileConfig.ts delete mode 100644 packages/open-next/src/build/compileTagCacheProvider.ts delete mode 100644 packages/open-next/src/build/constant.ts delete mode 100644 packages/open-next/src/build/copyAdapterFiles.ts delete mode 100644 packages/open-next/src/build/copyTracedFiles.ts delete mode 100644 packages/open-next/src/build/createAssets.ts delete mode 100644 packages/open-next/src/build/createImageOptimizationBundle.ts delete mode 100644 packages/open-next/src/build/createMiddleware.ts delete mode 100644 packages/open-next/src/build/createRevalidationBundle.ts delete mode 100644 packages/open-next/src/build/createServerBundle.ts delete mode 100644 packages/open-next/src/build/createWarmerBundle.ts delete mode 100644 packages/open-next/src/build/edge/createEdgeBundle.ts delete mode 100644 packages/open-next/src/build/generateOutput.ts delete mode 100644 packages/open-next/src/build/helper.ts delete mode 100644 packages/open-next/src/build/installDeps.ts delete mode 100644 packages/open-next/src/build/middleware/buildNodeMiddleware.ts delete mode 100644 packages/open-next/src/build/patch/astCodePatcher.ts delete mode 100644 packages/open-next/src/build/patch/codePatcher.ts delete mode 100644 packages/open-next/src/build/patch/patches/index.ts delete mode 100644 packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts delete mode 100644 packages/open-next/src/build/patch/patches/patchEnvVar.ts delete mode 100644 packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts delete mode 100644 packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts delete mode 100644 packages/open-next/src/build/patch/patches/patchNextServer.ts delete mode 100644 packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts delete mode 100644 packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts delete mode 100644 packages/open-next/src/build/utils.ts delete mode 100644 packages/open-next/src/build/validateConfig.ts delete mode 100644 packages/open-next/src/core/createGenericHandler.ts delete mode 100644 packages/open-next/src/core/createMainHandler.ts delete mode 100644 packages/open-next/src/core/edgeFunctionHandler.ts delete mode 100644 packages/open-next/src/core/nodeMiddlewareHandler.ts delete mode 100644 packages/open-next/src/core/requestHandler.ts delete mode 100644 packages/open-next/src/core/resolve.ts delete mode 100644 packages/open-next/src/core/routing/adapterHandler.ts delete mode 100644 packages/open-next/src/core/routing/cacheInterceptor.ts delete mode 100644 packages/open-next/src/core/routing/i18n/accept-header.ts delete mode 100644 packages/open-next/src/core/routing/i18n/index.ts delete mode 100644 packages/open-next/src/core/routing/matcher.ts delete mode 100644 packages/open-next/src/core/routing/middleware.ts delete mode 100644 packages/open-next/src/core/routing/queue.ts delete mode 100644 packages/open-next/src/core/routing/routeMatcher.ts delete mode 100644 packages/open-next/src/core/routing/util.ts delete mode 100644 packages/open-next/src/core/routingHandler.ts delete mode 100644 packages/open-next/src/debug.ts delete mode 100644 packages/open-next/src/http/index.ts delete mode 100644 packages/open-next/src/http/openNextResponse.ts delete mode 100644 packages/open-next/src/http/request.ts delete mode 100644 packages/open-next/src/http/util.ts delete mode 100644 packages/open-next/src/logger.ts delete mode 100644 packages/open-next/src/minimize-js.ts delete mode 100644 packages/open-next/src/overrides/assetResolver/dummy.ts delete mode 100644 packages/open-next/src/overrides/cdnInvalidation/dummy.ts delete mode 100644 packages/open-next/src/overrides/converters/dummy.ts delete mode 100644 packages/open-next/src/overrides/converters/edge.ts delete mode 100644 packages/open-next/src/overrides/converters/node.ts delete mode 100644 packages/open-next/src/overrides/converters/utils.ts delete mode 100644 packages/open-next/src/overrides/imageLoader/dummy.ts delete mode 100644 packages/open-next/src/overrides/imageLoader/fs-dev.ts delete mode 100644 packages/open-next/src/overrides/imageLoader/host.ts delete mode 100644 packages/open-next/src/overrides/incrementalCache/dummy.ts delete mode 100644 packages/open-next/src/overrides/incrementalCache/fs-dev.ts delete mode 100644 packages/open-next/src/overrides/originResolver/dummy.ts delete mode 100644 packages/open-next/src/overrides/originResolver/pattern-env.ts delete mode 100644 packages/open-next/src/overrides/proxyExternalRequest/dummy.ts delete mode 100644 packages/open-next/src/overrides/proxyExternalRequest/fetch.ts delete mode 100644 packages/open-next/src/overrides/proxyExternalRequest/node.ts delete mode 100644 packages/open-next/src/overrides/queue/direct.ts delete mode 100644 packages/open-next/src/overrides/queue/dummy.ts delete mode 100644 packages/open-next/src/overrides/tagCache/dummy.ts delete mode 100644 packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts delete mode 100644 packages/open-next/src/overrides/tagCache/fs-dev.ts delete mode 100644 packages/open-next/src/overrides/warmer/dummy.ts delete mode 100644 packages/open-next/src/overrides/wrappers/cloudflare-edge.ts delete mode 100644 packages/open-next/src/overrides/wrappers/cloudflare-node.ts delete mode 100644 packages/open-next/src/overrides/wrappers/dummy.ts delete mode 100644 packages/open-next/src/overrides/wrappers/express-dev.ts delete mode 100644 packages/open-next/src/overrides/wrappers/node.ts delete mode 100644 packages/open-next/src/plugins/content-updater.ts delete mode 100644 packages/open-next/src/plugins/edge.ts delete mode 100644 packages/open-next/src/plugins/externalMiddleware.ts delete mode 100644 packages/open-next/src/plugins/inline-require-resolve.ts delete mode 100644 packages/open-next/src/plugins/inlineRouteHandlers.ts delete mode 100644 packages/open-next/src/plugins/replacement.ts delete mode 100644 packages/open-next/src/plugins/resolve.ts delete mode 100644 packages/open-next/src/types/cache.ts delete mode 100644 packages/open-next/src/types/next-types.ts delete mode 100644 packages/open-next/src/types/open-next.ts delete mode 100644 packages/open-next/src/types/overrides.ts delete mode 100644 packages/open-next/src/utils/binary.ts delete mode 100644 packages/open-next/src/utils/cache.ts delete mode 100644 packages/open-next/src/utils/error.ts delete mode 100644 packages/open-next/src/utils/lru.ts delete mode 100644 packages/open-next/src/utils/normalize-path.ts delete mode 100644 packages/open-next/src/utils/promise.ts delete mode 100644 packages/open-next/src/utils/regex.ts delete mode 100644 packages/open-next/src/utils/safe-json-parse.ts delete mode 100644 packages/open-next/src/utils/stream.ts diff --git a/.omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json b/.omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json deleted file mode 100644 index 38e31e1f..00000000 --- a/.omo/run-continuation/ses_11f9e6737ffegwim4y24fFKT76.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sessionID": "ses_11f9e6737ffegwim4y24fFKT76", - "updatedAt": "2026-06-19T15:09:29.144Z", - "sources": { - "background-task": { - "state": "idle", - "updatedAt": "2026-06-19T15:09:29.144Z" - } - } -} \ No newline at end of file diff --git a/examples/app-pages-router/open-next.config.ts b/examples/app-pages-router/open-next.config.ts index f32adfba..c1124cb4 100644 --- a/examples/app-pages-router/open-next.config.ts +++ b/examples/app-pages-router/open-next.config.ts @@ -1,4 +1,4 @@ -import type { OpenNextConfig, OverrideOptions } from "@opennextjs/aws/types/open-next.js"; +import type { OpenNextConfig, OverrideOptions } from "@opennextjs/core/types/open-next.js"; const devOverride = { wrapper: "express-dev", diff --git a/examples/app-router/open-next.config.ts b/examples/app-router/open-next.config.ts index 311aa5bb..e52dd92b 100644 --- a/examples/app-router/open-next.config.ts +++ b/examples/app-router/open-next.config.ts @@ -1,4 +1,4 @@ -import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js"; +import type { OpenNextConfig } from "@opennextjs/core/types/open-next.js"; export default { default: { diff --git a/examples/experimental/open-next.config.ts b/examples/experimental/open-next.config.ts index 63d82c31..b08f90fe 100644 --- a/examples/experimental/open-next.config.ts +++ b/examples/experimental/open-next.config.ts @@ -1,4 +1,4 @@ -import type { OpenNextConfig } from "@opennextjs/aws/types/open-next.js"; +import type { OpenNextConfig } from "@opennextjs/core/types/open-next.js"; export default { default: { diff --git a/packages/open-next/src/adapter.ts b/packages/open-next/src/adapter.ts index d734d32f..f7433d7c 100644 --- a/packages/open-next/src/adapter.ts +++ b/packages/open-next/src/adapter.ts @@ -2,22 +2,22 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; -import type { NextConfig } from "@/types/next-types"; - -import { compileCache } from "./build/compileCache.js"; -import { compileOpenNextConfig } from "./build/compileConfig.js"; -import { compileTagCacheProvider } from "./build/compileTagCacheProvider.js"; -import { createCacheAssets, createStaticAssets } from "./build/createAssets.js"; -import { createImageOptimizationBundle } from "./build/createImageOptimizationBundle.js"; -import { createMiddleware } from "./build/createMiddleware.js"; -import { createRevalidationBundle } from "./build/createRevalidationBundle.js"; -import { createServerBundle } from "./build/createServerBundle.js"; -import { createWarmerBundle } from "./build/createWarmerBundle.js"; -import { generateOutput } from "./build/generateOutput.js"; -import * as buildHelper from "./build/helper.js"; -import { addDebugFile } from "./debug.js"; -import type { ContentUpdater } from "./plugins/content-updater.js"; -import { externalChunksPlugin, inlineRouteHandler } from "./plugins/inlineRouteHandlers.js"; +import type { NextConfig } from "@opennextjs/core/types/next-types.js"; + +import { compileCache } from "@opennextjs/core/build/compileCache.js"; +import { compileOpenNextConfig } from "@opennextjs/core/build/compileConfig.js"; +import { compileTagCacheProvider } from "@opennextjs/core/build/compileTagCacheProvider.js"; +import { createCacheAssets, createStaticAssets } from "@opennextjs/core/build/createAssets.js"; +import { createImageOptimizationBundle } from "@opennextjs/core/build/createImageOptimizationBundle.js"; +import { createMiddleware } from "@opennextjs/core/build/createMiddleware.js"; +import { createRevalidationBundle } from "@opennextjs/core/build/createRevalidationBundle.js"; +import { createServerBundle } from "@opennextjs/core/build/createServerBundle.js"; +import { createWarmerBundle } from "@opennextjs/core/build/createWarmerBundle.js"; +import { generateOutput } from "@opennextjs/core/build/generateOutput.js"; +import * as buildHelper from "@opennextjs/core/build/helper.js"; +import { addDebugFile } from "@opennextjs/core/debug.js"; +import type { ContentUpdater } from "@opennextjs/core/plugins/content-updater.js"; +import { externalChunksPlugin, inlineRouteHandler } from "@opennextjs/core/plugins/inlineRouteHandlers.js"; export type NextAdapterOutput = { pathname: string; diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts deleted file mode 100644 index 15ca9d4b..00000000 --- a/packages/open-next/src/adapters/cache.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/adapters/cache.js"; -export * from "@opennextjs/core/adapters/cache.js"; diff --git a/packages/open-next/src/adapters/composable-cache.ts b/packages/open-next/src/adapters/composable-cache.ts deleted file mode 100644 index 281b37dd..00000000 --- a/packages/open-next/src/adapters/composable-cache.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/adapters/composable-cache.js"; -export * from "@opennextjs/core/adapters/composable-cache.js"; diff --git a/packages/open-next/src/adapters/config/index.ts b/packages/open-next/src/adapters/config/index.ts deleted file mode 100644 index 06110ff0..00000000 --- a/packages/open-next/src/adapters/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/config/index.js"; diff --git a/packages/open-next/src/adapters/config/util.ts b/packages/open-next/src/adapters/config/util.ts deleted file mode 100644 index d9ce9aa6..00000000 --- a/packages/open-next/src/adapters/config/util.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/config/util.js"; diff --git a/packages/open-next/src/adapters/dynamo-provider.ts b/packages/open-next/src/adapters/dynamo-provider.ts index ed5a7dee..c0a615b7 100644 --- a/packages/open-next/src/adapters/dynamo-provider.ts +++ b/packages/open-next/src/adapters/dynamo-provider.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; -import { createGenericHandler } from "../core/createGenericHandler.js"; -import { resolveTagCache } from "../core/resolve.js"; +import { createGenericHandler } from "@opennextjs/core/core/createGenericHandler.js"; +import { resolveTagCache } from "@opennextjs/core/core/resolve.js"; const PHYSICAL_RESOURCE_ID = "dynamodb-cache" as const; diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts deleted file mode 100644 index a2ac96ef..00000000 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/adapters/edge-adapter.js"; -export * from "@opennextjs/core/adapters/edge-adapter.js"; diff --git a/packages/open-next/src/adapters/image-optimization-adapter.ts b/packages/open-next/src/adapters/image-optimization-adapter.ts deleted file mode 100644 index 075f3822..00000000 --- a/packages/open-next/src/adapters/image-optimization-adapter.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/image-optimization-adapter.js"; diff --git a/packages/open-next/src/adapters/logger.ts b/packages/open-next/src/adapters/logger.ts deleted file mode 100644 index 94abb09b..00000000 --- a/packages/open-next/src/adapters/logger.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/logger.js"; diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts deleted file mode 100644 index 01d283da..00000000 --- a/packages/open-next/src/adapters/middleware.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/adapters/middleware.js"; -export * from "@opennextjs/core/adapters/middleware.js"; diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts deleted file mode 100644 index 848ffc6e..00000000 --- a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/plugins/image-optimization/image-optimization.js"; diff --git a/packages/open-next/src/adapters/revalidate.ts b/packages/open-next/src/adapters/revalidate.ts deleted file mode 100644 index 05f3e49a..00000000 --- a/packages/open-next/src/adapters/revalidate.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/revalidate.js"; diff --git a/packages/open-next/src/adapters/server-adapter.ts b/packages/open-next/src/adapters/server-adapter.ts deleted file mode 100644 index 2f4f1cb8..00000000 --- a/packages/open-next/src/adapters/server-adapter.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/server-adapter.js"; diff --git a/packages/open-next/src/adapters/util.ts b/packages/open-next/src/adapters/util.ts deleted file mode 100644 index a8b76eef..00000000 --- a/packages/open-next/src/adapters/util.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/util.js"; diff --git a/packages/open-next/src/adapters/warmer-function.ts b/packages/open-next/src/adapters/warmer-function.ts deleted file mode 100644 index 0b5ad303..00000000 --- a/packages/open-next/src/adapters/warmer-function.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/adapters/warmer-function.js"; diff --git a/packages/open-next/src/build.ts b/packages/open-next/src/build.ts index 315a1e2b..d74befcd 100755 --- a/packages/open-next/src/build.ts +++ b/packages/open-next/src/build.ts @@ -2,21 +2,21 @@ import { createRequire } from "node:module"; import path from "node:path"; import url from "node:url"; -import { buildNextjsApp, setStandaloneBuildMode } from "./build/buildNextApp.js"; -import { compileCache } from "./build/compileCache.js"; -import { compileOpenNextConfig } from "./build/compileConfig.js"; -import { compileTagCacheProvider } from "./build/compileTagCacheProvider.js"; -import { createCacheAssets, createStaticAssets } from "./build/createAssets.js"; -import { createImageOptimizationBundle } from "./build/createImageOptimizationBundle.js"; -import { createMiddleware } from "./build/createMiddleware.js"; -import { createRevalidationBundle } from "./build/createRevalidationBundle.js"; -import { createServerBundle } from "./build/createServerBundle.js"; -import { createWarmerBundle } from "./build/createWarmerBundle.js"; -import { generateOutput } from "./build/generateOutput.js"; -import * as buildHelper from "./build/helper.js"; -import { patchOriginalNextConfig } from "./build/patch/patches/index.js"; -import { printHeader, showWarningOnWindows } from "./build/utils.js"; -import logger from "./logger.js"; +import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/core/build/buildNextApp.js"; +import { compileCache } from "@opennextjs/core/build/compileCache.js"; +import { compileOpenNextConfig } from "@opennextjs/core/build/compileConfig.js"; +import { compileTagCacheProvider } from "@opennextjs/core/build/compileTagCacheProvider.js"; +import { createCacheAssets, createStaticAssets } from "@opennextjs/core/build/createAssets.js"; +import { createImageOptimizationBundle } from "@opennextjs/core/build/createImageOptimizationBundle.js"; +import { createMiddleware } from "@opennextjs/core/build/createMiddleware.js"; +import { createRevalidationBundle } from "@opennextjs/core/build/createRevalidationBundle.js"; +import { createServerBundle } from "@opennextjs/core/build/createServerBundle.js"; +import { createWarmerBundle } from "@opennextjs/core/build/createWarmerBundle.js"; +import { generateOutput } from "@opennextjs/core/build/generateOutput.js"; +import * as buildHelper from "@opennextjs/core/build/helper.js"; +import { patchOriginalNextConfig } from "@opennextjs/core/build/patch/patches/index.js"; +import { printHeader, showWarningOnWindows } from "@opennextjs/core/build/utils.js"; +import logger from "@opennextjs/core/logger.js"; const require = createRequire(import.meta.url); diff --git a/packages/open-next/src/build/buildNextApp.ts b/packages/open-next/src/build/buildNextApp.ts deleted file mode 100644 index 61fd9e2e..00000000 --- a/packages/open-next/src/build/buildNextApp.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/buildNextApp.js"; diff --git a/packages/open-next/src/build/compileCache.ts b/packages/open-next/src/build/compileCache.ts deleted file mode 100644 index b81d7dcf..00000000 --- a/packages/open-next/src/build/compileCache.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/compileCache.js"; diff --git a/packages/open-next/src/build/compileConfig.ts b/packages/open-next/src/build/compileConfig.ts deleted file mode 100644 index e4b3fcf7..00000000 --- a/packages/open-next/src/build/compileConfig.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/compileConfig.js"; diff --git a/packages/open-next/src/build/compileTagCacheProvider.ts b/packages/open-next/src/build/compileTagCacheProvider.ts deleted file mode 100644 index 8963fc76..00000000 --- a/packages/open-next/src/build/compileTagCacheProvider.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/compileTagCacheProvider.js"; diff --git a/packages/open-next/src/build/constant.ts b/packages/open-next/src/build/constant.ts deleted file mode 100644 index 60719069..00000000 --- a/packages/open-next/src/build/constant.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/constant.js"; diff --git a/packages/open-next/src/build/copyAdapterFiles.ts b/packages/open-next/src/build/copyAdapterFiles.ts deleted file mode 100644 index d8cca979..00000000 --- a/packages/open-next/src/build/copyAdapterFiles.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/copyAdapterFiles.js"; diff --git a/packages/open-next/src/build/copyTracedFiles.ts b/packages/open-next/src/build/copyTracedFiles.ts deleted file mode 100644 index 624162dc..00000000 --- a/packages/open-next/src/build/copyTracedFiles.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/copyTracedFiles.js"; diff --git a/packages/open-next/src/build/createAssets.ts b/packages/open-next/src/build/createAssets.ts deleted file mode 100644 index ad010175..00000000 --- a/packages/open-next/src/build/createAssets.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/createAssets.js"; diff --git a/packages/open-next/src/build/createImageOptimizationBundle.ts b/packages/open-next/src/build/createImageOptimizationBundle.ts deleted file mode 100644 index a9af6747..00000000 --- a/packages/open-next/src/build/createImageOptimizationBundle.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/createImageOptimizationBundle.js"; diff --git a/packages/open-next/src/build/createMiddleware.ts b/packages/open-next/src/build/createMiddleware.ts deleted file mode 100644 index b609f88c..00000000 --- a/packages/open-next/src/build/createMiddleware.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/createMiddleware.js"; diff --git a/packages/open-next/src/build/createRevalidationBundle.ts b/packages/open-next/src/build/createRevalidationBundle.ts deleted file mode 100644 index 117c0bb4..00000000 --- a/packages/open-next/src/build/createRevalidationBundle.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/createRevalidationBundle.js"; diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts deleted file mode 100644 index 56fa25ca..00000000 --- a/packages/open-next/src/build/createServerBundle.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/createServerBundle.js"; diff --git a/packages/open-next/src/build/createWarmerBundle.ts b/packages/open-next/src/build/createWarmerBundle.ts deleted file mode 100644 index 90024cf4..00000000 --- a/packages/open-next/src/build/createWarmerBundle.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/createWarmerBundle.js"; diff --git a/packages/open-next/src/build/edge/createEdgeBundle.ts b/packages/open-next/src/build/edge/createEdgeBundle.ts deleted file mode 100644 index 7be2f83d..00000000 --- a/packages/open-next/src/build/edge/createEdgeBundle.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/edge/createEdgeBundle.js"; diff --git a/packages/open-next/src/build/generateOutput.ts b/packages/open-next/src/build/generateOutput.ts deleted file mode 100644 index 5ff1b4c5..00000000 --- a/packages/open-next/src/build/generateOutput.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/generateOutput.js"; diff --git a/packages/open-next/src/build/helper.ts b/packages/open-next/src/build/helper.ts deleted file mode 100644 index 727d3926..00000000 --- a/packages/open-next/src/build/helper.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/helper.js"; diff --git a/packages/open-next/src/build/installDeps.ts b/packages/open-next/src/build/installDeps.ts deleted file mode 100644 index a967c08a..00000000 --- a/packages/open-next/src/build/installDeps.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/installDeps.js"; diff --git a/packages/open-next/src/build/middleware/buildNodeMiddleware.ts b/packages/open-next/src/build/middleware/buildNodeMiddleware.ts deleted file mode 100644 index 7cdfdf50..00000000 --- a/packages/open-next/src/build/middleware/buildNodeMiddleware.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/middleware/buildNodeMiddleware.js"; diff --git a/packages/open-next/src/build/patch/astCodePatcher.ts b/packages/open-next/src/build/patch/astCodePatcher.ts deleted file mode 100644 index 6fab03b5..00000000 --- a/packages/open-next/src/build/patch/astCodePatcher.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/astCodePatcher.js"; diff --git a/packages/open-next/src/build/patch/codePatcher.ts b/packages/open-next/src/build/patch/codePatcher.ts deleted file mode 100644 index 2cacbab6..00000000 --- a/packages/open-next/src/build/patch/codePatcher.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/codePatcher.js"; diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts deleted file mode 100644 index a02f8f26..00000000 --- a/packages/open-next/src/build/patch/patches/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/index.js"; diff --git a/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts b/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts deleted file mode 100644 index 2345d463..00000000 --- a/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/patchBackgroundRevalidation.js"; diff --git a/packages/open-next/src/build/patch/patches/patchEnvVar.ts b/packages/open-next/src/build/patch/patches/patchEnvVar.ts deleted file mode 100644 index e65cd82b..00000000 --- a/packages/open-next/src/build/patch/patches/patchEnvVar.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/patchEnvVar.js"; diff --git a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts deleted file mode 100644 index 8923936b..00000000 --- a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/patchFetchCacheISR.js"; diff --git a/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts deleted file mode 100644 index fe29bab1..00000000 --- a/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/patchFetchCacheWaitUntil.js"; diff --git a/packages/open-next/src/build/patch/patches/patchNextServer.ts b/packages/open-next/src/build/patch/patches/patchNextServer.ts deleted file mode 100644 index c872cdb3..00000000 --- a/packages/open-next/src/build/patch/patches/patchNextServer.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/patchNextServer.js"; diff --git a/packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts b/packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts deleted file mode 100644 index e6c306f0..00000000 --- a/packages/open-next/src/build/patch/patches/patchNodeEnvironment.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/patchNodeEnvironment.js"; diff --git a/packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts b/packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts deleted file mode 100644 index a0a128b6..00000000 --- a/packages/open-next/src/build/patch/patches/patchOriginalNextConfig.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/patch/patches/patchOriginalNextConfig.js"; diff --git a/packages/open-next/src/build/utils.ts b/packages/open-next/src/build/utils.ts deleted file mode 100644 index 70e96a8b..00000000 --- a/packages/open-next/src/build/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/utils.js"; diff --git a/packages/open-next/src/build/validateConfig.ts b/packages/open-next/src/build/validateConfig.ts deleted file mode 100644 index 88be3ba3..00000000 --- a/packages/open-next/src/build/validateConfig.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/build/validateConfig.js"; diff --git a/packages/open-next/src/core/createGenericHandler.ts b/packages/open-next/src/core/createGenericHandler.ts deleted file mode 100644 index 19938c40..00000000 --- a/packages/open-next/src/core/createGenericHandler.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/createGenericHandler.js"; diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts deleted file mode 100644 index 793af42b..00000000 --- a/packages/open-next/src/core/createMainHandler.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/createMainHandler.js"; diff --git a/packages/open-next/src/core/edgeFunctionHandler.ts b/packages/open-next/src/core/edgeFunctionHandler.ts deleted file mode 100644 index b846bf72..00000000 --- a/packages/open-next/src/core/edgeFunctionHandler.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/core/edgeFunctionHandler.js"; -export * from "@opennextjs/core/core/edgeFunctionHandler.js"; diff --git a/packages/open-next/src/core/nodeMiddlewareHandler.ts b/packages/open-next/src/core/nodeMiddlewareHandler.ts deleted file mode 100644 index 6475ec53..00000000 --- a/packages/open-next/src/core/nodeMiddlewareHandler.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/core/nodeMiddlewareHandler.js"; -export * from "@opennextjs/core/core/nodeMiddlewareHandler.js"; diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts deleted file mode 100644 index 50b5bcf0..00000000 --- a/packages/open-next/src/core/requestHandler.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/requestHandler.js"; diff --git a/packages/open-next/src/core/resolve.ts b/packages/open-next/src/core/resolve.ts deleted file mode 100644 index b2eec57a..00000000 --- a/packages/open-next/src/core/resolve.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/resolve.js"; diff --git a/packages/open-next/src/core/routing/adapterHandler.ts b/packages/open-next/src/core/routing/adapterHandler.ts deleted file mode 100644 index 97757fe9..00000000 --- a/packages/open-next/src/core/routing/adapterHandler.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/adapterHandler.js"; diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts deleted file mode 100644 index f519f571..00000000 --- a/packages/open-next/src/core/routing/cacheInterceptor.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/cacheInterceptor.js"; diff --git a/packages/open-next/src/core/routing/i18n/accept-header.ts b/packages/open-next/src/core/routing/i18n/accept-header.ts deleted file mode 100644 index b3eb55b9..00000000 --- a/packages/open-next/src/core/routing/i18n/accept-header.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/i18n/accept-header.js"; diff --git a/packages/open-next/src/core/routing/i18n/index.ts b/packages/open-next/src/core/routing/i18n/index.ts deleted file mode 100644 index ffc76e3b..00000000 --- a/packages/open-next/src/core/routing/i18n/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/i18n/index.js"; diff --git a/packages/open-next/src/core/routing/matcher.ts b/packages/open-next/src/core/routing/matcher.ts deleted file mode 100644 index 93f2d035..00000000 --- a/packages/open-next/src/core/routing/matcher.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/matcher.js"; diff --git a/packages/open-next/src/core/routing/middleware.ts b/packages/open-next/src/core/routing/middleware.ts deleted file mode 100644 index 9c056129..00000000 --- a/packages/open-next/src/core/routing/middleware.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/middleware.js"; diff --git a/packages/open-next/src/core/routing/queue.ts b/packages/open-next/src/core/routing/queue.ts deleted file mode 100644 index 3c3511cb..00000000 --- a/packages/open-next/src/core/routing/queue.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/queue.js"; diff --git a/packages/open-next/src/core/routing/routeMatcher.ts b/packages/open-next/src/core/routing/routeMatcher.ts deleted file mode 100644 index ac30f4b9..00000000 --- a/packages/open-next/src/core/routing/routeMatcher.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/routeMatcher.js"; diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts deleted file mode 100644 index 9574ddea..00000000 --- a/packages/open-next/src/core/routing/util.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/core/routing/util.js"; diff --git a/packages/open-next/src/core/routingHandler.ts b/packages/open-next/src/core/routingHandler.ts deleted file mode 100644 index 14c9a5c9..00000000 --- a/packages/open-next/src/core/routingHandler.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/core/routingHandler.js"; -export * from "@opennextjs/core/core/routingHandler.js"; diff --git a/packages/open-next/src/debug.ts b/packages/open-next/src/debug.ts deleted file mode 100644 index 67457df7..00000000 --- a/packages/open-next/src/debug.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/debug.js"; diff --git a/packages/open-next/src/http/index.ts b/packages/open-next/src/http/index.ts deleted file mode 100644 index 9492b4bb..00000000 --- a/packages/open-next/src/http/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/http/index.js"; diff --git a/packages/open-next/src/http/openNextResponse.ts b/packages/open-next/src/http/openNextResponse.ts deleted file mode 100644 index 75431c81..00000000 --- a/packages/open-next/src/http/openNextResponse.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/http/openNextResponse.js"; diff --git a/packages/open-next/src/http/request.ts b/packages/open-next/src/http/request.ts deleted file mode 100644 index fc6b80fa..00000000 --- a/packages/open-next/src/http/request.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/http/request.js"; diff --git a/packages/open-next/src/http/util.ts b/packages/open-next/src/http/util.ts deleted file mode 100644 index 604249dc..00000000 --- a/packages/open-next/src/http/util.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/http/util.js"; diff --git a/packages/open-next/src/logger.ts b/packages/open-next/src/logger.ts deleted file mode 100644 index 4db02f0c..00000000 --- a/packages/open-next/src/logger.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/logger.js"; -export * from "@opennextjs/core/logger.js"; diff --git a/packages/open-next/src/minimize-js.ts b/packages/open-next/src/minimize-js.ts deleted file mode 100644 index d8ed6424..00000000 --- a/packages/open-next/src/minimize-js.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/minimize-js.js"; diff --git a/packages/open-next/src/overrides/assetResolver/dummy.ts b/packages/open-next/src/overrides/assetResolver/dummy.ts deleted file mode 100644 index 8f789714..00000000 --- a/packages/open-next/src/overrides/assetResolver/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/assetResolver/dummy.js"; -export * from "@opennextjs/core/overrides/assetResolver/dummy.js"; diff --git a/packages/open-next/src/overrides/cdnInvalidation/cloudfront.ts b/packages/open-next/src/overrides/cdnInvalidation/cloudfront.ts index f8faa9fd..a328c875 100644 --- a/packages/open-next/src/overrides/cdnInvalidation/cloudfront.ts +++ b/packages/open-next/src/overrides/cdnInvalidation/cloudfront.ts @@ -1,6 +1,6 @@ import { CloudFrontClient, CreateInvalidationCommand } from "@aws-sdk/client-cloudfront"; -import type { CDNInvalidationHandler } from "@/types/overrides"; +import type { CDNInvalidationHandler } from "@opennextjs/core/types/overrides.js"; const cloudfront = new CloudFrontClient({}); export default { diff --git a/packages/open-next/src/overrides/cdnInvalidation/dummy.ts b/packages/open-next/src/overrides/cdnInvalidation/dummy.ts deleted file mode 100644 index 12584686..00000000 --- a/packages/open-next/src/overrides/cdnInvalidation/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/cdnInvalidation/dummy.js"; -export * from "@opennextjs/core/overrides/cdnInvalidation/dummy.js"; diff --git a/packages/open-next/src/overrides/converters/aws-apigw-v1.ts b/packages/open-next/src/overrides/converters/aws-apigw-v1.ts index fdc83a07..55fcaef0 100644 --- a/packages/open-next/src/overrides/converters/aws-apigw-v1.ts +++ b/packages/open-next/src/overrides/converters/aws-apigw-v1.ts @@ -1,12 +1,12 @@ import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; -import type { InternalEvent, InternalResult } from "@/types/open-next"; -import type { Converter } from "@/types/overrides"; -import { fromReadableStream } from "@/utils/stream"; +import type { InternalEvent, InternalResult } from "@opennextjs/core/types/open-next.js"; +import type { Converter } from "@opennextjs/core/types/overrides.js"; +import { fromReadableStream } from "@opennextjs/core/utils/stream.js"; -import { debug } from "../../adapters/logger"; +import { debug } from "@opennextjs/core/adapters/logger.js"; -import { extractHostFromHeaders, removeUndefinedFromQuery } from "./utils"; +import { extractHostFromHeaders, removeUndefinedFromQuery } from "@opennextjs/core/overrides/converters/utils.js"; function normalizeAPIGatewayProxyEventHeaders(event: APIGatewayProxyEvent): Record { event.multiValueHeaders; diff --git a/packages/open-next/src/overrides/converters/aws-apigw-v2.ts b/packages/open-next/src/overrides/converters/aws-apigw-v2.ts index 87c53b84..708e7ffa 100644 --- a/packages/open-next/src/overrides/converters/aws-apigw-v2.ts +++ b/packages/open-next/src/overrides/converters/aws-apigw-v2.ts @@ -1,14 +1,14 @@ import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"; -import { parseSetCookieHeader } from "@/http/util"; -import type { InternalEvent, InternalResult } from "@/types/open-next"; -import type { Converter } from "@/types/overrides"; -import { fromReadableStream } from "@/utils/stream"; +import { parseSetCookieHeader } from "@opennextjs/core/http/util.js"; +import type { InternalEvent, InternalResult } from "@opennextjs/core/types/open-next.js"; +import type { Converter } from "@opennextjs/core/types/overrides.js"; +import { fromReadableStream } from "@opennextjs/core/utils/stream.js"; -import { debug } from "../../adapters/logger"; -import { convertToQuery } from "../../core/routing/util"; +import { debug } from "@opennextjs/core/adapters/logger.js"; +import { convertToQuery } from "@opennextjs/core/core/routing/util.js"; -import { extractHostFromHeaders, removeUndefinedFromQuery } from "./utils"; +import { extractHostFromHeaders, removeUndefinedFromQuery } from "@opennextjs/core/overrides/converters/utils.js"; // Not sure which one is really needed as this is not documented anywhere but server actions redirect are not working without this, // it causes a 500 error from cloudfront itself with a 'x-amzErrortype: InternalFailure' header diff --git a/packages/open-next/src/overrides/converters/aws-cloudfront.ts b/packages/open-next/src/overrides/converters/aws-cloudfront.ts index 678b114e..ce6bc8fb 100644 --- a/packages/open-next/src/overrides/converters/aws-cloudfront.ts +++ b/packages/open-next/src/overrides/converters/aws-cloudfront.ts @@ -8,15 +8,15 @@ import type { CloudFrontRequestResult, } from "aws-lambda"; -import { parseSetCookieHeader } from "@/http/util"; -import type { InternalEvent, InternalResult, MiddlewareResult } from "@/types/open-next"; -import type { Converter } from "@/types/overrides"; -import { fromReadableStream } from "@/utils/stream"; +import { parseSetCookieHeader } from "@opennextjs/core/http/util.js"; +import type { InternalEvent, InternalResult, MiddlewareResult } from "@opennextjs/core/types/open-next.js"; +import type { Converter } from "@opennextjs/core/types/overrides.js"; +import { fromReadableStream } from "@opennextjs/core/utils/stream.js"; -import { debug } from "../../adapters/logger"; -import { convertToQuery, convertToQueryString } from "../../core/routing/util"; +import { debug } from "@opennextjs/core/adapters/logger.js"; +import { convertToQuery, convertToQueryString } from "@opennextjs/core/core/routing/util.js"; -import { extractHostFromHeaders } from "./utils"; +import { extractHostFromHeaders } from "@opennextjs/core/overrides/converters/utils.js"; const cloudfrontBlacklistedHeaders = [ // Disallowed headers, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-function-restrictions-all.html#function-restrictions-disallowed-headers diff --git a/packages/open-next/src/overrides/converters/dummy.ts b/packages/open-next/src/overrides/converters/dummy.ts deleted file mode 100644 index 66f6fd35..00000000 --- a/packages/open-next/src/overrides/converters/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/converters/dummy.js"; -export * from "@opennextjs/core/overrides/converters/dummy.js"; diff --git a/packages/open-next/src/overrides/converters/edge.ts b/packages/open-next/src/overrides/converters/edge.ts deleted file mode 100644 index 9b230c35..00000000 --- a/packages/open-next/src/overrides/converters/edge.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/converters/edge.js"; -export * from "@opennextjs/core/overrides/converters/edge.js"; diff --git a/packages/open-next/src/overrides/converters/node.ts b/packages/open-next/src/overrides/converters/node.ts deleted file mode 100644 index f89bed98..00000000 --- a/packages/open-next/src/overrides/converters/node.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/converters/node.js"; -export * from "@opennextjs/core/overrides/converters/node.js"; diff --git a/packages/open-next/src/overrides/converters/sqs-revalidate.ts b/packages/open-next/src/overrides/converters/sqs-revalidate.ts index d4c2ee7a..4e476fab 100644 --- a/packages/open-next/src/overrides/converters/sqs-revalidate.ts +++ b/packages/open-next/src/overrides/converters/sqs-revalidate.ts @@ -1,8 +1,8 @@ import type { SQSEvent } from "aws-lambda"; -import type { Converter } from "@/types/overrides"; +import type { Converter } from "@opennextjs/core/types/overrides.js"; -import type { RevalidateEvent } from "../../adapters/revalidate"; +import type { RevalidateEvent } from "@opennextjs/core/adapters/revalidate.js"; const converter: Converter = { convertFrom(event: unknown) { diff --git a/packages/open-next/src/overrides/converters/utils.ts b/packages/open-next/src/overrides/converters/utils.ts deleted file mode 100644 index 6991cdb8..00000000 --- a/packages/open-next/src/overrides/converters/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/overrides/converters/utils.js"; diff --git a/packages/open-next/src/overrides/imageLoader/dummy.ts b/packages/open-next/src/overrides/imageLoader/dummy.ts deleted file mode 100644 index 7a776fba..00000000 --- a/packages/open-next/src/overrides/imageLoader/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/imageLoader/dummy.js"; -export * from "@opennextjs/core/overrides/imageLoader/dummy.js"; diff --git a/packages/open-next/src/overrides/imageLoader/fs-dev.ts b/packages/open-next/src/overrides/imageLoader/fs-dev.ts deleted file mode 100644 index 42fb4ef0..00000000 --- a/packages/open-next/src/overrides/imageLoader/fs-dev.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/imageLoader/fs-dev.js"; -export * from "@opennextjs/core/overrides/imageLoader/fs-dev.js"; diff --git a/packages/open-next/src/overrides/imageLoader/host.ts b/packages/open-next/src/overrides/imageLoader/host.ts deleted file mode 100644 index 9add6dfe..00000000 --- a/packages/open-next/src/overrides/imageLoader/host.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/imageLoader/host.js"; -export * from "@opennextjs/core/overrides/imageLoader/host.js"; diff --git a/packages/open-next/src/overrides/imageLoader/s3-lite.ts b/packages/open-next/src/overrides/imageLoader/s3-lite.ts index 23ec1a4a..e6dc9277 100644 --- a/packages/open-next/src/overrides/imageLoader/s3-lite.ts +++ b/packages/open-next/src/overrides/imageLoader/s3-lite.ts @@ -3,8 +3,8 @@ import type { ReadableStream } from "node:stream/web"; import { AwsClient } from "aws4fetch"; -import type { ImageLoader } from "@/types/overrides"; -import { FatalError } from "@/utils/error"; +import type { ImageLoader } from "@opennextjs/core/types/overrides.js"; +import { FatalError } from "@opennextjs/core/utils/error.js"; let awsClient: AwsClient | null = null; diff --git a/packages/open-next/src/overrides/imageLoader/s3.ts b/packages/open-next/src/overrides/imageLoader/s3.ts index 8471378a..3897078e 100644 --- a/packages/open-next/src/overrides/imageLoader/s3.ts +++ b/packages/open-next/src/overrides/imageLoader/s3.ts @@ -2,10 +2,10 @@ import type { Readable } from "node:stream"; import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; -import type { ImageLoader } from "@/types/overrides"; -import { FatalError } from "@/utils/error"; +import type { ImageLoader } from "@opennextjs/core/types/overrides.js"; +import { FatalError } from "@opennextjs/core/utils/error.js"; -import { awsLogger } from "../../adapters/logger"; +import { awsLogger } from "@opennextjs/core/adapters/logger.js"; const { BUCKET_NAME, BUCKET_KEY_PREFIX } = process.env; diff --git a/packages/open-next/src/overrides/incrementalCache/dummy.ts b/packages/open-next/src/overrides/incrementalCache/dummy.ts deleted file mode 100644 index abf5e4f9..00000000 --- a/packages/open-next/src/overrides/incrementalCache/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/incrementalCache/dummy.js"; -export * from "@opennextjs/core/overrides/incrementalCache/dummy.js"; diff --git a/packages/open-next/src/overrides/incrementalCache/fs-dev.ts b/packages/open-next/src/overrides/incrementalCache/fs-dev.ts deleted file mode 100644 index dd4da4f9..00000000 --- a/packages/open-next/src/overrides/incrementalCache/fs-dev.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/incrementalCache/fs-dev.js"; -export * from "@opennextjs/core/overrides/incrementalCache/fs-dev.js"; diff --git a/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts b/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts index d5e50707..c7a6f2fd 100644 --- a/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts +++ b/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts @@ -1,8 +1,8 @@ -import type { CacheEntryType, CacheValue, IncrementalCache } from "@/types/overrides"; -import { customFetchClient } from "@/utils/fetch"; -import { LRUCache } from "@/utils/lru"; +import type { CacheEntryType, CacheValue, IncrementalCache } from "@opennextjs/core/types/overrides.js"; +import { customFetchClient } from "../../utils/fetch.js"; +import { LRUCache } from "@opennextjs/core/utils/lru.js"; -import { debug } from "../../adapters/logger"; +import { debug } from "@opennextjs/core/adapters/logger.js"; import S3Cache, { getAwsClient } from "./s3-lite"; diff --git a/packages/open-next/src/overrides/incrementalCache/s3-lite.ts b/packages/open-next/src/overrides/incrementalCache/s3-lite.ts index 4b0b0def..9c4ccdbd 100644 --- a/packages/open-next/src/overrides/incrementalCache/s3-lite.ts +++ b/packages/open-next/src/overrides/incrementalCache/s3-lite.ts @@ -2,12 +2,12 @@ import path from "node:path"; import { AwsClient } from "aws4fetch"; -import type { Extension } from "@/types/cache"; -import type { CacheEntryType, CacheValue, IncrementalCache } from "@/types/overrides"; -import { IgnorableError, RecoverableError } from "@/utils/error"; -import { customFetchClient } from "@/utils/fetch"; +import type { Extension } from "@opennextjs/core/types/cache.js"; +import type { CacheEntryType, CacheValue, IncrementalCache } from "@opennextjs/core/types/overrides.js"; +import { IgnorableError, RecoverableError } from "@opennextjs/core/utils/error.js"; +import { customFetchClient } from "../../utils/fetch.js"; -import { parseNumberFromEnv } from "../../adapters/util"; +import { parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; let awsClient: AwsClient | null = null; diff --git a/packages/open-next/src/overrides/incrementalCache/s3.ts b/packages/open-next/src/overrides/incrementalCache/s3.ts index 01eafdaa..cf83fe57 100644 --- a/packages/open-next/src/overrides/incrementalCache/s3.ts +++ b/packages/open-next/src/overrides/incrementalCache/s3.ts @@ -3,11 +3,11 @@ import path from "node:path"; import type { S3ClientConfig } from "@aws-sdk/client-s3"; import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; -import type { Extension } from "@/types/cache"; -import type { IncrementalCache } from "@/types/overrides"; +import type { Extension } from "@opennextjs/core/types/cache.js"; +import type { IncrementalCache } from "@opennextjs/core/types/overrides.js"; -import { awsLogger } from "../../adapters/logger"; -import { parseNumberFromEnv } from "../../adapters/util"; +import { awsLogger } from "@opennextjs/core/adapters/logger.js"; +import { parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; const { CACHE_BUCKET_REGION, CACHE_BUCKET_KEY_PREFIX, NEXT_BUILD_ID, CACHE_BUCKET_NAME } = process.env; diff --git a/packages/open-next/src/overrides/originResolver/dummy.ts b/packages/open-next/src/overrides/originResolver/dummy.ts deleted file mode 100644 index 3cbc9d29..00000000 --- a/packages/open-next/src/overrides/originResolver/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/originResolver/dummy.js"; -export * from "@opennextjs/core/overrides/originResolver/dummy.js"; diff --git a/packages/open-next/src/overrides/originResolver/pattern-env.ts b/packages/open-next/src/overrides/originResolver/pattern-env.ts deleted file mode 100644 index e9644d32..00000000 --- a/packages/open-next/src/overrides/originResolver/pattern-env.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/originResolver/pattern-env.js"; -export * from "@opennextjs/core/overrides/originResolver/pattern-env.js"; diff --git a/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts b/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts deleted file mode 100644 index 49bcb948..00000000 --- a/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/proxyExternalRequest/dummy.js"; -export * from "@opennextjs/core/overrides/proxyExternalRequest/dummy.js"; diff --git a/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts b/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts deleted file mode 100644 index d2ff29ad..00000000 --- a/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/proxyExternalRequest/fetch.js"; -export * from "@opennextjs/core/overrides/proxyExternalRequest/fetch.js"; diff --git a/packages/open-next/src/overrides/proxyExternalRequest/node.ts b/packages/open-next/src/overrides/proxyExternalRequest/node.ts deleted file mode 100644 index 3f8ecdc3..00000000 --- a/packages/open-next/src/overrides/proxyExternalRequest/node.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/proxyExternalRequest/node.js"; -export * from "@opennextjs/core/overrides/proxyExternalRequest/node.js"; diff --git a/packages/open-next/src/overrides/queue/direct.ts b/packages/open-next/src/overrides/queue/direct.ts deleted file mode 100644 index 68470fda..00000000 --- a/packages/open-next/src/overrides/queue/direct.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/queue/direct.js"; -export * from "@opennextjs/core/overrides/queue/direct.js"; diff --git a/packages/open-next/src/overrides/queue/dummy.ts b/packages/open-next/src/overrides/queue/dummy.ts deleted file mode 100644 index 5d5d6a41..00000000 --- a/packages/open-next/src/overrides/queue/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/queue/dummy.js"; -export * from "@opennextjs/core/overrides/queue/dummy.js"; diff --git a/packages/open-next/src/overrides/queue/sqs-lite.ts b/packages/open-next/src/overrides/queue/sqs-lite.ts index d55a1e7f..c0fa2ca8 100644 --- a/packages/open-next/src/overrides/queue/sqs-lite.ts +++ b/packages/open-next/src/overrides/queue/sqs-lite.ts @@ -1,10 +1,10 @@ import { AwsClient } from "aws4fetch"; -import type { Queue } from "@/types/overrides"; -import { RecoverableError } from "@/utils/error"; -import { customFetchClient } from "@/utils/fetch"; +import type { Queue } from "@opennextjs/core/types/overrides.js"; +import { RecoverableError } from "@opennextjs/core/utils/error.js"; +import { customFetchClient } from "../../utils/fetch.js"; -import { error } from "../../adapters/logger"; +import { error } from "@opennextjs/core/adapters/logger.js"; let awsClient: AwsClient | null = null; diff --git a/packages/open-next/src/overrides/queue/sqs.ts b/packages/open-next/src/overrides/queue/sqs.ts index 156c14f7..28c6c7d4 100644 --- a/packages/open-next/src/overrides/queue/sqs.ts +++ b/packages/open-next/src/overrides/queue/sqs.ts @@ -1,8 +1,8 @@ import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; -import type { Queue } from "@/types/overrides"; +import type { Queue } from "@opennextjs/core/types/overrides.js"; -import { awsLogger } from "../../adapters/logger"; +import { awsLogger } from "@opennextjs/core/adapters/logger.js"; // Expected environment variables const { REVALIDATION_QUEUE_REGION, REVALIDATION_QUEUE_URL } = process.env; diff --git a/packages/open-next/src/overrides/tagCache/dummy.ts b/packages/open-next/src/overrides/tagCache/dummy.ts deleted file mode 100644 index 98349d03..00000000 --- a/packages/open-next/src/overrides/tagCache/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/tagCache/dummy.js"; -export * from "@opennextjs/core/overrides/tagCache/dummy.js"; diff --git a/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts b/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts index 8261484f..a64b8cdf 100644 --- a/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts +++ b/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts @@ -2,12 +2,12 @@ import path from "node:path"; import { AwsClient } from "aws4fetch"; -import type { OriginalTagCache } from "@/types/overrides"; -import { RecoverableError } from "@/utils/error"; -import { customFetchClient } from "@/utils/fetch"; +import type { OriginalTagCache } from "@opennextjs/core/types/overrides.js"; +import { RecoverableError } from "@opennextjs/core/utils/error.js"; +import { customFetchClient } from "../../utils/fetch.js"; -import { debug, error } from "../../adapters/logger"; -import { chunk, parseNumberFromEnv } from "../../adapters/util"; +import { debug, error } from "@opennextjs/core/adapters/logger.js"; +import { chunk, parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency } from "./constants"; diff --git a/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts b/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts index 93afbaf6..bdecf5d7 100644 --- a/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts +++ b/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts @@ -2,12 +2,12 @@ import path from "node:path"; import { AwsClient } from "aws4fetch"; -import type { NextModeTagCache } from "@/types/overrides"; -import { RecoverableError } from "@/utils/error"; -import { customFetchClient } from "@/utils/fetch"; +import type { NextModeTagCache } from "@opennextjs/core/types/overrides.js"; +import { RecoverableError } from "@opennextjs/core/utils/error.js"; +import { customFetchClient } from "../../utils/fetch.js"; -import { debug, error } from "../../adapters/logger"; -import { chunk, parseNumberFromEnv } from "../../adapters/util"; +import { debug, error } from "@opennextjs/core/adapters/logger.js"; +import { chunk, parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency } from "./constants"; diff --git a/packages/open-next/src/overrides/tagCache/dynamodb.ts b/packages/open-next/src/overrides/tagCache/dynamodb.ts index 8a69f64c..43d9da91 100644 --- a/packages/open-next/src/overrides/tagCache/dynamodb.ts +++ b/packages/open-next/src/overrides/tagCache/dynamodb.ts @@ -3,10 +3,10 @@ import path from "node:path"; import type { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb"; import { BatchWriteItemCommand, DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb"; -import type { TagCache } from "@/types/overrides"; +import type { TagCache } from "@opennextjs/core/types/overrides.js"; -import { awsLogger, debug, error } from "../../adapters/logger"; -import { chunk, parseNumberFromEnv } from "../../adapters/util"; +import { awsLogger, debug, error } from "@opennextjs/core/adapters/logger.js"; +import { chunk, parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency } from "./constants"; diff --git a/packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts b/packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts deleted file mode 100644 index f80feb09..00000000 --- a/packages/open-next/src/overrides/tagCache/fs-dev-nextMode.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/tagCache/fs-dev-nextMode.js"; -export * from "@opennextjs/core/overrides/tagCache/fs-dev-nextMode.js"; diff --git a/packages/open-next/src/overrides/tagCache/fs-dev.ts b/packages/open-next/src/overrides/tagCache/fs-dev.ts deleted file mode 100644 index 01def372..00000000 --- a/packages/open-next/src/overrides/tagCache/fs-dev.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/tagCache/fs-dev.js"; -export * from "@opennextjs/core/overrides/tagCache/fs-dev.js"; diff --git a/packages/open-next/src/overrides/warmer/aws-lambda.ts b/packages/open-next/src/overrides/warmer/aws-lambda.ts index 7fa1a8b2..d315763b 100644 --- a/packages/open-next/src/overrides/warmer/aws-lambda.ts +++ b/packages/open-next/src/overrides/warmer/aws-lambda.ts @@ -1,7 +1,7 @@ -import type { Warmer } from "@/types/overrides"; +import type { Warmer } from "@opennextjs/core/types/overrides.js"; -import { debug, error } from "../../adapters/logger"; -import type { WarmerEvent, WarmerResponse } from "../../adapters/warmer-function"; +import { debug, error } from "@opennextjs/core/adapters/logger.js"; +import type { WarmerEvent, WarmerResponse } from "@opennextjs/core/adapters/warmer-function.js"; const lambdaWarmerInvoke: Warmer = { name: "aws-invoke", diff --git a/packages/open-next/src/overrides/warmer/dummy.ts b/packages/open-next/src/overrides/warmer/dummy.ts deleted file mode 100644 index 9dda41e5..00000000 --- a/packages/open-next/src/overrides/warmer/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/warmer/dummy.js"; -export * from "@opennextjs/core/overrides/warmer/dummy.js"; diff --git a/packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts b/packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts index adc140aa..0265eceb 100644 --- a/packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts +++ b/packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts @@ -2,11 +2,11 @@ import { Readable, type Transform, Writable } from "node:stream"; import type { ReadableStream } from "node:stream/web"; import zlib from "node:zlib"; -import type { AwsLambdaEvent, AwsLambdaReturn } from "@/types/aws-lambda"; -import type { InternalResult, StreamCreator } from "@/types/open-next"; -import type { WrapperHandler } from "@/types/overrides"; +import type { AwsLambdaEvent, AwsLambdaReturn } from "../../types/aws-lambda.js"; +import type { InternalResult, StreamCreator } from "@opennextjs/core/types/open-next.js"; +import type { WrapperHandler } from "@opennextjs/core/types/overrides.js"; -import { error } from "../../adapters/logger"; +import { error } from "@opennextjs/core/adapters/logger.js"; import { formatWarmerResponse } from "./aws-lambda"; diff --git a/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts b/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts index 19765a2a..93ce098e 100644 --- a/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts +++ b/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts @@ -3,11 +3,11 @@ import zlib from "node:zlib"; import type { APIGatewayProxyEventV2 } from "aws-lambda"; -import type { StreamCreator } from "@/types/open-next"; -import type { Wrapper, WrapperHandler } from "@/types/overrides"; +import type { StreamCreator } from "@opennextjs/core/types/open-next.js"; +import type { Wrapper, WrapperHandler } from "@opennextjs/core/types/overrides.js"; -import { debug, error } from "../../adapters/logger"; -import type { WarmerEvent, WarmerResponse } from "../../adapters/warmer-function"; +import { debug, error } from "@opennextjs/core/adapters/logger.js"; +import type { WarmerEvent, WarmerResponse } from "@opennextjs/core/adapters/warmer-function.js"; type AwsLambdaEvent = APIGatewayProxyEventV2 | WarmerEvent; diff --git a/packages/open-next/src/overrides/wrappers/aws-lambda.ts b/packages/open-next/src/overrides/wrappers/aws-lambda.ts index a37827db..3be89bbb 100644 --- a/packages/open-next/src/overrides/wrappers/aws-lambda.ts +++ b/packages/open-next/src/overrides/wrappers/aws-lambda.ts @@ -1,10 +1,10 @@ import { Writable } from "node:stream"; -import type { AwsLambdaEvent, AwsLambdaReturn } from "@/types/aws-lambda"; -import type { StreamCreator } from "@/types/open-next"; -import type { WrapperHandler } from "@/types/overrides"; +import type { AwsLambdaEvent, AwsLambdaReturn } from "../../types/aws-lambda.js"; +import type { StreamCreator } from "@opennextjs/core/types/open-next.js"; +import type { WrapperHandler } from "@opennextjs/core/types/overrides.js"; -import type { WarmerEvent, WarmerResponse } from "../../adapters/warmer-function"; +import type { WarmerEvent, WarmerResponse } from "@opennextjs/core/adapters/warmer-function.js"; export function formatWarmerResponse(event: WarmerEvent) { return new Promise((resolve) => { diff --git a/packages/open-next/src/overrides/wrappers/cloudflare-edge.ts b/packages/open-next/src/overrides/wrappers/cloudflare-edge.ts deleted file mode 100644 index 98d62b29..00000000 --- a/packages/open-next/src/overrides/wrappers/cloudflare-edge.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/wrappers/cloudflare-edge.js"; -export * from "@opennextjs/core/overrides/wrappers/cloudflare-edge.js"; diff --git a/packages/open-next/src/overrides/wrappers/cloudflare-node.ts b/packages/open-next/src/overrides/wrappers/cloudflare-node.ts deleted file mode 100644 index cdadcc5b..00000000 --- a/packages/open-next/src/overrides/wrappers/cloudflare-node.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/wrappers/cloudflare-node.js"; -export * from "@opennextjs/core/overrides/wrappers/cloudflare-node.js"; diff --git a/packages/open-next/src/overrides/wrappers/dummy.ts b/packages/open-next/src/overrides/wrappers/dummy.ts deleted file mode 100644 index 1178320a..00000000 --- a/packages/open-next/src/overrides/wrappers/dummy.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/wrappers/dummy.js"; -export * from "@opennextjs/core/overrides/wrappers/dummy.js"; diff --git a/packages/open-next/src/overrides/wrappers/express-dev.ts b/packages/open-next/src/overrides/wrappers/express-dev.ts deleted file mode 100644 index 1238bd8f..00000000 --- a/packages/open-next/src/overrides/wrappers/express-dev.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/wrappers/express-dev.js"; -export * from "@opennextjs/core/overrides/wrappers/express-dev.js"; diff --git a/packages/open-next/src/overrides/wrappers/node.ts b/packages/open-next/src/overrides/wrappers/node.ts deleted file mode 100644 index 13986fd2..00000000 --- a/packages/open-next/src/overrides/wrappers/node.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "@opennextjs/core/overrides/wrappers/node.js"; -export * from "@opennextjs/core/overrides/wrappers/node.js"; diff --git a/packages/open-next/src/plugins/content-updater.ts b/packages/open-next/src/plugins/content-updater.ts deleted file mode 100644 index 1a0aa220..00000000 --- a/packages/open-next/src/plugins/content-updater.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/plugins/content-updater.js"; diff --git a/packages/open-next/src/plugins/edge.ts b/packages/open-next/src/plugins/edge.ts deleted file mode 100644 index ef5e044f..00000000 --- a/packages/open-next/src/plugins/edge.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/plugins/edge.js"; diff --git a/packages/open-next/src/plugins/externalMiddleware.ts b/packages/open-next/src/plugins/externalMiddleware.ts deleted file mode 100644 index b76638d1..00000000 --- a/packages/open-next/src/plugins/externalMiddleware.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/plugins/externalMiddleware.js"; diff --git a/packages/open-next/src/plugins/inline-require-resolve.ts b/packages/open-next/src/plugins/inline-require-resolve.ts deleted file mode 100644 index 762f8cad..00000000 --- a/packages/open-next/src/plugins/inline-require-resolve.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/plugins/inline-require-resolve.js"; diff --git a/packages/open-next/src/plugins/inlineRouteHandlers.ts b/packages/open-next/src/plugins/inlineRouteHandlers.ts deleted file mode 100644 index fc21a061..00000000 --- a/packages/open-next/src/plugins/inlineRouteHandlers.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/plugins/inlineRouteHandlers.js"; diff --git a/packages/open-next/src/plugins/replacement.ts b/packages/open-next/src/plugins/replacement.ts deleted file mode 100644 index c16d8a40..00000000 --- a/packages/open-next/src/plugins/replacement.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/plugins/replacement.js"; diff --git a/packages/open-next/src/plugins/resolve.ts b/packages/open-next/src/plugins/resolve.ts deleted file mode 100644 index a8e4bbaa..00000000 --- a/packages/open-next/src/plugins/resolve.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/plugins/resolve.js"; diff --git a/packages/open-next/src/types/aws-lambda.ts b/packages/open-next/src/types/aws-lambda.ts index a44372dc..c680a1b5 100644 --- a/packages/open-next/src/types/aws-lambda.ts +++ b/packages/open-next/src/types/aws-lambda.ts @@ -10,7 +10,7 @@ import type { Context, } from "aws-lambda"; -import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function"; +import type { WarmerEvent, WarmerResponse } from "@opennextjs/core/adapters/warmer-function.js"; export interface ResponseStream extends Writable { getBufferedData(): Buffer; diff --git a/packages/open-next/src/types/cache.ts b/packages/open-next/src/types/cache.ts deleted file mode 100644 index 2ca89304..00000000 --- a/packages/open-next/src/types/cache.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/types/cache.js"; diff --git a/packages/open-next/src/types/next-types.ts b/packages/open-next/src/types/next-types.ts deleted file mode 100644 index 97b81b80..00000000 --- a/packages/open-next/src/types/next-types.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/types/next-types.js"; diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts deleted file mode 100644 index 2f4b49cb..00000000 --- a/packages/open-next/src/types/open-next.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/types/open-next.js"; diff --git a/packages/open-next/src/types/overrides.ts b/packages/open-next/src/types/overrides.ts deleted file mode 100644 index a6574921..00000000 --- a/packages/open-next/src/types/overrides.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/types/overrides.js"; diff --git a/packages/open-next/src/utils/binary.ts b/packages/open-next/src/utils/binary.ts deleted file mode 100644 index d06adf6b..00000000 --- a/packages/open-next/src/utils/binary.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/binary.js"; diff --git a/packages/open-next/src/utils/cache.ts b/packages/open-next/src/utils/cache.ts deleted file mode 100644 index 3fc74eb0..00000000 --- a/packages/open-next/src/utils/cache.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/cache.js"; diff --git a/packages/open-next/src/utils/error.ts b/packages/open-next/src/utils/error.ts deleted file mode 100644 index d10fd395..00000000 --- a/packages/open-next/src/utils/error.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/error.js"; diff --git a/packages/open-next/src/utils/lru.ts b/packages/open-next/src/utils/lru.ts deleted file mode 100644 index c9e3b215..00000000 --- a/packages/open-next/src/utils/lru.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/lru.js"; diff --git a/packages/open-next/src/utils/normalize-path.ts b/packages/open-next/src/utils/normalize-path.ts deleted file mode 100644 index faebc369..00000000 --- a/packages/open-next/src/utils/normalize-path.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/normalize-path.js"; diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts deleted file mode 100644 index 60ddba68..00000000 --- a/packages/open-next/src/utils/promise.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/promise.js"; diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts deleted file mode 100644 index 51111cac..00000000 --- a/packages/open-next/src/utils/regex.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/regex.js"; diff --git a/packages/open-next/src/utils/safe-json-parse.ts b/packages/open-next/src/utils/safe-json-parse.ts deleted file mode 100644 index 2a569ea5..00000000 --- a/packages/open-next/src/utils/safe-json-parse.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/safe-json-parse.js"; diff --git a/packages/open-next/src/utils/stream.ts b/packages/open-next/src/utils/stream.ts deleted file mode 100644 index b85355fb..00000000 --- a/packages/open-next/src/utils/stream.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@opennextjs/core/utils/stream.js"; diff --git a/packages/open-next/tsconfig.json b/packages/open-next/tsconfig.json index 67b691f0..6c5a4c3e 100644 --- a/packages/open-next/tsconfig.json +++ b/packages/open-next/tsconfig.json @@ -6,11 +6,6 @@ "lib": ["DOM", "ESNext"], "outDir": "./dist", "allowSyntheticDefaultImports": true, - "paths": { - "@/types/*": ["./src/types/*"], - "@/config/*": ["./src/adapters/config/*"], - "@/http/*": ["./src/http/*"], - "@/utils/*": ["./src/utils/*"] - } + "paths": {} } } diff --git a/packages/tests-unit/tests/adapters/cache.test.ts b/packages/tests-unit/tests/adapters/cache.test.ts index fa739380..347f35c1 100644 --- a/packages/tests-unit/tests/adapters/cache.test.ts +++ b/packages/tests-unit/tests/adapters/cache.test.ts @@ -1,4 +1,4 @@ -import Cache, { SOFT_TAG_PREFIX } from "@opennextjs/aws/adapters/cache.js"; +import Cache, { SOFT_TAG_PREFIX } from "@opennextjs/core/adapters/cache.js"; import { type Mock, vi } from "vitest"; declare global { diff --git a/packages/tests-unit/tests/adapters/composable-cache.test.ts b/packages/tests-unit/tests/adapters/composable-cache.test.ts index 6176a23b..35b723b4 100644 --- a/packages/tests-unit/tests/adapters/composable-cache.test.ts +++ b/packages/tests-unit/tests/adapters/composable-cache.test.ts @@ -1,5 +1,5 @@ -import ComposableCache from "@opennextjs/aws/adapters/composable-cache"; -import { fromReadableStream, toReadableStream } from "@opennextjs/aws/utils/stream"; +import ComposableCache from "@opennextjs/core/adapters/composable-cache"; +import { fromReadableStream, toReadableStream } from "@opennextjs/core/utils/stream"; import { vi } from "vitest"; describe("Composable cache handler", () => { diff --git a/packages/tests-unit/tests/adapters/logger.test.ts b/packages/tests-unit/tests/adapters/logger.test.ts index d0d25b3d..addc3854 100644 --- a/packages/tests-unit/tests/adapters/logger.test.ts +++ b/packages/tests-unit/tests/adapters/logger.test.ts @@ -1,5 +1,5 @@ -import * as logger from "@opennextjs/aws/adapters/logger.js"; -import { FatalError, IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js"; +import * as logger from "@opennextjs/core/adapters/logger.js"; +import { FatalError, IgnorableError, RecoverableError } from "@opennextjs/core/utils/error.js"; import { vi } from "vitest"; describe("logger adapter", () => { diff --git a/packages/tests-unit/tests/binary.test.ts b/packages/tests-unit/tests/binary.test.ts index 805ef71f..3f57a688 100644 --- a/packages/tests-unit/tests/binary.test.ts +++ b/packages/tests-unit/tests/binary.test.ts @@ -1,4 +1,4 @@ -import { isBinaryContentType } from "@opennextjs/aws/utils/binary.js"; +import { isBinaryContentType } from "@opennextjs/core/utils/binary.js"; describe("isBinaryContentType", () => { const tests = [ diff --git a/packages/tests-unit/tests/build/copyTracedFiles.test.ts b/packages/tests-unit/tests/build/copyTracedFiles.test.ts index 9545d65a..92d91577 100644 --- a/packages/tests-unit/tests/build/copyTracedFiles.test.ts +++ b/packages/tests-unit/tests/build/copyTracedFiles.test.ts @@ -1,4 +1,4 @@ -import { isExcluded, isNonLinuxPlatformPackage } from "@opennextjs/aws/build/copyTracedFiles.js"; +import { isExcluded, isNonLinuxPlatformPackage } from "@opennextjs/core/build/copyTracedFiles.js"; describe("isExcluded", () => { test("should exclude sharp", () => { diff --git a/packages/tests-unit/tests/build/helper.test.ts b/packages/tests-unit/tests/build/helper.test.ts index c70d85c2..283b2548 100644 --- a/packages/tests-unit/tests/build/helper.test.ts +++ b/packages/tests-unit/tests/build/helper.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; -import { compareSemver, findNextConfig } from "@opennextjs/aws/build/helper.js"; +import { compareSemver, findNextConfig } from "@opennextjs/core/build/helper.js"; import { vi } from "vitest"; vi.mock("node:fs"); diff --git a/packages/tests-unit/tests/build/patch/codePatcher.test.ts b/packages/tests-unit/tests/build/patch/codePatcher.test.ts index 4eb28337..489cc198 100644 --- a/packages/tests-unit/tests/build/patch/codePatcher.test.ts +++ b/packages/tests-unit/tests/build/patch/codePatcher.test.ts @@ -1,4 +1,4 @@ -import { isVersionInRange, parseVersions } from "@opennextjs/aws/build/patch/codePatcher.js"; +import { isVersionInRange, parseVersions } from "@opennextjs/core/build/patch/codePatcher.js"; describe("isVersionInRange", () => { test("before", () => { diff --git a/packages/tests-unit/tests/build/patch/patches/patchBackgroundRevalidation.test.ts b/packages/tests-unit/tests/build/patch/patches/patchBackgroundRevalidation.test.ts index b5dfbddf..bb738e5b 100644 --- a/packages/tests-unit/tests/build/patch/patches/patchBackgroundRevalidation.test.ts +++ b/packages/tests-unit/tests/build/patch/patches/patchBackgroundRevalidation.test.ts @@ -1,5 +1,5 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import { rule } from "@opennextjs/aws/build/patch/patches/patchBackgroundRevalidation.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import { rule } from "@opennextjs/core/build/patch/patches/patchBackgroundRevalidation.js"; import { describe, it } from "vitest"; const codeToPatch = `if (cachedResponse && !isOnDemandRevalidate) { diff --git a/packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts b/packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts index 57d23ba8..ba9f62c3 100644 --- a/packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts +++ b/packages/tests-unit/tests/build/patch/patches/patchEnvVars.test.ts @@ -1,5 +1,5 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import { envVarRuleCreator } from "@opennextjs/aws/build/patch/patches/patchEnvVar.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import { envVarRuleCreator } from "@opennextjs/core/build/patch/patches/patchEnvVar.js"; import { describe, it } from "vitest"; const moduleCompiledCode = ` diff --git a/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts index c692b137..e0bd6c5a 100644 --- a/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts +++ b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts @@ -1,9 +1,9 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { fetchRule, unstable_cacheRule, useCacheRule, -} from "@opennextjs/aws/build/patch/patches/patchFetchCacheISR.js"; +} from "@opennextjs/core/build/patch/patches/patchFetchCacheISR.js"; import { describe } from "vitest"; const unstable_cacheCode = ` diff --git a/packages/tests-unit/tests/build/patch/patches/patchFetchCacheWaitUntil.test.ts b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheWaitUntil.test.ts index 80356004..5e3c63d0 100644 --- a/packages/tests-unit/tests/build/patch/patches/patchFetchCacheWaitUntil.test.ts +++ b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheWaitUntil.test.ts @@ -1,5 +1,5 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; -import { rule } from "@opennextjs/aws/build/patch/patches/patchFetchCacheWaitUntil.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; +import { rule } from "@opennextjs/core/build/patch/patches/patchFetchCacheWaitUntil.js"; import { describe, expect, test } from "vitest"; import { computePatchDiff } from "./util.js"; diff --git a/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts b/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts index 5db663bc..4250ec91 100644 --- a/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts +++ b/packages/tests-unit/tests/build/patch/patches/patchNextServer.test.ts @@ -1,10 +1,10 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { createEmptyBodyRule, disablePreloadingRule, emptyHandleNextImageRequestRule, removeMiddlewareManifestRule, -} from "@opennextjs/aws/build/patch/patches/patchNextServer.js"; +} from "@opennextjs/core/build/patch/patches/patchNextServer.js"; import { describe, it } from "vitest"; import { computePatchDiff } from "./util.js"; diff --git a/packages/tests-unit/tests/build/patch/patches/patchNodeEnvironment.test.ts b/packages/tests-unit/tests/build/patch/patches/patchNodeEnvironment.test.ts index e352bf3d..bc563357 100644 --- a/packages/tests-unit/tests/build/patch/patches/patchNodeEnvironment.test.ts +++ b/packages/tests-unit/tests/build/patch/patches/patchNodeEnvironment.test.ts @@ -1,4 +1,4 @@ -import { rule } from "@opennextjs/aws/build/patch/patches/patchNodeEnvironment.js"; +import { rule } from "@opennextjs/core/build/patch/patches/patchNodeEnvironment.js"; import { computePatchDiff } from "./util.js"; diff --git a/packages/tests-unit/tests/build/patch/patches/util.ts b/packages/tests-unit/tests/build/patch/patches/util.ts index 1a9602fd..c4ad51c2 100644 --- a/packages/tests-unit/tests/build/patch/patches/util.ts +++ b/packages/tests-unit/tests/build/patch/patches/util.ts @@ -1,4 +1,4 @@ -import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js"; +import { patchCode } from "@opennextjs/core/build/patch/astCodePatcher.js"; import { createPatch } from "diff"; /** diff --git a/packages/tests-unit/tests/converters/node.test.ts b/packages/tests-unit/tests/converters/node.test.ts index c1705ef3..aa80a877 100644 --- a/packages/tests-unit/tests/converters/node.test.ts +++ b/packages/tests-unit/tests/converters/node.test.ts @@ -1,5 +1,5 @@ -import { IncomingMessage } from "@opennextjs/aws/http/request.js"; -import converter from "@opennextjs/aws/overrides/converters/node.js"; +import { IncomingMessage } from "@opennextjs/core/http/request.js"; +import converter from "@opennextjs/core/overrides/converters/node.js"; describe("convertFrom", () => { it("should convert GET request", async () => { diff --git a/packages/tests-unit/tests/converters/utils.test.ts b/packages/tests-unit/tests/converters/utils.test.ts index d0eca1fc..1d59ec8e 100644 --- a/packages/tests-unit/tests/converters/utils.test.ts +++ b/packages/tests-unit/tests/converters/utils.test.ts @@ -1,4 +1,4 @@ -import { removeUndefinedFromQuery } from "@opennextjs/aws/overrides/converters/utils.js"; +import { removeUndefinedFromQuery } from "@opennextjs/core/overrides/converters/utils.js"; describe("removeUndefinedFromQuery", () => { it("should remove undefined from query", () => { diff --git a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts index d05749b6..4b2475a7 100644 --- a/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts +++ b/packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts @@ -1,8 +1,8 @@ -import { cacheInterceptor } from "@opennextjs/aws/core/routing/cacheInterceptor.js"; -import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js"; -import type { MiddlewareEvent } from "@opennextjs/aws/types/open-next.js"; -import type { Queue } from "@opennextjs/aws/types/overrides.js"; -import { fromReadableStream } from "@opennextjs/aws/utils/stream.js"; +import { cacheInterceptor } from "@opennextjs/core/core/routing/cacheInterceptor.js"; +import { convertFromQueryString } from "@opennextjs/core/core/routing/util.js"; +import type { MiddlewareEvent } from "@opennextjs/core/types/open-next.js"; +import type { Queue } from "@opennextjs/core/types/overrides.js"; +import { fromReadableStream } from "@opennextjs/core/utils/stream.js"; import { vi } from "vitest"; vi.mock("@/config/index.js", () => ({ @@ -24,7 +24,7 @@ vi.mock("@/config/index.js", () => ({ }, })); -vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({ +vi.mock("@opennextjs/core/core/routing/i18n/index.js", () => ({ localizePath: (event: MiddlewareEvent) => event.rawPath, })); diff --git a/packages/tests-unit/tests/core/routing/i18n.test.ts b/packages/tests-unit/tests/core/routing/i18n.test.ts index fc61bd20..52695d15 100644 --- a/packages/tests-unit/tests/core/routing/i18n.test.ts +++ b/packages/tests-unit/tests/core/routing/i18n.test.ts @@ -1,7 +1,7 @@ import { NextConfig } from "@/config/index.js"; -import { handleLocaleRedirect, localizePath } from "@opennextjs/aws/core/routing/i18n/index.js"; -import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js"; -import type { InternalEvent } from "@opennextjs/aws/types/open-next.js"; +import { handleLocaleRedirect, localizePath } from "@opennextjs/core/core/routing/i18n/index.js"; +import { convertFromQueryString } from "@opennextjs/core/core/routing/util.js"; +import type { InternalEvent } from "@opennextjs/core/types/open-next.js"; import { expect, vi } from "vitest"; vi.mock("@/config/index.js", () => { diff --git a/packages/tests-unit/tests/core/routing/matcher.test.ts b/packages/tests-unit/tests/core/routing/matcher.test.ts index afb337eb..70870a95 100644 --- a/packages/tests-unit/tests/core/routing/matcher.test.ts +++ b/packages/tests-unit/tests/core/routing/matcher.test.ts @@ -4,9 +4,9 @@ import { getNextConfigHeaders, handleRedirects, handleRewrites, -} from "@opennextjs/aws/core/routing/matcher.js"; -import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js"; -import type { InternalEvent } from "@opennextjs/aws/types/open-next.js"; +} from "@opennextjs/core/core/routing/matcher.js"; +import { convertFromQueryString } from "@opennextjs/core/core/routing/util.js"; +import type { InternalEvent } from "@opennextjs/core/types/open-next.js"; import { vi } from "vitest"; vi.mock("@/config/index.js", () => ({ @@ -82,7 +82,7 @@ vi.mock("@/config/index.js", () => ({ }, })); -vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({ +vi.mock("@opennextjs/core/core/routing/i18n/index.js", () => ({ localizePath: (event: InternalEvent) => event.rawPath, handleLocaleRedirect: (_event: InternalEvent) => false, })); diff --git a/packages/tests-unit/tests/core/routing/middleware.test.ts b/packages/tests-unit/tests/core/routing/middleware.test.ts index 1e4edb7e..4302e390 100644 --- a/packages/tests-unit/tests/core/routing/middleware.test.ts +++ b/packages/tests-unit/tests/core/routing/middleware.test.ts @@ -1,7 +1,7 @@ -import { handleMiddleware } from "@opennextjs/aws/core/routing/middleware.js"; -import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js"; -import type { InternalEvent } from "@opennextjs/aws/types/open-next.js"; -import { toReadableStream } from "@opennextjs/aws/utils/stream.js"; +import { handleMiddleware } from "@opennextjs/core/core/routing/middleware.js"; +import { convertFromQueryString } from "@opennextjs/core/core/routing/util.js"; +import type { InternalEvent } from "@opennextjs/core/types/open-next.js"; +import { toReadableStream } from "@opennextjs/core/utils/stream.js"; import { vi } from "vitest"; vi.mock("@/config/index.js", () => ({ @@ -35,7 +35,7 @@ vi.mock("@/config/index.js", () => ({ }, })); -vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({ +vi.mock("@opennextjs/core/core/routing/i18n/index.js", () => ({ localizePath: (event: InternalEvent) => event.rawPath, })); diff --git a/packages/tests-unit/tests/core/routing/routeMatcher.test.ts b/packages/tests-unit/tests/core/routing/routeMatcher.test.ts index 2c6d8e86..f7b86b00 100644 --- a/packages/tests-unit/tests/core/routing/routeMatcher.test.ts +++ b/packages/tests-unit/tests/core/routing/routeMatcher.test.ts @@ -1,4 +1,4 @@ -import { dynamicRouteMatcher, staticRouteMatcher } from "@opennextjs/aws/core/routing/routeMatcher.js"; +import { dynamicRouteMatcher, staticRouteMatcher } from "@opennextjs/core/core/routing/routeMatcher.js"; import { vi } from "vitest"; vi.mock("@/config/index.js", () => ({ diff --git a/packages/tests-unit/tests/core/routing/util.test.ts b/packages/tests-unit/tests/core/routing/util.test.ts index d9f73696..b4be10b3 100644 --- a/packages/tests-unit/tests/core/routing/util.test.ts +++ b/packages/tests-unit/tests/core/routing/util.test.ts @@ -19,8 +19,8 @@ import { normalizeLocationHeader, revalidateIfRequired, unescapeRegex, -} from "@opennextjs/aws/core/routing/util.js"; -import { fromReadableStream } from "@opennextjs/aws/utils/stream.js"; +} from "@opennextjs/core/core/routing/util.js"; +import { fromReadableStream } from "@opennextjs/core/utils/stream.js"; import { vi } from "vitest"; vi.mock("@/config/index.js", () => ({ diff --git a/packages/tests-unit/tests/http/utils.test.ts b/packages/tests-unit/tests/http/utils.test.ts index b38b808a..53519292 100644 --- a/packages/tests-unit/tests/http/utils.test.ts +++ b/packages/tests-unit/tests/http/utils.test.ts @@ -1,6 +1,6 @@ import type http from "node:http"; -import { parseHeaders, parseSetCookieHeader } from "@opennextjs/aws/http/util.js"; +import { parseHeaders, parseSetCookieHeader } from "@opennextjs/core/http/util.js"; describe("parseSetCookieHeader", () => { it("returns an empty list if cookies is emptyish", () => { diff --git a/packages/tests-unit/tests/overrides/proxyExternalRequest/fetch.test.ts b/packages/tests-unit/tests/overrides/proxyExternalRequest/fetch.test.ts index 1e0f27a8..69dc1820 100644 --- a/packages/tests-unit/tests/overrides/proxyExternalRequest/fetch.test.ts +++ b/packages/tests-unit/tests/overrides/proxyExternalRequest/fetch.test.ts @@ -1,4 +1,4 @@ -import fetchProxy from "@opennextjs/aws/overrides/proxyExternalRequest/fetch.js"; +import fetchProxy from "@opennextjs/core/overrides/proxyExternalRequest/fetch.js"; import { vi } from "vitest"; describe("proxyExternalRequest/fetch", () => { diff --git a/packages/tests-unit/tests/utils/regex.test.ts b/packages/tests-unit/tests/utils/regex.test.ts index c6c8144e..e11ccd84 100644 --- a/packages/tests-unit/tests/utils/regex.test.ts +++ b/packages/tests-unit/tests/utils/regex.test.ts @@ -1,4 +1,4 @@ -import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; +import { getCrossPlatformPathRegex } from "@opennextjs/core/utils/regex.js"; const specialChars = "^([123]+|[123]{1,3})*\\?$"; From df048fe28d3d362acab15e3ad118c93383b2ffdd Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 09:17:48 +0200 Subject: [PATCH 04/24] rename to aws --- packages/{open-next => aws}/CHANGELOG.md | 0 packages/{open-next => aws}/package.json | 0 packages/{open-next => aws}/src/adapter.ts | 0 .../src/adapters/dynamo-provider.ts | 0 packages/{open-next => aws}/src/build.ts | 0 packages/{open-next => aws}/src/index.ts | 0 .../src/overrides/cdnInvalidation/cloudfront.ts | 0 .../src/overrides/converters/aws-apigw-v1.ts | 0 .../src/overrides/converters/aws-apigw-v2.ts | 0 .../src/overrides/converters/aws-cloudfront.ts | 0 .../src/overrides/converters/sqs-revalidate.ts | 0 .../src/overrides/imageLoader/s3-lite.ts | 0 .../src/overrides/imageLoader/s3.ts | 0 .../overrides/incrementalCache/multi-tier-ddb-s3.ts | 6 +++--- .../src/overrides/incrementalCache/s3-lite.ts | 0 .../src/overrides/incrementalCache/s3.ts | 0 .../src/overrides/queue/sqs-lite.ts | 0 .../{open-next => aws}/src/overrides/queue/sqs.ts | 0 .../src/overrides/tagCache/constants.ts | 0 .../src/overrides/tagCache/dynamodb-lite.ts | 11 +++++------ .../src/overrides/tagCache/dynamodb-nextMode.ts | 11 +++++------ .../src/overrides/tagCache/dynamodb.ts | 0 .../src/overrides/warmer/aws-lambda.ts | 0 .../src/overrides/wrappers/aws-lambda-compressed.ts | 6 +++--- .../src/overrides/wrappers/aws-lambda-streaming.ts | 0 .../src/overrides/wrappers/aws-lambda.ts | 0 packages/{open-next => aws}/src/types/aws-lambda.ts | 0 packages/{open-next => aws}/src/types/global.ts | 0 packages/{open-next => aws}/src/utils/fetch.ts | 0 packages/{open-next => aws}/tsconfig.json | 0 30 files changed, 16 insertions(+), 18 deletions(-) rename packages/{open-next => aws}/CHANGELOG.md (100%) rename packages/{open-next => aws}/package.json (100%) rename packages/{open-next => aws}/src/adapter.ts (100%) rename packages/{open-next => aws}/src/adapters/dynamo-provider.ts (100%) rename packages/{open-next => aws}/src/build.ts (100%) rename packages/{open-next => aws}/src/index.ts (100%) rename packages/{open-next => aws}/src/overrides/cdnInvalidation/cloudfront.ts (100%) rename packages/{open-next => aws}/src/overrides/converters/aws-apigw-v1.ts (100%) rename packages/{open-next => aws}/src/overrides/converters/aws-apigw-v2.ts (100%) rename packages/{open-next => aws}/src/overrides/converters/aws-cloudfront.ts (100%) rename packages/{open-next => aws}/src/overrides/converters/sqs-revalidate.ts (100%) rename packages/{open-next => aws}/src/overrides/imageLoader/s3-lite.ts (100%) rename packages/{open-next => aws}/src/overrides/imageLoader/s3.ts (100%) rename packages/{open-next => aws}/src/overrides/incrementalCache/multi-tier-ddb-s3.ts (98%) rename packages/{open-next => aws}/src/overrides/incrementalCache/s3-lite.ts (100%) rename packages/{open-next => aws}/src/overrides/incrementalCache/s3.ts (100%) rename packages/{open-next => aws}/src/overrides/queue/sqs-lite.ts (100%) rename packages/{open-next => aws}/src/overrides/queue/sqs.ts (100%) rename packages/{open-next => aws}/src/overrides/tagCache/constants.ts (100%) rename packages/{open-next => aws}/src/overrides/tagCache/dynamodb-lite.ts (99%) rename packages/{open-next => aws}/src/overrides/tagCache/dynamodb-nextMode.ts (99%) rename packages/{open-next => aws}/src/overrides/tagCache/dynamodb.ts (100%) rename packages/{open-next => aws}/src/overrides/warmer/aws-lambda.ts (100%) rename packages/{open-next => aws}/src/overrides/wrappers/aws-lambda-compressed.ts (98%) rename packages/{open-next => aws}/src/overrides/wrappers/aws-lambda-streaming.ts (100%) rename packages/{open-next => aws}/src/overrides/wrappers/aws-lambda.ts (100%) rename packages/{open-next => aws}/src/types/aws-lambda.ts (100%) rename packages/{open-next => aws}/src/types/global.ts (100%) rename packages/{open-next => aws}/src/utils/fetch.ts (100%) rename packages/{open-next => aws}/tsconfig.json (100%) diff --git a/packages/open-next/CHANGELOG.md b/packages/aws/CHANGELOG.md similarity index 100% rename from packages/open-next/CHANGELOG.md rename to packages/aws/CHANGELOG.md diff --git a/packages/open-next/package.json b/packages/aws/package.json similarity index 100% rename from packages/open-next/package.json rename to packages/aws/package.json diff --git a/packages/open-next/src/adapter.ts b/packages/aws/src/adapter.ts similarity index 100% rename from packages/open-next/src/adapter.ts rename to packages/aws/src/adapter.ts diff --git a/packages/open-next/src/adapters/dynamo-provider.ts b/packages/aws/src/adapters/dynamo-provider.ts similarity index 100% rename from packages/open-next/src/adapters/dynamo-provider.ts rename to packages/aws/src/adapters/dynamo-provider.ts diff --git a/packages/open-next/src/build.ts b/packages/aws/src/build.ts similarity index 100% rename from packages/open-next/src/build.ts rename to packages/aws/src/build.ts diff --git a/packages/open-next/src/index.ts b/packages/aws/src/index.ts similarity index 100% rename from packages/open-next/src/index.ts rename to packages/aws/src/index.ts diff --git a/packages/open-next/src/overrides/cdnInvalidation/cloudfront.ts b/packages/aws/src/overrides/cdnInvalidation/cloudfront.ts similarity index 100% rename from packages/open-next/src/overrides/cdnInvalidation/cloudfront.ts rename to packages/aws/src/overrides/cdnInvalidation/cloudfront.ts diff --git a/packages/open-next/src/overrides/converters/aws-apigw-v1.ts b/packages/aws/src/overrides/converters/aws-apigw-v1.ts similarity index 100% rename from packages/open-next/src/overrides/converters/aws-apigw-v1.ts rename to packages/aws/src/overrides/converters/aws-apigw-v1.ts diff --git a/packages/open-next/src/overrides/converters/aws-apigw-v2.ts b/packages/aws/src/overrides/converters/aws-apigw-v2.ts similarity index 100% rename from packages/open-next/src/overrides/converters/aws-apigw-v2.ts rename to packages/aws/src/overrides/converters/aws-apigw-v2.ts diff --git a/packages/open-next/src/overrides/converters/aws-cloudfront.ts b/packages/aws/src/overrides/converters/aws-cloudfront.ts similarity index 100% rename from packages/open-next/src/overrides/converters/aws-cloudfront.ts rename to packages/aws/src/overrides/converters/aws-cloudfront.ts diff --git a/packages/open-next/src/overrides/converters/sqs-revalidate.ts b/packages/aws/src/overrides/converters/sqs-revalidate.ts similarity index 100% rename from packages/open-next/src/overrides/converters/sqs-revalidate.ts rename to packages/aws/src/overrides/converters/sqs-revalidate.ts diff --git a/packages/open-next/src/overrides/imageLoader/s3-lite.ts b/packages/aws/src/overrides/imageLoader/s3-lite.ts similarity index 100% rename from packages/open-next/src/overrides/imageLoader/s3-lite.ts rename to packages/aws/src/overrides/imageLoader/s3-lite.ts diff --git a/packages/open-next/src/overrides/imageLoader/s3.ts b/packages/aws/src/overrides/imageLoader/s3.ts similarity index 100% rename from packages/open-next/src/overrides/imageLoader/s3.ts rename to packages/aws/src/overrides/imageLoader/s3.ts diff --git a/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts b/packages/aws/src/overrides/incrementalCache/multi-tier-ddb-s3.ts similarity index 98% rename from packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts rename to packages/aws/src/overrides/incrementalCache/multi-tier-ddb-s3.ts index c7a6f2fd..5e9b7288 100644 --- a/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts +++ b/packages/aws/src/overrides/incrementalCache/multi-tier-ddb-s3.ts @@ -1,10 +1,10 @@ +import { debug } from "@opennextjs/core/adapters/logger.js"; import type { CacheEntryType, CacheValue, IncrementalCache } from "@opennextjs/core/types/overrides.js"; -import { customFetchClient } from "../../utils/fetch.js"; import { LRUCache } from "@opennextjs/core/utils/lru.js"; -import { debug } from "@opennextjs/core/adapters/logger.js"; +import { customFetchClient } from "../../utils/fetch.js"; -import S3Cache, { getAwsClient } from "./s3-lite"; +import S3Cache, { getAwsClient } from "./s3-lite.js"; // TTL for the local cache in milliseconds const localCacheTTL = process.env.OPEN_NEXT_LOCAL_CACHE_TTL_MS diff --git a/packages/open-next/src/overrides/incrementalCache/s3-lite.ts b/packages/aws/src/overrides/incrementalCache/s3-lite.ts similarity index 100% rename from packages/open-next/src/overrides/incrementalCache/s3-lite.ts rename to packages/aws/src/overrides/incrementalCache/s3-lite.ts diff --git a/packages/open-next/src/overrides/incrementalCache/s3.ts b/packages/aws/src/overrides/incrementalCache/s3.ts similarity index 100% rename from packages/open-next/src/overrides/incrementalCache/s3.ts rename to packages/aws/src/overrides/incrementalCache/s3.ts diff --git a/packages/open-next/src/overrides/queue/sqs-lite.ts b/packages/aws/src/overrides/queue/sqs-lite.ts similarity index 100% rename from packages/open-next/src/overrides/queue/sqs-lite.ts rename to packages/aws/src/overrides/queue/sqs-lite.ts diff --git a/packages/open-next/src/overrides/queue/sqs.ts b/packages/aws/src/overrides/queue/sqs.ts similarity index 100% rename from packages/open-next/src/overrides/queue/sqs.ts rename to packages/aws/src/overrides/queue/sqs.ts diff --git a/packages/open-next/src/overrides/tagCache/constants.ts b/packages/aws/src/overrides/tagCache/constants.ts similarity index 100% rename from packages/open-next/src/overrides/tagCache/constants.ts rename to packages/aws/src/overrides/tagCache/constants.ts diff --git a/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts b/packages/aws/src/overrides/tagCache/dynamodb-lite.ts similarity index 99% rename from packages/open-next/src/overrides/tagCache/dynamodb-lite.ts rename to packages/aws/src/overrides/tagCache/dynamodb-lite.ts index a64b8cdf..902bc29c 100644 --- a/packages/open-next/src/overrides/tagCache/dynamodb-lite.ts +++ b/packages/aws/src/overrides/tagCache/dynamodb-lite.ts @@ -1,15 +1,14 @@ import path from "node:path"; -import { AwsClient } from "aws4fetch"; - +import { debug, error } from "@opennextjs/core/adapters/logger.js"; +import { chunk, parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; import type { OriginalTagCache } from "@opennextjs/core/types/overrides.js"; import { RecoverableError } from "@opennextjs/core/utils/error.js"; -import { customFetchClient } from "../../utils/fetch.js"; +import { AwsClient } from "aws4fetch"; -import { debug, error } from "@opennextjs/core/adapters/logger.js"; -import { chunk, parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; +import { customFetchClient } from "../../utils/fetch.js"; -import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency } from "./constants"; +import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency } from "./constants.js"; type DynamoDBItem = { tag?: { S: string }; diff --git a/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts b/packages/aws/src/overrides/tagCache/dynamodb-nextMode.ts similarity index 99% rename from packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts rename to packages/aws/src/overrides/tagCache/dynamodb-nextMode.ts index bdecf5d7..88deb1a8 100644 --- a/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts +++ b/packages/aws/src/overrides/tagCache/dynamodb-nextMode.ts @@ -1,15 +1,14 @@ import path from "node:path"; -import { AwsClient } from "aws4fetch"; - +import { debug, error } from "@opennextjs/core/adapters/logger.js"; +import { chunk, parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; import type { NextModeTagCache } from "@opennextjs/core/types/overrides.js"; import { RecoverableError } from "@opennextjs/core/utils/error.js"; -import { customFetchClient } from "../../utils/fetch.js"; +import { AwsClient } from "aws4fetch"; -import { debug, error } from "@opennextjs/core/adapters/logger.js"; -import { chunk, parseNumberFromEnv } from "@opennextjs/core/adapters/util.js"; +import { customFetchClient } from "../../utils/fetch.js"; -import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency } from "./constants"; +import { MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT, getDynamoBatchWriteCommandConcurrency } from "./constants.js"; type DynamoDBTagItem = { revalidatedAt: { N: string }; diff --git a/packages/open-next/src/overrides/tagCache/dynamodb.ts b/packages/aws/src/overrides/tagCache/dynamodb.ts similarity index 100% rename from packages/open-next/src/overrides/tagCache/dynamodb.ts rename to packages/aws/src/overrides/tagCache/dynamodb.ts diff --git a/packages/open-next/src/overrides/warmer/aws-lambda.ts b/packages/aws/src/overrides/warmer/aws-lambda.ts similarity index 100% rename from packages/open-next/src/overrides/warmer/aws-lambda.ts rename to packages/aws/src/overrides/warmer/aws-lambda.ts diff --git a/packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts b/packages/aws/src/overrides/wrappers/aws-lambda-compressed.ts similarity index 98% rename from packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts rename to packages/aws/src/overrides/wrappers/aws-lambda-compressed.ts index 0265eceb..743449c4 100644 --- a/packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts +++ b/packages/aws/src/overrides/wrappers/aws-lambda-compressed.ts @@ -2,13 +2,13 @@ import { Readable, type Transform, Writable } from "node:stream"; import type { ReadableStream } from "node:stream/web"; import zlib from "node:zlib"; -import type { AwsLambdaEvent, AwsLambdaReturn } from "../../types/aws-lambda.js"; +import { error } from "@opennextjs/core/adapters/logger.js"; import type { InternalResult, StreamCreator } from "@opennextjs/core/types/open-next.js"; import type { WrapperHandler } from "@opennextjs/core/types/overrides.js"; -import { error } from "@opennextjs/core/adapters/logger.js"; +import type { AwsLambdaEvent, AwsLambdaReturn } from "../../types/aws-lambda.js"; -import { formatWarmerResponse } from "./aws-lambda"; +import { formatWarmerResponse } from "./aws-lambda.js"; const handler: WrapperHandler = async (handler, converter) => diff --git a/packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts b/packages/aws/src/overrides/wrappers/aws-lambda-streaming.ts similarity index 100% rename from packages/open-next/src/overrides/wrappers/aws-lambda-streaming.ts rename to packages/aws/src/overrides/wrappers/aws-lambda-streaming.ts diff --git a/packages/open-next/src/overrides/wrappers/aws-lambda.ts b/packages/aws/src/overrides/wrappers/aws-lambda.ts similarity index 100% rename from packages/open-next/src/overrides/wrappers/aws-lambda.ts rename to packages/aws/src/overrides/wrappers/aws-lambda.ts diff --git a/packages/open-next/src/types/aws-lambda.ts b/packages/aws/src/types/aws-lambda.ts similarity index 100% rename from packages/open-next/src/types/aws-lambda.ts rename to packages/aws/src/types/aws-lambda.ts diff --git a/packages/open-next/src/types/global.ts b/packages/aws/src/types/global.ts similarity index 100% rename from packages/open-next/src/types/global.ts rename to packages/aws/src/types/global.ts diff --git a/packages/open-next/src/utils/fetch.ts b/packages/aws/src/utils/fetch.ts similarity index 100% rename from packages/open-next/src/utils/fetch.ts rename to packages/aws/src/utils/fetch.ts diff --git a/packages/open-next/tsconfig.json b/packages/aws/tsconfig.json similarity index 100% rename from packages/open-next/tsconfig.json rename to packages/aws/tsconfig.json From 8be736a19e00f48cb078c4b8a73bd4aefbce848f Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 09:31:04 +0200 Subject: [PATCH 05/24] Update package.json and pnpm-lock.yaml to remove 'catalog:aws' references for @types/node and typescript, and adjust workspace dependencies. --- examples/app-pages-router/package.json | 4 +- examples/app-router/package.json | 4 +- examples/experimental/package.json | 4 +- examples/pages-router/package.json | 4 +- packages/aws/package.json | 5 +- packages/core/package.json | 19 +- pnpm-lock.yaml | 453 +++++++++++++------------ pnpm-workspace.yaml | 2 - 8 files changed, 266 insertions(+), 229 deletions(-) diff --git a/examples/app-pages-router/package.json b/examples/app-pages-router/package.json index cbc0ba1c..1b39a99c 100644 --- a/examples/app-pages-router/package.json +++ b/examples/app-pages-router/package.json @@ -23,13 +23,13 @@ "devDependencies": { "@types/express": "^5.0.6", "@types/express-http-proxy": "1.6.7", - "@types/node": "catalog:aws", + "@types/node": "catalog:", "@types/react": "catalog:aws", "@types/react-dom": "catalog:aws", "autoprefixer": "catalog:aws", "postcss": "catalog:aws", "tailwindcss": "catalog:aws", "tsx": "4.20.5", - "typescript": "catalog:aws" + "typescript": "catalog:" } } diff --git a/examples/app-router/package.json b/examples/app-router/package.json index a06fd870..e2223059 100644 --- a/examples/app-router/package.json +++ b/examples/app-router/package.json @@ -19,12 +19,12 @@ }, "devDependencies": { "@opennextjs/aws": "workspace:*", - "@types/node": "catalog:aws", + "@types/node": "catalog:", "@types/react": "catalog:aws", "@types/react-dom": "catalog:aws", "autoprefixer": "catalog:aws", "postcss": "catalog:aws", "tailwindcss": "catalog:aws", - "typescript": "catalog:aws" + "typescript": "catalog:" } } diff --git a/examples/experimental/package.json b/examples/experimental/package.json index e86a3601..2ef388f9 100644 --- a/examples/experimental/package.json +++ b/examples/experimental/package.json @@ -17,10 +17,10 @@ }, "devDependencies": { "@opennextjs/aws": "workspace:*", - "@types/node": "catalog:aws", + "@types/node": "catalog:", "@types/react": "catalog:aws", "@types/react-dom": "catalog:aws", - "typescript": "catalog:aws" + "typescript": "catalog:" }, "pnpm": { "overrides": { diff --git a/examples/pages-router/package.json b/examples/pages-router/package.json index ee01d0df..06735dfe 100644 --- a/examples/pages-router/package.json +++ b/examples/pages-router/package.json @@ -18,12 +18,12 @@ "react-dom": "catalog:aws" }, "devDependencies": { - "@types/node": "catalog:aws", + "@types/node": "catalog:", "@types/react": "catalog:aws", "@types/react-dom": "catalog:aws", "autoprefixer": "catalog:aws", "postcss": "catalog:aws", "tailwindcss": "catalog:aws", - "typescript": "catalog:aws" + "typescript": "catalog:" } } diff --git a/packages/aws/package.json b/packages/aws/package.json index 98e643c8..ea5fb1af 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -66,12 +66,13 @@ "yaml": "^2.8.1" }, "devDependencies": { + "@tsconfig/node22": "^22.0.5", "@types/aws-lambda": "^8.10.158", "@types/express": "5.0.6", - "@types/node": "catalog:aws", + "@types/node": "catalog:", "concurrently": "^9.2.1", "tsc-alias": "^1.8.16", - "typescript": "catalog:aws" + "typescript": "catalog:" }, "peerDependencies": { "next": "^16.0.10" diff --git a/packages/core/package.json b/packages/core/package.json index 0bcb561c..e6d1302d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,12 @@ "name": "@opennextjs/core", "version": "0.1.0", "description": "OpenNext core - platform-agnostic Next.js adapter infrastructure", - "keywords": ["next.js", "adapter", "serverless", "opennext"], + "keywords": [ + "adapter", + "next.js", + "opennext", + "serverless" + ], "homepage": "https://opennext.js.org", "bugs": { "url": "https://github.com/opennextjs/opennextjs-aws/issues" @@ -13,11 +18,15 @@ "url": "https://github.com/opennextjs/opennextjs-aws", "directory": "packages/core" }, - "files": ["dist"], + "files": [ + "dist" + ], "type": "module", "typesVersions": { "*": { - "*": ["dist/*"] + "*": [ + "dist/*" + ] } }, "exports": { @@ -49,10 +58,10 @@ }, "devDependencies": { "@types/express": "5.0.6", - "@types/node": "catalog:aws", + "@types/node": "catalog:", "concurrently": "^9.2.1", "tsc-alias": "^1.8.16", - "typescript": "catalog:aws" + "typescript": "catalog:" }, "peerDependencies": { "next": "^16.0.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cb4d3a3..39210f61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,9 +6,6 @@ settings: catalogs: aws: - '@types/node': - specifier: 20.17.6 - version: 20.17.6 '@types/react': specifier: ^19 version: 19.2.9 @@ -36,9 +33,6 @@ catalogs: tailwindcss: specifier: 3.3.3 version: 3.3.3 - typescript: - specifier: 5.9.3 - version: 5.9.3 default: '@cloudflare/workers-types': specifier: ^4.20260114.0 @@ -1077,7 +1071,7 @@ importers: version: link:../shared '@opennextjs/aws': specifier: workspace:* - version: link:../../packages/open-next + version: link:../../packages/aws express: specifier: ^5.2.1 version: 5.2.1 @@ -1101,8 +1095,8 @@ importers: specifier: 1.6.7 version: 1.6.7 '@types/node': - specifier: catalog:aws - version: 20.17.6 + specifier: 'catalog:' + version: 22.19.7 '@types/react': specifier: catalog:aws version: 19.2.9 @@ -1117,12 +1111,12 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:aws - version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) tsx: specifier: 4.20.5 version: 4.20.5 typescript: - specifier: catalog:aws + specifier: 'catalog:' version: 5.9.3 examples/app-router: @@ -1142,10 +1136,10 @@ importers: devDependencies: '@opennextjs/aws': specifier: workspace:* - version: link:../../packages/open-next + version: link:../../packages/aws '@types/node': - specifier: catalog:aws - version: 20.17.6 + specifier: 'catalog:' + version: 22.19.7 '@types/react': specifier: catalog:aws version: 19.2.9 @@ -1160,9 +1154,9 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:aws - version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) typescript: - specifier: catalog:aws + specifier: 'catalog:' version: 5.9.3 examples/experimental: @@ -1179,10 +1173,10 @@ importers: devDependencies: '@opennextjs/aws': specifier: workspace:* - version: link:../../packages/open-next + version: link:../../packages/aws '@types/node': - specifier: catalog:aws - version: 20.17.6 + specifier: 'catalog:' + version: 22.19.7 '@types/react': specifier: catalog:aws version: 19.2.9 @@ -1190,7 +1184,7 @@ importers: specifier: catalog:aws version: 19.2.3(@types/react@19.2.9) typescript: - specifier: catalog:aws + specifier: 'catalog:' version: 5.9.3 examples/pages-router: @@ -1209,8 +1203,8 @@ importers: version: 19.2.3(react@19.2.3) devDependencies: '@types/node': - specifier: catalog:aws - version: 20.17.6 + specifier: 'catalog:' + version: 22.19.7 '@types/react': specifier: catalog:aws version: 19.2.9 @@ -1225,9 +1219,9 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:aws - version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) typescript: - specifier: catalog:aws + specifier: 'catalog:' version: 5.9.3 examples/shared: @@ -1261,6 +1255,88 @@ importers: specifier: ^2.43.6 version: 2.44.0(@aws-sdk/client-sso-oidc@3.678.0(@aws-sdk/client-sts@3.678.0(aws-crt@1.23.0))(aws-crt@1.23.0))(@types/react@19.2.9)(aws-crt@1.23.0)(better-sqlite3@11.10.0) + packages/aws: + dependencies: + '@ast-grep/napi': + specifier: ^0.40.5 + version: 0.40.5 + '@aws-sdk/client-cloudfront': + specifier: 3.984.0 + version: 3.984.0(aws-crt@1.23.0) + '@aws-sdk/client-dynamodb': + specifier: 3.984.0 + version: 3.984.0(aws-crt@1.23.0) + '@aws-sdk/client-lambda': + specifier: 3.984.0 + version: 3.984.0(aws-crt@1.23.0) + '@aws-sdk/client-s3': + specifier: 3.984.0 + version: 3.984.0(aws-crt@1.23.0) + '@aws-sdk/client-sqs': + specifier: 3.984.0 + version: 3.984.0(aws-crt@1.23.0) + '@node-minify/core': + specifier: ^8.0.6 + version: 8.0.6 + '@node-minify/terser': + specifier: ^8.0.6 + version: 8.0.6 + '@opennextjs/core': + specifier: workspace:* + version: link:../core + '@tsconfig/node18': + specifier: ^1.0.3 + version: 1.0.3 + aws4fetch: + specifier: ^1.0.20 + version: 1.0.20 + chalk: + specifier: ^5.6.2 + version: 5.6.2 + cookie: + specifier: ^1.0.2 + version: 1.0.2 + esbuild: + specifier: catalog:aws + version: 0.27.0 + express: + specifier: ^5.2.1 + version: 5.2.1 + next: + specifier: ^16.0.10 + version: 16.1.4(@opentelemetry/api@1.9.0)(@playwright/test@1.58.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + path-to-regexp: + specifier: ^6.3.0 + version: 6.3.0 + urlpattern-polyfill: + specifier: ^10.1.0 + version: 10.1.0 + yaml: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@tsconfig/node22': + specifier: ^22.0.5 + version: 22.0.5 + '@types/aws-lambda': + specifier: ^8.10.158 + version: 8.10.158 + '@types/express': + specifier: 5.0.6 + version: 5.0.6 + '@types/node': + specifier: 'catalog:' + version: 22.19.7 + concurrently: + specifier: ^9.2.1 + version: 9.2.1 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/cloudflare: dependencies: '@ast-grep/napi': @@ -1271,7 +1347,7 @@ importers: version: 1.31.0 '@opennextjs/aws': specifier: workspace:* - version: link:../open-next + version: link:../aws '@opennextjs/core': specifier: workspace:* version: link:../core @@ -1389,87 +1465,8 @@ importers: specifier: 5.0.6 version: 5.0.6 '@types/node': - specifier: catalog:aws - version: 20.17.6 - concurrently: - specifier: ^9.2.1 - version: 9.2.1 - tsc-alias: - specifier: ^1.8.16 - version: 1.8.16 - typescript: - specifier: catalog:aws - version: 5.9.3 - - packages/open-next: - dependencies: - '@ast-grep/napi': - specifier: ^0.40.5 - version: 0.40.5 - '@aws-sdk/client-cloudfront': - specifier: 3.984.0 - version: 3.984.0(aws-crt@1.23.0) - '@aws-sdk/client-dynamodb': - specifier: 3.984.0 - version: 3.984.0(aws-crt@1.23.0) - '@aws-sdk/client-lambda': - specifier: 3.984.0 - version: 3.984.0(aws-crt@1.23.0) - '@aws-sdk/client-s3': - specifier: 3.984.0 - version: 3.984.0(aws-crt@1.23.0) - '@aws-sdk/client-sqs': - specifier: 3.984.0 - version: 3.984.0(aws-crt@1.23.0) - '@node-minify/core': - specifier: ^8.0.6 - version: 8.0.6 - '@node-minify/terser': - specifier: ^8.0.6 - version: 8.0.6 - '@opennextjs/core': - specifier: workspace:* - version: link:../core - '@tsconfig/node18': - specifier: ^1.0.3 - version: 1.0.3 - aws4fetch: - specifier: ^1.0.20 - version: 1.0.20 - chalk: - specifier: ^5.6.2 - version: 5.6.2 - cookie: - specifier: ^1.0.2 - version: 1.0.2 - esbuild: - specifier: catalog:aws - version: 0.27.0 - express: - specifier: ^5.2.1 - version: 5.2.1 - next: - specifier: ^16.0.10 - version: 16.1.4(@opentelemetry/api@1.9.0)(@playwright/test@1.58.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - path-to-regexp: - specifier: ^6.3.0 - version: 6.3.0 - urlpattern-polyfill: - specifier: ^10.1.0 - version: 10.1.0 - yaml: - specifier: ^2.8.1 - version: 2.8.1 - devDependencies: - '@types/aws-lambda': - specifier: ^8.10.158 - version: 8.10.158 - '@types/express': - specifier: 5.0.6 - version: 5.0.6 - '@types/node': - specifier: catalog:aws - version: 20.17.6 + specifier: 'catalog:' + version: 22.19.7 concurrently: specifier: ^9.2.1 version: 9.2.1 @@ -1477,7 +1474,7 @@ importers: specifier: ^1.8.16 version: 1.8.16 typescript: - specifier: catalog:aws + specifier: 'catalog:' version: 5.9.3 packages/tests-e2e: @@ -1496,7 +1493,7 @@ importers: dependencies: '@opennextjs/aws': specifier: workspace:* - version: link:../open-next + version: link:../aws '@opennextjs/core': specifier: workspace:* version: link:../core @@ -5666,6 +5663,9 @@ packages: '@tsconfig/node18@1.0.3': resolution: {integrity: sha512-RbwvSJQsuN9TB04AQbGULYfOGE/RnSFk/FLQ5b0NmDf5Kx2q/lABZbHQPKCO1vZ6Fiwkplu+yb9pGdLy1iGseQ==} + '@tsconfig/node22@22.0.5': + resolution: {integrity: sha512-hLf2ld+sYN/BtOJjHUWOk568dvjFQkHnLNa6zce25GIH+vxKfvTgm3qpaH6ToF5tu/NN0IH66s+Bb5wElHrLcw==} + '@tsconfig/strictest@2.0.8': resolution: {integrity: sha512-XnQ7vNz5HRN0r88GYf1J9JJjqtZPiHt2woGJOo2dYqyHGGcd6OLGqSlBB6p1j9mpzja6Oe5BoPqWmeDx6X9rLw==} @@ -10993,32 +10993,32 @@ snapshots: '@aws-sdk/util-endpoints': 3.984.0 '@aws-sdk/util-user-agent-browser': 3.972.8 '@aws-sdk/util-user-agent-node': 3.973.12(aws-crt@1.23.0) - '@smithy/config-resolver': 4.4.6 + '@smithy/config-resolver': 4.4.13 '@smithy/core': 3.23.12 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 '@smithy/middleware-endpoint': 4.4.27 '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 '@smithy/util-defaults-mode-browser': 4.3.43 '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-stream': 4.5.10 - '@smithy/util-utf8': 4.2.0 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 '@smithy/util-waiter': 4.2.8 tslib: 2.8.1 transitivePeerDependencies: @@ -11087,31 +11087,31 @@ snapshots: '@aws-sdk/util-endpoints': 3.984.0 '@aws-sdk/util-user-agent-browser': 3.972.8 '@aws-sdk/util-user-agent-node': 3.973.12(aws-crt@1.23.0) - '@smithy/config-resolver': 4.4.6 + '@smithy/config-resolver': 4.4.13 '@smithy/core': 3.23.12 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 '@smithy/middleware-endpoint': 4.4.27 '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 '@smithy/util-defaults-mode-browser': 4.3.43 '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-utf8': 4.2.0 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-utf8': 4.2.2 '@smithy/util-waiter': 4.2.8 tslib: 2.8.1 transitivePeerDependencies: @@ -11421,35 +11421,35 @@ snapshots: '@aws-sdk/util-endpoints': 3.984.0 '@aws-sdk/util-user-agent-browser': 3.972.8 '@aws-sdk/util-user-agent-node': 3.973.12(aws-crt@1.23.0) - '@smithy/config-resolver': 4.4.6 + '@smithy/config-resolver': 4.4.13 '@smithy/core': 3.23.12 '@smithy/eventstream-serde-browser': 4.2.8 '@smithy/eventstream-serde-config-resolver': 4.3.8 '@smithy/eventstream-serde-node': 4.2.8 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 '@smithy/middleware-endpoint': 4.4.27 '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 '@smithy/util-defaults-mode-browser': 4.3.43 '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-stream': 4.5.10 - '@smithy/util-utf8': 4.2.0 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 '@smithy/util-waiter': 4.2.8 tslib: 2.8.1 transitivePeerDependencies: @@ -11584,38 +11584,38 @@ snapshots: '@aws-sdk/util-endpoints': 3.984.0 '@aws-sdk/util-user-agent-browser': 3.972.8 '@aws-sdk/util-user-agent-node': 3.973.12(aws-crt@1.23.0) - '@smithy/config-resolver': 4.4.6 + '@smithy/config-resolver': 4.4.13 '@smithy/core': 3.23.12 '@smithy/eventstream-serde-browser': 4.2.8 '@smithy/eventstream-serde-config-resolver': 4.3.8 '@smithy/eventstream-serde-node': 4.2.8 - '@smithy/fetch-http-handler': 5.3.9 + '@smithy/fetch-http-handler': 5.3.15 '@smithy/hash-blob-browser': 4.2.9 - '@smithy/hash-node': 4.2.8 + '@smithy/hash-node': 4.2.12 '@smithy/hash-stream-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 + '@smithy/invalid-dependency': 4.2.12 '@smithy/md5-js': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-content-length': 4.2.12 '@smithy/middleware-endpoint': 4.4.27 '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 '@smithy/util-defaults-mode-browser': 4.3.43 '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-stream': 4.5.10 - '@smithy/util-utf8': 4.2.0 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 '@smithy/util-waiter': 4.2.8 tslib: 2.8.1 transitivePeerDependencies: @@ -11637,32 +11637,32 @@ snapshots: '@aws-sdk/util-endpoints': 3.984.0 '@aws-sdk/util-user-agent-browser': 3.972.8 '@aws-sdk/util-user-agent-node': 3.973.12(aws-crt@1.23.0) - '@smithy/config-resolver': 4.4.6 + '@smithy/config-resolver': 4.4.13 '@smithy/core': 3.23.12 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 '@smithy/md5-js': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-content-length': 4.2.12 '@smithy/middleware-endpoint': 4.4.27 '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 '@smithy/util-defaults-mode-browser': 4.3.43 '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-utf8': 4.2.0 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -12763,9 +12763,9 @@ snapshots: dependencies: '@aws-sdk/middleware-sdk-s3': 3.972.26 '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.8 - '@smithy/signature-v4': 5.3.8 - '@smithy/types': 4.12.0 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/types': 4.13.1 tslib: 2.8.1 '@aws-sdk/smithy-client@3.374.0': @@ -12865,9 +12865,9 @@ snapshots: '@aws-sdk/util-endpoints@3.984.0': dependencies: '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-endpoints': 3.2.8 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-endpoints': 3.3.3 tslib: 2.8.1 '@aws-sdk/util-endpoints@3.996.5': @@ -16420,6 +16420,8 @@ snapshots: '@tsconfig/node18@1.0.3': {} + '@tsconfig/node22@22.0.5': {} + '@tsconfig/strictest@2.0.8': {} '@types/aws-lambda@8.10.158': {} @@ -21645,6 +21647,33 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + tailwindcss@3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)): dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4a65273a..bf662e41 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -38,13 +38,11 @@ catalogs: next: 16.2.1 react: ^19 react-dom: ^19 - "@types/node": 20.17.6 "@types/react": ^19 "@types/react-dom": ^19 autoprefixer: 10.4.15 postcss: 8.4.27 tailwindcss: 3.3.3 - typescript: 5.9.3 esbuild: 0.27.0 e2e: "@types/node": 20.17.6 From 81d8bd97ae799f5e9d393adf3d33ffbf75dac466 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 09:41:25 +0200 Subject: [PATCH 06/24] remove unused utils --- packages/utils/package.json | 29 ----------------------------- packages/utils/src/index.ts | 14 -------------- packages/utils/src/logger.ts | 21 --------------------- packages/utils/tsconfig.json | 15 --------------- 4 files changed, 79 deletions(-) delete mode 100644 packages/utils/package.json delete mode 100644 packages/utils/src/index.ts delete mode 100644 packages/utils/src/logger.ts delete mode 100644 packages/utils/tsconfig.json diff --git a/packages/utils/package.json b/packages/utils/package.json deleted file mode 100644 index 8012558f..00000000 --- a/packages/utils/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@open-next/utils", - "private": true, - "typesVersions": { - "*": { - "types": [ - "./dist/*.d.ts" - ] - } - }, - "exports": { - ".": "./dist/index.js", - "./binary": "./dist/binary.js", - "./logger": "./dist/logger.js" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "tsup ./src/*.ts --format cjs --dts", - "dev": "tsup ./src/*.ts --format cjs --dts --watch", - "clean": "rm -rf .turbo && rm -rf node_modules" - }, - "dependencies": {}, - "devDependencies": { - "@types/node": "catalog:", - "tsup": "7.2.0" - } -} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts deleted file mode 100644 index 35e40c5c..00000000 --- a/packages/utils/src/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -// TODO: move util functions from open-next here (if/where it makes sense) -export function add(a: number, b: number) { - return a + b; -} - -export function generateUniqueId() { - return Math.random().toString(36).slice(2, 8); -} - -export async function wait(n = 1000) { - return new Promise((res) => { - setTimeout(res, n); - }); -} diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts deleted file mode 100644 index a017afd7..00000000 --- a/packages/utils/src/logger.ts +++ /dev/null @@ -1,21 +0,0 @@ -export function debug(...args: unknown[]) { - if (process.env.OPEN_NEXT_DEBUG) { - console.log(...args); - } -} - -export function warn(...args: unknown[]) { - console.warn(...args); -} - -export function error(...args: unknown[]) { - console.error(...args); -} - -export const awsLogger = { - trace: () => {}, - debug: () => {}, - info: debug, - warn, - error, -}; diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json deleted file mode 100644 index fd5c5208..00000000 --- a/packages/utils/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": false, - "moduleResolution": "NodeNext", - "module": "NodeNext", - "preserveWatchOutput": true, - "skipLibCheck": true, - "noEmit": true, - "strict": true, - "target": "ESNext" - }, - "exclude": ["node_modules"] -} From 282c8025189d2d0fdd26bb12f7fdf7318c42b1e4 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 09:41:30 +0200 Subject: [PATCH 07/24] bump tsconfig --- packages/aws/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws/tsconfig.json b/packages/aws/tsconfig.json index 6c5a4c3e..c088ad83 100644 --- a/packages/aws/tsconfig.json +++ b/packages/aws/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@tsconfig/node18/tsconfig.json", + "extends": "@tsconfig/node22/tsconfig.json", "compilerOptions": { "declaration": true, "module": "esnext", From b257d48f2e1d41cdbbad69079f86b67442f3f2f3 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 09:55:12 +0200 Subject: [PATCH 08/24] bump typescript version everywhere --- create-cloudflare/next/package.json | 2 +- examples-cloudflare/bugs/gh-119/package.json | 2 +- examples-cloudflare/bugs/gh-219/package.json | 2 +- examples-cloudflare/bugs/gh-223/package.json | 2 +- .../e2e/app-pages-router/package.json | 2 +- .../e2e/app-router/package.json | 2 +- .../e2e/experimental/package.json | 2 +- .../e2e/pages-router/package.json | 2 +- .../next-partial-prerendering/package.json | 2 +- packages/aws/tsconfig.json | 3 + packages/cloudflare/tsconfig.json | 4 +- packages/core/tsconfig.json | 4 +- pnpm-lock.yaml | 349 ++++++------------ pnpm-workspace.yaml | 2 +- 14 files changed, 123 insertions(+), 257 deletions(-) diff --git a/create-cloudflare/next/package.json b/create-cloudflare/next/package.json index 06e2cbe7..75248073 100644 --- a/create-cloudflare/next/package.json +++ b/create-cloudflare/next/package.json @@ -25,7 +25,7 @@ "@types/react-dom": "^19", "oxlint": "^1.42.0", "tailwindcss": "^4", - "typescript": "^5.7.4", + "typescript": "catalog:", "wrangler": "^4.59.3" } } diff --git a/examples-cloudflare/bugs/gh-119/package.json b/examples-cloudflare/bugs/gh-119/package.json index f1213118..c9274ae6 100644 --- a/examples-cloudflare/bugs/gh-119/package.json +++ b/examples-cloudflare/bugs/gh-119/package.json @@ -26,7 +26,7 @@ "@types/react-dom": "^18", "postcss": "^8", "tailwindcss": "^3.4.1", - "typescript": "^5", + "typescript": "catalog:", "wrangler": "catalog:" } } diff --git a/examples-cloudflare/bugs/gh-219/package.json b/examples-cloudflare/bugs/gh-219/package.json index 2d60db64..5d740f20 100644 --- a/examples-cloudflare/bugs/gh-219/package.json +++ b/examples-cloudflare/bugs/gh-219/package.json @@ -51,7 +51,7 @@ "drizzle-kit": "^0.30.1", "postcss": "^8", "tailwindcss": "^3.4.1", - "typescript": "^5", + "typescript": "catalog:", "vercel": "^39.2.2", "wrangler": "catalog:" } diff --git a/examples-cloudflare/bugs/gh-223/package.json b/examples-cloudflare/bugs/gh-223/package.json index bdedf95d..f230e2d5 100644 --- a/examples-cloudflare/bugs/gh-223/package.json +++ b/examples-cloudflare/bugs/gh-223/package.json @@ -29,7 +29,7 @@ "@types/react-dom": "^19.0.3", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", + "typescript": "catalog:", "wrangler": "catalog:" } } diff --git a/examples-cloudflare/e2e/app-pages-router/package.json b/examples-cloudflare/e2e/app-pages-router/package.json index 203d5868..b83f4ea3 100644 --- a/examples-cloudflare/e2e/app-pages-router/package.json +++ b/examples-cloudflare/e2e/app-pages-router/package.json @@ -31,7 +31,7 @@ "autoprefixer": "catalog:e2e", "postcss": "catalog:e2e", "tailwindcss": "catalog:e2e", - "typescript": "catalog:default", + "typescript": "catalog:", "wrangler": "catalog:" } } diff --git a/examples-cloudflare/e2e/app-router/package.json b/examples-cloudflare/e2e/app-router/package.json index a52334b4..b49aca97 100644 --- a/examples-cloudflare/e2e/app-router/package.json +++ b/examples-cloudflare/e2e/app-router/package.json @@ -31,7 +31,7 @@ "autoprefixer": "catalog:e2e", "postcss": "catalog:e2e", "tailwindcss": "catalog:e2e", - "typescript": "catalog:default", + "typescript": "catalog:", "wrangler": "catalog:" } } diff --git a/examples-cloudflare/e2e/experimental/package.json b/examples-cloudflare/e2e/experimental/package.json index 38f3c112..a2b07c3f 100644 --- a/examples-cloudflare/e2e/experimental/package.json +++ b/examples-cloudflare/e2e/experimental/package.json @@ -24,7 +24,7 @@ "@types/node": "catalog:e2e", "@types/react": "catalog:e2e", "@types/react-dom": "catalog:e2e", - "typescript": "catalog:default", + "typescript": "catalog:", "wrangler": "catalog:" } } diff --git a/examples-cloudflare/e2e/pages-router/package.json b/examples-cloudflare/e2e/pages-router/package.json index 24b8f5ce..59a47ed3 100644 --- a/examples-cloudflare/e2e/pages-router/package.json +++ b/examples-cloudflare/e2e/pages-router/package.json @@ -31,7 +31,7 @@ "autoprefixer": "catalog:e2e", "postcss": "catalog:e2e", "tailwindcss": "catalog:e2e", - "typescript": "catalog:default", + "typescript": "catalog:", "wrangler": "catalog:" } } diff --git a/examples-cloudflare/next-partial-prerendering/package.json b/examples-cloudflare/next-partial-prerendering/package.json index a4165a2e..96c077f3 100644 --- a/examples-cloudflare/next-partial-prerendering/package.json +++ b/examples-cloudflare/next-partial-prerendering/package.json @@ -30,7 +30,7 @@ "autoprefixer": "10.4.19", "postcss": "8.4.39", "tailwindcss": "3.4.5", - "typescript": "5.5.3", + "typescript": "catalog:", "wrangler": "catalog:" } } diff --git a/packages/aws/tsconfig.json b/packages/aws/tsconfig.json index c088ad83..a92ef286 100644 --- a/packages/aws/tsconfig.json +++ b/packages/aws/tsconfig.json @@ -3,7 +3,10 @@ "compilerOptions": { "declaration": true, "module": "esnext", + "moduleResolution": "bundler", + "types": ["node"], "lib": ["DOM", "ESNext"], + "rootDir": "./src", "outDir": "./dist", "allowSyntheticDefaultImports": true, "paths": {} diff --git a/packages/cloudflare/tsconfig.json b/packages/cloudflare/tsconfig.json index 48f1af14..85cb3321 100644 --- a/packages/cloudflare/tsconfig.json +++ b/packages/cloudflare/tsconfig.json @@ -12,9 +12,11 @@ "noImplicitReturns": false, "noPropertyAccessFromIndexSignature": false, "resolveJsonModule": true, + "rootDir": "./src", "outDir": "./dist", "target": "ES2022", - "types": ["@cloudflare/workers-types", "@opennextjs/aws/types/global.d.ts"] + "types": ["@cloudflare/workers-types", "@opennextjs/aws/types/global.d.ts"], + "ignoreDeprecations": "6.0" }, "include": ["src/**/*.ts", "env.d.ts"], "exclude": ["src/**/*.spec.ts"] diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 67b691f0..93b87b0f 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,6 +4,7 @@ "declaration": true, "module": "esnext", "lib": ["DOM", "ESNext"], + "rootDir": "./src", "outDir": "./dist", "allowSyntheticDefaultImports": true, "paths": { @@ -11,6 +12,7 @@ "@/config/*": ["./src/adapters/config/*"], "@/http/*": ["./src/http/*"], "@/utils/*": ["./src/utils/*"] - } + }, + "ignoreDeprecations": "6.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39210f61..d1d1243b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,8 +86,8 @@ catalogs: specifier: ^6.0.1 version: 6.1.2 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 vitest: specifier: ^2.1.1 version: 2.1.3 @@ -186,8 +186,8 @@ importers: specifier: ^4 version: 4.1.18 typescript: - specifier: ^5.7.4 - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: ^4.59.3 version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -224,10 +224,10 @@ importers: version: 8.5.6 tailwindcss: specifier: ^3.4.1 - version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: - specifier: ^5 - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -242,7 +242,7 @@ importers: version: 0.14.0 '@t3-oss/env-nextjs': specifier: ^0.11.1 - version: 0.11.1(typescript@5.9.3)(zod@3.25.76) + version: 0.11.1(typescript@6.0.3)(zod@3.25.76) '@tanstack/react-table': specifier: ^8.20.6 version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -257,7 +257,7 @@ importers: version: 2.1.1 drizzle-orm: specifier: ^0.38.3 - version: 0.38.4(@aws-sdk/client-rds-data@3.678.0(aws-crt@1.23.0))(@cloudflare/workers-types@4.20260123.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/react@19.2.9)(better-sqlite3@11.10.0)(kysely@0.25.0)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react@19.2.3) + version: 0.38.4(@aws-sdk/client-rds-data@3.678.0(aws-crt@1.23.0))(@cloudflare/workers-types@4.20260123.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@6.0.3))(typescript@6.0.3))(@types/better-sqlite3@7.6.13)(@types/react@19.2.9)(better-sqlite3@11.10.0)(kysely@0.25.0)(prisma@6.19.2(magicast@0.3.5)(typescript@6.0.3))(react@19.2.3) firebase: specifier: ^11.1.0 version: 11.10.0 @@ -302,7 +302,7 @@ importers: version: 2.6.0 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3))) + version: 1.0.7(tailwindcss@3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3))) zod: specifier: ^3.24.1 version: 3.25.76 @@ -339,10 +339,10 @@ importers: version: 8.5.6 tailwindcss: specifier: ^3.4.1 - version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: - specifier: ^5 - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 vercel: specifier: ^39.2.2 version: 39.4.2(rollup@4.24.0) @@ -391,10 +391,10 @@ importers: version: 8.5.6 tailwindcss: specifier: ^3.4.17 - version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: - specifier: ^5.7.2 - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -431,10 +431,10 @@ importers: version: 8.5.6 tailwindcss: specifier: ^3.4.1 - version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -477,10 +477,10 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:e2e - version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@6.0.3)) typescript: - specifier: catalog:default - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -523,10 +523,10 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:e2e - version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@6.0.3)) typescript: - specifier: catalog:default - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -559,8 +559,8 @@ importers: specifier: catalog:e2e version: 19.0.3(@types/react@19.0.3) typescript: - specifier: catalog:default - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -603,10 +603,10 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:e2e - version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@6.0.3)) typescript: - specifier: catalog:default - version: 5.9.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -662,7 +662,7 @@ importers: version: 18.3.0 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -699,10 +699,10 @@ importers: version: link:../../packages/cloudflare '@tailwindcss/forms': specifier: 0.5.7 - version: 0.5.7(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3))) + version: 0.5.7(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3))) '@tailwindcss/typography': specifier: 0.5.13 - version: 0.5.13(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3))) + version: 0.5.13(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3))) '@types/node': specifier: ^22 version: 22.19.7 @@ -720,10 +720,10 @@ importers: version: 8.4.39 tailwindcss: specifier: 3.4.5 - version: 3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)) + version: 3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: - specifier: 5.5.3 - version: 5.5.3 + specifier: 'catalog:' + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -757,7 +757,7 @@ importers: version: 19.0.3(@types/react@19.0.3) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -791,7 +791,7 @@ importers: version: 19.0.3(@types/react@19.0.3) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -825,7 +825,7 @@ importers: version: 19.0.3(@types/react@19.0.3) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -859,7 +859,7 @@ importers: version: 19.0.3(@types/react@19.0.3) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -893,7 +893,7 @@ importers: version: 19.0.3(@types/react@19.0.3) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -945,7 +945,7 @@ importers: version: 6.7.0 '@prisma/client': specifier: 6.7.0 - version: 6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3) + version: 6.7.0(prisma@6.7.0(typescript@6.0.3))(typescript@6.0.3) next: specifier: catalog:e2e version: 16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.0)(react-dom@19.0.3(react@19.0.3))(react@19.0.3) @@ -967,10 +967,10 @@ importers: version: 19.0.3(@types/react@19.0.3) prisma: specifier: 6.7.0 - version: 6.7.0(typescript@5.9.3) + version: 6.7.0(typescript@6.0.3) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -1004,7 +1004,7 @@ importers: version: 19.2.3(@types/react@19.2.9) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -1056,10 +1056,10 @@ importers: version: 8.5.6 tailwindcss: specifier: ^3.4.4 - version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 wrangler: specifier: 'catalog:' version: 4.60.0(@cloudflare/workers-types@4.20260123.0) @@ -1111,13 +1111,13 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:aws - version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) tsx: specifier: 4.20.5 version: 4.20.5 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 examples/app-router: dependencies: @@ -1154,10 +1154,10 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:aws - version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 examples/experimental: dependencies: @@ -1185,7 +1185,7 @@ importers: version: 19.2.3(@types/react@19.2.9) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 examples/pages-router: dependencies: @@ -1219,10 +1219,10 @@ importers: version: 8.4.27 tailwindcss: specifier: catalog:aws - version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + version: 3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 examples/shared: dependencies: @@ -1335,7 +1335,7 @@ importers: version: 1.8.16 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 packages/cloudflare: dependencies: @@ -1417,7 +1417,7 @@ importers: version: 6.1.2 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 vitest: specifier: 'catalog:' version: 2.1.3(@edge-runtime/vm@3.2.0)(@types/node@22.19.7)(jsdom@22.1.0)(lightningcss@1.30.2)(terser@5.16.9) @@ -1475,7 +1475,7 @@ importers: version: 1.8.16 typescript: specifier: 'catalog:' - version: 5.9.3 + version: 6.0.3 packages/tests-e2e: devDependencies: @@ -1487,7 +1487,7 @@ importers: version: 2.0.0 ts-node: specifier: 10.9.1 - version: 10.9.1(@types/node@22.19.7)(typescript@5.9.3) + version: 10.9.1(@types/node@22.19.7)(typescript@6.0.3) packages/tests-unit: dependencies: @@ -1520,15 +1520,6 @@ importers: specifier: ^2.1.3 version: 2.1.3(@edge-runtime/vm@3.2.0)(@types/node@22.19.7)(jsdom@22.1.0)(lightningcss@1.30.2)(terser@5.16.9) - packages/utils: - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 22.19.7 - tsup: - specifier: 7.2.0 - version: 7.2.0(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3))(typescript@5.9.3) - packages: '@actions/core@1.11.1': @@ -6300,12 +6291,6 @@ packages: resolution: {integrity: sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==} engines: {node: '>=6'} - bundle-require@4.2.1: - resolution: {integrity: sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.17' - busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -8034,10 +8019,6 @@ packages: jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -8252,10 +8233,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -8306,9 +8283,6 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -9579,11 +9553,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - rollup@3.29.5: - resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - rollup@4.24.0: resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -9784,11 +9753,6 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} - deprecated: The work that was done in this beta branch won't be included in future versions - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -10122,9 +10086,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - tr46@4.1.1: resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} engines: {node: '>=14'} @@ -10186,22 +10147,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@7.2.0: - resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==} - engines: {node: '>=16.14'} - hasBin: true - peerDependencies: - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.1.0' - peerDependenciesMeta: - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true - tsx@4.20.5: resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} engines: {node: '>=18.0.0'} @@ -10272,13 +10217,13 @@ packages: engines: {node: '>=4.2.0'} hasBin: true - typescript@5.5.3: - resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -10537,9 +10482,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -10568,9 +10510,6 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} - which-typed-array@1.1.15: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} @@ -15061,16 +15000,16 @@ snapshots: '@prisma/driver-adapter-utils': 6.7.0 ky: 1.7.5 - '@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3)': + '@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@6.0.3))(typescript@6.0.3)': optionalDependencies: - prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) - typescript: 5.9.3 + prisma: 6.19.2(magicast@0.3.5)(typescript@6.0.3) + typescript: 6.0.3 optional: true - '@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3)': + '@prisma/client@6.7.0(prisma@6.7.0(typescript@6.0.3))(typescript@6.0.3)': optionalDependencies: - prisma: 6.7.0(typescript@5.9.3) - typescript: 5.9.3 + prisma: 6.7.0(typescript@6.0.3) + typescript: 6.0.3 '@prisma/config@6.19.2(magicast@0.3.5)': dependencies: @@ -16294,23 +16233,23 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.11.1(typescript@5.9.3)(zod@3.25.76)': + '@t3-oss/env-core@0.11.1(typescript@6.0.3)(zod@3.25.76)': dependencies: zod: 3.25.76 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@t3-oss/env-nextjs@0.11.1(typescript@5.9.3)(zod@3.25.76)': + '@t3-oss/env-nextjs@0.11.1(typescript@6.0.3)(zod@3.25.76)': dependencies: - '@t3-oss/env-core': 0.11.1(typescript@5.9.3)(zod@3.25.76) + '@t3-oss/env-core': 0.11.1(typescript@6.0.3)(zod@3.25.76) zod: 3.25.76 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@tailwindcss/forms@0.5.7(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)))': + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)) + tailwindcss: 3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) '@tailwindcss/node@4.1.18': dependencies: @@ -16381,13 +16320,13 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 - '@tailwindcss/typography@0.5.13(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)))': + '@tailwindcss/typography@0.5.13(tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)))': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)) + tailwindcss: 3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: @@ -17273,11 +17212,6 @@ snapshots: builtin-modules@3.2.0: {} - bundle-require@4.2.1(esbuild@0.18.20): - dependencies: - esbuild: 0.18.20 - load-tsconfig: 0.2.5 - busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -17782,18 +17716,18 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.38.4(@aws-sdk/client-rds-data@3.678.0(aws-crt@1.23.0))(@cloudflare/workers-types@4.20260123.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/react@19.2.9)(better-sqlite3@11.10.0)(kysely@0.25.0)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react@19.2.3): + drizzle-orm@0.38.4(@aws-sdk/client-rds-data@3.678.0(aws-crt@1.23.0))(@cloudflare/workers-types@4.20260123.0)(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@6.0.3))(typescript@6.0.3))(@types/better-sqlite3@7.6.13)(@types/react@19.2.9)(better-sqlite3@11.10.0)(kysely@0.25.0)(prisma@6.19.2(magicast@0.3.5)(typescript@6.0.3))(react@19.2.3): optionalDependencies: '@aws-sdk/client-rds-data': 3.678.0(aws-crt@1.23.0) '@cloudflare/workers-types': 4.20260123.0 '@libsql/client': 0.14.0 '@opentelemetry/api': 1.9.0 - '@prisma/client': 6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3) + '@prisma/client': 6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@6.0.3))(typescript@6.0.3) '@types/better-sqlite3': 7.6.13 '@types/react': 19.2.9 better-sqlite3: 11.10.0 kysely: 0.25.0 - prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3) + prisma: 6.19.2(magicast@0.3.5)(typescript@6.0.3) react: 19.2.3 dset@3.1.4: {} @@ -19215,8 +19149,6 @@ snapshots: jose@4.15.9: {} - joycon@3.1.1: {} - js-base64@3.7.8: {} js-cookie@3.0.5: {} @@ -19425,8 +19357,6 @@ snapshots: lines-and-columns@1.2.4: {} - load-tsconfig@0.2.5: {} - locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -19464,8 +19394,6 @@ snapshots: lodash.once@4.1.1: {} - lodash.sortby@4.7.0: {} - lodash.startcase@4.4.0: {} lodash.truncate@4.4.2: {} @@ -20541,29 +20469,21 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.6 - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)): - dependencies: - lilconfig: 3.1.3 - yaml: 2.8.1 - optionalDependencies: - postcss: 8.5.6 - ts-node: 10.9.1(@types/node@20.17.6)(typescript@5.9.3) - - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)): + postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@20.17.6)(typescript@6.0.3)): dependencies: lilconfig: 3.1.3 yaml: 2.8.1 optionalDependencies: postcss: 8.5.6 - ts-node: 10.9.1(@types/node@22.19.7)(typescript@5.5.3) + ts-node: 10.9.1(@types/node@20.17.6)(typescript@6.0.3) - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)): + postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)): dependencies: lilconfig: 3.1.3 yaml: 2.8.1 optionalDependencies: postcss: 8.5.6 - ts-node: 10.9.1(@types/node@22.19.7)(typescript@5.9.3) + ts-node: 10.9.1(@types/node@22.19.7)(typescript@6.0.3) postcss-nested@6.2.0(postcss@8.5.6): dependencies: @@ -20642,23 +20562,23 @@ snapshots: dependencies: parse-ms: 2.1.0 - prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3): + prisma@6.19.2(magicast@0.3.5)(typescript@6.0.3): dependencies: '@prisma/config': 6.19.2(magicast@0.3.5) '@prisma/engines': 6.19.2 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - magicast optional: true - prisma@6.7.0(typescript@5.9.3): + prisma@6.7.0(typescript@6.0.3): dependencies: '@prisma/config': 6.7.0 '@prisma/engines': 6.7.0 optionalDependencies: fsevents: 2.3.3 - typescript: 5.9.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -20993,10 +20913,6 @@ snapshots: glob: 13.0.0 package-json-from-dist: 1.0.1 - rollup@3.29.5: - optionalDependencies: - fsevents: 2.3.3 - rollup@4.24.0: dependencies: '@types/estree': 1.0.6 @@ -21303,10 +21219,6 @@ snapshots: source-map@0.6.1: {} - source-map@0.8.0-beta.0: - dependencies: - whatwg-url: 7.1.0 - space-separated-tokens@2.0.2: {} spawndamnit@3.0.1: @@ -21616,11 +21528,11 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3))): dependencies: - tailwindcss: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + tailwindcss: 3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) - tailwindcss@3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)): + tailwindcss@3.3.3(ts-node@10.9.1(@types/node@20.17.6)(typescript@6.0.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -21639,7 +21551,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3)) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@20.17.6)(typescript@6.0.3)) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -21647,7 +21559,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)): + tailwindcss@3.3.3(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -21666,7 +21578,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -21674,7 +21586,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)): + tailwindcss@3.4.19(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -21693,7 +21605,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -21701,7 +21613,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)): + tailwindcss@3.4.5(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -21720,7 +21632,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3)) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3)) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -21843,10 +21755,6 @@ snapshots: tr46@0.0.3: {} - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - tr46@4.1.1: dependencies: punycode: 2.3.1 @@ -21882,7 +21790,7 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.1(@types/node@20.17.6)(typescript@5.9.3): + ts-node@10.9.1(@types/node@20.17.6)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -21896,12 +21804,12 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.9.3 + typescript: 6.0.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true - ts-node@10.9.1(@types/node@22.19.7)(typescript@5.5.3): + ts-node@10.9.1(@types/node@22.19.7)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -21915,26 +21823,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optional: true - - ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.19.7 - acorn: 8.13.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 + typescript: 6.0.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -21960,29 +21849,6 @@ snapshots: tslib@2.8.1: {} - tsup@7.2.0(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3))(typescript@5.9.3): - dependencies: - bundle-require: 4.2.1(esbuild@0.18.20) - cac: 6.7.14 - chokidar: 3.6.0 - debug: 4.3.7 - esbuild: 0.18.20 - execa: 5.1.1 - globby: 11.1.0 - joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.1(@types/node@22.19.7)(typescript@5.9.3)) - resolve-from: 5.0.0 - rollup: 3.29.5 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tree-kill: 1.2.2 - optionalDependencies: - postcss: 8.5.6 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - ts-node - tsx@4.20.5: dependencies: esbuild: 0.25.4 @@ -22042,9 +21908,10 @@ snapshots: typescript@4.9.5: {} - typescript@5.5.3: {} + typescript@5.9.3: + optional: true - typescript@5.9.3: {} + typescript@6.0.3: {} ufo@1.5.4: {} @@ -22313,8 +22180,6 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@4.0.2: {} - webidl-conversions@7.0.0: {} websocket-driver@0.7.4: @@ -22341,12 +22206,6 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - which-typed-array@1.1.15: dependencies: available-typed-arrays: 1.0.7 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bf662e41..c089600a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -28,7 +28,7 @@ catalog: react-dom: ^18 rimraf: ^6.0.1 tsx: ^4.19.2 - typescript: ^5.9.3 + typescript: ^6.0.3 vitest: ^2.1.1 wrangler: ^4.59.2 yargs: ^18.0.0 From 72433af70edc82c623b65d3cba30e100475006d9 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Sat, 27 Jun 2026 10:00:24 +0200 Subject: [PATCH 09/24] remove examples that were not using Next 16 --- examples-cloudflare/bugs/gh-119/.gitignore | 47 - examples-cloudflare/bugs/gh-119/README.md | 36 - .../bugs/gh-119/app/favicon.ico | Bin 25931 -> 0 bytes .../bugs/gh-119/app/fonts/GeistMonoVF.woff | Bin 67864 -> 0 bytes .../bugs/gh-119/app/fonts/GeistVF.woff | Bin 66268 -> 0 bytes .../bugs/gh-119/app/globals.css | 21 - .../bugs/gh-119/app/layout.tsx | 32 - examples-cloudflare/bugs/gh-119/app/page.tsx | 70 - .../bugs/gh-119/e2e/base.spec.ts | 8 - .../bugs/gh-119/e2e/playwright.config.ts | 3 - .../bugs/gh-119/next.config.ts | 8 - .../bugs/gh-119/open-next.config.ts | 3 - examples-cloudflare/bugs/gh-119/package.json | 32 - .../bugs/gh-119/postcss.config.mjs | 8 - .../bugs/gh-119/public/file.svg | 1 - .../bugs/gh-119/public/globe.svg | 1 - .../bugs/gh-119/public/next.svg | 1 - .../bugs/gh-119/public/vercel.svg | 1 - .../bugs/gh-119/public/window.svg | 1 - .../bugs/gh-119/tailwind.config.ts | 18 - examples-cloudflare/bugs/gh-119/tsconfig.json | 27 - .../bugs/gh-119/wrangler.jsonc | 11 - examples-cloudflare/bugs/gh-219/.dev.vars | 1 - examples-cloudflare/bugs/gh-219/.gitignore | 56 - examples-cloudflare/bugs/gh-219/README.md | 38 - .../bugs/gh-219/e2e/base.spec.ts | 8 - .../bugs/gh-219/e2e/playwright.config.ts | 3 - .../bugs/gh-219/next.config.ts | 8 - .../bugs/gh-219/open-next.config.ts | 3 - examples-cloudflare/bugs/gh-219/package.json | 58 - .../bugs/gh-219/postcss.config.mjs | 8 - .../bugs/gh-219/public/file.svg | 1 - .../bugs/gh-219/public/globe.svg | 1 - .../bugs/gh-219/public/next.svg | 1 - .../bugs/gh-219/public/vercel.svg | 1 - .../bugs/gh-219/public/window.svg | 1 - .../bugs/gh-219/src/app/favicon.ico | Bin 25931 -> 0 bytes .../bugs/gh-219/src/app/globals.css | 21 - .../bugs/gh-219/src/app/layout.tsx | 31 - .../bugs/gh-219/src/app/page.tsx | 70 - .../bugs/gh-219/src/firebase/config.js | 23 - .../bugs/gh-219/tailwind.config.ts | 18 - examples-cloudflare/bugs/gh-219/tsconfig.json | 27 - .../bugs/gh-219/wrangler.jsonc | 11 - examples-cloudflare/bugs/gh-223/.gitignore | 53 - examples-cloudflare/bugs/gh-223/README.md | 36 - .../bugs/gh-223/app/api/image/route.ts | 16 - .../bugs/gh-223/app/favicon.ico | Bin 25931 -> 0 bytes .../bugs/gh-223/app/globals.css | 29 - .../bugs/gh-223/app/layout.tsx | 23 - examples-cloudflare/bugs/gh-223/app/page.tsx | 113 - .../bugs/gh-223/e2e/base.spec.ts | 9 - .../bugs/gh-223/e2e/playwright.config.ts | 3 - .../bugs/gh-223/next.config.mjs | 7 - .../bugs/gh-223/open-next.config.ts | 3 - examples-cloudflare/bugs/gh-223/package.json | 35 - .../bugs/gh-223/postcss.config.mjs | 8 - .../bugs/gh-223/public/next.svg | 1 - .../bugs/gh-223/public/vercel.svg | 1 - .../bugs/gh-223/src/utils/common.ts | 29 - .../bugs/gh-223/src/utils/s3Bucket.ts | 30 - .../bugs/gh-223/tailwind.config.ts | 19 - examples-cloudflare/bugs/gh-223/tsconfig.json | 28 - .../bugs/gh-223/wrangler.jsonc | 11 - .../create-next-app/.gitignore | 45 - examples-cloudflare/create-next-app/README.md | 36 - .../create-next-app/e2e/base.spec.ts | 8 - .../create-next-app/e2e/playwright.config.ts | 3 - .../create-next-app/next.config.mjs | 11 - .../create-next-app/open-next.config.ts | 3 - .../create-next-app/package.json | 31 - .../create-next-app/postcss.config.mjs | 8 - .../create-next-app/public/next.svg | 1 - .../create-next-app/public/vercel.svg | 1 - .../create-next-app/src/app/favicon.ico | Bin 25931 -> 0 bytes .../src/app/fonts/GeistMonoVF.woff | Bin 67864 -> 0 bytes .../src/app/fonts/GeistVF.woff | Bin 66268 -> 0 bytes .../create-next-app/src/app/globals.css | 27 - .../create-next-app/src/app/layout.tsx | 32 - .../create-next-app/src/app/page.tsx | 111 - .../create-next-app/tailwind.config.ts | 19 - .../create-next-app/tsconfig.json | 27 - .../create-next-app/wrangler.jsonc | 11 - examples-cloudflare/middleware/.env | 1 - examples-cloudflare/middleware/.gitignore | 42 - examples-cloudflare/middleware/README.md | 31 - .../middleware/app/about/page.tsx | 3 - .../middleware/app/about2/page.tsx | 3 - .../middleware/app/another/page.tsx | 3 - .../middleware/app/clerk/route.ts | 3 - examples-cloudflare/middleware/app/layout.tsx | 14 - .../middleware/app/middleware/page.tsx | 25 - examples-cloudflare/middleware/app/page.tsx | 21 - .../middleware/app/redirected/page.tsx | 3 - .../middleware/app/rewrite/page.tsx | 3 - .../middleware/e2e/base.spec.ts | 39 - .../middleware/e2e/cloudflare-context.spec.ts | 11 - .../middleware/e2e/playwright.config.ts | 3 - .../middleware/e2e/playwright.dev.config.ts | 5 - examples-cloudflare/middleware/middleware.ts | 37 - .../middleware/next.config.mjs | 11 - .../middleware/open-next.config.ts | 3 - examples-cloudflare/middleware/package.json | 30 - .../middleware/public/favicon.ico | Bin 25931 -> 0 bytes .../middleware/public/vercel.svg | 4 - examples-cloudflare/middleware/tsconfig.json | 25 - examples-cloudflare/middleware/wrangler.jsonc | 15 - .../next-partial-prerendering/.gitignore | 40 - .../next-partial-prerendering/.prettierrc | 3 - .../next-partial-prerendering/README.md | 23 - .../next-partial-prerendering/app/favicon.ico | Bin 15086 -> 0 bytes .../next-partial-prerendering/app/layout.tsx | 48 - .../app/not-found.tsx | 8 - .../app/opengraph-image.png | Bin 98894 -> 0 bytes .../next-partial-prerendering/app/page.tsx | 26 - .../next-partial-prerendering/app/styles.tsx | 13 - .../app/twitter-image.png | Bin 98894 -> 0 bytes .../components/add-to-cart.tsx | 55 - .../components/byline.tsx | 31 - .../components/cart-count-context.tsx | 30 - .../components/cart-count.tsx | 8 - .../components/header.tsx | 58 - .../components/next-logo.tsx | 41 - .../components/ping.tsx | 12 - .../components/pricing.tsx | 78 - .../components/product-best-seller.tsx | 5 - .../components/product-card.tsx | 51 - .../components/product-currency-symbol.tsx | 23 - .../components/product-deal.tsx | 33 - .../components/product-estimated-arrival.tsx | 24 - .../components/product-lightening-deal.tsx | 29 - .../components/product-low-stock-warning.tsx | 11 - .../components/product-price.tsx | 48 - .../components/product-rating.tsx | 12 - .../components/product-review-card.tsx | 19 - .../components/product-split-payments.tsx | 18 - .../components/product-used-price.tsx | 14 - .../components/recommended-products.tsx | 65 - .../components/reviews.tsx | 52 - .../components/sidebar.tsx | 84 - .../components/single-product.tsx | 81 - .../components/vercel-logo.tsx | 7 - .../e2e/playwright.config.ts | 3 - .../next-partial-prerendering/e2e/ppr.test.ts | 28 - .../next-partial-prerendering/lib/delay.ts | 14 - .../next-partial-prerendering/lib/products.ts | 100 - .../next-partial-prerendering/lib/reviews.ts | 22 - .../next-partial-prerendering/next.config.js | 10 - .../open-next.config.ts | 6 - .../next-partial-prerendering/package.json | 36 - .../postcss.config.js | 6 - ...alexander-andrews-brAkTCdnhW8-unsplash.jpg | Bin 119158 -> 0 bytes .../public/eniko-kis-KsLPTsYaqIQ-unsplash.jpg | Bin 97041 -> 0 bytes .../next-partial-prerendering/public/grid.svg | 5 - .../guillaume-coupy-6HuoHgK7FN8-unsplash.jpg | Bin 111468 -> 0 bytes .../public/nextjs-icon-light-background.png | Bin 31283 -> 0 bytes .../public/patrick-OIFgeLnjwrM-unsplash.jpg | Bin 111044 -> 0 bytes .../prince-akachi-LWkFHEGpleE-unsplash.jpg | Bin 36336 -> 0 bytes .../yoann-siloine-_T4w3JDm6ug-unsplash.jpg | Bin 114615 -> 0 bytes .../tailwind.config.ts | 86 - .../next-partial-prerendering/tsconfig.json | 29 - .../types/product.d.ts | 37 - .../types/review.d.ts | 6 - .../next-partial-prerendering/wrangler.jsonc | 11 - examples-cloudflare/ssg-app/.dev.vars | 1 - examples-cloudflare/ssg-app/.gitignore | 47 - examples-cloudflare/ssg-app/app/favicon.ico | Bin 25931 -> 0 bytes examples-cloudflare/ssg-app/app/globals.css | 14 - examples-cloudflare/ssg-app/app/layout.tsx | 28 - .../ssg-app/app/page.module.css | 17 - examples-cloudflare/ssg-app/app/page.tsx | 18 - examples-cloudflare/ssg-app/e2e/base.spec.ts | 19 - .../ssg-app/e2e/playwright.config.ts | 3 - examples-cloudflare/ssg-app/next.config.ts | 11 - .../ssg-app/open-next.config.ts | 6 - examples-cloudflare/ssg-app/package.json | 29 - examples-cloudflare/ssg-app/tsconfig.json | 27 - .../ssg-app/worker-configuration.d.ts | 5 - examples-cloudflare/ssg-app/wrangler.jsonc | 14 - .../vercel-blog-starter/.gitignore | 40 - .../vercel-blog-starter/README.md | 63 - .../_posts/dynamic-routing.md | 19 - .../vercel-blog-starter/_posts/hello-world.md | 19 - .../vercel-blog-starter/_posts/preview.md | 19 - .../vercel-blog-starter/next.config.mjs | 7 - .../vercel-blog-starter/open-next.config.ts | 6 - .../vercel-blog-starter/package.json | 33 - .../vercel-blog-starter/postcss.config.js | 6 - .../public/assets/blog/authors/jj.jpeg | Bin 6186 -> 0 bytes .../public/assets/blog/authors/joe.jpeg | Bin 7196 -> 0 bytes .../public/assets/blog/authors/tim.jpeg | Bin 6148 -> 0 bytes .../assets/blog/dynamic-routing/cover.jpg | Bin 117724 -> 0 bytes .../public/assets/blog/hello-world/cover.jpg | Bin 105406 -> 0 bytes .../public/assets/blog/preview/cover.jpg | Bin 44270 -> 0 bytes .../public/favicon/android-chrome-192x192.png | Bin 4795 -> 0 bytes .../public/favicon/android-chrome-512x512.png | Bin 14640 -> 0 bytes .../public/favicon/apple-touch-icon.png | Bin 1327 -> 0 bytes .../public/favicon/browserconfig.xml | 9 - .../public/favicon/favicon-16x16.png | Bin 595 -> 0 bytes .../public/favicon/favicon-32x32.png | Bin 880 -> 0 bytes .../public/favicon/favicon.ico | Bin 15086 -> 0 bytes .../public/favicon/mstile-150x150.png | Bin 3567 -> 0 bytes .../public/favicon/safari-pinned-tab.svg | 33 - .../public/favicon/site.webmanifest | 19 - .../src/app/_components/alert.tsx | 49 - .../src/app/_components/avatar.tsx | 15 - .../src/app/_components/container.tsx | 9 - .../src/app/_components/cover-image.tsx | 36 - .../src/app/_components/date-formatter.tsx | 12 - .../src/app/_components/footer.tsx | 32 - .../src/app/_components/header.tsx | 14 - .../src/app/_components/hero-post.tsx | 42 - .../src/app/_components/intro.tsx | 19 - .../_components/markdown-styles.module.css | 18 - .../src/app/_components/more-stories.tsx | 28 - .../src/app/_components/post-body.tsx | 13 - .../src/app/_components/post-header.tsx | 35 - .../src/app/_components/post-preview.tsx | 36 - .../src/app/_components/post-title.tsx | 13 - .../src/app/_components/section-separator.tsx | 3 - .../src/app/_components/switch.module.css | 55 - .../src/app/_components/theme-switcher.tsx | 107 - .../vercel-blog-starter/src/app/globals.css | 3 - .../vercel-blog-starter/src/app/layout.tsx | 48 - .../vercel-blog-starter/src/app/page.tsx | 30 - .../src/app/posts/[slug]/page.tsx | 66 - .../src/interfaces/author.ts | 4 - .../src/interfaces/post.ts | 15 - .../vercel-blog-starter/src/lib/api.ts | 30 - .../vercel-blog-starter/src/lib/constants.ts | 4 - .../src/lib/markdownToHtml.ts | 7 - .../vercel-blog-starter/tailwind.config.ts | 43 - .../vercel-blog-starter/tsconfig.json | 27 - .../vercel-blog-starter/wrangler.jsonc | 11 - pnpm-lock.yaml | 5983 +---------------- 235 files changed, 138 insertions(+), 10511 deletions(-) delete mode 100644 examples-cloudflare/bugs/gh-119/.gitignore delete mode 100644 examples-cloudflare/bugs/gh-119/README.md delete mode 100644 examples-cloudflare/bugs/gh-119/app/favicon.ico delete mode 100644 examples-cloudflare/bugs/gh-119/app/fonts/GeistMonoVF.woff delete mode 100644 examples-cloudflare/bugs/gh-119/app/fonts/GeistVF.woff delete mode 100644 examples-cloudflare/bugs/gh-119/app/globals.css delete mode 100644 examples-cloudflare/bugs/gh-119/app/layout.tsx delete mode 100644 examples-cloudflare/bugs/gh-119/app/page.tsx delete mode 100644 examples-cloudflare/bugs/gh-119/e2e/base.spec.ts delete mode 100644 examples-cloudflare/bugs/gh-119/e2e/playwright.config.ts delete mode 100644 examples-cloudflare/bugs/gh-119/next.config.ts delete mode 100644 examples-cloudflare/bugs/gh-119/open-next.config.ts delete mode 100644 examples-cloudflare/bugs/gh-119/package.json delete mode 100644 examples-cloudflare/bugs/gh-119/postcss.config.mjs delete mode 100644 examples-cloudflare/bugs/gh-119/public/file.svg delete mode 100644 examples-cloudflare/bugs/gh-119/public/globe.svg delete mode 100644 examples-cloudflare/bugs/gh-119/public/next.svg delete mode 100644 examples-cloudflare/bugs/gh-119/public/vercel.svg delete mode 100644 examples-cloudflare/bugs/gh-119/public/window.svg delete mode 100644 examples-cloudflare/bugs/gh-119/tailwind.config.ts delete mode 100644 examples-cloudflare/bugs/gh-119/tsconfig.json delete mode 100644 examples-cloudflare/bugs/gh-119/wrangler.jsonc delete mode 100644 examples-cloudflare/bugs/gh-219/.dev.vars delete mode 100644 examples-cloudflare/bugs/gh-219/.gitignore delete mode 100644 examples-cloudflare/bugs/gh-219/README.md delete mode 100644 examples-cloudflare/bugs/gh-219/e2e/base.spec.ts delete mode 100644 examples-cloudflare/bugs/gh-219/e2e/playwright.config.ts delete mode 100644 examples-cloudflare/bugs/gh-219/next.config.ts delete mode 100644 examples-cloudflare/bugs/gh-219/open-next.config.ts delete mode 100644 examples-cloudflare/bugs/gh-219/package.json delete mode 100644 examples-cloudflare/bugs/gh-219/postcss.config.mjs delete mode 100644 examples-cloudflare/bugs/gh-219/public/file.svg delete mode 100644 examples-cloudflare/bugs/gh-219/public/globe.svg delete mode 100644 examples-cloudflare/bugs/gh-219/public/next.svg delete mode 100644 examples-cloudflare/bugs/gh-219/public/vercel.svg delete mode 100644 examples-cloudflare/bugs/gh-219/public/window.svg delete mode 100644 examples-cloudflare/bugs/gh-219/src/app/favicon.ico delete mode 100644 examples-cloudflare/bugs/gh-219/src/app/globals.css delete mode 100644 examples-cloudflare/bugs/gh-219/src/app/layout.tsx delete mode 100644 examples-cloudflare/bugs/gh-219/src/app/page.tsx delete mode 100644 examples-cloudflare/bugs/gh-219/src/firebase/config.js delete mode 100644 examples-cloudflare/bugs/gh-219/tailwind.config.ts delete mode 100644 examples-cloudflare/bugs/gh-219/tsconfig.json delete mode 100644 examples-cloudflare/bugs/gh-219/wrangler.jsonc delete mode 100644 examples-cloudflare/bugs/gh-223/.gitignore delete mode 100644 examples-cloudflare/bugs/gh-223/README.md delete mode 100644 examples-cloudflare/bugs/gh-223/app/api/image/route.ts delete mode 100644 examples-cloudflare/bugs/gh-223/app/favicon.ico delete mode 100644 examples-cloudflare/bugs/gh-223/app/globals.css delete mode 100644 examples-cloudflare/bugs/gh-223/app/layout.tsx delete mode 100644 examples-cloudflare/bugs/gh-223/app/page.tsx delete mode 100644 examples-cloudflare/bugs/gh-223/e2e/base.spec.ts delete mode 100644 examples-cloudflare/bugs/gh-223/e2e/playwright.config.ts delete mode 100644 examples-cloudflare/bugs/gh-223/next.config.mjs delete mode 100644 examples-cloudflare/bugs/gh-223/open-next.config.ts delete mode 100644 examples-cloudflare/bugs/gh-223/package.json delete mode 100644 examples-cloudflare/bugs/gh-223/postcss.config.mjs delete mode 100644 examples-cloudflare/bugs/gh-223/public/next.svg delete mode 100644 examples-cloudflare/bugs/gh-223/public/vercel.svg delete mode 100644 examples-cloudflare/bugs/gh-223/src/utils/common.ts delete mode 100644 examples-cloudflare/bugs/gh-223/src/utils/s3Bucket.ts delete mode 100644 examples-cloudflare/bugs/gh-223/tailwind.config.ts delete mode 100644 examples-cloudflare/bugs/gh-223/tsconfig.json delete mode 100644 examples-cloudflare/bugs/gh-223/wrangler.jsonc delete mode 100644 examples-cloudflare/create-next-app/.gitignore delete mode 100644 examples-cloudflare/create-next-app/README.md delete mode 100644 examples-cloudflare/create-next-app/e2e/base.spec.ts delete mode 100644 examples-cloudflare/create-next-app/e2e/playwright.config.ts delete mode 100644 examples-cloudflare/create-next-app/next.config.mjs delete mode 100644 examples-cloudflare/create-next-app/open-next.config.ts delete mode 100644 examples-cloudflare/create-next-app/package.json delete mode 100644 examples-cloudflare/create-next-app/postcss.config.mjs delete mode 100644 examples-cloudflare/create-next-app/public/next.svg delete mode 100644 examples-cloudflare/create-next-app/public/vercel.svg delete mode 100644 examples-cloudflare/create-next-app/src/app/favicon.ico delete mode 100644 examples-cloudflare/create-next-app/src/app/fonts/GeistMonoVF.woff delete mode 100644 examples-cloudflare/create-next-app/src/app/fonts/GeistVF.woff delete mode 100644 examples-cloudflare/create-next-app/src/app/globals.css delete mode 100644 examples-cloudflare/create-next-app/src/app/layout.tsx delete mode 100644 examples-cloudflare/create-next-app/src/app/page.tsx delete mode 100644 examples-cloudflare/create-next-app/tailwind.config.ts delete mode 100644 examples-cloudflare/create-next-app/tsconfig.json delete mode 100644 examples-cloudflare/create-next-app/wrangler.jsonc delete mode 100644 examples-cloudflare/middleware/.env delete mode 100755 examples-cloudflare/middleware/.gitignore delete mode 100755 examples-cloudflare/middleware/README.md delete mode 100644 examples-cloudflare/middleware/app/about/page.tsx delete mode 100644 examples-cloudflare/middleware/app/about2/page.tsx delete mode 100644 examples-cloudflare/middleware/app/another/page.tsx delete mode 100644 examples-cloudflare/middleware/app/clerk/route.ts delete mode 100644 examples-cloudflare/middleware/app/layout.tsx delete mode 100644 examples-cloudflare/middleware/app/middleware/page.tsx delete mode 100755 examples-cloudflare/middleware/app/page.tsx delete mode 100644 examples-cloudflare/middleware/app/redirected/page.tsx delete mode 100644 examples-cloudflare/middleware/app/rewrite/page.tsx delete mode 100644 examples-cloudflare/middleware/e2e/base.spec.ts delete mode 100644 examples-cloudflare/middleware/e2e/cloudflare-context.spec.ts delete mode 100644 examples-cloudflare/middleware/e2e/playwright.config.ts delete mode 100644 examples-cloudflare/middleware/e2e/playwright.dev.config.ts delete mode 100644 examples-cloudflare/middleware/middleware.ts delete mode 100644 examples-cloudflare/middleware/next.config.mjs delete mode 100644 examples-cloudflare/middleware/open-next.config.ts delete mode 100644 examples-cloudflare/middleware/package.json delete mode 100755 examples-cloudflare/middleware/public/favicon.ico delete mode 100755 examples-cloudflare/middleware/public/vercel.svg delete mode 100755 examples-cloudflare/middleware/tsconfig.json delete mode 100644 examples-cloudflare/middleware/wrangler.jsonc delete mode 100755 examples-cloudflare/next-partial-prerendering/.gitignore delete mode 100644 examples-cloudflare/next-partial-prerendering/.prettierrc delete mode 100755 examples-cloudflare/next-partial-prerendering/README.md delete mode 100644 examples-cloudflare/next-partial-prerendering/app/favicon.ico delete mode 100644 examples-cloudflare/next-partial-prerendering/app/layout.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/app/not-found.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/app/opengraph-image.png delete mode 100644 examples-cloudflare/next-partial-prerendering/app/page.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/app/styles.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/app/twitter-image.png delete mode 100644 examples-cloudflare/next-partial-prerendering/components/add-to-cart.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/byline.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/cart-count-context.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/cart-count.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/header.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/next-logo.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/ping.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/pricing.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-best-seller.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-card.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-currency-symbol.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-deal.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-estimated-arrival.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-lightening-deal.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-low-stock-warning.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-price.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-rating.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-review-card.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-split-payments.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/product-used-price.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/recommended-products.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/reviews.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/sidebar.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/single-product.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/components/vercel-logo.tsx delete mode 100644 examples-cloudflare/next-partial-prerendering/e2e/playwright.config.ts delete mode 100644 examples-cloudflare/next-partial-prerendering/e2e/ppr.test.ts delete mode 100644 examples-cloudflare/next-partial-prerendering/lib/delay.ts delete mode 100644 examples-cloudflare/next-partial-prerendering/lib/products.ts delete mode 100644 examples-cloudflare/next-partial-prerendering/lib/reviews.ts delete mode 100755 examples-cloudflare/next-partial-prerendering/next.config.js delete mode 100644 examples-cloudflare/next-partial-prerendering/open-next.config.ts delete mode 100644 examples-cloudflare/next-partial-prerendering/package.json delete mode 100644 examples-cloudflare/next-partial-prerendering/postcss.config.js delete mode 100644 examples-cloudflare/next-partial-prerendering/public/alexander-andrews-brAkTCdnhW8-unsplash.jpg delete mode 100644 examples-cloudflare/next-partial-prerendering/public/eniko-kis-KsLPTsYaqIQ-unsplash.jpg delete mode 100644 examples-cloudflare/next-partial-prerendering/public/grid.svg delete mode 100644 examples-cloudflare/next-partial-prerendering/public/guillaume-coupy-6HuoHgK7FN8-unsplash.jpg delete mode 100644 examples-cloudflare/next-partial-prerendering/public/nextjs-icon-light-background.png delete mode 100644 examples-cloudflare/next-partial-prerendering/public/patrick-OIFgeLnjwrM-unsplash.jpg delete mode 100644 examples-cloudflare/next-partial-prerendering/public/prince-akachi-LWkFHEGpleE-unsplash.jpg delete mode 100644 examples-cloudflare/next-partial-prerendering/public/yoann-siloine-_T4w3JDm6ug-unsplash.jpg delete mode 100644 examples-cloudflare/next-partial-prerendering/tailwind.config.ts delete mode 100755 examples-cloudflare/next-partial-prerendering/tsconfig.json delete mode 100644 examples-cloudflare/next-partial-prerendering/types/product.d.ts delete mode 100644 examples-cloudflare/next-partial-prerendering/types/review.d.ts delete mode 100644 examples-cloudflare/next-partial-prerendering/wrangler.jsonc delete mode 100644 examples-cloudflare/ssg-app/.dev.vars delete mode 100644 examples-cloudflare/ssg-app/.gitignore delete mode 100644 examples-cloudflare/ssg-app/app/favicon.ico delete mode 100644 examples-cloudflare/ssg-app/app/globals.css delete mode 100644 examples-cloudflare/ssg-app/app/layout.tsx delete mode 100644 examples-cloudflare/ssg-app/app/page.module.css delete mode 100644 examples-cloudflare/ssg-app/app/page.tsx delete mode 100644 examples-cloudflare/ssg-app/e2e/base.spec.ts delete mode 100644 examples-cloudflare/ssg-app/e2e/playwright.config.ts delete mode 100644 examples-cloudflare/ssg-app/next.config.ts delete mode 100644 examples-cloudflare/ssg-app/open-next.config.ts delete mode 100644 examples-cloudflare/ssg-app/package.json delete mode 100644 examples-cloudflare/ssg-app/tsconfig.json delete mode 100644 examples-cloudflare/ssg-app/worker-configuration.d.ts delete mode 100644 examples-cloudflare/ssg-app/wrangler.jsonc delete mode 100644 examples-cloudflare/vercel-blog-starter/.gitignore delete mode 100644 examples-cloudflare/vercel-blog-starter/README.md delete mode 100644 examples-cloudflare/vercel-blog-starter/_posts/dynamic-routing.md delete mode 100644 examples-cloudflare/vercel-blog-starter/_posts/hello-world.md delete mode 100644 examples-cloudflare/vercel-blog-starter/_posts/preview.md delete mode 100644 examples-cloudflare/vercel-blog-starter/next.config.mjs delete mode 100644 examples-cloudflare/vercel-blog-starter/open-next.config.ts delete mode 100644 examples-cloudflare/vercel-blog-starter/package.json delete mode 100644 examples-cloudflare/vercel-blog-starter/postcss.config.js delete mode 100644 examples-cloudflare/vercel-blog-starter/public/assets/blog/authors/jj.jpeg delete mode 100644 examples-cloudflare/vercel-blog-starter/public/assets/blog/authors/joe.jpeg delete mode 100644 examples-cloudflare/vercel-blog-starter/public/assets/blog/authors/tim.jpeg delete mode 100644 examples-cloudflare/vercel-blog-starter/public/assets/blog/dynamic-routing/cover.jpg delete mode 100644 examples-cloudflare/vercel-blog-starter/public/assets/blog/hello-world/cover.jpg delete mode 100644 examples-cloudflare/vercel-blog-starter/public/assets/blog/preview/cover.jpg delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/android-chrome-192x192.png delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/android-chrome-512x512.png delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/apple-touch-icon.png delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/browserconfig.xml delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/favicon-16x16.png delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/favicon-32x32.png delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/favicon.ico delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/mstile-150x150.png delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/safari-pinned-tab.svg delete mode 100644 examples-cloudflare/vercel-blog-starter/public/favicon/site.webmanifest delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/alert.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/avatar.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/container.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/cover-image.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/date-formatter.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/footer.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/header.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/hero-post.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/intro.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/markdown-styles.module.css delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/more-stories.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/post-body.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/post-header.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/post-preview.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/post-title.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/section-separator.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/switch.module.css delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/_components/theme-switcher.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/globals.css delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/layout.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/page.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/app/posts/[slug]/page.tsx delete mode 100644 examples-cloudflare/vercel-blog-starter/src/interfaces/author.ts delete mode 100644 examples-cloudflare/vercel-blog-starter/src/interfaces/post.ts delete mode 100644 examples-cloudflare/vercel-blog-starter/src/lib/api.ts delete mode 100644 examples-cloudflare/vercel-blog-starter/src/lib/constants.ts delete mode 100644 examples-cloudflare/vercel-blog-starter/src/lib/markdownToHtml.ts delete mode 100644 examples-cloudflare/vercel-blog-starter/tailwind.config.ts delete mode 100644 examples-cloudflare/vercel-blog-starter/tsconfig.json delete mode 100644 examples-cloudflare/vercel-blog-starter/wrangler.jsonc diff --git a/examples-cloudflare/bugs/gh-119/.gitignore b/examples-cloudflare/bugs/gh-119/.gitignore deleted file mode 100644 index 69566c5f..00000000 --- a/examples-cloudflare/bugs/gh-119/.gitignore +++ /dev/null @@ -1,47 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ - diff --git a/examples-cloudflare/bugs/gh-119/README.md b/examples-cloudflare/bugs/gh-119/README.md deleted file mode 100644 index e215bc4c..00000000 --- a/examples-cloudflare/bugs/gh-119/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples-cloudflare/bugs/gh-119/app/favicon.ico b/examples-cloudflare/bugs/gh-119/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/examples-cloudflare/bugs/gh-119/app/fonts/GeistMonoVF.woff b/examples-cloudflare/bugs/gh-119/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185cbfd16946a534d819e9eb03924abbcc49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67864 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ϣ%UY8`RWp~V-U_7 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 CLkb=M diff --git a/examples-cloudflare/bugs/gh-119/app/fonts/GeistVF.woff b/examples-cloudflare/bugs/gh-119/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daacff96dad6584e71cd962051b82957c313..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66268 zcmZsCWl$YW*X1l87)X>$?@vE);t4{YH1mFe0jBE_;zih3)d=3HtKOj};a$8LQ z;{mKizBoEx@QFoo%Q3U|F#Q_99{@n6699-amrKppH2XhZHUQxC)koh9Z`96Da}z^j z06>M|%Z~L6Y&1qSu;yQl0D#8RSN+!)NZ{U~8_aE--M@I|0KoT10055byf;V0+Ro^U zCui_=E#qI~`=w~)LS|#={?)gfz?a>x{{Y1Z*tIpZF#!PdSpa}6(AxtIw;VAx60fHIlil?>9x#H)4lkwAf#?OoR zq}|UH1-_GP?ro-XFe6E6ogAsB_lMb{eMTseU$Q#8C1b*`2YJE2UbHtB7q=F#8c?(} z7MH~UQP;KATrXR0jxH^-9xhh?btgLZV8`yP{4?~5t>#`dU`oKckttiKqS}=0h)-TL zm0*m)Fqi`0;=bZIlJL!*^OrHroA}Fuoxd5CU8V%At$}@aT%_Z<7=JytQ)D?oC4fu; zC9haKy!Hbi0eF1ipxzXiPt=aQ5wop-RG^?s>L>gO@@+lUXG(XGZgCD!0D&Zs4~^e% z(4?{(WBL;9gTH%!vIjaaOL4-?5F%AuAhqP$}Z5*a}4%FHO z__`OOSOe6f$5}vgbHKxcU-p9ue+OOu{ZSHabi?^-WyLLrt+h>i_s0J8MO%1(?6KJ{ z63srC7MKwg5YmV8R^udkjP>c;o0jS%3s1#VZSd_ZMMe}<_%<&|(8tdaVsob9SlD{! zxA!4>pO-DKVwcU1_Qs8{!D!x(rP>~w#&w_8M_z*m4KGu9`d7DfIq*xDA@Pot6Re`h`d%{lBo3am-vR=-J-SO9A>&egV84q&m&9c$A=5 z%sfs3V4GByk@8gn49E{h<(XwIcWcps58AEdX7(zpG>h`7(%)_eh+vz{k!pm%BiGC` z_=5Uzd3aO%4=d~2*uWjw8`-E&TB2z!BU(IgE;XDXw1NdI?B6(MBrV0BsbKgOQ)gVq zTiiW$Yclle$O3+`9mkU9lI}kdXSxZCVc3#pUpLeJh8n71U(M+H_oIWzXjf>?Ub;nl zgr}Vj|2|%YuvXf+F+N$AD`H8>BgpF)5=3ZV&6AF!QO#3~-9`j5fsyJ#B#%vv4OtoE zoN*Lf4;gCHrm9!=;fkWSwnDPm>OzFyN{<}u3vWw{2o9!32OW3*>roJVbmjZQzlG(e zE4}U2iH!Q@$Q{J!?*)q_&o{ma{Zw*#>>xizG(K?ovKtF`xdX~MyHu+y&V2B#8?UA} z3)GS+=ALKVHi<)w-QE08#-CNleh`G&y`sLDidTfmrv{gWy`!r=i}Q2v#-<1h==FuW zo4*3ygV;zyKBgxN{?HQ@hj_U+#I$gm{DHH5VFhB{&2 z43OeSH?8bW8=avoZjrZrTVFiF@fH_w@Xx3vrm3WK)B*ir9HxIFotJ&j?Ql0|_MlDW zFAFtz22CtP@SyIE`u?GZ)=dVaum({0Bk5$QOjPFeR;d)dg^tAMWb#XR zx1N+SC{!SJ|LgCF#-Y>9V0n)&ec+ON<`=rB^tflD@PO&5dd1P!f>fx9N5?Gz0tYaF*sLZO0G1fGI zJBmO(<#@h+D1mjw+HK82Tc@$VtNxi% zE|8*n7FS*<*b%&+mElheV^vn-j|^j#B3O7EpDyIt*oZgUdgrVD+nieQ%oCn z=tvim?Kk=%r6-5a5KYn{cSN(c#);ls)$rs z$>2WG89OeQn+$u%7X^jeuG!?UPZfU>)k2TT`WR;^in+~$27hvw5jonPA>KXZH+n=U z-HdTmV=8Uz@-l4RwROKIHX;)pYhnQ{-gA8{I9_E$1U2#W?a|Z=G1jId8eMbFB2X74 z`tO++;x+F#xG;{RF=LA2>8C&>LFr85=i$Wb6{aFrO{Wxnxot^AOP6_d{#zLQ$rDOh zmx8VSzye=SUQ$IMq75xI4HXEA59Fnh)i7cO!uVPQIAC%WY#)85)HZ%qC7?%_55Ys0-MmZ(mFLWpk4!|Q@tKYGc|M5aQKvdmMnP?P5ZYRPA@UcNk!m! zYM=N4>}|X9#ViD-@-{OA)mQFn9XsaS7Y9(?%-TyN$#35%!F`M`?q#}XOl%HVhbwjt zCD9hq%W@?Vb7iv9#SQ!^zs1Ahj*)z0u^gwJ$gQZK>LPl(dju$D&tWsLLmc6KaS3pr1Z2W;DVO|v_@95?1- zMM>VRwrEw^(?(cgn2z03cSM3w9re}A9@&J-iar~ThaWK;6qbgl9R+_nN+$C===>ifAHw@+mVJro54y_ie`FBKhGpGJfp{7P=$nYHDU85j@aE6xcjU`6`n+UdYu z;k~!=E%i><*SAqRV{@mB5+D#ad!{z`YfsejCwwfQ^S{HX?u$eA4ev+DnZ3iM@r`m+ zLRU?0^iI5+CYyk-JQeAW21GoJm#CuR4}=^0OawIPmLf^Bj+NP;px>mQ@ju91?hU?A z@^6NFDk5sm}DxK#dVoV-L%Npvrr+ooO@;l>4Y7QQ- zdW3cE{K)ywgL|nTIL7??f&XRGbC`}V$#eCsHr>w^yd7NU`;^EDQzm7ei3K5D%lm`+ z_NbNiy=Tm2b-)>1W5&6%wKhpFs?&aw_c-nSe6$OHn}oFM`AT6SSBsV1dD$@{#%ECO zaiNNq2pee!IeZP@I^E+v@_!MPqwA4mCt$2(@-z0LcW4k^>Eo>KuM~B@sNL97E6TFl z1)4A2mU)d_2f0GJOww_Oc7q4(mz@Oz)qi8`E+3Ka*{~&X^P|?>khUM&hA! za-0+zz-fA;NCpK8V8&lEAj~kov2%5g?yoc=(AvRjAGX}w(W#TavcyO)!zy( zBwy-z_~z`5c)^_D?7n6Bk6s#PY%1IH^>8*9DYTP!!0{`s;pmNC!t)DD8_4WWoHDid z?f}^jLEV%i`>#l)r6O{$EICF?lGtwyEIZdkw3-n3GcpRG_G3g24WI%{ z$9%gN{?t7?aUhEagsS=Crvcft)p%O>j4XBnA15^iRW@>yZTAu@VcFtzH z7Pjzcy@{m*?pI;}+Li)cVqSjK+o9$8<#htd>v|Z!spzHUXXhL2&VAWwmO>TOz#2F* zLKBCt%h1UO`bcZm61+W2uiv-$*AWdy4%*JD#Q%mVN~LX?P?L)W5)_vf~Eysd%ifN06o<4DrIb zo`rgBZ)aY-Er1H(R(loTgeRKc`aiNY*ov~%7tdG23sIk0S|&| zI`ym(F~+g~Z@5Ak*#hsXsk%wMma1o}98R11$`-WqDhE~YQA+mXDy(Q>%<^37G)?hj z+kV3owb?Lm^=xvbUF5qgnn3}%i9dP8l?^m`M069e_$gUu1G~Si$r#Db>RW?Xxr1i3 zU}3e66CnC_N(ryScVhF%p7!Zs;o9%K&6EYZ3oRWH+nY=r>ML5RV}UVM5LU3?&R^3c z*yGY}>NGt9GBX1LpI6=voIS=^Xvm|6n<>r?b&=nFv_-Z%Mm7gp! zSI@=w{S$c{z45YBG@x~lPoG6l=DOXaZPZVlw2+33otl)CnYysT!Y~2K-zCtw?30-Z z+j4f4G}f{>C*}kX%RUJeNc7CBpe@lm@?8X1D0HyuJA7fg9{pXg(i_i5pHz&enAz99 zWY3;MKvcgk8C$XtDv6Yv9nuV?irv9MVk&VuUm#O*IQgealiPX?FMl0-hGD?jlbT|; zME&f##=f<={Z30HDUKa?&A?`}^JL%n$By&#!^_LLX#Hw!dL^x^o6ADIYq{oZ_wI$f zBPDV!nu9vX(9U=M4q63-<+v6a=_auzKjbnp>~RgNBkd^lU158+SLy@%Fg|_0De54h z^rK{5>e-9~goCutBe7pS^s-`ZU@;qFoc`@|Uwyz__~mA3V5aaYCZ<4e6g-K3SmT;h z@it4I5vQD*>)Q*Fk+6`Eb4vzkclOo0&Bf~(wh1Wr-GBRg!}h;jXKPr10(}{2!1D1% zZnFF}mr~=Vjw0b47Mu_oQ`l$EqB>V3NVJyRF^Qh4r|cIXJIkCIu|e32zE3D{>g4&%2EEepV0ihrnN0lI*h$OJUUNEJ+f5_s5*kt zmQfjSrXy0*UszZofNBGqi063mn#*;wW}5WUXL;JVcPLTyPpbj}@IfE`+)C3>1iy6( zj@xZ`!%VYN^QX6s+4^nia$?ubBc1sgz=wkk0rC;u!2s(j`^WgqwSUq;DL&UAG&u(% ztx2nnfUn_>ZkfgUW8E9g}L@NcOjYNW~s;MKbcH~h0cpk{_HWNdfijblYz+h2z03P3!{w_^F+Z{6(m;mYyc?e=$R~S7W6r)rmnhc^ zWDY8UgC=qhHXPr6E&p}OFapx)Yqfq0c|%ScJfo!5%;`l<0^eYMGZSctYCudt4D;QS zllZXAwPzujN)eGld?PN9>@xFHYu!q3RYPgwD4^+{ZX+R4pqMO?|LJJ$&|pqT%}z(2 zws%$GBS~6_4OO$4U!NF5sidchXC;p!pWSoPq9I=D?mxL{Zt)>jI<~1LE1+Oz;S?N` zsjnlQu+gxjSKXW_*MzO^o#-wU70)7mu(uLfuB-0YqK5E?-e-<1nICGBYERzbSu?t- z1J9I?E{8Qu_&Px*?|>1;GK>itJ}M{~z2zc|c`DfS=_rwR>wbvoH*rc9Ca=CCq-4Jh z+IxAat$A_beud7*u*t20_~6e9o9BJn_Ho1ME|LyR2HWhz8j>^3+Tpo;1 z#OP$C#H+-wZB1(eXsCdjH8Y>Be8*l^l2z0+y_nU@-|33tBxzRwJX*%MM2dIi{#=IoY<7?7I@41JDTMl z|9r8UIP#bjPm~nR+<#Sib?~q)WS#taf5E>&WYVfkl0n+1X*26v+XO>&f<8pb)x%vS;$rMu{Rcy+BTIL?an0i7iczQl+`d} zYwfz$K@_rR)TcHqJ%uE`{3$4djVoPQ;Hn?ilq^IOYxj-eWN$8weIZ>f`k+fXTv4XV zxXVid5tejj=$k{SJ|9C8d_7#uwA^RYU!2J#ik0bpw9U$J7X!0I3Cu;srmBFnZmXU! zu!~xOmIrL+e;d4Fy_Yn8BTM_b>7-kEqBb{bS3=bJ-^ zArybG{xTk8B}Ff%l0yRj=@m6PP)-nCvyy%R%;|U!{>YrP!}BK`AZ-hu>ElmSHK=&> zEupkk&(|o!b>Z|PcSs`6=3@`isI1|I>wG~8HCk8BNXvslF zb2qb{NmN5#uR-97^5i7Y3#R5QJ74sp0$r%yKu?ed&+ivClsUAJZB~9o<~Q6;L}dp| zgxwnq#X_ME*@s7~+yMyT#C>E|gD=JjzeA}2|Gfez+Cs^Y@3HvO`zi4Y z2oH@RhUH`=t1aWXIifih7aEhgjrV*`ZHH6adZ_+ar&ZyfD2E$B z6i?p|;Ppl5a{2F&Nn$CdcSjfBzTQctXYmW#oGbBx!zpUKne^JrV-1O*A zte39UNS;l(F=?FNaY}cPnV{;IWxW<}kbX@ieFQx@krv%HfvG%4XlKg9O7V3+8>hFt zsZ_-g>;fy72bHS{qLMf>2diP8r87W*IH+%^i_F?^Vcf&!KcIFoE=h>1+K_QCN5_s_ z4q#&aN9h^Ld$%bf!>GnfOUhgzxE|*hE-EA?ojuK5A@-75Y%0`lR@w?JsH>*y%6tpk?I`Tui&N%cfoY1R<> ziTCSG=en`fKl@2rmFUkA)=$oTW&^T_;Wp@KWjYX;@4#NB@x@!36O)_Th#4Bu=8*MK zKC=NwyP~_@yce6Gz$)Y@)bwMU2i2q)9rf>$?y76AlgTZUdG4W6;#_}FOmo!8WcV9? z=tw8waqML#6=2IOVbtwANc83v@=3>m-{G0{Ny)8;7W=g^yEtkE^>yoYbICa)d+sE5R5 ziLK%3zGNws91-!M=Gf<__>gK>e=N=WaVosXzjacH1QSgiHH~f)O#=+XaX|Rsy<^PZ z+N0swA*aXW@XXfN_}RltlFet{@n-5?bzS1KAire&KbctG3g4A!B3yFxfvaUB0=oHU>7e+qgGXcrRVL zaJBKZ_7?3UZ~OFGJ@XP}4U>$LdyBF54(1j_{1m|hWwpUDgwKj})AR%%l7uYevu|w~ zkBOe1zQNCkzkSc_-nZ%ZL1wYmEb(6jIMU>7Yg+K%!3ogU`%s>|sEID}D>#`ArT1Xg zY3DbPR2EFVq|exiDiMyL{;h7zv1OiG^7pKqV>Nm=z2UX6`q@g1l92J6cc+a@kZm*I z1)8d3#;T!<7VjIabqo@eyQoJ)37|fr}Z$3c;pZLeiyn9}` zOV#On7kX{lo-U2XtHNsMgs1tS-$8(nM4yol$L~+TU_|hSo}B(aT+{L@Qqtw>&LoFVZ&5)JcX<|jF-?{%dp72IDUzD0V*CKhi2*j^8=68STUt&br&iVp zT&BuNStFLR+Z&i$V42R4;X^c+lSmq13oJAc!GbaOKI=Lp0;>JnzgjCjp67xP4qg9a zdR?9CTpwbT3D8_T3Xu@c7&a8<3RUEg#=nkbg0w+8cqc?u^a08zbMm@Aj|2z%eC+0^ zql|__mJH(p_&ZY9I9)`pcdL0P#sxFdeI2ZfGdQl2{heylGP}w_1jKaz3a+xS@%id) zUXNpAXIJ~d{kp)a&3uJ>KeBkF0>+^h%Q=^5J_{f0O-z>PK22*&cP1cXs-$D9ble+= z=~ByXN64k!9VyHHrr*1R(d9x1ns%vcOG)`V zQ)GPJ#*rwA?dc^MkkKtXkNRsa6q5~dJ6-YNo3j!4o!ms;ejpQ=^?m|rTJiRsg{K^5 zM7|8=3C>L;f(3o71q@ZNtzz4^=Fuj+G^&VWgU!g5T&)PxJb%5;=Q=oV5ZTVL+>-dx zhhj@57~9XMJMd%ThH!JwXU+%2)FLU@1Uk_VOT~m8v)Dkv{-tP3(1{W3lsxylL+)Ams{`mFkBBHjmQA(dV4hlVkETa_SZqb@%q znl$-FD&x1SE-}P^LFZj6804F6E=n>Fjh=Og^ix@pmsBrc;SD;KvAb}^#tTq|XnPVJ zpT2sEeG7j1wQD4@_IZCbtQ+%9$cJfH+nzm7ZuJ_=8dWlMMAS=kbX_atKBec%d{?j6 zMT6`Wiljm1dZ+vZ>{ozBVSFPAiexw&_`jBDO04g7sG4t^{7&T_s(;7^OJkPNAk7EeNPJB+3 zvnI>9baeSf@IPpZWe^9Ev^W9*!{4{x=I31$Z|j8kg4qYeZnj)K>zaEC-uPo>RSdLE zc5^nm$Is!d8}Ln;f6P3~vKgXj)_-B2uSEdl}Se4P3<09 z^@w?vWg%xH_Jh8+7{G4dT9PLFNw#Cn%B3(2XpP%XOtP_Pkbs9kV z$Q-3kxGQq+N6qKq^axgH)t_hF!-n7lva+Iw5CB1Z-2D814juglNK5g0+ch`iw<~fn zBWiwk;dB}#ap%1RpZax*IFkCNe69y@xvGr^2Afgy<;hRjPZ&4)J9UVSLbPd*Li8;& zj#t5gx0#(>uO7y{KHFrUSnY5iQ0@N6dsnw_XV|c+=cU4sBcs8D_UkF3q_a)o2PEyF zbx!;+GWe_i*JgQHGt(zo)>&;KdH-r4|K=fgzy_@zMbL|azNlnsLrvmF=z&Dr_F>=o zOyF^3ZU?9&s$M>Umkl(GgqVraCNJfNUCn%G@b_nHt!Eto8>uzL_&DQ#UKq=` zEOCp8rf~adZdQ?Loa}6dzb~63LkY2ne7g0#S%1Qt>FW9*{J};0(eM>Uzxxx+Jc=Sw zNbr5M_&QPzoZD-!SVIZ2uWzT1bQFtWLBLeutjw; z$)QUUFgL}$slTMW_j9~~-^lx*3A=|OsaHGxyolndAN+|6ft0Ht44TqVo7R95)TnNp zQPr`<3|W_hYJ{+oFnY|oclbRNqpM?1ZI3)7DWPW?MC-KgzoKB4o$cuW)CsOirDD1w zYu)U^(;c3@$p6$5*I$McZuo=gLiFH--|M}MGVvfh^UWW1Xk z488s>afB{8n19#I#%Qg?lGX-cA!ZQ4>3`_FPJvUKpF0!VF%u(QnO~)ezL2D@n4T!J z^TLk=W9ioU>M>iMaW}C(=-VESzwQY4UB6i(J)vX3hlOv*D;9`p!YA;Jo09ZALCS0x z``9xT+*}tmjgwkb^Ht;=)Ha!3m$Ej3da-!tbc8;59KaUhVqo*5YWio)fbPmVPBcs1 z+E63@FJJHMU>@vmiQydDtYDEDw-;?c`FlUhl)EW~JP2Mw#)x;w4hND9y52uN1_s_U zbd_D{vg>WVjMxf{SyxjYYv!SG;qijw`Avz%TbMSMhM?mvIZsNd^g$c$N zjY3h7e`WP_q^S_Dy4f4fx-AJ5imltL_1J#=C9HNs((E^m&@8SiY?#ONNoMOI@>V{| zzt8Ato5|}rgG6+Vlv&z@Jl89_!mE$lDYbygNM$O9HcfPZ8)J&)hQ5)GD`$Pp07xQF zz?AEtd23`xy<1Ka)JF^Wrs@gF){X)*UPwPU%$$DHY3tQ6>{Qy( zI+f9}N*VO;dNX^!aO=whm+vK|KxofHRE+nIq|`WcH)SPb3^IW+jjZ=GtMEFhD9ZBe*g4qo_y3(B`47t?#J9n|fsREt^6+oZnYE|O>VMg+UqNs?XySy+NRDe)ZhJ21Dg9^xuAx;~ADlE4?&9K+FY zLY4OquJPQc%9&G=agFz$sVapHEv;W~Z~-$7(71afdx?2z$CZQEcPm+W`E#ptJe_EF zNs=>4HZsJh-4Qn(h6^Ly;cS>|l~Oy?Vb**xPSqlKMvd+md;Jbp5$L(AjPu#&qk;SC zAt$%M%wCWtQ^L+WOVlob&+GL-GaUCk#gJ^FLpSQBfr6E<#a#buo+bMG8I6`=zw;r!Zr#``Y6%cj7(T>{_-N(%43famwv!j2H*;aMnE} z3GVb9&|gq~f{@+%UQ0=%)KWoB_Ja5(-oZW5k!XrVeL$#1)yf?DPP>*7gtBIkO=2|+ zk~!gxywqm20328+c`k!6&&}#+`iC12b(fR~H@v`kgQjgjkhYliLxiiTJFyoT;X5wY zcxSuxt=;A-b_ohLABKbb?a(Jhv(SoLXjJ*6#VgC^Io-IMR~6zl(u$kjz>u4tzd>T> z`OWiT@O8#+O-b3Dj>Cs(NV8K4hT@nw0v)>J!1}~dmAfC&V&Zcm*7+tb&a0Z2n8`=t z%UU0!STkH%} z$Gl|&T*vRGX=^F|=5m3yDO-g-DW8gQsZGYyk=GWZYos0>I=7MG=mlij%mv9*cE`-i zOfyQu?`5;Xqoa6A?@IAVZTZ+GKMps-AN9#tA#vufqKlEtZ$svUYH7;UrL&7ymjs2h z|KJgsm=GK=mx9x=_IzQv$QXlsJgVYsJOU@iW2Aue47K{Mnr(% zls~)ux`ll{bGrQkeB|0MiR_WX)dU3Fd+OF-Ge_2T_8?>Be~_-;ZvT)7Zx!wtQpoYp#(5_i;Y-fOez&Vj(Be{*bW0QNL}yF}Evr-^v_z zz`DK8xp-uCA?9=`PCl{K9OF*$Cm#5y5;OM?SL#}a#eLWpBhNG~@!M4?Z$4jfC!=gm zwl??6gY&C;;dY!;dQ0gQq^Oe0;%f}`irfoFJIxYe)A6OkkC#f3**Mwr55;81L&Q#h z4uWd~D;nFML_bM6Oc{`GjE-N8*A4VR6tbVinQavNGX(AZ9ne1yAqUQbT+waTR?Mf- z(1^OPqjl>UaH%1+UOZPb@dmn)9aTIjh$&r~avj7?&MSZ7ScL*zE({Z&cFZKv6Rs=B*a|GANc994A_xCl+Q`(OY-EcW-Fv$LZe zgIZN8U4pg4tAIGcvk0PLjwhoB7aq8huIOyN z`E5b`yf>PB|DN`}Lu}QTO#It#`Hguqc>QFXWJDlzEvMW0boIu_)MOBy(+b7MyFJ?xJ&+m}|daP2c&rshQpR z)GHe(QM5MdovXb$_%7Y(vrNMUtr4Yjn!qiQA=ixG3GH;1o_+P|hR5akMmE-M*Ms|i z1zcxF_VRVeWruX?W?FoDYr)}h6sI*;r_srH#qEkqTOKig7dN0^n|V^>(b-Xe>rT4A zPq`G!qtB#EBi#=wtL+upix1#Ta)5CyiF1vB6@sz*`dEY%4RsHD^&B9-h4mg`dY8x7 z_qZ?9dG$;j%KN(2{QcDTEikCJ_Yp)=duVdShqLMXqUZcR+3_cbp=_-2mp(`Io)J~S zFAl*AZH*t-rHT3z-tb6K2+XM0&3jcV?|oi06Z^?-6K&(f?2Z{PdVr08yrcFtJ=|C( z=PdRx-g375e6xI@43*Vhqn4SE;3Yl~Psq70Wa5WZ^LtC`1H@ip$VdGCBQf)3_^>k4 zr8Me`cr1T*IO|7V`=tNF%G35Z>{6%pImj2~0Q;yab~CH1QLk2})BHu3Nua~R0DD-H z>A@MT%`-#?+5~~3RlX7mc6-3{YnmIpgXfG=rKza{J>QoaRBXcUsfJY*4uWc4>uX>f z;YN5AT$9%>?^qn-sI$j#<{O|-pa1DOuQJgXN#A`IctZ)`h%a1qXvX{lQzj*xYo&<$ zIb$i9ixGfSF3|K1a&;?++Es`CP>1Sx_`Wq^a^Se*?(=izf-dxS^D=3}sYHF&%Wb0k za~X?P_o-`s4p?eSoIb(zv`qwQMo`-^0!B>BB+T+wm3*IbheA#Hfnr))SZBHSAZ z4eS_C>y$B@v{{G>!U8*7kWc{peLy0kp=;NT3SR=uIp1x3KEH90sVP5~g!6&rn@eo8 z)nZ&OldlPLX+U5!^1U@L)6d%grvfNvT7d~YvxXx0yJV+JW z>V$;VyO-ZZvijEI@THu7SJuJ(+inZ3f0%=5tYhab7?M?1VO-R7eYBwUm2FEiVl{W` zZsI228CZIWoMRr6?Gcg7e9e7Bm3{3${S-VrdSRM!kyYZW<<7V>3@JJj6#^W}Q#Oyi zN%4)!(CAN#GA-bbNg-<&troPLENSK6__zm49n`e(>h+4tVQV~{ntLxMDPP2`Nz9UJ zH_j{E7~py=u6`1GlT;;)+-1FmlHe*=2^YZYYFIU}s3x(QEt;e_dp5GsE}GS;Yjfwh z7WJAw0GcYg)F&#+_2+-yZTA@Mp9OM>drJzdj~zNDCUWcYDbb~6$2~;H&5@&3F5uyu zlpzWm>RN&8xG0O4^Ei0%)0XknL?Gpx5$Fvbj zrjP@9?#yj#Xi7eUK;y80gEP;1%|p0ir#CX9vKy}2+TlYwuq!QV4cjgh&3SdJ;^KdA zrd5@meTVihq&d?MrBRe1Lvi)Yf8#DlpkWs*b>Dg(qi}a)aFM=VoUPy8)Vd+T${eM{ zn89PbY{>3iDWyJGZ~XnG9eM0MKSccm4XG;XWQ%qRs+l(S3R&(59I)|IoeUosjNqhM zul>F@wJs_|#T-%vEua08J4^~3u%sFcdd&PM?upyceQ%p7e}XY*D5+1vJLo>+gy`M# zOXV{DQ0gX?5jtyb$ECyt!sTCR6s&`L{8?GvqU`*yxEA@yX5<-_Th;O~_UK4KL-(=U zgY*m8?FK(arYzh(_X*T2IqCB>qWd2pI>l;Cdf9nyNZ6I0^fkMVV=UN4-YDjfAN*9y zuGA&CPxFNRUGl;+pIsOao{pxAW5)x0aySe1>=7zh9G#0S{5Z@B+>?cFp0qknz^GCS z6Bl=f@_agDx+q83L8Vgy6^e|c04=289z#@%)S~3u$sGQ@#O=fR_;%re z{piCv?e+oLQf;nbp!Ya-t1~tpDHqL@F!dX6y%tVVF(E6JmelcdSdJpCHb}2;}aa zkk@zgTc?BFnc!0xqF%uxtrDf|_@ll}db$DzXKtS0nY$x)?oyw_<^k($+OZp!^JV3t zqH5tCLsBDTLEhi8`b=bhnJ60o|M94@fr80rc=m=vRMl{963-HZnm{mC(<||dNX8Lw^k|t^_-o{YXWA-TsoICH6tPD%?-ZfK2mpkDK zHKi;bEQ?_1qCcToxpUrTS(0QyRXrj`DSAkSu&^t51+cny?fdvNZgWPtp5Y=K{br>y z$ueJ`_-D~ANmmIx-c6(N{tjp;N!Vgxu`cM@hv^ve=8GF?zR zK=wg!M(GxY7zq#JgTlCd*rj^aIc%A`z4T~MeoS~-L$7tAqO@8?D`jRg6LZnH{+iH5 zsqdFfY~M#4AN`&5w;;*w=>1y3etqDPDNNQQ&;*UP9xbpL-8+bRstIN`Gjz0UZ(J#` zb5V!yFAQ$C^iF*Ib-~qE{BI>0DIP2a8KgkXn8~2JW=rs(roFg(d+xQ5{G~gRYcLP2 zvpxnoOKx#=3VU~tZyiKjK8;euXsnS*G_BjL2ozE;;ozoD*-Id}SCnyDq>g6J?ac@q zYtQz3*CPn8_C^exl^@oW>{DwX=u~i8@NFfLedDg<$f-MYd#yOQ$?3lZ7x=P}MZ_iG zlJ7>8Xab@bK@qRtYOg5(K;I+!z-N9NsOl+j{(mxiPTW1=EDeEB&S*32c{p8cAq2 zL-QEor6gyn{fpi$?UZdOh8;}^EcDPo46s&;TWsLb**!d-^UK>_-1y-}Jcu(7B{I8x za%>O##Iwe=R|0O=hR*i_5)Ix4L6vT%0M7~P=zec>+bfO`jH5M3@8f!a{m`j4dquPR zH_iLI2iDDHSElfWyDqG48tP>a=%I z?|0#@f`xRF@)L76(_pQ%Z>Qxv6_p$PDKAYWr_i7m@tEFPv_LU_!9@=I=3%z%KRi(a zvdOJ~bDuJ>*^y(lGt6XAHu=?Xk)O;_{6Y>hK9su*UW{^45yDx#At2tg!huQ5gq!;z z=bqLpDqHH1c5Z~|skW)Z2r0{M99}}a3r3G4=*rc`o1JiVEy*8&!Ih^?7cr;?Jipx4 z{0FUX?VG?B)}wPC&QD1c#++01q;9HUv?#Tm-7)jMX=Wt!dmbh zpWusIE@O`jmu8<(HkOy4|CEQLZIkXWYm;jei4t+)W!kBf@ML|H#M>~a`_~=ee(Nt7 z5Lhu5(x`IZgL}P!kOziuX$zKO#1s-a1Cbh;&9=*)O|~Ff4w8+~ZmwOZ^Dz1y@ATWP zV$dx^85>bx^Tde_2v(gX@_Mn3cl{)0J=G5XYOBxqw>_xj1%gLdZBTu_JvfW+f%)lQ zT6o_EhwP?1r+_(RoXlrqNHAfIAkVipcMEJPD13cfBt*f=UozVzQ9$;r(#tyc5g&fB zR6ilW?pNAe=MIEn_5bBVvx}U`Bzego8U0XWPM`I+oCWeI9UB}|Nrep<_p#0X>{z5% zD8~JGTyqiSu5rgWKXX!=-}6uS-5Z-b|AZK}v-F%&S(6 zEPe;|5fF5G|7eKpC2P5Hu@ zxXbm|NgqQx`l7Vy%KtK|P9APXPkOJ%QcpOaCG4i4Xeuyhb$w?AR-fN-UTc)L+T(FQ9VOHyPqPrC? z)grB4n=O;n**2AA=1=Yq=_l0n9+A}L**0X4Vs)YqRQZM)FQPynYW>(j->PDH{cQA7 z;z+-c0;7&W{q09lboEzA?YUd#mE41DMVt~D8t3GsmyBw{%2Er%A${%Hx`|B`HB}X_ zb4WWqF+IsX-IZd>y^L-)bxC!Neb{|%Sk{5uGyj{FKk1Y63yBbEX9|}MiAnBb500$5 zx7VE7F)#S1oo?g71etXDHPL#-%0NfmLs!}NCqH}lU+8C*GAJsH^lDL>Wtj!_RD`?< zaHfiI*blCmi>&wQD4JTq$*Z2GuQTg{;sK5M-B^^eh|UR8=khTgXo>kx50V8|r;inV z!)B0AhurOYjrd+-SGDpEThfjoK7#SYCsMWY= z>P7YkL5+9PBB1LBe=C7)A={TPH?y=;=u%4D>q4$|kgI_0(cn)AM?EKQC1+_ zKtX`)Z&cci!uc8Au;pf$*HS*@=7AL4=I*WYUQyXMoirTQcf1}d?K&q&=6^RNvgi~4 z9t^(us$1rfxe|!T=JH|w3pv*Jp|}^Re$@y;eC*>{b4_#10U`K_`~zK|CXzznaLMSQ zM88*atx|VQ(@>+G8n~djt&3|BZ!4f%4m(OHQjz<96m0ixKXfpY-=2VC!R5^CnxF*( zwKtBn{gb*N-NpN|qeQR=g8@KpQXDmac0nBla4)}2?r)G1c2LXIoX%&_!h&k6Zlxe7%cZ#Cp>b_Z#CMUt7GEg2T2-l1VO(=3oEh!?bzm z&>D)f3*B74eq%kzJ2tBGupu3k;ayq}f_rR?wA!Uivbkqe^h;{{pyZTmMSYNUz2Mam zlPq15NX;Kirpnns63I#}cUF-qq?ssZ6s^~quu%x3Ygls-sb{0Yz-X6y!kiPgQxj;a?=n<*Vp3XayHTD@# z4+Kx|fC>H$%O_?rHA%z&Yz09}1$an>(m!E8bJm-s_=QF?#~{aET=lUZEd(p8bHhpj zbu({YXPZHzKrr?rBoC4T4@#lLdWUL;K;Ark!9`|;78CR+3c{Aad~tXIOpgeA&ZUi+ zmR2VTFF0z@#$LX1+tqA2=K&wrCwY7rOs`~@J&hC>7;KjywBz(^PV7X=KY0fLj!^;d zNU((50g-@?a%j-(qJH@$o6S?V#vV$Rt~eGx3rs4iQ#%^CdhWq<*{n)R76NFhMkzy2 zgK@sU(m#7#K)|0Wm<;q)zB8p{0s5w&D_Wo)z@`@%cpZh~--IGAE`9K=mSUS+>^$Xu zeqW8$3>z9&6tWFNnqJ{Fn?-b}uvg_^%?#7R$a4K>2Gf1aBgbo%X^QLwIP$>pKBkCB zLO%UxlLbl3sjL+HZNntR;+Q;`GOG0Z>jg zmlY&Wc7YiVVHw`nZ>%*#%7Fo)p?~SI=nfO28*T;G_pQZ!sD4_62;v~;%j#8D z*q=JSpA|d$&6QQqBQe9VjC3 zh9o2m;i>M00DtxAVHEMw4=N1Ew(RWiY8FZsEiB`*$`=+<)dQB(=hiOOK44XwAuHy6 zamDmm^V<^NVe~SilUnwr*1p}T=C(|B@1tT~SQ3}{otzI=k~-!pS9H;5pCu~&`THa+ zXa0_`E<-ZbP}YXe~ecQe!#dJ*3NoDRAb<jpsxKx1@jJVeo=*MjpnVj( zEE$NdEEJSe@?tM9E^x};X)+Cdi)Cl_Gr!OJ`%D@q_N}2!8|BRZV}VzIPC8Y)kO!em z{P`^`La-O-bi^C`km6*B?ZZ!WFi%7gX|RYiV}ZrEO-+!B^(3vWxzlZorFZ+20AI16 zsk3?L%H~0FvcJGb8APAmE^m4~a-zvw>U_+;8Ur`Vij3nQ8f~P81WH49EkQaLNWm1t zM7o0H)%p{oIs0dG`uoluD3^0?Iwf0T$HO77n?1>O`-8||n5atn!MnX@D_5(>O2uAz%5r!#A7&QQqQWT37#AdY44R=aACIL%i*Vn zD1kB+ac@8e(U6LP3w*FU27y+5TGSbT6Xg9MdctdOHFnfeh0^6c%2ARj7G}QA9~p!D zIC~01GSW-?fL3JqX^ZaW0#x-9tbHN>hA|#DYRNY)Wv`;MB7<9ZtgUO&xL38?#n?eZ zq9(T;=Yh;D+iyktMfRK~xWASX%nuWkI)~qU38o5S$uN14?kQm(Dnq;Q^F8fg*cg>TA4oJQ%ZRlia zmQib%rxv0jS0I2m9;|A*qlIusT~9EdAgoJq@~=lMuzq?k24_6H&Z7^>VHNKb(zxxh0=$Op<-76-3k7Eq5H35 zhiuHU{rGE*qK5bYJtPvH6!(UZpeL90y+hvpwUK~&!I+-uL&=tfRXk!4fy7<>mg0tM z5gF2*zxlCKh1W~S3>`rYk&WRC+a;pEAN9SXOy{ff`2gWH#@>(9XYxcmc_BIEiJg!E zP6c}dE~s#gXT3(@VPW28<@VkUawKroZ!OpS$FM`CI1r;~oRo$Ph;w5?P;}beNgZMjCx#g4!?? z!&LY_^-$vBc0N2cSQCj6NAI6f>7F|H2m*!)h5|37#U=ZoIu=U-3d-WF%34!MX#A=^ z%z5PI$)x4R;g^Y+YDSs6oPji3g+>0T4J#P_qWe_nY`>vwl9pHQlJRVc zPR1Iy(h^veY%P|fu4G=7Z5WjeSRsYh=RsxWXQwHi@)BLmi+_`^mUI( zU$+l*K4j(~_z?KfLxfLCT@_ytJ?ZMMYwP*yK_XV#d1PFJtFw6I1t>;5UZK!F%l^{B zoxcsbS~yjiQVGh|!N?pHqirr2u0JA1#vzF>YU>%X3OYaK9$z?qB)*g}h(%|(fe9YD z^$pD7c%k>HaPB?O#14wkq{Zp9zD+XCE6<@^w`@k1H=u5Dtc00Q~_-C_jie3UGaF zF7FBlP>@V|{o%B^XZAV+>uOr0)LlGr`=^`Ix6(8T`ycn%zK@%6cAl<1P3K*ujBRi8 z!N)~r8u-{Ah=u5rVTP>-G0~EN*`uRe8YKQ5eSA+7LpC-NM zR!QT<-p-KjZ(F@#BAk=EU80_U`f)b$R91 zh&lcuyf`*4ETc&Jpjx7JH<2{6}dyAD#bMhmt zPI(>Lz@=zngFxv1B>?~l6D4YRAPv{OE>!)`J2ZV~?_1<}%&vLDdbr%N0S-39S+h`~ zf(cRcP^+)rJ!-yW2ejKSi^F63JjdeYhH`?Z+b?c=;Xd+)FWpscIf$x9#ZzwLPxnvy z_CkH|4d36FMx5ObxicOgwbyScPr0L*n;yk+upRv37iF~9@2s15ywam9M@lgmuIfe! zs3Pk`TjHIXez0JR4AVjXc@(8l4M`^$FojP1_1G2fs5i0YmUVaf$sgd8zbAXYaBIJ4 zaPR>700;nj0HD7!AOJi7@L$BVUm!F9U;t2eK$t$@-h6HVfLYCogCVy$$YXoA5Y3@xh)+T_)!ZjoX`QTufJRt&hP{XVFZGdlq$*Rk~GED^ZXW-&Wi7HPzgu`!Dy4PQ3K<( zywFs-+cCOHb!UPhD7lO9((Y{*j!=gcgpO^J>OS7vRtGo$`9d2+9Y7 zHHKGd*OE#6pc}7nLfksM}n%-ekpXs9W2`}q5{ zEbEwW#6gl%E-O^p!L*8bGwJHe8J9zh-kzGZL391=oYs!L)pafLQvMO*Fcl5~V z8P%27S-LGoH!k&H^)dA|?d#{)$hY+~F5J~{>%X@JKrQY*M_fE_)pG$f?6K5069Y9Na~@+#nS z0P-$QE0Apf_%5b9FmC|9JasY(ps+%?<6pynNabOge{IbXu)<9LaVpT3DPEL9U^*=3?(8-QjidsBtc1Z6$#8Uo~1tuf;mQO z%is~(#lMW=AL2{?V^&xv=Sc<}$2v;M)TJqLRb(@dV3DdQd73}Am}nGQN9HMxb=G-# zr1r$_3ghMHEB;|n#2O4|ki^)E_8lfS%5?A_E;uWb<)9I%n4@(D(h+KzHG0J964jf9 ze~iP-T$|K1rE`k)822_FY67YVR2jiCk*SB%(5vKgHRNiFxrA~>_sa2^lDJ@Y0At6_ zrkZABE1uY5v}J3_tQ z3k2`W+69lAQDn;SpoXUE9k0czguLi|uSK+m(&}BVHRGn08((njr+{}S&5c6eFLo!{ z_IKL_eg*0Fx7!7O1^xE-L#Pu`Owj$;kDMWlry#A2&?Jn^AXJIyCWvGTnH3_{ucL5D zzVl-xtWy9vmu)W7NW_Vx6Y-4-0#ENeBoDx!wAO5+I`eAtbCnZg&l>bQ+t6kI<$TtO zH?c-Iag&77e3CQ?)tG~03O7lQ1!rbdYJrP|UV9o|QR$h?d$z9$g*qx)L#Q=3*C=g6 z=_S`pFZ3C3NmUi0<4JEoR%~S^pFEpipu1D z)$y|YMV-#VwdIa8CC9F{^FrIy*3q@dOHJDF#2)HHIJmBqU9sD`*M-@AG2c=TE(*jt zm{QO{-$;CL%s{NcjlFRz4>uMsOphpLfuaHiOWd+3dSTeyiTX&+!QS1byO%d>0?{8N zB@oaCH}>eW!#ZxUy0e%`^UCxa&#X-|k4!r_%w;oQ z(xIgY1P0$%akLD@E+c##$YY1f*wNGWH8&%@9QbmFDqb5!Be5>|&Z2kgepR|Vppm|@ zzP>&)Yp$Y&HsXxkLrOr#8z?XWw_+Mn;B2Je&&{XWp0c4X@L@d@eSk0^w-NMzrobJr zDh0UGS^^=oLT;wP#%fzf`go1iEbo780mSluHlfSw#md;xacA>VDUr_4jYU??O$GNU z^)Z1@Bv454(0gvCz|5HcHhoaZkCGFY1 zBL15WE8sgG9YuNgTVz&AlXQ&$II(fOm!2Y@tRSy=SLju8KjS`UK^)l`*NLo`tT8U% zU|D=1d9z;~n!*8&P5k8HnBb=2O*>FS5o#7C*@QZHb1Xy4BTr5M!liKVCvG=)arM=M z8U?^LX6X+BpA@<{yENYyo1IdlpJ-HpU4>n7RAkW)D(PuIug-iAL%F0`e)}P@ zF0wZj%WDcn6LE{eS8WHGoHR{ha49V_Bot#VlvD1LA{&u_l0-J!Q1QQN4_X1QXS#rr zg2+X9qy3Z)`|n|rtIoca2a%&xz(1V-JiIFc;tJdGwsYL94|b4K3eI^fjJ9XD*}nI+ z=EDv#tBFKY`)FH(xHhSlmhj3iZcjN~xq`?5`GE5<0N!e8{_K7V#(e z=I56iKKyZna&ofkn~JG-0Jc)UrJq*`6mV;IXx#^DHUv7@-V++5sMAstmb*iJda>x6 z(C@R>%bg@3ZO#uREUef2(gtUO6vur(Ou8S4uezfBpby(j=$gTa$6MA$e!!#QE9*|I z#&MsDa|pJ1U$n^}uj>$5h_I%mcmQaId6-j$6N69KAM!-Bh#v?OD&g*FT}Iqg+Az;r;Y+l zV48VoQ)MbOdayno99glE@g2}(W^E2NfqvknaGOAIXTFKq+NH z!Z7V_J?breAgSDl(|F|iVp$zj9@(5~C0b3rYN#PUsy33YgKLS5K^8B{MhH=`Wb%j> z7Gf|--&xy(c;HwXfr)Y*l00V|0KTIcl9chy_il%DC0WlCzm@n9 zcWe)LLL!maQh};T2yI3B@`dG&c&yxQ@vS)l?o5i}2ZF_lLpR1bFVTWou5F(4Z!AW= z?2>bnsezZ4QD~%dW%9E0E-T9CaW=Wkn7b^i-m%Kfx5(*3pV-DtBSS7X%wX)-0X!LF zw9O}}cZ$ASB&ZjmTIIH|&{h|oQs>9D^FE6k*loa-@^tWo3F5ewm&uGbg3nK%GaKn0 zbZ`bd-}1{t;fm8#QUPZRhIZQ@OaD82^48c*!Qi(G@x!&GkiMG?E~rHx7LXbRC(8K1 z;GS^%5w>%3AgucVn9PN)`Tu$>_f9Y5PYBcAPmbSswj@6yO7A2%KtcxS@PB&F0Lmb{ zw|Bg^Z*d5vueWy>_AllEMl=QoW_+(8Sji7uw4C3-tAW5YFAO*aiZ2tx%xg`5e7|=< zf=obw0jGGZMEDs-yrRB7AVA3){4dh5JD~9la4kLq0@&@;QH9Np_5F3+`v3KYHq5qYD-Y#wFh@AZ(B%ghdn7P!NxVO&ElwQJDr& z@A@T;j+)N3KB|P4IWA&@qbUx?2j{827+bW-S0;k)G4=^rfZ|a(60qMC07&LgXyy>R z7?7Rn5UA>qy&Mom>`~cnA?R*teHFCU3a?0>4L*{-f|499n>8BJeiK-})+cRM*Fe!o-Dq1WG4@-tk0yb(LOUO^sTAb~&`N$WG>&uuf99z;YaIO1;F6$h0 zxGN0{4J%HoPMc0+PD@(7Y{XfUspMLb))p(W@7Le;+G*kG^$LKRqFTa^2_lE+Ln5FG zH1d8L+|7!i=QHXnBx9$HuKC;OvU1^Z%=YoHZSfn;YE<0kIoKI9_DzW63 z!1EoK;v6^Q9Pi^CDSsq~s>e%yQB2MKZ)pI+rQesDqqFffFfoyRk-OgyI=HA|oCX^0 z-7rAT5NyMCaUnWFZTgQ58VHbzK;=N;LEQxGjqFA2Wos$Yfy!LbazE|MRbofLih7k4`WE3lp!O7+LU5KeMq#~fmqCeo6J6Q*)nzcOo2v?1pc0S z<_^m4mLcyJcBdiBxqj3PpM*53-aM+MeR*_Ulk37-r!r0TLa}OY0INEpUA5($bE{;+ zxq93s*JggsQ~1QIk#;`lyaup*zJXIriCgr`x*=8pyGdC~h7^u0l-N+B2<^#2$VqcP zvhUFh0N7&O`Is?kjoLW&+87YLAqSWv99hHA#XURBJ-O5)y3{=s-6M|8Bg+j!oHRsP zw=^6|l7fkRMMqi7$;w)$D#L}P<$CY|M1flxNKP^B#G+S<`OxJ24k*SWg|t&tYrB-? zW{Dow^nqAF**n4k1;tS*d6fK>X7(6h7jq&s3}leG+9{0 zAw$TQbYXlM3Vo2_vCnB0o|rl| zTvIBJz6|@Orc-#+F1^(d!*W1UB{rE;`_r-X#RTSZm^t2GGQEY684MY)iz-&Fs=o)v z60|CzXI++58biO5u04{$j=XV% z`L28Dc9<8(TXrv+AV?yaGNzWl2~SbqbvsX0)AiD4rsw@MEc}9Tyxf2FuB~x0$A6|Ji!A(QdhsqoN$Q!l7WfjMHoz>v1~X^8`!V z+_`Kl#dJk;)7+(EDhCdp^K0=a&9+B~c~GdpY_DVFPv62V`=DT=x%l&^pMbrz{(mm# ztR5UeAlffVJU>VhBtq}7HBde%fahmUb8LG_YG}aU;Dp@x+Vr55n4F}B!ltUO;*5~C zvbv6zu(;Biw7jgSilXGsz{>3U$j0b`#B$C25A+{!Y)2^cUp+28O`?PRbgXUxwH+Rp=!&`}1O+oK2-)1yFUimoxl z)uYrVxKWyG)ROLsu%Mwath0K)DXvj4On#XXH?;J_83dE3v=HKq1XoD4=9Hb$Q;KZ1 zdd3+E(Wg`i0y9pQ$VAb(B=x2wC{ygrdMe4e`q+e1?}1c@f7p6X#CVETr`!X4CnO#? z5mx{pw5L#-p_whDsms9uAr5hiy=4^Lg{KGWab_9L?oC{5rtOpmn1g}Ft#wSt_JjK< zWE(83ApUq*_&cPsc%h0sV)&iQv|H&xfNvj&deJjt*`~N@#N4^ZJ+*7%#rCUV+`?0oFxes z#VA7IOHey}rEGLe)G29uQu_9Dq{ti3MQpM5XKgIwJ6DqWgPhAPM^M#~I&xNFMufp? z6<5fE{{-*~w2^7v+~*f&WDg1^+1Q=SGourJOtFSw&g#q;kPED@!yV8%m_?BIx3xf` z&L*0h*_KXs5FfZ_uKyR1TkH4cg;Qg91~G{H+5no!cZ2>ZM=%GYempSRTHTmw>Z(Z) zgu?e-Z#_*jQp1!hFS6MX92`e;5^~37^9TZD;%DOu?+32^>>ouqF2QvLS&oD39c}jG zR%GLB=g7*1>3FAQjuQ`|+(78im|DwZ!Zhu=;TVPk>-rI1l5V9E!~PcZo4YZHuXJmXS&w)mN?gKZXn$81IO$5?I zL0YHu3f15lgTDAqh3)|+QEt*MwuGYYODLO!S5(XAbF-T|$$`#|#}2qL=0`jQ6X_3R zAowK&5IKN8Ukh~{tJ43(AXSHykRy~sBvlk}NXnP~sh}4tpw*lksRs>{ub{wZHkmJ# z=!D7Yv_G9LmG1Zp2!+OAu$XQJODL60rL&lA2Z~6gR;f3cZiUKdHD9eZne7A!iN)p& z8cTD;5G$HZ>$Ex_t;cA&UGum<9bu{@j~C5UplVwGqW=MxsQ<$R?`1?v^3^Z9(0SPkzN7z`Gp_255- z15)WsMw{VEjt4Yq&3fyha+Zt#zNO7bHO~he4yWVgU>Va1t#-TP)o>Np3m&)U{pC;v z+YPVx`~B5OP58g`*5IP##^}myzrfu;I==_?{L?Sn<||FHO|fPhzK!Oo9e2@ZN~|L+ zw`mDEg$s-2+EkZHGhpnsLDS~iC8pe`?31ot5ju}GD&42dm99M*JC6;n?Wf!qpIssR zw^cIUr;HgHh9%|&%)K~F)B7|((+r!~w&M)DfDkkd>xkl14cm|uRSlb%rezJgpcvLQ z>!_;cx=2)OBd)H=;*_mMdKuCQYct+o-4K@Jx@HsC^}KciKn00#7#~D!Kq1CH%nQeU zSPK{w3WLpHIoS%C6w5vi(+~`S{6~_FCz@fJ8*O1P{XmxeEO}v?eF6_HK?JPr@HLQI z(dUdR_C5ur#QO?+=RKBLRAbkR?{!Yjmox_|^&tm;a8=?@$EpB_N%H)d!#cY-q>Jz0 zP|NkQcR2)Y1Yr~aeiZHP{p;B<@7XXQ^xemf?2f%@7?!JY!5lCdO^{&WLE<9gLzLvk zv)N*?JU}7Q=nQ(3;cQST)k=^340N9RaqJuK+cET=&)bQ-BUmG^1+DGpShubdANl7;aGW9Y+k#XhM{sM}`67t6(K$ARdRLi;RJ zl{V~Rips5R)N==_zUo2WyL;BE61q4i-#Txz#z9FbT?y)}PW3ViwxL>~ z0mjKQuF?u(-UY`YFNuwkz8l)vIRl4b#UzbhNyC zuX12_u~fVy7mo``N5y9k(}9OWW*@i_Ghhqa5$W>YvVIv4Gfk*`Bd&ZWSKsFklsi>J zCyf?&By_Jw4t;lN71}E0(^hv!?UFZ3j~9hX-ZG@Lrh8F#=I@8tSMUg)zRnR&ZM5T+ z?tI>3>#m+OylvH11G)DM`qEhicQD|Bg4A5>3rByJ+cfd42nUAhYcday?&T4W6}Omk z_io_(N(0F`QLv)2;I1D-W0Qx~*xn1SVbJ3TkM7X=$J7!AMcAoldZL@ue+cKcBCbWx zjb0Vu^>SPJ7B|uJF7Bmte5+30MQ5J0zO=`lxqNsqG~lDGdqUgtEvrTmP>U829?}&t=p^X zFgqi%udmGVI=RN{^ka_`7E<0sz9Z8bxvz<6UlP>po)Y{mJPLN<tNU_Zh? zq?&Gsil57+9up#eYjyDNgr{cOeJkQX=rXJQmQ83Xgtm z7Bmmc^!eT_A6}~;H|+b!LaiUje#XbhgT+ty9N&J@_ujK+(H1CEDFsRI>#gz><~4dm zg|c7EvB-K_c!Z8ZdN?#>pB5>DM2C-2|6jRu?Qk3vLhz7LgFp9;2xaL1OFF8DbEEx| z;tI~SCEiu^yw1v2p}--9wDX=qMqOY(j9eC^l5Q1A%ZesX{xFQ| zA%Y$hESfd9d(R#v>25wqJk0-0{|u0}$!vYOyXhQWJXXHd{RQlT*kI;IPR<`Vf49XX@pRgZ9ja2h$IK#oz?;;sHmt?@I~6p^`Yov zcwPtma5^yBKVf#i<57d^}DW{}Sy?13A znS6<4f|>W@1v$}!5Dl*71A76{>bnW}rbINgQYz~l?4H_xv(v*|{mfpKUh~0j zm4?yiP+_cWbjrI~lyFY;k07(k$XP$=ymaYQSo^8h?i*k-%ta!fo{G$?l0XvG_i&%W?PSYWux(ykS_}%|KMp@W z<)&~0#-;knw0<3r3(?4 z*Yk~A<-_*ij5(y=8~wFrlVDn7#5uEM7rMVtLaA5r15}AHk^OrfBAKiM6fgh)-lOCD z&H7^W@_XikL;v2u=;OD87$vSjj6^0~oNGP?#zHsCwg`}XbtGWr6y<`bC6wNJSQZHB z=4Hd`3AY}};pb=k*8^dg-aDA80aWB68r=a=f`9=k_yPFoE)Z%ot#3cMHK z)(#DTfk>>EZ?JNg4@n$~F(@#f`yaGsP_90EIuu$^%q~e%(%D3`sVU<`M%ARjG3-N> z$|{aEN%NnLfUB8Uqmz28)vZg3XRx$Hs)4D4W&4g+a^CV(@-rTY5i^t2oI4>gJ_0q4&m$)+_V~s+!Qg% zQj~vGk}}1yi+vn{+S<7_eanl~?kS5?GRF;$0v+W%3O^NDnqt=#u4-ac%qpmsw9cWQ zvPdmrQ~9MzkLHdoE1GiFJ+7Eg@?nvCA8Vnk!9RKx?7_6bT6!ODX}w|n2*FAC&*ZHZ zkzvJ@<~$qGb41zZoE}l5R)_B#yf)F}hMDdhJ5lk6(eHpi@qYeGyYBvp6q^qL9MHL{CrS=~6qy`BE()|<22ZF%{4Gy3BA zw)~0t;Q}IRBBCPf2_zOc&X?u_L`?9Xeh`D$TESJKY=mkE z_`yj+1g%J&A(ef|yM$y_q@vJyn6u1BVbw!^JZinfn=!lJ+;V=js_ehDCChWin1ykx zuEw@?imS|LA@rwXPp+;sUg^97zBxW@iD=hh*@J?+-d6)tHmgjTDY#>Pr>vAM$0|Zq zl8UOO5lzdS#$2tuD;QV2td;{;ijL5(SzRkWheWRWh2FDEYA3w5-leT(Te+9~wCRbX zyWA@VyVjPKnZ2}oGte_&I&=I|1U2$p1pPi6yp&OK}iH$00JPf z0%G+6FyM~^n)Kn>VXK2ic2Qp;z8T9hq@`s`0F<&VMxu>n>qRs&a7TDg5}j;XgEk?r zA@jm#M$!&Y@gAn$Y(E9RE91q;DU{J`=>^k?ve9gzYla#PdF!%A!@Guf6m`oQm6f0* zg)K>*QeCCci_z-|X5v@I!H*{HmEN$WAs>1b^ZoB@cZ4!0mq}E3MIpZ z6c!<4grR2zoR!8(8Wlq+p_6&W7yR+r(b>^2@jfxfu{6=AQLk~kvA(g(@DPbKiv)_K zjD?LAm?ato8+{w~9)&BFtu-%GBA3q27u>(ydtS$1zh6UMeP~)#6_^^I*D-9mTs6E3 zTNYPNKOU_@t({p)FtB5&hSijqz_lnUk(ZS&qH-3e4b|#dI=XoJc=hw#?m4m-dNYo+ z9eDR9TLDaK{5S_O4#G-;X{yyU$wQ{L1_${LX&zIm{6?1D5|nv6%C$XS$XKow;*n z(UxYN`Fdu4A8hjMW{$3h-dJfep2Y;uf&{9YQ&LusL$z1aHV?J8+dAdZ$lY`?M!2W7 zyu5dHz1-M%tz1nU6ci8wK`A0BN)SNC>uy`Ii*Fhq(iQ^0-Q_J*J54W58$VagZftIZ zw#c~+l+KC)!s7ru_7&}(77DUu$asfDA{CU^=`OHiD*b_>=9SCdK z3Hl*~xQ~U4E3J35m(RDf1R3t|YFYWa1kmNFfD*z6TVHs~w#S#Cwe4}tW}L(0_ipA> zABRQexw{|-`rF|QA3FZo)4v~EpXtJl*W=#U`>=16{rmY{W7wLt^ixRa8^?Dv3SVEj zmdZ()7ju9rMREf+D2d8hLt|}sS2?)i?DRA})6v>hlkH}wr>EoOuq^4-t6}-9+v}w| z?EI=2?N&&BXQLvF#!%!py=HAnA$4>WN;Gw3O@P4eIGFep=lyv%f)*9@Sc6P{3go|T z4+WkU31XHjohehcJK0s!^ZmZQ{D)${JDYjx4~+hivK%w=~%&b8TAF;M2z=)q(3=yLeG2(*J0eI_(4NfT{dzIl1YLgNjOL3s2|i+==U-#6lmGNjjorL zk%2|V#fl6Rdu8Qghd0fR?h^u2%rgZ7 zj5=DoP8Oq}1`RdqnH#5VzFm~rnAiqk3BkvTTEgXGMeG9wAzqmBw zJgy81tn5Pn;jsF^a4>-`igxs&hWZ76i5Ckw2-f`D6TV!zkPlL|T6=ly!bu>&a^Wl) zXt`n`8ECp}0cLTxULhRmS17E^t!dk3?Avt+Swxm#D@$GMZ@IagKST3*q{b}C)KX8+ z$A>R_xCmRN1;*QfJuV^s0JmaAvFLMXJa9$RAc0;k|K~vT7(1dw9(oA!4}Rl{F7I z6YVv3c{PWtPBnXf2~V{~1BvG1B?{X8i41yLMZ_#n{$KZZ=-t8jF6i{hNAbkurZ_coZ z3ELc%166D@o*>ab8c`!uRNA!OOOE=9#U2uTv8IINGi)wSyR9fJ_`l2S9RrEDU-u=l zD{E!RXELNL&^ChjDN~PGjJhvAI91rv9STm&BxYu?U;&WBNEzQqReUtl@bEUp9b1y> zl94HhXsL#h{mP2bWYpwC`@s~@m)!Laqs>G2B4#N!|1yDE}j~>b77}PNzdYxbT zL$j``C>9lenC{YmIdL_kG;>5+yjtLz^;6bxb7J2ZPCYF>_Swnm{W@h zffoE%GIRfdL)ifUb1|dbSuqiK(a&lnmBn1GHcRGj{=$M#yzH0ha`PBuQcz|D2JE{Tx99@?!K>3C( z?COjCP(C3hzhfd77@G-vDAz+7LmA^xJzJ~4qMe|4&C+^Tv|iGC6Q|mQy%c$e8YIvN zcu_1^_f`hSNH9d!icp9mmn0e*^fN0`%c)nPNFkNb)zXYM|6v+Z9b!T+o|u?0Gc!98 zRIrEk@g@~I;%+TE#!=?nuq*haJ;`9|sOUWt#(c)xRt-^kqDWp26?I6lR)ucV>`QH| z0B%{eRW6rnBB_MZKxKq={pa90*hUib5Gn_Gy8|)`t*lg{7gPma{k=yb*TJ5YhS){O zubtoR)>HJ2rN|c}mqL$ez+G=w&A+>*QrudOcs9GM&lg8iZp}(|dJC^C7dQBBpU9F= zWn&gvYm`r8;@OWB;+Qf@nNYU&^A;yWmFKr%1)^u*60yke3C`xdruu=S0Dn zHEWizn&MMs0c;=xKDU6<%uH?D_=wSmDOQa06=>#dHK zruB3@d<+Z>Iqa4^?}sTiIa{{hLgaTjG6CDF71wz)nZGk?3ECp_iTSsI#_6`np zeSFbI79N&)XY%x`TRu;eZ9#nq<8DwD-ax6TOs(Y8%v$+2TcS!T9U^hkk0YL*AkJuG zr$7~j(A-?@IsAJx*DH3NG!8 z(4AC&8}}|-wPQU`nwQbxa5@Gyl-T;Z zdfEPoLM&GiX{bEiGG#nV@o%WF)=c$-^G&B8(xKjl6=cX4UwX?X{ z9onZt#eH+P-izWybK*&Yp>YVSM8l(C8`@f%QO)>_vS)U z>NaUdNR}?W;t`Z&)m&W&&n`T>^*KV4C7KSm8{3__!m6sK?*4y@Wyz8>SS2>|{b)H`!gYk1?#iFvvqUh;x8F-j8o6*bcc4`PaZ(5y~Y+R^4 z4;wh238#OaeJ(6I1v_m_2?{)0KsdFl2-!u$H9H#1NJwTrxq@_k8{5dvA?;it0ys1K|vv>J($ zgxstXc?4laMUTr^nEnEytd24@ntmm{JHa20d+HAy1SIsM?)w+}8_ea1a^nrrdyOdh z@-bfhK(&?9fbTy)AJsrR08>JaUsmDeCN9c>YZOG&l#%0bj@;A2Fdb3~s4G}tOfHt3 zEwYR=-i4sTxDe18Rty{;>#Xw>Z+wm?xu!i#==6YIGDMP&K4lO*;vp*>Uh$0CMg;tB zFvSR-k%Rw(K5W>;c1dD0rZ_PwqBy=cdOyS#92bMsR;(-(2g!?t&g6>{QY*pGvfsU* zm}y1!yyh#dNA%0Z6=4d_w3=rwH;QL2$QnK~Hy3Gx3D7S`{6ybE>jAqK!vI;)Ir4M0Chl$znD&n4H0ILVjmM`m11Lrm5HqAtm$cHac=sF#grkL#qq#5GK(--$SUSm z;ufi_V*lo6^NGWSd}8e0XY2VyXfEUu<6?@okV|aIx?HQdM2Q^Aw z8NwLCBx83sG(Xo*cnsF(+6iO9PDp4~8PS}QIhR!XA7nUsT?d=szp0Vp>kaS{H1r%PO)+z+m z$YdZ|Yb|3Fo{}x;!nht;+5IozH{eJ$fZ&#&_YU3?W|!_p70WAYj*A|#BoX@ zucy%j)&)wSfj;$E1|VWpNYnlg=nloy4F0Q zWzW*TgY+LD?TV&x0kBl0%q)vMxpkX?Xk=k>GLcP1BUufeuSY`uQJi>JM5)I`pi?L` zd_JF_nusZ?+V^I%GKJ#BM#a*jsRKX@f+ihX2rdSrMqC-yOy0pV(1H1I)0ig-brn`K zpN_dk$3P~BRLZVSqN1f|p2cuvG0B-4>Vf7s8IP1s#zG+@COqm4T3V1TqTOCl zsn+cEVW8j`0N9@33k4i^_wKz(pGS-WTpk~VegVvT#*vJBLokOifUUzp-E=u1e_b== z2Q!YaUJ1*SLqiVRg)3LC__z|Kjn$qGW{#dOU=5L$<{ zq+aue^(qKWK1*L-o3lQaM)}Y}rKZAco}R`qOb!Vp{!+vjr%+T=i{hM-B&nU6zUiP2 z)CroQ$z|Z{R%I0s=PeY8;9u<89iBN+fA1G9O`+eXk)J`Xa8FLU;V1TeR#1p1ov?BL zxA?DK_5b8Cyd-ETDiVR8W*p~$g4Y3{nawQ3%w_UeaM3$6V~*#s$N6|w;1c@O`G(DDMO_<2mKjKVn^Ef_Z&wWk!TfY#I+_D@Tf$kTQMT)5!c1W zTC1*Xb^BO0?>%|p!i9I=?%u3hUc7i=f8CO9bLZ7}7vPwf)7x0Z5I?D~gT!Wm#y@AV zw74vw=!uH;C*;q0!u%8Ks9S$x_Bl@|)}Kf|=LzNd6XxeUkywAC{2NdF20rnd0MPLh zW?)NeYwNCd>jE!F>m%3e^g50V>CKCe!^^3 z@;onN3>QxJo;!E0_jJ!IM^7Bv+p@tNR~jzf~L);W8$JD78omzy2uvf zh;LsF-I5lFP^~mI6Us_cp3sJ3%9H&fQoD4?1Sz@cS^7&ze_5pME*Jcav)~h~t4jZ8 znu*;f&!0c}GtS0ApaA=#Tlg*jIsRo4NCE+mKiTMR8`YcBZ?fl?@0 z$0MX}Qoe|4H>4GWK9Qo*Ju6U#P=hp$5Ndjs@<>%81zJFSqmNl>B>Z|&=@cn#DXv?w zN=M-TBBc&NH~gPsd6L{7c~iPjwg#z9q{=X@$5c2TuDTWke2^O+9v=6l1S*xgA!9e$ zY;|>YN8oRW|JYwY%3>XguCA^_T}PD4BlS0mT2hmi+SghtqSd9e@ZJv2>(=S70xbb? zeuIJlcLc}^)MjJ91{e482OnNbZWh<{+k(LSfl_G@D5pgt;~OMdjkhIosf1Yxd-i=s zO`PMzgNjG)v9U!M!zdyi6j=8JN}^xG`g~sWp5FZ6;>89yfvon3z@B{>Wgw9o9wRI3 zL}}|T!uCmJI9S5Wg>svbZANC`R$NieWHREW_Aa^IS#Sxm=)9>43OzLVdXBo5#>PgE z9zA;M;?bi<*e}R*s$>p|dwLdYy#xSF+{nnp$e1fIGch_b<`20h@iH2XOm=1V0p{No zigYr(8n3}DO4}2OB<+lEVk%&#(|B4Uk1J6TR6^X&8Sz6kf1}CQa|)F~&#}XuFYfPr zv15;T!Ym#r)5bRZgbI_Y*nVtPC2bLmN~O_KrbG20$A5UKP)*3E@1vUd`mtM(yT`;& z6Yl=?cg@;Xb>YZ^@%v9a?loN)E$G6P;L^8PJ@!O*!{X~X(|z#3(IZ3;CUs3~dJtW5 z_f#4i)1gY5xQ8v=ohaESa;%QLRVKB1s|d{$Q!(^5yli*=yW zQVhj1_=8^k$7pj*4r61CM5tLbpRRs>C}6>0V}1xsMoN5!JV-uKj4_W+VgrUAuQbRp z)WC?i>$njeKwb>TX*gJou{egnP#XKXNQ`=1(zn=<))6`@O_hY2rD-{#ercK@w7fux z-8>@Fx_kFvC5t8~yAlr0O;1nH1;c>noDiPD(~Oxg+!OweYA67f_28_Y*>uSEG-=TO z%0-k?JBkVAw3a$R@AbNx=1^Sg`3u!r{$e$8P~1O?^sjQQekJ z$lbq>3o7KA!aU6M+@kN%@CeR}9Mdt}N@xO`n+(Tc4!719pHJCYIS&a`0Os9?4q|jX zzZ!0C;vntBF8<#TYbE^v3b?I7vnv8VYWv^xvZUvI0enAdd~a9AO3K7i8FVcI^`&mp4qH7sxm9Up{FUM z;*1{c=k)Y4Pm&AM=x07zO=d9%5A8PNaaIC&xt*T+{0qBg$e9Li)B1`a(qo7K$t{Ww z7gf0*&()S!qS5805FUH`UMuq_%C248(p8@0Sqd^awH9*>C`mYInY zx%X(=J32ZwGq$Qk9^q`xxR>l4CWJRBd9)g@zj5j6)weERzIy56s;W34Xp~BiJAOKE)|Wwd9|xS83+U-w1rFH*3-1V`r$96sp?%Pam&4SwEe(oOe?-@gOftvR&nK) zi55*kC8G=Bg=mUHVKC9?JSIgJGxD;U`i9yvE!SUivJoJ;xswuJ2Vn*&W*}^v6f57L z&N9Mm1@;cI_mJ)4^07$Bi&@@>ckhl)qaE?i2k}a3(Vpni;>Va$G%XSTqx<*oa~!w@ zDwDCR^EpVz@mh(e8P0A&=}s;zC&hdj?mu4)thj9I6yMtAi`N{!@SA_}7k}|9mo9zq zhxq%KUps?WcLTohy7l)ZoV*hmZG)i^>PTB~YVLyE+{W_@j%9k>zB1amikO z>eQ*O27P84`%qqPm4~M8{_p?&zyHq=zu8ID3C6&Sx{?lDRe!)>vTM);%J;aBq9!JnBWCZ&Q`2%D_QLxGszN(P0SX9kkZ0 z?zec+|H8>QSjS>OeCABpA5Eo#&>sHT2|xh` z*W}i)_6-taWO6=?5wU9#c~}Nah38$$;uojZ^xXMv{f5Y8=-z_swT8Xnlgmi3RL0^A-b84 z+>9)-gKf|;EHL>WGrisLUFy}->lE}76os1g|dZn!BMBH6^A`UV;Q(0+{6&-|c&q^JHLn5D% zsijy#?Zyc$ zU!%pI1)+^dOLQDXSnV?<3+Lj5RX)p(BRhetK_(X+UKypfh$m_WQ&|}W3$(>tMlCLi z+0{969GFUiTyCdk1|4+A!3K;N9t6-liU-^vMhp$%C7jdcXebz1Jxg=rOP%xTB|J=9 zQr905Cv){cP?gPbD(z|xQ8Z0VHj8IzTQpqOg(fe|RhC9W9L$mUyh}=6IYP^%X$7G& zX=>iE<~l-Wq^WYlb`ykJ)@ZR`KDpojvPlvXH{K9|Une5_)_Oz;BIjmt`8g0pLxU`0tLSg|$(UtwwL zCFq79NO&+L$9e?*V1sN(6pnA;bD?jzfj8iX-5XfN)bniS5|QQU4K!U84sEc5BG4t3 z`JNPoK;GoKRr*HS6#P$-UO@V{OQ{b&5$RQ=|F)FghJPv2-$gq3l)i=ZZKQ3S0x#NZ zmMskrDfrBi=Mi2{FjL`+rv6`N{{h%mk?oJ;bGy1^NtR_x?k#TV)r61)0tqY-Ah48O z>Qc7w-tu~XzETXk|JQqO-}cHbKiI+smR^>GkhsN8;@)l9mMrVaRxkh0NOCuMW$Y_m z&D^PX%9(RM=Zsn{aY;fgad?LTfdtZEMwYdyNN6!^uC1+=1lDC>nYl5r>8Q#wVI@)4 z3o`tltEv+vovpkUZd+YVO{KliXfzp&S|g_7(rwtQRyfFB zSynMD$5Ux=NH$A|ETk=Ya3qyV5rL#+O`e#JB$A8>&BSaA?xXzwGC~UDs0b8TP<&5- z>hS_`fI^Q3=qk;o(u|8`(f|YW_|j%bu`FqCPmf!prsxVmU{HLuMN`xuR_)wbw7*5g zimXOSsI42VQG5zY13mKWM)WX%!W2L3@hPi{WtvckDtO8wcAj&gc-p19I35zfo1&_4 z`}ezxFl|{XvI=HnQ$V9mQRJ|6=#WIJ5DNmV{5-wjg7Jbp1=}F1<#z6zdt-^N(h}96 zL~G|po})G5!fkx41%rTVK0S7G3)D?Et*)`G#?#Hq{lY*PTtq~RP$vww@q?BTng-KM zgcnbby_o(s5<*F`&+7?;YxVglK5!wm$W1yBLns-e`Eu0*%QyZ}9v@cMIcJTzOxH^LT##=ZVMj>`O0w`z7*a znFpNqUbG4{f5lTU;BoTgsg0E37;T+Ww9bFc9>xtUZImLk7NM$Jf^Tubci#=Z3v4C# zS~&a~zQuRBw}Q7|jQ$nhcJjB_%46hD$)7TnFCHV)KusEy9|Up3@u)6uXWgvIsi*Lp|sJrCZJ zBDa)))3G>)PJZ2=Wb#VO%4TQh!VJj=Y`IjY)(EXCE|TO#E=|%e?=dma==0AVDUqfi z8SzNA!a|#B7Dj%e1v~D2U}knv>ufj-!OQUzx1G2R?r?*X97Yx@M}0jtN^_*%sab^a z4uioUE(~6xs(rl!Gf|fg<6cmyBhdu4Wz$O5>rEFFys1`Sxzac~N=G5N%}p-6to`uA zrfEo`#&_%h&E5i?X*YDIUnVPD>3xV%>9Gh zhFSBE2(~l-pY+fYB{0Gd;hsHB9)b6UaTLI_bj_fe^c!tMOa~c`9~`t;Ixl_R(a)37 zOdlVLxVioNN#fOn^&Yf#0e0k$|pQJtdhVmBgV^jWbyd%<413SdM^2SnQ`b}-mt>4NGyk<`|k1^I98U${pVW=!>}v=EX&h> z&N?4qn8>^j<^{%mQL`C}n5ypn7A~3KIa$N;i6pt`&)c8pcU7w*8C}?d>V1Gb?yD{! zLv%5O%4|kceS5*w$&*uPi55PUBpmBP;v|`ZHu6DeBVWKkxd7S8!BeMRS#2pX(^5-l zsiWkt<+Ceu;|}=SV++0+&n$(jV$vU(oeu%@{K+RVazSRD>9m`HN{Qs_$2R4vFZPPP z6Ply5b4yVS?&qIB*<_ssC-RnCI!U?AX&px1#f0W$Y1?j$=tGUQudJnI)mUqDPSsX0 z%D=a`Kt3WDUF=1W398fQ_m4fLP<7o?F7^~TC9hi_sEv{=Zh?cXh(TW0V;LNkNybpb zFN_7B;(r0Cqh)&x1&C9K!KK3sSdPWAy7xlMG2hGNOD>*8#?T4VHY_L7)bLx#o}4;M z^CvVd8{TSu*%}R(YkFGtN!Cv;x+Rg8iu!gRr{za~-lPNG*0!Pq&hz+@U9GW-wn$iw zru?B;+O5J0on5Nk1z4h&mB6X49-mbMCslYJntF{D&U}?yHH!he*U7GEBke_Q)XJ%2 z{CnRU|AHJ}lh1CMBdI$EJ+r^G*L^|GzlL~Uobv&~;6l#)M<0Rx6jFScvwccPrNR$2 zRL<2QDi70O?%67H$5=EvcE=qWYc+(e)mBY!?;Ur<`yfT>ixUT;ojXUi&U>T96MvS% z)-R97n+b!9kWxCkwoOg7jgAUT0zEsyK&KKv?ATY^1yI*+9VH63EL|y`hKpW(wP^qT zC}#zIWaXk%Z*umt*Is)Kn&uir-n(~p_6B9#Fn{e?o~KR{1{WcfIja`_si9$eLE1l& zF=jF0PuuK6gOmP`J{lS#BanzuvkGoA01YM7Dnrif+sNEpROTF$lMZ*KHXaNHY;8uR&~%jcU9*5vcl5>(?#Isg}=`TJ4e8jVJjxk;yU(!HT{agM!k zaWs(7gTB=#0;8W@VAxn-7UcTyI3z%;B zE-KGHvA=-H0En4_{ZBlr1jT~#j46)tf?eCT?II0G2ONtUlxKf_)@a1_rKQ+%Iw%}U zw-q05_hvqvF1w$8m+q&xT(?%@?8{NqPOiV7d-wdsw)V^Kz542_=ndB{fA-0=6lBF815^G@t2V9{?dl6O-E*mZ_f%d&9p z+|pzq;bJuTvUI)eop;_j-`)EP$>@}0UU{&L6xuWMT1Ilo<=_DH13q@X?O)qI`Mmv; zbKigc+-H5TUGUzI{^hU!>R*2Js!YjU#%*8->~zouuc1adNKqluT80(iq7L_P9GgFO z8meVAHQVnz^X!W+K6~cQJ*HG@&r`?9Uy#3G?tDTPs{0uxod!oWjmB1=IzZ;motv|r zA{+J{3^Uk%`Q4Zh1p{$%@bk~{`@-w5zkXqmw4-xjt5GELCaqe-xmDv(Su9b7sn+87 z_?~?Sp7iz2BoYZ-8CVzNJMR7Z*S~)64!R@Gsw?uoV8kDFtBUd3yJp!Ht;ORx+;m0o zUA&#k7eD^sCm4Hg{_OJQUQBUUKK}Rv`i|(!!vrU@ct>ZsR5Xr_8wPQdQl@nl(M@+h z6;o&Mst)hpw{I8TRb5qC+0sWJeKZgkW#9cfui99RA3PuGP#%ufJ za=UwVFLZEa&ZBe7*0b%1tQ#7#TEAe@GZ@Bp>`)SVuy*wc<--qm>=^&(-~R32J{l*S z%&66_EhpSe-uL9Ja8&Em`YTtjbPW_5q{XS|TyNK>oI%^&t>r%akSiG&DB%VMsD7Im z^1+4DvLxkK!sSacn;svhMpBxZ=#|+Sa@UsZPaP+2@-O6nmHbM~HR`i%qgk4{xf#S78yOz*gz7E% zwnB%qw5+1C%Ij|a&#e7ycNRG+7)Hy6d{gt$g5p@Ay?W=N=9~9#HUqS6qY)du-Qg_S z)`S&n_pVvb-1OA7tDv0P+8w$6QI^wCH$j_yN1dJv27Qa6G_=}7=%F9&FL&`68pj`P zHHkleI3+Ya@Wd0(eC5kuLEAoy@Zah4yLjaF&iOSGpWR4J*Y?+c-FAb$;NQuAN4|E9 zbdfIMYyX8kA@I7}w*5_R_msmvT=>&Jy|8Xa@)z=-k!>0BfZ4WjXTqE&l$b;+f3kua zr;@3BTE0yd>OPcP*IKB{4?OWiV3U=)V>C7QT0?ak=I(wvcYkYn?kcJcAXU^DHb>Uw`^S=4!vO4_gzNwMcU5%*gH1e;??zJlU zKcHnlyGA>IPi~fQcKq$%c6hGog2RE;$nk=7DPx7#yl8kJlEQ9GOurXV&UN*lUV?H#4!A{4z4kMio z^x>_SF2H%dVBso&d0q@;jN_GIoNjvRDO-b3HE^R9Yjv*{%kI^h>Anu7--=&za=FIO zS;Kg}HhE5-+Qb_WXkB&#(0iDXnNB+1S>P*{d34XEkQ8eh75-XndY|OjAosiqGR| zYN{z~s6TYLx}>nEr12I^`^R>a>3zs;PF+N|eovp?T}o~Oi$quGFp2`u`PMvxA*J{i zXO~1tQmNroJj=+&n;I>AXaMCJ4D*&o2z;`&yCt_nwORVhg;&~@aY%MFX_rn5rkO9HDQs-?`ADV5wD-h`6AwTA^rQINljl(eFjSdG9$~_` z32PsDM2p=i)g&}YT7!yBFkHfwcd({V1Ct>K51P{pV~|su&1-le<}yN50&>qGXW7Qa zl2(Dw^a8%Z@{q?0e28kJbXO#!S^1H5mA}1_pXg~9JY};jSlXGLL^uM}d*@*RSQFjA z78VR}i2-3e)UBD~7t2Uvi7amSlo;=yF!ADfT7YbvLx^)YYr$YDC98USjmD18FMZxm zxrnj~EoAEJHIhD=!&q0&su~+f5#!QnIYf963U-jWeR3_TM`;a9i+0yCS8rWkeRtCOM9E<%#p_ zo+!=joK$tAKV`?h|NXI7kEWmJ{;<3I5AiL&%Kmh;j{GtBj-z+|YWlzl@_+Gn02uce z8DyS$<~SL|-5>GkU%hJ-0}fRd1d7DSd;_yA2=sEVS`>Sjzy;)O7cTY;dBJp_>xG-c zjc>H){Lct8KY9g5<}Q5t>1X)r8UjDOrI2Td2RN(ggub+-*yo)KaRnGv1tf)eluKhe z=3Z%lCGVS>?Ws}F*qHtxHb0p8VYJnJvQ4Dt@ zg>0khSR`o!98G__b%R~2@vQv2W(!*Z*)VZ6EHAf4>pTD8Q@wEcvY3^Z~6UKuJjCg z1@c~&e>m;t8XM#M%XuDj_0P{&RQ%{i^}BY}R(Oa;7NMJV;2_QJ^Upc{WwPE*kMNT~ zBWZ|wL)P|j8FR$4 z>8vx84|xu=8VJTVrZYj)xn=XpIY<5PhyRwAxCXkl!)zlm;FX*18EIla*KAJtI!)os z=Czm2$_Gmkw#;eF*&{1g5>%5>S;*)ijQbW?I#nzTQk!`Tnw}m_#sqXSNzLW)97liz z&|aJ-g`hqQ$@ImGuc#^+EI&-;@uzMhXUU&s{?3}8I(`$z$4$513FWLiZ?%8(n|6%k zR@o7YCIx+-$z+0%C>f2#b{7f(n1Blig}ZmlOftD?civ8G^x|@jw&&4kziFbTor3#D4^Up`fy|UF*W>IC- z&^4Ov`@pchX?K%GvqpYyS;upv-A4F0Dw7MO+r@T+02UsaJmdKlNhXhr`$&i!Ngk02 z;-a@$~)u@+;T4qvU_Hd)Fq<+MAk=lHb!DNoF&_r@SH) zGm>>YN?O-(HblDJ7#Osghj}K6O6JPdn3Id;qfA3tCxj@@Xb8XQ0!(qC(L~av>X}RE zD=I1=y3EH5sMw2jX>Wzc4{Wht_s~P&bJAHIvJEYla;bLOxp{2n0Tf!{f!;)AE8}3O zY?%{e%vs=MS0Z^JfH?iqorurt#VyAV#%zW z5vX61Nn&}#9xBVOspdSwavRE&C$x7PtV2FHp}Jb|4fz&iW2j<%v5L_Y9traC4$uY8 znwlD?rsLY1Z@zhL@yL-yVwV}MR@QDa1x8^`4=9hY}4kITblS-k;^ndestc>0OS z*38Wg+w%idg(Z--+J|SogJZHu(iKxx7K$WaiV;l1<;%($2k$#GF{8_AWoTz6&YV5~ zrbA&NMT*#$6*S1=;>3zchia=;C3A}1uH?#j^GbQhN=Y*15(She!d+||4=@DD1_c;=aBPHe-rRZJ&i zyoS<(^YgMgRt8zHC#EkebCVU$)_usU7F*Wx=6w$iWx%=qO8Uqxo4V~Ok~NGHO5~{)oo8fWhJX_D-`ad>b4;;j_?b9`?Mjd zl#Ak-_4;Ic5akoZ6DNkjS^W6Qu&h3M^ytk8_s-4jwYWIFK9O)|Y2@4tL*X2fkj1vE zAzjKJY#VGBMqGS;V^7aTxv>4n5w#7Y)uwL02A z`q^lVIyj`Z5MOm{kKE_Ngh4*XLJ)q43Fr7*jd?V(`ebSXUNCfO6`p`$L@OQ@#nsLL+!9TQ**YuHac`y4>*kI`N53)dB-j;gkIt>NfVT&V7oKm5Z_Zn(?( zyIYBiEa1=eU)pZX%K`&JY|Aaz%Fcz-V0n>`K8mc{NqhoMU(qr09r7KfXycB8d4PcY zSV?6{gNpD(l3cw-GHyq8Xi2@y6z3B{r&y^^(kbgf#qaO5)SNI zpOmV!baZqzxmB)UJ#DACH{O_Ahu1$RyVnBtiS-z95trV&4!BQA6b)@HvI^f{;R!ZV zp5W;BzBl?sbnxr4dkaF?srj{E(|i#z{G`k<%oh>FTgf4J-qF) zbwq!-wT$GMn2jr0i*am&R_yv^40!0R7BOp8)fURJ)~#2qjk^CUdna1H^|of|scz$+ za`Z$u($K0BpMIL`eL*BI$ZjyzTi4q>XLi?{(Zq@1{LC;=@}K?S-~0OJ=OfgHKCI$T zbyF$E`20MBDM7k;@%?s%8b*>BhA8dtqaT_scTY!&AtSmlkmz*x<<`1@h91~Og+Qe{ zsEnef;-;Has^}mH&Vi(D=jkV&c;enY)ztwAB&1U(ns+qqEaY91P`I;cNArnOvgy>_ z%{DUiDLuz)irAX(UPeFMl(RosvXImpVXRjbTj03R{74@-iGu_E0|N_O|L0sru9AkN zD^ZBK%Y|l^`S>hWS{Hh?c28q$iV< zU*%EqH|#Hq=;&@)ljhXggyDzpK$_;#LBsIw+mC`~C+P{cb%W;EQr4_-H}u2$rOr-C z=;#p06=4;wB}tNr#tuz=-ro|pg8(YZqyzVJ#Yu}A0 zzMDC@L0^r2R;|ySd!dd}Ntnh~z7t%UUFBe*BMOy-We@^Qu&KXniL90K(~YP0T8Q^^ zbgR$3#Ikq!1S>mXa1o-zCMZSH>2yzz7MY4QH6ggzD>^ZeNJ&K)=-NW zw3Q~EW;w#C*eRei%advUKwl4DhLV5a$>$=AoTZ%Z5pO>6rLX?RZyY(2B!^^UK~t^M zVP+IcbhSYX)1^s+wa%-N(rQy_KnrFdlVcFKEJPLt4 zUZ=v)^XbYgmNEvw38tj^!7uyf)g{fa#rLKA?>_^>11ApDk>f}@ufF~!D)6S z_l8I4Nqy)0hx{&0d@&k|gp?G9MXnB3!r;oRy-ZdHqjG4#iCz(?r4=7+b*GI&*_Jh(Eaz{dFK9y z?mP44haPy~fjjqCk-LzNlwYtNwXQSJ!xDQZCuQBab7qr71xFeKpWb*Dh?d&A;KP2; zY-O1kp6%?o-s@Rf3I+m!P+G{x(SLdIz#!Fq3vwg|L_s)}NW09Opr(hO@mH_T#^4eu zhLQD`rc!2bw<_|)&;UIPM1>Kobvl~vxNTuUEW){?XU^Pm_~>mAY#iB9!QySD3hGWi z_Sj=z+F49)M$)=`v({w}j19Fx&3(>l<)9e65KhDrvi^u8HU#9-Wo&91j~sDtI9;fy z5}KmZ)6t2EA`*}}!-4(#Wp?**38xEP{z)|IaNI;CpjMfSUp{wEX5SuPo&z95$AuTR zUqmz5%gU_y;?t=lMG1Na2Pg3rN~EmlzWS6Ot>8%+aG#f&!~J}U_E;^5Zz3>~1SK!t zrRCLt$xDntK$Xh{mpm~wkiY7f2VFX?D@KzQ>(YL|`#>>|#*r)*6Iyzs*5eNIg5#ry7l?z!jg*+;&C3{#0DsO(gPAw28S zvOHm8sWitVVV=I=&I1k(ATiEy;LbY>l9L@^V{}X=3kq^A_Eo~*!nia$9HUcl(cail zS(%r$4Jf8!0l28BDa9O8BECcYZIZA zwkmsI=F<4JYwjkSlz#N#V~rN?oM$=`3rA4Xl(uje)T?(kT7r1*3&x6l)b{872WrV} zNL*c0w;#Pi+uP-VmOY<{#F2Pxd`dR%sxhP%y0Q9QnNMh|cI|Snw~9+7YD}CkXUPQE z$D4WmyAcX%BeYc*n+@}96~<@7rnd^yWy9vT3e#u9rnU;>ZjhfU8>ZYK-o$@5O(`3e zB>9`eoY}C*`Y>TNP1lV>Hp#HF>G25rqBcq2IK?k$5$#rC+=iOnD8<`y`@w2mU!U&3 zu+rlk)ba5zSnjJsjsuqe!jiA1Vsmn%Wk1WAD$DZ1HR_Cfl%b#Mx4F=)cW&;(@O$D# zLf8M8i-t4Va1MJ#i5D}}z%KzGEgm2lTELa5E1yFrkUaNUHg8q(zT#gD|La@$Yv6C% z!e0x2?H2y|@Q-fcPxBSG@YloNu!X<*3(Bd3e|YP3Xn8hr3AwVskly_YH^P*r+&QX9 zmD^+S|G@xvCBMw46gw%EU)~TJV#dh?Lh}?0DcTs?!p$?pk5Ii)A+}9%eT5yftxMUtWj@Dq)H{<*yPWA{A|AzdJsM9)V9=??<`TL@0A_?1Y$QU(?=nfBC21Kq z#<4}>Xi&z+V4XrsCa>t-j81SB3Oa+S00&kTm<-f3Detr!I72>|qIMJ@2kkwZMavq& z)%ALeHXCTSC1SA$+-vB?GD2L!QY0Mi@24#wlvhZS#J(a5Bx8U`5J?(`QLxhZz5cQ`?)CW=W5fvjqu~`vFz1vU=o3!b{Bqc4ktk8 zsr=#5ATfeW)e}J=2HfaqVcaC`Vk6<0i(y#23fK>}D70-898_;G8KyL5luOqtqzNde zq>ODvE2HM*Z4QT7%TfA9ElFw)xRch6QgF zR6r`Wh(a#_rR-8M1SBxeLG$U0D06mpab$Lc{kUIc36ez%IkiYsgR_0nKy)xYrV8g1 zeVB~s$;yr?Yt1RikddL8C<8qxF1j!>oJ@v7BiFCY!1gvs&-p+Ios}9v)C5uAC1OB- z(6~7;wdPzr!xHR5h)OPX*o|rq=vz*0$SX*Z(o%b|-EK8o(G&C3YEl52oR=gcDrXSW z)S68^E^B9J%{qxXQOF@5?$2?h89{KFRT{#QbV;Fx#C&5D6CvztU3!M-=sV#%yHmw-E9OEo4l^K)ut6lz-l5WN7!Qh|>7B_f$nbCX1t zmfS>gv4T$Jsud0S7~NKr4WG2q45KnwQRjSv3ipyBANN)R9qKA-N1voQj&-S6jt+UA zQt~#7LBxO*4H!A;h~h(2_>@RGy=vq8bOw*Xuw&CH!CdMn(g+~W5kC=kVQdRp`Z`jJ zsK+7%9crGW7SXBrQmYH|0!g_r{LgAf7YTh%lX-0hKFO6jEP8fPSxk!@<0_C0dJ`Qp zTD3q&z1B)gof$uB6*O`&9GRt9E1Hx?k}QjthLl!b+R7~20zBO+=fP42AJw*PC&&(7QkPM{3E$~@Jy@Fo1kwAn6QS9iLkiqzp`HqfQX{lS#D9VWw z`($zeUbo)LClVXbT6Avj!Z5eGxrGHfTEWj=e>MjvG2nF)>)GrB`{ni4GGi2S3h%?vuAJ zqPPl5%avC<9J1sntSGOpzV+7D4fdmZI@^&ZMSjOZ_@=40a0#{uyIgA_n*bzl=h?hl zPu`70k@T#85vkH-`TpUdX=>1NvVXXry!&phE_dYS#7Z`aeZMG*ixbz*f5tK4*@@As z*!XpHTx`2^iDhwtyg)w-vD!RaC8*;9E{(CGWC%x1w}Unj*uRqC}!dGaNBNaFiG9y=KV^tE<%EJj=D-;OO~L_d1Ph zqE5Wq&0YJO*M`X7%fF{y$TKR=BR7?Re*C@cb0s<1lEDHq6$!!OdS4)nO@00(-+LR|?h={R6_VlmhpE4)lyd}F~(dNPhH@AED$cTI6 z88jX3v@Kr|7N7eXHBs@(`f$Nw9vdTL2%npI?5pJDa(F)4x&+}^$`}qUDsbFT`(PJ0 zHE=l~>m`r~Qb7%D9o7_p*3~9VWji20*U0pg75Gb7P}k$83ENMxg=O(q76 zL=Q0nK%VOfs%5DJCGxuH0Nni?!Ejura1Z2ULk>`gxxv`c)e~CeIBs!fh@QkTgJ}HB zymu06>%NJ}$q|<-Fhya${ZoNfM>M2>s{)&R_uYNhsh9;blLgYylaPf1XTWQ&j!woz7w_V|C_R>GGWLg zw0-LNlqB#x7nr_s;d6{`uXn5)qx(Wv_m#FbqM#Vcbf(tRbd;;pF;38FoK)?MO$)rs z3M=7SV{xI?Xt9vh_GuUypPL@MdbKC+IQaOJN-(Z3*>(V<{lwk(!3^Js7NmjJQ4f!L zddRwQ-_H69D;FL@At%xdCJ$RG8VDE|ySJVLAU3qSW%Mx8yC$A$ zdDR%<#@RswVI?KX!id2aJTZhP@)VA(?*AV@(ZcM^Jki3uNmhH`;f%IIM_VW45?#Zy z+zi?~>n^o*{P<^W5PrHqgS$+|(#3&`EAF#TeXUNc9|DmyMw>%fVm0QXa-9YoxNx|_ zt|3;rXsGXc@8A&JSW#(JRaIGGStY(oOQwg0+-q^z1f-7VC!;^{U>0Chk?*J!#e4UY zcY6W%W5n2ZvSl@`oECYV>wNRgPC8>S5!G20>t~<&>Q|q^!)_)f=34*09L-uAV^we> zMldJRJ2n=%etq;h+|b0t5WeV-2zEp!mZVv=$yVf;_IQ;j)v;!GHtA$tGR`m*?y=O} z#j@^Nm3I(sdJ&R^X?o{X6*(LSZim}dQL&4DA8b)5A)ziE{%>kovHv>GZLuz zx88jFLO2{_W2`9czvajga9r1y7lK?4E*Yi=R%CvRkM>@H>$%?7cfE(+^^T6Cyjr%a zdx>QQkc{!9%<7tUy7E|#M5*mhN0H5>X48b0mu07}!Fl6xFa4eZ*_6NQDBS+KhK9QR z^ln!^mnrX&Be(3AL>8qBhcCSS=36MQ1ZibJ<#djXE}<@b80Fmx>&m~{{p#y2%yvvw zV|Rb)?t5F9*H6pqsF~#_2e|KZuQOfSflXy!Wbb88zwRPyQzQ~c5%e7NH@+(=gZF&x zoJzlg zEA~z1uW*4Dc4sr;VtI{34X<3Ij~_sE~fL@P5Ei_B_332GIk zq9SO7(AEU|vI`bxq&L=B_j_HhcL0iE>BpR{f#juqV{m3cw{`4HY}>YHV%xTDCllM|#CGz; zwr$(CZ{B*p@5lXp`*d}k({<3hx_Y1L-M!YL%(Vv@Z?Qk8e~3bOdUkV_m9;CtCPXCT zSn}A~1YGLeXo|=~JZ}|%X%jnV`P~QwZh?#JcYk|5GpoU15Uslh3!+hoLO_V!R#Ebr zINvM~CbBXTR^^;?6AN+E*3}_y%<^0Z+vw5bUF3CF*UShQbHOIb_y0V1rg z+3{+2l|FoaCxfkIS-9TRsu@Pmc|Dy!JRnR+gsND&3D*x0)+yg_V#mih-5=hh)^d!Y z?x>6+)3TMLaR~DI&VEKKQpujM&V@BKJxNKChwnnadRl)z1T=o%tJD0DGQYWKj0`zf zSVUQC4~+kg%oFb2@O{tt^n@SX84=$K-=`vX;YEpW_dFO;=^LSgz-E(BZQcb+c92fV zQRtlP@Oi&9t_)EqDi!)u|6XxC8|&K{m6VEfShqs8p!H!_do3&M7A z2yD02R=ubKha0P0gtOQvS*5W4DlF~O?}<$mm0}Gc(V;-s@cH706!Kw5O_d2Zs04S1 zn8pfV*R&GR5t7jnDauwU^T5BekyX;xSSPeAVCcwqeXrJO&%(UX-C-O$4#X!PQvdCH zbWh3+Ol?Ud<6IAhuj}Fx&VET91&+Rl%~&2`<+>UNWU!))ZQIc~tWr>w$RGr!-L)2 z%XYOgt8CXyVA)mH>Tx|~BRc{5YQht<1zBKZcE!8o{8Ct^8{5Hl=ymrmuFT7`U+M|eDUNq|JpH>sUXVb1aXciU0K+e@BrM$Cz4m#fu2G&|LH3qUkx#+U(>4@j@3rbZ!(E2ny2fDlV@{$EA<~BZ`k2&}lQQV)<>6~70 zrOn%kKdZ<%b=TfV8-|OBe92-a{bw zuu7jk5H_4Ar@j2AXAiuU!V}YOzBAEse)_tM)6|$Vp zOAwbQF!fS0Rp$$5*{k;0meX09&JsY8aq=a~4yH$GE=y}K^t^>|GYhcqcMW0&zkb!= zmMa@^o#3Sf7WNRNwebh&0ozR8LK1ko^Xpr#_#OAh^12?0>s(F(9r4~RitXU@D=_#Y z{U8YOyna|Kf%gXD&mj{mbQ^)0m7<&|`XU&9D^msIo3x>V&IzDDc#1IwRmXaKAgQx9 z{?P|wuj$P{HnFk5KORo8RPcF*!v+)c3`Hk-WP^x;d2@6iRONdXzME zBM{sI=}2LC7yyp1X2!6oCxl^iszYyF(~*kC1S=fLvBaZxbrCv7XV#2C1gc~T(n;Xz z+5ICws2KxrpPE8ayVEg*?&!+Yd>; z%7(UQE}{YHn(}9RKwj9GI2=*m3VLa|yA+&Qb3fM^Lp_>FZvr!*2(8pmpPiKLm$g|fElhq+JDd)@N3zpl0(Gnk1o zca7tey(WnlX&lY7bF#fJzDw#Vx6{{|HTy{qCX^w% z_c7csci8eV4iO)d;G0h{<#EV0#bjYfJqFzh>#uc`L)~9MF8l-pNQ2OFHM|bvl}m)g ztVhGBuCCf~V`kXw@0F$)7Jp7vv|d0-$}D;khVlt_2{D9_ae3m4nCQoyYKDkM#Ya9a z1(Qqmhd^tx3|~0c)iX!V5Zw(QAMa_=QrL7B7Rmde8vBivh5HlMjnyej>#?t0q6vQo zkgfphGS&fhTY`2E%|9oj#6IeEQb(mhXNv$JSS+8#xFO zed`W+v%+a$<>krcWhhg2*Vb0dFE=3%V8#aULpJ#Lo`%h3c^1HDw%ge`1yCN%Mng$0 zrr~5l#-&%;D2X*f^k9(**%UHu#6ttB>ZgACEIe#9vyvjQl~uW91Y%xoVR`XTXW#gc z$YRcnz^VL{Z&RrdCj{xi;%{4u#3FRV`1F=PLl`(5h%%%$jD_`d*JF(J`KOX)F8M^zt$pw5!TXe_&Dx zsL^d2-o%86aSlz@4FF}Tr{~D;Q>SuK|jx_`&FFWdue87v#7C>u~L@` zUT)e`?YiE&U|^$oB%rb@AfAsebuN}McBkDac z=*%xM5u+5SX-b<_Z>YQTn>o1`eqCF#Od90`ym#c;I6dp@hH8U8pOhD`o!^ zeWrKQ!@HO6ot#jzfv1romiiN6okbRabli~v7YEf|8J;9*l}8OOtHOPf`TQyr?_Tec zTU0neOb?zkjNe)?h5n-lG^KVxhK`QD=YiI4*SQ}PA1)#^C=<*7cJdh-ah4H_$K%>E zCCWvr3Sqi0h49yERUhpGR7Z!eU`v0)BshG(tV_=CZ9Z2wGd4UWA;K|qvgi0HpC{Gj zDJ?6K26o+YQkoK!6PD@qas3GNMm9f#DhDLF%g9to8VP1opKJ?%!Gd|R*d+YUr~b{e zO93c%_y|J<{K<_U`w14cNrUVqbc@G~i7`@g3JI9fUpT-LkeU2-j@rDGhuBZAU*eX8 zR$(H6nnyx8V5k9ey=v0loHjmtQ!K3ivUjY>Cov%>E8TN|&&rWN{DkBR(H8zm==<(t zAZ4>SaAJsQvLq+>4>6Lu`cA*RE`#n;S66P|JMx@GErtM}_%PK?hrkv2KZP>|kYN zMOfa-uH$&OsB~)89oIXEC3efNJ3qGIq9MZZ`xAlh^=04fnp!0mVcY3hmx7#&58KYS zoMV1QlJ=519MbgDAw)xyxMK_AU$knbY=7mWOk9OE3wGfWnigpblta)|HY^nh=<+`m z4;%f1Y_}xB1=zqAEFv2XGRo9}u#663X^MJF?rJKCZr~CLo<38jmcUu=KT+IGaI|X9 z`Aj^?Bx0zB#Ymx{I>=DxdA3lB#>sSS4$!;qN;J$G+Cj=U9}m{Zi9U{|*v*|fJI&6I zvfuANj$dSa9@dBj)Wiq zVa})!t^B3rsxrja7dD%DN>N>ryjv{w_RLU0K>@fwiH9;l2%JPF(P;58rjVHrn1hXZ zn2{u>HQp*rIy4BtBKgqxo(Lw<9tp-ji7sDS9}dJ-lxO#Y5%vA@PSAGcp!RR4gyG*M z#ui)L+Hcmw*@d;V3*=uRk>h=ocDgTk-hMuiQjUpXs;c;jSIi+h8k~qziBD;_I_6yY zkoQZ{N}C@eTgCKEaacIkWCf@S75U$DH7}K;tM9wM2gAlgu~nH=^ShL1=vEvxb&*vV z>hH~3Wk=I}Ftw;sMiVm(hkH|kQK4 zCX+g zHIt17W+01jqIK}_8ro@oAVIQ;)8(-s)|TJr?dAzN+EnP%5gCyaO~ClyBTnFZ+BScg zXKtmVgA`OR?6bSI_7swWtCWxs1Zd~Ro16_mPK~?`Ivtpc$Yz@#y6yS%d2>9AOFO6( z>o;e*eHsyx2DZ^_dGM?yPRr{Ib3S=zxLS&>CH9%~QtaENv5)jG{pPMN^CVK^GEe8c z2(w{xX<=9hBPML8#;sMZ1!ok)YJu)BEAyQj{8Xvxt|9yA(|Bs&IGE1*p}dnbGXm!` zd~elj?b$Y}sa5OwdtOM>Gs#aj6_QiYm{#(*n3x8f#MzTvANgbN8x0CBm$M7*_MUOq zOwRZ~n!AXs;j6lK;gUV&woLder$%pT3Y9msz8&HNd1~ZH+P9B+wRSEl7`~lTjqLyd z(z5qz**6JVv^xgKNq43h^Z*)zz`MTz-bOiCA>Goo_Ar^Ux@iu5Nf0XMoKPd)ome9! zycH?|aJWy}!)CwtsqgQhN05He(NapL4eI{G1!QadV-SK({KU)k&ZoRb`P(yRDNmdp z6P%RHsQm4Zcsm&lQo1KoLWL^3keMa#S!XDN2F7%OH%xpjRic5LFnNb91>GoMo<@1J zwXtimYRif#kA9R=!NJYUeyOL_N-XB!kO!YU-moexPp}p2(GtA6%1PV8eca*HyC_Ic zNB_2rUMC(EY9?0qG?9l(nLnltLRRilBwxit<-hM5Zd?)xifR&|!8k%w&#c|(=KG}K z?0NwMIe^F~Uaj&&sKg{KQ6?z48!ub)=j0Q&sH!E)s5IK4ZwK@h@q$I8uk4a7*wPlA zW`OqC+Sb;U*iWY?_-gMfyyXMb;% zqft0L9jNlfdUUge}RIgR4JD0wg^N@h(qC!?mxkV`nC3cQcp+i!n88O6qL zCut3MU3Wg`cqM_SLNP%cU=}aAaQk3SvDeo2B#YF<5e_cxI*GecCQ)4KG#MBQegd_P^D&tA0<6fbpSxb2z2j$?+3 zxl7`e0^lB*lQ?X)*Ufj)A=l~k&R`w6{;>;j*`EG>9^MaWyClVzX^qz511*TKIj-JR zZz9=0VR2aldy`I5b11{)!(~d5gwPJHsf%*yFc1z1kE zN^;8RdKb2fRW%$OmvK58w-fEPI_`c46C4j)-+pxv zf2k5|c{9Bjtg;@P#d}IwQ$EO8QAO>>DQ;fgeJ>Bs;mx*ZY+~0u|GDSX1y}DE-kka8?gO70L$=s<#5OR$?|z6#lQ<+pd#0O zmo(4$(V1+>O9$w(guern8|41!Ml%L&~9hV_5ChmxjIwW{W;$KG2ZRNgZxGRit-j}=O+3D zU#;gUV+8o(SnJfcX}1C+7je18RIgGW{O$u0=v9JaJR5X!8Wbjz(r~WsouP)2HkHVm zOR>3@wMR{(sVPDANkfM^Hl-;wpuhOF6w3TVS$Z&K4v6m=k`Ep-*{n3M+2}iDmPi-O z6K|9*uWU@D9Me!B#BJ9sMMoD@^dPfU<)=r4ShD;`q-Lp)Bl`u(b}X@fZ%enQtfI0O zOPLx+Au0=_{k^r2y?BN8+D5mI{{eaJ3nYtN1w=TOKY~<(qIkPFfq-ABLJk(yIsKF% zGw0FOUeI5eaYN$f0>V?29c^m1AlHDPPuzmqvYIo=@AK-Ybsammc%{N)yQrMm-LvLU z)XyCec)grdsC8ui$M};rLQr+QaM9RC*94|`SJq)kDSd9Ua5RbjzV5WMvaSOD0$~hvNY1J70Yye!*w>O!2zT}a0ysLPSnV;< z6!c<92ECUSC+7tWZFTho+M;#0YrArmbFR9U-WJjM<#5;8$FCDH_qvJJ^X2Jy-EBQ=Ja=PU8m5fYTO$&n=9ZiJdGHza$40<~8AcPls{DyZjb$T$? zz-teug&EOyM(?TV^f(M zE91n#z~Oj?1N;o2$c39O+O|u=_Dc5n+yv~PTAK7R(fT1wj^2)FquE z7?Pe&Re5PP0;IAWL`8n&xveoNhc&46-%RIe^SGyGsO zCQKu2>5sKMVCePa{iKl?0Mnbh6xNuibG3LsevY{Ap8Sp}I8h-a^rNo+vHb;49{YN9 zB<$2c>uSL|$+&i48aX&WTu0afU3t0fb&Xd-z%N7R@truK*Jj-AEP?(U6B{_+wcL4y zD~QHoZ+p5Qn>v!otS4njL#+vJvR#vC=Pfkk5%O_<@aVQ>vB~JWhziRgajY_trJ^;} z7TBucwmvjd!FrXH*_l36H4&_tGS1wSC8S`kq4~0<%gpMWvR(4=#?iG)yd8v4?zC=W zwrpvT_b^cueC`0Nh&GR* z?bWmjy)K48?diIt2p!Z*&*wNBE&Z%`Dk~VHY^{?!-#KnuAi3uRBbNhw1rjhAmo{M`tfnU_>lN$iPZ<`6PRQk^5 zxaGdsq|jv4r5>+6|K;Wv76fZC$bfhzOF%>t`! zo0sQp>px*k2o?j3#F@R2xBac7f#~2r?YhI!+XCQZh_z#BjxBt6j!#5SP{!dH`SnI8Bs$Eb(yrC~yX} z2rYSEEx8#3(U5YIt7c(y>m`(jk^;VTAuIw(TN2m?#ku5b0?dQ2{Zd&l!yx&OWm`FlCIymY-g6DM6N>3Ra;?`&w%z+>*!en-Yn~9H z^Pb}fOmnW@Jqd1iH~@)OtW^&*8{y*{0+058jAlkQ3TBK@pPbGd9$(s41%&qXjxc%e z8~aL!mmNW%hqJqJT}X@yW+$mA5NK?7bWcz1&T|#@x`yZk*j(KEmHO&Cf#$AlZHV03 zwU$Y8xvtKBuhFq6H;MWj{DWw=vB5EA4EH$SI1$%lI2NTjaW-v`Jx)O`A)s@*uvFe) z{B!b1j;wn0m_tTj1{|WIg|oAn{)mS}qP4P9E6%Ken^S >-Aun5A4Gp>4U0IQJ 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 G)cz05(PHWV diff --git a/examples-cloudflare/bugs/gh-119/app/globals.css b/examples-cloudflare/bugs/gh-119/app/globals.css deleted file mode 100644 index f101930c..00000000 --- a/examples-cloudflare/bugs/gh-119/app/globals.css +++ /dev/null @@ -1,21 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/examples-cloudflare/bugs/gh-119/app/layout.tsx b/examples-cloudflare/bugs/gh-119/app/layout.tsx deleted file mode 100644 index 3d10f520..00000000 --- a/examples-cloudflare/bugs/gh-119/app/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import "./globals.css"; - -import type { Metadata } from "next"; -import localFont from "next/font/local"; - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/examples-cloudflare/bugs/gh-119/app/page.tsx b/examples-cloudflare/bugs/gh-119/app/page.tsx deleted file mode 100644 index fbad9f8f..00000000 --- a/examples-cloudflare/bugs/gh-119/app/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( - - ); -} diff --git a/examples-cloudflare/bugs/gh-119/e2e/base.spec.ts b/examples-cloudflare/bugs/gh-119/e2e/base.spec.ts deleted file mode 100644 index 9fe2a947..00000000 --- a/examples-cloudflare/bugs/gh-119/e2e/base.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("bugs/gh-119", () => { - test("the index page of the application shows the Next.js logo", async ({ page }) => { - await page.goto("/"); - await expect(page.getByAltText("Next.js logo")).toBeVisible(); - }); -}); diff --git a/examples-cloudflare/bugs/gh-119/e2e/playwright.config.ts b/examples-cloudflare/bugs/gh-119/e2e/playwright.config.ts deleted file mode 100644 index 8677f9be..00000000 --- a/examples-cloudflare/bugs/gh-119/e2e/playwright.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { configurePlaywright } from "../../../common/config-e2e"; - -export default configurePlaywright("gh-119"); diff --git a/examples-cloudflare/bugs/gh-119/next.config.ts b/examples-cloudflare/bugs/gh-119/next.config.ts deleted file mode 100644 index d0793cd5..00000000 --- a/examples-cloudflare/bugs/gh-119/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - typescript: { ignoreBuildErrors: true }, - eslint: { ignoreDuringBuilds: true }, -}; - -export default nextConfig; diff --git a/examples-cloudflare/bugs/gh-119/open-next.config.ts b/examples-cloudflare/bugs/gh-119/open-next.config.ts deleted file mode 100644 index ffd98878..00000000 --- a/examples-cloudflare/bugs/gh-119/open-next.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineCloudflareConfig } from "@opennextjs/cloudflare"; - -export default defineCloudflareConfig(); diff --git a/examples-cloudflare/bugs/gh-119/package.json b/examples-cloudflare/bugs/gh-119/package.json deleted file mode 100644 index c9274ae6..00000000 --- a/examples-cloudflare/bugs/gh-119/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "examples-cloudflare/gh-119", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "build:worker": "pnpm opennextjs-cloudflare build", - "preview:worker": "pnpm opennextjs-cloudflare preview", - "preview": "pnpm build:worker && pnpm preview:worker", - "e2e": "playwright test -c e2e/playwright.config.ts", - "cf-typegen": "wrangler types --env-interface CloudflareEnv" - }, - "dependencies": { - "next": "15.5.9", - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@opennextjs/cloudflare": "workspace:*", - "@playwright/test": "catalog:", - "@types/node": "^22", - "@types/react": "^18", - "@types/react-dom": "^18", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "catalog:", - "wrangler": "catalog:" - } -} diff --git a/examples-cloudflare/bugs/gh-119/postcss.config.mjs b/examples-cloudflare/bugs/gh-119/postcss.config.mjs deleted file mode 100644 index f6c3605a..00000000 --- a/examples-cloudflare/bugs/gh-119/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/examples-cloudflare/bugs/gh-119/public/file.svg b/examples-cloudflare/bugs/gh-119/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/examples-cloudflare/bugs/gh-119/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-119/public/globe.svg b/examples-cloudflare/bugs/gh-119/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/examples-cloudflare/bugs/gh-119/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-119/public/next.svg b/examples-cloudflare/bugs/gh-119/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/examples-cloudflare/bugs/gh-119/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-119/public/vercel.svg b/examples-cloudflare/bugs/gh-119/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/examples-cloudflare/bugs/gh-119/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-119/public/window.svg b/examples-cloudflare/bugs/gh-119/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/examples-cloudflare/bugs/gh-119/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-119/tailwind.config.ts b/examples-cloudflare/bugs/gh-119/tailwind.config.ts deleted file mode 100644 index c93eb9ca..00000000 --- a/examples-cloudflare/bugs/gh-119/tailwind.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Config } from "tailwindcss"; - -export default { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, - }, - plugins: [], -} satisfies Config; diff --git a/examples-cloudflare/bugs/gh-119/tsconfig.json b/examples-cloudflare/bugs/gh-119/tsconfig.json deleted file mode 100644 index 53b6ed51..00000000 --- a/examples-cloudflare/bugs/gh-119/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules", "open-next.config.ts"] -} diff --git a/examples-cloudflare/bugs/gh-119/wrangler.jsonc b/examples-cloudflare/bugs/gh-119/wrangler.jsonc deleted file mode 100644 index c8ecfcee..00000000 --- a/examples-cloudflare/bugs/gh-119/wrangler.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "main": ".open-next/worker.js", - "name": "gh-119", - "compatibility_date": "2024-12-30", - "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], - "assets": { - "directory": ".open-next/assets", - "binding": "ASSETS" - } -} diff --git a/examples-cloudflare/bugs/gh-219/.dev.vars b/examples-cloudflare/bugs/gh-219/.dev.vars deleted file mode 100644 index 17f2dcc2..00000000 --- a/examples-cloudflare/bugs/gh-219/.dev.vars +++ /dev/null @@ -1 +0,0 @@ -NEXTJS_ENV=development \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-219/.gitignore b/examples-cloudflare/bugs/gh-219/.gitignore deleted file mode 100644 index 4212e83f..00000000 --- a/examples-cloudflare/bugs/gh-219/.gitignore +++ /dev/null @@ -1,56 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# Firebase -.firebase/ -firebase-debug.log -.env -.env.local -.env.development -.env.test -.env.production - -# playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/examples-cloudflare/bugs/gh-219/README.md b/examples-cloudflare/bugs/gh-219/README.md deleted file mode 100644 index acfd4ae9..00000000 --- a/examples-cloudflare/bugs/gh-219/README.md +++ /dev/null @@ -1,38 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. - -# opentelemetry-issue diff --git a/examples-cloudflare/bugs/gh-219/e2e/base.spec.ts b/examples-cloudflare/bugs/gh-219/e2e/base.spec.ts deleted file mode 100644 index 45ab6fa9..00000000 --- a/examples-cloudflare/bugs/gh-219/e2e/base.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("bugs/gh-219", () => { - test("the index page of the application shows the Next.js logo", async ({ page }) => { - await page.goto("/"); - await expect(page.getByAltText("Next.js logo")).toBeVisible(); - }); -}); diff --git a/examples-cloudflare/bugs/gh-219/e2e/playwright.config.ts b/examples-cloudflare/bugs/gh-219/e2e/playwright.config.ts deleted file mode 100644 index d42e84f4..00000000 --- a/examples-cloudflare/bugs/gh-219/e2e/playwright.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { configurePlaywright } from "../../../common/config-e2e"; - -export default configurePlaywright("gh-219"); diff --git a/examples-cloudflare/bugs/gh-219/next.config.ts b/examples-cloudflare/bugs/gh-219/next.config.ts deleted file mode 100644 index d0793cd5..00000000 --- a/examples-cloudflare/bugs/gh-219/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - typescript: { ignoreBuildErrors: true }, - eslint: { ignoreDuringBuilds: true }, -}; - -export default nextConfig; diff --git a/examples-cloudflare/bugs/gh-219/open-next.config.ts b/examples-cloudflare/bugs/gh-219/open-next.config.ts deleted file mode 100644 index ffd98878..00000000 --- a/examples-cloudflare/bugs/gh-219/open-next.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineCloudflareConfig } from "@opennextjs/cloudflare"; - -export default defineCloudflareConfig(); diff --git a/examples-cloudflare/bugs/gh-219/package.json b/examples-cloudflare/bugs/gh-219/package.json deleted file mode 100644 index 5d740f20..00000000 --- a/examples-cloudflare/bugs/gh-219/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "examples-cloudflare/gh-219", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "build:worker": "pnpm opennextjs-cloudflare build", - "preview:worker": "pnpm opennextjs-cloudflare preview", - "preview": "pnpm build:worker && pnpm preview:worker", - "e2e": "playwright test -c e2e/playwright.config.ts", - "deploy:worker": "pnpm run build:worker && pnpm wrangler deploy" - }, - "dependencies": { - "@hookform/resolvers": "^3.9.1", - "@libsql/client": "^0.14.0", - "@t3-oss/env-nextjs": "^0.11.1", - "@tanstack/react-table": "^8.20.6", - "better-sqlite3": "^11.7.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "drizzle-orm": "^0.38.3", - "firebase": "^11.1.0", - "firebase-admin": "^13.0.2", - "lucide-react": "^0.469.0", - "nanoid": "^5.0.9", - "next": "15.5.9", - "next-auth": "^4.24.11", - "next-themes": "^0.4.4", - "qrcode.react": "^4.2.0", - "react": "^19.0.3", - "react-dom": "^19.0.3", - "react-hook-form": "^7.54.2", - "react-icons": "^5.4.0", - "sonner": "^1.7.1", - "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7", - "zod": "^3.24.1" - }, - "devDependencies": { - "@cloudflare/workers-types": "catalog:", - "@opennextjs/cloudflare": "workspace:*", - "@playwright/test": "catalog:", - "@types/better-sqlite3": "^7.6.12", - "@types/node": "^22", - "@types/react": "^19", - "@types/react-dom": "^19", - "cross-env": "^7.0.3", - "drizzle-kit": "^0.30.1", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "catalog:", - "vercel": "^39.2.2", - "wrangler": "catalog:" - } -} diff --git a/examples-cloudflare/bugs/gh-219/postcss.config.mjs b/examples-cloudflare/bugs/gh-219/postcss.config.mjs deleted file mode 100644 index f6c3605a..00000000 --- a/examples-cloudflare/bugs/gh-219/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/examples-cloudflare/bugs/gh-219/public/file.svg b/examples-cloudflare/bugs/gh-219/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/examples-cloudflare/bugs/gh-219/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-219/public/globe.svg b/examples-cloudflare/bugs/gh-219/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/examples-cloudflare/bugs/gh-219/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-219/public/next.svg b/examples-cloudflare/bugs/gh-219/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/examples-cloudflare/bugs/gh-219/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-219/public/vercel.svg b/examples-cloudflare/bugs/gh-219/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/examples-cloudflare/bugs/gh-219/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-219/public/window.svg b/examples-cloudflare/bugs/gh-219/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/examples-cloudflare/bugs/gh-219/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-219/src/app/favicon.ico b/examples-cloudflare/bugs/gh-219/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/examples-cloudflare/bugs/gh-219/src/app/globals.css b/examples-cloudflare/bugs/gh-219/src/app/globals.css deleted file mode 100644 index f101930c..00000000 --- a/examples-cloudflare/bugs/gh-219/src/app/globals.css +++ /dev/null @@ -1,21 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/examples-cloudflare/bugs/gh-219/src/app/layout.tsx b/examples-cloudflare/bugs/gh-219/src/app/layout.tsx deleted file mode 100644 index 3db18ea3..00000000 --- a/examples-cloudflare/bugs/gh-219/src/app/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import "./globals.css"; - -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/examples-cloudflare/bugs/gh-219/src/app/page.tsx b/examples-cloudflare/bugs/gh-219/src/app/page.tsx deleted file mode 100644 index 42598ee0..00000000 --- a/examples-cloudflare/bugs/gh-219/src/app/page.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- - -
- -
- ); -} diff --git a/examples-cloudflare/bugs/gh-219/src/firebase/config.js b/examples-cloudflare/bugs/gh-219/src/firebase/config.js deleted file mode 100644 index 2e8d38ed..00000000 --- a/examples-cloudflare/bugs/gh-219/src/firebase/config.js +++ /dev/null @@ -1,23 +0,0 @@ -import { initializeApp } from "firebase/app"; -import { getAuth } from "firebase/auth"; -import { getFirestore } from "firebase/firestore"; -import { getStorage } from "firebase/storage"; - -const firebaseConfig = { - apiKey: process.env.REACT_APP_FIREBASE_API_KEY, - authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, - projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, - storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.REACT_APP_FIREBASE_APP_ID, -}; - -// Initialize Firebase -const app = initializeApp(firebaseConfig); - -// Initialize Firebase services -export const auth = getAuth(app); -export const db = getFirestore(app); -export const storage = getStorage(app); - -export default app; diff --git a/examples-cloudflare/bugs/gh-219/tailwind.config.ts b/examples-cloudflare/bugs/gh-219/tailwind.config.ts deleted file mode 100644 index 5d3c1bd2..00000000 --- a/examples-cloudflare/bugs/gh-219/tailwind.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Config } from "tailwindcss"; - -export default { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, - }, - plugins: [], -} satisfies Config; diff --git a/examples-cloudflare/bugs/gh-219/tsconfig.json b/examples-cloudflare/bugs/gh-219/tsconfig.json deleted file mode 100644 index d55979b5..00000000 --- a/examples-cloudflare/bugs/gh-219/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules", "open-next.config.ts"] -} diff --git a/examples-cloudflare/bugs/gh-219/wrangler.jsonc b/examples-cloudflare/bugs/gh-219/wrangler.jsonc deleted file mode 100644 index 87558ac3..00000000 --- a/examples-cloudflare/bugs/gh-219/wrangler.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "main": ".open-next/worker.js", - "name": "gh-219", - "compatibility_date": "2024-12-30", - "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], - "assets": { - "directory": ".open-next/assets", - "binding": "ASSETS" - } -} diff --git a/examples-cloudflare/bugs/gh-223/.gitignore b/examples-cloudflare/bugs/gh-223/.gitignore deleted file mode 100644 index b5348851..00000000 --- a/examples-cloudflare/bugs/gh-223/.gitignore +++ /dev/null @@ -1,53 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - - -# Cloudflare related -/.open-next -/.wrangler - -# wrangler files -.wrangler -.dev.vars - -/.vscode - -# playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-223/README.md b/examples-cloudflare/bugs/gh-223/README.md deleted file mode 100644 index c4033664..00000000 --- a/examples-cloudflare/bugs/gh-223/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples-cloudflare/bugs/gh-223/app/api/image/route.ts b/examples-cloudflare/bugs/gh-223/app/api/image/route.ts deleted file mode 100644 index d8552fd4..00000000 --- a/examples-cloudflare/bugs/gh-223/app/api/image/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -import { getImageUrl } from "../../../src/utils/s3Bucket"; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const fileName = searchParams.get("fileName"); - return NextResponse.json( - { - image: fileName ? await getImageUrl(fileName) : "", - }, - { - status: 200, - } - ); -} diff --git a/examples-cloudflare/bugs/gh-223/app/favicon.ico b/examples-cloudflare/bugs/gh-223/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/examples-cloudflare/bugs/gh-223/app/globals.css b/examples-cloudflare/bugs/gh-223/app/globals.css deleted file mode 100644 index 3422b7e6..00000000 --- a/examples-cloudflare/bugs/gh-223/app/globals.css +++ /dev/null @@ -1,29 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} diff --git a/examples-cloudflare/bugs/gh-223/app/layout.tsx b/examples-cloudflare/bugs/gh-223/app/layout.tsx deleted file mode 100644 index eb2b4dd9..00000000 --- a/examples-cloudflare/bugs/gh-223/app/layout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import "./globals.css"; - -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/examples-cloudflare/bugs/gh-223/app/page.tsx b/examples-cloudflare/bugs/gh-223/app/page.tsx deleted file mode 100644 index cadc560e..00000000 --- a/examples-cloudflare/bugs/gh-223/app/page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import Image from "next/image"; - -export default function Home() { - return ( -
-
-

- Get started by editing  - src/app/page.tsx -

- -
- -
- Next.js Logo -
- - -
- ); -} diff --git a/examples-cloudflare/bugs/gh-223/e2e/base.spec.ts b/examples-cloudflare/bugs/gh-223/e2e/base.spec.ts deleted file mode 100644 index f79ad9ac..00000000 --- a/examples-cloudflare/bugs/gh-223/e2e/base.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("bugs/gh-223", () => { - test("api route", async ({ page }) => { - const res = await page.request.get("/api/image"); - expect(res.status()).toEqual(200); - expect((await res.json()).image).toEqual(""); - }); -}); diff --git a/examples-cloudflare/bugs/gh-223/e2e/playwright.config.ts b/examples-cloudflare/bugs/gh-223/e2e/playwright.config.ts deleted file mode 100644 index 6cf894bc..00000000 --- a/examples-cloudflare/bugs/gh-223/e2e/playwright.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { configurePlaywright } from "../../../common/config-e2e"; - -export default configurePlaywright("gh-223"); diff --git a/examples-cloudflare/bugs/gh-223/next.config.mjs b/examples-cloudflare/bugs/gh-223/next.config.mjs deleted file mode 100644 index 51c629d2..00000000 --- a/examples-cloudflare/bugs/gh-223/next.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - typescript: { ignoreBuildErrors: true }, - eslint: { ignoreDuringBuilds: true }, -}; - -export default nextConfig; diff --git a/examples-cloudflare/bugs/gh-223/open-next.config.ts b/examples-cloudflare/bugs/gh-223/open-next.config.ts deleted file mode 100644 index ffd98878..00000000 --- a/examples-cloudflare/bugs/gh-223/open-next.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineCloudflareConfig } from "@opennextjs/cloudflare"; - -export default defineCloudflareConfig(); diff --git a/examples-cloudflare/bugs/gh-223/package.json b/examples-cloudflare/bugs/gh-223/package.json deleted file mode 100644 index f230e2d5..00000000 --- a/examples-cloudflare/bugs/gh-223/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "examples-cloudflare/gh-223", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "build:worker": "pnpm opennextjs-cloudflare build", - "preview:worker": "pnpm opennextjs-cloudflare preview", - "preview": "pnpm build:worker && pnpm preview:worker", - "e2e": "playwright test -c e2e/playwright.config.ts", - "deploy:worker": "pnpm run build:worker && pnpm wrangler deploy" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.971.0", - "@aws-sdk/s3-request-presigner": "^3.971.0", - "next": "15.5.9", - "react": "^19.0.3", - "react-dom": "^19.0.3" - }, - "devDependencies": { - "@cloudflare/workers-types": "catalog:", - "@opennextjs/cloudflare": "workspace:*", - "@playwright/test": "catalog:", - "@types/node": "^22.10.2", - "@types/react": "^19.0.3", - "@types/react-dom": "^19.0.3", - "postcss": "^8.4.49", - "tailwindcss": "^3.4.17", - "typescript": "catalog:", - "wrangler": "catalog:" - } -} diff --git a/examples-cloudflare/bugs/gh-223/postcss.config.mjs b/examples-cloudflare/bugs/gh-223/postcss.config.mjs deleted file mode 100644 index f6c3605a..00000000 --- a/examples-cloudflare/bugs/gh-223/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/examples-cloudflare/bugs/gh-223/public/next.svg b/examples-cloudflare/bugs/gh-223/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/examples-cloudflare/bugs/gh-223/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-223/public/vercel.svg b/examples-cloudflare/bugs/gh-223/public/vercel.svg deleted file mode 100644 index d2f84222..00000000 --- a/examples-cloudflare/bugs/gh-223/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/bugs/gh-223/src/utils/common.ts b/examples-cloudflare/bugs/gh-223/src/utils/common.ts deleted file mode 100644 index 0e757e29..00000000 --- a/examples-cloudflare/bugs/gh-223/src/utils/common.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Optional: Check file size (e.g., max 5MB) -export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB - -export const validateImageFile = (file: File): void => { - const allowedImageTypes = ["image/jpeg", "image/png", "image/gif", "image/jpg"]; - - // Check file type - if (!allowedImageTypes.includes(file.type)) { - throw new Error("Invalid file type. Please upload a valid image file."); - } - - if (file.size > MAX_FILE_SIZE) { - throw new Error("File size exceeds the maximum limit of 5MB."); - } -}; - -export const getImageUrlFromS3 = async (fileName: string) => { - try { - const url = await fetch(`/api/image?fileName=${fileName}`, { - method: "GET", - }); - //@ts-ignore - const { image } = await url.json(); - return image; - } catch (error) { - console.log({ error }); - throw new Error("Failed to get image"); - } -}; diff --git a/examples-cloudflare/bugs/gh-223/src/utils/s3Bucket.ts b/examples-cloudflare/bugs/gh-223/src/utils/s3Bucket.ts deleted file mode 100644 index e16908cd..00000000 --- a/examples-cloudflare/bugs/gh-223/src/utils/s3Bucket.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; - -/** - * This function should only be used inside api calls - */ -export const getImageUrl = async (fileName: string) => { - try { - const s3Client = new S3Client({ - region: "REGION", - endpoint: "ENDPOINT", - credentials: { - accessKeyId: "ACCESS_KEY_ID", - secretAccessKey: "SECRET_ACCESS_KEY", - }, - }); - - const command = new GetObjectCommand({ - Key: fileName.trim().toLowerCase().replace(/ /g, "-"), - Bucket: process.env.CLOUDFLARE_R2_BUCKET || "", - ResponseExpires: new Date(Date.now() + 3600), - }); - const presignedUrl = await getSignedUrl(s3Client, command); - - return presignedUrl; - } catch (error) { - console.log({ error }); - throw new Error("Failed to get image"); - } -}; diff --git a/examples-cloudflare/bugs/gh-223/tailwind.config.ts b/examples-cloudflare/bugs/gh-223/tailwind.config.ts deleted file mode 100644 index 86fc7b5d..00000000 --- a/examples-cloudflare/bugs/gh-223/tailwind.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Config } from "tailwindcss"; - -const config: Config = { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - }, - }, - plugins: [], -}; -export default config; diff --git a/examples-cloudflare/bugs/gh-223/tsconfig.json b/examples-cloudflare/bugs/gh-223/tsconfig.json deleted file mode 100644 index ee0c2d8d..00000000 --- a/examples-cloudflare/bugs/gh-223/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - }, - "types": ["@cloudflare/workers-types/2023-07-01"], - "target": "ES2017" - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules", "open-next.config.ts"] -} diff --git a/examples-cloudflare/bugs/gh-223/wrangler.jsonc b/examples-cloudflare/bugs/gh-223/wrangler.jsonc deleted file mode 100644 index 87558ac3..00000000 --- a/examples-cloudflare/bugs/gh-223/wrangler.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "main": ".open-next/worker.js", - "name": "gh-219", - "compatibility_date": "2024-12-30", - "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], - "assets": { - "directory": ".open-next/assets", - "binding": "ASSETS" - } -} diff --git a/examples-cloudflare/create-next-app/.gitignore b/examples-cloudflare/create-next-app/.gitignore deleted file mode 100644 index 3a282111..00000000 --- a/examples-cloudflare/create-next-app/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# wrangler -.wrangler - -# playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/examples-cloudflare/create-next-app/README.md b/examples-cloudflare/create-next-app/README.md deleted file mode 100644 index e215bc4c..00000000 --- a/examples-cloudflare/create-next-app/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples-cloudflare/create-next-app/e2e/base.spec.ts b/examples-cloudflare/create-next-app/e2e/base.spec.ts deleted file mode 100644 index eda4eaef..00000000 --- a/examples-cloudflare/create-next-app/e2e/base.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("create-next-app", () => { - test("the index page of the application shows the Next.js logo", async ({ page }) => { - await page.goto("/"); - await expect(page.getByAltText("Next.js logo")).toBeVisible(); - }); -}); diff --git a/examples-cloudflare/create-next-app/e2e/playwright.config.ts b/examples-cloudflare/create-next-app/e2e/playwright.config.ts deleted file mode 100644 index f3cf5fe4..00000000 --- a/examples-cloudflare/create-next-app/e2e/playwright.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { configurePlaywright } from "../../common/config-e2e"; - -export default configurePlaywright("create-next-app", { multipleBrowsers: true }); diff --git a/examples-cloudflare/create-next-app/next.config.mjs b/examples-cloudflare/create-next-app/next.config.mjs deleted file mode 100644 index 2bd0079f..00000000 --- a/examples-cloudflare/create-next-app/next.config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; - -initOpenNextCloudflareForDev(); - -/** @type {import('next').NextConfig} */ -const nextConfig = { - typescript: { ignoreBuildErrors: true }, - eslint: { ignoreDuringBuilds: true }, -}; - -export default nextConfig; diff --git a/examples-cloudflare/create-next-app/open-next.config.ts b/examples-cloudflare/create-next-app/open-next.config.ts deleted file mode 100644 index ffd98878..00000000 --- a/examples-cloudflare/create-next-app/open-next.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineCloudflareConfig } from "@opennextjs/cloudflare"; - -export default defineCloudflareConfig(); diff --git a/examples-cloudflare/create-next-app/package.json b/examples-cloudflare/create-next-app/package.json deleted file mode 100644 index 1611543f..00000000 --- a/examples-cloudflare/create-next-app/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "examples-cloudflare/create-next-app", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "build:worker": "pnpm opennextjs-cloudflare build", - "preview:worker": "pnpm opennextjs-cloudflare preview", - "preview": "pnpm build:worker && pnpm preview:worker", - "e2e": "playwright test -c e2e/playwright.config.ts" - }, - "dependencies": { - "next": "catalog:", - "react": "catalog:", - "react-dom": "catalog:" - }, - "devDependencies": { - "@opennextjs/cloudflare": "workspace:*", - "@playwright/test": "catalog:", - "@types/node": "catalog:", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "catalog:", - "wrangler": "catalog:" - } -} diff --git a/examples-cloudflare/create-next-app/postcss.config.mjs b/examples-cloudflare/create-next-app/postcss.config.mjs deleted file mode 100644 index f6c3605a..00000000 --- a/examples-cloudflare/create-next-app/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -export default config; diff --git a/examples-cloudflare/create-next-app/public/next.svg b/examples-cloudflare/create-next-app/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/examples-cloudflare/create-next-app/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/create-next-app/public/vercel.svg b/examples-cloudflare/create-next-app/public/vercel.svg deleted file mode 100644 index d2f84222..00000000 --- a/examples-cloudflare/create-next-app/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples-cloudflare/create-next-app/src/app/favicon.ico b/examples-cloudflare/create-next-app/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/examples-cloudflare/create-next-app/src/app/fonts/GeistMonoVF.woff b/examples-cloudflare/create-next-app/src/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185cbfd16946a534d819e9eb03924abbcc49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67864 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ϣ%UY8`RWp~V-U_7 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 CLkb=M diff --git a/examples-cloudflare/create-next-app/src/app/fonts/GeistVF.woff b/examples-cloudflare/create-next-app/src/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daacff96dad6584e71cd962051b82957c313..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66268 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 >-Aun5A4Gp>4U0IQJ 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 G)cz05(PHWV diff --git a/examples-cloudflare/create-next-app/src/app/globals.css b/examples-cloudflare/create-next-app/src/app/globals.css deleted file mode 100644 index 1a4fd67a..00000000 --- a/examples-cloudflare/create-next-app/src/app/globals.css +++ /dev/null @@ -1,27 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} diff --git a/examples-cloudflare/create-next-app/src/app/layout.tsx b/examples-cloudflare/create-next-app/src/app/layout.tsx deleted file mode 100644 index 3d10f520..00000000 --- a/examples-cloudflare/create-next-app/src/app/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import "./globals.css"; - -import type { Metadata } from "next"; -import localFont from "next/font/local"; - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/examples-cloudflare/create-next-app/src/app/page.tsx b/examples-cloudflare/create-next-app/src/app/page.tsx deleted file mode 100644 index f1b83089..00000000 --- a/examples-cloudflare/create-next-app/src/app/page.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
- - -
- Next.js Logo -
- - -
- ); -} diff --git a/examples-cloudflare/create-next-app/tailwind.config.ts b/examples-cloudflare/create-next-app/tailwind.config.ts deleted file mode 100644 index 45e6dc97..00000000 --- a/examples-cloudflare/create-next-app/tailwind.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Config } from "tailwindcss"; - -const config: Config = { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, - }, - plugins: [], -}; -export default config; diff --git a/examples-cloudflare/create-next-app/tsconfig.json b/examples-cloudflare/create-next-app/tsconfig.json deleted file mode 100644 index e1b0b279..00000000 --- a/examples-cloudflare/create-next-app/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - }, - "target": "ES2017" - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], - "exclude": ["node_modules", "open-next.config.ts"] -} diff --git a/examples-cloudflare/create-next-app/wrangler.jsonc b/examples-cloudflare/create-next-app/wrangler.jsonc deleted file mode 100644 index db23504e..00000000 --- a/examples-cloudflare/create-next-app/wrangler.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "main": ".open-next/worker.js", - "name": "create-next-app", - "compatibility_date": "2024-12-30", - "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], - "assets": { - "directory": ".open-next/assets", - "binding": "ASSETS" - } -} diff --git a/examples-cloudflare/middleware/.env b/examples-cloudflare/middleware/.env deleted file mode 100644 index 7300e230..00000000 --- a/examples-cloudflare/middleware/.env +++ /dev/null @@ -1 +0,0 @@ -CLERK_ENCRYPTION_KEY="key" diff --git a/examples-cloudflare/middleware/.gitignore b/examples-cloudflare/middleware/.gitignore deleted file mode 100755 index 998933bb..00000000 --- a/examples-cloudflare/middleware/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# playwright -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/examples-cloudflare/middleware/README.md b/examples-cloudflare/middleware/README.md deleted file mode 100755 index a8eeb816..00000000 --- a/examples-cloudflare/middleware/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Middleware - -This example shows how to use [Middleware in Next.js](https://nextjs.org/docs/app/building-your-application/routing/middleware) to run code before a request is completed. - -The index page ([`app/page.tsx`](app/page.tsx)) has a list of links to pages with `redirect`, `rewrite`, or normal behavior. - -On the Middleware file ([`middleware.ts`](middleware.ts)) the routes are already being filtered by defining a `matcher` on the exported config. If you want the Middleware to run for every request, you can remove the `matcher`. - -## Deploy your own - -Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/middleware&project-name=middleware&repository-name=middleware) - -## How to use - -Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: - -```bash -npx create-next-app --example middleware middleware-app -``` - -```bash -yarn create next-app --example middleware middleware-app -``` - -```bash -pnpm create next-app --example middleware middleware-app -``` - -Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples-cloudflare/middleware/app/about/page.tsx b/examples-cloudflare/middleware/app/about/page.tsx deleted file mode 100644 index da125747..00000000 --- a/examples-cloudflare/middleware/app/about/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AboutPage() { - return

About

; -} diff --git a/examples-cloudflare/middleware/app/about2/page.tsx b/examples-cloudflare/middleware/app/about2/page.tsx deleted file mode 100644 index 31fcb8c4..00000000 --- a/examples-cloudflare/middleware/app/about2/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function About2Page() { - return

About 2

; -} diff --git a/examples-cloudflare/middleware/app/another/page.tsx b/examples-cloudflare/middleware/app/another/page.tsx deleted file mode 100644 index b4e3ecf1..00000000 --- a/examples-cloudflare/middleware/app/another/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AnotherPage() { - return

Another

; -} diff --git a/examples-cloudflare/middleware/app/clerk/route.ts b/examples-cloudflare/middleware/app/clerk/route.ts deleted file mode 100644 index 99230885..00000000 --- a/examples-cloudflare/middleware/app/clerk/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function POST(request: Request) { - return new Response(`Hello clerk`); -} diff --git a/examples-cloudflare/middleware/app/layout.tsx b/examples-cloudflare/middleware/app/layout.tsx deleted file mode 100644 index 308ce0c6..00000000 --- a/examples-cloudflare/middleware/app/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { Metadata } from "next"; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} - -export const metadata: Metadata = { - title: "Next.js Middleware example", - description: "Redirect and rewrite pages using Next.js Middleware.", -}; diff --git a/examples-cloudflare/middleware/app/middleware/page.tsx b/examples-cloudflare/middleware/app/middleware/page.tsx deleted file mode 100644 index ead59e0a..00000000 --- a/examples-cloudflare/middleware/app/middleware/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { headers } from "next/headers"; - -export default async function MiddlewarePage() { - const cloudflareContextHeader = (await headers()).get("x-cloudflare-context"); - - return ( - <> -

Via middleware

-

- The value of the x-cloudflare-context header is:
- - {cloudflareContextHeader} - -

- - ); -} diff --git a/examples-cloudflare/middleware/app/page.tsx b/examples-cloudflare/middleware/app/page.tsx deleted file mode 100755 index 234fd787..00000000 --- a/examples-cloudflare/middleware/app/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Link from "next/link"; - -export default function Home() { - return ( -
-

Index

-

- Go to about page (will redirect) -

-

- Go to another page (will rewrite) -

-

- Go to about 2 page (no redirect or rewrite) -

-

- Go to middleware page (using NextResponse.next()) -

-
- ); -} diff --git a/examples-cloudflare/middleware/app/redirected/page.tsx b/examples-cloudflare/middleware/app/redirected/page.tsx deleted file mode 100644 index 7c4d480c..00000000 --- a/examples-cloudflare/middleware/app/redirected/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function RedirectedPage() { - return

Redirected from /about

; -} diff --git a/examples-cloudflare/middleware/app/rewrite/page.tsx b/examples-cloudflare/middleware/app/rewrite/page.tsx deleted file mode 100644 index ee87ede1..00000000 --- a/examples-cloudflare/middleware/app/rewrite/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function RewritePage() { - return

Rewrite

; -} diff --git a/examples-cloudflare/middleware/e2e/base.spec.ts b/examples-cloudflare/middleware/e2e/base.spec.ts deleted file mode 100644 index eff0f5be..00000000 --- a/examples-cloudflare/middleware/e2e/base.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("middleware", () => { - test("redirect", async ({ page }) => { - await page.goto("/"); - await page.click('[href="/about"]'); - await page.waitForURL("**/redirected"); - expect(await page.textContent("h1")).toContain("Redirected"); - }); - - test("rewrite", async ({ page }) => { - await page.goto("/"); - await page.click('[href="/another"]'); - await page.waitForURL("**/another"); - expect(await page.textContent("h1")).toContain("Rewrite"); - }); - - test("no matching middleware", async ({ page }) => { - await page.goto("/"); - await page.click('[href="/about2"]'); - await page.waitForURL("**/about2"); - expect(await page.textContent("h1")).toContain("About 2"); - }); - - test("matching noop middleware", async ({ page }) => { - await page.goto("/"); - await page.click('[href="/middleware"]'); - await page.waitForURL("**/middleware"); - expect(await page.textContent("h1")).toContain("Via middleware"); - }); - - // Test for https://github.com/opennextjs/opennextjs-cloudflare/issues/201 - test("clerk middleware", async ({ page }) => { - const res = await page.request.post("/clerk", { data: "some body" }); - expect(res.ok()).toEqual(true); - expect(res.status()).toEqual(200); - await expect(res.text()).resolves.toEqual("Hello clerk"); - }); -}); diff --git a/examples-cloudflare/middleware/e2e/cloudflare-context.spec.ts b/examples-cloudflare/middleware/e2e/cloudflare-context.spec.ts deleted file mode 100644 index 6d3c89dd..00000000 --- a/examples-cloudflare/middleware/e2e/cloudflare-context.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("middleware/cloudflare-context", () => { - test("middlewares have access to the cloudflare context", async ({ page }) => { - await page.goto("/middleware"); - const cloudflareContextHeaderElement = page.getByTestId("cloudflare-context-header"); - expect(await cloudflareContextHeaderElement.textContent()).toContain( - "typeof `cloudflareContext.env` = object" - ); - }); -}); diff --git a/examples-cloudflare/middleware/e2e/playwright.config.ts b/examples-cloudflare/middleware/e2e/playwright.config.ts deleted file mode 100644 index 75ff66cc..00000000 --- a/examples-cloudflare/middleware/e2e/playwright.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { configurePlaywright } from "../../common/config-e2e"; - -export default configurePlaywright("middleware", { multipleBrowsers: true }); diff --git a/examples-cloudflare/middleware/e2e/playwright.dev.config.ts b/examples-cloudflare/middleware/e2e/playwright.dev.config.ts deleted file mode 100644 index c2589f84..00000000 --- a/examples-cloudflare/middleware/e2e/playwright.dev.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { configurePlaywright } from "../../common/config-e2e"; - -export default configurePlaywright("middleware", { - isWorker: false, -}); diff --git a/examples-cloudflare/middleware/middleware.ts b/examples-cloudflare/middleware/middleware.ts deleted file mode 100644 index 843d8926..00000000 --- a/examples-cloudflare/middleware/middleware.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { clerkMiddleware } from "@clerk/nextjs/server"; -import { getCloudflareContext } from "@opennextjs/cloudflare"; -import { NextRequest, NextResponse, NextFetchEvent } from "next/server"; - -export function middleware(request: NextRequest, event: NextFetchEvent) { - console.log("middleware"); - if (request.nextUrl.pathname === "/about") { - return NextResponse.redirect(new URL("/redirected", request.url)); - } - if (request.nextUrl.pathname === "/another") { - return NextResponse.rewrite(new URL("/rewrite", request.url)); - } - if (request.nextUrl.pathname === "/clerk") { - return clerkMiddleware(async () => {}, { - publishableKey: "pk_test_ZXhhbXBsZS5hY2NvdW50cy5kZXYk", - secretKey: "skey", - })(request, event); - } - - const requestHeaders = new Headers(request.headers); - const cloudflareContext = getCloudflareContext(); - - requestHeaders.set( - "x-cloudflare-context", - `typeof \`cloudflareContext.env\` = ${typeof cloudflareContext.env}` - ); - - return NextResponse.next({ - request: { - headers: requestHeaders, - }, - }); -} - -export const config = { - matcher: ["/about/:path*", "/another/:path*", "/middleware/:path*", "/clerk"], -}; diff --git a/examples-cloudflare/middleware/next.config.mjs b/examples-cloudflare/middleware/next.config.mjs deleted file mode 100644 index 2bd0079f..00000000 --- a/examples-cloudflare/middleware/next.config.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; - -initOpenNextCloudflareForDev(); - -/** @type {import('next').NextConfig} */ -const nextConfig = { - typescript: { ignoreBuildErrors: true }, - eslint: { ignoreDuringBuilds: true }, -}; - -export default nextConfig; diff --git a/examples-cloudflare/middleware/open-next.config.ts b/examples-cloudflare/middleware/open-next.config.ts deleted file mode 100644 index ffd98878..00000000 --- a/examples-cloudflare/middleware/open-next.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineCloudflareConfig } from "@opennextjs/cloudflare"; - -export default defineCloudflareConfig(); diff --git a/examples-cloudflare/middleware/package.json b/examples-cloudflare/middleware/package.json deleted file mode 100644 index 66dde54c..00000000 --- a/examples-cloudflare/middleware/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "examples-cloudflare/middleware", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "build:worker": "pnpm opennextjs-cloudflare build", - "preview:worker": "pnpm opennextjs-cloudflare preview", - "preview": "pnpm build:worker && pnpm preview:worker", - "e2e": "playwright test -c e2e/playwright.config.ts", - "e2e:dev": "playwright test -c e2e/playwright.dev.config.ts" - }, - "dependencies": { - "@clerk/nextjs": "^6.21.0", - "next": "catalog:", - "react": "catalog:", - "react-dom": "catalog:" - }, - "devDependencies": { - "@opennextjs/cloudflare": "workspace:*", - "@playwright/test": "catalog:", - "@types/node": "catalog:", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", - "typescript": "catalog:", - "wrangler": "catalog:" - } -} diff --git a/examples-cloudflare/middleware/public/favicon.ico b/examples-cloudflare/middleware/public/favicon.ico deleted file mode 100755 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/examples-cloudflare/middleware/public/vercel.svg b/examples-cloudflare/middleware/public/vercel.svg deleted file mode 100755 index fbf0e25a..00000000 --- a/examples-cloudflare/middleware/public/vercel.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/examples-cloudflare/middleware/tsconfig.json b/examples-cloudflare/middleware/tsconfig.json deleted file mode 100755 index 32f841ea..00000000 --- a/examples-cloudflare/middleware/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ] - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules", "open-next.config.ts", "worker.ts"] -} diff --git a/examples-cloudflare/middleware/wrangler.jsonc b/examples-cloudflare/middleware/wrangler.jsonc deleted file mode 100644 index 92ac1f42..00000000 --- a/examples-cloudflare/middleware/wrangler.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "main": ".open-next/worker.js", - "name": "middleware", - "compatibility_date": "2024-12-30", - "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], - "assets": { - "directory": ".open-next/assets", - "binding": "ASSETS" - }, - "vars": { - "MY_VAR": "my-var" - }, - "kv_namespaces": [{ "binding": "MY_KV", "id": "" }] -} diff --git a/examples-cloudflare/next-partial-prerendering/.gitignore b/examples-cloudflare/next-partial-prerendering/.gitignore deleted file mode 100755 index 05bb836c..00000000 --- a/examples-cloudflare/next-partial-prerendering/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -/.yarn - -# testing -/coverage -playwright-report -test-results - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env* -!.env*.example - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/examples-cloudflare/next-partial-prerendering/.prettierrc b/examples-cloudflare/next-partial-prerendering/.prettierrc deleted file mode 100644 index 62532247..00000000 --- a/examples-cloudflare/next-partial-prerendering/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "singleQuote": true -} diff --git a/examples-cloudflare/next-partial-prerendering/README.md b/examples-cloudflare/next-partial-prerendering/README.md deleted file mode 100755 index 967c9811..00000000 --- a/examples-cloudflare/next-partial-prerendering/README.md +++ /dev/null @@ -1,23 +0,0 @@ -## Next.js Partial Prerendering - -This is a demo of [Next.js](https://nextjs.org) using [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering). - -This template uses the new Next.js [App Router](https://nextjs.org/docs/app). This includes support for enhanced layouts, colocation of components, tests, and styles, component-level data fetching, and more. - -It also uses the experimental Partial Prerendering feature available in Next.js 14. Partial Prerendering combines ultra-quick static edge delivery with fully dynamic capabilities and we believe it has the potential to [become the default rendering model for web applications](https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model), bringing together the best of static site generation and dynamic delivery. - -> ⚠️ Please note that PPR is an experimental technology that is not recommended for production. You may run into some DX issues, especially on larger code bases. - -## How it works - -The index route `/` uses Partial Prerendering through: - -1. Enabling the experimental flag in `next.config.js`. - -```js -experimental: { - ppr: true, -}, -``` - -2. Using `` to wrap Dynamic content. diff --git a/examples-cloudflare/next-partial-prerendering/app/favicon.ico b/examples-cloudflare/next-partial-prerendering/app/favicon.ico deleted file mode 100644 index af98450595e8b8efd9e505cddc5ed705b665a4d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmdU$XN*0PW5y#)gXL_h{3FkJ#X`oXpI?bB5#G)w~S7Qa+`W<1AB#X3C=?&UhvAO=|S+ zQpCR%R9`2h-z)7_x}fw(>6KDpfVd}*GI-!c2H9UDrKwS>=#NU@Ddj7@Q4*g7FEYrY z16}AW?v$zq+9+i!-B+T1{bG*9hmHqIUn`*-8^w`osM0?vVJi}rWb@IzKnYt#l~PpE zZq^t6O{HGFdQ!P^WzlxPHWGIHcd>D{}x={sTUg9&WW6602m zQnBH6>!yx}4jn2B7A%n2vuDesNt2{;dd zh7B@$^l0CFc@ZrPKrArsz@`@AQzbS#)CVarSbb!I8ty{PH zk}X}jv}9#v$)iV)Gb-#m>4qUO6)cT2PV3 zpI*Is$&Ww&XxZfC$&*s6RxM+zLWK&_s8J(BjQt6=fsGRsIez?@ix)2jvEROZTiUm8 zAGA}oYE|jjv7_)^<8Q|B8-jA4x_+h43HdwG`akQZ{rmR^u|IqEOoj~`7J{KpojS(< z>D#xjnZIoEL8L15HMT(z#WR80C=8eJ6m1l4LW@cu}s8OTj^Upsw@4?*W6SJES z?6iS9&)0pV`lp_$Z=3e*+h^9ZUK>rCG?5uIW|;4L`0(N0^@=YH0qjNg{kCh@PHx@0 zWueQ33l}2#^Q~LAHa;_LgYTK1p6(a3o6pAd+gv_56 zTeSG07MmF9|6)G^Q&#}`NMn_Um~~0isUI6)0h8ZU;`^Lf>f{vqND)`@P9?$9TK1fc;tCyQ6!+t!2dtUtke$_><;tw&q0@ECem?{wl!rS3|>b6OetkXT}@W?k_+rGrYh zl>Vi}m;6*0c#P>*l+@N$%>EBBFmQe{Fc#%OC9f_nmD)f~S=dohhj8vYl zv6`=x&=pVWsLX#+q8mig9W$TUxvt*}bzk7L-lNk`>Y+?Gm163T#zTyXvm=~)sb0N0 zaaweuJ6Zx`(zaEHG-%L3KK=Al6T>DZPhLQGq zy?Yqg@b-sP&7-d+WX~-}jDPy{>9TCuG9i8(A^wLgY}(XWZ-_f3WuJ2&?4j-5yH}Ph zSrTpU9$VNX@qMmWifEgC6yB+S|Ng&-_q*kY6P-PKR*oJ$D&4wui>NOkgH3F=P`Ve$ z3oFArk%&op&JP9oiCbR3eqHX}yJupw-hGKM`tJs8Gp0rz*J9ak*s!6TKY!j}B<@Qe z2xHHihfO=4d&@$equD1uh)vR!D_5jhvt}W(F>o+n#QfeP*=Ky4KYza26Uxue7xt`( zwGs1UFEoa2%CXJd6_q!V{lS9=%abQh(nAuU?8F#8U%^agA*f5vUU zO`A5Rzftd-H*XGp=j=}sgJvHomR>El^LJEU_xn@7oB=$3{5YoN^y$-PoVT`!UrwAj z(d>cRyeL=eJNUAXP0n?!TD8j9&dbY_&Ye4l$oA~n)9gP*+v9QJq3wP5KkQGLGDTj# zd};PI#*Q5uVjI0fh76HSn>LxVdoDY_;0u&tn{OaexyiTnfB*h{;fw|QLCcpf4;xpW zFkyme8_o^J(yQeMlzfjrrqcE2$dM!Fd_g$&SWBkEa^gaG!|Jt={ z=KFH#39++i(IT_Yx^(H%ko++GcKr1<|Ih~0rcI04uj!BQ`Z)V#=O2Y;=E9`s-!ShO zsSe!pFZPSf|L}>y>wkcu$o#Ju7{d8q*e4a4|Eapd)ji*PB>xlpq$2UZRgc#`wqyC< z*e8K8>Gg-|00V0nbj5maWeG|22l`3AlJ{NwJ|l|`ufMHXz7k^tYsg%g=)+?N8;M_9LtdS2KI3UcQZk+)cWFG|X4L^R?nKNgM zJp7#1#y{fx7Zcyn1ijH<*t+zGuIF{p8Z6OUCbF zJuqm{AfqFUdUSJ))2_z+s>OeFWelDY)Ji4$N2uccI^tDkzj4ezJD0s z6WNgVqu%*n4eJGq6%4dL{ - - - - - -
-
-
-
- -
-
- - {children} -
-
-
-
-
-
- - - ); -} diff --git a/examples-cloudflare/next-partial-prerendering/app/not-found.tsx b/examples-cloudflare/next-partial-prerendering/app/not-found.tsx deleted file mode 100644 index caea3a91..00000000 --- a/examples-cloudflare/next-partial-prerendering/app/not-found.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function NotFound() { - return ( -
-

Not Found

-

Could not find requested resource

-
- ); -} diff --git a/examples-cloudflare/next-partial-prerendering/app/opengraph-image.png b/examples-cloudflare/next-partial-prerendering/app/opengraph-image.png deleted file mode 100644 index 44fd1ebe27723609a7ea29a966d80f1596ae77c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98894 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�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 - - - - - }> - - - - - - }> - - - - ); -} diff --git a/examples-cloudflare/next-partial-prerendering/app/styles.tsx b/examples-cloudflare/next-partial-prerendering/app/styles.tsx deleted file mode 100644 index 4427b397..00000000 --- a/examples-cloudflare/next-partial-prerendering/app/styles.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export function GlobalStyles() { - return ( -