mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +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
|
||||
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
retention-days: 3
|
||||
name: dist
|
||||
@ -85,7 +85,7 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||
uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||
with:
|
||||
languages: javascript
|
||||
queries: +security-and-quality
|
||||
@ -97,7 +97,7 @@ jobs:
|
||||
path: packages
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||
uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||
with:
|
||||
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
|
||||
# format to the repository Actions tab.
|
||||
- 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()
|
||||
with:
|
||||
name: SARIF file
|
||||
@ -68,7 +68,7 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||
uses: github/codeql-action/upload-sarif@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
|
||||
if: github.repository == 'nuxt/nuxt' && success()
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
@ -161,7 +161,13 @@ export default defineVitestConfig({
|
||||
|
||||
#### `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
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
The passed in component will be rendered inside a `<div id="test-wrapper"></div>`.
|
||||
@ -266,7 +273,9 @@ mockNuxtImport('useStorage', () => {
|
||||
// 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.
|
||||
|
||||
|
@ -12,19 +12,19 @@ To enable type-checking at build or development time, install `vue-tsc` and `typ
|
||||
::code-group
|
||||
|
||||
```bash [yarn]
|
||||
yarn add --dev vue-tsc@^1 typescript
|
||||
yarn add --dev vue-tsc typescript
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install --save-dev vue-tsc@^1 typescript
|
||||
npm install --save-dev vue-tsc typescript
|
||||
```
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add -D vue-tsc@^1 typescript
|
||||
pnpm add -D vue-tsc typescript
|
||||
```
|
||||
|
||||
```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
|
||||
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
|
||||
@ -226,6 +226,22 @@ definePageMeta({
|
||||
|
||||
: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
|
||||
|
||||
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.
|
||||
|
||||
:read-more{to="/docs/guide/going-further/custom-routing"}
|
||||
:read-more{to="/docs/guide/recipes/custom-routing"}
|
||||
|
||||
## 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
|
||||
|
||||
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).
|
||||
|
||||
```ts [app/router.options.ts]
|
||||
import type { RouterConfig } from '@nuxt/schema'
|
||||
|
||||
export default <RouterConfig> {
|
||||
export default {
|
||||
// https://router.vuejs.org/api/interfaces/routeroptions.html#routes
|
||||
routes: (_routes) => [
|
||||
{
|
||||
@ -25,7 +25,7 @@ export default <RouterConfig> {
|
||||
component: () => import('~/pages/home.vue').then(r => r.default || r)
|
||||
}
|
||||
],
|
||||
}
|
||||
} satisfies RouterConfig
|
||||
```
|
||||
|
||||
::note
|
||||
@ -90,8 +90,8 @@ This is the recommended way to specify [router options](/docs/api/nuxt-config#ro
|
||||
```ts [app/router.options.ts]
|
||||
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.
|
||||
@ -174,8 +174,8 @@ You can optionally override history mode using a function that accepts the base
|
||||
import type { RouterConfig } from '@nuxt/schema'
|
||||
import { createMemoryHistory } from 'vue-router'
|
||||
|
||||
export default <RouterConfig> {
|
||||
export default {
|
||||
// https://router.vuejs.org/api/interfaces/routeroptions.html
|
||||
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
|
||||
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
|
||||
@ -14,7 +14,7 @@ The `to` target of [`<Teleport>`](https://vuejs.org/guide/built-ins/teleport.htm
|
||||
<button @click="open = true">
|
||||
Open Modal
|
||||
</button>
|
||||
<Teleport to="body">
|
||||
<Teleport to="#teleports">
|
||||
<div v-if="open" class="modal">
|
||||
<p>Hello from the modal!</p>
|
||||
<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.
|
||||
::
|
||||
|
||||
### 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
|
||||
|
||||
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`
|
||||
|
||||
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]`**
|
||||
|
||||
|
@ -209,6 +209,7 @@ type NuxtMiddleware = {
|
||||
|
||||
interface AddRouteMiddlewareOptions {
|
||||
override?: boolean
|
||||
prepend?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
@ -246,7 +247,21 @@ A middleware object or an array of middleware objects with the following propert
|
||||
|
||||
**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
|
||||
|
||||
@ -272,7 +287,7 @@ export default defineNuxtModule({
|
||||
name: 'auth',
|
||||
path: resolver.resolve('runtime/auth.ts'),
|
||||
global: true
|
||||
})
|
||||
}, { prepend: true })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
@ -103,7 +103,7 @@ This feature is not yet supported in Nuxt 3.
|
||||
|
||||
## `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.
|
||||
|
||||
```diff [pages/index.vue]
|
||||
|
29
package.json
29
package.json
@ -31,7 +31,7 @@
|
||||
"test:types": "pnpm --filter './test/fixtures/**' test:types",
|
||||
"test:unit": "vitest run packages/",
|
||||
"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": {
|
||||
"@nuxt/kit": "workspace:*",
|
||||
@ -39,7 +39,7 @@
|
||||
"@nuxt/ui-templates": "workspace:*",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@types/node": "*",
|
||||
"@types/node": "20.14.15",
|
||||
"c12": "2.0.0-beta.1",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"jiti": "2.0.0-beta.3",
|
||||
@ -49,18 +49,18 @@
|
||||
"rollup": "^4.20.0",
|
||||
"typescript": "5.5.4",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vite": "5.3.5",
|
||||
"vue": "3.4.34"
|
||||
"vite": "5.4.0",
|
||||
"vue": "3.4.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.8.0",
|
||||
"@eslint/js": "9.9.0",
|
||||
"@nuxt/eslint-config": "0.5.0",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/test-utils": "3.14.0",
|
||||
"@nuxt/test-utils": "3.14.1",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@types/node": "20.14.14",
|
||||
"@types/node": "20.14.15",
|
||||
"@types/semver": "7.5.8",
|
||||
"@unhead/schema": "1.9.16",
|
||||
"@vitejs/plugin-vue": "5.1.2",
|
||||
@ -70,11 +70,12 @@
|
||||
"case-police": "0.6.1",
|
||||
"changelogen": "0.5.5",
|
||||
"consola": "3.2.3",
|
||||
"cssnano": "7.0.4",
|
||||
"cssnano": "7.0.5",
|
||||
"destr": "2.0.3",
|
||||
"devalue": "5.0.0",
|
||||
"eslint": "9.8.0",
|
||||
"eslint": "9.9.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",
|
||||
"execa": "9.3.0",
|
||||
"globby": "14.0.2",
|
||||
@ -88,7 +89,7 @@
|
||||
"nuxt-content-twoslash": "0.1.1",
|
||||
"ofetch": "1.3.4",
|
||||
"pathe": "1.1.2",
|
||||
"playwright-core": "1.45.3",
|
||||
"playwright-core": "1.46.0",
|
||||
"rimraf": "6.0.1",
|
||||
"semver": "7.6.3",
|
||||
"std-env": "3.7.0",
|
||||
@ -96,11 +97,11 @@
|
||||
"ufo": "1.5.4",
|
||||
"vitest": "2.0.5",
|
||||
"vitest-environment-nuxt": "1.0.0",
|
||||
"vue": "3.4.34",
|
||||
"vue-router": "4.4.2",
|
||||
"vue": "3.4.37",
|
||||
"vue-router": "4.4.3",
|
||||
"vue-tsc": "2.0.29"
|
||||
},
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"packageManager": "pnpm@9.7.0",
|
||||
"engines": {
|
||||
"node": "^16.10.0 || >=18.0.0"
|
||||
},
|
||||
|
@ -34,7 +34,7 @@
|
||||
"errx": "^0.1.0",
|
||||
"globby": "^14.0.2",
|
||||
"hash-sum": "^2.0.0",
|
||||
"ignore": "^5.3.1",
|
||||
"ignore": "^5.3.2",
|
||||
"jiti": "^2.0.0-beta.3",
|
||||
"klona": "^2.0.6",
|
||||
"mlly": "^1.7.1",
|
||||
@ -52,7 +52,7 @@
|
||||
"@types/semver": "7.5.8",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vite": "5.3.5",
|
||||
"vite": "5.4.0",
|
||||
"vitest": "2.0.5",
|
||||
"webpack": "5.93.0"
|
||||
},
|
||||
|
@ -35,6 +35,11 @@ export interface AddRouteMiddlewareOptions {
|
||||
* @default false
|
||||
*/
|
||||
override?: boolean
|
||||
/**
|
||||
* Prepend middleware to the list
|
||||
* @default false
|
||||
*/
|
||||
prepend?: boolean
|
||||
}
|
||||
|
||||
export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], options: AddRouteMiddlewareOptions = {}) {
|
||||
@ -51,6 +56,8 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
|
||||
} else {
|
||||
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 {
|
||||
app.middleware.push({ ...middleware })
|
||||
}
|
||||
|
@ -68,7 +68,7 @@
|
||||
"@unhead/dom": "^1.9.16",
|
||||
"@unhead/ssr": "^1.9.16",
|
||||
"@unhead/vue": "^1.9.16",
|
||||
"@vue/shared": "^3.4.34",
|
||||
"@vue/shared": "^3.4.37",
|
||||
"acorn": "8.12.1",
|
||||
"c12": "^2.0.0-beta.1",
|
||||
"chokidar": "^3.6.0",
|
||||
@ -85,7 +85,7 @@
|
||||
"globby": "^14.0.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"hookable": "^5.5.3",
|
||||
"ignore": "^5.3.1",
|
||||
"ignore": "^5.3.2",
|
||||
"jiti": "^2.0.0-beta.3",
|
||||
"klona": "^2.0.6",
|
||||
"knitwork": "^1.1.0",
|
||||
@ -110,24 +110,24 @@
|
||||
"unctx": "^2.3.1",
|
||||
"unenv": "^1.10.0",
|
||||
"unimport": "^3.10.0",
|
||||
"unplugin": "^1.12.0",
|
||||
"unplugin-vue-router": "^0.10.2",
|
||||
"unplugin": "^1.12.1",
|
||||
"unplugin-vue-router": "^0.10.3",
|
||||
"unstorage": "^1.10.2",
|
||||
"untyped": "^1.4.2",
|
||||
"vue": "^3.4.34",
|
||||
"vue": "^3.4.37",
|
||||
"vue-bundle-renderer": "^2.1.0",
|
||||
"vue-devtools-stub": "^0.1.0",
|
||||
"vue-router": "^4.4.2"
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/scripts": "0.6.5",
|
||||
"@nuxt/scripts": "0.6.6",
|
||||
"@nuxt/ui-templates": "1.3.4",
|
||||
"@parcel/watcher": "2.4.1",
|
||||
"@types/estree": "1.0.5",
|
||||
"@vitejs/plugin-vue": "5.1.2",
|
||||
"@vue/compiler-sfc": "3.4.34",
|
||||
"@vue/compiler-sfc": "3.4.37",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vite": "5.3.5",
|
||||
"vite": "5.4.0",
|
||||
"vitest": "2.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
|
||||
import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue'
|
||||
import { isPromise } from '@vue/shared'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { getFragmentHTML } from './utils'
|
||||
|
||||
@ -42,9 +43,10 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
||||
const clone = { ...component }
|
||||
|
||||
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) => {
|
||||
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)
|
||||
return (res.children === null || typeof res.children === 'string')
|
||||
? cloneVNode(res)
|
||||
@ -63,33 +65,39 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
|
||||
}
|
||||
|
||||
clone.setup = (props, ctx) => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const mounted$ = ref(import.meta.client && nuxtApp.isHydrating === false)
|
||||
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
|
||||
const directives = extractDirectives(instance)
|
||||
// prevent attrs inheritance since a staticVNode is rendered before hydration
|
||||
for (const key in attrs) {
|
||||
delete instance.attrs[key]
|
||||
onMounted(() => {
|
||||
Object.assign(instance.attrs, attrs)
|
||||
instance.vnode.dirs = directives
|
||||
})
|
||||
}
|
||||
const mounted$ = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
Object.assign(instance.attrs, attrs)
|
||||
instance.vnode.dirs = directives
|
||||
mounted$.value = true
|
||||
})
|
||||
const setupState = component.setup?.(props, ctx) || {}
|
||||
|
||||
return Promise.resolve(component.setup?.(props, ctx) || {})
|
||||
.then((setupState) => {
|
||||
if (isPromise(setupState)) {
|
||||
return Promise.resolve(setupState).then((setupState) => {
|
||||
if (typeof setupState !== 'function') {
|
||||
setupState = setupState || {}
|
||||
setupState.mounted$ = mounted$
|
||||
return setupState
|
||||
}
|
||||
return (...args: any[]) => {
|
||||
if (mounted$.value) {
|
||||
if (import.meta.client && (mounted$.value || !nuxtApp.isHydrating)) {
|
||||
const res = setupState(...args)
|
||||
return (res.children === null || typeof res.children === 'string')
|
||||
? 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)
|
||||
|
@ -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>): CookieRef<T> {
|
||||
const opts = { ...CookieDefaults, ..._opts }
|
||||
opts.filter ??= key => key === name
|
||||
const cookies = readRawCookies(opts) || {}
|
||||
|
||||
let delay: number | undefined
|
||||
|
@ -152,7 +152,7 @@ export function useFetch<
|
||||
let controller: AbortController
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
@ -164,7 +164,7 @@ export function useFetch<
|
||||
const timeoutLength = toValue(opts.timeout)
|
||||
let timeoutId: NodeJS.Timeout
|
||||
if (timeoutLength) {
|
||||
timeoutId = setTimeout(() => controller.abort(), timeoutLength)
|
||||
timeoutId = setTimeout(() => controller.abort('Request aborted due to timeout.'), timeoutLength)
|
||||
controller.signal.onabort = () => clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ const SEPARATOR = '-'
|
||||
|
||||
/**
|
||||
* 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 (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.')
|
||||
}
|
||||
|
||||
nuxtApp._id ||= 0
|
||||
nuxtApp._genId ||= 0
|
||||
instance._nuxtIdIndex ||= {}
|
||||
instance._nuxtIdIndex[key] ||= 0
|
||||
|
||||
@ -32,7 +34,7 @@ export function useId (key?: string): string {
|
||||
|
||||
if (import.meta.server) {
|
||||
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)
|
||||
return ids[instanceIndex]
|
||||
}
|
||||
@ -54,5 +56,5 @@ export function useId (key?: string): string {
|
||||
}
|
||||
|
||||
// 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 { parse } from 'devalue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { getCurrentInstance, onServerPrefetch } from 'vue'
|
||||
import { getCurrentInstance, onServerPrefetch, reactive } from 'vue'
|
||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||
import type { NuxtPayload } from '../nuxt'
|
||||
|
||||
@ -122,6 +122,10 @@ export async function getNuxtClientPayload () {
|
||||
...window.__NUXT__,
|
||||
}
|
||||
|
||||
if (payloadCache!.config?.public) {
|
||||
payloadCache!.config.public = reactive(payloadCache!.config.public)
|
||||
}
|
||||
|
||||
return payloadCache
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,31 @@ interface Preview {
|
||||
_initialized?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring preview mode.
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* 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
|
||||
/**
|
||||
* 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
|
||||
@ -54,9 +76,10 @@ export function usePreviewMode<S extends EnteredState> (options: PreviewModeOpti
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
export function useElementScriptTrigger (...args: unknown[]) {
|
||||
renderStubMessage('useElementScriptTrigger')
|
||||
export function useScriptTriggerElement (...args: unknown[]) {
|
||||
renderStubMessage('useScriptTriggerElement')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function useConsentScriptTrigger (...args: unknown[]) {
|
||||
renderStubMessage('useConsentScriptTrigger')
|
||||
export function useScriptTriggerConsent (...args: unknown[]) {
|
||||
renderStubMessage('useScriptTriggerConsent')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function useAnalyticsPageEvent (...args: unknown[]) {
|
||||
renderStubMessage('useAnalyticsPageEvent')
|
||||
export function useScriptEventPage (...args: unknown[]) {
|
||||
renderStubMessage('useScriptEventPage')
|
||||
}
|
||||
|
||||
// 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'
|
||||
|
||||
function getNuxtAppCtx (appName = appId || 'nuxt-app') {
|
||||
return getContext<NuxtApp>(appName, {
|
||||
function getNuxtAppCtx (id = appId || 'nuxt-app') {
|
||||
return getContext<NuxtApp>(id, {
|
||||
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
|
||||
})
|
||||
}
|
||||
@ -98,8 +98,6 @@ export interface NuxtPayload {
|
||||
}
|
||||
|
||||
interface _NuxtApp {
|
||||
/** @internal */
|
||||
_name: string
|
||||
vueApp: App<Element>
|
||||
versions: Record<string, string>
|
||||
|
||||
@ -113,8 +111,15 @@ interface _NuxtApp {
|
||||
|
||||
/** @internal */
|
||||
_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 */
|
||||
_scope: EffectScope
|
||||
/** @internal */
|
||||
@ -244,13 +249,17 @@ export type ObjectPluginInput<Injections extends Record<string, unknown> = Recor
|
||||
export interface CreateOptions {
|
||||
vueApp: NuxtApp['vueApp']
|
||||
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 */
|
||||
export function createNuxtApp (options: CreateOptions) {
|
||||
let hydratingCount = 0
|
||||
const nuxtApp: NuxtApp = {
|
||||
_name: appId || 'nuxt-app',
|
||||
_id: options.id || appId || 'nuxt-app',
|
||||
_scope: effectScope(),
|
||||
provide: undefined,
|
||||
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>) {
|
||||
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) {
|
||||
return nuxt.vueApp.runWithContext(() => nuxtAppCtx.callAsync(nuxt as NuxtApp, fn))
|
||||
} else {
|
||||
@ -507,13 +516,13 @@ export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp |
|
||||
* @since 3.10.0
|
||||
*/
|
||||
export function tryUseNuxtApp (): NuxtApp | null
|
||||
export function tryUseNuxtApp (appName?: string): NuxtApp | null {
|
||||
export function tryUseNuxtApp (id?: string): NuxtApp | null {
|
||||
let nuxtAppInstance
|
||||
if (hasInjectionContext()) {
|
||||
nuxtAppInstance = getCurrentInstance()?.appContext.app.$nuxt
|
||||
}
|
||||
|
||||
nuxtAppInstance = nuxtAppInstance || getNuxtAppCtx(appName).tryUse()
|
||||
nuxtAppInstance = nuxtAppInstance || getNuxtAppCtx(id).tryUse()
|
||||
|
||||
return nuxtAppInstance || null
|
||||
}
|
||||
@ -526,9 +535,9 @@ export function tryUseNuxtApp (appName?: string): NuxtApp | null {
|
||||
* @since 3.0.0
|
||||
*/
|
||||
export function useNuxtApp (): NuxtApp
|
||||
export function useNuxtApp (appName?: string): NuxtApp {
|
||||
// @ts-expect-error internal usage of appName
|
||||
const nuxtAppInstance = tryUseNuxtApp(appName)
|
||||
export function useNuxtApp (id?: string): NuxtApp {
|
||||
// @ts-expect-error internal usage of id
|
||||
const nuxtAppInstance = tryUseNuxtApp(id)
|
||||
|
||||
if (!nuxtAppInstance) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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'] = {}
|
||||
|
||||
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] = {
|
||||
...ssrContext.islandContext.components[clientUid],
|
||||
html,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
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 { generateTypes, resolveSchema } from 'untyped'
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
@ -98,19 +98,36 @@ export const serverPluginTemplate: NuxtTemplate = {
|
||||
|
||||
export const pluginsDeclaration: NuxtTemplate = {
|
||||
filename: 'types/plugins.d.ts',
|
||||
getContents: async (ctx) => {
|
||||
const EXTENSION_RE = new RegExp(`(?<=\\w)(${ctx.nuxt.options.extensions.map(e => escapeRE(e)).join('|')})$`, 'g')
|
||||
getContents: async ({ nuxt, app }) => {
|
||||
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[] = []
|
||||
for (const p of ctx.app.plugins) {
|
||||
const sources = [p.src, p.src.replace(EXTENSION_RE, '.d.ts')]
|
||||
if (!isAbsolute(p.src)) {
|
||||
tsImports.push(p.src.replace(EXTENSION_RE, ''))
|
||||
} 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 pluginNames: string[] = []
|
||||
|
||||
function exists (path: string) {
|
||||
return app.templates.some(t => t.write && path === t.dst) || existsSync(path)
|
||||
}
|
||||
|
||||
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'
|
||||
import type { Plugin } from '#app'
|
||||
@ -126,10 +143,18 @@ declare module '#app' {
|
||||
interface NuxtApp extends NuxtAppInjections { }
|
||||
|
||||
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' {
|
||||
interface ComponentCustomProperties extends NuxtAppInjections { }
|
||||
}
|
||||
@ -143,36 +168,74 @@ const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-
|
||||
export const schemaTemplate: NuxtTemplate = {
|
||||
filename: 'types/schema.d.ts',
|
||||
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 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)
|
||||
for (const key in nuxt.options.runtimeConfig) {
|
||||
if (key !== 'public') {
|
||||
privateRuntimeConfig[key] = nuxt.options.runtimeConfig[key]
|
||||
}
|
||||
}
|
||||
const moduleOptionsInterface = [
|
||||
...modules.map(([configKey, importName]) =>
|
||||
` [${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, meta]) => `[${genString(meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
||||
]
|
||||
|
||||
const moduleOptionsInterface = (jsdocTags: boolean) => [
|
||||
...modules.flatMap(([configKey, importName, mod]) => {
|
||||
let link: string | undefined
|
||||
|
||||
// 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 [
|
||||
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
|
||||
'declare module \'@nuxt/schema\' {',
|
||||
' 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\' {',
|
||||
' interface NuxtConfig {',
|
||||
moduleOptionsInterface,
|
||||
...moduleOptionsInterface(true),
|
||||
' }',
|
||||
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
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) {
|
||||
if (arr.length < 2) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { extname } from 'pathe'
|
||||
import { parseQuery, parseURL } from 'ufo'
|
||||
|
||||
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))
|
||||
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 = {
|
||||
imports: [
|
||||
'useConsentScriptTrigger',
|
||||
'useAnalyticsPageEvent',
|
||||
'useElementScriptTrigger',
|
||||
'useScriptTriggerConsent',
|
||||
'useScriptEventPage',
|
||||
'useScriptTriggerElement',
|
||||
'useScript',
|
||||
'useScriptGoogleAnalytics',
|
||||
'useScriptPlausibleAnalytics',
|
||||
|
@ -58,7 +58,7 @@ function _getHashElementScrollMarginTop (selector: string): number {
|
||||
try {
|
||||
const elem = document.querySelector(selector)
|
||||
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 {
|
||||
// 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 { NuxtPage } from 'nuxt/schema'
|
||||
|
||||
import { uniqueBy } from '../core/utils'
|
||||
import { getLoader, uniqueBy } from '../core/utils'
|
||||
import { toArray } from '../utils'
|
||||
import { distDir } from '../dirs'
|
||||
|
||||
@ -23,6 +23,7 @@ enum SegmentParserState {
|
||||
dynamic,
|
||||
optional,
|
||||
catchall,
|
||||
group,
|
||||
}
|
||||
|
||||
enum SegmentTokenType {
|
||||
@ -30,6 +31,7 @@ enum SegmentTokenType {
|
||||
dynamic,
|
||||
optional,
|
||||
catchall,
|
||||
group,
|
||||
}
|
||||
|
||||
interface SegmentToken {
|
||||
@ -115,7 +117,13 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
|
||||
const segment = segments[i]
|
||||
|
||||
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
|
||||
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] }
|
||||
|
||||
const script = extractScriptContent(contents)
|
||||
const loader = getLoader(absolutePath)
|
||||
const script = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : { code: contents, loader }
|
||||
if (!script) {
|
||||
metaCache[absolutePath] = {}
|
||||
return {}
|
||||
@ -297,7 +306,9 @@ function getRoutePath (tokens: SegmentToken[]): string {
|
||||
? `:${token.value}()`
|
||||
: token.type === SegmentTokenType.catchall
|
||||
? `:${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
|
||||
: state === SegmentParserState.optional
|
||||
? SegmentTokenType.optional
|
||||
: SegmentTokenType.catchall,
|
||||
: state === SegmentParserState.catchall
|
||||
? SegmentTokenType.catchall
|
||||
: SegmentTokenType.group,
|
||||
value: buffer,
|
||||
})
|
||||
|
||||
@ -342,6 +355,8 @@ function parseSegment (segment: string) {
|
||||
buffer = ''
|
||||
if (c === '[') {
|
||||
state = SegmentParserState.dynamic
|
||||
} else if (c === '(') {
|
||||
state = SegmentParserState.group
|
||||
} else {
|
||||
i--
|
||||
state = SegmentParserState.static
|
||||
@ -352,6 +367,9 @@ function parseSegment (segment: string) {
|
||||
if (c === '[') {
|
||||
consumeBuffer()
|
||||
state = SegmentParserState.dynamic
|
||||
} else if (c === '(') {
|
||||
consumeBuffer()
|
||||
state = SegmentParserState.group
|
||||
} else {
|
||||
buffer += c
|
||||
}
|
||||
@ -360,6 +378,7 @@ function parseSegment (segment: string) {
|
||||
case SegmentParserState.catchall:
|
||||
case SegmentParserState.dynamic:
|
||||
case SegmentParserState.optional:
|
||||
case SegmentParserState.group:
|
||||
if (buffer === '...') {
|
||||
buffer = ''
|
||||
state = SegmentParserState.catchall
|
||||
@ -374,10 +393,16 @@ function parseSegment (segment: string) {
|
||||
consumeBuffer()
|
||||
}
|
||||
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)) {
|
||||
buffer += c
|
||||
} else {
|
||||
|
||||
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
|
||||
}
|
||||
break
|
||||
|
@ -339,6 +339,34 @@
|
||||
"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": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
|
@ -234,6 +234,25 @@
|
||||
"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": [
|
||||
{
|
||||
"children": [
|
||||
|
@ -200,9 +200,9 @@ describe('imports:nuxt/scripts', () => {
|
||||
const scripts = scriptRegistry().map(s => s.import?.name).filter(Boolean)
|
||||
const globalScripts = new Set([
|
||||
'useScript',
|
||||
'useAnalyticsPageEvent',
|
||||
'useElementScriptTrigger',
|
||||
'useConsentScriptTrigger',
|
||||
'useScriptEventPage',
|
||||
'useScriptTriggerElement',
|
||||
'useScriptTriggerConsent',
|
||||
// registered separately
|
||||
'useScriptGoogleTagManager',
|
||||
'useScriptGoogleAnalytics',
|
||||
|
@ -10,6 +10,16 @@ describe('page metadata', () => {
|
||||
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 () => {
|
||||
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
|
||||
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> = {}
|
||||
|
@ -42,23 +42,23 @@
|
||||
"@unhead/schema": "1.9.16",
|
||||
"@vitejs/plugin-vue": "5.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "4.0.0",
|
||||
"@vue/compiler-core": "3.4.34",
|
||||
"@vue/compiler-sfc": "3.4.34",
|
||||
"@vue/compiler-core": "3.4.37",
|
||||
"@vue/compiler-sfc": "3.4.37",
|
||||
"@vue/language-core": "2.0.29",
|
||||
"c12": "2.0.0-beta.1",
|
||||
"esbuild-loader": "4.2.2",
|
||||
"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",
|
||||
"ofetch": "1.3.4",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"unctx": "2.3.1",
|
||||
"unenv": "1.10.0",
|
||||
"vite": "5.3.5",
|
||||
"vue": "3.4.34",
|
||||
"vite": "5.4.0",
|
||||
"vue": "3.4.37",
|
||||
"vue-bundle-renderer": "2.1.0",
|
||||
"vue-loader": "17.4.2",
|
||||
"vue-router": "4.4.2",
|
||||
"vue-router": "4.4.3",
|
||||
"webpack": "5.93.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: {
|
||||
$resolve: (val: string) => val ?? 'nuxt-app',
|
||||
|
@ -19,7 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-minifier": "4.0.5",
|
||||
"@unocss/reset": "0.61.9",
|
||||
"@unocss/reset": "0.62.1",
|
||||
"critters": "0.0.24",
|
||||
"execa": "9.3.0",
|
||||
"globby": "14.0.2",
|
||||
@ -30,7 +30,7 @@
|
||||
"pathe": "1.1.2",
|
||||
"prettier": "3.3.3",
|
||||
"scule": "1.3.0",
|
||||
"unocss": "0.61.9",
|
||||
"vite": "5.3.5"
|
||||
"unocss": "0.62.1",
|
||||
"vite": "5.4.0"
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
"@types/estree": "1.0.5",
|
||||
"rollup": "4.20.0",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vue": "3.4.34"
|
||||
"vue": "3.4.37"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "workspace:*",
|
||||
@ -39,7 +39,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"clear": "^0.1.0",
|
||||
"consola": "^3.2.3",
|
||||
"cssnano": "^7.0.4",
|
||||
"cssnano": "^7.0.5",
|
||||
"defu": "^6.1.4",
|
||||
"esbuild": "^0.23.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
@ -55,14 +55,14 @@
|
||||
"pathe": "^1.1.2",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^1.1.3",
|
||||
"postcss": "^8.4.40",
|
||||
"postcss": "^8.4.41",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"std-env": "^3.7.0",
|
||||
"strip-literal": "^2.1.0",
|
||||
"ufo": "^1.5.4",
|
||||
"unenv": "^1.10.0",
|
||||
"unplugin": "^1.12.0",
|
||||
"vite": "^5.3.5",
|
||||
"unplugin": "^1.12.1",
|
||||
"vite": "^5.4.0",
|
||||
"vite-node": "^2.0.5",
|
||||
"vite-plugin-checker": "^0.7.2",
|
||||
"vue-bundle-renderer": "^2.1.0"
|
||||
|
@ -8,12 +8,29 @@ import { normalizeViteManifest } from 'vue-bundle-renderer'
|
||||
import type { ViteBuildContext } from './vite'
|
||||
|
||||
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
|
||||
const clientDist = resolve(ctx.nuxt.options.buildDir, 'dist/client')
|
||||
const serverDist = resolve(ctx.nuxt.options.buildDir, 'dist/server')
|
||||
|
||||
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 BASE_RE = new RegExp(`^${escapeRE(buildAssetsDir)}`)
|
||||
|
@ -1,77 +1,112 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { useNitro } from '@nuxt/kit'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import type { UnpluginOptions } from 'unplugin'
|
||||
import { withLeadingSlash, withTrailingSlash } from 'ufo'
|
||||
import { dirname, relative } from 'pathe'
|
||||
import MagicString from 'magic-string'
|
||||
import { isCSSRequest } from 'vite'
|
||||
|
||||
const PREFIX = 'virtual:public?'
|
||||
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()
|
||||
|
||||
return {
|
||||
name: 'nuxt:vite-public-dir-resolution',
|
||||
const devTransformPlugin: UnpluginOptions = {
|
||||
name: 'nuxt:vite-public-dir-resolution-dev',
|
||||
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 }
|
||||
transform (code, id) {
|
||||
if (!isCSSRequest(id) || !CSS_URL_SINGLE_RE.test(code)) { 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})`)
|
||||
s.replace(full, `url(${options.baseURL}${url})`)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 () {
|
||||
|
@ -85,6 +85,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
||||
entryFileNames: '[name].mjs',
|
||||
format: 'module',
|
||||
generatedCode: {
|
||||
symbols: true, // temporary fix for https://github.com/vuejs/core/issues/8351,
|
||||
constBindings: true,
|
||||
},
|
||||
},
|
||||
@ -148,6 +149,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
||||
}
|
||||
|
||||
if (!ctx.nuxt.options.ssr) {
|
||||
await writeManifest(ctx)
|
||||
await onBuild()
|
||||
return
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
|
||||
|
||||
app.use('/module', defineLazyEventHandler(() => {
|
||||
const viteServer = ctx.ssrServer!
|
||||
const node: ViteNodeServer = new ViteNodeServer(viteServer, {
|
||||
const node = new ViteNodeServer(viteServer, {
|
||||
deps: {
|
||||
inline: [
|
||||
/\/node_modules\/(.*\/)?(nuxt|nuxt3|nuxt-nightly)\//,
|
||||
@ -139,6 +139,7 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
|
||||
web: [],
|
||||
},
|
||||
})
|
||||
|
||||
const isExternal = createIsExternal(viteServer, ctx.nuxt.options.rootDir, ctx.nuxt.options.modulesDir)
|
||||
node.shouldExternalize = async (id: string) => {
|
||||
const result = await isExternal(id)
|
||||
@ -156,13 +157,17 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
|
||||
if (isAbsolute(moduleId) && !isFileServingAllowed(moduleId, viteServer)) {
|
||||
throw createError({ statusCode: 403 /* Restricted */ })
|
||||
}
|
||||
const module = await node.fetchModule(moduleId).catch((err) => {
|
||||
const module = await node.fetchModule(moduleId).catch(async (err) => {
|
||||
const errorData = {
|
||||
code: 'VITE_ERROR',
|
||||
id: moduleId,
|
||||
stack: '',
|
||||
...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 })
|
||||
})
|
||||
return module
|
||||
|
@ -99,7 +99,11 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
},
|
||||
plugins: [
|
||||
// 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({
|
||||
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
|
||||
rootDir: nuxt.options.rootDir,
|
||||
|
@ -30,7 +30,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
"cssnano": "^7.0.4",
|
||||
"cssnano": "^7.0.5",
|
||||
"defu": "^6.1.4",
|
||||
"esbuild-loader": "^4.2.2",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
@ -50,7 +50,7 @@
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.2",
|
||||
"pify": "^6.1.0",
|
||||
"postcss": "^8.4.40",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-import-resolver": "^2.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
@ -60,7 +60,7 @@
|
||||
"time-fix-plugin": "^2.0.7",
|
||||
"ufo": "^1.5.4",
|
||||
"unenv": "^1.10.0",
|
||||
"unplugin": "^1.12.0",
|
||||
"unplugin": "^1.12.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-bundle-renderer": "^2.1.0",
|
||||
"vue-loader": "^17.4.2",
|
||||
@ -80,7 +80,7 @@
|
||||
"@types/webpack-hot-middleware": "2.25.9",
|
||||
"rollup": "4.20.0",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vue": "3.4.34"
|
||||
"vue": "3.4.37"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"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"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "vue",
|
||||
"matchPackageNames": [
|
||||
"vue"
|
||||
],
|
||||
"matchPackagePatterns": [
|
||||
"^@vue/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "vitest",
|
||||
"matchPackageNames": [
|
||||
|
@ -392,12 +392,14 @@ describe('pages', () => {
|
||||
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').innerHTML()).toContain('1')
|
||||
const waitForConsoleLog = page.waitForEvent('console', consoleLog => consoleLog.text() === 'has $el')
|
||||
|
||||
// ensure directives are reactive
|
||||
await page.locator('button#show-all').click()
|
||||
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
|
||||
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
|
||||
|
||||
await waitForConsoleLog
|
||||
expect(pageErrors).toEqual([])
|
||||
await page.close()
|
||||
// don't expect any errors or warning on client-side navigation
|
||||
@ -570,6 +572,14 @@ describe('pages', () => {
|
||||
|
||||
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', () => {
|
||||
@ -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]}"`)
|
||||
|
||||
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 {
|
||||
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')
|
||||
|
||||
@ -2126,7 +2136,7 @@ describe('component islands', () => {
|
||||
"props": [],
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"t": "fall",
|
||||
@ -2137,7 +2147,7 @@ describe('component islands', () => {
|
||||
],
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"t": 0,
|
||||
@ -2210,7 +2220,7 @@ describe('component islands', () => {
|
||||
"link": [],
|
||||
"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": {},
|
||||
}
|
||||
`)
|
||||
@ -2221,7 +2231,7 @@ describe('component islands', () => {
|
||||
"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')
|
||||
|
||||
// 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 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)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1346k"`)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1348k"`)
|
||||
|
||||
const packages = modules.files
|
||||
.filter(m => m.endsWith('package.json'))
|
||||
@ -58,6 +58,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
"db0",
|
||||
"devalue",
|
||||
"entities",
|
||||
"entities/dist/commonjs",
|
||||
"estree-walker",
|
||||
"hookable",
|
||||
"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 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)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"80.3k"`)
|
||||
|
@ -13,7 +13,7 @@
|
||||
class="interactive-component-wrapper"
|
||||
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
|
||||
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"
|
||||
/>
|
||||
|
||||
<WrapClientComponent v-if="show" />
|
||||
<button
|
||||
class="test-ref-1"
|
||||
@click="stringStatefulComp.add"
|
||||
@ -94,16 +95,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue'
|
||||
// bypass client import protection to ensure this is treeshaken from .client components
|
||||
import BreaksServer from '~~/components/BreaksServer.client'
|
||||
|
||||
type Comp = Ref<{ add: () => void }>
|
||||
|
||||
const stringStatefulComp = ref(null) as any as Comp
|
||||
const stringStatefulScriptComp = ref(null) as any as Comp
|
||||
const clientScript = ref(null) as any as Comp
|
||||
const clientSetupScript = ref(null) as any as Comp
|
||||
type Comp = { add: () => void }
|
||||
const stringStatefulComp = ref<Comp>(null)
|
||||
const stringStatefulScriptComp = ref<Comp>(null)
|
||||
const clientScript = ref<Comp>(null)
|
||||
const clientSetupScript = ref<Comp>(null)
|
||||
const BreakServerComponent = defineAsyncComponent(() => {
|
||||
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">
|
||||
// explicit import to bypass client import protection
|
||||
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'
|
||||
// direct import of .client components should be treeshaken
|
||||
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 { defineEventHandler } from 'h3'
|
||||
import { destr } from 'destr'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
@ -691,6 +692,38 @@ describe('useCookie', () => {
|
||||
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', () => {
|
||||
for (const value of ['shallow', false] as const) {
|
||||
const user = useCookie('shallowUserInfo', {
|
||||
|
Loading…
Reference in New Issue
Block a user