diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..68d3ffb9d6 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +FROM node:lts + +RUN apt-get update && \ + apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \ + apt-get clean autoclean && \ + apt-get autoremove --yes && \ + rm -rf /var/lib/{apt,dpkg,cache,log} + +RUN corepack enable && npx playwright install diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..cca355669d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +// https://code.visualstudio.com/docs/devcontainers/containers +// https://containers.dev/implementors/json_reference/ +{ + "name": "nuxt-devcontainer", + "dockerFile": "Dockerfile", + "features": {}, + "customizations": { + "vscode": { + "settings": {}, + "extensions": [ + "ms-azuretools.vscode-docker", + "dbaeumer.vscode-eslint", + "github.vscode-github-actions", + "vue.volar" + ] + } + }, + "postStartCommand": "pnpm install && pnpm build:stub", + "mounts": ["type=volume,target=${containerWorkspaceFolder}/node_modules"] +} diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 125155c9ed..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -dist -node_modules -schema -**/*.tmpl.* -sw.js diff --git a/.eslintrc b/.eslintrc index c78df72297..528bcc4a57 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,11 +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" - ], + "plugins": ["jsdoc", "no-only-tests"], "extends": [ "plugin:jsdoc/recommended", "@nuxtjs/eslint-config-typescript", @@ -18,8 +24,9 @@ "ignoreDeclarationSort": true } ], + "no-only-tests/no-only-tests": "error", "unicorn/prefer-node-protocol": "error", - "no-console": "off", + "no-console": "warn", "vue/multi-word-component-names": "off", "vue/one-component-per-file": "off", "vue/require-default-prop": "off", @@ -41,6 +48,10 @@ { "pattern": "@nuxt/test-utils", "group": "external" + }, + { + "pattern": "#vue-router", + "group": "external" } ] } @@ -73,6 +84,14 @@ "disallowTypeAnnotations": false } ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-expect-error": "allow-with-description", + "ts-ignore": true + } + ], + "@typescript-eslint/prefer-ts-expect-error": "error", "@typescript-eslint/no-unused-vars": [ "error", { @@ -80,8 +99,35 @@ "varsIgnorePattern": "^_", "ignoreRestSiblings": true } + ], + "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"] + } + ] + } + }, + { + "files": ["packages/nuxt/src/app/**", "test/**", "**/runtime/**"], + "rules": { + "no-console": "off" + } + } + ], "settings": { "jsdoc": { "ignoreInternal": true, diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index bbea7f9746..adb2612a93 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/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7a8914dca3..7b2d765c12 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,7 +4,7 @@ contact_links: url: https://nuxt.com/docs/ about: Check the documentation for usage of Nuxt 3 - name: 📚 Nuxt 2 Documentation - url: https://nuxtjs.org/ + url: https://v2.nuxt.com/ about: Check the documentation for usage of Nuxt 2 - name: 💬 Discussions url: https://github.com/nuxt/nuxt/discussions diff --git a/.github/ISSUE_TEMPLATE/z-bug-report-2.yml b/.github/ISSUE_TEMPLATE/z-bug-report-2.yml index ff81322879..d4124367b2 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/banner.svg b/.github/assets/banner.svg new file mode 100644 index 0000000000..9a7abed71b --- /dev/null +++ b/.github/assets/banner.svg @@ -0,0 +1 @@ + diff --git a/.github/assets/twitter.svg b/.github/assets/twitter.svg index 4940c90101..00212abdc6 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 0000000000..79ad109255 --- /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 prioritize 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 7c6840bcb6..40b944dc79 100644 --- a/.github/workflows/autofix-docs.yml +++ b/.github/workflows/autofix-docs.yml @@ -5,6 +5,9 @@ on: paths: - "docs/**" - ".github/workflows/docs.yml" + push: + branches: + - "renovate/**" permissions: contents: read @@ -14,11 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: 18 + node-version: 20 cache: "pnpm" - name: Install dependencies @@ -27,4 +30,4 @@ jobs: - name: Lint (docs) run: pnpm lint:docs:fix - - uses: autofix-ci/action@8bc06253bec489732e5f9c52884c7cace15c0160 + - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 3c47e98c69..7d067e44e5 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -13,18 +13,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: 18 + node-version: 20 cache: "pnpm" - name: Install dependencies run: pnpm install - name: Dedupe dependencies - if: ${{ contains(github.ref_name, 'renovate') }} + if: ${{ contains(github.head_ref, 'renovate') }} run: pnpm dedupe - name: Build (stub) @@ -36,7 +36,10 @@ jobs: - name: Test (unit) run: pnpm test:unit -u + - name: Run build + run: pnpm build + - name: Update bundle size run: pnpm vitest run bundle -u - - uses: autofix-ci/action@8bc06253bec489732e5f9c52884c7cace15c0160 + - uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc diff --git a/.github/workflows/changelogensets.yml b/.github/workflows/changelogensets.yml new file mode 100644 index 0000000000..845d1a03a1 --- /dev/null +++ b/.github/workflows/changelogensets.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + branches: + - main + - 2.x + +permissions: + pull-requests: write + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: ${{ github.event_name != 'push' }} + +jobs: + update-changelog: + if: github.repository_owner == 'nuxt' && !contains(github.event.head_commit.message, 'v3.') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + with: + fetch-depth: 0 + - run: corepack enable + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - run: pnpm jiti ./scripts/update-changelog.ts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 0000000000..e07d72cb8b --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,45 @@ +name: Check links with Lychee + +on: + pull_request: + paths: + - "docs/**" + - "*.md" + branches: + - main + +# Remove default permissions of GITHUB_TOKEN for security +# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: {} + +jobs: + link-checker: + runs-on: ubuntu-latest + steps: + # Cache lychee results (e.g. to avoid hitting rate limits) + - name: Restore lychee cache + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + # check links with Lychee + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + + - name: Lychee link checker + uses: lycheeverse/lychee-action@2ac9f030ccdea0033e2510a23a67da2a2da98492 # for v1.8.0 + with: + # arguments with file types to check + args: >- + --cache + --verbose + --no-progress + --max-cache-age=1d + './**/*.md' + './**/*.html' + # fail the action on broken links + fail: true + env: + # to be used in case rate limits are surpassed + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9773ce8f5d..5a490609d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,13 +4,16 @@ on: push: paths-ignore: - "docs/**" + - "*.md" branches: - main pull_request: paths-ignore: - "docs/**" + - "*.md" branches: - main + - "!v[0-9]*" # https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml env: @@ -35,11 +38,11 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: 18 + node-version: 20 cache: "pnpm" - name: Install dependencies @@ -55,37 +58,82 @@ jobs: run: pnpm build - name: Cache dist - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: retention-days: 3 name: dist path: packages/*/dist - typecheck: + codeql: runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + actions: read + contents: read + security-events: write + needs: + - build + + steps: + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - run: corepack enable + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Initialize CodeQL + uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + with: + languages: javascript + queries: +security-and-quality + + - name: Restore dist cache + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: dist + path: packages + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + with: + category: "/language:javascript" + + typecheck: + runs-on: ${{ matrix.os }} + timeout-minutes: 10 needs: - build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + module: ['bundler', 'node'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: 18 + node-version: 20 cache: "pnpm" - name: Install dependencies run: pnpm install - name: Restore dist cache - uses: actions/download-artifact@v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: dist path: packages - name: Test (types) run: pnpm test:types + env: + MODULE_RESOLUTION: ${{ matrix.module }} lint: # autofix workflow will be triggered instead for PRs @@ -94,11 +142,11 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: 18 + node-version: 20 cache: "pnpm" - name: Install dependencies @@ -121,17 +169,21 @@ jobs: os: [ubuntu-latest, windows-latest] env: ['dev', 'built'] builder: ['vite', 'webpack'] - node: [16] + context: ['async', 'default'] + manifest: ['manifest-on', 'manifest-off'] + node: [18] exclude: - env: 'dev' builder: 'webpack' + - manifest: 'manifest-off' + builder: 'webpack' - timeout-minutes: 10 + timeout-minutes: 15 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: ${{ matrix.node }} cache: "pnpm" @@ -155,7 +207,7 @@ jobs: echo "PLAYWRIGHT_VERSION=$env:PLAYWRIGHT_VERSION" >> $env:GITHUB_ENV - name: Cache Playwright's binary - uses: actions/cache@v3 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 with: key: ${{ runner.os }}-playwright-bin-v1-${{ env.PLAYWRIGHT_VERSION }} path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} @@ -163,11 +215,10 @@ jobs: ${{ runner.os }}-playwright-bin-v1- - name: Install Playwright - # does not need to explicitly set chromium after https://github.com/microsoft/playwright/issues/14862 is solved - run: pnpm playwright install chromium + run: pnpm playwright-core install chromium - name: Restore dist cache - uses: actions/download-artifact@v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: dist path: packages @@ -175,13 +226,21 @@ jobs: - name: Test (unit) run: pnpm test:unit + - name: Test (runtime unit) + run: pnpm test:runtime + - name: Test (fixtures) run: pnpm test:fixtures env: TEST_ENV: ${{ matrix.env }} TEST_BUILDER: ${{ matrix.builder }} + TEST_MANIFEST: ${{ matrix.manifest }} + 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: + id-token: write if: | github.event_name == 'push' && !contains(github.event.head_commit.message, '[skip-release]') && @@ -191,30 +250,24 @@ jobs: - lint - build - test-fixtures - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-latest] - node: [16] - + runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: fetch-depth: 0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: ${{ matrix.node }} + node-version: 20 cache: "pnpm" - name: Install dependencies run: pnpm install - name: Restore dist cache - uses: actions/download-artifact@v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: dist path: packages @@ -223,3 +276,43 @@ jobs: run: ./scripts/release-edge.sh env: NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} + NPM_CONFIG_PROVENANCE: true + + release-pr: + permissions: + id-token: write + pull-requests: write + if: | + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, '🧷 edge release') + needs: + - lint + - build + - test-fixtures + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + with: + fetch-depth: 0 + - run: corepack enable + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Restore dist cache + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: dist + path: packages + + - name: Release Edge + run: ./scripts/release-edge.sh pr-${{ github.event.issue.number }} + env: + NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} + NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..b0e0aa9bc7 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,22 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - name: 'Dependency Review' + uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 diff --git a/.github/workflows/docs-e2e.yml b/.github/workflows/docs-e2e.yml deleted file mode 100644 index f6aa4f6a05..0000000000 --- a/.github/workflows/docs-e2e.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: docs-e2e - -on: - workflow_dispatch: - inputs: - url: - required: false - description: The URL to run the test suite against. - type: string - deployment_status: - -jobs: - crawl-docs: - environment: - name: ${{ github.event.deployment.environment || 'Production' }} - url: ${{ github.event.inputs.url || github.event.deployment.payload.web_url || github.event.deployment_status.target_url }} - if: github.event.deployment_status.state == 'success' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - cache: "pnpm" - - - name: Install dependencies - run: pnpm install - - - run: node ./scripts/crawl.mjs - env: - BASE_URL: ${{ github.event.inputs.url || github.event.deployment.payload.web_url || github.event.deployment_status.target_url }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a2d96f9a06..d791010639 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,9 +5,11 @@ on: paths: - "docs/**" - ".github/workflows/docs.yml" + - "*.md" # autofix workflow will be triggered instead for PRs branches: - main + - "!v[0-9]*" # Remove default permissions of GITHUB_TOKEN for security # https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs @@ -18,11 +20,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - run: corepack enable - - uses: actions/setup-node@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: 18 + node-version: 20 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml new file mode 100644 index 0000000000..9cbd97021c --- /dev/null +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -0,0 +1,96 @@ +name: ecosystem-ci trigger + +on: + issue_comment: + types: [created] + +permissions: + pull-requests: write + +jobs: + trigger: + runs-on: ubuntu-latest + if: github.repository == 'nuxt/nuxt' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') + steps: + - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + with: + script: | + const user = context.payload.sender.login + console.log(`Validate user: ${user}`) + + let hasTriagePermission = false + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: user, + }); + hasTriagePermission = data.user.permissions.triage + } catch (e) { + console.warn(e) + } + + if (hasTriagePermission) { + console.log('Allowed') + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '+1', + }) + } else { + console.log('Not allowed') + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: '-1', + }) + throw new Error('not allowed') + } + - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + id: get-pr-data + with: + script: | + console.log(`Get PR info: ${context.repo.owner}/${context.repo.repo}#${context.issue.number}`) + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }) + return { + num: context.issue.number, + branchName: pr.head.ref, + repo: pr.head.repo.full_name + } + - id: generate-token + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 + with: + app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} + private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} + repository: "${{ github.repository_owner }}/ecosystem-ci" + - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + id: trigger + env: + COMMENT: ${{ github.event.comment.body }} + with: + github-token: ${{ steps.generate-token.outputs.token }} + result-encoding: string + script: | + const comment = process.env.COMMENT.trim() + const prData = ${{ steps.get-pr-data.outputs.result }} + + const suite = comment.split('\n')[0].replace(/^\/ecosystem-ci run/, '').trim() + + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: 'ecosystem-ci', + workflow_id: 'ecosystem-ci-from-pr.yml', + ref: 'main', + inputs: { + prNumber: '' + prData.num, + branchName: prData.branchName, + repo: prData.repo, + suite: suite === '' ? '-' : suite + } + }) diff --git a/.github/workflows/introspect.yml b/.github/workflows/introspect.yml index 99b843dc71..7617c941e7 100644 --- a/.github/workflows/introspect.yml +++ b/.github/workflows/introspect.yml @@ -11,15 +11,19 @@ on: - ".github/workflows/**" branches: - main + - "!v[0-9]*" + +permissions: + contents: read jobs: lint-workflows: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.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/main/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 4a6701f9b0..d029ece505 100644 --- a/.github/workflows/nuxt2-edge.yml +++ b/.github/workflows/nuxt2-edge.yml @@ -5,20 +5,31 @@ on: schedule: - cron: '0 0 * * *' +# https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml +env: + # 7 GiB by default on GitHub, setting to 6 GiB + # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources + NODE_OPTIONS: --max-old-space-size=6144 + +permissions: + contents: read + jobs: nightly: if: github.repository_owner == 'nuxt' runs-on: ubuntu-latest + permissions: + id-token: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.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@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: 14 + node-version: 16 registry-url: 'https://registry.npmjs.org' - name: install run: yarn --check-files --frozen-lockfile --non-interactive @@ -28,8 +39,6 @@ jobs: run: yarn run audit - name: build run: yarn test:fixtures -i - env: - NODE_OPTIONS: "--max_old_space_size=4096" - name: lint app run: yarn lint:app - name: test types @@ -48,4 +57,5 @@ jobs: run: ./scripts/workspace-run npm publish -q env: NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}} + NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 0000000000..cd741c2630 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,65 @@ +name: release + +on: + issue_comment: + types: [created] + +env: + # 7 GiB by default on GitHub, setting to 6 GiB + NODE_OPTIONS: --max-old-space-size=6144 + +permissions: + contents: read + +jobs: + release-pr: + if: github.repository == 'nuxt/nuxt' && github.event.issue.pull_request && github.event.comment.body == '/trigger release' + permissions: + id-token: write + pull-requests: write + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Ensure action is by maintainer + uses: octokit/request-action@89697eb6635e52c6e1e5559f15b5c91ba5100cb0 # v2.1.9 + id: check_role + with: + route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + with: + ref: refs/pull/${{ github.event.issue.number }}/merge + fetch-depth: 0 + + - run: corepack enable + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm build + + - name: Release Edge + run: ./scripts/release-edge.sh pr-${{ github.event.issue.number }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + NPM_CONFIG_PROVENANCE: true + + - name: Post comment + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `:rocket: Release triggered! You can now install [nuxt@npm:nuxt-nightly@pr-${{ github.event.issue.number }}](https://www.npmjs.com/package/nuxt-nightly/v/pr-${{ github.event.issue.number }})` + }) diff --git a/.github/workflows/reproduire-close.yml b/.github/workflows/reproduire-close.yml new file mode 100644 index 0000000000..61c550910b --- /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 0000000000..9c47a96f8d --- /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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.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 new file mode 100644 index 0000000000..db3f6a9ec1 --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,71 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '20 7 * * 2' + push: + branches: ["main"] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + contents: read + actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + with: + sarif_file: results.sarif diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml new file mode 100644 index 0000000000..c442dadd48 --- /dev/null +++ b/.github/workflows/semantic-pull-requests.yml @@ -0,0 +1,40 @@ +name: Semantic pull request + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + +jobs: + main: + 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' && !startsWith(github.head_ref, 'v') + runs-on: ubuntu-latest + name: Semantic pull request + steps: + - name: Validate PR title + uses: amannn/action-semantic-pull-request@47b15d52c5c30e94a17ec87eb8dd51ff5221fed9 # v5.3.0 + with: + scopes: | + kit + nuxi + nuxt + schema + test-utils + vite + webpack + deps + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + doesn't start with an uppercase character. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ffd4e5fb1f..35388c4fd2 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 @@ -16,11 +17,17 @@ packages/*/LICENSE .tmp .cache +# Lychee cache +.lycheecache + # Generated dirs dist .nuxt .nuxt-* +.vercel +.netlify .output +.output-* .gen # Junit reports @@ -65,3 +72,4 @@ Temporary Items .netlify fixtures-temp +.pnpm-store diff --git a/.stackblitz/codeflow.json b/.stackblitz/codeflow.json new file mode 100644 index 0000000000..c6f914a9e6 --- /dev/null +++ b/.stackblitz/codeflow.json @@ -0,0 +1,12 @@ +{ + "pnpm": { + "overrides": { + "@nuxt/kit": "./packages/kit", + "@nuxt/schema": "./packages/schema", + "@nuxt/test-utils": "./packages/test-utils", + "@nuxt/vite": "./packages/vite", + "@nuxt/webpack": "./packages/webpack", + "nuxt": "./packages/nuxt" + } + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..32d67b6ee6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "vue.volar" + ] +} diff --git a/.website/.gitignore b/.website/.gitignore new file mode 100755 index 0000000000..69f6b69d07 --- /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 0000000000..197a0d921a --- /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 meantime 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 0000000000..9692072acb --- /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 0000000000..8de14298ae --- /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 0000000000..3cb01d7ff8 --- /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.15.0" + } +} diff --git a/examples/advanced/config-extends/tsconfig.json b/.website/tsconfig.json old mode 100644 new mode 100755 similarity index 100% rename from examples/advanced/config-extends/tsconfig.json rename to .website/tsconfig.json diff --git a/README.md b/README.md index 4f04e345f0..488fc275cf 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,52 @@ +[![Nuxt banner](./.github/assets/banner.svg)](https://nuxt.com) -[![Nuxt banner](./.github/assets/banner.png)](https://nuxt.com) +# Nuxt -# 👋 Welcome to Nuxt - - -Nuxt's goal is to make web development intuitive and performant, with a great developer experience.
Learn more in the ['What is Nuxt?'](https://nuxt.com/docs/getting-started/introduction) section of our documentation. - - -

+

Version Downloads License - Website - Volta board -

+ Website + Discord +

- - - - - - - - - - - - - - - - - - - - -
-
-
- -
-

Documentation

-

- We highly recommend you take a look at the Nuxt documentation to level up. -

-
-
- -
-

Modules

-

- Discover our list of modules to supercharge your Nuxt project. Created by the Nuxt team and community. -

-
-
- -
-

Examples

-

- Explore different ways of using Nuxt features and get inspired with our list of examples. -

-
+Nuxt is a free and open-source framework with an intuitive and extendable way to create type-safe, performant and production-grade full-stack web applications and websites with Vue.js. - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-

Reporting bugs

-

- Check out the Reporting Bugs page.

-

-
-
- -
-

Suggestions

-

- Check out the Contribution page. -

-
-
- -
-

Questions

-

- Check out the Getting Help page. -

-
+It provides a number of features that make it easy to build fast, SEO-friendly, and scalable web applications, including: +- Server-side rendering, Static Site Generation or Hybrid Rendering +- Automatic routing with code-splitting +- State management +- SEO Optimization +- Extensible with [100+ modules](https://nuxt.com/modules) +- Deployment to a variety of hosting platforms +- ...[and much more](https://nuxt.com) 🚀 + +## Getting Started + +Use the following command to create a new starter project. This will create a starter project with all the necessary files and dependencies: + +```bash +npx nuxi@latest init +``` + +Discover also [nuxt.new](https://nuxt.new): Open a Nuxt starter on CodeSandbox, StackBlitz or locally to get up and running in a few seconds. + +## Documentation + +We highly recommend you take a look at the [Nuxt documentation](https://nuxt.com/docs) to level up. It’s a great resource for learning more about the framework. It covers everything from getting started to advanced topics. + +## Modules + +Discover our [list of modules](https://nuxt.com/modules) to supercharge your Nuxt project, created by the Nuxt team and community. + +## Contribute + +We invite you to contribute and help improve Nuxt 💚 + +Here are a few ways you can get involved: +- **Reporting Bugs:** If you come across any bugs or issues, please check out the [reporting bugs guide](https://nuxt.com/docs/community/reporting-bugs) to learn how to submit a bug report. +- **Suggestions:** Have ideas to enhance Nuxt? We'd love to hear them! Check out the [contribution guide](https://nuxt.com/docs/community/contribution#creating-an-issue) to share your suggestions. +- **Questions:** If you have questions or need assistance, the [getting help guide](https://nuxt.com/docs/community/getting-help) provides resources to help you out. ## Local Development @@ -115,7 +54,7 @@ Follow the docs to [Set Up Your Local Development Environment](https://nuxt.com/ ## Nuxt 2 -You can find the code for Nuxt 2 on the [`2.x` branch](https://github.com/nuxt/nuxt/tree/2.x) and the documentation at [nuxtjs.org](https://nuxtjs.org). +You can find the code for Nuxt 2 on the [`2.x` branch](https://github.com/nuxt/nuxt/tree/2.x) and the documentation at [v2.nuxt.com](https://v2.nuxt.com). ## Follow us diff --git a/docs/0.index.md b/docs/0.index.md index 923a6060e0..db35c15b03 100644 --- a/docs/0.index.md +++ b/docs/0.index.md @@ -32,7 +32,7 @@ buttons: - label: 'Explore Examples' size: 'sm' variant: 'secondary' - to: '/docs/examples/essentials/hello-world' + to: '/docs/examples/hello-world' --- #title Getting Started @@ -107,7 +107,7 @@ From an idea to a masterpiece, guides take you on the path to becoming a Nuxter. #title API #description -Every Nuxt components, composables and utilities, in details. +Every Nuxt component, composable and utility, in detail. #extra ::card-list :::card-item diff --git a/docs/1.getting-started/1.introduction.md b/docs/1.getting-started/1.introduction.md index 30ed20987d..4503a53691 100644 --- a/docs/1.getting-started/1.introduction.md +++ b/docs/1.getting-started/1.introduction.md @@ -19,7 +19,7 @@ Nuxt uses conventions and an opinionated directory structure to automate repetit - **File-based routing:** define routes based on the structure of your [`pages/` directory](/docs/guide/directory-structure/pages). This can make it easier to organize your application and avoid the need for manual route configuration. - **Code splitting:** Nuxt automatically splits your code into smaller chunks, which can help reduce the initial load time of your application. - **Server-side rendering out of the box:** Nuxt comes with built-in SSR capabilities, so you don't have to set up a separate server yourself. -- **Auto-imports:** write Vue composables and components in their respectives directories and use them without having to import them with the benefits of tree-shaking and optimised JS bundles. +- **Auto-imports:** write Vue composables and components in their respective directories and use them without having to import them with the benefits of tree-shaking and optimized JS bundles. - **Data-fetching utilities:** Nuxt provides composables to handle SSR-compatible data fetching as well as different strategies. - **Zero-config TypeScript support:** write type-safe code without having to learn TypeScript with our auto-generated types and `tsconfig.json` - **Configured build tools:** we use [Vite](https://vitejs.dev) by default to support hot module replacement (HMR) in development and bundling your code for production with best-practices baked-in. diff --git a/docs/1.getting-started/10.deployment.md b/docs/1.getting-started/10.deployment.md index b816e2b27d..672d1c5f65 100644 --- a/docs/1.getting-started/10.deployment.md +++ b/docs/1.getting-started/10.deployment.md @@ -76,31 +76,66 @@ By default, the workload gets distributed to the workers with the round robin st There are two ways to deploy a Nuxt application to any static hosting services: -- Static site generation (SSG) with `ssr: true` pre-renders routes of your application at build time. (This is the default behaviour when running `nuxi generate`.) It will also generate `/200.html` and `/404.html` single-page app fallback pages, which can render dynamic routes or 404 errors on the client (though you may need to configure this on your static host). +- Static site generation (SSG) with `ssr: true` pre-renders routes of your application at build time. (This is the default behavior when running `nuxi generate`.) It will also generate `/200.html` and `/404.html` single-page app fallback pages, which can render dynamic routes or 404 errors on the client (though you may need to configure this on your static host). - Alternatively, you can prerender your site with `ssr: false` (static single-page app). This will produce HTML pages with an empty `
` where your Vue app would normally be rendered. You will lose many of the benefits of prerendering your site, so it is suggested instead to use `` to wrap the portions of your site that cannot be server rendered (if any). ### 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 ``` -### Manual Pre-rendering +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`. -You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build. +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 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'] } } }) ``` +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({ + nitro: { + 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. @@ -140,12 +175,27 @@ Nuxt 3 can be deployed to several cloud providers with a minimal amount of confi - :icon{name="logos:aws" class="h-5 w-4 inline mb-2"} [AWS](https://nitro.unjs.io/deploy/providers/aws) - :icon{name="logos:microsoft-azure" class="h-5 w-4 inline mb-2"} [Azure](https://nitro.unjs.io/deploy/providers/azure) - :icon{name="ph:cloud-duotone" class="h-5 w-4 inline mb-2"} [Cleavr](https://nitro.unjs.io/deploy/providers/cleavr) -- :icon{name="logos:cloudflare" class="h-5 w-4 inline mb-2"} [CloudFlare](https://nitro.unjs.io/deploy/providers/cloudflare) +- :icon{name="logos:cloudflare" class="h-5 w-4 inline mb-2"} [Cloudflare](https://nitro.unjs.io/deploy/providers/cloudflare) - :icon{name="logos:digital-ocean" class="h-5 w-4 inline mb-2"} [DigitalOcean](https://nitro.unjs.io/deploy/providers/digitalocean) -- :icon{name="logos:firebase" class="h-5 w-4 inline mb-2"} [Firebase](https://nitro.unjs.io/deploy/providers/firebase) -- :icon{name="logos:heroku-icon" class="h-5 w-4 inline mb-2"} [heroku](https://nitro.unjs.io/deploy/providers/heroku) - :icon{name="ph:cloud-duotone" class="h-5 w-4 inline mb-2"} [Edgio](https://nitro.unjs.io/deploy/providers/edgio) +- :icon{name="logos:firebase" class="h-5 w-4 inline mb-2"} [Firebase](https://nitro.unjs.io/deploy/providers/firebase) +- :icon{name="logos:heroku-icon" class="h-5 w-4 inline mb-2"} [Heroku](https://nitro.unjs.io/deploy/providers/heroku) +- :icon{name="IconLagon" class="h-5 w-4 inline mb-2 text-black dark:text-white"} [Lagon](https://nitro.unjs.io/deploy/providers/lagon) - :icon{name="logos:netlify" class="h-5 w-4 inline mb-2"} [Netlify](https://nitro.unjs.io/deploy/providers/netlify) - :icon{name="simple-icons:render" class="h-5 w-4 inline mb-2"} [Render](https://nitro.unjs.io/deploy/providers/render) - :icon{name="ph:cloud-duotone" class="h-5 w-4 inline mb-2"} [Stormkit](https://nitro.unjs.io/deploy/providers/stormkit) - :icon{name="simple-icons:vercel" class="h-5 w-4 inline mb-2 text-black dark:text-white"} [Vercel](https://nitro.unjs.io/deploy/providers/vercel) + +## CDN Proxy + +In most cases, Nuxt can work with third-party content that is not generated or created by Nuxt itself. But sometimes such content can cause problems, especially Cloudflare's "Minification and Security Options". + +Accordingly, you should make sure that the following options are unchecked / disabled in Cloudflare. Otherwise, unnecessary re-rendering or hydration errors could impact your production application. + +1. Speed > Optimization > Auto Minify: Uncheck JavaScript, CSS and HTML +2. Speed > Optimization > Disable "Rocket Loader™" +3. Speed > Optimization > Disable "Mirage" +4. Scrape Shield > Disable "Email Address Obfuscation" +5. Scrape Shield > Disable "Server-side Excludes" + +With these settings, you can be sure that Cloudflare won't inject scripts into your Nuxt application that may cause unwanted side effects. diff --git a/docs/1.getting-started/11.testing.md b/docs/1.getting-started/11.testing.md index 45f06b5707..6ae12d8375 100644 --- a/docs/1.getting-started/11.testing.md +++ b/docs/1.getting-started/11.testing.md @@ -84,6 +84,13 @@ Whether to launch a server to respond to requests in the test suite. * Type: `boolean` * Default: `true` +#### `port` + +If provided, set the launched test server port to the value. + +* Type: `number | undefined` +* Default: `undefined` + #### `build` Whether to run a separate build step. diff --git a/docs/1.getting-started/12.upgrade.md b/docs/1.getting-started/12.upgrade.md index 5bacb6f264..3b2b459d93 100644 --- a/docs/1.getting-started/12.upgrade.md +++ b/docs/1.getting-started/12.upgrade.md @@ -35,7 +35,7 @@ Static sites | ✅ | ✅ | ✅ The migration guide provides a step-by-step comparison of Nuxt 2 features to Nuxt 3 features and guidance to adapt your current application. ::alert{type=info} -👉 Checkout the [**migration guide from From Nuxt 2 to Nuxt 3**](/docs/migration/overview). +👉 Check out the [**guide to migrating from Nuxt 2 to Nuxt 3**](/docs/migration/overview). :: ::alert{type=info} @@ -52,7 +52,7 @@ If you prefer to progressively migrate your Nuxt 2 application to Nuxt 3, you ca ## Upgrading Nuxt 3 -### Latest release +### Latest Release To upgrade Nuxt 3 to the [latest release](/docs/community/changelog), use the `nuxi upgrade` command. @@ -60,8 +60,8 @@ To upgrade Nuxt 3 to the [latest release](/docs/community/changelog), use the `n npx nuxi upgrade ``` -### Edge release channel +### Nightly Release Channel ::alert{type=info icon=👉} -To use the latest Nuxt 3 build and test features before their release, read the [edge release channel](/docs/guide/going-further/edge-channel) guide. +To use the latest Nuxt 3 build and test features before their release, read about the [nightly release channel](/docs/guide/going-further/nightly-release-channel) guide. :: diff --git a/docs/1.getting-started/2.installation.md b/docs/1.getting-started/2.installation.md index 2db33a6ec4..7c6b4cb12d 100644 --- a/docs/1.getting-started/2.installation.md +++ b/docs/1.getting-started/2.installation.md @@ -30,11 +30,11 @@ Start with one of our starters and themes directly by opening [nuxt.new](https:/ ::alert ::details :summary[Additional notes for an optimal setup:] -- **Node.js**: Make sure to use an even numbered version (16, 18, etc) - +- **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` file: +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: ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -52,11 +52,15 @@ Open a terminal (if you're using [Visual Studio Code](https://code.visualstudio. ::code-group ```bash [npx] -npx nuxi init +npx nuxi@latest init ``` ```bash [pnpm] -pnpm dlx nuxi init +pnpm dlx nuxi@latest init +``` + +```bash [bun] +bunx nuxi@latest init ``` :: @@ -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/3.configuration.md b/docs/1.getting-started/3.configuration.md index 7a79927616..c42aa152d8 100644 --- a/docs/1.getting-started/3.configuration.md +++ b/docs/1.getting-started/3.configuration.md @@ -9,7 +9,7 @@ By default, Nuxt is configured to cover most use cases. The [`nuxt.config.ts`](/ ## Nuxt Configuration -The `nuxt.config.ts` file is located at the root of a Nuxt project and can override or extend the application's behavior. +The [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file is located at the root of a Nuxt project and can override or extend the application's behavior. A minimal configuration file exports the `defineNuxtConfig` function containing an object with your configuration. The `defineNuxtConfig` helper is globally available without import. @@ -29,6 +29,27 @@ Every configuration option is described in the [Configuration Reference](/docs/a You don't have to use TypeScript to build an application with Nuxt. However, it is strongly recommended to use the `.ts` extension for the `nuxt.config` file. This way you can benefit from hints in your IDE to avoid typos and mistakes while editing your configuration. :: +### Environment overrides + +You can configure fully typed, per-environment overrides in your nuxt.config + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + $production: { + routeRules: { + '/**': { isr: true } + } + }, + $development: { + // + } +}) +``` + +::alert{type=info} +If you're authoring layers, you can also use the `$meta` key to provide metadata that you or the consumers of your layer might use. +:: + ### Environment Variables and Private Tokens The `runtimeConfig` API exposes values like environment variables to the rest of your application. By default, these keys are only available server-side. The keys within `runtimeConfig.public` are also available client-side. @@ -60,7 +81,7 @@ NUXT_API_SECRET=api_secret_token These variables are exposed to the rest of your application using the [`useRuntimeConfig`](/docs/api/composables/use-runtime-config) composable. ```vue [pages/index.vue] - ``` @@ -88,7 +109,7 @@ export default defineAppConfig({ These variables are exposed to the rest of your application using the [`useAppConfig`](/docs/api/composables/use-app-config) composable. ```vue [pages/index.vue] - ``` @@ -114,7 +135,7 @@ Non primitive JS types | ❌ No | ✅ Yes ## External Configuration Files -Nuxt uses `nuxt.config.ts` file as the single source of trust for configurations and skips reading external configuration files. During the course of building your project, you may have a need to configure those. The following table highlights common configurations and, where applicable, how they can be configured with Nuxt. +Nuxt uses [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file as the single source of trust for configurations and skips reading external configuration files. During the course of building your project, you may have a need to configure those. The following table highlights common configurations and, where applicable, how they can be configured with Nuxt. Name | Config File | How To Configure |---------------------------------------------|---------------------------|------------------------- @@ -131,5 +152,62 @@ Name | Config File | How To | [ESLint](https://eslint.org) | `.eslintrc.js` | [More Info](https://eslint.org/docs/latest/user-guide/configuring/configuration-files) | [Prettier](https://prettier.io) | `.prettierrc.json` | [More Info](https://prettier.io/docs/en/configuration.html) | [Stylelint](https://stylelint.io) | `.stylelintrc.json` | [More Info](https://stylelint.io/user-guide/configure) -| [TailwindCSS](https://tailwindcss.com) | `tailwind.config.js` | [More Info](https://tailwindcss.nuxt.dev/tailwind/config/) +| [TailwindCSS](https://tailwindcss.com) | `tailwind.config.js` | [More Info](https://tailwindcss.nuxtjs.org/tailwind/config/) | [Vitest](https://vitest.dev) | `vitest.config.ts` | [More Info](https://vitest.dev/config/) + +## Vue Configuration + +### With Vite + +If you need to pass options to `@vitejs/plugin-vue` or `@vitejs/plugin-vue-jsx`, you can do this in your `nuxt.config` file. + +- `vite.vue` for `@vitejs/plugin-vue`. Check available options [here](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue). +- `vite.vueJsx` for `@vitejs/plugin-vue-jsx`. Check available options [here](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue-jsx). + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + vite: { + vue: { + customElement: true + }, + vueJsx: { + mergeProps: true + } + } +}) +``` + +:ReadMore{link="/docs/guide/directory-structure/nuxt.config#vue"} + +### With webpack + +If you use webpack and need to configure `vue-loader`, you can do this using `webpack.loaders.vue` key inside your `nuxt.config` file. The available options are [defined here](https://github.com/vuejs/vue-loader/blob/main/src/index.ts#L32-L62). + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + webpack: { + loaders: { + vue: { + hotReload: true, + } + } + } +}) +``` + +:ReadMore{link="/docs/guide/directory-structure/nuxt.config#loaders"} + +### Enabling Experimental Vue Features + +You may need to enable experimental features in Vue, such as `defineModel` or `propsDestructure`. Nuxt provides an easy way to do that in `nuxt.config.ts`, no matter which builder you are using: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + vue: { + defineModel: true, + propsDestructure: true + } +}) +``` + +:ReadMore{link="/docs/guide/directory-structure/nuxt.config#vue-1"} diff --git a/docs/1.getting-started/3.views.md b/docs/1.getting-started/3.views.md index dee27d5e22..f694015447 100644 --- a/docs/1.getting-started/3.views.md +++ b/docs/1.getting-started/3.views.md @@ -28,7 +28,7 @@ If you are familiar with Vue, you might wonder where `main.js` is (the file that ![Components are reusable pieces of UI](/assets/docs/getting-started/views/components.svg) -Most components are reusable pieces of the user interface, like buttons and menus. In Nuxt, you can create these components in the `components/` directory, and they will be automatically available across your application without having to explicitly import them. +Most components are reusable pieces of the user interface, like buttons and menus. In Nuxt, you can create these components in the [`components/` directory](/docs/guide/directory-structure/components), and they will be automatically available across your application without having to explicitly import them. ::code-group @@ -57,9 +57,9 @@ Most components are reusable pieces of the user interface, like buttons and menu ![Pages are views tied to a specific route](/assets/docs/getting-started/views/pages.svg) -Pages represent views for each specific route pattern. Every file in the `pages/` directory represents a different route displaying its content. +Pages represent views for each specific route pattern. Every file in the [`pages/` directory](/docs/guide/directory-structure/pages) represents a different route displaying its content. -To use pages, create `pages/index.vue` file and add `` component to the `app.vue` (or remove `app.vue` for default entry). You can now create more pages and their corresponding routes by adding new files in the `pages/` directory. +To use pages, create `pages/index.vue` file and add `` component to the `app.vue` (or remove `app.vue` for default entry). You can now create more pages and their corresponding routes by adding new files in the [`pages/` directory](/docs/guide/directory-structure/pages). ::code-group @@ -132,3 +132,26 @@ If you only have a single layout in your application, we recommend using app.vue :: If you want to create more layouts and learn how to use them in your pages, find more information in the [Layouts section](/docs/guide/directory-structure/layouts). + +## Advanced: Extending the HTML template + +::alert{type=info} +If you only need to modify the head, you can refer to the [SEO and meta section](/docs/getting-started/seo-meta). +:: + +You can have full control over the HTML template by adding a Nitro plugin that registers a hook. +The callback function of the `render:html` hook allows you to mutate the HTML before it is sent to the client. + +```ts [server/plugins/extend-html.ts] +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('render:html', (html, { event }) => { + // This will be an object representation of the html template. + console.log(html) + html.head.push(``) + }) + // You can also intercept the response here. + nitroApp.hooks.hook('render:response', (response, { event }) => { console.log(response) }) +}) +``` + +:ReadMore{link="/docs/guide/going-further/hooks"} diff --git a/docs/1.getting-started/4.assets.md b/docs/1.getting-started/4.assets.md index 50ebbc9d35..efaab282f1 100644 --- a/docs/1.getting-started/4.assets.md +++ b/docs/1.getting-started/4.assets.md @@ -13,7 +13,7 @@ Nuxt uses two directories to handle assets like stylesheets, fonts or images. The [`public/` directory](/docs/guide/directory-structure/public) is used as a public server for static assets publicly available at a defined URL of your application. -You can get a file in the `public/` directory from your application's code or from a browser by the root URL `/`. +You can get a file in the [`public/` directory](/docs/guide/directory-structure/public) from your application's code or from a browser by the root URL `/`. ### Example @@ -29,9 +29,9 @@ For example, referencing an image file in the `public/img/` directory, available Nuxt uses [Vite](https://vitejs.dev/guide/assets.html) or [webpack](https://webpack.js.org/guides/asset-management/) to build and bundle your application. The main function of these build tools is to process JavaScript files, but they can be extended through [plugins](https://vitejs.dev/plugins/) (for Vite) or [loaders](https://webpack.js.org/loaders/) (for webpack) to process other kind of assets, like stylesheets, fonts or SVG. This step transforms the original file mainly for performance or caching purposes (such as stylesheets minification or browser cache invalidation). -By convention, Nuxt uses the `assets/` directory to store these files but there is no auto-scan functionality for this directory, and you can use any other name for it. +By convention, Nuxt uses the [`assets/` directory](/docs/guide/directory-structure/assets) to store these files but there is no auto-scan functionality for this directory, and you can use any other name for it. -In your application's code, you can reference a file located in the `assets/` directory by using the `~/assets/` path. +In your application's code, you can reference a file located in the [`assets/` directory](/docs/guide/directory-structure/assets) by using the `~/assets/` path. ### Example @@ -44,7 +44,7 @@ For example, referencing an image file that will be processed if a build tool is ``` ::alert{type=info icon=💡} -Nuxt won't serve files in the `assets/` directory at a static URL like `/assets/my-file.png`. If you need a static URL, use the [`public/` directory](#public-directory). +Nuxt won't serve files in the [`assets/` directory](/docs/guide/directory-structure/assets) at a static URL like `/assets/my-file.png`. If you need a static URL, use the [`public/` directory](#public-directory). :: ### Global Styles Imports diff --git a/docs/1.getting-started/4.styling.md b/docs/1.getting-started/4.styling.md new file mode 100644 index 0000000000..f9a68c154a --- /dev/null +++ b/docs/1.getting-started/4.styling.md @@ -0,0 +1,529 @@ +--- +navigation.icon: uil:palette +--- + +# Styling + +Nuxt is highly flexible when it comes to styling. Write your own styles, or reference local and external stylesheets. +You can use CSS preprocessors, CSS frameworks, UI libraries and Nuxt modules to style your application. + +## Local Stylesheets + +If you're writing local stylesheets, the natural place to put them is the [`assets/` directory](/docs/guide/directory-structure/assets). + +### Importing Within Components + +You can import stylesheets in your pages, layouts and components directly. +You can use a javascript import, or a css [`@import` statement](https://developer.mozilla.org/en-US/docs/Web/CSS/@import). + +```vue [pages/index.vue] + + + +``` + +::alert{type=info} +The stylesheets will be inlined in the HTML rendered by Nuxt. +:: + +### The CSS Property + +You can also use the `css` property in the Nuxt configuration. +The natural place for your stylesheets is the [`assets/` directory](/docs/guide/directory-structure/assets). You can then reference its path and Nuxt will include it to all the pages of your application. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + css: ['~/assets/css/main.css'] +}) +``` + +::alert{type=info} +The stylesheets will be inlined in the HTML rendered by Nuxt, injected globally and present in all pages. +:: + +### Working With Fonts + +Place your local fonts files in your `~/public/` directory, for example in `~/public/fonts`. You can then reference them in your stylesheets using `url()`. + +```css [assets/css/main.css] +@font-face { + font-family: 'FarAwayGalaxy'; + src: url('/fonts/FarAwayGalaxy.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} +``` + +Then reference your fonts by name in your stylesheets, pages or components: + +```vue + +``` + +### Stylesheets Distributed Through NPM + +You can also reference stylesheets that are distributed through npm. Let's use the popular `animate.css` library as an example. + +```bash +npm install animate.css +``` + +Then you can reference it directly in your pages, layouts and components: + +```vue [app.vue] + + + +``` + +The package can also be referenced as a string in the css property of your Nuxt configuration. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + css: ['animate.css'] +}) +``` + +## External Stylesheets + +You can include external stylesheets in your application by adding a link element in the head section of your nuxt.config file. You can achieve this result using different methods. Note that local stylesheets can also be included like this. + +You can manipulate the head with the [`app.head`](/docs/api/configuration/nuxt-config#head) property of your Nuxt configuration: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + app: { + head: { + link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }] + } +}}) +``` + +### Dynamically Adding Stylesheets + +You can use the useHead composable to dynamically set a value in your head in your code. + +::ReadMore{link="/docs/api/composables/use-head"} +:: + +```ts +useHead({ + link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }] +}) +``` + +Nuxt uses `unhead` under the hood, and you can refer to its full documentation [here](https://unhead.unjs.io/). + +### Modifying The Rendered Head With A Nitro Plugin + +If you need more advanced control, you can intercept the rendered html with a hook and modify the head programmatically. + +Create a plugin in `~/server/plugins/my-plugin.ts` like this: + +```ts [server/plugins/my-plugin.ts] +export default defineNitroPlugin((nitro) => { + nitro.hooks.hook('render:html', (html) => { + html.head.push('') + }) +}) +``` + +External stylesheets are render-blocking resources: they must be loaded and processed before the browser renders the page. Web pages that contain unnecessarily large styles take longer to render. You can read more about it on [web.dev](https://web.dev/defer-non-critical-css/). + +## Using Preprocessors + +To use a preprocessor like SCSS, Sass, Less or Stylus, install it first. + +::code-group + +```bash [Sass & SCSS] +npm install sass +``` + +```bash [Less] +npm install less +``` + +```bash [Stylus] +npm install stylus +``` + +:: + +The natural place to write your stylesheets is the `assets` directory. +You can then import your source files in your `app.vue` (or layouts files) using your preprocessor's syntax. + +```vue [pages/app.vue] + +``` + +Alternatively, you can use the `css` property of your Nuxt configuration. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + css: ['~/assets/scss/main.scss'] +}) +``` + +::alert{type=info} +In both cases, the compiled stylesheets will be inlined in the HTML rendered by Nuxt. +:: + +If you need to inject code in pre-processed files, like a [sass partial](https://sass-lang.com/documentation/at-rules/use#partials) with color variables, you can do so with the vite [preprocessors options](https://vitejs.dev/config/shared-options.html#css-preprocessoroptions). + +Create some partials in your `assets` directory: + +::code-group + +```scss [assets/_colors.scss] +$primary: #49240F; +$secondary: #E4A79D; +``` + +```sass [assets/_colors.sass] +$primary: #49240F +$secondary: #E4A79D +``` + +:: + +Then in your `nuxt.config` : + +::code-group + +```ts [SCSS] +export default defineNuxtConfig({ + vite: { + css: { + preprocessorOptions: { + scss: { + additionalData: '@use "@/assets/_colors.scss" as *;' + } + } + } + } +}) +``` + +```ts [SASS] +export default defineNuxtConfig({ + vite: { + css: { + preprocessorOptions: { + sass: { + additionalData: '@use "@/assets/_colors.sass" as *\n' + } + } + } + } +}) +``` + +:: + +Nuxt uses Vite by default. If you wish to use webpack instead, refer to each preprocessor loader [documentation](https://webpack.js.org/loaders/sass-loader/). + +## Single File Components (SFC) Styling + +One of the best thing about Vue and SFC is how great it is at naturally dealing with styling. You can directly write CSS or preprocessor code in the style block of your components file, therefore you will have fantastic developer experience without having to use something like CSS-in-JS. However if you wish to use CSS-in-JS, you can find 3rd party libraries and modules that support it, such as [pinceau](https://pinceau.dev/). + +You can refer to the [Vue docs](https://vuejs.org/api/sfc-css-features.html) for a comprehensive reference about styling components in SFC. + +### Class And Style Bindings + +You can leverage Vue SFC features to style your components with class and style attributes. + +::code-group + +```vue [Ref and Reactive] + + + +``` + +```vue [Computed] + + + +``` + +```vue [Array] + + + +``` + +```vue [Style] + + + +``` + +:: + +Refer to the [Vue docs](https://vuejs.org/guide/essentials/class-and-style.html) for more information. + +### Dynamic Styles With `v-bind` + +You can reference JavaScript variable and expression within your style blocks with the v-bind function. +The binding will be dynamic, meaning that if the variable value changes, the style will be updated. + +```vue + + + + + +``` + +### Scoped Styles + +The scoped attribute allows you to style components in isolation. The styles declared with this attribute will only apply to this component. + +```vue + + + +``` + +### CSS Modules + +You can use [CSS Modules](https://github.com/css-modules/css-modules) with the module attribute. Access it with the injected `$style` variable. + +```vue + + + +``` + +### Preprocessors Support + +SFC style blocks support preprocessors syntax. Vite come with built-in support for .scss, .sass, .less, .styl and .stylus files without configuration. You just need to install them first, and they will be available directly in SFC with the lang attribute. + +::code-group + +```vue [SCSS] + +``` + +```vue [Sass] + +``` + +```vue [LESS] + +``` + +```vue [Stylus] + +``` + +:: + +You can refer to the [Vite CSS docs](https://vitejs.dev/guide/features.html#css) and the [@vitejs/plugin-vue docs](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue). +For webpack users, refer to the [vue loader docs](https://vue-loader.vuejs.org/). + +## Using PostCSS + +Nuxt comes with postcss built-in. You can configure it in your `nuxt.config` file. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + postcss: { + plugins: { + 'postcss-nested': {} + "postcss-custom-media": {} + } + } +}) +``` + +For proper syntax highlighting in SFC, you can use the postcss lang attribute. + +```vue + +``` + +By default, Nuxt comes with the following plugins already pre-configured: + +- [postcss-import](https://github.com/postcss/postcss-import): Improves the `@import` rule +- [postcss-url](https://github.com/postcss/postcss-url): Transforms `url()` statements +- [autoprefixer](https://github.com/postcss/autoprefixer): Automatically adds vendor prefixes +- [cssnano](https://cssnano.co/): Minification and purge + +##  Leveraging Layouts For Multiple Styles + +If you need to style different parts of your application completely differently, you can use layouts. +Use different styles for different layouts. + +```vue + + + +``` + +::ReadMore{link="/docs/guide/directory-structure/layouts"} +:: + +## Third Party Libraries And Modules + +Nuxt isn't opinionated when it comes to styling and provides you with a wide variety of options. You can use any styling tool that you want, such as popular libraries like [UnoCSS](https://unocss.dev/) or [Tailwind CSS](https://tailwindcss.com/). + +The community and the Nuxt team have developed plenty of Nuxt modules to makes the integration easier. +You can discover them on the [modules section](https://nuxt.com/modules) of the website. +Here are a few modules to help you get started: + +- [UnoCSS](https://nuxt.com/modules/unocss): Instant on-demand atomic CSS engine +- [Tailwind CSS](https://nuxt.com/modules/tailwindcss): Utility-first CSS framework +- [Fontaine](https://github.com/nuxt-modules/fontaine): Font metric fallback +- [Pinceau](https://pinceau.dev/): Adaptable styling framework +- [Nuxt UI](https://ui.nuxt.com): A UI Library for Modern Web Apps + +Nuxt modules provide you with a good developer experience out of the box, but remember that if your favorite tool doesn't have a module, it doesn't mean that you can't use it with Nuxt! You can configure it yourself for your own project. Depending on the tool, you might need to use a [Nuxt plugin](/docs/guide/directory-structure/plugins) and/or [make your own module](/docs/guide/going-further/modules). Share them with the [community](https://nuxt.com/modules) if you do! + +### Easily Load Webfonts + +You can use [the Nuxt Google Fonts module](https://github.com/nuxt-modules/google-fonts) to load Google Fonts. + +If you are using [UnoCSS](https://unocss.dev/integrations/nuxt), note that it comes with a [web fonts presets](https://unocss.dev/presets/web-fonts) to conveniently load fonts from common providers, including Google Fonts and more. + +## Advanced + +### Transitions + +Nuxt comes with the same `` element that Vue has, and also has support for the experimental [View Transitions API](/docs/getting-started/transitions#view-transitions-api-experimental). + +::ReadMore{link="/docs/features/transitions"} +:: + +### Font Advanced Optimization + +We would recommend using [Fontaine](https://github.com/nuxt-modules/fontaine) to reduce your [CLS](https://web.dev/cls/). If you need something more advanced, consider creating a Nuxt module to extend the build process or the Nuxt runtime. + +::alert{type="info"} +Always remember to take advantage of the various tools and techniques available in the Web ecosystem at large to make styling your application easier and more efficient. Whether you're using native CSS, a preprocessor, postcss, a UI library or a module, Nuxt has got you covered. Happy styling! +:: + +### LCP Advanced optimizations + +You can do the following to speed-up the download of your global CSS files: + +- Use a CDN so the files are physically closer to your users +- Compress your assets, ideally using Brotli +- Use HTTP2/HTTP3 for delivery +- Host your assets on the same domain (do not use a different subdomain) + +Most of these things should be done for you automatically if you're using modern platforms like Cloudflare, Netlify or Vercel. +You can find an LCP optimization guide on [web.dev](https://web.dev/optimize-lcp/). + +If all of your CSS is inlined by Nuxt, you can (experimentally) completely stop external CSS files from being referenced in your rendered HTML. +You can achieve that with a hook, that you can place in a module, or in your Nuxt configuration file. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + hooks: { + 'build:manifest': (manifest) => { + // find the app entry, css list + const css = manifest['node_modules/nuxt/dist/app/entry.js']?.css + if (css) { + // start from the end of the array and go to the beginning + for (let i = css.length - 1; i >= 0; i--) { + // if it starts with 'entry', remove it from the list + if (css[i].startsWith('entry')) css.splice(i, 1) + } + } + }, + }, +}) +``` diff --git a/docs/1.getting-started/5.routing.md b/docs/1.getting-started/5.routing.md index d5b2bd754f..3f5ce8de01 100644 --- a/docs/1.getting-started/5.routing.md +++ b/docs/1.getting-started/5.routing.md @@ -4,11 +4,11 @@ description: Nuxt file-system routing creates a route for every file in the page --- # Routing -One core feature of Nuxt is the file system router. Every Vue file inside the `pages/` directory creates a corresponding URL (or route) that displays the contents of the file. By using dynamic imports for each page, Nuxt leverages code-splitting to ship the minimum amount of JavaScript for the requested route. +One core feature of Nuxt is the file system router. Every Vue file inside the [`pages/` directory](/docs/guide/directory-structure/pages) creates a corresponding URL (or route) that displays the contents of the file. By using dynamic imports for each page, Nuxt leverages code-splitting to ship the minimum amount of JavaScript for the requested route. ## Pages -Nuxt routing is based on [vue-router](https://router.vuejs.org/) and generates the routes from every component created in the [`pages/`](/docs/guide/directory-structure/pages) directory, based on their filename. +Nuxt routing is based on [vue-router](https://router.vuejs.org/) and generates the routes from every component created in the [`pages/` directory](/docs/guide/directory-structure/pages), based on their filename. This file system routing uses naming conventions to create dynamic and nested routes: @@ -17,6 +17,7 @@ This file system routing uses naming conventions to create dynamic and nested ro ```text [pages/ directory] pages/ --| about.vue +--| index.vue --| posts/ ----| [id].vue ``` @@ -28,6 +29,10 @@ pages/ "path": "/about", "component": "pages/about.vue" }, + { + "path": "/", + "component": "pages/index.vue" + }, { "path": "/posts/:id", "component": "pages/posts/[id].vue" @@ -66,8 +71,8 @@ When a `` enters the viewport on the client side, Nuxt will automatica The `useRoute()` composable can be used in a ` ``` -**Reactive example:** - -When inserting tags that are reactive, you should use the computed getter syntax (`() => value`): - -```vue [app.vue] - -``` - -Read more on the [`useSeoMeta`](https://unhead.harlanzw.com/guide/composables/use-seo-meta) composable. +Read more on the [`useSeoMeta`](/docs/api/composables/use-seo-meta) and [`useServerSeoMeta`](/docs/api/composables/use-server-seo-meta) composables. ## Components @@ -108,7 +89,7 @@ Because these component names match native HTML elements, it is very important t ```vue [app.vue] - @@ -127,7 +108,7 @@ const title = ref('Hello World') ## Types -The below is the non-reactive types used for `useHead`, `app.head` and components. +Below are the non-reactive types used for [`useHead`](/docs/api/composables/use-head), [`app.head`](/docs/api/configuration/nuxt-config#head) and components. ```ts interface MetaObject { @@ -180,7 +161,7 @@ It's recommended to use getters (`() => value`) over computed (`computed(() => v ``` ```vue [Components] - @@ -215,7 +196,7 @@ If you want to use a function (for full control), then this cannot be set in you :: -Now, if you set the title to `My Page` with `useHead` on another page of your site, the title would appear as 'My Page - Site Title' in the browser tab. You could also pass `null` to default to the site title. +Now, if you set the title to `My Page` with [`useHead`](/docs/api/composables/use-head) on another page of your site, the title would appear as 'My Page - Site Title' in the browser tab. You could also pass `null` to default to the site title. ### Body Tags @@ -241,12 +222,12 @@ useHead({ ### With `definePageMeta` -Within your `pages/` directory, you can use `definePageMeta` along with `useHead` to set metadata based on the current route. +Within your [`pages/` directory](/docs/guide/directory-structure/pages), you can use `definePageMeta` along with [`useHead`](/docs/api/composables/use-head) to set metadata based on the current route. For example, you can first set the current page title (this is extracted at build time via a macro, so it can't be set dynamically): ```vue{}[pages/some-page.vue] - ``` -:LinkExample{link="/docs/examples/composables/use-head"} +:LinkExample{link="/docs/examples/features/meta-tags"} :ReadMore{link="/docs/guide/directory-structure/pages/#page-metadata"} @@ -274,7 +255,7 @@ useHead({ In the example below, `titleTemplate` is set either as a string with the `%s` placeholder or as a `function`, which allows greater flexibility in setting the page title dynamically for each route of your Nuxt app: ```vue [app.vue] - @@ -32,19 +51,96 @@ const { data: count } = await useFetch('/api/count') ``` -::LinkExample{link="/docs/examples/composables/use-fetch"} +This composable is a wrapper around the [`useAsyncData`](/docs/api/composables/use-async-data) composable and `$fetch` utility. + +::ReadMore{link="/docs/api/composables/use-fetch"} :: -## `useLazyFetch` - -This composable behaves identically to `useFetch` with the `lazy: true` option set. In other words, the async function does not block navigation. That means you will need to handle the situation where the data is `null` (or whatever value you have provided in a custom `default` factory function). - -::ReadMore{link="/docs/api/composables/use-lazy-fetch"} +::LinkExample{link="/docs/examples/features/data-fetching"} :: -### Example +## `$fetch` + +Nuxt includes the `ofetch` library, and is auto-imported as the `$fetch` alias globally across your application. It's what `useFetch` uses behind the scenes. + +```ts +const users = await $fetch('/api/users').catch((error) => error.data) +``` + +::alert{type="warning"} +Beware that using only `$fetch` will not provide [network calls de-duplication and navigation prevention](#why-using-specific-composables). It is recommended to use `$fetch` when posting data to an event handler, when doing client-side only logic, or combined with `useAsyncData`. +:: + +The `ofetch` library is built on top of [the `fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) and adds handy features to it: + +- Works the same way in browser, Node or worker environments +- Automatic response parsing +- Error handling +- Auto-retry +- Interceptors + +::alert{icon=📘} +[Read the full documentation of ofetch](https://github.com/unjs/ofetch) +:: + +::ReadMore{link="/docs/api/utils/dollarfetch"} +:: + +## `useAsyncData` + +The `useAsyncData` composable is responsible for wrapping async logic and returning the result once it is resolved. + +Indeed, `useFetch(url)` is nearly equivalent to `useAsyncData(url, () => $fetch(url))` - it's developer experience sugar for the most common use case. + +There are some cases when using the [`useFetch`](/docs/api/composables/use-fetch) composable is not appropriate, for example when a CMS or a third-party provide their own query layer. In this case, you can use [`useAsyncData`](/docs/api/composables/use-async-data) to wrap your calls and still keep the benefits provided by the composable. + +The first argument of [`useAsyncData`](/docs/api/composables/use-async-data) is the unique key used to cache the response of the second argument, the querying function. This argument can be ignored by directly passing the querying function. In that case, it will be auto-generated. + +```ts +const { data, error } = await useAsyncData('users', () => myGetFunction('users')) +``` + +Since the autogenerated key only takes into account the file and line where `useAsyncData` is invoked, is recommended to always create your own key to avoid unwanted behavior, if you are creating your own custom composable that is wrapping `useAsyncData`. + +```ts +const id = ref(1) + +const { data, error } = await useAsyncData(`user:${id.value}`, () => { + return myGetFunction('users', { id: id.value }) +}) +``` + +The `useAsyncData` composable is a great way to wrap and wait for multiple `useFetch` to be done, and then retrieve the results of each. + +```ts +const { data: discounts, pending } = await useAsyncData('cart-discount', async () => { + const [coupons, offers] = await Promise.all([$fetch('/cart/coupons'), $fetch('/cart/offers')]) + + return { + coupons, + offers + } +}) +``` + +::ReadMore{link="/docs/api/composables/use-async-data"} +:: + +## Options + +[`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) return the same object type and accept a common set of options as their last argument. They can help you control the composables behavior, such as navigation blocking, caching or execution. + +### Lazy + +By default, data fetching composables will wait for the resolution of their asynchronous function before navigating to a new page by using Vue’s Suspense. This feature can be ignored on client-side navigation with the `lazy` option. In that case, you will have to manually handle loading state using the `pending` value. + +```vue [app.vue] + -```vue +``` - ``` -## `useAsyncData` - -Within your pages, components and plugins you can use `useAsyncData` to get access to data that resolves asynchronously. - -::alert -You might be asking yourself: what is the difference between `useFetch` and `useAsyncData`? - -In brief, `useFetch` receives a URL and gets that data, whereas `useAsyncData` might have more complex logic. `useFetch(url)` is nearly equivalent to `useAsyncData(url, () => $fetch(url))` - it's developer experience sugar for the most common use case. +::ReadMore{link="/docs/api/composables/use-lazy-fetch"} :: -::ReadMore{link="/docs/api/composables/use-async-data"} -:: - -### Example - -```ts [server/api/count.ts] -let counter = 0 -export default defineEventHandler(() => { - counter++ - return counter -}) -``` - -```vue [app.vue] - - - -``` - -::LinkExample{link="/docs/examples/composables/use-async-data"} -:: - -## `useLazyAsyncData` - -This composable behaves identically to `useAsyncData` with the `lazy: true` option set. In other words, the async function does not block navigation. That means you will need to handle the situation where the data is `null` (or whatever value you have provided in a custom `default` factory function). - ::ReadMore{link="/docs/api/composables/use-lazy-async-data"} :: -### Example +### Client-only fetching + +By default, data fetching composables will perform their asynchronous function on both client and server environments. Set the `server` option to `false` to only perform the call on the client-side. On initial load, the data will not be fetched before hydration is complete so you have to handle a pending state, though on subsequent client-side navigation the data will be awaited before loading the page. + +Combined with the `lazy` option, this can be useful for data that is not needed on the first render (for example, non-SEO sensitive data). + +```ts +/* This call is performed before hydration */ +const { article } = await useFetch('api/article') + +/* This call will only be performed on the client */ +const { pending, data: posts } = useFetch('/api/comments', { + lazy: true, + server: false +}) +``` + +The `useFetch` composable is meant to be invoked in setup method or called directly at the top level of a function in lifecycle hooks, otherwise you should use [`$fetch` method](#fetch). + +### Minimize payload size + +The `pick` option helps you to minimize the payload size stored in your HTML document by only selecting the fields that you want returned from the composables. ```vue + + + +``` + +If you need more control or map over several objects, you can use the `transform` function to alter the result of the query. + +```ts +const { data: mountains } = await useFetch('/api/mountains', { + transform: (mountains) => { + return mountains.map(mountain => ({ title: mountain.title, description: mountain.description })) + } +}) +``` + +::alert{type="warning"} +Both `pick` and `transform` don't prevent the unwanted data from being fetched initially. But they will prevent unwanted data from being added to the payload transferred from server to client. +:: + +### Caching and refetching + +#### Keys + +[`useFetch`](/docs/api/composables/use-fetch) and [`useAsyncData`](/docs/api/composables/use-async-data) use keys to prevent refetching the same data. + +- [`useFetch`](/docs/api/composables/use-fetch) uses the provided URL as a key. Alternatively, a `key` value can be provided in the `options` object passed as a last argument. +- [`useAsyncData`](/docs/api/composables/use-async-data) uses its first argument as a key if it is a string. If the first argument is the handler function that performs the query, then a key that is unique to the file name and line number of the instance of `useAsyncData` will be generated for you. + +::alert{icon=📘} +To get the cached data by key, you can use [`useNuxtData`](/docs/api/composables/use-nuxt-data) +:: + +#### Refresh and execute + +If you want to fetch or refresh data manually, use the `execute` or `refresh` function provided by the composables. + +```vue + + +``` - -``` -## Refreshing Data - -Sometimes throughout the course of your user's page visit, you may need to refresh the data loaded from the API. This can happen if the user chooses to paginate, filter results, search, etc. - -You can make use of the `refresh()` method returned from the `useFetch()` composable to refresh the data with different query parameters: - -```vue - -``` - -The key to making this work is to call the `refresh()` method returned from the `useFetch()` composable when a query parameter has changed. - -By default, `refresh()` will cancel any pending requests their result will not update the data or pending state. Any previously awaited promises will not resolve until this new request resolves. You can prevent this behaviour by setting the `dedupe` option, which will instead return the promise for the currently-executing request, if there is one. - -```js -refresh({ dedupe: true }) -``` - -### `refreshNuxtData` - -Invalidate the cache of `useAsyncData`, `useLazyAsyncData`, `useFetch` and `useLazyFetch` and trigger the refetch. - -This method is useful if you want to refresh all the data fetching for a current page. - -::ReadMore{link="/docs/api/utils/refresh-nuxt-data"} -:: - -#### Example - -```vue +``` - + + +``` + +For finer control, the `status` variable can be: + +- `idle` when the fetch hasn't started +- `pending` when a fetch has started but not yet completed +- `error` when the fetch fails +- `success` when the fetch is completed successfully + +## Passing Headers and cookies + +When we call `$fetch` in the browser, user headers like `cookie` will be directly sent to the API. But during server-side-rendering, since the `$fetch` request takes place 'internally' within the server, it doesn't include the user's browser cookies, nor does it pass on cookies from the fetch response. + +### Pass Client Headers to the API + +We can use [`useRequestHeaders`](/docs/api/composables/use-request-headers) to access and proxy cookies to the API from server-side. + +The example below adds the request headers to an isomorphic `$fetch` call to ensure that the API endpoint has access to the same `cookie` header originally sent by the user. + +```vue + ``` -### `clearNuxtData` +::alert{type="warning"} +Be very careful before proxying headers to an external API and just include headers that you need. Not all headers are safe to be bypassed and might introduce unwanted behavior. Here is a list of common headers that are NOT to be proxied: -Delete cached data, error status and pending promises of `useAsyncData` and `useFetch`. - -This method is useful if you want to invalidate the data fetching for another page. - -::ReadMore{link="/docs/api/utils/clear-nuxt-data"} +- `host`, `accept` +- `content-length`, `content-md5`, `content-type` +- `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-proto` +- `cf-connecting-ip`, `cf-ray` :: +### Pass Cookies From Server-side API Calls on SSR Response + + If you want to pass on/proxy cookies in the other direction, from an internal request back to the client, you will need to handle this yourself. + +```ts [composables/fetch.ts] +import { appendResponseHeader, H3Event } from 'h3' + +export const fetchWithCookie = async (event: H3Event, url: string) => { + /* Get the response from the server endpoint */ + const res = await $fetch.raw(url) + /* Get the cookies from the response */ + const cookies = (res.headers.get('set-cookie') || '').split(',') + /* Attach each cookie to our incoming Request */ + for (const cookie of cookies) { + appendResponseHeader(event, 'set-cookie', cookie) + } + /* Return the data of the response */ + return res._data +} +``` + +```vue + +``` + ## Options API support Nuxt 3 provides a way to perform `asyncData` fetching within the Options API. You must wrap your component definition within `defineNuxtComponent` for this to work. @@ -202,6 +424,7 @@ Nuxt 3 provides a way to perform `asyncData` fetching within the Options API. Yo ```vue -``` - -::alert{type="warning"} -Be very careful before proxying headers to an external API and just include headers that you need. Not all headers are safe to be bypassed and might introduce unwanted behavior. Here is a list of common headers that are NOT to be proxied: - -* `host`, `accept` -* `content-length`, `content-md5`, `content-type` -* `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-proto` -* `cf-connecting-ip`, `cf-ray` -:: - -#### Example: Pass Cookies From Server-side API Calls on SSR Response - - If you want to pass on/proxy cookies in the other direction, from an internal request back to the client, you will need to handle this yourself. - -```ts [composables/fetch.ts] -export const fetchWithCookie = async (event: H3Event, url: string) => { - const res = await $fetch.raw(url) - const cookies = (res.headers.get('set-cookie') || '').split(',') - for (const cookie of cookies) { - appendHeader(event, 'set-cookie', cookie) - } - return res._data -} -``` - -```vue - -``` - -## Best Practices - -### Minimize Payload - -The data returned by these composables will be stored inside the page payload. This means that every key returned that is not used in your component will be added to the payload. - -::alert{icon=👉} -**We strongly recommend you only select the keys that you will use in your component.** -:: - -Imagine that `/api/mountains/everest` returns the following object: - -```json -{ - "title": "Mount Everest", - "description": "Mount Everest is Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas. The China–Nepal border runs across its summit point", - "height": "8,848 m", - "countries": [ - "China", - "Nepal" - ], - "continent": "Asia", - "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Everest_kalapatthar.jpg/600px-Everest_kalapatthar.jpg" -} -``` - -If you plan to only use `title` and `description` in your component, you can select the keys by chaining the result of `$fetch` or `pick` option: - -```vue - - - -``` - -### Avoid double calls - -Calling `$fetch` in code that is executed on both server and client (such as in the top level of a `setup` function) will fetch the data twice - initially on the server and then again on the client during the hydration phase. This is because `$fetch` does not automatically serialize or transfer the data to the client. - -For example: - -**/pages/price.vue**: Isomorphic code below executes `$fetch` twice (initially on the server, then again on the client). - -```ts - -``` - -**/server/api/product.get.ts**: Server only code below executes `$fetch` only once at the server side. - -```ts -export default eventHandler(async (event: H3Event) => { - const price = $fetch('/api/price'); - return { color: getColor(), price }; -}); -``` - -If fetching twice isn't your intended behavior, to fetch only on the server side and transfer it to the client, wrap `$fetch` with `useAsyncData()` or use `useFetch()`. - -```ts - -``` - -## Using Async Setup - -If you are using `async setup()`, the current component instance will be lost after the first `await`. (This is a Vue 3 limitation.) If you want to use multiple async operations, such as multiple calls to `useFetch`, you will need to use ` - - -``` - ## Serialization -When fetching data from the `server` directory, the response is serialized using `JSON.stringify`. However, since serialization is limited to only JavaScript primitive types, Nuxt does its best to convert the return type of `$fetch` and `useFetch` to match the actual value. +When fetching data from the `server` directory, the response is serialized using `JSON.stringify`. However, since serialization is limited to only JavaScript primitive types, Nuxt does its best to convert the return type of `$fetch` and [`useFetch`](/docs/api/composables/use-fetch) to match the actual value. ::alert{icon=👉} You can learn more about `JSON.stringify` limitations [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description). diff --git a/docs/1.getting-started/7.state-management.md b/docs/1.getting-started/7.state-management.md index accfccc2c9..249971fa2b 100644 --- a/docs/1.getting-started/7.state-management.md +++ b/docs/1.getting-started/7.state-management.md @@ -5,18 +5,18 @@ description: Nuxt provides powerful state management libraries and the useState # State Management -Nuxt provides the `useState` composable to create a reactive and SSR-friendly shared state across components. +Nuxt provides the [`useState`](/docs/api/composables/use-state) composable to create a reactive and SSR-friendly shared state across components. -`useState` is an SSR-friendly [`ref`](https://vuejs.org/api/reactivity-core.html#ref) replacement. Its value will be preserved after server-side rendering (during client-side hydration) and shared across all components using a unique key. +[`useState`](/docs/api/composables/use-state) is an SSR-friendly [`ref`](https://vuejs.org/api/reactivity-core.html#ref) replacement. Its value will be preserved after server-side rendering (during client-side hydration) and shared across all components using a unique key. ::ReadMore{link="/docs/api/composables/use-state"} :: ::alert{icon=👉} -`useState` only works during `setup` or [`Lifecycle Hooks`](https://vuejs.org/api/composition-api-lifecycle.html#composition-api-lifecycle-hooks). +[`useState`](/docs/api/composables/use-state) only works during `setup` or [`Lifecycle Hooks`](https://vuejs.org/api/composition-api-lifecycle.html#composition-api-lifecycle-hooks). :: ::alert{type=warning} -Because the data inside `useState` will be serialized to JSON, it is important that it does not contain anything that cannot be serialized, such as classes, functions or symbols. +Because the data inside [`useState`](/docs/api/composables/use-state) will be serialized to JSON, it is important that it does not contain anything that cannot be serialized, such as classes, functions or symbols. :: ## Best Practices @@ -36,7 +36,7 @@ Instead use `const useX = () => useState('x')` In this example, we use a component-local counter state. Any other component that uses `useState('counter')` shares the same reactive state. ```vue [app.vue] - @@ -53,17 +53,82 @@ const counter = useState('counter', () => Math.round(Math.random() * 1000)) ``` -::LinkExample{link="/docs/examples/composables/use-state"} +::LinkExample{link="/docs/examples/features/state-management"} :: ::ReadMore{link="/docs/api/composables/use-state"} :: -### Advanced +::alert{icon=📘} +To globally invalidate cached state, see [`clearNuxtState`](/docs/api/utils/clear-nuxt-state). +:: + +### Advanced Usage In this example, we use a composable that detects the user's default locale from the HTTP request headers and keeps it in a `locale` state. -::LinkExample{link="/docs/examples/other/locale"} +```ts [composables/locale.ts] +import type { Ref } from 'vue' + +export const useLocale = () => useState('locale', () => useDefaultLocale().value) + +export const useDefaultLocale = (fallback = 'en-US') => { + const locale = ref(fallback) + if (process.server) { + const reqLocale = useRequestHeaders()['accept-language']?.split(',')[0] + if (reqLocale) { + locale.value = reqLocale + } + } else if (process.client) { + const navLang = navigator.language + if (navLang) { + locale.value = navLang + } + } + return locale +} + +export const useLocales = () => { + const locale = useLocale() + const locales = ref([ + 'en-US', + 'en-GB', + ... + 'ja-JP-u-ca-japanese' + ]) + if (!locales.value.includes(locale.value)) { + locales.value.unshift(locale.value) + } + return locales +} + +export const useLocaleDate = (date: Ref | Date, locale = useLocale()) => { + return computed(() => new Intl.DateTimeFormat(locale.value, { dateStyle: 'full' }).format(unref(date))) +} +``` + +```vue [app.vue] + + + +``` + +::LinkExample{link="/docs/examples/advanced/locale"} :: ## Shared State @@ -76,7 +141,7 @@ export const useColor = () => useState('color', () => 'pink') ``` ```vue [app.vue] - @@ -89,8 +154,8 @@ const color = useColor() // Same as useState('color') Nuxt **used to rely** on the Vuex library to provide global state management. If you are migrating from Nuxt 2, please head to [the migration guide](/docs/migration/configuration#vuex). -Nuxt is not opiniated about state management, so feel free to choose the right solution for your needs. There are multiple integrations with the most popular state management libraries, including: +Nuxt is not opinionated about state management, so feel free to choose the right solution for your needs. There are multiple integrations with the most popular state management libraries, including: - [Pinia](/modules/pinia) - the official Vue recommendation - [Harlem](/modules/harlem) - immutable global state management -- [XState](/modules/xstate) - state machine approach with tools for visualising and testing your state logic +- [XState](/modules/xstate) - state machine approach with tools for visualizing and testing your state logic diff --git a/docs/1.getting-started/8.error-handling.md b/docs/1.getting-started/8.error-handling.md index 0a6e7144ee..48afad45d6 100644 --- a/docs/1.getting-started/8.error-handling.md +++ b/docs/1.getting-started/8.error-handling.md @@ -41,6 +41,7 @@ This includes: * running Nuxt plugins * processing `app:created` and `app:beforeMount` hooks +* rendering your Vue app to HTML (on the server) * mounting the app (on client-side), though you should handle this case with `onErrorCaptured` or with `vue:error` * processing the `app:mounted` hook @@ -56,9 +57,36 @@ You can change this behavior by setting `experimental.emitRouteChunkError` to `f ## Rendering an Error Page -When Nuxt encounters a fatal error, whether during the server lifecycle, or when rendering your Vue application (both SSR and SPA), it will either render a JSON response (if requested with `Accept: application/json` header) or an HTML error page. +When Nuxt encounters a fatal error (any unhandled error on the server, or an error created with `fatal: true` on the client) it will either render a JSON response (if requested with `Accept: application/json` header) or trigger a full-screen error page. -You can customize this error page by adding `~/error.vue` in the source directory of your application, alongside `app.vue`. This page has a single prop - `error` which contains an error for you to handle. +This error may occur during the server lifecycle when: + +* processing your Nuxt plugins +* rendering your Vue app into HTML +* a server API route throws an error + +An error can also occur on the client side when: + +* processing your Nuxt plugins +* before mounting the application (`app:beforeMount` hook) +* mounting your app if the error was not handled with `onErrorCaptured` or `vue:error` hook +* the Vue app is initialized and mounted in browser (`app:mounted`). + +The lifecycle hooks are listed [here](/docs/api/advanced/hooks). + +You can customize this error page by adding `~/error.vue` in the source directory of your application, alongside `app.vue`. Although it is called an 'error page' it's not a route and shouldn't be placed in your `~/pages` directory. For the same reason, you shouldn't use `definePageMeta` within this page. + + The error page has a single prop - `error` which contains an error for you to handle. + +The `error` object provides the fields: `url`, `statusCode`, `statusMessage`, `message`, `description` and `data`. If you have an error with custom fields they will be lost; you should assign them to `data` instead. For custom errors we highly recommend to use `onErrorCaptured` composable that can be called in a page/component setup function or `vue:error` runtime nuxt hook that can be configured in a nuxt plugin. + +```ts +export default defineNuxtPlugin(nuxtApp => { + nuxtApp.hook('vue:error', (err) => { + // + }) +}) +``` When you are ready to remove the error page, you can call the `clearError` helper function, which takes an optional path to redirect to (for example, if you want to navigate to a 'safe' page). @@ -66,20 +94,24 @@ When you are ready to remove the error page, you can call the `clearError` helpe Make sure to check before using anything dependent on Nuxt plugins, such as `$route` or `useRouter`, as if a plugin threw an error, then it won't be re-run until you clear the error. :: +::alert{type="warning"} +If you are running on Node 16 and you set any cookies when rendering your error page, they will [overwrite cookies previously set](https://github.com/nuxt/nuxt/pull/20585). We recommend using a newer version of Node as Node 16 will reach end-of-life in September 2023. +:: + ### Example ```vue [error.vue] - - - + + ``` ## Error Helper Methods @@ -106,8 +138,8 @@ If you throw an error created with `createError`: ### Example -```vue [pages/movies/[slug].vue] - ``` -### Vue Auto-imports +#### Vue Auto-imports Vue 3 exposes Reactivity APIs like `ref` or `computed`, as well as lifecycle hooks and helpers that are auto-imported by Nuxt. ```vue - ``` -### Using Vue and Nuxt composables +#### Using Vue and Nuxt composables @@ -54,16 +56,16 @@ When you are using the built-in Composition API composables provided by Vue and During a component lifecycle, Vue tracks the temporary instance of the current component (and similarly, Nuxt tracks a temporary instance of `nuxtApp`) via a global variable, and then unsets it in same tick. This is essential when server rendering, both to avoid cross-request state pollution (leaking a shared reference between two users) and to avoid leakage between different components. -That means that (with very few exceptions) you cannot use them outside a Nuxt plugin, Nuxt route middleware or Vue setup function. On top of that, you must use them synchronously - that is, you cannot use `await` before calling a composable, except within ` ``` -## Disable Auto-imports +### Disabling Auto-imports -In case you want to disable auto-imports, you can set `imports.autoImport` to `false` in your `nuxt.config.ts`. +If you want to disable auto-importing composables and utilities, you can set `imports.autoImport` to `false` in the `nuxt.config` file. ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -121,4 +123,43 @@ export default defineNuxtConfig({ }) ``` -This will disable implicit auto imports completely but it's still possible to use [Explicit Imports](#explicit-imports). +This will disable auto-imports completely but it's still possible to use [explicit imports](#explicit-imports) from `#imports`. + +## Auto-imported Components + +Nuxt also automatically imports components from your `~/components` directory, although this is configured separately from auto-importing composables and utility functions. + +:ReadMore{link="/docs/guide/directory-structure/components"} + +To disable auto-importing components from your own `~/components` directory, you can set `components.dirs` to an empty array (though note that this will not affect components added by modules). + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + components: { + dirs: [] + } +}) +``` + +## Auto-import from third-party packages + +Nuxt also allows auto-importing from third-party packages. + +::alert +If you are using the Nuxt module for that package, it is likely that the module has already configured auto-imports for that package. +:: + +For example, you could enable the auto-import of the `useI18n` composable from the `vue-i18n` package like this: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + imports: { + presets: [ + { + from: 'vue-i18n', + imports: ['useI18n'] + } + ] + } +}) +``` diff --git a/docs/2.guide/1.concepts/2.vuejs-development.md b/docs/2.guide/1.concepts/2.vuejs-development.md index 137fb75f11..8209ee832f 100644 --- a/docs/2.guide/1.concepts/2.vuejs-development.md +++ b/docs/2.guide/1.concepts/2.vuejs-development.md @@ -36,7 +36,7 @@ Most applications need multiple pages and a way to navigate between them. This i The `app.vue` file is the entry point, which represents the page displayed in the browser window. -Inside the ` ``` -:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/nuxt/tree/main/examples/routing/nuxt-link?terminal=dev&file=/pages/index.vue" blank} - ### `target` and `rel` Attributes In this example, we use `` with `target`, `rel`, and `noRel` props. @@ -69,23 +65,21 @@ In this example, we use `` with `target`, `rel`, and `noRel` props. ``` -:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/nuxt/tree/main/examples/routing/nuxt-link?terminal=dev&file=/pages/index.vue" blank} - ## Props -- **to**: Any URL or a [route location object](https://router.vuejs.org/api/#routelocationraw) from Vue Router +- **to**: Any URL or a [route location object](https://router.vuejs.org/api/interfaces/RouteLocation.html) from Vue Router - **href**: An alias for `to`. If used with `to`, `href` will be ignored - **target**: A `target` attribute value to apply on the link - **rel**: A `rel` attribute value to apply on the link. Defaults to `"noopener noreferrer"` for external links. - **noRel**: If set to `true`, no `rel` attribute will be added to the link -- **activeClass**: A class to apply on active links. Works the same as [Vue Router's `active-class` prop](https://router.vuejs.org/api/#active-class) on internal links. Defaults to Vue Router's default (`"router-link-active"`) -- **exactActiveClass**: A class to apply on exact active links. Works the same as [Vue Router's `exact-active-class` prop](https://router.vuejs.org/api/#exact-active-class) on internal links. Defaults to Vue Router's default `"router-link-exact-active"`) -- **replace**: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/#replace) on internal links -- **ariaCurrentValue**: An `aria-current` attribute value to apply on exact active links. Works the same as [Vue Router's `aria-current-value` prop](https://router.vuejs.org/api/#aria-current-value) on internal links +- **activeClass**: A class to apply on active links. Works the same as [Vue Router's `active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-activeClass) on internal links. Defaults to Vue Router's default (`"router-link-active"`) +- **exactActiveClass**: A class to apply on exact active links. Works the same as [Vue Router's `exact-active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-exactActiveClass) on internal links. Defaults to Vue Router's default `"router-link-exact-active"`) +- **replace**: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/interfaces/RouteLocationOptions.html#Properties-replace) on internal links +- **ariaCurrentValue**: An `aria-current` attribute value to apply on exact active links. Works the same as [Vue Router's `aria-current-value` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-ariaCurrentValue) on internal links - **external**: Forces the link to be considered as external (`true`) or internal (`false`). This is helpful to handle edge-cases - **prefetch** and **noPrefetch**: Whether to enable prefetching assets for links that enter the view port. - **prefetchedClass**: A class to apply to links that have been prefetched. -- **custom**: Whether `` should wrap its content in an `` element. It allows taking full control of how a link is rendered and how navigation works when it is clicked. Works the same as [Vue Router's `custom` prop](https://router.vuejs.org/api/#custom) +- **custom**: Whether `` should wrap its content in an `` element. It allows taking full control of how a link is rendered and how navigation works when it is clicked. Works the same as [Vue Router's `custom` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-custom) ::alert{icon=👉} Defaults can be overwritten, see [overwriting defaults](#overwriting-defaults) if you want to change them. @@ -104,8 +98,6 @@ export default defineNuxtLink({ You can then use `` component as usual with your new defaults. -:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/nuxt/tree/main/examples/routing/nuxt-link?terminal=dev&file=/components/MyNuxtLink.ts" blank} - ### `defineNuxtLink` Signature ```ts @@ -121,10 +113,10 @@ defineNuxtLink({ - **componentName**: A name for the defined `` component. - **externalRelAttribute**: A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable -- **activeClass**: A default class to apply on active links. Works the same as [Vue Router's `linkActiveClass` option](https://router.vuejs.org/api/#linkactiveclass). Defaults to Vue Router's default (`"router-link-active"`) -- **exactActiveClass**: A default class to apply on exact active links. Works the same as [Vue Router's `linkExactActiveClass` option](https://router.vuejs.org/api/#linkexactactiveclass). Defaults to Vue Router's default (`"router-link-exact-active"`) +- **activeClass**: A default class to apply on active links. Works the same as [Vue Router's `linkActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkActiveClass). Defaults to Vue Router's default (`"router-link-active"`) +- **exactActiveClass**: A default class to apply on exact active links. Works the same as [Vue Router's `linkExactActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkExactActiveClass). Defaults to Vue Router's default (`"router-link-exact-active"`) - **prefetchedClass**: A default class to apply to links that have been prefetched. - **trailingSlash**: An option to either add or remove trailing slashes in the `href`. If unset or not matching the valid values `append` or `remove`, it will be ignored. -::LinkExample{link="/docs/examples/routing/nuxt-link"} +::LinkExample{link="/docs/examples/routing/pages"} :: diff --git a/docs/3.api/2.components/5.nuxt-loading-indicator.md b/docs/3.api/2.components/5.nuxt-loading-indicator.md index 13fe8a104b..17b96d6111 100644 --- a/docs/3.api/2.components/5.nuxt-loading-indicator.md +++ b/docs/3.api/2.components/5.nuxt-loading-indicator.md @@ -11,13 +11,15 @@ Add `` in your `app.vue` or layouts. ```vue [app.vue] ``` -:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/nuxt/tree/main/examples/routing/pages?terminal=dev&file=/app.vue" blank} +:button-link[Open on StackBlitz]{href="https://stackblitz.com/github/nuxt/examples/tree/main/routing/pages?file=app.vue&terminal=dev" blank} ::alert{type=warning} If you are changing layouts as well as page, the page transition you set here will not run. Instead, you should set a layout transition. diff --git a/docs/3.api/2.components/8.nuxt-island.md b/docs/3.api/2.components/8.nuxt-island.md new file mode 100644 index 0000000000..639103c496 --- /dev/null +++ b/docs/3.api/2.components/8.nuxt-island.md @@ -0,0 +1,50 @@ +--- +title: "" +description: "Nuxt provides `` component to render a non-interactive component without any client JS" +--- + +# `` + +Nuxt provide `` to render components only server side. + +When rendering an island component, the content of the island component is static, thus no JS is downloaded client-side. +Changing the island component props triggers a refetch of the island component to re-render it again. + +::alert{type=warning} +This component is experimental and in order to use it you must enable the `experimental.componentIslands` option in your `nuxt.config`. +:: + +::alert{type=info} +Global styles of your application are sent with the response +:: + +::alert{type=info} +Server only components use `` under the hood +:: + +## Props + +- **name** : Name of the component to render. + - **type**: `string` + - **required** +- **lazy**: Make the component non-blocking. + - **type**: `boolean` + - **default**: `false` +- **props**: Props to send to the component to render. + - **type**: `Record` +- **source**: Remote source to call the island to render. + - **type**: `string` + +::alert{type=warning} +Remote islands need `experimental.componentIslands` to be `'local+remote'` in your `nuxt.config`. +:: + +## `Slots` + +Slots can be passed to an island component if declared. + +Every slot is interactive since the parent component is the one providing it. + +Some slots are reserved to `NuxtIsland` for special cases. + +- **fallback**: Specify the content to be rendered before the island loads (if the component is lazy) or if `NuxtIsland` fails to fetch the component. diff --git a/docs/3.api/2.components/8.teleports.md b/docs/3.api/2.components/9.teleports.md similarity index 94% rename from docs/3.api/2.components/8.teleports.md rename to docs/3.api/2.components/9.teleports.md index 3c76d04cb1..bc3dd54071 100644 --- a/docs/3.api/2.components/8.teleports.md +++ b/docs/3.api/2.components/9.teleports.md @@ -37,5 +37,5 @@ The `to` target of [``](https://vuejs.org/guide/built-ins/teleport.htm ``` -::LinkExample{link="/docs/examples/app/teleport"} +::LinkExample{link="/docs/examples/advanced/teleport"} :: diff --git a/docs/3.api/3.utils/$fetch.md b/docs/3.api/3.utils/$fetch.md index 180a2a868c..c780897eee 100644 --- a/docs/3.api/3.utils/$fetch.md +++ b/docs/3.api/3.utils/$fetch.md @@ -9,14 +9,14 @@ Nuxt uses [ofetch](https://github.com/unjs/ofetch) to expose globally the `$fetc During server-side rendering, calling `$fetch` to fetch your internal [API routes](/docs/guide/directory-structure/server) will directly call the relevant function (emulating the request), **saving an additional API call**. -However, using `$fetch` in components without wrapping it with `useAsyncData` causes fetching the data twice: initially on the server, then again on the client-side during hydration, because `$fetch` does not transfer state from the server to the client. Thus, the fetch will be executed on both sides because the client has to get the data again. +However, using `$fetch` in components without wrapping it with [`useAsyncData`](/docs/api/composables/use-async-data) causes fetching the data twice: initially on the server, then again on the client-side during hydration, because `$fetch` does not transfer state from the server to the client. Thus, the fetch will be executed on both sides because the client has to get the data again. We recommend to use [`useFetch`](https://nuxt.com/docs/api/composables/use-fetch) or [`useAsyncData`](https://nuxt.com/docs/api/composables/use-async-data) + `$fetch` to prevent double data fetching when fetching the component data. ```vue - ``` @@ -25,6 +25,8 @@ definePageMeta(meta: PageMeta) => void interface PageMeta { validate?: (route: RouteLocationNormalized) => boolean | Promise | Partial | Promise> redirect?: RouteRecordRedirectOption + name?: string + path?: string alias?: string | string[] pageTransition?: boolean | TransitionProps layoutTransition?: boolean | TransitionProps @@ -32,7 +34,8 @@ interface PageMeta { keepalive?: boolean | KeepAliveProps layout?: false | LayoutKey | Ref | ComputedRef middleware?: MiddlewareKey | NavigationGuard | Array - [key: string]: any + scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean) + [key: string]: unknown } ``` @@ -44,6 +47,18 @@ interface PageMeta { An object accepting the following page metadata: + **`name`** + + - **Type**: `string` + + You may define a name for this page's route. By default, name is generated based on path inside the [`pages/` directory](/docs/guide/directory-structure/pages). + + **`path`** + + - **Type**: `string` + + You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. + **`alias`** - **Type**: `string | string[]` @@ -98,6 +113,12 @@ interface PageMeta { Validate whether a given route can validly be rendered with this page. Return true if it is valid, or false if not. If another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked). + **`scrollToTop`** + + - **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean` + + Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/directory-structure/pages/#router-options)) for more info. + **`[key: string]`** - **Type**: `any` @@ -115,16 +136,16 @@ The example below demonstrates: - adding `pageType` as a custom property: ```vue [pages/some-page.vue] - ``` @@ -133,42 +154,44 @@ The example below demonstrates: The example below shows how the middleware can be defined using a `function` directly within the `definePageMeta` or set as a `string` that matches the middleware file name located in the `middleware/` directory: ```vue [pages/some-page.vue] - ``` ### Defining Layout -You can define the layout that matches the layout's file name located (by default) in the `layouts/` directory. You can also disable the layout by setting the `layout` to `false`: +You can define the layout that matches the layout's file name located (by default) in the [`layouts/` directory](/docs/guide/directory-structure/layouts). You can also disable the layout by setting the `layout` to `false`: ```vue [pages/some-page.vue] - ``` diff --git a/docs/3.api/3.utils/navigate-to.md b/docs/3.api/3.utils/navigate-to.md index 6130891a66..e2dc246e91 100644 --- a/docs/3.api/3.utils/navigate-to.md +++ b/docs/3.api/3.utils/navigate-to.md @@ -18,6 +18,7 @@ interface NavigateToOptions { replace?: boolean redirectCode?: number external?: boolean + open?: OpenOptions } ``` @@ -29,7 +30,7 @@ Make sure to always use `await` or `return` on result of `navigateTo` when calli ### `to` -**Type**: [`RouteLocationRaw`](https://router.vuejs.org/api/#routelocationraw) | `undefined` | `null` +**Type**: [`RouteLocationRaw`](https://router.vuejs.org/api/interfaces/RouteLocation.html) | `undefined` | `null` **Default**: `'/'` @@ -69,12 +70,64 @@ An object accepting the following properties: Allows navigating to an external URL when set to `true`. Otherwise, `navigateTo` will throw an error, as external navigation is not allowed by default. +- `open` (optional) + + **Type**: `OpenOptions` + + Allows navigating to the URL using the [open()](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) method of the window. This option is only applicable on the client side and will be ignored on the server side. + + An object accepting the following properties: + + - `target` + + **Type**: `string` + + **Default**: `'_blank'` + + A string, without whitespace, specifying the name of the browsing context the resource is being loaded into. + + - `windowFeatures` (optional) + + **Type**: `OpenWindowFeatures` + + An object accepting the following properties: + + - `popup` (optional) + + **Type**: `boolean` + + - `width` or `innerWidth` (optional) + + **Type**: `number` + + - `height` or `innerHeight` (optional) + + **Type**: `number` + + - `left` or `screenX` (optional) + + **Type**: `number` + + - `top` or `screenY` (optional) + + **Type**: `number` + + - `noopener` (optional) + + **Type**: `boolean` + + - `noreferrer` (optional) + + **Type**: `boolean` + + Refer to the [documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) for more detailed information on the **windowFeatures** properties. + ## Examples ### Navigating Within a Vue Component ```vue - ``` + +### Navigating using open() + +```vue + +``` diff --git a/docs/3.api/3.utils/on-before-route-leave.md b/docs/3.api/3.utils/on-before-route-leave.md index ff484625bd..af5cc55469 100644 --- a/docs/3.api/3.utils/on-before-route-leave.md +++ b/docs/3.api/3.utils/on-before-route-leave.md @@ -7,5 +7,5 @@ description: The onBeforeRouteLeave composable allows registering a route guard The `onBeforeRouteLeave` composable adds a navigation guard that triggers whenever the component for the current location is about to be left. -::ReadMore{link="https://router.vuejs.org/api/index.html#onbeforerouteleave"} +::ReadMore{link="https://router.vuejs.org/api/#Functions-onBeforeRouteLeave"} :: diff --git a/docs/3.api/3.utils/on-before-route-update.md b/docs/3.api/3.utils/on-before-route-update.md index 16166a5288..c6886147e7 100644 --- a/docs/3.api/3.utils/on-before-route-update.md +++ b/docs/3.api/3.utils/on-before-route-update.md @@ -7,5 +7,5 @@ description: The onBeforeRouteUpdate composable allows registering a route guard The `onBeforeRouteUpdate` composable adds a navigation guard that triggers whenever the component for the current location is about to be updated. -::ReadMore{link="https://router.vuejs.org/api/index.html#onbeforerouteupdate"} +::ReadMore{link="https://router.vuejs.org/api/#Functions-onBeforeRouteUpdate"} :: diff --git a/docs/3.api/3.utils/prerender-routes.md b/docs/3.api/3.utils/prerender-routes.md new file mode 100644 index 0000000000..f672364210 --- /dev/null +++ b/docs/3.api/3.utils/prerender-routes.md @@ -0,0 +1,20 @@ +--- +description: prerenderRoutes hints to Nitro to prerender an additional route. +--- + +# `prerenderRoutes` + +When prerendering, you can hint to Nitro to prerender additional paths, even if their URLs do not show up in the HTML of the generated page. + +`prerenderRoutes` can only be called within component setup functions, plugins, and route middleware. + +```js +const route = useRoute() + +prerenderRoutes('/') +prerenderRoutes(['/', '/about']) +``` + +::alert{icon=👉} +In the browser, or if called outside prerendering, `prerenderRoutes` will have no effect. +:: diff --git a/docs/3.api/3.utils/refresh-nuxt-data.md b/docs/3.api/3.utils/refresh-nuxt-data.md index dc3fa423c5..2150d61670 100644 --- a/docs/3.api/3.utils/refresh-nuxt-data.md +++ b/docs/3.api/3.utils/refresh-nuxt-data.md @@ -5,7 +5,7 @@ description: refreshNuxtData refetches all data from the server and updates the # `refreshNuxtData` -`refreshNuxtData` re-fetches all data from the server and updates the page as well as invalidates the cache of `useAsyncData`, `useLazyAsyncData`, `useFetch` and `useLazyFetch`. +`refreshNuxtData` re-fetches all data from the server and updates the page as well as invalidates the cache of [`useAsyncData`](/docs/api/composables/use-async-data) , `useLazyAsyncData`, [`useFetch`](/docs/api/composables/use-fetch) and `useLazyFetch`. ## Type @@ -19,24 +19,16 @@ refreshNuxtData(keys?: string | string[]) **Type**: `String | String[]` - `refreshNuxtData` accepts a single or an array of strings as `keys` that are used to fetch the data. This parameter is **optional**. All `useAsyncData` and `useFetch` are re-fetched when no `keys` are specified. + `refreshNuxtData` accepts a single or an array of strings as `keys` that are used to fetch the data. This parameter is **optional**. All [`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) are re-fetched when no `keys` are specified. ## Examples ### Refresh All data -This example below refreshes all data being fetched using `useAsyncData` and `useFetch` on the current page. +This example below refreshes all data being fetched using [`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) on the current page. ```vue [pages/some-page.vue] - - - + + ``` ### Refresh Specific Data @@ -54,17 +54,17 @@ const refreshAll = async () => { This example below refreshes only data where the key matches to `count`. ```vue [pages/some-page.vue] + + - - ``` ::ReadMore{link="/docs/getting-started/data-fetching"} diff --git a/docs/3.api/3.utils/update-app-config.md b/docs/3.api/3.utils/update-app-config.md index b457ea0104..a51dfcacdf 100644 --- a/docs/3.api/3.utils/update-app-config.md +++ b/docs/3.api/3.utils/update-app-config.md @@ -5,7 +5,7 @@ Updates [app config](/docs/guide/directory-structure/app-config) using deep assi **Usage:** ```js -const appConfig = useAppConfig() // { foo: 'bar' } +const appConfig = useAppConfig() // { foo: 'bar' } const newAppConfig = { foo: 'baz' } diff --git a/docs/3.api/4.advanced/2.kit.md b/docs/3.api/4.advanced/2.kit.md deleted file mode 100644 index 6eb63473e1..0000000000 --- a/docs/3.api/4.advanced/2.kit.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: "Kit Utilities" -description: Nuxt Kit provides composable utilities to help interacting with Nuxt Hooks and Nuxt Builder. ---- - -# Kit Utilities - -::ReadMore{link="/docs/guide/going-further/kit"} -:: - -## Utilities - -### Modules - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/module) - -- `installModule(module, inlineOptions)` - -### Programmatic Usage - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/loader) - -- `loadNuxt(loadOptions)` -- `buildNuxt(nuxt)` -- `loadNuxtConfig(loadOptions)` - -### Compatibility - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/compatibility.ts) - -- `checkNuxtCompatibility(constraints)` -- `assertNuxtCompatibility(constraints)` -- `hasNuxtCompatibility(constraints)` -- `isNuxt2()` -- `isNuxt3()` -- `getNuxtVersion()` - -### Auto-imports - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/imports.ts) - -- `addImports(imports)` -- `addImportsDir(importDirs)` -- `addImportsSources(importSources)` - -### Components - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/components.ts) - -- `addComponentsDir(dir)` -- `addComponent(componentObject)` - -### Context - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/context.ts) - -- `useNuxt()` - -### Pages - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/pages.ts) - -- `extendPages (callback: pages => void)` -- `extendRouteRules (route: string, rule: NitroRouteConfig, options: ExtendRouteRulesOptions)` -- `addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], options: AddRouteMiddlewareOptions)` - -### Plugins - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/plugin.ts) - -- `addPlugin(pluginOptions, { append? })` -- `addPluginTemplate(pluginOptions, { append? })` - -### Templates - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/template.ts) - -- `addTemplate(templateOptions)` -- `updateTemplates({ filter?: ResolvedNuxtTemplate => boolean })` - -### Nitro - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/nitro.ts) - -- `addServerHandler (handler)` -- `addDevServerHandler (handler)` -- `useNitro()` (only usable after `ready` hook) -- `addServerPlugin` -- `addPrerenderRoutes` - -### Resolving - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/resolve.ts) - -- `resolvePath (path, resolveOptions?)` -- `resolveAlias (path, aliases?)` -- `findPath (paths, resolveOptions?)` -- `createResolver (base)` - -### Logging - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/logger.ts) - -- `useLogger(scope?)` - -### Builder - -[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/build.ts) - -- `extendWebpackConfig(callback, options?)` -- `extendViteConfig(callback, options?)` -- `addWebpackPlugin(webpackPlugin, options?)` -- `addVitePlugin(vitePlugin, options?)` diff --git a/docs/3.api/4.kit/1.modules.md b/docs/3.api/4.kit/1.modules.md new file mode 100644 index 0000000000..80e6f9adfe --- /dev/null +++ b/docs/3.api/4.kit/1.modules.md @@ -0,0 +1,170 @@ +--- +title: "Modules" +description: Nuxt Kit provides a set of utilities to help you create and use modules. You can use these utilities to create your own modules or to reuse existing modules. +--- +# Modules + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/module) + +Modules are the building blocks of Nuxt. Kit provides a set of utilities to help you create and use modules. You can use these utilities to create your own modules or to reuse existing modules. For example, you can use the `defineNuxtModule` function to define a module and the `installModule` function to install a module programmatically. + +## `defineNuxtModule` + +Define a Nuxt module, automatically merging defaults with user provided options, installing any hooks that are provided, and calling an optional setup function for full control. + +### Type + +```ts +function defineNuxtModule (definition: ModuleDefinition | NuxtModule): NuxtModule + +type ModuleOptions = Record + +interface ModuleDefinition { + meta?: ModuleMeta + defaults?: T | ((nuxt: Nuxt) => T) + schema?: T + hooks?: Partial + setup?: (this: void, resolvedOptions: T, nuxt: Nuxt) => Awaitable +} + +interface NuxtModule { + (this: void, inlineOptions: T, nuxt: Nuxt): Awaitable + getOptions?: (inlineOptions?: T, nuxt?: Nuxt) => Promise + getMeta?: () => Promise +} + +interface ModuleSetupReturn { + timings?: { + setup?: number + [key: string]: number | undefined + } +} + +interface ModuleMeta { + name?: string + version?: string + configKey?: string + compatibility?: NuxtCompatibility + [key: string]: unknown +} +``` + +### Parameters + +#### `definition` + +**Type**: `ModuleDefinition | NuxtModule` + +**Required**: `true` + +A module definition object or a module function. + +- `meta` (optional) + + **Type**: `ModuleMeta` + + Metadata of the module. It defines the module name, version, config key and compatibility. + +- `defaults` (optional) + + **Type**: `T | ((nuxt: Nuxt) => T)` + + Default options for the module. If a function is provided, it will be called with the Nuxt instance as the first argument. + +- `schema` (optional) + + **Type**: `T` + + Schema for the module options. If provided, options will be applied to the schema. + +- `hooks` (optional) + + **Type**: `Partial` + + Hooks to be installed for the module. If provided, the module will install the hooks. + +- `setup` (optional) + + **Type**: `(this: void, resolvedOptions: T, nuxt: Nuxt) => Awaitable` + + Setup function for the module. If provided, the module will call the setup function. + +### Examples + +```ts +// https://github.com/nuxt/starter/tree/module +import { defineNuxtModule } from '@nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: 'my-module', + configKey: 'myModule' + }, + defaults: { + test: 123 + }, + setup (options, nuxt) { + nuxt.hook('modules:done', () => { + console.log('My module is ready with current test option: ', options.test) + }) + } +}) +``` + +## `installModule` + +Install specified Nuxt module programmatically. This is helpful when your module depends on other modules. You can pass the module options as an object to `inlineOptions` and they will be passed to the module's `setup` function. + +### Type + +```ts +async function installModule (moduleToInstall: string | NuxtModule, inlineOptions?: any, nuxt?: Nuxt) +``` + +### Parameters + +#### `moduleToInstall` + +**Type**: `string` | `NuxtModule` + +**Required**: `true` + +The module to install. Can be either a string with the module name or a module object itself. + +#### `inlineOptions` + +**Type**: `any` + +**Default**: `{}` + +An object with the module options to be passed to the module's `setup` function. + +#### `nuxt` + +**Type**: `Nuxt` + +**Default**: `useNuxt()` + +Nuxt instance. If not provided, it will be retrieved from the context via `useNuxt()` call. + +### Examples + +```ts +import { defineNuxtModule, installModule } from '@nuxt/kit' + +export default defineNuxtModule({ + async setup (options, nuxt) { + // will install @nuxtjs/fontaine with Roboto font and Impact fallback + await installModule('@nuxtjs/fontaine', { + // module configuration + fonts: [ + { + family: 'Roboto', + fallbacks: ['Impact'], + fallbackName: 'fallback-a', + } + ] + }) + } +}) +``` diff --git a/docs/3.api/4.kit/10.templates.md b/docs/3.api/4.kit/10.templates.md new file mode 100644 index 0000000000..1c34766bb1 --- /dev/null +++ b/docs/3.api/4.kit/10.templates.md @@ -0,0 +1,281 @@ +--- +title: "Templates" +description: Nuxt Kit provides a set of utilities to help you work with templates. These functions allow you to generate extra files during development and build time. +--- + +# Templates + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/template.ts) + +Templates allows to generate extra files during development and build time. These files will be available in virtual filesystem and can be used in plugins, layouts, components, etc. `addTemplate` and `addTypeTemplate` allow you to add templates to the Nuxt application. `updateTemplates` allows you to regenerate templates that match the filter. + +## `addTemplate` + +Renders given template during build into the project buildDir. + +### Type + +```ts +function addTemplate (template: NuxtTemplate | string): ResolvedNuxtTemplate + +interface NuxtTemplate { + src?: string + filename?: string + dst?: string + options?: Record + getContents?: (data: Record) => string | Promise + write?: boolean +} + +interface ResolvedNuxtTemplate { + src: string + filename: string + dst: string + options: Record + getContents: (data: Record) => string | Promise + write: boolean + filename: string + dst: string +} +``` + +### Parameters + +#### `template` + +**Type**: `NuxtTemplate | string` + +**Required**: `true` + +A template object or a string with the path to the template. If a string is provided, it will be converted to a template object with `src` set to the string value. If a template object is provided, it must have the following properties: + +- `src` (optional) + + **Type**: `string` + + Path to the template. If `src` is not provided, `getContents` must be provided instead. + +- `filename` (optional) + + **Type**: `string` + + Filename of the template. If `filename` is not provided, it will be generated from the `src` path. In this case, the `src` option is required. + +- `dst` (optional) + + **Type**: `string` + + Path to the destination file. If `dst` is not provided, it will be generated from the `filename` path and nuxt `buildDir` option. + +- `options` (optional) + + **Type**: `Options` + + Options to pass to the template. + +- `getContents` (optional) + + **Type**: `(data: Options) => string | Promise` + + A function that will be called with the `options` object. It should return a string or a promise that resolves to a string. If `src` is provided, this function will be ignored. + +- `write` (optional) + + **Type**: `boolean` + + If set to `true`, the template will be written to the destination file. Otherwise, the template will be used only in virtual filesystem. + +### Examples + +::code-group + +```ts [module.ts] +// https://github.com/nuxt/bridge +import { addTemplate, defineNuxtModule } from '@nuxt/kit' +import { defu } from 'defu' + +export default defineNuxtModule({ + setup(options, nuxt) { + const globalMeta = defu(nuxt.options.app.head, { + charset: options.charset, + viewport: options.viewport + }) + + addTemplate({ + filename: 'meta.config.mjs', + getContents: () => 'export default ' + JSON.stringify({ globalMeta, mixinKey: 'setup' }) + }) + } +}) +``` + +```ts [plugin.ts] +import { createHead as createClientHead, createServerHead } from '@unhead/vue' +import { defineNuxtPlugin } from '#app' +// @ts-ignore +import metaConfig from '#build/meta.config.mjs' + +export default defineNuxtPlugin((nuxtApp) => { + const createHead = process.server ? createServerHead : createClientHead + const head = createHead() + head.push(metaConfig.globalMeta) + + nuxtApp.vueApp.use(head) +}) +``` + +:: + +## `addTypeTemplate` + +Renders given template during build into the project buildDir, then registers it as types. + +### Type + +```ts +function addTypeTemplate (template: NuxtTypeTemplate | string): ResolvedNuxtTemplate + +interface NuxtTemplate { + src?: string + filename?: string + dst?: string + options?: Record + getContents?: (data: Record) => string | Promise +} + +interface ResolvedNuxtTemplate { + src: string + filename: string + dst: string + options: Record + getContents: (data: Record) => string | Promise + write: boolean + filename: string + dst: string +} +``` + +### Parameters + +#### `template` + +**Type**: `NuxtTypeTemplate | string` + +**Required**: `true` + +A template object or a string with the path to the template. If a string is provided, it will be converted to a template object with `src` set to the string value. If a template object is provided, it must have the following properties: + +- `src` (optional) + + **Type**: `string` + + Path to the template. If `src` is not provided, `getContents` must be provided instead. + +- `filename` (optional) + + **Type**: `string` + + Filename of the template. If `filename` is not provided, it will be generated from the `src` path. In this case, the `src` option is required. + +- `dst` (optional) + + **Type**: `string` + + Path to the destination file. If `dst` is not provided, it will be generated from the `filename` path and nuxt `buildDir` option. + +- `options` (optional) + + **Type**: `Options` + + Options to pass to the template. + +- `getContents` (optional) + + **Type**: `(data: Options) => string | Promise` + + A function that will be called with the `options` object. It should return a string or a promise that resolves to a string. If `src` is provided, this function will be ignored. + +### Examples + +```ts +// https://github.com/Hebilicious/nuxtpress +import { addTypeTemplate, defineNuxtModule } from "@nuxt/kit" + +export default defineNuxtModule({ + setup() { + addTypeTemplate({ + filename: "types/markdown.d.ts", + getContents: () => /* ts */` + declare module '*.md' { + import type { ComponentOptions } from 'vue' + const Component: ComponentOptions + export default Component + }` + }) + } +} +``` + +## `updateTemplates` + +Regenerate templates that match the filter. If no filter is provided, all templates will be regenerated. + +### Type + +```ts +async function updateTemplates (options: UpdateTemplatesOptions): void + +interface UpdateTemplatesOptions { + filter?: (template: ResolvedNuxtTemplate) => boolean +} + +interface ResolvedNuxtTemplate { + src: string + filename: string + dst: string + options: Record + getContents: (data: Record) => string | Promise + write: boolean + filename: string + dst: string +} +``` + +### Parameters + +#### `options` + +**Type**: `UpdateTemplatesOptions` + +**Default**: `{}` + +Options to pass to the template. This object can have the following property: + +- `filter` (optional) + + **Type**: `(template: ResolvedNuxtTemplate) => boolean` + + A function that will be called with the `template` object. It should return a boolean indicating whether the template should be regenerated. If `filter` is not provided, all templates will be regenerated. + +### Example + +```ts +// https://github.com/nuxt/nuxt +import { defineNuxtModule, updateTemplates } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options, nuxt) { + // watch and rebuild routes template list when one of the pages changes + nuxt.hook('builder:watch', async (event, relativePath) => { + if (event === 'change') { return } + + const path = resolve(nuxt.options.srcDir, relativePath) + if (updateTemplatePaths.some(dir => path.startsWith(dir))) { + await updateTemplates({ + filter: template => template.filename === 'routes.mjs' + }) + } + }) + } +}) +``` diff --git a/docs/3.api/4.kit/11.nitro.md b/docs/3.api/4.kit/11.nitro.md new file mode 100644 index 0000000000..001c6424ea --- /dev/null +++ b/docs/3.api/4.kit/11.nitro.md @@ -0,0 +1,359 @@ +--- +title: "Nitro" +description: Nuxt Kit provides a set of utilities to help you work with Nitro. These functions allow you to add server handlers, plugins, and prerender routes. +--- + +# Nitro + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/nitro.ts) + +Nitro is an open source TypeScript framework to build ultra-fast web servers. Nuxt 3 (and, optionally, Nuxt Bridge) uses Nitro as its server engine. You can use `useNitro` to access the Nitro instance, `addServerHandler` to add a server handler, `addDevServerHandler` to add a server handler to be used only in development mode, `addServerPlugin` to add a plugin to extend Nitro's runtime behavior, and `addPrerenderRoutes` to add routes to be prerendered by Nitro. + +## `addServerHandler` + +Adds a nitro server handler. Use it if you want to create server middleware or custom route. + +### Type + +```ts +function addServerHandler (handler: NitroEventHandler): void + +export interface NitroEventHandler { + handler: string; + route?: string; + middleware?: boolean; + lazy?: boolean; + method?: string; +} +``` + +### Parameters + +#### `handler` + +**Type**: `NitroEventHandler` + +**Required**: `true` + +A handler object with the following properties: + +- `handler` (required) + + **Type**: `string` + + Path to event handler. + +- `route` (optional) + + **Type**: `string` + + Path prefix or route. If an empty string used, will be used as a middleware. + +- `middleware` (optional) + + **Type**: `boolean` + + Specifies this is a middleware handler. Middleware are called on every route and should normally return nothing to pass to the next handlers. + +- `lazy` (optional) + + **Type**: `boolean` + + Use lazy loading to import handler. + +- `method` (optional) + + **Type**: `string` + + Router method matcher. If handler name contains method name, it will be used as a default value. + +### Examples + +::code-group + +```ts [module.ts] +// https://github.com/nuxt-modules/robots +import { createResolver, defineNuxtModule, addServerHandler } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options) { + const resolver = createResolver(import.meta.url) + + addServerHandler({ + route: '/robots.txt' + handler: resolver.resolve('./runtime/robots.get.ts') + }) + } +}) +``` + +```ts [runtime/robots.get.ts] +export default defineEventHandler(() => { + return { + body: `User-agent: *\nDisallow: /` + } +}) +``` + +:: + +## `addDevServerHandler` + +Adds a nitro server handler to be used only in development mode. This handler will be excluded from production build. + +### Type + +```ts +function addDevServerHandler (handler: NitroEventHandler): void + +export interface NitroEventHandler { + handler: string; + route?: string; + middleware?: boolean; + lazy?: boolean; + method?: string; +} +``` + +### Parameters + +#### `handler` + +**Type**: `NitroEventHandler` + +**Required**: `true` + +A handler object with the following properties: + +- `handler` (required) + + **Type**: `string` + + Path to event handler. + +- `route` (optional) + + **Type**: `string` + + Path prefix or route. If an empty string used, will be used as a middleware. + +- `middleware` (optional) + + **Type**: `boolean` + + Specifies this is a middleware handler. Middleware are called on every route and should normally return nothing to pass to the next handlers. + +- `lazy` (optional) + + **Type**: `boolean` + + Use lazy loading to import handler. + +- `method` (optional) + + **Type**: `string` + + Router method matcher. + +### Examples + +::code-group + +```ts [module.ts] +import { createResolver, defineNuxtModule, addDevServerHandler } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const resolver = createResolver(import.meta.url) + + addDevServerHandler({ + handler: resolver.resolve('./runtime/uptime.get'), + route: '/_handler' + }) + } +}) +``` + +```ts [runtime/timer.get.ts] +export default defineEventHandler(() => { + return { + body: `Response generated at ${new Date().toISOString()}` + } +}) +``` + +:: + +```ts +// https://github.com/nuxt-modules/tailwindcss +import { joinURL } from 'ufo' +import { defineNuxtModule, addDevServerHandler } from '@nuxt/kit' + +export default defineNuxtModule({ + async setup(options) { + const route = joinURL(nuxt.options.app?.baseURL, '/_tailwind') + + // @ts-ignore + const createServer = await import('tailwind-config-viewer/server/index.js').then(r => r.default || r) as any + const viewerDevMiddleware = createServer({ tailwindConfigProvider: () => options, routerPrefix: route }).asMiddleware() + + addDevServerHandler({ route, handler: viewerDevMiddleware }) + } +}) +``` + +## `useNitro` + +Returns the Nitro instance. + +::alert{type=warning} +You can call `useNitro()` only after `ready` hook. +:: + +::alert{type=info} +Changes to the Nitro instance configuration are not applied. +:: + +### Type + +```ts +function useNitro (): Nitro + +export interface Nitro { + options: NitroOptions; + scannedHandlers: NitroEventHandler[]; + vfs: Record; + hooks: Hookable; + unimport?: Unimport; + logger: ConsolaInstance; + storage: Storage; + close: () => Promise; + updateConfig: (config: NitroDynamicConfig) => void | Promise; +} +``` + +### Examples + +```ts +// https://github.com/nuxt/nuxt/blob/4e05650cde31ca73be4d14b1f0d23c7854008749/packages/nuxt/src/core/nuxt.ts#L404 +import { defineNuxtModule, useNitro, addPlugin, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options, nuxt) { + const resolver = createResolver(import.meta.url) + + nuxt.hook('ready', () => { + const nitro = useNitro() + if (nitro.options.static && nuxt.options.experimental.payloadExtraction === undefined) { + console.warn('Using experimental payload extraction for full-static output. You can opt-out by setting `experimental.payloadExtraction` to `false`.') + nuxt.options.experimental.payloadExtraction = true + } + nitro.options.replace['process.env.NUXT_PAYLOAD_EXTRACTION'] = String(!!nuxt.options.experimental.payloadExtraction) + nitro.options._config.replace!['process.env.NUXT_PAYLOAD_EXTRACTION'] = String(!!nuxt.options.experimental.payloadExtraction) + + if (!nuxt.options.dev && nuxt.options.experimental.payloadExtraction) { + addPlugin(resolver.resolve(nuxt.options.appDir, 'plugins/payload.client')) + } + }) + } +}) +``` + +## `addServerPlugin` + +Add plugin to extend Nitro's runtime behavior. + +::alert{type=info} +You can read more about Nitro plugins in the [Nitro documentation](https://nitro.unjs.io/guide/plugins). +:: + +### Type + +```ts +function addServerPlugin (plugin: string): void +``` + +### Parameters + +#### `plugin` + +**Type**: `string` + +**Required**: `true` + +Path to the plugin. The plugin must export a function that accepts Nitro instance as an argument. + +### Examples + +::code-group + +```ts [module.ts] +import { createResolver, defineNuxtModule, addServerPlugin } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const resolver = createResolver(import.meta.url) + addServerPlugin(resolver.resolve('./runtime/plugin.ts')) + } +}) +``` + +```ts [runtime/plugin.ts] +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook("request", (event) => { + console.log("on request", event.path); + }); + + nitroApp.hooks.hook("beforeResponse", (event, { body }) => { + console.log("on response", event.path, { body }); + }); + + nitroApp.hooks.hook("afterResponse", (event, { body }) => { + console.log("on after response", event.path, { body }); + }); +}); +``` + +:: + +## `addPrerenderRoutes` + +Add routes to be prerendered to Nitro. + +### Type + +```ts +function function addPrerenderRoutes (routes: string | string[]): void +``` + +### Parameters + +#### `routes` + +**Type**: `string | string[]` + +**Required**: `true` + +A route or an array of routes to prerender. + +### Examples + +```ts +import { defineNuxtModule, prerenderRoutes } from '@nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: 'nuxt-sitemap', + configKey: 'sitemap', + }, + defaults: { + sitemapUrl: '/sitemap.xml', + prerender: true, + }, + setup(options) { + if (options.prerender) { + prerenderRoutes(options.sitemapUrl) + } + } +}) +``` diff --git a/docs/3.api/4.kit/12.resolving.md b/docs/3.api/4.kit/12.resolving.md new file mode 100644 index 0000000000..750179948c --- /dev/null +++ b/docs/3.api/4.kit/12.resolving.md @@ -0,0 +1,263 @@ +--- +title: Resolving +description: Nuxt Kit provides a set of utilities to help you resolve paths. These functions allow you to resolve paths relative to the current module, with unknown name or extension. +--- + +# Resolving + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/resolve.ts) + +Sometimes you need to resolve a paths: relative to the current module, with unknown name or extension. For example, you may want to add a plugin that is located in the same directory as the module. To handle this cases, nuxt provides a set of utilities to resolve paths. `resolvePath` and `resolveAlias` are used to resolve paths relative to the current module. `findPath` is used to find first existing file in given paths. `createResolver` is used to create resolver relative to base path. + +## `resolvePath` + +Resolves full path to a file or directory respecting Nuxt alias and extensions options. If path could not be resolved, normalized input path will be returned. + +### Type + +```ts +async function resolvePath (path: string, options?: ResolvePathOptions): Promise +``` + +### Parameters + +#### `path` + +**Type**: `string` + +**Required**: `true` + +Path to resolve. + +#### `options` + +**Type**: `ResolvePathOptions` + +**Default**: `{}` + +Options to pass to the resolver. This object can have the following properties: + +- `cwd` (optional) + + **Type**: `string` + + **Default**: `process.cwd()` + + Current working directory. + +- `alias` (optional) + + **Type**: `Record` + + **Default**: `{}` + + Alias map. + +- `extensions` (optional) + + **Type**: `string[]` + + **Default**: `['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json']` + + Extensions to try. + +### Examples + +```ts +// https://github.com/P4sca1/nuxt-headlessui +import { defineNuxtModule, resolvePath } from '@nuxt/kit' +import { join } from 'pathe' + +const headlessComponents: ComponentGroup[] = [ + { + relativePath: 'combobox/combobox.js', + chunkName: 'headlessui/combobox', + exports: [ + 'Combobox', + 'ComboboxLabel', + 'ComboboxButton', + 'ComboboxInput', + 'ComboboxOptions', + 'ComboboxOption' + ] + }, +] + +export default defineNuxtModule({ + meta: { + name: 'nuxt-headlessui', + configKey: 'headlessui', + }, + defaults: { + prefix: 'Headless' + }, + async setup (options) { + const entrypoint = await resolvePath('@headlessui/vue') + const root = join(entrypoint, '../components') + + for (const group of headlessComponents) { + for (const e of group.exports) { + addComponent( + { + name: e, + export: e, + filePath: join(root, group.relativePath), + chunkName: group.chunkName, + mode: 'all' + } + ) + } + } + } +}) +``` + +## `resolveAlias` + +Resolves path aliases respecting Nuxt alias options. + +### Type + +```ts +function resolveAlias (path: string, alias?: Record): string +``` + +### Parameters + +#### `path` + +**Type**: `string` + +**Required**: `true` + +Path to resolve. + +#### `alias` + +**Type**: `Record` + +**Default**: `{}` + +Alias map. If not provided, it will be read from `nuxt.options.alias`. + +## `findPath` + +Try to resolve first existing file in given paths. + +### Type + +```ts +async function findPath (paths: string | string[], options?: ResolvePathOptions, pathType: 'file' | 'dir'): Promise + +interface ResolvePathOptions { + cwd?: string + alias?: Record + extensions?: string[] +} +``` + +### Parameters + +#### `paths` + +**Type**: `string | string[]` + +**Required**: `true` + +A path or an array of paths to resolve. + +#### `options` + +**Type**: `ResolvePathOptions` + +**Default**: `{}` + +Options to pass to the resolver. This object can have the following properties: + +- `cwd` (optional) + + **Type**: `string` + + **Default**: `process.cwd()` + + Current working directory. + +- `alias` (optional) + + **Type**: `Record` + + **Default**: `{}` + + Alias map. + +- `extensions` (optional) + + **Type**: `string[]` + + **Default**: `['.js', '.mjs', '.ts', '.jsx', '.tsx', '.json']` + + Extensions to try. + +#### `pathType` + +**Type**: `'file' | 'dir'` + +**Default**: `'file'` + +Type of path to resolve. If set to `'file'`, the function will try to resolve a file. If set to `'dir'`, the function will try to resolve a directory. + +## `createResolver` + +Creates resolver relative to base path. + +### Type + +```ts +function createResolver (basePath: string | URL): Resolver + +interface Resolver { + resolve (...path: string[]): string + resolvePath (path: string, options?: ResolvePathOptions): Promise +} + +interface ResolvePathOptions { + cwd?: string + alias?: Record + extensions?: string[] +} +``` + +### Parameters + +#### `basePath` + +**Type**: `string` + +**Required**: `true` + +Base path to resolve from. + +### Examples + +```ts +// https://github.com/vuejs/pinia/blob/v2/packages/nuxt +import { + defineNuxtModule, + isNuxt2, + createResolver, +} from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options, nuxt) { + const resolver = createResolver(import.meta.url) + + nuxt.hook('modules:done', () => { + if (isNuxt2()) { + addPlugin(resolver.resolve('./runtime/plugin.vue2')) + } else { + addPlugin(resolver.resolve('./runtime/plugin.vue3')) + } + }) + } +}) +``` diff --git a/docs/3.api/4.kit/13.logging.md b/docs/3.api/4.kit/13.logging.md new file mode 100644 index 0000000000..2c7c379810 --- /dev/null +++ b/docs/3.api/4.kit/13.logging.md @@ -0,0 +1,44 @@ +--- +title: "Logging" +description: Nuxt Kit provides a set of utilities to help you work with logging. These functions allow you to log messages with extra features. +--- + +# Logging + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/logger.ts) + +Nuxt provides a logger instance that you can use to log messages with extra features. `useLogger` allows you to get a logger instance. + +## `useLogger` + +Returns a logger instance. It uses [consola](https://github.com/unjs/consola) under the hood. + +### Type + +```ts +function useLogger (tag?: string): ConsolaInstance +``` + +### Parameters + +#### `tag` + +**Type**: `string` + +***Optional**: `true` + +A tag to prefix all log messages with. + +### Examples + +```ts +import { defineNuxtModule, useLogger } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options, nuxt) { + const logger = useLogger('my-module') + + logger.info('Hello from my module!') + } +}) +``` diff --git a/docs/3.api/4.kit/14.builder.md b/docs/3.api/4.kit/14.builder.md new file mode 100644 index 0000000000..2ddbb943df --- /dev/null +++ b/docs/3.api/4.kit/14.builder.md @@ -0,0 +1,490 @@ +--- +title: Builder +description: Nuxt Kit provides a set of utilities to help you work with the builder. These functions allow you to extend the webpack and vite configurations. +--- + +# Builder + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/build.ts) + +Nuxt have builders based on [webpack](https://github.com/nuxt/nuxt/tree/main/packages/webpack) and [vite](https://github.com/nuxt/nuxt/tree/main/packages/vite). You can extend the config passed to each one using `extendWebpackConfig` and `extendViteConfig` functions. You can also add additional plugins via `addVitePlugin`, `addWebpackPlugin` and `addBuildPlugin`. + +## `extendWebpackConfig` + +Extends the webpack configuration. Callback function can be called multiple times, when applying to both client and server builds. + +### Type + +```ts +function extendWebpackConfig (callback: ((config: WebpackConfig) => void), options?: ExtendWebpackConfigOptions): void + +export interface ExtendWebpackConfigOptions { + dev?: boolean + build?: boolean + server?: boolean + client?: boolean + prepend?: boolean +} +``` + +::alert{type=info} +See [webpack website](https://webpack.js.org/configuration/) for more information about webpack configuration. +:: + +### Parameters + +#### `callback` + +**Type**: `(config: WebpackConfig) => void` + +**Required**: `true` + +A callback function that will be called with the webpack configuration object. + +#### `options` + +**Type**: `ExtendWebpackConfigOptions` + +**Default**: `{}` + +Options to pass to the callback function. This object can have the following properties: + +- `dev` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in development mode. + +- `build` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in production mode. + +- `server` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the server bundle. + +- `client` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the client bundle. + +- `prepend` (optional) + + **Type**: `boolean` + + If set to `true`, the callback function will be prepended to the array with `unshift()` instead of `push()`. + +### Examples + +```ts +import { defineNuxtModule, extendWebpackConfig } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + extendWebpackConfig((config) => { + config.module?.rules.push({ + test: /\.txt$/, + use: 'raw-loader' + }) + }) + } +}) +``` + +## `extendViteConfig` + +Extends the Vite configuration. Callback function can be called multiple times, when applying to both client and server builds. + +### Type + +```ts +function extendViteConfig (callback: ((config: ViteConfig) => void), options?: ExtendViteConfigOptions): void + +export interface ExtendViteConfigOptions { + dev?: boolean + build?: boolean + server?: boolean + client?: boolean + prepend?: boolean +} +``` + +::alert{type=info} +See [Vite website](https://vitejs.dev/config/) for more information about Vite configuration. +:: + +### Parameters + +#### `callback` + +**Type**: `(config: ViteConfig) => void` + +**Required**: `true` + +A callback function that will be called with the Vite configuration object. + +#### `options` + +**Type**: `ExtendViteConfigOptions` + +**Default**: `{}` + +Options to pass to the callback function. This object can have the following properties: + +- `dev` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in development mode. + +- `build` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in production mode. + +- `server` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the server bundle. + +- `client` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the client bundle. + +- `prepend` (optional) + + **Type**: `boolean` + + If set to `true`, the callback function will be prepended to the array with `unshift()` instead of `push()`. + +### Examples + +```ts +// https://github.com/Hrdtr/nuxt-appwrite +import { defineNuxtModule, extendViteConfig } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + extendViteConfig((config) => { + config.optimizeDeps = config.optimizeDeps || {} + config.optimizeDeps.include = config.optimizeDeps.include || [] + config.optimizeDeps.include.push('cross-fetch') + }) + } +}) +``` + +## `addWebpackPlugin` + +Append webpack plugin to the config. + +### Type + +```ts +function addWebpackPlugin (pluginOrGetter: PluginOrGetter, options?: ExtendWebpackConfigOptions): void + +type PluginOrGetter = WebpackPluginInstance | WebpackPluginInstance[] | (() => WebpackPluginInstance | WebpackPluginInstance[]) + +interface ExtendWebpackConfigOptions { + dev?: boolean + build?: boolean + server?: boolean + client?: boolean + prepend?: boolean +} +``` + +::alert{type=info} +See [webpack website](https://webpack.js.org/concepts/plugins/) for more information about webpack plugins. You can also use [this collection](https://webpack.js.org/awesome-webpack/#webpack-plugins) to find a plugin that suits your needs. +:: + +### Parameters + +#### `pluginOrGetter` + +**Type**: `PluginOrGetter` + +**Required**: `true` + +A webpack plugin instance or an array of webpack plugin instances. If a function is provided, it must return a webpack plugin instance or an array of webpack plugin instances. + +#### `options` + +**Type**: `ExtendWebpackConfigOptions` + +**Default**: `{}` + +Options to pass to the callback function. This object can have the following properties: + +- `dev` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in development mode. + +- `build` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in production mode. + +- `server` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the server bundle. + +- `client` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the client bundle. + +- `prepend` (optional) + + **Type**: `boolean` + + If set to `true`, the callback function will be prepended to the array with `unshift()` instead of `push()`. + +### Examples + +```ts +// https://github.com/nuxt-modules/eslint +import EslintWebpackPlugin from 'eslint-webpack-plugin' +import { defineNuxtModule, addWebpackPlugin } from '@nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: 'nuxt-eslint', + configKey: 'eslint', + }, + defaults: nuxt => ({ + include: [`${nuxt.options.srcDir}/**/*.{js,jsx,ts,tsx,vue}`], + lintOnStart: true, + }), + setup(options, nuxt) { + const webpackOptions = { + ...options, + context: nuxt.options.srcDir, + files: options.include, + lintDirtyModulesOnly: !options.lintOnStart + } + addWebpackPlugin(new EslintWebpackPlugin(webpackOptions), { server: false }) + } +}) +``` + +## `addVitePlugin` + +Append Vite plugin to the config. + +### Type + +```ts +function addVitePlugin (pluginOrGetter: PluginOrGetter, options?: ExtendViteConfigOptions): void + +type PluginOrGetter = VitePlugin | VitePlugin[] | (() => VitePlugin | VitePlugin[]) + +interface ExtendViteConfigOptions { + dev?: boolean + build?: boolean + server?: boolean + client?: boolean + prepend?: boolean +} +``` + +::alert{type=info} +See [Vite website](https://vitejs.dev/guide/api-plugin.html) for more information about Vite plugins. You can also use [this repository](https://github.com/vitejs/awesome-vite#plugins) to find a plugin that suits your needs. +:: + +### Parameters + +#### `pluginOrGetter` + +**Type**: `PluginOrGetter` + +**Required**: `true` + +A Vite plugin instance or an array of Vite plugin instances. If a function is provided, it must return a Vite plugin instance or an array of Vite plugin instances. + +#### `options` + +**Type**: `ExtendViteConfigOptions` + +**Default**: `{}` + +Options to pass to the callback function. This object can have the following properties: + +- `dev` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in development mode. + +- `build` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in production mode. + +- `server` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the server bundle. + +- `client` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the client bundle. + +- `prepend` (optional) + + **Type**: `boolean` + + If set to `true`, the callback function will be prepended to the array with `unshift()` instead of `push()`. + +### Examples + +```ts +// https://github.com/yisibell/nuxt-svg-icons +import { defineNuxtModule, addVitePlugin } from '@nuxt/kit' +import { svg4VuePlugin } from 'vite-plugin-svg4vue' + +export default defineNuxtModule({ + meta: { + name: 'nuxt-svg-icons', + configKey: 'nuxtSvgIcons', + }, + defaults: { + svg4vue: { + assetsDirName: 'assets/icons', + }, + }, + setup(options) { + addVitePlugin(svg4VuePlugin(options.svg4vue)) + }, +}) +``` + +## `addBuildPlugin` + +Builder-agnostic version of `addWebpackPlugin` and `addVitePlugin`. It will add the plugin to both webpack and vite configurations if they are present. + +### Type + +```ts +function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?: ExtendConfigOptions): void + +interface AddBuildPluginFactory { + vite?: () => VitePlugin | VitePlugin[] + webpack?: () => WebpackPluginInstance | WebpackPluginInstance[] +} + +interface ExtendConfigOptions { + dev?: boolean + build?: boolean + server?: boolean + client?: boolean + prepend?: boolean +} +``` + +### Parameters + +#### `pluginFactory` + +**Type**: `AddBuildPluginFactory` + +**Required**: `true` + +A factory function that returns an object with `vite` and/or `webpack` properties. These properties must be functions that return a Vite plugin instance or an array of Vite plugin instances and/or a webpack plugin instance or an array of webpack plugin instances. + +#### `options` + +**Type**: `ExtendConfigOptions` + +**Default**: `{}` + +Options to pass to the callback function. This object can have the following properties: + +- `dev` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in development mode. + +- `build` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building in production mode. + +- `server` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the server bundle. + +- `client` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, the callback function will be called when building the client bundle. + +- `prepend` (optional) + + **Type**: `boolean` + + If set to `true`, the callback function will be prepended to the array with `unshift()` instead of `push()`. diff --git a/docs/3.api/4.kit/15.examples.md b/docs/3.api/4.kit/15.examples.md new file mode 100644 index 0000000000..d0477a0e91 --- /dev/null +++ b/docs/3.api/4.kit/15.examples.md @@ -0,0 +1,43 @@ +--- +title: "Examples" +description: Examples of Nuxt Kit utilities in use. +--- + +# Examples + +## Accessing Nuxt Vite Config + +If you are building an integration that needs access to the runtime Vite or webpack config that Nuxt uses, it is possible to extract this using Kit utilities. + +Some examples of projects doing this already: + +- [histoire](https://github.com/histoire-dev/histoire/blob/main/packages/histoire-plugin-nuxt/src/index.ts) +- [nuxt-vitest](https://github.com/danielroe/nuxt-vitest/blob/main/packages/nuxt-vitest/src/config.ts) +- [@storybook-vue/nuxt](https://github.com/storybook-vue/storybook-nuxt/blob/main/packages/storybook-nuxt/src/preset.ts) + +Here is a brief example of how you might access the Vite config from a project; you could implement a similar approach to get the webpack configuration. + +```js +import { loadNuxt, buildNuxt } from '@nuxt/kit' + +// https://github.com/nuxt/nuxt/issues/14534 +async function getViteConfig() { + const nuxt = await loadNuxt({ cwd: process.cwd(), dev: false, overrides: { ssr: false } }) + return new Promise((resolve, reject) => { + nuxt.hook('vite:extendConfig', (config, { isClient }) => { + if (isClient) { + resolve(config) + throw new Error('_stop_') + } + }) + buildNuxt(nuxt).catch((err) => { + if (!err.toString().includes('_stop_')) { + reject(err) + } + }) + }).finally(() => nuxt.close()) +} + +const viteConfig = await getViteConfig() +console.log(viteConfig) +``` diff --git a/docs/3.api/4.kit/2.programmatic.md b/docs/3.api/4.kit/2.programmatic.md new file mode 100644 index 0000000000..8f28ac6a9a --- /dev/null +++ b/docs/3.api/4.kit/2.programmatic.md @@ -0,0 +1,146 @@ +--- +title: "Programmatic Usage" +description: Nuxt Kit provides a set of utilities to help you work with Nuxt programmatically. These functions allow you to load Nuxt, build Nuxt, and load Nuxt configuration. +--- + +# Programmatic Usage + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/loader) + +Programmatic usage can be helpful when you want to use Nuxt programmatically, for example, when building a [CLI tool](https://github.com/nuxt/cli) or [test utils](https://github.com/nuxt/nuxt/tree/main/packages/test-utils). + +## `loadNuxt` + +Load Nuxt programmatically. It will load the Nuxt configuration, instantiate and return the promise with Nuxt instance. + +### Type + +```ts +async function loadNuxt (loadOptions?: LoadNuxtOptions): Promise + +interface LoadNuxtOptions extends LoadNuxtConfigOptions { + dev?: boolean + ready?: boolean + rootDir?: string + config?: LoadNuxtConfigOptions['overrides'] +} +``` + +### Parameters + +#### `loadOptions` + +**Type**: `LoadNuxtOptions` + +**Default**: `{}` + +Loading conditions for Nuxt. `loadNuxt` uses [`c12`](https://github.com/unjs/c12) under the hood, so it accepts the same options as `c12.loadConfig` with some additional options: + +- `dev` (optional) + + **Type**: `boolean` + + **Default**: `false` + + If set to `true`, Nuxt will be loaded in development mode. + +- `ready` (optional) + + **Type**: `boolean` + + **Default**: `true` + + If set to `true`, Nuxt will be ready to use after the `loadNuxt` call. If set to `false`, you will need to call `nuxt.ready()` to make sure Nuxt is ready to use. + +- `rootDir` (optional) + + **Type**: `string` + + **Default**: `null` + + **Deprecated**: Use `cwd` option instead. + + The root directory of the Nuxt project. + +- `config` (optional) + + **Type**: `LoadNuxtConfigOptions['overrides']` + + **Default**: `{}` + + **Deprecated**: Use `overrides` option instead. + + Overrides for the Nuxt configuration. + +## `buildNuxt` + +Build Nuxt programmatically. It will invoke the builder (currently [@nuxt/vite-builder](https://github.com/nuxt/nuxt/tree/main/packages/vite) or [@nuxt/webpack-builder](https://github.com/nuxt/nuxt/tree/main/packages/webpack)) to bundle the application. + +### Type + +```ts +async function buildNuxt (nuxt: Nuxt): Promise +``` + +### Parameters + +#### `nuxt` + +**Type**: `Nuxt` + +**Required**: `true` + +Nuxt instance to build. It can be retrieved from the context via `useNuxt()` call. + +## `loadNuxtConfig` + +Load Nuxt configuration. It will return the promise with the configuration object. + +### Type + +```ts +async function loadNuxtConfig (options: LoadNuxtConfigOptions): Promise +``` + +### Parameters + +#### `options` + +**Type**: `LoadNuxtConfigOptions` + +**Required**: `true` + +Options to pass in [`c12`](https://github.com/unjs/c12#options) `loadConfig` call. + +## `writeTypes` + +Generates tsconfig.json and writes it to the project buildDir. + +### Type + +```ts +function writeTypes (nuxt?: Nuxt): void + +interface Nuxt { + options: NuxtOptions + hooks: Hookable + hook: Nuxt['hooks']['hook'] + callHook: Nuxt['hooks']['callHook'] + addHooks: Nuxt['hooks']['addHooks'] + ready: () => Promise + close: () => Promise + server?: any + vfs: Record + apps: Record +} +``` + +### Parameters + +#### `nuxt` + +**Type**: `Nuxt` + +**Required**: `true` + +Nuxt instance to build. It can be retrieved from the context via `useNuxt()` call. diff --git a/docs/3.api/4.kit/3.compatibility.md b/docs/3.api/4.kit/3.compatibility.md new file mode 100644 index 0000000000..ccd6ecc1c2 --- /dev/null +++ b/docs/3.api/4.kit/3.compatibility.md @@ -0,0 +1,223 @@ +--- +title: "Compatibility" +description: Nuxt Kit provides a set of utilities to help you check the compatibility of your modules with different Nuxt versions. +--- + +# Compatibility + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/compatibility.ts) + +Nuxt Kit utilities can be used in Nuxt 3, Nuxt 2 with Bridge and even Nuxt 2 without Bridge. To make sure your module is compatible with all versions, you can use the `checkNuxtCompatibility`, `assertNuxtCompatibility` and `hasNuxtCompatibility` functions. They will check if the current Nuxt version meets the constraints you provide. Also you can use `isNuxt2`, `isNuxt3` and `getNuxtVersion` functions for more granular checks. + +## `checkNuxtCompatibility` + +Checks if constraints are met for the current Nuxt version. If not, returns an array of messages. Nuxt 2 version also checks for `bridge` support. + +### Type + +```ts +async function checkNuxtCompatibility( + constraints: NuxtCompatibility, + nuxt?: Nuxt +): Promise; + +interface NuxtCompatibility { + nuxt?: string; + bridge?: boolean; +} + +interface NuxtCompatibilityIssue { + name: string; + message: string; +} + +interface NuxtCompatibilityIssues extends Array { + toString(): string; +} +``` + +### Parameters + +#### `constraints` + +**Type**: `NuxtCompatibility` + +**Default**: `{}` + +Constraints to check for. It accepts the following properties: + +- `nuxt` (optional) + + **Type**: `string` + + Nuxt version in semver format. Versions may be defined in Node.js way, for exmaple: `>=2.15.0 <3.0.0`. + +- `bridge` (optional) + + **Type**: `boolean` + + If set to `true`, it will check if the current Nuxt version supports `bridge`. + +#### `nuxt` + +**Type**: `Nuxt` + +**Default**: `useNuxt()` + +Nuxt instance. If not provided, it will be retrieved from the context via `useNuxt()` call. + +## `assertNuxtCompatibility` + +Asserts that constraints are met for the current Nuxt version. If not, throws an error with the list of issues as string. + +### Type + +```ts +async function assertNuxtCompatibility( + constraints: NuxtCompatibility, + nuxt?: Nuxt +): Promise; + +interface NuxtCompatibility { + nuxt?: string; + bridge?: boolean; +} +``` + +### Parameters + +#### `constraints` + +**Type**: `NuxtCompatibility` + +**Default**: `{}` + +Constraints to check for. It accepts the following properties: + +- `nuxt` (optional) + + **Type**: `string` + + Nuxt version in semver format. Versions may be defined in Node.js way, for exmaple: `>=2.15.0 <3.0.0`. + +- `bridge` (optional) + + **Type**: `boolean` + + If set to `true`, it will check if the current Nuxt version supports `bridge`. + +#### `nuxt` + +**Type**: `Nuxt` + +**Default**: `useNuxt()` + +Nuxt instance. If not provided, it will be retrieved from the context via `useNuxt()` call. + +## `hasNuxtCompatibility` + +Checks if constraints are met for the current Nuxt version. Return `true` if all constraints are met, otherwise returns `false`. Nuxt 2 version also checks for `bridge` support. + +### Type + +```ts +async function hasNuxtCompatibility( + constraints: NuxtCompatibility, + nuxt?: Nuxt +): Promise; + +interface NuxtCompatibility { + nuxt?: string; + bridge?: boolean; +} +``` + +### Parameters + +#### `constraints` + +**Type**: `NuxtCompatibility` + +**Default**: `{}` + +Constraints to check for. It accepts the following properties: + +- `nuxt` (optional) + + **Type**: `string` + + Nuxt version in semver format. Versions may be defined in Node.js way, for exmaple: `>=2.15.0 <3.0.0`. + +- `bridge` (optional) + + **Type**: `boolean` + + If set to `true`, it will check if the current Nuxt version supports `bridge`. + +#### `nuxt` + +**Type**: `Nuxt` + +**Default**: `useNuxt()` + +Nuxt instance. If not provided, it will be retrieved from the context via `useNuxt()` call. + +## `isNuxt2` + +Checks if the current Nuxt version is 2.x. + +### Type + +```ts +function isNuxt2(nuxt?: Nuxt): boolean; +``` + +### Parameters + +#### `nuxt` + +**Type**: `Nuxt` + +**Default**: `useNuxt()` + +Nuxt instance. If not provided, it will be retrieved from the context via `useNuxt()` call. + +## `isNuxt3` + +Checks if the current Nuxt version is 3.x. + +### Type + +```ts +function isNuxt3(nuxt?: Nuxt): boolean; +``` + +### Parameters + +#### `nuxt` + +**Type**: `Nuxt` + +**Default**: `useNuxt()` + +Nuxt instance. If not provided, it will be retrieved from the context via `useNuxt()` call. + +## `getNuxtVersion` + +Returns the current Nuxt version. + +### Type + +```ts +function getNuxtVersion(nuxt?: Nuxt): string; +``` + +### Parameters + +#### `nuxt` + +**Type**: `Nuxt` + +**Default**: `useNuxt()` + +Nuxt instance. If not provided, it will be retrieved from the context via `useNuxt()` call. diff --git a/docs/3.api/4.kit/4.autoimports.md b/docs/3.api/4.kit/4.autoimports.md new file mode 100644 index 0000000000..49a90b4e77 --- /dev/null +++ b/docs/3.api/4.kit/4.autoimports.md @@ -0,0 +1,322 @@ +--- +title: "Auto-imports" +description: Nuxt Kit provides a set of utilities to help you work with auto-imports. These functions allow you to register your own utils, composables and Vue APIs. +--- + +# Auto-imports + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/imports.ts) + +Nuxt auto-imports helper functions, composables and Vue APIs to use across your application without explicitly importing them. Based on the directory structure, every Nuxt application can also use auto-imports for its own composables and plugins. With Nuxt Kit you can also add your own auto-imports. `addImports` and `addImportsDir` allow you to add imports to the Nuxt application. `addImportsSources` allows you to add listed imports from 3rd party packages to the Nuxt application. + +::alert{type=info} +These functions are designed for registering your own utils, composables and Vue APIs. For pages, components and plugins, please refer to the specific sections: [Pages](/docs/api/kit/pages), [Components](/docs/api/kit/components), [Plugins](/docs/api/kit/plugins). +:: + +Nuxt auto-imports helper functions, composables and Vue APIs to use across your application without explicitly importing them. Based on the directory structure, every Nuxt application can also use auto-imports for its own composables and plugins. Composables or plugins can use these functions. + +## `addImports` + +Add imports to the Nuxt application. It makes your imports available in the Nuxt application without the need to import them manually. + +### Type + +```ts +function addImports (imports: Import | Import[]): void + +interface Import { + from: string + priority?: number + disabled?: boolean + meta?: { + description?: string + docsUrl?: string + [key: string]: any + } + type?: boolean + typeFrom?: string + name: string + as?: string +} +``` + +### Parameters + +#### `imports` + +**Type**: `Import | Import[]` + +**Required**: `true` + +An object or an array of objects with the following properties: + +- `from` (required) + + **Type**: `string` + + Module specifier to import from. + +- `priority` (optional) + + **Type**: `number` + + **Default**: `1` + + Priority of the import, if multiple imports have the same name, the one with the highest priority will be used. + +- `disabled` (optional) + + **Type**: `boolean` + + If this import is disabled. + +- `meta` (optional) + + **Type**: `object` + + Metadata of the import. + +- `meta.description` (optional) + + **Type**: `string` + + Short description of the import. + +- `meta.docsUrl` (optional) + + **Type**: `string` + + URL to the documentation. + +- `meta[key]` (optional) + + **Type**: `any` + + Additional metadata. + +- `type` (optional) + + **Type**: `boolean` + + If this import is a pure type import. + +- `typeFrom` (optional) + + **Type**: `string` + + Using this as the from when generating type declarations. + +- `name` (required) + + **Type**: `string` + + Import name to be detected. + +- `as` (optional) + + **Type**: `string` + + Import as this name. + +### Examples + +```ts +// https://github.com/pi0/storyblok-nuxt +import { defineNuxtModule, addImports, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options, nuxt) { + const names = [ + "useStoryblok", + "useStoryblokApi", + "useStoryblokBridge", + "renderRichText", + "RichTextSchema" + ]; + + names.forEach((name) => + addImports({ name, as: name, from: "@storyblok/vue" }) + ); + } +}) +``` + +## `addImportsDir` + +Add imports from a directory to the Nuxt application. It will automatically import all files from the directory and make them available in the Nuxt application without the need to import them manually. + +### Type + +```ts +function addImportsDir (dirs: string | string[], options?: { prepend?: boolean }): void +``` + +### Parameters + +#### `dirs` + +**Type**: `string | string[]` + +**Required**: `true` + +A string or an array of strings with the path to the directory to import from. + +#### `options` + +**Type**: `{ prepend?: boolean }` + +**Default**: `{}` + +Options to pass to the import. If `prepend` is set to `true`, the imports will be prepended to the list of imports. + +### Examples + +```ts +// https://github.com/vueuse/motion/tree/main/src/nuxt +import { defineNuxtModule, addImportsDir, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: '@vueuse/motion', + configKey: 'motion', + }, + setup(options, nuxt) { + const resolver = createResolver(import.meta.url) + addImportsDir(resolver.resolve('./runtime/composables')) + }, +}) +``` + +## `addImportsSources` + +Add listed imports to the Nuxt application. + +### Type + +```ts +function addImportsSources (importSources: ImportSource | ImportSource[]): void + +interface Import { + from: string + priority?: number + disabled?: boolean + meta?: { + description?: string + docsUrl?: string + [key: string]: any + } + type?: boolean + typeFrom?: string + name: string + as?: string +} + +interface ImportSource extends Import { + imports: (PresetImport | ImportSource)[] +} + +type PresetImport = Omit | string | [name: string, as?: string, from?: string] +``` + +### Parameters + +#### `importSources` + +**Type**: `ImportSource | ImportSource[]` + +**Required**: `true` + +An object or an array of objects with the following properties: + +- `imports` (required) + + **Type**: `PresetImport | ImportSource[]` + + **Required**: `true` + + An object or an array of objects, which can be import names, import objects or import sources. + +- `from` (required) + + **Type**: `string` + + Module specifier to import from. + +- `priority` (optional) + + **Type**: `number` + + **Default**: `1` + + Priority of the import, if multiple imports have the same name, the one with the highest priority will be used. + +- `disabled` (optional) + + **Type**: `boolean` + + If this import is disabled. + +- `meta` (optional) + + **Type**: `object` + + Metadata of the import. + +- `meta.description` (optional) + + **Type**: `string` + + Short description of the import. + +- `meta.docsUrl` (optional) + + **Type**: `string` + + URL to the documentation. + +- `meta[key]` (optional) + + **Type**: `any` + + Additional metadata. + +- `type` (optional) + + **Type**: `boolean` + + If this import is a pure type import. + +- `typeFrom` (optional) + + **Type**: `string` + + Using this as the from when generating type declarations. + +- `name` (required) + + **Type**: `string` + + Import name to be detected. + +- `as` (optional) + + **Type**: `string` + + Import as this name. + +### Examples + +```ts +// https://github.com/elk-zone/elk +import { defineNuxtModule, addImportsSources } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + // add imports from h3 to make them autoimported + addImportsSources({ + from: 'h3', + imports: ['defineEventHandler', 'getQuery', 'getRouterParams', 'readBody', 'sendRedirect'] as Array, + }) + } +}) +``` diff --git a/docs/3.api/4.kit/5.components.md b/docs/3.api/4.kit/5.components.md new file mode 100644 index 0000000000..c755cea8a9 --- /dev/null +++ b/docs/3.api/4.kit/5.components.md @@ -0,0 +1,284 @@ +--- +title: "Components" +description: Nuxt Kit provides a set of utilities to help you work with components. You can register components globally or locally, and also add directories to be scanned for components. +--- + +# Components + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/components.ts) + +Components are the building blocks of your Nuxt application. They are reusable Vue instances that can be used to create a user interface. In Nuxt, components from the components directory are automatically imported by default. However, if you need to import components from an alternative directory or wish to selectively import them as needed, `@nuxt/kit` provides the `addComponentsDir` and `addComponent` methods. These utils allow you to customize the component configuration to better suit your needs. + +## `addComponentsDir` + +Register a directory to be scanned for components and imported only when used. Keep in mind, that this does not register components globally, until you specify `global: true` option. + +### Type + +```ts +async function addComponentsDir (dir: ComponentsDir): void + +interface ComponentsDir { + path: string + pattern?: string | string[] + ignore?: string[] + prefix?: string + pathPrefix?: boolean + enabled?: boolean + prefetch?: boolean + preload?: boolean + isAsync?: boolean + extendComponent?: (component: Component) => Promise | (Component | void) + global?: boolean + island?: boolean + watch?: boolean + extensions?: string[] + transpile?: 'auto' | boolean +} + +interface Component { + pascalName: string + kebabName: string + export: string + filePath: string + shortPath: string + chunkName: string + prefetch: boolean + preload: boolean + global?: boolean + island?: boolean + mode?: 'client' | 'server' | 'all' + priority?: number +} +``` + +### Parameters + +#### `dir` + +**Type**: `ComponentsDir` + +**Required**: `true` + +An object with the following properties: + +- `path` (required) + + **Type**: `string` + + Path (absolute or relative) to the directory containing your components. + You can use Nuxt aliases (~ or @) to refer to directories inside project or directly use an npm package path similar to require. + +- `pattern` (optional) + + **Type**: `string | string[]` + + Accept Pattern that will be run against specified path. + +- `ignore` (optional) + + **Type**: `string[]` + + Ignore patterns that will be run against specified path. + +- `prefix` (optional) + + **Type**: `string` + + Prefix all matched components with this string. + +- `pathPrefix` (optional) + + **Type**: `boolean` + + Prefix component name by its path. + +- `enabled` (optional) + + **Type**: `boolean` + + Ignore scanning this directory if set to `true`. + +- `prefetch` (optional) + + **Type**: `boolean` + + These properties (prefetch/preload) are used in production to configure how components with Lazy prefix are handled by webpack via its magic comments. + Learn more on [webpack documentation](https://webpack.js.org/api/module-methods/#magic-comments) + +- `preload` (optional) + + **Type**: `boolean` + + These properties (prefetch/preload) are used in production to configure how components with Lazy prefix are handled by webpack via its magic comments. + Learn more on [webpack documentation](https://webpack.js.org/api/module-methods/#magic-comments) + +- `isAsync` (optional) + + **Type**: `boolean` + + This flag indicates, component should be loaded async (with a separate chunk) regardless of using Lazy prefix or not. + +- `extendComponent` (optional) + + **Type**: `(component: Component) => Promise | (Component | void)` + + A function that will be called for each component found in the directory. It accepts a component object and should return a component object or a promise that resolves to a component object. + +- `global` (optional) + + **Type**: `boolean` + + **Default**: `false` + + If enabled, registers components to be globally available. + +- `island` (optional) + + **Type**: `boolean` + + If enabled, registers components as islands. + +- `watch` (optional) + + **Type**: `boolean` + + Watch specified path for changes, including file additions and file deletions. + +- `extensions` (optional) + + **Type**: `string[]` + + Extensions supported by Nuxt builder. + +- `transpile` (optional) + + **Type**: `'auto' | boolean` + + Transpile specified path using build.transpile. If set to `'auto'`, it will set `transpile: true` if `node_modules/` is in path. + +## `addComponent` + +Register a component to be automatically imported. + +### Type + +```ts +async function addComponent (options: AddComponentOptions): void + +interface AddComponentOptions { + name: string, + filePath: string, + pascalName?: string, + kebabName?: string, + export?: string, + shortPath?: string, + chunkName?: string, + prefetch?: boolean, + preload?: boolean, + global?: boolean, + island?: boolean, + mode?: 'client' | 'server' | 'all', + priority?: number, +} +``` + +### Parameters + +#### `options` + +**Type**: `AddComponentOptions` + +**Required**: `true` + +An object with the following properties: + +- `name` (required) + + **Type**: `string` + + Component name. + +- `filePath` (required) + + **Type**: `string` + + Path to the component. + +- `pascalName` (optional) + + **Type**: `pascalCase(options.name)` + + Pascal case component name. If not provided, it will be generated from the component name. + +- `kebabName` (optional) + + **Type**: `kebabCase(options.name)` + + Kebab case component name. If not provided, it will be generated from the component name. + +- `export` (optional) + + **Type**: `string` + + **Default**: `'default'` + + Specify named or default export. If not provided, it will be set to `'default'`. + +- `shortPath` (optional) + + **Type**: `string` + + Short path to the component. If not provided, it will be generated from the component path. + +- `chunkName` (optional) + + **Type**: `string` + + **Default**: `'components/' + kebabCase(options.name)` + + Chunk name for the component. If not provided, it will be generated from the component name. + +- `prefetch` (optional) + + **Type**: `boolean` + + These properties (prefetch/preload) are used in production to configure how components with Lazy prefix are handled by webpack via its magic comments. + Learn more on [webpack documentation](https://webpack.js.org/api/module-methods/#magic-comments) + +- `preload` (optional) + + **Type**: `boolean` + + These properties (prefetch/preload) are used in production to configure how components with Lazy prefix are handled by webpack via its magic comments. + Learn more on [webpack documentation](https://webpack.js.org/api/module-methods/#magic-comments) + +- `global` (optional) + + **Type**: `boolean` + + **Default**: `false` + + If enabled, registers component to be globally available. + +- `island` (optional) + + **Type**: `boolean` + + If enabled, registers component as island. You can read more about islands in [](/docs/api/components/nuxt-island#nuxtisland) component description. + +- `mode` (optional) + + **Type**: `'client' | 'server' | 'all'` + + **Default**: `'all'` + + This options indicates if component should render on client, server or both. By default, it will render on both client and server. + +- `priority` (optional) + + **Type**: `number` + + **Default**: `1` + + Priority of the component, if multiple components have the same name, the one with the highest priority will be used. diff --git a/docs/3.api/4.kit/6.context.md b/docs/3.api/4.kit/6.context.md new file mode 100644 index 0000000000..b5a5caf083 --- /dev/null +++ b/docs/3.api/4.kit/6.context.md @@ -0,0 +1,129 @@ +--- +title: "Context" +description: Nuxt Kit provides a set of utilities to help you work with context. +--- + +# Context + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/context.ts) + +Nuxt modules allow you to enhance Nuxt's capabilities. They offer a structured way to keep your code organized and modular. If you're looking to break down your module into smaller components, Nuxt offers the `useNuxt` and `tryUseNuxt` functions. These functions enable you to conveniently access the Nuxt instance from the context without having to pass it as argument. + +::alert{type=info} +When you're working with the `setup` function in Nuxt modules, Nuxt is already provided as the second argument. This means you can directly utilize it without needing to call `useNuxt()`. You can look at [Nuxt Site Config](https://github.com/harlan-zw/nuxt-site-config) as an example of usage. +:: + +## `useNuxt` + +Get the Nuxt instance from the context. It will throw an error if Nuxt is not available. + +### Type + +```ts +function useNuxt(): Nuxt + +interface Nuxt { + options: NuxtOptions + hooks: Hookable + hook: Nuxt['hooks']['hook'] + callHook: Nuxt['hooks']['callHook'] + addHooks: Nuxt['hooks']['addHooks'] + ready: () => Promise + close: () => Promise + server?: any + vfs: Record + apps: Record +} +``` + +### Examples + +::code-group + +```ts [setupTranspilation.ts] +// https://github.com/Lexpeartha/nuxt-xstate/blob/main/src/parts/transpile.ts +import { useNuxt } from '@nuxt/kit' + +export const setupTranspilation = () => { + const nuxt = useNuxt() + + nuxt.options.build.transpile = nuxt.options.build.transpile || [] + + if (nuxt.options.builder === '@nuxt/webpack-builder') { + nuxt.options.build.transpile.push( + 'xstate', + ) + } +} +``` + +```ts [module.ts] +import { useNuxt } from '@nuxt/kit' +import { setupTranspilation } from './setupTranspilation' + +export default defineNuxtModule({ + setup() { + setupTranspilation() + } +}) +``` + +:: + +## `tryUseNuxt` + +Get the Nuxt instance from the context. It will return `null` if Nuxt is not available. + +### Type + +```ts +function tryUseNuxt(): Nuxt | null + +interface Nuxt { + options: NuxtOptions + hooks: Hookable + hook: Nuxt['hooks']['hook'] + callHook: Nuxt['hooks']['callHook'] + addHooks: Nuxt['hooks']['addHooks'] + ready: () => Promise + close: () => Promise + server?: any + vfs: Record + apps: Record +} +``` + +### Examples + +::code-group + +```ts [requireSiteConfig.ts] +// https://github.com/harlan-zw/nuxt-site-config/blob/main/test/assertions.test.ts +import { tryUseNuxt } from '@nuxt/kit' + +interface SiteConfig { + title: string +} + +export const requireSiteConfig = (): SiteConfig => { + const nuxt = tryUseNuxt() + if (!nuxt) { + return { title: null } + } + return nuxt.options.siteConfig +} +``` + +```ts [module.ts] +import { useNuxt } from '@nuxt/kit' +import { requireSiteConfig } from './requireSiteConfig' + +export default defineNuxtModule({ + setup(_, nuxt) { + const config = requireSiteConfig() + nuxt.options.app.head.title = config.title + } +}) +``` + +:: diff --git a/docs/3.api/4.kit/7.pages.md b/docs/3.api/4.kit/7.pages.md new file mode 100644 index 0000000000..078afd8e17 --- /dev/null +++ b/docs/3.api/4.kit/7.pages.md @@ -0,0 +1,267 @@ +--- +title: Pages +description: Nuxt Kit provides a set of utilities to help you create and use pages. You can use these utilities to manipulate the pages configuration or to define route rules. +--- + +# Pages + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/pages.ts) + +## `extendPages` + +In Nuxt 3, routes are automatically generated based on the structure of the files in the `pages` directory. However, there may be scenarios where you'd want to customize these routes. For instance, you might need to add a route for a dynamic page not generated by Nuxt, remove an existing route, or modify the configuration of a route. For such customizations, Nuxt 3 offers the `extendPages` feature, which allows you to extend and alter the pages configuration. + +### Type + +```ts +function extendPages (callback: (pages: NuxtPage[]) => void): void + +type NuxtPage = { + name?: string + path: string + file?: string + meta?: Record + alias?: string[] | string + redirect?: RouteLocationRaw + children?: NuxtPage[] +} +``` + +### Parameters + +#### `callback` + +**Type**: `(pages: NuxtPage[]) => void` + +**Required**: `true` + +A function that will be called with the pages configuration. You can alter this array by adding, deleting, or modifying its elements. Note: You should modify the provided pages array directly, as changes made to a copied array will not be reflected in the configuration. + +### Examples + +```ts +// https://github.com/nuxt-modules/prismic/blob/master/src/module.ts +import { createResolver, defineNuxtModule, extendPages } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options) { + const resolver = createResolver(import.meta.url) + + extendPages((pages) => { + pages.unshift({ + name: 'prismic-preview', + path: '/preview', + file: resolver.resolve('runtime/preview.vue') + }) + }) + } +}) +``` + +## `extendRouteRules` + +Nuxt is powered by the [Nitro](https://nitro.unjs.io) server engine. With Nitro, you can incorporate high-level logic directly into your configuration, which is useful for actions like redirects, proxying, caching, and appending headers to routes. This configuration works by associating route patterns with specific route settings. + +::alert{type=info icon=👉} +You can read more about Nitro route rules in the [Nitro documentation](https://nitro.unjs.io/guide/routing#route-rules). +:: + +### Type + +```ts +function extendRouteRules (route: string, rule: NitroRouteConfig, options: ExtendRouteRulesOptions): void + +interface NitroRouteConfig { + cache?: CacheOptions | false; + headers?: Record; + redirect?: string | { to: string; statusCode?: HTTPStatusCode }; + prerender?: boolean; + proxy?: string | ({ to: string } & ProxyOptions); + isr?: number | boolean; + cors?: boolean; + swr?: boolean | number; + static?: boolean | number; +} + +interface ExtendRouteRulesOptions { + override?: boolean +} + +interface CacheOptions { + swr?: boolean + name?: string + group?: string + integrity?: any + maxAge?: number + staleMaxAge?: number + base?: string + headersOnly?: boolean +} + +// See https://www.jsdocs.io/package/h3#ProxyOptions +interface ProxyOptions { + headers?: RequestHeaders | HeadersInit; + fetchOptions?: RequestInit & { duplex?: Duplex } & { + ignoreResponseError?: boolean; + }; + fetch?: typeof fetch; + sendStream?: boolean; + streamRequest?: boolean; + cookieDomainRewrite?: string | Record; + cookiePathRewrite?: string | Record; + onResponse?: (event: H3Event, response: Response) => void; +} +``` + +### Parameters + +#### `route` + +**Type**: `string` + +**Required**: `true` + +A route pattern to match against. + +#### `rule` + +**Type**: `NitroRouteConfig` + +**Required**: `true` + +A route configuration to apply to the matched route. + +#### `options` + +**Type**: `ExtendRouteRulesOptions` + +**Default**: `{}` + +Options to pass to the route configuration. If `override` is set to `true`, it will override the existing route configuration. + +### Examples + +```ts +// https://github.com/directus/website/blob/main/modules/redirects.ts +import { createResolver, defineNuxtModule, extendRouteRules, extendPages } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options) { + const resolver = createResolver(import.meta.url) + + extendPages((pages) => { + pages.unshift({ + name: 'preview-new', + path: '/preview-new', + file: resolver.resolve('runtime/preview.vue') + }) + }) + + extendRouteRules('/preview', { + redirect: { + to: '/preview-new', + statusCode: 302 + } + }) + + extendRouteRules('/preview-new', { + cache: { + maxAge: 60 * 60 * 24 * 7 + } + }) + } +}) +``` + +## `addRouteMiddleware` + +Registers route middlewares to be available for all routes or for specific routes. + +Route middlewares can be also defined in plugins via [`addRouteMiddleware`](/docs/api/utils/add-route-middleware) composable. + +::alert{type=info icon=👉} +Read more about route middlewares in the [Route middleware documentation](/docs/getting-started/routing#route-middleware). +:: + +### Type + +```ts +function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], options: AddRouteMiddlewareOptions): void + +type NuxtMiddleware = { + name: string + path: string + global?: boolean +} + +interface AddRouteMiddlewareOptions { + override?: boolean +} +``` + +### Parameters + +#### `input` + +**Type**: `NuxtMiddleware | NuxtMiddleware[]` + +**Required**: `true` + +A middleware object or an array of middleware objects with the following properties: + +- `name` (required) + + **Type**: `string` + + Middleware name. + +- `path` (required) + + **Type**: `string` + + Path to the middleware. + +- `global` (optional) + + **Type**: `boolean` + + If enabled, registers middleware to be available for all routes. + +#### `options` + +**Type**: `AddRouteMiddlewareOptions` + +**Default**: `{}` + +Options to pass to the middleware. If `override` is set to `true`, it will override the existing middleware with the same name. + +### Examples + +::code-group + +```ts [runtime/auth.ts] +export default defineNuxtRouteMiddleware((to, from) => { + // isAuthenticated() is an example method verifying if a user is authenticated + if (to.path !== '/login' && isAuthenticated() === false) { + return navigateTo('/login') + } +}) +``` + +```ts +import { createResolver, defineNuxtModule, addRouteMiddleware } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const resolver = createResolver(import.meta.url) + + addRouteMiddleware({ + name: 'auth', + path: resolver.resolve('runtime/auth.ts'), + global: true + }) + } +}) +``` + +:: diff --git a/docs/3.api/4.kit/8.layout.md b/docs/3.api/4.kit/8.layout.md new file mode 100644 index 0000000000..9affb24215 --- /dev/null +++ b/docs/3.api/4.kit/8.layout.md @@ -0,0 +1,79 @@ +--- +title: "Layout" +description: "Nuxt Kit provides a set of utilities to help you work with layouts." +--- + +# Layout + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/layout.ts) + +Layouts is used to be a wrapper around your pages. It can be used to wrap your pages with common components, for example, a header and a footer. Layouts can be registered using `addLayout` utility. + +## `addLayout` + +Register template as layout and add it to the layouts. + +::alert{type=info icon=👉} +In Nuxt 2 `error` layout can also be registered using this utility. In Nuxt 3 `error` layout [replaced](/docs/getting-started/error-handling#rendering-an-error-page) with `error.vue` page in project root. +:: + +### Type + +```ts +function addLayout (layout: NuxtTemplate | string, name: string): void + +interface NuxtTemplate { + src?: string + filename?: string + dst?: string + options?: Record + getContents?: (data: Record) => string | Promise + write?: boolean +} +``` + +### Parameters + +#### `layout` + +**Type**: `NuxtTemplate | string` + +**Required**: `true` + +A template object or a string with the path to the template. If a string is provided, it will be converted to a template object with `src` set to the string value. If a template object is provided, it must have the following properties: + +- `src` (optional) + + **Type**: `string` + + Path to the template. If `src` is not provided, `getContents` must be provided instead. + +- `filename` (optional) + + **Type**: `string` + + Filename of the template. If `filename` is not provided, it will be generated from the `src` path. In this case, the `src` option is required. + +- `dst` (optional) + + **Type**: `string` + + Path to the destination file. If `dst` is not provided, it will be generated from the `filename` path and nuxt `buildDir` option. + +- `options` (optional) + + **Type**: `Options` + + Options to pass to the template. + +- `getContents` (optional) + + **Type**: `(data: Options) => string | Promise` + + A function that will be called with the `options` object. It should return a string or a promise that resolves to a string. If `src` is provided, this function will be ignored. + +- `write` (optional) + + **Type**: `boolean` + + If set to `true`, the template will be written to the destination file. Otherwise, the template will be used only in virtual filesystem. diff --git a/docs/3.api/4.kit/9.plugins.md b/docs/3.api/4.kit/9.plugins.md new file mode 100644 index 0000000000..6785c91868 --- /dev/null +++ b/docs/3.api/4.kit/9.plugins.md @@ -0,0 +1,253 @@ +--- +title: Plugins +description: Nuxt Kit provides a set of utilities to help you create and use plugins. You can add plugins or plugin templates to your module using these functions. +--- + +# Plugins + +[source code](https://github.com/nuxt/nuxt/blob/main/packages/kit/src/plugin.ts) + +Plugins are self-contained code that usually add app-level functionality to Vue. In Nuxt, plugins are automatically imported from the `plugins` directory. However, if you need to ship a plugin with your module, Nuxt Kit provides the `addPlugin` and `addPluginTemplate` methods. These utils allow you to customize the plugin configuration to better suit your needs. + +## `addPlugin` + +Registers a Nuxt plugin and to the plugins array. + +### Type + +```ts +function addPlugin (plugin: NuxtPlugin | string, options: AddPluginOptions): NuxtPlugin + +interface NuxtPlugin { + src: string + mode?: 'all' | 'server' | 'client' + order?: number +} + +interface AddPluginOptions { append?: boolean } +``` + +### Parameters + +#### `plugin` + +**Type**: `NuxtPlugin | string` + +**Required**: `true` + +A plugin object or a string with the path to the plugin. If a string is provided, it will be converted to a plugin object with `src` set to the string value. If a plugin object is provided, it must have the following properties: + +- `src` (required) + + **Type**: `string` + + Path to the plugin. + +- `mode` (optional) + + **Type**: `'all' | 'server' | 'client'` + + **Default**: `'all'` + + If set to `'all'`, the plugin will be included in both client and server bundles. If set to `'server'`, the plugin will only be included in the server bundle. If set to `'client'`, the plugin will only be included in the client bundle. You can also use `.client` and `.server` modifiers when specifying `src` option to use plugin only in client or server side. + +- `order` (optional) + + **Type**: `number` + + **Default**: `0` + + Order of the plugin. This allows more granular control over plugin order and should only be used by advanced users. Lower numbers run first, and user plugins default to `0`. It's recommended to set `order` to a number between `-20` for `pre`-plugins (plugins that run before Nuxt plugins) and `20` for `post`-plugins (plugins that run after Nuxt plugins). + +::alert{type=warning} +Don't use `order` unless you know what you're doing. For most plugins, the default `order` of `0` is sufficient. To append a plugin to the end of the plugins array, use the `append` option instead. +:: + +#### `options` + +**Type**: `AddPluginOptions` + +**Default**: `{}` + +Options to pass to the plugin. If `append` is set to `true`, the plugin will be appended to the plugins array instead of prepended. + +### Examples + +::code-group + +```ts [module.ts] +import { createResolver, defineNuxtModule, addPlugin } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const resolver = createResolver(import.meta.url) + + addPlugin({ + src: resolver.resolve('runtime/plugin.js'), + mode: 'client' + }) + } +}) +``` + +```ts [runtime/plugin.js] +// https://github.com/nuxt/nuxters +export default defineNuxtPlugin((nuxtApp) => { + const colorMode = useColorMode() + + nuxtApp.hook('app:mounted', () => { + if (colorMode.preference !== 'dark') { + colorMode.preference = 'dark' + } + }) +}) +``` + +:: + +## `addPluginTemplate` + +Adds a template and registers as a nuxt plugin. This is useful for plugins that need to generate code at build time. + +### Type + +```ts +function addPluginTemplate (pluginOptions: NuxtPluginTemplate, options: AddPluginOptions): NuxtPlugin + +interface NuxtPluginTemplate> { + src?: string, + filename?: string, + dst?: string, + mode?: 'all' | 'server' | 'client', + options?: Options, + getContents?: (data: Options) => string | Promise, + write?: boolean, + order?: number +} + +interface AddPluginOptions { append?: boolean } + +interface NuxtPlugin { + src: string + mode?: 'all' | 'server' | 'client' + order?: number +} +``` + +### Parameters + +#### `pluginOptions` + +**Type**: `NuxtPluginTemplate` + +**Required**: `true` + +A plugin template object with the following properties: + +- `src` (optional) + + **Type**: `string` + + Path to the template. If `src` is not provided, `getContents` must be provided instead. + +- `filename` (optional) + + **Type**: `string` + + Filename of the template. If `filename` is not provided, it will be generated from the `src` path. In this case, the `src` option is required. + +- `dst` (optional) + + **Type**: `string` + + Path to the destination file. If `dst` is not provided, it will be generated from the `filename` path and nuxt `buildDir` option. + +- `mode` (optional) + + **Type**: `'all' | 'server' | 'client'` + + **Default**: `'all'` + + If set to `'all'`, the plugin will be included in both client and server bundles. If set to `'server'`, the plugin will only be included in the server bundle. If set to `'client'`, the plugin will only be included in the client bundle. You can also use `.client` and `.server` modifiers when specifying `src` option to use plugin only in client or server side. + +- `options` (optional) + + **Type**: `Options` + + Options to pass to the template. + +- `getContents` (optional) + + **Type**: `(data: Options) => string | Promise` + + A function that will be called with the `options` object. It should return a string or a promise that resolves to a string. If `src` is provided, this function will be ignored. + +- `write` (optional) + + **Type**: `boolean` + + If set to `true`, the template will be written to the destination file. Otherwise, the template will be used only in virtual filesystem. + +- `order` (optional) + + **Type**: `number` + + **Default**: `0` + + Order of the plugin. This allows more granular control over plugin order and should only be used by advanced users. Lower numbers run first, and user plugins default to `0`. It's recommended to set `order` to a number between `-20` for `pre`-plugins (plugins that run before Nuxt plugins) and `20` for `post`-plugins (plugins that run after Nuxt plugins). + +::alert{type=warning} +Don't use `order` unless you know what you're doing. For most plugins, the default `order` of `0` is sufficient. To append a plugin to the end of the plugins array, use the `append` option instead. +:: + +#### `options` + +**Type**: `AddPluginOptions` + +**Default**: `{}` + +Options to pass to the plugin. If `append` is set to `true`, the plugin will be appended to the plugins array instead of prepended. + +### Examples + +::code-group + +```ts [module.ts] +// https://github.com/vuejs/vuefire +import { createResolver, defineNuxtModule, addPluginTemplate } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const resolver = createResolver(import.meta.url) + + addPluginTemplate({ + src: resolve(templatesDir, 'plugin.ejs'), + options: { + ...options, + ssr: nuxt.options.ssr, + }, + }) + } +}) +``` + +```ts [runtime/plugin.ejs] +import { VueFire, useSSRInitialState } from 'vuefire' +import { defineNuxtPlugin } from '#app' + +export default defineNuxtPlugin((nuxtApp) => { + const firebaseApp = nuxtApp.$firebaseApp + + nuxtApp.vueApp.use(VueFire, { firebaseApp }) + + <% if(options.ssr) { %> + if (process.server) { + nuxtApp.payload.vuefire = useSSRInitialState(undefined, firebaseApp) + } else if (nuxtApp.payload?.vuefire) { + useSSRInitialState(nuxtApp.payload.vuefire, firebaseApp) + } + <% } %> +}) +``` + +:: diff --git a/docs/3.api/4.kit/_dir.yml b/docs/3.api/4.kit/_dir.yml new file mode 100644 index 0000000000..6c91c01d53 --- /dev/null +++ b/docs/3.api/4.kit/_dir.yml @@ -0,0 +1,3 @@ +title: Kit Utilities +navigation.icon: uil:suitcase-alt +image: '/socials/advanced.jpg' diff --git a/docs/3.api/4.advanced/1.hooks.md b/docs/3.api/5.advanced/1.hooks.md similarity index 92% rename from docs/3.api/4.advanced/1.hooks.md rename to docs/3.api/5.advanced/1.hooks.md index 412199d94f..d81f831935 100644 --- a/docs/3.api/4.advanced/1.hooks.md +++ b/docs/3.api/5.advanced/1.hooks.md @@ -71,9 +71,11 @@ Hook | Arguments | Description `schema:written` | - | Called after the schema is written. `vite:extend` | `viteBuildContext` | Allows to extend Vite default context. `vite:extendConfig` | `viteInlineConfig, env` | Allows to extend Vite default config. +`vite:configResolved` | `viteInlineConfig, env` | Allows to read the resolved Vite config. `vite:serverCreated` | `viteServer, env` | Called when the Vite server is created. `vite:compiled` | - | Called after Vite server is compiled. `webpack:config` | `webpackConfigs` | Called before configuring the webpack compiler. +`webpack:configResolved` | `webpackConfigs` | Allows to read the resolved webpack config. `webpack:compile` | `options` | Called right before compilation. `webpack:compiled` | `options` | Called after resources are loaded. `webpack:change` | `shortPath` | Called on `change` on WebpackBar. @@ -87,3 +89,4 @@ Hook | Arguments | Description -----------------------|-----------------------|--------------------------------------|------------------ `render:response` | `response, { event }` | Called before sending the response. | [response](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L24), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) `render:html` | `html, { event }` | Called before constructing the HTML. | [html](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L15), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38) +`render:island` | `islandResponse, { event, islandContext }` | Called before constructing the island HTML. | [islandResponse](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L28), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), [islandContext](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L38) diff --git a/docs/3.api/4.advanced/_dir.yml b/docs/3.api/5.advanced/_dir.yml similarity index 100% rename from docs/3.api/4.advanced/_dir.yml rename to docs/3.api/5.advanced/_dir.yml diff --git a/docs/3.api/5.commands/_dir.yml b/docs/3.api/6.commands/_dir.yml similarity index 100% rename from docs/3.api/5.commands/_dir.yml rename to docs/3.api/6.commands/_dir.yml diff --git a/docs/3.api/5.commands/add.md b/docs/3.api/6.commands/add.md similarity index 100% rename from docs/3.api/5.commands/add.md rename to docs/3.api/6.commands/add.md diff --git a/docs/3.api/5.commands/analyze.md b/docs/3.api/6.commands/analyze.md similarity index 100% rename from docs/3.api/5.commands/analyze.md rename to docs/3.api/6.commands/analyze.md diff --git a/docs/3.api/5.commands/build-module.md b/docs/3.api/6.commands/build-module.md similarity index 100% rename from docs/3.api/5.commands/build-module.md rename to docs/3.api/6.commands/build-module.md diff --git a/docs/3.api/5.commands/build.md b/docs/3.api/6.commands/build.md similarity index 100% rename from docs/3.api/5.commands/build.md rename to docs/3.api/6.commands/build.md diff --git a/docs/3.api/5.commands/cleanup.md b/docs/3.api/6.commands/cleanup.md similarity index 100% rename from docs/3.api/5.commands/cleanup.md rename to docs/3.api/6.commands/cleanup.md diff --git a/docs/3.api/5.commands/dev.md b/docs/3.api/6.commands/dev.md similarity index 86% rename from docs/3.api/5.commands/dev.md rename to docs/3.api/6.commands/dev.md index 3c63214091..e7eda852d8 100644 --- a/docs/3.api/5.commands/dev.md +++ b/docs/3.api/6.commands/dev.md @@ -26,6 +26,8 @@ Option | Default | Description The port and host can also be set via NUXT_PORT, PORT, NUXT_HOST or HOST environment variables. +Additionally to the above options, `nuxi` can pass options through to `listhen`, e.g. `--no-qr` to turn off the dev server QR code. You can find the list of `listhen` options in the [unjs/listhen](https://github.com/unjs/listhen) docs. + This command sets `process.env.NODE_ENV` to `development`. ::alert{type="info"} diff --git a/docs/3.api/5.commands/devtools.md b/docs/3.api/6.commands/devtools.md similarity index 100% rename from docs/3.api/5.commands/devtools.md rename to docs/3.api/6.commands/devtools.md diff --git a/docs/3.api/5.commands/generate.md b/docs/3.api/6.commands/generate.md similarity index 85% rename from docs/3.api/5.commands/generate.md rename to docs/3.api/6.commands/generate.md index 596dbc7262..5aae757cff 100644 --- a/docs/3.api/5.commands/generate.md +++ b/docs/3.api/6.commands/generate.md @@ -15,3 +15,7 @@ Option | Default | Description -------------------------|-----------------|------------------ `rootDir` | `.` | The root directory of the application to generate `--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory. + +::alert{type=info} +Read more about [pre-rendering and static hosting](/docs/getting-started/deployment#static-hosting). +:: diff --git a/docs/3.api/5.commands/info.md b/docs/3.api/6.commands/info.md similarity index 100% rename from docs/3.api/5.commands/info.md rename to docs/3.api/6.commands/info.md diff --git a/docs/3.api/5.commands/init.md b/docs/3.api/6.commands/init.md similarity index 100% rename from docs/3.api/5.commands/init.md rename to docs/3.api/6.commands/init.md diff --git a/docs/3.api/5.commands/prepare.md b/docs/3.api/6.commands/prepare.md similarity index 100% rename from docs/3.api/5.commands/prepare.md rename to docs/3.api/6.commands/prepare.md diff --git a/docs/3.api/5.commands/preview.md b/docs/3.api/6.commands/preview.md similarity index 81% rename from docs/3.api/5.commands/preview.md rename to docs/3.api/6.commands/preview.md index bd674166c6..363cb66d83 100644 --- a/docs/3.api/5.commands/preview.md +++ b/docs/3.api/6.commands/preview.md @@ -9,7 +9,7 @@ description: The preview command starts a server to preview your application aft npx nuxi preview [rootDir] [--dotenv] ``` -The `preview` command starts a server to preview your Nuxt application after running the `build` command. +The `preview` command starts a server to preview your Nuxt application after running the `build` command. The `start` command is an alias for `preview`. When running your application in production refer to the [Deployment section](/docs/getting-started/deployment). Option | Default | Description -------------------------|-----------------|------------------ diff --git a/docs/3.api/5.commands/typecheck.md b/docs/3.api/6.commands/typecheck.md similarity index 83% rename from docs/3.api/5.commands/typecheck.md rename to docs/3.api/6.commands/typecheck.md index 7f3c6cd7a7..234ab51021 100644 --- a/docs/3.api/5.commands/typecheck.md +++ b/docs/3.api/6.commands/typecheck.md @@ -9,7 +9,7 @@ description: The typecheck command runs vue-tsc to check types throughout your a npx nuxi typecheck [--log-level] [rootDir] ``` -The `typecheck` command runs [`vue-tsc`](https://github.com/johnsoncodehk/volar/tree/master/vue-language-tools/vue-tsc) to check types throughout your app. +The `typecheck` command runs [`vue-tsc`](https://github.com/vuejs/language-tools/tree/master/packages/vue-tsc) to check types throughout your app. Option | Default | Description -------------------------|-----------------|------------------ diff --git a/docs/3.api/5.commands/upgrade.md b/docs/3.api/6.commands/upgrade.md similarity index 100% rename from docs/3.api/5.commands/upgrade.md rename to docs/3.api/6.commands/upgrade.md diff --git a/docs/3.api/6.configuration/_dir.yml b/docs/3.api/7.configuration/_dir.yml similarity index 100% rename from docs/3.api/6.configuration/_dir.yml rename to docs/3.api/7.configuration/_dir.yml diff --git a/docs/3.api/6.configuration/nuxt-config.md b/docs/3.api/7.configuration/nuxt-config.md similarity index 100% rename from docs/3.api/6.configuration/nuxt-config.md rename to docs/3.api/7.configuration/nuxt-config.md diff --git a/docs/4.examples/0.essentials/hello-world.md b/docs/4.examples/0.essentials/hello-world.md deleted file mode 100644 index 15eda0a70b..0000000000 --- a/docs/4.examples/0.essentials/hello-world.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Hello World - -A minimal Nuxt 3 application only requires the `app.vue` and `nuxt.config.js` files. - -::ReadMore{link="/docs/getting-started/introduction"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/essentials/hello-world" file="app.vue"} -:: diff --git a/docs/4.examples/1.app/app-config.md b/docs/4.examples/1.app/app-config.md deleted file mode 100644 index 9f95a0da0b..0000000000 --- a/docs/4.examples/1.app/app-config.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -toc: false ---- - -# `app.config` - -> Nuxt 3 provides an app.config config file to expose reactive configuration within your application with the ability to update it at runtime within lifecycle or using a nuxt plugin and editing it with HMR (hot-module-replacement). - -This example shows how to use `app.config` feature. - -::ReadMore{link="/docs/guide/directory-structure/app-config"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/app-config" file="app.vue"} diff --git a/docs/4.examples/1.app/error-handling.md b/docs/4.examples/1.app/error-handling.md deleted file mode 100644 index 08bdc73e35..0000000000 --- a/docs/4.examples/1.app/error-handling.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Error Handling - -This example shows how to handle errors in different contexts: pages, plugins, components and middleware. - -::ReadMore{link="/docs/getting-started/error-handling"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/app/error-handling" file="app.vue"} -:: diff --git a/docs/4.examples/1.app/plugins.md b/docs/4.examples/1.app/plugins.md deleted file mode 100644 index 956f59b963..0000000000 --- a/docs/4.examples/1.app/plugins.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Plugins - -This example shows how to use the plugins/ directory to auto-register plugins. - -:ReadMore{link="/docs/guide/directory-structure/plugins"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/app/plugins" file="app.vue"} -:: diff --git a/docs/4.examples/1.app/teleport.md b/docs/4.examples/1.app/teleport.md deleted file mode 100644 index 9fd4647b84..0000000000 --- a/docs/4.examples/1.app/teleport.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -toc: false ---- - -# Teleport - -This example shows how to use the with client-side and server-side rendering. - -Vue 3 provides the [`` component](https://vuejs.org/guide/built-ins/teleport.html) which allows content to be rendered elsewhere in the DOM, outside of the Vue application. - -This example shows how to use the `` with client-side and server-side rendering. - -::ReadMore{link="/docs/api/components/teleports"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/app/teleport" file="app.vue"} -:: diff --git a/docs/4.examples/2.auto-imports/components.md b/docs/4.examples/2.auto-imports/components.md deleted file mode 100644 index f9fb58e9a9..0000000000 --- a/docs/4.examples/2.auto-imports/components.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Components - -Components in the `components/` directory are auto-imported and can be used directly in your templates. You can configure other directories to support components auto-imports. - -::ReadMore{link="/docs/guide/directory-structure/components"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/auto-imports/components" file="app.vue"} -:: diff --git a/docs/4.examples/2.auto-imports/composables.md b/docs/4.examples/2.auto-imports/composables.md deleted file mode 100644 index 4d7ac88dce..0000000000 --- a/docs/4.examples/2.auto-imports/composables.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -toc: false ---- - -# Composables - -This example shows how to use the composables/ directory to auto-import composables. - -If the composable file provides a default export, the name of the composable will be mapped to the name of the file. Named exports can be used as-is. - -::ReadMore{link="/docs/guide/directory-structure/composables"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/auto-imports/composables" file="app.vue"} -:: diff --git a/docs/4.examples/3.composables/use-async-data.md b/docs/4.examples/3.composables/use-async-data.md deleted file mode 100644 index 96fa445ec2..0000000000 --- a/docs/4.examples/3.composables/use-async-data.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -toc: false ---- - -# useAsyncData - -This example shows how to use useAsyncData to fetch data from an API endpoint. - -::alert{type=info icon=💡} -Nuxt will automatically read files in the `~/server/api` directory to create API endpoints. -:: - -::ReadMore{link="/docs/api/composables/use-async-data"} -:: - -::ReadMore{link="/docs/getting-started/data-fetching"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/composables/use-async-data" file="app.vue"} -:: diff --git a/docs/4.examples/3.composables/use-cookie.md b/docs/4.examples/3.composables/use-cookie.md deleted file mode 100644 index 4469b87996..0000000000 --- a/docs/4.examples/3.composables/use-cookie.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# useCookie - -This example shows how to use the useCookie API to persist small amounts of data that both client and server can use. - -::ReadMore{link="/docs/api/composables/use-cookie"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/composables/use-cookie" file="app.vue"} -:: diff --git a/docs/4.examples/3.composables/use-fetch.md b/docs/4.examples/3.composables/use-fetch.md deleted file mode 100644 index 4800accbd6..0000000000 --- a/docs/4.examples/3.composables/use-fetch.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -toc: false ---- - -# useFetch - -This example shows how to use useFetch to fetch data from an API endpoint. - -::alert{type=info icon=💡} -Nuxt will automatically read files in the `~/server/api` directory to create API endpoints. -:: - -::ReadMore{link="/docs/api/composables/use-fetch"} -:: - -::ReadMore{link="/docs/getting-started/data-fetching"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/composables/use-fetch" file="app.vue"} -:: diff --git a/docs/4.examples/3.composables/use-head.md b/docs/4.examples/3.composables/use-head.md deleted file mode 100644 index 05ad9d50da..0000000000 --- a/docs/4.examples/3.composables/use-head.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -toc: false ---- - -# useHead - -This example shows how to use useHead and Nuxt built-in components to bind meta data to the head of the page. - -::ReadMore{link="/docs/api/composables/use-head"} -:: - -::ReadMore{link="/docs/getting-started/seo-meta"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/composables/use-head" file="app.vue"} -:: diff --git a/docs/4.examples/3.composables/use-state.md b/docs/4.examples/3.composables/use-state.md deleted file mode 100644 index c78a05bfbf..0000000000 --- a/docs/4.examples/3.composables/use-state.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -toc: false ---- - -# useState - -This example showcase the `useState` composable, an SSR-friendly ref replacement. - -Its value will be preserved after server-side rendering and shared across all components using a unique key. - -::alert{type=info icon=👉} -Learn more about [useState](/docs/api/composables/use-state). -:: - -::ReadMore{link="/docs/api/composables/use-state"} -:: - -::ReadMore{link="/docs/getting-started/state-management"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/composables/use-state" file="app.vue"} -:: diff --git a/docs/4.examples/4.routing/layouts.md b/docs/4.examples/4.routing/layouts.md deleted file mode 100644 index a827569eae..0000000000 --- a/docs/4.examples/4.routing/layouts.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Layouts - -This example shows how to define default and custom layouts. - -::ReadMore{link="/docs/guide/directory-structure/layouts"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/routing/layouts" file="pages/index.vue"} -:: diff --git a/docs/4.examples/4.routing/middleware.md b/docs/4.examples/4.routing/middleware.md deleted file mode 100644 index 4dc925693a..0000000000 --- a/docs/4.examples/4.routing/middleware.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Middleware - -This example shows how to add route middleware with the middleware/ directory or with a plugin, and how to use them globally or per page. - -::ReadMore{link="/docs/guide/directory-structure/middleware"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/routing/middleware" file="app.vue"} -:: diff --git a/docs/4.examples/4.routing/nuxt-link.md b/docs/4.examples/4.routing/nuxt-link.md deleted file mode 100644 index 51ca7fb81b..0000000000 --- a/docs/4.examples/4.routing/nuxt-link.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -toc: false ---- - -# `` - -This example shows different ways to navigate between page with the `` component. - -::alert{type=info icon=💡} -`components/MyNuxtLink.ts` defines a custom ``. -:: - -::ReadMore{link="/docs/api/components/nuxt-link"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/routing/nuxt-link" file="app.vue"} -:: diff --git a/docs/4.examples/4.routing/pages.md b/docs/4.examples/4.routing/pages.md deleted file mode 100644 index 3d715f32ef..0000000000 --- a/docs/4.examples/4.routing/pages.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Pages - -This example shows how to use the pages/ directory to create application routes. - -::ReadMore{link="/docs/guide/directory-structure/pages"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/routing/pages" file="app.vue"} -:: diff --git a/docs/4.examples/4.routing/universal-router.md b/docs/4.examples/4.routing/universal-router.md deleted file mode 100644 index 37f5d5ae71..0000000000 --- a/docs/4.examples/4.routing/universal-router.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -toc: false ---- - -# Universal Router - -This example demonstrates Nuxt universal routing utilities without depending on `pages/` and `vue-router`. - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/routing/universal-router" file="app.vue"} -:: diff --git a/docs/4.examples/5.server/routes.md b/docs/4.examples/5.server/routes.md deleted file mode 100644 index 41b9a73cc2..0000000000 --- a/docs/4.examples/5.server/routes.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Server Routes - -This example shows how to create server routes inside the `server/api` directory. - -::ReadMore{link="/docs/guide/directory-structure/server"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/server/routes" file="app.vue"} -:: diff --git a/docs/4.examples/6.advanced/config-extends.md b/docs/4.examples/6.advanced/config-extends.md deleted file mode 100644 index 0666f2ac5a..0000000000 --- a/docs/4.examples/6.advanced/config-extends.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -toc: false ---- - -# Config Extends - -This example shows how to use the extends key in `nuxt.config.ts`. - -This example shows how to use the `extends` key in nuxt.config.ts to use the `base/` directory as a base Nuxt application, and use its components, composables or config and override them if necessary. - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/advanced/config-extends" file="nuxt.config.ts"} -:: diff --git a/docs/4.examples/6.advanced/jsx.md b/docs/4.examples/6.advanced/jsx.md deleted file mode 100644 index 06eea3aac2..0000000000 --- a/docs/4.examples/6.advanced/jsx.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -toc: false ---- - -# JSX / TSX - -This example shows how to use [jsx syntax](https://vuejs.org/guide/extras/render-function.html#jsx-tsx) with typescript in Nuxt pages and components. - -::ReadMore{link="https://vuejs.org/guide/extras/render-function.html#jsx-tsx"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/advanced/jsx" file="nuxt.config.ts"} diff --git a/docs/4.examples/6.advanced/module-extend-pages.md b/docs/4.examples/6.advanced/module-extend-pages.md deleted file mode 100644 index 1d81488bd1..0000000000 --- a/docs/4.examples/6.advanced/module-extend-pages.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Module Extend Pages - -This example defines a new `test` page using `extendPages` within a module. - -::ReadMore{link="/docs/guide/going-further/modules"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/advanced/module-extend-pages" file="pages/index.vue"} -:: diff --git a/docs/4.examples/6.advanced/testing.md b/docs/4.examples/6.advanced/testing.md deleted file mode 100644 index 693bf7f631..0000000000 --- a/docs/4.examples/6.advanced/testing.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -toc: false ---- - -# Testing - -This example shows how to test your Nuxt application. - -::alert{type=info icon=👉} -Learn more about [testing](/docs/getting-started/testing). -:: - -::ReadMore{link="/docs/getting-started/testing"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/advanced/testing" file="app.vue"} -:: diff --git a/docs/4.examples/7.experimental/reactivity-transform.md b/docs/4.examples/7.experimental/reactivity-transform.md deleted file mode 100644 index 3fb91a94af..0000000000 --- a/docs/4.examples/7.experimental/reactivity-transform.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Reactivity Transform - -This example demonstrates the support of Reactivity Transform in Nuxt 3. - -::ReadMore{link="https://vuejs.org/guide/extras/reactivity-transform.html" title="Reactivity Transform"} -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/experimental/reactivity-transform" file="app.vue"} -:: diff --git a/docs/4.examples/7.experimental/wasm.md b/docs/4.examples/7.experimental/wasm.md deleted file mode 100644 index 728bcb955d..0000000000 --- a/docs/4.examples/7.experimental/wasm.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -toc: false ---- - -# WASM - -This example demonstrates the server-side support of WebAssembly in Nuxt 3. - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/experimental/wasm" file="app.vue"} -:: diff --git a/docs/4.examples/8.other/locale.md b/docs/4.examples/8.other/locale.md deleted file mode 100644 index 7834d311a8..0000000000 --- a/docs/4.examples/8.other/locale.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -toc: false ---- - -# Locale - -This example shows how to define a locale composable to handle the application's locale, both server and client side. - -::alert{type=info icon=💡} -You can right-click to "View Page Source" and see that Nuxt renders the correct date in SSR based on the visitor's locale. -:: - -::sandbox{repo="nuxt/nuxt" branch="main" dir="examples/other/locale" file="app.vue"} diff --git a/docs/4.examples/_dir.yml b/docs/4.examples/_dir.yml deleted file mode 100644 index c07d805eac..0000000000 --- a/docs/4.examples/_dir.yml +++ /dev/null @@ -1 +0,0 @@ -image: '/socials/examples.jpg' diff --git a/docs/4.examples/index.md b/docs/4.examples/index.md deleted file mode 100644 index 111df6d8f5..0000000000 --- a/docs/4.examples/index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -navigation: false -title: "Examples" -redirect: /examples/essentials/hello-world ---- diff --git a/docs/5.community/3.reporting-bugs.md b/docs/5.community/3.reporting-bugs.md index 5d0f055584..7f8337a0a9 100644 --- a/docs/5.community/3.reporting-bugs.md +++ b/docs/5.community/3.reporting-bugs.md @@ -33,16 +33,16 @@ If your issue concerns Vue 3 or Vite, please try to reproduce it first with the **Nuxt 3**: :button-link[Nuxt 3 on StackBlitz]{href="https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz" blank .mr-2} -:button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox" blank} +:button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox" blank} **Nuxt Bridge**: -:button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v2-bridge-codesandbox" blank} +:button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v2-bridge-codesandbox" blank} **Vue 3**: :button-link[Vue 3 SSR on StackBlitz]{href="https://stackblitz.com/github/nuxt-contrib/vue3-ssr-starter/tree/main?terminal=dev" blank .mr-2} -:button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2} +:button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2} :button-link[Vue 3 SSR Template]{href="https://github.com/nuxt-contrib/vue3-ssr-starter/generate" blank} Once you've reproduced the issue, remove as much code from your reproduction as you can (while still recreating the bug). The time spent making the reproduction as minimal as possible will make a huge difference to whoever sets out to fix the issue. diff --git a/docs/5.community/4.contribution.md b/docs/5.community/4.contribution.md index aa90003391..cf0f7b2a4e 100644 --- a/docs/5.community/4.contribution.md +++ b/docs/5.community/4.contribution.md @@ -13,8 +13,7 @@ There is a range of different ways you might be able to contribute to the Nuxt e The Nuxt ecosystem includes many different projects and organizations. For example: * [nuxt/](https://github.com/nuxt) - core repositories for the Nuxt framework itself. [**nuxt/nuxt**](https://github.com/nuxt/nuxt) contains the Nuxt framework (both versions 2 and 3). -* [nuxt-community/](https://github.com/nuxt-community) - community-contributed and maintained modules and libraries. There is a [process to migrate a module](/docs/guide/going-further/modules/#joining-nuxt-community) to `nuxt-community`. While these modules have individual maintainers, they are not dependent on a single person. -* [nuxt-contrib/](https://github.com/nuxt-contrib) - the previous home for libraries that are not specific to Nuxt but produced and used by the Nuxt team. +* [nuxt-modules/](https://github.com/nuxt-modules) - community-contributed and maintained modules and libraries. There is a [process to migrate a module](/docs/guide/going-further/modules/#joining-nuxt-modules-and-nuxtjs) to `nuxt-modules`. While these modules have individual maintainers, they are not dependent on a single person. * [unjs/](https://github.com/unjs) - many of these libraries are used throughout the Nuxt ecosystem. They are designed to be universal libraries that are framework- and environment-agnostic. We welcome contributions and usage by other frameworks and projects. ## How To Contribute @@ -31,7 +30,7 @@ Thank you for taking the time to create an issue! ❤️ * **Feature requests**: Check that there is not an existing issue or discussion covering the scope of the feature you have in mind. If the feature is to another part of the Nuxt ecosystem (such as a module), please consider raising a feature request there first. If the feature you have in mind is general or the API is not entirely clear, consider opening a discussion in the **Ideas** section to discuss with the community first. -We'll do our best to follow our [internal issue decision making flowchart](https://kroki.io/mermaid/svg/eNp9Uktv2kAQvudXzM32IZEK5GJFqUgIhYagqOqlx7V3bLasd1f7SIKg_72ztrFpDuVgiZ35HvPNVFK_lztmPfxcXAH95sellvSIHIRzAcFjYyTz-PVPV7--vj_9QneCh-PaAQ9GivJTeatP8Jg-Su0QmOLA3B68hqolHgizqxbxMBA-XUCMFspH0CCQjd2Rf3FcMQeNUKJhEiwaq3kovdCKnLSti4F4GZ2y0gcm5QEYFKHu7S7OdN_SDStQ5pAoRO7-IUzuCnvfiHrnoWwNigqUhmA42YI7mAJnB9ePsxxUV-mXG_iBjX5DSAwqLlQN3gpWYwIyqkXeyQ3MOYeEPF28TrtXizEoSoOVe4J19SjPTJtKITF2z7puY4W2wh-6tmy0Ewdcxwz8DqHAHXujRqCAyRTyc17rwfn39OmDViRUu4p25igz7kSXrg2BokKejego9Jw-IxrQNDFt3AIXrgzOUY6Rw_4vj-5YYhojKOlzXRH_5rjQ-GmKhu0RtuHDQ1DBxUT61W6GcV4uYKEGVlVY-vZvw353iekKgkPrLrFxmO1xbjG20vdd2z2zOig6jzhYz3eO72XQe71NzW0OwdaofDYWI-HrLDWzHERjtPXsXN6O2ElqJjns6LZHvYumlmOammkeb59ckIPsL5zkNv8) when responding to issues. +We'll do our best to follow our [internal issue decision making flowchart](https://mermaid.live/view#pako:eNqFlE1v2zAMhv8K4UuToslhx2Bo0TZt12Edhm7YMCAXWqJtorLk6qOpkfS_j7KdfpyWQ-BQr8mHL6nsCuU0FauiMm6rGvQRfq03FuRzvvvTYIQHthpcBT_ugQNwPHuZjheLxf4i1VDx8x4udrf5EBCOQvSsYg4ffS79KS9pmX9QALTgyid2KYB7Ih-4bmKWbDk2YB0E1gRUVaRi-FDmmjAmT3u4nB3DmoNKIUA1BsGSohA49jnVMQhHbDh_EZQUImyxh-gAtfaiG-KWSJ-N8nt6YtpCdgEeE5rXPOdav5YwWJIJU7zrvNADV9C7JBIyIC07Wxupkx3LFQ5vCkguRno5f9fP2qnUko0Y2dk9rGdvHAa9IIhVGlCp5FFNPN-ce4DKeXBd53xMliOLp9IZtyORQVsnrGm-WJzejtUu5fFqdr5FGQ3bLslYvGthjZbJTLpReZG5_lLYw7XQ_CbPVT92ws9gnEJj-v84dk-PiaXnmF1XGAaPsOsMKywNvYmG80ZohV8k4wDR9_N3KN_dHm5mh1lnkM5FsYzRfNiTvJoT5gnQsl6uxjqXLhkNQ9syHJ0UZZ8ERUIlNShr6N8gZDEliR-ow7QZa0fhY4LoHLRo-8N7ZxPwjRj5ZZYXpvOSNs9v3Jjs8NXB4ets92xan3zydXZHvj64lKMayh4-gZC1bjASW2ipLeWuzIuToiXfImu5rbucclMIc0ubYiWPGv3DptjYF9Fhiu5nb1Wxij7RSZE6jZHWjLXHtlhVaIJESXN0_m68_sO_wMs_oO9gyg) when responding to issues. ### Send a Pull Request @@ -81,7 +80,7 @@ If we request changes on a PR, please ignore the red text! It doesn't mean we th If we mark a PR as 'pending', that means we likely have another task to do in reviewing the PR - it's an internal note-to-self, and not necessarily a reflection on whether the PR is a good idea or not. We will do our best to explain via a comment the reason for the pending status. -We'll do our best to follow [our PR decision making flowchart](https://kroki.io/mermaid/svg/eNqFk89vm1AMx-_9K3wrqZYcdpymRk3SdJ22qYqmVTuahwGrD0zfj6So7H-fHyTNehoHBH7G34-_NqWVg6nRBfi5uQC9bl4fawzwxG0BUsLDDtgDh-Wf6XQ-nw-rWEHJLwOsXu_TGSBc-uDYhBS-_Jy7a17QIr2QB2xB8j1L9CB7cp6rOqSUA4caWgHPBQGVJZng_1XZEoboaIB1dgUb9iZ6D-UUhJYMec-hT5WuQDFCzelGkJMPcMAeggAWhdO8Md4SFcspfUd7pgMYUeXniPatzk1RvElYzMn6Y7zrnMIDl9BL1BSyoB1LW1nVSX4lhdOXCpLEqFjMzu1sxMSG2oCBpR1gk50xLDolUKMKQGOiQ3PE-SbyBKU4kK4TF2LLgdVRbYybCchiW0WsaDafX9-PYmt9us1uDqhz4baLOhMnDTyI9DhljWmrxPSb_ABbJflFjst-aoJfwIpBa_v_mLWj58jabkiGG_SjPdh1lg3mls5J43mtpMquKdPs0PWzM8kPGeAuO005cXQS1C1G-25B0koeKT8ALarFp0lmLdEWMLasYymioWSRkmgopxp1_9yZQTdSi7gR2h93YiPk3xcIItBg25--W068d2rilyxtSue0aprctCrJ3dvJ3W0yOjtuTYp_zb6Tq04OpWgBeQ8fQbEaGU3EBhpqcv1DJp2Lvz5hNKI) when responding and reviewing to pull requests. +We'll do our best to follow [our PR decision making flowchart](https://mermaid.live/view#pako:eNp9VE1v2kAQ_SsjXzBSEqlALlaUisSh0ACK2l4qcVm8Y9hi7672Iwly-O-ZtYPt5FAOCHbee_PmzdpVlCmOURLlhXrJ9sw4-JNuJNBnWs1UQafIQVjrERyWumAOv58-AJeXt29_0b7BXbWwwL0uRPa1vlZvcB_fF8oiMMmB2QM4BXkt3UoON7Lh3LWaDz2SVkK6QGt7DHvw0CKt5sxCKaQoWQEGtVHcZ04oGdw04LTVngW_LHOeFcURGGz97mw6PSv-iJdsi0UCA4nI7SfNwc3W3JZit3eQ1SZFDlKB15yswQ2MgbOjbYeatY3n8bcr-IWlekYYaJRcyB04I9gOB1CEfkF5dAVTzmFAtnqn4-bUYAiMMmHZgWhNPRhgus5mW2BATxq0NkIZ4Y4NbNjzE2ZchBzcHmGLe_ZMSKCcyRXyLrVFa_5n_PBK2xKy3kk9eOjULUdltk6C8kI-7NFDr8f4EVGDoqlp-wa4sJm3ltIMIuZ_mTQXJyTSkQZtunPqsKxShV9GKdkBYe1fHXjpbcjlvONlO9Kqx_M7YHmOmav_luxfE5zKwVs09hM5DLSupgYDlr5flDkwo7ykixKG-xDsUly1LZ-uY32dgDc7lG7YqwbNp0msJwmIUivjWFtfd-xRrEcJ7Omydz37qFplHOtxEp4GskI2qB5dRCWakglOz3oV8JuITJa4iRL6yZk5bKKNPBGOead-H2UWJc54vIiaW53SPgwrz4fIhVNm1bw76lfI6R2_MW21) when responding and reviewing to pull requests. ### Create a Module @@ -157,7 +156,7 @@ We recommend using [VS Code](https://code.visualstudio.com/) along with the [ESL #### No Prettier -Since ESLint is already configured to format the code, there is no need to duplicate the functionality with Prettier. To format the code, you can run `yarn lint --fix` or `pnpm lint --fix` or referring the [ESLint section](#use-eslint) for IDE Setup. +Since ESLint is already configured to format the code, there is no need to duplicate the functionality with Prettier. To format the code, you can run `yarn lint --fix`, `pnpm lint --fix`, or `bun run lint --fix` or referring the [ESLint section](#use-eslint) for IDE Setup. If you have Prettier installed in your editor, we recommend you disable it when working on the project to avoid conflict. diff --git a/docs/5.community/5.framework-contribution.md b/docs/5.community/5.framework-contribution.md index e4c25cc756..bb95992942 100644 --- a/docs/5.community/5.framework-contribution.md +++ b/docs/5.community/5.framework-contribution.md @@ -59,13 +59,13 @@ Once you've read the [general contribution guide](/docs/community/contribution), While working on a PR, you will likely want to check if your changes are working correctly. -You can modify the example app in `playground/`, and run it with `yarn dev`. Please make sure not to commit it to your branch, but it could be helpful to add some example code to your PR description. This can help reviewers and other Nuxt users understand the feature you've built in-depth. +You can modify the example app in `playground/`, and run it with `pnpm dev`. Please make sure not to commit it to your branch, but it could be helpful to add some example code to your PR description. This can help reviewers and other Nuxt users understand the feature you've built in-depth. ## Testing Every new feature should have a corresponding unit test (if possible). The `test` folder in this repository is currently a work in progress, but do your best to create a new test following the example of what's already there. -Before creating a PR or marking it as ready-to-review, ensure that all tests pass by running `yarn test` locally. +Before creating a PR or marking it as ready-to-review, ensure that all tests pass by running `pnpm test` locally. ## Linting @@ -110,7 +110,7 @@ To contribute to Nuxt, you need to set up a local environment. 1. [Fork](https://help.github.com/articles/fork-a-repo/) the [nuxt/nuxt repository](https://github.com/nuxt/nuxt) to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. -1. Ensure using the latest Node.js (16.x) +1. Ensure using the latest Node.js (20.x) 1. Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` to have `pnpm` and `yarn` @@ -126,21 +126,16 @@ To contribute to Nuxt, you need to set up a local environment. git checkout -b my-new-branch ``` -::js-doc{file=packages/nuxt/src/test.js function=useState} -:: - -::doc-link{file=packages/nuxt/src/test.js function=useState} -:: - ### Set Up Documentation Website in Local Environment The Nuxt documentation is currently deployed within [nuxt/nuxt.com](https://github.com/nuxt/nuxt.com) as a layer. - Run `pnpm build:stub` once in the root directory - - - -- Before opening a PR, run `pnpm docs:lint:fix` to highlight and resolve any lint issues +- Go into the `.website` directory: `cd .website` +- Install dependencies using `pnpm install` + +- Run `pnpm dev` to start docs in development mode +- Before opening a PR, run `pnpm lint:docs:fix` to highlight and resolve any lint issues ::alert 🚧 This repository will be open-sourced shortly. Until then, you will need to open a pull request to see a preview of your changes. diff --git a/docs/5.community/6.roadmap.md b/docs/5.community/6.roadmap.md index fbfa9878f1..c1c651638f 100644 --- a/docs/5.community/6.roadmap.md +++ b/docs/5.community/6.roadmap.md @@ -14,38 +14,21 @@ See [our blog](/blog) for the latest framework and ecosystem announcements. - [Rendering Optimizations: Today and Tomorrow](https://github.com/nuxt/nuxt/discussions/16119) - [Nuxt Image: Performance and Status](https://github.com/nuxt/nuxt/discussions/16119) -### Release Cycle - -Since January 2023, we've adopted a consistent release cycle for **Nuxt 3**, following [semver](https://semver.org/). We aim for major framework releases every year, with an expectation of patch releases every week or so and minor releases every month or so. They should never contain breaking changes except within options clearly marked as `experimental`. - -### Current Packages - -The current active version of [Nuxt](https://nuxt.com) is **v3** which is available as `nuxt` on npm with the `latest` tag. - -Nuxt 2 is in maintenance mode and is available on npm with the `2x` tag. It will reach End of Life (EOL) on December 31st, 2023 at the same time as Vue 2 does. - -Each version has its own edge releases which are generated automatically. For more about enabling the Nuxt 3 edge channel, see [the edge channel docs](/docs/guide/going-further/edge-channel/). - -Release | npm | Status | Last Release | Docs | Repository ----------|----|---------|--------------|------|----------------- -Nuxt 3.x | [`nuxt`](https://npmjs.com/package/nuxt) | Stable | | [3.x docs](https://nuxt.com/docs/) | [nuxt/nuxt](https://github.com/nuxt/nuxt) -Nuxt 3.x (edge*) | [`nuxt3`](https://npmjs.com/package/nuxt3) | Development | | [3.x docs](https://nuxt.com/) | [nuxt/nuxt](https://github.com/nuxt/nuxt) -Nuxt 2.x | [`nuxt`](https://npmjs.com/package/nuxt) | Maintenance | | [2.x docs](https://nuxtjs.org/docs) | [nuxt/nuxt#2.x](https://github.com/nuxt/nuxt/tree/2.x) -Nuxt 2.x (edge*) | [`nuxt-edge`](https://npmjs.com/package/nuxt-edge) | Maintenance | | [2.x docs](https://nuxtjs.org/docs) | [nuxt/nuxt](https://github.com/nuxt/nuxt/tree/2.x) - ## 🛣️ Roadmap -In roadmap below are the major expected features that are coming soon with Nuxt 3. +In roadmap below are some features we are planning or working on at the moment. 💡 Check [Discussions](https://github.com/nuxt/nuxt/discussions) and [RFCs](https://github.com/nuxt/nuxt/discussions/categories/rfcs) for more upcoming features and ideas. -Milestone | Expected date | Notes | Description --------------|------------------|--------|----------------------- -Image | 2023 | [nuxt/image#548](https://github.com/nuxt/image/discussions/548) | Stable image optimization for Nuxt 3 -SEO & PWA | 2023 | [nuxt/nuxt#18395](https://github.com/nuxt/nuxt/discussions/18395) | Migrating from [nuxt-community/pwa-module](https://github.com/nuxt-community/pwa-module) for built-in SEO utils and service worker support -DevTools | 2023 | - | Integrated and modular devtools experience for Nuxt -Scripts | 2023 | [nuxt/nuxt#16119](https://github.com/nuxt/nuxt/discussions/) | Easy 3rd party script management. -Translations | - | [nuxt/translations#4](https://github.com/nuxt/translations/discussions/4) ([request access](https://github.com/nuxt/nuxt/discussions/16054)) | A collaborative project for a stable translation process for Nuxt 3 docs. Currently pending for ideas and documentation tooling support (content v2 with remote sources). +Milestone | Expected date | Notes | Description +-------------|---------------|------------------------------------------------------------------------|----------------------- +SEO & PWA | 2023 | [nuxt/nuxt#18395](https://github.com/nuxt/nuxt/discussions/18395) | Migrating from [nuxt-community/pwa-module](https://github.com/nuxt-community/pwa-module) for built-in SEO utils and service worker support +DevTools | 2023 | - | Integrated and modular devtools experience for Nuxt +Scripts | 2023 | [nuxt/nuxt#22016](https://github.com/nuxt/nuxt/discussions/22016) | Easy 3rd party script management. +Fonts | 2023 | [nuxt/nuxt#22014](https://github.com/nuxt/nuxt/discussions/22014) | Allow developers to easily configure fonts in their Nuxt apps. +Assets | 2023 | [nuxt/nuxt#22012](https://github.com/nuxt/nuxt/discussions/22012) | Allow developers and modules to handle loading third-party assets. +A11y | 2023 | [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255) | Accessibility hinting and utilties +Translations | - | [nuxt/translations#4](https://github.com/nuxt/translations/discussions/4) ([request access](https://github.com/nuxt/nuxt/discussions/16054)) | A collaborative project for a stable translation process for Nuxt 3 docs. Currently pending for ideas and documentation tooling support (content v2 with remote sources). ## 📦 Core Modules @@ -53,8 +36,39 @@ In addition to the Nuxt framework, there are modules that are vital for the ecos Module | Status | Nuxt Support | Repository | Description ---------------|---------------------|--------------|------------|------------------- -Content | Active | 3.x | [nuxt/content](https://github.com/nuxt/content) | Released Auth | Planned | 3.x | `nuxt/auth` to be announced | Nuxt 3 support is planned after session support Image | Active | 2.x and 3.x | [nuxt/image](https://github.com/nuxt/image) | Nuxt 3 support is in progress: [nuxt/image#548](https://github.com/nuxt/image/discussions/548) -Telemetry | Active | 2.x and 3.x | [nuxt/telemetry](https://github.com/nuxt/telemetry/) | Nuxt 3 is supported. Stats to be public soon! I18n | Active | 2.x and 3.x | [nuxt-modules/i18n](https://github.com/nuxt-modules/i18n) | See [nuxt-modules/i18n#1287](https://github.com/nuxt-modules/i18n/discussions/1287) for Nuxt 3 support + +### Release Cycle + +Since January 2023, we've adopted a consistent release cycle for **Nuxt 3**, following [semver](https://semver.org/). We aim for major framework releases every year, with an expectation of patch releases every week or so and minor releases every month or so. They should never contain breaking changes except within options clearly marked as `experimental`. + +#### 📅 Ongoing Support for Nuxt + +Going forward from v3, we commit to support each major version of Nuxt for a minimum of a year after the last release, and to providing an upgrade path for current users at that point. + +##### Current Packages + +The current active version of [Nuxt](https://nuxt.com) is **v3** which is available as `nuxt` on npm with the `latest` tag. + +Nuxt 2 is in maintenance mode and is available on npm with the `2x` tag. It will reach End of Life (EOL) on December 31st, 2023 at the same time as Vue 2 does. + +Each active version has its own nightly releases which are generated automatically. For more about enabling the Nuxt 3 nightly release channel, see [the nightly release channel docs](/docs/guide/going-further/nightly-release-channel). + +Release | | Initial release | End Of Life | Docs +----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|------- +**4.x** (scheduled) | | 2024 Q1 | | +3.x (stable) | | 2022-11-16 | TBA | [nuxt.com](https://nuxt.com/docs) +2.x (maintenance) | | 2018-09-21 | 2023-12-31 | [v2.nuxt.com](https://v2.nuxt.com/docs) +1.x (unsupported) | | 2018-01-08 | 2019-09-21 | + +##### Support Status + +Status | Description +------------|---------------------------------------------------------------------------------- +Unsupported | This version is not maintained any more and will not receive security patches +Maintenance | This version will only receive security patches +Stable | This version is being developed for and will receive security patches +Development | This version could be unstable +Scheduled | This version does not exist yet but it planned diff --git a/docs/6.bridge/1.overview.md b/docs/6.bridge/1.overview.md index 7eb4a90159..5099aef99b 100644 --- a/docs/6.bridge/1.overview.md +++ b/docs/6.bridge/1.overview.md @@ -1,26 +1,28 @@ # Overview -Experience Nuxt 3 features on existing Nuxt 2 projects. +Reduce the differences with Nuxt 3 and reduce the burden of migration to Nuxt 3. ::alert If you're starting a fresh Nuxt 3 project, please skip this section and go to [Nuxt 3 Installation](/docs/getting-started/introduction). :: ::alert{type=warning} -Nuxt Bridge provides identical features to Nuxt 3 ([docs](/docs/guide/concepts/auto-imports)) but there are some limitations, notably that `useAsyncData` and `useFetch` composables are not available. Please read the rest of this page for details. +Nuxt Bridge provides identical features to Nuxt 3 ([docs](/docs/guide/concepts/auto-imports)) but there are some limitations, notably that [`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) composables are not available. Please read the rest of this page for details. :: Bridge is a forward-compatibility layer that allows you to experience many of the new Nuxt 3 features by simply installing and enabling a Nuxt module. -Using Nuxt Bridge, you can make sure your project is (almost) ready for Nuxt 3 and have the best developer experience without needing a major rewrite or risk breaking changes. +Using Nuxt Bridge, you can make sure your project is (almost) ready for Nuxt 3 and you can gradually proceed with the transition to Nuxt 3. -## Upgrade Nuxt 2 +## First Step + +### Upgrade Nuxt 2 Make sure your dev server (`nuxt dev`) isn't running, remove any package lock files (`package-lock.json` and `yarn.lock`), and install the latest Nuxt 2 version: ```diff [package.json] -- "nuxt": "^2.15.0" -+ "nuxt": "^2.16.0" +- "nuxt": "^2.16.3" ++ "nuxt": "^2.17.0" ``` Then, reinstall your dependencies: @@ -41,7 +43,7 @@ npm install Once the installation is complete, make sure both development and production builds are working as expected before proceeding. :: -## Install Nuxt Bridge +### Install Nuxt Bridge Install `@nuxt/bridge-edge` as a development dependency: @@ -57,261 +59,62 @@ npm install -D @nuxt/bridge@npm:@nuxt/bridge-edge :: -## Update Your Scripts +### Update `nuxt.config` -You will also need to update your scripts within your `package.json` to reflect the fact that Nuxt will now produce a Nitro server as build output. +Please make sure to avoid any CommonJS syntax such as `module.exports`, `require` or `require.resolve` in your config file. It will soon be deprecated and unsupported. -### Nuxi +You can use static `import`, dynamic `import()` and `export default` instead. Using TypeScript by renaming to [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) is also possible and recommended. -Nuxt 3 introduced the new Nuxt CLI command [`nuxi`](/docs/api/commands/add). Update your scripts as follows to leverage the better support from Nuxt Bridge: +```ts [nuxt.config.js|ts] +import { defineNuxtConfig } from '@nuxt/bridge' + +export default defineNuxtConfig({ + bridge: false +}) +``` + +### Update Commands + +The `nuxt` command should now be changed to the `nuxt2` command. ```diff { "scripts": { - "dev": "nuxt", -+ "dev": "nuxi dev", ++ "dev": "nuxt2", - "build": "nuxt build", -+ "build": "nuxi build", ++ "build": "nuxt2 build", - "start": "nuxt start", -+ "start": "nuxi preview" ++ "start": "nuxt2 start" } } ``` -### Static Target +Try running `nuxt2` once here. You will see that the application works as before. -If you have set `target: 'static'` in your `nuxt.config` then you need to ensure that you update your build script to be `nuxi generate`. +(If 'bridge' is set to false, your application will operate without any changes as before.) -```json [package.json] -{ - "scripts": { - "build": "nuxi generate" - } -} -``` +## Upgrade Steps -### Server Target +With Nuxt Bridge, the migration to Nuxt 3 can proceed in steps. +The below `Upgrade Steps` does not need to be done all at once. -For all other situations, you can use the `nuxi build` command. +- [TypeScript](/docs/bridge/typescript) -```json [package.json] -{ - "scripts": { - "build": "nuxi build", - "start": "nuxi preview" - } -} -``` +- [Migrate Legacy Composition API](/docs/bridge/bridge-composition-api) -## Update `nuxt.config` +- [Plugins and Middleware](/docs/bridge/plugins-and-middleware) -Please make sure to avoid any CommonJS syntax such as `module.exports`, `require` or `require.resolve` in your config file. It will soon be deprecated and unsupported. +- [Migrate New Composition API](/docs/bridge/nuxt3-compatible-api) -You can use static `import`, dynamic `import()` and `export default` instead. Using TypeScript by renaming to `nuxt.config.ts` is also possible and recommended. +- [Meta Tags](/docs/bridge/meta) -```ts [nuxt.config.js|ts] -import { defineNuxtConfig } from '@nuxt/bridge' +- [Runtime Config](/docs/bridge/runtime-config) -export default defineNuxtConfig({ - // Your existing configuration -}) -``` +- [Nitro](/docs/bridge/nitro) -## Update `tsconfig.json` +- [Vite](/docs/bridge/vite) -If you are using TypeScript, you can edit your `tsconfig.json` to benefit from auto-generated Nuxt types: - -```diff [tsconfig.json] -{ -+ "extends": "./.nuxt/tsconfig.json", - "compilerOptions": { - ... - } -} -``` - -::alert -As `.nuxt/tsconfig.json` is generated and not checked into version control, you'll need to generate that file before running your tests. Add `nuxi prepare` as a step before your tests, otherwise you'll see `TS5083: Cannot read file '~/.nuxt/tsconfig.json'` -:: -::alert -You may also need to add `@vue/runtime-dom` as a devDependency if you are struggling to get template type inference working with [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar). -:: -::alert -Keep in mind that all options extended from `./.nuxt/tsconfig.json` will be overwritten by the options defined in your `tsconfig.json`. -Overwriting options such as `"compilerOptions.paths"` with your own configuration will lead TypeScript to not factor in the module resolutions from `./.nuxt/tsconfig.json`. This can lead to module resolutions such as `#imports` not being recognized. - -In case you need to extend options provided by `./.nuxt/tsconfig.json` further, you can use the `alias` property withing your `nuxt.config`. `nuxi` will pick them up and extend `./.nuxt/tsconfig.json` accordingly. -:: - -## Update Runtime Config - -Nuxt 3 approaches runtime config differently than Nuxt 2, using a new combined `runtimeConfig` option. - -First, you'll need to combine your `publicRuntimeConfig` and `privateRuntimeConfig` properties into a new one called `runtimeConfig`, with the public config within a key called `public`. - -```diff -// nuxt.config.js -- privateRuntimeConfig: { -- apiKey: process.env.NUXT_API_KEY || 'super-secret-key' -- }, -- publicRuntimeConfig: { -- websiteURL: 'https://public-data.com' -- } -+ runtimeConfig: { -+ apiKey: process.env.NUXT_API_KEY || 'super-secret-key', -+ public: { -+ websiteURL: 'https://public-data.com' -+ } -+ } -``` - -This also means that when you need to access public runtime config, it's behind a property called `public`. If you use public runtime config, you'll need to update your code. - -```diff -// MyWidget.vue --
Website: {{ $config.websiteURL }}
-+
Website: {{ $config.public.websiteURL }}
-``` - -## Migrate Composition API - -If you were using `@vue/composition-api` or `@nuxtjs/composition-api`, please read the [composition api migration guide](/docs/bridge/bridge-composition-api). - -### Migrate from CommonJS to ESM +## Migrate from CommonJS to ESM Nuxt 3 natively supports TypeScript and ECMAScript Modules. Please check [Native ES Modules](/docs/guide/concepts/esm) for more info and upgrading. - -## Remove Incompatible and Obsolete Modules - -- Remove `@nuxt/content` (1.x). A rewrite for Nuxt 3 [is available](https://content.nuxtjs.org/) (2.x) -- Remove `nuxt-vite`: Bridge enables same functionality -- Remove `@nuxt/typescript-build`: Bridge enables same functionality -- Remove `@nuxt/typescript-runtime` and `nuxt-ts`: Nuxt 2 has built-in runtime support -- Remove `@nuxt/nitro`: Bridge injects same functionality -- Remove `@vue/composition-api` from your dependencies ([migration guide](/docs/bridge/bridge-composition-api)). -- Remove `@nuxtjs/composition-api` from your dependencies (and from your modules in `nuxt.config`) ([migration guide](/docs/bridge/bridge-composition-api)). - -## Exclude Built Nitro Folder From Git - -Add the folder `.output` to the `.gitignore` file. - -## Ensure Everything Goes Well - -✔️ Try with `nuxi dev` and `nuxi build` (or `nuxi generate`) to see if everything goes well. - -🐛 Is something wrong? Please let us know by creating an issue. Also, you can easily disable the bridge in the meantime: - -```ts [nuxt.config.js|ts] -import { defineNuxtConfig } from '@nuxt/bridge' -export default defineNuxtConfig({ - bridge: false // Temporarily disable bridge integration -}) -``` - -## New Plugins Format (Optional) - -You can now migrate to the Nuxt 3 plugins API, which is slightly different in format from Nuxt 2. - -Plugins now take only one argument (`nuxtApp`). You can find out more in [the docs](/docs/guide/directory-structure/plugins). - -```js -export default defineNuxtPlugin(nuxtApp => { - nuxtApp.provide('injected', () => 'my injected function') - // now available on `nuxtApp.$injected` -}) -``` - -::alert -If you want to use the new Nuxt composables (such as `useNuxtApp` or `useRuntimeConfig`) within your plugins, you will need to use the `defineNuxtPlugin` helper for those plugins. -:: - -::alert{type=warning} -Although a compatibility interface is provided via `nuxtApp.vueApp` you should avoid registering plugins, directives, mixins or components this way without adding your own logic to ensure they are not installed more than once, or this may cause a memory leak. -:: - -## New `useHead` (Optional) - -Nuxt Bridge provides a new Nuxt 3 meta API that can be accessed with a new `useHead` composable. - -```vue - -``` - -You will also need to enable this feature explicitly in your `nuxt.config`: - -```js -import { defineNuxtConfig } from '@nuxt/bridge' -export default defineNuxtConfig({ - bridge: { - meta: true - } -}) -``` - -::alert -This `useHead` composable uses `@vueuse/head` under the hood (rather than `vue-meta`) to manipulate your ``. You need to add `@vueuse/head` to your package.json file for it to work properly. -:: - -::alert{type=warning} -We recommend not using the native Nuxt 2 `head()` properties in addition to `useHead`, as they may conflict. -:: - -For more information on how to use this composable, see [the docs](/docs/getting-started/seo-meta). - -## Feature Flags - -You can optionally disable some features from bridge or opt-in to less stable ones. In normal circumstances, it is always best to stick with defaults! - -You can check [bridge/src/module.ts](https://github.com/nuxt/bridge/blob/main/packages/bridge/src/module.ts) for latest defaults. - -```ts [nuxt.config.js|ts] -import { defineNuxtConfig } from '@nuxt/bridge' -export default defineNuxtConfig({ - bridge: { - - // -- Opt-in features -- - - // Use Vite as the bundler instead of webpack 4 - // vite: true, - - // Enable Nuxt 3 compatible useHead - // meta: true, - - // -- Default features -- - - // Use legacy server instead of Nitro - // nitro: false, - - // Disable Nuxt 3 compatible `nuxtApp` interface - // app: false, - - // Disable Composition API support - // capi: false, - - // ... or just disable legacy Composition API support - // capi: { - // legacy: false - // }, - - // Do not transpile modules - // transpile: false, - - // Disable +``` + +### Disabling Auto-imports + +If you want to disable auto-importing composables and utilities, you can set `imports.autoImport` to `false` in the `nuxt.config` file. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + imports: { + autoImport: false + } +}) +``` + +This will disable auto-imports completely but it's still possible to use [explicit imports](#explicit-imports) from `#imports`. diff --git a/docs/6.bridge/6.meta.md b/docs/6.bridge/6.meta.md new file mode 100644 index 0000000000..b8f0456de0 --- /dev/null +++ b/docs/6.bridge/6.meta.md @@ -0,0 +1,110 @@ +# Meta Tags + +If you need to access the component state with `head`, you should migrate to using [`useHead`](/docs/api/composables/use-head) . + +If you need to use the Options API, there is a `head()` method you can use when you use `defineNuxtComponent`. + +## Migration + +### Set `bridge.meta` + +```js +import { defineNuxtConfig } from '@nuxt/bridge' +export default defineNuxtConfig({ + bridge: { + meta: true, + nitro: false // If migration to Nitro is complete, set to true + } +}) +``` + +### Update head properties + +In your `nuxt.config`, rename `head` to `meta`. (Note that objects no longer have a `hid` key for deduplication.) + +::code-group + +```ts [Nuxt 2] +export default { + head: { + titleTemplate: '%s - Nuxt', + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { hid: 'description', name: 'description', content: 'Meta description' } + ] + } +} +``` + +```ts [Nuxt 3] +export default defineNuxtConfig({ + app: { + head: { + titleTemplate: '%s - Nuxt', + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { name: 'description', content: 'Meta description' } + ] + } + } +}) +``` + +:: + +## `useHead` Composables + +Nuxt Bridge provides a new Nuxt 3 meta API that can be accessed with a new [`useHead`](/docs/api/composables/use-head) composable. + +```vue + +``` + +::alert +This [`useHead`](/docs/api/composables/use-head) composable uses `@unhead/vue` under the hood (rather than `vue-meta`) to manipulate your ``. +:: + +::alert{type=warning} +We recommend not using the native Nuxt 2 `head()` properties in addition to [`useHead`](/docs/api/composables/use-head) , as they may conflict. +:: + +For more information on how to use this composable, see [the docs](/docs/getting-started/seo-meta). + +## Options API + +```vue + +``` + +## Title Template + +If you want to use a function (for full control), then this cannot be set in your nuxt.config, and it is recommended instead to set it within your `/layouts` directory. + +```vue [layouts/default.vue] + +``` diff --git a/docs/6.bridge/7.runtime-config.md b/docs/6.bridge/7.runtime-config.md new file mode 100644 index 0000000000..11d9e397b7 --- /dev/null +++ b/docs/6.bridge/7.runtime-config.md @@ -0,0 +1,35 @@ +# Runtime Config + +::alert{type=warning} +When using `runtimeConfig` option, [nitro](/docs/bridge/nitro) must have been configured. +:: + +## Update Runtime Config + +Nuxt 3 approaches runtime config differently than Nuxt 2, using a new combined `runtimeConfig` option. + +First, you'll need to combine your `publicRuntimeConfig` and `privateRuntimeConfig` properties into a new one called `runtimeConfig`, with the public config within a key called `public`. + +```diff +// nuxt.config.js +- privateRuntimeConfig: { +- apiKey: process.env.NUXT_API_KEY || 'super-secret-key' +- }, +- publicRuntimeConfig: { +- websiteURL: 'https://public-data.com' +- } ++ runtimeConfig: { ++ apiKey: process.env.NUXT_API_KEY || 'super-secret-key', ++ public: { ++ websiteURL: 'https://public-data.com' ++ } ++ } +``` + +This also means that when you need to access public runtime config, it's behind a property called `public`. If you use public runtime config, you'll need to update your code. + +```diff +// MyWidget.vue +-
Website: {{ $config.websiteURL }}
++
Website: {{ $config.public.websiteURL }}
+``` diff --git a/docs/6.bridge/8.nitro.md b/docs/6.bridge/8.nitro.md new file mode 100644 index 0000000000..01b5f5a902 --- /dev/null +++ b/docs/6.bridge/8.nitro.md @@ -0,0 +1,91 @@ +# Nitro + +## Remove Modules + +- Remove `@nuxt/nitro`: Bridge injects same functionality + +## Update Config + +```ts [nuxt.config.js|ts] +import { defineNuxtConfig } from '@nuxt/bridge' + +export default defineNuxtConfig({ + bridge: { + nitro: true + } +}) +``` + +## Update Your Scripts + +You will also need to update your scripts within your `package.json` to reflect the fact that Nuxt will now produce a Nitro server as build output. + +### Install Nuxi + +Install `nuxi` as a development dependency: + +::code-group + +```bash [Yarn] +yarn add --dev nuxi +``` + +```bash [npm] +npm install -D nuxi +``` + +:: + +### Nuxi + +Nuxt 3 introduced the new Nuxt CLI command [`nuxi`](/docs/api/commands/add). Update your scripts as follows to leverage the better support from Nuxt Bridge: + +```diff +{ + "scripts": { +- "dev": "nuxt", ++ "dev": "nuxi dev", +- "build": "nuxt build", ++ "build": "nuxi build", +- "start": "nuxt start", ++ "start": "nuxi preview" + } +} +``` + +::alert +If `nitro: false`, use the `nuxt2` command. +:: + +### Static Target + +If you have set `target: 'static'` in your `nuxt.config` then you need to ensure that you update your build script to be `nuxi generate`. + +```json [package.json] +{ + "scripts": { + "build": "nuxi generate" + } +} +``` + +### Server Target + +For all other situations, you can use the `nuxi build` command. + +```json [package.json] +{ + "scripts": { + "build": "nuxi build", + "start": "nuxi preview" + } +} +``` + +## Exclude Built Nitro Folder From Git + +Add the folder `.output` to the `.gitignore` file. + +## Ensure Everything Goes Well + +✔️ Try with `nuxi dev` and `nuxi build` (or `nuxi generate`) to see if everything goes well. diff --git a/docs/6.bridge/9.vite.md b/docs/6.bridge/9.vite.md new file mode 100644 index 0000000000..4bf7d289aa --- /dev/null +++ b/docs/6.bridge/9.vite.md @@ -0,0 +1,34 @@ +# Vite + +::alert{type=warning} +When using `vite`, [nitro](/docs/bridge/nitro) must have been configured. +:: + +## Remove Modules + +- Remove `nuxt-vite`: Bridge enables same functionality + +## Update Config + +```ts [nuxt.config.js|ts] +import { defineNuxtConfig } from '@nuxt/bridge' + +export default defineNuxtConfig({ + bridge: { + vite: true, + nitro: true + } +}) +``` + +## Configuration + +```ts [nuxt.config.js|ts] +import { defineNuxtConfig } from '@nuxt/bridge' + +export default defineNuxtConfig({ + vite: { + // Config for Vite + } +}) +``` diff --git a/docs/7.migration/1.overview.md b/docs/7.migration/1.overview.md index 2a5d2558a9..f19d6826c2 100644 --- a/docs/7.migration/1.overview.md +++ b/docs/7.migration/1.overview.md @@ -23,3 +23,7 @@ Some of these significant changes include: ::alert{type=info} If you need to remain on Nuxt 2, but want to benefit from Nuxt 3 features in Nuxt 2, you can alternatively check out [how to get started with Bridge](/docs/bridge/overview). :: + +## Next Steps + +- Learn about differences in [configuration](/docs/migration/configuration) diff --git a/docs/7.migration/2.configuration.md b/docs/7.migration/2.configuration.md index 84c8634e32..1f7aba5334 100644 --- a/docs/7.migration/2.configuration.md +++ b/docs/7.migration/2.configuration.md @@ -94,7 +94,7 @@ If you are a module author, you can check out [more information about module com ## Directory Changes -The `static/` directory (for storing static assets) has been renamed to `public/`. You can either rename your `static` directory to `public`, or keep the name by setting `dir.public` in your `nuxt.config`. +The [`static/` directory](/docs/guide/directory-structure/static) (for storing static assets) has been renamed to `public/`. You can either rename your `static` directory to `public`, or keep the name by setting `dir.public` in your `nuxt.config`. ::ReadMore{link="/docs/guide/directory-structure/public"} :: @@ -106,7 +106,7 @@ It will be much easier to migrate your application if you use Nuxt's TypeScript You can read more about Nuxt's TypeScript support [in the docs](/docs/guide/concepts/typescript). ::alert{icon=📦} -Nuxt can type-check your app using [`vue-tsc`](https://github.com/johnsoncodehk/volar/tree/master/vue-language-tools/vue-tsc) with `nuxi typecheck` command. +Nuxt can type-check your app using [`vue-tsc`](https://github.com/vuejs/language-tools/tree/master/packages/vue-tsc) with `nuxi typecheck` command. :: ### Migration @@ -128,7 +128,7 @@ There are a number of changes to what is recommended Vue best practice, as well It is recommended to read the [Vue 3 migration guide](https://v3-migration.vuejs.org/) and in particular the [breaking changes list](https://v3-migration.vuejs.org/breaking-changes/). -It is not currently possible to use the Vue 3 migration build with Nuxt 3 RC. +It is not currently possible to use the [Vue 3 migration build](https://v3-migration.vuejs.org/migration-build.html) with Nuxt 3. ## Vuex @@ -136,12 +136,22 @@ Nuxt no longer provides a Vuex integration. Instead, the official Vue recommenda A simple way to provide global state management with pinia would be: -Install the [@nuxt/pinia](https://nuxt.com/modules/pinia) module: +Install the [@pinia/nuxt](https://nuxt.com/modules/pinia) module: ```bash yarn add pinia @pinia/nuxt ``` +Enable the module in your nuxt configuration: + +```ts [nuxt.config.ts] +import { defineNuxtConfig } from 'nuxt/config'; + +export default defineNuxtConfig({ + modules: ['@pinia/nuxt'] +}) +``` + Create a `store` folder at the root of your application: ```ts [store/index.ts] diff --git a/docs/7.migration/4.meta.md b/docs/7.migration/4.meta.md index 0d9ab86e8e..cc59864230 100644 --- a/docs/7.migration/4.meta.md +++ b/docs/7.migration/4.meta.md @@ -3,7 +3,7 @@ Nuxt 3 provides several different ways to manage your meta tags. 1. Through your `nuxt.config`. -2. Through the `useHead` [composable](/docs/getting-started/seo-meta) +2. Through the [`useHead`](/docs/api/composables/use-head) [composable](/docs/getting-started/seo-meta) 3. Through [global meta components](/docs/getting-started/seo-meta) You can customize `title`, `titleTemplate`, `base`, `script`, `noscript`, `style`, `meta`, `link`, `htmlAttrs` and `bodyAttrs`. @@ -17,10 +17,10 @@ Nuxt currently uses [`vueuse/head`](https://github.com/vueuse/head) to manage yo ## Migration 1. In your `nuxt.config`, rename `head` to `meta`. Consider moving this shared meta configuration into your `app.vue` instead. (Note that objects no longer have a `hid` key for deduplication.) -1. If you need to access the component state with `head`, you should migrate to using `useHead`. You might also consider using the built-in meta-components. +1. If you need to access the component state with `head`, you should migrate to using [`useHead`](/docs/api/composables/use-head) . You might also consider using the built-in meta-components. 1. If you need to use the Options API, there is a `head()` method you can use when you use `defineNuxtComponent`. -### Example: `useHead` +### Example: useHead ::code-group @@ -46,7 +46,7 @@ export default { ``` ```vue [Nuxt 3] - ``` You can now use `post` inside of your Nuxt 3 template, or call `refresh` to update the data. ::alert{type=info} -Despite the names, `useFetch` is not a direct replacement of the `fetch()` hook. Rather, `useAsyncData` replaces both hooks and is more customizable; it can do more than simply fetching data from an endpoint. `useFetch` is a convenience wrapper around `useAsyncData` for simply fetching data from an endpoint. +Despite the names, [`useFetch`](/docs/api/composables/use-fetch) is not a direct replacement of the `fetch()` hook. Rather, [`useAsyncData`](/docs/api/composables/use-async-data) replaces both hooks and is more customizable; it can do more than simply fetching data from an endpoint. [`useFetch`](/docs/api/composables/use-fetch) is a convenience wrapper around [`useAsyncData`](/docs/api/composables/use-async-data) for simply fetching data from an endpoint. :: ### Migration -1. Replace the `asyncData` hook with `useAsyncData` or `useFetch` in your page/component. -1. Replace the `fetch` hook with `useAsyncData` or `useFetch` in your component. +1. Replace the `asyncData` hook with [`useAsyncData`](/docs/api/composables/use-async-data) or [`useFetch`](/docs/api/composables/use-fetch) in your page/component. +1. Replace the `fetch` hook with [`useAsyncData`](/docs/api/composables/use-async-data) or [`useFetch`](/docs/api/composables/use-fetch) in your component. ## `head` @@ -104,7 +104,7 @@ See [middleware migration](/docs/migration/plugins-and-middleware). ## `scrollToTop` -This feature is not yet supported in Nuxt 3. If you want to overwrite the default scroll behavior of `vue-router`, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/directory-structure/pages/#router-options)) for more info. +This feature is not yet supported in Nuxt 3. If you want to overwrite the default scroll behavior of `vue-router`, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/going-further/custom-routing#router-options)) for more info. ## `transition` @@ -114,7 +114,7 @@ See [layout migration](/docs/migration/pages-and-layouts). The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as in Nuxt 2, you can return a boolean value. If you return false and another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked). -```diff [pages/users/[id].vue] +```diff [pages/users/[id\\].vue] - ``` diff --git a/examples/README.md b/examples/README.md index 75ee27e050..ba22dfb66e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,4 @@ # Nuxt 3 Examples -👉 https://nuxt.com/docs/examples +- 👉 See examples in your browser at https://nuxt.com/docs/examples +- 👉 View on GitHub at https://github.com/nuxt/examples diff --git a/examples/advanced/config-extends/app.config.ts b/examples/advanced/config-extends/app.config.ts deleted file mode 100644 index 6ffbebb636..0000000000 --- a/examples/advanced/config-extends/app.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default defineAppConfig({ - foo: 'user', - bar: 'user', - baz: 'base', - array: [ - 'user', - 'user', - 'user' - ] -}) diff --git a/examples/advanced/config-extends/base/app.config.ts b/examples/advanced/config-extends/base/app.config.ts deleted file mode 100644 index e55c3765c3..0000000000 --- a/examples/advanced/config-extends/base/app.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -export default defineAppConfig({ - bar: 'base', - baz: 'base', - array: () => [ - 'base', - 'base', - 'base' - ], - arrayNested: { - nested: { - array: [ - 'base', - 'base', - 'base' - ] - } - } -}) diff --git a/examples/advanced/config-extends/base/components/BaseButton.vue b/examples/advanced/config-extends/base/components/BaseButton.vue deleted file mode 100644 index 3279975462..0000000000 --- a/examples/advanced/config-extends/base/components/BaseButton.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/advanced/config-extends/base/components/FancyButton.vue b/examples/advanced/config-extends/base/components/FancyButton.vue deleted file mode 100644 index 97170c811c..0000000000 --- a/examples/advanced/config-extends/base/components/FancyButton.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/advanced/config-extends/base/composables/foo.ts b/examples/advanced/config-extends/base/composables/foo.ts deleted file mode 100644 index 7dbb750401..0000000000 --- a/examples/advanced/config-extends/base/composables/foo.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { useState } from '#app' - -export const useFoo = () => useState('foo', () => 'foo') diff --git a/examples/advanced/config-extends/base/middleware/foo.ts b/examples/advanced/config-extends/base/middleware/foo.ts deleted file mode 100644 index 75a2c59ba7..0000000000 --- a/examples/advanced/config-extends/base/middleware/foo.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default defineNuxtRouteMiddleware(() => { - console.log('Hello from extended middleware !') -}) diff --git a/examples/advanced/config-extends/base/nuxt.config.ts b/examples/advanced/config-extends/base/nuxt.config.ts deleted file mode 100644 index 4e901201e7..0000000000 --- a/examples/advanced/config-extends/base/nuxt.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default defineNuxtConfig({ - imports: { - dirs: ['utils'] - }, - runtimeConfig: { - public: { - theme: { - primaryColor: 'base_primary', - secondaryColor: 'base_secondary' - } - } - } -}) diff --git a/examples/advanced/config-extends/base/pages/foo.vue b/examples/advanced/config-extends/base/pages/foo.vue deleted file mode 100644 index b80089ec2b..0000000000 --- a/examples/advanced/config-extends/base/pages/foo.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/examples/advanced/config-extends/base/plugins/my-plugin.ts b/examples/advanced/config-extends/base/plugins/my-plugin.ts deleted file mode 100644 index df28887c51..0000000000 --- a/examples/advanced/config-extends/base/plugins/my-plugin.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default defineNuxtPlugin((/* nuxtApp */) => { - return { - provide: { - myPlugin: () => 'String generated from my auto-imported plugin!' - } - } -}) diff --git a/examples/advanced/config-extends/base/server/api/base.ts b/examples/advanced/config-extends/base/server/api/base.ts deleted file mode 100644 index b10fe1c156..0000000000 --- a/examples/advanced/config-extends/base/server/api/base.ts +++ /dev/null @@ -1 +0,0 @@ -export default () => 'base' diff --git a/examples/advanced/config-extends/base/utils/bar.ts b/examples/advanced/config-extends/base/utils/bar.ts deleted file mode 100644 index f2f1a83f05..0000000000 --- a/examples/advanced/config-extends/base/utils/bar.ts +++ /dev/null @@ -1 +0,0 @@ -export const getBar = () => 'bar' diff --git a/examples/advanced/config-extends/components/FancyButton.vue b/examples/advanced/config-extends/components/FancyButton.vue deleted file mode 100644 index 3733ed0439..0000000000 --- a/examples/advanced/config-extends/components/FancyButton.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/examples/advanced/config-extends/nuxt.config.ts b/examples/advanced/config-extends/nuxt.config.ts deleted file mode 100644 index cb82d04407..0000000000 --- a/examples/advanced/config-extends/nuxt.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -export default defineNuxtConfig({ - extends: [ - './ui', - './base' - ], - runtimeConfig: { - public: { - theme: { - primaryColor: 'user_primary' - } - } - }, - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/advanced/config-extends/package.json b/examples/advanced/config-extends/package.json deleted file mode 100644 index 689d81fc0c..0000000000 --- a/examples/advanced/config-extends/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-config-extends", - "private": true, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - }, - "scripts": { - "dev": "nuxi dev", - "build": "nuxi build", - "start": "nuxi preview" - } -} diff --git a/examples/advanced/config-extends/pages/index.vue b/examples/advanced/config-extends/pages/index.vue deleted file mode 100644 index 1c754df854..0000000000 --- a/examples/advanced/config-extends/pages/index.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/examples/advanced/config-extends/server/api/hello.ts b/examples/advanced/config-extends/server/api/hello.ts deleted file mode 100644 index 5c4739d2b2..0000000000 --- a/examples/advanced/config-extends/server/api/hello.ts +++ /dev/null @@ -1 +0,0 @@ -export default () => 'hello' diff --git a/examples/advanced/config-extends/ui/components/Button.vue b/examples/advanced/config-extends/ui/components/Button.vue deleted file mode 100644 index 8508b7cd00..0000000000 --- a/examples/advanced/config-extends/ui/components/Button.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/examples/advanced/config-extends/ui/nuxt.config.ts b/examples/advanced/config-extends/ui/nuxt.config.ts deleted file mode 100644 index fc7b9424c6..0000000000 --- a/examples/advanced/config-extends/ui/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - components: [ - { path: './components', prefix: 'UI' } - ] -}) diff --git a/examples/advanced/jsx/app.vue b/examples/advanced/jsx/app.vue deleted file mode 100644 index c1aa9e0f25..0000000000 --- a/examples/advanced/jsx/app.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/examples/advanced/jsx/components/MyComponent.tsx b/examples/advanced/jsx/components/MyComponent.tsx deleted file mode 100644 index bc6942f48f..0000000000 --- a/examples/advanced/jsx/components/MyComponent.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export default defineComponent({ - props: { - message: String - }, - render: (props) => { - return ( -
- { props.message } -
- ) - } -}) diff --git a/examples/advanced/jsx/nuxt.config.ts b/examples/advanced/jsx/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/advanced/jsx/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/advanced/jsx/package.json b/examples/advanced/jsx/package.json deleted file mode 100644 index 98a35c623f..0000000000 --- a/examples/advanced/jsx/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-jsx", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/advanced/module-extend-pages/layouts/default.vue b/examples/advanced/module-extend-pages/layouts/default.vue deleted file mode 100644 index b1a49db13c..0000000000 --- a/examples/advanced/module-extend-pages/layouts/default.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/advanced/module-extend-pages/modules/pages/index.ts b/examples/advanced/module-extend-pages/modules/pages/index.ts deleted file mode 100644 index 1052078d0c..0000000000 --- a/examples/advanced/module-extend-pages/modules/pages/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineNuxtModule, extendPages } from '@nuxt/kit' -import { resolve } from 'pathe' - -export default defineNuxtModule({ - setup () { - extendPages((pages) => { - // Add /test page - pages.push({ - name: 'Test', - path: '/test', - file: resolve(__dirname, './pages/test.vue') - }) - }) - } -}) diff --git a/examples/advanced/module-extend-pages/modules/pages/pages/test.vue b/examples/advanced/module-extend-pages/modules/pages/pages/test.vue deleted file mode 100644 index 6d83a00dca..0000000000 --- a/examples/advanced/module-extend-pages/modules/pages/pages/test.vue +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/examples/advanced/module-extend-pages/nuxt.config.ts b/examples/advanced/module-extend-pages/nuxt.config.ts deleted file mode 100644 index b13e1285b1..0000000000 --- a/examples/advanced/module-extend-pages/nuxt.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '~/modules/pages', - '@nuxt/ui' - ] -}) diff --git a/examples/advanced/module-extend-pages/package.json b/examples/advanced/module-extend-pages/package.json deleted file mode 100644 index b8c76d2919..0000000000 --- a/examples/advanced/module-extend-pages/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-module-extend-pages", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/advanced/module-extend-pages/pages/index.vue b/examples/advanced/module-extend-pages/pages/index.vue deleted file mode 100644 index 709ee2486f..0000000000 --- a/examples/advanced/module-extend-pages/pages/index.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/examples/advanced/testing/app.vue b/examples/advanced/testing/app.vue deleted file mode 100644 index 19bf5ff27e..0000000000 --- a/examples/advanced/testing/app.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/examples/advanced/testing/nuxt.config.ts b/examples/advanced/testing/nuxt.config.ts deleted file mode 100644 index fc5628ebc3..0000000000 --- a/examples/advanced/testing/nuxt.config.ts +++ /dev/null @@ -1,2 +0,0 @@ -export default defineNuxtConfig({ -}) diff --git a/examples/advanced/testing/package.json b/examples/advanced/testing/package.json deleted file mode 100644 index bb138b0525..0000000000 --- a/examples/advanced/testing/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "example-testing", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview", - "test": "nuxi test" - }, - "devDependencies": { - "@nuxt/test-utils": "^3.0.0", - "nuxt": "^3.0.0", - "vitest": "latest" - } -} diff --git a/examples/advanced/testing/tests/basic.test.ts b/examples/advanced/testing/tests/basic.test.ts deleted file mode 100644 index 696e3ec940..0000000000 --- a/examples/advanced/testing/tests/basic.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { $fetch, isDev } from '@nuxt/test-utils' - -describe('example', () => { - it('Renders Hello Nuxt', async () => { - expect(await $fetch('/')).toMatch('Hello Nuxt!') - }) - - if (isDev()) { - it('[dev] ensure vite client script is added', async () => { - expect(await $fetch('/')).toMatch('/_nuxt/@vite/client"') - }) - } -}) diff --git a/examples/advanced/testing/tsconfig.json b/examples/advanced/testing/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/advanced/testing/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/app-config/app.config.ts b/examples/app-config/app.config.ts deleted file mode 100644 index ed5df7eb16..0000000000 --- a/examples/app-config/app.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default defineAppConfig({ - title: 'Hello App Config', - description: - 'This is some content coming from app.config.ts that support HMR, try to update it and see it in action.', - showButton: false -}) diff --git a/examples/app-config/app.vue b/examples/app-config/app.vue deleted file mode 100644 index 6bc63d859b..0000000000 --- a/examples/app-config/app.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/examples/app-config/nuxt.config.ts b/examples/app-config/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/app-config/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/app-config/package.json b/examples/app-config/package.json deleted file mode 100644 index 46d0eed72c..0000000000 --- a/examples/app-config/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-app-config", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/app-config/tsconfig.json b/examples/app-config/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/app-config/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/app/error-handling/app.vue b/examples/app/error-handling/app.vue deleted file mode 100644 index bbd937d8de..0000000000 --- a/examples/app/error-handling/app.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/examples/app/error-handling/components/FaultyComponent.vue b/examples/app/error-handling/components/FaultyComponent.vue deleted file mode 100644 index bca16e1f46..0000000000 --- a/examples/app/error-handling/components/FaultyComponent.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/examples/app/error-handling/components/ThrowError.vue b/examples/app/error-handling/components/ThrowError.vue deleted file mode 100644 index 2027018d70..0000000000 --- a/examples/app/error-handling/components/ThrowError.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/examples/app/error-handling/error.vue b/examples/app/error-handling/error.vue deleted file mode 100644 index 5a58812ce5..0000000000 --- a/examples/app/error-handling/error.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/examples/app/error-handling/middleware/error.global.ts b/examples/app/error-handling/middleware/error.global.ts deleted file mode 100644 index 4d3927f4e6..0000000000 --- a/examples/app/error-handling/middleware/error.global.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtRouteMiddleware((to) => { - if ('middleware' in to.query) { - return showError('error in middleware') - } -}) diff --git a/examples/app/error-handling/nuxt.config.ts b/examples/app/error-handling/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/app/error-handling/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/app/error-handling/package.json b/examples/app/error-handling/package.json deleted file mode 100644 index 6d67806679..0000000000 --- a/examples/app/error-handling/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-error-handling", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/app/error-handling/pages/other.vue b/examples/app/error-handling/pages/other.vue deleted file mode 100644 index 95aef348c6..0000000000 --- a/examples/app/error-handling/pages/other.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/examples/app/error-handling/plugins/error.ts b/examples/app/error-handling/plugins/error.ts deleted file mode 100644 index b06a954765..0000000000 --- a/examples/app/error-handling/plugins/error.ts +++ /dev/null @@ -1,20 +0,0 @@ -export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.hook('vue:error', (..._args) => { - console.log('vue:error') - // if (process.client) { - // console.log(..._args) - // } - }) - nuxtApp.hook('app:error', (..._args) => { - console.log('app:error') - // if (process.client) { - // console.log(..._args) - // } - }) - nuxtApp.vueApp.config.errorHandler = (..._args) => { - console.log('global error handler') - // if (process.client) { - // console.log(..._args) - // } - } -}) diff --git a/examples/app/error-handling/server/middleware/error.ts b/examples/app/error-handling/server/middleware/error.ts deleted file mode 100644 index 50b5576ef9..0000000000 --- a/examples/app/error-handling/server/middleware/error.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineEventHandler, getQuery } from 'h3' - -export default defineEventHandler((event) => { - if ('api' in getQuery(event)) { - throw new Error('Server middleware error') - } -}) diff --git a/examples/app/error-handling/tsconfig.json b/examples/app/error-handling/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/app/error-handling/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/app/plugins/app.vue b/examples/app/plugins/app.vue deleted file mode 100644 index 37142ca1c1..0000000000 --- a/examples/app/plugins/app.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/app/plugins/nuxt.config.ts b/examples/app/plugins/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/app/plugins/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/app/plugins/package.json b/examples/app/plugins/package.json deleted file mode 100644 index 0b8a356b64..0000000000 --- a/examples/app/plugins/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-plugins", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/app/plugins/plugins/my-plugin.ts b/examples/app/plugins/plugins/my-plugin.ts deleted file mode 100644 index df28887c51..0000000000 --- a/examples/app/plugins/plugins/my-plugin.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default defineNuxtPlugin((/* nuxtApp */) => { - return { - provide: { - myPlugin: () => 'String generated from my auto-imported plugin!' - } - } -}) diff --git a/examples/app/plugins/tsconfig.json b/examples/app/plugins/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/app/plugins/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/app/teleport/app.vue b/examples/app/teleport/app.vue deleted file mode 100644 index bc3f9b9966..0000000000 --- a/examples/app/teleport/app.vue +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/examples/app/teleport/components/MyModal.vue b/examples/app/teleport/components/MyModal.vue deleted file mode 100644 index 804c2b18f0..0000000000 --- a/examples/app/teleport/components/MyModal.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - diff --git a/examples/app/teleport/nuxt.config.ts b/examples/app/teleport/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/app/teleport/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/app/teleport/package.json b/examples/app/teleport/package.json deleted file mode 100644 index 6bff5420e3..0000000000 --- a/examples/app/teleport/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-teleport", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/app/teleport/tsconfig.json b/examples/app/teleport/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/app/teleport/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/auto-imports/components/app.vue b/examples/auto-imports/components/app.vue deleted file mode 100644 index fdbfbb8949..0000000000 --- a/examples/auto-imports/components/app.vue +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/examples/auto-imports/components/components/ClientAndServer.server.vue b/examples/auto-imports/components/components/ClientAndServer.server.vue deleted file mode 100644 index 8e1af3bf58..0000000000 --- a/examples/auto-imports/components/components/ClientAndServer.server.vue +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/examples/auto-imports/components/components/HelloWorld.vue b/examples/auto-imports/components/components/HelloWorld.vue deleted file mode 100644 index 92f324eb3e..0000000000 --- a/examples/auto-imports/components/components/HelloWorld.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/auto-imports/components/components/JustClient.client.vue b/examples/auto-imports/components/components/JustClient.client.vue deleted file mode 100644 index 68549aef07..0000000000 --- a/examples/auto-imports/components/components/JustClient.client.vue +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/examples/auto-imports/components/components/Nuxt3.vue b/examples/auto-imports/components/components/Nuxt3.vue deleted file mode 100644 index 0faa5c7efe..0000000000 --- a/examples/auto-imports/components/components/Nuxt3.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/auto-imports/components/components/ServerOnlyComponent.server.vue b/examples/auto-imports/components/components/ServerOnlyComponent.server.vue deleted file mode 100644 index 8aa6a2035d..0000000000 --- a/examples/auto-imports/components/components/ServerOnlyComponent.server.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/examples/auto-imports/components/components/parent-folder/Hello.vue b/examples/auto-imports/components/components/parent-folder/Hello.vue deleted file mode 100644 index 36614a01db..0000000000 --- a/examples/auto-imports/components/components/parent-folder/Hello.vue +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/examples/auto-imports/components/nuxt.config.ts b/examples/auto-imports/components/nuxt.config.ts deleted file mode 100644 index 340315d428..0000000000 --- a/examples/auto-imports/components/nuxt.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ], - components: { - dirs: [ - '~/components', - { - path: '~/other-components-folder', - extensions: ['vue'], - prefix: 'nuxt' - } - ] - } -}) diff --git a/examples/auto-imports/components/other-components-folder/with-prefix.vue b/examples/auto-imports/components/other-components-folder/with-prefix.vue deleted file mode 100644 index 43daa59fe1..0000000000 --- a/examples/auto-imports/components/other-components-folder/with-prefix.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/examples/auto-imports/components/package.json b/examples/auto-imports/components/package.json deleted file mode 100644 index 6fbd840489..0000000000 --- a/examples/auto-imports/components/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-components", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/auto-imports/components/tsconfig.json b/examples/auto-imports/components/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/auto-imports/components/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/auto-imports/composables/app.vue b/examples/auto-imports/composables/app.vue deleted file mode 100644 index 9ac9b2312f..0000000000 --- a/examples/auto-imports/composables/app.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/examples/auto-imports/composables/composables/export-star.ts b/examples/auto-imports/composables/composables/export-star.ts deleted file mode 100644 index e759d92d6b..0000000000 --- a/examples/auto-imports/composables/composables/export-star.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './nested/bar' diff --git a/examples/auto-imports/composables/composables/nested/bar.ts b/examples/auto-imports/composables/composables/nested/bar.ts deleted file mode 100644 index 0e9345c11e..0000000000 --- a/examples/auto-imports/composables/composables/nested/bar.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function useNestedBar () { - return 'bar' -} diff --git a/examples/auto-imports/composables/composables/use-foo.ts b/examples/auto-imports/composables/composables/use-foo.ts deleted file mode 100644 index 3550421ffe..0000000000 --- a/examples/auto-imports/composables/composables/use-foo.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useState } from '#app' - -export function useA () { - return 'a' -} - -function useB () { - return 'b' -} - -function _useC () { - return 'c' -} - -export const useD = () => { - return 'd' -} - -export { useB, _useC as useC } - -export default function () { - return useState('foo', () => 'bar') -} diff --git a/examples/auto-imports/composables/nuxt.config.ts b/examples/auto-imports/composables/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/auto-imports/composables/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/auto-imports/composables/package.json b/examples/auto-imports/composables/package.json deleted file mode 100644 index 4059ab55ff..0000000000 --- a/examples/auto-imports/composables/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-composables", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/composables/use-async-data/app.vue b/examples/composables/use-async-data/app.vue deleted file mode 100644 index 4987a0223d..0000000000 --- a/examples/composables/use-async-data/app.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/examples/composables/use-async-data/components/CounterExample.vue b/examples/composables/use-async-data/components/CounterExample.vue deleted file mode 100644 index 14ea1d01a5..0000000000 --- a/examples/composables/use-async-data/components/CounterExample.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/examples/composables/use-async-data/components/MountainExample.vue b/examples/composables/use-async-data/components/MountainExample.vue deleted file mode 100644 index 9632969f99..0000000000 --- a/examples/composables/use-async-data/components/MountainExample.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/examples/composables/use-async-data/nuxt.config.ts b/examples/composables/use-async-data/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/composables/use-async-data/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/composables/use-async-data/package.json b/examples/composables/use-async-data/package.json deleted file mode 100644 index d486a0393b..0000000000 --- a/examples/composables/use-async-data/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-use-async-data", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/composables/use-async-data/server/api/hello/[slug].ts b/examples/composables/use-async-data/server/api/hello/[slug].ts deleted file mode 100644 index 0cec73da7f..0000000000 --- a/examples/composables/use-async-data/server/api/hello/[slug].ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler(event => `Hello world (${event.node.req.url.substr(1)}) (Generated at ${new Date().toUTCString()})`) diff --git a/examples/composables/use-async-data/tsconfig.json b/examples/composables/use-async-data/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/composables/use-async-data/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/composables/use-cookie/app.vue b/examples/composables/use-cookie/app.vue deleted file mode 100644 index e941773be7..0000000000 --- a/examples/composables/use-cookie/app.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/examples/composables/use-cookie/nuxt.config.ts b/examples/composables/use-cookie/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/composables/use-cookie/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/composables/use-cookie/package.json b/examples/composables/use-cookie/package.json deleted file mode 100644 index 70d3b5b124..0000000000 --- a/examples/composables/use-cookie/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-use-cookie", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/composables/use-cookie/tsconfig.json b/examples/composables/use-cookie/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/composables/use-cookie/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/composables/use-fetch/app.vue b/examples/composables/use-fetch/app.vue deleted file mode 100644 index 9054d508f8..0000000000 --- a/examples/composables/use-fetch/app.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/examples/composables/use-fetch/nuxt.config.ts b/examples/composables/use-fetch/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/composables/use-fetch/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/composables/use-fetch/package.json b/examples/composables/use-fetch/package.json deleted file mode 100644 index 9815e9956d..0000000000 --- a/examples/composables/use-fetch/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-use-fetch", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/composables/use-fetch/server/api/[...hello].ts b/examples/composables/use-fetch/server/api/[...hello].ts deleted file mode 100644 index daf60d2f62..0000000000 --- a/examples/composables/use-fetch/server/api/[...hello].ts +++ /dev/null @@ -1,4 +0,0 @@ -export default defineEventHandler(event => ({ - path: '/api/' + event.context.params.hello, - query: getQuery(event) -})) diff --git a/examples/composables/use-fetch/tsconfig.json b/examples/composables/use-fetch/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/composables/use-fetch/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/composables/use-head/app.vue b/examples/composables/use-head/app.vue deleted file mode 100644 index 335ffbd8f9..0000000000 --- a/examples/composables/use-head/app.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/examples/composables/use-head/nuxt.config.ts b/examples/composables/use-head/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/composables/use-head/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/composables/use-head/package.json b/examples/composables/use-head/package.json deleted file mode 100644 index e64efc2dc4..0000000000 --- a/examples/composables/use-head/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-use-head", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "node .output/server" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/composables/use-head/tsconfig.json b/examples/composables/use-head/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/composables/use-head/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/composables/use-state/app.vue b/examples/composables/use-state/app.vue deleted file mode 100644 index 23650adaf2..0000000000 --- a/examples/composables/use-state/app.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/examples/composables/use-state/nuxt.config.ts b/examples/composables/use-state/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/composables/use-state/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/composables/use-state/package.json b/examples/composables/use-state/package.json deleted file mode 100644 index 0bf3812f99..0000000000 --- a/examples/composables/use-state/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-use-state", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/composables/use-state/tsconfig.json b/examples/composables/use-state/tsconfig.json deleted file mode 100644 index dfaf3c6d8f..0000000000 --- a/examples/composables/use-state/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json", -} diff --git a/examples/essentials/hello-world/app.vue b/examples/essentials/hello-world/app.vue deleted file mode 100644 index 19a4e3e12c..0000000000 --- a/examples/essentials/hello-world/app.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/examples/essentials/hello-world/nuxt.config.ts b/examples/essentials/hello-world/nuxt.config.ts deleted file mode 100644 index fc5628ebc3..0000000000 --- a/examples/essentials/hello-world/nuxt.config.ts +++ /dev/null @@ -1,2 +0,0 @@ -export default defineNuxtConfig({ -}) diff --git a/examples/essentials/hello-world/package.json b/examples/essentials/hello-world/package.json deleted file mode 100644 index 818247b527..0000000000 --- a/examples/essentials/hello-world/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "example-hello-world", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "nuxt": "^3.0.0" - } -} diff --git a/examples/essentials/hello-world/tsconfig.json b/examples/essentials/hello-world/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/essentials/hello-world/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/experimental/reactivity-transform/app.vue b/examples/experimental/reactivity-transform/app.vue deleted file mode 100644 index b89750f872..0000000000 --- a/examples/experimental/reactivity-transform/app.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/examples/experimental/reactivity-transform/components/label.vue b/examples/experimental/reactivity-transform/components/label.vue deleted file mode 100644 index 2148da8ab0..0000000000 --- a/examples/experimental/reactivity-transform/components/label.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/examples/experimental/reactivity-transform/nuxt.config.ts b/examples/experimental/reactivity-transform/nuxt.config.ts deleted file mode 100644 index a17b6a70ce..0000000000 --- a/examples/experimental/reactivity-transform/nuxt.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ], - experimental: { - reactivityTransform: true - } - // builder: 'webpack' -}) diff --git a/examples/experimental/reactivity-transform/package.json b/examples/experimental/reactivity-transform/package.json deleted file mode 100644 index babfbb76c7..0000000000 --- a/examples/experimental/reactivity-transform/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-reactivity-transform", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/experimental/reactivity-transform/tsconfig.json b/examples/experimental/reactivity-transform/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/experimental/reactivity-transform/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/experimental/vite-node/app.vue b/examples/experimental/vite-node/app.vue deleted file mode 100644 index f6c1c7031b..0000000000 --- a/examples/experimental/vite-node/app.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/examples/experimental/vite-node/nuxt.config.ts b/examples/experimental/vite-node/nuxt.config.ts deleted file mode 100644 index 5244ad314d..0000000000 --- a/examples/experimental/vite-node/nuxt.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ], - experimental: { - viteNode: true - } -}) diff --git a/examples/experimental/vite-node/package.json b/examples/experimental/vite-node/package.json deleted file mode 100644 index 4c5128725e..0000000000 --- a/examples/experimental/vite-node/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-vite-node", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/experimental/vite-node/tsconfig.json b/examples/experimental/vite-node/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/experimental/vite-node/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/experimental/wasm/app.vue b/examples/experimental/wasm/app.vue deleted file mode 100644 index 193b6581c4..0000000000 --- a/examples/experimental/wasm/app.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/examples/experimental/wasm/nuxt.config.ts b/examples/experimental/wasm/nuxt.config.ts deleted file mode 100644 index fc744fd0dd..0000000000 --- a/examples/experimental/wasm/nuxt.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default defineNuxtConfig({ - nitro: { - experimental: { - wasm: true - } - }, - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/experimental/wasm/package.json b/examples/experimental/wasm/package.json deleted file mode 100644 index 8235d294c1..0000000000 --- a/examples/experimental/wasm/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-wasm", - "private": true, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - }, - "scripts": { - "dev": "nuxi dev", - "build": "nuxi build", - "start": "nuxi preview" - } -} diff --git a/examples/experimental/wasm/server/api/sum.ts b/examples/experimental/wasm/server/api/sum.ts deleted file mode 100644 index fbfd774cd8..0000000000 --- a/examples/experimental/wasm/server/api/sum.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineLazyEventHandler } from 'h3' - -export default defineLazyEventHandler(async () => { - const { exports: { sum } } = await loadWasmInstance( - // @ts-ignore - () => import('~/server/wasm/sum.wasm') - ) - - return (event) => { - const { a = 0, b = 0 } = getQuery(event) - return { sum: sum(a, b) } - } -}) - -async function loadWasmInstance (importFn, imports = {}) { - const init = await importFn().then(m => m.default || m) - const { instance } = await init(imports) - return instance -} diff --git a/examples/experimental/wasm/server/wasm/sum.wasm b/examples/experimental/wasm/server/wasm/sum.wasm deleted file mode 100755 index 7267db6252..0000000000 Binary files a/examples/experimental/wasm/server/wasm/sum.wasm and /dev/null differ diff --git a/examples/experimental/wasm/server/wasm/sum.wat b/examples/experimental/wasm/server/wasm/sum.wat deleted file mode 100644 index 71d6e2859e..0000000000 --- a/examples/experimental/wasm/server/wasm/sum.wat +++ /dev/null @@ -1,7 +0,0 @@ -;; https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format -;; https://webassembly.github.io/wabt/demo/wat2wasm/ -(module - (func (export "sum") (param i32 i32) (result i32) - local.get 0 - local.get 1 - i32.add)) diff --git a/examples/experimental/wasm/tsconfig.json b/examples/experimental/wasm/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/experimental/wasm/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/other/locale/app.vue b/examples/other/locale/app.vue deleted file mode 100644 index d8239321aa..0000000000 --- a/examples/other/locale/app.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/examples/other/locale/composables/locale.ts b/examples/other/locale/composables/locale.ts deleted file mode 100644 index 2eb1f714ea..0000000000 --- a/examples/other/locale/composables/locale.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Ref } from 'vue' - -export const useLocale = () => useState('locale', () => useDefaultLocale().value) - -export const useDefaultLocale = (fallback = 'en-US') => { - const locale = ref(fallback) - if (process.server) { - // Learn more about the nuxtApp interface on https://nuxt.com/docs/guide/going-further/internals#the-nuxtapp-interface - const reqLocale = useRequestHeaders()['accept-language']?.split(',')[0] - if (reqLocale) { - locale.value = reqLocale - } - } else if (process.client) { - const navLang = navigator.language - if (navLang) { - locale.value = navLang - } - } - return locale -} - -export const useLocales = () => { - const locale = useLocale() - const locales = ref([ - 'en-US', - 'en-GB', - 'ko-KR', - 'zh-CN', - 'ar-EG', - 'fa-IR', - 'ja-JP-u-ca-japanese' - ]) - if (!locales.value.includes(locale.value)) { - locales.value.unshift(locale.value) - } - return locales -} - -// Using Intl.DateTimeFormat for language-sensitive date and time formatting -// Learn more: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat -export const useLocaleDate = (date: Ref | Date, locale = useLocale()) => { - return computed(() => new Intl.DateTimeFormat(locale.value, { dateStyle: 'full' }).format(unref(date))) -} diff --git a/examples/other/locale/nuxt.config.ts b/examples/other/locale/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/other/locale/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/other/locale/package.json b/examples/other/locale/package.json deleted file mode 100644 index 64c709405d..0000000000 --- a/examples/other/locale/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-locale", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/other/locale/tsconfig.json b/examples/other/locale/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/other/locale/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/routing/layouts/layouts/custom.vue b/examples/routing/layouts/layouts/custom.vue deleted file mode 100644 index 3ccb238316..0000000000 --- a/examples/routing/layouts/layouts/custom.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/examples/routing/layouts/layouts/default.vue b/examples/routing/layouts/layouts/default.vue deleted file mode 100644 index 6d83fd9b55..0000000000 --- a/examples/routing/layouts/layouts/default.vue +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/examples/routing/layouts/nuxt.config.ts b/examples/routing/layouts/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/routing/layouts/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/routing/layouts/package.json b/examples/routing/layouts/package.json deleted file mode 100644 index ba7b256878..0000000000 --- a/examples/routing/layouts/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-layouts", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/routing/layouts/pages/custom.vue b/examples/routing/layouts/pages/custom.vue deleted file mode 100644 index b8bb904e73..0000000000 --- a/examples/routing/layouts/pages/custom.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/examples/routing/layouts/pages/default.vue b/examples/routing/layouts/pages/default.vue deleted file mode 100644 index 175c83f07e..0000000000 --- a/examples/routing/layouts/pages/default.vue +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/examples/routing/layouts/pages/dynamic.vue b/examples/routing/layouts/pages/dynamic.vue deleted file mode 100644 index f07ea238b3..0000000000 --- a/examples/routing/layouts/pages/dynamic.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/examples/routing/layouts/pages/index.vue b/examples/routing/layouts/pages/index.vue deleted file mode 100644 index 8f38f2f09d..0000000000 --- a/examples/routing/layouts/pages/index.vue +++ /dev/null @@ -1,29 +0,0 @@ - diff --git a/examples/routing/layouts/pages/other.vue b/examples/routing/layouts/pages/other.vue deleted file mode 100644 index 00b14af93a..0000000000 --- a/examples/routing/layouts/pages/other.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/examples/routing/layouts/tsconfig.json b/examples/routing/layouts/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/routing/layouts/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/routing/middleware/app.vue b/examples/routing/middleware/app.vue deleted file mode 100644 index 6ecc44ead0..0000000000 --- a/examples/routing/middleware/app.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/examples/routing/middleware/middleware/always-run.global.ts b/examples/routing/middleware/middleware/always-run.global.ts deleted file mode 100644 index 7ec72f4acd..0000000000 --- a/examples/routing/middleware/middleware/always-run.global.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default defineNuxtRouteMiddleware(() => { - console.log('running global middleware') -}) diff --git a/examples/routing/middleware/middleware/redirect-me.ts b/examples/routing/middleware/middleware/redirect-me.ts deleted file mode 100644 index 03db33c244..0000000000 --- a/examples/routing/middleware/middleware/redirect-me.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default defineNuxtRouteMiddleware((to) => { - const { $config } = useNuxtApp() - if ($config) { - console.log('Accessed runtime config within middleware.') - } - console.log('Heading to', to.path, 'but I think we should go somewhere else...') - return '/secret' -}) diff --git a/examples/routing/middleware/nuxt.config.ts b/examples/routing/middleware/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/routing/middleware/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/routing/middleware/package.json b/examples/routing/middleware/package.json deleted file mode 100644 index a14a0c7bb4..0000000000 --- a/examples/routing/middleware/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-middleware", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/routing/middleware/pages/forbidden.vue b/examples/routing/middleware/pages/forbidden.vue deleted file mode 100644 index dcd621edad..0000000000 --- a/examples/routing/middleware/pages/forbidden.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/examples/routing/middleware/pages/index.vue b/examples/routing/middleware/pages/index.vue deleted file mode 100644 index a9168c0bdf..0000000000 --- a/examples/routing/middleware/pages/index.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/routing/middleware/pages/redirect.vue b/examples/routing/middleware/pages/redirect.vue deleted file mode 100644 index 045cf341bc..0000000000 --- a/examples/routing/middleware/pages/redirect.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/examples/routing/middleware/pages/secret.vue b/examples/routing/middleware/pages/secret.vue deleted file mode 100644 index 4f694d72fc..0000000000 --- a/examples/routing/middleware/pages/secret.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/examples/routing/middleware/plugins/add.ts b/examples/routing/middleware/plugins/add.ts deleted file mode 100644 index 9d9dc5ab1b..0000000000 --- a/examples/routing/middleware/plugins/add.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default defineNuxtPlugin(() => { - addRouteMiddleware('global-test', () => { - console.log('this global middleware was added in a plugin') - }, { global: true }) - - addRouteMiddleware('named-test', () => { - console.log('this named middleware was added in a plugin') - }) -}) diff --git a/examples/routing/middleware/tsconfig.json b/examples/routing/middleware/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/routing/middleware/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/routing/nuxt-link/app.vue b/examples/routing/nuxt-link/app.vue deleted file mode 100644 index 66f1679a6c..0000000000 --- a/examples/routing/nuxt-link/app.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/examples/routing/nuxt-link/components/MyNuxtLink.ts b/examples/routing/nuxt-link/components/MyNuxtLink.ts deleted file mode 100644 index 3ae8613b2c..0000000000 --- a/examples/routing/nuxt-link/components/MyNuxtLink.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default defineNuxtLink({ - componentName: 'MyNuxtLink', - externalRelAttribute: '', - activeClass: 'active', - exactActiveClass: 'exact-active' -}) diff --git a/examples/routing/nuxt-link/nuxt.config.ts b/examples/routing/nuxt-link/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/routing/nuxt-link/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/routing/nuxt-link/package.json b/examples/routing/nuxt-link/package.json deleted file mode 100644 index 357411c5f5..0000000000 --- a/examples/routing/nuxt-link/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-nuxt-link", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/routing/nuxt-link/pages/about.vue b/examples/routing/nuxt-link/pages/about.vue deleted file mode 100644 index d3c82f35e2..0000000000 --- a/examples/routing/nuxt-link/pages/about.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/routing/nuxt-link/pages/index.vue b/examples/routing/nuxt-link/pages/index.vue deleted file mode 100644 index 230bd2698a..0000000000 --- a/examples/routing/nuxt-link/pages/index.vue +++ /dev/null @@ -1,34 +0,0 @@ - diff --git a/examples/routing/nuxt-link/tsconfig.json b/examples/routing/nuxt-link/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/routing/nuxt-link/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/routing/pages/app.vue b/examples/routing/pages/app.vue deleted file mode 100644 index 8ffd82ba5d..0000000000 --- a/examples/routing/pages/app.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/examples/routing/pages/nuxt.config.ts b/examples/routing/pages/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/routing/pages/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/routing/pages/package.json b/examples/routing/pages/package.json deleted file mode 100644 index 54a68103e7..0000000000 --- a/examples/routing/pages/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-pages", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/routing/pages/pages/catchall/[...id].vue b/examples/routing/pages/pages/catchall/[...id].vue deleted file mode 100644 index c4e5f199e6..0000000000 --- a/examples/routing/pages/pages/catchall/[...id].vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/routing/pages/pages/index.vue b/examples/routing/pages/pages/index.vue deleted file mode 100644 index a9168c0bdf..0000000000 --- a/examples/routing/pages/pages/index.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/routing/pages/pages/parent/index.vue b/examples/routing/pages/pages/parent/index.vue deleted file mode 100644 index fe020db9bc..0000000000 --- a/examples/routing/pages/pages/parent/index.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/examples/routing/pages/pages/parent/reload-[id].vue b/examples/routing/pages/pages/parent/reload-[id].vue deleted file mode 100644 index baa06935fa..0000000000 --- a/examples/routing/pages/pages/parent/reload-[id].vue +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/examples/routing/pages/pages/parent/static-[id].vue b/examples/routing/pages/pages/parent/static-[id].vue deleted file mode 100644 index eaa7085ef9..0000000000 --- a/examples/routing/pages/pages/parent/static-[id].vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/examples/routing/pages/tsconfig.json b/examples/routing/pages/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/routing/pages/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/routing/universal-router/app.vue b/examples/routing/universal-router/app.vue deleted file mode 100644 index eb7103262a..0000000000 --- a/examples/routing/universal-router/app.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/examples/routing/universal-router/middleware/always-run.global.ts b/examples/routing/universal-router/middleware/always-run.global.ts deleted file mode 100644 index 7ec72f4acd..0000000000 --- a/examples/routing/universal-router/middleware/always-run.global.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default defineNuxtRouteMiddleware(() => { - console.log('running global middleware') -}) diff --git a/examples/routing/universal-router/nuxt.config.ts b/examples/routing/universal-router/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/routing/universal-router/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/routing/universal-router/package.json b/examples/routing/universal-router/package.json deleted file mode 100644 index 127c3beb42..0000000000 --- a/examples/routing/universal-router/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-universal-router", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/routing/universal-router/plugins/add.ts b/examples/routing/universal-router/plugins/add.ts deleted file mode 100644 index 6677989d22..0000000000 --- a/examples/routing/universal-router/plugins/add.ts +++ /dev/null @@ -1,33 +0,0 @@ -export default defineNuxtPlugin(() => { - const timer = useState('timer', () => 0) - - if (process.client) { - addRouteMiddleware(async () => { - console.log('Starting timer...') - timer.value = 5 - do { - await new Promise(resolve => setTimeout(resolve, 100)) - timer.value-- - } while (timer.value) - console.log('...and navigating') - }) - } - - addRouteMiddleware((to) => { - if (to.path === '/forbidden') { - return false - } - }) - - addRouteMiddleware((to) => { - const { $config } = useNuxtApp() - if ($config) { - console.log('Accessed runtime config within middleware.') - } - - if (to.path !== '/redirect') { return } - - console.log('Heading to', to.path, 'but I think we should go somewhere else...') - return '/secret' - }) -}) diff --git a/examples/routing/universal-router/tsconfig.json b/examples/routing/universal-router/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/routing/universal-router/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/examples/server/routes/app.vue b/examples/server/routes/app.vue deleted file mode 100644 index c5537ec950..0000000000 --- a/examples/server/routes/app.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/examples/server/routes/nuxt.config.ts b/examples/server/routes/nuxt.config.ts deleted file mode 100644 index 9f9029e2b1..0000000000 --- a/examples/server/routes/nuxt.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default defineNuxtConfig({ - modules: [ - '@nuxt/ui' - ] -}) diff --git a/examples/server/routes/package.json b/examples/server/routes/package.json deleted file mode 100644 index ed0cd6c92e..0000000000 --- a/examples/server/routes/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "example-server-routes", - "private": true, - "scripts": { - "build": "nuxi build", - "dev": "nuxi dev", - "start": "nuxi preview" - }, - "devDependencies": { - "@nuxt/ui": "^0.3.3", - "nuxt": "^3.0.0" - } -} diff --git a/examples/server/routes/server/api/mountain.js b/examples/server/routes/server/api/mountain.js deleted file mode 100644 index a3e85f2427..0000000000 --- a/examples/server/routes/server/api/mountain.js +++ /dev/null @@ -1,17 +0,0 @@ -export default defineEventHandler(() => { - return { - title: 'Mount Everest', - description: "Mount Everest is Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas. The China–Nepal border runs across its summit point", - height: '8,848 m', - countries: [ - 'China', - 'Nepal' - ], - continent: 'Asia', - image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Everest_kalapatthar.jpg/600px-Everest_kalapatthar.jpg', - dir: '/mountains', - path: '/mountains/mount-everest', - slug: 'mount-everest', - updatedAt: '2020-12-11T15:40:35.000Z' - } -}) diff --git a/examples/server/routes/tsconfig.json b/examples/server/routes/tsconfig.json deleted file mode 100644 index 4b34df1571..0000000000 --- a/examples/server/routes/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./.nuxt/tsconfig.json" -} diff --git a/knip.json b/knip.json new file mode 100644 index 0000000000..96a59fca9f --- /dev/null +++ b/knip.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://unpkg.com/knip@2/schema.json", + "workspaces": { + ".": { + "entry": [ + "scripts/*" + ] + }, + "packages/*": { + "entry": [ + "src/index.ts", + "src/runtime/**/*.ts" + ] + }, + "packages/test-utils": { + "entry": [ + "src/experimental.ts", + "src/index.ts" + ] + }, + "packages/nuxi": { + "entry": [ + "src/index.ts", + "src/commands/*.ts" + ] + }, + "packages/nuxt": { + "entry": [ + "src/app/**/*.ts", + "src/app/*.ts", + "src/*/runtime/**/*.ts", + "src/core/templates.ts", + "src/index.ts" + ] + } + } +} diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000000..22f73e611a --- /dev/null +++ b/lychee.toml @@ -0,0 +1,15 @@ +# Cache the results of Lychee if ran locally in order to minimise the chance of rate limiting +cache = true +# Ignore all private link (such as localhost) to avoid errors +exclude_all_private = true +# HTTP status code: 429 (Too Many Requests) will also be treated as a valid link if Lychee gets rate limited +accept = [200, 429] +# retry +max_retries = 6 +# Explicitly exclude some URLs +exclude = [ + "https://twitter.nuxt.dev/", + "https://github.com/nuxt/nuxt.com", + "https://github.com/nuxt/translations/discussions/4", + '(https?:\/\/github\.com\/)(.*\/)(generate)' +] diff --git a/package.json b/package.json index 79f02b87ab..b3d525c507 100644 --- a/package.json +++ b/package.json @@ -5,26 +5,26 @@ "license": "MIT", "type": "module", "scripts": { - "build": "FORCE_COLOR=1 pnpm --filter './packages/**' prepack", + "build": "pnpm --filter './packages/**' prepack", "build:stub": "pnpm --filter './packages/**' prepack --stub", - "cleanup": "rimraf 'packages/**/node_modules' 'examples/**/node_modules' 'docs/node_modules' 'playground/node_modules' 'node_modules'", + "cleanup": "rimraf 'packages/**/node_modules' 'playground/node_modules' 'node_modules'", "dev": "pnpm play", - "example": "./scripts/example.sh dev", - "example:build": "./scripts/example.sh build", "lint": "eslint --ext .vue,.ts,.js,.mjs .", "lint:fix": "eslint --ext .vue,.ts,.js,.mjs . --fix", - "lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md'", - "lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' --fix", - "nuxi": "JITI_ESM_RESOLVE=1 nuxi", - "nuxt": "JITI_ESM_RESOLVE=1 nuxi", - "play": "pnpm nuxi dev playground", - "play:build": "pnpm nuxi build playground", - "play:preview": "pnpm nuxi preview playground", - "test:fixtures": "pnpm nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/runtime-compiler && JITI_ESM_RESOLVE=1 vitest run --dir test", + "lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md", + "lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' *.md --fix", + "lint:knip": "pnpx knip", + "docs:dev": "nuxi dev .website", + "play": "nuxi dev playground", + "play:build": "nuxi build playground", + "play:preview": "nuxi preview playground", + "test": "pnpm test:fixtures && pnpm test:fixtures:dev && pnpm test:fixtures:webpack && pnpm test:unit && pnpm test:runtime && pnpm test:types && pnpm typecheck", + "test:fixtures": "nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/runtime-compiler && vitest run --dir test", "test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures", "test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures", - "test:types": "pnpm nuxi prepare test/fixtures/basic && cd test/fixtures/basic && npx vue-tsc --noEmit", - "test:unit": "JITI_ESM_RESOLVE=1 vitest run --dir packages", + "test:runtime": "vitest -c vitest.nuxt.config.ts", + "test:types": "pnpm --filter './test/fixtures/**' test:types", + "test:unit": "vitest run --dir packages", "typecheck": "tsc --noEmit" }, "resolutions": { @@ -33,53 +33,55 @@ "@nuxt/test-utils": "workspace:*", "@nuxt/vite-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*", - "nuxi": "workspace:*", "nuxt": "workspace:*", - "nuxt3": "workspace:nuxt@*", - "unbuild": "^1.2.0", - "vite": "^4.2.1", - "vue": "3.2.47", - "magic-string": "^0.30.0" + "vite": "4.4.11", + "vue": "3.3.4", + "magic-string": "^0.30.4" }, "devDependencies": { - "@actions/core": "^1.10.0", - "@nuxt/kit": "workspace:*", - "@nuxt/schema": "workspace:*", "@nuxt/test-utils": "workspace:*", - "@nuxt/vite-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*", - "@nuxtjs/eslint-config-typescript": "^12.0.0", - "@types/crawler": "^1.2.2", - "@types/node": "^18.15.11", - "@types/rimraf": "^3.0.2", - "@types/semver": "^7.3.13", - "@unocss/reset": "^0.50.8", - "case-police": "^0.5.14", - "changelogen": "^0.5.2", - "crawler": "^1.4.0", - "eslint": "^8.38.0", - "eslint-plugin-jsdoc": "^40.1.1", - "execa": "^7.1.1", - "expect-type": "^0.15.0", - "globby": "^13.1.3", - "jiti": "^1.18.2", + "@nuxtjs/eslint-config-typescript": "12.1.0", + "@types/fs-extra": "11.0.2", + "@types/node": "18.18.4", + "@types/semver": "7.5.3", + "case-police": "0.6.1", + "changelogen": "0.5.5", + "consola": "3.2.3", + "devalue": "4.3.2", + "eslint": "8.51.0", + "eslint-plugin-import": "2.28.1", + "eslint-plugin-jsdoc": "46.8.2", + "eslint-plugin-no-only-tests": "3.1.0", + "execa": "7.2.0", + "fs-extra": "11.1.1", + "globby": "13.2.2", + "h3": "1.8.2", + "happy-dom": "12.9.1", + "jiti": "1.20.0", "markdownlint-cli": "^0.33.0", - "nuxi": "workspace:*", + "nitropack": "2.6.3", + "nuxi": "3.9.0", "nuxt": "workspace:*", - "ofetch": "^1.0.1", - "pathe": "^1.1.0", - "rimraf": "^4.4.1", - "semver": "^7.3.8", - "std-env": "^3.3.2", - "typescript": "^5.0.4", - "ufo": "^1.1.1", - "unbuild": "^1.2.0", - "vite": "^4.2.1", - "vitest": "^0.29.8", - "vue-tsc": "^1.2.0" + "nuxt-vitest": "0.11.0", + "ofetch": "1.3.3", + "pathe": "1.1.1", + "playwright-core": "1.38.1", + "rimraf": "5.0.5", + "semver": "7.5.4", + "std-env": "3.4.3", + "typescript": "5.2.2", + "ufo": "1.3.1", + "vite": "4.4.11", + "vitest": "0.33.0", + "vitest-environment-nuxt": "0.11.0", + "vue": "3.3.4", + "vue-eslint-parser": "9.3.2", + "vue-router": "4.2.5", + "vue-tsc": "1.8.19" }, - "packageManager": "pnpm@8.1.1", + "packageManager": "pnpm@8.9.0", "engines": { - "node": "^14.18.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node": "^14.18.0 || >=16.10.0" } } diff --git a/packages/kit/build.config.ts b/packages/kit/build.config.ts index 700d94cd5b..a2f84767f8 100644 --- a/packages/kit/build.config.ts +++ b/packages/kit/build.config.ts @@ -11,5 +11,6 @@ export default defineBuildConfig({ 'webpack', 'vite', 'h3' - ] + ], + failOnWarn: false }) diff --git a/packages/kit/index.d.ts b/packages/kit/index.d.ts new file mode 100644 index 0000000000..9b281d5616 --- /dev/null +++ b/packages/kit/index.d.ts @@ -0,0 +1,6 @@ +declare global { + var __NUXT_PREPATHS__: string[] | string | undefined + var __NUXT_PATHS__: string[] | string | undefined +} + +export {} diff --git a/packages/kit/package.json b/packages/kit/package.json index 866707f141..99cba6f772 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -1,6 +1,6 @@ { "name": "@nuxt/kit", - "version": "3.3.3", + "version": "3.7.4", "repository": "nuxt/nuxt", "license": "MIT", "type": "module", @@ -20,31 +20,37 @@ "prepack": "unbuild" }, "dependencies": { - "@nuxt/schema": "workspace:../schema", - "c12": "^1.2.0", - "consola": "^3.0.0-3", + "@nuxt/schema": "workspace:*", + "c12": "^1.4.2", + "consola": "^3.2.3", "defu": "^6.1.2", - "globby": "^13.1.3", + "globby": "^13.2.2", "hash-sum": "^2.0.0", "ignore": "^5.2.4", - "jiti": "^1.18.2", + "jiti": "^1.20.0", "knitwork": "^1.0.0", - "lodash.template": "^4.5.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0", - "pkg-types": "^1.0.2", + "mlly": "^1.4.2", + "pathe": "^1.1.1", + "pkg-types": "^1.0.3", "scule": "^1.0.0", - "semver": "^7.3.8", - "unctx": "^2.1.2", - "unimport": "^3.0.6", - "untyped": "^1.3.2" + "semver": "^7.5.4", + "ufo": "^1.3.1", + "unctx": "^2.3.1", + "unimport": "^3.4.0", + "untyped": "^1.4.0" }, "devDependencies": { - "@types/lodash.template": "^4.5.1", - "@types/semver": "^7.3.13", - "unbuild": "latest" + "@types/hash-sum": "1.0.0", + "@types/lodash-es": "4.17.9", + "@types/semver": "7.5.3", + "lodash-es": "4.17.21", + "nitropack": "2.6.3", + "unbuild": "latest", + "vite": "4.4.11", + "vitest": "0.33.0", + "webpack": "5.88.2" }, "engines": { - "node": "^14.18.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node": "^14.18.0 || >=16.10.0" } } diff --git a/packages/kit/src/build.ts b/packages/kit/src/build.ts index e5678da2e6..14fce6347d 100644 --- a/packages/kit/src/build.ts +++ b/packages/kit/src/build.ts @@ -5,32 +5,31 @@ import { useNuxt } from './context' export interface ExtendConfigOptions { /** * Install plugin on dev - * * @default true */ dev?: boolean /** * Install plugin on build - * * @default true */ build?: boolean /** * Install plugin on server side - * * @default true */ server?: boolean /** * Install plugin on client side - * * @default true */ client?: boolean + /** + * Prepends the plugin to the array with `unshift()` instead of `push()`. + */ + prepend?: boolean } -export interface ExtendWebpackConfigOptions extends ExtendConfigOptions { -} +export interface ExtendWebpackConfigOptions extends ExtendConfigOptions {} export interface ExtendViteConfigOptions extends ExtendConfigOptions {} @@ -103,13 +102,16 @@ export function extendViteConfig ( /** * Append webpack plugin to the config. */ -export function addWebpackPlugin (plugin: WebpackPluginInstance | WebpackPluginInstance[], options?: ExtendWebpackConfigOptions) { +export function addWebpackPlugin (pluginOrGetter: WebpackPluginInstance | WebpackPluginInstance[] | (() => WebpackPluginInstance | WebpackPluginInstance[]), options?: ExtendWebpackConfigOptions) { extendWebpackConfig((config) => { + const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push' + const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter + config.plugins = config.plugins || [] if (Array.isArray(plugin)) { - config.plugins.push(...plugin) + config.plugins[method](...plugin) } else { - config.plugins.push(plugin) + config.plugins[method](plugin) } }, options) } @@ -117,13 +119,31 @@ export function addWebpackPlugin (plugin: WebpackPluginInstance | WebpackPluginI /** * Append Vite plugin to the config. */ -export function addVitePlugin (plugin: VitePlugin | VitePlugin[], options?: ExtendViteConfigOptions) { +export function addVitePlugin (pluginOrGetter: VitePlugin | VitePlugin[] | (() => VitePlugin | VitePlugin[]), options?: ExtendViteConfigOptions) { extendViteConfig((config) => { + const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push' + const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter + config.plugins = config.plugins || [] if (Array.isArray(plugin)) { - config.plugins.push(...plugin) + config.plugins[method](...plugin) } else { - config.plugins.push(plugin) + config.plugins[method](plugin) } }, options) } + +interface AddBuildPluginFactory { + vite?: () => VitePlugin | VitePlugin[] + webpack?: () => WebpackPluginInstance | WebpackPluginInstance[] +} + +export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?: ExtendConfigOptions) { + if (pluginFactory.vite) { + addVitePlugin(pluginFactory.vite, options) + } + + if (pluginFactory.webpack) { + addWebpackPlugin(pluginFactory.webpack, options) + } +} diff --git a/packages/kit/src/compatibility.ts b/packages/kit/src/compatibility.ts index 98462f9db9..431e3453d1 100644 --- a/packages/kit/src/compatibility.ts +++ b/packages/kit/src/compatibility.ts @@ -2,6 +2,10 @@ import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381 import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema' import { useNuxt } from './context' +export function normalizeSemanticVersion (version: string) { + return version.replace(/-[0-9]+\.[0-9a-f]+/, '') // Remove edge prefix +} + /** * Check version constraints and return incompatibility issues as an array */ @@ -11,9 +15,7 @@ export async function checkNuxtCompatibility (constraints: NuxtCompatibility, nu // Nuxt version check if (constraints.nuxt) { const nuxtVersion = getNuxtVersion(nuxt) - const nuxtSemanticVersion = nuxtVersion - .replace(/-[0-9]+\.[0-9a-f]+/, '') // Remove edge prefix - if (!satisfies(nuxtSemanticVersion, constraints.nuxt, { includePrerelease: true })) { + if (!satisfies(normalizeSemanticVersion(nuxtVersion), constraints.nuxt, { includePrerelease: true })) { issues.push({ name: 'nuxt', message: `Nuxt version \`${constraints.nuxt}\` is required but currently using \`${nuxtVersion}\`` diff --git a/packages/kit/src/components.ts b/packages/kit/src/components.ts index 9200c91c3c..e344b7a95a 100644 --- a/packages/kit/src/components.ts +++ b/packages/kit/src/components.ts @@ -2,6 +2,7 @@ import { kebabCase, pascalCase } from 'scule' import type { Component, ComponentsDir } from '@nuxt/schema' import { useNuxt } from './context' import { assertNuxtCompatibility } from './compatibility' +import { logger } from './logger' /** * Register a directory to be scanned for components and imported only when used. @@ -12,6 +13,7 @@ export async function addComponentsDir (dir: ComponentsDir) { const nuxt = useNuxt() await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt) nuxt.options.components = nuxt.options.components || [] + dir.priority ||= 0 nuxt.hook('components:dirs', (dirs) => { dirs.push(dir) }) } @@ -56,7 +58,7 @@ export async function addComponent (opts: AddComponentOptions) { // but we warn if they are equal. if (newPriority === existingPriority) { const name = existingComponent.pascalName || existingComponent.kebabName - console.warn(`Overriding ${name} component. You can specify a \`priority\` option when calling \`addComponent\` to avoid this warning.`) + logger.warn(`Overriding ${name} component. You can specify a \`priority\` option when calling \`addComponent\` to avoid this warning.`) } Object.assign(existingComponent, component) } else { diff --git a/packages/kit/src/context.ts b/packages/kit/src/context.ts index cac5fbae6e..d132b81c6c 100644 --- a/packages/kit/src/context.ts +++ b/packages/kit/src/context.ts @@ -10,7 +10,6 @@ export const nuxtCtx = getContext('nuxt') * Get access to Nuxt instance. * * Throws an error if Nuxt instance is unavailable. - * * @example * ```js * const nuxt = useNuxt() @@ -28,7 +27,6 @@ export function useNuxt (): Nuxt { * Get access to Nuxt instance. * * Returns null if Nuxt instance is unavailable. - * * @example * ```js * const nuxt = tryUseNuxt() diff --git a/packages/kit/src/ignore.ts b/packages/kit/src/ignore.ts index 825c3dbb6c..8c42d3a051 100644 --- a/packages/kit/src/ignore.ts +++ b/packages/kit/src/ignore.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from 'node:fs' import ignore from 'ignore' -import { join, relative } from 'pathe' +import { join, relative, resolve } from 'pathe' import { tryUseNuxt } from './context' /** @@ -16,14 +16,7 @@ export function isIgnored (pathname: string): boolean { if (!nuxt._ignore) { nuxt._ignore = ignore(nuxt.options.ignoreOptions) - const resolvedIgnore = nuxt.options.ignore.flatMap(s => resolveGroupSyntax(s)) - - nuxt._ignore.add(resolvedIgnore) - - const nuxtignoreFile = join(nuxt.options.rootDir, '.nuxtignore') - if (existsSync(nuxtignoreFile)) { - nuxt._ignore.add(readFileSync(nuxtignoreFile, 'utf-8')) - } + nuxt._ignore.add(resolveIgnorePatterns()) } const cwds = nuxt.options._layers?.map(layer => layer.cwd).sort((a, b) => b.length - a.length) @@ -35,11 +28,35 @@ export function isIgnored (pathname: string): boolean { return !!(relativePath && nuxt._ignore.ignores(relativePath)) } +export function resolveIgnorePatterns (relativePath?: string): string[] { + const nuxt = tryUseNuxt() + + // Happens with CLI reloads + if (!nuxt) { + return [] + } + + if (!nuxt._ignorePatterns) { + nuxt._ignorePatterns = nuxt.options.ignore.flatMap(s => resolveGroupSyntax(s)) + + const nuxtignoreFile = join(nuxt.options.rootDir, '.nuxtignore') + if (existsSync(nuxtignoreFile)) { + const contents = readFileSync(nuxtignoreFile, 'utf-8') + nuxt._ignorePatterns.push(...contents.trim().split(/\r?\n/)) + } + } + + if (relativePath) { + return nuxt._ignorePatterns.map(p => p.startsWith('*') || p.startsWith('!*') ? p : relative(relativePath, resolve(nuxt.options.rootDir, p))) + } + + return nuxt._ignorePatterns +} + /** * This function turns string containing groups '**\/*.{spec,test}.{js,ts}' into an array of strings. * For example will '**\/*.{spec,test}.{js,ts}' be resolved to: * ['**\/*.spec.js', '**\/*.spec.ts', '**\/*.test.js', '**\/*.test.ts'] - * * @param group string containing the group syntax * @returns {string[]} array of strings without the group syntax */ diff --git a/packages/kit/src/imports.ts b/packages/kit/src/imports.ts index cd532351cf..bf5a35759e 100644 --- a/packages/kit/src/imports.ts +++ b/packages/kit/src/imports.ts @@ -11,12 +11,12 @@ export function addImports (imports: Import | Import[]) { }) } -export function addImportsDir (dirs: string | string[]) { +export function addImportsDir (dirs: string | string[], opts: { prepend?: boolean } = {}) { assertNuxtCompatibility({ bridge: true }) useNuxt().hook('imports:dirs', (_dirs: string[]) => { for (const dir of (Array.isArray(dirs) ? dirs : [dirs])) { - _dirs.push(dir) + _dirs[opts.prepend ? 'unshift' : 'push'](dir) } }) } diff --git a/packages/kit/src/index.ts b/packages/kit/src/index.ts index 71343978ae..da2fe614ad 100644 --- a/packages/kit/src/index.ts +++ b/packages/kit/src/index.ts @@ -1,6 +1,7 @@ // Module export * from './module/define' export * from './module/install' +export * from './module/compatibility' // Loader export * from './loader/config' @@ -13,7 +14,7 @@ export * from './build' export * from './compatibility' export * from './components' export * from './context' -export { isIgnored } from './ignore' +export { isIgnored, resolveIgnorePatterns } from './ignore' export * from './layout' export * from './pages' export * from './plugin' diff --git a/packages/kit/src/internal/cjs.ts b/packages/kit/src/internal/cjs.ts index 23a28a6682..d15f4cf574 100644 --- a/packages/kit/src/internal/cjs.ts +++ b/packages/kit/src/internal/cjs.ts @@ -23,13 +23,13 @@ export interface RequireModuleOptions extends ResolveModuleOptions { } /** @deprecated Do not use CJS utils */ -export function isNodeModules (id: string) { +function isNodeModules (id: string) { // TODO: Follow symlinks return /[/\\]node_modules[/\\]/.test(id) } /** @deprecated Do not use CJS utils */ -export function clearRequireCache (id: string) { +function clearRequireCache (id: string) { if (isNodeModules(id)) { return } @@ -53,59 +53,27 @@ export function clearRequireCache (id: string) { } /** @deprecated Do not use CJS utils */ -export function scanRequireTree (id: string, files = new Set()) { - if (isNodeModules(id) || files.has(id)) { - return files - } - - const entry = getRequireCacheItem(id) - - if (!entry) { - files.add(id) - return files - } - - files.add(entry.id) - - for (const child of entry.children) { - scanRequireTree(child.id, files) - } - - return files -} - -/** @deprecated Do not use CJS utils */ -export function getRequireCacheItem (id: string) { +function getRequireCacheItem (id: string) { try { return _require.cache[id] } catch (e) { } } -/** @deprecated Do not use CJS utils */ -export function resolveModule (id: string, opts: ResolveModuleOptions = {}) { - return normalize(_require.resolve(id, { - paths: ([] as string[]).concat( - // @ts-ignore - global.__NUXT_PREPATHS__, - opts.paths || [], - process.cwd(), - // @ts-ignore - global.__NUXT_PATHS__ - ).filter(Boolean) - })) +export function getModulePaths (paths?: string[] | string) { + return ([] as Array).concat( + global.__NUXT_PREPATHS__, + paths || [], + process.cwd(), + global.__NUXT_PATHS__ + ).filter(Boolean) as string[] } /** @deprecated Do not use CJS utils */ -export function tryResolveModule (path: string, opts: ResolveModuleOptions = {}): string | null { - try { - return resolveModule(path, opts) - } catch (error: any) { - if (error?.code !== 'MODULE_NOT_FOUND') { - throw error - } - } - return null +export function resolveModule (id: string, opts: ResolveModuleOptions = {}) { + return normalize(_require.resolve(id, { + paths: getModulePaths(opts.paths) + })) } /** @deprecated Do not use CJS utils */ @@ -137,7 +105,7 @@ export function importModule (id: string, opts: RequireModuleOptions = {}) { export function tryImportModule (id: string, opts: RequireModuleOptions = {}) { try { return importModule(id, opts).catch(() => undefined) - } catch { } + } catch {} } /** @deprecated Do not use CJS utils */ diff --git a/packages/kit/src/internal/task.ts b/packages/kit/src/internal/task.ts deleted file mode 100644 index 45ed2c439b..0000000000 --- a/packages/kit/src/internal/task.ts +++ /dev/null @@ -1,42 +0,0 @@ -export function sequence ( - tasks: T[], - fn: (task: T) => R -) { - return tasks.reduce( - (promise, task): any => promise.then(() => fn(task)), - Promise.resolve() - ) -} - -export function parallel ( - tasks: T[], - fn: (task: T) => R -) { - return Promise.all(tasks.map(fn)) -} - -export function chainFn (base: Fn, fn: Fn): Fn { - if (typeof fn !== 'function') { - return base - } - return function (this: any, ...args: any[]) { - if (typeof base !== 'function') { - return fn.apply(this, args) - } - let baseResult = base.apply(this, args) - // Allow function to mutate the first argument instead of returning the result - if (baseResult === undefined) { - [baseResult] = args - } - const fnResult = fn.call( - this, - baseResult, - ...Array.prototype.slice.call(args, 1) - ) - // Return mutated argument if no result was returned - if (fnResult === undefined) { - return baseResult - } - return fnResult - } as unknown as Fn -} diff --git a/packages/kit/src/internal/template.ts b/packages/kit/src/internal/template.ts index af0a3506f8..f5024b52db 100644 --- a/packages/kit/src/internal/template.ts +++ b/packages/kit/src/internal/template.ts @@ -1,10 +1,13 @@ import { promises as fsp } from 'node:fs' -import lodashTemplate from 'lodash.template' +// TODO: swap out when https://github.com/lodash/lodash/pull/5649 is merged +import { template as lodashTemplate } from 'lodash-es' import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork' import type { NuxtTemplate } from '@nuxt/schema' +import { logger } from '../logger' /** @deprecated */ +// TODO: Remove support for compiling ejs templates in v4 export async function compileTemplate (template: NuxtTemplate, ctx: any) { const data = { ...ctx, options: template.options } if (template.src) { @@ -12,7 +15,7 @@ export async function compileTemplate (template: NuxtTemplate, ctx: any) { const srcContents = await fsp.readFile(template.src, 'utf-8') return lodashTemplate(srcContents, {})(data) } catch (err) { - console.error('Error compiling template: ', template) + logger.error('Error compiling template: ', template) throw err } } diff --git a/packages/kit/src/layout.ts b/packages/kit/src/layout.ts index 01d176e968..91e50310c4 100644 --- a/packages/kit/src/layout.ts +++ b/packages/kit/src/layout.ts @@ -6,7 +6,7 @@ import { useNuxt } from './context' import { logger } from './logger' import { addTemplate } from './template' -export function addLayout (this: any, template: NuxtTemplate, name?: string) { +export function addLayout (this: any, template: NuxtTemplate | string, name?: string) { const nuxt = useNuxt() const { filename, src } = addTemplate(template) const layoutName = kebabCase(name || parse(filename).name).replace(/["']/g, '') diff --git a/packages/kit/src/loader/config.ts b/packages/kit/src/loader/config.ts index 368781b7de..a4bce4f0c4 100644 --- a/packages/kit/src/loader/config.ts +++ b/packages/kit/src/loader/config.ts @@ -1,4 +1,5 @@ import { resolve } from 'pathe' +import type { JSValue } from 'untyped' import { applyDefaults } from 'untyped' import type { LoadConfigOptions } from 'c12' import { loadConfig } from 'c12' @@ -50,5 +51,5 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise) as unknown as NuxtOptions } diff --git a/packages/kit/src/loader/nuxt.ts b/packages/kit/src/loader/nuxt.ts index 722bc61eb7..aaec491fae 100644 --- a/packages/kit/src/loader/nuxt.ts +++ b/packages/kit/src/loader/nuxt.ts @@ -26,7 +26,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise { // Apply dev as config override opts.overrides.dev = !!opts.dev - const nearestNuxtPkg = await Promise.all(['nuxt3', 'nuxt', 'nuxt-edge'] + const nearestNuxtPkg = await Promise.all(['nuxt-nightly', 'nuxt3', 'nuxt', 'nuxt-edge'] .map(pkg => resolvePackageJSON(pkg, { url: opts.cwd }).catch(() => null))) .then(r => (r.filter(Boolean) as string[]).sort((a, b) => b.length - a.length)[0]) if (!nearestNuxtPkg) { @@ -54,6 +54,19 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise { envConfig: opts.dotenv // TODO: Backward format conversion }) + // Mock new hookable methods + nuxt.removeHook ||= nuxt.clearHook.bind(nuxt) + nuxt.removeAllHooks ||= nuxt.clearHooks.bind(nuxt) + nuxt.hookOnce ||= (name: string, fn: (...args: any[]) => any, ...hookArgs: any[]) => { + const unsub = nuxt.hook(name, (...args: any[]) => { + unsub() + return fn(...args) + }, ...hookArgs) + return unsub + } + // https://github.com/nuxt/nuxt/tree/main/packages/kit/src/module/define.ts#L111-L113 + nuxt.hooks ||= nuxt + return nuxt as Nuxt } @@ -62,7 +75,7 @@ export async function buildNuxt (nuxt: Nuxt): Promise { // Nuxt 3 if (nuxt.options._majorVersion === 3) { - const { build } = await tryImportModule('nuxt3', rootDir) || await importModule('nuxt', rootDir) + const { build } = await tryImportModule('nuxt-nightly', rootDir) || await tryImportModule('nuxt3', rootDir) || await importModule('nuxt', rootDir) return build(nuxt) } diff --git a/packages/kit/src/module/compatibility.test.ts b/packages/kit/src/module/compatibility.test.ts new file mode 100644 index 0000000000..ce20f54f02 --- /dev/null +++ b/packages/kit/src/module/compatibility.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { loadNuxt } from '../loader/nuxt' +import { getNuxtModuleVersion, hasNuxtModule, hasNuxtModuleCompatibility } from './compatibility' +import { defineNuxtModule } from './define' + +describe('nuxt module compatibility', () => { + it('check module installed', async () => { + const nuxt = await loadNuxt({ + overrides: { + modules: [ + defineNuxtModule({ + meta: { + name: 'nuxt-module-foo' + } + }), + [ + defineNuxtModule({ + meta: { + name: 'module-instance-with-options' + } + }), + { + foo: 'bar' + } + ] + ] + } + }) + expect(hasNuxtModule('nuxt-module-foo', nuxt)).toStrictEqual(true) + expect(hasNuxtModule('module-instance-with-options', nuxt)).toStrictEqual(true) + await nuxt.close() + }) + it('can retrieve module version from module instance', async () => { + const nuxt = await loadNuxt({}) + const module = defineNuxtModule({ + meta: { + name: 'nuxt-module-foo', + version: '1.0.0' + } + }) + expect(await getNuxtModuleVersion(module, nuxt)).toEqual('1.0.0') + await nuxt.close() + }) + it('check module instance version compatibility', async () => { + const nuxt = await loadNuxt({}) + const module = defineNuxtModule({ + meta: { + name: 'nuxt-module-foo', + version: '1.0.0' + } + }) + expect(await hasNuxtModuleCompatibility(module, '^1.0.0', nuxt)).toStrictEqual(true) + expect(await hasNuxtModuleCompatibility(module, '^2.0.0', nuxt)).toStrictEqual(false) + await nuxt.close() + }) +}) diff --git a/packages/kit/src/module/compatibility.ts b/packages/kit/src/module/compatibility.ts new file mode 100644 index 0000000000..666f4b4085 --- /dev/null +++ b/packages/kit/src/module/compatibility.ts @@ -0,0 +1,66 @@ +import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381 +import type { Nuxt, NuxtModule, NuxtOptions } from '@nuxt/schema' +import { useNuxt } from '../context' +import { normalizeSemanticVersion } from '../compatibility' +import { loadNuxtModuleInstance } from './install' + +function resolveNuxtModuleEntryName (m: NuxtOptions['modules'][number]): string | false { + if (typeof m === 'object' && !Array.isArray(m)) { + return (m as any as NuxtModule).name + } + if (Array.isArray(m)) { + return resolveNuxtModuleEntryName(m[0]) + } + return m as string || false +} + +/** + * Check if a Nuxt module is installed by name. + * + * This will check both the installed modules and the modules to be installed. Note + * that it cannot detect if a module is _going to be_ installed programmatically by another module. + */ +export function hasNuxtModule (moduleName: string, nuxt: Nuxt = useNuxt()) : boolean { + // check installed modules + return nuxt.options._installedModules.some(({ meta }) => meta.name === moduleName) || + // check modules to be installed + nuxt.options.modules.some(m => moduleName === resolveNuxtModuleEntryName(m)) +} + +/** + * Checks if a Nuxt Module is compatible with a given semver version. + */ +export async function hasNuxtModuleCompatibility (module: string | NuxtModule, semverVersion: string, nuxt: Nuxt = useNuxt()): Promise { + const version = await getNuxtModuleVersion(module, nuxt) + if (!version) { + return false + } + return satisfies(normalizeSemanticVersion(version), semverVersion, { + includePrerelease: true + }) +} + +/** + * Get the version of a Nuxt module. + * + * Scans installed modules for the version, if it's not found it will attempt to load the module instance and get the version from there. + */ +export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: Nuxt | any = useNuxt()): Promise { + const moduleMeta = (typeof module === 'string' ? { name: module } : await module.getMeta?.()) || {} + if (moduleMeta.version) { return moduleMeta.version } + // need a name from here + if (!moduleMeta.name) { return false } + // maybe the version got attached within the installed module instance? + const version = nuxt.options._installedModules + // @ts-expect-error _installedModules is not typed + .filter(m => m.meta.name === moduleMeta.name).map(m => m.meta.version)?.[0] + if (version) { + return version + } + // it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance + if (hasNuxtModule(moduleMeta.name)) { + const { buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt) + return buildTimeModuleMeta.version || false + } + return false +} diff --git a/packages/kit/src/module/define.ts b/packages/kit/src/module/define.ts index 2fd2828d0a..360d1a14db 100644 --- a/packages/kit/src/module/define.ts +++ b/packages/kit/src/module/define.ts @@ -13,20 +13,22 @@ import { compileTemplate, templateUtils } from '../internal/template' * Define a Nuxt module, automatically merging defaults with user provided options, installing * any hooks that are provided, and calling an optional setup function for full control. */ -export function defineNuxtModule (definition: ModuleDefinition): NuxtModule { +export function defineNuxtModule (definition: ModuleDefinition | NuxtModule): NuxtModule { + if (typeof definition === 'function') { return defineNuxtModule({ setup: definition }) } + // Normalize definition and meta - if (!definition.meta) { definition.meta = {} } - if (definition.meta.configKey === undefined) { - definition.meta.configKey = definition.meta.name + const module: ModuleDefinition & Required, 'meta'>> = defu(definition, { meta: {} }) + if (module.meta.configKey === undefined) { + module.meta.configKey = module.meta.name } // Resolves module options from inline options, [configKey] in nuxt.config, defaults and schema async function getOptions (inlineOptions?: OptionsT, nuxt: Nuxt = useNuxt()) { - const configKey = definition.meta!.configKey || definition.meta!.name! - const _defaults = definition.defaults instanceof Function ? definition.defaults(nuxt) : definition.defaults + const configKey = module.meta.configKey || module.meta.name! + const _defaults = module.defaults instanceof Function ? module.defaults(nuxt) : module.defaults let _options = defu(inlineOptions, nuxt.options[configKey as keyof NuxtOptions], _defaults) as OptionsT - if (definition.schema) { - _options = await applyDefaults(definition.schema, _options) as OptionsT + if (module.schema) { + _options = await applyDefaults(module.schema, _options) as OptionsT } return Promise.resolve(_options) } @@ -38,7 +40,7 @@ export function defineNuxtModule (definition: Mo } // Avoid duplicate installs - const uniqueKey = definition.meta!.name || definition.meta!.configKey + const uniqueKey = module.meta.name || module.meta.configKey if (uniqueKey) { nuxt.options._requiredModules = nuxt.options._requiredModules || {} if (nuxt.options._requiredModules[uniqueKey]) { @@ -48,10 +50,10 @@ export function defineNuxtModule (definition: Mo } // Check compatibility constraints - if (definition.meta!.compatibility) { - const issues = await checkNuxtCompatibility(definition.meta!.compatibility, nuxt) + if (module.meta.compatibility) { + const issues = await checkNuxtCompatibility(module.meta.compatibility, nuxt) if (issues.length) { - logger.warn(`Module \`${definition.meta!.name}\` is disabled due to incompatibility issues:\n${issues.toString()}`) + logger.warn(`Module \`${module.meta.name}\` is disabled due to incompatibility issues:\n${issues.toString()}`) return } } @@ -63,19 +65,19 @@ export function defineNuxtModule (definition: Mo const _options = await getOptions(inlineOptions, nuxt) // Register hooks - if (definition.hooks) { - nuxt.hooks.addHooks(definition.hooks) + if (module.hooks) { + nuxt.hooks.addHooks(module.hooks) } // Call setup const key = `nuxt:module:${uniqueKey || (Math.round(Math.random() * 10000))}` const mark = performance.mark(key) - const res = await definition.setup?.call(null as any, _options, nuxt) ?? {} + const res = await module.setup?.call(null as any, _options, nuxt) ?? {} const perf = performance.measure(key, 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 // Measure setup time - if (setupTime > 5000) { + if (setupTime > 5000 && uniqueKey !== '@nuxt/telemetry') { logger.warn(`Slow module \`${uniqueKey || ''}\` took \`${setupTime}ms\` to setup.`) } else if (nuxt.options.debug) { logger.info(`Module \`${uniqueKey || ''}\` took \`${setupTime}ms\` to setup.`) @@ -93,7 +95,7 @@ export function defineNuxtModule (definition: Mo } // Define getters for options and meta - normalizedModule.getMeta = () => Promise.resolve(definition.meta) + normalizedModule.getMeta = () => Promise.resolve(module.meta) normalizedModule.getOptions = getOptions return normalizedModule as NuxtModule @@ -103,10 +105,8 @@ export function defineNuxtModule (definition: Mo const NUXT2_SHIMS_KEY = '__nuxt2_shims_key__' function nuxt2Shims (nuxt: Nuxt) { // Avoid duplicate install and only apply to Nuxt2 - // @ts-ignore - if (!isNuxt2(nuxt) || nuxt[NUXT2_SHIMS_KEY]) { return } - // @ts-ignore - nuxt[NUXT2_SHIMS_KEY] = true + if (!isNuxt2(nuxt) || nuxt[NUXT2_SHIMS_KEY as keyof Nuxt]) { return } + nuxt[NUXT2_SHIMS_KEY as keyof Nuxt] = true // Allow using nuxt.hooks // @ts-expect-error Nuxt 2 extends hookable @@ -120,14 +120,14 @@ function nuxt2Shims (nuxt: Nuxt) { // Support virtual templates with getContents() by writing them to .nuxt directory let virtualTemplates: ResolvedNuxtTemplate[] - // @ts-ignore Nuxt 2 hook + // @ts-expect-error Nuxt 2 hook nuxt.hook('builder:prepared', (_builder, buildOptions) => { virtualTemplates = buildOptions.templates.filter((t: any) => t.getContents) for (const template of virtualTemplates) { buildOptions.templates.splice(buildOptions.templates.indexOf(template), 1) } }) - // @ts-ignore Nuxt 2 hook + // @ts-expect-error Nuxt 2 hook nuxt.hook('build:templates', async (templates) => { const context = { nuxt, diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts index a55f391c13..8d08e80e28 100644 --- a/packages/kit/src/module/install.ts +++ b/packages/kit/src/module/install.ts @@ -1,14 +1,17 @@ -import type { Nuxt, NuxtModule } from '@nuxt/schema' +import { existsSync, promises as fsp, lstatSync } from 'node:fs' +import type { ModuleMeta, Nuxt, NuxtModule } from '@nuxt/schema' +import { dirname, isAbsolute, join } from 'pathe' +import { defu } from 'defu' import { isNuxt2 } from '../compatibility' import { useNuxt } from '../context' -import { requireModule, resolveModule } from '../internal/cjs' +import { requireModule } from '../internal/cjs' import { importModule } from '../internal/esm' -import { resolveAlias } from '../resolve' +import { resolveAlias, resolvePath } from '../resolve' +import { logger } from '../logger' /** Installs a module on a Nuxt instance. */ -export async function installModule (moduleToInstall: string | NuxtModule, _inlineOptions?: any, _nuxt?: Nuxt) { - const nuxt = useNuxt() - const { nuxtModule, inlineOptions } = await normalizeModule(moduleToInstall, _inlineOptions) +export async function installModule (moduleToInstall: string | NuxtModule, inlineOptions?: any, nuxt: Nuxt = useNuxt()) { + const { nuxtModule, buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleToInstall, nuxt) // Call module const res = ( @@ -22,12 +25,16 @@ export async function installModule (moduleToInstall: string | NuxtModule, _inli } if (typeof moduleToInstall === 'string') { - nuxt.options.build.transpile.push(moduleToInstall) + nuxt.options.build.transpile.push(normalizeModuleTranspilePath(moduleToInstall)) + const directory = getDirectory(moduleToInstall) + if (directory !== moduleToInstall) { + nuxt.options.modulesDir.push(getDirectory(moduleToInstall)) + } } nuxt.options._installedModules = nuxt.options._installedModules || [] nuxt.options._installedModules.push({ - meta: await nuxtModule.getMeta?.(), + meta: defu(await nuxtModule.getMeta?.(), buildTimeModuleMeta), timings: res.timings, entryPath: typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall) : undefined }) @@ -35,21 +42,37 @@ export async function installModule (moduleToInstall: string | NuxtModule, _inli // --- Internal --- -async function normalizeModule (nuxtModule: string | NuxtModule, inlineOptions?: any) { - const nuxt = useNuxt() +function getDirectory (p: string) { + try { + // we need to target directories instead of module file paths themselves + // /home/user/project/node_modules/module/index.js -> /home/user/project/node_modules/module + return isAbsolute(p) && lstatSync(p).isFile() ? dirname(p) : p + } catch (e) { + // maybe the path is absolute but does not exist, allow this to bubble up + } + return p +} +export const normalizeModuleTranspilePath = (p: string) => { + return getDirectory(p).split('node_modules/').pop() as string +} + +export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) { + let buildTimeModuleMeta: ModuleMeta = {} // Import if input is string if (typeof nuxtModule === 'string') { - const _src = resolveModule(resolveAlias(nuxtModule), { paths: nuxt.options.modulesDir }) - // TODO: also check with type: 'module' in closest `package.json` - const isESM = _src.endsWith('.mjs') - + const src = await resolvePath(nuxtModule) try { - nuxtModule = isESM ? await importModule(_src, nuxt.options.rootDir) : requireModule(_src) + // Prefer ESM resolution if possible + nuxtModule = await importModule(src, nuxt.options.modulesDir).catch(() => null) ?? requireModule(src, { paths: nuxt.options.modulesDir }) } catch (error: unknown) { - console.error(`Error while requiring module \`${nuxtModule}\`: ${error}`) + logger.error(`Error while requiring module \`${nuxtModule}\`: ${error}`) throw error } + // nuxt-module-builder generates a module.json with metadata including the version + if (existsSync(join(dirname(src), 'module.json'))) { + buildTimeModuleMeta = JSON.parse(await fsp.readFile(join(dirname(src), 'module.json'), 'utf-8')) + } } // Throw error if input is not a function @@ -57,5 +80,5 @@ async function normalizeModule (nuxtModule: string | NuxtModule, inlineOptions?: throw new TypeError('Nuxt module should be a function: ' + nuxtModule) } - return { nuxtModule, inlineOptions } as { nuxtModule: NuxtModule, inlineOptions: undefined | Record } + return { nuxtModule, buildTimeModuleMeta } as { nuxtModule: NuxtModule, buildTimeModuleMeta: ModuleMeta } } diff --git a/packages/kit/src/nitro.ts b/packages/kit/src/nitro.ts index 43375ea113..be642efc4b 100644 --- a/packages/kit/src/nitro.ts +++ b/packages/kit/src/nitro.ts @@ -1,4 +1,5 @@ import type { Nitro, NitroDevEventHandler, NitroEventHandler } from 'nitropack' +import type { Import } from 'unimport' import { normalize } from 'pathe' import { useNuxt } from './context' @@ -66,7 +67,6 @@ export function addPrerenderRoutes (routes: string | string[]) { * **Note:** You can call `useNitro()` only after `ready` hook. * * **Note:** Changes to the Nitro instance configuration are not applied. - * * @example * * ```ts @@ -82,3 +82,32 @@ export function useNitro (): Nitro { } return (nuxt as any)._nitro } + +/** + * Add server imports to be auto-imported by Nitro + */ +export function addServerImports (imports: Import[]) { + const nuxt = useNuxt() + nuxt.hook('nitro:config', (config) => { + config.imports = config.imports || {} + if (Array.isArray(config.imports.imports)) { + config.imports.imports.push(...imports) + } else { + config.imports.imports = [config.imports.imports, ...imports] + } + }) +} + +/** + * Add directories to be scanned by Nitro + */ +export function addServerImportsDir (dirs: string | string[], opts: { prepend?: boolean } = {}) { + const nuxt = useNuxt() + nuxt.hook('nitro:config', (config) => { + config.scanDirs = config.scanDirs || [] + + for (const dir of (Array.isArray(dirs) ? dirs : [dirs])) { + config.scanDirs[opts.prepend ? 'unshift' : 'push'](dir) + } + }) +} diff --git a/packages/kit/src/pages.ts b/packages/kit/src/pages.ts index 685c3b83b5..688b78786e 100644 --- a/packages/kit/src/pages.ts +++ b/packages/kit/src/pages.ts @@ -3,11 +3,12 @@ import type { NitroRouteConfig } from 'nitropack' import { defu } from 'defu' import { useNuxt } from './context' import { isNuxt2 } from './compatibility' +import { logger } from './logger' export function extendPages (cb: NuxtHooks['pages:extend']) { const nuxt = useNuxt() if (isNuxt2(nuxt)) { - // @ts-expect-error + // @ts-expect-error TODO: Nuxt 2 hook nuxt.hook('build:extendRoutes', cb) } else { nuxt.hook('pages:extend', cb) @@ -17,7 +18,6 @@ export function extendPages (cb: NuxtHooks['pages:extend']) { export interface ExtendRouteRulesOptions { /** * Override route rule config - * * @default false */ override?: boolean @@ -38,7 +38,6 @@ export function extendRouteRules (route: string, rule: NitroRouteConfig, options export interface AddRouteMiddlewareOptions { /** * Override existing middleware with the same name, if it exists - * * @default false */ override?: boolean @@ -54,7 +53,7 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op if (options.override === true) { app.middleware[find] = middleware } else { - console.warn(`'${middleware.name}' middleware already exists at '${app.middleware[find].path}'. You can set \`override: true\` to replace it.`) + logger.warn(`'${middleware.name}' middleware already exists at '${app.middleware[find].path}'. You can set \`override: true\` to replace it.`) } } else { app.middleware.push(middleware) diff --git a/packages/kit/src/plugin.ts b/packages/kit/src/plugin.ts index 89929c9bed..21b6b33ee1 100644 --- a/packages/kit/src/plugin.ts +++ b/packages/kit/src/plugin.ts @@ -3,6 +3,7 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema' import { useNuxt } from './context' import { addTemplate } from './template' import { resolveAlias } from './resolve' +import { logger } from './logger' /** * Normalize a nuxt plugin object @@ -22,7 +23,7 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin { // TODO: only scan top-level files #18418 const nonTopLevelPlugin = plugin.src.match(/\/plugins\/[^/]+\/index\.[^/]+$/i) if (nonTopLevelPlugin && nonTopLevelPlugin.length > 0 && !useNuxt().options.plugins.find(i => (typeof i === 'string' ? i : i.src).endsWith(nonTopLevelPlugin[0]))) { - console.warn(`[warn] [nuxt] [deprecation] You are using a plugin that is within a subfolder of your plugins directory without adding it to your config explicitly. You can move it to the top-level plugins directory, or include the file '~${nonTopLevelPlugin[0]}' in your plugins config (https://nuxt.com/docs/api/configuration/nuxt-config#plugins-1) to remove this warning.`) + logger.warn(`[deprecation] You are using a plugin that is within a subfolder of your plugins directory without adding it to your config explicitly. You can move it to the top-level plugins directory, or include the file '~${nonTopLevelPlugin[0]}' in your plugins config (https://nuxt.com/docs/api/configuration/nuxt-config#plugins-1) to remove this warning.`) } // Normalize full path to plugin @@ -47,7 +48,6 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin { * to use plugin only in client or server side. * * Note: By default plugin is prepended to the plugins array. You can use second argument to append (push) instead. - * * @example * ```js * addPlugin({ diff --git a/packages/kit/src/resolve.ts b/packages/kit/src/resolve.ts index 70ae364d2a..54fdd96628 100644 --- a/packages/kit/src/resolve.ts +++ b/packages/kit/src/resolve.ts @@ -2,9 +2,9 @@ import { existsSync, promises as fsp } from 'node:fs' import { fileURLToPath } from 'node:url' import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe' import { globby } from 'globby' +import { resolvePath as _resolvePath } from 'mlly' import { resolveAlias as _resolveAlias } from 'pathe/utils' import { tryUseNuxt } from './context' -import { tryResolveModule } from './internal/cjs' import { isIgnored } from './ignore' export interface ResolvePathOptions { @@ -71,7 +71,7 @@ export async function resolvePath (path: string, opts: ResolvePathOptions = {}): } // Try to resolve as module id - const resolveModulePath = tryResolveModule(_path, { paths: [cwd, ...modulesDir] }) + const resolveModulePath = await _resolvePath(_path, { url: [cwd, ...modulesDir] }).catch(() => null) if (resolveModulePath) { return resolveModulePath } @@ -133,6 +133,22 @@ export function createResolver (base: string | URL): Resolver { } } +export async function resolveNuxtModule (base: string, paths: string[]) { + const resolved = [] + const resolver = createResolver(base) + + for (const path of paths) { + if (path.startsWith(base)) { + resolved.push(path.split('/index.ts')[0]) + } else { + const resolvedPath = await resolver.resolvePath(path) + resolved.push(resolvedPath.slice(0, resolvedPath.lastIndexOf(path) + path.length)) + } + } + + return resolved +} + // --- Internal --- async function existsSensitive (path: string) { diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts index 719d870830..f76cfe84d2 100644 --- a/packages/kit/src/template.ts +++ b/packages/kit/src/template.ts @@ -1,8 +1,16 @@ -import { existsSync } from 'node:fs' -import { basename, parse, resolve } from 'pathe' +import { existsSync, promises as fsp } from 'node:fs' +import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe' import hash from 'hash-sum' -import type { NuxtTemplate, ResolvedNuxtTemplate } from '@nuxt/schema' +import type { Nuxt, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema' +import { withTrailingSlash } from 'ufo' +import { defu } from 'defu' +import type { TSConfig } from 'pkg-types' +import { readPackageJSON } from 'pkg-types' + +import { tryResolveModule } from './internal/esm' import { tryUseNuxt, useNuxt } from './context' +import { getModulePaths } from './internal/cjs' +import { resolveNuxtModule } from './resolve' /** * Renders given template using lodash template during build into the project buildDir @@ -23,6 +31,27 @@ export function addTemplate (_template: NuxtTemplate | string) { return template } +/** + * Renders given types using lodash template during build into the project buildDir + * and register them as types. + */ +export function addTypeTemplate (_template: NuxtTypeTemplate) { + const nuxt = useNuxt() + + const template = addTemplate(_template) + + if (!template.filename.endsWith('.d.ts')) { + throw new Error(`Invalid type template. Filename must end with .d.ts : "${template.filename}"`) + } + + // Add template to types reference + nuxt.hook('prepare:types', ({ references }) => { + references.push({ path: template.dst }) + }) + + return template +} + /** * Normalize a nuxt template object */ @@ -80,3 +109,174 @@ export function normalizeTemplate (template: NuxtTemplate | string): Resolv export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate) => boolean }) { return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options) } +export async function writeTypes (nuxt: Nuxt) { + const modulePaths = getModulePaths(nuxt.options.modulesDir) + + const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir) + + const modules = await resolveNuxtModule(rootDirWithSlash, + nuxt.options._installedModules + .filter(m => m.entryPath) + .map(m => m.entryPath) + ) + + const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, { + compilerOptions: { + forceConsistentCasingInFileNames: true, + jsx: 'preserve', + jsxImportSource: 'vue', + target: 'ESNext', + module: 'ESNext', + moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node', + skipLibCheck: true, + isolatedModules: true, + useDefineForClassFields: true, + strict: nuxt.options.typescript?.strict ?? true, + noImplicitThis: true, + esModuleInterop: true, + types: [], + verbatimModuleSyntax: true, + allowJs: true, + noEmit: true, + resolveJsonModule: true, + allowSyntheticDefaultImports: true, + paths: {} + }, + include: [ + './nuxt.d.ts', + join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'), + ...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [], + ...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd) + .filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules')) + .map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')), + ...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : [], + ...modules.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime')) + ], + exclude: [ + ...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)), + ...modules.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime/server')), + // 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')) + ] + } satisfies TSConfig) + + const aliases: Record = { + ...nuxt.options.alias, + '#build': nuxt.options.buildDir + } + + // Exclude bridge alias types to support Volar + const excludedAlias = [/^@vue\/.*$/] + + const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir + + tsConfig.compilerOptions = tsConfig.compilerOptions || {} + tsConfig.include = tsConfig.include || [] + + for (const alias in aliases) { + if (excludedAlias.some(re => re.test(alias))) { + continue + } + let absolutePath = resolve(basePath, aliases[alias]) + let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */) + if (!stats) { + const resolvedModule = await tryResolveModule(aliases[alias], nuxt.options.modulesDir) + if (resolvedModule) { + absolutePath = resolvedModule + stats = await fsp.stat(resolvedModule).catch(() => null) + } + } + + const relativePath = relativeWithDot(nuxt.options.buildDir, absolutePath) + if (stats?.isDirectory()) { + tsConfig.compilerOptions.paths[alias] = [relativePath] + tsConfig.compilerOptions.paths[`${alias}/*`] = [`${relativePath}/*`] + + if (!absolutePath.startsWith(rootDirWithSlash)) { + tsConfig.include.push(relativePath) + } + } else { + const path = stats?.isFile() + // remove extension + ? relativePath.replace(/(?<=\w)\.\w+$/g, '') + // non-existent file probably shouldn't be resolved + : aliases[alias] + + tsConfig.compilerOptions.paths[alias] = [path] + + if (!absolutePath.startsWith(rootDirWithSlash)) { + tsConfig.include.push(path) + } + } + } + + const references: TSReference[] = await Promise.all([ + ...nuxt.options.modules, + ...nuxt.options._modules + ] + .filter(f => typeof f === 'string') + .map(async id => ({ types: (await readPackageJSON(id, { url: modulePaths }).catch(() => null))?.name || id }))) + + if (nuxt.options.experimental?.reactivityTransform) { + references.push({ types: 'vue/macros-global' }) + } + + const declarations: string[] = [] + + await nuxt.callHook('prepare:types', { references, declarations, tsConfig }) + + for (const alias in tsConfig.compilerOptions!.paths) { + const paths = tsConfig.compilerOptions!.paths[alias] + tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => { + if (!isAbsolute(path)) { return path } + const stats = await fsp.stat(path).catch(() => null /* file does not exist */) + return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ : path) + })) + } + + tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))] + tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))] + + const declaration = [ + ...references.map((ref) => { + if ('path' in ref && isAbsolute(ref.path)) { + ref.path = relative(nuxt.options.buildDir, ref.path) + } + return `/// ` + }), + ...declarations, + '', + 'export {}', + '' + ].join('\n') + + async function writeFile () { + const GeneratedBy = '// Generated by nuxi' + + const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json') + await fsp.mkdir(nuxt.options.buildDir, { recursive: true }) + await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2)) + + const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts') + await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration) + } + + // This is needed for Nuxt 2 which clears the build directory again before building + // https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144 + // @ts-expect-error TODO: Nuxt 2 hook + nuxt.hook('builder:prepared', writeFile) + + await writeFile() +} + +function renderAttrs (obj: Record) { + return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ') +} + +function renderAttr (key: string, value: string) { + return value ? `${key}="${value}"` : '' +} + +function relativeWithDot (from: string, to: string) { + return relative(from, to).replace(/^([^.])/, './$1') || '.' +} diff --git a/packages/nuxi/README.md b/packages/nuxi/README.md new file mode 100644 index 0000000000..4edd42b0e9 --- /dev/null +++ b/packages/nuxi/README.md @@ -0,0 +1,5 @@ +# Nuxt CLI (nuxi) + +⚡️ Next Generation CLI Experience for [Nuxt](https://nuxt.com/). + +- 👉 View on GitHub at https://github.com/nuxt/cli diff --git a/packages/nuxi/bin/nuxi.mjs b/packages/nuxi/bin/nuxi.mjs deleted file mode 100755 index 8dcfc256bd..0000000000 --- a/packages/nuxi/bin/nuxi.mjs +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import('../dist/cli-wrapper.mjs') diff --git a/packages/nuxi/build.config.ts b/packages/nuxi/build.config.ts deleted file mode 100644 index 522086d61a..0000000000 --- a/packages/nuxi/build.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { defineBuildConfig } from 'unbuild' - -export default defineBuildConfig({ - declaration: true, - rollup: { - inlineDependencies: true, - resolve: { - exportConditions: ['production', 'node'] as any - } - }, - entries: [ - 'src/cli', - 'src/cli-run', - 'src/cli-wrapper', - 'src/index' - ], - externals: [ - '@nuxt/kit', - '@nuxt/schema', - '@nuxt/test-utils', - 'fsevents', - // TODO: Fix rollup/unbuild issue - 'node:url', - 'node:buffer', - 'node:path', - 'node:child_process', - 'node:process', - 'node:path', - 'node:os' - ] -}) diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json deleted file mode 100644 index 559e3721ee..0000000000 --- a/packages/nuxi/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "nuxi", - "version": "3.3.3", - "repository": "nuxt/nuxt", - "license": "MIT", - "type": "module", - "types": "./dist/index.d.ts", - "exports": { - ".": "./dist/index.mjs", - "./cli": "./bin/nuxi.mjs" - }, - "bin": "./bin/nuxi.mjs", - "files": [ - "bin", - "dist" - ], - "scripts": { - "prepack": "unbuild" - }, - "devDependencies": { - "@nuxt/kit": "workspace:../kit", - "@nuxt/schema": "workspace:../schema", - "@types/clear": "^0.1.2", - "@types/flat": "^5.0.2", - "@types/mri": "^1.1.1", - "@types/semver": "^7.3.13", - "c12": "^1.2.0", - "chokidar": "^3.5.3", - "clear": "^0.1.0", - "clipboardy": "^3.0.0", - "colorette": "^2.0.19", - "consola": "^3.0.0-3", - "deep-object-diff": "^1.1.9", - "destr": "^1.2.2", - "execa": "^7.1.1", - "flat": "^5.0.2", - "giget": "^1.1.2", - "h3": "^1.6.4", - "jiti": "^1.18.2", - "listhen": "^1.0.4", - "mlly": "^1.2.0", - "mri": "^1.2.0", - "pathe": "^1.1.0", - "perfect-debounce": "^0.1.3", - "pkg-types": "^1.0.2", - "scule": "^1.0.0", - "semver": "^7.3.8", - "unbuild": "latest" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "engines": { - "node": "^14.18.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } -} diff --git a/packages/nuxi/src/cli-run.ts b/packages/nuxi/src/cli-run.ts deleted file mode 100644 index 07aef4afac..0000000000 --- a/packages/nuxi/src/cli-run.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-ignore -process._startTime = Date.now() - -// @ts-ignore -import('./cli').then(r => (r.default || r).main()) diff --git a/packages/nuxi/src/cli-wrapper.ts b/packages/nuxi/src/cli-wrapper.ts deleted file mode 100644 index 251ace7673..0000000000 --- a/packages/nuxi/src/cli-wrapper.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * This file is used to wrap the CLI entrypoint in a restartable process. - */ -import { fileURLToPath } from 'node:url' -import { fork } from 'node:child_process' -import type { ChildProcess } from 'node:child_process' - -const cliEntry = new URL('../dist/cli-run.mjs', import.meta.url) - -// Only enable wrapper for nuxt dev command -if (process.argv[2] === 'dev') { - process.env.__CLI_ARGV__ = JSON.stringify(process.argv) - startSubprocess() -} else { - import(cliEntry.href) -} - -function startSubprocess () { - let childProc: ChildProcess | undefined - - const onShutdown = () => { - if (childProc) { - childProc.kill() - childProc = undefined - } - } - - process.on('exit', onShutdown) - process.on('SIGTERM', onShutdown) // Graceful shutdown - process.on('SIGINT', onShutdown) // Ctrl-C - process.on('SIGQUIT', onShutdown) // Ctrl-\ - - start() - - function start () { - childProc = fork(fileURLToPath(cliEntry)) - childProc.on('close', (code) => { if (code) { process.exit(code) } }) - childProc.on('message', (message) => { - if ((message as { type: string })?.type === 'nuxt:restart') { - childProc?.kill() - startSubprocess() - } - }) - } -} diff --git a/packages/nuxi/src/cli.ts b/packages/nuxi/src/cli.ts deleted file mode 100755 index ea56732bcb..0000000000 --- a/packages/nuxi/src/cli.ts +++ /dev/null @@ -1,84 +0,0 @@ -import mri from 'mri' -import { red } from 'colorette' -import type { Consola } from 'consola' -import { consola } from 'consola' -import { checkEngines } from './utils/engines' -import type { Command, NuxtCommand } from './commands' -import { commands } from './commands' -import { showHelp } from './utils/help' -import { showBanner } from './utils/banner' - -async function _main () { - const _argv = (process.env.__CLI_ARGV__ ? JSON.parse(process.env.__CLI_ARGV__) : process.argv).slice(2) - const args = mri(_argv, { - boolean: [ - 'no-clear' - ] - }) - // @ts-ignore - const command = args._.shift() || 'usage' - - showBanner(command === 'dev' && args.clear !== false && !args.help) - - if (!(command in commands)) { - console.log('\n' + red('Invalid command ' + command)) - - await commands.usage().then(r => r.invoke()) - process.exit(1) - } - - // Check Node.js version in background - setTimeout(() => { checkEngines().catch(() => {}) }, 1000) - - // @ts-ignore default.default is hotfix for #621 - const cmd = await commands[command as Command]() as NuxtCommand - if (args.h || args.help) { - showHelp(cmd.meta) - } else { - const result = await cmd.invoke(args) - return result - } -} - -// Wrap all console logs with consola for better DX -consola.wrapAll() - -// Filter out unwanted logs -// TODO: Use better API from consola for intercepting logs -// TODO: export type from Consola -const wrapReporter = (reporter: Consola['_reporters'][0]) => { - log (logObj, ctx) { - if (!logObj.args || !logObj.args.length) { return } - const msg = logObj.args[0] - if (typeof msg === 'string' && !process.env.DEBUG) { - // Hide vue-router 404 warnings - if (msg.startsWith('[Vue Router warn]: No match found for location with path')) { - return - } - // Hide sourcemap warnings related to node_modules - if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { - return - } - } - return reporter.log(logObj, ctx) - } -} -consola._reporters = consola._reporters.map(wrapReporter) - -process.on('unhandledRejection', err => consola.error('[unhandledRejection]', err)) -process.on('uncaughtException', err => consola.error('[uncaughtException]', err)) - -export function main () { - _main() - .then((result) => { - if (result === 'error') { - process.exit(1) - } else if (result !== 'wait') { - process.exit() - } - }) - .catch((error) => { - consola.error(error) - process.exit(1) - }) -} diff --git a/packages/nuxi/src/commands/add.ts b/packages/nuxi/src/commands/add.ts deleted file mode 100644 index 1b795f4720..0000000000 --- a/packages/nuxi/src/commands/add.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { existsSync, promises as fsp } from 'node:fs' -import { dirname, resolve } from 'pathe' -import { consola } from 'consola' -import { loadKit } from '../utils/kit' -import { templates } from '../utils/templates' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'add', - usage: `npx nuxi add [--cwd] [--force] ${Object.keys(templates).join('|')} `, - description: 'Create a new template file.' - }, - async invoke (args) { - const cwd = resolve(args.cwd || '.') - - const template = args._[0] - const name = args._[1] - - // Validate template name - if (!templates[template]) { - consola.error(`Template ${template} is not supported. Possible values: ${Object.keys(templates).join(', ')}`) - process.exit(1) - } - - // Validate options - if (!name) { - consola.error('name argument is missing!') - process.exit(1) - } - - // Load config in order to respect srcDir - const kit = await loadKit(cwd) - const config = await kit.loadNuxtConfig({ cwd }) - - // Resolve template - const res = templates[template]({ name, args }) - - // Resolve full path to generated file - const path = resolve(config.srcDir, res.path) - - // Ensure not overriding user code - if (!args.force && existsSync(path)) { - consola.error(`File exists: ${path} . Use --force to override or use a different name.`) - process.exit(1) - } - - // Ensure parent directory exists - const parentDir = dirname(path) - if (!existsSync(parentDir)) { - consola.info('Creating directory', parentDir) - if (template === 'page') { - consola.info('This enables vue-router functionality!') - } - await fsp.mkdir(parentDir, { recursive: true }) - } - - // Write file - await fsp.writeFile(path, res.contents.trim() + '\n') - consola.info(`🪄 Generated a new ${template} in ${path}`) - } -}) diff --git a/packages/nuxi/src/commands/analyze.ts b/packages/nuxi/src/commands/analyze.ts deleted file mode 100644 index f450897a79..0000000000 --- a/packages/nuxi/src/commands/analyze.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { promises as fsp } from 'node:fs' -import { join, resolve } from 'pathe' -import { createApp, eventHandler, lazyEventHandler, toNodeListener } from 'h3' -import { listen } from 'listhen' -import { writeTypes } from '../utils/prepare' -import { loadKit } from '../utils/kit' -import { clearDir } from '../utils/fs' -import { overrideEnv } from '../utils/env' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'analyze', - usage: 'npx nuxi analyze [--log-level] [rootDir]', - description: 'Build nuxt and analyze production bundle (experimental)' - }, - async invoke (args) { - overrideEnv('production') - - const rootDir = resolve(args._[0] || '.') - const statsDir = join(rootDir, '.nuxt/stats') - - const { loadNuxt, buildNuxt } = await loadKit(rootDir) - - const nuxt = await loadNuxt({ - rootDir, - overrides: { - build: { analyze: true }, - logLevel: args['log-level'] - } - }) - - await clearDir(nuxt.options.buildDir) - await writeTypes(nuxt) - await buildNuxt(nuxt) - - const app = createApp() - - const serveFile = (filePath: string) => lazyEventHandler(async () => { - const contents = await fsp.readFile(filePath, 'utf-8') - return eventHandler((event) => { event.node.res.end(contents) }) - }) - - console.warn('Do not deploy analyze results! Use `nuxi build` before deploying.') - - console.info('Starting stats server...') - - app.use('/client', serveFile(join(statsDir, 'client.html'))) - app.use('/nitro', serveFile(join(statsDir, 'nitro.html'))) - app.use(eventHandler(() => ` - - - - Nuxt Bundle Stats (experimental) - -

Nuxt Bundle Stats (experimental)

- - - `)) - - await listen(toNodeListener(app)) - - return 'wait' as const - } -}) diff --git a/packages/nuxi/src/commands/build-module.ts b/packages/nuxi/src/commands/build-module.ts deleted file mode 100644 index e13e4cd675..0000000000 --- a/packages/nuxi/src/commands/build-module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { execa } from 'execa' -import { consola } from 'consola' -import { resolve } from 'pathe' -import { tryResolveModule } from '../utils/esm' -import { defineNuxtCommand } from './index' - -const MODULE_BUILDER_PKG = '@nuxt/module-builder' - -export default defineNuxtCommand({ - meta: { - name: 'build-module', - usage: 'npx nuxi build-module [--stub] [rootDir]', - description: `Helper command for using ${MODULE_BUILDER_PKG}` - }, - async invoke (args) { - // Find local installed version - const rootDir = resolve(args._[0] || '.') - const hasLocal = await tryResolveModule(`${MODULE_BUILDER_PKG}/package.json`, rootDir) - - const execArgs = Object.entries({ - '--stub': args.stub - }).filter(([, value]) => value).map(([key]) => key) - - let cmd = 'nuxt-module-build' - if (!hasLocal) { - consola.warn(`Cannot find locally installed version of \`${MODULE_BUILDER_PKG}\` (>=0.2.0). Falling back to \`npx ${MODULE_BUILDER_PKG}\``) - cmd = 'npx' - execArgs.unshift(MODULE_BUILDER_PKG) - } - - await execa(cmd, execArgs, { preferLocal: true, stdio: 'inherit', cwd: rootDir }) - } -}) diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts deleted file mode 100644 index dc2c2ab545..0000000000 --- a/packages/nuxi/src/commands/build.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { relative, resolve } from 'pathe' -import { consola } from 'consola' -import { writeTypes } from '../utils/prepare' -import { loadKit } from '../utils/kit' -import { clearDir } from '../utils/fs' -import { overrideEnv } from '../utils/env' -import { showVersions } from '../utils/banner' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'build', - usage: 'npx nuxi build [--prerender] [--dotenv] [--log-level] [rootDir]', - description: 'Build nuxt for production deployment' - }, - async invoke (args) { - overrideEnv('production') - - const rootDir = resolve(args._[0] || '.') - showVersions(rootDir) - - const { loadNuxt, buildNuxt, useNitro } = await loadKit(rootDir) - - const nuxt = await loadNuxt({ - rootDir, - dotenv: { - cwd: rootDir, - fileName: args.dotenv - }, - overrides: { - logLevel: args['log-level'], - _generate: args.prerender - } - }) - - // Use ? for backward compatibility for Nuxt <= RC.10 - const nitro = useNitro?.() - - await clearDir(nuxt.options.buildDir) - - await writeTypes(nuxt) - - nuxt.hook('build:error', (err) => { - consola.error('Nuxt Build Error:', err) - process.exit(1) - }) - - await buildNuxt(nuxt) - - if (args.prerender) { - if (!nuxt.options.ssr) { - consola.warn('HTML content not prerendered because `ssr: false` was set. You can read more in `https://nuxt.com/docs/getting-started/deployment#static-hosting`.') - } - // TODO: revisit later if/when nuxt build --prerender will output hybrid - const dir = nitro?.options.output.publicDir - const publicDir = dir ? relative(process.cwd(), dir) : '.output/public' - consola.success(`You can now deploy \`${publicDir}\` to any static hosting!`) - } - } -}) diff --git a/packages/nuxi/src/commands/cleanup.ts b/packages/nuxi/src/commands/cleanup.ts deleted file mode 100644 index 46a257aac3..0000000000 --- a/packages/nuxi/src/commands/cleanup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { resolve } from 'pathe' -import { cleanupNuxtDirs } from '../utils/nuxt' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'cleanup', - usage: 'npx nuxi clean|cleanup', - description: 'Cleanup generated nuxt files and caches' - }, - async invoke (args) { - const rootDir = resolve(args._[0] || '.') - await cleanupNuxtDirs(rootDir) - } -}) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts deleted file mode 100644 index 8cbd6f8505..0000000000 --- a/packages/nuxi/src/commands/dev.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type { AddressInfo } from 'node:net' -import type { RequestListener } from 'node:http' -import { relative, resolve } from 'pathe' -import chokidar from 'chokidar' -import { debounce } from 'perfect-debounce' -import type { Nuxt } from '@nuxt/schema' -import { consola } from 'consola' -import { withTrailingSlash } from 'ufo' -import { setupDotenv } from 'c12' -import { showBanner, showVersions } from '../utils/banner' -import { writeTypes } from '../utils/prepare' -import { loadKit } from '../utils/kit' -import { importModule } from '../utils/esm' -import { overrideEnv } from '../utils/env' -import { cleanupNuxtDirs, loadNuxtManifest, writeNuxtManifest } from '../utils/nuxt' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'dev', - usage: 'npx nuxi dev [rootDir] [--dotenv] [--log-level] [--clipboard] [--open, -o] [--port, -p] [--host, -h] [--https] [--ssl-cert] [--ssl-key]', - description: 'Run nuxt development server' - }, - async invoke (args, options = {}) { - overrideEnv('development') - - const { listen } = await import('listhen') - const { toNodeListener } = await import('h3') - let currentHandler: RequestListener | undefined - let loadingMessage = 'Nuxt is starting...' - const loadingHandler: RequestListener = async (_req, res) => { - const { loading: loadingTemplate } = await importModule('@nuxt/ui-templates') - res.setHeader('Content-Type', 'text/html; charset=UTF-8') - res.statusCode = 503 // Service Unavailable - res.end(loadingTemplate({ loading: loadingMessage })) - } - const serverHandler: RequestListener = (req, res) => { - return currentHandler ? currentHandler(req, res) : loadingHandler(req, res) - } - - const rootDir = resolve(args._[0] || '.') - showVersions(rootDir) - - await setupDotenv({ cwd: rootDir, fileName: args.dotenv }) - - const { loadNuxt, loadNuxtConfig, buildNuxt } = await loadKit(rootDir) - - const config = await loadNuxtConfig({ - cwd: rootDir, - overrides: { - dev: true, - logLevel: args['log-level'], - ...(options.overrides || {}) - } - }) - - const listener = await listen(serverHandler, { - showURL: false, - clipboard: args.clipboard, - open: args.open || args.o, - port: args.port || args.p || process.env.NUXT_PORT || config.devServer.port, - hostname: args.host || args.h || process.env.NUXT_HOST || config.devServer.host, - https: (args.https !== false && (args.https || config.devServer.https)) - ? { - cert: args['ssl-cert'] || (config.devServer.https && config.devServer.https.cert) || undefined, - key: args['ssl-key'] || (config.devServer.https && config.devServer.https.key) || undefined - } - : false - }) - - let currentNuxt: Nuxt - let distWatcher: chokidar.FSWatcher - - const showURL = () => { - listener.showURL({ - // TODO: Normalize URL with trailing slash within schema - baseURL: withTrailingSlash(currentNuxt?.options.app.baseURL) || '/' - }) - } - const load = async (isRestart: boolean, reason?: string) => { - try { - loadingMessage = `${reason ? reason + '. ' : ''}${isRestart ? 'Restarting' : 'Starting'} nuxt...` - currentHandler = undefined - if (isRestart) { - consola.info(loadingMessage) - } - if (currentNuxt) { - await currentNuxt.close() - } - if (distWatcher) { - await distWatcher.close() - } - - currentNuxt = await loadNuxt({ - rootDir, - dev: true, - ready: false, - overrides: { - logLevel: args['log-level'], - ...(options.overrides || {}) - } - }) - - if (!isRestart) { - showURL() - } - - // Write manifest and also check if we need cache invalidation - if (!isRestart) { - const previousManifest = await loadNuxtManifest(currentNuxt.options.buildDir) - const newManifest = await writeNuxtManifest(currentNuxt) - if (previousManifest && newManifest && previousManifest._hash !== newManifest._hash) { - await cleanupNuxtDirs(currentNuxt.options.rootDir) - } - } - - await currentNuxt.ready() - - distWatcher = chokidar.watch(resolve(currentNuxt.options.buildDir, 'dist'), { ignoreInitial: true, depth: 0 }) - distWatcher.on('unlinkDir', () => { - dLoad(true, '.nuxt/dist directory has been removed') - }) - - const unsub = currentNuxt.hooks.hook('restart', async (options) => { - unsub() // we use this instead of `hookOnce` for Nuxt Bridge support - if (options?.hard && process.send) { - await listener.close().catch(() => {}) - await currentNuxt.close().catch(() => {}) - await watcher.close().catch(() => {}) - await distWatcher.close().catch(() => {}) - process.send({ type: 'nuxt:restart' }) - } else { - await load(true) - } - }) - - await currentNuxt.hooks.callHook('listen', listener.server, listener) - const address = (listener.server.address() || {}) as AddressInfo - currentNuxt.options.devServer.url = listener.url - currentNuxt.options.devServer.port = address.port - currentNuxt.options.devServer.host = address.address - currentNuxt.options.devServer.https = listener.https - - await Promise.all([ - writeTypes(currentNuxt).catch(console.error), - buildNuxt(currentNuxt) - ]) - currentHandler = toNodeListener(currentNuxt.server.app) - if (isRestart && args.clear !== false) { - showBanner() - showURL() - } - } catch (err) { - consola.error(`Cannot ${isRestart ? 'restart' : 'start'} nuxt: `, err) - currentHandler = undefined - loadingMessage = 'Error while loading nuxt. Please check console and fix errors.' - } - } - - // Watch for config changes - // TODO: Watcher service, modules, and requireTree - const dLoad = debounce(load) - const watcher = chokidar.watch([rootDir], { ignoreInitial: true, depth: 0 }) - watcher.on('all', (_event, _file) => { - const file = relative(rootDir, _file) - if (file.match(/^(nuxt\.config\.(js|ts|mjs|cjs)|\.nuxtignore|\.env|\.nuxtrc)$/)) { - dLoad(true, `${file} updated`) - } - }) - - await load(false) - - return 'wait' as const - } -}) diff --git a/packages/nuxi/src/commands/devtools.ts b/packages/nuxi/src/commands/devtools.ts deleted file mode 100644 index 2abb64cb10..0000000000 --- a/packages/nuxi/src/commands/devtools.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { resolve } from 'pathe' -import { execa } from 'execa' -import { showHelp } from '../utils/help' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'enable', - usage: 'npx nuxi devtools enable|disable [rootDir]', - description: 'Enable or disable features in a Nuxt project' - }, - async invoke (args) { - const [command, _rootDir = '.'] = args._ - const rootDir = resolve(_rootDir) - - if (!['enable', 'disable'].includes(command)) { - console.error(`Unknown command \`${command}\`.`) - showHelp(this.meta) - process.exit(1) - } - - await execa('npx', ['@nuxt/devtools-wizard', command, rootDir], { stdio: 'inherit', cwd: rootDir }) - } -}) diff --git a/packages/nuxi/src/commands/generate.ts b/packages/nuxi/src/commands/generate.ts deleted file mode 100644 index cb344e878a..0000000000 --- a/packages/nuxi/src/commands/generate.ts +++ /dev/null @@ -1,14 +0,0 @@ -import buildCommand from './build' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'generate', - usage: 'npx nuxi generate [rootDir] [--dotenv]', - description: 'Build Nuxt and prerender static routes' - }, - async invoke (args) { - args.prerender = true - await buildCommand.invoke(args) - } -}) diff --git a/packages/nuxi/src/commands/index.ts b/packages/nuxi/src/commands/index.ts deleted file mode 100644 index 36019a785c..0000000000 --- a/packages/nuxi/src/commands/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Argv } from 'mri' - -const _rDefault = (r: any) => r.default || r - -export const commands = { - dev: () => import('./dev').then(_rDefault), - build: () => import('./build').then(_rDefault), - 'build-module': () => import('./build-module').then(_rDefault), - cleanup: () => import('./cleanup').then(_rDefault), - clean: () => import('./cleanup').then(_rDefault), - preview: () => import('./preview').then(_rDefault), - start: () => import('./preview').then(_rDefault), - analyze: () => import('./analyze').then(_rDefault), - generate: () => import('./generate').then(_rDefault), - prepare: () => import('./prepare').then(_rDefault), - typecheck: () => import('./typecheck').then(_rDefault), - usage: () => import('./usage').then(_rDefault), - info: () => import('./info').then(_rDefault), - init: () => import('./init').then(_rDefault), - create: () => import('./init').then(_rDefault), - devtools: () => import('./devtools').then(_rDefault), - upgrade: () => import('./upgrade').then(_rDefault), - test: () => import('./test').then(_rDefault), - add: () => import('./add').then(_rDefault), - new: () => import('./add').then(_rDefault) -} - -export type Command = keyof typeof commands - -export interface NuxtCommandMeta { - name: string; - usage: string; - description: string; - [key: string]: any; -} - -export type CLIInvokeResult = void | 'error' | 'wait' - -export interface NuxtCommand { - invoke(args: Argv, options?: Record): Promise | CLIInvokeResult - meta: NuxtCommandMeta -} - -export function defineNuxtCommand (command: NuxtCommand): NuxtCommand { - return command -} diff --git a/packages/nuxi/src/commands/info.ts b/packages/nuxi/src/commands/info.ts deleted file mode 100644 index 4e3dca2cab..0000000000 --- a/packages/nuxi/src/commands/info.ts +++ /dev/null @@ -1,160 +0,0 @@ -import os from 'node:os' -import { existsSync, readFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { resolve } from 'pathe' -import jiti from 'jiti' -import destr from 'destr' -import { splitByCase } from 'scule' -import clipboardy from 'clipboardy' -import type { NuxtModule } from '@nuxt/schema' -import { getPackageManager, getPackageManagerVersion } from '../utils/packageManagers' -import { findup } from '../utils/fs' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'info', - usage: 'npx nuxi info [rootDir]', - description: 'Get information about nuxt project' - }, - async invoke (args) { - // Resolve rootDir - const rootDir = resolve(args._[0] || '.') - - // Load nuxt.config - const nuxtConfig = getNuxtConfig(rootDir) - - // Find nearest package.json - const { dependencies = {}, devDependencies = {} } = findPackage(rootDir) - - // Utils to query a dependency version - const getDepVersion = (name: string) => getPkg(name, rootDir)?.version || dependencies[name] || devDependencies[name] - - const listModules = (arr = []) => arr - .map(m => normalizeConfigModule(m, rootDir)) - .filter(Boolean) - .map((name) => { - const npmName = name!.split('/').splice(0, 2).join('/') // @foo/bar/baz => @foo/bar - const v = getDepVersion(npmName) - return '`' + (v ? `${name}@${v}` : name) + '`' - }) - .join(', ') - - // Check nuxt version - const nuxtVersion = getDepVersion('nuxt') || getDepVersion('nuxt-edge') || getDepVersion('nuxt3') || '0.0.0' - const isNuxt3 = nuxtVersion.startsWith('3') - const builder = isNuxt3 - ? nuxtConfig.builder /* latest schema */ || (nuxtConfig.vite !== false ? 'vite' : 'webpack') /* previous schema */ - : nuxtConfig.bridge?.vite - ? 'vite' /* bridge vite implementation */ - : (nuxtConfig.buildModules?.includes('nuxt-vite') - ? 'vite' /* nuxt-vite */ - : 'webpack') - - let packageManager = getPackageManager(rootDir) - if (packageManager) { - packageManager += '@' + getPackageManagerVersion(packageManager) - } else { - // @ts-expect-error - packageManager = 'unknown' - } - - const infoObj = { - OperatingSystem: os.type(), - NodeVersion: process.version, - NuxtVersion: nuxtVersion, - NitroVersion: getDepVersion('nitropack'), - PackageManager: packageManager, - Builder: builder, - UserConfig: Object.keys(nuxtConfig).map(key => '`' + key + '`').join(', '), - RuntimeModules: listModules(nuxtConfig.modules), - BuildModules: listModules(nuxtConfig.buildModules || []) - } - - console.log('RootDir:', rootDir) - - let maxLength = 0 - const entries = Object.entries(infoObj).map(([key, val]) => { - const label = splitByCase(key).join(' ') - if (label.length > maxLength) { maxLength = label.length } - return [label, val || '-'] - }) - let infoStr = '' - for (const [label, value] of entries) { - infoStr += '- ' + (label + ': ').padEnd(maxLength + 2) + (value.includes('`') ? value : '`' + value + '`') + '\n' - } - - const copied = await clipboardy.write(infoStr).then(() => true).catch(() => false) - const splitter = '------------------------------' - console.log(`Nuxt project info: ${copied ? '(copied to clipboard)' : ''}\n\n${splitter}\n${infoStr}${splitter}\n`) - - const isNuxt3OrBridge = infoObj.NuxtVersion.startsWith('3') || infoObj.BuildModules.includes('bridge') - console.log([ - '👉 Report an issue: https://github.com/nuxt/nuxt/issues/new', - '👉 Suggest an improvement: https://github.com/nuxt/nuxt/discussions/new', - `👉 Read documentation: ${isNuxt3OrBridge ? 'https://nuxt.com' : 'https://nuxtjs.org'}` - ].join('\n\n') + '\n') - } -}) - -function normalizeConfigModule (module: NuxtModule | string | null | undefined, rootDir: string): string | null { - if (!module) { - return null - } - if (typeof module === 'string') { - return module - .split(rootDir).pop()! // Strip rootDir - .split('node_modules').pop()! // Strip node_modules - .replace(/^\//, '') - } - if (typeof module === 'function') { - return `${module.name}()` - } - if (Array.isArray(module)) { - return normalizeConfigModule(module[0], rootDir) - } - return null -} - -function getNuxtConfig (rootDir: string) { - try { - (globalThis as any).defineNuxtConfig = (c: any) => c - const result = jiti(rootDir, { interopDefault: true, esmResolve: true })('./nuxt.config') - delete (globalThis as any).defineNuxtConfig - return result - } catch (err) { - // TODO: Show error as warning if it is not 404 - return {} - } -} - -function getPkg (name: string, rootDir: string) { - // Assume it is in {rootDir}/node_modules/${name}/package.json - let pkgPath = resolve(rootDir, 'node_modules', name, 'package.json') - - // Try to resolve for more accuracy - const _require = createRequire(rootDir) - try { pkgPath = _require.resolve(name + '/package.json') } catch (_err) { - // console.log('not found:', name) - } - - return readJSONSync(pkgPath) -} - -function findPackage (rootDir: string) { - return findup(rootDir, (dir) => { - const p = resolve(dir, 'package.json') - if (existsSync(p)) { - return readJSONSync(p) - } - }) || {} -} - -function readJSONSync (filePath: string) { - try { - return destr(readFileSync(filePath, 'utf-8')) - } catch (err) { - // TODO: Warn error - return null - } -} diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts deleted file mode 100644 index 26cf9beaff..0000000000 --- a/packages/nuxi/src/commands/init.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { writeFile } from 'node:fs/promises' -import { downloadTemplate, startShell } from 'giget' -import { relative } from 'pathe' -import { consola } from 'consola' -import { defineNuxtCommand } from './index' - -const rpath = (p: string) => relative(process.cwd(), p) - -const DEFAULT_REGISTRY = 'https://raw.githubusercontent.com/nuxt/starter/templates/templates' - -export default defineNuxtCommand({ - meta: { - name: 'init', - usage: 'npx nuxi init|create [--template,-t] [--force] [--offline] [--prefer-offline] [--shell] [dir]', - description: 'Initialize a fresh project' - }, - async invoke (args) { - // Clone template - const template = args.template || args.t || 'v3' - - if (typeof template === 'boolean') { - consola.error('Please specify a template!') - process.exit(1) - } - - let t - - try { - t = await downloadTemplate(template, { - dir: args._[0] as string, - force: args.force, - offline: args.offline, - preferOffline: args['prefer-offline'], - registry: process.env.NUXI_INIT_REGISTRY || DEFAULT_REGISTRY - }) - } catch (err) { - if (process.env.DEBUG) { - throw err - } - consola.error((err as Error).toString()) - process.exit(1) - } - - // Show next steps - const relativeDist = rpath(t.dir) - - // Write .nuxtrc with `shamefully-hoist=true` for pnpm - const usingPnpm = (process.env.npm_config_user_agent || '').includes('pnpm') - if (usingPnpm) { - await writeFile(`${relativeDist}/.npmrc`, 'shamefully-hoist=true') - } - - const nextSteps = [ - !args.shell && relativeDist.length > 1 && `\`cd ${relativeDist}\``, - 'Install dependencies with `npm install` or `yarn install` or `pnpm install`', - 'Start development server with `npm run dev` or `yarn dev` or `pnpm run dev`' - ].filter(Boolean) - - consola.log(`✨ Nuxt project is created with \`${t.name}\` template. Next steps:`) - for (const step of nextSteps) { - consola.log(` › ${step}`) - } - - if (args.shell) { - startShell(t.dir) - } - } -}) diff --git a/packages/nuxi/src/commands/prepare.ts b/packages/nuxi/src/commands/prepare.ts deleted file mode 100644 index 121dad7873..0000000000 --- a/packages/nuxi/src/commands/prepare.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { buildNuxt } from '@nuxt/kit' -import { relative, resolve } from 'pathe' -import { consola } from 'consola' -import { clearDir } from '../utils/fs' -import { loadKit } from '../utils/kit' -import { writeTypes } from '../utils/prepare' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'prepare', - usage: 'npx nuxi prepare [--log-level] [rootDir]', - description: 'Prepare nuxt for development/build' - }, - async invoke (args) { - process.env.NODE_ENV = process.env.NODE_ENV || 'production' - const rootDir = resolve(args._[0] || '.') - - const { loadNuxt } = await loadKit(rootDir) - const nuxt = await loadNuxt({ - rootDir, - overrides: { - _prepare: true, - logLevel: args['log-level'] - } - }) - await clearDir(nuxt.options.buildDir) - - await buildNuxt(nuxt) - await writeTypes(nuxt) - consola.success('Types generated in', relative(process.cwd(), nuxt.options.buildDir)) - } -}) diff --git a/packages/nuxi/src/commands/preview.ts b/packages/nuxi/src/commands/preview.ts deleted file mode 100644 index de176b058d..0000000000 --- a/packages/nuxi/src/commands/preview.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { existsSync, promises as fsp } from 'node:fs' -import { dirname, relative } from 'node:path' -import { execa } from 'execa' -import { setupDotenv } from 'c12' -import { resolve } from 'pathe' -import { consola } from 'consola' -import { loadKit } from '../utils/kit' - -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'preview', - usage: 'npx nuxi preview|start [--dotenv] [rootDir]', - description: 'Launches nitro server for local testing after `nuxi build`.' - }, - async invoke (args) { - process.env.NODE_ENV = process.env.NODE_ENV || 'production' - const rootDir = resolve(args._[0] || '.') - const { loadNuxtConfig } = await loadKit(rootDir) - const config = await loadNuxtConfig({ cwd: rootDir }) - - const resolvedOutputDir = resolve(config.srcDir || rootDir, config.nitro.srcDir || 'server', config.nitro.output?.dir || '.output', 'nitro.json') - const defaultOutput = resolve(rootDir, '.output', 'nitro.json') // for backwards compatibility - - const nitroJSONPaths = [resolvedOutputDir, defaultOutput] - const nitroJSONPath = nitroJSONPaths.find(p => existsSync(p)) - if (!nitroJSONPath) { - consola.error('Cannot find `nitro.json`. Did you run `nuxi build` first? Search path:\n', nitroJSONPaths) - process.exit(1) - } - const outputPath = dirname(nitroJSONPath) - const nitroJSON = JSON.parse(await fsp.readFile(nitroJSONPath, 'utf-8')) - - consola.info('Node.js version:', process.versions.node) - consola.info('Preset:', nitroJSON.preset) - consola.info('Working dir:', relative(process.cwd(), outputPath)) - - if (!nitroJSON.commands.preview) { - consola.error('Preview is not supported for this build.') - process.exit(1) - } - - const envExists = args.dotenv ? existsSync(resolve(rootDir, args.dotenv)) : existsSync(rootDir) - if (envExists) { - consola.info('Loading `.env`. This will not be loaded when running the server in production.') - await setupDotenv({ cwd: rootDir, fileName: args.dotenv }) - } - - consola.info('Starting preview command:', nitroJSON.commands.preview) - const [command, ...commandArgs] = nitroJSON.commands.preview.split(' ') - consola.log('') - await execa(command, commandArgs, { stdio: 'inherit', cwd: outputPath }) - } -}) diff --git a/packages/nuxi/src/commands/test.ts b/packages/nuxi/src/commands/test.ts deleted file mode 100644 index 1761dc634f..0000000000 --- a/packages/nuxi/src/commands/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { resolve } from 'pathe' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'test', - usage: 'npx nuxi test [--dev] [--watch] [rootDir]', - description: 'Run tests' - }, - async invoke (args) { - process.env.NODE_ENV = process.env.NODE_ENV || 'test' - const rootDir = resolve(args._[0] || '.') - const { runTests } = await importTestUtils() - await runTests({ - rootDir, - dev: !!args.dev, - watch: !!args.watch - }) - - if (args.watch) { - return 'wait' as const - } - } -}) - -async function importTestUtils (): Promise { - let err - for (const pkg of ['@nuxt/test-utils-edge', '@nuxt/test-utils']) { - try { - const exports = await import(pkg) - // Detect old @nuxt/test-utils - if (!exports.runTests) { - throw new Error('Invalid version of `@nuxt/test-utils` is installed!') - } - return exports - } catch (_err) { - err = _err - } - } - console.error(err) - throw new Error('`@nuxt/test-utils-edge` seems missing. Run `npm i -D @nuxt/test-utils-edge` or `yarn add -D @nuxt/test-utils-edge` to install.') -} diff --git a/packages/nuxi/src/commands/typecheck.ts b/packages/nuxi/src/commands/typecheck.ts deleted file mode 100644 index afa901da5b..0000000000 --- a/packages/nuxi/src/commands/typecheck.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { execa } from 'execa' -import { resolve } from 'pathe' -import { tryResolveModule } from '../utils/esm' - -import { loadKit } from '../utils/kit' -import { writeTypes } from '../utils/prepare' -import { defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'typecheck', - usage: 'npx nuxi typecheck [--log-level] [rootDir]', - description: 'Runs `vue-tsc` to check types throughout your app.' - }, - async invoke (args) { - process.env.NODE_ENV = process.env.NODE_ENV || 'production' - const rootDir = resolve(args._[0] || '.') - - const { loadNuxt, buildNuxt } = await loadKit(rootDir) - const nuxt = await loadNuxt({ - rootDir, - overrides: { - _prepare: true, - logLevel: args['log-level'] - } - }) - - // Generate types and build nuxt instance - await writeTypes(nuxt) - await buildNuxt(nuxt) - await nuxt.close() - - // Prefer local install if possible - const hasLocalInstall = await tryResolveModule('typescript', rootDir) && await tryResolveModule('vue-tsc/package.json', rootDir) - if (hasLocalInstall) { - await execa('vue-tsc', ['--noEmit'], { preferLocal: true, stdio: 'inherit', cwd: rootDir }) - } else { - await execa('npx', '-p vue-tsc -p typescript vue-tsc --noEmit'.split(' '), { stdio: 'inherit', cwd: rootDir }) - } - } -}) diff --git a/packages/nuxi/src/commands/upgrade.ts b/packages/nuxi/src/commands/upgrade.ts deleted file mode 100644 index d229cbf3f4..0000000000 --- a/packages/nuxi/src/commands/upgrade.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { execSync } from 'node:child_process' -import { consola } from 'consola' -import { resolve } from 'pathe' -import { readPackageJSON } from 'pkg-types' -import { getPackageManager, packageManagerLocks } from '../utils/packageManagers' -import { rmRecursive, touchFile } from '../utils/fs' -import { cleanupNuxtDirs, nuxtVersionToGitIdentifier } from '../utils/nuxt' -import { defineNuxtCommand } from './index' - -async function getNuxtVersion (path: string): Promise { - try { - const pkg = await readPackageJSON('nuxt', { url: path }) - if (!pkg.version) { - consola.warn('Cannot find any installed nuxt versions in ', path) - } - return pkg.version || null - } catch { - return null - } -} - -export default defineNuxtCommand({ - meta: { - name: 'upgrade', - usage: 'npx nuxi upgrade [--force|-f]', - description: 'Upgrade nuxt' - }, - async invoke (args) { - const rootDir = resolve(args._[0] || '.') - - // Check package manager - const packageManager = getPackageManager(rootDir) - if (!packageManager) { - console.error('Cannot detect Package Manager in', rootDir) - process.exit(1) - } - const packageManagerVersion = execSync(`${packageManager} --version`).toString('utf8').trim() - consola.info('Package Manager:', packageManager, packageManagerVersion) - - // Check currently installed nuxt version - const currentVersion = await getNuxtVersion(rootDir) || '[unknown]' - consola.info('Current nuxt version:', currentVersion) - - // Force install - if (args.force || args.f) { - consola.info('Removing lock-file and node_modules...') - const pmLockFile = resolve(rootDir, packageManagerLocks[packageManager]) - await rmRecursive([pmLockFile, resolve(rootDir, 'node_modules')]) - await touchFile(pmLockFile) - } - - // Install latest version - consola.info('Installing latest Nuxt 3 release...') - execSync(`${packageManager} ${packageManager === 'yarn' ? 'add' : 'install'} -D nuxt`, { stdio: 'inherit', cwd: rootDir }) - - // Cleanup after upgrade - await cleanupNuxtDirs(rootDir) - - // Check installed nuxt version again - const upgradedVersion = await getNuxtVersion(rootDir) || '[unknown]' - consola.info('Upgraded nuxt version:', upgradedVersion) - - if (upgradedVersion === currentVersion) { - consola.success('You\'re already using the latest version of nuxt.') - } else { - consola.success('Successfully upgraded nuxt from', currentVersion, 'to', upgradedVersion) - const commitA = nuxtVersionToGitIdentifier(currentVersion) - const commitB = nuxtVersionToGitIdentifier(upgradedVersion) - if (commitA && commitB) { - consola.info('Changelog:', `https://github.com/nuxt/nuxt/compare/${commitA}...${commitB}`) - } - } - } -}) diff --git a/packages/nuxi/src/commands/usage.ts b/packages/nuxi/src/commands/usage.ts deleted file mode 100644 index 2aad941a18..0000000000 --- a/packages/nuxi/src/commands/usage.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { cyan } from 'colorette' -import { showHelp } from '../utils/help' -import { commands, defineNuxtCommand } from './index' - -export default defineNuxtCommand({ - meta: { - name: 'help', - usage: 'nuxt help', - description: 'Show help' - }, - invoke (_args) { - const sections: string[] = [] - - sections.push(`Usage: ${cyan(`npx nuxi ${Object.keys(commands).join('|')} [args]`)}`) - - console.log(sections.join('\n\n') + '\n') - - // Reuse the same wording as in `-h` commands - showHelp({}) - } -}) diff --git a/packages/nuxi/src/index.ts b/packages/nuxi/src/index.ts deleted file mode 100755 index b8492c1b01..0000000000 --- a/packages/nuxi/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './run' diff --git a/packages/nuxi/src/run.ts b/packages/nuxi/src/run.ts deleted file mode 100644 index 6640fe6b7d..0000000000 --- a/packages/nuxi/src/run.ts +++ /dev/null @@ -1,13 +0,0 @@ -import mri from 'mri' -import type { Command, NuxtCommand } from './commands' -import { commands } from './commands' - -export async function runCommand (command: string, argv = process.argv.slice(2), options: Record = {}) { - const args = mri(argv) - args.clear = false // used by dev - const cmd = await commands[command as Command]() as NuxtCommand - if (!cmd) { - throw new Error(`Invalid command ${command}`) - } - await cmd.invoke(args, options) -} diff --git a/packages/nuxi/src/utils/banner.ts b/packages/nuxi/src/utils/banner.ts deleted file mode 100644 index 3617a4c44d..0000000000 --- a/packages/nuxi/src/utils/banner.ts +++ /dev/null @@ -1,21 +0,0 @@ -import clear from 'clear' -import { bold, gray, green } from 'colorette' -import { version } from '../../package.json' -import { tryRequireModule } from './cjs' - -export function showBanner (_clear?: boolean) { - if (_clear) { clear() } - console.log(gray(`Nuxi ${(bold(version))}`)) -} - -export function showVersions (cwd: string) { - const getPkgVersion = (pkg: string) => { - return tryRequireModule(`${pkg}/package.json`, cwd)?.version || '' - } - const nuxtVersion = getPkgVersion('nuxt') || getPkgVersion('nuxt-edge') - const nitroVersion = getPkgVersion('nitropack') - console.log(gray( - green(`Nuxt ${bold(nuxtVersion)}`) + - (nitroVersion ? ` with Nitro ${(bold(nitroVersion))}` : '') - )) -} diff --git a/packages/nuxi/src/utils/cjs.ts b/packages/nuxi/src/utils/cjs.ts deleted file mode 100644 index b0233b61bc..0000000000 --- a/packages/nuxi/src/utils/cjs.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createRequire } from 'node:module' -import { dirname, normalize } from 'pathe' - -export function getModulePaths (paths?: string | string[]): string[] { - return ([] as Array) - .concat( - // @ts-expect-error global object - global.__NUXT_PREPATHS__, - paths, - process.cwd(), - // @ts-expect-error global object - global.__NUXT_PATHS__ - ) - .filter(Boolean) as string[] -} - -const _require = createRequire(process.cwd()) - -export function resolveModule (id: string, paths?: string | string[]) { - return normalize(_require.resolve(id, { paths: getModulePaths(paths) })) -} - -export function requireModule (id: string, paths?: string | string[]) { - return _require(resolveModule(id, paths)) -} - -export function tryRequireModule (id: string, paths?: string | string[]) { - try { return requireModule(id, paths) } catch { return null } -} - -export function getNearestPackage (id: string, paths?: string | string[]) { - while (dirname(id) !== id) { - try { return requireModule(id + '/package.json', paths) } catch { } - id = dirname(id) - } - return null -} diff --git a/packages/nuxi/src/utils/diff.ts b/packages/nuxi/src/utils/diff.ts deleted file mode 100644 index 748002fcf8..0000000000 --- a/packages/nuxi/src/utils/diff.ts +++ /dev/null @@ -1,31 +0,0 @@ -import flatten from 'flat' -import { detailedDiff } from 'deep-object-diff' -import { blue, cyan, green, red } from 'colorette' - -function normalizeDiff (diffObj: any, type: 'added' | 'deleted' | 'updated', ignore: string[]) { - return Object.entries(flatten(diffObj) as Record) - .map(([key, value]) => ({ key, value, type })) - .filter(item => !ignore.includes(item.key) && typeof item.value !== 'function') -} - -export function diff (a: any, b: any, ignore: string[]) { - const _diff: any = detailedDiff(a, b) - return [ - ...normalizeDiff(_diff.added, 'added', ignore), - ...normalizeDiff(_diff.deleted, 'deleted', ignore), - ...normalizeDiff(_diff.updated, 'updated', ignore) - ] -} - -const typeMap = { - added: green('added'), - deleted: red('deleted'), - updated: blue('updated') -} - -export function printDiff (diff: any) { - for (const item of diff) { - console.log(' ', typeMap[item.type as keyof typeof typeMap] || item.type, cyan(item.key), item.value ? `~> ${cyan(item.value)}` : '') - } - console.log() -} diff --git a/packages/nuxi/src/utils/engines.ts b/packages/nuxi/src/utils/engines.ts deleted file mode 100644 index 949db35831..0000000000 --- a/packages/nuxi/src/utils/engines.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { engines } from '../../package.json' - -export async function checkEngines () { - const satisfies = await import('semver/functions/satisfies.js') - .then(r => r.default || r as any as typeof import('semver/functions/satisfies.js')) // npm/node-semver#381 - const currentNode = process.versions.node - const nodeRange = engines.node - - if (!satisfies(currentNode, nodeRange)) { - console.warn(`Current version of Node.js (\`${currentNode}\`) is unsupported and might cause issues.\n Please upgrade to a compatible version (${nodeRange}).`) - } -} diff --git a/packages/nuxi/src/utils/env.ts b/packages/nuxi/src/utils/env.ts deleted file mode 100644 index 16816f09da..0000000000 --- a/packages/nuxi/src/utils/env.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const overrideEnv = (targetEnv: string) => { - const currentEnv = process.env.NODE_ENV - if (currentEnv && currentEnv !== targetEnv) { - console.warn(`Changing \`NODE_ENV\` from \`${currentEnv}\` to \`${targetEnv}\`, to avoid unintended behavior.`) - } - - process.env.NODE_ENV = targetEnv -} diff --git a/packages/nuxi/src/utils/esm.ts b/packages/nuxi/src/utils/esm.ts deleted file mode 100644 index 4effb0b19b..0000000000 --- a/packages/nuxi/src/utils/esm.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { pathToFileURL } from 'node:url' -import { interopDefault, resolvePath } from 'mlly' - -export async function tryResolveModule (id: string, url = import.meta.url) { - try { - return await resolvePath(id, { url }) - } catch { } -} - -export async function importModule (id: string, url = import.meta.url) { - const resolvedPath = await resolvePath(id, { url }) - return import(pathToFileURL(resolvedPath).href).then(interopDefault) -} - -export function tryImportModule (id: string, url = import.meta.url) { - try { - return importModule(id, url).catch(() => undefined) - } catch { } -} diff --git a/packages/nuxi/src/utils/fs.ts b/packages/nuxi/src/utils/fs.ts deleted file mode 100644 index 857cb47f2d..0000000000 --- a/packages/nuxi/src/utils/fs.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { promises as fsp } from 'node:fs' -import { dirname } from 'pathe' -import { consola } from 'consola' - -// Check if a file exists -export async function exists (path: string) { - try { - await fsp.access(path) - return true - } catch { - return false - } -} - -export async function clearDir (path: string) { - await fsp.rm(path, { recursive: true, force: true }) - await fsp.mkdir(path, { recursive: true }) -} - -export async function rmRecursive (paths: string[]) { - await Promise.all(paths.filter(p => typeof p === 'string').map(async (path) => { - consola.debug('Removing recursive path', path) - await fsp.rm(path, { recursive: true, force: true }).catch(() => {}) - })) -} - -export async function touchFile (path: string) { - const time = new Date() - await fsp.utimes(path, time, time).catch(() => {}) -} - -export function findup (rootDir: string, fn: (dir: string) => T | undefined): T | null { - let dir = rootDir - while (dir !== dirname(dir)) { - const res = fn(dir) - if (res) { - return res - } - dir = dirname(dir) - } - return null -} diff --git a/packages/nuxi/src/utils/help.ts b/packages/nuxi/src/utils/help.ts deleted file mode 100644 index c55575cfbb..0000000000 --- a/packages/nuxi/src/utils/help.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { cyan, magenta } from 'colorette' -import type { NuxtCommandMeta } from '../commands' - -export function showHelp (meta?: Partial) { - const sections: string[] = [] - - if (meta) { - if (meta.usage) { - sections.push(magenta('> ') + 'Usage: ' + cyan(meta.usage)) - } - - if (meta.description) { - sections.push(magenta('⋮ ') + meta.description) - } - } - - sections.push(`Use ${cyan('npx nuxi [command] --help')} to see help for each command`) - - console.log(sections.join('\n\n') + '\n') -} diff --git a/packages/nuxi/src/utils/kit.ts b/packages/nuxi/src/utils/kit.ts deleted file mode 100644 index 9a6a8bff54..0000000000 --- a/packages/nuxi/src/utils/kit.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { importModule, tryResolveModule } from './esm' - -export const loadKit = async (rootDir: string): Promise => { - try { - // Without PNP (or if users have a local install of kit, we bypass resolving from nuxt) - const localKit = await tryResolveModule('@nuxt/kit', rootDir) - // Otherwise, we resolve Nuxt _first_ as it is Nuxt's kit dependency that will be used - const rootURL = localKit ? rootDir : await tryResolveNuxt() || rootDir - return await importModule('@nuxt/kit', rootURL) as typeof import('@nuxt/kit') - } catch (e: any) { - if (e.toString().includes("Cannot find module '@nuxt/kit'")) { - throw new Error('nuxi requires `@nuxt/kit` to be installed in your project. Try installing `nuxt` v3 or `@nuxt/bridge` first.') - } - throw e - } -} - -async function tryResolveNuxt () { - for (const pkg of ['nuxt3', 'nuxt', 'nuxt-edge']) { - const path = await tryResolveModule(pkg) - if (path) { return path } - } - return null -} diff --git a/packages/nuxi/src/utils/nuxt.ts b/packages/nuxi/src/utils/nuxt.ts deleted file mode 100644 index 241acd8e61..0000000000 --- a/packages/nuxi/src/utils/nuxt.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { promises as fsp } from 'node:fs' -import { dirname, resolve } from 'pathe' -import { consola } from 'consola' -import { hash } from 'ohash' -import type { Nuxt } from '@nuxt/schema' -import { rmRecursive } from './fs' - -export interface NuxtProjectManifest { - _hash: string | null - project: { - rootDir: string - }, - versions: { - nuxt: string - } -} - -export async function cleanupNuxtDirs (rootDir: string) { - consola.info('Cleaning up generated nuxt files and caches...') - - await rmRecursive([ - '.nuxt', - '.output', - 'dist', - 'node_modules/.vite', - 'node_modules/.cache' - ].map(dir => resolve(rootDir, dir))) -} - -export function nuxtVersionToGitIdentifier (version: string) { - // match the git identifier in the release, for example: 3.0.0-rc.8-27677607.a3a8706 - const id = /\.([0-9a-f]{7,8})$/.exec(version) - if (id?.[1]) { - return id[1] - } - // match github tag, for example 3.0.0-rc.8 - return `v${version}` -} - -export function resolveNuxtManifest (nuxt: Nuxt): NuxtProjectManifest { - const manifest: NuxtProjectManifest = { - _hash: null, - project: { - rootDir: nuxt.options.rootDir - }, - versions: { - nuxt: nuxt._version - } - } - manifest._hash = hash(manifest) - return manifest -} - -export async function writeNuxtManifest (nuxt: Nuxt): Promise { - const manifest = resolveNuxtManifest(nuxt) - const manifestPath = resolve(nuxt.options.buildDir, 'nuxt.json') - await fsp.mkdir(dirname(manifestPath), { recursive: true }) - await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8') - return manifest -} - -export async function loadNuxtManifest (buildDir: string): Promise { - const manifestPath = resolve(buildDir, 'nuxt.json') - const manifest: NuxtProjectManifest | null = await fsp.readFile(manifestPath, 'utf-8') - .then(data => JSON.parse(data) as NuxtProjectManifest) - .catch(() => null) - return manifest -} diff --git a/packages/nuxi/src/utils/packageManagers.ts b/packages/nuxi/src/utils/packageManagers.ts deleted file mode 100644 index ba43eaeb92..0000000000 --- a/packages/nuxi/src/utils/packageManagers.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { execSync } from 'node:child_process' -import { existsSync } from 'node:fs' -import { resolve } from 'pathe' -import { findup } from './fs' - -export const packageManagerLocks = { - yarn: 'yarn.lock', - npm: 'package-lock.json', - pnpm: 'pnpm-lock.yaml' -} - -type PackageManager = keyof typeof packageManagerLocks - -export function getPackageManager (rootDir: string) { - return findup(rootDir, (dir) => { - for (const name in packageManagerLocks) { - const path = packageManagerLocks[name as PackageManager] - if (path && existsSync(resolve(dir, path))) { - return name - } - } - }) as PackageManager | null -} - -export function getPackageManagerVersion (name: string) { - return execSync(`${name} --version`).toString('utf8').trim() -} diff --git a/packages/nuxi/src/utils/prepare.ts b/packages/nuxi/src/utils/prepare.ts deleted file mode 100644 index 9a72c56d15..0000000000 --- a/packages/nuxi/src/utils/prepare.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { promises as fsp } from 'node:fs' -import { isAbsolute, join, relative, resolve } from 'pathe' -import type { Nuxt, TSReference } from '@nuxt/schema' -import { defu } from 'defu' -import type { TSConfig } from 'pkg-types' -import { getModulePaths, getNearestPackage } from './cjs' - -export const writeTypes = async (nuxt: Nuxt) => { - const modulePaths = getModulePaths(nuxt.options.modulesDir) - - const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, { - compilerOptions: { - forceConsistentCasingInFileNames: true, - jsx: 'preserve', - target: 'ESNext', - module: 'ESNext', - moduleResolution: 'Node', - skipLibCheck: true, - strict: nuxt.options.typescript?.strict ?? false, - allowJs: true, - noEmit: true, - resolveJsonModule: true, - allowSyntheticDefaultImports: true, - types: ['node'], - baseUrl: relative(nuxt.options.buildDir, nuxt.options.rootDir), - paths: {} - }, - include: [ - './nuxt.d.ts', - join(relative(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'), - ...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [], - ...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : [] - ], - exclude: [ - // nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186 - relative(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')) - ] - }) - - const aliases: Record = { - ...nuxt.options.alias, - '#build': nuxt.options.buildDir - } - - // Exclude bridge alias types to support Volar - const excludedAlias = [/^@vue\/.*$/] - - for (const alias in aliases) { - if (excludedAlias.some(re => re.test(alias))) { - continue - } - const relativePath = isAbsolute(aliases[alias]) - ? relative(nuxt.options.rootDir, aliases[alias]) || '.' - : aliases[alias] - - const stats = await fsp.stat(resolve(nuxt.options.rootDir, relativePath)).catch(() => null /* file does not exist */) - tsConfig.compilerOptions = tsConfig.compilerOptions || {} - if (stats?.isDirectory()) { - tsConfig.compilerOptions.paths[alias] = [relativePath] - tsConfig.compilerOptions.paths[`${alias}/*`] = [`${relativePath}/*`] - } else { - tsConfig.compilerOptions.paths[alias] = [relativePath.replace(/(?<=\w)\.\w+$/g, '')] /* remove extension */ - } - } - - const references: TSReference[] = [ - ...nuxt.options.modules, - ...nuxt.options._modules - ] - .filter(f => typeof f === 'string') - .map(id => ({ types: getNearestPackage(id, modulePaths)?.name || id })) - - if (nuxt.options.experimental?.reactivityTransform) { - references.push({ types: 'vue/macros-global' }) - } - - const declarations: string[] = [] - - await nuxt.callHook('prepare:types', { references, declarations, tsConfig }) - - const declaration = [ - ...references.map((ref) => { - if ('path' in ref && isAbsolute(ref.path)) { - ref.path = relative(nuxt.options.buildDir, ref.path) - } - return `/// ` - }), - ...declarations, - '', - 'export {}', - '' - ].join('\n') - - async function writeFile () { - const GeneratedBy = '// Generated by nuxi' - - const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json') - await fsp.mkdir(nuxt.options.buildDir, { recursive: true }) - await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2)) - - const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts') - await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration) - } - - // This is needed for Nuxt 2 which clears the build directory again before building - // https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144 - // @ts-expect-error - nuxt.hook('builder:prepared', writeFile) - - await writeFile() -} - -function renderAttrs (obj: Record) { - return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ') -} - -function renderAttr (key: string, value: string) { - return value ? `${key}="${value}"` : '' -} diff --git a/packages/nuxi/src/utils/templates.ts b/packages/nuxi/src/utils/templates.ts deleted file mode 100644 index 11c89820e7..0000000000 --- a/packages/nuxi/src/utils/templates.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { upperFirst } from 'scule' - -interface TemplateOptions { - name: string, - args: Record -} - -interface Template { - (options: TemplateOptions): { path: string, contents: string } -} - -const httpMethods = ['connect', 'delete', 'get', 'head', 'options', 'post', 'put', 'trace', 'patch'] -const api: Template = ({ name, args }) => ({ - path: `server/api/${name}${applySuffix(args, httpMethods, 'method')}.ts`, - contents: ` -export default defineEventHandler((event) => { - return 'Hello ${name}' -}) -` -}) - -const plugin: Template = ({ name, args }) => ({ - path: `plugins/${name}${applySuffix(args, ['client', 'server'], 'mode')}.ts`, - contents: ` -export default defineNuxtPlugin((nuxtApp) => {}) - ` -}) - -const component: Template = ({ name, args }) => ({ - path: `components/${name}${applySuffix(args, ['client', 'server'], 'mode')}.vue`, - contents: ` - - - - - -` -}) - -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/build.config.ts b/packages/nuxt/build.config.ts index 40bdd582a0..dfaf1d97a0 100644 --- a/packages/nuxt/build.config.ts +++ b/packages/nuxt/build.config.ts @@ -29,7 +29,6 @@ export default defineBuildConfig({ externals: [ 'nuxt', 'nuxt/schema', - '@vue/reactivity', '@vue/shared', '@unhead/vue' ] diff --git a/packages/nuxt/config.d.ts b/packages/nuxt/config.d.ts index 8d844bad60..8eab74f3c6 100644 --- a/packages/nuxt/config.d.ts +++ b/packages/nuxt/config.d.ts @@ -1,4 +1,6 @@ import type { NuxtConfig } from 'nuxt/schema' +import type { DefineConfig, InputConfig, UserInputConfig, ConfigLayerMeta } from 'c12' export { NuxtConfig } from 'nuxt/schema' -export declare function defineNuxtConfig(config: NuxtConfig): NuxtConfig +export interface DefineNuxtConfig extends DefineConfig {} +export declare const defineNuxtConfig: DefineNuxtConfig diff --git a/packages/nuxt/index.d.ts b/packages/nuxt/index.d.ts index 6b83f9e99c..194371806e 100644 --- a/packages/nuxt/index.d.ts +++ b/packages/nuxt/index.d.ts @@ -1,5 +1,14 @@ declare global { - const __NUXT_VERSION__: string + var __NUXT_VERSION__: string + var __NUXT_PREPATHS__: string[] | string | undefined + var __NUXT_PATHS__: string[] | string | undefined + + interface Navigator { + connection?: { + type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown' + effectiveType: 'slow-2g' | '2g' | '3g' | '4g' + } + } } export {} diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index cc1ddc19e4..4fd30537a6 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "nuxt", - "version": "3.3.3", + "version": "3.7.4", "repository": "nuxt/nuxt", "license": "MIT", "type": "module", @@ -37,10 +37,6 @@ "#app": { "types": "./dist/app/index.d.ts", "import": "./dist/app/index.js" - }, - "#pages": { - "types": "./dist/pages/runtime/index.d.ts", - "import": "./dist/pages/runtime/index.js" } }, "files": [ @@ -56,64 +52,84 @@ "prepack": "unbuild" }, "dependencies": { - "@nuxt/devalue": "^2.0.0", - "@nuxt/kit": "workspace:../kit", - "@nuxt/schema": "workspace:../schema", - "@nuxt/telemetry": "^2.1.10", - "@nuxt/ui-templates": "^1.1.1", - "@nuxt/vite-builder": "workspace:../vite", - "@unhead/ssr": "^1.1.25", - "@unhead/vue": "^1.1.25", - "@vue/reactivity": "^3.2.47", - "@vue/shared": "^3.2.47", + "@nuxt/devalue": "^2.0.2", + "@nuxt/devtools": "1.0.0-beta.1", + "@nuxt/kit": "workspace:*", + "@nuxt/schema": "workspace:*", + "@nuxt/telemetry": "^2.5.2", + "@nuxt/ui-templates": "^1.3.1", + "@nuxt/vite-builder": "workspace:*", + "@unhead/dom": "^1.7.4", + "@unhead/ssr": "^1.7.4", + "@unhead/vue": "^1.7.4", + "@vue/shared": "^3.3.4", + "acorn": "8.10.0", + "c12": "^1.4.2", "chokidar": "^3.5.3", - "cookie-es": "^0.5.0", + "cookie-es": "^1.0.0", "defu": "^6.1.2", - "destr": "^1.2.2", - "devalue": "^4.3.0", + "destr": "^2.0.1", + "devalue": "^4.3.2", + "esbuild": "^0.19.4", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "fs-extra": "^11.1.1", - "globby": "^13.1.3", - "h3": "^1.6.4", - "hash-sum": "^2.0.0", + "globby": "^13.2.2", + "h3": "^1.8.2", "hookable": "^5.5.3", - "jiti": "^1.18.2", + "jiti": "^1.20.0", + "klona": "^2.0.6", "knitwork": "^1.0.0", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.0", - "mlly": "^1.2.0", - "nitropack": "^2.3.2", - "nuxi": "workspace:../nuxi", - "nypm": "^0.1.0", - "ofetch": "^1.0.1", - "ohash": "^1.0.0", - "pathe": "^1.1.0", - "perfect-debounce": "^0.1.3", - "prompts": "^2.4.2", + "magic-string": "^0.30.4", + "mlly": "^1.4.2", + "nitropack": "^2.6.3", + "nuxi": "^3.9.0", + "nypm": "^0.3.3", + "ofetch": "^1.3.3", + "ohash": "^1.1.3", + "pathe": "^1.1.1", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.0.3", + "radix3": "^1.1.0", "scule": "^1.0.0", - "strip-literal": "^1.0.1", - "ufo": "^1.1.1", - "unctx": "^2.1.2", - "unenv": "^1.2.2", - "unimport": "^3.0.6", - "unplugin": "^1.3.1", - "untyped": "^1.3.2", - "vue": "^3.2.47", - "vue-bundle-renderer": "^1.0.3", + "std-env": "^3.4.3", + "strip-literal": "^1.3.0", + "ufo": "^1.3.1", + "ultrahtml": "^1.5.2", + "uncrypto": "^0.1.3", + "unctx": "^2.3.1", + "unenv": "^1.7.4", + "unimport": "^3.4.0", + "unplugin": "^1.5.0", + "unplugin-vue-router": "^0.7.0", + "untyped": "^1.4.0", + "vue": "^3.3.4", + "vue-bundle-renderer": "^2.0.0", "vue-devtools-stub": "^0.1.0", - "vue-router": "^4.1.6" + "vue-router": "^4.2.5" }, "devDependencies": { - "@types/fs-extra": "^11.0.1", - "@types/hash-sum": "^1.0.0", - "@types/prompts": "^2.4.4", - "unbuild": "latest" + "@parcel/watcher": "2.3.0", + "@types/estree": "1.0.2", + "@types/fs-extra": "11.0.2", + "@vitejs/plugin-vue": "4.4.0", + "unbuild": "latest", + "vite": "4.4.11", + "vitest": "0.33.0" }, "peerDependencies": { - "@types/node": "^14.18.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "@parcel/watcher": "^2.1.0", + "@types/node": "^14.18.0 || >=16.10.0" + }, + "peerDependenciesMeta": { + "@parcel/watcher": { + "optional": true + }, + "@types/node": { + "optional": true + } }, "engines": { - "node": "^14.18.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node": "^14.18.0 || >=16.10.0" } } diff --git a/packages/nuxt/src/app/compat/idle-callback.ts b/packages/nuxt/src/app/compat/idle-callback.ts index cc28a2b1a2..01779dc1fe 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.mjs b/packages/nuxt/src/app/components/client-fallback.client.mjs deleted file mode 100644 index 19c378daff..0000000000 --- a/packages/nuxt/src/app/components/client-fallback.client.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import { createElementBlock, defineComponent } from 'vue' - -export default defineComponent({ - name: 'NuxtClientFallback', - inheritAttrs: false, - props: { - uid: { - type: String - }, - fallbackTag: { - type: String, - default: () => 'div' - }, - fallback: { - type: String, - default: () => '' - }, - placeholder: { - type: String - }, - placeholderTag: { - type: String - } - }, - emits: ['ssr-error'], - setup (props, ctx) { - const mounted = ref(false) - const ssrFailed = useState(`${props.uid}`) - - if (ssrFailed.value) { - onMounted(() => { mounted.value = true }) - } - - return () => { - if (mounted.value) { return ctx.slots.default?.() } - if (ssrFailed.value) { - const slot = ctx.slots.placeholder || ctx.slots.fallback - if (slot) { return slot() } - const fallbackStr = props.placeholder || props.fallback - const fallbackTag = props.placeholderTag || props.fallbackTag - return createElementBlock(fallbackTag, null, fallbackStr) - } - return ctx.slots.default?.() - } - } -}) diff --git a/packages/nuxt/src/app/components/client-fallback.client.ts b/packages/nuxt/src/app/components/client-fallback.client.ts new file mode 100644 index 0000000000..d1f249ba3f --- /dev/null +++ b/packages/nuxt/src/app/components/client-fallback.client.ts @@ -0,0 +1,53 @@ +import { createElementBlock, defineComponent, onMounted, ref } from 'vue' +import { useState } from '../composables/state' + +export default defineComponent({ + name: 'NuxtClientFallback', + inheritAttrs: false, + props: { + uid: { + type: String + }, + fallbackTag: { + type: String, + default: () => 'div' + }, + fallback: { + type: String, + default: () => '' + }, + placeholder: { + type: String + }, + placeholderTag: { + type: String + }, + keepFallback: { + type: Boolean, + default: () => false + } + }, + 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. + const ssrFailed = useState(`${props.uid}`) + + if (ssrFailed.value) { + onMounted(() => { mounted.value = true }) + } + + return () => { + if (ssrFailed.value) { + if (!mounted.value || props.keepFallback) { + const slot = ctx.slots.placeholder || ctx.slots.fallback + if (slot) { return slot() } + const fallbackStr = props.placeholder || props.fallback + const fallbackTag = props.placeholderTag || props.fallbackTag + return createElementBlock(fallbackTag, null, fallbackStr) + } + } + return ctx.slots.default?.() + } + } +}) diff --git a/packages/nuxt/src/app/components/client-fallback.server.mjs b/packages/nuxt/src/app/components/client-fallback.server.ts similarity index 73% rename from packages/nuxt/src/app/components/client-fallback.server.mjs rename to packages/nuxt/src/app/components/client-fallback.server.ts index 547487d6be..f4229867c9 100644 --- a/packages/nuxt/src/app/components/client-fallback.server.mjs +++ b/packages/nuxt/src/app/components/client-fallback.server.ts @@ -1,5 +1,6 @@ -import { defineComponent, getCurrentInstance, onErrorCaptured } from 'vue' +import { defineComponent, getCurrentInstance, onErrorCaptured, ref } from 'vue' import { ssrRenderAttrs, ssrRenderSlot, ssrRenderVNode } from 'vue/server-renderer' +import { useState } from '../composables/state' import { createBuffer } from './utils' const NuxtClientFallbackServer = defineComponent({ @@ -22,17 +23,25 @@ const NuxtClientFallbackServer = defineComponent({ }, placeholderTag: { type: String + }, + keepFallback: { + type: Boolean, + default: () => false + } + }, + emits: { + 'ssr-error' (_error: unknown) { + return true } }, - emits: ['ssr-error'], setup (props, ctx) { const vm = getCurrentInstance() const ssrFailed = ref(false) - onErrorCaptured(() => { + onErrorCaptured((err) => { useState(`${props.uid}`, () => true) ssrFailed.value = true - ctx.emit('ssr-error') + ctx.emit('ssr-error', err) return false }) @@ -40,19 +49,19 @@ const NuxtClientFallbackServer = defineComponent({ const defaultSlot = ctx.slots.default?.() const ssrVNodes = createBuffer() - for (let i = 0; i < defaultSlot.length; i++) { - ssrRenderVNode(ssrVNodes.push, defaultSlot[i], vm) + for (let i = 0; i < (defaultSlot?.length || 0); i++) { + ssrRenderVNode(ssrVNodes.push, defaultSlot![i], vm!) } return { ssrFailed, ssrVNodes } - } catch { + } catch (ssrError) { // catch in dev useState(`${props.uid}`, () => true) - ctx.emit('ssr-error') + ctx.emit('ssr-error', ssrError) return { ssrFailed: true, ssrVNodes: [] } } }, - ssrRender (ctx, push, parent) { + ssrRender (ctx: any, push: any, parent: any) { if (ctx.ssrFailed) { const { fallback, placeholder } = ctx.$slots if (fallback || placeholder) { diff --git a/packages/nuxt/src/app/components/client-only.mjs b/packages/nuxt/src/app/components/client-only.ts similarity index 87% rename from packages/nuxt/src/app/components/client-only.mjs rename to packages/nuxt/src/app/components/client-only.ts index 7d3ce8b5f8..f6739e118a 100644 --- a/packages/nuxt/src/app/components/client-only.mjs +++ b/packages/nuxt/src/app/components/client-only.ts @@ -1,4 +1,5 @@ import { createElementBlock, createElementVNode, defineComponent, h, mergeProps, onMounted, ref } from 'vue' +import type { ComponentOptions } from 'vue' export default defineComponent({ name: 'ClientOnly', @@ -8,7 +9,7 @@ export default defineComponent({ setup (_, { slots, attrs }) { const mounted = ref(false) onMounted(() => { mounted.value = true }) - return (props) => { + return (props: any) => { if (mounted.value) { return slots.default?.() } const slot = slots.fallback || slots.placeholder if (slot) { return slot() } @@ -21,7 +22,8 @@ export default defineComponent({ const cache = new WeakMap() -export function createClientOnly (component) { +/*! @__NO_SIDE_EFFECTS__ */ +export function createClientOnly (component: T) { if (cache.has(component)) { return cache.get(component) } @@ -30,9 +32,9 @@ export function createClientOnly (component) { if (clone.render) { // override the component render (non script setup component) - clone.render = (ctx, ...args) => { + clone.render = (ctx: any, ...args: any[]) => { if (ctx.mounted$) { - const res = component.render(ctx, ...args) + const res = component.render?.bind(ctx)(ctx, ...args) return (res.children === null || typeof res.children === 'string') ? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag) : h(res) @@ -56,7 +58,7 @@ export function createClientOnly (component) { .then((setupState) => { return typeof setupState !== 'function' ? { ...setupState, mounted$ } - : (...args) => { + : (...args: any[]) => { if (mounted$.value) { const res = setupState(...args) return (res.children === null || typeof res.children === 'string') diff --git a/packages/nuxt/src/app/components/dev-only.mjs b/packages/nuxt/src/app/components/dev-only.ts similarity index 70% rename from packages/nuxt/src/app/components/dev-only.mjs rename to packages/nuxt/src/app/components/dev-only.ts index ba7cd22dea..6c8ec7c5e9 100644 --- a/packages/nuxt/src/app/components/dev-only.mjs +++ b/packages/nuxt/src/app/components/dev-only.ts @@ -3,9 +3,9 @@ import { defineComponent } from 'vue' export default defineComponent({ name: 'DevOnly', setup (_, props) { - if (process.dev) { + if (import.meta.dev) { return () => props.slots.default?.() } - return () => null + return () => props.slots.fallback?.() } }) diff --git a/packages/nuxt/src/app/components/injections.ts b/packages/nuxt/src/app/components/injections.ts new file mode 100644 index 0000000000..3f9660e607 --- /dev/null +++ b/packages/nuxt/src/app/components/injections.ts @@ -0,0 +1,10 @@ +import type { InjectionKey } from 'vue' +import type { RouteLocationNormalizedLoaded } from 'vue-router' + +export interface LayoutMeta { + isCurrent: (route: RouteLocationNormalizedLoaded) => boolean +} + +export const LayoutMetaSymbol: InjectionKey = Symbol('layout-meta') + +export const PageRouteSymbol: InjectionKey = Symbol('route') diff --git a/packages/nuxt/src/app/components/island-renderer.ts b/packages/nuxt/src/app/components/island-renderer.ts index 8e0b929c66..421e3863ab 100644 --- a/packages/nuxt/src/app/components/island-renderer.ts +++ b/packages/nuxt/src/app/components/island-renderer.ts @@ -1,7 +1,7 @@ import type { defineAsyncComponent } from 'vue' import { createVNode, defineComponent } from 'vue' -// @ts-ignore +// @ts-expect-error virtual file import * as islandComponents from '#build/components.islands.mjs' import { createError } from '#app/composables/error' @@ -18,9 +18,10 @@ 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}` }) } - return () => createVNode(component || 'span', props.context.props) + + return () => createVNode(component || 'span', { ...props.context.props, 'nuxt-ssr-component-uid': '' }) } }) diff --git a/packages/nuxt/src/app/components/layout.ts b/packages/nuxt/src/app/components/layout.ts index f82dcf8dc4..b88def1a94 100644 --- a/packages/nuxt/src/app/components/layout.ts +++ b/packages/nuxt/src/app/components/layout.ts @@ -1,130 +1,2 @@ -import type { Ref, VNode } from 'vue' -import { Transition, computed, defineComponent, h, inject, nextTick, onMounted, unref } from 'vue' -import type { RouteLocationNormalizedLoaded } from 'vue-router' -import { _wrapIf } from './utils' -import { useRoute } from '#app/composables/router' -// @ts-ignore -import { useRoute as useVueRouterRoute } from '#build/pages' -// @ts-ignore -import layouts from '#build/layouts' -// @ts-ignore -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, - ...process.dev ? { hasTransition: Boolean } : {} - }, - async setup (props, context) { - let vnode: VNode - - if (process.dev && process.client) { - onMounted(() => { - nextTick(() => { - if (props.name && ['#comment', '#text'].includes(vnode?.el?.nodeName)) { - console.warn(`[nuxt] \`${props.name}\` layout does not have a single root node and will cause errors when navigating between routes.`) - } - }) - }) - } - - const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r) - - return () => { - if (process.dev && process.client && props.hasTransition) { - vnode = h(LayoutComponent, context.attrs, context.slots) - return vnode - } - return h(LayoutComponent, context.attrs, 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) { - // Need to ensure (if we are not a child of ``) that we use synchronous route (not deferred) - const injectedRoute = inject('_route') as RouteLocationNormalizedLoaded - const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute - const layout = computed(() => { - const layoutName = unref(props.name) ?? route.meta.layout as string ?? 'default' - - if (typeof layoutName === 'string') { - const layoutPath = layoutName.replace(/-/g, '/') - - // Check if layout exists for example `desktop-default` will translate to `layouts/desktop/default` - if (layoutPath in layouts) { - return layoutPath - } - - // Check if layout exists for example `desktop` or `desktop-index` will translate to ` `layouts/desktop/index` - if ((layoutPath + '/index') in layouts) { - return layoutPath + '/index' - } - - // If the directory inside layouts has has a dash in the name such as `desktop-base` we need to check for that - if (layoutName.includes('-')) { - const layoutPath = layoutName.replace(/-/g, '/') - - // Check if layout exists for example `desktop-base` will translate to `layouts/desktop/base` - if (layoutPath in layouts) { - return layoutPath - } - - // Check if layout exists for example `desktop-base` will translate to `layouts/desktop-base/base` - const layoutPathLast = layoutName.split('-').pop() - if (layoutName + '/' + layoutPathLast in layouts) { - return layoutName + '/' + layoutPathLast - } - - // Check if layout exists for example `desktop-base` will translate to `layouts/desktop-base/index` - if (layoutName + '/index' in layouts) { - return layoutName + '/index' - } - } - } - - return layoutName - }) - - let vnode: VNode - let _layout: string | false - if (process.dev && process.client) { - onMounted(() => { - nextTick(() => { - if (_layout && _layout in layouts && ['#comment', '#text'].includes(vnode?.el?.nodeName)) { - console.warn(`[nuxt] \`${_layout}\` layout does not have a single root node and will cause errors when navigating between routes.`) - } - }) - }) - } - - 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: () => _wrapIf(LayoutLoader, hasLayout && { - key: layout.value, - name: layout.value, - ...(process.dev ? { hasTransition: !!transitionProps } : {}), - ...context.attrs - }, context.slots).default() - }).default() - } - } -}) +// 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 271a157b6c..cb8d2f6707 100644 --- a/packages/nuxt/src/app/components/nuxt-error-boundary.ts +++ b/packages/nuxt/src/app/components/nuxt-error-boundary.ts @@ -11,14 +11,19 @@ export default defineComponent({ const error = ref(null) const nuxtApp = useNuxtApp() - onErrorCaptured((err) => { - if (process.client && !nuxtApp.isHydrating) { + onErrorCaptured((err, target, info) => { + if (import.meta.client && !nuxtApp.isHydrating) { emit('error', err) + nuxtApp.hooks.callHook('vue:error', err, target, info) error.value = err return false } }) - return () => error.value ? slots.error?.({ error }) : slots.default?.() + function clearError () { + error.value = null + } + + return () => error.value ? slots.error?.({ error, clearError }) : slots.default?.() } }) diff --git a/packages/nuxt/src/app/components/nuxt-error-page.vue b/packages/nuxt/src/app/components/nuxt-error-page.vue index 521dae53f6..d3fa6e2636 100644 --- a/packages/nuxt/src/app/components/nuxt-error-page.vue +++ b/packages/nuxt/src/app/components/nuxt-error-page.vue @@ -5,14 +5,15 @@ diff --git a/packages/nuxt/src/app/components/route-provider.ts b/packages/nuxt/src/app/components/route-provider.ts new file mode 100644 index 0000000000..96a2bf0296 --- /dev/null +++ b/packages/nuxt/src/app/components/route-provider.ts @@ -0,0 +1,57 @@ +import { defineComponent, h, nextTick, onMounted, provide, shallowReactive } from 'vue' +import type { Ref, VNode } from 'vue' +import type { RouteLocation, RouteLocationNormalizedLoaded } from '#vue-router' +import { PageRouteSymbol } from '#app/components/injections' + +export const RouteProvider = defineComponent({ + name: 'RouteProvider', + props: { + vnode: { + type: Object as () => VNode, + required: true + }, + route: { + type: Object as () => RouteLocationNormalizedLoaded, + required: true + }, + vnodeRef: Object as () => Ref, + renderKey: String, + trackRootNodes: Boolean + }, + setup (props) { + // Prevent reactivity when the page will be rerendered in a different suspense fork + const previousKey = props.renderKey + const previousRoute = props.route + + // Provide a reactive route within the page + const route = {} as RouteLocation + for (const key in props.route) { + Object.defineProperty(route, key, { + get: () => previousKey === props.renderKey ? props.route[key as keyof RouteLocationNormalizedLoaded] : previousRoute[key as keyof RouteLocationNormalizedLoaded] + }) + } + + provide(PageRouteSymbol, shallowReactive(route)) + + let vnode: VNode + if (import.meta.dev && import.meta.client && props.trackRootNodes) { + onMounted(() => { + nextTick(() => { + if (['#comment', '#text'].includes(vnode?.el?.nodeName)) { + const filename = (vnode?.type as any).__file + console.warn(`[nuxt] \`${filename}\` does not have a single root node and will cause errors when navigating between routes.`) + } + }) + }) + } + + return () => { + if (import.meta.dev && import.meta.client) { + vnode = h(props.vnode, { ref: props.vnodeRef }) + return vnode + } + + return h(props.vnode, { ref: props.vnodeRef }) + } + } +}) diff --git a/packages/nuxt/src/app/components/test-component-wrapper.ts b/packages/nuxt/src/app/components/test-component-wrapper.ts index 676b821e8b..fd185150cc 100644 --- a/packages/nuxt/src/app/components/test-component-wrapper.ts +++ b/packages/nuxt/src/app/components/test-component-wrapper.ts @@ -1,13 +1,21 @@ import { parseURL } from 'ufo' import { defineComponent, h } from 'vue' import { parseQuery } from 'vue-router' +import { resolve } from 'pathe' +import destr from 'destr' +// @ts-expect-error virtual file +import { devRootDir } from '#build/nuxt.config.mjs' -export default (url:string) => defineComponent({ +export default (url: string) => defineComponent({ name: 'NuxtTestComponentWrapper', async setup (props, { attrs }) { const query = parseQuery(parseURL(url).search) - const urlProps = query.props ? JSON.parse(query.props as string) : {} + const urlProps = query.props ? destr>(query.props as string) : {} + const path = resolve(query.path as string) + if (!path.startsWith(devRootDir)) { + throw new Error(`[nuxt] Cannot access path outside of project root directory: \`${path}\`.`) + } const comp = await import(/* @vite-ignore */ query.path as string).then(r => r.default) return () => [ h('div', 'Component Test Wrapper for ' + query.path), diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index 5865558278..c98d8efaac 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -1,22 +1,16 @@ -import { defineComponent, h } from 'vue' -import type { Component } from 'vue' +import { h } from 'vue' +import type { Component, RendererNode } from 'vue' // eslint-disable-next-line -import { isString, isPromise, isArray } from '@vue/shared' - -const Fragment = defineComponent({ - name: 'FragmentWrapper', - setup (_props, { slots }) { - return () => slots.default?.() - } -}) +import { isString, isPromise, isArray, isObject } from '@vue/shared' +import destr from 'destr' /** * Internal utility - * * @private */ export const _wrapIf = (component: Component, props: any, slots: any) => { - return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) } + props = props === true ? {} : props + return { default: () => props ? h(component, props, slots) : slots.default?.() } } // eslint-disable-next-line no-use-before-define @@ -25,7 +19,6 @@ export type SSRBufferItem = string | SSRBuffer | Promise /** * create buffer retrieved from @vue/server-renderer - * * @see https://github.com/vuejs/core/blob/9617dd4b2abc07a5dc40de6e5b759e851b4d0da1/packages/server-renderer/src/render.ts#L57 * @private */ @@ -50,3 +43,115 @@ export function createBuffer () { } } } + +const TRANSLATE_RE = /&(nbsp|amp|quot|lt|gt);/g +const NUMSTR_RE = /&#(\d+);/gi +export function decodeHtmlEntities (html: string) { + const translateDict = { + nbsp: ' ', + amp: '&', + quot: '"', + lt: '<', + gt: '>' + } as const + return html.replace(TRANSLATE_RE, function (_, entity: keyof typeof translateDict) { + return translateDict[entity] + }).replace(NUMSTR_RE, function (_, numStr: string) { + const num = parseInt(numStr, 10) + return String.fromCharCode(num) + }) +} + +/** + * helper for NuxtIsland to generate a correct array for scoped data + */ +export function vforToArray (source: any): any[] { + if (isArray(source)) { + return source + } else if (isString(source)) { + return source.split('') + } else if (typeof source === 'number') { + if (import.meta.dev && !Number.isInteger(source)) { + console.warn(`The v-for range expect an integer value but got ${source}.`) + } + const array = [] + for (let i = 0; i < source; i++) { + array[i] = i + } + return array + } else if (isObject(source)) { + if (source[Symbol.iterator as any]) { + return Array.from(source as Iterable, item => + item + ) + } else { + const keys = Object.keys(source) + const array = new Array(keys.length) + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i] + array[i] = source[key] + } + return array + } + } + return [] +} + +/** + * Retrieve the HTML content from an element + * Handles `` Fragment elements + * @param element the element to retrieve the HTML + * @param withoutSlots purge all slots from the HTML string retrieved + * @returns {string[]} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML + */ +export function getFragmentHTML (element: RendererNode | null, withoutSlots = false) { + if (element) { + if (element.nodeName === '#comment' && element.nodeValue === '[') { + return getFragmentChildren(element, [], withoutSlots) + } + if (withoutSlots) { + const clone = element.cloneNode(true) + clone.querySelectorAll('[nuxt-ssr-slot-name]').forEach((n: Element) => { n.innerHTML = '' }) + return [clone.outerHTML] + } + return [element.outerHTML] + } + return [] +} + +function getFragmentChildren (element: RendererNode | null, blocks: string[] = [], withoutSlots = false) { + if (element && element.nodeName) { + if (isEndFragment(element)) { + return blocks + } else if (!isStartFragment(element)) { + const clone = element.cloneNode(true) as Element + if (withoutSlots) { + clone.querySelectorAll('[nuxt-ssr-slot-name]').forEach((n) => { n.innerHTML = '' }) + } + blocks.push(clone.outerHTML) + } + + getFragmentChildren(element.nextSibling, blocks, withoutSlots) + } + return blocks +} + +function isStartFragment (element: RendererNode) { + return element.nodeName === '#comment' && element.nodeValue === '[' +} + +function isEndFragment (element: RendererNode) { + return element.nodeName === '#comment' && element.nodeValue === ']' +} +const SLOT_PROPS_RE = /]*nuxt-ssr-slot-name="([^"]*)" nuxt-ssr-slot-data="([^"]*)"[^/|>]*>/g + +export function getSlotProps (html: string) { + const slotsDivs = html.matchAll(SLOT_PROPS_RE) + const data: Record = {} + for (const slot of slotsDivs) { + const [_, slotName, json] = slot + const slotData = destr(decodeHtmlEntities(json)) + data[slotName] = slotData + } + return data +} diff --git a/packages/nuxt/src/app/composables/asyncContext.ts b/packages/nuxt/src/app/composables/asyncContext.ts new file mode 100644 index 0000000000..5fa88df529 --- /dev/null +++ b/packages/nuxt/src/app/composables/asyncContext.ts @@ -0,0 +1,9 @@ +// @ts-expect-error withAsyncContext is internal API +import { getCurrentInstance, withAsyncContext as withVueAsyncContext } from 'vue' + +export function withAsyncContext (fn: () => PromiseLike) { + return withVueAsyncContext(() => { + const nuxtApp = getCurrentInstance()?.appContext.app.$nuxt + return nuxtApp ? nuxtApp.runWithContext(fn) : fn() + }) +} diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 38f8b5e387..62cae6ecd9 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -3,22 +3,27 @@ import type { Ref, WatchSource } from 'vue' import type { NuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt' import { createError } from './error' +import { onNuxtReady } from './ready' + +export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error' export type _Transform = (input: Input) => Output export type PickFrom> = T extends Array ? T : T extends Record - ? keyof T extends K[number] - ? T // Exact same keys as the target, skip Pick - : Pick - : T + ? keyof T extends K[number] + ? T // Exact same keys as the target, skip Pick + : K[number] extends never + ? T + : Pick + : T export type KeysOf = Array< T extends T // Include all keys of union types, not just common keys ? keyof T extends string ? keyof T - : string + : never : never > @@ -29,12 +34,13 @@ export type MultiWatchSources = (WatchSource | object)[] export interface AsyncDataOptions< ResT, DataT = ResT, - PickKeys extends KeysOf =KeysOf, + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > { server?: boolean lazy?: boolean - default?: () => DataT | Ref | null - transform?: _Transform, + default?: () => DefaultT | Ref + transform?: _Transform pick?: PickKeys watch?: MultiWatchSources immediate?: boolean @@ -51,11 +57,12 @@ export interface AsyncDataExecuteOptions { } export interface _AsyncData { - data: Ref + data: Ref pending: Ref refresh: (opts?: AsyncDataExecuteOptions) => Promise execute: (opts?: AsyncDataExecuteOptions) => Promise error: Ref + status: Ref } export type AsyncData = _AsyncData & Promise<_AsyncData> @@ -65,32 +72,56 @@ export function useAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( handler: (ctx?: NuxtApp) => Promise, - options?: AsyncDataOptions -): AsyncData, DataE | null> + options?: AsyncDataOptions +): AsyncData | DefaultT, DataE | null> export function useAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = DataT, +> ( + handler: (ctx?: NuxtApp) => Promise, + options?: AsyncDataOptions +): AsyncData | DefaultT, DataE | null> +export function useAsyncData< + ResT, + DataE = Error, + DataT = ResT, + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( key: string, handler: (ctx?: NuxtApp) => Promise, - options?: AsyncDataOptions -): AsyncData, DataE | null> + options?: AsyncDataOptions +): AsyncData | DefaultT, DataE | null> export function useAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = DataT, +> ( + key: string, + handler: (ctx?: NuxtApp) => Promise, + options?: AsyncDataOptions +): AsyncData | DefaultT, DataE | null> +export function useAsyncData< + ResT, + DataE = Error, + DataT = ResT, + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > (...args: any[]): AsyncData, DataE | null> { const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined if (typeof args[0] !== 'string') { args.unshift(autoKey) } // eslint-disable-next-line prefer-const - let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] // Validate arguments if (typeof key !== 'string') { @@ -102,7 +133,7 @@ export function useAsyncData< // Apply defaults options.server = options.server ?? true - options.default = options.default ?? getDefault + options.default = options.default ?? (getDefault as () => DefaultT) options.lazy = options.lazy ?? false options.immediate = options.immediate ?? true @@ -114,15 +145,19 @@ 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.payload._errors[key] ??= null + nuxt._asyncData[key] = { - data: ref(getCachedData() ?? options.default?.() ?? null), + data: ref(getCachedData() ?? options.default!()), pending: ref(!hasCachedData()), - error: toRef(nuxt.payload._errors, key) + error: toRef(nuxt.payload._errors, key), + status: ref('idle') } } + // TODO: Else, somehow check for conflicting keys with different defaults or fetcher - const asyncData = { ...nuxt._asyncData[key] } as AsyncData + const asyncData = { ...nuxt._asyncData[key] } as AsyncData asyncData.refresh = asyncData.execute = (opts = {}) => { if (nuxt._asyncDataPromises[key]) { @@ -133,10 +168,11 @@ export function useAsyncData< (nuxt._asyncDataPromises[key] as any).cancelled = true } // Avoid fetching same key that is already fetched - if (opts._initial && hasCachedData()) { + if ((opts._initial || (nuxt.isHydrating && opts._initial !== false)) && hasCachedData()) { return getCachedData() } asyncData.pending.value = true + asyncData.status.value = 'pending' // TODO: Cancel previous promise const promise = new Promise( (resolve, reject) => { @@ -159,13 +195,15 @@ export function useAsyncData< } asyncData.data.value = result asyncData.error.value = null + asyncData.status.value = 'success' }) .catch((error: any) => { // If this request is cancelled, resolve to the latest request. if ((promise as any).cancelled) { return nuxt._asyncDataPromises[key] } asyncData.error.value = error - asyncData.data.value = unref(options.default?.() ?? null) + asyncData.data.value = unref(options.default!()) + asyncData.status.value = 'error' }) .finally(() => { if ((promise as any).cancelled) { return } @@ -187,7 +225,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) @@ -197,7 +235,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) { @@ -215,6 +253,7 @@ export function useAsyncData< if (fetchOnServer && nuxt.isHydrating && hasCachedData()) { // 1. Hydration (server: true): no fetch asyncData.pending.value = false + asyncData.status.value = asyncData.error.value ? 'error' : 'success' } else if (instance && ((nuxt.payload.serverRendered && nuxt.isHydrating) || options.lazy) && options.immediate) { // 2. Initial load (server: false): fetch on mounted // 3. Initial load or navigation (lazy: true): fetch on mounted @@ -246,31 +285,56 @@ export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( handler: (ctx?: NuxtApp) => Promise, - options?: Omit, 'lazy'> -): AsyncData, DataE | null> + options?: Omit, 'lazy'> +): AsyncData | DefaultT, DataE | null> export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = DataT, +> ( + handler: (ctx?: NuxtApp) => Promise, + options?: Omit, 'lazy'> +): AsyncData | DefaultT, DataE | null> +export function useLazyAsyncData< + ResT, + DataE = Error, + DataT = ResT, + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( key: string, handler: (ctx?: NuxtApp) => Promise, - options?: Omit, 'lazy'> -): AsyncData, DataE | null> + options?: Omit, 'lazy'> +): AsyncData | DefaultT, DataE | null> export function useLazyAsyncData< ResT, DataE = Error, DataT = ResT, - PickKeys extends KeysOf = KeysOf -> (...args: any[]): AsyncData, DataE | null> { + PickKeys extends KeysOf = KeysOf, + DefaultT = DataT, +> ( + key: string, + handler: (ctx?: NuxtApp) => Promise, + options?: Omit, 'lazy'> +): AsyncData | DefaultT, DataE | null> + +export function useLazyAsyncData< + ResT, + DataE = Error, + DataT = ResT, + PickKeys extends KeysOf = KeysOf, + DefaultT = null, +> (...args: any[]): AsyncData | DefaultT, DataE | null> { const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined if (typeof args[0] !== 'string') { args.unshift(autoKey) } - const [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] - // @ts-ignore + const [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + // @ts-expect-error we pass an extra argument to prevent a key being injected return useAsyncData(key, handler, { ...options, lazy: true }, null) } @@ -288,9 +352,12 @@ export function useNuxtData (key: string): { data: Ref { - if (process.server) { + if (import.meta.server) { return Promise.resolve() } + + await new Promise(resolve => onNuxtReady(resolve)) + const _keys = keys ? Array.isArray(keys) ? keys : [keys] : undefined await useNuxtApp().hooks.callHookParallel('app:data:refresh', _keys) } @@ -309,12 +376,13 @@ export function clearNuxtData (keys?: string | string[] | ((key: string) => bool nuxtApp.payload.data[key] = undefined } if (key in nuxtApp.payload._errors) { - nuxtApp.payload._errors[key] = undefined + nuxtApp.payload._errors[key] = null } if (nuxtApp._asyncData[key]) { nuxtApp._asyncData[key]!.data.value = undefined - nuxtApp._asyncData[key]!.error.value = undefined + nuxtApp._asyncData[key]!.error.value = null nuxtApp._asyncData[key]!.pending.value = false + nuxtApp._asyncData[key]!.status.value = 'idle' } if (key in nuxtApp._asyncDataPromises) { nuxtApp._asyncDataPromises[key] = undefined diff --git a/packages/nuxt/src/app/composables/chunk.ts b/packages/nuxt/src/app/composables/chunk.ts index 1e0c1e4f13..c20cff0f9b 100644 --- a/packages/nuxt/src/app/composables/chunk.ts +++ b/packages/nuxt/src/app/composables/chunk.ts @@ -1,40 +1,37 @@ +import destr from 'destr' import { useNuxtApp } from '#app/nuxt' export interface ReloadNuxtAppOptions { /** * Number of milliseconds in which to ignore future reload requests - * * @default {10000} */ ttl?: number /** * Force a reload even if one has occurred within the previously specified TTL. - * * @default {false} */ force?: boolean /** * Whether to dump the current Nuxt state to sessionStorage (as `nuxt:reload:state`). - * * @default {false} */ persistState?: boolean /** * The path to reload. If this is different from the current window location it will * trigger a navigation and add an entry in the browser history. - * * @default {window.location.pathname} */ path?: string } export function reloadNuxtApp (options: ReloadNuxtAppOptions = {}) { - if (process.server) { return } + if (import.meta.server) { return } const path = options.path || window.location.pathname let handledPath: Record = {} try { - handledPath = JSON.parse(sessionStorage.getItem('nuxt:reload') || '{}') + handledPath = destr(sessionStorage.getItem('nuxt:reload') || '{}') } catch {} if (options.force || handledPath?.path !== path || handledPath?.expires < Date.now()) { diff --git a/packages/nuxt/src/app/composables/component.ts b/packages/nuxt/src/app/composables/component.ts index 1c401e16cf..5bfcc992ea 100644 --- a/packages/nuxt/src/app/composables/component.ts +++ b/packages/nuxt/src/app/composables/component.ts @@ -5,25 +5,32 @@ import type { NuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt' import { useAsyncData } from './asyncData' import { useRoute } from './router' +import { createError } from './error' export const NuxtComponentIndicator = '__nuxt_component' async function runLegacyAsyncData (res: Record | Promise>, fn: (nuxtApp: NuxtApp) => Promise>) { - const nuxt = useNuxtApp() + const nuxtApp = useNuxtApp() const route = useRoute() const vm = getCurrentInstance()! - const { fetchKey } = vm.proxy!.$options - const key = typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey || route.fullPath - const { data } = await useAsyncData(`options:asyncdata:${key}`, () => fn(nuxt)) + const { fetchKey, _fetchKeyBase } = vm.proxy!.$options + const key = (typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey) || + ([_fetchKeyBase, route.fullPath, route.matched.findIndex(r => Object.values(r.components || {}).includes(vm.type))].join(':')) + const { data, error } = await useAsyncData(`options:asyncdata:${key}`, () => nuxtApp.runWithContext(() => fn(nuxtApp))) + if (error.value) { + throw createError(error.value) + } if (data.value && typeof data.value === 'object') { Object.assign(await res, toRefs(reactive(data.value))) - } else if (process.dev) { + } else if (import.meta.dev) { console.warn('[nuxt] asyncData should return an object', data) } } +/*! @__NO_SIDE_EFFECTS__ */ export const defineNuxtComponent: typeof defineComponent = - function defineNuxtComponent (options: any): any { + function defineNuxtComponent (...args: any[]): any { + const [options, key] = args const { setup } = options // Avoid wrapping if no options api is used @@ -36,9 +43,11 @@ export const defineNuxtComponent: typeof defineComponent = return { [NuxtComponentIndicator]: true, + _fetchKeyBase: key, ...options, setup (props, ctx) { - const res = setup?.(props, ctx) || {} + const nuxtApp = useNuxtApp() + const res = setup ? Promise.resolve(nuxtApp.runWithContext(() => setup(props, ctx))).then(r => r || {}) : {} const promises: Promise[] = [] if (options.asyncData) { diff --git a/packages/nuxt/src/app/composables/cookie.ts b/packages/nuxt/src/app/composables/cookie.ts index b92a8903bf..16d703b52a 100644 --- a/packages/nuxt/src/app/composables/cookie.ts +++ b/packages/nuxt/src/app/composables/cookie.ts @@ -1,8 +1,8 @@ import type { Ref } from 'vue' -import { ref, watch } from 'vue' +import { getCurrentScope, nextTick, onScopeDispose, ref, watch } from 'vue' import type { CookieParseOptions, CookieSerializeOptions } from 'cookie-es' import { parse, serialize } from 'cookie-es' -import { appendHeader } from 'h3' +import { deleteCookie, getCookie, getRequestHeader, setCookie } from 'h3' import type { H3Event } from 'h3' import destr from 'destr' import { isEqual } from 'ohash' @@ -20,37 +20,57 @@ export interface CookieOptions extends _CookieOptions { export interface CookieRef extends Ref {} -const CookieDefaults: CookieOptions = { +const CookieDefaults = { path: '/', watch: true, decode: val => destr(decodeURIComponent(val)), encode: val => encodeURIComponent(typeof val === 'string' ? val : JSON.stringify(val)) -} +} satisfies CookieOptions -export function useCookie (name: string, _opts?: CookieOptions): CookieRef { +export function useCookie (name: string, _opts?: CookieOptions): CookieRef { const opts = { ...CookieDefaults, ..._opts } const cookies = readRawCookies(opts) || {} const cookie = ref(cookies[name] as any ?? opts.default?.()) - if (process.client) { - const callback = () => { writeClientCookie(name, cookie.value, opts as CookieSerializeOptions) } + if (import.meta.client) { + const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`) + if (getCurrentScope()) { onScopeDispose(() => { channel?.close() }) } + + const callback = () => { + writeClientCookie(name, cookie.value, opts as CookieSerializeOptions) + channel?.postMessage(opts.encode(cookie.value as T)) + } + + let watchPaused = false + + if (channel) { + channel.onmessage = (event) => { + watchPaused = true + cookie.value = opts.decode(event.data) + nextTick(() => { watchPaused = false }) + } + } + if (opts.watch) { - watch(cookie, callback, { deep: opts.watch !== 'shallow' }) + 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])) { - writeServerCookie(useRequestEvent(nuxtApp), name, cookie.value, opts) + writeServerCookie(useRequestEvent(nuxtApp), name, cookie.value, opts as CookieOptions) } } const unhook = nuxtApp.hooks.hookOnce('app:rendered', writeFinalCookieValue) - nuxtApp.hooks.hookOnce('app:redirected', () => { - // don't write cookie subsequently when app:rendered is called - unhook() + nuxtApp.hooks.hookOnce('app:error', () => { + unhook() // don't write cookie subsequently when app:rendered is called return writeFinalCookieValue() }) } @@ -59,9 +79,9 @@ export function useCookie (name: string, _opts?: } function readRawCookies (opts: CookieOptions = {}): Record | undefined { - if (process.server) { - return parse(useRequestEvent()?.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) } } @@ -74,14 +94,23 @@ 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) } } function writeServerCookie (event: H3Event, name: string, value: any, opts: CookieSerializeOptions = {}) { if (event) { - // TODO: Try to smart join with existing Set-Cookie headers - appendHeader(event, 'Set-Cookie', serializeCookie(name, value, opts)) + // update if value is set + if (value !== null && value !== undefined) { + return setCookie(event, name, value, opts) + } + + // delete if cookie exists in browser and value is null/undefined + if (getCookie(event, name) !== undefined) { + return deleteCookie(event, name, opts) + } + + // else ignore if cookie doesn't exist in browser and value is null/undefined } } diff --git a/packages/nuxt/src/app/composables/error.ts b/packages/nuxt/src/app/composables/error.ts index 3d1820f139..4a59bc6a7a 100644 --- a/packages/nuxt/src/app/composables/error.ts +++ b/packages/nuxt/src/app/composables/error.ts @@ -13,8 +13,10 @@ export const showError = (_err: string | Error | Partial) => { try { const nuxtApp = useNuxtApp() - nuxtApp.callHook('app:error', err) const error = useError() + if (import.meta.client) { + nuxtApp.hooks.callHook('app:error', err) + } error.value = error.value || err } catch { throw err diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 5e7277966f..c4c1d2b083 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -1,27 +1,35 @@ -import type { FetchError } from 'ofetch' -import type { AvailableRouterMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack' -import type { Ref } from 'vue' +import type { FetchError, FetchOptions } from 'ofetch' +import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack' +import type { MaybeRef, 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< ResT, DataT = ResT, PickKeys extends KeysOf = KeysOf, + DefaultT = null, R extends NitroFetchRequest = string & {}, M extends AvailableRouterMethod = AvailableRouterMethod -> extends Omit, 'watch'>, ComputedFetchOptions { +> extends Omit, 'watch'>, ComputedFetchOptions { key?: string $fetch?: typeof globalThis.$fetch watch?: MultiWatchSources | false @@ -31,37 +39,43 @@ export function useFetch< ResT = void, ErrorT = FetchError, ReqT extends NitroFetchRequest = NitroFetchRequest, - Method extends AvailableRouterMethod = 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod, + Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( request: Ref | ReqT | (() => ReqT), - opts?: UseFetchOptions<_ResT, DataT, PickKeys, ReqT, Method> -): AsyncData, ErrorT | null> + opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> +): AsyncData | DefaultT, ErrorT | null> export function useFetch< ResT = void, ErrorT = FetchError, ReqT extends NitroFetchRequest = NitroFetchRequest, - Method extends AvailableRouterMethod = 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod, + Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = DataT, > ( request: Ref | ReqT | (() => ReqT), - arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, ReqT, Method>, + opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method> +): AsyncData | DefaultT, ErrorT | null> +export function useFetch< + ResT = void, + ErrorT = FetchError, + ReqT extends NitroFetchRequest = NitroFetchRequest, + Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, + _ResT = ResT extends void ? FetchResult : ResT, + DataT = _ResT, + PickKeys extends KeysOf = KeysOf, + DefaultT = null, +> ( + request: Ref | ReqT | (() => ReqT), + arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, arg2?: string ) { const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] - const _key = opts.key || hash([autoKey, unref(opts.baseURL), typeof request === 'string' ? request : '', unref(opts.params || opts.query)]) - if (!_key || typeof _key !== 'string') { - throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key) - } - if (!request) { - throw new Error('[nuxt] [useFetch] request is missing.') - } - - const key = _key === autoKey ? '$f' + _key : _key const _request = computed(() => { let r = request @@ -71,6 +85,16 @@ export function useFetch< return unref(r) }) + const _key = opts.key || hash([autoKey, unref(opts.method as MaybeRef | undefined)?.toUpperCase() || 'GET', unref(opts.baseURL), typeof _request.value === 'string' ? _request.value : '', unref(opts.params || opts.query), unref(opts.headers)]) + if (!_key || typeof _key !== 'string') { + throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key) + } + if (!request) { + throw new Error('[nuxt] [useFetch] request is missing.') + } + + const key = _key === autoKey ? '$f' + _key : _key + if (!opts.baseURL && typeof _request.value === 'string' && _request.value.startsWith('//')) { throw new Error('[nuxt] [useFetch] the request URL must not start with "//".') } @@ -91,7 +115,7 @@ export function useFetch< cache: typeof opts.cache === 'boolean' ? undefined : opts.cache }) - const _asyncDataOptions: AsyncDataOptions<_ResT, DataT, PickKeys> = { + const _asyncDataOptions: AsyncDataOptions<_ResT, DataT, PickKeys, DefaultT> = { server, lazy, default: defaultFn, @@ -103,14 +127,14 @@ export function useFetch< let controller: AbortController - const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys>(key, () => { + const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => { controller?.abort?.() controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController 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() } @@ -124,33 +148,48 @@ export function useLazyFetch< ResT = void, ErrorT = FetchError, ReqT extends NitroFetchRequest = NitroFetchRequest, - Method extends AvailableRouterMethod = 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod, + Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = null, > ( request: Ref | ReqT | (() => ReqT), - opts?: Omit, 'lazy'> -): AsyncData, ErrorT | null> + opts?: Omit, 'lazy'> +): AsyncData | DefaultT, ErrorT | null> export function useLazyFetch< ResT = void, ErrorT = FetchError, ReqT extends NitroFetchRequest = NitroFetchRequest, - Method extends AvailableRouterMethod = 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod, + Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, _ResT = ResT extends void ? FetchResult : ResT, DataT = _ResT, - PickKeys extends KeysOf = KeysOf + PickKeys extends KeysOf = KeysOf, + DefaultT = DataT, > ( request: Ref | ReqT | (() => ReqT), - arg1?: string | Omit, 'lazy'>, + opts?: Omit, 'lazy'> +): AsyncData | DefaultT, ErrorT | null> +export function useLazyFetch< + ResT = void, + ErrorT = FetchError, + ReqT extends NitroFetchRequest = NitroFetchRequest, + Method extends AvailableRouterMethod = ResT extends void ? 'get' extends AvailableRouterMethod ? 'get' : AvailableRouterMethod : AvailableRouterMethod, + _ResT = ResT extends void ? FetchResult : ResT, + DataT = _ResT, + PickKeys extends KeysOf = KeysOf, + DefaultT = null, +> ( + request: Ref | ReqT | (() => ReqT), + arg1?: string | Omit, 'lazy'>, arg2?: string ) { const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] - return useFetch(request, { + return useFetch(request, { ...opts, lazy: true }, - // @ts-ignore + // @ts-expect-error we pass an extra argument with the resolved auto-key to prevent another from being injected autoKey) } diff --git a/packages/nuxt/src/app/composables/hydrate.ts b/packages/nuxt/src/app/composables/hydrate.ts index 873c472d25..1de7e356af 100644 --- a/packages/nuxt/src/app/composables/hydrate.ts +++ b/packages/nuxt/src/app/composables/hydrate.ts @@ -1,24 +1,24 @@ import { useNuxtApp } from '../nuxt' +import type { NuxtPayload } from '#app' /** * Allows full control of the hydration cycle to set and receive data from the server. - * * @param key a unique key to identify the data in the Nuxt payload * @param get a function that returns the value to set the initial data * @param set a function that will receive the data on the client-side */ -export const useHydration = (key: string, get: () => T, set: (value: T) => void) => { +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]) + set(nuxt.payload[key] as T) }) } } diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts index e40cb76ce6..a815dbd05e 100644 --- a/packages/nuxt/src/app/composables/index.ts +++ b/packages/nuxt/src/app/composables/index.ts @@ -16,18 +16,21 @@ export { defineNuxtComponent } from './component' export { useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData } from './asyncData' export type { AsyncDataOptions, AsyncData } from './asyncData' export { useHydration } from './hydrate' -export { useState } from './state' +export { useState, clearNuxtState } from './state' export { clearError, createError, isNuxtError, showError, useError } from './error' export type { NuxtError } from './error' export { useFetch, useLazyFetch } from './fetch' export type { FetchResult, UseFetchOptions } from './fetch' export { useCookie } from './cookie' export type { CookieOptions, CookieRef } from './cookie' -export { useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus } from './ssr' +export { prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus } from './ssr' export { onNuxtReady } from './ready' export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter } from './router' export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router' export { preloadComponents, prefetchComponents, preloadRouteComponents } from './preload' export { isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver } from './payload' +export { getAppManifest, getRouteRules } from './manifest' +export type { NuxtAppManifest, NuxtAppManifestMeta } from './manifest' export type { ReloadNuxtAppOptions } from './chunk' export { reloadNuxtApp } from './chunk' +export { useRequestURL } from './url' diff --git a/packages/nuxt/src/app/composables/manifest.ts b/packages/nuxt/src/app/composables/manifest.ts new file mode 100644 index 0000000000..0fc9cec5aa --- /dev/null +++ b/packages/nuxt/src/app/composables/manifest.ts @@ -0,0 +1,46 @@ +import { joinURL } from 'ufo' +import type { MatcherExport, RouteMatcher } from 'radix3' +import { createMatcherFromExport } from 'radix3' +import { defu } from 'defu' +import { useAppConfig, useRuntimeConfig } from '#app' +// @ts-expect-error virtual file +import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs' + +export interface NuxtAppManifestMeta { + id: string + timestamp: number +} + +export interface NuxtAppManifest extends NuxtAppManifestMeta { + matcher: MatcherExport + prerendered: string[] +} + +let manifest: Promise +let matcher: RouteMatcher + +function fetchManifest () { + if (!isAppManifestEnabled) { + throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`') + } + const config = useRuntimeConfig() + // @ts-expect-error private property + const buildId = useAppConfig().nuxt?.buildId + manifest = $fetch(joinURL(config.app.cdnURL || config.app.baseURL, config.app.buildAssetsDir, `builds/meta/${buildId}.json`)) + manifest.then((m) => { + matcher = createMatcherFromExport(m.matcher) + }) + return manifest +} + +export function getAppManifest (): Promise { + if (!isAppManifestEnabled) { + throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`') + } + return manifest || fetchManifest() +} + +export async function getRouteRules (url: string) { + await getAppManifest() + return defu({} as Record, ...matcher.matchAll(url).reverse()) +} diff --git a/packages/nuxt/src/app/composables/payload.ts b/packages/nuxt/src/app/composables/payload.ts index 219f03582f..8a3157cc19 100644 --- a/packages/nuxt/src/app/composables/payload.ts +++ b/packages/nuxt/src/app/composables/payload.ts @@ -4,8 +4,11 @@ import { useHead } from '@unhead/vue' import { getCurrentInstance } from 'vue' import { useNuxtApp, useRuntimeConfig } from '../nuxt' +import { getAppManifest, getRouteRules } from '#app/composables/manifest' +import { useRoute } from '#app/composables' + // @ts-expect-error virtual import -import { renderJsonPayloads } from '#build/nuxt.config.mjs' +import { appManifest, payloadExtraction, renderJsonPayloads } from '#build/nuxt.config.mjs' interface LoadPayloadOptions { fresh?: boolean @@ -13,19 +16,24 @@ interface LoadPayloadOptions { } export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record | Promise> | null { - if (process.server) { return null } + if (import.meta.server || !payloadExtraction) { return null } const payloadURL = _getPayloadURL(url, opts) const nuxtApp = useNuxtApp() const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {} - if (cache[payloadURL]) { + if (payloadURL in cache) { return cache[payloadURL] } - cache[payloadURL] = _importPayload(payloadURL).then((payload) => { - if (!payload) { - delete cache[payloadURL] + cache[payloadURL] = isPrerendered().then((prerendered) => { + if (!prerendered) { + cache[payloadURL] = null return null } - return payload + return _importPayload(payloadURL).then((payload) => { + if (payload) { return payload } + + delete cache[payloadURL] + return null + }) }) return cache[payloadURL] } @@ -55,26 +63,37 @@ function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) { } async function _importPayload (payloadURL: string) { - if (process.server) { return null } + if (import.meta.server || !payloadExtraction) { return null } + const payloadPromise = renderJsonPayloads + ? fetch(payloadURL).then(res => res.text().then(parsePayload)) + : import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r) + try { - return renderJsonPayloads - ? parsePayload(await fetch(payloadURL).then(res => res.text())) - : await import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r) + return await payloadPromise } catch (err) { console.warn('[nuxt] Cannot load payload ', payloadURL, err) } return null } -export function isPrerendered () { +export async function isPrerendered (url = useRoute().path) { // Note: Alternative for server is checking x-nitro-prerender header const nuxtApp = useNuxtApp() - return !!nuxtApp.payload.prerenderedAt + if (nuxtApp.payload.prerenderedAt) { + return true + } + if (!appManifest) { return false } + const manifest = await getAppManifest() + if (manifest.prerendered.includes(url)) { + return true + } + const rules = await getRouteRules(url) + return !!rules.prerender } let payloadCache: any = null export async function getNuxtClientPayload () { - if (process.server) { + if (import.meta.server) { return } if (payloadCache) { @@ -110,7 +129,7 @@ export function definePayloadReducer ( name: string, reduce: (data: any) => any ) { - if (process.server) { + if (import.meta.server) { useNuxtApp().ssrContext!._payloadReducers[name] = reduce } } @@ -122,12 +141,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 3844c9d0c0..e8e4216ec3 100644 --- a/packages/nuxt/src/app/composables/preload.ts +++ b/packages/nuxt/src/app/composables/preload.ts @@ -1,15 +1,14 @@ import type { Component } from 'vue' -import type { RouteLocationRaw, Router } from 'vue-router' +import type { RouteLocationRaw, Router } from '#vue-router' import { useNuxtApp } from '../nuxt' import { useRouter } from './router' /** * Preload a component or components that have been globally registered. - * * @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] @@ -18,7 +17,6 @@ export const preloadComponents = async (components: string | string[]) => { /** * Prefetch a component or components that have been globally registered. - * * @param components Pascal-cased name or names of components to prefetch */ export const prefetchComponents = (components: string | string[]) => { @@ -35,7 +33,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 58143d1840..a30d0d7caf 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 b73f31b203..7b4bcb6abf 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -1,27 +1,26 @@ -import { getCurrentInstance, inject, onUnmounted } from 'vue' +import { getCurrentInstance, hasInjectionContext, inject, onScopeDispose } from 'vue' import type { Ref } from 'vue' -import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocationPathRaw, RouteLocationRaw, Router } from 'vue-router' -import { sendRedirect } from 'h3' -import { hasProtocol, joinURL, parseURL } from 'ufo' +import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router' +import { sanitizeStatusCode } from 'h3' +import { hasProtocol, isScriptProtocol, joinURL, parseURL, withQuery } from 'ufo' import { useNuxtApp, useRuntimeConfig } from '../nuxt' import type { NuxtError } from './error' -import { createError } from './error' -import { useState } from './state' -import { setResponseStatus } from './ssr' +import { createError, showError } from './error' import type { PageMeta } from '#app' +import { PageRouteSymbol } from '#app/components/injections' -export const useRouter = () => { +export const useRouter: typeof _useRouter = () => { return useNuxtApp()?.$router as Router } -export const useRoute = (): RouteLocationNormalizedLoaded => { - if (process.dev && isProcessingMiddleware()) { +export const useRoute: typeof _useRoute = () => { + 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 (getCurrentInstance()) { - return inject('_route', useNuxtApp()._route) + if (hasInjectionContext()) { + return inject(PageRouteSymbol, useNuxtApp()._route) } return useNuxtApp()._route } @@ -31,19 +30,22 @@ export const onBeforeRouteLeave = (guard: NavigationGuard) => { if (to === from) { return } return guard(to, from, next) }) - onUnmounted(unsubscribe) + onScopeDispose(unsubscribe) } export const onBeforeRouteUpdate = (guard: NavigationGuard) => { const unsubscribe = useRouter().beforeEach(guard) - onUnmounted(unsubscribe) + onScopeDispose(unsubscribe) } export interface RouteMiddleware { (to: RouteLocationNormalized, from: RouteLocationNormalized): ReturnType } -export const defineNuxtRouteMiddleware = (middleware: RouteMiddleware) => middleware +/*! @__NO_SIDE_EFFECTS__ */ +export function defineNuxtRouteMiddleware (middleware: RouteMiddleware) { + return middleware +} export interface AddRouteMiddlewareOptions { global?: boolean @@ -81,55 +83,122 @@ const isProcessingMiddleware = () => { return false } -export interface NavigateToOptions { - replace?: boolean - redirectCode?: number, - external?: boolean +// Conditional types, either one or other +type Without = { [P in Exclude]?: never } +type XOR = (T | U) extends Object ? (Without & U) | (Without & T) : T | U + +export type OpenWindowFeatures = { + popup?: boolean + noopener?: boolean + noreferrer?: boolean +} & XOR<{width?: number}, {innerWidth?: number}> + & XOR<{height?: number}, {innerHeight?: number}> + & XOR<{left?: number}, {screenX?: number}> + & XOR<{top?: number}, {screenY?: number}> + +export type OpenOptions = { + target: '_blank' | '_parent' | '_self' | '_top' | (string & {}) + windowFeatures?: OpenWindowFeatures } -export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise | RouteLocationRaw => { +export interface NavigateToOptions { + replace?: boolean + redirectCode?: number + external?: boolean + open?: OpenOptions +} + +export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise | false | void | RouteLocationRaw => { if (!to) { to = '/' } - const toPath = typeof to === 'string' ? to : ((to as RouteLocationPathRaw).path || '/') - const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true }) - if (isExternal && !options?.external) { - throw new Error('Navigating to external URL is not allowed by default. Use `navigateTo (url, { external: true })`.') - } - if (isExternal && parseURL(toPath).protocol === 'script:') { - throw new Error('Cannot navigate to an URL with script protocol.') + const toPath = typeof to === 'string' ? to : (withQuery((to as RouteLocationPathRaw).path || '/', to.query || {}) + (to.hash || '')) + + // Early open handler + if (options?.open) { + if (import.meta.client) { + const { target = '_blank', windowFeatures = {} } = options.open + + const features = Object.entries(windowFeatures) + .filter(([_, value]) => value !== undefined) + .map(([feature, value]) => `${feature.toLowerCase()}=${value}`) + .join(', ') + + open(toPath, target, features) + } + + return Promise.resolve() } + const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true }) + if (isExternal) { + if (!options?.external) { + throw new Error('Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.') + } + const protocol = parseURL(toPath).protocol + if (protocol && isScriptProtocol(protocol)) { + throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`) + } + } + + const inMiddleware = isProcessingMiddleware() + // Early redirect on client-side - if (process.client && !isExternal && isProcessingMiddleware()) { + if (import.meta.client && !isExternal && inMiddleware) { return to } const router = useRouter() - if (process.server) { - const nuxtApp = useNuxtApp() - if (nuxtApp.ssrContext && nuxtApp.ssrContext.event) { - // Let vue-router handle internal redirects within middleware - // to prevent the navigation happening after response is sent - if (isProcessingMiddleware() && !isExternal) { - setResponseStatus(nuxtApp.ssrContext.event, options?.redirectCode || 302) + const nuxtApp = useNuxtApp() + + 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) + + async function redirect (response: any) { + // TODO: consider deprecating in favour of `app:rendered` and removing + await nuxtApp.callHook('app:redirected') + const encodedLoc = location.replace(/"/g, '%22') + nuxtApp.ssrContext!._renderResponse = { + statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302), + body: ``, + headers: { location } + } + return response + } + + // We wait to perform the redirect last in case any other middleware will intercept the redirect + // and redirect somewhere else instead. + if (!isExternal && inMiddleware) { + router.afterEach(final => final.fullPath === fullPath ? redirect(false) : undefined) return to } - const redirectLocation = isExternal ? toPath : joinURL(useRuntimeConfig().app.baseURL, router.resolve(to).fullPath || '/') - return nuxtApp.callHook('app:redirected') - .then(() => sendRedirect(nuxtApp.ssrContext!.event, redirectLocation, options?.redirectCode || 302)) + return redirect(!inMiddleware ? undefined : /* abort route navigation */ false) } } // Client-side redirection using vue-router if (isExternal) { + // Run any cleanup steps for the current scope, like ending BroadcastChannel + nuxtApp._scope.stop() if (options?.replace) { location.replace(toPath) } else { location.href = toPath } + // Within in a Nuxt route middleware handler + if (inMiddleware) { + // Abort navigation when app is hydrated + if (!nuxtApp.isHydrating) { + return false + } + // When app is hydrating (i.e. on page load), we don't want to abort navigation as + // it would lead to a 404 error / page that's blinking before location changes. + return new Promise(() => {}) + } return Promise.resolve() } @@ -138,28 +207,34 @@ 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.') } - if (err) { - throw createError(err) + + if (!err) { return false } + + err = createError(err) + + if (err.fatal) { + useNuxtApp().runWithContext(() => showError(err as NuxtError)) } - return false + + 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']) => { + const nuxtApp = useNuxtApp() + if (import.meta.server) { + if (import.meta.dev && getCurrentInstance() && nuxtApp.payload.state._layout !== 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 + nuxtApp.payload.state._layout = 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 && nuxtApp.payload.state._layout !== 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 f4c30eca8c..69fc78dd5a 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, appendHeader, getRequestHeaders } from 'h3' import type { NuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt' -export function useRequestHeaders (include: K[]): Record, string | undefined> -export function useRequestHeaders (): Readonly> +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,9 +29,16 @@ 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) } return _setResponseStatus(useRequestEvent(), arg1, arg2 as string | undefined) } + +export function prerenderRoutes (path: string | string[]) { + if (!process.server || !process.env.prerender) { return } + + const paths = Array.isArray(path) ? path : [path] + appendHeader(useRequestEvent(), 'x-nitro-prerender', paths.map(p => encodeURIComponent(p)).join(', ')) +} diff --git a/packages/nuxt/src/app/composables/state.ts b/packages/nuxt/src/app/composables/state.ts index 3a710ee6d3..3746b1f36b 100644 --- a/packages/nuxt/src/app/composables/state.ts +++ b/packages/nuxt/src/app/composables/state.ts @@ -2,9 +2,9 @@ import { isRef, toRef } from 'vue' import type { Ref } from 'vue' import { useNuxtApp } from '../nuxt' +const useStateKeyPrefix = '$s' /** * Create a global reactive ref that will be hydrated but not shared across ssr requests - * * @param key a unique key ensuring that data fetching can be properly de-duplicated across requests * @param init a function that provides initial value for the state when it's not initiated */ @@ -20,7 +20,7 @@ export function useState (...args: any): Ref { if (init !== undefined && typeof init !== 'function') { throw new Error('[nuxt] [useState] init must be a function: ' + init) } - const key = '$s' + _key + const key = useStateKeyPrefix + _key const nuxt = useNuxtApp() const state = toRef(nuxt.payload.state, key) @@ -35,3 +35,24 @@ export function useState (...args: any): Ref { } return state } + +export function clearNuxtState ( + keys?: string | string[] | ((key: string) => boolean) +): void { + const nuxtApp = useNuxtApp() + const _allKeys = Object.keys(nuxtApp.payload.state) + .map(key => key.substring(useStateKeyPrefix.length)) + + const _keys: string[] = !keys + ? _allKeys + : typeof keys === 'function' + ? _allKeys.filter(keys) + : Array.isArray(keys) ? keys : [keys] + + for (const _key of _keys) { + const key = useStateKeyPrefix + _key + if (key in nuxtApp.payload.state) { + nuxtApp.payload.state[key] = undefined + } + } +} diff --git a/packages/nuxt/src/app/composables/url.ts b/packages/nuxt/src/app/composables/url.ts new file mode 100644 index 0000000000..784ef860cd --- /dev/null +++ b/packages/nuxt/src/app/composables/url.ts @@ -0,0 +1,13 @@ +import { getRequestURL } from 'h3' +import { joinURL } from 'ufo' +import { useRequestEvent } from './ssr' +import { useRuntimeConfig } from '#app' + +export function useRequestURL () { + if (import.meta.server) { + const url = getRequestURL(useRequestEvent()) + url.pathname = joinURL(useRuntimeConfig().app.baseURL, url.pathname) + return url + } + return new URL(window.location.href) +} diff --git a/packages/nuxt/src/app/config.ts b/packages/nuxt/src/app/config.ts index 31d598cad1..3b8a86a2bc 100644 --- a/packages/nuxt/src/app/config.ts +++ b/packages/nuxt/src/app/config.ts @@ -1,7 +1,8 @@ import { reactive } from 'vue' +import { klona } from 'klona' import type { AppConfig } from 'nuxt/schema' import { useNuxtApp } from './nuxt' -// @ts-ignore +// @ts-expect-error virtual file import __appConfig from '#build/app.config.mjs' type DeepPartial = T extends Function ? T : T extends Record ? { [P in keyof T]?: DeepPartial } : T @@ -37,7 +38,7 @@ function deepAssign (obj: any, newObj: any) { export function useAppConfig (): AppConfig { const nuxtApp = useNuxtApp() if (!nuxtApp._appConfig) { - nuxtApp._appConfig = reactive(__appConfig) as AppConfig + nuxtApp._appConfig = (import.meta.server ? klona(__appConfig) : reactive(__appConfig)) as AppConfig } return nuxtApp._appConfig } @@ -53,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 4d5b6206ab..783151a062 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 6a08a00187..dede500a50 100644 --- a/packages/nuxt/src/app/entry.ts +++ b/packages/nuxt/src/app/entry.ts @@ -1,30 +1,32 @@ // We set __webpack_public_path via this import with webpack builder import { createApp, createSSRApp, nextTick } from 'vue' import { $fetch } from 'ofetch' -// @ts-ignore +import type { $Fetch, NitroFetchRequest } from 'nitropack' + +// This file must be imported first for webpack as we set __webpack_public_path__ there +// @ts-expect-error virtual file import { baseURL } from '#build/paths.mjs' + import type { CreateOptions } from '#app' -import { applyPlugins, createNuxtApp, normalizePlugins } from '#app/nuxt' +import { applyPlugins, createNuxtApp } from '#app/nuxt' + import '#build/css' -// @ts-ignore -import _plugins from '#build/plugins' -// @ts-ignore +// @ts-expect-error virtual file +import plugins from '#build/plugins' +// @ts-expect-error virtual file import RootComponent from '#build/root-component.mjs' -// @ts-ignore -import { appRootId } from '#build/nuxt.config.mjs' +// @ts-expect-error virtual file +import { vueAppRootContainer } from '#build/nuxt.config.mjs' if (!globalThis.$fetch) { - // @ts-ignore globalThis.$fetch = $fetch.create({ baseURL: baseURL() - }) + }) as $Fetch } let entry: Function -const plugins = normalizePlugins(_plugins) - -if (process.server) { +if (import.meta.server) { entry = async function createNuxtAppServer (ssrContext: CreateOptions['ssrContext']) { const vueApp = createApp(RootComponent) @@ -37,21 +39,24 @@ if (process.server) { await nuxt.hooks.callHook('app:error', err) nuxt.payload.error = (nuxt.payload.error || err) as any } + if (ssrContext?._renderResponse) { throw new Error('skipping render') } return vueApp } } -if (process.client) { +if (import.meta.client) { // TODO: temporary webpack 5 HMR fix // https://github.com/webpack-contrib/webpack-hot-middleware/issues/390 - // @ts-ignore - if (process.dev && import.meta.webpackHot) { - // @ts-ignore + if (import.meta.dev && import.meta.webpackHot) { import.meta.webpackHot.accept() } + // eslint-disable-next-line + let vueAppPromise: Promise + entry = async function initApp () { + if (vueAppPromise) { return vueAppPromise } const isSSR = Boolean( window.__NUXT__?.serverRendered || document.getElementById('__NUXT_DATA__')?.dataset.ssr === 'true' @@ -70,16 +75,18 @@ 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) { await nuxt.callHook('app:error', err) nuxt.payload.error = (nuxt.payload.error || err) as any } + + return vueApp } - entry().catch((error: unknown) => { + vueAppPromise = entry().catch((error: unknown) => { console.error('Error while mounting app:', error) }) } diff --git a/packages/nuxt/src/app/index.ts b/packages/nuxt/src/app/index.ts index 70ea4e391c..bd23bc8e74 100644 --- a/packages/nuxt/src/app/index.ts +++ b/packages/nuxt/src/app/index.ts @@ -4,6 +4,7 @@ export * from './nuxt' export * from './composables/index' export * from './components/index' export * from './config' +export * from './compat/idle-callback' // eslint-disable-next-line import/no-restricted-paths export type { PageMeta } from '../pages/runtime/index' diff --git a/packages/nuxt/src/app/middleware/manifest-route-rule.ts b/packages/nuxt/src/app/middleware/manifest-route-rule.ts new file mode 100644 index 0000000000..6cbd2b2d2a --- /dev/null +++ b/packages/nuxt/src/app/middleware/manifest-route-rule.ts @@ -0,0 +1,10 @@ +import { defineNuxtRouteMiddleware } from '#app/composables/router' +import { getRouteRules } from '#app/composables/manifest' + +export default defineNuxtRouteMiddleware(async (to) => { + if (import.meta.server || import.meta.test) { return } + const rules = await getRouteRules(to.path) + if (rules.redirect) { + return rules.redirect + } +}) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 35751ffe20..053eed35a3 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -1,33 +1,30 @@ /* eslint-disable no-use-before-define */ -import { getCurrentInstance, reactive, shallowReactive } from 'vue' -import type { App, Ref, VNode, onErrorCaptured } from 'vue' -import type { RouteLocationNormalizedLoaded } from 'vue-router' +import { effectScope, getCurrentInstance, hasInjectionContext, reactive } from 'vue' +import type { App, EffectScope, Ref, VNode, onErrorCaptured } from 'vue' +import type { RouteLocationNormalizedLoaded } from '#vue-router' import type { HookCallback, Hookable } from 'hookable' import { createHooks } from 'hookable' import { getContext } from 'unctx' -import type { SSRContext } from 'vue-bundle-renderer/runtime' +import type { SSRContext, createRenderer } from 'vue-bundle-renderer/runtime' 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' +import type { NuxtAppManifestMeta } from '#app/composables' -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 -type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'] } +type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited['renderToString']>> } export interface RuntimeNuxtHooks { 'app:created': (app: App) => HookResult 'app:beforeMount': (app: App) => HookResult @@ -39,9 +36,11 @@ export interface RuntimeNuxtHooks { 'app:error:cleared': (options: { redirect?: string }) => HookResult 'app:chunkError': (options: { error: any }) => HookResult 'app:data:refresh': (keys?: string[]) => HookResult + 'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult 'link:prefetch': (link: string) => HookResult 'page:start': (Component?: VNode) => HookResult 'page:finish': (Component?: VNode) => HookResult + 'page:transition:start': () => HookResult 'page:transition:finish': (Component?: VNode) => HookResult 'vue:setup': () => void 'vue:error': (...args: Parameters[0]>) => HookResult @@ -55,14 +54,37 @@ export interface NuxtSSRContext extends SSRContext { /** whether we are rendering an SSR error */ error?: boolean nuxt: _NuxtApp - payload: _NuxtApp['payload'] + 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 + /** @internal */ _payloadReducers: Record any> } +export interface NuxtPayload { + path?: string + serverRendered?: boolean + prerenderedAt?: number + data: Record + state: Record + config?: Pick + error?: Error | { + url: string + statusCode: number + statusMessage: string + message: string + description: string + data?: any + } | null + _errors: Record + [key: string]: unknown +} + interface _NuxtApp { vueApp: App globalName: string @@ -72,15 +94,20 @@ interface _NuxtApp { hook: _NuxtApp['hooks']['hook'] callHook: _NuxtApp['hooks']['callHook'] + runWithContext: any>(fn: T) => ReturnType | Promise>> + [key: string]: unknown + /** @internal */ + _scope: EffectScope /** @internal */ _asyncDataPromises: Record | undefined> /** @internal */ _asyncData: Record pending: Ref - error: Ref + error: Ref + status: Ref } | undefined> /** @internal */ @@ -92,7 +119,7 @@ interface _NuxtApp { /** @internal */ _observer?: { observe: (element: Element, callback: () => void) => () => void } /** @internal */ - _payloadCache?: Record> | Record> + _payloadCache?: Record> | Record | null> /** @internal */ _appConfig: AppConfig @@ -112,22 +139,7 @@ interface _NuxtApp { deferHydration: () => () => void | Promise ssrContext?: NuxtSSRContext - payload: { - serverRendered?: boolean - prerenderedAt?: number - data: Record - state: Record - error?: Error | { - url: string - statusCode: number - statusMessage: string - message: string - description: string - data?: any - } | null - _errors: Record - [key: string]: any - } + payload: NuxtPayload static: { data: Record } @@ -138,11 +150,51 @@ interface _NuxtApp { export interface NuxtApp extends _NuxtApp {} export const NuxtPluginIndicator = '__nuxt_plugin' + +export interface PluginMeta { + name?: string + enforce?: 'pre' | 'default' | 'post' + /** + * This allows more granular control over plugin order and should only be used by advanced users. + * It overrides the value of `enforce` and is used to sort plugins. + */ + 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 +} + export interface Plugin = Record> { (nuxt: _NuxtApp): Promise | Promise<{ provide?: Injections }> | void | { provide?: Injections } [NuxtPluginIndicator]?: true + meta?: ResolvedPluginMeta } +export interface ObjectPlugin = Record> extends PluginMeta { + hooks?: Partial + setup?: Plugin + env?: PluginEnvContext + /** + * Execute plugin in parallel with other parallel plugins. + * @default false + */ + parallel?: boolean +} + +/** @deprecated Use `ObjectPlugin` */ +export type ObjectPluginInput = Record> = ObjectPlugin + export interface CreateOptions { vueApp: NuxtApp['vueApp'] ssrContext?: NuxtApp['ssrContext'] @@ -152,22 +204,24 @@ export interface CreateOptions { export function createNuxtApp (options: CreateOptions) { let hydratingCount = 0 const nuxtApp: NuxtApp = { + _scope: effectScope(), provide: undefined, globalName: 'nuxt', versions: { get nuxt () { return __NUXT_VERSION__ }, get vue () { return nuxtApp.vueApp.version } }, - payload: shallowReactive({ - data: shallowReactive({}), - state: shallowReactive({}), - _errors: shallowReactive({}), - ...(process.client ? window.__NUXT__ ?? {} : { serverRendered: true }) + payload: reactive({ + data: {}, + state: {}, + _errors: {}, + ...(import.meta.client ? window.__NUXT__ ?? {} : { serverRendered: true }) }), static: { data: {} }, - isHydrating: process.client, + runWithContext: (fn: any) => nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn)), + isHydrating: import.meta.client, deferHydration () { if (!nuxtApp.isHydrating) { return () => {} } @@ -195,10 +249,10 @@ 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 nuxtAppCtx.call(nuxtApp, () => hook(...args)) + await nuxtApp.runWithContext(() => hook(...args)) } } // Patch callHook to preserve NuxtApp context on server @@ -216,17 +270,16 @@ export function createNuxtApp (options: CreateOptions) { // Inject $nuxt defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp) - // @ts-expect-error defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp) - if (process.server) { - // Expose nuxt to the renderContext + if (import.meta.server) { if (nuxtApp.ssrContext) { + // Expose nuxt to the renderContext nuxtApp.ssrContext.nuxt = nuxtApp - } - // Expose payload types - if (nuxtApp.ssrContext) { + // Expose payload types nuxtApp.ssrContext._payloadReducers = {} + // Expose current path + nuxtApp.payload.path = nuxtApp.ssrContext.url } // Expose to server renderer to create payload nuxtApp.ssrContext = nuxtApp.ssrContext || {} as any @@ -243,11 +296,13 @@ 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 }) }) + window.useNuxtApp = window.useNuxtApp || useNuxtApp + // Log errors captured when running plugins, in the `app:created` and `app:beforeMount` hooks // as well as when mounting the app. const unreg = nuxtApp.hook('app:error', (...args) => { console.error('[nuxt] error caught during app initialization', ...args) }) @@ -255,93 +310,51 @@ export function createNuxtApp (options: CreateOptions) { } // Expose runtime config - const runtimeConfig = process.server ? options.ssrContext!.runtimeConfig : reactive(nuxtApp.payload.config) - - // TODO: remove in v3.5 - // Backward compatibility following #4254 - const compatibilityConfig = new Proxy(runtimeConfig, { - get (target, prop: string) { - if (prop in target) { - return target[prop] - } - if (process.dev && prop in target.public) { - console.warn(`[nuxt] [runtimeConfig] You are trying to access a public runtime config value (\`${prop}\`) directly from the top level. This currently works (for backward compatibility with Nuxt 2) but this compatibility layer will be removed in v3.5. Instead, you can update \`config['${prop}']\` to \`config.public['${prop}']\`.`) - } - return target.public[prop] - }, - set (target, prop, value) { - if (process.server || prop === 'public' || prop === 'app') { - return false // Throws TypeError - } - target[prop] = value - target.public[prop] = value - return true - } - }) - - nuxtApp.provide('config', compatibilityConfig) + const runtimeConfig = import.meta.server ? options.ssrContext!.runtimeConfig : reactive(nuxtApp.payload.config!) + nuxtApp.provide('config', runtimeConfig) return nuxtApp } -export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin) { - if (typeof plugin !== 'function') { return } - const { provide } = await callWithNuxt(nuxtApp, plugin, [nuxtApp]) || {} - if (provide && typeof provide === 'object') { - for (const key in provide) { - nuxtApp.provide(key, provide[key]) +export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlugin) { + if (plugin.hooks) { + nuxtApp.hooks.addHooks(plugin.hooks) + } + if (typeof plugin === 'function') { + const { provide } = await nuxtApp.runWithContext(() => plugin(nuxtApp)) || {} + if (provide && typeof provide === 'object') { + for (const key in provide) { + nuxtApp.provide(key, provide[key]) + } } } } -export async function applyPlugins (nuxtApp: NuxtApp, plugins: Plugin[]) { +export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array>) { + const parallels: Promise[] = [] + const errors: Error[] = [] for (const plugin of plugins) { - await applyPlugin(nuxtApp, plugin) + 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))) + } else { + await promise + } } + await Promise.all(parallels) + if (errors.length) { throw errors[0] } } -export function normalizePlugins (_plugins: Plugin[]) { - const unwrappedPlugins: Plugin[] = [] - const legacyInjectPlugins: Plugin[] = [] - const invalidPlugins: Plugin[] = [] - - const plugins = _plugins.map((plugin) => { - if (typeof plugin !== 'function') { - invalidPlugins.push(plugin) - return null - } - if (plugin.length > 1) { - legacyInjectPlugins.push(plugin) - // Allow usage without wrapper but warn - // TODO: Skip invalid in next releases - // @ts-ignore - return (nuxtApp: NuxtApp) => plugin(nuxtApp, nuxtApp.provide) - // return null - } - if (!isNuxtPlugin(plugin)) { - unwrappedPlugins.push(plugin) - // Allow usage without wrapper but warn - } - return plugin - }).filter(Boolean) - - if (process.dev && legacyInjectPlugins.length) { - console.warn('[warn] [nuxt] You are using a plugin with legacy Nuxt 2 format (context, inject) which is likely to be broken. In the future they will be ignored:', legacyInjectPlugins.map(p => p.name || p).join(',')) - } - if (process.dev && invalidPlugins.length) { - console.warn('[warn] [nuxt] Some plugins are not exposing a function and skipped:', invalidPlugins) - } - if (process.dev && unwrappedPlugins.length) { - console.warn('[warn] [nuxt] You are using a plugin that has not been wrapped in `defineNuxtPlugin`. It is advised to wrap your plugins as in the future this may enable enhancements:', unwrappedPlugins.map(p => p.name || p).join(',')) - } - - return plugins as Plugin[] +/*! @__NO_SIDE_EFFECTS__ */ +export function defineNuxtPlugin> (plugin: Plugin | ObjectPlugin): Plugin & ObjectPlugin { + if (typeof plugin === 'function') { return plugin } + delete plugin.name + return Object.assign(plugin.setup || (() => {}), plugin, { [NuxtPluginIndicator]: true } as const) } -export function defineNuxtPlugin> (plugin: Plugin) { - plugin[NuxtPluginIndicator] = true - return plugin -} +/*! @__NO_SIDE_EFFECTS__ */ +export const definePayloadPlugin = defineNuxtPlugin export function isNuxtPlugin (plugin: unknown) { return typeof plugin === 'function' && NuxtPluginIndicator in plugin @@ -349,38 +362,44 @@ export function isNuxtPlugin (plugin: unknown) { /** * Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`. - * * @param nuxt A Nuxt instance * @param setup The function to call */ export function callWithNuxt any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters) { const fn: () => ReturnType = () => args ? setup(...args as Parameters) : setup() - if (process.server) { - return nuxtAppCtx.callAsync(nuxt as NuxtApp, fn) + 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 nuxtAppCtx.set(nuxt as NuxtApp) - return fn() + return nuxt.vueApp.runWithContext(fn) } } +/*! @__NO_SIDE_EFFECTS__ */ /** * Returns the current Nuxt instance. */ -export function useNuxtApp () { - const nuxtAppInstance = nuxtAppCtx.tryUse() +export function useNuxtApp (): NuxtApp { + let nuxtAppInstance + if (hasInjectionContext()) { + nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt + } + + nuxtAppInstance = nuxtAppInstance || nuxtAppCtx.tryUse() if (!nuxtAppInstance) { - const vm = getCurrentInstance() - if (!vm) { - throw new Error('nuxt instance unavailable') + 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') } - return vm.appContext.app.$nuxt as NuxtApp } return nuxtAppInstance } +/*! @__NO_SIDE_EFFECTS__ */ export function useRuntimeConfig (): RuntimeConfig { return useNuxtApp().$config } diff --git a/packages/nuxt/src/app/plugins/check-outdated-build.client.ts b/packages/nuxt/src/app/plugins/check-outdated-build.client.ts new file mode 100644 index 0000000000..27561909ad --- /dev/null +++ b/packages/nuxt/src/app/plugins/check-outdated-build.client.ts @@ -0,0 +1,23 @@ +import { joinURL } from 'ufo' +import type { NuxtAppManifestMeta } from '#app' +import { defineNuxtPlugin, getAppManifest, onNuxtReady, useRuntimeConfig } from '#app' + +export default defineNuxtPlugin((nuxtApp) => { + if (import.meta.test) { return } + + let timeout: NodeJS.Timeout + const config = useRuntimeConfig() + + async function getLatestManifest () { + const currentManifest = await getAppManifest() + if (timeout) { clearTimeout(timeout) } + timeout = setTimeout(getLatestManifest, 1000 * 60 * 60) + const meta = await $fetch(joinURL(config.app.cdnURL || config.app.baseURL, config.app.buildAssetsDir, 'builds/latest.json')) + if (meta.id !== currentManifest.id) { + // There is a newer build which we will let the user handle + nuxtApp.hooks.callHook('app:manifest:update', meta) + } + } + + onNuxtReady(() => { timeout = setTimeout(getLatestManifest, 1000 * 60 * 60) }) +}) diff --git a/packages/nuxt/src/app/plugins/chunk-reload.client.ts b/packages/nuxt/src/app/plugins/chunk-reload.client.ts index 55a1211e3f..f014272853 100644 --- a/packages/nuxt/src/app/plugins/chunk-reload.client.ts +++ b/packages/nuxt/src/app/plugins/chunk-reload.client.ts @@ -1,22 +1,34 @@ import { joinURL } from 'ufo' +import type { RouteLocationNormalized } from 'vue-router' import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt' import { useRouter } from '#app/composables/router' import { reloadNuxtApp } from '#app/composables/chunk' -export default defineNuxtPlugin((nuxtApp) => { - const router = useRouter() - const config = useRuntimeConfig() +export default defineNuxtPlugin({ + name: 'nuxt:chunk-reload', + setup (nuxtApp) { + const router = useRouter() + const config = useRuntimeConfig() - const chunkErrors = new Set() + const chunkErrors = new Set() - router.beforeEach(() => { chunkErrors.clear() }) - nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) }) + router.beforeEach(() => { chunkErrors.clear() }) + nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) }) - router.onError((error, to) => { - if (chunkErrors.has(error)) { + function reloadAppAtPath (to: RouteLocationNormalized) { const isHash = 'href' in to && (to.href as string).startsWith('#') const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath) reloadNuxtApp({ path, persistState: true }) } - }) + + nuxtApp.hook('app:manifest:update', () => { + router.beforeResolve(reloadAppAtPath) + }) + + router.onError((error, to) => { + if (chunkErrors.has(error)) { + reloadAppAtPath(to) + } + }) + } }) diff --git a/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts b/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts index 08e58dd695..191f69133b 100644 --- a/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts +++ b/packages/nuxt/src/app/plugins/cross-origin-prefetch.client.ts @@ -3,33 +3,36 @@ import { parseURL } from 'ufo' import { useHead } from '@unhead/vue' import { defineNuxtPlugin } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - const externalURLs = ref(new Set()) - function generateRules () { - return { - type: 'speculationrules', - key: 'speculationrules', - innerHTML: JSON.stringify({ - prefetch: [ - { - source: 'list', - urls: [...externalURLs.value], - requires: ['anonymous-client-ip-when-cross-origin'] - } - ] - }) +export default defineNuxtPlugin({ + name: 'nuxt:cross-origin-prefetch', + setup (nuxtApp) { + const externalURLs = ref(new Set()) + function generateRules () { + return { + type: 'speculationrules', + key: 'speculationrules', + innerHTML: JSON.stringify({ + prefetch: [ + { + source: 'list', + urls: [...externalURLs.value], + requires: ['anonymous-client-ip-when-cross-origin'] + } + ] + }) + } } + const head = useHead({ + script: [generateRules()] + }) + nuxtApp.hook('link:prefetch', (url) => { + const { protocol } = parseURL(url) + if (protocol && ['http:', 'https:'].includes(protocol)) { + externalURLs.value.add(url) + head?.patch({ + script: [generateRules()] + }) + } + }) } - const head = useHead({ - script: [generateRules()] - }) - nuxtApp.hook('link:prefetch', (url) => { - const { protocol } = parseURL(url) - if (protocol && ['http:', 'https:'].includes(protocol)) { - externalURLs.value.add(url) - head?.patch({ - script: [generateRules()] - }) - } - }) }) diff --git a/packages/nuxt/src/app/plugins/debug.ts b/packages/nuxt/src/app/plugins/debug.ts index f2909d069e..5479a84d45 100644 --- a/packages/nuxt/src/app/plugins/debug.ts +++ b/packages/nuxt/src/app/plugins/debug.ts @@ -1,6 +1,10 @@ import { createDebugger } from 'hookable' import { defineNuxtPlugin } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - createDebugger(nuxtApp.hooks, { tag: 'nuxt-app' }) +export default defineNuxtPlugin({ + name: 'nuxt:debug', + enforce: 'pre', + setup (nuxtApp) { + createDebugger(nuxtApp.hooks, { tag: 'nuxt-app' }) + } }) diff --git a/packages/nuxt/src/app/plugins/payload.client.ts b/packages/nuxt/src/app/plugins/payload.client.ts index 55d6417c31..0291ccd35f 100644 --- a/packages/nuxt/src/app/plugins/payload.client.ts +++ b/packages/nuxt/src/app/plugins/payload.client.ts @@ -1,27 +1,36 @@ import { parseURL } from 'ufo' import { defineNuxtPlugin } from '#app/nuxt' -import { isPrerendered, loadPayload } from '#app/composables/payload' +import { loadPayload } from '#app/composables/payload' +import { onNuxtReady } from '#app/composables/ready' import { useRouter } from '#app/composables/router' +import { getAppManifest } from '#app/composables/manifest' +// @ts-expect-error virtual file +import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs' -export default defineNuxtPlugin((nuxtApp) => { - // Only enable behavior if initial page is prerendered - // TODO: Support hybrid and dev - if (!isPrerendered()) { - return +export default defineNuxtPlugin({ + name: 'nuxt:payload', + setup (nuxtApp) { + // TODO: Support dev + if (process.dev) { return } + + // Load payload after middleware & once final route is resolved + useRouter().beforeResolve(async (to, from) => { + if (to.path === from.path) { return } + const payload = await loadPayload(to.path) + if (!payload) { return } + Object.assign(nuxtApp.static.data, payload.data) + }) + + onNuxtReady(() => { + // Load payload into cache + nuxtApp.hooks.hook('link:prefetch', async (url) => { + if (!parseURL(url).protocol) { + await loadPayload(url) + } + }) + if (isAppManifestEnabled && navigator.connection?.effectiveType !== 'slow-2g') { + setTimeout(getAppManifest, 1000) + } + }) } - - // Load payload into cache - nuxtApp.hooks.hook('link:prefetch', async (url) => { - if (!parseURL(url).protocol) { - await loadPayload(url) - } - }) - - // Load payload after middleware & once final route is resolved - useRouter().beforeResolve(async (to, from) => { - if (to.path === from.path) { return } - const payload = await loadPayload(to.path) - if (!payload) { return } - Object.assign(nuxtApp.static.data, payload.data) - }) }) diff --git a/packages/nuxt/src/app/plugins/preload.server.ts b/packages/nuxt/src/app/plugins/preload.server.ts index 9f6b25d62d..ec9501988b 100644 --- a/packages/nuxt/src/app/plugins/preload.server.ts +++ b/packages/nuxt/src/app/plugins/preload.server.ts @@ -1,11 +1,14 @@ import { defineNuxtPlugin } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.vueApp.mixin({ - beforeCreate () { - const { _registeredComponents } = this.$nuxt.ssrContext - const { __moduleIdentifier } = this.$options - _registeredComponents.add(__moduleIdentifier) - } - }) +export default defineNuxtPlugin({ + name: 'nuxt:webpack-preload', + setup (nuxtApp) { + nuxtApp.vueApp.mixin({ + beforeCreate () { + const { _registeredComponents } = this.$nuxt.ssrContext + const { __moduleIdentifier } = this.$options + _registeredComponents.add(__moduleIdentifier) + } + }) + } }) diff --git a/packages/nuxt/src/app/plugins/restore-state.client.ts b/packages/nuxt/src/app/plugins/restore-state.client.ts index 6533650ce9..0f47989c54 100644 --- a/packages/nuxt/src/app/plugins/restore-state.client.ts +++ b/packages/nuxt/src/app/plugins/restore-state.client.ts @@ -1,13 +1,18 @@ -import { defineNuxtPlugin } from '#app/nuxt' +import destr from 'destr' +import { defineNuxtPlugin, useNuxtApp } from '#app/nuxt' -export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.hook('app:mounted', () => { - try { - const state = sessionStorage.getItem('nuxt:reload:state') - if (state) { - sessionStorage.removeItem('nuxt:reload:state') - Object.assign(nuxtApp.payload.state, JSON.parse(state)?.state) - } - } catch {} - }) +export default defineNuxtPlugin({ + name: 'nuxt:restore-state', + hooks: { + 'app:mounted' () { + const nuxtApp = useNuxtApp() + try { + const state = sessionStorage.getItem('nuxt:reload:state') + if (state) { + sessionStorage.removeItem('nuxt:reload:state') + Object.assign(nuxtApp.payload.state, destr>(state)?.state) + } + } catch {} + } + } }) diff --git a/packages/nuxt/src/app/plugins/revive-payload.client.ts b/packages/nuxt/src/app/plugins/revive-payload.client.ts index e7fa0eaa4c..515311b554 100644 --- a/packages/nuxt/src/app/plugins/revive-payload.client.ts +++ b/packages/nuxt/src/app/plugins/revive-payload.client.ts @@ -1,23 +1,47 @@ import { reactive, ref, shallowReactive, shallowRef } from 'vue' +import destr from 'destr' import { definePayloadReviver, getNuxtClientPayload } from '#app/composables/payload' import { createError } from '#app/composables/error' -import { callWithNuxt, defineNuxtPlugin } from '#app/nuxt' +import { defineNuxtPlugin, useNuxtApp } from '#app/nuxt' -const revivers = { - NuxtError: (data: any) => createError(data), - EmptyShallowRef: (data: any) => shallowRef(JSON.parse(data)), - EmptyRef: (data: any) => ref(JSON.parse(data)), - ShallowRef: (data: any) => shallowRef(data), - ShallowReactive: (data: any) => shallowReactive(data), - Ref: (data: any) => ref(data), - Reactive: (data: any) => reactive(data) +// @ts-expect-error Virtual file. +import { componentIslands } from '#build/nuxt.config.mjs' + +const revivers: Record any> = { + NuxtError: data => createError(data), + EmptyShallowRef: data => shallowRef(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data)), + EmptyRef: data => ref(data === '_' ? undefined : data === '0n' ? BigInt(0) : destr(data)), + ShallowRef: data => shallowRef(data), + ShallowReactive: data => shallowReactive(data), + Ref: data => ref(data), + Reactive: data => reactive(data) } -export default defineNuxtPlugin(async (nuxtApp) => { - for (const reviver in revivers) { - definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers]) +if (componentIslands) { + revivers.Island = ({ key, params }: any) => { + const nuxtApp = useNuxtApp() + if (!nuxtApp.isHydrating) { + nuxtApp.payload.data[key] = nuxtApp.payload.data[key] || $fetch(`/__nuxt_island/${key}`, { + responseType: 'json', + ...params ? { params } : {} + }).then((r) => { + nuxtApp.payload.data[key] = r + return r + }) + } + return null + } +} + +export default defineNuxtPlugin({ + name: 'nuxt:revive-payload:client', + order: -30, + async setup (nuxtApp) { + for (const reviver in revivers) { + definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers]) + } + Object.assign(nuxtApp.payload, await nuxtApp.runWithContext(getNuxtClientPayload)) + // For backwards compatibility - TODO: remove later + window.__NUXT__ = nuxtApp.payload } - Object.assign(nuxtApp.payload, await callWithNuxt(nuxtApp, getNuxtClientPayload, [])) - // For backwards compatibility - TODO: remove later - window.__NUXT__ = nuxtApp.payload }) diff --git a/packages/nuxt/src/app/plugins/revive-payload.server.ts b/packages/nuxt/src/app/plugins/revive-payload.server.ts index d04c4ac4b0..9196257e81 100644 --- a/packages/nuxt/src/app/plugins/revive-payload.server.ts +++ b/packages/nuxt/src/app/plugins/revive-payload.server.ts @@ -2,20 +2,29 @@ import { isReactive, isRef, isShallow, toRaw } from 'vue' import { definePayloadReducer } from '#app/composables/payload' import { isNuxtError } from '#app/composables/error' import { defineNuxtPlugin } from '#app/nuxt' -/* Defining a plugin that will be used by the Nuxt framework. */ -const reducers = { - NuxtError: (data: any) => isNuxtError(data) && data.toJSON(), - EmptyShallowRef: (data: any) => isRef(data) && isShallow(data) && !data.value && JSON.stringify(data.value), - EmptyRef: (data: any) => isRef(data) && !data.value && JSON.stringify(data.value), - ShallowRef: (data: any) => isRef(data) && isShallow(data) && data.value, - ShallowReactive: (data: any) => isReactive(data) && isShallow(data) && toRaw(data), - Ref: (data: any) => isRef(data) && data.value, - Reactive: (data: any) => isReactive(data) && toRaw(data) +// @ts-expect-error Virtual file. +import { componentIslands } from '#build/nuxt.config.mjs' + +const reducers: Record any> = { + NuxtError: data => isNuxtError(data) && data.toJSON(), + EmptyShallowRef: data => isRef(data) && isShallow(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_')), + EmptyRef: data => isRef(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_')), + ShallowRef: data => isRef(data) && isShallow(data) && data.value, + ShallowReactive: data => isReactive(data) && isShallow(data) && toRaw(data), + Ref: data => isRef(data) && data.value, + Reactive: data => isReactive(data) && toRaw(data) } -export default defineNuxtPlugin(() => { - for (const reducer in reducers) { - definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers]) +if (componentIslands) { + reducers.Island = data => data && data?.__nuxt_island +} + +export default defineNuxtPlugin({ + name: 'nuxt:revive-payload:server', + setup () { + for (const reducer in reducers) { + definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers]) + } } }) diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 8b61b0f245..6920fdea46 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -1,13 +1,11 @@ import { h, isReadonly, reactive } from 'vue' import { isEqual, joinURL, parseQuery, parseURL, stringifyParsedURL, stringifyQuery, withoutBase } from 'ufo' import { createError } from 'h3' -import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig } from '../nuxt' +import { defineNuxtPlugin, useRuntimeConfig } from '../nuxt' import { clearError, showError } from '../composables/error' import { navigateTo } from '../composables/router' -import { useState } from '../composables/state' -import { useRequestEvent } from '../composables/ssr' -// @ts-ignore +// @ts-expect-error virtual file import { globalMiddleware } from '#build/middleware' interface Route { @@ -59,7 +57,7 @@ function getRouteFromPath (fullPath: string | Partial) { } } -type RouteGuardReturn = void | Error | string | false +type RouteGuardReturn = void | Error | string | boolean interface RouteGuard { (to: Route, from: Route): RouteGuardReturn | Promise @@ -96,176 +94,180 @@ interface Router { removeRoute: (name: string) => void } -export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { - const initialURL = process.client - ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash - : nuxtApp.ssrContext!.url +export default defineNuxtPlugin<{ route: Route, router: Router }>({ + name: 'nuxt:router', + enforce: 'pre', + setup (nuxtApp) { + const initialURL = import.meta.client + ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash + : nuxtApp.ssrContext!.url - const routes: Route[] = [] + const routes: Route[] = [] - const hooks: { [key in keyof RouterHooks]: RouterHooks[key][] } = { - 'navigate:before': [], - 'resolve:before': [], - 'navigate:after': [], - error: [] - } + const hooks: { [key in keyof RouterHooks]: RouterHooks[key][] } = { + 'navigate:before': [], + 'resolve:before': [], + 'navigate:after': [], + error: [] + } - const registerHook = (hook: T, guard: RouterHooks[T]) => { - hooks[hook].push(guard) - return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1) - } - const baseURL = useRuntimeConfig().app.baseURL + const registerHook = (hook: T, guard: RouterHooks[T]) => { + hooks[hook].push(guard) + return () => hooks[hook].splice(hooks[hook].indexOf(guard), 1) + } + const baseURL = useRuntimeConfig().app.baseURL - const route: Route = reactive(getRouteFromPath(initialURL)) - async function handleNavigation (url: string | Partial, replace?: boolean): Promise { - try { - // Resolve route - const to = getRouteFromPath(url) + const route: Route = reactive(getRouteFromPath(initialURL)) + async function handleNavigation (url: string | Partial, replace?: boolean): Promise { + try { + // Resolve route + const to = getRouteFromPath(url) - // Run beforeEach hooks - for (const middleware of hooks['navigate:before']) { - const result = await middleware(to, route) - // Cancel navigation - if (result === false || result instanceof Error) { return } - // Redirect - if (result) { return handleNavigation(result, true) } - } - - for (const handler of hooks['resolve:before']) { - await handler(to, route) - } - // Perform navigation - Object.assign(route, to) - if (process.client) { - window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath)) - if (!nuxtApp.isHydrating) { - // Clear any existing errors - await callWithNuxt(nuxtApp, clearError) + // Run beforeEach hooks + for (const middleware of hooks['navigate:before']) { + const result = await middleware(to, route) + // Cancel navigation + if (result === false || result instanceof Error) { return } + // Redirect + if (typeof result === 'string' && result.length) { return handleNavigation(result, true) } } - } - // Run afterEach hooks - for (const middleware of hooks['navigate:after']) { - await middleware(to, route) - } - } catch (err) { - if (process.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) { - await handler(err) - } - } - } - const router: Router = { - currentRoute: route, - isReady: () => Promise.resolve(), - // These options provide a similar API to vue-router but have no effect - options: {}, - install: () => Promise.resolve(), - // Navigation - push: (url: string) => handleNavigation(url, false), - replace: (url: string) => handleNavigation(url, true), - back: () => window.history.go(-1), - go: (delta: number) => window.history.go(delta), - forward: () => window.history.go(1), - // Guards - beforeResolve: (guard: RouterHooks['resolve:before']) => registerHook('resolve:before', guard), - beforeEach: (guard: RouterHooks['navigate:before']) => registerHook('navigate:before', guard), - afterEach: (guard: RouterHooks['navigate:after']) => registerHook('navigate:after', guard), - onError: (handler: RouterHooks['error']) => registerHook('error', handler), - // Routes - resolve: getRouteFromPath, - addRoute: (parentName: string, route: Route) => { routes.push(route) }, - getRoutes: () => routes, - hasRoute: (name: string) => routes.some(route => route.name === name), - removeRoute: (name: string) => { - const index = routes.findIndex(route => route.name === name) - if (index !== -1) { - routes.splice(index, 1) - } - } - } - - nuxtApp.vueApp.component('RouterLink', { - functional: true, - props: { - to: String, - custom: Boolean, - replace: Boolean, - // Not implemented - activeClass: String, - exactActiveClass: String, - ariaCurrentValue: String - }, - setup: (props, { slots }) => { - const navigate = () => handleNavigation(props.to, props.replace) - return () => { - const route = router.resolve(props.to) - return props.custom - ? slots.default?.({ href: props.to, navigate, route }) - : h('a', { href: props.to, onClick: (e: MouseEvent) => { e.preventDefault(); return navigate() } }, slots) - } - } - }) - - if (process.client) { - window.addEventListener('popstate', (event) => { - const location = (event.target as Window).location - router.replace(location.href.replace(location.origin, '')) - }) - } - - nuxtApp._route = route - - // Handle middleware - nuxtApp._middleware = nuxtApp._middleware || { - global: [], - named: {} - } - - const initialLayout = useState('_layout') - nuxtApp.hooks.hookOnce('app:created', async () => { - router.beforeEach(async (to, from) => { - to.meta = reactive(to.meta || {}) - if (nuxtApp.isHydrating && initialLayout.value && !isReadonly(to.meta.layout)) { - to.meta.layout = initialLayout.value - } - nuxtApp._processingMiddleware = true - - const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global]) - - for (const middleware of middlewareEntries) { - const result = await callWithNuxt(nuxtApp, middleware, [to, from]) - if (process.server) { - if (result === false || result instanceof Error) { - const error = result || createError({ - statusCode: 404, - statusMessage: `Page Not Found: ${initialURL}` - }) - return callWithNuxt(nuxtApp, showError, [error]) + for (const handler of hooks['resolve:before']) { + await handler(to, route) + } + // Perform navigation + Object.assign(route, to) + if (import.meta.client) { + window.history[replace ? 'replaceState' : 'pushState']({}, '', joinURL(baseURL, to.fullPath)) + if (!nuxtApp.isHydrating) { + // Clear any existing errors + await nuxtApp.runWithContext(clearError) } } - if (result || result === false) { return result } + // Run afterEach hooks + for (const middleware of hooks['navigate:after']) { + await middleware(to, route) + } + } catch (err) { + 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) { + await handler(err) + } + } + } + + const router: Router = { + currentRoute: route, + isReady: () => Promise.resolve(), + // These options provide a similar API to vue-router but have no effect + options: {}, + install: () => Promise.resolve(), + // Navigation + push: (url: string) => handleNavigation(url, false), + replace: (url: string) => handleNavigation(url, true), + back: () => window.history.go(-1), + go: (delta: number) => window.history.go(delta), + forward: () => window.history.go(1), + // Guards + beforeResolve: (guard: RouterHooks['resolve:before']) => registerHook('resolve:before', guard), + beforeEach: (guard: RouterHooks['navigate:before']) => registerHook('navigate:before', guard), + afterEach: (guard: RouterHooks['navigate:after']) => registerHook('navigate:after', guard), + onError: (handler: RouterHooks['error']) => registerHook('error', handler), + // Routes + resolve: getRouteFromPath, + addRoute: (parentName: string, route: Route) => { routes.push(route) }, + getRoutes: () => routes, + hasRoute: (name: string) => routes.some(route => route.name === name), + removeRoute: (name: string) => { + const index = routes.findIndex(route => route.name === name) + if (index !== -1) { + routes.splice(index, 1) + } + } + } + + nuxtApp.vueApp.component('RouterLink', { + functional: true, + props: { + to: String, + custom: Boolean, + replace: Boolean, + // Not implemented + activeClass: String, + exactActiveClass: String, + ariaCurrentValue: String + }, + setup: (props, { slots }) => { + const navigate = () => handleNavigation(props.to, props.replace) + return () => { + const route = router.resolve(props.to) + return props.custom + ? slots.default?.({ href: props.to, navigate, route }) + : h('a', { href: props.to, onClick: (e: MouseEvent) => { e.preventDefault(); return navigate() } }, slots) + } } }) - router.afterEach(() => { - delete nuxtApp._processingMiddleware + if (import.meta.client) { + window.addEventListener('popstate', (event) => { + const location = (event.target as Window).location + router.replace(location.href.replace(location.origin, '')) + }) + } + + nuxtApp._route = route + + // Handle middleware + nuxtApp._middleware = nuxtApp._middleware || { + global: [], + named: {} + } + + const initialLayout = nuxtApp.payload.state._layout + nuxtApp.hooks.hookOnce('app:created', async () => { + router.beforeEach(async (to, from) => { + to.meta = reactive(to.meta || {}) + if (nuxtApp.isHydrating && initialLayout && !isReadonly(to.meta.layout)) { + to.meta.layout = initialLayout + } + nuxtApp._processingMiddleware = true + + 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 (import.meta.server) { + if (result === false || result instanceof Error) { + const error = result || createError({ + statusCode: 404, + statusMessage: `Page Not Found: ${initialURL}` + }) + delete nuxtApp._processingMiddleware + return nuxtApp.runWithContext(() => showError(error)) + } + } + if (result === true) { continue } + if (result || result === false) { return result } + } + } + }) + + router.afterEach(() => { delete nuxtApp._processingMiddleware }) + + await router.replace(initialURL) + if (!isEqual(route.fullPath, initialURL)) { + await nuxtApp.runWithContext(() => navigateTo(route.fullPath)) + } }) - await router.replace(initialURL) - if (!isEqual(route.fullPath, initialURL)) { - const event = await callWithNuxt(nuxtApp, useRequestEvent) - const options = { redirectCode: event.node.res.statusCode !== 200 ? event.node.res.statusCode || 302 : 302 } - await callWithNuxt(nuxtApp, navigateTo, [route.fullPath, options]) - } - }) - - return { - provide: { - route, - router + return { + provide: { + route, + router + } } } }) diff --git a/packages/nuxt/src/app/plugins/view-transitions.client.ts b/packages/nuxt/src/app/plugins/view-transitions.client.ts new file mode 100644 index 0000000000..f0f75fcd2b --- /dev/null +++ b/packages/nuxt/src/app/plugins/view-transitions.client.ts @@ -0,0 +1,56 @@ +import { useRouter } from '#app/composables/router' +import { defineNuxtPlugin } from '#app/nuxt' + +export default defineNuxtPlugin((nuxtApp) => { + if (!document.startViewTransition) { return } + + let finishTransition: undefined | (() => void) + let abortTransition: undefined | (() => void) + + const router = useRouter() + + router.beforeResolve((to, from) => { + if (to === from || to.matched.every((comp, index) => comp.components && comp.components?.default === from.matched[index]?.components?.default)) { + return + } + const promise = new Promise((resolve, reject) => { + finishTransition = resolve + abortTransition = reject + }) + + let changeRoute: () => void + const ready = new Promise(resolve => (changeRoute = resolve)) + + const transition = document.startViewTransition!(() => { + changeRoute() + return promise + }) + + transition.finished.then(() => { + abortTransition = undefined + finishTransition = undefined + }) + + return ready + }) + + nuxtApp.hook('vue:error', () => { + abortTransition?.() + abortTransition = undefined + }) + + nuxtApp.hook('page:finish', () => { + finishTransition?.() + finishTransition = undefined + }) +}) + +declare global { + interface Document { + startViewTransition?: (callback: () => Promise | void) => { + finished: Promise + updateCallbackDone: Promise + ready: Promise + } + } +} diff --git a/packages/nuxt/src/app/types/augments.d.ts b/packages/nuxt/src/app/types/augments.d.ts index 675af7443d..8a6c9c82d8 100644 --- a/packages/nuxt/src/app/types/augments.d.ts +++ b/packages/nuxt/src/app/types/augments.d.ts @@ -1,19 +1,23 @@ -import type { NuxtApp } from '../nuxt' +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 } } @@ -22,6 +26,9 @@ declare module 'vue' { interface App { $nuxt: NuxtApp } + interface ComponentCustomProperties { + $nuxt: NuxtApp + } interface ComponentInternalInstance { _nuxtOnBeforeMountCbs: Function[] } diff --git a/packages/nuxt/src/components/client-fallback-auto-id.ts b/packages/nuxt/src/components/client-fallback-auto-id.ts index 370122be15..3f01c80c25 100644 --- a/packages/nuxt/src/components/client-fallback-auto-id.ts +++ b/packages/nuxt/src/components/client-fallback-auto-id.ts @@ -3,7 +3,7 @@ import type { ComponentsOptions } from '@nuxt/schema' import MagicString from 'magic-string' import { isAbsolute, relative } from 'pathe' import { hash } from 'ohash' -import { isVueTemplate } from './helpers' +import { isVue } from '../core/utils' interface LoaderOptions { sourcemap?: boolean transform?: ComponentsOptions['transform'], @@ -19,13 +19,13 @@ export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions name: 'nuxt:client-fallback-auto-id', enforce: 'pre', transformInclude (id) { - if (exclude.some(pattern => id.match(pattern))) { + if (exclude.some(pattern => pattern.test(id))) { return false } - if (include.some(pattern => id.match(pattern))) { + if (include.some(pattern => pattern.test(id))) { return true } - return isVueTemplate(id) + return isVue(id) }, transform (code, id) { if (!CLIENT_FALLBACK_RE.test(code)) { return } @@ -44,7 +44,7 @@ export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions return { code: s.toString(), map: options.sourcemap - ? s.generateMap({ source: id, includeContent: true }) + ? s.generateMap({ hires: true }) : undefined } } diff --git a/packages/nuxt/src/components/helpers.ts b/packages/nuxt/src/components/helpers.ts deleted file mode 100644 index ebd4ccf5fe..0000000000 --- a/packages/nuxt/src/components/helpers.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { pathToFileURL } from 'node:url' -import { parseQuery, parseURL } from 'ufo' - -export function isVueTemplate (id: string) { - // Bare `.vue` file (in Vite) - if (id.endsWith('.vue')) { - return true - } - - const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) - if (!search) { - return false - } - - const query = parseQuery(search) - - // Macro - if (query.macro) { - return true - } - - // Non-Vue or Styles - if (!('vue' in query) || query.type === 'style') { - return false - } - - // Query `?vue&type=template` (in Webpack or external template) - return true -} diff --git a/packages/nuxt/src/components/islandsTransform.ts b/packages/nuxt/src/components/islandsTransform.ts new file mode 100644 index 0000000000..388764f334 --- /dev/null +++ b/packages/nuxt/src/components/islandsTransform.ts @@ -0,0 +1,107 @@ +import { pathToFileURL } from 'node:url' +import type { Component } from '@nuxt/schema' +import { parseURL } from 'ufo' +import { createUnplugin } from 'unplugin' +import MagicString from 'magic-string' +import { ELEMENT_NODE, parse, walk } from 'ultrahtml' +import { isVue } from '../core/utils' + +interface ServerOnlyComponentTransformPluginOptions { + getComponents: () => Component[] +} + +const SCRIPT_RE = /]*>/g +const HAS_SLOT_RE = /]*>/ +const TEMPLATE_RE = / + + diff --git a/test/fixtures/basic/pages/async-parent.vue b/test/fixtures/basic/pages/async-parent.vue index 7eda70a494..322b0944d9 100644 --- a/test/fixtures/basic/pages/async-parent.vue +++ b/test/fixtures/basic/pages/async-parent.vue @@ -7,7 +7,9 @@ diff --git a/test/fixtures/basic/pages/client-fallback.vue b/test/fixtures/basic/pages/client-fallback.vue index c2b0fce213..9b4d7c5db8 100644 --- a/test/fixtures/basic/pages/client-fallback.vue +++ b/test/fixtures/basic/pages/client-fallback.vue @@ -38,6 +38,16 @@ + +
+ +
+ +
+ + to no script + + Test + + to big 1 + @@ -42,6 +70,11 @@ const config = useRuntimeConfig() const someValue = useState('val', () => 1) +const NestedSugarCounter = resolveComponent('NestedSugarCounter') +if (!NestedSugarCounter) { + throw new Error('Component not found') +} + definePageMeta({ alias: '/some-alias', other: ref('test'), diff --git a/test/fixtures/basic/pages/instance/error.vue b/test/fixtures/basic/pages/instance/error.vue new file mode 100644 index 0000000000..34bb64704b --- /dev/null +++ b/test/fixtures/basic/pages/instance/error.vue @@ -0,0 +1,13 @@ + + + diff --git a/test/fixtures/basic/pages/instance/next-request.vue b/test/fixtures/basic/pages/instance/next-request.vue new file mode 100644 index 0000000000..60ba42ba28 --- /dev/null +++ b/test/fixtures/basic/pages/instance/next-request.vue @@ -0,0 +1,14 @@ + + diff --git a/test/fixtures/basic/pages/islands.vue b/test/fixtures/basic/pages/islands.vue index 6ea3d6d5d7..df8d35af54 100644 --- a/test/fixtures/basic/pages/islands.vue +++ b/test/fixtures/basic/pages/islands.vue @@ -6,8 +6,9 @@ const islandProps = ref({ obj: { json: 'works' } }) +const showIslandSlot = ref(false) const routeIslandVisible = ref(false) - +const testCount = ref(0) const count = ref(0) @@ -18,7 +19,7 @@ const count = ref(0) -
@@ -26,18 +27,51 @@ const count = ref(0)
-

async .server component

- + +
+ Slot with in .server component +
+
- Async island component (20ms): - - +
+ +
+

Island with props mounted client side

+ +
+ +
Interactive testing slot post SSR
+ +
+
diff --git a/test/fixtures/basic/pages/json-payload.vue b/test/fixtures/basic/pages/json-payload.vue index 97a7fc8dd0..586db8fe33 100644 --- a/test/fixtures/basic/pages/json-payload.vue +++ b/test/fixtures/basic/pages/json-payload.vue @@ -1,14 +1,19 @@ @@ -16,9 +21,12 @@ if (process.server) {
{{ state }}
Date: {{ state.date instanceof Date }}
+ BigInt: {{ nonDisplayedState.bigint === 0n }}
Error: {{ isNuxtError(state.error) }}
Shallow reactive: {{ isReactive(state.shallowReactive) && isShallow(state.shallowReactive) }}
Shallow ref: {{ isShallow(state.shallowRef) }}
+ Undefined ref: {{ isRef(state.undefined) }}
+ BigInt ref: {{ isRef(nonDisplayedState.bigintRef) && typeof nonDisplayedState.bigintRef.value === 'bigint' }}
Reactive: {{ isReactive(state.reactive) }}
Ref: {{ isRef(state.ref) }}
Recursive objects: {{ state.ref === state.shallowReactive.nested.ref }}
diff --git a/test/fixtures/basic/pages/keyed-child-parent/[foo].vue b/test/fixtures/basic/pages/keyed-child-parent/[foo].vue index ed816b1cec..08641443f9 100644 --- a/test/fixtures/basic/pages/keyed-child-parent/[foo].vue +++ b/test/fixtures/basic/pages/keyed-child-parent/[foo].vue @@ -8,7 +8,9 @@ diff --git a/test/fixtures/basic/pages/navigate-to-redirect.vue b/test/fixtures/basic/pages/navigate-to-redirect.vue index b705612434..5d6da52c93 100644 --- a/test/fixtures/basic/pages/navigate-to-redirect.vue +++ b/test/fixtures/basic/pages/navigate-to-redirect.vue @@ -4,9 +4,15 @@ diff --git a/test/fixtures/basic/pages/nested/[foo].vue b/test/fixtures/basic/pages/nested/[foo].vue new file mode 100644 index 0000000000..1afdd3b4ab --- /dev/null +++ b/test/fixtures/basic/pages/nested/[foo].vue @@ -0,0 +1,21 @@ + + + + + diff --git a/test/fixtures/basic/pages/nested/[foo]/[bar].vue b/test/fixtures/basic/pages/nested/[foo]/[bar].vue index 2f0a50e69f..a0dce6983d 100644 --- a/test/fixtures/basic/pages/nested/[foo]/[bar].vue +++ b/test/fixtures/basic/pages/nested/[foo]/[bar].vue @@ -1,7 +1,17 @@ + + diff --git a/test/fixtures/basic/pages/nested/[foo]/index.vue b/test/fixtures/basic/pages/nested/[foo]/index.vue deleted file mode 100644 index b2ea680a54..0000000000 --- a/test/fixtures/basic/pages/nested/[foo]/index.vue +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/test/fixtures/basic/pages/nested/[foo]/user-[group].vue b/test/fixtures/basic/pages/nested/[foo]/user-[group].vue index 027545a80f..9847e51563 100644 --- a/test/fixtures/basic/pages/nested/[foo]/user-[group].vue +++ b/test/fixtures/basic/pages/nested/[foo]/user-[group].vue @@ -1,7 +1,18 @@ + + diff --git a/test/fixtures/basic/pages/no-scripts.vue b/test/fixtures/basic/pages/no-scripts.vue new file mode 100644 index 0000000000..a2d01e200c --- /dev/null +++ b/test/fixtures/basic/pages/no-scripts.vue @@ -0,0 +1,12 @@ + + + diff --git a/examples/auto-imports/components/components/ClientAndServer.client.vue b/test/fixtures/basic/pages/non-ascii/ç.vue similarity index 50% rename from examples/auto-imports/components/components/ClientAndServer.client.vue rename to test/fixtures/basic/pages/non-ascii/ç.vue index 3e055a3d66..047335e971 100644 --- a/examples/auto-imports/components/components/ClientAndServer.client.vue +++ b/test/fixtures/basic/pages/non-ascii/ç.vue @@ -1,10 +1,8 @@ diff --git a/test/fixtures/basic/pages/nuxt-link/trailing-slash.vue b/test/fixtures/basic/pages/nuxt-link/trailing-slash.vue index a0a22fc8d1..3e37cdf692 100644 --- a/test/fixtures/basic/pages/nuxt-link/trailing-slash.vue +++ b/test/fixtures/basic/pages/nuxt-link/trailing-slash.vue @@ -14,12 +14,15 @@ const links = [ { name: 'nuxt-link-trailing-slash' }, { query: { 'with-state': 'true' }, state: { foo: 'bar' } }, { query: { 'without-state': 'true' } } -] +] as const const route = useRoute() const windowState = computed(() => { - console.log(route.fullPath) - return process.client ? window.history.state.foo : '' + if (import.meta.client) { + console.log(route.fullPath) + return window.history.state.foo + } + return '' }) diff --git a/test/fixtures/basic/pages/prefetch/index.vue b/test/fixtures/basic/pages/prefetch/index.vue new file mode 100644 index 0000000000..e06910b636 --- /dev/null +++ b/test/fixtures/basic/pages/prefetch/index.vue @@ -0,0 +1,7 @@ + diff --git a/test/fixtures/basic/pages/prefetch/server-components.vue b/test/fixtures/basic/pages/prefetch/server-components.vue new file mode 100644 index 0000000000..cbb969ba48 --- /dev/null +++ b/test/fixtures/basic/pages/prefetch/server-components.vue @@ -0,0 +1,7 @@ + diff --git a/test/fixtures/basic/pages/random/[id].vue b/test/fixtures/basic/pages/random/[id].vue index 2a1f24ada7..75eb362f22 100644 --- a/test/fixtures/basic/pages/random/[id].vue +++ b/test/fixtures/basic/pages/random/[id].vue @@ -35,7 +35,7 @@ + + diff --git a/test/fixtures/basic/pages/server-components/lazy/end.vue b/test/fixtures/basic/pages/server-components/lazy/end.vue new file mode 100644 index 0000000000..7009c1e7ac --- /dev/null +++ b/test/fixtures/basic/pages/server-components/lazy/end.vue @@ -0,0 +1,26 @@ + + + diff --git a/test/fixtures/basic/pages/server-components/lazy/start.vue b/test/fixtures/basic/pages/server-components/lazy/start.vue new file mode 100644 index 0000000000..a14cf6eada --- /dev/null +++ b/test/fixtures/basic/pages/server-components/lazy/start.vue @@ -0,0 +1,10 @@ + diff --git a/test/fixtures/basic/pages/styles.vue b/test/fixtures/basic/pages/styles.vue index c02d7081cb..5fc55fafc9 100644 --- a/test/fixtures/basic/pages/styles.vue +++ b/test/fixtures/basic/pages/styles.vue @@ -2,6 +2,7 @@
+
diff --git a/test/fixtures/basic/pages/suspense.vue b/test/fixtures/basic/pages/suspense.vue new file mode 100644 index 0000000000..9621feb431 --- /dev/null +++ b/test/fixtures/basic/pages/suspense.vue @@ -0,0 +1,46 @@ + + + diff --git a/test/fixtures/basic/pages/suspense/async-[parent].vue b/test/fixtures/basic/pages/suspense/async-[parent].vue new file mode 100644 index 0000000000..01e60b0076 --- /dev/null +++ b/test/fixtures/basic/pages/suspense/async-[parent].vue @@ -0,0 +1,18 @@ + + + diff --git a/test/fixtures/basic/pages/suspense/async-[parent]/async-[child].vue b/test/fixtures/basic/pages/suspense/async-[parent]/async-[child].vue new file mode 100644 index 0000000000..1bea6e1144 --- /dev/null +++ b/test/fixtures/basic/pages/suspense/async-[parent]/async-[child].vue @@ -0,0 +1,19 @@ + + + diff --git a/test/fixtures/basic/pages/suspense/async-[parent]/sync-[child].vue b/test/fixtures/basic/pages/suspense/async-[parent]/sync-[child].vue new file mode 100644 index 0000000000..08a5d7d42f --- /dev/null +++ b/test/fixtures/basic/pages/suspense/async-[parent]/sync-[child].vue @@ -0,0 +1,12 @@ + + + diff --git a/test/fixtures/basic/pages/suspense/sync-[parent].vue b/test/fixtures/basic/pages/suspense/sync-[parent].vue new file mode 100644 index 0000000000..6ab6480fa1 --- /dev/null +++ b/test/fixtures/basic/pages/suspense/sync-[parent].vue @@ -0,0 +1,14 @@ + + + diff --git a/test/fixtures/basic/pages/suspense/sync-[parent]/async-[child].vue b/test/fixtures/basic/pages/suspense/sync-[parent]/async-[child].vue new file mode 100644 index 0000000000..bc4b0131b3 --- /dev/null +++ b/test/fixtures/basic/pages/suspense/sync-[parent]/async-[child].vue @@ -0,0 +1,19 @@ + + + diff --git a/test/fixtures/basic/pages/suspense/sync-[parent]/sync-[child].vue b/test/fixtures/basic/pages/suspense/sync-[parent]/sync-[child].vue new file mode 100644 index 0000000000..bfcc471463 --- /dev/null +++ b/test/fixtures/basic/pages/suspense/sync-[parent]/sync-[child].vue @@ -0,0 +1,12 @@ + + + diff --git a/test/fixtures/basic/pages/url.vue b/test/fixtures/basic/pages/url.vue new file mode 100644 index 0000000000..c3d77074b7 --- /dev/null +++ b/test/fixtures/basic/pages/url.vue @@ -0,0 +1,9 @@ + + + diff --git a/test/fixtures/basic/pages/useAsyncData/immediate-remove-unmounted.vue b/test/fixtures/basic/pages/useAsyncData/immediate-remove-unmounted.vue new file mode 100644 index 0000000000..eded79b477 --- /dev/null +++ b/test/fixtures/basic/pages/useAsyncData/immediate-remove-unmounted.vue @@ -0,0 +1,22 @@ + + + diff --git a/test/fixtures/basic/pages/useAsyncData/immediate.vue b/test/fixtures/basic/pages/useAsyncData/immediate.vue index edfa94a40b..892229e711 100644 --- a/test/fixtures/basic/pages/useAsyncData/immediate.vue +++ b/test/fixtures/basic/pages/useAsyncData/immediate.vue @@ -15,18 +15,18 @@ if (called.value !== 0) { throw new Error('Handled should have not been called') } -if (process.server && data.value !== null) { +if (import.meta.server && data.value !== null) { throw new Error('Initial data should be null: ' + data.value) } await execute() await execute() -if (process.server && called.value as number !== 2) { +if (import.meta.server && called.value as number !== 2) { throw new Error('Should have been called once after execute (server) but called ' + called.value + ' times') } -if (process.client && called.value as number !== 2) { +if (import.meta.client && called.value as number !== 2) { throw new Error('Should have been called once after execute (client) but called ' + called.value + ' times') } diff --git a/test/fixtures/basic/pages/useAsyncData/override.vue b/test/fixtures/basic/pages/useAsyncData/override.vue index 3d0328c43e..4741c30054 100644 --- a/test/fixtures/basic/pages/useAsyncData/override.vue +++ b/test/fixtures/basic/pages/useAsyncData/override.vue @@ -8,29 +8,29 @@ diff --git a/test/fixtures/basic/pages/useAsyncData/status.vue b/test/fixtures/basic/pages/useAsyncData/status.vue new file mode 100644 index 0000000000..66ef81eafa --- /dev/null +++ b/test/fixtures/basic/pages/useAsyncData/status.vue @@ -0,0 +1,49 @@ + + + diff --git a/test/fixtures/basic/pages/with-layout2.vue b/test/fixtures/basic/pages/with-layout2.vue index b256deb3ca..34d7ccc95c 100644 --- a/test/fixtures/basic/pages/with-layout2.vue +++ b/test/fixtures/basic/pages/with-layout2.vue @@ -1,10 +1,11 @@ -