Merge branch 'nuxt:main' into main

This commit is contained in:
Peter Buglavecz 2024-11-05 20:39:14 +01:00 committed by GitHub
commit 25a21f4d5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 2713 additions and 2224 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts@sha256:fffa89e023a3351904c04284029105d9e2ac7020886d683775a298569591e5bb
FROM node:lts@sha256:de4c8be8232b7081d8846360d73ce6dbff33c6636f2259cd14d82c0de1ac3ff2
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

@ -17,9 +17,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

@ -29,9 +29,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"
@ -46,7 +46,7 @@ jobs:
run: pnpm build
- name: Run benchmarks
uses: CodSpeedHQ/action@ab07afd34cbbb7a1306e8d14b7cc44e029eee37a # v3.0.0
uses: CodSpeedHQ/action@b587655f756aab640e742fec141261bc6f0a569d # v3.0.1
with:
run: pnpm vitest bench
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@ -22,11 +22,11 @@ jobs:
contents: write
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

@ -37,9 +37,9 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"
@ -72,10 +72,10 @@ jobs:
security-events: write
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
config: |
paths:
@ -91,7 +91,7 @@ jobs:
queries: +security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with:
category: "/language:javascript-typescript"
@ -107,9 +107,9 @@ jobs:
module: ["bundler", "node"]
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"
@ -138,9 +138,9 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"
@ -162,9 +162,9 @@ jobs:
needs:
- build
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"
@ -219,9 +219,9 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: ${{ matrix.node }}
cache: "pnpm"
@ -271,11 +271,11 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"
@ -312,11 +312,11 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

@ -17,6 +17,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4
uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0

View File

@ -19,17 +19,17 @@ jobs:
steps:
# Cache lychee results (e.g. to avoid hitting rate limits)
- name: Restore lychee cache
uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with:
path: .lycheecache
key: cache-lychee-${{ github.sha }}
restore-keys: cache-lychee-
# check links with Lychee
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Lychee link checker
uses: lycheeverse/lychee-action@2bb232618be239862e31382c5c0eaeba12e5e966 # for v1.8.0
uses: lycheeverse/lychee-action@ae4699150ab670dcfb64cc74e8680e776d9caae2 # for v1.8.0
with:
# arguments with file types to check
args: >-

View File

@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files
uses: docker://rhysd/actionlint:1.7.3@sha256:7617f05bd698cd2f1c3aedc05bc733ccec92cca0738f3e8722c32c5b42c70ae6
uses: docker://rhysd/actionlint:1.7.4@sha256:82244e1db1c60d82c7792180a48dd0bcb838370bb589d53ff132503fc9485868
with:
args: -color

View File

@ -48,13 +48,13 @@ jobs:
fi
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.pr.outputs.head_sha }}
fetch-depth: 1
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

@ -19,11 +19,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
registry-url: "https://registry.npmjs.org/"

View File

@ -10,7 +10,7 @@ jobs:
reproduire:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
with:
label: needs reproduction

View File

@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
@ -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@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif

View File

@ -11,7 +11,7 @@ jobs:
stackblitz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: huang-julien/reproduire-sur-stackblitz@9ceccbfbb0f2f9a9a8db2d1f0dd909cf5cfe67aa # v1.0.2
with:
reproduction-heading: '### Reproduction'

View File

