diff --git a/.cargo/config.toml b/.cargo/config.toml index a38223d24f3802..18dc923049119f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,15 +2,25 @@ CARGO_WORKSPACE_DIR = { value = "", relative = true } [build] - +rustflags = [ + "--cfg", + "tokio_unstable", + "-Zshare-generics=y", # make the current crate share its generic instantiations + "-Zthreads=8", # parallel frontend https://blog.rust-lang.org/2023/11/09/parallel-rustc.html + "-Csymbol-mangling-version=v0", +] rustdocflags = [] [target.x86_64-pc-windows-msvc] linker = "rust-lld" rustflags = ["-C", "target-feature=+crt-static"] + [target.i686-pc-windows-msvc] rustflags = ["-C", "target-feature=+crt-static"] +[target.aarch64-apple-darwin] +linker = "rust-lld" + [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" @@ -27,11 +37,3 @@ rustflags = [ [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" - -[target.'cfg(all())'] -rustflags = [ - "--cfg", - "tokio_unstable", - "-Zshare-generics=y", - "-Csymbol-mangling-version=v0", -] diff --git a/.eslintignore b/.eslintignore index 7e2800e2acb984..d9ff00758757a0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -42,3 +42,4 @@ test/development/basic/hmr/components/parse-error.js packages/next-swc/docs/assets/**/* test/lib/amp-validator-wasm.js test/production/pages-dir/production/fixture/amp-validator-wasm.js +test/e2e/async-modules/amp-validator-wasm.js diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml index 6ac3a51b696183..64f284b1b54412 100644 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: Create a bug report for the Next.js core -labels: ['template: bug'] +labels: ['bug'] body: - type: markdown attributes: @@ -71,29 +71,30 @@ body: multiple: true options: - 'Not sure' - - 'App Router' - - 'CLI (create-next-app)' - - 'Data fetching (gS(S)P, getInitialProps)' - - 'Dynamic imports (next/dynamic)' - - 'ESLint (eslint-config-next)' - - 'Font optimization (next/font)' - - 'Image optimization (next/image, next/legacy/image)' + - 'create-next-app' + - 'Documentation' + - 'Lazy Loading' + - 'Font (next/font)' + - 'Image (next/image)' + - 'Instrumentation' - 'Internationalization (i18n)' - - 'Jest (next/jest)' - - 'MDX (@next/mdx)' - - 'Metadata (metadata, generateMetadata, next/head)' - - 'Middleware / Edge (API routes, runtime)' - - 'Module resolution (CJS / ESM, module resolving)' - - 'Operating System (Windows, MacOS, Linux)' - - 'Package manager (npm, pnpm, Yarn)' - - 'Routing (next/router, next/navigation, next/link)' - - 'Script optimization (next/script)' - - 'Standalone mode (output: "standalone")' - - 'Static HTML Export (output: "export")' - - 'SWC minifier (swcMinify: true)' - - 'SWC transpilation' - - 'Turbopack (--turbo)' - - 'TypeScript (plugin, built-in types)' + - 'Linting' + - 'Markdown (MDX)' + - 'Metadata' + - 'Middleware' + - 'Output (export/standalone)' + - 'Pages Router' + - 'Parallel & Intercepting Routes' + - 'Partial Prerendering (PPR)' + - 'Performance' + - 'Runtime' + - 'Script (next/script)' + - 'Testing' + - 'Turbopack' + - 'TypeScript' + - 'SWC' + - 'Upstream' + validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/2.example_bug_report.yml b/.github/ISSUE_TEMPLATE/2.example_bug_report.yml index 04c4d53cbb9bba..5adcdb0d46b0e3 100644 --- a/.github/ISSUE_TEMPLATE/2.example_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/2.example_bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report for Examples description: Create a bug report for one of the Next.js examples -labels: ['area: examples'] +labels: ['example bug'] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/4.docs_request.yml b/.github/ISSUE_TEMPLATE/4.docs_request.yml index 67111ee7e776da..684bd5c879a1f9 100644 --- a/.github/ISSUE_TEMPLATE/4.docs_request.yml +++ b/.github/ISSUE_TEMPLATE/4.docs_request.yml @@ -2,7 +2,7 @@ name: 'Docs Request for an Update or Improvement' description: A request to update or improve Next.js documentation title: 'Docs: ' labels: - - 'template: documentation' + - 'Documentation' body: - type: markdown attributes: diff --git a/.github/actions/next-integration-stat/package.json b/.github/actions/next-integration-stat/package.json index f7139529c5c8ac..efe20f8ad066b2 100644 --- a/.github/actions/next-integration-stat/package.json +++ b/.github/actions/next-integration-stat/package.json @@ -22,7 +22,7 @@ }, "engines": { "node": ">=18.17.0", - "pnpm": "8.15.4" + "pnpm": "8.15.7" }, - "packageManager": "pnpm@8.15.4" + "packageManager": "pnpm@8.15.7" } diff --git a/.github/actions/next-stats-action/package.json b/.github/actions/next-stats-action/package.json index 1be4a941450e59..4269e374084f88 100644 --- a/.github/actions/next-stats-action/package.json +++ b/.github/actions/next-stats-action/package.json @@ -19,7 +19,7 @@ }, "engines": { "node": ">=18.17.0", - "pnpm": "8.15.4" + "pnpm": "8.15.7" }, - "packageManager": "pnpm@8.15.4" + "packageManager": "pnpm@8.15.7" } diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index bb6044cbf08f16..b64b23b584cedb 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -4,61 +4,17 @@ inputs: targets: description: 'Comma-separated list of target triples to install for this toolchain' required: false - components: - description: 'Comma-separated list of components to be additionally installed' - required: false - skip-install: - description: 'Sets environment variables without installing the rust toolchain' - required: false runs: using: 'composite' steps: - - name: 'Get toolchain version from file' - id: file - shell: bash - run: echo "toolchain=$(cat ./rust-toolchain)" >> $GITHUB_OUTPUT - - - shell: bash - run: | - : force toolchain version - echo "RUST_TOOLCHAIN=${{ steps.file.outputs.toolchain }}" >> $GITHUB_ENV - - - shell: bash - run: | - : disable incremental compilation - if [ -z "${CARGO_INCREMENTAL+set}" ]; then - echo CARGO_INCREMENTAL=0 >> $GITHUB_ENV - fi - - - shell: bash - run: | - : enable colors in Cargo output - if [ -z "${CARGO_TERM_COLOR+set}" ]; then - echo CARGO_TERM_COLOR=always >> $GITHUB_ENV - fi - - - shell: bash - run: | - : enable rust backtrace - if [ -z "${RUST_BACKTRACE+set}" ]; then - echo RUST_BACKTRACE=short >> $GITHUB_ENV - fi - - - shell: bash - run: | - : enable faster cargo sparse registry - if [ -z "${CARGO_REGISTRIES_CRATES_IO_PROTOCOL+set}" ]; then - echo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse >> $GITHUB_ENV - fi - - name: 'Setup Rust toolchain' - uses: dtolnay/rust-toolchain@master - if: ${{ !inputs.skip-install }} + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: ${{ steps.file.outputs.toolchain }} - targets: ${{ inputs.targets }} - components: ${{ inputs.components }} + target: ${{ inputs.targets }} + # needed to not make it override the defaults + rustflags: '' + cache: false - name: 'Add cargo problem matchers' shell: bash diff --git a/.github/actions/upload-turboyet-data/package.json b/.github/actions/upload-turboyet-data/package.json index d99a2194646522..ef65a1117b81a9 100644 --- a/.github/actions/upload-turboyet-data/package.json +++ b/.github/actions/upload-turboyet-data/package.json @@ -13,7 +13,7 @@ }, "engines": { "node": ">=18.17.0", - "pnpm": "8.15.4" + "pnpm": "8.15.7" }, - "packageManager": "pnpm@8.15.4" + "packageManager": "pnpm@8.15.7" } diff --git a/.github/labeler.json b/.github/labeler.json index 5a1e81f5f9418d..e15a84c26093d5 100644 --- a/.github/labeler.json +++ b/.github/labeler.json @@ -1,11 +1,11 @@ { "labels": { - "area: create-next-app": ["packages/create-next-app/**"], - "area: documentation": ["docs/**", "errors/**"], - "area: examples": ["examples/**"], - "area: Font Optimization": ["**/*font*"], - "area: tests": ["test/**", "bench/**"], - "area: Turbopack": ["packages/next-swc/crates/next-*/**"], + "create-next-app": ["packages/create-next-app/**"], + "documentation": ["docs/**", "errors/**"], + "examples": ["examples/**"], + "Font (next/font)": ["**/*font*"], + "tests": ["test/**", "bench/**"], + "Turbopack": ["packages/next-swc/crates/next-*/**"], "created-by: Chrome Aurora": [ { "type": "user", "pattern": "atcastle" }, { "type": "user", "pattern": "devknoll" }, @@ -46,7 +46,7 @@ { "type": "user", "pattern": "timeyoutakeit" }, { "type": "user", "pattern": "manovotny" } ], - "created-by: turbopack team": [ + "created-by: Turbopack team": [ { "type": "user", "pattern": "bgw" }, { "type": "user", "pattern": "ForsakenHarmony" }, { "type": "user", "pattern": "kdy1" }, @@ -65,7 +65,6 @@ "packages/next-mdx/**", "packages/next-swc/**", "packages/next/**", - "packages/react-dev-overlay/**", "packages/react-refresh-utils/**" ] } diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index 82ac9931614af7..85399c929ffef2 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -156,8 +156,7 @@ jobs: set -e && apt update && apt install -y pkg-config xz-utils dav1d libdav1d-dev && - rustup toolchain install "${RUST_TOOLCHAIN}" && - rustup default "${RUST_TOOLCHAIN}" && + rustup show && rustup target add x86_64-unknown-linux-gnu && npm i -g "@napi-rs/cli@${NAPI_CLI_VERSION}" && unset CC_x86_64_unknown_linux_gnu && unset CC && @@ -177,8 +176,7 @@ jobs: set -e && apk update && apk add --no-cache libc6-compat pkgconfig dav1d libdav1d dav1d-dev && - rustup toolchain install "${RUST_TOOLCHAIN}" && - rustup default "${RUST_TOOLCHAIN}" && + rustup show && rustup target add x86_64-unknown-linux-musl && npm i -g "@napi-rs/cli@${NAPI_CLI_VERSION}" && cd packages/next-swc && npm run build-native-release -- --target x86_64-unknown-linux-musl && @@ -197,8 +195,7 @@ jobs: apt update && apt install -y pkg-config xz-utils dav1d libdav1d-dev && export JEMALLOC_SYS_WITH_LG_PAGE=16 && - rustup toolchain install "${RUST_TOOLCHAIN}" && - rustup default "${RUST_TOOLCHAIN}" && + rustup show && rustup target add aarch64-unknown-linux-gnu && npm i -g "@napi-rs/cli@${NAPI_CLI_VERSION}" && export CC_aarch64_unknown_linux_gnu=/usr/aarch64-unknown-linux-gnu/bin/aarch64-unknown-linux-gnu-gcc && @@ -220,8 +217,7 @@ jobs: apk add --no-cache libc6-compat pkgconfig dav1d libdav1d dav1d-dev && export JEMALLOC_SYS_WITH_LG_PAGE=16 && npm i -g "@napi-rs/cli@${NAPI_CLI_VERSION}" && - rustup toolchain install "${RUST_TOOLCHAIN}" && - rustup default "${RUST_TOOLCHAIN}" && + rustup show && rustup target add aarch64-unknown-linux-musl && cd packages/next-swc && npm run build-native-release -- --target aarch64-unknown-linux-musl && llvm-strip -x native/next-swc.*.node @@ -255,11 +251,11 @@ jobs: check-latest: true - run: corepack enable + # we always want to run this to set environment variables - name: Install Rust uses: ./.github/actions/setup-rust with: targets: ${{ matrix.settings.target }} - skip-install: ${{ matrix.settings.docker }} - name: normalize versions run: node scripts/normalize-version-bump.js @@ -269,12 +265,15 @@ jobs: if: ${{ matrix.settings.setup }} - name: Cache on ${{ github.ref_name }} - uses: ijjk/rust-cache@turbo-cache-v1.0.7 + uses: ijjk/rust-cache@turbo-cache-v1.0.8 with: save-if: 'true' cache-provider: 'turbo' shared-key: build-${{ matrix.settings.target }}-${{ hashFiles('.cargo/config.toml') }} + - name: Clear native build + run: rm -rf packages/next-swc/native + # we only need custom caching for docker builds # as they are on an older Node.js version and have # issues with turbo caching @@ -289,7 +288,7 @@ jobs: - name: Build in docker if: ${{ matrix.settings.docker && steps.build-exists.outputs.BUILD_EXISTS == 'no' }} - run: docker run -v "/var/run/docker.sock":"/var/run/docker.sock" -e RUST_TOOLCHAIN -e RUST_BACKTRACE -e NAPI_CLI_VERSION -e CARGO_TERM_COLOR -e CARGO_INCREMENTAL -e CARGO_PROFILE_RELEASE_LTO -e CARGO_REGISTRIES_CRATES_IO_PROTOCOL -e TURBO_API -e TURBO_TEAM -e TURBO_TOKEN -e TURBO_VERSION -e TURBO_REMOTE_ONLY -v ${{ env.HOME }}/.cargo/git:/root/.cargo/git -v ${{ env.HOME }}/.cargo/registry:/root/.cargo/registry -v ${{ github.workspace }}:/build -w /build --entrypoint=bash ${{ matrix.settings.docker }} -c "${{ matrix.settings.build }}" + run: docker run -v "/var/run/docker.sock":"/var/run/docker.sock" -e RUST_BACKTRACE -e NAPI_CLI_VERSION -e CARGO_TERM_COLOR -e CARGO_INCREMENTAL -e CARGO_PROFILE_RELEASE_LTO -e CARGO_REGISTRIES_CRATES_IO_PROTOCOL -e TURBO_API -e TURBO_TEAM -e TURBO_TOKEN -e TURBO_VERSION -e TURBO_REMOTE_ONLY -v ${{ env.HOME }}/.cargo/git:/root/.cargo/git -v ${{ env.HOME }}/.cargo/registry:/root/.cargo/registry -v ${{ github.workspace }}:/build -w /build --entrypoint=bash ${{ matrix.settings.docker }} -c "${{ matrix.settings.build }}" - name: cache build if: ${{ matrix.settings.docker && steps.build-exists.outputs.BUILD_EXISTS == 'no' }} diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 1606168096e70e..b7a44dd852a131 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -461,6 +461,8 @@ jobs: 'test-turbopack-integration', 'test-new-tests-dev', 'test-new-tests-start', + 'test-turbopack-production', + 'test-turbopack-production-integration', ] if: always() diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml index 0d48f5403af8ac..43f7c1c419190a 100644 --- a/.github/workflows/build_reusable.yml +++ b/.github/workflows/build_reusable.yml @@ -105,8 +105,6 @@ jobs: - name: Install Rust uses: ./.github/actions/setup-rust if: ${{ inputs.skipNativeBuild != 'yes' || inputs.needsNextest == 'yes' || inputs.needsRust == 'yes' }} - with: - components: rustfmt, clippy - name: 'Install mold linker' if: ${{ inputs.mold == 'yes' }} @@ -125,7 +123,7 @@ jobs: - run: corepack prepare --activate yarn@1.22.19 && npm i -g "turbo@${TURBO_VERSION}" "@napi-rs/cli@${NAPI_CLI_VERSION}" - name: Cache on ${{ github.ref_name }} - uses: ijjk/rust-cache@turbo-cache-v1.0.7 + uses: ijjk/rust-cache@turbo-cache-v1.0.8 if: ${{ inputs.rustCacheKey }} with: cache-provider: 'turbo' diff --git a/.github/workflows/test_e2e_deploy.yml b/.github/workflows/test_e2e_deploy.yml deleted file mode 100644 index feb37aec4c4b4a..00000000000000 --- a/.github/workflows/test_e2e_deploy.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: test-e2e-deploy - -on: - schedule: - # run every day at midnight - - cron: '0 0 * * *' - # allow triggering manually as well - workflow_dispatch: - -jobs: - build: - if: github.repository_owner == 'vercel' - runs-on: ubuntu-latest - - env: - VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }} - VERCEL_TEST_TEAM: vtest314-next-e2e-tests - DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }} - NAPI_CLI_VERSION: 2.16.2 - TURBO_VERSION: 1.12.5 - NODE_LTS_VERSION: 20 - CARGO_PROFILE_RELEASE_LTO: 'true' - TURBO_TEAM: 'vercel' - TURBO_REMOTE_ONLY: 'true' - TEST_TIMINGS_TOKEN: ${{ secrets.TEST_TIMINGS_TOKEN }} - NEXT_TELEMETRY_DISABLED: 1 - - strategy: - fail-fast: false - matrix: - group: [1, 2] - - steps: - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_LTS_VERSION }} - check-latest: true - - run: corepack enable - - - uses: actions/checkout@v4 - with: - fetch-depth: 25 - - - run: pnpm install - - - run: pnpm run build - - - run: npm i -g vercel@latest - - - run: node scripts/run-e2e-test-project-reset.mjs - name: Reset test project - - - run: docker run --rm -v $(pwd):/work mcr.microsoft.com/playwright:v1.35.1-jammy /bin/bash -c "cd /work && NODE_VERSION=${{ env.NODE_LTS_VERSION }} ./scripts/setup-node.sh && corepack enable > /dev/null && NEXT_JUNIT_TEST_REPORT=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 }} xvfb-run node run-tests.js --type e2e --timings -g ${{ matrix.group }}/2 -c 1 >> /proc/1/fd/1" - name: Run test/e2e (deploy) - - - name: Upload test report - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-reports - if-no-files-found: ignore - retention-days: 2 - path: | - test/test-junit-report - - - name: Upload test report to datadog - continue-on-error: true - run: | - ls -al ./test/*junit - - DD_ENV=ci npx @datadog/datadog-ci@2.23.1 junit upload --tags test.type:nextjs_deploy_e2e --service nextjs ./test/test-junit-report diff --git a/.github/workflows/test_e2e_deploy_related.yml b/.github/workflows/test_e2e_deploy_related.yml new file mode 100644 index 00000000000000..f0ad0c51e8abad --- /dev/null +++ b/.github/workflows/test_e2e_deploy_related.yml @@ -0,0 +1,81 @@ +name: Test E2E (Vercel Deploy), related + +on: + pull_request: + types: [opened, synchronize] + +jobs: + test: + if: github.repository_owner == 'vercel' + runs-on: ubuntu-latest + + env: + CARGO_PROFILE_RELEASE_LTO: 'true' + DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }} + DD_ENV: 'ci' + NAPI_CLI_VERSION: 2.16.2 + NEXT_JUNIT_TEST_REPORT: 'true' + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + NEXT_TEST_MODE: 'deploy' + NODE_LTS_VERSION: 20 + TEST_TIMINGS_TOKEN: ${{ secrets.TEST_TIMINGS_TOKEN }} + TURBO_REMOTE_ONLY: 'true' + TURBO_TEAM: 'vercel' + TURBO_VERSION: 1.12.5 + VERCEL_TEST_TEAM: vtest314-next-e2e-tests + VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }} + + strategy: + fail-fast: false + + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_LTS_VERSION }} + check-latest: true + + - name: Setup pnpm + run: corepack enable + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 25 + + - name: Setup tests + run: | + pnpm install + pnpm run build + npm i -g vercel@latest + node scripts/run-e2e-test-project-reset.mjs + + - name: Run tests + run: | + docker run --rm -v $(pwd):/work mcr.microsoft.com/playwright:v1.41.2-jammy /bin/bash -c "cd /work && \ + NODE_VERSION=${{ env.NODE_LTS_VERSION }} ./scripts/setup-node.sh && \ + corepack enable > /dev/null && \ + NEXT_JUNIT_TEST_REPORT=${{ env.NEXT_JUNIT_TEST_REPORT }} \ + DATADOG_API_KEY=${{ env.DATADOG_API_KEY }} \ + DD_ENV=${{ env.DD_ENV }} \ + VERCEL_TEST_TOKEN=${{ env.VERCEL_TEST_TOKEN }} \ + VERCEL_TEST_TEAM=${{ env.VERCEL_TEST_TEAM }} \ + NEXT_TEST_JOB=${{ env.NEXT_TEST_JOB }} \ + NEXT_TEST_MODE=${{ env.NEXT_TEST_MODE }} \ + TEST_TIMINGS_TOKEN=${{ env.TEST_TIMINGS_TOKEN }} \ + xvfb-run node run-tests.js --related --timings -c 1 >> /proc/1/fd/1" + + - name: Save test report as artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports + if-no-files-found: ignore + retention-days: 2 + path: test/test-junit-report + + - name: Upload test report to Datadog + continue-on-error: true + run: | + pnpx @datadog/datadog-ci@2.23.1 junit upload --tags test.type:nextjs_deploy_e2e --service nextjs ./test/test-junit-report diff --git a/.github/workflows/test_e2e_deploy_scheduled.yml b/.github/workflows/test_e2e_deploy_scheduled.yml new file mode 100644 index 00000000000000..c0690525f258de --- /dev/null +++ b/.github/workflows/test_e2e_deploy_scheduled.yml @@ -0,0 +1,87 @@ +name: Test E2E (Vercel Deploy), scheduled + +on: + schedule: + # run every day at midnight + - cron: '0 0 * * *' + # allow triggering manually as well + workflow_dispatch: + +jobs: + test: + if: github.repository_owner == 'vercel' + runs-on: ubuntu-latest + + env: + CARGO_PROFILE_RELEASE_LTO: 'true' + DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }} + DD_ENV: 'ci' + NAPI_CLI_VERSION: 2.16.2 + NEXT_JUNIT_TEST_REPORT: 'true' + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_CONTINUE_ON_ERROR: 1 + NEXT_TEST_JOB: 1 + NEXT_TEST_MODE: 'deploy' + NODE_LTS_VERSION: 20 + TEST_TIMINGS_TOKEN: ${{ secrets.TEST_TIMINGS_TOKEN }} + TURBO_REMOTE_ONLY: 'true' + TURBO_TEAM: 'vercel' + TURBO_VERSION: 1.12.5 + VERCEL_TEST_TEAM: vtest314-next-e2e-tests + VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }} + + strategy: + fail-fast: false + matrix: + group: [1, 2] + + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_LTS_VERSION }} + check-latest: true + + - name: Setup pnpm + run: corepack enable + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 25 + + - name: Setup tests + run: | + pnpm install + pnpm run build + npm i -g vercel@latest + node scripts/run-e2e-test-project-reset.mjs + + - name: Run tests + run: | + docker run --rm -v $(pwd):/work mcr.microsoft.com/playwright:v1.41.2-jammy /bin/bash -c "cd /work && \ + NODE_VERSION=${{ env.NODE_LTS_VERSION }} ./scripts/setup-node.sh && \ + corepack enable > /dev/null && \ + NEXT_JUNIT_TEST_REPORT=${{ env.NEXT_JUNIT_TEST_REPORT }} \ + DATADOG_API_KEY=${{ env.DATADOG_API_KEY }} \ + DD_ENV=${{ env.DD_ENV }} \ + VERCEL_TEST_TOKEN=${{ env.VERCEL_TEST_TOKEN }} \ + VERCEL_TEST_TEAM=${{ env.VERCEL_TEST_TEAM }} \ + NEXT_TEST_JOB=${{ env.NEXT_TEST_JOB }} \ + NEXT_TEST_MODE=${{ env.NEXT_TEST_MODE }} \ + TEST_TIMINGS_TOKEN=${{ env.TEST_TIMINGS_TOKEN }} \ + xvfb-run node run-tests.js --type e2e --timings -g ${{ matrix.group }}/2 -c 1 >> /proc/1/fd/1" + + - name: Save test report as artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports + if-no-files-found: ignore + retention-days: 2 + path: test/test-junit-report + + - name: Upload test report to Datadog + continue-on-error: true + run: | + pnpx @datadog/datadog-ci@2.23.1 junit upload --tags test.type:nextjs_deploy_e2e --service nextjs ./test/test-junit-report diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 0a958c47efa971..58e7f7c3c1c85d 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -17,9 +17,9 @@ jobs: name: Nissuer runs-on: ubuntu-latest steps: - - uses: balazsorban44/nissuer@1.9.2 + - uses: balazsorban44/nissuer@1.9.3 with: - label-area-prefix: 'area:' + label-area-prefix: '' label-area-section: 'Which area\(s\) are affected\? \(Select all that apply\)(.*)### Additional context' label-comments: | { @@ -34,4 +34,5 @@ jobs: reproduction-blocklist: 'github.com/vercel/next.js.*,github.com/\\w*/?$,github.com$' reproduction-link-section: '### Link to the code that reproduces this issue(.*)### To Reproduce' reproduction-invalid-label: 'invalid link' - reproduction-issue-labels: 'template: bug,' + reproduction-issue-labels: 'bug,' + comment-unhelpful-weight: 0.5 diff --git a/.prettierignore b/.prettierignore index d78b968e08f6fc..a297d08637c345 100644 --- a/.prettierignore +++ b/.prettierignore @@ -39,3 +39,4 @@ bench/nested-deps/components/**/* **/.tina/__generated__/** test/lib/amp-validator-wasm.js test/production/pages-dir/production/fixture/amp-validator-wasm.js +test/e2e/async-modules/amp-validator-wasm.js diff --git a/Cargo.lock b/Cargo.lock index c8d43c6eadea77..dacf72792a5967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,7 +321,7 @@ dependencies = [ [[package]] name = "auto-hash-map" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "serde", "smallvec", @@ -384,51 +384,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "axum" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f8ccfd9221ee7d1f3d4b33e1f8319b3a81ed8f61f2ea40b37b859794b4491" -dependencies = [ - "async-trait", - "axum-core", - "bitflags 1.3.2", - "bytes", - "futures-util", - "http", - "http-body", - "hyper", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f958c80c248b34b9a877a643811be8dbca03ca5ba827f2b63baf3a81e5fc4e" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - [[package]] name = "backtrace" version = "0.3.68" @@ -838,42 +793,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "console-api" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57ff02e8ad8e06ab9731d5dc72dc23bef9200778eae1a89d555d8c42e5d4a86" -dependencies = [ - "prost", - "prost-types", - "tonic", - "tracing-core", -] - -[[package]] -name = "console-subscriber" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a3a81dfaf6b66bce5d159eddae701e3a002f194d378cbf7be5f053c281d9be" -dependencies = [ - "console-api", - "crossbeam-channel", - "crossbeam-utils", - "futures", - "hdrhistogram", - "humantime", - "prost-types", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic", - "tracing", - "tracing-core", - "tracing-subscriber", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -2003,19 +1922,6 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -[[package]] -name = "hdrhistogram" -version = "7.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" -dependencies = [ - "base64 0.13.1", - "byteorder", - "flate2", - "nom", - "num-traits", -] - [[package]] name = "heapless" version = "0.7.16" @@ -2114,12 +2020,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.28" @@ -2157,18 +2057,6 @@ dependencies = [ "tokio-rustls", ] -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper", - "pin-project-lite", - "tokio", - "tokio-io-timeout", -] - [[package]] name = "hyper-tls" version = "0.5.0" @@ -2801,12 +2689,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "matchit" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -3104,15 +2986,11 @@ dependencies = [ "anyhow", "futures", "indexmap 1.9.3", - "indoc", "next-core", - "once_cell", "serde", "serde_json", "shadow-rs", - "tokio", "tracing", - "tracing-subscriber", "turbo-tasks", "turbopack-binding", ] @@ -3121,9 +2999,7 @@ dependencies = [ name = "next-build" version = "0.1.0" dependencies = [ - "console-subscriber", "next-core", - "turbo-tasks", "turbopack-binding", "vergen 7.5.1", ] @@ -3218,7 +3094,7 @@ dependencies = [ [[package]] name = "node-file-trace" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "serde", @@ -3934,38 +3810,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48e50df39172a3e7eb17e14642445da64996989bc212b583015435d39a58537" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea9b0f8cbe5e15a8a042d030bd96668db28ecb567ec37d691971ff5731d2b1b" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-types" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379119666929a1afd7a043aa6cf96fa67a6dce9af60c88095a4686dbce4c9c88" -dependencies = [ - "prost", -] - [[package]] name = "psm" version = "0.1.21" @@ -6566,12 +6410,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sys-info" version = "0.9.1" @@ -6825,20 +6663,9 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.6", "tokio-macros", - "tracing", "windows-sys 0.48.0", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" version = "2.1.0" @@ -6976,64 +6803,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tonic" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" -dependencies = [ - "async-stream", - "async-trait", - "axum", - "base64 0.13.1", - "bytes", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-timeout", - "percent-encoding", - "pin-project", - "prost", - "prost-derive", - "tokio", - "tokio-stream", - "tokio-util", - "tower", - "tower-layer", - "tower-service", - "tracing", - "tracing-futures", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - [[package]] name = "tower-service" version = "0.3.2" @@ -7084,16 +6853,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.2.0" @@ -7105,16 +6864,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -7125,15 +6874,12 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", - "serde", - "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", ] [[package]] @@ -7174,7 +6920,7 @@ dependencies = [ [[package]] name = "turbo-tasks" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "async-trait", @@ -7205,7 +6951,7 @@ dependencies = [ [[package]] name = "turbo-tasks-build" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "cargo-lock", @@ -7217,7 +6963,7 @@ dependencies = [ [[package]] name = "turbo-tasks-bytes" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "bytes", @@ -7231,7 +6977,7 @@ dependencies = [ [[package]] name = "turbo-tasks-env" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "dotenvs", @@ -7245,7 +6991,7 @@ dependencies = [ [[package]] name = "turbo-tasks-fetch" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "lazy_static", @@ -7261,7 +7007,7 @@ dependencies = [ [[package]] name = "turbo-tasks-fs" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "auto-hash-map", @@ -7293,7 +7039,7 @@ dependencies = [ [[package]] name = "turbo-tasks-hash" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "base16", "hex", @@ -7305,7 +7051,7 @@ dependencies = [ [[package]] name = "turbo-tasks-macros" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "proc-macro-error", @@ -7318,7 +7064,7 @@ dependencies = [ [[package]] name = "turbo-tasks-macros-shared" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "proc-macro2", "quote", @@ -7328,7 +7074,7 @@ dependencies = [ [[package]] name = "turbo-tasks-malloc" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "mimalloc", ] @@ -7336,7 +7082,7 @@ dependencies = [ [[package]] name = "turbo-tasks-memory" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "auto-hash-map", @@ -7361,7 +7107,7 @@ dependencies = [ [[package]] name = "turbopack" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "async-recursion", @@ -7391,7 +7137,7 @@ dependencies = [ [[package]] name = "turbopack-binding" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "auto-hash-map", "mdxjs", @@ -7431,7 +7177,7 @@ dependencies = [ [[package]] name = "turbopack-browser" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7454,7 +7200,7 @@ dependencies = [ [[package]] name = "turbopack-cli-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "clap", @@ -7471,7 +7217,7 @@ dependencies = [ [[package]] name = "turbopack-core" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "async-recursion", @@ -7500,7 +7246,7 @@ dependencies = [ [[package]] name = "turbopack-css" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7527,7 +7273,7 @@ dependencies = [ [[package]] name = "turbopack-dev-server" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "async-compression", @@ -7563,7 +7309,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "async-trait", @@ -7598,7 +7344,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript-hmr-protocol" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "serde", "serde_json", @@ -7609,7 +7355,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript-plugins" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "async-trait", @@ -7633,7 +7379,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript-runtime" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "indoc", @@ -7649,7 +7395,7 @@ dependencies = [ [[package]] name = "turbopack-env" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7665,7 +7411,7 @@ dependencies = [ [[package]] name = "turbopack-image" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "base64 0.21.4", @@ -7684,7 +7430,7 @@ dependencies = [ [[package]] name = "turbopack-json" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "serde", @@ -7699,7 +7445,7 @@ dependencies = [ [[package]] name = "turbopack-mdx" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "mdxjs", @@ -7714,7 +7460,7 @@ dependencies = [ [[package]] name = "turbopack-node" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "async-stream", @@ -7748,7 +7494,7 @@ dependencies = [ [[package]] name = "turbopack-nodejs" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7768,7 +7514,7 @@ dependencies = [ [[package]] name = "turbopack-resolve" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7786,7 +7532,7 @@ dependencies = [ [[package]] name = "turbopack-static" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "serde", @@ -7802,7 +7548,7 @@ dependencies = [ [[package]] name = "turbopack-swc-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "swc_core", "turbo-tasks", @@ -7813,7 +7559,7 @@ dependencies = [ [[package]] name = "turbopack-trace-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "crossbeam-channel", @@ -7829,7 +7575,7 @@ dependencies = [ [[package]] name = "turbopack-wasm" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240411.3#9d45eacc8bf80f52cd445d2926fabbf9fe7a535c" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-240417.1#e7a5c60a3b899d0c4293589c2844016da82442f7" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -8304,6 +8050,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "swc_core", + "tracing", "turbopack-binding", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/Cargo.toml b/Cargo.toml index ad6f5669375278..7c551bb22b2e34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,11 +37,11 @@ swc_core = { version = "0.90.30", features = [ testing = { version = "0.35.22" } # Turbo crates -turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-240411.3" } +turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-240417.1" } # [TODO]: need to refactor embed_directory! macro usages, as well as resolving turbo_tasks::function, macros.. -turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-240411.3" } +turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-240417.1" } # [TODO]: need to refactor embed_directory! macro usage in next-core -turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-240411.3" } +turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-240417.1" } # General Deps diff --git a/contributing/core/developing.md b/contributing/core/developing.md index f9d3b5a55bd942..a3dbdc5cf35b37 100644 --- a/contributing/core/developing.md +++ b/contributing/core/developing.md @@ -10,7 +10,7 @@ To develop locally: 1. Install the [GitHub CLI](https://github.com/cli/cli#installation). 1. Clone the Next.js repository (download only recent commits for faster clone): ``` - gh repo clone vercel/next.js -- --depth=3000 --branch canary --single-branch + gh repo clone vercel/next.js -- --filter=blob:none --branch canary --single-branch ``` 1. Create a new branch: ``` diff --git a/contributing/repository/triaging.md b/contributing/repository/triaging.md index 98346637cb9663..508a8ced44da74 100644 --- a/contributing/repository/triaging.md +++ b/contributing/repository/triaging.md @@ -6,9 +6,9 @@ Repository maintainers triage every issue and PR opened in the repository. Issues are opened with one of these labels: -- `template: bug` - unverified issue with Next.js itself -- `template: documentation` - feedback for improvement or an unverified issue with the Next.js documentation -- `area: examples` - an issue with one of the examples in the [`examples`](https://github.com/vercel/next.js/tree/canary/examples) folder +- `bug` - issue with Next.js itself +- `documentation` - feedback for improvement or an issue with the Next.js documentation +- `example bug` - an issue with one of the examples in the [`examples`](https://github.com/vercel/next.js/tree/canary/examples) folder ## Bug reports @@ -16,7 +16,7 @@ Issues are opened with one of these labels: In case of a bug report, **if the reproduction is missing or insufficient, the issue is automatically closed**, and a comment is added with a correct course of action. The issue will receive [this comment](https://github.com/vercel/next.js/blob/canary/.github/invalid-link.md). We also add an `invalid link` label to mark the issue. To avoid your issue being closed, please follow the [bug report template](https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/1.bug_report.yml) carefully. -If you filled out the "Which area(s) are affected? (Select all that apply)" section of the bug report template, we will add the corresponding `area:` label(s). +If you fill out the "Which area(s) are affected? (Select all that apply)" section of the bug report template, we will add the corresponding [label(s)](https://github.com/vercel/next.js/labels). ### Manual triaging @@ -26,7 +26,7 @@ A maintainer can also manually label an issue with one of the following labels, 1. `please add a complete reproduction` -The provided reproduction is not enough for the maintainers to investigate. If a sufficient reproduction is not provided for more than 30 days, the issue becomes stale and will be automatically closed. If a reproduction is provided within 30 days, a `needs triage` label is added, indicating that the issue needs another look from a maintainer. +The provided reproduction is not enough for the maintainers to investigate. If sufficient reproduction is not provided for more than 30 days, the issue becomes stale and will be automatically closed. If a reproduction is provided within 30 days, a `needs triage` label is added, indicating that the issue needs another look from a maintainer. The issue will receive [this comment](https://github.com/vercel/next.js/blob/canary/.github/comments/invalid-reproduction.md) @@ -53,7 +53,7 @@ The issue will receive [this comment](https://github.com/vercel/next.js/blob/can ## Verified issues -If an issue is verified, it will receive the `linear: next`, `linear: dx` or `linear: web` label and will be tracked by the maintainers. Additionally, one or more `area:` label(s) can be added to indicate which part of Next.js is affected. +If an issue is verified, it will receive the `linear: next`, `linear: dx` or `linear: web` label and will be tracked by the maintainers. Additionally, one or more [label(s)](https://github.com/vercel/next.js/labels) can be added to indicate which part of Next.js is affected. Confirmed issues never become stale or are closed before resolution. diff --git a/docs/02-app/01-building-your-application/01-routing/07-route-groups.mdx b/docs/02-app/01-building-your-application/01-routing/07-route-groups.mdx index d0e11b5fb0bcac..a9b1516daf5dfc 100644 --- a/docs/02-app/01-building-your-application/01-routing/07-route-groups.mdx +++ b/docs/02-app/01-building-your-application/01-routing/07-route-groups.mdx @@ -22,7 +22,7 @@ A route group can be created by wrapping a folder's name in parenthesis: `(folde ### Organize routes without affecting the URL path -To organize routes without affecting the URL, create a group to keep related routes together. The folders in parenthesis will be omitted from the URL (e.g. `(marketing)` or `(shop)`). +To organize routes without affecting the URL, create a group to keep related routes together. The folders in parenthesis will be omitted from the URL (e.g. `(marketing)` or `(shop)`. Organizing Routes with Route Groups` and `` tags need to be added to each root layout. +To create multiple [root layouts](/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required), remove the top-level `layout.js` file, and add a `layout.js` file inside each route group. This is useful for partitioning an application into sections that have a completely different UI or experience. The `` and `` tags need to be added to each root layout. Route Groups with Multiple Root LayoutsContent` component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */} -In the context of Next.js, runtime refers to the set of libraries, APIs, and general functionality available to your code during execution. +Next.js has two server runtimes you can use in your application: -On the server, there are two runtimes where parts of your application code can be rendered: +- The **Node.js Runtime** (default) which has access to all Node.js APIs and compatible packages from the ecosystem. +- The **Edge Runtime** which contains a more limited [set of APIs](/docs/app/api-reference/edge). -- The **Node.js Runtime** (default) has access to all Node.js APIs and compatible packages from the ecosystem. -- The **Edge Runtime** is based on [Web APIs](/docs/app/api-reference/edge). +## Use Cases -## Runtime Differences +- The Node.js runtime is used for rendering your application. +- The Edge runtime is used for Middleware (routing rules like redirects, rewrites, and setting headers). -There are many considerations to make when choosing a runtime. This table shows the major differences at a glance. If you want a more in-depth analysis of the differences, check out the sections below. +## Caveats -| | Node | Serverless | Edge | -| ------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- | ---------------- | -| Cold Boot | / | Normal | Low | -| [HTTP Streaming](/docs/app/building-your-application/routing/loading-ui-and-streaming) | Yes | Yes | Yes | -| IO | All | All | `fetch` | -| Scalability | / | High | Highest | -| Security | Normal | High | High | -| Latency | Normal | Low | Lowest | -| npm Packages | All | All | A smaller subset | -| [Static Rendering](/docs/app/building-your-application/rendering/server-components#static-rendering-default) | Yes | Yes | No | -| [Dynamic Rendering](/docs/app/building-your-application/rendering/server-components#dynamic-rendering) | Yes | Yes | Yes | -| [Data Revalidation w/ `fetch`](/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#revalidating-data) | Yes | Yes | Yes | - -### Edge Runtime - -In Next.js, the lightweight Edge Runtime is a subset of available Node.js APIs. - -The Edge Runtime is ideal if you need to deliver dynamic, personalized content at low latency with small, simple functions. The Edge Runtime's speed comes from its minimal use of resources, but that can be limiting in many scenarios. - -For example, code executed in the Edge Runtime [on Vercel cannot exceed between 1 MB and 4 MB](https://vercel.com/docs/concepts/limits/overview#edge-middleware-and-edge-functions-size), this limit includes imported packages, fonts and files, and will vary depending on your deployment infrastructure. In addition, the Edge Runtime does not support all Node.js APIs meaning some `npm` packages may not work. For example, "Module not found: Can't resolve 'fs'" or similar errors. We recommend using the Node.js runtime if you need to use these APIs or packages. - -### Node.js Runtime - -Using the Node.js runtime gives you access to all Node.js APIs, and all npm packages that rely on them. However, it's not as fast to start up as routes using the Edge runtime. - -Deploying your Next.js application to a Node.js server will require managing, scaling, and configuring your infrastructure. Alternatively, you can consider deploying your Next.js application to a serverless platform like Vercel, which will handle this for you. - -### Serverless Node.js - -Serverless is ideal if you need a scalable solution that can handle more complex computational loads than the Edge Runtime. With Serverless Functions on Vercel, for example, your overall code size is [50MB](https://vercel.com/docs/concepts/limits/overview#serverless-function-size) including imported packages, fonts, and files. - -The downside compared to routes using the [Edge](https://vercel.com/docs/concepts/functions/edge-functions) is that it can take hundreds of milliseconds for Serverless Functions to boot up before they begin processing requests. Depending on the amount of traffic your site receives, this could be a frequent occurrence as the functions are not frequently "warm". - - - -## Examples - -### Segment Runtime Option - -You can specify a runtime for individual route segments in your Next.js application. To do so, [declare a variable called `runtime` and export it](/docs/app/api-reference/file-conventions/route-segment-config). The variable must be a string, and must have a value of either `'nodejs'` or `'edge'` runtime. - -The following example demonstrates a page route segment that exports a `runtime` with a value of `'edge'`: - -```tsx filename="app/page.tsx" switcher -export const runtime = 'edge' // 'nodejs' (default) | 'edge' -``` - -```jsx filename="app/page.js" switcher -export const runtime = 'edge' // 'nodejs' (default) | 'edge' -``` - -You can also define `runtime` on a layout level, which will make all routes under the layout run on the edge runtime: - -```tsx filename="app/layout.tsx" switcher -export const runtime = 'edge' // 'nodejs' (default) | 'edge' -``` - -```jsx filename="app/layout.js" switcher -export const runtime = 'edge' // 'nodejs' (default) | 'edge' -``` - -If the segment runtime is _not_ set, the default `nodejs` runtime will be used. You do not need to use the `runtime` option if you do not plan to change from the Node.js runtime. - - - -> Please refer to the [Node.js Docs](https://nodejs.org/docs/latest/api/) and [Edge Docs](/docs/app/api-reference/edge) for the full list of available APIs. Both runtimes can also support [streaming](/docs/app/building-your-application/routing/loading-ui-and-streaming) depending on your deployment infrastructure. +- The Edge runtime does not support all Node.js APIs. Some packages will not work. Learn more about the unsupported APIs in the [Edge Runtime](/docs/app/api-reference/edge#unsupported-apis). +- The Edge runtime does not support Incremental Static Regeneration (ISR). +- Both runtimes can support [streaming](/docs/app/building-your-application/routing/loading-ui-and-streaming) depending on your deployment infrastructure. diff --git a/docs/02-app/01-building-your-application/05-styling/03-css-in-js.mdx b/docs/02-app/01-building-your-application/05-styling/03-css-in-js.mdx index d28b15e3bc444e..45c5232a760702 100644 --- a/docs/02-app/01-building-your-application/05-styling/03-css-in-js.mdx +++ b/docs/02-app/01-building-your-application/05-styling/03-css-in-js.mdx @@ -24,6 +24,7 @@ The following libraries are supported in Client Components in the `app` director - [`tamagui`](https://tamagui.dev/docs/guides/next-js#server-components) - [`tss-react`](https://tss-react.dev/) - [`vanilla-extract`](https://vanilla-extract.style) +- [`ant-design`](https://ant.design/docs/react/use-with-next#using-app-router) The following are currently working on support: diff --git a/docs/02-app/01-building-your-application/06-optimizing/04-metadata.mdx b/docs/02-app/01-building-your-application/06-optimizing/04-metadata.mdx index e6bb31efd2e4d7..4648804555c4cb 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/04-metadata.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/04-metadata.mdx @@ -247,15 +247,11 @@ export const metadata = { The `ImageResponse` constructor allows you to generate dynamic images using JSX and CSS. This is useful for creating social media images such as Open Graph images, Twitter cards, and more. -`ImageResponse` uses the [Edge Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#edge-runtime), and Next.js automatically adds the correct headers to cached images at the edge, helping improve performance and reducing recomputation. - To use it, you can import `ImageResponse` from `next/og`: ```jsx filename="app/about/route.js" import { ImageResponse } from 'next/og' -export const runtime = 'edge' - export async function GET() { return new ImageResponse( ( diff --git a/docs/02-app/01-building-your-application/06-optimizing/09-instrumentation.mdx b/docs/02-app/01-building-your-application/06-optimizing/09-instrumentation.mdx index 901ea85aeab72c..a6e3257743454e 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/09-instrumentation.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/09-instrumentation.mdx @@ -70,7 +70,7 @@ export async function register() { ### Importing runtime-specific code -Next.js calls `register` in all environments, so it's important to conditionally import any code that doesn't support specific runtimes (e.g. [Edge](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#edge-runtime) or [Node.js](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#nodejs-runtime)). You can use the `NEXT_RUNTIME` environment variable to get the current environment: +Next.js calls `register` in all environments, so it's important to conditionally import any code that doesn't support specific runtimes (e.g. [Edge or Node.js](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes)). You can use the `NEXT_RUNTIME` environment variable to get the current environment: ```ts filename="instrumentation.ts" switcher export async function register() { diff --git a/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx b/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx index 4cbbda31cf8b36..4c6f826bdebdba 100644 --- a/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx +++ b/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx @@ -40,7 +40,7 @@ npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx Update the `next.config.mjs` file at your project's root to configure it to use MDX: -```js filename="next.config.mjs" switcher +```js filename="next.config.mjs" import createMDX from '@next/mdx' /** @type {import('next').NextConfig} */ @@ -539,6 +539,8 @@ export default function MDXPage({ children }) { } ``` + + ## Frontmatter Frontmatter is a YAML like key/value pairing that can be used to store data about a page. `@next/mdx` does **not** support frontmatter by default, though there are many solutions for adding frontmatter to your MDX content, such as: diff --git a/docs/02-app/01-building-your-application/09-authentication/index.mdx b/docs/02-app/01-building-your-application/09-authentication/index.mdx index c65b1f58b33fd8..ab2c22798038ea 100644 --- a/docs/02-app/01-building-your-application/09-authentication/index.mdx +++ b/docs/02-app/01-building-your-application/09-authentication/index.mdx @@ -1,38 +1,417 @@ --- title: Authentication -description: Learn how to implement authentication in Next.js, covering best practices, securing routes, authorization techniques, and session management. +description: Learn how to implement authentication in your Next.js application. --- -To implement authentication in Next.js, familiarize yourself with three foundational concepts: +Understanding authentication is crucial for protecting your application's data. This page will guide you through what React and Next.js features to use to implement auth. -- **[Authentication](#authentication)** verifies if the user is who they say they are. It requires the user to prove their identity with something they have, such as a username and password. -- **[Session Management](#session-management)** tracks the user's state (e.g. logged in) across multiple requests. -- **[Authorization](#authorization)** decides what parts of the application the user is allowed to access. +Before starting, it helps to break down the process into three concepts: -This page demonstrates how to use Next.js features to implement common authentication, authorization, and session management patterns so you can choose the best solutions based on your application's needs. +1. **[Authentication](#authentication)**: Verifies if the user is who they say they are. It requires the user to prove their identity with something they have, such as a username and password. +2. **[Session Management](#session-management)**: Tracks the user's auth state across requests. +3. **[Authorization](#authorization)**: Decides what routes and data the user can access. + +This diagram shows the authentication flow using React and Next.js features: + +Diagram showing the authentication flow with React and Next.js features + +The examples on this page walk through basic username and password auth for educational purposes. While you can implement a custom auth solution, for increased security and simplicity, we recommend using an authentication library. These offer built-in solutions for authentication, session management, and authorization, as well as additional features such as social logins, multi-factor authentication, and role-based access control. You can find a list in the [Auth Libraries](#auth-libraries) section. ## Authentication -Authentication verifies a user's identity. This happens when a user logs in, either with a username and password or through a service like Google. It's all about confirming that users are really who they claim to be, protecting both the user's data and the application from unauthorized access or fraudulent activities. + + +### Sign-up and login functionality + +You can use the [`
`](https://react.dev/reference/react-dom/components/form) element with React's [Server Actions](/docs/app/building-your-application/rendering/server-components), [`useFormStatus()`](https://react.dev/reference/react-dom/hooks/useFormStatus), and [`useFormState()`](https://react.dev/reference/react-dom/hooks/useFormState) to capture user credentials, validate form fields, and call your Authentication Provider's API or database. + +Since Server Actions always execute on the server, they provide a secure environment for handling authentication logic. + +Here are the steps to implement signup/login functionality: + +#### 1. Capture user credentials + +To capture user credentials, create a form that invokes a Server Action on submission. For example, a signup form that accepts the user's name, email, and password: + +```tsx filename="app/ui/signup-form.tsx" switcher +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + return ( + +
+ + +
+
+ + +
+
+ + +
+ +
+ ) +} +``` + +```jsx filename="app/ui/signup-form.js" switcher +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ) +} +``` + +```tsx filename="app/actions/auth.tsx" switcher +export async function signup(formData: FormData) {} +``` + +```jsx filename="app/actions/auth.js" switcher +export async function signup(formData) {} +``` + +#### 2. Validate form fields on the server + +Use the Server Action to validate the form fields on the server. If your authentication provider doesn't provide form validation, you can use a schema validation library like [Zod](https://zod.dev/) or [Yup](https://github.com/jquense/yup). + +Using Zod as an example, you can define a form schema with appropriate error messages: + +```ts filename="app/lib/definitions.ts" switcher +import { z } from 'zod' + +export const SignupFormSchema = z.object({ + name: z + .string() + .min(2, { message: 'Name must be at least 2 characters long.' }) + .trim(), + email: z.string().email({ message: 'Please enter a valid email.' }).trim(), + password: z + .string() + .min(8, { message: 'Be at least 8 characters long' }) + .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' }) + .regex(/[0-9]/, { message: 'Contain at least one number.' }) + .regex(/[^a-zA-Z0-9]/, { + message: 'Contain at least one special character.', + }) + .trim(), +}) + +export type FormState = + | { + errors?: { + name?: string[] + email?: string[] + password?: string[] + } + message?: string + } + | undefined +``` + +```js filename="app/lib/definitions.js" switcher +import { z } from 'zod' + +export const SignupFormSchema = z.object({ + name: z + .string() + .min(2, { message: 'Name must be at least 2 characters long.' }) + .trim(), + email: z.string().email({ message: 'Please enter a valid email.' }).trim(), + password: z + .string() + .min(8, { message: 'Be at least 8 characters long' }) + .regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' }) + .regex(/[0-9]/, { message: 'Contain at least one number.' }) + .regex(/[^a-zA-Z0-9]/, { + message: 'Contain at least one special character.', + }) + .trim(), +}) +``` + +To prevent unnecessary calls to your authentication provider's API or database, you can `return` early in the Server Action if any form fields do not match the defined schema. + +```ts filename="app/actions/auth.ts" switcher +import { SignupFormSchema, FormState } from '@/app/lib/definitions' + +export async function signup(state: FormState, formData: FormData) { + // Validate form fields + const validatedFields = SignupFormSchema.safeParse({ + name: formData.get('name'), + email: formData.get('email'), + password: formData.get('password'), + }) + + // If any form fields are invalid, return early + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + } + } + + // Call the provider or db to create a user... +} +``` + +```js filename="app/actions/auth.js" switcher +import { SignupFormSchema } from '@/app/lib/definitions' + +export async function signup(formData: FormData) { + // Validate form fields + const validatedFields = SignupFormSchema.safeParse({ + name: formData.get('name'), + email: formData.get('email'), + password: formData.get('password'), + }) + + // If any form fields are invalid, return early + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + } + } + + // Call the provider or db to create a user... +} +``` + +Back in your ``, you can use React's `useFormState()` hook to display validation errors to the user: + +```tsx filename="app/ui/signup-form.tsx" switcher highlight={7,15,21,27-36} +'use client' + +import { useFormState } from 'react-dom' +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + const [state, action] = useFormState(signup, undefined) -### Authentication Strategies + return ( +
+
+ + +
+ {state?.errors?.name &&

{state.errors.name}

} + +
+ + +
+ {state?.errors?.email &&

{state.errors.email}

} + +
+ + +
+ {state?.errors?.password && ( +
+

Password must:

+
    + {state.errors.password.map((error) => ( +
  • - {error}
  • + ))} +
+
+ )} + + + ) +} +``` + +```jsx filename="app/ui/signup-form.js" switcher highlight={7,15,21,27-36} +'use client' + +import { useFormState } from 'react-dom' +import { signup } from '@/app/actions/auth' + +export function SignupForm() { + const [state, action] = useFormState(signup, undefined) + + return ( +
+
+ + +
+ {state.errors.name &&

{state.errors.name}

} + +
+ + +
+ {state.errors.email &&

{state.errors.email}

} + +
+ + +
+ {state.errors.password && ( +
+

Password must:

+
    + {state.errors.password.map((error) => ( +
  • - {error}
  • + ))} +
+
+ )} + + + ) +} +``` + +You can also use the `useFormStatus()` hook to handle the pending state on form submission: + +```tsx filename="app/ui/signup-form.tsx" highlight={7} switcher +'use client' + +import { useFormStatus, useFormState } from 'react-dom' + +export function SignupButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + +```jsx filename="app/ui/signup-form.js" highlight={7} switcher +'use client' + +import { useFormStatus, useFormState } from 'react-dom' + +export function SignupButton() { + const { pending } = useFormStatus() -Modern web applications commonly use several authentication strategies: + return ( + + ) +} +``` + +> **Good to know:** `useFormStatus()` must be called from a component that is rendered inside a `
`. See the [React Docs](https://react.dev/reference/react-dom/hooks/useFormStatus#usage) for more information. + +#### 3. Create a user or check user credentials + +After validating form fields, you can create a new user account or check if the user exists by calling your authentication provider's API or database. + +Continuing from the previous example: + +```tsx filename="app/actions/auth.tsx" switcher +export async function signup(state: FormState, formData: FormData) { + // 1. Validate form fields + // ... + + // 2. Prepare data for insertion into database + const { name, email, password } = validatedFields.data + // e.g. Hash the user's password before storing it + const hashedPassword = await bcrypt.hash(password, 10) + + // 3. Insert the user into the database or call an Auth Library's API + const data = await db + .insert(users) + .values({ + name, + email, + password: hashedPassword, + }) + .returning({ id: users.id }) + + const user = data[0] + + if (!user) { + return { + message: 'An error occurred while creating your account.', + } + } + + // TODO: + // 4. Create user session + // 5. Redirect user +} +``` + +```jsx filename="app/actions/auth.js" switcher +export async function signup(state, formData) { + // 1. Validate form fields + // ... + + // 2. Prepare data for insertion into database + const { name, email, password } = validatedFields.data + // e.g. Hash the user's password before storing it + const hashedPassword = await bcrypt.hash(password, 10) + + // 3. Insert the user into the database or call an Library API + const data = await db + .insert(users) + .values({ + name, + email, + password: hashedPassword, + }) + .returning({ id: users.id }) + + const user = data[0] + + if (!user) { + return { + message: 'An error occurred while creating your account.', + } + } -1. **OAuth/OpenID Connect (OIDC)**: Enable third-party access without sharing user credentials. Ideal for social media logins and Single Sign-On (SSO) solutions. They add an identity layer with OpenID Connect. -2. **Credentials-based login (Email + Password)**: A standard choice for web applications, where users log in with an email and password. Familiar and easy to implement, it requires robust security measures against threats like phishing. -3. **Passwordless/Token-based authentication**: Use email magic links or SMS one-time codes for secure, password-free access. Popular for its convenience and enhanced security, this method helps reduce password fatigue. Its limitation is the dependency on the user's email or phone availability. -4. **Passkeys/WebAuthn**: Use cryptographic credentials unique to each site, offering high security against phishing. Secure but new, this strategy can be difficult to implement. + // TODO: + // 4. Create user session + // 5. Redirect user +} +``` -Selecting an authentication strategy should align with your application's specific requirements, user interface considerations, and security objectives. +After successfully creating the user account or verifying the user credentials, you can create a session to manage the user's auth state. Depending on your session management strategy, the session can be stored in a cookie or database, or both. Continue to the [Session Management](#session-management) section to learn more. -### Implementing Authentication +> **Tips:** +> +> - The example above is verbose since it breaks down the authentication steps for the purpose of education. This highlights that implementing your own secure solution can quickly become complex. Consider using an [Auth Library](#auth-libraries) to simplify the process. +> - To improve the user experience, you may want to check for duplicate emails or usernames earlier in the registration flow. For example, as the user types in a username or the input field loses focus. This can help prevent unnecessary form submissions and provide immediate feedback to the user. You can debounce requests with libraries such as [use-debounce](https://www.npmjs.com/package/use-debounce) to manage the frequency of these checks. -In this section, we'll explore the process of adding basic email-password authentication to a web application. While this method provides a fundamental level of security, it's worth considering more advanced options like OAuth or passwordless logins for enhanced protection against common security threats. The authentication flow we'll discuss is as follows: + -1. The user submits their credentials through a login form. +Here are the steps to implement a sign-up and/or login form: + +1. The user submits their credentials through a form. 2. The form sends a request that is handled by an API route. 3. Upon successful verification, the process is completed, indicating the user's successful authentication. 4. If verification is unsuccessful, an error message is shown. @@ -161,730 +540,1133 @@ export default async function handler(req, res) { +## Session Management + +Session management ensures that the user's authenticated state is preserved across requests. It involves creating, storing, refreshing, and deleting sessions or tokens. + +There are two types of sessions: + +1. [**Stateless**](#stateless-sessions): Session data (or a token) is stored in the browser's cookies. The cookie is sent with each request, allowing the session to be verified on the server. This method is simpler, but can be less secure if not implemented correctly. +2. [**Database**](#database-sessions): Session data is stored in a database, with the user's browser only receiving the encrypted session ID. This method is more secure, but can be complex and use more server resources. + +> **Good to know:** While you can use either method, or both, we recommend using session management library such as [iron-session](https://github.com/vvo/iron-session) or [Jose](https://github.com/panva/jose). + +### Stateless Sessions + -1. The user submits their credentials through a login form. -2. The form calls a Server Action. -3. Upon successful verification, the process is completed, indicating the user's successful authentication. -4. If verification is unsuccessful, an error message is shown. +To create and manage stateless sessions, there are a few steps you need to follow: -Consider a login form where users can input their credentials: +1. Generate a secret key, which will be used to sign your session, and store it as an [environment variable](/docs/app/building-your-application/configuring/environment-variables). +2. Write logic to encrypt/decrypt session data using a session management library. +3. Manage cookies using the Next.js [`cookies()`](/docs/app/api-reference/functions/cookies) API. -```tsx filename="app/login/page.tsx" switcher -import { authenticate } from '@/app/lib/actions' +In addition to the above, consider adding functionality to [update (or refresh)](#updating-or-refreshing-sessions) the session when the user returns to the application, and [delete](#deleting-the-session) the session when the user logs out. -export default function Page() { - return ( - - - - - - ) -} +> **Good to know:** Check if your [auth library](#auth-libraries) includes session management. + +#### 1. Generating a secret key + +There are a few ways you can generate secret key to sign your session. For example, you may choose to use the `openssl` command in your terminal: + +```bash filename="terminal" +openssl rand -base64 32 ``` -```jsx filename="app/login/page.jsx" switcher -import { authenticate } from '@/app/lib/actions' +This command generates a 32-character random string that you can use as your secret key and store in your [environment variables file](/docs/app/building-your-application/configuring/environment-variables): -export default function Page() { - return ( -
- - - -
- ) -} +```bash filename=".env" +SESSION_SECRET=your_secret_key ``` -The form above has two input fields for capturing the user's email and password. On submission, it calls the `authenticate` Server Action. +You can then reference this key in your session management logic: -You can then call your Authentication Provider's API in the Server Action to handle authentication: +```js filename="app/lib/session.js" +const secretKey = process.env.SESSION_SECRET +``` -```ts filename="app/lib/actions.ts" switcher -'use server' +#### 2. Encrypting and decrypting sessions -import { signIn } from '@/auth' +Next, you can use your preferred [session management library](#session-management-libraries) to encrypt and decrypt sessions. Continuing from the previous example, we'll use [Jose](https://www.npmjs.com/package/jose) (compatible with the [Edge Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes)) and React's [`server-only`](https://www.npmjs.com/package/server-only) package to ensure that your session management logic is only executed on the server. + +```tsx filename="app/lib/session.ts" switcher +import 'server-only' +import { SignJWT, jwtVerify } from 'jose' +import { SessionPayload } from '@/app/lib/definitions' + +const secretKey = process.env.SESSION_SECRET +const encodedKey = new TextEncoder().encode(secretKey) + +export async function encrypt(payload: SessionPayload) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(encodedKey) +} -export async function authenticate(_currentState: unknown, formData: FormData) { +export async function decrypt(session: string | undefined = '') { try { - await signIn('credentials', formData) + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }) + return payload } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' - } - } - throw error + console.log('Failed to verify session') } } ``` -```js filename="app/lib/actions.js" switcher -'use server' +```jsx filename="app/lib/session.js" switcher +import 'server-only' +import { SignJWT, jwtVerify } from 'jose' -import { signIn } from '@/auth' +const secretKey = process.env.SESSION_SECRET +const encodedKey = new TextEncoder().encode(secretKey) + +export async function encrypt(payload) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(encodedKey) +} -export async function authenticate(_currentState, formData) { +export async function decrypt(session) { try { - await signIn('credentials', formData) + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }) + return payload } catch (error) { - if (error) { - switch (error.type) { - case 'CredentialsSignin': - return 'Invalid credentials.' - default: - return 'Something went wrong.' - } - } - throw error + console.log('Failed to verify session') } } ``` -
- -In this code, the `signIn` method checks the credentials against stored user data. -After the authentication provider processes the credentials, there are two possible outcomes: +> **Tips**: +> +> - The payload should contain the **minimum**, unique user data that'll be used in subsequent requests, such as the user's ID, role, etc. It should not contain personally identifiable information like phone number, email address, credit card information, etc, or sensitive data like passwords. -- **Successful Authentication**: This outcome implies that the login was successful. Further actions, such as accessing protected routes and fetching user information, can then be initiated. -- **Failed Authentication**: In cases where the credentials are incorrect or an error is encountered, the function returns a corresponding error message to indicate the authentication failure. +#### 3. Setting cookies (recommended options) - +To store the session in a cookie, use the Next.js [`cookies()`](/docs/app/api-reference/functions/cookies) API. The cookie should be set on the server, and include the recommended options: -Finally, in your `login-form.tsx` component, you can use React's `useFormState` to call the Server Action and handle form errors, and use `useFormStatus` to handle the pending state of the form: +- **HttpOnly**: Prevents client-side JavaScript from accessing the cookie. +- **Secure**: Use https to send the cookie. +- **SameSite**: Specify whether the cookie can be sent with cross-site requests. +- **Max-Age or Expires**: Delete the cookie after a certain period. +- **Path**: Define the URL path for the cookie. -```tsx filename="app/login/page.tsx" switcher -'use client' +Please refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more information on each of these options. -import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' +```ts filename="app/lib/session.ts" switcher +import 'server-only' +import { cookies } from 'next/headers' -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +export async function createSession(userId: string) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + const session = await encrypt({ userId, expiresAt }) - return ( -
- - -
{errorMessage &&

{errorMessage}

}
- - - ) + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) } +``` -function LoginButton() { - const { pending } = useFormStatus() +```js filename="app/lib/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' - const handleClick = (event) => { - if (pending) { - event.preventDefault() - } - } +export async function createSession(userId: string) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + const session = await encrypt({ userId, expiresAt }) - return ( - - ) + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) } ``` -```jsx filename="app/login/page.jsx" switcher -'use client' +Back in your Server Action, you can invoke the `createSession()` function, and use the [`redirect()`](/docs/app/building-your-application/routing/redirecting) API to redirect the user to the appropriate page: -import { authenticate } from '@/app/lib/actions' -import { useFormState, useFormStatus } from 'react-dom' +```ts filename="app/actions/auth.ts" switcher +import { createSession } from '@/app/lib/session' -export default function Page() { - const [errorMessage, dispatch] = useFormState(authenticate, undefined) +export async function signup(state: FormState, formData: FormData) { + // Previous steps: + // 1. Validate form fields + // 2. Prepare data for insertion into database + // 3. Insert the user into the database or call an Library API - return ( -
- - -
{errorMessage &&

{errorMessage}

}
- - - ) + // Current steps: + // 4. Create user session + await createSession(user.id) + // 5. Redirect user + redirect('/profile') } +``` -function LoginButton() { - const { pending } = useFormStatus() +```js filename="app/actions/auth.js" switcher +import { createSession } from '@/app/lib/session' - const handleClick = (event) => { - if (pending) { - event.preventDefault() - } - } +export async function signup(state, formData) { + // Previous steps: + // 1. Validate form fields + // 2. Prepare data for insertion into database + // 3. Insert the user into the database or call an Library API - return ( - - ) + // Current steps: + // 4. Create user session + await createSession(user.id) + // 5. Redirect user + redirect('/profile') } ``` -
- -For a more streamlined authentication setup in Next.js projects, especially when offering multiple login methods, consider using a comprehensive [authentication solution](#examples). - -## Authorization - -Once a user is authenticated, you'll need to ensure the user is allowed to visit certain routes, and perform operations such as mutating data with Server Actions and calling Route Handlers. - -### Protecting Routes with Middleware - -[Middleware](/docs/app/building-your-application/routing/middleware) in Next.js helps you control who can access different parts of your website. This is important for keeping areas like the user dashboard protected while having other pages like marketing pages be public. It's recommended to apply Middleware across all routes and specify exclusions for public access. +> **Tips**: +> +> - **Cookies should be set on the server** to prevent client-side tampering. +> - 🎥 Watch: Learn more about stateless sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). -Here's how to implement Middleware for authentication in Next.js: +#### Updating (or refreshing) sessions -#### Setting Up Middleware: +You can also extend the session's expiration time. This is useful for keeping the user logged in after they access the application again. For example: -- Create a `middleware.ts` or `.js` file in your project's root directory. -- Include logic to authorize user access, such as checking for authentication tokens. - -#### Defining Protected Routes: - -- Not all routes require authorization. Use the `matcher` option in your Middleware to specify any routes that do not require authorization checks. - -#### Middleware Logic: - -- Write logic to verify if a user is authenticated. Check user roles or permissions for route authorization. - -#### Handling Unauthorized Access: +```ts filename="app/lib/session.ts" switcher +import 'server-only' +import { cookies } from 'next/headers' -- Redirect unauthorized users to a login or error page as appropriate. +export async function updateSession() { + const session = cookies().get('session')?.value + const payload = await decrypt(session) -Example Middleware file: + if (!session || !payload) { + return null + } -```ts filename="middleware.ts" switcher -import type { NextRequest } from 'next/server' + const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expires, + sameSite: 'lax', + path: '/', + }) +} +``` -export function middleware(request: NextRequest) { - const currentUser = request.cookies.get('currentUser')?.value +```js filename="app/lib/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' - if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) { - return Response.redirect(new URL('/dashboard', request.url)) - } +eexport async function updateSession() { + const session = cookies().get('session').value + const payload = await decrypt(session) - if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) { - return Response.redirect(new URL('/login', request.url)) + if (!session || !payload) { + return null } -} -export const config = { - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], + const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expires, + sameSite: 'lax', + path: '/', + }) } ``` -```js filename="middleware.js" switcher -export function middleware(request) { - const currentUser = request.cookies.get('currentUser')?.value +> **Tip:** Check if your auth library supports refresh tokens, which can be used to extend the user's session. - if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) { - return Response.redirect(new URL('/dashboard', request.url)) - } +#### Deleting the session - if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) { - return Response.redirect(new URL('/login', request.url)) - } -} +To delete the session, you can delete the cookie: -export const config = { - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], +```ts filename="app/lib/session.ts" switcher +import 'server-only' +import { cookies } from 'next/headers' + +export function deleteSession() { + cookies().delete('session') } ``` -This example uses [`Response.redirect`](https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static) for handling redirects early in the request pipeline, making it efficient and centralizing access control. - - +```js filename="app/lib/session.js" switcher +import 'server-only' +import { cookies } from 'next/headers' -For specific redirection needs, the `redirect` function can be used in Server Components, Route Handlers, and Server Actions to provide more control. This is useful for role-based navigation or context-sensitive scenarios. +export function deleteSession() { + cookies().delete('session') +} +``` -```ts filename="app/page.tsx" switcher -import { redirect } from 'next/navigation' +Then you can reuse the `deleteSession()` function in your application, for example, on logout: -export default function Page() { - // Logic to determine if a redirect is needed - const accessDenied = true - if (accessDenied) { - redirect('/login') - } +```ts filename="app/actions/auth.ts" switcher +import { cookies } from 'next/headers' +import { deleteSession } from '@/app/lib/session' - // Define other routes and logic +export async function logout() { + deleteSession() + redirect('/login') } ``` -```js filename="app/page.jsx" switcher -import { redirect } from 'next/navigation' - -export default function Page() { - // Logic to determine if a redirect is needed - const accessDenied = true - if (accessDenied) { - redirect('/login') - } +```js filename="app/actions/auth.js" switcher +import { cookies } from 'next/headers' +import { deleteSession } from '@/app/lib/session' - // Define other routes and logic +export async function logout() { + deleteSession() + redirect('/login') } ``` -After successful authentication, it's important to manage user navigation based on their roles. For example, an admin user might be redirected to an admin dashboard, while a regular user is sent to a different page. This is important for role-specific experiences and conditional navigation, such as prompting users to complete their profile if needed. + -When setting up authorization, it's important to ensure that the main security checks happen where your app accesses or changes data. While Middleware can be useful for initial validation, it should not be the sole line of defense in protecting your data. The bulk of security checks should be performed in the Data Access Layer (DAL). +#### Setting and deleting cookies - +You can use [API Routes](/docs/pages/building-your-application/routing/api-routes) to set the session as a cookie on the server: -### Protecting API Routes +```ts filename="pages/api/login.ts" switcher +import { serialize } from 'cookie' +import type { NextApiRequest, NextApiResponse } from 'next' -API Routes in Next.js are essential for handling server-side logic and data management. It's crucial to secure these routes to ensure that only authorized users can access specific functionalities. This typically involves verifying the user's authentication status and their role-based permissions. +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const sessionData = req.body + const encryptedSessionData = encrypt(sessionData) -Here's an example of securing an API Route: + const cookie = serialize('session', encryptedSessionData, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 7, // One week + path: '/', + }) + res.setHeader('Set-Cookie', cookie) + res.status(200).json({ message: 'Successfully set cookie!' }) +} +``` -```ts filename="pages/api/route.ts" switcher +```js filename="pages/api/login.js" switcher +import { serialize } from 'cookie' + +export default function handler(req, res) { + const sessionData = req.body + const encryptedSessionData = encrypt(sessionData) + + const cookie = serialize('session', encryptedSessionData, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + maxAge: 60 * 60 * 24 * 7, // One week + path: '/', + }) + res.setHeader('Set-Cookie', cookie) + res.status(200).json({ message: 'Successfully set cookie!' }) +} +``` + + + +### Database Sessions + +To create and manage database sessions, you'll need to follow these steps: + +1. Create a table in your database to store session and data (or check if your Auth Library handles this). +2. Implement functionality to insert, update, and delete sessions +3. Encrypt the session ID before storing it in the user's browser, and ensure the database and cookie stay in sync (this is optional, but recommended for optimistic auth checks in [Middleware](#optimistic-checks-with-middleware-optional)). + + + +For example: + +```ts filename="app/lib/session.ts" switcher +import cookies from 'next/headers' +import { db } from '@/app/lib/db' + +export async function createSession(id: number) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + + // 1. Create a session in the database + const data = await db + .insert(sessions) + .values({ + userId: id, + expiresAt, + }) + // Return the session ID + .returning({ id: sessions.id }) + + const sessionId = data[0].id + + // 2. Encrypt the session ID + const session = await encrypt({ sessionId, expiresAt }) + + // 3. Store the session in cookies for optimistic auth checks + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) +} +``` + +```js filename="app/lib/session.js" switcher +import cookies from 'next/headers' +import { db } from '@/app/lib/db' + +export async function createSession(id) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + + // 1. Create a session in the database + const data = await db + .insert(sessions) + .values({ + userId: id, + expiresAt, + }) + // Return the session ID + .returning({ id: sessions.id }) + + const sessionId = data[0].id + + // 2. Encrypt the session ID + const session = await encrypt({ sessionId, expiresAt }) + + // 3. Store the session in cookies for optimistic auth checks + cookies().set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }) +} +``` + +> **Tips**: +> +> - For faster data retrieval, consider using a database like [Vercel Redis](https://vercel.com/docs/storage/vercel-kv). However, you can also keep the session data in your primary database, and combine data requests to reduce the number of queries. +> - You may opt to use database sessions for more advanced use cases, such as keeping track of the last time a user logged in, or number of active devices, or give users the ability to log out of all devices. + +After implementing session management, you'll need to add authorization logic to control what users can access and do within your application. Continue to the [Authorization](#authorization) section to learn more. + + + + + +**Creating a Session on the Server**: + +```ts filename="pages/api/create-session.ts" switcher +import db from '../../lib/db' import { NextApiRequest, NextApiResponse } from 'next' export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - const session = await getSession(req) - - // Check if the user is authenticated - if (!session) { - res.status(401).json({ - error: 'User is not authenticated', + try { + const user = req.body + const sessionId = generateSessionId() + await db.insertSession({ + sessionId, + userId: user.id, + createdAt: new Date(), }) - return - } - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - res.status(401).json({ - error: 'Unauthorized access: User does not have admin privileges.', - }) - return + res.status(200).json({ sessionId }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) } - - // Proceed with the route for authorized users - // ... implementation of the API Route } ``` -```js filename="pages/api/route.js" switcher -export default async function handler(req, res) { - const session = await getSession(req) +```js filename="pages/api/create-session.js" switcher +import db from '../../lib/db' - // Check if the user is authenticated - if (!session) { - res.status(401).json({ - error: 'User is not authenticated', +export default async function handler(req, res) { + try { + const user = req.body + const sessionId = generateSessionId() + await db.insertSession({ + sessionId, + userId: user.id, + createdAt: new Date(), }) - return - } - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - res.status(401).json({ - error: 'Unauthorized access: User does not have admin privileges.', - }) - return + res.status(200).json({ sessionId }) + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }) } - - // Proceed with the route for authorized users - // ... implementation of the API Route } ``` -This example demonstrates an API Route with a two-tier security check for authentication and authorization. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. This approach ensures secure access, limited to authenticated and authorized users, maintaining robust security for request processing. - - +## Authorization -This approach, highlighted in [this security blog](/blog/security-nextjs-server-components-actions), advocates for consolidating all data access within a dedicated DAL. This strategy ensures consistent data access, minimizes authorization bugs, and simplifies maintenance. To ensure comprehensive security, consider the following key areas: +Once a user is authenticated and a session is created, you can implement authorization to control what the user can access and do within your application. -- Server Actions: Implement security checks in server-side processes, especially for sensitive operations. -- Route Handlers: Manage incoming requests with security measures to ensure access is limited to authorized users. -- Data Access Layer (DAL): Directly interacts with the database and is crucial for validating and authorizing data transactions. It's vital to perform critical checks within the DAL to secure data at its most crucial interaction point—access or modification. +There are two main types of authorization checks: -For a detailed guide on securing the DAL, including example code snippets and advanced security practices, refer to our [Data Access Layer section](/blog/security-nextjs-server-components-actions#data-access-layer) of the security guide. +1. **Optimistic**: Checks if the user is authorized to access a route or perform an action using the session data stored in the cookie. These checks are useful for quick operations, such as showing/hiding UI elements or redirecting users based on permissions or roles. +2. **Secure**: Checks if the user is authorized to access a route or perform an action using the session data stored in the database. These checks are more secure and are used for operations that require access to sensitive data or actions. -### Protecting Server Actions +For both cases, we recommend: -It is important to treat [Server Actions](/docs/app/building-your-application/data-fetching/server-actions-and-mutations) with the same security considerations as public-facing API endpoints. Verifying user authorization for each action is crucial. Implement checks within Server Actions to determine user permissions, such as restricting certain actions to admin users. +- Creating a [Data Access Layer](#creating-a-data-access-layer-dal) to centralize your authorization logic +- Using [Data Transfer Objects (DTO)](#using-data-transfer-objects-dto) to only return the necessary data +- Optionally use [Middleware](#optimistic-checks-with-middleware-optional) to perform optimistic checks. -In the example below, we check the user's role before allowing the action to proceed: +### Optimistic checks with Middleware (Optional) -```ts filename="app/lib/actions.ts" switcher -'use server' +There are some cases where you may want to use [Middleware](/docs/app/building-your-application/routing/middleware) and redirect users based on permissions: -// ... +- To perform optimistic checks. Since Middleware runs on every route, it's a good way to centralize redirect logic and pre-filter unauthorized users. +- To protect static routes that share data between users (e.g. content behind a paywall). -export async function serverAction() { - const session = await getSession() - const userRole = session?.user?.role +However, since Middleware runs on every route, including [prefetched](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) routes, it's important to only read the session from the cookie (optimistic checks), and avoid database checks to prevent performance issues. - // Check if user is authorized to perform the action - if (userRole !== 'admin') { - throw new Error('Unauthorized access: User does not have admin privileges.') +For example: + +```tsx filename="middleware.ts" switcher +import { NextRequest, NextResponse } from 'next/server' +import { decrypt } from '@/app/lib/session' +import { cookies } from 'next/headers' + +// 1. Specify protected and public routes +const protectedRoutes = ['/dashboard'] +const publicRoutes = ['/login', '/signup', '/'] + +export default async function middleware(req: NextRequest) { + // 2. Check if the current route is protected or public + const path = req.nextUrl.pathname + const isProtectedRoute = protectedRoutes.includes(path) + const isPublicRoute = publicRoutes.includes(path) + + // 3. Decrypt the session from the cookie + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + + // 5. Redirect to /login if the user is not authenticated + if (isProtectedRoute && !session?.userId) { + return NextResponse.redirect(new URL('/login', req.nextUrl)) } - // Proceed with the action for authorized users - // ... implementation of the action + // 6. Redirect to /dashboard if the user is authenticated + if ( + isPublicRoute && + session?.userId && + !req.nextUrl.pathname.startsWith('/dashboard') + ) { + return NextResponse.redirect(new URL('/dashboard', req.nextUrl)) + } + + return NextResponse.next() +} + +// Routes Middleware should not run on +export const config = { + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], } ``` -```js filename="app/lib/actions.js" switcher -'use server' +```js filename="middleware.js" switcher +import { NextResponse } from 'next/server' +import { decrypt } from '@/app/lib/session' +import { cookies } from 'next/headers' -// ... +// 1. Specify protected and public routes +const protectedRoutes = ['/dashboard'] +const publicRoutes = ['/login', '/signup', '/'] -export async function serverAction() { - const session = await getSession() - const userRole = session?.user?.role +export default async function middleware(req) { + // 2. Check if the current route is protected or public + const path = req.nextUrl.pathname + const isProtectedRoute = protectedRoutes.includes(path) + const isPublicRoute = publicRoutes.includes(path) - // Check if user is authorized to perform the action - if (userRole !== 'admin') { - throw new Error('Unauthorized access: User does not have admin privileges.') + // 3. Decrypt the session from the cookie + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + + // 5. Redirect to /login if the user is not authenticated + if (isProtectedRoute && !session?.userId) { + return NextResponse.redirect(new URL('/login', req.nextUrl)) } - // Proceed with the action for authorized users - // ... implementation of the action + // 6. Redirect to /dashboard if the user is authenticated + if ( + isPublicRoute && + session?.userId && + !req.nextUrl.pathname.startsWith('/dashboard') + ) { + return NextResponse.redirect(new URL('/dashboard', req.nextUrl)) + } + + return NextResponse.next() +} + +// Routes Middleware should not run on +export const config = { + matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], } ``` -### Protecting Route Handlers +While Middleware can be useful for initial checks, it should not be your only line of defense in protecting your data. The majority of security checks should be performed as close as possible to your data source, see [Data Access Layer](#creating-a-data-access-layer-dal) for more information. -Route Handlers in Next.js play a vital role in managing incoming requests. Just like Server Actions, they should be secured to ensure that only authorized users can access certain functionalities. This often involves verifying the user's authentication status and their permissions. +> **Tips**: +> +> - In Middleware, you can also read cookies using `req.cookies.get('session).value`. +> - Middleware uses the [Edge Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes), check if your Auth library and session management library are compatible. +> - You can use the `matcher` property in the Middleware to specify which routes Middleware should run on. Although, for auth, it's recommended Middleware runs on all routes. -Here's an example of securing a Route Handler: + -```ts filename="app/api/route.ts" switcher -export async function GET() { - // User authentication and role verification - const session = await getSession() +### Creating a Data Access Layer (DAL) - // Check if the user is authenticated - if (!session) { - return new Response(null, { status: 401 }) // User is not authenticated +We recommend creating a DAL to centralize your data requests and authorization logic. + +The DAL should include a function that verifies the user's session as they interact with your application. At the very least, the function should check if the session is valid, then redirect or return the user information needed to make further requests. + +For example, create a separate file for your DAL that includes a `verifySession()` function. Then use React's [cache](https://react.dev/reference/react/cache) API to memoize the return value of the function during a React render pass: + +```tsx filename="app/lib/dal.ts" switcher +import 'server-only' + +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session' + +export const verifySession = cache(async () => { + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + + if (!session?.userId) { + redirect('/login') } - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - return new Response(null, { status: 403 }) // User is authenticated but does not have the right permissions + return { isAuth: true, userId: session.userId } +}) +``` + +```js filename="app/lib/dal.js" switcher +import 'server-only' + +import { cookies } from 'next/headers' +import { decrypt } from '@/app/lib/session' + +export const verifySession = cache(async () => { + const cookie = cookies().get('session').value + const session = await decrypt(cookie) + + if (!session.userId) { + redirect('/login') } - // Data fetching for authorized users -} + return { isAuth: true, userId: session.userId } +}) ``` -```js filename="app/api/route.js" switcher -export async function GET() { - // User authentication and role verification - const session = await getSession() +You can then invoke the `verifySession()` function in your data requests, Server Actions, Route Handlers: - // Check if the user is authenticated - if (!session) { - return new Response(null, { status: 401 }) // User is not authenticated +```tsx filename="app/lib/dal.ts" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null + + try { + const data = await db.query.users.findMany({ + where: eq(users.id, session.userId), + // Explicitly return the columns you need rather than the whole user object + columns: { + id: true, + name: true, + email: true, + }, + }) + + const user = data[0] + + return user + } catch (error) { + console.log('Failed to fetch user') + return null } +}) +``` - // Check if the user has the 'admin' role - if (session.user.role !== 'admin') { - return new Response(null, { status: 403 }) // User is authenticated but does not have the right permissions +```jsx filename="app/lib/dal.js" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null + + try { + const data = await db.query.users.findMany({ + where: eq(users.id, session.userId), + // Explicitly return the columns you need rather than the whole user object + columns: { + id: true, + name: true, + email: true, + }, + }) + + const user = data[0] + + return user + } catch (error) { + console.log('Failed to fetch user') + return null } +}) +``` + +> **Tip**: +> +> - A DAL can be used to protect data fetched at request time. However, for static routes that share data between users, data will be fetched at build time and not at request time. Use [Middleware](#optimistic-checks-with-middleware-optional) to protect static routes. +> - For secure checks, you can check if the session is valid by comparing the session ID with your database. Use React's [cache](https://react.dev/reference/react/cache) function to avoid unnecessary duplicate requests to the database during a render pass. +> - You may wish to consolidate related data requests in a JavaScript class that runs `verifySession()` before any methods. + +### Using Data Transfer Objects (DTO) + +When retrieving data, it's recommended you return only the necessary data that will be used in your application, and not entire objects. For example, if you're fetching user data, you might only return the user's ID and name, rather than the entire user object which could contain passwords, phone numbers, etc. + +However, if you have no control over the returned data structure, or are working in a team where you want to avoid whole objects being passed to the client, you can use strategies such as specifying what fields are safe to be exposed to the client. + +```tsx filename="app/lib/dto.ts" switcher +import 'server-only' +import { getUser } from '@/app/lib/dal' + +function canSeeUsername(viewer: User) { + return true +} - // Data fetching for authorized users +function canSeePhoneNumber(viewer: User, team: string) { + return viewer.isAdmin || team === viewer.team +} + +export async function getProfileDTO(slug: string) { + const data = await db.query.users.findMany({ + where: eq(users.slug, slug), + // Return specific columns here + }) + const user = data[0] + + const currentUser = await getUser(user.id) + + // Or return only what's specific to the query here + return { + username: canSeeUsername(currentUser) ? user.username : null, + phonenumber: canSeePhoneNumber(currentUser, user.team) + ? user.phonenumber + : null, + } } ``` -This example demonstrates a Route Handler with a two-tier security check for authentication and authorization. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. This approach ensures secure access, limited to authenticated and authorized users, maintaining robust security for request processing. +```js filename="app/lib/dto.js" switcher +import 'server-only' +import { getUser } from '@/app/lib/dal' -### Authorization Using Server Components +function canSeeUsername(viewer) { + return true +} -[Server Components](/docs/app/building-your-application/rendering/server-components) in Next.js are designed for server-side execution and offer a secure environment for integrating complex logic like authorization. They enable direct access to back-end resources, optimizing performance for data-heavy tasks and enhancing security for sensitive operations. +function canSeePhoneNumber(viewer, team) { + return viewer.isAdmin || team === viewer.team +} + +export async function getProfileDTO(slug) { + const data = await db.query.users.findMany({ + where: eq(users.slug, slug), + // Return specific columns here + }) + const user = data[0] + + const currentUser = await getUser(user.id) + + // Or return only what's specific to the query here + return { + username: canSeeUsername(currentUser) ? user.username : null, + phonenumber: canSeePhoneNumber(currentUser, user.team) + ? user.phonenumber + : null, + } +} +``` -In Server Components, a common practice is to conditionally render UI elements based on the user's role. This approach enhances user experience and security by ensuring users only access content they are authorized to view. +By centralizing your data requests and authorization logic in a DAL and using DTOs, you can ensure that all data requests are secure and consistent, making it easier to maintain, audit, and debug as your application scales. -**Example:** +> **Good to know**: +> +> - There are a couple of different ways you can define a DTO, from using `toJSON()`, to individual functions like the example above, or JS classes. Since these are JavaScript patterns and not a React or Next.js feature, we recommend doing some research to find the best pattern for your application. +> - Learn more about security best practices in our [Security in Next.js article](/blog/security-nextjs-server-components-actions). + +### Server Components + +Auth check in [Server Components](/docs/app/building-your-application/rendering/server-components) are useful for role-based access. For example, to conditionally render components based on the user's role: ```tsx filename="app/dashboard/page.tsx" switcher -export default async function Dashboard() { - const session = await getSession() +import { verifySession } from '@/app/lib/dal' + +export default function Dashboard() { + const session = await verifySession() const userRole = session?.user?.role // Assuming 'role' is part of the session object if (userRole === 'admin') { - return // Component for admin users + return } else if (userRole === 'user') { - return // Component for regular users + return } else { - return // Component shown for unauthorized access + redirect('/login') } } ``` ```jsx filename="app/dashboard/page.jsx" switcher +import { verifySession } from '@/app/lib/dal' + export default function Dashboard() { - const session = await getSession() - const userRole = session?.user?.role // Assuming 'role' is part of the session object + const session = await verifySession() + const userRole = session.role // Assuming 'role' is part of the session object if (userRole === 'admin') { - return // Component for admin users + return } else if (userRole === 'user') { - return // Component for regular users + return } else { - return // Component shown for unauthorized access + redirect('/login') } } ``` -In this example, the Dashboard component renders different UIs for 'admin', 'user', and unauthorized roles. This pattern ensures that each user interacts only with components appropriate to their role, enhancing both security and user experience. +In the example, we use the `verifySession()` function from our DAL to check for 'admin', 'user', and unauthorized roles. This pattern ensures that each user interacts only with components appropriate to their role. - +### Layouts and auth checks -### Best Practices +Due to [Partial Rendering](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), be cautious when doing checks in [Layouts](/docs/app/building-your-application/routing/pages-and-layouts) as these don't re-render on navigation, meaning the user session won't be checked on every route change. -- **Secure Session Management**: Prioritize the security of session data to prevent unauthorized access and data breaches. Use encryption and secure storage practices. -- **Dynamic Role Management**: Use a flexible system for user roles to easily adjust to changes in permissions and roles, avoiding hardcoded roles. -- **Security-First Approach**: In all aspects of authorization logic, prioritize security to safeguard user data and maintain the integrity of your application. This includes thorough testing and considering potential security vulnerabilities. +Instead, you should do the checks close to your data source or the component that'll be conditionally rendered. -## Session Management +For example, consider a shared layout that fetches the user data and displays the user image in a nav. Instead of doing the auth check in the layout, you should fetch the user data (`getUser()`) in the layout and do the auth check in your DAL. + +This guarantees that wherever `getUser()` is called within your application, the auth check is performed, and prevents developers forgetting to check the user is authorized to access the data. + +```tsx filename="app/layout.tsx" switcher +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const user = await getUser(); + + return ( + // ... + ) +} +``` -Session management involves tracking and managing a user's interaction with the application over time, ensuring that their authenticated state is preserved across different parts of the application. +```jsx filename="app/layout.js" switcher +export default async function Layout({ children }) { + const user = await getUser(); -This prevents the need for repeated logins, enhancing both security and user convenience. There are two primary methods used for session management: cookie-based and database sessions. + return ( + // ... + ) +} +``` -### Cookie-Based Sessions +```ts filename="app/lib/dal.ts" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null -> **🎥 Watch:** Learn more about cookie-based sessions and authentication with Next.js → [YouTube (11 minutes)](https://www.youtube.com/watch?v=DJvM2lSPn6w). + // Get user ID from session and fetch data +}) +``` -Cookie-based sessions manage user data by storing encrypted session information directly in browser cookies. Upon user login, this encrypted data is stored in the cookie. Each subsequent server request includes this cookie, minimizing the need for repeated server queries and enhancing client-side efficiency. +```js filename="app/lib/dal.js" switcher +export const getUser = cache(async () => { + const session = await verifySession() + if (!session) return null -However, this method requires careful encryption to protect sensitive data, as cookies are susceptible to client-side security risks. Encrypting session data in cookies is key to safeguarding user information from unauthorized access. It ensures that even if a cookie is stolen, the data inside remains unreadable. + // Get user ID from session and fetch data +}) +``` -Additionally, while individual cookies are limited in size (typically around 4KB), techniques like cookie-chunking can overcome this limitation by dividing large session data into multiple cookies. +> **Good to know:** +> +> - A common pattern in SPAs is to `return null` in a layout or a top-level component if a user is not authorized. This pattern is not **not recommended** since Next.js applications have multiple entry points, which will not prevent nested route segments and Server Actions from being accessed. -Setting a cookie in a Next.js project might look something like this: +### Server Actions -**Setting a cookie on the server:** +Treat [Server Actions](/docs/app/building-your-application/data-fetching/server-actions-and-mutations) with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation. - +In the example below, we check the user's role before allowing the action to proceed: -```ts filename="pages/api/login.ts" switcher -import { serialize } from 'cookie' -import type { NextApiRequest, NextApiResponse } from 'next' +```ts filename="app/lib/actions.ts" switcher +'use server' +import { verifySession } from '@/app/lib/dal' -export default function handler(req: NextApiRequest, res: NextApiResponse) { - const sessionData = req.body - const encryptedSessionData = encrypt(sessionData) +export async function serverAction(formData: FormData) { + const session = await verifySession() + const userRole = session?.user?.role - const cookie = serialize('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - res.setHeader('Set-Cookie', cookie) - res.status(200).json({ message: 'Successfully set cookie!' }) + // Return early if user is not authorized to perform the action + if (userRole !== 'admin') { + return null + } + + // Proceed with the action for authorized users } ``` -```js filename="pages/api/login.js" switcher -import { serialize } from 'cookie' +```js filename="app/lib/actions.js" switcher +'use server' +import { verifySession } from '@/app/lib/dal' -export default function handler(req, res) { - const sessionData = req.body - const encryptedSessionData = encrypt(sessionData) +export async function serverAction() { + const session = await verifySession() + const userRole = session.user.role - const cookie = serialize('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - res.setHeader('Set-Cookie', cookie) - res.status(200).json({ message: 'Successfully set cookie!' }) + // Return early if user is not authorized to perform the action + if (userRole !== 'admin') { + return null + } + + // Proceed with the action for authorized users } ``` - +### Route Handlers - +Treat [Route Handlers](/docs/app/building-your-application/routing/route-handlers) with the same security considerations as public-facing API endpoints, and verify if the user is allowed to access the Route Handler. -```ts filename="app/actions.ts" switcher -'use server' +For example: -import { cookies } from 'next/headers' +```ts filename="app/api/route.ts" switcher +import { verifySession } from '@/app/lib/dal' -export async function handleLogin(sessionData) { - const encryptedSessionData = encrypt(sessionData) // Encrypt your session data - cookies().set('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - // Redirect or handle the response after setting the cookie +export async function GET() { + // User authentication and role verification + const session = await verifySession() + + // Check if the user is authenticated + if (!session) { + // User is not authenticated + return new Response(null, { status: 401 }) + } + + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + // User is authenticated but does not have the right permissions + return new Response(null, { status: 403 }) + } + + // Continue for authorized users } ``` -```js filename="app/actions.js" switcher -'use server' +```js filename="app/api/route.js" switcher +import { verifySession } from '@/app/lib/dal' -import { cookies } from 'next/headers' +export async function GET() { + // User authentication and role verification + const session = await verifySession() -export async function handleLogin(sessionData) { - const encryptedSessionData = encrypt(sessionData) // Encrypt your session data - cookies().set('session', encryptedSessionData, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 60 * 24 * 7, // One week - path: '/', - }) - // Redirect or handle the response after setting the cookie + // Check if the user is authenticated + if (!session) { + // User is not authenticated + return new Response(null, { status: 401 }) + } + + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + // User is authenticated but does not have the right permissions + return new Response(null, { status: 403 }) + } + + // Continue for authorized users } ``` -**Accessing the session data stored in the cookie in a server component:** +The example above demonstrates a Route Handler with a two-tier security check. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. -```tsx filename="app/page.tsx" switcher -import { cookies } from 'next/headers' +## Context Providers + +Using context providers for auth work due to [interleaving](/docs/app/building-your-application/rendering/composition-patterns#interleaving-server-and-client-components). However, React `context` is not supported in Server Components, making them only applicable to Client Components. + +This works, but any child Server Components will be rendered on the server first, and will not have access to the context provider’s session data: -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null +```tsx filename="app/layout.ts" switcher +import { ContextProvider } from 'auth-lib' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) } ``` -```jsx filename="app/page.jsx" switcher -import { cookies } from 'next/headers' +```tsx filename="app/layout.ts" switcher +import { ContextProvider } from 'auth-lib' -export async function getSessionData(req) { - const encryptedSessionData = cookies().get('session')?.value - return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null +export default function RootLayout({ children }) { + return ( + + + {children} + + + ) } ``` - +```tsx filename="app/ui/profile.ts switcher +"use client"; -### Database Sessions +import { useSession } from "auth-lib"; -Database session management involves storing session data on the server, with the user's browser only receiving a session ID. This ID references the session data stored server-side, without containing the data itself. This method enhances security, as it keeps sensitive session data away from the client-side environment, reducing the risk of exposure to client-side attacks. Database sessions are also more scalable, accommodating larger data storage needs. +export default function Profile() { + const { userId } = useSession(); + const { data } = useSWR(`/api/user/${userId}`, fetcher) -However, this approach has its tradeoffs. It can increase performance overhead due to the need for database lookups at each user interaction. Strategies like session data caching can help mitigate this. Additionally, reliance on the database means that session management is as reliable as the database's performance and availability. + return ( + // ... + ); +} +``` -Here's a simplified example of implementing database sessions in a Next.js application: +```jsx filename="app/ui/profile.js switcher +"use client"; -**Creating a Session on the Server**: +import { useSession } from "auth-lib"; + +export default function Profile() { + const { userId } = useSession(); + const { data } = useSWR(`/api/user/${userId}`, fetcher) + + return ( + // ... + ); +} +``` + +If session data is needed in Client Components (e.g. for client-side data fetching),use React’s [`taintUniqueValue`](https://react.dev/reference/react/experimental_taintUniqueValue) API to prevent sensitive session data from being exposed to the client. + + -```ts filename="pages/api/create-session.ts" switcher -import db from '../../lib/db' +### Creating a Data Access Layer (DAL) + +#### Protecting API Routes + +API Routes in Next.js are essential for handling server-side logic and data management. It's crucial to secure these routes to ensure that only authorized users can access specific functionalities. This typically involves verifying the user's authentication status and their role-based permissions. + +Here's an example of securing an API Route: + +```ts filename="pages/api/route.ts" switcher import { NextApiRequest, NextApiResponse } from 'next' export default async function handler( req: NextApiRequest, res: NextApiResponse ) { - try { - const user = req.body - const sessionId = generateSessionId() - await db.insertSession({ - sessionId, - userId: user.id, - createdAt: new Date(), - }) + const session = await getSession(req) - res.status(200).json({ sessionId }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) + // Check if the user is authenticated + if (!session) { + res.status(401).json({ + error: 'User is not authenticated', + }) + return } -} -``` -```js filename="pages/api/create-session.js" switcher -import db from '../../lib/db' - -export default async function handler(req, res) { - try { - const user = req.body - const sessionId = generateSessionId() - await db.insertSession({ - sessionId, - userId: user.id, - createdAt: new Date(), + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + res.status(401).json({ + error: 'Unauthorized access: User does not have admin privileges.', }) - - res.status(200).json({ sessionId }) - } catch (error) { - res.status(500).json({ error: 'Internal Server Error' }) + return } -} -``` - - - - - -```js -import db from './lib/db' -export async function createSession(user) { - const sessionId = generateSessionId() // Generate a unique session ID - await db.insertSession({ sessionId, userId: user.id, createdAt: new Date() }) - return sessionId + // Proceed with the route for authorized users + // ... implementation of the API Route } ``` -**Retrieving a Session in Middleware or Server-Side Logic**: +```js filename="pages/api/route.js" switcher +export default async function handler(req, res) { + const session = await getSession(req) -```js -import { cookies } from 'next/headers' -import db from './lib/db' + // Check if the user is authenticated + if (!session) { + res.status(401).json({ + error: 'User is not authenticated', + }) + return + } + + // Check if the user has the 'admin' role + if (session.user.role !== 'admin') { + res.status(401).json({ + error: 'Unauthorized access: User does not have admin privileges.', + }) + return + } -export async function getSession() { - const sessionId = cookies().get('sessionId')?.value - return sessionId ? await db.findSession(sessionId) : null + // Proceed with the route for authorized users + // ... implementation of the API Route } ``` - - -### Selecting Session Management in Next.js - -Deciding between cookie-based and database sessions in Next.js depends on your application's needs. Cookie-based sessions are simpler and suit smaller applications with lower server load but may offer less security. Database sessions, while more complex, provide better security and scalability, ideal for larger, data-sensitive applications. - -With [authentication solutions](#examples) such as [NextAuth.js](https://authjs.dev/guides/upgrade-to-v5), session management becomes more efficient, using either cookies or database storage. This automation simplifies the development process, but it's important to understand the session management method used by your chosen solution. Ensure it aligns with your application's security and performance requirements. +This example demonstrates an API Route with a two-tier security check for authentication and authorization. It first checks for an active session, and then verifies if the logged-in user is an 'admin'. This approach ensures secure access, limited to authenticated and authorized users, maintaining robust security for request processing. -Regardless of your choice, prioritize security in your session management strategy. For cookie-based sessions, using secure and HTTP-only cookies is crucial to protect session data. For database sessions, regular backups and secure handling of session data are essential. Implementing session expiry and cleanup mechanisms is vital in both approaches to prevent unauthorized access and maintain application performance and reliability. + -## Examples +## Resources -Here are authentication solutions compatible with Next.js, please refer to the quickstart guides below to learn how to configure them in your Next.js application: +Now that you've learned about authentication in Next.js, here are Next.js-compatible libraries and resources to help you implement secure authentication and session management: -{/* TODO: Change link to authjs.dev when new documentation is ready */} +### Auth Libraries - [Auth0](https://auth0.com/docs/quickstart/webapp/nextjs/01-login) - [Clerk](https://clerk.com/docs/quickstarts/nextjs) - [Kinde](https://kinde.com/docs/developer-tools/nextjs-sdk) - [Lucia](https://lucia-auth.com/getting-started/nextjs-app) -- [NextAuth.js](https://authjs.dev/guides/upgrade-to-v5) +- [NextAuth.js](https://authjs.dev/getting-started/installation?framework=next.js) - [Supabase](https://supabase.com/docs/guides/getting-started/quickstarts/nextjs) - [Stytch](https://stytch.com/docs/guides/quickstarts/nextjs) + +### Session Management Libraries + - [Iron Session](https://github.com/vvo/iron-session) +- [Jose](https://github.com/panva/jose) ## Further Reading To continue learning about authentication and security, check out the following resources: +- [How to think about security in Next.js](/blog/security-nextjs-server-components-actions) - [Understanding XSS Attacks](https://vercel.com/guides/understanding-xss-attacks) - [Understanding CSRF Attacks](https://vercel.com/guides/understanding-csrf-attacks) +- [The Copenhagen Book](https://thecopenhagenbook.com/) diff --git a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/app-icons.mdx b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/app-icons.mdx index b55321180cc8ec..c3137be5bd895f 100644 --- a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/app-icons.mdx +++ b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/app-icons.mdx @@ -83,9 +83,6 @@ The easiest way to generate an icon is to use the [`ImageResponse`](/docs/app/ap ```tsx filename="app/icon.tsx" switcher import { ImageResponse } from 'next/og' -// Route segment config -export const runtime = 'edge' - // Image metadata export const size = { width: 32, @@ -126,9 +123,6 @@ export default function Icon() { ```jsx filename="app/icon.js" switcher import { ImageResponse } from 'next/og' -// Route segment config -export const runtime = 'edge' - // Image metadata export const size = { width: 32, @@ -258,25 +252,6 @@ export default function Icon() {} `icon` and `apple-icon` are specialized [Route Handlers](/docs/app/building-your-application/routing/route-handlers) that can use the same [route segment configuration](/docs/app/api-reference/file-conventions/route-segment-config) options as Pages and Layouts. -| Option | Type | Default | -| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------- | -| [`dynamic`](/docs/app/api-reference/file-conventions/route-segment-config#dynamic) | `'auto' \| 'force-dynamic' \| 'error' \| 'force-static'` | `'auto'` | -| [`revalidate`](/docs/app/api-reference/file-conventions/route-segment-config#revalidate) | `false \| 'force-cache' \| 0 \| number` | `false` | -| [`runtime`](/docs/app/api-reference/file-conventions/route-segment-config#runtime) | `'nodejs' \| 'edge'` | `'nodejs'` | -| [`preferredRegion`](/docs/app/api-reference/file-conventions/route-segment-config#preferredregion) | `'auto' \| 'global' \| 'home' \| string \| string[]` | `'auto'` | - -```tsx filename="app/icon.tsx" switcher -export const runtime = 'edge' - -export default function Icon() {} -``` - -```jsx filename="app/icon.js" switcher -export const runtime = 'edge' - -export default function Icon() {} -``` - ## Version History | Version | Changes | diff --git a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/opengraph-image.mdx b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/opengraph-image.mdx index fb6a4394543335..434683518e6b1d 100644 --- a/docs/02-app/02-api-reference/02-file-conventions/01-metadata/opengraph-image.mdx +++ b/docs/02-app/02-api-reference/02-file-conventions/01-metadata/opengraph-image.mdx @@ -92,9 +92,6 @@ The easiest way to generate an image is to use the [ImageResponse](/docs/app/api ```tsx filename="app/about/opengraph-image.tsx" switcher import { ImageResponse } from 'next/og' -// Route segment config -export const runtime = 'edge' - // Image metadata export const alt = 'About Acme' export const size = { @@ -149,9 +146,6 @@ export default async function Image() { ```jsx filename="app/about/opengraph-image.js" switcher import { ImageResponse } from 'next/og' -// Route segment config -export const runtime = 'edge' - // Image metadata export const alt = 'About Acme' export const size = { @@ -313,25 +307,6 @@ export default function Image() {} `opengraph-image` and `twitter-image` are specialized [Route Handlers](/docs/app/building-your-application/routing/route-handlers) that can use the same [route segment configuration](/docs/app/api-reference/file-conventions/route-segment-config) options as Pages and Layouts. -| Option | Type | Default | -| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------- | -| [`dynamic`](/docs/app/api-reference/file-conventions/route-segment-config#dynamic) | `'auto' \| 'force-dynamic' \| 'error' \| 'force-static'` | `'auto'` | -| [`revalidate`](/docs/app/api-reference/file-conventions/route-segment-config#revalidate) | `false \| 'force-cache' \| 0 \| number` | `false` | -| [`runtime`](/docs/app/api-reference/file-conventions/route-segment-config#runtime) | `'nodejs' \| 'edge'` | `'nodejs'` | -| [`preferredRegion`](/docs/app/api-reference/file-conventions/route-segment-config#preferredregion) | `'auto' \| 'global' \| 'home' \| string \| string[]` | `'auto'` | - -```tsx filename="app/opengraph-image.tsx" switcher -export const runtime = 'edge' - -export default function Image() {} -``` - -```jsx filename="app/opengraph-image.js" switcher -export const runtime = 'edge' - -export default function Image() {} -``` - ### Examples #### Using external data @@ -344,8 +319,6 @@ This example uses the `params` object and external data to generate the image. ```tsx filename="app/posts/[slug]/opengraph-image.tsx" switcher import { ImageResponse } from 'next/og' -export const runtime = 'edge' - export const alt = 'About Acme' export const size = { width: 1200, @@ -384,8 +357,6 @@ export default async function Image({ params }: { params: { slug: string } }) { ```jsx filename="app/posts/[slug]/opengraph-image.js" switcher import { ImageResponse } from 'next/og' -export const runtime = 'edge' - export const alt = 'About Acme' export const size = { width: 1200, @@ -421,6 +392,65 @@ export default async function Image({ params }) { } ``` +#### Using Edge runtime with local assets + +This example uses the Edge runtime to fetch a local image on the file system and passes it as an `ArrayBuffer` to the `src` attribute of an `` element. + +```jsx filename="app/opengraph-image.js" switcher +import { ImageResponse } from 'next/og' +import { readFile } from 'node:fs/promises' + +export const runtime = 'edge' + +export async function GET() { + const logoSrc = await fetch(new URL('./logo.png', import.meta.url)).then( + (res) => res.arrayBuffer() + ) + + return new ImageResponse( + ( +
+ +
+ ) + ) +} +``` + +#### Using Node.js runtime with local assets + +This example uses the Node.js runtime to fetch a local image on the file system and passes it as an `ArrayBuffer` to the `src` attribute of an `` element. + +```jsx filename="app/opengraph-image.js" switcher +import { ImageResponse } from 'next/og' +import { readFile } from 'node:fs/promises' + +export async function GET() { + const logoData = await readFile('./logo.png') + const logoSrc = Uint8Array.from(logoData).buffer + + return new ImageResponse( + ( +
+ +
+ ) + ) +} +``` + ## Version History | Version | Changes | diff --git a/docs/02-app/02-api-reference/02-file-conventions/default.mdx b/docs/02-app/02-api-reference/02-file-conventions/default.mdx index e80b06713afe25..b1275af15d8178 100644 --- a/docs/02-app/02-api-reference/02-file-conventions/default.mdx +++ b/docs/02-app/02-api-reference/02-file-conventions/default.mdx @@ -35,5 +35,5 @@ An object containing the [dynamic route parameters](/docs/app/building-your-appl | Example | URL | `params` | | ------------------------------------------ | ------------ | ----------------------------------- | -| `app/@sidebar/[artist]/default.js` | `/zack` | `{ artist: 'zack' }` | -| `app/@sidebar/[artist]/[album]/default.js` | `/zack/next` | `{ artist: 'zack', album: 'next' }` | +| `app/[artist]/@sidebar/default.js` | `/zack` | `{ artist: 'zack' }` | +| `app/[artist]/[album]/@sidebar/default.js` | `/zack/next` | `{ artist: 'zack', album: 'next' }` | diff --git a/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx b/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx index 048ed09ec17987..42bbade1e1d786 100644 --- a/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx +++ b/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx @@ -15,34 +15,6 @@ The Route Segment options allows you to configure the behavior of a [Page](/docs | [`preferredRegion`](#preferredregion) | `'auto' \| 'global' \| 'home' \| string \| string[]` | `'auto'` | | [`maxDuration`](#maxduration) | `number` | Set by deployment platform | -```tsx filename="layout.tsx | page.tsx | route.ts" switcher -export const dynamic = 'auto' -export const dynamicParams = true -export const revalidate = false -export const fetchCache = 'auto' -export const runtime = 'nodejs' -export const preferredRegion = 'auto' -export const maxDuration = 5 - -export default function MyComponent() {} -``` - -```jsx filename="layout.js | page.js | route.js" switcher -export const dynamic = 'auto' -export const dynamicParams = true -export const revalidate = false -export const fetchCache = 'auto' -export const runtime = 'nodejs' -export const preferredRegion = 'auto' -export const maxDuration = 5 - -export default function MyComponent() {} -``` - -> **Good to know**: -> -> - The values of the config options currently need be statically analyzable. For example `revalidate = 600` is valid, but `revalidate = 60 * 10` is not. - ## Options ### `dynamic` @@ -118,7 +90,10 @@ export const revalidate = false - **`0`**: Ensure a layout or page is always [dynamically rendered](/docs/app/building-your-application/rendering/server-components#dynamic-rendering) even if no dynamic functions or uncached data fetches are discovered. This option changes the default of `fetch` requests that do not set a `cache` option to `'no-store'` but leaves `fetch` requests that opt into `'force-cache'` or use a positive `revalidate` as is. - **`number`**: (in seconds) Set the default revalidation frequency of a layout or page to `n` seconds. -> **Good to know**: The `revalidate` option is only available when using the [Node.js Runtime](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#nodejs-runtime). This means using the `revalidate` option with `runtime = 'edge'` will not work. +> **Good to know**: +> +> - The revalidate value needs to be statically analyzable. For example `revalidate = 600` is valid, but `revalidate = 60 * 10` is not. +> - The revalidate value is not available when using `runtime = 'edge'`. #### Revalidation Frequency @@ -168,6 +143,8 @@ export const fetchCache = 'auto' ### `runtime` +We recommend using the Node.js runtime for rendering your application, and the Edge runtime for Middleware (only supported option). + ```tsx filename="layout.tsx | page.tsx | route.ts" switcher export const runtime = 'nodejs' // 'nodejs' | 'edge' @@ -181,7 +158,7 @@ export const runtime = 'nodejs' - **`'nodejs'`** (default) - **`'edge'`** -Learn more about the [Edge and Node.js runtimes](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes). +Learn more about the [different runtimes](/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes). ### `preferredRegion` diff --git a/docs/02-app/02-api-reference/04-functions/use-report-web-vitals.mdx b/docs/02-app/02-api-reference/04-functions/use-report-web-vitals.mdx index 74103b6be4c14d..2ed2d08f2bee6e 100644 --- a/docs/02-app/02-api-reference/04-functions/use-report-web-vitals.mdx +++ b/docs/02-app/02-api-reference/04-functions/use-report-web-vitals.mdx @@ -194,7 +194,8 @@ These metrics work in all browsers that support the [User Timing API](https://ca ## Usage on Vercel -[Vercel Speed Insights](https://vercel.com/docs/concepts/speed-insights) are automatically configured on Vercel deployments, and don't require the use of `useReportWebVitals`. This hook is useful in local development, or if you're using a different analytics service. +[Vercel Speed Insights](https://vercel.com/docs/speed-insights/quickstart) does not `useReportWebVitals`, but `@vercel/speed-insights` package instead. +`useReportWebVitals` hook is useful in local development, or if you're using a different service for collecting Web Vitals. ## Sending results to external systems diff --git a/docs/02-app/02-api-reference/05-next-config-js/images.mdx b/docs/02-app/02-api-reference/05-next-config-js/images.mdx index 8c296d7c4d71f1..b0f14494ca4aec 100644 --- a/docs/02-app/02-api-reference/05-next-config-js/images.mdx +++ b/docs/02-app/02-api-reference/05-next-config-js/images.mdx @@ -61,10 +61,11 @@ To learn more about configuring the behavior of the built-in [Image Optimization - [Gumlet](#gumlet) - [ImageEngine](#imageengine) - [Imgix](#imgix) -- [Thumbor](#thumbor) +- [PixelBin](#pixelbin) - [Sanity](#sanity) - [Sirv](#sirv) - [Supabase](#supabase) +- [Thumbor](#thumbor) ### Akamai @@ -172,13 +173,16 @@ export default function imgixLoader({ src, width, quality }) { } ``` -### Thumbor +### PixelBin ```js -// Docs: https://thumbor.readthedocs.io/en/latest/ -export default function thumborLoader({ src, width, quality }) { - const params = [`${width}x0`, `filters:quality(${quality || 75})`] - return `https://example.com${params.join('/')}${src}` +// Doc (Resize): https://www.pixelbin.io/docs/transformations/basic/resize/#width-w +// Doc (Optimise): https://www.pixelbin.io/docs/optimizations/quality/#image-quality-when-delivering +// Doc (Auto Format Delivery): https://www.pixelbin.io/docs/optimizations/format/#automatic-format-selection-with-f_auto-url-parameter +export default function pixelBinLoader({ src, width, quality }) { + const name = '' + const opt = `t.resize(w:${width})~t.compress(q:${quality || 75})` + return `https://cdn.pixelbin.io/v2/${name}/${opt}/${src}?f_auto=true` } ``` @@ -225,3 +229,13 @@ export default function supabaseLoader({ src, width, quality }) { return url.href } ``` + +### Thumbor + +```js +// Docs: https://thumbor.readthedocs.io/en/latest/ +export default function thumborLoader({ src, width, quality }) { + const params = [`${width}x0`, `filters:quality(${quality || 75})`] + return `https://example.com${params.join('/')}${src}` +} +``` diff --git a/docs/02-app/02-api-reference/05-next-config-js/serverComponentsExternalPackages.mdx b/docs/02-app/02-api-reference/05-next-config-js/serverComponentsExternalPackages.mdx index da52a4dbe55427..548a0673283d80 100644 --- a/docs/02-app/02-api-reference/05-next-config-js/serverComponentsExternalPackages.mdx +++ b/docs/02-app/02-api-reference/05-next-config-js/serverComponentsExternalPackages.mdx @@ -20,6 +20,7 @@ module.exports = nextConfig Next.js includes a [short list of popular packages](https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/server-external-packages.json) that currently are working on compatibility and automatically opt-ed out: +- `@appsignal/nodejs` - `@aws-sdk/client-s3` - `@aws-sdk/s3-presigned-post` - `@blockfrost/blockfrost-js` diff --git a/docs/02-app/02-api-reference/07-edge.mdx b/docs/02-app/02-api-reference/07-edge.mdx index 3bed7bfb2bbb0e..c4a7f0fc27fcb1 100644 --- a/docs/02-app/02-api-reference/07-edge.mdx +++ b/docs/02-app/02-api-reference/07-edge.mdx @@ -5,7 +5,7 @@ description: API Reference for the Edge Runtime. {/* The content of this doc is shared between the app and pages router. You can use the `Content` component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */} -The Next.js Edge Runtime is based on standard Web APIs, it supports the following APIs: +The Next.js Edge Runtime is used for Middleware and supports the following APIs: ## Network APIs @@ -146,11 +146,10 @@ The following JavaScript language features are disabled, and **will not work:** | [`WebAssembly.instantiate`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate) | Compiles and instantiates a WebAssembly module from a buffer source | In rare cases, your code could contain (or import) some dynamic code evaluation statements which _can not be reached at runtime_ and which can not be removed by treeshaking. -You can relax the check to allow specific files with your Middleware or Edge API Route exported configuration: +You can relax the check to allow specific files with your Middleware configuration: -```javascript +```javascript filename="middleware.ts" export const config = { - runtime: 'edge', // for Edge API Routes only unstable_allowDynamic: [ // allows a single file '/lib/utilities.js', diff --git a/errors/edge-dynamic-code-evaluation.mdx b/errors/edge-dynamic-code-evaluation.mdx index bb842f4181ddd4..68d60f62a3752c 100644 --- a/errors/edge-dynamic-code-evaluation.mdx +++ b/errors/edge-dynamic-code-evaluation.mdx @@ -1,10 +1,10 @@ --- -title: Dynamic code evaluation is not available in Middlewares or Edge API Routes +title: Dynamic code evaluation is not available in Middleware --- ## Why This Error Occurred -`eval()`, `new Function()` or compiling WASM binaries dynamically is not allowed in Middlewares or Edge API Routes. +`eval()`, `new Function()` or compiling WASM binaries dynamically is not allowed in Middleware. Specifically, the following APIs are not supported: - `eval()` @@ -31,11 +31,10 @@ export default async function middleware() { ``` In rare cases, your code could contain (or import) some dynamic code evaluation statements which _can not be reached at runtime_ and which can not be removed by tree-shaking. -You can relax the check to allow specific files with your Middleware or Edge API Route exported [configuration](/docs/pages/api-reference/edge#unsupported-apis): +You can relax the check to allow specific files with your Middleware [configuration](/docs/pages/api-reference/edge#unsupported-apis): ```tsx filename="pages/api/example.ts" export const config = { - runtime: 'edge', // for Edge API Routes only unstable_allowDynamic: [ '/lib/utilities.js', // allows a single file '/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 4780a0e0c653e3..124cea4abc57bd 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -16,23 +16,23 @@ "@sanity/assist": "3.0.3", "@sanity/icons": "2.11.8", "@sanity/image-url": "1.0.2", - "@sanity/preview-url-secret": "1.6.7", - "@sanity/vision": "3.37.2", + "@sanity/preview-url-secret": "1.6.8", + "@sanity/vision": "3.38.0", "@tailwindcss/typography": "^0.5.12", "@types/node": "^20.12.7", - "@types/react": "^18.2.75", - "@types/react-dom": "^18.2.24", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", "@vercel/speed-insights": "^1.0.10", "autoprefixer": "^10.4.19", "date-fns": "^3.6.0", "next": "latest", - "next-sanity": "9.0.0", + "next-sanity": "9.0.3", "postcss": "^8.4.38", "react": "^18.2.0", "react-dom": "^18.2.0", "rxjs": "^7.8.1", - "sanity": "3.37.2", - "sanity-plugin-asset-source-unsplash": "3.0.0", + "sanity": "3.38.0", + "sanity-plugin-asset-source-unsplash": "3.0.1", "server-only": "^0.0.1", "styled-components": "6.1.8", "tailwindcss": "^3.4.3", diff --git a/examples/cms-sanity/tsconfig.json b/examples/cms-sanity/tsconfig.json index ad4848de01650a..2d9e949e10320c 100644 --- a/examples/cms-sanity/tsconfig.json +++ b/examples/cms-sanity/tsconfig.json @@ -7,11 +7,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, - // Use module: preserve, and remove moduleResolution, esModuleInterop and resolveJsonModule once Next ships support - "module": "esnext", - "moduleResolution": "bundler", - "esModuleInterop": true, - "resolveJsonModule": true, + "module": "preserve", "isolatedModules": true, "jsx": "preserve", "incremental": true, diff --git a/examples/with-service-worker/README.md b/examples/with-service-worker/README.md index 420b6ffa20493c..1080c6ea6c7716 100644 --- a/examples/with-service-worker/README.md +++ b/examples/with-service-worker/README.md @@ -1,6 +1,6 @@ # Service Worker Example -This example shows how to add a simple service worker to a Next.js application. The service worker is in [`public/sw.js`](public/sw.js) and it's installed by [`pages/_app.js`](pages/_app.js) after the first render. +This example shows how to add a simple service worker to a Next.js application. The service worker is in [`public/sw.js`](public/sw.js) and it's installed by [`pages/index.tsx`](pages/index.tsx) after the first render. The example is based on the following blog post: [Adding a service worker into your Next.js application](https://dev.to/josedonato/adding-a-service-worker-into-your-next-js-application-1dib). diff --git a/lerna.json b/lerna.json index 57e533ec2737b3..d22cfb7e4984c1 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "14.2.1-canary.5" + "version": "14.3.0-canary.11" } diff --git a/package.json b/package.json index 2cfe2af78f688c..4c3bf41f28c499 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "@swc/helpers": "0.5.5", "@testing-library/jest-dom": "6.1.2", "@testing-library/react": "13.0.0", + "@types/busboy": "1.5.3", "@types/cheerio": "0.22.16", "@types/cookie": "0.3.3", "@types/cross-spawn": "6.0.0", @@ -103,10 +104,10 @@ "@types/html-validator": "5.0.3", "@types/http-proxy": "1.17.3", "@types/jest": "29.5.5", - "@types/node": "20.2.5", + "@types/node": "20.12.3", "@types/node-fetch": "2.6.1", - "@types/react": "18.2.37", - "@types/react-dom": "18.2.15", + "@types/react": "18.2.74", + "@types/react-dom": "18.2.23", "@types/relay-runtime": "14.1.13", "@types/selenium-webdriver": "4.0.15", "@types/sharp": "0.29.3", @@ -233,7 +234,7 @@ "tree-kill": "1.2.2", "tsec": "0.2.1", "turbo": "1.12.5", - "typescript": "5.2.2", + "typescript": "5.3.3", "unfetch": "4.2.0", "wait-port": "0.2.2", "webpack": "5.90.0", @@ -246,17 +247,17 @@ "webpack": "5.90.0", "browserslist": "4.22.2", "caniuse-lite": "1.0.30001579", - "@types/node": "20.2.5", + "@types/node": "20.12.3", "@babel/core": "7.22.5", "@babel/parser": "7.22.5", "@babel/types": "7.22.5", "@babel/traverse": "7.22.5", - "@types/react": "18.2.37", - "@types/react-dom": "18.2.15" + "@types/react": "18.2.74", + "@types/react-dom": "18.2.23" }, "engines": { "node": ">=18.17.0", - "pnpm": "8.15.4" + "pnpm": "8.15.7" }, - "packageManager": "pnpm@8.15.4" + "packageManager": "pnpm@8.15.7" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 81348dcff979f2..b5a333b257f887 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "keywords": [ "react", "next", @@ -32,7 +32,7 @@ "@types/async-retry": "1.4.2", "@types/ci-info": "2.0.0", "@types/cross-spawn": "6.0.0", - "@types/node": "^20.2.5", + "@types/node": "^20.12.3", "@types/prompts": "2.4.2", "@types/tar": "6.1.5", "@types/validate-npm-package-name": "3.0.0", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 5579c022b4d0ba..552f430285abdf 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "description": "ESLint configuration used by Next.js.", "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": "14.2.1-canary.5", + "@next/eslint-plugin-next": "14.3.0-canary.11", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.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 6446adbe0c79bd..685d0a2f885fa9 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": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index c5d63913ec65ff..dd2c56ce5bb585 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/font/src/google/font-data.json b/packages/font/src/google/font-data.json index 06566c6989c378..30e3508ce0657f 100644 --- a/packages/font/src/google/font-data.json +++ b/packages/font/src/google/font-data.json @@ -4009,8 +4009,16 @@ "subsets": ["latin", "malayalam"] }, "Gelasio": { - "weights": ["400", "500", "600", "700"], + "weights": ["400", "500", "600", "700", "variable"], "styles": ["normal", "italic"], + "axes": [ + { + "tag": "wght", + "min": 400, + "max": 700, + "defaultValue": 400 + } + ], "subsets": ["latin", "latin-ext", "vietnamese"] }, "Gemunu Libre": { @@ -5142,11 +5150,26 @@ "styles": ["normal"], "subsets": ["latin", "latin-ext", "thai", "vietnamese"] }, + "Jacquard 12 Charted": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext", "math", "symbols"] + }, + "Jacquard 24": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, "Jacquarda Bastarda 9": { "weights": ["400"], "styles": ["normal"], "subsets": ["latin", "latin-ext", "math", "symbols"] }, + "Jacquarda Bastarda 9 Charted": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext", "math", "symbols"] + }, "Jacques Francois": { "weights": ["400"], "styles": ["normal"], @@ -5162,6 +5185,41 @@ "styles": ["normal"], "subsets": ["devanagari", "latin", "latin-ext"] }, + "Jersey 10": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, + "Jersey 10 Charted": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, + "Jersey 15": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, + "Jersey 15 Charted": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, + "Jersey 20": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, + "Jersey 20 Charted": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, + "Jersey 25": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext"] + }, "JetBrains Mono": { "weights": [ "100", @@ -6828,6 +6886,11 @@ "styles": ["normal"], "subsets": ["latin", "latin-ext", "math", "symbols"] }, + "Micro 5 Charted": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext", "math", "symbols"] + }, "Milonga": { "weights": ["400"], "styles": ["normal"], @@ -7417,7 +7480,7 @@ "defaultValue": 400 } ], - "subsets": ["arabic", "latin", "latin-ext"] + "subsets": ["arabic", "latin", "latin-ext", "math", "symbols"] }, "Noto Nastaliq Urdu": { "weights": ["400", "500", "600", "700", "variable"], @@ -10708,6 +10771,19 @@ "styles": ["normal"], "subsets": ["latin", "latin-ext"] }, + "Platypi": { + "weights": ["300", "400", "500", "600", "700", "800", "variable"], + "styles": ["normal", "italic"], + "axes": [ + { + "tag": "wght", + "min": 300, + "max": 800, + "defaultValue": 400 + } + ], + "subsets": ["latin", "latin-ext", "vietnamese"] + }, "Play": { "weights": ["400", "700"], "styles": ["normal"], @@ -12260,6 +12336,11 @@ "styles": ["normal"], "subsets": ["hebrew", "latin", "latin-ext"] }, + "Sedan": { + "weights": ["400"], + "styles": ["normal", "italic"], + "subsets": ["latin", "latin-ext"] + }, "Sedgwick Ave": { "weights": ["400"], "styles": ["normal"], diff --git a/packages/font/src/google/index.ts b/packages/font/src/google/index.ts index ffe39bf9880cdd..e710b6c4d2e4b1 100644 --- a/packages/font/src/google/index.ts +++ b/packages/font/src/google/index.ts @@ -7134,8 +7134,14 @@ export declare function Gayathri< }): T extends undefined ? NextFont : NextFontWithVariable export declare function Gelasio< T extends CssVariable | undefined = undefined ->(options: { - weight: '400' | '500' | '600' | '700' | Array<'400' | '500' | '600' | '700'> +>(options?: { + weight?: + | '400' + | '500' + | '600' + | '700' + | 'variable' + | Array<'400' | '500' | '600' | '700'> style?: 'normal' | 'italic' | Array<'normal' | 'italic'> display?: Display variable?: T @@ -9167,6 +9173,30 @@ export declare function Itim< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext' | 'thai' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jacquard_12_Charted< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext' | 'math' | 'symbols'> +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jacquard_24< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Jacquarda_Bastarda_9< T extends CssVariable | undefined = undefined >(options: { @@ -9179,6 +9209,18 @@ export declare function Jacquarda_Bastarda_9< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext' | 'math' | 'symbols'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jacquarda_Bastarda_9_Charted< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext' | 'math' | 'symbols'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Jacques_Francois< T extends CssVariable | undefined = undefined >(options: { @@ -9215,6 +9257,90 @@ export declare function Jaldi< adjustFontFallback?: boolean subsets?: Array<'devanagari' | 'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jersey_10< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jersey_10_Charted< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jersey_15< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jersey_15_Charted< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jersey_20< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jersey_20_Charted< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable +export declare function Jersey_25< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function JetBrains_Mono< T extends CssVariable | undefined = undefined >(options?: { @@ -12262,6 +12388,18 @@ export declare function Micro_5< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext' | 'math' | 'symbols'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Micro_5_Charted< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext' | 'math' | 'symbols'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Milonga< T extends CssVariable | undefined = undefined >(options: { @@ -13488,7 +13626,7 @@ export declare function Noto_Naskh_Arabic< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'arabic' | 'latin' | 'latin-ext'> + subsets?: Array<'arabic' | 'latin' | 'latin-ext' | 'math' | 'symbols'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Nastaliq_Urdu< T extends CssVariable | undefined = undefined @@ -18207,6 +18345,26 @@ export declare function Plaster< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Platypi< + T extends CssVariable | undefined = undefined +>(options?: { + weight?: + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | 'variable' + | Array<'300' | '400' | '500' | '600' | '700' | '800'> + style?: 'normal' | 'italic' | Array<'normal' | 'italic'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext' | 'vietnamese'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Play< T extends CssVariable | undefined = undefined >(options: { @@ -20902,6 +21060,18 @@ export declare function Secular_One< adjustFontFallback?: boolean subsets?: Array<'hebrew' | 'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Sedan< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | 'italic' | Array<'normal' | 'italic'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Sedgwick_Ave< T extends CssVariable | undefined = undefined >(options: { diff --git a/packages/font/tsconfig.json b/packages/font/tsconfig.json index 7834550630a184..e5df5eeddf320c 100644 --- a/packages/font/tsconfig.json +++ b/packages/font/tsconfig.json @@ -9,5 +9,5 @@ "rootDir": "src" }, "include": ["src/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "src/**/*.test.ts"] } diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index b99c69bd5ec78f..d341b3b542c123 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 724dd073291457..5952fda7a8baac 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 8141efb798262e..830c7659bec22d 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 064d91cd8426b5..c2624f656b38e7 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index bb49e449e20d5e..e59443785bcdce 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "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 0866b59abfeeed..13cbab59a42edf 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "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 dcf1f263f01b89..4bd8baf958b84d 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "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 beb03af47b6d5b..2f6e522a230c44 100644 --- a/packages/next-swc/crates/napi/Cargo.toml +++ b/packages/next-swc/crates/napi/Cargo.toml @@ -43,10 +43,16 @@ __internal_dhat-ad-hoc = ["dhat"] turbopack-binding = { workspace = true, features = ["__turbo_tasks_malloc"] } [target.'cfg(all(target_os = "linux", not(any(target_arch = "aarch64", target_arch = "wasm32"))))'.dependencies] -turbopack-binding = { workspace = true, features = ["__turbo_tasks_malloc", "__turbo_tasks_malloc_custom_allocator"] } +turbopack-binding = { workspace = true, features = [ + "__turbo_tasks_malloc", + "__turbo_tasks_malloc_custom_allocator", +] } [target.'cfg(not(any(target_os = "linux", target_arch = "wasm32")))'.dependencies] -turbopack-binding = { workspace = true, features = ["__turbo_tasks_malloc", "__turbo_tasks_malloc_custom_allocator"] } +turbopack-binding = { workspace = true, features = [ + "__turbo_tasks_malloc", + "__turbo_tasks_malloc_custom_allocator", +] } # Enable specific tls features per-target. [target.'cfg(all(target_os = "windows", target_arch = "aarch64"))'.dependencies] @@ -75,15 +81,14 @@ napi = { version = "2", default-features = false, features = [ ] } napi-derive = "2" next-custom-transforms = { workspace = true } - serde = "1" serde_json = "1" shadow-rs = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-chrome = "0.5.0" -url = {workspace = true} -urlencoding = {workspace = true} +url = { workspace = true } +urlencoding = { workspace = true } # Dependencies for the native, non-wasm32 build. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/packages/next-swc/crates/napi/build.rs b/packages/next-swc/crates/napi/build.rs index 46e896b33fd9e6..3e94346bda3c84 100644 --- a/packages/next-swc/crates/napi/build.rs +++ b/packages/next-swc/crates/napi/build.rs @@ -6,8 +6,20 @@ fn main() { // So failing build if this fails. shadow_rs::new().expect("Should able to generate build time information"); + #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] napi_build::setup(); + // This is a workaround for napi always including a GCC specific flag. + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + println!("cargo:rerun-if-env-changed=DEBUG_GENERATED_CODE"); + println!("cargo:rerun-if-env-changed=TYPE_DEF_TMP_PATH"); + println!("cargo:rerun-if-env-changed=CARGO_CFG_NAPI_RS_CLI_VERSION"); + + println!("cargo:rustc-cdylib-link-arg=-undefined"); + println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); + } + // Resolve a potential linker issue for unit tests on linux // https://github.com/napi-rs/napi-rs/issues/1782 #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))] diff --git a/packages/next-swc/crates/next-api/Cargo.toml b/packages/next-swc/crates/next-api/Cargo.toml index ef362476f0da45..f9b9f7a2caf984 100644 --- a/packages/next-swc/crates/next-api/Cargo.toml +++ b/packages/next-swc/crates/next-api/Cargo.toml @@ -16,13 +16,10 @@ workspace = true anyhow = { workspace = true, features = ["backtrace"] } futures = { workspace = true } indexmap = { workspace = true } -indoc = { workspace = true } next-core = { workspace = true } -once_cell = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } shadow-rs = { workspace = true } -tokio = { workspace = true } turbopack-binding = { workspace = true, features = [ "__turbo_tasks_memory", "__turbo_tasks_env", @@ -37,7 +34,6 @@ turbopack-binding = { workspace = true, features = [ ] } turbo-tasks = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } [build-dependencies] # It is not a mistake this dependency is specified in dep / build-dep both. diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 5c8d73a56b5117..1fda6368d3aed5 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -103,6 +103,7 @@ impl AppProject { fn route_ty(self: Vc) -> ServerContextType { ServerContextType::AppRoute { app_dir: self.app_dir(), + ecmascript_client_reference_transition_name: Some(self.client_transition_name()), } } @@ -282,6 +283,10 @@ impl AppProject { ))), ), ("next-ssr".to_string(), Vc::upcast(self.ssr_transition())), + ( + "next-shared".to_string(), + Vc::upcast(self.shared_transition()), + ), ] .into_iter() .collect(); @@ -314,6 +319,10 @@ impl AppProject { "next-ssr".to_string(), Vc::upcast(self.edge_ssr_transition()), ), + ( + "next-shared".to_string(), + Vc::upcast(self.edge_shared_transition()), + ), ] .into_iter() .collect(); @@ -328,8 +337,31 @@ impl AppProject { #[turbo_tasks::function] fn route_module_context(self: Vc) -> Vc { + let transitions = [ + ( + ECMASCRIPT_CLIENT_TRANSITION_NAME.to_string(), + Vc::upcast(NextEcmascriptClientReferenceTransition::new( + Vc::upcast(self.client_transition()), + self.ssr_transition(), + )), + ), + ( + "next-dynamic".to_string(), + Vc::upcast(NextDynamicTransition::new(Vc::upcast( + self.client_transition(), + ))), + ), + ("next-ssr".to_string(), Vc::upcast(self.ssr_transition())), + ( + "next-shared".to_string(), + Vc::upcast(self.shared_transition()), + ), + ] + .into_iter() + .collect(); + ModuleAssetContext::new( - Default::default(), + Vc::cell(transitions), self.project().server_compile_time_info(), self.route_module_options_context(), self.route_resolve_options_context(), @@ -339,8 +371,30 @@ impl AppProject { #[turbo_tasks::function] fn edge_route_module_context(self: Vc) -> Vc { + let transitions = [ + ( + ECMASCRIPT_CLIENT_TRANSITION_NAME.to_string(), + Vc::upcast(NextEcmascriptClientReferenceTransition::new( + Vc::upcast(self.client_transition()), + self.edge_ssr_transition(), + )), + ), + ( + "next-dynamic".to_string(), + Vc::upcast(NextDynamicTransition::new(Vc::upcast( + self.client_transition(), + ))), + ), + ("next-ssr".to_string(), Vc::upcast(self.ssr_transition())), + ( + "next-shared".to_string(), + Vc::upcast(self.edge_shared_transition()), + ), + ] + .into_iter() + .collect(); ModuleAssetContext::new( - Default::default(), + Vc::cell(transitions), self.project().edge_compile_time_info(), self.edge_route_module_options_context(), self.edge_route_resolve_options_context(), @@ -415,6 +469,16 @@ impl AppProject { ) } + #[turbo_tasks::function] + fn shared_transition(self: Vc) -> Vc { + ContextTransition::new( + self.project().server_compile_time_info(), + self.ssr_module_options_context(), + self.ssr_resolve_options_context(), + Vc::cell("app-shared".to_string()), + ) + } + #[turbo_tasks::function] fn edge_ssr_transition(self: Vc) -> Vc { ContextTransition::new( @@ -425,6 +489,16 @@ impl AppProject { ) } + #[turbo_tasks::function] + fn edge_shared_transition(self: Vc) -> Vc { + ContextTransition::new( + self.project().edge_compile_time_info(), + self.edge_ssr_module_options_context(), + self.edge_ssr_resolve_options_context(), + Vc::cell("app-edge-shared".to_string()), + ) + } + #[turbo_tasks::function] async fn runtime_entries(self: Vc) -> Result> { Ok(get_server_runtime_entries( diff --git a/packages/next-swc/crates/next-api/src/versioned_content_map.rs b/packages/next-swc/crates/next-api/src/versioned_content_map.rs index b566534a00715d..f2deaaf8e13d9a 100644 --- a/packages/next-swc/crates/next-api/src/versioned_content_map.rs +++ b/packages/next-swc/crates/next-api/src/versioned_content_map.rs @@ -28,23 +28,26 @@ pub struct OutputAssetsOperation(Vc); )] struct MapEntry { assets_operation: Vc, - emit_operation: Vc, + side_effects: Vc, } #[turbo_tasks::value(transparent)] struct OptionMapEntry(Option); -type VersionedContentMapInner = HashMap, MapEntry>; +type PathToOutputOperation = HashMap, Vc>; +type OutputOperationToSideEffects = HashMap, Vc>; #[turbo_tasks::value] pub struct VersionedContentMap { - map: State, + map_path_to_op: State, + map_op_to_side_effects: State, } impl ValueDefault for VersionedContentMap { fn value_default() -> Vc { VersionedContentMap { - map: State::new(HashMap::new()), + map_path_to_op: State::new(HashMap::new()), + map_op_to_side_effects: State::new(HashMap::new()), } .cell() } @@ -66,30 +69,45 @@ impl VersionedContentMap { assets_operation: Vc, client_relative_path: Vc, client_output_path: Vc, - ) -> Result<()> { - let assets_operation = *assets_operation.await?; - // Make sure all written client assets are up-to-date - let emit_operation = - emit_client_assets(assets_operation, client_relative_path, client_output_path); - let assets = assets_operation.await?; + ) -> Result> { + let this = self.await?; + let side_effects = + self.output_side_effects(assets_operation, client_relative_path, client_output_path); + let assets = *assets_operation.await?; + this.map_op_to_side_effects + .update_conditionally(|map| map.insert(assets, side_effects) != Some(side_effects)); + Ok(side_effects) + } + + #[turbo_tasks::function] + async fn output_side_effects( + self: Vc, + assets_operation: Vc, + client_relative_path: Vc, + client_output_path: Vc, + ) -> Result> { + let assets = *assets_operation.await?; let entries: Vec<_> = assets + .await? .iter() - .map(|&asset| async move { - Ok(( - asset.ident().path().resolve().await?, - MapEntry { - assets_operation, - emit_operation, - }, - )) - }) + .map(|&asset| async move { Ok((asset.ident().path().resolve().await?, assets)) }) .try_join() .await?; - self.await?.map.update_conditionally(move |map| { - map.extend(entries); - true + self.await?.map_path_to_op.update_conditionally(move |map| { + let mut changed = false; + for (k, v) in entries { + if map.insert(k, v) != Some(v) { + changed = true; + } + } + changed }); - Ok(()) + // Make sure all written client assets are up-to-date + Ok(emit_client_assets( + assets, + client_relative_path, + client_output_path, + )) } #[turbo_tasks::function] @@ -125,12 +143,14 @@ impl VersionedContentMap { let result = self.raw_get(path).await?; if let Some(MapEntry { assets_operation, - emit_operation, + side_effects, }) = *result { // NOTE(alexkirsz) This is necessary to mark the task as active again. Vc::connect(assets_operation); - Vc::connect(emit_operation); + Vc::connect(side_effects); + + side_effects.await?; for &asset in assets_operation.await?.iter() { if asset.ident().path().resolve().await? == path { @@ -145,7 +165,7 @@ impl VersionedContentMap { #[turbo_tasks::function] pub async fn keys_in_path(&self, root: Vc) -> Result>> { let keys = { - let map = self.map.get(); + let map = self.map_path_to_op.get(); map.keys().copied().collect::>() }; let root = &root.await?; @@ -159,10 +179,23 @@ impl VersionedContentMap { #[turbo_tasks::function] async fn raw_get(&self, path: Vc) -> Result> { - let result = { - let map = self.map.get(); + let assets = { + let map = self.map_path_to_op.get(); map.get(&path).copied() }; - Ok(Vc::cell(result)) + let Some(assets) = assets else { + return Ok(Vc::cell(None)); + }; + let side_effects = { + let map = self.map_op_to_side_effects.get(); + map.get(&assets).copied() + }; + let Some(side_effects) = side_effects else { + return Ok(Vc::cell(None)); + }; + Ok(Vc::cell(Some(MapEntry { + assets_operation: assets, + side_effects, + }))) } } diff --git a/packages/next-swc/crates/next-build/Cargo.toml b/packages/next-swc/crates/next-build/Cargo.toml index 2079887132035d..c1d46586a33aa5 100644 --- a/packages/next-swc/crates/next-build/Cargo.toml +++ b/packages/next-swc/crates/next-build/Cargo.toml @@ -9,15 +9,10 @@ autobenches = false [lib] bench = false -[features] -serializable = [] -profile = [] - [lints] workspace = true [dependencies] -console-subscriber = { workspace = true, optional = true } next-core = { workspace = true } turbopack-binding = { workspace = true, features = [ @@ -35,7 +30,6 @@ turbopack-binding = { workspace = true, features = [ "__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-core/src/next_config.rs b/packages/next-swc/crates/next-core/src/next_config.rs index a25775c32cd245..3a08c3a72f157d 100644 --- a/packages/next-swc/crates/next-core/src/next_config.rs +++ b/packages/next-swc/crates/next-core/src/next_config.rs @@ -83,7 +83,6 @@ pub struct NextConfig { pub cross_origin: Option, pub dev_indicators: Option, pub output: Option, - pub analytics_id: Option, #[serde(rename = "_originalRedirects")] pub original_redirects: Option>, diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index 76114d0e6b1862..1be71bf8e7cc8b 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -575,7 +575,7 @@ async fn insert_next_server_special_aliases( // the logic closely follows the one in createRSCAliases in webpack-config.ts ServerContextType::AppSSR { app_dir } | ServerContextType::AppRSC { app_dir, .. } - | ServerContextType::AppRoute { app_dir } => { + | ServerContextType::AppRoute { app_dir, .. } => { import_map.insert_exact_alias( "styled-jsx", request_to_import_mapping(get_next_package(app_dir), "styled-jsx"), diff --git a/packages/next-swc/crates/next-core/src/next_server/context.rs b/packages/next-swc/crates/next-core/src/next_server/context.rs index c044e1f32e01e0..d4bd4703e92cff 100644 --- a/packages/next-swc/crates/next-core/src/next_server/context.rs +++ b/packages/next-swc/crates/next-core/src/next_server/context.rs @@ -93,6 +93,7 @@ pub enum ServerContextType { }, AppRoute { app_dir: Vc, + ecmascript_client_reference_transition_name: Option>, }, Middleware, Instrumentation, @@ -616,8 +617,27 @@ pub async fn get_server_module_options_context( ..module_options_context } } - ServerContextType::AppRoute { .. } => { + ServerContextType::AppRoute { + app_dir, + ecmascript_client_reference_transition_name, + } => { next_server_rules.extend(source_transform_rules); + if let Some(ecmascript_client_reference_transition_name) = + ecmascript_client_reference_transition_name + { + next_server_rules.push(get_ecma_transform_rule( + Box::new(ClientDirectiveTransformer::new( + ecmascript_client_reference_transition_name, + )), + enable_mdx_rs.is_some(), + true, + )); + } + + next_server_rules.push( + get_next_react_server_components_transform_rule(next_config, true, Some(app_dir)) + .await?, + ); let module_options_context = ModuleOptionsContext { esm_url_rewrite_behavior: Some(UrlRewriteBehavior::Full), diff --git a/packages/next-swc/crates/next-core/src/next_server/resolve.rs b/packages/next-swc/crates/next-core/src/next_server/resolve.rs index 1ca85ce2edeb29..fad175f7fa0b27 100644 --- a/packages/next-swc/crates/next-core/src/next_server/resolve.rs +++ b/packages/next-swc/crates/next-core/src/next_server/resolve.rs @@ -234,7 +234,7 @@ impl ResolvePlugin for ExternalCjsModulesResolvePlugin { way Node.js resolves modules is slightly different from the way Next.js \ resolves modules. Next.js was able to resolve it, while Node.js would not be \ able to.\nTry to remove this package from \ - serverComponentsExtenalPackages.\nOr update the import side to use a \ + serverComponentsExternalPackages.\nOr update the import side to use a \ compatible request that can be resolved by Node.js.", ); }; @@ -486,7 +486,7 @@ impl Issue for UnableToExternalize { StyledString::Text("Package ".to_string()), StyledString::Code(package), StyledString::Text(" (".to_string()), - StyledString::Code("serverComponentsExtenalPackages".to_string()), + StyledString::Code("serverComponentsExternalPackages".to_string()), StyledString::Text(" or default list) can't be external".to_string()), ]) .cell()) @@ -510,7 +510,7 @@ impl Issue for UnableToExternalize { StyledString::Text("The request ".to_string()), StyledString::Code(self.request.to_string()), StyledString::Text(" matches ".to_string()), - StyledString::Code("serverComponentsExtenalPackages".to_string()), + StyledString::Code("serverComponentsExternalPackages".to_string()), StyledString::Text( " (or the default list), but it can't be external:".to_string(), ), diff --git a/packages/next-swc/crates/next-custom-transforms/Cargo.toml b/packages/next-swc/crates/next-custom-transforms/Cargo.toml index 2b0d4b746478c4..30409ef6bd644a 100644 --- a/packages/next-swc/crates/next-custom-transforms/Cargo.toml +++ b/packages/next-swc/crates/next-custom-transforms/Cargo.toml @@ -37,7 +37,7 @@ turbopack-binding = { workspace = true, features = [ "__swc_transform_relay", ] } # To allow quote! macro works -swc_core = { workspace = true, features = ["ecma_quote"]} +swc_core = { workspace = true, features = ["ecma_quote"] } react_remove_properties = "0.24.7" remove_console = "0.25.7" preset_env_base = "0.4.12" diff --git a/packages/next-swc/crates/wasm/Cargo.toml b/packages/next-swc/crates/wasm/Cargo.toml index bc3bf0bc680c13..c184f6b6c5f84c 100644 --- a/packages/next-swc/crates/wasm/Cargo.toml +++ b/packages/next-swc/crates/wasm/Cargo.toml @@ -21,6 +21,7 @@ anyhow = "1.0.66" console_error_panic_hook = "0.1.6" next-custom-transforms = { workspace = true } serde_json = "1" +tracing = { version = "0.1.37" } wasm-bindgen = { version = "0.2", features = ["enable-interning"] } wasm-bindgen-futures = "0.4.8" getrandom = { version = "0.2.9", default-features = false, features = ["js"] } diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index fef0a7250db95d..4114be8757bb9e 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", @@ -11,7 +11,7 @@ "build-native-no-plugin-woa-release": "napi build --platform -p next-swc-napi --cargo-cwd ../../ --cargo-name next_swc_napi --release --cargo-flags=--no-default-features --features image-webp,tracing/release_max_level_info --js false native", "build-native-wasi": "npx --package=@napi-rs/cli@3.0.0-alpha.45 napi build --platform --target wasm32-wasip1-threads -p next-swc-napi --cwd ../../ --output-dir packages/next-swc/native", "build-wasm": "wasm-pack build crates/wasm --scope=next", - "cache-build-native": "echo $(ls native)", + "cache-build-native": "[ -d native ] && echo $(ls native)", "rust-check-fmt": "cd ../..; cargo fmt -- --check", "rust-check-clippy": "cargo clippy --workspace --all-targets -- -D warnings -A deprecated", "rust-check-napi-rustls": "cargo check -p next-swc-napi", diff --git a/packages/next/index.d.ts b/packages/next/index.d.ts index 348fd2e07a2db6..f91a6df44cf922 100644 --- a/packages/next/index.d.ts +++ b/packages/next/index.d.ts @@ -1,5 +1,4 @@ /// -/// /// /// /// diff --git a/packages/next/package.json b/packages/next/package.json index 553c8b2e34c132..aad0ea55538f17 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -49,8 +49,9 @@ "amp.d.ts", "og.js", "og.d.ts", + "types.d.ts", + "types.js", "index.d.ts", - "types/index.d.ts", "types/global.d.ts", "types/compiled.d.ts", "image-types/global.d.ts", @@ -92,7 +93,7 @@ ] }, "dependencies": { - "@next/env": "14.2.1-canary.5", + "@next/env": "14.3.0-canary.11", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -140,7 +141,7 @@ "@babel/runtime": "7.22.5", "@babel/traverse": "7.22.5", "@babel/types": "7.22.5", - "@capsizecss/metrics": "2.2.0", + "@capsizecss/metrics": "3.0.0", "@edge-runtime/cookies": "4.1.1", "@edge-runtime/ponyfill": "2.4.2", "@edge-runtime/primitives": "4.1.0", @@ -149,10 +150,10 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/polyfill-module": "14.2.1-canary.5", - "@next/polyfill-nomodule": "14.2.1-canary.5", - "@next/react-refresh-utils": "14.2.1-canary.5", - "@next/swc": "14.2.1-canary.5", + "@next/polyfill-module": "14.3.0-canary.11", + "@next/polyfill-nomodule": "14.3.0-canary.11", + "@next/react-refresh-utils": "14.3.0-canary.11", + "@next/swc": "14.3.0-canary.11", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@taskr/clear": "1.1.0", @@ -182,9 +183,9 @@ "@types/path-to-regexp": "1.7.0", "@types/picomatch": "2.3.3", "@types/platform": "1.3.4", - "@types/react": "18.2.37", - "@types/react-dom": "18.2.15", - "@types/react-is": "17.0.3", + "@types/react": "18.2.74", + "@types/react-dom": "18.2.23", + "@types/react-is": "18.2.4", "@types/semver": "7.3.1", "@types/send": "0.14.4", "@types/shell-quote": "1.7.1", @@ -196,7 +197,7 @@ "@types/ws": "8.2.0", "@vercel/ncc": "0.34.0", "@vercel/nft": "0.26.4", - "@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240411.3", + "@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240417.1", "acorn": "8.5.0", "amphtml-validator": "1.0.35", "anser": "1.4.9", @@ -320,6 +321,18 @@ "ws": "8.2.3", "zod": "3.22.3" }, + "keywords": [ + "react", + "framework", + "nextjs", + "web", + "server", + "node", + "front-end", + "back-end", + "cli", + "vercel" + ], "engines": { "node": ">=18.17.0" } diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 4420c266144c38..3469e0bd7b5246 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -4,7 +4,7 @@ import type { Middleware, RouteHas } from '../../lib/load-custom-routes' import { promises as fs } from 'fs' import LRUCache from 'next/dist/compiled/lru-cache' import picomatch from 'next/dist/compiled/picomatch' -import type { ServerRuntime } from 'next/types' +import type { ServerRuntime } from '../../types' import { extractExportedConstValue, UnsupportedValueError, diff --git a/packages/next/src/build/babel/plugins/next-page-config.ts b/packages/next/src/build/babel/plugins/next-page-config.ts index d3a20d4f04dc7e..c4473ea02cb326 100644 --- a/packages/next/src/build/babel/plugins/next-page-config.ts +++ b/packages/next/src/build/babel/plugins/next-page-config.ts @@ -5,7 +5,7 @@ import type { Visitor, NodePath, } from 'next/dist/compiled/babel/core' -import type { PageConfig } from 'next/types' +import type { PageConfig } from '../../../types' import { STRING_LITERAL_DROP_BUNDLE } from '../../../shared/lib/constants' const CONFIG_KEY = 'config' diff --git a/packages/next/src/build/collect-build-traces.ts b/packages/next/src/build/collect-build-traces.ts index 75d0d18d102e47..86cdcc49336b48 100644 --- a/packages/next/src/build/collect-build-traces.ts +++ b/packages/next/src/build/collect-build-traces.ts @@ -14,11 +14,6 @@ import { import path from 'path' import fs from 'fs/promises' -import { - deserializePageInfos, - type PageInfos, - type SerializedPageInfos, -} from './utils' import { loadBindings } from './swc' import { nonNullable } from '../lib/non-nullable' import * as ciEnvironment from '../telemetry/ci-info' @@ -30,6 +25,7 @@ import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import isError from '../lib/is-error' import type { NodeFileTraceReasons } from '@vercel/nft' +import type { RoutesUsingEdgeRuntime } from './utils' const debug = debugOriginal('next:build:build-traces') @@ -86,7 +82,7 @@ export async function collectBuildTraces({ dir, config, distDir, - pageInfos, + edgeRuntimeRoutes, staticPages, nextBuildSpan = new Span({ name: 'build' }), hasSsrAmpPages, @@ -99,7 +95,7 @@ export async function collectBuildTraces({ hasSsrAmpPages: boolean outputFileTracingRoot: string // pageInfos is serialized when this function runs in a worker. - pageInfos: PageInfos | SerializedPageInfos + edgeRuntimeRoutes: RoutesUsingEdgeRuntime nextBuildSpan?: Span config: NextConfigComplete buildTraceContext?: BuildTraceContext @@ -673,8 +669,6 @@ export async function collectBuildTraces({ } const { entryNameFilesMap } = buildTraceContext?.chunksTrace || {} - const infos = - pageInfos instanceof Map ? pageInfos : deserializePageInfos(pageInfos) await Promise.all( [ @@ -695,8 +689,7 @@ export async function collectBuildTraces({ } // edge routes have no trace files - const pageInfo = infos.get(route) - if (pageInfo?.runtime === 'edge') { + if (edgeRuntimeRoutes.hasOwnProperty(route)) { return } diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 62c6040c3cde9b..7ae4c637f1b982 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -50,7 +50,7 @@ import { import { getPageStaticInfo } from './analysis/get-page-static-info' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' -import type { ServerRuntime } from '../../types' +import type { ServerRuntime } from '../types' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { encodeMatchers } from './webpack/loaders/next-middleware-loader' import type { EdgeFunctionLoaderOptions } from './webpack/loaders/next-edge-function-loader' diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index d5c68ec9b97733..585609c7499b31 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -120,7 +120,7 @@ import { copyTracedFiles, isReservedPage, isAppBuiltinNotFoundPage, - serializePageInfos, + collectRoutesUsingEdgeRuntime, } from './utils' import type { PageInfo, PageInfos, AppConfig } from './utils' import { writeBuildId } from './write-build-id' @@ -1650,7 +1650,7 @@ export default async function build( config, distDir, // Serialize Map as this is sent to the worker. - pageInfos: serializePageInfos(new Map()), + edgeRuntimeRoutes: collectRoutesUsingEdgeRuntime(new Map()), staticPages: [], hasSsrAmpPages: false, buildTraceContext, @@ -2387,7 +2387,7 @@ export default async function build( dir, config, distDir, - pageInfos, + edgeRuntimeRoutes: collectRoutesUsingEdgeRuntime(pageInfos), staticPages: [...staticPages], nextBuildSpan, hasSsrAmpPages, @@ -3358,13 +3358,6 @@ export default async function build( return Promise.reject(err) }) - // TODO: remove in the next major version - if (config.analyticsId) { - Log.warn( - `\`config.analyticsId\` is deprecated and will be removed in next major version. Read more: https://nextjs.org/docs/messages/deprecated-analyticsid` - ) - } - if (Boolean(config.experimental.nextScriptWorkers)) { await nextBuildSpan .traceChild('verify-partytown-setup') diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 86aef60666c6c3..dd0bc86b998083 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -227,8 +227,11 @@ export async function loadBindings( !!triple?.raw && knownDefaultWasmFallbackTriples.includes(triple.raw) ) const isWebContainer = process.versions.webcontainer + // Normal execution relies on the param `useWasmBinary` flag to load, but + // in certain cases where there isn't a native binary we always load wasm fallback first. const shouldLoadWasmFallbackFirst = - (!disableWasmFallback && unsupportedPlatform && useWasmBinary) || + (!disableWasmFallback && useWasmBinary) || + unsupportedPlatform || isWebContainer if (!unsupportedPlatform && useWasmBinary) { diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 6e1c9c06bc6bf6..203557b34246ef 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -6,7 +6,7 @@ import type { GetStaticPathsResult, PageConfig, ServerRuntime, -} from 'next/types' +} from '../types' import type { BuildManifest } from '../server/get-page-files' import type { Redirect, @@ -360,14 +360,21 @@ export interface PageInfo { export type PageInfos = Map -export type SerializedPageInfos = [string, PageInfo][] - -export function serializePageInfos(input: PageInfos): SerializedPageInfos { - return Array.from(input.entries()) +export interface RoutesUsingEdgeRuntime { + [route: string]: 0 } -export function deserializePageInfos(input: SerializedPageInfos): PageInfos { - return new Map(input) +export function collectRoutesUsingEdgeRuntime( + input: PageInfos +): RoutesUsingEdgeRuntime { + const routesUsingEdgeRuntime: RoutesUsingEdgeRuntime = {} + for (const [route, info] of input.entries()) { + if (isEdgeRuntime(info.runtime)) { + routesUsingEdgeRuntime[route] = 0 + } + } + + return routesUsingEdgeRuntime } export async function printTreeView( diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 0c0d191c61b5a4..f5d4ba9d97f760 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1383,7 +1383,6 @@ export default async function getBaseWebpackConfig( // Alias react for switching between default set and share subset. oneOf: [ { - exclude: asyncStoragesRegex, issuerLayer: isWebpackServerOnlyLayer, test: { // Resolve it if it is a source code file, and it has NOT been @@ -1391,7 +1390,7 @@ export default async function getBaseWebpackConfig( and: [ codeCondition.test, { - not: [optOutBundlingPackageRegex], + not: [optOutBundlingPackageRegex, asyncStoragesRegex], }, ], }, @@ -1499,6 +1498,7 @@ export default async function getBaseWebpackConfig( { test: codeCondition.test, issuerLayer: WEBPACK_LAYERS.serverSideRendering, + exclude: asyncStoragesRegex, use: appSSRLayerLoaders, resolve: { mainFields: getMainField(compilerType, true), @@ -1815,7 +1815,6 @@ export default async function getBaseWebpackConfig( buildId, rewrites, isDevFallback, - exportRuntime: true, appDirEnabled: hasAppDir, }), new ProfilingPlugin({ runWebpackSpan, rootDir: dir }), @@ -1880,7 +1879,8 @@ export default async function getBaseWebpackConfig( new NextFontManifestPlugin({ appDir, }), - isClient && + !dev && + isClient && new CssChunkingPlugin(config.experimental.cssChunking === 'strict'), !dev && isClient && diff --git a/packages/next/src/build/webpack/config/blocks/base.ts b/packages/next/src/build/webpack/config/blocks/base.ts index 522fe745dbef4a..7cac0d9addd63a 100644 --- a/packages/next/src/build/webpack/config/blocks/base.ts +++ b/packages/next/src/build/webpack/config/blocks/base.ts @@ -18,7 +18,7 @@ export const base = curry(function base( ? 'node18.17' // Same version defined in packages/next/package.json#engines : ctx.isEdgeRuntime ? ['web', 'es6'] - : ['web', 'es5'] + : ['web', 'es6'] // https://webpack.js.org/configuration/devtool/#development if (ctx.isDevelopment) { diff --git a/packages/next/src/build/webpack/config/blocks/css/index.ts b/packages/next/src/build/webpack/config/blocks/css/index.ts index 42ac2171769b82..6866b88dce81c2 100644 --- a/packages/next/src/build/webpack/config/blocks/css/index.ts +++ b/packages/next/src/build/webpack/config/blocks/css/index.ts @@ -57,7 +57,8 @@ let postcssInstancePromise: Promise export async function lazyPostCSS( rootDirectory: string, supportedBrowsers: string[] | undefined, - disablePostcssPresetEnv: boolean | undefined + disablePostcssPresetEnv: boolean | undefined, + useLightningcss: boolean | undefined ) { if (!postcssInstancePromise) { postcssInstancePromise = (async () => { @@ -128,7 +129,8 @@ export async function lazyPostCSS( const postCssPlugins = await getPostCssPlugins( rootDirectory, supportedBrowsers, - disablePostcssPresetEnv + disablePostcssPresetEnv, + useLightningcss ) return { @@ -155,7 +157,8 @@ export const css = curry(async function css( lazyPostCSS( ctx.rootDirectory, ctx.supportedBrowsers, - ctx.experimental.disablePostcssPresetEnv + ctx.experimental.disablePostcssPresetEnv, + ctx.experimental.useLightningcss ) const sassPreprocessors: webpack.RuleSetUseItem[] = [ diff --git a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts b/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts index bb43722bf535e0..ddb47eda7d0256 100644 --- a/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts +++ b/packages/next/src/build/webpack/config/blocks/css/loaders/global.ts @@ -34,6 +34,7 @@ export function getGlobalCssLoader( import: (url: string, _: any, resourcePath: string) => cssFileResolve(url, resourcePath, ctx.experimental.urlImports), targets: ctx.supportedBrowsers, + postcss, }, }) } else { diff --git a/packages/next/src/build/webpack/config/blocks/css/loaders/modules.ts b/packages/next/src/build/webpack/config/blocks/css/loaders/modules.ts index 1479c5d17c9d3b..c34250b6e843ad 100644 --- a/packages/next/src/build/webpack/config/blocks/css/loaders/modules.ts +++ b/packages/next/src/build/webpack/config/blocks/css/loaders/modules.ts @@ -40,6 +40,7 @@ export function getCssModuleLoader( exportOnlyLocals: ctx.isServer, }, targets: ctx.supportedBrowsers, + postcss, }, }) } else { diff --git a/packages/next/src/build/webpack/config/blocks/css/plugins.ts b/packages/next/src/build/webpack/config/blocks/css/plugins.ts index c25a9eb149213b..bfa391ee9055af 100644 --- a/packages/next/src/build/webpack/config/blocks/css/plugins.ts +++ b/packages/next/src/build/webpack/config/blocks/css/plugins.ts @@ -116,7 +116,8 @@ function getDefaultPlugins( export async function getPostCssPlugins( dir: string, supportedBrowsers: string[] | undefined, - disablePostcssPresetEnv: boolean = false + disablePostcssPresetEnv: boolean = false, + useLightningcss: boolean = false ): Promise { let config = await findConfig<{ plugins: CssPluginCollection }>( dir, @@ -125,7 +126,9 @@ export async function getPostCssPlugins( if (config == null) { config = { - plugins: getDefaultPlugins(supportedBrowsers, disablePostcssPresetEnv), + plugins: useLightningcss + ? [] + : getDefaultPlugins(supportedBrowsers, disablePostcssPresetEnv), } } diff --git a/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts b/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts index 932b2729e1292c..88a4b052777716 100644 --- a/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts +++ b/packages/next/src/build/webpack/loaders/lightningcss-loader/src/loader.ts @@ -281,6 +281,16 @@ export async function LightningCssLoader( return } + if (options.postcss) { + const { postcssWithPlugins } = await options.postcss() + + if (postcssWithPlugins?.plugins?.length > 0) { + throw new Error( + `[${LOADER_NAME}]: experimental.useLightningcss does not work with postcss plugins. Please remove 'useLightningcss: true' from your configuration.` + ) + } + } + const exports: CssExport[] = [] const imports: CssImport[] = [] const icssImports: CssImport[] = [] diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts index e0f0728e8eaad0..8e82cf9aabe238 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts @@ -1,5 +1,5 @@ import type webpack from 'webpack' -import type { SizeLimit } from '../../../../../types' +import type { SizeLimit } from '../../../../types' import type { PagesRouteModuleOptions } from '../../../../server/future/route-modules/pages/module' import type { MiddlewareConfig } from '../../../analysis/get-page-static-info' diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts index 80a6d4b1e554ac..1bda32f320d47d 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -15,7 +15,7 @@ import { import { SERVER_RUNTIME } from '../../../../lib/constants' import type { ManifestRewriteRoute, PrerenderManifest } from '../../..' import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths' -import type { SizeLimit } from '../../../../../types' +import type { SizeLimit } from '../../../../types' import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until' import type { PAGE_TYPES } from '../../../../lib/page-types' import type { NextRequestHint } from '../../../../server/web/adapter' diff --git a/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts index 8d39455a0a82cf..819154545a01ac 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -48,7 +48,8 @@ export default function transformSource( // When we cannot determine the export names, we use eager mode to include the whole module. // Otherwise, we use eager mode with webpackExports to only include the necessary exports. - if (ids.length === 0) { + // If we have '*' in the ids, we include all the imports + if (ids.length === 0 || ids.includes('*')) { return `import(/* webpackMode: "eager" */ ${importPath});\n` } else { return `import(/* webpackMode: "eager", webpackExports: ${JSON.stringify( diff --git a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts index 7f66c7f2e8bcb5..d8481d235d70f3 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts @@ -1,3 +1,4 @@ +import type { webpack } from 'next/dist/compiled/webpack/webpack' import { RSC_MOD_REF_PROXY_ALIAS } from '../../../../lib/constants' import { BARREL_OPTIMIZATION_PREFIX, @@ -13,6 +14,36 @@ const noopHeadPath = require.resolve('next/dist/client/components/noop-head') const MODULE_PROXY_PATH = 'next/dist/build/webpack/loaders/next-flight-loader/module-proxy' +type SourceType = 'auto' | 'commonjs' | 'module' +export function getAssumedSourceType( + mod: webpack.Module, + sourceType: SourceType +): SourceType { + const buildInfo = getModuleBuildInfo(mod) + const detectedClientEntryType = buildInfo?.rsc?.clientEntryType + const clientRefs = buildInfo?.rsc?.clientRefs || [] + + // It's tricky to detect the type of a client boundary, but we should always + // use the `module` type when we can, to support `export *` and `export from` + // syntax in other modules that import this client boundary. + let assumedSourceType = sourceType + if (assumedSourceType === 'auto' && detectedClientEntryType === 'auto') { + if ( + clientRefs.length === 0 || + (clientRefs.length === 1 && clientRefs[0] === '') + ) { + // If there's zero export detected in the client boundary, and it's the + // `auto` type, we can safely assume it's a CJS module because it doesn't + // have ESM exports. + assumedSourceType = 'commonjs' + } else if (!clientRefs.includes('*')) { + // Otherwise, we assume it's an ESM module. + assumedSourceType = 'module' + } + } + return assumedSourceType +} + export default function transformSource( this: any, source: string, @@ -50,29 +81,12 @@ export default function transformSource( // A client boundary. if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) { - const sourceType = this._module?.parser?.sourceType - const detectedClientEntryType = buildInfo.rsc.clientEntryType + const assumedSourceType = getAssumedSourceType( + this._module, + this._module?.parser?.sourceType + ) const clientRefs = buildInfo.rsc.clientRefs! - // It's tricky to detect the type of a client boundary, but we should always - // use the `module` type when we can, to support `export *` and `export from` - // syntax in other modules that import this client boundary. - let assumedSourceType = sourceType - if (assumedSourceType === 'auto' && detectedClientEntryType === 'auto') { - if ( - clientRefs.length === 0 || - (clientRefs.length === 1 && clientRefs[0] === '') - ) { - // If there's zero export detected in the client boundary, and it's the - // `auto` type, we can safely assume it's a CJS module because it doesn't - // have ESM exports. - assumedSourceType = 'commonjs' - } else if (!clientRefs.includes('*')) { - // Otherwise, we assume it's an ESM module. - assumedSourceType = 'module' - } - } - if (assumedSourceType === 'module') { if (clientRefs.includes('*')) { this.callback( @@ -123,9 +137,9 @@ export { e${cnt++} as ${ref} };` } } - this.callback( - null, - source.replace(RSC_MOD_REF_PROXY_ALIAS, MODULE_PROXY_PATH), - sourceMap + const replacedSource = source.replace( + RSC_MOD_REF_PROXY_ALIAS, + MODULE_PROXY_PATH ) + this.callback(null, replacedSource, sourceMap) } diff --git a/packages/next/src/build/webpack/loaders/next-swc-loader.ts b/packages/next/src/build/webpack/loaders/next-swc-loader.ts index 6ca51b575dcd08..f60acae3ddedc1 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -26,7 +26,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { NextConfig } from '../../../../types' +import type { NextConfig } from '../../../types' import type { WebpackLayerName } from '../../../lib/constants' import { isWasm, transform } from '../../swc' import { getLoaderSWCOptions } from '../../swc/options' diff --git a/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts index 9b9c3006d9a56c..5486a50b715957 100644 --- a/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts @@ -123,14 +123,12 @@ export default class BuildManifestPlugin { private buildId: string private rewrites: CustomRoutes['rewrites'] private isDevFallback: boolean - private exportRuntime: boolean private appDirEnabled: boolean constructor(options: { buildId: string rewrites: CustomRoutes['rewrites'] isDevFallback?: boolean - exportRuntime?: boolean appDirEnabled: boolean }) { this.buildId = options.buildId @@ -144,7 +142,6 @@ export default class BuildManifestPlugin { this.rewrites.beforeFiles = options.rewrites.beforeFiles.map(processRoute) this.rewrites.afterFiles = options.rewrites.afterFiles.map(processRoute) this.rewrites.fallback = options.rewrites.fallback.map(processRoute) - this.exportRuntime = !!options.exportRuntime } createAssets(compiler: any, compilation: any, assets: any) { @@ -258,12 +255,9 @@ export default class BuildManifestPlugin { JSON.stringify(assetMap, null, 2) ) - if (this.exportRuntime) { - assets[`server/${MIDDLEWARE_BUILD_MANIFEST}.js`] = - new sources.RawSource( - `self.__BUILD_MANIFEST=${JSON.stringify(assetMap)}` - ) - } + assets[`server/${MIDDLEWARE_BUILD_MANIFEST}.js`] = new sources.RawSource( + `self.__BUILD_MANIFEST=${JSON.stringify(assetMap)}` + ) if (!this.isDevFallback) { const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index aea7e34537f2db..ecd022f9e2c2ea 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -225,7 +225,6 @@ export function getDefineEnv({ 'process.env.__NEXT_CONFIG_OUTPUT': config.output, 'process.env.__NEXT_I18N_SUPPORT': !!config.i18n, 'process.env.__NEXT_I18N_DOMAINS': config.i18n?.domains ?? false, - 'process.env.__NEXT_ANALYTICS_ID': config.analyticsId, // TODO: remove in the next major version 'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': config.skipMiddlewareUrlNormalize, 'process.env.__NEXT_EXTERNAL_MIDDLEWARE_REWRITE_RESOLVE': diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index 4d2a5c3f433638..7f6eb572afa5ef 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -41,6 +41,8 @@ import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-s import { getProxiedPluginState } from '../../build-context' import { PAGE_TYPES } from '../../../lib/page-types' import { isWebpackServerOnlyLayer } from '../../utils' +import { getModuleBuildInfo } from '../loaders/get-module-build-info' +import { getAssumedSourceType } from '../loaders/next-flight-loader' interface Options { dev: boolean @@ -664,9 +666,12 @@ export class FlightClientEntryPlugin { if (!modRequest) return if (visited.has(modRequest)) { if (clientComponentImports[modRequest]) { - for (const name of importedIdentifiers) { - clientComponentImports[modRequest].add(name) - } + addClientImport( + mod, + modRequest, + clientComponentImports, + importedIdentifiers + ) } return } @@ -698,9 +703,13 @@ export class FlightClientEntryPlugin { if (!clientComponentImports[modRequest]) { clientComponentImports[modRequest] = new Set() } - for (const name of importedIdentifiers) { - clientComponentImports[modRequest].add(name) - } + addClientImport( + mod, + modRequest, + clientComponentImports, + importedIdentifiers + ) + return } @@ -1016,3 +1025,37 @@ export class FlightClientEntryPlugin { new sources.RawSource(json) as unknown as webpack.sources.RawSource } } + +function addClientImport( + mod: webpack.NormalModule, + modRequest: string, + clientComponentImports: ClientComponentImports, + importedIdentifiers: string[] +) { + const clientEntryType = getModuleBuildInfo(mod).rsc?.clientEntryType + const isCjsModule = clientEntryType === 'cjs' + const assumedSourceType = getAssumedSourceType( + mod, + isCjsModule ? 'commonjs' : 'auto' + ) + + const isAutoModuleSourceType = assumedSourceType === 'auto' + if (isAutoModuleSourceType) { + clientComponentImports[modRequest] = new Set(['*']) + } else { + // If it's not analyzed as named ESM exports, e.g. if it's mixing `export *` with named exports, + // We'll include all modules since it's not able to do tree-shaking. + for (const name of importedIdentifiers) { + // For cjs module default import, we include the whole module since + const isCjsDefaultImport = isCjsModule && name === 'default' + + // Always include __esModule along with cjs module default export, + // to make sure it work with client module proxy from React. + if (isCjsDefaultImport) { + clientComponentImports[modRequest].add('__esModule') + } + + clientComponentImports[modRequest].add(name) + } + } +} diff --git a/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts b/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts index d1b4854812f2af..de056f719699f0 100644 --- a/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts +++ b/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts @@ -441,8 +441,8 @@ declare namespace __next_route_internal_types__ { } declare module 'next' { - export { default } from 'next/types/index.js' - export * from 'next/types/index.js' + export { default } from 'next/types.js' + export * from 'next/types.js' export type Route = __next_route_internal_types__.RouteImpl diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts index f8d7ba0e6680b5..4abc0d8db73182 100644 --- a/packages/next/src/cli/next-dev.ts +++ b/packages/next/src/cli/next-dev.ts @@ -200,13 +200,6 @@ const nextDev = async ( traceUploadUrl = options.experimentalUploadTrace } - // TODO: remove in the next major version - if (config.analyticsId) { - Log.warn( - `\`config.analyticsId\` is deprecated and will be removed in next major version. Read more: https://nextjs.org/docs/messages/deprecated-analyticsid` - ) - } - const devServerOptions: StartServerOptions = { dir, port, diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx index e4c9d7632d7b8d..f54d9ac06ca394 100644 --- a/packages/next/src/client/app-index.tsx +++ b/packages/next/src/client/app-index.tsx @@ -131,14 +131,6 @@ const StrictModeIfEnabled = process.env.__NEXT_STRICT_MODE_APP : React.Fragment function Root({ children }: React.PropsWithChildren<{}>) { - // TODO: remove in the next major version - if (process.env.__NEXT_ANALYTICS_ID) { - // eslint-disable-next-line react-hooks/rules-of-hooks - React.useEffect(() => { - require('./performance-relayer-app')() - }, []) - } - if (process.env.__NEXT_TEST_MODE) { // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { diff --git a/packages/next/src/client/components/action-async-storage-instance.ts b/packages/next/src/client/components/action-async-storage-instance.ts new file mode 100644 index 00000000000000..161879ae2d7607 --- /dev/null +++ b/packages/next/src/client/components/action-async-storage-instance.ts @@ -0,0 +1,4 @@ +import type { ActionAsyncStorage } from './action-async-storage.external' +import { createAsyncLocalStorage } from './async-local-storage' + +export const actionAsyncStorage: ActionAsyncStorage = createAsyncLocalStorage() diff --git a/packages/next/src/client/components/action-async-storage.external.ts b/packages/next/src/client/components/action-async-storage.external.ts index d34b8225c818b1..beaac5d06902f9 100644 --- a/packages/next/src/client/components/action-async-storage.external.ts +++ b/packages/next/src/client/components/action-async-storage.external.ts @@ -1,6 +1,9 @@ import type { AsyncLocalStorage } from 'async_hooks' -import { createAsyncLocalStorage } from './async-local-storage' +// Share the instance module in the next-shared layer +// eslint-disable-next-line @typescript-eslint/no-unused-expressions +;('TURBOPACK { transition: next-shared }') +import { actionAsyncStorage } from './action-async-storage-instance' export interface ActionStore { readonly isAction?: boolean readonly isAppRoute?: boolean @@ -8,4 +11,4 @@ export interface ActionStore { export type ActionAsyncStorage = AsyncLocalStorage -export const actionAsyncStorage: ActionAsyncStorage = createAsyncLocalStorage() +export { actionAsyncStorage } diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index 86ea7dfd478ebc..99b3b8660be5d6 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -618,6 +618,27 @@ function Router({ return getSelectedParams(tree) }, [tree]) + const layoutRouterContext = useMemo(() => { + return { + childNodes: cache.parallelRoutes, + tree, + // Root node always has `url` + // Provided in AppTreeContext to ensure it can be overwritten in layout-router + url: canonicalUrl, + loading: cache.loading, + } + }, [cache.parallelRoutes, tree, canonicalUrl, cache.loading]) + + const globalLayoutRouterContext = useMemo(() => { + return { + buildId, + changeByServerResponse, + tree, + focusAndScrollRef, + nextUrl, + } + }, [buildId, changeByServerResponse, tree, focusAndScrollRef, nextUrl]) + let head if (matchingHead !== null) { // The head is wrapped in an extra component so we can use @@ -668,25 +689,10 @@ function Router({ - + {content} diff --git a/packages/next/src/client/components/layout-router.tsx b/packages/next/src/client/components/layout-router.tsx index 13790442d665b3..1c3698c6049674 100644 --- a/packages/next/src/client/components/layout-router.tsx +++ b/packages/next/src/client/components/layout-router.tsx @@ -34,6 +34,7 @@ import { RedirectBoundary } from './redirect-boundary' import { NotFoundBoundary } from './not-found-boundary' import { getSegmentValue } from './router-reducer/reducers/get-segment-value' import { createRouterCacheKey } from './router-reducer/create-router-cache-key' +import { hasInterceptionRouteInCurrentTree } from './router-reducer/reducers/has-interception-route-in-current-tree' /** * Add refetch marker to router state at the point of the current layout segment. @@ -408,10 +409,11 @@ function InnerLayoutRouter({ */ // TODO-APP: remove '' const refetchTree = walkAddRefetch(['', ...segmentPath], fullTree) + const includeNextUrl = hasInterceptionRouteInCurrentTree(fullTree) childNode.lazyData = lazyData = fetchServerResponse( new URL(url, location.origin), refetchTree, - context.nextUrl, + includeNextUrl ? context.nextUrl : null, buildId ) childNode.lazyDataResolved = false @@ -437,10 +439,10 @@ function InnerLayoutRouter({ // It's important that we mark this as resolved, in case this branch is replayed, we don't want to continously re-apply // the patch to the tree. childNode.lazyDataResolved = true - } - // Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered. - use(unresolvedThenable) as never + // Suspend infinitely as `changeByServerResponse` will cause a different part of the tree to be rendered. + use(unresolvedThenable) as never + } } // If we get to this point, then we know we have something we can render. diff --git a/packages/next/src/client/components/react-dev-overlay/server/shared.ts b/packages/next/src/client/components/react-dev-overlay/server/shared.ts index a9f098a5703add..58eef73c562cce 100644 --- a/packages/next/src/client/components/react-dev-overlay/server/shared.ts +++ b/packages/next/src/client/components/react-dev-overlay/server/shared.ts @@ -1,6 +1,11 @@ import type { StackFrame } from 'stacktrace-parser' import type { ServerResponse } from 'http' import { codeFrameColumns } from 'next/dist/compiled/babel/code-frame' +import isInternal, { + nextInternalsRe, + reactNodeModulesRe, + reactVendoredRe, +} from '../../../../shared/lib/is-internal' export type SourcePackage = 'react' | 'next' @@ -11,28 +16,8 @@ export interface OriginalStackFrameResponse { sourcePackage?: SourcePackage | null } -/** React that's compiled with `next`. Used by App Router. */ -const reactVendoredRe = - /[\\/]next[\\/]dist[\\/]compiled[\\/](react|react-dom|react-server-dom-(webpack|turbopack)|scheduler)[\\/]/ - -/** React the user installed. Used by Pages Router, or user imports in App Router. */ -const reactNodeModulesRe = /node_modules[\\/](react|react-dom|scheduler)[\\/]/ - -const nextInternalsRe = - /(node_modules[\\/]next[\\/]|[\\/].next[\\/]static[\\/]chunks[\\/]webpack\.js$|(edge-runtime-webpack|webpack-runtime)\.js$)/ - const nextMethodRe = /(^__webpack_.*|node_modules[\\/]next[\\/])/ -function isInternal(file: string | null) { - if (!file) return false - - return ( - nextInternalsRe.test(file) || - reactVendoredRe.test(file) || - reactNodeModulesRe.test(file) - ) -} - /** Given a frame, it parses which package it belongs to. */ export function findSourcePackage({ file, @@ -58,17 +43,13 @@ export function findSourcePackage({ /** * It looks up the code frame of the traced source. - * @note It ignores node_modules or Next.js/React internals, as these can often be huge budnled files. + * @note It ignores Next.js/React internals, as these can often be huge bundled files. */ export function getOriginalCodeFrame( frame: StackFrame, source: string | null ): string | null | undefined { - if ( - !source || - frame.file?.includes('node_modules') || - isInternal(frame.file) - ) { + if (!source || isInternal(frame.file)) { return null } diff --git a/packages/next/src/client/components/request-async-storage-instance.ts b/packages/next/src/client/components/request-async-storage-instance.ts new file mode 100644 index 00000000000000..a9b67084329875 --- /dev/null +++ b/packages/next/src/client/components/request-async-storage-instance.ts @@ -0,0 +1,5 @@ +import { createAsyncLocalStorage } from './async-local-storage' +import type { RequestAsyncStorage } from './request-async-storage.external' + +export const requestAsyncStorage: RequestAsyncStorage = + createAsyncLocalStorage() diff --git a/packages/next/src/client/components/request-async-storage.external.ts b/packages/next/src/client/components/request-async-storage.external.ts index dbd33809881c31..0af201362a3da5 100644 --- a/packages/next/src/client/components/request-async-storage.external.ts +++ b/packages/next/src/client/components/request-async-storage.external.ts @@ -4,7 +4,10 @@ import type { ResponseCookies } from '../../server/web/spec-extension/cookies' import type { ReadonlyHeaders } from '../../server/web/spec-extension/adapters/headers' import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/adapters/request-cookies' -import { createAsyncLocalStorage } from './async-local-storage' +// Share the instance module in the next-shared layer +// eslint-disable-next-line @typescript-eslint/no-unused-expressions +;('TURBOPACK { transition: next-shared }') +import { requestAsyncStorage } from './request-async-storage-instance' import type { DeepReadonly } from '../../shared/lib/deep-readonly' export interface RequestStore { @@ -20,8 +23,7 @@ export interface RequestStore { export type RequestAsyncStorage = AsyncLocalStorage -export const requestAsyncStorage: RequestAsyncStorage = - createAsyncLocalStorage() +export { requestAsyncStorage } export function getExpectedRequestStore(callingExpression: string) { const store = requestAsyncStorage.getStore() diff --git a/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts b/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts index 7b51e3660215c3..c9cd5a7be7a527 100644 --- a/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts +++ b/packages/next/src/client/components/router-reducer/apply-router-state-patch-to-tree.ts @@ -80,7 +80,7 @@ export function applyRouterStatePatchToTree( flightSegmentPath: FlightSegmentPath, flightRouterState: FlightRouterState, treePatch: FlightRouterState, - pathname: string + path: string ): FlightRouterState | null { const [segment, parallelRoutes, url, refetch, isRootLayout] = flightRouterState @@ -93,7 +93,7 @@ export function applyRouterStatePatchToTree( flightSegmentPath ) - addRefreshMarkerToActiveParallelSegments(tree, pathname) + addRefreshMarkerToActiveParallelSegments(tree, path) return tree } @@ -119,7 +119,7 @@ export function applyRouterStatePatchToTree( flightSegmentPath.slice(2), parallelRoutes[parallelRouteKey], treePatch, - pathname + path ) if (parallelRoutePatch === null) { @@ -142,7 +142,7 @@ export function applyRouterStatePatchToTree( tree[4] = true } - addRefreshMarkerToActiveParallelSegments(tree, pathname) + addRefreshMarkerToActiveParallelSegments(tree, path) return tree } diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts index d4c0e138ad39c2..9a8b5bc901dc67 100644 --- a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts +++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts @@ -49,7 +49,15 @@ export function createInitialRouterState({ loading: initialSeedData[3], } - addRefreshMarkerToActiveParallelSegments(initialTree, initialCanonicalUrl) + const canonicalUrl = + // location.href is read as the initial value for canonicalUrl in the browser + // This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates in the useEffect further down in this file. + location + ? // window.location does not have the same type as URL but has all the fields createHrefFromUrl needs. + createHrefFromUrl(location) + : initialCanonicalUrl + + addRefreshMarkerToActiveParallelSegments(initialTree, canonicalUrl) const prefetchCache = new Map() @@ -82,13 +90,7 @@ export function createInitialRouterState({ hashFragment: null, segmentPaths: [], }, - canonicalUrl: - // location.href is read as the initial value for canonicalUrl in the browser - // This is safe to do as canonicalUrl can't be rendered, it's only used to control the history updates in the useEffect further down in this file. - location - ? // window.location does not have the same type as URL but has all the fields createHrefFromUrl needs. - createHrefFromUrl(location) - : initialCanonicalUrl, + canonicalUrl, nextUrl: // the || operator is intentional, the pathname can be an empty string (extractPathFromFlightRouterState(initialTree) || location?.pathname) ?? diff --git a/packages/next/src/client/components/router-reducer/ppr-navigations.ts b/packages/next/src/client/components/router-reducer/ppr-navigations.ts index 35f975d0df5875..641dab8c4a8ec9 100644 --- a/packages/next/src/client/components/router-reducer/ppr-navigations.ts +++ b/packages/next/src/client/components/router-reducer/ppr-navigations.ts @@ -524,8 +524,8 @@ function createPendingCacheNode( // Create a deferred promise. This will be fulfilled once the dynamic // response is received from the server. - rsc: createDeferredRsc(), - head: isLeafSegment ? createDeferredRsc() : null, + rsc: createDeferredRsc() as React.ReactNode, + head: isLeafSegment ? (createDeferredRsc() as React.ReactNode) : null, lazyDataResolved: false, } } diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts index 7d8e314b848ad1..4784cfbdee5f74 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts @@ -225,7 +225,9 @@ export function serverActionReducer( [''], currentTree, treePatch, - href + redirectLocation + ? createHrefFromUrl(redirectLocation) + : state.canonicalUrl ) if (newTree === null) { diff --git a/packages/next/src/client/components/router-reducer/refetch-inactive-parallel-segments.ts b/packages/next/src/client/components/router-reducer/refetch-inactive-parallel-segments.ts index 95eb0ba806408e..05c1ddee9c3a27 100644 --- a/packages/next/src/client/components/router-reducer/refetch-inactive-parallel-segments.ts +++ b/packages/next/src/client/components/router-reducer/refetch-inactive-parallel-segments.ts @@ -45,23 +45,23 @@ async function refreshInactiveParallelSegmentsImpl({ fetchedSegments: Set rootTree: FlightRouterState }) { - const [, parallelRoutes, refetchPathname, refetchMarker] = updatedTree + const [, parallelRoutes, refetchPath, refetchMarker] = updatedTree const fetchPromises = [] if ( - refetchPathname && - refetchPathname !== location.pathname && + refetchPath && + refetchPath !== location.pathname + location.search && refetchMarker === 'refresh' && // it's possible for the tree to contain multiple segments that contain data at the same URL // we keep track of them so we can dedupe the requests - !fetchedSegments.has(refetchPathname) + !fetchedSegments.has(refetchPath) ) { - fetchedSegments.add(refetchPathname) // Mark this URL as fetched + fetchedSegments.add(refetchPath) // Mark this URL as fetched // Eagerly kick off the fetch for the refetch path & the parallel routes. This should be fine to do as they each operate // independently on their own cache nodes, and `applyFlightData` will copy anything it doesn't care about from the existing cache. const fetchPromise = fetchServerResponse( - new URL(refetchPathname, location.origin), + new URL(refetchPath, location.origin), // refetch from the root of the updated tree, otherwise it will be scoped to the current segment // and might not contain the data we need to patch in interception route data (such as dynamic params from a previous segment) [rootTree[0], rootTree[1], rootTree[2], 'refetch'], @@ -110,16 +110,16 @@ async function refreshInactiveParallelSegmentsImpl({ */ export function addRefreshMarkerToActiveParallelSegments( tree: FlightRouterState, - pathname: string + path: string ) { const [segment, parallelRoutes, , refetchMarker] = tree // a page segment might also contain concatenated search params, so we do a partial match on the key if (segment.includes(PAGE_SEGMENT_KEY) && refetchMarker !== 'refresh') { - tree[2] = pathname + tree[2] = path tree[3] = 'refresh' } for (const key in parallelRoutes) { - addRefreshMarkerToActiveParallelSegments(parallelRoutes[key], pathname) + addRefreshMarkerToActiveParallelSegments(parallelRoutes[key], path) } } diff --git a/packages/next/src/client/components/static-generation-async-storage-instance.ts b/packages/next/src/client/components/static-generation-async-storage-instance.ts new file mode 100644 index 00000000000000..3ac18d8f1c2ef9 --- /dev/null +++ b/packages/next/src/client/components/static-generation-async-storage-instance.ts @@ -0,0 +1,5 @@ +import type { StaticGenerationAsyncStorage } from './static-generation-async-storage.external' +import { createAsyncLocalStorage } from './async-local-storage' + +export const staticGenerationAsyncStorage: StaticGenerationAsyncStorage = + createAsyncLocalStorage() diff --git a/packages/next/src/client/components/static-generation-async-storage.external.ts b/packages/next/src/client/components/static-generation-async-storage.external.ts index 4433992a8f331f..62a7b2c04490e8 100644 --- a/packages/next/src/client/components/static-generation-async-storage.external.ts +++ b/packages/next/src/client/components/static-generation-async-storage.external.ts @@ -5,7 +5,10 @@ import type { FetchMetrics } from '../../server/base-http' import type { Revalidate } from '../../server/lib/revalidate' import type { PrerenderState } from '../../server/app-render/dynamic-rendering' -import { createAsyncLocalStorage } from './async-local-storage' +// Share the instance module in the next-shared layer +// eslint-disable-next-line @typescript-eslint/no-unused-expressions +;('TURBOPACK { transition: next-shared }') +import { staticGenerationAsyncStorage } from './static-generation-async-storage-instance' export interface StaticGenerationStore { readonly isStaticGeneration: boolean @@ -48,10 +51,11 @@ export interface StaticGenerationStore { isDraftMode?: boolean isUnstableNoStore?: boolean + + requestEndedState?: { ended?: boolean } } export type StaticGenerationAsyncStorage = AsyncLocalStorage -export const staticGenerationAsyncStorage: StaticGenerationAsyncStorage = - createAsyncLocalStorage() +export { staticGenerationAsyncStorage } diff --git a/packages/next/src/client/head-manager.ts b/packages/next/src/client/head-manager.ts index 5d9bc835653afd..8d477e46e9fb41 100644 --- a/packages/next/src/client/head-manager.ts +++ b/packages/next/src/client/head-manager.ts @@ -1,30 +1,8 @@ -export const DOMAttributeNames: Record = { - acceptCharset: 'accept-charset', - className: 'class', - htmlFor: 'for', - httpEquiv: 'http-equiv', - noModule: 'noModule', -} +import { setAttributesFromProps } from './set-attributes-from-props' function reactElementToDOM({ type, props }: JSX.Element): HTMLElement { const el: HTMLElement = document.createElement(type) - for (const p in props) { - if (!props.hasOwnProperty(p)) continue - if (p === 'children' || p === 'dangerouslySetInnerHTML') continue - - // we don't render undefined props to the DOM - if (props[p] === undefined) continue - - const attr = DOMAttributeNames[p] || p.toLowerCase() - if ( - type === 'script' && - (attr === 'async' || attr === 'defer' || attr === 'noModule') - ) { - ;(el as HTMLScriptElement)[attr] = !!props[p] - } else { - el.setAttribute(attr, props[p]) - } - } + setAttributesFromProps(el, props) const { children, dangerouslySetInnerHTML } = props if (dangerouslySetInnerHTML) { diff --git a/packages/next/src/client/index.tsx b/packages/next/src/client/index.tsx index dc5ba991c6450c..2af72e60a5bfc0 100644 --- a/packages/next/src/client/index.tsx +++ b/packages/next/src/client/index.tsx @@ -27,7 +27,6 @@ import { Portal } from './portal' import initHeadManager from './head-manager' import PageLoader from './page-loader' import type { StyleSheetTuple } from './page-loader' -import measureWebVitals from './performance-relayer' // TODO: remove in the next major version import { RouteAnnouncer } from './route-announcer' import { createRouter, makePublicRouterInstance } from './router' import { getProperError } from '../lib/is-error' @@ -604,12 +603,6 @@ function Root({ () => callbacks.forEach((callback) => callback()), [callbacks] ) - // TODO: remove in the next major version - // We should ask to measure the Web Vitals after rendering completes so we - // don't cause any hydration delay: - React.useEffect(() => { - measureWebVitals(onPerfEntry) - }, []) if (process.env.__NEXT_TEST_MODE) { // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/packages/next/src/client/performance-relayer-app.ts b/packages/next/src/client/performance-relayer-app.ts deleted file mode 100644 index 9dea54164db612..00000000000000 --- a/packages/next/src/client/performance-relayer-app.ts +++ /dev/null @@ -1,104 +0,0 @@ -// TODO: remove in the next major version -/* global location */ -import type { Metric, ReportCallback } from 'next/dist/compiled/web-vitals' - -// copied to prevent pulling in un-necessary utils -const WEB_VITALS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'] - -const initialHref = location.href -let isRegistered = false -let userReportHandler: ReportCallback | undefined -type Attribution = (typeof WEB_VITALS)[number] - -function onReport(metric: Metric): void { - if (userReportHandler) { - userReportHandler(metric) - } - - // This code is not shipped, executed, or present in the client-side - // JavaScript bundle unless explicitly enabled in your application. - // - // When this feature is enabled, we'll make it very clear by printing a - // message during the build (`next build`). - if ( - process.env.NODE_ENV === 'production' && - // This field is empty unless you explicitly configure it: - process.env.__NEXT_ANALYTICS_ID - ) { - const body: Record = { - dsn: process.env.__NEXT_ANALYTICS_ID, - id: metric.id, - page: window.__NEXT_DATA__?.page, - href: initialHref, - event_name: metric.name, - value: metric.value.toString(), - speed: - 'connection' in navigator && - (navigator as any)['connection'] && - 'effectiveType' in (navigator as any)['connection'] - ? ((navigator as any)['connection']['effectiveType'] as string) - : '', - } - - const blob = new Blob([new URLSearchParams(body).toString()], { - // This content type is necessary for `sendBeacon`: - type: 'application/x-www-form-urlencoded', - }) - const vitalsUrl = 'https://vitals.vercel-insights.com/v1/vitals' - // Navigator has to be bound to ensure it does not error in some browsers - // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch - const send = navigator.sendBeacon && navigator.sendBeacon.bind(navigator) - - function fallbackSend() { - fetch(vitalsUrl, { - body: blob, - method: 'POST', - credentials: 'omit', - keepalive: true, - // console.error is used here as when the fetch fails it does not affect functioning of the app - }).catch(console.error) - } - - try { - // If send is undefined it'll throw as well. This reduces output code size. - send!(vitalsUrl, blob) || fallbackSend() - } catch (err) { - fallbackSend() - } - } -} - -export default (onPerfEntry?: ReportCallback): void => { - if (process.env.__NEXT_ANALYTICS_ID) { - // Update function if it changes: - userReportHandler = onPerfEntry - - // Only register listeners once: - if (isRegistered) { - return - } - isRegistered = true - - const attributions: Attribution[] | undefined = process.env - .__NEXT_WEB_VITALS_ATTRIBUTION as any - - for (const webVital of WEB_VITALS) { - try { - let mod: any - - if (process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION) { - if (attributions?.includes(webVital)) { - mod = require('next/dist/compiled/web-vitals-attribution') - } - } - if (!mod) { - mod = require('next/dist/compiled/web-vitals') - } - mod[`on${webVital}`](onReport) - } catch (err) { - // Do nothing if the module fails to load - console.warn(`Failed to track ${webVital} web-vital`, err) - } - } - } -} diff --git a/packages/next/src/client/performance-relayer.ts b/packages/next/src/client/performance-relayer.ts deleted file mode 100644 index eef50765085de7..00000000000000 --- a/packages/next/src/client/performance-relayer.ts +++ /dev/null @@ -1,102 +0,0 @@ -// TODO: remove in the next major version -/* global location */ -import type { Metric, ReportCallback } from 'next/dist/compiled/web-vitals' - -// copied to prevent pulling in un-necessary utils -const WEB_VITALS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'] - -const initialHref = location.href -let isRegistered = false -let userReportHandler: ReportCallback | undefined -type Attribution = (typeof WEB_VITALS)[number] - -function onReport(metric: Metric): void { - if (userReportHandler) { - userReportHandler(metric) - } - - // This code is not shipped, executed, or present in the client-side - // JavaScript bundle unless explicitly enabled in your application. - // - // When this feature is enabled, we'll make it very clear by printing a - // message during the build (`next build`). - if ( - process.env.NODE_ENV === 'production' && - // This field is empty unless you explicitly configure it: - process.env.__NEXT_ANALYTICS_ID - ) { - const body: Record = { - dsn: process.env.__NEXT_ANALYTICS_ID, - id: metric.id, - page: window.__NEXT_DATA__?.page, - href: initialHref, - event_name: metric.name, - value: metric.value.toString(), - speed: - 'connection' in navigator && - (navigator as any)['connection'] && - 'effectiveType' in (navigator as any)['connection'] - ? ((navigator as any)['connection']['effectiveType'] as string) - : '', - } - - const blob = new Blob([new URLSearchParams(body).toString()], { - // This content type is necessary for `sendBeacon`: - type: 'application/x-www-form-urlencoded', - }) - const vitalsUrl = 'https://vitals.vercel-insights.com/v1/vitals' - // Navigator has to be bound to ensure it does not error in some browsers - // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch - const send = navigator.sendBeacon && navigator.sendBeacon.bind(navigator) - - function fallbackSend() { - fetch(vitalsUrl, { - body: blob, - method: 'POST', - credentials: 'omit', - keepalive: true, - // console.error is used here as when the fetch fails it does not affect functioning of the app - }).catch(console.error) - } - - try { - // If send is undefined it'll throw as well. This reduces output code size. - send!(vitalsUrl, blob) || fallbackSend() - } catch (err) { - fallbackSend() - } - } -} - -export default (onPerfEntry?: ReportCallback): void => { - // Update function if it changes: - userReportHandler = onPerfEntry - - // Only register listeners once: - if (isRegistered) { - return - } - isRegistered = true - - const attributions: Attribution[] | undefined = process.env - .__NEXT_WEB_VITALS_ATTRIBUTION as any - - for (const webVital of WEB_VITALS) { - try { - let mod: any - - if (process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION) { - if (attributions?.includes(webVital)) { - mod = require('next/dist/compiled/web-vitals-attribution') - } - } - if (!mod) { - mod = require('next/dist/compiled/web-vitals') - } - mod[`on${webVital}`](onReport) - } catch (err) { - // Do nothing if the module fails to load - console.warn(`Failed to track ${webVital} web-vital`, err) - } - } -} diff --git a/packages/next/src/client/script.tsx b/packages/next/src/client/script.tsx index 32f8ea719b0f37..834dc83e804a22 100644 --- a/packages/next/src/client/script.tsx +++ b/packages/next/src/client/script.tsx @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom' import React, { useEffect, useContext, useRef } from 'react' import type { ScriptHTMLAttributes } from 'react' import { HeadManagerContext } from '../shared/lib/head-manager-context.shared-runtime' -import { DOMAttributeNames } from './head-manager' +import { setAttributesFromProps } from './set-attributes-from-props' import { requestIdleCallback } from './request-idle-callback' const ScriptCache = new Map() @@ -25,16 +25,6 @@ export interface ScriptProps extends ScriptHTMLAttributes { */ export type Props = ScriptProps -const ignoreProps = [ - 'onLoad', - 'onReady', - 'dangerouslySetInnerHTML', - 'children', - 'onError', - 'strategy', - 'stylesheets', -] - const insertStylesheets = (stylesheets: string[]) => { // Case 1: Styles for afterInteractive/lazyOnload with appDir injected via handleClientScriptLoad // @@ -148,14 +138,7 @@ const loadScript = (props: ScriptProps): void => { ScriptCache.set(src, loadPromise) } - for (const [k, value] of Object.entries(props)) { - if (value === undefined || ignoreProps.includes(k)) { - continue - } - - const attr = DOMAttributeNames[k] || k.toLowerCase() - el.setAttribute(attr, value) - } + setAttributesFromProps(el, props) if (strategy === 'worker') { el.setAttribute('type', 'text/partytown') diff --git a/packages/next/src/client/set-attributes-from-props.ts b/packages/next/src/client/set-attributes-from-props.ts new file mode 100644 index 00000000000000..f5141a57d6f27a --- /dev/null +++ b/packages/next/src/client/set-attributes-from-props.ts @@ -0,0 +1,59 @@ +const DOMAttributeNames: Record = { + acceptCharset: 'accept-charset', + className: 'class', + htmlFor: 'for', + httpEquiv: 'http-equiv', + noModule: 'noModule', +} + +const ignoreProps = [ + 'onLoad', + 'onReady', + 'dangerouslySetInnerHTML', + 'children', + 'onError', + 'strategy', + 'stylesheets', +] + +function isBooleanScriptAttribute( + attr: string +): attr is 'async' | 'defer' | 'noModule' { + return ['async', 'defer', 'noModule'].includes(attr) +} + +export function setAttributesFromProps(el: HTMLElement, props: object) { + for (const [p, value] of Object.entries(props)) { + if (!props.hasOwnProperty(p)) continue + if (ignoreProps.includes(p)) continue + + // we don't render undefined props to the DOM + if (value === undefined) { + continue + } + + const attr = DOMAttributeNames[p] || p.toLowerCase() + + if (el.tagName === 'SCRIPT' && isBooleanScriptAttribute(attr)) { + // Correctly assign boolean script attributes + // https://github.com/vercel/next.js/pull/20748 + ;(el as HTMLScriptElement)[attr] = !!value + } else { + el.setAttribute(attr, String(value)) + } + + // Remove falsy non-zero boolean attributes so they are correctly interpreted + // (e.g. if we set them to false, this coerces to the string "false", which the browser interprets as true) + if ( + value === false || + (el.tagName === 'SCRIPT' && + isBooleanScriptAttribute(attr) && + (!value || value === 'false')) + ) { + // Call setAttribute before, as we need to set and unset the attribute to override force async: + // https://html.spec.whatwg.org/multipage/scripting.html#script-force-async + el.setAttribute(attr, '') + el.removeAttribute(attr) + } + } +} diff --git a/packages/next/src/experimental/testmode/playwright/step.ts b/packages/next/src/experimental/testmode/playwright/step.ts index 3a25386ec5178f..86f9e64cf598a8 100644 --- a/packages/next/src/experimental/testmode/playwright/step.ts +++ b/packages/next/src/experimental/testmode/playwright/step.ts @@ -39,7 +39,6 @@ export async function step( let result: Awaited let reportedError: any try { - console.log(props.title, props) await test.step(props.title, async () => { result = await handler(({ error }) => { reportedError = error diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index ae1f9926b7103b..cfd84f9118b1b3 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -18,6 +18,7 @@ import { import { hasNextSupport } from '../../telemetry/ci-info' import { lazyRenderAppPage } from '../../server/future/route-modules/app-page/module.render' import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' +import { NodeNextRequest, NodeNextResponse } from '../../server/base-http/node' export const enum ExportedAppPageFiles { HTML = 'HTML', @@ -50,8 +51,8 @@ export async function exportAppPage( try { const result = await lazyRenderAppPage( - req, - res, + new NodeNextRequest(req), + new NodeNextResponse(res), pathname, query, renderOpts diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index 7c8c320e256b09..11a1d792ec2435 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -1,4 +1,4 @@ -import type { ServerRuntime } from '../../types' +import type { ServerRuntime } from '../types' export const NEXT_QUERY_PARAM_PREFIX = 'nxtP' @@ -8,6 +8,7 @@ export const PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER = export const RSC_PREFETCH_SUFFIX = '.prefetch.rsc' export const RSC_SUFFIX = '.rsc' +export const ACTION_SUFFIX = '.action' export const NEXT_DATA_SUFFIX = '.json' export const NEXT_META_SUFFIX = '.meta' export const NEXT_BODY_SUFFIX = '.body' @@ -170,7 +171,6 @@ const WEBPACK_LAYERS = { clientOnly: [ WEBPACK_LAYERS_NAMES.serverSideRendering, WEBPACK_LAYERS_NAMES.appPagesBrowser, - WEBPACK_LAYERS_NAMES.shared, ], nonClientServerTarget: [ // middleware and pages api diff --git a/packages/next/src/lib/helpers/install.ts b/packages/next/src/lib/helpers/install.ts index 8a3666139b3983..b6cc20c92918d5 100644 --- a/packages/next/src/lib/helpers/install.ts +++ b/packages/next/src/lib/helpers/install.ts @@ -1,15 +1,14 @@ import { yellow } from '../picocolors' import spawn from 'next/dist/compiled/cross-spawn' - -export type PackageManager = 'npm' | 'pnpm' | 'yarn' +import type { PackageManager } from './get-pkg-manager' interface InstallArgs { /** - * Indicate whether to install packages using npm, pnpm or Yarn. + * Indicate whether to install packages using npm, pnpm, or yarn. */ packageManager: PackageManager /** - * Indicate whether there is an active Internet connection. + * Indicate whether there is an active internet connection. */ isOnline: boolean /** @@ -19,81 +18,52 @@ interface InstallArgs { } /** - * Spawn a package manager installation with either Yarn or NPM. + * Spawn a package manager installation with either npm, pnpm, or yarn. * * @returns A Promise that resolves once the installation is finished. */ export function install( root: string, - dependencies: string[] | null, + dependencies: string[], { packageManager, isOnline, devDependencies }: InstallArgs ): Promise { - /** - * (p)npm-specific command-line flags. - */ - const npmFlags: string[] = [] - /** - * Yarn-specific command-line flags. - */ - const yarnFlags: string[] = [] - /** - * Return a Promise that resolves once the installation is finished. - */ - return new Promise((resolve, reject) => { - let args: string[] - let command = packageManager - const useYarn = packageManager === 'yarn' + let args: string[] = [] - if (dependencies && dependencies.length) { - /** - * If there are dependencies, run a variation of `{packageManager} add`. - */ - if (useYarn) { - /** - * Call `yarn add --exact (--offline)? (-D)? ...`. - */ - args = ['add', '--exact'] - if (!isOnline) args.push('--offline') - args.push('--cwd', root) - if (devDependencies) args.push('--dev') - args.push(...dependencies) - } else { - /** - * Call `(p)npm install [--save|--save-dev] ...`. - */ - args = ['install', '--save-exact'] - args.push(devDependencies ? '--save-dev' : '--save') - args.push(...dependencies) - } + if (dependencies.length > 0) { + if (packageManager === 'yarn') { + args = ['add', '--exact'] + if (devDependencies) args.push('--dev') + } else if (packageManager === 'pnpm') { + args = ['add', '--save-exact'] + args.push(devDependencies ? '--save-dev' : '--save-prod') } else { - /** - * If there are no dependencies, run a variation of `{packageManager} - * install`. - */ - args = ['install'] - if (!isOnline) { - console.log(yellow('You appear to be offline.')) - if (useYarn) { - console.log(yellow('Falling back to the local Yarn cache.')) - console.log() - args.push('--offline') - } else { - console.log() - } - } + // npm + args = ['install', '--save-exact'] + args.push(devDependencies ? '--save-dev' : '--save') } - /** - * Add any package manager-specific flags. - */ - if (useYarn) { - args.push(...yarnFlags) - } else { - args.push(...npmFlags) + + args.push(...dependencies) + } else { + args = ['install'] // npm, pnpm, and yarn all support `install` + + if (!isOnline) { + args.push('--offline') + console.log(yellow('You appear to be offline.')) + if (packageManager !== 'npm') { + console.log( + yellow(`Falling back to the local ${packageManager} cache.`) + ) + } + console.log() } + } + + return new Promise((resolve, reject) => { /** * Spawn the installation process. */ - const child = spawn(command, args, { + const child = spawn(packageManager, args, { + cwd: root, stdio: 'inherit', env: { ...process.env, @@ -106,7 +76,7 @@ export function install( }) child.on('close', (code) => { if (code !== 0) { - reject({ command: `${command} ${args.join(' ')}` }) + reject({ command: `${packageManager} ${args.join(' ')}` }) return } resolve() diff --git a/packages/next/src/lib/is-edge-runtime.ts b/packages/next/src/lib/is-edge-runtime.ts index 486f73006d2f5c..5e3705e2306615 100644 --- a/packages/next/src/lib/is-edge-runtime.ts +++ b/packages/next/src/lib/is-edge-runtime.ts @@ -1,4 +1,4 @@ -import type { ServerRuntime } from '../../types' +import type { ServerRuntime } from '../types' import { SERVER_RUNTIME } from './constants' export function isEdgeRuntime(value?: string): value is ServerRuntime { diff --git a/packages/next/src/lib/memory/startup.ts b/packages/next/src/lib/memory/startup.ts index b9a5c5fa6aca48..b18a6a0f4cd98b 100644 --- a/packages/next/src/lib/memory/startup.ts +++ b/packages/next/src/lib/memory/startup.ts @@ -9,7 +9,6 @@ export function enableMemoryDebuggingMode(): void { // memory limit. It does not give any warning to the user though which // can be jarring. If memory is large, this may take a long time. if ('setHeapSnapshotNearHeapLimit' in v8) { - // @ts-expect-error - this method exists since Node 16. v8.setHeapSnapshotNearHeapLimit(1) } diff --git a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts index dae6fc94ddbbc9..61209b8464d06b 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts @@ -96,24 +96,20 @@ export function resolveImages( return nonNullableImages } +const ogTypeToFields: Record = { + article: OgTypeFields.article, + book: OgTypeFields.article, + 'music.song': OgTypeFields.song, + 'music.album': OgTypeFields.song, + 'music.playlist': OgTypeFields.playlist, + 'music.radio_station': OgTypeFields.radio, + 'video.movie': OgTypeFields.video, + 'video.episode': OgTypeFields.video, +} + function getFieldsByOgType(ogType: OpenGraphType | undefined) { - switch (ogType) { - case 'article': - case 'book': - return OgTypeFields.article - case 'music.song': - case 'music.album': - return OgTypeFields.song - case 'music.playlist': - return OgTypeFields.playlist - case 'music.radio_station': - return OgTypeFields.radio - case 'video.movie': - case 'video.episode': - return OgTypeFields.video - default: - return OgTypeFields.basic - } + if (!ogType || !(ogType in ogTypeToFields)) return OgTypeFields.basic + return ogTypeToFields[ogType].concat(OgTypeFields.basic) } function validateResolvedImageUrl( diff --git a/packages/next/src/lib/server-external-packages.json b/packages/next/src/lib/server-external-packages.json index dfcf2686b3e1cf..1d6e8abd797322 100644 --- a/packages/next/src/lib/server-external-packages.json +++ b/packages/next/src/lib/server-external-packages.json @@ -1,4 +1,5 @@ [ + "@appsignal/nodejs", "@aws-sdk/client-s3", "@aws-sdk/s3-presigned-post", "@blockfrost/blockfrost-js", diff --git a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts index ef0718fab1b6f4..607bf0d501ed43 100644 --- a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts @@ -9,7 +9,7 @@ import * as Log from '../../build/output/log' type DesiredCompilerOptionsShape = { [K in keyof CompilerOptions]: - | { suggested: any } + | { suggested: any; reason?: string } | { parsedValue?: any parsedValues?: Array @@ -23,6 +23,11 @@ function getDesiredCompilerOptions( tsOptions?: CompilerOptions ): DesiredCompilerOptionsShape { const o: DesiredCompilerOptionsShape = { + target: { + suggested: 'ES2017', + reason: + 'For top-level `await`. Note: Next.js only polyfills for the esmodules target.', + }, // These are suggested values and will be set when not present in the // tsconfig.json lib: { suggested: ['dom', 'dom.iterable', 'esnext'] }, @@ -168,7 +173,12 @@ export async function writeConfigurationDefaults( } userTsConfig.compilerOptions[optionKey] = check.suggested suggestedActions.push( - cyan(optionKey) + ' was set to ' + bold(check.suggested) + cyan(optionKey) + + ' was set to ' + + bold(check.suggested) + + check.reason + ? ` (${check.reason})` + : '' ) } } else if ('value' in check) { diff --git a/packages/next/src/server/api-utils/node/api-resolver.ts b/packages/next/src/server/api-utils/node/api-resolver.ts index 614c8eee438796..8f85fb8202c552 100644 --- a/packages/next/src/server/api-utils/node/api-resolver.ts +++ b/packages/next/src/server/api-utils/node/api-resolver.ts @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from 'http' import type { NextApiRequest, NextApiResponse } from '../../../shared/lib/utils' -import type { PageConfig, ResponseLimit } from 'next/types' +import type { PageConfig, ResponseLimit } from '../../../types' import type { __ApiPreviewProps } from '../.' import type { CookieSerializeOptions } from 'next/dist/compiled/cookie' diff --git a/packages/next/src/server/api-utils/node/parse-body.ts b/packages/next/src/server/api-utils/node/parse-body.ts index e63918e4630e65..ac9d46a4078c56 100644 --- a/packages/next/src/server/api-utils/node/parse-body.ts +++ b/packages/next/src/server/api-utils/node/parse-body.ts @@ -1,8 +1,8 @@ import type { IncomingMessage } from 'http' -import type { SizeLimit } from 'next/types' import { parse } from 'next/dist/compiled/content-type' import isError from '../../../lib/is-error' +import type { SizeLimit } from '../../../types' import { ApiError } from '../index' /** diff --git a/packages/next/src/server/api-utils/node/try-get-preview-data.ts b/packages/next/src/server/api-utils/node/try-get-preview-data.ts index ca81d1379e0bbf..6140bd2e9f3d59 100644 --- a/packages/next/src/server/api-utils/node/try-get-preview-data.ts +++ b/packages/next/src/server/api-utils/node/try-get-preview-data.ts @@ -3,7 +3,7 @@ import type { NextApiResponse } from '../../../shared/lib/utils' import { checkIsOnDemandRevalidate } from '../.' import type { __ApiPreviewProps } from '../.' import type { BaseNextRequest, BaseNextResponse } from '../../base-http' -import type { PreviewData } from 'next/types' +import type { PreviewData } from '../../../types' import { clearPreviewData, diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index 55c0234ec89333..80f373f2777e78 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -1,14 +1,9 @@ -import type { - IncomingHttpHeaders, - IncomingMessage, - OutgoingHttpHeaders, - ServerResponse, -} from 'http' -import type { WebNextRequest } from '../base-http/web' -import type { SizeLimit } from '../../../types' +import type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http' +import type { SizeLimit } from '../../types' import type { RequestStore } from '../../client/components/request-async-storage.external' import type { AppRenderContext, GenerateFlight } from './app-render' import type { AppPageModule } from '../../server/future/route-modules/app-page/module' +import type { BaseNextRequest, BaseNextResponse } from '../base-http' import { RSC_HEADER, @@ -43,6 +38,7 @@ import { RequestCookies, ResponseCookies } from '../web/spec-extension/cookies' import { HeadersAdapter } from '../web/spec-extension/adapters/headers' import { fromNodeOutgoingHttpHeaders } from '../web/utils' import { selectWorkerForForwarding } from './action-utils' +import { isNodeNextRequest, isWebNextRequest } from '../base-http/helpers' function formDataFromSearchQueryString(query: string) { const searchParams = new URLSearchParams(query) @@ -66,8 +62,8 @@ function nodeHeadersToRecord( } function getForwardedHeaders( - req: IncomingMessage, - res: ServerResponse + req: BaseNextRequest, + res: BaseNextResponse ): Headers { // Get request headers and cookies const requestHeaders = req.headers @@ -108,7 +104,7 @@ function getForwardedHeaders( } async function addRevalidationHeader( - res: ServerResponse, + res: BaseNextResponse, { staticGenerationStore, requestStore, @@ -151,8 +147,8 @@ async function addRevalidationHeader( * Forwards a server action request to a separate worker. Used when the requested action is not available in the current worker. */ async function createForwardedActionResponse( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, host: Host, workerPathname: string, basePath: string, @@ -181,35 +177,33 @@ async function createForwardedActionResponse( const fetchUrl = new URL(`${origin}${basePath}${workerPathname}`) try { - let readableStream: ReadableStream | undefined - if (process.env.NEXT_RUNTIME === 'edge') { - const webRequest = req as unknown as WebNextRequest - if (!webRequest.body) { - throw new Error('invariant: Missing request body.') + let body: BodyInit | AsyncIterable | undefined + if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME === 'edge' && + isWebNextRequest(req) + ) { + if (!req.body) { + throw new Error('Invariant: missing request body.') } - readableStream = webRequest.body + body = req.body + } else if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME !== 'edge' && + isNodeNextRequest(req) + ) { + body = req.stream() } else { - // Convert the Node.js readable stream to a Web Stream. - readableStream = new ReadableStream({ - start(controller) { - req.on('data', (chunk) => { - controller.enqueue(new Uint8Array(chunk)) - }) - req.on('end', () => { - controller.close() - }) - req.on('error', (err) => { - controller.error(err) - }) - }, - }) + throw new Error('Invariant: Unknown request type.') } // Forward the request to the new worker const response = await fetch(fetchUrl, { method: 'POST', - body: readableStream, + body, duplex: 'half', headers: forwardedHeaders, next: { @@ -238,8 +232,8 @@ async function createForwardedActionResponse( } async function createRedirectRenderResult( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, originalHost: Host, redirectUrl: string, basePath: string, @@ -359,6 +353,11 @@ type ServerModuleMap = Record< | undefined > +type ServerActionsConfig = { + bodySizeLimit?: SizeLimit + allowedOrigins?: string[] +} + export async function handleAction({ req, res, @@ -370,17 +369,14 @@ export async function handleAction({ serverActions, ctx, }: { - req: IncomingMessage - res: ServerResponse + req: BaseNextRequest + res: BaseNextResponse ComponentMod: AppPageModule serverModuleMap: ServerModuleMap generateFlight: GenerateFlight staticGenerationStore: StaticGenerationStore requestStore: RequestStore - serverActions?: { - bodySizeLimit?: SizeLimit - allowedOrigins?: string[] - } + serverActions?: ServerActionsConfig ctx: AppRenderContext }): Promise< | undefined @@ -550,18 +546,23 @@ export async function handleAction({ try { await actionAsyncStorage.run({ isAction: true }, async () => { - if (process.env.NEXT_RUNTIME === 'edge') { + if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME === 'edge' && + isWebNextRequest(req) + ) { // Use react-server-dom-webpack/server.edge const { decodeReply, decodeAction, decodeFormState } = ComponentMod - - const webRequest = req as unknown as WebNextRequest - if (!webRequest.body) { + if (!req.body) { throw new Error('invariant: Missing request body.') } + // TODO: add body limit + if (isMultipartAction) { // TODO-APP: Add streaming support - const formData = await webRequest.request.formData() + const formData = await req.request.formData() if (isFetchAction) { bound = await decodeReply(formData, serverModuleMap) } else { @@ -590,7 +591,7 @@ export async function handleAction({ let actionData = '' - const reader = webRequest.body.getReader() + const reader = req.body.getReader() while (true) { const { done, value } = await reader.read() if (done) { @@ -607,7 +608,12 @@ export async function handleAction({ bound = await decodeReply(actionData, serverModuleMap) } } - } else { + } else if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME !== 'edge' && + isNodeNextRequest(req) + ) { // Use react-server-dom-webpack/server.node which supports streaming const { decodeReply, @@ -616,43 +622,72 @@ export async function handleAction({ decodeFormState, } = require(`./react-server.node`) + const { Transform } = + require('node:stream') as typeof import('node:stream') + + const defaultBodySizeLimit = '1 MB' + const bodySizeLimit = + serverActions?.bodySizeLimit ?? defaultBodySizeLimit + const bodySizeLimitBytes = + bodySizeLimit !== defaultBodySizeLimit + ? ( + require('next/dist/compiled/bytes') as typeof import('bytes') + ).parse(bodySizeLimit) + : 1024 * 1024 // 1 MB + + let size = 0 + const body = req.body.pipe( + new Transform({ + transform(chunk, encoding, callback) { + size += Buffer.byteLength(chunk, encoding) + if (size > bodySizeLimitBytes) { + const { ApiError } = require('../api-utils') + + callback( + new ApiError( + 413, + `Body exceeded ${bodySizeLimit} limit. + To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit` + ) + ) + return + } + + callback(null, chunk) + }, + }) + ) + if (isMultipartAction) { if (isFetchAction) { - const readableLimit = serverActions?.bodySizeLimit ?? '1 MB' - const limit = require('next/dist/compiled/bytes').parse( - readableLimit - ) - const busboy = require('busboy') - const bb = busboy({ + const busboy = (require('busboy') as typeof import('busboy'))({ headers: req.headers, - limits: { fieldSize: limit }, + limits: { fieldSize: bodySizeLimitBytes }, }) - req.pipe(bb) - bound = await decodeReplyFromBusboy(bb, serverModuleMap) - } else { - // Convert the Node.js readable stream to a Web Stream. - const readableStream = new ReadableStream({ - start(controller) { - req.on('data', (chunk) => { - controller.enqueue(new Uint8Array(chunk)) - }) - req.on('end', () => { - controller.close() - }) - req.on('error', (err) => { - controller.error(err) - }) - }, - }) + body.pipe(busboy) + bound = await decodeReplyFromBusboy(busboy, serverModuleMap) + } else { // React doesn't yet publish a busboy version of decodeAction // so we polyfill the parsing of FormData. const fakeRequest = new Request('http://localhost', { method: 'POST', // @ts-expect-error headers: { 'Content-Type': contentType }, - body: readableStream, + body: new ReadableStream({ + start: (controller) => { + body.on('data', (chunk) => { + controller.enqueue(new Uint8Array(chunk)) + }) + body.on('end', () => { + controller.close() + }) + body.on('error', (err) => { + controller.error(err) + }) + }, + }), duplex: 'half', }) const formData = await fakeRequest.formData() @@ -679,26 +714,13 @@ export async function handleAction({ } } - const chunks = [] - - for await (const chunk of req) { + const chunks: Buffer[] = [] + for await (const chunk of req.body) { chunks.push(Buffer.from(chunk)) } const actionData = Buffer.concat(chunks).toString('utf-8') - const readableLimit = serverActions?.bodySizeLimit ?? '1 MB' - const limit = require('next/dist/compiled/bytes').parse(readableLimit) - - if (actionData.length > limit) { - const { ApiError } = require('../api-utils') - throw new ApiError( - 413, - `Body exceeded ${readableLimit} limit. -To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit` - ) - } - if (isURLEncodedAction) { const formData = formDataFromSearchQueryString(actionData) bound = await decodeReply(formData, serverModuleMap) @@ -706,6 +728,8 @@ To configure the body size limit for Server Actions, see: https://nextjs.org/doc bound = await decodeReply(actionData, serverModuleMap) } } + } else { + throw new Error('Invariant: Unknown request type.') } // actions.js diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 81c437be3d6a35..a1ed364d107486 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -1,4 +1,3 @@ -import type { IncomingMessage, ServerResponse } from 'http' import type { ActionResult, DynamicParamTypesShort, @@ -16,6 +15,8 @@ import type { LoaderTree } from '../lib/app-dir-module' import type { AppPageModule } from '../future/route-modules/app-page/module' import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin' import type { Revalidate } from '../lib/revalidate' +import type { DeepReadonly } from '../../shared/lib/deep-readonly' +import type { BaseNextRequest, BaseNextResponse } from '../base-http' import React from 'react' @@ -107,7 +108,7 @@ import { wrapClientComponentLoader, } from '../client-component-renderer-logger' import { createServerModuleMap } from './action-utils' -import type { DeepReadonly } from '../../shared/lib/deep-readonly' +import { isNodeNextRequest } from '../base-http/helpers' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -143,7 +144,7 @@ export type AppRenderContext = AppRenderBaseContext & { flightDataRendererErrorHandler: ErrorHandler serverComponentsErrorHandler: ErrorHandler isNotFoundPath: boolean - res: ServerResponse + res: BaseNextResponse } function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree { @@ -458,9 +459,10 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { couldBeIntercepted={couldBeIntercepted} initialHead={ <> - {ctx.res.statusCode > 400 && ( - - )} + {typeof ctx.res.statusCode === 'number' && + ctx.res.statusCode > 400 && ( + + )} {/* Adding requestId as react key to make metadata remount for each render */} @@ -514,7 +516,9 @@ async function ReactServerError({ <> {/* Adding requestId as react key to make metadata remount for each render */} - {res.statusCode >= 400 && } + {typeof res.statusCode === 'number' && res.statusCode >= 400 && ( + + )} {process.env.NODE_ENV === 'development' && ( )} @@ -581,12 +585,13 @@ function ReactServerEntrypoint({ export type BinaryStreamOf = ReadableStream async function renderToHTMLOrFlightImpl( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pagePath: string, query: NextParsedUrlQuery, renderOpts: RenderOpts, - baseCtx: AppRenderBaseContext + baseCtx: AppRenderBaseContext, + requestEndedState: { ended?: boolean } ) { const isNotFoundPath = pagePath === '/404' @@ -620,8 +625,15 @@ async function renderToHTMLOrFlightImpl( globalThis.__next_chunk_load__ = instrumented.loadChunk } - if (typeof req.on === 'function') { - req.on('end', () => { + if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME !== 'edge' && + isNodeNextRequest(req) + ) { + req.originalRequest.on('end', () => { + requestEndedState.ended = true + if ('performance' in globalThis) { const metrics = getClientComponentLoaderMetrics({ reset: true }) if (metrics) { @@ -1419,8 +1431,8 @@ async function renderToHTMLOrFlightImpl( } export type AppPageRender = ( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, pagePath: string, query: NextParsedUrlQuery, renderOpts: RenderOpts @@ -1445,14 +1457,23 @@ export const renderToHTMLOrFlight: AppPageRender = ( { urlPathname: pathname, renderOpts, + requestEndedState: { ended: false }, }, (staticGenerationStore) => - renderToHTMLOrFlightImpl(req, res, pagePath, query, renderOpts, { - requestStore, - staticGenerationStore, - componentMod: renderOpts.ComponentMod, + renderToHTMLOrFlightImpl( + req, + res, + pagePath, + query, renderOpts, - }) + { + requestStore, + staticGenerationStore, + componentMod: renderOpts.ComponentMod, + renderOpts, + }, + staticGenerationStore.requestEndedState || {} + ) ) ) } diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 148d095309f356..e27f807c16ff31 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -126,7 +126,7 @@ export function trackDynamicDataAccessed( const pathname = getPathname(store.urlPathname) if (store.isUnstableCacheCallback) { throw new Error( - `Route ${pathname} used "${expression}" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" oustide of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` + `Route ${pathname} used "${expression}" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` ) } else if (store.dynamicShouldError) { throw new StaticGenBailoutError( diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 8b31319571fa49..d145661053ee2c 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -1,5 +1,5 @@ import type { LoadComponentsReturnType } from '../load-components' -import type { ServerRuntime, SizeLimit } from '../../../types' +import type { ServerRuntime, SizeLimit } from '../../types' import type { NextConfigComplete } from '../../server/config-shared' import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin' import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin' diff --git a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts index 2f11e7cf850973..bbe9d42e1bb860 100644 --- a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts @@ -9,6 +9,7 @@ import type { FetchMetric } from '../base-http' export type StaticGenerationContext = { urlPathname: string + requestEndedState?: { ended?: boolean } renderOpts: { incrementalCache?: IncrementalCache isOnDemandRevalidate?: boolean @@ -50,7 +51,7 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper< > = { wrap( storage: AsyncLocalStorage, - { urlPathname, renderOpts }: StaticGenerationContext, + { urlPathname, renderOpts, requestEndedState }: StaticGenerationContext, callback: (store: StaticGenerationStore) => Result ): Result { /** @@ -96,6 +97,7 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper< isDraftMode: renderOpts.isDraftMode, prerenderState, + requestEndedState, } // TODO: remove this when we resolve accessing the store outside the execution context diff --git a/packages/next/src/server/base-http/helpers.ts b/packages/next/src/server/base-http/helpers.ts new file mode 100644 index 00000000000000..bbea5c7bbcf3d2 --- /dev/null +++ b/packages/next/src/server/base-http/helpers.ts @@ -0,0 +1,49 @@ +import type { BaseNextRequest, BaseNextResponse } from './' +import type { NodeNextRequest, NodeNextResponse } from './node' +import type { WebNextRequest, WebNextResponse } from './web' + +/** + * This file provides some helpers that should be used in conjunction with + * explicit environment checks. When combined with the environment checks, it + * will ensure that the correct typings are used as well as enable code + * elimination. + */ + +/** + * Type guard to determine if a request is a WebNextRequest. This does not + * actually check the type of the request, but rather the runtime environment. + * It's expected that when the runtime environment is the edge runtime, that any + * base request is a WebNextRequest. + */ +export const isWebNextRequest = (req: BaseNextRequest): req is WebNextRequest => + process.env.NEXT_RUNTIME === 'edge' + +/** + * Type guard to determine if a response is a WebNextResponse. This does not + * actually check the type of the response, but rather the runtime environment. + * It's expected that when the runtime environment is the edge runtime, that any + * base response is a WebNextResponse. + */ +export const isWebNextResponse = ( + res: BaseNextResponse +): res is WebNextResponse => process.env.NEXT_RUNTIME === 'edge' + +/** + * Type guard to determine if a request is a NodeNextRequest. This does not + * actually check the type of the request, but rather the runtime environment. + * It's expected that when the runtime environment is the node runtime, that any + * base request is a NodeNextRequest. + */ +export const isNodeNextRequest = ( + req: BaseNextRequest +): req is NodeNextRequest => process.env.NEXT_RUNTIME !== 'edge' + +/** + * Type guard to determine if a response is a NodeNextResponse. This does not + * actually check the type of the response, but rather the runtime environment. + * It's expected that when the runtime environment is the node runtime, that any + * base response is a NodeNextResponse. + */ +export const isNodeNextResponse = ( + res: BaseNextResponse +): res is NodeNextResponse => process.env.NEXT_RUNTIME !== 'edge' diff --git a/packages/next/src/server/base-http/index.ts b/packages/next/src/server/base-http/index.ts index af72b338cd9e94..86c384184d5706 100644 --- a/packages/next/src/server/base-http/index.ts +++ b/packages/next/src/server/base-http/index.ts @@ -81,7 +81,7 @@ export abstract class BaseNextResponse { // Utils implemented using the abstract methods above - redirect(destination: string, statusCode: number) { + public redirect(destination: string, statusCode: number) { this.setHeader('Location', destination) this.statusCode = statusCode @@ -90,6 +90,7 @@ export abstract class BaseNextResponse { if (statusCode === RedirectStatusCode.PermanentRedirect) { this.setHeader('Refresh', `0;url=${destination}`) } + return this } } diff --git a/packages/next/src/server/base-http/node.ts b/packages/next/src/server/base-http/node.ts index 1b7ed2953d06c8..fe41e84182a997 100644 --- a/packages/next/src/server/base-http/node.ts +++ b/packages/next/src/server/base-http/node.ts @@ -18,10 +18,14 @@ type Req = IncomingMessage & { export class NodeNextRequest extends BaseNextRequest { public headers = this._req.headers - public fetchMetrics?: FetchMetric[] = this._req?.fetchMetrics; + public fetchMetrics: FetchMetric[] | undefined = this._req?.fetchMetrics; [NEXT_REQUEST_META]: RequestMeta = this._req[NEXT_REQUEST_META] || {} + constructor(private _req: Req) { + super(_req.method!.toUpperCase(), _req.url!, _req) + } + get originalRequest() { // Need to mimic these changes to the original req object for places where we use it: // render.tsx, api/ssg requests @@ -35,8 +39,36 @@ export class NodeNextRequest extends BaseNextRequest { this._req = value } - constructor(private _req: Req) { - super(_req.method!.toUpperCase(), _req.url!, _req) + private streaming = false + + /** + * Returns the request body as a Web Readable Stream. The body here can only + * be read once as the body will start flowing as soon as the data handler + * is attached. + * + * @internal + */ + public stream() { + if (this.streaming) { + throw new Error( + 'Invariant: NodeNextRequest.stream() can only be called once' + ) + } + this.streaming = true + + return new ReadableStream({ + start: (controller) => { + this._req.on('data', (chunk) => { + controller.enqueue(new Uint8Array(chunk)) + }) + this._req.on('end', () => { + controller.close() + }) + this._req.on('error', (err) => { + controller.error(err) + }) + }, + }) } } diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index b84ff961a94eef..081bdd2ced81cc 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -20,7 +20,7 @@ import { normalizeRepeatedSlashes, MissingStaticPage, } from '../shared/lib/utils' -import type { PreviewData } from 'next/types' +import type { PreviewData } from '../types' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { BaseNextRequest, BaseNextResponse } from './base-http' import type { @@ -31,11 +31,13 @@ import type { import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin' import type { NextFontManifest } from '../build/webpack/plugins/next-font-manifest-plugin' import type { AppPageRouteModule } from './future/route-modules/app-page/module' -import type { NodeNextRequest, NodeNextResponse } from './base-http/node' -import type { WebNextRequest, WebNextResponse } from './base-http/web' import type { PagesAPIRouteMatch } from './future/route-matches/pages-api-route-match' import type { AppRouteRouteHandlerContext } from './future/route-modules/app-route/module' -import type { Server as HTTPServer, IncomingMessage } from 'http' +import type { + Server as HTTPServer, + IncomingMessage, + ServerResponse as HTTPServerResponse, +} from 'http' import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info' import type { TLSSocket } from 'tls' import type { PathnameNormalizer } from './future/normalizers/request/pathname-normalizer' @@ -100,7 +102,7 @@ import { AppRouteRouteMatcherProvider } from './future/route-matcher-providers/a import { PagesAPIRouteMatcherProvider } from './future/route-matcher-providers/pages-api-route-matcher-provider' import { PagesRouteMatcherProvider } from './future/route-matcher-providers/pages-route-matcher-provider' import { ServerManifestLoader } from './future/route-matcher-providers/helpers/manifest-loaders/server-manifest-loader' -import { getTracer, SpanKind } from './lib/trace/tracer' +import { getTracer, isBubbledError, SpanKind } from './lib/trace/tracer' import { BaseServerSpan } from './lib/trace/constants' import { I18NProvider } from './future/helpers/i18n-provider' import { sendResponse } from './send-response' @@ -124,6 +126,7 @@ import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-ass import { stripInternalHeaders } from './internal-utils' import { RSCPathnameNormalizer } from './future/normalizers/request/rsc' import { PostponedPathnameNormalizer } from './future/normalizers/request/postponed' +import { ActionPathnameNormalizer } from './future/normalizers/request/action' import { stripFlightHeaders } from './app-render/strip-flight-headers' import { isAppPageRouteModule, @@ -136,6 +139,8 @@ import { getIsServerAction } from './lib/server-action-request-meta' import { isInterceptionRouteAppPath } from './future/helpers/interception-routes' import { toRoute } from './lib/to-route' import type { DeepReadonly } from '../shared/lib/deep-readonly' +import { isNodeNextRequest, isNodeNextResponse } from './base-http/helpers' +import { patchSetHeaderWithCookieSupport } from './lib/patch-set-header' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -148,9 +153,12 @@ export interface MiddlewareRoutingItem { matchers?: MiddlewareMatcher[] } -export type RouteHandler = ( - req: BaseNextRequest, - res: BaseNextResponse, +export type RouteHandler< + ServerRequest extends BaseNextRequest = BaseNextRequest, + ServerResponse extends BaseNextResponse = BaseNextResponse +> = ( + req: ServerRequest, + res: ServerResponse, parsedUrl: NextUrlWithParsedQuery ) => PromiseLike | boolean @@ -227,17 +235,31 @@ type BaseRenderOpts = RenderOpts & { previewProps: __ApiPreviewProps } -export interface BaseRequestHandler { +/** + * The public interface for rendering with the server programmatically. This + * would typically only allow the base request or response to extend it, but + * because this can be programmatically accessed, we assume that it could also + * be the base Node.js request and response types. + */ +export interface BaseRequestHandler< + ServerRequest extends BaseNextRequest | IncomingMessage = BaseNextRequest, + ServerResponse extends + | BaseNextResponse + | HTTPServerResponse = BaseNextResponse +> { ( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, parsedUrl?: NextUrlWithParsedQuery | undefined ): Promise | void } -export type RequestContext = { - req: BaseNextRequest - res: BaseNextResponse +export type RequestContext< + ServerRequest extends BaseNextRequest = BaseNextRequest, + ServerResponse extends BaseNextResponse = BaseNextResponse +> = { + req: ServerRequest + res: ServerResponse pathname: string query: NextParsedUrlQuery renderOpts: RenderOpts @@ -269,7 +291,11 @@ export type NextEnabledDirectories = { readonly app: boolean } -export default abstract class Server { +export default abstract class Server< + ServerOptions extends Options = Options, + ServerRequest extends BaseNextRequest = BaseNextRequest, + ServerResponse extends BaseNextResponse = BaseNextResponse +> { public readonly hostname?: string public readonly fetchHostname?: string public readonly port?: number @@ -321,15 +347,15 @@ export default abstract class Server { | DeepReadonly | undefined protected abstract attachRequestMeta( - req: BaseNextRequest, + req: ServerRequest, parsedUrl: NextUrlWithParsedQuery ): void protected abstract getFallback(page: string): Promise protected abstract hasPage(pathname: string): Promise protected abstract sendRenderResult( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, options: { result: RenderResult type: 'html' | 'json' | 'rsc' @@ -341,15 +367,15 @@ export default abstract class Server { ): Promise protected abstract runApi( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, query: ParsedUrlQuery, match: PagesAPIRouteMatch ): Promise protected abstract renderHTML( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: NextParsedUrlQuery, renderOpts: LoadedRenderOpts @@ -377,6 +403,7 @@ export default abstract class Server { protected readonly localeNormalizer?: LocaleRouteNormalizer protected readonly normalizers: { + readonly action: ActionPathnameNormalizer | undefined readonly postponed: PostponedPathnameNormalizer | undefined readonly rsc: RSCPathnameNormalizer | undefined readonly prefetchRSC: PrefetchRSCPathnameNormalizer | undefined @@ -471,6 +498,10 @@ export default abstract class Server { data: this.enabledDirectories.pages ? new NextDataPathnameNormalizer(this.buildId) : undefined, + action: + this.enabledDirectories.app && this.minimalMode + ? new ActionPathnameNormalizer() + : undefined, } this.nextFontManifest = this.getNextFontManifest() @@ -559,7 +590,11 @@ export default abstract class Server { return this.matchers.reload() } - private handleRSCRequest: RouteHandler = (req, _res, parsedUrl) => { + private handleRSCRequest: RouteHandler = ( + req, + _res, + parsedUrl + ) => { if (!parsedUrl.pathname) return false if (this.normalizers.prefetchRSC?.match(parsedUrl.pathname)) { @@ -608,102 +643,114 @@ export default abstract class Server { return false } - private handleNextDataRequest: RouteHandler = async (req, res, parsedUrl) => { - const middleware = this.getMiddleware() - const params = matchNextDataPathname(parsedUrl.pathname) + private handleNextDataRequest: RouteHandler = + async (req, res, parsedUrl) => { + const middleware = this.getMiddleware() + const params = matchNextDataPathname(parsedUrl.pathname) - // ignore for non-next data URLs - if (!params || !params.path) { - return false - } - - if (params.path[0] !== this.buildId) { - // Ignore if its a middleware request when we aren't on edge. - if ( - process.env.NEXT_RUNTIME !== 'edge' && - req.headers['x-middleware-invoke'] - ) { + // ignore for non-next data URLs + if (!params || !params.path) { return false } - // Make sure to 404 if the buildId isn't correct - await this.render404(req, res, parsedUrl) - return true - } - - // remove buildId from URL - params.path.shift() + if (params.path[0] !== this.buildId) { + // Ignore if its a middleware request when we aren't on edge. + if ( + process.env.NEXT_RUNTIME !== 'edge' && + req.headers['x-middleware-invoke'] + ) { + return false + } - const lastParam = params.path[params.path.length - 1] + // Make sure to 404 if the buildId isn't correct + await this.render404(req, res, parsedUrl) + return true + } - // show 404 if it doesn't end with .json - if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) { - await this.render404(req, res, parsedUrl) - return true - } + // remove buildId from URL + params.path.shift() - // re-create page's pathname - let pathname = `/${params.path.join('/')}` - pathname = getRouteFromAssetPath(pathname, '.json') + const lastParam = params.path[params.path.length - 1] - // ensure trailing slash is normalized per config - if (middleware) { - if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) { - pathname += '/' + // show 404 if it doesn't end with .json + if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) { + await this.render404(req, res, parsedUrl) + return true } - if ( - !this.nextConfig.trailingSlash && - pathname.length > 1 && - pathname.endsWith('/') - ) { - pathname = pathname.substring(0, pathname.length - 1) + + // re-create page's pathname + let pathname = `/${params.path.join('/')}` + pathname = getRouteFromAssetPath(pathname, '.json') + + // ensure trailing slash is normalized per config + if (middleware) { + if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) { + pathname += '/' + } + if ( + !this.nextConfig.trailingSlash && + pathname.length > 1 && + pathname.endsWith('/') + ) { + pathname = pathname.substring(0, pathname.length - 1) + } } - } - if (this.i18nProvider) { - // Remove the port from the hostname if present. - const hostname = req?.headers.host?.split(':', 1)[0].toLowerCase() + if (this.i18nProvider) { + // Remove the port from the hostname if present. + const hostname = req?.headers.host?.split(':', 1)[0].toLowerCase() - const domainLocale = this.i18nProvider.detectDomainLocale(hostname) - const defaultLocale = - domainLocale?.defaultLocale ?? this.i18nProvider.config.defaultLocale + const domainLocale = this.i18nProvider.detectDomainLocale(hostname) + const defaultLocale = + domainLocale?.defaultLocale ?? this.i18nProvider.config.defaultLocale - const localePathResult = this.i18nProvider.analyze(pathname) + const localePathResult = this.i18nProvider.analyze(pathname) - // If the locale is detected from the path, we need to remove it - // from the pathname. - if (localePathResult.detectedLocale) { - pathname = localePathResult.pathname - } + // If the locale is detected from the path, we need to remove it + // from the pathname. + if (localePathResult.detectedLocale) { + pathname = localePathResult.pathname + } - // Update the query with the detected locale and default locale. - parsedUrl.query.__nextLocale = localePathResult.detectedLocale - parsedUrl.query.__nextDefaultLocale = defaultLocale + // Update the query with the detected locale and default locale. + parsedUrl.query.__nextLocale = localePathResult.detectedLocale + parsedUrl.query.__nextDefaultLocale = defaultLocale - // If the locale is not detected from the path, we need to mark that - // it was not inferred from default. - if (!localePathResult.detectedLocale) { - delete parsedUrl.query.__nextInferredLocaleFromDefault - } + // If the locale is not detected from the path, we need to mark that + // it was not inferred from default. + if (!localePathResult.detectedLocale) { + delete parsedUrl.query.__nextInferredLocaleFromDefault + } - // If no locale was detected and we don't have middleware, we need - // to render a 404 page. - if (!localePathResult.detectedLocale && !middleware) { - parsedUrl.query.__nextLocale = defaultLocale - await this.render404(req, res, parsedUrl) - return true + // If no locale was detected and we don't have middleware, we need + // to render a 404 page. + if (!localePathResult.detectedLocale && !middleware) { + parsedUrl.query.__nextLocale = defaultLocale + await this.render404(req, res, parsedUrl) + return true + } } + + parsedUrl.pathname = pathname + parsedUrl.query.__nextDataReq = '1' + + return false } - parsedUrl.pathname = pathname - parsedUrl.query.__nextDataReq = '1' + protected handleNextImageRequest: RouteHandler< + ServerRequest, + ServerResponse + > = () => false - return false - } + protected handleCatchallRenderRequest: RouteHandler< + ServerRequest, + ServerResponse + > = () => false - protected handleNextImageRequest: RouteHandler = () => false - protected handleCatchallRenderRequest: RouteHandler = () => false - protected handleCatchallMiddlewareRequest: RouteHandler = () => false + protected handleCatchallMiddlewareRequest: RouteHandler< + ServerRequest, + ServerResponse + > = () => false protected getRouteMatchers(): RouteMatcherManager { // Create a new manifest loader that get's the manifests from the server. @@ -759,8 +806,8 @@ export default abstract class Server { } public async handleRequest( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, parsedUrl?: NextUrlWithParsedQuery ): Promise { await this.prepare() @@ -818,8 +865,8 @@ export default abstract class Server { } private async handleRequestImpl( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, parsedUrl?: NextUrlWithParsedQuery ): Promise { try { @@ -828,38 +875,10 @@ export default abstract class Server { // ensure cookies set in middleware are merged and // not overridden by API routes/getServerSideProps - const _res = (res as any).originalResponse || res - const origSetHeader = _res.setHeader.bind(_res) - - _res.setHeader = (name: string, val: string | string[]) => { - // When renders /_error after page is failed, - // it could attempt to set headers after headers - if (_res.headersSent) { - return - } - if (name.toLowerCase() === 'set-cookie') { - const middlewareValue = getRequestMeta(req, 'middlewareCookie') - - if ( - !middlewareValue || - !Array.isArray(val) || - !val.every((item, idx) => item === middlewareValue[idx]) - ) { - val = [ - // TODO: (wyattjoh) find out why this is called multiple times resulting in duplicate cookies being added - ...new Set([ - ...(middlewareValue || []), - ...(typeof val === 'string' - ? [val] - : Array.isArray(val) - ? val - : []), - ]), - ] - } - } - return origSetHeader(name, val) - } + patchSetHeaderWithCookieSupport( + req, + isNodeNextResponse(res) ? res.originalResponse : res + ) const urlParts = (req.url || '').split('?', 1) const urlNoQuery = urlParts[0] @@ -894,7 +913,7 @@ export default abstract class Server { ) } - const { originalRequest } = req as NodeNextRequest + const { originalRequest = null } = isNodeNextRequest(req) ? req : {} const xForwardedProto = originalRequest?.headers['x-forwarded-proto'] const isHttps = xForwardedProto ? xForwardedProto === 'https' @@ -907,7 +926,7 @@ export default abstract class Server { ? '443' : '80' req.headers['x-forwarded-proto'] ??= isHttps ? 'https' : 'http' - req.headers['x-forwarded-for'] ??= originalRequest.socket?.remoteAddress + req.headers['x-forwarded-for'] ??= originalRequest?.socket?.remoteAddress // This should be done before any normalization of the pathname happens as // it captures the initial URL. @@ -963,7 +982,7 @@ export default abstract class Server { let { pathname: urlPathname } = new URL(req.url, 'http://localhost') - // For ISR the URL is normalized to the prerenderPath so if + // For ISR the URL is normalized to the prerenderPath so if // it's a data request the URL path will be the data URL, // basePath is already stripped by this point if (this.normalizers.data?.match(urlPathname)) { @@ -1379,7 +1398,11 @@ export default abstract class Server { return this.renderError(null, req, res, '/_error', {}) } - if (this.minimalMode || this.renderOpts.dev || (err as any).bubble) { + if ( + this.minimalMode || + this.renderOpts.dev || + (isBubbledError(err) && err.bubble) + ) { throw err } this.logError(getProperError(err)) @@ -1416,6 +1439,10 @@ export default abstract class Server { normalizers.push(this.normalizers.rsc) } + if (this.normalizers.action) { + normalizers.push(this.normalizers.action) + } + for (const normalizer of normalizers) { if (!normalizer.match(pathname)) continue @@ -1425,7 +1452,10 @@ export default abstract class Server { return pathname } - private normalizeAndAttachMetadata: RouteHandler = async (req, res, url) => { + private normalizeAndAttachMetadata: RouteHandler< + ServerRequest, + ServerResponse + > = async (req, res, url) => { let finished = await this.handleNextImageRequest(req, res, url) if (finished) return true @@ -1440,7 +1470,9 @@ export default abstract class Server { /** * @internal - this method is internal to Next.js and should not be used directly by end-users */ - public getRequestHandlerWithMetadata(meta: RequestMeta): BaseRequestHandler { + public getRequestHandlerWithMetadata( + meta: RequestMeta + ): BaseRequestHandler { const handler = this.getRequestHandler() return (req, res, parsedUrl) => { setRequestMeta(req, meta) @@ -1448,12 +1480,15 @@ export default abstract class Server { } } - public getRequestHandler(): BaseRequestHandler { + public getRequestHandler(): BaseRequestHandler< + ServerRequest, + ServerResponse + > { return this.handleRequest.bind(this) } protected abstract handleUpgrade( - req: BaseNextRequest, + req: ServerRequest, socket: any, head?: any ): Promise @@ -1498,8 +1533,8 @@ export default abstract class Server { } protected async run( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, parsedUrl: UrlWithParsedQuery ): Promise { return getTracer().trace(BaseServerSpan.run, async () => @@ -1508,16 +1543,21 @@ export default abstract class Server { } private async runImpl( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, parsedUrl: UrlWithParsedQuery ): Promise { await this.handleCatchallRenderRequest(req, res, parsedUrl) } private async pipe( - fn: (ctx: RequestContext) => Promise, - partialContext: Omit + fn: ( + ctx: RequestContext + ) => Promise, + partialContext: Omit< + RequestContext, + 'renderOpts' + > ): Promise { return getTracer().trace(BaseServerSpan.pipe, async () => this.pipeImpl(fn, partialContext) @@ -1525,11 +1565,16 @@ export default abstract class Server { } private async pipeImpl( - fn: (ctx: RequestContext) => Promise, - partialContext: Omit + fn: ( + ctx: RequestContext + ) => Promise, + partialContext: Omit< + RequestContext, + 'renderOpts' + > ): Promise { const isBotRequest = isBot(partialContext.req.headers['user-agent'] || '') - const ctx: RequestContext = { + const ctx: RequestContext = { ...partialContext, renderOpts: { ...this.renderOpts, @@ -1567,10 +1612,15 @@ export default abstract class Server { } private async getStaticHTML( - fn: (ctx: RequestContext) => Promise, - partialContext: Omit + fn: ( + ctx: RequestContext + ) => Promise, + partialContext: Omit< + RequestContext, + 'renderOpts' + > ): Promise { - const ctx: RequestContext = { + const ctx: RequestContext = { ...partialContext, renderOpts: { ...this.renderOpts, @@ -1585,8 +1635,8 @@ export default abstract class Server { } public async render( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: NextParsedUrlQuery = {}, parsedUrl?: NextUrlWithParsedQuery, @@ -1598,8 +1648,8 @@ export default abstract class Server { } private async renderImpl( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: NextParsedUrlQuery = {}, parsedUrl?: NextUrlWithParsedQuery, @@ -1676,7 +1726,7 @@ export default abstract class Server { } private async renderToResponseWithComponents( - requestContext: RequestContext, + requestContext: RequestContext, findComponentsResult: FindComponentsResult ): Promise { return getTracer().trace( @@ -1689,7 +1739,7 @@ export default abstract class Server { ) } - protected stripInternalHeaders(req: BaseNextRequest): void { + protected stripInternalHeaders(req: ServerRequest): void { // Skip stripping internal headers in test mode while the header stripping // has been explicitly disabled. This allows tests to verify internal // routing behavior. @@ -1703,11 +1753,14 @@ export default abstract class Server { // Strip the internal headers from both the request and the original // request. stripInternalHeaders(req.headers) + if ( - 'originalRequest' in req && - 'headers' in (req as NodeNextRequest).originalRequest + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME !== 'edge' && + isNodeNextRequest(req) ) { - stripInternalHeaders((req as NodeNextRequest).originalRequest.headers) + stripInternalHeaders(req.originalRequest.headers) } } @@ -1721,8 +1774,8 @@ export default abstract class Server { } protected setVaryHeader( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, isAppPath: boolean, resolvedPathname: string ): void { @@ -1750,7 +1803,12 @@ export default abstract class Server { } private async renderToResponseWithComponentsImpl( - { req, res, pathname, renderOpts: opts }: RequestContext, + { + req, + res, + pathname, + renderOpts: opts, + }: RequestContext, { components, query }: FindComponentsResult ): Promise { if (pathname === UNDERSCORE_NOT_FOUND_ROUTE) { @@ -2246,6 +2304,18 @@ export default abstract class Server { if (routeModule) { if (isAppRouteRouteModule(routeModule)) { + if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME === 'edge' || + !isNodeNextRequest(req) || + !isNodeNextResponse(res) + ) { + throw new Error( + 'Invariant: App Route Route Modules cannot be used in the edge runtime' + ) + } + const context: AppRouteRouteHandlerContext = { params: opts.params, prerenderManifest, @@ -2260,9 +2330,9 @@ export default abstract class Server { } try { - const request = NextRequestAdapter.fromBaseNextRequest( + const request = NextRequestAdapter.fromNodeNextRequest( req, - signalFromNodeResponse((res as NodeNextResponse).originalResponse) + signalFromNodeResponse(res.originalResponse) ) const response = await routeModule.handle(request, context) @@ -2275,7 +2345,7 @@ export default abstract class Server { // If the request is for a static response, we can cache it so long // as it's not edge. - if (isSSG && process.env.NEXT_RUNTIME !== 'edge') { + if (isSSG) { const blob = await response.blob() // Copy the headers from the response. @@ -2328,12 +2398,21 @@ export default abstract class Server { renderOpts.clientReferenceManifest = components.clientReferenceManifest + const request = isNodeNextRequest(req) ? req.originalRequest : req + const response = isNodeNextResponse(res) ? res.originalResponse : res + // Call the built-in render method on the module. result = await routeModule.render( - (req as NodeNextRequest).originalRequest ?? (req as WebNextRequest), - (res as NodeNextResponse).originalResponse ?? - (res as WebNextResponse), - { page: pathname, params: opts.params, query, renderOpts } + // TODO: fix this type + // @ts-expect-error - preexisting accepted this + request, + response, + { + page: pathname, + params: opts.params, + query, + renderOpts, + } ) } else if (isAppPageRouteModule(routeModule)) { const module = components.routeModule as AppPageRouteModule @@ -2344,17 +2423,12 @@ export default abstract class Server { renderOpts.nextFontManifest = this.nextFontManifest // Call the built-in render method on the module. - result = await module.render( - (req as NodeNextRequest).originalRequest ?? (req as WebNextRequest), - (res as NodeNextResponse).originalResponse ?? - (res as WebNextResponse), - { - page: is404Page ? '/404' : pathname, - params: opts.params, - query, - renderOpts, - } - ) + result = await module.render(req, res, { + page: is404Page ? '/404' : pathname, + params: opts.params, + query, + renderOpts, + }) } else { throw new Error('Invariant: Unknown route module type') } @@ -2989,7 +3063,7 @@ export default abstract class Server { } protected async renderPageComponent( - ctx: RequestContext, + ctx: RequestContext, bubbleNoFallback: boolean ) { const { query, pathname } = ctx @@ -3029,7 +3103,7 @@ export default abstract class Server { } private async renderToResponse( - ctx: RequestContext + ctx: RequestContext ): Promise { return getTracer().trace( BaseServerSpan.renderToResponse, @@ -3052,7 +3126,7 @@ export default abstract class Server { protected abstract getRoutesManifest(): NormalizedRouteManifest | undefined private async renderToResponseImpl( - ctx: RequestContext + ctx: RequestContext ): Promise { const { res, query, pathname } = ctx let page = pathname @@ -3183,8 +3257,8 @@ export default abstract class Server { } public async renderToHTML( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: ParsedUrlQuery = {} ): Promise { @@ -3194,8 +3268,8 @@ export default abstract class Server { } private async renderToHTMLImpl( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: ParsedUrlQuery = {} ): Promise { @@ -3209,8 +3283,8 @@ export default abstract class Server { public async renderError( err: Error | null, - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: NextParsedUrlQuery = {}, setHeaders = true @@ -3222,8 +3296,8 @@ export default abstract class Server { private async renderErrorImpl( err: Error | null, - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: NextParsedUrlQuery = {}, setHeaders = true @@ -3254,7 +3328,7 @@ export default abstract class Server { }) private async renderErrorToResponse( - ctx: RequestContext, + ctx: RequestContext, err: Error | null ): Promise { return getTracer().trace(BaseServerSpan.renderErrorToResponse, async () => { @@ -3263,7 +3337,7 @@ export default abstract class Server { } protected async renderErrorToResponseImpl( - ctx: RequestContext, + ctx: RequestContext, err: Error | null ): Promise { // Short-circuit favicon.ico in development to avoid compiling 404 page when the app has no favicon.ico. @@ -3462,8 +3536,8 @@ export default abstract class Server { public async renderErrorToHTML( err: Error | null, - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, pathname: string, query: ParsedUrlQuery = {} ): Promise { @@ -3476,8 +3550,8 @@ export default abstract class Server { } public async render404( - req: BaseNextRequest, - res: BaseNextResponse, + req: ServerRequest, + res: ServerResponse, parsedUrl?: Pick, setHeaders = true ): Promise { @@ -3493,9 +3567,7 @@ export default abstract class Server { } } -export function isRSCRequestCheck( - req: IncomingMessage | BaseNextRequest -): boolean { +export function isRSCRequestCheck(req: BaseNextRequest): boolean { return ( req.headers[RSC_HEADER.toLowerCase()] === '1' || Boolean(getRequestMeta(req, 'isRSCRequest')) diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index e23a8ea3fb885d..f230c427b3e1d3 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -4,7 +4,7 @@ import { VALID_LOADERS } from '../shared/lib/image-config' import { z } from 'next/dist/compiled/zod' import type zod from 'next/dist/compiled/zod' -import type { SizeLimit } from '../../types' +import type { SizeLimit } from '../types' import type { ExportPathMap, TurboLoaderItem, @@ -130,7 +130,6 @@ export const configSchema: zod.ZodType = z.lazy(() => canonicalBase: z.string().optional(), }) .optional(), - analyticsId: z.string().optional(), // TODO: remove in the next major version assetPrefix: z.string().optional(), basePath: z.string().optional(), cacheHandler: z.string().min(1).optional(), diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 43c4ab7c2c46c2..f436271e6a1e1b 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -9,7 +9,7 @@ import type { import type { SubresourceIntegrityAlgorithm } from '../build/webpack/plugins/subresource-integrity-plugin' import type { WEB_VITALS } from '../shared/lib/utils' import type { NextParsedUrlQuery } from './request-meta' -import type { SizeLimit } from '../../types' +import type { SizeLimit } from '../types' import type { SwrDelta } from './lib/revalidate' export type NextConfigComplete = Required & { @@ -608,16 +608,6 @@ export interface NextConfig extends Record { /** @see [Compression documentation](https://nextjs.org/docs/api-reference/next.config.js/compression) */ compress?: boolean - /** - * The field should only be used when a Next.js project is not hosted on Vercel while using Vercel Speed Insights. - * Vercel provides zero-configuration insights for Next.js projects hosted on Vercel. - * - * @default '' - * @deprecated will be removed in next major version. Read more: https://nextjs.org/docs/messages/deprecated-analyticsid - * @see [how to fix deprecated analyticsId](https://nextjs.org/docs/messages/deprecated-analyticsid) - */ - analyticsId?: string - /** @see [Disabling x-powered-by](https://nextjs.org/docs/api-reference/next.config.js/disabling-x-powered-by) */ poweredByHeader?: boolean @@ -849,7 +839,6 @@ export const defaultConfig: NextConfig = { pageExtensions: ['tsx', 'ts', 'jsx', 'js'], poweredByHeader: true, compress: true, - analyticsId: process.env.VERCEL_ANALYTICS_ID || '', // TODO: remove in the next major version images: imageConfigDefault, devIndicators: { buildActivity: true, diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 859d834374eb1c..cbc1d7068c4b3c 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -491,7 +491,7 @@ function assignDefaults( warnOptionHasBeenDeprecated( result, 'swcMinify', - 'Disabling SWC Minifer will not be an option in the next major version. Please report any issues you may be experiencing to https://github.com/vercel/next.js/issues', + 'Disabling SWC Minifier will not be an option in the next major version. Please report any issues you may be experiencing to https://github.com/vercel/next.js/issues', silent ) } @@ -1014,7 +1014,7 @@ export default async function loadConfig( require('./config-schema') as typeof import('./config-schema') const state = configSchema.safeParse(userConfig) - if (!state.success) { + if (state.success === false) { // error message header const messages = [`Invalid ${configFileName} options detected: `] diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index fd72464d33e541..333b31a952d52b 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -5,7 +5,6 @@ import type { Params } from '../../shared/lib/router/utils/route-matcher' import type { ParsedUrl } from '../../shared/lib/router/utils/parse-url' import type { ParsedUrlQuery } from 'querystring' import type { UrlWithParsedQuery } from 'url' -import type { BaseNextRequest, BaseNextResponse } from '../base-http' import type { FallbackMode, MiddlewareRoutingItem } from '../base-server' import type { FunctionComponent } from 'react' import type { RouteDefinition } from '../future/route-definitions/route-definition' @@ -362,8 +361,8 @@ export default class DevServer extends Server { } async runMiddleware(params: { - request: BaseNextRequest - response: BaseNextResponse + request: NodeNextRequest + response: NodeNextResponse parsedUrl: ParsedUrl parsed: UrlWithParsedQuery middlewareList: MiddlewareRoutingItem[] @@ -421,8 +420,8 @@ export default class DevServer extends Server { } async runEdgeFunction(params: { - req: BaseNextRequest - res: BaseNextResponse + req: NodeNextRequest + res: NodeNextResponse query: ParsedUrlQuery params: Params | undefined page: string @@ -451,8 +450,8 @@ export default class DevServer extends Server { } public async handleRequest( - req: BaseNextRequest, - res: BaseNextResponse, + req: NodeNextRequest, + res: NodeNextResponse, parsedUrl?: NextUrlWithParsedQuery ): Promise { const span = trace('handle-request', undefined, { url: req.url }) diff --git a/packages/next/src/server/dev/turbopack-utils.ts b/packages/next/src/server/dev/turbopack-utils.ts index 75d6d9a98460e3..3295310c0e6705 100644 --- a/packages/next/src/server/dev/turbopack-utils.ts +++ b/packages/next/src/server/dev/turbopack-utils.ts @@ -32,6 +32,7 @@ import { splitEntryKey, } from './turbopack/entry-key' import type ws from 'next/dist/compiled/ws' +import isInternal from '../../shared/lib/is-internal' export async function getTurbopackJsConfig( dir: string, @@ -41,7 +42,7 @@ export async function getTurbopackJsConfig( return jsConfig ?? { compilerOptions: {} } } -class ModuleBuildError extends Error {} +export class ModuleBuildError extends Error {} /** * Thin stopgap workaround layer to mimic existing wellknown-errors-plugin in webpack's build @@ -115,7 +116,12 @@ export function formatIssue(issue: Issue) { } message += '\n' - if (source?.range && source.source.content) { + if ( + source?.range && + source.source.content && + // ignore Next.js/React internals, as these can often be huge bundled files. + !isInternal(filePath) + ) { const { start, end } = source.range const { codeFrameColumns } = require('next/dist/compiled/babel/code-frame') diff --git a/packages/next/src/server/future/normalizers/request/action.ts b/packages/next/src/server/future/normalizers/request/action.ts new file mode 100644 index 00000000000000..07e14dedcd658c --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/action.ts @@ -0,0 +1,13 @@ +import type { PathnameNormalizer } from './pathname-normalizer' + +import { ACTION_SUFFIX } from '../../../../lib/constants' +import { SuffixPathnameNormalizer } from './suffix' + +export class ActionPathnameNormalizer + extends SuffixPathnameNormalizer + implements PathnameNormalizer +{ + constructor() { + super(ACTION_SUFFIX) + } +} diff --git a/packages/next/src/server/future/route-modules/app-page/module.ts b/packages/next/src/server/future/route-modules/app-page/module.ts index 0c5e7cbcb17d40..92812f4274bb83 100644 --- a/packages/next/src/server/future/route-modules/app-page/module.ts +++ b/packages/next/src/server/future/route-modules/app-page/module.ts @@ -1,4 +1,3 @@ -import type { IncomingMessage, ServerResponse } from 'http' import type { AppPageRouteDefinition } from '../../route-definitions/app-page-route-definition' import type RenderResult from '../../../render-result' import type { RenderOpts } from '../../../app-render/types' @@ -12,6 +11,7 @@ import { type RouteModuleHandleContext, } from '../route-module' import * as vendoredContexts from './vendored/contexts/entrypoints' +import type { BaseNextRequest, BaseNextResponse } from '../../../base-http' let vendoredReactRSC let vendoredReactSSR @@ -52,8 +52,8 @@ export class AppPageRouteModule extends RouteModule< AppPageUserlandModule > { public render( - req: IncomingMessage, - res: ServerResponse, + req: BaseNextRequest, + res: BaseNextResponse, context: AppPageRouteHandlerContext ): Promise { return renderToHTMLOrFlight( diff --git a/packages/next/src/server/future/route-modules/pages-api/module.ts b/packages/next/src/server/future/route-modules/pages-api/module.ts index ac48492c5029cf..a709d2ea870d76 100644 --- a/packages/next/src/server/future/route-modules/pages-api/module.ts +++ b/packages/next/src/server/future/route-modules/pages-api/module.ts @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from 'http' import type { PagesAPIRouteDefinition } from '../../route-definitions/pages-api-route-definition' -import type { PageConfig } from '../../../../../types' +import type { PageConfig } from '../../../../types' import type { ParsedUrlQuery } from 'querystring' import { wrapApiHandler, type __ApiPreviewProps } from '../../../api-utils' import type { RouteModuleOptions } from '../route-module' diff --git a/packages/next/src/server/future/route-modules/pages/module.ts b/packages/next/src/server/future/route-modules/pages/module.ts index c50b9c3eb718d6..97fc26d4114140 100644 --- a/packages/next/src/server/future/route-modules/pages/module.ts +++ b/packages/next/src/server/future/route-modules/pages/module.ts @@ -5,7 +5,7 @@ import type { GetStaticProps, NextComponentType, PageConfig, -} from '../../../../../types' +} from '../../../../types' import type { PagesRouteDefinition } from '../../route-definitions/pages-route-definition' import type { NextParsedUrlQuery } from '../../../request-meta' import type { RenderOpts } from '../../../render' diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 755da67fdf1d94..569c216ff16c2a 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -385,14 +385,19 @@ export class IncrementalCache implements IncrementalCacheType { } } + const headers = + typeof (init.headers || {}).keys === 'function' + ? Object.fromEntries(init.headers as Headers) + : Object.assign(init.headers || {}, {}) + + if ('traceparent' in headers) delete headers['traceparent'] + const cacheString = JSON.stringify([ MAIN_KEY_PREFIX, this.fetchCacheKeyPrefix || '', url, init.method, - typeof (init.headers || {}).keys === 'function' - ? Object.fromEntries(init.headers as Headers) - : init.headers, + headers, init.mode, init.redirect, init.credentials, diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index e5318dc371796e..2e7687d728714b 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -163,7 +163,13 @@ function trackFetchMetric( staticGenerationStore: StaticGenerationStore, ctx: Omit ) { - if (!staticGenerationStore) return + if ( + !staticGenerationStore || + staticGenerationStore.requestEndedState?.ended || + process.env.NODE_ENV !== 'development' + ) { + return + } staticGenerationStore.fetchMetrics ??= [] const dedupeFields = ['url', 'status', 'method'] as const @@ -181,6 +187,25 @@ function trackFetchMetric( end: Date.now(), idx: staticGenerationStore.nextFetchId || 0, }) + + // only store top 10 metrics to avoid storing too many + if (staticGenerationStore.fetchMetrics.length > 10) { + // sort slowest first as these should be highlighted + staticGenerationStore.fetchMetrics.sort((a, b) => { + const aDur = a.end - a.start + const bDur = b.end - b.start + + if (aDur < bDur) { + return 1 + } else if (aDur > bDur) { + return -1 + } + return 0 + }) + // now grab top 10 + staticGenerationStore.fetchMetrics = + staticGenerationStore.fetchMetrics.slice(0, 10) + } } interface PatchableModule { @@ -668,6 +693,7 @@ function createPatchedFetcher( const err = new DynamicServerError(dynamicUsageReason) staticGenerationStore.dynamicUsageErr = err staticGenerationStore.dynamicUsageDescription = dynamicUsageReason + throw err } const hasNextConfig = 'next' in init @@ -695,6 +721,7 @@ function createPatchedFetcher( const err = new DynamicServerError(dynamicUsageReason) staticGenerationStore.dynamicUsageErr = err staticGenerationStore.dynamicUsageDescription = dynamicUsageReason + throw err } if (!staticGenerationStore.forceStatic || next.revalidate !== 0) { diff --git a/packages/next/src/server/lib/patch-set-header.ts b/packages/next/src/server/lib/patch-set-header.ts new file mode 100644 index 00000000000000..83d40c7ad30eb3 --- /dev/null +++ b/packages/next/src/server/lib/patch-set-header.ts @@ -0,0 +1,53 @@ +import { getRequestMeta, type NextIncomingMessage } from '../request-meta' + +type PatchableResponse = { + setHeader(key: string, value: string | string[]): PatchableResponse +} + +/** + * Ensure cookies set in middleware are merged and not overridden by API + * routes/getServerSideProps. + * + * @param req Incoming request + * @param res Outgoing response + */ +export function patchSetHeaderWithCookieSupport( + req: NextIncomingMessage, + res: PatchableResponse +) { + const setHeader = res.setHeader.bind(res) + res.setHeader = ( + name: string, + value: string | string[] + ): PatchableResponse => { + // When renders /_error after page is failed, it could attempt to set + // headers after headers. + if ('headersSent' in res && res.headersSent) { + return res + } + + if (name.toLowerCase() === 'set-cookie') { + const middlewareValue = getRequestMeta(req, 'middlewareCookie') + + if ( + !middlewareValue || + !Array.isArray(value) || + !value.every((item, idx) => item === middlewareValue[idx]) + ) { + value = [ + // TODO: (wyattjoh) find out why this is called multiple times resulting in duplicate cookies being added + ...new Set([ + ...(middlewareValue || []), + ...(typeof value === 'string' + ? [value] + : Array.isArray(value) + ? value + : []), + ]), + ] + } + } + + return setHeader(name, value) + } +} diff --git a/packages/next/src/server/lib/render-server.ts b/packages/next/src/server/lib/render-server.ts index d89c019f94959d..a025b14728bba8 100644 --- a/packages/next/src/server/lib/render-server.ts +++ b/packages/next/src/server/lib/render-server.ts @@ -87,6 +87,7 @@ async function initializeImpl(opts: { _ipcKey?: string bundlerService: DevBundlerService | undefined startServerSpan: Span | undefined + quiet?: boolean }) { const type = process.env.__NEXT_PRIVATE_RENDER_WORKER if (type) { diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index 0f0c8187ef798f..aa4a3d415206f8 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -72,6 +72,7 @@ export async function initialize(opts: { customServer?: boolean experimentalHttpsServer?: boolean startServerSpan?: Span + quiet?: boolean }): Promise<[WorkerRequestHandler, WorkerUpgradeHandler, NextServer]> { if (!process.env.NODE_ENV) { // @ts-ignore not readonly @@ -604,6 +605,7 @@ export async function initialize(opts: { experimentalHttpsServer: !!opts.experimentalHttpsServer, bundlerService: devBundlerService, startServerSpan: opts.startServerSpan, + quiet: opts.quiet, } renderServerOpts.serverFields.routerServerHandler = requestHandlerImpl diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index 8b2b7c9606ca1f..b9585c9c9cb11c 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -3,7 +3,7 @@ import type { IncomingMessage, ServerResponse } from 'http' import type { NextConfigComplete } from '../../config-shared' import type { RenderServer, initialize } from '../router-server' import type { PatchMatcher } from '../../../shared/lib/router/utils/path-match' -import type { Redirect } from '../../../../types' +import type { Redirect } from '../../../types' import type { Header } from '../../../lib/load-custom-routes' import type { UnwrapPromise } from '../../../lib/coalesced-function' import type { NextUrlWithParsedQuery } from '../../request-meta' diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index f3eb481850a506..79fbba8c00c418 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -76,6 +76,7 @@ import { createHotReloaderTurbopack } from '../../dev/hot-reloader-turbopack' import { getErrorSource } from '../../../shared/lib/error-source' import type { StackFrame } from 'next/dist/compiled/stacktrace-parser' import { generateEncryptionKeyBase64 } from '../../app-render/encryption-utils' +import { ModuleBuildError } from '../../dev/turbopack-utils' export type SetupOpts = { renderServer: LazyRenderServerInstance @@ -128,8 +129,6 @@ async function verifyTypeScript(opts: SetupOpts) { return usingTypeScript } -class ModuleBuildError extends Error {} - export async function propagateServerField( opts: SetupOpts, field: PropagateToWorkersField, @@ -972,15 +971,7 @@ async function startWatcher(opts: SetupOpts) { errorToLog = err } - if (type === 'warning') { - Log.warn(errorToLog) - } else if (type === 'app-dir') { - logAppDirError(errorToLog) - } else if (type) { - Log.error(`${type}:`, errorToLog) - } else { - Log.error(errorToLog) - } + logError(errorToLog, type) console[type === 'warning' ? 'warn' : 'error'](originalCodeFrame) usedOriginalStack = true } @@ -993,17 +984,7 @@ async function startWatcher(opts: SetupOpts) { } if (!usedOriginalStack) { - if (err instanceof ModuleBuildError) { - Log.error(err.message) - } else if (type === 'warning') { - Log.warn(err) - } else if (type === 'app-dir') { - logAppDirError(err) - } else if (type) { - Log.error(`${type}:`, err) - } else { - Log.error(err) - } + logError(err, type) } } @@ -1025,6 +1006,23 @@ async function startWatcher(opts: SetupOpts) { } } +function logError( + err: unknown, + type?: 'unhandledRejection' | 'uncaughtException' | 'warning' | 'app-dir' +) { + if (err instanceof ModuleBuildError) { + Log.error(err.message) + } else if (type === 'warning') { + Log.warn(err) + } else if (type === 'app-dir') { + logAppDirError(err) + } else if (type) { + Log.error(`${type}:`, err) + } else { + Log.error(err) + } +} + export async function setupDevBundler(opts: SetupOpts) { const isSrcDir = path .relative(opts.dir, opts.pagesDir || opts.appDir || '') diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 13f20677acce0c..85398a2930af83 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/packages/next/src/server/lib/start-server.ts @@ -52,6 +52,7 @@ export async function getRequestHandlers({ isNodeDebugging, keepAliveTimeout, experimentalHttpsServer, + quiet, }: { dir: string port: number @@ -62,6 +63,7 @@ export async function getRequestHandlers({ isNodeDebugging?: boolean keepAliveTimeout?: number experimentalHttpsServer?: boolean + quiet?: boolean }): ReturnType { return initialize({ dir, @@ -74,6 +76,7 @@ export async function getRequestHandlers({ keepAliveTimeout, experimentalHttpsServer, startServerSpan, + quiet, }) } diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index 05edc405d4fc1d..428eaa0da942f7 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -1,4 +1,5 @@ import type { TextMapSetter } from '@opentelemetry/api' +import type { FetchEventResult } from '../../web/types' import type { SpanTypes } from './constants' import { LogSpanAllowList, NextVanillaSpanAllowlist } from './constants' @@ -37,10 +38,22 @@ const isPromise = (p: any): p is Promise => { return p !== null && typeof p === 'object' && typeof p.then === 'function' } -type BubbledError = Error & { bubble?: boolean } +export class BubbledError extends Error { + constructor( + public readonly bubble?: boolean, + public readonly result?: FetchEventResult + ) { + super() + } +} + +export function isBubbledError(error: unknown): error is BubbledError { + if (typeof error !== 'object' || error === null) return false + return error instanceof BubbledError +} const closeSpanWithError = (span: Span, error?: Error) => { - if ((error as BubbledError | undefined)?.bubble === true) { + if (isBubbledError(error) && error.bubble) { span.setAttribute('next.bubble', true) } else { if (error) { @@ -296,7 +309,7 @@ class NextTracerImpl implements NextTracer { options, (span: Span) => { const startTime = - 'performance' in globalThis + 'performance' in globalThis && 'measure' in performance ? globalThis.performance.now() : undefined @@ -335,7 +348,7 @@ class NextTracerImpl implements NextTracer { } try { if (fn.length > 1) { - return fn(span, (err?: Error) => closeSpanWithError(span, err)) + return fn(span, (err) => closeSpanWithError(span, err)) } const result = fn(span) diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 5e3978d0f4eef5..3e43beaef83727 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -9,7 +9,7 @@ import type { GetStaticPaths, GetServerSideProps, GetStaticProps, -} from 'next/types' +} from '../types' import type { RouteModule } from './future/route-modules/route-module' import type { BuildManifest } from './get-page-files' import type { ActionManifest } from '../build/webpack/plugins/flight-client-entry-plugin' @@ -134,8 +134,8 @@ async function loadComponentsImpl({ let AppMod = {} if (!isAppPath) { ;[DocumentMod, AppMod] = await Promise.all([ - Promise.resolve().then(() => requirePage('/_document', distDir, false)), - Promise.resolve().then(() => requirePage('/_app', distDir, false)), + requirePage('/_document', distDir, false), + requirePage('/_app', distDir, false), ]) } @@ -186,9 +186,7 @@ async function loadComponentsImpl({ }) } - const ComponentMod = await Promise.resolve().then(() => - requirePage(page, distDir, isAppPath) - ) + const ComponentMod = await requirePage(page, distDir, isAppPath) const Component = interopDefault(ComponentMod) const Document = interopDefault(DocumentMod) diff --git a/packages/next/src/server/load-default-error-components.ts b/packages/next/src/server/load-default-error-components.ts index b57567a5f92520..bbd703709047e8 100644 --- a/packages/next/src/server/load-default-error-components.ts +++ b/packages/next/src/server/load-default-error-components.ts @@ -9,7 +9,7 @@ import type { GetStaticPaths, GetServerSideProps, GetStaticProps, -} from 'next/types' +} from '../types' import type { RouteModule } from './future/route-modules/route-module' import type { BuildManifest } from './get-page-files' diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 87e0cc79726485..36c6c68312c9d8 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -12,11 +12,7 @@ import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plu import type RenderResult from './render-result' import type { FetchEventResult } from './web/types' import type { PrerenderManifest } from '../build' -import type { - BaseNextRequest, - BaseNextResponse, - FetchMetric, -} from './base-http' +import type { FetchMetric } from './base-http' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' import type { Params } from '../shared/lib/router/utils/route-matcher' @@ -61,6 +57,7 @@ import type { LoadedRenderOpts, RouteHandler, NextEnabledDirectories, + BaseRequestHandler, } from './base-server' import BaseServer, { NoFallbackError, isRSCRequestCheck } from './base-server' import { getMaybePagePath, getPagePath, requireFontManifest } from './require' @@ -91,7 +88,7 @@ import { INSTRUMENTATION_HOOK_FILENAME, RSC_PREFETCH_SUFFIX, } from '../lib/constants' -import { getTracer } from './lib/trace/tracer' +import { BubbledError, getTracer } from './lib/trace/tracer' import { NextNodeServerSpan } from './lib/trace/constants' import { nodeFs } from './lib/node-fs-methods' import { getRouteRegex } from '../shared/lib/router/utils/route-regex' @@ -133,13 +130,12 @@ function formatRequestUrl(url: string, maxLength: number | undefined) { : url } -export interface NodeRequestHandler { - ( - req: IncomingMessage | BaseNextRequest, - res: ServerResponse | BaseNextResponse, - parsedUrl?: NextUrlWithParsedQuery | undefined - ): Promise | void -} +export type NodeRequestHandler = BaseRequestHandler< + IncomingMessage | NodeNextRequest, + ServerResponse | NodeNextResponse +> + +type NodeRouteHandler = RouteHandler const MiddlewareMatcherCache = new WeakMap< MiddlewareManifest['middleware'][string], @@ -165,7 +161,11 @@ function getMiddlewareMatcher( return matcher } -export default class NextNodeServer extends BaseServer { +export default class NextNodeServer extends BaseServer< + Options, + NodeNextRequest, + NodeNextResponse +> { protected middlewareManifestPath: string private _serverDistDir: string | undefined private imageResponseCache?: ResponseCache @@ -485,8 +485,8 @@ export default class NextNodeServer extends BaseServer { } protected async runApi( - req: BaseNextRequest | NodeNextRequest, - res: BaseNextResponse | NodeNextResponse, + req: NodeNextRequest, + res: NodeNextResponse, query: ParsedUrlQuery, match: PagesAPIRouteMatch ): Promise { @@ -520,23 +520,19 @@ export default class NextNodeServer extends BaseServer { delete query.__nextDefaultLocale delete query.__nextInferredLocaleFromDefault - await module.render( - (req as NodeNextRequest).originalRequest, - (res as NodeNextResponse).originalResponse, - { - previewProps: this.renderOpts.previewProps, - revalidate: this.revalidate.bind(this), - trustHostHeader: this.nextConfig.experimental.trustHostHeader, - allowedRevalidateHeaderKeys: - this.nextConfig.experimental.allowedRevalidateHeaderKeys, - hostname: this.fetchHostname, - minimalMode: this.minimalMode, - dev: this.renderOpts.dev === true, - query, - params: match.params, - page: match.definition.pathname, - } - ) + await module.render(req.originalRequest, res.originalResponse, { + previewProps: this.renderOpts.previewProps, + revalidate: this.revalidate.bind(this), + trustHostHeader: this.nextConfig.experimental.trustHostHeader, + allowedRevalidateHeaderKeys: + this.nextConfig.experimental.allowedRevalidateHeaderKeys, + hostname: this.fetchHostname, + minimalMode: this.minimalMode, + dev: this.renderOpts.dev === true, + query, + params: match.params, + page: match.definition.pathname, + }) return true } @@ -572,13 +568,7 @@ export default class NextNodeServer extends BaseServer { renderOpts.nextFontManifest = this.nextFontManifest if (this.enabledDirectories.app && renderOpts.isAppPath) { - return lazyRenderAppPage( - req.originalRequest, - res.originalResponse, - pathname, - query, - renderOpts - ) + return lazyRenderAppPage(req, res, pathname, query, renderOpts) } // TODO: re-enable this once we've refactored to use implicit matches @@ -653,7 +643,7 @@ export default class NextNodeServer extends BaseServer { } protected async renderPageComponent( - ctx: RequestContext, + ctx: RequestContext, bubbleNoFallback: boolean ) { const edgeFunctionsPages = this.getEdgeFunctionsPages() || [] @@ -815,7 +805,7 @@ export default class NextNodeServer extends BaseServer { ) } - protected handleNextImageRequest: RouteHandler = async ( + protected handleNextImageRequest: NodeRouteHandler = async ( req, res, parsedUrl @@ -856,7 +846,7 @@ export default class NextNodeServer extends BaseServer { } const paramsResult = ImageOptimizerCache.validateParams( - (req as NodeNextRequest).originalRequest, + req.originalRequest, parsedUrl.query, this.nextConfig, !!this.renderOpts.dev @@ -877,8 +867,8 @@ export default class NextNodeServer extends BaseServer { cacheKey, async () => { const { buffer, contentType, maxAge } = await this.imageOptimizer( - req as NodeNextRequest, - res as NodeNextResponse, + req, + res, paramsResult ) const etag = getHash([buffer]) @@ -905,8 +895,8 @@ export default class NextNodeServer extends BaseServer { } sendResponse( - (req as NodeNextRequest).originalRequest, - (res as NodeNextResponse).originalResponse, + req.originalRequest, + res.originalResponse, paramsResult.href, cacheEntry.value.extension, cacheEntry.value.buffer, @@ -928,7 +918,7 @@ export default class NextNodeServer extends BaseServer { } } - protected handleCatchallRenderRequest: RouteHandler = async ( + protected handleCatchallRenderRequest: NodeRouteHandler = async ( req, res, parsedUrl @@ -1060,8 +1050,8 @@ export default class NextNodeServer extends BaseServer { * @param pathname path of request */ protected async handleApiRequest( - req: BaseNextRequest, - res: BaseNextResponse, + req: NodeNextRequest, + res: NodeNextResponse, query: ParsedUrlQuery, match: PagesAPIRouteMatch ): Promise { @@ -1080,19 +1070,15 @@ export default class NextNodeServer extends BaseServer { } private normalizeReq( - req: BaseNextRequest | IncomingMessage - ): BaseNextRequest { - return !(req instanceof NodeNextRequest) - ? new NodeNextRequest(req as IncomingMessage) - : req + req: NodeNextRequest | IncomingMessage + ): NodeNextRequest { + return !(req instanceof NodeNextRequest) ? new NodeNextRequest(req) : req } private normalizeRes( - res: BaseNextResponse | ServerResponse - ): BaseNextResponse { - return !(res instanceof NodeNextResponse) - ? new NodeNextResponse(res as ServerResponse) - : res + res: NodeNextResponse | ServerResponse + ): NodeNextResponse { + return !(res instanceof NodeNextResponse) ? new NodeNextResponse(res) : res } public getRequestHandler(): NodeRequestHandler { @@ -1127,17 +1113,18 @@ export default class NextNodeServer extends BaseServer { if (this.renderOpts.dev) { const { blue, green, yellow, red, gray, white } = require('../lib/picocolors') as typeof import('../lib/picocolors') - const _res = res as NodeNextResponse | ServerResponse - const origRes = - 'originalResponse' in _res ? _res.originalResponse : _res + + const { originalResponse } = normalizedRes const reqStart = Date.now() + const isMiddlewareRequest = req.headers['x-middleware-invoke'] const reqCallback = () => { // we don't log for non-route requests - const isRouteRequest = getRequestMeta(req).match - const isRSC = isRSCRequestCheck(req) - if (!isRouteRequest || isRSC) return + const routeMatch = getRequestMeta(req).match + + const isRSC = isRSCRequestCheck(normalizedReq) + if (!routeMatch || isRSC || isMiddlewareRequest) return const reqEnd = Date.now() const fetchMetrics = normalizedReq.fetchMetrics || [] @@ -1245,9 +1232,10 @@ export default class NextNodeServer extends BaseServer { } } } - origRes.off('close', reqCallback) + delete normalizedReq.fetchMetrics + originalResponse.off('close', reqCallback) } - origRes.on('close', reqCallback) + originalResponse.on('close', reqCallback) } return handler(normalizedReq, normalizedRes, parsedUrl) } @@ -1283,8 +1271,8 @@ export default class NextNodeServer extends BaseServer { } public async render( - req: BaseNextRequest | IncomingMessage, - res: BaseNextResponse | ServerResponse, + req: NodeNextRequest | IncomingMessage, + res: NodeNextResponse | ServerResponse, pathname: string, query?: NextParsedUrlQuery, parsedUrl?: NextUrlWithParsedQuery, @@ -1301,8 +1289,8 @@ export default class NextNodeServer extends BaseServer { } public async renderToHTML( - req: BaseNextRequest | IncomingMessage, - res: BaseNextResponse | ServerResponse, + req: NodeNextRequest | IncomingMessage, + res: NodeNextResponse | ServerResponse, pathname: string, query?: ParsedUrlQuery ): Promise { @@ -1315,7 +1303,7 @@ export default class NextNodeServer extends BaseServer { } protected async renderErrorToResponseImpl( - ctx: RequestContext, + ctx: RequestContext, err: Error | null ) { const { req, res, query } = ctx @@ -1334,8 +1322,8 @@ export default class NextNodeServer extends BaseServer { this.getEdgeFunctionsPages().includes(UNDERSCORE_NOT_FOUND_ROUTE_ENTRY) ) { await this.runEdgeFunction({ - req: req as BaseNextRequest, - res: res as BaseNextResponse, + req, + res, query: query || {}, params: {}, page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, @@ -1349,8 +1337,8 @@ export default class NextNodeServer extends BaseServer { public async renderError( err: Error | null, - req: BaseNextRequest | IncomingMessage, - res: BaseNextResponse | ServerResponse, + req: NodeNextRequest | IncomingMessage, + res: NodeNextResponse | ServerResponse, pathname: string, query?: NextParsedUrlQuery, setHeaders?: boolean @@ -1367,8 +1355,8 @@ export default class NextNodeServer extends BaseServer { public async renderErrorToHTML( err: Error | null, - req: BaseNextRequest | IncomingMessage, - res: BaseNextResponse | ServerResponse, + req: NodeNextRequest | IncomingMessage, + res: NodeNextResponse | ServerResponse, pathname: string, query?: ParsedUrlQuery ): Promise { @@ -1382,8 +1370,8 @@ export default class NextNodeServer extends BaseServer { } public async render404( - req: BaseNextRequest | IncomingMessage, - res: BaseNextResponse | ServerResponse, + req: NodeNextRequest | IncomingMessage, + res: NodeNextResponse | ServerResponse, parsedUrl?: NextUrlWithParsedQuery, setHeaders?: boolean ): Promise { @@ -1510,8 +1498,8 @@ export default class NextNodeServer extends BaseServer { * and errors with rich traces. */ protected async runMiddleware(params: { - request: BaseNextRequest - response: BaseNextResponse + request: NodeNextRequest + response: NodeNextResponse parsedUrl: ParsedUrl parsed: UrlWithParsedQuery onWarning?: (warning: Error) => void @@ -1596,9 +1584,7 @@ export default class NextNodeServer extends BaseServer { url: url, page, body: getRequestMeta(params.request, 'clonableBody'), - signal: signalFromNodeResponse( - (params.response as NodeNextResponse).originalResponse - ), + signal: signalFromNodeResponse(params.response.originalResponse), }, useCache: true, onWarning: params.onWarning, @@ -1634,10 +1620,10 @@ export default class NextNodeServer extends BaseServer { return result } - protected handleCatchallMiddlewareRequest: RouteHandler = async ( - req: BaseNextRequest, - res: BaseNextResponse, - parsed: NextUrlWithParsedQuery + protected handleCatchallMiddlewareRequest: NodeRouteHandler = async ( + req, + res, + parsed ) => { const isMiddlewareInvoke = req.headers['x-middleware-invoke'] @@ -1690,10 +1676,7 @@ export default class NextNodeServer extends BaseServer { if ('response' in result) { if (isMiddlewareInvoke) { bubblingResult = true - const err = new Error() - ;(err as any).result = result - ;(err as any).bubble = true - throw err + throw new BubbledError(true, result) } for (const [key, value] of Object.entries( @@ -1705,7 +1688,7 @@ export default class NextNodeServer extends BaseServer { } res.statusCode = result.response.status - const { originalResponse } = res as NodeNextResponse + const { originalResponse } = res if (result.response.body) { await pipeToNodeResponse(result.response.body, originalResponse) } else { @@ -1798,7 +1781,7 @@ export default class NextNodeServer extends BaseServer { } protected attachRequestMeta( - req: BaseNextRequest, + req: NodeNextRequest, parsedUrl: NextUrlWithParsedQuery, isUpgradeReq?: boolean ) { @@ -1820,13 +1803,13 @@ export default class NextNodeServer extends BaseServer { addRequestMeta(req, 'initProtocol', protocol) if (!isUpgradeReq) { - addRequestMeta(req, 'clonableBody', getCloneableBody(req.body)) + addRequestMeta(req, 'clonableBody', getCloneableBody(req.originalRequest)) } } protected async runEdgeFunction(params: { - req: BaseNextRequest | NodeNextRequest - res: BaseNextResponse | NodeNextResponse + req: NodeNextRequest + res: NodeNextResponse query: ParsedUrlQuery params: Params | undefined page: string @@ -1903,9 +1886,7 @@ export default class NextNodeServer extends BaseServer { ...(params.params && { params: params.params }), }, body: getRequestMeta(params.req, 'clonableBody'), - signal: signalFromNodeResponse( - (params.res as NodeNextResponse).originalResponse - ), + signal: signalFromNodeResponse(params.res.originalResponse), }, useCache: true, onError: params.onError, @@ -1938,11 +1919,11 @@ export default class NextNodeServer extends BaseServer { } }) - const nodeResStream = (params.res as NodeNextResponse).originalResponse + const { originalResponse } = params.res if (result.response.body) { - await pipeToNodeResponse(result.response.body, nodeResStream) + await pipeToNodeResponse(result.response.body, originalResponse) } else { - nodeResStream.end() + originalResponse.end() } return result diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index 6b2b8b7e4cb252..86da3d005918e0 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -286,6 +286,7 @@ class NextCustomServer extends NextServer { hostname: this.options.hostname || 'localhost', minimalMode: this.options.minimalMode, isNodeDebugging: !!isNodeDebugging, + quiet: this.options.quiet, }) this.requestHandler = initResult[0] this.upgradeHandler = initResult[1] diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index 6ef1e948d103ce..c9cd388215ed6f 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -29,7 +29,7 @@ import type { PreviewData, ServerRuntime, SizeLimit, -} from 'next/types' +} from '../types' import type { UnwrapPromise } from '../lib/coalesced-function' import type { ReactReadableStream } from './stream-utils/node-web-streams-helper' import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin' @@ -81,11 +81,8 @@ import { allowedStatusCodes, getRedirectStatus } from '../lib/redirect-status' import RenderResult, { type PagesRenderResultMetadata } from './render-result' import isError from '../lib/is-error' import { - streamFromString, streamToString, - chainStreams, renderToInitialFizzStream, - continueFizzStream, } from './stream-utils/node-web-streams-helper' import { ImageConfigContext } from '../shared/lib/image-config-context.shared-runtime' import stripAnsi from 'next/dist/compiled/strip-ansi' @@ -471,11 +468,6 @@ export async function renderToHTMLImpl( renderOpts.Component const OriginComponent = Component - let serverComponentsInlinedTransformStream: TransformStream< - Uint8Array, - Uint8Array - > | null = null - const isFallback = !!query.__nextFallback const notFoundSrcPage = query.__nextNotFoundSrcPage @@ -1246,7 +1238,7 @@ export async function renderToHTMLImpl( } async function loadDocumentInitialProps( - renderShell?: ( + renderShell: ( _App: AppType, _Component: NextComponentType ) => Promise @@ -1277,26 +1269,10 @@ export async function renderToHTMLImpl( const { App: EnhancedApp, Component: EnhancedComponent } = enhanceComponents(options, App, Component) - if (renderShell) { - return renderShell(EnhancedApp, EnhancedComponent).then( - async (stream) => { - await stream.allReady - const html = await streamToString(stream) - return { html, head } - } - ) - } + const stream = await renderShell(EnhancedApp, EnhancedComponent) + await stream.allReady + const html = await streamToString(stream) - const html = await renderToString( - - - {renderPageTree(EnhancedApp, EnhancedComponent, { - ...props, - router, - })} - - - ) return { html, head } } const documentCtx = { ...ctx, renderPage } @@ -1349,48 +1325,39 @@ export async function renderToHTMLImpl( }) } - const createBodyResult = getTracer().wrap( - RenderSpan.createBodyResult, - (initialStream: ReactReadableStream, suffix?: string) => { - return continueFizzStream(initialStream, { - suffix, - inlinedDataStream: serverComponentsInlinedTransformStream?.readable, - isStaticGeneration: true, - // this must be called inside bodyResult so appWrappers is - // up to date when `wrapApp` is called - getServerInsertedHTML: () => { - return renderToString(styledJsxInsertedHTML()) - }, - serverInsertedHTMLToHead: false, - validateRootLayout: undefined, - }) - } - ) - - const hasDocumentGetInitialProps = !( - process.env.NEXT_RUNTIME === 'edge' || !Document.getInitialProps - ) - - let bodyResult: (s: string) => Promise> + const hasDocumentGetInitialProps = + process.env.NEXT_RUNTIME !== 'edge' && !!Document.getInitialProps // If it has getInitialProps, we will render the shell in `renderPage`. // Otherwise we do it right now. let documentInitialPropsRes: | {} | Awaited> - if (hasDocumentGetInitialProps) { - documentInitialPropsRes = await loadDocumentInitialProps(renderShell) - if (documentInitialPropsRes === null) return null - const { docProps } = documentInitialPropsRes as any - // includes suffix in initial html stream - bodyResult = (suffix: string) => - createBodyResult(streamFromString(docProps.html + suffix)) - } else { - const stream = await renderShell(App, Component) - bodyResult = (suffix: string) => createBodyResult(stream, suffix) - documentInitialPropsRes = {} + + const [rawStyledJsxInsertedHTML, content] = await Promise.all([ + renderToString(styledJsxInsertedHTML()), + (async () => { + if (hasDocumentGetInitialProps) { + documentInitialPropsRes = await loadDocumentInitialProps(renderShell) + if (documentInitialPropsRes === null) return null + const { docProps } = documentInitialPropsRes as any + return docProps.html + } else { + documentInitialPropsRes = {} + const stream = await renderShell(App, Component) + await stream.allReady + return streamToString(stream) + } + })(), + ]) + + if (content === null) { + return null } + const contentHTML = rawStyledJsxInsertedHTML + content + + // @ts-ignore: documentInitialPropsRes is set const { docProps } = (documentInitialPropsRes as any) || {} const documentElement = (htmlProps: any) => { if (process.env.NEXT_RUNTIME === 'edge') { @@ -1410,7 +1377,7 @@ export async function renderToHTMLImpl( } return { - bodyResult, + contentHTML, documentElement, head, headTags: [], @@ -1577,12 +1544,7 @@ export async function renderToHTMLImpl( prefix += '' } - const content = await streamToString( - chainStreams( - streamFromString(prefix), - await documentResult.bodyResult(renderTargetSuffix) - ) - ) + const content = prefix + documentResult.contentHTML + renderTargetSuffix const optimizedHtml = await postProcessHTML(pathname, content, renderOpts, { inAmpMode, diff --git a/packages/next/src/server/require.ts b/packages/next/src/server/require.ts index 255577c81910d3..edf9e9e865c25a 100644 --- a/packages/next/src/server/require.ts +++ b/packages/next/src/server/require.ts @@ -105,11 +105,11 @@ export function getPagePath( return pagePath } -export function requirePage( +export async function requirePage( page: string, distDir: string, isAppPath: boolean -): any { +): Promise { const pagePath = getPagePath(page, distDir, undefined, isAppPath) if (pagePath.endsWith('.html')) { return promises.readFile(pagePath, 'utf8').catch((err) => { diff --git a/packages/next/src/server/send-response.ts b/packages/next/src/server/send-response.ts index 20dd088b788bb8..070b400e7d053b 100644 --- a/packages/next/src/server/send-response.ts +++ b/packages/next/src/server/send-response.ts @@ -1,5 +1,5 @@ import type { BaseNextRequest, BaseNextResponse } from './base-http' -import type { NodeNextResponse } from './base-http/node' +import { isNodeNextResponse } from './base-http/helpers' import { pipeToNodeResponse } from './pipe-readable' import { splitCookiesString } from './web/utils' @@ -17,8 +17,12 @@ export async function sendResponse( response: Response, waitUntil?: Promise ): Promise { - // Don't use in edge runtime - if (process.env.NEXT_RUNTIME !== 'edge') { + if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME !== 'edge' && + isNodeNextResponse(res) + ) { // Copy over the response status. res.statusCode = response.status res.statusMessage = response.statusText @@ -43,7 +47,7 @@ export async function sendResponse( * See packages/next/server/next-server.ts */ - const originalResponse = (res as NodeNextResponse).originalResponse + const { originalResponse } = res // A response body must not be sent for HEAD requests. See https://httpwg.org/specs/rfc9110.html#HEAD if (response.body && req.method !== 'HEAD') { diff --git a/packages/next/src/server/setup-http-agent-env.ts b/packages/next/src/server/setup-http-agent-env.ts index 6c4deea8c2a46b..4e0bea31b09dd8 100644 --- a/packages/next/src/server/setup-http-agent-env.ts +++ b/packages/next/src/server/setup-http-agent-env.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from '../../types' +import type { NextConfig } from '../types' import { Agent as HttpAgent } from 'http' import { Agent as HttpsAgent } from 'https' diff --git a/packages/next/src/server/web-server.ts b/packages/next/src/server/web-server.ts index 3c983a1f99f251..2419ed5477c34e 100644 --- a/packages/next/src/server/web-server.ts +++ b/packages/next/src/server/web-server.ts @@ -54,7 +54,13 @@ interface WebServerOptions extends Options { } } -export default class NextWebServer extends BaseServer { +type WebRouteHandler = RouteHandler + +export default class NextWebServer extends BaseServer< + WebServerOptions, + WebNextRequest, + WebNextResponse +> { constructor(options: WebServerOptions) { super(options) @@ -152,7 +158,7 @@ export default class NextWebServer extends BaseServer { return this.serverOptions.webServerConfig.extendRenderOpts.nextFontManifest } - protected handleCatchallRenderRequest: RouteHandler = async ( + protected handleCatchallRenderRequest: WebRouteHandler = async ( req, res, parsedUrl diff --git a/packages/next/src/server/web/spec-extension/adapters/next-request.ts b/packages/next/src/server/web/spec-extension/adapters/next-request.ts index caf766e70ffe12..34ca8b83d95e89 100644 --- a/packages/next/src/server/web/spec-extension/adapters/next-request.ts +++ b/packages/next/src/server/web/spec-extension/adapters/next-request.ts @@ -6,6 +6,7 @@ import type { Writable } from 'node:stream' import { getRequestMeta } from '../../../request-meta' import { fromNodeOutgoingHttpHeaders } from '../../utils' import { NextRequest } from '../request' +import { isNodeNextRequest, isWebNextRequest } from '../../../base-http/helpers' export const ResponseAbortedName = 'ResponseAborted' export class ResponseAborted extends Error { @@ -57,15 +58,23 @@ export class NextRequestAdapter { request: BaseNextRequest, signal: AbortSignal ): NextRequest { - // TODO: look at refining this check - if ('request' in request && (request as WebNextRequest).request) { - return NextRequestAdapter.fromWebNextRequest(request as WebNextRequest) + if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME === 'edge' && + isWebNextRequest(request) + ) { + return NextRequestAdapter.fromWebNextRequest(request) + } else if ( + // The type check here ensures that `req` is correctly typed, and the + // environment variable check provides dead code elimination. + process.env.NEXT_RUNTIME !== 'edge' && + isNodeNextRequest(request) + ) { + return NextRequestAdapter.fromNodeNextRequest(request, signal) + } else { + throw new Error('Invariant: Unsupported NextRequest type') } - - return NextRequestAdapter.fromNodeNextRequest( - request as NodeNextRequest, - signal - ) } public static fromNodeNextRequest( diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index ca98abeb119646..e700985c714532 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -54,6 +54,9 @@ export function unstable_cache( cb: T, keyParts?: string[], options: { + /** + * The revalidation interval in seconds. + */ revalidate?: number | false tags?: string[] } = {} diff --git a/packages/next/src/shared/lib/get-hostname.test.ts b/packages/next/src/shared/lib/get-hostname.test.ts index 9130f5445d9c2c..f8f7dbf8dffda9 100644 --- a/packages/next/src/shared/lib/get-hostname.test.ts +++ b/packages/next/src/shared/lib/get-hostname.test.ts @@ -26,14 +26,17 @@ describe('getHostname', () => { // With headers. expect(getHostname(parsed, { host: parsed.host })).toBe(hostname) // With an empty headers array. + // @ts-expect-error passing an array of strings is not allowed expect(getHostname(parsed, { host: [] })).toBe(hostname) // With a headers array. + // @ts-expect-error passing an array of strings is not allowed expect(getHostname({}, { host: [parsed.host] })).toBe(undefined) }) }) it('should return undefined for empty input', () => { expect(getHostname({})).toBe(undefined) + // @ts-expect-error passing an array of strings is not allowed expect(getHostname({}, { host: [] })).toBe(undefined) }) }) diff --git a/packages/next/src/shared/lib/html-context.shared-runtime.ts b/packages/next/src/shared/lib/html-context.shared-runtime.ts index a1c060ef8f2fac..7b36d909e7a5e3 100644 --- a/packages/next/src/shared/lib/html-context.shared-runtime.ts +++ b/packages/next/src/shared/lib/html-context.shared-runtime.ts @@ -1,5 +1,5 @@ import type { BuildManifest } from '../../server/get-page-files' -import type { ServerRuntime } from 'next/types' +import type { ServerRuntime } from '../../types' import type { NEXT_DATA } from './utils' import type { FontConfig } from '../../server/font-utils' import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin' diff --git a/packages/next/src/shared/lib/is-internal.ts b/packages/next/src/shared/lib/is-internal.ts new file mode 100644 index 00000000000000..d22e12559a4513 --- /dev/null +++ b/packages/next/src/shared/lib/is-internal.ts @@ -0,0 +1,20 @@ +/** React that's compiled with `next`. Used by App Router. */ +export const reactVendoredRe = + /[\\/]next[\\/]dist[\\/]compiled[\\/](react|react-dom|react-server-dom-(webpack|turbopack)|scheduler)[\\/]/ + +/** React the user installed. Used by Pages Router, or user imports in App Router. */ +export const reactNodeModulesRe = + /node_modules[\\/](react|react-dom|scheduler)[\\/]/ + +export const nextInternalsRe = + /(node_modules[\\/]next[\\/]|[\\/].next[\\/]static[\\/]chunks[\\/]webpack\.js$|(edge-runtime-webpack|webpack-runtime)\.js$)/ + +export default function isInternal(file: string | null) { + if (!file) return false + + return ( + nextInternalsRe.test(file) || + reactVendoredRe.test(file) || + reactNodeModulesRe.test(file) + ) +} diff --git a/packages/next/src/shared/lib/lazy-dynamic/preload-css.tsx b/packages/next/src/shared/lib/lazy-dynamic/preload-css.tsx index cc1549e970cc07..aac260fc2b2518 100644 --- a/packages/next/src/shared/lib/lazy-dynamic/preload-css.tsx +++ b/packages/next/src/shared/lib/lazy-dynamic/preload-css.tsx @@ -1,15 +1,14 @@ 'use client' +import { getExpectedRequestStore } from '../../../client/components/request-async-storage.external' + export function PreloadCss({ moduleIds }: { moduleIds: string[] | undefined }) { // Early return in client compilation and only load requestStore on server side if (typeof window !== 'undefined') { return null } - const { - getExpectedRequestStore, - } = require('../../../client/components/request-async-storage.external') - const requestStore = getExpectedRequestStore() + const requestStore = getExpectedRequestStore('next/dynamic css') const allFiles = [] // Search the current dynamic call unique key id in react loadable manifest, diff --git a/packages/next/src/shared/lib/router/action-queue.ts b/packages/next/src/shared/lib/router/action-queue.ts index 07a652a03452b5..966b3cec083e94 100644 --- a/packages/next/src/shared/lib/router/action-queue.ts +++ b/packages/next/src/shared/lib/router/action-queue.ts @@ -48,6 +48,18 @@ function runRemainingActions( action: actionQueue.pending, setState, }) + } else { + // No more actions are pending, check if a refresh is needed + if (actionQueue.needsRefresh) { + actionQueue.needsRefresh = false + actionQueue.dispatch( + { + type: ACTION_REFRESH, + origin: window.location.origin, + }, + setState + ) + } } } } @@ -75,17 +87,6 @@ async function runAction({ function handleResult(nextState: AppRouterState) { // if we discarded this action, the state should also be discarded if (action.discarded) { - // if a refresh is needed, we only want to trigger it once the action queue is empty - if (actionQueue.needsRefresh && actionQueue.pending === null) { - actionQueue.needsRefresh = false - actionQueue.dispatch( - { - type: ACTION_REFRESH, - origin: window.location.origin, - }, - setState - ) - } return } diff --git a/packages/next/src/shared/lib/utils.ts b/packages/next/src/shared/lib/utils.ts index 98098d8128a275..6e82f21f8e80b0 100644 --- a/packages/next/src/shared/lib/utils.ts +++ b/packages/next/src/shared/lib/utils.ts @@ -5,7 +5,7 @@ import type { Env } from '@next/env' import type { IncomingMessage, ServerResponse } from 'http' import type { NextRouter } from './router/router' import type { ParsedUrlQuery } from 'querystring' -import type { PreviewData } from 'next/types' +import type { PreviewData } from '../../types' import type { COMPILER_NAMES } from './constants' import type fs from 'fs' diff --git a/packages/next/types/index.d.ts b/packages/next/src/types.ts similarity index 94% rename from packages/next/types/index.d.ts rename to packages/next/src/types.ts index 0f60e949d42dcd..2a375309a0f692 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/src/types.ts @@ -11,27 +11,22 @@ import type React from 'react' import type { ParsedUrlQuery } from 'querystring' import type { IncomingMessage, ServerResponse } from 'http' -import { +import type { NextPageContext, NextComponentType, NextApiResponse, NextApiRequest, NextApiHandler, - // @ts-ignore This path is generated at build time and conflicts otherwise -} from '../dist/shared/lib/utils' +} from './shared/lib/utils' -import type { - NextApiRequestCookies, - // @ts-ignore This path is generated at build time and conflicts otherwise -} from '../dist/server/api-utils' +import type { NextApiRequestCookies } from './server/api-utils' -// @ts-ignore This path is generated at build time and conflicts otherwise -import next from '../dist/server/next' +import next from './server/next' export type ServerRuntime = 'nodejs' | 'experimental-edge' | 'edge' | undefined // @ts-ignore This path is generated at build time and conflicts otherwise -export { NextConfig } from '../dist/server/config' +export { NextConfig } from './server/config' export type { Metadata, @@ -41,8 +36,7 @@ export type { Viewport, ResolvingViewport, ResolvedViewport, - // @ts-ignore This path is generated at build time and conflicts otherwise -} from '../dist/lib/metadata/types/metadata-interface' +} from './lib/metadata/types/metadata-interface' /** * Stub route type for typedRoutes before `next dev` or `next build` is run @@ -160,7 +154,7 @@ export type PageConfig = { unstable_excludeFiles?: string[] } -export { +export type { NextPageContext, NextComponentType, NextApiResponse, diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index e8f536d2f57e06..558e9c0a453cca 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -2576,7 +2576,10 @@ export async function trace(task, opts) { } export async function build(task, opts) { - await task.serial(['precompile', 'compile', 'generate_types'], opts) + await task.serial( + ['precompile', 'compile', 'generate_types', 'rewrite_compiled_references'], + opts + ) } export async function generate_types(task, opts) { @@ -2585,6 +2588,37 @@ export async function generate_types(task, opts) { }) } +/** + * TypeScript will emit references to the compiled types used to type the implementation. + * The declarations however don't need such detailed types. + * We rewrite the references to reference a more lightweight solution instead. + * @param {import('taskr').Task} task + */ +export async function rewrite_compiled_references(task, opts) { + const declarationDirectory = join(__dirname, 'dist') + const declarationFiles = glob.sync('**/*.d.ts', { cwd: declarationDirectory }) + + for (const declarationFile of declarationFiles) { + const content = await fs.readFile( + join(declarationDirectory, declarationFile), + 'utf8' + ) + // Rewrite + // /// + // to + // /// + if (content.indexOf('/types/$$compiled.internal.d.ts" />') !== -1) { + await fs.writeFile( + join(declarationDirectory, declarationFile), + content.replace( + /\/types\/\$\$compiled\.internal\.d\.ts" \/>/g, + '/types/compiled.d.ts" />' + ) + ) + } + } +} + export default async function (task) { const opts = { dev: true } await task.clear('dist') diff --git a/packages/next/types.d.ts b/packages/next/types.d.ts new file mode 100644 index 00000000000000..473727365d3a31 --- /dev/null +++ b/packages/next/types.d.ts @@ -0,0 +1,9 @@ +// Triple slash directives are copied from src/types.ts. +// TypeScript currently does not preserve the tripple-slash directives. +// Once https://github.com/microsoft/TypeScript/pull/57681 is released, we can remove the triple slash directives here. +/// +/// +/// +/// +export * from './dist/types' +export { default } from './dist/types' diff --git a/packages/next/types.js b/packages/next/types.js new file mode 100644 index 00000000000000..7f55d5d37f1c4e --- /dev/null +++ b/packages/next/types.js @@ -0,0 +1 @@ +// types-only diff --git a/packages/next/types/misc.d.ts b/packages/next/types/$$compiled.internal.d.ts similarity index 94% rename from packages/next/types/misc.d.ts rename to packages/next/types/$$compiled.internal.d.ts index 5adefbbac5333e..17dd16aa9d1d6e 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/$$compiled.internal.d.ts @@ -446,3 +446,33 @@ declare module 'next/dist/compiled/zod' { import * as m from 'zod' export = m } + +declare module 'mini-css-extract-plugin' +declare module 'next/dist/compiled/loader-utils3' + +declare module 'next/dist/compiled/webpack/webpack' { + import type webpackSources from 'webpack-sources1' + export function init(): void + export let BasicEvaluatedExpression: any + export let GraphHelpers: any + export let sources: typeof webpackSources + export let StringXor: any + export { + default as webpack, + Compiler, + Compilation, + Module, + Stats, + Template, + RuntimeModule, + RuntimeGlobals, + NormalModule, + ResolvePluginInstance, + ModuleFilenameHelpers, + } from 'webpack' + export type { + LoaderDefinitionFunction, + LoaderContext, + ModuleGraph, + } from 'webpack' +} diff --git a/packages/next/types/webpack.d.ts b/packages/next/types/webpack.d.ts deleted file mode 100644 index da080ab6d13f7f..00000000000000 --- a/packages/next/types/webpack.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars , no-shadow */ -// Type definitions for webpack 4.39 -// Project: https://github.com/webpack/webpack -// Definitions by: Qubo -// Benjamin Lim -// Boris Cherny -// Tommy Troy Lin -// Mohsen Azimi -// Jonathan Creamer -// Alan Agius -// Spencer Elliott -// Jason Cheatham -// Dennis George -// Christophe Hurpeau -// ZSkycat -// John Reilly -// Ryan Waskiewicz -// Kyle Uehlein -// Grgur Grisogono -// Rubens Pinheiro Gonçalves Cavalcante -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.3 - -/// - -declare module 'mini-css-extract-plugin' -declare module 'next/dist/compiled/loader-utils3' - -declare module 'next/dist/compiled/webpack/webpack' { - import type webpackSources from 'webpack-sources1' - export function init(): void - export let BasicEvaluatedExpression: any - export let GraphHelpers: any - export let sources: typeof webpackSources - export let StringXor: any - export { - default as webpack, - Compiler, - Compilation, - Module, - Stats, - Template, - RuntimeModule, - RuntimeGlobals, - NormalModule, - ResolvePluginInstance, - ModuleFilenameHelpers, - } from 'webpack' - export type { - LoaderDefinitionFunction, - LoaderContext, - ModuleGraph, - } from 'webpack' -} diff --git a/packages/next/webpack.config.js b/packages/next/webpack.config.js index 94ff70bff05abd..31ca02cf6b7f2a 100644 --- a/packages/next/webpack.config.js +++ b/packages/next/webpack.config.js @@ -177,6 +177,7 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => { 'this.serverOptions.experimentalTestProxy': JSON.stringify(false), 'this.minimalMode': JSON.stringify(true), 'this.renderOpts.dev': JSON.stringify(dev), + 'renderOpts.dev': JSON.stringify(dev), 'process.env.NODE_ENV': JSON.stringify( dev ? 'development' : 'production' ), diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 849afce488d510..d54586bb46f108 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": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 0a2a77d7b283b2..488010ade205de 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "14.2.1-canary.5", + "version": "14.3.0-canary.11", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "14.2.1-canary.5", + "next": "14.3.0-canary.11", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d76d412c8578bf..29d7da49957515 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,13 +8,13 @@ overrides: webpack: 5.90.0 browserslist: 4.22.2 caniuse-lite: 1.0.30001579 - '@types/node': 20.2.5 + '@types/node': 20.12.3 '@babel/core': 7.22.5 '@babel/parser': 7.22.5 '@babel/types': 7.22.5 '@babel/traverse': 7.22.5 - '@types/react': 18.2.37 - '@types/react-dom': 18.2.15 + '@types/react': 18.2.74 + '@types/react-dom': 18.2.23 importers: @@ -52,7 +52,7 @@ importers: version: 11.11.0 '@emotion/react': specifier: 11.11.1 - version: 11.11.1(@types/react@18.2.37)(react@18.2.0) + version: 11.11.1(@types/react@18.2.74)(react@18.2.0) '@fullhuman/postcss-purgecss': specifier: 1.3.0 version: 1.3.0 @@ -125,6 +125,9 @@ importers: '@testing-library/react': specifier: 13.0.0 version: 13.0.0(react-dom@18.2.0)(react@18.2.0) + '@types/busboy': + specifier: 1.5.3 + version: 1.5.3 '@types/cheerio': specifier: 0.22.16 version: 0.22.16 @@ -150,17 +153,17 @@ importers: specifier: 29.5.5 version: 29.5.5 '@types/node': - specifier: 20.2.5 - version: 20.2.5 + specifier: 20.12.3 + version: 20.12.3 '@types/node-fetch': specifier: 2.6.1 version: 2.6.1 '@types/react': - specifier: 18.2.37 - version: 18.2.37 + specifier: 18.2.74 + version: 18.2.74 '@types/react-dom': - specifier: 18.2.15 - version: 18.2.15 + specifier: 18.2.23 + version: 18.2.23 '@types/relay-runtime': specifier: 14.1.13 version: 14.1.13 @@ -178,10 +181,10 @@ importers: version: 2.0.3 '@typescript-eslint/eslint-plugin': specifier: 6.14.0 - version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.2.2) + version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': specifier: 6.14.0 - version: 6.14.0(eslint@8.56.0)(typescript@5.2.2) + version: 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@vercel/fetch': specifier: 6.1.1 version: 6.1.1(@types/node-fetch@2.6.1)(node-fetch@2.6.7) @@ -256,7 +259,7 @@ importers: version: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) eslint-plugin-jest: specifier: 27.6.3 - version: 27.6.3(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0)(jest@29.7.0)(typescript@5.2.2) + version: 27.6.3(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0)(jest@29.7.0)(typescript@5.3.3) eslint-plugin-jsdoc: specifier: 48.0.4 version: 48.0.4(eslint@8.56.0) @@ -328,7 +331,7 @@ importers: version: 3.0.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.2.5) + version: 29.7.0(@types/node@20.12.3) jest-environment-jsdom: specifier: 29.7.0 version: 29.7.0 @@ -535,13 +538,13 @@ importers: version: 1.2.2 tsec: specifier: 0.2.1 - version: 0.2.1(@bazel/bazelisk@1.18.0)(typescript@5.2.2) + version: 0.2.1(@bazel/bazelisk@1.18.0)(typescript@5.3.3) turbo: specifier: 1.12.5 version: 1.12.5 typescript: - specifier: 5.2.2 - version: 5.2.2 + specifier: 5.3.3 + version: 5.3.3 unfetch: specifier: 4.2.0 version: 4.2.0 @@ -690,8 +693,8 @@ importers: specifier: 6.0.0 version: 6.0.0 '@types/node': - specifier: 20.2.5 - version: 20.2.5 + specifier: 20.12.3 + version: 20.12.3 '@types/prompts': specifier: 2.4.2 version: 2.4.2 @@ -744,7 +747,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 14.2.1-canary.5 + specifier: 14.3.0-canary.11 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -806,7 +809,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 14.2.1-canary.5 + specifier: 14.3.0-canary.11 version: link:../next-env '@swc/helpers': specifier: 0.5.5 @@ -900,8 +903,8 @@ importers: specifier: 7.22.5 version: 7.22.5 '@capsizecss/metrics': - specifier: 2.2.0 - version: 2.2.0 + specifier: 3.0.0 + version: 3.0.0 '@edge-runtime/cookies': specifier: 4.1.1 version: 4.1.1 @@ -927,16 +930,16 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/polyfill-module': - specifier: 14.2.1-canary.5 + specifier: 14.3.0-canary.11 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 14.2.1-canary.5 + specifier: 14.3.0-canary.11 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 14.2.1-canary.5 + specifier: 14.3.0-canary.11 version: link:../react-refresh-utils '@next/swc': - specifier: 14.2.1-canary.5 + specifier: 14.3.0-canary.11 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1026,14 +1029,14 @@ importers: specifier: 1.3.4 version: 1.3.4 '@types/react': - specifier: 18.2.37 - version: 18.2.37 + specifier: 18.2.74 + version: 18.2.74 '@types/react-dom': - specifier: 18.2.15 - version: 18.2.15 + specifier: 18.2.23 + version: 18.2.23 '@types/react-is': - specifier: 17.0.3 - version: 17.0.3 + specifier: 18.2.4 + version: 18.2.4 '@types/semver': specifier: 7.3.1 version: 7.3.1 @@ -1068,8 +1071,8 @@ importers: specifier: 0.26.4 version: 0.26.4 '@vercel/turbopack-ecmascript-runtime': - specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240411.3 - version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240411.3' + specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240417.1 + version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240417.1' acorn: specifier: 8.5.0 version: 8.5.0 @@ -1243,7 +1246,7 @@ importers: version: 2.4.4(webpack@5.90.0) msw: specifier: 1.3.0 - version: 1.3.0(typescript@5.2.2) + version: 1.3.0(typescript@5.3.3) nanoid: specifier: 3.1.32 version: 3.1.32 @@ -1551,7 +1554,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 14.2.1-canary.5 + specifier: 14.3.0-canary.11 version: link:../next outdent: specifier: 0.8.0 @@ -3385,8 +3388,8 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@capsizecss/metrics@2.2.0: - resolution: {integrity: sha512-DkFIser1KbGxWyG2hhQQeCit72TnOQDx5pr9bkA7+XlIy7qv+4lYtslH3bidVxm2qkY2guAgypSIPYuQQuk70A==} + /@capsizecss/metrics@3.0.0: + resolution: {integrity: sha512-GTOIvDhuZKBDLeLPaA4mgwuKVwm2eaKR6Oaa76reaSkWTa5GCSe6W+DWPKEUJqjJyxsvkOYpC8reQ8njk90t5w==} dev: true /@csstools/postcss-color-function@1.1.0(postcss@8.4.31): @@ -3607,7 +3610,7 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: true - /@emotion/react@11.11.1(@types/react@18.2.37)(react@18.2.0): + /@emotion/react@11.11.1(@types/react@18.2.74)(react@18.2.0): resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} peerDependencies: '@types/react': '*' @@ -3623,7 +3626,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.37 + '@types/react': 18.2.74 hoist-non-react-statics: 3.3.2 react: 18.2.0 dev: true @@ -4135,7 +4138,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.1.2 jest-message-util: 27.5.1 jest-util: 27.5.1 @@ -4147,7 +4150,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -4168,14 +4171,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 ansi-escapes: 4.3.2 chalk: 4.0.0 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.2.5) + jest-config: 29.7.0(@types/node@20.12.3) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4203,7 +4206,7 @@ packages: dependencies: '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-mock: 27.5.1 dev: true @@ -4213,7 +4216,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.5.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-mock: 29.7.0 dev: true @@ -4223,7 +4226,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-mock: 29.7.0 dev: true @@ -4250,7 +4253,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@sinonjs/fake-timers': 8.1.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-message-util: 27.5.1 jest-mock: 27.5.1 jest-util: 27.5.1 @@ -4262,7 +4265,7 @@ packages: dependencies: '@jest/types': 29.5.0 '@sinonjs/fake-timers': 10.2.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4274,7 +4277,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.2.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4316,7 +4319,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.19 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4475,7 +4478,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 '@types/yargs': 16.0.9 chalk: 4.1.2 @@ -4486,7 +4489,7 @@ packages: '@jest/schemas': 29.4.3 '@types/istanbul-lib-coverage': 2.0.3 '@types/istanbul-reports': 3.0.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 '@types/yargs': 17.0.10 chalk: 4.1.2 dev: true @@ -4498,7 +4501,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 '@types/yargs': 17.0.10 chalk: 4.0.0 dev: true @@ -5305,7 +5308,7 @@ packages: react: '>=16' dependencies: '@types/mdx': 2.0.3 - '@types/react': 18.2.37 + '@types/react': 18.2.74 react: 18.2.0 /@mswjs/cookies@0.2.2: @@ -5880,7 +5883,7 @@ packages: resolution: {integrity: sha512-ZNAy8z77ewKZ5LCX0KaUm4tWdgloWQ6FWJCh06qgahq/MH13sQefIPKSo0dBdPU3bcioltyZUcC0k8oHHfjvnQ==} dependencies: '@octokit/openapi-types': 4.0.2 - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@octokit/webhooks-methods@4.0.0: @@ -6661,7 +6664,7 @@ packages: chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.5.13 - jest: 29.7.0(@types/node@20.2.5) + jest: 29.7.0(@types/node@20.12.3) lodash: 4.17.20 redent: 3.0.0 dev: true @@ -6675,7 +6678,7 @@ packages: dependencies: '@babel/runtime': 7.22.5 '@testing-library/dom': 8.20.0 - '@types/react-dom': 18.2.15 + '@types/react-dom': 18.2.23 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true @@ -6703,7 +6706,7 @@ packages: /@types/amphtml-validator@1.0.0: resolution: {integrity: sha512-CJOi00fReT1JehItkgTZDI47v9WJxUH/OLX0XzkDgyEed7dGdeUQfXk5CTRM7N9FkHdv3klSjsZxo5sH1oTIGg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/aria-query@5.0.1: @@ -6777,13 +6780,19 @@ packages: resolution: {integrity: sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==} dependencies: '@types/connect': 3.4.33 - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/btoa-lite@1.0.0: resolution: {integrity: sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==} dev: true + /@types/busboy@1.5.3: + resolution: {integrity: sha512-YMBLFN/xBD8bnqywIlGyYqsNFXu6bsiY7h3Ae0kO17qEuTjsqeyYMRPSUDacIKIquws2Y6KjmxAyNx8xB3xQbw==} + dependencies: + '@types/node': 20.12.3 + dev: true + /@types/bytes@3.1.1: resolution: {integrity: sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==} dev: true @@ -6791,7 +6800,7 @@ packages: /@types/cheerio@0.22.16: resolution: {integrity: sha512-bSbnU/D4yzFdzLpp3+rcDj0aQQMIRUBNJU7azPxdqMpnexjUSvGJyDuOBQBHeOZh1mMKgsJm6Dy+LLh80Ew4tQ==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/ci-info@2.0.0: @@ -6807,7 +6816,7 @@ packages: /@types/connect@3.4.33: resolution: {integrity: sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/content-disposition@0.5.4: @@ -6829,7 +6838,7 @@ packages: /@types/cross-spawn@6.0.0: resolution: {integrity: sha512-evp2ZGsFw9YKprDbg8ySgC9NA15g3YgiI8ANkGmKKvvi0P2aDGYLPxQIC5qfeKNUOe3TjABVGuah6omPRpIYhg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/debug@4.1.5: @@ -6875,7 +6884,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 '@types/qs': 6.9.7 '@types/range-parser': 1.2.3 dev: true @@ -6891,7 +6900,7 @@ packages: /@types/fontkit@2.0.0: resolution: {integrity: sha512-Qe+6szpPLTNsqkDFs2MScJyB51d5Hpobyg/T0QoUWO53WuNOTNLsV8fkE4QQPOJbhOMN5wlwmNeDdsh/e6Uqdg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/fresh@0.5.0: @@ -6901,7 +6910,7 @@ packages: /@types/fs-extra@8.1.0: resolution: {integrity: sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/glob@7.1.1: @@ -6909,13 +6918,13 @@ packages: dependencies: '@types/events': 3.0.0 '@types/minimatch': 3.0.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/hast@2.3.1: @@ -6926,13 +6935,13 @@ packages: /@types/html-validator@5.0.3: resolution: {integrity: sha512-QcKpR0cAWhLy7T3J24dwCuviRyS8xj/gVtFxcfZer9lfRgHdSAUFvE02nI/rsgIxSr71Z/2moXVYvWl2fTAzHQ==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/http-proxy@1.17.3: resolution: {integrity: sha512-wIPqXANye5BbORbuh74exbwNzj+UWCwWyeEFJzUQ7Fq3W2NSAy+7x7nX1fgbEypr2/TdKqpeuxLnXWgzN533/Q==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/inquirer@8.2.9: @@ -6986,7 +6995,7 @@ packages: /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 '@types/tough-cookie': 4.0.3 parse5: 7.1.2 dev: true @@ -7000,13 +7009,13 @@ packages: /@types/jsonwebtoken@9.0.0: resolution: {integrity: sha512-mM4TkDpA9oixqg1Fv2vVpOFyIVLJjm5x4k0V+K/rEsizfjD7Tk7LKk3GTtbB7KCfP0FEHQtsZqFxYA0+sijNVg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/keyv@3.1.1: resolution: {integrity: sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/lodash.curry@4.1.6: @@ -7057,18 +7066,20 @@ packages: /@types/node-fetch@2.3.2: resolution: {integrity: sha512-yW0EOebSsQme9yKu09XbdDfle4/SmWZMK4dfteWcSLCYNQQcF+YOv0kIrvm+9pO11/ghA4E6A+RNQqvYj4Nr3A==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/node-fetch@2.6.1: resolution: {integrity: sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 form-data: 3.0.1 dev: true - /@types/node@20.2.5: - resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==} + /@types/node@20.12.3: + resolution: {integrity: sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==} + dependencies: + undici-types: 5.26.5 /@types/normalize-package-data@2.4.0: resolution: {integrity: sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==} @@ -7103,7 +7114,7 @@ packages: /@types/prompts@2.4.2: resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 kleur: 3.0.3 dev: true @@ -7122,23 +7133,22 @@ packages: resolution: {integrity: sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==} dev: true - /@types/react-dom@18.2.15: - resolution: {integrity: sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==} + /@types/react-dom@18.2.23: + resolution: {integrity: sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==} dependencies: - '@types/react': 18.2.37 + '@types/react': 18.2.74 dev: true - /@types/react-is@17.0.3: - resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==} + /@types/react-is@18.2.4: + resolution: {integrity: sha512-wBc7HgmbCcrvw0fZjxbgz/xrrlZKzEqmABBMeSvpTvdm25u6KI6xdIi9pRE2G0C1Lw5ETFdcn4UbYZ4/rpqUYw==} dependencies: - '@types/react': 18.2.37 + '@types/react': 18.2.74 dev: true - /@types/react@18.2.37: - resolution: {integrity: sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==} + /@types/react@18.2.74: + resolution: {integrity: sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==} dependencies: '@types/prop-types': 15.7.8 - '@types/scheduler': 0.16.4 csstype: 3.1.2 /@types/relay-runtime@14.1.13: @@ -7148,22 +7158,19 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/retry@0.12.0: resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} dev: true - /@types/scheduler@0.16.4: - resolution: {integrity: sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==} - /@types/selenium-webdriver@4.0.15: resolution: {integrity: sha512-5760PIZkzhPejy3hsKAdCKe5LJygGdxLKOLxmZL9GEUcFlO5OgzM6G2EbdbvOnaw4xvUSa9Uip6Ipwkih12BPA==} dev: true @@ -7171,7 +7178,7 @@ packages: /@types/semver@7.3.1: resolution: {integrity: sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/semver@7.5.0: @@ -7186,7 +7193,7 @@ packages: resolution: {integrity: sha512-SCVCRRjSbpwoKgA34wK8cq14OUPu4qrKigO85/ZH6J04NGws37khLtq7YQr17zyOH01p4T5oy8e1TxEzql01Pg==} dependencies: '@types/mime': 2.0.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/serve-static@1.13.3: @@ -7199,13 +7206,13 @@ packages: /@types/set-cookie-parser@2.4.3: resolution: {integrity: sha512-7QhnH7bi+6KAhBB+Auejz1uV9DHiopZqu7LfR/5gZZTkejJV5nYeZZpgfFoE0N8aDsXuiYpfKyfyMatCwQhyTQ==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/sharp@0.29.3: resolution: {integrity: sha512-83Xp05eK2hvfNnmKLr2Fz0C2A0jrr2TnSLqKRbkLTYuAu+Erj6mKQLoEMGafE73Om8p3q3ryZxtHFM/7hy4Adg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/shell-quote@1.7.1: @@ -7226,7 +7233,7 @@ packages: /@types/tar@6.1.5: resolution: {integrity: sha512-qm2I/RlZij5RofuY7vohTpYNaYcrSQlN2MyjucQc7ZweDwaEWkdN/EeNh6e9zjK6uEm6PwjdMXkcj05BxZdX1Q==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 minipass: 4.2.8 dev: true @@ -7237,7 +7244,7 @@ packages: /@types/through@0.0.30: resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/tough-cookie@4.0.3: @@ -7266,7 +7273,7 @@ packages: /@types/webpack-sources@0.1.5: resolution: {integrity: sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 '@types/source-list-map': 0.1.2 source-map: 0.6.1 dev: true @@ -7274,7 +7281,7 @@ packages: /@types/ws@8.2.0: resolution: {integrity: sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /@types/yargs-parser@21.0.0: @@ -7295,10 +7302,10 @@ packages: resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} requiresBuild: true dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 optional: true - /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.2.2): + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7310,10 +7317,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.14.0 - '@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4 eslint: 8.56.0 @@ -7321,8 +7328,8 @@ packages: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true @@ -7348,7 +7355,7 @@ packages: - supports-color dev: false - /@typescript-eslint/parser@6.14.0(eslint@8.56.0)(typescript@5.2.2): + /@typescript-eslint/parser@6.14.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7360,11 +7367,11 @@ packages: dependencies: '@typescript-eslint/scope-manager': 6.14.0 '@typescript-eslint/types': 6.14.0 - '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4 eslint: 8.56.0 - typescript: 5.2.2 + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true @@ -7384,7 +7391,7 @@ packages: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0 - /@typescript-eslint/type-utils@6.14.0(eslint@8.56.0)(typescript@5.2.2): + /@typescript-eslint/type-utils@6.14.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7394,12 +7401,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.14.0(eslint@8.56.0)(typescript@5.3.3) debug: 4.3.4 eslint: 8.56.0 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true @@ -7413,7 +7420,7 @@ packages: resolution: {integrity: sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==} engines: {node: ^16.0.0 || >=18.0.0} - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.2.2): + /@typescript-eslint/typescript-estree@5.62.0(typescript@5.3.3): resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7428,8 +7435,8 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.7 - tsutils: 3.21.0(typescript@5.2.2) - typescript: 5.2.2 + tsutils: 3.21.0(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true @@ -7455,7 +7462,7 @@ packages: - supports-color dev: false - /@typescript-eslint/typescript-estree@6.14.0(typescript@5.2.2): + /@typescript-eslint/typescript-estree@6.14.0(typescript@5.3.3): resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7470,13 +7477,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.2.2): + /@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7487,7 +7494,7 @@ packages: '@types/semver': 7.5.6 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) eslint: 8.56.0 eslint-scope: 5.1.1 semver: 7.3.7 @@ -7496,7 +7503,7 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.14.0(eslint@8.56.0)(typescript@5.2.2): + /@typescript-eslint/utils@6.14.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -7507,7 +7514,7 @@ packages: '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 6.14.0 '@typescript-eslint/types': 6.14.0 - '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) eslint: 8.56.0 semver: 7.5.4 transitivePeerDependencies: @@ -7716,7 +7723,7 @@ packages: dependencies: '@types/async-retry': 1.2.1 '@types/lru-cache': 4.1.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 async-retry: 1.2.3 lru-cache: 5.1.1 dev: true @@ -10052,7 +10059,7 @@ packages: sha.js: 2.4.11 dev: true - /create-jest@29.7.0(@types/node@20.2.5): + /create-jest@29.7.0(@types/node@20.12.3): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -10061,7 +10068,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.2.5) + jest-config: 29.7.0(@types/node@20.12.3) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -11649,7 +11656,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) debug: 3.2.7 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 @@ -11713,7 +11720,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -11738,7 +11745,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest@27.6.3(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0)(jest@29.7.0)(typescript@5.2.2): + /eslint-plugin-jest@27.6.3(@typescript-eslint/eslint-plugin@6.14.0)(eslint@8.56.0)(jest@29.7.0)(typescript@5.3.3): resolution: {integrity: sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -11751,10 +11758,10 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.2.2) - '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.2.2) + '@typescript-eslint/eslint-plugin': 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.3.3) eslint: 8.56.0 - jest: 29.7.0(@types/node@20.2.5) + jest: 29.7.0(@types/node@20.12.3) transitivePeerDependencies: - supports-color - typescript @@ -15159,7 +15166,7 @@ packages: '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.0.0 co: 4.6.0 dedent: 0.7.0 @@ -15187,7 +15194,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -15208,7 +15215,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@20.2.5): + /jest-cli@29.7.0(@types/node@20.12.3): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -15222,10 +15229,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.0.0 - create-jest: 29.7.0(@types/node@20.2.5) + create-jest: 29.7.0(@types/node@20.12.3) exit: 0.1.2 import-local: 3.0.2 - jest-config: 29.7.0(@types/node@20.2.5) + jest-config: 29.7.0(@types/node@20.12.3) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.5.1 @@ -15236,11 +15243,11 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.2.5): + /jest-config@29.7.0(@types/node@20.12.3): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 ts-node: '>=9.0.0' peerDependenciesMeta: '@types/node': @@ -15251,7 +15258,7 @@ packages: '@babel/core': 7.22.5 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 babel-jest: 29.7.0(@babel/core@7.22.5) chalk: 4.1.2 ci-info: 3.8.0 @@ -15337,7 +15344,7 @@ packages: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -15354,7 +15361,7 @@ packages: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-mock: 27.5.1 jest-util: 27.5.1 dev: true @@ -15366,7 +15373,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -15380,7 +15387,7 @@ packages: jest: optional: true dependencies: - jest: 29.7.0(@types/node@20.2.5) + jest: 29.7.0(@types/node@20.12.3) jest-diff: 29.7.0 jest-get-type: 29.6.3 dev: true @@ -15400,7 +15407,7 @@ packages: dependencies: '@jest/types': 27.5.1 '@types/graceful-fs': 4.1.9 - '@types/node': 20.2.5 + '@types/node': 20.12.3 anymatch: 3.1.3 fb-watchman: 2.0.1 graceful-fs: 4.2.11 @@ -15420,7 +15427,7 @@ packages: dependencies: '@jest/types': 29.5.0 '@types/graceful-fs': 4.1.9 - '@types/node': 20.2.5 + '@types/node': 20.12.3 anymatch: 3.1.3 fb-watchman: 2.0.1 graceful-fs: 4.2.11 @@ -15439,7 +15446,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.2.5 + '@types/node': 20.12.3 anymatch: 3.1.3 fb-watchman: 2.0.1 graceful-fs: 4.2.11 @@ -15532,7 +15539,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true /jest-mock@29.5.0: @@ -15540,7 +15547,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-util: 29.7.0 dev: true @@ -15549,7 +15556,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-util: 29.7.0 dev: true @@ -15641,7 +15648,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -15702,7 +15709,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -15725,7 +15732,7 @@ packages: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 graceful-fs: 4.2.11 dev: true @@ -15792,7 +15799,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -15804,7 +15811,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.5.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.0.0 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -15816,7 +15823,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 chalk: 4.0.0 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -15853,7 +15860,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.2.5 + '@types/node': 20.12.3 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -15865,7 +15872,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -15874,7 +15881,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -15882,7 +15889,7 @@ packages: resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -15892,13 +15899,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@20.2.5): + /jest@29.7.0(@types/node@20.12.3): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -15911,7 +15918,7 @@ packages: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.0.2 - jest-cli: 29.7.0(@types/node@20.2.5) + jest-cli: 29.7.0(@types/node@20.12.3) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -17916,7 +17923,7 @@ packages: isarray: 1.0.0 dev: true - /msw@1.3.0(typescript@5.2.2): + /msw@1.3.0(typescript@5.3.3): resolution: {integrity: sha512-nnWAZlQyQOKeYRblCpseT1kSPt1aF5e/jHz1hn/18IxbsMFreSVV1cJriT0uV+YG6+wvwFRMHXU3zVuMvuwERQ==} engines: {node: '>=14'} hasBin: true @@ -17945,7 +17952,7 @@ packages: path-to-regexp: 6.2.1 strict-event-emitter: 0.4.6 type-fest: 2.19.0 - typescript: 5.2.2 + typescript: 5.3.3 yargs: 17.5.1 transitivePeerDependencies: - encoding @@ -20837,7 +20844,7 @@ packages: '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 '@types/long': 4.0.1 - '@types/node': 20.2.5 + '@types/node': 20.12.3 long: 4.0.0 dev: true @@ -20856,7 +20863,7 @@ packages: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.2.5 + '@types/node': 20.12.3 long: 5.2.3 dev: true @@ -23960,13 +23967,13 @@ packages: typescript: 4.8.2 dev: false - /ts-api-utils@1.0.1(typescript@5.2.2): + /ts-api-utils@1.0.1(typescript@5.3.3): resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} engines: {node: '>=16.13.0'} peerDependencies: typescript: '>=4.2.0' dependencies: - typescript: 5.2.2 + typescript: 5.3.3 dev: true /tsconfig-paths@3.14.2: @@ -23987,7 +23994,7 @@ packages: strip-bom: 3.0.0 dev: true - /tsec@0.2.1(@bazel/bazelisk@1.18.0)(typescript@5.2.2): + /tsec@0.2.1(@bazel/bazelisk@1.18.0)(typescript@5.3.3): resolution: {integrity: sha512-RP9vhbRbRI9VH4CfOlQvo5W9HdfiPKq0gdiUOWI5oKmLaZKNFN8CsPwBfT5ySmhnKNwmmAS/BtY3WoTfABwwig==} hasBin: true peerDependencies: @@ -23997,7 +24004,7 @@ packages: '@bazel/bazelisk': 1.18.0 glob: 7.1.7 minimatch: 3.1.2 - typescript: 5.2.2 + typescript: 5.3.3 dev: true /tslib@1.11.1: @@ -24026,14 +24033,14 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsutils@3.21.0(typescript@5.2.2): + /tsutils@3.21.0(typescript@5.3.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.2.2 + typescript: 5.3.3 dev: true /tty-browserify@0.0.1: @@ -24238,8 +24245,8 @@ packages: engines: {node: '>=4.2.0'} hasBin: true - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} engines: {node: '>=14.17'} hasBin: true dev: true @@ -24276,6 +24283,9 @@ packages: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /undici@5.26.3: resolution: {integrity: sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw==} engines: {node: '>=14.0'} @@ -25543,12 +25553,12 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240411.3': - resolution: {tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240411.3} + '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240417.1': + resolution: {tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240417.1} name: '@vercel/turbopack-ecmascript-runtime' version: 0.0.0 dependencies: - '@types/node': 20.2.5 + '@types/node': 20.12.3 dev: true github.com/watson/ci-info/f43f6a1cefff47fb361c88cf4b943fdbcaafe540: diff --git a/release.js b/release.js index 37a1c25689713f..ba4f2a58217cb3 100644 --- a/release.js +++ b/release.js @@ -1,8 +1,8 @@ // section -> label const sectionLabelMap = { 'Core Changes': 'type: next', - 'Documentation Changes': 'area: documentation', - 'Example Changes': 'area: examples', + 'Documentation Changes': 'documentation', + 'Example Changes': 'examples', } const fallbackSection = 'Misc Changes' diff --git a/run-tests.js b/run-tests.js index 32e0a5a4413464..257614be73dc8f 100644 --- a/run-tests.js +++ b/run-tests.js @@ -1,3 +1,5 @@ +//@ts-check + const os = require('os') const path = require('path') const _glob = require('glob') @@ -5,6 +7,7 @@ const { existsSync } = require('fs') const fsp = require('fs/promises') const nodeFetch = require('node-fetch') const vercelFetch = require('@vercel/fetch') +// @ts-expect-error const fetch = vercelFetch(nodeFetch) const { promisify } = require('util') const { Sema } = require('async-sema') @@ -25,6 +28,8 @@ let argv = require('yargs/yargs')(process.argv.slice(2)) .string('g') .alias('g', 'group') .number('c') + .boolean('related') + .alias('r', 'related') .alias('c', 'concurrency').argv function escapeRegexp(str) { @@ -197,6 +202,7 @@ async function main() { group: argv.group ?? false, testPattern: argv.testPattern ?? false, type: argv.type ?? false, + related: argv.related ?? false, retries: argv.retries ?? DEFAULT_NUM_RETRIES, } let numRetries = options.retries @@ -223,21 +229,33 @@ async function main() { console.log('Running tests with concurrency:', options.concurrency) /** @type TestFile[] */ - let tests = argv._.filter((arg) => arg.match(/\.test\.(js|ts|tsx)/)).map( - (file) => ({ - file, - excludedCases: [], - }) - ) + let tests = argv._.filter((arg) => + arg.toString().match(/\.test\.(js|ts|tsx)/) + ).map((file) => ({ file: file.toString(), excludedCases: [] })) let prevTimings if (tests.length === 0) { + /** @type {RegExp | undefined} */ let testPatternRegex - if (options.testPattern) { + if (options.testPattern && typeof options.testPattern === 'string') { testPatternRegex = new RegExp(options.testPattern) } + if (options.related) { + const { getRelatedTests } = await import('./scripts/run-related-test.mjs') + const tests = await getRelatedTests() + if (tests.length) + testPatternRegex = new RegExp(tests.map(escapeRegexp).join('|')) + + if (testPatternRegex) { + console.log('Running related tests:', testPatternRegex.toString()) + } else { + console.log('No matching related tests, exiting.') + process.exit(0) + } + } + tests = ( await glob('**/*.test.{js,ts,tsx}', { nodir: true, @@ -311,12 +329,13 @@ async function main() { return true }) - if (options.group) { + if (options.group && typeof options.group === 'string') { const groupParts = options.group.split('/') const groupPos = parseInt(groupParts[0], 10) const groupTotal = parseInt(groupParts[1], 10) if (prevTimings) { + /** @type {TestFile[][]} */ const groups = [[]] const groupTimes = [0] @@ -463,6 +482,7 @@ ${ENDGROUP}`) // Format the output of junit report to include the test name // For the debugging purpose to compare actual run list to the generated reports // [NOTE]: This won't affect if junit reporter is not enabled + // @ts-expect-error .replaceAll() does exist. Follow-up why TS is not recognizing it JEST_JUNIT_OUTPUT_NAME: test.file.replaceAll('/', '_'), // Specify suite name for the test to avoid unexpected merging across different env / grouped tests // This is not individual suites name (corresponding 'describe'), top level suite name which have redundant names by default @@ -553,6 +573,7 @@ ${ENDGROUP}`) const err = new Error( code ? `failed with code: ${code}` : `failed with signal: ${signal}` ) + // @ts-expect-error err.output = outputChunks .map(({ chunk }) => chunk.toString()) .join('') diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index fb98c0f091c331..00000000000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly-2024-04-03 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000000000..59c3a03ab6aafa --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2024-04-03" +components = ["rustfmt", "clippy"] +profile = "minimal" diff --git a/scripts/pull-turbo-cache.js b/scripts/pull-turbo-cache.js index 919618a347f611..fa7e9a06e6f286 100644 --- a/scripts/pull-turbo-cache.js +++ b/scripts/pull-turbo-cache.js @@ -42,6 +42,7 @@ const { spawn } = require('child_process') // pull cache if it was available if (task.cache.local || task.cache.remote) { + console.log('Cache Status', task.taskId, task.hash, task.cache) await new Promise((resolve, reject) => { const child = spawn( '/bin/bash', diff --git a/scripts/run-related-test.mjs b/scripts/run-related-test.mjs index 4dddae9b0e5675..2a92a6e9f8be6e 100644 --- a/scripts/run-related-test.mjs +++ b/scripts/run-related-test.mjs @@ -19,7 +19,9 @@ async function getChangedFilesFromPackages(baseBranch = 'canary') { await exec('git config --global --add safe.directory /work') await exec(`git remote set-branches --add origin ${baseBranch}`) await exec(`git fetch origin ${baseBranch} --depth=20`) - const { stdout } = await exec(`git diff --name-only ${baseBranch}`) + const { stdout } = await exec( + `git diff 'origin/${baseBranch}...' --name-only` + ) return stdout .trim() .split('\n') diff --git a/scripts/send-trace-to-jaeger/Cargo.toml b/scripts/send-trace-to-jaeger/Cargo.toml index 3ce726e52c10eb..3d04ccfcae7349 100644 --- a/scripts/send-trace-to-jaeger/Cargo.toml +++ b/scripts/send-trace-to-jaeger/Cargo.toml @@ -11,4 +11,4 @@ workspace = true [dependencies] serde_json = "1.0.59" -reqwest = { version = "0.11.6", features = ["blocking"] } \ No newline at end of file +reqwest = { version = "0.11.6", features = ["blocking"] } diff --git a/test/.stats-app/package.json b/test/.stats-app/package.json index 6df5695a60378f..299013ea72d1bd 100644 --- a/test/.stats-app/package.json +++ b/test/.stats-app/package.json @@ -9,7 +9,7 @@ }, "engines": { "node": ">=18.17.0", - "pnpm": "8.15.4" + "pnpm": "8.15.7" }, - "packageManager": "pnpm@8.15.4" + "packageManager": "pnpm@8.15.7" } diff --git a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts index e22624e77b709c..c6cf3fec0c3c8c 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts @@ -64,6 +64,9 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => { expect(await session.getRedboxSource()).toMatchInlineSnapshot(` "./node_modules/my-package/index.js:1:1 Module not found: Can't resolve 'dns' + > 1 | const dns = require('dns') + | ^ + 2 | module.exports = dns https://nextjs.org/docs/messages/module-not-found diff --git a/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts b/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts index 649078543793c7..7dea7a2af3dbe4 100644 --- a/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts +++ b/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts @@ -59,6 +59,9 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => { expect(await session.getRedboxSource()).toMatchInlineSnapshot(` "./node_modules/my-package/index.js:1:1 Module not found: Can't resolve 'dns' + > 1 | const dns = require('dns') + | ^ + 2 | module.exports = dns https://nextjs.org/docs/messages/module-not-found diff --git a/test/development/basic/next-rs-api.test.ts b/test/development/basic/next-rs-api.test.ts index 04cde12ede22d2..ce1137a93153f9 100644 --- a/test/development/basic/next-rs-api.test.ts +++ b/test/development/basic/next-rs-api.test.ts @@ -1,5 +1,5 @@ import { NextInstance, createNext } from 'e2e-utils' -import { trace } from 'next/src/trace' +import { trace } from 'next/dist/trace' import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' import { createDefineEnv, @@ -11,8 +11,8 @@ import { StyledString, TurbopackResult, UpdateInfo, -} from 'next/src/build/swc' -import loadConfig from 'next/src/server/config' +} from 'next/dist/build/swc' +import loadConfig from 'next/dist/server/config' import path from 'path' function normalizePath(path: string) { diff --git a/test/development/internal-traces/internal-traces.test.ts b/test/development/internal-traces/internal-traces.test.ts new file mode 100644 index 00000000000000..0b8b12e93dd616 --- /dev/null +++ b/test/development/internal-traces/internal-traces.test.ts @@ -0,0 +1,16 @@ +import { FileRef, nextTestSetup } from 'e2e-utils' +import path from 'path' + +describe('internal traces', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname)), + }) + + it('should not write long internal traces to stdio', async () => { + await next.render$('/traces') + expect(next.cliOutput.length).toBeLessThan(128 * 1024 /* 128kb of ascii */) + expect(next.cliOutput).not.toContain( + 'https://nextjs.org/docs/messages/large-page-data' + ) + }) +}) diff --git a/test/development/internal-traces/pages/traces.js b/test/development/internal-traces/pages/traces.js new file mode 100644 index 00000000000000..5f24bdb7c24df7 --- /dev/null +++ b/test/development/internal-traces/pages/traces.js @@ -0,0 +1,7 @@ +import * as a from 'next/dist/compiled/next-server/app-page-experimental.runtime.dev.js' + +console.log(a) + +export default function Traces() { + return
Traces
+} diff --git a/test/development/pages-dir/client-navigation/fixture/pages/head.js b/test/development/pages-dir/client-navigation/fixture/pages/head.js index bd50dddcc54d46..a143355259fa23 100644 --- a/test/development/pages-dir/client-navigation/fixture/pages/head.js +++ b/test/development/pages-dir/client-navigation/fixture/pages/head.js @@ -116,7 +116,9 @@ export default () => ( {/* this should not execute twice on the client */} - + + {/* this should have async set to false on the client */} + {/* this should not execute twice on the client (intentionally sets defer to `yas` to test boolean coercion) */} diff --git a/test/development/pages-dir/client-navigation/fixture/pages/script.js b/test/development/pages-dir/client-navigation/fixture/pages/script.js new file mode 100644 index 00000000000000..7e3558850fd92e --- /dev/null +++ b/test/development/pages-dir/client-navigation/fixture/pages/script.js @@ -0,0 +1,11 @@ +import React from 'react' +import Script from 'next/script' + +export default () => ( +
+

I am a page to test next/script

+