diff --git a/.cargo/config.toml b/.cargo/config.toml index 4b7106839cd3f..f81bce8a1ef47 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,6 +5,10 @@ CARGO_WORKSPACE_DIR = { value = "", relative = true } rustdocflags = [] +[target.x86_64-unknown-linux-gnu] +# Should be kept in sync with turbopack's linker +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + [target.x86_64-pc-windows-msvc] linker = "rust-lld" diff --git a/.github/actions/next-stats-action/Dockerfile b/.github/actions/next-stats-action/Dockerfile index d61c0eedb1091..7d5f2454714be 100644 --- a/.github/actions/next-stats-action/Dockerfile +++ b/.github/actions/next-stats-action/Dockerfile @@ -14,7 +14,7 @@ RUN ln $(which python3) /usr/bin/python RUN curl -sfLS https://install-node.vercel.app/v18 | bash -s -- -f # Install node_modules -RUN npm i -g pnpm@7.24.3 yarn@1.22.19 +RUN npm i -g pnpm@7.32.2 yarn@1.22.19 RUN cd /next-stats && pnpm install --production RUN git config --global user.email 'stats@localhost' diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index bb6044cbf08f1..34569e7eb4720 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -52,6 +52,12 @@ runs: echo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse >> $GITHUB_ENV fi + - shell: bash + run: | + : install mold linker + sudo apt update + sudo apt install -y mold + - name: 'Setup Rust toolchain' uses: dtolnay/rust-toolchain@master if: ${{ !inputs.skip-install }} diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index 68991b1b7752b..0a5798fb4e227 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -8,7 +8,7 @@ on: env: NAPI_CLI_VERSION: 2.14.7 TURBO_VERSION: 1.9.6 - PNPM_VERSION: 7.24.3 + PNPM_VERSION: 7.32.2 NODE_MAINTENANCE_VERSION: 16 NODE_LTS_VERSION: 18 @@ -418,6 +418,7 @@ jobs: NEXT_TELEMETRY_DISABLED: 1 VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }} VERCEL_TEST_TEAM: vtest314-next-e2e-tests + DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }} steps: - uses: actions/cache@v3 timeout-minutes: 5 @@ -436,7 +437,7 @@ jobs: - run: RESET_VC_PROJECT=true node scripts/reset-vercel-project.mjs name: Reset test project - - run: docker run --rm -v $(pwd):/work mcr.microsoft.com/playwright:v1.28.1-jammy /bin/bash -c "cd /work && NODE_VERSION=${{ env.NODE_LTS_VERSION }} ./scripts/setup-node.sh && npm i -g pnpm@${PNPM_VERSION} > /dev/null && VERCEL_TEST_TOKEN=${{ secrets.VERCEL_TEST_TOKEN }} VERCEL_TEST_TEAM=vtest314-next-e2e-tests NEXT_TEST_JOB=1 NEXT_TEST_MODE=deploy TEST_TIMINGS_TOKEN=${{ secrets.TEST_TIMINGS_TOKEN }} NEXT_TEST_CONTINUE_ON_ERROR=1 xvfb-run node run-tests.js --type e2e >> /proc/1/fd/1" + - run: docker run --rm -v $(pwd):/work mcr.microsoft.com/playwright:v1.28.1-jammy /bin/bash -c "cd /work && NODE_VERSION=${{ env.NODE_LTS_VERSION }} ./scripts/setup-node.sh && npm i -g pnpm@${PNPM_VERSION} > /dev/null && DATADOG_TRACE_NEXTJS_TEST=TRUE DATADOG_API_KEY=${DATADOG_API_KEY} DD_ENV=ci VERCEL_TEST_TOKEN=${{ secrets.VERCEL_TEST_TOKEN }} VERCEL_TEST_TEAM=vtest314-next-e2e-tests NEXT_TEST_JOB=1 NEXT_TEST_MODE=deploy TEST_TIMINGS_TOKEN=${{ secrets.TEST_TIMINGS_TOKEN }} NEXT_TEST_CONTINUE_ON_ERROR=1 xvfb-run node run-tests.js --type e2e >> /proc/1/fd/1" name: Run test/e2e (deploy) - name: Upload test trace @@ -449,6 +450,14 @@ jobs: path: | test/traces + - name: Upload test trace to datadog + continue-on-error: true + run: | + ls -al ./test + npm install -g junit-report-merger@6.0.2 @datadog/datadog-ci@2.14.0 + jrm ./nextjs-test-result-junit.xml "test/test-junit-report/**/*.xml" + DD_ENV=ci datadog-ci junit upload --tags test.type:nextjs_deploy_e2e --service nextjs ./nextjs-test-result-junit.xml + releaseStats: name: Release Stats runs-on: ubuntu-latest diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 06d4e666de5ba..f27b46c42b2ee 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -9,7 +9,7 @@ on: env: NAPI_CLI_VERSION: 2.14.7 TURBO_VERSION: 1.9.6 - PNPM_VERSION: 7.24.3 + PNPM_VERSION: 7.32.2 NODE_MAINTENANCE_VERSION: 16 NODE_LTS_VERSION: 18 TEST_CONCURRENCY: 6 diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml index 0a3a2821b3ce5..9cce0986fbfb1 100644 --- a/.github/workflows/build_reusable.yml +++ b/.github/workflows/build_reusable.yml @@ -47,7 +47,7 @@ on: env: NAPI_CLI_VERSION: 2.14.7 TURBO_VERSION: 1.9.6 - PNPM_VERSION: 7.24.3 + PNPM_VERSION: 7.32.2 NODE_MAINTENANCE_VERSION: 16 NODE_LTS_VERSION: 18 TEST_CONCURRENCY: 6 diff --git a/.github/workflows/pull_request_stats.yml b/.github/workflows/pull_request_stats.yml index 0b061b8291029..cb3cdcf90fc91 100644 --- a/.github/workflows/pull_request_stats.yml +++ b/.github/workflows/pull_request_stats.yml @@ -7,7 +7,7 @@ name: Generate Pull Request Stats env: NAPI_CLI_VERSION: 2.14.7 TURBO_VERSION: 1.9.6 - PNPM_VERSION: 7.24.3 + PNPM_VERSION: 7.32.2 NODE_MAINTENANCE_VERSION: 16 NODE_LTS_VERSION: 18 TEST_CONCURRENCY: 6 diff --git a/.github/workflows/test_examples.yml b/.github/workflows/test_examples.yml index b128f5a46db33..c38fad768577c 100644 --- a/.github/workflows/test_examples.yml +++ b/.github/workflows/test_examples.yml @@ -14,7 +14,7 @@ on: name: Test examples env: - PNPM_VERSION: 7.24.3 + PNPM_VERSION: 7.32.2 jobs: testExamples: diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 45882d9512530..bc1c7bbd820fc 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -26,7 +26,7 @@ name: Trigger Release env: NAPI_CLI_VERSION: 2.14.7 TURBO_VERSION: 1.9.6 - PNPM_VERSION: 7.24.3 + PNPM_VERSION: 7.32.2 NODE_MAINTENANCE_VERSION: 16 NODE_LTS_VERSION: 18 diff --git a/Cargo.lock b/Cargo.lock index d93abde232486..4596dc3c65343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3260,7 +3260,16 @@ name = "next-build" version = "0.1.0" dependencies = [ "anyhow", + "clap 4.1.11", + "console-subscriber", + "dunce", "next-core", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "turbo-tasks", "turbopack-binding", "vergen", ] @@ -7376,6 +7385,7 @@ dependencies = [ "turbo-tasks-testing", "turbopack", "turbopack-bench", + "turbopack-build", "turbopack-cli-utils", "turbopack-core", "turbopack-dev", @@ -7390,6 +7400,26 @@ dependencies = [ "turbopack-test-utils", ] +[[package]] +name = "turbopack-build" +version = "0.1.0" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230615.1#1ff1956dc18ff1805b2ac87f21f79e1abea75fc8" +dependencies = [ + "anyhow", + "indexmap", + "indoc", + "serde", + "serde_json", + "serde_qs", + "turbo-tasks", + "turbo-tasks-build", + "turbo-tasks-fs", + "turbopack-core", + "turbopack-css", + "turbopack-ecmascript", + "turbopack-ecmascript-runtime", +] + [[package]] name = "turbopack-cli-utils" version = "0.1.0" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 70f62eba5723e..e3055603edae9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -33,7 +33,7 @@ pr: variables: PNPM_CACHE_FOLDER: $(Pipeline.Workspace)/.pnpm-store - PNPM_VERSION: 7.24.3 + PNPM_VERSION: 7.32.2 NEXT_TELEMETRY_DISABLED: '1' node_16_version: ^16.8.0 diff --git a/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx b/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx index 7b4b91560a1bf..b51f2a3389040 100644 --- a/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx +++ b/docs/02-app/01-building-your-application/01-routing/10-router-handlers.mdx @@ -170,7 +170,7 @@ export async function POST() { } ``` -> **Good to know**: Previously, API Routes could have been used for use cases like handling form submissions. Route Handlers are likely not the solution for these uses cases. We will be recommending the use of [mutations](/docs/app/building-your-application/data-fetching/server-actions) for this when ready. +> **Good to know**: Previously, API Routes could have been used for use cases like handling form submissions. Route Handlers are likely not the solution for these use cases. We will be recommending the use of [mutations](/docs/app/building-your-application/data-fetching/server-actions) for this when ready. ### Route Resolution @@ -388,6 +388,60 @@ export async function GET(request, { params }) { ### Streaming +Streaming is commonly used in combination with Large Language Models (LLMs), such an OpenAI, for AI-generated content. Learn more about the [AI SDK](https://sdk.vercel.ai/docs). + +```ts filename="app/api/completion/route.ts" switcher +import { Configuration, OpenAIApi } from 'openai-edge' +import { OpenAIStream, StreamingTextResponse } from 'ai' + +const config = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}) +const openai = new OpenAIApi(config) + +export const runtime = 'edge' + +export async function POST(req: Request) { + const { prompt } = await req.json() + const response = await openai.createCompletion({ + model: 'text-davinci-003', + stream: true, + temperature: 0.6, + prompt: 'What is Next.js?', + }) + + const stream = OpenAIStream(response) + return new StreamingTextResponse(stream) +} +``` + +```js filename="app/api/completion/route.js" switcher +import { Configuration, OpenAIApi } from 'openai-edge' +import { OpenAIStream, StreamingTextResponse } from 'ai' + +const config = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}) +const openai = new OpenAIApi(config) + +export const runtime = 'edge' + +export async function POST(req) { + const { prompt } = await req.json() + const response = await openai.createCompletion({ + model: 'text-davinci-003', + stream: true, + temperature: 0.6, + prompt: 'What is Next.js?', + }) + + const stream = OpenAIStream(response) + return new StreamingTextResponse(stream) +} +``` + +These abstractions use the Web APIs to create a stream. You can also use the underlying Web APIs directly. + ```ts filename="app/api/route.ts" switcher // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#convert_async_iterator_to_stream function iteratorToStream(iterator: any) { diff --git a/docs/02-app/01-building-your-application/01-routing/index.mdx b/docs/02-app/01-building-your-application/01-routing/index.mdx index 8859c0ccbc6bb..06118147f65fe 100644 --- a/docs/02-app/01-building-your-application/01-routing/index.mdx +++ b/docs/02-app/01-building-your-application/01-routing/index.mdx @@ -132,7 +132,7 @@ In a nested route, the components of a segment will be nested **inside** the com ## Colocation -In addition to special files, you have the option to colocate your own files (e.g. components, styles, tests etc) inside folders in the `app` directory. +In addition to special files, you have the option to colocate your own files (e.g. components, styles, tests, etc) inside folders in the `app` directory. This is because while folders define routes, only the contents returned by `page.js` or `route.js` are publically addressable. @@ -172,7 +172,7 @@ Without partial rendering, each navigation would cause the full page to re-rende ## Advanced Routing Patterns -The App Router also provides a set conventions to help you implement more advanced routing patterns. These include: +The App Router also provides a set of conventions to help you implement more advanced routing patterns. These include: - [Parallel Routes](/docs/app/building-your-application/routing/parallel-routes): Allow you to simultaneously show two or more pages in the same view that can be navigated independently. You can use them for split views that have their own sub-navigation. E.g. Dashboards. - [Intercepting Routes](/docs/app/building-your-application/routing/intercepting-routes): Allow you to intercept a route and show it in the context of another route. You can use these when keeping the context for the current page is important. E.g. Seeing all tasks while editing one task or expanding a photo in a feed. diff --git a/docs/02-app/01-building-your-application/02-rendering/index.mdx b/docs/02-app/01-building-your-application/02-rendering/index.mdx index 5d1d1dcd71214..57b6a830413eb 100644 --- a/docs/02-app/01-building-your-application/02-rendering/index.mdx +++ b/docs/02-app/01-building-your-application/02-rendering/index.mdx @@ -12,7 +12,7 @@ React 18 and Next.js 13 introduced new ways to render your application. This pag There are two environments where your application code can be rendered: the client and the server. Client and Server Environemnts` API, and enable out-of-the-box progressive enhancement and [loading states](#experimental-useformstatus). Similar to the HTML primitive [`formaction`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formation). +[Actions](#actions) integrated into the web standard `
` API, and enable out-of-the-box progressive enhancement and [loading states](#experimental-useformstatus). Similar to the HTML primitive [`formaction`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formaction). ### Server Functions diff --git a/docs/02-app/01-building-your-application/05-optimizing/01-images.mdx b/docs/02-app/01-building-your-application/05-optimizing/01-images.mdx index 798f1c6295113..d5864c587c018 100644 --- a/docs/02-app/01-building-your-application/05-optimizing/01-images.mdx +++ b/docs/02-app/01-building-your-application/05-optimizing/01-images.mdx @@ -233,9 +233,112 @@ Styling the Image component is similar to styling a normal `` element, but - When using `fill`, the parent element must have `display: block` - This is the default for `
` elements but should be specified otherwise. -For examples, see the [Image Component Demo](https://image-component.nextjs.gallery). +## Examples -### Examples +### Responsive + +Responsive image filling the width and height of its parent container + +```jsx +import Image from 'next/image' +import mountains from '../public/mountains.jpg' + +export default function Responsive() { + return ( +
+ Mountains +
+ ) +} +``` + +### Fill Container + +Grid of images filling parent container width + +```jsx +import Image from 'next/image' +import mountains from '../public/mountains.jpg' + +export default function Fill() { + return ( +
+
+ Mountains +
+ {/* And more images in the grid... */} +
+ ) +} +``` + +### Background Image + +Background image taking full width and height of page + +```jsx +import Image from 'next/image' +import mountains from '../public/mountains.jpg' + +export default function Background() { + return ( + Mountains + ) +} +``` For examples of the Image component used with the various styles, see the [Image Component Demo](https://image-component.nextjs.gallery). diff --git a/docs/02-app/01-building-your-application/05-optimizing/08-open-telemetry.mdx b/docs/02-app/01-building-your-application/05-optimizing/08-open-telemetry.mdx index b0a607bbc73c9..eb829cd6c5849 100644 --- a/docs/02-app/01-building-your-application/05-optimizing/08-open-telemetry.mdx +++ b/docs/02-app/01-building-your-application/05-optimizing/08-open-telemetry.mdx @@ -35,7 +35,7 @@ To get started, you must install `@vercel/otel`: npm install @vercel/otel ``` -Next, create a custom [`instrumentation.ts`](/docs/pages/building-your-application/optimizing/instrumentation) (or `.js`) file in the **root directory** of the project: +Next, create a custom [`instrumentation.ts`](/docs/pages/building-your-application/optimizing/instrumentation) (or `.js`) file in the **root directory** of the project (or inside `src` folder if using one): ```ts filename="your-project/instrumentation.ts" switcher import { registerOTel } from '@vercel/otel' @@ -55,7 +55,7 @@ export function register() { > **Good to know** > -> - The `instrumentation` file should be in the root of your project and not the `app` or `pages` directory. +> - The `instrumentation` file should be in the root of your project and not inside the `app` or `pages` directory. If you're using the `src` folder, then place the file inside `src` alongside `pages` and `app`. > - If you use the [`pagesExtension` config option](/docs/pages/api-reference/next-config-js/pageExtensions) to add a suffix, you will also need to update the `instrumentation` filename to match. > - We have created a basic [with-opentelemetry](https://github.com/vercel/next.js/tree/canary/examples/with-opentelemetry) example that you can use. diff --git a/docs/02-app/01-building-your-application/05-optimizing/09-instrumentation.mdx b/docs/02-app/01-building-your-application/05-optimizing/09-instrumentation.mdx index a2ee7c07202f4..04237c48a42d5 100644 --- a/docs/02-app/01-building-your-application/05-optimizing/09-instrumentation.mdx +++ b/docs/02-app/01-building-your-application/05-optimizing/09-instrumentation.mdx @@ -3,13 +3,12 @@ title: Instrumentation description: Learn how to use instrumentation to run code at server startup in your Next.js app --- -> **Good to know**: This feature is **experimental**. To use it, you must explicitly opt in by defining `experimental.instrumentationHook = true;` in your `next.config.js`. - -If you export a function named `register` from a `instrumentation.ts` (or `.js`) file in the **root directory** of your project, we will call that function whenever a new Next.js server instance is bootstrapped. +If you export a function named `register` from a `instrumentation.ts` (or `.js`) file in the **root directory** of your project (or inside the `src` folder if using one), we will call that function whenever a new Next.js server instance is bootstrapped. > **Good to know** > -> - The `instrumentation` file should be in the root of your project and not the `app` or `pages` directory. +> - This feature is **experimental**. To use it, you must explicitly opt in by defining `experimental.instrumentationHook = true;` in your `next.config.js`. +> - The `instrumentation` file should be in the root of your project and not inside the `app` or `pages` directory. If you're using the `src` folder, then place the file inside `src` alongside `pages` and `app`. > - If you use the [`pagesExtension` config option](/docs/pages/api-reference/next-config-js/pageExtensions) to add a suffix, you will also need to update the `instrumentation` filename to match. > - We have created a basic [with-opentelemetry](https://github.com/vercel/next.js/tree/canary/examples/with-opentelemetry) example that you can use. diff --git a/docs/02-app/01-building-your-application/06-configuring/02-eslint.mdx b/docs/02-app/01-building-your-application/06-configuring/02-eslint.mdx index bed538a401770..55e3f9655ab0d 100644 --- a/docs/02-app/01-building-your-application/06-configuring/02-eslint.mdx +++ b/docs/02-app/01-building-your-application/06-configuring/02-eslint.mdx @@ -127,7 +127,7 @@ If you're using `eslint-plugin-next` in a project where Next.js isn't installed ## Linting Custom Directories and Files -By default, Next.js will run ESLint for all files in the `pages/`, `app` (only if the experimental `appDir` feature is enabled), `components/`, `lib/`, and `src/` directories. However, you can specify which directories using the `dirs` option in the `eslint` config in `next.config.js` for production builds: +By default, Next.js will run ESLint for all files in the `pages/`, `app/`, `components/`, `lib/`, and `src/` directories. However, you can specify which directories using the `dirs` option in the `eslint` config in `next.config.js` for production builds: ```js filename="next.config.js" module.exports = { diff --git a/docs/02-app/01-building-your-application/08-upgrading/02-app-router-migration.mdx b/docs/02-app/01-building-your-application/08-upgrading/02-app-router-migration.mdx index fe48eb3f0ab2c..042600e8c9394 100644 --- a/docs/02-app/01-building-your-application/08-upgrading/02-app-router-migration.mdx +++ b/docs/02-app/01-building-your-application/08-upgrading/02-app-router-migration.mdx @@ -113,7 +113,6 @@ We recommend reducing the combined complexity of these updates by breaking down - `pages/_app.js` and `pages/_document.js` have been replaced with a single `app/layout.js` root layout. [Learn more](/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required). - `pages/_error.js` has been replaced with more granular `error.js` special files. [Learn more](/docs/app/building-your-application/routing/error-handling). - `pages/404.js` has been replaced with the [`not-found.js`](/docs/app/api-reference/file-conventions/not-found) file. -- You can colocate other files inside the `app` directory such as components, styles, tests, and more. [Learn more](/docs/app/building-your-application/routing). - `pages/api/*` currently remain inside the `pages` directory. ### Step 1: Creating the `app` directory diff --git a/docs/02-app/02-api-reference/04-functions/cookies.mdx b/docs/02-app/02-api-reference/04-functions/cookies.mdx index 45684db1aba0a..cf8549342e06e 100644 --- a/docs/02-app/02-api-reference/04-functions/cookies.mdx +++ b/docs/02-app/02-api-reference/04-functions/cookies.mdx @@ -98,7 +98,7 @@ async function create(data) { cookies().set({ name: 'name', value: '', - expires: new Date('2016-10-05') + expires: new Date('2016-10-05'), path: '/', // For all paths }); } diff --git a/docs/03-pages/01-building-your-application/01-routing/09-authenticating.mdx b/docs/03-pages/01-building-your-application/01-routing/09-authenticating.mdx index 26ce82e3b5916..6c5985fff78bf 100644 --- a/docs/03-pages/01-building-your-application/01-routing/09-authenticating.mdx +++ b/docs/03-pages/01-building-your-application/01-routing/09-authenticating.mdx @@ -134,7 +134,7 @@ To see examples with other authentication providers, check out the [examples fol - [Magic](https://github.com/vercel/next.js/tree/canary/examples/with-magic) - [Nhost](https://github.com/vercel/next.js/tree/canary/examples/with-nhost-auth-realtime-graphql) - [Ory](https://github.com/vercel/examples/tree/main/solutions/auth-with-ory) -- [Supabase](https://github.com/vercel/next.js/tree/canary/examples/with-supabase-auth-realtime-db) +- [Supabase](https://github.com/vercel/next.js/tree/canary/examples/with-supabase) - [Supertokens](https://github.com/vercel/next.js/tree/canary/examples/with-supertokens) - [Userbase](https://github.com/vercel/next.js/tree/canary/examples/with-userbase) diff --git a/docs/03-pages/01-building-your-application/03-data-fetching/03-get-server-side-props.mdx b/docs/03-pages/01-building-your-application/03-data-fetching/03-get-server-side-props.mdx index 570ec6f74c6b8..85e42822b79ff 100644 --- a/docs/03-pages/01-building-your-application/03-data-fetching/03-get-server-side-props.mdx +++ b/docs/03-pages/01-building-your-application/03-data-fetching/03-get-server-side-props.mdx @@ -140,7 +140,7 @@ export async function getServerSideProps({ req, res }) { } ``` -Learn more about [caching](/docs/pages/building-your-application/deploying/production-checklist). +Learn more about [caching](/docs/pages/building-your-application/deploying/production-checklist#caching). ## Does getServerSideProps render an error page diff --git a/docs/05-community/01-contribution-guide.mdx b/docs/05-community/01-contribution-guide.mdx index 46e277951052e..187bfa618b9d6 100644 --- a/docs/05-community/01-contribution-guide.mdx +++ b/docs/05-community/01-contribution-guide.mdx @@ -68,7 +68,7 @@ Please let us know if you have any questions or need further assistance in your The docs use **file-system routing**. Each folder and files inside [`/docs`](/vercel/next.js/tree/canary/docs) represent a route segment. These segments are used to generate the URL paths, navigation, and breadcrumbs. -The file structure reflects the navigation that you see on the site, and by default, navigation items are sorted alphabetically. However, we can change the order of the items by appending a two-digit number (`00-`) to the folder or file name. +The file structure reflects the navigation that you see on the site, and by default, navigation items are sorted alphabetically. However, we can change the order of the items by prepending a two-digit number (`00-`) to the folder or file name. For example, in the [functions API Reference](/docs/app/api-reference/functions), the pages are sorted alphabetically because it makes it easier for developers to find a specific function: diff --git a/examples/cms-payload/.env.example b/examples/cms-payload/.env.example new file mode 100644 index 0000000000000..ecd2377395e9e --- /dev/null +++ b/examples/cms-payload/.env.example @@ -0,0 +1,12 @@ +MONGODB_URI=mongodb://localhost/payload-vercel-functions +PAYLOAD_SECRET=YOUR_SECRET_HERE +PAYLOAD_CONFIG_PATH=dist/payload.config.js +NEXT_PUBLIC_APP_URL=http://localhost:3000 +PAYLOAD_PUBLIC_CMS_URL=http://localhost:3000 +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +S3_REGION= +NEXT_PUBLIC_S3_ENDPOINT= +NEXT_PUBLIC_S3_BUCKET= +PAYLOAD_PRIVATE_REGENERATION_SECRET= +NEXT_PRIVATE_REGENERATION_SECRET= \ No newline at end of file diff --git a/examples/cms-payload/.gitignore b/examples/cms-payload/.gitignore new file mode 100644 index 0000000000000..9589f809ca02f --- /dev/null +++ b/examples/cms-payload/.gitignore @@ -0,0 +1,171 @@ +.DS_Store +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode +.vercel + +build +public/admin diff --git a/examples/cms-payload/.npmrc b/examples/cms-payload/.npmrc new file mode 100644 index 0000000000000..521a9f7c07735 --- /dev/null +++ b/examples/cms-payload/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/examples/cms-payload/README.md b/examples/cms-payload/README.md new file mode 100644 index 0000000000000..73ca6c545e465 --- /dev/null +++ b/examples/cms-payload/README.md @@ -0,0 +1,66 @@ +# Next + Payload Serverless Demo + +This is a demo showing how to utilize `@payloadcms/next-payload` to deploy Payload serverlessly, in the same repo alongside of a NextJS app. + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-payload&project-name=cms-payload&repository-name=cms-payload) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: + +```bash +npx create-next-app --example cms-payload cms-payload-app +``` + +```bash +yarn create next-app --example cms-payload cms-payload-app +``` + +```bash +pnpm create next-app --example cms-payload cms-payload-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). + +The only thing you need to do to deploy to Vercel is to ensure that you have a Mongo Atlas database connection string and an S3 bucket (if desired). + +Fill out the same environment variables that are shown in the `.env.example` with your own values, and then you're good to go! + +### Developing locally + +To develop with this package locally, make sure you have the following required software: + +1. MongoDB +2. Node + NPM / Yarn +3. An S3 bucket to store media (optional) + +### Getting started + +Follow the steps below to spin up a local dev environment: + +1. Clone the repo +2. Run `yarn` or `npm install` +3. Run `cp .env.example .env` and fill out all ENV variables as shown +4. Run `yarn dev` to start up the dev server + +From there, you can visit your admin panel via navigating to `http://localhost:3000/admin`. Go ahead and start working! + +### Related examples + +- [Strapi](/examples/cms-strapi) +- [Contentful](/examples/cms-contentful) +- [WordPress](/examples/cms-wordpress) +- [Sanity](/examples/cms-sanity) +- [DatoCMS](/examples/cms-datocms) +- [TakeShape](/examples/cms-takeshape) +- [Prismic](/examples/cms-prismic) +- [Agility CMS](/examples/cms-agilitycms) +- [Cosmic](/examples/cms-cosmic) +- [ButterCMS](/examples/cms-buttercms) +- [Storyblok](/examples/cms-storyblok) +- [GraphCMS](/examples/cms-graphcms) +- [Kontent](/examples/cms-kontent) diff --git a/examples/cms-payload/app/(payload)/admin/[...slug]/page.tsx b/examples/cms-payload/app/(payload)/admin/[...slug]/page.tsx new file mode 100644 index 0000000000000..0e200ea3395db --- /dev/null +++ b/examples/cms-payload/app/(payload)/admin/[...slug]/page.tsx @@ -0,0 +1,2 @@ +// Need to render the same component for anything within /admin +export { default } from '../page' diff --git a/examples/cms-payload/app/(payload)/admin/page.tsx b/examples/cms-payload/app/(payload)/admin/page.tsx new file mode 100644 index 0000000000000..bd95269965b77 --- /dev/null +++ b/examples/cms-payload/app/(payload)/admin/page.tsx @@ -0,0 +1,18 @@ +'use client' + +import React from 'react' +import Root from 'payload/dist/admin/Root' + +const PayloadAdmin = () => { + const [mounted, setMounted] = React.useState(false) + + React.useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) return null + + return +} + +export default PayloadAdmin diff --git a/examples/cms-payload/app/(site)/[slug]/page.tsx b/examples/cms-payload/app/(site)/[slug]/page.tsx new file mode 100644 index 0000000000000..d980b583a8682 --- /dev/null +++ b/examples/cms-payload/app/(site)/[slug]/page.tsx @@ -0,0 +1,52 @@ +import { notFound } from 'next/navigation' +import { getPayloadClient } from '../../../payload/payloadClient' +import Blocks from '../../../components/Blocks' +import { Hero } from '../../../components/Hero' +import { AdminBar } from '../../../components/AdminBar' +import { Metadata } from 'next' + +export async function generateMetadata({ + params: { slug }, +}): Promise { + return { + title: slug, + } +} + +const Page = async ({ params: { slug } }) => { + const payload = await getPayloadClient() + + const pages = await payload.find({ + collection: 'pages', + where: { + slug: { + equals: slug || 'home', + }, + }, + }) + + const page = pages.docs[0] + + if (!page) return notFound() + + return ( + <> + + + + + ) +} + +export async function generateStaticParams() { + const payload = await getPayloadClient() + + const pages = await payload.find({ + collection: 'pages', + limit: 0, + }) + + return pages.docs.map(({ slug }) => ({ slug })) +} + +export default Page diff --git a/examples/cms-payload/app/(site)/layout.tsx b/examples/cms-payload/app/(site)/layout.tsx new file mode 100644 index 0000000000000..1e8bfc514095d --- /dev/null +++ b/examples/cms-payload/app/(site)/layout.tsx @@ -0,0 +1,14 @@ +import Layout from '../../components/Layout' +import { getPayloadClient } from '../../payload/payloadClient' + +const SiteLayout = async ({ children }: { children: React.ReactNode }) => { + const payload = await getPayloadClient() + + const mainMenu = await payload.findGlobal({ + slug: 'main-menu', + }) + + return {children} +} + +export default SiteLayout diff --git a/examples/cms-payload/app/(site)/page.tsx b/examples/cms-payload/app/(site)/page.tsx new file mode 100644 index 0000000000000..21a55ae54cda1 --- /dev/null +++ b/examples/cms-payload/app/(site)/page.tsx @@ -0,0 +1,3 @@ +import Page from './[slug]/page' + +export default Page diff --git a/examples/cms-payload/app/layout.tsx b/examples/cms-payload/app/layout.tsx new file mode 100644 index 0000000000000..08eaa94fdc889 --- /dev/null +++ b/examples/cms-payload/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/examples/cms-payload/components/AdminBar/index.module.scss b/examples/cms-payload/components/AdminBar/index.module.scss new file mode 100644 index 0000000000000..09265d88179ba --- /dev/null +++ b/examples/cms-payload/components/AdminBar/index.module.scss @@ -0,0 +1,40 @@ +.adminBar { + z-index: 10; + width: 100%; + background-color: var(--color-base-1000); + color: var(--color-white); + padding: 5px 0; + font-size: calc(#{var(--html-font-size)} * 1px); + display: none; +} + +.show { + display: block; +} + +.controls { + & > *:not(:last-child) { + margin-right: 10px !important; + } +} + +.user { + margin-right: 10px !important; +} + +.logo { + margin-right: 10px !important; +} + +.blockContainer { + position: relative; +} + +.hr { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background-color: var(--light-gray); + height: 2px; +} diff --git a/examples/cms-payload/components/AdminBar/index.tsx b/examples/cms-payload/components/AdminBar/index.tsx new file mode 100644 index 0000000000000..71e2472062dd3 --- /dev/null +++ b/examples/cms-payload/components/AdminBar/index.tsx @@ -0,0 +1,49 @@ +'use client' + +import React, { useState } from 'react' +import { + PayloadMeUser, + PayloadAdminBarProps, + PayloadAdminBar, +} from 'payload-admin-bar' +import { Gutter } from '../Gutter' +import classes from './index.module.scss' + +const Title: React.FC = () => Payload + Vercel + +export const AdminBar: React.FC<{ + adminBarProps: PayloadAdminBarProps +}> = (props) => { + const { adminBarProps } = props + + const [user, setUser] = useState() + + return ( +
+ + } + style={{ + position: 'relative', + zIndex: 'unset', + padding: 0, + backgroundColor: 'transparent', + }} + /> + +
+ ) +} diff --git a/examples/cms-payload/components/BackgroundColor/index.module.scss b/examples/cms-payload/components/BackgroundColor/index.module.scss new file mode 100644 index 0000000000000..3f9b3c8226a18 --- /dev/null +++ b/examples/cms-payload/components/BackgroundColor/index.module.scss @@ -0,0 +1,9 @@ +.white { + color: var(--color-base-1000); + background-color: var(--color-base-0); +} + +.black { + color: var(--color-base-0); + background-color: var(--color-base-1000); +} diff --git a/examples/cms-payload/components/BackgroundColor/index.tsx b/examples/cms-payload/components/BackgroundColor/index.tsx new file mode 100644 index 0000000000000..a3b318e3bc9ca --- /dev/null +++ b/examples/cms-payload/components/BackgroundColor/index.tsx @@ -0,0 +1,46 @@ +'use client' + +import React, { useContext, createContext } from 'react' +import { VerticalPadding, VerticalPaddingOptions } from '../VerticalPadding' +import classes from './index.module.scss' + +type BackgroundColorField = 'white' | 'black' + +export const BackgroundColorContext = + createContext('white') + +export const useBackgroundColor = (): BackgroundColorField => + useContext(BackgroundColorContext) + +type Props = { + color?: BackgroundColorField + paddingTop?: VerticalPaddingOptions + paddingBottom?: VerticalPaddingOptions + className?: string + children?: React.ReactNode + id?: string +} + +export const BackgroundColor: React.FC = (props) => { + const { + id, + className, + children, + paddingTop, + paddingBottom, + color = 'white', + } = props + + return ( +
+ + + {children} + + +
+ ) +} diff --git a/examples/cms-payload/components/Blocks/CallToAction/index.module.scss b/examples/cms-payload/components/Blocks/CallToAction/index.module.scss new file mode 100644 index 0000000000000..266e3ead3a79b --- /dev/null +++ b/examples/cms-payload/components/Blocks/CallToAction/index.module.scss @@ -0,0 +1,34 @@ +@use '../.../../../../css/queries.scss' as *; + +$spacer-h: calc(var(--block-padding) / 2); + +.callToAction { + padding-left: $spacer-h; + padding-right: $spacer-h; +} + +.background--white { + background-color: var(--color-base-1000); + color: var(--color-base-0); +} + +.richText { + :last-child { + margin-bottom: 0; + } +} + +.linkGroup { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + + :last-child { + margin-bottom: 0; + } + + @include mid-break { + margin-top: 20px; + } +} diff --git a/examples/cms-payload/components/Blocks/CallToAction/index.tsx b/examples/cms-payload/components/Blocks/CallToAction/index.tsx new file mode 100644 index 0000000000000..4c9eb5da8b6ee --- /dev/null +++ b/examples/cms-payload/components/Blocks/CallToAction/index.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import { Cell, Grid } from '@faceless-ui/css-grid' +import { Page } from '../../../payload-types' +import { BackgroundColor } from '../../BackgroundColor' +import { Gutter } from '../../Gutter' +import { CMSLink } from '../../Link' +import RichText from '../../RichText' + +import classes from './index.module.scss' + +type Props = { + ctaBackgroundColor?: 'white' | 'black' + richText: { + [k: string]: unknown + }[] + links: { + link: { + type?: 'reference' | 'custom' + newTab?: boolean + reference: { + value: string | Page + relationTo: 'pages' + } + url: string + label: string + } + id?: string + }[] + id?: string + blockName?: string + blockType: 'cta' +} +export const CallToActionBlock: React.FC = ({ + ctaBackgroundColor, + links, + richText, +}) => { + const oppositeBackgroundColor = + ctaBackgroundColor === 'white' ? 'black' : 'white' + + return ( + + +
+ + +
+ +
+
+ + +
+ {(links || []).map(({ link }, i) => { + return + })} +
+
+
+
+
+
+ ) +} diff --git a/examples/cms-payload/components/Blocks/Content/index.module.scss b/examples/cms-payload/components/Blocks/Content/index.module.scss new file mode 100644 index 0000000000000..42054b810581b --- /dev/null +++ b/examples/cms-payload/components/Blocks/Content/index.module.scss @@ -0,0 +1,3 @@ +.link { + margin-top: var(--base); +} diff --git a/examples/cms-payload/components/Blocks/Content/index.tsx b/examples/cms-payload/components/Blocks/Content/index.tsx new file mode 100644 index 0000000000000..1aa88b834efee --- /dev/null +++ b/examples/cms-payload/components/Blocks/Content/index.tsx @@ -0,0 +1,96 @@ +import React from 'react' +import { Grid, Cell } from '@faceless-ui/css-grid' +import { Page } from '../../../payload-types' +import RichText from '../../RichText' +import { Gutter } from '../../Gutter' +import { CMSLink } from '../../Link' +import classes from './index.module.scss' + +type Props = Extract + +const Columns: React.FC = ({ + layout, + columnOne, + columnTwo, + columnThree, +}) => { + switch (layout) { + case 'oneColumn': { + return ( + + + {columnOne.enableLink && ( + + )} + + ) + } + + case 'halfAndHalf': + case 'twoThirdsOneThird': { + let col1Cols = 6 + let col2Cols = 6 + + if (layout === 'twoThirdsOneThird') { + col1Cols = 8 + col2Cols = 4 + } + + return ( + + + + {columnOne.enableLink && ( + + )} + + + + {columnTwo.enableLink && ( + + )} + + + ) + } + + case 'threeColumns': { + return ( + + + + {columnOne.enableLink && ( + + )} + + + + {columnTwo.enableLink && ( + + )} + + + + {columnThree.enableLink && ( + + )} + + + ) + } + default: + break + } + + return null +} + +export const ContentBlock: React.FC = (props) => { + return ( + + + + + + ) +} diff --git a/examples/cms-payload/components/Blocks/MediaBlock/index.module.scss b/examples/cms-payload/components/Blocks/MediaBlock/index.module.scss new file mode 100644 index 0000000000000..92c84c084ab1d --- /dev/null +++ b/examples/cms-payload/components/Blocks/MediaBlock/index.module.scss @@ -0,0 +1,7 @@ +.mediaBlock { + position: relative; +} + +.caption { + margin-top: var(--base); +} diff --git a/examples/cms-payload/components/Blocks/MediaBlock/index.tsx b/examples/cms-payload/components/Blocks/MediaBlock/index.tsx new file mode 100644 index 0000000000000..cd194198d069d --- /dev/null +++ b/examples/cms-payload/components/Blocks/MediaBlock/index.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { Gutter } from '../../Gutter' +import { Media } from '../../Media' +import { Media as MediaType } from '../../../payload-types' +import RichText from '../../RichText' +import classes from './index.module.scss' + +export const MediaBlock: React.FC<{ + media?: MediaType + caption?: string + position?: 'default' | 'fullscreen' + mediaBackgroundColor?: string +}> = (props) => { + const { media, caption, position = 'default' } = props + + return ( +
+ {position === 'fullscreen' && ( +
+ +
+ )} + {position === 'default' && ( + +
+ +
+
+ )} + {caption && ( + + + + )} +
+ ) +} diff --git a/examples/cms-payload/components/Blocks/index.tsx b/examples/cms-payload/components/Blocks/index.tsx new file mode 100644 index 0000000000000..7ac6f1ebc5229 --- /dev/null +++ b/examples/cms-payload/components/Blocks/index.tsx @@ -0,0 +1,79 @@ +'use client' + +import React, { Fragment } from 'react' +import { Page } from '../../payload-types' +import { toKebabCase } from '../../utilities/toKebabCase' +import { BackgroundColor } from '../BackgroundColor' +import { VerticalPaddingOptions } from '../VerticalPadding' +import { CallToActionBlock } from './CallToAction' +import { ContentBlock } from './Content' +import { MediaBlock } from './MediaBlock' + +const blockComponents = { + cta: CallToActionBlock, + content: ContentBlock, + mediaBlock: MediaBlock, +} + +const Blocks: React.FC<{ + blocks: Page['layout'] +}> = (props) => { + const { blocks } = props + + const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0 + + if (hasBlocks) { + return ( + + {blocks.map((block, index) => { + const { blockName, blockType } = block + + if (blockType && blockType in blockComponents) { + const Block = blockComponents[blockType] + const backgroundColor = block[`${blockType}BackgroundColor`] + const prevBlock = blocks[index - 1] + const nextBlock = blocks[index + 1] + + const prevBlockBackground = + prevBlock?.[`${prevBlock.blockType}BackgroundColor`] + const nextBlockBackground = + nextBlock?.[`${nextBlock.blockType}BackgroundColor`] + + let paddingTop: VerticalPaddingOptions = 'large' + let paddingBottom: VerticalPaddingOptions = 'large' + + if (backgroundColor === prevBlockBackground) { + paddingTop = 'medium' + } + + if (backgroundColor === nextBlockBackground) { + paddingBottom = 'medium' + } + + if (Block) { + return ( + + + + ) + } + } + return null + })} + + ) + } + + return null +} + +export default Blocks diff --git a/examples/cms-payload/components/Button/index.module.scss b/examples/cms-payload/components/Button/index.module.scss new file mode 100644 index 0000000000000..56a0141bd3907 --- /dev/null +++ b/examples/cms-payload/components/Button/index.module.scss @@ -0,0 +1,51 @@ +@import '../../css/type.scss'; + +.content { + display: flex; + align-items: center; + justify-content: space-around; + + svg { + margin-right: calc(var(--base) / 2); + width: var(--base); + height: var(--base); + } +} + +.label { + @extend %label; + display: flex; + align-items: center; +} + +.button { + text-decoration: none; + display: inline-flex; + padding: 12px 18px; + margin-bottom: var(--base); +} + +.primary--white { + background-color: black; + color: white; +} + +.primary--black { + background-color: white; + color: black; +} + +.secondary--white { + background-color: white; + box-shadow: inset 0 0 0 1px black; +} + +.secondary--black { + background-color: black; + box-shadow: inset 0 0 0 1px white; +} + +.appearance--default { + padding: 0; + margin-left: -8px; +} diff --git a/examples/cms-payload/components/Button/index.tsx b/examples/cms-payload/components/Button/index.tsx new file mode 100644 index 0000000000000..a60f7ae57b654 --- /dev/null +++ b/examples/cms-payload/components/Button/index.tsx @@ -0,0 +1,60 @@ +import Link from 'next/link' +import React, { ElementType } from 'react' +import { useBackgroundColor } from '../BackgroundColor' +import { Chevron } from '../icons/Chevron' +import classes from './index.module.scss' + +export type Props = { + label?: string + appearance?: 'default' | 'primary' | 'secondary' + el?: 'button' | 'link' | 'a' + onClick?: () => void + href?: string + newTab?: boolean + className?: string +} + +export const Button: React.FC = ({ + el = 'button', + label, + newTab, + href, + appearance, + className: classNameFromProps, +}) => { + const backgroundColor = useBackgroundColor() + const newTabProps = newTab + ? { target: '_blank', rel: 'noopener noreferrer' } + : {} + const className = [ + classNameFromProps, + classes[`appearance--${appearance}`], + classes[`${appearance}--${backgroundColor}`], + classes.button, + ] + .filter(Boolean) + .join(' ') + + const content = ( +
+ + {label} +
+ ) + + if (el === 'link') { + return ( + + {content} + + ) + } + + const Element: ElementType = el + + return ( + + {content} + + ) +} diff --git a/examples/cms-payload/components/Gutter/index.module.scss b/examples/cms-payload/components/Gutter/index.module.scss new file mode 100644 index 0000000000000..065eb85eaedf9 --- /dev/null +++ b/examples/cms-payload/components/Gutter/index.module.scss @@ -0,0 +1,7 @@ +.gutterLeft { + padding-left: var(--gutter-h); +} + +.gutterRight { + padding-right: var(--gutter-h); +} diff --git a/examples/cms-payload/components/Gutter/index.tsx b/examples/cms-payload/components/Gutter/index.tsx new file mode 100644 index 0000000000000..fe66a5d9c911e --- /dev/null +++ b/examples/cms-payload/components/Gutter/index.tsx @@ -0,0 +1,33 @@ +import React, { forwardRef, Ref } from 'react' +import classes from './index.module.scss' + +type Props = { + left?: boolean + right?: boolean + className?: string + children: React.ReactNode + ref?: Ref +} + +export const Gutter: React.FC = forwardRef( + (props, ref) => { + const { left = true, right = true, className, children } = props + + return ( +
+ {children} +
+ ) + } +) + +Gutter.displayName = 'Gutter' diff --git a/examples/cms-payload/components/Header/MobileMenuModal.tsx b/examples/cms-payload/components/Header/MobileMenuModal.tsx new file mode 100644 index 0000000000000..ffa0085ee4524 --- /dev/null +++ b/examples/cms-payload/components/Header/MobileMenuModal.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { Modal } from '@faceless-ui/modal' +import { HeaderBar } from '.' +import { MainMenu } from '../../payload-types' +import { Gutter } from '../Gutter' +import { CMSLink } from '../Link' + +import classes from './mobileMenuModal.module.scss' + +type Props = { + navItems: MainMenu['navItems'] +} + +export const slug = 'menu-modal' + +export const MobileMenuModal: React.FC = ({ navItems }) => { + return ( + + + + +
+ {navItems.map(({ link }, i) => { + return + })} +
+
+
+ ) +} diff --git a/examples/cms-payload/components/Header/index.module.scss b/examples/cms-payload/components/Header/index.module.scss new file mode 100644 index 0000000000000..167963d4225f1 --- /dev/null +++ b/examples/cms-payload/components/Header/index.module.scss @@ -0,0 +1,36 @@ +@use '../../css/queries.scss' as *; + +.header { + padding: var(--base) 0; + z-index: var(--header-z-index); +} + +.wrap { + display: flex; + justify-content: space-between; +} + +.nav { + a { + text-decoration: none; + margin-left: var(--base); + } + + @include mid-break { + display: none; + } +} + +.mobileMenuToggler { + all: unset; + cursor: pointer; + display: none; + + &[aria-expanded='true'] { + transform: rotate(-25deg); + } + + @include mid-break { + display: block; + } +} diff --git a/examples/cms-payload/components/Header/index.tsx b/examples/cms-payload/components/Header/index.tsx new file mode 100644 index 0000000000000..80c98ff8c3688 --- /dev/null +++ b/examples/cms-payload/components/Header/index.tsx @@ -0,0 +1,53 @@ +import { ModalToggler } from '@faceless-ui/modal' +import Link from 'next/link' +import React from 'react' +import { MainMenu } from '../../payload-types' +import { Gutter } from '../Gutter' +import { MenuIcon } from '../icons/Menu' +import { CMSLink } from '../Link' +import { Logo } from '../Logo' +import { MobileMenuModal, slug as menuModalSlug } from './MobileMenuModal' + +import classes from './index.module.scss' + +type HeaderBarProps = { + children?: React.ReactNode +} +export const HeaderBar: React.FC = ({ children }) => { + return ( +
+ + + + + + {children} + + + + + +
+ ) +} + +export const Header: React.FC<{ mainMenu: MainMenu }> = ({ mainMenu }) => { + const navItems = mainMenu?.navItems || [] + + return ( + <> + + + + + + + ) +} diff --git a/examples/cms-payload/components/Header/mobileMenuModal.module.scss b/examples/cms-payload/components/Header/mobileMenuModal.module.scss new file mode 100644 index 0000000000000..c79e25740d303 --- /dev/null +++ b/examples/cms-payload/components/Header/mobileMenuModal.module.scss @@ -0,0 +1,31 @@ +@use '../../css/common.scss' as *; + +.mobileMenuModal { + position: relative; + width: 100%; + height: 100%; + border: none; + padding: 0; + opacity: 1; + display: none; + + @include mid-break { + display: block; + } +} + +.contentContainer { + padding: 20px; +} + +.mobileMenuItems { + display: flex; + flex-direction: column; + height: 100%; + margin-top: 30px; +} + +.menuItem { + @extend %h4; + margin-top: 0; +} diff --git a/examples/cms-payload/components/Hero/HighImpact/index.module.scss b/examples/cms-payload/components/Hero/HighImpact/index.module.scss new file mode 100644 index 0000000000000..02731f33d4fec --- /dev/null +++ b/examples/cms-payload/components/Hero/HighImpact/index.module.scss @@ -0,0 +1,49 @@ +@import '../../../css/queries'; + +.hero { + padding-top: calc(var(--base) * 3); + + @include mid-break { + padding-top: var(--base); + } +} + +.media { + margin-top: calc(var(--base) * 3); + width: calc(100% + var(--gutter-h)); + position: relative; + + @include mid-break { + margin-top: var(--base); + margin-left: calc(var(--gutter-h) * -1); + width: calc(100% + var(--gutter-h) * 2); + } +} + +.links { + list-style: none; + margin: 0; + padding: 0; + display: flex; + position: absolute; + background: white; + padding: 0 48px 24px 0; + + li { + margin-right: 12px; + } + + @include mid-break { + position: static; + padding: 0 var(--gutter-h); + display: block; + + li { + margin-right: 0; + + > * { + width: 100%; + } + } + } +} diff --git a/examples/cms-payload/components/Hero/HighImpact/index.tsx b/examples/cms-payload/components/Hero/HighImpact/index.tsx new file mode 100644 index 0000000000000..591e954273731 --- /dev/null +++ b/examples/cms-payload/components/Hero/HighImpact/index.tsx @@ -0,0 +1,39 @@ +import { Cell, Grid } from '@faceless-ui/css-grid' +import React from 'react' +import { Page } from '../../../payload-types' +import { Gutter } from '../../Gutter' +import { CMSLink } from '../../Link' +import { Media } from '../../Media' +import RichText from '../../RichText' + +import classes from './index.module.scss' + +export const HighImpactHero: React.FC = ({ + richText, + media, + links, +}) => { + return ( + + + + + + +
+ {Array.isArray(links) && links.length > 0 && ( +
    + {links.map(({ link }, i) => { + return ( +
  • + +
  • + ) + })} +
+ )} + {typeof media === 'object' && } +
+
+ ) +} diff --git a/examples/cms-payload/components/Hero/LowImpact/index.module.scss b/examples/cms-payload/components/Hero/LowImpact/index.module.scss new file mode 100644 index 0000000000000..89e0885da226e --- /dev/null +++ b/examples/cms-payload/components/Hero/LowImpact/index.module.scss @@ -0,0 +1,7 @@ +@use '../../../css/type.scss' as *; + +.richText { + h1 { + @extend %h2; + } +} diff --git a/examples/cms-payload/components/Hero/LowImpact/index.tsx b/examples/cms-payload/components/Hero/LowImpact/index.tsx new file mode 100644 index 0000000000000..be2340dd2e2eb --- /dev/null +++ b/examples/cms-payload/components/Hero/LowImpact/index.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { Cell, Grid } from '@faceless-ui/css-grid' +import { Page } from '../../../payload-types' +import { Gutter } from '../../Gutter' +import RichText from '../../RichText' +import { VerticalPadding } from '../../VerticalPadding' + +import classes from './index.module.scss' + +export const LowImpactHero: React.FC = ({ richText }) => { + return ( + + + + + + + + + + ) +} diff --git a/examples/cms-payload/components/Hero/MediumImpact/index.module.scss b/examples/cms-payload/components/Hero/MediumImpact/index.module.scss new file mode 100644 index 0000000000000..ad7b2b09d3671 --- /dev/null +++ b/examples/cms-payload/components/Hero/MediumImpact/index.module.scss @@ -0,0 +1,66 @@ +@use '../../../css/common.scss' as *; + +.hero { + padding-top: calc(var(--base) * 3); + + @include mid-break { + padding-top: var(--base); + } +} + +.richText { + position: relative; + + h1 { + @extend %h2; + } + + &::after { + content: ''; + display: block; + position: absolute; + width: 100vw; + left: calc(var(--gutter-h) * -1); + height: 200px; + background: linear-gradient(to bottom, var(--color-base-100), transparent); + top: calc(100% + (var(--base) * 2)); + right: 0; + + @include mid-break { + display: none; + } + } +} + +.links { + position: relative; + list-style: none; + margin: 0; + padding: 0; + display: flex; + margin-top: calc(var(--base) * 4); + + li { + margin-right: 12px; + } + + @include mid-break { + display: block; + margin-top: var(--base); + + li { + margin-right: 0; + } + } +} + +.link { + @include mid-break { + width: 100%; + } +} + +.media { + position: relative; + width: calc(100% + var(--gutter-h)); +} diff --git a/examples/cms-payload/components/Hero/MediumImpact/index.tsx b/examples/cms-payload/components/Hero/MediumImpact/index.tsx new file mode 100644 index 0000000000000..bb5b66643f130 --- /dev/null +++ b/examples/cms-payload/components/Hero/MediumImpact/index.tsx @@ -0,0 +1,39 @@ +import { Cell, Grid } from '@faceless-ui/css-grid' +import React from 'react' +import { Page } from '../../../payload-types' +import { Gutter } from '../../Gutter' +import { CMSLink } from '../../Link' +import { Media } from '../../Media' +import RichText from '../../RichText' + +import classes from './index.module.scss' + +export const MediumImpactHero: React.FC = (props) => { + const { richText, media, links } = props + + return ( + + + + + {Array.isArray(links) && ( +
    + {links.map(({ link }, i) => { + return ( +
  • + +
  • + ) + })} +
+ )} +
+ + {typeof media === 'object' && ( + + )} + +
+
+ ) +} diff --git a/examples/cms-payload/components/Hero/index.tsx b/examples/cms-payload/components/Hero/index.tsx new file mode 100644 index 0000000000000..9db47889c9397 --- /dev/null +++ b/examples/cms-payload/components/Hero/index.tsx @@ -0,0 +1,21 @@ +'use client' + +import React from 'react' +import { Page } from '../../payload-types' +import { HighImpactHero } from './HighImpact' +import { MediumImpactHero } from './MediumImpact' +import { LowImpactHero } from './LowImpact' + +const heroes = { + highImpact: HighImpactHero, + mediumImpact: MediumImpactHero, + lowImpact: LowImpactHero, +} + +export const Hero: React.FC = (props) => { + const { type } = props + const HeroToRender = heroes[type] + + if (!HeroToRender) return null + return +} diff --git a/examples/cms-payload/components/Label/index.module.scss b/examples/cms-payload/components/Label/index.module.scss new file mode 100644 index 0000000000000..bf78e79c4da12 --- /dev/null +++ b/examples/cms-payload/components/Label/index.module.scss @@ -0,0 +1,5 @@ +@import '../../css/type.scss'; + +.label { + @extend %label; +} diff --git a/examples/cms-payload/components/Label/index.tsx b/examples/cms-payload/components/Label/index.tsx new file mode 100644 index 0000000000000..e401184f91573 --- /dev/null +++ b/examples/cms-payload/components/Label/index.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import classes from './index.module.scss' + +export const Label: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + return

