Merge remote-tracking branch 'origin/main' into patch-21

This commit is contained in:
Daniel Roe 2024-09-11 21:06:10 +01:00
commit 889a5642fb
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
146 changed files with 4764 additions and 4869 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts FROM node:lts@sha256:48db4f6ea21d134be744207225753a1730c4bc1b4cdf836d44511c36bf0e34d7
RUN apt-get update && \ 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 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 && \

View File

@ -36,7 +36,7 @@ body:
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: additonal id: additional
attributes: attributes:
label: Additional context label: Additional context
description: If applicable, add any other context about the problem here description: If applicable, add any other context about the problem here

10
.github/codeql/codeql-config.yml vendored Normal file
View File

@ -0,0 +1,10 @@
paths:
- 'packages/*/dist/**'
- 'packages/nuxt/bin/**'
- 'packages/schema/schema/**'
paths-ignore:
- 'test/**'
- '**/*.test.js'
- '**/*.test.ts'
- '**/*.test.tsx'
- '**/__tests__/**'

View File

@ -6,6 +6,8 @@ on:
types: types:
- closed - closed
permissions: {}
jobs: jobs:
cleanup: cleanup:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -20,14 +22,14 @@ jobs:
gh extension install actions/gh-actions-cache gh extension install actions/gh-actions-cache
echo "Fetching list of cache keys" echo "Fetching list of cache keys"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) cacheKeysForPR=$(gh actions-cache list -R "$REPO" -B "$BRANCH" -L 100 | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys. ## Setting this to not fail the workflow while deleting cache keys.
set +e set +e
echo "Deleting caches..." echo "Deleting caches..."
for cacheKey in $cacheKeysForPR for cacheKey in $cacheKeysForPR
do do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm gh actions-cache delete "$cacheKey" -R "$REPO" -B "$BRANCH" --confirm
done done
echo "Done" echo "Done"
env: env:

View File

@ -6,9 +6,7 @@ on:
- main - main
- 3.x - 3.x
permissions: permissions: {}
pull-requests: write
contents: write
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }} group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
@ -19,6 +17,10 @@ jobs:
if: github.repository_owner == 'nuxt' && !contains(github.event.head_commit.message, 'v3.') && !contains(github.event.head_commit.message, 'v4.') if: github.repository_owner == 'nuxt' && !contains(github.event.head_commit.message, 'v3.') && !contains(github.event.head_commit.message, 'v4.')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps: steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with: with:

View File

@ -57,7 +57,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Cache dist - name: Cache dist
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with: with:
retention-days: 3 retention-days: 3
name: dist name: dist
@ -70,8 +70,6 @@ jobs:
actions: read actions: read
contents: read contents: read
security-events: write security-events: write
needs:
- build
steps: steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
@ -81,25 +79,26 @@ jobs:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3 uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
with: with:
languages: javascript config: |
paths:
- 'packages/*/src/**'
- 'packages/nuxt/bin/**'
- 'packages/schema/schema/**'
paths-ignore:
- 'test/**'
- '**/*.spec.ts'
- '**/*.test.ts'
- '**/__snapshots__/**'
languages: javascript-typescript
queries: +security-and-quality queries: +security-and-quality
- name: Restore dist cache
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dist
path: packages
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3 uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
with: with:
category: "/language:javascript" category: "/language:javascript-typescript"
typecheck: typecheck:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}

View File

@ -1,4 +1,4 @@
name: Check links with Lychee name: docs
on: on:
pull_request: pull_request:
@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Lychee link checker - name: Lychee link checker
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0 uses: lycheeverse/lychee-action@c38ba4f281730ee0d64e6963f49b708e01567b86 # for v1.8.0
with: with:
# arguments with file types to check # arguments with file types to check
args: >- args: >-

View File

@ -1,11 +1,11 @@
name: Deploy docs name: docs
on: on:
push: push:
paths: paths:
- "docs/**" - "docs/**"
branches: branches:
- main - 3.x
# Remove default permissions of GITHUB_TOKEN for security # Remove default permissions of GITHUB_TOKEN for security
# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs # https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs

View File

@ -1,4 +1,4 @@
name: Docs name: docs
on: on:
push: push:

28
.github/workflows/label-issue.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: chore
on:
issues:
types:
- opened
permissions:
issues: write
jobs:
add-issue-labels:
name: Add labels
runs-on: ubuntu-latest
if: github.repository == 'nuxt/nuxt'
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
// add 'pending triage' label if issue is created with no labels
if (context.payload.issue.labels.length === 0) {
github.rest.issues.addLabels({
issue_number: context.payload.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['pending triage']
})
}

View File

@ -1,4 +1,4 @@
name: Label PR name: chore
on: on:
pull_request_target: pull_request_target:
@ -8,6 +8,8 @@ on:
- main - main
- 3.x - 3.x
permissions: {}
jobs: jobs:
add-pr-labels: add-pr-labels:
name: Add PR labels name: Add PR labels

36
.github/workflows/lint-sherif.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: CI
on:
push:
paths:
- "**/package.json"
branches:
- main
- 3.x
pull_request:
paths:
- "**/package.json"
branches:
- main
- 3.x
- "!v[0-9]*"
permissions:
contents: read
jobs:
lint-monorepo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Lint monorepo
run: pnpm sherif -r multiple-dependency-versions

View File

@ -26,6 +26,6 @@ jobs:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files - name: Check workflow files
run: | uses: docker://rhysd/actionlint:1.7.1@sha256:435ecdb63b1169e80ca3e136290072548c07fc4d76a044cf5541021712f8f344
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/590d3bd9dde0c91f7a66071d40eb84716526e5a6/scripts/download-actionlint.bash) 1.6.25 with:
./actionlint -color -shellcheck="" args: -color

View File

@ -4,6 +4,9 @@ on:
types: [closed] types: [closed]
paths: paths:
- "packages/nuxt/src/app/composables/**" - "packages/nuxt/src/app/composables/**"
permissions: {}
jobs: jobs:
notify: notify:
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true

View File

@ -39,7 +39,7 @@ jobs:
GH_REPO: ${{ github.repository }} GH_REPO: ${{ github.repository }}
COMMENT_AT: ${{ github.event.comment.created_at }} COMMENT_AT: ${{ github.event.comment.created_at }}
run: | run: |
pr="$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GH_REPO}/pulls/${PR_NUMBER})" pr="$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/"${GH_REPO}"/pulls/"${PR_NUMBER}")"
head_sha="$(echo "$pr" | jq -r .head.sha)" head_sha="$(echo "$pr" | jq -r .head.sha)"
updated_at="$(echo "$pr" | jq -r .updated_at)" updated_at="$(echo "$pr" | jq -r .updated_at)"
@ -47,7 +47,7 @@ jobs:
exit 1 exit 1
fi fi
echo "head_sha=$head_sha" >> $GITHUB_OUTPUT echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with: with:
ref: ${{ steps.pr.outputs.head_sha }} ref: ${{ steps.pr.outputs.head_sha }}

View File

@ -1,17 +0,0 @@
name: reproduire-sur-stackblitz
on:
issues:
types:
opened
permissions:
issues: write
jobs:
reproduire-sur-stackblitz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: huang-julien/reproduire-sur-stackblitz@v1.0.0
with:
reproduction-heading: '### Reproduction'

View File

@ -1,4 +1,4 @@
name: Reproduire name: chore
on: on:
issues: issues:
types: [labeled] types: [labeled]

View File

@ -2,7 +2,7 @@
# by a third-party and are governed by separate terms of service, privacy # by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation. # policy, and support documentation.
name: Scorecard supply-chain security name: ossf
on: on:
# For Branch-Protection check. Only the default branch is supported. See # For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
if: github.repository == 'nuxt/nuxt' && success() if: github.repository == 'nuxt/nuxt' && success()
with: with:
name: SARIF file name: SARIF file
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3 uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
if: github.repository == 'nuxt/nuxt' && success() if: github.repository == 'nuxt/nuxt' && success()
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -1,4 +1,4 @@
name: Semantic pull request name: chore
on: on:
pull_request_target: pull_request_target:
@ -7,12 +7,12 @@ on:
- edited - edited
- synchronize - synchronize
permissions: permissions: {}
contents: read
jobs: jobs:
main: semantic-pr:
permissions: permissions:
contents: read
pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs 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 statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v') if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v')

17
.github/workflows/stackblitz-link.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: chore
on:
issues:
types:
opened
permissions:
issues: write
jobs:
stackblitz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: huang-julien/reproduire-sur-stackblitz@9ceccbfbb0f2f9a9a8db2d1f0dd909cf5cfe67aa # v1.0.2
with:
reproduction-heading: '### Reproduction'

View File

@ -1,4 +1,4 @@
name: Close incomplete issues name: chore
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:

View File

@ -427,8 +427,8 @@ Nuxt comes with postcss built-in. You can configure it in your `nuxt.config` fil
export default defineNuxtConfig({ export default defineNuxtConfig({
postcss: { postcss: {
plugins: { plugins: {
'postcss-nested': {} 'postcss-nested': {},
"postcss-custom-media": {} 'postcss-custom-media': {}
} }
} }
}) })

View File

@ -55,7 +55,7 @@ This includes:
You cannot currently define a server-side handler for these errors, but can render an error page, see the [Render an Error Page](#error-page) section. You cannot currently define a server-side handler for these errors, but can render an error page, see the [Render an Error Page](#error-page) section.
## Errors with JS chunks ## Errors with JS Chunks
You might encounter chunk loading errors due to a network connectivity failure or a new deployment (which invalidates your old, hashed JS chunk URLs). Nuxt provides built-in support for handling chunk loading errors by performing a hard reload when a chunk fails to load during route navigation. You might encounter chunk loading errors due to a network connectivity failure or a new deployment (which invalidates your old, hashed JS chunk URLs). Nuxt provides built-in support for handling chunk loading errors by performing a hard reload when a chunk fails to load during route navigation.

View File

@ -7,7 +7,7 @@ While building Nuxt 3, we created a new server engine: [Nitro](https://nitro.unj
It is shipped with many features: It is shipped with many features:
- Cross-platform support for Node.js, Browsers, service-workers and more. - Cross-platform support for Node.js, browsers, service workers and more.
- Serverless support out-of-the-box. - Serverless support out-of-the-box.
- API routes support. - API routes support.
- Automatic code-splitting and async-loaded chunks. - Automatic code-splitting and async-loaded chunks.

View File

@ -121,3 +121,37 @@ export default defineAppConfig({
``` ```
:: ::
## Known Limitations
As of Nuxt v3.3, the `app.config.ts` file is shared with Nitro, which results in the following limitations:
1. You cannot import Vue components directly in `app.config.ts`.
2. Some auto-imports are not available in the Nitro context.
These limitations occur because Nitro processes the app config without full Vue component support.
While it's possible to use Vite plugins in the Nitro config as a workaround, this approach is not recommended:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
vite: {
plugins: [vue()]
}
}
})
```
::warning
Using this workaround may lead to unexpected behavior and bugs. The Vue plugin is one of many that are not available in the Nitro context.
::
Related issues:
- [Issue #19858](https://github.com/nuxt/nuxt/issues/19858)
- [Issue #19854](https://github.com/nuxt/nuxt/issues/19854)
::info
Nitro v3 will resolve these limitations by removing support for the app config.
You can track the progress in [this pull request](https://github.com/unjs/nitro/pull/2521).
::

View File

@ -359,3 +359,34 @@ export default defineNuxtConfig({
::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/API/CookieStore" target="_blank"} ::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/API/CookieStore" target="_blank"}
Read more about the **CookieStore**. Read more about the **CookieStore**.
:: ::
## buildCache
Caches Nuxt build artifacts based on a hash of the configuration and source files.
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
buildCache: true
}
})
```
When enabled, changes to the following files will trigger a full rebuild:
```bash [Directory structure]
.nuxtrc
.npmrc
package.json
package-lock.json
yarn.lock
pnpm-lock.yaml
tsconfig.json
bun.lockb
```
In addition, any changes to files within `srcDir` will trigger a rebuild of the Vue client/server bundle. Nitro will always be rebuilt (though work is in progress to allow Nitro to announce its cacheable artifacts and their hashes).
::note
A maximum of 10 cache tarballs are kept.
::

View File

@ -22,7 +22,7 @@ export default {
{ {
name: 'home', name: 'home',
path: '/', path: '/',
component: () => import('~/pages/home.vue').then(r => r.default || r) component: () => import('~/pages/home.vue')
} }
], ],
} satisfies RouterConfig } satisfies RouterConfig

View File

@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtPicture> component to handle automatic image
links: links:
- label: Source - label: Source
icon: i-simple-icons-github icon: i-simple-icons-github
to: https://github.com/nuxt/image/blob/main/src/runtime/components/nuxt-picture.ts to: https://github.com/nuxt/image/blob/main/src/runtime/components/NuxtPicture.vue
size: xs size: xs
--- ---

View File

@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtImg> component to handle automatic image opti
links: links:
- label: Source - label: Source
icon: i-simple-icons-github icon: i-simple-icons-github
to: https://github.com/nuxt/image/blob/main/src/runtime/components/nuxt-img.ts to: https://github.com/nuxt/image/blob/main/src/runtime/components/NuxtImg.vue
size: xs size: xs
--- ---

View File

