Merge branch 'main' into feat/islands-use-cache

This commit is contained in:
Julien Huang 2024-07-28 22:56:02 +02:00 committed by GitHub
commit 0861bdb7ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1707 additions and 1575 deletions

View File

@ -33,4 +33,4 @@ jobs:
- name: Lint (docs)
run: pnpm lint:docs:fix
- uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c

View File

@ -52,4 +52,4 @@ jobs:
- name: Lint (code)
run: pnpm lint:fix
- uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c

View File

@ -85,7 +85,7 @@ jobs:
run: pnpm install
- name: Initialize CodeQL
uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
uses: github/codeql-action/init@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14
with:
languages: javascript
queries: +security-and-quality
@ -97,7 +97,7 @@ jobs:
path: packages
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
uses: github/codeql-action/analyze@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14
with:
category: "/language:javascript"

View 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@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
uses: github/codeql-action/upload-sarif@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif

View File

@ -97,6 +97,24 @@ Breaking or significant changes will be noted here along with migration steps fo
This section is subject to change until the final release, so please check back here regularly if you are testing Nuxt 4 using `compatibilityVersion: 4`.
::
#### Migrating Using Codemods
To facilitate the upgrade process, we have collaborated with the [Codemod](https://github.com/codemod-com/codemod) team to automate many migration steps with some open-source codemods.
::note
If you encounter any issues, please report them to the Codemod team with `npx codemod feedback` 🙏
::
For a complete list of Nuxt 4 codemods, detailed information on each, their source, and various ways to run them, visit the [Codemod Registry](https://go.codemod.com/codemod-registry).
You can run all the codemods mentioned in this guide using the following `codemod` recipe:
```bash
npx codemod@latest nuxt/4/migration-recipe
```
This command will execute all codemods in sequence, with the option to deselect any that you do not wish to run. Each codemod is also listed below alongside its respective change and can be executed independently.
#### New Directory Structure
🚦 **Impact Level**: Significant
@ -161,6 +179,10 @@ nuxt.config.ts
1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`, `app.config.ts`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same.
1. Make sure your `nuxt.config.ts`, `content/`, `layers/`, `modules/`, `public/` and `server/` folders remain outside the `app/` folder, in the root of your project.
::tip
You can automate this migration by running `npx codemod@latest nuxt/4/file-structure`
::
However, migration is _not required_. If you wish to keep your current folder structure, Nuxt should auto-detect it. (If it does not, please raise an issue.) The one exception is that if you _already_ have a custom `srcDir`. In this case, you should be aware that your `modules/`, `public/` and `server/` folders will be resolved from your `rootDir` rather than from your custom `srcDir`. You can override this by configuring `dir.modules`, `dir.public` and `serverDir` if you need to.
You can also force a v3 folder structure with the following configuration:
@ -231,6 +253,12 @@ Previously `data` was initialized to `null` but reset in `clearNuxtData` to `und
##### Migration Steps
If you were checking if `data.value` or `error.value` were `null`, you can update these checks to check for `undefined` instead.
::tip
You can automate this step by running `npx codemod@latest nuxt/4/default-data-error-value`
::
If you encounter any issues you can revert back to the previous behavior with:
```ts twoslash [nuxt.config.ts]
@ -290,6 +318,10 @@ The migration should be straightforward:
}
```
::tip
You can automate this step by running `npx codemod@latest nuxt/4/deprecated-dedupe-value`
::
#### Respect defaults when clearing `data` in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal
@ -353,6 +385,10 @@ In most cases, no migration steps are required, but if you rely on the reactivit
})
```
::tip
If you need to, you can automate this step by running `npx codemod@latest nuxt/4/shallow-function-reactivity`
::
#### Absolute Watch Paths in `builder:watch`
🚦 **Impact Level**: Minimal
@ -380,6 +416,10 @@ However, if you are a module author using the `builder:watch` hook and wishing t
})
```
::tip
You can automate this step by running `npx codemod@latest nuxt/4/absolute-watch-path`
::
#### Removal of `window.__NUXT__` object
##### What Changed
@ -487,6 +527,10 @@ const importSources = (sources: string | string[], { lazy = false } = {}) => {
const importName = genSafeVariableName
```
::tip
You can automate this step by running `npx codemod@latest nuxt/4/template-compilation-changes`
::
#### Removal of Experimental Features
🚦 **Impact Level**: Minimal

View File

@ -466,13 +466,14 @@ Be very careful before proxying headers to an external API and just include head
If you want to pass on/proxy cookies in the other direction, from an internal request back to the client, you will need to handle this yourself.
```ts [composables/fetch.ts]
import { appendResponseHeader, H3Event } from 'h3'
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
/* Get the response from the server endpoint */
const res = await $fetch.raw(url)
/* Get the cookies from the response */
const cookies = (res.headers.get('set-cookie') || '').split(',')
const cookies = res.headers.getSetCookie()
/* Attach each cookie to our incoming Request */
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)

