Merge remote-tracking branch 'origin/main' into feat-add-auto-import-directives

This commit is contained in:
Daniel Roe 2024-11-02 23:44:08 +00:00
commit cf14552c5e
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
43 changed files with 1294 additions and 988 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts@sha256:a5e0ed56f2c20b9689e0f7dd498cac7e08d2a3a283e92d9304e7b9b83e3c6ff3 FROM node:lts@sha256:de4c8be8232b7081d8846360d73ce6dbff33c6636f2259cd14d82c0de1ac3ff2
RUN apt-get update && \ RUN apt-get update && \
apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \ apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \

View File

@ -19,4 +19,4 @@ jobs:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5 uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0

View File

@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Lychee link checker - name: Lychee link checker
uses: lycheeverse/lychee-action@7cd0af4c74a61395d455af97419279d86aafaede # for v1.8.0 uses: lycheeverse/lychee-action@ae4699150ab670dcfb64cc74e8680e776d9caae2 # for v1.8.0
with: with:
# arguments with file types to check # arguments with file types to check
args: >- args: >-

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 You can configure fully typed, per-environment overrides in your nuxt.config
```ts twoslash [nuxt.config.ts] ```ts twoslash [nuxt.config.ts]
// @errors: 2353
export default defineNuxtConfig({ export default defineNuxtConfig({
$production: { $production: {
routeRules: { routeRules: {
@ -41,10 +42,17 @@ export default defineNuxtConfig({
}, },
$development: { $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"} ::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`. 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`. 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` ## `useFetch`
The [`useFetch`](/docs/api/composables/use-fetch) composable uses `$fetch` under-the-hood to make SSR-safe network calls in the setup function. 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. The `useAsyncData` composable is responsible for wrapping async logic and returning the result once it is resolved.
::tip ::tip
`useFetch(url)` is nearly equivalent to `useAsyncData(url, () => $fetch(url))`. :br `useFetch(url)` is nearly equivalent to `useAsyncData(url, () => event.$fetch(url))`. :br
It's developer experience sugar for the most common use case. 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"} ::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 - `error` when the fetch fails
- `success` when the fetch is completed successfully - `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. 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`).
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`
::
### Pass Cookies From Server-side API Calls on SSR Response ### Pass Cookies From Server-side API Calls on SSR Response

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. 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 ### 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`: 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

@ -59,14 +59,16 @@ This feature will likely be removed in a near future.
## emitRouteChunkError ## 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`. 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] ```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({ export default defineNuxtConfig({
experimental: { experimental: {
emitRouteChunkError: 'automatic' // or 'manual' or false emitRouteChunkError: 'automatic' // or 'automatic-immediate', 'manual' or false
} }
}) })
``` ```

View File

@ -77,7 +77,7 @@ export function useAPI<T>(
) { ) {
return useFetch(url, { return useFetch(url, {
...options, ...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

@ -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. `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: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. `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. `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. `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. `imports:sources` | `presets` | Called at setup allowing modules to extend sources.

View File

@ -40,19 +40,19 @@
"@nuxt/ui-templates": "workspace:*", "@nuxt/ui-templates": "workspace:*",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@types/node": "20.17.0", "@types/node": "22.8.6",
"@vue/compiler-core": "3.5.12", "@vue/compiler-core": "3.5.12",
"@vue/compiler-dom": "3.5.12", "@vue/compiler-dom": "3.5.12",
"@vue/shared": "3.5.12", "@vue/shared": "3.5.12",
"c12": "2.0.1", "c12": "2.0.1",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "2.3.3", "jiti": "2.4.0",
"magic-string": "^0.30.12", "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:*", "nuxt": "workspace:*",
"ohash": "1.1.4", "ohash": "1.1.4",
"postcss": "8.4.47", "postcss": "8.4.47",
"rollup": "4.24.0", "rollup": "4.24.3",
"send": ">=1.1.0", "send": ">=1.1.0",
"typescript": "5.6.3", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
@ -61,20 +61,20 @@
"vue": "3.5.12" "vue": "3.5.12"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.13.0", "@eslint/js": "9.14.0",
"@nuxt/eslint-config": "0.6.0", "@nuxt/eslint-config": "0.6.1",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*", "@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.14.4", "@nuxt/test-utils": "3.14.4",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@types/node": "20.17.0", "@types/node": "22.8.6",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.11.10", "@unhead/schema": "1.11.11",
"@unhead/vue": "1.11.10", "@unhead/vue": "1.11.10",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vitest/coverage-v8": "2.1.3", "@vitest/coverage-v8": "2.1.4",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"case-police": "0.7.0", "case-police": "0.7.0",
@ -83,36 +83,36 @@
"cssnano": "7.0.6", "cssnano": "7.0.6",
"destr": "2.0.3", "destr": "2.0.3",
"devalue": "5.1.1", "devalue": "5.1.1",
"eslint": "9.13.0", "eslint": "9.14.0",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "3.9.1", "eslint-plugin-perfectionist": "3.9.1",
"eslint-typegen": "0.3.2", "eslint-typegen": "0.3.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "15.7.4", "happy-dom": "15.8.0",
"jiti": "2.3.3", "jiti": "2.4.0",
"markdownlint-cli": "0.42.0", "markdownlint-cli": "0.42.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "3.15.0", "nuxi": "3.15.0",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1", "nuxt-content-twoslash": "0.1.1",
"ofetch": "1.4.1", "ofetch": "1.4.1",
"pathe": "1.1.2", "pathe": "1.1.2",
"playwright-core": "1.48.1", "playwright-core": "1.48.2",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"semver": "7.6.3", "semver": "7.6.3",
"sherif": "1.0.1", "sherif": "1.0.1",
"std-env": "3.7.0", "std-env": "3.7.0",
"tinyexec": "0.3.1", "tinyexec": "0.3.1",
"tinyglobby": "0.2.9", "tinyglobby": "0.2.10",
"typescript": "5.6.3", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"vitest": "2.1.3", "vitest": "2.1.4",
"vitest-environment-nuxt": "1.0.1", "vitest-environment-nuxt": "1.0.1",
"vue": "3.5.12", "vue": "3.5.12",
"vue-router": "4.4.5", "vue-router": "4.4.5",
"vue-tsc": "2.1.6" "vue-tsc": "2.1.10"
}, },
"packageManager": "pnpm@9.12.2", "packageManager": "pnpm@9.12.3",
"engines": { "engines": {
"node": "^16.10.0 || >=18.0.0" "node": "^16.10.0 || >=18.0.0"
}, },

View File

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

View File

@ -65,12 +65,12 @@
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.0", "@nuxt/telemetry": "^2.6.0",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.11.10", "@unhead/dom": "^1.11.11",
"@unhead/shared": "^1.11.10", "@unhead/shared": "^1.11.11",
"@unhead/ssr": "^1.11.10", "@unhead/ssr": "^1.11.10",
"@unhead/vue": "^1.11.10", "@unhead/vue": "^1.11.10",
"@vue/shared": "^3.5.12", "@vue/shared": "^3.5.12",
"acorn": "8.13.0", "acorn": "8.14.0",
"c12": "^2.0.1", "c12": "^2.0.1",
"chokidar": "^4.0.1", "chokidar": "^4.0.1",
"compatx": "^0.1.8", "compatx": "^0.1.8",
@ -88,13 +88,13 @@
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^6.0.2", "ignore": "^6.0.2",
"impound": "^0.2.0", "impound": "^0.2.0",
"jiti": "^2.3.3", "jiti": "^2.4.0",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.12", "magic-string": "^0.30.12",
"mlly": "^1.7.2", "mlly": "^1.7.2",
"nanotar": "^0.1.1", "nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.15.0", "nuxi": "^3.15.0",
"nypm": "^0.3.12", "nypm": "^0.3.12",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
@ -107,7 +107,7 @@
"semver": "^7.6.3", "semver": "^7.6.3",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"strip-literal": "^2.1.0", "strip-literal": "^2.1.0",
"tinyglobby": "0.2.9", "tinyglobby": "0.2.10",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"ultrahtml": "^1.5.3", "ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
@ -115,9 +115,9 @@
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unhead": "^1.11.10", "unhead": "^1.11.10",
"unimport": "^3.13.1", "unimport": "^3.13.1",
"unplugin": "^1.14.1", "unplugin": "^1.15.0",
"unplugin-vue-router": "^0.10.8", "unplugin-vue-router": "^0.10.8",
"unstorage": "^1.12.0", "unstorage": "^1.13.1",
"untyped": "^1.5.1", "untyped": "^1.5.1",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
@ -133,7 +133,7 @@
"@vue/compiler-sfc": "3.5.12", "@vue/compiler-sfc": "3.5.12",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vite": "5.4.10", "vite": "5.4.10",
"vitest": "2.1.3" "vitest": "2.1.4"
}, },
"peerDependencies": { "peerDependencies": {
"@parcel/watcher": "^2.1.0", "@parcel/watcher": "^2.1.0",

View File

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

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 { 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 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 type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
export { defineNuxtLink } from './components/index' export { defineNuxtLink } from './components/index'

View File

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

@ -17,7 +17,7 @@ import { version as nuxtVersion } from '../../package.json'
import { distDir } from '../dirs' import { distDir } from '../dirs'
import { toArray } from '../utils' import { toArray } from '../utils'
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon' import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
import { nuxtImportProtections } from './plugins/import-protection' import { createImportProtectionPatterns } from './plugins/import-protection'
import { EXTENSION_RE } from './utils' import { EXTENSION_RE } from './utils'
const logLevelMapReverse = { const logLevelMapReverse = {
@ -49,6 +49,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
.map(m => m.entryPath!), .map(m => m.entryPath!),
) )
const isNuxtV4 = nuxt.options.future?.compatibilityVersion === 4
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, { const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {
debug: nuxt.options.debug, debug: nuxt.options.debug,
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,
@ -66,6 +68,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}, },
imports: { imports: {
autoImport: nuxt.options.imports.autoImport as boolean, autoImport: nuxt.options.imports.autoImport as boolean,
dirs: isNuxtV4
? [
resolve(nuxt.options.rootDir, 'shared', 'utils'),
resolve(nuxt.options.rootDir, 'shared', 'types'),
]
: [],
imports: [ imports: [
{ {
as: '__buildAssetsURL', as: '__buildAssetsURL',
@ -362,11 +370,20 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Register nuxt protection patterns // Register nuxt protection patterns
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || [] nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins) nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)
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( nitroConfig.rollupConfig!.plugins!.push(
ImpoundPlugin.rollup({ ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir, cwd: nuxt.options.rootDir,
patterns: nuxtImportProtections(nuxt, { isNitro: true }), include: sharedPatterns,
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/], patterns: createImportProtectionPatterns(nuxt, { context: 'shared' }),
}),
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
patterns: createImportProtectionPatterns(nuxt, { context: 'nitro-app' }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/, ...sharedPatterns],
}), }),
) )

View File

@ -18,7 +18,6 @@ import type { DateString } from 'compatx'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { withTrailingSlash, withoutLeadingSlash } from 'ufo' import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
import { ImpoundPlugin } from 'impound' import { ImpoundPlugin } from 'impound'
import type { ImpoundOptions } from 'impound'
import defu from 'defu' import defu from 'defu'
import { gt, satisfies } from 'semver' import { gt, satisfies } from 'semver'
import { hasTTY, isCI } from 'std-env' import { hasTTY, isCI } from 'std-env'
@ -32,7 +31,7 @@ import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json' import { version } from '../../package.json'
import { scriptsStubsPreset } from '../imports/presets' import { scriptsStubsPreset } from '../imports/presets'
import { resolveTypePath } from './utils/types' import { resolveTypePath } from './utils/types'
import { nuxtImportProtections } from './plugins/import-protection' import { createImportProtectionPatterns } from './plugins/import-protection'
import { UnctxTransformPlugin } from './plugins/unctx' import { UnctxTransformPlugin } from './plugins/unctx'
import { TreeShakeComposablesPlugin } from './plugins/tree-shake' import { TreeShakeComposablesPlugin } from './plugins/tree-shake'
import { DevOnlyPlugin } from './plugins/dev-only' import { DevOnlyPlugin } from './plugins/dev-only'
@ -249,16 +248,28 @@ async function initNuxt (nuxt: Nuxt) {
// Add plugin normalization plugin // Add plugin normalization plugin
addBuildPlugin(RemovePluginMetadataPlugin(nuxt)) 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 // Add import protection
const config: ImpoundOptions = { const nuxtProtectionConfig = {
cwd: nuxt.options.rootDir, cwd: nuxt.options.rootDir,
// Exclude top-level resolutions by plugins // Exclude top-level resolutions by plugins
exclude: [join(nuxt.options.srcDir, 'index.html')], exclude: [relative(nuxt.options.rootDir, join(nuxt.options.srcDir, 'index.html')), ...sharedPatterns],
patterns: nuxtImportProtections(nuxt), 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({ ...nuxtProtectionConfig, error: false }), { name: 'nuxt:import-protection' }), { client: false })
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: true }), { name: 'nuxt:import-protection' }), { server: false }) addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...nuxtProtectionConfig, error: true }), { name: 'nuxt:import-protection' }), { server: false })
addWebpackPlugin(() => ImpoundPlugin.webpack(config)) addWebpackPlugin(() => ImpoundPlugin.webpack(nuxtProtectionConfig))
// add resolver for modules used in virtual files // add resolver for modules used in virtual files
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false }) addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false })
@ -565,6 +576,11 @@ async function initNuxt (nuxt: Nuxt) {
if (nuxt.options.experimental.emitRouteChunkError === 'automatic') { if (nuxt.options.experimental.emitRouteChunkError === 'automatic') {
addPlugin(resolve(nuxt.options.appDir, 'plugins/chunk-reload.client')) 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 // Add experimental session restoration support
if (nuxt.options.experimental.restoreState) { if (nuxt.options.experimental.restoreState) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/restore-state.client')) addPlugin(resolve(nuxt.options.appDir, 'plugins/restore-state.client'))

View File

@ -9,12 +9,17 @@ interface ImportProtectionOptions {
exclude?: Array<RegExp | string> 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 patterns: ImportProtectionOptions['patterns'] = []
const context = contextFlags[options.context]
patterns.push([ patterns.push([
/^(nuxt|nuxt3|nuxt-nightly)$/, /^(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([ 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')) { for (const mod of nuxt.options.modules.filter(m => typeof m === 'string')) {
patterns.push([ patterns.push([
new RegExp(`^${escapeRE(mod as string)}$`), new RegExp(`^${escapeRE(mod)}$`),
'Importing directly from module entry-points is not allowed.', '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)/]) { 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.')]) 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(\/|$)/]) { 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([ patterns.push([
new RegExp(escapeRE(relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, nuxt.options.serverDir || 'server'))) + '\\/(api|routes|middleware|plugins)\\/'), 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 return patterns
} }
const contextFlags = {
'nitro-app': 'server runtime',
'nuxt-app': 'the Vue part of your app',
'shared': 'the #shared directory',
} as const

View File

@ -106,6 +106,8 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
await nuxt.callHook('imports:context', ctx) await nuxt.callHook('imports:context', ctx)
const isNuxtV4 = nuxt.options.future?.compatibilityVersion === 4
// composables/ dirs from all layers // composables/ dirs from all layers
let composablesDirs: string[] = [] let composablesDirs: string[] = []
if (options.scan) { if (options.scan) {
@ -117,6 +119,12 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
composablesDirs.push(resolve(layer.config.srcDir, 'composables')) composablesDirs.push(resolve(layer.config.srcDir, 'composables'))
composablesDirs.push(resolve(layer.config.srcDir, 'utils')) composablesDirs.push(resolve(layer.config.srcDir, 'utils'))
composablesDirs.push(resolve(layer.config.srcDir, 'directives')) composablesDirs.push(resolve(layer.config.srcDir, 'directives'))
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 ?? [])) { for (const dir of (layer.config.imports?.dirs ?? [])) {
if (!dir) { if (!dir) {
continue continue

View File

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

View File

@ -507,13 +507,14 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
const route: NormalizedRoute = { const route: NormalizedRoute = {
path: serializeRouteValue(page.path), path: serializeRouteValue(page.path),
props: serializeRouteValue(page.props),
name: serializeRouteValue(page.name), name: serializeRouteValue(page.name),
meta: serializeRouteValue(metaFiltered, skipMeta), meta: serializeRouteValue(metaFiltered, skipMeta),
alias: serializeRouteValue(toArray(page.alias), skipAlias), alias: serializeRouteValue(toArray(page.alias), skipAlias),
redirect: serializeRouteValue(page.redirect), 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) { if (route[key] === undefined) {
delete route[key] delete route[key]
} }
@ -542,7 +543,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
const metaRoute: NormalizedRoute = { const metaRoute: NormalizedRoute = {
name: `${metaImportName}?.name ?? ${route.name}`, name: `${metaImportName}?.name ?? ${route.name}`,
path: `${metaImportName}?.path ?? ${route.path}`, path: `${metaImportName}?.path ?? ${route.path}`,
props: `${metaImportName}?.props ?? false`, props: `${metaImportName}?.props ?? ${route.props ?? false}`,
meta: `${metaImportName} || {}`, meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`, alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`, redirect: `${metaImportName}?.redirect`,

View File

@ -36,7 +36,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "page-with-props"", "name": "mockMeta?.name ?? "page-with-props"",
"path": "mockMeta?.path ?? "/page-with-props"", "path": "mockMeta?.path ?? "/page-with-props"",
"props": "mockMeta?.props ?? false", "props": "mockMeta?.props ?? true",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],

View File

@ -29,6 +29,7 @@
"component": "() => import("pages/page-with-props.vue")", "component": "() => import("pages/page-with-props.vue")",
"name": ""page-with-props"", "name": ""page-with-props"",
"path": ""/page-with-props"", "path": ""/page-with-props"",
"props": "true",
}, },
], ],
"should allow pages with `:` in their path": [ "should allow pages with `:` in their path": [

View File

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

View File

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

View File

@ -45,7 +45,7 @@
"globby": "^14.0.2", "globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"jiti": "^2.3.3", "jiti": "^2.4.0",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"magic-string": "^0.30.12", "magic-string": "^0.30.12",
@ -64,7 +64,7 @@
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^1.14.1", "unplugin": "^1.15.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
@ -81,7 +81,7 @@
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.24.0", "rollup": "4.24.3",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vue": "3.5.12" "vue": "3.5.12"
}, },

View File

@ -39,16 +39,16 @@
"@types/file-loader": "5.0.4", "@types/file-loader": "5.0.4",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/sass-loader": "8.0.9", "@types/sass-loader": "8.0.9",
"@unhead/schema": "1.11.10", "@unhead/schema": "1.11.11",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vitejs/plugin-vue-jsx": "4.0.1", "@vitejs/plugin-vue-jsx": "4.0.1",
"@vue/compiler-core": "3.5.12", "@vue/compiler-core": "3.5.12",
"@vue/compiler-sfc": "3.5.12", "@vue/compiler-sfc": "3.5.12",
"@vue/language-core": "2.1.6", "@vue/language-core": "2.1.10",
"esbuild-loader": "4.2.2", "esbuild-loader": "4.2.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"ignore": "6.0.2", "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", "ofetch": "1.4.1",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"unctx": "2.3.1", "unctx": "2.3.1",
@ -58,7 +58,7 @@
"vue-bundle-renderer": "2.1.1", "vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue-router": "4.4.5", "vue-router": "4.4.5",
"webpack": "5.95.0", "webpack": "5.96.1",
"webpack-dev-middleware": "7.4.2" "webpack-dev-middleware": "7.4.2"
}, },
"dependencies": { "dependencies": {

View File

@ -355,6 +355,11 @@ export default defineUntypedSchema({
*/ */
plugins: 'plugins', 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 * 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. * and copied across into your `dist` folder when your app is generated.
@ -424,12 +429,13 @@ export default defineUntypedSchema({
*/ */
alias: { alias: {
$resolve: async (val: Record<string, string>, get): Promise<Record<string, string>> => { $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 { return {
'~': srcDir, '~': srcDir,
'@': srcDir, '@': srcDir,
'~~': rootDir, '~~': rootDir,
'@@': rootDir, '@@': rootDir,
'#shared': resolve(rootDir, sharedDir),
[basename(assetsDir)]: resolve(srcDir, assetsDir), [basename(assetsDir)]: resolve(srcDir, assetsDir),
[basename(publicDir)]: resolve(srcDir, publicDir), [basename(publicDir)]: resolve(srcDir, publicDir),
'#build': buildDir, '#build': buildDir,

View File

@ -116,13 +116,16 @@ export default defineUntypedSchema({
* Emit `app:chunkError` hook when there is an error loading vite/webpack * Emit `app:chunkError` hook when there is an error loading vite/webpack
* chunks. * chunks.
* *
* By default, Nuxt will also perform a hard reload of the 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. * 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 * You can disable automatic handling by setting this to `false`, or handle
* chunk errors manually by setting it to `manual`. * chunk errors manually by setting it to `manual`.
* @see [Nuxt PR #19038](https://github.com/nuxt/nuxt/pull/19038) * @see [Nuxt PR #19038](https://github.com/nuxt/nuxt/pull/19038)
* @type {false | 'manual' | 'automatic'} * @type {false | 'manual' | 'automatic' | 'automatic-immediate'}
*/ */
emitRouteChunkError: { emitRouteChunkError: {
$resolve: (val) => { $resolve: (val) => {

View File

@ -22,13 +22,13 @@
"critters": "0.0.25", "critters": "0.0.25",
"html-validate": "8.24.2", "html-validate": "8.24.2",
"htmlnano": "2.1.1", "htmlnano": "2.1.1",
"jiti": "2.3.3", "jiti": "2.4.0",
"knitwork": "1.1.0", "knitwork": "1.1.0",
"pathe": "1.1.2", "pathe": "1.1.2",
"prettier": "3.3.3", "prettier": "3.3.3",
"scule": "1.3.0", "scule": "1.3.0",
"tinyexec": "0.3.1", "tinyexec": "0.3.1",
"tinyglobby": "0.2.9", "tinyglobby": "0.2.10",
"unocss": "0.63.6", "unocss": "0.63.6",
"vite": "5.4.10" "vite": "5.4.10"
} }

View File

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

View File

@ -7,6 +7,7 @@ import { joinURL, withTrailingSlash, withoutLeadingSlash } from 'ufo'
import type { ViteConfig } from '@nuxt/schema' import type { ViteConfig } from '@nuxt/schema'
import defu from 'defu' import defu from 'defu'
import type { Nitro } from 'nitro/types' import type { Nitro } from 'nitro/types'
import escapeStringRegexp from 'escape-string-regexp'
import type { ViteBuildContext } from './vite' import type { ViteBuildContext } from './vite'
import { createViteLogger } from './utils/logger' import { createViteLogger } from './utils/logger'
import { initViteNodeServer } from './vite-node' import { initViteNodeServer } from './vite-node'
@ -80,7 +81,13 @@ export async function buildServer (ctx: ViteBuildContext) {
ssr: true, ssr: true,
rollupOptions: { rollupOptions: {
input: { server: entry }, 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: { output: {
entryFileNames: '[name].mjs', entryFileNames: '[name].mjs',
format: 'module', format: 'module',

View File

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

@ -140,7 +140,7 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
}, },
}) })
const isExternal = createIsExternal(viteServer, ctx.nuxt.options.rootDir, ctx.nuxt.options.modulesDir) const isExternal = createIsExternal(viteServer, ctx.nuxt)
node.shouldExternalize = async (id: string) => { node.shouldExternalize = async (id: string) => {
const result = await isExternal(id) const result = await isExternal(id)
if (result?.external) { if (result?.external) {

View File

@ -44,12 +44,12 @@
"globby": "^14.0.2", "globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"jiti": "^2.3.3", "jiti": "^2.4.0",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"magic-string": "^0.30.12", "magic-string": "^0.30.12",
"memfs": "^4.14.0", "memfs": "^4.14.0",
"mini-css-extract-plugin": "^2.9.1", "mini-css-extract-plugin": "^2.9.2",
"mlly": "^1.7.2", "mlly": "^1.7.2",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
@ -64,11 +64,11 @@
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^1.14.1", "unplugin": "^1.15.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
"webpack": "^5.95.0", "webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2", "webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-middleware": "^7.4.2", "webpack-dev-middleware": "^7.4.2",
"webpack-hot-middleware": "^2.26.1", "webpack-hot-middleware": "^2.26.1",
@ -82,7 +82,7 @@
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.24.0", "rollup": "4.24.3",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vue": "3.5.12" "vue": "3.5.12"
}, },

View File

@ -1,4 +1,4 @@
import { isAbsolute } from 'pathe' import { isAbsolute, resolve } from 'pathe'
import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin' import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
import type { WebpackConfigContext } from '../utils/config' import type { WebpackConfigContext } from '../utils/config'
@ -53,7 +53,11 @@ function serverStandalone (ctx: WebpackConfigContext) {
'#', '#',
...ctx.options.build.transpile, ...ctx.options.build.transpile,
] ]
const external = ['nitro/runtime'] const external = [
'nitro/runtime',
'#shared',
resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared),
]
if (!ctx.nuxt.options.dev) { if (!ctx.nuxt.options.dev) {
external.push('#internal/nuxt/paths', '#internal/nuxt/app-config') external.push('#internal/nuxt/paths', '#internal/nuxt/app-config')
} }

File diff suppressed because it is too large Load Diff

View File

@ -2374,7 +2374,7 @@ describe('component islands', () => {
"link": [], "link": [],
"style": [ "style": [
{ {
"innerHTML": "pre[data-v-xxxxx]{color:blue}", "innerHTML": "pre[data-v-xxxxx]{color:#00f}",
}, },
], ],
} }
@ -2743,7 +2743,11 @@ function normaliseIslandResult (result: NuxtIslandResponse) {
for (const style of result.head.style) { for (const style of result.head.style) {
if (typeof style !== 'string') { if (typeof style !== 'string') {
if (style.innerHTML) { if (style.innerHTML) {
style.innerHTML = (style.innerHTML as string).replace(/data-v-[a-z0-9]+/g, 'data-v-xxxxx') style.innerHTML =
(style.innerHTML as string)
.replace(/data-v-[a-z0-9]+/g, 'data-v-xxxxx')
// Vite 6 enables CSS minify by default for SSR
.replace(/blue/, '#00f')
} }
if (style.key) { if (style.key) {
style.key = style.key.replace(/-[a-z0-9]+$/i, '') style.key = style.key.replace(/-[a-z0-9]+$/i, '')

View File

@ -21,8 +21,8 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const [clientStats, clientStatsInlined] = await Promise.all((['.output', '.output-inline']) const [clientStats, clientStatsInlined] = await Promise.all((['.output', '.output-inline'])
.map(outputDir => analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public')))) .map(outputDir => analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public'))))
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"115k"`) expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"119k"`)
expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"115k"`) expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"119k"`)
const files = new Set([...clientStats!.files, ...clientStatsInlined!.files].map(f => f.replace(/\..*\.js/, '.js'))) const files = new Set([...clientStats!.files, ...clientStatsInlined!.files].map(f => f.replace(/\..*\.js/, '.js')))

View File

@ -20,6 +20,7 @@ import { callOnce } from '#app/composables/once'
import { useLoadingIndicator } from '#app/composables/loading-indicator' import { useLoadingIndicator } from '#app/composables/loading-indicator'
import { useRouteAnnouncer } from '#app/composables/route-announcer' import { useRouteAnnouncer } from '#app/composables/route-announcer'
import { encodeURL, resolveRouteObject } from '#app/composables/router' import { encodeURL, resolveRouteObject } from '#app/composables/router'
import { useRuntimeHook } from '#app/composables/runtime-hook'
registerEndpoint('/api/test', defineEventHandler(event => ({ registerEndpoint('/api/test', defineEventHandler(event => ({
method: event.method, method: event.method,
@ -93,6 +94,7 @@ describe('composables', () => {
'abortNavigation', 'abortNavigation',
'setPageLayout', 'setPageLayout',
'defineNuxtComponent', 'defineNuxtComponent',
'useRuntimeHook',
] ]
const skippedComposables: string[] = [ const skippedComposables: string[] = [
'addRouteMiddleware', 'addRouteMiddleware',
@ -577,6 +579,36 @@ describe.skipIf(process.env.TEST_MANIFEST === 'manifest-off')('app manifests', (
}) })
}) })
describe('useRuntimeHook', () => {
it('types work', () => {
// @ts-expect-error should not allow unknown hooks
useRuntimeHook('test', () => {})
useRuntimeHook('app:beforeMount', (_app) => {
// @ts-expect-error argument should be typed
_app = 'test'
})
})
it('should call hooks', async () => {
const nuxtApp = useNuxtApp()
let called = 1
const wrapper = await mountSuspended(defineNuxtComponent({
setup () {
useRuntimeHook('test-hook' as any, () => {
called++
})
},
render: () => h('div', 'hi there'),
}))
expect(called).toBe(1)
await nuxtApp.callHook('test-hook' as any)
expect(called).toBe(2)
wrapper.unmount()
await nuxtApp.callHook('test-hook' as any)
expect(called).toBe(2)
})
})
describe('routing utilities: `navigateTo`', () => { describe('routing utilities: `navigateTo`', () => {
it('navigateTo should disallow navigation to external URLs by default', () => { it('navigateTo should disallow navigation to external URLs by default', () => {
expect(() => navigateTo('https://test.com')).toThrowErrorMatchingInlineSnapshot('[Error: Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.]') expect(() => navigateTo('https://test.com')).toThrowErrorMatchingInlineSnapshot('[Error: Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.]')