@ -114,7 +114,7 @@ function useAsyncData<DataT, DataE>(
key: string, key: string,
handler: (nuxtApp?: NuxtApp) => Promise<DataT>, handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
options?: AsyncDataOptions<DataT> options?: AsyncDataOptions<DataT>
): Promise<AsyncData<DataT, DataE> ): Promise<AsyncData<DataT, DataE>>
type AsyncDataOptions<DataT> = { type AsyncDataOptions<DataT> = {
server?: boolean server?: boolean

View File

@ -70,6 +70,10 @@ const { data, status, error, refresh, clear } = await useFetch('/api/auth/login'
`useFetch` is a reserved function name transformed by the compiler, so you should not name your own function `useFetch`. `useFetch` is a reserved function name transformed by the compiler, so you should not name your own function `useFetch`.
:: ::
::warning
If you encounter the `data` variable destructured from a `useFetch` returns a string and not a JSON parsed object then make sure your component doesn't include an import statement like `import { useFetch } from '@vueuse/core`.
::
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=njsGVmcWviY" target="_blank"} ::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=njsGVmcWviY" target="_blank"}
Watch the video from Alexander Lichter to avoid using `useFetch` the wrong way! Watch the video from Alexander Lichter to avoid using `useFetch` the wrong way!
:: ::

View File

@ -38,6 +38,7 @@ Apart from dynamic parameters and query parameters, `useRoute()` also provides t
- `fullPath`: encoded URL associated with the current route that contains path, query and hash - `fullPath`: encoded URL associated with the current route that contains path, query and hash
- `hash`: decoded hash section of the URL that starts with a # - `hash`: decoded hash section of the URL that starts with a #
- `query`: access route query parameters
- `matched`: array of normalized matched routes with current route location - `matched`: array of normalized matched routes with current route location
- `meta`: custom data attached to the record - `meta`: custom data attached to the record
- `name`: unique name for the route record - `name`: unique name for the route record

View File

@ -34,7 +34,7 @@ Hook | Arguments | Environment | Description
## Nuxt Hooks (build time) ## Nuxt Hooks (build time)
Check the [schema source code](https://github.com/nuxt/nuxt/blob/main/packages/schema/src/types/hooks.ts#L53) for all available hooks. Check the [schema source code](https://github.com/nuxt/nuxt/blob/main/packages/schema/src/types/hooks.ts#L83) for all available hooks.
Hook | Arguments | Description Hook | Arguments | Description
-------------------------|----------------------------|------------- -------------------------|----------------------------|-------------

View File

@ -25,9 +25,9 @@ To contribute to Nuxt, you need to set up a local environment.
```bash [Terminal] ```bash [Terminal]
corepack enable corepack enable
``` ```
4. Run `pnpm install` to Install the dependencies with pnpm: 4. Run `pnpm install --frozen-lockfile` to Install the dependencies with pnpm:
```bash [Terminal] ```bash [Terminal]
pnpm install pnpm install --frozen-lockfile
``` ```
::note ::note
If you are adding a dependency, please use `pnpm add`. :br If you are adding a dependency, please use `pnpm add`. :br

View File

@ -40,7 +40,7 @@ In addition to the Nuxt framework, there are modules that are vital for the ecos
Module | Status | Nuxt Support | Repository | Description Module | Status | Nuxt Support | Repository | Description
------------------------------------|---------------------|--------------|------------|------------------- ------------------------------------|---------------------|--------------|------------|-------------------
[Scripts](https://scripts.nuxt.com) | Public Preview | 3.x | [nuxt/scripts](https://github.com/nuxt/scripts) | Easy 3rd party script management. [Scripts](https://scripts.nuxt.com) | Public Beta | 3.x | [nuxt/scripts](https://github.com/nuxt/scripts) | Easy 3rd party script management.
A11y | Planned | 3.x | `nuxt/a11y` to be announced | Accessibility hinting and utilities [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255) A11y | Planned | 3.x | `nuxt/a11y` to be announced | Accessibility hinting and utilities [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255)
Auth | Planned | 3.x | `nuxt/auth` to be announced | Support is planned after session support. Auth | Planned | 3.x | `nuxt/auth` to be announced | Support is planned after session support.
Hints | Planned | 3.x | `nuxt/hints` to be announced | Guidance and suggestions for enhancing development practices. Hints | Planned | 3.x | `nuxt/hints` to be announced | Guidance and suggestions for enhancing development practices.

View File

@ -73,7 +73,7 @@ navigation.icon: i-ph-notification-duotone
target: _blank target: _blank
ui.icon.base: text-black dark:text-white ui.icon.base: text-black dark:text-white
--- ---
Nuxt Scripts releases. (Public Preview) Nuxt Scripts releases.
:: ::
::card ::card
--- ---

View File

@ -1,16 +1,21 @@
// For pnpm typecheck:docs to generate correct types // For pnpm typecheck:docs to generate correct types
import { addPluginTemplate } from 'nuxt/kit' import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit'
export default defineNuxtConfig({ export default defineNuxtConfig({
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' }, typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
pages: process.env.DOCS_TYPECHECK === 'true', pages: process.env.DOCS_TYPECHECK === 'true',
modules: [ modules: [
function () { function () {
if (!process.env.DOCS_TYPECHECK) { return }
addPluginTemplate({ addPluginTemplate({
filename: 'plugins/my-plugin.mjs', filename: 'plugins/my-plugin.mjs',
getContents: () => 'export default defineNuxtPlugin({ name: \'my-plugin\' })', getContents: () => 'export default defineNuxtPlugin({ name: \'my-plugin\' })',
}) })
addRouteMiddleware({
name: 'auth',
path: '#build/auth.js',
})
}, },
], ],
}) })

View File

@ -39,74 +39,74 @@
"@nuxt/ui-templates": "workspace:*", "@nuxt/ui-templates": "workspace:*",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@types/node": "20.16.1", "@types/node": "20.16.5",
"c12": "2.0.0-beta.1", "c12": "2.0.0-beta.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "2.0.0-beta.3", "jiti": "2.0.0-beta.3",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"rollup": "^4.21.0", "postcss": "8.4.45",
"typescript": "5.5.4", "rollup": "4.21.2",
"send": ">=0.19.0",
"typescript": "5.6.2",
"ufo": "1.5.4",
"unbuild": "3.0.0-rc.7", "unbuild": "3.0.0-rc.7",
"vite": "5.4.1", "vite": "5.4.4",
"vue": "3.5.0-beta.1", "vue": "3.5.4"
"@vue/compiler-core": "3.5.0-beta.1",
"@vue/compiler-dom": "3.5.0-beta.1",
"@vue/compiler-sfc": "3.5.0-beta.1",
"@vue/compiler-ssr": "3.5.0-beta.1",
"@vue/shared": "3.5.0-beta.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.9.0", "@eslint/js": "9.10.0",
"@nuxt/eslint-config": "0.5.1", "@nuxt/eslint-config": "0.5.7",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/test-utils": "3.14.1", "@nuxt/test-utils": "3.14.2",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@types/node": "20.16.1", "@types/node": "20.16.5",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.9.16", "@unhead/schema": "1.11.2",
"@vitejs/plugin-vue": "5.1.2", "@unhead/vue": "1.11.2",
"@vitejs/plugin-vue": "5.1.3",
"@vitest/coverage-v8": "2.0.5", "@vitest/coverage-v8": "2.0.5",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"case-police": "0.7.0", "case-police": "0.7.0",
"changelogen": "0.5.5", "changelogen": "0.5.5",
"consola": "3.2.3", "consola": "3.2.3",
"cssnano": "7.0.5", "cssnano": "7.0.6",
"destr": "2.0.3", "destr": "2.0.3",
"devalue": "5.0.0", "devalue": "5.0.0",
"eslint": "9.9.0", "eslint": "9.10.0",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "3.2.0", "eslint-plugin-perfectionist": "3.5.0",
"eslint-typegen": "0.3.1", "eslint-typegen": "0.3.2",
"execa": "9.3.1",
"globby": "14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "14.12.3", "happy-dom": "15.7.3",
"jiti": "2.0.0-beta.3", "jiti": "2.0.0-beta.3",
"markdownlint-cli": "0.41.0", "markdownlint-cli": "0.41.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxi": "3.12.0", "nuxi": "3.13.1",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1", "nuxt-content-twoslash": "0.1.1",
"ofetch": "1.3.4", "ofetch": "1.3.4",
"pathe": "1.1.2", "pathe": "1.1.2",
"playwright-core": "1.46.1", "playwright-core": "1.47.0",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"semver": "7.6.3", "semver": "7.6.3",
"sherif": "1.0.0",
"std-env": "3.7.0", "std-env": "3.7.0",
"typescript": "5.5.4", "tinyexec": "0.3.0",
"tinyglobby": "0.2.6",
"typescript": "5.6.2",
"ufo": "1.5.4", "ufo": "1.5.4",
"vitest": "2.0.5", "vitest": "2.0.5",
"vitest-environment-nuxt": "1.0.1", "vitest-environment-nuxt": "1.0.1",
"vue": "3.4.38", "vue": "3.5.4",
"vue-router": "4.4.3", "vue-router": "4.4.4",
"vue-tsc": "2.0.29" "vue-tsc": "2.1.6"
}, },
"packageManager": "pnpm@9.7.1", "packageManager": "pnpm@9.10.0",
"engines": { "engines": {
"node": "^16.10.0 || >=18.0.0" "node": "^16.10.0 || >=18.0.0"
}, },

View File

@ -27,7 +27,7 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"c12": "^2.0.0-beta.1", "c12": "^2.0.0-beta.2",
"consola": "^3.2.3", "consola": "^3.2.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
@ -39,12 +39,12 @@
"klona": "^2.0.6", "klona": "^2.0.6",
"mlly": "^1.7.1", "mlly": "^1.7.1",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pkg-types": "^1.1.3", "pkg-types": "^1.2.0",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unimport": "^3.10.0", "unimport": "^3.11.1",
"untyped": "^1.4.2" "untyped": "^1.4.2"
}, },
"devDependencies": { "devDependencies": {
@ -52,9 +52,9 @@
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"unbuild": "3.0.0-rc.7", "unbuild": "3.0.0-rc.7",
"vite": "5.4.1", "vite": "5.4.4",
"vitest": "2.0.5", "vitest": "2.0.5",
"webpack": "5.93.0" "webpack": "5.94.0"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"

View File

@ -6,8 +6,6 @@ import { logger } from './logger'
/** /**
* Register a directory to be scanned for components and imported only when used. * Register a directory to be scanned for components and imported only when used.
*
* Requires Nuxt 2.13+
*/ */
export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) { export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt() const nuxt = useNuxt()
@ -23,8 +21,6 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial<E
/** /**
* Register a component by its name and filePath. * Register a component by its name and filePath.
*
* Requires Nuxt 2.13+
*/ */
export async function addComponent (opts: AddComponentOptions) { export async function addComponent (opts: AddComponentOptions) {
const nuxt = useNuxt() const nuxt = useNuxt()

View File

@ -1,20 +1,15 @@
import type { Import } from 'unimport' import type { Import } from 'unimport'
import type { ImportPresetWithDeprecation } from '@nuxt/schema' import type { ImportPresetWithDeprecation } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
import { assertNuxtCompatibility } from './compatibility'
import { toArray } from './utils' import { toArray } from './utils'
export function addImports (imports: Import | Import[]) { export function addImports (imports: Import | Import[]) {
assertNuxtCompatibility({ bridge: true })
useNuxt().hook('imports:extend', (_imports) => { useNuxt().hook('imports:extend', (_imports) => {
_imports.push(...toArray(imports)) _imports.push(...toArray(imports))
}) })
} }
export function addImportsDir (dirs: string | string[], opts: { prepend?: boolean } = {}) { export function addImportsDir (dirs: string | string[], opts: { prepend?: boolean } = {}) {
assertNuxtCompatibility({ bridge: true })
useNuxt().hook('imports:dirs', (_dirs: string[]) => { useNuxt().hook('imports:dirs', (_dirs: string[]) => {
for (const dir of toArray(dirs)) { for (const dir of toArray(dirs)) {
_dirs[opts.prepend ? 'unshift' : 'push'](dir) _dirs[opts.prepend ? 'unshift' : 'push'](dir)
@ -22,8 +17,6 @@ export function addImportsDir (dirs: string | string[], opts: { prepend?: boolea
}) })
} }
export function addImportsSources (presets: ImportPresetWithDeprecation | ImportPresetWithDeprecation[]) { export function addImportsSources (presets: ImportPresetWithDeprecation | ImportPresetWithDeprecation[]) {
assertNuxtCompatibility({ bridge: true })
useNuxt().hook('imports:sources', (_presets: ImportPresetWithDeprecation[]) => { useNuxt().hook('imports:sources', (_presets: ImportPresetWithDeprecation[]) => {
for (const preset of toArray(presets)) { for (const preset of toArray(presets)) {
_presets.push(preset) _presets.push(preset)

View File

@ -5,7 +5,7 @@ import { useNuxt } from './context'
import { logger } from './logger' import { logger } from './logger'
import { addTemplate } from './template' import { addTemplate } from './template'
export function addLayout (this: any, template: NuxtTemplate | string, name?: string) { export function addLayout (template: NuxtTemplate | string, name?: string) {
const nuxt = useNuxt() const nuxt = useNuxt()
const { filename, src } = addTemplate(template) const { filename, src } = addTemplate(template)
const layoutName = kebabCase(name || parse(filename).name).replace(/["']/g, '') const layoutName = kebabCase(name || parse(filename).name).replace(/["']/g, '')

View File

@ -1,3 +1,4 @@
import { existsSync } from 'node:fs'
import type { JSValue } from 'untyped' import type { JSValue } from 'untyped'
import { applyDefaults } from 'untyped' import { applyDefaults } from 'untyped'
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12' import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
@ -6,6 +7,7 @@ import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
import { NuxtConfigSchema } from '@nuxt/schema' import { NuxtConfigSchema } from '@nuxt/schema'
import { globby } from 'globby' import { globby } from 'globby'
import defu from 'defu' import defu from 'defu'
import { join } from 'pathe'
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> { export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@ -47,6 +49,11 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
nuxtConfig._nuxtConfigFile = configFile nuxtConfig._nuxtConfigFile = configFile
nuxtConfig._nuxtConfigFiles = [configFile] nuxtConfig._nuxtConfigFiles = [configFile]
const defaultBuildDir = join(nuxtConfig.rootDir!, '.nuxt')
if (!opts.overrides?._prepare && !nuxtConfig.dev && !nuxtConfig.buildDir && existsSync(defaultBuildDir)) {
nuxtConfig.buildDir = join(nuxtConfig.rootDir!, 'node_modules/.cache/nuxt/.nuxt')
}
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = [] const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
const processedLayers = new Set<string>() const processedLayers = new Set<string>()
for (const layer of layers) { for (const layer of layers) {

View File

@ -79,9 +79,9 @@ function _defineNuxtModule<
} }
// Module format is always a simple function // Module format is always a simple function
async function normalizedModule (this: any, inlineOptions: Partial<TOptions>, nuxt: Nuxt): Promise<ModuleSetupReturn> { async function normalizedModule (inlineOptions: Partial<TOptions>, nuxt = tryUseNuxt()!): Promise<ModuleSetupReturn> {
if (!nuxt) { if (!nuxt) {
nuxt = tryUseNuxt() || this.nuxt /* invoked by nuxt 2 */ throw new TypeError('Cannot use module outside of Nuxt context')
} }
// Avoid duplicate installs // Avoid duplicate installs

View File

@ -320,11 +320,6 @@ export async function writeTypes (nuxt: Nuxt) {
await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration) 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() await writeFile()
} }

View File

@ -60,17 +60,18 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/devalue": "^2.0.2", "@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.3.9", "@nuxt/devtools": "^1.4.2",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.4", "@nuxt/telemetry": "^2.6.0",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.16", "@unhead/dom": "^1.11.2",
"@unhead/ssr": "^1.9.16", "@unhead/shared": "^1.11.2",
"@unhead/vue": "^1.9.16", "@unhead/ssr": "^1.11.2",
"@vue/shared": "^3.4.38", "@unhead/vue": "^1.11.2",
"@vue/shared": "^3.5.4",
"acorn": "8.12.1", "acorn": "8.12.1",
"c12": "^2.0.0-beta.1", "c12": "^2.0.0-beta.2",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"compatx": "^0.1.8", "compatx": "^0.1.8",
"consola": "^3.2.3", "consola": "^3.2.3",
@ -86,48 +87,52 @@
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^5.3.2", "ignore": "^5.3.2",
"impound": "^0.1.0",
"jiti": "^2.0.0-beta.3", "jiti": "^2.0.0-beta.3",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"mlly": "^1.7.1", "mlly": "^1.7.1",
"nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxi": "^3.12.0", "nuxi": "^3.13.1",
"nypm": "^0.3.9", "nypm": "^0.3.11",
"ofetch": "^1.3.4", "ofetch": "^1.3.4",
"ohash": "^1.1.3", "ohash": "^1.1.3",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.1.3", "pkg-types": "^1.2.0",
"radix3": "^1.1.2", "radix3": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"strip-literal": "^2.1.0", "strip-literal": "^2.1.0",
"tinyglobby": "0.2.6",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"ultrahtml": "^1.5.3", "ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unimport": "^3.10.0", "unhead": "^1.11.2",
"unplugin": "^1.12.2", "unimport": "^3.11.1",
"unplugin-vue-router": "^0.10.7", "unplugin": "^1.14.1",
"unstorage": "^1.10.2", "unplugin-vue-router": "^0.10.8",
"unstorage": "^1.12.0",
"untyped": "^1.4.2", "untyped": "^1.4.2",
"vue": "^3.4.38", "vue": "^3.5.4",
"vue-bundle-renderer": "^2.1.0", "vue-bundle-renderer": "^2.1.0",
"vue-devtools-stub": "^0.1.0", "vue-devtools-stub": "^0.1.0",
"vue-router": "^4.4.3" "vue-router": "^4.4.4"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/scripts": "0.6.6", "@nuxt/scripts": "0.8.5",
"@nuxt/ui-templates": "1.3.4", "@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1", "@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@vitejs/plugin-vue": "5.1.2", "@vitejs/plugin-vue": "5.1.3",
"@vue/compiler-sfc": "3.4.38", "@vue/compiler-sfc": "3.5.4",
"unbuild": "3.0.0-rc.7", "unbuild": "3.0.0-rc.7",
"vite": "5.4.1", "vite": "5.4.4",
"vitest": "2.0.5" "vitest": "2.0.5"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -2,6 +2,7 @@ import { defineComponent } from 'vue'
export default defineComponent({ export default defineComponent({
name: 'DevOnly', name: 'DevOnly',
inheritAttrs: false,
setup (_, props) { setup (_, props) {
if (import.meta.dev) { if (import.meta.dev) {
return () => props.slots.default?.() return () => props.slots.default?.()

View File

@ -1,12 +1,14 @@
import type { defineAsyncComponent } from 'vue' import type { defineAsyncComponent } from 'vue'
import { createVNode, defineComponent, onErrorCaptured } from 'vue' import { createVNode, defineComponent, onErrorCaptured } from 'vue'
import { injectHead } from '@unhead/vue'
import { createError } from '../composables/error' import { createError } from '../composables/error'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { islandComponents } from '#build/components.islands.mjs' import { islandComponents } from '#build/components.islands.mjs'
export default defineComponent({ export default defineComponent({
name: 'IslandRenderer',
props: { props: {
context: { context: {
type: Object as () => { name: string, props?: Record<string, any> }, type: Object as () => { name: string, props?: Record<string, any> },
@ -14,6 +16,10 @@ export default defineComponent({
}, },
}, },
setup (props) { setup (props) {
// reset head - we don't want to have any head tags from plugin or anywhere else.
const head = injectHead()
head.headEntries().splice(0, head.headEntries().length)
const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent> const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>
if (!component) { if (!component) {

View File

@ -2,6 +2,8 @@ import { defineComponent, onErrorCaptured, ref } from 'vue'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
export default defineComponent({ export default defineComponent({
name: 'NuxtErrorBoundary',
inheritAttrs: false,
emits: { emits: {
error (_error: unknown) { error (_error: unknown) {
return true return true
@ -11,14 +13,16 @@ export default defineComponent({
const error = ref<Error | null>(null) const error = ref<Error | null>(null)
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
if (import.meta.client) {
onErrorCaptured((err, target, info) => { onErrorCaptured((err, target, info) => {
if (import.meta.client && (!nuxtApp.isHydrating || !nuxtApp.payload.serverRendered)) { if (!nuxtApp.isHydrating || !nuxtApp.payload.serverRendered) {
emit('error', err) emit('error', err)
nuxtApp.hooks.callHook('vue:error', err, target, info) nuxtApp.hooks.callHook('vue:error', err, target, info)
error.value = err error.value = err
return false return false
} }
}) })
}
function clearError () { function clearError () {
error.value = null error.value = null

View File

@ -40,10 +40,10 @@ const description = _error.message || _error.toString()
const stack = import.meta.dev && !is404 ? _error.description || `<pre>${stacktrace}</pre>` : undefined const stack = import.meta.dev && !is404 ? _error.description || `<pre>${stacktrace}</pre>` : undefined
// TODO: Investigate side-effect issue with imports // TODO: Investigate side-effect issue with imports
const _Error404 = defineAsyncComponent(() => import('./error-404.vue').then(r => r.default || r)) const _Error404 = defineAsyncComponent(() => import('./error-404.vue'))
const _Error = import.meta.dev const _Error = import.meta.dev
? defineAsyncComponent(() => import('./error-dev.vue').then(r => r.default || r)) ? defineAsyncComponent(() => import('./error-dev.vue'))
: defineAsyncComponent(() => import('./error-500.vue').then(r => r.default || r)) : defineAsyncComponent(() => import('./error-500.vue'))
const ErrorTemplate = is404 ? _Error404 : _Error const ErrorTemplate = is404 ? _Error404 : _Error
</script> </script>

View File

@ -3,7 +3,7 @@ import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineCom
import { debounce } from 'perfect-debounce' import { debounce } from 'perfect-debounce'
import { hash } from 'ohash' import { hash } from 'ohash'
import { appendResponseHeader } from 'h3' import { appendResponseHeader } from 'h3'
import { useHead } from '@unhead/vue' import { injectHead } from '@unhead/vue'
import { randomUUID } from 'uncrypto' import { randomUUID } from 'uncrypto'
import { joinURL, withQuery } from 'ufo' import { joinURL, withQuery } from 'ufo'
import type { FetchResponse } from 'ofetch' import type { FetchResponse } from 'ofetch'
@ -45,6 +45,7 @@ async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['c
export default defineComponent({ export default defineComponent({
name: 'NuxtIsland', name: 'NuxtIsland',
inheritAttrs: false,
props: { props: {
name: { name: {
type: String, type: String,
@ -96,7 +97,7 @@ export default defineComponent({
if (result.props) { toRevive.props = result.props } if (result.props) { toRevive.props = result.props }
if (result.slots) { toRevive.slots = result.slots } if (result.slots) { toRevive.slots = result.slots }
if (result.components) { toRevive.components = result.components } if (result.components) { toRevive.components = result.components }
if (result.head) { toRevive.head = result.head }
nuxtApp.payload.data[key] = { nuxtApp.payload.data[key] = {
__nuxt_island: { __nuxt_island: {
key, key,
@ -158,8 +159,7 @@ export default defineComponent({
return html return html
}) })
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] }) const head = injectHead()
useHead(cHead)
async function _fetchComponent (force = false) { async function _fetchComponent (force = false) {
const key = `${props.name}_${hashId.value}` const key = `${props.name}_${hashId.value}`
@ -199,8 +199,7 @@ export default defineComponent({
} }
try { try {
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value] const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
cHead.value.link = res.head.link
cHead.value.style = res.head.style
ssrHTML.value = res.html.replaceAll(DATA_ISLAND_UID_RE, `data-island-uid="${uid.value}"`) ssrHTML.value = res.html.replaceAll(DATA_ISLAND_UID_RE, `data-island-uid="${uid.value}"`)
key.value++ key.value++
error.value = null error.value = null
@ -248,6 +247,14 @@ export default defineComponent({
await loadComponents(props.source, payloads.components) await loadComponents(props.source, payloads.components)
} }
if (import.meta.server || nuxtApp.isHydrating) {
// re-push head into active head instance
const responseHead = (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head
if (responseHead) {
head.push(responseHead)
}
}
return (_ctx: any, _cache: any) => { return (_ctx: any, _cache: any) => {
if (!html.value || error.value) { if (!html.value || error.value) {
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')] return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]

View File

@ -253,10 +253,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
}, },
prefetchOn: { prefetchOn: {
type: [String, Object] as PropType<NuxtLinkProps['prefetchOn']>, type: [String, Object] as PropType<NuxtLinkProps['prefetchOn']>,
default: options.prefetchOn || { default: undefined,
visibility: true,
interaction: false,
} satisfies NuxtLinkProps['prefetchOn'],
required: false, required: false,
}, },
noPrefetch: { noPrefetch: {
@ -384,13 +381,15 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
replace: props.replace, replace: props.replace,
ariaCurrentValue: props.ariaCurrentValue, ariaCurrentValue: props.ariaCurrentValue,
custom: props.custom, custom: props.custom,
onPointerenter: shouldPrefetch('interaction') ? prefetch.bind(null, undefined) : undefined,
onFocus: shouldPrefetch('interaction') ? prefetch.bind(null, undefined) : undefined,
} }
// `custom` API cannot support fallthrough attributes as the slot // `custom` API cannot support fallthrough attributes as the slot
// may render fragment or text root nodes (#14897, #19375) // may render fragment or text root nodes (#14897, #19375)
if (!props.custom) { if (!props.custom) {
if (shouldPrefetch('interaction')) {
routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
routerLinkProps.onFocus = prefetch.bind(null, undefined)
}
if (prefetched.value) { if (prefetched.value) {
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
} }
@ -430,6 +429,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return slots.default({ return slots.default({
href: href.value, href: href.value,
navigate, navigate,
prefetch,
get route () { get route () {
if (!href.value) { return undefined } if (!href.value) { return undefined }

View File

@ -18,6 +18,7 @@ export const NuxtTeleportIslandSymbol = Symbol('NuxtTeleportIslandComponent') as
/* @__PURE__ */ /* @__PURE__ */
export default defineComponent({ export default defineComponent({
name: 'NuxtTeleportIslandComponent', name: 'NuxtTeleportIslandComponent',
inheritAttrs: false,
props: { props: {
to: { to: {
type: String, type: String,

View File

@ -9,6 +9,7 @@ import { NuxtTeleportIslandSymbol } from './nuxt-teleport-island-component'
/* @__PURE__ */ /* @__PURE__ */
export default defineComponent({ export default defineComponent({
name: 'NuxtTeleportIslandSlot', name: 'NuxtTeleportIslandSlot',
inheritAttrs: false,
props: { props: {
name: { name: {
type: String, type: String,

View File

@ -27,6 +27,7 @@ export const RouteProvider = defineComponent({
for (const key in props.route) { for (const key in props.route) {
Object.defineProperty(route, key, { Object.defineProperty(route, key, {
get: () => previousKey === props.renderKey ? props.route[key as keyof RouteLocationNormalizedLoaded] : previousRoute[key as keyof RouteLocationNormalizedLoaded], get: () => previousKey === props.renderKey ? props.route[key as keyof RouteLocationNormalizedLoaded] : previousRoute[key as keyof RouteLocationNormalizedLoaded],
enumerable: true,
}) })
} }

View File

@ -7,7 +7,7 @@ import { devRootDir } from '#build/nuxt.config.mjs'
export default (url: string) => defineComponent({ export default (url: string) => defineComponent({
name: 'NuxtTestComponentWrapper', name: 'NuxtTestComponentWrapper',
inheritAttrs: false,
async setup (props, { attrs }) { async setup (props, { attrs }) {
const query = parseQuery(new URL(url, 'http://localhost').search) const query = parseQuery(new URL(url, 'http://localhost').search)
const urlProps = query.props ? destr<Record<string, any>>(query.props as string) : {} const urlProps = query.props ? destr<Record<string, any>>(query.props as string) : {}

View File

@ -1,5 +1,5 @@
import { computed, getCurrentInstance, getCurrentScope, onBeforeMount, onScopeDispose, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue' import { computed, getCurrentInstance, getCurrentScope, onBeforeMount, onScopeDispose, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue'
import type { Ref, WatchSource } from 'vue' import type { MultiWatchSources, Ref } from 'vue'
import type { NuxtApp } from '../nuxt' import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
import { toArray } from '../utils' import { toArray } from '../utils'
@ -34,7 +34,7 @@ export type KeysOf<T> = Array<
export type KeyOfRes<Transform extends _Transform> = KeysOf<ReturnType<Transform>> export type KeyOfRes<Transform extends _Transform> = KeysOf<ReturnType<Transform>>
export type MultiWatchSources = (WatchSource<unknown> | object)[] export type { MultiWatchSources }
export type NoInfer<T> = [T][T extends any ? 0 : never] export type NoInfer<T> = [T][T extends any ? 0 : never]

View File

@ -16,7 +16,7 @@ async function runLegacyAsyncData (res: Record<string, any> | Promise<Record<str
const { fetchKey, _fetchKeyBase } = vm.proxy!.$options const { fetchKey, _fetchKeyBase } = vm.proxy!.$options
const key = (typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey) || const key = (typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey) ||
([_fetchKeyBase, route.fullPath, route.matched.findIndex(r => Object.values(r.components || {}).includes(vm.type))].join(':')) ([_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))) const { data, error } = await useAsyncData(`options:asyncdata:${key}`, () => import.meta.server ? nuxtApp.runWithContext(() => fn(nuxtApp)) : fn(nuxtApp))
if (error.value) { if (error.value) {
throw createError(error.value) throw createError(error.value)
} }

View File

@ -103,9 +103,18 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
} }
if (store) { if (store) {
/* event is of type CookieChangeEvent */
const changeHandler = (event: any) => { const changeHandler = (event: any) => {
const cookie = event.changed.find((c: any) => c.name === name) const changedCookie = event.changed.find((c: any) => c.name === name)
if (cookie) { handleChange({ value: cookie.value }) } const removedCookie = event.deleted.find((c: any) => c.name === name)
if (changedCookie) {
handleChange({ value: changedCookie.value })
}
if (removedCookie) {
handleChange({ value: null })
}
} }
store.addEventListener('change', changeHandler) store.addEventListener('change', changeHandler)
if (hasScope) { if (hasScope) {

View File

@ -53,7 +53,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
/** @since 3.0.0 */ /** @since 3.0.0 */
export const isNuxtError = <DataT = unknown>( export const isNuxtError = <DataT = unknown>(
error?: string | object, error: unknown,
): error is NuxtError<DataT> => !!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error ): error is NuxtError<DataT> => !!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error
/** @since 3.0.0 */ /** @since 3.0.0 */

View File

@ -11,6 +11,15 @@ type DeepPartial<T> = T extends Function ? T : T extends Record<string, any> ? {
// Workaround for vite HMR with virtual modules // Workaround for vite HMR with virtual modules
export const _getAppConfig = () => __appConfig as AppConfig export const _getAppConfig = () => __appConfig as AppConfig
function isPojoOrArray (val: unknown): val is object {
return (
Array.isArray(val) ||
(!!val &&
typeof val === 'object' &&
val.constructor?.name === 'Object')
)
}
function deepDelete (obj: any, newObj: any) { function deepDelete (obj: any, newObj: any) {
for (const key in obj) { for (const key in obj) {
const val = newObj[key] const val = newObj[key]
@ -18,7 +27,7 @@ function deepDelete (obj: any, newObj: any) {
delete (obj as any)[key] delete (obj as any)[key]
} }
if (val !== null && typeof val === 'object') { if (isPojoOrArray(val)) {
deepDelete(obj[key], newObj[key]) deepDelete(obj[key], newObj[key])
} }
} }
@ -27,7 +36,7 @@ function deepDelete (obj: any, newObj: any) {
function deepAssign (obj: any, newObj: any) { function deepAssign (obj: any, newObj: any) {
for (const key in newObj) { for (const key in newObj) {
const val = newObj[key] const val = newObj[key]
if (val !== null && typeof val === 'object') { if (isPojoOrArray(val)) {
const defaultVal = Array.isArray(val) ? [] : {} const defaultVal = Array.isArray(val) ? [] : {}
obj[key] = obj[key] || defaultVal obj[key] = obj[key] || defaultVal
deepAssign(obj[key], val) deepAssign(obj[key], val)

View File

@ -2,6 +2,7 @@ import { createApp, createSSRApp, nextTick } from 'vue'
import type { App } from 'vue' import type { App } from 'vue'
// This file must be imported first as we set globalThis.$fetch via this import // This file must be imported first as we set globalThis.$fetch via this import
// @ts-expect-error virtual file
import '#build/fetch.mjs' import '#build/fetch.mjs'
import { applyPlugins, createNuxtApp } from './nuxt' import { applyPlugins, createNuxtApp } from './nuxt'
@ -9,6 +10,7 @@ import type { CreateOptions } from './nuxt'
import { createError } from './composables/error' import { createError } from './composables/error'
// @ts-expect-error virtual file
import '#build/css' import '#build/css'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import plugins from '#build/plugins' import plugins from '#build/plugins'

View File

@ -18,10 +18,9 @@ import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
import type { NuxtAppManifestMeta } from '../app/composables/manifest' import type { NuxtAppManifestMeta } from '../app/composables/manifest'
import type { LoadingIndicator } from '../app/composables/loading-indicator' import type { LoadingIndicator } from '../app/composables/loading-indicator'
import type { RouteAnnouncer } from '../app/composables/route-announcer' import type { RouteAnnouncer } from '../app/composables/route-announcer'
import type { ViewTransition } from './plugins/view-transitions.client'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appId, multiApp } from '#build/nuxt.config.mjs' import { appId, chunkErrorEvent, multiApp } from '#build/nuxt.config.mjs'
import type { NuxtAppLiterals } from '#app' import type { NuxtAppLiterals } from '#app'
@ -267,6 +266,7 @@ export function createNuxtApp (options: CreateOptions) {
get vue () { return nuxtApp.vueApp.version }, get vue () { return nuxtApp.vueApp.version },
}, },
payload: shallowReactive({ payload: shallowReactive({
...options.ssrContext?.payload || {},
data: shallowReactive({}), data: shallowReactive({}),
state: reactive({}), state: reactive({}),
once: new Set<string>(), once: new Set<string>(),
@ -275,7 +275,7 @@ export function createNuxtApp (options: CreateOptions) {
static: { static: {
data: {}, data: {},
}, },
runWithContext (fn: any) { runWithContext <T>(fn: () => T) {
if (nuxtApp._scope.active && !getCurrentScope()) { if (nuxtApp._scope.active && !getCurrentScope()) {
return nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn)) return nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn))
} }
@ -310,6 +310,20 @@ export function createNuxtApp (options: CreateOptions) {
nuxtApp.payload.serverRendered = true nuxtApp.payload.serverRendered = true
} }
if (import.meta.server && nuxtApp.ssrContext) {
nuxtApp.payload.path = nuxtApp.ssrContext.url
// Expose nuxt to the renderContext
nuxtApp.ssrContext.nuxt = nuxtApp
nuxtApp.ssrContext.payload = nuxtApp.payload
// Expose client runtime-config to the payload
nuxtApp.ssrContext.config = {
public: nuxtApp.ssrContext.runtimeConfig.public,
app: nuxtApp.ssrContext.runtimeConfig.app,
}
}
if (import.meta.client) { if (import.meta.client) {
const __NUXT__ = multiApp ? window.__NUXT__?.[nuxtApp._id] : window.__NUXT__ const __NUXT__ = multiApp ? window.__NUXT__?.[nuxtApp._id] : window.__NUXT__
// TODO: remove/refactor in https://github.com/nuxt/nuxt/issues/25336 // TODO: remove/refactor in https://github.com/nuxt/nuxt/issues/25336
@ -356,35 +370,14 @@ export function createNuxtApp (options: CreateOptions) {
defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp) defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp)
defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp) defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp)
if (import.meta.server) {
if (nuxtApp.ssrContext) {
// Expose nuxt to the renderContext
nuxtApp.ssrContext.nuxt = nuxtApp
// 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
if (nuxtApp.ssrContext!.payload) {
Object.assign(nuxtApp.payload, nuxtApp.ssrContext!.payload)
}
nuxtApp.ssrContext!.payload = nuxtApp.payload
// Expose client runtime-config to the payload
nuxtApp.ssrContext!.config = {
public: options.ssrContext!.runtimeConfig.public,
app: options.ssrContext!.runtimeConfig.app,
}
}
// Listen to chunk load errors
if (import.meta.client) { if (import.meta.client) {
window.addEventListener('nuxt.preloadError', (event) => { // Listen to chunk load errors
if (chunkErrorEvent) {
window.addEventListener(chunkErrorEvent, (event) => {
nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload }) nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload })
event.preventDefault()
}) })
}
window.useNuxtApp = window.useNuxtApp || useNuxtApp window.useNuxtApp = window.useNuxtApp || useNuxtApp
// Log errors captured when running plugins, in the `app:created` and `app:beforeMount` hooks // Log errors captured when running plugins, in the `app:created` and `app:beforeMount` hooks

View File

@ -26,7 +26,7 @@ export default defineNuxtPlugin({
}) })
router.onError((error, to) => { router.onError((error, to) => {
if (chunkErrors.has(error)) { if (chunkErrors.has(error) || error.message.includes('Failed to fetch dynamically imported module')) {
reloadAppAtPath(to) reloadAppAtPath(to)
} }
}) })

View File

@ -40,7 +40,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
} }
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const nuxtLogsElement = document.querySelector(`[data-nuxt-logs="${nuxtApp._name}"]`) const nuxtLogsElement = document.querySelector(`[data-nuxt-logs="${nuxtApp._id}"]`)
const content = nuxtLogsElement?.textContent const content = nuxtLogsElement?.textContent
const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : [] const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
await nuxtApp.hooks.callHook('dev:ssr-logs', logs) await nuxtApp.hooks.callHook('dev:ssr-logs', logs)

View File

@ -31,11 +31,6 @@ if (componentIslands) {
} }
return { return {
html: '', html: '',
state: {},
head: {
link: [],
style: [],
},
...result, ...result,
} }
} }

View File

@ -6,25 +6,25 @@ import { defineNuxtPlugin } from '../nuxt'
// @ts-expect-error Virtual file. // @ts-expect-error Virtual file.
import { componentIslands } from '#build/nuxt.config.mjs' import { componentIslands } from '#build/nuxt.config.mjs'
const reducers: Record<string, (data: any) => any> = { const reducers: [string, (data: any) => any][] = [
NuxtError: data => isNuxtError(data) && data.toJSON(), ['NuxtError', data => isNuxtError(data) && data.toJSON()],
EmptyShallowRef: data => isRef(data) && isShallow(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_')), ['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) || '_')), ['EmptyRef', data => isRef(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_'))],
ShallowRef: data => isRef(data) && isShallow(data) && data.value, ['ShallowRef', data => isRef(data) && isShallow(data) && data.value],
ShallowReactive: data => isReactive(data) && isShallow(data) && toRaw(data), ['ShallowReactive', data => isReactive(data) && isShallow(data) && toRaw(data)],
Ref: data => isRef(data) && data.value, ['Ref', data => isRef(data) && data.value],
Reactive: data => isReactive(data) && toRaw(data), ['Reactive', data => isReactive(data) && toRaw(data)],
} ]
if (componentIslands) { if (componentIslands) {
reducers.Island = data => data && data?.__nuxt_island reducers.push(['Island', data => data && data?.__nuxt_island])
} }
export default defineNuxtPlugin({ export default defineNuxtPlugin({
name: 'nuxt:revive-payload:server', name: 'nuxt:revive-payload:server',
setup () { setup () {
for (const reducer in reducers) { for (const [reducer, fn] of reducers) {
definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers]) definePayloadReducer(reducer, fn)
} }
}, },
}) })

View File

@ -56,15 +56,3 @@ export default defineNuxtPlugin((nuxtApp) => {
finishTransition = undefined finishTransition = undefined
}) })
}) })
export interface ViewTransition {
ready: Promise<void>
finished: Promise<void>
updateCallbackDone: Promise<void>
}
declare global {
interface Document {
startViewTransition?: (callback: () => Promise<void> | void) => ViewTransition
}
}

View File

@ -24,7 +24,7 @@ interface ComponentChunkOptions {
buildDir: string buildDir: string
} }
const SCRIPT_RE = /<script[^>]*>/g const SCRIPT_RE = /<script[^>]*>/gi
const HAS_SLOT_OR_CLIENT_RE = /<slot[^>]*>|nuxt-client/ const HAS_SLOT_OR_CLIENT_RE = /<slot[^>]*>|nuxt-client/
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/ const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g

View File

@ -1,6 +1,6 @@
import fs, { statSync } from 'node:fs' import { existsSync, statSync, writeFileSync } from 'node:fs'
import { join, normalize, relative, resolve } from 'pathe' import { join, normalize, relative, resolve } from 'pathe'
import { addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, logger, resolveAlias, updateTemplates } from '@nuxt/kit' import { addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema' import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
import { distDir } from '../dirs' import { distDir } from '../dirs'
@ -169,6 +169,10 @@ export default defineNuxtModule<ComponentsOptions>({
await nuxt.callHook('components:extend', newComponents) await nuxt.callHook('components:extend', newComponents)
// add server placeholder for .client components server side. issue: #7085 // add server placeholder for .client components server side. issue: #7085
for (const component of newComponents) { for (const component of newComponents) {
if (!(component as any /* untyped internal property */)._scanned && !(component.filePath in nuxt.vfs) && !existsSync(component.filePath)) {
// attempt to resolve component path
component.filePath = await resolvePath(component.filePath, { fallbackToOriginal: true })
}
if (component.mode === 'client' && !newComponents.some(c => c.pascalName === component.pascalName && c.mode === 'server')) { if (component.mode === 'client' && !newComponents.some(c => c.pascalName === component.pascalName && c.mode === 'server')) {
newComponents.push({ newComponents.push({
...component, ...component,
@ -236,17 +240,17 @@ export default defineNuxtModule<ComponentsOptions>({
const selectiveClient = typeof nuxt.options.experimental.componentIslands === 'object' && nuxt.options.experimental.componentIslands.selectiveClient const selectiveClient = typeof nuxt.options.experimental.componentIslands === 'object' && nuxt.options.experimental.componentIslands.selectiveClient
if (isClient && selectiveClient) { if (isClient && selectiveClient) {
fs.writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}') writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
if (!nuxt.options.dev) { if (!nuxt.options.dev) {
config.plugins.push(componentsChunkPlugin.vite({ config.plugins.push(componentsChunkPlugin.vite({
getComponents, getComponents,
buildDir: nuxt.options.buildDir, buildDir: nuxt.options.buildDir,
})) }))
} else { } else {
fs.writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), `export const paths = ${JSON.stringify( writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), `export const paths = ${JSON.stringify(
getComponents().filter(c => c.mode === 'client' || c.mode === 'all').reduce((acc, c) => { getComponents().filter(c => c.mode === 'client' || c.mode === 'all').reduce((acc, c) => {
if (c.filePath.endsWith('.vue') || c.filePath.endsWith('.js') || c.filePath.endsWith('.ts')) { return Object.assign(acc, { [c.pascalName]: `/@fs/${c.filePath}` }) } if (c.filePath.endsWith('.vue') || c.filePath.endsWith('.js') || c.filePath.endsWith('.ts')) { return Object.assign(acc, { [c.pascalName]: `/@fs/${c.filePath}` }) }
const filePath = fs.existsSync(`${c.filePath}.vue`) ? `${c.filePath}.vue` : fs.existsSync(`${c.filePath}.js`) ? `${c.filePath}.js` : `${c.filePath}.ts` const filePath = existsSync(`${c.filePath}.vue`) ? `${c.filePath}.vue` : existsSync(`${c.filePath}.js`) ? `${c.filePath}.js` : `${c.filePath}.ts`
return Object.assign(acc, { [c.pascalName]: `/@fs/${filePath}` }) return Object.assign(acc, { [c.pascalName]: `/@fs/${filePath}` })
}, {} as Record<string, string>), }, {} as Record<string, string>),
)}`) )}`)
@ -307,7 +311,7 @@ export default defineNuxtModule<ComponentsOptions>({
getComponents, getComponents,
})) }))
} else { } else {
fs.writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}') writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
} }
} }
}) })

View File

@ -126,6 +126,8 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
export: 'default', export: 'default',
// by default, give priority to scanned components // by default, give priority to scanned components
priority: dir.priority ?? 1, priority: dir.priority ?? 1,
// @ts-expect-error untyped property
_scanned: true,
} }
if (typeof dir.extendComponent === 'function') { if (typeof dir.extendComponent === 'function') {

View File

@ -22,6 +22,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
}, },
], ],
virtualImports: ['#components'], virtualImports: ['#components'],
injectAtEnd: true,
}) })
function getComponentsImports (): Import[] { function getComponentsImports (): Import[] {
@ -50,6 +51,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
return createUnplugin(() => ({ return createUnplugin(() => ({
name: 'nuxt:components:imports', name: 'nuxt:components:imports',
enforce: 'post',
transformInclude (id) { transformInclude (id) {
id = normalize(id) id = normalize(id)
return id.startsWith('virtual:') || id.startsWith('\0virtual:') || id.startsWith(nuxt.options.buildDir) || !isIgnored(id) return id.startsWith('virtual:') || id.startsWith('\0virtual:') || id.startsWith(nuxt.options.buildDir) || !isIgnored(id)

View File

@ -8,6 +8,7 @@ import type { Nuxt, NuxtBuilder } from 'nuxt/schema'
import { generateApp as _generateApp, createApp } from './app' import { generateApp as _generateApp, createApp } from './app'
import { checkForExternalConfigurationFiles } from './external-config-files' import { checkForExternalConfigurationFiles } from './external-config-files'
import { cleanupCaches, getVueHash } from './cache'
export async function build (nuxt: Nuxt) { export async function build (nuxt: Nuxt) {
const app = createApp(nuxt) const app = createApp(nuxt)
@ -40,17 +41,33 @@ export async function build (nuxt: Nuxt) {
}) })
} }
if (!nuxt.options._prepare && !nuxt.options.dev && nuxt.options.experimental.buildCache) {
const { restoreCache, collectCache } = await getVueHash(nuxt)
if (await restoreCache()) {
await nuxt.callHook('build:done')
return await nuxt.callHook('close', nuxt)
}
nuxt.hooks.hookOnce('nitro:build:before', () => collectCache())
nuxt.hooks.hookOnce('close', () => cleanupCaches(nuxt))
}
await nuxt.callHook('build:before') await nuxt.callHook('build:before')
if (!nuxt.options._prepare) { if (nuxt.options._prepare) {
await Promise.all([checkForExternalConfigurationFiles(), bundle(nuxt)]) nuxt.hook('prepare:types', () => nuxt.close())
return
}
if (nuxt.options.dev) {
checkForExternalConfigurationFiles()
}
await bundle(nuxt)
await nuxt.callHook('build:done') await nuxt.callHook('build:done')
if (!nuxt.options.dev) { if (!nuxt.options.dev) {
await nuxt.callHook('close', nuxt) await nuxt.callHook('close', nuxt)
} }
} else {
nuxt.hook('prepare:types', () => nuxt.close())
}
} }
const watchEvents: Record<EventType, 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'> = { const watchEvents: Record<EventType, 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'> = {

View File

@ -0,0 +1,275 @@
import { mkdir, open, readFile, stat, unlink, writeFile } from 'node:fs/promises'
import type { FileHandle } from 'node:fs/promises'
import { resolve } from 'node:path'
import { existsSync } from 'node:fs'
import { isIgnored } from '@nuxt/kit'
import type { Nuxt, NuxtConfig, NuxtConfigLayer } from '@nuxt/schema'
import { hash, murmurHash, objectHash } from 'ohash'
import { glob } from 'tinyglobby'
import _consola, { consola } from 'consola'
import { dirname, join, relative } from 'pathe'
import { createTar, parseTar } from 'nanotar'
import type { TarFileInput } from 'nanotar'
export async function getVueHash (nuxt: Nuxt) {
const id = 'vue'
const { hash } = await getHashes(nuxt, {
id,
cwd: layer => layer.config?.srcDir,
patterns: layer => [
join(relative(layer.cwd, layer.config.srcDir), '**'),
`!${relative(layer.cwd, layer.config.serverDir || join(layer.cwd, 'server'))}/**`,
`!${relative(layer.cwd, resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.public || 'public'))}/**`,
`!${relative(layer.cwd, resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.static || 'public'))}/**`,
'!node_modules/**',
'!nuxt.config.*',
],
configOverrides: {
buildId: undefined,
serverDir: undefined,
nitro: undefined,
devServer: undefined,
runtimeConfig: undefined,
logLevel: undefined,
devServerHandlers: undefined,
generate: undefined,
devtools: undefined,
},
})
const cacheFile = join(nuxt.options.workspaceDir, 'node_modules/.cache/nuxt/builds', id, hash + '.tar')
return {
hash,
async collectCache () {
const start = Date.now()
await writeCache(nuxt.options.buildDir, nuxt.options.buildDir, cacheFile)
const elapsed = Date.now() - start
consola.success(`Cached Vue client and server builds in \`${elapsed}ms\`.`)
},
async restoreCache () {
const start = Date.now()
const res = await restoreCache(nuxt.options.buildDir, cacheFile)
const elapsed = Date.now() - start
if (res) {
consola.success(`Restored Vue client and server builds from cache in \`${elapsed}ms\`.`)
}
return res
},
}
}
export async function cleanupCaches (nuxt: Nuxt) {
const start = Date.now()
const caches = await glob(['*/*.tar'], {
cwd: join(nuxt.options.workspaceDir, 'node_modules/.cache/nuxt/builds'),
absolute: true,
})
if (caches.length >= 10) {
const cachesWithMeta = await Promise.all(caches.map(async (cache) => {
return [cache, await stat(cache).then(r => r.mtime.getTime()).catch(() => 0)] as const
}))
cachesWithMeta.sort((a, b) => a[1] - b[1])
for (const [cache] of cachesWithMeta.slice(0, cachesWithMeta.length - 10)) {
await unlink(cache)
}
const elapsed = Date.now() - start
consola.success(`Cleaned up old build caches in \`${elapsed}ms\`.`)
}
}
// internal
type HashSource = { name: string, data: any }
type Hashes = { hash: string, sources: HashSource[] }
interface GetHashOptions {
id: string
cwd: (layer: NuxtConfigLayer) => string
patterns: (layer: NuxtConfigLayer) => string[]
configOverrides: Partial<Record<keyof NuxtConfig, unknown>>
}
async function getHashes (nuxt: Nuxt, options: GetHashOptions): Promise<Hashes> {
if ((nuxt as any)[`_${options.id}BuildHash`]) {
return (nuxt as any)[`_${options.id}BuildHash`]
}
const start = Date.now()
const hashSources: HashSource[] = []
// Layers
let layerCtr = 0
for (const layer of nuxt.options._layers) {
if (layer.cwd.includes('node_modules')) { continue }
const layerName = `layer#${layerCtr++}`
hashSources.push({
name: `${layerName}:config`,
data: objectHash({
...layer.config,
...options.configOverrides || {},
}),
})
const normalizeFiles = (files: Awaited<ReturnType<typeof readFilesRecursive>>) => files.map(f => ({
name: f.name,
size: (f.attrs as any)?.size,
data: murmurHash(f.data as any /* ArrayBuffer */),
}))
const sourceFiles = await readFilesRecursive(options.cwd(layer), {
shouldIgnore: isIgnored, // TODO: Validate if works with absolute paths
cwd: nuxt.options.rootDir,
patterns: options.patterns(layer),
})
hashSources.push({
name: `${layerName}:src`,
data: normalizeFiles(sourceFiles),
})
const rootFiles = await readFilesRecursive(layer.config?.rootDir || layer.cwd, {
shouldIgnore: isIgnored, // TODO: Validate if works with absolute paths
cwd: nuxt.options.rootDir,
patterns: [
'.nuxtrc',
'.npmrc',
'package.json',
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'tsconfig.json',
'bun.lockb',
],
})
hashSources.push({
name: `${layerName}:root`,
data: normalizeFiles(rootFiles),
})
}
const res = ((nuxt as any)[`_${options.id}BuildHash`] = {
hash: hash(hashSources),
sources: hashSources,
})
const elapsed = Date.now() - start
consola.debug(`Computed \`${options.id}\` build hash in \`${elapsed}ms\`.`)
return res
}
type FileWithMeta = TarFileInput & {
attrs: {
mtime: number
size: number
}
}
interface ReadFilesRecursiveOptions {
shouldIgnore?: (name: string) => boolean
patterns: string[]
cwd: string
}
async function readFilesRecursive (dir: string | string[], opts: ReadFilesRecursiveOptions): Promise<FileWithMeta[]> {
if (Array.isArray(dir)) {
return (await Promise.all(dir.map(d => readFilesRecursive(d, opts)))).flat()
}
const files = await glob(opts.patterns, { cwd: dir })
const fileEntries = await Promise.all(files.map(async (fileName) => {
if (!opts.shouldIgnore?.(fileName)) {
const file = await readFileWithMeta(dir, fileName)
if (!file) { return }
return {
...file,
name: relative(opts.cwd, join(dir, file.name)),
}
}
}))
return fileEntries.filter(Boolean) as FileWithMeta[]
}
async function readFileWithMeta (dir: string, fileName: string, count = 0): Promise<FileWithMeta | undefined> {
let fd: FileHandle | undefined = undefined
try {
fd = await open(resolve(dir, fileName))
const stats = await fd.stat()
if (!stats?.isFile()) { return }
const mtime = stats.mtime.getTime()
const data = await fd.readFile()
// retry if file has changed during read
if ((await fd.stat()).mtime.getTime() !== mtime) {
if (count < 5) {
return readFileWithMeta(dir, fileName, count + 1)
}
console.warn(`Failed to read file \`${fileName}\` as it changed during read.`)
return
}
return {
name: fileName,
data,
attrs: {
mtime,
size: stats.size,
},
}
} catch (err) {
console.warn(`Failed to read file \`${fileName}\`:`, err)
} finally {
await fd?.close()
}
}
async function restoreCache (cwd: string, cacheFile: string) {
if (!existsSync(cacheFile)) {
return false
}
const files = parseTar(await readFile(cacheFile))
for (const file of files) {
let fd: FileHandle | undefined = undefined
try {
const filePath = resolve(cwd, file.name)
await mkdir(dirname(filePath), { recursive: true })
fd = await open(filePath, 'w')
const stats = await fd.stat().catch(() => null)
if (stats?.isFile() && stats.size) {
const lastModified = Number.parseInt(file.attrs?.mtime?.toString().padEnd(13, '0') || '0')
if (stats.mtime.getTime() >= lastModified) {
consola.debug(`Skipping \`${file.name}\` (up to date or newer than cache)`)
continue
}
}
await fd.writeFile(file.data!)
} catch (err) {
console.error(err)
} finally {
await fd?.close()
}
}
return true
}
async function writeCache (cwd: string, sources: string | string[], cacheFile: string) {
const fileEntries = await readFilesRecursive(sources, {
patterns: ['**/*', '!analyze/**'],
cwd,
})
const tarData = createTar(fileEntries)
await mkdir(dirname(cacheFile), { recursive: true })
await writeFile(cacheFile, tarData)
}