@ -68,6 +68,10 @@ When importing `@nuxt/test-utils` in your vitest config, It is necessary to have
> ie. `vitest.config.m{ts,js}`.
::
::tip
It is possible to set environment variables for testing by using the `.env.test` file.
::
### Using a Nuxt Runtime Environment
By default, `@nuxt/test-utils` will not change your default Vitest environment, so you can do fine-grained opt-in and run Nuxt tests together with other unit tests.
@ -285,7 +289,7 @@ import { mockNuxtImport } from '@nuxt/test-utils/runtime'
const { useStorageMock } = vi.hoisted(() => {
return {
useStorageMock: vi.fn().mockImplementation(() => {
useStorageMock: vi.fn(() => {
return { value: 'mocked storage'}
})
}

View File

@ -67,6 +67,7 @@ export default defineNuxtConfig({
// app: 'app'
// },
// experimental: {
// scanPageMeta: 'after-resolve',
// sharedPrerenderData: false,
// compileTemplate: true,
// resetAsyncDataToUndefined: true,
@ -236,6 +237,45 @@ export default defineNuxtConfig({
})
```
#### Scan Page Meta After Resolution
🚦 **Impact Level**: Minimal
##### What Changed
We now scan page metadata (defined in `definePageMeta`) _after_ calling the `pages:extend` hook rather than before.
##### Reasons for Change
This was to allow scanning metadata for pages that users wanted to add in `pages:extend`. We still offer an opportunity to change or override page metadata in a new `pages:resolved` hook.
##### Migration Steps
If you want to override page metadata, do that in `pages:resolved` rather than in `pages:extend`.
```diff
export default defineNuxtConfig({
hooks: {
- 'pages:extend'(pages) {
+ 'pages:resolved'(pages) {
const myPage = pages.find(page => page.path === '/')
myPage.meta ||= {}
myPage.meta.layout = 'overridden-layout'
}
}
})
```
Alternatively, you can revert to the previous behaviour with:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
scanPageMeta: true
}
})
```
#### Shared Prerender Data
🚦 **Impact Level**: Medium

View File

@ -33,6 +33,7 @@ You don't have to use TypeScript to build an application with Nuxt. However, it
You can configure fully typed, per-environment overrides in your nuxt.config
```ts twoslash [nuxt.config.ts]
// @errors: 2353
export default defineNuxtConfig({
$production: {
routeRules: {
@ -41,10 +42,17 @@ export default defineNuxtConfig({
},
$development: {
//
}
},
$myCustomName: {
//
},
})
```
To select an environment when running a Nuxt CLI command, simply pass the name to the `--envName` flag, like so: `nuxi build --envName myCustomName`.
To learn more about the mechanism behind these overrides, please refer to the `c12` documentation on [environment-specific configuration](https://github.com/unjs/c12?tab=readme-ov-file#environment-specific-configuration).
::tip{icon="i-ph-video" to="https://www.youtube.com/watch?v=DFZI2iVCrNc" target="_blank"}
Watch a video from Alexander Lichter about the env-aware `nuxt.config.ts`.
::

View File

@ -88,6 +88,37 @@ It is recommended to use `$fetch` for client-side interactions (event based) or
Read more about `$fetch`.
::
### Pass Client Headers to the API
During server-side-rendering, since the `$fetch` request takes place 'internally' within the server, it won't include the user's browser cookies.
We can use [`useRequestHeaders`](/docs/api/composables/use-request-headers) to access and proxy cookies to the API from server-side.
The example below adds the request headers to an isomorphic `$fetch` call to ensure that the API endpoint has access to the same `cookie` header originally sent by the user.
```vue
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser() {
return await $fetch('/api/me', { headers: headers.value })
}
</script>
```
::caution
Be very careful before proxying headers to an external API and just include headers that you need. Not all headers are safe to be bypassed and might introduce unwanted behavior. Here is a list of common headers that are NOT to be proxied:
- `host`, `accept`
- `content-length`, `content-md5`, `content-type`
- `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-proto`
- `cf-connecting-ip`, `cf-ray`
::
::tip
You can also use [`useRequestFetch`](/docs/api/composables/use-request-fetch) to proxy headers to the call automatically.
::
## `useFetch`
The [`useFetch`](/docs/api/composables/use-fetch) composable uses `$fetch` under-the-hood to make SSR-safe network calls in the setup function.
@ -117,8 +148,8 @@ Watch the video from Alexander Lichter to avoid using `useFetch` the wrong way!
The `useAsyncData` composable is responsible for wrapping async logic and returning the result once it is resolved.
::tip
`useFetch(url)` is nearly equivalent to `useAsyncData(url, () => $fetch(url))`. :br
It's developer experience sugar for the most common use case.
`useFetch(url)` is nearly equivalent to `useAsyncData(url, () => event.$fetch(url))`. :br
It's developer experience sugar for the most common use case. (You can find out more about `event.fetch` at [`useRequestFetch`](/docs/api/composables/use-request-fetch).)
::
::tip{icon="i-ph-video" to="https://www.youtube.com/watch?v=0X-aOpSGabA" target="_blank"}
@ -458,32 +489,13 @@ For finer control, the `status` variable can be:
- `error` when the fetch fails
- `success` when the fetch is completed successfully
## Passing Headers and cookies
## Passing Headers and Cookies
When we call `$fetch` in the browser, user headers like `cookie` will be directly sent to the API. But during server-side-rendering, since the `$fetch` request takes place 'internally' within the server, it doesn't include the user's browser cookies, nor does it pass on cookies from the fetch response.
When we call `$fetch` in the browser, user headers like `cookie` will be directly sent to the API.
### Pass Client Headers to the API
Normally, during server-side-rendering, since the `$fetch` request takes place 'internally' within the server, it wouldn't include the user's browser cookies, nor pass on cookies from the fetch response.
We can use [`useRequestHeaders`](/docs/api/composables/use-request-headers) to access and proxy cookies to the API from server-side.
The example below adds the request headers to an isomorphic `$fetch` call to ensure that the API endpoint has access to the same `cookie` header originally sent by the user.
```vue
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
const { data } = await useFetch('/api/me', { headers })
</script>
```
::caution
Be very careful before proxying headers to an external API and just include headers that you need. Not all headers are safe to be bypassed and might introduce unwanted behavior. Here is a list of common headers that are NOT to be proxied:
- `host`, `accept`
- `content-length`, `content-md5`, `content-type`
- `x-forwarded-host`, `x-forwarded-port`, `x-forwarded-proto`
- `cf-connecting-ip`, `cf-ray`
::
However, when calling `useFetch` on the server, Nuxt will use [`useRequestFetch`](/docs/api/composables/use-request-fetch) to proxy headers and cookies (with the exception of headers not meant to be forwarded, like `host`).
### Pass Cookies From Server-side API Calls on SSR Response

View File

@ -48,7 +48,7 @@ const double = computed(() => count.value * 2)
</script>
```
### Vue and Nuxt composables
### Vue and Nuxt Composables
<!-- TODO: move to separate page with https://github.com/nuxt/nuxt/issues/14723 and add more information -->
@ -159,7 +159,7 @@ export default defineNuxtConfig({
})
```
## Auto-import from third-party packages
## Auto-import from Third-Party Packages
Nuxt also allows auto-importing from third-party packages.

View File

@ -323,6 +323,10 @@ You may define a name for this page's route.
You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. See [the `vue-router` docs](https://router.vuejs.org/guide/essentials/route-matching-syntax.html#custom-regex-in-params) for more information.
#### `props`
Allows accessing the route `params` as props passed to the page component. See[the `vue-router` docs](https://router.vuejs.org/guide/essentials/passing-props) for more information.
### Typing Custom Metadata
If you add custom metadata for your pages, you may wish to do so in a type-safe way. It is possible to augment the type of the object accepted by `definePageMeta`:

View File

@ -158,7 +158,7 @@ export default defineEventHandler((event) => {
})
```
::tip
::tip{to="https://h3.unjs.io/examples/validate-data#validate-params"}
Alternatively, use `getValidatedRouterParams` with a schema validator such as Zod for runtime and type safety.
::

View File

@ -19,6 +19,10 @@ export default defineAppConfig({
Do not put any secret values inside `app.config` file. It is exposed to the user client bundle.
::
::note
When configuring a custom [`srcDir`](/docs/api/nuxt-config#srcdir), make sure to place the `app.config` file at the root of the new `srcDir` path.
::
## Usage
To expose config and environment variables to the rest of your app, you will need to define configuration in `app.config` file.
@ -31,7 +35,7 @@ export default defineAppConfig({
})
```
When adding `theme` to the `app.config`, Nuxt uses Vite or webpack to bundle the code. We can universally access `theme` both when server-rendering the page and in the browser using [`useAppConfig`](/docs/api/composables/use-app-config) composable.
We can now universally access `theme` both when server-rendering the page and in the browser using [`useAppConfig`](/docs/api/composables/use-app-config) composable.
```vue [pages/index.vue]
<script setup lang="ts">
@ -41,7 +45,23 @@ console.log(appConfig.theme)
</script>
```
When configuring a custom [`srcDir`](/docs/api/nuxt-config#srcdir), make sure to place the `app.config` file at the root of the new `srcDir` path.
The [`updateAppConfig`](/docs/api/utils/update-app-config) utility can be used to update the `app.config` at runtime.
```vue [pages/index.vue]
<script setup>
const appConfig = useAppConfig() // { foo: 'bar' }
const newAppConfig = { foo: 'baz' }
updateAppConfig(newAppConfig)
console.log(appConfig) // { foo: 'baz' }
</script>
```
::read-more{to="/docs/api/utils/update-app-config"}
Read more about the `updateAppConfig` utility.
::
## Typing App Config

View File

@ -59,14 +59,16 @@ This feature will likely be removed in a near future.
## emitRouteChunkError
Emits `app:chunkError` hook when there is an error loading vite/webpack chunks. Default behavior is to perform a hard reload of the new route when a chunk fails to load.
Emits `app:chunkError` hook when there is an error loading vite/webpack chunks. Default behavior is to perform a reload of the new route on navigation to a new route when a chunk fails to load.
If you set this to `'automatic-immediate'` Nuxt will reload the current route immediatly, instead of waiting for a navigation. This is useful for chunk errors that are not triggered by navigation, e.g., when your Nuxt app fails to load a [lazy component](/docs/guide/directory-structure/components#dynamic-imports). A potential downside of this behavior is undesired reloads, e.g., when your app does not need the chunk that caused the error.
You can disable automatic handling by setting this to `false`, or handle chunk errors manually by setting it to `manual`.
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
emitRouteChunkError: 'automatic' // or 'manual' or false
emitRouteChunkError: 'automatic' // or 'automatic-immediate', 'manual' or false
}
})
```
@ -334,6 +336,8 @@ This option allows exposing some route metadata defined in `definePageMeta` at b
This only works with static or strings/arrays rather than variables or conditional assignment. See [original issue](https://github.com/nuxt/nuxt/issues/24770) for more information and context.
It is also possible to scan page metadata only after all routes have been registered in `pages:extend`. Then another hook, `pages:resolved` will be called. To enable this behavior, set `scanPageMeta: 'after-resolve'`.
You can disable this feature if it causes issues in your project.
```ts twoslash [nuxt.config.ts]

View File

@ -61,6 +61,7 @@ export default defineNuxtConfig({
app: 'app'
},
experimental: {
scanPageMeta: 'after-resolve',
sharedPrerenderData: false,
compileTemplate: true,
resetAsyncDataToUndefined: true,

View File

@ -77,7 +77,7 @@ export function useAPI<T>(
) {
return useFetch(url, {
...options,
$fetch: useNuxtApp().$api
$fetch: useNuxtApp().$api as typeof $fetch
})
}
```

View File

@ -0,0 +1,43 @@
---
title: useRuntimeHook
description: Registers a runtime hook in a Nuxt application and ensures it is properly disposed of when the scope is destroyed.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/runtime-hook.ts
size: xs
---
::important
This composable is available in Nuxt v3.14+.
::
```ts [signature]
function useRuntimeHook<THookName extends keyof RuntimeNuxtHooks>(
name: THookName,
fn: RuntimeNuxtHooks[THookName] extends HookCallback ? RuntimeNuxtHooks[THookName] : never
): void
```
## Usage
### Parameters
- `name`: The name of the runtime hook to register. You can see the full list of [runtime Nuxt hooks here](/docs/api/advanced/hooks#app-hooks-runtime).
- `fn`: The callback function to execute when the hook is triggered. The function signature varies based on the hook name.
### Returns
The composable doesn't return a value, but it automatically unregisters the hook when the component's scope is destroyed.
## Example
```vue twoslash [pages/index.vue]
<script setup lang="ts">
// Register a hook that runs every time a link is prefetched, but which will be
// automatically cleaned up (and not called again) when the component is unmounted
useRuntimeHook('link:prefetch', (link) => {
console.log('Prefetching', link)
})
</script>
```

View File

@ -30,6 +30,7 @@ interface PageMeta {
redirect?: RouteRecordRedirectOption
name?: string
path?: string
props?: RouteRecordRaw['props']
alias?: string | string[]
pageTransition?: boolean | TransitionProps
layoutTransition?: boolean | TransitionProps
@ -63,6 +64,12 @@ interface PageMeta {
You may define a [custom regular expression](#using-a-custom-regular-expression) if you have a more complex pattern than can be expressed with the file name.
**`props`**
- **Type**: [`RouteRecordRaw['props']`](https://router.vuejs.org/guide/essentials/passing-props)
Allows accessing the route `params` as props passed to the page component.
**`alias`**
- **Type**: `string | string[]`

View File

@ -125,6 +125,19 @@ Make sure to always use `await` or `return` on result of `navigateTo` when calli
`to` can be a plain string or a route object to redirect to. When passed as `undefined` or `null`, it will default to `'/'`.
#### Example
```ts
// Passing the URL directly will redirect to the '/blog' page
await navigateTo('/blog')
// Using the route object, will redirect to the route with the name 'blog'
await navigateTo({ name: 'blog' })
// Redirects to the 'product' route while passing a parameter (id = 1) using the route object.
await navigateTo({ name: 'product', params: { id: 1 } })
```
### `options` (optional)
**Type**: `NavigateToOptions`

View File

@ -52,7 +52,8 @@ Hook | Arguments | Description
`build:manifest` | `manifest` | Called during the manifest build by Vite and webpack. This allows customizing the manifest that Nitro will use to render `<script>` and `<link>` tags in the final HTML.
`builder:generateApp` | `options` | Called before generating the app.
`builder:watch` | `event, path` | Called at build time in development when the watcher spots a change to a file or directory in the project.
`pages:extend` | `pages` | Called after pages routes are resolved.
`pages:extend` | `pages` | Called after page routes are scanned from the file system.
`pages:resolved` | `pages` | Called after page routes have been augmented with scanned metadata.
`pages:routerOptions` | `{ files: Array<{ path: string, optional?: boolean }> }` | Called when resolving `router.options` files. Later items in the array override earlier ones.
`server:devHandler` | `handler` | Called when the dev middleware is being registered on the Nitro dev server.
`imports:sources` | `presets` | Called at setup allowing modules to extend sources.

View File

@ -3,8 +3,6 @@
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 }
@ -18,4 +16,6 @@ export default defineNuxtConfig({
})
},
],
pages: process.env.DOCS_TYPECHECK === 'true',
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
})

View File

@ -40,41 +40,47 @@
"@nuxt/ui-templates": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"@types/node": "20.16.11",
"@vue/compiler-core": "3.5.11",
"@vue/compiler-dom": "3.5.11",
"@vue/shared": "3.5.11",
"@types/node": "22.9.0",
"@unhead/dom": "1.11.11",
"@unhead/shared": "1.11.11",
"@unhead/vue": "1.11.11",
"@unhead/schema": "1.11.11",
"@unhead/ssr": "1.11.11",
"@vue/compiler-core": "3.5.12",
"@vue/compiler-dom": "3.5.12",
"@vue/shared": "3.5.12",
"c12": "2.0.1",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "2.3.3",
"jiti": "2.4.0",
"magic-string": "^0.30.12",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxt": "workspace:*",
"ohash": "1.1.4",
"postcss": "8.4.47",
"rollup": "4.24.0",
"rollup": "4.24.4",
"send": ">=1.1.0",
"typescript": "5.6.3",
"ufo": "1.5.4",
"unbuild": "3.0.0-rc.11",
"vite": "5.4.8",
"vue": "3.5.11"
"unhead": "1.11.11",
"vite": "5.4.10",
"vue": "3.5.12"
},
"devDependencies": {
"@eslint/js": "9.12.0",
"@nuxt/eslint-config": "0.5.7",
"@eslint/js": "9.14.0",
"@nuxt/eslint-config": "0.6.1",
"@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.14.3",
"@nuxt/test-utils": "3.14.4",
"@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3",
"@types/node": "20.16.11",
"@types/node": "22.9.0",
"@types/semver": "7.5.8",
"@unhead/schema": "1.11.7",
"@unhead/vue": "1.11.7",
"@unhead/schema": "1.11.11",
"@unhead/vue": "1.11.11",
"@vitejs/plugin-vue": "5.1.4",
"@vitest/coverage-v8": "2.1.2",
"@vitest/coverage-v8": "2.1.4",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20",
"case-police": "0.7.0",
@ -83,36 +89,36 @@
"cssnano": "7.0.6",
"destr": "2.0.3",
"devalue": "5.1.1",
"eslint": "9.12.0",
"eslint": "9.14.0",
"eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "3.8.0",
"eslint-plugin-perfectionist": "3.9.1",
"eslint-typegen": "0.3.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "15.7.4",
"jiti": "2.3.3",
"happy-dom": "15.9.0",
"jiti": "2.4.0",
"markdownlint-cli": "0.42.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxi": "3.14.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "3.15.0",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1",
"ofetch": "1.4.1",
"pathe": "1.1.2",
"playwright-core": "1.48.0",
"playwright-core": "1.48.2",
"rimraf": "6.0.1",
"semver": "7.6.3",
"sherif": "1.0.0",
"sherif": "1.0.1",
"std-env": "3.7.0",
"tinyexec": "0.3.0",
"tinyglobby": "0.2.9",
"tinyexec": "0.3.1",
"tinyglobby": "0.2.10",
"typescript": "5.6.3",
"ufo": "1.5.4",
"vitest": "2.1.2",
"vitest": "2.1.4",
"vitest-environment-nuxt": "1.0.1",
"vue": "3.5.11",
"vue": "3.5.12",
"vue-router": "4.4.5",
"vue-tsc": "2.1.6"
"vue-tsc": "2.1.10"
},
"packageManager": "pnpm@9.12.1",
"packageManager": "pnpm@9.12.3",
"engines": {
"node": "^16.10.0 || >=18.0.0"
},

View File

@ -35,7 +35,7 @@
"globby": "^14.0.2",
"hash-sum": "^2.0.0",
"ignore": "^6.0.2",
"jiti": "^2.3.3",
"jiti": "^2.4.0",
"klona": "^2.0.6",
"mlly": "^1.7.2",
"pathe": "^1.1.2",
@ -48,14 +48,14 @@
"untyped": "^1.5.1"
},
"devDependencies": {
"@rspack/core": "1.0.10",
"@rspack/core": "1.0.14",
"@types/hash-sum": "1.0.2",
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"unbuild": "3.0.0-rc.11",
"vite": "5.4.8",
"vitest": "2.1.2",
"webpack": "5.95.0"
"vite": "5.4.10",
"vitest": "2.1.4",
"webpack": "5.96.1"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"

View File

@ -3,8 +3,9 @@ import { readPackageJSON } from 'pkg-types'
import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema'
import { useNuxt } from './context'
const SEMANTIC_VERSION_RE = /-\d+\.[0-9a-f]+/
export function normalizeSemanticVersion (version: string) {
return version.replace(/-\d+\.[0-9a-f]+/, '') // Remove edge prefix
return version.replace(SEMANTIC_VERSION_RE, '') // Remove edge prefix
}
const builderMap = {
@ -104,6 +105,7 @@ export function isNuxt3 (nuxt: Nuxt = useNuxt()) {
return isNuxtMajorVersion(3, nuxt)
}
const NUXT_VERSION_RE = /^v/g
/**
* Get nuxt version
*/
@ -112,5 +114,5 @@ export function getNuxtVersion (nuxt: Nuxt | any = useNuxt() /* TODO: LegacyNuxt
if (typeof rawVersion !== 'string') {
throw new TypeError('Cannot determine nuxt version! Is current instance passed?')
}
return rawVersion.replace(/^v/g, '')
return rawVersion.replace(NUXT_VERSION_RE, '')
}

View File

@ -3,6 +3,7 @@ import type { Component, ComponentsDir } from '@nuxt/schema'
import { useNuxt } from './context'
import { assertNuxtCompatibility } from './compatibility'
import { logger } from './logger'
import { MODE_RE } from './utils'
/**
* Register a directory to be scanned for components and imported only when used.
@ -28,7 +29,7 @@ export async function addComponent (opts: AddComponentOptions) {
nuxt.options.components = nuxt.options.components || []
if (!opts.mode) {
const [, mode = 'all'] = opts.filePath.match(/\.(server|client)(\.\w+)*$/) || []
const [, mode = 'all'] = opts.filePath.match(MODE_RE) || []
opts.mode = mode as 'all' | 'client' | 'server'
}

View File

@ -5,10 +5,11 @@ import { useNuxt } from './context'
import { logger } from './logger'
import { addTemplate } from './template'
const LAYOUT_RE = /["']/g
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, '')
const layoutName = kebabCase(name || parse(filename).name).replace(LAYOUT_RE, '')
// Nuxt 3 adds layouts on app
nuxt.hook('app:templates', (app) => {

View File

@ -58,8 +58,8 @@ export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: N
}
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
if (hasNuxtModule(moduleMeta.name)) {
const { buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt)
return buildTimeModuleMeta.version || false
const { nuxtModule, buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt)
return buildTimeModuleMeta.version || await nuxtModule.getMeta?.().then(r => r.version) || false
}
return false
}

View File

@ -1,10 +1,12 @@
import { existsSync, promises as fsp, lstatSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import type { ModuleMeta, Nuxt, NuxtConfig, NuxtModule } from '@nuxt/schema'
import { dirname, isAbsolute, join, resolve } from 'pathe'
import { defu } from 'defu'
import { createJiti } from 'jiti'
import { resolve as resolveModule } from 'mlly'
import { useNuxt } from '../context'
import { resolveAlias } from '../resolve'
import { resolveAlias, resolvePath } from '../resolve'
import { logger } from '../logger'
const NODE_MODULES_RE = /[/\\]node_modules[/\\]/
@ -77,11 +79,14 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
// Import if input is string
if (typeof nuxtModule === 'string') {
const paths = [join(nuxtModule, 'nuxt'), join(nuxtModule, 'module'), nuxtModule, join(nuxt.options.rootDir, nuxtModule)]
for (const parentURL of nuxt.options.modulesDir) {
for (const path of paths) {
for (const path of paths) {
for (const parentURL of nuxt.options.modulesDir) {
try {
const src = jiti.esmResolve(path, { parentURL: parentURL.replace(/\/node_modules\/?$/, '') })
const resolved = resolveAlias(path, nuxt.options.alias)
const src = isAbsolute(resolved)
? await resolvePath(resolved, { cwd: parentURL, fallbackToOriginal: false, extensions: nuxt.options.extensions })
: await resolveModule(resolved, { url: pathToFileURL(parentURL.replace(/\/node_modules\/?$/, '')), extensions: nuxt.options.extensions })
nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
// nuxt-module-builder generates a module.json with metadata including the version
@ -92,7 +97,7 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
break
} catch (error: unknown) {
const code = (error as Error & { code?: string }).code
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') {
continue
}
logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`)

View File

@ -4,13 +4,14 @@ import { normalize } from 'pathe'
import { useNuxt } from './context'
import { toArray } from './utils'
const HANDLER_METHOD_RE = /\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/
/**
* normalize handler object
*
*/
function normalizeHandlerMethod (handler: NitroEventHandler) {
// retrieve method from handler file name
const [, method = undefined] = handler.handler.match(/\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/) || []
const [, method = undefined] = handler.handler.match(HANDLER_METHOD_RE) || []
return {
method: method as 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | undefined,
...handler,

View File

@ -3,6 +3,7 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
import { useNuxt } from './context'
import { addTemplate } from './template'
import { resolveAlias } from './resolve'
import { MODE_RE } from './utils'
/**
* Normalize a nuxt plugin object
@ -27,7 +28,7 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin.mode = 'server'
}
if (!plugin.mode) {
const [, mode = 'all'] = plugin.src.match(/\.(server|client)(\.\w+)*$/) || []
const [, mode = 'all'] = plugin.src.match(MODE_RE) || []
plugin.mode = mode as 'all' | 'client' | 'server'
}

View File

@ -123,6 +123,9 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
}
const EXTENSION_RE = /\b\.\w+$/g
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/, /^#internal\/nuxt/]
export async function _generateTypes (nuxt: Nuxt) {
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
@ -225,9 +228,6 @@ export async function _generateTypes (nuxt: Nuxt) {
const aliases: Record<string, string> = nuxt.options.alias
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/, /^#internal\/nuxt/]
const basePath = tsConfig.compilerOptions!.baseUrl
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
: nuxt.options.buildDir
@ -260,7 +260,7 @@ export async function _generateTypes (nuxt: Nuxt) {
} else {
const path = stats?.isFile()
// remove extension
? relativePath.replace(/\b\.\w+$/g, '')
? relativePath.replace(EXTENSION_RE, '')
// non-existent file probably shouldn't be resolved
: aliases[alias]!
@ -289,7 +289,7 @@ export async function _generateTypes (nuxt: Nuxt) {
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
if (!isAbsolute(path)) { return path }
const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/\b\.\w+$/g, '') /* remove extension */ : path)
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(EXTENSION_RE, '') /* remove extension */ : path)
}))
}
@ -344,6 +344,7 @@ function renderAttr (key: string, value?: string) {
return value ? `${key}="${value}"` : ''
}
const RELATIVE_WITH_DOT_RE = /^([^.])/
function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.'
return relative(from, to).replace(RELATIVE_WITH_DOT_RE, './$1') || '.'
}

View File

@ -2,3 +2,5 @@
export function toArray<T> (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value]
}
export const MODE_RE = /\.(server|client)(\.\w+)*$/

View File

@ -60,17 +60,17 @@
},
"dependencies": {
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.5.2",
"@nuxt/devtools": "^1.6.0",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.0",
"@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.11.7",
"@unhead/shared": "^1.11.7",
"@unhead/ssr": "^1.11.7",
"@unhead/vue": "^1.11.7",
"@vue/shared": "^3.5.11",
"acorn": "8.12.1",
"@unhead/dom": "^1.11.11",
"@unhead/shared": "^1.11.11",
"@unhead/ssr": "^1.11.11",
"@unhead/vue": "^1.11.11",
"@vue/shared": "^3.5.12",
"acorn": "8.14.0",
"c12": "^2.0.1",
"chokidar": "^4.0.1",
"compatx": "^0.1.8",
@ -88,14 +88,14 @@
"hookable": "^5.5.3",
"ignore": "^6.0.2",
"impound": "^0.2.0",
"jiti": "^2.3.3",
"jiti": "^2.4.0",
"klona": "^2.0.6",
"knitwork": "^1.1.0",
"magic-string": "^0.30.12",
"mlly": "^1.7.2",
"nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxi": "^3.14.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.15.0",
"nypm": "^0.3.12",
"ofetch": "^1.4.1",
"ohash": "^1.1.4",
@ -107,33 +107,33 @@
"semver": "^7.6.3",
"std-env": "^3.7.0",
"strip-literal": "^2.1.0",
"tinyglobby": "0.2.9",
"tinyglobby": "0.2.10",
"ufo": "^1.5.4",
"ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3",
"unctx": "^2.3.1",
"unenv": "^1.10.0",
"unhead": "^1.11.7",
"unhead": "^1.11.11",
"unimport": "^3.13.1",
"unplugin": "^1.14.1",
"unplugin": "^1.15.0",
"unplugin-vue-router": "^0.10.8",
"unstorage": "^1.12.0",
"unstorage": "^1.13.1",
"untyped": "^1.5.1",
"vue": "^3.5.11",
"vue": "^3.5.12",
"vue-bundle-renderer": "^2.1.1",
"vue-devtools-stub": "^0.1.0",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@nuxt/scripts": "0.9.4",
"@nuxt/scripts": "0.9.5",
"@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1",
"@parcel/watcher": "2.5.0",
"@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.11",
"@vue/compiler-sfc": "3.5.12",
"unbuild": "3.0.0-rc.11",
"vite": "5.4.8",
"vitest": "2.1.2"
"vite": "5.4.10",
"vitest": "2.1.4"
},
"peerDependencies": {
"@parcel/watcher": "^2.1.0",

View File

@ -22,6 +22,7 @@ const SSR_UID_RE = /data-island-uid="([^"]*)"/
const DATA_ISLAND_UID_RE = /data-island-uid(="")?(?!="[^"])/g
const SLOTNAME_RE = /data-island-slot="([^"]*)"/g
const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g
const ISLAND_SCOPE_ID_RE = /^<[^> ]*/
let id = 1
const getId = import.meta.client ? () => (id++).toString() : randomUUID
@ -142,7 +143,7 @@ export default defineComponent({
let html = ssrHTML.value
if (props.scopeId) {
html = html.replace(/^<[^> ]*/, full => full + ' ' + props.scopeId)
html = html.replace(ISLAND_SCOPE_ID_RE, full => full + ' ' + props.scopeId)
}
if (import.meta.client && !canLoadClientComponent.value) {

View File

@ -521,11 +521,12 @@ function useObserver (): { observe: ObserveFn } | undefined {
return _observer
}
const IS_2G_RE = /2g/
function isSlowConnection () {
if (import.meta.server) { return }
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
const cn = (navigator as any).connection as { saveData: boolean, effectiveType: string } | null
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true }
if (cn && (cn.saveData || IS_2G_RE.test(cn.effectiveType))) { return true }
return false
}

View File

@ -15,13 +15,16 @@ export const _wrapIf = (component: Component, props: any, slots: any) => {
return { default: () => props ? h(component, props, slots) : slots.default?.() }
}
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g
const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g
const ROUTE_KEY_NORMAL_RE = /:\w+/g
// TODO: consider refactoring into single utility
// See https://github.com/nuxt/nuxt/tree/main/packages/nuxt/src/pages/runtime/utils.ts#L8-L19
function generateRouteKey (route: RouteLocationNormalized) {
const source = route?.meta.key ?? route.path
.replace(/(:\w+)\([^)]+\)/g, '$1')
.replace(/(:\w+)[?+*]/g, '$1')
.replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '')
.replace(ROUTE_KEY_PARENTHESES_RE, '$1')
.replace(ROUTE_KEY_SYMBOLS_RE, '$1')
.replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '')
return typeof source === 'function' ? source(route) : source
}

