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 && \
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:
required: true
- type: textarea
id: additonal
id: additional
attributes:
label: Additional context
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:
- closed
permissions: {}
jobs:
cleanup:
runs-on: ubuntu-latest
@ -20,14 +22,14 @@ jobs:
gh extension install actions/gh-actions-cache
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.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
gh actions-cache delete "$cacheKey" -R "$REPO" -B "$BRANCH" --confirm
done
echo "Done"
env:

View File

@ -6,9 +6,7 @@ on:
- main
- 3.x
permissions:
pull-requests: write
contents: write
permissions: {}
concurrency:
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.')
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
name: Docs
name: docs
on:
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:
pull_request_target:
@ -8,6 +8,8 @@ on:
- main
- 3.x
permissions: {}
jobs:
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
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files
run: |
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/590d3bd9dde0c91f7a66071d40eb84716526e5a6/scripts/download-actionlint.bash) 1.6.25
./actionlint -color -shellcheck=""
uses: docker://rhysd/actionlint:1.7.1@sha256:435ecdb63b1169e80ca3e136290072548c07fc4d76a044cf5541021712f8f344
with:
args: -color

View File

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

View File

@ -39,7 +39,7 @@ jobs:
GH_REPO: ${{ github.repository }}
COMMENT_AT: ${{ github.event.comment.created_at }}
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)"
updated_at="$(echo "$pr" | jq -r .updated_at)"
@ -47,7 +47,7 @@ jobs:
exit 1
fi
echo "head_sha=$head_sha" >> $GITHUB_OUTPUT
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
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:
issues:
types: [labeled]

View File

@ -2,7 +2,7 @@
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
name: ossf
on:
# For Branch-Protection check. Only the default branch is supported. See
# 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
# format to the repository Actions tab.
- 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()
with:
name: SARIF file
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- 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()
with:
sarif_file: results.sarif

