mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-26 07:32:01 +00:00
Merge branch 'main' into patch-21
This commit is contained in:
commit
8d066205c2
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
|||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Cache dist
|
- name: Cache dist
|
||||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||||
with:
|
with:
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
name: dist
|
name: dist
|
||||||
@ -85,7 +85,7 @@ jobs:
|
|||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
queries: +security-and-quality
|
queries: +security-and-quality
|
||||||
@ -97,7 +97,7 @@ jobs:
|
|||||||
path: packages
|
path: packages
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||||
with:
|
with:
|
||||||
category: "/language:javascript"
|
category: "/language:javascript"
|
||||||
|
|
||||||
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@ -59,7 +59,7 @@ jobs:
|
|||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
# format to the repository Actions tab.
|
# format to the repository Actions tab.
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||||
if: github.repository == 'nuxt/nuxt' && success()
|
if: github.repository == 'nuxt/nuxt' && success()
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
@ -68,7 +68,7 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||||
if: github.repository == 'nuxt/nuxt' && success()
|
if: github.repository == 'nuxt/nuxt' && success()
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
@ -161,7 +161,13 @@ export default defineVitestConfig({
|
|||||||
|
|
||||||
#### `mountSuspended`
|
#### `mountSuspended`
|
||||||
|
|
||||||
`mountSuspended` allows you to mount any Vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins. For example:
|
`mountSuspended` allows you to mount any Vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins.
|
||||||
|
|
||||||
|
::alert{type=info}
|
||||||
|
Under the hood, `mountSuspended` wraps `mount` from `@vue/test-utils`, so you can check out [the Vue Test Utils documentation](https://test-utils.vuejs.org/guide/) for more on the options you can pass, and how to use this utility.
|
||||||
|
::
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
```ts twoslash
|
```ts twoslash
|
||||||
import { it, expect } from 'vitest'
|
import { it, expect } from 'vitest'
|
||||||
@ -207,6 +213,7 @@ it('can also mount an app', async () => {
|
|||||||
`renderSuspended` allows you to render any Vue component within the Nuxt environment using `@testing-library/vue`, allowing async setup and access to injections from your Nuxt plugins.
|
`renderSuspended` allows you to render any Vue component within the Nuxt environment using `@testing-library/vue`, allowing async setup and access to injections from your Nuxt plugins.
|
||||||
|
|
||||||
This should be used together with utilities from Testing Library, e.g. `screen` and `fireEvent`. Install [@testing-library/vue](https://testing-library.com/docs/vue-testing-library/intro) in your project to use these.
|
This should be used together with utilities from Testing Library, e.g. `screen` and `fireEvent`. Install [@testing-library/vue](https://testing-library.com/docs/vue-testing-library/intro) in your project to use these.
|
||||||
|
|
||||||
Additionally, Testing Library also relies on testing globals for cleanup. You should turn these on in your [Vitest config](https://vitest.dev/config/#globals).
|
Additionally, Testing Library also relies on testing globals for cleanup. You should turn these on in your [Vitest config](https://vitest.dev/config/#globals).
|
||||||
|
|
||||||
The passed in component will be rendered inside a `<div id="test-wrapper"></div>`.
|
The passed in component will be rendered inside a `<div id="test-wrapper"></div>`.
|
||||||
@ -266,7 +273,9 @@ mockNuxtImport('useStorage', () => {
|
|||||||
// your tests here
|
// your tests here
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note**: `mockNuxtImport` can only be used once per mocked import per test file. It is actually a macro that gets transformed to `vi.mock` and `vi.mock` is hoisted, as described [here](https://vitest.dev/api/vi.html#vi-mock).
|
::alert{type=info}
|
||||||
|
`mockNuxtImport` can only be used once per mocked import per test file. It is actually a macro that gets transformed to `vi.mock` and `vi.mock` is hoisted, as described [here](https://vitest.dev/api/vi.html#vi-mock).
|
||||||
|
::
|
||||||
|
|
||||||
If you need to mock a Nuxt import and provide different implementations between tests, you can do it by creating and exposing your mocks using [`vi.hoisted`](https://vitest.dev/api/vi.html#vi-hoisted), and then use those mocks in `mockNuxtImport`. You then have access to the mocked imports, and can change the implementation between tests. Be careful to [restore mocks](https://vitest.dev/api/mock.html#mockrestore) before or after each test to undo mock state changes between runs.
|
If you need to mock a Nuxt import and provide different implementations between tests, you can do it by creating and exposing your mocks using [`vi.hoisted`](https://vitest.dev/api/vi.html#vi-hoisted), and then use those mocks in `mockNuxtImport`. You then have access to the mocked imports, and can change the implementation between tests. Be careful to [restore mocks](https://vitest.dev/api/mock.html#mockrestore) before or after each test to undo mock state changes between runs.
|
||||||
|
|
||||||
|
@ -12,19 +12,19 @@ To enable type-checking at build or development time, install `vue-tsc` and `typ
|
|||||||
::code-group
|
::code-group
|
||||||
|
|
||||||
```bash [yarn]
|
```bash [yarn]
|
||||||
yarn add --dev vue-tsc@^1 typescript
|
yarn add --dev vue-tsc typescript
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash [npm]
|
```bash [npm]
|
||||||
npm install --save-dev vue-tsc@^1 typescript
|
npm install --save-dev vue-tsc typescript
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash [pnpm]
|
```bash [pnpm]
|
||||||
pnpm add -D vue-tsc@^1 typescript
|
pnpm add -D vue-tsc typescript
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash [bun]
|
```bash [bun]
|
||||||
bun add -D vue-tsc@^1 typescript
|
bun add -D vue-tsc typescript
|
||||||
```
|
```
|
||||||
|
|
||||||
::
|
::
|
||||||
|
@ -6,7 +6,7 @@ navigation.icon: i-ph-folder-duotone
|
|||||||
---
|
---
|
||||||
|
|
||||||
::note
|
::note
|
||||||
To reduce your application's bundle size, this directory is **optional**, meaning that [`vue-router`](https://router.vuejs.org) won't be included if you only use [`app.vue`](/docs/guide/directory-structure/app). To force the pages system, set `pages: true` in `nuxt.config` or have a [`app/router.options.ts`](/docs/guide/going-further/custom-routing#using-approuteroptions).
|
To reduce your application's bundle size, this directory is **optional**, meaning that [`vue-router`](https://router.vuejs.org) won't be included if you only use [`app.vue`](/docs/guide/directory-structure/app). To force the pages system, set `pages: true` in `nuxt.config` or have a [`app/router.options.ts`](/docs/guide/recipes/custom-routing#using-approuteroptions).
|
||||||
::
|
::
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -226,6 +226,22 @@ definePageMeta({
|
|||||||
|
|
||||||
:link-example{to="/docs/examples/routing/pages"}
|
:link-example{to="/docs/examples/routing/pages"}
|
||||||
|
|
||||||
|
## Route Groups
|
||||||
|
|
||||||
|
In some cases, you may want to group a set of routes together in a way which doesn't affect file-based routing. For this purpose, you can put files in a folder which is wrapped in parentheses - `(` and `)`.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash [Directory structure]
|
||||||
|
-| pages/
|
||||||
|
---| index.vue
|
||||||
|
---| (marketing)/
|
||||||
|
-----| about.vue
|
||||||
|
-----| contact.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
This will produce `/`, `/about` and `/contact` pages in your app. The `marketing` group is ignored for purposes of your URL structure.
|
||||||
|
|
||||||
## Page Metadata
|
## Page Metadata
|
||||||
|
|
||||||
You might want to define metadata for each route in your app. You can do this using the `definePageMeta` macro, which will work both in `<script>` and in `<script setup>`:
|
You might want to define metadata for each route in your app. You can do this using the `definePageMeta` macro, which will work both in `<script>` and in `<script setup>`:
|
||||||
@ -381,7 +397,7 @@ Server-only pages must have a single root element. (HTML comments are considered
|
|||||||
|
|
||||||
As your app gets bigger and more complex, your routing might require more flexibility. For this reason, Nuxt directly exposes the router, routes and router options for customization in different ways.
|
As your app gets bigger and more complex, your routing might require more flexibility. For this reason, Nuxt directly exposes the router, routes and router options for customization in different ways.
|
||||||
|
|
||||||
:read-more{to="/docs/guide/going-further/custom-routing"}
|
:read-more{to="/docs/guide/recipes/custom-routing"}
|
||||||
|
|
||||||
## Multiple Pages Directories
|
## Multiple Pages Directories
|
||||||
|
|
||||||
|
@ -9,14 +9,14 @@ In Nuxt 3, your routing is defined by the structure of your files inside the [pa
|
|||||||
|
|
||||||
### Router Config
|
### Router Config
|
||||||
|
|
||||||
Using [router options](/docs/guide/going-further/custom-routing#router-options), you can optionally override or extend your routes using a function that accepts the scanned routes and returns customized routes.
|
Using [router options](/docs/guide/recipes/custom-routing#router-options), you can optionally override or extend your routes using a function that accepts the scanned routes and returns customized routes.
|
||||||
|
|
||||||
If it returns `null` or `undefined`, Nuxt will fall back to the default routes (useful to modify input array).
|
If it returns `null` or `undefined`, Nuxt will fall back to the default routes (useful to modify input array).
|
||||||
|
|
||||||
```ts [app/router.options.ts]
|
```ts [app/router.options.ts]
|
||||||
import type { RouterConfig } from '@nuxt/schema'
|
import type { RouterConfig } from '@nuxt/schema'
|
||||||
|
|
||||||
export default <RouterConfig> {
|
export default {
|
||||||
// https://router.vuejs.org/api/interfaces/routeroptions.html#routes
|
// https://router.vuejs.org/api/interfaces/routeroptions.html#routes
|
||||||
routes: (_routes) => [
|
routes: (_routes) => [
|
||||||
{
|
{
|
||||||
@ -25,7 +25,7 @@ export default <RouterConfig> {
|
|||||||
component: () => import('~/pages/home.vue').then(r => r.default || r)
|
component: () => import('~/pages/home.vue').then(r => r.default || r)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
} satisfies RouterConfig
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
::note
|
||||||
@ -90,8 +90,8 @@ This is the recommended way to specify [router options](/docs/api/nuxt-config#ro
|
|||||||
```ts [app/router.options.ts]
|
```ts [app/router.options.ts]
|
||||||
import type { RouterConfig } from '@nuxt/schema'
|
import type { RouterConfig } from '@nuxt/schema'
|
||||||
|
|
||||||
export default <RouterConfig> {
|
export default {
|
||||||
}
|
} satisfies RouterConfig
|
||||||
```
|
```
|
||||||
|
|
||||||
It is possible to add more router options files by adding files within the `pages:routerOptions` hook. Later items in the array override earlier ones.
|
It is possible to add more router options files by adding files within the `pages:routerOptions` hook. Later items in the array override earlier ones.
|
||||||
@ -174,8 +174,8 @@ You can optionally override history mode using a function that accepts the base
|
|||||||
import type { RouterConfig } from '@nuxt/schema'
|
import type { RouterConfig } from '@nuxt/schema'
|
||||||
import { createMemoryHistory } from 'vue-router'
|
import { createMemoryHistory } from 'vue-router'
|
||||||
|
|
||||||
export default <RouterConfig> {
|
export default {
|
||||||
// https://router.vuejs.org/api/interfaces/routeroptions.html
|
// https://router.vuejs.org/api/interfaces/routeroptions.html
|
||||||
history: base => import.meta.client ? createMemoryHistory(base) : null /* default */
|
history: base => import.meta.client ? createMemoryHistory(base) : null /* default */
|
||||||
}
|
} satisfies RouterConfig
|
||||||
```
|
```
|
||||||
|
@ -4,7 +4,7 @@ description: The <Teleport> component teleports a component to a different locat
|
|||||||
---
|
---
|
||||||
|
|
||||||
::warning
|
::warning
|
||||||
The `to` target of [`<Teleport>`](https://vuejs.org/guide/built-ins/teleport.html) expects a CSS selector string or an actual DOM node. Nuxt currently has SSR support for teleports to `body` only, with client-side support for other targets using a `<ClientOnly>` wrapper.
|
The `to` target of [`<Teleport>`](https://vuejs.org/guide/built-ins/teleport.html) expects a CSS selector string or an actual DOM node. Nuxt currently has SSR support for teleports to `#teleports` only, with client-side support for other targets using a `<ClientOnly>` wrapper.
|
||||||
::
|
::
|
||||||
|
|
||||||
## Body Teleport
|
## Body Teleport
|
||||||
@ -14,7 +14,7 @@ The `to` target of [`<Teleport>`](https://vuejs.org/guide/built-ins/teleport.htm
|
|||||||
<button @click="open = true">
|
<button @click="open = true">
|
||||||
Open Modal
|
Open Modal
|
||||||
</button>
|
</button>
|
||||||
<Teleport to="body">
|
<Teleport to="#teleports">
|
||||||
<div v-if="open" class="modal">
|
<div v-if="open" class="modal">
|
||||||
<p>Hello from the modal!</p>
|
<p>Hello from the modal!</p>
|
||||||
<button @click="open = false">
|
<button @click="open = false">
|
||||||
|
@ -52,6 +52,25 @@ const { enabled, state } = usePreviewMode({
|
|||||||
The `getState` function will append returned values to current state, so be careful not to accidentally overwrite important state.
|
The `getState` function will append returned values to current state, so be careful not to accidentally overwrite important state.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Customize the `onEnable` and `onDisable` callbacks
|
||||||
|
|
||||||
|
By default, when `usePreviewMode` is enabled, it will call `refreshNuxtData()` to re-fetch all data from the server.
|
||||||
|
|
||||||
|
When preview mode is disabled, the composable will attach a callback to call `refreshNuxtData()` to run after a subsequent router navigation.
|
||||||
|
|
||||||
|
You can specify custom callbacks to be triggered by providing your own functions for the `onEnable` and `onDisable` options.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { enabled, state } = usePreviewMode({
|
||||||
|
onEnable: () => {
|
||||||
|
console.log('preview mode has been enabled')
|
||||||
|
},
|
||||||
|
onDisable: () => {
|
||||||
|
console.log('preview mode has been disabled')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The example below creates a page where part of a content is rendered only in preview mode.
|
The example below creates a page where part of a content is rendered only in preview mode.
|
||||||
|
@ -129,7 +129,7 @@ interface PageMeta {
|
|||||||
|
|
||||||
- **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean`
|
- **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean`
|
||||||
|
|
||||||
Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [custom routing](/docs/guide/going-further/custom-routing#using-approuteroptions)) for more info.
|
Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [custom routing](/docs/guide/recipes/custom-routing#using-approuteroptions)) for more info.
|
||||||
|
|
||||||
**`[key: string]`**
|
**`[key: string]`**
|
||||||
|
|
||||||
|
@ -209,6 +209,7 @@ type NuxtMiddleware = {
|
|||||||
|
|
||||||
interface AddRouteMiddlewareOptions {
|
interface AddRouteMiddlewareOptions {
|
||||||
override?: boolean
|
override?: boolean
|
||||||
|
prepend?: boolean
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -246,7 +247,21 @@ A middleware object or an array of middleware objects with the following propert
|
|||||||
|
|
||||||
**Default**: `{}`
|
**Default**: `{}`
|
||||||
|
|
||||||
Options to pass to the middleware. If `override` is set to `true`, it will override the existing middleware with the same name.
|
- `override` (optional)
|
||||||
|
|
||||||
|
**Type**: `boolean`
|
||||||
|
|
||||||
|
**Default**: `false`
|
||||||
|
|
||||||
|
If enabled, overrides the existing middleware with the same name.
|
||||||
|
|
||||||
|
- `prepend` (optional)
|
||||||
|
|
||||||
|
**Type**: `boolean`
|
||||||
|
|
||||||
|
**Default**: `false`
|
||||||
|
|
||||||
|
If enabled, prepends the middleware to the list of existing middleware.
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
@ -272,7 +287,7 @@ export default defineNuxtModule({
|
|||||||
name: 'auth',
|
name: 'auth',
|
||||||
path: resolver.resolve('runtime/auth.ts'),
|
path: resolver.resolve('runtime/auth.ts'),
|
||||||
global: true
|
global: true
|
||||||
})
|
}, { prepend: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
@ -103,7 +103,7 @@ This feature is not yet supported in Nuxt 3.
|
|||||||
|
|
||||||
## `scrollToTop`
|
## `scrollToTop`
|
||||||
|
|
||||||
This feature is not yet supported in Nuxt 3. If you want to overwrite the default scroll behavior of `vue-router`, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/going-further/custom-routing#router-options)) for more info.
|
This feature is not yet supported in Nuxt 3. If you want to overwrite the default scroll behavior of `vue-router`, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/recipes/custom-routing#router-options)) for more info.
|
||||||
Similar to `key`, specify it within the [`definePageMeta`](/docs/api/utils/define-page-meta) compiler macro.
|
Similar to `key`, specify it within the [`definePageMeta`](/docs/api/utils/define-page-meta) compiler macro.
|
||||||
|
|
||||||
```diff [pages/index.vue]
|
```diff [pages/index.vue]
|
||||||
|
29
package.json
29
package.json
@ -31,7 +31,7 @@
|
|||||||
"test:types": "pnpm --filter './test/fixtures/**' test:types",
|
"test:types": "pnpm --filter './test/fixtures/**' test:types",
|
||||||
"test:unit": "vitest run packages/",
|
"test:unit": "vitest run packages/",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs"
|
"typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs --languages html"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@nuxt/kit": "workspace:*",
|
"@nuxt/kit": "workspace:*",
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"@nuxt/ui-templates": "workspace:*",
|
"@nuxt/ui-templates": "workspace:*",
|
||||||
"@nuxt/vite-builder": "workspace:*",
|
"@nuxt/vite-builder": "workspace:*",
|
||||||
"@nuxt/webpack-builder": "workspace:*",
|
"@nuxt/webpack-builder": "workspace:*",
|
||||||
"@types/node": "*",
|
"@types/node": "20.14.15",
|
||||||
"c12": "2.0.0-beta.1",
|
"c12": "2.0.0-beta.1",
|
||||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||||
"jiti": "2.0.0-beta.3",
|
"jiti": "2.0.0-beta.3",
|
||||||
@ -49,18 +49,18 @@
|
|||||||
"rollup": "^4.20.0",
|
"rollup": "^4.20.0",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vite": "5.3.5",
|
"vite": "5.4.0",
|
||||||
"vue": "3.4.34"
|
"vue": "3.4.37"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.8.0",
|
"@eslint/js": "9.9.0",
|
||||||
"@nuxt/eslint-config": "0.5.0",
|
"@nuxt/eslint-config": "0.5.0",
|
||||||
"@nuxt/kit": "workspace:*",
|
"@nuxt/kit": "workspace:*",
|
||||||
"@nuxt/test-utils": "3.14.0",
|
"@nuxt/test-utils": "3.14.1",
|
||||||
"@nuxt/webpack-builder": "workspace:*",
|
"@nuxt/webpack-builder": "workspace:*",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/eslint__js": "8.42.3",
|
"@types/eslint__js": "8.42.3",
|
||||||
"@types/node": "20.14.14",
|
"@types/node": "20.14.15",
|
||||||
"@types/semver": "7.5.8",
|
"@types/semver": "7.5.8",
|
||||||
"@unhead/schema": "1.9.16",
|
"@unhead/schema": "1.9.16",
|
||||||
"@vitejs/plugin-vue": "5.1.2",
|
"@vitejs/plugin-vue": "5.1.2",
|
||||||
@ -70,11 +70,12 @@
|
|||||||
"case-police": "0.6.1",
|
"case-police": "0.6.1",
|
||||||
"changelogen": "0.5.5",
|
"changelogen": "0.5.5",
|
||||||
"consola": "3.2.3",
|
"consola": "3.2.3",
|
||||||
"cssnano": "7.0.4",
|
"cssnano": "7.0.5",
|
||||||
|
"destr": "2.0.3",
|
||||||
"devalue": "5.0.0",
|
"devalue": "5.0.0",
|
||||||
"eslint": "9.8.0",
|
"eslint": "9.9.0",
|
||||||
"eslint-plugin-no-only-tests": "3.1.0",
|
"eslint-plugin-no-only-tests": "3.1.0",
|
||||||
"eslint-plugin-perfectionist": "3.1.2",
|
"eslint-plugin-perfectionist": "3.1.3",
|
||||||
"eslint-typegen": "0.3.0",
|
"eslint-typegen": "0.3.0",
|
||||||
"execa": "9.3.0",
|
"execa": "9.3.0",
|
||||||
"globby": "14.0.2",
|
"globby": "14.0.2",
|
||||||
@ -88,7 +89,7 @@
|
|||||||
"nuxt-content-twoslash": "0.1.1",
|
"nuxt-content-twoslash": "0.1.1",
|
||||||
"ofetch": "1.3.4",
|
"ofetch": "1.3.4",
|
||||||
"pathe": "1.1.2",
|
"pathe": "1.1.2",
|
||||||
"playwright-core": "1.45.3",
|
"playwright-core": "1.46.0",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
"semver": "7.6.3",
|
"semver": "7.6.3",
|
||||||
"std-env": "3.7.0",
|
"std-env": "3.7.0",
|
||||||
@ -96,11 +97,11 @@
|
|||||||
"ufo": "1.5.4",
|
"ufo": "1.5.4",
|
||||||
"vitest": "2.0.5",
|
"vitest": "2.0.5",
|
||||||
"vitest-environment-nuxt": "1.0.0",
|
"vitest-environment-nuxt": "1.0.0",
|
||||||
"vue": "3.4.34",
|
"vue": "3.4.37",
|
||||||
"vue-router": "4.4.2",
|
"vue-router": "4.4.3",
|
||||||
"vue-tsc": "2.0.29"
|
"vue-tsc": "2.0.29"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.6.0",
|
"packageManager": "pnpm@9.7.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.10.0 || >=18.0.0"
|
"node": "^16.10.0 || >=18.0.0"
|
||||||
},
|
},
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
"errx": "^0.1.0",
|
"errx": "^0.1.0",
|
||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"hash-sum": "^2.0.0",
|
"hash-sum": "^2.0.0",
|
||||||
"ignore": "^5.3.1",
|
"ignore": "^5.3.2",
|
||||||
"jiti": "^2.0.0-beta.3",
|
"jiti": "^2.0.0-beta.3",
|
||||||
"klona": "^2.0.6",
|
"klona": "^2.0.6",
|
||||||
"mlly": "^1.7.1",
|
"mlly": "^1.7.1",
|
||||||
@ -52,7 +52,7 @@
|
|||||||
"@types/semver": "7.5.8",
|
"@types/semver": "7.5.8",
|
||||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vite": "5.3.5",
|
"vite": "5.4.0",
|
||||||
"vitest": "2.0.5",
|
"vitest": "2.0.5",
|
||||||
"webpack": "5.93.0"
|
"webpack": "5.93.0"
|
||||||
},
|
},
|
||||||
|
@ -35,6 +35,11 @@ export interface AddRouteMiddlewareOptions {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
override?: boolean
|
override?: boolean
|
||||||
|
/**
|
||||||
|
* Prepend middleware to the list
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
prepend?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], options: AddRouteMiddlewareOptions = {}) {
|
export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], options: AddRouteMiddlewareOptions = {}) {
|
||||||
@ -51,6 +56,8 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
|
|||||||
} else {
|
} else {
|
||||||
logger.warn(`'${middleware.name}' middleware already exists at '${foundPath}'. You can set \`override: true\` to replace it.`)
|
logger.warn(`'${middleware.name}' middleware already exists at '${foundPath}'. You can set \`override: true\` to replace it.`)
|
||||||
}
|
}
|
||||||
|
} else if (options.prepend === true) {
|
||||||
|
app.middleware.unshift({ ...middleware })
|
||||||
} else {
|
} else {
|
||||||
app.middleware.push({ ...middleware })
|
app.middleware.push({ ...middleware })
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@
|
|||||||
"@unhead/dom": "^1.9.16",
|
"@unhead/dom": "^1.9.16",
|
||||||
"@unhead/ssr": "^1.9.16",
|
"@unhead/ssr": "^1.9.16",
|
||||||
"@unhead/vue": "^1.9.16",
|
"@unhead/vue": "^1.9.16",
|
||||||
"@vue/shared": "^3.4.34",
|
"@vue/shared": "^3.4.37",
|
||||||
"acorn": "8.12.1",
|
"acorn": "8.12.1",
|
||||||
"c12": "^2.0.0-beta.1",
|
"c12": "^2.0.0-beta.1",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
@ -85,7 +85,7 @@
|
|||||||
"globby": "^14.0.2",
|
"globby": "^14.0.2",
|
||||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||||
"hookable": "^5.5.3",
|
"hookable": "^5.5.3",
|
||||||
"ignore": "^5.3.1",
|
"ignore": "^5.3.2",
|
||||||
"jiti": "^2.0.0-beta.3",
|
"jiti": "^2.0.0-beta.3",
|
||||||
"klona": "^2.0.6",
|
"klona": "^2.0.6",
|
||||||
"knitwork": "^1.1.0",
|
"knitwork": "^1.1.0",
|
||||||
@ -110,24 +110,24 @@
|
|||||||
"unctx": "^2.3.1",
|
"unctx": "^2.3.1",
|
||||||
"unenv": "^1.10.0",
|
"unenv": "^1.10.0",
|
||||||
"unimport": "^3.10.0",
|
"unimport": "^3.10.0",
|
||||||
"unplugin": "^1.12.0",
|
"unplugin": "^1.12.1",
|
||||||
"unplugin-vue-router": "^0.10.2",
|
"unplugin-vue-router": "^0.10.3",
|
||||||
"unstorage": "^1.10.2",
|
"unstorage": "^1.10.2",
|
||||||
"untyped": "^1.4.2",
|
"untyped": "^1.4.2",
|
||||||
"vue": "^3.4.34",
|
"vue": "^3.4.37",
|
||||||
"vue-bundle-renderer": "^2.1.0",
|
"vue-bundle-renderer": "^2.1.0",
|
||||||
"vue-devtools-stub": "^0.1.0",
|
"vue-devtools-stub": "^0.1.0",
|
||||||
"vue-router": "^4.4.2"
|
"vue-router": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/scripts": "0.6.5",
|
"@nuxt/scripts": "0.6.6",
|
||||||
"@nuxt/ui-templates": "1.3.4",
|
"@nuxt/ui-templates": "1.3.4",
|
||||||
"@parcel/watcher": "2.4.1",
|
"@parcel/watcher": "2.4.1",
|
||||||
"@types/estree": "1.0.5",
|
"@types/estree": "1.0.5",
|
||||||
"@vitejs/plugin-vue": "5.1.2",
|
"@vitejs/plugin-vue": "5.1.2",
|
||||||
"@vue/compiler-sfc": "3.4.34",
|
"@vue/compiler-sfc": "3.4.37",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vite": "5.3.5",
|
"vite": "5.4.0",
|
||||||
"vitest": "2.0.5"
|
"vitest": "2.0.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
|
import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
|
||||||
import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue'
|
import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue'
|
||||||
|
import { isPromise } from '@vue/shared'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
import { getFragmentHTML } from './utils'
|
import { getFragmentHTML } from './utils'
|
||||||
|
|
||||||
@ -42,9 +43,10 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
|||||||
const clone = { ...component }
|
const clone = { ...component }
|
||||||
|
|
||||||
if (clone.render) {
|
if (clone.render) {
|
||||||
// override the component render (non script setup component)
|
// override the component render (non script setup component) or dev mode
|
||||||
clone.render = (ctx: any, cache: any, $props: any, $setup: any, $data: any, $options: any) => {
|
clone.render = (ctx: any, cache: any, $props: any, $setup: any, $data: any, $options: any) => {
|
||||||
if ($setup.mounted$ ?? ctx.mounted$) {
|
// import.meta.client for server-side treeshakking
|
||||||
|
if (import.meta.client && ($setup.mounted$ ?? ctx.mounted$)) {
|
||||||
const res = component.render?.bind(ctx)(ctx, cache, $props, $setup, $data, $options)
|
const res = component.render?.bind(ctx)(ctx, cache, $props, $setup, $data, $options)
|
||||||
return (res.children === null || typeof res.children === 'string')
|
return (res.children === null || typeof res.children === 'string')
|
||||||
? cloneVNode(res)
|
? cloneVNode(res)
|
||||||
@ -63,33 +65,39 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone.setup = (props, ctx) => {
|
clone.setup = (props, ctx) => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const mounted$ = ref(import.meta.client && nuxtApp.isHydrating === false)
|
||||||
const instance = getCurrentInstance()!
|
const instance = getCurrentInstance()!
|
||||||
|
|
||||||
const attrs = { ...instance.attrs }
|
if (import.meta.server || nuxtApp.isHydrating) {
|
||||||
|
const attrs = { ...instance.attrs }
|
||||||
|
// remove existing directives during hydration
|
||||||
|
const directives = extractDirectives(instance)
|
||||||
|
// prevent attrs inheritance since a staticVNode is rendered before hydration
|
||||||
|
for (const key in attrs) {
|
||||||
|
delete instance.attrs[key]
|
||||||
|
}
|
||||||
|
|
||||||
// remove existing directives during hydration
|
onMounted(() => {
|
||||||
const directives = extractDirectives(instance)
|
Object.assign(instance.attrs, attrs)
|
||||||
// prevent attrs inheritance since a staticVNode is rendered before hydration
|
instance.vnode.dirs = directives
|
||||||
for (const key in attrs) {
|
})
|
||||||
delete instance.attrs[key]
|
|
||||||
}
|
}
|
||||||
const mounted$ = ref(false)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
Object.assign(instance.attrs, attrs)
|
|
||||||
instance.vnode.dirs = directives
|
|
||||||
mounted$.value = true
|
mounted$.value = true
|
||||||
})
|
})
|
||||||
|
const setupState = component.setup?.(props, ctx) || {}
|
||||||
|
|
||||||
return Promise.resolve(component.setup?.(props, ctx) || {})
|
if (isPromise(setupState)) {
|
||||||
.then((setupState) => {
|
return Promise.resolve(setupState).then((setupState) => {
|
||||||
if (typeof setupState !== 'function') {
|
if (typeof setupState !== 'function') {
|
||||||
setupState = setupState || {}
|
setupState = setupState || {}
|
||||||
setupState.mounted$ = mounted$
|
setupState.mounted$ = mounted$
|
||||||
return setupState
|
return setupState
|
||||||
}
|
}
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
if (mounted$.value) {
|
if (import.meta.client && (mounted$.value || !nuxtApp.isHydrating)) {
|
||||||
const res = setupState(...args)
|
const res = setupState(...args)
|
||||||
return (res.children === null || typeof res.children === 'string')
|
return (res.children === null || typeof res.children === 'string')
|
||||||
? cloneVNode(res)
|
? cloneVNode(res)
|
||||||
@ -100,6 +108,20 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
if (typeof setupState === 'function') {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
if (mounted$.value) {
|
||||||
|
return h(setupState(...args), ctx.attrs)
|
||||||
|
}
|
||||||
|
const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['<div></div>']
|
||||||
|
return import.meta.client
|
||||||
|
? createStaticVNode(fragment.join(''), fragment.length) :
|
||||||
|
h('div', ctx.attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.assign(setupState, { mounted$ })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.set(component, clone)
|
cache.set(component, clone)
|
||||||
|
@ -40,6 +40,7 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
|||||||
export function useCookie<T = string | null | undefined> (name: string, _opts: CookieOptions<T> & { readonly: true }): Readonly<CookieRef<T>>
|
export function useCookie<T = string | null | undefined> (name: string, _opts: CookieOptions<T> & { readonly: true }): Readonly<CookieRef<T>>
|
||||||
export function useCookie<T = string | null | undefined> (name: string, _opts?: CookieOptions<T>): CookieRef<T> {
|
export function useCookie<T = string | null | undefined> (name: string, _opts?: CookieOptions<T>): CookieRef<T> {
|
||||||
const opts = { ...CookieDefaults, ..._opts }
|
const opts = { ...CookieDefaults, ..._opts }
|
||||||
|
opts.filter ??= key => key === name
|
||||||
const cookies = readRawCookies(opts) || {}
|
const cookies = readRawCookies(opts) || {}
|
||||||
|
|
||||||
let delay: number | undefined
|
let delay: number | undefined
|
||||||
|
@ -152,7 +152,7 @@ export function useFetch<
|
|||||||
let controller: AbortController
|
let controller: AbortController
|
||||||
|
|
||||||
const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => {
|
const asyncData = useAsyncData<_ResT, ErrorT, DataT, PickKeys, DefaultT>(key, () => {
|
||||||
controller?.abort?.()
|
controller?.abort?.('Request aborted as another request to the same endpoint was initiated.')
|
||||||
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
|
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,7 +164,7 @@ export function useFetch<
|
|||||||
const timeoutLength = toValue(opts.timeout)
|
const timeoutLength = toValue(opts.timeout)
|
||||||
let timeoutId: NodeJS.Timeout
|
let timeoutId: NodeJS.Timeout
|
||||||
if (timeoutLength) {
|
if (timeoutLength) {
|
||||||
timeoutId = setTimeout(() => controller.abort(), timeoutLength)
|
timeoutId = setTimeout(() => controller.abort('Request aborted due to timeout.'), timeoutLength)
|
||||||
controller.signal.onabort = () => clearTimeout(timeoutId)
|
controller.signal.onabort = () => clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ const SEPARATOR = '-'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an SSR-friendly unique identifier that can be passed to accessibility attributes.
|
* Generate an SSR-friendly unique identifier that can be passed to accessibility attributes.
|
||||||
|
*
|
||||||
|
* The generated ID is unique in the context of the current Nuxt instance and key.
|
||||||
*/
|
*/
|
||||||
export function useId (): string
|
export function useId (): string
|
||||||
export function useId (key?: string): string {
|
export function useId (key?: string): string {
|
||||||
@ -24,7 +26,7 @@ export function useId (key?: string): string {
|
|||||||
throw new TypeError('[nuxt] `useId` must be called within a component setup function.')
|
throw new TypeError('[nuxt] `useId` must be called within a component setup function.')
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxtApp._id ||= 0
|
nuxtApp._genId ||= 0
|
||||||
instance._nuxtIdIndex ||= {}
|
instance._nuxtIdIndex ||= {}
|
||||||
instance._nuxtIdIndex[key] ||= 0
|
instance._nuxtIdIndex[key] ||= 0
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ export function useId (key?: string): string {
|
|||||||
|
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
const ids = JSON.parse(instance.attrs[ATTR_KEY] as string | undefined || '{}')
|
const ids = JSON.parse(instance.attrs[ATTR_KEY] as string | undefined || '{}')
|
||||||
ids[instanceIndex] = key + SEPARATOR + nuxtApp._id++
|
ids[instanceIndex] = key + SEPARATOR + nuxtApp._genId++
|
||||||
instance.attrs[ATTR_KEY] = JSON.stringify(ids)
|
instance.attrs[ATTR_KEY] = JSON.stringify(ids)
|
||||||
return ids[instanceIndex]
|
return ids[instanceIndex]
|
||||||
}
|
}
|
||||||
@ -54,5 +56,5 @@ export function useId (key?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pure client-side ids, avoiding potential collision with server-side ids
|
// pure client-side ids, avoiding potential collision with server-side ids
|
||||||
return key + '_' + nuxtApp._id++
|
return key + '_' + nuxtApp._genId++
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo'
|
import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo'
|
||||||
import { parse } from 'devalue'
|
import { parse } from 'devalue'
|
||||||
import { useHead } from '@unhead/vue'
|
import { useHead } from '@unhead/vue'
|
||||||
import { getCurrentInstance, onServerPrefetch } from 'vue'
|
import { getCurrentInstance, onServerPrefetch, reactive } from 'vue'
|
||||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||||
import type { NuxtPayload } from '../nuxt'
|
import type { NuxtPayload } from '../nuxt'
|
||||||
|
|
||||||
@ -122,6 +122,10 @@ export async function getNuxtClientPayload () {
|
|||||||
...window.__NUXT__,
|
...window.__NUXT__,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payloadCache!.config?.public) {
|
||||||
|
payloadCache!.config.public = reactive(payloadCache!.config.public)
|
||||||
|
}
|
||||||
|
|
||||||
return payloadCache
|
return payloadCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,9 +10,31 @@ interface Preview {
|
|||||||
_initialized?: boolean
|
_initialized?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for configuring preview mode.
|
||||||
|
*/
|
||||||
interface PreviewModeOptions<S> {
|
interface PreviewModeOptions<S> {
|
||||||
|
/**
|
||||||
|
* A function that determines whether preview mode should be enabled based on the current state.
|
||||||
|
* @param {Record<any, unknown>} state - The state of the preview.
|
||||||
|
* @returns {boolean} A boolean indicating whether the preview mode is enabled.
|
||||||
|
*/
|
||||||
shouldEnable?: (state: Preview['state']) => boolean
|
shouldEnable?: (state: Preview['state']) => boolean
|
||||||
|
/**
|
||||||
|
* A function that retrieves the current state.
|
||||||
|
* The `getState` function will append returned values to current state, so be careful not to accidentally overwrite important state.
|
||||||
|
* @param {Record<any, unknown>} state - The preview state.
|
||||||
|
* @returns {Record<any, unknown>} The preview state.
|
||||||
|
*/
|
||||||
getState?: (state: Preview['state']) => S
|
getState?: (state: Preview['state']) => S
|
||||||
|
/**
|
||||||
|
* A function to be called when the preview mode is enabled.
|
||||||
|
*/
|
||||||
|
onEnable?: () => void
|
||||||
|
/**
|
||||||
|
* A function to be called when the preview mode is disabled.
|
||||||
|
*/
|
||||||
|
onDisable?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnteredState = Record<any, unknown> | null | undefined | void
|
type EnteredState = Record<any, unknown> | null | undefined | void
|
||||||
@ -54,9 +76,10 @@ export function usePreviewMode<S extends EnteredState> (options: PreviewModeOpti
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.client && !unregisterRefreshHook) {
|
if (import.meta.client && !unregisterRefreshHook) {
|
||||||
refreshNuxtData()
|
const onEnable = options.onEnable ?? refreshNuxtData
|
||||||
|
onEnable()
|
||||||
|
|
||||||
unregisterRefreshHook = useRouter().afterEach(() => refreshNuxtData())
|
unregisterRefreshHook = options.onDisable ?? useRouter().afterEach(() => refreshNuxtData())
|
||||||
}
|
}
|
||||||
} else if (unregisterRefreshHook) {
|
} else if (unregisterRefreshHook) {
|
||||||
unregisterRefreshHook()
|
unregisterRefreshHook()
|
||||||
|
@ -18,18 +18,18 @@ export function useScript<T extends Record<string | symbol, any>> (input: UseScr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export function useElementScriptTrigger (...args: unknown[]) {
|
export function useScriptTriggerElement (...args: unknown[]) {
|
||||||
renderStubMessage('useElementScriptTrigger')
|
renderStubMessage('useScriptTriggerElement')
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export function useConsentScriptTrigger (...args: unknown[]) {
|
export function useScriptTriggerConsent (...args: unknown[]) {
|
||||||
renderStubMessage('useConsentScriptTrigger')
|
renderStubMessage('useScriptTriggerConsent')
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export function useAnalyticsPageEvent (...args: unknown[]) {
|
export function useScriptEventPage (...args: unknown[]) {
|
||||||
renderStubMessage('useAnalyticsPageEvent')
|
renderStubMessage('useScriptEventPage')
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@ -25,8 +25,8 @@ import { appId } from '#build/nuxt.config.mjs'
|
|||||||
|
|
||||||
import type { NuxtAppLiterals } from '#app'
|
import type { NuxtAppLiterals } from '#app'
|
||||||
|
|
||||||
function getNuxtAppCtx (appName = appId || 'nuxt-app') {
|
function getNuxtAppCtx (id = appId || 'nuxt-app') {
|
||||||
return getContext<NuxtApp>(appName, {
|
return getContext<NuxtApp>(id, {
|
||||||
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
|
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -98,8 +98,6 @@ export interface NuxtPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface _NuxtApp {
|
interface _NuxtApp {
|
||||||
/** @internal */
|
|
||||||
_name: string
|
|
||||||
vueApp: App<Element>
|
vueApp: App<Element>
|
||||||
versions: Record<string, string>
|
versions: Record<string, string>
|
||||||
|
|
||||||
@ -113,8 +111,15 @@ interface _NuxtApp {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_cookies?: Record<string, unknown>
|
_cookies?: Record<string, unknown>
|
||||||
/** @internal */
|
/**
|
||||||
_id?: number
|
* The id of the Nuxt application.
|
||||||
|
* @internal */
|
||||||
|
_id: string
|
||||||
|
/**
|
||||||
|
* The next id that can be used for generating unique ids via `useId`.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_genId?: number
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_scope: EffectScope
|
_scope: EffectScope
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@ -244,13 +249,17 @@ export type ObjectPluginInput<Injections extends Record<string, unknown> = Recor
|
|||||||
export interface CreateOptions {
|
export interface CreateOptions {
|
||||||
vueApp: NuxtApp['vueApp']
|
vueApp: NuxtApp['vueApp']
|
||||||
ssrContext?: NuxtApp['ssrContext']
|
ssrContext?: NuxtApp['ssrContext']
|
||||||
|
/**
|
||||||
|
* The id of the Nuxt application, overrides the default id specified in the Nuxt config (default: `nuxt-app`).
|
||||||
|
*/
|
||||||
|
id?: NuxtApp['_id']
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @since 3.0.0 */
|
/** @since 3.0.0 */
|
||||||
export function createNuxtApp (options: CreateOptions) {
|
export function createNuxtApp (options: CreateOptions) {
|
||||||
let hydratingCount = 0
|
let hydratingCount = 0
|
||||||
const nuxtApp: NuxtApp = {
|
const nuxtApp: NuxtApp = {
|
||||||
_name: appId || 'nuxt-app',
|
_id: options.id || appId || 'nuxt-app',
|
||||||
_scope: effectScope(),
|
_scope: effectScope(),
|
||||||
provide: undefined,
|
provide: undefined,
|
||||||
versions: {
|
versions: {
|
||||||
@ -489,7 +498,7 @@ export function isNuxtPlugin (plugin: unknown) {
|
|||||||
*/
|
*/
|
||||||
export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) {
|
export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) {
|
||||||
const fn: () => ReturnType<T> = () => args ? setup(...args as Parameters<T>) : setup()
|
const fn: () => ReturnType<T> = () => args ? setup(...args as Parameters<T>) : setup()
|
||||||
const nuxtAppCtx = getNuxtAppCtx(nuxt._name)
|
const nuxtAppCtx = getNuxtAppCtx(nuxt._id)
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn))
|
return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn))
|
||||||
} else {
|
} else {
|
||||||
@ -507,13 +516,13 @@ export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp |
|
|||||||
* @since 3.10.0
|
* @since 3.10.0
|
||||||
*/
|
*/
|
||||||
export function tryUseNuxtApp (): NuxtApp | null
|
export function tryUseNuxtApp (): NuxtApp | null
|
||||||
export function tryUseNuxtApp (appName?: string): NuxtApp | null {
|
export function tryUseNuxtApp (id?: string): NuxtApp | null {
|
||||||
let nuxtAppInstance
|
let nuxtAppInstance
|
||||||
if (hasInjectionContext()) {
|
if (hasInjectionContext()) {
|
||||||
nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt
|
nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxtAppInstance = nuxtAppInstance || getNuxtAppCtx(appName).tryUse()
|
nuxtAppInstance = nuxtAppInstance || getNuxtAppCtx(id).tryUse()
|
||||||
|
|
||||||
return nuxtAppInstance || null
|
return nuxtAppInstance || null
|
||||||
}
|
}
|
||||||
@ -526,9 +535,9 @@ export function tryUseNuxtApp (appName?: string): NuxtApp | null {
|
|||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
export function useNuxtApp (): NuxtApp
|
export function useNuxtApp (): NuxtApp
|
||||||
export function useNuxtApp (appName?: string): NuxtApp {
|
export function useNuxtApp (id?: string): NuxtApp {
|
||||||
// @ts-expect-error internal usage of appName
|
// @ts-expect-error internal usage of id
|
||||||
const nuxtAppInstance = tryUseNuxtApp(appName)
|
const nuxtAppInstance = tryUseNuxtApp(id)
|
||||||
|
|
||||||
if (!nuxtAppInstance) {
|
if (!nuxtAppInstance) {
|
||||||
if (import.meta.dev) {
|
if (import.meta.dev) {
|
||||||
|
20
packages/nuxt/src/app/types/augments.d.ts
vendored
20
packages/nuxt/src/app/types/augments.d.ts
vendored
@ -51,3 +51,23 @@ declare module 'vue' {
|
|||||||
head?(nuxtApp: NuxtApp): UseHeadInput
|
head?(nuxtApp: NuxtApp): UseHeadInput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
interface App<HostElement> {
|
||||||
|
$nuxt: NuxtApp
|
||||||
|
}
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
$nuxt: NuxtApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-dom' {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
interface App<HostElement> {
|
||||||
|
$nuxt: NuxtApp
|
||||||
|
}
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
$nuxt: NuxtApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -660,7 +660,8 @@ function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandRespons
|
|||||||
const response: NuxtIslandResponse['components'] = {}
|
const response: NuxtIslandResponse['components'] = {}
|
||||||
|
|
||||||
for (const clientUid in ssrContext.islandContext.components) {
|
for (const clientUid in ssrContext.islandContext.components) {
|
||||||
const html = ssrContext.teleports?.[clientUid] || ''
|
// remove teleport anchor to avoid hydration issues
|
||||||
|
const html = ssrContext.teleports?.[clientUid].replaceAll('<!--teleport start anchor-->', '') || ''
|
||||||
response[clientUid] = {
|
response[clientUid] = {
|
||||||
...ssrContext.islandContext.components[clientUid],
|
...ssrContext.islandContext.components[clientUid],
|
||||||
html,
|
html,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork'
|
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork'
|
||||||
import { isAbsolute, join, relative, resolve } from 'pathe'
|
import { join, relative, resolve } from 'pathe'
|
||||||
import type { JSValue } from 'untyped'
|
import type { JSValue } from 'untyped'
|
||||||
import { generateTypes, resolveSchema } from 'untyped'
|
import { generateTypes, resolveSchema } from 'untyped'
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
@ -98,19 +98,36 @@ export const serverPluginTemplate: NuxtTemplate = {
|
|||||||
|
|
||||||
export const pluginsDeclaration: NuxtTemplate = {
|
export const pluginsDeclaration: NuxtTemplate = {
|
||||||
filename: 'types/plugins.d.ts',
|
filename: 'types/plugins.d.ts',
|
||||||
getContents: async (ctx) => {
|
getContents: async ({ nuxt, app }) => {
|
||||||
const EXTENSION_RE = new RegExp(`(?<=\\w)(${ctx.nuxt.options.extensions.map(e => escapeRE(e)).join('|')})$`, 'g')
|
const EXTENSION_RE = new RegExp(`(?<=\\w)(${nuxt.options.extensions.map(e => escapeRE(e)).join('|')})$`, 'g')
|
||||||
|
|
||||||
|
const typesDir = join(nuxt.options.buildDir, 'types')
|
||||||
const tsImports: string[] = []
|
const tsImports: string[] = []
|
||||||
for (const p of ctx.app.plugins) {
|
const pluginNames: string[] = []
|
||||||
const sources = [p.src, p.src.replace(EXTENSION_RE, '.d.ts')]
|
|
||||||
if (!isAbsolute(p.src)) {
|
function exists (path: string) {
|
||||||
tsImports.push(p.src.replace(EXTENSION_RE, ''))
|
return app.templates.some(t => t.write && path === t.dst) || existsSync(path)
|
||||||
} else if (ctx.app.templates.some(t => t.write && t.dst && sources.includes(t.dst)) || sources.some(s => existsSync(s))) {
|
|
||||||
tsImports.push(relative(join(ctx.nuxt.options.buildDir, 'types'), p.src).replace(EXTENSION_RE, ''))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginsName = (await annotatePlugins(ctx.nuxt, ctx.app.plugins)).filter(p => p.name).map(p => `'${p.name}'`)
|
for (const plugin of await annotatePlugins(nuxt, app.plugins)) {
|
||||||
|
if (plugin.name) {
|
||||||
|
pluginNames.push(`'${plugin.name}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginPath = resolve(typesDir, plugin.src)
|
||||||
|
const relativePath = relative(typesDir, pluginPath)
|
||||||
|
|
||||||
|
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts')
|
||||||
|
if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
|
||||||
|
tsImports.push(relativePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists(pluginPath)) {
|
||||||
|
tsImports.push(relativePath.replace(EXTENSION_RE, ''))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return `// Generated by Nuxt'
|
return `// Generated by Nuxt'
|
||||||
import type { Plugin } from '#app'
|
import type { Plugin } from '#app'
|
||||||
@ -126,10 +143,18 @@ declare module '#app' {
|
|||||||
interface NuxtApp extends NuxtAppInjections { }
|
interface NuxtApp extends NuxtAppInjections { }
|
||||||
|
|
||||||
interface NuxtAppLiterals {
|
interface NuxtAppLiterals {
|
||||||
pluginName: ${pluginsName.join(' | ')}
|
pluginName: ${pluginNames.join(' | ')}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
interface ComponentCustomProperties extends NuxtAppInjections { }
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@vue/runtime-dom' {
|
||||||
|
interface ComponentCustomProperties extends NuxtAppInjections { }
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
interface ComponentCustomProperties extends NuxtAppInjections { }
|
interface ComponentCustomProperties extends NuxtAppInjections { }
|
||||||
}
|
}
|
||||||
@ -143,36 +168,74 @@ const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-
|
|||||||
export const schemaTemplate: NuxtTemplate = {
|
export const schemaTemplate: NuxtTemplate = {
|
||||||
filename: 'types/schema.d.ts',
|
filename: 'types/schema.d.ts',
|
||||||
getContents: async ({ nuxt }) => {
|
getContents: async ({ nuxt }) => {
|
||||||
const moduleInfo = nuxt.options._installedModules.map(m => ({
|
|
||||||
...m.meta,
|
|
||||||
importName: m.entryPath || m.meta?.name,
|
|
||||||
})).filter(m => m.configKey && m.name && !adHocModules.includes(m.name))
|
|
||||||
|
|
||||||
const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir)
|
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(/\.\w+$/, '')
|
||||||
const modules = moduleInfo.map(meta => [genString(meta.configKey), getImportName(meta.importName), meta])
|
|
||||||
|
const modules = nuxt.options._installedModules
|
||||||
|
.filter(m => m.meta && m.meta.configKey && m.meta.name && !adHocModules.includes(m.meta.name))
|
||||||
|
.map(m => [genString(m.meta.configKey), getImportName(m.entryPath || m.meta.name), m] as const)
|
||||||
|
|
||||||
const privateRuntimeConfig = Object.create(null)
|
const privateRuntimeConfig = Object.create(null)
|
||||||
for (const key in nuxt.options.runtimeConfig) {
|
for (const key in nuxt.options.runtimeConfig) {
|
||||||
if (key !== 'public') {
|
if (key !== 'public') {
|
||||||
privateRuntimeConfig[key] = nuxt.options.runtimeConfig[key]
|
privateRuntimeConfig[key] = nuxt.options.runtimeConfig[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const moduleOptionsInterface = [
|
|
||||||
...modules.map(([configKey, importName]) =>
|
const moduleOptionsInterface = (jsdocTags: boolean) => [
|
||||||
` [${configKey}]?: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`,
|
...modules.flatMap(([configKey, importName, mod]) => {
|
||||||
),
|
let link: string | undefined
|
||||||
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, meta]) => `[${genString(meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
|
||||||
]
|
// If it's not a local module, provide a link based on its name
|
||||||
|
if (!mod.meta?.rawPath) {
|
||||||
|
link = `https://www.npmjs.com/package/${importName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof mod.meta?.docs === 'string') {
|
||||||
|
link = mod.meta.docs
|
||||||
|
} else if (mod.meta?.repository) {
|
||||||
|
if (typeof mod.meta.repository === 'string') {
|
||||||
|
link = mod.meta.repository
|
||||||
|
} else if (typeof mod.meta.repository.url === 'string') {
|
||||||
|
link = mod.meta.repository.url
|
||||||
|
}
|
||||||
|
if (link) {
|
||||||
|
if (link.startsWith('git+')) {
|
||||||
|
link = link.replace(/^git\+/, '')
|
||||||
|
}
|
||||||
|
if (!link.startsWith('http')) {
|
||||||
|
link = 'https://github.com/' + link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
` /**`,
|
||||||
|
` * Configuration for \`${importName}\``,
|
||||||
|
...jsdocTags && link
|
||||||
|
? [
|
||||||
|
` * @see ${link}`,
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
` */`,
|
||||||
|
` [${configKey}]?: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`,
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, mod]) => `[${genString(mod.meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
|
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
|
||||||
'declare module \'@nuxt/schema\' {',
|
'declare module \'@nuxt/schema\' {',
|
||||||
' interface NuxtConfig {',
|
' interface NuxtConfig {',
|
||||||
moduleOptionsInterface,
|
// TypeScript will duplicate the jsdoc tags if we augment it twice
|
||||||
|
// So here we only generate tags for `nuxt/schema`
|
||||||
|
...moduleOptionsInterface(false),
|
||||||
' }',
|
' }',
|
||||||
'}',
|
'}',
|
||||||
'declare module \'nuxt/schema\' {',
|
'declare module \'nuxt/schema\' {',
|
||||||
' interface NuxtConfig {',
|
' interface NuxtConfig {',
|
||||||
moduleOptionsInterface,
|
...moduleOptionsInterface(true),
|
||||||
' }',
|
' }',
|
||||||
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
|
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export { getNameFromPath, hasSuffix, resolveComponentNameSegments } from './names'
|
export { getNameFromPath, hasSuffix, resolveComponentNameSegments } from './names'
|
||||||
export { isJS, isVue } from './plugins'
|
export { getLoader, isJS, isVue } from './plugins'
|
||||||
|
|
||||||
export function uniqueBy<T, K extends keyof T> (arr: T[], key: K) {
|
export function uniqueBy<T, K extends keyof T> (arr: T[], key: K) {
|
||||||
if (arr.length < 2) {
|
if (arr.length < 2) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { pathToFileURL } from 'node:url'
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { extname } from 'pathe'
|
||||||
import { parseQuery, parseURL } from 'ufo'
|
import { parseQuery, parseURL } from 'ufo'
|
||||||
|
|
||||||
export function isVue (id: string, opts: { type?: Array<'template' | 'script' | 'style'> } = {}) {
|
export function isVue (id: string, opts: { type?: Array<'template' | 'script' | 'style'> } = {}) {
|
||||||
@ -41,3 +42,15 @@ export function isJS (id: string) {
|
|||||||
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||||
return JS_RE.test(pathname)
|
return JS_RE.test(pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLoader (id: string): 'vue' | 'ts' | 'tsx' | null {
|
||||||
|
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||||
|
const ext = extname(pathname)
|
||||||
|
if (ext === '.vue') {
|
||||||
|
return 'vue'
|
||||||
|
}
|
||||||
|
if (!JS_RE.test(ext)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return ext.endsWith('x') ? 'tsx' : 'ts'
|
||||||
|
}
|
||||||
|
@ -117,9 +117,9 @@ const granularAppPresets: InlinePreset[] = [
|
|||||||
|
|
||||||
export const scriptsStubsPreset = {
|
export const scriptsStubsPreset = {
|
||||||
imports: [
|
imports: [
|
||||||
'useConsentScriptTrigger',
|
'useScriptTriggerConsent',
|
||||||
'useAnalyticsPageEvent',
|
'useScriptEventPage',
|
||||||
'useElementScriptTrigger',
|
'useScriptTriggerElement',
|
||||||
'useScript',
|
'useScript',
|
||||||
'useScriptGoogleAnalytics',
|
'useScriptGoogleAnalytics',
|
||||||
'useScriptPlausibleAnalytics',
|
'useScriptPlausibleAnalytics',
|
||||||
|
@ -58,7 +58,7 @@ function _getHashElementScrollMarginTop (selector: string): number {
|
|||||||
try {
|
try {
|
||||||
const elem = document.querySelector(selector)
|
const elem = document.querySelector(selector)
|
||||||
if (elem) {
|
if (elem) {
|
||||||
return Number.parseFloat(getComputedStyle(elem).scrollMarginTop) + Number.parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop)
|
return (Number.parseFloat(getComputedStyle(elem).scrollMarginTop) || 0) + (Number.parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 0)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore any errors parsing scrollMarginTop
|
// ignore any errors parsing scrollMarginTop
|
||||||
|
@ -13,7 +13,7 @@ import { walk } from 'estree-walker'
|
|||||||
import type { CallExpression, ExpressionStatement, ObjectExpression, Program, Property } from 'estree'
|
import type { CallExpression, ExpressionStatement, ObjectExpression, Program, Property } from 'estree'
|
||||||
import type { NuxtPage } from 'nuxt/schema'
|
import type { NuxtPage } from 'nuxt/schema'
|
||||||
|
|
||||||
import { uniqueBy } from '../core/utils'
|
import { getLoader, uniqueBy } from '../core/utils'
|
||||||
import { toArray } from '../utils'
|
import { toArray } from '../utils'
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ enum SegmentParserState {
|
|||||||
dynamic,
|
dynamic,
|
||||||
optional,
|
optional,
|
||||||
catchall,
|
catchall,
|
||||||
|
group,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SegmentTokenType {
|
enum SegmentTokenType {
|
||||||
@ -30,6 +31,7 @@ enum SegmentTokenType {
|
|||||||
dynamic,
|
dynamic,
|
||||||
optional,
|
optional,
|
||||||
catchall,
|
catchall,
|
||||||
|
group,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SegmentToken {
|
interface SegmentToken {
|
||||||
@ -115,7 +117,13 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
|
|||||||
const segment = segments[i]
|
const segment = segments[i]
|
||||||
|
|
||||||
const tokens = parseSegment(segment)
|
const tokens = parseSegment(segment)
|
||||||
const segmentName = tokens.map(({ value }) => value).join('')
|
|
||||||
|
// Skip group segments
|
||||||
|
if (tokens.every(token => token.type === SegmentTokenType.group)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentName = tokens.map(({ value, type }) => type === SegmentTokenType.group ? '' : value).join('')
|
||||||
|
|
||||||
// ex: parent/[slug].vue -> parent-slug
|
// ex: parent/[slug].vue -> parent-slug
|
||||||
route.name += (route.name && '/') + segmentName
|
route.name += (route.name && '/') + segmentName
|
||||||
@ -188,7 +196,8 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
|
|||||||
|
|
||||||
if (absolutePath in metaCache) { return metaCache[absolutePath] }
|
if (absolutePath in metaCache) { return metaCache[absolutePath] }
|
||||||
|
|
||||||
const script = extractScriptContent(contents)
|
const loader = getLoader(absolutePath)
|
||||||
|
const script = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : { code: contents, loader }
|
||||||
if (!script) {
|
if (!script) {
|
||||||
metaCache[absolutePath] = {}
|
metaCache[absolutePath] = {}
|
||||||
return {}
|
return {}
|
||||||
@ -297,7 +306,9 @@ function getRoutePath (tokens: SegmentToken[]): string {
|
|||||||
? `:${token.value}()`
|
? `:${token.value}()`
|
||||||
: token.type === SegmentTokenType.catchall
|
: token.type === SegmentTokenType.catchall
|
||||||
? `:${token.value}(.*)*`
|
? `:${token.value}(.*)*`
|
||||||
: encodePath(token.value).replace(/:/g, '\\:'))
|
: token.type === SegmentTokenType.group
|
||||||
|
? ''
|
||||||
|
: encodePath(token.value).replace(/:/g, '\\:'))
|
||||||
)
|
)
|
||||||
}, '/')
|
}, '/')
|
||||||
}
|
}
|
||||||
@ -327,7 +338,9 @@ function parseSegment (segment: string) {
|
|||||||
? SegmentTokenType.dynamic
|
? SegmentTokenType.dynamic
|
||||||
: state === SegmentParserState.optional
|
: state === SegmentParserState.optional
|
||||||
? SegmentTokenType.optional
|
? SegmentTokenType.optional
|
||||||
: SegmentTokenType.catchall,
|
: state === SegmentParserState.catchall
|
||||||
|
? SegmentTokenType.catchall
|
||||||
|
: SegmentTokenType.group,
|
||||||
value: buffer,
|
value: buffer,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -342,6 +355,8 @@ function parseSegment (segment: string) {
|
|||||||
buffer = ''
|
buffer = ''
|
||||||
if (c === '[') {
|
if (c === '[') {
|
||||||
state = SegmentParserState.dynamic
|
state = SegmentParserState.dynamic
|
||||||
|
} else if (c === '(') {
|
||||||
|
state = SegmentParserState.group
|
||||||
} else {
|
} else {
|
||||||
i--
|
i--
|
||||||
state = SegmentParserState.static
|
state = SegmentParserState.static
|
||||||
@ -352,6 +367,9 @@ function parseSegment (segment: string) {
|
|||||||
if (c === '[') {
|
if (c === '[') {
|
||||||
consumeBuffer()
|
consumeBuffer()
|
||||||
state = SegmentParserState.dynamic
|
state = SegmentParserState.dynamic
|
||||||
|
} else if (c === '(') {
|
||||||
|
consumeBuffer()
|
||||||
|
state = SegmentParserState.group
|
||||||
} else {
|
} else {
|
||||||
buffer += c
|
buffer += c
|
||||||
}
|
}
|
||||||
@ -360,6 +378,7 @@ function parseSegment (segment: string) {
|
|||||||
case SegmentParserState.catchall:
|
case SegmentParserState.catchall:
|
||||||
case SegmentParserState.dynamic:
|
case SegmentParserState.dynamic:
|
||||||
case SegmentParserState.optional:
|
case SegmentParserState.optional:
|
||||||
|
case SegmentParserState.group:
|
||||||
if (buffer === '...') {
|
if (buffer === '...') {
|
||||||
buffer = ''
|
buffer = ''
|
||||||
state = SegmentParserState.catchall
|
state = SegmentParserState.catchall
|
||||||
@ -374,10 +393,16 @@ function parseSegment (segment: string) {
|
|||||||
consumeBuffer()
|
consumeBuffer()
|
||||||
}
|
}
|
||||||
state = SegmentParserState.initial
|
state = SegmentParserState.initial
|
||||||
|
} else if (c === ')' && state === SegmentParserState.group) {
|
||||||
|
if (!buffer) {
|
||||||
|
throw new Error('Empty group')
|
||||||
|
} else {
|
||||||
|
consumeBuffer()
|
||||||
|
}
|
||||||
|
state = SegmentParserState.initial
|
||||||
} else if (PARAM_CHAR_RE.test(c)) {
|
} else if (PARAM_CHAR_RE.test(c)) {
|
||||||
buffer += c
|
buffer += c
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
|
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -339,6 +339,34 @@
|
|||||||
"redirect": "mockMeta?.redirect",
|
"redirect": "mockMeta?.redirect",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"should handle route groups": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "index"",
|
||||||
|
"path": "mockMeta?.path ?? "/"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"alias": "mockMeta?.alias || []",
|
||||||
|
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? "about"",
|
||||||
|
"path": "mockMeta?.path ?? """,
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
|
||||||
|
"meta": "mockMeta || {}",
|
||||||
|
"name": "mockMeta?.name ?? undefined",
|
||||||
|
"path": "mockMeta?.path ?? "/about"",
|
||||||
|
"redirect": "mockMeta?.redirect",
|
||||||
|
},
|
||||||
|
],
|
||||||
"should handle trailing slashes with index routes": [
|
"should handle trailing slashes with index routes": [
|
||||||
{
|
{
|
||||||
"alias": "mockMeta?.alias || []",
|
"alias": "mockMeta?.alias || []",
|
||||||
|
@ -234,6 +234,25 @@
|
|||||||
"path": ""/parent"",
|
"path": ""/parent"",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"should handle route groups": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""index"",
|
||||||
|
"path": ""/"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
|
||||||
|
"name": ""about"",
|
||||||
|
"path": """",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
|
||||||
|
"name": "mockMeta?.name",
|
||||||
|
"path": ""/about"",
|
||||||
|
},
|
||||||
|
],
|
||||||
"should handle trailing slashes with index routes": [
|
"should handle trailing slashes with index routes": [
|
||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
|
@ -200,9 +200,9 @@ describe('imports:nuxt/scripts', () => {
|
|||||||
const scripts = scriptRegistry().map(s => s.import?.name).filter(Boolean)
|
const scripts = scriptRegistry().map(s => s.import?.name).filter(Boolean)
|
||||||
const globalScripts = new Set([
|
const globalScripts = new Set([
|
||||||
'useScript',
|
'useScript',
|
||||||
'useAnalyticsPageEvent',
|
'useScriptEventPage',
|
||||||
'useElementScriptTrigger',
|
'useScriptTriggerElement',
|
||||||
'useConsentScriptTrigger',
|
'useScriptTriggerConsent',
|
||||||
// registered separately
|
// registered separately
|
||||||
'useScriptGoogleTagManager',
|
'useScriptGoogleTagManager',
|
||||||
'useScriptGoogleAnalytics',
|
'useScriptGoogleAnalytics',
|
||||||
|
@ -10,6 +10,16 @@ describe('page metadata', () => {
|
|||||||
expect(await getRouteMeta('<template><div>Hi</div></template>', filePath)).toEqual({})
|
expect(await getRouteMeta('<template><div>Hi</div></template>', filePath)).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should extract metadata from JS/JSX files', async () => {
|
||||||
|
const fileContents = `definePageMeta({ name: 'bar' })`
|
||||||
|
for (const ext of ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs']) {
|
||||||
|
const meta = await getRouteMeta(fileContents, `/app/pages/index.${ext}`)
|
||||||
|
expect(meta).toStrictEqual({
|
||||||
|
name: 'bar',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('should use and invalidate cache', async () => {
|
it('should use and invalidate cache', async () => {
|
||||||
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
|
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
|
||||||
const meta = await getRouteMeta(fileContents, filePath)
|
const meta = await getRouteMeta(fileContents, filePath)
|
||||||
|
@ -601,6 +601,37 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: 'should handle route groups',
|
||||||
|
files: [
|
||||||
|
{ path: `${pagesDir}/(foo)/index.vue` },
|
||||||
|
{ path: `${pagesDir}/(foo)/about.vue` },
|
||||||
|
{ path: `${pagesDir}/(bar)/about/index.vue` },
|
||||||
|
],
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
name: 'index',
|
||||||
|
path: '/',
|
||||||
|
file: `${pagesDir}/(foo)/index.vue`,
|
||||||
|
meta: undefined,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
file: `${pagesDir}/(foo)/about.vue`,
|
||||||
|
meta: undefined,
|
||||||
|
children: [
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'about',
|
||||||
|
path: '',
|
||||||
|
file: `${pagesDir}/(bar)/about/index.vue`,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const normalizedResults: Record<string, any> = {}
|
const normalizedResults: Record<string, any> = {}
|
||||||
|
@ -42,23 +42,23 @@
|
|||||||
"@unhead/schema": "1.9.16",
|
"@unhead/schema": "1.9.16",
|
||||||
"@vitejs/plugin-vue": "5.1.2",
|
"@vitejs/plugin-vue": "5.1.2",
|
||||||
"@vitejs/plugin-vue-jsx": "4.0.0",
|
"@vitejs/plugin-vue-jsx": "4.0.0",
|
||||||
"@vue/compiler-core": "3.4.34",
|
"@vue/compiler-core": "3.4.37",
|
||||||
"@vue/compiler-sfc": "3.4.34",
|
"@vue/compiler-sfc": "3.4.37",
|
||||||
"@vue/language-core": "2.0.29",
|
"@vue/language-core": "2.0.29",
|
||||||
"c12": "2.0.0-beta.1",
|
"c12": "2.0.0-beta.1",
|
||||||
"esbuild-loader": "4.2.2",
|
"esbuild-loader": "4.2.2",
|
||||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||||
"ignore": "5.3.1",
|
"ignore": "5.3.2",
|
||||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||||
"ofetch": "1.3.4",
|
"ofetch": "1.3.4",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"unctx": "2.3.1",
|
"unctx": "2.3.1",
|
||||||
"unenv": "1.10.0",
|
"unenv": "1.10.0",
|
||||||
"vite": "5.3.5",
|
"vite": "5.4.0",
|
||||||
"vue": "3.4.34",
|
"vue": "3.4.37",
|
||||||
"vue-bundle-renderer": "2.1.0",
|
"vue-bundle-renderer": "2.1.0",
|
||||||
"vue-loader": "17.4.2",
|
"vue-loader": "17.4.2",
|
||||||
"vue-router": "4.4.2",
|
"vue-router": "4.4.3",
|
||||||
"webpack": "5.93.0",
|
"webpack": "5.93.0",
|
||||||
"webpack-dev-middleware": "7.3.0"
|
"webpack-dev-middleware": "7.3.0"
|
||||||
},
|
},
|
||||||
|
@ -179,7 +179,9 @@ export default defineUntypedSchema({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For multi-app projects, the unique name of the Nuxt application.
|
* For multi-app projects, the unique id of the Nuxt application.
|
||||||
|
*
|
||||||
|
* Defaults to `nuxt-app`.
|
||||||
*/
|
*/
|
||||||
appId: {
|
appId: {
|
||||||
$resolve: (val: string) => val ?? 'nuxt-app',
|
$resolve: (val: string) => val ?? 'nuxt-app',
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/html-minifier": "4.0.5",
|
"@types/html-minifier": "4.0.5",
|
||||||
"@unocss/reset": "0.61.9",
|
"@unocss/reset": "0.62.1",
|
||||||
"critters": "0.0.24",
|
"critters": "0.0.24",
|
||||||
"execa": "9.3.0",
|
"execa": "9.3.0",
|
||||||
"globby": "14.0.2",
|
"globby": "14.0.2",
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"pathe": "1.1.2",
|
"pathe": "1.1.2",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"scule": "1.3.0",
|
"scule": "1.3.0",
|
||||||
"unocss": "0.61.9",
|
"unocss": "0.62.1",
|
||||||
"vite": "5.3.5"
|
"vite": "5.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
"@types/estree": "1.0.5",
|
"@types/estree": "1.0.5",
|
||||||
"rollup": "4.20.0",
|
"rollup": "4.20.0",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vue": "3.4.34"
|
"vue": "3.4.37"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/kit": "workspace:*",
|
"@nuxt/kit": "workspace:*",
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"clear": "^0.1.0",
|
"clear": "^0.1.0",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"cssnano": "^7.0.4",
|
"cssnano": "^7.0.5",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"esbuild": "^0.23.0",
|
"esbuild": "^0.23.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
@ -55,14 +55,14 @@
|
|||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"perfect-debounce": "^1.0.0",
|
"perfect-debounce": "^1.0.0",
|
||||||
"pkg-types": "^1.1.3",
|
"pkg-types": "^1.1.3",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.41",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"std-env": "^3.7.0",
|
"std-env": "^3.7.0",
|
||||||
"strip-literal": "^2.1.0",
|
"strip-literal": "^2.1.0",
|
||||||
"ufo": "^1.5.4",
|
"ufo": "^1.5.4",
|
||||||
"unenv": "^1.10.0",
|
"unenv": "^1.10.0",
|
||||||
"unplugin": "^1.12.0",
|
"unplugin": "^1.12.1",
|
||||||
"vite": "^5.3.5",
|
"vite": "^5.4.0",
|
||||||
"vite-node": "^2.0.5",
|
"vite-node": "^2.0.5",
|
||||||
"vite-plugin-checker": "^0.7.2",
|
"vite-plugin-checker": "^0.7.2",
|
||||||
"vue-bundle-renderer": "^2.1.0"
|
"vue-bundle-renderer": "^2.1.0"
|
||||||
|
@ -8,12 +8,29 @@ import { normalizeViteManifest } from 'vue-bundle-renderer'
|
|||||||
import type { ViteBuildContext } from './vite'
|
import type { ViteBuildContext } from './vite'
|
||||||
|
|
||||||
export async function writeManifest (ctx: ViteBuildContext) {
|
export async function writeManifest (ctx: ViteBuildContext) {
|
||||||
|
// This is only used for ssr: false - when ssr is enabled we use vite-node runtime manifest
|
||||||
|
const devClientManifest = {
|
||||||
|
'@vite/client': {
|
||||||
|
isEntry: true,
|
||||||
|
file: '@vite/client',
|
||||||
|
css: [],
|
||||||
|
module: true,
|
||||||
|
resourceType: 'script',
|
||||||
|
},
|
||||||
|
[ctx.entry]: {
|
||||||
|
isEntry: true,
|
||||||
|
file: ctx.entry,
|
||||||
|
module: true,
|
||||||
|
resourceType: 'script',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Write client manifest for use in vue-bundle-renderer
|
// Write client manifest for use in vue-bundle-renderer
|
||||||
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
|
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
|
||||||
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
|
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
|
||||||
|
|
||||||
const manifestFile = resolve(clientDist, 'manifest.json')
|
const manifestFile = resolve(clientDist, 'manifest.json')
|
||||||
const clientManifest = JSON.parse(readFileSync(manifestFile, 'utf-8'))
|
const clientManifest = ctx.nuxt.options.dev ? devClientManifest : JSON.parse(readFileSync(manifestFile, 'utf-8'))
|
||||||
|
|
||||||
const buildAssetsDir = withTrailingSlash(withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir))
|
const buildAssetsDir = withTrailingSlash(withoutLeadingSlash(ctx.nuxt.options.app.buildAssetsDir))
|
||||||
const BASE_RE = new RegExp(`^${escapeRE(buildAssetsDir)}`)
|
const BASE_RE = new RegExp(`^${escapeRE(buildAssetsDir)}`)
|
||||||
|
@ -1,77 +1,112 @@
|
|||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { useNitro } from '@nuxt/kit'
|
import { useNitro } from '@nuxt/kit'
|
||||||
import { createUnplugin } from 'unplugin'
|
import { createUnplugin } from 'unplugin'
|
||||||
|
import type { UnpluginOptions } from 'unplugin'
|
||||||
import { withLeadingSlash, withTrailingSlash } from 'ufo'
|
import { withLeadingSlash, withTrailingSlash } from 'ufo'
|
||||||
import { dirname, relative } from 'pathe'
|
import { dirname, relative } from 'pathe'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
|
import { isCSSRequest } from 'vite'
|
||||||
|
|
||||||
const PREFIX = 'virtual:public?'
|
const PREFIX = 'virtual:public?'
|
||||||
const CSS_URL_RE = /url\((\/[^)]+)\)/g
|
const CSS_URL_RE = /url\((\/[^)]+)\)/g
|
||||||
|
const CSS_URL_SINGLE_RE = /url\(\/[^)]+\)/
|
||||||
|
|
||||||
export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boolean }) => {
|
interface VitePublicDirsPluginOptions {
|
||||||
|
dev?: boolean
|
||||||
|
sourcemap?: boolean
|
||||||
|
baseURL?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VitePublicDirsPlugin = createUnplugin((options: VitePublicDirsPluginOptions) => {
|
||||||
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
|
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
|
||||||
|
|
||||||
return {
|
const devTransformPlugin: UnpluginOptions = {
|
||||||
name: 'nuxt:vite-public-dir-resolution',
|
name: 'nuxt:vite-public-dir-resolution-dev',
|
||||||
vite: {
|
vite: {
|
||||||
load: {
|
transform (code, id) {
|
||||||
enforce: 'pre',
|
if (!isCSSRequest(id) || !CSS_URL_SINGLE_RE.test(code)) { return }
|
||||||
handler (id) {
|
|
||||||
if (id.startsWith(PREFIX)) {
|
|
||||||
return `import { publicAssetsURL } from '#internal/nuxt/paths';export default publicAssetsURL(${JSON.stringify(decodeURIComponent(id.slice(PREFIX.length)))})`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolveId: {
|
|
||||||
enforce: 'post',
|
|
||||||
handler (id) {
|
|
||||||
if (id === '/__skip_vite' || id[0] !== '/' || id.startsWith('/@fs')) { return }
|
|
||||||
|
|
||||||
if (resolveFromPublicAssets(id)) {
|
|
||||||
return PREFIX + encodeURIComponent(id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
renderChunk (code, chunk) {
|
|
||||||
if (!chunk.facadeModuleId?.includes('?inline&used')) { return }
|
|
||||||
|
|
||||||
const s = new MagicString(code)
|
const s = new MagicString(code)
|
||||||
const q = code.match(/(?<= = )['"`]/)?.[0] || '"'
|
|
||||||
for (const [full, url] of code.matchAll(CSS_URL_RE)) {
|
for (const [full, url] of code.matchAll(CSS_URL_RE)) {
|
||||||
if (url && resolveFromPublicAssets(url)) {
|
if (url && resolveFromPublicAssets(url)) {
|
||||||
s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`)
|
s.replace(full, `url(${options.baseURL}${url})`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.hasChanged()) {
|
if (s.hasChanged()) {
|
||||||
s.prepend(`import { publicAssetsURL } from '#internal/nuxt/paths';`)
|
|
||||||
return {
|
return {
|
||||||
code: s.toString(),
|
code: s.toString(),
|
||||||
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined,
|
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
generateBundle (_outputOptions, bundle) {
|
|
||||||
for (const file in bundle) {
|
|
||||||
const chunk = bundle[file]!
|
|
||||||
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }
|
|
||||||
|
|
||||||
let css = chunk.source.toString()
|
|
||||||
let wasReplaced = false
|
|
||||||
for (const [full, url] of css.matchAll(CSS_URL_RE)) {
|
|
||||||
if (url && resolveFromPublicAssets(url)) {
|
|
||||||
const relativeURL = relative(withLeadingSlash(dirname(file)), url)
|
|
||||||
css = css.replace(full, `url(${relativeURL})`)
|
|
||||||
wasReplaced = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (wasReplaced) {
|
|
||||||
chunk.source = css
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(options.dev && options.baseURL && options.baseURL !== '/' ? [devTransformPlugin] : []),
|
||||||
|
{
|
||||||
|
name: 'nuxt:vite-public-dir-resolution',
|
||||||
|
vite: {
|
||||||
|
load: {
|
||||||
|
enforce: 'pre',
|
||||||
|
handler (id) {
|
||||||
|
if (id.startsWith(PREFIX)) {
|
||||||
|
return `import { publicAssetsURL } from '#internal/nuxt/paths';export default publicAssetsURL(${JSON.stringify(decodeURIComponent(id.slice(PREFIX.length)))})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolveId: {
|
||||||
|
enforce: 'post',
|
||||||
|
handler (id) {
|
||||||
|
if (id === '/__skip_vite' || id[0] !== '/' || id.startsWith('/@fs')) { return }
|
||||||
|
|
||||||
|
if (resolveFromPublicAssets(id)) {
|
||||||
|
return PREFIX + encodeURIComponent(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
renderChunk (code, chunk) {
|
||||||
|
if (!chunk.facadeModuleId?.includes('?inline&used')) { return }
|
||||||
|
|
||||||
|
const s = new MagicString(code)
|
||||||
|
const q = code.match(/(?<= = )['"`]/)?.[0] || '"'
|
||||||
|
for (const [full, url] of code.matchAll(CSS_URL_RE)) {
|
||||||
|
if (url && resolveFromPublicAssets(url)) {
|
||||||
|
s.replace(full, `url(${q} + publicAssetsURL(${q}${url}${q}) + ${q})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.hasChanged()) {
|
||||||
|
s.prepend(`import { publicAssetsURL } from '#internal/nuxt/paths';`)
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: options.sourcemap ? s.generateMap({ hires: true }) : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generateBundle (_outputOptions, bundle) {
|
||||||
|
for (const file in bundle) {
|
||||||
|
const chunk = bundle[file]!
|
||||||
|
if (!file.endsWith('.css') || chunk.type !== 'asset') { continue }
|
||||||
|
|
||||||
|
let css = chunk.source.toString()
|
||||||
|
let wasReplaced = false
|
||||||
|
for (const [full, url] of css.matchAll(CSS_URL_RE)) {
|
||||||
|
if (url && resolveFromPublicAssets(url)) {
|
||||||
|
const relativeURL = relative(withLeadingSlash(dirname(file)), url)
|
||||||
|
css = css.replace(full, `url(${relativeURL})`)
|
||||||
|
wasReplaced = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wasReplaced) {
|
||||||
|
chunk.source = css
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useResolveFromPublicAssets () {
|
export function useResolveFromPublicAssets () {
|
||||||
|
@ -85,6 +85,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
|||||||
entryFileNames: '[name].mjs',
|
entryFileNames: '[name].mjs',
|
||||||
format: 'module',
|
format: 'module',
|
||||||
generatedCode: {
|
generatedCode: {
|
||||||
|
symbols: true, // temporary fix for https://github.com/vuejs/core/issues/8351,
|
||||||
constBindings: true,
|
constBindings: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -148,6 +149,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.nuxt.options.ssr) {
|
if (!ctx.nuxt.options.ssr) {
|
||||||
|
await writeManifest(ctx)
|
||||||
await onBuild()
|
await onBuild()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
|
|||||||
|
|
||||||
app.use('/module', defineLazyEventHandler(() => {
|
app.use('/module', defineLazyEventHandler(() => {
|
||||||
const viteServer = ctx.ssrServer!
|
const viteServer = ctx.ssrServer!
|
||||||
const node: ViteNodeServer = new ViteNodeServer(viteServer, {
|
const node = new ViteNodeServer(viteServer, {
|
||||||
deps: {
|
deps: {
|
||||||
inline: [
|
inline: [
|
||||||
/\/node_modules\/(.*\/)?(nuxt|nuxt3|nuxt-nightly)\//,
|
/\/node_modules\/(.*\/)?(nuxt|nuxt3|nuxt-nightly)\//,
|
||||||
@ -139,6 +139,7 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
|
|||||||
web: [],
|
web: [],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isExternal = createIsExternal(viteServer, ctx.nuxt.options.rootDir, ctx.nuxt.options.modulesDir)
|
const isExternal = createIsExternal(viteServer, ctx.nuxt.options.rootDir, ctx.nuxt.options.modulesDir)
|
||||||
node.shouldExternalize = async (id: string) => {
|
node.shouldExternalize = async (id: string) => {
|
||||||
const result = await isExternal(id)
|
const result = await isExternal(id)
|
||||||
@ -156,13 +157,17 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
|
|||||||
if (isAbsolute(moduleId) && !isFileServingAllowed(moduleId, viteServer)) {
|
if (isAbsolute(moduleId) && !isFileServingAllowed(moduleId, viteServer)) {
|
||||||
throw createError({ statusCode: 403 /* Restricted */ })
|
throw createError({ statusCode: 403 /* Restricted */ })
|
||||||
}
|
}
|
||||||
const module = await node.fetchModule(moduleId).catch((err) => {
|
const module = await node.fetchModule(moduleId).catch(async (err) => {
|
||||||
const errorData = {
|
const errorData = {
|
||||||
code: 'VITE_ERROR',
|
code: 'VITE_ERROR',
|
||||||
id: moduleId,
|
id: moduleId,
|
||||||
stack: '',
|
stack: '',
|
||||||
...err,
|
...err,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!errorData.frame && errorData.code === 'PARSE_ERROR') {
|
||||||
|
errorData.frame = await node.transformModule(moduleId, 'web').then(({ code }) => `${err.message || ''}\n${code}`).catch(() => undefined)
|
||||||
|
}
|
||||||
throw createError({ data: errorData })
|
throw createError({ data: errorData })
|
||||||
})
|
})
|
||||||
return module
|
return module
|
||||||
|
@ -99,7 +99,11 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// add resolver for files in public assets directories
|
// add resolver for files in public assets directories
|
||||||
VitePublicDirsPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server }),
|
VitePublicDirsPlugin.vite({
|
||||||
|
dev: nuxt.options.dev,
|
||||||
|
sourcemap: !!nuxt.options.sourcemap.server,
|
||||||
|
baseURL: nuxt.options.app.baseURL,
|
||||||
|
}),
|
||||||
composableKeysPlugin.vite({
|
composableKeysPlugin.vite({
|
||||||
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
|
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
|
||||||
rootDir: nuxt.options.rootDir,
|
rootDir: nuxt.options.rootDir,
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
"cssnano": "^7.0.4",
|
"cssnano": "^7.0.5",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"esbuild-loader": "^4.2.2",
|
"esbuild-loader": "^4.2.2",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
@ -50,7 +50,7 @@
|
|||||||
"ohash": "^1.1.3",
|
"ohash": "^1.1.3",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"pify": "^6.1.0",
|
"pify": "^6.1.0",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.41",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-import-resolver": "^2.0.0",
|
"postcss-import-resolver": "^2.0.0",
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.1.1",
|
||||||
@ -60,7 +60,7 @@
|
|||||||
"time-fix-plugin": "^2.0.7",
|
"time-fix-plugin": "^2.0.7",
|
||||||
"ufo": "^1.5.4",
|
"ufo": "^1.5.4",
|
||||||
"unenv": "^1.10.0",
|
"unenv": "^1.10.0",
|
||||||
"unplugin": "^1.12.0",
|
"unplugin": "^1.12.1",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"vue-bundle-renderer": "^2.1.0",
|
"vue-bundle-renderer": "^2.1.0",
|
||||||
"vue-loader": "^17.4.2",
|
"vue-loader": "^17.4.2",
|
||||||
@ -80,7 +80,7 @@
|
|||||||
"@types/webpack-hot-middleware": "2.25.9",
|
"@types/webpack-hot-middleware": "2.25.9",
|
||||||
"rollup": "4.20.0",
|
"rollup": "4.20.0",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vue": "3.4.34"
|
"vue": "3.4.37"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.3.4"
|
"vue": "^3.3.4"
|
||||||
|
1820
pnpm-lock.yaml
1820
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -10,15 +10,6 @@
|
|||||||
"3.x"
|
"3.x"
|
||||||
],
|
],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
|
||||||
"groupName": "vue",
|
|
||||||
"matchPackageNames": [
|
|
||||||
"vue"
|
|
||||||
],
|
|
||||||
"matchPackagePatterns": [
|
|
||||||
"^@vue/"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"groupName": "vitest",
|
"groupName": "vitest",
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
|
@ -392,12 +392,14 @@ describe('pages', () => {
|
|||||||
expect(await page.locator('.client-only-script button').innerHTML()).toContain('2')
|
expect(await page.locator('.client-only-script button').innerHTML()).toContain('2')
|
||||||
expect(await page.locator('.string-stateful-script').innerHTML()).toContain('1')
|
expect(await page.locator('.string-stateful-script').innerHTML()).toContain('1')
|
||||||
expect(await page.locator('.string-stateful').innerHTML()).toContain('1')
|
expect(await page.locator('.string-stateful').innerHTML()).toContain('1')
|
||||||
|
const waitForConsoleLog = page.waitForEvent('console', consoleLog => consoleLog.text() === 'has $el')
|
||||||
|
|
||||||
// ensure directives are reactive
|
// ensure directives are reactive
|
||||||
await page.locator('button#show-all').click()
|
await page.locator('button#show-all').click()
|
||||||
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
||||||
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
|
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
|
||||||
|
|
||||||
|
await waitForConsoleLog
|
||||||
expect(pageErrors).toEqual([])
|
expect(pageErrors).toEqual([])
|
||||||
await page.close()
|
await page.close()
|
||||||
// don't expect any errors or warning on client-side navigation
|
// don't expect any errors or warning on client-side navigation
|
||||||
@ -570,6 +572,14 @@ describe('pages', () => {
|
|||||||
|
|
||||||
await normalInitialPage.close()
|
await normalInitialPage.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('groups routes', async () => {
|
||||||
|
for (const targetRoute of ['/group-page', '/nested-group/group-page', '/nested-group']) {
|
||||||
|
const { status } = await fetch(targetRoute)
|
||||||
|
|
||||||
|
expect(status).toBe(200)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('nuxt composables', () => {
|
describe('nuxt composables', () => {
|
||||||
@ -1865,9 +1875,9 @@ describe('server components/islands', () => {
|
|||||||
const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-component="([^"]*)"/g, (_, content) => `data-island-component="${content.split('-')[0]}"`)
|
const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-component="([^"]*)"/g, (_, content) => `data-island-component="${content.split('-')[0]}"`)
|
||||||
|
|
||||||
if (isWebpack) {
|
if (isWebpack) {
|
||||||
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"')
|
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"')
|
||||||
} else {
|
} else {
|
||||||
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>"')
|
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>"')
|
||||||
}
|
}
|
||||||
expect(text).toContain('async component that was very long')
|
expect(text).toContain('async component that was very long')
|
||||||
|
|
||||||
@ -2126,7 +2136,7 @@ describe('component islands', () => {
|
|||||||
"props": [],
|
"props": [],
|
||||||
},
|
},
|
||||||
"fallback": {
|
"fallback": {
|
||||||
"fallback": "<!--[--><div style="display:contents;"><div>fall slot -- index: 0</div><div class="fallback-slot-content"> wonderful fallback </div></div><div style="display:contents;"><div>back slot -- index: 1</div><div class="fallback-slot-content"> wonderful fallback </div></div><!--]--><!--teleport anchor-->",
|
"fallback": "<!--teleport start anchor--><!--[--><div style="display:contents;"><div>fall slot -- index: 0</div><div class="fallback-slot-content"> wonderful fallback </div></div><div style="display:contents;"><div>back slot -- index: 1</div><div class="fallback-slot-content"> wonderful fallback </div></div><!--]--><!--teleport anchor-->",
|
||||||
"props": [
|
"props": [
|
||||||
{
|
{
|
||||||
"t": "fall",
|
"t": "fall",
|
||||||
@ -2137,7 +2147,7 @@ describe('component islands', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
"hello": {
|
"hello": {
|
||||||
"fallback": "<!--[--><div style="display:contents;"><div> fallback slot -- index: 0</div></div><div style="display:contents;"><div> fallback slot -- index: 1</div></div><div style="display:contents;"><div> fallback slot -- index: 2</div></div><!--]--><!--teleport anchor-->",
|
"fallback": "<!--teleport start anchor--><!--[--><div style="display:contents;"><div> fallback slot -- index: 0</div></div><div style="display:contents;"><div> fallback slot -- index: 1</div></div><div style="display:contents;"><div> fallback slot -- index: 2</div></div><!--]--><!--teleport anchor-->",
|
||||||
"props": [
|
"props": [
|
||||||
{
|
{
|
||||||
"t": 0,
|
"t": 0,
|
||||||
@ -2210,7 +2220,7 @@ describe('component islands', () => {
|
|||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
|
"html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
|
||||||
"slots": {},
|
"slots": {},
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
@ -2221,7 +2231,7 @@ describe('component islands', () => {
|
|||||||
"multiplier": 1,
|
"multiplier": 1,
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
expect(teleportsEntries[0]![1].html).toMatchInlineSnapshot('"<div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--teleport anchor-->"')
|
expect(teleportsEntries[0]![1].html).toMatchInlineSnapshot(`"<div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--teleport anchor-->"`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2603,7 +2613,7 @@ describe('teleports', () => {
|
|||||||
const html = await $fetch<string>('/nuxt-teleport')
|
const html = await $fetch<string>('/nuxt-teleport')
|
||||||
|
|
||||||
// Teleport is appended to body, after the __nuxt div
|
// Teleport is appended to body, after the __nuxt div
|
||||||
expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div></div></div><span id="nuxt-teleport"><div>Nuxt Teleport</div><!--teleport anchor--></span><script')
|
expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div></div></div><span id="nuxt-teleport"><!--teleport start anchor--><div>Nuxt Teleport</div><!--teleport anchor--></span><script')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -32,10 +32,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(rootDir, '.output/server')
|
const serverDir = join(rootDir, '.output/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"205k"`)
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"211k"`)
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1346k"`)
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1348k"`)
|
||||||
|
|
||||||
const packages = modules.files
|
const packages = modules.files
|
||||||
.filter(m => m.endsWith('package.json'))
|
.filter(m => m.endsWith('package.json'))
|
||||||
@ -58,6 +58,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
"db0",
|
"db0",
|
||||||
"devalue",
|
"devalue",
|
||||||
"entities",
|
"entities",
|
||||||
|
"entities/dist/commonjs",
|
||||||
"estree-walker",
|
"estree-walker",
|
||||||
"hookable",
|
"hookable",
|
||||||
"source-map-js",
|
"source-map-js",
|
||||||
@ -73,7 +74,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(rootDir, '.output-inline/server')
|
const serverDir = join(rootDir, '.output-inline/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"528k"`)
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"535k"`)
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"80.3k"`)
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"80.3k"`)
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
class="interactive-component-wrapper"
|
class="interactive-component-wrapper"
|
||||||
style="border: solid 1px red;"
|
style="border: solid 1px red;"
|
||||||
>
|
>
|
||||||
The component bellow is not a slot but declared as interactive
|
The component below is not a slot but declared as interactive
|
||||||
|
|
||||||
<Counter
|
<Counter
|
||||||
nuxt-client
|
nuxt-client
|
||||||
|
16
test/fixtures/basic/components/WrapClientComponent.vue
vendored
Normal file
16
test/fixtures/basic/components/WrapClientComponent.vue
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ClientSetupScript
|
||||||
|
ref="clientSetupScript"
|
||||||
|
class="client-only-script-setup"
|
||||||
|
foo="hello"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const clientSetupScript = ref<{ $el: HTMLElement }>()
|
||||||
|
onMounted(() => {
|
||||||
|
console.log(clientSetupScript.value?.$el as HTMLElement ? 'has $el' : 'no $el')
|
||||||
|
})
|
||||||
|
</script>
|
5
test/fixtures/basic/pages/(new-group)/group-page.vue
vendored
Normal file
5
test/fixtures/basic/pages/(new-group)/group-page.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Hello from new group
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -59,6 +59,7 @@
|
|||||||
class="no-state-hidden"
|
class="no-state-hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<WrapClientComponent v-if="show" />
|
||||||
<button
|
<button
|
||||||
class="test-ref-1"
|
class="test-ref-1"
|
||||||
@click="stringStatefulComp.add"
|
@click="stringStatefulComp.add"
|
||||||
@ -94,16 +95,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Ref } from 'vue'
|
|
||||||
// bypass client import protection to ensure this is treeshaken from .client components
|
// bypass client import protection to ensure this is treeshaken from .client components
|
||||||
import BreaksServer from '~~/components/BreaksServer.client'
|
import BreaksServer from '~~/components/BreaksServer.client'
|
||||||
|
|
||||||
type Comp = Ref<{ add: () => void }>
|
type Comp = { add: () => void }
|
||||||
|
const stringStatefulComp = ref<Comp>(null)
|
||||||
const stringStatefulComp = ref(null) as any as Comp
|
const stringStatefulScriptComp = ref<Comp>(null)
|
||||||
const stringStatefulScriptComp = ref(null) as any as Comp
|
const clientScript = ref<Comp>(null)
|
||||||
const clientScript = ref(null) as any as Comp
|
const clientSetupScript = ref<Comp>(null)
|
||||||
const clientSetupScript = ref(null) as any as Comp
|
|
||||||
const BreakServerComponent = defineAsyncComponent(() => {
|
const BreakServerComponent = defineAsyncComponent(() => {
|
||||||
return import('./../components/BreaksServer.client')
|
return import('./../components/BreaksServer.client')
|
||||||
})
|
})
|
||||||
|
2
test/fixtures/basic/pages/client.vue
vendored
2
test/fixtures/basic/pages/client.vue
vendored
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// explicit import to bypass client import protection
|
// explicit import to bypass client import protection
|
||||||
import BreaksServer from '../components/BreaksServer.client'
|
import BreaksServer from '../components/BreaksServer.client'
|
||||||
// ensure treeshake-client-only module remove theses imports without breaking
|
// ensure treeshake-client-only module remove these imports without breaking
|
||||||
import TestGlobal from '../components/global/TestGlobal.vue'
|
import TestGlobal from '../components/global/TestGlobal.vue'
|
||||||
// direct import of .client components should be treeshaken
|
// direct import of .client components should be treeshaken
|
||||||
import { FunctionalComponent, LazyClientOnlyScript } from '#components'
|
import { FunctionalComponent, LazyClientOnlyScript } from '#components'
|
||||||
|
5
test/fixtures/basic/pages/nested-group/(deep-group)/group-page.vue
vendored
Normal file
5
test/fixtures/basic/pages/nested-group/(deep-group)/group-page.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Page deep in group
|
||||||
|
</div>
|
||||||
|
</template>
|
5
test/fixtures/basic/pages/nested-group/(index-group)/index.vue
vendored
Normal file
5
test/fixtures/basic/pages/nested-group/(index-group)/index.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Index page of a group
|
||||||
|
</div>
|
||||||
|
</template>
|
5
test/fixtures/basic/pages/nested-group/more-nested/(more-deep)/group-page.vue
vendored
Normal file
5
test/fixtures/basic/pages/nested-group/more-nested/(more-deep)/group-page.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Page deep, deep in group
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { defineEventHandler } from 'h3'
|
import { defineEventHandler } from 'h3'
|
||||||
|
import { destr } from 'destr'
|
||||||
|
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||||
@ -691,6 +692,38 @@ describe('useCookie', () => {
|
|||||||
expect(computedVal.value).toBe(0)
|
expect(computedVal.value).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('cookie decode function should be invoked once', () => {
|
||||||
|
// Pre-set cookies
|
||||||
|
document.cookie = 'foo=Foo'
|
||||||
|
document.cookie = 'bar=%7B%22s2%22%3A0%7D'
|
||||||
|
document.cookie = 'baz=%7B%22s2%22%3A0%7D'
|
||||||
|
|
||||||
|
let barCallCount = 0
|
||||||
|
const bazCookie = useCookie<{ s2: number }>('baz', {
|
||||||
|
default: () => ({ s2: -1 }),
|
||||||
|
decode (value) {
|
||||||
|
barCallCount++
|
||||||
|
return destr(decodeURIComponent(value))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
bazCookie.value.s2++
|
||||||
|
expect(bazCookie.value.s2).toEqual(1)
|
||||||
|
expect(barCallCount).toBe(1)
|
||||||
|
|
||||||
|
let quxCallCount = 0
|
||||||
|
const quxCookie = useCookie<{ s3: number }>('qux', {
|
||||||
|
default: () => ({ s3: -1 }),
|
||||||
|
filter: key => key === 'bar' || key === 'baz',
|
||||||
|
decode (value) {
|
||||||
|
quxCallCount++
|
||||||
|
return destr(decodeURIComponent(value))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
quxCookie.value.s3++
|
||||||
|
expect(quxCookie.value.s3).toBe(0)
|
||||||
|
expect(quxCallCount).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
it('should not watch custom cookie refs when shallow', () => {
|
it('should not watch custom cookie refs when shallow', () => {
|
||||||
for (const value of ['shallow', false] as const) {
|
for (const value of ['shallow', false] as const) {
|
||||||
const user = useCookie('shallowUserInfo', {
|
const user = useCookie('shallowUserInfo', {
|
||||||
|
Loading…
Reference in New Issue
Block a user