{children}

+} diff --git a/examples/cms-payload/components/LargeBody/index.module.scss b/examples/cms-payload/components/LargeBody/index.module.scss new file mode 100644 index 0000000000000..46543d854f794 --- /dev/null +++ b/examples/cms-payload/components/LargeBody/index.module.scss @@ -0,0 +1,5 @@ +@import '../../css/type.scss'; + +.largeBody { + @extend %large-body; +} diff --git a/examples/cms-payload/components/LargeBody/index.tsx b/examples/cms-payload/components/LargeBody/index.tsx new file mode 100644 index 0000000000000..5656b6556d838 --- /dev/null +++ b/examples/cms-payload/components/LargeBody/index.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import classes from './index.module.scss' + +export const LargeBody: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + return

{children}

+} diff --git a/examples/cms-payload/components/Layout/index.tsx b/examples/cms-payload/components/Layout/index.tsx new file mode 100644 index 0000000000000..8bbcae3069476 --- /dev/null +++ b/examples/cms-payload/components/Layout/index.tsx @@ -0,0 +1,48 @@ +'use client' + +import { GridProvider } from '@faceless-ui/css-grid' +import { ModalContainer, ModalProvider } from '@faceless-ui/modal' +import React from 'react' +import { Header } from '../Header' +import { MainMenu } from '../../payload-types' +import cssVariables from '../../cssVariables' +import '../../css/app.scss' + +type Props = { + mainMenu: MainMenu + children: React.ReactNode +} + +const Layout = ({ mainMenu, children }: Props): React.ReactElement => { + return ( + + + +
+ {children} + + + + + ) +} + +export default Layout diff --git a/examples/cms-payload/components/Link/index.tsx b/examples/cms-payload/components/Link/index.tsx new file mode 100644 index 0000000000000..8b67e46cd4265 --- /dev/null +++ b/examples/cms-payload/components/Link/index.tsx @@ -0,0 +1,69 @@ +import Link from 'next/link' +import React from 'react' +import { Page } from '../../payload-types' +import { Button } from '../Button' + +type CMSLinkType = { + type?: 'custom' | 'reference' + url?: string + newTab?: boolean + reference?: { + value: string | Page + relationTo: 'pages' + } + label?: string + appearance?: 'default' | 'primary' | 'secondary' + children?: React.ReactNode + className?: string +} + +export const CMSLink: React.FC = ({ + type, + url, + newTab, + reference, + label, + appearance, + children, + className, +}) => { + const href = + type === 'reference' && + typeof reference?.value === 'object' && + reference.value.slug + ? `/${reference.value.slug}` + : url + + if (!appearance) { + const newTabProps = newTab + ? { target: '_blank', rel: 'noopener noreferrer' } + : {} + + if (type === 'custom') { + return ( + + {label && label} + {children && children} + + ) + } + + if (href) { + return ( + + {label && label} + {children && children} + + ) + } + } + + const buttonProps = { + newTab, + href, + appearance, + label, + } + + return @@ -32,7 +33,7 @@ function Counter() { @@ -42,23 +43,25 @@ function Counter() { className={styles.textbox} aria-label="Set increment amount" value={incrementAmount} - onChange={(e) => setIncrementAmount(e.target.value)} + onChange={(e) => setIncrementAmount(Number(e.target.value ?? 0))} /> @@ -66,5 +69,3 @@ function Counter() {
) } - -export default Counter diff --git a/examples/with-redux/src/features/counter/Counter.module.css b/examples/with-redux/app/components/Counter/counter.module.css similarity index 100% rename from examples/with-redux/src/features/counter/Counter.module.css rename to examples/with-redux/app/components/Counter/counter.module.css diff --git a/examples/with-redux/app/components/Nav.tsx b/examples/with-redux/app/components/Nav.tsx new file mode 100644 index 0000000000000..3cf3ed002ce0b --- /dev/null +++ b/examples/with-redux/app/components/Nav.tsx @@ -0,0 +1,31 @@ +'use client' + +/* Core */ +import Link from 'next/link' +import { usePathname } from 'next/navigation' + +/* Instruments */ +import styles from '../styles/layout.module.css' + +export const Nav = () => { + const pathname = usePathname() + + return ( + + ) +} diff --git a/examples/with-redux/public/favicon.ico b/examples/with-redux/app/icon.ico similarity index 100% rename from examples/with-redux/public/favicon.ico rename to examples/with-redux/app/icon.ico diff --git a/examples/with-redux/app/layout.tsx b/examples/with-redux/app/layout.tsx new file mode 100644 index 0000000000000..c071150ee449f --- /dev/null +++ b/examples/with-redux/app/layout.tsx @@ -0,0 +1,66 @@ +/* Components */ +import { Providers } from '@/lib/providers' +import { Nav } from './components/Nav' + +/* Instruments */ +import styles from './styles/layout.module.css' +import './styles/globals.css' + +export default function RootLayout(props: React.PropsWithChildren) { + return ( + + + +
+
+ + +
+ ) +} diff --git a/examples/with-redux/app/page.tsx b/examples/with-redux/app/page.tsx new file mode 100644 index 0000000000000..559704d119ecc --- /dev/null +++ b/examples/with-redux/app/page.tsx @@ -0,0 +1,10 @@ +/* Components */ +import { Counter } from './components/Counter/Counter' + +export default function IndexPage() { + return +} + +export const metadata = { + title: 'Redux Toolkit', +} diff --git a/examples/with-redux/app/styles/globals.css b/examples/with-redux/app/styles/globals.css new file mode 100644 index 0000000000000..0665acd3f5fe8 --- /dev/null +++ b/examples/with-redux/app/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + min-height: 100vh; + padding: 0; + margin: 0; + font-family: system-ui, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/examples/with-redux/app/styles/layout.module.css b/examples/with-redux/app/styles/layout.module.css new file mode 100644 index 0000000000000..a9ff48222af67 --- /dev/null +++ b/examples/with-redux/app/styles/layout.module.css @@ -0,0 +1,77 @@ +.container { + display: grid; + grid-template-areas: + 'nav' + 'header' + 'main' + 'footer'; + grid-template-rows: auto auto 1fr 36px; + align-items: center; + min-height: 100vh; +} + +.logo { + height: 40vmin; + pointer-events: none; +} + +.header { + grid-area: header; +} + +.main { + grid-area: main; +} + +.header, +.main { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.footer { + grid-area: footer; + justify-self: center; +} + +.nav { + grid-area: nav; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px; + font-size: calc(10px + 2vmin); +} + +.link:hover { + text-decoration: underline; +} + +.link { + color: #704cb6; +} + +.link.active { + text-decoration: underline; +} + +@media (prefers-reduced-motion: no-preference) { + .logo { + animation: logo-float infinite 3s ease-in-out; + } +} + +@keyframes logo-float { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(10px); + } + 100% { + transform: translateY(0px); + } +} diff --git a/examples/with-redux/app/verify/page.tsx b/examples/with-redux/app/verify/page.tsx new file mode 100644 index 0000000000000..2af0400962e5f --- /dev/null +++ b/examples/with-redux/app/verify/page.tsx @@ -0,0 +1,11 @@ +export default function VerifyPage() { + return ( + <> +

Verify page

+

+ This page is intended to verify that Redux state is persisted across + page navigations. +

+ + ) +} diff --git a/examples/with-redux/jest.config.ts b/examples/with-redux/jest.config.ts deleted file mode 100644 index 174ee5dcf423f..0000000000000 --- a/examples/with-redux/jest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { InitialOptionsTsJest } from 'ts-jest/dist/types' - -const config: InitialOptionsTsJest = { - preset: 'ts-jest', - setupFilesAfterEnv: ['/setupTests.ts'], - transform: { - '.+\\.(css|styl|less|sass|scss)$': 'jest-css-modules-transform', - }, - testEnvironment: 'jsdom', - globals: { - 'ts-jest': { - tsconfig: 'tsconfig.test.json', - }, - }, -} - -export default config diff --git a/examples/with-redux/lib/providers.tsx b/examples/with-redux/lib/providers.tsx new file mode 100644 index 0000000000000..4150f7d22dff6 --- /dev/null +++ b/examples/with-redux/lib/providers.tsx @@ -0,0 +1,11 @@ +'use client' + +/* Core */ +import { Provider } from 'react-redux' + +/* Instruments */ +import { reduxStore } from '@/lib/redux' + +export const Providers = (props: React.PropsWithChildren) => { + return {props.children} +} diff --git a/examples/with-redux/lib/redux/createAppAsyncThunk.ts b/examples/with-redux/lib/redux/createAppAsyncThunk.ts new file mode 100644 index 0000000000000..df0b3ebc67410 --- /dev/null +++ b/examples/with-redux/lib/redux/createAppAsyncThunk.ts @@ -0,0 +1,14 @@ +/* Core */ +import { createAsyncThunk } from '@reduxjs/toolkit' + +/* Instruments */ +import type { ReduxState, ReduxDispatch } from './store' + +/** + * ? A utility function to create a typed Async Thnuk Actions. + */ +export const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: ReduxState + dispatch: ReduxDispatch + rejectValue: string +}>() diff --git a/examples/with-redux/lib/redux/index.ts b/examples/with-redux/lib/redux/index.ts new file mode 100644 index 0000000000000..e6f7630ff02b7 --- /dev/null +++ b/examples/with-redux/lib/redux/index.ts @@ -0,0 +1,2 @@ +export * from './store' +export * from './slices' diff --git a/examples/with-redux/lib/redux/middleware.ts b/examples/with-redux/lib/redux/middleware.ts new file mode 100644 index 0000000000000..ba0940c76e0d9 --- /dev/null +++ b/examples/with-redux/lib/redux/middleware.ts @@ -0,0 +1,20 @@ +/* Core */ +import { createLogger } from 'redux-logger' + +const middleware = [ + createLogger({ + duration: true, + timestamp: false, + collapsed: true, + colors: { + title: () => '#139BFE', + prevState: () => '#1C5FAF', + action: () => '#149945', + nextState: () => '#A47104', + error: () => '#ff0005', + }, + predicate: () => typeof window !== 'undefined', + }), +] + +export { middleware } diff --git a/examples/with-redux/lib/redux/rootReducer.ts b/examples/with-redux/lib/redux/rootReducer.ts new file mode 100644 index 0000000000000..4023a34eb8774 --- /dev/null +++ b/examples/with-redux/lib/redux/rootReducer.ts @@ -0,0 +1,6 @@ +/* Instruments */ +import { counterSlice } from './slices' + +export const reducer = { + counter: counterSlice.reducer, +} diff --git a/examples/with-redux/lib/redux/slices/counterSlice/counterSlice.ts b/examples/with-redux/lib/redux/slices/counterSlice/counterSlice.ts new file mode 100644 index 0000000000000..fa1fe58a9afcc --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/counterSlice.ts @@ -0,0 +1,50 @@ +/* Core */ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' + +/* Instruments */ +import { incrementAsync } from './thunks' + +const initialState: CounterSliceState = { + value: 0, + status: 'idle', +} + +export const counterSlice = createSlice({ + name: 'counter', + initialState, + // The `reducers` field lets us define reducers and generate associated actions + reducers: { + increment: (state) => { + // Redux Toolkit allows us to write "mutating" logic in reducers. It + // doesn't actually mutate the state because it uses the Immer library, + // which detects changes to a "draft state" and produces a brand new + // immutable state based off those changes + state.value += 1 + }, + decrement: (state) => { + state.value -= 1 + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload + }, + }, + // The `extraReducers` field lets the slice handle actions defined elsewhere, + // including actions generated by createAsyncThunk or in other slices. + extraReducers: (builder) => { + builder + .addCase(incrementAsync.pending, (state) => { + state.status = 'loading' + }) + .addCase(incrementAsync.fulfilled, (state, action) => { + state.status = 'idle' + state.value += action.payload + }) + }, +}) + +/* Types */ +export interface CounterSliceState { + value: number + status: 'idle' | 'loading' | 'failed' +} diff --git a/examples/with-redux/lib/redux/slices/counterSlice/fetchIdentityCount.ts b/examples/with-redux/lib/redux/slices/counterSlice/fetchIdentityCount.ts new file mode 100644 index 0000000000000..4e444820fbee2 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/fetchIdentityCount.ts @@ -0,0 +1,12 @@ +export const fetchIdentityCount = async ( + amount = 1 +): Promise<{ data: number }> => { + const response = await fetch('http://localhost:3000/api/identity-count', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ amount }), + }) + const result = await response.json() + + return result +} diff --git a/examples/with-redux/lib/redux/slices/counterSlice/index.ts b/examples/with-redux/lib/redux/slices/counterSlice/index.ts new file mode 100644 index 0000000000000..9609b3a0687ca --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/index.ts @@ -0,0 +1,3 @@ +export * from './counterSlice' +export * from './thunks' +export * from './selectors' diff --git a/examples/with-redux/lib/redux/slices/counterSlice/selectors.ts b/examples/with-redux/lib/redux/slices/counterSlice/selectors.ts new file mode 100644 index 0000000000000..5e6261408c030 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/selectors.ts @@ -0,0 +1,7 @@ +/* Instruments */ +import type { ReduxState } from '@/lib/redux' + +// The function below is called a selector and allows us to select a value from +// the state. Selectors can also be defined inline where they're used instead of +// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` +export const selectCount = (state: ReduxState) => state.counter.value diff --git a/examples/with-redux/lib/redux/slices/counterSlice/thunks.ts b/examples/with-redux/lib/redux/slices/counterSlice/thunks.ts new file mode 100644 index 0000000000000..5668498cd0a36 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/counterSlice/thunks.ts @@ -0,0 +1,33 @@ +/* Instruments */ +import { createAppAsyncThunk } from '@/lib/redux/createAppAsyncThunk' +import { fetchIdentityCount } from './fetchIdentityCount' +import { selectCount } from './selectors' +import { counterSlice } from './counterSlice' +import type { ReduxThunkAction } from '@/lib/redux' + +// The function below is called a thunk and allows us to perform async logic. It +// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This +// will call the thunk with the `dispatch` function as the first argument. Async +// code can then be executed and other actions can be dispatched. Thunks are +// typically used to make async requests. +export const incrementAsync = createAppAsyncThunk( + 'counter/fetchIdentityCount', + async (amount: number) => { + const response = await fetchIdentityCount(amount) + + // The value we return becomes the `fulfilled` action payload + return response.data + } +) + +// We can also write thunks by hand, which may contain both sync and async logic. +// Here's an example of conditionally dispatching actions based on current state. +export const incrementIfOddAsync = + (amount: number): ReduxThunkAction => + (dispatch, getState) => { + const currentValue = selectCount(getState()) + + if (currentValue % 2 === 1) { + dispatch(counterSlice.actions.incrementByAmount(amount)) + } + } diff --git a/examples/with-redux/lib/redux/slices/index.ts b/examples/with-redux/lib/redux/slices/index.ts new file mode 100644 index 0000000000000..6540ea7467869 --- /dev/null +++ b/examples/with-redux/lib/redux/slices/index.ts @@ -0,0 +1 @@ +export * from './counterSlice' diff --git a/examples/with-redux/lib/redux/store.ts b/examples/with-redux/lib/redux/store.ts new file mode 100644 index 0000000000000..0d23fc8f509ba --- /dev/null +++ b/examples/with-redux/lib/redux/store.ts @@ -0,0 +1,46 @@ +/* Core */ +import { + configureStore, + type ConfigureStoreOptions, + type ThunkAction, + type Action, +} from '@reduxjs/toolkit' +import { + useSelector as useReduxSelector, + useDispatch as useReduxDispatch, + type TypedUseSelectorHook, +} from 'react-redux' + +/* Instruments */ +import { reducer } from './rootReducer' +import { middleware } from './middleware' + +const configreStoreDefaultOptions: ConfigureStoreOptions = { reducer } + +export const makeReduxStore = ( + options: ConfigureStoreOptions = configreStoreDefaultOptions +) => { + const store = configureStore(options) + + return store +} + +export const reduxStore = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => { + return getDefaultMiddleware().concat(middleware) + }, +}) +export const useDispatch = () => useReduxDispatch() +export const useSelector: TypedUseSelectorHook = useReduxSelector + +/* Types */ +export type ReduxStore = typeof reduxStore +export type ReduxState = ReturnType +export type ReduxDispatch = typeof reduxStore.dispatch +export type ReduxThunkAction = ThunkAction< + ReturnType, + ReduxState, + unknown, + Action +> diff --git a/examples/with-redux/next.config.mjs b/examples/with-redux/next.config.mjs new file mode 100644 index 0000000000000..94be31c3d55d8 --- /dev/null +++ b/examples/with-redux/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +export default nextConfig diff --git a/examples/with-redux/package.json b/examples/with-redux/package.json index 23b138a05f013..02db447cef8a9 100644 --- a/examples/with-redux/package.json +++ b/examples/with-redux/package.json @@ -3,30 +3,21 @@ "scripts": { "dev": "next", "build": "next build", - "start": "next start", - "type-check": "tsc", - "test": "jest" + "start": "next start" }, "dependencies": { - "@reduxjs/toolkit": "^1.3.6", + "@reduxjs/toolkit": "1.9.5", "next": "latest", - "react": "^18.1.0", - "react-dom": "^18.1.0", - "react-redux": "^7.2.0" + "react": "18.2.0", + "react-dom": "18.2.0", + "react-redux": "8.1.0" }, "devDependencies": { - "@testing-library/jest-dom": "^5.0.0", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^13.0.0", - "@types/jest": "^27.0.1", - "@types/node": "^16.9.1", - "@types/react": "^18.0.10", - "@types/react-dom": "^18.0.5", - "@types/react-redux": "^7.1.18", - "jest": "^27.2.0", - "jest-css-modules-transform": "^4.2.0", - "ts-jest": "^27.0.5", - "ts-node": "^10.2.1", - "typescript": "^4.3.4" + "@types/node": "20.3.1", + "@types/react": "18.2.12", + "@types/react-dom": "18.2.5", + "@types/redux-logger": "3.0.9", + "redux-logger": "3.0.6", + "typescript": "5.1.3" } } diff --git a/examples/with-redux/setupTests.ts b/examples/with-redux/setupTests.ts deleted file mode 100644 index 7aa390db552bd..0000000000000 --- a/examples/with-redux/setupTests.ts +++ /dev/null @@ -1,4 +0,0 @@ -import '@testing-library/jest-dom' -import { loadEnvConfig } from '@next/env' - -loadEnvConfig(__dirname, true, { info: () => null, error: console.error }) diff --git a/examples/with-redux/src/features/counter/Counter.spec.tsx b/examples/with-redux/src/features/counter/Counter.spec.tsx deleted file mode 100644 index 558c4104ce0e6..0000000000000 --- a/examples/with-redux/src/features/counter/Counter.spec.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { render, screen } from '@testing-library/react' -import user from '@testing-library/user-event' -import { Provider } from 'react-redux' - -jest.mock('./counterAPI', () => ({ - fetchCount: (amount: number) => - new Promise<{ data: number }>((resolve) => - setTimeout(() => resolve({ data: amount }), 500) - ), -})) - -import { makeStore } from '../../store' -import Counter from './Counter' - -describe('', () => { - it('renders the component', () => { - const store = makeStore() - - render( - - - - ) - - expect(screen.getByText('0')).toBeInTheDocument() - }) - - it('decrements the value', () => { - const store = makeStore() - - render( - - - - ) - - user.click(screen.getByRole('button', { name: /decrement value/i })) - - expect(screen.getByText('-1')).toBeInTheDocument() - }) - - it('increments the value', () => { - const store = makeStore() - - render( - - - - ) - - user.click(screen.getByRole('button', { name: /increment value/i })) - - expect(screen.getByText('1')).toBeInTheDocument() - }) - - it('increments by amount', () => { - const store = makeStore() - - render( - - - - ) - - user.type(screen.getByLabelText(/set increment amount/i), '{backspace}5') - user.click(screen.getByRole('button', { name: /add amount/i })) - - expect(screen.getByText('5')).toBeInTheDocument() - }) - - it('increments async', async () => { - const store = makeStore() - - render( - - - - ) - - user.type(screen.getByLabelText(/set increment amount/i), '{backspace}3') - user.click(screen.getByRole('button', { name: /add async/i })) - - await expect(screen.findByText('3')).resolves.toBeInTheDocument() - }) - - it('increments if amount is odd', async () => { - const store = makeStore() - - render( - - - - ) - - user.click(screen.getByRole('button', { name: /add if odd/i })) - - expect(screen.getByText('0')).toBeInTheDocument() - - user.click(screen.getByRole('button', { name: /increment value/i })) - user.type(screen.getByLabelText(/set increment amount/i), '{backspace}8') - user.click(screen.getByRole('button', { name: /add if odd/i })) - - await expect(screen.findByText('9')).resolves.toBeInTheDocument() - }) -}) diff --git a/examples/with-redux/src/features/counter/counterAPI.ts b/examples/with-redux/src/features/counter/counterAPI.ts deleted file mode 100644 index 9f6c4bd877ee3..0000000000000 --- a/examples/with-redux/src/features/counter/counterAPI.ts +++ /dev/null @@ -1,12 +0,0 @@ -export async function fetchCount(amount = 1): Promise<{ data: number }> { - const response = await fetch('/api/counter', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ amount }), - }) - const result = await response.json() - - return result -} diff --git a/examples/with-redux/src/features/counter/counterSlice.ts b/examples/with-redux/src/features/counter/counterSlice.ts deleted file mode 100644 index e9e1718019212..0000000000000 --- a/examples/with-redux/src/features/counter/counterSlice.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' - -import type { AppState, AppThunk } from '../../store' -import { fetchCount } from './counterAPI' - -export interface CounterState { - value: number - status: 'idle' | 'loading' | 'failed' -} - -const initialState: CounterState = { - value: 0, - status: 'idle', -} - -// The function below is called a thunk and allows us to perform async logic. It -// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This -// will call the thunk with the `dispatch` function as the first argument. Async -// code can then be executed and other actions can be dispatched. Thunks are -// typically used to make async requests. -export const incrementAsync = createAsyncThunk( - 'counter/fetchCount', - async (amount: number) => { - const response = await fetchCount(amount) - // The value we return becomes the `fulfilled` action payload - return response.data - } -) - -export const counterSlice = createSlice({ - name: 'counter', - initialState, - // The `reducers` field lets us define reducers and generate associated actions - reducers: { - increment: (state) => { - // Redux Toolkit allows us to write "mutating" logic in reducers. It - // doesn't actually mutate the state because it uses the Immer library, - // which detects changes to a "draft state" and produces a brand new - // immutable state based off those changes - state.value += 1 - }, - decrement: (state) => { - state.value -= 1 - }, - // Use the PayloadAction type to declare the contents of `action.payload` - incrementByAmount: (state, action: PayloadAction) => { - state.value += action.payload - }, - }, - // The `extraReducers` field lets the slice handle actions defined elsewhere, - // including actions generated by createAsyncThunk or in other slices. - extraReducers: (builder) => { - builder - .addCase(incrementAsync.pending, (state) => { - state.status = 'loading' - }) - .addCase(incrementAsync.fulfilled, (state, action) => { - state.status = 'idle' - state.value += action.payload - }) - }, -}) - -export const { increment, decrement, incrementByAmount } = counterSlice.actions - -// The function below is called a selector and allows us to select a value from -// the state. Selectors can also be defined inline where they're used instead of -// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` -export const selectCount = (state: AppState) => state.counter.value - -// We can also write thunks by hand, which may contain both sync and async logic. -// Here's an example of conditionally dispatching actions based on current state. -export const incrementIfOdd = - (amount: number): AppThunk => - (dispatch, getState) => { - const currentValue = selectCount(getState()) - if (currentValue % 2 === 1) { - dispatch(incrementByAmount(amount)) - } - } - -export default counterSlice.reducer diff --git a/examples/with-redux/src/hooks.ts b/examples/with-redux/src/hooks.ts deleted file mode 100644 index 96d0b0199c759..0000000000000 --- a/examples/with-redux/src/hooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { ChangeEvent } from 'react' -import { useEffect, useRef } from 'react' -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' - -import type { AppDispatch, AppState } from './store' - -export const useForm = - (defaultValues: TContent) => - (handler: (content: TContent) => void) => - async (event: ChangeEvent) => { - event.preventDefault() - event.persist() - - const form = event.target as HTMLFormElement - const elements = Array.from(form.elements) as HTMLInputElement[] - const data = elements - .filter((element) => element.hasAttribute('name')) - .reduce( - (object, element) => ({ - ...object, - [`${element.getAttribute('name')}`]: element.value, - }), - defaultValues - ) - await handler(data) - form.reset() - } - -// https://overreacted.io/making-setinterval-declarative-with-react-hooks/ -export const useInterval = (callback: Function, delay: number) => { - const savedCallback = useRef() - useEffect(() => { - savedCallback.current = callback - }, [callback]) - useEffect(() => { - const handler = (...args: any) => savedCallback.current?.(...args) - - if (delay !== null) { - const id = setInterval(handler, delay) - return () => clearInterval(id) - } - }, [delay]) -} - -// Use throughout your app instead of plain `useDispatch` and `useSelector` -export const useAppDispatch = () => useDispatch() - -export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/examples/with-redux/src/pages/_app.tsx b/examples/with-redux/src/pages/_app.tsx deleted file mode 100644 index d99399f5f07b9..0000000000000 --- a/examples/with-redux/src/pages/_app.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import '../styles/globals.css' - -import { Provider } from 'react-redux' -import type { AppProps } from 'next/app' - -import store from '../store' - -export default function MyApp({ Component, pageProps }: AppProps) { - return ( - - - - ) -} diff --git a/examples/with-redux/src/pages/api/counter.ts b/examples/with-redux/src/pages/api/counter.ts deleted file mode 100644 index 1aa9b5093d4a5..0000000000000 --- a/examples/with-redux/src/pages/api/counter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { NextApiHandler } from 'next' - -const countHandler: NextApiHandler = async (request, response) => { - const { amount = 1 } = request.body - - // simulate IO latency - await new Promise((resolve) => setTimeout(resolve, 500)) - - response.json({ data: amount }) -} - -export default countHandler diff --git a/examples/with-redux/src/pages/index.tsx b/examples/with-redux/src/pages/index.tsx deleted file mode 100644 index 2ed50fb6db169..0000000000000 --- a/examples/with-redux/src/pages/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { NextPage } from 'next' -import Head from 'next/head' - -import Counter from '../features/counter/Counter' -import styles from '../styles/Home.module.css' - -const IndexPage: NextPage = () => { - return ( -
- - Redux Toolkit - - -
- logo - -

- Edit src/App.tsx and save to reload. -

- - Learn - - React - - , - - Redux - - , - - Redux Toolkit - - , and - - React Redux - - -
-
- ) -} - -export default IndexPage diff --git a/examples/with-redux/src/store.ts b/examples/with-redux/src/store.ts deleted file mode 100644 index c0e5753bea4ae..0000000000000 --- a/examples/with-redux/src/store.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit' - -import counterReducer from './features/counter/counterSlice' - -export function makeStore() { - return configureStore({ - reducer: { counter: counterReducer }, - }) -} - -const store = makeStore() - -export type AppState = ReturnType - -export type AppDispatch = typeof store.dispatch - -export type AppThunk = ThunkAction< - ReturnType, - AppState, - unknown, - Action -> - -export default store diff --git a/examples/with-redux/src/styles/Home.module.css b/examples/with-redux/src/styles/Home.module.css deleted file mode 100644 index aeef5641ac5d4..0000000000000 --- a/examples/with-redux/src/styles/Home.module.css +++ /dev/null @@ -1,39 +0,0 @@ -.container { - text-align: center; -} - -.logo { - height: 40vmin; - pointer-events: none; -} - -.header { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); -} - -.link { - color: rgb(112, 76, 182); -} - -@media (prefers-reduced-motion: no-preference) { - .logo { - animation: logo-float infinite 3s ease-in-out; - } -} - -@keyframes logo-float { - 0% { - transform: translateY(0); - } - 50% { - transform: translateY(10px); - } - 100% { - transform: translateY(0px); - } -} diff --git a/examples/with-redux/src/styles/globals.css b/examples/with-redux/src/styles/globals.css deleted file mode 100644 index e5e2dcc23baf1..0000000000000 --- a/examples/with-redux/src/styles/globals.css +++ /dev/null @@ -1,16 +0,0 @@ -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} - -a { - color: inherit; - text-decoration: none; -} - -* { - box-sizing: border-box; -} diff --git a/examples/with-redux/tsconfig.json b/examples/with-redux/tsconfig.json index 4fa631c261428..31f3bc8b20a7e 100644 --- a/examples/with-redux/tsconfig.json +++ b/examples/with-redux/tsconfig.json @@ -1,19 +1,24 @@ { + "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "baseUrl": ".", + "paths": { "@/*": ["./*"] }, + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, - "module": "esnext", + "module": "ESNEXT", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/examples/with-redux/tsconfig.test.json b/examples/with-redux/tsconfig.test.json deleted file mode 100644 index 4fd5045d7dcc0..0000000000000 --- a/examples/with-redux/tsconfig.test.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx" - } -} diff --git a/examples/with-supabase-auth-realtime-db/.env.local.example b/examples/with-supabase-auth-realtime-db/.env.local.example deleted file mode 100644 index 477da3d401d67..0000000000000 --- a/examples/with-supabase-auth-realtime-db/.env.local.example +++ /dev/null @@ -1,3 +0,0 @@ -# Update these with your Supabase details from your project settings > API -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \ No newline at end of file diff --git a/examples/with-supabase-auth-realtime-db/README.md b/examples/with-supabase-auth-realtime-db/README.md deleted file mode 100644 index 6efb44d5afdb1..0000000000000 --- a/examples/with-supabase-auth-realtime-db/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Supabase Authentication - -This example shows how to use Supabase Auth on the client and server in both [API Routes](https://nextjs.org/docs/api-routes/introduction) and when using [server-side rendering (SSR)](https://nextjs.org/docs/basic-features/pages#server-side-rendering). - -## Deploy with Vercel - -The Vercel deployment will guide you through creating a Supabase account and project. After installation of the Supabase integration, all relevant environment variables will be set up so that the project is usable immediately after deployment 🚀 - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-supabase-auth-realtime-db&project-name=nextjs-with-supabase-auth&repository-name=nextjs-with-supabase-auth&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv) - -## Running Locally - -1. `cd` into this directory -1. Run `npm install` to install dependencies -1. Create a Supabase account and new project -1. Copy `.env.local.example` into `.env.local` and add the project keys -1. Run `npm run dev` to start the local development server - -## Feedback and issues - -Please file feedback and issues over on the [Supabase GitHub org](https://github.com/supabase/supabase/issues/new/choose). - -## More Supabase examples - -- [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments) -- [Next.js Slack Clone](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone) -- [Next.js Todo List](https://github.com/supabase/supabase/tree/master/examples/todo-list) -- [Next.js Live Tracker Map](https://github.com/supabase/supabase/tree/master/examples/with-leaflet) -- [And many more...](https://github.com/supabase/supabase/tree/master/examples) diff --git a/examples/with-supabase-auth-realtime-db/lib/initSupabase.js b/examples/with-supabase-auth-realtime-db/lib/initSupabase.js deleted file mode 100644 index 75092fc556cd9..0000000000000 --- a/examples/with-supabase-auth-realtime-db/lib/initSupabase.js +++ /dev/null @@ -1,6 +0,0 @@ -import { createClient } from '@supabase/supabase-js' - -export const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY -) diff --git a/examples/with-supabase-auth-realtime-db/package.json b/examples/with-supabase-auth-realtime-db/package.json deleted file mode 100644 index cb248b6b3eaf4..0000000000000 --- a/examples/with-supabase-auth-realtime-db/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "@supabase/supabase-js": "^1.2.1", - "@supabase/ui": "^0.36.2", - "next": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "swr": "^2.0.0" - } -} diff --git a/examples/with-supabase-auth-realtime-db/pages/_app.js b/examples/with-supabase-auth-realtime-db/pages/_app.js deleted file mode 100644 index 34c77f3ab7081..0000000000000 --- a/examples/with-supabase-auth-realtime-db/pages/_app.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Auth } from '@supabase/ui' -import { supabase } from '../lib/initSupabase' -import './../style.css' - -export default function MyApp({ Component, pageProps }) { - return ( -
- - - -
- ) -} diff --git a/examples/with-supabase-auth-realtime-db/pages/api/auth.js b/examples/with-supabase-auth-realtime-db/pages/api/auth.js deleted file mode 100644 index fabb279679e9c..0000000000000 --- a/examples/with-supabase-auth-realtime-db/pages/api/auth.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * NOTE: this file is only needed if you're doing SSR (getServerSideProps)! - */ -import { supabase } from '../../lib/initSupabase' - -export default function handler(req, res) { - supabase.auth.api.setAuthCookie(req, res) -} diff --git a/examples/with-supabase-auth-realtime-db/pages/api/getUser.js b/examples/with-supabase-auth-realtime-db/pages/api/getUser.js deleted file mode 100644 index 2c8fabaf10099..0000000000000 --- a/examples/with-supabase-auth-realtime-db/pages/api/getUser.js +++ /dev/null @@ -1,13 +0,0 @@ -import { supabase } from '../../lib/initSupabase' - -// Example of how to verify and get user data server-side. -const getUser = async (req, res) => { - const token = req.headers.token - - const { data: user, error } = await supabase.auth.api.getUser(token) - - if (error) return res.status(401).json({ error: error.message }) - return res.status(200).json(user) -} - -export default getUser diff --git a/examples/with-supabase-auth-realtime-db/pages/index.js b/examples/with-supabase-auth-realtime-db/pages/index.js deleted file mode 100644 index 1740097cf7cf3..0000000000000 --- a/examples/with-supabase-auth-realtime-db/pages/index.js +++ /dev/null @@ -1,119 +0,0 @@ -import Link from 'next/link' -import useSWR from 'swr' -import { Auth, Card, Typography, Space, Button, Icon } from '@supabase/ui' -import { supabase } from '../lib/initSupabase' -import { useEffect, useState } from 'react' - -const fetcher = ([url, token]) => - fetch(url, { - method: 'GET', - headers: new Headers({ 'Content-Type': 'application/json', token }), - credentials: 'same-origin', - }).then((res) => res.json()) - -const Index = () => { - const { user, session } = Auth.useUser() - const { data, error } = useSWR( - session ? ['/api/getUser', session.access_token] : null, - fetcher - ) - const [authView, setAuthView] = useState('sign_in') - - useEffect(() => { - const { data: authListener } = supabase.auth.onAuthStateChange( - (event, session) => { - if (event === 'PASSWORD_RECOVERY') setAuthView('update_password') - if (event === 'USER_UPDATED') - setTimeout(() => setAuthView('sign_in'), 1000) - // Send session to /api/auth route to set the auth cookie. - // NOTE: this is only needed if you're doing SSR (getServerSideProps)! - fetch('/api/auth', { - method: 'POST', - headers: new Headers({ 'Content-Type': 'application/json' }), - credentials: 'same-origin', - body: JSON.stringify({ event, session }), - }).then((res) => res.json()) - } - ) - - return () => { - authListener.unsubscribe() - } - }, []) - - const View = () => { - if (!user) - return ( - -
- - - Welcome to Supabase Auth - -
- -
- ) - - return ( - - {authView === 'update_password' && ( - - )} - {user && ( - <> - You're signed in - Email: {user.email} - - - {error && ( - Failed to fetch user! - )} - {data && !error ? ( - <> - - User data retrieved server-side (in API route): - - - -
{JSON.stringify(data, null, 2)}
-
- - ) : ( -
Loading...
- )} - - - SSR example with getServerSideProps - - - )} -
- ) - } - - return ( -
- - - -
- ) -} - -export default Index diff --git a/examples/with-supabase-auth-realtime-db/pages/profile.js b/examples/with-supabase-auth-realtime-db/pages/profile.js deleted file mode 100644 index 33e30b1240722..0000000000000 --- a/examples/with-supabase-auth-realtime-db/pages/profile.js +++ /dev/null @@ -1,39 +0,0 @@ -import Link from 'next/link' -import { Card, Typography, Space } from '@supabase/ui' -import { supabase } from '../lib/initSupabase' - -export default function Profile({ user }) { - return ( -
- - - You're signed in - Email: {user.email} - - User data retrieved server-side (from Cookie in getServerSideProps): - - - -
{JSON.stringify(user, null, 2)}
-
- - - Static example with useSWR - -
-
-
- ) -} - -export async function getServerSideProps({ req }) { - const { user } = await supabase.auth.api.getUserByCookie(req) - - if (!user) { - // If no user, redirect to index. - return { props: {}, redirect: { destination: '/', permanent: false } } - } - - // If there is a user, return it. - return { props: { user } } -} diff --git a/examples/with-supabase-auth-realtime-db/style.css b/examples/with-supabase-auth-realtime-db/style.css deleted file mode 100644 index f1bf15cc28835..0000000000000 --- a/examples/with-supabase-auth-realtime-db/style.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - background: #3d3d3d; - font-family: Helvetica, Arial, Sans-Serif; -} diff --git a/examples/with-supabase/.env.local.example b/examples/with-supabase/.env.local.example new file mode 100644 index 0000000000000..693703197a72b --- /dev/null +++ b/examples/with-supabase/.env.local.example @@ -0,0 +1,4 @@ +# Update these with your Supabase details from your project settings > API +# https://app.supabase.com/project/_/settings/api +NEXT_PUBLIC_SUPABASE_URL=your-project-url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key diff --git a/examples/with-supabase-auth-realtime-db/.gitignore b/examples/with-supabase/.gitignore similarity index 100% rename from examples/with-supabase-auth-realtime-db/.gitignore rename to examples/with-supabase/.gitignore diff --git a/examples/with-supabase/README.md b/examples/with-supabase/README.md new file mode 100644 index 0000000000000..8e7904a5a84f0 --- /dev/null +++ b/examples/with-supabase/README.md @@ -0,0 +1,50 @@ +# Supabase Starter + +This starter configures Supabase Auth to use cookies, making the user's session available throughout the entire Next.js app - Client Components, Server Components, Route Handlers, Server Actions and Middleware. + +## Deploy your own + +The Vercel deployment will guide you through creating a Supabase account and project. After installation of the Supabase integration, all relevant environment variables will be set up so that the project is usable immediately after deployment 🚀 + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv) + +## How to use + +1. Create a [new Supabase project](https://database.new) +1. Run `npx create-next-app -e with-supabase myapp` to create a Next.js app using the Supabase Starter template +1. Run `cd myapp` to change into the app's directory +1. Run `npm install` to install dependencies +1. Rename `.env.local.example` to `.env.local` and update the values for `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` from [your Supabase project's API settings](https://app.supabase.com/project/_/settings/api) +1. Run `npm run dev` to start the local development server + +> Check out [the docs for Local Development](https://supabase.com/docs/guides/getting-started/local-development) to also run Supabase locally. + +### Create Table and seed with data (optional) + +Navigate to [your project's SQL Editor](https://app.supabase.com/project/_/sql), click `New query`, paste the following SQL 👇 and click `RUN`. + +```sql +create table if not exists todos ( + id uuid default gen_random_uuid() primary key, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + title text, + is_complete boolean default false +); + +insert into todos(title) +values + ('Create Supabase project'), + ('Create Next.js app from Supabase Starter template'), + ('Keeping building cool stuff!'); +``` + +## Feedback and issues + +Please file feedback and issues over on the [Supabase GitHub org](https://github.com/supabase/supabase/issues/new/choose). + +## More Supabase examples + +- [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments) +- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF) +- [Supabase Auth and the Next.js App Router](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) +- [Next.js Auth Helpers Docs](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) diff --git a/examples/with-supabase/app/auth/callback/route.ts b/examples/with-supabase/app/auth/callback/route.ts new file mode 100644 index 0000000000000..dbb00609f69f6 --- /dev/null +++ b/examples/with-supabase/app/auth/callback/route.ts @@ -0,0 +1,19 @@ +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +export async function GET(request: Request) { + // The `/auth/callback` route is required for the server-side auth flow implemented + // by the Auth Helpers package. It exchanges an auth code for the user's session. + // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange + const requestUrl = new URL(request.url) + const code = requestUrl.searchParams.get('code') + + if (code) { + const supabase = createRouteHandlerClient({ cookies }) + await supabase.auth.exchangeCodeForSession(code) + } + + // URL to redirect to after sign in process completes + return NextResponse.redirect(requestUrl.origin) +} diff --git a/examples/with-supabase/app/client-component-example/page.tsx b/examples/with-supabase/app/client-component-example/page.tsx new file mode 100644 index 0000000000000..6406dcd277591 --- /dev/null +++ b/examples/with-supabase/app/client-component-example/page.tsx @@ -0,0 +1,27 @@ +'use client' + +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' +import { useEffect, useState } from 'react' + +export default function ClientComponent() { + const [todos, setTodos] = useState([]) + + // Create a Supabase client configured to use cookies + const supabase = createClientComponentClient() + + useEffect(() => { + const getTodos = async () => { + // This assumes you have a `todos` table in Supabase. Check out + // the `Create Table and seed with data` section of the README 👇 + // https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md + const { data } = await supabase.from('todos').select() + if (data) { + setTodos(data) + } + } + + getTodos() + }, [supabase, setTodos]) + + return
{JSON.stringify(todos, null, 2)}
+} diff --git a/examples/with-supabase/app/favicon.ico b/examples/with-supabase/app/favicon.ico new file mode 100644 index 0000000000000..718d6fea4835e Binary files /dev/null and b/examples/with-supabase/app/favicon.ico differ diff --git a/examples/with-supabase/app/globals.css b/examples/with-supabase/app/globals.css new file mode 100644 index 0000000000000..fd81e885836d8 --- /dev/null +++ b/examples/with-supabase/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} diff --git a/examples/with-supabase/app/layout.tsx b/examples/with-supabase/app/layout.tsx new file mode 100644 index 0000000000000..1e600e2cb50e7 --- /dev/null +++ b/examples/with-supabase/app/layout.tsx @@ -0,0 +1,22 @@ +import './globals.css' + +export const metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + +
+ {children} +
+ + + ) +} diff --git a/examples/with-supabase/app/login/page.tsx b/examples/with-supabase/app/login/page.tsx new file mode 100644 index 0000000000000..9546799c9ab9c --- /dev/null +++ b/examples/with-supabase/app/login/page.tsx @@ -0,0 +1,105 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' + +export default function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [view, setView] = useState('sign-in') + const router = useRouter() + const supabase = createClientComponentClient() + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault() + await supabase.auth.signUp({ + email, + password, + options: { + emailRedirectTo: `${location.origin}/auth/callback`, + }, + }) + setView('check-email') + } + + const handleSignIn = async (e: React.FormEvent) => { + e.preventDefault() + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + console.log({ data, error }) + router.push('/') + } + + return ( +
+ {view === 'check-email' ? ( +

+ Check {email} to + continue signing up +

+ ) : ( + + + setEmail(e.target.value)} + value={email} + placeholder="you@example.com" + /> + + setPassword(e.target.value)} + value={password} + placeholder="••••••••" + /> + {view === 'sign-in' ? ( + <> + +

+ Don't have an account? + +

+ + ) : null} + {view === 'sign-up' ? ( + <> + +

+ Already have an account? + +

+ + ) : null} + + )} +
+ ) +} diff --git a/examples/with-supabase/app/page.tsx b/examples/with-supabase/app/page.tsx new file mode 100644 index 0000000000000..90a11d9e1b621 --- /dev/null +++ b/examples/with-supabase/app/page.tsx @@ -0,0 +1,94 @@ +import { + createServerActionClient, + createServerComponentClient, +} from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' + +const resources = [ + { + title: 'Cookie-based Auth and the Next.js App Router', + subtitle: + 'This free course by Jon Meyers, shows you how to configure Supabase Auth to use cookies, and steps through some common patterns.', + url: 'https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF', + }, + { + title: 'Supabase Auth Helpers Docs', + subtitle: + 'This template has configured Supabase Auth to use cookies for you, but the docs are a great place to learn more.', + url: 'https://supabase.com/docs/guides/auth/auth-helpers/nextjs', + }, + { + title: 'Supabase Next.js App Router Example', + subtitle: + 'Want to see a code example containing some common patterns with Next.js and Supabase? Check out this repo!', + url: 'https://github.com/supabase/supabase/tree/master/examples/auth/nextjs', + }, +] + +export default async function Index() { + const supabase = createServerComponentClient({ cookies }) + + const { + data: { user }, + } = await supabase.auth.getUser() + + // This is a Protected Route that can only be accessed by authenticated users + // users who are not signed in will be redirected to the `/login` route + if (!user) { + redirect('/login') + } + + const signOut = async () => { + 'use server' + const supabase = createServerActionClient({ cookies }) + await supabase.auth.signOut() + redirect('/login') + } + + return ( +
+