View File

@ -11,12 +11,13 @@ import escapeRE from 'escape-string-regexp'
import { defu } from 'defu' import { defu } from 'defu'
import { dynamicEventHandler } from 'h3' import { dynamicEventHandler } from 'h3'
import { isWindows } from 'std-env' import { isWindows } from 'std-env'
import { ImpoundPlugin } from 'impound'
import type { Nuxt, NuxtOptions } from 'nuxt/schema' import type { Nuxt, NuxtOptions } from 'nuxt/schema'
import { version as nuxtVersion } from '../../package.json' import { version as nuxtVersion } from '../../package.json'
import { distDir } from '../dirs' import { distDir } from '../dirs'
import { toArray } from '../utils' import { toArray } from '../utils'
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon' import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection' import { nuxtImportProtections } from './plugins/import-protection'
const logLevelMapReverse = { const logLevelMapReverse = {
silent: 0, silent: 0,
@ -358,9 +359,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || [] nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins) nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)
nitroConfig.rollupConfig!.plugins!.push( nitroConfig.rollupConfig!.plugins!.push(
ImportProtectionPlugin.rollup({ ImpoundPlugin.rollup({
rootDir: nuxt.options.rootDir, cwd: nuxt.options.rootDir,
modulesDir: nuxt.options.modulesDir,
patterns: nuxtImportProtections(nuxt, { isNitro: true }), patterns: nuxtImportProtections(nuxt, { isNitro: true }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/], exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
}), }),
@ -517,19 +517,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}) })
} }
// nuxt build/dev async function symlinkDist () {
nuxt.hook('build:done', async () => {
await nuxt.callHook('nitro:build:before', nitro)
if (nuxt.options.dev) {
await build(nitro)
} else {
await prepare(nitro)
await prerender(nitro)
logger.restoreAll()
await build(nitro)
logger.wrapAll()
if (nitro.options.static) { if (nitro.options.static) {
const distDir = resolve(nuxt.options.rootDir, 'dist') const distDir = resolve(nuxt.options.rootDir, 'dist')
if (!existsSync(distDir)) { if (!existsSync(distDir)) {
@ -537,6 +525,22 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
} }
} }
} }
// nuxt build/dev
nuxt.hook('build:done', async () => {
await nuxt.callHook('nitro:build:before', nitro)
if (nuxt.options.dev) {
return build(nitro)
}
await prepare(nitro)
await prerender(nitro)
logger.restoreAll()
await build(nitro)
logger.wrapAll()
await symlinkDist()
}) })
// nuxt dev // nuxt dev

