diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 23dbcc36d818d..e6b172e58b770 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,45 +1,2 @@ # Learn how to add code owners here: # https://help.github.com/en/articles/about-code-owners - - -* @timneutkens @ijjk @shuding @huozhi @feedthejim @ztanner @wyattjoh -/.git* @vercel/next-js @vercel/devex -/docs/ @vercel/next-js @vercel/devex -/errors/ @vercel/next-js @vercel/devex -/examples/ @vercel/next-js @vercel/devex -/scripts/ @vercel/next-js -/.alex* @vercel/next-js @leerob -/.eslint* @vercel/next-js @leerob -/.prettier* @vercel/next-js @leerob -/*.md @vercel/next-js @vercel/devex -/*.mdx @vercel/next-js @vercel/devex -/packages/create-next-app/ @vercel/next-js -/pnpm-lock.yaml @vercel/next-js @vercel/turbopack - -# Next.js CLI - -/packages/next/src/bin @timneutkens @ijjk @shuding @samcx -/packages/next/src/cli @timneutkens @ijjk @shuding @samcx - -# Image Component (@styfle) - -/**/*image* @timneutkens @ijjk @shuding @styfle @huozhi @ztanner @vercel/devex -/**/*image*/** @timneutkens @ijjk @shuding @styfle @huozhi @ztanner @vercel/devex -/**/*img* @timneutkens @ijjk @shuding @styfle @huozhi @ztanner @vercel/devex -/packages/next/client/use-intersection.tsx @timneutkens @ijjk @shuding @styfle -/packages/next/server/lib/squoosh/ @timneutkens @ijjk @shuding @styfle -/packages/next/server/serve-static.ts @timneutkens @ijjk @shuding @styfle @huozhi @ztanner -/packages/next/server/config.ts @timneutkens @ijjk @shuding @styfle @huozhi @ztanner - -# Tooling & Telemetry - -/packages/next/src/build/ @timneutkens @ijjk @shuding @huozhi @ztanner @feedthejim @vercel/turbopack -/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @timneutkens @ijjk @shuding @huozhi @feedthejim @ztanner @wyattjoh @vercel/turbopack -/packages/next/src/telemetry/ @timneutkens @ijjk @shuding @padmaia -/packages/next-swc/ @timneutkens @ijjk @shuding @huozhi @vercel/turbopack -Cargo.toml @timneutkens @ijjk @shuding @huozhi @vercel/turbopack -Cargo.lock @timneutkens @ijjk @shuding @huozhi @vercel/turbopack -/.cargo/config.toml @timneutkens @ijjk @shuding @huozhi @vercel/turbopack -/.config/nextest.toml @timneutkens @ijjk @shuding @huozhi @vercel/turbopack -/test/build-turbopack-dev-tests-manifest.js @timneutkens @ijjk @shuding @huozhi @vercel/turbopack -/test/turbopack-dev-tests-manifest.json @timneutkens @ijjk @shuding @huozhi @vercel/turbopack 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_deploy.yml b/.github/workflows/build_and_deploy.yml index 83c2f9b183235..804486ab74c84 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -29,7 +29,10 @@ jobs: with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true - - run: corepack enable + - name: Setup corepack + run: | + npm i -g corepack@0.31 + corepack enable - uses: actions/checkout@v4 with: @@ -253,7 +256,10 @@ jobs: with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true - - run: corepack enable + - name: Setup corepack + run: | + npm i -g corepack@0.31 + corepack enable - name: Install Rust uses: ./.github/actions/setup-rust @@ -283,7 +289,7 @@ jobs: # issues with turbo caching - name: pull build cache if: ${{ matrix.settings.docker }} - run: node ./scripts/pull-turbo-cache.js ${{ matrix.settings.target }} + run: npm i -g turbo@${{ env.TURBO_VERSION }} && node ./scripts/pull-turbo-cache.js ${{ matrix.settings.target }} - name: check build exists if: ${{ matrix.settings.docker }} @@ -362,7 +368,10 @@ jobs: with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true - - run: corepack enable + - name: Setup corepack + run: | + npm i -g corepack@0.31 + corepack enable - name: Install Rust uses: ./.github/actions/setup-rust @@ -412,8 +421,10 @@ jobs: with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true - - run: corepack enable - + - name: Setup corepack + run: | + npm i -g corepack@0.31 + corepack enable # https://github.com/actions/virtual-environments/issues/1187 - name: tune linux network run: sudo ethtool -K eth0 tx off rx off @@ -460,8 +471,10 @@ jobs: with: node-version: ${{ env.NODE_LTS_VERSION }} check-latest: true - - run: corepack enable - + - name: Setup corepack + run: | + npm i -g corepack@0.31 + corepack enable # https://github.com/actions/virtual-environments/issues/1187 - name: tune linux network run: sudo ethtool -K eth0 tx off rx off diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 3b025d292054a..52dc66e6a7fc9 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'] @@ -260,29 +215,30 @@ jobs: secrets: inherit - test-new-tests-dev: - name: Test new tests for flakes (dev) - needs: ['changes', 'build-native', 'build-next'] - if: ${{ needs.changes.outputs.docs-only == 'false' }} + # disabled for backport branch + # test-new-tests-dev: + # name: Test new tests for flakes (dev) + # needs: ['changes', 'build-native', 'build-next'] + # if: ${{ needs.changes.outputs.docs-only == 'false' }} - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: node scripts/test-new-tests.mjs --dev-mode - stepName: 'test-new-tests-dev' + # uses: ./.github/workflows/build_reusable.yml + # with: + # afterBuild: node scripts/test-new-tests.mjs --dev-mode + # stepName: 'test-new-tests-dev' - secrets: inherit + # secrets: inherit - test-new-tests-start: - name: Test new tests for flakes (prod) - needs: ['changes', 'build-native', 'build-next'] - if: ${{ needs.changes.outputs.docs-only == 'false' }} + # test-new-tests-start: + # name: Test new tests for flakes (prod) + # needs: ['changes', 'build-native', 'build-next'] + # if: ${{ needs.changes.outputs.docs-only == 'false' }} - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: node scripts/test-new-tests.mjs --prod-mode - stepName: 'test-new-tests-start' + # uses: ./.github/workflows/build_reusable.yml + # with: + # afterBuild: node scripts/test-new-tests.mjs --prod-mode + # stepName: 'test-new-tests-start' - secrets: inherit + # secrets: inherit test-dev: name: test dev @@ -353,114 +309,24 @@ 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: - [ + 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', 'test-turbopack-dev', 'test-turbopack-integration', - 'test-new-tests-dev', - 'test-new-tests-start', + # 'test-new-tests-dev', + # 'test-new-tests-start', ] if: always() diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml index 0d48f5403af8a..fc65bcaad6b81 100644 --- a/.github/workflows/build_reusable.yml +++ b/.github/workflows/build_reusable.yml @@ -171,7 +171,7 @@ jobs: - run: turbo run get-test-timings -- --build ${{ github.sha }} - run: /bin/bash -c "${{ inputs.afterBuild }}" - timeout-minutes: 15 + timeout-minutes: 20 - name: Upload artifact uses: actions/upload-artifact@v4 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/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index e83f51406f06d..102145d90a90a 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -71,7 +71,11 @@ jobs: - name: tune linux network run: sudo ethtool -K eth0 tx off rx off - - run: corepack enable && pnpm --version + - name: Setup corepack + run: | + npm i -g corepack@0.31 + corepack enable + pnpm --version - id: get-store-path run: echo STORE_PATH=$(pnpm store path) >> $GITHUB_OUTPUT diff --git a/Cargo.lock b/Cargo.lock index 1273b4834cd13..9699803cfd067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1225,7 +1225,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.10.1", "serde", "smallvec", ] @@ -3688,7 +3688,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ + "phf_macros 0.10.0", "phf_shared 0.10.0", + "proc-macro-hack", ] [[package]] @@ -3697,7 +3699,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_macros", + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -3731,6 +3733,20 @@ dependencies = [ "rand", ] +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "phf_macros" version = "0.11.2" @@ -3925,6 +3941,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.79" @@ -5428,9 +5450,9 @@ dependencies = [ [[package]] name = "swc_core" -version = "0.90.30" +version = "0.90.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7651ba172f4a82cd6f27b73e51d363e9b32aa97b9f6aab2e63e58f4df9ea62" +checksum = "06abbb96671d9fb89f051609242b6acb9ddde9e6ff6325f49fce453d96a4c533" dependencies = [ "binding_macros", "swc", @@ -6086,9 +6108,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_compat" -version = "0.163.18" +version = "0.163.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27a864b81fc36e2933f60015fc6df62e244339acde78e06e4640ec5656584f82" +checksum = "ed183e0eb761a1eddd9ef2232612bcd6790a9fb8b6dd1885b2a9ea0a2f93752c" dependencies = [ "arrayvec", "indexmap 2.2.3", 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/01-images.mdx b/docs/02-app/01-building-your-application/06-optimizing/01-images.mdx index 76f210fbb17b2..698d8c6cce291 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/01-images.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/01-images.mdx @@ -89,6 +89,21 @@ export default function Page() { > **Warning:** Dynamic `await import()` or `require()` are _not_ supported. The `import` must be static so it can be analyzed at build time. +You can optionally configure `localPatterns` in your `next.config.js` file in order to allow specific images and block all others. + +```js filename="next.config.js" +module.exports = { + images: { + localPatterns: [ + { + pathname: '/assets/images/**', + search: '', + }, + ], + }, +} +``` + ### Remote Images To use a remote image, the `src` property should be a URL string. @@ -123,6 +138,7 @@ module.exports = { hostname: 's3.amazonaws.com', port: '', pathname: '/my-bucket/**', + search: '', }, ], }, 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/06-optimizing/13-memory-usage.mdx b/docs/02-app/01-building-your-application/06-optimizing/13-memory-usage.mdx index 70d12064ea678..5d1e609dafd58 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/13-memory-usage.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/13-memory-usage.mdx @@ -11,7 +11,7 @@ Let's explore some strategies and techniques to optimize memory and address comm Applications with a large amount of dependencies will use more memory. -The [Bundle Analyzer](/docs/app/building-your-application/optimizing/bundle-analyzer) can help you investigate large dependencies in your application that may be able to be removed to improve performance and memory usage. +The [Bundle Analyzer](/docs/app/building-your-application/optimizing/package-bundling#analyzing-javascript-bundles) can help you investigate large dependencies in your application that may be able to be removed to improve performance and memory usage. ## Run `next build` with `--experimental-debug-memory-usage` @@ -72,11 +72,10 @@ const nextConfig = { config, { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } ) => { - if (cfg.cache && !dev) { - cfg.cache = Object.freeze({ + if (config.cache && !dev) { + config.cache = Object.freeze({ type: 'memory', }) - cfg.cache.maxMemoryGenerations = 0 } // Important: return the modified config return config 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/01-building-your-application/10-deploying/03-multi-zones.mdx b/docs/02-app/01-building-your-application/10-deploying/03-multi-zones.mdx new file mode 100644 index 0000000000000..17a6f2a476010 --- /dev/null +++ b/docs/02-app/01-building-your-application/10-deploying/03-multi-zones.mdx @@ -0,0 +1,89 @@ +--- +title: Multi-Zones +description: Learn how to build micro-frontends using Next.js Multi-Zones to deploy multiple Next.js apps under a single domain. +--- + +{/* 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. */} + +
+ Examples + +- [With Zones](https://github.com/vercel/next.js/tree/canary/examples/with-zones) + +
+ +Multi-Zones are an approach to micro-frontends that separate a large application on a domain into smaller Next.js applications that each serve a set of paths. This is useful when there are collections of pages unrelated to the other pages in the application. By moving those pages to a separate zone (i.e., a separate application), you can reduce the size of each application which improves build times and removes code that is only necessary for one of the zones. + +For example, let's say you have the following set of pages that you would like to split up: + +- `/blog/*` for all blog posts +- `/dashboard/*` for all pages when the user is logged-in to the dashboard +- `/*` for the rest of your website not covered by other zones + +With Multi-Zones support, you can create three applications that all are served on the same domain and look the same to the user, but you can develop and deploy each of the applications independently. + +Three zones: A, B, C. Showing a hard navigation between routes from different zones, and soft navigations between routes within the same zone. + +Navigating between pages in the same zone will perform soft navigations, a navigation that does not require reloading the page. For example, in this diagram, navigating from `/` to `/products` will be a soft navigation. + +Navigating from a page in one zone to a page in another zone, such as from `/` to `/dashboard`, will perform a hard navigation, unloading the resources of the current page and loading the resources of the new page. Pages that are frequently visited together should live in the same zone to avoid hard navigations. + +## How to define a zone + +There are no special APIs to define a new zone. A zone is a normal Next.js application where you also configure a [basePath](/docs/app/api-reference/next-config-js/basePath) to avoid conflicts with pages and static files in other zones. + +```js filename="next.config.js" +/** @type {import('next').NextConfig} */ +const nextConfig = { + basePath: '/blog', +} +``` + +The default application that will handle all paths not sent to a more specific zone does not need a `basePath`. + +Next.js assets, such as JavaScript and CSS, will also be prefixed with `basePath` to make sure that they don't conflict with assets from other zones. These assets will be served under `/basePath/_next/...` for each of the zones. + +If the zone serves pages that don't share a common path prefix, such as `/home` and `/blog`, then you can also set [`assetPrefix`](/docs/app/api-reference/next-config-js/assetPrefix) to ensure that all Next.js assets are served under a unique path prefix for the zone without adding a path prefix to the rest of the routes in your application. + +## How to route requests to the right zone + +With the Multi Zones set-up, you need to route the paths to the correct zone since they are served by different applications. You can use any HTTP proxy to do this, but one of the Next.js applications can also be used to route requests for the entire domain. + +To route to the correct zone using a Next.js application, you can use [`rewrites`](/docs/app/api-reference/next-config-js/rewrites). For each path served by a different zone, you would add a rewrite rule to send that path to the domain of the other zone. For example: + +```js filename="next.config.js" +async rewrites() { + return [ + { + source: '/blog', + destination: `${process.env.BLOG_DOMAIN}/blog`, + }, + { + source: '/blog/:path+', + destination: `${process.env.BLOG_DOMAIN}/blog/:path+`, + } + ]; +} +``` + +`destination` should be a URL that is served by the zone, including scheme and domain. This should point to the zone's production domain, but it can also be used to route requests to `localhost` in local development. + +> **Good to know**: URL paths should be unique to a zone. For example, two zones trying to serve `/blog` would create a routing conflict. + +## Linking between zones + +Links to paths in a different zone should use an `a` tag instead of the Next.js [``](/docs/pages/api-reference/components/link) component. This is because Next.js will try to prefetch and soft navigate to any relative path in `` component, which will not work across zones. + +## Sharing code + +The Next.js applications that make up the different zones can live in any repository. However, it is often convenient to put these zones in a [monorepo](https://en.wikipedia.org/wiki/Monorepo) to more easily share code. For zones that live in different repositories, code can also be shared using public or private NPM packages. + +Since the pages in different zones may be released at different times, feature flags can be useful for enabling or disabling features in unison across the different zones. + +For [Next.js on Vercel](https://vercel.com?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) applications, you can use a [monorepo](https://vercel.com/blog/monorepos-are-changing-how-teams-build-software?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) to deploy all affected zones with a single `git push`. diff --git a/docs/02-app/02-api-reference/01-components/image.mdx b/docs/02-app/02-api-reference/01-components/image.mdx index 74a11d6fc106c..fa90c59936a2a 100644 --- a/docs/02-app/02-api-reference/01-components/image.mdx +++ b/docs/02-app/02-api-reference/01-components/image.mdx @@ -247,6 +247,10 @@ quality={75} // {number 1-100} The quality of the optimized image, an integer between `1` and `100`, where `100` is the best quality and therefore largest file size. Defaults to `75`. +If the [`qualities`](#qualities) configuration is defined in `next.config.js`, the `quality` prop must match one of the values defined in the configuration. + +> **Good to know**: If the original source image was already low quality, setting the quality prop too high could cause the resulting optimized image to be larger than the original source image. + ### `priority` ```js @@ -461,18 +465,48 @@ For example, when upgrading an existing website from `` to ``, you m /> ``` +### decoding + +A hint to the browser indicating if it should wait for the image to be decoded before presenting other content updates or not. Defaults to `async`. + +Possible values are the following: + +- `async` - Asynchronously decode the image and allow other content to be rendered before it completes. +- `sync` - Synchronously decode the image for atomic presentation with other content. +- `auto` - No preference for the decoding mode; the browser decides what's best. + +Learn more about the [`decoding` attribute](https://developer.mozilla.org/docs/Web/HTML/Element/img#loading). + ### Other Props Other properties on the `` component will be passed to the underlying `img` element with the exception of the following: - `srcSet`. Use [Device Sizes](#devicesizes) instead. -- `decoding`. It is always `"async"`. ## Configuration Options In addition to props, you can configure the Image Component in `next.config.js`. The following options are available: +### `localPatterns` + +You can optionally configure `localPatterns` in your `next.config.js` file in order to allow specific paths to be optimized and block all others paths. + +```js filename="next.config.js" +module.exports = { + images: { + localPatterns: [ + { + pathname: '/assets/images/**', + search: '', + }, + ], + }, +} +``` + +> **Good to know**: The example above will ensure the `src` property of `next/image` must start with `/assets/images/` and must not have a query string. Attempting to optimize any other path will respond with 400 Bad Request. + ### `remotePatterns` To protect your application from malicious users, configuration is required in order to use external images. This ensures that only external images from your account can be served from the Next.js Image Optimization API. These external images can be configured with the `remotePatterns` property in your `next.config.js` file, as shown below: @@ -486,15 +520,16 @@ module.exports = { hostname: 'example.com', port: '', pathname: '/account123/**', + search: '', }, ], }, } ``` -> **Good to know**: The example above will ensure the `src` property of `next/image` must start with `https://example.com/account123/`. Any other protocol, hostname, port, or unmatched path will respond with 400 Bad Request. +> **Good to know**: The example above will ensure the `src` property of `next/image` must start with `https://example.com/account123/` and must not have a query string. Any other protocol, hostname, port, or unmatched path will respond with 400 Bad Request. -Below is another example of the `remotePatterns` property in the `next.config.js` file: +Below is an example of the `remotePatterns` property in the `next.config.js` file using a wildcard pattern in the `hostname`: ```js filename="next.config.js" module.exports = { @@ -504,13 +539,14 @@ module.exports = { protocol: 'https', hostname: '**.example.com', port: '', + search: '', }, ], }, } ``` -> **Good to know**: The example above will ensure the `src` property of `next/image` must start with `https://img1.example.com` or `https://me.avatar.example.com` or any number of subdomains. Any other protocol, port, or unmatched hostname will respond with 400 Bad Request. +> **Good to know**: The example above will ensure the `src` property of `next/image` must start with `https://img1.example.com` or `https://me.avatar.example.com` or any number of subdomains. It cannot have a port or query string. Any other protocol or unmatched hostname will respond with 400 Bad Request. Wildcard patterns can be used for both `pathname` and `hostname` and have the following syntax: @@ -519,7 +555,25 @@ Wildcard patterns can be used for both `pathname` and `hostname` and have the fo The `**` syntax does not work in the middle of the pattern. -> **Good to know**: When omitting `protocol`, `port` or `pathname`, then the wildcard `**` is implied. This is not recommended because it may allow malicious actors to optimize urls you did not intend. +> **Good to know**: When omitting `protocol`, `port`, `pathname`, or `search` then the wildcard `**` is implied. This is not recommended because it may allow malicious actors to optimize urls you did not intend. + +Below is an example of the `remotePatterns` property in the `next.config.js` file using `search`: + +```js filename="next.config.js" +module.exports = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'assets.example.com', + search: '?v=1727111025337', + }, + ], + }, +} +``` + +> **Good to know**: The example above will ensure the `src` property of `next/image` must start with `https://assets.example.com` and must have the exact query string `?v=1727111025337`. Any other protocol or query string will respond with 400 Bad Request. ### `domains` @@ -622,6 +676,20 @@ module.exports = { } ``` +### `qualities` + +The default [Image Optimization API](#loader) will automatically allow all qualities from 1 to 100. If you wish to restrict the allowed qualities, you can add configuration to `next.config.js`. + +```js filename="next.config.js" +module.exports = { + images: { + qualities: [25, 50, 75], + }, +} +``` + +In this example above, only three qualities are allowed: 25, 50, and 75. If the [`quality`](#quality) prop does not match a value in this array, the image will fail with 400 Bad Request. + ### `formats` The default [Image Optimization API](#loader) will automatically detect the browser's supported image formats via the request's `Accept` header. @@ -1000,6 +1068,9 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c | Version | Changes | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `v14.2.23` | `qualities` configuration added. | +| `v14.2.15` | `decoding` prop added and `localPatterns` configuration added. | +| `v14.2.14` | `remotePatterns.search` prop added. | | `v14.2.0` | `overrideSrc` prop added. | | `v14.1.0` | `getImageProps()` is stable. | | `v14.0.0` | `onLoadingComplete` prop and `domains` config deprecated. | diff --git a/docs/02-app/02-api-reference/04-functions/fetch.mdx b/docs/02-app/02-api-reference/04-functions/fetch.mdx index 281079c1685ba..8e4a088785fef 100644 --- a/docs/02-app/02-api-reference/04-functions/fetch.mdx +++ b/docs/02-app/02-api-reference/04-functions/fetch.mdx @@ -98,7 +98,7 @@ Set the cache lifetime of a resource (in seconds). fetch(`https://...`, { next: { tags: ['collection'] } }) ``` -Set the cache tags of a resource. Data can then be revalidated on-demand using [`revalidateTag`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag). The max length for a custom tag is 256 characters and the max tag items is 64. +Set the cache tags of a resource. Data can then be revalidated on-demand using [`revalidateTag`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag). The max length for a custom tag is 256 characters and the max tag items is 128. ## Version History 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/serverComponentsExternalPackages.mdx b/docs/02-app/02-api-reference/05-next-config-js/serverComponentsExternalPackages.mdx index da52a4dbe5542..c4953d16953ac 100644 --- a/docs/02-app/02-api-reference/05-next-config-js/serverComponentsExternalPackages.mdx +++ b/docs/02-app/02-api-reference/05-next-config-js/serverComponentsExternalPackages.mdx @@ -68,7 +68,6 @@ Next.js includes a [short list of popular packages](https://github.com/vercel/ne - `ts-node` - `typescript` - `vscode-oniguruma` -- `undici` - `webpack` - `websocket` - `zeromq` 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/07-edge.mdx b/docs/02-app/02-api-reference/07-edge.mdx index 3bed7bfb2bbb0..4a25ad53f2630 100644 --- a/docs/02-app/02-api-reference/07-edge.mdx +++ b/docs/02-app/02-api-reference/07-edge.mdx @@ -155,7 +155,7 @@ export const config = { // allows a single file '/lib/utilities.js', // use a glob to allow anything in the function-bind 3rd party module - '/node_modules/function-bind/**', + '**/node_modules/function-bind/**', ], } ``` 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/01-building-your-application/09-deploying/03-multi-zones.mdx b/docs/03-pages/01-building-your-application/09-deploying/03-multi-zones.mdx index 568a7d0c7f4e8..d7463ceab1616 100644 --- a/docs/03-pages/01-building-your-application/09-deploying/03-multi-zones.mdx +++ b/docs/03-pages/01-building-your-application/09-deploying/03-multi-zones.mdx @@ -1,33 +1,7 @@ --- -title: Multi Zones -description: Learn how to use multi zones to deploy multiple Next.js apps as a single app. +title: Multi-Zones +description: Learn how to build micro-frontends using Next.js Multi-Zones to deploy multiple Next.js apps under a single domain. +source: app/building-your-application/deploying/multi-zones --- -
- Examples - -- [With Zones](https://github.com/vercel/next.js/tree/canary/examples/with-zones) - -
- -A zone is a single deployment of a Next.js app. You can have multiple zones and merge them as a single app. - -For example, let's say you have the following apps: - -- An app for serving `/blog/**` -- Another app for serving all other pages - -With multi zones support, you can merge both these apps into a single one allowing your customers to browse it using a single URL, but you can develop and deploy both apps independently. - -## How to define a zone - -There are no zone related APIs. You only need to do the following: - -- Make sure to keep only the pages you need in your app, meaning that an app can't have pages from another app, if app `A` has `/blog` then app `B` shouldn't have it too. -- Make sure to configure a [basePath](/docs/app/api-reference/next-config-js/basePath) to avoid conflicts with pages and static files. - -## How to merge zones - -You can merge zones using [`rewrites`](/docs/pages/api-reference/next-config-js/rewrites) in one of the apps or any HTTP proxy. - -For [Next.js on Vercel](https://vercel.com?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) applications, you can use a [monorepo](https://vercel.com/blog/monorepos-are-changing-how-teams-build-software?utm_source=next-site&utm_medium=docs&utm_campaign=next-website) to deploy both apps with a single `git push`. +{/* 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/01-components/image-legacy.mdx b/docs/03-pages/02-api-reference/01-components/image-legacy.mdx index b2b81a7d10c09..c4feae199b039 100644 --- a/docs/03-pages/02-api-reference/01-components/image-legacy.mdx +++ b/docs/03-pages/02-api-reference/01-components/image-legacy.mdx @@ -362,15 +362,16 @@ module.exports = { hostname: 'example.com', port: '', pathname: '/account123/**', + search: '', }, ], }, } ``` -> **Good to know**: The example above will ensure the `src` property of `next/legacy/image` must start with `https://example.com/account123/`. Any other protocol, hostname, port, or unmatched path will respond with 400 Bad Request. +> **Good to know**: The example above will ensure the `src` property of `next/legacy/image` must start with `https://example.com/account123/` and must not have a query string. Any other protocol, hostname, port, or unmatched path will respond with 400 Bad Request. -Below is another example of the `remotePatterns` property in the `next.config.js` file: +Below is an example of the `remotePatterns` property in the `next.config.js` file using a wildcard pattern in the `hostname`: ```js filename="next.config.js" module.exports = { @@ -380,13 +381,14 @@ module.exports = { protocol: 'https', hostname: '**.example.com', port: '', + search: '', }, ], }, } ``` -> **Good to know**: The example above will ensure the `src` property of `next/legacy/image` must start with `https://img1.example.com` or `https://me.avatar.example.com` or any number of subdomains. Any other protocol, port, or unmatched hostname will respond with 400 Bad Request. +> **Good to know**: The example above will ensure the `src` property of `next/legacy/image` must start with `https://img1.example.com` or `https://me.avatar.example.com` or any number of subdomains. It cannot have a port or query string. Any other protocol or unmatched hostname will respond with 400 Bad Request. Wildcard patterns can be used for both `pathname` and `hostname` and have the following syntax: @@ -395,7 +397,25 @@ Wildcard patterns can be used for both `pathname` and `hostname` and have the fo The `**` syntax does not work in the middle of the pattern. -> **Good to know**: When omitting `protocol`, `port` or `pathname`, then the wildcard `**` is implied. This is not recommended because it may allow malicious actors to optimize urls you did not intend. +> **Good to know**: When omitting `protocol`, `port`, `pathname`, or `search` then the wildcard `**` is implied. This is not recommended because it may allow malicious actors to optimize urls you did not intend. + +Below is an example of the `remotePatterns` property in the `next.config.js` file using `search`: + +```js filename="next.config.js" +module.exports = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'assets.example.com', + search: '?v=1727111025337', + }, + ], + }, +} +``` + +> **Good to know**: The example above will ensure the `src` property of `next/legacy/image` must start with `https://assets.example.com` and must have the exact query string `?v=1727111025337`. Any other protocol or query string will respond with 400 Bad Request. ### Domains 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/edge-dynamic-code-evaluation.mdx b/errors/edge-dynamic-code-evaluation.mdx index bb842f4181ddd..fcf7bcdc79b73 100644 --- a/errors/edge-dynamic-code-evaluation.mdx +++ b/errors/edge-dynamic-code-evaluation.mdx @@ -38,7 +38,7 @@ export const config = { runtime: 'edge', // for Edge API Routes only unstable_allowDynamic: [ '/lib/utilities.js', // allows a single file - '/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module + '**/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module ], } ``` 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-images-config.mdx b/errors/invalid-images-config.mdx index 4f14b668eb3a0..2fe213dc34690 100644 --- a/errors/invalid-images-config.mdx +++ b/errors/invalid-images-config.mdx @@ -37,8 +37,12 @@ module.exports = { contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", // sets the Content-Disposition header (inline or attachment) contentDispositionType: 'inline', + // limit of 25 objects + localPatterns: [], // limit of 50 objects remotePatterns: [], + // limit of 20 integers + qualities: [25, 50, 75], // when true, every image will be unoptimized unoptimized: false, }, 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/errors/next-image-unconfigured-host.mdx b/errors/next-image-unconfigured-host.mdx index ea0496b71fbbd..d6622a608d5f6 100644 --- a/errors/next-image-unconfigured-host.mdx +++ b/errors/next-image-unconfigured-host.mdx @@ -19,6 +19,7 @@ module.exports = { hostname: 'assets.example.com', port: '', pathname: '/account123/**', + search: '', }, ], }, diff --git a/errors/next-image-unconfigured-localpatterns.mdx b/errors/next-image-unconfigured-localpatterns.mdx new file mode 100644 index 0000000000000..34a9a19d5e34f --- /dev/null +++ b/errors/next-image-unconfigured-localpatterns.mdx @@ -0,0 +1,29 @@ +--- +title: '`next/image` Un-configured localPatterns' +--- + +## Why This Error Occurred + +One of your pages that leverages the `next/image` component, passed a `src` value that uses a URL that isn't defined in the `images.localPatterns` property in `next.config.js`. + +## Possible Ways to Fix It + +Add an entry to `images.localPatterns` array in `next.config.js` with the expected URL pattern. For example: + +```js filename="next.config.js" +module.exports = { + images: { + localPatterns: [ + { + pathname: '/assets/**', + search: '', + }, + ], + }, +} +``` + +## Useful Links + +- [Image Optimization Documentation](/docs/pages/building-your-application/optimizing/images) +- [Local Patterns Documentation](/docs/pages/api-reference/components/image#localpatterns) diff --git a/errors/next-image-unconfigured-qualities.mdx b/errors/next-image-unconfigured-qualities.mdx new file mode 100644 index 0000000000000..bc19847668d85 --- /dev/null +++ b/errors/next-image-unconfigured-qualities.mdx @@ -0,0 +1,24 @@ +--- +title: '`next/image` Un-configured qualities' +--- + +## Why This Error Occurred + +One of your pages that leverages the `next/image` component, passed a `quality` value that isn't defined in the `images.qualities` property in `next.config.js`. + +## Possible Ways to Fix It + +Add an entry to `images.qualities` array in `next.config.js` with the expected value. For example: + +```js filename="next.config.js" +module.exports = { + images: { + qualities: [25, 50, 75], + }, +} +``` + +## Useful Links + +- [Image Optimization Documentation](/docs/pages/building-your-application/optimizing/images) +- [Qualities Config Documentation](/docs/pages/api-reference/components/image#qualities) 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-storyblok/README.md b/examples/cms-storyblok/README.md index 00ca1e90f1750..0a67e8509a09a 100644 --- a/examples/cms-storyblok/README.md +++ b/examples/cms-storyblok/README.md @@ -149,12 +149,14 @@ module.exports = { hostname: 'a.storyblok.com', port: '', pathname: '**', + search: '', }, { protocol: 'https', hostname: 'images.unsplash.com', port: '', pathname: '**', + search: '', }, ], }, 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 1fd840fe4272e..17d4a7fc03bfd 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "14.2.4" + "version": "14.2.30" } diff --git a/package.json b/package.json index 2cfe2af78f688..b2fbaf678315a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/preset-flow": "7.22.5", "@babel/preset-react": "7.22.5", - "@edge-runtime/jest-environment": "2.3.10", + "@edge-runtime/jest-environment": "3.0.0", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", "@fullhuman/postcss-purgecss": "1.3.0", @@ -115,7 +115,7 @@ "@typescript-eslint/eslint-plugin": "6.14.0", "@typescript-eslint/parser": "6.14.0", "@vercel/fetch": "6.1.1", - "@vercel/og": "0.6.2", + "@vercel/og": "0.6.3", "abort-controller": "3.0.0", "alex": "9.1.0", "amphtml-validator": "1.0.35", @@ -198,16 +198,16 @@ "random-seed": "0.3.0", "react": "18.2.0", "react-17": "npm:react@17.0.2", - "react-builtin": "npm:react@18.3.0-canary-14898b6a9-20240318", + "react-builtin": "npm:react@18.3.0-canary-178c267a4e-20241218", "react-dom": "18.2.0", "react-dom-17": "npm:react-dom@17.0.2", - "react-dom-builtin": "npm:react-dom@18.3.0-canary-14898b6a9-20240318", - "react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-14898b6a9-20240318", - "react-experimental-builtin": "npm:react@0.0.0-experimental-14898b6a9-20240318", - "react-server-dom-turbopack": "18.3.0-canary-14898b6a9-20240318", - "react-server-dom-turbopack-experimental": "npm:react-server-dom-turbopack@0.0.0-experimental-14898b6a9-20240318", - "react-server-dom-webpack": "18.3.0-canary-14898b6a9-20240318", - "react-server-dom-webpack-experimental": "npm:react-server-dom-webpack@0.0.0-experimental-14898b6a9-20240318", + "react-dom-builtin": "npm:react-dom@18.3.0-canary-178c267a4e-20241218", + "react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-178c267a4e-20241218", + "react-experimental-builtin": "npm:react@0.0.0-experimental-178c267a4e-20241218", + "react-server-dom-turbopack": "18.3.0-canary-178c267a4e-20241218", + "react-server-dom-turbopack-experimental": "npm:react-server-dom-turbopack@0.0.0-experimental-178c267a4e-20241218", + "react-server-dom-webpack": "18.3.0-canary-178c267a4e-20241218", + "react-server-dom-webpack-experimental": "npm:react-server-dom-webpack@0.0.0-experimental-178c267a4e-20241218", "react-ssr-prepass": "1.0.8", "react-virtualized": "9.22.3", "relay-compiler": "13.0.2", @@ -217,8 +217,8 @@ "resolve-from": "5.0.0", "sass": "1.54.0", "satori": "0.10.9", - "scheduler-builtin": "npm:scheduler@0.24.0-canary-14898b6a9-20240318", - "scheduler-experimental-builtin": "npm:scheduler@0.0.0-experimental-14898b6a9-20240318", + "scheduler-builtin": "npm:scheduler@0.24.0-canary-178c267a4e-20241218", + "scheduler-experimental-builtin": "npm:scheduler@0.0.0-experimental-178c267a4e-20241218", "seedrandom": "3.0.5", "selenium-webdriver": "4.0.0-beta.4", "semver": "7.3.7", diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 8421f3392fe4e..3901c523c04d2 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.4", + "version": "14.2.30", "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 7ae889eefc153..4c17410a958c3 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.4", + "version": "14.2.30", "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.4", + "@next/eslint-plugin-next": "14.2.30", "@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 04645aa595134..8162b106109e2 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.4", + "version": "14.2.30", "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 4e22e125c30a8..9c52181cb474a 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "14.2.4", + "version": "14.2.30", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/font/src/google/font-data.json b/packages/font/src/google/font-data.json index 30e3508ce0657..ee9d9a4d1bd43 100644 --- a/packages/font/src/google/font-data.json +++ b/packages/font/src/google/font-data.json @@ -1965,7 +1965,7 @@ "defaultValue": 400 } ], - "subsets": ["cyrillic-ext", "latin", "latin-ext", "vietnamese"] + "subsets": ["latin", "latin-ext", "vietnamese"] }, "Bruno Ace": { "weights": ["400"], @@ -5063,13 +5063,13 @@ "900", "variable" ], - "styles": ["normal"], + "styles": ["normal", "italic"], "axes": [ { - "tag": "slnt", - "min": -10, - "max": 0, - "defaultValue": 0 + "tag": "opsz", + "min": 14, + "max": 32, + "defaultValue": 14 }, { "tag": "wght", @@ -10685,7 +10685,7 @@ "Philosopher": { "weights": ["400", "700"], "styles": ["normal", "italic"], - "subsets": ["cyrillic", "cyrillic-ext", "latin", "vietnamese"] + "subsets": ["cyrillic", "cyrillic-ext", "latin", "latin-ext", "vietnamese"] }, "Phudu": { "weights": ["300", "400", "500", "600", "700", "800", "900", "variable"], @@ -12235,6 +12235,11 @@ "styles": ["normal"], "subsets": ["latin", "latin-ext"] }, + "Sankofa Display": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["latin", "latin-ext", "vietnamese"] + }, "Sansita": { "weights": ["400", "700", "800", "900"], "styles": ["normal", "italic"], @@ -14400,6 +14405,11 @@ "styles": ["normal"], "subsets": ["latin"] }, + "Zain": { + "weights": ["200", "300", "400", "700", "800", "900"], + "styles": ["normal"], + "subsets": ["arabic", "latin"] + }, "Zen Antique": { "weights": ["400"], "styles": ["normal"], diff --git a/packages/font/src/google/get-font-axes.test.ts b/packages/font/src/google/get-font-axes.test.ts index 2991c11772e8a..2ac2507f4d240 100644 --- a/packages/font/src/google/get-font-axes.test.ts +++ b/packages/font/src/google/get-font-axes.test.ts @@ -13,7 +13,7 @@ describe('getFontAxes errors', () => { expect(() => getFontAxes('Inter', ['variable'], [], true as any)) .toThrowErrorMatchingInlineSnapshot(` "Invalid axes value for font \`Inter\`, expected an array of axes. - Available axes: \`slnt\`" + Available axes: \`opsz\`" `) }) diff --git a/packages/font/src/google/index.ts b/packages/font/src/google/index.ts index e710b6c4d2e4b..2f77f7883d26b 100644 --- a/packages/font/src/google/index.ts +++ b/packages/font/src/google/index.ts @@ -3351,7 +3351,7 @@ export declare function Bricolage_Grotesque< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'cyrillic-ext' | 'latin' | 'latin-ext' | 'vietnamese'> + subsets?: Array<'latin' | 'latin-ext' | 'vietnamese'> axes?: ('opsz' | 'wdth')[] }): T extends undefined ? NextFont : NextFontWithVariable export declare function Bruno_Ace< @@ -9051,7 +9051,7 @@ export declare function Inter< | Array< '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' > - style?: 'normal' | Array<'normal'> + style?: 'normal' | 'italic' | Array<'normal' | 'italic'> display?: Display variable?: T preload?: boolean @@ -9066,7 +9066,7 @@ export declare function Inter< | 'latin-ext' | 'vietnamese' > - axes?: 'slnt'[] + axes?: 'opsz'[] }): T extends undefined ? NextFont : NextFontWithVariable export declare function Inter_Tight< T extends CssVariable | undefined = undefined @@ -18222,7 +18222,9 @@ export declare function Philosopher< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'cyrillic' | 'cyrillic-ext' | 'latin' | 'vietnamese'> + subsets?: Array< + 'cyrillic' | 'cyrillic-ext' | 'latin' | 'latin-ext' | 'vietnamese' + > }): T extends undefined ? NextFont : NextFontWithVariable export declare function Phudu< T extends CssVariable | undefined = undefined @@ -20823,6 +20825,18 @@ export declare function Sancreek< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Sankofa_Display< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext' | 'vietnamese'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Sansita< T extends CssVariable | undefined = undefined >(options: { @@ -24687,6 +24701,25 @@ export declare function ZCOOL_XiaoWei< adjustFontFallback?: boolean subsets?: Array<'latin'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Zain< + T extends CssVariable | undefined = undefined +>(options: { + weight: + | '200' + | '300' + | '400' + | '700' + | '800' + | '900' + | Array<'200' | '300' | '400' | '700' | '800' | '900'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'arabic' | 'latin'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Zen_Antique< T extends CssVariable | undefined = undefined >(options: { diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 933e1cf8d8437..9e0366fddc165 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.4", + "version": "14.2.30", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 47c0ea377aed3..d628b286f46f3 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "14.2.4", + "version": "14.2.30", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 67e99ed16adde..31aceaab90847 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "14.2.4", + "version": "14.2.30", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 0bafcaf2fc3d6..ecc8d15c01f5d 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "14.2.4", + "version": "14.2.30", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 86538f06712ed..2174a76a3ecc7 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.4", + "version": "14.2.30", "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 d0dfdbdde5583..d66ce3bf846c1 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.4", + "version": "14.2.30", "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-module/src/index.js b/packages/next-polyfill-module/src/index.js index 803e2c51ee312..f5cd21abf4721 100644 --- a/packages/next-polyfill-module/src/index.js +++ b/packages/next-polyfill-module/src/index.js @@ -161,3 +161,23 @@ if (!Object.hasOwn) { return Object.prototype.hasOwnProperty.call(Object(object), property) } } + +/** + * Available in: + * Edge: 120 + * Firefox: 115 + * Chrome: 120 + * Safari: 17.0 + * + * https://caniuse.com/mdn-api_url_canparse_static + */ +// Modified from https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/web.url.can-parse.js +if (!('canParse' in URL)) { + URL.canParse = function (url, base) { + try { + return !!new URL(url, base) + } catch { + return false + } + } +} diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index d78b6e870adee..87de5f5dc50af 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.4", + "version": "14.2.30", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", @@ -14,7 +14,7 @@ "prepublishOnly": "cd ../../ && turbo run build" }, "devDependencies": { - "core-js": "3.6.5", + "core-js": "3.38.1", "microbundle": "0.15.0", "object-assign": "4.1.1", "whatwg-fetch": "3.0.0" diff --git a/packages/next-polyfill-nomodule/src/index.js b/packages/next-polyfill-nomodule/src/index.js index 677deeba184e4..f7fa20a032b53 100644 --- a/packages/next-polyfill-nomodule/src/index.js +++ b/packages/next-polyfill-nomodule/src/index.js @@ -49,6 +49,7 @@ import 'core-js/features/string/trim-left' import 'core-js/features/string/trim-right' import 'core-js/features/url' import 'core-js/features/url/to-json' +import 'core-js/features/url/can-parse' import 'core-js/features/url-search-params' import 'core-js/features/weak-map' import 'core-js/features/weak-set' 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_client_reference/ecmascript_client_reference/ecmascript_client_reference_proxy_module.rs b/packages/next-swc/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_proxy_module.rs index 2d46831dd95ad..a45cedca0d869 100644 --- a/packages/next-swc/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_proxy_module.rs +++ b/packages/next-swc/crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_proxy_module.rs @@ -67,11 +67,13 @@ impl EcmascriptClientReferenceProxyModule { #[turbo_tasks::function] async fn proxy_module(&self) -> Result> { let mut code = CodeBuilder::default(); + let is_esm: bool; let server_module_path = &*self.server_module_ident.path().to_string().await?; // Adapted from https://github.com/facebook/react/blob/c5b9375767e2c4102d7e5559d383523736f1c902/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js#L323-L354 if let EcmascriptExports::EsmExports(exports) = &*self.client_module.get_exports().await? { + is_esm = true; let exports = exports.expand_exports().await?; if !exports.dynamic_exports.is_empty() { @@ -127,6 +129,7 @@ impl EcmascriptClientReferenceProxyModule { } } } else { + is_esm = false; writedoc!( code, r#" @@ -143,7 +146,13 @@ impl EcmascriptClientReferenceProxyModule { AssetContent::file(File::from(code.source_code().clone()).into()); let proxy_source = VirtualSource::new( - self.server_module_ident.path().join("proxy.js".to_string()), + self.server_module_ident.path().join( + // Depending on the original format, we call the file `proxy.mjs` or `proxy.cjs`. + // This is because we're placing the virtual module next to the original code, so + // its parsing will be affected by `type` fields in package.json -- + // a bare `proxy.js` may end up being unexpectedly parsed as the wrong format. + format!("proxy.{}", if is_esm { "mjs" } else { "cjs" }), + ), proxy_module_content, ); 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 c684125831a79..493abd5a3821a 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "14.2.4", + "version": "14.2.30", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 0fe915c21278c..96c5ecdae289c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "14.2.4", + "version": "14.2.30", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "14.2.4", + "@next/env": "14.2.30", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -141,18 +141,18 @@ "@babel/traverse": "7.22.5", "@babel/types": "7.22.5", "@capsizecss/metrics": "2.2.0", - "@edge-runtime/cookies": "4.1.1", - "@edge-runtime/ponyfill": "2.4.2", - "@edge-runtime/primitives": "4.1.0", + "@edge-runtime/cookies": "5.0.0", + "@edge-runtime/ponyfill": "3.0.0", + "@edge-runtime/primitives": "5.0.0", "@hapi/accept": "5.0.2", "@jest/transform": "29.5.0", "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/polyfill-module": "14.2.4", - "@next/polyfill-nomodule": "14.2.4", - "@next/react-refresh-utils": "14.2.4", - "@next/swc": "14.2.4", + "@next/polyfill-module": "14.2.30", + "@next/polyfill-nomodule": "14.2.30", + "@next/react-refresh-utils": "14.2.30", + "@next/swc": "14.2.30", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@taskr/clear": "1.1.0", @@ -197,7 +197,7 @@ "@vercel/ncc": "0.34.0", "@vercel/nft": "0.26.4", "@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-240417.2", - "acorn": "8.5.0", + "acorn": "8.11.3", "amphtml-validator": "1.0.35", "anser": "1.4.9", "arg": "4.1.0", @@ -229,7 +229,7 @@ "debug": "4.1.1", "devalue": "2.0.1", "domain-browser": "4.19.0", - "edge-runtime": "2.5.4", + "edge-runtime": "3.0.0", "events": "3.3.0", "find-up": "4.1.0", "fresh": "0.5.2", diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 4420c266144c3..590fcd855ac1f 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -409,7 +409,7 @@ function getMiddlewareConfig( : [config.unstable_allowDynamic] for (const glob of result.unstable_allowDynamicGlobs ?? []) { try { - picomatch(glob) + picomatch(glob, { dot: true }) } catch (err) { throw new Error( `${pageFilePath} exported 'config.unstable_allowDynamic' contains invalid pattern '${glob}': ${ 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..3bbd8b4c7a1fc 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -185,6 +185,8 @@ 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' +import { getNodeOptionsWithoutInspect } from '../server/lib/utils' interface ExperimentalBypassForInfo { experimentalBypassFor?: RouteHas[] @@ -337,37 +339,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, @@ -435,14 +413,26 @@ async function writeImagesManifest( const images = { ...config.images } const { deviceSizes, imageSizes } = images ;(images as any).sizes = [...deviceSizes, ...imageSizes] + + // By default, remotePatterns will allow no remote images ([]) images.remotePatterns = (config?.images?.remotePatterns || []).map((p) => ({ - // Should be the same as matchRemotePattern() + // Modifying the manifest should also modify matchRemotePattern() protocol: p.protocol, hostname: makeRe(p.hostname).source, port: p.port, pathname: makeRe(p.pathname ?? '**', { dot: true }).source, + search: p.search, })) + // By default, localPatterns will allow all local images (undefined) + if (config?.images?.localPatterns) { + images.localPatterns = config.images.localPatterns.map((p) => ({ + // Modifying the manifest should also modify matchLocalPattern() + pathname: makeRe(p.pathname ?? '**', { dot: true }).source, + search: p.search, + })) + } + await writeManifest(path.join(distDir, IMAGES_MANIFEST), { version: 1, images, @@ -575,7 +565,6 @@ function createStaticWorker( ): StaticWorker { let infoPrinted = false const timeout = config.staticPageGenerationTimeout || 0 - return new Worker(staticWorkerPath, { timeout: timeout * 1000, logger: Log, @@ -618,6 +607,11 @@ function createStaticWorker( ? incrementalCacheIpcPort + '' : undefined, __NEXT_INCREMENTAL_CACHE_IPC_KEY: incrementalCacheIpcValidationKey, + // we don't pass down NODE_OPTIONS as it can + // extra memory usage + NODE_OPTIONS: getNodeOptionsWithoutInspect() + .replace(/--max-old-space-size=[\d]{1,}/, '') + .trim(), }, }, enableWorkerThreads: config.experimental.workerThreads, @@ -1240,8 +1234,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 +1276,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 +1375,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 +2751,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 +2808,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 +3318,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 86aef60666c6c..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 @@ -325,7 +326,7 @@ async function tryLoadWasmWithFallback(attempts: any) { downloadWasmPromise = downloadWasmSwc(nextVersion, wasmDirectory) } await downloadWasmPromise - let bindings = await loadWasm(pathToFileURL(wasmDirectory).href) + let bindings = await loadWasm(wasmDirectory) // @ts-expect-error TODO: this event has a wrong type. eventSwcLoadFailure({ wasm: 'fallback', @@ -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/type-check.ts b/packages/next/src/build/type-check.ts index c8c18352311da..4d5fa4008ec6a 100644 --- a/packages/next/src/build/type-check.ts +++ b/packages/next/src/build/type-check.ts @@ -4,7 +4,7 @@ import type { Span } from '../trace' import path from 'path' import * as Log from './output/log' -import { Worker as JestWorker } from 'next/dist/compiled/jest-worker' +import { Worker } from '../lib/worker' import { verifyAndLint } from '../lib/verifyAndLint' import createSpinner from './spinner' import { eventTypeCheckCompleted } from '../telemetry/events' @@ -30,20 +30,18 @@ function verifyTypeScriptSetup( hasAppDir: boolean, hasPagesDir: boolean ) { - const typeCheckWorker = new JestWorker( + const typeCheckWorker = new Worker( require.resolve('../lib/verify-typescript-setup'), { + exposedMethods: ['verifyTypeScriptSetup'], numWorkers: 1, enableWorkerThreads, maxRetries: 0, } - ) as JestWorker & { + ) as Worker & { verifyTypeScriptSetup: typeof import('../lib/verify-typescript-setup').verifyTypeScriptSetup } - typeCheckWorker.getStdout().pipe(process.stdout) - typeCheckWorker.getStderr().pipe(process.stderr) - return typeCheckWorker .verifyTypeScriptSetup({ dir, diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 6e1c9c06bc6bf..0611d74083de0 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 }, @@ -1569,6 +1569,7 @@ export async function isPageStatic({ distDir, page: originalAppPath || page, isAppPath: pageType === 'app', + isDev: false, }) } const Comp = componentsResult.Component as NextComponentType | undefined @@ -1802,6 +1803,7 @@ export async function hasCustomGetInitialProps({ distDir, page: page, isAppPath: false, + isDev: false, }) let mod = components.ComponentMod @@ -1828,6 +1830,7 @@ export async function getDefinedNamedExports({ distDir, page: page, isAppPath: false, + isDev: false, }) return Object.keys(components.ComponentMod).filter((key) => { 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-build/index.ts b/packages/next/src/build/webpack-build/index.ts index 5200e65da7aee..7f74a2226694c 100644 --- a/packages/next/src/build/webpack-build/index.ts +++ b/packages/next/src/build/webpack-build/index.ts @@ -2,9 +2,8 @@ import type { COMPILER_INDEXES } from '../../shared/lib/constants' import * as Log from '../output/log' import { NextBuildContext } from '../build-context' import type { BuildTraceContext } from '../webpack/plugins/next-trace-entrypoints-plugin' -import { Worker } from 'next/dist/compiled/jest-worker' +import { Worker } from '../../lib/worker' import origDebug from 'next/dist/compiled/debug' -import type { ChildProcess } from 'child_process' import path from 'path' import { exportTraceState, recordTraceEvents } from '../../trace' @@ -38,8 +37,13 @@ async function webpackBuildWithWorker( prunedBuildContext.pluginState = pluginState - const getWorker = (compilerName: string) => { - const _worker = new Worker(path.join(__dirname, 'impl.js'), { + const combinedResult = { + duration: 0, + buildTraceContext: {} as BuildTraceContext, + } + + for (const compilerName of compilerNames) { + const worker = new Worker(path.join(__dirname, 'impl.js'), { exposedMethods: ['workerMain'], numWorkers: 1, maxRetries: 0, @@ -50,31 +54,6 @@ async function webpackBuildWithWorker( }, }, }) as Worker & typeof import('./impl') - _worker.getStderr().pipe(process.stderr) - _worker.getStdout().pipe(process.stdout) - - for (const worker of ((_worker as any)._workerPool?._workers || []) as { - _child: ChildProcess - }[]) { - worker._child.on('exit', (code, signal) => { - if (code || (signal && signal !== 'SIGINT')) { - debug( - `Compiler ${compilerName} unexpectedly exited with code: ${code} and signal: ${signal}` - ) - } - }) - } - - return _worker - } - - const combinedResult = { - duration: 0, - buildTraceContext: {} as BuildTraceContext, - } - - for (const compilerName of compilerNames) { - const worker = getWorker(compilerName) const curResult = await worker.workerMain({ buildContext: prunedBuildContext, diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 24b72f045bfdb..6f72f2db528b5 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -844,6 +844,8 @@ export default async function getBaseWebpackConfig( const aliasCodeConditionTest = [codeCondition.test, pageExtensionsRegex] + const builtinModules = require('module').builtinModules + let webpackConfig: webpack.Configuration = { parallelism: Number(process.env.NEXT_WEBPACK_PARALLELISM) || undefined, ...(isNodeServer ? { externalsPresets: { node: true } } : {}), @@ -867,6 +869,7 @@ export default async function getBaseWebpackConfig( : []), ] : [ + ...builtinModules, ({ context, request, @@ -1189,6 +1192,7 @@ export default async function getBaseWebpackConfig( 'next-metadata-route-loader', 'modularize-import-loader', 'next-barrel-loader', + 'next-error-browser-binary-loader', ].reduce((alias, loader) => { // using multiple aliases to replace `resolveLoader.modules` alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader) @@ -1273,6 +1277,14 @@ export default async function getBaseWebpackConfig( or: WEBPACK_LAYERS.GROUP.nonClientServerTarget, }, }, + ...(isNodeServer + ? [] + : [ + { + test: /[\\/].*?\.node$/, + loader: 'next-error-browser-binary-loader', + }, + ]), ...(hasAppDir ? [ { @@ -1812,7 +1824,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-error-browser-binary-loader.ts b/packages/next/src/build/webpack/loaders/next-error-browser-binary-loader.ts new file mode 100644 index 0000000000000..36958fa050fa2 --- /dev/null +++ b/packages/next/src/build/webpack/loaders/next-error-browser-binary-loader.ts @@ -0,0 +1,11 @@ +import type { webpack } from 'next/dist/compiled/webpack/webpack' + +export default function nextErrorBrowserBinaryLoader( + this: webpack.LoaderContext +) { + const { resourcePath, rootContext } = this + const relativePath = resourcePath.slice(rootContext.length + 1) + throw new Error( + `Node.js binary module ./${relativePath} is not supported in the browser. Please only use the module on server side` + ) +} 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/next-metadata-route-loader.ts b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts index 3f67dfe1b3360..f8eb52016d18c 100644 --- a/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts @@ -14,6 +14,27 @@ function errorOnBadHandler(resourcePath: string) { ` } +/* re-export the userland route configs */ +async function createReExportsCode( + resourcePath: string, + loaderContext: webpack.LoaderContext +) { + const exportNames = await getLoaderModuleNamedExports( + resourcePath, + loaderContext + ) + // Re-export configs but avoid conflicted exports + const reExportNames = exportNames.filter( + (name) => name !== 'default' && name !== 'generateSitemaps' + ) + + return reExportNames.length > 0 + ? `export { ${reExportNames.join(', ')} } from ${JSON.stringify( + resourcePath + )}\n` + : '' +} + const cacheHeader = { none: 'no-cache, no-store', longCache: 'public, immutable, no-transform, max-age=31536000', @@ -83,7 +104,10 @@ export const dynamic = 'force-static' return code } -function getDynamicTextRouteCode(resourcePath: string) { +async function getDynamicTextRouteCode( + resourcePath: string, + loaderContext: webpack.LoaderContext +) { return `\ /* dynamic asset route */ import { NextResponse } from 'next/server' @@ -94,6 +118,7 @@ const contentType = ${JSON.stringify(getContentType(resourcePath))} const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)} ${errorOnBadHandler(resourcePath)} +${await createReExportsCode(resourcePath, loaderContext)} export async function GET() { const data = await handler() @@ -110,7 +135,10 @@ export async function GET() { } // /[id]/route.js -function getDynamicImageRouteCode(resourcePath: string) { +async function getDynamicImageRouteCode( + resourcePath: string, + loaderContext: webpack.LoaderContext +) { return `\ /* dynamic image route */ import { NextResponse } from 'next/server' @@ -122,6 +150,7 @@ const handler = imageModule.default const generateImageMetadata = imageModule.generateImageMetadata ${errorOnBadHandler(resourcePath)} +${await createReExportsCode(resourcePath, loaderContext)} export async function GET(_, ctx) { const { __metadata_id__, ...params } = ctx.params || {} @@ -160,10 +189,6 @@ async function getDynamicSiteMapRouteCode( resourcePath, loaderContext ) - // Re-export configs but avoid conflicted exports - const reExportNames = exportNames.filter( - (name) => name !== 'default' && name !== 'generateSitemaps' - ) const hasGenerateSiteMaps = exportNames.includes('generateSitemaps') if ( @@ -197,15 +222,7 @@ const contentType = ${JSON.stringify(getContentType(resourcePath))} const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)} ${errorOnBadHandler(resourcePath)} - -${'' /* re-export the userland route configs */} -${ - reExportNames.length > 0 - ? `export { ${reExportNames.join(', ')} } from ${JSON.stringify( - resourcePath - )}\n` - : '' -} +${await createReExportsCode(resourcePath, loaderContext)} export async function GET(_, ctx) { const { __metadata_id__, ...params } = ctx.params || {} @@ -266,11 +283,11 @@ const nextMetadataRouterLoader: webpack.LoaderDefinitionFunction/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/css-chunking-plugin.ts b/packages/next/src/build/webpack/plugins/css-chunking-plugin.ts index 3a2c48e9cf5fa..a4699d04b7b76 100644 --- a/packages/next/src/build/webpack/plugins/css-chunking-plugin.ts +++ b/packages/next/src/build/webpack/plugins/css-chunking-plugin.ts @@ -11,6 +11,10 @@ const MIN_CSS_CHUNK_SIZE = 30 * 1024 */ const MAX_CSS_CHUNK_SIZE = 100 * 1024 +function isGlobalCss(module: Module) { + return !/\.module\.(css|scss|sass)$/.test(module.nameForCondition() || '') +} + type ChunkState = { chunk: Chunk modules: Module[] @@ -125,6 +129,8 @@ export class CssChunkingPlugin { // Process through all modules for (const startModule of remainingModules) { + let globalCssMode = isGlobalCss(startModule) + // The current position of processing in all selected chunks let allChunkStates = new Map(chunkStatesByModule.get(startModule)!) @@ -225,8 +231,36 @@ export class CssChunkingPlugin { } } } + + // Global CSS must not leak into unrelated chunks + const nextIsGlobalCss = isGlobalCss(nextModule) + if (nextIsGlobalCss && globalCssMode) { + if (allChunkStates.size !== nextChunkStates.size) { + // Fast check + continue + } + } + if (globalCssMode) { + for (const chunkState of nextChunkStates.keys()) { + if (!allChunkStates.has(chunkState)) { + // Global CSS would leak into chunkState + continue loop + } + } + } + if (nextIsGlobalCss) { + for (const chunkState of allChunkStates.keys()) { + if (!nextChunkStates.has(chunkState)) { + // Global CSS would leak into chunkState + continue loop + } + } + } potentialNextModules.delete(nextModule) currentSize += size + if (nextIsGlobalCss) { + globalCssMode = true + } for (const [chunkState, i] of nextChunkStates) { if (allChunkStates.has(chunkState)) { // This reduces the request count of the chunk group diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index aea7e34537f2d..3df3abc9b4e74 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -62,7 +62,7 @@ function getNextPublicEnvironmentVariables(): DefineEnv { for (const key in process.env) { if (key.startsWith('NEXT_PUBLIC_')) { const value = process.env[key] - if (value) { + if (value != null) { defineEnv[`process.env.${key}`] = value } } @@ -79,7 +79,7 @@ function getNextConfigEnv(config: NextConfigComplete): DefineEnv { const env = config.env for (const key in env) { const value = env[key] - if (value) { + if (value != null) { errorIfEnvConflicted(config, key) defineEnv[`process.env.${key}`] = value } @@ -108,15 +108,17 @@ function getImageConfig( 'process.env.__NEXT_IMAGE_OPTS': { deviceSizes: config.images.deviceSizes, imageSizes: config.images.imageSizes, + qualities: config.images.qualities, path: config.images.path, loader: config.images.loader, dangerouslyAllowSVG: config.images.dangerouslyAllowSVG, unoptimized: config?.images?.unoptimized, ...(dev ? { - // pass domains in development to allow validating on the client + // additional config in dev to allow validating on the client domains: config.images.domains, remotePatterns: config.images?.remotePatterns, + localPatterns: config.images?.localPatterns, output: config.output, } : {}), 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 932c5356c3e04..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 || '' - // 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 | undefined = - modPath + mod.resourceResolveData?.query - - // 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..5d0fd0e977cc0 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 }), } @@ -296,7 +291,9 @@ function isDynamicCodeEvaluationAllowed( const name = fileName.replace(rootDir ?? '', '') - return picomatch(middlewareConfig?.unstable_allowDynamicGlobs ?? [])(name) + return picomatch(middlewareConfig?.unstable_allowDynamicGlobs ?? [], { + dot: true, + })(name) } function buildUnsupportedApiError({ @@ -739,18 +736,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 69e3eb7af9c7c..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 @@ -1,3 +1,5 @@ +import { normalizedAssetPrefix } from '../../../../../shared/lib/normalized-asset-prefix' + function getSocketProtocol(assetPrefix: string): string { let protocol = window.location.protocol @@ -6,21 +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): string { - const { hostname, port } = window.location - const protocol = getSocketProtocol(assetPrefix) - const normalizedAssetPrefix = assetPrefix.replace(/^\/+/, '') - - let url = `${protocol}://${hostname}:${port}${ - normalizedAssetPrefix ? `/${normalizedAssetPrefix}` : '' - }` +export function getSocketUrl(assetPrefix: string | undefined): string { + const prefix = normalizedAssetPrefix(assetPrefix) + const protocol = getSocketProtocol(assetPrefix || '') - if (normalizedAssetPrefix.startsWith('http')) { - url = `${protocol}://${normalizedAssetPrefix.split('://', 2)[1]}` + 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 url + const { hostname, port } = window.location + return `${protocol}//${hostname}${port ? `:${port}` : ''}${prefix}` } diff --git a/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts b/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts index 315c4dfc974ee..9fdea5b9ddef2 100644 --- a/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts +++ b/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts @@ -1,4 +1,5 @@ import type { HMR_ACTION_TYPES } from '../../../../server/dev/hot-reloader-types' +import { getSocketUrl } from '../internal/helpers/get-socket-url' let source: WebSocket @@ -6,17 +7,6 @@ type ActionCallback = (action: HMR_ACTION_TYPES) => void const eventCallbacks: Array = [] -function getSocketProtocol(assetPrefix: string): string { - let protocol = location.protocol - - try { - // assetPrefix is a url - protocol = new URL(assetPrefix).protocol - } catch {} - - return protocol === 'http:' ? 'ws' : 'wss' -} - export function addMessageListener(callback: ActionCallback) { eventCallbacks.push(callback) } @@ -62,17 +52,7 @@ export function connectHMR(options: { path: string; assetPrefix: string }) { timer = setTimeout(init, reconnections > 5 ? 5000 : 1000) } - const { hostname, port } = location - const protocol = getSocketProtocol(options.assetPrefix || '') - const assetPrefix = options.assetPrefix.replace(/^\/+/, '') - - let url = `${protocol}://${hostname}:${port}${ - assetPrefix ? `/${assetPrefix}` : '' - }` - - if (assetPrefix.startsWith('http')) { - url = `${protocol}://${assetPrefix.split('://', 2)[1]}` - } + const url = getSocketUrl(options.assetPrefix) source = new window.WebSocket(`${url}${options.path}`) source.onopen = handleOnline 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 9a8b5bc901dc6..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] @@ -101,7 +105,10 @@ export function createInitialRouterState({ // Seed the prefetch cache with this page's data. // This is to prevent needlessly re-prefetching a page that is already reusable, // and will avoid triggering a loading state/data fetch stall when navigating back to the page. - const url = new URL(location.pathname, location.origin) + const url = new URL( + `${location.pathname}${location.search}`, + location.origin + ) const initialFlightData: FlightData = [['', initialTree, null, null]] createPrefetchCacheEntryForInitialLoad({ diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 2af83aa8a203c..1b8c4d6f2787a 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -56,6 +56,7 @@ export async function fetchServerResponse( [NEXT_ROUTER_STATE_TREE]: string [NEXT_URL]?: string [NEXT_ROUTER_PREFETCH_HEADER]?: '1' + 'x-deployment-id'?: string } = { // Enable flight response [RSC_HEADER]: '1', @@ -79,6 +80,10 @@ export async function fetchServerResponse( headers[NEXT_URL] = nextUrl } + if (process.env.NEXT_DEPLOYMENT_ID) { + headers['x-deployment-id'] = process.env.NEXT_DEPLOYMENT_ID + } + const uniqueCacheQuery = hexHash( [ headers[NEXT_ROUTER_PREFETCH_HEADER] || '0', diff --git a/packages/next/src/client/image-component.tsx b/packages/next/src/client/image-component.tsx index a4ef2efe32f8b..6faf65c8bf7f1 100644 --- a/packages/next/src/client/image-component.tsx +++ b/packages/next/src/client/image-component.tsx @@ -374,7 +374,8 @@ export const Image = forwardRef( const c = configEnv || configContext || imageConfigDefault const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b) const deviceSizes = c.deviceSizes.sort((a, b) => a - b) - return { ...c, allSizes, deviceSizes } + const qualities = c.qualities?.sort((a, b) => a - b) + return { ...c, allSizes, deviceSizes, qualities } }, [configContext]) const { onLoad, onLoadingComplete } = props diff --git a/packages/next/src/client/index.tsx b/packages/next/src/client/index.tsx index dc5ba991c6450..f830642717c2f 100644 --- a/packages/next/src/client/index.tsx +++ b/packages/next/src/client/index.tsx @@ -496,8 +496,8 @@ function markHydrateComplete(): void { if ( process.env.NODE_ENV === 'development' && // Old versions of Safari don't return `PerformanceMeasure`s from `performance.measure()` - beforeHydrationMeasure !== undefined && - hydrationMeasure !== undefined + beforeHydrationMeasure && + hydrationMeasure ) { tracer .startSpan('navigation-to-hydration', { diff --git a/packages/next/src/client/legacy/image.tsx b/packages/next/src/client/legacy/image.tsx index d982e9fa8a330..06ea510c7ba7d 100644 --- a/packages/next/src/client/legacy/image.tsx +++ b/packages/next/src/client/legacy/image.tsx @@ -25,7 +25,7 @@ import { normalizePathTrailingSlash } from '../normalize-trailing-slash' function normalizeSrc(src: string): string { return src[0] === '/' ? src.slice(1) : src } - +const DEFAULT_Q = 75 const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete const loadedImageURLs = new Set() const allImgs = new Map< @@ -139,6 +139,25 @@ function defaultLoader({ ) } + if (src.startsWith('/') && config.localPatterns) { + if ( + process.env.NODE_ENV !== 'test' && + // micromatch isn't compatible with edge runtime + process.env.NEXT_RUNTIME !== 'edge' + ) { + // We use dynamic require because this should only error in development + const { + hasLocalMatch, + } = require('../../shared/lib/match-local-pattern') + if (!hasLocalMatch(config.localPatterns, src)) { + throw new Error( + `Invalid src prop (${src}) on \`next/image\` does not match \`images.localPatterns\` configured in your \`next.config.js\`\n` + + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns` + ) + } + } + } + if (!src.startsWith('/') && (config.domains || config.remotePatterns)) { let parsedSrc: URL try { @@ -156,8 +175,10 @@ function defaultLoader({ process.env.NEXT_RUNTIME !== 'edge' ) { // We use dynamic require because this should only error in development - const { hasMatch } = require('../../shared/lib/match-remote-pattern') - if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) { + const { + hasRemoteMatch, + } = require('../../shared/lib/match-remote-pattern') + if (!hasRemoteMatch(config.domains, config.remotePatterns, parsedSrc)) { throw new Error( `Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host` @@ -165,8 +186,22 @@ function defaultLoader({ } } } + + if (quality && config.qualities && !config.qualities.includes(quality)) { + throw new Error( + `Invalid quality prop (${quality}) on \`next/image\` does not match \`images.qualities\` configured in your \`next.config.js\`\n` + + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities` + ) + } } + const q = + quality || + config.qualities?.reduce((prev, cur) => + Math.abs(cur - DEFAULT_Q) < Math.abs(prev - DEFAULT_Q) ? cur : prev + ) || + DEFAULT_Q + if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) { // Special case to make svg serve as-is to avoid proxying // through the built-in Image Optimization API. @@ -175,7 +210,7 @@ function defaultLoader({ return `${normalizePathTrailingSlash(config.path)}?url=${encodeURIComponent( src - )}&w=${width}&q=${quality || 75}` + )}&w=${width}&q=${q}` } const loaders = new Map< @@ -616,7 +651,8 @@ export default function Image({ const c = configEnv || configContext || imageConfigDefault const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b) const deviceSizes = c.deviceSizes.sort((a, b) => a - b) - return { ...c, allSizes, deviceSizes } + const qualities = c.qualities?.sort((a, b) => a - b) + return { ...c, allSizes, deviceSizes, qualities } }, [configContext]) let rest: Partial = all 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/client/script.tsx b/packages/next/src/client/script.tsx index 32f8ea719b0f3..bbc5d1ee02af2 100644 --- a/packages/next/src/client/script.tsx +++ b/packages/next/src/client/script.tsx @@ -349,8 +349,13 @@ function Script(props: ScriptProps): JSX.Element | null { ReactDOM.preload( src, restProps.integrity - ? { as: 'script', integrity: restProps.integrity, nonce } - : { as: 'script', nonce } + ? { + as: 'script', + integrity: restProps.integrity, + nonce, + crossOrigin: restProps.crossOrigin, + } + : { as: 'script', nonce, crossOrigin: restProps.crossOrigin } ) return ( + + + `) + }) + + const port = await findPort() + await new Promise((res) => { + server.listen(port, () => res()) + }) + + return { + server, + port, + } +} + +describe.each([['', '/docs']])( + 'allowed-dev-origins, basePath: %p', + (basePath: string) => { + let next: NextInstance + + describe('warn mode', () => { + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'misc/pages')), + public: new FileRef(join(__dirname, 'misc/public')), + }, + nextConfig: { + basePath, + }, + }) + + await retry(async () => { + // make sure host server is running + const asset = await fetchViaHTTP( + next.appPort, + basePath + '/_next/static/chunks/pages/_app.js' + ) + if (asset.status >= 300) { + throw new Error('Host server is not running') + } + }) + }) + afterAll(() => next.destroy()) + + it('should warn about WebSocket from cross-site', async () => { + const { server, port } = await createHostServer() + try { + const websocketSnippet = `(() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const ws = new WebSocket("${next.url.replace( + 'http://', + 'ws://' + )}/_next/webpack-hmr") + + ws.addEventListener('error', (err) => { + statusEl.innerText = 'error' + }) + ws.addEventListener('open', () => { + statusEl.innerText = 'connected' + }) + })()` + + // ensure direct port with mismatching port is blocked + const browser = await webdriver(`http://127.0.0.1:${port}`, '/about') + await browser.eval(websocketSnippet) + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe( + 'connected' + ) + }) + + // ensure different host is blocked + await browser.get(`https://example.vercel.sh/`) + await browser.eval(websocketSnippet) + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe( + 'connected' + ) + }) + + expect(next.cliOutput).toContain('Cross origin request detected from') + } finally { + server.close() + } + }) + + it('should warn about loading scripts from cross-site', async () => { + const { server, port } = await createHostServer() + + try { + const scriptSnippet = `(() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const script = document.createElement('script') + script.src = "${next.url}/_next/static/chunks/pages/_app.js" + + script.onerror = (err) => { + statusEl.innerText = 'error' + } + script.onload = () => { + statusEl.innerText = 'connected' + } + document.querySelector('body').appendChild(script) + })()` + + // ensure direct port with mismatching port is blocked + const browser = await webdriver(`http://127.0.0.1:${port}`, '/about') + await browser.eval(scriptSnippet) + + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe( + 'connected' + ) + }) + + // ensure different host is blocked + await browser.get(`https://example.vercel.sh/`) + await browser.eval(scriptSnippet) + + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe( + 'connected' + ) + }) + + expect(next.cliOutput).toContain('Cross origin request detected from') + } finally { + server.close() + } + }) + + it('should warn about loading internal middleware from cross-site', async () => { + const { server, port } = await createHostServer() + try { + const browser = await webdriver(`http://127.0.0.1:${port}`, '/about') + + const middlewareSnippet = `(() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const xhr = new XMLHttpRequest() + xhr.open('GET', '${next.url}/__nextjs_error_feedback?errorCode=0&wasHelpful=true', true) + xhr.send() + + xhr.onload = () => { + statusEl.innerText = "OK" + } + xhr.onerror = () => { + statusEl.innerText = "Unauthorized" + } + })()` + + await browser.eval(middlewareSnippet) + + await retry(async () => { + // TODO: These requests seem to be blocked regardless of our handling only when running with Turbopack + // Investigate why this is the case + if (!process.env.TURBOPACK) { + expect(await browser.elementByCss('#status').text()).toBe('OK') + } + + expect(next.cliOutput).toContain( + 'Cross origin request detected from' + ) + }) + } finally { + server.close() + } + }) + }) + + describe('block mode', () => { + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'misc/pages')), + public: new FileRef(join(__dirname, 'misc/public')), + }, + nextConfig: { + basePath, + allowedDevOrigins: ['localhost'], + }, + }) + + await retry(async () => { + // make sure host server is running + const asset = await fetchViaHTTP( + next.appPort, + basePath + '/_next/static/chunks/pages/_app.js' + ) + if (asset.status >= 300) { + throw new Error('Host server is not running') + } + }) + }) + afterAll(() => next.destroy()) + + it('should not allow dev WebSocket from cross-site', async () => { + const { server, port } = await createHostServer() + try { + const websocketSnippet = `(() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const ws = new WebSocket("${next.url.replace( + 'http://', + 'ws://' + )}/_next/webpack-hmr") + + ws.addEventListener('error', (err) => { + statusEl.innerText = 'error' + }) + ws.addEventListener('open', () => { + statusEl.innerText = 'connected' + }) + })()` + + // ensure direct port with mismatching port is blocked + const browser = await webdriver(`http://127.0.0.1:${port}`, '/about') + await browser.eval(websocketSnippet) + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe('error') + }) + + // ensure different host is blocked + await browser.get(`https://example.vercel.sh/`) + await browser.eval(websocketSnippet) + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe('error') + }) + } finally { + server.close() + } + }) + + it('should not allow loading scripts from cross-site', async () => { + const { server, port } = await createHostServer() + try { + const scriptSnippet = `(() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const script = document.createElement('script') + script.src = "${next.url}/_next/static/chunks/pages/_app.js" + + script.onerror = (err) => { + statusEl.innerText = 'error' + } + script.onload = () => { + statusEl.innerText = 'connected' + } + document.querySelector('body').appendChild(script) + })()` + + // ensure direct port with mismatching port is blocked + const browser = await webdriver(`http://127.0.0.1:${port}`, '/about') + await browser.eval(scriptSnippet) + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe('error') + }) + + // ensure different host is blocked + await browser.get(`https://example.vercel.sh/`) + await browser.eval(scriptSnippet) + + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe('error') + }) + } finally { + server.close() + } + }) + + it('should not allow loading internal middleware from cross-site', async () => { + const { server, port } = await createHostServer() + try { + const browser = await webdriver(`http://127.0.0.1:${port}`, '/about') + + const middlewareSnippet = `(() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const xhr = new XMLHttpRequest() + xhr.open('GET', '${next.url}/__nextjs_error_feedback?errorCode=0&wasHelpful=true', true) + xhr.send() + + xhr.onload = () => { + statusEl.innerText = "OK" + } + xhr.onerror = () => { + statusEl.innerText = "Unauthorized" + } + })()` + + await browser.eval(middlewareSnippet) + + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe( + 'Unauthorized' + ) + }) + } finally { + server.close() + } + }) + + it('should load images regardless of allowed origins', async () => { + const { server, port } = await createHostServer() + try { + const browser = await webdriver(`http://127.0.0.1:${port}`, '/about') + + const imageSnippet = `(() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const image = document.createElement('img') + image.src = "${next.url}/_next/image?url=%2Fimage.png&w=256&q=75" + document.querySelector('body').appendChild(image) + image.onload = () => { + statusEl.innerText = 'OK' + } + image.onerror = () => { + statusEl.innerText = 'Unauthorized' + } + })()` + + await browser.eval(imageSnippet) + + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe('OK') + }) + } finally { + server.close() + } + }) + }) + } +) diff --git a/test/development/basic/define-class-fields/define-class-fields.test.ts b/test/development/basic/define-class-fields/define-class-fields.test.ts index 9b8f78bb4f389..df2bd4c441c6f 100644 --- a/test/development/basic/define-class-fields/define-class-fields.test.ts +++ b/test/development/basic/define-class-fields/define-class-fields.test.ts @@ -10,7 +10,7 @@ createNextDescribe( dependencies: { mobx: '6.3.7', typescript: 'latest', - '@types/react': 'latest', + '@types/react': '^18.2.0', '@types/node': 'latest', 'mobx-react': '7.2.1', }, diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index 0385ccf69b966..df72d7b73ea5f 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -15,431 +15,422 @@ import { import { createNext } from 'e2e-utils' import { NextInstance } from 'e2e-utils' import { outdent } from 'outdent' - -describe.each([[''], ['/docs']])( - 'basic HMR, basePath: %p', - (basePath: string) => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: join(__dirname, 'hmr'), - nextConfig: { - basePath, - }, - }) - }) - afterAll(() => next.destroy()) - - it('should show hydration error correctly', async () => { - const browser = await webdriver(next.url, basePath + '/hydration-error') - await check(async () => { - const logs = await browser.log() - return logs.some((log) => - log.message.includes('messages/react-hydration-error') - ) - ? 'success' - : JSON.stringify(logs, null, 2) - }, 'success') +import { NextConfig } from 'next' + +describe.each([ + { basePath: '', assetPrefix: '' }, + { basePath: '/docs', assetPrefix: '' }, + // this is a long running test reduce runtime by + // only running on main cases above + // { basePath: '', assetPrefix: '/asset-prefix' }, + // { basePath: '/docs', assetPrefix: '/asset-prefix' }, +])('basic HMR, nextConfig: %o', (nextConfig: Partial) => { + const { basePath } = nextConfig + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: join(__dirname, 'hmr'), + nextConfig, }) - - it('should have correct router.isReady for auto-export page', async () => { - let browser = await webdriver( - next.url, - basePath + '/auto-export-is-ready' - ) - - expect(await browser.elementByCss('#ready').text()).toBe('yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual( - {} - ) - - browser = await webdriver( - next.url, - basePath + '/auto-export-is-ready?hello=world' + }) + afterAll(() => next.destroy()) + + it('should show hydration error correctly', async () => { + const browser = await webdriver(next.url, basePath + '/hydration-error') + await check(async () => { + const logs = await browser.log() + return logs.some((log) => + log.message.includes('messages/react-hydration-error') ) - - await check(async () => { - return browser.elementByCss('#ready').text() - }, 'yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ - hello: 'world', - }) + ? 'success' + : JSON.stringify(logs, null, 2) + }, 'success') + }) + + it('should have correct router.isReady for auto-export page', async () => { + let browser = await webdriver(next.url, basePath + '/auto-export-is-ready') + + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) + + browser = await webdriver( + next.url, + basePath + '/auto-export-is-ready?hello=world' + ) + + await check(async () => { + return browser.elementByCss('#ready').text() + }, 'yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', }) + }) - it('should have correct router.isReady for getStaticProps page', async () => { - let browser = await webdriver(next.url, basePath + '/gsp-is-ready') + it('should have correct router.isReady for getStaticProps page', async () => { + let browser = await webdriver(next.url, basePath + '/gsp-is-ready') - expect(await browser.elementByCss('#ready').text()).toBe('yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual( - {} - ) + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) - browser = await webdriver( - next.url, - basePath + '/gsp-is-ready?hello=world' - ) + browser = await webdriver(next.url, basePath + '/gsp-is-ready?hello=world') - await check(async () => { - return browser.elementByCss('#ready').text() - }, 'yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ - hello: 'world', - }) + await check(async () => { + return browser.elementByCss('#ready').text() + }, 'yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', }) + }) - describe('Hot Module Reloading', () => { - describe('delete a page and add it back', () => { - it('should load the page properly', async () => { - const contactPagePath = join('pages', 'hmr', 'contact.js') - const newContactPagePath = join('pages', 'hmr', '_contact.js') - let browser - try { - browser = await webdriver(next.url, basePath + '/hmr/contact') - const text = await browser.elementByCss('p').text() - expect(text).toBe('This is the contact page.') + describe('Hot Module Reloading', () => { + describe('delete a page and add it back', () => { + it('should load the page properly', async () => { + const contactPagePath = join('pages', 'hmr', 'contact.js') + const newContactPagePath = join('pages', 'hmr', '_contact.js') + let browser + try { + browser = await webdriver(next.url, basePath + '/hmr/contact') + const text = await browser.elementByCss('p').text() + expect(text).toBe('This is the contact page.') - // Rename the file to mimic a deleted page - await next.renameFile(contactPagePath, newContactPagePath) + // Rename the file to mimic a deleted page + await next.renameFile(contactPagePath, newContactPagePath) - await check( - () => getBrowserBodyText(browser), - /This page could not be found/ - ) + await check( + () => getBrowserBodyText(browser), + /This page could not be found/ + ) - // Rename the file back to the original filename - await next.renameFile(newContactPagePath, contactPagePath) + // Rename the file back to the original filename + await next.renameFile(newContactPagePath, contactPagePath) - // wait until the page comes back - await check( - () => getBrowserBodyText(browser), - /This is the contact page/ - ) + // wait until the page comes back + await check( + () => getBrowserBodyText(browser), + /This is the contact page/ + ) - expect(next.cliOutput).toContain('Compiled /_error') - } finally { - if (browser) { - await browser.close() - } - await next - .renameFile(newContactPagePath, contactPagePath) - .catch(() => {}) + expect(next.cliOutput).toContain('Compiled /_error') + } finally { + if (browser) { + await browser.close() } - }) + await next + .renameFile(newContactPagePath, contactPagePath) + .catch(() => {}) + } }) + }) - describe('editing a page', () => { - it('should detect the changes and display it', async () => { - let browser - try { - browser = await webdriver(next.url, basePath + '/hmr/about') - const text = await browser.elementByCss('p').text() - expect(text).toBe('This is the about page.') - - const aboutPagePath = join('pages', 'hmr', 'about.js') + describe('editing a page', () => { + it('should detect the changes and display it', async () => { + let browser + try { + browser = await webdriver(next.url, basePath + '/hmr/about') + const text = await browser.elementByCss('p').text() + expect(text).toBe('This is the about page.') - const originalContent = await next.readFile(aboutPagePath) - const editedContent = originalContent.replace( - 'This is the about page', - 'COOL page' - ) + const aboutPagePath = join('pages', 'hmr', 'about.js') - // change the content - try { - await next.patchFile(aboutPagePath, editedContent) - await check(() => getBrowserBodyText(browser), /COOL page/) - } finally { - // add the original content - await next.patchFile(aboutPagePath, originalContent) - } + const originalContent = await next.readFile(aboutPagePath) + const editedContent = originalContent.replace( + 'This is the about page', + 'COOL page' + ) - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + // change the content + try { + await next.patchFile(aboutPagePath, editedContent) + await check(() => getBrowserBodyText(browser), /COOL page/) } finally { - if (browser) { - await browser.close() - } + // add the original content + await next.patchFile(aboutPagePath, originalContent) } - }) - - it('should not reload unrelated pages', async () => { - let browser - try { - browser = await webdriver(next.url, basePath + '/hmr/counter') - const text = await browser - .elementByCss('button') - .click() - .elementByCss('button') - .click() - .elementByCss('p') - .text() - expect(text).toBe('COUNT: 2') - - const aboutPagePath = join('pages', 'hmr', 'about.js') - - const originalContent = await next.readFile(aboutPagePath) - const editedContent = originalContent.replace( - 'This is the about page', - 'COOL page' - ) - try { - // Change the about.js page - await next.patchFile(aboutPagePath, editedContent) - - // Check whether the this page has reloaded or not. - await check(() => browser.elementByCss('p').text(), /COUNT: 2/) - } finally { - // restore the about page content. - await next.patchFile(aboutPagePath, originalContent) - } - } finally { - if (browser) { - await browser.close() - } + await check( + () => getBrowserBodyText(browser), + /This is the about page/ + ) + } finally { + if (browser) { + await browser.close() } - }) + } + }) + + it('should not reload unrelated pages', async () => { + let browser + try { + browser = await webdriver(next.url, basePath + '/hmr/counter') + const text = await browser + .elementByCss('button') + .click() + .elementByCss('button') + .click() + .elementByCss('p') + .text() + expect(text).toBe('COUNT: 2') + + const aboutPagePath = join('pages', 'hmr', 'about.js') + + const originalContent = await next.readFile(aboutPagePath) + const editedContent = originalContent.replace( + 'This is the about page', + 'COOL page' + ) - // Added because of a regression in react-hot-loader, see issues: #4246 #4273 - // Also: https://github.com/vercel/styled-jsx/issues/425 - it('should update styles correctly', async () => { - let browser try { - browser = await webdriver(next.url, basePath + '/hmr/style') - const pTag = await browser.elementByCss('.hmr-style-page p') - const initialFontSize = await pTag.getComputedCss('font-size') - - expect(initialFontSize).toBe('100px') - - const pagePath = join('pages', 'hmr', 'style.js') - - const originalContent = await next.readFile(pagePath) - const editedContent = originalContent.replace('100px', '200px') - - // Change the page - await next.patchFile(pagePath, editedContent) - - try { - // Check whether the this page has reloaded or not. - await check(async () => { - const editedPTag = await browser.elementByCss( - '.hmr-style-page p' - ) - return editedPTag.getComputedCss('font-size') - }, /200px/) - } finally { - // Finally is used so that we revert the content back to the original regardless of the test outcome - // restore the about page content. - await next.patchFile(pagePath, originalContent) - } + // Change the about.js page + await next.patchFile(aboutPagePath, editedContent) + + // Check whether the this page has reloaded or not. + await check(() => browser.elementByCss('p').text(), /COUNT: 2/) } finally { - if (browser) { - await browser.close() - } + // restore the about page content. + await next.patchFile(aboutPagePath, originalContent) } - }) + } finally { + if (browser) { + await browser.close() + } + } + }) - // Added because of a regression in react-hot-loader, see issues: #4246 #4273 - // Also: https://github.com/vercel/styled-jsx/issues/425 - it('should update styles in a stateful component correctly', async () => { - let browser - const pagePath = join('pages', 'hmr', 'style-stateful-component.js') - const originalContent = await next.readFile(pagePath) - try { - browser = await webdriver( - next.url, - basePath + '/hmr/style-stateful-component' - ) - const pTag = await browser.elementByCss('.hmr-style-page p') - const initialFontSize = await pTag.getComputedCss('font-size') + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/vercel/styled-jsx/issues/425 + it('should update styles correctly', async () => { + let browser + try { + browser = await webdriver(next.url, basePath + '/hmr/style') + const pTag = await browser.elementByCss('.hmr-style-page p') + const initialFontSize = await pTag.getComputedCss('font-size') + + expect(initialFontSize).toBe('100px') - expect(initialFontSize).toBe('100px') - const editedContent = originalContent.replace('100px', '200px') + const pagePath = join('pages', 'hmr', 'style.js') - // Change the page - await next.patchFile(pagePath, editedContent) + const originalContent = await next.readFile(pagePath) + const editedContent = originalContent.replace('100px', '200px') + + // Change the page + await next.patchFile(pagePath, editedContent) + try { // Check whether the this page has reloaded or not. await check(async () => { const editedPTag = await browser.elementByCss('.hmr-style-page p') return editedPTag.getComputedCss('font-size') }, /200px/) } finally { - if (browser) { - await browser.close() - } + // Finally is used so that we revert the content back to the original regardless of the test outcome + // restore the about page content. await next.patchFile(pagePath, originalContent) } - }) - - // Added because of a regression in react-hot-loader, see issues: #4246 #4273 - // Also: https://github.com/vercel/styled-jsx/issues/425 - it('should update styles in a dynamic component correctly', async () => { - let browser = null - let secondBrowser = null - const pagePath = join('components', 'hmr', 'dynamic.js') - const originalContent = await next.readFile(pagePath) - try { - browser = await webdriver( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - const div = await browser.elementByCss('#dynamic-component') - const initialClientClassName = await div.getAttribute('class') - const initialFontSize = await div.getComputedCss('font-size') - - expect(initialFontSize).toBe('100px') - - const initialHtml = await renderViaHTTP( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - expect(initialHtml.includes('100px')).toBeTruthy() - - const $initialHtml = cheerio.load(initialHtml) - const initialServerClassName = - $initialHtml('#dynamic-component').attr('class') + } finally { + if (browser) { + await browser.close() + } + } + }) - expect( - initialClientClassName === initialServerClassName - ).toBeTruthy() + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/vercel/styled-jsx/issues/425 + it('should update styles in a stateful component correctly', async () => { + let browser + const pagePath = join('pages', 'hmr', 'style-stateful-component.js') + const originalContent = await next.readFile(pagePath) + try { + browser = await webdriver( + next.url, + basePath + '/hmr/style-stateful-component' + ) + const pTag = await browser.elementByCss('.hmr-style-page p') + const initialFontSize = await pTag.getComputedCss('font-size') - const editedContent = originalContent.replace('100px', '200px') + expect(initialFontSize).toBe('100px') + const editedContent = originalContent.replace('100px', '200px') - // Change the page - await next.patchFile(pagePath, editedContent) + // Change the page + await next.patchFile(pagePath, editedContent) - // wait for 5 seconds - await waitFor(5000) + // Check whether the this page has reloaded or not. + await check(async () => { + const editedPTag = await browser.elementByCss('.hmr-style-page p') + return editedPTag.getComputedCss('font-size') + }, /200px/) + } finally { + if (browser) { + await browser.close() + } + await next.patchFile(pagePath, originalContent) + } + }) - secondBrowser = await webdriver( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - // Check whether the this page has reloaded or not. - const editedDiv = await secondBrowser.elementByCss( - '#dynamic-component' - ) - const editedClientClassName = await editedDiv.getAttribute('class') - const editedFontSize = await editedDiv.getComputedCss('font-size') - const browserHtml = await secondBrowser.eval( - 'document.documentElement.innerHTML' - ) + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/vercel/styled-jsx/issues/425 + it('should update styles in a dynamic component correctly', async () => { + let browser = null + let secondBrowser = null + const pagePath = join('components', 'hmr', 'dynamic.js') + const originalContent = await next.readFile(pagePath) + try { + browser = await webdriver( + next.url, + basePath + '/hmr/style-dynamic-component' + ) + const div = await browser.elementByCss('#dynamic-component') + const initialClientClassName = await div.getAttribute('class') + const initialFontSize = await div.getComputedCss('font-size') - expect(editedFontSize).toBe('200px') - expect(browserHtml.includes('font-size:200px')).toBe(true) - expect(browserHtml.includes('font-size:100px')).toBe(false) + expect(initialFontSize).toBe('100px') - const editedHtml = await renderViaHTTP( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - expect(editedHtml.includes('200px')).toBeTruthy() - const $editedHtml = cheerio.load(editedHtml) - const editedServerClassName = - $editedHtml('#dynamic-component').attr('class') + const initialHtml = await renderViaHTTP( + next.url, + basePath + '/hmr/style-dynamic-component' + ) + expect(initialHtml.includes('100px')).toBeTruthy() - expect(editedClientClassName === editedServerClassName).toBe(true) - } finally { - // Finally is used so that we revert the content back to the original regardless of the test outcome - // restore the about page content. - await next.patchFile(pagePath, originalContent) + const $initialHtml = cheerio.load(initialHtml) + const initialServerClassName = + $initialHtml('#dynamic-component').attr('class') - if (browser) { - await browser.close() - } + expect(initialClientClassName === initialServerClassName).toBeTruthy() - if (secondBrowser) { - secondBrowser.close() - } - } - }) - }) - }) + const editedContent = originalContent.replace('100px', '200px') - describe('Error Recovery', () => { - it('should recover from 404 after a page has been added', async () => { - let browser - const newPage = join('pages', 'hmr', 'new-page.js') + // Change the page + await next.patchFile(pagePath, editedContent) - try { - browser = await webdriver(next.url, basePath + '/hmr/new-page') + // wait for 5 seconds + await waitFor(5000) - expect(await browser.elementByCss('body').text()).toMatch( - /This page could not be found/ + secondBrowser = await webdriver( + next.url, + basePath + '/hmr/style-dynamic-component' ) - - // Add the page - await next.patchFile( - newPage, - 'export default () => (
the-new-page
)' + // Check whether the this page has reloaded or not. + const editedDiv = await secondBrowser.elementByCss( + '#dynamic-component' + ) + const editedClientClassName = await editedDiv.getAttribute('class') + const editedFontSize = await editedDiv.getComputedCss('font-size') + const browserHtml = await secondBrowser.eval( + 'document.documentElement.innerHTML' ) - await check(() => getBrowserBodyText(browser), /the-new-page/) - - await next.deleteFile(newPage) + expect(editedFontSize).toBe('200px') + expect(browserHtml.includes('font-size:200px')).toBe(true) + expect(browserHtml.includes('font-size:100px')).toBe(false) - await check( - () => getBrowserBodyText(browser), - /This page could not be found/ + const editedHtml = await renderViaHTTP( + next.url, + basePath + '/hmr/style-dynamic-component' ) + expect(editedHtml.includes('200px')).toBeTruthy() + const $editedHtml = cheerio.load(editedHtml) + const editedServerClassName = + $editedHtml('#dynamic-component').attr('class') - expect(next.cliOutput).toContain('Compiled /_error') - } catch (err) { - await next.deleteFile(newPage) - throw err + expect(editedClientClassName === editedServerClassName).toBe(true) } finally { + // Finally is used so that we revert the content back to the original regardless of the test outcome + // restore the about page content. + await next.patchFile(pagePath, originalContent) + if (browser) { await browser.close() } + + if (secondBrowser) { + secondBrowser.close() + } } }) + }) + }) - it('should recover from 404 after a page has been added with dynamic segments', async () => { - let browser - const newPage = join('pages', 'hmr', '[foo]', 'page.js') + describe('Error Recovery', () => { + it('should recover from 404 after a page has been added', async () => { + let browser + const newPage = join('pages', 'hmr', 'new-page.js') - try { - browser = await webdriver(next.url, basePath + '/hmr/foo/page') + try { + browser = await webdriver(next.url, basePath + '/hmr/new-page') - expect(await browser.elementByCss('body').text()).toMatch( - /This page could not be found/ - ) + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) - // Add the page - await next.patchFile( - newPage, - 'export default () => (
the-new-page
)' - ) + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) - await check(() => getBrowserBodyText(browser), /the-new-page/) + await check(() => getBrowserBodyText(browser), /the-new-page/) - await next.deleteFile(newPage) + await next.deleteFile(newPage) - await check( - () => getBrowserBodyText(browser), - /This page could not be found/ - ) + await check( + () => getBrowserBodyText(browser), + /This page could not be found/ + ) - expect(next.cliOutput).toContain('Compiled /_error') - } catch (err) { - await next.deleteFile(newPage) - throw err - } finally { - if (browser) { - await browser.close() - } + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should not continously poll a custom error page', async () => { - const errorPage = join('pages', '_error.js') + it('should recover from 404 after a page has been added with dynamic segments', async () => { + let browser + const newPage = join('pages', 'hmr', '[foo]', 'page.js') + + try { + browser = await webdriver(next.url, basePath + '/hmr/foo/page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + // Add the page await next.patchFile( - errorPage, - outdent` + newPage, + 'export default () => (
the-new-page
)' + ) + + await check(() => getBrowserBodyText(browser), /the-new-page/) + + await next.deleteFile(newPage) + + await check( + () => getBrowserBodyText(browser), + /This page could not be found/ + ) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should not continously poll a custom error page', async () => { + const errorPage = join('pages', '_error.js') + + await next.patchFile( + errorPage, + outdent` function Error({ statusCode, message, count }) { return (
@@ -459,46 +450,46 @@ describe.each([[''], ['/docs']])( export default Error ` - ) + ) - try { - // navigate to a 404 page - await webdriver(next.url, basePath + '/does-not-exist') + try { + // navigate to a 404 page + await webdriver(next.url, basePath + '/does-not-exist') - await check(() => next.cliOutput, /getInitialProps called/) + await check(() => next.cliOutput, /getInitialProps called/) - const outputIndex = next.cliOutput.length + const outputIndex = next.cliOutput.length - // wait a few seconds to ensure polling didn't happen - await waitFor(3000) + // wait a few seconds to ensure polling didn't happen + await waitFor(3000) - const logOccurrences = - next.cliOutput.slice(outputIndex).split('getInitialProps called') - .length - 1 - expect(logOccurrences).toBe(0) - } finally { - await next.deleteFile(errorPage) - } - }) + const logOccurrences = + next.cliOutput.slice(outputIndex).split('getInitialProps called') + .length - 1 + expect(logOccurrences).toBe(0) + } finally { + await next.deleteFile(errorPage) + } + }) - it('should detect syntax errors and recover', async () => { - const browser = await webdriver(next.url, basePath + '/hmr/about2') - const aboutPage = join('pages', 'hmr', 'about2.js') - const aboutContent = await next.readFile(aboutPage) - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) + it('should detect syntax errors and recover', async () => { + const browser = await webdriver(next.url, basePath + '/hmr/about2') + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) - await next.patchFile(aboutPage, aboutContent.replace('
', 'div')) + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) - expect(await hasRedbox(browser)).toBe(true) - const source = next.normalizeTestDirContent( - await getRedboxSource(browser) - ) - if (basePath === '' && !process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + expect(await hasRedbox(browser)).toBe(true) + const source = next.normalizeTestDirContent( + await getRedboxSource(browser) + ) + if (basePath === '' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? @@ -523,8 +514,8 @@ describe.each([[''], ['/docs']])( Import trace for requested module: ./pages/hmr/about2.js" `) - } else if (basePath === '' && process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + } else if (basePath === '' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js:7:1 Parsing ecmascript source code failed 5 | div @@ -535,8 +526,8 @@ describe.each([[''], ['/docs']])( Unexpected token. Did you mean \`{'}'}\` or \`}\`?" `) - } else if (basePath === '/docs' && !process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + } else if (basePath === '/docs' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? @@ -561,8 +552,8 @@ describe.each([[''], ['/docs']])( Import trace for requested module: ./pages/hmr/about2.js" `) - } else if (basePath === '/docs' && process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + } else if (basePath === '/docs' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js:7:1 Parsing ecmascript source code failed 5 | div @@ -573,321 +564,285 @@ describe.each([[''], ['/docs']])( Unexpected token. Did you mean \`{'}'}\` or \`}\`?" `) - } + } - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) }) + }) - if (!process.env.TURBOPACK) { - // Turbopack doesn't have this restriction - it('should show the error on all pages', async () => { - const aboutPage = join('pages', 'hmr', 'about2.js') - const aboutContent = await next.readFile(aboutPage) - let browser - try { - await renderViaHTTP(next.url, basePath + '/hmr/about2') + if (!process.env.TURBOPACK) { + // Turbopack doesn't have this restriction + it('should show the error on all pages', async () => { + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + let browser + try { + await renderViaHTTP(next.url, basePath + '/hmr/about2') - await next.patchFile( - aboutPage, - aboutContent.replace('', 'div') - ) + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) - // Ensure dev server has time to break: - await new Promise((resolve) => setTimeout(resolve, 2000)) + // Ensure dev server has time to break: + await new Promise((resolve) => setTimeout(resolve, 2000)) - browser = await webdriver(next.url, basePath + '/hmr/contact') + browser = await webdriver(next.url, basePath + '/hmr/contact') - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + await check( + () => getBrowserBodyText(browser), + /This is the contact page/ + ) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the contact page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the contact page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + throw err + } finally { + if (browser) { + await browser.close() } - }) - } + } + }) + } - it('should detect runtime errors on the module scope', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about3.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about3') - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + it('should detect runtime errors on the module scope', async () => { + let browser + const aboutPage = join('pages', 'hmr', 'about3.js') + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.url, basePath + '/hmr/about3') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace('export', 'aa=20;\nexport') - ) + await next.patchFile( + aboutPage, + aboutContent.replace('export', 'aa=20;\nexport') + ) - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } finally { - await next.patchFile(aboutPage, aboutContent) - if (browser) { - await browser.close() - } + await check(() => getBrowserBodyText(browser), /This is the about page/) + } finally { + await next.patchFile(aboutPage, aboutContent) + if (browser) { + await browser.close() } - }) + } + }) - it('should recover from errors in the render function', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about4.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about4') - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + it('should recover from errors in the render function', async () => { + let browser + const aboutPage = join('pages', 'hmr', 'about4.js') + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.url, basePath + '/hmr/about4') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'return', - 'throw new Error("an-expected-error");\nreturn' - ) + await next.patchFile( + aboutPage, + aboutContent.replace( + 'return', + 'throw new Error("an-expected-error");\nreturn' ) + ) - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + await check(() => getBrowserBodyText(browser), /This is the about page/) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should recover after exporting an invalid page', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about5.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about5') - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + it('should recover after exporting an invalid page', async () => { + let browser + const aboutPage = join('pages', 'hmr', 'about5.js') + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.url, basePath + '/hmr/about5') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default {};\nexport const fn =' - ) + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default {};\nexport const fn =' ) + ) - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: The default export is not a React Component in page: "/hmr/about5""` - ) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about5""` + ) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + await check(() => getBrowserBodyText(browser), /This is the about page/) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should recover after a bad return from the render function', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about6.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about6') - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + it('should recover after a bad return from the render function', async () => { + let browser + const aboutPage = join('pages', 'hmr', 'about6.js') + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.url, basePath + '/hmr/about6') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default () => /search/;\nexport const fn =' - ) + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default () => /search/;\nexport const fn =' ) + ) - expect(await hasRedbox(browser)).toBe(true) - // TODO: Replace this when webpack 5 is the default - expect(await getRedboxHeader(browser)).toMatch( - `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` - ) + expect(await hasRedbox(browser)).toBe(true) + // TODO: Replace this when webpack 5 is the default + expect(await getRedboxHeader(browser)).toMatch( + `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` + ) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should recover after undefined exported as default', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about7.js') + it('should recover after undefined exported as default', async () => { + let browser + const aboutPage = join('pages', 'hmr', 'about7.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about7') - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.url, basePath + '/hmr/about7') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default undefined;\nexport const fn =' - ) + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default undefined;\nexport const fn =' ) + ) - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: The default export is not a React Component in page: "/hmr/about7""` - ) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about7""` + ) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + expect(await hasRedbox(browser)).toBe(false) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - expect(await hasRedbox(browser)).toBe(false) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should recover after webpack parse error in an imported file', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about8.js') + it('should recover after webpack parse error in an imported file', async () => { + let browser + const aboutPage = join('pages', 'hmr', 'about8.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.appPort, basePath + '/hmr/about8') - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.appPort, basePath + '/hmr/about8') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'import "../../components/parse-error.xyz"\nexport default' - ) + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.xyz"\nexport default' ) + ) - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') - if (process.env.TURBOPACK) { - expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + if (process.env.TURBOPACK) { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` "./components/parse-error.xyz Unknown module type This module doesn't have an associated type. Use a known file extension, or register a loader for it. Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders" `) - } else { - expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + } else { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` "./components/parse-error.xyz Module parse failed: Unexpected token (3:0) You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders @@ -901,334 +856,319 @@ describe.each([[''], ['/docs']])( ./components/parse-error.xyz ./pages/hmr/about8.js" `) - } - await next.patchFile(aboutPage, aboutContent) + } + await next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + expect(await hasRedbox(browser)).toBe(false) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - expect(await hasRedbox(browser)).toBe(false) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should recover after loader parse error in an imported file', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about9.js') + // assertion is highly flakey + it.skip('should recover after loader parse error in an imported file', async () => { + let browser + const aboutPage = join('pages', 'hmr', 'about9.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.appPort, basePath + '/hmr/about9') - await check( - () => getBrowserBodyText(browser), - /This is the about page/ + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.appPort, basePath + '/hmr/about9') + await check(() => getBrowserBodyText(browser), /This is the about page/) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.js"\nexport default' ) + ) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'import "../../components/parse-error.js"\nexport default' - ) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + let redboxSource = await getRedboxSource(browser) + + redboxSource = redboxSource.replace(`${next.testDir}`, '.') + if (process.env.TURBOPACK) { + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js:3:1 + Parsing ecmascript source code failed + 1 | This + 2 | is + > 3 | }}} + | ^ + 4 | invalid + 5 | js + + Expression expected" + `) + } else { + redboxSource = redboxSource.substring( + 0, + redboxSource.indexOf('`----') ) - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxHeader(browser)).toMatch('Failed to compile') - let redboxSource = await getRedboxSource(browser) - - redboxSource = redboxSource.replace(`${next.testDir}`, '.') - if (process.env.TURBOPACK) { - expect(next.normalizeTestDirContent(redboxSource)) - .toMatchInlineSnapshot(` - "./components/parse-error.js:3:1 - Parsing ecmascript source code failed + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js + Error: + x Expression expected + ,-[./components/parse-error.js:1:1] 1 | This 2 | is - > 3 | }}} - | ^ + 3 | }}} + : ^ 4 | invalid 5 | js + " + `) + } - Expression expected" - `) - } else { - redboxSource = redboxSource.substring( - 0, - redboxSource.indexOf('`----') - ) - - expect(next.normalizeTestDirContent(redboxSource)) - .toMatchInlineSnapshot(` - "./components/parse-error.js - Error: - x Expression expected - ,-[./components/parse-error.js:1:1] - 1 | This - 2 | is - 3 | }}} - : ^ - 4 | invalid - 5 | js - " - `) - } + await next.patchFile(aboutPage, aboutContent) - await next.patchFile(aboutPage, aboutContent) + await check(() => getBrowserBodyText(browser), /This is the about page/) + expect(await hasRedbox(browser)).toBe(false) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - expect(await hasRedbox(browser)).toBe(false) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } - } finally { - if (browser) { - await browser.close() - } } - }) - - it('should recover from errors in getInitialProps in client', async () => { - let browser - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - const errorContent = await next.readFile(erroredPage) - try { - browser = await webdriver(next.url, basePath + '/hmr') - await browser.elementByCss('#error-in-gip-link').click() + } finally { + if (browser) { + await browser.close() + } + } + }) - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: an-expected-error-in-gip"` - ) + it('should recover from errors in getInitialProps in client', async () => { + let browser + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + browser = await webdriver(next.url, basePath + '/hmr') + await browser.elementByCss('#error-in-gip-link').click() - await next.patchFile( - erroredPage, - errorContent.replace('throw error', 'return {}') - ) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) - await check(() => getBrowserBodyText(browser), /Hello/) + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) - await next.patchFile(erroredPage, errorContent) + await check(() => getBrowserBodyText(browser), /Hello/) - await check(async () => { - await browser.refresh() - await waitFor(2000) - const text = await getBrowserBodyText(browser) - if (text.includes('Hello')) { - throw new Error('waiting') - } - return getRedboxSource(browser) - }, /an-expected-error-in-gip/) - } catch (err) { - await next.patchFile(erroredPage, errorContent) + await next.patchFile(erroredPage, errorContent) - throw err - } finally { - if (browser) { - await browser.close() + await check(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') } + return getRedboxSource(browser) + }, /an-expected-error-in-gip/) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should recover after an error reported via SSR', async () => { - let browser - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - const errorContent = await next.readFile(erroredPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/error-in-gip') + it('should recover after an error reported via SSR', async () => { + let browser + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + browser = await webdriver(next.url, basePath + '/hmr/error-in-gip') - expect(await hasRedbox(browser)).toBe(true) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: an-expected-error-in-gip"` - ) + expect(await hasRedbox(browser)).toBe(true) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - await next.patchFile( - erroredPage, - errorContent.replace('throw error', 'return {}') - ) + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) - await check(() => getBrowserBodyText(browser), /Hello/) + await check(() => getBrowserBodyText(browser), /Hello/) - await next.patchFile(erroredPage, errorContent) + await next.patchFile(erroredPage, errorContent) - await check(async () => { - await browser.refresh() - await waitFor(2000) - const text = await getBrowserBodyText(browser) - if (text.includes('Hello')) { - throw new Error('waiting') - } - return getRedboxSource(browser) - }, /an-expected-error-in-gip/) - } catch (err) { - await next.patchFile(erroredPage, errorContent) - - throw err - } finally { - if (browser) { - await browser.close() + await check(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') } + return getRedboxSource(browser) + }, /an-expected-error-in-gip/) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } finally { + if (browser) { + await browser.close() } - }) + } }) + }) - describe('Full reload', () => { - it('should warn about full reload in cli output - anonymous page function', async () => { - const start = next.cliOutput.length - const browser = await webdriver( - next.url, - basePath + '/hmr/anonymous-page-function' - ) - const cliWarning = - 'Fast Refresh had to perform a full reload when ./pages/hmr/anonymous-page-function.js changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload' + describe('Full reload', () => { + it('should warn about full reload in cli output - anonymous page function', async () => { + const start = next.cliOutput.length + const browser = await webdriver( + next.url, + basePath + '/hmr/anonymous-page-function' + ) + const cliWarning = + 'Fast Refresh had to perform a full reload when ./pages/hmr/anonymous-page-function.js changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload' - expect(await browser.elementByCss('p').text()).toBe('hello world') - expect(next.cliOutput.slice(start)).not.toContain(cliWarning) + expect(await browser.elementByCss('p').text()).toBe('hello world') + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) - const currentFileContent = await next.readFile( - './pages/hmr/anonymous-page-function.js' - ) - const newFileContent = currentFileContent.replace( - '

hello world

', - '

hello world!!!

' - ) - await next.patchFile( - './pages/hmr/anonymous-page-function.js', - newFileContent - ) + const currentFileContent = await next.readFile( + './pages/hmr/anonymous-page-function.js' + ) + const newFileContent = currentFileContent.replace( + '

hello world

', + '

hello world!!!

' + ) + await next.patchFile( + './pages/hmr/anonymous-page-function.js', + newFileContent + ) - expect(await browser.waitForElementByCss('#updated').text()).toBe( - 'hello world!!!' - ) + expect(await browser.waitForElementByCss('#updated').text()).toBe( + 'hello world!!!' + ) - // CLI warning - expect(next.cliOutput.slice(start)).toContain(cliWarning) + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) - // Browser warning - const browserLogs = await browser.log() - expect( - browserLogs.some(({ message }) => - message.includes( - "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree." - ) + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree." ) - ).toBeTruthy() - }) - - it('should warn about full reload in cli output - runtime-error', async () => { - const start = next.cliOutput.length - const browser = await webdriver( - next.url, - basePath + '/hmr/runtime-error' ) - const cliWarning = - 'Fast Refresh had to perform a full reload due to a runtime error.' - - await check( - () => getRedboxHeader(browser), - /ReferenceError: whoops is not defined/ - ) - expect(next.cliOutput.slice(start)).not.toContain(cliWarning) - - const currentFileContent = await next.readFile( - './pages/hmr/runtime-error.js' - ) - const newFileContent = currentFileContent.replace( - 'whoops', - '

whoops

' - ) - await next.patchFile('./pages/hmr/runtime-error.js', newFileContent) + ).toBeTruthy() + }) - expect(await browser.waitForElementByCss('#updated').text()).toBe( - 'whoops' - ) + it('should warn about full reload in cli output - runtime-error', async () => { + const start = next.cliOutput.length + const browser = await webdriver(next.url, basePath + '/hmr/runtime-error') + const cliWarning = + 'Fast Refresh had to perform a full reload due to a runtime error.' - // CLI warning - expect(next.cliOutput.slice(start)).toContain(cliWarning) + await check( + () => getRedboxHeader(browser), + /ReferenceError: whoops is not defined/ + ) + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) - // Browser warning - const browserLogs = await browser.log() - expect( - browserLogs.some(({ message }) => - message.includes( - '[Fast Refresh] performing full reload because your application had an unrecoverable error' - ) - ) - ).toBeTruthy() - }) - }) + const currentFileContent = await next.readFile( + './pages/hmr/runtime-error.js' + ) + const newFileContent = currentFileContent.replace( + 'whoops', + '

whoops

' + ) + await next.patchFile('./pages/hmr/runtime-error.js', newFileContent) - if (!process.env.TURBOPACK) { - it('should have client HMR events in trace file', async () => { - const traceData = await next.readFile('.next/trace') - expect(traceData).toContain('client-hmr-latency') - expect(traceData).toContain('client-error') - expect(traceData).toContain('client-success') - expect(traceData).toContain('client-full-reload') - }) - } + expect(await browser.waitForElementByCss('#updated').text()).toBe( + 'whoops' + ) - it('should have correct compile timing after fixing error', async () => { - const pageName = 'pages/auto-export-is-ready.js' - const originalContent = await next.readFile(pageName) + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) - try { - const browser = await webdriver( - next.url, - basePath + '/auto-export-is-ready' - ) - const outputLength = next.cliOutput.length - await next.patchFile( - pageName, - `import hello from 'non-existent'\n` + originalContent - ) - expect(await hasRedbox(browser)).toBe(true) - await waitFor(3000) - await next.patchFile(pageName, originalContent) - await check( - () => next.cliOutput.substring(outputLength), - /Compiled.*?/i + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + '[Fast Refresh] performing full reload because your application had an unrecoverable error' + ) ) - const compileTimeStr = next.cliOutput.substring(outputLength) + ).toBeTruthy() + }) + }) + + if (!process.env.TURBOPACK) { + it('should have client HMR events in trace file', async () => { + const traceData = await next.readFile('.next/trace') + expect(traceData).toContain('client-hmr-latency') + expect(traceData).toContain('client-error') + expect(traceData).toContain('client-success') + expect(traceData).toContain('client-full-reload') + }) + } - const matches = [ - ...compileTimeStr.match(/Compiled.*? in ([\d.]{1,})\s?(?:s|ms)/i), - ] - const [, compileTime, timeUnit] = matches + it('should have correct compile timing after fixing error', async () => { + const pageName = 'pages/auto-export-is-ready.js' + const originalContent = await next.readFile(pageName) - let compileTimeMs = parseFloat(compileTime) - if (timeUnit === 's') { - compileTimeMs = compileTimeMs * 1000 - } - expect(compileTimeMs).toBeLessThan(3000) - } finally { - await next.patchFile(pageName, originalContent) + try { + const browser = await webdriver( + next.url, + basePath + '/auto-export-is-ready' + ) + const outputLength = next.cliOutput.length + await next.patchFile( + pageName, + `import hello from 'non-existent'\n` + originalContent + ) + expect(await hasRedbox(browser)).toBe(true) + await waitFor(3000) + await next.patchFile(pageName, originalContent) + await check(() => next.cliOutput.substring(outputLength), /Compiled.*?/i) + const compileTimeStr = next.cliOutput.substring(outputLength) + + const matches = [ + ...compileTimeStr.match(/Compiled.*? in ([\d.]{1,})\s?(?:s|ms)/i), + ] + const [, compileTime, timeUnit] = matches + + let compileTimeMs = parseFloat(compileTime) + if (timeUnit === 's') { + compileTimeMs = compileTimeMs * 1000 } - }) - } -) + expect(compileTimeMs).toBeLessThan(3000) + } finally { + await next.patchFile(pageName, originalContent) + } + }) +}) diff --git a/test/development/basic/misc/public/image.png b/test/development/basic/misc/public/image.png new file mode 100644 index 0000000000000..7cbc1d2673361 Binary files /dev/null and b/test/development/basic/misc/public/image.png differ 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/development/basic/node-builtins/app/server-component/page.js b/test/development/basic/node-builtins/app/server-component/page.js index a995bd45e81f4..688377107ffa6 100644 --- a/test/development/basic/node-builtins/app/server-component/page.js +++ b/test/development/basic/node-builtins/app/server-component/page.js @@ -18,6 +18,7 @@ import timers from 'timers' import tty from 'tty' import util from 'util' import zlib from 'zlib' + import 'setimmediate' async function getData() { diff --git a/test/development/correct-tsconfig-defaults/index.test.ts b/test/development/correct-tsconfig-defaults/index.test.ts index 0c4e250fd2cbe..9f73eae59c449 100644 --- a/test/development/correct-tsconfig-defaults/index.test.ts +++ b/test/development/correct-tsconfig-defaults/index.test.ts @@ -13,7 +13,7 @@ describe('correct tsconfig.json defaults', () => { skipStart: true, dependencies: { typescript: 'latest', - '@types/react': 'latest', + '@types/react': '^18.2.0', '@types/node': 'latest', }, }) diff --git a/test/development/jsconfig-path-reloading/index.test.ts b/test/development/jsconfig-path-reloading/index.test.ts index 70a39854be71a..ff40cca9d28be 100644 --- a/test/development/jsconfig-path-reloading/index.test.ts +++ b/test/development/jsconfig-path-reloading/index.test.ts @@ -36,7 +36,7 @@ describe('jsconfig-path-reloading', () => { }, dependencies: { typescript: 'latest', - '@types/react': 'latest', + '@types/react': '^18.2.0', '@types/node': 'latest', }, }) diff --git a/test/development/next-font/deprecated-package.test.ts b/test/development/next-font/deprecated-package.test.ts index 7b9b3fdb3fe37..56211cb3714ca 100644 --- a/test/development/next-font/deprecated-package.test.ts +++ b/test/development/next-font/deprecated-package.test.ts @@ -9,8 +9,8 @@ createNextDescribe( 'pages/index.js': '', }, dependencies: { - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', '@next/font': 'canary', }, skipStart: true, diff --git a/test/development/tsconfig-path-reloading/index.test.ts b/test/development/tsconfig-path-reloading/index.test.ts index dab32fabe3f38..1936dcd99a991 100644 --- a/test/development/tsconfig-path-reloading/index.test.ts +++ b/test/development/tsconfig-path-reloading/index.test.ts @@ -36,7 +36,7 @@ describe('tsconfig-path-reloading', () => { }, dependencies: { typescript: 'latest', - '@types/react': 'latest', + '@types/react': '^18.2.0', '@types/node': 'latest', }, }) diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts index e360c003d0dae..c4db9571dbe8a 100644 --- a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts @@ -8,8 +8,8 @@ createNextDescribe( files: join(__dirname, 'safe-origins'), skipDeployment: true, dependencies: { - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', 'server-only': 'latest', }, // An arbitrary & random port. diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-disallowed-origins.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-disallowed-origins.test.ts index 5a4145d0be118..6e569e574a807 100644 --- a/test/e2e/app-dir/actions-allowed-origins/app-action-disallowed-origins.test.ts +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-disallowed-origins.test.ts @@ -8,8 +8,8 @@ createNextDescribe( files: join(__dirname, 'unsafe-origins'), skipDeployment: true, dependencies: { - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', 'server-only': 'latest', }, }, diff --git a/test/e2e/app-dir/actions/app-action-export.test.ts b/test/e2e/app-dir/actions/app-action-export.test.ts index 565efac0a945d..5d1bd81ee850a 100644 --- a/test/e2e/app-dir/actions/app-action-export.test.ts +++ b/test/e2e/app-dir/actions/app-action-export.test.ts @@ -7,8 +7,8 @@ createNextDescribe( skipStart: true, skipDeployment: true, dependencies: { - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', 'server-only': 'latest', }, }, diff --git a/test/e2e/app-dir/actions/app-action-form-state.test.ts b/test/e2e/app-dir/actions/app-action-form-state.test.ts index 400a47eef33f1..97aa41ca0ea75 100644 --- a/test/e2e/app-dir/actions/app-action-form-state.test.ts +++ b/test/e2e/app-dir/actions/app-action-form-state.test.ts @@ -7,8 +7,8 @@ createNextDescribe( { files: __dirname, dependencies: { - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', }, }, ({ next }) => { diff --git a/test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts b/test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts index d07b0b5a9d684..73d45f6f2397f 100644 --- a/test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts +++ b/test/e2e/app-dir/actions/app-action-progressive-enhancement.test.ts @@ -8,9 +8,9 @@ createNextDescribe( { files: __dirname, dependencies: { - react: 'latest', + react: '^18.2.0', nanoid: 'latest', - 'react-dom': 'latest', + 'react-dom': '^18.2.0', 'server-only': 'latest', }, }, diff --git a/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts b/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts index 9614d3beb8132..8299e04702857 100644 --- a/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts +++ b/test/e2e/app-dir/actions/app-action-size-limit-invalid.test.ts @@ -10,8 +10,8 @@ createNextDescribe( files: __dirname, skipDeployment: true, dependencies: { - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', 'server-only': 'latest', }, }, diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 603f44931a0e4..9771269069283 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -19,9 +19,9 @@ createNextDescribe( { files: __dirname, dependencies: { - react: 'latest', + react: '^18.2.0', nanoid: 'latest', - 'react-dom': 'latest', + 'react-dom': '^18.2.0', 'server-only': 'latest', }, }, diff --git a/test/e2e/app-dir/app-css-pageextensions/index.test.ts b/test/e2e/app-dir/app-css-pageextensions/index.test.ts index a2cb488426ae8..d5796d92e0861 100644 --- a/test/e2e/app-dir/app-css-pageextensions/index.test.ts +++ b/test/e2e/app-dir/app-css-pageextensions/index.test.ts @@ -7,8 +7,8 @@ createNextDescribe( skipDeployment: true, dependencies: { '@picocss/pico': '1.5.7', - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', sass: 'latest', }, }, diff --git a/test/e2e/app-dir/app-css/index.test.ts b/test/e2e/app-dir/app-css/index.test.ts index 1e65901051977..f1d018c28be88 100644 --- a/test/e2e/app-dir/app-css/index.test.ts +++ b/test/e2e/app-dir/app-css/index.test.ts @@ -8,8 +8,8 @@ createNextDescribe( skipDeployment: true, dependencies: { '@picocss/pico': '1.5.7', - react: 'latest', - 'react-dom': 'latest', + react: '^18.2.0', + 'react-dom': '^18.2.0', sass: 'latest', '@next/mdx': 'canary', }, 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-fetch-deduping/app-fetch-deduping.test.ts b/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts index 23c54b12787d1..73c5a24f014a3 100644 --- a/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts +++ b/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts @@ -1,4 +1,4 @@ -import { findPort, waitFor } from 'next-test-utils' +import { findPort, retry } from 'next-test-utils' import http from 'http' import { outdent } from 'outdent' import { isNextDev, isNextStart, nextTestSetup } from 'e2e-utils' @@ -104,12 +104,10 @@ describe('app-fetch-deduping', () => { expect(invocation(next.cliOutput)).toBe(1) - // wait for the revalidation to finish - await waitFor(revalidate * 1000 + 1000) - - await next.render('/test') - - expect(invocation(next.cliOutput)).toBe(2) + await retry(async () => { + await next.render('/test') + expect(invocation(next.cliOutput)).toBe(2) + }, 10_000) }) }) } else { 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..8d9a64f0f9549 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' @@ -23,6 +23,10 @@ createNextDescribe( }, /app-dir/) }) + it('should not include any warnings about using Node.js APIs', async () => { + expect(next.cliOutput).not.toContain('A Node.js module is loaded') + }) + describe.each([ { title: 'Serverless Functions', @@ -134,6 +138,108 @@ 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 omit internal headers for middleware cookies', async () => { + const response = await next.fetch('/rsc-cookies/cookie-options') + expect(response.status).toBe(200) + expect(response.headers.get('x-middleware-set-cookie')).toBeNull() + + const response2 = await next.fetch('/cookies/api') + expect(response2.status).toBe(200) + expect(response2.headers.get('x-middleware-set-cookie')).toBeNull() + expect(response2.headers.get('set-cookie')).toBeDefined() + expect(response2.headers.get('set-cookie')).toContain('example') + }) + + 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/cookies/api/route.js b/test/e2e/app-dir/app-middleware/app/cookies/api/route.js new file mode 100644 index 0000000000000..598c70f384dac --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/cookies/api/route.js @@ -0,0 +1,11 @@ +import { NextResponse } from 'next/server' + +export function GET() { + const response = new NextResponse() + response.cookies.set({ + name: 'example', + value: 'example', + }) + + return response +} diff --git a/test/e2e/app-dir/app-middleware/app/cookies/page.js b/test/e2e/app-dir/app-middleware/app/cookies/page.js new file mode 100644 index 0000000000000..cdcfe3addce7f --- /dev/null +++ b/test/e2e/app-dir/app-middleware/app/cookies/page.js @@ -0,0 +1,6 @@ +import { cookies } from 'next/headers' + +export default async function Page() { + const cookieLength = (await cookies()).size + return
cookies: {cookieLength}
+} 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-prefetch/app/page.js b/test/e2e/app-dir/app-prefetch/app/page.js index 17c9aeae53c0b..ccded0f324b50 100644 --- a/test/e2e/app-dir/app-prefetch/app/page.js +++ b/test/e2e/app-dir/app-prefetch/app/page.js @@ -8,6 +8,9 @@ export default function HomePage() { To Static Page + + To Dynamic Slug Page + ) } diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js index a9123d41fc185..f404ca3e0a91f 100644 --- a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js @@ -2,7 +2,18 @@ import Link from 'next/link' export const dynamic = 'force-dynamic' +function getData() { + const res = new Promise((resolve) => { + setTimeout(() => { + resolve({ message: 'Layout Data!' }) + }, 2000) + }) + return res +} + export default async function Layout({ children }) { + const result = await getData() + return (

Layout

@@ -10,6 +21,7 @@ export default async function Layout({ children }) { Prefetch Link {children} +

{JSON.stringify(result)}

) } diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js index 5b91c2379fa9c..9105a53b67e8f 100644 --- a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js @@ -1,3 +1,3 @@ export default function Loading() { - return

Loading Prefetch Auto

+ return

Loading Prefetch Auto

} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js index b6aa1a5033af3..23c8bdc9b5cb1 100644 --- a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js @@ -3,7 +3,7 @@ export const dynamic = 'force-dynamic' function getData() { const res = new Promise((resolve) => { setTimeout(() => { - resolve({ message: 'Hello World!' }) + resolve({ message: 'Page Data!' }) }, 2000) }) return res @@ -13,9 +13,9 @@ export default async function Page({ params }) { const result = await getData() return ( - <> +

{JSON.stringify(params)}

{JSON.stringify(result)}

- +
) } diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index df83f886da1a1..6fabca7170f61 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -263,7 +263,8 @@ createNextDescribe( }) const prefetchResponse = await response.text() - expect(prefetchResponse).not.toContain('Hello World') + expect(prefetchResponse).toContain('Page Data') + expect(prefetchResponse).not.toContain('Layout Data!') expect(prefetchResponse).not.toContain('Loading Prefetch Auto') }) @@ -297,10 +298,23 @@ createNextDescribe( }) const prefetchResponse = await response.text() - expect(prefetchResponse).not.toContain('Hello World') + expect(prefetchResponse).not.toContain('Page Data!') expect(prefetchResponse).toContain('Loading Prefetch Auto') }) + it('should immediately render the loading state for a dynamic segment when fetched from higher up in the tree', async () => { + const browser = await next.browser('/') + const loadingText = await browser + .elementById('to-dynamic-page') + .click() + .waitForElementByCss('#loading-text') + .text() + + expect(loadingText).toBe('Loading Prefetch Auto') + + await browser.waitForElementByCss('#prefetch-auto-page-data') + }) + describe('dynamic rendering', () => { describe.each(['/force-dynamic', '/revalidate-0'])('%s', (basePath) => { it('should not re-render layout when navigating between sub-pages', async () => { 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-routes-subrequests/app-routes-subrequests.test.ts b/test/e2e/app-dir/app-routes-subrequests/app-routes-subrequests.test.ts deleted file mode 100644 index 13482c7d488f0..0000000000000 --- a/test/e2e/app-dir/app-routes-subrequests/app-routes-subrequests.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createNextDescribe } from 'e2e-utils' - -const bathPath = process.env.BASE_PATH ?? '' - -createNextDescribe( - 'app-routes-subrequests', - { - files: __dirname, - skipDeployment: true, - }, - ({ next }) => { - it('shortcuts after 5 subrequests', async () => { - expect(JSON.parse(await next.render(bathPath + '/'))).toEqual({ - count: 5, - }) - }) - } -) diff --git a/test/e2e/app-dir/app-routes-subrequests/app/route.ts b/test/e2e/app-dir/app-routes-subrequests/app/route.ts deleted file mode 100644 index f083b407ad2cb..0000000000000 --- a/test/e2e/app-dir/app-routes-subrequests/app/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -export const runtime = 'edge' - -let count = 0 - -export const GET = async (req: NextRequest) => { - await fetch(req.nextUrl) - count++ - return NextResponse.json({ count }) -} diff --git a/test/e2e/app-dir/app-routes-subrequests/next.config.js b/test/e2e/app-dir/app-routes-subrequests/next.config.js deleted file mode 100644 index d54bad4c24cbe..0000000000000 --- a/test/e2e/app-dir/app-routes-subrequests/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @type {import('next').NextConfig} - */ -const config = { - typescript: { - ignoreBuildErrors: true, - }, -} - -module.exports = config 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..73eee83e78bc6 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-129') }) + }) - 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-static/app/too-many-cache-tags/page.tsx b/test/e2e/app-dir/app-static/app/too-many-cache-tags/page.tsx index 086813a91309a..2664adb45081f 100644 --- a/test/e2e/app-dir/app-static/app/too-many-cache-tags/page.tsx +++ b/test/e2e/app-dir/app-static/app/too-many-cache-tags/page.tsx @@ -3,7 +3,7 @@ export const revalidate = 0 export default async function Page() { const tags: string[] = [] - for (let i = 0; i < 96; i++) { + for (let i = 0; i < 130; i++) { tags.push(`tag-${i}`) } const data = await fetch( diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 9976271b0054b..e70e36758c4a0 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,1748 +23,1740 @@ 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 (isNextStart) { + it('should have correct cache-control for SSR routes', async () => { + for (const path of ['/catch-all/first', '/ssr']) { + const res = await next.fetch(path) + expect(res.status).toBe(200) + expect(res.headers.get('Cache-Control')).toBe( + 'private, no-cache, no-store, max-age=0, must-revalidate' ) - }) - - 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/ - ) - }) } + }) + } - 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 (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) - expect(middlewareManifest.functions['/test-page/page'].regions).toEqual( - ['home'] - ) + const $ = cheerio.load(await res.text()) + expect(JSON.parse($('#query').text())).toEqual({ hello: 'world' }) + }) + } - // Inherits from the root layout. - expect( - middlewareManifest.functions['/slow-page-with-loading/page'].regions - ).toEqual(['sfo1']) + 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 have correct size in build output', async () => { + expect(next.cliOutput).toMatch( + /\/dashboard\/another.*? *?[^0]\d{1,} [\w]{1,}B/ + ) }) } - 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') - 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') + await browser.elementByCss('a').click() + await browser.waitForElementByCss('#from-dashboard') + }) + } - 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 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()) + }) - it('should not apply client router filter on shallow', async () => { - const browser = await next.browser('/') - await browser.eval('window.beforeNav = 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 () => { - 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) - }) + const href = await browser.eval('location.href') + return href.includes('example.vercel.sh') ? 'yes' : href + }, 'yes') - 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 (pathname.includes('/blog')) { + browser = await next.browser('/blog/first') + await browser.eval('window.beforeNav = 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(() => {}) - - 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' - ) - }) + // 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) + } + } } + ) - 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 not apply client router filter on shallow', async () => { + const browser = await next.browser('/') + await browser.eval('window.beforeNav = 1') - 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') + 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) }) + } - 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') - }) + 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(() => {}) - 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' - ) - }) + await waitFor(1000) + controller1.abort() - 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' + 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' ) }) + } - it('should pass props from getServerSideProps in root layout', async () => { - const $ = await next.render$('/dashboard') - expect($('title').first().text()).toBe('hello world') + 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 serve from pages', async () => { - const html = await next.render('/') - expect(html).toContain('hello from pages/index') + it('should use text/x-component for flight', async () => { + const res = await next.fetch('/dashboard/deployments/123', { + headers: { + ['RSC'.toString()]: '1', + }, }) - - it('should serve dynamic route from pages', async () => { - const html = await next.render('/blog/first') - expect(html).toContain('hello from pages/blog/[slug]') + 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 serve from public', async () => { - const html = await next.render('/hello.txt') - expect(html).toContain('hello world') + it('should return the `vary` header from pages for flight requests', async () => { + const res = await next.fetch('/', { + headers: { + ['RSC'.toString()]: '1', + }, }) - - it('should serve from app', async () => { - const html = await next.render('/dashboard') - expect(html).toContain('hello from app/dashboard') + 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) + }) + 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 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) + it('should serve polyfills for browsers that do not support modules', async () => { + const html = await next.render('/dashboard/index') + expect(html).toMatch( + /