├── .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 |
45 | 46 | https://opennext.js.org 47 | 48 | 49 | https://github.com/opennextjs/docs 50 | 51 |
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 |
45 | 46 | https://opennext.js.org 47 | 48 | 49 | https://github.com/opennextjs/docs 50 | 51 |
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 | {/*
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 | Corporate Holiday Card 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 |
Header
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 |
20 |
21 | 32 | 43 |
44 | 45 | {routeA && a} 46 | {routeB && b} 47 | {/* {children} */} 48 |
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 | {/*
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 |
4 |

I was redirected from next.config.js

5 |

/next-config-redirect => /config-redirect

6 | 10 | /next-config-redirect-encoding?q=äöå€ 11 | 12 | 16 | /next-config-redirect-encoding?q=%C3%A4%C3%B6%C3%A5%E2%82%AC 17 | 18 |
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 | Open Next architecture 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 |
20 |
21 | 32 | 43 |
44 | 45 | {routeA && a} 46 | {routeB && b} 47 | {/* {children} */} 48 |
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 |
7 |

{Date.now()}

8 |
9 | ); 10 | } 11 | 12 | export async function FullyCachedComponentWithTag() { 13 | "use cache"; 14 | unstable_cacheTag("fullyTagged"); 15 | return ( 16 |
17 |

{Date.now()}

18 |
19 | ); 20 | } 21 | 22 | export async function ISRComponent() { 23 | "use cache"; 24 | unstable_cacheLife({ 25 | stale: 1, 26 | revalidate: 5, 27 | }); 28 | return ( 29 |
30 |

{Date.now()}

31 |
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 | --------------------------------------------------------------------------------