View File

@ -15,13 +15,14 @@ import { colorize } from 'consola/utils'
import { updateConfig } from 'c12/update' import { updateConfig } from 'c12/update'
import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx' import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx'
import type { DateString } from 'compatx' import type { DateString } from 'compatx'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { withTrailingSlash, withoutLeadingSlash } from 'ufo' import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
import { ImpoundPlugin } from 'impound'
import type { ImpoundOptions } from 'impound'
import defu from 'defu' import defu from 'defu'
import { gt, satisfies } from 'semver' import { gt, satisfies } from 'semver'
import { hasTTY, isCI } from 'std-env' import { hasTTY, isCI } from 'std-env'
import pagesModule from '../pages/module' import pagesModule from '../pages/module'
import metaModule from '../head/module' import metaModule from '../head/module'
import componentsModule from '../components/module' import componentsModule from '../components/module'
@ -31,7 +32,7 @@ import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json' import { version } from '../../package.json'
import { scriptsStubsPreset } from '../imports/presets' import { scriptsStubsPreset } from '../imports/presets'
import { resolveTypePath } from './utils/types' import { resolveTypePath } from './utils/types'
import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection' import { nuxtImportProtections } from './plugins/import-protection'
import type { UnctxTransformPluginOptions } from './plugins/unctx' import type { UnctxTransformPluginOptions } from './plugins/unctx'
import { UnctxTransformPlugin } from './plugins/unctx' import { UnctxTransformPlugin } from './plugins/unctx'
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake' import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
@ -103,7 +104,7 @@ async function initNuxt (nuxt: Nuxt) {
const shouldShowPrompt = nuxt.options.dev && hasTTY && !isCI const shouldShowPrompt = nuxt.options.dev && hasTTY && !isCI
if (!shouldShowPrompt) { if (!shouldShowPrompt) {
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`) logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
} }
async function promptAndUpdate () { async function promptAndUpdate () {
@ -112,7 +113,7 @@ async function initNuxt (nuxt: Nuxt) {
default: true, default: true,
}) })
if (result !== true) { if (result !== true) {
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`) logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
return return
} }
@ -146,7 +147,7 @@ async function initNuxt (nuxt: Nuxt) {
consola.error(`Failed to update config: ${message}`) consola.error(`Failed to update config: ${message}`)
} }
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`) logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
} }
nuxt.hooks.hookOnce('nitro:init', (nitro) => { nuxt.hooks.hookOnce('nitro:init', (nitro) => {
@ -155,7 +156,7 @@ async function initNuxt (nuxt: Nuxt) {
nitro.hooks.hookOnce('compiled', () => { nitro.hooks.hookOnce('compiled', () => {
warnedAboutCompatDate = true warnedAboutCompatDate = true
// Print warning // Print warning
console.info(`Nuxt now supports pinning the behavior of provider and deployment presets with a compatibility date. We recommend you specify a \`compatibilityDate\` in your \`nuxt.config\` file, or set an environment variable, such as \`COMPATIBILITY_DATE=${todaysDate}\`.`) logger.info(`Nuxt now supports pinning the behavior of provider and deployment presets with a compatibility date. We recommend you specify a \`compatibilityDate\` in your \`nuxt.config\` file, or set an environment variable, such as \`COMPATIBILITY_DATE=${todaysDate}\`.`)
if (shouldShowPrompt) { promptAndUpdate() } if (shouldShowPrompt) { promptAndUpdate() }
}) })
}) })
@ -245,18 +246,19 @@ async function initNuxt (nuxt: Nuxt) {
addBuildPlugin(RemovePluginMetadataPlugin(nuxt)) addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
// Add import protection // Add import protection
const config = { const config: ImpoundOptions = {
rootDir: nuxt.options.rootDir, cwd: nuxt.options.rootDir,
// Exclude top-level resolutions by plugins // Exclude top-level resolutions by plugins
exclude: [join(nuxt.options.srcDir, 'index.html')], exclude: [join(nuxt.options.srcDir, 'index.html')],
patterns: nuxtImportProtections(nuxt), patterns: nuxtImportProtections(nuxt),
modulesDir: nuxt.options.modulesDir,
} }
addVitePlugin(() => ImportProtectionPlugin.vite(config)) addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: false }), { name: 'nuxt:import-protection' }), { client: false })
addWebpackPlugin(() => ImportProtectionPlugin.webpack(config)) addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: true }), { name: 'nuxt:import-protection' }), { server: false })
addWebpackPlugin(() => ImpoundPlugin.webpack(config))
// add resolver for modules used in virtual files // add resolver for modules used in virtual files
addVitePlugin(() => resolveDeepImportsPlugin(nuxt)) addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false })
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { server: false })
// Add transform for `onPrehydrate` lifecycle hook // Add transform for `onPrehydrate` lifecycle hook
addBuildPlugin(prehydrateTransformPlugin(nuxt)) addBuildPlugin(prehydrateTransformPlugin(nuxt))
@ -664,7 +666,7 @@ async function initNuxt (nuxt: Nuxt) {
// Show compatibility version banner when Nuxt is running with a compatibility version // Show compatibility version banner when Nuxt is running with a compatibility version
// that is different from the current major version // that is different from the current major version
if (!(satisfies(nuxt._version, nuxt.options.future.compatibilityVersion + '.x'))) { if (!(satisfies(nuxt._version, nuxt.options.future.compatibilityVersion + '.x'))) {
console.info(`Running with compatibility version \`${nuxt.options.future.compatibilityVersion}\``) logger.info(`Running with compatibility version \`${nuxt.options.future.compatibilityVersion}\``)
} }
await nuxt.callHook('ready', nuxt) await nuxt.callHook('ready', nuxt)

View File

@ -1,7 +1,4 @@
import { createUnplugin } from 'unplugin' import { relative, resolve } from 'pathe'
import { logger } from '@nuxt/kit'
import { resolvePath } from 'mlly'
import { isAbsolute, join, relative, resolve } from 'pathe'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import type { NuxtOptions } from 'nuxt/schema' import type { NuxtOptions } from 'nuxt/schema'
@ -53,41 +50,3 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
return patterns return patterns
} }
export const ImportProtectionPlugin = createUnplugin(function (options: ImportProtectionOptions) {
const cache: Record<string, Map<string | RegExp, boolean>> = {}
const importersToExclude = options?.exclude || []
const proxy = resolvePath('unenv/runtime/mock/proxy', { url: options.modulesDir })
return {
name: 'nuxt:import-protection',
enforce: 'pre',
resolveId (id, importer) {
if (!importer) { return }
if (id[0] === '.') {
id = join(importer, '..', id)
}
if (isAbsolute(id)) {
id = relative(options.rootDir, id)
}
if (importersToExclude.some(p => typeof p === 'string' ? importer === p : p.test(importer))) { return }
const invalidImports = options.patterns.filter(([pattern]) => pattern instanceof RegExp ? pattern.test(id) : pattern === id)
let matched = false
for (const match of invalidImports) {
cache[id] = cache[id] || new Map()
const [pattern, warning] = match
// Skip if already warned
if (cache[id].has(pattern)) { continue }
const relativeImporter = isAbsolute(importer) ? relative(options.rootDir, importer) : importer
logger.error(warning || 'Invalid import', `[importing \`${id}\` from \`${relativeImporter}\`]`)
cache[id].set(pattern, true)
matched = true
}
if (matched) {
return proxy
}
return null
},
}
})

