mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
fix(nuxt): experimental build manifest + client route rules (#21641)
This commit is contained in:
parent
2bf9028f7e
commit
7dce07653c
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -170,10 +170,13 @@ jobs:
|
||||
env: ['dev', 'built']
|
||||
builder: ['vite', 'webpack']
|
||||
context: ['async', 'default']
|
||||
manifest: ['manifest-on', 'manifest-off']
|
||||
node: [18]
|
||||
exclude:
|
||||
- env: 'dev'
|
||||
builder: 'webpack'
|
||||
- manifest: 'manifest-off'
|
||||
builder: 'webpack'
|
||||
|
||||
timeout-minutes: 15
|
||||
|
||||
@ -231,6 +234,7 @@ jobs:
|
||||
env:
|
||||
TEST_ENV: ${{ matrix.env }}
|
||||
TEST_BUILDER: ${{ matrix.builder }}
|
||||
TEST_MANIFEST: ${{ matrix.manifest }}
|
||||
TEST_CONTEXT: ${{ matrix.context }}
|
||||
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }}
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
"play": "nuxi dev playground",
|
||||
"play:build": "nuxi build playground",
|
||||
"play:preview": "nuxi preview playground",
|
||||
"test": "pnpm test:fixtures && pnpm test:fixtures:payload && pnpm test:fixtures:dev && pnpm test:fixtures:webpack && pnpm test:unit && pnpm typecheck",
|
||||
"test": "pnpm test:fixtures && pnpm test:fixtures:dev && pnpm test:fixtures:webpack && pnpm test:unit && pnpm test:runtime && pnpm test:types && pnpm typecheck",
|
||||
"test:fixtures": "nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/runtime-compiler && vitest run --dir test",
|
||||
"test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures",
|
||||
"test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures",
|
||||
|
7
packages/nuxt/index.d.ts
vendored
7
packages/nuxt/index.d.ts
vendored
@ -2,6 +2,13 @@ declare global {
|
||||
var __NUXT_VERSION__: string
|
||||
var __NUXT_PREPATHS__: string[] | string | undefined
|
||||
var __NUXT_PATHS__: string[] | string | undefined
|
||||
|
||||
interface Navigator {
|
||||
connection?: {
|
||||
type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown'
|
||||
effectiveType: 'slow-2g' | '2g' | '3g' | '4g'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
@ -89,6 +89,7 @@
|
||||
"pathe": "^1.1.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^1.0.3",
|
||||
"radix3": "^1.1.0",
|
||||
"scule": "^1.0.0",
|
||||
"std-env": "^3.4.3",
|
||||
"strip-literal": "^1.3.0",
|
||||
|
@ -29,6 +29,8 @@ export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBefor
|
||||
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
|
||||
export { preloadComponents, prefetchComponents, preloadRouteComponents } from './preload'
|
||||
export { isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver } from './payload'
|
||||
export { getAppManifest, getRouteRules } from './manifest'
|
||||
export type { NuxtAppManifest, NuxtAppManifestMeta } from './manifest'
|
||||
export type { ReloadNuxtAppOptions } from './chunk'
|
||||
export { reloadNuxtApp } from './chunk'
|
||||
export { useRequestURL } from './url'
|
||||
|
46
packages/nuxt/src/app/composables/manifest.ts
Normal file
46
packages/nuxt/src/app/composables/manifest.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { joinURL } from 'ufo'
|
||||
import type { MatcherExport, RouteMatcher } from 'radix3'
|
||||
import { createMatcherFromExport } from 'radix3'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig, useRuntimeConfig } from '#app'
|
||||
// @ts-expect-error virtual file
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
|
||||
export interface NuxtAppManifestMeta {
|
||||
id: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface NuxtAppManifest extends NuxtAppManifestMeta {
|
||||
matcher: MatcherExport
|
||||
prerendered: string[]
|
||||
}
|
||||
|
||||
let manifest: Promise<NuxtAppManifest>
|
||||
let matcher: RouteMatcher
|
||||
|
||||
function fetchManifest () {
|
||||
if (!isAppManifestEnabled) {
|
||||
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
|
||||
}
|
||||
const config = useRuntimeConfig()
|
||||
// @ts-expect-error private property
|
||||
const buildId = useAppConfig().nuxt?.buildId
|
||||
manifest = $fetch<NuxtAppManifest>(joinURL(config.app.cdnURL || config.app.baseURL, config.app.buildAssetsDir, `builds/meta/${buildId}.json`))
|
||||
manifest.then((m) => {
|
||||
matcher = createMatcherFromExport(m.matcher)
|
||||
})
|
||||
return manifest
|
||||
}
|
||||
|
||||
export function getAppManifest (): Promise<NuxtAppManifest> {
|
||||
if (!isAppManifestEnabled) {
|
||||
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
|
||||
}
|
||||
return manifest || fetchManifest()
|
||||
}
|
||||
|
||||
export async function getRouteRules (url: string) {
|
||||
await getAppManifest()
|
||||
return defu({} as Record<string, any>, ...matcher.matchAll(url).reverse())
|
||||
}
|
@ -4,8 +4,11 @@ import { useHead } from '@unhead/vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||
|
||||
import { getAppManifest, getRouteRules } from '#app/composables/manifest'
|
||||
import { useRoute } from '#app/composables'
|
||||
|
||||
// @ts-expect-error virtual import
|
||||
import { renderJsonPayloads } from '#build/nuxt.config.mjs'
|
||||
import { appManifest, payloadExtraction, renderJsonPayloads } from '#build/nuxt.config.mjs'
|
||||
|
||||
interface LoadPayloadOptions {
|
||||
fresh?: boolean
|
||||
@ -13,19 +16,24 @@ interface LoadPayloadOptions {
|
||||
}
|
||||
|
||||
export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record<string, any> | Promise<Record<string, any>> | null {
|
||||
if (import.meta.server) { return null }
|
||||
if (import.meta.server || !payloadExtraction) { return null }
|
||||
const payloadURL = _getPayloadURL(url, opts)
|
||||
const nuxtApp = useNuxtApp()
|
||||
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
|
||||
if (cache[payloadURL]) {
|
||||
if (payloadURL in cache) {
|
||||
return cache[payloadURL]
|
||||
}
|
||||
cache[payloadURL] = _importPayload(payloadURL).then((payload) => {
|
||||
if (!payload) {
|
||||
delete cache[payloadURL]
|
||||
cache[payloadURL] = isPrerendered().then((prerendered) => {
|
||||
if (!prerendered) {
|
||||
cache[payloadURL] = null
|
||||
return null
|
||||
}
|
||||
return payload
|
||||
return _importPayload(payloadURL).then((payload) => {
|
||||
if (payload) { return payload }
|
||||
|
||||
delete cache[payloadURL]
|
||||
return null
|
||||
})
|
||||
})
|
||||
return cache[payloadURL]
|
||||
}
|
||||
@ -55,7 +63,7 @@ function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
||||
}
|
||||
|
||||
async function _importPayload (payloadURL: string) {
|
||||
if (import.meta.server) { return null }
|
||||
if (import.meta.server || !payloadExtraction) { return null }
|
||||
const payloadPromise = renderJsonPayloads
|
||||
? fetch(payloadURL).then(res => res.text().then(parsePayload))
|
||||
: import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r)
|
||||
@ -68,10 +76,19 @@ async function _importPayload (payloadURL: string) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function isPrerendered () {
|
||||
export async function isPrerendered (url = useRoute().path) {
|
||||
// Note: Alternative for server is checking x-nitro-prerender header
|
||||
const nuxtApp = useNuxtApp()
|
||||
return !!nuxtApp.payload.prerenderedAt
|
||||
if (nuxtApp.payload.prerenderedAt) {
|
||||
return true
|
||||
}
|
||||
if (!appManifest) { return false }
|
||||
const manifest = await getAppManifest()
|
||||
if (manifest.prerendered.includes(url)) {
|
||||
return true
|
||||
}
|
||||
const rules = await getRouteRules(url)
|
||||
return !!rules.prerender
|
||||
}
|
||||
|
||||
let payloadCache: any = null
|
||||
|
10
packages/nuxt/src/app/middleware/manifest-route-rule.ts
Normal file
10
packages/nuxt/src/app/middleware/manifest-route-rule.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineNuxtRouteMiddleware } from '#app/composables/router'
|
||||
import { getRouteRules } from '#app/composables/manifest'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
if (import.meta.server || import.meta.test) { return }
|
||||
const rules = await getRouteRules(to.path)
|
||||
if (rules.redirect) {
|
||||
return rules.redirect
|
||||
}
|
||||
})
|
@ -16,6 +16,7 @@ import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
|
||||
import type { RouteMiddleware } from '../../app'
|
||||
import type { NuxtError } from '../app/composables/error'
|
||||
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
||||
import type { NuxtAppManifestMeta } from '#app/composables'
|
||||
|
||||
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app', {
|
||||
asyncContext: !!process.env.NUXT_ASYNC_CONTEXT && process.server
|
||||
@ -35,6 +36,7 @@ export interface RuntimeNuxtHooks {
|
||||
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
||||
'app:chunkError': (options: { error: any }) => HookResult
|
||||
'app:data:refresh': (keys?: string[]) => HookResult
|
||||
'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult
|
||||
'link:prefetch': (link: string) => HookResult
|
||||
'page:start': (Component?: VNode) => HookResult
|
||||
'page:finish': (Component?: VNode) => HookResult
|
||||
@ -115,7 +117,7 @@ interface _NuxtApp {
|
||||
/** @internal */
|
||||
_observer?: { observe: (element: Element, callback: () => void) => () => void }
|
||||
/** @internal */
|
||||
_payloadCache?: Record<string, Promise<Record<string, any>> | Record<string, any>>
|
||||
_payloadCache?: Record<string, Promise<Record<string, any>> | Record<string, any> | null>
|
||||
|
||||
/** @internal */
|
||||
_appConfig: AppConfig
|
||||
|
23
packages/nuxt/src/app/plugins/check-outdated-build.client.ts
Normal file
23
packages/nuxt/src/app/plugins/check-outdated-build.client.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { joinURL } from 'ufo'
|
||||
import type { NuxtAppManifestMeta } from '#app'
|
||||
import { defineNuxtPlugin, getAppManifest, onNuxtReady, useRuntimeConfig } from '#app'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
if (import.meta.test) { return }
|
||||
|
||||
let timeout: NodeJS.Timeout
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
async function getLatestManifest () {
|
||||
const currentManifest = await getAppManifest()
|
||||
if (timeout) { clearTimeout(timeout) }
|
||||
timeout = setTimeout(getLatestManifest, 1000 * 60 * 60)
|
||||
const meta = await $fetch<NuxtAppManifestMeta>(joinURL(config.app.cdnURL || config.app.baseURL, config.app.buildAssetsDir, 'builds/latest.json'))
|
||||
if (meta.id !== currentManifest.id) {
|
||||
// There is a newer build which we will let the user handle
|
||||
nuxtApp.hooks.callHook('app:manifest:update', meta)
|
||||
}
|
||||
}
|
||||
|
||||
onNuxtReady(() => { timeout = setTimeout(getLatestManifest, 1000 * 60 * 60) })
|
||||
})
|
@ -1,4 +1,5 @@
|
||||
import { joinURL } from 'ufo'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
|
||||
import { useRouter } from '#app/composables/router'
|
||||
import { reloadNuxtApp } from '#app/composables/chunk'
|
||||
@ -14,11 +15,19 @@ export default defineNuxtPlugin({
|
||||
router.beforeEach(() => { chunkErrors.clear() })
|
||||
nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) })
|
||||
|
||||
function reloadAppAtPath (to: RouteLocationNormalized) {
|
||||
const isHash = 'href' in to && (to.href as string).startsWith('#')
|
||||
const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath)
|
||||
reloadNuxtApp({ path, persistState: true })
|
||||
}
|
||||
|
||||
nuxtApp.hook('app:manifest:update', () => {
|
||||
router.beforeResolve(reloadAppAtPath)
|
||||
})
|
||||
|
||||
router.onError((error, to) => {
|
||||
if (chunkErrors.has(error)) {
|
||||
const isHash = 'href' in to && (to.href as string).startsWith('#')
|
||||
const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath)
|
||||
reloadNuxtApp({ path, persistState: true })
|
||||
reloadAppAtPath(to)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,23 +1,17 @@
|
||||
import { parseURL } from 'ufo'
|
||||
import { defineNuxtPlugin } from '#app/nuxt'
|
||||
import { isPrerendered, loadPayload } from '#app/composables/payload'
|
||||
import { loadPayload } from '#app/composables/payload'
|
||||
import { onNuxtReady } from '#app/composables/ready'
|
||||
import { useRouter } from '#app/composables/router'
|
||||
import { getAppManifest } from '#app/composables/manifest'
|
||||
// @ts-expect-error virtual file
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt:payload',
|
||||
setup (nuxtApp) {
|
||||
// Only enable behavior if initial page is prerendered
|
||||
// TODO: Support hybrid and dev
|
||||
if (!isPrerendered()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Load payload into cache
|
||||
nuxtApp.hooks.hook('link:prefetch', async (url) => {
|
||||
if (!parseURL(url).protocol) {
|
||||
await loadPayload(url)
|
||||
}
|
||||
})
|
||||
// TODO: Support dev
|
||||
if (process.dev) { return }
|
||||
|
||||
// Load payload after middleware & once final route is resolved
|
||||
useRouter().beforeResolve(async (to, from) => {
|
||||
@ -26,5 +20,17 @@ export default defineNuxtPlugin({
|
||||
if (!payload) { return }
|
||||
Object.assign(nuxtApp.static.data, payload.data)
|
||||
})
|
||||
|
||||
onNuxtReady(() => {
|
||||
// Load payload into cache
|
||||
nuxtApp.hooks.hook('link:prefetch', async (url) => {
|
||||
if (!parseURL(url).protocol) {
|
||||
await loadPayload(url)
|
||||
}
|
||||
})
|
||||
if (isAppManifestEnabled && navigator.connection?.effectiveType !== 'slow-2g') {
|
||||
setTimeout(getAppManifest, 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { existsSync, promises as fsp, readFileSync } from 'node:fs'
|
||||
import { cpus } from 'node:os'
|
||||
import { join, relative, resolve } from 'pathe'
|
||||
import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3'
|
||||
import { randomUUID } from 'uncrypto'
|
||||
import { joinURL } from 'ufo'
|
||||
import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack'
|
||||
import type { Nitro, NitroConfig } from 'nitropack'
|
||||
import { logger, resolveIgnorePatterns } from '@nuxt/kit'
|
||||
@ -198,6 +201,80 @@ 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)]
|
||||
|
||||
// Add app manifest handler and prerender configuration
|
||||
if (nuxt.options.experimental.appManifest) {
|
||||
// @ts-expect-error untyped nuxt property
|
||||
const buildId = nuxt.options.appConfig.nuxt!.buildId ||= randomUUID()
|
||||
const buildTimestamp = Date.now()
|
||||
|
||||
const manifestPrefix = joinURL(nuxt.options.app.buildAssetsDir, 'builds')
|
||||
const tempDir = join(nuxt.options.buildDir, 'manifest')
|
||||
|
||||
nitroConfig.publicAssets!.unshift(
|
||||
// build manifest
|
||||
{
|
||||
dir: join(tempDir, 'meta'),
|
||||
maxAge: 31536000 /* 1 year */,
|
||||
baseURL: joinURL(manifestPrefix, 'meta')
|
||||
},
|
||||
// latest build
|
||||
{
|
||||
dir: tempDir,
|
||||
maxAge: 1,
|
||||
baseURL: manifestPrefix
|
||||
}
|
||||
)
|
||||
|
||||
nuxt.hook('nitro:build:before', async (nitro) => {
|
||||
const routeRules = {} as Record<string, any>
|
||||
const _routeRules = nitro.options.routeRules
|
||||
for (const key in _routeRules) {
|
||||
if (key === '/__nuxt_error') { continue }
|
||||
const filteredRules = Object.entries(_routeRules[key])
|
||||
.filter(([key, value]) => ['prerender', 'redirect'].includes(key) && value)
|
||||
.map(([key, value]: any) => {
|
||||
if (key === 'redirect') {
|
||||
return [key, typeof value === 'string' ? value : value.to]
|
||||
}
|
||||
return [key, value]
|
||||
})
|
||||
if (filteredRules.length > 0) {
|
||||
routeRules[key] = Object.fromEntries(filteredRules)
|
||||
}
|
||||
}
|
||||
|
||||
// Add pages prerendered but not covered by route rules
|
||||
const prerenderedRoutes = new Set<string>()
|
||||
const routeRulesMatcher = toRouteMatcher(
|
||||
createRadixRouter({ routes: routeRules })
|
||||
)
|
||||
const payloadSuffix = nuxt.options.experimental.renderJsonPayloads ? '/_payload.json' : '/_payload.js'
|
||||
for (const route of nitro._prerenderedRoutes || []) {
|
||||
if (!route.error && route.route.endsWith(payloadSuffix)) {
|
||||
const url = route.route.slice(0, -payloadSuffix.length) || '/'
|
||||
const rules = defu({}, ...routeRulesMatcher.matchAll(url).reverse()) as Record<string, any>
|
||||
if (!rules.prerender) {
|
||||
prerenderedRoutes.add(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
id: buildId,
|
||||
timestamp: buildTimestamp,
|
||||
matcher: exportMatcher(routeRulesMatcher),
|
||||
prerendered: nuxt.options.dev ? [] : [...prerenderedRoutes]
|
||||
}
|
||||
|
||||
await fsp.mkdir(join(tempDir, 'meta'), { recursive: true })
|
||||
await fsp.writeFile(join(tempDir, 'latest.json'), JSON.stringify({
|
||||
id: buildId,
|
||||
timestamp: buildTimestamp
|
||||
}))
|
||||
await fsp.writeFile(join(tempDir, `meta/${buildId}.json`), JSON.stringify(manifest))
|
||||
})
|
||||
}
|
||||
|
||||
// Add fallback server for `ssr: false`
|
||||
if (!nuxt.options.ssr) {
|
||||
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { join, normalize, relative, resolve } from 'pathe'
|
||||
import { createDebugger, createHooks } from 'hookable'
|
||||
import type { LoadNuxtOptions } from '@nuxt/kit'
|
||||
import { addBuildPlugin, addComponent, addPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
|
||||
import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
|
||||
import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema'
|
||||
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
@ -90,6 +90,16 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
addVitePlugin(() => ImportProtectionPlugin.vite(config))
|
||||
addWebpackPlugin(() => ImportProtectionPlugin.webpack(config))
|
||||
|
||||
if (nuxt.options.experimental.appManifest) {
|
||||
addRouteMiddleware({
|
||||
name: 'manifest-route-rule',
|
||||
path: resolve(nuxt.options.appDir, 'middleware/manifest-route-rule'),
|
||||
global: true
|
||||
})
|
||||
|
||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/check-outdated-build.client'))
|
||||
}
|
||||
|
||||
// add resolver for modules used in virtual files
|
||||
addVitePlugin(() => resolveDeepImportsPlugin(nuxt))
|
||||
|
||||
@ -295,6 +305,11 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add prerender payload support
|
||||
if (!nuxt.options.dev && nuxt.options.experimental.payloadExtraction) {
|
||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client'))
|
||||
}
|
||||
|
||||
// Add experimental cross-origin prefetch support using Speculation Rules API
|
||||
if (nuxt.options.experimental.crossOriginPrefetch) {
|
||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/cross-origin-prefetch.client'))
|
||||
|
@ -337,6 +337,8 @@ export const nuxtConfigTemplate = {
|
||||
...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
|
||||
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
|
||||
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
|
||||
`export const payloadExtraction = ${!!ctx.nuxt.options.experimental.payloadExtraction}`,
|
||||
`export const appManifest = ${!!ctx.nuxt.options.experimental.appManifest}`,
|
||||
`export const remoteComponentIslands = ${ctx.nuxt.options.experimental.componentIslands === 'local+remote'}`,
|
||||
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
|
||||
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
|
||||
|
@ -59,6 +59,8 @@ const appPreset = defineUnimportPreset({
|
||||
'loadPayload',
|
||||
'preloadPayload',
|
||||
'isPrerendered',
|
||||
'getAppManifest',
|
||||
'getRouteRules',
|
||||
'definePayloadReducer',
|
||||
'definePayloadReviver',
|
||||
'requestIdleCallback',
|
||||
|
@ -474,7 +474,9 @@ export default defineUntypedSchema({
|
||||
*
|
||||
* @type {typeof import('../src/types/config').AppConfig}
|
||||
*/
|
||||
appConfig: {},
|
||||
appConfig: {
|
||||
nuxt: {}
|
||||
},
|
||||
|
||||
$schema: {}
|
||||
})
|
||||
|
@ -120,11 +120,11 @@ export default defineUntypedSchema({
|
||||
noVueServer: false,
|
||||
|
||||
/**
|
||||
* When this option is enabled (by default) payload of pages generated with `nuxt generate` are extracted
|
||||
* When this option is enabled (by default) payload of pages that are prerendered are extracted
|
||||
*
|
||||
* @type {boolean | undefined}
|
||||
*/
|
||||
payloadExtraction: undefined,
|
||||
payloadExtraction: true,
|
||||
|
||||
/**
|
||||
* Whether to enable the experimental `<NuxtClientFallback>` component for rendering content on the client
|
||||
@ -207,6 +207,17 @@ export default defineUntypedSchema({
|
||||
/** Enable the new experimental typed router using [unplugin-vue-router](https://github.com/posva/unplugin-vue-router). */
|
||||
typedPages: false,
|
||||
|
||||
/**
|
||||
* Use app manifests to respect route rules on client-side.
|
||||
*/
|
||||
// TODO: enable by default in v3.8
|
||||
appManifest: false,
|
||||
|
||||
// This is enabled when `experimental.payloadExtraction` is set to `true`.
|
||||
// appManifest: {
|
||||
// $resolve: (val, get) => val ?? get('experimental.payloadExtraction')
|
||||
// },
|
||||
|
||||
/**
|
||||
* Set an alternative watcher that will be used as the watching service for Nuxt.
|
||||
*
|
||||
|
@ -356,6 +356,9 @@ importers:
|
||||
pkg-types:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
radix3:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
scule:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
|
@ -11,6 +11,7 @@ import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro
|
||||
import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils'
|
||||
|
||||
const isWebpack = process.env.TEST_BUILDER === 'webpack'
|
||||
const isTestingAppManifest = process.env.TEST_MANIFEST === 'manifest-on'
|
||||
|
||||
await setup({
|
||||
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
|
||||
@ -1622,18 +1623,24 @@ describe('app config', () => {
|
||||
it('should work', async () => {
|
||||
const html = await $fetch('/app-config')
|
||||
|
||||
const expectedAppConfig = {
|
||||
const expectedAppConfig: Record<string, any> = {
|
||||
fromNuxtConfig: true,
|
||||
nested: {
|
||||
val: 2
|
||||
},
|
||||
nuxt: {},
|
||||
fromLayer: true,
|
||||
userConfig: 123
|
||||
}
|
||||
|
||||
expect(html).toContain(JSON.stringify(expectedAppConfig))
|
||||
if (isTestingAppManifest) {
|
||||
expectedAppConfig.nuxt.buildId = 'test'
|
||||
}
|
||||
expect.soft(html.replace(/"nuxt":\{"buildId":"[^"]+"\}/, '"nuxt":{"buildId":"test"}')).toContain(JSON.stringify(expectedAppConfig))
|
||||
|
||||
const serverAppConfig = await $fetch('/api/app-config')
|
||||
if (isTestingAppManifest) {
|
||||
serverAppConfig.appConfig.nuxt.buildId = 'test'
|
||||
}
|
||||
expect(serverAppConfig).toMatchObject({ appConfig: expectedAppConfig })
|
||||
})
|
||||
})
|
||||
|
@ -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('"96.1k"')
|
||||
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.2k"')
|
||||
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||
[
|
||||
"_nuxt/entry.js",
|
||||
|
1
test/fixtures/basic-types/nuxt.config.ts
vendored
1
test/fixtures/basic-types/nuxt.config.ts
vendored
@ -3,6 +3,7 @@ import { addTypeTemplate } from 'nuxt/kit'
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
typedPages: true,
|
||||
appManifest: true,
|
||||
typescriptBundlerResolution: process.env.MODULE_RESOLUTION === 'bundler'
|
||||
},
|
||||
buildDir: process.env.NITRO_BUILD_DIR,
|
||||
|
1
test/fixtures/basic-types/types.ts
vendored
1
test/fixtures/basic-types/types.ts
vendored
@ -425,6 +425,7 @@ describe('composables', () => {
|
||||
describe('app config', () => {
|
||||
it('merges app config as expected', () => {
|
||||
interface ExpectedMergedAppConfig {
|
||||
nuxt: { buildId: string }
|
||||
fromLayer: boolean
|
||||
fromNuxtConfig: boolean
|
||||
nested: {
|
||||
|
7
test/fixtures/basic/nuxt.config.ts
vendored
7
test/fixtures/basic/nuxt.config.ts
vendored
@ -56,7 +56,8 @@ export default defineNuxtConfig({
|
||||
routes: [
|
||||
'/random/a',
|
||||
'/random/b',
|
||||
'/random/c'
|
||||
'/random/c',
|
||||
'/prefetch/server-components'
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -193,8 +194,10 @@ export default defineNuxtConfig({
|
||||
componentIslands: true,
|
||||
reactivityTransform: true,
|
||||
treeshakeClientOnly: true,
|
||||
payloadExtraction: true,
|
||||
asyncContext: process.env.TEST_CONTEXT === 'async',
|
||||
// TODO: remove this in v3.8
|
||||
payloadExtraction: true,
|
||||
appManifest: process.env.TEST_MANIFEST === 'manifest-on',
|
||||
headNext: true,
|
||||
inlineRouteRules: true
|
||||
},
|
||||
|
4
test/fixtures/minimal-types/nuxt.config.ts
vendored
4
test/fixtures/minimal-types/nuxt.config.ts
vendored
@ -1 +1,3 @@
|
||||
export default defineNuxtConfig({})
|
||||
export default defineNuxtConfig({
|
||||
experimental: { appManifest: true }
|
||||
})
|
||||
|
6
test/fixtures/minimal-types/types.ts
vendored
6
test/fixtures/minimal-types/types.ts
vendored
@ -43,6 +43,10 @@ describe('config typings', () => {
|
||||
})
|
||||
|
||||
it('appConfig', () => {
|
||||
expectTypeOf(useAppConfig()).toEqualTypeOf<{ [key: string]: unknown }>()
|
||||
expectTypeOf(useAppConfig().foo).toEqualTypeOf<unknown>()
|
||||
expectTypeOf(useAppConfig()).toEqualTypeOf<{
|
||||
nuxt: { buildId: string }
|
||||
[key: string]: unknown
|
||||
}>()
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,9 @@
|
||||
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineEventHandler } from 'h3'
|
||||
|
||||
import { registerEndpoint } from 'nuxt-vitest/utils'
|
||||
|
||||
import * as composables from '#app/composables'
|
||||
|
||||
@ -10,11 +13,24 @@ import { onNuxtReady } from '#app/composables/ready'
|
||||
import { setResponseStatus, useRequestEvent, useRequestFetch, useRequestHeaders } from '#app/composables/ssr'
|
||||
import { clearNuxtState, useState } from '#app/composables/state'
|
||||
import { useRequestURL } from '#app/composables/url'
|
||||
import { getAppManifest, getRouteRules } from '#app/composables/manifest'
|
||||
|
||||
vi.mock('#app/compat/idle-callback', () => ({
|
||||
requestIdleCallback: (cb: Function) => cb()
|
||||
}))
|
||||
|
||||
const timestamp = Date.now()
|
||||
registerEndpoint('/_nuxt/builds/latest.json', defineEventHandler(() => ({
|
||||
id: 'test',
|
||||
timestamp
|
||||
})))
|
||||
registerEndpoint('/_nuxt/builds/meta/test.json', defineEventHandler(() => ({
|
||||
id: 'test',
|
||||
timestamp,
|
||||
matcher: { static: { '/': null, '/pre': null }, wildcard: { '/pre': { prerender: true } }, dynamic: {} },
|
||||
prerendered: ['/specific-prerendered']
|
||||
})))
|
||||
|
||||
describe('composables', () => {
|
||||
it('are all tested', () => {
|
||||
const testedComposables: string[] = [
|
||||
@ -27,10 +43,13 @@ describe('composables', () => {
|
||||
'clearError',
|
||||
'showError',
|
||||
'useError',
|
||||
'getAppManifest',
|
||||
'getRouteRules',
|
||||
'onNuxtReady',
|
||||
'setResponseStatus',
|
||||
'useRequestEvent',
|
||||
'useRequestFetch',
|
||||
'isPrerendered',
|
||||
'useRequestHeaders',
|
||||
'clearNuxtState',
|
||||
'useState',
|
||||
@ -43,7 +62,6 @@ describe('composables', () => {
|
||||
'defineNuxtRouteMiddleware',
|
||||
'definePayloadReducer',
|
||||
'definePayloadReviver',
|
||||
'isPrerendered',
|
||||
'loadPayload',
|
||||
'navigateTo',
|
||||
'onBeforeRouteLeave',
|
||||
@ -204,3 +222,39 @@ describe('url', () => {
|
||||
expect(url.protocol).toMatchInlineSnapshot('"http:"')
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(process.env.TEST_MANIFEST !== 'manifest-on')('app manifests', () => {
|
||||
it('getAppManifest', async () => {
|
||||
const manifest = await getAppManifest()
|
||||
delete manifest.timestamp
|
||||
expect(manifest).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "test",
|
||||
"matcher": {
|
||||
"dynamic": {},
|
||||
"static": {
|
||||
"/": null,
|
||||
"/pre": null,
|
||||
},
|
||||
"wildcard": {
|
||||
"/pre": {
|
||||
"prerender": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"prerendered": [
|
||||
"/specific-prerendered",
|
||||
],
|
||||
}
|
||||
`)
|
||||
})
|
||||
it('getRouteRules', async () => {
|
||||
const rules = await getRouteRules('/')
|
||||
expect(rules).toMatchInlineSnapshot('{}')
|
||||
})
|
||||
it('isPrerendered', async () => {
|
||||
expect(await isPrerendered('/specific-prerendered')).toBeTruthy()
|
||||
expect(await isPrerendered('/prerendered/test')).toBeTruthy()
|
||||
expect(await isPrerendered('/test')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
@ -1,8 +1,23 @@
|
||||
import { defineVitestConfig } from 'nuxt-vitest/config'
|
||||
|
||||
export default defineVitestConfig({
|
||||
// TODO: investigate
|
||||
define: {
|
||||
'import.meta.test': true
|
||||
},
|
||||
test: {
|
||||
dir: './test/nuxt',
|
||||
environment: 'nuxt'
|
||||
environment: 'nuxt',
|
||||
environmentOptions: {
|
||||
nuxt: {
|
||||
overrides: {
|
||||
appConfig: {
|
||||
nuxt: {
|
||||
buildId: 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user