` tag. The contents of `handleClick` is never executed here. During hydration in the browser, the `counter` ref is re-initialized. The `handleClick` finally binds itself to the button; Therefore it is reasonable to deduce that the body of `handleClick` will always run in a browser environment.
+
+[Middlewares](/docs/guide/directory-structure/middleware) and [pages](/docs/guide/directory-structure/pages) run in the server and on the client during hydration. [Plugins](/docs/guide/directory-structure/plugins) can be rendered on the server or client or both. [Components](/docs/guide/directory-structure/components) can be forced to run on the client only as well. [Composables](/docs/guide/directory-structure/composables) and [utilities](/docs/guide/directory-structure/utils) are rendered based on the context of their usage.
+
**Benefits of server-side rendering:**
-- **Performance**: Users can get immediate access to the page's content because browsers can display static content much faster than JavaScript-generated content. At the same time, Nuxt preserves the interactivity of a web application when the hydration process happens.
+- **Performance**: Users can get immediate access to the page's content because browsers can display static content much faster than JavaScript-generated content. At the same time, Nuxt preserves the interactivity of a web application during the hydration process.
- **Search Engine Optimization**: Universal rendering delivers the entire HTML content of the page to the browser as a classic server application. Web crawlers can directly index the page's content, which makes Universal rendering a great choice for any content that you want to index quickly.
**Downsides of server-side rendering:**
diff --git a/docs/2.guide/2.directory-structure/1.components.md b/docs/2.guide/2.directory-structure/1.components.md
index 8fb3a0c59f..0656b96e75 100644
--- a/docs/2.guide/2.directory-structure/1.components.md
+++ b/docs/2.guide/2.directory-structure/1.components.md
@@ -8,9 +8,9 @@ navigation.icon: i-ph-folder
Nuxt automatically imports any components in this directory (along with components that are registered by any modules you may be using).
```bash [Directory Structure]
-| components/
---| AppHeader.vue
---| AppFooter.vue
+-| components/
+---| AppHeader.vue
+---| AppFooter.vue
```
```html [app.vue]
@@ -28,10 +28,10 @@ Nuxt automatically imports any components in this directory (along with componen
If you have a component in nested directories such as:
```bash [Directory Structure]
-| components/
---| base/
-----| foo/
-------| Button.vue
+-| components/
+---| base/
+-----| foo/
+-------| Button.vue
```
... then the component's name will be based on its own path directory and filename, with duplicate segments being removed. Therefore, the component's name will be:
@@ -285,8 +285,8 @@ export default defineNuxtConfig({
Now you can register server-only components with the `.server` suffix and use them anywhere in your application automatically.
```bash [Directory Structure]
-| components/
---| HighlightedMarkdown.server.vue
+-| components/
+---| HighlightedMarkdown.server.vue
```
```vue [pages/example.vue]
@@ -359,9 +359,9 @@ Slots can be interactive and are wrapped within a `
` with `display: content
In this case, the `.server` + `.client` components are two 'halves' of a component and can be used in advanced use cases for separate implementations of a component on server and client side.
```bash [Directory Structure]
-| components/
---| Comments.client.vue
---| Comments.server.vue
+-| components/
+---| Comments.client.vue
+---| Comments.server.vue
```
```vue [pages/example.vue]
@@ -389,15 +389,15 @@ You can use the `components:dirs` hook to extend the directory list without requ
Imagine a directory structure like this:
```bash [Directory Structure]
-| node_modules/
+-| node_modules/
---| awesome-ui/
-------| components/
----------| Alert.vue
----------| Button.vue
-------| nuxt.js
-| pages/
+-----| components/
+-------| Alert.vue
+-------| Button.vue
+-----| nuxt.js
+-| pages/
---| index.vue
-| nuxt.config.js
+-| nuxt.config.js
```
Then in `awesome-ui/nuxt.js` you can use the `components:dirs` hook:
diff --git a/docs/2.guide/2.directory-structure/1.composables.md b/docs/2.guide/2.directory-structure/1.composables.md
index dbd3510b56..ed96746656 100644
--- a/docs/2.guide/2.directory-structure/1.composables.md
+++ b/docs/2.guide/2.directory-structure/1.composables.md
@@ -85,11 +85,11 @@ export const useHello = () => {
Nuxt only scans files at the top level of the [`composables/` directory](/docs/guide/directory-structure/composables), e.g.:
```bash [Directory Structure]
-| composables/
+-| composables/
---| index.ts // scanned
---| useFoo.ts // scanned
------| nested/
--------| utils.ts // not scanned
+---| nested/
+-----| utils.ts // not scanned
```
Only `composables/index.ts` and `composables/useFoo.ts` would be searched for imports.
diff --git a/docs/2.guide/2.directory-structure/1.middleware.md b/docs/2.guide/2.directory-structure/1.middleware.md
index 3fbcc732ce..d17c6ceb7c 100644
--- a/docs/2.guide/2.directory-structure/1.middleware.md
+++ b/docs/2.guide/2.directory-structure/1.middleware.md
@@ -72,11 +72,11 @@ Middleware runs in the following order:
For example, assuming you have the following middleware and component:
-```text [middleware/ directory]
-middleware/
---| analytics.global.ts
---| setup.global.ts
---| auth.ts
+```bash [middleware/ directory]
+-| middleware/
+---| analytics.global.ts
+---| setup.global.ts
+---| auth.ts
```
```vue twoslash [pages/profile.vue]
@@ -105,11 +105,11 @@ By default, global middleware is executed alphabetically based on the filename.
However, there may be times you want to define a specific order. For example, in the last scenario, `setup.global.ts` may need to run before `analytics.global.ts`. In that case, we recommend prefixing global middleware with 'alphabetical' numbering.
-```text [Directory structure]
-middleware/
---| 01.setup.global.ts
---| 02.analytics.global.ts
---| auth.ts
+```bash [Directory structure]
+-| middleware/
+---| 01.setup.global.ts
+---| 02.analytics.global.ts
+---| auth.ts
```
::note
diff --git a/docs/2.guide/2.directory-structure/1.pages.md b/docs/2.guide/2.directory-structure/1.pages.md
index ce14e9075f..0efb13daac 100644
--- a/docs/2.guide/2.directory-structure/1.pages.md
+++ b/docs/2.guide/2.directory-structure/1.pages.md
@@ -159,7 +159,7 @@ Example:
```bash [Directory Structure]
-| pages/
---| parent/
-------| child.vue
+-----| child.vue
---| parent.vue
```
@@ -408,7 +408,7 @@ However, you can use [Nuxt Layers](/docs/getting-started/layers) to create group
```bash [Directory Structure]
-| some-app/
---| nuxt.config.ts
----| pages
+---| pages/
-----| app-page.vue
-| nuxt.config.ts
```
diff --git a/docs/2.guide/2.directory-structure/1.plugins.md b/docs/2.guide/2.directory-structure/1.plugins.md
index d5edce56b3..572675300c 100644
--- a/docs/2.guide/2.directory-structure/1.plugins.md
+++ b/docs/2.guide/2.directory-structure/1.plugins.md
@@ -108,7 +108,7 @@ In case you're new to 'alphabetical' numbering, remember that filenames are sort
### Parallel Plugins
-By default, Nuxt loads plugins sequentially. You can define a plugin as `parallel` so Nuxt won't wait the end of the plugin's execution before loading the next plugin.
+By default, Nuxt loads plugins sequentially. You can define a plugin as `parallel` so Nuxt won't wait until the end of the plugin's execution before loading the next plugin.
```ts twoslash [plugins/my-plugin.ts]
export default defineNuxtPlugin({
diff --git a/docs/2.guide/2.directory-structure/3.app-config.md b/docs/2.guide/2.directory-structure/3.app-config.md
index 656d5c16a1..d170a6ce5a 100644
--- a/docs/2.guide/2.directory-structure/3.app-config.md
+++ b/docs/2.guide/2.directory-structure/3.app-config.md
@@ -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]
```
-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]
+
+```
+
+::read-more{to="/docs/api/utils/update-app-config"}
+Read more about the `updateAppConfig` utility.
+::
## Typing App Config
diff --git a/docs/2.guide/3.going-further/1.experimental-features.md b/docs/2.guide/3.going-further/1.experimental-features.md
index 31bed1a8ae..57cd77803e 100644
--- a/docs/2.guide/3.going-further/1.experimental-features.md
+++ b/docs/2.guide/3.going-further/1.experimental-features.md
@@ -334,6 +334,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]
diff --git a/docs/2.guide/3.going-further/1.features.md b/docs/2.guide/3.going-further/1.features.md
index 247df516e2..46150d86d3 100644
--- a/docs/2.guide/3.going-further/1.features.md
+++ b/docs/2.guide/3.going-further/1.features.md
@@ -61,6 +61,7 @@ export default defineNuxtConfig({
app: 'app'
},
experimental: {
+ scanPageMeta: 'after-resolve',
sharedPrerenderData: false,
compileTemplate: true,
resetAsyncDataToUndefined: true,
diff --git a/docs/2.guide/4.recipes/3.custom-usefetch.md b/docs/2.guide/4.recipes/3.custom-usefetch.md
index e8f25f6a2b..a0ac6d7e29 100644
--- a/docs/2.guide/4.recipes/3.custom-usefetch.md
+++ b/docs/2.guide/4.recipes/3.custom-usefetch.md
@@ -31,14 +31,8 @@ export default defineNuxtPlugin((nuxtApp) => {
baseURL: 'https://api.nuxt.com',
onRequest({ request, options, error }) {
if (session.value?.token) {
- const headers = options.headers ||= {}
- if (Array.isArray(headers)) {
- headers.push(['Authorization', `Bearer ${session.value?.token}`])
- } else if (headers instanceof Headers) {
- headers.set('Authorization', `Bearer ${session.value?.token}`)
- } else {
- headers.Authorization = `Bearer ${session.value?.token}`
- }
+ // note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
+ options.headers.set('Authorization', `Bearer ${session.value?.token}`)
}
},
async onResponseError({ response }) {
@@ -96,6 +90,28 @@ const { data: modules } = await useAPI('/modules')
```
+If you want to customize the type of any error returned, you can also do so:
+
+```ts
+import type { FetchError } from 'ofetch'
+import type { UseFetchOptions } from 'nuxt/app'
+
+interface CustomError {
+ message: string
+ statusCode: number
+}
+
+export function useAPI
(
+ url: string | (() => string),
+ options?: UseFetchOptions,
+) {
+ return useFetch>(url, {
+ ...options,
+ $fetch: useNuxtApp().$api
+ })
+}
+```
+
::note
This example demonstrates how to use a custom `useFetch`, but the same structure is identical for a custom `useAsyncData`.
::
diff --git a/docs/3.api/2.composables/use-fetch.md b/docs/3.api/2.composables/use-fetch.md
index 6b07d44737..56effdc62c 100644
--- a/docs/3.api/2.composables/use-fetch.md
+++ b/docs/3.api/2.composables/use-fetch.md
@@ -50,8 +50,8 @@ You can also use [interceptors](https://github.com/unjs/ofetch#%EF%B8%8F-interce
const { data, status, error, refresh, clear } = await useFetch('/api/auth/login', {
onRequest({ request, options }) {
// Set the request headers
- options.headers = options.headers || {}
- options.headers.authorization = '...'
+ // note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
+ options.headers.set('Authorization', '...')
},
onRequestError({ request, options, error }) {
// Handle the request errors
diff --git a/docs/3.api/2.composables/use-response-header.md b/docs/3.api/2.composables/use-response-header.md
new file mode 100644
index 0000000000..d78fd89a4a
--- /dev/null
+++ b/docs/3.api/2.composables/use-response-header.md
@@ -0,0 +1,48 @@
+---
+title: "useResponseHeader"
+description: "Use useResponseHeader to set a server response header."
+links:
+ - label: Source
+ icon: i-simple-icons-github
+ to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/ssr.ts
+ size: xs
+---
+
+::important
+This composable is available in Nuxt v3.14+.
+::
+
+You can use the built-in [`useResponseHeader`](/docs/api/composables/use-response-header) composable to set any server response header within your pages, components, and plugins.
+
+```ts
+// Set the a custom response header
+const header = useResponseHeader('X-My-Header');
+header.value = 'my-value';
+```
+
+## Example
+
+We can use `useResponseHeader` to easily set a response header on a per-page basis.
+
+```vue [pages/test.vue]
+
+
+
+ Test page with custom header
+ The response from the server for this "/test" page will have a custom "X-My-Header" header.
+
+```
+
+We can use `useResponseHeader` for example in Nuxt [middleware](/docs/guide/directory-structure/middleware) to set a response header for all pages.
+
+```ts [middleware/my-header-middleware.ts]
+export default defineNuxtRouteMiddleware((to, from) => {
+ const header = useResponseHeader('X-My-Always-Header');
+ header.value = `I'm Always here!`;
+});
+
+```
diff --git a/docs/3.api/3.utils/define-page-meta.md b/docs/3.api/3.utils/define-page-meta.md
index e8427faf84..49aa8bc849 100644
--- a/docs/3.api/3.utils/define-page-meta.md
+++ b/docs/3.api/3.utils/define-page-meta.md
@@ -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[]`
diff --git a/docs/3.api/3.utils/navigate-to.md b/docs/3.api/3.utils/navigate-to.md
index 664de1773b..270bc80a00 100644
--- a/docs/3.api/3.utils/navigate-to.md
+++ b/docs/3.api/3.utils/navigate-to.md
@@ -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`
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 54f547026e..b6777a9d28 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -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' },
})
diff --git a/package.json b/package.json
index 77e239e970..43b63a3cc0 100644
--- a/package.json
+++ b/package.json
@@ -35,44 +35,46 @@
},
"resolutions": {
"@nuxt/kit": "workspace:*",
+ "@nuxt/rspack-builder": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/ui-templates": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
- "@types/node": "20.16.10",
- "@vue/compiler-core": "3.5.10",
- "@vue/compiler-dom": "3.5.10",
- "@vue/shared": "3.5.10",
- "c12": "2.0.0-beta.3",
+ "@types/node": "20.17.0",
+ "@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.0.0",
- "magic-string": "^0.30.11",
+ "jiti": "2.3.3",
+ "magic-string": "^0.30.12",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxt": "workspace:*",
"ohash": "1.1.4",
"postcss": "8.4.47",
- "rollup": "4.22.5",
- "send": ">=0.19.0",
- "typescript": "5.6.2",
+ "rollup": "4.24.0",
+ "send": ">=1.1.0",
+ "typescript": "5.6.3",
"ufo": "1.5.4",
- "unbuild": "3.0.0-rc.8",
- "vite": "5.4.8",
- "vue": "3.5.10"
+ "unbuild": "3.0.0-rc.11",
+ "vite": "5.4.10",
+ "vue": "3.5.12"
},
"devDependencies": {
- "@eslint/js": "9.11.1",
- "@nuxt/eslint-config": "0.5.7",
+ "@eslint/js": "9.13.0",
+ "@nuxt/eslint-config": "0.6.0",
"@nuxt/kit": "workspace:*",
- "@nuxt/test-utils": "3.14.2",
+ "@nuxt/rspack-builder": "workspace:*",
+ "@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.10",
+ "@types/node": "20.17.0",
"@types/semver": "7.5.8",
- "@unhead/schema": "1.11.6",
- "@unhead/vue": "1.11.6",
+ "@unhead/schema": "1.11.10",
+ "@unhead/vue": "1.11.10",
"@vitejs/plugin-vue": "5.1.4",
- "@vitest/coverage-v8": "2.1.1",
+ "@vitest/coverage-v8": "2.1.3",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20",
"case-police": "0.7.0",
@@ -81,36 +83,36 @@
"cssnano": "7.0.6",
"destr": "2.0.3",
"devalue": "5.1.1",
- "eslint": "9.11.1",
+ "eslint": "9.13.0",
"eslint-plugin-no-only-tests": "3.3.0",
- "eslint-plugin-perfectionist": "3.7.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.0.0",
+ "jiti": "2.3.3",
"markdownlint-cli": "0.42.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
- "nuxi": "3.14.0",
+ "nuxi": "3.15.0",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1",
- "ofetch": "1.4.0",
+ "ofetch": "1.4.1",
"pathe": "1.1.2",
- "playwright-core": "1.47.2",
+ "playwright-core": "1.48.1",
"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.6",
- "typescript": "5.6.2",
+ "tinyexec": "0.3.1",
+ "tinyglobby": "0.2.9",
+ "typescript": "5.6.3",
"ufo": "1.5.4",
- "vitest": "2.1.1",
+ "vitest": "2.1.3",
"vitest-environment-nuxt": "1.0.1",
- "vue": "3.5.10",
+ "vue": "3.5.12",
"vue-router": "4.4.5",
"vue-tsc": "2.1.6"
},
- "packageManager": "pnpm@9.11.0",
+ "packageManager": "pnpm@9.12.2",
"engines": {
"node": "^16.10.0 || >=18.0.0"
},
diff --git a/packages/kit/build.config.ts b/packages/kit/build.config.ts
index 87a8ccb072..d2a1f7fa7d 100644
--- a/packages/kit/build.config.ts
+++ b/packages/kit/build.config.ts
@@ -6,6 +6,7 @@ export default defineBuildConfig({
'src/index',
],
externals: [
+ '@rspack/core',
'@nuxt/schema',
'nitropack',
'nitro',
diff --git a/packages/kit/package.json b/packages/kit/package.json
index e5c091f984..edfc5b5cd5 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -27,7 +27,7 @@
},
"dependencies": {
"@nuxt/schema": "workspace:*",
- "c12": "^2.0.0-beta.3",
+ "c12": "^2.0.1",
"consola": "^3.2.3",
"defu": "^6.1.4",
"destr": "^2.0.3",
@@ -35,25 +35,26 @@
"globby": "^14.0.2",
"hash-sum": "^2.0.0",
"ignore": "^6.0.2",
- "jiti": "^2.0.0",
+ "jiti": "^2.3.3",
"klona": "^2.0.6",
- "mlly": "^1.7.1",
+ "mlly": "^1.7.2",
"pathe": "^1.1.2",
- "pkg-types": "^1.2.0",
+ "pkg-types": "^1.2.1",
"scule": "^1.3.0",
"semver": "^7.6.3",
"ufo": "^1.5.4",
"unctx": "^2.3.1",
"unimport": "^3.13.1",
- "untyped": "^1.5.0"
+ "untyped": "^1.5.1"
},
"devDependencies": {
+ "@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",
- "unbuild": "3.0.0-rc.8",
- "vite": "5.4.8",
- "vitest": "2.1.1",
+ "unbuild": "3.0.0-rc.11",
+ "vite": "5.4.10",
+ "vitest": "2.1.3",
"webpack": "5.95.0"
},
"engines": {
diff --git a/packages/kit/src/build.ts b/packages/kit/src/build.ts
index fab8c45228..ba4a1d17ba 100644
--- a/packages/kit/src/build.ts
+++ b/packages/kit/src/build.ts
@@ -1,4 +1,5 @@
import type { Configuration as WebpackConfig, WebpackPluginInstance } from 'webpack'
+import type { RspackPluginInstance } from '@rspack/core'
import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite'
import { useNuxt } from './context'
import { toArray } from './utils'
@@ -36,16 +37,7 @@ export interface ExtendWebpackConfigOptions extends ExtendConfigOptions {}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ExtendViteConfigOptions extends ExtendConfigOptions {}
-/**
- * Extend webpack config
- *
- * The fallback function might be called multiple times
- * when applying to both client and server builds.
- */
-export function extendWebpackConfig (
- fn: ((config: WebpackConfig) => void),
- options: ExtendWebpackConfigOptions = {},
-) {
+const extendWebpackCompatibleConfig = (builder: 'rspack' | 'webpack') => (fn: ((config: WebpackConfig) => void), options: ExtendWebpackConfigOptions = {}) => {
const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) {
@@ -55,7 +47,7 @@ export function extendWebpackConfig (
return
}
- nuxt.hook('webpack:config', (configs: WebpackConfig[]) => {
+ nuxt.hook(`${builder}:config`, (configs) => {
if (options.server !== false) {
const config = configs.find(i => i.name === 'server')
if (config) {
@@ -71,13 +63,25 @@ export function extendWebpackConfig (
})
}
+/**
+ * Extend webpack config
+ *
+ * The fallback function might be called multiple times
+ * when applying to both client and server builds.
+ */
+export const extendWebpackConfig = extendWebpackCompatibleConfig('webpack')
+/**
+ * Extend rspack config
+ *
+ * The fallback function might be called multiple times
+ * when applying to both client and server builds.
+ */
+export const extendRspackConfig = extendWebpackCompatibleConfig('rspack')
+
/**
* Extend Vite config
*/
-export function extendViteConfig (
- fn: ((config: ViteConfig) => void),
- options: ExtendViteConfigOptions = {},
-) {
+export function extendViteConfig (fn: ((config: ViteConfig) => void), options: ExtendViteConfigOptions = {}) {
const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) {
@@ -114,6 +118,18 @@ export function addWebpackPlugin (pluginOrGetter: WebpackPluginInstance | Webpac
config.plugins[method](...toArray(plugin))
}, options)
}
+/**
+ * Append rspack plugin to the config.
+ */
+export function addRspackPlugin (pluginOrGetter: RspackPluginInstance | RspackPluginInstance[] | (() => RspackPluginInstance | RspackPluginInstance[]), options?: ExtendWebpackConfigOptions) {
+ extendRspackConfig((config) => {
+ const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
+ const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
+
+ config.plugins = config.plugins || []
+ config.plugins[method](...toArray(plugin))
+ }, options)
+}
/**
* Append Vite plugin to the config.
@@ -131,6 +147,7 @@ export function addVitePlugin (pluginOrGetter: VitePlugin | VitePlugin[] | (() =
interface AddBuildPluginFactory {
vite?: () => VitePlugin | VitePlugin[]
webpack?: () => WebpackPluginInstance | WebpackPluginInstance[]
+ rspack?: () => RspackPluginInstance | RspackPluginInstance[]
}
export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?: ExtendConfigOptions) {
@@ -141,4 +158,8 @@ export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?:
if (pluginFactory.webpack) {
addWebpackPlugin(pluginFactory.webpack, options)
}
+
+ if (pluginFactory.rspack) {
+ addRspackPlugin(pluginFactory.rspack, options)
+ }
}
diff --git a/packages/kit/src/compatibility.ts b/packages/kit/src/compatibility.ts
index 00ad74b67c..e650dc0710 100644
--- a/packages/kit/src/compatibility.ts
+++ b/packages/kit/src/compatibility.ts
@@ -3,11 +3,13 @@ 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 = {
+ '@nuxt/rspack-builder': 'rspack',
'@nuxt/vite-builder': 'vite',
'@nuxt/webpack-builder': 'webpack',
}
@@ -103,6 +105,7 @@ export function isNuxt3 (nuxt: Nuxt = useNuxt()) {
return isNuxtMajorVersion(3, nuxt)
}
+const NUXT_VERSION_RE = /^v/g
/**
* Get nuxt version
*/
@@ -111,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, '')
}
diff --git a/packages/kit/src/components.ts b/packages/kit/src/components.ts
index 669d6ce410..82394e3811 100644
--- a/packages/kit/src/components.ts
+++ b/packages/kit/src/components.ts
@@ -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'
}
diff --git a/packages/kit/src/index.ts b/packages/kit/src/index.ts
index bde038e6fb..c9b94204b0 100644
--- a/packages/kit/src/index.ts
+++ b/packages/kit/src/index.ts
@@ -13,7 +13,7 @@ export type { LoadNuxtOptions } from './loader/nuxt'
// Utils
export { addImports, addImportsDir, addImportsSources } from './imports'
export { updateRuntimeConfig, useRuntimeConfig } from './runtime-config'
-export { addBuildPlugin, addVitePlugin, addWebpackPlugin, extendViteConfig, extendWebpackConfig } from './build'
+export { addBuildPlugin, addVitePlugin, addRspackPlugin, addWebpackPlugin, extendViteConfig, extendRspackConfig, extendWebpackConfig } from './build'
export type { ExtendConfigOptions, ExtendViteConfigOptions, ExtendWebpackConfigOptions } from './build'
export { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility'
export { addComponent, addComponentsDir } from './components'
diff --git a/packages/kit/src/layout.ts b/packages/kit/src/layout.ts
index 65fd183163..a416fa8b21 100644
--- a/packages/kit/src/layout.ts
+++ b/packages/kit/src/layout.ts
@@ -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) => {
diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts
index 1149fc9d94..39a460cbed 100644
--- a/packages/kit/src/module/install.ts
+++ b/packages/kit/src/module/install.ts
@@ -72,10 +72,7 @@ export const normalizeModuleTranspilePath = (p: string) => {
export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) {
let buildTimeModuleMeta: ModuleMeta = {}
- const jiti = createJiti(nuxt.options.rootDir, {
- interopDefault: true,
- alias: nuxt.options.alias,
- })
+ const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias })
// Import if input is string
if (typeof nuxtModule === 'string') {
@@ -85,7 +82,7 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
for (const path of paths) {
try {
const src = jiti.esmResolve(path, { parentURL: parentURL.replace(/\/node_modules\/?$/, '') })
- nuxtModule = await jiti.import(src) as NuxtModule
+ nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
// nuxt-module-builder generates a module.json with metadata including the version
const moduleMetadataPath = join(dirname(src), 'module.json')
diff --git a/packages/kit/src/nitro.ts b/packages/kit/src/nitro.ts
index 29b0b44981..4c046f6a9d 100644
--- a/packages/kit/src/nitro.ts
+++ b/packages/kit/src/nitro.ts
@@ -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,
diff --git a/packages/kit/src/plugin.ts b/packages/kit/src/plugin.ts
index aadcdbc718..116721214f 100644
--- a/packages/kit/src/plugin.ts
+++ b/packages/kit/src/plugin.ts
@@ -3,6 +3,7 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
import { useNuxt } from './context'
import { addTemplate } from './template'
import { resolveAlias } from './resolve'
+import { 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'
}
diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts
index 903308497f..8b4ae046fd 100644
--- a/packages/kit/src/template.ts
+++ b/packages/kit/src/template.ts
@@ -1,7 +1,7 @@
import { existsSync, promises as fsp } from 'node:fs'
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
import hash from 'hash-sum'
-import type { Nuxt, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
+import type { Nuxt, NuxtServerTemplate, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
import { withTrailingSlash } from 'ufo'
import { defu } from 'defu'
import type { TSConfig } from 'pkg-types'
@@ -32,6 +32,18 @@ export function addTemplate (_template: NuxtTemplate | string) {
return template
}
+/**
+ * Adds a virtual file that can be used within the Nuxt Nitro server build.
+ */
+export function addServerTemplate (template: NuxtServerTemplate) {
+ const nuxt = useNuxt()
+
+ nuxt.options.nitro.virtual ||= {}
+ nuxt.options.nitro.virtual[template.filename] = template.getContents
+
+ return template
+}
+
/**
* Renders given types using lodash template during build into the project buildDir
* and register them as types.
@@ -111,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)
@@ -211,13 +226,7 @@ export async function _generateTypes (nuxt: Nuxt) {
exclude: [...exclude],
} satisfies TSConfig)
- const aliases: Record = {
- ...nuxt.options.alias,
- '#build': nuxt.options.buildDir,
- }
-
- // Exclude bridge alias types to support Volar
- const excludedAlias = [/^@vue\/.*$/]
+ const aliases: Record = nuxt.options.alias
const basePath = tsConfig.compilerOptions!.baseUrl
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
@@ -251,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]!
@@ -280,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)
}))
}
@@ -335,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') || '.'
}
diff --git a/packages/kit/src/utils.ts b/packages/kit/src/utils.ts
index 72b096120b..89fa591c50 100644
--- a/packages/kit/src/utils.ts
+++ b/packages/kit/src/utils.ts
@@ -2,3 +2,5 @@
export function toArray (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value]
}
+
+export const MODE_RE = /\.(server|client)(\.\w+)*$/
diff --git a/packages/kit/test/generate-types.spec.ts b/packages/kit/test/generate-types.spec.ts
index b5bcc9a6bb..d65d6809bd 100644
--- a/packages/kit/test/generate-types.spec.ts
+++ b/packages/kit/test/generate-types.spec.ts
@@ -34,9 +34,6 @@ describe('tsConfig generation', () => {
const { tsConfig } = await _generateTypes(mockNuxt)
expect(tsConfig.compilerOptions?.paths).toMatchInlineSnapshot(`
{
- "#build": [
- ".",
- ],
"some-custom-alias": [
"../some-alias",
],
diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json
index b07297467d..21c02fa5f6 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -60,19 +60,19 @@
},
"dependencies": {
"@nuxt/devalue": "^2.0.2",
- "@nuxt/devtools": "^1.5.1",
+ "@nuxt/devtools": "^1.6.0",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.0",
"@nuxt/vite-builder": "workspace:*",
- "@unhead/dom": "^1.11.6",
- "@unhead/shared": "^1.11.6",
- "@unhead/ssr": "^1.11.6",
- "@unhead/vue": "^1.11.6",
- "@vue/shared": "^3.5.10",
- "acorn": "8.12.1",
- "c12": "^2.0.0-beta.3",
- "chokidar": "^3.6.0",
+ "@unhead/dom": "^1.11.10",
+ "@unhead/shared": "^1.11.10",
+ "@unhead/ssr": "^1.11.10",
+ "@unhead/vue": "^1.11.10",
+ "@vue/shared": "^3.5.12",
+ "acorn": "8.13.0",
+ "c12": "^2.0.1",
+ "chokidar": "^4.0.1",
"compatx": "^0.1.8",
"consola": "^3.2.3",
"cookie-es": "^1.2.2",
@@ -87,53 +87,53 @@
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hookable": "^5.5.3",
"ignore": "^6.0.2",
- "impound": "^0.1.0",
- "jiti": "^2.0.0",
+ "impound": "^0.2.0",
+ "jiti": "^2.3.3",
"klona": "^2.0.6",
"knitwork": "^1.1.0",
- "magic-string": "^0.30.11",
- "mlly": "^1.7.1",
+ "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",
+ "nuxi": "^3.15.0",
"nypm": "^0.3.12",
- "ofetch": "^1.4.0",
+ "ofetch": "^1.4.1",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"perfect-debounce": "^1.0.0",
- "pkg-types": "^1.2.0",
+ "pkg-types": "^1.2.1",
"radix3": "^1.1.2",
"scule": "^1.3.0",
"semver": "^7.6.3",
"std-env": "^3.7.0",
"strip-literal": "^2.1.0",
- "tinyglobby": "0.2.6",
+ "tinyglobby": "0.2.9",
"ufo": "^1.5.4",
"ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3",
"unctx": "^2.3.1",
"unenv": "^1.10.0",
- "unhead": "^1.11.6",
+ "unhead": "^1.11.10",
"unimport": "^3.13.1",
"unplugin": "^1.14.1",
"unplugin-vue-router": "^0.10.8",
"unstorage": "^1.12.0",
- "untyped": "^1.5.0",
- "vue": "^3.5.10",
+ "untyped": "^1.5.1",
+ "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",
"@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.1.4",
- "@vue/compiler-sfc": "3.5.10",
- "unbuild": "3.0.0-rc.8",
- "vite": "5.4.8",
- "vitest": "2.1.1"
+ "@vue/compiler-sfc": "3.5.12",
+ "unbuild": "3.0.0-rc.11",
+ "vite": "5.4.10",
+ "vitest": "2.1.3"
},
"peerDependencies": {
"@parcel/watcher": "^2.1.0",
diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts
index c6bb6d8657..20d1f11cbf 100644
--- a/packages/nuxt/src/app/components/nuxt-island.ts
+++ b/packages/nuxt/src/app/components/nuxt-island.ts
@@ -1,9 +1,9 @@
import type { Component, PropType, VNode } from 'vue'
-import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
+import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendResponseHeader } from 'h3'
-import { injectHead } from '@unhead/vue'
+import { type ActiveHeadEntry, type Head, injectHead } from '@unhead/vue'
import { randomUUID } from 'uncrypto'
import { joinURL, withQuery } from 'ufo'
import type { FetchResponse } from 'ofetch'
@@ -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
@@ -90,11 +91,13 @@ export default defineComponent({
const instance = getCurrentInstance()!
const event = useRequestEvent()
+ let activeHead: ActiveHeadEntry
+
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
const eventFetch = import.meta.server ? event!.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch
const mounted = ref(false)
onMounted(() => { mounted.value = true; teleportKey.value++ })
-
+ onBeforeUnmount(() => { if (activeHead) { activeHead.dispose() } })
function setPayload (key: string, result: NuxtIslandResponse) {
const toRevive: Partial = {}
if (result.props) { toRevive.props = result.props }
@@ -140,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) {
@@ -215,6 +218,14 @@ export default defineComponent({
}
}
+ if (res?.head) {
+ if (activeHead) {
+ activeHead.patch(res.head)
+ } else {
+ activeHead = head.push(res.head)
+ }
+ }
+
if (import.meta.client) {
// must await next tick for Teleport to work correctly with static node re-rendering
nextTick(() => {
@@ -250,14 +261,6 @@ export default defineComponent({
await loadComponents(props.source, payloads.components)
}
- if (import.meta.server || nuxtApp.isHydrating) {
- // re-push head into active head instance
- const responseHead = (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head
- if (responseHead) {
- head.push(responseHead)
- }
- }
-
return (_ctx: any, _cache: any) => {
if (!html.value || error.value) {
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts
index f38ddbd777..aa50e11e09 100644
--- a/packages/nuxt/src/app/components/nuxt-link.ts
+++ b/packages/nuxt/src/app/components/nuxt-link.ts
@@ -328,8 +328,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const path = typeof to.value === 'string'
? to.value
: isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath
+ const normalizedPath = isExternal.value ? new URL(path, window.location.href).href : path
await Promise.all([
- nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}),
+ nuxtApp.hooks.callHook('link:prefetch', normalizedPath).catch(() => {}),
!isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
])
}
@@ -520,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
}
diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts
index 0bde127ec5..5fe9739e31 100644
--- a/packages/nuxt/src/app/components/utils.ts
+++ b/packages/nuxt/src/app/components/utils.ts
@@ -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
}
diff --git a/packages/nuxt/src/app/composables/component.ts b/packages/nuxt/src/app/composables/component.ts
index e8bd93e20c..914533e757 100644
--- a/packages/nuxt/src/app/composables/component.ts
+++ b/packages/nuxt/src/app/composables/component.ts
@@ -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)
}
diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts
index c0abdec2e4..05ba560d09 100644
--- a/packages/nuxt/src/app/composables/index.ts
+++ b/packages/nuxt/src/app/composables/index.ts
@@ -24,7 +24,7 @@ export { useFetch, useLazyFetch } from './fetch'
export type { FetchResult, UseFetchOptions } from './fetch'
export { useCookie, refreshCookie } from './cookie'
export type { CookieOptions, CookieRef } from './cookie'
-export { onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus } from './ssr'
+export { onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader } from './ssr'
export { onNuxtReady } from './ready'
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter } from './router'
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts
index fa8be0805c..0814873ff0 100644
--- a/packages/nuxt/src/app/composables/router.ts
+++ b/packages/nuxt/src/app/composables/router.ts
@@ -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 | 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 = {
diff --git a/packages/nuxt/src/app/composables/ssr.ts b/packages/nuxt/src/app/composables/ssr.ts
index 56f3383109..59db9bd327 100644
--- a/packages/nuxt/src/app/composables/ssr.ts
+++ b/packages/nuxt/src/app/composables/ssr.ts
@@ -1,6 +1,6 @@
import type { H3Event } from 'h3'
-import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders } from 'h3'
-import { getCurrentInstance } from 'vue'
+import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders, getResponseHeader, removeResponseHeader, setResponseHeader } from 'h3'
+import { computed, getCurrentInstance, ref } from 'vue'
import { useServerHead } from '@unhead/vue'
import type { NuxtApp } from '../nuxt'
@@ -61,6 +61,34 @@ export function setResponseStatus (arg1: H3Event | number | undefined, arg2?: nu
}
}
+/** @since 3.14.0 */
+export function useResponseHeader (header: string) {
+ if (import.meta.client) {
+ if (import.meta.dev) {
+ return computed({
+ get: () => undefined,
+ set: () => console.warn('[nuxt] Setting response headers is not supported in the browser.'),
+ })
+ }
+ return ref()
+ }
+
+ const event = useRequestEvent()!
+
+ return computed({
+ get () {
+ return getResponseHeader(event, header)
+ },
+ set (newValue) {
+ if (!newValue) {
+ return removeResponseHeader(event, header)
+ }
+
+ return setResponseHeader(event, header, newValue)
+ },
+ })
+}
+
/** @since 3.8.0 */
export function prerenderRoutes (path: string | string[]) {
if (!import.meta.server || !import.meta.prerender) { return }
diff --git a/packages/nuxt/src/app/index.ts b/packages/nuxt/src/app/index.ts
index 363c72d1bf..43fcc404e1 100644
--- a/packages/nuxt/src/app/index.ts
+++ b/packages/nuxt/src/app/index.ts
@@ -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, 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 } 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'
diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts
index 5c2d3102db..a2a1d8ca7d 100644
--- a/packages/nuxt/src/components/module.ts
+++ b/packages/nuxt/src/components/module.ts
@@ -1,6 +1,6 @@
import { existsSync, statSync, writeFileSync } from 'node:fs'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
-import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
+import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
import { distDir } from '../dirs'
@@ -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[]
@@ -32,7 +34,7 @@ export default defineNuxtModule({
defaults: {
dirs: [],
},
- setup (componentOptions, nuxt) {
+ async setup (componentOptions, nuxt) {
let componentDirs: ComponentsDir[] = []
const context = {
components: [] as Component[],
@@ -89,7 +91,7 @@ export default defineNuxtModule({
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)) {
@@ -134,8 +136,9 @@ export default defineNuxtModule({
addTemplate(componentsMetadataTemplate)
}
- addBuildPlugin(TransformPlugin(nuxt, getComponents, 'server'), { server: true, client: false })
- addBuildPlugin(TransformPlugin(nuxt, getComponents, 'client'), { server: false, client: true })
+ const serverComponentRuntime = await findPath(join(distDir, 'components/runtime/server-component')) ?? join(distDir, 'components/runtime/server-component')
+ addBuildPlugin(TransformPlugin(nuxt, { getComponents, serverComponentRuntime, mode: 'server' }), { server: true, client: false })
+ addBuildPlugin(TransformPlugin(nuxt, { getComponents, serverComponentRuntime, mode: 'client' }), { server: false, client: true })
// Do not prefetch global components chunks
nuxt.hook('build:manifest', (manifest) => {
@@ -162,7 +165,7 @@ export default defineNuxtModule({
}
})
- const serverPlaceholderPath = resolve(distDir, 'app/components/server-placeholder')
+ const serverPlaceholderPath = await findPath(join(distDir, 'app/components/server-placeholder')) ?? join(distDir, 'app/components/server-placeholder')
// Scan components and add to plugin
nuxt.hook('app:templates', async (app) => {
@@ -222,6 +225,7 @@ export default defineNuxtModule({
const sharedLoaderOptions = {
getComponents,
+ serverComponentRuntime,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands,
}
@@ -272,16 +276,18 @@ export default defineNuxtModule({
}
})
- nuxt.hook('webpack:config', (configs) => {
- configs.forEach((config) => {
- const mode = config.name === 'client' ? 'client' : 'server'
- config.plugins = config.plugins || []
+ for (const key of ['rspack:config', 'webpack:config'] as const) {
+ nuxt.hook(key, (configs) => {
+ configs.forEach((config) => {
+ const mode = config.name === 'client' ? 'client' : 'server'
+ config.plugins = config.plugins || []
- if (mode !== 'server') {
- writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
- }
+ if (mode !== 'server') {
+ writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
+ }
+ })
})
- })
+ }
}
},
})
diff --git a/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts
index 9f3e1b119c..85c3d8b82d 100644
--- a/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts
+++ b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts
@@ -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 ?? ''}>`
})
diff --git a/packages/nuxt/src/components/plugins/component-names.ts b/packages/nuxt/src/components/plugins/component-names.ts
index 4a24ebe96b..af01adb5dc 100644
--- a/packages/nuxt/src/components/plugins/component-names.ts
+++ b/packages/nuxt/src/components/plugins/component-names.ts
@@ -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
}
diff --git a/packages/nuxt/src/components/plugins/islands-transform.ts b/packages/nuxt/src/components/plugins/islands-transform.ts
index 70ad9fb895..a3e2aba41a 100644
--- a/packages/nuxt/src/components/plugins/islands-transform.ts
+++ b/packages/nuxt/src/components/plugins/islands-transform.ts
@@ -30,6 +30,7 @@ const TEMPLATE_RE = /([\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 `${code}
`
@@ -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, `${attributes['v-for'] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}`)
} 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, ''))
diff --git a/packages/nuxt/src/components/plugins/loader.ts b/packages/nuxt/src/components/plugins/loader.ts
index 2eecc471b1..247553ae47 100644
--- a/packages/nuxt/src/components/plugins/loader.ts
+++ b/packages/nuxt/src/components/plugins/loader.ts
@@ -2,25 +2,26 @@ import { createUnplugin } from 'unplugin'
import { genDynamicImport, genImport } from 'knitwork'
import MagicString from 'magic-string'
import { pascalCase } from 'scule'
-import { resolve } from 'pathe'
+import { relative } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit'
-import { distDir } from '../../dirs'
-import { isVue } from '../../core/utils'
+import { QUOTE_RE, SX_RE, isVue } from '../../core/utils'
interface LoaderOptions {
getComponents (): Component[]
mode: 'server' | 'client'
+ serverComponentRuntime: string
sourcemap?: boolean
transform?: ComponentsOptions['transform']
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 || []
- const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component')
+ const nuxt = tryUseNuxt()
return {
name: 'nuxt:components-loader',
@@ -32,9 +33,9 @@ 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) {
+ transform (code, id) {
const components = options.getComponents()
let num = 0
@@ -43,13 +44,17 @@ 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) {
- // @ts-expect-error TODO: refactor to nuxi
- if (component._internal_install && tryUseNuxt()?.options.test === false) {
- // @ts-expect-error TODO: refactor to nuxi
- import('../../core/features').then(({ installNuxtModule }) => installNuxtModule(component._internal_install))
+ // TODO: refactor to nuxi
+ const internalInstall = ((component as any)._internal_install) as string
+ if (internalInstall && nuxt?.options.test === false) {
+ if (!nuxt.options.dev) {
+ const relativePath = relative(nuxt.options.rootDir, id)
+ throw new Error(`[nuxt] \`~/${relativePath}\` is using \`${component.pascalName}\` which requires \`${internalInstall}\``)
+ }
+ import('../../core/features').then(({ installNuxtModule }) => installNuxtModule(internalInstall))
}
let identifier = map.get(component) || `__nuxt_component_${num++}`
map.set(component, identifier)
@@ -57,7 +62,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const isServerOnly = !component._raw && component.mode === 'server' &&
!components.some(c => c.pascalName === component.pascalName && c.mode === 'client')
if (isServerOnly) {
- imports.add(genImport(serverComponentRuntime, [{ name: 'createServerComponent' }]))
+ imports.add(genImport(options.serverComponentRuntime, [{ name: 'createServerComponent' }]))
imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(component.pascalName)})`)
if (!options.experimentalComponentIslands) {
logger.warn(`Standalone server components (\`${name}\`) are not yet supported without enabling \`experimental.componentIslands\`.`)
@@ -107,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 }
diff --git a/packages/nuxt/src/components/plugins/transform.ts b/packages/nuxt/src/components/plugins/transform.ts
index 85108f0122..ba819bbfa3 100644
--- a/packages/nuxt/src/components/plugins/transform.ts
+++ b/packages/nuxt/src/components/plugins/transform.ts
@@ -5,15 +5,19 @@ import { createUnimport } from 'unimport'
import { createUnplugin } from 'unplugin'
import { parseURL } from 'ufo'
import { parseQuery } from 'vue-router'
-import { normalize, resolve } from 'pathe'
+import { normalize } from 'pathe'
import { genImport } from 'knitwork'
-import { distDir } from '../../dirs'
import type { getComponentsT } from '../module'
const COMPONENT_QUERY_RE = /[?&]nuxt_component=/
-export function TransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode: 'client' | 'server' | 'all') {
- const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component')
+interface TransformPluginOptions {
+ getComponents: getComponentsT
+ mode: 'client' | 'server' | 'all'
+ serverComponentRuntime: string
+}
+
+export function TransformPlugin (nuxt: Nuxt, options: TransformPluginOptions) {
const componentUnimport = createUnimport({
imports: [
{
@@ -26,7 +30,7 @@ export function TransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode
})
function getComponentsImports (): Import[] {
- const components = getComponents(mode)
+ const components = options.getComponents(options.mode)
return components.flatMap((c): Import[] => {
const withMode = (mode: string | undefined) => mode
? `${c.filePath}${c.filePath.includes('?') ? '&' : '?'}nuxt_component=${mode}&nuxt_component_name=${c.pascalName}&nuxt_component_export=${c.export || 'default'}`
@@ -95,7 +99,7 @@ export function TransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode
const name = query.nuxt_component_name
return {
code: [
- `import { createServerComponent } from ${JSON.stringify(serverComponentRuntime)}`,
+ `import { createServerComponent } from ${JSON.stringify(options.serverComponentRuntime)}`,
`${exportWording} createServerComponent(${JSON.stringify(name)})`,
].join('\n'),
map: null,
diff --git a/packages/nuxt/src/components/plugins/tree-shake.ts b/packages/nuxt/src/components/plugins/tree-shake.ts
index ff68f2fa21..2ebb669a03 100644
--- a/packages/nuxt/src/components/plugins/tree-shake.ts
+++ b/packages/nuxt/src/components/plugins/tree-shake.ts
@@ -33,8 +33,9 @@ export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions)
const components = options.getComponents()
if (!regexpMap.has(components)) {
+ const serverPlaceholderPath = resolve(distDir, 'app/components/server-placeholder')
const clientOnlyComponents = components
- .filter(c => c.mode === 'client' && !components.some(other => other.mode !== 'client' && other.pascalName === c.pascalName && other.filePath !== resolve(distDir, 'app/components/server-placeholder')))
+ .filter(c => c.mode === 'client' && !components.some(other => other.mode !== 'client' && other.pascalName === c.pascalName && !other.filePath.startsWith(serverPlaceholderPath)))
.flatMap(c => [c.pascalName, c.kebabName.replaceAll('-', '_')])
.concat(['ClientOnly', 'client_only'])
diff --git a/packages/nuxt/src/components/scan.ts b/packages/nuxt/src/components/scan.ts
index 01d8331cb3..30adc35a21 100644
--- a/packages/nuxt/src/components/scan.ts
+++ b/packages/nuxt/src/components/scan.ts
@@ -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)) {
diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts
index 9707894ff1..09384a2e57 100644
--- a/packages/nuxt/src/components/templates.ts
+++ b/packages/nuxt/src/components/templates.ts
@@ -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,
diff --git a/packages/nuxt/src/core/app.ts b/packages/nuxt/src/core/app.ts
index 50ac4118e5..4aebfd2902 100644
--- a/packages/nuxt/src/core/app.ts
+++ b/packages/nuxt/src/core/app.ts
@@ -57,7 +57,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
const writes: Array<() => void> = []
const changedTemplates: Array> = []
-
+ const FORWARD_SLASH_RE = /\//g
async function processTemplate (template: ResolvedNuxtTemplate) {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const start = performance.now()
@@ -72,12 +72,12 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
if (template.modified) {
nuxt.vfs[fullPath] = contents
- const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
+ const aliasPath = '#build/' + template.filename
nuxt.vfs[aliasPath] = contents
// 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)
diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts
index 1e8d7eb0e1..013c80fbe7 100644
--- a/packages/nuxt/src/core/builder.ts
+++ b/packages/nuxt/src/core/builder.ts
@@ -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
diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts
index 8c2b746d5c..d00d4639e6 100644
--- a/packages/nuxt/src/core/nitro.ts
+++ b/packages/nuxt/src/core/nitro.ts
@@ -18,6 +18,7 @@ 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 { EXTENSION_RE } from './utils'
const logLevelMapReverse = {
silent: 0,
@@ -25,12 +26,14 @@ const logLevelMapReverse = {
verbose: 3,
} satisfies Record
+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))
@@ -101,8 +104,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
devHandlers: [],
baseURL: nuxt.options.app.baseURL,
virtual: {
- '#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config'],
- '#internal/nuxt/app-config': () => nuxt.vfs['#build/app.config']?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, ''),
+ '#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config.mjs'],
+ '#internal/nuxt/app-config': () => nuxt.vfs['#build/app.config.mjs']?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, ''),
'#spa-template': async () => `export const template = ${JSON.stringify(await spaLoadingTemplate(nuxt))}`,
},
routeRules: {
@@ -189,11 +192,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
},
'@vue/devtools-api': 'vue-devtools-stub',
- // Paths
- '#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/paths'),
-
// Nuxt aliases
...nuxt.options.alias,
+
+ // Paths
+ '#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/paths'),
},
replace: {
'process.env.NUXT_NO_SSR': nuxt.options.ssr === false,
@@ -339,19 +342,20 @@ 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 () => {}'
}
}
- if (nuxt.options.builder === '@nuxt/webpack-builder' || nuxt.options.dev) {
+ if (nuxt.options.dev || nuxt.options.builder === '@nuxt/webpack-builder' || nuxt.options.builder === '@nuxt/rspack-builder') {
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 {}'
}
}
@@ -389,7 +393,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 */
}
}
@@ -448,18 +452,20 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}
}
})
- nuxt.hook('webpack:config', (configuration) => {
- const clientConfig = configuration.find(config => config.name === 'client')
- if (!clientConfig!.resolve) { clientConfig!.resolve!.alias = {} }
- if (Array.isArray(clientConfig!.resolve!.alias)) {
- clientConfig!.resolve!.alias.push({
- name: 'vue',
- alias: 'vue/dist/vue.esm-bundler',
- })
- } else {
- clientConfig!.resolve!.alias!.vue = 'vue/dist/vue.esm-bundler'
- }
- })
+ for (const hook of ['webpack:config', 'rspack:config'] as const) {
+ nuxt.hook(hook, (configuration) => {
+ const clientConfig = configuration.find(config => config.name === 'client')
+ if (!clientConfig!.resolve) { clientConfig!.resolve!.alias = {} }
+ if (Array.isArray(clientConfig!.resolve!.alias)) {
+ clientConfig!.resolve!.alias.push({
+ name: 'vue',
+ alias: 'vue/dist/vue.esm-bundler',
+ })
+ } else {
+ clientConfig!.resolve!.alias!.vue = 'vue/dist/vue.esm-bundler'
+ }
+ })
+ }
}
// Setup handlers
@@ -545,13 +551,15 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// nuxt dev
if (nuxt.options.dev) {
- nuxt.hook('webpack:compile', ({ name, compiler }) => {
- if (name === 'server') {
- const memfs = compiler.outputFileSystem as typeof import('node:fs')
- nitro.options.virtual['#build/dist/server/server.mjs'] = () => memfs.readFileSync(join(nuxt.options.buildDir, 'dist/server/server.mjs'), 'utf-8')
- }
- })
- nuxt.hook('webpack:compiled', () => { nuxt.server.reload() })
+ for (const builder of ['webpack', 'rspack'] as const) {
+ nuxt.hook(`${builder}:compile`, ({ name, compiler }) => {
+ if (name === 'server') {
+ const memfs = compiler.outputFileSystem as typeof import('node:fs')
+ nitro.options.virtual['#build/dist/server/server.mjs'] = () => memfs.readFileSync(join(nuxt.options.buildDir, 'dist/server/server.mjs'), 'utf-8')
+ }
+ })
+ nuxt.hook(`${builder}:compiled`, () => { nuxt.server.reload() })
+ }
nuxt.hook('vite:compiled', () => { nuxt.server.reload() })
nuxt.hook('server:devHandler', (h) => { devMiddlewareHandler.set(h) })
@@ -562,8 +570,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) {
diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts
index 6af0933b8a..a4c7887817 100644
--- a/packages/nuxt/src/core/nuxt.ts
+++ b/packages/nuxt/src/core/nuxt.ts
@@ -44,6 +44,7 @@ import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
import { AsyncContextInjectionPlugin } from './plugins/async-context'
import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports'
import { prehydrateTransformPlugin } from './plugins/prehydrate'
+import { VirtualFSPlugin } from './plugins/virtual'
export function createNuxt (options: NuxtOptions): Nuxt {
const hooks = createHooks()
@@ -177,9 +178,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
@@ -240,6 +242,10 @@ async function initNuxt (nuxt: Nuxt) {
}
}
+ // Support Nuxt VFS
+ addBuildPlugin(VirtualFSPlugin(nuxt, { mode: 'server' }), { client: false })
+ addBuildPlugin(VirtualFSPlugin(nuxt, { mode: 'client', alias: { 'nitro/runtime': join(nuxt.options.buildDir, 'nitro.client.mjs') } }), { server: false })
+
// Add plugin normalization plugin
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
@@ -492,9 +498,11 @@ async function initNuxt (nuxt: Nuxt) {
const envMap = {
// defaults from `builder` based on package name
+ '@nuxt/rspack-builder': '@rspack/core/module',
'@nuxt/vite-builder': 'vite/client',
'@nuxt/webpack-builder': 'webpack/module',
// simpler overrides from `typescript.builder` for better DX
+ 'rspack': '@rspack/core/module',
'vite': 'vite/client',
'webpack': 'webpack/module',
// default 'merged' builder environment for module authors
diff --git a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts
index fee568d654..75d67c3784 100644
--- a/packages/nuxt/src/core/plugins/resolve-deep-imports.ts
+++ b/packages/nuxt/src/core/plugins/resolve-deep-imports.ts
@@ -16,7 +16,7 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
conditions = config.mode === 'test' ? [...config.resolve.conditions, 'import', 'require'] : config.resolve.conditions
},
async resolveId (id, importer) {
- if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:')) || exclude.some(e => id.startsWith(e))) {
+ if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:') && !importer.startsWith('\0virtual:')) || exclude.some(e => id.startsWith(e))) {
return
}
diff --git a/packages/nuxt/src/core/plugins/virtual.ts b/packages/nuxt/src/core/plugins/virtual.ts
new file mode 100644
index 0000000000..a855985031
--- /dev/null
+++ b/packages/nuxt/src/core/plugins/virtual.ts
@@ -0,0 +1,68 @@
+import { resolveAlias } from '@nuxt/kit'
+import type { Nuxt } from '@nuxt/schema'
+import { dirname, isAbsolute, resolve } from 'pathe'
+import { createUnplugin } from 'unplugin'
+
+const PREFIX = '\0virtual:nuxt:'
+
+interface VirtualFSPluginOptions {
+ mode: 'client' | 'server'
+ alias?: Record
+}
+
+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 }
+
+ const resolveWithExt = (id: string) => {
+ for (const suffix of ['', '.' + options.mode]) {
+ for (const ext of extensions) {
+ const rId = id + suffix + ext
+ if (rId in nuxt.vfs) {
+ return rId
+ }
+ }
+ }
+ }
+
+ return {
+ name: 'nuxt:virtual',
+ resolveId (id, importer) {
+ id = resolveAlias(id, alias)
+
+ if (process.platform === 'win32' && isAbsolute(id)) {
+ // Add back C: prefix on Windows
+ id = resolve(id)
+ }
+
+ const resolvedId = resolveWithExt(id)
+ if (resolvedId) {
+ return PREFIX + resolvedId
+ }
+
+ if (importer && RELATIVE_ID_RE.test(id)) {
+ const path = resolve(dirname(withoutPrefix(importer)), id)
+ const resolved = resolveWithExt(path)
+ if (resolved) {
+ return PREFIX + resolved
+ }
+ }
+ },
+
+ loadInclude (id) {
+ return id.startsWith(PREFIX) && withoutPrefix(id) in nuxt.vfs
+ },
+
+ load (id) {
+ return {
+ code: nuxt.vfs[withoutPrefix(id)] || '',
+ map: null,
+ }
+ },
+ }
+})
+
+function withoutPrefix (id: string) {
+ return id.startsWith(PREFIX) ? id.slice(PREFIX.length) : id
+}
diff --git a/packages/nuxt/src/core/schema.ts b/packages/nuxt/src/core/schema.ts
index f93030f9d0..a2bf41d7dd 100644
--- a/packages/nuxt/src/core/schema.ts
+++ b/packages/nuxt/src/core/schema.ts
@@ -23,7 +23,6 @@ export default defineNuxtModule({
// Initialize untyped/jiti loader
const _resolveSchema = createJiti(fileURLToPath(import.meta.url), {
- interopDefault: true,
cache: false,
transformOptions: {
babel: {
@@ -97,7 +96,7 @@ export default defineNuxtModule({
let loadedConfig: SchemaDefinition
try {
// TODO: fix type for second argument of `import`
- loadedConfig = await _resolveSchema.import(filePath, {}) as SchemaDefinition
+ loadedConfig = await _resolveSchema.import(filePath, { default: true }) as SchemaDefinition
} catch (err) {
logger.warn(
'Unable to load schema from',
diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts
index 32e2c49b24..a21352deba 100644
--- a/packages/nuxt/src/core/templates.ts
+++ b/packages/nuxt/src/core/templates.ts
@@ -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,8 +58,9 @@ 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',
+ filename: 'plugins.client.mjs',
async getContents (ctx) {
const clientPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server'))
checkForCircularDependencies(clientPlugins)
@@ -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))
}
@@ -78,7 +80,7 @@ export const clientPluginTemplate: NuxtTemplate = {
}
export const serverPluginTemplate: NuxtTemplate = {
- filename: 'plugins/server.mjs',
+ filename: 'plugins.server.mjs',
async getContents (ctx) {
const serverPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'client'))
checkForCircularDependencies(serverPlugins)
@@ -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 = /\.(?[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(/\.(?[cm])?jsx?$/, '.d.$ts')
+ const correspondingDeclaration = pluginPath.replace(JS_LETTER_RE, '.d.$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'
diff --git a/packages/nuxt/src/core/utils/index.ts b/packages/nuxt/src/core/utils/index.ts
index 2653f9a993..00974ac965 100644
--- a/packages/nuxt/src/core/utils/index.ts
+++ b/packages/nuxt/src/core/utils/index.ts
@@ -14,3 +14,7 @@ export function uniqueBy (arr: T[], key: K) {
}
return res
}
+
+export const QUOTE_RE = /["']/g
+export const EXTENSION_RE = /\b\.\w+$/g
+export const SX_RE = /\.[tj]sx$/
diff --git a/packages/nuxt/src/core/utils/names.ts b/packages/nuxt/src/core/utils/names.ts
index df6732c96f..88535cab3d 100644
--- a/packages/nuxt/src/core/utils/names.ts
+++ b/packages/nuxt/src/core/utils/names.ts
@@ -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) {
diff --git a/packages/nuxt/src/head/module.ts b/packages/nuxt/src/head/module.ts
index 520091428b..d8b4d74a01 100644
--- a/packages/nuxt/src/head/module.ts
+++ b/packages/nuxt/src/head/module.ts
@@ -76,8 +76,8 @@ export default import.meta.server ? [CapoPlugin({ track: true })] : [];`
// template is only exposed in nuxt context, expose in nitro context as well
nuxt.hooks.hook('nitro:config', (config) => {
- config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins']
- config.virtual!['#internal/unhead.config.mjs'] = () => nuxt.vfs['#build/unhead.config']
+ config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins.mjs']
+ config.virtual!['#internal/unhead.config.mjs'] = () => nuxt.vfs['#build/unhead.config.mjs']
})
// Add library-specific plugin
diff --git a/packages/nuxt/src/imports/presets.ts b/packages/nuxt/src/imports/presets.ts
index 1016f87d1f..ab0e7c7d32 100644
--- a/packages/nuxt/src/imports/presets.ts
+++ b/packages/nuxt/src/imports/presets.ts
@@ -66,7 +66,7 @@ const granularAppPresets: InlinePreset[] = [
from: '#app/composables/cookie',
},
{
- imports: ['onPrehydrate', 'prerenderRoutes', 'useRequestHeader', 'useRequestHeaders', 'useRequestEvent', 'useRequestFetch', 'setResponseStatus'],
+ imports: ['onPrehydrate', 'prerenderRoutes', 'useRequestHeader', 'useRequestHeaders', 'useResponseHeader', 'useRequestEvent', 'useRequestFetch', 'setResponseStatus'],
from: '#app/composables/ssr',
},
{
diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts
index 3cb562dc88..e3eba25859 100644
--- a/packages/nuxt/src/pages/module.ts
+++ b/packages/nuxt/src/pages/module.ts
@@ -52,7 +52,7 @@ export default defineNuxtModule({
}
// Add default options at beginning
- context.files.unshift({ path: resolve(runtimeDir, 'router.options'), optional: true })
+ context.files.unshift({ path: await findPath(resolve(runtimeDir, 'router.options')) || resolve(runtimeDir, 'router.options'), optional: true })
await nuxt.callHook('pages:routerOptions', context)
return context.files
@@ -170,10 +170,15 @@ export default defineNuxtModule({
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages
}
+ const addedPagePaths = new Set()
function addPage (parent: EditableTreeNode, page: NuxtPage) {
+ // Avoid duplicate keys in the generated RouteNamedMap type
+ const absolutePagePath = joinURL(parent.path, page.path)
+
// @ts-expect-error TODO: either fix types upstream or figure out another
// way to add a route without a file, which must be possible
- const route = parent.insert(page.path, page.file)
+ const route = addedPagePaths.has(absolutePagePath) ? parent : parent.insert(page.path, page.file)
+ addedPagePaths.add(absolutePagePath)
if (page.meta) {
route.addToMeta(page.meta)
}
@@ -414,8 +419,18 @@ export default defineNuxtModule({
})
}
+ const componentStubPath = await resolvePath(resolve(runtimeDir, 'component-stub'))
+ if (nuxt.options.test && nuxt.options.dev) {
+ // add component testing route so 404 won't be triggered
+ nuxt.hook('pages:extend', (routes) => {
+ routes.push({
+ _sync: true,
+ path: '/__nuxt_component_test__/:pathMatch(.*)',
+ file: componentStubPath,
+ })
+ })
+ }
if (nuxt.options.experimental.appManifest) {
- const componentStubPath = await resolvePath(resolve(runtimeDir, 'component-stub'))
// Add all redirect paths as valid routes to router; we will handle these in a client-side middleware
// when the app manifest is enabled.
nuxt.hook('pages:extend', (routes) => {
@@ -477,12 +492,19 @@ export default defineNuxtModule({
}
})
+ const serverComponentRuntime = await findPath(join(distDir, 'components/runtime/server-component')) ?? join(distDir, 'components/runtime/server-component')
+ const clientComponentRuntime = await findPath(join(distDir, 'components/runtime/client-component')) ?? join(distDir, 'components/runtime/client-component')
+
// Add routes template
addTemplate({
filename: 'routes.mjs',
getContents ({ app }) {
if (!app.pages) { return 'export default []' }
- const { routes, imports } = normalizeRoutes(app.pages, new Set(), nuxt.options.experimental.scanPageMeta)
+ const { routes, imports } = normalizeRoutes(app.pages, new Set(), {
+ serverComponentRuntime,
+ clientComponentRuntime,
+ overrideMeta: !!nuxt.options.experimental.scanPageMeta,
+ })
return [...imports, `export default ${routes}`].join('\n')
},
})
diff --git a/packages/nuxt/src/pages/plugins/page-meta.ts b/packages/nuxt/src/pages/plugins/page-meta.ts
index f6645a5653..76ae4dc59f 100644
--- a/packages/nuxt/src/pages/plugins/page-meta.ts
+++ b/packages/nuxt/src/pages/plugins/page-meta.ts
@@ -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 = /¯o=true/
function rewriteQuery (id: string) {
- return id.replace(/\?.+$/, r => '?macro=true&' + r.replace(/^\?/, '').replace(/¯o=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]
}
diff --git a/packages/nuxt/src/pages/runtime/composables.ts b/packages/nuxt/src/pages/runtime/composables.ts
index eb60aada99..b752a101d4 100644
--- a/packages/nuxt/src/pages/runtime/composables.ts
+++ b/packages/nuxt/src/pages/runtime/composables.ts
@@ -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)
}
diff --git a/packages/nuxt/src/pages/runtime/plugins/router.ts b/packages/nuxt/src/pages/runtime/plugins/router.ts
index 7f2e5faca6..bb41746480 100644
--- a/packages/nuxt/src/pages/runtime/plugins/router.ts
+++ b/packages/nuxt/src/pages/runtime/plugins/router.ts
@@ -148,16 +148,8 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
if (import.meta.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) {
return
}
- if (to.matched.length === 0) {
- await nuxtApp.runWithContext(() => showError(createError({
- statusCode: 404,
- fatal: false,
- statusMessage: `Page not found: ${to.fullPath}`,
- data: {
- path: to.fullPath,
- },
- })))
- } else if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
+
+ if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
}
})
@@ -252,6 +244,19 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
await nuxtApp.callHook('page:loading:end')
})
+ router.afterEach(async (to, _from) => {
+ if (to.matched.length === 0) {
+ await nuxtApp.runWithContext(() => showError(createError({
+ statusCode: 404,
+ fatal: false,
+ statusMessage: `Page not found: ${to.fullPath}`,
+ data: {
+ path: to.fullPath,
+ },
+ })))
+ }
+ })
+
nuxtApp.hooks.hookOnce('app:created', async () => {
try {
// #4920, #4982
diff --git a/packages/nuxt/src/pages/runtime/utils.ts b/packages/nuxt/src/pages/runtime/utils.ts
index 7dc4851ead..d9b8d90a7d 100644
--- a/packages/nuxt/src/pages/runtime/utils.ts
+++ b/packages/nuxt/src/pages/runtime/utils.ts
@@ -5,11 +5,14 @@ type InstanceOf = T extends new (...args: any[]) => infer R ? R : never
type RouterViewSlot = Exclude['$slots']['default'], undefined>
export type RouterViewSlotProps = Parameters[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)) => {
diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts
index e1e14517e9..d3d62b4ee4 100644
--- a/packages/nuxt/src/pages/utils.ts
+++ b/packages/nuxt/src/pages/utils.ts
@@ -15,7 +15,6 @@ import type { NuxtPage } from 'nuxt/schema'
import { getLoader, uniqueBy } from '../core/utils'
import { toArray } from '../utils'
-import { distDir } from '../dirs'
enum SegmentParserState {
initial,
@@ -65,18 +64,25 @@ export async function resolvePagesRoutes (): Promise {
})
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
}
@@ -84,6 +90,7 @@ type GenerateRoutesFromFilesOptions = {
shouldUseServerComponents?: boolean
}
+const INDEX_PAGE_RE = /\/index$/
export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] {
const routes: NuxtPage[] = []
@@ -129,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) {
@@ -184,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 = {}
@@ -266,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
@@ -301,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 (
@@ -313,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, '\\:'))
)
}, '/')
}
@@ -433,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()) {
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)
@@ -476,7 +485,12 @@ function serializeRouteValue (value: any, skipSerialisation = false) {
type NormalizedRoute = Partial, string>> & { component?: string }
type NormalizedRouteKeys = (keyof NormalizedRoute)[]
-export function normalizeRoutes (routes: NuxtPage[], metaImports: Set = new Set(), overrideMeta = false): { imports: Set, routes: string } {
+interface NormalizeRoutesOptions {
+ overrideMeta?: boolean
+ serverComponentRuntime: string
+ clientComponentRuntime: string
+}
+export function normalizeRoutes (routes: NuxtPage[], metaImports: Set = new Set(), options: NormalizeRoutesOptions): { imports: Set, routes: string } {
return {
imports: metaImports,
routes: genArrayFromRaw(routes.map((page) => {
@@ -506,7 +520,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set =
}
if (page.children?.length) {
- route.children = normalizeRoutes(page.children, metaImports, overrideMeta).routes
+ route.children = normalizeRoutes(page.children, metaImports, options).routes
}
// Without a file, we can't use `definePageMeta` to extract route-level meta from the file
@@ -528,6 +542,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set =
const metaRoute: NormalizedRoute = {
name: `${metaImportName}?.name ?? ${route.name}`,
path: `${metaImportName}?.path ?? ${route.path}`,
+ props: `${metaImportName}?.props ?? false`,
meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`,
@@ -542,14 +557,14 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set =
metaImports.add(`
let _createIslandPage
async function createIslandPage (name) {
- _createIslandPage ||= await import(${JSON.stringify(resolve(distDir, 'components/runtime/server-component'))}).then(r => r.createIslandPage)
+ _createIslandPage ||= await import(${JSON.stringify(options?.serverComponentRuntime)}).then(r => r.createIslandPage)
return _createIslandPage(name)
};`)
} else if (page.mode === 'client') {
metaImports.add(`
let _createClientPage
async function createClientPage(loader) {
- _createClientPage ||= await import(${JSON.stringify(resolve(distDir, 'components/runtime/client-component'))}).then(r => r.createClientPage)
+ _createClientPage ||= await import(${JSON.stringify(options?.clientComponentRuntime)}).then(r => r.createClientPage)
return _createClientPage(loader);
}`)
}
@@ -562,7 +577,7 @@ async function createClientPage(loader) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
}
- if (overrideMeta) {
+ if (options?.overrideMeta) {
// skip and retain fallback if marked dynamic
// set to extracted value or fallback if none extracted
for (const key of ['name', 'path'] satisfies NormalizedRouteKeys) {
@@ -571,7 +586,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) {
@@ -596,6 +611,7 @@ async function createClientPage(loader) {
}
}
+const PATH_TO_NITRO_GLOB_RE = /\/[^:/]*:\w.*$/
export function pathToNitroGlob (path: string) {
if (!path) {
return null
@@ -605,7 +621,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[] {
diff --git a/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap b/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap
index 8bc2211aff..7dd113b1af 100644
--- a/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap
+++ b/packages/nuxt/test/__snapshots__/pages-override-meta-disabled.test.ts.snap
@@ -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 ?? false",
"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",
},
],
diff --git a/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap b/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap
index 26a4cc97a1..d02977f9b3 100644
--- a/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap
+++ b/packages/nuxt/test/__snapshots__/pages-override-meta-enabled.test.ts.snap
@@ -24,6 +24,13 @@
"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"",
+ },
+ ],
"should allow pages with `:` in their path": [
{
"component": "() => import("pages/test:name.vue")",
diff --git a/packages/nuxt/test/components-transform.test.ts b/packages/nuxt/test/components-transform.test.ts
index 167229a4dc..0792f7902e 100644
--- a/packages/nuxt/test/components-transform.test.ts
+++ b/packages/nuxt/test/components-transform.test.ts
@@ -92,7 +92,11 @@ function createTransformer (components: Component[], mode: 'client' | 'server' |
},
},
} as Nuxt
- const plugin = TransformPlugin(stubNuxt, () => components, mode).vite()
+ const plugin = TransformPlugin(stubNuxt, {
+ mode,
+ getComponents: () => components,
+ serverComponentRuntime: '/nuxt/src/components/runtime/server-component',
+ }).vite()
return async (code: string, id: string) => {
const result = await (plugin as any).transform!(code, id)
diff --git a/packages/nuxt/test/page-metadata.test.ts b/packages/nuxt/test/page-metadata.test.ts
index 9e57ab407f..6206e07c08 100644
--- a/packages/nuxt/test/page-metadata.test.ts
+++ b/packages/nuxt/test/page-metadata.test.ts
@@ -168,7 +168,11 @@ describe('normalizeRoutes', () => {
page.meta.layout = 'test'
page.meta.foo = 'bar'
- const { routes, imports } = normalizeRoutes([page], new Set(), true)
+ const { routes, imports } = normalizeRoutes([page], new Set(), {
+ clientComponentRuntime: '',
+ serverComponentRuntime: '',
+ overrideMeta: true,
+ })
expect({ routes, imports }).toMatchInlineSnapshot(`
{
"imports": Set {
@@ -193,7 +197,11 @@ describe('normalizeRoutes', () => {
page.meta.layout = 'test'
page.meta.foo = 'bar'
- const { routes, imports } = normalizeRoutes([page], new Set())
+ const { routes, imports } = normalizeRoutes([page], new Set(), {
+ clientComponentRuntime: '',
+ serverComponentRuntime: '',
+ overrideMeta: false,
+ })
expect({ routes, imports }).toMatchInlineSnapshot(`
{
"imports": Set {
@@ -203,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,
diff --git a/packages/nuxt/test/pages.test.ts b/packages/nuxt/test/pages.test.ts
index f2b2820e81..3ff396e5cb 100644
--- a/packages/nuxt/test/pages.test.ts
+++ b/packages/nuxt/test/pages.test.ts
@@ -601,6 +601,30 @@ describe('pages:generateRoutesFromFiles', () => {
},
],
},
+ {
+ description: 'route.meta props generate by file',
+ files: [
+ {
+ path: `${pagesDir}/page-with-props.vue`,
+ template: `
+
+ `,
+ },
+ ],
+ output: [
+ {
+ name: 'page-with-props',
+ path: '/page-with-props',
+ file: `${pagesDir}/page-with-props.vue`,
+ children: [],
+ props: true,
+ },
+ ],
+ },
{
description: 'should handle route groups',
files: [
@@ -667,8 +691,18 @@ describe('pages:generateRoutesFromFiles', () => {
if (result) {
expect(result).toEqual(test.output)
- normalizedResults[test.description] = normalizeRoutes(result, new Set()).routes
- normalizedOverrideMetaResults[test.description] = normalizeRoutes(result, new Set(), true).routes
+
+ normalizedResults[test.description] = normalizeRoutes(result, new Set(), {
+ clientComponentRuntime: '',
+ serverComponentRuntime: '',
+ overrideMeta: false,
+ }).routes
+
+ normalizedOverrideMetaResults[test.description] = normalizeRoutes(result, new Set(), {
+ clientComponentRuntime: '',
+ serverComponentRuntime: '',
+ overrideMeta: true,
+ }).routes
}
})
}
diff --git a/packages/rspack/build.config.ts b/packages/rspack/build.config.ts
new file mode 100644
index 0000000000..c11f11a10c
--- /dev/null
+++ b/packages/rspack/build.config.ts
@@ -0,0 +1,18 @@
+import { defineBuildConfig } from 'unbuild'
+import config from '../webpack/build.config'
+
+export default defineBuildConfig({
+ ...config[0],
+ externals: [
+ '@rspack/core',
+ '#builder',
+ '@nuxt/schema',
+ ],
+ entries: [
+ {
+ input: '../webpack/src/index',
+ name: 'index',
+ declaration: true,
+ },
+ ],
+})
diff --git a/packages/rspack/builder.mjs b/packages/rspack/builder.mjs
new file mode 100644
index 0000000000..14b2475188
--- /dev/null
+++ b/packages/rspack/builder.mjs
@@ -0,0 +1,5 @@
+import webpack from '@rspack/core'
+
+export const builder = 'rspack'
+export { webpack }
+export const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin
diff --git a/packages/rspack/package.json b/packages/rspack/package.json
new file mode 100644
index 0000000000..acf9629c7d
--- /dev/null
+++ b/packages/rspack/package.json
@@ -0,0 +1,94 @@
+{
+ "name": "@nuxt/rspack-builder",
+ "version": "3.12.2",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/nuxt/nuxt.git",
+ "directory": "packages/rspack"
+ },
+ "description": "rspack bundler for Nuxt",
+ "homepage": "https://nuxt.com",
+ "license": "MIT",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "imports": {
+ "#builder": "./builder.mjs"
+ },
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs"
+ },
+ "./dist/*": "./dist/*"
+ },
+ "files": [
+ "dist",
+ "builder.mjs"
+ ],
+ "scripts": {
+ "prepack": "unbuild"
+ },
+ "dependencies": {
+ "@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
+ "@nuxt/kit": "workspace:*",
+ "@rspack/core": "^1.0.14",
+ "autoprefixer": "^10.4.20",
+ "css-loader": "^7.1.2",
+ "css-minimizer-webpack-plugin": "^7.0.0",
+ "cssnano": "^7.0.6",
+ "defu": "^6.1.4",
+ "esbuild-loader": "^4.2.2",
+ "escape-string-regexp": "^5.0.0",
+ "estree-walker": "^3.0.3",
+ "file-loader": "^6.2.0",
+ "fork-ts-checker-webpack-plugin": "^9.0.2",
+ "globby": "^14.0.2",
+ "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
+ "hash-sum": "^2.0.0",
+ "jiti": "^2.3.3",
+ "knitwork": "^1.1.0",
+ "lodash-es": "4.17.21",
+ "magic-string": "^0.30.12",
+ "memfs": "^4.14.0",
+ "mlly": "^1.7.2",
+ "ohash": "^1.1.4",
+ "pathe": "^1.1.2",
+ "pify": "^6.1.0",
+ "postcss": "^8.4.47",
+ "postcss-import": "^16.1.0",
+ "postcss-import-resolver": "^2.0.0",
+ "postcss-loader": "^8.1.1",
+ "postcss-url": "^10.1.3",
+ "pug-plain-loader": "^1.1.0",
+ "std-env": "^3.7.0",
+ "time-fix-plugin": "^2.0.7",
+ "ufo": "^1.5.4",
+ "unenv": "^1.10.0",
+ "unplugin": "^1.14.1",
+ "url-loader": "^4.1.1",
+ "vue-bundle-renderer": "^2.1.1",
+ "vue-loader": "^17.4.2",
+ "webpack-bundle-analyzer": "^4.10.2",
+ "webpack-dev-middleware": "^7.4.2",
+ "webpack-hot-middleware": "^2.26.1",
+ "webpack-virtual-modules": "^0.6.2",
+ "webpackbar": "^6.0.1"
+ },
+ "devDependencies": {
+ "@nuxt/schema": "workspace:*",
+ "@types/hash-sum": "1.0.2",
+ "@types/lodash-es": "4.17.12",
+ "@types/pify": "5.0.4",
+ "@types/webpack-bundle-analyzer": "4.7.0",
+ "@types/webpack-hot-middleware": "2.25.9",
+ "rollup": "4.24.0",
+ "unbuild": "3.0.0-rc.11",
+ "vue": "3.5.12"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.4"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+}
diff --git a/packages/schema/package.json b/packages/schema/package.json
index 0733e75b4b..44a0993b5b 100644
--- a/packages/schema/package.json
+++ b/packages/schema/package.json
@@ -39,23 +39,22 @@
"@types/file-loader": "5.0.4",
"@types/pug": "2.0.10",
"@types/sass-loader": "8.0.9",
- "@unhead/schema": "1.11.6",
+ "@unhead/schema": "1.11.10",
"@vitejs/plugin-vue": "5.1.4",
"@vitejs/plugin-vue-jsx": "4.0.1",
- "@vue/compiler-core": "3.5.10",
- "@vue/compiler-sfc": "3.5.10",
+ "@vue/compiler-core": "3.5.12",
+ "@vue/compiler-sfc": "3.5.12",
"@vue/language-core": "2.1.6",
- "c12": "2.0.0-beta.3",
"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",
- "ofetch": "1.4.0",
- "unbuild": "3.0.0-rc.8",
+ "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.10",
+ "vite": "5.4.10",
+ "vue": "3.5.12",
"vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2",
"vue-router": "4.4.5",
@@ -63,18 +62,19 @@
"webpack-dev-middleware": "7.4.2"
},
"dependencies": {
+ "c12": "^2.0.1",
"compatx": "^0.1.8",
"consola": "^3.2.3",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"pathe": "^1.1.2",
- "pkg-types": "^1.2.0",
+ "pkg-types": "^1.2.1",
"scule": "^1.3.0",
"std-env": "^3.7.0",
"ufo": "^1.5.4",
"uncrypto": "^0.1.3",
"unimport": "^3.13.1",
- "untyped": "^1.5.0"
+ "untyped": "^1.5.1"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"
diff --git a/packages/schema/src/config/build.ts b/packages/schema/src/config/build.ts
index 6527a2aaa3..fbacf2dd79 100644
--- a/packages/schema/src/config/build.ts
+++ b/packages/schema/src/config/build.ts
@@ -7,14 +7,15 @@ import { consola } from 'consola'
export default defineUntypedSchema({
/**
* The builder to use for bundling the Vue part of your application.
- * @type {'vite' | 'webpack' | { bundle: (nuxt: typeof import('../src/types/nuxt').Nuxt) => Promise }}
+ * @type {'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: typeof import('../src/types/nuxt').Nuxt) => Promise }}
*/
builder: {
- $resolve: async (val: 'vite' | 'webpack' | { bundle: (nuxt: unknown) => Promise } | undefined = 'vite', get) => {
+ $resolve: async (val: 'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: unknown) => Promise } | undefined = 'vite', get) => {
if (typeof val === 'object') {
return val
}
const map: Record = {
+ rspack: '@nuxt/rspack-builder',
vite: '@nuxt/vite-builder',
webpack: '@nuxt/webpack-builder',
}
diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts
index 61fcd1f2a5..d77d4e8f59 100644
--- a/packages/schema/src/config/common.ts
+++ b/packages/schema/src/config/common.ts
@@ -424,7 +424,7 @@ export default defineUntypedSchema({
*/
alias: {
$resolve: async (val: Record, get): Promise> => {
- const [srcDir, rootDir, assetsDir, publicDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public')]) as [string, string, 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]
return {
'~': srcDir,
'@': srcDir,
@@ -432,6 +432,8 @@ export default defineUntypedSchema({
'@@': rootDir,
[basename(assetsDir)]: resolve(srcDir, assetsDir),
[basename(publicDir)]: resolve(srcDir, publicDir),
+ '#build': buildDir,
+ '#internal/nuxt/paths': resolve(buildDir, 'paths.mjs'),
...val,
}
},
diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts
index 711a20b776..5743c313f0 100644
--- a/packages/schema/src/config/experimental.ts
+++ b/packages/schema/src/config/experimental.ts
@@ -297,8 +297,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).compatibilityVersion === 4 ? 'after-resolve' : true)
+ },
+ },
/**
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant
diff --git a/packages/schema/src/config/typescript.ts b/packages/schema/src/config/typescript.ts
index 402cd007fd..742f5a20f9 100644
--- a/packages/schema/src/config/typescript.ts
+++ b/packages/schema/src/config/typescript.ts
@@ -20,7 +20,7 @@ export default defineUntypedSchema({
* builder environment types (with `false`) to handle this fully yourself, or opt for a 'shared' option.
*
* The 'shared' option is advised for module authors, who will want to support multiple possible builders.
- * @type {'vite' | 'webpack' | 'shared' | false | undefined}
+ * @type {'vite' | 'webpack' | 'rspack' | 'shared' | false | undefined}
*/
builder: {
$resolve: val => val ?? null,
diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts
index 0eb5c1588f..a682bb2f61 100644
--- a/packages/schema/src/index.ts
+++ b/packages/schema/src/index.ts
@@ -6,7 +6,7 @@ export type { GenerateAppOptions, HookResult, ImportPresetWithDeprecation, NuxtA
export type { ImportsOptions } from './types/imports'
export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations } from './types/head'
export type { ModuleDefinition, ModuleMeta, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, NuxtModule, ResolvedModuleOptions } from './types/module'
-export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate } from './types/nuxt'
+export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, NuxtServerTemplate, ResolvedNuxtTemplate } from './types/nuxt'
export type { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router'
// Schema
diff --git a/packages/schema/src/types/compatibility.ts b/packages/schema/src/types/compatibility.ts
index 562b187cb0..209b373072 100644
--- a/packages/schema/src/types/compatibility.ts
+++ b/packages/schema/src/types/compatibility.ts
@@ -26,7 +26,7 @@ export interface NuxtCompatibility {
* })
* ```
*/
- builder?: Partial>
+ builder?: Partial>
}
export interface NuxtCompatibilityIssue {
diff --git a/packages/schema/src/types/config.ts b/packages/schema/src/types/config.ts
index 3de59b8493..2d9c1222ef 100644
--- a/packages/schema/src/types/config.ts
+++ b/packages/schema/src/types/config.ts
@@ -5,6 +5,7 @@ import type { Options as VueJsxPluginOptions } from '@vitejs/plugin-vue-jsx'
import type { SchemaDefinition } from 'untyped'
import type { NitroRuntimeConfig, NitroRuntimeConfigApp } from 'nitro/types'
import type { SnakeCase } from 'scule'
+import type { ResolvedConfig } from 'c12'
import type { ConfigSchema } from '../../schema/config'
import type { Nuxt } from './nuxt'
import type { AppHeadMetaObject } from './head'
@@ -62,16 +63,13 @@ export interface NuxtConfig extends DeepPartial from c12
-interface ConfigLayer {
- config: T
+export type NuxtConfigLayer = ResolvedConfig & {
cwd: string
configFile: string
}
-export type NuxtConfigLayer = ConfigLayer
export interface NuxtBuilder {
bundle: (nuxt: Nuxt) => Promise
@@ -81,7 +79,7 @@ export interface NuxtBuilder {
export interface NuxtOptions extends Omit {
vue: Omit & { config?: Partial> }
sourcemap: Required>
- builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | NuxtBuilder
+ builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | '@nuxt/rspack-builder' | NuxtBuilder
postcss: Omit & { order: Exclude }
webpack: ConfigSchema['webpack'] & {
$client: ConfigSchema['webpack']
diff --git a/packages/schema/src/types/hooks.ts b/packages/schema/src/types/hooks.ts
index 780b8b0614..3b88d3ec82 100644
--- a/packages/schema/src/types/hooks.ts
+++ b/packages/schema/src/types/hooks.ts
@@ -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
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.
@@ -408,6 +416,55 @@ export interface NuxtHooks {
* @returns void
*/
'webpack:progress': (statesArray: any[]) => void
+
+ // rspack
+ /**
+ * Called before configuring the webpack compiler.
+ * @param webpackConfigs Configs objects to be pushed to the compiler
+ * @returns Promise
+ */
+ 'rspack:config': (webpackConfigs: Configuration[]) => HookResult
+ /**
+ * Allows to read the resolved webpack config
+ * @param webpackConfigs Configs objects to be pushed to the compiler
+ * @returns Promise
+ */
+ 'rspack:configResolved': (webpackConfigs: Readonly[]) => HookResult
+ /**
+ * Called right before compilation.
+ * @param options The options to be added
+ * @returns Promise
+ */
+ 'rspack:compile': (options: { name: string, compiler: Compiler }) => HookResult
+ /**
+ * Called after resources are loaded.
+ * @param options The compiler options
+ * @returns Promise
+ */
+ 'rspack:compiled': (options: { name: string, compiler: Compiler, stats: Stats }) => HookResult
+
+ /**
+ * Called on `change` on WebpackBar.
+ * @param shortPath the short path
+ * @returns void
+ */
+ 'rspack:change': (shortPath: string) => void
+ /**
+ * Called on `done` if has errors on WebpackBar.
+ * @returns void
+ */
+ 'rspack:error': () => void
+ /**
+ * Called on `allDone` on WebpackBar.
+ * @returns void
+ */
+ 'rspack:done': () => void
+ /**
+ * Called on `progress` on WebpackBar.
+ * @param statesArray The array containing the states on progress
+ * @returns void
+ */
+ 'rspack:progress': (statesArray: any[]) => void
}
export type NuxtHookName = keyof NuxtHooks
diff --git a/packages/schema/src/types/nuxt.ts b/packages/schema/src/types/nuxt.ts
index 41130835a6..ab9d1a2eeb 100644
--- a/packages/schema/src/types/nuxt.ts
+++ b/packages/schema/src/types/nuxt.ts
@@ -42,6 +42,12 @@ export interface NuxtTemplate {
write?: boolean
}
+export interface NuxtServerTemplate {
+ /** The target filename once the template is copied into the Nuxt buildDir */
+ filename: string
+ getContents: () => string | Promise
+}
+
export interface ResolvedNuxtTemplate extends NuxtTemplate {
filename: string
dst: string
diff --git a/packages/ui-templates/package.json b/packages/ui-templates/package.json
index de258a3e68..539fff3794 100644
--- a/packages/ui-templates/package.json
+++ b/packages/ui-templates/package.json
@@ -18,18 +18,18 @@
"test": "pnpm lint && pnpm build"
},
"devDependencies": {
- "@unocss/reset": "0.62.4",
- "critters": "0.0.24",
- "html-validate": "8.24.0",
+ "@unocss/reset": "0.63.6",
+ "critters": "0.0.25",
+ "html-validate": "8.24.2",
"htmlnano": "2.1.1",
- "jiti": "2.0.0",
+ "jiti": "2.3.3",
"knitwork": "1.1.0",
"pathe": "1.1.2",
"prettier": "3.3.3",
"scule": "1.3.0",
- "tinyexec": "0.3.0",
- "tinyglobby": "0.2.6",
- "unocss": "0.62.4",
- "vite": "5.4.8"
+ "tinyexec": "0.3.1",
+ "tinyglobby": "0.2.9",
+ "unocss": "0.63.6",
+ "vite": "5.4.10"
}
}
diff --git a/packages/ui-templates/test/templates.spec.ts b/packages/ui-templates/test/templates.spec.ts
index 33aa989914..5390af3d52 100644
--- a/packages/ui-templates/test/templates.spec.ts
+++ b/packages/ui-templates/test/templates.spec.ts
@@ -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))
diff --git a/packages/vite/package.json b/packages/vite/package.json
index bbc869337c..34fa01462d 100644
--- a/packages/vite/package.json
+++ b/packages/vite/package.json
@@ -27,9 +27,9 @@
"@nuxt/schema": "workspace:*",
"@types/clear": "0.1.4",
"@types/estree": "1.0.6",
- "rollup": "4.22.5",
- "unbuild": "3.0.0-rc.8",
- "vue": "3.5.10"
+ "rollup": "4.24.0",
+ "unbuild": "3.0.0-rc.11",
+ "vue": "3.5.12"
},
"dependencies": {
"@nuxt/kit": "workspace:*",
@@ -47,14 +47,14 @@
"externality": "^1.0.2",
"get-port-please": "^3.1.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
- "jiti": "^2.0.0",
+ "jiti": "^2.3.3",
"knitwork": "^1.1.0",
- "magic-string": "^0.30.11",
- "mlly": "^1.7.1",
+ "magic-string": "^0.30.12",
+ "mlly": "^1.7.2",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"perfect-debounce": "^1.0.0",
- "pkg-types": "^1.2.0",
+ "pkg-types": "^1.2.1",
"postcss": "^8.4.47",
"rollup-plugin-visualizer": "^5.12.0",
"std-env": "^3.7.0",
@@ -62,8 +62,8 @@
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^1.14.1",
- "vite": "^5.4.8",
- "vite-node": "^2.1.1",
+ "vite": "^5.4.10",
+ "vite-node": "^2.1.3",
"vite-plugin-checker": "^0.8.0",
"vue-bundle-renderer": "^2.1.1"
},
diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts
index 6d8ea7e81d..f26303b2ee 100644
--- a/packages/vite/src/client.ts
+++ b/packages/vite/src/client.ts
@@ -109,9 +109,7 @@ export async function buildClient (ctx: ViteBuildContext) {
alias: {
...nodeCompat.alias,
...ctx.config.resolve?.alias,
- '#internal/nuxt/paths': resolve(ctx.nuxt.options.buildDir, 'paths.mjs'),
- '#build/plugins': resolve(ctx.nuxt.options.buildDir, 'plugins/client'),
- 'nitro/runtime': resolve(ctx.nuxt.options.buildDir, 'nitro.client.mjs'),
+ 'nitro/runtime': join(ctx.nuxt.options.buildDir, 'nitro.client.mjs'),
},
dedupe: [
'vue',
diff --git a/packages/vite/src/css.ts b/packages/vite/src/css.ts
index c3f6871422..9b059082ff 100644
--- a/packages/vite/src/css.ts
+++ b/packages/vite/src/css.ts
@@ -9,19 +9,15 @@ function sortPlugins ({ plugins, order }: NuxtOptions['postcss']): string[] {
}
export async function resolveCSSOptions (nuxt: Nuxt): Promise {
- const css: ViteConfig['css'] & { postcss: NonNullable['postcss'], string>> } = {
+ const css: ViteConfig['css'] & { postcss: NonNullable['postcss'], string>> & { plugins: Plugin[] } } = {
postcss: {
plugins: [],
},
}
- css.postcss.plugins = []
const postcssOptions = nuxt.options.postcss
- const jiti = createJiti(nuxt.options.rootDir, {
- interopDefault: true,
- alias: nuxt.options.alias,
- })
+ const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias })
for (const pluginName of sortPlugins(postcssOptions)) {
const pluginOptions = postcssOptions.plugins[pluginName]
@@ -29,7 +25,7 @@ export async function resolveCSSOptions (nuxt: Nuxt): Promise
let pluginFn: ((opts: Record) => Plugin) | undefined
for (const parentURL of nuxt.options.modulesDir) {
- pluginFn = await jiti.import(pluginName, { parentURL: parentURL.replace(/\/node_modules\/?$/, ''), try: true }) as (opts: Record) => Plugin
+ pluginFn = await jiti.import(pluginName, { parentURL: parentURL.replace(/\/node_modules\/?$/, ''), try: true, default: true }) as (opts: Record) => Plugin
if (typeof pluginFn === 'function') {
css.postcss.plugins.push(pluginFn(pluginOptions))
break
diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts
index 7e252f6ac0..14233469aa 100644
--- a/packages/vite/src/plugins/composable-keys.ts
+++ b/packages/vite/src/plugins/composable-keys.ts
@@ -20,6 +20,7 @@ interface ComposableKeysOptions {
const stringTypes: Array = ['Literal', 'TemplateLiteral']
const NUXT_LIB_RE = /node_modules\/(?:nuxt|nuxt3|nuxt-nightly)\//
const SUPPORTED_EXT_RE = /\.(?:m?[jt]sx?|vue)/
+const SCRIPT_RE = /(?<=
+
+
+
+
+
+
diff --git a/test/fixtures/basic-types/types.ts b/test/fixtures/basic-types/types.ts
index dbaf63e0f7..5b9f7b9883 100644
--- a/test/fixtures/basic-types/types.ts
+++ b/test/fixtures/basic-types/types.ts
@@ -162,6 +162,10 @@ describe('typed router integration', () => {
// @ts-expect-error this is an invalid param
router.push({ name: 'param-id', params: { bob: 23 } })
router.push({ name: 'param-id', params: { id: 4 } })
+ // @ts-expect-error this is an invalid route
+ router.push({ name: 'param' })
+ // @ts-expect-error this is an invalid route
+ router.push({ name: '/param' })
})
it('correctly reads custom names typed in `definePageMeta`', () => {
diff --git a/test/fixtures/basic/components/ServerOnlyComponent.server.vue b/test/fixtures/basic/components/ServerOnlyComponent.server.vue
index ff3b40a1ba..c4085eb133 100644
--- a/test/fixtures/basic/components/ServerOnlyComponent.server.vue
+++ b/test/fixtures/basic/components/ServerOnlyComponent.server.vue
@@ -1,5 +1,5 @@
diff --git a/test/fixtures/basic/middleware/redirect.global.ts b/test/fixtures/basic/middleware/redirect.global.ts
index 9ca90920ff..9638eb41cc 100644
--- a/test/fixtures/basic/middleware/redirect.global.ts
+++ b/test/fixtures/basic/middleware/redirect.global.ts
@@ -9,9 +9,9 @@ export default defineNuxtRouteMiddleware(async (to) => {
await new Promise(resolve => setTimeout(resolve, 100))
return navigateTo(to.path.slice('/redirect/'.length - 1))
}
- if (to.path === '/redirect-infinite') {
+ if (to.path === '/catchall/redirect-infinite') {
// the path will be the same in this new route and vue-router should send a 500 response
- return navigateTo('/redirect-infinite?test=true')
+ return navigateTo('/catchall/redirect-infinite?test=true')
}
if (to.path === '/navigate-to-external') {
return navigateTo('/', { external: true })
diff --git a/test/fixtures/basic/modules/page-extend/index.ts b/test/fixtures/basic/modules/page-extend/index.ts
index 60ef2c224c..a72c5a791a 100644
--- a/test/fixtures/basic/modules/page-extend/index.ts
+++ b/test/fixtures/basic/modules/page-extend/index.ts
@@ -13,13 +13,18 @@ export default defineNuxtModule({
name: 'page-extend',
path: '/page-extend',
file: resolver.resolve('../runtime/page.vue'),
- }, {
+ })
+ })
+
+ nuxt.hook('pages:resolved', (pages) => {
+ pages.push({
path: '/big-page-1',
file: resolver.resolve('./pages/big-page.vue'),
meta: {
layout: false,
},
- }, {
+ },
+ {
path: '/big-page-2',
file: resolver.resolve('./pages/big-page.vue'),
meta: {
diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts
index da0103e4b4..e4cf482bf0 100644
--- a/test/fixtures/basic/nuxt.config.ts
+++ b/test/fixtures/basic/nuxt.config.ts
@@ -12,85 +12,12 @@ declare module 'nitro/types' {
}
export default defineNuxtConfig({
- compatibilityDate: '2024-06-28',
- app: {
- pageTransition: true,
- layoutTransition: true,
- teleportId: 'nuxt-teleport',
- teleportTag: 'span',
- head: {
- charset: 'utf-8',
- link: [undefined],
- meta: [
- { name: 'viewport', content: 'width=1024, initial-scale=1' },
- { charset: 'utf-8' },
- { name: 'description', content: 'Nuxt Fixture' },
- ],
- },
- keepalive: {
- include: ['keepalive-in-config', 'not-keepalive-in-nuxtpage'],
- },
- },
- builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite',
appId: 'nuxt-app-basic',
- build: {
- transpile: [
- (ctx) => {
- if (typeof ctx.isDev !== 'boolean') { throw new TypeError('context not passed') }
- return false
- },
- ],
- },
- css: ['~/assets/global.css'],
- // this produces an order of `~` > `~/extends/bar` > `~/extends/node_modules/foo`
- theme: './extends/bar',
extends: [
'./extends/node_modules/foo',
],
- nitro: {
- publicAssets: [
- {
- dir: '../custom-public',
- baseURL: '/custom',
- },
- ],
- esbuild: {
- options: {
- // in order to test bigint serialization
- target: 'es2022',
- },
- },
- routeRules: {
- '/route-rules/spa': { ssr: false },
- '/head-spa': { ssr: false },
- '/route-rules/middleware': { appMiddleware: 'route-rules-middleware' },
- '/hydration/spa-redirection/**': { ssr: false },
- '/no-scripts': { experimentalNoScripts: true },
- '/prerender/**': { prerender: true },
- },
- prerender: {
- routes: [
- '/random/a',
- '/random/b',
- '/random/c',
- '/prefetch/server-components',
- ],
- },
- },
- optimization: {
- keyedComposables: [
- {
- name: 'useCustomKeyedComposable',
- source: '~/other-composables-folder/custom-keyed-composable',
- argumentLength: 1,
- },
- ],
- },
- runtimeConfig: {
- public: {
- needsFallback: undefined,
- },
- },
+ // this produces an order of `~` > `~/extends/bar` > `~/extends/node_modules/foo`
+ theme: './extends/bar',
modules: [
function (_options, nuxt) {
// ensure setting `runtimeConfig` also sets `nitro.runtimeConfig`
@@ -122,7 +49,7 @@ export default defineNuxtConfig({
if (id === 'virtual.css') { return 'virtual.css' }
},
load (id) {
- if (id === 'virtual.css') { return ':root { --virtual: red }' }
+ if (id.includes('virtual.css')) { return ':root { --virtual: red }' }
},
}))
addBuildPlugin(plugin)
@@ -148,7 +75,7 @@ export default defineNuxtConfig({
_layout: page.meta?.layout,
},
})
- nuxt.hook('pages:extend', (pages) => {
+ nuxt.hook('pages:resolved', (pages) => {
const newPages = []
for (const page of pages) {
if (routesToDuplicate.includes(page.path)) {
@@ -161,7 +88,7 @@ export default defineNuxtConfig({
},
function (_options, nuxt) {
// to check that page metadata is preserved
- nuxt.hook('pages:extend', (pages) => {
+ nuxt.hook('pages:resolved', (pages) => {
const customName = pages.find(page => page.name === 'some-custom-name')
if (!customName) { throw new Error('Page with custom name not found') }
if (customName.path !== '/some-custom-path') { throw new Error('Page path not extracted') }
@@ -173,6 +100,111 @@ export default defineNuxtConfig({
// To test falsy module values
undefined,
],
+ app: {
+ pageTransition: true,
+ layoutTransition: true,
+ teleportId: 'nuxt-teleport',
+ teleportTag: 'span',
+ head: {
+ charset: 'utf-8',
+ link: [undefined],
+ meta: [
+ { name: 'viewport', content: 'width=1024, initial-scale=1' },
+ { charset: 'utf-8' },
+ { name: 'description', content: 'Nuxt Fixture' },
+ ],
+ },
+ keepalive: {
+ include: ['keepalive-in-config', 'not-keepalive-in-nuxtpage'],
+ },
+ },
+ css: ['~/assets/global.css'],
+ vue: {
+ compilerOptions: {
+ isCustomElement: (tag) => {
+ return tag === 'custom-component'
+ },
+ },
+ },
+ appConfig: {
+ fromNuxtConfig: true,
+ nested: {
+ val: 1,
+ },
+ },
+ runtimeConfig: {
+ public: {
+ needsFallback: undefined,
+ },
+ },
+ builder: process.env.TEST_BUILDER as 'webpack' | 'rspack' | 'vite' ?? 'vite',
+ build: {
+ transpile: [
+ (ctx) => {
+ if (typeof ctx.isDev !== 'boolean') { throw new TypeError('context not passed') }
+ return false
+ },
+ ],
+ },
+ optimization: {
+ keyedComposables: [
+ {
+ name: 'useCustomKeyedComposable',
+ source: '~/other-composables-folder/custom-keyed-composable',
+ argumentLength: 1,
+ },
+ ],
+ },
+ features: {
+ inlineStyles: id => !!id && !id.includes('assets.vue'),
+ },
+ experimental: {
+ serverAppConfig: true,
+ typedPages: true,
+ clientFallback: true,
+ restoreState: true,
+ clientNodeCompat: true,
+ componentIslands: {
+ selectiveClient: 'deep',
+ },
+ asyncContext: process.env.TEST_CONTEXT === 'async',
+ appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
+ renderJsonPayloads: process.env.TEST_PAYLOAD !== 'js',
+ headNext: true,
+ inlineRouteRules: true,
+ },
+ compatibilityDate: '2024-06-28',
+ nitro: {
+ publicAssets: [
+ {
+ dir: '../custom-public',
+ baseURL: '/custom',
+ },
+ ],
+ esbuild: {
+ options: {
+ // in order to test bigint serialization
+ target: 'es2022',
+ },
+ },
+ routeRules: {
+ '/route-rules/spa': { ssr: false },
+ '/redirect/catchall': { ssr: false },
+ '/head-spa': { ssr: false },
+ '/route-rules/middleware': { appMiddleware: 'route-rules-middleware' },
+ '/hydration/spa-redirection/**': { ssr: false },
+ '/no-scripts': { experimentalNoScripts: true },
+ '/prerender/**': { prerender: true },
+ },
+ prerender: {
+ routes: [
+ '/random/a',
+ '/random/b',
+ '/random/c',
+ '/prefetch/server-components',
+ ],
+ },
+ },
vite: {
logLevel: 'silent',
build: {
@@ -231,35 +263,4 @@ export default defineNuxtConfig({
})
},
},
- vue: {
- compilerOptions: {
- isCustomElement: (tag) => {
- return tag === 'custom-component'
- },
- },
- },
- features: {
- inlineStyles: id => !!id && !id.includes('assets.vue'),
- },
- experimental: {
- serverAppConfig: true,
- typedPages: true,
- clientFallback: true,
- restoreState: true,
- clientNodeCompat: true,
- componentIslands: {
- selectiveClient: 'deep',
- },
- asyncContext: process.env.TEST_CONTEXT === 'async',
- appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
- renderJsonPayloads: process.env.TEST_PAYLOAD !== 'js',
- headNext: true,
- inlineRouteRules: true,
- },
- appConfig: {
- fromNuxtConfig: true,
- nested: {
- val: 1,
- },
- },
})
diff --git a/test/fixtures/basic/package.json b/test/fixtures/basic/package.json
index 5eceaeb30d..b4103147af 100644
--- a/test/fixtures/basic/package.json
+++ b/test/fixtures/basic/package.json
@@ -5,6 +5,7 @@
"build": "nuxi build"
},
"dependencies": {
+ "@nuxt/rspack-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"nuxt": "workspace:*"
},
diff --git a/test/fixtures/basic/pages/[...slug].vue b/test/fixtures/basic/pages/catchall/[...slug].vue
similarity index 81%
rename from test/fixtures/basic/pages/[...slug].vue
rename to test/fixtures/basic/pages/catchall/[...slug].vue
index 22f81cd64f..1af000b72b 100644
--- a/test/fixtures/basic/pages/[...slug].vue
+++ b/test/fixtures/basic/pages/catchall/[...slug].vue
@@ -9,9 +9,9 @@