View File

@ -149,29 +149,12 @@ export const RemovePluginMetadataPlugin = (nuxt: Nuxt) => createUnplugin(() => {
if (_node.type === 'ImportSpecifier' && (_node.imported.name === 'defineNuxtPlugin' || _node.imported.name === 'definePayloadPlugin')) { if (_node.type === 'ImportSpecifier' && (_node.imported.name === 'defineNuxtPlugin' || _node.imported.name === 'definePayloadPlugin')) {
wrapperNames.add(_node.local.name) wrapperNames.add(_node.local.name)
} }
if (_node.type === 'ExportDefaultDeclaration' && (_node.declaration.type === 'FunctionDeclaration' || _node.declaration.type === 'ArrowFunctionExpression')) {
if ('params' in _node.declaration && _node.declaration.params.length > 1) {
logger.warn(`Plugin \`${plugin.src}\` is in legacy Nuxt 2 format (context, inject) which is likely to be broken and will be ignored.`)
s.overwrite(0, code.length, 'export default () => {}')
wrapped = true // silence a duplicate error
return
}
}
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return } if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
const node = _node as CallExpression & { start: number, end: number } const node = _node as CallExpression & { start: number, end: number }
const name = 'name' in node.callee && node.callee.name const name = 'name' in node.callee && node.callee.name
if (!name || !wrapperNames.has(name)) { return } if (!name || !wrapperNames.has(name)) { return }
wrapped = true wrapped = true
if (node.arguments[0].type !== 'ObjectExpression') {
// TODO: Warn if legacy plugin format is detected
if ('params' in node.arguments[0] && node.arguments[0].params.length > 1) {
logger.warn(`Plugin \`${plugin.src}\` is in legacy Nuxt 2 format (context, inject) which is likely to be broken and will be ignored.`)
s.overwrite(0, code.length, 'export default () => {}')
return
}
}
// Remove metadata that already has been extracted // Remove metadata that already has been extracted
if (!('order' in plugin) && !('name' in plugin)) { return } if (!('order' in plugin) && !('name' in plugin)) { return }
for (const [argIndex, _arg] of node.arguments.entries()) { for (const [argIndex, _arg] of node.arguments.entries()) {

View File

@ -8,20 +8,25 @@ import { pkgDir } from '../../dirs'
export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin { export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite'] const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite']
let conditions: string[]
return { return {
name: 'nuxt:resolve-bare-imports', name: 'nuxt:resolve-bare-imports',
enforce: 'post', enforce: 'post',
async resolveId (id, importer, options) { configResolved (config) {
conditions = config.mode === 'test' ? [...config.resolve.conditions, 'import', 'require'] : config.resolve.conditions
},
async resolveId (id, importer) {
if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:')) || exclude.some(e => id.startsWith(e))) { if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:')) || exclude.some(e => id.startsWith(e))) {
return return
} }
id = normalize(id)
id = resolveAlias(id, nuxt.options.alias) const normalisedId = resolveAlias(normalize(id), nuxt.options.alias)
const { dir } = parseNodeModulePath(importer) const normalisedImporter = importer.replace(/^\0?virtual:(?:nuxt:)?/, '')
return await this.resolve?.(id, dir || pkgDir, { skipSelf: true }) ?? await resolvePath(id, { const dir = parseNodeModulePath(normalisedImporter).dir || pkgDir
url: [dir || pkgDir, ...nuxt.options.modulesDir],
// TODO: respect nitro runtime conditions return await this.resolve?.(normalisedId, dir, { skipSelf: true }) ?? await resolvePath(id, {
conditions: options.ssr ? ['node', 'import', 'require'] : ['import', 'require'], url: [dir, ...nuxt.options.modulesDir],
conditions,
}).catch(() => { }).catch(() => {
logger.debug('Could not resolve id', id, importer) logger.debug('Could not resolve id', id, importer)
return null return null

View File

@ -20,8 +20,8 @@ export default defineDriver((opts) => {
...fs, // fall back to file system - only the bottom three methods are used in renderer ...fs, // fall back to file system - only the bottom three methods are used in renderer
async setItem (key, value, opts) { async setItem (key, value, opts) {
await Promise.all([ await Promise.all([
fs.setItem(normalizeFsKey(key), value, opts), fs.setItem?.(normalizeFsKey(key), value, opts),
lru.setItem(key, value, opts), lru.setItem?.(key, value, opts),
]) ])
}, },
async hasItem (key, opts) { async hasItem (key, opts) {

View File

@ -77,7 +77,8 @@ export default (nitroApp: NitroApp) => {
const ctx = asyncContext.tryUse() const ctx = asyncContext.tryUse()
if (!ctx) { return } if (!ctx) { return }
try { try {
htmlContext.bodyAppend.unshift(`<script type="application/json" data-nuxt-logs="${appId}">${stringify(ctx.logs, { ...devReducers, ...ctx.event.context._payloadReducers })}</script>`) const reducers = Object.assign(Object.create(null), devReducers, ctx.event.context._payloadReducers)
htmlContext.bodyAppend.unshift(`<script type="application/json" data-nuxt-logs="${appId}">${stringify(ctx.logs, reducers)}</script>`)
} catch (e) { } catch (e) {
const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : '' const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : ''
console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`) console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`)

View File

@ -31,7 +31,7 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
error.fatal && '[fatal]', error.fatal && '[fatal]',
Number(errorObject.statusCode) !== 200 && `[${errorObject.statusCode}]`, Number(errorObject.statusCode) !== 200 && `[${errorObject.statusCode}]`,
].filter(Boolean).join(' ') ].filter(Boolean).join(' ')
console.error(tags, errorObject.message + '\n' + stack.map(l => ' ' + l.text).join(' \n')) console.error(tags, (error.message || error.toString() || 'internal server error') + '\n' + stack.map(l => ' ' + l.text).join(' \n'))
} }
if (event.handled) { return } if (event.handled) { return }
@ -119,7 +119,7 @@ function normalizeError (error: any) {
// Hide details of unhandled/fatal errors in production // Hide details of unhandled/fatal errors in production
const hideDetails = !import.meta.dev && error.unhandled const hideDetails = !import.meta.dev && error.unhandled
const stack = hideDetails const stack = hideDetails && !import.meta.prerender
? [] ? []
: ((error.stack as string) || '') : ((error.stack as string) || '')
.split('\n') .split('\n')

View File

@ -16,11 +16,10 @@ import { stringify, uneval } from 'devalue'
import destr from 'destr' import destr from 'destr'
import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo' import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo'
import { renderToString as _renderToString } from 'vue/server-renderer' import { renderToString as _renderToString } from 'vue/server-renderer'
import { hash } from 'ohash'
import { propsToString, renderSSRHead } from '@unhead/ssr' import { propsToString, renderSSRHead } from '@unhead/ssr'
import type { HeadEntryOptions } from '@unhead/schema' import type { Head, HeadEntryOptions } from '@unhead/schema'
import type { Link, Script, Style } from '@unhead/vue' import type { Link, Script, Style } from '@unhead/vue'
import { createServerHead } from '@unhead/vue' import { createServerHead, resolveUnrefHeadInput } from '@unhead/vue'
import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig, useStorage } from 'nitro/runtime' import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig, useStorage } from 'nitro/runtime'
@ -79,10 +78,7 @@ export interface NuxtIslandContext {
export interface NuxtIslandResponse { export interface NuxtIslandResponse {
id?: string id?: string
html: string html: string
head: { head: Head
link: (Record<string, string>)[]
style: ({ innerHTML: string, key: string })[]
}
props?: Record<string, Record<string, any>> props?: Record<string, Record<string, any>>
components?: Record<string, NuxtIslandClientResponse> components?: Record<string, NuxtIslandClientResponse>
slots?: Record<string, NuxtIslandSlotResponse> slots?: Record<string, NuxtIslandSlotResponse>
@ -162,9 +158,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
const renderToString = (ssrContext: NuxtSSRContext) => { const renderToString = (ssrContext: NuxtSSRContext) => {
const config = useRuntimeConfig(ssrContext.event) const config = useRuntimeConfig(ssrContext.event)
ssrContext.modules = ssrContext.modules || new Set<string>() ssrContext.modules = ssrContext.modules || new Set<string>()
ssrContext!.payload = { ssrContext.payload.serverRendered = false
serverRendered: false,
}
ssrContext.config = { ssrContext.config = {
public: config.public, public: config.public,
app: config.app, app: config.app,
@ -288,6 +282,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
const head = createServerHead({ const head = createServerHead({
plugins: unheadPlugins, plugins: unheadPlugins,
}) })
// needed for hash hydration plugin to work // needed for hash hydration plugin to work
const headEntryOptions: HeadEntryOptions = { mode: 'server' } const headEntryOptions: HeadEntryOptions = { mode: 'server' }
if (!isRenderingIsland) { if (!isRenderingIsland) {
@ -308,7 +303,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
error: !!ssrError, error: !!ssrError,
nuxt: undefined!, /* NuxtApp */ nuxt: undefined!, /* NuxtApp */
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload, payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
_payloadReducers: {}, _payloadReducers: Object.create(null),
modules: new Set(), modules: new Set(),
islandContext, islandContext,
} }
@ -394,7 +389,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
} }
// 2. Styles // 2. Styles
if (inlinedStyles.length) {
head.push({ style: inlinedStyles }) head.push({ style: inlinedStyles })
}
if (!isRenderingIsland || import.meta.dev) { if (!isRenderingIsland || import.meta.dev) {
const link: Link[] = [] const link: Link[] = []
for (const style in styles) { for (const style in styles) {
@ -411,8 +408,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
link.push({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) }) link.push({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
} }
} }
if (link.length) {
head.push({ link }, headEntryOptions) head.push({ link }, headEntryOptions)
} }
}
if (!NO_SCRIPTS && !isRenderingIsland) { if (!NO_SCRIPTS && !isRenderingIsland) {
// 3. Resource Hints // 3. Resource Hints
@ -460,17 +459,21 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Response for component islands // Response for component islands
if (isRenderingIsland && islandContext) { if (isRenderingIsland && islandContext) {
const islandHead: NuxtIslandResponse['head'] = { const islandHead: Head = {}
link: [], for (const entry of head.headEntries()) {
style: [], for (const [key, value] of Object.entries(resolveUnrefHeadInput(entry.input) as Head)) {
const currentValue = islandHead[key as keyof Head]
if (Array.isArray(currentValue)) {
currentValue.push(...value)
} }
for (const tag of await head.resolveTags()) { islandHead[key as keyof Head] = value
if (tag.tag === 'link') {
islandHead.link.push({ key: 'island-link-' + hash(tag.props), ...tag.props })
} else if (tag.tag === 'style' && tag.innerHTML) {
islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
} }
} }
// TODO: remove for v4
islandHead.link = islandHead.link || []
islandHead.style = islandHead.style || []
const islandResponse: NuxtIslandResponse = { const islandResponse: NuxtIslandResponse = {
id: islandContext.id, id: islandContext.id,
head: islandHead, head: islandHead,

View File

@ -120,11 +120,21 @@ export const pluginsDeclaration: NuxtTemplate = {
const relativePath = relative(typesDir, pluginPath) const relativePath = relative(typesDir, pluginPath)
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts') const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts')
// if `.d.ts` file exists alongside a `.js` plugin, or if `.d.mts` file exists alongside a `.mjs` plugin, we can use the entire path
if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) { if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
tsImports.push(relativePath) tsImports.push(relativePath)
continue continue
} }
const incorrectDeclaration = pluginPath.replace(/\.[cm]jsx?$/, '.d.ts')
// if `.d.ts` file exists, but plugin is `.mjs`, add `.js` extension to the import
// to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0`
if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) {
tsImports.push(relativePath.replace(/\.[cm](jsx?)$/, '.$1'))
continue
}
// if there is no declaration we only want to remove the extension if it's a TypeScript file
if (exists(pluginPath)) { if (exists(pluginPath)) {
if (TS_RE.test(pluginPath)) { if (TS_RE.test(pluginPath)) {
tsImports.push(relativePath.replace(EXTENSION_RE, '')) tsImports.push(relativePath.replace(EXTENSION_RE, ''))
@ -181,7 +191,7 @@ export const schemaTemplate: NuxtTemplate = {
} }
} }
const moduleOptionsInterface = (jsdocTags: boolean) => [ const moduleOptionsInterface = (options: { addJSDocTags: boolean, unresolved: boolean }) => [
...modules.flatMap(([configKey, importName, mod]) => { ...modules.flatMap(([configKey, importName, mod]) => {
let link: string | undefined let link: string | undefined
@ -211,30 +221,32 @@ export const schemaTemplate: NuxtTemplate = {
return [ return [
` /**`, ` /**`,
` * Configuration for \`${importName}\``, ` * Configuration for \`${importName}\``,
...jsdocTags && link ...options.addJSDocTags && link ? [` * @see ${link}`] : [],
? [
` * @see ${link}`,
]
: [],
` */`, ` */`,
` [${configKey}]?: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`, ` [${configKey}]${options.unresolved ? '?' : ''}: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? ${options.unresolved ? 'Partial<O>' : 'O'} : Record<string, any>`,
] ]
}), }),
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, mod]) => `[${genString(mod.meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '', modules.length > 0 && options.unresolved ? ` modules?: (undefined | null | false | NuxtModule<any> | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, mod]) => `[${genString(mod.meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
].filter(Boolean) ].filter(Boolean)
return [ return [
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'', 'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
'declare module \'@nuxt/schema\' {', 'declare module \'@nuxt/schema\' {',
' interface NuxtOptions {',
...moduleOptionsInterface({ addJSDocTags: false, unresolved: false }),
' }',
' interface NuxtConfig {', ' interface NuxtConfig {',
// TypeScript will duplicate the jsdoc tags if we augment it twice // TypeScript will duplicate the jsdoc tags if we augment it twice
// So here we only generate tags for `nuxt/schema` // So here we only generate tags for `nuxt/schema`
...moduleOptionsInterface(false), ...moduleOptionsInterface({ addJSDocTags: false, unresolved: true }),
' }', ' }',
'}', '}',
'declare module \'nuxt/schema\' {', 'declare module \'nuxt/schema\' {',
' interface NuxtOptions {',
...moduleOptionsInterface({ addJSDocTags: true, unresolved: false }),
' }',
' interface NuxtConfig {', ' interface NuxtConfig {',
...moduleOptionsInterface(true), ...moduleOptionsInterface({ addJSDocTags: true, unresolved: true }),
' }', ' }',
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>), generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
{ {
@ -267,7 +279,7 @@ export const layoutTemplate: NuxtTemplate = {
filename: 'layouts.mjs', filename: 'layouts.mjs',
getContents ({ app }) { getContents ({ app }) {
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => { const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
return [name, genDynamicImport(file, { interopDefault: true })] return [name, genDynamicImport(file)]
})) }))
return [ return [
`export default ${layoutsObject}`, `export default ${layoutsObject}`,
@ -504,6 +516,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`, `export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`,
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`, `export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`, `export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
].join('\n\n') ].join('\n\n')
}, },
} }

View File

@ -1,9 +1,10 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, isIgnored, logger, resolveAlias, tryResolveModule, updateTemplates, useNuxt } from '@nuxt/kit' import { addBuildPlugin, addTemplate, addTypeTemplate, defineNuxtModule, isIgnored, logger, resolveAlias, tryResolveModule, updateTemplates, useNuxt } from '@nuxt/kit'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe' import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
import type { Import, Unimport } from 'unimport' import type { Import, Unimport } from 'unimport'
import { createUnimport, scanDirExports, toExports } from 'unimport' import { createUnimport, scanDirExports, toExports } from 'unimport'
import type { ImportPresetWithDeprecation, ImportsOptions, ResolvedNuxtTemplate } from 'nuxt/schema' import type { ImportPresetWithDeprecation, ImportsOptions, ResolvedNuxtTemplate } from 'nuxt/schema'
import escapeRE from 'escape-string-regexp'
import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly' import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly'
import { isDirectory } from '../utils' import { isDirectory } from '../utils'
@ -15,7 +16,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
name: 'imports', name: 'imports',
configKey: 'imports', configKey: 'imports',
}, },
defaults: { defaults: nuxt => ({
autoImport: true, autoImport: true,
scan: true, scan: true,
presets: defaultPresets, presets: defaultPresets,
@ -23,11 +24,13 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
imports: [], imports: [],
dirs: [], dirs: [],
transform: { transform: {
include: [], include: [
new RegExp('^' + escapeRE(nuxt.options.buildDir)),
],
exclude: undefined, exclude: undefined,
}, },
virtualImports: ['#imports'], virtualImports: ['#imports'],
}, }),
async setup (options, nuxt) { async setup (options, nuxt) {
// TODO: fix sharing of defaults between invocations of modules // TODO: fix sharing of defaults between invocations of modules
const presets = JSON.parse(JSON.stringify(options.presets)) as ImportPresetWithDeprecation[] const presets = JSON.parse(JSON.stringify(options.presets)) as ImportPresetWithDeprecation[]
@ -40,6 +43,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
// Create a context to share state between module internals // Create a context to share state between module internals
const ctx = createUnimport({ const ctx = createUnimport({
injectAtEnd: true,
...options, ...options,
addons: { addons: {
vueTemplate: options.autoImport, vueTemplate: options.autoImport,
@ -91,8 +95,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
nuxt.options.alias['#imports'] = join(nuxt.options.buildDir, 'imports') nuxt.options.alias['#imports'] = join(nuxt.options.buildDir, 'imports')
// Transform to inject imports in production mode // Transform to inject imports in production mode
addVitePlugin(() => TransformPlugin.vite({ ctx, options, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client })) addBuildPlugin(TransformPlugin({ ctx, options, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }))
addWebpackPlugin(() => TransformPlugin.webpack({ ctx, options, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }))
const priorities = nuxt.options._layers.map((layer, i) => [layer.config.srcDir, -i] as const).sort(([a], [b]) => b.length - a.length) const priorities = nuxt.options._layers.map((layer, i) => [layer.config.srcDir, -i] as const).sort(([a], [b]) => b.length - a.length)

View File

@ -42,7 +42,7 @@ const granularAppPresets: InlinePreset[] = [
from: '#app/composables/asyncData', from: '#app/composables/asyncData',
}, },
{ {
imports: ['useHydration', 'createVisibleLoader', 'createIdleLoader', 'createEventLoader'], imports: ['useHydration'],
from: '#app/composables/hydrate', from: '#app/composables/hydrate',
}, },
{ {
@ -229,12 +229,8 @@ const vuePreset = defineUnimportPreset({
'useTransitionState', 'useTransitionState',
'useId', 'useId',
'useTemplateRef', 'useTemplateRef',
'hydrateOnInteraction',
'hydrateOnMediaQuery',
'hydrateOnVisible',
'hydrateOnIdle',
'useHost',
'useShadowRoot', 'useShadowRoot',
'useCssVars',
], ],
}) })

View File

@ -8,7 +8,7 @@ import { isJS, isVue } from '../core/utils'
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/ const NODE_MODULES_RE = /[\\/]node_modules[\\/]/
const IMPORTS_RE = /(['"])#imports\1/ const IMPORTS_RE = /(['"])#imports\1/
export const TransformPlugin = createUnplugin(({ ctx, options, sourcemap }: { ctx: Unimport, options: Partial<ImportsOptions>, sourcemap?: boolean }) => { export const TransformPlugin = ({ ctx, options, sourcemap }: { ctx: Unimport, options: Partial<ImportsOptions>, sourcemap?: boolean }) => createUnplugin(() => {
return { return {
name: 'nuxt:imports-transform', name: 'nuxt:imports-transform',
enforce: 'post', enforce: 'post',

View File

@ -515,7 +515,7 @@ export default defineNuxtModule({
const namedMiddleware = app.middleware.filter(mw => !mw.global) const namedMiddleware = app.middleware.filter(mw => !mw.global)
return [ return [
'import type { NavigationGuard } from \'vue-router\'', 'import type { NavigationGuard } from \'vue-router\'',
`export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'string'}`, `export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'never'}`,
`declare module ${genString(composablesFile)} {`, `declare module ${genString(composablesFile)} {`,
' interface PageMeta {', ' interface PageMeta {',
' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>', ' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>',

View File

@ -18,10 +18,12 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
} }
if (!ROUTE_RULE_RE.test(code)) { return null } if (!ROUTE_RULE_RE.test(code)) { return null }
const script = extractScriptContent(code)
code = script?.code || code
let rule: NitroRouteConfig | null = null let rule: NitroRouteConfig | null = null
const contents = extractScriptContent(code)
for (const script of contents) {
if (rule) { break }
code = script?.code || code
const js = await transform(code, { loader: script?.loader || 'ts' }) const js = await transform(code, { loader: script?.loader || 'ts' })
walk(parse(js.code, { walk(parse(js.code, {
@ -42,6 +44,7 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
} }
}, },
}) })
}
ruleCache[code] = rule ruleCache[code] = rule
return rule return rule

View File

@ -122,6 +122,7 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
for (const key in _route.value) { for (const key in _route.value) {
Object.defineProperty(route, key, { Object.defineProperty(route, key, {
get: () => _route.value[key as keyof RouteLocation], get: () => _route.value[key as keyof RouteLocation],
enumerable: true,
}) })
} }

View File

@ -12,13 +12,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (result === true) { if (result === true) {
return return
} }
if (import.meta.server) {
return result
}
const error = createError({ const error = createError({
statusCode: 404, statusCode: (result && result.statusCode) || 404,
statusMessage: `Page Not Found: ${to.fullPath}`, statusMessage: (result && result.statusMessage) || `Page Not Found: ${to.fullPath}`,
data: { data: {
path: to.fullPath, path: to.fullPath,
}, },
@ -32,7 +29,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
// We pretend to have navigated to the invalid route so // We pretend to have navigated to the invalid route so
// that the user can return to the previous page with // that the user can return to the previous page with
// the back button. // the back button.
window.history.pushState({}, '', to.fullPath) window?.history.pushState({}, '', to.fullPath)
}) })
// We stop the navigation immediately before it resolves // We stop the navigation immediately before it resolves
// if there is no other route matching it. // if there is no other route matching it.

View File

@ -168,21 +168,23 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
return augmentedPages return augmentedPages
} }
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/i const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/gi
export function extractScriptContent (html: string) { export function extractScriptContent (html: string) {
const groups = html.match(SFC_SCRIPT_RE)?.groups || {} const contents: Array<{ loader: 'tsx' | 'ts', code: string }> = []
for (const match of html.matchAll(SFC_SCRIPT_RE)) {
if (groups.content) { if (match?.groups?.content) {
return { contents.push({
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts', loader: match.groups.attrs.includes('tsx') ? 'tsx' : 'ts',
code: groups.content.trim(), code: match.groups.content.trim(),
} as const })
}
} }
return null return contents
} }
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/ const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {} const pageContentsCache: Record<string, string> = {}
@ -197,15 +199,17 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
if (absolutePath in metaCache) { return metaCache[absolutePath] } if (absolutePath in metaCache) { return metaCache[absolutePath] }
const loader = getLoader(absolutePath) const loader = getLoader(absolutePath)
const script = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : { code: contents, loader } const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }]
if (!script) { if (!scriptBlocks) {
metaCache[absolutePath] = {} metaCache[absolutePath] = {}
return {} return {}
} }
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
for (const script of scriptBlocks) {
if (!PAGE_META_RE.test(script.code)) { if (!PAGE_META_RE.test(script.code)) {
metaCache[absolutePath] = {} continue
return {}
} }
const js = await transform(script.code, { loader: script.loader }) const js = await transform(script.code, { loader: script.loader })
@ -215,8 +219,6 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
ranges: true, ranges: true,
}) as unknown as Program }) as unknown as Program
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
const dynamicProperties = new Set<keyof NuxtPage>() const dynamicProperties = new Set<keyof NuxtPage>()
let foundMeta = false let foundMeta = false
@ -291,6 +293,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
} }
}, },
}) })
}
metaCache[absolutePath] = extractedMeta metaCache[absolutePath] = extractedMeta
return extractedMeta return extractedMeta
@ -518,7 +521,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
metaImports.add(genImport(file, [{ name: 'default', as: pageImportName }])) metaImports.add(genImport(file, [{ name: 'default', as: pageImportName }]))
} }
const pageImport = page._sync && page.mode !== 'client' ? pageImportName : genDynamicImport(file, { interopDefault: true }) const pageImport = page._sync && page.mode !== 'client' ? pageImportName : genDynamicImport(file)
const metaRoute: NormalizedRoute = { const metaRoute: NormalizedRoute = {
name: `${metaImportName}?.name ?? ${route.name}`, name: `${metaImportName}?.name ?? ${route.name}`,

View File

@ -2,7 +2,7 @@
"pushed route, skips generation from file": [ "pushed route, skips generation from file": [
{ {
"alias": "["pushed-route-alias"].concat(mockMeta?.alias || [])", "alias": "["pushed-route-alias"].concat(mockMeta?.alias || [])",
"component": "() => import("pages/route-file.vue").then(m => m.default || m)", "component": "() => import("pages/route-file.vue")",
"meta": "{ ...(mockMeta || {}), ...{"someMetaData":true} }", "meta": "{ ...(mockMeta || {}), ...{"someMetaData":true} }",
"name": "mockMeta?.name ?? "pushed-route"", "name": "mockMeta?.name ?? "pushed-route"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -20,7 +20,7 @@
"route.meta generated from file": [ "route.meta generated from file": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)", "component": "() => import("pages/page-with-meta.vue")",
"meta": "{ ...(mockMeta || {}), ...{"test":1} }", "meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"", "name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"", "path": "mockMeta?.path ?? "/page-with-meta"",
@ -30,7 +30,7 @@
"should allow pages with `:` in their path": [ "should allow pages with `:` in their path": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/test:name.vue").then(m => m.default || m)", "component": "() => import("pages/test:name.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "test:name"", "name": "mockMeta?.name ?? "test:name"",
"path": "mockMeta?.path ?? "/test\\:name"", "path": "mockMeta?.path ?? "/test\\:name"",
@ -46,7 +46,7 @@
"children": [ "children": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/param/index/index.vue").then(m => m.default || m)", "component": "() => import("pages/param/index/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-index"", "name": "mockMeta?.name ?? "param-index"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
@ -54,14 +54,14 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("layer/pages/param/index/sibling.vue").then(m => m.default || m)", "component": "() => import("layer/pages/param/index/sibling.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-index-sibling"", "name": "mockMeta?.name ?? "param-index-sibling"",
"path": "mockMeta?.path ?? "sibling"", "path": "mockMeta?.path ?? "sibling"",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
"component": "() => import("layer/pages/param/index.vue").then(m => m.default || m)", "component": "() => import("layer/pages/param/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
@ -69,14 +69,14 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/param/sibling.vue").then(m => m.default || m)", "component": "() => import("pages/param/sibling.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-sibling"", "name": "mockMeta?.name ?? "param-sibling"",
"path": "mockMeta?.path ?? "sibling"", "path": "mockMeta?.path ?? "sibling"",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
"component": "() => import("pages/param.vue").then(m => m.default || m)", "component": "() => import("pages/param.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/param"", "path": "mockMeta?.path ?? "/param"",
@ -87,7 +87,7 @@
"children": [ "children": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("layer/pages/wrapper-expose/other/index.vue").then(m => m.default || m)", "component": "() => import("layer/pages/wrapper-expose/other/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "wrapper-expose-other"", "name": "mockMeta?.name ?? "wrapper-expose-other"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
@ -95,14 +95,14 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/wrapper-expose/other/sibling.vue").then(m => m.default || m)", "component": "() => import("pages/wrapper-expose/other/sibling.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "wrapper-expose-other-sibling"", "name": "mockMeta?.name ?? "wrapper-expose-other-sibling"",
"path": "mockMeta?.path ?? "sibling"", "path": "mockMeta?.path ?? "sibling"",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
"component": "() => import("pages/wrapper-expose/other.vue").then(m => m.default || m)", "component": "() => import("pages/wrapper-expose/other.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/wrapper-expose/other"", "path": "mockMeta?.path ?? "/wrapper-expose/other"",
@ -112,7 +112,7 @@
"should extract serializable values and override fallback when normalized with `overrideMeta: true`": [ "should extract serializable values and override fallback when normalized with `overrideMeta: true`": [
{ {
"alias": "["sweet-home"].concat(mockMeta?.alias || [])", "alias": "["sweet-home"].concat(mockMeta?.alias || [])",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "home"", "name": "mockMeta?.name ?? "home"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -122,7 +122,7 @@
"should generate correct catch-all route": [ "should generate correct catch-all route": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[...slug].vue").then(m => m.default || m)", "component": "() => import("pages/[...slug].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "slug"", "name": "mockMeta?.name ?? "slug"",
"path": "mockMeta?.path ?? "/:slug(.*)*"", "path": "mockMeta?.path ?? "/:slug(.*)*"",
@ -130,7 +130,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -140,7 +140,7 @@
"should generate correct dynamic routes": [ "should generate correct dynamic routes": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -148,7 +148,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[slug].vue").then(m => m.default || m)", "component": "() => import("pages/[slug].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "slug"", "name": "mockMeta?.name ?? "slug"",
"path": "mockMeta?.path ?? "/:slug()"", "path": "mockMeta?.path ?? "/:slug()"",
@ -159,14 +159,14 @@
"children": [ "children": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[[foo]]/index.vue").then(m => m.default || m)", "component": "() => import("pages/[[foo]]/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"", "name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
"component": "() => import("pages/[[foo]]").then(m => m.default || m)", "component": "() => import("pages/[[foo]]")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/:foo?"", "path": "mockMeta?.path ?? "/:foo?"",
@ -174,7 +174,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/optional/[[opt]].vue").then(m => m.default || m)", "component": "() => import("pages/optional/[[opt]].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-opt"", "name": "mockMeta?.name ?? "optional-opt"",
"path": "mockMeta?.path ?? "/optional/:opt?"", "path": "mockMeta?.path ?? "/optional/:opt?"",
@ -182,7 +182,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/optional/prefix-[[opt]].vue").then(m => m.default || m)", "component": "() => import("pages/optional/prefix-[[opt]].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-prefix-opt"", "name": "mockMeta?.name ?? "optional-prefix-opt"",
"path": "mockMeta?.path ?? "/optional/prefix-:opt?"", "path": "mockMeta?.path ?? "/optional/prefix-:opt?"",
@ -190,7 +190,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/optional/[[opt]]-postfix.vue").then(m => m.default || m)", "component": "() => import("pages/optional/[[opt]]-postfix.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-opt-postfix"", "name": "mockMeta?.name ?? "optional-opt-postfix"",
"path": "mockMeta?.path ?? "/optional/:opt?-postfix"", "path": "mockMeta?.path ?? "/optional/:opt?-postfix"",
@ -198,7 +198,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue").then(m => m.default || m)", "component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-prefix-opt-postfix"", "name": "mockMeta?.name ?? "optional-prefix-opt-postfix"",
"path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"", "path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"",
@ -206,7 +206,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[bar]/index.vue").then(m => m.default || m)", "component": "() => import("pages/[bar]/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "bar"", "name": "mockMeta?.name ?? "bar"",
"path": "mockMeta?.path ?? "/:bar()"", "path": "mockMeta?.path ?? "/:bar()"",
@ -214,7 +214,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/nonopt/[slug].vue").then(m => m.default || m)", "component": "() => import("pages/nonopt/[slug].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "nonopt-slug"", "name": "mockMeta?.name ?? "nonopt-slug"",
"path": "mockMeta?.path ?? "/nonopt/:slug()"", "path": "mockMeta?.path ?? "/nonopt/:slug()"",
@ -222,7 +222,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/opt/[[slug]].vue").then(m => m.default || m)", "component": "() => import("pages/opt/[[slug]].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "opt-slug"", "name": "mockMeta?.name ?? "opt-slug"",
"path": "mockMeta?.path ?? "/opt/:slug?"", "path": "mockMeta?.path ?? "/opt/:slug?"",
@ -230,7 +230,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[[sub]]/route-[slug].vue").then(m => m.default || m)", "component": "() => import("pages/[[sub]]/route-[slug].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "sub-route-slug"", "name": "mockMeta?.name ?? "sub-route-slug"",
"path": "mockMeta?.path ?? "/:sub?/route-:slug()"", "path": "mockMeta?.path ?? "/:sub?/route-:slug()"",
@ -240,7 +240,7 @@
"should generate correct id for catchall (order 1)": [ "should generate correct id for catchall (order 1)": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)", "component": "() => import("pages/[...stories].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories"", "name": "mockMeta?.name ?? "stories"",
"path": "mockMeta?.path ?? "/:stories(.*)*"", "path": "mockMeta?.path ?? "/:stories(.*)*"",
@ -248,7 +248,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)", "component": "() => import("pages/stories/[id].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories-id"", "name": "mockMeta?.name ?? "stories-id"",
"path": "mockMeta?.path ?? "/stories/:id()"", "path": "mockMeta?.path ?? "/stories/:id()"",
@ -258,7 +258,7 @@
"should generate correct id for catchall (order 2)": [ "should generate correct id for catchall (order 2)": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)", "component": "() => import("pages/stories/[id].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories-id"", "name": "mockMeta?.name ?? "stories-id"",
"path": "mockMeta?.path ?? "/stories/:id()"", "path": "mockMeta?.path ?? "/stories/:id()"",
@ -266,7 +266,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)", "component": "() => import("pages/[...stories].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories"", "name": "mockMeta?.name ?? "stories"",
"path": "mockMeta?.path ?? "/:stories(.*)*"", "path": "mockMeta?.path ?? "/:stories(.*)*"",
@ -276,7 +276,7 @@
"should generate correct route for kebab-case file": [ "should generate correct route for kebab-case file": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/kebab-case.vue").then(m => m.default || m)", "component": "() => import("pages/kebab-case.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "kebab-case"", "name": "mockMeta?.name ?? "kebab-case"",
"path": "mockMeta?.path ?? "/kebab-case"", "path": "mockMeta?.path ?? "/kebab-case"",
@ -286,7 +286,7 @@
"should generate correct route for snake_case file": [ "should generate correct route for snake_case file": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/snake_case.vue").then(m => m.default || m)", "component": "() => import("pages/snake_case.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "snake_case"", "name": "mockMeta?.name ?? "snake_case"",
"path": "mockMeta?.path ?? "/snake_case"", "path": "mockMeta?.path ?? "/snake_case"",
@ -296,7 +296,7 @@
"should generate correct routes for index pages": [ "should generate correct routes for index pages": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -304,7 +304,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/parent/index.vue").then(m => m.default || m)", "component": "() => import("pages/parent/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent"", "name": "mockMeta?.name ?? "parent"",
"path": "mockMeta?.path ?? "/parent"", "path": "mockMeta?.path ?? "/parent"",
@ -312,7 +312,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/parent/child/index.vue").then(m => m.default || m)", "component": "() => import("pages/parent/child/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent/child"", "path": "mockMeta?.path ?? "/parent/child"",
@ -325,14 +325,14 @@
"children": [ "children": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/parent/child.vue").then(m => m.default || m)", "component": "() => import("pages/parent/child.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "child"", "path": "mockMeta?.path ?? "child"",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
"component": "() => import("pages/parent.vue").then(m => m.default || m)", "component": "() => import("pages/parent.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent"", "name": "mockMeta?.name ?? "parent"",
"path": "mockMeta?.path ?? "/parent"", "path": "mockMeta?.path ?? "/parent"",
@ -342,7 +342,7 @@
"should handle route groups": [ "should handle route groups": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)", "component": "() => import("pages/(foo)/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -353,14 +353,14 @@
"children": [ "children": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)", "component": "() => import("pages/(bar)/about/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "about"", "name": "mockMeta?.name ?? "about"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)", "component": "() => import("pages/(foo)/about.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/about"", "path": "mockMeta?.path ?? "/about"",
@ -373,14 +373,14 @@
"children": [ "children": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/index/index/all.vue").then(m => m.default || m)", "component": "() => import("pages/index/index/all.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index-index-all"", "name": "mockMeta?.name ?? "index-index-all"",
"path": "mockMeta?.path ?? "all"", "path": "mockMeta?.path ?? "all"",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
"component": "() => import("pages/index/index.vue").then(m => m.default || m)", "component": "() => import("pages/index/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -390,7 +390,7 @@
"should merge route.meta with meta from file": [ "should merge route.meta with meta from file": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)", "component": "() => import("pages/page-with-meta.vue")",
"meta": "{ ...(mockMeta || {}), ...{"test":1} }", "meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"", "name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"", "path": "mockMeta?.path ?? "/page-with-meta"",
@ -400,7 +400,7 @@
"should not generate colliding route names when hyphens are in file name": [ "should not generate colliding route names when hyphens are in file name": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/parent/[child].vue").then(m => m.default || m)", "component": "() => import("pages/parent/[child].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent/:child()"", "path": "mockMeta?.path ?? "/parent/:child()"",
@ -408,7 +408,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/parent-[child].vue").then(m => m.default || m)", "component": "() => import("pages/parent-[child].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent-:child()"", "path": "mockMeta?.path ?? "/parent-:child()"",
@ -418,7 +418,7 @@
"should not merge required param as a child of optional param": [ "should not merge required param as a child of optional param": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[[foo]].vue").then(m => m.default || m)", "component": "() => import("pages/[[foo]].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"", "name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? "/:foo?"", "path": "mockMeta?.path ?? "/:foo?"",
@ -426,7 +426,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[foo].vue").then(m => m.default || m)", "component": "() => import("pages/[foo].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"", "name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? "/:foo()"", "path": "mockMeta?.path ?? "/:foo()"",
@ -436,7 +436,7 @@
"should only allow "_" & "." as special character for dynamic route": [ "should only allow "_" & "." as special character for dynamic route": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[a1_1a].vue").then(m => m.default || m)", "component": "() => import("pages/[a1_1a].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "a1_1a"", "name": "mockMeta?.name ?? "a1_1a"",
"path": "mockMeta?.path ?? "/:a1_1a()"", "path": "mockMeta?.path ?? "/:a1_1a()"",
@ -444,7 +444,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[b2.2b].vue").then(m => m.default || m)", "component": "() => import("pages/[b2.2b].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "b2.2b"", "name": "mockMeta?.name ?? "b2.2b"",
"path": "mockMeta?.path ?? "/:b2.2b()"", "path": "mockMeta?.path ?? "/:b2.2b()"",
@ -452,7 +452,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[b2]_[2b].vue").then(m => m.default || m)", "component": "() => import("pages/[b2]_[2b].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "b2_2b"", "name": "mockMeta?.name ?? "b2_2b"",
"path": "mockMeta?.path ?? "/:b2()_:2b()"", "path": "mockMeta?.path ?? "/:b2()_:2b()"",
@ -460,7 +460,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[[c3@3c]].vue").then(m => m.default || m)", "component": "() => import("pages/[[c3@3c]].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "c33c"", "name": "mockMeta?.name ?? "c33c"",
"path": "mockMeta?.path ?? "/:c33c?"", "path": "mockMeta?.path ?? "/:c33c?"",
@ -468,7 +468,7 @@
}, },
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/[[d4-4d]].vue").then(m => m.default || m)", "component": "() => import("pages/[[d4-4d]].vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "d44d"", "name": "mockMeta?.name ?? "d44d"",
"path": "mockMeta?.path ?? "/:d44d?"", "path": "mockMeta?.path ?? "/:d44d?"",
@ -478,7 +478,7 @@
"should properly override route name if definePageMeta name override is defined.": [ "should properly override route name if definePageMeta name override is defined.": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "home"", "name": "mockMeta?.name ?? "home"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
@ -488,7 +488,7 @@
"should use fallbacks when normalized with `overrideMeta: true`": [ "should use fallbacks when normalized with `overrideMeta: true`": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",

View File

@ -2,7 +2,7 @@
"pushed route, skips generation from file": [ "pushed route, skips generation from file": [
{ {
"alias": "["pushed-route-alias"]", "alias": "["pushed-route-alias"]",
"component": "() => import("pages/route-file.vue").then(m => m.default || m)", "component": "() => import("pages/route-file.vue")",
"meta": "{"someMetaData":true}", "meta": "{"someMetaData":true}",
"name": ""pushed-route"", "name": ""pushed-route"",
"path": ""/"", "path": ""/"",
@ -18,7 +18,7 @@
], ],
"route.meta generated from file": [ "route.meta generated from file": [
{ {
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)", "component": "() => import("pages/page-with-meta.vue")",
"meta": "{"test":1}", "meta": "{"test":1}",
"name": ""page-with-meta"", "name": ""page-with-meta"",
"path": ""/page-with-meta"", "path": ""/page-with-meta"",
@ -26,7 +26,7 @@
], ],
"should allow pages with `:` in their path": [ "should allow pages with `:` in their path": [
{ {
"component": "() => import("pages/test:name.vue").then(m => m.default || m)", "component": "() => import("pages/test:name.vue")",
"name": ""test:name"", "name": ""test:name"",
"path": ""/test\\:name"", "path": ""/test\\:name"",
}, },
@ -37,44 +37,44 @@
{ {
"children": [ "children": [
{ {
"component": "() => import("pages/param/index/index.vue").then(m => m.default || m)", "component": "() => import("pages/param/index/index.vue")",
"name": ""param-index"", "name": ""param-index"",
"path": """", "path": """",
}, },
{ {
"component": "() => import("layer/pages/param/index/sibling.vue").then(m => m.default || m)", "component": "() => import("layer/pages/param/index/sibling.vue")",
"name": ""param-index-sibling"", "name": ""param-index-sibling"",
"path": ""sibling"", "path": ""sibling"",
}, },
], ],
"component": "() => import("layer/pages/param/index.vue").then(m => m.default || m)", "component": "() => import("layer/pages/param/index.vue")",
"name": "mockMeta?.name", "name": "mockMeta?.name",
"path": """", "path": """",
}, },
{ {
"component": "() => import("pages/param/sibling.vue").then(m => m.default || m)", "component": "() => import("pages/param/sibling.vue")",
"name": ""param-sibling"", "name": ""param-sibling"",
"path": ""sibling"", "path": ""sibling"",
}, },
], ],
"component": "() => import("pages/param.vue").then(m => m.default || m)", "component": "() => import("pages/param.vue")",
"name": "mockMeta?.name", "name": "mockMeta?.name",
"path": ""/param"", "path": ""/param"",
}, },
{ {
"children": [ "children": [
{ {
"component": "() => import("layer/pages/wrapper-expose/other/index.vue").then(m => m.default || m)", "component": "() => import("layer/pages/wrapper-expose/other/index.vue")",
"name": ""wrapper-expose-other"", "name": ""wrapper-expose-other"",
"path": """", "path": """",
}, },
{ {
"component": "() => import("pages/wrapper-expose/other/sibling.vue").then(m => m.default || m)", "component": "() => import("pages/wrapper-expose/other/sibling.vue")",
"name": ""wrapper-expose-other-sibling"", "name": ""wrapper-expose-other-sibling"",
"path": ""sibling"", "path": ""sibling"",
}, },
], ],
"component": "() => import("pages/wrapper-expose/other.vue").then(m => m.default || m)", "component": "() => import("pages/wrapper-expose/other.vue")",
"name": "mockMeta?.name", "name": "mockMeta?.name",
"path": ""/wrapper-expose/other"", "path": ""/wrapper-expose/other"",
}, },
@ -82,7 +82,7 @@
"should extract serializable values and override fallback when normalized with `overrideMeta: true`": [ "should extract serializable values and override fallback when normalized with `overrideMeta: true`": [
{ {
"alias": "["sweet-home"]", "alias": "["sweet-home"]",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": ""home"", "name": ""home"",
"path": ""/"", "path": ""/"",
@ -91,131 +91,131 @@
], ],
"should generate correct catch-all route": [ "should generate correct catch-all route": [
{ {
"component": "() => import("pages/[...slug].vue").then(m => m.default || m)", "component": "() => import("pages/[...slug].vue")",
"name": ""slug"", "name": ""slug"",
"path": ""/:slug(.*)*"", "path": ""/:slug(.*)*"",
}, },
{ {
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"name": ""index"", "name": ""index"",
"path": ""/"", "path": ""/"",
}, },
], ],
"should generate correct dynamic routes": [ "should generate correct dynamic routes": [
{ {
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"name": ""index"", "name": ""index"",
"path": ""/"", "path": ""/"",
}, },
{ {
"component": "() => import("pages/[slug].vue").then(m => m.default || m)", "component": "() => import("pages/[slug].vue")",
"name": ""slug"", "name": ""slug"",
"path": ""/:slug()"", "path": ""/:slug()"",
}, },
{ {
"children": [ "children": [
{ {
"component": "() => import("pages/[[foo]]/index.vue").then(m => m.default || m)", "component": "() => import("pages/[[foo]]/index.vue")",
"name": ""foo"", "name": ""foo"",
"path": """", "path": """",
}, },
], ],
"component": "() => import("pages/[[foo]]").then(m => m.default || m)", "component": "() => import("pages/[[foo]]")",
"name": "mockMeta?.name", "name": "mockMeta?.name",
"path": ""/:foo?"", "path": ""/:foo?"",
}, },
{ {
"component": "() => import("pages/optional/[[opt]].vue").then(m => m.default || m)", "component": "() => import("pages/optional/[[opt]].vue")",
"name": ""optional-opt"", "name": ""optional-opt"",
"path": ""/optional/:opt?"", "path": ""/optional/:opt?"",
}, },
{ {
"component": "() => import("pages/optional/prefix-[[opt]].vue").then(m => m.default || m)", "component": "() => import("pages/optional/prefix-[[opt]].vue")",
"name": ""optional-prefix-opt"", "name": ""optional-prefix-opt"",
"path": ""/optional/prefix-:opt?"", "path": ""/optional/prefix-:opt?"",
}, },
{ {
"component": "() => import("pages/optional/[[opt]]-postfix.vue").then(m => m.default || m)", "component": "() => import("pages/optional/[[opt]]-postfix.vue")",
"name": ""optional-opt-postfix"", "name": ""optional-opt-postfix"",
"path": ""/optional/:opt?-postfix"", "path": ""/optional/:opt?-postfix"",
}, },
{ {
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue").then(m => m.default || m)", "component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue")",
"name": ""optional-prefix-opt-postfix"", "name": ""optional-prefix-opt-postfix"",
"path": ""/optional/prefix-:opt?-postfix"", "path": ""/optional/prefix-:opt?-postfix"",
}, },
{ {
"component": "() => import("pages/[bar]/index.vue").then(m => m.default || m)", "component": "() => import("pages/[bar]/index.vue")",
"name": ""bar"", "name": ""bar"",
"path": ""/:bar()"", "path": ""/:bar()"",
}, },
{ {
"component": "() => import("pages/nonopt/[slug].vue").then(m => m.default || m)", "component": "() => import("pages/nonopt/[slug].vue")",
"name": ""nonopt-slug"", "name": ""nonopt-slug"",
"path": ""/nonopt/:slug()"", "path": ""/nonopt/:slug()"",
}, },
{ {
"component": "() => import("pages/opt/[[slug]].vue").then(m => m.default || m)", "component": "() => import("pages/opt/[[slug]].vue")",
"name": ""opt-slug"", "name": ""opt-slug"",
"path": ""/opt/:slug?"", "path": ""/opt/:slug?"",
}, },
{ {
"component": "() => import("pages/[[sub]]/route-[slug].vue").then(m => m.default || m)", "component": "() => import("pages/[[sub]]/route-[slug].vue")",
"name": ""sub-route-slug"", "name": ""sub-route-slug"",
"path": ""/:sub?/route-:slug()"", "path": ""/:sub?/route-:slug()"",
}, },
], ],
"should generate correct id for catchall (order 1)": [ "should generate correct id for catchall (order 1)": [
{ {
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)", "component": "() => import("pages/[...stories].vue")",
"name": ""stories"", "name": ""stories"",
"path": ""/:stories(.*)*"", "path": ""/:stories(.*)*"",
}, },
{ {
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)", "component": "() => import("pages/stories/[id].vue")",
"name": ""stories-id"", "name": ""stories-id"",
"path": ""/stories/:id()"", "path": ""/stories/:id()"",
}, },
], ],
"should generate correct id for catchall (order 2)": [ "should generate correct id for catchall (order 2)": [
{ {
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)", "component": "() => import("pages/stories/[id].vue")",
"name": ""stories-id"", "name": ""stories-id"",
"path": ""/stories/:id()"", "path": ""/stories/:id()"",
}, },
{ {
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)", "component": "() => import("pages/[...stories].vue")",
"name": ""stories"", "name": ""stories"",
"path": ""/:stories(.*)*"", "path": ""/:stories(.*)*"",
}, },
], ],
"should generate correct route for kebab-case file": [ "should generate correct route for kebab-case file": [
{ {
"component": "() => import("pages/kebab-case.vue").then(m => m.default || m)", "component": "() => import("pages/kebab-case.vue")",
"name": ""kebab-case"", "name": ""kebab-case"",
"path": ""/kebab-case"", "path": ""/kebab-case"",
}, },
], ],
"should generate correct route for snake_case file": [ "should generate correct route for snake_case file": [
{ {
"component": "() => import("pages/snake_case.vue").then(m => m.default || m)", "component": "() => import("pages/snake_case.vue")",
"name": ""snake_case"", "name": ""snake_case"",
"path": ""/snake_case"", "path": ""/snake_case"",
}, },
], ],
"should generate correct routes for index pages": [ "should generate correct routes for index pages": [
{ {
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"name": ""index"", "name": ""index"",
"path": ""/"", "path": ""/"",
}, },
{ {
"component": "() => import("pages/parent/index.vue").then(m => m.default || m)", "component": "() => import("pages/parent/index.vue")",
"name": ""parent"", "name": ""parent"",
"path": ""/parent"", "path": ""/parent"",
}, },
{ {
"component": "() => import("pages/parent/child/index.vue").then(m => m.default || m)", "component": "() => import("pages/parent/child/index.vue")",
"name": ""parent-child"", "name": ""parent-child"",
"path": ""/parent/child"", "path": ""/parent/child"",
}, },
@ -224,31 +224,31 @@
{ {
"children": [ "children": [
{ {
"component": "() => import("pages/parent/child.vue").then(m => m.default || m)", "component": "() => import("pages/parent/child.vue")",
"name": ""parent-child"", "name": ""parent-child"",
"path": ""child"", "path": ""child"",
}, },
], ],
"component": "() => import("pages/parent.vue").then(m => m.default || m)", "component": "() => import("pages/parent.vue")",
"name": ""parent"", "name": ""parent"",
"path": ""/parent"", "path": ""/parent"",
}, },
], ],
"should handle route groups": [ "should handle route groups": [
{ {
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)", "component": "() => import("pages/(foo)/index.vue")",
"name": ""index"", "name": ""index"",
"path": ""/"", "path": ""/"",
}, },
{ {
"children": [ "children": [
{ {
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)", "component": "() => import("pages/(bar)/about/index.vue")",
"name": ""about"", "name": ""about"",
"path": """", "path": """",
}, },
], ],
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)", "component": "() => import("pages/(foo)/about.vue")",
"name": "mockMeta?.name", "name": "mockMeta?.name",
"path": ""/about"", "path": ""/about"",
}, },
@ -257,19 +257,19 @@
{ {
"children": [ "children": [
{ {
"component": "() => import("pages/index/index/all.vue").then(m => m.default || m)", "component": "() => import("pages/index/index/all.vue")",
"name": ""index-index-all"", "name": ""index-index-all"",
"path": ""all"", "path": ""all"",
}, },
], ],
"component": "() => import("pages/index/index.vue").then(m => m.default || m)", "component": "() => import("pages/index/index.vue")",
"name": ""index"", "name": ""index"",
"path": ""/"", "path": ""/"",
}, },
], ],
"should merge route.meta with meta from file": [ "should merge route.meta with meta from file": [
{ {
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)", "component": "() => import("pages/page-with-meta.vue")",
"meta": "{ ...(mockMeta || {}), ...{"test":1} }", "meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": ""page-with-meta"", "name": ""page-with-meta"",
"path": ""/page-with-meta"", "path": ""/page-with-meta"",
@ -277,58 +277,58 @@
], ],
"should not generate colliding route names when hyphens are in file name": [ "should not generate colliding route names when hyphens are in file name": [
{ {
"component": "() => import("pages/parent/[child].vue").then(m => m.default || m)", "component": "() => import("pages/parent/[child].vue")",
"name": ""parent-child"", "name": ""parent-child"",
"path": ""/parent/:child()"", "path": ""/parent/:child()"",
}, },
{ {
"component": "() => import("pages/parent-[child].vue").then(m => m.default || m)", "component": "() => import("pages/parent-[child].vue")",
"name": ""parent-child"", "name": ""parent-child"",
"path": ""/parent-:child()"", "path": ""/parent-:child()"",
}, },
], ],
"should not merge required param as a child of optional param": [ "should not merge required param as a child of optional param": [
{ {
"component": "() => import("pages/[[foo]].vue").then(m => m.default || m)", "component": "() => import("pages/[[foo]].vue")",
"name": ""foo"", "name": ""foo"",
"path": ""/:foo?"", "path": ""/:foo?"",
}, },
{ {
"component": "() => import("pages/[foo].vue").then(m => m.default || m)", "component": "() => import("pages/[foo].vue")",
"name": ""foo"", "name": ""foo"",
"path": ""/:foo()"", "path": ""/:foo()"",
}, },
], ],
"should only allow "_" & "." as special character for dynamic route": [ "should only allow "_" & "." as special character for dynamic route": [
{ {
"component": "() => import("pages/[a1_1a].vue").then(m => m.default || m)", "component": "() => import("pages/[a1_1a].vue")",
"name": ""a1_1a"", "name": ""a1_1a"",
"path": ""/:a1_1a()"", "path": ""/:a1_1a()"",
}, },
{ {
"component": "() => import("pages/[b2.2b].vue").then(m => m.default || m)", "component": "() => import("pages/[b2.2b].vue")",
"name": ""b2.2b"", "name": ""b2.2b"",
"path": ""/:b2.2b()"", "path": ""/:b2.2b()"",
}, },
{ {
"component": "() => import("pages/[b2]_[2b].vue").then(m => m.default || m)", "component": "() => import("pages/[b2]_[2b].vue")",
"name": ""b2_2b"", "name": ""b2_2b"",
"path": ""/:b2()_:2b()"", "path": ""/:b2()_:2b()"",
}, },
{ {
"component": "() => import("pages/[[c3@3c]].vue").then(m => m.default || m)", "component": "() => import("pages/[[c3@3c]].vue")",
"name": ""c33c"", "name": ""c33c"",
"path": ""/:c33c?"", "path": ""/:c33c?"",
}, },
{ {
"component": "() => import("pages/[[d4-4d]].vue").then(m => m.default || m)", "component": "() => import("pages/[[d4-4d]].vue")",
"name": ""d44d"", "name": ""d44d"",
"path": ""/:d44d?"", "path": ""/:d44d?"",
}, },
], ],
"should properly override route name if definePageMeta name override is defined.": [ "should properly override route name if definePageMeta name override is defined.": [
{ {
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"name": ""home"", "name": ""home"",
"path": ""/"", "path": ""/"",
}, },
@ -336,7 +336,7 @@
"should use fallbacks when normalized with `overrideMeta: true`": [ "should use fallbacks when normalized with `overrideMeta: true`": [
{ {
"alias": "mockMeta?.alias || []", "alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)", "component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": ""/"", "path": ""/"",

View File

@ -18,10 +18,11 @@ describe('imports:transform', () => {
] ]
const ctx = createUnimport({ const ctx = createUnimport({
injectAtEnd: true,
imports, imports,
}) })
const transformPlugin = TransformPlugin.raw({ ctx, options: { transform: { exclude: [/node_modules/] } } }, { framework: 'rollup' }) as Plugin const transformPlugin = TransformPlugin({ ctx, options: { transform: { exclude: [/node_modules/] } } }).raw({}, { framework: 'rollup' }) as Plugin
const transform = async (source: string) => { const transform = async (source: string) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const result = await (transformPlugin.transform! as Function).call({ error: null, warn: null } as any, source, '') const result = await (transformPlugin.transform! as Function).call({ error: null, warn: null } as any, source, '')
@ -171,7 +172,6 @@ const excludedVueHelpers = [
'hydrate', 'hydrate',
'initDirectivesForSSR', 'initDirectivesForSSR',
'render', 'render',
'useCssVars',
'vModelCheckbox', 'vModelCheckbox',
'vModelDynamic', 'vModelDynamic',
'vModelRadio', 'vModelRadio',
@ -183,6 +183,13 @@ const excludedVueHelpers = [
'ErrorCodes', 'ErrorCodes',
'TrackOpTypes', 'TrackOpTypes',
'TriggerOpTypes', 'TriggerOpTypes',
'useHost',
'hydrateOnVisible',
'hydrateOnMediaQuery',
'hydrateOnInteraction',
'hydrateOnIdle',
'onWatcherCleanup',
'getCurrentWatcher',
] ]
describe('imports:vue', () => { describe('imports:vue', () => {

View File

@ -15,7 +15,8 @@ describe('components:transform', () => {
const code = await transform('import { Foo, Bar } from \'#components\'', '/app.vue') const code = await transform('import { Foo, Bar } from \'#components\'', '/app.vue')
expect(code).toMatchInlineSnapshot(` expect(code).toMatchInlineSnapshot(`
"import Foo from '/Foo.vue'; "
import Foo from '/Foo.vue';
import { Bar } from '/Bar.vue'; import { Bar } from '/Bar.vue';
" "
`) `)
@ -28,7 +29,8 @@ describe('components:transform', () => {
const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue') const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
expect(code).toMatchInlineSnapshot(` expect(code).toMatchInlineSnapshot(`
"import Foo from '/Foo.vue?nuxt_component=server&nuxt_component_name=Foo&nuxt_component_export=default'; "
import Foo from '/Foo.vue?nuxt_component=server&nuxt_component_name=Foo&nuxt_component_export=default';
import LazyFoo from '/Foo.vue?nuxt_component=server,async&nuxt_component_name=Foo&nuxt_component_export=default'; import LazyFoo from '/Foo.vue?nuxt_component=server,async&nuxt_component_name=Foo&nuxt_component_export=default';
" "
`) `)
@ -54,7 +56,8 @@ describe('components:transform', () => {
const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue') const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
expect(code).toMatchInlineSnapshot(` expect(code).toMatchInlineSnapshot(`
"import Foo from '/Foo.vue?nuxt_component=client&nuxt_component_name=Foo&nuxt_component_export=default'; "
import Foo from '/Foo.vue?nuxt_component=client&nuxt_component_name=Foo&nuxt_component_export=default';
import LazyFoo from '/Foo.vue?nuxt_component=client,async&nuxt_component_name=Foo&nuxt_component_export=default'; import LazyFoo from '/Foo.vue?nuxt_component=client,async&nuxt_component_name=Foo&nuxt_component_export=default';
" "
`) `)

View File

@ -1,7 +1,7 @@
import { fileURLToPath } from 'node:url'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { ImportProtectionPlugin, nuxtImportProtections } from '../src/core/plugins/import-protection' import { ImpoundPlugin } from 'impound'
import { nuxtImportProtections } from '../src/core/plugins/import-protection'
import type { NuxtOptions } from '../schema' import type { NuxtOptions } from '../schema'
const testsToTriggerOn = [ const testsToTriggerOn = [
@ -39,9 +39,8 @@ describe('import protection', () => {
}) })
const transformWithImportProtection = (id: string, importer: string) => { const transformWithImportProtection = (id: string, importer: string) => {
const plugin = ImportProtectionPlugin.rollup({ const plugin = ImpoundPlugin.rollup({
rootDir: '/root', cwd: '/root',
modulesDir: [fileURLToPath(new URL('..', import.meta.url))],
patterns: nuxtImportProtections({ patterns: nuxtImportProtections({
options: { options: {
modules: ['some-nuxt-module'], modules: ['some-nuxt-module'],
@ -51,5 +50,5 @@ const transformWithImportProtection = (id: string, importer: string) => {
}), }),
}) })
return (plugin as any).resolveId(id, importer) return (plugin as any).resolveId.call({ error: () => {} }, id, importer)
} }

View File

@ -58,6 +58,41 @@ describe('page metadata', () => {
`) `)
}) })
it('should extract serialisable metadata from files with multiple blocks', async () => {
const meta = await getRouteMeta(`
<script lang="ts">
export default {
name: 'thing'
}
</script>
<script setup>
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
validate: () => true,
middleware: [
function () {},
],
otherValue: {
foo: 'bar',
},
})
</script>
`, filePath)
expect(meta).toMatchInlineSnapshot(`
{
"meta": {
"__nuxt_dynamic_meta_key": Set {
"meta",
},
},
"name": "some-custom-name",
"path": "/some-custom-path",
}
`)
})
it('should extract serialisable metadata in options api', async () => { it('should extract serialisable metadata in options api', async () => {
const meta = await getRouteMeta(` const meta = await getRouteMeta(`
<script> <script>
@ -145,7 +180,7 @@ describe('normalizeRoutes', () => {
path: indexN6pT4Un8hYMeta?.path ?? "/", path: indexN6pT4Un8hYMeta?.path ?? "/",
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} }, meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
redirect: "/", redirect: "/",
component: () => import("/app/pages/index.vue").then(m => m.default || m) component: () => import("/app/pages/index.vue")
} }
]", ]",
} }
@ -171,7 +206,7 @@ describe('normalizeRoutes', () => {
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} }, meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
alias: indexN6pT4Un8hYMeta?.alias || [], alias: indexN6pT4Un8hYMeta?.alias || [],
redirect: indexN6pT4Un8hYMeta?.redirect, redirect: indexN6pT4Un8hYMeta?.redirect,
component: () => import("/app/pages/index.vue").then(m => m.default || m) component: () => import("/app/pages/index.vue")
} }
]", ]",
} }

View File

@ -40,7 +40,6 @@ describe('plugin-metadata', () => {
it('should overwrite invalid plugins', () => { it('should overwrite invalid plugins', () => {
const invalidPlugins = [ const invalidPlugins = [
'export const plugin = {}', 'export const plugin = {}',
'export default function (ctx, inject) {}',
] ]
for (const plugin of invalidPlugins) { for (const plugin of invalidPlugins) {
expect(transformPlugin.transform.call({ parse }, plugin, 'my-plugin.mjs').code).toBe('export default () => {}') expect(transformPlugin.transform.call({ parse }, plugin, 'my-plugin.mjs').code).toBe('export default () => {}')

View File

@ -241,6 +241,8 @@ it('components:scanComponents', async () => {
for (const c of scannedComponents) { for (const c of scannedComponents) {
// @ts-expect-error filePath is not optional but we don't want it to be in the snapshot // @ts-expect-error filePath is not optional but we don't want it to be in the snapshot
delete c.filePath delete c.filePath
// @ts-expect-error _scanned is added internally but we don't want it to be in the snapshot
delete c._scanned
} }
expect(scannedComponents).deep.eq(expectedComponents) expect(scannedComponents).deep.eq(expectedComponents)
}) })

Some files were not shown because too many files have changed in this diff Show More