+ Hey, {user.email} +
+ +
+

+ +
+ +

+ Here are some helpful resources to get you started! 👇 +

+ +
+ {resources.map(({ title, subtitle, url }) => ( + +

{title}

+

{subtitle}

+
+ ))} +
+ +

+ Ready to build your app? Head over to `app/page.tsx` 👉 +

+ +
+

+ We have also included examples for creating a Supabase client in: +

+
    +
  • Client Components - `app/client-component-example/page.tsx`
  • +
  • Server Components - `app/server-component-example/page.tsx`
  • +
  • Server Actions - `app/server-action-example/page.tsx`
  • +
  • Route Handlers - `app/route-handler-example/route.ts`
  • +
  • Middleware - `middleware.ts`
  • +
+
+
+ ) +} diff --git a/examples/with-supabase/app/route-hander-example/route.ts b/examples/with-supabase/app/route-hander-example/route.ts new file mode 100644 index 0000000000000..74c90117b4f76 --- /dev/null +++ b/examples/with-supabase/app/route-hander-example/route.ts @@ -0,0 +1,15 @@ +import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' +import { NextResponse } from 'next/server' + +export async function GET() { + // Create a Supabase client configured to use cookies + const supabase = createRouteHandlerClient({ cookies }) + + // This assumes you have a `todos` table in Supabase. Check out + // the `Create Table and seed with data` section of the README 👇 + // https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md + const { data: todos } = await supabase.from('todos').select() + + return NextResponse.json(todos) +} diff --git a/examples/with-supabase/app/server-action-example/page.tsx b/examples/with-supabase/app/server-action-example/page.tsx new file mode 100644 index 0000000000000..5634fc096291e --- /dev/null +++ b/examples/with-supabase/app/server-action-example/page.tsx @@ -0,0 +1,27 @@ +import { createServerActionClient } from '@supabase/auth-helpers-nextjs' +import { revalidatePath } from 'next/cache' +import { cookies } from 'next/headers' + +export default async function ServerAction() { + const addTodo = async (formData: FormData) => { + 'use server' + const title = formData.get('title') + + if (title) { + // Create a Supabase client configured to use cookies + const supabase = createServerActionClient({ cookies }) + + // This assumes you have a `todos` table in Supabase. Check out + // the `Create Table and seed with data` section of the README 👇 + // https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md + await supabase.from('todos').insert({ title }) + revalidatePath('/server-action-example') + } + } + + return ( +
+ +
+ ) +} diff --git a/examples/with-supabase/app/server-component-example/page.tsx b/examples/with-supabase/app/server-component-example/page.tsx new file mode 100644 index 0000000000000..73f1289026068 --- /dev/null +++ b/examples/with-supabase/app/server-component-example/page.tsx @@ -0,0 +1,14 @@ +import { createServerComponentClient } from '@supabase/auth-helpers-nextjs' +import { cookies } from 'next/headers' + +export default async function ServerComponent() { + // Create a Supabase client configured to use cookies + const supabase = createServerComponentClient({ cookies }) + + // This assumes you have a `todos` table in Supabase. Check out + // the `Create Table and seed with data` section of the README 👇 + // https://github.com/vercel/next.js/blob/canary/examples/with-supabase/README.md + const { data: todos } = await supabase.from('todos').select() + + return
{JSON.stringify(todos, null, 2)}
+} diff --git a/examples/with-supabase/middleware.ts b/examples/with-supabase/middleware.ts new file mode 100644 index 0000000000000..2eab4b703bf93 --- /dev/null +++ b/examples/with-supabase/middleware.ts @@ -0,0 +1,17 @@ +import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' +import { NextResponse } from 'next/server' + +import type { NextRequest } from 'next/server' + +export async function middleware(req: NextRequest) { + const res = NextResponse.next() + + // Create a Supabase client configured to use cookies + const supabase = createMiddlewareClient({ req, res }) + + // Refresh session if expired - required for Server Components + // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware + await supabase.auth.getSession() + + return res +} diff --git a/examples/with-supabase/next.config.js b/examples/with-supabase/next.config.js new file mode 100644 index 0000000000000..950e2f42ec32e --- /dev/null +++ b/examples/with-supabase/next.config.js @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + serverActions: true, + }, +} + +module.exports = nextConfig diff --git a/examples/with-supabase/package.json b/examples/with-supabase/package.json new file mode 100644 index 0000000000000..735d579b011b6 --- /dev/null +++ b/examples/with-supabase/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@supabase/auth-helpers-nextjs": "latest", + "autoprefixer": "10.4.14", + "next": "latest", + "postcss": "8.4.24", + "react": "18.2.0", + "react-dom": "18.2.0", + "tailwindcss": "3.3.2", + "typescript": "5.1.3" + }, + "devDependencies": { + "@types/node": "20.3.1", + "@types/react": "18.2.12", + "@types/react-dom": "18.2.5" + } +} diff --git a/examples/with-supabase/postcss.config.js b/examples/with-supabase/postcss.config.js new file mode 100644 index 0000000000000..33ad091d26d8a --- /dev/null +++ b/examples/with-supabase/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/with-supabase/public/next.svg b/examples/with-supabase/public/next.svg new file mode 100644 index 0000000000000..5174b28c565c2 --- /dev/null +++ b/examples/with-supabase/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/with-supabase/public/vercel.svg b/examples/with-supabase/public/vercel.svg new file mode 100644 index 0000000000000..d2f84222734f2 --- /dev/null +++ b/examples/with-supabase/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/with-supabase/tailwind.config.js b/examples/with-supabase/tailwind.config.js new file mode 100644 index 0000000000000..d49788cf91312 --- /dev/null +++ b/examples/with-supabase/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./app/**/*.{js,ts,jsx,tsx,mdx}'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/examples/with-supabase/tsconfig.json b/examples/with-supabase/tsconfig.json new file mode 100644 index 0000000000000..e06a4454ab062 --- /dev/null +++ b/examples/with-supabase/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/lerna.json b/lerna.json index c0431d0794edb..abc79e258bece 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "13.4.6-canary.8" + "version": "13.4.7-canary.1" } diff --git a/package.json b/package.json index 62d7fdd69afba..a0e66643a4010 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "clean-trace-jaeger": "node scripts/rm.mjs test/integration/basic/.next && TRACE_TARGET=JAEGER pnpm next build test/integration/basic", "debug": "cross-env NEXT_TELEMETRY_DISABLED=1 node --inspect packages/next/dist/bin/next", "postinstall": "git config index.skipHash false && node scripts/install-native.mjs", - "version": "npx pnpm@7.24.3 install --no-frozen-lockfile && IS_PUBLISH=yes ./scripts/check-pre-compiled.sh && git add .", + "version": "npx pnpm@7.32.2 install --no-frozen-lockfile && IS_PUBLISH=yes ./scripts/check-pre-compiled.sh && git add .", "prepare": "husky install", "sync-react": "node ./scripts/sync-react.js", "update-google-fonts": "node ./scripts/update-google-fonts.js" @@ -239,5 +239,5 @@ "engines": { "node": ">=16.8.0" }, - "packageManager": "pnpm@7.24.3" + "packageManager": "pnpm@7.32.2" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 5395ddea599a9..8ff40bdabaa43 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 7e7002360f8ac..b60c9f6362890 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "13.4.6-canary.8", + "@next/eslint-plugin-next": "13.4.7-canary.1", "@rushstack/eslint-patch": "^1.1.3", "@typescript-eslint/parser": "^5.42.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 5f6c36b3e3e04..786b2003fa2a4 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "description": "ESLint plugin for NextJS.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index 658dd63a15fd4..9db464d5e1ee3 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index f35c6d74984b7..f2ba331040dc0 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 50e6a26ac0d36..a391687192f6d 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index be8e478327b1f..f7f867c434f61 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 27bb9d5a283c9..6f1fc34beab4d 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index f08cf10770728..2c94c2ada70cf 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 7f295f040e96d..172879935a98d 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 081160ccb1693..122456e58e0e2 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/crates/napi/Cargo.toml b/packages/next-swc/crates/napi/Cargo.toml index 6bc95d46acd58..bd573cdf93687 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -48,9 +48,9 @@ turbo-tasks = { workspace = true } once_cell = { workspace = true } serde = "1" serde_json = "1" -tracing = { version = "0.1.37" } +tracing = { workspace = true } tracing-futures = "0.2.5" -tracing-subscriber = "0.3.9" +tracing-subscriber = { workspace = true } tracing-chrome = "0.5.0" turbopack-binding = { workspace = true, features = [ "__swc_core_binding_napi", diff --git a/packages/next-swc/crates/napi/src/turbopack.rs b/packages/next-swc/crates/napi/src/turbopack.rs index e31c5a2a986dc..04e5c18c2e1ec 100644 --- a/packages/next-swc/crates/napi/src/turbopack.rs +++ b/packages/next-swc/crates/napi/src/turbopack.rs @@ -1,7 +1,14 @@ -use std::convert::TryFrom; +use std::{ + convert::{TryFrom, TryInto}, + path::PathBuf, +}; +use anyhow::Context; use napi::bindgen_prelude::*; -use next_build::{next_build as turbo_next_build, NextBuildOptions}; +use next_build::{ + build as turbo_next_build, build_options::BuildContext, BuildOptions as NextBuildOptions, +}; +use next_core::next_config::{Rewrite, Rewrites, RouteHas}; use next_dev::{devserver_options::DevServerOptions, start_server}; use crate::util::MapErr; @@ -15,95 +22,162 @@ pub async fn start_turbo_dev(options: Buffer) -> napi::Result<()> { #[napi(object, object_to_js = false)] #[derive(Debug)] pub struct NextBuildContext { + // Added by Next.js for next build --turbo specifically. + /// The root directory of the workspace. + pub root: Option, + + /// The project's directory. pub dir: Option, - pub app_dir: Option, - pub pages_dir: Option, - pub rewrites: Option, - pub original_rewrites: Option, - pub original_redirects: Option>, + + /// The build ID. + pub build_id: Option, + + /// The rewrites, as computed by Next.js. + pub rewrites: Option, + // TODO(alexkirsz) These are detected directly by Turbopack for now. + // pub app_dir: Option, + // pub pages_dir: Option, + // TODO(alexkirsz) These are used to generate route types. + // pub original_rewrites: Option, + // pub original_redirects: Option>, } -#[napi(object, object_to_js = false)] -#[derive(Debug)] -pub struct Rewrites { - pub fallback: Vec, - pub after_files: Vec, - pub before_files: Vec, +impl TryFrom for NextBuildOptions { + type Error = napi::Error; + + fn try_from(value: NextBuildContext) -> Result { + Ok(Self { + dir: value.dir.map(PathBuf::try_from).transpose()?, + root: value.root.map(PathBuf::try_from).transpose()?, + log_level: None, + show_all: true, + log_detail: true, + full_stats: true, + memory_limit: None, + build_context: Some(BuildContext { + build_id: value + .build_id + .context("NextBuildContext must provide a build ID")?, + rewrites: value + .rewrites + .context("NextBuildContext must provide rewrites")? + .into(), + }), + }) + } } +/// Keep in sync with [`next_core::next_config::Rewrites`] #[napi(object, object_to_js = false)] #[derive(Debug)] -pub struct Rewrite { - pub source: String, - pub destination: String, +pub struct NapiRewrites { + pub fallback: Vec, + pub after_files: Vec, + pub before_files: Vec, +} + +impl From for Rewrites { + fn from(val: NapiRewrites) -> Self { + Rewrites { + fallback: val + .fallback + .into_iter() + .map(|rewrite| rewrite.into()) + .collect(), + after_files: val + .after_files + .into_iter() + .map(|rewrite| rewrite.into()) + .collect(), + before_files: val + .before_files + .into_iter() + .map(|rewrite| rewrite.into()) + .collect(), + } + } } +/// Keep in sync with [`next_core::next_config::Rewrite`] #[napi(object, object_to_js = false)] #[derive(Debug)] -pub struct Redirect { +pub struct NapiRewrite { pub source: String, pub destination: String, - pub permanent: Option, - pub status_code: Option, - pub has: Option, - pub missing: Option, + pub base_path: Option, + pub locale: Option, + pub has: Option>, + pub missing: Option>, } -#[derive(Debug)] -pub struct RouteHas { - pub r#type: RouteType, - pub key: Option, - pub value: Option, +impl From for Rewrite { + fn from(val: NapiRewrite) -> Self { + Rewrite { + source: val.source, + destination: val.destination, + base_path: val.base_path, + locale: val.locale, + has: val + .has + .map(|has| has.into_iter().map(|has| has.into()).collect()), + missing: val + .missing + .map(|missing| missing.into_iter().map(|missing| missing.into()).collect()), + } + } } +/// Keep in sync with [`next_core::next_config::RouteHas`] #[derive(Debug)] -pub enum RouteType { - Header, - Query, - Cookie, - Host, +pub enum NapiRouteHas { + Header { key: String, value: Option }, + Query { key: String, value: Option }, + Cookie { key: String, value: Option }, + Host { value: String }, } -impl TryFrom for RouteType { - type Error = napi::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "header" => Ok(RouteType::Header), - "query" => Ok(RouteType::Query), - "cookie" => Ok(RouteType::Cookie), - "host" => Ok(RouteType::Host), - _ => Err(napi::Error::new( - napi::Status::InvalidArg, - "Invalid route type", - )), - } - } -} - -impl FromNapiValue for RouteHas { +impl FromNapiValue for NapiRouteHas { unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { let object = Object::from_napi_value(env, napi_val)?; - let r#type = object.get_named_property::("type")?; - Ok(RouteHas { - r#type: RouteType::try_from(r#type)?, - key: object.get("key")?, - value: object.get("value")?, + let type_ = object.get_named_property::("type")?; + Ok(match type_.as_str() { + "header" => NapiRouteHas::Header { + key: object.get_named_property("key")?, + value: object.get_named_property("value")?, + }, + "query" => NapiRouteHas::Query { + key: object.get_named_property("key")?, + value: object.get_named_property("value")?, + }, + "cookie" => NapiRouteHas::Cookie { + key: object.get_named_property("key")?, + value: object.get_named_property("value")?, + }, + "host" => NapiRouteHas::Host { + value: object.get_named_property("value")?, + }, + _ => { + return Err(napi::Error::new( + Status::GenericFailure, + format!("invalid type for RouteHas: {}", type_), + )) + } }) } } -impl From for NextBuildOptions { - fn from(value: NextBuildContext) -> Self { - Self { - dir: value.dir, - memory_limit: None, - full_stats: None, +impl From for RouteHas { + fn from(val: NapiRouteHas) -> Self { + match val { + NapiRouteHas::Header { key, value } => RouteHas::Header { key, value }, + NapiRouteHas::Query { key, value } => RouteHas::Query { key, value }, + NapiRouteHas::Cookie { key, value } => RouteHas::Cookie { key, value }, + NapiRouteHas::Host { value } => RouteHas::Host { value }, } } } #[napi] pub async fn next_build(ctx: NextBuildContext) -> napi::Result<()> { - turbo_next_build(ctx.into()).await.convert_err() + turbo_next_build(ctx.try_into()?).await.convert_err() } diff --git a/packages/next-swc/crates/next-build/Cargo.toml b/packages/next-swc/crates/next-build/Cargo.toml index 804333c3ce07f..cfc2e97f7db81 100644 --- a/packages/next-swc/crates/next-build/Cargo.toml +++ b/packages/next-swc/crates/next-build/Cargo.toml @@ -6,16 +6,67 @@ license = "MPL-2.0" edition = "2021" autobenches = false +[[bin]] +name = "next-build" +path = "src/main.rs" +bench = false +required-features = ["cli"] + +[lib] +bench = false + [features] +# By default, we enable native-tls for reqwest via downstream transitive features. +# This is for the convenience of running daily dev workflows, i.e running +# `cargo xxx` without explicitly specifying features, not that we want to +# promote this as default backend. Actual configuration is done when building next-swc, +# and also turbopack standalone when we have it. +default = ["cli", "custom_allocator", "native-tls"] +cli = ["clap"] +tokio_console = [ + "dep:console-subscriber", + "tokio/tracing", + "turbo-tasks/tokio_tracing", +] native-tls = ["next-core/native-tls"] rustls-tls = ["next-core/rustls-tls"] -custom_allocator = ["turbopack-binding/__turbo_tasks_malloc", "turbopack-binding/__turbo_tasks_malloc_custom_allocator"] +custom_allocator = [ + "turbopack-binding/__turbo_tasks_malloc", + "turbopack-binding/__turbo_tasks_malloc_custom_allocator", +] +serializable = [] +profile = [] [dependencies] -anyhow = "1.0.47" +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive", "env"], optional = true } +console-subscriber = { workspace = true, optional = true } +dunce = { workspace = true } next-core = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } -turbopack-binding = { workspace = true, features = ["__turbo_tasks", "__turbo_tasks_memory"] } +turbopack-binding = { workspace = true, features = [ + "__turbo_tasks", + "__turbo_tasks_malloc", + "__turbo_tasks_memory", + "__turbo_tasks_env", + "__turbo_tasks_fs", + "__turbo_tasks_memory", + "__turbopack", + "__turbopack_build", + "__turbopack_cli_utils", + "__turbopack_core", + "__turbopack_dev", + "__turbopack_ecmascript", + "__turbopack_ecmascript_runtime", + "__turbopack_env", + "__turbopack_node", +] } +turbo-tasks = { workspace = true } [build-dependencies] turbopack-binding = { workspace = true, features = ["__turbo_tasks_build"] } diff --git a/packages/next-swc/crates/next-build/src/build_options.rs b/packages/next-swc/crates/next-build/src/build_options.rs new file mode 100644 index 0000000000000..91fb71b51a224 --- /dev/null +++ b/packages/next-swc/crates/next-build/src/build_options.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; + +use next_core::{next_config::Rewrites, turbopack::core::issue::IssueSeverity}; + +#[derive(Clone, Debug)] +pub struct BuildOptions { + /// The root directory of the workspace. + pub root: Option, + + /// The project's directory. + pub dir: Option, + + /// The maximum memory to use for the build. + pub memory_limit: Option, + + /// The log level to use for the build. + pub log_level: Option, + + /// Whether to show all logs. + pub show_all: bool, + + /// Whether to show detailed logs. + pub log_detail: bool, + + /// Whether to compute full stats. + pub full_stats: bool, + + /// The Next.js build context. + pub build_context: Option, +} + +#[derive(Clone, Debug)] +pub struct BuildContext { + /// The build id. + pub build_id: String, + + /// Next.js config rewrites. + pub rewrites: Rewrites, +} diff --git a/packages/next-swc/crates/next-build/src/lib.rs b/packages/next-swc/crates/next-build/src/lib.rs index bdcfbc7c876e6..05bde3b3a4679 100644 --- a/packages/next-swc/crates/next-build/src/lib.rs +++ b/packages/next-swc/crates/next-build/src/lib.rs @@ -1,35 +1,66 @@ use turbopack_binding::turbo::{ - tasks::{NothingVc, StatsType, TurboTasks, TurboTasksBackendApi}, + tasks::{run_once, TransientInstance, TurboTasks}, tasks_memory::MemoryBackend, }; -pub fn register() { - turbopack_binding::turbo::tasks::register(); - include!(concat!(env!("OUT_DIR"), "/register.rs")); -} +pub mod build_options; +pub mod manifests; +pub(crate) mod next_build; +pub(crate) mod next_pages; -pub struct NextBuildOptions { - pub dir: Option, - pub memory_limit: Option, - pub full_stats: Option, -} +use anyhow::Result; +use turbo_tasks::{StatsType, TurboTasksBackendApi}; + +pub use self::build_options::BuildOptions; -pub async fn next_build(options: NextBuildOptions) -> anyhow::Result<()> { +pub async fn build(options: BuildOptions) -> Result<()> { + #[cfg(feature = "tokio_console")] + console_subscriber::init(); register(); + + setup_tracing(); + let tt = TurboTasks::new(MemoryBackend::new( options.memory_limit.map_or(usize::MAX, |l| l * 1024 * 1024), )); + let stats_type = match options.full_stats { - Some(true) => StatsType::Full, - _ => StatsType::Essential, + true => StatsType::Full, + false => StatsType::Essential, }; tt.set_stats_type(stats_type); - let task = tt.spawn_root_task(move || { - Box::pin(async move { - // run next build here - Ok(NothingVc::new().into()) - }) - }); - tt.wait_task_completion(task, true).await?; + + run_once(tt, async move { + next_build::next_build(TransientInstance::new(options)).await?; + + Ok(()) + }) + .await?; + Ok(()) } + +fn setup_tracing() { + use tracing_subscriber::{prelude::*, EnvFilter, Registry}; + + let subscriber = Registry::default(); + + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + let subscriber = subscriber.with(stdout_log); + + let subscriber = subscriber.with(EnvFilter::from_default_env()); + + subscriber.init(); +} + +pub fn register() { + turbopack_binding::turbo::tasks::register(); + turbopack_binding::turbo::tasks_fs::register(); + turbopack_binding::turbopack::turbopack::register(); + turbopack_binding::turbopack::core::register(); + turbopack_binding::turbopack::node::register(); + turbopack_binding::turbopack::dev::register(); + turbopack_binding::turbopack::build::register(); + next_core::register(); + include!(concat!(env!("OUT_DIR"), "/register.rs")); +} diff --git a/packages/next-swc/crates/next-build/src/main.rs b/packages/next-swc/crates/next-build/src/main.rs new file mode 100644 index 0000000000000..a76e86619e01b --- /dev/null +++ b/packages/next-swc/crates/next-build/src/main.rs @@ -0,0 +1,102 @@ +use std::path::PathBuf; + +use anyhow::Result; +use clap::Parser; +use next_build::BuildOptions; +use turbopack_binding::turbopack::cli_utils::issue::IssueSeverityCliOption; + +#[global_allocator] +static ALLOC: turbopack_binding::turbo::malloc::TurboMalloc = + turbopack_binding::turbo::malloc::TurboMalloc; + +#[derive(Debug, Parser)] +#[clap(author, version, about, long_about = None)] +pub struct BuildCliArgs { + /// The directory of the Next.js application. + /// If no directory is provided, the current directory will be used. + #[clap(value_parser)] + pub dir: Option, + + /// The root directory of the project. Nothing outside of this directory can + /// be accessed. e. g. the monorepo root. + /// If no directory is provided, `dir` will be used. + #[clap(long, value_parser)] + pub root: Option, + + /// Display version of the binary. Noop if used in library mode. + #[clap(long)] + pub display_version: bool, + + /// Filter by issue severity. + #[clap(short, long)] + pub log_level: Option, + + /// Show all log messages without limit. + #[clap(long)] + pub show_all: bool, + + /// Expand the log details. + #[clap(long)] + pub log_detail: bool, + + /// Whether to enable full task stats recording in Turbo Engine. + #[clap(long)] + pub full_stats: bool, + + /// Enable experimental garbage collection with the provided memory limit in + /// MB. + #[clap(long)] + pub memory_limit: Option, +} + +fn main() { + use turbopack_binding::turbo::malloc::TurboMalloc; + + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .on_thread_stop(|| { + TurboMalloc::thread_stop(); + }) + .build() + .unwrap() + .block_on(main_inner()) + .unwrap() +} + +async fn main_inner() -> Result<()> { + let args = BuildCliArgs::parse(); + + if args.display_version { + // Note: enabling git causes trouble with aarch64 linux builds with libz-sys + println!( + "Build Timestamp\t\t{:#?}", + option_env!("VERGEN_BUILD_TIMESTAMP").unwrap_or_else(|| "N/A") + ); + println!( + "Build Version\t\t{:#?}", + option_env!("VERGEN_BUILD_SEMVER").unwrap_or_else(|| "N/A") + ); + println!( + "Cargo Target Triple\t{:#?}", + option_env!("VERGEN_CARGO_TARGET_TRIPLE").unwrap_or_else(|| "N/A") + ); + println!( + "Cargo Profile\t\t{:#?}", + option_env!("VERGEN_CARGO_PROFILE").unwrap_or_else(|| "N/A") + ); + + return Ok(()); + } + + next_build::build(BuildOptions { + dir: args.dir, + root: args.root, + memory_limit: args.memory_limit, + log_level: args.log_level.map(|l| l.0), + show_all: args.show_all, + log_detail: args.log_detail, + full_stats: args.full_stats, + build_context: None, + }) + .await +} diff --git a/packages/next-swc/crates/next-build/src/manifests.rs b/packages/next-swc/crates/next-build/src/manifests.rs new file mode 100644 index 0000000000000..0f85d3e9dcadc --- /dev/null +++ b/packages/next-swc/crates/next-build/src/manifests.rs @@ -0,0 +1,179 @@ +//! Type definitions for the Next.js manifest formats. + +use std::collections::HashMap; + +use next_core::next_config::Rewrites; +use serde::Serialize; + +#[derive(Serialize, Default, Debug)] +pub struct PagesManifest { + #[serde(flatten)] + pub pages: HashMap, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct BuildManifest { + pub dev_files: Vec, + pub amp_dev_files: Vec, + pub polyfill_files: Vec, + pub low_priority_files: Vec, + pub root_main_files: Vec, + pub pages: HashMap>, + pub amp_first_pages: Vec, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase", tag = "version")] +pub enum MiddlewaresManifest { + #[serde(rename = "2")] + MiddlewaresManifestV2(MiddlewaresManifestV2), + #[serde(other)] + Unsupported, +} + +impl Default for MiddlewaresManifest { + fn default() -> Self { + Self::MiddlewaresManifestV2(Default::default()) + } +} + +#[derive(Serialize, Default, Debug)] +pub struct MiddlewaresManifestV2 { + pub sorted_middleware: Vec<()>, + pub middleware: HashMap, + pub functions: HashMap, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ReactLoadableManifest { + #[serde(flatten)] + pub manifest: HashMap, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ReactLoadableManifestEntry { + pub id: u32, + pub files: Vec, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct NextFontManifest { + pub pages: HashMap>, + pub app: HashMap>, + pub app_using_size_adjust: bool, + pub pages_using_size_adjust: bool, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AppPathsManifest { + #[serde(flatten)] + pub edge_server_app_paths: PagesManifest, + #[serde(flatten)] + pub node_server_app_paths: PagesManifest, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ServerReferenceManifest { + #[serde(flatten)] + pub server_actions: ActionManifest, + #[serde(flatten)] + pub edge_server_actions: ActionManifest, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ActionManifest { + #[serde(flatten)] + pub actions: HashMap, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ActionManifestEntry { + pub workers: HashMap, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum ActionManifestWorkerEntry { + String(String), + Number(f64), +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ClientReferenceManifest { + pub client_modules: ManifestNode, + pub ssr_module_mapping: HashMap, + #[serde(rename = "edgeSSRModuleMapping")] + pub edge_ssr_module_mapping: HashMap, + pub css_files: HashMap>, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ClientCssReferenceManifest { + pub css_imports: HashMap>, + pub css_modules: HashMap>, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ManifestNode { + #[serde(flatten)] + pub module_exports: HashMap, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ManifestNodeEntry { + pub id: ModuleId, + pub name: String, + pub chunks: Vec, + pub r#async: bool, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum ModuleId { + String(String), + Number(f64), +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FontManifest(pub Vec); + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FontManifestEntry { + pub url: String, + pub content: String, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AppBuildManifest { + pub pages: HashMap>, +} + +// TODO(alexkirsz) Unify with the one for dev. +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ClientBuildManifest<'a> { + #[serde(rename = "__rewrites")] + pub rewrites: &'a Rewrites, + + pub sorted_pages: &'a [String], + + #[serde(flatten)] + pub pages: HashMap>, +} diff --git a/packages/next-swc/crates/next-build/src/next_build.rs b/packages/next-swc/crates/next-build/src/next_build.rs new file mode 100644 index 0000000000000..d0e555a142b48 --- /dev/null +++ b/packages/next-swc/crates/next-build/src/next_build.rs @@ -0,0 +1,579 @@ +use std::{ + collections::{HashMap, HashSet}, + env::current_dir, + path::{PathBuf, MAIN_SEPARATOR}, +}; + +use anyhow::{anyhow, Context, Result}; +use dunce::canonicalize; +use next_core::{ + self, next_config::load_next_config, pages_structure::find_pages_structure, + turbopack::ecmascript::utils::StringifyJs, url_node::get_sorted_routes, +}; +use serde::Serialize; +use turbo_tasks::{ + graph::{GraphTraversal, ReverseTopological}, + CollectiblesSource, CompletionVc, RawVc, TransientInstance, TransientValue, TryJoinIterExt, + ValueToString, +}; +use turbopack_binding::{ + turbo::tasks_fs::{DiskFileSystemVc, FileContent, FileSystem, FileSystemPathVc, FileSystemVc}, + turbopack::{ + cli_utils::issue::{ConsoleUiVc, LogOptions}, + core::{ + asset::{Asset, AssetVc, AssetsVc}, + environment::ServerAddrVc, + issue::{IssueReporter, IssueReporterVc, IssueSeverity, IssueVc}, + reference::AssetReference, + virtual_fs::VirtualFileSystemVc, + }, + dev::DevChunkingContextVc, + env::dotenv::load_env, + node::execution_context::ExecutionContextVc, + turbopack::evaluate_context::node_build_environment, + }, +}; + +use crate::{ + build_options::{BuildContext, BuildOptions}, + manifests::{ + AppBuildManifest, AppPathsManifest, BuildManifest, ClientBuildManifest, + ClientCssReferenceManifest, ClientReferenceManifest, FontManifest, MiddlewaresManifest, + NextFontManifest, PagesManifest, ReactLoadableManifest, ServerReferenceManifest, + }, + next_pages::page_chunks::get_page_chunks, +}; + +#[turbo_tasks::function] +pub(crate) async fn next_build(options: TransientInstance) -> Result { + let project_root = options + .dir + .as_ref() + .map(canonicalize) + .unwrap_or_else(current_dir) + .context("project directory can't be found")? + .to_str() + .context("project directory contains invalid characters")? + .to_string(); + + let workspace_root = if let Some(root) = options.root.as_ref() { + canonicalize(root) + .context("root directory can't be found")? + .to_str() + .context("root directory contains invalid characters")? + .to_string() + } else { + project_root.clone() + }; + + let browserslist_query = "last 1 Chrome versions, last 1 Firefox versions, last 1 Safari \ + versions, last 1 Edge versions"; + + let log_options = LogOptions { + project_dir: PathBuf::from(project_root.clone()), + current_dir: current_dir().unwrap(), + show_all: options.show_all, + log_detail: options.log_detail, + log_level: options.log_level.unwrap_or(IssueSeverity::Warning), + }; + + let issue_reporter: IssueReporterVc = + ConsoleUiVc::new(TransientInstance::new(log_options)).into(); + let node_fs = node_fs(&project_root, issue_reporter); + let node_root = node_fs.root().join(".next"); + let client_fs = client_fs(&project_root, issue_reporter); + let client_root = client_fs.root().join(".next"); + // TODO(alexkirsz) This should accept a URL for assetPrefix. + // let client_public_fs = VirtualFileSystemVc::new(); + // let client_public_root = client_public_fs.root(); + let workspace_fs = workspace_fs(&workspace_root, issue_reporter); + let project_relative = project_root.strip_prefix(&workspace_root).unwrap(); + let project_relative = project_relative + .strip_prefix(MAIN_SEPARATOR) + .unwrap_or(project_relative) + .replace(MAIN_SEPARATOR, "/"); + let project_root = workspace_fs.root().join(&project_relative); + + let next_router_fs = VirtualFileSystemVc::new().as_file_system(); + let next_router_root = next_router_fs.root(); + + let build_chunking_context = DevChunkingContextVc::builder( + project_root, + node_root, + node_root.join("chunks"), + node_root.join("assets"), + node_build_environment(), + ) + .build(); + + let env = load_env(project_root); + // TODO(alexkirsz) Should this accept `node_root` at all? + let execution_context = ExecutionContextVc::new(project_root, build_chunking_context, env); + let next_config = load_next_config(execution_context.with_layer("next_config")); + + let pages_structure = find_pages_structure(project_root, next_router_root, next_config); + + let page_chunks = get_page_chunks( + pages_structure, + project_root, + execution_context, + node_root, + client_root, + env, + browserslist_query, + next_config, + ServerAddrVc::empty(), + ); + + handle_issues(page_chunks, issue_reporter).await?; + + let filter_pages = std::env::var("NEXT_TURBO_FILTER_PAGES"); + let filter_pages = filter_pages + .as_ref() + .ok() + .map(|filter| filter.split(',').collect::>()); + let filter_pages = filter_pages.as_ref(); + + { + // Client manifest. + let mut build_manifest: BuildManifest = Default::default(); + // Server manifest. + let mut pages_manifest: PagesManifest = Default::default(); + + let build_manifest_path = client_root.join("build-manifest.json"); + let pages_manifest_path = node_root.join("server/pages-manifest.json"); + + let page_chunks_and_url = page_chunks + .await? + .iter() + .map(|page_chunk| async move { + let page_chunk = page_chunk.await?; + let pathname = page_chunk.pathname.await?; + + if let Some(filter_pages) = &filter_pages { + if !filter_pages.contains(pathname.as_str()) { + return Ok(None); + } + } + + // We can't use partitioning for client assets as client assets might be created + // by non-client assets referred from client assets. + // Although this should perhaps be enforced by Turbopack semantics. + let all_node_assets: Vec<_> = all_assets_from_entry(page_chunk.node_chunk) + .await? + .iter() + .map(|asset| async move { + Ok(( + asset.ident().path().await?.is_inside(&*node_root.await?), + asset, + )) + }) + .try_join() + .await? + .into_iter() + .filter_map(|(is_inside, asset)| if is_inside { Some(*asset) } else { None }) + .collect(); + + let client_chunks = page_chunk.client_chunks; + + // We can't use partitioning for client assets as client assets might be created + // by non-client assets referred from client assets. + // Although this should perhaps be enforced by Turbopack semantics. + let all_client_assets: Vec<_> = all_assets_from_entries(client_chunks) + .await? + .iter() + .map(|asset| async move { + Ok(( + asset.ident().path().await?.is_inside(&*client_root.await?), + asset, + )) + }) + .try_join() + .await? + .into_iter() + .filter_map(|(is_inside, asset)| if is_inside { Some(*asset) } else { None }) + .collect(); + + Ok(Some(( + pathname, + page_chunk.node_chunk, + all_node_assets, + client_chunks, + all_client_assets, + ))) + }) + .try_join() + .await? + .into_iter() + .flatten() + .collect::>(); + + { + let build_manifest_dir_path = build_manifest_path.parent().await?; + let pages_manifest_dir_path = pages_manifest_path.parent().await?; + + let mut deduplicated_node_assets = HashMap::new(); + let mut deduplicated_client_assets = HashMap::new(); + + // TODO(alexkirsz) We want all assets to emit them to the output directory, but + // we only want runtime assets in the manifest. Furthermore, the pages + // manifest (server) only wants a single runtime asset, so we need to + // bundle node assets somewhat. + for (pathname, node_chunk, all_node_assets, client_chunks, all_client_assets) in + page_chunks_and_url + { + tracing::debug!("pathname: {}", pathname.to_string(),); + tracing::debug!( + "node chunk: {}", + node_chunk.ident().path().to_string().await? + ); + tracing::debug!( + "client_chunks:\n{}", + client_chunks + .await? + .iter() + .map(|chunk| async move { + Ok(format!(" - {}", chunk.ident().path().to_string().await?)) + }) + .try_join() + .await? + .join("\n") + ); + + // TODO(alexkirsz) Deduplication should not happen at this level, but + // right now we have chunks with the same path being generated + // from different entrypoints, and writing them multiple times causes + // an infinite invalidation loop. + deduplicated_node_assets.extend( + all_node_assets + .into_iter() + .map(|asset| async move { Ok((asset.ident().path().to_string().await?, asset)) }) + .try_join() + .await?, + ); + deduplicated_client_assets.extend( + all_client_assets + .into_iter() + .map(|asset| async move { Ok((asset.ident().path().to_string().await?, asset)) }) + .try_join() + .await? + ); + + let build_manifest_pages_entry = build_manifest + .pages + .entry(pathname.clone_value()) + .or_default(); + for chunk in client_chunks.await?.iter() { + let chunk_path = chunk.ident().path().await?; + if let Some(asset_path) = build_manifest_dir_path.get_path_to(&chunk_path) { + build_manifest_pages_entry.push(asset_path.to_string()); + } + } + + let chunk_path = node_chunk.ident().path().await?; + if let Some(asset_path) = pages_manifest_dir_path.get_path_to(&chunk_path) { + pages_manifest + .pages + .insert(pathname.clone_value(), asset_path.to_string()); + } + } + + tracing::debug!( + "all node assets: {}", + deduplicated_node_assets + .values() + .map(|asset| async move { + Ok(format!(" - {}", asset.ident().path().to_string().await?)) + }) + .try_join() + .await? + .join("\n") + ); + deduplicated_node_assets + .into_values() + .map(|asset| async move { + emit(asset).await?; + Ok(()) + }) + .try_join() + .await?; + + tracing::debug!( + "all client assets: {}", + deduplicated_client_assets + .values() + .map(|asset| async move { + Ok(format!(" - {}", asset.ident().path().to_string().await?)) + }) + .try_join() + .await? + .join("\n") + ); + deduplicated_client_assets + .into_values() + .map(|asset| async move { + emit(asset).await?; + Ok(()) + }) + .try_join() + .await?; + } + + write_placeholder_manifest( + &MiddlewaresManifest::default(), + node_root, + "server/middleware-manifest.json", + ) + .await?; + write_placeholder_manifest( + &NextFontManifest::default(), + node_root, + "server/next-font-manifest.json", + ) + .await?; + write_placeholder_manifest( + &FontManifest::default(), + node_root, + "server/font-manifest.json", + ) + .await?; + write_placeholder_manifest( + &AppPathsManifest::default(), + node_root, + "server/app-paths-manifest.json", + ) + .await?; + write_placeholder_manifest( + &ServerReferenceManifest::default(), + node_root, + "server/server-reference-manifest.json", + ) + .await?; + write_placeholder_manifest( + &ClientReferenceManifest::default(), + node_root, + "server/client-reference-manifest.json", + ) + .await?; + write_placeholder_manifest( + &ClientCssReferenceManifest::default(), + node_root, + "server/flight-server-css-manifest.json", + ) + .await?; + write_placeholder_manifest( + &ReactLoadableManifest::default(), + node_root, + "react-loadable-manifest.json", + ) + .await?; + write_placeholder_manifest( + &AppBuildManifest::default(), + node_root, + "app-build-manifest.json", + ) + .await?; + + if let Some(build_context) = &options.build_context { + let BuildContext { build_id, rewrites } = build_context; + + tracing::debug!("writing _ssgManifest.js for build id: {}", build_id); + + let ssg_manifest_path = format!("static/{build_id}/_ssgManifest.js"); + + let ssg_manifest_fs_path = node_root.join(&ssg_manifest_path); + ssg_manifest_fs_path + .write( + FileContent::Content( + "self.__SSG_MANIFEST=new \ + Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()" + .into(), + ) + .cell(), + ) + .await?; + + build_manifest.low_priority_files.push(ssg_manifest_path); + + let sorted_pages = + get_sorted_routes(&pages_manifest.pages.keys().cloned().collect::>())?; + + let app_dependencies: HashSet<&str> = pages_manifest + .pages + .get("/_app") + .iter() + .map(|s| s.as_str()) + .collect(); + let mut pages = HashMap::new(); + + for page in &sorted_pages { + if page == "_app" { + continue; + } + + let dependencies = pages_manifest + .pages + .get(page) + .iter() + .map(|dep| dep.as_str()) + .filter(|dep| !app_dependencies.contains(*dep)) + .collect::>(); + + if !dependencies.is_empty() { + pages.insert(page.to_string(), dependencies); + } + } + + let client_manifest = ClientBuildManifest { + rewrites, + sorted_pages: &sorted_pages, + pages, + }; + + let client_manifest_path = format!("static/{build_id}/_buildManifest.js"); + + let client_manifest_fs_path = node_root.join(&client_manifest_path); + client_manifest_fs_path + .write( + FileContent::Content( + format!( + "self.__BUILD_MANIFEST={};self.__BUILD_MANIFEST_CB && \ + self.__BUILD_MANIFEST_CB()", + StringifyJs(&client_manifest) + ) + .into(), + ) + .cell(), + ) + .await?; + + build_manifest.low_priority_files.push(client_manifest_path); + } + + // TODO(alexkirsz) These manifests should be assets. + let build_manifest_contents = serde_json::to_string_pretty(&build_manifest)?; + let pages_manifest_contents = serde_json::to_string_pretty(&pages_manifest)?; + + build_manifest_path + .write(FileContent::Content(build_manifest_contents.into()).cell()) + .await?; + pages_manifest_path + .write(FileContent::Content(pages_manifest_contents.into()).cell()) + .await?; + } + + Ok(CompletionVc::immutable()) +} + +#[turbo_tasks::function] +fn emit(asset: AssetVc) -> CompletionVc { + asset.content().write(asset.ident().path()) +} + +#[turbo_tasks::function] +async fn workspace_fs( + workspace_root: &str, + issue_reporter: IssueReporterVc, +) -> Result { + let disk_fs = DiskFileSystemVc::new("workspace".to_string(), workspace_root.to_string()); + handle_issues(disk_fs, issue_reporter).await?; + Ok(disk_fs.into()) +} + +#[turbo_tasks::function] +async fn node_fs(node_root: &str, issue_reporter: IssueReporterVc) -> Result { + let disk_fs = DiskFileSystemVc::new("node".to_string(), node_root.to_string()); + handle_issues(disk_fs, issue_reporter).await?; + Ok(disk_fs.into()) +} + +#[turbo_tasks::function] +async fn client_fs(client_root: &str, issue_reporter: IssueReporterVc) -> Result { + let disk_fs = DiskFileSystemVc::new("client".to_string(), client_root.to_string()); + handle_issues(disk_fs, issue_reporter).await?; + Ok(disk_fs.into()) +} + +async fn handle_issues + CollectiblesSource + Copy>( + source: T, + issue_reporter: IssueReporterVc, +) -> Result<()> { + let issues = IssueVc::peek_issues_with_path(source) + .await? + .strongly_consistent() + .await?; + + let has_fatal = issue_reporter.report_issues( + TransientInstance::new(issues.clone()), + TransientValue::new(source.into()), + ); + + if *has_fatal.await? { + Err(anyhow!("Fatal issue(s) occurred")) + } else { + Ok(()) + } +} + +/// Walks the asset graph from a single asset and collect all referenced assets. +#[turbo_tasks::function] +async fn all_assets_from_entry(entry: AssetVc) -> Result { + Ok(AssetsVc::cell( + ReverseTopological::new() + .skip_duplicates() + .visit([entry], get_referenced_assets) + .await + .completed()? + .into_inner() + .into_iter() + .collect(), + )) +} + +/// Walks the asset graph from multiple assets and collect all referenced +/// assets. +#[turbo_tasks::function] +async fn all_assets_from_entries(entries: AssetsVc) -> Result { + Ok(AssetsVc::cell( + ReverseTopological::new() + .skip_duplicates() + .visit(entries.await?.iter().copied(), get_referenced_assets) + .await + .completed()? + .into_inner() + .into_iter() + .collect(), + )) +} + +/// Computes the list of all chunk children of a given chunk. +async fn get_referenced_assets(asset: AssetVc) -> Result + Send> { + Ok(asset + .references() + .await? + .iter() + .map(|reference| async move { + let primary_assets = reference.resolve_reference().primary_assets().await?; + Ok(primary_assets.clone_value()) + }) + .try_join() + .await? + .into_iter() + .flatten()) +} + +async fn write_placeholder_manifest( + manifest: &T, + node_root: FileSystemPathVc, + path: &str, +) -> Result<()> +where + T: Serialize, +{ + let json = serde_json::to_string_pretty(manifest)?; + let node_path = node_root.join(path); + node_path + .write(FileContent::Content(json.into()).cell()) + .await?; + Ok(()) +} diff --git a/packages/next-swc/crates/next-build/src/next_pages/client_context.rs b/packages/next-swc/crates/next-build/src/next_pages/client_context.rs new file mode 100644 index 0000000000000..1aa60f193fcf2 --- /dev/null +++ b/packages/next-swc/crates/next-build/src/next_pages/client_context.rs @@ -0,0 +1,88 @@ +use anyhow::{bail, Result}; +use next_core::{ + create_page_loader_entry_asset, + turbopack::core::{asset::AssetsVc, chunk::EvaluatableAssetsVc}, +}; +use turbopack_binding::{ + turbo::{ + tasks::{primitives::StringVc, Value}, + tasks_fs::FileSystemPathVc, + }, + turbopack::{ + core::{ + asset::AssetVc, + chunk::{ChunkableAsset, ChunkingContext, ChunkingContextVc}, + context::{AssetContext, AssetContextVc}, + reference_type::ReferenceType, + }, + dev::DevChunkingContextVc, + ecmascript::EcmascriptModuleAssetVc, + }, +}; + +#[turbo_tasks::value] +pub(crate) struct PagesBuildClientContext { + project_root: FileSystemPathVc, + client_root: FileSystemPathVc, + client_asset_context: AssetContextVc, + client_runtime_entries: EvaluatableAssetsVc, +} + +#[turbo_tasks::value_impl] +impl PagesBuildClientContextVc { + #[turbo_tasks::function] + pub fn new( + project_root: FileSystemPathVc, + client_root: FileSystemPathVc, + client_asset_context: AssetContextVc, + client_runtime_entries: EvaluatableAssetsVc, + ) -> PagesBuildClientContextVc { + PagesBuildClientContext { + project_root, + client_root, + client_asset_context, + client_runtime_entries, + } + .cell() + } + + #[turbo_tasks::function] + async fn client_chunking_context(self) -> Result { + let this = self.await?; + + Ok(DevChunkingContextVc::builder( + this.project_root, + this.client_root, + this.client_root.join("static/chunks"), + this.client_root.join("static/media"), + this.client_asset_context.compile_time_info().environment(), + ) + .build()) + } + + #[turbo_tasks::function] + pub async fn client_chunk( + self, + asset: AssetVc, + pathname: StringVc, + reference_type: Value, + ) -> Result { + let this = self.await?; + + let client_asset_page = this.client_asset_context.process(asset, reference_type); + let client_asset_page = + create_page_loader_entry_asset(this.client_asset_context, client_asset_page, pathname); + + let Some(client_module_asset) = EcmascriptModuleAssetVc::resolve_from(client_asset_page).await? else { + bail!("Expected an EcmaScript module asset"); + }; + + let client_chunking_context = self.client_chunking_context(); + + Ok(client_chunking_context.evaluated_chunk_group( + client_module_asset.as_root_chunk(client_chunking_context), + this.client_runtime_entries + .with_entry(client_module_asset.into()), + )) + } +} diff --git a/packages/next-swc/crates/next-build/src/next_pages/mod.rs b/packages/next-swc/crates/next-build/src/next_pages/mod.rs new file mode 100644 index 0000000000000..9620d50b08699 --- /dev/null +++ b/packages/next-swc/crates/next-build/src/next_pages/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod client_context; +pub(crate) mod node_context; +pub(crate) mod page_chunks; diff --git a/packages/next-swc/crates/next-build/src/next_pages/node_context.rs b/packages/next-swc/crates/next-build/src/next_pages/node_context.rs new file mode 100644 index 0000000000000..dade75976bff1 --- /dev/null +++ b/packages/next-swc/crates/next-build/src/next_pages/node_context.rs @@ -0,0 +1,100 @@ +use anyhow::{bail, Result}; +use next_core::{next_client::RuntimeEntriesVc, turbopack::core::chunk::EvaluatableAssetsVc}; +use turbopack_binding::{ + turbo::{tasks::Value, tasks_fs::FileSystemPathVc}, + turbopack::{ + build::BuildChunkingContextVc, + core::{ + asset::AssetVc, + context::{AssetContext, AssetContextVc}, + reference_type::{EntryReferenceSubType, ReferenceType}, + resolve::{parse::RequestVc, pattern::QueryMapVc}, + }, + ecmascript::EcmascriptModuleAssetVc, + }, +}; + +#[turbo_tasks::value] +pub(crate) struct PagesBuildNodeContext { + project_root: FileSystemPathVc, + node_root: FileSystemPathVc, + node_asset_context: AssetContextVc, + node_runtime_entries: EvaluatableAssetsVc, +} + +#[turbo_tasks::value_impl] +impl PagesBuildNodeContextVc { + #[turbo_tasks::function] + pub fn new( + project_root: FileSystemPathVc, + node_root: FileSystemPathVc, + node_asset_context: AssetContextVc, + node_runtime_entries: RuntimeEntriesVc, + ) -> PagesBuildNodeContextVc { + PagesBuildNodeContext { + project_root, + node_root, + node_asset_context, + node_runtime_entries: node_runtime_entries.resolve_entries(node_asset_context), + } + .cell() + } + + #[turbo_tasks::function] + pub async fn resolve_module( + self, + origin: FileSystemPathVc, + package: String, + path: String, + ) -> Result { + let this = self.await?; + let Some(asset) = this + .node_asset_context + .resolve_asset( + origin, + RequestVc::module(package.clone(), Value::new(path.clone().into()), QueryMapVc::none()), + this.node_asset_context.resolve_options(origin, Value::new(ReferenceType::Entry(EntryReferenceSubType::Page))), + Value::new(ReferenceType::Entry(EntryReferenceSubType::Page)) + ) + .primary_assets() + .await? + .first() + .copied() + else { + bail!("module {}/{} not found in {}", package, path, origin.await?); + }; + Ok(asset) + } + + #[turbo_tasks::function] + async fn node_chunking_context(self) -> Result { + let this = self.await?; + + Ok(BuildChunkingContextVc::builder( + this.project_root, + this.node_root, + this.node_root.join("server/pages"), + this.node_root.join("server/assets"), + this.node_asset_context.compile_time_info().environment(), + ) + .build()) + } + + #[turbo_tasks::function] + pub async fn node_chunk( + self, + asset: AssetVc, + reference_type: Value, + ) -> Result { + let this = self.await?; + + let node_asset_page = this.node_asset_context.process(asset, reference_type); + + let Some(node_module_asset) = EcmascriptModuleAssetVc::resolve_from(node_asset_page).await? else { + bail!("Expected an EcmaScript module asset"); + }; + + let chunking_context = self.node_chunking_context(); + Ok(chunking_context.generate_exported_chunk(node_module_asset, this.node_runtime_entries)) + } +} diff --git a/packages/next-swc/crates/next-build/src/next_pages/page_chunks.rs b/packages/next-swc/crates/next-build/src/next_pages/page_chunks.rs new file mode 100644 index 0000000000000..44ac379a8d49c --- /dev/null +++ b/packages/next-swc/crates/next-build/src/next_pages/page_chunks.rs @@ -0,0 +1,355 @@ +use anyhow::Result; +use next_core::{ + env::env_for_js, + mode::NextMode, + next_client::{ + get_client_compile_time_info, get_client_module_options_context, + get_client_resolve_options_context, get_client_runtime_entries, ClientContextType, + RuntimeEntriesVc, RuntimeEntry, + }, + next_client_chunks::NextClientChunksTransitionVc, + next_config::NextConfigVc, + next_server::{ + get_server_compile_time_info, get_server_module_options_context, + get_server_resolve_options_context, ServerContextType, + }, + pages_structure::{ + OptionPagesStructureVc, PagesDirectoryStructure, PagesDirectoryStructureVc, PagesStructure, + PagesStructureItem, PagesStructureVc, + }, + pathname_for_path, + turbopack::core::asset::AssetsVc, + PathType, +}; +use turbopack_binding::{ + turbo::{ + tasks::{primitives::StringVc, Value}, + tasks_env::ProcessEnvVc, + tasks_fs::FileSystemPathVc, + }, + turbopack::{ + core::{ + asset::AssetVc, + context::AssetContextVc, + environment::ServerAddrVc, + reference_type::{EntryReferenceSubType, ReferenceType}, + source_asset::SourceAssetVc, + }, + env::ProcessEnvAssetVc, + node::execution_context::ExecutionContextVc, + turbopack::{transition::TransitionsByNameVc, ModuleAssetContextVc}, + }, +}; + +use super::{client_context::PagesBuildClientContextVc, node_context::PagesBuildNodeContextVc}; + +#[turbo_tasks::value(transparent)] +pub struct PageChunks(Vec); + +#[turbo_tasks::value_impl] +impl PageChunksVc { + #[turbo_tasks::function] + pub fn empty() -> Self { + PageChunks(vec![]).cell() + } +} + +/// Returns a list of page chunks. +#[turbo_tasks::function] +pub async fn get_page_chunks( + pages_structure: OptionPagesStructureVc, + project_root: FileSystemPathVc, + execution_context: ExecutionContextVc, + node_root: FileSystemPathVc, + client_root: FileSystemPathVc, + env: ProcessEnvVc, + browserslist_query: &str, + next_config: NextConfigVc, + node_addr: ServerAddrVc, +) -> Result { + let Some(pages_structure) = *pages_structure.await? else { + return Ok(PageChunksVc::empty()); + }; + let pages_dir = pages_structure.project_path().resolve().await?; + + let mode = NextMode::Build; + + let client_ty = Value::new(ClientContextType::Pages { pages_dir }); + let node_ty = Value::new(ServerContextType::Pages { pages_dir }); + + let client_compile_time_info = get_client_compile_time_info(mode, browserslist_query); + + let transitions = TransitionsByNameVc::cell( + [( + // This is necessary for the next dynamic transform to work. + // TODO(alexkirsz) Should accept client chunking context? But how do we get this? + "next-client-chunks".to_string(), + NextClientChunksTransitionVc::new( + project_root, + execution_context, + client_ty, + mode, + client_root, + client_compile_time_info, + next_config, + ) + .into(), + )] + .into_iter() + .collect(), + ); + + let client_module_options_context = get_client_module_options_context( + project_root, + execution_context, + client_compile_time_info.environment(), + client_ty, + mode, + next_config, + ); + let client_resolve_options_context = get_client_resolve_options_context( + project_root, + client_ty, + mode, + next_config, + execution_context, + ); + let client_asset_context: AssetContextVc = ModuleAssetContextVc::new( + transitions, + client_compile_time_info, + client_module_options_context, + client_resolve_options_context, + ) + .into(); + + let node_compile_time_info = get_server_compile_time_info(node_ty, mode, env, node_addr); + let node_resolve_options_context = get_server_resolve_options_context( + project_root, + node_ty, + mode, + next_config, + execution_context, + ); + let node_module_options_context = get_server_module_options_context( + project_root, + execution_context, + node_ty, + mode, + next_config, + ); + + let node_asset_context = ModuleAssetContextVc::new( + transitions, + node_compile_time_info, + node_module_options_context, + node_resolve_options_context, + ) + .into(); + + let node_runtime_entries = get_node_runtime_entries(project_root, env, next_config); + + let client_runtime_entries = get_client_runtime_entries( + project_root, + env, + client_ty, + mode, + next_config, + execution_context, + ); + let client_runtime_entries = client_runtime_entries.resolve_entries(client_asset_context); + + let node_build_context = PagesBuildNodeContextVc::new( + project_root, + node_root, + node_asset_context, + node_runtime_entries, + ); + let client_build_context = PagesBuildClientContextVc::new( + project_root, + client_root, + client_asset_context, + client_runtime_entries, + ); + + Ok(get_page_chunks_for_root_directory( + node_build_context, + client_build_context, + pages_structure, + )) +} + +#[turbo_tasks::function] +async fn get_page_chunks_for_root_directory( + node_build_context: PagesBuildNodeContextVc, + client_build_context: PagesBuildClientContextVc, + pages_structure: PagesStructureVc, +) -> Result { + let PagesStructure { + app, + document, + error, + api, + pages, + } = *pages_structure.await?; + let mut chunks = vec![]; + + let next_router_root = pages.next_router_path(); + + // This only makes sense on both the client and the server, but they should map + // to different assets (server can be an external module). + let app = app.await?; + chunks.push(get_page_chunk_for_file( + node_build_context, + client_build_context, + SourceAssetVc::new(app.project_path).into(), + next_router_root, + app.next_router_path, + )); + + // This only makes sense on the server. + let document = document.await?; + chunks.push(get_page_chunk_for_file( + node_build_context, + client_build_context, + SourceAssetVc::new(document.project_path).into(), + next_router_root, + document.next_router_path, + )); + + // This only makes sense on both the client and the server, but they should map + // to different assets (server can be an external module). + let error = error.await?; + chunks.push(get_page_chunk_for_file( + node_build_context, + client_build_context, + SourceAssetVc::new(error.project_path).into(), + next_router_root, + error.next_router_path, + )); + + if let Some(api) = api { + chunks.extend( + get_page_chunks_for_directory( + node_build_context, + client_build_context, + api, + next_router_root, + ) + .await? + .iter() + .copied(), + ); + } + + chunks.extend( + get_page_chunks_for_directory( + node_build_context, + client_build_context, + pages, + next_router_root, + ) + .await? + .iter() + .copied(), + ); + + Ok(PageChunksVc::cell(chunks)) +} + +#[turbo_tasks::function] +async fn get_page_chunks_for_directory( + node_build_context: PagesBuildNodeContextVc, + client_build_context: PagesBuildClientContextVc, + pages_structure: PagesDirectoryStructureVc, + next_router_root: FileSystemPathVc, +) -> Result { + let PagesDirectoryStructure { + ref items, + ref children, + .. + } = *pages_structure.await?; + let mut chunks = vec![]; + + for item in items.iter() { + let PagesStructureItem { + project_path, + next_router_path, + specificity: _, + } = *item.await?; + chunks.push(get_page_chunk_for_file( + node_build_context, + client_build_context, + SourceAssetVc::new(project_path).into(), + next_router_root, + next_router_path, + )); + } + + for child in children.iter() { + chunks.extend( + // TODO(alexkirsz) This should be a tree structure instead of a flattened list. + get_page_chunks_for_directory( + node_build_context, + client_build_context, + *child, + next_router_root, + ) + .await? + .iter() + .copied(), + ) + } + + Ok(PageChunksVc::cell(chunks)) +} + +/// A page chunk corresponding to some route. +#[turbo_tasks::value] +pub struct PageChunk { + /// The pathname of the page. + pub pathname: StringVc, + /// The Node.js chunk. + pub node_chunk: AssetVc, + /// The client chunks. + pub client_chunks: AssetsVc, +} + +#[turbo_tasks::function] +async fn get_page_chunk_for_file( + node_build_context: PagesBuildNodeContextVc, + client_build_context: PagesBuildClientContextVc, + page_asset: AssetVc, + next_router_root: FileSystemPathVc, + next_router_path: FileSystemPathVc, +) -> Result { + let reference_type = Value::new(ReferenceType::Entry(EntryReferenceSubType::Page)); + + let pathname = pathname_for_path(next_router_root, next_router_path, PathType::Page); + + Ok(PageChunk { + pathname, + node_chunk: node_build_context.node_chunk(page_asset, reference_type.clone()), + client_chunks: client_build_context.client_chunk(page_asset, pathname, reference_type), + } + .cell()) +} + +#[turbo_tasks::function] +async fn pathname_from_path(next_router_path: FileSystemPathVc) -> Result { + let pathname = next_router_path.await?; + Ok(StringVc::cell(pathname.path.clone())) +} + +#[turbo_tasks::function] +fn get_node_runtime_entries( + project_root: FileSystemPathVc, + env: ProcessEnvVc, + next_config: NextConfigVc, +) -> RuntimeEntriesVc { + let node_runtime_entries = vec![RuntimeEntry::Source( + ProcessEnvAssetVc::new(project_root, env_for_js(env, false, next_config)).into(), + ) + .cell()]; + + RuntimeEntriesVc::cell(node_runtime_entries) +} diff --git a/packages/next-swc/crates/next-core/src/lib.rs b/packages/next-swc/crates/next-core/src/lib.rs index 41cdb7e6460c8..03f2a530af470 100644 --- a/packages/next-swc/crates/next-core/src/lib.rs +++ b/packages/next-swc/crates/next-core/src/lib.rs @@ -15,7 +15,7 @@ pub mod manifest; pub mod mode; mod next_build; pub mod next_client; -mod next_client_chunks; +pub mod next_client_chunks; mod next_client_component; pub mod next_config; mod next_edge; @@ -38,8 +38,10 @@ mod util; mod web_entry_source; pub use app_source::create_app_source; +pub use page_loader::create_page_loader_entry_asset; pub use page_source::create_page_source; pub use turbopack_binding::{turbopack::node::source_map, *}; +pub use util::{pathname_for_path, PathType}; pub use web_entry_source::create_web_entry_source; pub fn register() { diff --git a/packages/next-swc/crates/next-core/src/next_client/context.rs b/packages/next-swc/crates/next-core/src/next_client/context.rs index a4e42e1a0415d..8d845969f3661 100644 --- a/packages/next-swc/crates/next-core/src/next_client/context.rs +++ b/packages/next-swc/crates/next-core/src/next_client/context.rs @@ -337,14 +337,14 @@ pub fn get_client_chunking_context( #[turbo_tasks::function] pub fn get_client_assets_path( - server_root: FileSystemPathVc, + client_root: FileSystemPathVc, ty: Value, ) -> FileSystemPathVc { match ty.into_value() { ClientContextType::Pages { .. } | ClientContextType::App { .. } - | ClientContextType::Fallback => server_root.join("/_next/static/media"), - ClientContextType::Other => server_root.join("/_assets"), + | ClientContextType::Fallback => client_root.join("/_next/static/media"), + ClientContextType::Other => client_root.join("/_assets"), } } diff --git a/packages/next-swc/crates/next-core/src/next_client/mod.rs b/packages/next-swc/crates/next-core/src/next_client/mod.rs index f0e90e14f68aa..48e4a969c2293 100644 --- a/packages/next-swc/crates/next-core/src/next_client/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_client/mod.rs @@ -2,3 +2,10 @@ pub(crate) mod context; pub(crate) mod runtime_entry; pub(crate) mod transforms; pub(crate) mod transition; + +pub use context::{ + get_client_compile_time_info, get_client_module_options_context, + get_client_resolve_options_context, get_client_runtime_entries, ClientContextType, +}; +pub use runtime_entry::{RuntimeEntries, RuntimeEntriesVc, RuntimeEntry, RuntimeEntryVc}; +pub use transition::NextClientTransition; diff --git a/packages/next-swc/crates/next-core/src/next_client_chunks/mod.rs b/packages/next-swc/crates/next-core/src/next_client_chunks/mod.rs index 88fc1481ccfaa..84cd4ff822021 100644 --- a/packages/next-swc/crates/next-core/src/next_client_chunks/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_client_chunks/mod.rs @@ -1,3 +1,5 @@ pub(crate) mod client_chunks_transition; pub(crate) mod in_chunking_context_asset; pub(crate) mod with_chunks; + +pub use client_chunks_transition::NextClientChunksTransitionVc; diff --git a/packages/next-swc/crates/next-core/src/next_config.rs b/packages/next-swc/crates/next-core/src/next_config.rs index 57eebad31f4f3..828b4c4b571b9 100644 --- a/packages/next-swc/crates/next-core/src/next_config.rs +++ b/packages/next-swc/crates/next-core/src/next_config.rs @@ -678,7 +678,6 @@ pub async fn load_next_config_internal( next_asset("entry/config/next.js"), Value::new(ReferenceType::Entry(EntryReferenceSubType::Undefined)), ); - let config_value = evaluate( load_next_config_asset, project_path, diff --git a/packages/next-swc/crates/next-core/src/next_server/mod.rs b/packages/next-swc/crates/next-core/src/next_server/mod.rs index 1afe8b82c4fc2..14de79be40e9c 100644 --- a/packages/next-swc/crates/next-core/src/next_server/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_server/mod.rs @@ -1,3 +1,8 @@ pub(crate) mod context; pub(crate) mod resolve; pub(crate) mod transforms; + +pub use context::{ + get_server_compile_time_info, get_server_module_options_context, + get_server_resolve_options_context, ServerContextType, +}; diff --git a/packages/next-swc/crates/next-core/src/page_loader.rs b/packages/next-swc/crates/next-core/src/page_loader.rs index 585d5093f5989..fe2438542d2b4 100644 --- a/packages/next-swc/crates/next-core/src/page_loader.rs +++ b/packages/next-swc/crates/next-core/src/page_loader.rs @@ -54,48 +54,51 @@ pub struct PageLoaderAsset { pub pathname: StringVc, } -#[turbo_tasks::value_impl] -impl PageLoaderAssetVc { - #[turbo_tasks::function] - async fn get_loader_entry_asset(self) -> Result { - let this = &*self.await?; +#[turbo_tasks::function] +pub async fn create_page_loader_entry_asset( + client_context: AssetContextVc, + entry_asset: AssetVc, + pathname: StringVc, +) -> Result { + let mut result = RopeBuilder::default(); + writeln!( + result, + "const PAGE_PATH = {};\n", + StringifyJs(&*pathname.await?) + )?; + + let page_loader_path = next_js_file_path("entry/page-loader.ts"); + let base_code = page_loader_path.read(); + if let FileContent::Content(base_file) = &*base_code.await? { + result += base_file.content() + } else { + bail!("required file `entry/page-loader.ts` not found"); + } - let mut result = RopeBuilder::default(); - writeln!( - result, - "const PAGE_PATH = {};\n", - StringifyJs(&*this.pathname.await?) - )?; - - let page_loader_path = next_js_file_path("entry/page-loader.ts"); - let base_code = page_loader_path.read(); - if let FileContent::Content(base_file) = &*base_code.await? { - result += base_file.content() - } else { - bail!("required file `entry/page-loader.ts` not found"); - } + let file = File::from(result.build()); - let file = File::from(result.build()); + let virtual_asset = VirtualAssetVc::new(page_loader_path, file.into()).into(); - Ok(VirtualAssetVc::new(page_loader_path, file.into()).into()) - } + Ok(client_context.process( + virtual_asset, + Value::new(ReferenceType::Internal( + InnerAssetsVc::cell(indexmap! { + "PAGE".to_string() => client_context.process(entry_asset, Value::new(ReferenceType::Entry(EntryReferenceSubType::Page))) + }) + ))) + ) +} +#[turbo_tasks::value_impl] +impl PageLoaderAssetVc { #[turbo_tasks::function] async fn get_page_chunks(self) -> Result { let this = &*self.await?; - let loader_entry_asset = self.get_loader_entry_asset(); - - let module = this.client_context.process( - loader_entry_asset, - Value::new(ReferenceType::Internal( - InnerAssetsVc::cell(indexmap! { - "PAGE".to_string() => this.client_context.process(this.entry_asset, Value::new(ReferenceType::Entry(EntryReferenceSubType::Page))) - }) - )), - ); + let page_loader_entry_asset = + create_page_loader_entry_asset(this.client_context, this.entry_asset, this.pathname); - let Some(module) = EvaluatableAssetVc::resolve_from(module).await? else { + let Some(module) = EvaluatableAssetVc::resolve_from(page_loader_entry_asset).await? else { bail!("internal module must be evaluatable"); }; diff --git a/packages/next-swc/crates/next-core/src/pages_structure.rs b/packages/next-swc/crates/next-core/src/pages_structure.rs index 03ab4e133aaa5..7d1bcb5c1b4aa 100644 --- a/packages/next-swc/crates/next-core/src/pages_structure.rs +++ b/packages/next-swc/crates/next-core/src/pages_structure.rs @@ -107,6 +107,12 @@ pub struct PagesDirectoryStructure { #[turbo_tasks::value_impl] impl PagesDirectoryStructureVc { + /// Returns the router path of this directory. + #[turbo_tasks::function] + pub async fn next_router_path(self) -> Result { + Ok(self.await?.next_router_path) + } + /// Returns the path to the directory of this structure in the project file /// system. #[turbo_tasks::function] diff --git a/packages/next-swc/crates/next-core/src/web_entry_source.rs b/packages/next-swc/crates/next-core/src/web_entry_source.rs index 4c83e27837ee3..16307596c2a82 100644 --- a/packages/next-swc/crates/next-core/src/web_entry_source.rs +++ b/packages/next-swc/crates/next-core/src/web_entry_source.rs @@ -38,7 +38,7 @@ use crate::{ get_client_asset_context, get_client_chunking_context, get_client_resolve_options_context, ClientContextType, }, - runtime_entry::{RuntimeEntriesVc, RuntimeEntry}, + RuntimeEntriesVc, RuntimeEntry, }, next_config::NextConfigVc, }; diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index ef17452fbaff8..b4d88dfabd876 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 39c06bb1b9638..c7cffb06be912 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -83,7 +83,7 @@ ] }, "dependencies": { - "@next/env": "13.4.6-canary.8", + "@next/env": "13.4.7-canary.1", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -141,11 +141,11 @@ "@jest/types": "29.5.0", "@napi-rs/cli": "2.14.7", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "13.4.6-canary.8", - "@next/polyfill-nomodule": "13.4.6-canary.8", - "@next/react-dev-overlay": "13.4.6-canary.8", - "@next/react-refresh-utils": "13.4.6-canary.8", - "@next/swc": "13.4.6-canary.8", + "@next/polyfill-module": "13.4.7-canary.1", + "@next/polyfill-nomodule": "13.4.7-canary.1", + "@next/react-dev-overlay": "13.4.7-canary.1", + "@next/react-refresh-utils": "13.4.7-canary.1", + "@next/swc": "13.4.7-canary.1", "@opentelemetry/api": "1.4.1", "@segment/ajv-human-errors": "2.1.2", "@taskr/clear": "1.1.0", diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 6f4bf58ea5a1e..9319ef32b0e4e 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -210,6 +210,7 @@ export default async function build( noMangling = false, appDirOnly = false, turboNextBuild = false, + turboNextBuildRoot = null, buildMode: 'default' | 'experimental-compile' | 'experimental-generate' ): Promise { const isCompile = buildMode === 'experimental-compile' @@ -550,6 +551,23 @@ export default async function build( app: appPageKeys.length > 0 ? appPageKeys : undefined, } + if (turboNextBuild) { + // TODO(WEB-397) This is a temporary workaround to allow for filtering a + // subset of pages when building with --experimental-turbo, until we + // have complete support for all pages. + if (process.env.NEXT_TURBO_FILTER_PAGES) { + const filterPages = process.env.NEXT_TURBO_FILTER_PAGES.split(',') + pageKeys.pages = pageKeys.pages.filter((page) => { + return filterPages.some((filterPage) => { + return isMatch(page, filterPage) + }) + }) + } + + // TODO(alexkirsz) Filter out app pages entirely as they are not supported yet. + pageKeys.app = undefined + } + const numConflictingAppPaths = conflictingAppPagePaths.length if (mappedAppPages && numConflictingAppPaths > 0) { Log.error( @@ -932,7 +950,23 @@ export default async function build( async function turbopackBuild() { const turboNextBuildStart = process.hrtime() - await binding.turbo.nextBuild(NextBuildContext) + + const turboJson = findUp.sync('turbo.json', { cwd: dir }) + // eslint-disable-next-line no-shadow + const packagePath = findUp.sync('package.json', { cwd: dir }) + + let root = + turboNextBuildRoot ?? + (turboJson + ? path.dirname(turboJson) + : packagePath + ? path.dirname(packagePath) + : undefined) + await binding.turbo.nextBuild({ + ...NextBuildContext, + root, + }) + const [duration] = process.hrtime(turboNextBuildStart) return { duration, turbotraceContext: null } } diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 8cf1e2ac3cce5..20dd6b7ce131d 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1385,7 +1385,7 @@ export default async function getBaseWebpackConfig( // so that the DefinePlugin can inject process.env values. // Treat next internals as non-external for server layer - if (layer === WEBPACK_LAYERS.server) { + if (layer === WEBPACK_LAYERS.server || layer === WEBPACK_LAYERS.action) { return } @@ -1520,7 +1520,7 @@ export default async function getBaseWebpackConfig( (isEsm && isAppLayer) if (/node_modules[/\\].*\.[mc]?js$/.test(res)) { - if (layer === WEBPACK_LAYERS.server) { + if (layer === WEBPACK_LAYERS.server || layer === WEBPACK_LAYERS.action) { // All packages should be bundled for the server layer if they're not opted out. // This option takes priority over the transpilePackages option. diff --git a/packages/next/src/build/webpack/loaders/next-app-loader.ts b/packages/next/src/build/webpack/loaders/next-app-loader.ts index 5a6fee4e8547d..b34d51668c903 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader.ts @@ -304,19 +304,26 @@ async function createTreeCodeFromPath( continue } + const subSegmentPath = [...segments] + if (parallelKey !== 'children') { + subSegmentPath.push(parallelKey) + } + + const normalizedParallelSegments = Array.isArray(parallelSegment) + ? parallelSegment.slice(0, 1) + : [parallelSegment] + + subSegmentPath.push( + ...normalizedParallelSegments.filter( + (segment) => segment !== PAGE_SEGMENT + ) + ) + const { treeCode: subtreeCode } = await createSubtreePropsFromSegmentPath( - [ - ...segments, - ...(parallelKey === 'children' ? [] : [parallelKey]), - Array.isArray(parallelSegment) ? parallelSegment[0] : parallelSegment, - ] + subSegmentPath ) - const parallelSegmentPath = - segmentPath + - '/' + - (parallelKey === 'children' ? '' : `${parallelKey}/`) + - (Array.isArray(parallelSegment) ? parallelSegment[0] : parallelSegment) + const parallelSegmentPath = subSegmentPath.join('/') // `page` is not included here as it's added above. const filePaths = await Promise.all( @@ -394,7 +401,6 @@ async function createTreeCodeFromPath( ]` } } - return { treeCode: `{ ${Object.entries(props) @@ -473,10 +479,9 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { const isParallelRoute = rest[0].startsWith('@') if (isParallelRoute && rest.length === 2 && rest[1] === 'page') { - matched[rest[0]] = PAGE_SEGMENT + matched[rest[0]] = [PAGE_SEGMENT] continue } - if (isParallelRoute) { matched[rest[0]] = rest.slice(1) continue @@ -485,7 +490,6 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { matched.children = rest[0] } } - return Object.entries(matched) } diff --git a/packages/next/src/cli/next-build.ts b/packages/next/src/cli/next-build.ts index 043c49f445e4a..3956aa988b674 100755 --- a/packages/next/src/cli/next-build.ts +++ b/packages/next/src/cli/next-build.ts @@ -18,6 +18,7 @@ const nextBuild: CliCommand = (argv) => { '--no-mangling': Boolean, '--experimental-app-only': Boolean, '--experimental-turbo': Boolean, + '--experimental-turbo-root': String, '--build-mode': String, // Aliases '-h': '--help', @@ -82,6 +83,7 @@ const nextBuild: CliCommand = (argv) => { args['--no-mangling'], args['--experimental-app-only'], args['--experimental-turbo'], + args['--experimental-turbo-root'], args['--build-mode'] || 'default' ).catch((err) => { console.error('') diff --git a/packages/next/src/server/lib/render-server.ts b/packages/next/src/server/lib/render-server.ts index 0fe872014c76a..5f4c72feeeddd 100644 --- a/packages/next/src/server/lib/render-server.ts +++ b/packages/next/src/server/lib/render-server.ts @@ -137,6 +137,6 @@ export async function initialize(opts: { return reject(err) } }) - server.listen(await getFreePort(), opts.hostname) + server.listen(await getFreePort(), '0.0.0.0') }) } diff --git a/packages/next/src/server/lib/server-ipc/index.ts b/packages/next/src/server/lib/server-ipc/index.ts index 04f030b46dc8f..4c3cbf9200bf6 100644 --- a/packages/next/src/server/lib/server-ipc/index.ts +++ b/packages/next/src/server/lib/server-ipc/index.ts @@ -63,7 +63,7 @@ export async function createIpcServer( ) const ipcPort = await new Promise((resolveIpc) => { - ipcServer.listen(0, server.hostname, () => { + ipcServer.listen(0, '0.0.0.0', () => { const addr = ipcServer.address() if (addr && typeof addr === 'object') { diff --git a/packages/next/src/server/lib/server-ipc/invoke-request.ts b/packages/next/src/server/lib/server-ipc/invoke-request.ts index b2f4694e70cf0..105cb139a9ec9 100644 --- a/packages/next/src/server/lib/server-ipc/invoke-request.ts +++ b/packages/next/src/server/lib/server-ipc/invoke-request.ts @@ -18,8 +18,13 @@ export const invokeRequest = async ( const http = require('http') as typeof import('http') try { + // force to 127.0.0.1 as IPC always runs on this hostname + // to avoid localhost issues + const parsedTargetUrl = new URL(targetUrl) + parsedTargetUrl.hostname = '127.0.0.1' + const invokeReq = http.request( - targetUrl, + parsedTargetUrl.toString(), { headers: invokeHeaders, method: requestInit.method, diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index bd5cde5400090..10ea8b13643c7 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index d7255a1b18a2c..1e9ce2e624d04 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "13.4.6-canary.8", + "version": "13.4.7-canary.1", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 811091ddd0c2c..a67a53315258b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -426,7 +426,7 @@ importers: packages/eslint-config-next: specifiers: - '@next/eslint-plugin-next': 13.4.6-canary.8 + '@next/eslint-plugin-next': 13.4.7-canary.1 '@rushstack/eslint-patch': ^1.1.3 '@typescript-eslint/parser': ^5.42.0 eslint: ^7.23.0 || ^8.0.0 @@ -503,12 +503,12 @@ importers: '@jest/types': 29.5.0 '@napi-rs/cli': 2.14.7 '@napi-rs/triples': 1.1.0 - '@next/env': 13.4.6-canary.8 - '@next/polyfill-module': 13.4.6-canary.8 - '@next/polyfill-nomodule': 13.4.6-canary.8 - '@next/react-dev-overlay': 13.4.6-canary.8 - '@next/react-refresh-utils': 13.4.6-canary.8 - '@next/swc': 13.4.6-canary.8 + '@next/env': 13.4.7-canary.1 + '@next/polyfill-module': 13.4.7-canary.1 + '@next/polyfill-nomodule': 13.4.7-canary.1 + '@next/react-dev-overlay': 13.4.7-canary.1 + '@next/react-refresh-utils': 13.4.7-canary.1 + '@next/swc': 13.4.7-canary.1 '@opentelemetry/api': 1.4.1 '@segment/ajv-human-errors': 2.1.2 '@swc/helpers': 0.5.1 diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 2fcd5459bcebe..62b857c5a38ea 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -14,6 +14,7 @@ createNextDescribe( files: __dirname, dependencies: { react: 'latest', + nanoid: 'latest', 'react-dom': 'latest', 'server-only': 'latest', }, @@ -281,6 +282,14 @@ createNextDescribe( } }) }) + + it('should bundle external libraries if they are on the action layer', async () => { + await next.fetch('/client') + const pageBundle = await fs.readFile( + join(next.testDir, '.next', 'server', 'app', 'client', 'page.js') + ) + expect(pageBundle.toString()).toContain('node_modules/nanoid/index.js') + }) } describe('Edge SSR', () => { diff --git a/test/e2e/app-dir/actions/app/client/actions-lib.js b/test/e2e/app-dir/actions/app/client/actions-lib.js new file mode 100644 index 0000000000000..bb51170953b2a --- /dev/null +++ b/test/e2e/app-dir/actions/app/client/actions-lib.js @@ -0,0 +1,9 @@ +'use server' + +// Any arbitrary library just to ensure it's bundled. +// https://github.com/vercel/next.js/pull/51367 +import nanoid from 'nanoid' + +export async function test() { + console.log(nanoid) +} diff --git a/test/e2e/app-dir/actions/app/client/page.js b/test/e2e/app-dir/actions/app/client/page.js index 9b768ba0dcc49..87a812e5d1a05 100644 --- a/test/e2e/app-dir/actions/app/client/page.js +++ b/test/e2e/app-dir/actions/app/client/page.js @@ -3,6 +3,7 @@ import { useState } from 'react' import double, { inc, dec, redirectAction, getHeaders } from './actions' +import { test } from './actions-lib' export default function Counter() { const [count, setCount] = useState(0) @@ -58,6 +59,9 @@ export default function Counter() { submit +
+ +
) } diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/(.)sub/[slug]/page.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/(.)sub/[slug]/page.tsx new file mode 100644 index 0000000000000..6c1bd4cf8beee --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/(.)sub/[slug]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'intercepted parallel layout slug' +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/(.)sub/layout.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/(.)sub/layout.tsx new file mode 100644 index 0000000000000..643dc39ea5cc8 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/(.)sub/layout.tsx @@ -0,0 +1,8 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> +

intercepted layout

+ {children} + + ) +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@groupslot/(slot)/layout.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@groupslot/(slot)/layout.tsx new file mode 100644 index 0000000000000..e08a02aabdc16 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@groupslot/(slot)/layout.tsx @@ -0,0 +1,8 @@ +export default function Layout({ children }) { + return ( +
+

Parallel group slot Layout

+ {children} +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@groupslot/(slot)/page.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@groupslot/(slot)/page.tsx new file mode 100644 index 0000000000000..8785e2eec5937 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@groupslot/(slot)/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'group slot children' +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@slot/layout.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@slot/layout.tsx new file mode 100644 index 0000000000000..8edbe6cb7661d --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@slot/layout.tsx @@ -0,0 +1,8 @@ +export default function Layout({ children }) { + return ( +
+

parallel layout

+ {children} +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@slot/page.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@slot/page.tsx new file mode 100644 index 0000000000000..5695dda668610 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/@slot/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'slot children' +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/layout.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/layout.tsx new file mode 100644 index 0000000000000..505ed95e27b56 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/layout.tsx @@ -0,0 +1,13 @@ +import Link from 'next/link' + +export default function Layout({ children, slot, groupslot }) { + return ( +
+

Main Layout

+ {children} +
{slot}
+
{groupslot}
+ /sub/route +
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/page.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/page.tsx new file mode 100644 index 0000000000000..3e874159d2fd5 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'children' +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/sub/[slug]/page.tsx b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/sub/[slug]/page.tsx new file mode 100644 index 0000000000000..1b6c8caafedd2 --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-and-interception/app/parallel-layout/sub/[slug]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return 'parallel layout slug' +} diff --git a/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts b/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts index 659ed62762570..f85e2f2448722 100644 --- a/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts +++ b/test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts @@ -242,6 +242,14 @@ createNextDescribe( ) }) + it('should support layout files in parallel routes', async () => { + const browser = await next.browser('/parallel-layout') + await check( + () => browser.waitForElementByCss('#parallel-layout').text(), + 'parallel layout' + ) + }) + it('should only scroll to the parallel route that was navigated to', async () => { const browser = await next.browser('/parallel-scroll') diff --git a/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index a5c3016b1e1c3..3724ddb725d11 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -171,11 +171,6 @@ describe('Build Output', () => { expect.stringMatching(/\/2000\/10 \(\d+ ms\)$/), expect.stringMatching(/\/10\/1000 \(\d+ ms\)$/), expect.stringMatching(/\/300\/10 \(\d+ ms\)$/), - // kept in original order - expect.stringMatching(/\/5\/5/), - expect.stringMatching(/\/25\/25/), - expect.stringMatching(/\/20\/20/), - expect.stringMatching(/\/10\/10/), // max of 7 preview paths ' [+2 more paths]', ]) { diff --git a/turbo.json b/turbo.json index e0c10b9a9a1aa..e669205ba76c7 100644 --- a/turbo.json +++ b/turbo.json @@ -38,10 +38,18 @@ "outputs": ["dist/**"] }, "typescript": {}, - "rust-check": {}, - "test-cargo-unit": {}, - "test-cargo-integration": {}, - "test-cargo-bench": {}, + "rust-check": { + "inputs": [".cargo/**", "**/*.rs", "**/Cargo.toml"] + }, + "test-cargo-unit": { + "inputs": [".cargo/**", "**/*.rs", "**/Cargo.toml"] + }, + "test-cargo-integration": { + "inputs": [".cargo/**", "**/*.rs", "**/Cargo.toml"] + }, + "test-cargo-bench": { + "inputs": [".cargo/**", "**/*.rs", "**/Cargo.toml"] + }, "//#get-test-timings": { "inputs": ["run-tests.js"], "outputs": ["test-timings.json"]