View File

@ -56,7 +56,6 @@ export const defineNuxtComponent: typeof defineComponent =
}
if (options.head) {
const nuxtApp = useNuxtApp()
useHead(typeof options.head === 'function' ? () => options.head(nuxtApp) : options.head)
}

View File

@ -38,3 +38,4 @@ export { useRequestURL } from './url'
export { usePreviewMode } from './preview'
export { useId } from './id'
export { useRouteAnnouncer } from './route-announcer'
export { useRuntimeHook } from './runtime-hook'

View File

@ -114,6 +114,7 @@ export interface NavigateToOptions {
open?: OpenOptions
}
const URL_QUOTE_RE = /"/g
/** @since 3.0.0 */
export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw => {
if (!to) {
@ -166,7 +167,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
const redirect = async function (response: any) {
// TODO: consider deprecating in favour of `app:rendered` and removing
await nuxtApp.callHook('app:redirected')
const encodedLoc = location.replace(/"/g, '%22')
const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost)
nuxtApp.ssrContext!._renderResponse = {

View File

@ -0,0 +1,21 @@
import { onScopeDispose } from 'vue'
import type { HookCallback } from 'hookable'
import { useNuxtApp } from '../nuxt'
import type { RuntimeNuxtHooks } from '../nuxt'
/**
* Registers a runtime hook in a Nuxt application and ensures it is properly disposed of when the scope is destroyed.
* @param name - The name of the hook to register.
* @param fn - The callback function to be executed when the hook is triggered.
* @since 3.14.0
*/
export function useRuntimeHook<THookName extends keyof RuntimeNuxtHooks> (
name: THookName,
fn: RuntimeNuxtHooks[THookName] extends HookCallback ? RuntimeNuxtHooks[THookName] : never,
): void {
const nuxtApp = useNuxtApp()
const unregister = nuxtApp.hook(name, fn)
onScopeDispose(unregister)
}

View File

@ -1,7 +1,7 @@
export { applyPlugin, applyPlugins, callWithNuxt, createNuxtApp, defineAppConfig, defineNuxtPlugin, definePayloadPlugin, isNuxtPlugin, registerPluginHooks, tryUseNuxtApp, useNuxtApp, useRuntimeConfig } from './nuxt'
export type { CreateOptions, NuxtApp, NuxtPayload, NuxtPluginIndicator, NuxtSSRContext, ObjectPlugin, Plugin, PluginEnvContext, PluginMeta, ResolvedPluginMeta, RuntimeNuxtHooks } from './nuxt'
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta } from './composables/index'
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta, useRuntimeHook } from './composables/index'
export type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
export { defineNuxtLink } from './components/index'

View File

@ -45,7 +45,7 @@ export interface RuntimeNuxtHooks {
'app:chunkError': (options: { error: any }) => HookResult
'app:data:refresh': (keys?: string[]) => HookResult
'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult
'dev:ssr-logs': (logs: LogObject[]) => void | Promise<void>
'dev:ssr-logs': (logs: LogObject[]) => HookResult
'link:prefetch': (link: string) => HookResult
'page:start': (Component?: VNode) => HookResult
'page:finish': (Component?: VNode) => HookResult

View File

@ -0,0 +1,23 @@
import { defineNuxtPlugin } from '../nuxt'
import { reloadNuxtApp } from '../composables/chunk'
import { addRouteMiddleware } from '../composables/router'
const reloadNuxtApp_ = (path: string) => { reloadNuxtApp({ persistState: true, path }) }
// See https://github.com/nuxt/nuxt/issues/23612 for more context
export default defineNuxtPlugin({
name: 'nuxt:chunk-reload-immediate',
setup (nuxtApp) {
// Remember `to.path` when navigating to a new path: A `chunkError` may occur during navigation, we then want to then reload at `to.path`
let currentlyNavigationTo: null | string = null
addRouteMiddleware((to) => {
currentlyNavigationTo = to.path
})
// Reload when a `chunkError` is thrown
nuxtApp.hook('app:chunkError', () => reloadNuxtApp_(currentlyNavigationTo ?? nuxtApp._route.path))
// Reload when the app manifest updates
nuxtApp.hook('app:manifest:update', () => reloadNuxtApp_(nuxtApp._route.path))
},
})

View File

@ -16,11 +16,13 @@ import { ComponentNamePlugin } from './plugins/component-names'
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch { return false } }
const SLASH_SEPARATOR_RE = /[\\/]/
function compareDirByPathLength ({ path: pathA }: { path: string }, { path: pathB }: { path: string }) {
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
return pathB.split(SLASH_SEPARATOR_RE).filter(Boolean).length - pathA.split(SLASH_SEPARATOR_RE).filter(Boolean).length
}
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/
const STARTER_DOT_RE = /^\./g
export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
@ -89,7 +91,7 @@ export default defineNuxtModule<ComponentsOptions>({
const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir }
const dirPath = resolveAlias(dirOptions.path)
const transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto'
const extensions = (dirOptions.extensions || nuxt.options.extensions).map(e => e.replace(/^\./g, ''))
const extensions = (dirOptions.extensions || nuxt.options.extensions).map(e => e.replace(STARTER_DOT_RE, ''))
const present = isDirectory(dirPath)
if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) {

View File

@ -12,6 +12,7 @@ interface LoaderOptions {
}
const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
const UID_RE = / :?uid=/
export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || []
const include = options.transform?.include || []
@ -37,7 +38,7 @@ export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnpl
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
count++
if (/ :?uid=/.test(attrs)) { return full }
if (UID_RE.test(attrs)) { return full }
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
})

View File

@ -1,12 +1,13 @@
import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import type { Component } from 'nuxt/schema'
import { isVue } from '../../core/utils'
import { SX_RE, isVue } from '../../core/utils'
interface NameDevPluginOptions {
sourcemap: boolean
getComponents: () => Component[]
}
const FILENAME_RE = /([^/\\]+)\.\w+$/
/**
* Set the default name of components to their PascalCase name
*/
@ -15,10 +16,10 @@ export const ComponentNamePlugin = (options: NameDevPluginOptions) => createUnpl
name: 'nuxt:component-name-plugin',
enforce: 'post',
transformInclude (id) {
return isVue(id) || !!id.match(/\.[tj]sx$/)
return isVue(id) || !!id.match(SX_RE)
},
transform (code, id) {
const filename = id.match(/([^/\\]+)\.\w+$/)?.[1]
const filename = id.match(FILENAME_RE)?.[1]
if (!filename) {
return
}

View File

@ -30,6 +30,7 @@ const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g
const IMPORT_CODE = '\nimport { mergeProps as __mergeProps } from \'vue\'' + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandComponent from \'#app/components/nuxt-teleport-island-component\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\''
const EXTRACTED_ATTRS_RE = /v-(?:if|else-if|else)(="[^"]*")?/g
const KEY_RE = /:?key="[^"]"/g
function wrapWithVForDiv (code: string, vfor: string): string {
return `<div v-for="${vfor}" style="display: contents;">${code}</div>`
@ -90,7 +91,7 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
if (children.length) {
// pass slot fallback to NuxtTeleportSsrSlot fallback
const attrString = attributeToString(attributes)
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '')
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(KEY_RE, '')
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot${attrString.replaceAll(EXTRACTED_ATTRS_RE, '')}/><template #fallback>${attributes['v-for'] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}</template>`)
} else {
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replaceAll(EXTRACTED_ATTRS_RE, ''))

View File

@ -6,7 +6,7 @@ import { relative } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit'
import { isVue } from '../../core/utils'
import { QUOTE_RE, SX_RE, isVue } from '../../core/utils'
interface LoaderOptions {
getComponents (): Component[]
@ -17,6 +17,7 @@ interface LoaderOptions {
experimentalComponentIslands?: boolean
}
const REPLACE_COMPONENT_TO_DIRECT_IMPORT_RE = /(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g
export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || []
const include = options.transform?.include || []
@ -32,7 +33,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
if (include.some(pattern => pattern.test(id))) {
return true
}
return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/)
return isVue(id, { type: ['template', 'script'] }) || !!id.match(SX_RE)
},
transform (code, id) {
const components = options.getComponents()
@ -43,7 +44,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const s = new MagicString(code)
// replace `_resolveComponent("...")` to direct import
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => {
s.replace(REPLACE_COMPONENT_TO_DIRECT_IMPORT_RE, (full: string, lazy: string, name: string) => {
const component = findComponent(components, name, options.mode)
if (component) {
// TODO: refactor to nuxi
@ -111,7 +112,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
})
function findComponent (components: Component[], name: string, mode: LoaderOptions['mode']) {
const id = pascalCase(name).replace(/["']/g, '')
const id = pascalCase(name).replace(QUOTE_RE, '')
// Prefer exact match
const component = components.find(component => id === component.pascalName && ['all', mode, undefined].includes(component.mode))
if (component) { return component }

View File

@ -6,8 +6,12 @@ import { isIgnored, logger, useNuxt } from '@nuxt/kit'
import { withTrailingSlash } from 'ufo'
import type { Component, ComponentsDir } from 'nuxt/schema'
import { resolveComponentNameSegments } from '../core/utils'
import { QUOTE_RE, resolveComponentNameSegments } from '../core/utils'
const ISLAND_RE = /\.island(?:\.global)?$/
const GLOBAL_RE = /\.global(?:\.island)?$/
const COMPONENT_MODE_RE = /(?<=\.)(client|server)(\.global|\.island)*$/
const MODE_REPLACEMENT_RE = /(\.(client|server))?(\.global|\.island)*$/
/**
* Scan the components inside different components folders
* and return a unique list of components
@ -83,17 +87,17 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
*/
let fileName = basename(filePath, extname(filePath))
const island = /\.island(?:\.global)?$/.test(fileName) || dir.island
const global = /\.global(?:\.island)?$/.test(fileName) || dir.global
const mode = island ? 'server' : (fileName.match(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || 'all') as 'client' | 'server' | 'all'
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '')
const island = ISLAND_RE.test(fileName) || dir.island
const global = GLOBAL_RE.test(fileName) || dir.global
const mode = island ? 'server' : (fileName.match(COMPONENT_MODE_RE)?.[1] || 'all') as 'client' | 'server' | 'all'
fileName = fileName.replace(MODE_REPLACEMENT_RE, '')
if (fileName.toLowerCase() === 'index') {
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
}
const suffix = (mode !== 'all' ? `-${mode}` : '')
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)
const componentNameSegments = resolveComponentNameSegments(fileName.replace(QUOTE_RE, ''), prefixParts)
const pascalName = pascalCase(componentNameSegments)
if (LAZY_COMPONENT_NAME_REGEX.test(pascalName)) {

View File

@ -102,14 +102,15 @@ export const componentsIslandsTemplate: NuxtTemplate = {
},
}
const NON_VUE_RE = /\b\.(?!vue)\w+$/g
export const componentsTypeTemplate = {
filename: 'components.d.ts' as const,
getContents: ({ app, nuxt }) => {
const buildDir = nuxt.options.buildDir
const componentTypes = app.components.filter(c => !c.island).map((c) => {
const type = `typeof ${genDynamicImport(isAbsolute(c.filePath)
? relative(buildDir, c.filePath).replace(/\b\.(?!vue)\w+$/g, '')
: c.filePath.replace(/\b\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']`
? relative(buildDir, c.filePath).replace(NON_VUE_RE, '')
: c.filePath.replace(NON_VUE_RE, ''), { wrapper: false })}['${c.export}']`
return [
c.pascalName,
c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type,

View File

@ -57,7 +57,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
const writes: Array<() => void> = []
const changedTemplates: Array<ResolvedNuxtTemplate<any>> = []
const FORWARD_SLASH_RE = /\//g
async function processTemplate (template: ResolvedNuxtTemplate) {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const start = performance.now()
@ -77,7 +77,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
// In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') {
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
nuxt.vfs[fullPath.replace(FORWARD_SLASH_RE, '\\')] = contents
}
changedTemplates.push(template)

View File

@ -10,6 +10,7 @@ import { generateApp as _generateApp, createApp } from './app'
import { checkForExternalConfigurationFiles } from './external-config-files'
import { cleanupCaches, getVueHash } from './cache'
const IS_RESTART_PATH_RE = /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i
export async function build (nuxt: Nuxt) {
const app = createApp(nuxt)
nuxt.apps.default = app
@ -23,7 +24,7 @@ export async function build (nuxt: Nuxt) {
if (event === 'change') { return }
const path = resolve(nuxt.options.srcDir, relativePath)
const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
const restartPath = relativePaths.find(relativePath => /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath))
const restartPath = relativePaths.find(relativePath => IS_RESTART_PATH_RE.test(relativePath))
if (restartPath) {
if (restartPath.startsWith('app')) {
app.mainComponent = undefined

View File

@ -17,7 +17,8 @@ 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 { nuxtImportProtections } from './plugins/import-protection'
import { createImportProtectionPatterns } from './plugins/import-protection'
import { EXTENSION_RE } from './utils'
const logLevelMapReverse = {
silent: 0,
@ -25,12 +26,14 @@ const logLevelMapReverse = {
verbose: 3,
} satisfies Record<NuxtOptions['logLevel'], NitroConfig['logLevel']>
const NODE_MODULES_RE = /(?<=\/)node_modules\/(.+)$/
const PNPM_NODE_MODULES_RE = /\.pnpm\/.+\/node_modules\/(.+)$/
export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve config
const excludePaths = nuxt.options._layers
.flatMap(l => [
l.cwd.match(/(?<=\/)node_modules\/(.+)$/)?.[1],
l.cwd.match(/\.pnpm\/.+\/node_modules\/(.+)$/)?.[1],
l.cwd.match(NODE_MODULES_RE)?.[1],
l.cwd.match(PNPM_NODE_MODULES_RE)?.[1],
])
.filter((dir): dir is string => Boolean(dir))
.map(dir => escapeRE(dir))
@ -46,6 +49,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
.map(m => m.entryPath!),
)
const isNuxtV4 = nuxt.options.future?.compatibilityVersion === 4
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {
debug: nuxt.options.debug,
rootDir: nuxt.options.rootDir,
@ -63,6 +68,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
},
imports: {
autoImport: nuxt.options.imports.autoImport as boolean,
dirs: isNuxtV4
? [
resolve(nuxt.options.rootDir, 'shared', 'utils'),
resolve(nuxt.options.rootDir, 'shared', 'types'),
]
: [],
imports: [
{
as: '__buildAssetsURL',
@ -339,11 +350,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}
// Add fallback server for `ssr: false`
const FORWARD_SLASH_RE = /\//g
if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
// In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') {
nitroConfig.virtual!['#build/dist/server/server.mjs'.replace(/\//g, '\\')] = 'export default () => {}'
nitroConfig.virtual!['#build/dist/server/server.mjs'.replace(FORWARD_SLASH_RE, '\\')] = 'export default () => {}'
}
}
@ -351,18 +363,27 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitroConfig.virtual!['#build/dist/server/styles.mjs'] = 'export default {}'
// In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') {
nitroConfig.virtual!['#build/dist/server/styles.mjs'.replace(/\//g, '\\')] = 'export default {}'
nitroConfig.virtual!['#build/dist/server/styles.mjs'.replace(FORWARD_SLASH_RE, '\\')] = 'export default {}'
}
}
// Register nuxt protection patterns
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)
const sharedDir = withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared))
const relativeSharedDir = withTrailingSlash(relative(nuxt.options.rootDir, resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))
const sharedPatterns = [/^#shared\//, new RegExp('^' + escapeRE(sharedDir)), new RegExp('^' + escapeRE(relativeSharedDir))]
nitroConfig.rollupConfig!.plugins!.push(
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
patterns: nuxtImportProtections(nuxt, { isNitro: true }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
include: sharedPatterns,
patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
}),
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
patterns: createImportProtectionPatterns(nuxt, { context: 'nitro-app' }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/, ...sharedPatterns],
}),
)
@ -389,7 +410,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
tsConfig.compilerOptions.paths[alias] = [absolutePath]
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`]
} else {
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/\b\.\w+$/g, '')] /* remove extension */
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(EXTENSION_RE, '')] /* remove extension */
}
}
@ -566,8 +587,9 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}
}
const RELATIVE_RE = /^([^.])/
function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.'
return relative(from, to).replace(RELATIVE_RE, './$1') || '.'
}
async function spaLoadingTemplatePath (nuxt: Nuxt) {

View File

@ -18,7 +18,6 @@ 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'
@ -32,7 +31,7 @@ import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json'
import { scriptsStubsPreset } from '../imports/presets'
import { resolveTypePath } from './utils/types'
import { nuxtImportProtections } from './plugins/import-protection'
import { createImportProtectionPatterns } from './plugins/import-protection'
import { UnctxTransformPlugin } from './plugins/unctx'
import { TreeShakeComposablesPlugin } from './plugins/tree-shake'
import { DevOnlyPlugin } from './plugins/dev-only'
@ -178,9 +177,10 @@ async function initNuxt (nuxt: Nuxt) {
const coreTypePackages = nuxt.options.typescript.hoist || []
const packageJSON = await readPackageJSON(nuxt.options.rootDir).catch(() => ({}) as PackageJson)
const NESTED_PKG_RE = /^[^@]+\//
nuxt._dependencies = new Set([...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.devDependencies || {})])
const paths = Object.fromEntries(await Promise.all(coreTypePackages.map(async (pkg) => {
const [_pkg = pkg, _subpath] = /^[^@]+\//.test(pkg) ? pkg.split('/') : [pkg]
const [_pkg = pkg, _subpath] = NESTED_PKG_RE.test(pkg) ? pkg.split('/') : [pkg]
const subpath = _subpath ? '/' + _subpath : ''
// ignore packages that exist in `package.json` as these can be resolved by TypeScript
@ -248,16 +248,28 @@ async function initNuxt (nuxt: Nuxt) {
// Add plugin normalization plugin
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
// shared folder import protection
const sharedDir = withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared))
const relativeSharedDir = withTrailingSlash(relative(nuxt.options.rootDir, resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))
const sharedPatterns = [/^#shared\//, new RegExp('^' + escapeRE(sharedDir)), new RegExp('^' + escapeRE(relativeSharedDir))]
const sharedProtectionConfig = {
cwd: nuxt.options.rootDir,
include: sharedPatterns,
patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
}
addVitePlugin(() => ImpoundPlugin.vite(sharedProtectionConfig), { server: false })
addWebpackPlugin(() => ImpoundPlugin.webpack(sharedProtectionConfig), { server: false })
// Add import protection
const config: ImpoundOptions = {
const nuxtProtectionConfig = {
cwd: nuxt.options.rootDir,
// Exclude top-level resolutions by plugins
exclude: [join(nuxt.options.srcDir, 'index.html')],
patterns: nuxtImportProtections(nuxt),
exclude: [relative(nuxt.options.rootDir, join(nuxt.options.srcDir, 'index.html')), ...sharedPatterns],
patterns: createImportProtectionPatterns(nuxt, { context: 'nuxt-app' }),
}
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))
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: false }), { name: 'nuxt:import-protection' }), { client: false })
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: true }), { name: 'nuxt:import-protection' }), { server: false })
addWebpackPlugin(() => ImpoundPlugin.webpack(nuxtProtectionConfig))
// add resolver for modules used in virtual files
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false })
@ -564,6 +576,11 @@ async function initNuxt (nuxt: Nuxt) {
if (nuxt.options.experimental.emitRouteChunkError === 'automatic') {
addPlugin(resolve(nuxt.options.appDir, 'plugins/chunk-reload.client'))
}
// Add experimental immediate page reload support
if (nuxt.options.experimental.emitRouteChunkError === 'automatic-immediate') {
addPlugin(resolve(nuxt.options.appDir, 'plugins/chunk-reload-immediate.client'))
}
// Add experimental session restoration support
if (nuxt.options.experimental.restoreState) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/restore-state.client'))

View File

@ -9,12 +9,17 @@ interface ImportProtectionOptions {
exclude?: Array<RegExp | string>
}
export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: { isNitro?: boolean } = {}) => {
interface NuxtImportProtectionOptions {
context: 'nuxt-app' | 'nitro-app' | 'shared'
}
export const createImportProtectionPatterns = (nuxt: { options: NuxtOptions }, options: NuxtImportProtectionOptions) => {
const patterns: ImportProtectionOptions['patterns'] = []
const context = contextFlags[options.context]
patterns.push([
/^(nuxt|nuxt3|nuxt-nightly)$/,
'`nuxt`, `nuxt3` or `nuxt-nightly` cannot be imported directly.' + (options.isNitro ? '' : ' Instead, import runtime Nuxt composables from `#app` or `#imports`.'),
`\`nuxt\`, or \`nuxt-nightly\` cannot be imported directly in ${context}.` + (options.context === 'nuxt-app' ? ' Instead, import runtime Nuxt composables from `#app` or `#imports`.' : ''),
])
patterns.push([
@ -26,27 +31,33 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
for (const mod of nuxt.options.modules.filter(m => typeof m === 'string')) {
patterns.push([
new RegExp(`^${escapeRE(mod as string)}$`),
new RegExp(`^${escapeRE(mod)}$`),
'Importing directly from module entry-points is not allowed.',
])
}
for (const i of [/(^|node_modules\/)@nuxt\/(kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?runtime|types)/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {
patterns.push([i, 'This module cannot be imported' + (options.isNitro ? ' in server runtime.' : ' in the Vue part of your app.')])
for (const i of [/(^|node_modules\/)@nuxt\/(kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?(?:presets|runtime|types))/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {
patterns.push([i, `This module cannot be imported in ${context}.`])
}
if (options.isNitro) {
if (options.context === 'nitro-app' || options.context === 'shared') {
for (const i of ['#app', /^#build(\/|$)/]) {
patterns.push([i, 'Vue app aliases are not allowed in server runtime.'])
patterns.push([i, `Vue app aliases are not allowed in ${context}.`])
}
}
if (!options.isNitro) {
if (options.context === 'nuxt-app' || options.context === 'shared') {
patterns.push([
new RegExp(escapeRE(relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, nuxt.options.serverDir || 'server'))) + '\\/(api|routes|middleware|plugins)\\/'),
'Importing from server is not allowed in the Vue part of your app.',
`Importing from server is not allowed in ${context}.`,
])
}
return patterns
}
const contextFlags = {
'nitro-app': 'server runtime',
'nuxt-app': 'the Vue part of your app',
'shared': 'the #shared directory',
} as const

View File

@ -10,6 +10,7 @@ interface VirtualFSPluginOptions {
alias?: Record<string, string>
}
const RELATIVE_ID_RE = /^\.{1,2}[\\/]/
export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) => createUnplugin(() => {
const extensions = ['', ...nuxt.options.extensions]
const alias = { ...nuxt.options.alias, ...options.alias }
@ -40,7 +41,7 @@ export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) =>
return PREFIX + resolvedId
}
if (importer && /^\.{1,2}[\\/]/.test(id)) {
if (importer && RELATIVE_ID_RE.test(id)) {
const path = resolve(dirname(withoutPrefix(importer)), id)
const resolved = resolveWithExt(path)
if (resolved) {

View File

@ -11,6 +11,7 @@ import type { NuxtTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitro/types'
import { annotatePlugins, checkForCircularDependencies } from './app'
import { EXTENSION_RE } from './utils'
export const vueShim: NuxtTemplate = {
filename: 'types/vue-shim.d.ts',
@ -57,6 +58,7 @@ export const cssTemplate: NuxtTemplate = {
getContents: ctx => ctx.nuxt.options.css.map(i => genImport(i)).join('\n'),
}
const PLUGIN_TEMPLATE_RE = /_(45|46|47)/g
export const clientPluginTemplate: NuxtTemplate = {
filename: 'plugins.client.mjs',
async getContents (ctx) {
@ -66,7 +68,7 @@ export const clientPluginTemplate: NuxtTemplate = {
const imports: string[] = []
for (const plugin of clientPlugins) {
const path = relative(ctx.nuxt.options.rootDir, plugin.src)
const variable = genSafeVariableName(filename(plugin.src)).replace(/_(45|46|47)/g, '_') + '_' + hash(path)
const variable = genSafeVariableName(filename(plugin.src)).replace(PLUGIN_TEMPLATE_RE, '_') + '_' + hash(path)
exports.push(variable)
imports.push(genImport(plugin.src, variable))
}
@ -86,7 +88,7 @@ export const serverPluginTemplate: NuxtTemplate = {
const imports: string[] = []
for (const plugin of serverPlugins) {
const path = relative(ctx.nuxt.options.rootDir, plugin.src)
const variable = genSafeVariableName(filename(path)).replace(/_(45|46|47)/g, '_') + '_' + hash(path)
const variable = genSafeVariableName(filename(path)).replace(PLUGIN_TEMPLATE_RE, '_') + '_' + hash(path)
exports.push(variable)
imports.push(genImport(plugin.src, variable))
}
@ -98,7 +100,9 @@ export const serverPluginTemplate: NuxtTemplate = {
}
const TS_RE = /\.[cm]?tsx?$/
const JS_LETTER_RE = /\.(?<letter>[cm])?jsx?$/
const JS_RE = /\.[cm]jsx?$/
const JS_CAPTURE_RE = /\.[cm](jsx?)$/
export const pluginsDeclaration: NuxtTemplate = {
filename: 'types/plugins.d.ts',
getContents: async ({ nuxt, app }) => {
@ -120,18 +124,18 @@ export const pluginsDeclaration: NuxtTemplate = {
const pluginPath = resolve(typesDir, plugin.src)
const relativePath = relative(typesDir, pluginPath)
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts')
const correspondingDeclaration = pluginPath.replace(JS_LETTER_RE, '.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')
const incorrectDeclaration = pluginPath.replace(JS_RE, '.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'))
tsImports.push(relativePath.replace(JS_CAPTURE_RE, '.$1'))
continue
}
@ -174,11 +178,13 @@ export { }
}
const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-config-schema']
const IMPORT_NAME_RE = /\.\w+$/
const GIT_RE = /^git\+/
export const schemaTemplate: NuxtTemplate = {
filename: 'types/schema.d.ts',
getContents: async ({ nuxt }) => {
const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir)
const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(/\.\w+$/, '')
const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(IMPORT_NAME_RE, '')
const modules = nuxt.options._installedModules
.filter(m => m.meta && m.meta.configKey && m.meta.name && !adHocModules.includes(m.meta.name))
@ -210,7 +216,7 @@ export const schemaTemplate: NuxtTemplate = {
}
if (link) {
if (link.startsWith('git+')) {
link = link.replace(/^git\+/, '')
link = link.replace(GIT_RE, '')
}
if (!link.startsWith('http')) {
link = 'https://github.com/' + link
@ -377,7 +383,7 @@ export const appConfigDeclarationTemplate: NuxtTemplate = {
filename: 'types/app.config.d.ts',
getContents ({ app, nuxt }) {
const typesDir = join(nuxt.options.buildDir, 'types')
const configPaths = app.configs.map(path => relative(typesDir, path).replace(/\b\.\w+$/g, ''))
const configPaths = app.configs.map(path => relative(typesDir, path).replace(EXTENSION_RE, ''))
return `
import type { CustomAppConfig } from 'nuxt/schema'

View File

@ -14,3 +14,7 @@ export function uniqueBy<T, K extends keyof T> (arr: T[], key: K) {
}
return res
}
export const QUOTE_RE = /["']/g
export const EXTENSION_RE = /\b\.\w+$/g
export const SX_RE = /\.[tj]sx$/

View File

@ -1,6 +1,7 @@
import { basename, dirname, extname, normalize } from 'pathe'
import { kebabCase, splitByCase } from 'scule'
import { withTrailingSlash } from 'ufo'
import { QUOTE_RE } from '.'
export function getNameFromPath (path: string, relativeTo?: string) {
const relativePath = relativeTo
@ -9,7 +10,7 @@ export function getNameFromPath (path: string, relativeTo?: string) {
const prefixParts = splitByCase(dirname(relativePath))
const fileName = basename(relativePath, extname(relativePath))
const segments = resolveComponentNameSegments(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts).filter(Boolean)
return kebabCase(segments).replace(/["']/g, '')
return kebabCase(segments).replace(QUOTE_RE, '')
}
export function hasSuffix (path: string, suffix: string) {

View File

@ -54,6 +54,8 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
await nuxt.callHook('imports:context', ctx)
const isNuxtV4 = nuxt.options.future?.compatibilityVersion === 4
// composables/ dirs from all layers
let composablesDirs: string[] = []
if (options.scan) {
@ -64,6 +66,12 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
}
composablesDirs.push(resolve(layer.config.srcDir, 'composables'))
composablesDirs.push(resolve(layer.config.srcDir, 'utils'))
if (isNuxtV4) {
composablesDirs.push(resolve(layer.config.rootDir, 'shared', 'utils'))
composablesDirs.push(resolve(layer.config.rootDir, 'shared', 'types'))
}
for (const dir of (layer.config.imports?.dirs ?? [])) {
if (!dir) {
continue

View File

@ -109,6 +109,10 @@ const granularAppPresets: InlinePreset[] = [
imports: ['useRouteAnnouncer'],
from: '#app/composables/route-announcer',
},
{
imports: ['useRuntimeHook'],
from: '#app/composables/runtime-hook',
},
]
export const scriptsStubsPreset = {
@ -216,9 +220,6 @@ const vuePreset = defineUnimportPreset({
'hasInjectionContext',
'nextTick',
'provide',
'defineModel',
'defineOptions',
'defineSlots',
'mergeModels',
'toValue',
'useModel',

View File

@ -503,7 +503,7 @@ export default defineNuxtModule({
const { routes, imports } = normalizeRoutes(app.pages, new Set(), {
serverComponentRuntime,
clientComponentRuntime,
overrideMeta: nuxt.options.experimental.scanPageMeta,
overrideMeta: !!nuxt.options.experimental.scanPageMeta,
})
return [...imports, `export default ${routes}`].join('\n')
},

View File

@ -176,8 +176,10 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
// https://github.com/vuejs/vue-loader/pull/1911
// https://github.com/vitejs/vite/issues/8473
const QUERY_START_RE = /^\?/
const MACRO_RE = /&macro=true/
function rewriteQuery (id: string) {
return id.replace(/\?.+$/, r => '?macro=true&' + r.replace(/^\?/, '').replace(/&macro=true/, ''))
return id.replace(/\?.+$/, r => '?macro=true&' + r.replace(QUERY_START_RE, '').replace(MACRO_RE, ''))
}
function parseMacroQuery (id: string) {
@ -189,6 +191,7 @@ function parseMacroQuery (id: string) {
return query
}
const QUOTED_SPECIFIER_RE = /(["']).*\1/
function getQuotedSpecifier (id: string) {
return id.match(/(["']).*\1/)?.[0]
return id.match(QUOTED_SPECIFIER_RE)?.[0]
}

View File

@ -1,6 +1,6 @@
import type { KeepAliveProps, TransitionProps, UnwrapRef } from 'vue'
import { getCurrentInstance } from 'vue'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from 'vue-router'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router'
import { useRoute } from 'vue-router'
import type { NitroRouteConfig } from 'nitro/types'
import { useNuxtApp } from '#app/nuxt'
@ -37,6 +37,11 @@ export interface PageMeta {
name?: string
/** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */
path?: string
/**
* Allows accessing the route `params` as props passed to the page component.
* @see https://router.vuejs.org/guide/essentials/passing-props
*/
props?: RouteRecordRaw['props']
/** Set to `false` to avoid scrolling to top on page navigations */
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
}

View File

@ -5,11 +5,14 @@ type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
type RouterViewSlot = Exclude<InstanceOf<typeof RouterView>['$slots']['default'], undefined>
export type RouterViewSlotProps = Parameters<RouterViewSlot>[0]
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g
const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g
const ROUTE_KEY_NORMAL_RE = /:\w+/g
const interpolatePath = (route: RouteLocationNormalizedLoaded, match: RouteLocationMatched) => {
return match.path
.replace(/(:\w+)\([^)]+\)/g, '$1')
.replace(/(:\w+)[?+*]/g, '$1')
.replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '')
.replace(ROUTE_KEY_PARENTHESES_RE, '$1')
.replace(ROUTE_KEY_SYMBOLS_RE, '$1')
.replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '')
}
export const generateRouteKey = (routeProps: RouterViewSlotProps, override?: string | ((route: RouteLocationNormalizedLoaded) => string)) => {

View File

@ -64,18 +64,25 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
})
const pages = uniqueBy(allRoutes, 'path')
const shouldAugment = nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages
if (shouldAugment) {
if (shouldAugment === false) {
await nuxt.callHook('pages:extend', pages)
return pages
}
if (shouldAugment === 'after-resolve') {
await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs)
} else {
const augmentedPages = await augmentPages(pages, nuxt.vfs)
await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs, augmentedPages)
augmentedPages.clear()
} else {
await nuxt.callHook('pages:extend', pages)
}
await nuxt.callHook('pages:resolved', pages)
return pages
}
@ -83,6 +90,7 @@ type GenerateRoutesFromFilesOptions = {
shouldUseServerComponents?: boolean
}
const INDEX_PAGE_RE = /\/index$/
export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] {
const routes: NuxtPage[] = []
@ -128,7 +136,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
route.name += (route.name && '/') + segmentName
// ex: parent.vue + parent/child.vue
const path = withLeadingSlash(joinURL(route.path, getRoutePath(tokens).replace(/\/index$/, '/')))
const path = withLeadingSlash(joinURL(route.path, getRoutePath(tokens).replace(INDEX_PAGE_RE, '/')))
const child = parent.find(parentRoute => parentRoute.name === route.name && parentRoute.path === path)
if (child && child.children) {
@ -183,7 +191,7 @@ export function extractScriptContent (html: string) {
}
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
const extractionKeys = ['name', 'path', 'props', 'alias', 'redirect'] as const
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {}
@ -265,7 +273,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
continue
}
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
if (property.value.type !== 'Literal' || (typeof property.value.value !== 'string' && typeof property.value.value !== 'boolean')) {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
@ -300,6 +308,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
return extractedMeta
}
const COLON_RE = /:/g
function getRoutePath (tokens: SegmentToken[]): string {
return tokens.reduce((path, token) => {
return (
@ -312,7 +321,7 @@ function getRoutePath (tokens: SegmentToken[]): string {
? `:${token.value}(.*)*`
: token.type === SegmentTokenType.group
? ''
: encodePath(token.value).replace(/:/g, '\\:'))
: encodePath(token.value).replace(COLON_RE, '\\:'))
)
}, '/')
}
@ -432,13 +441,14 @@ function findRouteByName (name: string, routes: NuxtPage[]): NuxtPage | undefine
return findRouteByName(name, routes)
}
const NESTED_PAGE_RE = /\//g
function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<string>()) {
for (const route of routes) {
// Remove -index
if (route.name) {
route.name = route.name
.replace(/\/index$/, '')
.replace(/\//g, '-')
.replace(INDEX_PAGE_RE, '')
.replace(NESTED_PAGE_RE, '-')
if (names.has(route.name)) {
const existingRoute = findRouteByName(route.name, routes)
@ -497,13 +507,14 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
const route: NormalizedRoute = {
path: serializeRouteValue(page.path),
props: serializeRouteValue(page.props),
name: serializeRouteValue(page.name),
meta: serializeRouteValue(metaFiltered, skipMeta),
alias: serializeRouteValue(toArray(page.alias), skipAlias),
redirect: serializeRouteValue(page.redirect),
}
for (const key of ['path', 'name', 'meta', 'alias', 'redirect'] satisfies NormalizedRouteKeys) {
for (const key of ['path', 'props', 'name', 'meta', 'alias', 'redirect'] satisfies NormalizedRouteKeys) {
if (route[key] === undefined) {
delete route[key]
}
@ -532,6 +543,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
const metaRoute: NormalizedRoute = {
name: `${metaImportName}?.name ?? ${route.name}`,
path: `${metaImportName}?.path ?? ${route.path}`,
props: `${metaImportName}?.props ?? ${route.props ?? false}`,
meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`,
@ -575,7 +587,7 @@ async function createClientPage(loader) {
}
// set to extracted value or delete if none extracted
for (const key of ['meta', 'alias', 'redirect'] satisfies NormalizedRouteKeys) {
for (const key of ['meta', 'alias', 'redirect', 'props'] satisfies NormalizedRouteKeys) {
if (markedDynamic.has(key)) { continue }
if (route[key] == null) {
@ -600,6 +612,7 @@ async function createClientPage(loader) {
}
}
const PATH_TO_NITRO_GLOB_RE = /\/[^:/]*:\w.*$/
export function pathToNitroGlob (path: string) {
if (!path) {
return null
@ -609,7 +622,7 @@ export function pathToNitroGlob (path: string) {
return null
}
return path.replace(/\/[^:/]*:\w.*$/, '/**')
return path.replace(PATH_TO_NITRO_GLOB_RE, '/**')
}
export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] {

View File

@ -6,6 +6,7 @@
"meta": "{ ...(mockMeta || {}), ...{"someMetaData":true} }",
"name": "mockMeta?.name ?? "pushed-route"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -24,6 +25,18 @@
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
"route.meta props generate by file": [
{
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/page-with-props.vue")",
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "page-with-props"",
"path": "mockMeta?.path ?? "/page-with-props"",
"props": "mockMeta?.props ?? true",
"redirect": "mockMeta?.redirect",
},
],
@ -34,6 +47,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "test:name"",
"path": "mockMeta?.path ?? "/test\\:name"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -50,6 +64,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-index"",
"path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -58,6 +73,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-index-sibling"",
"path": "mockMeta?.path ?? "sibling"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -65,6 +81,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -73,6 +90,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-sibling"",
"path": "mockMeta?.path ?? "sibling"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -80,6 +98,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/param"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -91,6 +110,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "wrapper-expose-other"",
"path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -99,6 +119,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "wrapper-expose-other-sibling"",
"path": "mockMeta?.path ?? "sibling"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -106,6 +127,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/wrapper-expose/other"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -116,6 +138,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "home"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": ""/"",
},
],
@ -126,6 +149,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "slug"",
"path": "mockMeta?.path ?? "/:slug(.*)*"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -134,6 +158,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -144,6 +169,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -152,6 +178,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "slug"",
"path": "mockMeta?.path ?? "/:slug()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -163,6 +190,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -170,6 +198,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/:foo?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -178,6 +207,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-opt"",
"path": "mockMeta?.path ?? "/optional/:opt?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -186,6 +216,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-prefix-opt"",
"path": "mockMeta?.path ?? "/optional/prefix-:opt?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -194,6 +225,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-opt-postfix"",
"path": "mockMeta?.path ?? "/optional/:opt?-postfix"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -202,6 +234,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-prefix-opt-postfix"",
"path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -210,6 +243,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "bar"",
"path": "mockMeta?.path ?? "/:bar()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -218,6 +252,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "nonopt-slug"",
"path": "mockMeta?.path ?? "/nonopt/:slug()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -226,6 +261,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "opt-slug"",
"path": "mockMeta?.path ?? "/opt/:slug?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -234,6 +270,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "sub-route-slug"",
"path": "mockMeta?.path ?? "/:sub?/route-:slug()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -244,6 +281,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories"",
"path": "mockMeta?.path ?? "/:stories(.*)*"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -252,6 +290,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories-id"",
"path": "mockMeta?.path ?? "/stories/:id()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -262,6 +301,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories-id"",
"path": "mockMeta?.path ?? "/stories/:id()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -270,6 +310,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories"",
"path": "mockMeta?.path ?? "/:stories(.*)*"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -280,6 +321,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "kebab-case"",
"path": "mockMeta?.path ?? "/kebab-case"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -290,6 +332,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "snake_case"",
"path": "mockMeta?.path ?? "/snake_case"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -300,6 +343,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -308,6 +352,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent"",
"path": "mockMeta?.path ?? "/parent"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -316,6 +361,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent/child"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -329,6 +375,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "child"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -336,6 +383,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent"",
"path": "mockMeta?.path ?? "/parent"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -346,6 +394,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -357,6 +406,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "about"",
"path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -364,6 +414,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/about"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -377,6 +428,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index-index-all"",
"path": "mockMeta?.path ?? "all"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -384,6 +436,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -394,6 +447,7 @@
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -404,6 +458,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent/:child()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -412,6 +467,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent-:child()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -422,6 +478,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? "/:foo?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -430,6 +487,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? "/:foo()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -440,6 +498,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "a1_1a"",
"path": "mockMeta?.path ?? "/:a1_1a()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -448,6 +507,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "b2.2b"",
"path": "mockMeta?.path ?? "/:b2.2b()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -456,6 +516,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "b2_2b"",
"path": "mockMeta?.path ?? "/:b2()_:2b()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -464,6 +525,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "c33c"",
"path": "mockMeta?.path ?? "/:c33c?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
{
@ -472,6 +534,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "d44d"",
"path": "mockMeta?.path ?? "/:d44d?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -482,6 +545,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "home"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
@ -492,6 +556,7 @@
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],

View File

@ -24,6 +24,14 @@
"path": ""/page-with-meta"",
},
],
"route.meta props generate by file": [
{
"component": "() => import("pages/page-with-props.vue")",
"name": ""page-with-props"",
"path": ""/page-with-props"",
"props": "true",
},
],
"should allow pages with `:` in their path": [
{
"component": "() => import("pages/test:name.vue")",

View File

@ -86,7 +86,10 @@ const excludedVueHelpers = [
// Already globally registered
'defineEmits',
'defineExpose',
'defineModel',
'defineOptions',
'defineProps',
'defineSlots',
'withDefaults',
'stop',
//

View File

@ -1,7 +1,7 @@
import { normalize } from 'pathe'
import { describe, expect, it } from 'vitest'
import { ImpoundPlugin } from 'impound'
import { nuxtImportProtections } from '../src/core/plugins/import-protection'
import { createImportProtectionPatterns } from '../src/core/plugins/import-protection'
import type { NuxtOptions } from '../schema'
const testsToTriggerOn = [
@ -28,7 +28,7 @@ const testsToTriggerOn = [
describe('import protection', () => {
it.each(testsToTriggerOn)('should protect %s', async (id, importer, isProtected) => {
const result = await transformWithImportProtection(id, importer)
const result = await transformWithImportProtection(id, importer, 'nuxt-app')
if (!isProtected) {
expect(result).toBeNull()
} else {
@ -38,16 +38,16 @@ describe('import protection', () => {
})
})
const transformWithImportProtection = (id: string, importer: string) => {
const transformWithImportProtection = (id: string, importer: string, context: 'nitro-app' | 'nuxt-app' | 'shared') => {
const plugin = ImpoundPlugin.rollup({
cwd: '/root',
patterns: nuxtImportProtections({
patterns: createImportProtectionPatterns({
options: {
modules: ['some-nuxt-module'],
srcDir: '/root/src/',
serverDir: '/root/src/server',
} satisfies Partial<NuxtOptions> as NuxtOptions,
}),
}, { context }),
})
return (plugin as any).resolveId.call({ error: () => {} }, id, importer)

View File

@ -211,6 +211,7 @@ describe('normalizeRoutes', () => {
{
name: indexN6pT4Un8hYMeta?.name ?? undefined,
path: indexN6pT4Un8hYMeta?.path ?? "/",
props: indexN6pT4Un8hYMeta?.props ?? false,
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
alias: indexN6pT4Un8hYMeta?.alias || [],
redirect: indexN6pT4Un8hYMeta?.redirect,

View File

@ -601,6 +601,30 @@ describe('pages:generateRoutesFromFiles', () => {
},
],
},
{
description: 'route.meta props generate by file',
files: [
{
path: `${pagesDir}/page-with-props.vue`,
template: `
<script setup lang="ts">
definePageMeta({
props: true
})
</script>
`,
},
],
output: [
{
name: 'page-with-props',
path: '/page-with-props',
file: `${pagesDir}/page-with-props.vue`,
children: [],
props: true,
},
],
},
{
description: 'should handle route groups',
files: [

View File

@ -31,7 +31,7 @@
"dependencies": {
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*",
"@rspack/core": "^1.0.10",
"@rspack/core": "^1.0.14",
"autoprefixer": "^10.4.20",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
@ -45,11 +45,11 @@
"globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hash-sum": "^2.0.0",
"jiti": "^2.3.3",
"jiti": "^2.4.0",
"knitwork": "^1.1.0",
"lodash-es": "4.17.21",
"magic-string": "^0.30.12",
"memfs": "^4.13.0",
"memfs": "^4.14.0",
"mlly": "^1.7.2",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
@ -64,7 +64,7 @@
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^1.14.1",
"unplugin": "^1.15.0",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2",
@ -81,9 +81,9 @@
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.24.0",
"rollup": "4.24.4",
"unbuild": "3.0.0-rc.11",
"vue": "3.5.11"
"vue": "3.5.12"
},
"peerDependencies": {
"vue": "^3.3.4"

View File

@ -39,26 +39,26 @@
"@types/file-loader": "5.0.4",
"@types/pug": "2.0.10",
"@types/sass-loader": "8.0.9",
"@unhead/schema": "1.11.7",
"@unhead/schema": "1.11.11",
"@vitejs/plugin-vue": "5.1.4",
"@vitejs/plugin-vue-jsx": "4.0.1",
"@vue/compiler-core": "3.5.11",
"@vue/compiler-sfc": "3.5.11",
"@vue/language-core": "2.1.6",
"@vue/compiler-core": "3.5.12",
"@vue/compiler-sfc": "3.5.12",
"@vue/language-core": "2.1.10",
"esbuild-loader": "4.2.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"ignore": "6.0.2",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"ofetch": "1.4.1",
"unbuild": "3.0.0-rc.11",
"unctx": "2.3.1",
"unenv": "1.10.0",
"vite": "5.4.8",
"vue": "3.5.11",
"vite": "5.4.10",
"vue": "3.5.12",
"vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2",
"vue-router": "4.4.5",
"webpack": "5.95.0",
"webpack": "5.96.1",
"webpack-dev-middleware": "7.4.2"
},
"dependencies": {

View File

@ -355,6 +355,11 @@ export default defineUntypedSchema({
*/
plugins: 'plugins',
/**
* The shared directory. This directory is shared between the app and the server.
*/
shared: 'shared',
/**
* The directory containing your static files, which will be directly accessible via the Nuxt server
* and copied across into your `dist` folder when your app is generated.
@ -424,12 +429,13 @@ export default defineUntypedSchema({
*/
alias: {
$resolve: async (val: Record<string, string>, get): Promise<Record<string, string>> => {
const [srcDir, rootDir, assetsDir, publicDir, buildDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public'), get('buildDir')]) as [string, string, string, string, string]
const [srcDir, rootDir, assetsDir, publicDir, buildDir, sharedDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public'), get('buildDir'), get('dir.shared')]) as [string, string, string, string, string, string]
return {
'~': srcDir,
'@': srcDir,
'~~': rootDir,
'@@': rootDir,
'#shared': resolve(rootDir, sharedDir),
[basename(assetsDir)]: resolve(srcDir, assetsDir),
[basename(publicDir)]: resolve(srcDir, publicDir),
'#build': buildDir,

View File

@ -116,13 +116,16 @@ export default defineUntypedSchema({
* Emit `app:chunkError` hook when there is an error loading vite/webpack
* chunks.
*
* By default, Nuxt will also perform a hard reload of the new route
* when a chunk fails to load when navigating to a new route.
* By default, Nuxt will also perform a reload of the new route
* when a chunk fails to load when navigating to a new route (`automatic`).
*
* Setting `automatic-immediate` will lead Nuxt to perform a reload of the current route
* right when a chunk fails to load (instead of waiting for navigation).
*
* You can disable automatic handling by setting this to `false`, or handle
* chunk errors manually by setting it to `manual`.
* @see [Nuxt PR #19038](https://github.com/nuxt/nuxt/pull/19038)
* @type {false | 'manual' | 'automatic'}
* @type {false | 'manual' | 'automatic' | 'automatic-immediate'}
*/
emitRouteChunkError: {
$resolve: (val) => {
@ -297,8 +300,13 @@ export default defineUntypedSchema({
* This only works with static or strings/arrays rather than variables or conditional assignment.
*
* @see [Nuxt Issues #24770](https://github.com/nuxt/nuxt/issues/24770)
* @type {boolean | 'after-resolve'}
*/
scanPageMeta: true,
scanPageMeta: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'after-resolve' : true)
},
},
/**
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant

View File

@ -48,7 +48,7 @@ export default defineUntypedSchema({
* Each handler accepts the following options:
*
* - handler: The path to the file defining the handler.
* - route: The route under which the handler is available. This follows the conventions of [rou3](https://github.com/unjs/rou3.)
* - route: The route under which the handler is available. This follows the conventions of [rou3](https://github.com/unjs/rou3).
* - method: The HTTP method of requests that should be handled.
* - middleware: Specifies whether it is a middleware handler.
* - lazy: Specifies whether to use lazy loading to import the handler.

View File

@ -68,6 +68,7 @@ export type NuxtConfigLayer = ResolvedConfig<NuxtConfig & {
rootDir: ConfigSchema['rootDir']
}> & {
cwd: string
configFile: string
}
export interface NuxtBuilder {

View File

@ -8,7 +8,7 @@ import type { Import, InlinePreset, Unimport } from 'unimport'
import type { Compiler, Configuration, Stats } from 'webpack'
import type { Nitro, NitroConfig } from 'nitro/types'
import type { Schema, SchemaDefinition } from 'untyped'
import type { RouteLocationRaw } from 'vue-router'
import type { RouteLocationRaw, RouteRecordRaw } from 'vue-router'
import type { VueCompilerOptions } from '@vue/language-core'
import type { NuxtCompatibility, NuxtCompatibilityIssues, ViteConfig } from '..'
import type { Component, ComponentsOptions } from './components'
@ -28,6 +28,7 @@ export type VueTSConfig = 0 extends 1 & VueCompilerOptions ? TSConfig : TSConfig
export type NuxtPage = {
name?: string
path: string
props?: RouteRecordRaw['props']
file?: string
meta?: Record<string, any>
alias?: string[] | string
@ -183,12 +184,19 @@ export interface NuxtHooks {
'builder:watch': (event: WatchEvent, path: string) => HookResult
/**
* Called after pages routes are resolved.
* @param pages Array containing resolved pages
* Called after page routes are scanned from the file system.
* @param pages Array containing scanned pages
* @returns Promise
*/
'pages:extend': (pages: NuxtPage[]) => HookResult
/**
* Called after page routes have been augmented with scanned metadata.
* @param pages Array containing resolved pages
* @returns Promise
*/
'pages:resolved': (pages: NuxtPage[]) => HookResult
/**
* Called when resolving `app/router.options` files. It allows modifying the detected router options files
* and adding new ones.

View File

@ -3,8 +3,7 @@ import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
import { copyFile } from 'node:fs/promises'
import { basename, dirname, join } from 'pathe'
import type { Plugin } from 'vite'
// @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151
import Critters from 'critters'
import Beasties from 'beasties'
import { genObjectFromRawEntries } from 'knitwork'
import htmlnano from 'htmlnano'
import { glob } from 'tinyglobby'
@ -25,7 +24,7 @@ export const RenderPlugin = () => {
},
enforce: 'post',
async writeBundle () {
const critters = new Critters({ path: outputDir })
const critters = new Beasties({ path: outputDir })
const htmlFiles = await glob(['templates/**/*.html'], {
cwd: outputDir,
absolute: true,

View File

@ -18,18 +18,18 @@
"test": "pnpm lint && pnpm build"
},
"devDependencies": {
"@unocss/reset": "0.63.4",
"critters": "0.0.25",
"html-validate": "8.24.1",
"@unocss/reset": "0.64.0",
"beasties": "0.1.0",
"html-validate": "8.24.2",
"htmlnano": "2.1.1",
"jiti": "2.3.3",
"jiti": "2.4.0",
"knitwork": "1.1.0",
"pathe": "1.1.2",
"prettier": "3.3.3",
"scule": "1.3.0",
"tinyexec": "0.3.0",
"tinyglobby": "0.2.9",
"unocss": "0.63.4",
"vite": "5.4.8"
"tinyexec": "0.3.1",
"tinyglobby": "0.2.10",
"unocss": "0.64.0",
"vite": "5.4.10"
}
}

View File

@ -5,7 +5,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { exec } from 'tinyexec'
import { format } from 'prettier'
import { createJiti } from 'jiti'
// @ts-expect-error types not valid for bundler resolution
import { HtmlValidate } from 'html-validate'
const distDir = fileURLToPath(new URL('../node_modules/.temp/dist/templates', import.meta.url))

View File

@ -27,9 +27,9 @@
"@nuxt/schema": "workspace:*",
"@types/clear": "0.1.4",
"@types/estree": "1.0.6",
"rollup": "4.24.0",
"rollup": "4.24.4",
"unbuild": "3.0.0-rc.11",
"vue": "3.5.11"
"vue": "3.5.12"
},
"dependencies": {
"@nuxt/kit": "workspace:*",
@ -47,7 +47,7 @@
"externality": "^1.0.2",
"get-port-please": "^3.1.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "^2.3.3",
"jiti": "^2.4.0",
"knitwork": "^1.1.0",
"magic-string": "^0.30.12",
"mlly": "^1.7.2",
@ -61,9 +61,9 @@
"strip-literal": "^2.1.0",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^1.14.1",
"vite": "^5.4.8",
"vite-node": "^2.1.2",
"unplugin": "^1.15.0",
"vite": "^5.4.10",
"vite-node": "^2.1.4",
"vite-plugin-checker": "^0.8.0",
"vue-bundle-renderer": "^2.1.1"
},

View File

@ -9,13 +9,12 @@ function sortPlugins ({ plugins, order }: NuxtOptions['postcss']): string[] {
}
export async function resolveCSSOptions (nuxt: Nuxt): Promise<ViteConfig['css']> {
const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> } = {
const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> & { plugins: Plugin[] } } = {
postcss: {
plugins: [],
},
}
css.postcss.plugins = []
const postcssOptions = nuxt.options.postcss
const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias })

View File

@ -20,6 +20,7 @@ interface ComposableKeysOptions {
const stringTypes: Array<string | undefined> = ['Literal', 'TemplateLiteral']
const NUXT_LIB_RE = /node_modules\/(?:nuxt|nuxt3|nuxt-nightly)\//
const SUPPORTED_EXT_RE = /\.(?:m?[jt]sx?|vue)/
const SCRIPT_RE = /(?<=<script[^>]*>)[\s\S]*?(?=<\/script>)/i
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
const composableMeta: Record<string, any> = {}
@ -43,7 +44,7 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
},
transform (code, id) {
if (!KEYED_FUNCTIONS_RE.test(code)) { return }
const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=<script[^>]*>)[\s\S]*?(?=<\/script>)/i) || { index: 0, 0: code }
const { 0: script = code, index: codeIndex = 0 } = code.match(SCRIPT_RE) || { index: 0, 0: code }
const s = new MagicString(code)
// https://github.com/unjs/unplugin/issues/90
let imports: Set<string> | undefined

View File

@ -10,6 +10,7 @@ import { isCSSRequest } from 'vite'
const PREFIX = 'virtual:public?'
const CSS_URL_RE = /url\((\/[^)]+)\)/g
const CSS_URL_SINGLE_RE = /url\(\/[^)]+\)/
const RENDER_CHUNK_RE = /(?<= = )['"`]/
interface VitePublicDirsPluginOptions {
dev?: boolean
@ -70,7 +71,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: VitePublicDirsPlugi
if (!chunk.facadeModuleId?.includes('?inline&used')) { return }
const s = new MagicString(code)
const q = code.match(/(?<= = )['"`]/)?.[0] || '"'
const q = code.match(RENDER_CHUNK_RE)?.[0] || '"'
for (const [full, url] of code.matchAll(CSS_URL_RE)) {
if (url && resolveFromPublicAssets(url)) {
s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`)
@ -108,13 +109,14 @@ export const VitePublicDirsPlugin = createUnplugin((options: VitePublicDirsPlugi
]
})
const PUBLIC_ASSETS_RE = /[?#].*$/
export function useResolveFromPublicAssets () {
const nitro = useNitro()
function resolveFromPublicAssets (id: string) {
for (const dir of nitro.options.publicAssets) {
if (!id.startsWith(withTrailingSlash(dir.baseURL || '/'))) { continue }
const path = id.replace(/[?#].*$/, '').replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
const path = id.replace(PUBLIC_ASSETS_RE, '').replace(withTrailingSlash(dir.baseURL || '/'), withTrailingSlash(dir.dir))
if (existsSync(path)) {
return id
}

View File

@ -7,6 +7,7 @@ import { joinURL, withTrailingSlash, withoutLeadingSlash } from 'ufo'
import type { ViteConfig } from '@nuxt/schema'
import defu from 'defu'
import type { Nitro } from 'nitro/types'
import escapeStringRegexp from 'escape-string-regexp'
import type { ViteBuildContext } from './vite'
import { createViteLogger } from './utils/logger'
import { initViteNodeServer } from './vite-node'
@ -80,7 +81,13 @@ export async function buildServer (ctx: ViteBuildContext) {
ssr: true,
rollupOptions: {
input: { server: entry },
external: ['nitro/runtime', '#internal/nuxt/paths', '#internal/nuxt/app-config'],
external: [
'nitro/runtime',
'#internal/nuxt/paths',
'#internal/nuxt/app-config',
'#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))),
],
output: {
entryFileNames: '[name].mjs',
format: 'module',

View File

@ -1,9 +1,13 @@
import type { ExternalsOptions } from 'externality'
import { ExternalsDefaults, isExternal } from 'externality'
import type { ViteDevServer } from 'vite'
import escapeStringRegexp from 'escape-string-regexp'
import { withTrailingSlash } from 'ufo'
import type { Nuxt } from 'nuxt/schema'
import { resolve } from 'pathe'
import { toArray } from '.'
export function createIsExternal (viteServer: ViteDevServer, rootDir: string, modulesDirs?: string[]) {
export function createIsExternal (viteServer: ViteDevServer, nuxt: Nuxt) {
const externalOpts: ExternalsOptions = {
inline: [
/virtual:/,
@ -16,15 +20,17 @@ export function createIsExternal (viteServer: ViteDevServer, rootDir: string, mo
),
],
external: [
'#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))),
...(viteServer.config.ssr.external as string[]) || [],
/node_modules/,
],
resolve: {
modules: modulesDirs,
modules: nuxt.options.modulesDir,
type: 'module',
extensions: ['.ts', '.js', '.json', '.vue', '.mjs', '.jsx', '.tsx', '.wasm'],
},
}
return (id: string) => isExternal(id, rootDir, externalOpts)
return (id: string) => isExternal(id, nuxt.options.rootDir, externalOpts)
}

View File

@ -1,5 +1,6 @@
import type * as vite from 'vite'
import { logger } from '@nuxt/kit'
import { colorize } from 'consola/utils'
import { hasTTY, isCI } from 'std-env'
import clear from 'clear'
import type { NuxtOptions } from '@nuxt/schema'
@ -22,6 +23,7 @@ const logLevelMapReverse: Record<NonNullable<vite.UserConfig['logLevel']>, numbe
info: 3,
}
const RUNTIME_RESOLVE_REF_RE = /^([^ ]+) referenced in/m
export function createViteLogger (config: vite.InlineConfig): vite.Logger {
const loggedErrors = new WeakSet<any>()
const canClearScreen = hasTTY && !isCI && config.clearScreen
@ -36,7 +38,7 @@ export function createViteLogger (config: vite.InlineConfig): vite.Logger {
if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { return }
// Hide warnings about externals produced by https://github.com/vitejs/vite/blob/v5.2.11/packages/vite/src/node/plugins/css.ts#L350-L355
if (msg.includes('didn\'t resolve at build time, it will remain unchanged to be resolved at runtime')) {
const id = msg.trim().match(/^([^ ]+) referenced in/m)?.[1]
const id = msg.trim().match(RUNTIME_RESOLVE_REF_RE)?.[1]
if (id && resolveFromPublicAssets(id)) { return }
}
}
@ -61,8 +63,7 @@ export function createViteLogger (config: vite.InlineConfig): vite.Logger {
const prevLevel = logger.level
logger.level = logLevelMapReverse[config.logLevel || 'info']
// TODO: colorize counter after https://github.com/unjs/consola/pull/166
logger[type](msg + (sameAsLast ? ` (x${duplicateCount + 1})` : ''))
logger[type](msg + (sameAsLast ? colorize('dim', ` (x${duplicateCount + 1})`) : ''))
logger.level = prevLevel
}

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