View File

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

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:
workflow_dispatch:
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({
postcss: {
plugins: {
'postcss-nested': {}
"postcss-custom-media": {}
'postcss-nested': {},
'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.
## 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.

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:
- 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.
- API routes support.
- 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 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',
path: '/',
component: () => import('~/pages/home.vue').then(r => r.default || r)
component: () => import('~/pages/home.vue')
}
],
} satisfies RouterConfig

View File

@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtPicture> component to handle automatic image
links:
- label: Source
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
---

View File

@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtImg> component to handle automatic image opti
links:
- label: Source
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
---

View File

@ -114,7 +114,7 @@ function useAsyncData<DataT, DataE>(
key: string,
handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
options?: AsyncDataOptions<DataT>
): Promise<AsyncData<DataT, DataE>
): Promise<AsyncData<DataT, DataE>>
type AsyncDataOptions<DataT> = {
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`.
::
::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"}
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
- `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
- `meta`: custom data attached to the record
- `name`: unique name for the route record

View File

@ -34,7 +34,7 @@ Hook | Arguments | Environment | Description
## 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
-------------------------|----------------------------|-------------

View File

@ -25,9 +25,9 @@ To contribute to Nuxt, you need to set up a local environment.
```bash [Terminal]
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]
pnpm install
pnpm install --frozen-lockfile
```
::note
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
------------------------------------|---------------------|--------------|------------|-------------------
[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)
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.

View File

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

View File

@ -1,16 +1,21 @@
// For pnpm typecheck:docs to generate correct types
import { addPluginTemplate } from 'nuxt/kit'
import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit'
export default defineNuxtConfig({
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
pages: process.env.DOCS_TYPECHECK === 'true',
modules: [
function () {
if (!process.env.DOCS_TYPECHECK) { return }
addPluginTemplate({
filename: 'plugins/my-plugin.mjs',
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/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"@types/node": "20.16.1",
"c12": "2.0.0-beta.1",
"@types/node": "20.16.5",
"c12": "2.0.0-beta.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "2.0.0-beta.3",
"magic-string": "^0.30.11",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxt": "workspace:*",
"rollup": "^4.21.0",
"typescript": "5.5.4",
"postcss": "8.4.45",
"rollup": "4.21.2",
"send": ">=0.19.0",
"typescript": "5.6.2",
"ufo": "1.5.4",
"unbuild": "3.0.0-rc.7",
"vite": "5.4.1",
"vue": "3.5.0-beta.1",
"@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"
"vite": "5.4.4",
"vue": "3.5.4"
},
"devDependencies": {
"@eslint/js": "9.9.0",
"@nuxt/eslint-config": "0.5.1",
"@eslint/js": "9.10.0",
"@nuxt/eslint-config": "0.5.7",
"@nuxt/kit": "workspace:*",
"@nuxt/test-utils": "3.14.1",
"@nuxt/test-utils": "3.14.2",
"@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3",
"@types/node": "20.16.1",
"@types/node": "20.16.5",
"@types/semver": "7.5.8",
"@unhead/schema": "1.9.16",
"@vitejs/plugin-vue": "5.1.2",
"@unhead/schema": "1.11.2",
"@unhead/vue": "1.11.2",
"@vitejs/plugin-vue": "5.1.3",
"@vitest/coverage-v8": "2.0.5",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20",
"case-police": "0.7.0",
"changelogen": "0.5.5",
"consola": "3.2.3",
"cssnano": "7.0.5",
"cssnano": "7.0.6",
"destr": "2.0.3",
"devalue": "5.0.0",
"eslint": "9.9.0",
"eslint": "9.10.0",
"eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "3.2.0",
"eslint-typegen": "0.3.1",
"execa": "9.3.1",
"globby": "14.0.2",
"eslint-plugin-perfectionist": "3.5.0",
"eslint-typegen": "0.3.2",
"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",
"markdownlint-cli": "0.41.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxi": "3.12.0",
"nuxi": "3.13.1",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1",
"ofetch": "1.3.4",
"pathe": "1.1.2",
"playwright-core": "1.46.1",
"playwright-core": "1.47.0",
"rimraf": "6.0.1",
"semver": "7.6.3",
"sherif": "1.0.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",
"vitest": "2.0.5",
"vitest-environment-nuxt": "1.0.1",
"vue": "3.4.38",
"vue-router": "4.4.3",
"vue-tsc": "2.0.29"
"vue": "3.5.4",
"vue-router": "4.4.4",
"vue-tsc": "2.1.6"
},
"packageManager": "pnpm@9.7.1",
"packageManager": "pnpm@9.10.0",
"engines": {
"node": "^16.10.0 || >=18.0.0"
},

View File

@ -27,7 +27,7 @@
},
"dependencies": {
"@nuxt/schema": "workspace:*",
"c12": "^2.0.0-beta.1",
"c12": "^2.0.0-beta.2",
"consola": "^3.2.3",
"defu": "^6.1.4",
"destr": "^2.0.3",
@ -39,12 +39,12 @@
"klona": "^2.0.6",
"mlly": "^1.7.1",
"pathe": "^1.1.2",
"pkg-types": "^1.1.3",
"pkg-types": "^1.2.0",
"scule": "^1.3.0",
"semver": "^7.6.3",
"ufo": "^1.5.4",
"unctx": "^2.3.1",
"unimport": "^3.10.0",
"unimport": "^3.11.1",
"untyped": "^1.4.2"
},
"devDependencies": {
@ -52,9 +52,9 @@
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"unbuild": "3.0.0-rc.7",
"vite": "5.4.1",
"vite": "5.4.4",
"vitest": "2.0.5",
"webpack": "5.93.0"
"webpack": "5.94.0"
},
"engines": {
"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.
*
* Requires Nuxt 2.13+
*/
export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt()
@ -23,8 +21,6 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial<E
/**
* Register a component by its name and filePath.
*
* Requires Nuxt 2.13+
*/
export async function addComponent (opts: AddComponentOptions) {
const nuxt = useNuxt()

View File

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

View File

@ -5,7 +5,7 @@ import { useNuxt } from './context'
import { logger } from './logger'
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 { filename, src } = addTemplate(template)
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 { applyDefaults } from 'untyped'
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
@ -6,6 +7,7 @@ import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
import { NuxtConfigSchema } from '@nuxt/schema'
import { globby } from 'globby'
import defu from 'defu'
import { join } from 'pathe'
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
// 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._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 processedLayers = new Set<string>()
for (const layer of layers) {

View File

@ -79,9 +79,9 @@ function _defineNuxtModule<
}
// 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) {
nuxt = tryUseNuxt() || this.nuxt /* invoked by nuxt 2 */
throw new TypeError('Cannot use module outside of Nuxt context')
}
// Avoid duplicate installs

View File

@ -320,11 +320,6 @@ export async function writeTypes (nuxt: Nuxt) {
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()
}

View File

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

View File

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

View File

@ -1,12 +1,14 @@
import type { defineAsyncComponent } from 'vue'
import { createVNode, defineComponent, onErrorCaptured } from 'vue'
import { injectHead } from '@unhead/vue'
import { createError } from '../composables/error'
// @ts-expect-error virtual file
import { islandComponents } from '#build/components.islands.mjs'
export default defineComponent({
name: 'IslandRenderer',
props: {
context: {
type: Object as () => { name: string, props?: Record<string, any> },
@ -14,6 +16,10 @@ export default defineComponent({
},
},
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>
if (!component) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { devRootDir } from '#build/nuxt.config.mjs'
export default (url: string) => defineComponent({
name: 'NuxtTestComponentWrapper',
inheritAttrs: false,
async setup (props, { attrs }) {
const query = parseQuery(new URL(url, 'http://localhost').search)
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 type { Ref, WatchSource } from 'vue'
import type { MultiWatchSources, Ref } from 'vue'
import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt'
import { toArray } from '../utils'
@ -34,7 +34,7 @@ export type KeysOf<T> = Array<
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]

View File

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

View File

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

View File

@ -53,7 +53,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
/** @since 3.0.0 */
export const isNuxtError = <DataT = unknown>(
error?: string | object,
error: unknown,
): error is NuxtError<DataT> => !!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error
/** @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
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) {
for (const key in obj) {
const val = newObj[key]
@ -18,7 +27,7 @@ function deepDelete (obj: any, newObj: any) {
delete (obj as any)[key]
}
if (val !== null && typeof val === 'object') {
if (isPojoOrArray(val)) {
deepDelete(obj[key], newObj[key])
}
}
@ -27,7 +36,7 @@ function deepDelete (obj: any, newObj: any) {
function deepAssign (obj: any, newObj: any) {
for (const key in newObj) {
const val = newObj[key]
if (val !== null && typeof val === 'object') {
if (isPojoOrArray(val)) {
const defaultVal = Array.isArray(val) ? [] : {}
obj[key] = obj[key] || defaultVal
deepAssign(obj[key], val)

View File

@ -2,6 +2,7 @@ import { createApp, createSSRApp, nextTick } from 'vue'
import type { App } from 'vue'
// This file must be imported first as we set globalThis.$fetch via this import
// @ts-expect-error virtual file
import '#build/fetch.mjs'
import { applyPlugins, createNuxtApp } from './nuxt'
@ -9,6 +10,7 @@ import type { CreateOptions } from './nuxt'
import { createError } from './composables/error'
// @ts-expect-error virtual file
import '#build/css'
// @ts-expect-error virtual file
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 { LoadingIndicator } from '../app/composables/loading-indicator'
import type { RouteAnnouncer } from '../app/composables/route-announcer'
import type { ViewTransition } from './plugins/view-transitions.client'
// @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'
@ -267,6 +266,7 @@ export function createNuxtApp (options: CreateOptions) {
get vue () { return nuxtApp.vueApp.version },
},
payload: shallowReactive({
...options.ssrContext?.payload || {},
data: shallowReactive({}),
state: reactive({}),
once: new Set<string>(),
@ -275,7 +275,7 @@ export function createNuxtApp (options: CreateOptions) {
static: {
data: {},
},
runWithContext (fn: any) {
runWithContext <T>(fn: () => T) {
if (nuxtApp._scope.active && !getCurrentScope()) {
return nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn))
}
@ -310,6 +310,20 @@ export function createNuxtApp (options: CreateOptions) {
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) {
const __NUXT__ = multiApp ? window.__NUXT__?.[nuxtApp._id] : window.__NUXT__
// 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.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) {
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 })
event.preventDefault()
})
}
window.useNuxtApp = window.useNuxtApp || useNuxtApp
// 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) => {
if (chunkErrors.has(error)) {
if (chunkErrors.has(error) || error.message.includes('Failed to fetch dynamically imported module')) {
reloadAppAtPath(to)
}
})

View File

@ -40,7 +40,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
}
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 logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)

View File

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

View File

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

View File

@ -56,15 +56,3 @@ export default defineNuxtPlugin((nuxtApp) => {
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
}
const SCRIPT_RE = /<script[^>]*>/g
const SCRIPT_RE = /<script[^>]*>/gi
const HAS_SLOT_OR_CLIENT_RE = /<slot[^>]*>|nuxt-client/
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
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 { 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 { distDir } from '../dirs'
@ -169,6 +169,10 @@ export default defineNuxtModule<ComponentsOptions>({
await nuxt.callHook('components:extend', newComponents)
// add server placeholder for .client components server side. issue: #7085
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')) {
newComponents.push({
...component,
@ -236,17 +240,17 @@ export default defineNuxtModule<ComponentsOptions>({
const selectiveClient = typeof nuxt.options.experimental.componentIslands === 'object' && nuxt.options.experimental.componentIslands.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) {
config.plugins.push(componentsChunkPlugin.vite({
getComponents,
buildDir: nuxt.options.buildDir,
}))
} 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) => {
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}` })
}, {} as Record<string, string>),
)}`)
@ -307,7 +311,7 @@ export default defineNuxtModule<ComponentsOptions>({
getComponents,
}))
} 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',
// by default, give priority to scanned components
priority: dir.priority ?? 1,
// @ts-expect-error untyped property
_scanned: true,
}
if (typeof dir.extendComponent === 'function') {

View File

@ -22,6 +22,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
},
],
virtualImports: ['#components'],
injectAtEnd: true,
})
function getComponentsImports (): Import[] {
@ -50,6 +51,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
return createUnplugin(() => ({
name: 'nuxt:components:imports',
enforce: 'post',
transformInclude (id) {
id = normalize(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 { checkForExternalConfigurationFiles } from './external-config-files'
import { cleanupCaches, getVueHash } from './cache'
export async function build (nuxt: 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')
if (!nuxt.options._prepare) {
await Promise.all([checkForExternalConfigurationFiles(), bundle(nuxt)])
if (nuxt.options._prepare) {
nuxt.hook('prepare:types', () => nuxt.close())
return
}
if (nuxt.options.dev) {
checkForExternalConfigurationFiles()
}
await bundle(nuxt)
await nuxt.callHook('build:done')
if (!nuxt.options.dev) {
await nuxt.callHook('close', nuxt)
}
} else {
nuxt.hook('prepare:types', () => nuxt.close())
}
}
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 { dynamicEventHandler } from 'h3'
import { isWindows } from 'std-env'
import { ImpoundPlugin } from 'impound'
import type { Nuxt, NuxtOptions } from 'nuxt/schema'
import { version as nuxtVersion } from '../../package.json'
import { distDir } from '../dirs'
import { toArray } from '../utils'
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 = {
silent: 0,
@ -358,9 +359,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)
nitroConfig.rollupConfig!.plugins!.push(
ImportProtectionPlugin.rollup({
rootDir: nuxt.options.rootDir,
modulesDir: nuxt.options.modulesDir,
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
patterns: nuxtImportProtections(nuxt, { isNitro: true }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
}),
@ -517,19 +517,7 @@ 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) {
await build(nitro)
} else {
await prepare(nitro)
await prerender(nitro)
logger.restoreAll()
await build(nitro)
logger.wrapAll()
async function symlinkDist () {
if (nitro.options.static) {
const distDir = resolve(nuxt.options.rootDir, 'dist')
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

View File

@ -15,13 +15,14 @@ import { colorize } from 'consola/utils'
import { updateConfig } from 'c12/update'
import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx'
import type { DateString } from 'compatx'
import escapeRE from 'escape-string-regexp'
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
import { ImpoundPlugin } from 'impound'
import type { ImpoundOptions } from 'impound'
import defu from 'defu'
import { gt, satisfies } from 'semver'
import { hasTTY, isCI } from 'std-env'
import pagesModule from '../pages/module'
import metaModule from '../head/module'
import componentsModule from '../components/module'
@ -31,7 +32,7 @@ import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json'
import { scriptsStubsPreset } from '../imports/presets'
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 { UnctxTransformPlugin } from './plugins/unctx'
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
@ -103,7 +104,7 @@ async function initNuxt (nuxt: Nuxt) {
const shouldShowPrompt = nuxt.options.dev && hasTTY && !isCI
if (!shouldShowPrompt) {
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
}
async function promptAndUpdate () {
@ -112,7 +113,7 @@ async function initNuxt (nuxt: Nuxt) {
default: true,
})
if (result !== true) {
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
return
}
@ -146,7 +147,7 @@ async function initNuxt (nuxt: Nuxt) {
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) => {
@ -155,7 +156,7 @@ async function initNuxt (nuxt: Nuxt) {
nitro.hooks.hookOnce('compiled', () => {
warnedAboutCompatDate = true
// 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() }
})
})
@ -245,18 +246,19 @@ async function initNuxt (nuxt: Nuxt) {
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
// Add import protection
const config = {
rootDir: nuxt.options.rootDir,
const config: ImpoundOptions = {
cwd: nuxt.options.rootDir,
// Exclude top-level resolutions by plugins
exclude: [join(nuxt.options.srcDir, 'index.html')],
patterns: nuxtImportProtections(nuxt),
modulesDir: nuxt.options.modulesDir,
}
addVitePlugin(() => ImportProtectionPlugin.vite(config))
addWebpackPlugin(() => ImportProtectionPlugin.webpack(config))
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: false }), { name: 'nuxt:import-protection' }), { client: false })
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
addVitePlugin(() => resolveDeepImportsPlugin(nuxt))
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false })
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { server: false })
// Add transform for `onPrehydrate` lifecycle hook
addBuildPlugin(prehydrateTransformPlugin(nuxt))
@ -664,7 +666,7 @@ async function initNuxt (nuxt: Nuxt) {
// Show compatibility version banner when Nuxt is running with a compatibility version
// that is different from the current major version
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)

View File

@ -1,7 +1,4 @@
import { createUnplugin } from 'unplugin'
import { logger } from '@nuxt/kit'
import { resolvePath } from 'mlly'
import { isAbsolute, join, relative, resolve } from 'pathe'
import { relative, resolve } from 'pathe'
import escapeRE from 'escape-string-regexp'
import type { NuxtOptions } from 'nuxt/schema'
@ -53,41 +50,3 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
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')) {
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 }
const node = _node as CallExpression & { start: number, end: number }
const name = 'name' in node.callee && node.callee.name
if (!name || !wrapperNames.has(name)) { return }
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
if (!('order' in plugin) && !('name' in plugin)) { return }
for (const [argIndex, _arg] of node.arguments.entries()) {

View File

@ -8,20 +8,25 @@ import { pkgDir } from '../../dirs'
export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite']
let conditions: string[]
return {
name: 'nuxt:resolve-bare-imports',
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))) {
return
}
id = normalize(id)
id = resolveAlias(id, nuxt.options.alias)
const { dir } = parseNodeModulePath(importer)
return await this.resolve?.(id, dir || pkgDir, { skipSelf: true }) ?? await resolvePath(id, {
url: [dir || pkgDir, ...nuxt.options.modulesDir],
// TODO: respect nitro runtime conditions
conditions: options.ssr ? ['node', 'import', 'require'] : ['import', 'require'],
const normalisedId = resolveAlias(normalize(id), nuxt.options.alias)
const normalisedImporter = importer.replace(/^\0?virtual:(?:nuxt:)?/, '')
const dir = parseNodeModulePath(normalisedImporter).dir || pkgDir
return await this.resolve?.(normalisedId, dir, { skipSelf: true }) ?? await resolvePath(id, {
url: [dir, ...nuxt.options.modulesDir],
conditions,
}).catch(() => {
logger.debug('Could not resolve id', id, importer)
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
async setItem (key, value, opts) {
await Promise.all([
fs.setItem(normalizeFsKey(key), value, opts),
lru.setItem(key, value, opts),
fs.setItem?.(normalizeFsKey(key), value, opts),
lru.setItem?.(key, value, opts),
])
},
async hasItem (key, opts) {

View File

@ -77,7 +77,8 @@ export default (nitroApp: NitroApp) => {
const ctx = asyncContext.tryUse()
if (!ctx) { return }
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) {
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.`)

View File

@ -31,7 +31,7 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
error.fatal && '[fatal]',
Number(errorObject.statusCode) !== 200 && `[${errorObject.statusCode}]`,
].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 }
@ -119,7 +119,7 @@ function normalizeError (error: any) {
// Hide details of unhandled/fatal errors in production
const hideDetails = !import.meta.dev && error.unhandled
const stack = hideDetails
const stack = hideDetails && !import.meta.prerender
? []
: ((error.stack as string) || '')
.split('\n')

View File

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

View File

@ -120,11 +120,21 @@ export const pluginsDeclaration: NuxtTemplate = {
const relativePath = relative(typesDir, pluginPath)
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)) {
tsImports.push(relativePath)
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 (TS_RE.test(pluginPath)) {
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]) => {
let link: string | undefined
@ -211,30 +221,32 @@ export const schemaTemplate: NuxtTemplate = {
return [
` /**`,
` * Configuration for \`${importName}\``,
...jsdocTags && link
? [
` * @see ${link}`,
]
: [],
...options.addJSDocTags && 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)
return [
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
'declare module \'@nuxt/schema\' {',
' interface NuxtOptions {',
...moduleOptionsInterface({ addJSDocTags: false, unresolved: false }),
' }',
' interface NuxtConfig {',
// TypeScript will duplicate the jsdoc tags if we augment it twice
// So here we only generate tags for `nuxt/schema`
...moduleOptionsInterface(false),
...moduleOptionsInterface({ addJSDocTags: false, unresolved: true }),
' }',
'}',
'declare module \'nuxt/schema\' {',
' interface NuxtOptions {',
...moduleOptionsInterface({ addJSDocTags: true, unresolved: false }),
' }',
' interface NuxtConfig {',
...moduleOptionsInterface(true),
...moduleOptionsInterface({ addJSDocTags: true, unresolved: true }),
' }',
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
{
@ -267,7 +279,7 @@ export const layoutTemplate: NuxtTemplate = {
filename: 'layouts.mjs',
getContents ({ app }) {
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
return [name, genDynamicImport(file, { interopDefault: true })]
return [name, genDynamicImport(file)]
}))
return [
`export default ${layoutsObject}`,
@ -504,6 +516,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`,
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
`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')
},
}

View File

@ -1,9 +1,10 @@
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 type { Import, Unimport } from 'unimport'
import { createUnimport, scanDirExports, toExports } from 'unimport'
import type { ImportPresetWithDeprecation, ImportsOptions, ResolvedNuxtTemplate } from 'nuxt/schema'
import escapeRE from 'escape-string-regexp'
import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly'
import { isDirectory } from '../utils'
@ -15,7 +16,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
name: 'imports',
configKey: 'imports',
},
defaults: {
defaults: nuxt => ({
autoImport: true,
scan: true,
presets: defaultPresets,
@ -23,11 +24,13 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
imports: [],
dirs: [],
transform: {
include: [],
include: [
new RegExp('^' + escapeRE(nuxt.options.buildDir)),
],
exclude: undefined,
},
virtualImports: ['#imports'],
},
}),
async setup (options, nuxt) {
// TODO: fix sharing of defaults between invocations of modules
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
const ctx = createUnimport({
injectAtEnd: true,
...options,
addons: {
vueTemplate: options.autoImport,
@ -91,8 +95,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
nuxt.options.alias['#imports'] = join(nuxt.options.buildDir, 'imports')
// Transform to inject imports in production mode
addVitePlugin(() => TransformPlugin.vite({ 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 }))
addBuildPlugin(TransformPlugin({ 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)

View File

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

View File

@ -8,7 +8,7 @@ import { isJS, isVue } from '../core/utils'
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/
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 {
name: 'nuxt:imports-transform',
enforce: 'post',

View File

@ -515,7 +515,7 @@ export default defineNuxtModule({
const namedMiddleware = app.middleware.filter(mw => !mw.global)
return [
'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)} {`,
' interface PageMeta {',
' 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 }
const script = extractScriptContent(code)
code = script?.code || code
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' })
walk(parse(js.code, {
@ -42,6 +44,7 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
}
},
})
}
ruleCache[code] = rule
return rule

View File

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

View File

@ -12,13 +12,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (result === true) {
return
}
if (import.meta.server) {
return result
}
const error = createError({
statusCode: 404,
statusMessage: `Page Not Found: ${to.fullPath}`,
statusCode: (result && result.statusCode) || 404,
statusMessage: (result && result.statusMessage) || `Page Not Found: ${to.fullPath}`,
data: {
path: to.fullPath,
},
@ -32,7 +29,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
// We pretend to have navigated to the invalid route so
// that the user can return to the previous page with
// the back button.
window.history.pushState({}, '', to.fullPath)
window?.history.pushState({}, '', to.fullPath)
})
// We stop the navigation immediately before it resolves
// 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
}
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) {
const groups = html.match(SFC_SCRIPT_RE)?.groups || {}
if (groups.content) {
return {
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts',
code: groups.content.trim(),
} as const
const contents: Array<{ loader: 'tsx' | 'ts', code: string }> = []
for (const match of html.matchAll(SFC_SCRIPT_RE)) {
if (match?.groups?.content) {
contents.push({
loader: match.groups.attrs.includes('tsx') ? 'tsx' : 'ts',
code: match.groups.content.trim(),
})
}
}
return null
return contents
}
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 pageContentsCache: Record<string, string> = {}
@ -197,15 +199,17 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
if (absolutePath in metaCache) { return metaCache[absolutePath] }
const loader = getLoader(absolutePath)
const script = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : { code: contents, loader }
if (!script) {
const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }]
if (!scriptBlocks) {
metaCache[absolutePath] = {}
return {}
}
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
for (const script of scriptBlocks) {
if (!PAGE_META_RE.test(script.code)) {
metaCache[absolutePath] = {}
return {}
continue
}
const js = await transform(script.code, { loader: script.loader })
@ -215,8 +219,6 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
ranges: true,
}) 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>()
let foundMeta = false
@ -291,6 +293,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
}
},
})
}
metaCache[absolutePath] = extractedMeta
return extractedMeta
@ -518,7 +521,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
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 = {
name: `${metaImportName}?.name ?? ${route.name}`,

View File

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

View File

@ -2,7 +2,7 @@
"pushed route, skips generation from file": [
{
"alias": "["pushed-route-alias"]",
"component": "() => import("pages/route-file.vue").then(m => m.default || m)",
"component": "() => import("pages/route-file.vue")",
"meta": "{"someMetaData":true}",
"name": ""pushed-route"",
"path": ""/"",
@ -18,7 +18,7 @@
],
"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}",
"name": ""page-with-meta"",
"path": ""/page-with-meta"",
@ -26,7 +26,7 @@
],
"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"",
"path": ""/test\\:name"",
},
@ -37,44 +37,44 @@
{
"children": [
{
"component": "() => import("pages/param/index/index.vue").then(m => m.default || m)",
"component": "() => import("pages/param/index/index.vue")",
"name": ""param-index"",
"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"",
"path": ""sibling"",
},
],
"component": "() => import("layer/pages/param/index.vue").then(m => m.default || m)",
"component": "() => import("layer/pages/param/index.vue")",
"name": "mockMeta?.name",
"path": """",
},
{
"component": "() => import("pages/param/sibling.vue").then(m => m.default || m)",
"component": "() => import("pages/param/sibling.vue")",
"name": ""param-sibling"",
"path": ""sibling"",
},
],
"component": "() => import("pages/param.vue").then(m => m.default || m)",
"component": "() => import("pages/param.vue")",
"name": "mockMeta?.name",
"path": ""/param"",
},
{
"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"",
"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"",
"path": ""sibling"",
},
],
"component": "() => import("pages/wrapper-expose/other.vue").then(m => m.default || m)",
"component": "() => import("pages/wrapper-expose/other.vue")",
"name": "mockMeta?.name",
"path": ""/wrapper-expose/other"",
},
@ -82,7 +82,7 @@
"should extract serializable values and override fallback when normalized with `overrideMeta: true`": [
{
"alias": "["sweet-home"]",
"component": "() => import("pages/index.vue").then(m => m.default || m)",
"component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}",
"name": ""home"",
"path": ""/"",
@ -91,131 +91,131 @@
],
"should generate correct catch-all route": [
{
"component": "() => import("pages/[...slug].vue").then(m => m.default || m)",
"component": "() => import("pages/[...slug].vue")",
"name": ""slug"",
"path": ""/:slug(.*)*"",
},
{
"component": "() => import("pages/index.vue").then(m => m.default || m)",
"component": "() => import("pages/index.vue")",
"name": ""index"",
"path": ""/"",
},
],
"should generate correct dynamic routes": [
{
"component": "() => import("pages/index.vue").then(m => m.default || m)",
"component": "() => import("pages/index.vue")",
"name": ""index"",
"path": ""/"",
},
{
"component": "() => import("pages/[slug].vue").then(m => m.default || m)",
"component": "() => import("pages/[slug].vue")",
"name": ""slug"",
"path": ""/:slug()"",
},
{
"children": [
{
"component": "() => import("pages/[[foo]]/index.vue").then(m => m.default || m)",
"component": "() => import("pages/[[foo]]/index.vue")",
"name": ""foo"",
"path": """",
},
],
"component": "() => import("pages/[[foo]]").then(m => m.default || m)",
"component": "() => import("pages/[[foo]]")",
"name": "mockMeta?.name",
"path": ""/:foo?"",
},
{
"component": "() => import("pages/optional/[[opt]].vue").then(m => m.default || m)",
"component": "() => import("pages/optional/[[opt]].vue")",
"name": ""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"",
"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"",
"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"",
"path": ""/optional/prefix-:opt?-postfix"",
},
{
"component": "() => import("pages/[bar]/index.vue").then(m => m.default || m)",
"component": "() => import("pages/[bar]/index.vue")",
"name": ""bar"",
"path": ""/:bar()"",
},
{
"component": "() => import("pages/nonopt/[slug].vue").then(m => m.default || m)",
"component": "() => import("pages/nonopt/[slug].vue")",
"name": ""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"",
"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"",
"path": ""/:sub?/route-:slug()"",
},
],
"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"",
"path": ""/:stories(.*)*"",
},
{
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
"component": "() => import("pages/stories/[id].vue")",
"name": ""stories-id"",
"path": ""/stories/:id()"",
},
],
"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"",
"path": ""/stories/:id()"",
},
{
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
"component": "() => import("pages/[...stories].vue")",
"name": ""stories"",
"path": ""/:stories(.*)*"",
},
],
"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"",
"path": ""/kebab-case"",
},
],
"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"",
"path": ""/snake_case"",
},
],
"should generate correct routes for index pages": [
{
"component": "() => import("pages/index.vue").then(m => m.default || m)",
"component": "() => import("pages/index.vue")",
"name": ""index"",
"path": ""/"",
},
{
"component": "() => import("pages/parent/index.vue").then(m => m.default || m)",
"component": "() => import("pages/parent/index.vue")",
"name": ""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"",
"path": ""/parent/child"",
},
@ -224,31 +224,31 @@
{
"children": [
{
"component": "() => import("pages/parent/child.vue").then(m => m.default || m)",
"component": "() => import("pages/parent/child.vue")",
"name": ""parent-child"",
"path": ""child"",
},
],
"component": "() => import("pages/parent.vue").then(m => m.default || m)",
"component": "() => import("pages/parent.vue")",
"name": ""parent"",
"path": ""/parent"",
},
],
"should handle route groups": [
{
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
"component": "() => import("pages/(foo)/index.vue")",
"name": ""index"",
"path": ""/"",
},
{
"children": [
{
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
"component": "() => import("pages/(bar)/about/index.vue")",
"name": ""about"",
"path": """",
},
],
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
"component": "() => import("pages/(foo)/about.vue")",
"name": "mockMeta?.name",
"path": ""/about"",
},
@ -257,19 +257,19 @@
{
"children": [
{
"component": "() => import("pages/index/index/all.vue").then(m => m.default || m)",
"component": "() => import("pages/index/index/all.vue")",
"name": ""index-index-all"",
"path": ""all"",
},
],
"component": "() => import("pages/index/index.vue").then(m => m.default || m)",
"component": "() => import("pages/index/index.vue")",
"name": ""index"",
"path": ""/"",
},
],
"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} }",
"name": ""page-with-meta"",
"path": ""/page-with-meta"",
@ -277,58 +277,58 @@
],
"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"",
"path": ""/parent/:child()"",
},
{
"component": "() => import("pages/parent-[child].vue").then(m => m.default || m)",
"component": "() => import("pages/parent-[child].vue")",
"name": ""parent-child"",
"path": ""/parent-:child()"",
},
],
"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"",
"path": ""/:foo?"",
},
{
"component": "() => import("pages/[foo].vue").then(m => m.default || m)",
"component": "() => import("pages/[foo].vue")",
"name": ""foo"",
"path": ""/:foo()"",
},
],
"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"",
"path": ""/:a1_1a()"",
},
{
"component": "() => import("pages/[b2.2b].vue").then(m => m.default || m)",
"component": "() => import("pages/[b2.2b].vue")",
"name": ""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"",
"path": ""/:b2()_:2b()"",
},
{
"component": "() => import("pages/[[c3@3c]].vue").then(m => m.default || m)",
"component": "() => import("pages/[[c3@3c]].vue")",
"name": ""c33c"",
"path": ""/:c33c?"",
},
{
"component": "() => import("pages/[[d4-4d]].vue").then(m => m.default || m)",
"component": "() => import("pages/[[d4-4d]].vue")",
"name": ""d44d"",
"path": ""/:d44d?"",
},
],
"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"",
"path": ""/"",
},
@ -336,7 +336,7 @@
"should use fallbacks when normalized with `overrideMeta: true`": [
{
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)",
"component": "() => import("pages/index.vue")",
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": ""/"",

View File

@ -18,10 +18,11 @@ describe('imports:transform', () => {
]
const ctx = createUnimport({
injectAtEnd: true,
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) => {
// 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, '')
@ -171,7 +172,6 @@ const excludedVueHelpers = [
'hydrate',
'initDirectivesForSSR',
'render',
'useCssVars',
'vModelCheckbox',
'vModelDynamic',
'vModelRadio',
@ -183,6 +183,13 @@ const excludedVueHelpers = [
'ErrorCodes',
'TrackOpTypes',
'TriggerOpTypes',
'useHost',
'hydrateOnVisible',
'hydrateOnMediaQuery',
'hydrateOnInteraction',
'hydrateOnIdle',
'onWatcherCleanup',
'getCurrentWatcher',
]
describe('imports:vue', () => {

View File

@ -15,7 +15,8 @@ describe('components:transform', () => {
const code = await transform('import { Foo, Bar } from \'#components\'', '/app.vue')
expect(code).toMatchInlineSnapshot(`
"import Foo from '/Foo.vue';
"
import Foo from '/Foo.vue';
import { Bar } from '/Bar.vue';
"
`)
@ -28,7 +29,8 @@ describe('components:transform', () => {
const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
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';
"
`)
@ -54,7 +56,8 @@ describe('components:transform', () => {
const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
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';
"
`)

View File

@ -1,7 +1,7 @@
import { fileURLToPath } from 'node:url'
import { normalize } from 'pathe'
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'
const testsToTriggerOn = [
@ -39,9 +39,8 @@ describe('import protection', () => {
})
const transformWithImportProtection = (id: string, importer: string) => {
const plugin = ImportProtectionPlugin.rollup({
rootDir: '/root',
modulesDir: [fileURLToPath(new URL('..', import.meta.url))],
const plugin = ImpoundPlugin.rollup({
cwd: '/root',
patterns: nuxtImportProtections({
options: {
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 () => {
const meta = await getRouteMeta(`
<script>
@ -145,7 +180,7 @@ describe('normalizeRoutes', () => {
path: indexN6pT4Un8hYMeta?.path ?? "/",
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
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"} },
alias: indexN6pT4Un8hYMeta?.alias || [],
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', () => {
const invalidPlugins = [
'export const plugin = {}',
'export default function (ctx, inject) {}',
]
for (const plugin of invalidPlugins) {
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) {
// @ts-expect-error filePath is not optional but we don't want it to be in the snapshot
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)
})

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