diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 125155c9edac..000000000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -dist -node_modules -schema -**/*.tmpl.* -sw.js diff --git a/.eslintrc b/.eslintrc index a6d2bd213026..8cc17845b553 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,13 +1,17 @@ { + "ignorePatterns": [ + "dist", + "node_modules", + "packages/schema/schema", + "**/*.tmpl.*", + "sw.js" + ], "$schema": "https://json.schemastore.org/eslintrc", "globals": { "NodeJS": true, "$fetch": true }, - "plugins": [ - "jsdoc", - "no-only-tests" - ], + "plugins": ["jsdoc", "no-only-tests"], "extends": [ "plugin:jsdoc/recommended", "@nuxtjs/eslint-config-typescript", @@ -96,10 +100,28 @@ "ignoreRestSiblings": true } ], - "jsdoc/check-tag-names": ["error", { - "definedTags": ["__NO_SIDE_EFFECTS__"] - }] + "jsdoc/check-tag-names": [ + "error", + { + "definedTags": ["__NO_SIDE_EFFECTS__"] + } + ] }, + "overrides": [ + { + "files": ["packages/schema/**"], + "rules": { + "jsdoc/no-undefined-types": "off", + "jsdoc/valid-types": "off", + "jsdoc/check-tag-names": [ + "error", + { + "definedTags": ["experimental"] + } + ] + } + } + ], "settings": { "jsdoc": { "ignoreInternal": true, diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index bbea7f97464b..adb2612a93f5 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -10,7 +10,7 @@ body: Please use a template below to create a minimal reproduction 👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz - 👉 https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox + 👉 https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox - type: textarea id: bug-env attributes: diff --git a/.github/ISSUE_TEMPLATE/z-bug-report-2.yml b/.github/ISSUE_TEMPLATE/z-bug-report-2.yml index ff8132287948..d4124367b22c 100644 --- a/.github/ISSUE_TEMPLATE/z-bug-report-2.yml +++ b/.github/ISSUE_TEMPLATE/z-bug-report-2.yml @@ -10,7 +10,7 @@ body: Please use a template below to create a minimal reproduction 👉 https://stackblitz.com/github/nuxt/starter/tree/v2 - 👉 https://codesandbox.io/p/github/nuxt/starter/v2 + 👉 https://codesandbox.io/s/github/nuxt/starter/v2 - type: textarea id: bug-env attributes: diff --git a/.github/assets/twitter.svg b/.github/assets/twitter.svg index 4940c9010112..00212abdc636 100644 --- a/.github/assets/twitter.svg +++ b/.github/assets/twitter.svg @@ -1 +1 @@ - + diff --git a/.github/reproduire/needs-reproduction.md b/.github/reproduire/needs-reproduction.md new file mode 100644 index 000000000000..7b260b818593 --- /dev/null +++ b/.github/reproduire/needs-reproduction.md @@ -0,0 +1,32 @@ +Would you be able to provide a [reproduction](https://nuxt.com/docs/community/reporting-bugs/#create-a-minimal-reproduction)? 🙏 + +
+More info + +### Why do I need to provide a reproduction? + +Reproductions make it possible for us to triage and fix issues quickly with a relatively small team. It helps us discover the source of the problem, and also can reveal assumptions you or we might be making. + +### What will happen? + +If you've provided a reproduction, we'll remove the label and try to reproduce the issue. If we can, we'll mark it as a bug and prioritise it based on its severity and how many people we think it might affect. + +If `needs reproduction` labeled issues don't receive any substantial activity (e.g., new comments featuring a reproduction link), we'll close them. That's not because we don't care! At any point, feel free to comment with a reproduction and we'll reopen it. + +### How can I create a reproduction? + +We have a couple of templates for starting with a minimal reproduction: + +👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz +👉 https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox + +A public GitHub repository is also perfect. 👌 + +Please ensure that the reproduction is as **minimal** as possible. See more details [in our guide](https://nuxt.com/docs/community/reporting-bugs/#create-a-minimal-reproduction). + +You might also find these other articles interesting and/or helpful: + +- [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required) +- [How to Generate a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve) + +
diff --git a/.github/workflows/autofix-docs.yml b/.github/workflows/autofix-docs.yml index 9673c5ed8dc0..2e16eabe9ec6 100644 --- a/.github/workflows/autofix-docs.yml +++ b/.github/workflows/autofix-docs.yml @@ -17,9 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 654f9b251676..432cde27aa68 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" diff --git a/.github/workflows/changelogensets.yml b/.github/workflows/changelogensets.yml index 2e20f61f1304..3ebb3ee07e45 100644 --- a/.github/workflows/changelogensets.yml +++ b/.github/workflows/changelogensets.yml @@ -19,11 +19,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: fetch-depth: 0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76db0d7b9589..ee6b2780931f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,9 +38,9 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" @@ -75,9 +75,9 @@ jobs: - build steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" @@ -86,7 +86,7 @@ jobs: run: pnpm install - name: Initialize CodeQL - uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 + uses: github/codeql-action/init@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 with: languages: javascript queries: +security-and-quality @@ -98,7 +98,7 @@ jobs: path: packages - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 + uses: github/codeql-action/analyze@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 with: category: "/language:javascript" @@ -112,12 +112,11 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] module: ['bundler', 'node'] - base: ['with-base-url', 'without-base-url'] steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" @@ -135,7 +134,6 @@ jobs: run: pnpm test:types env: MODULE_RESOLUTION: ${{ matrix.module }} - TS_BASE_URL: ${{ matrix.base }} lint: # autofix workflow will be triggered instead for PRs @@ -144,9 +142,9 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" @@ -171,18 +169,18 @@ jobs: os: [ubuntu-latest, windows-latest] env: ['dev', 'built'] builder: ['vite', 'webpack'] - payload: ['json', 'js'] + context: ['async', 'default'] node: [16] exclude: - env: 'dev' builder: 'webpack' - timeout-minutes: 10 + timeout-minutes: 15 steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: ${{ matrix.node }} cache: "pnpm" @@ -233,8 +231,8 @@ jobs: env: TEST_ENV: ${{ matrix.env }} TEST_BUILDER: ${{ matrix.builder }} - TEST_PAYLOAD: ${{ matrix.payload }} - SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.payload == 'js' || runner.os == 'Windows' }} + TEST_CONTEXT: ${{ matrix.context }} + SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }} build-release: permissions: @@ -252,11 +250,11 @@ jobs: timeout-minutes: 20 steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: fetch-depth: 0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" @@ -291,11 +289,11 @@ jobs: timeout-minutes: 20 steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: fetch-depth: 0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1ffd05de3074..da83c3abfe25 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@1360a344ccb0ab6e9475edef90ad2f46bf8003b1 # v3.0.6 + uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 diff --git a/.github/workflows/docs-e2e.yml b/.github/workflows/docs-e2e.yml index c7d17f8fb2b3..ba94c2f8da8c 100644 --- a/.github/workflows/docs-e2e.yml +++ b/.github/workflows/docs-e2e.yml @@ -21,9 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: cache: "pnpm" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e7006e798c7b..461316066e2c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,9 +20,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" diff --git a/.github/workflows/introspect.yml b/.github/workflows/introspect.yml index b6d9125e64fc..9d94f7beb1d9 100644 --- a/.github/workflows/introspect.yml +++ b/.github/workflows/introspect.yml @@ -21,9 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions - name: Check workflow files run: | - bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/590d3bd9dde0c91f7a66071d40eb84716526e5a6/scripts/download-actionlint.bash) + bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/590d3bd9dde0c91f7a66071d40eb84716526e5a6/scripts/download-actionlint.bash) 1.6.25 ./actionlint -color -shellcheck="" diff --git a/.github/workflows/nuxt2-edge.yml b/.github/workflows/nuxt2-edge.yml index 0d58210c744e..057f0b4ee31f 100644 --- a/.github/workflows/nuxt2-edge.yml +++ b/.github/workflows/nuxt2-edge.yml @@ -21,13 +21,13 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: '2.x' fetch-depth: 0 # All history - name: fetch tags run: git fetch --depth=1 origin "+refs/tags/*:refs/tags/*" - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 16 registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 10fcbde4b374..64f47e6a691f 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -29,13 +29,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: refs/pull/${{ github.event.issue.number }}/merge fetch-depth: 0 - run: corepack enable - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: 20 cache: "pnpm" diff --git a/.github/workflows/reproduire-close.yml b/.github/workflows/reproduire-close.yml new file mode 100644 index 000000000000..61c550910bd8 --- /dev/null +++ b/.github/workflows/reproduire-close.yml @@ -0,0 +1,24 @@ +name: Close incomplete issues +on: + workflow_dispatch: + schedule: + - cron: '30 1 * * *' # run every day + +permissions: + issues: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + with: + days-before-stale: -1 # Issues and PR will never be flagged stale automatically. + stale-issue-label: 'needs reproduction' # Label that flags an issue as stale. + only-labels: 'needs reproduction' # Only process these issues + days-before-issue-close: 7 + ignore-updates: true + remove-stale-when-updated: false + close-issue-message: This issue was closed because it was open for 7 days without a reproduction. + close-issue-label: closed-by-bot + operations-per-run: 300 #default 30 diff --git a/.github/workflows/reproduire.yml b/.github/workflows/reproduire.yml new file mode 100644 index 000000000000..9586bd970bcd --- /dev/null +++ b/.github/workflows/reproduire.yml @@ -0,0 +1,16 @@ +name: Reproduire +on: + issues: + types: [labeled] + +permissions: + issues: write + +jobs: + reproduire: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp + with: + label: needs reproduction diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index b5ad8bf27331..db434826b42f 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -31,7 +31,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: persist-credentials: false @@ -66,6 +66,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 + uses: github/codeql-action/upload-sarif@a09933a12a80f87b87005513f0abb1494c27a716 # v2.21.4 with: sarif_file: results.sarif diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml index 2ef53df62ae3..d000f52cbd13 100644 --- a/.github/workflows/semantic-pull-requests.yml +++ b/.github/workflows/semantic-pull-requests.yml @@ -15,7 +15,7 @@ jobs: permissions: pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR - if: github.repository == 'nuxt/nuxt' + if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v') runs-on: ubuntu-latest name: Semantic pull request steps: diff --git a/.gitignore b/.gitignore index 621e94602114..7c284d1371b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ jspm_packages package-lock.json packages/*/README.md +!packages/nuxi/README.md packages/*/LICENSE */**/yarn.lock /.yarn diff --git a/.stackblitz/codeflow.json b/.stackblitz/codeflow.json index 06a7fa58ac72..c6f914a9e62d 100644 --- a/.stackblitz/codeflow.json +++ b/.stackblitz/codeflow.json @@ -6,7 +6,6 @@ "@nuxt/test-utils": "./packages/test-utils", "@nuxt/vite": "./packages/vite", "@nuxt/webpack": "./packages/webpack", - "nuxi": "./packages/nuxi", "nuxt": "./packages/nuxt" } } diff --git a/.website/.gitignore b/.website/.gitignore new file mode 100755 index 000000000000..69f6b69d0721 --- /dev/null +++ b/.website/.gitignore @@ -0,0 +1,12 @@ +node_modules +*.iml +.idea +*.log* +.nuxt +.vscode +.DS_Store +coverage +dist +sw.* +.env +.output diff --git a/.website/README.md b/.website/README.md new file mode 100755 index 000000000000..610b0a4d57e0 --- /dev/null +++ b/.website/README.md @@ -0,0 +1,35 @@ +# Nuxt Docs Website + +This is a temporary directory until we open source the repository for nuxt.com. + +The goal is to simplify the contribution in the meantine to the documentation by having the possibility to preview the changes locally. + +## Setup + +Install dependencies in the root of the `nuxt` folder: + +```bash +pnpm i +``` + +Then stub the dependencies: + +```bash +pnpm build:stub +``` + +## Development + +In the root of the `nuxt` folder, run: + +```bash +pnpm docs:dev +``` + +Then open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +Update the documentation within the `docs` folder. + +--- + +For a detailed explanation of how things work, check out [Docus](https://docus.dev). diff --git a/.website/app.config.ts b/.website/app.config.ts new file mode 100644 index 000000000000..9692072acbc6 --- /dev/null +++ b/.website/app.config.ts @@ -0,0 +1,14 @@ +export default defineAppConfig({ + docus: { + title: 'Nuxt Docs [dev]', + description: 'The best place to start your documentation.', + socials: { + twitter: 'nuxt_js', + github: 'nuxt/nuxt' + }, + aside: { + level: 1, + collapsed: false, + }, + } +}) diff --git a/.website/nuxt.config.ts b/.website/nuxt.config.ts new file mode 100755 index 000000000000..8de14298ae0e --- /dev/null +++ b/.website/nuxt.config.ts @@ -0,0 +1,20 @@ +import { createResolver } from 'nuxt/kit' + +const { resolve } = createResolver(import.meta.url) + +export default defineNuxtConfig({ + // https://github.com/nuxt-themes/docus + extends: '@nuxt-themes/docus', + content: { + sources: { + docs: { + driver: 'fs', + prefix: '/', + base: resolve('../docs') + } + } + }, + experimental: { + renderJsonPayloads: false + } +}) diff --git a/.website/package.json b/.website/package.json new file mode 100755 index 000000000000..2d49c9d6ae06 --- /dev/null +++ b/.website/package.json @@ -0,0 +1,14 @@ +{ + "name": "docus-starter", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "generate": "nuxt generate", + "preview": "nuxt preview" + }, + "devDependencies": { + "@nuxt-themes/docus": "1.14.6" + } +} diff --git a/.website/tsconfig.json b/.website/tsconfig.json new file mode 100755 index 000000000000..4b34df1571f7 --- /dev/null +++ b/.website/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/docs/1.getting-started/10.deployment.md b/docs/1.getting-started/10.deployment.md index 0713165789ee..672d1c5f6581 100644 --- a/docs/1.getting-started/10.deployment.md +++ b/docs/1.getting-started/10.deployment.md @@ -81,52 +81,61 @@ There are two ways to deploy a Nuxt application to any static hosting services: ### Crawl-based Pre-rendering -Use the [`nuxi generate` command](/docs/api/commands/generate) to build your application. For every page, Nuxt uses a crawler to generate a corresponding HTML and payload files. The built files will be generated in the `.output/public` directory. +Use the [`nuxi generate` command](/docs/api/commands/generate) to build and pre-render your application using the [Nitro](/docs/guide/concepts/server-engine) crawler. This command is similar to `nuxt build` with the `nitro.static` option set to `true`, or running `nuxt build --prerender`. ```bash npx nuxi generate ``` -You can enable crawl-based pre-rendering when using `nuxt build` in the `nuxt.config` file: +That's it! You can now deploy the `.output/public` directory to any static hosting service or preview it locally with `npx serve .output/public`. -```ts [nuxt.config.ts|js] -defineNuxtConfig({ - nitro: { - prerender: { - crawlLinks: true - } - } -}) -``` +Working of the Nitro crawler: + +1. Load the HTML of your application's root route (`/`), any non-dynamic pages in your `~/pages` directory, and any other routes in the `nitro.prerender.routes` array. +2. Save the HTML and `payload.json` to the `~/.output/public/` directory to be served statically. +3. Find all anchor tags (``) in the HTML to navigate to other routes. +4. Repeat steps 1-3 for each anchor tag found until there are no more anchor tags to crawl. + +This is important to understand since pages that are not linked to a discoverable page can't be pre-rendered automatically. + +::alert{type=info} +Read more about the [`nuxi generate` command](/docs/api/commands/generate#nuxi-generate). +:: ### Selective Pre-rendering -You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build. +You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build or ignore routes that you don't want to pre-render like `/dynamic` in the `nuxt.config` file: ```ts [nuxt.config.ts|js] defineNuxtConfig({ nitro: { prerender: { routes: ['/user/1', '/user/2'] + ignore: ['/dynamic'] } } }) ``` -When using this option with `nuxi build`, static payloads won't be generated by default at build time. For now, selective payload generation is under an experimental flag. +You can combine this with the `crawLinks` option to pre-render a set of routes that the crawler can't discover like your `/sitemap.xml` or `/robots.txt`: ```ts [nuxt.config.ts|js] defineNuxtConfig({ - /* The /dynamic route won't be crawled */ nitro: { - prerender: { crawlLinks: true, ignore: ['/dynamic'] } - }, - experimental: { - payloadExtraction: true + prerender: { + crawlLinks: true, + routes: ['/sitemap.xml', '/robots.txt'] + } } }) ``` +Setting `nitro.prerender` to `true` is similar to `nitro.prerender.crawlLinks` to `true`. + +::alert{type=info} +Read more about [pre-rendering](https://nitro.unjs.io/config#prerender) in the Nitro documentation. +:: + ### Client-side Only Rendering If you don't want to pre-render your routes, another way of using static hosting is to set the `ssr` property to `false` in the `nuxt.config` file. The `nuxi generate` command will then output an `.output/public/index.html` entrypoint and JavaScript bundles like a classic client-side Vue.js application. diff --git a/docs/1.getting-started/2.installation.md b/docs/1.getting-started/2.installation.md index 4d706543c1ef..7c6b4cb12d44 100644 --- a/docs/1.getting-started/2.installation.md +++ b/docs/1.getting-started/2.installation.md @@ -31,7 +31,7 @@ Start with one of our starters and themes directly by opening [nuxt.new](https:/ ::details :summary[Additional notes for an optimal setup:] - **Node.js**: Make sure to use an even numbered version (18, 20, etc) - +- **Nuxtr**: Install the community-developed [Nuxtr extension](https://marketplace.visualstudio.com/items?itemName=Nuxtr.nuxtr-vscode) - **Volar**: Either enable [**Take Over Mode**](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) (recommended) or add the [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) If you have enabled **Take Over Mode** or installed the **TypeScript Vue Plugin (Volar)**, you can disable generating the shim for `*.vue` files in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file: @@ -59,6 +59,10 @@ npx nuxi@latest init pnpm dlx nuxi@latest init ``` +```bash [bun] +bunx nuxi@latest init +``` + :: Open your project folder in Visual Studio Code: @@ -84,6 +88,10 @@ npm install pnpm install ``` +```bash [bun] +bun install +``` + :: ## Development Server @@ -104,6 +112,10 @@ npm run dev -- -o pnpm dev -o ``` +```bash [bun] +bun run dev -o +``` + :: ::alert{type=success icon=✨ .font-bold} diff --git a/docs/1.getting-started/5.routing.md b/docs/1.getting-started/5.routing.md index 9209b8525544..3f5ce8de01fe 100644 --- a/docs/1.getting-started/5.routing.md +++ b/docs/1.getting-started/5.routing.md @@ -71,7 +71,7 @@ When a `` enters the viewport on the client side, Nuxt will automatica The `useRoute()` composable can be used in a ` - - - - -` -}) - -const composable: Template = ({ name }) => { - const nameWithUsePrefix = name.startsWith('use') ? name : `use${upperFirst(name)}` - return { - path: `composables/${name}.ts`, - contents: ` -export const ${nameWithUsePrefix} = () => { - return ref() -} - ` - } -} - -const middleware: Template = ({ name, args }) => ({ - path: `middleware/${name}${applySuffix(args, ['global'])}.ts`, - contents: ` -export default defineNuxtRouteMiddleware((to, from) => {}) -` -}) - -const layout: Template = ({ name }) => ({ - path: `layouts/${name}.vue`, - contents: ` - - - - - -` -}) - -const page: Template = ({ name }) => ({ - path: `pages/${name}.vue`, - contents: ` - - - - - -` -}) - -export const templates = { - api, - plugin, - component, - composable, - middleware, - layout, - page -} as Record - -// -- internal utils -- - -function applySuffix (args: TemplateOptions['args'], suffixes: string[], unwrapFrom?: string): string { - let suffix = '' - // --client - for (const s of suffixes) { - if (args[s]) { - suffix += '.' + s - } - } - // --mode=server - if (unwrapFrom && args[unwrapFrom] && suffixes.includes(args[unwrapFrom])) { - suffix += '.' + args[unwrapFrom] - } - return suffix -} diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 6e21f147fd8c..020c990a5ede 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "nuxt", - "version": "3.6.5", + "version": "3.7.0", "repository": "nuxt/nuxt", "license": "MIT", "type": "module", @@ -55,64 +55,66 @@ "@nuxt/devalue": "^2.0.2", "@nuxt/kit": "workspace:../kit", "@nuxt/schema": "workspace:../schema", - "@nuxt/telemetry": "^2.3.2", - "@nuxt/ui-templates": "^1.2.1", + "@nuxt/telemetry": "^2.4.1", + "@nuxt/ui-templates": "^1.3.1", "@nuxt/vite-builder": "workspace:../vite", - "@unhead/ssr": "^1.1.32", - "@unhead/vue": "^1.1.32", + "@unhead/dom": "^1.3.7", + "@unhead/ssr": "^1.3.7", + "@unhead/vue": "^1.3.7", "@vue/shared": "^3.3.4", "acorn": "8.10.0", "c12": "^1.4.2", "chokidar": "^3.5.3", "cookie-es": "^1.0.0", "defu": "^6.1.2", - "destr": "^2.0.0", + "destr": "^2.0.1", "devalue": "^4.3.2", - "esbuild": "^0.18.16", + "esbuild": "^0.19.2", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "fs-extra": "^11.1.1", "globby": "^13.2.2", - "h3": "^1.7.1", + "h3": "^1.8.0", "hookable": "^5.5.3", - "jiti": "^1.19.1", + "jiti": "^1.19.3", "klona": "^2.0.6", "knitwork": "^1.0.0", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "mlly": "^1.4.0", - "nitropack": "^2.5.2", - "nuxi": "workspace:../nuxi", - "nypm": "^0.2.2", - "ofetch": "^1.1.1", - "ohash": "^1.1.2", + "magic-string": "^0.30.3", + "mlly": "^1.4.1", + "nitropack": "^2.6.1", + "nuxi": "^3.7.0", + "nypm": "^0.3.1", + "ofetch": "^1.3.3", + "ohash": "^1.1.3", "pathe": "^1.1.1", "perfect-debounce": "^1.0.0", + "pkg-types": "^1.0.3", "prompts": "^2.4.2", "scule": "^1.0.0", - "strip-literal": "^1.0.1", - "ufo": "^1.2.0", + "std-env": "^3.4.3", + "strip-literal": "^1.3.0", + "ufo": "^1.3.0", "ultrahtml": "^1.3.0", "uncrypto": "^0.1.3", "unctx": "^2.3.1", - "unenv": "^1.5.2", - "unimport": "^3.1.0", + "unenv": "^1.7.3", + "unimport": "^3.2.0", "unplugin": "^1.4.0", "unplugin-vue-router": "^0.6.4", "untyped": "^1.4.0", "vue": "^3.3.4", - "vue-bundle-renderer": "^1.0.3", + "vue-bundle-renderer": "^2.0.0", "vue-devtools-stub": "^0.1.0", "vue-router": "^4.2.4" }, "devDependencies": { - "@parcel/watcher": "2.2.0", + "@parcel/watcher": "2.3.0", "@types/estree": "1.0.1", "@types/fs-extra": "11.0.1", "@types/prompts": "2.4.4", - "@vitejs/plugin-vue": "4.2.3", + "@vitejs/plugin-vue": "4.3.3", "unbuild": "latest", - "vite": "4.4.7", + "vite": "4.4.9", "vitest": "0.33.0" }, "peerDependencies": { @@ -122,6 +124,9 @@ "peerDependenciesMeta": { "@parcel/watcher": { "optional": true + }, + "@types/node": { + "optional": true } }, "engines": { diff --git a/packages/nuxt/src/app/compat/idle-callback.ts b/packages/nuxt/src/app/compat/idle-callback.ts index cc28a2b1a2be..01779dc1febe 100644 --- a/packages/nuxt/src/app/compat/idle-callback.ts +++ b/packages/nuxt/src/app/compat/idle-callback.ts @@ -1,6 +1,6 @@ // Polyfills for Safari support // https://caniuse.com/requestidlecallback -export const requestIdleCallback: Window['requestIdleCallback'] = process.server +export const requestIdleCallback: Window['requestIdleCallback'] = import.meta.server ? (() => {}) as any : (globalThis.requestIdleCallback || ((cb) => { const start = Date.now() @@ -11,6 +11,6 @@ export const requestIdleCallback: Window['requestIdleCallback'] = process.server return setTimeout(() => { cb(idleDeadline) }, 1) })) -export const cancelIdleCallback: Window['cancelIdleCallback'] = process.server +export const cancelIdleCallback: Window['cancelIdleCallback'] = import.meta.server ? (() => {}) as any : (globalThis.cancelIdleCallback || ((id) => { clearTimeout(id) })) diff --git a/packages/nuxt/src/app/components/client-fallback.client.ts b/packages/nuxt/src/app/components/client-fallback.client.ts index f0a14d65ecac..2f32e0530cc1 100644 --- a/packages/nuxt/src/app/components/client-fallback.client.ts +++ b/packages/nuxt/src/app/components/client-fallback.client.ts @@ -30,6 +30,8 @@ export default defineComponent({ emits: ['ssr-error'], setup (props, ctx) { const mounted = ref(false) + // This is deliberate - `uid` should not be provided by user but by a transform plugin and will not be reactive. + // eslint-disable-next-line vue/no-setup-props-destructure const ssrFailed = useState(`${props.uid}`) if (ssrFailed.value) { diff --git a/packages/nuxt/src/app/components/dev-only.ts b/packages/nuxt/src/app/components/dev-only.ts index 914ba6f6e445..6c8ec7c5e9e5 100644 --- a/packages/nuxt/src/app/components/dev-only.ts +++ b/packages/nuxt/src/app/components/dev-only.ts @@ -3,7 +3,7 @@ import { defineComponent } from 'vue' export default defineComponent({ name: 'DevOnly', setup (_, props) { - if (process.dev) { + if (import.meta.dev) { return () => props.slots.default?.() } return () => props.slots.fallback?.() diff --git a/packages/nuxt/src/app/components/island-renderer.ts b/packages/nuxt/src/app/components/island-renderer.ts index 07f5409803f2..421e3863abad 100644 --- a/packages/nuxt/src/app/components/island-renderer.ts +++ b/packages/nuxt/src/app/components/island-renderer.ts @@ -18,7 +18,7 @@ export default defineComponent({ if (!component) { throw createError({ statusCode: 404, - statusMessage: `Island component not found: ${JSON.stringify(component)}` + statusMessage: `Island component not found: ${props.context.name}` }) } diff --git a/packages/nuxt/src/app/components/layout.ts b/packages/nuxt/src/app/components/layout.ts index bb1b149265e3..b88def1a9434 100644 --- a/packages/nuxt/src/app/components/layout.ts +++ b/packages/nuxt/src/app/components/layout.ts @@ -1,150 +1,2 @@ -import type { Ref, VNode } from 'vue' -import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue' -import type { RouteLocationNormalizedLoaded } from 'vue-router' -import { _wrapIf } from './utils' -import { LayoutMetaSymbol, PageRouteSymbol } from './injections' - -import { useRoute } from '#app/composables/router' -// @ts-expect-error virtual file -import { useRoute as useVueRouterRoute } from '#build/pages' -// @ts-expect-error virtual file -import layouts from '#build/layouts' -// @ts-expect-error virtual file -import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs' -import { useNuxtApp } from '#app' - -// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved -const LayoutLoader = defineComponent({ - name: 'LayoutLoader', - inheritAttrs: false, - props: { - name: String, - layoutProps: Object - }, - async setup (props, context) { - const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r) - - return () => h(LayoutComponent, props.layoutProps, context.slots) - } -}) - -export default defineComponent({ - name: 'NuxtLayout', - inheritAttrs: false, - props: { - name: { - type: [String, Boolean, Object] as unknown as () => string | false | Ref, - default: null - } - }, - setup (props, context) { - const nuxtApp = useNuxtApp() - // Need to ensure (if we are not a child of ``) that we use synchronous route (not deferred) - const injectedRoute = inject(PageRouteSymbol) - const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute - - const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default') - - const layoutRef = ref() - context.expose({ layoutRef }) - - const done = nuxtApp.deferHydration() - - return () => { - const hasLayout = layout.value && layout.value in layouts - if (process.dev && layout.value && !hasLayout && layout.value !== 'default') { - console.warn(`Invalid layout \`${layout.value}\` selected.`) - } - - const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition - - // We avoid rendering layout transition if there is no layout to render - return _wrapIf(Transition, hasLayout && transitionProps, { - default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, { - default: () => h( - // @ts-expect-error seems to be an issue in vue types - LayoutProvider, - { - layoutProps: mergeProps(context.attrs, { ref: layoutRef }), - key: layout.value, - name: layout.value, - shouldProvide: !props.name, - hasTransition: !!transitionProps - }, context.slots) - }) - }).default() - } - } -}) - -const LayoutProvider = defineComponent({ - name: 'NuxtLayoutProvider', - inheritAttrs: false, - props: { - name: { - type: [String, Boolean] - }, - layoutProps: { - type: Object - }, - hasTransition: { - type: Boolean - }, - shouldProvide: { - type: Boolean - } - }, - setup (props, context) { - // Prevent reactivity when the page will be rerendered in a different suspense fork - // eslint-disable-next-line vue/no-setup-props-destructure - const name = props.name - if (props.shouldProvide) { - provide(LayoutMetaSymbol, { - isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default') - }) - } - - let vnode: VNode | undefined - if (process.dev && process.client) { - onMounted(() => { - nextTick(() => { - if (['#comment', '#text'].includes(vnode?.el?.nodeName)) { - if (name) { - console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`) - } else { - console.warn('[nuxt] `` needs to be passed a single root node in its default slot.') - } - } - }) - }) - } - - return () => { - if (!name || (typeof name === 'string' && !(name in layouts))) { - if (process.dev && process.client && props.hasTransition) { - vnode = context.slots.default?.() as VNode | undefined - return vnode - } - return context.slots.default?.() - } - - if (process.dev && process.client && props.hasTransition) { - vnode = h( - // @ts-expect-error seems to be an issue in vue types - LayoutLoader, - { key: name, layoutProps: props.layoutProps, name }, - context.slots - ) - - return vnode - } - - return h( - // @ts-expect-error seems to be an issue in vue types - LayoutLoader, - { key: name, layoutProps: props.layoutProps, name }, - context.slots - ) - } - } -}) +// TODO: remove in 4.x +export { default } from './nuxt-layout' diff --git a/packages/nuxt/src/app/components/nuxt-error-boundary.ts b/packages/nuxt/src/app/components/nuxt-error-boundary.ts index a58516c1ce82..cb8d2f67071a 100644 --- a/packages/nuxt/src/app/components/nuxt-error-boundary.ts +++ b/packages/nuxt/src/app/components/nuxt-error-boundary.ts @@ -12,7 +12,7 @@ export default defineComponent({ const nuxtApp = useNuxtApp() onErrorCaptured((err, target, info) => { - if (process.client && !nuxtApp.isHydrating) { + if (import.meta.client && !nuxtApp.isHydrating) { emit('error', err) nuxtApp.hooks.callHook('vue:error', err, target, info) error.value = err diff --git a/packages/nuxt/src/app/components/nuxt-error-page.vue b/packages/nuxt/src/app/components/nuxt-error-page.vue index 0976b1a2f3e5..cf32d26835a3 100644 --- a/packages/nuxt/src/app/components/nuxt-error-page.vue +++ b/packages/nuxt/src/app/components/nuxt-error-page.vue @@ -36,11 +36,11 @@ const is404 = statusCode === 404 const statusMessage = _error.statusMessage ?? (is404 ? 'Page Not Found' : 'Internal Server Error') const description = _error.message || _error.toString() -const stack = process.dev && !is404 ? _error.description || `
${stacktrace}
` : undefined +const stack = import.meta.dev && !is404 ? _error.description || `
${stacktrace}
` : undefined // TODO: Investigate side-effect issue with imports const _Error404 = defineAsyncComponent(() => import('@nuxt/ui-templates/templates/error-404.vue').then(r => r.default || r)) -const _Error = process.dev +const _Error = import.meta.dev ? defineAsyncComponent(() => import('@nuxt/ui-templates/templates/error-dev.vue').then(r => r.default || r)) : defineAsyncComponent(() => import('@nuxt/ui-templates/templates/error-500.vue').then(r => r.default || r)) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index c268265d6c51..3955b8d8ef07 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -13,6 +13,9 @@ import { getFragmentHTML, getSlotProps } from './utils' import { useNuxtApp, useRuntimeConfig } from '#app/nuxt' import { useRequestEvent } from '#app/composables/ssr' +// @ts-expect-error virtual file +import { remoteComponentIslands } from '#build/nuxt.config.mjs' + const pKey = '_islandPromises' const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/ const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/ @@ -20,7 +23,7 @@ const SLOTNAME_RE = /nuxt-ssr-slot-name="([^"]*)"/g const SLOT_FALLBACK_RE = /
]*><\/div>(((?!
]*>)[\s\S])*)
]*><\/div>/g let id = 0 -const getId = process.client ? () => (id++).toString() : randomUUID +const getId = import.meta.client ? () => (id++).toString() : randomUUID export default defineComponent({ name: 'NuxtIsland', @@ -29,6 +32,7 @@ export default defineComponent({ type: String, required: true }, + lazy: Boolean, props: { type: Object, default: () => undefined @@ -36,16 +40,21 @@ export default defineComponent({ context: { type: Object, default: () => ({}) + }, + source: { + type: String, + default: () => undefined } }, async setup (props, { slots }) { + const error = ref(null) const config = useRuntimeConfig() const nuxtApp = useNuxtApp() - const hashId = computed(() => hash([props.name, props.props, props.context])) + const hashId = computed(() => hash([props.name, props.props, props.context, props.source])) const instance = getCurrentInstance()! const event = useRequestEvent() // TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved - const eventFetch = process.server ? event.fetch : process.dev ? $fetch.raw : globalThis.fetch + const eventFetch = import.meta.server ? event.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch const mounted = ref(false) onMounted(() => { mounted.value = true }) @@ -53,7 +62,7 @@ export default defineComponent({ nuxtApp.payload.data[key] = { __nuxt_island: { key, - ...(process.server && process.env.prerender) + ...(import.meta.server && import.meta.prerender) ? {} : { params: { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined } } }, @@ -61,8 +70,8 @@ export default defineComponent({ } } - const ssrHTML = ref('
') - if (process.client) { + const ssrHTML = ref('') + if (import.meta.client) { const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('') if (renderedHTML && nuxtApp.isHydrating) { setPayload(`${props.name}_${hashId.value}`, { @@ -74,7 +83,7 @@ export default defineComponent({ } }) } - ssrHTML.value = renderedHTML ?? '
' + ssrHTML.value = renderedHTML } const slotProps = computed(() => getSlotProps(ssrHTML.value)) const uid = ref(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID()) @@ -100,20 +109,21 @@ export default defineComponent({ const key = `${props.name}_${hashId.value}` if (nuxtApp.payload.data[key] && !force) { return nuxtApp.payload.data[key] } - const url = `/__nuxt_island/${key}` - if (process.server && process.env.prerender) { + const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key}`, props.source).href : `/__nuxt_island/${key}` + + if (import.meta.server && import.meta.prerender) { // Hint to Nitro to prerender the island component appendResponseHeader(event, 'x-nitro-prerender', url) } // TODO: Validate response // $fetch handles the app.baseURL in dev - const r = await eventFetch(withQuery(process.dev && process.client ? url : joinURL(config.app.baseURL ?? '', url), { + const r = await eventFetch(withQuery(import.meta.dev && import.meta.client ? url : joinURL(config.app.baseURL ?? '', url), { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined })) - const result = process.server || !process.dev ? await r.json() : (r as FetchResponse)._data + const result = import.meta.server || !import.meta.dev ? await r.json() : (r as FetchResponse)._data // TODO: support passing on more headers - if (process.server && process.env.prerender) { + if (import.meta.server && import.meta.prerender) { const hints = r.headers.get('x-nitro-prerender') if (hints) { appendResponseHeader(event, 'x-nitro-prerender', hints) @@ -130,18 +140,23 @@ export default defineComponent({ delete nuxtApp[pKey]![uid.value] }) } - const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value] - cHead.value.link = res.head.link - cHead.value.style = res.head.style - ssrHTML.value = res.html.replace(UID_ATTR, () => { - return `nuxt-ssr-component-uid="${getId()}"` - }) - key.value++ - if (process.client) { - // must await next tick for Teleport to work correctly with static node re-rendering - await nextTick() + try { + const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value] + cHead.value.link = res.head.link + cHead.value.style = res.head.style + ssrHTML.value = res.html.replace(UID_ATTR, () => { + return `nuxt-ssr-component-uid="${getId()}"` + }) + key.value++ + error.value = null + if (import.meta.client) { + // must await next tick for Teleport to work correctly with static node re-rendering + await nextTick() + } + setUid() + } catch (e) { + error.value = e } - setUid() } if (import.meta.hot) { @@ -150,23 +165,27 @@ export default defineComponent({ }) } - if (process.client) { + if (import.meta.client) { watch(props, debounce(() => fetchComponent(), 100)) } - // TODO: allow lazy loading server islands - if (process.server || !nuxtApp.isHydrating) { + if (import.meta.client && !nuxtApp.isHydrating && props.lazy) { + fetchComponent() + } else if (import.meta.server || !nuxtApp.isHydrating) { await fetchComponent() } return () => { + if ((!html.value || error.value) && slots.fallback) { + return [slots.fallback({ error: error.value })] + } const nodes = [createVNode(Fragment, { key: key.value - }, [h(createStaticVNode(html.value, 1))])] - if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) { + }, [h(createStaticVNode(html.value || '
', 1))])] + if (uid.value && (mounted.value || nuxtApp.isHydrating || import.meta.server)) { for (const slot in slots) { if (availableSlots.value.includes(slot)) { - nodes.push(createVNode(Teleport, { to: process.client ? `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-slot-name='${slot}']` : `uid=${uid.value};slot=${slot}` }, { + nodes.push(createVNode(Teleport, { to: import.meta.client ? `[nuxt-ssr-component-uid='${uid.value}'] [nuxt-ssr-slot-name='${slot}']` : `uid=${uid.value};slot=${slot}` }, { default: () => (slotProps.value[slot] ?? [undefined]).map((data: any) => slots[slot]?.(data)) })) } diff --git a/packages/nuxt/src/app/components/nuxt-layout.ts b/packages/nuxt/src/app/components/nuxt-layout.ts new file mode 100644 index 000000000000..82c321479dd8 --- /dev/null +++ b/packages/nuxt/src/app/components/nuxt-layout.ts @@ -0,0 +1,157 @@ +import type { DefineComponent, MaybeRef, VNode } from 'vue' +import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue' +import type { RouteLocationNormalizedLoaded } from 'vue-router' +import { _wrapIf } from './utils' +import { LayoutMetaSymbol, PageRouteSymbol } from './injections' +import type { PageMeta } from '#app' + +import { useRoute } from '#app/composables/router' +import { useNuxtApp } from '#app/nuxt' +// @ts-expect-error virtual file +import { useRoute as useVueRouterRoute } from '#build/pages' +// @ts-expect-error virtual file +import layouts from '#build/layouts' +// @ts-expect-error virtual file +import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs' + +// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved +const LayoutLoader = defineComponent({ + name: 'LayoutLoader', + inheritAttrs: false, + props: { + name: String, + layoutProps: Object + }, + async setup (props, context) { + // This is a deliberate hack - this component must always be called with an explicit key to ensure + // that setup reruns when the name changes. + + // eslint-disable-next-line vue/no-setup-props-destructure + const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r) + + return () => h(LayoutComponent, props.layoutProps, context.slots) + } +}) + +export default defineComponent({ + name: 'NuxtLayout', + inheritAttrs: false, + props: { + name: { + type: [String, Boolean, Object] as unknown as () => unknown extends PageMeta['layout'] ? MaybeRef : PageMeta['layout'], + default: null + } + }, + setup (props, context) { + const nuxtApp = useNuxtApp() + // Need to ensure (if we are not a child of ``) that we use synchronous route (not deferred) + const injectedRoute = inject(PageRouteSymbol) + const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute + + const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default') + + const layoutRef = ref() + context.expose({ layoutRef }) + + const done = nuxtApp.deferHydration() + + return () => { + const hasLayout = layout.value && layout.value in layouts + if (import.meta.dev && layout.value && !hasLayout && layout.value !== 'default') { + console.warn(`Invalid layout \`${layout.value}\` selected.`) + } + + const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition + + // We avoid rendering layout transition if there is no layout to render + return _wrapIf(Transition, hasLayout && transitionProps, { + default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, { + default: () => h( + // @ts-expect-error seems to be an issue in vue types + LayoutProvider, + { + layoutProps: mergeProps(context.attrs, { ref: layoutRef }), + key: layout.value, + name: layout.value, + shouldProvide: !props.name, + hasTransition: !!transitionProps + }, context.slots) + }) + }).default() + } + } +}) as unknown as DefineComponent<{ + name?: unknown extends PageMeta['layout'] ? MaybeRef : PageMeta['layout'] +}> + +const LayoutProvider = defineComponent({ + name: 'NuxtLayoutProvider', + inheritAttrs: false, + props: { + name: { + type: [String, Boolean] + }, + layoutProps: { + type: Object + }, + hasTransition: { + type: Boolean + }, + shouldProvide: { + type: Boolean + } + }, + setup (props, context) { + // Prevent reactivity when the page will be rerendered in a different suspense fork + // eslint-disable-next-line vue/no-setup-props-destructure + const name = props.name + if (props.shouldProvide) { + provide(LayoutMetaSymbol, { + isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default') + }) + } + + let vnode: VNode | undefined + if (import.meta.dev && import.meta.client) { + onMounted(() => { + nextTick(() => { + if (['#comment', '#text'].includes(vnode?.el?.nodeName)) { + if (name) { + console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`) + } else { + console.warn('[nuxt] `` needs to be passed a single root node in its default slot.') + } + } + }) + }) + } + + return () => { + if (!name || (typeof name === 'string' && !(name in layouts))) { + if (import.meta.dev && import.meta.client && props.hasTransition) { + vnode = context.slots.default?.() as VNode | undefined + return vnode + } + return context.slots.default?.() + } + + if (import.meta.dev && import.meta.client && props.hasTransition) { + vnode = h( + // @ts-expect-error seems to be an issue in vue types + LayoutLoader, + { key: name, layoutProps: props.layoutProps, name }, + context.slots + ) + + return vnode + } + + return h( + // @ts-expect-error seems to be an issue in vue types + LayoutLoader, + { key: name, layoutProps: props.layoutProps, name }, + context.slots + ) + } + } +}) diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index 4e89cd042daf..dd65580a2476 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -51,7 +51,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) { const componentName = options.componentName || 'NuxtLink' const checkPropConflicts = (props: NuxtLinkProps, main: keyof NuxtLinkProps, sub: keyof NuxtLinkProps): void => { - if (process.dev && props[main] !== undefined && props[sub] !== undefined) { + if (import.meta.dev && props[main] !== undefined && props[sub] !== undefined) { console.warn(`[${componentName}] \`${main}\` and \`${sub}\` cannot be used together. \`${sub}\` will be ignored.`) } } @@ -198,10 +198,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) { // Prefetching const prefetched = ref(false) - const el = process.server ? undefined : ref(null) - const elRef = process.server ? undefined : (ref: any) => { el!.value = props.custom ? ref?.$el?.nextElementSibling : ref?.$el } + const el = import.meta.server ? undefined : ref(null) + const elRef = import.meta.server ? undefined : (ref: any) => { el!.value = props.custom ? ref?.$el?.nextElementSibling : ref?.$el } - if (process.client) { + if (import.meta.client) { checkPropConflicts(props, 'prefetch', 'noPrefetch') const shouldPrefetch = props.prefetch !== false && props.noPrefetch !== true && props.target !== '_blank' && !isSlowConnection() if (shouldPrefetch) { @@ -329,7 +329,7 @@ type CallbackFn = () => void type ObserveFn = (element: Element, callback: CallbackFn) => () => void function useObserver (): { observe: ObserveFn } | undefined { - if (process.server) { return } + if (import.meta.server) { return } const nuxtApp = useNuxtApp() if (nuxtApp._observer) { @@ -370,7 +370,7 @@ function useObserver (): { observe: ObserveFn } | undefined { } function isSlowConnection () { - if (process.server) { return } + if (import.meta.server) { return } // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection const cn = (navigator as any).connection as { saveData: boolean, effectiveType: string } | null diff --git a/packages/nuxt/src/app/components/nuxt-loading-indicator.ts b/packages/nuxt/src/app/components/nuxt-loading-indicator.ts index f9542449654b..01cf194bb25a 100644 --- a/packages/nuxt/src/app/components/nuxt-loading-indicator.ts +++ b/packages/nuxt/src/app/components/nuxt-loading-indicator.ts @@ -26,6 +26,8 @@ export default defineComponent({ } }, setup (props, { slots }) { + // TODO: use computed values in useLoadingIndicator + // eslint-disable-next-line vue/no-setup-props-destructure const indicator = useLoadingIndicator({ duration: props.duration, throttle: props.throttle @@ -52,14 +54,16 @@ export default defineComponent({ } }) - nuxtApp.hook('page:finish', indicator.finish) - nuxtApp.hook('vue:error', indicator.finish) + const unsubPage = nuxtApp.hook('page:finish', indicator.finish) + const unsubError = nuxtApp.hook('vue:error', indicator.finish) onBeforeUnmount(() => { const index = globalMiddleware.indexOf(indicator.start) if (index >= 0) { globalMiddleware.splice(index, 1) } + unsubPage() + unsubError() indicator.clear() }) @@ -99,7 +103,7 @@ function useLoadingIndicator (opts: { function start () { clear() progress.value = 0 - if (opts.throttle && process.client) { + if (opts.throttle && import.meta.client) { _throttle = setTimeout(() => { isLoading.value = true _startTimer() @@ -127,7 +131,7 @@ function useLoadingIndicator (opts: { function _hide () { clear() - if (process.client) { + if (import.meta.client) { setTimeout(() => { isLoading.value = false setTimeout(() => { progress.value = 0 }, 400) @@ -136,7 +140,7 @@ function useLoadingIndicator (opts: { } function _startTimer () { - if (process.client) { + if (import.meta.client) { _timer = setInterval(() => { _increase(step.value) }, 100) } } diff --git a/packages/nuxt/src/app/components/nuxt-root.vue b/packages/nuxt/src/app/components/nuxt-root.vue index c39c0d704196..80a9d38ac130 100644 --- a/packages/nuxt/src/app/components/nuxt-root.vue +++ b/packages/nuxt/src/app/components/nuxt-root.vue @@ -16,23 +16,23 @@ import { PageRouteSymbol } from '#app/components/injections' import AppComponent from '#build/app-component.mjs' import ErrorComponent from '#build/error-component.mjs' -const IslandRenderer = process.server +const IslandRenderer = import.meta.server ? defineAsyncComponent(() => import('./island-renderer').then(r => r.default || r)) : () => null const nuxtApp = useNuxtApp() const onResolve = nuxtApp.deferHydration() -const url = process.server ? nuxtApp.ssrContext.url : window.location.pathname -const SingleRenderer = process.test && process.dev && process.server && url.startsWith('/__nuxt_component_test__/') && /* #__PURE__ */ defineAsyncComponent(() => import('#build/test-component-wrapper.mjs') - .then(r => r.default(process.server ? url : window.location.href))) +const url = import.meta.server ? nuxtApp.ssrContext.url : window.location.pathname +const SingleRenderer = import.meta.test && import.meta.dev && import.meta.server && url.startsWith('/__nuxt_component_test__/') && /* #__PURE__ */ defineAsyncComponent(() => import('#build/test-component-wrapper.mjs') + .then(r => r.default(import.meta.server ? url : window.location.href))) // Inject default route (outside of pages) as active route provide(PageRouteSymbol, useRoute()) // vue:setup hook const results = nuxtApp.hooks.callHookWith(hooks => hooks.map(hook => hook()), 'vue:setup') -if (process.dev && results && results.some(i => i && 'then' in i)) { +if (import.meta.dev && results && results.some(i => i && 'then' in i)) { console.error('[nuxt] Error in `vue:setup`. Callbacks must be synchronous.') } @@ -40,7 +40,7 @@ if (process.dev && results && results.some(i => i && 'then' in i)) { const error = useError() onErrorCaptured((err, target, info) => { nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError)) - if (process.server || (isNuxtError(err) && (err.fatal || err.unhandled))) { + if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) { const p = nuxtApp.runWithContext(() => showError(err)) onServerPrefetch(() => p) return false // suppress error from breaking render @@ -48,5 +48,5 @@ onErrorCaptured((err, target, info) => { }) // Component islands context -const { islandContext } = process.server && nuxtApp.ssrContext +const islandContext = import.meta.server && nuxtApp.ssrContext.islandContext diff --git a/packages/nuxt/src/app/components/route-provider.ts b/packages/nuxt/src/app/components/route-provider.ts index d24b807aad49..5db44c3f7954 100644 --- a/packages/nuxt/src/app/components/route-provider.ts +++ b/packages/nuxt/src/app/components/route-provider.ts @@ -36,7 +36,7 @@ export const RouteProvider = defineComponent({ provide(PageRouteSymbol, shallowReactive(route)) let vnode: VNode - if (process.dev && process.client && props.trackRootNodes) { + if (import.meta.dev && import.meta.client && props.trackRootNodes) { onMounted(() => { nextTick(() => { if (['#comment', '#text'].includes(vnode?.el?.nodeName)) { @@ -48,7 +48,7 @@ export const RouteProvider = defineComponent({ } return () => { - if (process.dev && process.client) { + if (import.meta.dev && import.meta.client) { vnode = h(props.vnode, { ref: props.vnodeRef }) return vnode } diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index d95ed64f50e6..cc7c5e741cd9 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -73,7 +73,7 @@ export function vforToArray (source: any): any[] { } else if (isString(source)) { return source.split('') } else if (typeof source === 'number') { - if (process.dev && !Number.isInteger(source)) { + if (import.meta.dev && !Number.isInteger(source)) { console.warn(`The v-for range expect an integer value but got ${source}.`) } const array = [] diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 817c7d0432b2..cd50a3c549cc 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -145,7 +145,7 @@ export function useAsyncData< const hasCachedData = () => getCachedData() !== undefined // Create or use a shared asyncData entity - if (!nuxt._asyncData[key]) { + if (!nuxt._asyncData[key] || !options.immediate) { nuxt._asyncData[key] = { data: ref(getCachedData() ?? options.default!()), pending: ref(!hasCachedData()), @@ -222,7 +222,7 @@ export function useAsyncData< const fetchOnServer = options.server !== false && nuxt.payload.serverRendered // Server side - if (process.server && fetchOnServer && options.immediate) { + if (import.meta.server && fetchOnServer && options.immediate) { const promise = initialFetch() if (getCurrentInstance()) { onServerPrefetch(() => promise) @@ -232,7 +232,7 @@ export function useAsyncData< } // Client side - if (process.client) { + if (import.meta.client) { // Setup hook callbacks once per instance const instance = getCurrentInstance() if (instance && !instance._nuxtOnBeforeMountCbs) { @@ -349,7 +349,7 @@ export function useNuxtData (key: string): { data: Ref { - if (process.server) { + if (import.meta.server) { return Promise.resolve() } diff --git a/packages/nuxt/src/app/composables/chunk.ts b/packages/nuxt/src/app/composables/chunk.ts index 1e0c1e4f1301..9bdb5ea34f84 100644 --- a/packages/nuxt/src/app/composables/chunk.ts +++ b/packages/nuxt/src/app/composables/chunk.ts @@ -29,7 +29,7 @@ export interface ReloadNuxtAppOptions { } export function reloadNuxtApp (options: ReloadNuxtAppOptions = {}) { - if (process.server) { return } + if (import.meta.server) { return } const path = options.path || window.location.pathname let handledPath: Record = {} diff --git a/packages/nuxt/src/app/composables/component.ts b/packages/nuxt/src/app/composables/component.ts index 175dabc83278..5bfcc992ea41 100644 --- a/packages/nuxt/src/app/composables/component.ts +++ b/packages/nuxt/src/app/composables/component.ts @@ -22,7 +22,7 @@ async function runLegacyAsyncData (res: Record | Promise (name: string, _opts?: const cookie = ref(cookies[name] as any ?? opts.default?.()) - if (process.client) { + if (import.meta.client) { const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`) if (getCurrentInstance()) { onUnmounted(() => { channel?.close() }) } @@ -53,15 +53,15 @@ export function useCookie (name: string, _opts?: } if (opts.watch) { - watch(cookie, (newVal, oldVal) => { - if (watchPaused || isEqual(newVal, oldVal)) { return } + watch(cookie, () => { + if (watchPaused) { return } callback() }, { deep: opts.watch !== 'shallow' }) } else { callback() } - } else if (process.server) { + } else if (import.meta.server) { const nuxtApp = useNuxtApp() const writeFinalCookieValue = () => { if (!isEqual(cookie.value, cookies[name])) { @@ -79,9 +79,9 @@ export function useCookie (name: string, _opts?: } function readRawCookies (opts: CookieOptions = {}): Record | undefined { - if (process.server) { - return parse(useRequestEvent()?.node.req.headers.cookie || '', opts) - } else if (process.client) { + if (import.meta.server) { + return parse(getRequestHeader(useRequestEvent(), 'cookie') || '', opts) + } else if (import.meta.client) { return parse(document.cookie, opts) } } @@ -94,7 +94,7 @@ function serializeCookie (name: string, value: any, opts: CookieSerializeOptions } function writeClientCookie (name: string, value: any, opts: CookieSerializeOptions = {}) { - if (process.client) { + if (import.meta.client) { document.cookie = serializeCookie(name, value, opts) } } diff --git a/packages/nuxt/src/app/composables/error.ts b/packages/nuxt/src/app/composables/error.ts index b23fcd32ee57..4a59bc6a7a7c 100644 --- a/packages/nuxt/src/app/composables/error.ts +++ b/packages/nuxt/src/app/composables/error.ts @@ -14,7 +14,7 @@ export const showError = (_err: string | Error | Partial) => { try { const nuxtApp = useNuxtApp() const error = useError() - if (process.client) { + if (import.meta.client) { nuxtApp.hooks.callHook('app:error', err) } error.value = error.value || err diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 3ca36e005a7f..e70a1d690778 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -1,18 +1,25 @@ -import type { FetchError } from 'ofetch' -import type { AvailableRouterMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack' +import type { FetchError, FetchOptions } from 'ofetch' +import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack' import type { Ref } from 'vue' import { computed, reactive, unref } from 'vue' import { hash } from 'ohash' import { useRequestFetch } from './ssr' -import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom, _Transform } from './asyncData' +import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData' import { useAsyncData } from './asyncData' -export type FetchResult> = TypedInternalResponse +// support uppercase methods, detail: https://github.com/nuxt/nuxt/issues/22313 +type AvailableRouterMethod = _AvailableRouterMethod | Uppercase<_AvailableRouterMethod> + +export type FetchResult> = TypedInternalResponse> type ComputedOptions> = { [K in keyof T]: T[K] extends Function ? T[K] : T[K] extends Record ? ComputedOptions | Ref | T[K] : Ref | T[K] } +interface NitroFetchOptions = AvailableRouterMethod> extends FetchOptions { + method?: M; +} + type ComputedFetchOptions> = ComputedOptions> export interface UseFetchOptions< @@ -127,7 +134,7 @@ export function useFetch< const isLocalFetch = typeof _request.value === 'string' && _request.value.startsWith('/') let _$fetch = opts.$fetch || globalThis.$fetch // Use fetch with request context and headers for server direct API calls - if (process.server && !opts.$fetch && isLocalFetch) { + if (import.meta.server && !opts.$fetch && isLocalFetch) { _$fetch = useRequestFetch() } diff --git a/packages/nuxt/src/app/composables/hydrate.ts b/packages/nuxt/src/app/composables/hydrate.ts index 447318d76a2b..3aa4053402c0 100644 --- a/packages/nuxt/src/app/composables/hydrate.ts +++ b/packages/nuxt/src/app/composables/hydrate.ts @@ -11,13 +11,13 @@ import type { NuxtPayload } from '#app' export const useHydration = (key: K, get: () => T, set: (value: T) => void) => { const nuxt = useNuxtApp() - if (process.server) { + if (import.meta.server) { nuxt.hooks.hook('app:rendered', () => { nuxt.payload[key] = get() }) } - if (process.client) { + if (import.meta.client) { nuxt.hooks.hook('app:created', () => { set(nuxt.payload[key] as T) }) diff --git a/packages/nuxt/src/app/composables/payload.ts b/packages/nuxt/src/app/composables/payload.ts index 219f03582f7e..7d0e782dc903 100644 --- a/packages/nuxt/src/app/composables/payload.ts +++ b/packages/nuxt/src/app/composables/payload.ts @@ -13,7 +13,7 @@ interface LoadPayloadOptions { } export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record | Promise> | null { - if (process.server) { return null } + if (import.meta.server) { return null } const payloadURL = _getPayloadURL(url, opts) const nuxtApp = useNuxtApp() const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {} @@ -55,7 +55,7 @@ function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) { } async function _importPayload (payloadURL: string) { - if (process.server) { return null } + if (import.meta.server) { return null } try { return renderJsonPayloads ? parsePayload(await fetch(payloadURL).then(res => res.text())) @@ -74,7 +74,7 @@ export function isPrerendered () { let payloadCache: any = null export async function getNuxtClientPayload () { - if (process.server) { + if (import.meta.server) { return } if (payloadCache) { @@ -110,7 +110,7 @@ export function definePayloadReducer ( name: string, reduce: (data: any) => any ) { - if (process.server) { + if (import.meta.server) { useNuxtApp().ssrContext!._payloadReducers[name] = reduce } } @@ -122,12 +122,12 @@ export function definePayloadReducer ( */ export function definePayloadReviver ( name: string, - revive: (data: string) => any | undefined + revive: (data: any) => any | undefined ) { - if (process.dev && getCurrentInstance()) { + if (import.meta.dev && getCurrentInstance()) { console.warn('[nuxt] [definePayloadReviver] This function must be called in a Nuxt plugin that is `unshift`ed to the beginning of the Nuxt plugins array.') } - if (process.client) { + if (import.meta.client) { useNuxtApp()._payloadRevivers[name] = revive } } diff --git a/packages/nuxt/src/app/composables/preload.ts b/packages/nuxt/src/app/composables/preload.ts index 6710ae2b8210..b012accc51c8 100644 --- a/packages/nuxt/src/app/composables/preload.ts +++ b/packages/nuxt/src/app/composables/preload.ts @@ -9,7 +9,7 @@ import { useRouter } from './router' * @param components Pascal-cased name or names of components to prefetch */ export const preloadComponents = async (components: string | string[]) => { - if (process.server) { return } + if (import.meta.server) { return } const nuxtApp = useNuxtApp() components = Array.isArray(components) ? components : [components] @@ -35,7 +35,7 @@ function _loadAsyncComponent (component: Component) { } export async function preloadRouteComponents (to: RouteLocationRaw, router: Router & { _routePreloaded?: Set; _preloadPromises?: Array> } = useRouter()): Promise { - if (process.server) { return } + if (import.meta.server) { return } const { path, matched } = router.resolve(to) diff --git a/packages/nuxt/src/app/composables/ready.ts b/packages/nuxt/src/app/composables/ready.ts index 58143d18403d..a30d0d7cafef 100644 --- a/packages/nuxt/src/app/composables/ready.ts +++ b/packages/nuxt/src/app/composables/ready.ts @@ -2,7 +2,7 @@ import { useNuxtApp } from '../nuxt' import { requestIdleCallback } from '../compat/idle-callback' export const onNuxtReady = (callback: () => any) => { - if (process.server) { return } + if (import.meta.server) { return } const nuxtApp = useNuxtApp() if (nuxtApp.isHydrating) { diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index 905ffe63a8bf..0ffead1cdd48 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -17,7 +17,7 @@ export const useRouter: typeof _useRouter = () => { } export const useRoute: typeof _useRoute = () => { - if (process.dev && isProcessingMiddleware()) { + if (import.meta.dev && isProcessingMiddleware()) { console.warn('[nuxt] Calling `useRoute` within middleware may lead to misleading results. Instead, use the (to, from) arguments passed to the middleware to access the new and old routes.') } if (hasInjectionContext()) { @@ -118,7 +118,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na // Early open handler if (options?.open) { - if (process.client) { + if (import.meta.client) { const { target = '_blank', windowFeatures = {} } = options.open const features = Object.entries(windowFeatures) @@ -146,7 +146,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na const inMiddleware = isProcessingMiddleware() // Early redirect on client-side - if (process.client && !isExternal && inMiddleware) { + if (import.meta.client && !isExternal && inMiddleware) { return to } @@ -154,7 +154,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na const nuxtApp = useNuxtApp() - if (process.server) { + if (import.meta.server) { if (nuxtApp.ssrContext) { const fullPath = typeof to === 'string' || isExternal ? toPath : router.resolve(to).fullPath || '/' const location = isExternal ? toPath : joinURL(useRuntimeConfig().app.baseURL, fullPath) @@ -206,7 +206,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na /** This will abort navigation within a Nuxt route middleware handler. */ export const abortNavigation = (err?: string | Partial) => { - if (process.dev && !isProcessingMiddleware()) { + if (import.meta.dev && !isProcessingMiddleware()) { throw new Error('abortNavigation() is only usable inside a route middleware handler.') } @@ -221,19 +221,19 @@ export const abortNavigation = (err?: string | Partial) => { throw err } -export const setPageLayout = (layout: string) => { - if (process.server) { - if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) { +export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => { + if (import.meta.server) { + if (import.meta.dev && getCurrentInstance() && useState('_layout').value !== layout) { console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.') } useState('_layout').value = layout } const nuxtApp = useNuxtApp() - if (process.dev && nuxtApp.isHydrating && nuxtApp.payload.serverRendered && useState('_layout').value !== layout) { + if (import.meta.dev && nuxtApp.isHydrating && nuxtApp.payload.serverRendered && useState('_layout').value !== layout) { console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout during hydration as this will cause hydration errors.') } const inMiddleware = isProcessingMiddleware() - if (inMiddleware || process.server || nuxtApp.isHydrating) { + if (inMiddleware || import.meta.server || nuxtApp.isHydrating) { const unsubscribe = useRouter().beforeResolve((to) => { to.meta.layout = layout as Exclude unsubscribe() diff --git a/packages/nuxt/src/app/composables/ssr.ts b/packages/nuxt/src/app/composables/ssr.ts index 9b2e883af442..f81b0d8d5c35 100644 --- a/packages/nuxt/src/app/composables/ssr.ts +++ b/packages/nuxt/src/app/composables/ssr.ts @@ -1,14 +1,14 @@ - import type { H3Event } from 'h3' -import { setResponseStatus as _setResponseStatus } from 'h3' +import { setResponseStatus as _setResponseStatus, getRequestHeaders } from 'h3' import type { NuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt' export function useRequestHeaders (include: K[]): { [key in Lowercase]?: string } export function useRequestHeaders (): Readonly> export function useRequestHeaders (include?: any[]) { - if (process.client) { return {} } - const headers = useNuxtApp().ssrContext?.event.node.req.headers ?? {} + if (import.meta.client) { return {} } + const event = useNuxtApp().ssrContext?.event + const headers = event ? getRequestHeaders(event) : {} if (!include) { return headers } return Object.fromEntries(include.map(key => key.toLowerCase()).filter(key => headers[key]).map(key => [key, headers[key]])) } @@ -18,7 +18,7 @@ export function useRequestEvent (nuxtApp: NuxtApp = useNuxtApp()): H3Event { } export function useRequestFetch (): typeof global.$fetch { - if (process.client) { + if (import.meta.client) { return globalThis.$fetch } const event = useNuxtApp().ssrContext?.event as H3Event @@ -29,7 +29,7 @@ export function setResponseStatus (event: H3Event, code?: number, message?: stri /** @deprecated Pass `event` as first option. */ export function setResponseStatus (code: number, message?: string): void export function setResponseStatus (arg1: H3Event | number | undefined, arg2?: number | string, arg3?: string) { - if (process.client) { return } + if (import.meta.client) { return } if (arg1 && typeof arg1 !== 'number') { return _setResponseStatus(arg1, arg2 as number | undefined, arg3) } diff --git a/packages/nuxt/src/app/composables/url.ts b/packages/nuxt/src/app/composables/url.ts index 4f43ba8481c3..784ef860cd35 100644 --- a/packages/nuxt/src/app/composables/url.ts +++ b/packages/nuxt/src/app/composables/url.ts @@ -4,7 +4,7 @@ import { useRequestEvent } from './ssr' import { useRuntimeConfig } from '#app' export function useRequestURL () { - if (process.server) { + if (import.meta.server) { const url = getRequestURL(useRequestEvent()) url.pathname = joinURL(useRuntimeConfig().app.baseURL, url.pathname) return url diff --git a/packages/nuxt/src/app/config.ts b/packages/nuxt/src/app/config.ts index 77efe98805a9..3b8a86a2bce4 100644 --- a/packages/nuxt/src/app/config.ts +++ b/packages/nuxt/src/app/config.ts @@ -38,7 +38,7 @@ function deepAssign (obj: any, newObj: any) { export function useAppConfig (): AppConfig { const nuxtApp = useNuxtApp() if (!nuxtApp._appConfig) { - nuxtApp._appConfig = (process.server ? klona(__appConfig) : reactive(__appConfig)) as AppConfig + nuxtApp._appConfig = (import.meta.server ? klona(__appConfig) : reactive(__appConfig)) as AppConfig } return nuxtApp._appConfig } @@ -54,7 +54,7 @@ export function updateAppConfig (appConfig: DeepPartial) { } // HMR Support -if (process.dev) { +if (import.meta.dev) { function applyHMR (newConfig: AppConfig) { const appConfig = useAppConfig() if (newConfig && appConfig) { diff --git a/packages/nuxt/src/app/entry.async.ts b/packages/nuxt/src/app/entry.async.ts index 4d5b6206ab06..783151a0628c 100644 --- a/packages/nuxt/src/app/entry.async.ts +++ b/packages/nuxt/src/app/entry.async.ts @@ -1,10 +1,10 @@ import type { CreateOptions } from '#app' -const entry = process.server +const entry = import.meta.server ? (ctx?: CreateOptions['ssrContext']) => import('#app/entry').then(m => m.default(ctx)) : () => import('#app/entry').then(m => m.default) -if (process.client) { +if (import.meta.client) { entry() } diff --git a/packages/nuxt/src/app/entry.ts b/packages/nuxt/src/app/entry.ts index be990030b9d5..dede500a5045 100644 --- a/packages/nuxt/src/app/entry.ts +++ b/packages/nuxt/src/app/entry.ts @@ -16,7 +16,7 @@ import plugins from '#build/plugins' // @ts-expect-error virtual file import RootComponent from '#build/root-component.mjs' // @ts-expect-error virtual file -import { appRootId } from '#build/nuxt.config.mjs' +import { vueAppRootContainer } from '#build/nuxt.config.mjs' if (!globalThis.$fetch) { globalThis.$fetch = $fetch.create({ @@ -26,7 +26,7 @@ if (!globalThis.$fetch) { let entry: Function -if (process.server) { +if (import.meta.server) { entry = async function createNuxtAppServer (ssrContext: CreateOptions['ssrContext']) { const vueApp = createApp(RootComponent) @@ -45,10 +45,10 @@ if (process.server) { } } -if (process.client) { +if (import.meta.client) { // TODO: temporary webpack 5 HMR fix // https://github.com/webpack-contrib/webpack-hot-middleware/issues/390 - if (process.dev && import.meta.webpackHot) { + if (import.meta.dev && import.meta.webpackHot) { import.meta.webpackHot.accept() } @@ -75,7 +75,7 @@ if (process.client) { try { await nuxt.hooks.callHook('app:created', vueApp) await nuxt.hooks.callHook('app:beforeMount', vueApp) - vueApp.mount('#' + appRootId) + vueApp.mount(vueAppRootContainer) await nuxt.hooks.callHook('app:mounted', vueApp) await nextTick() } catch (err) { diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index fe8aed8f051d..9f91b855b455 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -10,22 +10,16 @@ import type { H3Event } from 'h3' import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema' import type { RenderResponse } from 'nitropack' +import type { MergeHead, VueHeadClient } from '@unhead/vue' // eslint-disable-next-line import/no-restricted-paths import type { NuxtIslandContext } from '../core/runtime/nitro/renderer' import type { RouteMiddleware } from '../../app' import type { NuxtError } from '../app/composables/error' import type { AsyncDataRequestStatus } from '../app/composables/asyncData' -const nuxtAppCtx = /* #__PURE__ */ getContext('nuxt-app') - -type NuxtMeta = { - htmlAttrs?: string - headAttrs?: string - bodyAttrs?: string - headTags?: string - bodyScriptsPrepend?: string - bodyScripts?: string -} +const nuxtAppCtx = /* #__PURE__ */ getContext('nuxt-app', { + asyncContext: !!process.env.NUXT_ASYNC_CONTEXT && process.server +}) type HookResult = Promise | void @@ -59,10 +53,10 @@ export interface NuxtSSRContext extends SSRContext { error?: boolean nuxt: _NuxtApp payload: NuxtPayload + head: VueHeadClient /** This is used solely to render runtime config with SPA renderer. */ config?: Pick teleports?: Record - renderMeta?: () => Promise | NuxtMeta islandContext?: NuxtIslandContext /** @internal */ _renderResponse?: Partial @@ -163,6 +157,16 @@ export interface PluginMeta { order?: number } +export interface PluginEnvContext { + /** + * This enable the plugin for islands components. + * Require `experimental.componentsIslands`. + * + * @default true + */ + islands?: boolean +} + export interface ResolvedPluginMeta { name?: string parallel?: boolean @@ -177,6 +181,7 @@ export interface Plugin = Record = Record> extends PluginMeta { hooks?: Partial setup?: Plugin + env?: PluginEnvContext /** * Execute plugin in parallel with other parallel plugins. * @@ -207,13 +212,13 @@ export function createNuxtApp (options: CreateOptions) { data: {}, state: {}, _errors: {}, - ...(process.client ? window.__NUXT__ ?? {} : { serverRendered: true }) + ...(import.meta.client ? window.__NUXT__ ?? {} : { serverRendered: true }) }), static: { data: {} }, runWithContext: (fn: any) => callWithNuxt(nuxtApp, fn), - isHydrating: process.client, + isHydrating: import.meta.client, deferHydration () { if (!nuxtApp.isHydrating) { return () => {} } @@ -241,7 +246,7 @@ export function createNuxtApp (options: CreateOptions) { nuxtApp.hooks = createHooks() nuxtApp.hook = nuxtApp.hooks.hook - if (process.server) { + if (import.meta.server) { async function contextCaller (hooks: HookCallback[], args: any[]) { for (const hook of hooks) { await nuxtApp.runWithContext(() => hook(...args)) @@ -264,7 +269,7 @@ export function createNuxtApp (options: CreateOptions) { defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp) defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp) - if (process.server) { + if (import.meta.server) { if (nuxtApp.ssrContext) { // Expose nuxt to the renderContext nuxtApp.ssrContext.nuxt = nuxtApp @@ -288,7 +293,7 @@ export function createNuxtApp (options: CreateOptions) { } // Listen to chunk load errors - if (process.client) { + if (import.meta.client) { window.addEventListener('nuxt.preloadError', (event) => { nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload }) }) @@ -302,7 +307,7 @@ export function createNuxtApp (options: CreateOptions) { } // Expose runtime config - const runtimeConfig = process.server ? options.ssrContext!.runtimeConfig : reactive(nuxtApp.payload.config!) + const runtimeConfig = import.meta.server ? options.ssrContext!.runtimeConfig : reactive(nuxtApp.payload.config!) nuxtApp.provide('config', runtimeConfig) return nuxtApp @@ -326,6 +331,7 @@ export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array[] = [] const errors: Error[] = [] for (const plugin of plugins) { + if (import.meta.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue } const promise = applyPlugin(nuxtApp, plugin) if (plugin.parallel) { parallels.push(promise.catch(e => errors.push(e))) @@ -359,7 +365,7 @@ export function isNuxtPlugin (plugin: unknown) { */ export function callWithNuxt any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters) { const fn: () => ReturnType = () => args ? setup(...args as Parameters) : setup() - if (process.server) { + if (import.meta.server) { return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn)) } else { // In client side we could assume nuxt app is singleton @@ -381,7 +387,7 @@ export function useNuxtApp (): NuxtApp { nuxtAppInstance = nuxtAppInstance || nuxtAppCtx.tryUse() if (!nuxtAppInstance) { - if (process.dev) { + if (import.meta.dev) { throw new Error('[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug. Find out more at `https://nuxt.com/docs/guide/concepts/auto-imports#using-vue-and-nuxt-composables`.') } else { throw new Error('[nuxt] instance unavailable') diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index ae4ba74952e9..06c5764c317f 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -99,7 +99,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({ name: 'nuxt:router', enforce: 'pre', setup (nuxtApp) { - const initialURL = process.client + const initialURL = import.meta.client ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash : nuxtApp.ssrContext!.url @@ -138,7 +138,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({ } // Perform navigation Object.assign(route, to) - if (process.client) { + if (import.meta.client) { window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath)) if (!nuxtApp.isHydrating) { // Clear any existing errors @@ -150,7 +150,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({ await middleware(to, route) } } catch (err) { - if (process.dev && !hooks.error.length) { + if (import.meta.dev && !hooks.error.length) { console.warn('No error handlers registered to handle middleware errors. You can register an error handler with `router.onError()`', err) } for (const handler of hooks.error) { @@ -211,7 +211,7 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({ } }) - if (process.client) { + if (import.meta.client) { window.addEventListener('popstate', (event) => { const location = (event.target as Window).location router.replace(location.href.replace(location.origin, '')) @@ -235,12 +235,12 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>({ } nuxtApp._processingMiddleware = true - if (process.client || !nuxtApp.ssrContext?.islandContext) { + if (import.meta.client || !nuxtApp.ssrContext?.islandContext) { const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global]) for (const middleware of middlewareEntries) { const result = await nuxtApp.runWithContext(() => middleware(to, from)) - if (process.server) { + if (import.meta.server) { if (result === false || result instanceof Error) { const error = result || createError({ statusCode: 404, diff --git a/packages/nuxt/src/app/types/augments.d.ts b/packages/nuxt/src/app/types/augments.d.ts index d93c85760c35..8a6c9c82d875 100644 --- a/packages/nuxt/src/app/types/augments.d.ts +++ b/packages/nuxt/src/app/types/augments.d.ts @@ -1,17 +1,20 @@ import type { NuxtApp, useNuxtApp } from '../nuxt' +interface NuxtStaticBuildFlags { + browser: boolean + client: boolean + dev: boolean + server: boolean + test: boolean +} + declare global { namespace NodeJS { - interface Process { - browser: boolean - client: boolean - dev: boolean - mode: 'spa' | 'universal' - server: boolean - static: boolean - } + interface Process extends NuxtStaticBuildFlags {} } + interface ImportMeta extends NuxtStaticBuildFlags {} + interface Window { __NUXT__?: Record useNuxtApp?: typeof useNuxtApp diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 94ecdac4762b..b0c858692e43 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -208,20 +208,20 @@ export default defineNuxtModule({ config.plugins = config.plugins || [] if (nuxt.options.experimental.treeshakeClientOnly && isServer) { config.plugins.push(TreeShakeTemplatePlugin.vite({ - sourcemap: nuxt.options.sourcemap[mode], + sourcemap: !!nuxt.options.sourcemap[mode], getComponents })) } config.plugins.push(clientFallbackAutoIdPlugin.vite({ - sourcemap: nuxt.options.sourcemap[mode], + sourcemap: !!nuxt.options.sourcemap[mode], rootDir: nuxt.options.rootDir })) config.plugins.push(loaderPlugin.vite({ - sourcemap: nuxt.options.sourcemap[mode], + sourcemap: !!nuxt.options.sourcemap[mode], getComponents, mode, transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined, - experimentalComponentIslands: nuxt.options.experimental.componentIslands + experimentalComponentIslands: !!nuxt.options.experimental.componentIslands })) if (isServer && nuxt.options.experimental.componentIslands) { @@ -252,20 +252,20 @@ export default defineNuxtModule({ config.plugins = config.plugins || [] if (nuxt.options.experimental.treeshakeClientOnly && mode === 'server') { config.plugins.push(TreeShakeTemplatePlugin.webpack({ - sourcemap: nuxt.options.sourcemap[mode], + sourcemap: !!nuxt.options.sourcemap[mode], getComponents })) } config.plugins.push(clientFallbackAutoIdPlugin.webpack({ - sourcemap: nuxt.options.sourcemap[mode], + sourcemap: !!nuxt.options.sourcemap[mode], rootDir: nuxt.options.rootDir })) config.plugins.push(loaderPlugin.webpack({ - sourcemap: nuxt.options.sourcemap[mode], + sourcemap: !!nuxt.options.sourcemap[mode], getComponents, mode, transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined, - experimentalComponentIslands: nuxt.options.experimental.componentIslands + experimentalComponentIslands: !!nuxt.options.experimental.componentIslands })) if (nuxt.options.experimental.componentIslands && mode === 'server') { diff --git a/packages/nuxt/src/components/runtime/server-component.ts b/packages/nuxt/src/components/runtime/server-component.ts index 4dcfa8cce502..777204209e91 100644 --- a/packages/nuxt/src/components/runtime/server-component.ts +++ b/packages/nuxt/src/components/runtime/server-component.ts @@ -5,9 +5,11 @@ export const createServerComponent = (name: string) => { return defineComponent({ name, inheritAttrs: false, - setup (_props, { attrs, slots }) { + props: { lazy: Boolean }, + setup (props, { attrs, slots }) { return () => h(NuxtIsland, { name, + lazy: props.lazy, props: attrs }, slots) } diff --git a/packages/nuxt/src/components/scan.ts b/packages/nuxt/src/components/scan.ts index 3ca401816ce3..cfee6e2ad01a 100644 --- a/packages/nuxt/src/components/scan.ts +++ b/packages/nuxt/src/components/scan.ts @@ -38,7 +38,8 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr const directory = basename(dir.path) if (!siblings.includes(directory)) { - const caseCorrected = siblings.find(sibling => sibling.toLowerCase() === directory.toLowerCase()) + const directoryLowerCase = directory.toLowerCase() + const caseCorrected = siblings.find(sibling => sibling.toLowerCase() === directoryLowerCase) if (caseCorrected) { const nuxt = useNuxt() const original = relative(nuxt.options.srcDir, dir.path) @@ -95,10 +96,7 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr const componentName = resolveComponentName(fileName, prefixParts) if (resolvedNames.has(componentName + suffix) || resolvedNames.has(componentName)) { - console.warn(`[nuxt] Two component files resolving to the same name \`${componentName}\`:\n` + - `\n - ${filePath}` + - `\n - ${resolvedNames.get(componentName)}` - ) + warnAboutDuplicateComponent(componentName, filePath, resolvedNames.get(componentName) || resolvedNames.get(componentName + suffix)!) continue } resolvedNames.set(componentName + suffix, filePath) @@ -136,10 +134,14 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr continue } - // Ignore component if component is already defined (with same mode) - if (!components.some(c => c.pascalName === component.pascalName && ['all', component.mode].includes(c.mode))) { - components.push(component) + const existingComponent = components.find(c => c.pascalName === component.pascalName && ['all', component.mode].includes(c.mode)) + if (existingComponent) { + // Ignore component if component is already defined (with same mode) + warnAboutDuplicateComponent(componentName, filePath, existingComponent.filePath) + continue } + + components.push(component) } scannedPaths.push(dir.path) } @@ -174,3 +176,10 @@ export function resolveComponentName (fileName: string, prefixParts: string[]) { return pascalCase(componentNameParts) + pascalCase(fileNameParts) } + +function warnAboutDuplicateComponent (componentName: string, filePath: string, duplicatePath: string) { + console.warn(`[nuxt] Two component files resolving to the same name \`${componentName}\`:\n` + + `\n - ${filePath}` + + `\n - ${duplicatePath}` + ) +} diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index ce9f8ee088a4..6bb86c50fb59 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/packages/nuxt/src/components/templates.ts @@ -36,20 +36,25 @@ export default defineNuxtPlugin({ export const componentsPluginTemplate: NuxtPluginTemplate = { filename: 'components.plugin.mjs', getContents ({ app }) { - const globalComponents = new Set() + const lazyGlobalComponents = new Set() + const syncGlobalComponents = new Set() for (const component of app.components) { - if (component.global) { - globalComponents.add(component.pascalName) + if (component.global === 'sync') { + syncGlobalComponents.add(component.pascalName) + } else if (component.global) { + lazyGlobalComponents.add(component.pascalName) } } - if (!globalComponents.size) { return emptyComponentsPlugin } + if (!lazyGlobalComponents.size && !syncGlobalComponents.size) { return emptyComponentsPlugin } - const components = [...globalComponents] + const lazyComponents = [...lazyGlobalComponents] + const syncComponents = [...syncGlobalComponents] return `import { defineNuxtPlugin } from '#app/nuxt' -import { ${components.map(c => 'Lazy' + c).join(', ')} } from '#components' +import { ${[...lazyComponents.map(c => 'Lazy' + c), ...syncComponents].join(', ')} } from '#components' const lazyGlobalComponents = [ - ${components.map(c => `["${c}", Lazy${c}]`).join(',\n')} + ${lazyComponents.map(c => `["${c}", Lazy${c}]`).join(',\n')}, + ${syncComponents.map(c => `["${c}", ${c}]`).join(',\n')} ] export default defineNuxtPlugin({ diff --git a/packages/nuxt/src/components/transform.ts b/packages/nuxt/src/components/transform.ts index 579cf42e7bd9..6d1a45af391d 100644 --- a/packages/nuxt/src/components/transform.ts +++ b/packages/nuxt/src/components/transform.ts @@ -47,7 +47,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT name: 'nuxt:components:imports', transformInclude (id) { id = normalize(id) - return id.startsWith('virtual:') || id.startsWith(nuxt.options.buildDir) || !isIgnored(id) + return id.startsWith('virtual:') || id.startsWith('\0virtual:') || id.startsWith(nuxt.options.buildDir) || !isIgnored(id) }, async transform (code, id) { // Virtual component wrapper diff --git a/packages/nuxt/src/core/app.ts b/packages/nuxt/src/core/app.ts index 565e5e29592c..e0680e271c72 100644 --- a/packages/nuxt/src/core/app.ts +++ b/packages/nuxt/src/core/app.ts @@ -1,4 +1,4 @@ -import { promises as fsp } from 'node:fs' +import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs' import { dirname, join, resolve } from 'pathe' import { defu } from 'defu' import { compileTemplate, findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils, tryResolveModule } from '@nuxt/kit' @@ -32,15 +32,21 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?: app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl)) // Compile templates into vfs + // TODO: remove utils in v4 const templateContext = { utils: templateUtils, nuxt, app } const filteredTemplates = (app.templates as Array>) .filter(template => !options.filter || options.filter(template)) - await Promise.all(filteredTemplates + const writes: Array<() => void> = [] + await Promise.allSettled(filteredTemplates .map(async (template) => { - const contents = await compileTemplate(template, templateContext) - const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!) + const mark = performance.mark(fullPath) + const contents = await compileTemplate(template, templateContext).catch((e) => { + console.error(`[nuxt] Could not compile template \`${template.filename}\`.`) + throw e + }) + nuxt.vfs[fullPath] = contents const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '') @@ -51,12 +57,25 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?: nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents } + const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL + const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL + + if (nuxt.options.debug || setupTime > 500) { + console.info(`[nuxt] compiled \`${template.filename}\` in ${setupTime}ms`) + } + if (template.write) { - await fsp.mkdir(dirname(fullPath), { recursive: true }) - await fsp.writeFile(fullPath, contents, 'utf8') + writes.push(() => { + mkdirSync(dirname(fullPath), { recursive: true }) + writeFileSync(fullPath, contents, 'utf8') + }) } })) + // Write template files in single synchronous step to avoid (possible) additional + // runtime overhead of cascading HMRs from vite/webpack + for (const write of writes) { write() } + await nuxt.callHook('app:templatesGenerated', app, filteredTemplates, options) } @@ -71,7 +90,7 @@ async function resolveApp (nuxt: Nuxt, app: NuxtApp) { ) } if (!app.mainComponent) { - app.mainComponent = (await tryResolveModule('@nuxt/ui-templates/templates/welcome.vue', nuxt.options.modulesDir))! + app.mainComponent = (await tryResolveModule('@nuxt/ui-templates/templates/welcome.vue', nuxt.options.modulesDir)) ?? '@nuxt/ui-templates/templates/welcome.vue' } // Resolve root component diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts index 2ba343d6e5a7..1a106c3094cd 100644 --- a/packages/nuxt/src/core/builder.ts +++ b/packages/nuxt/src/core/builder.ts @@ -6,9 +6,10 @@ import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit' import { interopDefault } from 'mlly' import { debounce } from 'perfect-debounce' import { normalize, relative, resolve } from 'pathe' -import type { Nuxt } from 'nuxt/schema' +import type { Nuxt, NuxtBuilder } from 'nuxt/schema' import { generateApp as _generateApp, createApp } from './app' +import { checkForExternalConfigurationFiles } from './external-config-files' export async function build (nuxt: Nuxt) { const app = createApp(nuxt) @@ -43,7 +44,7 @@ export async function build (nuxt: Nuxt) { await nuxt.callHook('build:before') if (!nuxt.options._prepare) { - await bundle(nuxt) + await Promise.all([checkForExternalConfigurationFiles(), bundle(nuxt)]) await nuxt.callHook('build:done') } @@ -178,23 +179,23 @@ async function bundle (nuxt: Nuxt) { ? await loadBuilder(nuxt, nuxt.options.builder) : nuxt.options.builder - return bundle(nuxt) + await bundle(nuxt) } catch (error: any) { await nuxt.callHook('build:error', error) if (error.toString().includes('Cannot find module \'@nuxt/webpack-builder\'')) { - throw new Error([ - 'Could not load `@nuxt/webpack-builder`. You may need to add it to your project dependencies, following the steps in `https://github.com/nuxt/framework/pull/2812`.' - ].join('\n')) + throw new Error('Could not load `@nuxt/webpack-builder`. You may need to add it to your project dependencies, following the steps in `https://github.com/nuxt/framework/pull/2812`.') } throw error } } -async function loadBuilder (nuxt: Nuxt, builder: string) { +async function loadBuilder (nuxt: Nuxt, builder: string): Promise { const builderPath = await tryResolveModule(builder, [nuxt.options.rootDir, import.meta.url]) - if (builderPath) { - return import(pathToFileURL(builderPath).href) + + if (!builderPath) { + throw new Error(`Loading \`${builder}\` builder failed. You can read more about the nuxt \`builder\` option at: \`https://nuxt.com/docs/api/configuration/nuxt-config#builder\``) } + return import(pathToFileURL(builderPath).href) } diff --git a/packages/nuxt/src/core/external-config-files.ts b/packages/nuxt/src/core/external-config-files.ts new file mode 100644 index 000000000000..26cc360fa79a --- /dev/null +++ b/packages/nuxt/src/core/external-config-files.ts @@ -0,0 +1,77 @@ +import { findPath } from '@nuxt/kit' +import { basename } from 'pathe' +import { generateApp as _generateApp } from './app' + +/** + * Check for those external configuration files that are not compatible with Nuxt, + * and warns the user about them. + * + * @see {@link https://nuxt.com/docs/getting-started/configuration#external-configuration-files} + */ +export async function checkForExternalConfigurationFiles () { + const checkResults = await Promise.all([checkViteConfig(), checkWebpackConfig(), checkNitroConfig(), checkPostCSSConfig()]) + const warningMessages = checkResults.filter(Boolean) as string[] + + if (!warningMessages.length) { + return + } + + const foundOneExternalConfig = warningMessages.length === 1 + if (foundOneExternalConfig) { + console.warn(warningMessages[0]) + } else { + const warningsAsList = warningMessages.map(message => `- ${message}`).join('\n') + const warning = `Found multiple external configuration files: \n\n${warningsAsList}` + console.warn(warning) + } +} + +async function checkViteConfig () { + // https://github.com/vitejs/vite/blob/8fe69524d25d45290179175ba9b9956cbce87a91/packages/vite/src/node/constants.ts#L38 + return await checkAndWarnAboutConfigFileExistence({ + fileName: 'vite.config', + extensions: ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'], + createWarningMessage: foundFile => `Using \`${foundFile}\` is not supported together with Nuxt. Use \`options.vite\` instead. You can read more in \`https://nuxt.com/docs/api/configuration/nuxt-config#vite\`.` + }) +} + +async function checkWebpackConfig () { + // https://webpack.js.org/configuration/configuration-languages/ + return await checkAndWarnAboutConfigFileExistence({ + fileName: 'webpack.config', + extensions: ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts', 'coffee'], + createWarningMessage: foundFile => `Using \`${foundFile}\` is not supported together with Nuxt. Use \`options.webpack\` instead. You can read more in \`https://nuxt.com/docs/api/configuration/nuxt-config#webpack-1\`.` + }) +} + +async function checkNitroConfig () { + // https://nitro.unjs.io/config#configuration + return await checkAndWarnAboutConfigFileExistence({ + fileName: 'nitro.config', + extensions: ['.ts', '.mts'], + createWarningMessage: foundFile => `Using \`${foundFile}\` is not supported together with Nuxt. Use \`options.nitro\` instead. You can read more in \`https://nuxt.com/docs/api/configuration/nuxt-config#nitro\`.` + }) +} + +async function checkPostCSSConfig () { + return await checkAndWarnAboutConfigFileExistence({ + fileName: 'postcss.config', + extensions: ['.js', '.cjs'], + createWarningMessage: foundFile => `Using \`${foundFile}\` is not supported together with Nuxt. Use \`options.postcss\` instead. You can read more in \`https://nuxt.com/docs/api/configuration/nuxt-config#postcss\`.` + }) +} + +interface CheckAndWarnAboutConfigFileExistenceOptions { + fileName: string, + extensions: string[], + createWarningMessage: (foundFile: string) => string +} + +async function checkAndWarnAboutConfigFileExistence (options: CheckAndWarnAboutConfigFileExistenceOptions) { + const { fileName, extensions, createWarningMessage } = options + + const configFile = await findPath(fileName, { extensions }).catch(() => null) + if (configFile) { + return createWarningMessage(basename(configFile)) + } +} diff --git a/packages/nuxt/src/core/features.ts b/packages/nuxt/src/core/features.ts index 96616c996af7..882700601cc5 100644 --- a/packages/nuxt/src/core/features.ts +++ b/packages/nuxt/src/core/features.ts @@ -1,14 +1,18 @@ import { addDependency } from 'nypm' -import { isPackageExists } from 'local-pkg' +import { resolvePackageJSON } from 'pkg-types' import { logger } from '@nuxt/kit' +import { isCI } from 'std-env' import prompts from 'prompts' export async function ensurePackageInstalled (rootDir: string, name: string, searchPaths?: string[]) { - if (isPackageExists(name, { paths: searchPaths })) { + if (await resolvePackageJSON(name, { url: searchPaths }).catch(() => null)) { return true } logger.info(`Package ${name} is missing`) + if (isCI) { + return false + } const { confirm } = await prompts({ type: 'confirm', diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index 812d92c3efec..73a7a8337727 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -3,13 +3,11 @@ import { cpus } from 'node:os' import { join, relative, resolve } from 'pathe' import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack' import type { Nitro, NitroConfig } from 'nitropack' -import { logger } from '@nuxt/kit' +import { logger, resolveIgnorePatterns } from '@nuxt/kit' import escapeRE from 'escape-string-regexp' import { defu } from 'defu' import fsExtra from 'fs-extra' import { dynamicEventHandler } from 'h3' -import { createHeadCore } from '@unhead/vue' -import { renderSSRHead } from '@unhead/ssr' import type { Nuxt } from 'nuxt/schema' // @ts-expect-error TODO: add legacy type support for subpath imports import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs' @@ -33,11 +31,10 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { : [/node_modules/] const spaLoadingTemplate = nuxt.options.spaLoadingTemplate ?? resolve(nuxt.options.srcDir, 'app/spa-loading-template.html') - if (spaLoadingTemplate && nuxt.options.spaLoadingTemplate && !existsSync(spaLoadingTemplate)) { + if (spaLoadingTemplate && nuxt.options.spaLoadingTemplate && typeof spaLoadingTemplate !== 'boolean' && !existsSync(spaLoadingTemplate)) { console.warn(`[nuxt] Could not load custom \`spaLoadingTemplate\` path as it does not exist: \`${spaLoadingTemplate}\`.`) } - // @ts-expect-error `typescriptBundlerResolution` coming in next nitro version const nitroConfig: NitroConfig = defu(_nitroConfig, { debug: nuxt.options.debug, rootDir: nuxt.options.rootDir, @@ -46,7 +43,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { dev: nuxt.options.dev, buildDir: nuxt.options.buildDir, experimental: { - // @ts-expect-error `typescriptBundlerResolution` coming in next nitro version + asyncContext: nuxt.options.experimental.asyncContext, typescriptBundlerResolution: nuxt.options.experimental.typescriptBundlerResolution || nuxt.options.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' || _nitroConfig.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' }, imports: { @@ -92,7 +89,9 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { '#spa-template': () => { if (!spaLoadingTemplate) { return 'export const template = ""' } try { - return `export const template = ${JSON.stringify(readFileSync(spaLoadingTemplate, 'utf-8'))}` + if (spaLoadingTemplate !== true) { + return `export const template = ${JSON.stringify(readFileSync(spaLoadingTemplate, 'utf-8'))}` + } } catch {} return `export const template = ${JSON.stringify(defaultSpaLoadingTemplate({}))}` } @@ -118,6 +117,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { tsConfig: { include: [ join(nuxt.options.buildDir, 'types/nitro-nuxt.d.ts') + ], + exclude: [ + ...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)), + // nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186 + relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')) ] } }, @@ -193,6 +197,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { 'process.env.NUXT_INLINE_STYLES': !!nuxt.options.experimental.inlineSSRStyles, 'process.env.NUXT_JSON_PAYLOADS': !!nuxt.options.experimental.renderJsonPayloads, 'process.env.NUXT_COMPONENT_ISLANDS': !!nuxt.options.experimental.componentIslands, + 'process.env.NUXT_ASYNC_CONTEXT': !!nuxt.options.experimental.asyncContext, 'process.dev': nuxt.options.dev, __VUE_PROD_DEVTOOLS__: false }, @@ -204,12 +209,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { // Resolve user-provided paths nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!) - - // Add head chunk for SPA renders - const head = createHeadCore() - head.push(nuxt.options.app.head) - const headChunk = await renderSSRHead(head) - nitroConfig.virtual!['#head-static'] = `export default ${JSON.stringify(headChunk)}` + nitroConfig.ignore = [...(nitroConfig.ignore || []), ...resolveIgnorePatterns(nitroConfig.srcDir)] // Add fallback server for `ssr: false` if (!nuxt.options.ssr) { @@ -281,6 +281,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { // Init nitro const nitro = await createNitro(nitroConfig) + // Set prerender-only options + nitro.options._config.storage ||= {} + nitro.options._config.storage['internal:nuxt:prerender'] = { driver: 'memory' } + nitro.options._config.storage['internal:nuxt:prerender:island'] = { driver: 'lruCache', max: 1000 } + nitro.options._config.storage['internal:nuxt:prerender:payload'] = { driver: 'lruCache', max: 1000 } + // Expose nitro to modules and kit nuxt._nitro = nitro await nuxt.callHook('nitro:init', nitro) @@ -405,3 +411,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { nuxt.hook('build:done', () => waitUntilCompile) } } + +function relativeWithDot (from: string, to: string) { + return relative(from, to).replace(/^([^.])/, './$1') || '.' +} diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 9aa9a65dfc7d..36ccb593be3b 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -1,4 +1,4 @@ -import { join, normalize, resolve } from 'pathe' +import { join, normalize, relative, resolve } from 'pathe' import { createDebugger, createHooks } from 'hookable' import type { LoadNuxtOptions } from '@nuxt/kit' import { addBuildPlugin, addComponent, addPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' @@ -16,6 +16,7 @@ import importsModule from '../imports/module' import { distDir, pkgDir } from '../dirs' import { version } from '../../package.json' import { ImportProtectionPlugin, vueAppPatterns } from './plugins/import-protection' +import type { UnctxTransformPluginOptions } from './plugins/unctx' import { UnctxTransformPlugin } from './plugins/unctx' import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake' import { TreeShakeComposablesPlugin } from './plugins/tree-shake' @@ -25,6 +26,8 @@ import { addModuleTranspiles } from './modules' import { initNitro } from './nitro' import schemaModule from './schema' import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata' +import { AsyncContextInjectionPlugin } from './plugins/async-context' +import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports' export function createNuxt (options: NuxtOptions): Nuxt { const hooks = createHooks() @@ -86,17 +89,20 @@ async function initNuxt (nuxt: Nuxt) { addVitePlugin(() => ImportProtectionPlugin.vite(config)) addWebpackPlugin(() => ImportProtectionPlugin.webpack(config)) + // add resolver for modules used in virtual files + addVitePlugin(() => resolveDeepImportsPlugin(nuxt)) + if (nuxt.options.experimental.localLayerAliases) { // Add layer aliasing support for ~, ~~, @ and @@ aliases addVitePlugin(() => LayerAliasingPlugin.vite({ - sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client, + sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, dev: nuxt.options.dev, root: nuxt.options.srcDir, // skip top-level layer (user's project) as the aliases will already be correctly resolved layers: nuxt.options._layers.slice(1) })) addWebpackPlugin(() => LayerAliasingPlugin.webpack({ - sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client, + sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, dev: nuxt.options.dev, root: nuxt.options.srcDir, // skip top-level layer (user's project) as the aliases will already be correctly resolved @@ -105,18 +111,21 @@ async function initNuxt (nuxt: Nuxt) { })) } - nuxt.hook('modules:done', () => { + nuxt.hook('modules:done', async () => { // Add unctx transform const options = { - sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client, - transformerOptions: nuxt.options.optimization.asyncTransforms - } + sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, + transformerOptions: { + ...nuxt.options.optimization.asyncTransforms, + helperModule: await tryResolveModule('unctx', nuxt.options.modulesDir) ?? 'unctx' + } + } satisfies UnctxTransformPluginOptions addVitePlugin(() => UnctxTransformPlugin.vite(options)) addWebpackPlugin(() => UnctxTransformPlugin.webpack(options)) // Add composable tree-shaking optimisations const serverTreeShakeOptions: TreeShakeComposablesPluginOptions = { - sourcemap: nuxt.options.sourcemap.server, + sourcemap: !!nuxt.options.sourcemap.server, composables: nuxt.options.optimization.treeShake.composables.server } if (Object.keys(serverTreeShakeOptions.composables).length) { @@ -124,7 +133,7 @@ async function initNuxt (nuxt: Nuxt) { addWebpackPlugin(() => TreeShakeComposablesPlugin.webpack(serverTreeShakeOptions), { client: false }) } const clientTreeShakeOptions: TreeShakeComposablesPluginOptions = { - sourcemap: nuxt.options.sourcemap.client, + sourcemap: !!nuxt.options.sourcemap.client, composables: nuxt.options.optimization.treeShake.composables.client } if (Object.keys(clientTreeShakeOptions.composables).length) { @@ -135,8 +144,13 @@ async function initNuxt (nuxt: Nuxt) { if (!nuxt.options.dev) { // DevOnly component tree-shaking - build time only - addVitePlugin(() => DevOnlyPlugin.vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) - addWebpackPlugin(() => DevOnlyPlugin.webpack({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client })) + addVitePlugin(() => DevOnlyPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client })) + addWebpackPlugin(() => DevOnlyPlugin.webpack({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client })) + } + + // Transform initial composable call within `` + - `` + const payload: Script = { + type: 'application/json', + id: opts.id, + innerHTML: contents, + 'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR) + } + if (opts.src) { + payload['data-src'] = opts.src + } + return [ + payload, + { + innerHTML: `window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}` + } + ] } -function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }) { +function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] { opts.data.config = opts.ssrContext.config - const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR + const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR if (_PAYLOAD_EXTRACTION) { - return `` + return [ + { + type: 'module', + innerHTML: `import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})` + } + ] } - return `` + return [ + { + innerHTML: `window.__NUXT__=${devalue(opts.data)}` + } + ] } function splitPayload (ssrContext: NuxtSSRContext) { diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index 55f3013dd280..cd8918a7c10a 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -280,10 +280,18 @@ export const appConfigTemplate: NuxtTemplate = { write: true, getContents: async ({ app, nuxt }) => { return ` +import { updateAppConfig } from '#app' import { defuFn } from '${await _resolveId('defu')}' const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)} +// Vite - webpack is handled directly in #app/config +if (import.meta.hot) { + import.meta.hot.accept((newModule) => { + updateAppConfig(newModule.default) + }) +} + ${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id)}`).join('\n')} export default /* #__PURE__ */ defuFn(${app.configs.map((_id: string, index: number) => `cfg${index}`).concat(['inlineConfig']).join(', ')}) @@ -313,7 +321,7 @@ export const publicPathTemplate: NuxtTemplate = { '}', // On server these are registered directly in packages/nuxt/src/core/runtime/nitro/renderer.ts - 'if (process.client) {', + 'if (import.meta.client) {', ' globalThis.__buildAssetsURL = buildAssetsURL', ' globalThis.__publicAssetsURL = publicAssetsURL', '}' @@ -329,8 +337,10 @@ export const nuxtConfigTemplate = { ...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`), `export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`, `export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`, + `export const remoteComponentIslands = ${ctx.nuxt.options.experimental.componentIslands === 'local+remote'}`, `export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`, - `export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}` + `export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`, + `export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}` ].join('\n\n') } } diff --git a/packages/nuxt/src/head/module.ts b/packages/nuxt/src/head/module.ts index fa8f83118d37..9131c015cecb 100644 --- a/packages/nuxt/src/head/module.ts +++ b/packages/nuxt/src/head/module.ts @@ -1,5 +1,5 @@ import { resolve } from 'pathe' -import { addComponent, addImportsSources, addPlugin, defineNuxtModule, tryResolveModule } from '@nuxt/kit' +import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, tryResolveModule } from '@nuxt/kit' import { distDir } from '../dirs' const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head', 'Html', 'Body'] @@ -54,6 +54,22 @@ export default defineNuxtModule({ addPlugin({ src: resolve(runtimeDir, 'plugins/vueuse-head-polyfill') }) } + addTemplate({ + filename: 'unhead-plugins.mjs', + getContents () { + if (!nuxt.options.experimental.headNext) { + return 'export default []' + } + return `import { CapoPlugin } from '@unhead/vue'; +export default process.server ? [CapoPlugin({ track: true })] : [];` + } + }) + + // template is only exposed in nuxt context, expose in nitro context as well + nuxt.hooks.hook('nitro:config', (config) => { + config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins'] + }) + // Add library-specific plugin addPlugin({ src: resolve(runtimeDir, 'plugins/unhead') }) } diff --git a/packages/nuxt/src/head/runtime/components.ts b/packages/nuxt/src/head/runtime/components.ts index 1c58c8fa140d..1bfe842a1717 100644 --- a/packages/nuxt/src/head/runtime/components.ts +++ b/packages/nuxt/src/head/runtime/components.ts @@ -149,7 +149,7 @@ export const Title = defineComponent({ name: 'Title', inheritAttrs: false, setup: setupForUseMeta((_, { slots }) => { - if (process.dev) { + if (import.meta.dev) { const defaultSlot = slots.default?.() if (defaultSlot && (defaultSlot.length > 1 || typeof defaultSlot[0].children !== 'string')) { @@ -217,7 +217,7 @@ export const Style = defineComponent({ const style = { ...props } const textContent = slots.default?.()?.[0]?.children if (textContent) { - if (process.dev && typeof textContent !== 'string') { + if (import.meta.dev && typeof textContent !== 'string') { console.error(' * ``` - * - * @type {string | false} + * @type {string | boolean} */ spaLoadingTemplate: { - $resolve: async (val, get) => typeof val === 'string' ? resolve(await get('srcDir'), val) : (val ?? null) + $resolve: async (val, get) => typeof val === 'string' ? resolve(await get('srcDir'), val) : (val ?? false) }, /** @@ -249,9 +259,7 @@ export default defineUntypedSchema({ * @note Plugins are also auto-registered from the `~/plugins` directory * and these plugins do not need to be listed in `nuxt.config` unless you * need to customize their order. All plugins are deduplicated by their src path. - * * @see https://nuxt.com/docs/guide/directory-structure/plugins - * * @example * ```js * plugins: [ diff --git a/packages/schema/src/config/build.ts b/packages/schema/src/config/build.ts index 0a9e61841b6a..072c4fead999 100644 --- a/packages/schema/src/config/build.ts +++ b/packages/schema/src/config/build.ts @@ -16,7 +16,7 @@ export default defineUntypedSchema({ } const map: Record = { vite: '@nuxt/vite-builder', - webpack: '@nuxt/webpack-builder', + webpack: '@nuxt/webpack-builder' } return map[val] || val || (await get('vite') === false ? map.webpack : map.vite) } @@ -25,7 +25,7 @@ export default defineUntypedSchema({ /** * Whether to generate sourcemaps. * - * @type {boolean | { server?: boolean, client?: boolean }} + * @type {boolean | { server?: boolean | 'hidden', client?: boolean | 'hidden' }} */ sourcemap: { $resolve: async (val, get) => { @@ -36,7 +36,7 @@ export default defineUntypedSchema({ server: true, client: await get('dev') }) - }, + } }, /** @@ -96,7 +96,6 @@ export default defineUntypedSchema({ * } * ] * ``` - * * @type {typeof import('../src/types/nuxt').NuxtTemplate[]} */ templates: [], @@ -113,7 +112,6 @@ export default defineUntypedSchema({ * } * ``` * @type {boolean | typeof import('webpack-bundle-analyzer').BundleAnalyzerPlugin.Options | typeof import('rollup-plugin-visualizer').PluginVisualizerOptions} - * */ analyze: { $resolve: async (val, get) => { @@ -128,7 +126,7 @@ export default defineUntypedSchema({ filename: join(analyzeDir, '{name}.html') } } - }, + } }, /** @@ -147,13 +145,13 @@ export default defineUntypedSchema({ * @type {Array<{ name: string, source?: string | RegExp, argumentLength: number }>} */ keyedComposables: { - $resolve: (val) => [ + $resolve: val => [ { name: 'defineNuxtComponent', argumentLength: 2 }, { name: 'useState', argumentLength: 2 }, { name: 'useFetch', argumentLength: 3 }, { name: 'useAsyncData', argumentLength: 3 }, { name: 'useLazyAsyncData', argumentLength: 3 }, - { name: 'useLazyFetch', argumentLength: 3 }, + { name: 'useLazyFetch', argumentLength: 3 } ].concat(val).filter(Boolean) }, @@ -172,18 +170,22 @@ export default defineUntypedSchema({ composables: { server: { $resolve: async (val, get) => defu(val || {}, - await get('dev') ? {} : { - vue: ['onBeforeMount', 'onMounted', 'onBeforeUpdate', 'onRenderTracked', 'onRenderTriggered', 'onActivated', 'onDeactivated', 'onBeforeUnmount'], - '#app': ['definePayloadReviver', 'definePageMeta'] - } + await get('dev') + ? {} + : { + vue: ['onBeforeMount', 'onMounted', 'onBeforeUpdate', 'onRenderTracked', 'onRenderTriggered', 'onActivated', 'onDeactivated', 'onBeforeUnmount'], + '#app': ['definePayloadReviver', 'definePageMeta'] + } ) }, client: { $resolve: async (val, get) => defu(val || {}, - await get('dev') ? {} : { - vue: ['onServerPrefetch', 'onRenderTracked', 'onRenderTriggered'], - '#app': ['definePayloadReducer', 'definePageMeta'] - } + await get('dev') + ? {} + : { + vue: ['onServerPrefetch', 'onRenderTracked', 'onRenderTriggered'], + '#app': ['definePayloadReducer', 'definePageMeta'] + } ) } } diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index d5801081f2d8..4311b6388048 100644 --- a/packages/schema/src/config/common.ts +++ b/packages/schema/src/config/common.ts @@ -14,7 +14,6 @@ export default defineUntypedSchema({ * You can use `github:`, `gitlab:`, `bitbucket:` or `https://` to extend from a remote git repository. * * @type {string|string[]} - * */ extends: null, @@ -26,7 +25,6 @@ export default defineUntypedSchema({ * You can use `github:`, `gitlab:`, `bitbucket:` or `https://` to extend from a remote git repository. * * @type {string} - * */ theme: null, @@ -170,7 +168,7 @@ export default defineUntypedSchema({ * */ debug: { - $resolve: async (val, get) => val ?? isDebug + $resolve: val => val ?? isDebug }, /** @@ -178,7 +176,7 @@ export default defineUntypedSchema({ * If set to `false` generated pages will have no content. */ ssr: { - $resolve: (val) => val ?? true, + $resolve: val => val ?? true }, /** @@ -191,7 +189,6 @@ export default defineUntypedSchema({ * (in `node_modules`) and then will be resolved from project `srcDir` if `~` alias is used. * * @note Modules are executed sequentially so the order is important. - * * @example * ```js * modules: [ @@ -252,12 +249,12 @@ export default defineUntypedSchema({ * and copied across into your `dist` folder when your app is generated. */ public: { - $resolve: async (val, get) => val || await get('dir.static') || 'public', + $resolve: async (val, get) => val || await get('dir.static') || 'public' }, static: { $schema: { deprecated: 'use `dir.public` option instead' }, - $resolve: async (val, get) => val || await get('dir.public') || 'public', + $resolve: async (val, get) => val || await get('dir.public') || 'public' } }, @@ -274,11 +271,9 @@ export default defineUntypedSchema({ * * @note Within a webpack context (image sources, CSS - but not JavaScript) you _must_ access * your alias by prefixing it with `~`. - * * @note These aliases will be automatically added to the generated `.nuxt/tsconfig.json` so you can get full * type support and path auto-complete. In case you need to extend options provided by `./.nuxt/tsconfig.json` * further, make sure to add them here or within the `typescript.tsConfig` property in `nuxt.config`. - * * @example * ```js * export default { @@ -309,7 +304,6 @@ export default defineUntypedSchema({ * } * * ``` - * * @type {Record} */ alias: { @@ -328,7 +322,6 @@ export default defineUntypedSchema({ * Pass options directly to `node-ignore` (which is used by Nuxt to ignore files). * * @see [node-ignore](https://github.com/kaelzhang/node-ignore) - * * @example * ```js * ignoreOptions: { @@ -343,7 +336,7 @@ export default defineUntypedSchema({ * building if its filename starts with the prefix specified by `ignorePrefix`. */ ignorePrefix: { - $resolve: (val) => val ?? '-', + $resolve: val => val ?? '-' }, /** @@ -365,13 +358,14 @@ export default defineUntypedSchema({ /** * The watch property lets you define patterns that will restart the Nuxt dev server when changed. * - * It is an array of strings or regular expressions, which will be matched against the file path - * relative to the project `srcDir`. + * It is an array of strings or regular expressions. Strings should be either absolute paths or + * relative to the `srcDir` (and the `srcDir` of any layers). Regular expressions will be matched + * against the path relative to the project `srcDir` (and the `srcDir` of any layers). * * @type {Array} */ watch: { - $resolve: val => [].concat(val).filter((b: unknown) => typeof b === 'string' || b instanceof RegExp), + $resolve: val => [].concat(val).filter((b: unknown) => typeof b === 'string' || b instanceof RegExp) }, /** @@ -384,7 +378,7 @@ export default defineUntypedSchema({ * `watchOptions` to pass directly to webpack. * * @see [webpack@4 watch options](https://v4.webpack.js.org/configuration/watch/#watchoptions). - * */ + */ webpack: { aggregateTimeout: 1000 }, @@ -463,7 +457,7 @@ export default defineUntypedSchema({ app: { baseURL: (await get('app')).baseURL, buildAssetsDir: (await get('app')).buildAssetsDir, - cdnURL: (await get('app')).cdnURL, + cdnURL: (await get('app')).cdnURL } }) } diff --git a/packages/schema/src/config/dev.ts b/packages/schema/src/config/dev.ts index 5f46df1949a8..a9bc37d71846 100644 --- a/packages/schema/src/config/dev.ts +++ b/packages/schema/src/config/dev.ts @@ -17,10 +17,7 @@ export default defineUntypedSchema({ * } * }) * ``` - * - * * @type {boolean | { key: string; cert: string }} - * */ https: false, @@ -43,6 +40,6 @@ export default defineUntypedSchema({ * * @type {(data: { loading?: string }) => string} */ - loadingTemplate: loadingTemplate + loadingTemplate } }) diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 17507c8f4eca..66ef5d69ad7b 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -6,11 +6,12 @@ export default defineUntypedSchema({ * Set to true to generate an async entry point for the Vue bundle (for module federation support). */ asyncEntry: { - $resolve: (val) => val ?? false + $resolve: val => val ?? false }, /** * Enable Vue's reactivity transform + * * @see https://vuejs.org/guide/extras/reactivity-transform.html * * Warning: Reactivity transform feature has been marked as deprecated in Vue 3.3 and is planned to be @@ -19,16 +20,18 @@ export default defineUntypedSchema({ */ reactivityTransform: false, - // TODO: Remove in v3.6 when nitro has support for mocking traced dependencies + // TODO: Remove in v3.8 when nitro has support for mocking traced dependencies // https://github.com/unjs/nitro/issues/1118 /** * Externalize `vue`, `@vue/*` and `vue-router` when building. + * * @see https://github.com/nuxt/nuxt/issues/13632 */ externalVue: true, /** * Tree shakes contents of client-only components from server bundle. + * * @see https://github.com/nuxt/framework/pull/5750 */ treeshakeClientOnly: true, @@ -47,7 +50,7 @@ export default defineUntypedSchema({ * @type {false | 'manual' | 'automatic'} */ emitRouteChunkError: { - $resolve: val => { + $resolve: (val) => { if (val === true) { return 'manual' } @@ -55,9 +58,20 @@ export default defineUntypedSchema({ return 'automatic' } return val ?? 'automatic' - }, + } }, + /** + * By default the route object returned by the auto-imported `useRoute()` composable + * is kept in sync with the current page in view in ``. This is not true for + * `vue-router`'s exported `useRoute` or for the default `$route` object available in your + * Vue templates. + * + * By enabling this option a mixin will be injected to keep the `$route` template object + * in sync with Nuxt's managed `useRoute()`. + */ + templateRouteInjection: true, + /** * Whether to restore Nuxt app state from `sessionStorage` when reloading the page * after a chunk error or manual `reloadNuxtApp()` call. @@ -102,7 +116,7 @@ export default defineUntypedSchema({ /** * Disable vue server renderer endpoint within nitro. - */ + */ noVueServer: false, /** @@ -137,8 +151,16 @@ export default defineUntypedSchema({ /** * Experimental component islands support with and .island.vue files. + * + * @type {true | 'local' | 'local+remote' | false} */ - componentIslands: false, + componentIslands: { + $resolve: (val) => { + if (typeof val === 'string') { return val } + if (val === true) { return 'local' } + return false + } + }, /** * Config schema support @@ -200,6 +222,34 @@ export default defineUntypedSchema({ * @see https://github.com/parcel-bundler/watcher * @type {'chokidar' | 'parcel' | 'chokidar-granular'} */ - watcher: 'chokidar-granular' + watcher: 'chokidar-granular', + + /** + * Enable native async context to be accessable for nested composables + * + * @see https://github.com/nuxt/nuxt/pull/20918 + */ + asyncContext: false, + + /** + * Use new experimental head optimisations: + * - Add the capo.js head plugin in order to render tags in of the head in a more performant way. + * - Uses the hash hydration plugin to reduce initial hydration + * + * @see https://github.com/nuxt/nuxt/discussions/22632 + */ + headNext: false, + + /** + * Allow defining `routeRules` directly within your `~/pages` directory using `defineRouteRules`. + * + * Rules are converted (based on the path) and applied for server requests. For example, a rule + * defined in `~/pages/foo/bar.vue` will be applied to `/foo/bar` requests. A rule in `~/pages/foo/[id].vue` + * will be applied to `/foo/**` requests. + * + * For more control, such as if you are using a custom `path` or `alias` set in the page's `definePageMeta`, you + * should set `routeRules` directly within your `nuxt.config`. + */ + inlineRouteRules: false } }) diff --git a/packages/schema/src/config/generate.ts b/packages/schema/src/config/generate.ts index 2ad0b5ad83fa..9882e6bb9b77 100644 --- a/packages/schema/src/config/generate.ts +++ b/packages/schema/src/config/generate.ts @@ -14,14 +14,13 @@ export default defineUntypedSchema({ * ```js * routes: ['/users/1', '/users/2', '/users/3'] * ``` - * * @type {string | string[]} */ routes: [], /** * This option is no longer used. Instead, use `nitro.prerender.ignore`. - * + * * @deprecated */ exclude: [] diff --git a/packages/schema/src/config/nitro.ts b/packages/schema/src/config/nitro.ts index 5e1a79cb76a1..359ce184614f 100644 --- a/packages/schema/src/config/nitro.ts +++ b/packages/schema/src/config/nitro.ts @@ -5,7 +5,6 @@ export default defineUntypedSchema({ * Configuration for Nitro. * * @see https://nitro.unjs.io/config/ - * * @type {typeof import('nitropack')['NitroConfig']} */ nitro: { @@ -21,9 +20,7 @@ export default defineUntypedSchema({ * Global route options applied to matching server routes. * * @experimental This is an experimental feature and API may change in the future. - * * @see https://nitro.unjs.io/config/#routerules - * * @type {typeof import('nitropack')['NitroConfig']['routeRules']} */ routeRules: {}, @@ -39,16 +36,13 @@ export default defineUntypedSchema({ * - lazy: Specifies whether to use lazy loading to import the handler. * * @see https://nuxt.com/docs/guide/directory-structure/server - * * @note Files from `server/api`, `server/middleware` and `server/routes` will be automatically registered by Nuxt. - * * @example * ```js * serverHandlers: [ * { route: '/path/foo/**:name', handler: '~/server/foohandler.ts' } * ] * ``` - * * @type {typeof import('nitropack')['NitroEventHandler'][]} */ serverHandlers: [], @@ -57,7 +51,6 @@ export default defineUntypedSchema({ * Nitro development-only server handlers. * * @see https://nitro.unjs.io/guide/routing - * * @type {typeof import('nitropack')['NitroDevEventHandler'][]} */ devServerHandlers: [] diff --git a/packages/schema/src/config/postcss.ts b/packages/schema/src/config/postcss.ts index ee9439724e89..29effcd4dd26 100644 --- a/packages/schema/src/config/postcss.ts +++ b/packages/schema/src/config/postcss.ts @@ -8,6 +8,7 @@ export default defineUntypedSchema({ * Options for configuring PostCSS plugins. * * https://postcss.org/ + * * @type {Record} */ plugins: { @@ -15,16 +16,18 @@ export default defineUntypedSchema({ * https://github.com/postcss/postcss-import */ 'postcss-import': { - $resolve: async (val, get) => val !== false ? defu(val || {}, { - resolve: createResolver({ - alias: { ...(await get('alias')) }, - modules: [ - await get('srcDir'), - await get('rootDir'), - ...(await get('modulesDir')) - ] + $resolve: async (val, get) => val !== false + ? defu(val || {}, { + resolve: createResolver({ + alias: { ...(await get('alias')) }, + modules: [ + await get('srcDir'), + await get('rootDir'), + ...(await get('modulesDir')) + ] + }) }) - }) : val, + : val }, /** diff --git a/packages/schema/src/config/router.ts b/packages/schema/src/config/router.ts index 3f10c1ceb066..279a609c87e1 100644 --- a/packages/schema/src/config/router.ts +++ b/packages/schema/src/config/router.ts @@ -11,7 +11,6 @@ export default defineUntypedSchema({ * * @see [documentation](https://router.vuejs.org/api/interfaces/routeroptions.html). * @type {typeof import('../src/types/router').RouterConfigSerializable} - * */ options: {} } diff --git a/packages/schema/src/config/typescript.ts b/packages/schema/src/config/typescript.ts index 5bf18adc5eac..29b7886edab1 100644 --- a/packages/schema/src/config/typescript.ts +++ b/packages/schema/src/config/typescript.ts @@ -24,7 +24,7 @@ export default defineUntypedSchema({ * @type {'vite' | 'webpack' | 'shared' | false | undefined} */ builder: { - $resolve: async (val, get) => val ?? null + $resolve: val => val ?? null }, /** @@ -39,13 +39,13 @@ export default defineUntypedSchema({ * Requires to install `typescript` and `vue-tsc` as dev dependencies. * * @see https://nuxt.com/docs/guide/concepts/typescript - * * @type {boolean | 'build'} */ typeCheck: false, /** * You can extend generated `.nuxt/tsconfig.json` using this option. + * * @type {typeof import('pkg-types')['TSConfig']} */ tsConfig: {}, diff --git a/packages/schema/src/config/vite.ts b/packages/schema/src/config/vite.ts index a991a654a255..be25e44cf4c6 100644 --- a/packages/schema/src/config/vite.ts +++ b/packages/schema/src/config/vite.ts @@ -22,7 +22,9 @@ export default defineUntypedSchema({ define: { $resolve: async (val, get) => ({ 'process.dev': await get('dev'), + 'import.meta.dev': await get('dev'), 'process.test': isTest, + 'import.meta.test': isTest, ...val || {} }) }, @@ -48,11 +50,11 @@ export default defineUntypedSchema({ }, script: { propsDestructure: { - $resolve: async (val, get) => val ?? Boolean((await get('vue')).propsDestructure), + $resolve: async (val, get) => val ?? Boolean((await get('vue')).propsDestructure) }, defineModel: { - $resolve: async (val, get) => val ?? Boolean((await get('vue')).defineModel), - }, + $resolve: async (val, get) => val ?? Boolean((await get('vue')).defineModel) + } } }, vueJsx: { diff --git a/packages/schema/src/config/webpack.ts b/packages/schema/src/config/webpack.ts index 4621e5909d4f..f8938ddda6e4 100644 --- a/packages/schema/src/config/webpack.ts +++ b/packages/schema/src/config/webpack.ts @@ -65,7 +65,6 @@ export default defineUntypedSchema({ * Extracting into multiple CSS files is better for caching and preload isolation. It * can also improve page performance by downloading and resolving only those resources * that are needed. - * * @example * ```js * export default { @@ -113,14 +112,12 @@ export default defineUntypedSchema({ * as most browsers will cache the asset and not detect the changes on first load. * * This example changes fancy chunk names to numerical ids: - * * @example * ```js * filenames: { * chunk: ({ isDev }) => (isDev ? '[name].js' : '[id].[contenthash].js') * } * ``` - * * @type { * Record< * string, @@ -140,8 +137,8 @@ export default defineUntypedSchema({ * } */ filenames: { - app: ({ isDev }: { isDev: boolean }) => isDev ? `[name].js` : `[contenthash:7].js`, - chunk: ({ isDev }: { isDev: boolean }) => isDev ? `[name].js` : `[contenthash:7].js`, + app: ({ isDev }: { isDev: boolean }) => isDev ? '[name].js' : '[contenthash:7].js', + chunk: ({ isDev }: { isDev: boolean }) => isDev ? '[name].js' : '[contenthash:7].js', css: ({ isDev }: { isDev: boolean }) => isDev ? '[name].css' : 'css/[contenthash:7].css', img: ({ isDev }: { isDev: boolean }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]', font: ({ isDev }: { isDev: boolean }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]', @@ -168,14 +165,15 @@ export default defineUntypedSchema({ /** * See https://github.com/esbuild-kit/esbuild-loader + * * @type {Omit} - */ + */ esbuild: {}, /** * See: https://github.com/webpack-contrib/file-loader#options - * @type {Omit} * + * @type {Omit} * @default * ```ts * { esModule: false } @@ -185,8 +183,8 @@ export default defineUntypedSchema({ /** * See: https://github.com/webpack-contrib/file-loader#options - * @type {Omit} * + * @type {Omit} * @default * ```ts * { esModule: false, limit: 1000 } @@ -196,8 +194,8 @@ export default defineUntypedSchema({ /** * See: https://github.com/webpack-contrib/file-loader#options - * @type {Omit} * + * @type {Omit} * @default * ```ts * { esModule: false, limit: 1000 } @@ -207,12 +205,14 @@ export default defineUntypedSchema({ /** * See: https://pugjs.org/api/reference.html#options + * * @type {typeof import('pug')['Options']} */ pugPlain: {}, /** * See [vue-loader](https://github.com/vuejs/vue-loader) for available options. + * * @type {Partial} */ vue: { @@ -224,13 +224,13 @@ export default defineUntypedSchema({ }, compilerOptions: { $resolve: async (val, get) => val ?? (await get('vue.compilerOptions')) }, propsDestructure: { $resolve: async (val, get) => val ?? Boolean(await get('vue.propsDestructure')) }, - defineModel: { $resolve: async (val, get) => val ?? Boolean(await get('vue.defineModel')) }, + defineModel: { $resolve: async (val, get) => val ?? Boolean(await get('vue.defineModel')) } }, css: { importLoaders: 0, url: { - filter: (url: string, resourcePath: string) => !url.startsWith('/'), + filter: (url: string, _resourcePath: string) => !url.startsWith('/') }, esModule: false }, @@ -238,7 +238,7 @@ export default defineUntypedSchema({ cssModules: { importLoaders: 0, url: { - filter: (url: string, resourcePath: string) => !url.startsWith('/'), + filter: (url: string, _resourcePath: string) => !url.startsWith('/') }, esModule: false, modules: { @@ -253,8 +253,8 @@ export default defineUntypedSchema({ /** * See: https://github.com/webpack-contrib/sass-loader#options + * * @type {typeof import('sass-loader')['Options']} - * * @default * ```ts * { @@ -272,6 +272,7 @@ export default defineUntypedSchema({ /** * See: https://github.com/webpack-contrib/sass-loader#options + * * @type {typeof import('sass-loader')['Options']} */ scss: {}, @@ -312,7 +313,6 @@ export default defineUntypedSchema({ * Defaults to true when `extractCSS` is enabled. * * @see [css-minimizer-webpack-plugin documentation](https://github.com/webpack-contrib/css-minimizer-webpack-plugin). - * * @type {false | typeof import('css-minimizer-webpack-plugin').BasePluginOptions & typeof import('css-minimizer-webpack-plugin').DefinedDefaultMinimizerAndOptions} */ optimizeCSS: { @@ -321,6 +321,7 @@ export default defineUntypedSchema({ /** * Configure [webpack optimization](https://webpack.js.org/configuration/optimization/). + * * @type {false | typeof import('webpack').Configuration['optimization']} */ optimization: { @@ -350,11 +351,12 @@ export default defineUntypedSchema({ plugins: { $resolve: async (val, get) => val ?? (await get('postcss.plugins')) } - }, + } }, /** * See [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) for available options. + * * @type {typeof import('webpack-dev-middleware').Options} */ devMiddleware: { @@ -363,6 +365,7 @@ export default defineUntypedSchema({ /** * See [webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware) for available options. + * * @type {typeof import('webpack-hot-middleware').MiddlewareOptions & { client?: typeof import('webpack-hot-middleware').ClientOptions }} */ hotMiddleware: {}, @@ -374,12 +377,14 @@ export default defineUntypedSchema({ /** * Filters to hide build warnings. + * * @type {Array<(warn: typeof import('webpack').WebpackError) => boolean>} */ warningIgnoreFilters: [], /** * Configure [webpack experiments](https://webpack.js.org/configuration/experiments/) + * * @type {false | typeof import('webpack').Configuration['experiments']} */ experiments: {} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 8178924cfb68..eebdb6e56b96 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -1,4 +1,3 @@ - // Types export * from './types/compatibility' export * from './types/components' diff --git a/packages/schema/src/types/compatibility.ts b/packages/schema/src/types/compatibility.ts index dabc01b1f085..63fe1d7fd98d 100644 --- a/packages/schema/src/types/compatibility.ts +++ b/packages/schema/src/types/compatibility.ts @@ -3,7 +3,6 @@ export interface NuxtCompatibility { * Required nuxt version in semver format. * * @example `^2.14.0` or `>=3.0.0-27219851.6e49637`. - * */ nuxt?: string @@ -12,7 +11,7 @@ export interface NuxtCompatibility { * * - `true`: When using Nuxt 2, using bridge module is required. * - `false`: When using Nuxt 2, using bridge module is not supported. - */ + */ bridge?: boolean } diff --git a/packages/schema/src/types/components.ts b/packages/schema/src/types/components.ts index 64eef5be174a..6ca1ab773f39 100644 --- a/packages/schema/src/types/components.ts +++ b/packages/schema/src/types/components.ts @@ -7,7 +7,7 @@ export interface Component { chunkName: string prefetch: boolean preload: boolean - global?: boolean + global?: boolean | 'sync' island?: boolean mode?: 'client' | 'server' | 'all' /** diff --git a/packages/schema/src/types/config.ts b/packages/schema/src/types/config.ts index 9465276a239f..cdfd08a68ba6 100644 --- a/packages/schema/src/types/config.ts +++ b/packages/schema/src/types/config.ts @@ -1,12 +1,12 @@ import type { KeepAliveProps, TransitionProps } from 'vue' -import type { ConfigSchema } from '../../schema/config' import type { ServerOptions as ViteServerOptions, UserConfig as ViteUserConfig } from 'vite' import type { Options as VuePluginOptions } from '@vitejs/plugin-vue' import type { Options as VueJsxPluginOptions } from '@vitejs/plugin-vue-jsx' -import type { AppHeadMetaObject } from './head' -import type { Nuxt } from './nuxt' import type { SchemaDefinition } from 'untyped' import type { NitroRuntimeConfig, NitroRuntimeConfigApp } from 'nitropack' +import type { ConfigSchema } from '../../schema/config' +import type { Nuxt } from './nuxt' +import type { AppHeadMetaObject } from './head' export type { SchemaDefinition } from 'untyped' type DeepPartial = T extends Function ? T : T extends Record ? { [P in keyof T]?: DeepPartial } : T @@ -63,22 +63,35 @@ type Overrideable, Path extends string = ''> = { : never } -/** User configuration in `nuxt.config` file */ -export interface NuxtConfig extends DeepPartial> { - // Avoid DeepPartial for vite config interface (#4772) - vite?: ConfigSchema['vite'] - runtimeConfig?: Overrideable - webpack?: ConfigSchema['webpack'] & { - $client?: ConfigSchema['webpack'] - $server?: ConfigSchema['webpack'] - } +// Runtime Config - /** - * Experimental custom config schema - * - * @see https://github.com/nuxt/nuxt/issues/15592 - */ - $schema?: SchemaDefinition +type RuntimeConfigNamespace = Record + +export interface PublicRuntimeConfig extends RuntimeConfigNamespace { } + +export interface RuntimeConfig extends RuntimeConfigNamespace { + app: NitroRuntimeConfigApp + /** Only available on the server. */ + nitro?: NitroRuntimeConfig['nitro'] + public: PublicRuntimeConfig +} + +// User configuration in `nuxt.config` file +export interface NuxtConfig extends DeepPartial> { + // Avoid DeepPartial for vite config interface (#4772) + vite?: ConfigSchema['vite'] + runtimeConfig?: Overrideable + webpack?: DeepPartial & { + $client?: DeepPartial + $server?: DeepPartial + } + + /** + * Experimental custom config schema + * + * @see https://github.com/nuxt/nuxt/issues/15592 + */ + $schema?: SchemaDefinition } // TODO: Expose ConfigLayer from c12 @@ -92,10 +105,14 @@ export type NuxtConfigLayer = ConfigLayer -/** Normalized Nuxt options available as `nuxt.options.*` */ +export interface NuxtBuilder { + bundle: (nuxt: Nuxt) => Promise +} + +// Normalized Nuxt options available as `nuxt.options.*` export interface NuxtOptions extends Omit { sourcemap: Required> - builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | { bundle: (nuxt: Nuxt) => Promise } + builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | NuxtBuilder webpack: ConfigSchema['webpack'] & { $client: ConfigSchema['webpack'] $server: ConfigSchema['webpack'] @@ -109,18 +126,21 @@ export interface ViteConfig extends Omit { entry?: string /** * Options passed to @vitejs/plugin-vue. + * * @see https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue */ vue?: VuePluginOptions /** * Options passed to @vitejs/plugin-vue-jsx. + * * @see https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue-jsx */ vueJsx?: VueJsxPluginOptions /** * Bundler for dev time server-side rendering. + * * @default 'vite-node' */ devBundler?: 'vite-node' | 'legacy' @@ -144,22 +164,7 @@ export interface ViteConfig extends Omit { publicDir?: never } - -// -- Runtime Config -- - -type RuntimeConfigNamespace = Record - -export interface PublicRuntimeConfig extends RuntimeConfigNamespace { } - -export interface RuntimeConfig extends RuntimeConfigNamespace { - app: NitroRuntimeConfigApp - /** Only available on the server. */ - nitro?: NitroRuntimeConfig['nitro'] - public: PublicRuntimeConfig -} - -// -- App Config -- - +// App Config export interface CustomAppConfig { [key: string]: unknown } diff --git a/packages/schema/src/types/hooks.ts b/packages/schema/src/types/hooks.ts index 6bd5fc3a910d..99d727939bbb 100644 --- a/packages/schema/src/types/hooks.ts +++ b/packages/schema/src/types/hooks.ts @@ -1,17 +1,17 @@ -import type { TSConfig } from 'pkg-types' import type { Server as HttpServer } from 'node:http' import type { Server as HttpsServer } from 'node:https' +import type { TSConfig } from 'pkg-types' import type { ViteDevServer } from 'vite' import type { Manifest } from 'vue-bundle-renderer' import type { EventHandler } from 'h3' import type { Import, InlinePreset, Unimport } from 'unimport' import type { Compiler, Configuration, Stats } from 'webpack' -import type { Nuxt, NuxtApp, ResolvedNuxtTemplate } from './nuxt' import type { Nitro, NitroConfig } from 'nitropack' -import type { Component, ComponentsOptions } from './components' -import type { NuxtCompatibility, NuxtCompatibilityIssues, ViteConfig } from '..' import type { Schema, SchemaDefinition } from 'untyped' import type { RouteLocationRaw } from 'vue-router' +import type { NuxtCompatibility, NuxtCompatibilityIssues, ViteConfig } from '..' +import type { Component, ComponentsOptions } from './components' +import type { Nuxt, NuxtApp, ResolvedNuxtTemplate } from './nuxt' export type HookResult = Promise | void @@ -65,6 +65,7 @@ export interface NuxtHooks { // Kit /** * Allows extending compatibility checks. + * * @param compatibility Compatibility object * @param issues Issues to be mapped * @returns Promise @@ -74,18 +75,21 @@ export interface NuxtHooks { // Nuxt /** * Called after Nuxt initialization, when the Nuxt instance is ready to work. + * * @param nuxt The configured Nuxt object * @returns Promise */ 'ready': (nuxt: Nuxt) => HookResult /** * Called when Nuxt instance is gracefully closing. + * * @param nuxt The configured Nuxt object * @returns Promise */ 'close': (nuxt: Nuxt) => HookResult /** * Called to restart the current Nuxt instance. + * * @returns Promise */ 'restart': (options?: { @@ -97,29 +101,34 @@ export interface NuxtHooks { /** * Called during Nuxt initialization, before installing user modules. + * * @returns Promise */ 'modules:before': () => HookResult /** * Called during Nuxt initialization, after installing user modules. + * * @returns Promise */ 'modules:done': () => HookResult /** * Called after resolving the `app` instance. + * * @param app The resolved `NuxtApp` object * @returns Promise */ 'app:resolve': (app: NuxtApp) => HookResult /** * Called during `NuxtApp` generation, to allow customizing, modifying or adding new files to the build directory (either virtually or to written to `.nuxt`). + * * @param app The configured `NuxtApp` object * @returns Promise */ 'app:templates': (app: NuxtApp) => HookResult /** * Called after templates are compiled into the [virtual file system](https://nuxt.com/docs/guide/directory-structure/nuxt#virtual-file-system) (vfs). + * * @param app The configured `NuxtApp` object * @returns Promise */ @@ -127,16 +136,19 @@ export interface NuxtHooks { /** * Called before Nuxt bundle builder. + * * @returns Promise */ 'build:before': () => HookResult /** * Called after Nuxt bundle builder is complete. + * * @returns Promise */ 'build:done': () => HookResult /** * Called during the manifest build by Vite and Webpack. This allows customizing the manifest that Nitro will use to render ` diff --git a/test/fixtures/basic-types/nuxt.config.ts b/test/fixtures/basic-types/nuxt.config.ts index f9ec4ab7ec45..972097ab4192 100644 --- a/test/fixtures/basic-types/nuxt.config.ts +++ b/test/fixtures/basic-types/nuxt.config.ts @@ -28,14 +28,6 @@ export default defineNuxtConfig({ } }, modules: [ - function (_, nuxt) { - // TODO: remove in v3.7 - if (process.env.TS_BASE_URL === 'without-base-url') { - nuxt.hook('prepare:types', ({ tsConfig }) => { - delete tsConfig.compilerOptions!.baseUrl - }) - } - }, function () { addTypeTemplate({ filename: 'test.d.ts', diff --git a/test/fixtures/basic-types/package.json b/test/fixtures/basic-types/package.json index 560bf99f7214..a286c1ef0db3 100644 --- a/test/fixtures/basic-types/package.json +++ b/test/fixtures/basic-types/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "ofetch": "latest", - "vitest": "latest", + "vitest": "0.33.0", "vue-router": "latest", "vue": "latest" } diff --git a/test/fixtures/basic-types/types.ts b/test/fixtures/basic-types/types.ts index c0b3f28d555a..d0a46e66fa61 100644 --- a/test/fixtures/basic-types/types.ts +++ b/test/fixtures/basic-types/types.ts @@ -56,9 +56,9 @@ describe('API routes', () => { it('works with useFetch', () => { expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf>() expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf>() - // @ts-expect-error TODO: remove when fixed upstream: https://github.com/unjs/nitro/pull/1247 expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf>() expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf>() + expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf>() expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf>() // @ts-expect-error not a valid method useFetch('/api/hey', { method: 'PATCH' }) diff --git a/test/fixtures/basic/.nuxtignore b/test/fixtures/basic/.nuxtignore new file mode 100644 index 000000000000..59e57a945400 --- /dev/null +++ b/test/fixtures/basic/.nuxtignore @@ -0,0 +1,3 @@ +composables/ignored.* +**/ignore/public-asset +server/routes/ignore/scanned.ts diff --git a/test/fixtures/basic/components/GlobalSync.vue b/test/fixtures/basic/components/GlobalSync.vue new file mode 100644 index 000000000000..9e075fed9ef6 --- /dev/null +++ b/test/fixtures/basic/components/GlobalSync.vue @@ -0,0 +1,5 @@ + diff --git a/test/fixtures/basic/components/clientFallback/StatefulSetup.vue b/test/fixtures/basic/components/clientFallback/StatefulSetup.vue index 1643e2007e86..43a63debca73 100644 --- a/test/fixtures/basic/components/clientFallback/StatefulSetup.vue +++ b/test/fixtures/basic/components/clientFallback/StatefulSetup.vue @@ -1,4 +1,3 @@ - diff --git a/test/fixtures/basic/composables/async-context.ts b/test/fixtures/basic/composables/async-context.ts new file mode 100644 index 000000000000..6ebdf88ec774 --- /dev/null +++ b/test/fixtures/basic/composables/async-context.ts @@ -0,0 +1,19 @@ +const delay = () => new Promise(resolve => setTimeout(resolve, 10)) + +export async function nestedAsyncComposable () { + await delay() + return await fn1() +} + +async function fn1 () { + await delay() + return await fn2() +} + +async function fn2 () { + await delay() + const app = useNuxtApp() + return { + hasApp: !!app + } +} diff --git a/test/fixtures/basic/composables/ignored.ts b/test/fixtures/basic/composables/ignored.ts new file mode 100644 index 000000000000..8ba085c8a059 --- /dev/null +++ b/test/fixtures/basic/composables/ignored.ts @@ -0,0 +1,3 @@ +export function useIgnoredImport () { + +} diff --git a/test/fixtures/basic/composables/tree-shake.ts b/test/fixtures/basic/composables/tree-shake.ts index f050457f95a8..d057c120f732 100644 --- a/test/fixtures/basic/composables/tree-shake.ts +++ b/test/fixtures/basic/composables/tree-shake.ts @@ -1,12 +1,12 @@ export function useServerOnlyComposable () { - if (process.client) { + if (import.meta.client) { throw new Error('this should not be called in the browser') } } export function useClientOnlyComposable () { // need to do some code that fails in node but not in the browser - if (process.server) { + if (import.meta.server) { throw new Error('this should not be called on the server') } } diff --git a/test/fixtures/basic/middleware/redirect.global.ts b/test/fixtures/basic/middleware/redirect.global.ts index abf48b1ebc36..9ca90920ff51 100644 --- a/test/fixtures/basic/middleware/redirect.global.ts +++ b/test/fixtures/basic/middleware/redirect.global.ts @@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async (to) => { return false } const pluginPath = nuxtApp.$path() - if (process.server && !/redirect|navigate/.test(pluginPath) && to.path !== pluginPath) { + if (import.meta.server && !/redirect|navigate/.test(pluginPath) && to.path !== pluginPath) { throw new Error('plugin did not run before middleware') } }) diff --git a/test/fixtures/basic/modules/runtime/page.vue b/test/fixtures/basic/modules/runtime/page.vue index 5174b019cb65..3c9029c07075 100644 --- a/test/fixtures/basic/modules/runtime/page.vue +++ b/test/fixtures/basic/modules/runtime/page.vue @@ -5,7 +5,7 @@ definePageMeta({ value: 'added in pages:extend' }) -if (process.server) { +if (import.meta.server) { setResponseHeader(useRequestEvent(), 'x-extend', useRoute().meta.value as string) } diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 82ea65ef0375..e375c2fac1c0 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -146,6 +146,13 @@ export default defineNuxtConfig({ filePath: '~/other-components-folder/named-export' }) }, + 'components:extend' (components) { + for (const comp of components) { + if (comp.pascalName === 'GlobalSync') { + comp.global = 'sync' + } + } + }, 'vite:extendConfig' (config) { config.plugins!.push({ name: 'nuxt:server', @@ -179,7 +186,6 @@ export default defineNuxtConfig({ experimental: { typedPages: true, polyfillVueUseHead: true, - renderJsonPayloads: process.env.TEST_PAYLOAD !== 'js', respectNoSSRHeader: true, clientFallback: true, restoreState: true, @@ -187,7 +193,10 @@ export default defineNuxtConfig({ componentIslands: true, reactivityTransform: true, treeshakeClientOnly: true, - payloadExtraction: true + payloadExtraction: true, + asyncContext: process.env.TEST_CONTEXT === 'async', + headNext: true, + inlineRouteRules: true }, appConfig: { fromNuxtConfig: true, diff --git a/test/fixtures/basic/other-composables-folder/local.ts b/test/fixtures/basic/other-composables-folder/local.ts new file mode 100644 index 000000000000..46858a277930 --- /dev/null +++ b/test/fixtures/basic/other-composables-folder/local.ts @@ -0,0 +1,5 @@ +function useAsyncData (s?: any) { return s } + +export const ShouldNotBeKeyed = (() => { + return useAsyncData() +})() diff --git a/test/fixtures/basic/pages/async-context.vue b/test/fixtures/basic/pages/async-context.vue new file mode 100644 index 000000000000..8979942d2bda --- /dev/null +++ b/test/fixtures/basic/pages/async-context.vue @@ -0,0 +1,12 @@ + + + diff --git a/test/fixtures/basic/pages/chunk-error.vue b/test/fixtures/basic/pages/chunk-error.vue index 5e7c5bf016de..36407f27f576 100644 --- a/test/fixtures/basic/pages/chunk-error.vue +++ b/test/fixtures/basic/pages/chunk-error.vue @@ -3,7 +3,7 @@ definePageMeta({ async middleware (to, from) { await new Promise(resolve => setTimeout(resolve, 1)) const nuxtApp = useNuxtApp() - if (process.client && from !== to && !nuxtApp.isHydrating) { + if (import.meta.client && from !== to && !nuxtApp.isHydrating) { // trigger a loading error when navigated to via client-side navigation await import(/* webpackIgnore: true */ /* @vite-ignore */ `some-non-exis${''}ting-module`) } diff --git a/test/fixtures/basic/pages/client-only-explicit-import.vue b/test/fixtures/basic/pages/client-only-explicit-import.vue index 23463cd4019f..af51dca04b1a 100644 --- a/test/fixtures/basic/pages/client-only-explicit-import.vue +++ b/test/fixtures/basic/pages/client-only-explicit-import.vue @@ -1,4 +1,3 @@ -