diff --git a/docs/3.api/2.composables/use-cookie.md b/docs/3.api/2.composables/use-cookie.md index edc95916833a..250eebf3baab 100644 --- a/docs/3.api/2.composables/use-cookie.md +++ b/docs/3.api/2.composables/use-cookie.md @@ -45,6 +45,10 @@ counter.value = counter.value || Math.round(Math.random() * 1000) :link-example{to="/docs/examples/advanced/use-cookie"} +::note +Обновите значения `useCookie` вручную, когда cookies изменились, используя [`refreshCookie`](/api/utils/refresh-cookie). +:: + ## Параметры Данный композабл принимает несколько опций, которые позволяют изменять поведение cookie. @@ -148,6 +152,10 @@ counter.value = counter.value || Math.round(Math.random() * 1000) - `shallow` - Будет следить за изменениями данных cookie ref только для свойств верхнего уровня. - `false` - Не будет следить за изменениями данных cookie ref. +::note +Обновите значения `useCookie` вручную, когда cookies изменились, используя [`refreshCookie`](/api/utils/refresh-cookie). +:: + **Пример 1:** ```vue diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index e4fa62d8a424..a126078b8a98 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -8,10 +8,10 @@ import type { } from 'vue' import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue' import type { RouteLocation, RouteLocationRaw, Router, RouterLink, RouterLinkProps, useLink } from '#vue-router' -import { hasProtocol, joinURL, parseQuery, withQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo' +import { hasProtocol, joinURL, parseQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo' import { preloadRouteComponents } from '../composables/preload' import { onNuxtReady } from '../composables/ready' -import { navigateTo, useRouter } from '../composables/router' +import { navigateTo, resolveRouteObject, useRouter } from '../composables/router' import { useNuxtApp, useRuntimeConfig } from '../nuxt' import { cancelIdleCallback, requestIdleCallback } from '../compat/idle-callback' @@ -495,7 +495,3 @@ function isSlowConnection () { if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true } return false } - -function resolveRouteObject (to: Exclude) { - return withQuery(to.path || '', to.query || {}) + (to.hash ? '#' + to.hash : '') -} diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index d6395e5832c3..fad568a88556 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -1,6 +1,6 @@ import { getCurrentInstance, hasInjectionContext, inject, onScopeDispose } from 'vue' import type { Ref } from 'vue' -import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router' +import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router' import { sanitizeStatusCode } from 'h3' import { hasProtocol, isScriptProtocol, joinURL, withQuery } from 'ufo' @@ -120,7 +120,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na to = '/' } - const toPath = typeof to === 'string' ? to : (withQuery((to as RouteLocationPathRaw).path || '/', to.query || {}) + (to.hash || '')) + const toPath = typeof to === 'string' ? to : 'path' in to ? resolveRouteObject(to) : useRouter().resolve(to).href // Early open handler if (import.meta.client && options?.open) { @@ -252,3 +252,10 @@ export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? strin useRoute().meta.layout = layout as Exclude } } + +/** + * @internal + */ +export function resolveRouteObject (to: Exclude) { + return withQuery(to.path || '', to.query || {}) + (to.hash || '') +} diff --git a/packages/nuxt/src/app/plugins/navigation-repaint.client.ts b/packages/nuxt/src/app/plugins/navigation-repaint.client.ts new file mode 100644 index 000000000000..30f3a69cf18e --- /dev/null +++ b/packages/nuxt/src/app/plugins/navigation-repaint.client.ts @@ -0,0 +1,19 @@ +import { defineNuxtPlugin } from '../nuxt' +import { useRouter } from '../composables' + +export default defineNuxtPlugin(() => { + useRouter().beforeResolve(async () => { + /** + * This gives an opportunity for the browser to repaint, acknowledging user interaction. + * It can reduce INP when navigating on prerendered routes. + * + * @see https://github.com/nuxt/nuxt/issues/26271#issuecomment-2178582037 + * @see https://vercel.com/blog/demystifying-inp-new-tools-and-actionable-insights + */ + await new Promise((resolve) => { + // Ensure we always resolve, even if the animation frame never fires + setTimeout(resolve, 100) + requestAnimationFrame(() => { setTimeout(resolve, 0) }) + }) + }) +}) diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index 0628188db6d7..c8bcab3ede42 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -6,7 +6,7 @@ import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from import { joinURL, withTrailingSlash } from 'ufo' import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack' import type { Nitro, NitroConfig, NitroOptions } from 'nitropack' -import { findPath, logger, resolveIgnorePatterns, resolveNuxtModule, resolvePath } from '@nuxt/kit' +import { findPath, logger, resolveAlias, resolveIgnorePatterns, resolveNuxtModule, resolvePath } from '@nuxt/kit' import escapeRE from 'escape-string-regexp' import { defu } from 'defu' import fsExtra from 'fs-extra' @@ -218,6 +218,9 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!) nitroConfig.ignore = [...(nitroConfig.ignore || []), ...resolveIgnorePatterns(nitroConfig.srcDir), `!${join(nuxt.options.buildDir, 'dist/client', nuxt.options.app.buildAssetsDir, '**/*')}`] + // Resolve aliases in user-provided input - so `~/server/test` will work + nitroConfig.plugins = nitroConfig.plugins?.map(plugin => plugin ? resolveAlias(plugin, nuxt.options.alias) : plugin) + // Add app manifest handler and prerender configuration if (nuxt.options.experimental.appManifest) { const buildId = nuxt.options.runtimeConfig.app.buildId ||= nuxt.options.buildId diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index a121ab844271..d5fe6f87408a 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -99,12 +99,12 @@ async function initNuxt (nuxt: Nuxt) { const packageJSON = await readPackageJSON(nuxt.options.rootDir).catch(() => ({}) as PackageJson) const dependencies = new Set([...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.devDependencies || {})]) const paths = Object.fromEntries(await Promise.all(coreTypePackages.map(async (pkg) => { - // ignore packages that exist in `package.json` as these can be resolved by TypeScript - if (dependencies.has(pkg) && !(pkg in nightlies)) { return [] } - const [_pkg = pkg, _subpath] = /^[^@]+\//.test(pkg) ? pkg.split('/') : [pkg] const subpath = _subpath ? '/' + _subpath : '' + // ignore packages that exist in `package.json` as these can be resolved by TypeScript + if (dependencies.has(_pkg) && !(_pkg in nightlies)) { return [] } + async function resolveTypePath (path: string) { try { const r = await _resolvePath(path, { url: nuxt.options.modulesDir, conditions: ['types', 'import', 'require'] }) @@ -120,14 +120,14 @@ async function initNuxt (nuxt: Nuxt) { // deduplicate types for nightly releases if (_pkg in nightlies) { - const nightly = nightlies[pkg as keyof typeof nightlies] + const nightly = nightlies[_pkg as keyof typeof nightlies] const path = await resolveTypePath(nightly + subpath) if (path) { return [[pkg, [path]], [nightly + subpath, [path]]] } } - const path = await resolveTypePath(pkg + subpath) + const path = await resolveTypePath(_pkg + subpath) if (path) { return [[pkg, [path]]] } @@ -532,6 +532,12 @@ async function initNuxt (nuxt: Nuxt) { } } + if (nuxt.options.experimental.navigationRepaint) { + addPlugin({ + src: resolve(nuxt.options.appDir, 'plugins/navigation-repaint.client'), + }) + } + nuxt.hooks.hook('builder:watch', (event, relativePath) => { const path = resolve(nuxt.options.srcDir, relativePath) // Local module patterns diff --git a/packages/nuxt/src/pages/build.d.ts b/packages/nuxt/src/pages/build.d.ts new file mode 100644 index 000000000000..4a17fca4e581 --- /dev/null +++ b/packages/nuxt/src/pages/build.d.ts @@ -0,0 +1,6 @@ +declare module '#build/router.options' { + import type { RouterOptions } from '@nuxt/schema' + + const _default: RouterOptions + export default _default +} diff --git a/packages/nuxt/src/pages/runtime/plugins/prerender.server.ts b/packages/nuxt/src/pages/runtime/plugins/prerender.server.ts index a8cf539151de..9a191f9dce1c 100644 --- a/packages/nuxt/src/pages/runtime/plugins/prerender.server.ts +++ b/packages/nuxt/src/pages/runtime/plugins/prerender.server.ts @@ -5,7 +5,6 @@ import { defineNuxtPlugin } from '#app/nuxt' import { prerenderRoutes } from '#app/composables/ssr' // @ts-expect-error virtual file import _routes from '#build/routes' -// @ts-expect-error virtual file import routerOptions from '#build/router.options' let routes: string[] diff --git a/packages/nuxt/src/pages/runtime/plugins/router.ts b/packages/nuxt/src/pages/runtime/plugins/router.ts index fccd8ff78a6a..f284a80d29f7 100644 --- a/packages/nuxt/src/pages/runtime/plugins/router.ts +++ b/packages/nuxt/src/pages/runtime/plugins/router.ts @@ -24,7 +24,6 @@ import { navigateTo } from '#app/composables/router' import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs' // @ts-expect-error virtual file import _routes from '#build/routes' -// @ts-expect-error virtual file import routerOptions from '#build/router.options' // @ts-expect-error virtual file import { globalMiddleware, namedMiddleware } from '#build/middleware' @@ -67,7 +66,7 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({ : createMemoryHistory(routerBase) ) - const routes = routerOptions.routes?.(_routes) ?? _routes + const routes = routerOptions.routes ? await routerOptions.routes(_routes) ?? _routes : _routes let startPosition: Parameters[2] | null diff --git a/packages/nuxt/test/app.test.ts b/packages/nuxt/test/app.test.ts index 2e66d85bdf5f..c7424e95ad84 100644 --- a/packages/nuxt/test/app.test.ts +++ b/packages/nuxt/test/app.test.ts @@ -43,6 +43,10 @@ describe('resolveApp', () => { "mode": "client", "src": "/packages/nuxt/src/app/plugins/payload.client.ts", }, + { + "mode": "client", + "src": "/packages/nuxt/src/app/plugins/navigation-repaint.client.ts", + }, { "mode": "client", "src": "/packages/nuxt/src/app/plugins/check-outdated-build.client.ts", diff --git a/packages/nuxt/test/nuxt-link.test.ts b/packages/nuxt/test/nuxt-link.test.ts index a712d0f267c8..40e714045d2d 100644 --- a/packages/nuxt/test/nuxt-link.test.ts +++ b/packages/nuxt/test/nuxt-link.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import type { RouteLocation } from 'vue-router' +import type { RouteLocation, RouteLocationRaw } from 'vue-router' +import { withQuery } from 'ufo' import type { NuxtLinkOptions, NuxtLinkProps } from '../src/app/components/nuxt-link' import { defineNuxtLink } from '../src/app/components/nuxt-link' import { useRuntimeConfig } from '../src/app/nuxt' @@ -25,6 +26,9 @@ vi.mock('vue', async () => { // Mocks Nuxt `useRouter()` vi.mock('../src/app/composables/router', () => ({ + resolveRouteObject (to: Exclude) { + return withQuery(to.path || '', to.query || {}) + (to.hash || '') + }, useRouter: () => ({ resolve: (route: string | RouteLocation & { to?: string }): Partial & { href?: string } => { if (typeof route === 'string') { diff --git a/packages/schema/src/config/common.ts b/packages/schema/src/config/common.ts index aebb01b081fe..a8d50cde5a9f 100644 --- a/packages/schema/src/config/common.ts +++ b/packages/schema/src/config/common.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs' import { readdir } from 'node:fs/promises' import { defineUntypedSchema } from 'untyped' -import { join, relative, resolve } from 'pathe' +import { basename, join, relative, resolve } from 'pathe' import { isDebug, isDevelopment, isTest } from 'std-env' import { defu } from 'defu' import { findWorkspaceDir } from 'pkg-types' @@ -420,8 +420,8 @@ export default defineUntypedSchema({ '@': srcDir, '~~': rootDir, '@@': rootDir, - [assetsDir]: join(srcDir, assetsDir), - [publicDir]: join(srcDir, publicDir), + [basename(assetsDir)]: join(srcDir, assetsDir), + [basename(publicDir)]: join(srcDir, publicDir), ...val, } }, diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index c1d903ba5d59..a9b9b26dcd86 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -417,5 +417,13 @@ export default defineUntypedSchema({ * @type {boolean} */ clientNodeCompat: false, + + /** + * Wait for a single animation frame before navigation, which gives an opportunity + * for the browser to repaint, acknowledging user interaction. + * + * It can reduce INP when navigating on prerendered routes. + */ + navigationRepaint: true, }, }) diff --git a/packages/schema/src/types/router.ts b/packages/schema/src/types/router.ts index 76abc6acb848..c7cb0ce850fc 100644 --- a/packages/schema/src/types/router.ts +++ b/packages/schema/src/types/router.ts @@ -2,7 +2,7 @@ import type { RouterHistory, RouterOptions as _RouterOptions } from 'vue-router' export type RouterOptions = Partial> & { history?: (baseURL?: string) => RouterHistory - routes?: (_routes: _RouterOptions['routes']) => _RouterOptions['routes'] + routes?: (_routes: _RouterOptions['routes']) => _RouterOptions['routes'] | Promise<_RouterOptions['routes']> hashMode?: boolean scrollBehaviorType?: 'smooth' | 'auto' }