View File

@ -20,7 +20,9 @@ One of the core features of Nuxt is the layers and extending support. You can ex
By default, any layers within your project in the `~/layers` directory will be automatically registered as layers in your project
::note Layer auto-registration was introduced in Nuxt v3.12.0 ::
::note
Layer auto-registration was introduced in Nuxt v3.12.0
::
In addition, you can extend from a layer by adding the [extends](/docs/api/nuxt-config#extends) property to your [`nuxt.config`](/docs/guide/directory-structure/nuxt-config) file.

View File

@ -51,7 +51,7 @@ Sets the message with `politeness = "assertive"`
## Example
```ts
```vue [pages/index.vue]
<script setup lang="ts">
const { message, politeness, set, polite, assertive } = useRouteAnnouncer({
politeness: 'assertive'

View File

@ -3,7 +3,6 @@ import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
// @ts-expect-error missing types
import noOnlyTests from 'eslint-plugin-no-only-tests'
import typegen from 'eslint-typegen'
// @ts-expect-error missing types
import perfectionist from 'eslint-plugin-perfectionist'
export default createConfigForNuxt({
@ -189,6 +188,7 @@ export default createConfigForNuxt({
},
},
// Sort rule keys in eslint config
// @ts-expect-error incorrect types 🤔
{
files: ['**/eslint.config.mjs'],
name: 'local/sort-eslint-config',

View File

@ -45,11 +45,11 @@
"magic-string": "^0.30.10",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxt": "workspace:*",
"rollup": "^4.18.1",
"typescript": "5.5.3",
"unbuild": "3.0.0-rc.6",
"vite": "5.3.4",
"vue": "3.4.31"
"rollup": "^4.19.0",
"typescript": "5.5.4",
"unbuild": "3.0.0-rc.7",
"vite": "5.3.5",
"vue": "3.4.34"
},
"devDependencies": {
"@eslint/js": "9.7.0",
@ -59,11 +59,11 @@
"@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3",
"@types/node": "20.14.10",
"@types/node": "20.14.12",
"@types/semver": "7.5.8",
"@unhead/schema": "1.9.16",
"@vitejs/plugin-vue": "5.0.5",
"@vitest/coverage-v8": "2.0.3",
"@vitejs/plugin-vue": "5.1.0",
"@vitest/coverage-v8": "2.0.4",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.19",
"case-police": "0.6.1",
@ -73,7 +73,7 @@
"devalue": "5.0.0",
"eslint": "9.7.0",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-perfectionist": "2.11.0",
"eslint-plugin-perfectionist": "3.0.0",
"eslint-typegen": "0.2.4",
"execa": "9.3.0",
"globby": "14.0.2",
@ -87,19 +87,19 @@
"nuxt-content-twoslash": "0.1.0",
"ofetch": "1.3.4",
"pathe": "1.1.2",
"playwright-core": "1.45.2",
"playwright-core": "1.45.3",
"rimraf": "6.0.1",
"semver": "7.6.2",
"semver": "7.6.3",
"std-env": "3.7.0",
"typescript": "5.5.3",
"ufo": "1.5.3",
"vitest": "2.0.3",
"typescript": "5.5.4",
"ufo": "1.5.4",
"vitest": "2.0.4",
"vitest-environment-nuxt": "1.0.0",
"vue": "3.4.31",
"vue": "3.4.34",
"vue-router": "4.4.0",
"vue-tsc": "2.0.26"
"vue-tsc": "2.0.29"
},
"packageManager": "pnpm@9.5.0",
"packageManager": "pnpm@9.6.0",
"engines": {
"node": "^16.10.0 || >=18.0.0"
},

View File

@ -31,6 +31,7 @@
"consola": "^3.2.3",
"defu": "^6.1.4",
"destr": "^2.0.3",
"errx": "^0.1.0",
"globby": "^14.0.2",
"hash-sum": "^2.0.0",
"ignore": "^5.3.1",
@ -40,19 +41,19 @@
"pathe": "^1.1.2",
"pkg-types": "^1.1.3",
"scule": "^1.3.0",
"semver": "^7.6.2",
"ufo": "^1.5.3",
"semver": "^7.6.3",
"ufo": "^1.5.4",
"unctx": "^2.3.1",
"unimport": "^3.8.0",
"unimport": "^3.9.0",
"untyped": "^1.4.2"
},
"devDependencies": {
"@types/hash-sum": "1.0.2",
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"unbuild": "3.0.0-rc.6",
"vite": "5.3.4",
"vitest": "2.0.3",
"unbuild": "3.0.0-rc.7",
"vite": "5.3.5",
"vitest": "2.0.4",
"webpack": "5.93.0"
},
"engines": {

View File

@ -1,6 +1,7 @@
import { pathToFileURL } from 'node:url'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { interopDefault, resolvePath, resolvePathSync } from 'mlly'
import { createJiti } from 'jiti'
import { captureStackTrace } from 'errx'
export interface ResolveModuleOptions {
paths?: string | string[]
@ -48,10 +49,12 @@ const warnings = new Set<string>()
* @deprecated Please use `importModule` instead.
*/
export function requireModule<T = unknown> (id: string, opts?: ImportModuleOptions) {
if (!warnings.has(id)) {
// TODO: add more information on stack trace
console.warn('[@nuxt/kit] `requireModule` is deprecated. Please use `importModule` instead.')
warnings.add(id)
const { source, line, column } = captureStackTrace().find(entry => entry.source !== import.meta.url) ?? {}
const explanation = source ? ` (used at \`${fileURLToPath(source)}:${line}:${column}\`)` : ''
const warning = `[@nuxt/kit] \`requireModule\` is deprecated${explanation}. Please use \`importModule\` instead.`
if (!warnings.has(warning)) {
console.warn(warning)
warnings.add(warning)
}
const resolvedPath = resolveModule(id, opts)
const jiti = createJiti(import.meta.url, {

View File

@ -158,6 +158,8 @@ export async function _generateTypes (nuxt: Nuxt) {
const relative = relativeWithDot(nuxt.options.buildDir, path)
include.add(join(relative, 'runtime'))
exclude.add(join(relative, 'runtime/server'))
include.add(join(relative, 'dist/runtime'))
exclude.add(join(relative, 'dist/runtime/server'))
}
const isV4 = nuxt.options.future?.compatibilityVersion === 4

View File

@ -12,7 +12,8 @@ declare global {
interface Window {
cookieStore?: {
onchange: (event: any) => void
addEventListener: (type: 'change', listener: (event: any) => void) => void
removeEventListener: (type: 'change', listener: (event: any) => void) => void
}
}
}

View File

@ -68,16 +68,17 @@
"@unhead/dom": "^1.9.16",
"@unhead/ssr": "^1.9.16",
"@unhead/vue": "^1.9.16",
"@vue/shared": "^3.4.31",
"@vue/shared": "^3.4.34",
"acorn": "8.12.1",
"c12": "^2.0.0-beta.1",
"chokidar": "^3.6.0",
"compatx": "^0.1.8",
"consola": "^3.2.3",
"cookie-es": "^1.1.0",
"cookie-es": "^1.2.2",
"defu": "^6.1.4",
"destr": "^2.0.3",
"devalue": "^5.0.0",
"errx": "^0.1.0",
"esbuild": "^0.23.0",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
@ -100,20 +101,20 @@
"pkg-types": "^1.1.3",
"radix3": "^1.1.2",
"scule": "^1.3.0",
"semver": "^7.6.2",
"semver": "^7.6.3",
"std-env": "^3.7.0",
"strip-literal": "^2.1.0",
"ufo": "^1.5.3",
"ufo": "^1.5.4",
"ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3",
"unctx": "^2.3.1",
"unenv": "^1.9.0",
"unimport": "^3.8.0",
"unplugin": "^1.11.0",
"unplugin-vue-router": "^0.10.0",
"unenv": "^1.10.0",
"unimport": "^3.9.0",
"unplugin": "^1.12.0",
"unplugin-vue-router": "^0.10.1",
"unstorage": "^1.10.2",
"untyped": "^1.4.2",
"vue": "^3.4.31",
"vue": "^3.4.34",
"vue-bundle-renderer": "^2.1.0",
"vue-devtools-stub": "^0.1.0",
"vue-router": "^4.4.0"
@ -123,11 +124,11 @@
"@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5",
"@vitejs/plugin-vue": "5.0.5",
"@vue/compiler-sfc": "3.4.31",
"unbuild": "3.0.0-rc.6",
"vite": "5.3.4",
"vitest": "2.0.3"
"@vitejs/plugin-vue": "5.1.0",
"@vue/compiler-sfc": "3.4.34",
"unbuild": "3.0.0-rc.7",
"vite": "5.3.5",
"vitest": "2.0.4"
},
"peerDependencies": {
"@parcel/watcher": "^2.1.0",

View File

@ -60,10 +60,10 @@ export interface AsyncDataOptions<
default?: () => DefaultT | Ref<DefaultT>
/**
* Provide a function which returns cached data.
* A `null` or `undefined` return value will trigger a fetch.
* An `undefined` return value will trigger a fetch.
* Default is `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]` which only caches data when payloadExtraction is enabled.
*/
getCachedData?: (key: string, nuxtApp: NuxtApp) => NoInfer<DataT>
getCachedData?: (key: string, nuxtApp: NuxtApp) => NoInfer<DataT> | undefined
/**
* A function that can be used to alter handler function result after resolving.
* Do not use it along with the `pick` option.
@ -174,7 +174,7 @@ export function useAsyncData<
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
* @param key A unique key to ensure that data fetching can be properly de-duplicated across requests.
* @param handler An asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side.
* @param handler An asynchronous function that must return a value (it should not be `undefined`) or the request may be duplicated on the client side.
* @param options customize the behavior of useAsyncData
*/
export function useAsyncData<

View File

@ -83,13 +83,16 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
const handleChange = (data: { value?: any, refresh?: boolean }) => {
const value = data.refresh ? readRawCookies(opts)?.[name] : opts.decode(data.value)
watchPaused = true
cookies[name] = cookie.value = value
cookie.value = value
cookies[name] = klona(value)
nextTick(() => { watchPaused = false })
}
let watchPaused = false
if (getCurrentScope()) {
const hasScope = !!getCurrentScope()
if (hasScope) {
onScopeDispose(() => {
watchPaused = true
callback()
@ -98,10 +101,14 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
}
if (store) {
store.onchange = (event) => {
const changeHandler = (event: any) => {
const cookie = event.changed.find((c: any) => c.name === name)
if (cookie) { handleChange({ value: cookie.value }) }
}
store.addEventListener('change', changeHandler)
if (hasScope) {
onScopeDispose(() => store.removeEventListener('change', changeHandler))
}
} else if (channel) {
channel.onmessage = ({ data }) => handleChange(data)
}
@ -119,6 +126,16 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
const nuxtApp = useNuxtApp()
const writeFinalCookieValue = () => {
if (opts.readonly || isEqual(cookie.value, cookies[name])) { return }
nuxtApp._cookies ||= {}
if (name in nuxtApp._cookies) {
// do not append a second `set-cookie` header
if (isEqual(cookie.value, nuxtApp._cookies[name])) { return }
// warn in dev mode
if (import.meta.dev) {
console.warn(`[nuxt] cookie \`${name}\` was previously set to \`${opts.encode(nuxtApp._cookies[name] as any)}\` and is being overridden to \`${opts.encode(cookie.value as any)}\`. This may cause unexpected issues.`)
}
}
nuxtApp._cookies[name] = cookie.value
writeServerCookie(useRequestEvent(nuxtApp)!, name, cookie.value, opts as CookieOptions<any>)
}
const unhook = nuxtApp.hooks.hookOnce('app:rendered', writeFinalCookieValue)

View File

@ -45,6 +45,10 @@ export function useScriptCloudflareWebAnalytics (...args: unknown[]) {
renderStubMessage('useScriptCloudflareWebAnalytics')
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function useScriptCrisp (...args: unknown[]) {
renderStubMessage('useScriptCrisp')
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function useScriptFathomAnalytics (...args: unknown[]) {
renderStubMessage('useScriptFathomAnalytics')
}

View File

@ -112,6 +112,8 @@ interface _NuxtApp {
[key: string]: unknown
/** @internal */
_cookies?: Record<string, unknown>
/** @internal */
_id?: number
/** @internal */

View File

@ -1,6 +1,7 @@
import { createConsola } from 'consola'
import type { LogObject } from 'consola'
import { parse } from 'devalue'
import type { ParsedTrace } from 'errx'
import { h } from 'vue'
import { defineNuxtPlugin } from '../nuxt'
@ -45,15 +46,24 @@ export default defineNuxtPlugin(async (nuxtApp) => {
}
})
function normalizeFilenames (stack?: string) {
stack = stack?.split('\n')[0] || ''
stack = stack.replace(`${devRootDir}/`, '')
stack = stack.replace(/:\d+:\d+\)?$/, '')
return stack
function normalizeFilenames (stack?: ParsedTrace[]) {
if (!stack) {
return ''
}
let message = ''
for (const item of stack) {
const source = item.source.replace(`${devRootDir}/`, '')
if (item.function) {
message += ` at ${item.function} (${source})\n`
} else {
message += ` at ${source}\n`
}
}
return message
}
function normalizeServerLog (log: LogObject) {
log.additional = normalizeFilenames(log.stack as string)
log.additional = normalizeFilenames(log.stack as ParsedTrace[])
log.tag = 'ssr'
delete log.stack
return log

View File

@ -4,7 +4,13 @@ import ClientOnly from '#app/components/client-only'
/* @__NO_SIDE_EFFECTS__ */
export const createClientPage = (loader: AsyncComponentLoader) => {
const page = defineAsyncComponent(loader)
const page = defineAsyncComponent(import.meta.dev
? () => loader().then((m) => {
// mark component as client-only for `definePageMeta`
(m.default || m).__clientOnlyPage = true
return m.default || m
})
: loader)
return defineComponent({
inheritAttrs: false,

View File

@ -5,6 +5,8 @@ import { stringify } from 'devalue'
import type { H3Event } from 'h3'
import { withTrailingSlash } from 'ufo'
import { getContext } from 'unctx'
import { captureRawStackTrace, parseRawStackTrace } from 'errx'
import type { ParsedTrace } from 'errx'
import { isVNode } from 'vue'
import type { NitroApp } from 'nitro/types'
@ -34,15 +36,28 @@ export default (nitroApp: NitroApp) => {
const ctx = asyncContext.tryUse()
if (!ctx) { return }
const stack = getStack()
if (stack.includes('runtime/vite-node.mjs')) { return }
const rawStack = captureRawStackTrace()
if (!rawStack || rawStack.includes('runtime/vite-node.mjs')) { return }
const trace: ParsedTrace[] = []
let filename = ''
for (const entry of parseRawStackTrace(rawStack)) {
if (entry.source === import.meta.url) { continue }
if (EXCLUDE_TRACE_RE.test(entry.source)) { continue }
filename ||= entry.source.replace(withTrailingSlash(rootDir), '')
trace.push({
...entry,
source: entry.source.startsWith('file://') ? entry.source.replace('file://', '') : entry.source,
})
}
const log = {
..._log,
// Pass along filename to allow the client to display more info about where log comes from
filename: extractFilenameFromStack(stack),
filename,
// Clean up file names in stack trace
stack: normalizeFilenames(stack),
stack: trace,
}
// retain log to be include in the next render
@ -68,24 +83,7 @@ export default (nitroApp: NitroApp) => {
})
}
const EXCLUDE_TRACE_RE = /^.*at.*(\/node_modules\/(.*\/)?(nuxt|nuxt-nightly|nuxt-edge|nuxt3|consola|@vue)\/.*|core\/runtime\/nitro.*)$\n?/gm
function getStack () {
// Pass along stack traces if needed (for error and warns)
// eslint-disable-next-line unicorn/error-message
const stack = new Error()
Error.captureStackTrace(stack)
return stack.stack?.replace(EXCLUDE_TRACE_RE, '').replace(/^Error.*\n/, '') || ''
}
const FILENAME_RE = /at[^(]*\(([^:)]+)[):]/
const FILENAME_RE_GLOBAL = /at[^(]*\(([^)]+)\)/g
function extractFilenameFromStack (stacktrace: string) {
return stacktrace.match(FILENAME_RE)?.[1].replace(withTrailingSlash(rootDir), '')
}
function normalizeFilenames (stacktrace: string) {
// remove line numbers and file: protocol - TODO: sourcemap support for line numbers
return stacktrace.replace(FILENAME_RE_GLOBAL, (match, filename) => match.replace(filename, filename.replace('file:///', '/').replace(/:.*$/, '')))
}
const EXCLUDE_TRACE_RE = /\/node_modules\/(?:.*\/)?(?:nuxt|nuxt-nightly|nuxt-edge|nuxt3|consola|@vue)\/|core\/runtime\/nitro/
function onConsoleLog (callback: (log: LogObject) => void) {
consola.addReporter({

View File

@ -115,7 +115,13 @@ function normalizeError (error: any) {
// temp fix for https://github.com/unjs/nitro/issues/759
// TODO: investigate vercel-edge not using unenv pollyfill
const cwd = typeof process.cwd === 'function' ? process.cwd() : '/'
const stack = ((error.stack as string) || '')
// Hide details of unhandled/fatal errors in production
const hideDetails = !import.meta.dev && error.unhandled
const stack = hideDetails
? []
: ((error.stack as string) || '')
.split('\n')
.splice(1)
.filter(line => line.includes('at '))
@ -135,9 +141,8 @@ function normalizeError (error: any) {
})
const statusCode = error.statusCode || 500
const statusMessage =
error.statusMessage ?? (statusCode === 404 ? 'Not Found' : '')
const message = error.message || error.toString()
const statusMessage = error.statusMessage ?? (statusCode === 404 ? 'Not Found' : '')
const message = hideDetails ? 'internal server error' : (error.message || error.toString())
return {
stack,

View File

@ -60,7 +60,7 @@ export const definePageMeta = (meta: PageMeta): void => {
try {
const isRouteComponent = component && useRoute().matched.some(p => Object.values(p.components || {}).includes(component))
const isRenderingServerPage = import.meta.server && useNuxtApp().ssrContext?.islandContext
if (isRouteComponent || isRenderingServerPage) {
if (isRouteComponent || isRenderingServerPage || ((component as any)?.__clientOnlyPage)) {
// don't warn if it's being used in a route component (or server page)
return
}

View File

@ -144,7 +144,12 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
for (const route of routes) {
if (route.file && !augmentedPages.has(route.file)) {
const fileContent = route.file in vfs ? vfs[route.file] : fs.readFileSync(await resolvePath(route.file), 'utf-8')
Object.assign(route, await getRouteMeta(fileContent, route.file))
const routeMeta = await getRouteMeta(fileContent, route.file)
if (route.meta) {
routeMeta.meta = { ...routeMeta.meta, ...route.meta }
}
Object.assign(route, routeMeta)
augmentedPages.add(route.file)
}

View File

@ -17,6 +17,16 @@
"path": ""/"",
},
],
"route.meta generated from file": [
{
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"",
"redirect": "mockMeta?.redirect",
},
],
"should allow pages with `:` in their path": [
{
"alias": "mockMeta?.alias || []",
@ -349,6 +359,16 @@
"redirect": "mockMeta?.redirect",
},
],
"should merge route.meta with meta from file": [
{
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"",
"redirect": "mockMeta?.redirect",
},
],
"should not generate colliding route names when hyphens are in file name": [
{
"alias": "mockMeta?.alias || []",

View File

@ -16,6 +16,14 @@
"path": ""/"",
},
],
"route.meta generated from file": [
{
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
"meta": "{"test":1}",
"name": ""page-with-meta"",
"path": ""/page-with-meta"",
},
],
"should allow pages with `:` in their path": [
{
"component": "() => import("pages/test:name.vue").then(m => m.default || m)",
@ -240,6 +248,14 @@
"path": ""/"",
},
],
"should merge route.meta with meta from file": [
{
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": ""page-with-meta"",
"path": ""/page-with-meta"",
},
],
"should not generate colliding route names when hyphens are in file name": [
{
"component": "() => import("pages/parent/[child].vue").then(m => m.default || m)",

View File

@ -20,7 +20,7 @@ describe('pages:generateRoutesFromFiles', () => {
const tests: Array<{
description: string
files?: Array<{ path: string, template?: string }>
files?: Array<{ path: string, template?: string, meta?: Record<string, any> }>
output?: NuxtPage[]
normalized?: Record<string, any>[]
error?: string
@ -554,6 +554,53 @@ describe('pages:generateRoutesFromFiles', () => {
},
],
},
{
description: 'route.meta generated from file',
files: [
{
path: `${pagesDir}/page-with-meta.vue`,
meta: {
test: 1,
},
},
],
output: [
{
name: 'page-with-meta',
path: '/page-with-meta',
file: `${pagesDir}/page-with-meta.vue`,
children: [],
meta: { test: 1 },
},
],
},
{
description: 'should merge route.meta with meta from file',
files: [
{
path: `${pagesDir}/page-with-meta.vue`,
meta: {
test: 1,
},
template: `
<script setup lang="ts">
definePageMeta({
hello: 'world'
})
</script>
`,
},
],
output: [
{
name: 'page-with-meta',
path: '/page-with-meta',
file: `${pagesDir}/page-with-meta.vue`,
children: [],
meta: { [DYNAMIC_META_KEY]: new Set(['meta']), test: 1 },
},
],
},
]
const normalizedResults: Record<string, any> = {}
@ -572,7 +619,13 @@ describe('pages:generateRoutesFromFiles', () => {
shouldUseServerComponents: true,
absolutePath: file.path,
relativePath: file.path.replace(/^(pages|layer\/pages)\//, ''),
})))
}))).map((route, index) => {
return {
...route,
meta: test.files![index].meta,
}
})
await augmentPages(result, vfs)
} catch (error: any) {
expect(error.message).toEqual(test.error)

View File

@ -40,27 +40,27 @@
"@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8",
"@unhead/schema": "1.9.16",
"@vitejs/plugin-vue": "5.0.5",
"@vitejs/plugin-vue": "5.1.0",
"@vitejs/plugin-vue-jsx": "4.0.0",
"@vue/compiler-core": "3.4.31",
"@vue/compiler-sfc": "3.4.31",
"@vue/language-core": "2.0.26",
"@vue/compiler-core": "3.4.34",
"@vue/compiler-sfc": "3.4.34",
"@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",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"ofetch": "1.3.4",
"unbuild": "3.0.0-rc.6",
"unbuild": "3.0.0-rc.7",
"unctx": "2.3.1",
"unenv": "1.9.0",
"vite": "5.3.4",
"vue": "3.4.31",
"unenv": "1.10.0",
"vite": "5.3.5",
"vue": "3.4.34",
"vue-bundle-renderer": "2.1.0",
"vue-loader": "17.4.2",
"vue-router": "4.4.0",
"webpack": "5.93.0",
"webpack-dev-middleware": "7.2.1"
"webpack-dev-middleware": "7.3.0"
},
"dependencies": {
"compatx": "^0.1.8",
@ -71,9 +71,9 @@
"pkg-types": "^1.1.3",
"scule": "^1.3.0",
"std-env": "^3.7.0",
"ufo": "^1.5.3",
"ufo": "^1.5.4",
"uncrypto": "^0.1.3",
"unimport": "^3.8.0",
"unimport": "^3.9.0",
"untyped": "^1.4.2"
},
"engines": {

View File

@ -1,7 +1,7 @@
import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises'
import { defineUntypedSchema } from 'untyped'
import { basename, join, relative, resolve } from 'pathe'
import { basename, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env'
import { defu } from 'defu'
import { findWorkspaceDir } from 'pkg-types'
@ -158,7 +158,7 @@ export default defineUntypedSchema({
$resolve: async (val: string | undefined, get): Promise<string> => {
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
return resolve(await get('rootDir') as string, (val || isV4) ? 'server' : resolve(await get('srcDir') as string, 'server'))
return resolve(isV4 ? await get('rootDir') as string : await get('srcDir') as string, val ?? 'server')
},
},
@ -422,7 +422,7 @@ export default defineUntypedSchema({
'@': srcDir,
'~~': rootDir,
'@@': rootDir,
[basename(assetsDir)]: join(srcDir, assetsDir),
[basename(assetsDir)]: resolve(srcDir, assetsDir),
[basename(publicDir)]: resolve(srcDir, publicDir),
...val,
}
@ -554,7 +554,7 @@ export default defineUntypedSchema({
* ```js
* export default {
* runtimeConfig: {
* apiKey: '' // Default to an empty string, automatically set at runtime using process.env.NUXT_API_KEY
* apiKey: '', // Default to an empty string, automatically set at runtime using process.env.NUXT_API_KEY
* public: {
* baseURL: '' // Exposed to the frontend as well.
* }

View File

@ -6,7 +6,7 @@ export default defineUntypedSchema({
/**
* Whether to enable HTTPS.
* @example
* ```
* ```ts
* export default defineNuxtConfig({
* devServer: {
* https: {

View File

@ -77,6 +77,23 @@ describe('nuxt folder structure', () => {
}
`)
})
it('should not override value from user for serverDir', async () => {
const result = await applyDefaults(NuxtConfigSchema, { future: { compatibilityVersion: 4 }, serverDir: '/myServer' })
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "<cwd>/app",
"modules": "<cwd>/modules",
"public": "<cwd>/public",
},
"rootDir": "<cwd>",
"serverDir": "/myServer",
"srcDir": "<cwd>/app",
"workspaceDir": "<cwd>",
}
`)
})
})
function getDirs (options: NuxtOptions) {

View File

@ -19,18 +19,18 @@
},
"devDependencies": {
"@types/html-minifier": "4.0.5",
"@unocss/reset": "0.61.4",
"@unocss/reset": "0.61.5",
"critters": "0.0.24",
"execa": "9.3.0",
"globby": "14.0.2",
"html-minifier": "4.0.0",
"html-validate": "8.20.1",
"html-validate": "8.21.0",
"jiti": "2.0.0-beta.3",
"knitwork": "1.1.0",
"pathe": "1.1.2",
"prettier": "3.3.3",
"scule": "1.3.0",
"unocss": "0.61.4",
"vite": "5.3.4"
"unocss": "0.61.5",
"vite": "5.3.5"
}
}

View File

@ -27,14 +27,14 @@
"@nuxt/schema": "workspace:*",
"@types/clear": "0.1.4",
"@types/estree": "1.0.5",
"rollup": "4.18.1",
"unbuild": "3.0.0-rc.6",
"vue": "3.4.31"
"rollup": "4.19.0",
"unbuild": "3.0.0-rc.7",
"vue": "3.4.34"
},
"dependencies": {
"@nuxt/kit": "workspace:*",
"@rollup/plugin-replace": "^5.0.7",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue": "^5.1.0",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"autoprefixer": "^10.4.19",
"clear": "^0.1.0",
@ -55,15 +55,15 @@
"pathe": "^1.1.2",
"perfect-debounce": "^1.0.0",
"pkg-types": "^1.1.3",
"postcss": "^8.4.39",
"postcss": "^8.4.40",
"rollup-plugin-visualizer": "^5.12.0",
"std-env": "^3.7.0",
"strip-literal": "^2.1.0",
"ufo": "^1.5.3",
"unenv": "^1.9.0",
"unplugin": "^1.11.0",
"vite": "^5.3.4",
"vite-node": "^2.0.3",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^1.12.0",
"vite": "^5.3.5",
"vite-node": "^2.0.4",
"vite-plugin-checker": "^0.7.2",
"vue-bundle-renderer": "^2.1.0"
},

View File

@ -44,13 +44,13 @@
"knitwork": "^1.1.0",
"lodash-es": "4.17.21",
"magic-string": "^0.30.10",
"memfs": "^4.9.3",
"memfs": "^4.9.4",
"mini-css-extract-plugin": "^2.9.0",
"mlly": "^1.7.1",
"ohash": "^1.1.3",
"pathe": "^1.1.2",
"pify": "^6.1.0",
"postcss": "^8.4.39",
"postcss": "^8.4.40",
"postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1",
@ -58,15 +58,15 @@
"pug-plain-loader": "^1.1.0",
"std-env": "^3.7.0",
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.3",
"unenv": "^1.9.0",
"unplugin": "^1.11.0",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^1.12.0",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.0",
"vue-loader": "^17.4.2",
"webpack": "^5.93.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-middleware": "^7.2.1",
"webpack-dev-middleware": "^7.3.0",
"webpack-hot-middleware": "^2.26.1",
"webpack-virtual-modules": "^0.6.2",
"webpackbar": "^6.0.1"
@ -78,9 +78,9 @@
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.18.1",
"unbuild": "3.0.0-rc.6",
"vue": "3.4.31"
"rollup": "4.19.0",
"unbuild": "3.0.0-rc.7",
"vue": "3.4.34"
},
"peerDependencies": {
"vue": "^3.3.4"

File diff suppressed because it is too large Load Diff

View File

@ -533,8 +533,7 @@ describe('pages', () => {
await clientInitialPage.waitForFunction(() => window.useNuxtApp?.()._route.fullPath === '/client-only-page/normal')
// that page should be client rendered
// TODO: investigate why multiple elements are appearing on page
expect(await clientInitialPage.locator('#server-rendered').first().textContent()).toMatchInlineSnapshot('"false"')
expect(await clientInitialPage.locator('#server-rendered').textContent()).toMatchInlineSnapshot('"false"')
// and not contain any errors or warnings
expect(errors.length).toBe(0)
@ -591,7 +590,7 @@ describe('nuxt composables', () => {
},
})
const cookies = res.headers.get('set-cookie')
expect(cookies).toMatchInlineSnapshot('"set-in-plugin=true; Path=/, set=set; Path=/, browser-set=set; Path=/, browser-set-to-null=; Max-Age=0; Path=/, browser-set-to-null-with-default=; Max-Age=0; Path=/, browser-object-default=%7B%22foo%22%3A%22bar%22%7D; Path=/"')
expect(cookies).toMatchInlineSnapshot('"set-in-plugin=true; Path=/, accessed-with-default-value=default; Path=/, set=set; Path=/, browser-set=set; Path=/, browser-set-to-null=; Max-Age=0; Path=/, browser-set-to-null-with-default=; Max-Age=0; Path=/, browser-object-default=%7B%22foo%22%3A%22bar%22%7D; Path=/"')
})
it('updates cookies when they are changed', async () => {
const { page } = await renderPage('/cookies')

View File

@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
for (const outputDir of ['.output', '.output-inline']) {
it('default client bundle size', async () => {
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot(`"106k"`)
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot(`"107k"`)
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[
"_nuxt/entry.js",
@ -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(`"1344k"`)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1347k"`)
const packages = modules.files
.filter(m => m.endsWith('package.json'))
@ -73,7 +73,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(`"526k"`)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"534k"`)
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"80.3k"`)

View File

@ -1,4 +1,10 @@
<script setup lang="ts">
definePageMeta({
// Disable page transition for this page to avoid having multiple time the same page during transition
pageTransition: false,
layoutTransition: false,
})
const state = useState('test', () => {
let hasAccessToWindow = null as null | boolean

View File

@ -1,5 +1,10 @@
<script setup lang="ts">
const renderedOnServer = useState(() => import.meta.server)
definePageMeta({
// Disable page transition for this page to avoid having multiple time the same page during transition
pageTransition: false,
layoutTransition: false,
})
</script>
<template>

View File

@ -1,28 +1,38 @@
<script setup>
<script setup lang="ts">
useCookie('accessed-but-not-used')
useCookie('accessed-with-default-value', () => 'default')
useCookie('accessed-with-default-value', { default: () => 'default' })
useCookie('set').value = 'set'
useCookie('set-to-null').value = null
useCookie('set-to-null-with-default', () => 'default').value = null
useCookie<string | null>('set-to-null-with-default', { default: () => 'default' }).value = null
// the next set are all sent by browser
useCookie('browser-accessed-but-not-used')
useCookie('browser-accessed-with-default-value', () => 'default')
useCookie('browser-accessed-with-default-value', { default: () => 'default' })
useCookie('browser-set').value = 'set'
// confirm that it only sets one `set-cookie` header
useCookie('browser-set').value = 'set'
useCookie('browser-set-to-null').value = null
useCookie('browser-set-to-null-with-default', () => 'default').value = null
useCookie<string | null>('browser-set-to-null-with-default', { default: () => 'default' }).value = null
const objectCookie = useCookie('browser-object-default')
const objectCookie = useCookie<{ foo: string } | undefined>('browser-object-default')
const objectCookieSecond = useCookie('browser-object-default', {
default: () => ({ foo: 'bar' }),
})
function changeCookie () {
if (objectCookie.value!.foo === 'baz') {
objectCookie.value!.foo = 'bar'
} else {
objectCookie.value!.foo = 'baz'
}
}
</script>
<template>
<div>
<div>cookies testing page</div>
<pre>{{ objectCookie?.foo }}</pre>
<pre>{{ objectCookieSecond.foo }}</pre>
<button @click="objectCookie.foo === 'baz' ? objectCookie.foo = 'bar' : objectCookie.foo = 'baz'">
<button @click="changeCookie">
Change cookie
</button>
<button @click="refreshCookie('browser-object-default')">