├── .changeset
├── README.md
├── aggregate.mjs
├── config.json
├── release
└── version
├── .git-blame-ignore-revs
├── .github
├── actions
│ ├── lint
│ │ └── action.yml
│ ├── pnpm-setup
│ │ └── action.yml
│ └── test
│ │ └── action.yml
└── workflows
│ ├── check.yml
│ ├── docs.yml
│ ├── e2e.yml
│ ├── pre-release.yml
│ ├── release.yml
│ └── v2-release.yml
├── .gitignore
├── .npmrc
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── CNAME
├── LICENSE
├── README.md
├── biome.jsonc
├── docs
├── 404.html
├── index.html
└── public
│ ├── logo-dark.svg
│ └── logo-light.svg
├── examples
├── app-pages-router
│ ├── .env
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── app
│ │ ├── albums
│ │ │ ├── @modal
│ │ │ │ ├── (.)[album]
│ │ │ │ │ ├── [song]
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ └── default.tsx
│ │ │ ├── [album]
│ │ │ │ ├── [song]
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── api
│ │ │ ├── client
│ │ │ │ └── route.ts
│ │ │ ├── host
│ │ │ │ └── route.ts
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── image-optimization
│ │ │ └── page.tsx
│ │ ├── isr
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── parallel
│ │ │ ├── @a
│ │ │ │ ├── a-page
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── @b
│ │ │ │ ├── b-page
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── redirect-destination
│ │ │ └── page.tsx
│ │ ├── rewrite-destination
│ │ │ └── page.tsx
│ │ ├── server-actions
│ │ │ ├── client.tsx
│ │ │ └── page.tsx
│ │ └── ssr
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ ├── middleware.ts
│ ├── next.config.ts
│ ├── open-next.config.ts
│ ├── package.json
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── api
│ │ │ └── hello.ts
│ │ ├── pages_isr
│ │ │ └── index.tsx
│ │ └── pages_ssr
│ │ │ └── index.tsx
│ ├── postcss.config.js
│ ├── public
│ │ ├── favicon.ico
│ │ └── static
│ │ │ ├── corporate_holiday_card.jpg
│ │ │ └── frank.webp
│ ├── sst-env.d.ts
│ ├── styles
│ │ └── globals.css
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── app-router
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── app
│ │ ├── albums
│ │ │ ├── @modal
│ │ │ │ ├── (.)[album]
│ │ │ │ │ ├── [song]
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ └── default.tsx
│ │ │ ├── [album]
│ │ │ │ ├── [song]
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── api
│ │ │ ├── after
│ │ │ │ ├── revalidate
│ │ │ │ │ └── route.ts
│ │ │ │ └── ssg
│ │ │ │ │ └── route.ts
│ │ │ ├── client
│ │ │ │ └── route.ts
│ │ │ ├── host
│ │ │ │ └── route.ts
│ │ │ ├── isr
│ │ │ │ └── route.ts
│ │ │ ├── og
│ │ │ │ └── route.tsx
│ │ │ ├── page.tsx
│ │ │ ├── revalidate-path
│ │ │ │ └── route.ts
│ │ │ ├── revalidate-tag
│ │ │ │ └── route.ts
│ │ │ └── sse
│ │ │ │ └── route.ts
│ │ ├── config-redirect
│ │ │ ├── dest
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── cookies
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── headers
│ │ │ └── page.tsx
│ │ ├── image-optimization
│ │ │ └── page.tsx
│ │ ├── isr-data-cache
│ │ │ └── page.tsx
│ │ ├── isr
│ │ │ ├── dynamic-params-false
│ │ │ │ └── [id]
│ │ │ │ │ └── page.tsx
│ │ │ ├── dynamic-params-true
│ │ │ │ └── [id]
│ │ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── methods
│ │ │ ├── get
│ │ │ │ ├── dynamic-segments
│ │ │ │ │ └── [slug]
│ │ │ │ │ │ └── route.ts
│ │ │ │ ├── query
│ │ │ │ │ └── route.ts
│ │ │ │ ├── redirect
│ │ │ │ │ └── route.ts
│ │ │ │ ├── revalidate
│ │ │ │ │ └── route.ts
│ │ │ │ └── static
│ │ │ │ │ └── route.ts
│ │ │ ├── post
│ │ │ │ ├── cookies
│ │ │ │ │ └── route.ts
│ │ │ │ └── formdata
│ │ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ ├── og
│ │ │ ├── opengraph-image.tsx
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── parallel
│ │ │ ├── @a
│ │ │ │ ├── a-page
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── @b
│ │ │ │ ├── b-page
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── redirect-destination
│ │ │ └── page.tsx
│ │ ├── revalidate-path
│ │ │ └── page.tsx
│ │ ├── revalidate-tag
│ │ │ ├── layout.tsx
│ │ │ ├── nested
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── rewrite-destination
│ │ │ └── page.tsx
│ │ ├── search-query
│ │ │ └── page.tsx
│ │ ├── server-actions
│ │ │ ├── client.tsx
│ │ │ └── page.tsx
│ │ ├── sse
│ │ │ └── page.tsx
│ │ └── ssr
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ ├── middleware.ts
│ ├── next.config.ts
│ ├── open-next.config.ts
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ ├── favicon.ico
│ │ └── static
│ │ │ ├── corporate_holiday_card.jpg
│ │ │ └── frank.webp
│ ├── sst-env.d.ts
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── experimental
│ ├── .gitignore
│ ├── README.md
│ ├── next.config.ts
│ ├── open-next.config.ts
│ ├── package.json
│ ├── src
│ │ ├── app
│ │ │ ├── api
│ │ │ │ └── revalidate
│ │ │ │ │ └── route.ts
│ │ │ ├── favicon.ico
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── page.module.css
│ │ │ ├── page.tsx
│ │ │ ├── ppr
│ │ │ │ └── page.tsx
│ │ │ └── use-cache
│ │ │ │ ├── isr
│ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── ssr
│ │ │ │ └── page.tsx
│ │ ├── components
│ │ │ ├── cached.tsx
│ │ │ ├── dynamic.tsx
│ │ │ └── static.tsx
│ │ └── middleware.ts
│ └── tsconfig.json
├── pages-router
│ ├── .env.production
│ ├── .gitignore
│ ├── README.md
│ ├── next.config.ts
│ ├── open-next.config.ts
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ ├── favicon.ico
│ │ └── static
│ │ │ └── frank.webp
│ ├── src
│ │ ├── components
│ │ │ └── home.tsx
│ │ ├── middleware.ts
│ │ ├── pages
│ │ │ ├── [[...page]].tsx
│ │ │ ├── _app.tsx
│ │ │ ├── _document.tsx
│ │ │ ├── amp
│ │ │ │ └── index.tsx
│ │ │ ├── api
│ │ │ │ ├── dynamic
│ │ │ │ │ ├── [slug].ts
│ │ │ │ │ ├── catch-all-optional
│ │ │ │ │ │ └── [[...slug]].ts
│ │ │ │ │ ├── catch-all
│ │ │ │ │ │ └── [...slug].ts
│ │ │ │ │ └── precedence
│ │ │ │ │ │ └── index.ts
│ │ │ │ ├── hello.ts
│ │ │ │ ├── query.ts
│ │ │ │ └── streaming
│ │ │ │ │ └── index.ts
│ │ │ ├── fallback-intercepted
│ │ │ │ ├── [...slugs].tsx
│ │ │ │ ├── [slug].tsx
│ │ │ │ ├── ssg.tsx
│ │ │ │ └── static.tsx
│ │ │ ├── fallback
│ │ │ │ └── [slug].tsx
│ │ │ ├── head
│ │ │ │ └── index.tsx
│ │ │ ├── isr
│ │ │ │ └── index.tsx
│ │ │ ├── sse
│ │ │ │ └── index.tsx
│ │ │ └── ssr
│ │ │ │ └── index.tsx
│ │ └── styles
│ │ │ └── globals.css
│ ├── sst-env.d.ts
│ └── tsconfig.json
├── shared
│ ├── api
│ │ ├── index.ts
│ │ └── songs.json
│ ├── components
│ │ ├── Album
│ │ │ ├── Album.tsx
│ │ │ ├── Song.tsx
│ │ │ └── index.tsx
│ │ ├── Filler
│ │ │ └── index.tsx
│ │ ├── Modal
│ │ │ └── index.tsx
│ │ └── Nav
│ │ │ └── index.tsx
│ ├── package.json
│ ├── sst-env.d.ts
│ └── tsconfig.json
└── sst
│ ├── README.md
│ ├── package.json
│ ├── sst.config.ts
│ └── stacks
│ ├── AppPagesRouter.ts
│ ├── AppRouter.ts
│ ├── Experimental.ts
│ ├── OpenNextReferenceImplementation.ts
│ └── PagesRouter.ts
├── package.json
├── packages
├── open-next
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── adapters
│ │ │ ├── cache.ts
│ │ │ ├── composable-cache.ts
│ │ │ ├── config
│ │ │ │ ├── index.ts
│ │ │ │ └── util.ts
│ │ │ ├── dynamo-provider.ts
│ │ │ ├── edge-adapter.ts
│ │ │ ├── image-optimization-adapter.ts
│ │ │ ├── logger.ts
│ │ │ ├── middleware.ts
│ │ │ ├── plugins
│ │ │ │ ├── 14.1
│ │ │ │ │ └── util.ts
│ │ │ │ ├── README.md
│ │ │ │ └── image-optimization
│ │ │ │ │ ├── image-optimization.replacement.ts
│ │ │ │ │ └── image-optimization.ts
│ │ │ ├── revalidate.ts
│ │ │ ├── server-adapter.ts
│ │ │ ├── util.ts
│ │ │ └── warmer-function.ts
│ │ ├── build.ts
│ │ ├── build
│ │ │ ├── buildNextApp.ts
│ │ │ ├── bundleNextServer.ts
│ │ │ ├── compileCache.ts
│ │ │ ├── compileConfig.ts
│ │ │ ├── compileTagCacheProvider.ts
│ │ │ ├── constant.ts
│ │ │ ├── copyTracedFiles.ts
│ │ │ ├── createAssets.ts
│ │ │ ├── createImageOptimizationBundle.ts
│ │ │ ├── createMiddleware.ts
│ │ │ ├── createRevalidationBundle.ts
│ │ │ ├── createServerBundle.ts
│ │ │ ├── createWarmerBundle.ts
│ │ │ ├── edge
│ │ │ │ └── createEdgeBundle.ts
│ │ │ ├── generateOutput.ts
│ │ │ ├── helper.ts
│ │ │ ├── installDeps.ts
│ │ │ ├── middleware
│ │ │ │ └── buildNodeMiddleware.ts
│ │ │ ├── patch
│ │ │ │ ├── astCodePatcher.ts
│ │ │ │ ├── codePatcher.ts
│ │ │ │ ├── patchedAsyncStorage.ts
│ │ │ │ └── patches
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── patchBackgroundRevalidation.ts
│ │ │ │ │ ├── patchEnvVar.ts
│ │ │ │ │ ├── patchFetchCacheISR.ts
│ │ │ │ │ ├── patchFetchCacheWaitUntil.ts
│ │ │ │ │ └── patchNextServer.ts
│ │ │ ├── utils.ts
│ │ │ └── validateConfig.ts
│ │ ├── core
│ │ │ ├── createGenericHandler.ts
│ │ │ ├── createMainHandler.ts
│ │ │ ├── edgeFunctionHandler.ts
│ │ │ ├── nodeMiddlewareHandler.ts
│ │ │ ├── patchAsyncStorage.ts
│ │ │ ├── requestHandler.ts
│ │ │ ├── require-hooks.ts
│ │ │ ├── resolve.ts
│ │ │ ├── routing
│ │ │ │ ├── cacheInterceptor.ts
│ │ │ │ ├── i18n
│ │ │ │ │ ├── accept-header.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── matcher.ts
│ │ │ │ ├── middleware.ts
│ │ │ │ ├── queue.ts
│ │ │ │ ├── routeMatcher.ts
│ │ │ │ └── util.ts
│ │ │ ├── routingHandler.ts
│ │ │ └── util.ts
│ │ ├── helpers
│ │ │ ├── withCloudflare.ts
│ │ │ └── withSST.ts
│ │ ├── http
│ │ │ ├── index.ts
│ │ │ ├── openNextResponse.ts
│ │ │ ├── request.ts
│ │ │ └── util.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ ├── minimize-js.ts
│ │ ├── overrides
│ │ │ ├── cdnInvalidation
│ │ │ │ ├── cloudfront.ts
│ │ │ │ └── dummy.ts
│ │ │ ├── converters
│ │ │ │ ├── aws-apigw-v1.ts
│ │ │ │ ├── aws-apigw-v2.ts
│ │ │ │ ├── aws-cloudfront.ts
│ │ │ │ ├── dummy.ts
│ │ │ │ ├── edge.ts
│ │ │ │ ├── node.ts
│ │ │ │ ├── sqs-revalidate.ts
│ │ │ │ └── utils.ts
│ │ │ ├── imageLoader
│ │ │ │ ├── dummy.ts
│ │ │ │ ├── fs-dev.ts
│ │ │ │ ├── host.ts
│ │ │ │ ├── s3-lite.ts
│ │ │ │ └── s3.ts
│ │ │ ├── incrementalCache
│ │ │ │ ├── dummy.ts
│ │ │ │ ├── fs-dev.ts
│ │ │ │ ├── multi-tier-ddb-s3.ts
│ │ │ │ ├── s3-lite.ts
│ │ │ │ └── s3.ts
│ │ │ ├── originResolver
│ │ │ │ ├── dummy.ts
│ │ │ │ └── pattern-env.ts
│ │ │ ├── proxyExternalRequest
│ │ │ │ ├── dummy.ts
│ │ │ │ ├── fetch.ts
│ │ │ │ └── node.ts
│ │ │ ├── queue
│ │ │ │ ├── direct.ts
│ │ │ │ ├── dummy.ts
│ │ │ │ ├── sqs-lite.ts
│ │ │ │ └── sqs.ts
│ │ │ ├── tagCache
│ │ │ │ ├── constants.ts
│ │ │ │ ├── dummy.ts
│ │ │ │ ├── dynamodb-lite.ts
│ │ │ │ ├── dynamodb-nextMode.ts
│ │ │ │ ├── dynamodb.ts
│ │ │ │ └── fs-dev.ts
│ │ │ ├── warmer
│ │ │ │ ├── aws-lambda.ts
│ │ │ │ └── dummy.ts
│ │ │ └── wrappers
│ │ │ │ ├── aws-lambda-compressed.ts
│ │ │ │ ├── aws-lambda-streaming.ts
│ │ │ │ ├── aws-lambda.ts
│ │ │ │ ├── cloudflare-edge.ts
│ │ │ │ ├── cloudflare-node.ts
│ │ │ │ ├── dummy.ts
│ │ │ │ ├── express-dev.ts
│ │ │ │ └── node.ts
│ │ ├── plugins
│ │ │ ├── content-updater.ts
│ │ │ ├── edge.ts
│ │ │ ├── externalMiddleware.ts
│ │ │ ├── replacement.ts
│ │ │ └── resolve.ts
│ │ ├── types
│ │ │ ├── aws-lambda.ts
│ │ │ ├── cache.ts
│ │ │ ├── global.ts
│ │ │ ├── next-types.ts
│ │ │ ├── open-next.ts
│ │ │ └── overrides.ts
│ │ └── utils
│ │ │ ├── binary.ts
│ │ │ ├── cache.ts
│ │ │ ├── error.ts
│ │ │ ├── fetch.ts
│ │ │ ├── lru.ts
│ │ │ ├── normalize-path.ts
│ │ │ ├── promise.ts
│ │ │ ├── regex.ts
│ │ │ └── stream.ts
│ └── tsconfig.json
├── tests-e2e
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── playwright.config.js
│ ├── tests
│ │ ├── appPagesRouter
│ │ │ ├── api.test.ts
│ │ │ ├── host.test.ts
│ │ │ ├── image-optimization.test.ts
│ │ │ ├── isr.test.ts
│ │ │ ├── middleware.redirect.test.ts
│ │ │ ├── middleware.rewrite.test.ts
│ │ │ ├── middleware.test.ts
│ │ │ ├── modals.test.ts
│ │ │ ├── pages_isr.test.ts
│ │ │ ├── pages_ssr.test.ts
│ │ │ ├── parallel.test.ts
│ │ │ ├── serverActions.test.ts
│ │ │ ├── skip_trailing.test.ts
│ │ │ └── ssr.test.ts
│ │ ├── appRouter
│ │ │ ├── after.test.ts
│ │ │ ├── api.test.ts
│ │ │ ├── config.redirect.test.ts
│ │ │ ├── headers.test.ts
│ │ │ ├── host.test.ts
│ │ │ ├── image-optimization.test.ts
│ │ │ ├── isr.revalidate.test.ts
│ │ │ ├── isr.test.ts
│ │ │ ├── methods.test.ts
│ │ │ ├── middleware.cookies.test.ts
│ │ │ ├── middleware.redirect.test.ts
│ │ │ ├── middleware.rewrite.test.ts
│ │ │ ├── modals.test.ts
│ │ │ ├── og.test.ts
│ │ │ ├── parallel.test.ts
│ │ │ ├── query.test.ts
│ │ │ ├── revalidateTag.test.ts
│ │ │ ├── serverActions.test.ts
│ │ │ ├── sse.test.ts
│ │ │ ├── ssr.test.ts
│ │ │ └── trailing.test.ts
│ │ ├── experimental
│ │ │ ├── nodeMiddleware.test.ts
│ │ │ ├── ppr.test.ts
│ │ │ └── use-cache.test.ts
│ │ ├── pagesRouter
│ │ │ ├── 404.test.ts
│ │ │ ├── amp.test.ts
│ │ │ ├── api.test.ts
│ │ │ ├── catch-all-optional.test.ts
│ │ │ ├── data.test.ts
│ │ │ ├── fallback.test.ts
│ │ │ ├── head.test.ts
│ │ │ ├── header.test.ts
│ │ │ ├── i18n.test.ts
│ │ │ ├── isr.test.ts
│ │ │ ├── middleware.test.ts
│ │ │ ├── redirect.test.ts
│ │ │ ├── rewrite.test.ts
│ │ │ ├── ssr.test.ts
│ │ │ ├── streaming.test.ts
│ │ │ └── trailing.test.ts
│ │ └── utils.ts
│ └── tsconfig.json
├── tests-unit
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── setup.ts
│ ├── tests
│ │ ├── adapters
│ │ │ ├── cache.test.ts
│ │ │ └── logger.test.ts
│ │ ├── binary.test.ts
│ │ ├── build
│ │ │ ├── helper.test.ts
│ │ │ └── patch
│ │ │ │ ├── codePatcher.test.ts
│ │ │ │ └── patches
│ │ │ │ ├── patchBackgroundRevalidation.test.ts
│ │ │ │ ├── patchEnvVars.test.ts
│ │ │ │ ├── patchFetchCacheISR.test.ts
│ │ │ │ ├── patchFetchCacheWaitUntil.test.ts
│ │ │ │ └── patchNextServer.test.ts
│ │ ├── converters
│ │ │ ├── aws-apigw-v1.test.ts
│ │ │ ├── aws-apigw-v2.test.ts
│ │ │ ├── aws-cloudfront.test.ts
│ │ │ ├── node.test.ts
│ │ │ └── utils.test.ts
│ │ ├── core
│ │ │ └── routing
│ │ │ │ ├── cacheInterceptor.test.ts
│ │ │ │ ├── i18n.test.ts
│ │ │ │ ├── matcher.test.ts
│ │ │ │ ├── middleware.test.ts
│ │ │ │ ├── routeMatcher.test.ts
│ │ │ │ └── util.test.ts
│ │ ├── http
│ │ │ └── utils.test.ts
│ │ ├── overrides
│ │ │ └── proxyExternalRequest
│ │ │ │ └── fetch.test.ts
│ │ └── utils
│ │ │ └── regex.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
└── utils
│ ├── package.json
│ ├── src
│ ├── index.ts
│ └── logger.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | { "repo": "opennextjs/opennextjs-aws" }
6 | ],
7 | "commit": false,
8 | "fixed": [["@opennextjs/aws"]],
9 | "linked": [],
10 | "access": "public",
11 | "baseBranch": "main",
12 | "updateInternalDependencies": "patch",
13 | "ignore": []
14 | }
15 |
--------------------------------------------------------------------------------
/.changeset/release:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | pnpm build
5 | #cp packages/open-next/package.json packages/open-next/dist/package.json
6 | cp README.md packages/open-next/README.md
7 | #sed -i.bak -e '2,5d' packages/open-next/dist/package.json
8 | pnpm changeset publish
--------------------------------------------------------------------------------
/.changeset/version:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | pnpm changeset version
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | b36196d4ff17934ed71b0926effa20a5a6c1433d
2 |
--------------------------------------------------------------------------------
/.github/actions/lint/action.yml:
--------------------------------------------------------------------------------
1 | name: Lint codebase
2 | description: Run biome to lint codebase and ensure code quality
3 |
4 | runs:
5 | using: "composite"
6 | steps:
7 | - name: Install dependencies
8 | run: pnpm install
9 | shell: bash
10 |
11 | - name: Setup Biome CLI
12 | uses: biomejs/setup-biome@v2.2.1
13 |
14 | - name: Run biome
15 | run: biome ci --reporter=github
16 | shell: bash
17 |
--------------------------------------------------------------------------------
/.github/actions/pnpm-setup/action.yml:
--------------------------------------------------------------------------------
1 | name: Install & setup pnpm
2 | description: Install's node, pnpm, restores cache
3 |
4 | runs:
5 | using: 'composite'
6 | steps:
7 | # Install nodejs. https://github.com/actions/setup-node
8 | - name: Setup Node.js
9 | uses: actions/setup-node@v4
10 | with:
11 | node-version: 18.x
12 |
13 | # Install pnpm. https://github.com/pnpm/action-setup
14 | - uses: pnpm/action-setup@v4
15 | with:
16 | version: 9
17 | # run_install: false
18 |
19 | # Get pnpm store path so we can cache it
20 | - name: Get pnpm store directory
21 | shell: bash
22 | run: |
23 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
24 |
25 | - uses: actions/cache@v4
26 | name: Setup pnpm cache
27 | with:
28 | path: ${{ env.STORE_PATH }}
29 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
30 | restore-keys: |
31 | ${{ runner.os }}-pnpm-store-
32 |
--------------------------------------------------------------------------------
/.github/actions/test/action.yml:
--------------------------------------------------------------------------------
1 | name: Unit tests
2 | description: Run unit tests and ensure code quality
3 |
4 | runs:
5 | using: 'composite'
6 | steps:
7 | - name: Run tests
8 | run: pnpm test
9 | shell: bash
10 |
11 | - name: Report test coverage
12 | if: always()
13 | uses: davelosert/vitest-coverage-report-action@v2
14 | with:
15 | vite-config-path: ./packages/tests-unit/vitest.config.ts
16 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Validate merge requests
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | validate:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: read
13 | pull-requests: write
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | - uses: ./.github/actions/pnpm-setup
18 | - uses: ./.github/actions/lint
19 | - uses: ./.github/actions/test
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Deploy docs to GitHub Pages
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | defaults:
7 | run:
8 | working-directory: docs
9 |
10 | permissions:
11 | contents: read
12 | pages: write
13 | id-token: write
14 |
15 | concurrency:
16 | group: 'pages'
17 | cancel-in-progress: false
18 |
19 | jobs:
20 | deploy:
21 | environment:
22 | name: github-pages
23 | url: ${{ steps.deployment.outputs.page_url }}
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v4
28 | - name: Setup Pages
29 | uses: actions/configure-pages@v5
30 | - name: Upload artifact
31 | uses: actions/upload-pages-artifact@v3
32 | with:
33 | path: 'docs/'
34 | - name: Deploy to GitHub Pages
35 | id: deployment
36 | uses: actions/deploy-pages@v4
37 |
--------------------------------------------------------------------------------
/.github/workflows/pre-release.yml:
--------------------------------------------------------------------------------
1 | name: Pre-release
2 |
3 | on:
4 | pull_request:
5 | branches: [main, experimental]
6 | paths:
7 | - packages/open-next/**
8 | push:
9 | branches: [main, experimental]
10 | paths:
11 | - packages/open-next/**
12 |
13 | jobs:
14 | release:
15 | name: Pre-release
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout Repo
19 | # https://github.com/actions/checkout
20 | uses: actions/checkout@v4
21 |
22 | # Setup .npmrc file to publish to npm
23 | - uses: actions/setup-node@v4
24 | with:
25 | registry-url: "https://registry.npmjs.org"
26 |
27 | - uses: ./.github/actions/pnpm-setup
28 |
29 | - name: Install dependencies
30 | run: pnpm install
31 |
32 | - name: Build package
33 | run: pnpm -F @opennextjs/aws build
34 |
35 | - name: Publish prerelease
36 | run: pnpm exec pkg-pr-new publish --pnpm --compact './packages/open-next'
37 |
--------------------------------------------------------------------------------
/.github/workflows/v2-release.yml:
--------------------------------------------------------------------------------
1 | name: v2-release
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | release:
7 | name: Pre-release
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout Repo
11 | # https://github.com/actions/checkout
12 | uses: actions/checkout@v4
13 |
14 | # Setup .npmrc file to publish to npm
15 | - uses: actions/setup-node@v4
16 | with:
17 | registry-url: "https://registry.npmjs.org"
18 |
19 | - uses: ./.github/actions/pnpm-setup
20 |
21 | - name: Install dependencies
22 | run: pnpm install
23 |
24 | - name: Publish Pre-release to npm
25 | run: pnpm release-v2
26 | env:
27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | *.log
3 |
4 | # Dependency directories
5 | node_modules/
6 |
7 | # Build output
8 | dist
9 |
10 | # next.js
11 | docs/.next/
12 | docs/out/
13 |
14 | # misc
15 | .DS_Store
16 |
17 | # typescript
18 | next-env.d.ts
19 |
20 | .turbo
21 | # Tests
22 | packages/tests-unit/coverage
23 | test-results
24 |
25 | .sst/
26 | .sst.config*
27 | .next/
28 | .open-next/
29 |
30 | coverage/
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shared-workspace-lockfile = true
2 | auto-install-peers = true
3 | node-linker=hoisted
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["biomejs.biome"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.turbo": true
4 | },
5 | "tailwindCSS.classAttributes": ["class", "className", "tw"],
6 | "[typescript]": {
7 | "editor.defaultFormatter": "biomejs.biome"
8 | },
9 | "[typescriptreact]": {
10 | "editor.defaultFormatter": "biomejs.biome"
11 | },
12 | "[javascript]": {
13 | "editor.defaultFormatter": "biomejs.biome"
14 | },
15 | "[javascriptreact]": {
16 | "editor.defaultFormatter": "biomejs.biome"
17 | },
18 | "[json]": {
19 | "editor.defaultFormatter": "biomejs.biome"
20 | },
21 | "[css]": {
22 | "editor.defaultFormatter": "biomejs.biome"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # open-next
2 |
3 | ## 0.8.1
4 |
5 | ### Patch Changes
6 |
7 | - [`bdd29d1`](https://github.com/serverless-stack/open-next/commit/bdd29d1) Fix spawn error on Windows
8 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | open-next.js.org
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 SST
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | OpenNext docs have moved
8 |
38 |
39 |
40 |
41 |
42 | The docs have moved to its own repository. You can find it here:
43 |
44 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | OpenNext docs have moved
8 |
38 |
39 |
40 |
41 |
42 | The docs have moved to its own repository. You can find it here:
43 |
44 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/examples/app-pages-router/.env:
--------------------------------------------------------------------------------
1 | SOME_ENV_VAR=foo
--------------------------------------------------------------------------------
/examples/app-pages-router/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | .open-next
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/app-pages-router/README.md:
--------------------------------------------------------------------------------
1 | # App Pages Router
2 |
3 | This project uses both the App and Pages router.
--------------------------------------------------------------------------------
/examples/app-pages-router/app/albums/@modal/(.)[album]/[song]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getSong } from "@example/shared/api";
2 | import Modal from "@example/shared/components/Modal";
3 |
4 | type Props = {
5 | params: Promise<{
6 | album: string;
7 | song: string;
8 | }>;
9 | };
10 | export default async function SongPage(props: Props) {
11 | const params = await props.params;
12 | const song = await getSong(params.album, params.song);
13 | return (
14 |
15 | Modal
16 | Album: {decodeURIComponent(params.album)}
17 |
18 | {/* */}
19 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/albums/@modal/(.)[album]/page.tsx:
--------------------------------------------------------------------------------
1 | import Modal from "@example/shared/components/Modal";
2 |
3 | type Props = {
4 | params: Promise<{
5 | artist: string;
6 | }>;
7 | };
8 | export default async function ArtistPage(props: Props) {
9 | const params = await props.params;
10 | return Artists {params.artist};
11 | }
12 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/albums/@modal/default.tsx:
--------------------------------------------------------------------------------
1 | export default function Default() {
2 | return null;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/albums/[album]/[song]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getSong } from "@example/shared/api";
2 |
3 | type Props = {
4 | params: Promise<{
5 | album: string;
6 | song: string;
7 | }>;
8 | };
9 | export default async function Song(props: Props) {
10 | const params = await props.params;
11 | const song = await getSong(params.album, params.song);
12 |
13 | return (
14 |
15 |
Not Modal
16 | {decodeURIComponent(params.album)}
17 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/albums/[album]/page.tsx:
--------------------------------------------------------------------------------
1 | export default function ArtistPage() {
2 | return Artist
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/albums/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | export default function Layout({
4 | children,
5 | modal,
6 | }: {
7 | children: ReactNode;
8 | modal: ReactNode;
9 | }) {
10 | return (
11 |
12 | {children}
13 | {modal}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/albums/page.tsx:
--------------------------------------------------------------------------------
1 | import { getAlbums } from "@example/shared/api";
2 | import Album from "@example/shared/components/Album";
3 |
4 | export default async function AlbumPage() {
5 | const albums = await getAlbums();
6 | return (
7 |
8 | {albums.map((album) => (
9 |
10 | ))}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/api/client/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export async function GET(request: Request) {
4 | return NextResponse.json({
5 | hello: "client",
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/api/host/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export async function GET(request: Request) {
4 | return NextResponse.json({
5 | url: request.url,
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/api/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useState } from "react";
4 |
5 | /**
6 | * Make /api/hello call exclusively on the client
7 | * - we already know SSR can fetch itself w/o issues
8 | */
9 | export default function Page() {
10 | const [data, setData] = useState();
11 |
12 | const onClientClick = useCallback(async () => {
13 | const { protocol, host } = window.location;
14 | const url = `${protocol}//${host}`;
15 | const r = await fetch(`${url}/api/client`);
16 | const d = await r.json();
17 | setData(d);
18 | }, []);
19 |
20 | const onMiddlewareClick = useCallback(async () => {
21 | const { protocol, host } = window.location;
22 | const url = `${protocol}//${host}`;
23 | const r = await fetch(`${url}/api/middleware`);
24 | const d = await r.json();
25 | setData(d);
26 | }, []);
27 |
28 | return (
29 |
30 |
API: {data ? JSON.stringify(data, null, 2) : "N/A"}
31 |
32 |
35 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/image-optimization/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export default function ImageOptimization() {
4 | return (
5 |
6 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/isr/page.tsx:
--------------------------------------------------------------------------------
1 | async function getTime() {
2 | return new Date().toISOString();
3 | }
4 |
5 | export const revalidate = 10;
6 | export default async function ISR() {
7 | const time = getTime();
8 | return Time: {time}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 |
3 | import type { Metadata } from "next";
4 | import { Inter } from "next/font/google";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Nextjs App Router",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }) {
18 | return (
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/parallel/@a/a-page/page.tsx:
--------------------------------------------------------------------------------
1 | export default function APage() {
2 | return A Page
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/parallel/@a/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function A() {
4 | return (
5 |
6 |
Parallel Route A
7 | Go to a-page
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/parallel/@b/b-page/page.tsx:
--------------------------------------------------------------------------------
1 | export default function BPage() {
2 | return B Page
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/parallel/@b/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function B() {
4 | return (
5 |
6 |
Parallel Route B
7 |
8 | Go to b-page
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/parallel/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState } from "react";
3 |
4 | import type { ReactNode } from "react";
5 |
6 | export default function Layout({
7 | a,
8 | b,
9 | children,
10 | }: {
11 | children: ReactNode;
12 | a: ReactNode;
13 | b: ReactNode;
14 | }) {
15 | const [routeA, setRouteA] = useState(false);
16 | const [routeB, setRouteB] = useState(false);
17 |
18 | return (
19 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/parallel/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return null;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/redirect-destination/page.tsx:
--------------------------------------------------------------------------------
1 | export default function RedirectDestination() {
2 | return Redirect Destination
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/rewrite-destination/page.tsx:
--------------------------------------------------------------------------------
1 | export default async function RewriteDestination(props: {
2 | searchParams: Promise<{ a: string; multi?: string[] }>;
3 | }) {
4 | const searchParams = await props.searchParams;
5 | return (
6 |
7 |
Rewritten Destination
8 |
a: {searchParams.a}
9 |
multi: {searchParams.multi?.join(", ")}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/server-actions/client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useCallback, useState, useTransition } from "react";
3 |
4 | import type { Song as SongType } from "@example/shared/api";
5 | import { getSong } from "@example/shared/api";
6 | import Song from "@example/shared/components/Album/Song";
7 |
8 | export default function Client() {
9 | const [isPending, startTransition] = useTransition();
10 | const [song, setSong] = useState();
11 |
12 | const onClick = useCallback(() => {
13 | startTransition(async () => {
14 | const song = await getSong(
15 | "Hold Me In Your Arms",
16 | "I'm never gonna give you up",
17 | );
18 | setSong(song);
19 | });
20 | }, []);
21 |
22 | return (
23 |
24 |
25 | {isPending &&
☎️ing Server Actions...
}
26 | {song &&
}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/server-actions/page.tsx:
--------------------------------------------------------------------------------
1 | import Client from "./client";
2 |
3 | export default function Page() {
4 | return (
5 |
6 |
Server Actions
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/ssr/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 |
3 | export default function Layout({ children }: PropsWithChildren) {
4 | return (
5 |
6 |
SSR
7 | {children}
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/ssr/loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return Loading...
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-pages-router/app/ssr/page.tsx:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers";
2 |
3 | async function getTime() {
4 | const res = await new Promise((resolve) => {
5 | setTimeout(() => {
6 | resolve(new Date().toISOString());
7 | }, 1500);
8 | });
9 | return res;
10 | }
11 |
12 | export default async function SSR() {
13 | const time = await getTime();
14 | const headerList = await headers();
15 | return (
16 |
17 |
Time: {time}
18 |
{headerList.get("host")}
19 |
Env: {process.env.SOME_ENV_VAR}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/app-pages-router/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | poweredByHeader: false,
5 | cleanDistDir: true,
6 | transpilePackages: ["@example/shared"],
7 | output: "standalone",
8 | // outputFileTracingRoot: "../sst",
9 | eslint: {
10 | ignoreDuringBuilds: true,
11 | },
12 | trailingSlash: true,
13 | skipTrailingSlashRedirect: true,
14 | };
15 |
16 | export default nextConfig;
17 |
--------------------------------------------------------------------------------
/examples/app-pages-router/open-next.config.ts:
--------------------------------------------------------------------------------
1 | const config = {
2 | default: {},
3 | functions: {
4 | api: {
5 | routes: ["app/api/client/route", "app/api/host/route", "pages/api/hello"],
6 | patterns: ["/api/*"],
7 | },
8 | },
9 | dangerous: {
10 | enableCacheInterception: true,
11 | },
12 | buildCommand: "npx turbo build",
13 | };
14 |
15 | module.exports = config;
16 |
--------------------------------------------------------------------------------
/examples/app-pages-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app-pages-router",
3 | "version": "0.1.21",
4 | "private": true,
5 | "scripts": {
6 | "openbuild": "node ../../packages/open-next/dist/index.js build --build-command \"npx turbo build\"",
7 | "dev": "next dev --turbopack --port 3003",
8 | "build": "next build",
9 | "start": "next start --port 3003",
10 | "lint": "next lint",
11 | "clean": "rm -rf .turbo node_modules .next .open-next"
12 | },
13 | "dependencies": {
14 | "@example/shared": "workspace:*",
15 | "next": "catalog:",
16 | "@opennextjs/aws": "workspace:*",
17 | "react": "catalog:",
18 | "react-dom": "catalog:"
19 | },
20 | "devDependencies": {
21 | "@types/node": "catalog:",
22 | "@types/react": "catalog:",
23 | "@types/react-dom": "catalog:",
24 | "autoprefixer": "catalog:",
25 | "postcss": "catalog:",
26 | "tailwindcss": "catalog:",
27 | "typescript": "catalog:"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/app-pages-router/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css";
2 |
3 | import type { AppProps } from "next/app";
4 |
5 | export default function App({ Component, pageProps }: AppProps) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app-pages-router/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from "next/document";
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/app-pages-router/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from "next";
3 |
4 | type Data = {
5 | hello: string;
6 | };
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse,
11 | ) {
12 | res.status(200).json({ hello: "world" });
13 | }
14 |
--------------------------------------------------------------------------------
/examples/app-pages-router/pages/pages_isr/index.tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetStaticPropsType } from "next";
2 |
3 | export async function getStaticProps() {
4 | return {
5 | props: {
6 | time: new Date().toISOString(),
7 | },
8 | revalidate: 10,
9 | };
10 | }
11 |
12 | export default function Page({
13 | time,
14 | }: InferGetStaticPropsType) {
15 | return Time: {time}
;
16 | }
17 |
--------------------------------------------------------------------------------
/examples/app-pages-router/pages/pages_ssr/index.tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetServerSidePropsType } from "next";
2 |
3 | export async function getServerSideProps() {
4 | return {
5 | props: {
6 | time: new Date().toISOString(),
7 | },
8 | };
9 | }
10 |
11 | export default function Page({
12 | time,
13 | }: InferGetServerSidePropsType) {
14 | return Time: {time}
;
15 | }
16 |
--------------------------------------------------------------------------------
/examples/app-pages-router/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/examples/app-pages-router/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/app-pages-router/public/favicon.ico
--------------------------------------------------------------------------------
/examples/app-pages-router/public/static/corporate_holiday_card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/app-pages-router/public/static/corporate_holiday_card.jpg
--------------------------------------------------------------------------------
/examples/app-pages-router/public/static/frank.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/app-pages-router/public/static/frank.webp
--------------------------------------------------------------------------------
/examples/app-pages-router/sst-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/app-pages-router/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
6 | "../../examples/shared/**/*.{jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
12 | "gradient-conic":
13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | };
19 | export default config;
20 |
--------------------------------------------------------------------------------
/examples/app-pages-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "NodeNext",
12 | "moduleResolution": "NodeNext",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/examples/app-router/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | .open-next
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/app-router/README.md:
--------------------------------------------------------------------------------
1 | # App Router
2 |
3 | This project uses the App Router exclusively...
--------------------------------------------------------------------------------
/examples/app-router/app/albums/@modal/(.)[album]/[song]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getSong } from "@example/shared/api";
2 | import Modal from "@example/shared/components/Modal";
3 |
4 | type Props = {
5 | params: Promise<{
6 | album: string;
7 | song: string;
8 | }>;
9 | };
10 | export default async function SongPage(props: Props) {
11 | const params = await props.params;
12 | const song = await getSong(params.album, params.song);
13 | return (
14 |
15 | Modal
16 | Album: {decodeURIComponent(params.album)}
17 |
18 | {/* */}
19 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/examples/app-router/app/albums/@modal/(.)[album]/page.tsx:
--------------------------------------------------------------------------------
1 | import Modal from "@example/shared/components/Modal";
2 |
3 | type Props = {
4 | params: Promise<{
5 | artist: string;
6 | }>;
7 | };
8 | export default async function ArtistPage(props: Props) {
9 | const params = await props.params;
10 | return Artists {params.artist};
11 | }
12 |
--------------------------------------------------------------------------------
/examples/app-router/app/albums/@modal/default.tsx:
--------------------------------------------------------------------------------
1 | export default function Default() {
2 | return null;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/albums/[album]/[song]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getSong } from "@example/shared/api";
2 |
3 | type Props = {
4 | params: Promise<{
5 | album: string;
6 | song: string;
7 | }>;
8 | };
9 | export default async function Song(props: Props) {
10 | const params = await props.params;
11 | const song = await getSong(params.album, params.song);
12 |
13 | return (
14 |
15 |
Not Modal
16 | {decodeURIComponent(params.album)}
17 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/examples/app-router/app/albums/[album]/page.tsx:
--------------------------------------------------------------------------------
1 | export default function ArtistPage() {
2 | return Artist
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/albums/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 |
3 | export default function Layout({
4 | children,
5 | modal,
6 | }: {
7 | children: ReactNode;
8 | modal: ReactNode;
9 | }) {
10 | return (
11 |
12 | {children}
13 | {modal}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/app-router/app/albums/page.tsx:
--------------------------------------------------------------------------------
1 | import { getAlbums } from "@example/shared/api";
2 | import Album from "@example/shared/components/Album";
3 |
4 | export default async function AlbumPage() {
5 | const albums = await getAlbums();
6 | return (
7 |
8 | {albums.map((album) => (
9 |
10 | ))}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/after/revalidate/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidateTag } from "next/cache";
2 | import { NextResponse, after } from "next/server";
3 |
4 | export function POST() {
5 | after(
6 | () =>
7 | new Promise((resolve) =>
8 | setTimeout(() => {
9 | revalidateTag("date");
10 | resolve();
11 | }, 5000),
12 | ),
13 | );
14 |
15 | return NextResponse.json({ success: true });
16 | }
17 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/after/ssg/route.ts:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 | import { NextResponse } from "next/server";
3 |
4 | export const dynamic = "force-static";
5 |
6 | export async function GET() {
7 | const dateFn = unstable_cache(() => new Date().toISOString(), ["date"], {
8 | tags: ["date"],
9 | });
10 | const date = await dateFn();
11 | return NextResponse.json({ date });
12 | }
13 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/client/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export async function GET(request: Request) {
4 | return NextResponse.json({
5 | hello: "client",
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/host/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export async function GET(request: Request) {
4 | return NextResponse.json({
5 | url: request.url,
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/isr/route.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs/promises";
2 | import path from "node:path";
3 | import type { NextRequest } from "next/server";
4 | import { NextResponse } from "next/server";
5 |
6 | export const dynamic = "force-dynamic";
7 |
8 | // This endpoint simulates an on demand revalidation request
9 | export async function GET(request: NextRequest) {
10 | const cwd = process.cwd();
11 | const prerenderManifest = await fs.readFile(
12 | path.join(cwd, ".next/prerender-manifest.json"),
13 | "utf-8",
14 | );
15 | const manifest = JSON.parse(prerenderManifest);
16 | const previewId = manifest.preview.previewModeId;
17 |
18 | const result = await fetch(`https://${request.headers.get("host")}/isr`, {
19 | headers: { "x-prerender-revalidate": previewId },
20 | method: "HEAD",
21 | });
22 |
23 | return NextResponse.json({
24 | status: 200,
25 | body: {
26 | result: result.ok,
27 | cacheControl: result.headers.get("cache-control"),
28 | },
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useState } from "react";
4 |
5 | /**
6 | * Make /api/hello call exclusively on the client
7 | * - we already know SSR can fetch itself w/o issues
8 | */
9 | export default function Page() {
10 | const [data, setData] = useState();
11 |
12 | const onClientClick = useCallback(async () => {
13 | const { protocol, host } = window.location;
14 | const url = `${protocol}//${host}`;
15 | const r = await fetch(`${url}/api/client`);
16 | const d = await r.json();
17 | setData(d);
18 | }, []);
19 |
20 | const onMiddlewareClick = useCallback(async () => {
21 | const { protocol, host } = window.location;
22 | const url = `${protocol}//${host}`;
23 | const r = await fetch(`${url}/api/middleware`);
24 | const d = await r.json();
25 | setData(d);
26 | }, []);
27 |
28 | return (
29 |
30 |
API: {data ? JSON.stringify(data, null, 2) : "N/A"}
31 |
32 |
35 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/revalidate-path/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from "next/cache";
2 |
3 | export const dynamic = "force-dynamic";
4 |
5 | export async function GET() {
6 | revalidatePath("/revalidate-path");
7 |
8 | return new Response("ok");
9 | }
10 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/revalidate-tag/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidateTag } from "next/cache";
2 |
3 | export const dynamic = "force-dynamic";
4 |
5 | export async function GET() {
6 | revalidateTag("revalidate");
7 |
8 | return new Response("ok");
9 | }
10 |
--------------------------------------------------------------------------------
/examples/app-router/app/api/sse/route.ts:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 |
3 | function wait(ms: number) {
4 | return new Promise((resolve) => {
5 | setTimeout(resolve, ms);
6 | });
7 | }
8 |
9 | export const dynamic = "force-dynamic";
10 |
11 | export async function GET(request: NextRequest) {
12 | const resStream = new TransformStream();
13 | const writer = resStream.writable.getWriter();
14 |
15 | const res = new Response(resStream.readable, {
16 | headers: {
17 | "Content-Type": "text/event-stream",
18 | Connection: "keep-alive",
19 | "Cache-Control": "no-cache, no-transform",
20 | },
21 | });
22 |
23 | setTimeout(async () => {
24 | await writer.write(
25 | `data: ${JSON.stringify({
26 | message: "open",
27 | time: new Date().toISOString(),
28 | })}\n\n`,
29 | );
30 | for (let i = 1; i <= 4; i++) {
31 | await wait(2000);
32 | await writer.write(
33 | `data: ${JSON.stringify({
34 | message: `hello:${i}`,
35 | time: new Date().toISOString(),
36 | })}\n\n`,
37 | );
38 | }
39 |
40 | await wait(2000); // Wait for 4 seconds
41 | await writer.write(
42 | `data: ${JSON.stringify({
43 | message: "close",
44 | time: new Date().toISOString(),
45 | })}\n\n`,
46 | );
47 | await wait(5000);
48 | await writer.close();
49 | }, 100);
50 |
51 | return res;
52 | }
53 |
--------------------------------------------------------------------------------
/examples/app-router/app/config-redirect/dest/page.tsx:
--------------------------------------------------------------------------------
1 | export default async function Page({
2 | searchParams,
3 | }: {
4 | searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
5 | }) {
6 | const q = (await searchParams).q;
7 |
8 | return (
9 | <>
10 | q: {q}
11 | >
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/app-router/app/config-redirect/page.tsx:
--------------------------------------------------------------------------------
1 | export default function RedirectDestination() {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/app-router/app/cookies/page.tsx:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 |
3 | export default async function Page() {
4 | const foo = (await cookies()).get("foo")?.value;
5 |
6 | return {foo}
;
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app-router/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
--------------------------------------------------------------------------------
/examples/app-router/app/headers/page.tsx:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers";
2 |
3 | export default async function Headers() {
4 | const middlewareHeader = (await headers()).get("request-header");
5 | return (
6 |
7 |
Headers
8 |
{middlewareHeader}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/examples/app-router/app/image-optimization/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export default function ImageOptimization() {
4 | return (
5 |
6 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/app-router/app/isr-data-cache/page.tsx:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 |
3 | async function getTime() {
4 | return new Date().toISOString();
5 | }
6 |
7 | const cachedTime = unstable_cache(getTime, { revalidate: false });
8 |
9 | export const revalidate = 10;
10 |
11 | export default async function ISR() {
12 | const responseOpenNext = await fetch("https://opennext.js.org", {
13 | cache: "force-cache",
14 | });
15 | const dateInOpenNext = responseOpenNext.headers.get("date");
16 | const cachedTimeValue = await cachedTime();
17 | const time = getTime();
18 | return (
19 |
20 |
Date from from OpenNext
21 |
22 | Date from from OpenNext: {dateInOpenNext}
23 |
24 |
Cached Time
25 |
Cached Time: {cachedTimeValue}
26 |
Time
27 |
Time: {time}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | // https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
2 | export const dynamicParams = false; // or true, to make it try SSR unknown paths
3 |
4 | const POSTS = Array.from({ length: 20 }, (_, i) => ({
5 | id: String(i + 1),
6 | title: `Post ${i + 1}`,
7 | content: `This is post ${i + 1}`,
8 | }));
9 |
10 | async function fakeGetPostsFetch() {
11 | return POSTS.slice(0, 10);
12 | }
13 |
14 | async function fakeGetPostFetch(id: string) {
15 | return POSTS.find((post) => post.id === id);
16 | }
17 |
18 | export async function generateStaticParams() {
19 | const fakePosts = await fakeGetPostsFetch();
20 | return fakePosts.map((post) => ({
21 | id: post.id,
22 | }));
23 | }
24 |
25 | export default async function Page({
26 | params,
27 | }: {
28 | params: Promise<{ id: string }>;
29 | }) {
30 | const { id } = await params;
31 | const post = await fakeGetPostFetch(id);
32 | return (
33 |
34 | {post?.title}
35 | {post?.content}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | // We'll prerender only the params from `generateStaticParams` at build time.
4 | // If a request comes in for a path that hasn't been generated,
5 | // Next.js will server-render the page on-demand.
6 | // https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams
7 | export const dynamicParams = true; // or false, to 404 on unknown paths
8 |
9 | const POSTS = Array.from({ length: 20 }, (_, i) => ({
10 | id: String(i + 1),
11 | title: `Post ${i + 1}`,
12 | content: `This is post ${i + 1}`,
13 | }));
14 |
15 | async function fakeGetPostsFetch() {
16 | return POSTS.slice(0, 10);
17 | }
18 |
19 | async function fakeGetPostFetch(id: string) {
20 | return POSTS.find((post) => post.id === id);
21 | }
22 |
23 | export async function generateStaticParams() {
24 | const fakePosts = await fakeGetPostsFetch();
25 | return fakePosts.map((post) => ({
26 | id: post.id,
27 | }));
28 | }
29 |
30 | export default async function Page({
31 | params,
32 | }: {
33 | params: Promise<{ id: string }>;
34 | }) {
35 | const { id } = await params;
36 | const post = await fakeGetPostFetch(id);
37 | if (Number(id) === 1337) {
38 | throw new Error("This is an error!");
39 | }
40 | if (!post) {
41 | notFound();
42 | }
43 | return (
44 |
45 | {post.title}
46 | {post.content}
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/examples/app-router/app/isr/page.tsx:
--------------------------------------------------------------------------------
1 | async function getTime() {
2 | return new Date().toISOString();
3 | }
4 |
5 | export const revalidate = 10;
6 | export default async function ISR() {
7 | const time = getTime();
8 | return Time: {time}
;
9 | }
10 |
--------------------------------------------------------------------------------
/examples/app-router/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 |
3 | import type { Metadata } from "next";
4 | import { Inter } from "next/font/google";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Nextjs App Router",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }) {
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/examples/app-router/app/methods/get/dynamic-segments/[slug]/route.ts:
--------------------------------------------------------------------------------
1 | export async function GET(
2 | request: Request,
3 | { params }: { params: Promise<{ slug: string }> },
4 | ) {
5 | const { slug } = await params;
6 | return Response.json({ slug });
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app-router/app/methods/get/query/route.ts:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 |
3 | export function GET(request: NextRequest) {
4 | const searchParams = request.nextUrl.searchParams;
5 | const query = searchParams.get("query");
6 | if (query === "OpenNext is awesome!") {
7 | return Response.json({ query });
8 | }
9 | return new Response("Internal Server Error", { status: 500 });
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-router/app/methods/get/redirect/route.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | export async function GET(request: Request) {
4 | redirect("https://nextjs.org/");
5 | }
6 |
--------------------------------------------------------------------------------
/examples/app-router/app/methods/get/revalidate/route.ts:
--------------------------------------------------------------------------------
1 | export const revalidate = 5;
2 |
3 | async function getTime() {
4 | return new Date().toISOString();
5 | }
6 |
7 | export async function GET() {
8 | const time = await getTime();
9 | return Response.json({ time });
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-router/app/methods/get/static/route.ts:
--------------------------------------------------------------------------------
1 | export const dynamic = "force-static";
2 |
3 | async function getTime() {
4 | return new Date().toISOString();
5 | }
6 |
7 | export async function GET() {
8 | const time = await getTime();
9 | return Response.json({ time });
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-router/app/methods/post/cookies/route.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 |
3 | export async function POST(request: Request) {
4 | const formData = await request.formData();
5 | const username = formData.get("username");
6 | const password = formData.get("password");
7 | if (username === "hakuna" && password === "matata") {
8 | (await cookies()).set("auth_session", "SUPER_SECRET_SESSION_ID_1234");
9 | return Response.json(
10 | {
11 | message: "ok",
12 | },
13 | {
14 | status: 202,
15 | },
16 | );
17 | }
18 | return Response.json({ message: "you must login" }, { status: 401 });
19 | }
20 |
--------------------------------------------------------------------------------
/examples/app-router/app/methods/post/formdata/route.ts:
--------------------------------------------------------------------------------
1 | export async function POST(request: Request) {
2 | const formData = await request.formData();
3 | const name = formData.get("name");
4 | const email = formData.get("email");
5 | if (name === "OpenNext [] () %!%$#" && email === "opennext@opennext.com") {
6 | return Response.json(
7 | {
8 | message: "ok",
9 | },
10 | {
11 | status: 202,
12 | },
13 | );
14 | }
15 | return Response.json({ message: "forbidden" }, { status: 403 });
16 | }
17 |
--------------------------------------------------------------------------------
/examples/app-router/app/og/opengraph-image.tsx:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from "next/og";
2 |
3 | // Image metadata
4 | export const alt = "OpenNext";
5 | export const size = {
6 | width: 1200,
7 | height: 630,
8 | };
9 |
10 | export const contentType = "image/png";
11 |
12 | // Image generation
13 | export default async function Image() {
14 | return new ImageResponse(
15 | // ImageResponse JSX element
16 |
27 | OpenNext
28 |
,
29 | // ImageResponse options
30 | {
31 | // For convenience, we can re-use the exported opengraph-image
32 | // size config to also set the ImageResponse's width and height.
33 | ...size,
34 | },
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/examples/app-router/app/og/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return ;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/parallel/@a/a-page/page.tsx:
--------------------------------------------------------------------------------
1 | export default function APage() {
2 | return A Page
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/parallel/@a/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function A() {
4 | return (
5 |
6 |
Parallel Route A
7 | Go to a-page
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-router/app/parallel/@b/b-page/page.tsx:
--------------------------------------------------------------------------------
1 | export default function BPage() {
2 | return B Page
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/parallel/@b/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function B() {
4 | return (
5 |
6 |
Parallel Route B
7 |
8 | Go to b-page
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/examples/app-router/app/parallel/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState } from "react";
3 |
4 | import type { ReactNode } from "react";
5 |
6 | export default function Layout({
7 | a,
8 | b,
9 | children,
10 | }: {
11 | children: ReactNode;
12 | a: ReactNode;
13 | b: ReactNode;
14 | }) {
15 | const [routeA, setRouteA] = useState(false);
16 | const [routeB, setRouteB] = useState(false);
17 |
18 | return (
19 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/examples/app-router/app/parallel/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return null;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/redirect-destination/page.tsx:
--------------------------------------------------------------------------------
1 | export default function RedirectDestination() {
2 | return Redirect Destination
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/revalidate-path/page.tsx:
--------------------------------------------------------------------------------
1 | export default async function Page() {
2 | const responseSST = await fetch("https://sst.dev", {
3 | next: {
4 | tags: ["path"],
5 | },
6 | });
7 | // This one doesn't have a tag
8 | const responseOpenNext = await fetch("https://opennext.js.org");
9 | const reqIdSst = responseSST.headers.get("x-amz-cf-id");
10 | const dateInOpenNext = responseOpenNext.headers.get("date");
11 | return (
12 |
13 |
Request id from SST
14 |
RequestID: {reqIdSst}
15 |
Date from from OpenNext
16 |
Date: {dateInOpenNext}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/examples/app-router/app/revalidate-tag/layout.tsx:
--------------------------------------------------------------------------------
1 | import { unstable_cache } from "next/cache";
2 | import type { ReactNode } from "react";
3 |
4 | export default async function Layout({ children }: { children: ReactNode }) {
5 | const fakeFetch = unstable_cache(
6 | async () => new Date().getTime(),
7 | ["fakeFetch"],
8 | {
9 | tags: ["revalidate"],
10 | },
11 | );
12 | const fetchedDate = await fakeFetch();
13 | return (
14 |
15 |
Fetched time: {new Date(fetchedDate).toISOString()}
16 | {children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/examples/app-router/app/revalidate-tag/nested/page.tsx:
--------------------------------------------------------------------------------
1 | export default async function Nested() {
2 | return Nested
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/revalidate-tag/page.tsx:
--------------------------------------------------------------------------------
1 | async function getTime() {
2 | return new Date().toISOString();
3 | }
4 |
5 | export default async function ISR() {
6 | const time = getTime();
7 | return Time: {time}
;
8 | }
9 |
--------------------------------------------------------------------------------
/examples/app-router/app/rewrite-destination/page.tsx:
--------------------------------------------------------------------------------
1 | export default function RewriteDestination() {
2 | return Rewritten Destination
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/search-query/page.tsx:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers";
2 |
3 | export default async function SearchQuery(props: {
4 | searchParams: Promise>;
5 | }) {
6 | const propsSearchParams = await props.searchParams;
7 | const mwSearchParams = (await headers()).get("search-params");
8 | const multiValueParams = propsSearchParams.multi;
9 | const multiValueArray = Array.isArray(multiValueParams)
10 | ? multiValueParams
11 | : [multiValueParams];
12 | return (
13 | <>
14 | Search Query
15 | Search Params via Props: {propsSearchParams.searchParams}
16 | Search Params via Middleware: {mwSearchParams}
17 | {multiValueParams && (
18 | <>
19 | Multi-value Params (key: multi): {multiValueArray.length}
20 | {multiValueArray.map((value) => (
21 | {value}
22 | ))}
23 | >
24 | )}
25 | >
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/examples/app-router/app/server-actions/client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useCallback, useState, useTransition } from "react";
3 |
4 | import type { Song as SongType } from "@example/shared/api";
5 | import { getSong } from "@example/shared/api";
6 | import Song from "@example/shared/components/Album/Song";
7 |
8 | export default function Client() {
9 | const [isPending, startTransition] = useTransition();
10 | const [song, setSong] = useState();
11 |
12 | const onClick = useCallback(() => {
13 | startTransition(async () => {
14 | const song = await getSong(
15 | "Hold Me In Your Arms",
16 | "I'm never gonna give you up",
17 | );
18 | setSong(song);
19 | });
20 | }, []);
21 |
22 | return (
23 |
24 |
25 | {isPending &&
☎️ing Server Actions...
}
26 | {song &&
}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/examples/app-router/app/server-actions/page.tsx:
--------------------------------------------------------------------------------
1 | import Client from "./client";
2 |
3 | export default function Page() {
4 | return (
5 |
6 |
Server Actions
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/examples/app-router/app/sse/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | export default function SSE() {
6 | const [events, setEvents] = useState([]);
7 |
8 | useEffect(() => {
9 | const e = new EventSource("/api/sse");
10 |
11 | e.onmessage = (msg) => {
12 | console.log(msg);
13 | try {
14 | const data = JSON.parse(msg.data);
15 | if (data.message === "close") {
16 | e.close();
17 | console.log("closing");
18 | }
19 | setEvents((prev) => prev.concat(data));
20 | } catch (err) {
21 | console.log("failed to parse: ", err, msg);
22 | }
23 | };
24 | }, []);
25 |
26 | return (
27 | <>
28 | Server Sent Event
29 | {events.map((e, i) => (
30 |
31 | Message {i}: {JSON.stringify(e)}
32 |
33 | ))}
34 | >
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/examples/app-router/app/ssr/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 |
3 | export default function Layout({ children }: PropsWithChildren) {
4 | return (
5 |
6 |
SSR
7 | {/* 16 kb seems necessary here to prevent any buffering*/}
8 | {/* */}
9 | {children}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/app-router/app/ssr/loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return Loading...
;
3 | }
4 |
--------------------------------------------------------------------------------
/examples/app-router/app/ssr/page.tsx:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers";
2 |
3 | async function getTime() {
4 | const res = await new Promise((resolve) => {
5 | setTimeout(() => {
6 | resolve(new Date().toISOString());
7 | }, 1500);
8 | });
9 | return res;
10 | }
11 |
12 | export default async function SSR() {
13 | const time = await getTime();
14 | const headerList = await headers();
15 | const responseOpenNext = await fetch("https://opennext.js.org", {
16 | cache: "force-cache",
17 | });
18 | return (
19 |
20 |
Time: {time}
21 |
{headerList.get("host")}
22 |
Cached fetch: {responseOpenNext.headers.get("date")}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/examples/app-router/open-next.config.ts:
--------------------------------------------------------------------------------
1 | const config = {
2 | default: {
3 | override: {
4 | wrapper: "aws-lambda-streaming",
5 | queue: "sqs-lite",
6 | incrementalCache: "s3-lite",
7 | tagCache: "dynamodb-lite",
8 | },
9 | },
10 | functions: {},
11 | buildCommand: "npx turbo build",
12 | };
13 |
14 | export default config;
15 |
--------------------------------------------------------------------------------
/examples/app-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app-router",
3 | "version": "0.1.21",
4 | "private": true,
5 | "scripts": {
6 | "openbuild": "node ../../packages/open-next/dist/index.js build --streaming --build-command \"npx turbo build\"",
7 | "dev": "next dev --turbopack --port 3001",
8 | "build": "next build",
9 | "start": "next start --port 3001",
10 | "lint": "next lint",
11 | "clean": "rm -rf .turbo node_modules .next .open-next"
12 | },
13 | "dependencies": {
14 | "@example/shared": "workspace:*",
15 | "@opennextjs/aws": "workspace:*",
16 | "next": "catalog:",
17 | "react": "catalog:",
18 | "react-dom": "catalog:"
19 | },
20 | "devDependencies": {
21 | "@types/node": "catalog:",
22 | "@types/react": "catalog:",
23 | "@types/react-dom": "catalog:",
24 | "autoprefixer": "catalog:",
25 | "postcss": "catalog:",
26 | "tailwindcss": "catalog:",
27 | "typescript": "catalog:"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/app-router/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/examples/app-router/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/app-router/public/favicon.ico
--------------------------------------------------------------------------------
/examples/app-router/public/static/corporate_holiday_card.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/app-router/public/static/corporate_holiday_card.jpg
--------------------------------------------------------------------------------
/examples/app-router/public/static/frank.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/app-router/public/static/frank.webp
--------------------------------------------------------------------------------
/examples/app-router/sst-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/app-router/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
6 | "../../examples/shared/**/*.{jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
12 | "gradient-conic":
13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | };
19 | export default config;
20 |
--------------------------------------------------------------------------------
/examples/app-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "NodeNext",
12 | "moduleResolution": "NodeNext",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/examples/experimental/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/examples/experimental/README.md:
--------------------------------------------------------------------------------
1 | # Experimental
2 |
3 | This project is meant to test experimental features that are only available on canary builds of Next.js.
--------------------------------------------------------------------------------
/examples/experimental/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | /* config options here */
5 | cleanDistDir: true,
6 | output: "standalone",
7 | eslint: {
8 | ignoreDuringBuilds: true,
9 | },
10 | experimental: {
11 | ppr: "incremental",
12 | nodeMiddleware: true,
13 | dynamicIO: true,
14 | },
15 | };
16 |
17 | export default nextConfig;
18 |
--------------------------------------------------------------------------------
/examples/experimental/open-next.config.ts:
--------------------------------------------------------------------------------
1 | const config = {
2 | default: {
3 | override: {
4 | wrapper: "aws-lambda-streaming",
5 | queue: "sqs-lite",
6 | incrementalCache: "s3-lite",
7 | tagCache: "dynamodb-lite",
8 | },
9 | },
10 | functions: {},
11 | buildCommand: "npx turbo build",
12 | };
13 |
14 | export default config;
15 |
--------------------------------------------------------------------------------
/examples/experimental/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "experimental",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "openbuild": "node ../../packages/open-next/dist/index.js build",
7 | "dev": "next dev --turbopack --port 3004",
8 | "build": "next build",
9 | "start": "next start --port 3004",
10 | "lint": "next lint",
11 | "clean": "rm -rf .turbo node_modules .next .open-next"
12 | },
13 | "dependencies": {
14 | "next": "15.4.0-canary.14",
15 | "react": "catalog:",
16 | "react-dom": "catalog:"
17 | },
18 | "devDependencies": {
19 | "@types/node": "catalog:",
20 | "@types/react": "catalog:",
21 | "@types/react-dom": "catalog:",
22 | "typescript": "catalog:"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/api/revalidate/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidateTag } from "next/cache";
2 |
3 | export function GET() {
4 | revalidateTag("fullyTagged");
5 | return new Response("DONE");
6 | }
7 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/experimental/src/app/favicon.ico
--------------------------------------------------------------------------------
/examples/experimental/src/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background: #ffffff;
3 | --foreground: #171717;
4 | }
5 |
6 | @media (prefers-color-scheme: dark) {
7 | :root {
8 | --background: #0a0a0a;
9 | --foreground: #ededed;
10 | }
11 | }
12 |
13 | html,
14 | body {
15 | max-width: 100vw;
16 | overflow-x: hidden;
17 | }
18 |
19 | body {
20 | color: var(--foreground);
21 | background: var(--background);
22 | font-family: Arial, Helvetica, sans-serif;
23 | -webkit-font-smoothing: antialiased;
24 | -moz-osx-font-smoothing: grayscale;
25 | }
26 |
27 | * {
28 | box-sizing: border-box;
29 | padding: 0;
30 | margin: 0;
31 | }
32 |
33 | a {
34 | color: inherit;
35 | text-decoration: none;
36 | }
37 |
38 | @media (prefers-color-scheme: dark) {
39 | html {
40 | color-scheme: dark;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const geistSans = Geist({
6 | variable: "--font-geist-sans",
7 | subsets: ["latin"],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: "--font-geist-mono",
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: "Create Next App",
17 | description: "Generated by create next app",
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: Readonly<{
23 | children: React.ReactNode;
24 | }>) {
25 | return (
26 |
27 |
28 | {children}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./page.module.css";
3 |
4 | export default function Home() {
5 | return (
6 |
7 |
8 |
9 | Incremental PPR
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/ppr/page.tsx:
--------------------------------------------------------------------------------
1 | import { DynamicComponent } from "@/components/dynamic";
2 | import { StaticComponent } from "@/components/static";
3 | import { Suspense } from "react";
4 |
5 | export const experimental_ppr = true;
6 |
7 | export default function PPRPage() {
8 | return (
9 |
10 |
11 | Loading...
}>
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/use-cache/isr/page.tsx:
--------------------------------------------------------------------------------
1 | import { FullyCachedComponent, ISRComponent } from "@/components/cached";
2 | import { Suspense } from "react";
3 |
4 | export default async function Page() {
5 | // Not working for now, need a patch in next to disable full revalidation during ISR revalidation
6 | return (
7 |
8 |
Cache
9 | Loading...}>
10 |
11 |
12 | Loading...}>
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/use-cache/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | export default function Layout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 | Loading...}>{children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/experimental/src/app/use-cache/ssr/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FullyCachedComponent,
3 | FullyCachedComponentWithTag,
4 | ISRComponent,
5 | } from "@/components/cached";
6 | import { headers } from "next/headers";
7 | import { Suspense } from "react";
8 |
9 | export default async function Page() {
10 | // To opt into SSR
11 | const _headers = await headers();
12 | return (
13 |
14 |
Cache
15 |
{_headers.get("accept") ?? "No accept headers"}
16 |
Loading...}>
17 |
18 |
19 |
Loading...}>
20 |
21 |
22 |
Loading...}>
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/examples/experimental/src/components/cached.tsx:
--------------------------------------------------------------------------------
1 | import { unstable_cacheLife, unstable_cacheTag } from "next/cache";
2 |
3 | export async function FullyCachedComponent() {
4 | "use cache";
5 | return (
6 |
9 | );
10 | }
11 |
12 | export async function FullyCachedComponentWithTag() {
13 | "use cache";
14 | unstable_cacheTag("fullyTagged");
15 | return (
16 |
19 | );
20 | }
21 |
22 | export async function ISRComponent() {
23 | "use cache";
24 | unstable_cacheLife({
25 | stale: 1,
26 | revalidate: 5,
27 | });
28 | return (
29 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/examples/experimental/src/components/dynamic.tsx:
--------------------------------------------------------------------------------
1 | import { setTimeout } from "node:timers/promises";
2 | import { headers } from "next/headers";
3 |
4 | export async function DynamicComponent() {
5 | const _headers = await headers();
6 | // Simulate a delay to mimic server-side calls
7 | await setTimeout(1000, new Date().toString());
8 | return (
9 |
10 |
Dynamic Component
11 |
This component should be SSR
12 |
{_headers.get("referer")}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/examples/experimental/src/components/static.tsx:
--------------------------------------------------------------------------------
1 | export function StaticComponent() {
2 | return (
3 |
4 |
Static Component
5 |
This is a static component that does not change.
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/examples/experimental/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import crypto from "node:crypto";
2 | import { type NextRequest, NextResponse } from "next/server";
3 |
4 | export default function middleware(request: NextRequest) {
5 | if (request.nextUrl.pathname === "/api/hello") {
6 | return NextResponse.json({
7 | name: "World",
8 | });
9 | }
10 | if (request.nextUrl.pathname === "/redirect") {
11 | return NextResponse.redirect(new URL("/", request.url));
12 | }
13 | if (request.nextUrl.pathname === "/rewrite") {
14 | return NextResponse.rewrite(new URL("/", request.url));
15 | }
16 |
17 | return NextResponse.next({
18 | headers: {
19 | "x-middleware-test": "1",
20 | "x-random-node": crypto.randomUUID(),
21 | },
22 | });
23 | }
24 |
25 | export const config = {
26 | runtime: "nodejs",
27 | };
28 |
--------------------------------------------------------------------------------
/examples/experimental/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/examples/pages-router/.env.production:
--------------------------------------------------------------------------------
1 | SOME_PROD_VAR=bar
--------------------------------------------------------------------------------
/examples/pages-router/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | .open-next
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/examples/pages-router/README.md:
--------------------------------------------------------------------------------
1 | # Pages Router
2 |
3 | This project uses the Pages Router exclusively.
--------------------------------------------------------------------------------
/examples/pages-router/open-next.config.ts:
--------------------------------------------------------------------------------
1 | const config = {
2 | default: {
3 | override: {
4 | wrapper: "aws-lambda-streaming",
5 | },
6 | },
7 | functions: {},
8 | buildCommand: "npx turbo build",
9 | };
10 |
11 | module.exports = config;
12 |
--------------------------------------------------------------------------------
/examples/pages-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pages-router",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "openbuild": "node ../../packages/open-next/dist/index.js build --build-command \"npx turbo build\"",
7 | "dev": "next dev --turbopack --port 3002",
8 | "build": "next build",
9 | "start": "next start --port 3002",
10 | "lint": "next lint",
11 | "clean": "rm -rf .turbo node_modules .next .open-next"
12 | },
13 | "dependencies": {
14 | "@example/shared": "workspace:*",
15 | "next": "catalog:",
16 | "react": "catalog:",
17 | "react-dom": "catalog:"
18 | },
19 | "devDependencies": {
20 | "@types/node": "catalog:",
21 | "@types/react": "catalog:",
22 | "@types/react-dom": "catalog:",
23 | "tailwindcss": "catalog:",
24 | "postcss": "catalog:",
25 | "autoprefixer": "catalog:",
26 | "typescript": "catalog:"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/pages-router/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {
4 | content: [
5 | "./src/**/*.{js,ts,jsx,tsx,mdx}",
6 | "../../examples/shared/**/*.{jsx,tsx}",
7 | ],
8 | },
9 | autoprefixer: {},
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/examples/pages-router/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/pages-router/public/favicon.ico
--------------------------------------------------------------------------------
/examples/pages-router/public/static/frank.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opennextjs/opennextjs-aws/ab492ca53d4494aa81234c7d6c0cdaf42cd05ba6/examples/pages-router/public/static/frank.webp
--------------------------------------------------------------------------------
/examples/pages-router/src/components/home.tsx:
--------------------------------------------------------------------------------
1 | import Nav from "@example/shared/components/Nav";
2 | import Head from "next/head";
3 |
4 | export default function Home() {
5 | return (
6 | <>
7 |
8 | Nextjs Pages Router
9 |
10 |
11 |
12 |
13 | Nextjs Pages Router
14 |
15 |
18 |
21 |
22 |
23 | >
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/examples/pages-router/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 | import { NextResponse } from "next/server";
3 |
4 | export function middleware(request: NextRequest) {
5 | if (request.headers.get("x-throw")) {
6 | throw new Error("Middleware error");
7 | }
8 | return NextResponse.next({
9 | headers: {
10 | "x-from-middleware": "true",
11 | },
12 | });
13 | }
14 |
15 | export const config = {
16 | matcher: ["/"],
17 | };
18 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css";
2 |
3 | import type { AppProps } from "next/app";
4 |
5 | export default function App({ Component, pageProps }: AppProps) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from "next/document";
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/amp/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * When doing `next build` you would get the error below:
3 | * TypeScript error: Property 'amp-timeago' does not exist on type 'JSX.IntrinsicElements'.
4 | * https://stackoverflow.com/questions/50585952/property-amp-img-does-not-exist-on-type-jsx-intrinsicelements/50601125#50601125
5 | * The workaround in that SO post doesn't work in this (mono)repo so I ended up using @ts-expect-error and @ts-ignore
6 | *
7 | */
8 |
9 | export const config = { amp: true };
10 |
11 | export async function getServerSideProps() {
12 | return {
13 | props: {
14 | time: new Date().toISOString(),
15 | },
16 | };
17 | }
18 |
19 | function MyAmpPage({ time }: { time: string }) {
20 | const date = new Date(time);
21 |
22 | return (
23 |
24 |
Some time: {date.toJSON()}
25 | {/* @ts-expect-error AMP Component not recognized by TypeScript */}
26 |
33 | .{/* @ts-ignore */}
34 |
35 |
36 | );
37 | }
38 |
39 | export default MyAmpPage;
40 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/api/dynamic/[slug].ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
4 | const { slug } = req.query;
5 | res.status(200).json({ slug });
6 | }
7 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/api/dynamic/catch-all-optional/[[...slug]].ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
4 | res.status(200).json({ optional: "true" });
5 | }
6 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/api/dynamic/catch-all/[...slug].ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
4 | const { slug } = req.query;
5 | if (!Array.isArray(slug)) {
6 | return res.status(500).json({ error: "Invalid" });
7 | }
8 | res.status(200).json({ slug });
9 | }
10 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/api/dynamic/precedence/index.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
4 | res.status(200).json({ precedence: "true" });
5 | }
6 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
2 | import type { NextApiRequest, NextApiResponse } from "next";
3 |
4 | type Data = {
5 | hello: string;
6 | };
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse,
11 | ) {
12 | res.status(200).json({ hello: "OpenNext rocks!" });
13 | }
14 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/api/query.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
4 | res.status(200).json({ query: req.query });
5 | }
6 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/fallback-intercepted/[...slugs].tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetServerSidePropsType } from "next";
2 |
3 | export function getServerSideProps() {
4 | return {
5 | props: {
6 | message: "This is a dynamic fallback page.",
7 | },
8 | };
9 | }
10 |
11 | export default function Page({
12 | message,
13 | }: InferGetServerSidePropsType) {
14 | return (
15 |
16 |
Dynamic Fallback Page
17 |
{message}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/fallback-intercepted/[slug].tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetStaticPropsType } from "next";
2 |
3 | export function getStaticPaths() {
4 | return {
5 | paths: [
6 | {
7 | params: {
8 | slug: "fallback",
9 | },
10 | },
11 | ],
12 | fallback: false,
13 | };
14 | }
15 |
16 | export function getStaticProps() {
17 | return {
18 | props: {
19 | message: "This is a static fallback page.",
20 | },
21 | };
22 | }
23 |
24 | export default function Page({
25 | message,
26 | }: InferGetStaticPropsType) {
27 | return (
28 |
29 |
Static Fallback Page
30 |
{message}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/fallback-intercepted/ssg.tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetStaticPropsType } from "next";
2 |
3 | export function getStaticProps() {
4 | return {
5 | props: {
6 | message: "This is a static ssg page.",
7 | },
8 | };
9 | }
10 |
11 | export default function Page({
12 | message,
13 | }: InferGetStaticPropsType) {
14 | return (
15 |
16 |
Static Fallback Page
17 |
{message}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/fallback-intercepted/static.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
Static Fallback Page
5 |
This is a fully static page.
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/fallback/[slug].tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetStaticPropsType } from "next";
2 |
3 | export function getStaticPaths() {
4 | return {
5 | paths: [
6 | {
7 | params: {
8 | slug: "fallback",
9 | },
10 | },
11 | ],
12 | fallback: false,
13 | };
14 | }
15 |
16 | export function getStaticProps() {
17 | return {
18 | props: {
19 | message: "This is a static fallback page.",
20 | },
21 | };
22 | }
23 |
24 | export default function Page({
25 | message,
26 | }: InferGetStaticPropsType) {
27 | return (
28 |
29 |
Static Fallback Page
30 |
{message}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/head/index.tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetServerSidePropsType } from "next";
2 | import Head from "next/head";
3 |
4 | export async function getServerSideProps() {
5 | return {
6 | props: {
7 | time: new Date().toISOString(),
8 | envVar: process.env.SOME_PROD_VAR,
9 | },
10 | };
11 | }
12 |
13 | export default function Page({
14 | time,
15 | envVar,
16 | }: InferGetServerSidePropsType) {
17 | return (
18 |
19 |
20 |
OpenNext head
21 |
25 |
26 |
30 |
31 |
This is a page!
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/isr/index.tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetStaticPropsType } from "next";
2 | import Link from "next/link";
3 |
4 | export async function getStaticProps() {
5 | return {
6 | props: {
7 | time: new Date().toISOString(),
8 | },
9 | revalidate: 10,
10 | };
11 | }
12 |
13 | export default function Page({
14 | time,
15 | }: InferGetStaticPropsType) {
16 | return (
17 |
18 |
Time: {time}
19 |
Home
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/pages-router/src/pages/ssr/index.tsx:
--------------------------------------------------------------------------------
1 | import type { InferGetServerSidePropsType } from "next";
2 |
3 | export async function getServerSideProps() {
4 | return {
5 | props: {
6 | time: new Date().toISOString(),
7 | envVar: process.env.SOME_PROD_VAR,
8 | },
9 | };
10 | }
11 |
12 | export default function Page({
13 | time,
14 | envVar,
15 | }: InferGetServerSidePropsType) {
16 | return (
17 | <>
18 | SSR
19 | Time: {time}
20 | Env: {envVar}
21 | >
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/examples/pages-router/sst-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/pages-router/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "NodeNext",
12 | "moduleResolution": "NodeNext",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["./src/*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/examples/shared/api/index.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import data from "./songs.json";
3 |
4 | export type Song = (typeof data.songs)[0];
5 | export type Album = { album: string; artist: string; songs: Song[] };
6 | const albumsMap: { [key: string]: Song[] } = {};
7 |
8 | const albums: Album[] = [];
9 | data.songs.forEach((s) => {
10 | if (!albumsMap[s.album]) {
11 | albumsMap[s.album] = [s];
12 | } else {
13 | albumsMap[s.album].push(s);
14 | }
15 | });
16 |
17 | Object.entries(albumsMap).forEach(([key, album]) => {
18 | albums.push({
19 | album: album[0].album,
20 | artist: album[0].artist,
21 | songs: album,
22 | });
23 | });
24 |
25 | export async function getAlbums() {
26 | return albums;
27 | }
28 |
29 | export async function getSongs() {
30 | return data.songs;
31 | }
32 |
33 | export async function getSong(album: string, title: string) {
34 | return data.songs.find(
35 | (song) =>
36 | song.album === decodeURIComponent(album) &&
37 | song.title === decodeURIComponent(title),
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/examples/shared/api/songs.json:
--------------------------------------------------------------------------------
1 | {
2 | "songs": [
3 | {
4 | "rank": 1,
5 | "title": "I'm never gonna give you up",
6 | "artist": "Rick Astley",
7 | "album": "Hold Me In Your Arms",
8 | "year": "1965",
9 | "videoId": "dQw4w9WgXcQ"
10 | },
11 | {
12 | "rank": 2,
13 | "title": "My Wang",
14 | "artist": "Frank Wangnatra",
15 | "album": "@franjiewang",
16 | "year": "2023",
17 | "videoId": "qQzdAsjWGPg"
18 | },
19 | {
20 | "rank": 3,
21 | "title": "Excuse me miSST",
22 | "artist": "Jay-Air",
23 | "album": "@Jayair",
24 | "year": "2023",
25 | "videoId": "tnDh0JhmaFw"
26 | },
27 | {
28 | "rank": 4,
29 | "title": "I don't want another CONSOLE-RRY",
30 | "artist": "Dax",
31 | "album": "@thxdr",
32 | "year": "2023",
33 | "videoId": "4JI70_9acgE"
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/examples/shared/components/Album/Album.tsx:
--------------------------------------------------------------------------------
1 | import type { Album } from "../../api/index";
2 | import Song from "./Song";
3 |
4 | type Props = {
5 | album: Album;
6 | };
7 | export default function Album({ album }: Props) {
8 | return (
9 |
10 |
Album: {album.album}
11 |
Artist: {album.artist}
12 | {album.songs.map((song) => (
13 |
14 | ))}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/examples/shared/components/Album/Song.tsx:
--------------------------------------------------------------------------------
1 | import type { Song } from "../../api/index";
2 |
3 | type Props = {
4 | song: Song;
5 | play?: boolean;
6 | };
7 | export default function Song({ song, play }: Props) {
8 | return (
9 |
10 |
Song: {song.title}
11 |
Year: {song.year}
12 | {play && (
13 |
20 | )}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/examples/shared/components/Album/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import type { Album } from "../../api/index";
4 | import Song from "./Song";
5 |
6 | type Props = {
7 | album: Album;
8 | };
9 | export default function Album({ album }: Props) {
10 | return (
11 |
12 |
Album: {album.album}
13 |
Artist: {album.artist}
14 | {album.songs.map((song) => (
15 |
16 |
17 |
18 | ))}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/examples/shared/components/Filler/index.tsx:
--------------------------------------------------------------------------------
1 | interface FillerProps {
2 | // Size in kb of the filler
3 | size: number;
4 | }
5 |
6 | //This component is there to demonstrate how you could bypass streaming buffering in aws lambda.
7 | //Hopefully, this will be fixed in the future and this component will be removed.
8 | // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/issues/94
9 | export default function Filler({ size }: FillerProps) {
10 | const str = "a".repeat(size * 1024);
11 | const byteSize = new TextEncoder().encode(str).length;
12 | return (
13 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/examples/shared/components/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Link from "next/link";
3 | import type { PropsWithChildren } from "react";
4 |
5 | type Props = PropsWithChildren & {
6 | href: string;
7 | title: string;
8 | icon?: string;
9 | };
10 | export default function Nav(p: Props) {
11 | const { children, href, title, icon = "/static/frank.webp" } = p;
12 | return (
13 |
17 |
18 |
{title}
19 |
20 |

25 |
26 |
27 | {children}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/examples/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@example/shared",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "clean": "rm -rf .turbo && rm -rf node_modules"
7 | },
8 | "dependencies": {
9 | "react": "catalog:",
10 | "react-dom": "catalog:"
11 | },
12 | "devDependencies": {
13 | "@types/react": "catalog:",
14 | "@types/react-dom": "catalog:"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/examples/shared/sst-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["."],
3 | "exclude": ["dist", "build", "node_modules"],
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": false,
7 | "declarationMap": false,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "emitDecoratorMetadata": true,
13 | "experimentalDecorators": true,
14 | "moduleResolution": "NodeNext",
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "preserveWatchOutput": true,
18 | "skipLibCheck": true,
19 | "strict": true,
20 | "removeComments": true,
21 | "resolveJsonModule": true,
22 | "sourceMap": true,
23 | "baseUrl": ".",
24 | "jsx": "react-jsx",
25 | "lib": ["ES2022", "DOM"],
26 | "module": "NodeNext",
27 | "target": "ESNext",
28 | "paths": {
29 | "~/*": ["./*"]
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/sst/README.md:
--------------------------------------------------------------------------------
1 | # sst
2 |
3 | This example contains the AppRouter, PagesRouter, and AppPagesRouter Stacks for the end to end tests to hit.
4 |
5 | ## AppRouter
6 |
7 | This app uses the app router exclusively and it uses the streaming feature
8 |
9 | ## PagesRouter
10 |
11 | This app uses the pages router exclusively
12 |
13 | ## AppPagesRouter
14 |
15 | This app uses a mix of app and pages router and *DOES NOT* use the streaming feature
--------------------------------------------------------------------------------
/examples/sst/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example_sst",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {},
7 | "devDependencies": {
8 | "aws-cdk-lib": "2.161.1",
9 | "constructs": "10.3.0",
10 | "sst": "^2.43.6"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/sst/sst.config.ts:
--------------------------------------------------------------------------------
1 | import type { SSTConfig } from "sst";
2 |
3 | import { AppPagesRouter } from "./stacks/AppPagesRouter";
4 | import { AppRouter } from "./stacks/AppRouter";
5 | import { Experimental } from "./stacks/Experimental";
6 | import { PagesRouter } from "./stacks/PagesRouter";
7 |
8 | export default {
9 | config(_input) {
10 | return {
11 | name: "example",
12 | region: "us-east-1",
13 | };
14 | },
15 | stacks(app) {
16 | app
17 | .stack(AppRouter)
18 | .stack(PagesRouter)
19 | .stack(AppPagesRouter)
20 | .stack(Experimental);
21 | },
22 | } satisfies SSTConfig;
23 |
--------------------------------------------------------------------------------
/examples/sst/stacks/AppPagesRouter.ts:
--------------------------------------------------------------------------------
1 | import { OpenNextCdkReferenceImplementation } from "./OpenNextReferenceImplementation";
2 |
3 | // NOTE: App Pages Router doesn't do streaming
4 | export function AppPagesRouter({ stack }) {
5 | const site = new OpenNextCdkReferenceImplementation(stack, "apppagesrouter", {
6 | path: "../app-pages-router",
7 | });
8 | // const site = new NextjsSite(stack, "apppagesrouter", {
9 | // path: "../app-pages-router",
10 | // buildCommand: "npm run openbuild",
11 | // bind: [],
12 | // environment: {},
13 | // });
14 |
15 | stack.addOutputs({
16 | url: `https://${site.distribution.domainName}`,
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/examples/sst/stacks/AppRouter.ts:
--------------------------------------------------------------------------------
1 | import { OpenNextCdkReferenceImplementation } from "./OpenNextReferenceImplementation";
2 |
3 | export function AppRouter({ stack }) {
4 | // We should probably switch to ion once it's ready
5 | const site = new OpenNextCdkReferenceImplementation(stack, "approuter", {
6 | path: "../app-router",
7 | environment: {
8 | OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE: "true",
9 | },
10 | });
11 | // const site = new NextjsSite(stack, "approuter", {
12 | // path: "../app-router",
13 | // buildCommand: "npm run openbuild",
14 | // bind: [],
15 | // environment: {},
16 | // timeout: "20 seconds",
17 | // experimental: {
18 | // streaming: true,
19 | // },
20 | // });
21 |
22 | stack.addOutputs({
23 | url: `https://${site.distribution.domainName}`,
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/examples/sst/stacks/Experimental.ts:
--------------------------------------------------------------------------------
1 | import { OpenNextCdkReferenceImplementation } from "./OpenNextReferenceImplementation";
2 |
3 | export function Experimental({ stack }) {
4 | const site = new OpenNextCdkReferenceImplementation(stack, "experimental", {
5 | path: "../experimental",
6 | environment: {
7 | OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE: "true",
8 | },
9 | });
10 |
11 | stack.addOutputs({
12 | url: `https://${site.distribution.domainName}`,
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/examples/sst/stacks/PagesRouter.ts:
--------------------------------------------------------------------------------
1 | import { OpenNextCdkReferenceImplementation } from "./OpenNextReferenceImplementation";
2 |
3 | export function PagesRouter({ stack }) {
4 | const site = new OpenNextCdkReferenceImplementation(stack, "pagesrouter", {
5 | path: "../pages-router",
6 | /*
7 | * We need to set this environment variable to not break other E2E tests that have an empty body. (i.e: /redirect)
8 | * https://opennext.js.org/aws/common_issues#empty-body-in-response-when-streaming-in-aws-lambda
9 | *
10 | */
11 | environment: {
12 | OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE: "true",
13 | },
14 | });
15 | // const site = new NextjsSite(stack, "pagesrouter", {
16 | // path: "../pages-router",
17 | // buildCommand: "npm run openbuild",
18 | // bind: [],
19 | // environment: {},
20 | // });
21 |
22 | stack.addOutputs({
23 | url: `https://${site.distribution.domainName}`,
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monorepo",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "private": true,
6 | "type": "module",
7 | "scripts": {
8 | "dev": "turbo run dev",
9 | "build": "turbo run build",
10 | "clean": "turbo run clean && rm -rf node_modules pnpm-lock.yaml",
11 | "lint": "biome check",
12 | "lint:fix": "biome check --fix",
13 | "test": "turbo run test",
14 | "e2e:test": "turbo run e2e:test",
15 | "version": "./.changeset/version",
16 | "release": "./.changeset/release"
17 | },
18 | "devDependencies": {
19 | "@biomejs/biome": "1.9.4",
20 | "@changesets/changelog-github": "^0.4.4",
21 | "@changesets/cli": "^2.22.0",
22 | "pkg-pr-new": "^0.0.29",
23 | "turbo": "1.10.12"
24 | },
25 | "engines": {
26 | "node": ">=18",
27 | "pnpm": ">=9"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/open-next/src/adapters/plugins/14.1/util.ts:
--------------------------------------------------------------------------------
1 | import { NextConfig } from "../../config";
2 | import { debug } from "../../logger.js";
3 |
4 | //#override requestHandler
5 | // @ts-ignore
6 | export const requestHandler = new NextServer.default({
7 | conf: {
8 | ...NextConfig,
9 | // Next.js compression should be disabled because of a bug in the bundled
10 | // `compression` package — https://github.com/vercel/next.js/issues/11669
11 | compress: false,
12 | // By default, Next.js uses local disk to store ISR cache. We will use
13 | // our own cache handler to store the cache on S3.
14 | cacheHandler: `${process.env.LAMBDA_TASK_ROOT}/cache.cjs`,
15 | cacheMaxMemorySize: 0,
16 | experimental: {
17 | ...NextConfig.experimental,
18 | // This uses the request.headers.host as the URL
19 | // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/next-server.ts#L1749-L1754
20 | trustHostHeader: true,
21 | },
22 | },
23 | customServer: false,
24 | dev: false,
25 | dir: __dirname,
26 | }).getRequestHandler();
27 | //#endOverride
28 |
29 | //#override requireHooks
30 | debug("No need to override require hooks with next 13.4.20+");
31 | //#endOverride
32 |
--------------------------------------------------------------------------------
/packages/open-next/src/adapters/plugins/README.md:
--------------------------------------------------------------------------------
1 | # Known issues
2 |
3 | Do not include `types` in #override and #imports, as esbuild will remove preceding comments (ie it removes //#override id)when it builds.
4 |
5 | Instead, put the `import type` outside like:
6 |
7 | ```ts
8 | import type { PluginHandler } from "../next-types.js";
9 | import type { IncomingMessage } from "../request.js";
10 | import type { ServerResponse } from "../response.js";
11 |
12 | //#override imports
13 | import { requestHandler } from "./util.js";
14 | //#endOverride
15 | ```
16 |
17 | The types are removed in the final output anyways.
18 |
--------------------------------------------------------------------------------
/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // This file is used only for Next 14.1 and below. Typing is correct for these versions.
3 | import type { IncomingMessage, ServerResponse } from "node:http";
4 |
5 | import type { APIGatewayProxyEventHeaders } from "aws-lambda";
6 | import type { NextConfig } from "next/dist/server/config-shared";
7 | //#override imports
8 | import { imageOptimizer } from "next/dist/server/image-optimizer";
9 | //#endOverride
10 | import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
11 |
12 | import { debug } from "../../logger.js";
13 |
14 | //#override optimizeImage
15 | export async function optimizeImage(
16 | headers: APIGatewayProxyEventHeaders,
17 | imageParams: any,
18 | nextConfig: NextConfig,
19 | handleRequest: (
20 | newReq: IncomingMessage,
21 | newRes: ServerResponse,
22 | newParsedUrl: NextUrlWithParsedQuery,
23 | ) => Promise,
24 | ) {
25 | const result = await imageOptimizer(
26 | { headers },
27 | {}, // res object is not necessary as it's not actually used.
28 | imageParams,
29 | nextConfig,
30 | false, // not in dev mode
31 | handleRequest,
32 | );
33 | debug("optimized result", result);
34 | return result;
35 | }
36 | //#endOverride
37 |
--------------------------------------------------------------------------------
/packages/open-next/src/adapters/server-adapter.ts:
--------------------------------------------------------------------------------
1 | // We load every config here so that they are only loaded once
2 | // and during cold starts
3 | import { BuildId } from "config/index.js";
4 |
5 | import { createMainHandler } from "../core/createMainHandler.js";
6 | import { setNodeEnv } from "./util.js";
7 |
8 | // We load every config here so that they are only loaded once
9 | // and during cold starts
10 | setNodeEnv();
11 | setBuildIdEnv();
12 | setNextjsServerWorkingDirectory();
13 |
14 | // Because next is messing with fetch, we have to make sure that we use an untouched version of fetch
15 | globalThis.internalFetch = fetch;
16 |
17 | /////////////
18 | // Handler //
19 | /////////////
20 |
21 | export const handler = await createMainHandler();
22 |
23 | //////////////////////
24 | // Helper functions //
25 | //////////////////////
26 |
27 | function setNextjsServerWorkingDirectory() {
28 | // WORKAROUND: Set `NextServer` working directory (AWS specific)
29 | // See https://opennext.js.org/aws/v2/advanced/workaround#workaround-set-nextserver-working-directory-aws-specific
30 | process.chdir(__dirname);
31 | }
32 |
33 | function setBuildIdEnv() {
34 | // This allows users to access the CloudFront invalidating path when doing on-demand
35 | // invalidations. ie. `/_next/data/${process.env.NEXT_BUILD_ID}/foo.json`
36 | process.env.NEXT_BUILD_ID = BuildId;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/open-next/src/adapters/util.ts:
--------------------------------------------------------------------------------
1 | //TODO: We should probably move all the utils to a separate location
2 |
3 | export function setNodeEnv() {
4 | // Note: we create a `processEnv` variable instead of just using `process.env` directly
5 | // because build tools can substitute `process.env.NODE_ENV` on build making
6 | // assignments such as `process.env.NODE_ENV = ...` problematic
7 | const processEnv = process.env;
8 | processEnv.NODE_ENV = process.env.NODE_ENV ?? "production";
9 | }
10 |
11 | export function generateUniqueId() {
12 | return Math.random().toString(36).slice(2, 8);
13 | }
14 |
15 | /**
16 | * Create an array of arrays of size `chunkSize` from `items`
17 | * @param items Array of T
18 | * @param chunkSize size of each chunk
19 | * @returns T[][]
20 | */
21 | export function chunk(items: T[], chunkSize: number): T[][] {
22 | const chunked = items.reduce((acc, curr, i) => {
23 | const chunkIndex = Math.floor(i / chunkSize);
24 | acc[chunkIndex] = [...(acc[chunkIndex] ?? []), curr];
25 | return acc;
26 | }, new Array());
27 |
28 | return chunked;
29 | }
30 |
31 | export function parseNumberFromEnv(
32 | envValue: string | undefined,
33 | ): number | undefined {
34 | if (typeof envValue !== "string") {
35 | return envValue;
36 | }
37 |
38 | const parsedValue = Number.parseInt(envValue);
39 |
40 | return Number.isNaN(parsedValue) ? undefined : parsedValue;
41 | }
42 |
--------------------------------------------------------------------------------
/packages/open-next/src/adapters/warmer-function.ts:
--------------------------------------------------------------------------------
1 | import { createGenericHandler } from "../core/createGenericHandler.js";
2 | import { resolveWarmerInvoke } from "../core/resolve.js";
3 | import { generateUniqueId } from "./util.js";
4 |
5 | export interface WarmerEvent {
6 | type: "warmer";
7 | warmerId: string;
8 | index: number;
9 | concurrency: number;
10 | delay: number;
11 | }
12 |
13 | export interface WarmerResponse {
14 | type: "warmer";
15 | serverId: string;
16 | }
17 |
18 | export const handler = await createGenericHandler({
19 | handler: defaultHandler,
20 | type: "warmer",
21 | });
22 |
23 | async function defaultHandler() {
24 | const warmerId = `warmer-${generateUniqueId()}`;
25 |
26 | const invokeFn = await resolveWarmerInvoke(
27 | globalThis.openNextConfig.warmer?.invokeFunction,
28 | );
29 |
30 | await invokeFn.invoke(warmerId);
31 |
32 | return {
33 | type: "warmer",
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/buildNextApp.ts:
--------------------------------------------------------------------------------
1 | import cp from "node:child_process";
2 | import path from "node:path";
3 |
4 | import type * as buildHelper from "./helper.js";
5 |
6 | export function setStandaloneBuildMode(options: buildHelper.BuildOptions) {
7 | // Equivalent to setting `output: "standalone"` in next.config.js
8 | process.env.NEXT_PRIVATE_STANDALONE = "true";
9 | // Equivalent to setting `experimental.outputFileTracingRoot` in next.config.js
10 | process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = options.monorepoRoot;
11 | }
12 |
13 | export function buildNextjsApp(options: buildHelper.BuildOptions) {
14 | const { config, packager } = options;
15 | const command =
16 | config.buildCommand ??
17 | (["bun", "npm"].includes(packager)
18 | ? `${packager} run build`
19 | : `${packager} build`);
20 | cp.execSync(command, {
21 | stdio: "inherit",
22 | cwd: path.dirname(options.appPackageJsonPath),
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/compileTagCacheProvider.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 |
3 | import { openNextResolvePlugin } from "../plugins/resolve.js";
4 | import * as buildHelper from "./helper.js";
5 | import { installDependencies } from "./installDeps.js";
6 |
7 | export async function compileTagCacheProvider(
8 | options: buildHelper.BuildOptions,
9 | ) {
10 | const providerPath = path.join(options.outputDir, "dynamodb-provider");
11 |
12 | const overrides = options.config.initializationFunction?.override;
13 |
14 | await buildHelper.esbuildAsync(
15 | {
16 | external: ["@aws-sdk/client-dynamodb"],
17 | entryPoints: [
18 | path.join(options.openNextDistDir, "adapters", "dynamo-provider.js"),
19 | ],
20 | outfile: path.join(providerPath, "index.mjs"),
21 | target: ["node18"],
22 | plugins: [
23 | openNextResolvePlugin({
24 | fnName: "initializationFunction",
25 | overrides: {
26 | converter: overrides?.converter ?? "dummy",
27 | wrapper: overrides?.wrapper,
28 | tagCache: options.config.initializationFunction?.tagCache,
29 | },
30 | }),
31 | ],
32 | },
33 | options,
34 | );
35 |
36 | installDependencies(
37 | providerPath,
38 | options.config.initializationFunction?.install,
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/constant.ts:
--------------------------------------------------------------------------------
1 | //TODO: Move all other manifest path here as well
2 | export const MIDDLEWARE_TRACE_FILE = "server/middleware.js.nft.json";
3 | export const INSTRUMENTATION_TRACE_FILE = "server/instrumentation.js.nft.json";
4 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/patch/patchedAsyncStorage.ts:
--------------------------------------------------------------------------------
1 | //@ts-nocheck
2 |
3 | const asyncStorage = require("next/dist/client/components/static-generation-async-storage.external.original");
4 |
5 | const staticGenerationAsyncStorage = {
6 | run: (store, cb, ...args) =>
7 | asyncStorage.staticGenerationAsyncStorage.run(store, cb, ...args),
8 | getStore: () => {
9 | const store = asyncStorage.staticGenerationAsyncStorage.getStore();
10 | if (store) {
11 | store.isOnDemandRevalidate =
12 | store.isOnDemandRevalidate &&
13 | !globalThis.__openNextAls.getStore().isISRRevalidation;
14 | }
15 | return store;
16 | },
17 | };
18 |
19 | exports.staticGenerationAsyncStorage = staticGenerationAsyncStorage;
20 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/patch/patches/index.ts:
--------------------------------------------------------------------------------
1 | export { envVarRuleCreator, patchEnvVars } from "./patchEnvVar.js";
2 | export { patchNextServer } from "./patchNextServer.js";
3 | export {
4 | patchFetchCacheForISR,
5 | patchUnstableCacheForISR,
6 | patchUseCacheForISR,
7 | } from "./patchFetchCacheISR.js";
8 | export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
9 | export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
10 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/patch/patches/patchBackgroundRevalidation.ts:
--------------------------------------------------------------------------------
1 | import { getCrossPlatformPathRegex } from "utils/regex.js";
2 | import { createPatchCode } from "../astCodePatcher.js";
3 | import type { CodePatcher } from "../codePatcher.js";
4 |
5 | export const rule = `
6 | rule:
7 | kind: binary_expression
8 | all:
9 | - has:
10 | kind: unary_expression
11 | regex: "!cachedResponse.isStale"
12 | - has:
13 | kind: member_expression
14 | regex: "context.isPrefetch"
15 | fix:
16 | 'true'`;
17 |
18 | export const patchBackgroundRevalidation = {
19 | name: "patchBackgroundRevalidation",
20 | patches: [
21 | {
22 | // TODO: test for earlier versions of Next
23 | versions: ">=14.1.0",
24 | field: {
25 | pathFilter: getCrossPlatformPathRegex("server/response-cache/index.js"),
26 | patchCode: createPatchCode(rule),
27 | },
28 | },
29 | ],
30 | } satisfies CodePatcher;
31 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/patch/patches/patchFetchCacheWaitUntil.ts:
--------------------------------------------------------------------------------
1 | import { getCrossPlatformPathRegex } from "utils/regex.js";
2 | import { createPatchCode } from "../astCodePatcher.js";
3 | import type { CodePatcher } from "../codePatcher.js";
4 |
5 | export const rule = `
6 | rule:
7 | kind: call_expression
8 | pattern: $PROMISE
9 | all:
10 | - has: { pattern: $_.arrayBuffer().then, stopBy: end }
11 | - has: { pattern: "Buffer.from", stopBy: end }
12 | - any:
13 | - inside:
14 | kind: sequence_expression
15 | inside:
16 | kind: return_statement
17 | - inside:
18 | kind: expression_statement
19 | precedes:
20 | kind: return_statement
21 | - has: { pattern: $_.FETCH, stopBy: end }
22 |
23 | fix: |
24 | globalThis.__openNextAls?.getStore()?.pendingPromiseRunner.add($PROMISE)
25 | `;
26 |
27 | export const patchFetchCacheSetMissingWaitUntil: CodePatcher = {
28 | name: "patch-fetch-cache-set-missing-wait-until",
29 | patches: [
30 | {
31 | versions: ">=15.0.0",
32 | field: {
33 | pathFilter: getCrossPlatformPathRegex(
34 | String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|patch-fetch\.js)$`,
35 | { escape: false },
36 | ),
37 | contentFilter: /arrayBuffer\(\)\s*\.then/,
38 | patchCode: createPatchCode(rule),
39 | },
40 | },
41 | ],
42 | };
43 |
--------------------------------------------------------------------------------
/packages/open-next/src/build/utils.ts:
--------------------------------------------------------------------------------
1 | import os from "node:os";
2 |
3 | import logger from "../logger.js";
4 |
5 | export function printHeader(header: string) {
6 | // biome-ignore lint/style/noParameterAssign:
7 | header = `OpenNext — ${header}`;
8 | logger.info(
9 | [
10 | "",
11 | `┌${"─".repeat(header.length + 2)}┐`,
12 | `│ ${header} │`,
13 | `└${"─".repeat(header.length + 2)}┘`,
14 | "",
15 | ].join("\n"),
16 | );
17 | }
18 |
19 | /**
20 | * Displays a warning on windows platform.
21 | */
22 | export function showWarningOnWindows() {
23 | if (os.platform() !== "win32") return;
24 |
25 | logger.warn("OpenNext is not fully compatible with Windows.");
26 | logger.warn(
27 | "For optimal performance, it is recommended to use Windows Subsystem for Linux (WSL).",
28 | );
29 | logger.warn(
30 | "While OpenNext may function on Windows, it could encounter unpredictable failures during runtime.",
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/open-next/src/core/edgeFunctionHandler.ts:
--------------------------------------------------------------------------------
1 | // Necessary files will be imported here with banner in esbuild
2 |
3 | import type { RequestData } from "types/global";
4 |
5 | type EdgeRequest = Omit;
6 |
7 | export default async function edgeFunctionHandler(
8 | request: EdgeRequest,
9 | ): Promise {
10 | const path = new URL(request.url).pathname;
11 | const routes = globalThis._ROUTES;
12 | const correspondingRoute = routes.find((route) =>
13 | route.regex.some((r) => new RegExp(r).test(path)),
14 | );
15 |
16 | if (!correspondingRoute) {
17 | throw new Error(`No route found for ${request.url}`);
18 | }
19 |
20 | const entry = await self._ENTRIES[`middleware_${correspondingRoute.name}`];
21 |
22 | const result = await entry.default({
23 | page: correspondingRoute.page,
24 | request: {
25 | ...request,
26 | page: {
27 | name: correspondingRoute.name,
28 | },
29 | },
30 | });
31 | globalThis.__openNextAls
32 | .getStore()
33 | ?.pendingPromiseRunner.add(result.waitUntil);
34 | const response = result.response;
35 | return response;
36 | }
37 |
--------------------------------------------------------------------------------
/packages/open-next/src/core/patchAsyncStorage.ts:
--------------------------------------------------------------------------------
1 | const mod = require("node:module");
2 |
3 | const resolveFilename = mod._resolveFilename;
4 |
5 | export function patchAsyncStorage() {
6 | mod._resolveFilename = ((
7 | originalResolveFilename: typeof resolveFilename,
8 | request: string,
9 | parent: any,
10 | isMain: boolean,
11 | options: any,
12 | ) => {
13 | if (
14 | request.endsWith("static-generation-async-storage.external") ||
15 | request.endsWith("static-generation-async-storage.external.js")
16 | ) {
17 | return require.resolve("./patchedAsyncStorage.cjs");
18 | }
19 | if (request.endsWith("static-generation-async-storage.external.original")) {
20 | return originalResolveFilename.call(
21 | mod,
22 | request.replace(".original", ".js"),
23 | parent,
24 | isMain,
25 | options,
26 | );
27 | }
28 | return originalResolveFilename.call(mod, request, parent, isMain, options);
29 |
30 | // We use `bind` here to avoid referencing outside variables to create potential memory leaks.
31 | }).bind(null, resolveFilename);
32 | }
33 |
--------------------------------------------------------------------------------
/packages/open-next/src/http/index.ts:
--------------------------------------------------------------------------------
1 | // @__PURE__
2 | export * from "./openNextResponse.js";
3 | // @__PURE__
4 | export * from "./request.js";
5 |
--------------------------------------------------------------------------------
/packages/open-next/src/http/request.ts:
--------------------------------------------------------------------------------
1 | // Copied and modified from serverless-http by Doug Moscrop
2 | // https://github.com/dougmoscrop/serverless-http/blob/master/lib/request.js
3 | // Licensed under the MIT License
4 |
5 | // @ts-nocheck
6 | import http from "node:http";
7 |
8 | export class IncomingMessage extends http.IncomingMessage {
9 | constructor({
10 | method,
11 | url,
12 | headers,
13 | body,
14 | remoteAddress,
15 | }: {
16 | method: string;
17 | url: string;
18 | headers: Record;
19 | body?: Buffer;
20 | remoteAddress?: string;
21 | }) {
22 | super({
23 | encrypted: true,
24 | readable: false,
25 | remoteAddress,
26 | address: () => ({ port: 443 }),
27 | end: Function.prototype,
28 | destroy: Function.prototype,
29 | });
30 |
31 | // Set the content length when there is a body.
32 | // See https://httpwg.org/specs/rfc9110.html#field.content-length
33 | if (body) {
34 | headers["content-length"] ??= String(Buffer.byteLength(body));
35 | }
36 |
37 | Object.assign(this, {
38 | ip: remoteAddress,
39 | complete: true,
40 | httpVersion: "1.1",
41 | httpVersionMajor: "1",
42 | httpVersionMinor: "1",
43 | method,
44 | headers,
45 | body,
46 | url,
47 | });
48 |
49 | this._read = () => {
50 | this.push(body);
51 | this.push(null);
52 | };
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/open-next/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { build } from "./build.js";
4 |
5 | const command = process.argv[2];
6 | if (command !== "build") printHelp();
7 |
8 | const args = parseArgs();
9 | if (Object.keys(args).includes("--help")) printHelp();
10 |
11 | await build(args["--config-path"], args["--node-externals"]);
12 |
13 | function parseArgs() {
14 | return process.argv.slice(2).reduce(
15 | (acc, key, ind, self) => {
16 | if (key.startsWith("--")) {
17 | if (self[ind + 1] && self[ind + 1].startsWith("-")) {
18 | acc[key] = undefined;
19 | } else if (self[ind + 1]) {
20 | acc[key] = self[ind + 1];
21 | // eslint-disable-next-line sonarjs/elseif-without-else
22 | } else if (!self[ind + 1]) {
23 | acc[key] = undefined;
24 | }
25 | }
26 | return acc;
27 | },
28 | {} as Record,
29 | );
30 | }
31 |
32 | function printHelp() {
33 | console.log("Unknown command");
34 | console.log("");
35 | console.log("Usage:");
36 | console.log(" npx open-next build");
37 | console.log("You can use a custom config path here");
38 | console.log(
39 | " npx open-next build --config-path ./path/to/open-next.config.ts",
40 | );
41 | console.log(
42 | "You can configure externals for the esbuild compilation of the open-next.config.ts file",
43 | );
44 | console.log(" npx open-next build --node-externals aws-sdk,sharp,sqlite3");
45 | console.log("");
46 |
47 | process.exit(1);
48 | }
49 |
--------------------------------------------------------------------------------
/packages/open-next/src/logger.ts:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 |
3 | type LEVEL = "info" | "debug";
4 |
5 | let logLevel: LEVEL = "info";
6 |
7 | export default {
8 | setLevel: (level: LEVEL) => (logLevel = level),
9 | debug: (...args: any[]) => {
10 | if (logLevel !== "debug") return;
11 | console.log(chalk.magenta("DEBUG"), ...args);
12 | },
13 | info: console.log,
14 | warn: (...args: any[]) => console.warn(chalk.yellow("WARN"), ...args),
15 | error: (...args: any[]) => console.error(chalk.red("ERROR"), ...args),
16 | time: console.time,
17 | timeEnd: console.timeEnd,
18 | };
19 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/cdnInvalidation/cloudfront.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CloudFrontClient,
3 | CreateInvalidationCommand,
4 | } from "@aws-sdk/client-cloudfront";
5 | import type { CDNInvalidationHandler } from "types/overrides";
6 |
7 | const cloudfront = new CloudFrontClient({});
8 | export default {
9 | name: "cloudfront",
10 | invalidatePaths: async (paths) => {
11 | const constructedPaths = paths.flatMap(
12 | ({ initialPath, resolvedRoutes }) => {
13 | const isAppRouter = resolvedRoutes.some(
14 | (route) => route.type === "app",
15 | );
16 | // revalidateTag doesn't have any leading slash, remove it just to be sure
17 | const path = initialPath.replace(/^\//, "");
18 | return isAppRouter
19 | ? [`/${path}`, `/${path}?_rsc=*`]
20 | : [
21 | `/${path}`,
22 | `/_next/data/${process.env.NEXT_BUILD_ID}${path === "/" ? "/index" : `/${path}`}.json*`,
23 | ];
24 | },
25 | );
26 | await cloudfront.send(
27 | new CreateInvalidationCommand({
28 | DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID!,
29 | InvalidationBatch: {
30 | // Do we need to limit the number of paths? Or batch them into multiple commands?
31 | Paths: { Quantity: constructedPaths.length, Items: constructedPaths },
32 | CallerReference: `${Date.now()}`,
33 | },
34 | }),
35 | );
36 | },
37 | } satisfies CDNInvalidationHandler;
38 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/cdnInvalidation/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { CDNInvalidationHandler } from "types/overrides";
2 |
3 | export default {
4 | name: "dummy",
5 | invalidatePaths: (_) => {
6 | return Promise.resolve();
7 | },
8 | } satisfies CDNInvalidationHandler;
9 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/converters/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { Converter } from "types/overrides";
2 |
3 | type DummyEventOrResult = {
4 | type: "dummy";
5 | original: any;
6 | };
7 |
8 | const converter: Converter = {
9 | convertFrom(event) {
10 | return Promise.resolve({
11 | type: "dummy",
12 | original: event,
13 | });
14 | },
15 | convertTo(internalResult) {
16 | return Promise.resolve({
17 | type: "dummy",
18 | original: internalResult,
19 | });
20 | },
21 | name: "dummy",
22 | };
23 |
24 | export default converter;
25 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/converters/sqs-revalidate.ts:
--------------------------------------------------------------------------------
1 | import type { SQSEvent } from "aws-lambda";
2 | import type { Converter } from "types/overrides";
3 |
4 | import type { RevalidateEvent } from "../../adapters/revalidate";
5 |
6 | const converter: Converter = {
7 | convertFrom(event: SQSEvent) {
8 | const records = event.Records.map((record) => {
9 | const { host, url } = JSON.parse(record.body);
10 | return { host, url, id: record.messageId };
11 | });
12 | return Promise.resolve({
13 | type: "revalidate",
14 | records,
15 | });
16 | },
17 | convertTo(revalidateEvent) {
18 | return Promise.resolve({
19 | type: "revalidate",
20 | batchItemFailures: revalidateEvent.records.map((record) => ({
21 | itemIdentifier: record.id,
22 | })),
23 | });
24 | },
25 | name: "sqs-revalidate",
26 | };
27 |
28 | export default converter;
29 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/converters/utils.ts:
--------------------------------------------------------------------------------
1 | import { getQueryFromIterator } from "http/util.js";
2 |
3 | export function removeUndefinedFromQuery(
4 | query: Record,
5 | ) {
6 | const newQuery: Record = {};
7 | for (const [key, value] of Object.entries(query)) {
8 | if (value !== undefined) {
9 | newQuery[key] = value;
10 | }
11 | }
12 | return newQuery;
13 | }
14 |
15 | /**
16 | * Extract the host from the headers (default to "on")
17 | *
18 | * @param headers
19 | * @returns The host
20 | */
21 | export function extractHostFromHeaders(
22 | headers: Record,
23 | ): string {
24 | return headers["x-forwarded-host"] ?? headers.host ?? "on";
25 | }
26 |
27 | /**
28 | * Get the query object from an URLSearchParams
29 | *
30 | * @param searchParams
31 | * @returns
32 | */
33 | export function getQueryFromSearchParams(searchParams: URLSearchParams) {
34 | return getQueryFromIterator(searchParams.entries());
35 | }
36 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/imageLoader/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { ImageLoader } from "types/overrides";
2 | import { FatalError } from "utils/error";
3 |
4 | const dummyLoader: ImageLoader = {
5 | name: "dummy",
6 | load: async (_: string) => {
7 | throw new FatalError("Dummy loader is not implemented");
8 | },
9 | };
10 |
11 | export default dummyLoader;
12 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/imageLoader/fs-dev.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 |
4 | import { NextConfig } from "config/index";
5 | import type { ImageLoader } from "types/overrides";
6 | import { getMonorepoRelativePath } from "utils/normalize-path";
7 |
8 | export default {
9 | name: "fs-dev",
10 | load: async (url: string) => {
11 | const urlWithoutBasePath = NextConfig.basePath
12 | ? url.slice(NextConfig.basePath.length)
13 | : url;
14 | const imagePath = path.join(
15 | getMonorepoRelativePath(),
16 | "assets",
17 | urlWithoutBasePath,
18 | );
19 | const body = fs.createReadStream(imagePath);
20 | const contentType = url.endsWith(".png") ? "image/png" : "image/jpeg";
21 | return {
22 | body,
23 | contentType,
24 | cacheControl: "public, max-age=31536000, immutable",
25 | };
26 | },
27 | } satisfies ImageLoader;
28 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/imageLoader/host.ts:
--------------------------------------------------------------------------------
1 | import { Readable } from "node:stream";
2 | import type { ReadableStream } from "node:stream/web";
3 |
4 | import type { ImageLoader } from "types/overrides";
5 | import { FatalError } from "utils/error";
6 |
7 | const hostLoader: ImageLoader = {
8 | name: "host",
9 | load: async (key: string) => {
10 | const host = process.env.HOST;
11 | if (!host) {
12 | throw new FatalError("Host must be defined!");
13 | }
14 | const url = `https://${host}${key}`;
15 | const response = await fetch(url);
16 | if (!response.ok) {
17 | throw new FatalError(`Failed to fetch image from ${url}`);
18 | }
19 | if (!response.body) {
20 | throw new FatalError("No body in response");
21 | }
22 | const body = Readable.fromWeb(response.body as ReadableStream);
23 | const contentType = response.headers.get("content-type") ?? "image/jpeg";
24 | const cacheControl =
25 | response.headers.get("cache-control") ??
26 | "private, max-age=0, must-revalidate";
27 | return {
28 | body,
29 | contentType,
30 | cacheControl,
31 | };
32 | },
33 | };
34 |
35 | export default hostLoader;
36 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/imageLoader/s3.ts:
--------------------------------------------------------------------------------
1 | import type { Readable } from "node:stream";
2 |
3 | import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
4 | import type { ImageLoader } from "types/overrides";
5 | import { FatalError } from "utils/error";
6 |
7 | import { awsLogger } from "../../adapters/logger";
8 |
9 | const { BUCKET_NAME, BUCKET_KEY_PREFIX } = process.env;
10 |
11 | function ensureBucketExists() {
12 | if (!BUCKET_NAME) {
13 | throw new Error("Bucket name must be defined!");
14 | }
15 | }
16 |
17 | const s3Loader: ImageLoader = {
18 | name: "s3",
19 | load: async (key: string) => {
20 | const s3Client = new S3Client({ logger: awsLogger });
21 |
22 | ensureBucketExists();
23 | const keyPrefix = BUCKET_KEY_PREFIX?.replace(/^\/|\/$/g, "");
24 | const response = await s3Client.send(
25 | new GetObjectCommand({
26 | Bucket: BUCKET_NAME,
27 | Key: keyPrefix
28 | ? `${keyPrefix}/${key.replace(/^\//, "")}`
29 | : key.replace(/^\//, ""),
30 | }),
31 | );
32 | const body = response.Body as Readable | undefined;
33 |
34 | if (!body) {
35 | throw new FatalError("No body in S3 response");
36 | }
37 |
38 | return {
39 | body: body,
40 | contentType: response.ContentType,
41 | cacheControl: response.CacheControl,
42 | };
43 | },
44 | };
45 |
46 | export default s3Loader;
47 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/incrementalCache/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { IncrementalCache } from "types/overrides";
2 | import { IgnorableError } from "utils/error";
3 |
4 | const dummyIncrementalCache: IncrementalCache = {
5 | name: "dummy",
6 | get: async () => {
7 | throw new IgnorableError('"Dummy" cache does not cache anything');
8 | },
9 | set: async () => {
10 | throw new IgnorableError('"Dummy" cache does not cache anything');
11 | },
12 | delete: async () => {
13 | throw new IgnorableError('"Dummy" cache does not cache anything');
14 | },
15 | };
16 |
17 | export default dummyIncrementalCache;
18 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/incrementalCache/fs-dev.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs/promises";
2 | import path from "node:path";
3 |
4 | import type { IncrementalCache } from "types/overrides.js";
5 | import { getMonorepoRelativePath } from "utils/normalize-path";
6 |
7 | const buildId = process.env.NEXT_BUILD_ID;
8 | const basePath = path.join(getMonorepoRelativePath(), `cache/${buildId}`);
9 |
10 | const getCacheKey = (key: string) => {
11 | return path.join(basePath, `${key}.cache`);
12 | };
13 |
14 | const cache: IncrementalCache = {
15 | name: "fs-dev",
16 | get: async (key: string) => {
17 | const fileData = await fs.readFile(getCacheKey(key), "utf-8");
18 | const data = JSON.parse(fileData);
19 | const { mtime } = await fs.stat(getCacheKey(key));
20 | return {
21 | value: data,
22 | lastModified: mtime.getTime(),
23 | };
24 | },
25 | set: async (key, value, isFetch) => {
26 | const data = JSON.stringify(value);
27 | const cacheKey = getCacheKey(key);
28 | // We need to create the directory before writing the file
29 | await fs.mkdir(path.dirname(cacheKey), { recursive: true });
30 | await fs.writeFile(cacheKey, data);
31 | },
32 | delete: async (key) => {
33 | await fs.rm(getCacheKey(key));
34 | },
35 | };
36 |
37 | export default cache;
38 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/originResolver/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { OriginResolver } from "types/overrides";
2 |
3 | const dummyOriginResolver: OriginResolver = {
4 | name: "dummy",
5 | resolve: async (_path: string) => {
6 | return false as const;
7 | },
8 | };
9 |
10 | export default dummyOriginResolver;
11 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/proxyExternalRequest/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { ProxyExternalRequest } from "types/overrides";
2 | import { FatalError } from "utils/error";
3 |
4 | const DummyProxyExternalRequest: ProxyExternalRequest = {
5 | name: "dummy",
6 | proxy: async (_event) => {
7 | throw new FatalError("This is a dummy implementation");
8 | },
9 | };
10 |
11 | export default DummyProxyExternalRequest;
12 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/proxyExternalRequest/fetch.ts:
--------------------------------------------------------------------------------
1 | import type { ProxyExternalRequest } from "types/overrides";
2 | import { emptyReadableStream } from "utils/stream";
3 |
4 | const fetchProxy: ProxyExternalRequest = {
5 | name: "fetch-proxy",
6 | // @ts-ignore
7 | proxy: async (internalEvent) => {
8 | const { url, headers: eventHeaders, method, body } = internalEvent;
9 |
10 | const headers = Object.fromEntries(
11 | Object.entries(eventHeaders).filter(
12 | ([key]) => key.toLowerCase() !== "cf-connecting-ip",
13 | ),
14 | );
15 |
16 | const response = await fetch(url, {
17 | method,
18 | headers,
19 | body,
20 | });
21 | const responseHeaders: Record = {};
22 | response.headers.forEach((value, key) => {
23 | responseHeaders[key] = value;
24 | });
25 | return {
26 | type: "core",
27 | headers: responseHeaders,
28 | statusCode: response.status,
29 | isBase64Encoded: true,
30 | body: response.body ?? emptyReadableStream(),
31 | };
32 | },
33 | };
34 |
35 | export default fetchProxy;
36 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/queue/direct.ts:
--------------------------------------------------------------------------------
1 | import type { Queue } from "types/overrides.js";
2 |
3 | const queue: Queue = {
4 | name: "dev-queue",
5 | send: async (message) => {
6 | const prerenderManifest = (await import("../../adapters/config/index.js"))
7 | .PrerenderManifest as any;
8 | const { host, url } = message.MessageBody;
9 | const protocol = host.includes("localhost") ? "http" : "https";
10 | const revalidateId: string = prerenderManifest.preview.previewModeId;
11 | await globalThis.internalFetch(`${protocol}://${host}${url}`, {
12 | method: "HEAD",
13 | headers: {
14 | "x-prerender-revalidate": revalidateId,
15 | "x-isr": "1",
16 | },
17 | });
18 | },
19 | };
20 |
21 | export default queue;
22 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/queue/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { Queue } from "types/overrides";
2 | import { FatalError } from "utils/error";
3 |
4 | const dummyQueue: Queue = {
5 | name: "dummy",
6 | send: async () => {
7 | throw new FatalError("Dummy queue is not implemented");
8 | },
9 | };
10 |
11 | export default dummyQueue;
12 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/queue/sqs.ts:
--------------------------------------------------------------------------------
1 | import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
2 | import type { Queue } from "types/overrides";
3 |
4 | import { awsLogger } from "../../adapters/logger";
5 |
6 | // Expected environment variables
7 | const { REVALIDATION_QUEUE_REGION, REVALIDATION_QUEUE_URL } = process.env;
8 |
9 | const sqsClient = new SQSClient({
10 | region: REVALIDATION_QUEUE_REGION,
11 | logger: awsLogger,
12 | });
13 |
14 | const queue: Queue = {
15 | send: async ({ MessageBody, MessageDeduplicationId, MessageGroupId }) => {
16 | await sqsClient.send(
17 | new SendMessageCommand({
18 | QueueUrl: REVALIDATION_QUEUE_URL,
19 | MessageBody: JSON.stringify(MessageBody),
20 | MessageDeduplicationId,
21 | MessageGroupId,
22 | }),
23 | );
24 | },
25 | name: "sqs",
26 | };
27 |
28 | export default queue;
29 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/tagCache/constants.ts:
--------------------------------------------------------------------------------
1 | export const MAX_DYNAMO_BATCH_WRITE_ITEM_COUNT = 25;
2 |
3 | /**
4 | * Sending to dynamo X commands at a time, using about X * 25 write units per batch to not overwhelm DDB
5 | * and give production plenty of room to work with. With DDB Response times, you can expect about 10 batches per second.
6 | */
7 | const DEFAULT_DYNAMO_BATCH_WRITE_COMMAND_CONCURRENCY = 4;
8 |
9 | export const getDynamoBatchWriteCommandConcurrency = (): number => {
10 | const dynamoBatchWriteCommandConcurrencyFromEnv =
11 | process.env.DYNAMO_BATCH_WRITE_COMMAND_CONCURRENCY;
12 | const parsedDynamoBatchWriteCommandConcurrencyFromEnv =
13 | dynamoBatchWriteCommandConcurrencyFromEnv
14 | ? Number.parseInt(dynamoBatchWriteCommandConcurrencyFromEnv)
15 | : undefined;
16 |
17 | if (
18 | parsedDynamoBatchWriteCommandConcurrencyFromEnv &&
19 | !Number.isNaN(parsedDynamoBatchWriteCommandConcurrencyFromEnv)
20 | ) {
21 | return parsedDynamoBatchWriteCommandConcurrencyFromEnv;
22 | }
23 |
24 | return DEFAULT_DYNAMO_BATCH_WRITE_COMMAND_CONCURRENCY;
25 | };
26 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/tagCache/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { TagCache } from "types/overrides";
2 |
3 | // We don't want to throw error on this one because we might use it when we don't need tag cache
4 | const dummyTagCache: TagCache = {
5 | name: "dummy",
6 | mode: "original",
7 | getByPath: async () => {
8 | return [];
9 | },
10 | getByTag: async () => {
11 | return [];
12 | },
13 | getLastModified: async (_: string, lastModified) => {
14 | return lastModified ?? Date.now();
15 | },
16 | writeTags: async () => {
17 | return;
18 | },
19 | };
20 |
21 | export default dummyTagCache;
22 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/warmer/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { Warmer } from "types/overrides";
2 | import { FatalError } from "utils/error";
3 |
4 | const dummyWarmer: Warmer = {
5 | name: "dummy",
6 | invoke: async (_: string) => {
7 | throw new FatalError("Dummy warmer is not implemented");
8 | },
9 | };
10 |
11 | export default dummyWarmer;
12 |
--------------------------------------------------------------------------------
/packages/open-next/src/overrides/wrappers/dummy.ts:
--------------------------------------------------------------------------------
1 | import type { InternalEvent } from "types/open-next";
2 | import type {
3 | OpenNextHandlerOptions,
4 | Wrapper,
5 | WrapperHandler,
6 | } from "types/overrides";
7 |
8 | const dummyWrapper: WrapperHandler = async (handler, converter) => {
9 | return async (event: InternalEvent, options?: OpenNextHandlerOptions) => {
10 | return await handler(event, options);
11 | };
12 | };
13 |
14 | export default {
15 | name: "dummy",
16 | wrapper: dummyWrapper,
17 | supportStreaming: true,
18 | } satisfies Wrapper;
19 |
--------------------------------------------------------------------------------
/packages/open-next/src/plugins/externalMiddleware.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from "esbuild";
2 | import { getCrossPlatformPathRegex } from "utils/regex.js";
3 |
4 | export function openNextExternalMiddlewarePlugin(functionPath: string): Plugin {
5 | return {
6 | name: "open-next-external-node-middleware",
7 | setup(build) {
8 | // If we bundle the routing, we need to resolve the middleware
9 | build.onResolve(
10 | { filter: getCrossPlatformPathRegex("./middleware.mjs") },
11 | () => ({ path: functionPath }),
12 | );
13 | },
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/packages/open-next/src/types/aws-lambda.ts:
--------------------------------------------------------------------------------
1 | import type { Writable } from "node:stream";
2 |
3 | import type {
4 | APIGatewayProxyEvent,
5 | APIGatewayProxyEventV2,
6 | APIGatewayProxyResult,
7 | APIGatewayProxyResultV2,
8 | CloudFrontRequestEvent,
9 | CloudFrontRequestResult,
10 | Context,
11 | } from "aws-lambda";
12 | import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";
13 |
14 | export interface ResponseStream extends Writable {
15 | getBufferedData(): Buffer;
16 | setContentType(contentType: string): void;
17 | }
18 |
19 | type Handler = (
20 | event: APIGatewayProxyEventV2,
21 | responseStream: ResponseStream,
22 | context: Context,
23 | ) => Promise;
24 |
25 | interface Metadata {
26 | statusCode: number;
27 | headers: Record;
28 | }
29 |
30 | declare global {
31 | namespace awslambda {
32 | function streamifyResponse(handler: Handler): Handler;
33 | namespace HttpResponseStream {
34 | function from(res: Writable, metadata: Metadata): ResponseStream;
35 | }
36 | }
37 | }
38 |
39 | export type AwsLambdaEvent =
40 | | APIGatewayProxyEventV2
41 | | CloudFrontRequestEvent
42 | | APIGatewayProxyEvent
43 | | WarmerEvent;
44 |
45 | export type AwsLambdaReturn =
46 | | APIGatewayProxyResultV2
47 | | APIGatewayProxyResult
48 | | CloudFrontRequestResult
49 | | WarmerResponse;
50 |
--------------------------------------------------------------------------------
/packages/open-next/src/utils/cache.ts:
--------------------------------------------------------------------------------
1 | import type { CacheValue, WithLastModified } from "types/overrides";
2 |
3 | export async function hasBeenRevalidated(
4 | key: string,
5 | tags: string[],
6 | cacheEntry: WithLastModified>,
7 | ): Promise {
8 | if (globalThis.openNextConfig.dangerous?.disableTagCache) {
9 | return false;
10 | }
11 | const value = cacheEntry.value;
12 | if (!value) {
13 | // We should never reach this point
14 | return true;
15 | }
16 | if ("type" in cacheEntry && cacheEntry.type === "page") {
17 | return false;
18 | }
19 | const lastModified = cacheEntry.lastModified ?? Date.now();
20 | if (globalThis.tagCache.mode === "nextMode") {
21 | return await globalThis.tagCache.hasBeenRevalidated(tags, lastModified);
22 | }
23 | // TODO: refactor this, we should introduce a new method in the tagCache interface so that both implementations use hasBeenRevalidated
24 | const _lastModified = await globalThis.tagCache.getLastModified(
25 | key,
26 | lastModified,
27 | );
28 | return _lastModified === -1;
29 | }
30 |
31 | export function getTagsFromValue(value?: CacheValue<"cache">) {
32 | if (!value) {
33 | return [];
34 | }
35 | // The try catch is necessary for older version of next.js that may fail on this
36 | try {
37 | return value.meta?.headers?.["x-next-cache-tags"]?.split(",") ?? [];
38 | } catch (e) {
39 | return [];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/open-next/src/utils/fetch.ts:
--------------------------------------------------------------------------------
1 | import type { AwsClient } from "aws4fetch";
2 | // Because of Next take on fetch, and until it's fixed in Next 15 we have to pass some internals stuff to next for every fetch we do with aws4fetch
3 | // For some reason passing this directly to client.fetch doesn't work
4 | export function customFetchClient(client: AwsClient) {
5 | return async (input: RequestInfo, init: RequestInit) => {
6 | const signed = await client.sign(input, init);
7 | const headers: Record = {};
8 | signed.headers.forEach((value, key) => {
9 | headers[key] = value;
10 | });
11 | const response = await globalThis.internalFetch(signed.url, {
12 | method: signed.method,
13 | headers,
14 | body: init.body,
15 | });
16 | /**
17 | * Response body must be consumed to avoid socket error.
18 | * This is necessary otherwise we get some error : SocketError: other side closed
19 | * https://github.com/nodejs/undici/issues/583#issuecomment-855384858
20 | */
21 | const clonedResponse = response.clone();
22 | return clonedResponse;
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/packages/open-next/src/utils/lru.ts:
--------------------------------------------------------------------------------
1 | export class LRUCache {
2 | private cache: Map = new Map();
3 |
4 | constructor(private maxSize: number) {}
5 |
6 | get(key: string) {
7 | const result = this.cache.get(key);
8 | // We could have used .has to allow for nullish value to be stored but we don't need that right now
9 | if (result) {
10 | // By removing and setting the key again we ensure it's the most recently used
11 | this.cache.delete(key);
12 | this.cache.set(key, result);
13 | }
14 | return result;
15 | }
16 |
17 | set(key: string, value: any) {
18 | if (this.cache.size >= this.maxSize) {
19 | const firstKey = this.cache.keys().next().value;
20 | if (firstKey !== undefined) {
21 | this.cache.delete(firstKey);
22 | }
23 | }
24 | this.cache.set(key, value);
25 | }
26 |
27 | delete(key: string) {
28 | this.cache.delete(key);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/open-next/src/utils/normalize-path.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 |
3 | export function normalizePath(path: string) {
4 | return path.replace(/\\/g, "/");
5 | }
6 |
7 | // See: https://github.com/vercel/next.js/blob/3ecf087f10fdfba4426daa02b459387bc9c3c54f/packages/next/src/shared/lib/utils.ts#L348
8 | export function normalizeRepeatedSlashes(url: URL) {
9 | const urlNoQuery = url.host + url.pathname;
10 | return `${url.protocol}//${urlNoQuery
11 | .replace(/\\/g, "/")
12 | .replace(/\/\/+/g, "/")}${url.search}`;
13 | }
14 |
15 | export function getMonorepoRelativePath(relativePath = "../.."): string {
16 | return path.join(
17 | globalThis.monorepoPackagePath
18 | .split("/")
19 | .filter(Boolean)
20 | .map(() => "..")
21 | .join("/"),
22 | relativePath,
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/packages/open-next/src/utils/regex.ts:
--------------------------------------------------------------------------------
1 | type Options = {
2 | escape?: boolean;
3 | flags?: string;
4 | };
5 |
6 | /**
7 | * Constructs a regular expression for a path that supports separators for multiple platforms
8 | * - Uses posix separators (`/`) as the input that should be made cross-platform.
9 | * - Special characters are escaped by default but can be controlled through opts.escape.
10 | * - Posix separators are always escaped.
11 | *
12 | * @example
13 | * ```ts
14 | * getCrossPlatformPathRegex("./middleware.mjs")
15 | * getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false })
16 | * ```
17 | */
18 | export function getCrossPlatformPathRegex(
19 | regex: string,
20 | { escape: shouldEscape = true, flags = "" }: Options = {},
21 | ) {
22 | const newExpr = (
23 | shouldEscape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex
24 | ).replaceAll("/", String.raw`(?:\/|\\)`);
25 |
26 | return new RegExp(newExpr, flags);
27 | }
28 |
--------------------------------------------------------------------------------
/packages/open-next/src/utils/stream.ts:
--------------------------------------------------------------------------------
1 | import { Readable } from "node:stream";
2 | import type { ReadableStream } from "node:stream/web";
3 |
4 | export function fromReadableStream(
5 | stream: ReadableStream,
6 | base64?: boolean,
7 | ): Promise {
8 | const reader = stream.getReader();
9 | const chunks: Uint8Array[] = [];
10 |
11 | return new Promise((resolve, reject) => {
12 | function pump() {
13 | reader
14 | .read()
15 | .then(({ done, value }) => {
16 | if (done) {
17 | resolve(Buffer.concat(chunks).toString(base64 ? "base64" : "utf8"));
18 | return;
19 | }
20 | chunks.push(value);
21 | pump();
22 | })
23 | .catch(reject);
24 | }
25 | pump();
26 | });
27 | }
28 |
29 | export function toReadableStream(
30 | value: string,
31 | isBase64?: boolean,
32 | ): ReadableStream {
33 | return Readable.toWeb(
34 | Readable.from(Buffer.from(value, isBase64 ? "base64" : "utf8")),
35 | );
36 | }
37 |
38 | export function emptyReadableStream(): ReadableStream {
39 | if (process.env.OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE === "true") {
40 | return Readable.toWeb(Readable.from([Buffer.from("SOMETHING")]));
41 | }
42 | return Readable.toWeb(Readable.from([]));
43 | }
44 |
--------------------------------------------------------------------------------
/packages/open-next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "module": "esnext",
6 | "lib": ["DOM", "ESNext"],
7 | "outDir": "./dist",
8 | "allowSyntheticDefaultImports": true,
9 | "paths": {
10 | "types/*": ["./src/types/*"],
11 | "config/*": ["./src/adapters/config/*"],
12 | "http/*": ["./src/http/*"],
13 | "utils/*": ["./src/utils/*"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/tests-e2e/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # tests-e2e
2 |
3 | ## null
4 |
5 | ### Patch Changes
6 |
7 | - c80f1be: Fix trailing slash redirect to external domain
8 |
--------------------------------------------------------------------------------
/packages/tests-e2e/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tests-e2e",
3 | "private": true,
4 | "scripts": {
5 | "e2e:dev": "playwright test --headed",
6 | "e2e:test": "playwright test --retries=5",
7 | "clean": "rm -rf .turbo && rm -rf node_modules"
8 | },
9 | "dependencies": {},
10 | "devDependencies": {
11 | "@playwright/test": "1.49.1",
12 | "start-server-and-test": "2.0.0",
13 | "ts-node": "10.9.1"
14 | },
15 | "version": null
16 | }
17 |
--------------------------------------------------------------------------------
/packages/tests-e2e/playwright.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@playwright/test";
2 |
3 | export default defineConfig({
4 | projects: [
5 | {
6 | name: "appRouter",
7 | testMatch: ["tests/appRouter/*.test.ts"],
8 | use: {
9 | baseURL: process.env.APP_ROUTER_URL || "http://localhost:3001",
10 | },
11 | },
12 | {
13 | name: "pagesRouter",
14 | testMatch: ["tests/pagesRouter/*.test.ts"],
15 | use: {
16 | baseURL: process.env.PAGES_ROUTER_URL || "http://localhost:3002",
17 | },
18 | },
19 | {
20 | name: "appPagesRouter",
21 | testMatch: ["tests/appPagesRouter/*.test.ts"],
22 | use: {
23 | baseURL: process.env.APP_PAGES_ROUTER_URL || "http://localhost:3003",
24 | },
25 | },
26 | {
27 | name: "experimental",
28 | testMatch: ["tests/experimental/*.test.ts"],
29 | use: {
30 | baseURL: process.env.EXPERIMENTAL_APP_URL || "http://localhost:3004",
31 | },
32 | },
33 | ],
34 | });
35 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/api.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("API call from client", async ({ page }) => {
4 | await page.goto("/");
5 | await page.locator('[href="/api"]').click();
6 |
7 | await page.waitForURL("/api");
8 |
9 | let el = page.getByText("API: N/A");
10 | await expect(el).toBeVisible();
11 |
12 | await page.getByRole("button", { name: "Call /api/client" }).click();
13 | el = page.getByText('API: { "hello": "client" }');
14 | await expect(el).toBeVisible();
15 | });
16 |
17 | test("API call from middleware", async ({ page }) => {
18 | await page.goto("/");
19 | await page.getByRole("link", { name: "/API" }).click();
20 |
21 | await page.waitForURL("/api");
22 |
23 | let el = page.getByText("API: N/A");
24 | await expect(el).toBeVisible();
25 |
26 | await page.getByRole("button", { name: "Call /api/middleware" }).click();
27 | el = page.getByText('API: { "hello": "middleware" }');
28 | await expect(el).toBeVisible();
29 | });
30 |
31 | test("API call from middleware with top-level await", async ({ request }) => {
32 | const response = await request.get("/api/middlewareTopLevelAwait");
33 | const data = await response.json();
34 | expect(data).toEqual({ hello: "top-level-await" });
35 | });
36 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/host.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | /**
4 | * Tests that the request.url is the deployed host and not localhost
5 | */
6 | test("Request.url is host", async ({ baseURL, page }) => {
7 | await page.goto("/api/host");
8 |
9 | const el = page.getByText(`{"url":"${baseURL}/api/host"}`);
10 | await expect(el).toBeVisible();
11 | });
12 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Image Optimization", async ({ page }) => {
4 | await page.goto("/");
5 |
6 | const imageResponsePromise = page.waitForResponse(
7 | /corporate_holiday_card.jpg/,
8 | );
9 | await page.locator('[href="/image-optimization"]').click();
10 | const imageResponse = await imageResponsePromise;
11 |
12 | await page.waitForURL("/image-optimization");
13 |
14 | const imageContentType = imageResponse.headers()["content-type"];
15 | expect(imageContentType).toBe("image/webp");
16 |
17 | const el = page.locator("img");
18 | await expect(el).toHaveJSProperty("complete", true);
19 | await expect(el).not.toHaveJSProperty("naturalWidth", 0);
20 | });
21 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/isr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Incremental Static Regeneration", async ({ page }) => {
4 | test.setTimeout(60000);
5 | await page.goto("/");
6 | await page.locator('[href="/isr"]').click();
7 | // Load the page a couple times to regenerate ISR
8 |
9 | let el = page.getByText("Time:");
10 | // Track the static time
11 | let time = await el.textContent();
12 | let newTime: typeof time;
13 | let tempTime = time;
14 | do {
15 | await page.waitForTimeout(1000);
16 | await page.reload();
17 | time = tempTime;
18 | el = page.getByText("Time:");
19 | newTime = await el.textContent();
20 | tempTime = newTime;
21 | } while (time !== newTime);
22 | await page.reload();
23 |
24 | await page.waitForTimeout(1000);
25 | el = page.getByText("Time:");
26 | const midTime = await el.textContent();
27 | // Expect that the time is still stale
28 | expect(midTime).toEqual(newTime);
29 |
30 | // Wait 10 + 1 seconds for ISR to regenerate time
31 | await page.waitForTimeout(11000);
32 | let finalTime = newTime;
33 | do {
34 | await page.waitForTimeout(2000);
35 | el = page.getByText("Time:");
36 | finalTime = await el.textContent();
37 | await page.reload();
38 | } while (newTime === finalTime);
39 |
40 | expect(newTime).not.toEqual(finalTime);
41 | });
42 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/middleware.redirect.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Middleware Redirect", async ({ page, context }) => {
4 | await page.goto("/");
5 | await page.locator('[href="/redirect"]').click();
6 |
7 | // URL is immediately redirected
8 | await page.waitForURL("/redirect-destination");
9 | let el = page.getByText("Redirect Destination", { exact: true });
10 | await expect(el).toBeVisible();
11 |
12 | // Loading page should also redirect
13 | await page.goto("/redirect");
14 | await page.waitForURL("/redirect-destination");
15 | expect(
16 | await context
17 | .cookies()
18 | .then((res) => res.find((cookie) => cookie.name === "test")?.value),
19 | ).toBe("success");
20 | el = page.getByText("Redirect Destination", { exact: true });
21 | await expect(el).toBeVisible();
22 | });
23 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/middleware.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("should return correctly on HEAD request with an empty body", async ({
4 | request,
5 | }) => {
6 | const response = await request.head("/head");
7 | expect(response.status()).toBe(200);
8 | const body = await response.text();
9 | expect(body).toBe("");
10 | expect(response.headers()["x-from-middleware"]).toBe("true");
11 | });
12 |
13 | test("should return correctly for directly returning a fetch response", async ({
14 | request,
15 | }) => {
16 | const response = await request.get("/fetch");
17 | expect(response.status()).toBe(200);
18 | const body = await response.json();
19 | expect(body).toEqual({ hello: "world" });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/modals.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Route modal and interception", async ({ page }) => {
4 | await page.goto("/");
5 | await page.getByRole("link", { name: "Albums" }).click();
6 | await page
7 | .getByRole("link", { name: "Song: I'm never gonna give you up Year: 1965" })
8 | .click();
9 |
10 | await page.waitForURL(
11 | `/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`,
12 | );
13 |
14 | const modal = page.getByText("Modal", { exact: true });
15 | await expect(modal).toBeVisible();
16 |
17 | // Reload the page to load non intercepted modal
18 | await page.reload();
19 | await page.waitForURL(
20 | `/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`,
21 | );
22 | const notModal = page.getByText("Not Modal", { exact: true });
23 | await expect(notModal).toBeVisible();
24 | });
25 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/pages_isr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Incremental Static Regeneration", async ({ page }) => {
4 | test.setTimeout(60000);
5 | await page.goto("/");
6 | await page.locator('[href="/pages_isr"]').click();
7 |
8 | await page.waitForURL("/pages_isr");
9 | // Load the page a couple times to regenerate ISR
10 |
11 | let el = page.getByText("Time:");
12 | // Track the static time
13 | let time = await el.textContent();
14 | let newTime: typeof time;
15 | let tempTime = time;
16 | do {
17 | await page.waitForTimeout(1000);
18 | await page.reload();
19 | time = tempTime;
20 | el = page.getByText("Time:");
21 | newTime = await el.textContent();
22 | tempTime = newTime;
23 | } while (time !== newTime);
24 | await page.reload();
25 | await page.waitForTimeout(1000);
26 | el = page.getByText("Time:");
27 | const midTime = await el.textContent();
28 | // Expect that the time is still stale
29 | expect(midTime).toEqual(newTime);
30 |
31 | // Wait 10 + 1 seconds for ISR to regenerate time
32 | await page.waitForTimeout(11000);
33 | let finalTime = newTime;
34 | do {
35 | await page.waitForTimeout(2000);
36 | el = page.getByText("Time:");
37 | finalTime = await el.textContent();
38 | await page.reload();
39 | } while (newTime === finalTime);
40 |
41 | expect(newTime).not.toEqual(finalTime);
42 | });
43 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/pages_ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Server Side Render", async ({ page }) => {
4 | await page.goto("/");
5 | await page.locator('[href="/pages_ssr"]').click();
6 |
7 | await page.waitForURL("/pages_ssr");
8 | let el = page.getByText("Time:");
9 | await expect(el).toBeVisible();
10 | let time = await el.textContent();
11 |
12 | await page.reload();
13 |
14 | el = page.getByText("Time:");
15 | let newTime = await el.textContent();
16 | await expect(el).toBeVisible();
17 |
18 | for (let i = 0; i < 5; i++) {
19 | await page.reload();
20 | el = page.getByText("Time:");
21 | newTime = await el.textContent();
22 | await expect(el).toBeVisible();
23 | expect(time).not.toEqual(newTime);
24 | time = newTime;
25 | await page.waitForTimeout(250);
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/serverActions.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Server Actions", async ({ page }) => {
4 | await page.goto("/");
5 | await page.locator('[href="/server-actions"]').click();
6 |
7 | await page.waitForURL("/server-actions");
8 | let el = page.getByText("Song: I'm never gonna give you up");
9 | await expect(el).not.toBeVisible();
10 |
11 | await page.getByRole("button", { name: "Fire Server Actions" }).click();
12 | el = page.getByText("Song: I'm never gonna give you up");
13 | await expect(el).toBeVisible();
14 |
15 | // Reload page
16 | await page.reload();
17 | el = page.getByText("Song: I'm never gonna give you up");
18 | await expect(el).not.toBeVisible();
19 | await page.getByRole("button", { name: "Fire Server Actions" }).click();
20 | el = page.getByText("Song: I'm never gonna give you up");
21 | await expect(el).toBeVisible();
22 | });
23 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/skip_trailing.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("skipTrailingSlashRedirect redirect", async ({ page }) => {
4 | const response = await page.goto("/ssr");
5 |
6 | expect(response?.request().redirectedFrom()).toBeNull();
7 | expect(response?.request().url()).toMatch(/\/ssr$/);
8 | });
9 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appPagesRouter/ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Server Side Render", async ({ page }) => {
4 | await page.goto("/");
5 | await page.locator('[href="/ssr"]').click();
6 |
7 | await page.waitForURL("/ssr");
8 | let el = page.getByText("Time:");
9 | await expect(el).toBeVisible();
10 | let time = await el.textContent();
11 |
12 | await page.reload();
13 |
14 | el = page.getByText("Time:");
15 | let newTime = await el.textContent();
16 | await expect(el).toBeVisible();
17 |
18 | for (let i = 0; i < 5; i++) {
19 | await page.reload();
20 | el = page.getByText("Time:");
21 | newTime = await el.textContent();
22 | await expect(el).toBeVisible();
23 | expect(time).not.toEqual(newTime);
24 | time = newTime;
25 | await page.waitForTimeout(250);
26 | }
27 | });
28 |
29 | test("Server Side Render with env", async ({ page }) => {
30 | await page.goto("/ssr");
31 | const el = page.getByText("Env:");
32 | expect(await el.textContent()).toEqual("Env: foo");
33 | });
34 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/api.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("API call from client", async ({ page }) => {
4 | await page.goto("/");
5 | await page.getByRole("link", { name: "/API" }).click();
6 |
7 | await page.waitForURL("/api");
8 |
9 | let el = page.getByText("API: N/A");
10 | await expect(el).toBeVisible();
11 |
12 | await page.getByRole("button", { name: "Call /api/client" }).click();
13 | el = page.getByText('API: { "hello": "client" }');
14 | await expect(el).toBeVisible();
15 | });
16 |
17 | test("API call from middleware", async ({ page }) => {
18 | await page.goto("/");
19 | await page.getByRole("link", { name: "/API" }).click();
20 |
21 | await page.waitForURL("/api");
22 |
23 | let el = page.getByText("API: N/A");
24 | await expect(el).toBeVisible();
25 |
26 | await page.getByRole("button", { name: "Call /api/middleware" }).click();
27 | el = page.getByText('API: { "hello": "middleware" }');
28 | await expect(el).toBeVisible();
29 | });
30 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/headers.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | /**
4 | * Tests that the headers are available in RSC and response headers
5 | */
6 | test("Headers", async ({ page }) => {
7 | const responsePromise = page.waitForResponse((response) => {
8 | return response.status() === 200;
9 | });
10 | await page.goto("/headers");
11 |
12 | const response = await responsePromise;
13 | // Response header should be set
14 | const headers = response.headers();
15 | expect(headers["response-header"]).toEqual("response-header");
16 |
17 | // The next.config.js headers should be also set in response
18 | expect(headers["e2e-headers"]).toEqual("next.config.js");
19 |
20 | // Request header should be available in RSC
21 | const el = page.getByText("request-header");
22 | await expect(el).toBeVisible();
23 |
24 | // Both these headers should not be present cause poweredByHeader is false in appRouter
25 | expect(headers["x-powered-by"]).toBeFalsy();
26 | expect(headers["x-opennext"]).toBeFalsy();
27 | });
28 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/host.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | /**
4 | * Tests that the request.url is the deployed host and not localhost
5 | */
6 | test("Request.url is host", async ({ baseURL, page }) => {
7 | await page.goto("/api/host");
8 |
9 | const el = page.getByText(`{"url":"${baseURL}/api/host"}`);
10 | await expect(el).toBeVisible();
11 | });
12 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/image-optimization.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Image Optimization", async ({ page }) => {
4 | await page.goto("/");
5 |
6 | const imageResponsePromise = page.waitForResponse(
7 | /https%3A%2F%2Fopennext.js.org%2Farchitecture.png/,
8 | );
9 | await page.locator('[href="/image-optimization"]').click();
10 | const imageResponse = await imageResponsePromise;
11 |
12 | await page.waitForURL("/image-optimization");
13 |
14 | const imageContentType = imageResponse.headers()["content-type"];
15 | expect(imageContentType).toBe("image/webp");
16 |
17 | const el = page.locator("img");
18 | await expect(el).toHaveJSProperty("complete", true);
19 | await expect(el).not.toHaveJSProperty("naturalWidth", 0);
20 | });
21 |
22 | test("should return 400 when validateParams returns an errorMessage", async ({
23 | request,
24 | }) => {
25 | const res = await request.get("/_next/image");
26 | expect(res.status()).toBe(400);
27 | expect(res.headers()["cache-control"]).toBe("public,max-age=60,immutable");
28 | expect(await res.text()).toBe(`"url" parameter is required`);
29 | });
30 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/isr.revalidate.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Test revalidate", async ({ request }) => {
4 | const result = await request.get("/api/isr");
5 |
6 | expect(result.status()).toEqual(200);
7 | const json = await result.json();
8 | const body = json.body;
9 |
10 | expect(json.status).toEqual(200);
11 | expect(body.result).toEqual(true);
12 | expect(body.cacheControl).toEqual(
13 | "private, no-cache, no-store, max-age=0, must-revalidate",
14 | );
15 | });
16 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/middleware.redirect.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Middleware Redirect", async ({ page, context }) => {
4 | await page.goto("/");
5 | await page.getByRole("link", { name: "/Redirect" }).click();
6 |
7 | // URL is immediately redirected
8 | await page.waitForURL("/redirect-destination");
9 | let el = page.getByText("Redirect Destination", { exact: true });
10 | await expect(el).toBeVisible();
11 |
12 | // Loading page should also redirect
13 | await page.goto("/redirect");
14 | await page.waitForURL("/redirect-destination");
15 | expect(
16 | await context
17 | .cookies()
18 | .then((res) => res.find((cookie) => cookie.name === "test")?.value),
19 | ).toBe("success");
20 | el = page.getByText("Redirect Destination", { exact: true });
21 | await expect(el).toBeVisible();
22 | });
23 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/middleware.rewrite.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 | import { validateMd5 } from "../utils";
3 |
4 | /*
5 | * `curl -s https://opennext.js.org/share.png | md5sum`
6 | * This is the MD5 hash of the image. It is used to validate the image content.
7 | */
8 | const OPENNEXT_PNG_MD5 = "405f45cc3397b09717a13ebd6f1e027b";
9 |
10 | test("Middleware Rewrite", async ({ page }) => {
11 | await page.goto("/");
12 | await page.getByRole("link", { name: "/Rewrite" }).click();
13 |
14 | await page.waitForURL("/rewrite");
15 | let el = page.getByText("Rewritten Destination", { exact: true });
16 | await expect(el).toBeVisible();
17 |
18 | // Loading page should also rewrite
19 | await page.goto("/rewrite");
20 | await page.waitForURL("/rewrite");
21 | el = page.getByText("Rewritten Destination", { exact: true });
22 | await expect(el).toBeVisible();
23 | });
24 |
25 | test("Middleware Rewrite External Image", async ({ page }) => {
26 | await page.goto("/rewrite-external");
27 | page.on("response", async (response) => {
28 | expect(response.status()).toBe(200);
29 | expect(response.headers()["content-type"]).toBe("image/png");
30 | expect(response.headers()["cache-control"]).toBe("max-age=600");
31 | const bodyBuffer = await response.body();
32 | expect(validateMd5(bodyBuffer, OPENNEXT_PNG_MD5)).toBe(true);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/modals.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Route modal and interception", async ({ page }) => {
4 | await page.goto("/");
5 | await page.getByRole("link", { name: "Albums" }).click();
6 | await page
7 | .getByRole("link", { name: "Song: I'm never gonna give you up Year: 1965" })
8 | .click();
9 |
10 | await page.waitForURL(
11 | `/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`,
12 | );
13 |
14 | const modal = page.getByText("Modal", { exact: true });
15 | await expect(modal).toBeVisible();
16 |
17 | // Reload the page to load non intercepted modal
18 | await page.reload();
19 | await page.waitForURL(
20 | `/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`,
21 | );
22 | const notModal = page.getByText("Not Modal", { exact: true });
23 | await expect(notModal).toBeVisible();
24 | });
25 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/query.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | /**
4 | * Tests that query params are available in middleware and RSC
5 | */
6 | test("SearchQuery", async ({ page }) => {
7 | await page.goto("/search-query?searchParams=e2etest&multi=one&multi=two");
8 |
9 | const propsEl = page.getByText("Search Params via Props: e2etest");
10 | const mwEl = page.getByText("Search Params via Middleware: mw/e2etest");
11 | const multiEl = page.getByText("Multi-value Params (key: multi): 2");
12 | const multiOne = page.getByText("one");
13 | const multiTwo = page.getByText("two");
14 | await expect(propsEl).toBeVisible();
15 | await expect(mwEl).toBeVisible();
16 | await expect(multiEl).toBeVisible();
17 | await expect(multiOne).toBeVisible();
18 | await expect(multiTwo).toBeVisible();
19 | });
20 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/serverActions.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Server Actions", async ({ page }) => {
4 | await page.goto("/");
5 | await page.getByRole("link", { name: "Server Actions" }).click();
6 |
7 | await page.waitForURL("/server-actions");
8 | let el = page.getByText("Song: I'm never gonna give you up");
9 | await expect(el).not.toBeVisible();
10 |
11 | await page.getByRole("button", { name: "Fire Server Actions" }).click();
12 | el = page.getByText("Song: I'm never gonna give you up");
13 | await expect(el).toBeVisible();
14 |
15 | // Reload page
16 | await page.reload();
17 | el = page.getByText("Song: I'm never gonna give you up");
18 | await expect(el).not.toBeVisible();
19 | await page.getByRole("button", { name: "Fire Server Actions" }).click();
20 | el = page.getByText("Song: I'm never gonna give you up");
21 | await expect(el).toBeVisible();
22 | });
23 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | // NOTE: We don't await page load b/c we want to see the Loading page
4 | test("Server Side Render and loading.tsx", async ({ page }) => {
5 | test.setTimeout(600000);
6 | await page.goto("/");
7 | await page.getByRole("link", { name: "SSR" }).click();
8 | await page.waitForURL("/ssr");
9 |
10 | let loading: any;
11 | let lastTime = "";
12 |
13 | for (let i = 0; i < 5; i++) {
14 | void page.reload();
15 |
16 | loading = page.getByText("Loading...");
17 | await expect(loading).toBeVisible();
18 | const el = page.getByText("Time:");
19 | await expect(el).toBeVisible();
20 | const time = await el.textContent();
21 | expect(time).not.toEqual(lastTime);
22 | lastTime = time!;
23 | await page.waitForTimeout(1000);
24 | }
25 | });
26 |
27 | test("Fetch cache properly cached", async ({ page }) => {
28 | await page.goto("/ssr");
29 | const originalDate = await page.getByText("Cached fetch:").textContent();
30 | await page.waitForTimeout(2000);
31 | await page.reload();
32 | const newDate = await page.getByText("Cached fetch:").textContent();
33 | expect(originalDate).toEqual(newDate);
34 | });
35 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/appRouter/trailing.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("trailingSlash redirect", async ({ page }) => {
4 | const response = await page.goto("/ssr/");
5 |
6 | expect(response?.request().redirectedFrom()?.url()).toMatch(/\/ssr\/$/);
7 | expect(response?.request().url()).toMatch(/\/ssr$/);
8 | });
9 |
10 | test("trailingSlash redirect with search parameters", async ({ page }) => {
11 | const response = await page.goto("/ssr/?happy=true");
12 |
13 | expect(response?.request().redirectedFrom()?.url()).toMatch(
14 | /\/ssr\/\?happy=true$/,
15 | );
16 | expect(response?.request().url()).toMatch(/\/ssr\?happy=true$/);
17 | });
18 |
19 | test("trailingSlash redirect to external domain", async ({ page, baseURL }) => {
20 | const response = await page.goto(`${baseURL}//sst.dev/`);
21 | expect(response?.status()).toBe(404);
22 | });
23 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/experimental/nodeMiddleware.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test.describe("Node Middleware", () => {
4 | test("Node middleware should add headers", async ({ request }) => {
5 | const resp = await request.get("/");
6 | expect(resp.status()).toEqual(200);
7 | const headers = resp.headers();
8 | expect(headers["x-middleware-test"]).toEqual("1");
9 | expect(headers["x-random-node"]).toBeDefined();
10 | });
11 |
12 | test("Node middleware should return json", async ({ request }) => {
13 | const resp = await request.get("/api/hello");
14 | expect(resp.status()).toEqual(200);
15 | const json = await resp.json();
16 | expect(json).toEqual({ name: "World" });
17 | });
18 |
19 | test("Node middleware should redirect", async ({ page }) => {
20 | await page.goto("/redirect");
21 | await page.waitForURL("/");
22 | const el = page.getByText("Incremental PPR");
23 | await expect(el).toBeVisible();
24 | });
25 |
26 | test("Node middleware should rewrite", async ({ page }) => {
27 | await page.goto("/rewrite");
28 | const el = page.getByText("Incremental PPR");
29 | await expect(el).toBeVisible();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/experimental/ppr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test.describe("PPR", () => {
4 | test("PPR should show loading first", async ({ page }) => {
5 | await page.goto("/");
6 | await page.getByRole("link", { name: "Incremental PPR" }).click();
7 | await page.waitForURL("/ppr");
8 | const loading = page.getByText("Loading...");
9 | await expect(loading).toBeVisible();
10 | const el = page.getByText("Dynamic Component");
11 | await expect(el).toBeVisible();
12 | });
13 |
14 | test("PPR rsc prefetch request should be cached", async ({ request }) => {
15 | const resp = await request.get("/ppr", {
16 | headers: { rsc: "1", "next-router-prefetch": "1" },
17 | });
18 | expect(resp.status()).toEqual(200);
19 | const headers = resp.headers();
20 | expect(headers["x-nextjs-postponed"]).toEqual("1");
21 | expect(headers["x-nextjs-cache"]).toEqual("HIT");
22 | expect(headers["cache-control"]).toEqual("s-maxage=31536000");
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/404.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("should return 404 on a route not corresponding to any route", async ({
4 | page,
5 | }) => {
6 | const result = await page.goto("/not-existing/route");
7 | expect(result).toBeDefined();
8 | expect(result?.status()).toBe(404);
9 | const headers = result?.headers();
10 | expect(headers?.["cache-control"]).toBe(
11 | "private, no-cache, no-store, max-age=0, must-revalidate",
12 | );
13 | });
14 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/amp.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test.describe("next/amp", () => {
4 | test("should load and display the timeago component", async ({ page }) => {
5 | await page.goto("/amp");
6 | const timeago = await page.getByTestId("amp-timeago").textContent();
7 | // We can safely assume this will always show `just now` as its using `format()` from `timeago.js`.
8 | // It will show `just now` if the time is less than 10s ago.
9 | expect(timeago).toBe("just now");
10 | const htmlEl = page.locator("html");
11 | await expect(htmlEl).toHaveAttribute("amp");
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/catch-all-optional.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | // Going to `/`, `/conico974`, `/kheuzy` and `/sommeeer` should be catched by our `[[...page]]` route.
4 | // Also the /super/long/path/to/secret/page should be pregenerated by the `getStaticPaths` function.
5 | test.describe("Catch-all optional route in root should work", () => {
6 | test("should be possible to visit home and a pregenerated subpage", async ({
7 | page,
8 | }) => {
9 | await page.goto("/");
10 | await page.locator("h1").getByText("Pages Router").isVisible();
11 |
12 | await page.goto("/conico974");
13 | const pElement = page.getByText("Path: conico974", { exact: true });
14 | await pElement.isVisible();
15 | });
16 | test("should be possible to visit a long pregenerated path", async ({
17 | page,
18 | }) => {
19 | await page.goto("/super/long/path/to/secret/page");
20 | const h1Text = await page.getByTestId("page").textContent();
21 | expect(h1Text).toBe("Page: super,long,path,to,secret,page");
22 | });
23 | test("should be possible to request an API route when you have a catch-all in root", async ({
24 | request,
25 | }) => {
26 | const response = await request.get("/api/hello");
27 | expect(response.status()).toBe(200);
28 | const body = await response.json();
29 | expect(body).toEqual({ hello: "OpenNext rocks!" });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/data.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("fix _next/data", async ({ page }) => {
4 | await page.goto("/");
5 |
6 | const isrJson = page.waitForResponse("/_next/data/*/en/isr.json");
7 | await page.locator('[href="/isr/"]').click();
8 | const response = await isrJson;
9 | expect(response.ok()).toBe(true);
10 | expect(response.request().url()).toMatch(/\/_next\/data\/.*\/en\/isr\.json$/);
11 | await page.waitForURL("/isr/");
12 |
13 | const homeJson = page.waitForResponse("/_next/data/*/en.json");
14 | await page.locator('[href="/"]').click();
15 | const response2 = await homeJson;
16 | expect(response2.ok()).toBe(true);
17 | expect(response2.request().url()).toMatch(/\/_next\/data\/.*\/en\.json$/);
18 | await page.waitForURL("/");
19 | const body = await response2.json();
20 | expect(body).toEqual({
21 | pageProps: { subpage: [], pageType: "home" },
22 | __N_SSG: true,
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/head.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test.describe("next/head", () => {
4 | test("should have the correct title", async ({ page }) => {
5 | await page.goto("/head");
6 | const title = await page.title();
7 | expect(title).toBe("OpenNext head");
8 | });
9 | test("should have the correct meta tags", async ({ page }) => {
10 | await page.goto("/head");
11 | const ogTitle = await page
12 | .locator('meta[property="og:title"]')
13 | .getAttribute("content");
14 | const ogDesc = await page
15 | .locator('meta[name="description"]')
16 | .getAttribute("content");
17 | const time = await page
18 | .locator('meta[property="time"]')
19 | .getAttribute("content");
20 | expect(ogTitle).toBe("OpenNext pages router head bar");
21 | expect(ogDesc).toBe(
22 | "OpenNext takes the Next.js build output and converts it into packages that can be deployed across a variety of environments. Natively OpenNext has support for AWS Lambda, Cloudflare, and classic Node.js Server.",
23 | );
24 |
25 | expect(new Date(time!).getTime()).toBeLessThan(Date.now());
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/header.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("should test if poweredByHeader adds the correct headers ", async ({
4 | page,
5 | }) => {
6 | const result = await page.goto("/");
7 | expect(result).toBeDefined();
8 | expect(result?.status()).toBe(200);
9 | const headers = result?.headers();
10 |
11 | // Both these headers should be present cause poweredByHeader is true in pagesRouter
12 | expect(headers?.["x-powered-by"]).toBe("Next.js");
13 | expect(headers?.["x-opennext"]).toBe("1");
14 | });
15 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/i18n.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Next config headers with i18n", async ({ page }) => {
4 | const responsePromise = page.waitForResponse((response) => {
5 | return response.status() === 200;
6 | });
7 | await page.goto("/");
8 |
9 | const response = await responsePromise;
10 | // Response header should be set
11 | const headers = response.headers();
12 | // Headers from next.config.js should be set
13 | expect(headers["x-custom-header"]).toEqual("my custom header value");
14 |
15 | // Headers from middleware should be set
16 | expect(headers["x-from-middleware"]).toEqual("true");
17 | });
18 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/isr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Incremental Static Regeneration", async ({ page }) => {
4 | test.setTimeout(45000);
5 | await page.goto("/");
6 | await page.locator("[href='/isr/']").click();
7 | await page.waitForURL("/isr/");
8 | // Load the page a couple times to regenerate ISR
9 |
10 | let el = page.getByText("Time:");
11 | // Track the static time
12 | let time = await el.textContent();
13 | let newTime: typeof time;
14 | let tempTime = time;
15 | do {
16 | await page.waitForTimeout(1000);
17 | await page.reload();
18 | time = tempTime;
19 | el = page.getByText("Time:");
20 | newTime = await el.textContent();
21 | tempTime = newTime;
22 | } while (time !== newTime);
23 | await page.reload();
24 | await page.waitForTimeout(1000);
25 | el = page.getByText("Time:");
26 | const midTime = await el.textContent();
27 | // Expect that the time is still stale
28 | expect(midTime).toEqual(newTime);
29 |
30 | // Wait 10 + 1 seconds for ISR to regenerate time
31 | await page.waitForTimeout(11000);
32 | let finalTime = newTime;
33 | do {
34 | await page.waitForTimeout(2000);
35 | el = page.getByText("Time:");
36 | finalTime = await el.textContent();
37 | await page.reload();
38 | } while (newTime === finalTime);
39 |
40 | expect(newTime).not.toEqual(finalTime);
41 | });
42 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/middleware.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "playwright/test";
2 |
3 | test("should return 500 on middleware error", async ({ request }) => {
4 | const response = await request.get("/", {
5 | headers: {
6 | "x-throw": "true",
7 | },
8 | });
9 | const body = await response.text();
10 | expect(response.status()).toBe(500);
11 | expect(body).toContain("Internal Server Error");
12 | });
13 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/redirect.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Single redirect", async ({ page }) => {
4 | await page.goto("/next-config-redirect-without-locale-support/");
5 |
6 | await page.waitForURL("https://opennext.js.org/");
7 | const el = page.getByRole("heading", { name: "OpenNext" });
8 | await expect(el).toBeVisible();
9 | });
10 |
11 | test("Redirect with default locale support", async ({ page }) => {
12 | await page.goto("/redirect-with-locale/");
13 |
14 | await page.waitForURL("/ssr/");
15 | const el = page.getByText("SSR");
16 | await expect(el).toBeVisible();
17 | });
18 |
19 | test("Redirect with locale support", async ({ page }) => {
20 | await page.goto("/nl/redirect-with-locale/");
21 |
22 | await page.waitForURL("/nl/ssr/");
23 | const el = page.getByText("SSR");
24 | await expect(el).toBeVisible();
25 | });
26 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/ssr.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("Server Side Render", async ({ page }) => {
4 | await page.goto("/");
5 | await page.locator('[href="/ssr/"]').click();
6 |
7 | await page.waitForURL("/ssr/");
8 | let el = page.getByText("Time:");
9 | await expect(el).toBeVisible();
10 | let time = await el.textContent();
11 |
12 | await page.reload();
13 |
14 | el = page.getByText("Time:");
15 | let newTime = await el.textContent();
16 | await expect(el).toBeVisible();
17 |
18 | for (let i = 0; i < 5; i++) {
19 | await page.reload();
20 | el = page.getByText("Time:");
21 | newTime = await el.textContent();
22 | await expect(el).toBeVisible();
23 | expect(time).not.toEqual(newTime);
24 | time = newTime;
25 | await page.waitForTimeout(250);
26 | }
27 | });
28 |
29 | test("Server Side Render with env", async ({ page }) => {
30 | await page.goto("/ssr/");
31 | const el = page.getByText("Env:");
32 | expect(await el.textContent()).toEqual("Env: bar");
33 | });
34 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/pagesRouter/trailing.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@playwright/test";
2 |
3 | test("trailingSlash redirect", async ({ page }) => {
4 | const response = await page.goto("/ssr");
5 |
6 | expect(response?.request().redirectedFrom()?.url()).toMatch(/\/ssr$/);
7 | expect(response?.request().url()).toMatch(/\/ssr\/$/);
8 | });
9 |
10 | test("trailingSlash redirect with search parameters", async ({ page }) => {
11 | const response = await page.goto("/ssr?happy=true");
12 |
13 | expect(response?.request().redirectedFrom()?.url()).toMatch(
14 | /\/ssr\?happy=true$/,
15 | );
16 | expect(response?.request().url()).toMatch(/\/ssr\/\?happy=true$/);
17 | });
18 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from "node:crypto";
2 |
3 | export function validateMd5(data: Buffer, expectedHash: string) {
4 | return createHash("md5").update(data).digest("hex") === expectedHash;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/tests-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Test Config",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": false,
7 | "declarationMap": false,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": false,
12 | "emitDecoratorMetadata": true,
13 | "experimentalDecorators": true,
14 | "moduleResolution": "nodenext",
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "preserveWatchOutput": true,
18 | "skipLibCheck": true,
19 | "strict": true,
20 | "removeComments": true,
21 | "module": "NodeNext",
22 | "target": "ES2022",
23 | "sourceMap": true
24 | },
25 | "include": ["."],
26 | "exclude": ["dist", "build", "node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/tests-unit/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tests-unit",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "clean": "rm -rf .turbo && rm -rf node_modules",
7 | "dev": "vitest",
8 | "test": "vitest run --coverage"
9 | },
10 | "dependencies": {
11 | "@opennextjs/aws": "workspace:*"
12 | },
13 | "devDependencies": {
14 | "@types/testing-library__jest-dom": "^5.14.9",
15 | "@vitest/coverage-v8": "^2.1.3",
16 | "jsdom": "^22.1.0",
17 | "vite": "5.4.9",
18 | "vite-tsconfig-paths": "^5.0.1",
19 | "vitest": "^2.1.3"
20 | },
21 | "version": null
22 | }
23 |
--------------------------------------------------------------------------------
/packages/tests-unit/setup.ts:
--------------------------------------------------------------------------------
1 | import { afterEach } from "vitest";
2 |
3 | // runs a cleanup after each test case (e.g. clearing jsdom)
4 | afterEach(() => {});
5 |
--------------------------------------------------------------------------------
/packages/tests-unit/tests/converters/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { removeUndefinedFromQuery } from "@opennextjs/aws/overrides/converters/utils.js";
2 |
3 | describe("removeUndefinedFromQuery", () => {
4 | it("should remove undefined from query", () => {
5 | const result = removeUndefinedFromQuery({
6 | a: "1",
7 | b: ["2", "3"],
8 | c: undefined,
9 | });
10 |
11 | expect(result).toEqual({
12 | a: "1",
13 | b: ["2", "3"],
14 | });
15 | });
16 |
17 | it("should return empty object if input is empty", () => {
18 | const result = removeUndefinedFromQuery({});
19 |
20 | expect(result).toEqual({});
21 | });
22 |
23 | it("should return empty object if all values are undefined", () => {
24 | const result = removeUndefinedFromQuery({
25 | a: undefined,
26 | b: undefined,
27 | });
28 |
29 | expect(result).toEqual({});
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/packages/tests-unit/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": false,
7 | "declarationMap": false,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": false,
12 | "emitDecoratorMetadata": true,
13 | "experimentalDecorators": true,
14 | "moduleResolution": "nodenext",
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "preserveWatchOutput": true,
18 | "skipLibCheck": true,
19 | "strict": true,
20 | "removeComments": true,
21 | "module": "NodeNext",
22 | "target": "ES2022",
23 | "sourceMap": true,
24 | "paths": {
25 | "@opennextjs/aws/*": ["../open-next/src/*"]
26 | }
27 | },
28 | "include": ["."],
29 | "exclude": ["dist", "build", "node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/packages/tests-unit/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from "vite-tsconfig-paths";
2 | import { defineConfig } from "vitest/config";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [tsconfigPaths()],
7 | test: {
8 | globals: true,
9 | environment: "node",
10 | setupFiles: "./packages/tests-unit/setup.ts",
11 | coverage: {
12 | all: true,
13 | include: ["packages/**"],
14 | exclude: [
15 | "packages/tests-*/**",
16 | "**/node_modules/**",
17 | "**/dist/**",
18 | "**/coverage/**",
19 | ],
20 | reporter: ["text", "html", "json", "json-summary"],
21 | reportOnFailure: true,
22 | },
23 | root: "../../",
24 | include: ["packages/tests-unit/**/*.{test,spec}.?(c|m)ts"],
25 | exclude: ["**/node_modules/**", "**/dist/**", "**/coverage/**"],
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/packages/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-next/utils",
3 | "private": true,
4 | "exports": {
5 | ".": "./dist/index.js",
6 | "./binary": "./dist/binary.js",
7 | "./logger": "./dist/logger.js"
8 | },
9 | "typesVersions": {
10 | "*": {
11 | "types": ["./dist/*.d.ts"]
12 | }
13 | },
14 | "scripts": {
15 | "build": "tsup ./src/*.ts --format cjs --dts",
16 | "dev": "tsup ./src/*.ts --format cjs --dts --watch",
17 | "clean": "rm -rf .turbo && rm -rf node_modules"
18 | },
19 | "dependencies": {},
20 | "devDependencies": {
21 | "tsup": "7.2.0",
22 | "@types/node": "catalog:"
23 | },
24 | "publishConfig": {
25 | "access": "public"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | // TODO: move util functions from open-next here (if/where it makes sense)
2 | export function add(a: number, b: number) {
3 | return a + b;
4 | }
5 |
6 | export function generateUniqueId() {
7 | return Math.random().toString(36).slice(2, 8);
8 | }
9 |
10 | export async function wait(n = 1000) {
11 | return new Promise((res) => {
12 | setTimeout(res, n);
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/packages/utils/src/logger.ts:
--------------------------------------------------------------------------------
1 | export function debug(...args: any[]) {
2 | if (process.env.OPEN_NEXT_DEBUG) {
3 | console.log(...args);
4 | }
5 | }
6 |
7 | export function warn(...args: any[]) {
8 | console.warn(...args);
9 | }
10 |
11 | export function error(...args: any[]) {
12 | console.error(...args);
13 | }
14 |
15 | export const awsLogger = {
16 | trace: () => {},
17 | debug: () => {},
18 | info: debug,
19 | warn,
20 | error,
21 | };
22 |
--------------------------------------------------------------------------------
/packages/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "forceConsistentCasingInFileNames": true,
5 | "isolatedModules": false,
6 | "moduleResolution": "NodeNext",
7 | "module": "NodeNext",
8 | "preserveWatchOutput": true,
9 | "skipLibCheck": true,
10 | "noEmit": true,
11 | "strict": true,
12 | "target": "ESNext"
13 | },
14 | "exclude": ["node_modules"]
15 | }
16 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "examples/*"
3 | - "packages/*"
4 |
5 | catalog:
6 | next: 15.2.0
7 | react: 19.0.0
8 | react-dom: 19.0.0
9 | "@types/node": 20.17.6
10 | "@types/react": 19.0.0
11 | "@types/react-dom": 19.0.0
12 | autoprefixer: 10.4.15
13 | postcss: 8.4.27
14 | tailwindcss: 3.3.3
15 | typescript: 5.6.3
16 | esbuild: 0.25.4
17 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalEnv": ["APP_ROUTER_URL", "PAGES_ROUTER_URL", "APP_PAGES_ROUTER_URL"],
4 | "pipeline": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "outputs": ["dist/**"]
8 | },
9 | "e2e:test": {
10 | "cache": false
11 | },
12 | "test": {
13 | "dependsOn": ["^build"],
14 | "cache": false
15 | },
16 | "lint": {
17 | "outputs": []
18 | },
19 | "dev": {
20 | "cache": false
21 | },
22 | "clean": {
23 | "cache": false
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------