diff --git a/.github/actions/next-stats-action/src/prepare/repo-setup.js b/.github/actions/next-stats-action/src/prepare/repo-setup.js index 81fa2847a8aad..4f4a44ea67902 100644 --- a/.github/actions/next-stats-action/src/prepare/repo-setup.js +++ b/.github/actions/next-stats-action/src/prepare/repo-setup.js @@ -1,9 +1,13 @@ +// @ts-check const path = require('path') const fs = require('fs') const { existsSync } = require('fs') const exec = require('../util/exec') const logger = require('../util/logger') const execa = require('execa') +const mockSpan = require('../util/mock-trace') + +/** @typedef {import('../util/mock-trace').Span} Span */ module.exports = (actionInfo) => { return { @@ -56,10 +60,14 @@ module.exports = (actionInfo) => { }, /** * Runs `pnpm pack` on each package in the `packages` folder of the provided `repoDir` - * @param {{ repoDir: string, nextSwcVersion: null | string }} options Required options + * @param {{ repoDir: string, nextSwcVersion: null | string, parentSpan?: Span }} options Required options * @returns {Promise>} List packages key is the package name, value is the path to the packed tar file.' */ - async linkPackages({ repoDir, nextSwcVersion }) { + async linkPackages({ repoDir, nextSwcVersion, parentSpan }) { + if (!parentSpan) { + // Not all callers provide a parent span + parentSpan = mockSpan() + } /** @type {Map} */ const pkgPaths = new Map() /** @type {Map} */ @@ -68,9 +76,11 @@ module.exports = (actionInfo) => { let packageFolders try { - packageFolders = await fs.promises.readdir( - path.join(repoDir, 'packages') - ) + packageFolders = await parentSpan + .traceChild('read-packages-folder') + .traceAsyncFn(() => + fs.promises.readdir(path.join(repoDir, 'packages')) + ) } catch (err) { if (err.code === 'ENOENT') { require('console').log('no packages to link') @@ -79,162 +89,188 @@ module.exports = (actionInfo) => { throw err } - for (const packageFolder of packageFolders) { - const packagePath = path.join(repoDir, 'packages', packageFolder) - const packedPackageTarPath = path.join( - packagePath, - `${packageFolder}-packed.tgz` - ) - const packageJsonPath = path.join(packagePath, 'package.json') - - if (!existsSync(packageJsonPath)) { - require('console').log(`Skipping ${packageFolder}, no package.json`) - continue - } - - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath)) - const { name: packageName } = packageJson + parentSpan.traceChild('get-pkgdatas').traceFn(() => { + for (const packageFolder of packageFolders) { + const packagePath = path.join(repoDir, 'packages', packageFolder) + const packedPackageTarPath = path.join( + packagePath, + `${packageFolder}-packed.tgz` + ) + const packageJsonPath = path.join(packagePath, 'package.json') - pkgDatas.set(packageName, { - packageJsonPath, - packagePath, - packageJson, - packedPackageTarPath, - }) - pkgPaths.set(packageName, packedPackageTarPath) - } + if (!existsSync(packageJsonPath)) { + require('console').log(`Skipping ${packageFolder}, no package.json`) + continue + } - for (const [ - packageName, - { packageJsonPath, packagePath, packageJson }, - ] of pkgDatas.entries()) { - // This loops through all items to get the packagedPkgPath of each item and add it to pkgData.dependencies - for (const [ - packageName, - { packedPackageTarPath }, - ] of pkgDatas.entries()) { - if ( - !packageJson.dependencies || - !packageJson.dependencies[packageName] + const packageJson = JSON.parse( + fs.readFileSync(packageJsonPath, 'utf-8') ) - continue - // Edit the pkgData of the current item to point to the packed tgz - packageJson.dependencies[packageName] = packedPackageTarPath + const { name: packageName } = packageJson + + pkgDatas.set(packageName, { + packageJsonPath, + packagePath, + packageJson, + packedPackageTarPath, + }) + pkgPaths.set(packageName, packedPackageTarPath) } + }) - // make sure native binaries are included in local linking - if (packageName === '@next/swc') { - packageJson.files ||= [] + await parentSpan + .traceChild('write-packagejson') + .traceAsyncFn(async () => { + for (const [ + packageName, + { packageJsonPath, packagePath, packageJson }, + ] of pkgDatas.entries()) { + // This loops through all items to get the packagedPkgPath of each item and add it to pkgData.dependencies + for (const [ + packageName, + { packedPackageTarPath }, + ] of pkgDatas.entries()) { + if ( + !packageJson.dependencies || + !packageJson.dependencies[packageName] + ) + continue + // Edit the pkgData of the current item to point to the packed tgz + packageJson.dependencies[packageName] = packedPackageTarPath + } - packageJson.files.push('native') + // make sure native binaries are included in local linking + if (packageName === '@next/swc') { + packageJson.files ||= [] - try { - const swcBinariesDirContents = ( - await fs.promises.readdir(path.join(packagePath, 'native')) - ).filter((file) => file !== '.gitignore' && file !== 'index.d.ts') + packageJson.files.push('native') - require('console').log( - 'using swc binaries: ', - swcBinariesDirContents.join(', ') - ) - } catch (err) { - if (err.code === 'ENOENT') { - require('console').log('swc binaries dir is missing!') - } - throw err - } - } else if (packageName === 'next') { - const nextSwcPkg = pkgDatas.get('@next/swc') + try { + const swcBinariesDirContents = ( + await fs.promises.readdir(path.join(packagePath, 'native')) + ).filter( + (file) => file !== '.gitignore' && file !== 'index.d.ts' + ) - console.log('using swc dep', { - nextSwcVersion, - nextSwcPkg, - }) - if (nextSwcVersion) { - Object.assign(packageJson.dependencies, { - '@next/swc-linux-x64-gnu': nextSwcVersion, - }) - } else { - if (nextSwcPkg) { - packageJson.dependencies['@next/swc'] = - nextSwcPkg.packedPackageTarPath + require('console').log( + 'using swc binaries: ', + swcBinariesDirContents.join(', ') + ) + } catch (err) { + if (err.code === 'ENOENT') { + require('console').log('swc binaries dir is missing!') + } + throw err + } + } else if (packageName === 'next') { + const nextSwcPkg = pkgDatas.get('@next/swc') + + console.log('using swc dep', { + nextSwcVersion, + nextSwcPkg, + }) + if (nextSwcVersion) { + Object.assign(packageJson.dependencies, { + '@next/swc-linux-x64-gnu': nextSwcVersion, + }) + } else { + } } + + await fs.promises.writeFile( + packageJsonPath, + JSON.stringify(packageJson, null, 2), + 'utf8' + ) } - } + }) - await fs.promises.writeFile( - packageJsonPath, - JSON.stringify(packageJson, null, 2), - 'utf8' - ) - } + await parentSpan + .traceChild('pnpm-packing') + .traceAsyncFn(async (packingSpan) => { + // wait to pack packages until after dependency paths have been updated + // to the correct versions + await Promise.all( + Array.from(pkgDatas.entries()).map( + async ([ + packageName, + { packagePath: pkgPath, packedPackageTarPath: packedPkgPath }, + ]) => { + return packingSpan + .traceChild('handle-package', { packageName }) + .traceAsyncFn(async (handlePackageSpan) => { + /** @type {null | (() => Promise)} */ + let cleanup = null - // wait to pack packages until after dependency paths have been updated - // to the correct versions - await Promise.all( - Array.from(pkgDatas.entries()).map( - async ([ - packageName, - { packagePath: pkgPath, packedPackageTarPath: packedPkgPath }, - ]) => { - /** @type {null | () => Promise} */ - let cleanup = null + if (packageName === '@next/swc') { + // next-swc uses a gitignore to prevent the committing of native builds but it doesn't + // use files in package.json because it publishes to individual packages based on architecture. + // When we used yarn to pack these packages the gitignore was ignored so the native builds were packed + // however npm does respect gitignore when packing so we need to remove it in this specific case + // to ensure the native builds are packed for use in gh actions and related scripts - if (packageName === '@next/swc') { - // next-swc uses a gitignore to prevent the committing of native builds but it doesn't - // use files in package.json because it publishes to individual packages based on architecture. - // When we used yarn to pack these packages the gitignore was ignored so the native builds were packed - // however npm does respect gitignore when packing so we need to remove it in this specific case - // to ensure the native builds are packed for use in gh actions and related scripts - - const nativeGitignorePath = path.join( - pkgPath, - 'native/.gitignore' - ) - const renamedGitignorePath = path.join( - pkgPath, - 'disabled-native-gitignore' - ) + const nativeGitignorePath = path.join( + pkgPath, + 'native/.gitignore' + ) + const renamedGitignorePath = path.join( + pkgPath, + 'disabled-native-gitignore' + ) - await fs.promises.rename( - nativeGitignorePath, - renamedGitignorePath - ) - cleanup = async () => { - await fs.promises.rename( - renamedGitignorePath, - nativeGitignorePath - ) - } - } + await handlePackageSpan + .traceChild('rename-gitignore') + .traceAsyncFn(() => + fs.promises.rename( + nativeGitignorePath, + renamedGitignorePath + ) + ) + cleanup = async () => { + await fs.promises.rename( + renamedGitignorePath, + nativeGitignorePath + ) + } + } - const options = { - cwd: pkgPath, - env: { - ...process.env, - COREPACK_ENABLE_STRICT: '0', - }, - } - let execResult - try { - execResult = await execa('pnpm', ['pack'], options) - } catch { - execResult = await execa('pnpm', ['pack'], options) - } - const { stdout } = execResult + const options = { + cwd: pkgPath, + env: { + ...process.env, + COREPACK_ENABLE_STRICT: '0', + }, + } + let execResult + try { + execResult = await handlePackageSpan + .traceChild('pnpm-pack-try-1') + .traceAsyncFn(() => execa('pnpm', ['pack'], options)) + } catch { + execResult = await handlePackageSpan + .traceChild('pnpm-pack-try-2') + .traceAsyncFn(() => execa('pnpm', ['pack'], options)) + } + const { stdout } = execResult - const packedFileName = stdout.trim() + const packedFileName = stdout.trim() - await Promise.all([ - fs.promises.rename( - path.join(pkgPath, packedFileName), - packedPkgPath - ), - cleanup?.(), - ]) - } - ) - ) + await handlePackageSpan + .traceChild('rename-packed-tar-and-cleanup') + .traceAsyncFn(() => + Promise.all([ + fs.promises.rename( + path.join(pkgPath, packedFileName), + packedPkgPath + ), + cleanup?.(), + ]) + ) + }) + } + ) + ) + }) return pkgPaths }, diff --git a/.github/actions/next-stats-action/src/util/mock-trace.js b/.github/actions/next-stats-action/src/util/mock-trace.js new file mode 100644 index 0000000000000..9658fd87ebdcc --- /dev/null +++ b/.github/actions/next-stats-action/src/util/mock-trace.js @@ -0,0 +1,14 @@ +/** @returns {Span} */ +const createMockSpan = () => ({ + traceAsyncFn: (fn) => fn(createMockSpan()), + traceFn: (fn) => fn(createMockSpan()), + traceChild: () => createMockSpan(), +}) + +/** @typedef {{ + * traceAsyncFn: (fn: (span: Span) => Promise) => Promise, + * traceFn: (fn: (span: Span) => T) => T, + * traceChild: (id: string, data?: Record) => Span, + * }} Span */ + +module.exports = createMockSpan diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 3b025d292054a..c0d7ca2371752 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -85,19 +85,6 @@ jobs: stepName: 'lint' secrets: inherit - validate-docs-links: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - run: corepack enable - - name: 'Run link checker' - run: node ./.github/actions/validate-docs-links/dist/index.js - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - check-types-precompiled: name: types and precompiled needs: ['changes', 'build-native', 'build-next'] @@ -183,38 +170,6 @@ jobs: stepName: 'test-turbopack-integration-${{ matrix.group }}' secrets: inherit - test-turbopack-production: - name: test turbopack production - needs: ['changes', 'build-next', 'build-native'] - if: ${{ needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/5, 2/5, 3/5, 4/5, 5/5] - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 18.17.0 - afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/turbopack-build-tests-manifest.json" TURBOPACK=1 TURBOPACK_BUILD=1 NEXT_TEST_MODE=start node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type production - stepName: 'test-turbopack-production-${{ matrix.group }}' - secrets: inherit - - test-turbopack-production-integration: - name: test turbopack production integration - needs: ['changes', 'build-next', 'build-native'] - if: ${{ needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/5, 2/5, 3/5, 4/5, 5/5] - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 18.17.0 - afterBuild: RUST_BACKTRACE=0 NEXT_EXTERNAL_TESTS_FILTERS="$(pwd)/test/turbopack-build-tests-manifest.json" TURBOPACK=1 TURBOPACK_BUILD=1 node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type integration - stepName: 'test-turbopack-production-integration-${{ matrix.group }}' - secrets: inherit - test-next-swc-wasm: name: test next-swc wasm needs: ['changes', 'build-next'] @@ -353,107 +308,18 @@ jobs: stepName: 'test-firefox-safari' secrets: inherit - # TODO: remove these jobs once PPR is the default - # Manifest generated via: https://gist.github.com/wyattjoh/2ceaebd82a5bcff4819600fd60126431 - test-ppr-integration: - name: test ppr integration - needs: ['changes', 'build-native', 'build-next'] - if: ${{ needs.changes.outputs.docs-only == 'false' }} - - uses: ./.github/workflows/build_reusable.yml - with: - nodeVersion: 18.17.0 - afterBuild: __NEXT_EXPERIMENTAL_PPR=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" node run-tests.js --timings -c ${TEST_CONCURRENCY} --type integration - stepName: 'test-ppr-integration' - secrets: inherit - - test-ppr-dev: - name: test ppr dev - needs: ['changes', 'build-native', 'build-next'] - if: ${{ needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/4, 2/4, 3/4, 4/4] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: __NEXT_EXPERIMENTAL_PPR=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" NEXT_TEST_MODE=dev node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type development - stepName: 'test-ppr-dev-${{ matrix.group }}' - secrets: inherit - - test-ppr-prod: - name: test ppr prod - needs: ['changes', 'build-native', 'build-next'] - if: ${{ needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/4, 2/4, 3/4, 4/4] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: __NEXT_EXPERIMENTAL_PPR=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" NEXT_TEST_MODE=start node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type production - stepName: 'test-ppr-prod-${{ matrix.group }}' - secrets: inherit - - report-test-results-to-datadog: - needs: - [ - 'changes', - 'test-unit', - 'test-dev', - 'test-prod', - 'test-integration', - 'test-ppr-dev', - 'test-ppr-prod', - 'test-ppr-integration', - 'test-turbopack-dev', - 'test-turbopack-integration', - 'test-turbopack-production', - 'test-turbopack-production-integration', - ] - if: ${{ always() && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork }} - - runs-on: ubuntu-latest - name: report test results to datadog - steps: - - name: Download test report artifacts - id: download-test-reports - uses: actions/download-artifact@v4 - with: - pattern: test-reports-* - path: test - merge-multiple: true - - - name: Upload test report to datadog - run: | - if [ -d ./test/test-junit-report ]; then - # Add a `test.type` tag to distinguish between turbopack and next.js runs - DD_ENV=ci npx @datadog/datadog-ci@2.23.1 junit upload --tags test.type:nextjs --service nextjs ./test/test-junit-report - fi - - if [ -d ./test/turbopack-test-junit-report ]; then - # Add a `test.type` tag to distinguish between turbopack and next.js runs - DD_ENV=ci npx @datadog/datadog-ci@2.23.1 junit upload --tags test.type:turbopack --service nextjs ./test/turbopack-test-junit-report - fi - tests-pass: needs: [ 'build-native', 'build-next', 'lint', - 'validate-docs-links', 'check-types-precompiled', 'test-unit', 'test-dev', 'test-prod', 'test-integration', 'test-firefox-safari', - 'test-ppr-dev', - 'test-ppr-prod', - 'test-ppr-integration', 'test-cargo-unit', 'rust-check', 'test-next-swc-wasm', diff --git a/.github/workflows/setup-nextjs-build.yml b/.github/workflows/setup-nextjs-build.yml index ad4f85e1545a1..419c8c5b34713 100644 --- a/.github/workflows/setup-nextjs-build.yml +++ b/.github/workflows/setup-nextjs-build.yml @@ -76,28 +76,8 @@ jobs: corepack enable pnpm install --loglevel error - - name: Build next-swc with latest turbopack - id: build-next-swc-turbopack-patch - continue-on-error: true - run: | - export TURBOPACK_REMOTE="https://github.com/vercel/turbo" - # Apply patches to the cargo to the latest turbopack's sha. - # Basic recipe to apply patch to cargo via cli looks like this: - # cargo check --config 'patch."https://github.com/vercel/turbo".$PKG_NAME.git="https://github.com/vercel/turbo.git?rev=$SHA"' - # Careful to preserve quote to allow dot expression can access git url based property key. - export BINDING=$(printf 'patch.\\"%s\\".%s.git=\\"%s?rev=%s\\"' "$TURBOPACK_REMOTE" "turbopack-binding" "$TURBOPACK_REMOTE" "$GITHUB_SHA") - export TASKS=$(printf 'patch.\\"%s\\".%s.git=\\"%s?rev=%s\\"' "$TURBOPACK_REMOTE" "turbo-tasks" "$TURBOPACK_REMOTE" "$GITHUB_SHA") - export TASKS_FS=$(printf 'patch.\\"%s\\".%s.git=\\"%s?rev=%s\\"' "$TURBOPACK_REMOTE" "turbo-tasks-fs" "$TURBOPACK_REMOTE" "$GITHUB_SHA") - - echo "Trying to build next-swc with turbopack $GITHUB_SHA" - hyperfine --min-runs 1 --show-output 'pnpm run --filter=@next/swc build-native --features plugin --release --cargo-flags="--config $BINDING --config $TASKS --config $TASKS_FS"' - echo "built=pass" >> $GITHUB_OUTPUT - echo "Successfully built next-swc with turbopack $GITHUB_SHA" - - name: Build next-swc - if: steps.build-next-swc-turbopack-patch.outputs.built != 'pass' run: | - echo "Looks like we could not apply latest turbopack to next-swc. Trying to build next-swc with published turbopack. This might happen when there is a breaking changes in turbopack, and next.js is not yet updated." hyperfine --min-runs 1 --show-output 'pnpm run --filter=@next/swc build-native --features plugin --release' echo "Successfully built next-swc with published turbopack" diff --git a/docs/01-getting-started/01-installation.mdx b/docs/01-getting-started/01-installation.mdx index 7d20232680772..5fbf18b4b9404 100644 --- a/docs/01-getting-started/01-installation.mdx +++ b/docs/01-getting-started/01-installation.mdx @@ -15,7 +15,7 @@ System Requirements: ## Automatic Installation -We recommend starting a new Next.js app using [`create-next-app`](/docs/app/api-reference/create-next-app), which sets up everything automatically for you. To create a project, run: +We recommend starting a new Next.js app using [`create-next-app`](/docs/app/api-reference/cli/create-next-app), which sets up everything automatically for you. To create a project, run: ```bash filename="Terminal" npx create-next-app@latest @@ -34,7 +34,7 @@ Would you like to customize the default import alias (@/*)? No / Yes What import alias would you like configured? @/* ``` -After the prompts, `create-next-app` will create a folder with your project name and install the required dependencies. +After the prompts, [`create-next-app`](/docs/app/api-reference/cli/create-next-app) will create a folder with your project name and install the required dependencies. If you're new to Next.js, see the [project structure](/docs/getting-started/project-structure) docs for an overview of all the possible files and folders in your application. @@ -66,10 +66,10 @@ Open your `package.json` file and add the following `scripts`: These scripts refer to the different stages of developing an application: -- `dev`: runs [`next dev`](/docs/app/api-reference/next-cli#development) to start Next.js in development mode. -- `build`: runs [`next build`](/docs/app/api-reference/next-cli#build) to build the application for production usage. -- `start`: runs [`next start`](/docs/app/api-reference/next-cli#production) to start a Next.js production server. -- `lint`: runs [`next lint`](/docs/app/api-reference/next-cli#lint) to set up Next.js' built-in ESLint configuration. +- `dev`: runs [`next dev`](/docs/app/api-reference/cli/next#next-dev-options) to start Next.js in development mode. +- `build`: runs [`next build`](/docs/app/api-reference/cli/next#next-build-options) to build the application for production usage. +- `start`: runs [`next start`](/docs/app/api-reference/cli/next#next-start-options) to start a Next.js production server. +- `lint`: runs [`next lint`](/docs/app/api-reference/cli/next#next-lint-options) to set up Next.js' built-in ESLint configuration. ### Creating directories diff --git a/docs/02-app/01-building-your-application/04-caching/index.mdx b/docs/02-app/01-building-your-application/04-caching/index.mdx index ac03be2436fda..5d6c3228c9b8d 100644 --- a/docs/02-app/01-building-your-application/04-caching/index.mdx +++ b/docs/02-app/01-building-your-application/04-caching/index.mdx @@ -363,13 +363,13 @@ This results in an improved navigation experience for the user: The cache is stored in the browser's temporary memory. Two factors determine how long the router cache lasts: - **Session**: The cache persists across navigation. However, it's cleared on page refresh. -- **Automatic Invalidation Period**: The cache of an individual segment is automatically invalidated after a specific time. The duration depends on how the resource was [prefetched](/docs/app/api-reference/components/link#prefetch): - - **Default Prefetching** (`prefetch={null}` or unspecified): 30 seconds - - **Full Prefetching**: (`prefetch={true}` or `router.prefetch`): 5 minutes +- **Automatic Invalidation Period**: The cache of layouts and loading states is automatically invalidated after a specific time. The duration depends on how the resource was [prefetched](/docs/app/api-reference/components/link#prefetch), and if the resource was [statically generated](/docs/app/building-your-application/rendering/server-components#static-rendering-default): + - **Default Prefetching** (`prefetch={null}` or unspecified): not cached for dynamic pages, 5 minutes for static pages. + - **Full Prefetching** (`prefetch={true}` or `router.prefetch`): 5 minutes for both static & dynamic pages. While a page refresh will clear **all** cached segments, the automatic invalidation period only affects the individual segment from the time it was prefetched. -> **Note**: There is [experimental support](/docs/app/api-reference/next-config-js/staleTimes) for configuring these values as of v14.2.0-canary.53. +> **Good to know**: The experimental [`staleTimes`](/docs/app/api-reference/next-config-js/staleTimes) config option can be used to adjust the automatic invalidation times mentioned above. ### Invalidation diff --git a/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx b/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx index 9212cc09cc13f..4955ea32a37a3 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/11-static-assets.mdx @@ -47,4 +47,4 @@ For static metadata files, such as `robots.txt`, `favicon.ico`, etc, you should > Good to know: > > - The directory must be named `public`. The name cannot be changed and it's the only directory used to serve static assets. -> - Only assets that are in the `public` directory at [build time](/docs/app/api-reference/next-cli#build) will be served by Next.js. Files added at request time won't be available. We recommend using a third-party service like [Vercel Blob](https://vercel.com/docs/storage/vercel-blob?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) for persistent file storage. +> - Only assets that are in the `public` directory at [build time](/docs/app/api-reference/cli/next#next-build-options) will be served by Next.js. Files added at request time won't be available. We recommend using a third-party service like [Vercel Blob](https://vercel.com/docs/storage/vercel-blob?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) for persistent file storage. diff --git a/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx b/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx index 4926de6334050..5b0b63d34b380 100644 --- a/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx +++ b/docs/02-app/01-building-your-application/07-configuring/02-eslint.mdx @@ -200,7 +200,20 @@ The `next/core-web-vitals` rule set is enabled when `next lint` is run for the f `next/core-web-vitals` updates `eslint-plugin-next` to error on a number of rules that are warnings by default if they affect [Core Web Vitals](https://web.dev/vitals/). -> The `next/core-web-vitals` entry point is automatically included for new applications built with [Create Next App](/docs/app/api-reference/create-next-app). +> The `next/core-web-vitals` entry point is automatically included for new applications built with [Create Next App](/docs/app/api-reference/cli/create-next-app). + +### TypeScript + +In addition to the Next.js ESLint rules, `create-next-app --typescript` will also add TypeScript-specific lint rules with `next/typescript` to your config: + +```json filename=".eslintrc.json" +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} +``` + +Those rules are based on [`plugin:@typescript-eslint/recommended`](https://typescript-eslint.io/linting/configs#recommended). +See [typescript-eslint > Configs](https://typescript-eslint.io/linting/configs) for more details. ## Usage With Other Tools diff --git a/docs/02-app/02-api-reference/04-functions/generate-metadata.mdx b/docs/02-app/02-api-reference/04-functions/generate-metadata.mdx index f4054aac6dc92..6b8f9a0c5343e 100644 --- a/docs/02-app/02-api-reference/04-functions/generate-metadata.mdx +++ b/docs/02-app/02-api-reference/04-functions/generate-metadata.mdx @@ -490,6 +490,18 @@ export const metadata = { alt: 'My custom alt', }, ], + videos: [ + { + url: 'https://nextjs.org/video.mp4', // Must be an absolute URL + width: 800, + height: 600, + }, + ], + audio: [ + { + url: 'https://nextjs.org/audio.mp3', // Must be an absolute URL + }, + ], locale: 'en_US', type: 'website', }, @@ -509,6 +521,10 @@ export const metadata = { + + + + ``` @@ -921,6 +937,51 @@ export const metadata = { ``` +### `facebook` + +You can connect a Facebook app or Facebook account to you webpage for certain Facebook Social Plugins [Facebook Documentation](https://developers.facebook.com/docs/plugins/comments/#moderation-setup-instructions) + +> **Good to know**: You can specify either appId or admins, but not both. + +```jsx filename="layout.js | page.js" +export const metadata = { + facebook: { + appId: '12345678', + }, +} +``` + +```html filename=" output" hideLineNumbers + +``` + +```jsx filename="layout.js | page.js" +export const metadata = { + facebook: { + admins: '12345678', + }, +} +``` + +```html filename=" output" hideLineNumbers + +``` + +If you want to generate multiple fb:admins meta tags you can use array value. + +```jsx filename="layout.js | page.js" +export const metadata = { + facebook: { + admins: ['12345678', '87654321'], + }, +} +``` + +```html filename=" output" hideLineNumbers + + +``` + ### `other` All metadata options should be covered using the built-in support. However, there may be custom metadata tags specific to your site, or brand new metadata tags just released. You can use the `other` option to render any custom metadata tag. diff --git a/docs/02-app/02-api-reference/05-next-config-js/assetPrefix.mdx b/docs/02-app/02-api-reference/05-next-config-js/assetPrefix.mdx index 1f9d6ef40d094..40a1db9d946e9 100644 --- a/docs/02-app/02-api-reference/05-next-config-js/assetPrefix.mdx +++ b/docs/02-app/02-api-reference/05-next-config-js/assetPrefix.mdx @@ -23,16 +23,25 @@ description: Learn how to use the assetPrefix config option to configure your CD > suited for hosting your application on a sub-path like `/docs`. > We do not suggest you use a custom Asset Prefix for this use case. -To set up a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network), you can set up an asset prefix and configure your CDN's origin to resolve to the domain that Next.js is hosted on. - -Open `next.config.js` and add the `assetPrefix` config: +## Set up a CDN -```js filename="next.config.js" -const isProd = process.env.NODE_ENV === 'production' +To set up a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network), you can set up an asset prefix and configure your CDN's origin to resolve to the domain that Next.js is hosted on. -module.exports = { - // Use the CDN in production and localhost for development. - assetPrefix: isProd ? 'https://cdn.mydomain.com' : undefined, +Open `next.config.mjs` and add the `assetPrefix` config based on the [phase](/docs/app/api-reference/next-config-js#async-configuration): + +```js filename="next.config.mjs" +// @ts-check +import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' + +export default (phase) => { + const isDev = phase === PHASE_DEVELOPMENT_SERVER + /** + * @type {import('next').NextConfig} + */ + const nextConfig = { + assetPrefix: isDev ? undefined : 'https://cdn.mydomain.com', + } + return nextConfig } ``` diff --git a/docs/02-app/02-api-reference/05-next-config-js/exportPathMap.mdx b/docs/02-app/02-api-reference/05-next-config-js/exportPathMap.mdx index be454aaf42141..87fa7c3870f87 100644 --- a/docs/02-app/02-api-reference/05-next-config-js/exportPathMap.mdx +++ b/docs/02-app/02-api-reference/05-next-config-js/exportPathMap.mdx @@ -15,7 +15,7 @@ description: Customize the pages that will be exported as HTML files when using -`exportPathMap` allows you to specify a mapping of request paths to page destinations, to be used during export. Paths defined in `exportPathMap` will also be available when using [`next dev`](/docs/app/api-reference/next-cli#development). +`exportPathMap` allows you to specify a mapping of request paths to page destinations, to be used during export. Paths defined in `exportPathMap` will also be available when using [`next dev`](/docs/app/api-reference/cli/next#next-dev-options). Let's start with an example, to create a custom `exportPathMap` for an app with the following pages: diff --git a/docs/02-app/02-api-reference/05-next-config-js/staleTimes.mdx b/docs/02-app/02-api-reference/05-next-config-js/staleTimes.mdx index d3d61b5df4729..0efe7cb11b61f 100644 --- a/docs/02-app/02-api-reference/05-next-config-js/staleTimes.mdx +++ b/docs/02-app/02-api-reference/05-next-config-js/staleTimes.mdx @@ -27,16 +27,21 @@ module.exports = nextConfig The `static` and `dynamic` properties correspond with the time period (in seconds) based on different types of [link prefetching](/docs/app/api-reference/components/link#prefetch). -- The `dynamic` property is used when the `prefetch` prop on `Link` is left unspecified. - - Default: 30 seconds -- The `static` property is used when the `prefetch` prop on `Link` is set to `true`, or when calling [`router.prefetch`](/docs/app/building-your-application/caching#routerprefetch). +- The `dynamic` property is used when the page is neither statically generated nor fully prefetched (i.e., with prefetch={true}). + - Default: 0 seconds (not cached) +- The `static` property is used for statically generated pages, or when the `prefetch` prop on `Link` is set to `true`, or when calling [`router.prefetch`](/docs/app/building-your-application/caching#routerprefetch). - Default: 5 minutes > **Good to know:** > > - [Loading boundaries](/docs/app/api-reference/file-conventions/loading) are considered reusable for the `static` period defined in this configuration. -> - This doesn't disable [partial rendering support](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), **meaning shared layouts won't automatically be refetched every navigation, only the new segment data.** -> - This doesn't change [back/forward caching](/docs/app/building-your-application/caching#router-cache) behavior to prevent layout shift & to prevent losing the browser scroll position. -> - The different properties of this config refer to variable levels of "liveness" and are unrelated to whether the segment itself is opting into static or dynamic rendering. In other words, the current `static` default of 5 minutes suggests that data feels static by virtue of it being revalidated infrequently. +> - This doesn't affect [partial rendering](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), **meaning shared layouts won't automatically be refetched on every navigation, only the page segment that changes.** +> - This doesn't change [back/forward caching](/docs/app/building-your-application/caching#client-side-router-cache) behavior to prevent layout shift and to prevent losing the browser scroll position. -You can learn more about the Client Router Cache [here](/docs/app/building-your-application/caching#router-cache). +You can learn more about the Client Router Cache [here](/docs/app/building-your-application/caching#client-side-router-cache). + +### Version History + +| Version | Changes | +| --------- | ------------------------------------ | +| `v14.2.0` | experimental `staleTimes` introduced | diff --git a/docs/02-app/02-api-reference/06-cli/create-next-app.mdx b/docs/02-app/02-api-reference/06-cli/create-next-app.mdx new file mode 100644 index 0000000000000..8bd3260587874 --- /dev/null +++ b/docs/02-app/02-api-reference/06-cli/create-next-app.mdx @@ -0,0 +1,85 @@ +--- +title: create-next-app +description: Create Next.js apps using one command with the create-next-app CLI. +--- + +{/* 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 `create-next-app` CLI allow you to quickly create a new Next.js application using the default template or an [example](https://github.com/vercel/next.js/tree/canary/examples) from a public Github repository. It is the easiest way to get started with Next.js. + +Basic usage: + +```bash filename="Terminal" +npx create-next-app@latest [project-name] [options] +``` + +## Reference + +The following options are available: + +| Options | Description | +| --------------------------------------- | --------------------------------------------------------------- | +| `-h` or `--help` | Show all available options | +| `-v` or `--version` | Output the version number | +| `--no-*` | Negate default options. E.g. `--no-eslint` | +| `--ts` or `--typescript` | Initialize as a TypeScript project (default) | +| `--js` or `--javascript` | Initialize as a JavaScript project | +| `--tailwind` | Initialize with Tailwind CSS config (default) | +| `--eslint` | Initialize with ESLint config | +| `--app` | Initialize as an App Router project | +| `--src-dir` | Initialize inside a `src/` directory | +| `--turbo` | Enable Turbopack by default for development | +| `--import-alias ` | Specify import alias to use (default "@/\*") | +| `--empty` | Initialize an empty project | +| `--use-npm` | Explicitly tell the CLI to bootstrap the application using npm | +| `--use-pnpm` | Explicitly tell the CLI to bootstrap the application using pnpm | +| `--use-yarn` | Explicitly tell the CLI to bootstrap the application using Yarn | +| `--use-bun` | Explicitly tell the CLI to bootstrap the application using Bun | +| `-e` or `--example [name] [github-url]` | An example to bootstrap the app with | +| `--example-path ` | Specify the path to the example separately | +| `--reset-preferences` | Explicitly tell the CLI to reset any stored preferences | +| `--skip-install` | Explicitly tell the CLI to skip installing packages | +| `--yes` | Use previous preferences or defaults for all options | + +## Examples + +## With the default template + +To create a new app using the default template, run the following command in your terminal: + +```bash filename="Terminal" +npx create-next-app@latest +``` + +You will then be asked the following prompts: + +```txt filename="Terminal" +What is your project named? my-app +Would you like to use TypeScript? No / Yes +Would you like to use ESLint? No / Yes +Would you like to use Tailwind CSS? No / Yes +Would you like your code inside a `src/` directory? No / Yes +Would you like to use App Router? (recommended) No / Yes +Would you like to use Turbopack for `next dev`? No / Yes +Would you like to customize the import alias (`@/*` by default)? No / Yes +``` + +Once you've answered the prompts, a new project will be created with your chosen configuration. + +## With an official Next.js example + +To create a new app using an official Next.js example, use the `--example` flag with the following command: + +```bash filename="Terminal" +npx create-next-app@latest --example [your-project-name] [example-name] +``` + +You can view a list of all available examples along with setup instructions in the [Next.js repository](https://github.com/vercel/next.js/tree/canary/examples). + +## With any public Github example + +To create a new app using any public Github example, use the `--example` option with the Github repo's URL. For example: + +```bash filename="Terminal" +npx create-next-app@latest --example [your-project-name] "https://github.com/.../" +``` diff --git a/docs/02-app/02-api-reference/06-cli/index.mdx b/docs/02-app/02-api-reference/06-cli/index.mdx new file mode 100644 index 0000000000000..f66eb21ea66c4 --- /dev/null +++ b/docs/02-app/02-api-reference/06-cli/index.mdx @@ -0,0 +1,11 @@ +--- +title: CLI +description: API Reference for the Next.js Command Line Interface (CLI) tools. +--- + +{/* 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. */} + +Next.js comes with **two** Command Line Interface (CLI) tools: + +- **`create-next-app`**: Quickly create a new Next.js application using the default template or an [example](https://github.com/vercel/next.js/tree/canary/examples) from a public Github repository. +- **`next`**: Run the Next.js development server, build your application, and more. diff --git a/docs/02-app/02-api-reference/06-cli/next.mdx b/docs/02-app/02-api-reference/06-cli/next.mdx new file mode 100644 index 0000000000000..196668d83a78d --- /dev/null +++ b/docs/02-app/02-api-reference/06-cli/next.mdx @@ -0,0 +1,238 @@ +--- +title: next CLI +description: Learn how to run and build your application with the Next.js CLI. +--- + +{/* 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 CLI allows you to develop, build, start your application, and more. + +Basic usage: + +```bash filename="Terminal" +npm run next [command] [options] +``` + +## Reference + +The following options are available: + +| Options | Description | +| ------------------- | ---------------------------------- | +| `-h` or `--help` | Shows all available options | +| `-v` or `--version` | Outputs the Next.js version number | + +### Commands + +The following commands are available: + +| Command | Description | +| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`dev`](#next-dev-options) | Starts Next.js in development mode with Hot Module Reloading, error reporting, and more. | +| [`build`](#next-build-options) | Creates an optimized production build of your application. Displaying information about each route. | +| [`start`](#next-start-options) | Starts Next.js in production mode. The application should be compiled with `next build` first. | +| [`info`](next-info-options) | Prints relevant details about the current system which can be used to report Next.js bugs. | +| [`lint`](next-lint-options) | Runs ESLint for all files in the `/src`, `/app`, `/pages`, `/components`, and `/lib` directories. It also provides a guided setup to install any required dependencies if ESLint it is not already configured in your application. | +| [`telemetry`](next-telemetry-options) | Allows you to enable or disable Next.js' completely anonymous telemetry collection. | + +> **Good to know**: Running `next` without a command is an alias for `next dev`. + +### `next dev` options + +`next dev` starts the application in development mode with Hot Module Reloading (HMR), error reporting, and more. The following options are available when running `next dev`: + +| Option | Description | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-h, --help` | Show all available options. | +| `[directory]` | A directory in which to build the application. If not provided, current directory is used. | +| `--turbo` | Starts development mode using [Turbopack](https://nextjs.org/docs/architecture/turbopack). | +| `-p` or `--port ` | Specify a port number on which to start the application. Default: 3000, env: PORT | +| `-H`or `--hostname ` | Specify a hostname on which to start the application. Useful for making the application available for other devices on the network. Default: 0.0.0.0 | +| `--experimental-https` | Starts the server with HTTPS and generates a self-signed certificate. | +| `--experimental-https-key ` | Path to a HTTPS key file. | +| `--experimental-https-cert ` | Path to a HTTPS certificate file. | +| `--experimental-https-ca ` | Path to a HTTPS certificate authority file. | +| `--experimental-upload-trace ` | Reports a subset of the debugging trace to a remote HTTP URL. | + +### `next build` options + +`next build` creates an optimized production build of your application. The output displays information about each route. For example: + +```bash filename="Terminal" +Route (app) Size First Load JS +┌ ○ /_not-found 0 B 0 kB +└ ƒ /products/[id] 0 B 0 kB + +○ (Static) prerendered as static content +ƒ (Dynamic) server-rendered on demand +``` + +- **Size**: The size of assets downloaded when navigating to the page client-side. The size for each route only includes its dependencies. +- **First Load JS**: The size of assets downloaded when visiting the page from the server. The amount of JS shared by all is shown as a separate metric. + +Both of these values are [**compressed with gzip**](/docs/app/api-reference/next-config-js/compress). The first load is indicated by green, yellow, or red. Aim for green for performant applications. + +The following options are available for the `next build` command: + +| Option | Description | +| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `-h, --help` | Show all available options. | +| `[directory]` | A directory on which to build the application. If not provided, the current directory will be used. | +| `-d` or `--debug` | Enables a more verbose build output. With this flag enabled additional build output like rewrites, redirects, and headers will be shown. | +| | +| `--profile` | Enables production [profiling for React](https://react.dev/reference/react/Profiler). | +| `--no-lint` | Disables linting. | +| `--no-mangling` | Disables [mangling](https://en.wikipedia.org/wiki/Name_mangling). This may affect performance and should only be used for debugging purposes. | +| `--experimental-app-only` | Builds only App Router routes. | +| `--experimental-build-mode [mode]` | Uses an experimental build mode. (choices: "compile", "generate", default: "default") | + +### `next start` options + +`next start` starts the application in production mode. The application should be compiled with [`next build`](#next-build-options) first. + +The following options are available for the `next start` command: + +| Option | Description | +| --------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `-h` or `--help` | Show all available options. | +| `[directory]` | A directory on which to start the application. If no directory is provided, the current directory will be used. | +| `-p` or `--port ` | Specify a port number on which to start the application. (default: 3000, env: PORT) | +| `-H` or `--hostname ` | Specify a hostname on which to start the application (default: 0.0.0.0). | +| `--keepAliveTimeout ` | Specify the maximum amount of milliseconds to wait before closing the inactive connections. | + +### `next info` options + +`next info` prints relevant details about the current system which can be used to report Next.js bugs when opening a [GitHub issue](https://github.com/vercel/next.js/issues). This information includes Operating System platform/arch/version, Binaries (Node.js, npm, Yarn, pnpm), package versions (`next`, `react`, `react-dom`), and more. + +The output should look like this: + +```bash filename="Terminal" +Operating System: + Platform: darwin + Arch: arm64 + Version: Darwin Kernel Version 23.6.0 + Available memory (MB): 65536 + Available CPU cores: 10 +Binaries: + Node: 20.12.0 + npm: 10.5.0 + Yarn: 1.22.19 + pnpm: 9.6.0 +Relevant Packages: + next: 15.0.0-canary.115 // Latest available version is detected (15.0.0-canary.115). + eslint-config-next: 14.2.5 + react: 19.0.0-rc + react-dom: 19.0.0 + typescript: 5.5.4 +Next.js Config: + output: N/A +``` + +The following options are available for the `next info` command: + +| Option | Description | +| ---------------- | ---------------------------------------------- | +| `-h` or `--help` | Show all available options | +| `--verbose` | Collects additional information for debugging. | + +### `next lint` options + +`next lint` runs ESLint for all files in the `pages/`, `app/`, `components/`, `lib/`, and `src/` directories. It also provides a guided setup to install any required dependencies if ESLint is not already configured in your application. + +The following options are available for the `next lint` command: + +| Option | Description | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `[directory]` | A base directory on which to lint the application. If not provided, the current directory will be used. | +| `-d, --dir, ` | Include directory, or directories, to run ESLint. | +| `--file, ` | Include file, or files, to run ESLint. | +| `--ext, [exts...]` | Specify JavaScript file extensions. (default: [".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"]) | +| `-c, --config, ` | Uses this configuration file, overriding all other configuration options. | +| `--resolve-plugins-relative-to, ` | Specify a directory where plugins should be resolved from. | +| `--strict` | Creates a `.eslintrc.json` file using the Next.js strict configuration. | +| `--rulesdir, ` | Uses additional rules from this directory(s). | +| `--fix` | Automatically fix linting issues. | +| `--fix-type ` | Specify the types of fixes to apply (e.g., problem, suggestion, layout). | +| `--ignore-path ` | Specify a file to ignore. | +| `--no-ignore ` | Disables the `--ignore-path` option. | +| `--quiet` | Reports errors only. | +| `--max-warnings [maxWarnings]` | Specify the number of warnings before triggering a non-zero exit code. (default: -1) | +| `-o, --output-file, ` | Specify a file to write report to. | +| `-f, --format, ` | Uses a specific output format. | +| `--no-inline-config` | Prevents comments from changing config or rules. | +| `--report-unused-disable-directives-severity ` | Specify severity level for unused eslint-disable directives. (choices: "error", "off", "warn") | +| `--no-cache` | Disables caching. | +| `--cache-location, ` | Specify a location for cache. | +| `--cache-strategy, [cacheStrategy]` | Specify a strategy to use for detecting changed files in the cache. (default: "metadata") | +| `--error-on-unmatched-pattern` | Reports errors when any file patterns are unmatched. | +| `-h, --help` | Displays this message. | + +### `next telemetry` options + +Next.js collects **completely anonymous** telemetry data about general usage. Participation in this anonymous program is optional, and you can opt-out if you prefer not to share information. + +The following options are available for the `next telemetry` command: + +| Option | Description | +| ------------ | --------------------------------------- | +| `-h, --help` | Show all available options. | +| `--enable` | Enables Next.js' telemetry collection. | +| `--disable` | Disables Next.js' telemetry collection. | + +Learn more about [Telemetry](/telemetry). + +## Examples + +### Changing the default port + +By default, Next.js uses `http://localhost:3000` during development and with `next start`. The default port can be changed with the `-p` option, like so: + +```bash filename="Terminal" +next dev -p 4000 +``` + +Or using the `PORT` environment variable: + +```bash filename="Terminal" +PORT=4000 next dev +``` + +> **Good to know**: `PORT` cannot be set in `.env` as booting up the HTTP server happens before any other code is initialized. + +### Using HTTPS during development + +For certain use cases like webhooks or authentication, it may be required to use HTTPS to have a secure environment on `localhost`. Next.js can generate a self-signed certificate with `next dev` using the `--experimental-https` flag: + +```bash filename="Terminal" +next dev --experimental-https +``` + +You can also provide a custom certificate and key with `--experimental-https-key` and `--experimental-https-cert`. Optionally, you can provide a custom CA certificate with `--experimental-https-ca` as well. + +```bash filename="Terminal" +next dev --experimental-https --experimental-https-key ./certificates/localhost-key.pem --experimental-https-cert ./certificates/localhost.pem +``` + +`next dev --experimental-https` is only intended for development and creates a locally trusted certificate with [`mkcert`](https://github.com/FiloSottile/mkcert). In production, use properly issued certificates from trusted authorities. + +> **Good to know**: When deploying to Vercel, HTTPS is [automatically configured](https://vercel.com/docs/security/encryption) for your Next.js application. + +### Configuring a timeout for downstream proxies + +When deploying Next.js behind a downstream proxy (e.g. a load-balancer like AWS ELB/ALB), it's important to configure Next's underlying HTTP server with [keep-alive timeouts](https://nodejs.org/api/http.html#http_server_keepalivetimeout) that are _larger_ than the downstream proxy's timeouts. Otherwise, once a keep-alive timeout is reached for a given TCP connection, Node.js will immediately terminate that connection without notifying the downstream proxy. This results in a proxy error whenever it attempts to reuse a connection that Node.js has already terminated. + +To configure the timeout values for the production Next.js server, pass `--keepAliveTimeout` (in milliseconds) to `next start`, like so: + +```bash filename="Terminal" +next start --keepAliveTimeout 70000 +``` + +### Passing Node.js arguments + +You can pass any [node arguments](https://nodejs.org/api/cli.html#cli_node_options_options) to `next` commands. For example: + +```bash filename="Terminal" +NODE_OPTIONS='--throw-deprecation' next +NODE_OPTIONS='-r esm' next +NODE_OPTIONS='--inspect' next +``` diff --git a/docs/02-app/02-api-reference/06-create-next-app.mdx b/docs/02-app/02-api-reference/06-create-next-app.mdx deleted file mode 100644 index 3911d56724f81..0000000000000 --- a/docs/02-app/02-api-reference/06-create-next-app.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: create-next-app -description: Create Next.js apps in one command with create-next-app. ---- - -{/* 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 easiest way to get started with Next.js is by using `create-next-app`. This CLI tool enables you to quickly start building a new Next.js application, with everything set up for you. - -You can create a new app using the default Next.js template, or by using one of the [official Next.js examples](https://github.com/vercel/next.js/tree/canary/examples). - -### Interactive - -You can create a new project interactively by running: - -```bash filename="Terminal" -npx create-next-app@latest -``` - -```bash filename="Terminal" -yarn create next-app -``` - -```bash filename="Terminal" -pnpm create next-app -``` - -```bash filename="Terminal" -bunx create-next-app -``` - -You will then be asked the following prompts: - -```txt filename="Terminal" -What is your project named? my-app -Would you like to use TypeScript? No / Yes -Would you like to use ESLint? No / Yes -Would you like to use Tailwind CSS? No / Yes -Would you like to use `src/` directory? No / Yes -Would you like to use App Router? (recommended) No / Yes -Would you like to customize the default import alias (@/*)? No / Yes -``` - -Once you've answered the prompts, a new project will be created with the correct configuration depending on your answers. - -### Non-interactive - -You can also pass command line arguments to set up a new project non-interactively. - -Further, you can negate default options by prefixing them with `--no-` (e.g. `--no-eslint`). - -See `create-next-app --help`: - -```bash filename="Terminal" -Usage: create-next-app [options] - -Options: - -V, --version output the version number - --ts, --typescript - - Initialize as a TypeScript project. (default) - - --js, --javascript - - Initialize as a JavaScript project. - - --tailwind - - Initialize with Tailwind CSS config. (default) - - --eslint - - Initialize with ESLint config. - - --app - - Initialize as an App Router project. - - --src-dir - - Initialize inside a `src/` directory. - - --import-alias - - Specify import alias to use (default "@/*"). - - --use-npm - - Explicitly tell the CLI to bootstrap the app using npm - - --use-pnpm - - Explicitly tell the CLI to bootstrap the app using pnpm - - --use-yarn - - Explicitly tell the CLI to bootstrap the app using Yarn - - --use-bun - - Explicitly tell the CLI to bootstrap the app using Bun - - -e, --example [name]|[github-url] - - An example to bootstrap the app with. You can use an example name - from the official Next.js repo or a public GitHub URL. The URL can use - any branch and/or subdirectory - - --example-path - - In a rare case, your GitHub URL might contain a branch name with - a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). - In this case, you must specify the path to the example separately: - --example-path foo/bar - - --reset-preferences - - Explicitly tell the CLI to reset any stored preferences - - -h, --help output usage information -``` - -### Why use Create Next App? - -`create-next-app` allows you to create a new Next.js app within seconds. It is officially maintained by the creators of Next.js, and includes a number of benefits: - -- **Interactive Experience**: Running `npx create-next-app@latest` (with no arguments) launches an interactive experience that guides you through setting up a project. -- **Zero Dependencies**: Initializing a project is as quick as one second. Create Next App has zero dependencies. -- **Offline Support**: Create Next App will automatically detect if you're offline and bootstrap your project using your local package cache. -- **Support for Examples**: Create Next App can bootstrap your application using an example from the Next.js examples collection (e.g. `npx create-next-app --example api-routes`) or any public GitHub repository. -- **Tested**: The package is part of the Next.js monorepo and tested using the same integration test suite as Next.js itself, ensuring it works as expected with every release. diff --git a/docs/02-app/02-api-reference/08-next-cli.mdx b/docs/02-app/02-api-reference/08-next-cli.mdx deleted file mode 100644 index e33a96e7648c4..0000000000000 --- a/docs/02-app/02-api-reference/08-next-cli.mdx +++ /dev/null @@ -1,436 +0,0 @@ ---- -title: Next.js CLI -description: Learn how the Next.js CLI allows you to develop, build, and start your application, and more. ---- - -{/* 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 CLI allows you to develop, build, start your application, and more. - -To get a list of the available CLI commands, run the following command inside your project directory: - -```bash filename="Terminal" -next -h -``` - -The output should look like this: - -```bash filename="Terminal" -Usage next [options] [command] - -The Next.js CLI allows you to develop, build, start your application, and more. - -Options: - -v, --version Outputs the Next.js version. - -h, --help Displays this message. - -Commands: - build [directory] [options] Creates an optimized production build of your application. - The output displays information about each route. - dev [directory] [options] Starts Next.js in development mode with hot-code reloading, - error reporting, and more. - info [options] Prints relevant details about the current system which can be - used to report Next.js bugs. - lint [directory] [options] Runs ESLint for all files in the `/src`, `/app`, `/pages`, - `/components`, and `/lib` directories. It also provides a - guided setup to install any required dependencies if ESLint - is not already configured in your application. - start [directory] [options] Starts Next.js in production mode. The application should be - compiled with `next build` first. - telemetry [options] Allows you to enable or disable Next.js' completely - anonymous telemetry collection. -``` - -You can pass any [node arguments](https://nodejs.org/api/cli.html#cli_node_options_options) to `next` commands: - -```bash filename="Terminal" -NODE_OPTIONS='--throw-deprecation' next -NODE_OPTIONS='-r esm' next -NODE_OPTIONS='--inspect' next -``` - -> **Good to know**: Running `next` without a command is the same as running `next dev` - -## Development - -`next dev` starts the application in development mode with hot-code reloading, error reporting, and more. - -To get a list of the available options with `next dev`, run the following command inside your project directory: - -```bash filename="Terminal" -next dev -h -``` - -The output should look like this: - -```bash filename="Terminal" -Usage: next dev [directory] [options] - -Starts Next.js in development mode with hot-code reloading, error reporting, and more. - -Arguments: - [directory] A directory on which to build the application. - If no directory is provided, the current - directory will be used. - -Options: - --turbo Starts development mode using Turbopack (beta). - -p, --port Specify a port number on which to start the - application. (default: 3000, env: PORT) - -H, --hostname Specify a hostname on which to start the - application (default: 0.0.0.0). - --experimental-https Starts the server with HTTPS and generates a - self-signed certificate. - --experimental-https-key, Path to a HTTPS key file. - --experimental-https-cert, Path to a HTTPS certificate file. - --experimental-https-ca, Path to a HTTPS certificate authority file. - --experimental-upload-trace, Reports a subset of the debugging trace to a - remote HTTP URL. Includes sensitive data. - -h, --help Displays this message. -``` - -The application will start at `http://localhost:3000` by default. The default port can be changed with `-p`, like so: - -```bash filename="Terminal" -next dev -p 4000 -``` - -Or using the `PORT` environment variable: - -```bash filename="Terminal" -PORT=4000 next dev -``` - -> **Good to know**: -> -> - `PORT` cannot be set in `.env` as booting up the HTTP server happens before any other code is initialized. -> - Next.js will automatically retry with another port until a port is available if a port is not specified with the CLI option `--port` or the `PORT` environment variable. - -You can also set the hostname to be different from the default of `0.0.0.0`, this can be useful for making the application available for other devices on the network. The default hostname can be changed with `-H`, like so: - -```bash filename="Terminal" -next dev -H 192.168.1.2 -``` - -### Turbopack - -[Turbopack](/docs/architecture/turbopack) (beta), our new bundler, which is being tested and stabilized in Next.js, helps speed up local iterations while working on your application. - -To use Turbopack in development mode, add the `--turbo` option: - -```bash filename="Terminal" -next dev --turbo -``` - -### HTTPS for Local Development - -For certain use cases like webhooks or authentication, it may be required to use HTTPS to have a secure environment on `localhost`. Next.js can generate a self-signed certificate with `next dev` as follows: - -```bash filename="Terminal" -next dev --experimental-https -``` - -You can also provide a custom certificate and key with `--experimental-https-key` and `--experimental-https-cert`. Optionally, you can provide a custom CA certificate with `--experimental-https-ca` as well. - -```bash filename="Terminal" -next dev --experimental-https --experimental-https-key ./certificates/localhost-key.pem --experimental-https-cert ./certificates/localhost.pem -``` - -`next dev --experimental-https` is only intended for development and creates a locally-trusted certificate with `mkcert`. In production, use properly issued certificates from trusted authorities. When deploying to Vercel, HTTPS is [automatically configured](https://vercel.com/docs/security/encryption) for your Next.js application. - -## Build - -`next build` creates an optimized production build of your application. The output displays information about each route: - -```bash filename="Terminal" -Route (app) Size First Load JS -┌ ○ / 5.3 kB 89.5 kB -├ ○ /_not-found 885 B 85.1 kB -└ ○ /about 137 B 84.4 kB -+ First Load JS shared by all 84.2 kB - ├ chunks/184-d3bb186aac44da98.js 28.9 kB - ├ chunks/30b509c0-f3503c24f98f3936.js 53.4 kB - └ other shared chunks (total) - - -○ (Static) prerendered as static content -``` - -- **Size**: The number of assets downloaded when navigating to the page client-side. The size for each route only includes its dependencies. -- **First Load JS**: The number of assets downloaded when visiting the page from the server. The amount of JS shared by all is shown as a separate metric. - -Both of these values are [**compressed with gzip**](/docs/app/api-reference/next-config-js/compress). The first load is indicated by green, yellow, or red. Aim for green for performant applications. - -To get a list of the available options with `next build`, run the following command inside your project directory: - -```bash filename="Terminal" -next build -h -``` - -The output should look like this: - -```bash filename="Terminal" -Usage: next build [directory] [options] - -Creates an optimized production build of your application. The output displays information -about each route. - -Arguments: - [directory] A directory on which to build the application. If no - provided, the current directory will be - used. - -Options: - -d, --debug Enables a more verbose build output. - --profile Enables production profiling for React. - --no-lint Disables linting. - --no-mangling Disables mangling. - --experimental-app-only Builds only App Router routes. - --experimental-build-mode [mode] Uses an experimental build mode. (choices: "compile" - "generate", default: "default") - -h, --help Displays this message. -``` - -### Debug - -You can enable more verbose build output with the `--debug` flag in `next build`. - -```bash filename="Terminal" -next build --debug -``` - -With this flag enabled additional build output like rewrites, redirects, and headers will be shown. - -### Linting - -You can disable linting for builds like so: - -```bash filename="Terminal" -next build --no-lint -``` - -### Mangling - -You can disable [mangling](https://en.wikipedia.org/wiki/Name_mangling) for builds like so: - -```bash filename="Terminal" -next build --no-mangling -``` - -> **Good to know**: This may affect performance and should only be used for debugging purposes. - -### Profiling - -You can enable production profiling for React with the `--profile` flag in `next build`. - -```bash filename="Terminal" -next build --profile -``` - -After that, you can use the profiler in the same way as you would in development. - -## Production - -`next start` starts the application in production mode. The application should be compiled with [`next build`](#build) first. - -To get a list of the available options with `next start`, run the follow command inside your project directory: - -```bash filename="Terminal" -next start -h -``` - -The output should look like this: - -```bash filename="Terminal" -Usage: next start [directory] [options] - -Starts Next.js in production mode. The application should be compiled with `next build` -first. - -Arguments: - [directory] A directory on which to start the application. - If not directory is provided, the current - directory will be used. - -Options: - -p, --port Specify a port number on which to start the - application. (default: 3000, env: PORT) - -H, --hostname Specify a hostname on which to start the - application (default: 0.0.0.0). - --keepAliveTimeout Specify the maximum amount of milliseconds to wait - before closing the inactive connections. - -h, --help Displays this message. -``` - -The application will start at `http://localhost:3000` by default. The default port can be changed with `-p`, like so: - -```bash filename="Terminal" -next start -p 4000 -``` - -Or using the `PORT` environment variable: - -```bash filename="Terminal" -PORT=4000 next start -``` - -> **Good to know**: -> -> - `PORT` cannot be set in `.env` as booting up the HTTP server happens before any other code is initialized. -> - `next start` cannot be used with `output: 'standalone'` or `output: 'export'`. - -### Keep Alive Timeout - -When deploying Next.js behind a downstream proxy (e.g. a load-balancer like AWS ELB/ALB) it's important to configure Next's underlying HTTP server with [keep-alive timeouts](https://nodejs.org/api/http.html#http_server_keepalivetimeout) that are _larger_ than the downstream proxy's timeouts. Otherwise, once a keep-alive timeout is reached for a given TCP connection, Node.js will immediately terminate that connection without notifying the downstream proxy. This results in a proxy error whenever it attempts to reuse a connection that Node.js has already terminated. - -To configure the timeout values for the production Next.js server, pass `--keepAliveTimeout` (in milliseconds) to `next start`, like so: - -```bash filename="Terminal" -next start --keepAliveTimeout 70000 -``` - -## Info - -`next info` prints relevant details about the current system which can be used to report Next.js bugs. -This information includes Operating System platform/arch/version, Binaries (Node.js, npm, Yarn, pnpm) and npm package versions (`next`, `react`, `react-dom`). - -To get a list of the available options with `next info`, run the following command inside your project directory: - -```bash filename="Terminal" -next info -h -``` - -The output should look like this: - -```bash filename="Terminal" -Usage: next info [options] - -Prints relevant details about the current system which can be used to report Next.js bugs. - -Options: - --verbose Collections additional information for debugging. - -h, --help Displays this message. -``` - -Running `next info` will give you information like this example: - -```bash filename="Terminal" - -Operating System: - Platform: linux - Arch: x64 - Version: #22-Ubuntu SMP Fri Nov 5 13:21:36 UTC 2021 - Available memory (MB): 31795 - Available CPU cores: 16 -Binaries: - Node: 16.13.0 - npm: 8.1.0 - Yarn: 1.22.17 - pnpm: 6.24.2 -Relevant Packages: - next: 14.1.1-canary.61 // Latest available version is detected (14.1.1-canary.61). - react: 18.2.0 - react-dom: 18.2.0 -Next.js Config: - output: N/A - -``` - -This information should then be pasted into GitHub Issues. - -You can also run `next info --verbose` which will print additional information about the system and the installation of packages related to `next`. - -## Lint - -`next lint` runs ESLint for all files in the `pages/`, `app/`, `components/`, `lib/`, and `src/` directories. It also -provides a guided setup to install any required dependencies if ESLint is not already configured in -your application. - -To get a list of the available options with `next lint`, run the following command inside your project directory: - -```bash filename="Terminal" -next lint -h -``` - -The output should look like this: - -```bash filename="Terminal" -Usage: next lint [directory] [options] - -Runs ESLint for all files in the `/src`, `/app`, `/pages`, `/components`, and `/lib` directories. It also -provides a guided setup to install any required dependencies if ESLint is not already configured in your -application. - -Arguments: - [directory] A base directory on which to lint the application. - If no directory is provided, the current directory - will be used. - -Options: - -d, --dir, Include directory, or directories, to run ESLint. - --file, Include file, or files, to run ESLint. - --ext, [exts...] Specify JavaScript file extensions. (default: - [".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"]) - -c, --config, Uses this configuration file, overriding all other - configuration options. - --resolve-plugins-relative-to, Specify a directory where plugins should be resolved - from. - --strict Creates a `.eslintrc.json` file using the Next.js - strict configuration. - --rulesdir, Uses additional rules from this directory(s). - --fix Automatically fix linting issues. - --fix-type Specify the types of fixes to apply (e.g., problem, - suggestion, layout). - --ignore-path Specify a file to ignore. - --no-ignore Disables the `--ignore-path` option. - --quiet Reports errors only. - --max-warnings [maxWarnings] Specify the number of warnings before triggering a - non-zero exit code. (default: -1) - -o, --output-file, Specify a file to write report to. - -f, --format, Uses a specifc output format. - --no-inline-config Prevents comments from changing config or rules. - --report-unused-disable-directives-severity Specify severity level for unused eslint-disable - directives. (choices: "error", "off", "warn") - --no-cache Disables caching. - --cache-location, Specify a location for cache. - --cache-strategy, [cacheStrategy] Specify a strategy to use for detecting changed files - in the cache. (default: "metadata") - --error-on-unmatched-pattern Reports errors when any file patterns are unmatched. - -h, --help Displays this message. -``` - -If you have other directories that you would like to lint, you can specify them using the `--dir` flag: - -```bash filename="Terminal" -next lint --dir utils -``` - -For more information on the other options, check out our [ESLint](/docs/app/building-your-application/configuring/eslint) configuration documentation. - -## Telemetry - -Next.js collects **completely anonymous** telemetry data about general usage. -Participation in this anonymous program is optional, and you may opt-out if you'd not like to share any information. - -To get a list of the available options with `next telemetry`, run the following command in your project directory: - -```bash filename="Terminal" -next telemetry -h -``` - -The output should look like this: - -```bash filename="Terminal" -Usage: next telemetry [options] - -Allows you to enable or disable Next.js' completely anonymous telemetry collection. - -Options: - --enable Eanbles Next.js' telemetry collection. - --disable Disables Next.js' telemetry collection. - -h, --help Displays this message. - -Learn more: https://nextjs.org/telemetry -``` - -Learn more about [Telemetry](/telemetry/). diff --git a/docs/03-pages/01-building-your-application/03-data-fetching/04-incremental-static-regeneration.mdx b/docs/03-pages/01-building-your-application/03-data-fetching/04-incremental-static-regeneration.mdx index bb2b6aedec0b2..40f5b5cfa2497 100644 --- a/docs/03-pages/01-building-your-application/03-data-fetching/04-incremental-static-regeneration.mdx +++ b/docs/03-pages/01-building-your-application/03-data-fetching/04-incremental-static-regeneration.mdx @@ -126,7 +126,7 @@ export default async function handler(req, res) { ### Testing on-Demand ISR during development -When running locally with `next dev`, `getStaticProps` is invoked on every request. To verify your on-demand ISR configuration is correct, you will need to create a [production build](/docs/pages/api-reference/next-cli#build) and start the [production server](/docs/pages/api-reference/next-cli#production): +When running locally with `next dev`, `getStaticProps` is invoked on every request. To verify your on-demand ISR configuration is correct, you will need to create a [production build](/docs/pages/api-reference/cli/next#next-build-options) and start the [production server](/docs/pages/api-reference/cli/next#next-start-options): ```bash filename="Terminal" $ next build diff --git a/docs/03-pages/01-building-your-application/06-configuring/13-debugging.mdx b/docs/03-pages/01-building-your-application/06-configuring/13-debugging.mdx index 3821c7dee3313..67833f7227330 100644 --- a/docs/03-pages/01-building-your-application/06-configuring/13-debugging.mdx +++ b/docs/03-pages/01-building-your-application/06-configuring/13-debugging.mdx @@ -44,7 +44,7 @@ Create a file named `.vscode/launch.json` at the root of your project with the f `npm run dev` can be replaced with `yarn dev` if you're using Yarn or `pnpm dev` if you're using pnpm. -If you're [changing the port number](/docs/pages/api-reference/next-cli#development) your application starts on, replace the `3000` in `http://localhost:3000` with the port you're using instead. +If you're [changing the port number](/docs/pages/api-reference/cli/next#next-dev-options) your application starts on, replace the `3000` in `http://localhost:3000` with the port you're using instead. If you're running Next.js from a directory other than root (for example, if you're using Turborepo) then you need to add `cwd` to the server-side and full stack debugging tasks. For example, `"cwd": "${workspaceFolder}/apps/web"`. diff --git a/docs/03-pages/02-api-reference/06-edge.mdx b/docs/03-pages/02-api-reference/05-edge.mdx similarity index 100% rename from docs/03-pages/02-api-reference/06-edge.mdx rename to docs/03-pages/02-api-reference/05-edge.mdx diff --git a/docs/03-pages/02-api-reference/06-cli/create-next-app.mdx b/docs/03-pages/02-api-reference/06-cli/create-next-app.mdx new file mode 100644 index 0000000000000..fc95de3c6eb96 --- /dev/null +++ b/docs/03-pages/02-api-reference/06-cli/create-next-app.mdx @@ -0,0 +1,7 @@ +--- +title: CLI +description: Create Next.js apps using one command with the create-next-app CLI. +source: app/api-reference/cli/create-next-app +--- + +{/* DO NOT EDIT. The content of this doc is generated from the source above. To edit the content of this page, navigate to the source page in your editor. 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. */} diff --git a/docs/03-pages/02-api-reference/04-create-next-app.mdx b/docs/03-pages/02-api-reference/06-cli/index.mdx similarity index 73% rename from docs/03-pages/02-api-reference/04-create-next-app.mdx rename to docs/03-pages/02-api-reference/06-cli/index.mdx index 526b85f346219..b343c5925c977 100644 --- a/docs/03-pages/02-api-reference/04-create-next-app.mdx +++ b/docs/03-pages/02-api-reference/06-cli/index.mdx @@ -1,7 +1,7 @@ --- -title: create-next-app -description: create-next-app -source: app/api-reference/create-next-app +title: CLI +description: API Reference for the Next.js Command Line Interface (CLI) tools. +source: app/api-reference/cli --- {/* DO NOT EDIT. The content of this doc is generated from the source above. To edit the content of this page, navigate to the source page in your editor. 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. */} diff --git a/docs/03-pages/02-api-reference/05-next-cli.mdx b/docs/03-pages/02-api-reference/06-cli/next.mdx similarity index 72% rename from docs/03-pages/02-api-reference/05-next-cli.mdx rename to docs/03-pages/02-api-reference/06-cli/next.mdx index b2a171358327f..f4863058bc41f 100644 --- a/docs/03-pages/02-api-reference/05-next-cli.mdx +++ b/docs/03-pages/02-api-reference/06-cli/next.mdx @@ -1,7 +1,7 @@ --- -title: Next.js CLI -description: Next.js CLI -source: app/api-reference/next-cli +title: next CLI +description: Learn how to run and build your application with the Next.js CLI. +source: app/api-reference/cli/next --- {/* DO NOT EDIT. The content of this doc is generated from the source above. To edit the content of this page, navigate to the source page in your editor. 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. */} diff --git a/errors/export-image-api.mdx b/errors/export-image-api.mdx index b2682cd43b2d7..273e65c3ed485 100644 --- a/errors/export-image-api.mdx +++ b/errors/export-image-api.mdx @@ -12,7 +12,7 @@ This is because Next.js optimizes images on-demand, as users request them (not a ## Possible Ways to Fix It -- Use [`next start`](/docs/pages/api-reference/next-cli#production) to run a server, which includes the Image Optimization API. +- Use [`next start`](/docs/pages/api-reference/cli/next#next-start-options) to run a server, which includes the Image Optimization API. - Use any provider which supports Image Optimization (such as [Vercel](https://vercel.com)). - [Configure `loader`](/docs/pages/api-reference/components/image#loader) in `next.config.js`. - [Configure `unoptimized`](/docs/pages/api-reference/components/image#unoptimized) in `next.config.js`. diff --git a/errors/export-no-custom-routes.mdx b/errors/export-no-custom-routes.mdx index 7fc0d4c61624f..840ba66846a54 100644 --- a/errors/export-no-custom-routes.mdx +++ b/errors/export-no-custom-routes.mdx @@ -11,7 +11,7 @@ These configs do not apply when exporting your Next.js application manually. ## Possible Ways to Fix It - Remove `rewrites`, `redirects`, and `headers` from your `next.config.js` to disable these features or -- Remove `output: 'export'` (or `next export`) in favor of [`next start`](/docs/pages/api-reference/next-cli#production) to run a production server +- Remove `output: 'export'` (or `next export`) in favor of [`next start`](/docs/pages/api-reference/cli/next#next-start-options) to run a production server ## Useful Links diff --git a/errors/export-no-i18n.mdx b/errors/export-no-i18n.mdx index bd79044f9c08d..de6f094a14ef8 100644 --- a/errors/export-no-i18n.mdx +++ b/errors/export-no-i18n.mdx @@ -9,7 +9,7 @@ In your `next.config.js` you defined `i18n`, along with `output: 'export'` (or y ## Possible Ways to Fix It - Remove `i18n` from your `next.config.js` to disable Internationalization or -- Remove `output: 'export'` (or `next export`) in favor of [`next start`](/docs/pages/api-reference/next-cli#production) to run a production server +- Remove `output: 'export'` (or `next export`) in favor of [`next start`](/docs/pages/api-reference/cli/next#next-start-options) to run a production server ## Useful Links diff --git a/errors/invalid-project-dir-casing.mdx b/errors/invalid-project-dir-casing.mdx index bdfedbd9774fb..544ed5d90b668 100644 --- a/errors/invalid-project-dir-casing.mdx +++ b/errors/invalid-project-dir-casing.mdx @@ -14,5 +14,5 @@ Ensure the casing for the current working directory matches the actual case of t ## Useful Links -- [Next.js CLI documentation](/docs/pages/api-reference/next-cli) +- [Next.js CLI documentation](/docs/pages/api-reference/cli/next) - [Case sensitivity in filesystems](https://en.wikipedia.org/wiki/Case_sensitivity#In_filesystems) diff --git a/examples/active-class-name/next-env.d.ts b/examples/active-class-name/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/active-class-name/next-env.d.ts +++ b/examples/active-class-name/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/amp/next-env.d.ts b/examples/amp/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/amp/next-env.d.ts +++ b/examples/amp/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/analyze-bundles/next-env.d.ts b/examples/analyze-bundles/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/analyze-bundles/next-env.d.ts +++ b/examples/analyze-bundles/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-apollo-server-and-client-auth/next-env.d.ts b/examples/api-routes-apollo-server-and-client-auth/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-apollo-server-and-client-auth/next-env.d.ts +++ b/examples/api-routes-apollo-server-and-client-auth/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-apollo-server-and-client/next-env.d.ts b/examples/api-routes-apollo-server-and-client/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-apollo-server-and-client/next-env.d.ts +++ b/examples/api-routes-apollo-server-and-client/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-apollo-server/next-env.d.ts b/examples/api-routes-apollo-server/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-apollo-server/next-env.d.ts +++ b/examples/api-routes-apollo-server/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-cors/next-env.d.ts b/examples/api-routes-cors/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-cors/next-env.d.ts +++ b/examples/api-routes-cors/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-graphql/next-env.d.ts b/examples/api-routes-graphql/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-graphql/next-env.d.ts +++ b/examples/api-routes-graphql/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-middleware/next-env.d.ts b/examples/api-routes-middleware/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-middleware/next-env.d.ts +++ b/examples/api-routes-middleware/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-rate-limit/next-env.d.ts b/examples/api-routes-rate-limit/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-rate-limit/next-env.d.ts +++ b/examples/api-routes-rate-limit/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes-rest/next-env.d.ts b/examples/api-routes-rest/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes-rest/next-env.d.ts +++ b/examples/api-routes-rest/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/api-routes/next-env.d.ts b/examples/api-routes/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/api-routes/next-env.d.ts +++ b/examples/api-routes/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/app-dir-i18n-routing/next-env.d.ts b/examples/app-dir-i18n-routing/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/app-dir-i18n-routing/next-env.d.ts +++ b/examples/app-dir-i18n-routing/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/app-dir-mdx/next-env.d.ts b/examples/app-dir-mdx/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/app-dir-mdx/next-env.d.ts +++ b/examples/app-dir-mdx/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/auth0/next-env.d.ts b/examples/auth0/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/auth0/next-env.d.ts +++ b/examples/auth0/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/basic-css/next-env.d.ts b/examples/basic-css/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/basic-css/next-env.d.ts +++ b/examples/basic-css/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/basic-export/next-env.d.ts b/examples/basic-export/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/basic-export/next-env.d.ts +++ b/examples/basic-export/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/blog-starter/next-env.d.ts b/examples/blog-starter/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/blog-starter/next-env.d.ts +++ b/examples/blog-starter/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/blog-with-comment/next-env.d.ts b/examples/blog-with-comment/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/blog-with-comment/next-env.d.ts +++ b/examples/blog-with-comment/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/blog/next-env.d.ts b/examples/blog/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/blog/next-env.d.ts +++ b/examples/blog/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/catch-all-routes/next-env.d.ts b/examples/catch-all-routes/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/catch-all-routes/next-env.d.ts +++ b/examples/catch-all-routes/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cloudflare-turnstile/next-env.d.ts b/examples/cloudflare-turnstile/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cloudflare-turnstile/next-env.d.ts +++ b/examples/cloudflare-turnstile/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-agilitycms/next-env.d.ts b/examples/cms-agilitycms/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-agilitycms/next-env.d.ts +++ b/examples/cms-agilitycms/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-cosmic/next-env.d.ts b/examples/cms-cosmic/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-cosmic/next-env.d.ts +++ b/examples/cms-cosmic/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-dotcms/next-env.d.ts b/examples/cms-dotcms/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-dotcms/next-env.d.ts +++ b/examples/cms-dotcms/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-enterspeed/next-env.d.ts b/examples/cms-enterspeed/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-enterspeed/next-env.d.ts +++ b/examples/cms-enterspeed/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-kontent-ai/next-env.d.ts b/examples/cms-kontent-ai/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-kontent-ai/next-env.d.ts +++ b/examples/cms-kontent-ai/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-makeswift/next-env.d.ts b/examples/cms-makeswift/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-makeswift/next-env.d.ts +++ b/examples/cms-makeswift/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-payload/next-env.d.ts b/examples/cms-payload/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/cms-payload/next-env.d.ts +++ b/examples/cms-payload/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-plasmic/next-env.d.ts b/examples/cms-plasmic/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-plasmic/next-env.d.ts +++ b/examples/cms-plasmic/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-prismic/next-env.d.ts b/examples/cms-prismic/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-prismic/next-env.d.ts +++ b/examples/cms-prismic/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-sitecore-xmcloud/next-env.d.ts b/examples/cms-sitecore-xmcloud/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-sitecore-xmcloud/next-env.d.ts +++ b/examples/cms-sitecore-xmcloud/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-sitefinity/next-env.d.ts b/examples/cms-sitefinity/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-sitefinity/next-env.d.ts +++ b/examples/cms-sitefinity/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-webiny/next-env.d.ts b/examples/cms-webiny/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-webiny/next-env.d.ts +++ b/examples/cms-webiny/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/cms-wordpress/next-env.d.ts b/examples/cms-wordpress/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/cms-wordpress/next-env.d.ts +++ b/examples/cms-wordpress/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/convex/next-env.d.ts b/examples/convex/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/convex/next-env.d.ts +++ b/examples/convex/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/custom-routes-proxying/next-env.d.ts b/examples/custom-routes-proxying/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/custom-routes-proxying/next-env.d.ts +++ b/examples/custom-routes-proxying/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/custom-server/next-env.d.ts b/examples/custom-server/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/custom-server/next-env.d.ts +++ b/examples/custom-server/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/dynamic-routing/next-env.d.ts b/examples/dynamic-routing/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/dynamic-routing/next-env.d.ts +++ b/examples/dynamic-routing/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/environment-variables/next-env.d.ts b/examples/environment-variables/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/environment-variables/next-env.d.ts +++ b/examples/environment-variables/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/github-pages/next-env.d.ts b/examples/github-pages/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/github-pages/next-env.d.ts +++ b/examples/github-pages/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/head-elements/next-env.d.ts b/examples/head-elements/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/head-elements/next-env.d.ts +++ b/examples/head-elements/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/headers/next-env.d.ts b/examples/headers/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/headers/next-env.d.ts +++ b/examples/headers/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/i18n-routing/next-env.d.ts b/examples/i18n-routing/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/i18n-routing/next-env.d.ts +++ b/examples/i18n-routing/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/image-component/next-env.d.ts b/examples/image-component/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/image-component/next-env.d.ts +++ b/examples/image-component/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/image-legacy-component/next-env.d.ts b/examples/image-legacy-component/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/image-legacy-component/next-env.d.ts +++ b/examples/image-legacy-component/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/layout-component/next-env.d.ts b/examples/layout-component/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/layout-component/next-env.d.ts +++ b/examples/layout-component/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/markdoc/next-env.d.ts b/examples/markdoc/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/markdoc/next-env.d.ts +++ b/examples/markdoc/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/middleware-matcher/next-env.d.ts b/examples/middleware-matcher/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/middleware-matcher/next-env.d.ts +++ b/examples/middleware-matcher/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/middleware/next-env.d.ts b/examples/middleware/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/middleware/next-env.d.ts +++ b/examples/middleware/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/modularize-imports/next-env.d.ts b/examples/modularize-imports/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/modularize-imports/next-env.d.ts +++ b/examples/modularize-imports/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/nested-components/next-env.d.ts b/examples/nested-components/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/nested-components/next-env.d.ts +++ b/examples/nested-components/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/next-css/next-env.d.ts b/examples/next-css/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/next-css/next-env.d.ts +++ b/examples/next-css/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/next-forms/next-env.d.ts b/examples/next-forms/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/next-forms/next-env.d.ts +++ b/examples/next-forms/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/next-offline/next-env.d.ts b/examples/next-offline/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/next-offline/next-env.d.ts +++ b/examples/next-offline/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/progressive-web-app/next-env.d.ts b/examples/progressive-web-app/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/progressive-web-app/next-env.d.ts +++ b/examples/progressive-web-app/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/radix-ui/next-env.d.ts b/examples/radix-ui/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/radix-ui/next-env.d.ts +++ b/examples/radix-ui/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/react-remove-properties/next-env.d.ts b/examples/react-remove-properties/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/react-remove-properties/next-env.d.ts +++ b/examples/react-remove-properties/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/redirects/next-env.d.ts b/examples/redirects/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/redirects/next-env.d.ts +++ b/examples/redirects/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/remove-console/next-env.d.ts b/examples/remove-console/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/remove-console/next-env.d.ts +++ b/examples/remove-console/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/reproduction-template/next-env.d.ts b/examples/reproduction-template/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/reproduction-template/next-env.d.ts +++ b/examples/reproduction-template/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/rewrites/next-env.d.ts b/examples/rewrites/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/rewrites/next-env.d.ts +++ b/examples/rewrites/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/script-component/next-env.d.ts b/examples/script-component/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/script-component/next-env.d.ts +++ b/examples/script-component/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/ssr-caching/next-env.d.ts b/examples/ssr-caching/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/ssr-caching/next-env.d.ts +++ b/examples/ssr-caching/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/svg-components/next-env.d.ts b/examples/svg-components/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/svg-components/next-env.d.ts +++ b/examples/svg-components/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-ably/next-env.d.ts b/examples/with-ably/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-ably/next-env.d.ts +++ b/examples/with-ably/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-absolute-imports/next-env.d.ts b/examples/with-absolute-imports/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-absolute-imports/next-env.d.ts +++ b/examples/with-absolute-imports/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-algolia-react-instantsearch/next-env.d.ts b/examples/with-algolia-react-instantsearch/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-algolia-react-instantsearch/next-env.d.ts +++ b/examples/with-algolia-react-instantsearch/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-ant-design/next-env.d.ts b/examples/with-ant-design/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-ant-design/next-env.d.ts +++ b/examples/with-ant-design/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-apivideo/next-env.d.ts b/examples/with-apivideo/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-apivideo/next-env.d.ts +++ b/examples/with-apivideo/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-axiom/next-env.d.ts b/examples/with-axiom/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-axiom/next-env.d.ts +++ b/examples/with-axiom/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-chakra-ui/next-env.d.ts b/examples/with-chakra-ui/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-chakra-ui/next-env.d.ts +++ b/examples/with-chakra-ui/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-cloudinary/next-env.d.ts b/examples/with-cloudinary/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-cloudinary/next-env.d.ts +++ b/examples/with-cloudinary/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-context-api/next-env.d.ts b/examples/with-context-api/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-context-api/next-env.d.ts +++ b/examples/with-context-api/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-cookies-next/next-env.d.ts b/examples/with-cookies-next/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-cookies-next/next-env.d.ts +++ b/examples/with-cookies-next/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-cssed/next-env.d.ts b/examples/with-cssed/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-cssed/next-env.d.ts +++ b/examples/with-cssed/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-cxs/next-env.d.ts b/examples/with-cxs/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-cxs/next-env.d.ts +++ b/examples/with-cxs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-cypress/next-env.d.ts b/examples/with-cypress/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-cypress/next-env.d.ts +++ b/examples/with-cypress/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-dynamic-import/next-env.d.ts b/examples/with-dynamic-import/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-dynamic-import/next-env.d.ts +++ b/examples/with-dynamic-import/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-edgedb/next-env.d.ts b/examples/with-edgedb/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-edgedb/next-env.d.ts +++ b/examples/with-edgedb/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-expo-typescript/next-env.d.ts b/examples/with-expo-typescript/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-expo-typescript/next-env.d.ts +++ b/examples/with-expo-typescript/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-fingerprintjs-pro/next-env.d.ts b/examples/with-fingerprintjs-pro/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-fingerprintjs-pro/next-env.d.ts +++ b/examples/with-fingerprintjs-pro/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-goober/next-env.d.ts b/examples/with-goober/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-goober/next-env.d.ts +++ b/examples/with-goober/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-grafbase/next-env.d.ts b/examples/with-grafbase/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-grafbase/next-env.d.ts +++ b/examples/with-grafbase/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-graphql-gateway/next-env.d.ts b/examples/with-graphql-gateway/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-graphql-gateway/next-env.d.ts +++ b/examples/with-graphql-gateway/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-gsap/next-env.d.ts b/examples/with-gsap/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-gsap/next-env.d.ts +++ b/examples/with-gsap/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-ionic-typescript/next-env.d.ts b/examples/with-ionic-typescript/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-ionic-typescript/next-env.d.ts +++ b/examples/with-ionic-typescript/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-jest-babel/next-env.d.ts b/examples/with-jest-babel/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-jest-babel/next-env.d.ts +++ b/examples/with-jest-babel/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-jest/next-env.d.ts b/examples/with-jest/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-jest/next-env.d.ts +++ b/examples/with-jest/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-jotai/next-env.d.ts b/examples/with-jotai/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-jotai/next-env.d.ts +++ b/examples/with-jotai/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-linaria/next-env.d.ts b/examples/with-linaria/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-linaria/next-env.d.ts +++ b/examples/with-linaria/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-mantine/next-env.d.ts b/examples/with-mantine/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-mantine/next-env.d.ts +++ b/examples/with-mantine/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-mobx-state-tree/next-env.d.ts b/examples/with-mobx-state-tree/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-mobx-state-tree/next-env.d.ts +++ b/examples/with-mobx-state-tree/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-mongodb-mongoose/next-env.d.ts b/examples/with-mongodb-mongoose/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-mongodb-mongoose/next-env.d.ts +++ b/examples/with-mongodb-mongoose/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-mongodb/next-env.d.ts b/examples/with-mongodb/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-mongodb/next-env.d.ts +++ b/examples/with-mongodb/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-mqtt-js/next-env.d.ts b/examples/with-mqtt-js/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-mqtt-js/next-env.d.ts +++ b/examples/with-mqtt-js/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-msw/next-env.d.ts b/examples/with-msw/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-msw/next-env.d.ts +++ b/examples/with-msw/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-mux-video/next-env.d.ts b/examples/with-mux-video/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-mux-video/next-env.d.ts +++ b/examples/with-mux-video/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-next-sitemap/next-env.d.ts b/examples/with-next-sitemap/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-next-sitemap/next-env.d.ts +++ b/examples/with-next-sitemap/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-next-ui/next-env.d.ts b/examples/with-next-ui/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-next-ui/next-env.d.ts +++ b/examples/with-next-ui/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-opentelemetry/next-env.d.ts b/examples/with-opentelemetry/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-opentelemetry/next-env.d.ts +++ b/examples/with-opentelemetry/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-particles/next-env.d.ts b/examples/with-particles/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-particles/next-env.d.ts +++ b/examples/with-particles/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-paste-typescript/next-env.d.ts b/examples/with-paste-typescript/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-paste-typescript/next-env.d.ts +++ b/examples/with-paste-typescript/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-postgres/next-env.d.ts b/examples/with-postgres/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-postgres/next-env.d.ts +++ b/examples/with-postgres/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-prefetching/next-env.d.ts b/examples/with-prefetching/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-prefetching/next-env.d.ts +++ b/examples/with-prefetching/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-react-foundation/next-env.d.ts b/examples/with-react-foundation/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-react-foundation/next-env.d.ts +++ b/examples/with-react-foundation/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-react-hook-form/next-env.d.ts b/examples/with-react-hook-form/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-react-hook-form/next-env.d.ts +++ b/examples/with-react-hook-form/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-react-intl/next-env.d.ts b/examples/with-react-intl/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-react-intl/next-env.d.ts +++ b/examples/with-react-intl/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-react-jss/next-env.d.ts b/examples/with-react-jss/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-react-jss/next-env.d.ts +++ b/examples/with-react-jss/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-react-md-typescript/next-env.d.ts b/examples/with-react-md-typescript/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-react-md-typescript/next-env.d.ts +++ b/examples/with-react-md-typescript/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-react-multi-carousel/next-env.d.ts b/examples/with-react-multi-carousel/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-react-multi-carousel/next-env.d.ts +++ b/examples/with-react-multi-carousel/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-redis/next-env.d.ts b/examples/with-redis/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-redis/next-env.d.ts +++ b/examples/with-redis/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-redux/next-env.d.ts b/examples/with-redux/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-redux/next-env.d.ts +++ b/examples/with-redux/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-reflexjs/next-env.d.ts b/examples/with-reflexjs/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-reflexjs/next-env.d.ts +++ b/examples/with-reflexjs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-sentry/next-env.d.ts b/examples/with-sentry/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-sentry/next-env.d.ts +++ b/examples/with-sentry/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-service-worker/next-env.d.ts b/examples/with-service-worker/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-service-worker/next-env.d.ts +++ b/examples/with-service-worker/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-sfcc/next-env.d.ts b/examples/with-sfcc/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-sfcc/next-env.d.ts +++ b/examples/with-sfcc/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-slate/next-env.d.ts b/examples/with-slate/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-slate/next-env.d.ts +++ b/examples/with-slate/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-static-export/next-env.d.ts b/examples/with-static-export/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-static-export/next-env.d.ts +++ b/examples/with-static-export/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-stitches/next-env.d.ts b/examples/with-stitches/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-stitches/next-env.d.ts +++ b/examples/with-stitches/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-storybook-styled-jsx-scss/next-env.d.ts b/examples/with-storybook-styled-jsx-scss/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-storybook-styled-jsx-scss/next-env.d.ts +++ b/examples/with-storybook-styled-jsx-scss/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-stripe-typescript/next-env.d.ts b/examples/with-stripe-typescript/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-stripe-typescript/next-env.d.ts +++ b/examples/with-stripe-typescript/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-styled-components-babel/next-env.d.ts b/examples/with-styled-components-babel/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-styled-components-babel/next-env.d.ts +++ b/examples/with-styled-components-babel/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-styled-components-rtl/next-env.d.ts b/examples/with-styled-components-rtl/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-styled-components-rtl/next-env.d.ts +++ b/examples/with-styled-components-rtl/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-styled-components/next-env.d.ts b/examples/with-styled-components/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-styled-components/next-env.d.ts +++ b/examples/with-styled-components/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-styled-jsx-plugins/next-env.d.ts b/examples/with-styled-jsx-plugins/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-styled-jsx-plugins/next-env.d.ts +++ b/examples/with-styled-jsx-plugins/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-styled-jsx-scss/next-env.d.ts b/examples/with-styled-jsx-scss/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-styled-jsx-scss/next-env.d.ts +++ b/examples/with-styled-jsx-scss/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-styled-jsx/next-env.d.ts b/examples/with-styled-jsx/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-styled-jsx/next-env.d.ts +++ b/examples/with-styled-jsx/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-temporal/next-env.d.ts b/examples/with-temporal/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-temporal/next-env.d.ts +++ b/examples/with-temporal/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-temporal/package.json b/examples/with-temporal/package.json index cfe16dabbdb57..5a895270e6215 100644 --- a/examples/with-temporal/package.json +++ b/examples/with-temporal/package.json @@ -24,8 +24,8 @@ "@types/node-fetch": "^3.0.3", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", - "@typescript-eslint/eslint-plugin": "^5.3.0", - "@typescript-eslint/parser": "^5.3.0", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", "cross-env": "^7.0.3", "nodemon": "^2.0.12", "ts-node": "^10.2.1", diff --git a/examples/with-tigris/next-env.d.ts b/examples/with-tigris/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-tigris/next-env.d.ts +++ b/examples/with-tigris/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-turbopack/next-env.d.ts b/examples/with-turbopack/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-turbopack/next-env.d.ts +++ b/examples/with-turbopack/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-typescript-graphql/next-env.d.ts b/examples/with-typescript-graphql/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-typescript-graphql/next-env.d.ts +++ b/examples/with-typescript-graphql/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-typescript-types/next-env.d.ts b/examples/with-typescript-types/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-typescript-types/next-env.d.ts +++ b/examples/with-typescript-types/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-typescript/next-env.d.ts b/examples/with-typescript/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-typescript/next-env.d.ts +++ b/examples/with-typescript/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-unsplash/next-env.d.ts b/examples/with-unsplash/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-unsplash/next-env.d.ts +++ b/examples/with-unsplash/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-vanilla-extract/next-env.d.ts b/examples/with-vanilla-extract/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-vanilla-extract/next-env.d.ts +++ b/examples/with-vanilla-extract/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-vercel-fetch/next-env.d.ts b/examples/with-vercel-fetch/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-vercel-fetch/next-env.d.ts +++ b/examples/with-vercel-fetch/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-videojs/next-env.d.ts b/examples/with-videojs/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-videojs/next-env.d.ts +++ b/examples/with-videojs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-vitest/next-env.d.ts b/examples/with-vitest/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-vitest/next-env.d.ts +++ b/examples/with-vitest/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-web-worker/next-env.d.ts b/examples/with-web-worker/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-web-worker/next-env.d.ts +++ b/examples/with-web-worker/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-webassembly/next-env.d.ts b/examples/with-webassembly/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/examples/with-webassembly/next-env.d.ts +++ b/examples/with-webassembly/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/examples/with-windicss/next-env.d.ts b/examples/with-windicss/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-windicss/next-env.d.ts +++ b/examples/with-windicss/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-xata/next-env.d.ts b/examples/with-xata/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-xata/next-env.d.ts +++ b/examples/with-xata/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-xstate/next-env.d.ts b/examples/with-xstate/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-xstate/next-env.d.ts +++ b/examples/with-xstate/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-yoga/next-env.d.ts b/examples/with-yoga/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-yoga/next-env.d.ts +++ b/examples/with-yoga/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/examples/with-zustand/next-env.d.ts b/examples/with-zustand/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/examples/with-zustand/next-env.d.ts +++ b/examples/with-zustand/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/lerna.json b/lerna.json index 135b3d8a5df9c..3bac1daf7ac71 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "14.2.7" + "version": "14.2.12" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 6d0a53a63f129..f8905b07e1444 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.7", + "version": "14.2.12", "keywords": [ "react", "next", diff --git a/packages/create-next-app/templates/app-tw/js/app/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/app-tw/js/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/app-tw/js/app/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/app-tw/js/app/fonts/GeistVF.woff b/packages/create-next-app/templates/app-tw/js/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/app-tw/js/app/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/app-tw/js/app/globals.css b/packages/create-next-app/templates/app-tw/js/app/globals.css index 875c01e819b90..13d40b892057e 100644 --- a/packages/create-next-app/templates/app-tw/js/app/globals.css +++ b/packages/create-next-app/templates/app-tw/js/app/globals.css @@ -3,27 +3,21 @@ @tailwind utilities; :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: #0a0a0a; + --foreground: #ededed; } } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; } @layer utilities { diff --git a/packages/create-next-app/templates/app-tw/js/app/layout.js b/packages/create-next-app/templates/app-tw/js/app/layout.js index 9aef1df7d6c3d..9800bf8dde1c4 100644 --- a/packages/create-next-app/templates/app-tw/js/app/layout.js +++ b/packages/create-next-app/templates/app-tw/js/app/layout.js @@ -1,7 +1,16 @@ -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; import "./globals.css"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export const metadata = { title: "Create Next App", @@ -11,7 +20,11 @@ export const metadata = { export default function RootLayout({ children }) { return ( - {children} + + {children} + ); } diff --git a/packages/create-next-app/templates/app-tw/js/app/page.js b/packages/create-next-app/templates/app-tw/js/app/page.js index a7c20368391e3..7f0afc267b23f 100644 --- a/packages/create-next-app/templates/app-tw/js/app/page.js +++ b/packages/create-next-app/templates/app-tw/js/app/page.js @@ -2,112 +2,100 @@ import Image from "next/image"; export default function Home() { return ( -
-
-

- Get started by editing  - app/page.js -

-
+
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.js + + . +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+ + ); } diff --git a/packages/create-next-app/templates/app-tw/js/public/next.svg b/packages/create-next-app/templates/app-tw/js/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/app-tw/js/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/app-tw/js/public/vercel.svg b/packages/create-next-app/templates/app-tw/js/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/app-tw/js/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/app-tw/js/tailwind.config.js b/packages/create-next-app/templates/app-tw/js/tailwind.config.js index 78ebc4e710e1c..af99692719e72 100644 --- a/packages/create-next-app/templates/app-tw/js/tailwind.config.js +++ b/packages/create-next-app/templates/app-tw/js/tailwind.config.js @@ -7,10 +7,9 @@ module.exports = { ], theme: { extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + background: "var(--background)", + foreground: "var(--foreground)", }, }, }, diff --git a/packages/create-next-app/templates/app-tw/ts/README-template.md b/packages/create-next-app/templates/app-tw/ts/README-template.md index c4033664f80d3..e215bc4ccf138 100644 --- a/packages/create-next-app/templates/app-tw/ts/README-template.md +++ b/packages/create-next-app/templates/app-tw/ts/README-template.md @@ -1,4 +1,4 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). ## Getting Started @@ -18,7 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More @@ -27,10 +27,10 @@ To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/packages/create-next-app/templates/app-tw/ts/app/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/app-tw/ts/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/app-tw/ts/app/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/app-tw/ts/app/fonts/GeistVF.woff b/packages/create-next-app/templates/app-tw/ts/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/app-tw/ts/app/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/app-tw/ts/app/globals.css b/packages/create-next-app/templates/app-tw/ts/app/globals.css index 875c01e819b90..13d40b892057e 100644 --- a/packages/create-next-app/templates/app-tw/ts/app/globals.css +++ b/packages/create-next-app/templates/app-tw/ts/app/globals.css @@ -3,27 +3,21 @@ @tailwind utilities; :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: #0a0a0a; + --foreground: #ededed; } } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; } @layer utilities { diff --git a/packages/create-next-app/templates/app-tw/ts/app/layout.tsx b/packages/create-next-app/templates/app-tw/ts/app/layout.tsx index 3314e4780a0c8..a36cde01c60b9 100644 --- a/packages/create-next-app/templates/app-tw/ts/app/layout.tsx +++ b/packages/create-next-app/templates/app-tw/ts/app/layout.tsx @@ -1,8 +1,17 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; import "./globals.css"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export const metadata: Metadata = { title: "Create Next App", @@ -16,7 +25,11 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/packages/create-next-app/templates/app-tw/ts/app/page.tsx b/packages/create-next-app/templates/app-tw/ts/app/page.tsx index 5705d4ea04573..433c8aa7fd732 100644 --- a/packages/create-next-app/templates/app-tw/ts/app/page.tsx +++ b/packages/create-next-app/templates/app-tw/ts/app/page.tsx @@ -2,112 +2,100 @@ import Image from "next/image"; export default function Home() { return ( -
-
-

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

-
+
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.tsx + + . +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+ + ); } diff --git a/packages/create-next-app/templates/app-tw/ts/eslintrc.json b/packages/create-next-app/templates/app-tw/ts/eslintrc.json index bffb357a71225..37224185490e6 100644 --- a/packages/create-next-app/templates/app-tw/ts/eslintrc.json +++ b/packages/create-next-app/templates/app-tw/ts/eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals", "next/typescript"] } diff --git a/packages/create-next-app/templates/app-tw/ts/next-env.d.ts b/packages/create-next-app/templates/app-tw/ts/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/packages/create-next-app/templates/app-tw/ts/next-env.d.ts +++ b/packages/create-next-app/templates/app-tw/ts/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/packages/create-next-app/templates/app-tw/ts/public/next.svg b/packages/create-next-app/templates/app-tw/ts/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/app-tw/ts/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/app-tw/ts/public/vercel.svg b/packages/create-next-app/templates/app-tw/ts/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/app-tw/ts/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/app-tw/ts/tailwind.config.ts b/packages/create-next-app/templates/app-tw/ts/tailwind.config.ts index 7e4bd91a03437..d43da912d03f9 100644 --- a/packages/create-next-app/templates/app-tw/ts/tailwind.config.ts +++ b/packages/create-next-app/templates/app-tw/ts/tailwind.config.ts @@ -8,10 +8,9 @@ const config: Config = { ], theme: { extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + background: "var(--background)", + foreground: "var(--foreground)", }, }, }, diff --git a/packages/create-next-app/templates/app/js/README-template.md b/packages/create-next-app/templates/app/js/README-template.md index 0dc9ea2bcc410..09a8a4d2c4ead 100644 --- a/packages/create-next-app/templates/app/js/README-template.md +++ b/packages/create-next-app/templates/app/js/README-template.md @@ -1,4 +1,4 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). ## Getting Started @@ -18,7 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More @@ -27,10 +27,10 @@ To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/packages/create-next-app/templates/app/js/app/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/app/js/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/app/js/app/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/app/js/app/fonts/GeistVF.woff b/packages/create-next-app/templates/app/js/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/app/js/app/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/app/js/app/globals.css b/packages/create-next-app/templates/app/js/app/globals.css index f4bd77c0ccacd..e3734be15e1f6 100644 --- a/packages/create-next-app/templates/app/js/app/globals.css +++ b/packages/create-next-app/templates/app/js/app/globals.css @@ -1,84 +1,15 @@ :root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", - "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", - "Fira Mono", "Droid Sans Mono", "Courier New", monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; + --background: #0a0a0a; + --foreground: #ededed; } } -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - html, body { max-width: 100vw; @@ -86,13 +17,17 @@ body { } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; } a { diff --git a/packages/create-next-app/templates/app/js/app/layout.js b/packages/create-next-app/templates/app/js/app/layout.js index 9aef1df7d6c3d..08210ccaab532 100644 --- a/packages/create-next-app/templates/app/js/app/layout.js +++ b/packages/create-next-app/templates/app/js/app/layout.js @@ -1,7 +1,16 @@ -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; import "./globals.css"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export const metadata = { title: "Create Next App", @@ -11,7 +20,9 @@ export const metadata = { export default function RootLayout({ children }) { return ( - {children} + + {children} + ); } diff --git a/packages/create-next-app/templates/app/js/app/page.js b/packages/create-next-app/templates/app/js/app/page.js index 6f7146072b8c7..5fe5d87daf280 100644 --- a/packages/create-next-app/templates/app/js/app/page.js +++ b/packages/create-next-app/templates/app/js/app/page.js @@ -3,93 +3,93 @@ import styles from "./page.module.css"; export default function Home() { return ( -
-
-

- Get started by editing  - app/page.js -

-
+
+
+ Next.js logo +
    +
  1. + Get started by editing app/page.js. +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+ + ); } diff --git a/packages/create-next-app/templates/app/js/app/page.module.css b/packages/create-next-app/templates/app/js/app/page.module.css index 5c4b1e6a2c614..8a460419f91fb 100644 --- a/packages/create-next-app/templates/app/js/app/page.module.css +++ b/packages/create-next-app/templates/app/js/app/page.module.css @@ -1,230 +1,165 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; +.page { + --gray-rgb: 0, 0, 0; + --gray-alpha-200: rgba(var(--gray-rgb), 0.08); + --gray-alpha-100: rgba(var(--gray-rgb), 0.05); + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + + display: grid; + grid-template-rows: 20px 1fr 20px; align-items: center; - padding: 6rem; - min-height: 100vh; + justify-items: center; + min-height: 100svh; + padding: 80px; + gap: 64px; + font-family: var(--font-geist-sans); } -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); +@media (prefers-color-scheme: dark) { + .page { + --gray-rgb: 255, 255, 255; + --gray-alpha-200: rgba(var(--gray-rgb), 0.145); + --gray-alpha-100: rgba(var(--gray-rgb), 0.06); + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + } } -.description a { +.main { display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; + flex-direction: column; + gap: 32px; + grid-row-start: 2; } -.description p { - position: relative; +.main ol { + font-family: var(--font-geist-mono); + padding-left: 0; margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); + font-size: 14px; + line-height: 24px; + letter-spacing: -0.01em; + list-style-position: inside; } -.code { - font-weight: 700; - font-family: var(--font-mono); +.main li:not(:last-of-type) { + margin-bottom: 8px; } -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - max-width: 100%; - width: var(--max-width); +.main code { + font-family: inherit; + background: var(--gray-alpha-100); + padding: 2px 4px; + border-radius: 4px; + font-weight: 600; } -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; +.ctas { + display: flex; + gap: 16px; +} + +.ctas a { + appearance: none; + border-radius: 128px; + height: 48px; + padding: 0 20px; + border: none; + border: 1px solid transparent; + transition: background 0.2s, color 0.2s, border-color 0.2s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + line-height: 20px; + font-weight: 500; } -.card span { - display: inline-block; - transition: transform 200ms; +a.primary { + background: var(--foreground); + color: var(--background); + gap: 8px; } -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; +a.secondary { + border-color: var(--gray-alpha-200); + min-width: 180px; } -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; - text-wrap: balance; +.footer { + grid-row-start: 3; + display: flex; + gap: 24px; } -.center { +.footer a { display: flex; - justify-content: center; align-items: center; - position: relative; - padding: 4rem 0; + gap: 8px; } -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; +.footer img { + flex-shrink: 0; } -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ""; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; } - .card:hover span { - transform: translateX(4px); + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; } -} -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; + .footer a:hover { + text-decoration: underline; + text-underline-offset: 4px; } } -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; +@media (max-width: 600px) { + .page { + padding: 32px; + padding-bottom: 80px; } - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; + .main { + align-items: center; } - .center::before { - transform: none; - height: 300px; + .main ol { + text-align: center; } - .description { - font-size: 0.8rem; + .ctas { + flex-direction: column; } - .description a { - padding: 1rem; + .ctas a { + font-size: 14px; + height: 40px; + padding: 0 16px; } - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; + a.secondary { + min-width: auto; } - .description p { + .footer { + flex-wrap: wrap; align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); + justify-content: center; } } @media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); + filter: invert(); } } diff --git a/packages/create-next-app/templates/app/js/public/next.svg b/packages/create-next-app/templates/app/js/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/app/js/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/app/js/public/vercel.svg b/packages/create-next-app/templates/app/js/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/app/js/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/app/ts/README-template.md b/packages/create-next-app/templates/app/ts/README-template.md index c4033664f80d3..e215bc4ccf138 100644 --- a/packages/create-next-app/templates/app/ts/README-template.md +++ b/packages/create-next-app/templates/app/ts/README-template.md @@ -1,4 +1,4 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). ## Getting Started @@ -18,7 +18,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More @@ -27,10 +27,10 @@ To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deploy on Vercel The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/packages/create-next-app/templates/app/ts/app/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/app/ts/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/app/ts/app/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/app/ts/app/fonts/GeistVF.woff b/packages/create-next-app/templates/app/ts/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/app/ts/app/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/app/ts/app/globals.css b/packages/create-next-app/templates/app/ts/app/globals.css index f4bd77c0ccacd..e3734be15e1f6 100644 --- a/packages/create-next-app/templates/app/ts/app/globals.css +++ b/packages/create-next-app/templates/app/ts/app/globals.css @@ -1,84 +1,15 @@ :root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", - "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", - "Fira Mono", "Droid Sans Mono", "Courier New", monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; + --background: #0a0a0a; + --foreground: #ededed; } } -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - html, body { max-width: 100vw; @@ -86,13 +17,17 @@ body { } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; } a { diff --git a/packages/create-next-app/templates/app/ts/app/layout.tsx b/packages/create-next-app/templates/app/ts/app/layout.tsx index 3314e4780a0c8..dca06aee77143 100644 --- a/packages/create-next-app/templates/app/ts/app/layout.tsx +++ b/packages/create-next-app/templates/app/ts/app/layout.tsx @@ -1,8 +1,17 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; import "./globals.css"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export const metadata: Metadata = { title: "Create Next App", @@ -16,7 +25,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/packages/create-next-app/templates/app/ts/app/page.module.css b/packages/create-next-app/templates/app/ts/app/page.module.css index 5c4b1e6a2c614..8a460419f91fb 100644 --- a/packages/create-next-app/templates/app/ts/app/page.module.css +++ b/packages/create-next-app/templates/app/ts/app/page.module.css @@ -1,230 +1,165 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; +.page { + --gray-rgb: 0, 0, 0; + --gray-alpha-200: rgba(var(--gray-rgb), 0.08); + --gray-alpha-100: rgba(var(--gray-rgb), 0.05); + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + + display: grid; + grid-template-rows: 20px 1fr 20px; align-items: center; - padding: 6rem; - min-height: 100vh; + justify-items: center; + min-height: 100svh; + padding: 80px; + gap: 64px; + font-family: var(--font-geist-sans); } -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); +@media (prefers-color-scheme: dark) { + .page { + --gray-rgb: 255, 255, 255; + --gray-alpha-200: rgba(var(--gray-rgb), 0.145); + --gray-alpha-100: rgba(var(--gray-rgb), 0.06); + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + } } -.description a { +.main { display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; + flex-direction: column; + gap: 32px; + grid-row-start: 2; } -.description p { - position: relative; +.main ol { + font-family: var(--font-geist-mono); + padding-left: 0; margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); + font-size: 14px; + line-height: 24px; + letter-spacing: -0.01em; + list-style-position: inside; } -.code { - font-weight: 700; - font-family: var(--font-mono); +.main li:not(:last-of-type) { + margin-bottom: 8px; } -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - max-width: 100%; - width: var(--max-width); +.main code { + font-family: inherit; + background: var(--gray-alpha-100); + padding: 2px 4px; + border-radius: 4px; + font-weight: 600; } -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; +.ctas { + display: flex; + gap: 16px; +} + +.ctas a { + appearance: none; + border-radius: 128px; + height: 48px; + padding: 0 20px; + border: none; + border: 1px solid transparent; + transition: background 0.2s, color 0.2s, border-color 0.2s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + line-height: 20px; + font-weight: 500; } -.card span { - display: inline-block; - transition: transform 200ms; +a.primary { + background: var(--foreground); + color: var(--background); + gap: 8px; } -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; +a.secondary { + border-color: var(--gray-alpha-200); + min-width: 180px; } -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; - text-wrap: balance; +.footer { + grid-row-start: 3; + display: flex; + gap: 24px; } -.center { +.footer a { display: flex; - justify-content: center; align-items: center; - position: relative; - padding: 4rem 0; + gap: 8px; } -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; +.footer img { + flex-shrink: 0; } -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ""; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; } - .card:hover span { - transform: translateX(4px); + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; } -} -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; + .footer a:hover { + text-decoration: underline; + text-underline-offset: 4px; } } -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; +@media (max-width: 600px) { + .page { + padding: 32px; + padding-bottom: 80px; } - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; + .main { + align-items: center; } - .center::before { - transform: none; - height: 300px; + .main ol { + text-align: center; } - .description { - font-size: 0.8rem; + .ctas { + flex-direction: column; } - .description a { - padding: 1rem; + .ctas a { + font-size: 14px; + height: 40px; + padding: 0 16px; } - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; + a.secondary { + min-width: auto; } - .description p { + .footer { + flex-wrap: wrap; align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); + justify-content: center; } } @media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); + filter: invert(); } } diff --git a/packages/create-next-app/templates/app/ts/app/page.tsx b/packages/create-next-app/templates/app/ts/app/page.tsx index 810709063d560..df5d042428aa0 100644 --- a/packages/create-next-app/templates/app/ts/app/page.tsx +++ b/packages/create-next-app/templates/app/ts/app/page.tsx @@ -3,93 +3,93 @@ import styles from "./page.module.css"; export default function Home() { return ( -
-
-

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

-
+
+
+ Next.js logo +
    +
  1. + Get started by editing app/page.tsx. +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+ + ); } diff --git a/packages/create-next-app/templates/app/ts/eslintrc.json b/packages/create-next-app/templates/app/ts/eslintrc.json index bffb357a71225..37224185490e6 100644 --- a/packages/create-next-app/templates/app/ts/eslintrc.json +++ b/packages/create-next-app/templates/app/ts/eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals", "next/typescript"] } diff --git a/packages/create-next-app/templates/app/ts/next-env.d.ts b/packages/create-next-app/templates/app/ts/next-env.d.ts index 4f11a03dc6cc3..40c3d68096c27 100644 --- a/packages/create-next-app/templates/app/ts/next-env.d.ts +++ b/packages/create-next-app/templates/app/ts/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/packages/create-next-app/templates/app/ts/public/next.svg b/packages/create-next-app/templates/app/ts/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/app/ts/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/app/ts/public/vercel.svg b/packages/create-next-app/templates/app/ts/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/app/ts/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default-tw/js/pages/_document.js b/packages/create-next-app/templates/default-tw/js/pages/_document.js index b2fff8b4262dd..628a7334c84a5 100644 --- a/packages/create-next-app/templates/default-tw/js/pages/_document.js +++ b/packages/create-next-app/templates/default-tw/js/pages/_document.js @@ -4,7 +4,7 @@ export default function Document() { return ( - +
diff --git a/packages/create-next-app/templates/default-tw/js/pages/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/default-tw/js/pages/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/default-tw/js/pages/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/default-tw/js/pages/fonts/GeistVF.woff b/packages/create-next-app/templates/default-tw/js/pages/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/default-tw/js/pages/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/default-tw/js/pages/index.js b/packages/create-next-app/templates/default-tw/js/pages/index.js index fa528e8e8e680..ada8e19417d67 100644 --- a/packages/create-next-app/templates/default-tw/js/pages/index.js +++ b/packages/create-next-app/templates/default-tw/js/pages/index.js @@ -1,118 +1,115 @@ import Image from "next/image"; -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export default function Home() { return ( -
-
-

- Get started by editing  - pages/index.js -

-
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + pages/index.js + + . +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+
+ + ); } diff --git a/packages/create-next-app/templates/default-tw/js/public/next.svg b/packages/create-next-app/templates/default-tw/js/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/default-tw/js/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default-tw/js/public/vercel.svg b/packages/create-next-app/templates/default-tw/js/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/default-tw/js/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default-tw/js/styles/globals.css b/packages/create-next-app/templates/default-tw/js/styles/globals.css index 875c01e819b90..13d40b892057e 100644 --- a/packages/create-next-app/templates/default-tw/js/styles/globals.css +++ b/packages/create-next-app/templates/default-tw/js/styles/globals.css @@ -3,27 +3,21 @@ @tailwind utilities; :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: #0a0a0a; + --foreground: #ededed; } } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; } @layer utilities { diff --git a/packages/create-next-app/templates/default-tw/js/tailwind.config.js b/packages/create-next-app/templates/default-tw/js/tailwind.config.js index 78ebc4e710e1c..af99692719e72 100644 --- a/packages/create-next-app/templates/default-tw/js/tailwind.config.js +++ b/packages/create-next-app/templates/default-tw/js/tailwind.config.js @@ -7,10 +7,9 @@ module.exports = { ], theme: { extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + background: "var(--background)", + foreground: "var(--foreground)", }, }, }, diff --git a/packages/create-next-app/templates/default-tw/ts/eslintrc.json b/packages/create-next-app/templates/default-tw/ts/eslintrc.json index bffb357a71225..37224185490e6 100644 --- a/packages/create-next-app/templates/default-tw/ts/eslintrc.json +++ b/packages/create-next-app/templates/default-tw/ts/eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals", "next/typescript"] } diff --git a/packages/create-next-app/templates/default-tw/ts/next-env.d.ts b/packages/create-next-app/templates/default-tw/ts/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/packages/create-next-app/templates/default-tw/ts/next-env.d.ts +++ b/packages/create-next-app/templates/default-tw/ts/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/packages/create-next-app/templates/default-tw/ts/pages/_document.tsx b/packages/create-next-app/templates/default-tw/ts/pages/_document.tsx index b2fff8b4262dd..628a7334c84a5 100644 --- a/packages/create-next-app/templates/default-tw/ts/pages/_document.tsx +++ b/packages/create-next-app/templates/default-tw/ts/pages/_document.tsx @@ -4,7 +4,7 @@ export default function Document() { return ( - +
diff --git a/packages/create-next-app/templates/default-tw/ts/pages/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/default-tw/ts/pages/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/default-tw/ts/pages/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/default-tw/ts/pages/fonts/GeistVF.woff b/packages/create-next-app/templates/default-tw/ts/pages/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/default-tw/ts/pages/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/default-tw/ts/pages/index.tsx b/packages/create-next-app/templates/default-tw/ts/pages/index.tsx index e5667d4021752..32c2c5899135d 100644 --- a/packages/create-next-app/templates/default-tw/ts/pages/index.tsx +++ b/packages/create-next-app/templates/default-tw/ts/pages/index.tsx @@ -1,118 +1,115 @@ import Image from "next/image"; -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export default function Home() { return ( -
-
-

- Get started by editing  - pages/index.tsx -

-
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + pages/index.tsx + + . +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+
+ + ); } diff --git a/packages/create-next-app/templates/default-tw/ts/public/next.svg b/packages/create-next-app/templates/default-tw/ts/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/default-tw/ts/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default-tw/ts/public/vercel.svg b/packages/create-next-app/templates/default-tw/ts/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/default-tw/ts/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default-tw/ts/styles/globals.css b/packages/create-next-app/templates/default-tw/ts/styles/globals.css index 875c01e819b90..13d40b892057e 100644 --- a/packages/create-next-app/templates/default-tw/ts/styles/globals.css +++ b/packages/create-next-app/templates/default-tw/ts/styles/globals.css @@ -3,27 +3,21 @@ @tailwind utilities; :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --background: #0a0a0a; + --foreground: #ededed; } } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; } @layer utilities { diff --git a/packages/create-next-app/templates/default-tw/ts/tailwind.config.ts b/packages/create-next-app/templates/default-tw/ts/tailwind.config.ts index 7e4bd91a03437..d43da912d03f9 100644 --- a/packages/create-next-app/templates/default-tw/ts/tailwind.config.ts +++ b/packages/create-next-app/templates/default-tw/ts/tailwind.config.ts @@ -8,10 +8,9 @@ const config: Config = { ], theme: { extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + colors: { + background: "var(--background)", + foreground: "var(--foreground)", }, }, }, diff --git a/packages/create-next-app/templates/default/js/pages/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/default/js/pages/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/default/js/pages/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/default/js/pages/fonts/GeistVF.woff b/packages/create-next-app/templates/default/js/pages/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/default/js/pages/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/default/js/pages/index.js b/packages/create-next-app/templates/default/js/pages/index.js index e49f72cce9960..a7d4b165b4431 100644 --- a/packages/create-next-app/templates/default/js/pages/index.js +++ b/packages/create-next-app/templates/default/js/pages/index.js @@ -1,9 +1,18 @@ import Head from "next/head"; import Image from "next/image"; -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; import styles from "@/styles/Home.module.css"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export default function Home() { return ( @@ -14,101 +23,96 @@ export default function Home() { -
-
-

- Get started by editing  - pages/index.js -

-
+
+
+ Next.js logo +
    +
  1. + Get started by editing pages/index.js. +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+ + ); } diff --git a/packages/create-next-app/templates/default/js/public/next.svg b/packages/create-next-app/templates/default/js/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/default/js/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default/js/public/vercel.svg b/packages/create-next-app/templates/default/js/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/default/js/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default/js/styles/Home.module.css b/packages/create-next-app/templates/default/js/styles/Home.module.css index 827f96590beae..8a460419f91fb 100644 --- a/packages/create-next-app/templates/default/js/styles/Home.module.css +++ b/packages/create-next-app/templates/default/js/styles/Home.module.css @@ -1,229 +1,165 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; +.page { + --gray-rgb: 0, 0, 0; + --gray-alpha-200: rgba(var(--gray-rgb), 0.08); + --gray-alpha-100: rgba(var(--gray-rgb), 0.05); + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + + display: grid; + grid-template-rows: 20px 1fr 20px; align-items: center; - padding: 6rem; - min-height: 100vh; + justify-items: center; + min-height: 100svh; + padding: 80px; + gap: 64px; + font-family: var(--font-geist-sans); } -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); +@media (prefers-color-scheme: dark) { + .page { + --gray-rgb: 255, 255, 255; + --gray-alpha-200: rgba(var(--gray-rgb), 0.145); + --gray-alpha-100: rgba(var(--gray-rgb), 0.06); + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + } } -.description a { +.main { display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; + flex-direction: column; + gap: 32px; + grid-row-start: 2; } -.description p { - position: relative; +.main ol { + font-family: var(--font-geist-mono); + padding-left: 0; margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); + font-size: 14px; + line-height: 24px; + letter-spacing: -0.01em; + list-style-position: inside; } -.code { - font-weight: 700; - font-family: var(--font-mono); +.main li:not(:last-of-type) { + margin-bottom: 8px; } -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - max-width: var(--max-width); - width: 100%; +.main code { + font-family: inherit; + background: var(--gray-alpha-100); + padding: 2px 4px; + border-radius: 4px; + font-weight: 600; } -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; +.ctas { + display: flex; + gap: 16px; +} + +.ctas a { + appearance: none; + border-radius: 128px; + height: 48px; + padding: 0 20px; + border: none; + border: 1px solid transparent; + transition: background 0.2s, color 0.2s, border-color 0.2s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + line-height: 20px; + font-weight: 500; } -.card span { - display: inline-block; - transition: transform 200ms; +a.primary { + background: var(--foreground); + color: var(--background); + gap: 8px; } -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; +a.secondary { + border-color: var(--gray-alpha-200); + min-width: 180px; } -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; +.footer { + grid-row-start: 3; + display: flex; + gap: 24px; } -.center { +.footer a { display: flex; - justify-content: center; align-items: center; - position: relative; - padding: 4rem 0; + gap: 8px; } -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; +.footer img { + flex-shrink: 0; } -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ""; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; } - .card:hover span { - transform: translateX(4px); + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; } -} -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; + .footer a:hover { + text-decoration: underline; + text-underline-offset: 4px; } } -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; +@media (max-width: 600px) { + .page { + padding: 32px; + padding-bottom: 80px; } - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; + .main { + align-items: center; } - .center::before { - transform: none; - height: 300px; + .main ol { + text-align: center; } - .description { - font-size: 0.8rem; + .ctas { + flex-direction: column; } - .description a { - padding: 1rem; + .ctas a { + font-size: 14px; + height: 40px; + padding: 0 16px; } - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; + a.secondary { + min-width: auto; } - .description p { + .footer { + flex-wrap: wrap; align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); + justify-content: center; } } @media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); + filter: invert(); } } diff --git a/packages/create-next-app/templates/default/js/styles/globals.css b/packages/create-next-app/templates/default/js/styles/globals.css index f4bd77c0ccacd..e3734be15e1f6 100644 --- a/packages/create-next-app/templates/default/js/styles/globals.css +++ b/packages/create-next-app/templates/default/js/styles/globals.css @@ -1,84 +1,15 @@ :root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", - "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", - "Fira Mono", "Droid Sans Mono", "Courier New", monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; + --background: #0a0a0a; + --foreground: #ededed; } } -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - html, body { max-width: 100vw; @@ -86,13 +17,17 @@ body { } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; } a { diff --git a/packages/create-next-app/templates/default/ts/eslintrc.json b/packages/create-next-app/templates/default/ts/eslintrc.json index bffb357a71225..37224185490e6 100644 --- a/packages/create-next-app/templates/default/ts/eslintrc.json +++ b/packages/create-next-app/templates/default/ts/eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals", "next/typescript"] } diff --git a/packages/create-next-app/templates/default/ts/next-env.d.ts b/packages/create-next-app/templates/default/ts/next-env.d.ts index 4f11a03dc6cc3..a4a7b3f5cfa2f 100644 --- a/packages/create-next-app/templates/default/ts/next-env.d.ts +++ b/packages/create-next-app/templates/default/ts/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/packages/create-next-app/templates/default/ts/pages/fonts/GeistMonoVF.woff b/packages/create-next-app/templates/default/ts/pages/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000..f2ae185cbfd16 Binary files /dev/null and b/packages/create-next-app/templates/default/ts/pages/fonts/GeistMonoVF.woff differ diff --git a/packages/create-next-app/templates/default/ts/pages/fonts/GeistVF.woff b/packages/create-next-app/templates/default/ts/pages/fonts/GeistVF.woff new file mode 100644 index 0000000000000..1b62daacff96d Binary files /dev/null and b/packages/create-next-app/templates/default/ts/pages/fonts/GeistVF.woff differ diff --git a/packages/create-next-app/templates/default/ts/pages/index.tsx b/packages/create-next-app/templates/default/ts/pages/index.tsx index acabe9ca2088e..141e6a5e7c908 100644 --- a/packages/create-next-app/templates/default/ts/pages/index.tsx +++ b/packages/create-next-app/templates/default/ts/pages/index.tsx @@ -1,9 +1,18 @@ import Head from "next/head"; import Image from "next/image"; -import { Inter } from "next/font/google"; +import localFont from "next/font/local"; import styles from "@/styles/Home.module.css"; -const inter = Inter({ subsets: ["latin"] }); +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); export default function Home() { return ( @@ -14,101 +23,96 @@ export default function Home() { -
-
-

- Get started by editing  - pages/index.tsx -

-
+
+
+ Next.js logo +
    +
  1. + Get started by editing pages/index.tsx. +
  2. +
  3. Save and see your changes instantly.
  4. +
+ + -
- -
- Next.js Logo -
- -
+ + ); } diff --git a/packages/create-next-app/templates/default/ts/public/next.svg b/packages/create-next-app/templates/default/ts/public/next.svg deleted file mode 100644 index 5174b28c565c2..0000000000000 --- a/packages/create-next-app/templates/default/ts/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default/ts/public/vercel.svg b/packages/create-next-app/templates/default/ts/public/vercel.svg deleted file mode 100644 index d2f84222734f2..0000000000000 --- a/packages/create-next-app/templates/default/ts/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/create-next-app/templates/default/ts/styles/Home.module.css b/packages/create-next-app/templates/default/ts/styles/Home.module.css index eee920e64c6ef..8a460419f91fb 100644 --- a/packages/create-next-app/templates/default/ts/styles/Home.module.css +++ b/packages/create-next-app/templates/default/ts/styles/Home.module.css @@ -1,229 +1,165 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; +.page { + --gray-rgb: 0, 0, 0; + --gray-alpha-200: rgba(var(--gray-rgb), 0.08); + --gray-alpha-100: rgba(var(--gray-rgb), 0.05); + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + + display: grid; + grid-template-rows: 20px 1fr 20px; align-items: center; - padding: 6rem; - min-height: 100vh; + justify-items: center; + min-height: 100svh; + padding: 80px; + gap: 64px; + font-family: var(--font-geist-sans); } -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); +@media (prefers-color-scheme: dark) { + .page { + --gray-rgb: 255, 255, 255; + --gray-alpha-200: rgba(var(--gray-rgb), 0.145); + --gray-alpha-100: rgba(var(--gray-rgb), 0.06); + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + } } -.description a { +.main { display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; + flex-direction: column; + gap: 32px; + grid-row-start: 2; } -.description p { - position: relative; +.main ol { + font-family: var(--font-geist-mono); + padding-left: 0; margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); + font-size: 14px; + line-height: 24px; + letter-spacing: -0.01em; + list-style-position: inside; } -.code { - font-weight: 700; - font-family: var(--font-mono); +.main li:not(:last-of-type) { + margin-bottom: 8px; } -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - max-width: 100%; - width: var(--max-width); +.main code { + font-family: inherit; + background: var(--gray-alpha-100); + padding: 2px 4px; + border-radius: 4px; + font-weight: 600; } -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; +.ctas { + display: flex; + gap: 16px; +} + +.ctas a { + appearance: none; + border-radius: 128px; + height: 48px; + padding: 0 20px; + border: none; + border: 1px solid transparent; + transition: background 0.2s, color 0.2s, border-color 0.2s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + line-height: 20px; + font-weight: 500; } -.card span { - display: inline-block; - transition: transform 200ms; +a.primary { + background: var(--foreground); + color: var(--background); + gap: 8px; } -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; +a.secondary { + border-color: var(--gray-alpha-200); + min-width: 180px; } -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; +.footer { + grid-row-start: 3; + display: flex; + gap: 24px; } -.center { +.footer a { display: flex; - justify-content: center; align-items: center; - position: relative; - padding: 4rem 0; + gap: 8px; } -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; +.footer img { + flex-shrink: 0; } -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ""; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} /* Enable hover only on non-touch devices */ @media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; } - .card:hover span { - transform: translateX(4px); + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; } -} -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; + .footer a:hover { + text-decoration: underline; + text-underline-offset: 4px; } } -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; +@media (max-width: 600px) { + .page { + padding: 32px; + padding-bottom: 80px; } - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; + .main { + align-items: center; } - .center::before { - transform: none; - height: 300px; + .main ol { + text-align: center; } - .description { - font-size: 0.8rem; + .ctas { + flex-direction: column; } - .description a { - padding: 1rem; + .ctas a { + font-size: 14px; + height: 40px; + padding: 0 16px; } - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; + a.secondary { + min-width: auto; } - .description p { + .footer { + flex-wrap: wrap; align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); + justify-content: center; } } @media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); + filter: invert(); } } diff --git a/packages/create-next-app/templates/default/ts/styles/globals.css b/packages/create-next-app/templates/default/ts/styles/globals.css index f4bd77c0ccacd..e3734be15e1f6 100644 --- a/packages/create-next-app/templates/default/ts/styles/globals.css +++ b/packages/create-next-app/templates/default/ts/styles/globals.css @@ -1,84 +1,15 @@ :root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", - "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", - "Fira Mono", "Droid Sans Mono", "Courier New", monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; + --background: #0a0a0a; + --foreground: #ededed; } } -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - html, body { max-width: 100vw; @@ -86,13 +17,17 @@ body { } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; } a { diff --git a/packages/create-next-app/templates/index.ts b/packages/create-next-app/templates/index.ts index c4976b2f1bde5..c3ebc78d30ffd 100644 --- a/packages/create-next-app/templates/index.ts +++ b/packages/create-next-app/templates/index.ts @@ -97,7 +97,16 @@ export const installTemplate = async ({ stats: false, // We don't want to modify compiler options in [ts/js]config.json // and none of the files in the .git folder - ignore: ["tsconfig.json", "jsconfig.json", ".git/**/*"], + // TODO: Refactor this to be an allowlist, rather than a denylist, + // to avoid corrupting files that weren't intended to be replaced + + ignore: [ + "tsconfig.json", + "jsconfig.json", + ".git/**/*", + "**/fonts/**", + "**/favicon.ico", + ], }); const writeSema = new Sema(8, { capacity: files.length }); await Promise.all( diff --git a/packages/eslint-config-next/index.js b/packages/eslint-config-next/index.js index a17aa9d618f9f..4c95a88ad4fba 100644 --- a/packages/eslint-config-next/index.js +++ b/packages/eslint-config-next/index.js @@ -30,12 +30,13 @@ sortedPaths.push(...keptPaths) const hookPropertyMap = new Map( [ - ['eslint-plugin-import', 'eslint-plugin-import'], - ['eslint-plugin-react', 'eslint-plugin-react'], - ['eslint-plugin-jsx-a11y', 'eslint-plugin-jsx-a11y'], - ].map(([request, replacement]) => [ + '@typescript-eslint/eslint-plugin', + 'eslint-plugin-import', + 'eslint-plugin-react', + 'eslint-plugin-jsx-a11y', + ].map((request) => [ request, - require.resolve(replacement, { paths: sortedPaths }), + require.resolve(request, { paths: sortedPaths }), ]) ) @@ -96,10 +97,6 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - warnOnUnsupportedTypeScriptVersion: true, }, }, ], diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 7e67a8d8a854f..5bebd0f94e72c 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.7", + "version": "14.2.12", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,9 +10,10 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "14.2.7", + "@next/eslint-plugin-next": "14.2.12", "@rushstack/eslint-patch": "^1.3.3", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.28.1", diff --git a/packages/eslint-config-next/typescript.js b/packages/eslint-config-next/typescript.js new file mode 100644 index 0000000000000..810b7df219d98 --- /dev/null +++ b/packages/eslint-config-next/typescript.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['plugin:@typescript-eslint/recommended'], +} diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 65d49e6488e39..5c98fd6fe794c 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.7", + "version": "14.2.12", "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 6bb96b462654a..7c24bde19ff8b 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "14.2.7", + "version": "14.2.12", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 395dc3c5dd5ea..49762e0e9f448 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.7", + "version": "14.2.12", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 0512bd7316745..67c6e55f405fd 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "14.2.7", + "version": "14.2.12", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 4485b6bb70a29..cc76024c60d5b 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "14.2.7", + "version": "14.2.12", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 2ef5299a18169..f605bc7f774b3 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "14.2.7", + "version": "14.2.12", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index d1b3f6b2fa455..454ffec8564dd 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.7", + "version": "14.2.12", "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 2e6a4d7fb9b8b..994e1ad81d44e 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.7", + "version": "14.2.12", "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 44d01e1aee958..4e4c447b93381 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.7", + "version": "14.2.12", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/crates/napi/src/next_api/project.rs b/packages/next-swc/crates/napi/src/next_api/project.rs index 1e2c64de86485..85adc65879623 100644 --- a/packages/next-swc/crates/napi/src/next_api/project.rs +++ b/packages/next-swc/crates/napi/src/next_api/project.rs @@ -9,8 +9,8 @@ use napi::{ use next_api::{ entrypoints::Entrypoints, project::{ - DefineEnv, Instrumentation, Middleware, PartialProjectOptions, Project, ProjectContainer, - ProjectOptions, + DefineEnv, DraftModeOptions, Instrumentation, Middleware, PartialProjectOptions, Project, + ProjectContainer, ProjectOptions, }, route::{Endpoint, Route}, }; @@ -62,6 +62,23 @@ pub struct NapiEnvVar { pub value: String, } +#[napi(object)] +pub struct NapiDraftModeOptions { + pub preview_mode_id: String, + pub preview_mode_encryption_key: String, + pub preview_mode_signing_key: String, +} + +impl From for DraftModeOptions { + fn from(val: NapiDraftModeOptions) -> Self { + DraftModeOptions { + preview_mode_id: val.preview_mode_id, + preview_mode_encryption_key: val.preview_mode_encryption_key, + preview_mode_signing_key: val.preview_mode_signing_key, + } + } +} + #[napi(object)] pub struct NapiProjectOptions { /// A root path from which all files must be nested under. Trying to access @@ -93,6 +110,15 @@ pub struct NapiProjectOptions { /// The mode in which Next.js is running. pub dev: bool, + + /// The server actions encryption key. + pub encryption_key: String, + + /// The build id. + pub build_id: String, + + /// Options for draft mode. + pub preview_props: NapiDraftModeOptions, } /// [NapiProjectOptions] with all fields optional. @@ -127,6 +153,15 @@ pub struct NapiPartialProjectOptions { /// The mode in which Next.js is running. pub dev: Option, + + /// The server actions encryption key. + pub encryption_key: Option, + + /// The build id. + pub build_id: Option, + + /// Options for draft mode. + pub preview_props: Option, } #[napi(object)] @@ -158,6 +193,9 @@ impl From for ProjectOptions { .collect(), define_env: val.define_env.into(), dev: val.dev, + encryption_key: val.encryption_key, + build_id: val.build_id, + preview_props: val.preview_props.into(), } } } @@ -175,6 +213,9 @@ impl From for PartialProjectOptions { .map(|env| env.into_iter().map(|var| (var.name, var.value)).collect()), define_env: val.define_env.map(|env| env.into()), dev: val.dev, + encryption_key: val.encryption_key, + build_id: val.build_id, + preview_props: val.preview_props.map(|props| props.into()), } } } diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 1fda6368d3aed..6382f389ff7ee 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -846,13 +846,13 @@ impl AppEndpoint { { entry_client_chunks.extend(chunks.await?.iter().copied()); } - for chunks in client_references_chunks_ref + for (chunks, _) in client_references_chunks_ref .client_component_client_chunks .values() { client_assets.extend(chunks.await?.iter().copied()); } - for chunks in client_references_chunks_ref + for (chunks, _) in client_references_chunks_ref .client_component_ssr_chunks .values() { @@ -929,7 +929,7 @@ impl AppEndpoint { // initialization let client_references_chunks = &*client_references_chunks.await?; - for &ssr_chunks in client_references_chunks + for (ssr_chunks, _) in client_references_chunks .client_component_ssr_chunks .values() { @@ -1078,6 +1078,7 @@ impl AppEndpoint { .clone() .map(Regions::Multiple), matchers: vec![matchers], + env: this.app_project.project().edge_env().await?.clone_value(), ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { diff --git a/packages/next-swc/crates/next-api/src/middleware.rs b/packages/next-swc/crates/next-api/src/middleware.rs index de957905b8a13..8faa79793b0a2 100644 --- a/packages/next-swc/crates/next-api/src/middleware.rs +++ b/packages/next-swc/crates/next-api/src/middleware.rs @@ -158,6 +158,7 @@ impl MiddlewareEndpoint { page: "/".to_string(), regions: None, matchers, + env: this.project.edge_env().await?.clone_value(), ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { diff --git a/packages/next-swc/crates/next-api/src/pages.rs b/packages/next-swc/crates/next-api/src/pages.rs index b76e96322af53..e4e7595cc2bb4 100644 --- a/packages/next-swc/crates/next-api/src/pages.rs +++ b/packages/next-swc/crates/next-api/src/pages.rs @@ -1095,6 +1095,7 @@ impl PageEndpoint { page: original_name.to_string(), regions: None, matchers: vec![matchers], + env: this.pages_project.project().edge_env().await?.clone_value(), ..Default::default() }; let middleware_manifest_v2 = MiddlewaresManifestV2 { diff --git a/packages/next-swc/crates/next-api/src/project.rs b/packages/next-swc/crates/next-api/src/project.rs index ee0ee8333549c..9c9c9eaf05367 100644 --- a/packages/next-swc/crates/next-api/src/project.rs +++ b/packages/next-swc/crates/next-api/src/project.rs @@ -1,7 +1,7 @@ use std::path::MAIN_SEPARATOR; use anyhow::Result; -use indexmap::{map::Entry, IndexMap}; +use indexmap::{indexmap, map::Entry, IndexMap}; use next_core::{ all_assets_from_entries, app_structure::find_app_dir, @@ -68,6 +68,14 @@ use crate::{ versioned_content_map::{OutputAssetsOperation, VersionedContentMap}, }; +#[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] +#[serde(rename_all = "camelCase")] +pub struct DraftModeOptions { + pub preview_mode_id: String, + pub preview_mode_encryption_key: String, + pub preview_mode_signing_key: String, +} + #[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] #[serde(rename_all = "camelCase")] pub struct ProjectOptions { @@ -96,6 +104,15 @@ pub struct ProjectOptions { /// The mode in which Next.js is running. pub dev: bool, + + /// The server actions encryption key. + pub encryption_key: String, + + /// The build id. + pub build_id: String, + + /// Options for draft mode. + pub preview_props: DraftModeOptions, } #[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] @@ -126,6 +143,15 @@ pub struct PartialProjectOptions { /// The mode in which Next.js is running. pub dev: Option, + + /// The server actions encryption key. + pub encryption_key: Option, + + /// The build id. + pub build_id: Option, + + /// Options for draft mode. + pub preview_props: Option, } #[derive(Debug, Serialize, Deserialize, Clone, TaskInput, PartialEq, Eq, TraceRawVcs)] @@ -166,29 +192,55 @@ impl ProjectContainer { #[turbo_tasks::function] pub fn update(&self, options: PartialProjectOptions) -> Vc<()> { + let PartialProjectOptions { + root_path, + project_path, + next_config, + js_config, + env, + define_env, + watch, + dev, + encryption_key, + build_id, + preview_props, + } = options; + let mut new_options = self.options_state.get().clone(); - if let Some(root_path) = options.root_path { + if let Some(root_path) = root_path { new_options.root_path = root_path; } - if let Some(project_path) = options.project_path { + if let Some(project_path) = project_path { new_options.project_path = project_path; } - if let Some(next_config) = options.next_config { + if let Some(next_config) = next_config { new_options.next_config = next_config; } - if let Some(js_config) = options.js_config { + if let Some(js_config) = js_config { new_options.js_config = js_config; } - if let Some(env) = options.env { + if let Some(env) = env { new_options.env = env; } - if let Some(define_env) = options.define_env { + if let Some(define_env) = define_env { new_options.define_env = define_env; } - if let Some(watch) = options.watch { + if let Some(watch) = watch { new_options.watch = watch; } + if let Some(dev) = dev { + new_options.dev = dev; + } + if let Some(encryption_key) = encryption_key { + new_options.encryption_key = encryption_key; + } + if let Some(build_id) = build_id { + new_options.build_id = build_id; + } + if let Some(preview_props) = preview_props { + new_options.preview_props = preview_props; + } // TODO: Handle mode switch, should prevent mode being switched. @@ -201,32 +253,36 @@ impl ProjectContainer { pub async fn project(self: Vc) -> Result> { let this = self.await?; - let (env, define_env, next_config, js_config, root_path, project_path, watch, dev) = { + let env_map: Vc; + let next_config; + let define_env; + let js_config; + let root_path; + let project_path; + let watch; + let dev; + let encryption_key; + let build_id; + let preview_props; + { let options = this.options_state.get(); - let env: Vc = Vc::cell(options.env.iter().cloned().collect()); - let define_env: Vc = ProjectDefineEnv { + env_map = Vc::cell(options.env.iter().cloned().collect()); + define_env = ProjectDefineEnv { client: Vc::cell(options.define_env.client.iter().cloned().collect()), edge: Vc::cell(options.define_env.edge.iter().cloned().collect()), nodejs: Vc::cell(options.define_env.nodejs.iter().cloned().collect()), } .cell(); - let next_config = NextConfig::from_string(Vc::cell(options.next_config.clone())); - let js_config = JsConfig::from_string(Vc::cell(options.js_config.clone())); - let root_path = options.root_path.clone(); - let project_path = options.project_path.clone(); - let watch = options.watch; - let dev = options.dev; - ( - env, - define_env, - next_config, - js_config, - root_path, - project_path, - watch, - dev, - ) - }; + next_config = NextConfig::from_string(Vc::cell(options.next_config.clone())); + js_config = JsConfig::from_string(Vc::cell(options.js_config.clone())); + root_path = options.root_path.clone(); + project_path = options.project_path.clone(); + watch = options.watch; + dev = options.dev; + encryption_key = options.encryption_key.clone(); + build_id = options.build_id.clone(); + preview_props = options.preview_props.clone(); + } let dist_dir = next_config .await? @@ -241,7 +297,7 @@ impl ProjectContainer { next_config, js_config, dist_dir, - env: Vc::upcast(env), + env: Vc::upcast(env_map), define_env, browserslist_query: "last 1 Chrome versions, last 1 Firefox versions, last 1 Safari \ versions, last 1 Edge versions" @@ -252,6 +308,9 @@ impl ProjectContainer { NextMode::Build.cell() }, versioned_content_map: this.versioned_content_map, + build_id, + encryption_key, + preview_props, } .cell()) } @@ -323,6 +382,12 @@ pub struct Project { mode: Vc, versioned_content_map: Vc, + + build_id: String, + + encryption_key: String, + + preview_props: DraftModeOptions, } #[turbo_tasks::value] @@ -545,6 +610,18 @@ impl Project { )) } + #[turbo_tasks::function] + pub(super) fn edge_env(&self) -> Vc { + let edge_env = indexmap! { + "__NEXT_BUILD_ID".to_string() => self.build_id.clone(), + "NEXT_SERVER_ACTIONS_ENCRYPTION_KEY".to_string() => self.encryption_key.clone(), + "__NEXT_PREVIEW_MODE_ID".to_string() => self.preview_props.preview_mode_id.clone(), + "__NEXT_PREVIEW_MODE_ENCRYPTION_KEY".to_string() => self.preview_props.preview_mode_encryption_key.clone(), + "__NEXT_PREVIEW_MODE_SIGNING_KEY".to_string() => self.preview_props.preview_mode_signing_key.clone(), + }; + Vc::cell(edge_env) + } + #[turbo_tasks::function] pub(super) async fn client_chunking_context( self: Vc, diff --git a/packages/next-swc/crates/next-core/src/app_structure.rs b/packages/next-swc/crates/next-core/src/app_structure.rs index 744b633515567..b84abb93a673e 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -769,17 +769,6 @@ async fn directory_tree_to_loader_tree( .then_some(components.page) .flatten() { - // When resolving metadata with corresponding module - // (https://github.com/vercel/next.js/blob/aa1ee5995cdd92cc9a2236ce4b6aa2b67c9d32b2/packages/next/src/lib/metadata/resolve-metadata.ts#L340) - // layout takes precedence over page (https://github.com/vercel/next.js/blob/aa1ee5995cdd92cc9a2236ce4b6aa2b67c9d32b2/packages/next/src/server/lib/app-dir-module.ts#L22) - // If the component have layout and page both, do not attach same metadata to - // the page. - let metadata = if components.layout.is_some() { - Default::default() - } else { - components.metadata.clone() - }; - tree.parallel_routes.insert( "children".to_string(), LoaderTree { @@ -788,7 +777,7 @@ async fn directory_tree_to_loader_tree( parallel_routes: IndexMap::new(), components: Components { page: Some(page), - metadata, + metadata: components.metadata, ..Default::default() } .cell(), diff --git a/packages/next-swc/crates/next-core/src/loader_tree.rs b/packages/next-swc/crates/next-core/src/loader_tree.rs index 58c6bec8f4972..46fe54f8b1019 100644 --- a/packages/next-swc/crates/next-core/src/loader_tree.rs +++ b/packages/next-swc/crates/next-core/src/loader_tree.rs @@ -142,7 +142,11 @@ impl LoaderTreeBuilder { metadata: &Metadata, global_metadata: Option<&GlobalMetadata>, ) -> Result<()> { - if metadata.is_empty() { + if metadata.is_empty() + && global_metadata + .map(|global| global.is_empty()) + .unwrap_or_default() + { return Ok(()); } let Metadata { diff --git a/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs b/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs index d00d0fc491dec..40b6857ef35f8 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_client_references_chunks.rs @@ -32,8 +32,10 @@ fn client_modules_ssr_modifier() -> Vc { #[turbo_tasks::value] pub struct ClientReferencesChunks { - pub client_component_client_chunks: IndexMap>, - pub client_component_ssr_chunks: IndexMap>, + pub client_component_client_chunks: + IndexMap, AvailabilityInfo)>, + pub client_component_ssr_chunks: + IndexMap, AvailabilityInfo)>, pub layout_segment_client_chunks: IndexMap, Vc>, } @@ -66,23 +68,47 @@ pub async fn get_app_client_references_chunks( ) => { let ecmascript_client_reference_ref = ecmascript_client_reference.await?; - ( - client_chunking_context.root_chunk_group_assets(Vc::upcast( + + let client_chunk_group = client_chunking_context + .root_chunk_group(Vc::upcast( ecmascript_client_reference_ref.client_module, - )), - ssr_chunking_context.map(|ssr_chunking_context| { - ssr_chunking_context.root_chunk_group_assets(Vc::upcast( - ecmascript_client_reference_ref.ssr_module, + )) + .await?; + + ( + ( + client_chunk_group.assets, + client_chunk_group.availability_info, + ), + if let Some(ssr_chunking_context) = ssr_chunking_context { + let ssr_chunk_group = ssr_chunking_context + .root_chunk_group(Vc::upcast( + ecmascript_client_reference_ref.ssr_module, + )) + .await?; + + Some(( + ssr_chunk_group.assets, + ssr_chunk_group.availability_info, )) - }), + } else { + None + }, ) } ClientReferenceType::CssClientReference(css_client_reference) => { let css_client_reference_ref = css_client_reference.await?; - ( - client_chunking_context.root_chunk_group_assets(Vc::upcast( + let client_chunk_group = client_chunking_context + .root_chunk_group(Vc::upcast( css_client_reference_ref.client_module, - )), + )) + .await?; + + ( + ( + client_chunk_group.assets, + client_chunk_group.availability_info, + ), None, ) } @@ -165,6 +191,7 @@ pub async fn get_app_client_references_chunks( }) .try_flat_join() .await?; + let ssr_chunk_group = if !ssr_modules.is_empty() { let ssr_entry_module = IncludeModulesModule::new( base_ident.with_modifier(client_modules_ssr_modifier()), @@ -184,6 +211,7 @@ pub async fn get_app_client_references_chunks( } else { None }; + let client_modules = client_reference_types .iter() .map(|client_reference_ty| async move { @@ -240,8 +268,10 @@ pub async fn get_app_client_references_chunks( if let ClientReferenceType::EcmascriptClientReference(_) = client_reference_ty { - client_component_client_chunks - .insert(client_reference_ty, client_chunks); + client_component_client_chunks.insert( + client_reference_ty, + (client_chunks, client_chunk_group.availability_info), + ); } } } @@ -261,7 +291,10 @@ pub async fn get_app_client_references_chunks( if let ClientReferenceType::EcmascriptClientReference(_) = client_reference_ty { - client_component_ssr_chunks.insert(client_reference_ty, ssr_chunks); + client_component_ssr_chunks.insert( + client_reference_ty, + (ssr_chunks, ssr_chunk_group.availability_info), + ); } } } diff --git a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs index fdb7dc3193efe..77f4a1b14857c 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs @@ -152,7 +152,6 @@ async fn wrap_edge_page( let next_config = &*next_config.await?; // TODO(WEB-1824): add build support - let build_id = "development"; let dev = true; // TODO(timneutkens): remove this @@ -174,7 +173,6 @@ async fn wrap_edge_page( indexmap! { "VAR_USERLAND" => INNER.to_string(), "VAR_PAGE" => page.to_string(), - "VAR_BUILD_ID" => build_id.to_string(), }, indexmap! { "sriEnabled" => serde_json::Value::Bool(sri_enabled).to_string(), diff --git a/packages/next-swc/crates/next-core/src/next_edge/context.rs b/packages/next-swc/crates/next-core/src/next_edge/context.rs index 3022e54aba549..a3b11a95add37 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/context.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/context.rs @@ -108,15 +108,8 @@ pub async fn get_edge_resolve_options_context( .map(ToString::to_string), ); - match ty { - ServerContextType::AppRSC { .. } => custom_conditions.push("react-server".to_string()), - ServerContextType::AppRoute { .. } - | ServerContextType::Pages { .. } - | ServerContextType::PagesData { .. } - | ServerContextType::PagesApi { .. } - | ServerContextType::AppSSR { .. } - | ServerContextType::Middleware { .. } - | ServerContextType::Instrumentation { .. } => {} + if ty.supports_react_server() { + custom_conditions.push("react-server".to_string()); }; let resolve_options_context = ResolveOptionsContext { 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 1be71bf8e7cc8..8116929c721b9 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 @@ -728,7 +728,7 @@ async fn rsc_aliases( } if runtime == NextRuntime::Edge { - if matches!(ty, ServerContextType::AppRSC { .. }) { + if ty.supports_react_server() { alias["react"] = format!("next/dist/compiled/react{react_channel}/react.react-server"); alias["react-dom"] = format!("next/dist/compiled/react-dom{react_channel}/react-dom.react-server"); diff --git a/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs b/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs index 286fe3837a239..e4414a488009c 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/client_reference_manifest.rs @@ -5,7 +5,10 @@ use turbo_tasks_fs::{File, FileSystemPath}; use turbopack_binding::turbopack::{ core::{ asset::AssetContent, - chunk::{ChunkItemExt, ChunkableModule, ModuleId as TurbopackModuleId}, + chunk::{ + availability_info::AvailabilityInfo, ChunkItem, ChunkItemExt, ChunkableModule, + ModuleId as TurbopackModuleId, + }, output::OutputAsset, virtual_output::VirtualOutputAsset, }, @@ -66,61 +69,70 @@ impl ClientReferenceManifest { .to_string() .await?; - let client_module_id = ecmascript_client_reference + let client_chunk_item = ecmascript_client_reference .client_module - .as_chunk_item(Vc::upcast(client_chunking_context)) - .id() - .await?; + .as_chunk_item(Vc::upcast(client_chunking_context)); + + let client_module_id = client_chunk_item.id().await?; + + let (client_chunks_paths, client_is_async) = + if let Some((client_chunks, client_availability_info)) = + client_references_chunks + .client_component_client_chunks + .get(&app_client_reference_ty) + { + let client_chunks = client_chunks.await?; + let client_chunks_paths = client_chunks + .iter() + .map(|chunk| chunk.ident().path()) + .try_join() + .await?; + + let chunk_paths = client_chunks_paths + .iter() + .filter_map(|chunk_path| client_relative_path.get_path_to(chunk_path)) + .map(ToString::to_string) + // It's possible that a chunk also emits CSS files, that will + // be handled separatedly. + .filter(|path| path.ends_with(".js")) + // .map(RcStr::from) // BACKPORT: no RcStr + .collect::>(); + + let is_async = + is_item_async(client_availability_info, client_chunk_item).await?; + + (chunk_paths, is_async) + } else { + (Vec::new(), false) + }; - let client_chunks_paths = if let Some(client_chunks) = client_references_chunks - .client_component_client_chunks - .get(&app_client_reference_ty) - { - let client_chunks = client_chunks.await?; - let client_chunks_paths = client_chunks - .iter() - .map(|chunk| chunk.ident().path()) - .try_join() - .await?; - - client_chunks_paths - .iter() - .filter_map(|chunk_path| client_relative_path.get_path_to(chunk_path)) - .map(ToString::to_string) - // It's possible that a chunk also emits CSS files, that will - // be handled separatedly. - .filter(|path| path.ends_with(".js")) - .collect::>() - } else { - Vec::new() - }; entry_manifest.client_modules.module_exports.insert( get_client_reference_module_key(&server_path, "*"), ManifestNodeEntry { name: "*".to_string(), id: (&*client_module_id).into(), chunks: client_chunks_paths, - // TODO(WEB-434) - r#async: false, + r#async: client_is_async, }, ); if let Some(ssr_chunking_context) = ssr_chunking_context { - let ssr_module_id = ecmascript_client_reference + let ssr_chunk_item = ecmascript_client_reference .ssr_module - .as_chunk_item(Vc::upcast(ssr_chunking_context)) - .id() - .await?; + .as_chunk_item(Vc::upcast(ssr_chunking_context)); - let ssr_chunks_paths = if runtime == NextRuntime::Edge { + let ssr_module_id = ssr_chunk_item.id().await?; + + let (ssr_chunks_paths, ssr_is_async) = if runtime == NextRuntime::Edge { // the chunks get added to the middleware-manifest.json instead // of this file because the // edge runtime doesn't support dynamically // loading chunks. - Vec::new() - } else if let Some(ssr_chunks) = client_references_chunks - .client_component_ssr_chunks - .get(&app_client_reference_ty) + (Vec::new(), false) + } else if let Some((ssr_chunks, ssr_availability_info)) = + client_references_chunks + .client_component_ssr_chunks + .get(&app_client_reference_ty) { let ssr_chunks = ssr_chunks.await?; @@ -130,14 +142,20 @@ impl ClientReferenceManifest { .try_join() .await?; - ssr_chunks_paths + let chunk_paths = ssr_chunks_paths .iter() .filter_map(|chunk_path| node_root_ref.get_path_to(chunk_path)) .map(ToString::to_string) - .collect::>() + // .map(RcStr::from) // BACKPORT: no RcStr + .collect::>(); + + let is_async = is_item_async(ssr_availability_info, ssr_chunk_item).await?; + + (chunk_paths, is_async) } else { - Vec::new() + (Vec::new(), false) }; + let mut ssr_manifest_node = ManifestNode::default(); ssr_manifest_node.module_exports.insert( "*".to_string(), @@ -145,8 +163,7 @@ impl ClientReferenceManifest { name: "*".to_string(), id: (&*ssr_module_id).into(), chunks: ssr_chunks_paths, - // TODO(WEB-434) - r#async: false, + r#async: ssr_is_async, }, ); @@ -250,3 +267,18 @@ pub fn get_client_reference_module_key(server_path: &str, export_name: &str) -> format!("{}#{}", server_path, export_name) } } + +async fn is_item_async( + availability_info: &AvailabilityInfo, + chunk_item: Vc>, +) -> Result { + let Some(available_chunk_items) = availability_info.available_chunk_items() else { + return Ok(false); + }; + + let Some(info) = &*available_chunk_items.get(chunk_item).await? else { + return Ok(false); + }; + + Ok(info.is_async) +} diff --git a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs index 25485c43a2eb1..29f260b4d0af4 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs @@ -4,7 +4,7 @@ pub(crate) mod client_reference_manifest; use std::collections::HashMap; -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use serde::{Deserialize, Serialize}; use turbo_tasks::{trace::TraceRawVcs, TaskInput}; @@ -98,6 +98,7 @@ pub struct EdgeFunctionDefinition { pub assets: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub regions: Option, + pub env: IndexMap, } #[derive(Serialize, Default, Debug)] diff --git a/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs b/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs index dbdfffaedac98..4800d5b772d4d 100644 --- a/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_pages/page_entry.rs @@ -212,7 +212,6 @@ async fn wrap_edge_page( let next_config = &*next_config.await?; // TODO(WEB-1824): add build support - let build_id = "development"; let dev = true; let sri_enabled = !dev @@ -229,7 +228,6 @@ async fn wrap_edge_page( indexmap! { "VAR_USERLAND" => INNER.to_string(), "VAR_PAGE" => pathname.clone(), - "VAR_BUILD_ID" => build_id.to_string(), "VAR_MODULE_DOCUMENT" => INNER_DOCUMENT.to_string(), "VAR_MODULE_APP" => INNER_APP.to_string(), "VAR_MODULE_GLOBAL_ERROR" => INNER_ERROR.to_string(), 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 d4bd4703e92cf..129df91e6dea2 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 @@ -99,6 +99,17 @@ pub enum ServerContextType { Instrumentation, } +impl ServerContextType { + pub fn supports_react_server(&self) -> bool { + matches!( + self, + ServerContextType::AppRSC { .. } + | ServerContextType::AppRoute { .. } + | ServerContextType::PagesApi { .. } + ) + } +} + #[turbo_tasks::function] pub async fn get_server_resolve_options_context( project_path: Vc, @@ -135,14 +146,15 @@ pub async fn get_server_resolve_options_context( .cloned(), ); + let ty = ty.into_value(); + let server_component_externals_plugin = ExternalCjsModulesResolvePlugin::new( project_path, project_path.root(), ExternalPredicate::Only(Vc::cell(external_packages)).cell(), - // TODO(sokra) esmExternals support - false, + // app-ssr can't have esm externals as that would make the module async on the server only + *next_config.import_externals().await? && !matches!(ty, ServerContextType::AppSSR { .. }), ); - let ty = ty.into_value(); let mut custom_conditions = vec![mode.await?.condition().to_string()]; custom_conditions.extend( @@ -152,18 +164,10 @@ pub async fn get_server_resolve_options_context( .map(ToString::to_string), ); - match ty { - ServerContextType::AppRSC { .. } - | ServerContextType::AppRoute { .. } - | ServerContextType::PagesApi { .. } - | ServerContextType::Middleware { .. } => { - custom_conditions.push("react-server".to_string()) - } - ServerContextType::Pages { .. } - | ServerContextType::PagesData { .. } - | ServerContextType::AppSSR { .. } - | ServerContextType::Instrumentation { .. } => {} + if ty.supports_react_server() { + custom_conditions.push("react-server".to_string()); }; + let external_cjs_modules_plugin = ExternalCjsModulesResolvePlugin::new( project_path, project_path.root(), @@ -325,7 +329,7 @@ pub async fn get_server_module_options_context( let mut foreign_next_server_rules = get_next_server_transforms_rules(next_config, ty.into_value(), mode, true, next_runtime) .await?; - let internal_custom_rules = + let mut internal_custom_rules = get_next_server_internal_transforms_rules(ty.into_value(), *next_config.mdx_rs().await?) .await?; @@ -622,10 +626,16 @@ pub async fn get_server_module_options_context( ecmascript_client_reference_transition_name, } => { next_server_rules.extend(source_transform_rules); + + let mut common_next_server_rules = vec![ + get_next_react_server_components_transform_rule(next_config, true, Some(app_dir)) + .await?, + ]; + if let Some(ecmascript_client_reference_transition_name) = ecmascript_client_reference_transition_name { - next_server_rules.push(get_ecma_transform_rule( + common_next_server_rules.push(get_ecma_transform_rule( Box::new(ClientDirectiveTransformer::new( ecmascript_client_reference_transition_name, )), @@ -634,10 +644,8 @@ pub async fn get_server_module_options_context( )); } - next_server_rules.push( - get_next_react_server_components_transform_rule(next_config, true, Some(app_dir)) - .await?, - ); + next_server_rules.extend(common_next_server_rules.iter().cloned()); + internal_custom_rules.extend(common_next_server_rules); let module_options_context = ModuleOptionsContext { esm_url_rewrite_behavior: Some(UrlRewriteBehavior::Full), diff --git a/packages/next-swc/crates/next-core/src/next_shared/transforms/server_actions.rs b/packages/next-swc/crates/next-core/src/next_shared/transforms/server_actions.rs index 24f34ab87c7dd..3b9fe5d75e974 100644 --- a/packages/next-swc/crates/next-core/src/next_shared/transforms/server_actions.rs +++ b/packages/next-swc/crates/next-core/src/next_shared/transforms/server_actions.rs @@ -50,6 +50,7 @@ impl CustomTransformer for NextServerActions { Config { is_react_server_layer: matches!(self.transform, ActionsTransform::Server), enabled: true, + hash_salt: "".into(), }, ctx.comments.clone(), ); diff --git a/packages/next-swc/crates/next-custom-transforms/src/transforms/server_actions.rs b/packages/next-swc/crates/next-custom-transforms/src/transforms/server_actions.rs index d61a9a79f977b..95128587724d4 100644 --- a/packages/next-swc/crates/next-custom-transforms/src/transforms/server_actions.rs +++ b/packages/next-swc/crates/next-custom-transforms/src/transforms/server_actions.rs @@ -28,6 +28,7 @@ use turbopack_binding::swc::core::{ pub struct Config { pub is_react_server_layer: bool, pub enabled: bool, + pub hash_salt: String, } /// A mapping of hashed action id to the action's exported function name. @@ -174,8 +175,11 @@ impl ServerActions { .cloned() .map(|id| Some(id.as_arg())) .collect(), - &self.file_name, - export_name.to_string(), + generate_action_id( + &self.config.hash_salt, + &self.file_name, + export_name.to_string().as_str(), + ), ); if let BlockStmtOrExpr::BlockStmt(block) = &mut *a.body { @@ -223,7 +227,12 @@ impl ServerActions { span: DUMMY_SP, callee: quote_ident!("decryptActionBoundArgs").as_callee(), args: vec![ - generate_action_id(&self.file_name, &export_name).as_arg(), + generate_action_id( + &self.config.hash_salt, + &self.file_name, + &export_name, + ) + .as_arg(), quote_ident!("$$ACTION_CLOSURE_BOUND").as_arg(), ], type_args: None, @@ -299,8 +308,7 @@ impl ServerActions { .cloned() .map(|id| Some(id.as_arg())) .collect(), - &self.file_name, - export_name.to_string(), + generate_action_id(&self.config.hash_salt, &self.file_name, &export_name), ); f.body.visit_mut_with(&mut ClosureReplacer { @@ -347,7 +355,12 @@ impl ServerActions { span: DUMMY_SP, callee: quote_ident!("decryptActionBoundArgs").as_callee(), args: vec![ - generate_action_id(&self.file_name, &export_name).as_arg(), + generate_action_id( + &self.config.hash_salt, + &self.file_name, + &export_name, + ) + .as_arg(), quote_ident!("$$ACTION_CLOSURE_BOUND").as_arg(), ], type_args: None, @@ -437,7 +450,7 @@ impl VisitMut for ServerActions { let old_in_default_export_decl = self.in_default_export_decl; self.in_action_fn = is_action_fn; self.in_module_level = false; - self.should_track_names = true; + self.should_track_names = is_action_fn || self.should_track_names; self.in_export_decl = false; self.in_default_export_decl = false; f.visit_mut_children_with(self); @@ -448,8 +461,14 @@ impl VisitMut for ServerActions { self.in_default_export_decl = old_in_default_export_decl; } - let mut child_names = self.names.clone(); - self.names.extend(current_names); + let mut child_names = if self.should_track_names { + let names = take(&mut self.names); + self.names = current_names; + self.names.extend(names.iter().cloned()); + names + } else { + take(&mut self.names) + }; if !is_action_fn { return; @@ -510,7 +529,7 @@ impl VisitMut for ServerActions { fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) { let is_action_fn = self.get_action_info(f.function.body.as_mut(), true); - let current_declared_idents = self.declared_idents.clone(); + let declared_idents_until = self.declared_idents.len(); let current_names = take(&mut self.names); { @@ -522,7 +541,7 @@ impl VisitMut for ServerActions { let old_in_default_export_decl = self.in_default_export_decl; self.in_action_fn = is_action_fn; self.in_module_level = false; - self.should_track_names = true; + self.should_track_names = is_action_fn || self.should_track_names; self.in_export_decl = false; self.in_default_export_decl = false; f.visit_mut_children_with(self); @@ -533,8 +552,14 @@ impl VisitMut for ServerActions { self.in_default_export_decl = old_in_default_export_decl; } - let mut child_names = self.names.clone(); - self.names.extend(current_names); + let mut child_names = if self.should_track_names { + let names = take(&mut self.names); + self.names = current_names; + self.names.extend(names.iter().cloned()); + names + } else { + take(&mut self.names) + }; if !is_action_fn { return; @@ -551,7 +576,10 @@ impl VisitMut for ServerActions { if !(self.in_action_file && self.in_export_decl) { // Collect all the identifiers defined inside the closure and used // in the action function. With deduplication. - retain_names_from_declared_idents(&mut child_names, ¤t_declared_idents); + retain_names_from_declared_idents( + &mut child_names, + &self.declared_idents[..declared_idents_until], + ); let maybe_new_expr = self.maybe_hoist_and_create_proxy(child_names, Some(&mut f.function), None); @@ -616,12 +644,12 @@ impl VisitMut for ServerActions { let old_in_default_export_decl = self.in_default_export_decl; self.in_action_fn = is_action_fn; self.in_module_level = false; - self.should_track_names = true; + self.should_track_names = is_action_fn || self.should_track_names; self.in_export_decl = false; self.in_default_export_decl = false; { for n in &mut a.params { - collect_pat_idents(n, &mut self.declared_idents); + collect_idents_in_pat(n, &mut self.declared_idents); } } a.visit_mut_children_with(self); @@ -632,8 +660,14 @@ impl VisitMut for ServerActions { self.in_default_export_decl = old_in_default_export_decl; } - let mut child_names = self.names.clone(); - self.names.extend(current_names); + let mut child_names = if self.should_track_names { + let names = take(&mut self.names); + self.names = current_names; + self.names.extend(names.iter().cloned()); + names + } else { + take(&mut self.names) + }; if !is_action_fn { return; @@ -672,7 +706,7 @@ impl VisitMut for ServerActions { // If it's a closure (not in the module level), we need to collect // identifiers defined in the closure. - self.declared_idents.extend(collect_decl_idents_in_stmt(n)); + collect_decl_idents_in_stmt(n, &mut self.declared_idents); } fn visit_mut_param(&mut self, n: &mut Param) { @@ -682,7 +716,7 @@ impl VisitMut for ServerActions { return; } - collect_pat_idents(&n.pat, &mut self.declared_idents); + collect_idents_in_pat(&n.pat, &mut self.declared_idents); } fn visit_mut_prop_or_spread(&mut self, n: &mut PropOrSpread) { @@ -746,7 +780,8 @@ impl VisitMut for ServerActions { } Decl::Var(var) => { // export const foo = 1 - let ids: Vec = collect_idents_in_var_decls(&var.decls); + let mut ids: Vec = Vec::new(); + collect_idents_in_var_decls(&var.decls, &mut ids); self.exported_idents.extend( ids.into_iter().map(|id| (id.clone(), id.0.to_string())), ); @@ -937,7 +972,8 @@ impl VisitMut for ServerActions { let ident = Ident::new(id.0.clone(), DUMMY_SP.with_ctxt(id.1)); if !self.config.is_react_server_layer { - let action_id = generate_action_id(&self.file_name, export_name); + let action_id = + generate_action_id(&self.config.hash_salt, &self.file_name, export_name); if export_name == "default" { let export_expr = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr( @@ -987,8 +1023,11 @@ impl VisitMut for ServerActions { expr: Box::new(annotate_ident_as_action( ident.clone(), Vec::new(), - &self.file_name, - export_name.to_string(), + generate_action_id( + &self.config.hash_salt, + &self.file_name, + export_name, + ), )), })); } @@ -1061,7 +1100,12 @@ impl VisitMut for ServerActions { let actions = actions .into_iter() - .map(|name| (generate_action_id(&self.file_name, &name), name)) + .map(|name| { + ( + generate_action_id(&self.config.hash_salt, &self.file_name, &name), + name, + ) + }) .collect::(); // Prepend a special comment to the top of the file. self.comments.add_leading( @@ -1208,30 +1252,11 @@ fn attach_name_to_expr(ident: Ident, expr: Expr, extra_items: &mut Vec) { - match &pat { - Pat::Ident(ident) => { - closure_idents.push(ident.id.to_id()); - } - Pat::Array(array) => { - closure_idents.extend(collect_idents_in_array_pat(&array.elems)); - } - Pat::Object(object) => { - closure_idents.extend(collect_idents_in_object_pat(&object.props)); - } - Pat::Rest(rest) => { - if let Pat::Ident(ident) = &*rest.arg { - closure_idents.push(ident.id.to_id()); - } - } - _ => {} - } -} - -fn generate_action_id(file_name: &str, export_name: &str) -> String { +fn generate_action_id(hash_salt: &str, file_name: &str, export_name: &str) -> String { // Attach a checksum to the action using sha1: - // $$id = sha1('file_name' + ':' + 'export_name'); + // $$id = sha1('hash_salt' + 'file_name' + ':' + 'export_name'); let mut hasher = Sha1::new(); + hasher.update(hash_salt.as_bytes()); hasher.update(file_name.as_bytes()); hasher.update(b":"); hasher.update(export_name.as_bytes()); @@ -1243,12 +1268,10 @@ fn generate_action_id(file_name: &str, export_name: &str) -> String { fn annotate_ident_as_action( ident: Ident, bound: Vec>, - file_name: &str, - export_name: String, + action_id: String, ) -> Expr { // Add the proxy wrapper call `registerServerReference($$id, $$bound, myAction, // maybe_orig_action)`. - let action_id = generate_action_id(file_name, &export_name); let proxy_expr = Expr::Call(CallExpr { span: DUMMY_SP, @@ -1489,35 +1512,32 @@ fn remove_server_directive_index_in_fn( }); } -fn collect_idents_in_array_pat(elems: &[Option]) -> Vec { - let mut ids = Vec::new(); - +fn collect_idents_in_array_pat(elems: &[Option], ids: &mut Vec) { for elem in elems.iter().flatten() { match elem { Pat::Ident(ident) => { ids.push(ident.id.to_id()); } Pat::Array(array) => { - ids.extend(collect_idents_in_array_pat(&array.elems)); + collect_idents_in_array_pat(&array.elems, ids); } Pat::Object(object) => { - ids.extend(collect_idents_in_object_pat(&object.props)); + collect_idents_in_object_pat(&object.props, ids); } Pat::Rest(rest) => { if let Pat::Ident(ident) = &*rest.arg { ids.push(ident.id.to_id()); } } - _ => {} + Pat::Assign(AssignPat { left, .. }) => { + collect_idents_in_pat(left, ids); + } + Pat::Expr(..) | Pat::Invalid(..) => {} } } - - ids } -fn collect_idents_in_object_pat(props: &[ObjectPatProp]) -> Vec { - let mut ids = Vec::new(); - +fn collect_idents_in_object_pat(props: &[ObjectPatProp], ids: &mut Vec) { for prop in props { match prop { ObjectPatProp::KeyValue(KeyValuePatProp { key, value }) => { @@ -1530,10 +1550,10 @@ fn collect_idents_in_object_pat(props: &[ObjectPatProp]) -> Vec { ids.push(ident.id.to_id()); } Pat::Array(array) => { - ids.extend(collect_idents_in_array_pat(&array.elems)); + collect_idents_in_array_pat(&array.elems, ids); } Pat::Object(object) => { - ids.extend(collect_idents_in_object_pat(&object.props)); + collect_idents_in_object_pat(&object.props, ids); } _ => {} } @@ -1548,39 +1568,41 @@ fn collect_idents_in_object_pat(props: &[ObjectPatProp]) -> Vec { } } } - - ids } -fn collect_idents_in_var_decls(decls: &[VarDeclarator]) -> Vec { - let mut ids = Vec::new(); - +fn collect_idents_in_var_decls(decls: &[VarDeclarator], ids: &mut Vec) { for decl in decls { - match &decl.name { - Pat::Ident(ident) => { + collect_idents_in_pat(&decl.name, ids); + } +} + +fn collect_idents_in_pat(pat: &Pat, ids: &mut Vec) { + match pat { + Pat::Ident(ident) => { + ids.push(ident.id.to_id()); + } + Pat::Array(array) => { + collect_idents_in_array_pat(&array.elems, ids); + } + Pat::Object(object) => { + collect_idents_in_object_pat(&object.props, ids); + } + Pat::Assign(AssignPat { left, .. }) => { + collect_idents_in_pat(left, ids); + } + Pat::Rest(RestPat { arg, .. }) => { + if let Pat::Ident(ident) = &**arg { ids.push(ident.id.to_id()); } - Pat::Array(array) => { - ids.extend(collect_idents_in_array_pat(&array.elems)); - } - Pat::Object(object) => { - ids.extend(collect_idents_in_object_pat(&object.props)); - } - _ => {} } + Pat::Expr(..) | Pat::Invalid(..) => {} } - - ids } -fn collect_decl_idents_in_stmt(stmt: &Stmt) -> Vec { - let mut ids = Vec::new(); - +fn collect_decl_idents_in_stmt(stmt: &Stmt, ids: &mut Vec) { if let Stmt::Decl(Decl::Var(var)) = &stmt { - ids.extend(collect_idents_in_var_decls(&var.decls)); + collect_idents_in_var_decls(&var.decls, ids); } - - ids } pub(crate) struct ClosureReplacer<'a> { diff --git a/packages/next-swc/crates/next-custom-transforms/tests/errors.rs b/packages/next-swc/crates/next-custom-transforms/tests/errors.rs index be74f9fc9a6b1..34698f34ed89b 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/errors.rs +++ b/packages/next-swc/crates/next-custom-transforms/tests/errors.rs @@ -178,7 +178,8 @@ fn react_server_actions_server_errors(input: PathBuf) { &FileName::Real("/app/item.js".into()), server_actions::Config { is_react_server_layer: true, - enabled: true + enabled: true, + hash_salt: "".into() }, tr.comments.as_ref().clone(), ) @@ -214,7 +215,8 @@ fn react_server_actions_client_errors(input: PathBuf) { &FileName::Real("/app/item.js".into()), server_actions::Config { is_react_server_layer: false, - enabled: true + enabled: true, + hash_salt: "".into() }, tr.comments.as_ref().clone(), ) diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture.rs b/packages/next-swc/crates/next-custom-transforms/tests/fixture.rs index e14ed2fba985d..1851aa27bc928 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture.rs +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture.rs @@ -381,7 +381,8 @@ fn server_actions_server_fixture(input: PathBuf) { &FileName::Real("/app/item.js".into()), server_actions::Config { is_react_server_layer: true, - enabled: true + enabled: true, + hash_salt: "".into() }, _tr.comments.as_ref().clone(), ) @@ -405,7 +406,8 @@ fn server_actions_client_fixture(input: PathBuf) { &FileName::Real("/app/item.js".into()), server_actions::Config { is_react_server_layer: false, - enabled: true + enabled: true, + hash_salt: "".into() }, _tr.comments.as_ref().clone(), ) diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/28/output.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/28/output.js index 715b8857175ab..a97c8af548dee 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/28/output.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/28/output.js @@ -3,9 +3,9 @@ import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc let a, f; function Comp(b, c, ...g) { return registerServerReference("9878bfa39811ca7650992850a8751f9591b6a557", $$ACTION_2).bind(null, encryptActionBoundArgs("9878bfa39811ca7650992850a8751f9591b6a557", [ + b, c, - g, - b + g ])); } export async function $$ACTION_0($$ACTION_CLOSURE_BOUND, e) { @@ -23,19 +23,19 @@ export async function $$ACTION_2($$ACTION_CLOSURE_BOUND, d) { console.log(...window, { window }); - console.log(a, $$ACTION_ARG_2, action2); + console.log(a, $$ACTION_ARG_0, action2); var action2 = registerServerReference("6d53ce510b2e36499b8f56038817b9bad86cabb4", $$ACTION_0).bind(null, encryptActionBoundArgs("6d53ce510b2e36499b8f56038817b9bad86cabb4", [ - $$ACTION_ARG_0, + $$ACTION_ARG_1, d, f, - $$ACTION_ARG_1 + $$ACTION_ARG_2 ])); return [ action2, registerServerReference("188d5d945750dc32e2c842b93c75a65763d4a922", $$ACTION_1).bind(null, encryptActionBoundArgs("188d5d945750dc32e2c842b93c75a65763d4a922", [ action2, - $$ACTION_ARG_0, + $$ACTION_ARG_1, d ])) ]; -} \ No newline at end of file +} diff --git a/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/30/output.js b/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/30/output.js index 1a04f8c57eca6..4a041c67610b2 100644 --- a/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/30/output.js +++ b/packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/30/output.js @@ -3,9 +3,9 @@ import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc let a, f; export async function action0(b, c, ...g) { return registerServerReference("9878bfa39811ca7650992850a8751f9591b6a557", $$ACTION_2).bind(null, encryptActionBoundArgs("9878bfa39811ca7650992850a8751f9591b6a557", [ + b, c, - g, - b + g ])); } export async function $$ACTION_0($$ACTION_CLOSURE_BOUND, e) { @@ -23,18 +23,18 @@ export async function $$ACTION_2($$ACTION_CLOSURE_BOUND, d) { console.log(...window, { window }); - console.log(a, $$ACTION_ARG_2, action2); + console.log(a, $$ACTION_ARG_0, action2); var action2 = registerServerReference("6d53ce510b2e36499b8f56038817b9bad86cabb4", $$ACTION_0).bind(null, encryptActionBoundArgs("6d53ce510b2e36499b8f56038817b9bad86cabb4", [ - $$ACTION_ARG_0, + $$ACTION_ARG_1, d, f, - $$ACTION_ARG_1 + $$ACTION_ARG_2 ])); return [ action2, registerServerReference("188d5d945750dc32e2c842b93c75a65763d4a922", $$ACTION_1).bind(null, encryptActionBoundArgs("188d5d945750dc32e2c842b93c75a65763d4a922", [ action2, - $$ACTION_ARG_0, + $$ACTION_ARG_1, d ])) ]; diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 97c998e239377..2571b331c8afa 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "14.2.7", + "version": "14.2.12", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 366aa3b9f561e..bea11f0056e02 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "14.2.7", + "version": "14.2.12", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "14.2.7", + "@next/env": "14.2.12", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -149,10 +149,10 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/polyfill-module": "14.2.7", - "@next/polyfill-nomodule": "14.2.7", - "@next/react-refresh-utils": "14.2.7", - "@next/swc": "14.2.7", + "@next/polyfill-module": "14.2.12", + "@next/polyfill-nomodule": "14.2.12", + "@next/react-refresh-utils": "14.2.12", + "@next/swc": "14.2.12", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@taskr/clear": "1.1.0", diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 62c6040c3cde9..8ba069cda5a28 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -406,7 +406,6 @@ export function getEdgeServerEntry(opts: { absoluteDocumentPath: opts.pages['/_document'], absoluteErrorPath: opts.pages['/_error'], absolutePagePath: opts.absolutePagePath, - buildId: opts.buildId, dev: opts.isDev, isServerComponent: opts.isServerComponent, page: opts.page, diff --git a/packages/next/src/build/handle-externals.ts b/packages/next/src/build/handle-externals.ts index b77d1f848f5dc..95339719abbe8 100644 --- a/packages/next/src/build/handle-externals.ts +++ b/packages/next/src/build/handle-externals.ts @@ -1,5 +1,6 @@ -import { WEBPACK_LAYERS } from '../lib/constants' import type { WebpackLayerName } from '../lib/constants' +import type { NextConfigComplete } from '../server/config-shared' +import type { ResolveOptions } from 'webpack' import { defaultOverrides } from '../server/require-hook' import { BARREL_OPTIMIZATION_PREFIX } from '../shared/lib/constants' import path from '../shared/lib/isomorphic/path' @@ -10,7 +11,6 @@ import { NODE_RESOLVE_OPTIONS, } from './webpack-config' import { isWebpackAppLayer, isWebpackServerOnlyLayer } from './utils' -import type { NextConfigComplete } from '../server/config-shared' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' const reactPackagesRegex = /^(react|react-dom|react-server-dom-webpack)($|\/)/ @@ -25,15 +25,6 @@ const externalPattern = new RegExp( const nodeModulesRegex = /node_modules[/\\].*\.[mc]?js$/ -function containsImportInPackages( - request: string, - packages: string[] -): boolean { - return packages.some( - (pkg) => request === pkg || request.startsWith(pkg + '/') - ) -} - export function isResourceInPackages( resource: string, packageNames?: string[], @@ -57,9 +48,9 @@ export async function resolveExternal( context: string, request: string, isEsmRequested: boolean, - optOutBundlingPackages: string[], + _optOutBundlingPackages: string[], getResolve: ( - options: any + options: ResolveOptions ) => ( resolveContext: string, resolveRequest: string @@ -78,19 +69,12 @@ export async function resolveExternal( let isEsm: boolean = false const preferEsmOptions = - esmExternals && - isEsmRequested && - // For package that marked as externals that should be not bundled, - // we don't resolve them as ESM since it could be resolved as async module, - // such as `import(external package)` in the bundle, valued as a `Promise`. - !containsImportInPackages(request, optOutBundlingPackages) - ? [true, false] - : [false] + esmExternals && isEsmRequested ? [true, false] : [false] for (const preferEsm of preferEsmOptions) { - const resolve = getResolve( - preferEsm ? esmResolveOptions : nodeResolveOptions - ) + const resolveOptions = preferEsm ? esmResolveOptions : nodeResolveOptions + + const resolve = getResolve(resolveOptions) // Resolve the import with the webpack provided context, this // ensures we're resolving the correct version when multiple @@ -273,23 +257,6 @@ export function makeExternalHandler({ return resolveNextExternal(request) } - // Early return if the request needs to be bundled, such as in the client layer. - // Treat react packages and next internals as external for SSR layer, - // also map react to builtin ones with require-hook. - // Otherwise keep continue the process to resolve the externals. - if (layer === WEBPACK_LAYERS.serverSideRendering) { - const isRelative = request.startsWith('.') - const fullRequest = isRelative - ? normalizePathSep(path.join(context, request)) - : request - - // Check if it's opt out bundling package first - if (containsImportInPackages(fullRequest, optOutBundlingPackages)) { - return fullRequest - } - return resolveNextExternal(fullRequest) - } - // TODO-APP: Let's avoid this resolve call as much as possible, and eventually get rid of it. const resolveResult = await resolveExternal( dir, @@ -320,6 +287,13 @@ export function makeExternalHandler({ return } + const isOptOutBundling = optOutBundlingPackageRegex.test(res) + // Apply bundling rules to all app layers. + // Since handleExternals only handle the server layers, we don't need to exclude client here + if (!isOptOutBundling && isAppLayer) { + return + } + // ESM externals can only be imported (and not required). // Make an exception in loose mode. if (!isEsmRequested && isEsm && !looseEsmExternals && !isLocal) { @@ -370,13 +344,11 @@ export function makeExternalHandler({ const resolvedBundlingOptOutRes = resolveBundlingOptOutPackages({ resolvedRes: res, - optOutBundlingPackageRegex, config, resolvedExternalPackageDirs, - isEsm, isAppLayer, - layer, externalType, + isOptOutBundling, request, }) if (resolvedBundlingOptOutRes) { @@ -390,41 +362,34 @@ export function makeExternalHandler({ function resolveBundlingOptOutPackages({ resolvedRes, - optOutBundlingPackageRegex, config, resolvedExternalPackageDirs, - isEsm, isAppLayer, - layer, externalType, + isOptOutBundling, request, }: { resolvedRes: string - optOutBundlingPackageRegex: RegExp config: NextConfigComplete resolvedExternalPackageDirs: Map - isEsm: boolean isAppLayer: boolean - layer: WebpackLayerName | null externalType: string + isOptOutBundling: boolean request: string }) { - const shouldBeBundled = - isResourceInPackages( - resolvedRes, - config.transpilePackages, - resolvedExternalPackageDirs - ) || - (isEsm && isAppLayer) || - (!isAppLayer && config.experimental.bundlePagesExternals) - if (nodeModulesRegex.test(resolvedRes)) { - const isOptOutBundling = optOutBundlingPackageRegex.test(resolvedRes) - if (isWebpackServerOnlyLayer(layer)) { - if (isOptOutBundling) { - return `${externalType} ${request}` // Externalize if opted out - } - } else if (!shouldBeBundled || isOptOutBundling) { + const shouldBundlePages = + !isAppLayer && + config.experimental.bundlePagesExternals && + !isOptOutBundling + const shouldBeBundled = + shouldBundlePages || + isResourceInPackages( + resolvedRes, + config.transpilePackages, + resolvedExternalPackageDirs + ) + if (!shouldBeBundled) { return `${externalType} ${request}` // Externalize if not bundled or opted out } } diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 6d3ba36007350..298e3510b0faa 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -185,6 +185,7 @@ import { buildCustomRoute } from '../lib/build-custom-route' import { createProgress } from './progress' import { traceMemoryUsage } from '../lib/memory/trace' import { generateEncryptionKeyBase64 } from '../server/app-render/encryption-utils' +import type { DeepReadonly } from '../shared/lib/deep-readonly' interface ExperimentalBypassForInfo { experimentalBypassFor?: RouteHas[] @@ -337,37 +338,13 @@ async function readManifest(filePath: string): Promise { async function writePrerenderManifest( distDir: string, - manifest: Readonly + manifest: DeepReadonly ): Promise { await writeManifest(path.join(distDir, PRERENDER_MANIFEST), manifest) - await writeEdgePartialPrerenderManifest(distDir, manifest) -} - -async function writeEdgePartialPrerenderManifest( - distDir: string, - manifest: Readonly> -): Promise { - // We need to write a partial prerender manifest to make preview mode settings available in edge middleware. - // Use env vars in JS bundle and inject the actual vars to edge manifest. - const edgePartialPrerenderManifest: Partial = { - ...manifest, - preview: { - previewModeId: 'process.env.__NEXT_PREVIEW_MODE_ID', - previewModeSigningKey: 'process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY', - previewModeEncryptionKey: - 'process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY', - }, - } - await writeFileUtf8( - path.join(distDir, PRERENDER_MANIFEST.replace(/\.json$/, '.js')), - `self.__PRERENDER_MANIFEST=${JSON.stringify( - JSON.stringify(edgePartialPrerenderManifest) - )}` - ) } async function writeClientSsgManifest( - prerenderManifest: PrerenderManifest, + prerenderManifest: DeepReadonly, { buildId, distDir, @@ -1240,8 +1217,6 @@ export default async function build( .traceChild('write-routes-manifest') .traceAsyncFn(() => writeManifest(routesManifestPath, routesManifest)) - await writeEdgePartialPrerenderManifest(distDir, {}) - const outputFileTracingRoot = config.experimental.outputFileTracingRoot || dir @@ -1284,7 +1259,6 @@ export default async function build( path.relative(distDir, pagesManifestPath), BUILD_MANIFEST, PRERENDER_MANIFEST, - PRERENDER_MANIFEST.replace(/\.json$/, '.js'), path.join(SERVER_DIRECTORY, MIDDLEWARE_MANIFEST), path.join(SERVER_DIRECTORY, MIDDLEWARE_BUILD_MANIFEST + '.js'), path.join( @@ -1384,6 +1358,9 @@ export default async function build( // TODO: Implement middlewareMatchers: undefined, }), + buildId: NextBuildContext.buildId!, + encryptionKey: NextBuildContext.encryptionKey!, + previewProps: NextBuildContext.previewProps!, }) await fs.mkdir(path.join(distDir, 'server'), { recursive: true }) @@ -2757,7 +2734,8 @@ export default async function build( }, ] - routes.forEach((route) => { + // Always sort the routes to get consistent output in manifests + getSortedRoutes(routes).forEach((route) => { if (isDynamicRoute(page) && route === page) return if (route === UNDERSCORE_NOT_FOUND_ROUTE) return @@ -2813,6 +2791,10 @@ export default async function build( // normalize header values as initialHeaders // must be Record for (const key of headerKeys) { + // set-cookie is already handled - the middleware cookie setting case + // isn't needed for the prerender manifest since it can't read cookies + if (key === 'x-middleware-set-cookie') continue + let value = exportHeaders[key] if (Array.isArray(value)) { @@ -3319,7 +3301,7 @@ export default async function build( NextBuildContext.allowedRevalidateHeaderKeys = config.experimental.allowedRevalidateHeaderKeys - const prerenderManifest: Readonly = { + const prerenderManifest: DeepReadonly = { version: 4, routes: finalPrerenderRoutes, dynamicRoutes: finalDynamicRoutes, diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 627433adecf9e..b7c0f6f14ea16 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -19,6 +19,7 @@ import { isDeepStrictEqual } from 'util' import type { DefineEnvPluginOptions } from '../webpack/plugins/define-env-plugin' import { getDefineEnv } from '../webpack/plugins/define-env-plugin' import type { PageExtensions } from '../page-extensions-type' +import type { __ApiPreviewProps } from '../../server/api-utils' const nextVersion = process.env.__NEXT_VERSION as string @@ -384,7 +385,6 @@ function logLoadFailure(attempts: any, triedWasm = false) { process.exit(1) }) } - export interface ProjectOptions { /** * A root path from which all files must be nested under. Trying to access @@ -429,6 +429,21 @@ export interface ProjectOptions { * The mode in which Next.js is running. */ dev: boolean + + /** + * The server actions encryption key. + */ + encryptionKey: string + + /** + * The build id. + */ + buildId: string + + /** + * Options for draft mode. + */ + previewProps: __ApiPreviewProps } type RustifiedEnv = { name: string; value: string }[] @@ -1329,12 +1344,24 @@ function loadNative(importPath?: string) { let bindings: any let attempts: any[] = [] + const NEXT_TEST_NATIVE_DIR = process.env.NEXT_TEST_NATIVE_DIR for (const triple of triples) { - try { - bindings = require(`@next/swc/native/next-swc.${triple.platformArchABI}.node`) - infoLog('next-swc build: local built @next/swc') - break - } catch (e) {} + if (NEXT_TEST_NATIVE_DIR) { + try { + // Use the binary directly to skip `pnpm pack` for testing as it's slow because of the large native binary. + bindings = require(`${NEXT_TEST_NATIVE_DIR}/next-swc.${triple.platformArchABI}.node`) + console.log( + 'next-swc build: local built @next/swc from NEXT_TEST_NATIVE_DIR' + ) + break + } catch (e) {} + } else { + try { + bindings = require(`@next/swc/native/next-swc.${triple.platformArchABI}.node`) + console.log('next-swc build: local built @next/swc') + break + } catch (e) {} + } } if (!bindings) { diff --git a/packages/next/src/build/swc/options.ts b/packages/next/src/build/swc/options.ts index 3ea92e5bbdf9c..bb999f96621b9 100644 --- a/packages/next/src/build/swc/options.ts +++ b/packages/next/src/build/swc/options.ts @@ -206,6 +206,7 @@ function getBaseSWCOptions({ // TODO: remove this option enabled: true, isReactServerLayer, + hashSalt: '', } : undefined, // For app router we prefer to bundle ESM, diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index 4d0de74d437f3..b9bde515d3851 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -37,7 +37,6 @@ declare const nextConfig: NextConfigComplete const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined) const buildManifest: BuildManifest = self.__BUILD_MANIFEST as any -const prerenderManifest = maybeJSONParse(self.__PRERENDER_MANIFEST) const reactLoadableManifest = maybeJSONParse(self.__REACT_LOADABLE_MANIFEST) const rscManifest = self.__RSC_MANIFEST?.['VAR_PAGE'] const rscServerManifest = maybeJSONParse(self.__RSC_SERVER_MANIFEST) @@ -70,7 +69,6 @@ const render = getRender({ error500Mod, Document, buildManifest, - prerenderManifest, renderToHTML, reactLoadableManifest, clientReferenceManifest: isServerComponent ? rscManifest : null, @@ -78,7 +76,7 @@ const render = getRender({ serverActions: isServerComponent ? serverActions : undefined, subresourceIntegrityManifest, config: nextConfig, - buildId: 'VAR_BUILD_ID', + buildId: process.env.__NEXT_BUILD_ID!, nextFontManifest, incrementalCacheHandler, interceptionRouteRewrites, diff --git a/packages/next/src/build/templates/edge-ssr.ts b/packages/next/src/build/templates/edge-ssr.ts index d5611928ab102..3cc6ccaee03d9 100644 --- a/packages/next/src/build/templates/edge-ssr.ts +++ b/packages/next/src/build/templates/edge-ssr.ts @@ -82,7 +82,6 @@ const error500Mod = userland500Page const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined) const buildManifest: BuildManifest = self.__BUILD_MANIFEST as any -const prerenderManifest = maybeJSONParse(self.__PRERENDER_MANIFEST) const reactLoadableManifest = maybeJSONParse(self.__REACT_LOADABLE_MANIFEST) const subresourceIntegrityManifest = sriEnabled ? maybeJSONParse(self.__SUBRESOURCE_INTEGRITY_MANIFEST) @@ -99,12 +98,11 @@ const render = getRender({ error500Mod, Document, buildManifest, - prerenderManifest, renderToHTML, reactLoadableManifest, subresourceIntegrityManifest, config: nextConfig, - buildId: 'VAR_BUILD_ID', + buildId: process.env.__NEXT_BUILD_ID!, nextFontManifest, incrementalCacheHandler, }) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 6e1c9c06bc6bf..3ad1e6173062a 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1364,7 +1364,7 @@ export async function buildAppStaticPaths({ renderOpts: { originalPathname: page, incrementalCache, - supportsDynamicHTML: true, + supportsDynamicResponse: true, isRevalidate: false, // building static paths should never postpone experimental: { ppr: false }, diff --git a/packages/next/src/build/webpack-build/impl.ts b/packages/next/src/build/webpack-build/impl.ts index 452060269102c..39ff87a40a0b4 100644 --- a/packages/next/src/build/webpack-build/impl.ts +++ b/packages/next/src/build/webpack-build/impl.ts @@ -152,7 +152,14 @@ export async function webpackBuildImpl( middlewareMatchers: entrypoints.middlewareMatchers, compilerType: COMPILER_NAMES.edgeServer, entrypoints: entrypoints.edgeServer, - edgePreviewProps: NextBuildContext.previewProps!, + edgePreviewProps: { + __NEXT_PREVIEW_MODE_ID: + NextBuildContext.previewProps!.previewModeId, + __NEXT_PREVIEW_MODE_ENCRYPTION_KEY: + NextBuildContext.previewProps!.previewModeEncryptionKey, + __NEXT_PREVIEW_MODE_SIGNING_KEY: + NextBuildContext.previewProps!.previewModeSigningKey, + }, ...info, }), ]) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 24b72f045bfdb..fc1105a5a79f1 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1812,7 +1812,11 @@ export default async function getBaseWebpackConfig( dev, sriEnabled: !dev && !!config.experimental.sri?.algorithm, rewrites, - edgeEnvironments: edgePreviewProps || {}, + edgeEnvironments: { + __NEXT_BUILD_ID: buildId, + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: encryptionKey, + ...edgePreviewProps, + }, }), isClient && new BuildManifestPlugin({ 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 e0f0728e8eaad..dbddccbe1a99f 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 @@ -16,7 +16,6 @@ export type EdgeSSRLoaderQuery = { absoluteDocumentPath: string absoluteErrorPath: string absolutePagePath: string - buildId: string dev: boolean isServerComponent: boolean page: string @@ -65,7 +64,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = const { dev, page, - buildId, absolutePagePath, absoluteAppPath, absoluteDocumentPath, @@ -145,7 +143,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = { VAR_USERLAND: pageModPath, VAR_PAGE: page, - VAR_BUILD_ID: buildId, }, { sriEnabled: JSON.stringify(sriEnabled), @@ -167,7 +164,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = { VAR_USERLAND: pageModPath, VAR_PAGE: page, - VAR_BUILD_ID: buildId, VAR_MODULE_DOCUMENT: documentPath, VAR_MODULE_APP: appPath, VAR_MODULE_GLOBAL_ERROR: errorPath, 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 8f6f1d71415a1..a475c36eeacce 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 @@ -13,7 +13,7 @@ import { WebNextResponse, } from '../../../../server/base-http/web' import { SERVER_RUNTIME } from '../../../../lib/constants' -import type { ManifestRewriteRoute, PrerenderManifest } from '../../..' +import type { ManifestRewriteRoute } from '../../..' import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths' import type { SizeLimit } from '../../../../../types' import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until' @@ -30,7 +30,6 @@ export function getRender({ pagesType, Document, buildManifest, - prerenderManifest, reactLoadableManifest, interceptionRouteRewrites, renderToHTML, @@ -53,7 +52,6 @@ export function getRender({ renderToHTML?: any Document: DocumentType buildManifest: BuildManifest - prerenderManifest: PrerenderManifest reactLoadableManifest: ReactLoadableManifest subresourceIntegrityManifest?: Record interceptionRouteRewrites?: ManifestRewriteRoute[] @@ -87,12 +85,11 @@ export function getRender({ page, pathname: isAppPath ? normalizeAppPath(page) : page, pagesType, - prerenderManifest, interceptionRouteRewrites, extendRenderOpts: { buildId, runtime: SERVER_RUNTIME.experimentalEdge, - supportsDynamicHTML: true, + supportsDynamicResponse: true, disableOptimizedLoading: true, serverActionsManifest, serverActions, 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 d8481d235d70f..79a0ae48ef6ea 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 @@ -99,14 +99,6 @@ export default function transformSource( let esmSource = `\ import { createProxy } from "${MODULE_PROXY_PATH}" -const proxy = createProxy(String.raw\`${resourceKey}\`) - -// Accessing the __esModule property and exporting $$typeof are required here. -// The __esModule getter forces the proxy target to create the default export -// and the $$typeof value is for rendering logic to determine if the module -// is a client boundary. -const { __esModule, $$typeof } = proxy; -const __default__ = proxy.default; ` let cnt = 0 for (const ref of clientRefs) { @@ -114,7 +106,6 @@ const __default__ = proxy.default; esmSource += `\nexports[''] = createProxy(String.raw\`${resourceKey}#\`);` } else if (ref === 'default') { esmSource += `\ -export { __esModule, $$typeof }; export default createProxy(String.raw\`${resourceKey}#default\`); ` } else { diff --git a/packages/next/src/build/webpack/loaders/utils.ts b/packages/next/src/build/webpack/loaders/utils.ts index ed43e1a387c47..8ff7185449688 100644 --- a/packages/next/src/build/webpack/loaders/utils.ts +++ b/packages/next/src/build/webpack/loaders/utils.ts @@ -5,14 +5,28 @@ import { RSC_MODULE_TYPES } from '../../../shared/lib/constants' const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif', 'ico', 'svg'] const imageRegex = new RegExp(`\\.(${imageExtensions.join('|')})$`) +// Determine if the whole module is server action, 'use server' in the top level of module +export function isActionServerLayerEntryModule(mod: { + resource: string + buildInfo?: any +}) { + const rscInfo = mod.buildInfo.rsc + return !!(rscInfo?.actions && rscInfo?.type === RSC_MODULE_TYPES.server) +} + +// Determine if the whole module is client action, 'use server' in nested closure in the client module +function isActionClientLayerModule(mod: { resource: string; buildInfo?: any }) { + const rscInfo = mod.buildInfo.rsc + return !!(rscInfo?.actions && rscInfo?.type === RSC_MODULE_TYPES.client) +} + export function isClientComponentEntryModule(mod: { resource: string buildInfo?: any }) { const rscInfo = mod.buildInfo.rsc const hasClientDirective = rscInfo?.isClientRef - const isActionLayerEntry = - rscInfo?.actions && rscInfo?.type === RSC_MODULE_TYPES.client + const isActionLayerEntry = isActionClientLayerModule(mod) return ( hasClientDirective || isActionLayerEntry || imageRegex.test(mod.resource) ) @@ -39,7 +53,7 @@ export function isCSSMod(mod: { ) } -export function getActions(mod: { +export function getActionsFromBuildInfo(mod: { resource: string buildInfo?: any }): undefined | string[] { 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 9b9c3006d9a56..07927e7b90fa8 100644 --- a/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/build-manifest-plugin.ts @@ -29,6 +29,38 @@ export type ClientBuildManifest = { // generated). export const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()` +// nodejs: '/static//low-priority.js' +function buildNodejsLowPriorityPath(filename: string, buildId: string) { + return `${CLIENT_STATIC_FILES_PATH}/${buildId}/${filename}` +} + +function createEdgeRuntimeManifest(originAssetMap: BuildManifest): string { + const manifestFilenames = ['_buildManifest.js', '_ssgManifest.js'] + + const assetMap: BuildManifest = { + ...originAssetMap, + lowPriorityFiles: [], + } + + const manifestDefCode = `self.__BUILD_MANIFEST = ${JSON.stringify( + assetMap, + null, + 2 + )};\n` + // edge lowPriorityFiles item: '"/static/" + process.env.__NEXT_BUILD_ID + "/low-priority.js"'. + // Since lowPriorityFiles is not fixed and relying on `process.env.__NEXT_BUILD_ID`, we'll produce code creating it dynamically. + const lowPriorityFilesCode = + `self.__BUILD_MANIFEST.lowPriorityFiles = [\n` + + manifestFilenames + .map((filename) => { + return `"/static/" + process.env.__NEXT_BUILD_ID + "/${filename}",\n` + }) + .join(',') + + `\n];` + + return manifestDefCode + lowPriorityFilesCode +} + function normalizeRewrite(item: { source: string destination: string @@ -234,19 +266,25 @@ export default class BuildManifestPlugin { // Add the runtime build manifest file (generated later in this file) // as a dependency for the app. If the flag is false, the file won't be // downloaded by the client. - assetMap.lowPriorityFiles.push( - `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` + const buildManifestPath = buildNodejsLowPriorityPath( + '_buildManifest.js', + this.buildId ) - const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js` - - assetMap.lowPriorityFiles.push(ssgManifestPath) + const ssgManifestPath = buildNodejsLowPriorityPath( + '_ssgManifest.js', + this.buildId + ) + assetMap.lowPriorityFiles.push(buildManifestPath, ssgManifestPath) assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest) } assetMap.pages = Object.keys(assetMap.pages) .sort() - // eslint-disable-next-line - .reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any) + .reduce( + // eslint-disable-next-line + (a, c) => ((a[c] = assetMap.pages[c]), a), + {} as typeof assetMap.pages + ) let buildManifestName = BUILD_MANIFEST @@ -258,12 +296,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( + `${createEdgeRuntimeManifest(assetMap)}` + ) if (!this.isDevFallback) { const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` 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 9b0f6ef7f24d6..c539db3baf206 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 @@ -25,7 +25,7 @@ import { UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, } from '../../../shared/lib/constants' import { - getActions, + getActionsFromBuildInfo, generateActionId, isClientComponentEntryModule, isCSSMod, @@ -371,10 +371,10 @@ export class FlightClientEntryPlugin { ...clientEntryToInject.clientComponentImports, ...( dedupedCSSImports[clientEntryToInject.absolutePagePath] || [] - ).reduce((res, curr) => { + ).reduce((res, curr) => { res[curr] = new Set() return res - }, {} as ClientComponentImports), + }, {}), }, }) @@ -430,75 +430,6 @@ export class FlightClientEntryPlugin { ) } - compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async () => { - const addedClientActionEntryList: Promise[] = [] - const actionMapsPerClientEntry: Record> = {} - - // We need to create extra action entries that are created from the - // client layer. - // Start from each entry's created SSR dependency from our previous step. - for (const [name, ssrEntryDependencies] of Object.entries( - createdSSRDependenciesForEntry - )) { - // Collect from all entries, e.g. layout.js, page.js, loading.js, ... - // add aggregate them. - const actionEntryImports = this.collectClientActionsFromDependencies({ - compilation, - dependencies: ssrEntryDependencies, - }) - - if (actionEntryImports.size > 0) { - if (!actionMapsPerClientEntry[name]) { - actionMapsPerClientEntry[name] = new Map() - } - actionMapsPerClientEntry[name] = new Map([ - ...actionMapsPerClientEntry[name], - ...actionEntryImports, - ]) - } - } - - for (const [name, actionEntryImports] of Object.entries( - actionMapsPerClientEntry - )) { - // If an action method is already created in the server layer, we don't - // need to create it again in the action layer. - // This is to avoid duplicate action instances and make sure the module - // state is shared. - let remainingClientImportedActions = false - const remainingActionEntryImports = new Map() - for (const [dep, actionNames] of actionEntryImports) { - const remainingActionNames = [] - for (const actionName of actionNames) { - const id = name + '@' + dep + '@' + actionName - if (!createdActions.has(id)) { - remainingActionNames.push(actionName) - } - } - if (remainingActionNames.length > 0) { - remainingActionEntryImports.set(dep, remainingActionNames) - remainingClientImportedActions = true - } - } - - if (remainingClientImportedActions) { - addedClientActionEntryList.push( - this.injectActionEntry({ - compiler, - compilation, - actions: remainingActionEntryImports, - entryName: name, - bundlePath: name, - fromClient: true, - }) - ) - } - } - - await Promise.all(addedClientActionEntryList) - return - }) - // Invalidate in development to trigger recompilation const invalidator = getInvalidator(compiler.outputPath) // Check if any of the entry injections need an invalidation @@ -521,6 +452,72 @@ export class FlightClientEntryPlugin { // Wait for action entries to be added. await Promise.all(addActionEntryList) + + const addedClientActionEntryList: Promise[] = [] + const actionMapsPerClientEntry: Record> = {} + + // We need to create extra action entries that are created from the + // client layer. + // Start from each entry's created SSR dependency from our previous step. + for (const [name, ssrEntryDependencies] of Object.entries( + createdSSRDependenciesForEntry + )) { + // Collect from all entries, e.g. layout.js, page.js, loading.js, ... + // add aggregate them. + const actionEntryImports = this.collectClientActionsFromDependencies({ + compilation, + dependencies: ssrEntryDependencies, + }) + + if (actionEntryImports.size > 0) { + if (!actionMapsPerClientEntry[name]) { + actionMapsPerClientEntry[name] = new Map() + } + actionMapsPerClientEntry[name] = new Map([ + ...actionMapsPerClientEntry[name], + ...actionEntryImports, + ]) + } + } + + for (const [name, actionEntryImports] of Object.entries( + actionMapsPerClientEntry + )) { + // If an action method is already created in the server layer, we don't + // need to create it again in the action layer. + // This is to avoid duplicate action instances and make sure the module + // state is shared. + let remainingClientImportedActions = false + const remainingActionEntryImports = new Map() + for (const [dep, actionNames] of actionEntryImports) { + const remainingActionNames = [] + for (const actionName of actionNames) { + const id = name + '@' + dep + '@' + actionName + if (!createdActions.has(id)) { + remainingActionNames.push(actionName) + } + } + if (remainingActionNames.length > 0) { + remainingActionEntryImports.set(dep, remainingActionNames) + remainingClientImportedActions = true + } + } + + if (remainingClientImportedActions) { + addedClientActionEntryList.push( + this.injectActionEntry({ + compiler, + compilation, + actions: remainingActionEntryImports, + entryName: name, + bundlePath: name, + fromClient: true, + }) + ) + } + } + + await Promise.all(addedClientActionEntryList) } collectClientActionsFromDependencies({ @@ -547,27 +544,14 @@ export class FlightClientEntryPlugin { const collectActionsInDep = (mod: webpack.NormalModule): void => { if (!mod) return - const modPath: string = mod.resourceResolveData?.path || '' - // We have to always use the resolved request here to make sure the - // server and client are using the same module path (required by RSC), as - // the server compiler and client compiler have different resolve configs. - let modRequest: string = - modPath + (mod.resourceResolveData?.query || '') - - // For the barrel optimization, we need to use the match resource instead - // because there will be 2 modules for the same file (same resource path) - // but they're different modules and can't be deduped via `visitedModule`. - // The first module is a virtual re-export module created by the loader. - if (mod.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) { - modRequest = mod.matchResource + ':' + modRequest - } + const modResource = getModuleResource(mod) - if (!modRequest || visitedModule.has(modRequest)) return - visitedModule.add(modRequest) + if (!modResource || visitedModule.has(modResource)) return + visitedModule.add(modResource) - const actions = getActions(mod) + const actions = getActionsFromBuildInfo(mod) if (actions) { - collectedActions.set(modRequest, actions) + collectedActions.set(modResource, actions) } getModuleReferencesInOrder(mod, compilation.moduleGraph).forEach( @@ -596,8 +580,8 @@ export class FlightClientEntryPlugin { ssrEntryModule, compilation.moduleGraph )) { - const dependency = connection.dependency! - const request = (dependency as unknown as webpack.NormalModule).request + const depModule = connection.dependency + const request = (depModule as unknown as webpack.NormalModule).request // It is possible that the same entry is added multiple times in the // connection graph. We can just skip these to speed up the process. @@ -642,33 +626,14 @@ export class FlightClientEntryPlugin { if (!mod) return const isCSS = isCSSMod(mod) + const modResource = getModuleResource(mod) - const modPath: string = mod.resourceResolveData?.path || '' - const modQuery = mod.resourceResolveData?.query || '' - // We have to always use the resolved request here to make sure the - // server and client are using the same module path (required by RSC), as - // the server compiler and client compiler have different resolve configs. - let modRequest: string = modPath + modQuery - - // Context modules don't have a resource path, we use the identifier instead. - if (mod.constructor.name === 'ContextModule') { - modRequest = (mod as any)._identifier - } - - // For the barrel optimization, we need to use the match resource instead - // because there will be 2 modules for the same file (same resource path) - // but they're different modules and can't be deduped via `visitedModule`. - // The first module is a virtual re-export module created by the loader. - if (mod.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) { - modRequest = mod.matchResource + ':' + modRequest - } - - if (!modRequest) return - if (visited.has(modRequest)) { - if (clientComponentImports[modRequest]) { + if (!modResource) return + if (visited.has(modResource)) { + if (clientComponentImports[modResource]) { addClientImport( mod, - modRequest, + modResource, clientComponentImports, importedIdentifiers, false @@ -676,11 +641,11 @@ export class FlightClientEntryPlugin { } return } - visited.add(modRequest) + visited.add(modResource) - const actions = getActions(mod) + const actions = getActionsFromBuildInfo(mod) if (actions) { - actionImports.push([modRequest, actions]) + actionImports.push([modResource, actions]) } const webpackRuntime = this.isEdgeServer @@ -699,14 +664,14 @@ export class FlightClientEntryPlugin { if (unused) return } - CSSImports.add(modRequest) + CSSImports.add(modResource) } else if (isClientComponentEntryModule(mod)) { - if (!clientComponentImports[modRequest]) { - clientComponentImports[modRequest] = new Set() + if (!clientComponentImports[modResource]) { + clientComponentImports[modResource] = new Set() } addClientImport( mod, - modRequest, + modResource, clientComponentImports, importedIdentifiers, true @@ -718,7 +683,6 @@ export class FlightClientEntryPlugin { getModuleReferencesInOrder(mod, compilation.moduleGraph).forEach( (connection: any) => { let dependencyIds: string[] = [] - const depModule = connection.resolvedModule // `ids` are the identifiers that are imported from the dependency, // if it's present, it's an array of strings. @@ -728,7 +692,7 @@ export class FlightClientEntryPlugin { dependencyIds = ['*'] } - filterClientComponents(depModule, dependencyIds) + filterClientComponents(connection.resolvedModule, dependencyIds) } ) } @@ -1016,19 +980,26 @@ export class FlightClientEntryPlugin { edgeServerActions[id] = action } - const json = JSON.stringify( - { - node: serverActions, - edge: edgeServerActions, - encryptionKey: this.encryptionKey, - }, + const serverManifest = { + node: serverActions, + edge: edgeServerActions, + encryptionKey: this.encryptionKey, + } + const edgeServerManifest = { + ...serverManifest, + encryptionKey: 'process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY', + } + + const json = JSON.stringify(serverManifest, null, this.dev ? 2 : undefined) + const edgeJson = JSON.stringify( + edgeServerManifest, null, this.dev ? 2 : undefined ) assets[`${this.assetPrefix}${SERVER_REFERENCE_MANIFEST}.js`] = new sources.RawSource( - `self.__RSC_SERVER_MANIFEST=${JSON.stringify(json)}` + `self.__RSC_SERVER_MANIFEST=${JSON.stringify(edgeJson)}` ) as unknown as webpack.sources.RawSource assets[`${this.assetPrefix}${SERVER_REFERENCE_MANIFEST}.json`] = new sources.RawSource(json) as unknown as webpack.sources.RawSource @@ -1040,7 +1011,7 @@ function addClientImport( modRequest: string, clientComponentImports: ClientComponentImports, importedIdentifiers: string[], - isFirstImport: boolean + isFirstVisitModule: boolean ) { const clientEntryType = getModuleBuildInfo(mod).rsc?.clientEntryType const isCjsModule = clientEntryType === 'cjs' @@ -1055,7 +1026,7 @@ function addClientImport( // If there's collected import path with named import identifiers, // or there's nothing in collected imports are empty. // we should include the whole module. - if (!isFirstImport && [...clientImportsSet][0] !== '*') { + if (!isFirstVisitModule && [...clientImportsSet][0] !== '*') { clientComponentImports[modRequest] = new Set(['*']) } } else { @@ -1080,3 +1051,26 @@ function addClientImport( } } } + +function getModuleResource(mod: webpack.NormalModule): string { + const modPath: string = mod.resourceResolveData?.path || '' + const modQuery = mod.resourceResolveData?.query || '' + // We have to always use the resolved request here to make sure the + // server and client are using the same module path (required by RSC), as + // the server compiler and client compiler have different resolve configs. + let modResource: string = modPath + modQuery + + // Context modules don't have a resource path, we use the identifier instead. + if (mod.constructor.name === 'ContextModule') { + modResource = mod.identifier() + } + + // For the barrel optimization, we need to use the match resource instead + // because there will be 2 modules for the same file (same resource path) + // but they're different modules and can't be deduped via `visitedModule`. + // The first module is a virtual re-export module created by the loader. + if (mod.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) { + modResource = mod.matchResource + ':' + modResource + } + return modResource +} diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index 02f41c7923710..1271fe61e571a 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -70,7 +70,7 @@ export interface ManifestNode { } export type ClientReferenceManifest = { - moduleLoading: { + readonly moduleLoading: { prefix: string crossOrigin: string | null } diff --git a/packages/next/src/build/webpack/plugins/middleware-plugin.ts b/packages/next/src/build/webpack/plugins/middleware-plugin.ts index 5c9ec94fd14c2..f0a6492f6b2f8 100644 --- a/packages/next/src/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/src/build/webpack/plugins/middleware-plugin.ts @@ -20,7 +20,6 @@ import { SUBRESOURCE_INTEGRITY_MANIFEST, NEXT_FONT_MANIFEST, SERVER_REFERENCE_MANIFEST, - PRERENDER_MANIFEST, INTERCEPTION_ROUTE_REWRITE_MANIFEST, } from '../../../shared/lib/constants' import type { MiddlewareConfig } from '../../analysis/get-page-static-info' @@ -42,10 +41,10 @@ export interface EdgeFunctionDefinition { name: string page: string matchers: MiddlewareMatcher[] + env: Record wasm?: AssetBinding[] assets?: AssetBinding[] regions?: string[] | string - environments?: Record } export interface MiddlewareManifest { @@ -135,10 +134,6 @@ function getEntryFiles( files.push(`server/edge-${INSTRUMENTATION_HOOK_FILENAME}.js`) } - if (process.env.NODE_ENV === 'production') { - files.push(PRERENDER_MANIFEST.replace('json', 'js')) - } - files.push( ...entryFiles .filter((file) => !file.endsWith('.hot-update.js')) @@ -227,7 +222,7 @@ function getCreateAssets(params: { name, filePath, })), - environments: opts.edgeEnvironments, + env: opts.edgeEnvironments, ...(metadata.regions && { regions: metadata.regions }), } @@ -739,18 +734,26 @@ function getExtractMetadata(params: { } } +// These values will be replaced again in edge runtime deployment build. +// `buildId` represents BUILD_ID to be externalized in env vars. +// `encryptionKey` represents server action encryption key to be externalized in env vars. +type EdgeRuntimeEnvironments = Record & { + __NEXT_BUILD_ID: string + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: string +} + interface Options { dev: boolean sriEnabled: boolean rewrites: CustomRoutes['rewrites'] - edgeEnvironments: Record + edgeEnvironments: EdgeRuntimeEnvironments } export default class MiddlewarePlugin { private readonly dev: Options['dev'] private readonly sriEnabled: Options['sriEnabled'] private readonly rewrites: Options['rewrites'] - private readonly edgeEnvironments: Record + private readonly edgeEnvironments: EdgeRuntimeEnvironments constructor({ dev, sriEnabled, rewrites, edgeEnvironments }: Options) { this.dev = dev diff --git a/packages/next/src/build/webpack/plugins/terser-webpack-plugin/src/index.ts b/packages/next/src/build/webpack/plugins/terser-webpack-plugin/src/index.ts index 96f847c2a94c4..771a4fde89e51 100644 --- a/packages/next/src/build/webpack/plugins/terser-webpack-plugin/src/index.ts +++ b/packages/next/src/build/webpack/plugins/terser-webpack-plugin/src/index.ts @@ -157,6 +157,9 @@ export class TerserPlugin { : {}), compress: true, mangle: true, + output: { + comments: false, + }, } ) diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index 23c3ab55619aa..0a6260289c435 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -291,7 +291,7 @@ function Router({ buildId, initialHead, initialTree, - initialCanonicalUrl, + urlParts, initialSeedData, couldBeIntercepted, assetPrefix, @@ -302,7 +302,7 @@ function Router({ createInitialRouterState({ buildId, initialSeedData, - initialCanonicalUrl, + urlParts, initialTree, initialParallelRoutes, location: !isServer ? window.location : null, @@ -312,7 +312,7 @@ function Router({ [ buildId, initialSeedData, - initialCanonicalUrl, + urlParts, initialTree, initialHead, couldBeIntercepted, diff --git a/packages/next/src/client/components/layout-router.tsx b/packages/next/src/client/components/layout-router.tsx index cfa1f7c298b34..286bb8de8482a 100644 --- a/packages/next/src/client/components/layout-router.tsx +++ b/packages/next/src/client/components/layout-router.tsx @@ -517,7 +517,6 @@ export default function OuterLayoutRouter({ template, notFound, notFoundStyles, - styles, }: { parallelRouterKey: string segmentPath: FlightSegmentPath @@ -529,7 +528,6 @@ export default function OuterLayoutRouter({ template: React.ReactNode notFound: React.ReactNode | undefined notFoundStyles: React.ReactNode | undefined - styles?: React.ReactNode }) { const context = useContext(LayoutRouterContext) if (!context) { @@ -562,7 +560,6 @@ export default function OuterLayoutRouter({ return ( <> - {styles} {preservedSegments.map((preservedSegment) => { const preservedSegmentValue = getSegmentValue(preservedSegment) const cacheKey = createRouterCacheKey(preservedSegment) diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts index b9a01f9bcc71e..3a3b87ac28a5b 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts @@ -8,19 +8,19 @@ function getSocketProtocol(assetPrefix: string): string { protocol = new URL(assetPrefix).protocol } catch {} - return protocol === 'http:' ? 'ws' : 'wss' + return protocol === 'http:' ? 'ws:' : 'wss:' } export function getSocketUrl(assetPrefix: string | undefined): string { - const { hostname, port } = window.location - const protocol = getSocketProtocol(assetPrefix || '') const prefix = normalizedAssetPrefix(assetPrefix) + const protocol = getSocketProtocol(assetPrefix || '') - // if original assetPrefix is a full URL with protocol - // we just update to use the correct `ws` protocol - if (assetPrefix?.replace(/^\/+/, '').includes('://')) { - return `${protocol}://${prefix}` + if (URL.canParse(prefix)) { + // since normalized asset prefix is ensured to be a URL format, + // we can safely replace the protocol + return prefix.replace(/^http/, 'ws') } - return `${protocol}://${hostname}:${port}${prefix}` + const { hostname, port } = window.location + return `${protocol}//${hostname}${port ? `:${port}` : ''}${prefix}` } 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 2af955ca7315f..0af201362a3da 100644 --- a/packages/next/src/client/components/request-async-storage.external.ts +++ b/packages/next/src/client/components/request-async-storage.external.ts @@ -8,13 +8,16 @@ import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/ada // 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 { readonly headers: ReadonlyHeaders readonly cookies: ReadonlyRequestCookies readonly mutableCookies: ResponseCookies readonly draftMode: DraftModeProvider - readonly reactLoadableManifest: Record + readonly reactLoadableManifest: DeepReadonly< + Record + > readonly assetPrefix: string } diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx b/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx index 46e99b41c7a95..d63c3fe0672b4 100644 --- a/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx +++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx @@ -36,7 +36,7 @@ describe('createInitialRouterState', () => { const state = createInitialRouterState({ buildId, initialTree, - initialCanonicalUrl, + urlParts: initialCanonicalUrl.split('/'), initialSeedData: ['', {}, children, null], initialParallelRoutes, location: new URL('/linking', 'https://localhost') as any, @@ -47,7 +47,7 @@ describe('createInitialRouterState', () => { const state2 = createInitialRouterState({ buildId, initialTree, - initialCanonicalUrl, + urlParts: initialCanonicalUrl.split('/'), initialSeedData: ['', {}, children, null], initialParallelRoutes, location: new URL('/linking', 'https://localhost') as any, 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 e3a2cbcdd832e..4a0371c2d6bd7 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 @@ -16,7 +16,7 @@ import { addRefreshMarkerToActiveParallelSegments } from './refetch-inactive-par export interface InitialRouterStateParameters { buildId: string initialTree: FlightRouterState - initialCanonicalUrl: string + urlParts: string[] initialSeedData: CacheNodeSeedData initialParallelRoutes: CacheNode['parallelRoutes'] location: Location | null @@ -28,12 +28,16 @@ export function createInitialRouterState({ buildId, initialTree, initialSeedData, - initialCanonicalUrl, + urlParts, initialParallelRoutes, location, initialHead, couldBeIntercepted, }: InitialRouterStateParameters) { + // When initialized on the server, the canonical URL is provided as an array of parts. + // This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it + // as a URL that should be crawled. + const initialCanonicalUrl = urlParts.join('/') const isServer = !location const rsc = initialSeedData[2] diff --git a/packages/next/src/client/link.tsx b/packages/next/src/client/link.tsx index 400c03282f461..af5ca061c6a07 100644 --- a/packages/next/src/client/link.tsx +++ b/packages/next/src/client/link.tsx @@ -57,7 +57,7 @@ type InternalLinkProps = { */ scroll?: boolean /** - * Update the path of the current page without rerunning [`getStaticProps`](/docs/pages/building-your-application/data-fetching/get-static-props), [`getServerSideProps`](/docs/pages/building-your-application/data-fetching/get-server-side-props) or [`getInitialProps`](/docs/pages/api-reference/functions/get-initial-props). + * Update the path of the current page without rerunning [`getStaticProps`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props), [`getServerSideProps`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props) or [`getInitialProps`](/docs/pages/api-reference/functions/get-initial-props). * * @defaultValue `false` */ @@ -70,12 +70,20 @@ type InternalLinkProps = { passHref?: boolean /** * Prefetch the page in the background. - * Any `` that is in the viewport (initially or through scroll) will be preloaded. - * Prefetch can be disabled by passing `prefetch={false}`. When `prefetch` is set to `false`, prefetching will still occur on hover in pages router but not in app router. Pages using [Static Generation](/docs/basic-features/data-fetching/get-static-props.md) will preload `JSON` files with the data for faster page transitions. Prefetching is only enabled in production. + * Any `` that is in the viewport (initially or through scroll) will be prefetched. + * Prefetch can be disabled by passing `prefetch={false}`. Prefetching is only enabled in production. * - * @defaultValue `true` + * In App Router: + * - `null` (default): For statically generated pages, this will prefetch the full React Server Component data. For dynamic pages, this will prefetch up to the nearest route segment with a [`loading.js`](https://nextjs.org/docs/app/api-reference/file-conventions/loading) file. If there is no loading file, it will not fetch the full tree to avoid fetching too much data. + * - `true`: This will prefetch the full React Server Component data for all route segments, regardless of whether they contain a segment with `loading.js`. + * - `false`: This will not prefetch any data, even on hover. + * + * In Pages Router: + * - `true` (default): The full route & its data will be prefetched. + * - `false`: Prefetching will not happen when entering the viewport, but will still happen on hover. + * @defaultValue `true` (pages router) or `null` (app router) */ - prefetch?: boolean + prefetch?: boolean | null /** * The active locale is automatically prepended. `locale` allows for providing a different locale. * When `false` `href` has to include the locale as the default behavior is disabled. diff --git a/packages/next/src/client/route-loader.ts b/packages/next/src/client/route-loader.ts index ab0337eff033f..133dae84dec1f 100644 --- a/packages/next/src/client/route-loader.ts +++ b/packages/next/src/client/route-loader.ts @@ -17,7 +17,6 @@ declare global { __BUILD_MANIFEST_CB?: Function __MIDDLEWARE_MATCHERS?: MiddlewareMatcher[] __MIDDLEWARE_MANIFEST_CB?: Function - __PRERENDER_MANIFEST?: string __REACT_LOADABLE_MANIFEST?: any __RSC_MANIFEST?: any __RSC_SERVER_MANIFEST?: any diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 7fc87fe264564..062d55a8a70fe 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -56,6 +56,7 @@ import { formatManifest } from '../build/manifests/formatter/format-manifest' import { validateRevalidate } from '../server/lib/patch-fetch' import { TurborepoAccessTraceResult } from '../build/turborepo-access-trace' import { createProgress } from '../build/progress' +import type { DeepReadonly } from '../shared/lib/deep-readonly' export class ExportError extends Error { code = 'NEXT_EXPORT_ERROR' @@ -188,7 +189,7 @@ export async function exportAppImpl( !options.pages && (require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST)) as PagesManifest) - let prerenderManifest: PrerenderManifest | undefined + let prerenderManifest: DeepReadonly | undefined try { prerenderManifest = require(join(distDir, PRERENDER_MANIFEST)) } catch {} @@ -398,7 +399,7 @@ export async function exportAppImpl( domainLocales: i18n?.domains, disableOptimizedLoading: nextConfig.experimental.disableOptimizedLoading, // Exported pages do not currently support dynamic HTML. - supportsDynamicHTML: false, + supportsDynamicResponse: false, crossOrigin: nextConfig.crossOrigin, optimizeCss: nextConfig.experimental.optimizeCss, nextConfigOutput: nextConfig.output, diff --git a/packages/next/src/export/routes/app-route.ts b/packages/next/src/export/routes/app-route.ts index 562e55d6803c0..b29e2955aed7b 100644 --- a/packages/next/src/export/routes/app-route.ts +++ b/packages/next/src/export/routes/app-route.ts @@ -67,7 +67,7 @@ export async function exportAppRoute( experimental: { ppr: false }, originalPathname: page, nextExport: true, - supportsDynamicHTML: false, + supportsDynamicResponse: false, incrementalCache, }, } diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index 59c7719ac7518..c27f3919944ce 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -269,7 +269,7 @@ async function exportPageImpl( disableOptimizedLoading, fontManifest: optimizeFonts ? requireFontManifest(distDir) : undefined, locale, - supportsDynamicHTML: false, + supportsDynamicResponse: false, originalPathname: page, } diff --git a/packages/next/src/lib/client-reference.ts b/packages/next/src/lib/client-reference.ts index 8a7122ab83359..28b51c4c8decd 100644 --- a/packages/next/src/lib/client-reference.ts +++ b/packages/next/src/lib/client-reference.ts @@ -1,3 +1,4 @@ -export function isClientReference(reference: any): boolean { - return reference?.$$typeof === Symbol.for('react.client.reference') +export function isClientReference(mod: any): boolean { + const defaultExport = mod?.default || mod + return defaultExport?.$$typeof === Symbol.for('react.client.reference') } diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index 825cb114c7665..9afc9f881a248 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -78,26 +78,6 @@ export const SSG_FALLBACK_EXPORT_ERROR = `Pages with \`fallback\` enabled in \`g export const ESLINT_DEFAULT_DIRS = ['app', 'pages', 'components', 'lib', 'src'] -export const ESLINT_PROMPT_VALUES = [ - { - title: 'Strict', - recommended: true, - config: { - extends: 'next/core-web-vitals', - }, - }, - { - title: 'Base', - config: { - extends: 'next', - }, - }, - { - title: 'Cancel', - config: null, - }, -] - export const SERVER_RUNTIME: Record = { edge: 'edge', experimentalEdge: 'experimental-edge', diff --git a/packages/next/src/lib/eslint/getESLintPromptValues.ts b/packages/next/src/lib/eslint/getESLintPromptValues.ts new file mode 100644 index 0000000000000..277546289a9fb --- /dev/null +++ b/packages/next/src/lib/eslint/getESLintPromptValues.ts @@ -0,0 +1,32 @@ +import findUp from 'next/dist/compiled/find-up' + +export const getESLintStrictValue = async (cwd: string) => { + const tsConfigLocation = await findUp('tsconfig.json', { cwd }) + const hasTSConfig = tsConfigLocation !== undefined + + return { + title: 'Strict', + recommended: true, + config: { + extends: hasTSConfig + ? ['next/core-web-vitals', 'next/typescript'] + : 'next/core-web-vitals', + }, + } +} + +export const getESLintPromptValues = async (cwd: string) => { + return [ + await getESLintStrictValue(cwd), + { + title: 'Base', + config: { + extends: 'next', + }, + }, + { + title: 'Cancel', + config: null, + }, + ] +} diff --git a/packages/next/src/lib/eslint/runLintCheck.ts b/packages/next/src/lib/eslint/runLintCheck.ts index 6e11c138d0604..b00f8fb1427ea 100644 --- a/packages/next/src/lib/eslint/runLintCheck.ts +++ b/packages/next/src/lib/eslint/runLintCheck.ts @@ -12,7 +12,6 @@ import { writeDefaultConfig } from './writeDefaultConfig' import { hasEslintConfiguration } from './hasEslintConfiguration' import { writeOutputFile } from './writeOutputFile' -import { ESLINT_PROMPT_VALUES } from '../constants' import { findPagesDir } from '../find-pages-dir' import { installDependencies } from '../install-dependencies' import { hasNecessaryDependencies } from '../has-necessary-dependencies' @@ -21,6 +20,10 @@ import * as Log from '../../build/output/log' import type { EventLintCheckCompleted } from '../../telemetry/events/build' import isError, { getProperError } from '../is-error' import { getPkgManager } from '../helpers/get-pkg-manager' +import { + getESLintStrictValue, + getESLintPromptValues, +} from './getESLintPromptValues' type Config = { plugins: string[] @@ -44,7 +47,7 @@ const requiredPackages = [ }, ] -async function cliPrompt(): Promise<{ config?: any }> { +async function cliPrompt(cwd: string): Promise<{ config?: any }> { console.log( bold( `${cyan( @@ -58,7 +61,7 @@ async function cliPrompt(): Promise<{ config?: any }> { await Promise.resolve(require('next/dist/compiled/cli-select')) ).default const { value } = await cliSelect({ - values: ESLINT_PROMPT_VALUES, + values: await getESLintPromptValues(cwd), valueRenderer: ( { title, @@ -355,10 +358,8 @@ export async function runLintCheck( } else { // Ask user what config they would like to start with for first time "next lint" setup const { config: selectedConfig } = strict - ? ESLINT_PROMPT_VALUES.find( - (opt: { title: string }) => opt.title === 'Strict' - )! - : await cliPrompt() + ? await getESLintStrictValue(baseDir) + : await cliPrompt(baseDir) if (selectedConfig == null) { // Show a warning if no option is selected in prompt diff --git a/packages/next/src/lib/metadata/default-metadata.tsx b/packages/next/src/lib/metadata/default-metadata.tsx index 75207ea6fea35..2449688350c52 100644 --- a/packages/next/src/lib/metadata/default-metadata.tsx +++ b/packages/next/src/lib/metadata/default-metadata.tsx @@ -47,6 +47,7 @@ export function createDefaultMetadata(): ResolvedMetadata { appleWebApp: null, formatDetection: null, itunes: null, + facebook: null, abstract: null, appLinks: null, archives: null, diff --git a/packages/next/src/lib/metadata/generate/basic.tsx b/packages/next/src/lib/metadata/generate/basic.tsx index c2f5460d62e0a..78dc89e4a51fd 100644 --- a/packages/next/src/lib/metadata/generate/basic.tsx +++ b/packages/next/src/lib/metadata/generate/basic.tsx @@ -114,6 +114,23 @@ export function ItunesMeta({ itunes }: { itunes: ResolvedMetadata['itunes'] }) { return } +export function FacebookMeta({ + facebook, +}: { + facebook: ResolvedMetadata['facebook'] +}) { + if (!facebook) return null + + const { appId, admins } = facebook + + return MetaFilter([ + appId ? : null, + ...(admins + ? admins.map((admin) => ) + : []), + ]) +} + const formatDetectionKeys = [ 'telephone', 'date', diff --git a/packages/next/src/lib/metadata/generate/meta.tsx b/packages/next/src/lib/metadata/generate/meta.tsx index 96394bc7b008a..27cb2bedb37fa 100644 --- a/packages/next/src/lib/metadata/generate/meta.tsx +++ b/packages/next/src/lib/metadata/generate/meta.tsx @@ -53,10 +53,17 @@ function camelToSnake(camelCaseStr: string) { }) } +const aliasPropPrefixes = new Set([ + 'og:image', + 'twitter:image', + 'og:video', + 'og:audio', +]) function getMetaKey(prefix: string, key: string) { // Use `twitter:image` and `og:image` instead of `twitter:image:url` and `og:image:url` - // to be more compatible as it's a more common format - if ((prefix === 'og:image' || prefix === 'twitter:image') && key === 'url') { + // to be more compatible as it's a more common format. + // `og:video` & `og:audio` do not have a `:url` suffix alias + if (aliasPropPrefixes.has(prefix) && key === 'url') { return prefix } if (prefix.startsWith('og:') || prefix.startsWith('twitter:')) { diff --git a/packages/next/src/lib/metadata/metadata.tsx b/packages/next/src/lib/metadata/metadata.tsx index 1be2eb2e20717..0814043364983 100644 --- a/packages/next/src/lib/metadata/metadata.tsx +++ b/packages/next/src/lib/metadata/metadata.tsx @@ -1,5 +1,8 @@ import type { ParsedUrlQuery } from 'querystring' -import type { GetDynamicParamFromSegment } from '../../server/app-render/app-render' +import type { + AppRenderContext, + GetDynamicParamFromSegment, +} from '../../server/app-render/app-render' import type { LoaderTree } from '../../server/lib/app-dir-module' import React from 'react' @@ -10,6 +13,7 @@ import { BasicMeta, ViewportMeta, VerificationMeta, + FacebookMeta, } from './generate/basic' import { AlternatesMetadata } from './generate/alternate' import { @@ -29,6 +33,18 @@ import { createDefaultViewport, } from './default-metadata' import { isNotFoundError } from '../../client/components/not-found' +import type { MetadataContext } from './types/resolvers' + +export function createMetadataContext( + urlPathname: string, + renderOpts: AppRenderContext['renderOpts'] +): MetadataContext { + return { + pathname: urlPathname.split('?')[0], + trailingSlash: renderOpts.trailingSlash, + isStandaloneMode: renderOpts.nextConfigOutput === 'standalone', + } +} // Use a promise to share the status of the metadata resolving, // returning two components `MetadataTree` and `MetadataOutlet` @@ -38,18 +54,16 @@ import { isNotFoundError } from '../../client/components/not-found' // and the error will be caught by the error boundary and trigger fallbacks. export function createMetadataComponents({ tree, - pathname, - trailingSlash, query, + metadataContext, getDynamicParamFromSegment, appUsingSizeAdjustment, errorType, createDynamicallyTrackedSearchParams, }: { tree: LoaderTree - pathname: string - trailingSlash: boolean query: ParsedUrlQuery + metadataContext: MetadataContext getDynamicParamFromSegment: GetDynamicParamFromSegment appUsingSizeAdjustment: boolean errorType?: 'not-found' | 'redirect' @@ -57,12 +71,6 @@ export function createMetadataComponents({ searchParams: ParsedUrlQuery ) => ParsedUrlQuery }): [React.ComponentType, React.ComponentType] { - const metadataContext = { - // Make sure the pathname without query string - pathname: pathname.split('?')[0], - trailingSlash, - } - let resolve: (value: Error | undefined) => void | undefined // Only use promise.resolve here to avoid unhandled rejections const metadataErrorResolving = new Promise((res) => { @@ -123,6 +131,7 @@ export function createMetadataComponents({ BasicMeta({ metadata }), AlternatesMetadata({ alternates: metadata.alternates }), ItunesMeta({ itunes: metadata.itunes }), + FacebookMeta({ facebook: metadata.facebook }), FormatDetectionMeta({ formatDetection: metadata.formatDetection }), VerificationMeta({ verification: metadata.verification }), AppleWebAppMeta({ appleWebApp: metadata.appleWebApp }), diff --git a/packages/next/src/lib/metadata/resolve-metadata.test.ts b/packages/next/src/lib/metadata/resolve-metadata.test.ts index 9cd6f2447bb4d..5754d0d94ad0e 100644 --- a/packages/next/src/lib/metadata/resolve-metadata.test.ts +++ b/packages/next/src/lib/metadata/resolve-metadata.test.ts @@ -17,6 +17,7 @@ function accumulateMetadata(metadataItems: MetadataItems) { return originAccumulateMetadata(fullMetadataItems, { pathname: '/test', trailingSlash: false, + isStandaloneMode: false, }) } diff --git a/packages/next/src/lib/metadata/resolve-metadata.ts b/packages/next/src/lib/metadata/resolve-metadata.ts index 0109b19df27e3..3fc16c7c0abc6 100644 --- a/packages/next/src/lib/metadata/resolve-metadata.ts +++ b/packages/next/src/lib/metadata/resolve-metadata.ts @@ -13,8 +13,13 @@ import type { OpenGraph } from './types/opengraph-types' import type { ComponentsType } from '../../build/webpack/loaders/next-app-loader' import type { MetadataContext } from './types/resolvers' import type { LoaderTree } from '../../server/lib/app-dir-module' -import type { AbsoluteTemplateString } from './types/metadata-types' +import type { + AbsoluteTemplateString, + IconDescriptor, + ResolvedIcons, +} from './types/metadata-types' import type { ParsedUrlQuery } from 'querystring' +import type { StaticMetadata } from './types/icons' import { createDefaultMetadata, @@ -37,6 +42,7 @@ import { resolveThemeColor, resolveVerification, resolveItunes, + resolveFacebook, } from './resolvers/resolve-basics' import { resolveIcons } from './resolvers/resolve-icons' import { getTracer } from '../../server/lib/trace/tracer' @@ -44,7 +50,7 @@ import { ResolveMetadataSpan } from '../../server/lib/trace/constants' import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment' import * as Log from '../../build/output/log' -type StaticMetadata = Awaited> +type StaticIcons = Pick type MetadataResolver = ( parent: ResolvingMetadata @@ -77,23 +83,17 @@ type PageProps = { searchParams: { [key: string]: any } } -function hasIconsProperty( - icons: Metadata['icons'], - prop: 'icon' | 'apple' -): boolean { - if (!icons) return false - if (prop === 'icon') { - // Detect if icons.icon will be presented, icons array and icons string will all be merged into icons.icon - return !!( - typeof icons === 'string' || - icons instanceof URL || - Array.isArray(icons) || - (prop in icons && icons[prop]) - ) - } else { - // Detect if icons.apple will be presented, only icons.apple will be merged into icons.apple - return !!(typeof icons === 'object' && prop in icons && icons[prop]) +function isFavicon(icon: IconDescriptor | undefined): boolean { + if (!icon) { + return false } + + // turbopack appends a hash to all images + return ( + (icon.url === '/favicon.ico' || + icon.url.toString().startsWith('/favicon.ico?')) && + icon.type === 'image/x-icon' + ) } function mergeStaticMetadata( @@ -101,25 +101,27 @@ function mergeStaticMetadata( target: ResolvedMetadata, staticFilesMetadata: StaticMetadata, metadataContext: MetadataContext, - titleTemplates: TitleTemplates + titleTemplates: TitleTemplates, + leafSegmentStaticIcons: StaticIcons ) { if (!staticFilesMetadata) return const { icon, apple, openGraph, twitter, manifest } = staticFilesMetadata - // file based metadata is specified and current level metadata icons is not specified - if ( - (icon && !hasIconsProperty(source?.icons, 'icon')) || - (apple && !hasIconsProperty(source?.icons, 'apple')) - ) { - target.icons = { - icon: icon || [], - apple: apple || [], - } + + // Keep updating the static icons in the most leaf node + + if (icon) { + leafSegmentStaticIcons.icon = icon + } + if (apple) { + leafSegmentStaticIcons.apple = apple } + // file based metadata is specified and current level metadata twitter.images is not specified if (twitter && !source?.twitter?.hasOwnProperty('images')) { const resolvedTwitter = resolveTwitter( { ...target.twitter, images: twitter } as Twitter, target.metadataBase, + metadataContext, titleTemplates.twitter ) target.twitter = resolvedTwitter @@ -150,6 +152,7 @@ function mergeMetadata({ titleTemplates, metadataContext, buildState, + leafSegmentStaticIcons, }: { source: Metadata | null target: ResolvedMetadata @@ -157,6 +160,7 @@ function mergeMetadata({ titleTemplates: TitleTemplates metadataContext: MetadataContext buildState: BuildState + leafSegmentStaticIcons: StaticIcons }): void { // If there's override metadata, prefer it otherwise fallback to the default metadata. const metadataBase = @@ -192,10 +196,15 @@ function mergeMetadata({ target.twitter = resolveTwitter( source.twitter, metadataBase, + metadataContext, titleTemplates.twitter ) break } + case 'facebook': + target.facebook = resolveFacebook(source.facebook) + break + case 'verification': target.verification = resolveVerification(source.verification) break @@ -274,7 +283,8 @@ function mergeMetadata({ target, staticFilesMetadata, metadataContext, - titleTemplates + titleTemplates, + leafSegmentStaticIcons ) } @@ -376,7 +386,10 @@ async function collectStaticImagesFiles( : undefined } -async function resolveStaticMetadata(components: ComponentsType, props: any) { +async function resolveStaticMetadata( + components: ComponentsType, + props: any +): Promise { const { metadata } = components if (!metadata) return null @@ -566,7 +579,9 @@ function inheritFromMetadata( const commonOgKeys = ['title', 'description', 'images'] as const function postProcessMetadata( metadata: ResolvedMetadata, - titleTemplates: TitleTemplates + favicon: any, + titleTemplates: TitleTemplates, + metadataContext: MetadataContext ): ResolvedMetadata { const { openGraph, twitter } = metadata @@ -599,6 +614,7 @@ function postProcessMetadata( const partialTwitter = resolveTwitter( autoFillProps, metadata.metadataBase, + metadataContext, titleTemplates.twitter ) if (metadata.twitter) { @@ -620,6 +636,17 @@ function postProcessMetadata( inheritFromMetadata(openGraph, metadata) inheritFromMetadata(twitter, metadata) + if (favicon) { + if (!metadata.icons) { + metadata.icons = { + icon: [], + apple: [], + } + } + + metadata.icons.icon.unshift(favicon) + } + return metadata } @@ -672,7 +699,7 @@ async function getMetadataFromExport( // Only preload at the beginning when resolves are empty if (!dynamicMetadataResolvers.length) { for (let j = currentIndex; j < metadataItems.length; j++) { - const preloadMetadataExport = getPreloadMetadataExport(metadataItems[j]) // metadataItems[j][0] + const preloadMetadataExport = getPreloadMetadataExport(metadataItems[j]) // call each `generateMetadata function concurrently and stash their resolver if (typeof preloadMetadataExport === 'function') { collectMetadataExportPreloading( @@ -739,9 +766,25 @@ export async function accumulateMetadata( const buildState = { warnings: new Set(), } + + let favicon + + // Collect the static icons in the most leaf node, + // since we don't collect all the static metadata icons in the parent segments. + const leafSegmentStaticIcons = { + icon: [], + apple: [], + } for (let i = 0; i < metadataItems.length; i++) { const staticFilesMetadata = metadataItems[i][1] + // Treat favicon as special case, it should be the first icon in the list + // i <= 1 represents root layout, and if current page is also at root + if (i <= 1 && isFavicon(staticFilesMetadata?.icon?.[0])) { + const iconMod = staticFilesMetadata?.icon?.shift() + if (i === 0) favicon = iconMod + } + const metadata = await getMetadataFromExport( (metadataItem) => metadataItem[0], dynamicMetadataResolvers, @@ -758,6 +801,7 @@ export async function accumulateMetadata( staticFilesMetadata, titleTemplates, buildState, + leafSegmentStaticIcons, }) // If the layout is the same layer with page, skip the leaf layout and leaf page @@ -771,6 +815,24 @@ export async function accumulateMetadata( } } + if ( + leafSegmentStaticIcons.icon.length > 0 || + leafSegmentStaticIcons.apple.length > 0 + ) { + if (!resolvedMetadata.icons) { + resolvedMetadata.icons = { + icon: [], + apple: [], + } + if (leafSegmentStaticIcons.icon.length > 0) { + resolvedMetadata.icons.icon.unshift(...leafSegmentStaticIcons.icon) + } + if (leafSegmentStaticIcons.apple.length > 0) { + resolvedMetadata.icons.apple.unshift(...leafSegmentStaticIcons.apple) + } + } + } + // Only log warnings if there are any, and only once after the metadata resolving process is finished if (buildState.warnings.size > 0) { for (const warning of buildState.warnings) { @@ -778,7 +840,12 @@ export async function accumulateMetadata( } } - return postProcessMetadata(resolvedMetadata, titleTemplates) + return postProcessMetadata( + resolvedMetadata, + favicon, + titleTemplates, + metadataContext + ) } export async function accumulateViewport( diff --git a/packages/next/src/lib/metadata/resolvers/resolve-basics.ts b/packages/next/src/lib/metadata/resolvers/resolve-basics.ts index bf1913520bda8..e9602afcc43de 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-basics.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-basics.ts @@ -251,3 +251,11 @@ export const resolveItunes: FieldResolverExtraArgs< : undefined, } } + +export const resolveFacebook: FieldResolver<'facebook'> = (facebook) => { + if (!facebook) return null + return { + appId: facebook.appId, + admins: resolveAsArrayOrUndefined(facebook.admins), + } +} diff --git a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.test.ts b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.test.ts index 822b98023f082..7155723b7555f 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.test.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.test.ts @@ -7,7 +7,7 @@ describe('resolveImages', () => { it(`should resolve images`, () => { const images = [image1, { url: image2, alt: 'Image2' }] - expect(resolveImages(images, null)).toEqual([ + expect(resolveImages(images, null, false)).toEqual([ { url: new URL(image1) }, { url: new URL(image2), alt: 'Image2' }, ]) @@ -16,7 +16,7 @@ describe('resolveImages', () => { it('should not mutate passed images', () => { const images = [image1, { url: image2, alt: 'Image2' }] - resolveImages(images, null) + resolveImages(images, null, false) expect(images).toEqual([image1, { url: image2, alt: 'Image2' }]) }) diff --git a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts index 61209b8464d06..780f3c2e45ab0 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-opengraph.ts @@ -42,14 +42,20 @@ const OgTypeFields = { function resolveAndValidateImage( item: FlattenArray, metadataBase: NonNullable, - isMetadataBaseMissing: boolean + isMetadataBaseMissing: boolean, + isStandaloneMode: boolean ) { if (!item) return undefined const isItemUrl = isStringOrURL(item) const inputUrl = isItemUrl ? item : item.url if (!inputUrl) return undefined - validateResolvedImageUrl(inputUrl, metadataBase, isMetadataBaseMissing) + const isNonVercelDeployment = + !process.env.VERCEL && process.env.NODE_ENV === 'production' + // Validate url in self-host standalone mode or non-Vercel deployment + if (isStandaloneMode || isNonVercelDeployment) { + validateResolvedImageUrl(inputUrl, metadataBase, isMetadataBaseMissing) + } return isItemUrl ? { @@ -64,15 +70,18 @@ function resolveAndValidateImage( export function resolveImages( images: Twitter['images'], - metadataBase: ResolvedMetadataBase + metadataBase: ResolvedMetadataBase, + isStandaloneMode: boolean ): NonNullable['images'] export function resolveImages( images: OpenGraph['images'], - metadataBase: ResolvedMetadataBase + metadataBase: ResolvedMetadataBase, + isStandaloneMode: boolean ): NonNullable['images'] export function resolveImages( images: OpenGraph['images'] | Twitter['images'], - metadataBase: ResolvedMetadataBase + metadataBase: ResolvedMetadataBase, + isStandaloneMode: boolean ): | NonNullable['images'] | NonNullable['images'] { @@ -86,7 +95,8 @@ export function resolveImages( const resolvedItem = resolveAndValidateImage( item, fallbackMetadataBase, - isMetadataBaseMissing + isMetadataBaseMissing, + isStandaloneMode ) if (!resolvedItem) continue @@ -149,7 +159,11 @@ export const resolveOpenGraph: FieldResolverExtraArgs< } } } - target.images = resolveImages(og.images, metadataBase) + target.images = resolveImages( + og.images, + metadataBase, + metadataContext.isStandaloneMode + ) } const resolved = { @@ -179,8 +193,8 @@ const TwitterBasicInfoKeys = [ export const resolveTwitter: FieldResolverExtraArgs< 'twitter', - [ResolvedMetadataBase, string | null] -> = (twitter, metadataBase, titleTemplate) => { + [ResolvedMetadataBase, MetadataContext, string | null] +> = (twitter, metadataBase, metadataContext, titleTemplate) => { if (!twitter) return null let card = 'card' in twitter ? twitter.card : undefined const resolved = { @@ -191,7 +205,11 @@ export const resolveTwitter: FieldResolverExtraArgs< resolved[infoKey] = twitter[infoKey] || null } - resolved.images = resolveImages(twitter.images, metadataBase) + resolved.images = resolveImages( + twitter.images, + metadataBase, + metadataContext.isStandaloneMode + ) card = card || (resolved.images?.length ? 'summary_large_image' : 'summary') resolved.card = card diff --git a/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts b/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts index 4a92cd5b18e64..d70c7bc5dc81a 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-url.test.ts @@ -53,6 +53,7 @@ describe('resolveAbsoluteUrlWithPathname', () => { const opts = { trailingSlash: false, pathname: '/', + isStandaloneMode: false, } const resolver = (url: string | URL) => resolveAbsoluteUrlWithPathname(url, metadataBase, opts) @@ -68,6 +69,7 @@ describe('resolveAbsoluteUrlWithPathname', () => { const opts = { trailingSlash: true, pathname: '/', + isStandaloneMode: false, } const resolver = (url: string | URL) => resolveAbsoluteUrlWithPathname(url, metadataBase, opts) diff --git a/packages/next/src/lib/metadata/types/extra-types.ts b/packages/next/src/lib/metadata/types/extra-types.ts index 514ad9b507cf2..0afd54a3bfaa6 100644 --- a/packages/next/src/lib/metadata/types/extra-types.ts +++ b/packages/next/src/lib/metadata/types/extra-types.ts @@ -88,6 +88,20 @@ export type ResolvedAppleWebApp = { statusBarStyle?: 'default' | 'black' | 'black-translucent' } +export type Facebook = FacebookAppId | FacebookAdmins +export type FacebookAppId = { + appId: string + admins?: never +} +export type FacebookAdmins = { + appId?: never + admins: string | string[] +} +export type ResolvedFacebook = { + appId?: string + admins?: string[] +} + // Format Detection // This is a poorly specified metadata export type that is supposed to // control whether the device attempts to conver text that matches diff --git a/packages/next/src/lib/metadata/types/icons.ts b/packages/next/src/lib/metadata/types/icons.ts new file mode 100644 index 0000000000000..87f00d80efc6c --- /dev/null +++ b/packages/next/src/lib/metadata/types/icons.ts @@ -0,0 +1,7 @@ +export type StaticMetadata = { + icon: any[] | undefined + apple: any[] | undefined + openGraph: any[] | undefined + twitter: any[] | undefined + manifest: string | undefined +} | null diff --git a/packages/next/src/lib/metadata/types/metadata-interface.ts b/packages/next/src/lib/metadata/types/metadata-interface.ts index a840df2dfae37..ebc7e340daf5c 100644 --- a/packages/next/src/lib/metadata/types/metadata-interface.ts +++ b/packages/next/src/lib/metadata/types/metadata-interface.ts @@ -6,10 +6,12 @@ import type { import type { AppleWebApp, AppLinks, + Facebook, FormatDetection, ItunesApp, ResolvedAppleWebApp, ResolvedAppLinks, + ResolvedFacebook, ViewportLayout, } from './extra-types' import type { @@ -320,6 +322,25 @@ interface Metadata extends DeprecatedMetadataFields { */ twitter?: null | Twitter + /** + * The Facebook metadata for the document. + * You can specify either appId or admins, but not both. + * @example + * ```tsx + * { appId: "12345678" } + * + * + * ``` + * + * @example + * ```tsx + * { admins: ["12345678"] } + * + * + * ``` + */ + facebook?: null | Facebook + /** * The common verification tokens for the document. * @example @@ -508,6 +529,8 @@ interface ResolvedMetadata extends DeprecatedMetadataFields { twitter: null | ResolvedTwitterMetadata + facebook: null | ResolvedFacebook + // common verification tokens verification: null | ResolvedVerification diff --git a/packages/next/src/lib/metadata/types/resolvers.ts b/packages/next/src/lib/metadata/types/resolvers.ts index fcc9f2b41786e..eec96b8f446a6 100644 --- a/packages/next/src/lib/metadata/types/resolvers.ts +++ b/packages/next/src/lib/metadata/types/resolvers.ts @@ -16,4 +16,5 @@ export type FieldResolverExtraArgs< export type MetadataContext = { pathname: string trailingSlash: boolean + isStandaloneMode: boolean } diff --git a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts index 5af5c97736f8d..4a1ff464357c1 100644 --- a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts +++ b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts @@ -59,7 +59,9 @@ export async function writeAppTypeDeclarations({ directives.push( '', '// NOTE: This file should not be edited', - '// see https://nextjs.org/docs/basic-features/typescript for more information.' + `// see https://nextjs.org/docs/${ + hasAppDir ? 'app' : 'pages' + }/building-your-application/configuring/typescript for more information.` ) const content = directives.join(eol) + eol diff --git a/packages/next/src/pages/_document.tsx b/packages/next/src/pages/_document.tsx index 891327bc15710..792c45f0c2d29 100644 --- a/packages/next/src/pages/_document.tsx +++ b/packages/next/src/pages/_document.tsx @@ -25,6 +25,7 @@ import { } from '../shared/lib/html-context.shared-runtime' import type { HtmlProps } from '../shared/lib/html-context.shared-runtime' import { encodeURIPath } from '../shared/lib/encode-uri-path' +import type { DeepReadonly } from '../shared/lib/deep-readonly' export type { DocumentContext, DocumentInitialProps, DocumentProps } @@ -360,7 +361,7 @@ function getAmpPath(ampPath: string, asPath: string): string { } function getNextFontLinkTags( - nextFontManifest: NextFontManifest | undefined, + nextFontManifest: DeepReadonly | undefined, dangerousAsPath: string, assetPrefix: string = '' ) { 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 614c8eee43879..57382abad2f3a 100644 --- a/packages/next/src/server/api-utils/node/api-resolver.ts +++ b/packages/next/src/server/api-utils/node/api-resolver.ts @@ -41,6 +41,7 @@ type ApiContext = __ApiPreviewProps & { allowedRevalidateHeaderKeys?: string[] hostname?: string revalidate?: RevalidateFn + multiZoneDraftMode?: boolean } function getMaxContentLength(responseLimit?: ResponseLimit) { @@ -346,7 +347,7 @@ export async function apiResolver( apiReq.query = query // Parsing preview data setLazyProp({ req: apiReq }, 'previewData', () => - tryGetPreviewData(req, res, apiContext) + tryGetPreviewData(req, res, apiContext, !!apiContext.multiZoneDraftMode) ) // Checking if preview mode is enabled setLazyProp({ req: apiReq }, 'preview', () => 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 ca81d1379e0bb..9c11b5f2dd012 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 @@ -17,7 +17,8 @@ import { HeadersAdapter } from '../../web/spec-extension/adapters/headers' export function tryGetPreviewData( req: IncomingMessage | BaseNextRequest | Request, res: ServerResponse | BaseNextResponse, - options: __ApiPreviewProps + options: __ApiPreviewProps, + multiZoneDraftMode: boolean ): PreviewData { // if an On-Demand revalidation is being done preview mode // is disabled @@ -61,13 +62,17 @@ export function tryGetPreviewData( // Case: one cookie is set, but not the other. if (!previewModeId || !tokenPreviewData) { - clearPreviewData(res as NextApiResponse) + if (!multiZoneDraftMode) { + clearPreviewData(res as NextApiResponse) + } return false } // Case: preview session is for an old build. if (previewModeId !== options.previewModeId) { - clearPreviewData(res as NextApiResponse) + if (!multiZoneDraftMode) { + clearPreviewData(res as NextApiResponse) + } return false } diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index 55c0234ec8933..6e404ca2a247e 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -117,9 +117,12 @@ async function addRevalidationHeader( requestStore: RequestStore } ) { - await Promise.all( - Object.values(staticGenerationStore.pendingRevalidates || []) - ) + await Promise.all([ + staticGenerationStore.incrementalCache?.revalidateTag( + staticGenerationStore.revalidatedTags || [] + ), + ...Object.values(staticGenerationStore.pendingRevalidates || {}), + ]) // If a tag was revalidated, the client router needs to invalidate all the // client router cache as they may be stale. And if a path was revalidated, the @@ -481,9 +484,12 @@ export async function handleAction({ if (isFetchAction) { res.statusCode = 500 - await Promise.all( - Object.values(staticGenerationStore.pendingRevalidates || []) - ) + await Promise.all([ + staticGenerationStore.incrementalCache?.revalidateTag( + staticGenerationStore.revalidatedTags || [] + ), + ...Object.values(staticGenerationStore.pendingRevalidates || {}), + ]) const promise = Promise.reject(error) try { @@ -840,9 +846,12 @@ To configure the body size limit for Server Actions, see: https://nextjs.org/doc if (isFetchAction) { res.statusCode = 500 - await Promise.all( - Object.values(staticGenerationStore.pendingRevalidates || []) - ) + await Promise.all([ + staticGenerationStore.incrementalCache?.revalidateTag( + staticGenerationStore.revalidatedTags || [] + ), + ...Object.values(staticGenerationStore.pendingRevalidates || {}), + ]) const promise = Promise.reject(err) try { // we need to await the promise to trigger the rejection early diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 0c7c66133f009..3ecfc5f8dbf55 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -41,7 +41,10 @@ import { NEXT_URL, RSC_HEADER, } from '../../client/components/app-router-headers' -import { createMetadataComponents } from '../../lib/metadata/metadata' +import { + createMetadataComponents, + createMetadataContext, +} from '../../lib/metadata/metadata' import { RequestAsyncStorageWrapper } from '../async-storage/request-async-storage-wrapper' import { StaticGenerationAsyncStorageWrapper } from '../async-storage/static-generation-async-storage-wrapper' import { isNotFoundError } from '../../client/components/not-found' @@ -107,6 +110,7 @@ import { wrapClientComponentLoader, } from '../client-component-renderer-logger' import { createServerModuleMap } from './action-utils' +import type { DeepReadonly } from '../../shared/lib/deep-readonly' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -137,7 +141,7 @@ export type AppRenderContext = AppRenderBaseContext & { requestId: string defaultRevalidate: Revalidate pagePath: string - clientReferenceManifest: ClientReferenceManifest + clientReferenceManifest: DeepReadonly assetPrefix: string flightDataRendererErrorHandler: ErrorHandler serverComponentsErrorHandler: ErrorHandler @@ -300,9 +304,8 @@ async function generateFlight( if (!options?.skipFlight) { const [MetadataTree, MetadataOutlet] = createMetadataComponents({ tree: loaderTree, - pathname: urlPathname, - trailingSlash: ctx.renderOpts.trailingSlash, query, + metadataContext: createMetadataContext(urlPathname, ctx.renderOpts), getDynamicParamFromSegment, appUsingSizeAdjustment, createDynamicallyTrackedSearchParams, @@ -316,13 +319,11 @@ async function generateFlight( flightRouterState, isFirst: true, // For flight, render metadata inside leaf page - rscPayloadHead: ( - <> - - {/* Adding requestId as react key to make metadata remount for each render */} - - - ), + // NOTE: in 14.2, fragment doesn't work well with React, using array instead + rscPayloadHead: [ + , + , + ], injectedCSS: new Set(), injectedJS: new Set(), injectedFontPreloadTags: new Set(), @@ -400,6 +401,17 @@ type ReactServerAppProps = { ctx: AppRenderContext asNotFound: boolean } + +/** + * Crawlers will inadvertently think the canonicalUrl in the RSC payload should be crawled + * when our intention is to just seed the router state with the current URL. + * This function splits up the pathname so that we can later join it on + * when we're ready to consume the path. + */ +function prepareInitialCanonicalUrl(pathname: string) { + return pathname.split('/') +} + // This is the root component that runs in the RSC context async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { // Create full component tree from root to leaf. @@ -427,15 +439,14 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { const [MetadataTree, MetadataOutlet] = createMetadataComponents({ tree, errorType: asNotFound ? 'not-found' : undefined, - pathname: urlPathname, - trailingSlash: ctx.renderOpts.trailingSlash, query, + metadataContext: createMetadataContext(urlPathname, ctx.renderOpts), getDynamicParamFromSegment: getDynamicParamFromSegment, appUsingSizeAdjustment: appUsingSizeAdjustment, createDynamicallyTrackedSearchParams, }) - const { seedData, styles } = await createComponentTree({ + const seedData = await createComponentTree({ ctx, createSegmentPath: (child) => child, loaderTree: tree, @@ -458,30 +469,27 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) { typeof varyHeader === 'string' && varyHeader.includes(NEXT_URL) return ( - <> - {styles} - - - {/* Adding requestId as react key to make metadata remount for each render */} - - - } - globalErrorComponent={GlobalError} - // This is used to provide debug information (when in development mode) - // about which slots were not filled by page components while creating the component tree. - missingSlots={missingSlots} - /> - + + + {/* Adding requestId as react key to make metadata remount for each render */} + + + } + globalErrorComponent={GlobalError} + // This is used to provide debug information (when in development mode) + // about which slots were not filled by page components while creating the component tree. + missingSlots={missingSlots} + /> ) } @@ -511,8 +519,7 @@ async function ReactServerError({ const [MetadataTree] = createMetadataComponents({ tree, - pathname: urlPathname, - trailingSlash: ctx.renderOpts.trailingSlash, + metadataContext: createMetadataContext(urlPathname, ctx.renderOpts), errorType, query, getDynamicParamFromSegment, @@ -522,12 +529,12 @@ async function ReactServerError({ const head = ( <> - {/* Adding requestId as react key to make metadata remount for each render */} {process.env.NODE_ENV === 'development' && ( )} + ) @@ -552,7 +559,7 @@ async function ReactServerError({ -}): Promise { +}): Promise { return getTracer().trace( NextNodeServerSpan.createComponentTree, { @@ -77,7 +72,7 @@ async function createComponentTreeInternal({ metadataOutlet?: React.ReactNode ctx: AppRenderContext missingSlots?: Set -}): Promise { +}): Promise { const { renderOpts: { nextConfigOutput, experimental }, staticGenerationStore, @@ -265,7 +260,7 @@ async function createComponentTreeInternal({ } const LayoutOrPage: React.ComponentType | undefined = layoutOrPageMod - ? interopDefault(layoutOrPageMod) + ? await interopDefault(layoutOrPageMod) : undefined /** @@ -379,7 +374,6 @@ async function createComponentTreeInternal({ // if we're prefetching and that there's a Loading component, we bail out // otherwise we keep rendering for the prefetch. // We also want to bail out if there's no Loading component in the tree. - let currentStyles = undefined let childCacheNodeSeedData: CacheNodeSeedData | null = null if ( @@ -431,24 +425,22 @@ async function createComponentTreeInternal({ } } - const { seedData, styles: childComponentStyles } = - await createComponentTreeInternal({ - createSegmentPath: (child) => { - return createSegmentPath([...currentSegmentPath, ...child]) - }, - loaderTree: parallelRoute, - parentParams: currentParams, - rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, - injectedCSS: injectedCSSWithCurrentLayout, - injectedJS: injectedJSWithCurrentLayout, - injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, - asNotFound, - metadataOutlet, - ctx, - missingSlots, - }) - - currentStyles = childComponentStyles + const seedData = await createComponentTreeInternal({ + createSegmentPath: (child) => { + return createSegmentPath([...currentSegmentPath, ...child]) + }, + loaderTree: parallelRoute, + parentParams: currentParams, + rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove, + injectedCSS: injectedCSSWithCurrentLayout, + injectedJS: injectedJSWithCurrentLayout, + injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout, + asNotFound, + metadataOutlet, + ctx, + missingSlots, + }) + childCacheNodeSeedData = seedData } @@ -471,7 +463,6 @@ async function createComponentTreeInternal({ templateScripts={templateScripts} notFound={notFoundComponent} notFoundStyles={notFoundStyles} - styles={currentStyles} />, childCacheNodeSeedData, ] @@ -496,20 +487,20 @@ async function createComponentTreeInternal({ // When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component if (!Component) { - return { - seedData: [ - actualSegment, - parallelRouteCacheNodeSeedData, - // TODO: I don't think the extra fragment is necessary. React treats top - // level fragments as transparent, i.e. the runtime behavior should be - // identical even without it. But maybe there's some findDOMNode-related - // reason that I'm not aware of, so I'm leaving it as-is out of extreme - // caution, for now. - <>{parallelRouteProps.children}, - loadingData, - ], - styles: layerAssets, - } + return [ + actualSegment, + parallelRouteCacheNodeSeedData, + // TODO: I don't think the extra fragment is necessary. React treats top + // level fragments as transparent, i.e. the runtime behavior should be + // identical even without it. But maybe there's some findDOMNode-related + // reason that I'm not aware of, so I'm leaving it as-is out of extreme + // caution, for now. + <> + {layerAssets} + {parallelRouteProps.children} + , + loadingData, + ] } // If force-dynamic is used and the current render supports postponing, we @@ -527,19 +518,19 @@ async function createComponentTreeInternal({ staticGenerationStore.forceDynamic && staticGenerationStore.prerenderState ) { - return { - seedData: [ - actualSegment, - parallelRouteCacheNodeSeedData, + return [ + actualSegment, + parallelRouteCacheNodeSeedData, + <> , - loadingData, - ], - styles: layerAssets, - } + /> + {layerAssets} + , + loadingData, + ] } const isClientComponent = isClientReference(layoutOrPageMod) @@ -594,6 +585,7 @@ async function createComponentTreeInternal({ <> {metadataOutlet} + {layerAssets} ) } else { @@ -604,21 +596,26 @@ async function createComponentTreeInternal({ <> {metadataOutlet} + {layerAssets} ) } } else { // For layouts we just render the component - segmentElement = + segmentElement = ( + <> + {layerAssets} + + + ) } - return { - seedData: [ - actualSegment, - parallelRouteCacheNodeSeedData, - <> - {segmentElement} - {/* This null is currently critical. The wrapped Component can render null and if there was not fragment + return [ + actualSegment, + parallelRouteCacheNodeSeedData, + <> + {segmentElement} + {/* This null is currently critical. The wrapped Component can render null and if there was not fragment surrounding it this would look like a pending tree data state on the client which will cause an error and break the app. Long-term we need to move away from using null as a partial tree identifier since it is a valid return type for the components we wrap. Once we make this change we can safely remove the @@ -626,10 +623,8 @@ async function createComponentTreeInternal({ If the Component above renders null the actual tree data will look like `[null, null]`. If we remove the extra null it will look like `null` (the array is elided) and this is what confuses the client router. TODO-APP update router to use a Symbol for partial tree detection */} - {null} - , - loadingData, - ], - styles: layerAssets, - } + {null} + , + loadingData, + ] } diff --git a/packages/next/src/server/app-render/encryption-utils.ts b/packages/next/src/server/app-render/encryption-utils.ts index e29d46484d9f5..76a23a743940a 100644 --- a/packages/next/src/server/app-render/encryption-utils.ts +++ b/packages/next/src/server/app-render/encryption-utils.ts @@ -1,5 +1,6 @@ import type { ActionManifest } from '../../build/webpack/plugins/flight-client-entry-plugin' import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin' +import type { DeepReadonly } from '../../shared/lib/deep-readonly' // Keep the key in memory as it should never change during the lifetime of the server in // both development and production. @@ -116,8 +117,8 @@ export function setReferenceManifestsSingleton({ serverActionsManifest, serverModuleMap, }: { - clientReferenceManifest: ClientReferenceManifest - serverActionsManifest: ActionManifest + clientReferenceManifest: DeepReadonly + serverActionsManifest: DeepReadonly serverModuleMap: { [id: string]: { id: string @@ -160,8 +161,8 @@ export function getClientReferenceManifestSingleton() { const serverActionsManifestSingleton = (globalThis as any)[ SERVER_ACTION_MANIFESTS_SINGLETON ] as { - clientReferenceManifest: ClientReferenceManifest - serverActionsManifest: ActionManifest + clientReferenceManifest: DeepReadonly + serverActionsManifest: DeepReadonly } if (!serverActionsManifestSingleton) { @@ -181,8 +182,8 @@ export async function getActionEncryptionKey() { const serverActionsManifestSingleton = (globalThis as any)[ SERVER_ACTION_MANIFESTS_SINGLETON ] as { - clientReferenceManifest: ClientReferenceManifest - serverActionsManifest: ActionManifest + clientReferenceManifest: DeepReadonly + serverActionsManifest: DeepReadonly } if (!serverActionsManifestSingleton) { diff --git a/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx b/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx index e8c5ea360eab5..b177883e371fa 100644 --- a/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx +++ b/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx @@ -1,10 +1,11 @@ import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin' +import type { DeepReadonly } from '../../shared/lib/deep-readonly' /** * Get external stylesheet link hrefs based on server CSS manifest. */ export function getLinkAndScriptTags( - clientReferenceManifest: ClientReferenceManifest, + clientReferenceManifest: DeepReadonly, filePath: string, injectedCSS: Set, injectedScripts: Set, diff --git a/packages/next/src/server/app-render/get-preloadable-fonts.tsx b/packages/next/src/server/app-render/get-preloadable-fonts.tsx index 61991ababccf8..99e94cdfad193 100644 --- a/packages/next/src/server/app-render/get-preloadable-fonts.tsx +++ b/packages/next/src/server/app-render/get-preloadable-fonts.tsx @@ -1,4 +1,5 @@ import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin' +import type { DeepReadonly } from '../../shared/lib/deep-readonly' /** * Get hrefs for fonts to preload @@ -8,7 +9,7 @@ import type { NextFontManifest } from '../../build/webpack/plugins/next-font-man * Returns null if there are fonts but none to preload and at least some were previously preloaded */ export function getPreloadableFonts( - nextFontManifest: NextFontManifest | undefined, + nextFontManifest: DeepReadonly | undefined, filePath: string | undefined, injectedFontPreloadTags: Set ): string[] | null { diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 7ed9c21b9e6d8..f415902b65c90 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -7,6 +7,7 @@ import type { ParsedUrlQuery } from 'querystring' import type { AppPageModule } from '../future/route-modules/app-page/module' import type { SwrDelta } from '../lib/revalidate' import type { LoadingModuleData } from '../../shared/lib/app-router-context.shared-runtime' +import type { DeepReadonly } from '../../shared/lib/deep-readonly' import s from 'next/dist/compiled/superstruct' @@ -126,14 +127,14 @@ export interface RenderOptsPartial { buildId: string basePath: string trailingSlash: boolean - clientReferenceManifest?: ClientReferenceManifest - supportsDynamicHTML: boolean + clientReferenceManifest?: DeepReadonly + supportsDynamicResponse: boolean runtime?: ServerRuntime serverComponents?: boolean enableTainting?: boolean assetPrefix?: string crossOrigin?: '' | 'anonymous' | 'use-credentials' | undefined - nextFontManifest?: NextFontManifest + nextFontManifest?: DeepReadonly isBot?: boolean incrementalCache?: import('../lib/incremental-cache').IncrementalCache isRevalidate?: boolean diff --git a/packages/next/src/server/app-render/use-flight-response.tsx b/packages/next/src/server/app-render/use-flight-response.tsx index 1483de5df7efa..6069a71aff358 100644 --- a/packages/next/src/server/app-render/use-flight-response.tsx +++ b/packages/next/src/server/app-render/use-flight-response.tsx @@ -2,6 +2,7 @@ import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight import type { BinaryStreamOf } from './app-render' import { htmlEscapeJsonString } from '../htmlescape' +import type { DeepReadonly } from '../../shared/lib/deep-readonly' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -18,7 +19,7 @@ const encoder = new TextEncoder() */ export function useFlightStream( flightStream: BinaryStreamOf, - clientReferenceManifest: ClientReferenceManifest, + clientReferenceManifest: DeepReadonly, nonce?: string ): Promise { const response = flightResponses.get(flightStream) diff --git a/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx b/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx index 3a0dd77ea61ec..3876c9356b35d 100644 --- a/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx +++ b/packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx @@ -4,7 +4,6 @@ import type { FlightSegmentPath, Segment, } from './types' -import React from 'react' import { canSegmentBeOverridden, matchSegment, @@ -16,9 +15,7 @@ import { addSearchParamsIfPageSegment, createFlightRouterStateFromLoaderTree, } from './create-flight-router-state-from-loader-tree' -import { parseLoaderTree } from './parse-loader-tree' import type { CreateSegmentPath, AppRenderContext } from './app-render' -import { getLayerAssets } from './get-layer-assets' import { hasLoadingComponentInTree } from './has-loading-component-in-tree' import { createComponentTree } from './create-component-tree' import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment' @@ -139,7 +136,7 @@ export async function walkTreeWithFlightRouterState({ return [[overriddenSegment, routerState, null, null]] } else { // Create component tree using the slice of the loaderTree - const { seedData } = await createComponentTree( + const seedData = await createComponentTree( // This ensures flightRouterPath is valid and filters down the tree { ctx, @@ -157,23 +154,7 @@ export async function walkTreeWithFlightRouterState({ } ) - // Create head - const { layoutOrPagePath } = parseLoaderTree(loaderTreeToFilter) - const layerAssets = getLayerAssets({ - ctx, - layoutOrPagePath, - injectedCSS: new Set(injectedCSS), - injectedJS: new Set(injectedJS), - injectedFontPreloadTags: new Set(injectedFontPreloadTags), - }) - const head = ( - <> - {layerAssets} - {rscPayloadHead} - - ) - - return [[overriddenSegment, routerState, seedData, head]] + return [[overriddenSegment, routerState, seedData, rscPayloadHead]] } } diff --git a/packages/next/src/server/async-storage/draft-mode-provider.ts b/packages/next/src/server/async-storage/draft-mode-provider.ts index 1d0cac44570cc..45325691950b6 100644 --- a/packages/next/src/server/async-storage/draft-mode-provider.ts +++ b/packages/next/src/server/async-storage/draft-mode-provider.ts @@ -41,7 +41,10 @@ export class DraftModeProvider { !isOnDemandRevalidate && cookieValue && previewProps && - cookieValue === previewProps.previewModeId + (cookieValue === previewProps.previewModeId || + // In dev mode, the cookie can be actual hash value preview id but the preview props can still be `development-id`. + (process.env.NODE_ENV !== 'production' && + previewProps.previewModeId === 'development-id')) ) this._previewModeId = previewProps?.previewModeId diff --git a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts index c0d12f6873fcd..68ecd09332d61 100644 --- a/packages/next/src/server/async-storage/request-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/request-async-storage-wrapper.ts @@ -17,9 +17,9 @@ import { RequestCookiesAdapter, type ReadonlyRequestCookies, } from '../web/spec-extension/adapters/request-cookies' -import type { ResponseCookies } from '../web/spec-extension/cookies' -import { RequestCookies } from '../web/spec-extension/cookies' +import { ResponseCookies, RequestCookies } from '../web/spec-extension/cookies' import { DraftModeProvider } from './draft-mode-provider' +import { splitCookiesString } from '../web/utils' function getHeaders(headers: Headers | IncomingHttpHeaders): ReadonlyHeaders { const cleaned = HeadersAdapter.from(headers) @@ -30,13 +30,6 @@ function getHeaders(headers: Headers | IncomingHttpHeaders): ReadonlyHeaders { return HeadersAdapter.seal(cleaned) } -function getCookies( - headers: Headers | IncomingHttpHeaders -): ReadonlyRequestCookies { - const cookies = new RequestCookies(HeadersAdapter.from(headers)) - return RequestCookiesAdapter.seal(cookies) -} - function getMutableCookies( headers: Headers | IncomingHttpHeaders, onUpdateCookies?: (cookies: string[]) => void @@ -51,6 +44,35 @@ export type RequestContext = { renderOpts?: RenderOpts } +/** + * If middleware set cookies in this request (indicated by `x-middleware-set-cookie`), + * then merge those into the existing cookie object, so that when `cookies()` is accessed + * it's able to read the newly set cookies. + */ +function mergeMiddlewareCookies( + req: RequestContext['req'], + existingCookies: RequestCookies | ResponseCookies +) { + if ( + 'x-middleware-set-cookie' in req.headers && + typeof req.headers['x-middleware-set-cookie'] === 'string' + ) { + const setCookieValue = req.headers['x-middleware-set-cookie'] + const responseHeaders = new Headers() + + for (const cookie of splitCookiesString(setCookieValue)) { + responseHeaders.append('set-cookie', cookie) + } + + const responseCookies = new ResponseCookies(responseHeaders) + + // Transfer cookies from ResponseCookies to RequestCookies + for (const cookie of responseCookies.getAll()) { + existingCookies.set(cookie) + } + } +} + export const RequestAsyncStorageWrapper: AsyncStorageWrapper< RequestStore, RequestContext @@ -101,20 +123,32 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper< }, get cookies() { if (!cache.cookies) { + // if middleware is setting cookie(s), then include those in + // the initial cached cookies so they can be read in render + const requestCookies = new RequestCookies( + HeadersAdapter.from(req.headers) + ) + + mergeMiddlewareCookies(req, requestCookies) + // Seal the cookies object that'll freeze out any methods that could // mutate the underlying data. - cache.cookies = getCookies(req.headers) + cache.cookies = RequestCookiesAdapter.seal(requestCookies) } return cache.cookies }, get mutableCookies() { if (!cache.mutableCookies) { - cache.mutableCookies = getMutableCookies( + const mutableCookies = getMutableCookies( req.headers, renderOpts?.onUpdateCookies || (res ? defaultOnUpdateCookies : undefined) ) + + mergeMiddlewareCookies(req, mutableCookies) + + cache.mutableCookies = mutableCookies } return cache.mutableCookies }, 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 bbe9d42e1bb86..e086b4e9033a2 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 @@ -37,7 +37,7 @@ export type StaticGenerationContext = { // mirrored. RenderOptsPartial, | 'originalPathname' - | 'supportsDynamicHTML' + | 'supportsDynamicResponse' | 'isRevalidate' | 'nextExport' | 'isDraftMode' @@ -72,7 +72,7 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper< * coalescing, and ISR continue working as intended. */ const isStaticGeneration = - !renderOpts.supportsDynamicHTML && + !renderOpts.supportsDynamicResponse && !renderOpts.isDraftMode && !renderOpts.isServerAction diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 14e1d1e5f1d9d..deca9b2c32e99 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -134,6 +134,8 @@ import { PrefetchRSCPathnameNormalizer } from './future/normalizers/request/pref import { NextDataPathnameNormalizer } from './future/normalizers/request/next-data' 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' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -284,9 +286,9 @@ export default abstract class Server { protected readonly renderOpts: BaseRenderOpts protected readonly serverOptions: Readonly protected readonly appPathRoutes?: Record - protected readonly clientReferenceManifest?: ClientReferenceManifest + protected readonly clientReferenceManifest?: DeepReadonly protected interceptionRoutePatterns: RegExp[] - protected nextFontManifest?: NextFontManifest + protected nextFontManifest?: DeepReadonly private readonly responseCache: ResponseCacheBase protected abstract getPublicDir(): string @@ -313,9 +315,11 @@ export default abstract class Server { shouldEnsure?: boolean url?: string }): Promise - protected abstract getFontManifest(): FontManifest | undefined - protected abstract getPrerenderManifest(): PrerenderManifest - protected abstract getNextFontManifest(): NextFontManifest | undefined + protected abstract getFontManifest(): DeepReadonly | undefined + protected abstract getPrerenderManifest(): DeepReadonly + protected abstract getNextFontManifest(): + | DeepReadonly + | undefined protected abstract attachRequestMeta( req: BaseNextRequest, parsedUrl: NextUrlWithParsedQuery @@ -481,7 +485,7 @@ export default abstract class Server { } this.renderOpts = { - supportsDynamicHTML: true, + supportsDynamicResponse: true, trailingSlash: this.nextConfig.trailingSlash, deploymentId: this.nextConfig.deploymentId, strictNextHead: !!this.nextConfig.experimental.strictNextHead, @@ -594,10 +598,6 @@ export default abstract class Server { return false } - // If we're here, this is a data request, as it didn't return and it matched - // either a RSC or a prefetch RSC request. - parsedUrl.query.__nextDataReq = '1' - if (req.url) { const parsed = parseUrl(req.url) parsed.pathname = parsedUrl.pathname @@ -1524,7 +1524,7 @@ export default abstract class Server { ...partialContext, renderOpts: { ...this.renderOpts, - supportsDynamicHTML: !isBotRequest, + supportsDynamicResponse: !isBotRequest, isBot: !!isBotRequest, }, } @@ -1565,7 +1565,7 @@ export default abstract class Server { ...partialContext, renderOpts: { ...this.renderOpts, - supportsDynamicHTML: false, + supportsDynamicResponse: false, }, } const payload = await fn(ctx) @@ -1795,12 +1795,11 @@ export default abstract class Server { ) { isSSG = true } else if (!this.renderOpts.dev) { - isSSG ||= - !!prerenderManifest.routes[pathname === '/index' ? '/' : pathname] + isSSG ||= !!prerenderManifest.routes[toRoute(pathname)] } // Toggle whether or not this is a Data request - let isDataReq = + const isNextDataRequest = !!( query.__nextDataReq || (req.headers['x-nextjs-data'] && @@ -1873,7 +1872,7 @@ export default abstract class Server { opts.experimental.ppr && isRSCRequest && !isPrefetchRSCRequest // we need to ensure the status code if /404 is visited directly - if (is404Page && !isDataReq && !isRSCRequest) { + if (is404Page && !isNextDataRequest && !isRSCRequest) { res.statusCode = 404 } @@ -1914,7 +1913,7 @@ export default abstract class Server { delete query.amp } - if (opts.supportsDynamicHTML === true) { + if (opts.supportsDynamicResponse === true) { const isBotRequest = isBot(req.headers['user-agent'] || '') const isSupportedDocument = typeof components.Document?.getInitialProps !== 'function' || @@ -1926,19 +1925,14 @@ export default abstract class Server { // TODO-APP: should the first render for a dynamic app path // be static so we can collect revalidate and populate the // cache if there are no dynamic data requirements - opts.supportsDynamicHTML = + opts.supportsDynamicResponse = !isSSG && !isBotRequest && !query.amp && isSupportedDocument opts.isBot = isBotRequest } // In development, we always want to generate dynamic HTML. - if ( - !isDataReq && - isAppPath && - opts.dev && - opts.supportsDynamicHTML === false - ) { - opts.supportsDynamicHTML = true + if (!isNextDataRequest && isAppPath && opts.dev) { + opts.supportsDynamicResponse = true } const defaultLocale = isSSG @@ -1956,32 +1950,30 @@ export default abstract class Server { if (process.env.NEXT_RUNTIME !== 'edge') { const { tryGetPreviewData } = require('./api-utils/node/try-get-preview-data') as typeof import('./api-utils/node/try-get-preview-data') - previewData = tryGetPreviewData(req, res, this.renderOpts.previewProps) + previewData = tryGetPreviewData( + req, + res, + this.renderOpts.previewProps, + !!this.nextConfig.experimental.multiZoneDraftMode + ) isPreviewMode = previewData !== false } } - if (isAppPath) { - if (!this.renderOpts.dev && !isPreviewMode && isSSG && isRSCRequest) { - // If this is an RSC request but we aren't in minimal mode, then we mark - // that this is a data request so that we can generate the flight data - // only. - if (!this.minimalMode) { - isDataReq = true - } - - // If this is a dynamic RSC request, ensure that we don't purge the - // flight headers to ensure that we will only produce the RSC response. - // We only need to do this in non-edge environments (as edge doesn't - // support static generation). - if ( - !isDynamicRSCRequest && - (!isEdgeRuntime(opts.runtime) || - (this.serverOptions as any).webServerConfig) - ) { - stripFlightHeaders(req.headers) - } - } + // If this is a request for an app path that should be statically generated + // and we aren't in the edge runtime, strip the flight headers so it will + // generate the static response. + if ( + isAppPath && + !opts.dev && + !isPreviewMode && + isSSG && + isRSCRequest && + !isDynamicRSCRequest && + (!isEdgeRuntime(opts.runtime) || + (this.serverOptions as any).webServerConfig) + ) { + stripFlightHeaders(req.headers) } let isOnDemandRevalidate = false @@ -2032,7 +2024,7 @@ export default abstract class Server { // remove /_next/data prefix from urlPathname so it matches // for direct page visit and /_next/data visit - if (isDataReq) { + if (isNextDataRequest) { resolvedUrlPathname = this.stripNextDataPath(resolvedUrlPathname) urlPathname = this.stripNextDataPath(urlPathname) } @@ -2041,7 +2033,7 @@ export default abstract class Server { if ( !isPreviewMode && isSSG && - !opts.supportsDynamicHTML && + !opts.supportsDynamicResponse && !isServerAction && !minimalPostponed && !isDynamicRSCRequest @@ -2126,10 +2118,10 @@ export default abstract class Server { const doRender: Renderer = async ({ postponed }) => { // In development, we always want to generate dynamic HTML. - let supportsDynamicHTML: boolean = - // If this isn't a data request and we're not in development, then we - // support dynamic HTML. - (!isDataReq && opts.dev === true) || + let supportsDynamicResponse: boolean = + // If we're in development, we always support dynamic HTML, unless it's + // a data request, in which case we only produce static HTML. + (!isNextDataRequest && opts.dev === true) || // If this is not SSG or does not have static paths, then it supports // dynamic HTML. (!isSSG && !hasStaticPaths) || @@ -2173,11 +2165,12 @@ export default abstract class Server { serverActions: this.nextConfig.experimental.serverActions, } : {}), - isDataReq, + isNextDataRequest, resolvedUrl, locale, locales, defaultLocale, + multiZoneDraftMode: this.nextConfig.experimental.multiZoneDraftMode, // For getServerSideProps and getInitialProps we need to ensure we use the original URL // and not the resolved URL to prevent a hydration mismatch on // asPath @@ -2190,8 +2183,7 @@ export default abstract class Server { query: origQuery, }) : resolvedUrl, - - supportsDynamicHTML, + supportsDynamicResponse, isOnDemandRevalidate, isDraftMode: isPreviewMode, isServerAction, @@ -2199,9 +2191,9 @@ export default abstract class Server { } if (isDebugPPRSkeleton) { - supportsDynamicHTML = false + supportsDynamicResponse = false renderOpts.nextExport = true - renderOpts.supportsDynamicHTML = false + renderOpts.supportsDynamicResponse = false renderOpts.isStaticGeneration = true renderOpts.isRevalidate = true renderOpts.isDebugPPRSkeleton = true @@ -2220,7 +2212,7 @@ export default abstract class Server { // App Route's cannot postpone, so don't enable it. experimental: { ppr: false }, originalPathname: components.ComponentMod.originalPathname, - supportsDynamicHTML, + supportsDynamicResponse, incrementalCache, isRevalidate: isSSG, }, @@ -2515,7 +2507,7 @@ export default abstract class Server { throw new NoFallbackError() } - if (!isDataReq) { + if (!isNextDataRequest) { // Production already emitted the fallback as static HTML. if (isProduction) { const html = await this.getFallback( @@ -2564,10 +2556,7 @@ export default abstract class Server { return { ...result, - revalidate: - result.revalidate !== undefined - ? result.revalidate - : /* default to minimum revalidate (this should be an invariant) */ 1, + revalidate: result.revalidate, } }, { @@ -2578,6 +2567,13 @@ export default abstract class Server { } ) + if (isPreviewMode) { + res.setHeader( + 'Cache-Control', + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + } + if (!cacheEntry) { if (ssgCacheKey && !(isOnDemandRevalidate && revalidateOnlyGenerated)) { // A cache entry might not be generated if a response is written @@ -2644,10 +2640,11 @@ export default abstract class Server { revalidate = 0 } else if ( typeof cacheEntry.revalidate !== 'undefined' && - (!this.renderOpts.dev || (hasServerProps && !isDataReq)) + (!this.renderOpts.dev || (hasServerProps && !isNextDataRequest)) ) { - // If this is a preview mode request, we shouldn't cache it - if (isPreviewMode) { + // If this is a preview mode request, we shouldn't cache it. We also don't + // cache 404 pages. + if (isPreviewMode || (is404Page && !isNextDataRequest)) { revalidate = 0 } @@ -2710,7 +2707,9 @@ export default abstract class Server { // for the revalidate value addRequestMeta(req, 'notFoundRevalidate', cacheEntry.revalidate) - if (cacheEntry.revalidate) { + // If cache control is already set on the response we don't + // override it to allow users to customize it via next.config + if (cacheEntry.revalidate && !res.getHeader('Cache-Control')) { res.setHeader( 'Cache-Control', formatRevalidate({ @@ -2719,7 +2718,7 @@ export default abstract class Server { }) ) } - if (isDataReq) { + if (isNextDataRequest) { res.statusCode = 404 res.body('{"notFound":true}').send() return null @@ -2731,7 +2730,9 @@ export default abstract class Server { await this.render404(req, res, { pathname, query }, false) return null } else if (cachedData.kind === 'REDIRECT') { - if (cacheEntry.revalidate) { + // If cache control is already set on the response we don't + // override it to allow users to customize it via next.config + if (cacheEntry.revalidate && !res.getHeader('Cache-Control')) { res.setHeader( 'Cache-Control', formatRevalidate({ @@ -2741,7 +2742,7 @@ export default abstract class Server { ) } - if (isDataReq) { + if (isNextDataRequest) { return { type: 'json', body: RenderResult.fromStatic( @@ -2816,7 +2817,7 @@ export default abstract class Server { // If the request is a data request, then we shouldn't set the status code // from the response because it should always be 200. This should be gated // behind the experimental PPR flag. - if (cachedData.status && (!isDataReq || !opts.experimental.ppr)) { + if (cachedData.status && (!isRSCRequest || !opts.experimental.ppr)) { res.statusCode = cachedData.status } @@ -2829,13 +2830,9 @@ export default abstract class Server { // as preview mode is a dynamic request (bypasses cache) and doesn't // generate both HTML and payloads in the same request so continue to just // return the generated payload - if (isDataReq && !isPreviewMode) { + if (isRSCRequest && !isPreviewMode) { // If this is a dynamic RSC request, then stream the response. - if (isDynamicRSCRequest) { - if (cachedData.pageData) { - throw new Error('Invariant: Expected pageData to be undefined') - } - + if (typeof cachedData.pageData !== 'string') { if (cachedData.postponed) { throw new Error('Invariant: Expected postponed to be undefined') } @@ -2848,16 +2845,10 @@ export default abstract class Server { // distinguishing between `force-static` and pages that have no // postponed state. // TODO: distinguish `force-static` from pages with no postponed state (static) - revalidate: 0, + revalidate: isDynamicRSCRequest ? 0 : cacheEntry.revalidate, } } - if (typeof cachedData.pageData !== 'string') { - throw new Error( - `Invariant: expected pageData to be a string, got ${typeof cachedData.pageData}` - ) - } - // As this isn't a prefetch request, we should serve the static flight // data. return { @@ -2927,7 +2918,7 @@ export default abstract class Server { // to the client on the same request. revalidate: 0, } - } else if (isDataReq) { + } else if (isNextDataRequest) { return { type: 'json', body: RenderResult.fromStatic(JSON.stringify(cachedData.pageData)), @@ -3214,7 +3205,7 @@ export default abstract class Server { if (setHeaders) { res.setHeader( 'Cache-Control', - 'no-cache, no-store, max-age=0, must-revalidate' + 'private, no-cache, no-store, max-age=0, must-revalidate' ) } diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 8d0978edb0f4f..e7e258800a59b 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -297,6 +297,7 @@ export const configSchema: zod.ZodType = z.lazy(() => linkNoTouchStart: z.boolean().optional(), manualClientBasePath: z.boolean().optional(), middlewarePrefetch: z.enum(['strict', 'flexible']).optional(), + multiZoneDraftMode: z.boolean().optional(), cssChunking: z.enum(['strict', 'loose']).optional(), nextScriptWorkers: z.boolean().optional(), // The critter option is unknown, use z.any() here diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index a10f1748a137a..36fd339a6d7ef 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -181,6 +181,7 @@ export interface NextJsWebpackConfig { } export interface ExperimentalConfig { + multiZoneDraftMode?: boolean prerenderEarlyExit?: boolean linkNoTouchStart?: boolean caseSensitiveRoutes?: boolean @@ -877,6 +878,7 @@ export const defaultConfig: NextConfig = { output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined, modularizeImports: undefined, experimental: { + multiZoneDraftMode: false, prerenderEarlyExit: false, serverMinification: true, serverSourceMaps: false, diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index b1afde773f350..15f70dcdd89af 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -121,6 +121,7 @@ export async function createHotReloaderTurbopack( // of the current `next dev` invocation. hotReloaderSpan.stop() + const encryptionKey = await generateEncryptionKeyBase64(true) const project = await bindings.turbo.createProject({ projectPath: dir, rootPath: opts.nextConfig.experimental.outputFileTracingRoot || dir, @@ -141,6 +142,9 @@ export async function createHotReloaderTurbopack( // TODO: Implement middlewareMatchers: undefined, }), + buildId, + encryptionKey, + previewProps: opts.fsChecker.prerenderManifest.preview, }) const entrypointsSubscription = project.entrypointsSubscribe() @@ -164,7 +168,7 @@ export async function createHotReloaderTurbopack( const manifestLoader = new TurbopackManifestLoader({ buildId, distDir, - encryptionKey: await generateEncryptionKeyBase64(), + encryptionKey, }) // Dev specific diff --git a/packages/next/src/server/future/route-modules/app-route/module.ts b/packages/next/src/server/future/route-modules/app-route/module.ts index d38560afb2ea4..69f786e94ddf0 100644 --- a/packages/next/src/server/future/route-modules/app-route/module.ts +++ b/packages/next/src/server/future/route-modules/app-route/module.ts @@ -4,6 +4,7 @@ import type { AppConfig } from '../../../../build/utils' import type { NextRequest } from '../../../web/spec-extension/request' import type { PrerenderManifest } from '../../../../build' import type { NextURL } from '../../../web/next-url' +import type { DeepReadonly } from '../../../../shared/lib/deep-readonly' import { RouteModule, @@ -68,7 +69,7 @@ export type AppRouteModule = */ export interface AppRouteRouteHandlerContext extends RouteModuleHandleContext { renderOpts: StaticGenerationContext['renderOpts'] - prerenderManifest: PrerenderManifest + prerenderManifest: DeepReadonly } /** @@ -382,11 +383,14 @@ export class AppRouteRouteModule extends RouteModule< context.renderOpts.fetchMetrics = staticGenerationStore.fetchMetrics - context.renderOpts.waitUntil = Promise.all( - Object.values( - staticGenerationStore.pendingRevalidates || [] - ) - ) + context.renderOpts.waitUntil = Promise.all([ + staticGenerationStore.incrementalCache?.revalidateTag( + staticGenerationStore.revalidatedTags || [] + ), + ...Object.values( + staticGenerationStore.pendingRevalidates || {} + ), + ]) addImplicitTags(staticGenerationStore) ;(context.renderOpts as any).fetchTags = 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 ac48492c5029c..e380b020d0866 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 @@ -93,6 +93,11 @@ type PagesAPIRouteHandlerContext = RouteModuleHandleContext & { * The page that's being rendered. */ page: string + + /** + * whether multi-zone flag is enabled for draft mode + */ + multiZoneDraftMode?: boolean } export type PagesAPIRouteModuleOptions = RouteModuleOptions< @@ -144,6 +149,7 @@ export class PagesAPIRouteModule extends RouteModule< trustHostHeader: context.trustHostHeader, allowedRevalidateHeaderKeys: context.allowedRevalidateHeaderKeys, hostname: context.hostname, + multiZoneDraftMode: context.multiZoneDraftMode, }, context.minimalMode, context.dev, diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 32ff21246ad04..34ea5e9c9ff5e 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -198,21 +198,20 @@ export class ImageOptimizerCache { } } - const parsedUrl = parseUrl(url) - if (parsedUrl) { - const decodedPathname = decodeURIComponent(parsedUrl.pathname) - if (/\/_next\/image($|\/)/.test(decodedPathname)) { - return { - errorMessage: '"url" parameter cannot be recursive', - } - } - } - let isAbsolute: boolean if (url.startsWith('/')) { href = url isAbsolute = false + if ( + /\/_next\/image($|\/)/.test( + decodeURIComponent(parseUrl(url)?.pathname ?? '') + ) + ) { + return { + errorMessage: '"url" parameter cannot be recursive', + } + } } else { let hrefParsed: URL diff --git a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts index 4ed3cf0ad7292..f2e62275d3a56 100644 --- a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts @@ -176,29 +176,32 @@ export default class FetchCache implements CacheHandler { return } - try { - const res = await fetchRetryWithTimeout( - `${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${tags - .map((tag) => encodeURIComponent(tag)) - .join(',')}`, - { - method: 'POST', - headers: this.headers, - // @ts-expect-error not on public type - next: { internal: true }, - } - ) + for (let i = 0; i < Math.ceil(tags.length / 64); i++) { + const currentTags = tags.slice(i * 64, i * 64 + 64) + try { + const res = await fetchRetryWithTimeout( + `${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${currentTags + .map((tag) => encodeURIComponent(tag)) + .join(',')}`, + { + method: 'POST', + headers: this.headers, + // @ts-expect-error not on public type + next: { internal: true }, + } + ) - if (res.status === 429) { - const retryAfter = res.headers.get('retry-after') || '60000' - rateLimitedUntil = Date.now() + parseInt(retryAfter) - } + if (res.status === 429) { + const retryAfter = res.headers.get('retry-after') || '60000' + rateLimitedUntil = Date.now() + parseInt(retryAfter) + } - if (!res.ok) { - throw new Error(`Request failed with status ${res.status}.`) + if (!res.ok) { + throw new Error(`Request failed with status ${res.status}.`) + } + } catch (err) { + console.warn(`Failed to revalidate tag`, currentTags, err) } - } catch (err) { - console.warn(`Failed to revalidate tag ${tags}`, err) } } diff --git a/packages/next/src/server/lib/incremental-cache/index.ts b/packages/next/src/server/lib/incremental-cache/index.ts index 08d3b51761945..021ec29344d33 100644 --- a/packages/next/src/server/lib/incremental-cache/index.ts +++ b/packages/next/src/server/lib/incremental-cache/index.ts @@ -6,10 +6,11 @@ import type { IncrementalCache as IncrementalCacheType, IncrementalCacheKindHint, } from '../../response-cache' +import type { Revalidate } from '../revalidate' +import type { DeepReadonly } from '../../../shared/lib/deep-readonly' import FetchCache from './fetch-cache' import FileSystemCache from './file-system-cache' -import path from '../../../shared/lib/isomorphic/path' import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' import { @@ -18,10 +19,8 @@ import { NEXT_CACHE_REVALIDATE_TAG_TOKEN_HEADER, PRERENDER_REVALIDATE_HEADER, } from '../../../lib/constants' - -function toRoute(pathname: string): string { - return pathname.replace(/\/$/, '').replace(/\/index$/, '') || '/' -} +import { toRoute } from '../to-route' +import { SharedRevalidateTimings } from './shared-revalidate-timings' export interface CacheHandlerContext { fs?: CacheFs @@ -67,20 +66,27 @@ export class CacheHandler { } export class IncrementalCache implements IncrementalCacheType { - dev?: boolean - disableForTestmode?: boolean - cacheHandler?: CacheHandler - hasCustomCacheHandler: boolean - prerenderManifest: PrerenderManifest - requestHeaders: Record - requestProtocol?: 'http' | 'https' - allowedRevalidateHeaderKeys?: string[] - minimalMode?: boolean - fetchCacheKeyPrefix?: string - revalidatedTags?: string[] - isOnDemandRevalidate?: boolean - private locks = new Map>() - private unlocks = new Map Promise>() + readonly dev?: boolean + readonly disableForTestmode?: boolean + readonly cacheHandler?: CacheHandler + readonly hasCustomCacheHandler: boolean + readonly prerenderManifest: DeepReadonly + readonly requestHeaders: Record + readonly requestProtocol?: 'http' | 'https' + readonly allowedRevalidateHeaderKeys?: string[] + readonly minimalMode?: boolean + readonly fetchCacheKeyPrefix?: string + readonly revalidatedTags?: string[] + readonly isOnDemandRevalidate?: boolean + + private readonly locks = new Map>() + private readonly unlocks = new Map Promise>() + + /** + * The revalidate timings for routes. This will source the timings from the + * prerender manifest until the in-memory cache is updated with new timings. + */ + private readonly revalidateTimings: SharedRevalidateTimings constructor({ fs, @@ -112,7 +118,7 @@ export class IncrementalCache implements IncrementalCacheType { allowedRevalidateHeaderKeys?: string[] requestHeaders: IncrementalCache['requestHeaders'] maxMemoryCacheSize?: number - getPrerenderManifest: () => PrerenderManifest + getPrerenderManifest: () => DeepReadonly fetchCacheKeyPrefix?: string CurCacheHandler?: typeof CacheHandler experimental: { ppr: boolean } @@ -154,6 +160,7 @@ export class IncrementalCache implements IncrementalCacheType { this.requestProtocol = requestProtocol this.allowedRevalidateHeaderKeys = allowedRevalidateHeaderKeys this.prerenderManifest = getPrerenderManifest() + this.revalidateTimings = new SharedRevalidateTimings(this.prerenderManifest) this.fetchCacheKeyPrefix = fetchCacheKeyPrefix let revalidatedTags: string[] = [] @@ -195,18 +202,16 @@ export class IncrementalCache implements IncrementalCacheType { pathname: string, fromTime: number, dev?: boolean - ): number | false { + ): Revalidate { // in development we don't have a prerender-manifest // and default to always revalidating to allow easier debugging if (dev) return new Date().getTime() - 1000 // if an entry isn't present in routes we fallback to a default - // of revalidating after 1 second - const { initialRevalidateSeconds } = this.prerenderManifest.routes[ - toRoute(pathname) - ] || { - initialRevalidateSeconds: 1, - } + // of revalidating after 1 second. + const initialRevalidateSeconds = + this.revalidateTimings.get(toRoute(pathname)) ?? 1 + const revalidateAfter = typeof initialRevalidateSeconds === 'number' ? initialRevalidateSeconds * 1000 + fromTime @@ -492,11 +497,10 @@ export class IncrementalCache implements IncrementalCacheType { } } - const curRevalidate = - this.prerenderManifest.routes[toRoute(cacheKey)]?.initialRevalidateSeconds + const curRevalidate = this.revalidateTimings.get(toRoute(cacheKey)) let isStale: boolean | -1 | undefined - let revalidateAfter: false | number + let revalidateAfter: Revalidate if (cacheData?.lastModified === -1) { isStale = -1 @@ -591,22 +595,12 @@ export class IncrementalCache implements IncrementalCacheType { pathname = this._getPathname(pathname, ctx.fetchCache) try { - // we use the prerender manifest memory instance - // to store revalidate timings for calculating - // revalidateAfter values so we update this on set + // Set the value for the revalidate seconds so if it changes we can + // update the cache with the new value. if (typeof ctx.revalidate !== 'undefined' && !ctx.fetchCache) { - this.prerenderManifest.routes[pathname] = { - experimentalPPR: undefined, - dataRoute: path.posix.join( - '/_next/data', - `${normalizePagePath(pathname)}.json` - ), - srcRoute: null, // FIXME: provide actual source route, however, when dynamically appending it doesn't really matter - initialRevalidateSeconds: ctx.revalidate, - // Pages routes do not have a prefetch data route. - prefetchDataRoute: undefined, - } + this.revalidateTimings.set(pathname, ctx.revalidate) } + await this.cacheHandler?.set(pathname, data, ctx) } catch (error) { console.warn('Failed to update prerender cache for', pathname, error) diff --git a/packages/next/src/server/lib/incremental-cache/shared-revalidate-timings.test.ts b/packages/next/src/server/lib/incremental-cache/shared-revalidate-timings.test.ts new file mode 100644 index 0000000000000..8b08fedc24138 --- /dev/null +++ b/packages/next/src/server/lib/incremental-cache/shared-revalidate-timings.test.ts @@ -0,0 +1,61 @@ +import { SharedRevalidateTimings } from './shared-revalidate-timings' + +describe('SharedRevalidateTimings', () => { + let sharedRevalidateTimings: SharedRevalidateTimings + let prerenderManifest + + beforeEach(() => { + prerenderManifest = { + routes: { + '/route1': { + initialRevalidateSeconds: 10, + dataRoute: null, + srcRoute: null, + prefetchDataRoute: null, + experimentalPPR: undefined, + }, + '/route2': { + initialRevalidateSeconds: 20, + dataRoute: null, + srcRoute: null, + prefetchDataRoute: null, + experimentalPPR: undefined, + }, + }, + } + sharedRevalidateTimings = new SharedRevalidateTimings(prerenderManifest) + }) + + afterEach(() => { + sharedRevalidateTimings.clear() + }) + + it('should get revalidate timing from in-memory cache', () => { + sharedRevalidateTimings.set('/route1', 15) + const revalidate = sharedRevalidateTimings.get('/route1') + expect(revalidate).toBe(15) + }) + + it('should get revalidate timing from prerender manifest if not in cache', () => { + const revalidate = sharedRevalidateTimings.get('/route2') + expect(revalidate).toBe(20) + }) + + it('should return undefined if revalidate timing not found', () => { + const revalidate = sharedRevalidateTimings.get('/route3') + expect(revalidate).toBeUndefined() + }) + + it('should set revalidate timing in cache', () => { + sharedRevalidateTimings.set('/route3', 30) + const revalidate = sharedRevalidateTimings.get('/route3') + expect(revalidate).toBe(30) + }) + + it('should clear the in-memory cache', () => { + sharedRevalidateTimings.set('/route3', 30) + sharedRevalidateTimings.clear() + const revalidate = sharedRevalidateTimings.get('/route3') + expect(revalidate).toBeUndefined() + }) +}) diff --git a/packages/next/src/server/lib/incremental-cache/shared-revalidate-timings.ts b/packages/next/src/server/lib/incremental-cache/shared-revalidate-timings.ts new file mode 100644 index 0000000000000..b3d3e008aa8f8 --- /dev/null +++ b/packages/next/src/server/lib/incremental-cache/shared-revalidate-timings.ts @@ -0,0 +1,66 @@ +import type { PrerenderManifest } from '../../../build' +import type { DeepReadonly } from '../../../shared/lib/deep-readonly' +import type { Revalidate } from '../revalidate' + +/** + * A shared cache of revalidate timings for routes. This cache is used so we + * don't have to modify the prerender manifest when we want to update the + * revalidate timings for a route. + */ +export class SharedRevalidateTimings { + /** + * The in-memory cache of revalidate timings for routes. This cache is + * populated when the cache is updated with new timings. + */ + private static readonly timings = new Map() + + constructor( + /** + * The prerender manifest that contains the initial revalidate timings for + * routes. + */ + private readonly prerenderManifest: DeepReadonly< + Pick + > + ) {} + + /** + * Try to get the revalidate timings for a route. This will first try to get + * the timings from the in-memory cache. If the timings are not present in the + * in-memory cache, then the timings will be sourced from the prerender + * manifest. + * + * @param route the route to get the revalidate timings for + * @returns the revalidate timings for the route, or undefined if the timings + * are not present in the in-memory cache or the prerender manifest + */ + public get(route: string): Revalidate | undefined { + // This is a copy on write cache that is updated when the cache is updated. + // If the cache is never written to, then the timings will be sourced from + // the prerender manifest. + let revalidate = SharedRevalidateTimings.timings.get(route) + if (typeof revalidate !== 'undefined') return revalidate + + revalidate = this.prerenderManifest.routes[route]?.initialRevalidateSeconds + if (typeof revalidate !== 'undefined') return revalidate + + return undefined + } + + /** + * Set the revalidate timings for a route. + * + * @param route the route to set the revalidate timings for + * @param revalidate the revalidate timings for the route + */ + public set(route: string, revalidate: Revalidate) { + SharedRevalidateTimings.timings.set(route, revalidate) + } + + /** + * Clear the in-memory cache of revalidate timings for routes. + */ + public clear() { + SharedRevalidateTimings.timings.clear() + } +} diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index 4757b7b0aba6a..1739d21bc5112 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -526,7 +526,7 @@ export async function initialize(opts: { // 404 case res.setHeader( 'Cache-Control', - 'no-cache, no-store, max-age=0, must-revalidate' + 'private, no-cache, no-store, max-age=0, must-revalidate' ) // Short-circuit favicon.ico serving so that the 404 page doesn't get built as favicon is requested by the browser when loading any route. @@ -657,7 +657,15 @@ export async function initialize(opts: { // assetPrefix overrides basePath for HMR path if (assetPrefix) { hmrPrefix = normalizedAssetPrefix(assetPrefix) + + if (URL.canParse(hmrPrefix)) { + // remove trailing slash from pathname + // return empty string if pathname is '/' + // to avoid conflicts with '/_next' below + hmrPrefix = new URL(hmrPrefix).pathname.replace(/\/$/, '') + } } + const isHMRRequest = req.url.startsWith( ensureLeadingSlash(`${hmrPrefix}/_next/webpack-hmr`) ) 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 1250838f93447..7b9d119eb2d54 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -471,6 +471,9 @@ export function getResolveRoutes( throw new Error(`Failed to initialize render server "middleware"`) } + addRequestMeta(req, 'invokePath', '') + addRequestMeta(req, 'invokeOutput', '') + addRequestMeta(req, 'invokeQuery', {}) addRequestMeta(req, 'middlewareInvoke', true) debug('invoking middleware', req.url, req.headers) diff --git a/packages/next/src/server/lib/to-route.test.ts b/packages/next/src/server/lib/to-route.test.ts new file mode 100644 index 0000000000000..a5beb8bd88392 --- /dev/null +++ b/packages/next/src/server/lib/to-route.test.ts @@ -0,0 +1,33 @@ +import { toRoute } from './to-route' + +describe('toRoute Function', () => { + it('should remove trailing slash', () => { + const result = toRoute('/example/') + expect(result).toBe('/example') + }) + + it('should remove trailing `/index`', () => { + const result = toRoute('/example/index') + expect(result).toBe('/example') + }) + + it('should return `/` when input is `/index`', () => { + const result = toRoute('/index') + expect(result).toBe('/') + }) + + it('should return `/` when input is `/index/`', () => { + const result = toRoute('/index/') + expect(result).toBe('/') + }) + + it('should return `/` when input is only a slash', () => { + const result = toRoute('/') + expect(result).toBe('/') + }) + + it('should return `/` when input is empty', () => { + const result = toRoute('') + expect(result).toBe('/') + }) +}) diff --git a/packages/next/src/server/lib/to-route.ts b/packages/next/src/server/lib/to-route.ts new file mode 100644 index 0000000000000..8685052e37f81 --- /dev/null +++ b/packages/next/src/server/lib/to-route.ts @@ -0,0 +1,26 @@ +/** + * This transforms a URL pathname into a route. It removes any trailing slashes + * and the `/index` suffix. + * + * @param {string} pathname - The URL path that needs to be optimized. + * @returns {string} - The route + * + * @example + * // returns '/example' + * toRoute('/example/index/'); + * + * @example + * // returns '/example' + * toRoute('/example/'); + * + * @example + * // returns '/' + * toRoute('/index/'); + * + * @example + * // returns '/' + * toRoute('/'); + */ +export function toRoute(pathname: string): string { + return pathname.replace(/(?:\/index)?\/?$/, '') || '/' +} diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 46b1e6d35b0aa..c7e7d2843ffc1 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -30,6 +30,7 @@ import { evalManifest, loadManifest } from './load-manifest' import { wait } from '../lib/wait' import { setReferenceManifestsSingleton } from './app-render/encryption-utils' import { createServerModuleMap } from './app-render/action-utils' +import type { DeepReadonly } from '../shared/lib/deep-readonly' export type ManifestItem = { id: number | string @@ -52,10 +53,10 @@ export interface LoadableManifest { export type LoadComponentsReturnType = { Component: NextComponentType pageConfig: PageConfig - buildManifest: BuildManifest - subresourceIntegrityManifest?: Record - reactLoadableManifest: ReactLoadableManifest - clientReferenceManifest?: ClientReferenceManifest + buildManifest: DeepReadonly + subresourceIntegrityManifest?: DeepReadonly> + reactLoadableManifest: DeepReadonly + clientReferenceManifest?: DeepReadonly serverActionsManifest?: any Document: DocumentType App: AppType @@ -66,18 +67,19 @@ export type LoadComponentsReturnType = { routeModule?: RouteModule isAppPath?: boolean page: string + multiZoneDraftMode?: boolean } /** * Load manifest file with retries, defaults to 3 attempts. */ -export async function loadManifestWithRetries( +export async function loadManifestWithRetries( manifestPath: string, attempts = 3 -): Promise { +) { while (true) { try { - return loadManifest(manifestPath) + return loadManifest(manifestPath) } catch (err) { attempts-- if (attempts <= 0) throw err @@ -90,13 +92,13 @@ export async function loadManifestWithRetries( /** * Load manifest file with retries, defaults to 3 attempts. */ -export async function evalManifestWithRetries( +export async function evalManifestWithRetries( manifestPath: string, attempts = 3 -): Promise { +) { while (true) { try { - return evalManifest(manifestPath) + return evalManifest(manifestPath) } catch (err) { attempts-- if (attempts <= 0) throw err @@ -109,12 +111,12 @@ export async function evalManifestWithRetries( async function loadClientReferenceManifest( manifestPath: string, entryName: string -): Promise { +) { try { - const context = (await evalManifestWithRetries(manifestPath)) as { + const context = await evalManifestWithRetries<{ __RSC_MANIFEST: { [key: string]: ClientReferenceManifest } - } - return context.__RSC_MANIFEST[entryName] as ClientReferenceManifest + }>(manifestPath) + return context.__RSC_MANIFEST[entryName] } catch (err) { return undefined } @@ -149,12 +151,10 @@ async function loadComponentsImpl({ clientReferenceManifest, serverActionsManifest, ] = await Promise.all([ - loadManifestWithRetries( - join(distDir, BUILD_MANIFEST) - ) as Promise, - loadManifestWithRetries( + loadManifestWithRetries(join(distDir, BUILD_MANIFEST)), + loadManifestWithRetries( join(distDir, REACT_LOADABLE_MANIFEST) - ) as Promise, + ), hasClientManifest ? loadClientReferenceManifest( join( @@ -167,9 +167,9 @@ async function loadComponentsImpl({ ) : undefined, isAppPath - ? (loadManifestWithRetries( + ? loadManifestWithRetries( join(distDir, 'server', SERVER_REFERENCE_MANIFEST + '.json') - ).catch(() => null) as Promise) + ).catch(() => null) : null, ]) diff --git a/packages/next/src/server/load-manifest.test.ts b/packages/next/src/server/load-manifest.test.ts new file mode 100644 index 0000000000000..32e77fa81c8aa --- /dev/null +++ b/packages/next/src/server/load-manifest.test.ts @@ -0,0 +1,82 @@ +import { loadManifest } from './load-manifest' +import { readFileSync } from 'fs' + +jest.mock('fs') + +describe('loadManifest', () => { + const cache = new Map() + + afterEach(() => { + jest.resetAllMocks() + cache.clear() + }) + + it('should load the manifest from the file system when not cached', () => { + const mockManifest = { key: 'value' } + ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockManifest)) + + let result = loadManifest('path/to/manifest', false) + expect(result).toEqual(mockManifest) + expect(readFileSync).toHaveBeenCalledTimes(1) + expect(readFileSync).toHaveBeenCalledWith('path/to/manifest', 'utf8') + expect(cache.has('path/to/manifest')).toBe(false) + + result = loadManifest('path/to/manifest', false) + expect(result).toEqual(mockManifest) + expect(readFileSync).toHaveBeenCalledTimes(2) + expect(readFileSync).toHaveBeenCalledWith('path/to/manifest', 'utf8') + expect(cache.has('path/to/manifest')).toBe(false) + }) + + it('should return the cached manifest when available', () => { + const mockManifest = { key: 'value' } + cache.set('path/to/manifest', mockManifest) + + let result = loadManifest('path/to/manifest', true, cache) + expect(result).toBe(mockManifest) + expect(readFileSync).not.toHaveBeenCalled() + + result = loadManifest('path/to/manifest', true, cache) + expect(result).toBe(mockManifest) + expect(readFileSync).not.toHaveBeenCalled() + }) + + it('should cache the manifest when not already cached', () => { + const mockManifest = { key: 'value' } + ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockManifest)) + + const result = loadManifest('path/to/manifest', true, cache) + + expect(result).toEqual(mockManifest) + expect(cache.get('path/to/manifest')).toEqual(mockManifest) + expect(readFileSync).toHaveBeenCalledWith('path/to/manifest', 'utf8') + }) + + it('should throw an error when the manifest file cannot be read', () => { + ;(readFileSync as jest.Mock).mockImplementation(() => { + throw new Error('File not found') + }) + + expect(() => loadManifest('path/to/manifest', false)).toThrow( + 'File not found' + ) + }) + + it('should freeze the manifest when caching', () => { + const mockManifest = { key: 'value', nested: { key: 'value' } } + ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockManifest)) + + const result = loadManifest( + 'path/to/manifest', + true, + cache + ) as typeof mockManifest + expect(Object.isFrozen(result)).toBe(true) + expect(Object.isFrozen(result.nested)).toBe(true) + + const result2 = loadManifest('path/to/manifest', true, cache) + expect(Object.isFrozen(result2)).toBe(true) + + expect(result).toBe(result2) + }) +}) diff --git a/packages/next/src/server/load-manifest.ts b/packages/next/src/server/load-manifest.ts index 82b2ae1d0272d..59a724e84b0e3 100644 --- a/packages/next/src/server/load-manifest.ts +++ b/packages/next/src/server/load-manifest.ts @@ -1,19 +1,52 @@ +import type { DeepReadonly } from '../shared/lib/deep-readonly' + import { readFileSync } from 'fs' import { runInNewContext } from 'vm' +import { deepFreeze } from '../shared/lib/deep-freeze' -const cache = new Map() +const sharedCache = new Map() -export function loadManifest( +/** + * Load a manifest file from the file system. Optionally cache the manifest in + * memory to avoid reading the file multiple times using the provided cache or + * defaulting to a shared module cache. The manifest is frozen to prevent + * modifications if it is cached. + * + * @param path the path to the manifest file + * @param shouldCache whether to cache the manifest in memory + * @param cache the cache to use for storing the manifest + * @returns the manifest object + */ +export function loadManifest( + path: string, + shouldCache: false +): T +export function loadManifest( + path: string, + shouldCache?: boolean, + cache?: Map +): DeepReadonly +export function loadManifest( + path: string, + shouldCache?: true, + cache?: Map +): DeepReadonly +export function loadManifest( path: string, - shouldCache: boolean = true -): unknown { + shouldCache: boolean = true, + cache = sharedCache +): T { const cached = shouldCache && cache.get(path) - if (cached) { - return cached + return cached as T } - const manifest = JSON.parse(readFileSync(path, 'utf8')) + let manifest = JSON.parse(readFileSync(path, 'utf8')) + + // Freeze the manifest so it cannot be modified if we're caching it. + if (shouldCache) { + manifest = deepFreeze(manifest) + } if (shouldCache) { cache.set(path, manifest) @@ -22,14 +55,28 @@ export function loadManifest( return manifest } -export function evalManifest( +export function evalManifest( path: string, - shouldCache: boolean = true -): unknown { + shouldCache: false +): T +export function evalManifest( + path: string, + shouldCache?: boolean, + cache?: Map +): DeepReadonly +export function evalManifest( + path: string, + shouldCache?: true, + cache?: Map +): DeepReadonly +export function evalManifest( + path: string, + shouldCache: boolean = true, + cache = sharedCache +): T { const cached = shouldCache && cache.get(path) - if (cached) { - return cached + return cached as T } const content = readFileSync(path, 'utf8') @@ -37,16 +84,21 @@ export function evalManifest( throw new Error('Manifest file is empty') } - const contextObject = {} + let contextObject = {} runInNewContext(content, contextObject) + // Freeze the context object so it cannot be modified if we're caching it. + if (shouldCache) { + contextObject = deepFreeze(contextObject) + } + if (shouldCache) { cache.set(path, contextObject) } - return contextObject + return contextObject as T } -export function clearManifestCache(path: string): boolean { +export function clearManifestCache(path: string, cache = sharedCache): boolean { return cache.delete(path) } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index ceffa0884776b..b91fb7f9b0a62 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -535,6 +535,7 @@ export default class NextNodeServer extends BaseServer { query, params: match.params, page: match.definition.pathname, + multiZoneDraftMode: this.nextConfig.experimental.multiZoneDraftMode, } ) @@ -1441,6 +1442,7 @@ export default class NextNodeServer extends BaseServer { name: string paths: string[] wasm: { filePath: string; name: string }[] + env: { [key: string]: string } assets?: { filePath: string; name: string }[] } | null { const manifest = this.getMiddlewareManifest() @@ -1482,6 +1484,7 @@ export default class NextNodeServer extends BaseServer { filePath: join(this.distDir, binding.filePath), } }), + env: pageInfo.env, } } @@ -1773,11 +1776,11 @@ export default class NextNodeServer extends BaseServer { return this._cachedPreviewManifest } - const manifest = loadManifest( + this._cachedPreviewManifest = loadManifest( join(this.distDir, PRERENDER_MANIFEST) ) as PrerenderManifest - return (this._cachedPreviewManifest = manifest) + return this._cachedPreviewManifest } protected getRoutesManifest(): NormalizedRouteManifest | undefined { @@ -1865,7 +1868,7 @@ export default class NextNodeServer extends BaseServer { } // For edge to "fetch" we must always provide an absolute URL - const isDataReq = !!query.__nextDataReq + const isNextDataRequest = !!query.__nextDataReq const initialUrl = new URL( getRequestMeta(params.req, 'initURL') || '/', 'http://n' @@ -1876,7 +1879,7 @@ export default class NextNodeServer extends BaseServer { ...params.params, }).toString() - if (isDataReq) { + if (isNextDataRequest) { params.req.headers['x-nextjs-data'] = '1' } initialUrl.search = queryString diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index 9709e740d7d02..c59ad662fb404 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -22,7 +22,7 @@ import { } from './api-utils' import { getCookieParser } from './api-utils/get-cookie-parser' import type { FontManifest, FontConfig } from './font-utils' -import type { LoadComponentsReturnType, ManifestItem } from './load-components' +import type { LoadComponentsReturnType } from './load-components' import type { GetServerSideProps, GetStaticProps, @@ -106,6 +106,7 @@ import { RenderSpan } from './lib/trace/constants' import { ReflectAdapter } from './web/spec-extension/adapters/reflect' import { formatRevalidate } from './lib/revalidate' import { getErrorSource } from '../shared/lib/error-source' +import type { DeepReadonly } from '../shared/lib/deep-readonly' let tryGetPreviewData: typeof import('./api-utils/node/try-get-preview-data').tryGetPreviewData let warn: typeof import('../build/output/log').warn @@ -248,29 +249,29 @@ export type RenderOptsPartial = { ampValidator?: (html: string, pathname: string) => Promise ampSkipValidation?: boolean ampOptimizerConfig?: { [key: string]: any } - isDataReq?: boolean + isNextDataRequest?: boolean params?: ParsedUrlQuery previewProps: __ApiPreviewProps | undefined basePath: string unstable_runtimeJS?: false unstable_JsPreload?: false optimizeFonts: FontConfig - fontManifest?: FontManifest + fontManifest?: DeepReadonly optimizeCss: any nextConfigOutput?: 'standalone' | 'export' nextScriptWorkers: any assetQueryString?: string resolvedUrl?: string resolvedAsPath?: string - clientReferenceManifest?: ClientReferenceManifest - nextFontManifest?: NextFontManifest + clientReferenceManifest?: DeepReadonly + nextFontManifest?: DeepReadonly distDir?: string locale?: string locales?: string[] defaultLocale?: string domainLocales?: DomainLocale[] disableOptimizedLoading?: boolean - supportsDynamicHTML: boolean + supportsDynamicResponse: boolean isBot?: boolean runtime?: ServerRuntime serverComponents?: boolean @@ -451,7 +452,7 @@ export async function renderToHTMLImpl( getStaticProps, getStaticPaths, getServerSideProps, - isDataReq, + isNextDataRequest, params, previewProps, basePath, @@ -647,7 +648,12 @@ export async function renderToHTMLImpl( // Reads of this are cached on the `req` object, so this should resolve // instantly. There's no need to pass this data down from a previous // invoke. - previewData = tryGetPreviewData(req, res, previewProps) + previewData = tryGetPreviewData( + req, + res, + previewProps, + !!renderOpts.multiZoneDraftMode + ) isPreview = previewData !== false } @@ -1083,6 +1089,7 @@ export async function renderToHTMLImpl( }) ) canAccessRes = false + metadata.revalidate = 0 } catch (serverSidePropsError: any) { // remove not found error code to prevent triggering legacy // 404 rendering @@ -1177,7 +1184,7 @@ export async function renderToHTMLImpl( // Avoid rendering page un-necessarily for getServerSideProps data request // and getServerSideProps/getStaticProps redirects - if ((isDataReq && !isSSG) || metadata.isRedirect) { + if ((isNextDataRequest && !isSSG) || metadata.isRedirect) { return new RenderResult(JSON.stringify(props), { metadata, }) @@ -1436,7 +1443,7 @@ export async function renderToHTMLImpl( const dynamicImports = new Set() for (const mod of reactLoadableModules) { - const manifestItem: ManifestItem = reactLoadableManifest[mod] + const manifestItem = reactLoadableManifest[mod] if (manifestItem) { dynamicImportsIds.add(manifestItem.id) diff --git a/packages/next/src/server/request-meta.ts b/packages/next/src/server/request-meta.ts index 9d192f47bf4e3..f197db197deff 100644 --- a/packages/next/src/server/request-meta.ts +++ b/packages/next/src/server/request-meta.ts @@ -224,6 +224,11 @@ type NextQueryMetadata = { __nextSsgPath?: string _nextBubbleNoFallback?: '1' + + /** + * When set to `1`, the request is for the `/_next/data` route using the pages + * router. + */ __nextDataReq?: '1' __nextCustomErrorRender?: '1' [NEXT_RSC_UNION_QUERY]?: string diff --git a/packages/next/src/server/response-cache/types.ts b/packages/next/src/server/response-cache/types.ts index 87fa6e9285413..4f098edf9932c 100644 --- a/packages/next/src/server/response-cache/types.ts +++ b/packages/next/src/server/response-cache/types.ts @@ -81,9 +81,9 @@ interface IncrementalCachedPageValue { } export type IncrementalCacheEntry = { - curRevalidate?: number | false + curRevalidate?: Revalidate // milliseconds to revalidate after - revalidateAfter: number | false + revalidateAfter: Revalidate // -1 here dictates a blocking revalidate should be used isStale?: boolean | -1 value: IncrementalCacheValue | null diff --git a/packages/next/src/server/send-payload.ts b/packages/next/src/server/send-payload.ts index e79c33f8a0b01..de5a45ec1aadf 100644 --- a/packages/next/src/server/send-payload.ts +++ b/packages/next/src/server/send-payload.ts @@ -59,7 +59,9 @@ export async function sendRenderResult({ res.setHeader('X-Powered-By', 'Next.js') } - if (typeof revalidate !== 'undefined') { + // If cache control is already set on the response we don't + // override it to allow users to customize it via next.config + if (typeof revalidate !== 'undefined' && !res.getHeader('Cache-Control')) { res.setHeader( 'Cache-Control', formatRevalidate({ diff --git a/packages/next/src/server/web-server.ts b/packages/next/src/server/web-server.ts index 797079023b4e2..7bdf915f00681 100644 --- a/packages/next/src/server/web-server.ts +++ b/packages/next/src/server/web-server.ts @@ -3,7 +3,6 @@ import type RenderResult from './render-result' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' import type { Params } from '../shared/lib/router/utils/route-matcher' import type { LoadComponentsReturnType } from './load-components' -import type { PrerenderManifest } from '../build' import type { LoadedRenderOpts, MiddlewareRoutingItem, @@ -33,6 +32,7 @@ import type { PAGE_TYPES } from '../lib/page-types' import type { Rewrite } from '../lib/load-custom-routes' import { buildCustomRoute } from '../lib/build-custom-route' import { UNDERSCORE_NOT_FOUND_ROUTE } from '../api/constants' +import { getEdgePreviewProps } from './web/get-edge-preview-props' interface WebServerOptions extends Options { webServerConfig: { @@ -48,7 +48,6 @@ interface WebServerOptions extends Options { | typeof import('./app-render/app-render').renderToHTMLOrFlight | undefined incrementalCacheHandler?: any - prerenderManifest: PrerenderManifest | undefined interceptionRouteRewrites?: Rewrite[] } } @@ -132,19 +131,13 @@ export default class NextWebServer extends BaseServer { } protected getPrerenderManifest() { - const { prerenderManifest } = this.serverOptions.webServerConfig - if (this.renderOpts?.dev || !prerenderManifest) { - return { - version: -1 as any, // letting us know this doesn't conform to spec - routes: {}, - dynamicRoutes: {}, - notFoundRoutes: [], - preview: { - previewModeId: 'development-id', - } as any, // `preview` is special case read in next-dev-server - } + return { + version: -1 as any, // letting us know this doesn't conform to spec + routes: {}, + dynamicRoutes: {}, + notFoundRoutes: [], + preview: getEdgePreviewProps(), } - return prerenderManifest } protected getNextFontManifest() { diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index b14ea7cf6e6a7..db813b513242a 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -1,6 +1,5 @@ import type { RequestData, FetchEventResult } from './types' import type { RequestInit } from './spec-extension/request' -import type { PrerenderManifest } from '../../build' import { PageSignatureError } from './error' import { fromNodeOutgoingHttpHeaders } from './utils' import { NextFetchEvent } from './spec-extension/fetch-event' @@ -19,6 +18,7 @@ import { requestAsyncStorage } from '../../client/components/request-async-stora import { getTracer } from '../lib/trace/tracer' import type { TextMapGetter } from 'next/dist/compiled/@opentelemetry/api' import { MiddlewareSpan } from '../lib/trace/constants' +import { getEdgePreviewProps } from './get-edge-preview-props' export class NextRequestHint extends NextRequest { sourcePage: string @@ -90,10 +90,6 @@ export async function adapter( // TODO-APP: use explicit marker for this const isEdgeRendering = typeof self.__BUILD_MANIFEST !== 'undefined' - const prerenderManifest: PrerenderManifest | undefined = - typeof self.__PRERENDER_MANIFEST === 'string' - ? JSON.parse(self.__PRERENDER_MANIFEST) - : undefined params.request.url = normalizeRscURL(params.request.url) @@ -126,9 +122,9 @@ export async function adapter( const buildId = requestUrl.buildId requestUrl.buildId = '' - const isDataReq = params.request.headers['x-nextjs-data'] + const isNextDataRequest = params.request.headers['x-nextjs-data'] - if (isDataReq && requestUrl.pathname === '/index') { + if (isNextDataRequest && requestUrl.pathname === '/index') { requestUrl.pathname = '/' } @@ -170,7 +166,7 @@ export async function adapter( * need to know about this property neither use it. We add it for testing * purposes. */ - if (isDataReq) { + if (isNextDataRequest) { Object.defineProperty(request, '__isData', { enumerable: false, value: true, @@ -197,9 +193,7 @@ export async function adapter( routes: {}, dynamicRoutes: {}, notFoundRoutes: [], - preview: { - previewModeId: 'development-id', - } as any, // `preview` is special case read in next-dev-server + preview: getEdgePreviewProps(), } }, }) @@ -233,11 +227,7 @@ export async function adapter( cookiesFromResponse = cookies }, // @ts-expect-error: TODO: investigate why previewProps isn't on RenderOpts - previewProps: prerenderManifest?.preview || { - previewModeId: 'development-id', - previewModeEncryptionKey: '', - previewModeSigningKey: '', - }, + previewProps: getEdgePreviewProps(), }, }, () => params.handler(request, event) @@ -288,7 +278,7 @@ export async function adapter( ) if ( - isDataReq && + isNextDataRequest && // if the rewrite is external and external rewrite // resolving config is enabled don't add this header // so the upstream app can set it instead @@ -332,7 +322,7 @@ export async function adapter( * it may end up with CORS error. Instead we map to an internal header so * the client knows the destination. */ - if (isDataReq) { + if (isNextDataRequest) { response.headers.delete('Location') response.headers.set( 'x-nextjs-redirect', diff --git a/packages/next/src/server/web/edge-route-module-wrapper.ts b/packages/next/src/server/web/edge-route-module-wrapper.ts index 68a14fc80d0e2..aa5d68c1c8e37 100644 --- a/packages/next/src/server/web/edge-route-module-wrapper.ts +++ b/packages/next/src/server/web/edge-route-module-wrapper.ts @@ -3,7 +3,6 @@ import type { AppRouteRouteHandlerContext, AppRouteRouteModule, } from '../future/route-modules/app-route/module' -import type { PrerenderManifest } from '../../build' import './globals' @@ -14,6 +13,7 @@ import type { NextFetchEvent } from './spec-extension/fetch-event' import { internal_getCurrentFunctionWaitUntil } from './internal-edge-wait-until' import { getUtils } from '../server-utils' import { searchParamsToUrlQuery } from '../../shared/lib/router/utils/querystring' +import { getEdgePreviewProps } from './get-edge-preview-props' type WrapOptions = Partial> @@ -82,10 +82,7 @@ export class EdgeRouteModuleWrapper { searchParamsToUrlQuery(request.nextUrl.searchParams) ) - const prerenderManifest: PrerenderManifest | undefined = - typeof self.__PRERENDER_MANIFEST === 'string' - ? JSON.parse(self.__PRERENDER_MANIFEST) - : undefined + const previewProps = getEdgePreviewProps() // Create the context for the handler. This contains the params from the // match (if any). @@ -95,15 +92,11 @@ export class EdgeRouteModuleWrapper { version: 4, routes: {}, dynamicRoutes: {}, - preview: prerenderManifest?.preview || { - previewModeEncryptionKey: '', - previewModeId: 'development-id', - previewModeSigningKey: '', - }, + preview: previewProps, notFoundRoutes: [], }, renderOpts: { - supportsDynamicHTML: true, + supportsDynamicResponse: true, // App Route's cannot be postponed. experimental: { ppr: false }, }, diff --git a/packages/next/src/server/web/get-edge-preview-props.ts b/packages/next/src/server/web/get-edge-preview-props.ts new file mode 100644 index 0000000000000..dfca7c5c85539 --- /dev/null +++ b/packages/next/src/server/web/get-edge-preview-props.ts @@ -0,0 +1,16 @@ +/** + * In edge runtime, these props directly accessed from environment variables. + * - local: env vars will be injected through edge-runtime as runtime env vars + * - deployment: env vars will be replaced by edge build pipeline + */ +export function getEdgePreviewProps() { + return { + previewModeId: + process.env.NODE_ENV === 'production' + ? process.env.__NEXT_PREVIEW_MODE_ID! + : 'development-id', + previewModeSigningKey: process.env.__NEXT_PREVIEW_MODE_SIGNING_KEY || '', + previewModeEncryptionKey: + process.env.__NEXT_PREVIEW_MODE_ENCRYPTION_KEY || '', + } +} diff --git a/packages/next/src/server/web/sandbox/context.ts b/packages/next/src/server/web/sandbox/context.ts index a872f4dd31c51..899758cb46f2a 100644 --- a/packages/next/src/server/web/sandbox/context.ts +++ b/packages/next/src/server/web/sandbox/context.ts @@ -107,9 +107,14 @@ async function loadWasm( return modules } -function buildEnvironmentVariablesFrom(): Record { +function buildEnvironmentVariablesFrom( + injectedEnvironments: Record +): Record { const pairs = Object.keys(process.env).map((key) => [key, process.env[key]]) const env = Object.fromEntries(pairs) + for (const key of Object.keys(injectedEnvironments)) { + env[key] = injectedEnvironments[key] + } env.NEXT_RUNTIME = 'edge' return env } @@ -122,15 +127,16 @@ Learn more: https://nextjs.org/docs/api-reference/edge-runtime`) throw error } -function createProcessPolyfill() { - const processPolyfill = { env: buildEnvironmentVariablesFrom() } - const overridenValue: Record = {} +function createProcessPolyfill(env: Record) { + const processPolyfill = { env: buildEnvironmentVariablesFrom(env) } + const overriddenValue: Record = {} + for (const key of Object.keys(process)) { if (key === 'env') continue Object.defineProperty(processPolyfill, key, { get() { - if (overridenValue[key] !== undefined) { - return overridenValue[key] + if (overriddenValue[key] !== undefined) { + return overriddenValue[key] } if (typeof (process as any)[key] === 'function') { return () => throwUnsupportedAPIError(`process.${key}`) @@ -138,7 +144,7 @@ function createProcessPolyfill() { return undefined }, set(value) { - overridenValue[key] = value + overriddenValue[key] = value }, enumerable: false, }) @@ -244,14 +250,15 @@ export const requestStore = new AsyncLocalStorage<{ async function createModuleContext(options: ModuleContextOptions) { const warnedEvals = new Set() const warnedWasmCodegens = new Set() - const wasm = await loadWasm(options.edgeFunctionEntry.wasm ?? []) + const { edgeFunctionEntry } = options + const wasm = await loadWasm(edgeFunctionEntry.wasm ?? []) const runtime = new EdgeRuntime({ codeGeneration: process.env.NODE_ENV !== 'production' ? { strings: true, wasm: true } : undefined, extend: (context) => { - context.process = createProcessPolyfill() + context.process = createProcessPolyfill(edgeFunctionEntry.env) Object.defineProperty(context, 'require', { enumerable: false, @@ -470,7 +477,7 @@ interface ModuleContextOptions { onWarning: (warn: Error) => void useCache: boolean distDir: string - edgeFunctionEntry: Pick + edgeFunctionEntry: Pick } function getModuleContextShared(options: ModuleContextOptions) { diff --git a/packages/next/src/server/web/spec-extension/cookies.ts b/packages/next/src/server/web/spec-extension/cookies.ts index bfa953c5c9e8c..1fd37f5075b2b 100644 --- a/packages/next/src/server/web/spec-extension/cookies.ts +++ b/packages/next/src/server/web/spec-extension/cookies.ts @@ -1,4 +1,5 @@ export { RequestCookies, ResponseCookies, + stringifyCookie, } from 'next/dist/compiled/@edge-runtime/cookies' diff --git a/packages/next/src/server/web/spec-extension/response.ts b/packages/next/src/server/web/spec-extension/response.ts index db14979fb8d91..0e855ef701de6 100644 --- a/packages/next/src/server/web/spec-extension/response.ts +++ b/packages/next/src/server/web/spec-extension/response.ts @@ -1,6 +1,8 @@ +import { stringifyCookie } from '../../web/spec-extension/cookies' import type { I18NConfig } from '../../config-shared' import { NextURL } from '../next-url' import { toNodeOutgoingHttpHeaders, validateURL } from '../utils' +import { ReflectAdapter } from './adapters/reflect' import { ResponseCookies } from './cookies' @@ -41,11 +43,43 @@ export class NextResponse extends Response { constructor(body?: BodyInit | null, init: ResponseInit = {}) { super(body, init) + const headers = this.headers + const cookies = new ResponseCookies(headers) + + const cookiesProxy = new Proxy(cookies, { + get(target, prop, receiver) { + switch (prop) { + case 'delete': + case 'set': { + return (...args: [string, string]) => { + const result = Reflect.apply(target[prop], target, args) + const newHeaders = new Headers(headers) + + if (result instanceof ResponseCookies) { + headers.set( + 'x-middleware-set-cookie', + result + .getAll() + .map((cookie) => stringifyCookie(cookie)) + .join(',') + ) + } + + handleMiddlewareField(init, newHeaders) + return result + } + } + default: + return ReflectAdapter.get(target, prop, receiver) + } + }, + }) + this[INTERNALS] = { - cookies: new ResponseCookies(this.headers), + cookies: cookiesProxy, url: init.url ? new NextURL(init.url, { - headers: toNodeOutgoingHttpHeaders(this.headers), + headers: toNodeOutgoingHttpHeaders(headers), nextConfig: init.nextConfig, }) : undefined, diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index ef10e466fe07f..00996c7d188f2 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -68,15 +68,6 @@ function revalidate(tag: string, expression: string) { store.revalidatedTags.push(tag) } - if (!store.pendingRevalidates) { - store.pendingRevalidates = {} - } - store.pendingRevalidates[tag] = store.incrementalCache - .revalidateTag?.(tag) - .catch((err) => { - console.error(`revalidate failed for ${tag}`, err) - }) - // TODO: only revalidate if the path matches store.pathWasRevalidated = true } 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 c75133826dcbd..54ab49270dbc5 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -259,15 +259,19 @@ export function unstable_cache( cb, ...args ) - cacheNewResult( - result, - incrementalCache, - cacheKey, - tags, - options.revalidate, - fetchIdx, - fetchUrl - ) + + if (!store.isDraftMode) { + cacheNewResult( + result, + incrementalCache, + cacheKey, + tags, + options.revalidate, + fetchIdx, + fetchUrl + ) + } + return result } else { noStoreFetchIdx += 1 diff --git a/packages/next/src/shared/lib/deep-freeze.test.ts b/packages/next/src/shared/lib/deep-freeze.test.ts new file mode 100644 index 0000000000000..a487c5f5866d0 --- /dev/null +++ b/packages/next/src/shared/lib/deep-freeze.test.ts @@ -0,0 +1,41 @@ +import { deepFreeze } from './deep-freeze' + +describe('freeze', () => { + it('should freeze an object', () => { + const obj = { a: 1, b: 2 } + deepFreeze(obj) + expect(Object.isFrozen(obj)).toBe(true) + }) + + it('should freeze an array', () => { + const arr = [1, 2, 3] + deepFreeze(arr) + expect(Object.isFrozen(arr)).toBe(true) + }) + + it('should freeze nested objects', () => { + const obj = { a: { b: 2 }, c: 3 } + deepFreeze(obj) + expect(Object.isFrozen(obj)).toBe(true) + expect(Object.isFrozen(obj.a)).toBe(true) + }) + + it('should freeze nested arrays', () => { + const arr = [ + [1, 2], + [3, 4], + ] + deepFreeze(arr) + expect(Object.isFrozen(arr)).toBe(true) + expect(Object.isFrozen(arr[0])).toBe(true) + expect(Object.isFrozen(arr[1])).toBe(true) + }) + + it('should freeze nested objects and arrays', () => { + const obj = { a: [1, 2], b: { c: 3 } } + deepFreeze(obj) + expect(Object.isFrozen(obj)).toBe(true) + expect(Object.isFrozen(obj.a)).toBe(true) + expect(Object.isFrozen(obj.b)).toBe(true) + }) +}) diff --git a/packages/next/src/shared/lib/deep-freeze.ts b/packages/next/src/shared/lib/deep-freeze.ts new file mode 100644 index 0000000000000..cc51e767236f5 --- /dev/null +++ b/packages/next/src/shared/lib/deep-freeze.ts @@ -0,0 +1,32 @@ +import type { DeepReadonly } from './deep-readonly' + +/** + * Recursively freezes an object and all of its properties. This prevents the + * object from being modified at runtime. When the JS runtime is running in + * strict mode, any attempts to modify a frozen object will throw an error. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze + * @param obj The object to freeze. + */ +export function deepFreeze(obj: T): DeepReadonly { + // If the object is already frozen, there's no need to freeze it again. + if (Object.isFrozen(obj)) return obj as DeepReadonly + + // An array is an object, but we also want to freeze each element in the array + // as well. + if (Array.isArray(obj)) { + for (const item of obj) { + if (!item || typeof item !== 'object') continue + deepFreeze(item) + } + + return Object.freeze(obj) as DeepReadonly + } + + for (const value of Object.values(obj)) { + if (!value || typeof value !== 'object') continue + deepFreeze(value) + } + + return Object.freeze(obj) as DeepReadonly +} diff --git a/packages/next/src/shared/lib/deep-readonly.ts b/packages/next/src/shared/lib/deep-readonly.ts new file mode 100644 index 0000000000000..f6b700a6b6bd4 --- /dev/null +++ b/packages/next/src/shared/lib/deep-readonly.ts @@ -0,0 +1,12 @@ +/** + * A type that represents a deeply readonly object. This is similar to + * TypeScript's `Readonly` type, but it recursively applies the `readonly` + * modifier to all properties of an object and all elements of arrays. + */ +export type DeepReadonly = T extends (infer R)[] + ? ReadonlyArray> + : T extends object + ? { + readonly [K in keyof T]: DeepReadonly + } + : T 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 33b2dce0f06ba..a1c060ef8f2fa 100644 --- a/packages/next/src/shared/lib/html-context.shared-runtime.ts +++ b/packages/next/src/shared/lib/html-context.shared-runtime.ts @@ -3,6 +3,7 @@ import type { ServerRuntime } from 'next/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' +import type { DeepReadonly } from './deep-readonly' import { createContext, useContext } from 'react' @@ -45,7 +46,7 @@ export type HtmlProps = { runtime?: ServerRuntime hasConcurrentFeatures?: boolean largePageDataBytes?: number - nextFontManifest?: NextFontManifest + nextFontManifest?: DeepReadonly } export const HtmlContext = createContext(undefined) diff --git a/packages/next/src/shared/lib/normalized-asset-prefix.test.ts b/packages/next/src/shared/lib/normalized-asset-prefix.test.ts new file mode 100644 index 0000000000000..2c6171c0eadde --- /dev/null +++ b/packages/next/src/shared/lib/normalized-asset-prefix.test.ts @@ -0,0 +1,44 @@ +import { normalizedAssetPrefix } from './normalized-asset-prefix' + +describe('normalizedAssetPrefix', () => { + it('should return an empty string when assetPrefix is nullish', () => { + expect(normalizedAssetPrefix(undefined)).toBe('') + }) + + it('should return an empty string when assetPrefix is an empty string', () => { + expect(normalizedAssetPrefix('')).toBe('') + }) + + it('should return an empty string when assetPrefix is a single slash', () => { + expect(normalizedAssetPrefix('/')).toBe('') + }) + + // we expect an empty string because it could be an unnecessary trailing slash + it('should remove leading slash(es) when assetPrefix has more than one', () => { + expect(normalizedAssetPrefix('///path/to/asset')).toBe('/path/to/asset') + }) + + it('should not remove the leading slash when assetPrefix has only one', () => { + expect(normalizedAssetPrefix('/path/to/asset')).toBe('/path/to/asset') + }) + + it('should add a leading slash when assetPrefix is missing one', () => { + expect(normalizedAssetPrefix('path/to/asset')).toBe('/path/to/asset') + }) + + it('should remove all trailing slash(es) when assetPrefix has one', () => { + expect(normalizedAssetPrefix('/path/to/asset///')).toBe('/path/to/asset') + }) + + it('should return the URL when assetPrefix is a URL', () => { + expect(normalizedAssetPrefix('https://example.com/path/to/asset')).toBe( + 'https://example.com/path/to/asset' + ) + }) + + it('should not leave a trailing slash when assetPrefix is a URL with no pathname', () => { + expect(normalizedAssetPrefix('https://example.com')).toBe( + 'https://example.com' + ) + }) +}) diff --git a/packages/next/src/shared/lib/normalized-asset-prefix.ts b/packages/next/src/shared/lib/normalized-asset-prefix.ts index b7da7771915a6..352e836698c5c 100644 --- a/packages/next/src/shared/lib/normalized-asset-prefix.ts +++ b/packages/next/src/shared/lib/normalized-asset-prefix.ts @@ -1,16 +1,19 @@ export function normalizedAssetPrefix(assetPrefix: string | undefined): string { - const escapedAssetPrefix = assetPrefix?.replace(/^\/+/, '') || false + // remove all leading slashes and trailing slashes + const escapedAssetPrefix = assetPrefix?.replace(/^\/+|\/+$/g, '') || false - // assetPrefix as a url - if (escapedAssetPrefix && escapedAssetPrefix.startsWith('://')) { - return escapedAssetPrefix.split('://', 2)[1] - } - - // assetPrefix is set to `undefined` or '/' + // if an assetPrefix was '/', we return empty string + // because it could be an unnecessary trailing slash if (!escapedAssetPrefix) { return '' } - // assetPrefix is a common path but escaped so let's add one leading slash + if (URL.canParse(escapedAssetPrefix)) { + const url = new URL(escapedAssetPrefix).toString() + return url.endsWith('/') ? url.slice(0, -1) : url + } + + // assuming assetPrefix here is a pathname-style, + // restore the leading slash return `/${escapedAssetPrefix}` } diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 0baee76b8490d..9f04635237ad0 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.7", + "version": "14.2.12", "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 7443ab5183a93..354cd6335de68 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "14.2.7", + "version": "14.2.12", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "14.2.7", + "next": "14.2.12", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 905bb77f9af37..1c2d4cac4ba7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -744,13 +744,16 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 14.2.7 + specifier: 14.2.12 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 version: 1.3.3 + '@typescript-eslint/eslint-plugin': + specifier: ^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0 + version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.31.0)(typescript@4.8.2) '@typescript-eslint/parser': - specifier: ^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0 + specifier: ^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0 version: 6.14.0(eslint@8.31.0)(typescript@4.8.2) eslint: specifier: ^7.23.0 || ^8.0.0 @@ -806,7 +809,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 14.2.7 + specifier: 14.2.12 version: link:../next-env '@swc/helpers': specifier: 0.5.5 @@ -927,16 +930,16 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/polyfill-module': - specifier: 14.2.7 + specifier: 14.2.12 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 14.2.7 + specifier: 14.2.12 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 14.2.7 + specifier: 14.2.12 version: link:../react-refresh-utils '@next/swc': - specifier: 14.2.7 + specifier: 14.2.12 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1551,7 +1554,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 14.2.7 + specifier: 14.2.12 version: link:../next outdent: specifier: 0.8.0 @@ -3670,6 +3673,16 @@ packages: jsdoc-type-pratt-parser: 4.0.0 dev: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.31.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.31.0 + eslint-visitor-keys: 3.4.3 + dev: false + /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3683,7 +3696,6 @@ packages: /@eslint-community/regexpp@4.10.0: resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true /@eslint-community/regexpp@4.5.1: resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} @@ -7094,13 +7106,8 @@ packages: '@types/node': 20.2.5 dev: true - /@types/semver@7.5.0: - resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} - dev: true - /@types/semver@7.5.6: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} - dev: true /@types/send@0.14.4: resolution: {integrity: sha512-SCVCRRjSbpwoKgA34wK8cq14OUPu4qrKigO85/ZH6J04NGws37khLtq7YQr17zyOH01p4T5oy8e1TxEzql01Pg==} @@ -7222,6 +7229,35 @@ packages: dev: true optional: true + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.31.0)(typescript@4.8.2): + resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.14.0(eslint@8.31.0)(typescript@4.8.2) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/type-utils': 6.14.0(eslint@8.31.0)(typescript@4.8.2) + '@typescript-eslint/utils': 6.14.0(eslint@8.31.0)(typescript@4.8.2) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4 + eslint: 8.31.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.6.2 + ts-api-utils: 1.0.1(typescript@4.8.2) + typescript: 4.8.2 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.2.2): resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -7308,6 +7344,26 @@ packages: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/visitor-keys': 6.14.0 + /@typescript-eslint/type-utils@6.14.0(eslint@8.31.0)(typescript@4.8.2): + resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.14.0(typescript@4.8.2) + '@typescript-eslint/utils': 6.14.0(eslint@8.31.0)(typescript@4.8.2) + debug: 4.3.4 + eslint: 8.31.0 + ts-api-utils: 1.0.1(typescript@4.8.2) + typescript: 4.8.2 + transitivePeerDependencies: + - supports-color + dev: false + /@typescript-eslint/type-utils@6.14.0(eslint@8.56.0)(typescript@5.2.2): resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} engines: {node: ^16.0.0 || >=18.0.0} @@ -7372,7 +7428,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.2 ts-api-utils: 1.0.1(typescript@4.8.2) typescript: 4.8.2 transitivePeerDependencies: @@ -7393,7 +7449,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.2 ts-api-utils: 1.0.1(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -7420,6 +7476,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.14.0(eslint@8.31.0)(typescript@4.8.2): + resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.31.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@4.8.2) + eslint: 8.31.0 + semver: 7.6.2 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@typescript-eslint/utils@6.14.0(eslint@8.56.0)(typescript@5.2.2): resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -7428,12 +7503,12 @@ packages: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 + '@types/semver': 7.5.6 '@typescript-eslint/scope-manager': 6.14.0 '@typescript-eslint/types': 6.14.0 '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.2.2) eslint: 8.56.0 - semver: 7.5.4 + semver: 7.6.2 transitivePeerDependencies: - supports-color - typescript @@ -12024,7 +12099,7 @@ packages: dependencies: acorn: 8.11.3 acorn-jsx: 5.3.2(acorn@8.11.3) - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.3 dev: false /espree@9.6.1: @@ -13408,7 +13483,6 @@ packages: /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true /graphql@16.7.1: resolution: {integrity: sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==} @@ -13951,7 +14025,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.1.1 transitivePeerDependencies: - supports-color dev: true @@ -14246,7 +14320,7 @@ packages: run-async: 2.4.1 rxjs: 7.8.1 string-width: 4.2.3 - strip-ansi: 6.0.1 + strip-ansi: 6.0.0 through: 2.3.8 dev: true @@ -15006,7 +15080,7 @@ packages: '@babel/parser': 7.22.5 '@istanbuljs/schema': 0.1.2 istanbul-lib-coverage: 3.2.0 - semver: 7.5.4 + semver: 7.6.2 transitivePeerDependencies: - supports-color dev: true @@ -15721,7 +15795,7 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.5.4 + semver: 7.6.2 transitivePeerDependencies: - supports-color dev: true @@ -16780,6 +16854,7 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 + dev: true /lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} @@ -22285,6 +22360,13 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 + dev: true + + /semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + requiresBuild: true /send@0.17.1: resolution: {integrity: sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==} @@ -25366,6 +25448,7 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} diff --git a/run-tests.js b/run-tests.js index 32e0a5a441346..82af76c3e4707 100644 --- a/run-tests.js +++ b/run-tests.js @@ -1,3 +1,4 @@ +// @ts-check const os = require('os') const path = require('path') const _glob = require('glob') @@ -5,7 +6,12 @@ const { existsSync } = require('fs') const fsp = require('fs/promises') const nodeFetch = require('node-fetch') const vercelFetch = require('@vercel/fetch') -const fetch = vercelFetch(nodeFetch) + +/** @type {import('@vercel/fetch').Fetch} */ +const fetch = + // @ts-expect-error: TS doesn't understand default exports + vercelFetch(nodeFetch) + const { promisify } = require('util') const { Sema } = require('async-sema') const { spawn, exec: execOrig } = require('child_process') @@ -14,7 +20,9 @@ const glob = promisify(_glob) const exec = promisify(execOrig) const core = require('@actions/core') const { getTestFilter } = require('./test/get-test-filter') +const mockSpan = require('./.github/actions/next-stats-action/src/util/mock-trace') +// @ts-expect-error: missing type declarations for yargs let argv = require('yargs/yargs')(process.argv.slice(2)) .string('type') .string('test-pattern') @@ -78,11 +86,6 @@ const testFilters = { e2e: 'test/e2e/', } -const mockTrace = () => ({ - traceAsyncFn: (fn) => fn(mockTrace()), - traceChild: () => mockTrace(), -}) - // which types we have configured to run separate const configuredTestTypes = Object.values(testFilters) const errorsPerTests = new Map() @@ -317,6 +320,7 @@ async function main() { const groupTotal = parseInt(groupParts[1], 10) if (prevTimings) { + /** @type {TestFile[][]} */ const groups = [[]] const groupTimes = [0] @@ -387,7 +391,7 @@ ${ENDGROUP}`) console.log(`${GROUP}Creating Next.js install for isolated tests`) const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest' const { installDir, pkgPaths, tmpRepoDir } = await createNextInstall({ - parentSpan: mockTrace(), + parentSpan: mockSpan(), dependencies: { react: reactVersion, 'react-dom': reactVersion, @@ -463,7 +467,9 @@ ${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 - JEST_JUNIT_OUTPUT_NAME: test.file.replaceAll('/', '_'), + JEST_JUNIT_OUTPUT_NAME: + // @ts-expect-error missing lib: es2021 + 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 // [NOTE]: This won't affect if junit reporter is not enabled @@ -550,9 +556,12 @@ ${ENDGROUP}`) } outputSema.release() } + 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/scripts/check-examples.sh b/scripts/check-examples.sh index 8977bf6c7e640..fe24792596277 100755 --- a/scripts/check-examples.sh +++ b/scripts/check-examples.sh @@ -8,7 +8,11 @@ for folder in examples/* ; do ' | sponge $folder/package.json fi if [ -f "$folder/tsconfig.json" ]; then - cp packages/create-next-app/templates/default/ts/next-env.d.ts $folder/next-env.d.ts + if [ -d "$folder/app" ]; then + cp packages/create-next-app/templates/app/ts/next-env.d.ts $folder/next-env.d.ts + else + cp packages/create-next-app/templates/default/ts/next-env.d.ts $folder/next-env.d.ts + fi fi if [ ! -f "$folder/.gitignore" ]; then cp packages/create-next-app/templates/default/js/gitignore $folder/.gitignore; diff --git a/scripts/test-new-tests.mjs b/scripts/test-new-tests.mjs index 680fcafff2f7c..4a68ce024588f 100644 --- a/scripts/test-new-tests.mjs +++ b/scripts/test-new-tests.mjs @@ -39,16 +39,42 @@ async function main() { return } - try { - await execa('git remote set-branches --add origin canary', EXECA_OPTS_STDIO) - await execa('git fetch origin canary --depth=20', EXECA_OPTS_STDIO) - } catch (err) { - console.error(await execa('git remote -v', EXECA_OPTS_STDIO)) - console.error(`Failed to fetch origin/canary`, err) + let diffRevision + if ( + process.env.GITHUB_ACTIONS === 'true' && + process.env.GITHUB_EVENT_NAME === 'pull_request' + ) { + // GH Actions for `pull_request` run on the merge commit so HEAD~1: + // 1. includes all changes in the PR + // e.g. in + // A-B-C-main - F + // \ / + // D-E-branch + // GH actions for `branch` runs on F, so a diff for HEAD~1 includes the diff of D and E combined + // 2. Includes all changes of the commit for pushes + diffRevision = 'HEAD~1' + } else { + try { + await execa( + 'git remote set-branches --add origin canary', + EXECA_OPTS_STDIO + ) + await execa('git fetch origin canary --depth=20', EXECA_OPTS_STDIO) + } catch (err) { + console.error(await execa('git remote -v', EXECA_OPTS_STDIO)) + console.error(`Failed to fetch origin/canary`, err) + } + // TODO: We should diff against the merge base with origin/canary not directly against origin/canary. + // A --- B ---- origin/canary + // \ + // \-- C ---- HEAD + // `git diff origin/canary` includes B and C + // But we should only include C. + diffRevision = 'origin/canary' } const changesResult = await execa( - `git diff origin/canary --name-only`, + `git diff ${diffRevision} --name-only`, EXECA_OPTS ).catch((err) => { console.error(err) diff --git a/test/.stats-app/stats-config.js b/test/.stats-app/stats-config.js index e2a8202f12879..acc434ec1a1a1 100644 --- a/test/.stats-app/stats-config.js +++ b/test/.stats-app/stats-config.js @@ -79,7 +79,9 @@ module.exports = { appStartCommand: 'NEXT_TELEMETRY_DISABLED=1 pnpm next start --port $PORT', appDevCommand: 'NEXT_TELEMETRY_DISABLED=1 pnpm next --port $PORT', mainRepo: 'vercel/next.js', - mainBranch: 'canary', + // BACKPORT: we can't base off of canary here + // mainBranch: 'canary', + mainBranch: '14-2-1', autoMergeMain: true, configs: [ { diff --git a/test/development/acceptance-app/hydration-error.test.ts b/test/development/acceptance-app/hydration-error.test.ts index 5a47289516d23..42d4291fced62 100644 --- a/test/development/acceptance-app/hydration-error.test.ts +++ b/test/development/acceptance-app/hydration-error.test.ts @@ -54,9 +54,9 @@ describe('Error overlay for hydration errors', () => { if (isTurbopack) { expect(pseudoHtml).toMatchInlineSnapshot(` "... - - - + + +
@@ -251,16 +251,16 @@ describe('Error overlay for hydration errors', () => { if (isTurbopack) { expect(pseudoHtml).toMatchInlineSnapshot(` - "... - - - - - -
-
- "only"" - `) + "... + + + + + +
+
+ "only"" + `) } else { expect(pseudoHtml).toMatchInlineSnapshot(` " @@ -643,44 +643,45 @@ describe('Error overlay for hydration errors', () => { const fullPseudoHtml = await session.getRedboxComponentStack() if (isTurbopack) { expect(fullPseudoHtml).toMatchInlineSnapshot(` - " - - - - - - - - - - - - - - - - - - - - - - - - - - - + " + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
-
-
- -

- - "server" - "client"" - `) + +

+ + "server" + "client"" + `) } else { expect(fullPseudoHtml).toMatchInlineSnapshot(` " diff --git a/test/development/app-dir/hmr-asset-prefix-full-url/app/layout.tsx b/test/development/app-dir/hmr-asset-prefix-full-url/app/layout.tsx new file mode 100644 index 0000000000000..a3a86a5ca1e12 --- /dev/null +++ b/test/development/app-dir/hmr-asset-prefix-full-url/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Root({ children }) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/hmr-asset-prefix-full-url/app/page.tsx b/test/development/app-dir/hmr-asset-prefix-full-url/app/page.tsx new file mode 100644 index 0000000000000..fb9b4085fcd27 --- /dev/null +++ b/test/development/app-dir/hmr-asset-prefix-full-url/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

before edit

+} diff --git a/test/development/app-dir/hmr-asset-prefix-full-url/asset-prefix.test.ts b/test/development/app-dir/hmr-asset-prefix-full-url/asset-prefix.test.ts new file mode 100644 index 0000000000000..42bb67e99d429 --- /dev/null +++ b/test/development/app-dir/hmr-asset-prefix-full-url/asset-prefix.test.ts @@ -0,0 +1,32 @@ +import { createNext } from 'e2e-utils' +import { findPort, retry } from 'next-test-utils' + +describe('app-dir assetPrefix full URL', () => { + let next, forcedPort + beforeAll(async () => { + forcedPort = ((await findPort()) ?? '54321').toString() + + next = await createNext({ + files: __dirname, + forcedPort, + nextConfig: { + assetPrefix: `http://localhost:${forcedPort}`, + }, + }) + }) + afterAll(() => next.destroy()) + + it('should not break HMR when asset prefix set to full URL', async () => { + const browser = await next.browser('/') + const text = await browser.elementByCss('p').text() + expect(text).toBe('before edit') + + await next.patchFile('app/page.tsx', (content) => { + return content.replace('before', 'after') + }) + + await retry(async () => { + expect(await browser.elementByCss('p').text()).toBe('after edit') + }) + }) +}) diff --git a/test/development/basic/next-rs-api.test.ts b/test/development/basic/next-rs-api.test.ts index 04cde12ede22d..0e11eb60cf744 100644 --- a/test/development/basic/next-rs-api.test.ts +++ b/test/development/basic/next-rs-api.test.ts @@ -218,6 +218,13 @@ describe('next.rs api', () => { hasRewrites: false, middlewareMatchers: undefined, }), + buildId: 'development', + encryptionKey: '12345', + previewProps: { + previewModeId: 'development', + previewModeEncryptionKey: '12345', + previewModeSigningKey: '12345', + }, }) projectUpdateSubscription = filterMapAsyncIterator( project.updateInfoSubscribe(1000), diff --git a/test/e2e/app-dir/app-external/app-external.test.ts b/test/e2e/app-dir/app-external/app-external.test.ts index ca7aede529d34..06507b27c5d44 100644 --- a/test/e2e/app-dir/app-external/app-external.test.ts +++ b/test/e2e/app-dir/app-external/app-external.test.ts @@ -1,4 +1,4 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check, hasRedbox, retry, shouldRunTurboDevTest } from 'next-test-utils' async function resolveStreamResponse(response: any, onData?: any) { @@ -15,9 +15,8 @@ async function resolveStreamResponse(response: any, onData?: any) { return result } -createNextDescribe( - 'app dir - external dependency', - { +describe('app dir - external dependency', () => { + const { next, skipped } = nextTestSetup({ files: __dirname, dependencies: { swr: 'latest', @@ -34,288 +33,283 @@ createNextDescribe( startCommand: (global as any).isNextDev ? 'pnpm dev' : 'pnpm start', buildCommand: 'pnpm build', skipDeployment: true, - }, - ({ next }) => { - it('should be able to opt-out 3rd party packages being bundled in server components', async () => { - await next.fetch('/react-server/optout').then(async (response) => { - const result = await resolveStreamResponse(response) - expect(result).toContain('Server: index.default') - expect(result).toContain('Server subpath: subpath.default') - expect(result).toContain('Client: index.default') - expect(result).toContain('Client subpath: subpath.default') - expect(result).toContain('opt-out-react-version: 18.3.1') - }) - }) - - it('should handle external async module libraries correctly', async () => { - const clientHtml = await next.render('/external-imports/client') - const serverHtml = await next.render('/external-imports/server') - const sharedHtml = await next.render('/shared-esm-dep') - - const browser = await next.browser('/external-imports/client') - const browserClientText = await browser.elementByCss('#content').text() - - function containClientContent(content) { - expect(content).toContain('module type:esm-export') - expect(content).toContain('export named:named') - expect(content).toContain('export value:123') - expect(content).toContain('export array:4,5,6') - expect(content).toContain('export object:{x:1}') - expect(content).toContain('swr-state') - } - - containClientContent(clientHtml) - containClientContent(browserClientText) - - // support esm module imports on server side, and indirect imports from shared components - expect(serverHtml).toContain('pure-esm-module') - expect(sharedHtml).toContain( - 'node_modules instance from client module pure-esm-module' - ) - }) + }) - it('should transpile specific external packages with the `transpilePackages` option', async () => { - const clientHtml = await next.render('/external-imports/client') - expect(clientHtml).toContain('transpilePackages:5') - }) + if (skipped) { + return + } - it('should resolve the subset react in server components based on the react-server condition', async () => { - await next.fetch('/react-server').then(async (response) => { - const result = await resolveStreamResponse(response) - expect(result).toContain('Server: subset') - expect(result).toContain('Client: full') - }) + it('should be able to opt-out 3rd party packages being bundled in server components', async () => { + await next.fetch('/react-server/optout').then(async (response) => { + const result = await resolveStreamResponse(response) + expect(result).toContain('Server: index.default') + expect(result).toContain('Server subpath: subpath.default') + expect(result).toContain('Client: index.default') + expect(result).toContain('Client subpath: subpath.default') + expect(result).not.toContain('opt-out-react-version: 18.3.0-canary') }) + }) - it('should resolve 3rd party package exports based on the react-server condition', async () => { - const $ = await next.render$('/react-server/3rd-party-package') - - const result = $('body').text() - - // Package should be resolved based on the react-server condition, - // as well as package's internal & external dependencies. - expect(result).toContain( - 'Server: index.react-server:react.subset:dep.server' - ) - expect(result).toContain('Client: index.default:react.full:dep.default') + it('should handle external async module libraries correctly', async () => { + const clientHtml = await next.render('/external-imports/client') + const serverHtml = await next.render('/external-imports/server') + const sharedHtml = await next.render('/shared-esm-dep') + + const browser = await next.browser('/external-imports/client') + const browserClientText = await browser.elementByCss('#content').text() + + function containClientContent(content) { + expect(content).toContain('module type:esm-export') + expect(content).toContain('export named:named') + expect(content).toContain('export value:123') + expect(content).toContain('export array:4,5,6') + expect(content).toContain('export object:{x:1}') + expect(content).toContain('swr-state') + } + + containClientContent(clientHtml) + containClientContent(browserClientText) + + // support esm module imports on server side, and indirect imports from shared components + expect(serverHtml).toContain('pure-esm-module') + expect(sharedHtml).toContain( + 'node_modules instance from client module pure-esm-module' + ) + }) - // Subpath exports should be resolved based on the condition too. - expect(result).toContain('Server subpath: subpath.react-server') - expect(result).toContain('Client subpath: subpath.default') + it('should transpile specific external packages with the `transpilePackages` option', async () => { + const clientHtml = await next.render('/external-imports/client') + expect(clientHtml).toContain('transpilePackages:5') + }) - // Prefer `module` field for isomorphic packages. - expect($('#main-field').text()).toContain('server-module-field:module') + it('should resolve the subset react in server components based on the react-server condition', async () => { + await next.fetch('/react-server').then(async (response) => { + const result = await resolveStreamResponse(response) + expect(result).toContain('Server: subset') + expect(result).toContain('Client: full') }) + }) - it('should correctly collect global css imports and mark them as side effects', async () => { - await next.fetch('/css/a').then(async (response) => { - const result = await resolveStreamResponse(response) + it('should resolve 3rd party package exports based on the react-server condition', async () => { + const $ = await next.render$('/react-server/3rd-party-package') - // It should include the global CSS import - expect(result).toMatch(/\.css/) - }) - }) + const result = $('body').text() - it('should handle external css modules', async () => { - const browser = await next.browser('/css/modules') + // Package should be resolved based on the react-server condition, + // as well as package's internal & external dependencies. + expect(result).toContain( + 'Server: index.react-server:react.subset:dep.server' + ) + expect(result).toContain('Client: index.default:react.full:dep.default') - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('h1')).color` - ) - ).toBe('rgb(255, 0, 0)') - }) + // Subpath exports should be resolved based on the condition too. + expect(result).toContain('Server subpath: subpath.react-server') + expect(result).toContain('Client subpath: subpath.default') - it('should use the same export type for packages in both ssr and client', async () => { - const browser = await next.browser('/client-dep') - expect(await browser.eval(`window.document.body.innerText`)).toBe('hello') - }) + // Prefer `module` field for isomorphic packages. + expect($('#main-field').text()).toContain('server-module-field:module') + }) - it('should handle external css modules in pages', async () => { - const browser = await next.browser('/test-pages') + it('should correctly collect global css imports and mark them as side effects', async () => { + await next.fetch('/css/a').then(async (response) => { + const result = await resolveStreamResponse(response) - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('h1')).color` - ) - ).toBe('rgb(255, 0, 0)') + // It should include the global CSS import + expect(result).toMatch(/\.css/) }) + }) - it('should handle external next/font', async () => { - const browser = await next.browser('/font') + it('should handle external css modules', async () => { + const browser = await next.browser('/css/modules') - expect( - await browser.eval( - `window.getComputedStyle(document.querySelector('p')).fontFamily` - ) - ).toMatch(/^__myFont_.{6}, __myFont_Fallback_.{6}$/) - }) - // TODO: This test depends on `new Worker` which is not supported in Turbopack yet. - ;(process.env.TURBOPACK ? it.skip : it)( - 'should not apply swc optimizer transform for external packages in browser layer in web worker', - async () => { - const browser = await next.browser('/browser') - // eslint-disable-next-line jest/no-standalone-expect - expect(await browser.elementByCss('#worker-state').text()).toBe( - 'default' - ) + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('h1')).color` + ) + ).toBe('rgb(255, 0, 0)') + }) - await browser.elementByCss('button').click() + it('should use the same export type for packages in both ssr and client', async () => { + const browser = await next.browser('/client-dep') + expect(await browser.eval(`window.document.body.innerText`)).toBe('hello') + }) - await retry(async () => { - // eslint-disable-next-line jest/no-standalone-expect - expect(await browser.elementByCss('#worker-state').text()).toBe( - 'worker.js:browser-module/other' - ) - }) - } - ) + it('should handle external css modules in pages', async () => { + const browser = await next.browser('/test-pages') - describe('react in external esm packages', () => { - it('should use the same react in client app', async () => { - const html = await next.render('/esm/client') + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('h1')).color` + ) + ).toBe('rgb(255, 0, 0)') + }) - const v1 = html.match(/App React Version: ([^<]+) { + const browser = await next.browser('/font') - // Should work with both esm and cjs imports - expect(html).toContain( - 'CJS-ESM Compat package: cjs-esm-compat/index.mjs' - ) - expect(html).toContain('CJS package: cjs-lib') - expect(html).toContain( - 'Nested imports: nested-import:esm:cjs-esm-compat/index.mjs' - ) - }) - - it('should use the same react in server app', async () => { - const html = await next.render('/esm/server') + expect( + await browser.eval( + `window.getComputedStyle(document.querySelector('p')).fontFamily` + ) + ).toMatch(/^__myFont_.{6}, __myFont_Fallback_.{6}$/) + }) + // TODO: This test depends on `new Worker` which is not supported in Turbopack yet. + ;(process.env.TURBOPACK ? it.skip : it)( + 'should not apply swc optimizer transform for external packages in browser layer in web worker', + async () => { + const browser = await next.browser('/browser') + // eslint-disable-next-line jest/no-standalone-expect + expect(await browser.elementByCss('#worker-state').text()).toBe('default') - const v1 = html.match(/App React Version: ([^<]+) { + // eslint-disable-next-line jest/no-standalone-expect + expect(await browser.elementByCss('#worker-state').text()).toBe( + 'worker.js:browser-module/other' ) - expect(html).toContain('CJS package: cjs-lib') }) + } + ) + + describe('react in external esm packages', () => { + it('should use the same react in client app', async () => { + const html = await next.render('/esm/client') + + const v1 = html.match(/App React Version: ([^<]+) { - const html = await next.render('/esm/edge-server') + it('should use the same react in server app', async () => { + const html = await next.render('/esm/server') - const v1 = html.match(/App React Version: ([^<]+) { - const html = await next.render('/test-pages-esm') + it('should use the same react in edge server app', async () => { + const html = await next.render('/esm/edge-server') - const v1 = html.match(/App React Version: ([^<]+) { - const $ = await next.render$('/esm/react-namespace-import') - expect($('#namespace-import-esm').text()).toBe('namespace-import:esm') - }) + // Should work with both esm and cjs imports + expect(html).toContain('CJS-ESM Compat package: cjs-esm-compat/index.mjs') + expect(html).toContain('CJS package: cjs-lib') }) - describe('mixed syntax external modules', () => { - it('should handle mixed module with next/dynamic', async () => { - const browser = await next.browser('/mixed/dynamic') - expect(await browser.elementByCss('#component').text()).toContain( - 'mixed-syntax-esm' - ) - }) + it('should use the same react in pages', async () => { + const html = await next.render('/test-pages-esm') - it('should handle mixed module in server and client components', async () => { - const $ = await next.render$('/mixed/import') - expect(await $('#server').text()).toContain('server:mixed-syntax-esm') - expect(await $('#client').text()).toContain('client:mixed-syntax-esm') - expect(await $('#relative-mixed').text()).toContain( - 'relative-mixed-syntax-esm' - ) - }) + const v1 = html.match(/App React Version: ([^<]+) { - const $ = await next.render$('/cjs/client') - expect($('#private-prop').text()).toBe('prop') - expect($('#transpile-cjs-lib').text()).toBe('transpile-cjs-lib') - - const browser = await next.browser('/cjs/client') - expect(await hasRedbox(browser)).toBe(false) + it('should support namespace import with ESM packages', async () => { + const $ = await next.render$('/esm/react-namespace-import') + expect($('#namespace-import-esm').text()).toBe('namespace-import:esm') }) + }) - it('should export client module references in esm', async () => { - const html = await next.render('/esm-client-ref') - expect(html).toContain('hello') + describe('mixed syntax external modules', () => { + it('should handle mixed module with next/dynamic', async () => { + const browser = await next.browser('/mixed/dynamic') + expect(await browser.elementByCss('#component').text()).toContain( + 'mixed-syntax-esm' + ) }) - it('should support exporting multiple star re-exports', async () => { - const html = await next.render('/wildcard') - expect(html).toContain('Foo') + it('should handle mixed module in server and client components', async () => { + const $ = await next.render$('/mixed/import') + expect(await $('#server').text()).toContain('server:mixed-syntax-esm') + expect(await $('#client').text()).toContain('client:mixed-syntax-esm') + expect(await $('#relative-mixed').text()).toContain( + 'relative-mixed-syntax-esm' + ) }) + }) - it('should have proper tree-shaking for known modules in CJS', async () => { - const html = await next.render('/cjs/server') - expect(html).toContain('resolve response') + it('should emit cjs helpers for external cjs modules when compiled', async () => { + const $ = await next.render$('/cjs/client') + expect($('#private-prop').text()).toBe('prop') + expect($('#transpile-cjs-lib').text()).toBe('transpile-cjs-lib') - const outputFile = await next.readFile( - '.next/server/app/cjs/server/page.js' - ) - expect(outputFile).not.toContain('image-response') - }) + const browser = await next.browser('/cjs/client') + expect(await hasRedbox(browser)).toBe(false) + }) - it('should use the same async storages if imported directly', async () => { - const html = await next.render('/async-storage') - expect(html).toContain('success') - }) + it('should export client module references in esm', async () => { + const html = await next.render('/esm-client-ref') + expect(html).toContain('hello') + }) - describe('server actions', () => { - it('should not prefer to resolve esm over cjs for bundling optout packages', async () => { - const browser = await next.browser('/optout/action') - expect(await browser.elementByCss('#dual-pkg-outout p').text()).toBe('') - - browser.elementByCss('#dual-pkg-outout button').click() - await check(async () => { - const text = await browser.elementByCss('#dual-pkg-outout p').text() - expect(text).toBe('dual-pkg-optout:cjs') - return 'success' - }, /success/) - }) + it('should support exporting multiple star re-exports', async () => { + const html = await next.render('/wildcard') + expect(html).toContain('Foo') + }) - it('should compile server actions from node_modules in client components', async () => { - // before action there's no action log - expect(next.cliOutput).not.toContain('action-log:server:action1') - const browser = await next.browser('/action/client') - await browser.elementByCss('#action').click() + it('should have proper tree-shaking for known modules in CJS', async () => { + const html = await next.render('/cjs/server') + expect(html).toContain('resolve response') - await check(() => { - expect(next.cliOutput).toContain('action-log:server:action1') - return 'success' - }, /success/) - }) + const outputFile = await next.readFile( + '.next/server/app/cjs/server/page.js' + ) + expect(outputFile).not.toContain('image-response') + }) + + it('should use the same async storages if imported directly', async () => { + const html = await next.render('/async-storage') + expect(html).toContain('success') + }) + + describe('server actions', () => { + it('should prefer to resolve esm over cjs for bundling optout packages', async () => { + const browser = await next.browser('/optout/action') + expect(await browser.elementByCss('#dual-pkg-outout p').text()).toBe('') + + browser.elementByCss('#dual-pkg-outout button').click() + await check(async () => { + const text = await browser.elementByCss('#dual-pkg-outout p').text() + expect(text).toBe('dual-pkg-optout:mjs') + return 'success' + }, /success/) }) - describe('app route', () => { - it('should resolve next/server api from external esm package', async () => { - const res = await next.fetch('/app-routes') - const text = await res.text() - expect(res.status).toBe(200) - expect(text).toBe('get route') - }) + it('should compile server actions from node_modules in client components', async () => { + // before action there's no action log + expect(next.cliOutput).not.toContain('action-log:server:action1') + const browser = await next.browser('/action/client') + await browser.elementByCss('#action').click() + + await check(() => { + expect(next.cliOutput).toContain('action-log:server:action1') + return 'success' + }, /success/) }) - } -) + }) + + describe('app route', () => { + it('should resolve next/server api from external esm package', async () => { + const res = await next.fetch('/app-routes') + const text = await res.text() + expect(res.status).toBe(200) + expect(text).toBe('get route') + }) + }) +}) diff --git a/test/e2e/app-dir/app-middleware/app-middleware.test.ts b/test/e2e/app-dir/app-middleware/app-middleware.test.ts index 661839a2aa37d..77e7e8bca5e71 100644 --- a/test/e2e/app-dir/app-middleware/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware/app-middleware.test.ts @@ -1,7 +1,7 @@ /* eslint-env jest */ import path from 'path' import cheerio from 'cheerio' -import { check, withQuery } from 'next-test-utils' +import { check, retry, withQuery } from 'next-test-utils' import { createNextDescribe, FileRef } from 'e2e-utils' import type { Response } from 'node-fetch' @@ -134,6 +134,96 @@ createNextDescribe( expect(bypassCookie).toBeDefined() }) }) + + it('should be possible to modify cookies & read them in an RSC in a single request', async () => { + const browser = await next.browser('/rsc-cookies') + + const initialRandom1 = await browser.elementById('rsc-cookie-1').text() + const initialRandom2 = await browser.elementById('rsc-cookie-2').text() + const totalCookies = await browser.elementById('total-cookies').text() + + // cookies were set in middleware, assert they are present and match the Math.random() pattern + expect(initialRandom1).toMatch(/Cookie 1: \d+\.\d+/) + expect(initialRandom2).toMatch(/Cookie 2: \d+\.\d+/) + expect(totalCookies).toBe('Total Cookie Length: 2') + + await browser.refresh() + + const refreshedRandom1 = await browser.elementById('rsc-cookie-1').text() + const refreshedRandom2 = await browser.elementById('rsc-cookie-2').text() + + // the cookies should be refreshed and have new values + expect(refreshedRandom1).toMatch(/Cookie 1: \d+\.\d+/) + expect(refreshedRandom2).toMatch(/Cookie 2: \d+\.\d+/) + expect(refreshedRandom1).not.toBe(initialRandom1) + expect(refreshedRandom2).not.toBe(initialRandom2) + + // navigate to delete cookies route + await browser.elementByCss('[href="/rsc-cookies-delete"]').click() + await retry(async () => { + // only the first cookie should be deleted + expect(await browser.elementById('rsc-cookie-1').text()).toBe( + 'Cookie 1:' + ) + + expect(await browser.elementById('rsc-cookie-2').text()).toMatch( + /Cookie 2: \d+\.\d+/ + ) + }) + // Cleanup + await browser.deleteCookies() + }) + + it('should respect cookie options of merged middleware cookies', async () => { + const browser = await next.browser('/rsc-cookies/cookie-options') + + const totalCookies = await browser.elementById('total-cookies').text() + + // a secure cookie was set in middleware + expect(totalCookies).toBe('Total Cookie Length: 1') + + // we don't expect to be able to read it + expect(await browser.eval('document.cookie')).toBeFalsy() + + await browser.elementById('submit-server-action').click() + + await retry(() => { + expect(next.cliOutput).toMatch(/\[Cookie From Action\]: \d+\.\d+/) + }) + + // ensure that we still can't read the secure cookie + expect(await browser.eval('document.cookie')).toBeFalsy() + + // Cleanup + await browser.deleteCookies() + }) + + it('should be possible to read cookies that are set during the middleware handling of a server action', async () => { + const browser = await next.browser('/rsc-cookies') + const initialRandom1 = await browser.elementById('rsc-cookie-1').text() + const initialRandom2 = await browser.elementById('rsc-cookie-2').text() + const totalCookies = await browser.elementById('total-cookies').text() + + // cookies were set in middleware, assert they are present and match the Math.random() pattern + expect(initialRandom1).toMatch(/Cookie 1: \d+\.\d+/) + expect(initialRandom2).toMatch(/Cookie 2: \d+\.\d+/) + expect(totalCookies).toBe('Total Cookie Length: 2') + + expect(await browser.eval('document.cookie')).toBeTruthy() + + await browser.deleteCookies() + + // assert that document.cookie is empty + expect(await browser.eval('document.cookie')).toBeFalsy() + + await browser.elementById('submit-server-action').click() + + await retry(() => { + expect(next.cliOutput).toMatch(/\[Cookie From Action\]: \d+\.\d+/) + }) + + await browser.deleteCookies() + }) } ) diff --git a/test/e2e/app-dir/app-middleware/app/rsc-cookies-delete/page.js b/test/e2e/app-dir/app-middleware/app/rsc-cookies-delete/page.js new file mode 100644 index 0000000000000..38245781cbd8d --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/rsc-cookies-delete/page.js @@ -0,0 +1,14 @@ +import { cookies } from 'next/headers' + +export default function Page() { + const rscCookie1 = cookies().get('rsc-cookie-value-1')?.value + const rscCookie2 = cookies().get('rsc-cookie-value-2')?.value + + return ( +
+ + +

Total Cookie Length: {cookies().size}

+
+ ) +} diff --git a/test/e2e/app-dir/app-middleware/app/rsc-cookies/cookie-options/page.js b/test/e2e/app-dir/app-middleware/app/rsc-cookies/cookie-options/page.js new file mode 100644 index 0000000000000..569bcb8bd625b --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/rsc-cookies/cookie-options/page.js @@ -0,0 +1,25 @@ +import { cookies } from 'next/headers' +import Link from 'next/link' + +export default function Page() { + return ( +
+

Total Cookie Length: {cookies().size}

+ To Delete Cookies Route + +
{ + 'use server' + console.log( + '[Cookie From Action]:', + cookies().get('rsc-secure-cookie').value + ) + }} + > + +
+
+ ) +} diff --git a/test/e2e/app-dir/app-middleware/app/rsc-cookies/page.js b/test/e2e/app-dir/app-middleware/app/rsc-cookies/page.js new file mode 100644 index 0000000000000..774e3003953ed --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/rsc-cookies/page.js @@ -0,0 +1,30 @@ +import { cookies } from 'next/headers' +import Link from 'next/link' + +export default function Page() { + const rscCookie1 = cookies().get('rsc-cookie-value-1')?.value + const rscCookie2 = cookies().get('rsc-cookie-value-2')?.value + + return ( +
+ + +

Total Cookie Length: {cookies().size}

+ To Delete Cookies Route + +
{ + 'use server' + console.log( + '[Cookie From Action]:', + cookies().get('rsc-cookie-value-1')?.value + ) + }} + > + +
+
+ ) +} diff --git a/test/e2e/app-dir/app-middleware/middleware.js b/test/e2e/app-dir/app-middleware/middleware.js index 0048747a3812c..7c6123536f339 100644 --- a/test/e2e/app-dir/app-middleware/middleware.js +++ b/test/e2e/app-dir/app-middleware/middleware.js @@ -44,6 +44,31 @@ export async function middleware(request) { return NextResponse.rewrite(request.nextUrl) } + if (request.nextUrl.pathname === '/rsc-cookies') { + const res = NextResponse.next() + res.cookies.set('rsc-cookie-value-1', `${Math.random()}`) + res.cookies.set('rsc-cookie-value-2', `${Math.random()}`) + + return res + } + + if (request.nextUrl.pathname === '/rsc-cookies/cookie-options') { + const res = NextResponse.next() + res.cookies.set('rsc-secure-cookie', `${Math.random()}`, { + secure: true, + httpOnly: true, + }) + + return res + } + + if (request.nextUrl.pathname === '/rsc-cookies-delete') { + const res = NextResponse.next() + res.cookies.delete('rsc-cookie-value-1') + + return res + } + return NextResponse.next({ request: { headers: headersFromRequest, diff --git a/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts b/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts index c85903614f4d9..a98b19ff6eb37 100644 --- a/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts +++ b/test/e2e/app-dir/app-routes-client-component/app-routes-client-component.test.ts @@ -4,16 +4,13 @@ import path from 'path' describe('referencing a client component in an app route', () => { const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname)), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, }) it('responds without error', async () => { expect(JSON.parse(await next.render('/runtime'))).toEqual({ // Turbopack's proxy components are functions clientComponent: process.env.TURBOPACK ? 'function' : 'object', + myModuleClientComponent: process.env.TURBOPACK ? 'function' : 'object', }) }) }) diff --git a/test/e2e/app-dir/app-routes-client-component/app/runtime/route.ts b/test/e2e/app-dir/app-routes-client-component/app/runtime/route.ts index e4ce5094e904d..fd7505a005999 100644 --- a/test/e2e/app-dir/app-routes-client-component/app/runtime/route.ts +++ b/test/e2e/app-dir/app-routes-client-component/app/runtime/route.ts @@ -1,8 +1,10 @@ import { NextResponse } from 'next/server' import { ClientComponent } from '../../ClientComponent' +import { MyModuleClientComponent } from 'my-module/MyModuleClientComponent' export function GET() { return NextResponse.json({ clientComponent: typeof ClientComponent, + myModuleClientComponent: typeof MyModuleClientComponent, }) } diff --git a/test/e2e/app-dir/app-routes-client-component/node_modules/my-module/MyModuleClientComponent.tsx b/test/e2e/app-dir/app-routes-client-component/node_modules/my-module/MyModuleClientComponent.tsx new file mode 100644 index 0000000000000..c6cd8470693c8 --- /dev/null +++ b/test/e2e/app-dir/app-routes-client-component/node_modules/my-module/MyModuleClientComponent.tsx @@ -0,0 +1,5 @@ +'use client' + +export function MyModuleClientComponent() { + return
MyModuleClientComponent
+} diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 59cd0d58b9e9c..e910cd03b657c 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -2,7 +2,7 @@ import globOrig from 'glob' import cheerio from 'cheerio' import { promisify } from 'util' import { join } from 'path' -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check, fetchViaHTTP, @@ -14,9 +14,13 @@ import stripAnsi from 'strip-ansi' const glob = promisify(globOrig) -createNextDescribe( - 'app-dir static/dynamic handling', - { +describe('app-dir static/dynamic handling', () => { + const { + next, + isNextDev: isDev, + isNextStart, + isNextDeploy, + } = nextTestSetup({ files: __dirname, env: { NEXT_DEBUG_BUILD: '1', @@ -26,449 +30,404 @@ createNextDescribe( } : {}), }, - }, - ({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => { - let prerenderManifest - let buildCliOutputIndex = 0 + }) + let prerenderManifest + let buildCliOutputIndex = 0 - beforeAll(async () => { - if (isNextStart) { - prerenderManifest = JSON.parse( - await next.readFile('.next/prerender-manifest.json') - ) - buildCliOutputIndex = next.cliOutput.length - } + beforeAll(async () => { + if (isNextStart) { + prerenderManifest = JSON.parse( + await next.readFile('.next/prerender-manifest.json') + ) + buildCliOutputIndex = next.cliOutput.length + } + }) + + it('should still cache even though the `traceparent` header was different', async () => { + const res = await next.fetch('/strip-header-traceparent') + expect(res.status).toBe(200) + + const html = await res.text() + const $ = cheerio.load(html) + + const data1 = $('#data1').text() + const data2 = $('#data2').text() + expect(data1).toBeTruthy() + expect(data1).toBe(data2) + + const echoedHeaders = JSON.parse($('#echoedHeaders').text()) + expect(echoedHeaders.headers.traceparent).toEqual('C') + }) + + it('should warn for too many cache tags', async () => { + const res = await next.fetch('/too-many-cache-tags') + expect(res.status).toBe(200) + await retry(() => { + expect(next.cliOutput).toContain('exceeded max tag count for') + expect(next.cliOutput).toContain('tag-65') }) + }) - it('should still cache even though the `traceparent` header was different', async () => { - const res = await next.fetch('/strip-header-traceparent') - expect(res.status).toBe(200) - - const html = await res.text() - const $ = cheerio.load(html) + if (isNextDeploy) { + describe('new tags have been specified on subsequent fetch', () => { + it('should not fetch from memory cache', async () => { + const res1 = await next.fetch('/specify-new-tags/one-tag') + expect(res1.status).toBe(200) - const data1 = $('#data1').text() - const data2 = $('#data2').text() - expect(data1).toBeTruthy() - expect(data1).toBe(data2) + const res2 = await next.fetch('/specify-new-tags/two-tags') + expect(res2.status).toBe(200) - const echoedHeaders = JSON.parse($('#echoedHeaders').text()) - expect(echoedHeaders.headers.traceparent).toEqual('C') - }) + const html1 = await res1.text() + const html2 = await res2.text() + const $1 = cheerio.load(html1) + const $2 = cheerio.load(html2) - it('should warn for too many cache tags', async () => { - const res = await next.fetch('/too-many-cache-tags') - expect(res.status).toBe(200) - await retry(() => { - expect(next.cliOutput).toContain('exceeded max tag count for') - expect(next.cliOutput).toContain('tag-65') + const data1 = $1('#page-data').text() + const data2 = $2('#page-data').text() + expect(data1).not.toBe(data2) }) - }) - if (isNextDeploy) { - describe('new tags have been specified on subsequent fetch', () => { - it('should not fetch from memory cache', async () => { - const res1 = await next.fetch('/specify-new-tags/one-tag') - expect(res1.status).toBe(200) + it('should not fetch from memory cache after revalidateTag is used', async () => { + const res1 = await next.fetch('/specify-new-tags/one-tag') + expect(res1.status).toBe(200) - const res2 = await next.fetch('/specify-new-tags/two-tags') - expect(res2.status).toBe(200) - - const html1 = await res1.text() - const html2 = await res2.text() - const $1 = cheerio.load(html1) - const $2 = cheerio.load(html2) - - const data1 = $1('#page-data').text() - const data2 = $2('#page-data').text() - expect(data1).not.toBe(data2) - }) - - it('should not fetch from memory cache after revalidateTag is used', async () => { - const res1 = await next.fetch('/specify-new-tags/one-tag') - expect(res1.status).toBe(200) - - const revalidateRes = await next.fetch( - '/api/revlidate-tag-node?tag=thankyounext' - ) - expect((await revalidateRes.json()).revalidated).toBe(true) + const revalidateRes = await next.fetch( + '/api/revlidate-tag-node?tag=thankyounext' + ) + expect((await revalidateRes.json()).revalidated).toBe(true) - const res2 = await next.fetch('/specify-new-tags/two-tags') - expect(res2.status).toBe(200) + const res2 = await next.fetch('/specify-new-tags/two-tags') + expect(res2.status).toBe(200) - const html1 = await res1.text() - const html2 = await res2.text() - const $1 = cheerio.load(html1) - const $2 = cheerio.load(html2) + const html1 = await res1.text() + const html2 = await res2.text() + const $1 = cheerio.load(html1) + const $2 = cheerio.load(html2) - const data1 = $1('#page-data').text() - const data2 = $2('#page-data').text() - expect(data1).not.toBe(data2) - }) + const data1 = $1('#page-data').text() + const data2 = $2('#page-data').text() + expect(data1).not.toBe(data2) }) - } + }) + } - if (isNextStart) { - it('should propagate unstable_cache tags correctly', async () => { - const meta = JSON.parse( - await next.readFile( - '.next/server/app/variable-revalidate/revalidate-360-isr.meta' - ) + if (isNextStart) { + it('should propagate unstable_cache tags correctly', async () => { + const meta = JSON.parse( + await next.readFile( + '.next/server/app/variable-revalidate/revalidate-360-isr.meta' ) - expect(meta.headers['x-next-cache-tags']).toContain( - 'unstable_cache_tag1' - ) - }) + ) + expect(meta.headers['x-next-cache-tags']).toContain('unstable_cache_tag1') + }) - if (!process.env.CUSTOM_CACHE_HANDLER) { - it('should honor force-static with fetch cache: no-store correctly', async () => { - const res = await next.fetch('/force-static-fetch-no-store') - expect(res.status).toBe(200) - expect(res.headers.get('x-nextjs-cache').toLowerCase()).toBe('hit') - }) - } + if (!process.env.CUSTOM_CACHE_HANDLER) { + it('should honor force-static with fetch cache: no-store correctly', async () => { + const res = await next.fetch('/force-static-fetch-no-store') + expect(res.status).toBe(200) + expect(res.headers.get('x-nextjs-cache').toLowerCase()).toBe('hit') + }) } + } - it('should correctly include headers instance in cache key', async () => { - const res = await next.fetch('/variable-revalidate/headers-instance') - expect(res.status).toBe(200) + it('should correctly include headers instance in cache key', async () => { + const res = await next.fetch('/variable-revalidate/headers-instance') + expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const html = await res.text() + const $ = cheerio.load(html) - const data1 = $('#page-data').text() - const data2 = $('#page-data2').text() - expect(data1).not.toBe(data2) + const data1 = $('#page-data').text() + const data2 = $('#page-data2').text() + expect(data1).not.toBe(data2) - expect(data1).toBeTruthy() - expect(data2).toBeTruthy() - }) + expect(data1).toBeTruthy() + expect(data2).toBeTruthy() + }) - it.skip.each([ - { - path: '/react-fetch-deduping-node', - }, - { - path: '/react-fetch-deduping-edge', - }, - ])( - 'should correctly de-dupe fetch without next cache $path', - async ({ path }) => { - for (let i = 0; i < 5; i++) { - const res = await next.fetch(path, { - redirect: 'manual', - }) + it.skip.each([ + { + path: '/react-fetch-deduping-node', + }, + { + path: '/react-fetch-deduping-edge', + }, + ])( + 'should correctly de-dupe fetch without next cache $path', + async ({ path }) => { + for (let i = 0; i < 5; i++) { + const res = await next.fetch(path, { + redirect: 'manual', + }) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - const data1 = $('#data-1').text() - const data2 = $('#data-2').text() + const data1 = $('#data-1').text() + const data2 = $('#data-2').text() - expect(data1).toBeTruthy() - expect(data1).toBe(data2) + expect(data1).toBeTruthy() + expect(data1).toBe(data2) - await waitFor(250) - } + await waitFor(250) } - ) + } + ) + + it.each([ + { pathname: '/unstable-cache-node' }, + { pathname: '/unstable-cache-edge' }, + { pathname: '/api/unstable-cache-node' }, + { pathname: '/api/unstable-cache-edge' }, + ])('unstable-cache should work in pages$pathname', async ({ pathname }) => { + let res = await next.fetch(pathname) + expect(res.status).toBe(200) + const isApi = pathname.startsWith('/api') + let prevData + + if (isApi) { + prevData = await res.json() + } else { + const initialHtml = await res.text() + const initial$ = isApi ? undefined : cheerio.load(initialHtml) + prevData = JSON.parse(initial$('#props').text()) + } - it.each([ - { pathname: '/unstable-cache-node' }, - { pathname: '/unstable-cache-edge' }, - { pathname: '/api/unstable-cache-node' }, - { pathname: '/api/unstable-cache-edge' }, - ])('unstable-cache should work in pages$pathname', async ({ pathname }) => { - let res = await next.fetch(pathname) + expect(prevData.data.random).toBeTruthy() + + await check(async () => { + res = await next.fetch(pathname) expect(res.status).toBe(200) - const isApi = pathname.startsWith('/api') - let prevData + let curData if (isApi) { - prevData = await res.json() + curData = await res.json() } else { - const initialHtml = await res.text() - const initial$ = isApi ? undefined : cheerio.load(initialHtml) - prevData = JSON.parse(initial$('#props').text()) + const curHtml = await res.text() + const cur$ = cheerio.load(curHtml) + curData = JSON.parse(cur$('#props').text()) } - expect(prevData.data.random).toBeTruthy() - - await check(async () => { - res = await next.fetch(pathname) - expect(res.status).toBe(200) - let curData - - if (isApi) { - curData = await res.json() - } else { - const curHtml = await res.text() - const cur$ = cheerio.load(curHtml) - curData = JSON.parse(cur$('#props').text()) - } - - try { - expect(curData.data.random).toBeTruthy() - expect(curData.data.random).toBe(prevData.data.random) - } finally { - prevData = curData - } - return 'success' - }, 'success') - }) - - it('should not have cache tags header for non-minimal mode', async () => { - for (const path of [ - '/ssr-forced', - '/ssr-forced', - '/variable-revalidate/revalidate-3', - '/variable-revalidate/revalidate-360', - '/variable-revalidate/revalidate-360-isr', - ]) { - const res = await fetchViaHTTP(next.url, path, undefined, { - redirect: 'manual', - }) - expect(res.status).toBe(200) - expect(res.headers.get('x-next-cache-tags')).toBeFalsy() + try { + expect(curData.data.random).toBeTruthy() + expect(curData.data.random).toBe(prevData.data.random) + } finally { + prevData = curData } - }) + return 'success' + }, 'success') + }) + + it('should not have cache tags header for non-minimal mode', async () => { + for (const path of [ + '/ssr-forced', + '/ssr-forced', + '/variable-revalidate/revalidate-3', + '/variable-revalidate/revalidate-360', + '/variable-revalidate/revalidate-360-isr', + ]) { + const res = await fetchViaHTTP(next.url, path, undefined, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(res.headers.get('x-next-cache-tags')).toBeFalsy() + } + }) - if (isDev) { - it('should error correctly for invalid params from generateStaticParams', async () => { - await next.patchFile( - 'app/invalid/[slug]/page.js', - ` + if (isDev) { + it('should error correctly for invalid params from generateStaticParams', async () => { + await next.patchFile( + 'app/invalid/[slug]/page.js', + ` export function generateStaticParams() { return [{slug: { invalid: true }}] } ` - ) + ) - // The page may take a moment to compile, so try it a few times. - await check(async () => { - return next.render('/invalid/first') - }, /A required parameter \(slug\) was not provided as a string received object/) + // The page may take a moment to compile, so try it a few times. + await check(async () => { + return next.render('/invalid/first') + }, /A required parameter \(slug\) was not provided as a string received object/) - await next.deleteFile('app/invalid/[slug]/page.js') - }) + await next.deleteFile('app/invalid/[slug]/page.js') + }) - it('should correctly handle multi-level generateStaticParams when some levels are missing', async () => { - const browser = await next.browser('/flight/foo/bar') - const v = ~~(Math.random() * 1000) - await browser.eval(`document.cookie = "test-cookie=${v}"`) - await browser.elementByCss('button').click() - await check(async () => { - return await browser.elementByCss('h1').text() - }, v.toString()) - }) - } + it('should correctly handle multi-level generateStaticParams when some levels are missing', async () => { + const browser = await next.browser('/flight/foo/bar') + const v = ~~(Math.random() * 1000) + await browser.eval(`document.cookie = "test-cookie=${v}"`) + await browser.elementByCss('button').click() + await check(async () => { + return await browser.elementByCss('h1').text() + }, v.toString()) + }) + } + + it('should correctly skip caching POST fetch for POST handler', async () => { + const res = await next.fetch('/route-handler/post', { + method: 'POST', + }) + expect(res.status).toBe(200) + + const data = await res.json() + expect(data).toBeTruthy() - it('should correctly skip caching POST fetch for POST handler', async () => { - const res = await next.fetch('/route-handler/post', { + for (let i = 0; i < 5; i++) { + const res2 = await next.fetch('/route-handler/post', { method: 'POST', }) - expect(res.status).toBe(200) + expect(res2.status).toBe(200) + const newData = await res2.json() + expect(newData).toBeTruthy() + expect(newData).not.toEqual(data) + } + }) - const data = await res.json() - expect(data).toBeTruthy() + if (!isDev && !process.env.CUSTOM_CACHE_HANDLER) { + it('should properly revalidate a route handler that triggers dynamic usage with force-static', async () => { + // wait for the revalidation period + let res = await next.fetch('/route-handler/no-store-force-static') - for (let i = 0; i < 5; i++) { - const res2 = await next.fetch('/route-handler/post', { - method: 'POST', - }) - expect(res2.status).toBe(200) - const newData = await res2.json() - expect(newData).toBeTruthy() - expect(newData).not.toEqual(data) - } - }) + let data = await res.json() + // grab the initial timestamp + const initialTimestamp = data.now + + // confirm its cached still + res = await next.fetch('/route-handler/no-store-force-static') - if (!isDev && !process.env.CUSTOM_CACHE_HANDLER) { - it('should properly revalidate a route handler that triggers dynamic usage with force-static', async () => { - // wait for the revalidation period - let res = await next.fetch('/route-handler/no-store-force-static') + data = await res.json() - let data = await res.json() - // grab the initial timestamp - const initialTimestamp = data.now + expect(data.now).toBe(initialTimestamp) - // confirm its cached still - res = await next.fetch('/route-handler/no-store-force-static') + // wait for the revalidation time + await waitFor(3000) - data = await res.json() + // verify fresh data + res = await next.fetch('/route-handler/no-store-force-static') + data = await res.json() + + expect(data.now).not.toBe(initialTimestamp) + }) + } - expect(data.now).toBe(initialTimestamp) + if (!process.env.CUSTOM_CACHE_HANDLER) { + it.each([ + { + type: 'edge route handler', + revalidateApi: '/api/revalidate-tag-edge', + }, + { + type: 'node route handler', + revalidateApi: '/api/revalidate-tag-node', + }, + ])( + 'it should revalidate tag correctly with $type', + async ({ revalidateApi }) => { + const initRes = await next.fetch('/variable-revalidate/revalidate-360') + const html = await initRes.text() + const $ = cheerio.load(html) + const initLayoutData = $('#layout-data').text() + const initPageData = $('#page-data').text() + const initNestedCacheData = $('#nested-cache').text() - // wait for the revalidation time - await waitFor(3000) + const routeHandlerRes = await next.fetch( + '/route-handler/revalidate-360' + ) + const initRouteHandlerData = await routeHandlerRes.json() - // verify fresh data - res = await next.fetch('/route-handler/no-store-force-static') - data = await res.json() + const edgeRouteHandlerRes = await next.fetch( + '/route-handler-edge/revalidate-360' + ) + const initEdgeRouteHandlerRes = await edgeRouteHandlerRes.json() - expect(data.now).not.toBe(initialTimestamp) - }) - } + expect(initLayoutData).toBeTruthy() + expect(initPageData).toBeTruthy() - if (!process.env.CUSTOM_CACHE_HANDLER) { - it.each([ - { - type: 'edge route handler', - revalidateApi: '/api/revalidate-tag-edge', - }, - { - type: 'node route handler', - revalidateApi: '/api/revalidate-tag-node', - }, - ])( - 'it should revalidate tag correctly with $type', - async ({ revalidateApi }) => { - const initRes = await next.fetch( - '/variable-revalidate/revalidate-360' + await check(async () => { + const revalidateRes = await next.fetch( + `${revalidateApi}?tag=thankyounext` ) - const html = await initRes.text() - const $ = cheerio.load(html) - const initLayoutData = $('#layout-data').text() - const initPageData = $('#page-data').text() - const initNestedCacheData = $('#nested-cache').text() + expect((await revalidateRes.json()).revalidated).toBe(true) + + const newRes = await next.fetch('/variable-revalidate/revalidate-360') + const cacheHeader = newRes.headers.get('x-nextjs-cache') + + if ((global as any).isNextStart && cacheHeader) { + expect(cacheHeader).toBe('MISS') + } + const newHtml = await newRes.text() + const new$ = cheerio.load(newHtml) + const newLayoutData = new$('#layout-data').text() + const newPageData = new$('#page-data').text() + const newNestedCacheData = new$('#nested-cache').text() - const routeHandlerRes = await next.fetch( + const newRouteHandlerRes = await next.fetch( '/route-handler/revalidate-360' ) - const initRouteHandlerData = await routeHandlerRes.json() + const newRouteHandlerData = await newRouteHandlerRes.json() - const edgeRouteHandlerRes = await next.fetch( + const newEdgeRouteHandlerRes = await next.fetch( '/route-handler-edge/revalidate-360' ) - const initEdgeRouteHandlerRes = await edgeRouteHandlerRes.json() - - expect(initLayoutData).toBeTruthy() - expect(initPageData).toBeTruthy() - - await check(async () => { - const revalidateRes = await next.fetch( - `${revalidateApi}?tag=thankyounext` - ) - expect((await revalidateRes.json()).revalidated).toBe(true) - - const newRes = await next.fetch( - '/variable-revalidate/revalidate-360' - ) - const cacheHeader = newRes.headers.get('x-nextjs-cache') - - if ((global as any).isNextStart && cacheHeader) { - expect(cacheHeader).toBe('MISS') - } - const newHtml = await newRes.text() - const new$ = cheerio.load(newHtml) - const newLayoutData = new$('#layout-data').text() - const newPageData = new$('#page-data').text() - const newNestedCacheData = new$('#nested-cache').text() - - const newRouteHandlerRes = await next.fetch( - '/route-handler/revalidate-360' - ) - const newRouteHandlerData = await newRouteHandlerRes.json() - - const newEdgeRouteHandlerRes = await next.fetch( - '/route-handler-edge/revalidate-360' - ) - const newEdgeRouteHandlerData = await newEdgeRouteHandlerRes.json() - - expect(newLayoutData).toBeTruthy() - expect(newPageData).toBeTruthy() - expect(newRouteHandlerData).toBeTruthy() - expect(newEdgeRouteHandlerData).toBeTruthy() - expect(newLayoutData).not.toBe(initLayoutData) - expect(newPageData).not.toBe(initPageData) - expect(newNestedCacheData).not.toBe(initNestedCacheData) - expect(newRouteHandlerData).not.toEqual(initRouteHandlerData) - expect(newEdgeRouteHandlerData).not.toEqual(initEdgeRouteHandlerRes) - return 'success' - }, 'success') - } - ) - } + const newEdgeRouteHandlerData = await newEdgeRouteHandlerRes.json() - // On-Demand Revalidate has not effect in dev since app routes - // aren't considered static until prerendering - if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) { - it('should not revalidate / when revalidate is not used', async () => { - let prevData + expect(newLayoutData).toBeTruthy() + expect(newPageData).toBeTruthy() + expect(newRouteHandlerData).toBeTruthy() + expect(newEdgeRouteHandlerData).toBeTruthy() + expect(newLayoutData).not.toBe(initLayoutData) + expect(newPageData).not.toBe(initPageData) + expect(newNestedCacheData).not.toBe(initNestedCacheData) + expect(newRouteHandlerData).not.toEqual(initRouteHandlerData) + expect(newEdgeRouteHandlerData).not.toEqual(initEdgeRouteHandlerRes) + return 'success' + }, 'success') + } + ) + } - for (let i = 0; i < 5; i++) { - const res = await next.fetch('/') - const html = await res.text() - const $ = cheerio.load(html) - const data = $('#page-data').text() + // On-Demand Revalidate has not effect in dev since app routes + // aren't considered static until prerendering + if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) { + it('should not revalidate / when revalidate is not used', async () => { + let prevData - expect(res.status).toBe(200) + for (let i = 0; i < 5; i++) { + const res = await next.fetch('/') + const html = await res.text() + const $ = cheerio.load(html) + const data = $('#page-data').text() - if (prevData) { - expect(prevData).toBe(data) - prevData = data - } - await waitFor(500) - } + expect(res.status).toBe(200) - if (isNextStart) { - expect(next.cliOutput.substring(buildCliOutputIndex)).not.toContain( - 'rendering index' - ) + if (prevData) { + expect(prevData).toBe(data) + prevData = data } - }) + await waitFor(500) + } - it.each([ - { - type: 'edge route handler', - revalidateApi: '/api/revalidate-path-edge', - }, - { - type: 'node route handler', - revalidateApi: '/api/revalidate-path-node', - }, - ])( - 'it should revalidate correctly with $type', - async ({ revalidateApi }) => { - const initRes = await next.fetch( - '/variable-revalidate/revalidate-360-isr' - ) - const html = await initRes.text() - const $ = cheerio.load(html) - const initLayoutData = $('#layout-data').text() - const initPageData = $('#page-data').text() - - expect(initLayoutData).toBeTruthy() - expect(initPageData).toBeTruthy() - - await check(async () => { - const revalidateRes = await next.fetch( - `${revalidateApi}?path=/variable-revalidate/revalidate-360-isr` - ) - expect((await revalidateRes.json()).revalidated).toBe(true) - - const newRes = await next.fetch( - '/variable-revalidate/revalidate-360-isr' - ) - const newHtml = await newRes.text() - const new$ = cheerio.load(newHtml) - const newLayoutData = new$('#layout-data').text() - const newPageData = new$('#page-data').text() - - expect(newLayoutData).toBeTruthy() - expect(newPageData).toBeTruthy() - expect(newLayoutData).not.toBe(initLayoutData) - expect(newPageData).not.toBe(initPageData) - return 'success' - }, 'success') - } - ) - } + if (isNextStart) { + expect(next.cliOutput.substring(buildCliOutputIndex)).not.toContain( + 'rendering index' + ) + } + }) - // On-Demand Revalidate has not effect in dev - if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) { - it('should revalidate all fetches during on-demand revalidate', async () => { + it.each([ + { + type: 'edge route handler', + revalidateApi: '/api/revalidate-path-edge', + }, + { + type: 'node route handler', + revalidateApi: '/api/revalidate-path-node', + }, + ])( + 'it should revalidate correctly with $type', + async ({ revalidateApi }) => { const initRes = await next.fetch( '/variable-revalidate/revalidate-360-isr' ) @@ -482,7 +441,7 @@ createNextDescribe( await check(async () => { const revalidateRes = await next.fetch( - '/api/revalidate-path-node?path=/variable-revalidate/revalidate-360-isr' + `${revalidateApi}?path=/variable-revalidate/revalidate-360-isr` ) expect((await revalidateRes.json()).revalidated).toBe(true) @@ -500,140 +459,174 @@ createNextDescribe( expect(newPageData).not.toBe(initPageData) return 'success' }, 'success') - }) - } + } + ) + } - it('should correctly handle fetchCache = "force-no-store"', async () => { - const initRes = await next.fetch('/force-no-store') + // On-Demand Revalidate has not effect in dev + if (!(global as any).isNextDev && !process.env.CUSTOM_CACHE_HANDLER) { + it('should revalidate all fetches during on-demand revalidate', async () => { + const initRes = await next.fetch( + '/variable-revalidate/revalidate-360-isr' + ) const html = await initRes.text() const $ = cheerio.load(html) + const initLayoutData = $('#layout-data').text() const initPageData = $('#page-data').text() - expect(initPageData).toBeTruthy() - - const newRes = await next.fetch('/force-no-store') - const newHtml = await newRes.text() - const new$ = cheerio.load(newHtml) - const newPageData = new$('#page-data').text() - expect(newPageData).toBeTruthy() - expect(newPageData).not.toBe(initPageData) - }) + expect(initLayoutData).toBeTruthy() + expect(initPageData).toBeTruthy() - if (!process.env.CUSTOM_CACHE_HANDLER) { - it('should revalidate correctly with config and fetch revalidate', async () => { - const initial$ = await next.render$( - '/variable-config-revalidate/revalidate-3' + await check(async () => { + const revalidateRes = await next.fetch( + '/api/revalidate-path-node?path=/variable-revalidate/revalidate-360-isr' ) - const initialDate = initial$('#date').text() - const initialRandomData = initial$('#random-data').text() + expect((await revalidateRes.json()).revalidated).toBe(true) - expect(initialDate).toBeTruthy() - expect(initialRandomData).toBeTruthy() - - let prevInitialDate - let prevInitialRandomData + const newRes = await next.fetch( + '/variable-revalidate/revalidate-360-isr' + ) + const newHtml = await newRes.text() + const new$ = cheerio.load(newHtml) + const newLayoutData = new$('#layout-data').text() + const newPageData = new$('#page-data').text() + + expect(newLayoutData).toBeTruthy() + expect(newPageData).toBeTruthy() + expect(newLayoutData).not.toBe(initLayoutData) + expect(newPageData).not.toBe(initPageData) + return 'success' + }, 'success') + }) + } - // wait for a fresh revalidation - await check(async () => { - const $ = await next.render$( - '/variable-config-revalidate/revalidate-3' - ) - prevInitialDate = $('#date').text() - prevInitialRandomData = $('#random-data').text() + it('should correctly handle fetchCache = "force-no-store"', async () => { + const initRes = await next.fetch('/force-no-store') + const html = await initRes.text() + const $ = cheerio.load(html) + const initPageData = $('#page-data').text() + expect(initPageData).toBeTruthy() + + const newRes = await next.fetch('/force-no-store') + const newHtml = await newRes.text() + const new$ = cheerio.load(newHtml) + const newPageData = new$('#page-data').text() + + expect(newPageData).toBeTruthy() + expect(newPageData).not.toBe(initPageData) + }) + + if (!process.env.CUSTOM_CACHE_HANDLER) { + it('should revalidate correctly with config and fetch revalidate', async () => { + const initial$ = await next.render$( + '/variable-config-revalidate/revalidate-3' + ) + const initialDate = initial$('#date').text() + const initialRandomData = initial$('#random-data').text() - expect(prevInitialDate).not.toBe(initialDate) - expect(prevInitialRandomData).not.toBe(initialRandomData) - return 'success' - }, 'success') + expect(initialDate).toBeTruthy() + expect(initialRandomData).toBeTruthy() - // the date should revalidate first after 3 seconds - // while the fetch data stays in place for 9 seconds - await check(async () => { - const $ = await next.render$( - '/variable-config-revalidate/revalidate-3' - ) - const curDate = $('#date').text() - const curRandomData = $('#random-data').text() + let prevInitialDate + let prevInitialRandomData - expect(curDate).not.toBe(prevInitialDate) - expect(curRandomData).not.toBe(prevInitialRandomData) + // wait for a fresh revalidation + await check(async () => { + const $ = await next.render$('/variable-config-revalidate/revalidate-3') + prevInitialDate = $('#date').text() + prevInitialRandomData = $('#random-data').text() - prevInitialDate = curDate - prevInitialRandomData = curRandomData - return 'success' - }, 'success') - }) - } + expect(prevInitialDate).not.toBe(initialDate) + expect(prevInitialRandomData).not.toBe(initialRandomData) + return 'success' + }, 'success') - it('should not cache non-ok statusCode', async () => { + // the date should revalidate first after 3 seconds + // while the fetch data stays in place for 9 seconds await check(async () => { - const $ = await next.render$('/variable-revalidate/status-code') - const origData = JSON.parse($('#page-data').text()) + const $ = await next.render$('/variable-config-revalidate/revalidate-3') + const curDate = $('#date').text() + const curRandomData = $('#random-data').text() - expect(origData.status).toBe(404) + expect(curDate).not.toBe(prevInitialDate) + expect(curRandomData).not.toBe(prevInitialRandomData) - const new$ = await next.render$('/variable-revalidate/status-code') - const newData = JSON.parse(new$('#page-data').text()) - expect(newData.status).toBe(origData.status) - expect(newData.text).not.toBe(origData.text) + prevInitialDate = curDate + prevInitialRandomData = curRandomData return 'success' }, 'success') }) + } - if (isNextStart) { - if (!process.env.__NEXT_EXPERIMENTAL_PPR) { - it('should have deterministic etag across revalidates', async () => { - const initialRes = await next.fetch( - '/variable-revalidate-stable/revalidate-3' - ) - expect(initialRes.status).toBe(200) - - // check 2 revalidate passes to ensure it's consistent - for (let i = 0; i < 2; i++) { - let startIdx = next.cliOutput.length - - await retry( - async () => { - const res = await next.fetch( - '/variable-revalidate-stable/revalidate-3' - ) - expect(next.cliOutput.substring(startIdx)).toContain( - 'rendering /variable-revalidate-stable' - ) - expect(initialRes.headers.get('etag')).toBe( - res.headers.get('etag') - ) - }, - 12_000, - 3_000 - ) - } - }) - } + it('should not cache non-ok statusCode', async () => { + await check(async () => { + const $ = await next.render$('/variable-revalidate/status-code') + const origData = JSON.parse($('#page-data').text()) + + expect(origData.status).toBe(404) - it('should output HTML/RSC files for static paths', async () => { - const files = ( - await glob('**/*', { - cwd: join(next.testDir, '.next/server/app'), - }) + const new$ = await next.render$('/variable-revalidate/status-code') + const newData = JSON.parse(new$('#page-data').text()) + expect(newData.status).toBe(origData.status) + expect(newData.text).not.toBe(origData.text) + return 'success' + }, 'success') + }) + + if (isNextStart) { + if (!process.env.__NEXT_EXPERIMENTAL_PPR) { + it('should have deterministic etag across revalidates', async () => { + const initialRes = await next.fetch( + '/variable-revalidate-stable/revalidate-3' ) - .filter((file) => file.match(/.*\.(js|html|rsc)$/)) - .map((file) => { - return file.replace( - /partial-gen-params-no-additional-([\w]{1,})\/([\w]{1,})\/([\d]{1,})/, - 'partial-gen-params-no-additional-$1/$2/RAND' - ) - }) - - expect(files.sort()).toMatchInlineSnapshot(` - [ - "(new)/custom/page.js", - "(new)/custom/page_client-reference-manifest.js", - "_not-found.html", - "_not-found.rsc", - "_not-found/page.js", - "_not-found/page_client-reference-manifest.js", - "api/draft-mode/route.js", + expect(initialRes.status).toBe(200) + + // check 2 revalidate passes to ensure it's consistent + for (let i = 0; i < 2; i++) { + let startIdx = next.cliOutput.length + + await retry( + async () => { + const res = await next.fetch( + '/variable-revalidate-stable/revalidate-3' + ) + expect(next.cliOutput.substring(startIdx)).toContain( + 'rendering /variable-revalidate-stable' + ) + expect(initialRes.headers.get('etag')).toBe( + res.headers.get('etag') + ) + }, + 12_000, + 3_000 + ) + } + }) + } + + it('should output HTML/RSC files for static paths', async () => { + const files = ( + await glob('**/*', { + cwd: join(next.testDir, '.next/server/app'), + }) + ) + .filter((file) => file.match(/.*\.(js|html|rsc)$/)) + .map((file) => { + return file.replace( + /partial-gen-params-no-additional-([\w]{1,})\/([\w]{1,})\/([\d]{1,})/, + 'partial-gen-params-no-additional-$1/$2/RAND' + ) + }) + + expect(files.sort()).toMatchInlineSnapshot(` + [ + "(new)/custom/page.js", + "(new)/custom/page_client-reference-manifest.js", + "_not-found.html", + "_not-found.rsc", + "_not-found/page.js", + "_not-found/page_client-reference-manifest.js", + "api/draft-mode/route.js", "api/large-data/route.js", "api/revalidate-path-edge/route.js", "api/revalidate-path-node/route.js", @@ -876,39 +869,39 @@ createNextDescribe( "variable-revalidate/status-code/page_client-reference-manifest.js", ] `) - }) + }) - it('should have correct prerender-manifest entries', async () => { - const curManifest = JSON.parse(JSON.stringify(prerenderManifest)) + it('should have correct prerender-manifest entries', async () => { + const curManifest = JSON.parse(JSON.stringify(prerenderManifest)) - for (const key of Object.keys(curManifest.dynamicRoutes)) { - const item = curManifest.dynamicRoutes[key] + for (const key of Object.keys(curManifest.dynamicRoutes)) { + const item = curManifest.dynamicRoutes[key] - if (item.dataRouteRegex) { - item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex) - } - if (item.routeRegex) { - item.routeRegex = normalizeRegEx(item.routeRegex) - } + if (item.dataRouteRegex) { + item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex) } + if (item.routeRegex) { + item.routeRegex = normalizeRegEx(item.routeRegex) + } + } - for (const key of Object.keys(curManifest.routes)) { - const newKey = key.replace( - /partial-gen-params-no-additional-([\w]{1,})\/([\w]{1,})\/([\d]{1,})/, - 'partial-gen-params-no-additional-$1/$2/RAND' - ) - if (newKey !== key) { - const route = curManifest.routes[key] - delete curManifest.routes[key] - curManifest.routes[newKey] = { - ...route, - dataRoute: `${newKey}.rsc`, - } + for (const key of Object.keys(curManifest.routes)) { + const newKey = key.replace( + /partial-gen-params-no-additional-([\w]{1,})\/([\w]{1,})\/([\d]{1,})/, + 'partial-gen-params-no-additional-$1/$2/RAND' + ) + if (newKey !== key) { + const route = curManifest.routes[key] + delete curManifest.routes[key] + curManifest.routes[newKey] = { + ...route, + dataRoute: `${newKey}.rsc`, } } + } - expect(curManifest.version).toBe(4) - expect(curManifest.routes).toMatchInlineSnapshot(` + expect(curManifest.version).toBe(4) + expect(curManifest.routes).toMatchInlineSnapshot(` { "/": { "dataRoute": "/index.rsc", @@ -1696,7 +1689,7 @@ createNextDescribe( }, } `) - expect(curManifest.dynamicRoutes).toMatchInlineSnapshot(` + expect(curManifest.dynamicRoutes).toMatchInlineSnapshot(` { "/articles/[slug]": { "dataRoute": "/articles/[slug].rsc", @@ -1887,144 +1880,183 @@ createNextDescribe( }, } `) - }) + }) - it('should output debug info for static bailouts', async () => { - const cleanedOutput = stripAnsi(next.cliOutput) + it('should output debug info for static bailouts', async () => { + const cleanedOutput = stripAnsi(next.cliOutput) - expect(cleanedOutput).toContain( - 'Static generation failed due to dynamic usage on /force-static, reason: headers' - ) - expect(cleanedOutput).toContain( - 'Static generation failed due to dynamic usage on /ssr-auto/cache-no-store, reason: no-store fetch' - ) + expect(cleanedOutput).toContain( + 'Static generation failed due to dynamic usage on /force-static, reason: headers' + ) + expect(cleanedOutput).toContain( + 'Static generation failed due to dynamic usage on /ssr-auto/cache-no-store, reason: no-store fetch' + ) + }) + + // build cache not leveraged for custom cache handler so not seeded + if (!process.env.CUSTOM_CACHE_HANDLER) { + it('should correctly error and not update cache for ISR', async () => { + await next.patchFile('app/isr-error-handling/error.txt', 'yes') + + for (let i = 0; i < 3; i++) { + const res = await next.fetch('/isr-error-handling') + const html = await res.text() + const $ = cheerio.load(html) + const now = $('#now').text() + + expect(res.status).toBe(200) + expect(now).toBeTruthy() + + // wait revalidate period + await waitFor(3000) + } + expect(next.cliOutput).toContain('intentional error') }) + } + } - // build cache not leveraged for custom cache handler so not seeded - if (!process.env.CUSTOM_CACHE_HANDLER) { - it('should correctly error and not update cache for ISR', async () => { - await next.patchFile('app/isr-error-handling/error.txt', 'yes') + it.each([ + { path: '/stale-cache-serving/app-page' }, + { path: '/stale-cache-serving/route-handler' }, + { path: '/stale-cache-serving-edge/app-page' }, + { path: '/stale-cache-serving-edge/route-handler' }, + ])('should stream properly for $path', async ({ path }) => { + // Prime the cache. + let res = await next.fetch(path) + expect(res.status).toBe(200) + + // Consume the cache, the revalidations are completed on the end of the + // stream so we need to wait for that to complete. + await res.text() + + for (let i = 0; i < 6; i++) { + await waitFor(1000) - for (let i = 0; i < 3; i++) { - const res = await next.fetch('/isr-error-handling') - const html = await res.text() - const $ = cheerio.load(html) - const now = $('#now').text() + const timings = { + start: Date.now(), + startedStreaming: 0, + } - expect(res.status).toBe(200) - expect(now).toBeTruthy() + res = await next.fetch(path) - // wait revalidate period - await waitFor(3000) + // eslint-disable-next-line no-loop-func + await new Promise((resolve) => { + res.body.on('data', () => { + if (!timings.startedStreaming) { + timings.startedStreaming = Date.now() } - expect(next.cliOutput).toContain('intentional error') }) - } - } - it.each([ - { path: '/stale-cache-serving/app-page' }, - { path: '/stale-cache-serving/route-handler' }, - { path: '/stale-cache-serving-edge/app-page' }, - { path: '/stale-cache-serving-edge/route-handler' }, - ])('should stream properly for $path', async ({ path }) => { - // Prime the cache. - let res = await next.fetch(path) - expect(res.status).toBe(200) + res.body.on('end', () => { + resolve() + }) + }) - // Consume the cache, the revalidations are completed on the end of the - // stream so we need to wait for that to complete. - await res.text() + expect(timings.startedStreaming - timings.start).toBeLessThan(3000) + } + }) - for (let i = 0; i < 6; i++) { - await waitFor(1000) + it('should correctly handle statusCode with notFound + ISR', async () => { + for (let i = 0; i < 5; i++) { + const res = await next.fetch('/articles/non-existent') + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + await waitFor(500) + } + }) - const timings = { - start: Date.now(), - startedStreaming: 0, - } + it('should cache correctly for fetchCache = default-cache', async () => { + const res = await next.fetch('/default-cache') + expect(res.status).toBe(200) - res = await next.fetch(path) + let prevHtml = await res.text() + let prev$ = cheerio.load(prevHtml) - // eslint-disable-next-line no-loop-func - await new Promise((resolve) => { - res.body.on('data', () => { - if (!timings.startedStreaming) { - timings.startedStreaming = Date.now() - } - }) + await check(async () => { + const curRes = await next.fetch('/default-cache') + expect(curRes.status).toBe(200) - res.body.on('end', () => { - resolve() - }) - }) + const curHtml = await curRes.text() + const cur$ = cheerio.load(curHtml) - expect(timings.startedStreaming - timings.start).toBeLessThan(3000) + try { + expect(cur$('#data-no-cache').text()).not.toBe( + prev$('#data-no-cache').text() + ) + expect(cur$('#data-force-cache').text()).toBe( + prev$('#data-force-cache').text() + ) + expect(cur$('#data-revalidate-cache').text()).toBe( + prev$('#data-revalidate-cache').text() + ) + expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe( + prev$('#data-revalidate-and-fetch-cache').text() + ) + expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe( + prev$('#data-revalidate-and-fetch-cache').text() + ) + } finally { + prevHtml = curHtml + prev$ = cur$ } - }) + return 'success' + }, 'success') + }) - it('should correctly handle statusCode with notFound + ISR', async () => { - for (let i = 0; i < 5; i++) { - const res = await next.fetch('/articles/non-existent') - expect(res.status).toBe(404) - expect(await res.text()).toContain('This page could not be found') - await waitFor(500) - } - }) + it('should cache correctly for fetchCache = force-cache', async () => { + const res = await next.fetch('/force-cache') + expect(res.status).toBe(200) - it('should cache correctly for fetchCache = default-cache', async () => { - const res = await next.fetch('/default-cache') - expect(res.status).toBe(200) + let prevHtml = await res.text() + let prev$ = cheerio.load(prevHtml) - let prevHtml = await res.text() - let prev$ = cheerio.load(prevHtml) + await check(async () => { + const curRes = await next.fetch('/force-cache') + expect(curRes.status).toBe(200) - await check(async () => { - const curRes = await next.fetch('/default-cache') - expect(curRes.status).toBe(200) + const curHtml = await curRes.text() + const cur$ = cheerio.load(curHtml) - const curHtml = await curRes.text() - const cur$ = cheerio.load(curHtml) + expect(cur$('#data-no-cache').text()).toBe(prev$('#data-no-cache').text()) + expect(cur$('#data-force-cache').text()).toBe( + prev$('#data-force-cache').text() + ) + expect(cur$('#data-revalidate-cache').text()).toBe( + prev$('#data-revalidate-cache').text() + ) + expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe( + prev$('#data-revalidate-and-fetch-cache').text() + ) + expect(cur$('#data-auto-cache').text()).toBe( + prev$('#data-auto-cache').text() + ) - try { - expect(cur$('#data-no-cache').text()).not.toBe( - prev$('#data-no-cache').text() - ) - expect(cur$('#data-force-cache').text()).toBe( - prev$('#data-force-cache').text() - ) - expect(cur$('#data-revalidate-cache').text()).toBe( - prev$('#data-revalidate-cache').text() - ) - expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe( - prev$('#data-revalidate-and-fetch-cache').text() - ) - expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe( - prev$('#data-revalidate-and-fetch-cache').text() - ) - } finally { - prevHtml = curHtml - prev$ = cur$ - } - return 'success' - }, 'success') - }) + return 'success' + }, 'success') - it('should cache correctly for fetchCache = force-cache', async () => { - const res = await next.fetch('/force-cache') - expect(res.status).toBe(200) + if (!isNextDeploy) { + expect(next.cliOutput).toContain( + 'fetch for https://next-data-api-endpoint.vercel.app/api/random?d4 on /force-cache specified "cache: force-cache" and "revalidate: 3", only one should be specified.' + ) + } + }) - let prevHtml = await res.text() - let prev$ = cheerio.load(prevHtml) + it('should cache correctly for cache: no-store', async () => { + const res = await next.fetch('/fetch-no-cache') + expect(res.status).toBe(200) - await check(async () => { - const curRes = await next.fetch('/force-cache') - expect(curRes.status).toBe(200) + let prevHtml = await res.text() + let prev$ = cheerio.load(prevHtml) - const curHtml = await curRes.text() - const cur$ = cheerio.load(curHtml) + await check(async () => { + const curRes = await next.fetch('/fetch-no-cache') + expect(curRes.status).toBe(200) - expect(cur$('#data-no-cache').text()).toBe( + const curHtml = await curRes.text() + const cur$ = cheerio.load(curHtml) + + try { + expect(cur$('#data-no-cache').text()).not.toBe( prev$('#data-no-cache').text() ) expect(cur$('#data-force-cache').text()).toBe( @@ -2036,351 +2068,348 @@ createNextDescribe( expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe( prev$('#data-revalidate-and-fetch-cache').text() ) - expect(cur$('#data-auto-cache').text()).toBe( + expect(cur$('#data-auto-cache').text()).not.toBe( prev$('#data-auto-cache').text() ) + } finally { + prevHtml = curHtml + prev$ = cur$ + } + return 'success' + }, 'success') + }) - return 'success' - }, 'success') + if (isDev) { + it('should bypass fetch cache with cache-control: no-cache', async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate/revalidate-3' + ) - if (!isNextDeploy) { - expect(next.cliOutput).toContain( - 'fetch for https://next-data-api-endpoint.vercel.app/api/random?d4 on /force-cache specified "cache: force-cache" and "revalidate: 3", only one should be specified.' - ) - } + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) + + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() + + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate/revalidate-3', + undefined, + { + headers: { + 'cache-control': 'no-cache', + }, + } + ) + + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) + expect($2('#layout-data').text()).not.toBe(layoutData) + expect($2('#page-data').text()).not.toBe(pageData) }) + } else { + it('should not error with dynamic server usage with force-static', async () => { + const res = await next.fetch( + '/static-to-dynamic-error-forced/static-bailout-1' + ) + const outputIndex = next.cliOutput.length + const html = await res.text() - it('should cache correctly for cache: no-store', async () => { - const res = await next.fetch('/fetch-no-cache') expect(res.status).toBe(200) + expect(html).toContain('/static-to-dynamic-error-forced') + expect(html).toMatch(/id:.*?static-bailout-1/) - let prevHtml = await res.text() - let prev$ = cheerio.load(prevHtml) + if (isNextStart) { + expect(stripAnsi(next.cliOutput).substring(outputIndex)).not.toMatch( + /Page changed from static to dynamic at runtime \/static-to-dynamic-error-forced\/static-bailout-1, reason: cookies/ + ) + } + }) - await check(async () => { - const curRes = await next.fetch('/fetch-no-cache') - expect(curRes.status).toBe(200) + it('should produce response with url from fetch', async () => { + const res = await next.fetch('/response-url') + expect(res.status).toBe(200) - const curHtml = await curRes.text() - const cur$ = cheerio.load(curHtml) + const html = await res.text() + const $ = cheerio.load(html) - try { - expect(cur$('#data-no-cache').text()).not.toBe( - prev$('#data-no-cache').text() - ) - expect(cur$('#data-force-cache').text()).toBe( - prev$('#data-force-cache').text() - ) - expect(cur$('#data-revalidate-cache').text()).toBe( - prev$('#data-revalidate-cache').text() - ) - expect(cur$('#data-revalidate-and-fetch-cache').text()).toBe( - prev$('#data-revalidate-and-fetch-cache').text() - ) - expect(cur$('#data-auto-cache').text()).not.toBe( - prev$('#data-auto-cache').text() - ) - } finally { - prevHtml = curHtml - prev$ = cur$ - } - return 'success' - }, 'success') + expect($('#data-url-default-cache').text()).toBe( + 'https://next-data-api-endpoint.vercel.app/api/random?a1' + ) + expect($('#data-url-no-cache').text()).toBe( + 'https://next-data-api-endpoint.vercel.app/api/random?b2' + ) + expect($('#data-url-cached').text()).toBe( + 'https://next-data-api-endpoint.vercel.app/api/random?a1' + ) + expect($('#data-value-default-cache').text()).toBe( + $('#data-value-cached').text() + ) }) - if (isDev) { - it('should bypass fetch cache with cache-control: no-cache', async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate/revalidate-3' - ) - - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + it('should properly error when dynamic = "error" page uses dynamic', async () => { + const res = await next.fetch('/dynamic-error/static-bailout-1') + const outputIndex = next.cliOutput.length - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + expect(res.status).toBe(500) - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate/revalidate-3', - undefined, - { - headers: { - 'cache-control': 'no-cache', - }, - } + if (isNextStart) { + expect(stripAnsi(next.cliOutput).substring(outputIndex)).not.toMatch( + /Page with dynamic = "error" encountered dynamic data method on \/dynamic-error\/static-bailout-1/ ) + } + }) + } - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) - expect($2('#layout-data').text()).not.toBe(layoutData) - expect($2('#page-data').text()).not.toBe(pageData) - }) - } else { - it('should not error with dynamic server usage with force-static', async () => { - const res = await next.fetch( - '/static-to-dynamic-error-forced/static-bailout-1' - ) - const outputIndex = next.cliOutput.length - const html = await res.text() + it('should skip cache in draft mode', async () => { + const draftRes = await next.fetch('/api/draft-mode?status=enable') + const setCookie = draftRes.headers.get('set-cookie') + const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] } - expect(res.status).toBe(200) - expect(html).toContain('/static-to-dynamic-error-forced') - expect(html).toMatch(/id:.*?static-bailout-1/) + expect(cookieHeader.Cookie).toBeTruthy() - if (isNextStart) { - expect(stripAnsi(next.cliOutput).substring(outputIndex)).not.toMatch( - /Page changed from static to dynamic at runtime \/static-to-dynamic-error-forced\/static-bailout-1, reason: cookies/ - ) - } - }) + const res = await next.fetch('/ssg-draft-mode/test-1', { + headers: cookieHeader, + }) - it('should produce response with url from fetch', async () => { - const res = await next.fetch('/response-url') - expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) + const data1 = $('#data').text() - const html = await res.text() - const $ = cheerio.load(html) + expect(data1).toBeTruthy() + expect(JSON.parse($('#draft-mode').text())).toEqual({ isEnabled: true }) - expect($('#data-url-default-cache').text()).toBe( - 'https://next-data-api-endpoint.vercel.app/api/random?a1' - ) - expect($('#data-url-no-cache').text()).toBe( - 'https://next-data-api-endpoint.vercel.app/api/random?b2' - ) - expect($('#data-url-cached').text()).toBe( - 'https://next-data-api-endpoint.vercel.app/api/random?a1' - ) - expect($('#data-value-default-cache').text()).toBe( - $('#data-value-cached').text() - ) - }) + const res2 = await next.fetch('/ssg-draft-mode/test-1', { + headers: cookieHeader, + }) - it('should properly error when dynamic = "error" page uses dynamic', async () => { - const res = await next.fetch('/dynamic-error/static-bailout-1') - const outputIndex = next.cliOutput.length + const html2 = await res2.text() + const $2 = cheerio.load(html2) + const data2 = $2('#data').text() - expect(res.status).toBe(500) + expect(data2).toBeTruthy() + expect(data1).not.toBe(data2) + expect(JSON.parse($2('#draft-mode').text())).toEqual({ isEnabled: true }) + }) - if (isNextStart) { - expect(stripAnsi(next.cliOutput).substring(outputIndex)).not.toMatch( - /Page with dynamic = "error" encountered dynamic data method on \/dynamic-error\/static-bailout-1/ - ) - } - }) - } + it('should handle partial-gen-params with default dynamicParams correctly', async () => { + const res = await next.fetch('/partial-gen-params/en/first') + expect(res.status).toBe(200) - it('should skip cache in draft mode', async () => { - const draftRes = await next.fetch('/api/draft-mode?status=enable') - const setCookie = draftRes.headers.get('set-cookie') - const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] } + const html = await res.text() + const $ = cheerio.load(html) + const params = JSON.parse($('#params').text()) - expect(cookieHeader.Cookie).toBeTruthy() + expect(params).toEqual({ lang: 'en', slug: 'first' }) + }) - const res = await next.fetch('/ssg-draft-mode/test-1', { - headers: cookieHeader, - }) + it('should handle partial-gen-params with layout dynamicParams = false correctly', async () => { + for (const { path, status, params } of [ + // these checks don't work with custom memory only + // cache handler + ...(process.env.CUSTOM_CACHE_HANDLER + ? [] + : [ + { + path: '/partial-gen-params-no-additional-lang/en/first', + status: 200, + params: { lang: 'en', slug: 'first' }, + }, + ]), + { + path: '/partial-gen-params-no-additional-lang/de/first', + status: 404, + params: {}, + }, + { + path: '/partial-gen-params-no-additional-lang/en/non-existent', + status: 404, + params: {}, + }, + ]) { + const res = await next.fetch(path) + expect(res.status).toBe(status) const html = await res.text() const $ = cheerio.load(html) - const data1 = $('#data').text() + const curParams = JSON.parse($('#params').text() || '{}') - expect(data1).toBeTruthy() - expect(JSON.parse($('#draft-mode').text())).toEqual({ isEnabled: true }) + expect(curParams).toEqual(params) + } + }) - const res2 = await next.fetch('/ssg-draft-mode/test-1', { - headers: cookieHeader, - }) + it('should handle partial-gen-params with page dynamicParams = false correctly', async () => { + for (const { path, status, params } of [ + // these checks don't work with custom memory only + // cache handler + ...(process.env.CUSTOM_CACHE_HANDLER + ? [] + : [ + { + path: '/partial-gen-params-no-additional-slug/en/first', + status: 200, + params: { lang: 'en', slug: 'first' }, + }, + ]), + { + path: '/partial-gen-params-no-additional-slug/de/first', + status: 404, + params: {}, + }, + { + path: '/partial-gen-params-no-additional-slug/en/non-existent', + status: 404, + params: {}, + }, + ]) { + const res = await next.fetch(path) + expect(res.status).toBe(status) - const html2 = await res2.text() - const $2 = cheerio.load(html2) - const data2 = $2('#data').text() + const html = await res.text() + const $ = cheerio.load(html) + const curParams = JSON.parse($('#params').text() || '{}') - expect(data2).toBeTruthy() - expect(data1).not.toBe(data2) - expect(JSON.parse($2('#draft-mode').text())).toEqual({ isEnabled: true }) - }) + expect(curParams).toEqual(params) + } + }) + + // fetch cache in generateStaticParams needs fs for persistence + // so doesn't behave as expected with custom in memory only + // cache handler + if (!process.env.CUSTOM_CACHE_HANDLER) { + it('should honor fetch cache in generateStaticParams', async () => { + const initialRes = await next.fetch( + `/partial-gen-params-no-additional-lang/en/first` + ) - it('should handle partial-gen-params with default dynamicParams correctly', async () => { - const res = await next.fetch('/partial-gen-params/en/first') - expect(res.status).toBe(200) + expect(initialRes.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) - const params = JSON.parse($('#params').text()) + // we can't read prerender-manifest from deployment + if (isNextDeploy) return - expect(params).toEqual({ lang: 'en', slug: 'first' }) - }) + let langFetchSlug + let slugFetchSlug - it('should handle partial-gen-params with layout dynamicParams = false correctly', async () => { - for (const { path, status, params } of [ - // these checks don't work with custom memory only - // cache handler - ...(process.env.CUSTOM_CACHE_HANDLER - ? [] - : [ - { - path: '/partial-gen-params-no-additional-lang/en/first', - status: 200, - params: { lang: 'en', slug: 'first' }, - }, - ]), - { - path: '/partial-gen-params-no-additional-lang/de/first', - status: 404, - params: {}, - }, - { - path: '/partial-gen-params-no-additional-lang/en/non-existent', - status: 404, - params: {}, - }, - ]) { - const res = await next.fetch(path) - expect(res.status).toBe(status) + if (isDev) { + await check(() => { + const matches = stripAnsi(next.cliOutput).match( + /partial-gen-params fetch ([\d]{1,})/ + ) - const html = await res.text() - const $ = cheerio.load(html) - const curParams = JSON.parse($('#params').text() || '{}') + if (matches[1]) { + langFetchSlug = matches[1] + slugFetchSlug = langFetchSlug + } + return langFetchSlug ? 'success' : next.cliOutput + }, 'success') + } else { + // the fetch cache can potentially be a miss since + // the generateStaticParams are executed parallel + // in separate workers so parse value from + // prerender-manifest + const routes = Object.keys(prerenderManifest.routes) + + for (const route of routes) { + const langSlug = route.match( + /partial-gen-params-no-additional-lang\/en\/([\d]{1,})/ + )?.[1] + + if (langSlug) { + langFetchSlug = langSlug + } - expect(curParams).toEqual(params) + const slugSlug = route.match( + /partial-gen-params-no-additional-slug\/en\/([\d]{1,})/ + )?.[1] + + if (slugSlug) { + slugFetchSlug = slugSlug + } + } } - }) + require('console').log({ langFetchSlug, slugFetchSlug }) - it('should handle partial-gen-params with page dynamicParams = false correctly', async () => { - for (const { path, status, params } of [ - // these checks don't work with custom memory only - // cache handler - ...(process.env.CUSTOM_CACHE_HANDLER - ? [] - : [ - { - path: '/partial-gen-params-no-additional-slug/en/first', - status: 200, - params: { lang: 'en', slug: 'first' }, - }, - ]), + for (const { pathname, slug } of [ { - path: '/partial-gen-params-no-additional-slug/de/first', - status: 404, - params: {}, + pathname: '/partial-gen-params-no-additional-lang/en', + slug: langFetchSlug, }, { - path: '/partial-gen-params-no-additional-slug/en/non-existent', - status: 404, - params: {}, + pathname: '/partial-gen-params-no-additional-slug/en', + slug: slugFetchSlug, }, ]) { - const res = await next.fetch(path) - expect(res.status).toBe(status) - - const html = await res.text() - const $ = cheerio.load(html) - const curParams = JSON.parse($('#params').text() || '{}') - - expect(curParams).toEqual(params) + const res = await next.fetch(`${pathname}/${slug}`) + expect(res.status).toBe(200) + expect( + JSON.parse( + cheerio + .load(await res.text())('#params') + .text() + ) + ).toEqual({ lang: 'en', slug }) } }) + } - // fetch cache in generateStaticParams needs fs for persistence - // so doesn't behave as expected with custom in memory only - // cache handler - if (!process.env.CUSTOM_CACHE_HANDLER) { - it('should honor fetch cache in generateStaticParams', async () => { - const initialRes = await next.fetch( - `/partial-gen-params-no-additional-lang/en/first` - ) - - expect(initialRes.status).toBe(200) - - // we can't read prerender-manifest from deployment - if (isNextDeploy) return - - let langFetchSlug - let slugFetchSlug - - if (isDev) { - await check(() => { - const matches = stripAnsi(next.cliOutput).match( - /partial-gen-params fetch ([\d]{1,})/ - ) + it('should honor fetch cache correctly', async () => { + await check(async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate/revalidate-3' + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - if (matches[1]) { - langFetchSlug = matches[1] - slugFetchSlug = langFetchSlug - } - return langFetchSlug ? 'success' : next.cliOutput - }, 'success') - } else { - // the fetch cache can potentially be a miss since - // the generateStaticParams are executed parallel - // in separate workers so parse value from - // prerender-manifest - const routes = Object.keys(prerenderManifest.routes) - - for (const route of routes) { - const langSlug = route.match( - /partial-gen-params-no-additional-lang\/en\/([\d]{1,})/ - )?.[1] - - if (langSlug) { - langFetchSlug = langSlug - } + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() + const pageData2 = $('#page-data-2').text() - const slugSlug = route.match( - /partial-gen-params-no-additional-slug\/en\/([\d]{1,})/ - )?.[1] + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate/revalidate-3' + ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - if (slugSlug) { - slugFetchSlug = slugSlug - } - } - } - require('console').log({ langFetchSlug, slugFetchSlug }) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + expect($2('#page-data-2').text()).toBe(pageData2) + expect(pageData).toBe(pageData2) + return 'success' + }, 'success') - for (const { pathname, slug } of [ - { - pathname: '/partial-gen-params-no-additional-lang/en', - slug: langFetchSlug, - }, - { - pathname: '/partial-gen-params-no-additional-slug/en', - slug: slugFetchSlug, - }, - ]) { - const res = await next.fetch(`${pathname}/${slug}`) - expect(res.status).toBe(200) - expect( - JSON.parse( - cheerio - .load(await res.text())('#params') - .text() - ) - ).toEqual({ lang: 'en', slug }) - } - }) + if (isNextStart) { + expect(next.cliOutput).toContain( + `Page "/variable-revalidate-edge/revalidate-3" is using runtime = 'edge' which is currently incompatible with dynamic = 'force-static'. Please remove either "runtime" or "force-static" for correct behavior` + ) } + }) - it('should honor fetch cache correctly', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate/revalidate-3' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + it('should honor fetch cache correctly (edge)', async () => { + await check(async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate-edge/revalidate-3' + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) + // the test cache handler is simple and doesn't share + // state across workers so not guaranteed to have cache hit + if (!(isNextDeploy && process.env.CUSTOM_CACHE_HANDLER)) { const layoutData = $('#layout-data').text() const pageData = $('#page-data').text() - const pageData2 = $('#page-data-2').text() const res2 = await fetchViaHTTP( next.url, - '/variable-revalidate/revalidate-3' + '/variable-revalidate-edge/revalidate-3' ) expect(res2.status).toBe(200) const html2 = await res2.text() @@ -2388,1037 +2417,991 @@ createNextDescribe( expect($2('#layout-data').text()).toBe(layoutData) expect($2('#page-data').text()).toBe(pageData) - expect($2('#page-data-2').text()).toBe(pageData2) - expect(pageData).toBe(pageData2) - return 'success' - }, 'success') - - if (isNextStart) { - expect(next.cliOutput).toContain( - `Page "/variable-revalidate-edge/revalidate-3" is using runtime = 'edge' which is currently incompatible with dynamic = 'force-static'. Please remove either "runtime" or "force-static" for correct behavior` - ) } - }) - - it('should honor fetch cache correctly (edge)', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/revalidate-3' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) - - // the test cache handler is simple and doesn't share - // state across workers so not guaranteed to have cache hit - if (!(isNextDeploy && process.env.CUSTOM_CACHE_HANDLER)) { - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + return 'success' + }, 'success') + }) - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/revalidate-3' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) - - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - } - return 'success' - }, 'success') - }) + it('should cache correctly with authorization header and revalidate', async () => { + await check(async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate/authorization' + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - it('should cache correctly with authorization header and revalidate', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate/authorization' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate/authorization' + ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate/authorization' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + return 'success' + }, 'success') + }) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - return 'success' - }, 'success') - }) + it('should skip fetch cache when an authorization header is present after dynamic usage', async () => { + const initialReq = await next.fetch( + '/variable-revalidate/authorization/route-cookies' + ) + const initialJson = await initialReq.json() - it('should skip fetch cache when an authorization header is present after dynamic usage', async () => { - const initialReq = await next.fetch( + await retry(async () => { + const req = await next.fetch( '/variable-revalidate/authorization/route-cookies' ) - const initialJson = await initialReq.json() + const json = await req.json() - await retry(async () => { - const req = await next.fetch( - '/variable-revalidate/authorization/route-cookies' - ) - const json = await req.json() - - expect(json).not.toEqual(initialJson) - }) + expect(json).not.toEqual(initialJson) }) + }) - it('should skip fetch cache when accessing request properties', async () => { - const initialReq = await next.fetch( + it('should skip fetch cache when accessing request properties', async () => { + const initialReq = await next.fetch( + '/variable-revalidate/authorization/route-request' + ) + const initialJson = await initialReq.json() + + await retry(async () => { + const req = await next.fetch( '/variable-revalidate/authorization/route-request' ) - const initialJson = await initialReq.json() - - await retry(async () => { - const req = await next.fetch( - '/variable-revalidate/authorization/route-request' - ) - const json = await req.json() + const json = await req.json() - expect(json).not.toEqual(initialJson) - }) + expect(json).not.toEqual(initialJson) }) + }) - it('should not cache correctly with POST method request init', async () => { - const res = await fetchViaHTTP( + it('should not cache correctly with POST method request init', async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate-edge/post-method-request' + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) + + const pageData2 = $('#page-data2').text() + + for (let i = 0; i < 3; i++) { + const res2 = await fetchViaHTTP( next.url, '/variable-revalidate-edge/post-method-request' ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) + + expect($2('#page-data2').text()).not.toBe(pageData2) + } + }) + + it('should cache correctly with post method and revalidate', async () => { + await check(async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate/post-method' + ) expect(res.status).toBe(200) const html = await res.text() const $ = cheerio.load(html) - const pageData2 = $('#page-data2').text() - - for (let i = 0; i < 3; i++) { - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/post-method-request' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) - - expect($2('#page-data2').text()).not.toBe(pageData2) - } - }) - - it('should cache correctly with post method and revalidate', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate/post-method' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) - - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() - const dataBody1 = $('#data-body1').text() - const dataBody2 = $('#data-body2').text() - const dataBody3 = $('#data-body3').text() - const dataBody4 = $('#data-body4').text() - - expect(dataBody1).not.toBe(dataBody2) - expect(dataBody2).not.toBe(dataBody3) - expect(dataBody3).not.toBe(dataBody4) + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() + const dataBody1 = $('#data-body1').text() + const dataBody2 = $('#data-body2').text() + const dataBody3 = $('#data-body3').text() + const dataBody4 = $('#data-body4').text() - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate/post-method' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + expect(dataBody1).not.toBe(dataBody2) + expect(dataBody2).not.toBe(dataBody3) + expect(dataBody3).not.toBe(dataBody4) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - expect($2('#data-body1').text()).toBe(dataBody1) - expect($2('#data-body2').text()).toBe(dataBody2) - expect($2('#data-body3').text()).toBe(dataBody3) - return 'success' - }, 'success') - }) + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate/post-method' + ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - it('should cache correctly with post method and revalidate edge', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/post-method' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + expect($2('#data-body1').text()).toBe(dataBody1) + expect($2('#data-body2').text()).toBe(dataBody2) + expect($2('#data-body3').text()).toBe(dataBody3) + return 'success' + }, 'success') + }) + + it('should cache correctly with post method and revalidate edge', async () => { + await check(async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate-edge/post-method' + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() - const dataBody1 = $('#data-body1').text() - const dataBody2 = $('#data-body2').text() - const dataBody3 = $('#data-body3').text() - const dataBody4 = $('#data-body4').text() + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() + const dataBody1 = $('#data-body1').text() + const dataBody2 = $('#data-body2').text() + const dataBody3 = $('#data-body3').text() + const dataBody4 = $('#data-body4').text() - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/post-method' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate-edge/post-method' + ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - expect($2('#data-body1').text()).toBe(dataBody1) - expect($2('#data-body2').text()).toBe(dataBody2) - expect($2('#data-body3').text()).toBe(dataBody3) - expect($2('#data-body4').text()).toBe(dataBody4) - return 'success' - }, 'success') - }) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + expect($2('#data-body1').text()).toBe(dataBody1) + expect($2('#data-body2').text()).toBe(dataBody2) + expect($2('#data-body3').text()).toBe(dataBody3) + expect($2('#data-body4').text()).toBe(dataBody4) + return 'success' + }, 'success') + }) + + it('should cache correctly with POST method and revalidate', async () => { + await check(async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate/post-method' + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - it('should cache correctly with POST method and revalidate', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate/post-method' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate/post-method' + ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate/post-method' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + return 'success' + }, 'success') + }) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - return 'success' - }, 'success') - }) + it('should cache correctly with cookie header and revalidate', async () => { + await check(async () => { + const res = await fetchViaHTTP(next.url, '/variable-revalidate/cookie') + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - it('should cache correctly with cookie header and revalidate', async () => { - await check(async () => { - const res = await fetchViaHTTP(next.url, '/variable-revalidate/cookie') - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + const res2 = await fetchViaHTTP(next.url, '/variable-revalidate/cookie') + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - const res2 = await fetchViaHTTP(next.url, '/variable-revalidate/cookie') - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + return 'success' + }, 'success') + }) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - return 'success' - }, 'success') - }) + it('should cache correctly with utf8 encoding', async () => { + await check(async () => { + const res = await fetchViaHTTP(next.url, '/variable-revalidate/encoding') + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - it('should cache correctly with utf8 encoding', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate/encoding' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + expect(JSON.parse(pageData).jp).toBe( + '超鬼畜!激辛ボム兵スピンジャンプ Bomb Spin Jump' + ) - expect(JSON.parse(pageData).jp).toBe( - '超鬼畜!激辛ボム兵スピンジャンプ Bomb Spin Jump' - ) + const res2 = await fetchViaHTTP(next.url, '/variable-revalidate/encoding') + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate/encoding' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + return 'success' + }, 'success') + }) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - return 'success' - }, 'success') - }) + it('should cache correctly with utf8 encoding edge', async () => { + await check(async () => { + const res = await fetchViaHTTP( + next.url, + '/variable-revalidate-edge/encoding' + ) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - it('should cache correctly with utf8 encoding edge', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/encoding' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + expect(JSON.parse(pageData).jp).toBe( + '超鬼畜!激辛ボム兵スピンジャンプ Bomb Spin Jump' + ) - expect(JSON.parse(pageData).jp).toBe( - '超鬼畜!激辛ボム兵スピンジャンプ Bomb Spin Jump' - ) + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate-edge/encoding' + ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/encoding' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + return 'success' + }, 'success') + }) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - return 'success' - }, 'success') - }) + it('should cache correctly handle JSON body', async () => { + await check(async () => { + const res = await fetchViaHTTP(next.url, '/variable-revalidate-edge/body') + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) - it('should cache correctly handle JSON body', async () => { - await check(async () => { - const res = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/body' - ) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const layoutData = $('#layout-data').text() + const pageData = $('#page-data').text() - const layoutData = $('#layout-data').text() - const pageData = $('#page-data').text() + expect(pageData).toBe('{"hello":"world"}') - expect(pageData).toBe('{"hello":"world"}') + const res2 = await fetchViaHTTP( + next.url, + '/variable-revalidate-edge/body' + ) + expect(res2.status).toBe(200) + const html2 = await res2.text() + const $2 = cheerio.load(html2) - const res2 = await fetchViaHTTP( - next.url, - '/variable-revalidate-edge/body' - ) - expect(res2.status).toBe(200) - const html2 = await res2.text() - const $2 = cheerio.load(html2) + expect($2('#layout-data').text()).toBe(layoutData) + expect($2('#page-data').text()).toBe(pageData) + return 'success' + }, 'success') + }) - expect($2('#layout-data').text()).toBe(layoutData) - expect($2('#page-data').text()).toBe(pageData) - return 'success' - }, 'success') - }) + it('should not throw Dynamic Server Usage error when using generateStaticParams with draftMode', async () => { + const browserOnIndexPage = await next.browser('/ssg-draft-mode') - it('should not throw Dynamic Server Usage error when using generateStaticParams with draftMode', async () => { - const browserOnIndexPage = await next.browser('/ssg-draft-mode') + const content = await browserOnIndexPage.elementByCss('#draft-mode').text() - const content = await browserOnIndexPage - .elementByCss('#draft-mode') - .text() + expect(content).toBe('{"isEnabled":false}') + }) - expect(content).toBe('{"isEnabled":false}') + it('should force SSR correctly for headers usage', async () => { + const res = await next.fetch('/force-static', { + headers: { + Cookie: 'myCookie=cookieValue', + another: 'header', + }, }) + expect(res.status).toBe(200) - it('should force SSR correctly for headers usage', async () => { - const res = await next.fetch('/force-static', { - headers: { - Cookie: 'myCookie=cookieValue', - another: 'header', - }, - }) - expect(res.status).toBe(200) - - const html = await res.text() - const $ = cheerio.load(html) - - expect(JSON.parse($('#headers').text())).toIncludeAllMembers([ - 'cookie', - 'another', - ]) - expect(JSON.parse($('#cookies').text())).toEqual([ - { - name: 'myCookie', - value: 'cookieValue', - }, - ]) - - const firstTime = $('#now').text() + const html = await res.text() + const $ = cheerio.load(html) - if (!(global as any).isNextDev) { - const res2 = await next.fetch('/force-static') - expect(res2.status).toBe(200) - - const $2 = cheerio.load(await res2.text()) - expect(firstTime).not.toBe($2('#now').text()) - } - }) + expect(JSON.parse($('#headers').text())).toIncludeAllMembers([ + 'cookie', + 'another', + ]) + expect(JSON.parse($('#cookies').text())).toEqual([ + { + name: 'myCookie', + value: 'cookieValue', + }, + ]) - it('should allow dynamic routes to access cookies', async () => { - for (const slug of ['books', 'frameworks']) { - for (let i = 0; i < 2; i++) { - let $ = await next.render$( - `/force-dynamic-prerender/${slug}`, - {}, - { headers: { cookie: 'session=value' } } - ) + const firstTime = $('#now').text() - expect($('#slug').text()).toBe(slug) - expect($('#cookie-result').text()).toBe('has cookie') + if (!(global as any).isNextDev) { + const res2 = await next.fetch('/force-static') + expect(res2.status).toBe(200) - $ = await next.render$(`/force-dynamic-prerender/${slug}`) + const $2 = cheerio.load(await res2.text()) + expect(firstTime).not.toBe($2('#now').text()) + } + }) + + it('should allow dynamic routes to access cookies', async () => { + for (const slug of ['books', 'frameworks']) { + for (let i = 0; i < 2; i++) { + let $ = await next.render$( + `/force-dynamic-prerender/${slug}`, + {}, + { headers: { cookie: 'session=value' } } + ) - expect($('#slug').text()).toBe(slug) - expect($('#cookie-result').text()).toBe('no cookie') - } - } - }) + expect($('#slug').text()).toBe(slug) + expect($('#cookie-result').text()).toBe('has cookie') - it('should not error with generateStaticParams and dynamic data', async () => { - const res = await next.fetch('/gen-params-dynamic/one') - const html = await res.text() - expect(res.status).toBe(200) - expect(html).toContain('gen-params-dynamic/[slug]') - expect(html).toContain('one') + $ = await next.render$(`/force-dynamic-prerender/${slug}`) - const data = cheerio.load(html)('#data').text() - - for (let i = 0; i < 5; i++) { - const res2 = await next.fetch('/gen-params-dynamic/one') - expect(res2.status).toBe(200) - expect( - cheerio - .load(await res2.text())('#data') - .text() - ).not.toBe(data) + expect($('#slug').text()).toBe(slug) + expect($('#cookie-result').text()).toBe('no cookie') } - }) - - it('should not error with force-dynamic and catch-all routes', async () => { - // Regression test for https://github.com/vercel/next.js/issues/45603 - const res = await next.fetch('/force-dynamic-catch-all/slug/a') - const html = await res.text() + } + }) + + it('should not error with generateStaticParams and dynamic data', async () => { + const res = await next.fetch('/gen-params-dynamic/one') + const html = await res.text() + expect(res.status).toBe(200) + expect(html).toContain('gen-params-dynamic/[slug]') + expect(html).toContain('one') + + const data = cheerio.load(html)('#data').text() + + for (let i = 0; i < 5; i++) { + const res2 = await next.fetch('/gen-params-dynamic/one') + expect(res2.status).toBe(200) + expect( + cheerio + .load(await res2.text())('#data') + .text() + ).not.toBe(data) + } + }) + + it('should not error with force-dynamic and catch-all routes', async () => { + // Regression test for https://github.com/vercel/next.js/issues/45603 + const res = await next.fetch('/force-dynamic-catch-all/slug/a') + const html = await res.text() + expect(res.status).toBe(200) + expect(html).toContain('Dynamic catch-all route') + }) + + it('should not error with generateStaticParams and authed data on revalidate', async () => { + const res = await next.fetch('/gen-params-dynamic-revalidate/one') + const html = await res.text() + expect(res.status).toBe(200) + expect(html).toContain('gen-params-dynamic/[slug]') + expect(html).toContain('one') + const initData = cheerio.load(html)('#data').text() + + await check(async () => { + const res2 = await next.fetch('/gen-params-dynamic-revalidate/one') + + expect(res2.status).toBe(200) + + const $ = cheerio.load(await res2.text()) + expect($('#data').text()).toBeTruthy() + expect($('#data').text()).not.toBe(initData) + return 'success' + }, 'success') + }) + + if (!process.env.CUSTOM_CACHE_HANDLER) { + it('should honor dynamic = "force-static" correctly', async () => { + const res = await next.fetch('/force-static/first') expect(res.status).toBe(200) - expect(html).toContain('Dynamic catch-all route') - }) - it('should not error with generateStaticParams and authed data on revalidate', async () => { - const res = await next.fetch('/gen-params-dynamic-revalidate/one') const html = await res.text() - expect(res.status).toBe(200) - expect(html).toContain('gen-params-dynamic/[slug]') - expect(html).toContain('one') - const initData = cheerio.load(html)('#data').text() + const $ = cheerio.load(html) - await check(async () => { - const res2 = await next.fetch('/gen-params-dynamic-revalidate/one') + expect(JSON.parse($('#params').text())).toEqual({ slug: 'first' }) + expect(JSON.parse($('#headers').text())).toEqual([]) + expect(JSON.parse($('#cookies').text())).toEqual([]) + const firstTime = $('#now').text() + + if (!(global as any).isNextDev) { + const res2 = await next.fetch('/force-static/first') expect(res2.status).toBe(200) - const $ = cheerio.load(await res2.text()) - expect($('#data').text()).toBeTruthy() - expect($('#data').text()).not.toBe(initData) - return 'success' - }, 'success') + const $2 = cheerio.load(await res2.text()) + expect(firstTime).toBe($2('#now').text()) + } }) - if (!process.env.CUSTOM_CACHE_HANDLER) { - it('should honor dynamic = "force-static" correctly', async () => { - const res = await next.fetch('/force-static/first') - expect(res.status).toBe(200) + it('should honor dynamic = "force-static" correctly (lazy)', async () => { + const res = await next.fetch('/force-static/random') + expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + const html = await res.text() + const $ = cheerio.load(html) - expect(JSON.parse($('#params').text())).toEqual({ slug: 'first' }) - expect(JSON.parse($('#headers').text())).toEqual([]) - expect(JSON.parse($('#cookies').text())).toEqual([]) + expect(JSON.parse($('#params').text())).toEqual({ slug: 'random' }) + expect(JSON.parse($('#headers').text())).toEqual([]) + expect(JSON.parse($('#cookies').text())).toEqual([]) - const firstTime = $('#now').text() + const firstTime = $('#now').text() - if (!(global as any).isNextDev) { - const res2 = await next.fetch('/force-static/first') - expect(res2.status).toBe(200) + if (!(global as any).isNextDev) { + const res2 = await next.fetch('/force-static/random') + expect(res2.status).toBe(200) - const $2 = cheerio.load(await res2.text()) - expect(firstTime).toBe($2('#now').text()) - } - }) + const $2 = cheerio.load(await res2.text()) + expect(firstTime).toBe($2('#now').text()) + } + }) + } - it('should honor dynamic = "force-static" correctly (lazy)', async () => { - const res = await next.fetch('/force-static/random') - expect(res.status).toBe(200) + // since we aren't leveraging fs cache with custom handler + // then these will 404 as they are cache misses + if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) { + it('should handle dynamicParams: false correctly', async () => { + const validParams = ['tim', 'seb', 'styfle'] + for (const param of validParams) { + const res = await next.fetch(`/blog/${param}`, { + redirect: 'manual', + }) + expect(res.status).toBe(200) const html = await res.text() const $ = cheerio.load(html) - expect(JSON.parse($('#params').text())).toEqual({ slug: 'random' }) - expect(JSON.parse($('#headers').text())).toEqual([]) - expect(JSON.parse($('#cookies').text())).toEqual([]) - - const firstTime = $('#now').text() + expect(JSON.parse($('#params').text())).toEqual({ + author: param, + }) + expect($('#page').text()).toBe('/blog/[author]') + } + const invalidParams = ['timm', 'non-existent'] - if (!(global as any).isNextDev) { - const res2 = await next.fetch('/force-static/random') - expect(res2.status).toBe(200) + for (const param of invalidParams) { + const invalidRes = await next.fetch(`/blog/${param}`, { + redirect: 'manual', + }) + expect(invalidRes.status).toBe(404) + expect(await invalidRes.text()).toContain('page could not be found') + } + }) + } - const $2 = cheerio.load(await res2.text()) - expect(firstTime).toBe($2('#now').text()) - } + it('should work with forced dynamic path', async () => { + for (const slug of ['first', 'second']) { + const res = await next.fetch(`/dynamic-no-gen-params-ssr/${slug}`, { + redirect: 'manual', }) + expect(res.status).toBe(200) + expect(await res.text()).toContain(`${slug}`) } + }) - // since we aren't leveraging fs cache with custom handler - // then these will 404 as they are cache misses - if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) { - it('should handle dynamicParams: false correctly', async () => { - const validParams = ['tim', 'seb', 'styfle'] + it('should work with dynamic path no generateStaticParams', async () => { + for (const slug of ['first', 'second']) { + const res = await next.fetch(`/dynamic-no-gen-params/${slug}`, { + redirect: 'manual', + }) + expect(res.status).toBe(200) + expect(await res.text()).toContain(`${slug}`) + } + }) - for (const param of validParams) { - const res = await next.fetch(`/blog/${param}`, { - redirect: 'manual', - }) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + it('should handle dynamicParams: true correctly', async () => { + const paramsToCheck = [ + { + author: 'tim', + slug: 'first-post', + }, + { + author: 'seb', + slug: 'second-post', + }, + { + author: 'styfle', + slug: 'first-post', + }, + { + author: 'new-author', + slug: 'first-post', + }, + ] - expect(JSON.parse($('#params').text())).toEqual({ - author: param, - }) - expect($('#page').text()).toBe('/blog/[author]') - } - const invalidParams = ['timm', 'non-existent'] - - for (const param of invalidParams) { - const invalidRes = await next.fetch(`/blog/${param}`, { - redirect: 'manual', - }) - expect(invalidRes.status).toBe(404) - expect(await invalidRes.text()).toContain('page could not be found') - } + for (const params of paramsToCheck) { + const res = await next.fetch(`/blog/${params.author}/${params.slug}`, { + redirect: 'manual', }) + expect(res.status).toBe(200) + const html = await res.text() + const $ = cheerio.load(html) + + expect(JSON.parse($('#params').text())).toEqual(params) + expect($('#page').text()).toBe('/blog/[author]/[slug]') } + }) - it('should work with forced dynamic path', async () => { - for (const slug of ['first', 'second']) { - const res = await next.fetch(`/dynamic-no-gen-params-ssr/${slug}`, { - redirect: 'manual', - }) - expect(res.status).toBe(200) - expect(await res.text()).toContain(`${slug}`) - } - }) + // since we aren't leveraging fs cache with custom handler + // then these will 404 as they are cache misses + if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) { + it('should navigate to static path correctly', async () => { + const browser = await next.browser('/blog/tim') + await browser.eval('window.beforeNav = 1') - it('should work with dynamic path no generateStaticParams', async () => { - for (const slug of ['first', 'second']) { - const res = await next.fetch(`/dynamic-no-gen-params/${slug}`, { - redirect: 'manual', - }) - expect(res.status).toBe(200) - expect(await res.text()).toContain(`${slug}`) - } - }) + expect( + await browser.eval('document.documentElement.innerHTML') + ).toContain('/blog/[author]') + await browser.elementByCss('#author-2').click() - it('should handle dynamicParams: true correctly', async () => { - const paramsToCheck = [ - { - author: 'tim', - slug: 'first-post', - }, - { - author: 'seb', - slug: 'second-post', - }, - { - author: 'styfle', - slug: 'first-post', - }, - { - author: 'new-author', - slug: 'first-post', - }, - ] + await check(async () => { + const params = JSON.parse(await browser.elementByCss('#params').text()) + return params.author === 'seb' ? 'found' : params + }, 'found') - for (const params of paramsToCheck) { - const res = await next.fetch(`/blog/${params.author}/${params.slug}`, { - redirect: 'manual', - }) - expect(res.status).toBe(200) - const html = await res.text() - const $ = cheerio.load(html) + expect(await browser.eval('window.beforeNav')).toBe(1) + await browser.elementByCss('#author-1-post-1').click() - expect(JSON.parse($('#params').text())).toEqual(params) - expect($('#page').text()).toBe('/blog/[author]/[slug]') - } - }) + await check(async () => { + const params = JSON.parse(await browser.elementByCss('#params').text()) + return params.author === 'tim' && params.slug === 'first-post' + ? 'found' + : params + }, 'found') - // since we aren't leveraging fs cache with custom handler - // then these will 404 as they are cache misses - if (!(isNextStart && process.env.CUSTOM_CACHE_HANDLER)) { - it('should navigate to static path correctly', async () => { - const browser = await next.browser('/blog/tim') - await browser.eval('window.beforeNav = 1') + expect(await browser.eval('window.beforeNav')).toBe(1) + await browser.back() - expect( - await browser.eval('document.documentElement.innerHTML') - ).toContain('/blog/[author]') - await browser.elementByCss('#author-2').click() + await check(async () => { + const params = JSON.parse(await browser.elementByCss('#params').text()) + return params.author === 'seb' ? 'found' : params + }, 'found') - await check(async () => { - const params = JSON.parse( - await browser.elementByCss('#params').text() - ) - return params.author === 'seb' ? 'found' : params - }, 'found') + expect(await browser.eval('window.beforeNav')).toBe(1) + }) + } - expect(await browser.eval('window.beforeNav')).toBe(1) - await browser.elementByCss('#author-1-post-1').click() + it('should ssr dynamically when detected automatically with fetch cache option', async () => { + const pathname = '/ssr-auto/cache-no-store' + const initialRes = await next.fetch(pathname, { + redirect: 'manual', + }) + expect(initialRes.status).toBe(200) - await check(async () => { - const params = JSON.parse( - await browser.elementByCss('#params').text() - ) - return params.author === 'tim' && params.slug === 'first-post' - ? 'found' - : params - }, 'found') + const initialHtml = await initialRes.text() + const initial$ = cheerio.load(initialHtml) - expect(await browser.eval('window.beforeNav')).toBe(1) - await browser.back() + expect(initial$('#page').text()).toBe(pathname) + const initialDate = initial$('#date').text() - await check(async () => { - const params = JSON.parse( - await browser.elementByCss('#params').text() - ) - return params.author === 'seb' ? 'found' : params - }, 'found') + expect(initialHtml).toContain('Example Domain') - expect(await browser.eval('window.beforeNav')).toBe(1) - }) - } + const secondRes = await next.fetch(pathname, { + redirect: 'manual', + }) + expect(secondRes.status).toBe(200) - it('should ssr dynamically when detected automatically with fetch cache option', async () => { - const pathname = '/ssr-auto/cache-no-store' - const initialRes = await next.fetch(pathname, { - redirect: 'manual', - }) - expect(initialRes.status).toBe(200) + const secondHtml = await secondRes.text() + const second$ = cheerio.load(secondHtml) - const initialHtml = await initialRes.text() - const initial$ = cheerio.load(initialHtml) + expect(second$('#page').text()).toBe(pathname) + const secondDate = second$('#date').text() - expect(initial$('#page').text()).toBe(pathname) - const initialDate = initial$('#date').text() + expect(secondHtml).toContain('Example Domain') + expect(secondDate).not.toBe(initialDate) + }) - expect(initialHtml).toContain('Example Domain') + it('should render not found pages correctly and fallback to the default one', async () => { + const res = await next.fetch(`/blog/shu/hi`, { + redirect: 'manual', + }) + expect(res.status).toBe(404) + const html = await res.text() + expect(html).toInclude('"noindex"') + expect(html).toInclude('This page could not be found.') + }) + + // TODO-APP: support fetch revalidate case for dynamic rendering + it.skip('should ssr dynamically when detected automatically with fetch revalidate option', async () => { + const pathname = '/ssr-auto/fetch-revalidate-zero' + const initialRes = await next.fetch(pathname, { + redirect: 'manual', + }) + expect(initialRes.status).toBe(200) - const secondRes = await next.fetch(pathname, { - redirect: 'manual', - }) - expect(secondRes.status).toBe(200) + const initialHtml = await initialRes.text() + const initial$ = cheerio.load(initialHtml) - const secondHtml = await secondRes.text() - const second$ = cheerio.load(secondHtml) + expect(initial$('#page').text()).toBe(pathname) + const initialDate = initial$('#date').text() - expect(second$('#page').text()).toBe(pathname) - const secondDate = second$('#date').text() + expect(initialHtml).toContain('Example Domain') - expect(secondHtml).toContain('Example Domain') - expect(secondDate).not.toBe(initialDate) + const secondRes = await next.fetch(pathname, { + redirect: 'manual', }) + expect(secondRes.status).toBe(200) - it('should render not found pages correctly and fallback to the default one', async () => { - const res = await next.fetch(`/blog/shu/hi`, { - redirect: 'manual', - }) - expect(res.status).toBe(404) - const html = await res.text() - expect(html).toInclude('"noindex"') - expect(html).toInclude('This page could not be found.') - }) + const secondHtml = await secondRes.text() + const second$ = cheerio.load(secondHtml) - // TODO-APP: support fetch revalidate case for dynamic rendering - it.skip('should ssr dynamically when detected automatically with fetch revalidate option', async () => { - const pathname = '/ssr-auto/fetch-revalidate-zero' - const initialRes = await next.fetch(pathname, { - redirect: 'manual', - }) - expect(initialRes.status).toBe(200) + expect(second$('#page').text()).toBe(pathname) + const secondDate = second$('#date').text() - const initialHtml = await initialRes.text() - const initial$ = cheerio.load(initialHtml) + expect(secondHtml).toContain('Example Domain') + expect(secondDate).not.toBe(initialDate) + }) - expect(initial$('#page').text()).toBe(pathname) - const initialDate = initial$('#date').text() + it('should ssr dynamically when forced via config', async () => { + const initialRes = await next.fetch('/ssr-forced', { + redirect: 'manual', + }) + expect(initialRes.status).toBe(200) - expect(initialHtml).toContain('Example Domain') + const initialHtml = await initialRes.text() + const initial$ = cheerio.load(initialHtml) - const secondRes = await next.fetch(pathname, { - redirect: 'manual', - }) - expect(secondRes.status).toBe(200) + expect(initial$('#page').text()).toBe('/ssr-forced') + const initialDate = initial$('#date').text() - const secondHtml = await secondRes.text() - const second$ = cheerio.load(secondHtml) + const secondRes = await next.fetch('/ssr-forced', { + redirect: 'manual', + }) + expect(secondRes.status).toBe(200) - expect(second$('#page').text()).toBe(pathname) - const secondDate = second$('#date').text() + const secondHtml = await secondRes.text() + const second$ = cheerio.load(secondHtml) - expect(secondHtml).toContain('Example Domain') - expect(secondDate).not.toBe(initialDate) - }) + expect(second$('#page').text()).toBe('/ssr-forced') + const secondDate = second$('#date').text() - it('should ssr dynamically when forced via config', async () => { - const initialRes = await next.fetch('/ssr-forced', { - redirect: 'manual', - }) - expect(initialRes.status).toBe(200) + expect(secondDate).not.toBe(initialDate) + }) - const initialHtml = await initialRes.text() - const initial$ = cheerio.load(initialHtml) + describe('useSearchParams', () => { + describe('client', () => { + it('should bailout to client rendering - with suspense boundary', async () => { + const url = + '/hooks/use-search-params/with-suspense?first=value&second=other&third' + const browser = await next.browser(url) - expect(initial$('#page').text()).toBe('/ssr-forced') - const initialDate = initial$('#date').text() + expect(await browser.elementByCss('#params-first').text()).toBe('value') + expect(await browser.elementByCss('#params-second').text()).toBe( + 'other' + ) + expect(await browser.elementByCss('#params-third').text()).toBe('') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) - const secondRes = await next.fetch('/ssr-forced', { - redirect: 'manual', + const $ = await next.render$(url) + // dynamic page doesn't have bail out + expect($('html#__next_error__').length).toBe(0) + expect($('meta[content=noindex]').length).toBe(0) }) - expect(secondRes.status).toBe(200) - const secondHtml = await secondRes.text() - const second$ = cheerio.load(secondHtml) + it.skip('should have empty search params on force-static', async () => { + const browser = await next.browser( + '/hooks/use-search-params/force-static?first=value&second=other&third' + ) - expect(second$('#page').text()).toBe('/ssr-forced') - const secondDate = second$('#date').text() + expect(await browser.elementByCss('#params-first').text()).toBe('N/A') + expect(await browser.elementByCss('#params-second').text()).toBe('N/A') + expect(await browser.elementByCss('#params-third').text()).toBe('N/A') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) - expect(secondDate).not.toBe(initialDate) - }) + await browser.elementById('to-use-search-params').click() + await browser.waitForElementByCss('#hooks-use-search-params') - describe('useSearchParams', () => { - describe('client', () => { - it('should bailout to client rendering - with suspense boundary', async () => { - const url = - '/hooks/use-search-params/with-suspense?first=value&second=other&third' - const browser = await next.browser(url) + // Should not be empty after navigating to another page with useSearchParams + expect(await browser.elementByCss('#params-first').text()).toBe('1') + expect(await browser.elementByCss('#params-second').text()).toBe('2') + expect(await browser.elementByCss('#params-third').text()).toBe('3') + expect(await browser.elementByCss('#params-not-real').text()).toBe( + 'N/A' + ) + }) - expect(await browser.elementByCss('#params-first').text()).toBe( - 'value' - ) - expect(await browser.elementByCss('#params-second').text()).toBe( - 'other' + // TODO-APP: re-enable after investigating rewrite params + if (!(global as any).isNextDeploy) { + it('should have values from canonical url on rewrite', async () => { + const browser = await next.browser( + '/rewritten-use-search-params?first=a&second=b&third=c' ) - expect(await browser.elementByCss('#params-third').text()).toBe('') + + expect(await browser.elementByCss('#params-first').text()).toBe('a') + expect(await browser.elementByCss('#params-second').text()).toBe('b') + expect(await browser.elementByCss('#params-third').text()).toBe('c') expect(await browser.elementByCss('#params-not-real').text()).toBe( 'N/A' ) - - const $ = await next.render$(url) - // dynamic page doesn't have bail out - expect($('html#__next_error__').length).toBe(0) - expect($('meta[content=noindex]').length).toBe(0) + }) + } + }) + // Don't run these tests in development mode since they won't be statically generated + if (!isDev) { + describe('server response', () => { + it('should bailout to client rendering - with suspense boundary', async () => { + const res = await next.fetch('/hooks/use-search-params/with-suspense') + const html = await res.text() + expect(html).toInclude('

search params suspense

') }) it.skip('should have empty search params on force-static', async () => { - const browser = await next.browser( + const res = await next.fetch( '/hooks/use-search-params/force-static?first=value&second=other&third' ) + const html = await res.text() - expect(await browser.elementByCss('#params-first').text()).toBe('N/A') - expect(await browser.elementByCss('#params-second').text()).toBe( - 'N/A' - ) - expect(await browser.elementByCss('#params-third').text()).toBe('N/A') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) - - await browser.elementById('to-use-search-params').click() - await browser.waitForElementByCss('#hooks-use-search-params') + // Should not bail out to client rendering + expect(html).not.toInclude('

search params suspense

') - // Should not be empty after navigating to another page with useSearchParams - expect(await browser.elementByCss('#params-first').text()).toBe('1') - expect(await browser.elementByCss('#params-second').text()).toBe('2') - expect(await browser.elementByCss('#params-third').text()).toBe('3') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) + // Use empty search params instead + const $ = cheerio.load(html) + expect($('#params-first').text()).toBe('N/A') + expect($('#params-second').text()).toBe('N/A') + expect($('#params-third').text()).toBe('N/A') + expect($('#params-not-real').text()).toBe('N/A') }) - - // TODO-APP: re-enable after investigating rewrite params - if (!(global as any).isNextDeploy) { - it('should have values from canonical url on rewrite', async () => { - const browser = await next.browser( - '/rewritten-use-search-params?first=a&second=b&third=c' - ) - - expect(await browser.elementByCss('#params-first').text()).toBe('a') - expect(await browser.elementByCss('#params-second').text()).toBe( - 'b' - ) - expect(await browser.elementByCss('#params-third').text()).toBe('c') - expect(await browser.elementByCss('#params-not-real').text()).toBe( - 'N/A' - ) - }) - } }) - // Don't run these tests in development mode since they won't be statically generated - if (!isDev) { - describe('server response', () => { - it('should bailout to client rendering - with suspense boundary', async () => { - const res = await next.fetch( - '/hooks/use-search-params/with-suspense' - ) - const html = await res.text() - expect(html).toInclude('

search params suspense

') - }) - - it.skip('should have empty search params on force-static', async () => { - const res = await next.fetch( - '/hooks/use-search-params/force-static?first=value&second=other&third' - ) - const html = await res.text() - - // Should not bail out to client rendering - expect(html).not.toInclude('

search params suspense

') - - // Use empty search params instead - const $ = cheerio.load(html) - expect($('#params-first').text()).toBe('N/A') - expect($('#params-second').text()).toBe('N/A') - expect($('#params-third').text()).toBe('N/A') - expect($('#params-not-real').text()).toBe('N/A') - }) - }) - } - }) + } + }) - describe('usePathname', () => { - it('should have the correct values', async () => { - const $ = await next.render$('/hooks/use-pathname/slug') - expect($('#pathname').text()).toContain('/hooks/use-pathname/slug') + describe('usePathname', () => { + it('should have the correct values', async () => { + const $ = await next.render$('/hooks/use-pathname/slug') + expect($('#pathname').text()).toContain('/hooks/use-pathname/slug') - const browser = await next.browser('/hooks/use-pathname/slug') + const browser = await next.browser('/hooks/use-pathname/slug') - expect(await browser.elementByCss('#pathname').text()).toBe( - '/hooks/use-pathname/slug' - ) - }) + expect(await browser.elementByCss('#pathname').text()).toBe( + '/hooks/use-pathname/slug' + ) + }) - it('should have values from canonical url on rewrite', async () => { - const browser = await next.browser('/rewritten-use-pathname') + it('should have values from canonical url on rewrite', async () => { + const browser = await next.browser('/rewritten-use-pathname') - expect(await browser.elementByCss('#pathname').text()).toBe( - '/rewritten-use-pathname' - ) - }) + expect(await browser.elementByCss('#pathname').text()).toBe( + '/rewritten-use-pathname' + ) }) + }) - describe('unstable_noStore', () => { - it('should opt-out of static optimization', async () => { - const res = await next.fetch('/no-store/dynamic') - const html = await res.text() - const data = cheerio.load(html)('#uncached-data').text() - const res2 = await next.fetch('/no-store/dynamic') - const html2 = await res2.text() - const data2 = cheerio.load(html2)('#uncached-data').text() + describe('unstable_noStore', () => { + it('should opt-out of static optimization', async () => { + const res = await next.fetch('/no-store/dynamic') + const html = await res.text() + const data = cheerio.load(html)('#uncached-data').text() + const res2 = await next.fetch('/no-store/dynamic') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#uncached-data').text() - expect(data).not.toEqual(data2) - }) + expect(data).not.toEqual(data2) + }) - it('should not opt-out of static optimization when used in next/cache', async () => { - const res = await next.fetch('/no-store/static') - const html = await res.text() - const data = cheerio.load(html)('#data').text() - const res2 = await next.fetch('/no-store/static') - const html2 = await res2.text() - const data2 = cheerio.load(html2)('#data').text() + it('should not opt-out of static optimization when used in next/cache', async () => { + const res = await next.fetch('/no-store/static') + const html = await res.text() + const data = cheerio.load(html)('#data').text() + const res2 = await next.fetch('/no-store/static') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#data').text() - expect(data).toEqual(data2) - }) + expect(data).toEqual(data2) }) + }) - describe('unstable_cache', () => { - it('should retrieve the same value on second request', async () => { - const res = await next.fetch('/unstable-cache/dynamic') - const html = await res.text() - const data = cheerio.load(html)('#cached-data').text() - const res2 = await next.fetch('/unstable-cache/dynamic') - const html2 = await res2.text() - const data2 = cheerio.load(html2)('#cached-data').text() + describe('unstable_cache', () => { + it('should retrieve the same value on second request', async () => { + const res = await next.fetch('/unstable-cache/dynamic') + const html = await res.text() + const data = cheerio.load(html)('#cached-data').text() + const res2 = await next.fetch('/unstable-cache/dynamic') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#cached-data').text() + + expect(data).toEqual(data2) + }) - expect(data).toEqual(data2) + it('should bypass cache in draft mode', async () => { + const draftRes = await next.fetch('/api/draft-mode?status=enable') + const setCookie = draftRes.headers.get('set-cookie') + const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] } + + expect(cookieHeader.Cookie).toBeTruthy() + + const res = await next.fetch('/unstable-cache/dynamic', { + headers: cookieHeader, + }) + const html = await res.text() + const data = cheerio.load(html)('#cached-data').text() + const res2 = await next.fetch('/unstable-cache/dynamic', { + headers: cookieHeader, }) + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#cached-data').text() - it('should bypass cache in draft mode', async () => { - const draftRes = await next.fetch('/api/draft-mode?status=enable') - const setCookie = draftRes.headers.get('set-cookie') - const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] } + expect(data).not.toEqual(data2) + }) - expect(cookieHeader.Cookie).toBeTruthy() + it('should not cache new result in draft mode', async () => { + const draftRes = await next.fetch('/api/draft-mode?status=enable') + const setCookie = draftRes.headers.get('set-cookie') + const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] } - const res = await next.fetch('/unstable-cache/dynamic', { - headers: cookieHeader, - }) - const html = await res.text() - const data = cheerio.load(html)('#cached-data').text() - const res2 = await next.fetch('/unstable-cache/dynamic', { - headers: cookieHeader, - }) - const html2 = await res2.text() - const data2 = cheerio.load(html2)('#cached-data').text() + expect(cookieHeader.Cookie).toBeTruthy() - expect(data).not.toEqual(data2) + const res = await next.fetch('/unstable-cache/dynamic', { + headers: cookieHeader, }) + const html = await res.text() + const data = cheerio.load(html)('#cached-data').text() - it('should not error when retrieving the value undefined', async () => { - const res = await next.fetch('/unstable-cache/dynamic-undefined') - const html = await res.text() - const data = cheerio.load(html)('#cached-data').text() - const res2 = await next.fetch('/unstable-cache/dynamic-undefined') - const html2 = await res2.text() - const data2 = cheerio.load(html2)('#cached-data').text() + const res2 = await next.fetch('/unstable-cache/dynamic') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#cached-data').text() - expect(data).toEqual(data2) - expect(data).toEqual('typeof cachedData: undefined') - }) + expect(data).not.toEqual(data2) }) - it('should keep querystring on static page', async () => { - const browser = await next.browser('/blog/tim?message=hello-world') - const checkUrl = async () => - expect(await browser.url()).toBe( - next.url + '/blog/tim?message=hello-world' - ) + it('should not error when retrieving the value undefined', async () => { + const res = await next.fetch('/unstable-cache/dynamic-undefined') + const html = await res.text() + const data = cheerio.load(html)('#cached-data').text() + const res2 = await next.fetch('/unstable-cache/dynamic-undefined') + const html2 = await res2.text() + const data2 = cheerio.load(html2)('#cached-data').text() - checkUrl() - await waitFor(1000) - checkUrl() + expect(data).toEqual(data2) + expect(data).toEqual('typeof cachedData: undefined') + }) + }) + + it('should keep querystring on static page', async () => { + const browser = await next.browser('/blog/tim?message=hello-world') + const checkUrl = async () => + expect(await browser.url()).toBe( + next.url + '/blog/tim?message=hello-world' + ) + + checkUrl() + await waitFor(1000) + checkUrl() + }) + + if (process.env.CUSTOM_CACHE_HANDLER && !isNextDeploy) { + it('should have logs from cache-handler', () => { + expect(next.cliOutput).toContain('initialized custom cache-handler') + expect(next.cliOutput).toContain('cache-handler get') + expect(next.cliOutput).toContain('cache-handler set') }) + } - if (process.env.CUSTOM_CACHE_HANDLER && !isNextDeploy) { - it('should have logs from cache-handler', () => { - expect(next.cliOutput).toContain('initialized custom cache-handler') - expect(next.cliOutput).toContain('cache-handler get') - expect(next.cliOutput).toContain('cache-handler set') + describe('Incremental cache limits', () => { + if (process.env.CUSTOM_CACHE_HANDLER && isNextStart) { + it('should cache large data when using custom cache handler and force-cache mode', async () => { + const resp1 = await next.fetch('/force-cache/large-data') + const resp1Text = await resp1.text() + const dom1 = cheerio.load(resp1Text) + + const resp2 = await next.fetch('/force-cache/large-data') + const resp2Text = await resp2.text() + const dom2 = cheerio.load(resp2Text) + + const data1 = dom1('#now').text() + const data2 = dom2('#now').text() + expect(data1 && data2).toBeTruthy() + expect(data1).toEqual(data2) }) } + if (!process.env.CUSTOM_CACHE_HANDLER && isNextStart) { + it('should load data only at build time even if response data size is greater than 2MB and FetchCache is possible', async () => { + const cliOutputStart = next.cliOutput.length + const resp1 = await next.fetch('/force-cache/large-data') + const resp1Text = await resp1.text() + const dom1 = cheerio.load(resp1Text) + + const resp2 = await next.fetch('/force-cache/large-data') + const resp2Text = await resp2.text() + const dom2 = cheerio.load(resp2Text) + + const data1 = dom1('#now').text() + const data2 = dom2('#now').text() + expect(data1 && data2).toBeTruthy() + expect(data1).toEqual(data2) + expect( + next.cliOutput.substring(cliOutputStart).match(/Load data/g) + ).toBeNull() + }) + } + if (!process.env.CUSTOM_CACHE_HANDLER && isDev) { + it('should not cache request if response data size is greater than 2MB and FetchCache is possible in development mode', async () => { + const cliOutputStart = next.cliOutput.length + const resp1 = await next.fetch('/force-cache/large-data') + const resp1Text = await resp1.text() + const dom1 = cheerio.load(resp1Text) + + const resp2 = await next.fetch('/force-cache/large-data') + const resp2Text = await resp2.text() + const dom2 = cheerio.load(resp2Text) + + const data1 = dom1('#now').text() + const data2 = dom2('#now').text() + expect(data1 && data2).toBeTruthy() + expect(data1).not.toEqual(data2) - describe('Incremental cache limits', () => { - if (process.env.CUSTOM_CACHE_HANDLER && isNextStart) { - it('should cache large data when using custom cache handler and force-cache mode', async () => { - const resp1 = await next.fetch('/force-cache/large-data') - const resp1Text = await resp1.text() - const dom1 = cheerio.load(resp1Text) - - const resp2 = await next.fetch('/force-cache/large-data') - const resp2Text = await resp2.text() - const dom2 = cheerio.load(resp2Text) - - const data1 = dom1('#now').text() - const data2 = dom2('#now').text() - expect(data1 && data2).toBeTruthy() - expect(data1).toEqual(data2) - }) - } - if (!process.env.CUSTOM_CACHE_HANDLER && isNextStart) { - it('should load data only at build time even if response data size is greater than 2MB and FetchCache is possible', async () => { - const cliOutputStart = next.cliOutput.length - const resp1 = await next.fetch('/force-cache/large-data') - const resp1Text = await resp1.text() - const dom1 = cheerio.load(resp1Text) - - const resp2 = await next.fetch('/force-cache/large-data') - const resp2Text = await resp2.text() - const dom2 = cheerio.load(resp2Text) - - const data1 = dom1('#now').text() - const data2 = dom2('#now').text() - expect(data1 && data2).toBeTruthy() - expect(data1).toEqual(data2) + await check(async () => { expect( - next.cliOutput.substring(cliOutputStart).match(/Load data/g) - ).toBeNull() - }) - } - if (!process.env.CUSTOM_CACHE_HANDLER && isDev) { - it('should not cache request if response data size is greater than 2MB and FetchCache is possible in development mode', async () => { - const cliOutputStart = next.cliOutput.length - const resp1 = await next.fetch('/force-cache/large-data') - const resp1Text = await resp1.text() - const dom1 = cheerio.load(resp1Text) - - const resp2 = await next.fetch('/force-cache/large-data') - const resp2Text = await resp2.text() - const dom2 = cheerio.load(resp2Text) - - const data1 = dom1('#now').text() - const data2 = dom2('#now').text() - expect(data1 && data2).toBeTruthy() - expect(data1).not.toEqual(data2) - - await check(async () => { - expect( - next.cliOutput.substring(cliOutputStart).match(/Load data/g) - .length - ).toBe(2) - expect(next.cliOutput.substring(cliOutputStart)).toContain( - 'Error: Failed to set Next.js data cache, items over 2MB can not be cached' - ) - return 'success' - }, 'success') - }) - } - if (process.env.CUSTOM_CACHE_HANDLER && isDev) { - it('should cache request if response data size is greater than 2MB in development mode', async () => { - const cliOutputStart = next.cliOutput.length - const resp1 = await next.fetch('/force-cache/large-data') - const resp1Text = await resp1.text() - const dom1 = cheerio.load(resp1Text) - - const resp2 = await next.fetch('/force-cache/large-data') - const resp2Text = await resp2.text() - const dom2 = cheerio.load(resp2Text) - - const data1 = dom1('#now').text() - const data2 = dom2('#now').text() - expect(data1 && data2).toBeTruthy() - expect(data1).toEqual(data2) - - await check(async () => { - expect( - next.cliOutput.substring(cliOutputStart).match(/Load data/g) - .length - ).toBe(1) - return 'success' - }, 'success') - - expect(next.cliOutput.substring(cliOutputStart)).not.toContain( + next.cliOutput.substring(cliOutputStart).match(/Load data/g).length + ).toBe(2) + expect(next.cliOutput.substring(cliOutputStart)).toContain( 'Error: Failed to set Next.js data cache, items over 2MB can not be cached' ) - }) - } - }) + return 'success' + }, 'success') + }) + } + if (process.env.CUSTOM_CACHE_HANDLER && isDev) { + it('should cache request if response data size is greater than 2MB in development mode', async () => { + const cliOutputStart = next.cliOutput.length + const resp1 = await next.fetch('/force-cache/large-data') + const resp1Text = await resp1.text() + const dom1 = cheerio.load(resp1Text) + + const resp2 = await next.fetch('/force-cache/large-data') + const resp2Text = await resp2.text() + const dom2 = cheerio.load(resp2Text) + + const data1 = dom1('#now').text() + const data2 = dom2('#now').text() + expect(data1 && data2).toBeTruthy() + expect(data1).toEqual(data2) - it('should build dynamic param with edge runtime correctly', async () => { - const browser = await next.browser('/dynamic-param-edge/hello') - expect(await browser.elementByCss('#slug').text()).toBe('hello') - }) - } -) + await check(async () => { + expect( + next.cliOutput.substring(cliOutputStart).match(/Load data/g).length + ).toBe(1) + return 'success' + }, 'success') + + expect(next.cliOutput.substring(cliOutputStart)).not.toContain( + 'Error: Failed to set Next.js data cache, items over 2MB can not be cached' + ) + }) + } + }) + + it('should build dynamic param with edge runtime correctly', async () => { + const browser = await next.browser('/dynamic-param-edge/hello') + expect(await browser.elementByCss('#slug').text()).toBe('hello') + }) +}) diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 9976271b0054b..acd75f7f29aa9 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -1,4 +1,4 @@ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check, retry, waitFor } from 'next-test-utils' import cheerio from 'cheerio' import stripAnsi from 'strip-ansi' @@ -8,9 +8,14 @@ import stripAnsi from 'strip-ansi' // gates like this one into a single module. const isPPREnabledByDefault = process.env.__NEXT_EXPERIMENTAL_PPR === 'true' -createNextDescribe( - 'app dir - basic', - { +describe('app dir - basic', () => { + const { + next, + isNextDev: isDev, + isNextStart, + isNextDeploy, + isTurbopack, + } = nextTestSetup({ files: __dirname, buildCommand: process.env.NEXT_EXPERIMENTAL_COMPILE ? `pnpm next build --experimental-build-mode=compile` @@ -18,1559 +23,1561 @@ createNextDescribe( dependencies: { nanoid: '4.0.1', }, - }, - ({ next, isNextDev: isDev, isNextStart, isNextDeploy, isTurbopack }) => { - if (isDev && isPPREnabledByDefault) { - it('should allow returning just skeleton in dev with query', async () => { - const res = await next.fetch('/skeleton?__nextppronly=1') - expect(res.status).toBe(200) - - const html = await res.text() - expect(html).toContain('Skeleton') - expect(html).not.toContain('suspended content') - }) - } + }) - if (process.env.NEXT_EXPERIMENTAL_COMPILE) { - it('should provide query for getStaticProps page correctly', async () => { - const res = await next.fetch('/ssg?hello=world') - expect(res.status).toBe(200) + if (isDev && isPPREnabledByDefault) { + it('should allow returning just skeleton in dev with query', async () => { + const res = await next.fetch('/skeleton?__nextppronly=1') + expect(res.status).toBe(200) - const $ = cheerio.load(await res.text()) - expect(JSON.parse($('#query').text())).toEqual({ hello: 'world' }) - }) - } + const html = await res.text() + expect(html).toContain('Skeleton') + expect(html).not.toContain('suspended content') + }) + } - if (isNextStart && !process.env.NEXT_EXPERIMENTAL_COMPILE) { - it('should not have loader generated function for edge runtime', async () => { - expect( - await next.readFile('.next/server/app/dashboard/page.js') - ).not.toContain('_stringifiedConfig') - expect(await next.readFile('.next/server/middleware.js')).not.toContain( - '_middlewareConfig' - ) - }) + if (process.env.NEXT_EXPERIMENTAL_COMPILE) { + it('should provide query for getStaticProps page correctly', async () => { + const res = await next.fetch('/ssg?hello=world') + expect(res.status).toBe(200) - if (!process.env.NEXT_EXPERIMENTAL_COMPILE) { - it('should have correct size in build output', async () => { - expect(next.cliOutput).toMatch( - /\/dashboard\/another.*? *?[^0]\d{1,} [\w]{1,}B/ - ) - }) - } + const $ = cheerio.load(await res.text()) + expect(JSON.parse($('#query').text())).toEqual({ hello: 'world' }) + }) + } - it('should have correct preferredRegion values in manifest', async () => { - const middlewareManifest = JSON.parse( - await next.readFile('.next/server/middleware-manifest.json') - ) - expect( - middlewareManifest.functions['/(rootonly)/dashboard/hello/page'] - .regions - ).toEqual(['iad1', 'sfo1']) - expect(middlewareManifest.functions['/dashboard/page'].regions).toEqual( - ['iad1'] - ) - expect( - middlewareManifest.functions['/slow-page-no-loading/page'].regions - ).toEqual(['global']) + if (isNextStart && !process.env.NEXT_EXPERIMENTAL_COMPILE) { + it('should not have loader generated function for edge runtime', async () => { + expect( + await next.readFile('.next/server/app/dashboard/page.js') + ).not.toContain('_stringifiedConfig') + expect(await next.readFile('.next/server/middleware.js')).not.toContain( + '_middlewareConfig' + ) + }) - expect(middlewareManifest.functions['/test-page/page'].regions).toEqual( - ['home'] + if (!process.env.NEXT_EXPERIMENTAL_COMPILE) { + it('should have correct size in build output', async () => { + expect(next.cliOutput).toMatch( + /\/dashboard\/another.*? *?[^0]\d{1,} [\w]{1,}B/ ) - - // Inherits from the root layout. - expect( - middlewareManifest.functions['/slow-page-with-loading/page'].regions - ).toEqual(['sfo1']) }) } - it('should work for catch-all edge page', async () => { - const html = await next.render('/catch-all-edge/hello123') - const $ = cheerio.load(html) + it('should have correct preferredRegion values in manifest', async () => { + const middlewareManifest = JSON.parse( + await next.readFile('.next/server/middleware-manifest.json') + ) + expect( + middlewareManifest.functions['/(rootonly)/dashboard/hello/page'].regions + ).toEqual(['iad1', 'sfo1']) + expect(middlewareManifest.functions['/dashboard/page'].regions).toEqual([ + 'iad1', + ]) + expect( + middlewareManifest.functions['/slow-page-no-loading/page'].regions + ).toEqual(['global']) - expect(JSON.parse($('#params').text())).toEqual({ - slug: ['hello123'], - }) + expect(middlewareManifest.functions['/test-page/page'].regions).toEqual([ + 'home', + ]) + + // Inherits from the root layout. + expect( + middlewareManifest.functions['/slow-page-with-loading/page'].regions + ).toEqual(['sfo1']) }) + } - it('should return normalized dynamic route params for catch-all edge page', async () => { - const html = await next.render('/catch-all-edge/a/b/c') - const $ = cheerio.load(html) + it('should work for catch-all edge page', async () => { + const html = await next.render('/catch-all-edge/hello123') + const $ = cheerio.load(html) - expect(JSON.parse($('#params').text())).toEqual({ - slug: ['a', 'b', 'c'], - }) + expect(JSON.parse($('#params').text())).toEqual({ + slug: ['hello123'], }) + }) - it('should have correct searchParams and params (server)', async () => { - const html = await next.render('/dynamic/category-1/id-2?query1=value2') - const $ = cheerio.load(html) + it('should return normalized dynamic route params for catch-all edge page', async () => { + const html = await next.render('/catch-all-edge/a/b/c') + const $ = cheerio.load(html) - expect(JSON.parse($('#id-page-params').text())).toEqual({ - category: 'category-1', - id: 'id-2', - }) - expect(JSON.parse($('#search-params').text())).toEqual({ - query1: 'value2', - }) + expect(JSON.parse($('#params').text())).toEqual({ + slug: ['a', 'b', 'c'], }) + }) - it('should have correct searchParams and params (client)', async () => { - const browser = await next.browser( - '/dynamic-client/category-1/id-2?query1=value2' - ) - const html = await browser.eval('document.documentElement.innerHTML') - const $ = cheerio.load(html) + it('should have correct searchParams and params (server)', async () => { + const html = await next.render('/dynamic/category-1/id-2?query1=value2') + const $ = cheerio.load(html) - expect(JSON.parse($('#id-page-params').text())).toEqual({ - category: 'category-1', - id: 'id-2', - }) - expect(JSON.parse($('#search-params').text())).toEqual({ - query1: 'value2', - }) + expect(JSON.parse($('#id-page-params').text())).toEqual({ + category: 'category-1', + id: 'id-2', }) + expect(JSON.parse($('#search-params').text())).toEqual({ + query1: 'value2', + }) + }) - if (!isDev) { - it('should successfully detect app route during prefetch', async () => { - const browser = await next.browser('/') - - await check(async () => { - const found = await browser.eval( - '!!window.next.router.components["/dashboard"]' - ) - return found - ? 'success' - : await browser.eval('Object.keys(window.next.router.components)') - }, 'success') + it('should have correct searchParams and params (client)', async () => { + const browser = await next.browser( + '/dynamic-client/category-1/id-2?query1=value2' + ) + const html = await browser.eval('document.documentElement.innerHTML') + const $ = cheerio.load(html) - await browser.elementByCss('a').click() - await browser.waitForElementByCss('#from-dashboard') - }) - } + expect(JSON.parse($('#id-page-params').text())).toEqual({ + category: 'category-1', + id: 'id-2', + }) + expect(JSON.parse($('#search-params').text())).toEqual({ + query1: 'value2', + }) + }) - it('should encode chunk path correctly', async () => { - await next.fetch('/dynamic-client/first/second') + if (!isDev) { + it('should successfully detect app route during prefetch', async () => { const browser = await next.browser('/') - const requests = [] - browser.on('request', (req) => { - requests.push(req.url()) - }) - - await browser.eval( - 'window.location.href = "/dynamic-client/first/second"' - ) await check(async () => { - return requests.some( - (req) => - req.includes( - encodeURI(isTurbopack ? '[category]_[id]' : '/[category]/[id]') - ) && req.endsWith('.js') + const found = await browser.eval( + '!!window.next.router.components["/dashboard"]' ) - ? 'found' - : // When it fails will log out the paths. - JSON.stringify(requests) - }, 'found') + return found + ? 'success' + : await browser.eval('Object.keys(window.next.router.components)') + }, 'success') + + await browser.elementByCss('a').click() + await browser.waitForElementByCss('#from-dashboard') }) + } - it.each([ - { pathname: '/redirect-1' }, - { pathname: '/redirect-2' }, - { pathname: '/blog/old-post' }, - { pathname: '/redirect-3/some' }, - { pathname: '/redirect-4' }, - ])( - 'should match redirects in pages correctly $path', - async ({ pathname }) => { - let browser = await next.browser('/') - - await browser.eval(`next.router.push("${pathname}")`) - await check(async () => { - const href = await browser.eval('location.href') - return href.includes('example.vercel.sh') ? 'yes' : href - }, 'yes') + it('should encode chunk path correctly', async () => { + await next.fetch('/dynamic-client/first/second') + const browser = await next.browser('/') + const requests = [] + browser.on('request', (req) => { + requests.push(req.url()) + }) - if (pathname.includes('/blog')) { - browser = await next.browser('/blog/first') - await browser.eval('window.beforeNav = 1') - - // check 5 times to ensure a reload didn't occur - for (let i = 0; i < 5; i++) { - await waitFor(500) - expect( - await browser.eval('document.documentElement.innerHTML') - ).toContain('hello from pages/blog/[slug]') - expect(await browser.eval('window.beforeNav')).toBe(1) - } + await browser.eval('window.location.href = "/dynamic-client/first/second"') + + await check(async () => { + return requests.some( + (req) => + req.includes( + encodeURI(isTurbopack ? '[category]_[id]' : '/[category]/[id]') + ) && req.endsWith('.js') + ) + ? 'found' + : // When it fails will log out the paths. + JSON.stringify(requests) + }, 'found') + }) + + it.each([ + { pathname: '/redirect-1' }, + { pathname: '/redirect-2' }, + { pathname: '/blog/old-post' }, + { pathname: '/redirect-3/some' }, + { pathname: '/redirect-4' }, + ])( + 'should match redirects in pages correctly $path', + async ({ pathname }) => { + let browser = await next.browser('/') + + await browser.eval(`next.router.push("${pathname}")`) + await check(async () => { + const href = await browser.eval('location.href') + return href.includes('example.vercel.sh') ? 'yes' : href + }, 'yes') + + if (pathname.includes('/blog')) { + browser = await next.browser('/blog/first') + await browser.eval('window.beforeNav = 1') + + // check 5 times to ensure a reload didn't occur + for (let i = 0; i < 5; i++) { + await waitFor(500) + expect( + await browser.eval('document.documentElement.innerHTML') + ).toContain('hello from pages/blog/[slug]') + expect(await browser.eval('window.beforeNav')).toBe(1) } } - ) + } + ) - it('should not apply client router filter on shallow', async () => { - const browser = await next.browser('/') - await browser.eval('window.beforeNav = 1') + it('should not apply client router filter on shallow', async () => { + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') - await check(async () => { - await browser.eval( - `window.next.router.push('/', '/redirect-1', { shallow: true })` - ) - return await browser.eval('window.location.pathname') - }, '/redirect-1') - expect(await browser.eval('window.beforeNav')).toBe(1) + await check(async () => { + await browser.eval( + `window.next.router.push('/', '/redirect-1', { shallow: true })` + ) + return await browser.eval('window.location.pathname') + }, '/redirect-1') + expect(await browser.eval('window.beforeNav')).toBe(1) + }) + + if (isDev) { + it('should not have duplicate config warnings', async () => { + await next.fetch('/') + expect( + stripAnsi(next.cliOutput).match(/Experiments \(use with caution\):/g) + .length + ).toBe(1) }) + } - if (isDev) { - it('should not have duplicate config warnings', async () => { - await next.fetch('/') - expect( - stripAnsi(next.cliOutput).match(/Experiments \(use with caution\):/g) - .length - ).toBe(1) - }) - } + if (!isNextDeploy) { + it('should not share edge workers', async () => { + const controller1 = new AbortController() + const controller2 = new AbortController() + next + .fetch('/slow-page-no-loading', { + signal: controller1.signal, + }) + .catch(() => {}) + next + .fetch('/slow-page-no-loading', { + signal: controller2.signal, + }) + .catch(() => {}) - if (!isNextDeploy) { - it('should not share edge workers', async () => { - const controller1 = new AbortController() - const controller2 = new AbortController() - next - .fetch('/slow-page-no-loading', { - signal: controller1.signal, - }) - .catch(() => {}) - next - .fetch('/slow-page-no-loading', { - signal: controller2.signal, - }) - .catch(() => {}) - - await waitFor(1000) - controller1.abort() - - const controller3 = new AbortController() - next - .fetch('/slow-page-no-loading', { - signal: controller3.signal, - }) - .catch(() => {}) - await waitFor(1000) - controller2.abort() - controller3.abort() - - const res = await next.fetch('/slow-page-no-loading') - expect(res.status).toBe(200) - expect(await res.text()).toContain('hello from slow page') - expect(next.cliOutput).not.toContain( - 'A separate worker must be used for each render' - ) - }) - } + await waitFor(1000) + controller1.abort() - if (isNextStart) { - it('should generate build traces correctly', async () => { - const trace = JSON.parse( - await next.readFile( - '.next/server/app/dashboard/deployments/[id]/page.js.nft.json' - ) - ) as { files: string[] } - expect(trace.files.some((file) => file.endsWith('data.json'))).toBe( - true + const controller3 = new AbortController() + next + .fetch('/slow-page-no-loading', { + signal: controller3.signal, + }) + .catch(() => {}) + await waitFor(1000) + controller2.abort() + controller3.abort() + + const res = await next.fetch('/slow-page-no-loading') + expect(res.status).toBe(200) + expect(await res.text()).toContain('hello from slow page') + expect(next.cliOutput).not.toContain( + 'A separate worker must be used for each render' + ) + }) + } + + if (isNextStart) { + it('should generate build traces correctly', async () => { + const trace = JSON.parse( + await next.readFile( + '.next/server/app/dashboard/deployments/[id]/page.js.nft.json' ) - }) - } + ) as { files: string[] } + expect(trace.files.some((file) => file.endsWith('data.json'))).toBe(true) + }) + } - it('should use text/x-component for flight', async () => { - const res = await next.fetch('/dashboard/deployments/123', { - headers: { - ['RSC'.toString()]: '1', - }, - }) - expect(res.headers.get('Content-Type')).toBe('text/x-component') + it('should use text/x-component for flight', async () => { + const res = await next.fetch('/dashboard/deployments/123', { + headers: { + ['RSC'.toString()]: '1', + }, }) + expect(res.headers.get('Content-Type')).toBe('text/x-component') + }) + + it('should use text/x-component for flight with edge runtime', async () => { + const res = await next.fetch('/dashboard', { + headers: { + ['RSC'.toString()]: '1', + }, + }) + expect(res.headers.get('Content-Type')).toBe('text/x-component') + }) + + it('should return the `vary` header from edge runtime', async () => { + const res = await next.fetch('/dashboard') + expect(res.headers.get('x-edge-runtime')).toBe('1') + expect(res.headers.get('vary')).toBe( + 'RSC, Next-Router-State-Tree, Next-Router-Prefetch' + ) + }) - it('should use text/x-component for flight with edge runtime', async () => { - const res = await next.fetch('/dashboard', { - headers: { - ['RSC'.toString()]: '1', - }, + it('should return the `vary` header from pages for flight requests', async () => { + const res = await next.fetch('/', { + headers: { + ['RSC'.toString()]: '1', + }, + }) + expect(res.headers.get('vary')).toBe( + isNextDeploy + ? 'RSC, Next-Router-State-Tree, Next-Router-Prefetch' + : 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Accept-Encoding' + ) + }) + + it('should pass props from getServerSideProps in root layout', async () => { + const $ = await next.render$('/dashboard') + expect($('title').first().text()).toBe('hello world') + }) + + it('should serve from pages', async () => { + const html = await next.render('/') + expect(html).toContain('hello from pages/index') + }) + + it('should serve dynamic route from pages', async () => { + const html = await next.render('/blog/first') + expect(html).toContain('hello from pages/blog/[slug]') + }) + + it('should serve from public', async () => { + const html = await next.render('/hello.txt') + expect(html).toContain('hello world') + }) + + it('should serve from app', async () => { + const html = await next.render('/dashboard') + expect(html).toContain('hello from app/dashboard') + }) + + it('should ensure the suffix is at the end of the stream', async () => { + const html = await next.render('/dashboard') + + // It must end with the suffix and not contain it anywhere else. + const suffix = '' + expect(html).toEndWith(suffix) + expect(html.slice(0, -suffix.length)).not.toContain(suffix) + }) + + if (!isNextDeploy) { + it('should serve /index as separate page', async () => { + const stderr = [] + next.on('stderr', (err) => { + stderr.push(err) }) - expect(res.headers.get('Content-Type')).toBe('text/x-component') + const html = await next.render('/dashboard/index') + expect(html).toContain('hello from app/dashboard/index') + expect(stderr.some((err) => err.includes('Invalid hook call'))).toBe( + false + ) }) - it('should return the `vary` header from edge runtime', async () => { - const res = await next.fetch('/dashboard') - expect(res.headers.get('x-edge-runtime')).toBe('1') - expect(res.headers.get('vary')).toBe( - 'RSC, Next-Router-State-Tree, Next-Router-Prefetch' + it('should serve polyfills for browsers that do not support modules', async () => { + const html = await next.render('/dashboard/index') + expect(html).toMatch( + /