mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +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']
|
env: ['dev', 'built']
|
||||||
builder: ['vite', 'webpack']
|
builder: ['vite', 'webpack']
|
||||||
context: ['async', 'default']
|
context: ['async', 'default']
|
||||||
|
manifest: ['manifest-on', 'manifest-off']
|
||||||
node: [18]
|
node: [18]
|
||||||
exclude:
|
exclude:
|
||||||
- env: 'dev'
|
- env: 'dev'
|
||||||
builder: 'webpack'
|
builder: 'webpack'
|
||||||
|
- manifest: 'manifest-off'
|
||||||
|
builder: 'webpack'
|
||||||
|
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
@ -231,6 +234,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TEST_ENV: ${{ matrix.env }}
|
TEST_ENV: ${{ matrix.env }}
|
||||||
TEST_BUILDER: ${{ matrix.builder }}
|
TEST_BUILDER: ${{ matrix.builder }}
|
||||||
|
TEST_MANIFEST: ${{ matrix.manifest }}
|
||||||
TEST_CONTEXT: ${{ matrix.context }}
|
TEST_CONTEXT: ${{ matrix.context }}
|
||||||
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }}
|
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": "nuxi dev playground",
|
||||||
"play:build": "nuxi build playground",
|
"play:build": "nuxi build playground",
|
||||||
"play:preview": "nuxi preview 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": "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:dev": "TEST_ENV=dev pnpm test:fixtures",
|
||||||
"test:fixtures:webpack": "TEST_BUILDER=webpack 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_VERSION__: string
|
||||||
var __NUXT_PREPATHS__: string[] | string | undefined
|
var __NUXT_PREPATHS__: string[] | string | undefined
|
||||||
var __NUXT_PATHS__: 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 {}
|
export {}
|
||||||
|
@ -89,6 +89,7 @@
|
|||||||
"pathe": "^1.1.1",
|
"pathe": "^1.1.1",
|
||||||
"perfect-debounce": "^1.0.0",
|
"perfect-debounce": "^1.0.0",
|
||||||
"pkg-types": "^1.0.3",
|
"pkg-types": "^1.0.3",
|
||||||
|
"radix3": "^1.1.0",
|
||||||
"scule": "^1.0.0",
|
"scule": "^1.0.0",
|
||||||
"std-env": "^3.4.3",
|
"std-env": "^3.4.3",
|
||||||
"strip-literal": "^1.3.0",
|
"strip-literal": "^1.3.0",
|
||||||
|
@ -29,6 +29,8 @@ export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBefor
|
|||||||
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
|
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
|
||||||
export { preloadComponents, prefetchComponents, preloadRouteComponents } from './preload'
|
export { preloadComponents, prefetchComponents, preloadRouteComponents } from './preload'
|
||||||
export { isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver } from './payload'
|
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 type { ReloadNuxtAppOptions } from './chunk'
|
||||||
export { reloadNuxtApp } from './chunk'
|
export { reloadNuxtApp } from './chunk'
|
||||||
export { useRequestURL } from './url'
|
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 { getCurrentInstance } from 'vue'
|
||||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||||
|
|
||||||
|
import { getAppManifest, getRouteRules } from '#app/composables/manifest'
|
||||||
|
import { useRoute } from '#app/composables'
|
||||||
|
|
||||||
// @ts-expect-error virtual import
|
// @ts-expect-error virtual import
|
||||||
import { renderJsonPayloads } from '#build/nuxt.config.mjs'
|
import { appManifest, payloadExtraction, renderJsonPayloads } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
interface LoadPayloadOptions {
|
interface LoadPayloadOptions {
|
||||||
fresh?: boolean
|
fresh?: boolean
|
||||||
@ -13,19 +16,24 @@ interface LoadPayloadOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record<string, any> | Promise<Record<string, any>> | null {
|
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 payloadURL = _getPayloadURL(url, opts)
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
|
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
|
||||||
if (cache[payloadURL]) {
|
if (payloadURL in cache) {
|
||||||
return cache[payloadURL]
|
return cache[payloadURL]
|
||||||
}
|
}
|
||||||
cache[payloadURL] = _importPayload(payloadURL).then((payload) => {
|
cache[payloadURL] = isPrerendered().then((prerendered) => {
|
||||||
if (!payload) {
|
if (!prerendered) {
|
||||||
delete cache[payloadURL]
|
cache[payloadURL] = null
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return payload
|
return _importPayload(payloadURL).then((payload) => {
|
||||||
|
if (payload) { return payload }
|
||||||
|
|
||||||
|
delete cache[payloadURL]
|
||||||
|
return null
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return cache[payloadURL]
|
return cache[payloadURL]
|
||||||
}
|
}
|
||||||
@ -55,7 +63,7 @@ function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function _importPayload (payloadURL: string) {
|
async function _importPayload (payloadURL: string) {
|
||||||
if (import.meta.server) { return null }
|
if (import.meta.server || !payloadExtraction) { return null }
|
||||||
const payloadPromise = renderJsonPayloads
|
const payloadPromise = renderJsonPayloads
|
||||||
? fetch(payloadURL).then(res => res.text().then(parsePayload))
|
? fetch(payloadURL).then(res => res.text().then(parsePayload))
|
||||||
: import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r)
|
: import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r)
|
||||||
@ -68,10 +76,19 @@ async function _importPayload (payloadURL: string) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPrerendered () {
|
export async function isPrerendered (url = useRoute().path) {
|
||||||
// Note: Alternative for server is checking x-nitro-prerender header
|
// Note: Alternative for server is checking x-nitro-prerender header
|
||||||
const nuxtApp = useNuxtApp()
|
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
|
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 { RouteMiddleware } from '../../app'
|
||||||
import type { NuxtError } from '../app/composables/error'
|
import type { NuxtError } from '../app/composables/error'
|
||||||
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
||||||
|
import type { NuxtAppManifestMeta } from '#app/composables'
|
||||||
|
|
||||||
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app', {
|
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app', {
|
||||||
asyncContext: !!process.env.NUXT_ASYNC_CONTEXT && process.server
|
asyncContext: !!process.env.NUXT_ASYNC_CONTEXT && process.server
|
||||||
@ -35,6 +36,7 @@ export interface RuntimeNuxtHooks {
|
|||||||
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
'app:error:cleared': (options: { redirect?: string }) => HookResult
|
||||||
'app:chunkError': (options: { error: any }) => HookResult
|
'app:chunkError': (options: { error: any }) => HookResult
|
||||||
'app:data:refresh': (keys?: string[]) => HookResult
|
'app:data:refresh': (keys?: string[]) => HookResult
|
||||||
|
'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult
|
||||||
'link:prefetch': (link: string) => HookResult
|
'link:prefetch': (link: string) => HookResult
|
||||||
'page:start': (Component?: VNode) => HookResult
|
'page:start': (Component?: VNode) => HookResult
|
||||||
'page:finish': (Component?: VNode) => HookResult
|
'page:finish': (Component?: VNode) => HookResult
|
||||||
@ -115,7 +117,7 @@ interface _NuxtApp {
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
_observer?: { observe: (element: Element, callback: () => void) => () => void }
|
_observer?: { observe: (element: Element, callback: () => void) => () => void }
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_payloadCache?: Record<string, Promise<Record<string, any>> | Record<string, any>>
|
_payloadCache?: Record<string, Promise<Record<string, any>> | Record<string, any> | null>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_appConfig: AppConfig
|
_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 { joinURL } from 'ufo'
|
||||||
|
import type { RouteLocationNormalized } from 'vue-router'
|
||||||
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
|
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
|
||||||
import { useRouter } from '#app/composables/router'
|
import { useRouter } from '#app/composables/router'
|
||||||
import { reloadNuxtApp } from '#app/composables/chunk'
|
import { reloadNuxtApp } from '#app/composables/chunk'
|
||||||
@ -14,11 +15,19 @@ export default defineNuxtPlugin({
|
|||||||
router.beforeEach(() => { chunkErrors.clear() })
|
router.beforeEach(() => { chunkErrors.clear() })
|
||||||
nuxtApp.hook('app:chunkError', ({ error }) => { chunkErrors.add(error) })
|
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) => {
|
router.onError((error, to) => {
|
||||||
if (chunkErrors.has(error)) {
|
if (chunkErrors.has(error)) {
|
||||||
const isHash = 'href' in to && (to.href as string).startsWith('#')
|
reloadAppAtPath(to)
|
||||||
const path = isHash ? config.app.baseURL + (to as any).href : joinURL(config.app.baseURL, to.fullPath)
|
|
||||||
reloadNuxtApp({ path, persistState: true })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import { parseURL } from 'ufo'
|
import { parseURL } from 'ufo'
|
||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
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 { 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({
|
export default defineNuxtPlugin({
|
||||||
name: 'nuxt:payload',
|
name: 'nuxt:payload',
|
||||||
setup (nuxtApp) {
|
setup (nuxtApp) {
|
||||||
// Only enable behavior if initial page is prerendered
|
// TODO: Support dev
|
||||||
// TODO: Support hybrid and dev
|
if (process.dev) { return }
|
||||||
if (!isPrerendered()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load payload into cache
|
|
||||||
nuxtApp.hooks.hook('link:prefetch', async (url) => {
|
|
||||||
if (!parseURL(url).protocol) {
|
|
||||||
await loadPayload(url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Load payload after middleware & once final route is resolved
|
// Load payload after middleware & once final route is resolved
|
||||||
useRouter().beforeResolve(async (to, from) => {
|
useRouter().beforeResolve(async (to, from) => {
|
||||||
@ -26,5 +20,17 @@ export default defineNuxtPlugin({
|
|||||||
if (!payload) { return }
|
if (!payload) { return }
|
||||||
Object.assign(nuxtApp.static.data, payload.data)
|
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 { existsSync, promises as fsp, readFileSync } from 'node:fs'
|
||||||
import { cpus } from 'node:os'
|
import { cpus } from 'node:os'
|
||||||
import { join, relative, resolve } from 'pathe'
|
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 { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack'
|
||||||
import type { Nitro, NitroConfig } from 'nitropack'
|
import type { Nitro, NitroConfig } from 'nitropack'
|
||||||
import { logger, resolveIgnorePatterns } from '@nuxt/kit'
|
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.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
|
||||||
nitroConfig.ignore = [...(nitroConfig.ignore || []), ...resolveIgnorePatterns(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`
|
// Add fallback server for `ssr: false`
|
||||||
if (!nuxt.options.ssr) {
|
if (!nuxt.options.ssr) {
|
||||||
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { join, normalize, relative, resolve } from 'pathe'
|
import { join, normalize, relative, resolve } from 'pathe'
|
||||||
import { createDebugger, createHooks } from 'hookable'
|
import { createDebugger, createHooks } from 'hookable'
|
||||||
import type { LoadNuxtOptions } from '@nuxt/kit'
|
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 type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema'
|
||||||
|
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
@ -90,6 +90,16 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
addVitePlugin(() => ImportProtectionPlugin.vite(config))
|
addVitePlugin(() => ImportProtectionPlugin.vite(config))
|
||||||
addWebpackPlugin(() => ImportProtectionPlugin.webpack(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
|
// add resolver for modules used in virtual files
|
||||||
addVitePlugin(() => resolveDeepImportsPlugin(nuxt))
|
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
|
// Add experimental cross-origin prefetch support using Speculation Rules API
|
||||||
if (nuxt.options.experimental.crossOriginPrefetch) {
|
if (nuxt.options.experimental.crossOriginPrefetch) {
|
||||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/cross-origin-prefetch.client'))
|
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)}`),
|
...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 renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
|
||||||
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
|
`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 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 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'}`,
|
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
|
||||||
|
@ -59,6 +59,8 @@ const appPreset = defineUnimportPreset({
|
|||||||
'loadPayload',
|
'loadPayload',
|
||||||
'preloadPayload',
|
'preloadPayload',
|
||||||
'isPrerendered',
|
'isPrerendered',
|
||||||
|
'getAppManifest',
|
||||||
|
'getRouteRules',
|
||||||
'definePayloadReducer',
|
'definePayloadReducer',
|
||||||
'definePayloadReviver',
|
'definePayloadReviver',
|
||||||
'requestIdleCallback',
|
'requestIdleCallback',
|
||||||
|
@ -474,7 +474,9 @@ export default defineUntypedSchema({
|
|||||||
*
|
*
|
||||||
* @type {typeof import('../src/types/config').AppConfig}
|
* @type {typeof import('../src/types/config').AppConfig}
|
||||||
*/
|
*/
|
||||||
appConfig: {},
|
appConfig: {
|
||||||
|
nuxt: {}
|
||||||
|
},
|
||||||
|
|
||||||
$schema: {}
|
$schema: {}
|
||||||
})
|
})
|
||||||
|
@ -120,11 +120,11 @@ export default defineUntypedSchema({
|
|||||||
noVueServer: false,
|
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}
|
* @type {boolean | undefined}
|
||||||
*/
|
*/
|
||||||
payloadExtraction: undefined,
|
payloadExtraction: true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to enable the experimental `<NuxtClientFallback>` component for rendering content on the client
|
* 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). */
|
/** Enable the new experimental typed router using [unplugin-vue-router](https://github.com/posva/unplugin-vue-router). */
|
||||||
typedPages: false,
|
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.
|
* Set an alternative watcher that will be used as the watching service for Nuxt.
|
||||||
*
|
*
|
||||||
|
@ -356,6 +356,9 @@ importers:
|
|||||||
pkg-types:
|
pkg-types:
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
|
radix3:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
scule:
|
scule:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 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'
|
import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils'
|
||||||
|
|
||||||
const isWebpack = process.env.TEST_BUILDER === 'webpack'
|
const isWebpack = process.env.TEST_BUILDER === 'webpack'
|
||||||
|
const isTestingAppManifest = process.env.TEST_MANIFEST === 'manifest-on'
|
||||||
|
|
||||||
await setup({
|
await setup({
|
||||||
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
|
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
|
||||||
@ -1622,18 +1623,24 @@ describe('app config', () => {
|
|||||||
it('should work', async () => {
|
it('should work', async () => {
|
||||||
const html = await $fetch('/app-config')
|
const html = await $fetch('/app-config')
|
||||||
|
|
||||||
const expectedAppConfig = {
|
const expectedAppConfig: Record<string, any> = {
|
||||||
fromNuxtConfig: true,
|
fromNuxtConfig: true,
|
||||||
nested: {
|
nested: {
|
||||||
val: 2
|
val: 2
|
||||||
},
|
},
|
||||||
|
nuxt: {},
|
||||||
fromLayer: true,
|
fromLayer: true,
|
||||||
userConfig: 123
|
userConfig: 123
|
||||||
}
|
}
|
||||||
|
if (isTestingAppManifest) {
|
||||||
expect(html).toContain(JSON.stringify(expectedAppConfig))
|
expectedAppConfig.nuxt.buildId = 'test'
|
||||||
|
}
|
||||||
|
expect.soft(html.replace(/"nuxt":\{"buildId":"[^"]+"\}/, '"nuxt":{"buildId":"test"}')).toContain(JSON.stringify(expectedAppConfig))
|
||||||
|
|
||||||
const serverAppConfig = await $fetch('/api/app-config')
|
const serverAppConfig = await $fetch('/api/app-config')
|
||||||
|
if (isTestingAppManifest) {
|
||||||
|
serverAppConfig.appConfig.nuxt.buildId = 'test'
|
||||||
|
}
|
||||||
expect(serverAppConfig).toMatchObject({ appConfig: expectedAppConfig })
|
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']) {
|
for (const outputDir of ['.output', '.output-inline']) {
|
||||||
it('default client bundle size', async () => {
|
it('default client bundle size', async () => {
|
||||||
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
|
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(`
|
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"_nuxt/entry.js",
|
"_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({
|
export default defineNuxtConfig({
|
||||||
experimental: {
|
experimental: {
|
||||||
typedPages: true,
|
typedPages: true,
|
||||||
|
appManifest: true,
|
||||||
typescriptBundlerResolution: process.env.MODULE_RESOLUTION === 'bundler'
|
typescriptBundlerResolution: process.env.MODULE_RESOLUTION === 'bundler'
|
||||||
},
|
},
|
||||||
buildDir: process.env.NITRO_BUILD_DIR,
|
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', () => {
|
describe('app config', () => {
|
||||||
it('merges app config as expected', () => {
|
it('merges app config as expected', () => {
|
||||||
interface ExpectedMergedAppConfig {
|
interface ExpectedMergedAppConfig {
|
||||||
|
nuxt: { buildId: string }
|
||||||
fromLayer: boolean
|
fromLayer: boolean
|
||||||
fromNuxtConfig: boolean
|
fromNuxtConfig: boolean
|
||||||
nested: {
|
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: [
|
routes: [
|
||||||
'/random/a',
|
'/random/a',
|
||||||
'/random/b',
|
'/random/b',
|
||||||
'/random/c'
|
'/random/c',
|
||||||
|
'/prefetch/server-components'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -193,8 +194,10 @@ export default defineNuxtConfig({
|
|||||||
componentIslands: true,
|
componentIslands: true,
|
||||||
reactivityTransform: true,
|
reactivityTransform: true,
|
||||||
treeshakeClientOnly: true,
|
treeshakeClientOnly: true,
|
||||||
payloadExtraction: true,
|
|
||||||
asyncContext: process.env.TEST_CONTEXT === 'async',
|
asyncContext: process.env.TEST_CONTEXT === 'async',
|
||||||
|
// TODO: remove this in v3.8
|
||||||
|
payloadExtraction: true,
|
||||||
|
appManifest: process.env.TEST_MANIFEST === 'manifest-on',
|
||||||
headNext: true,
|
headNext: true,
|
||||||
inlineRouteRules: 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', () => {
|
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" />
|
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
|
||||||
|
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { defineEventHandler } from 'h3'
|
||||||
|
|
||||||
|
import { registerEndpoint } from 'nuxt-vitest/utils'
|
||||||
|
|
||||||
import * as composables from '#app/composables'
|
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 { setResponseStatus, useRequestEvent, useRequestFetch, useRequestHeaders } from '#app/composables/ssr'
|
||||||
import { clearNuxtState, useState } from '#app/composables/state'
|
import { clearNuxtState, useState } from '#app/composables/state'
|
||||||
import { useRequestURL } from '#app/composables/url'
|
import { useRequestURL } from '#app/composables/url'
|
||||||
|
import { getAppManifest, getRouteRules } from '#app/composables/manifest'
|
||||||
|
|
||||||
vi.mock('#app/compat/idle-callback', () => ({
|
vi.mock('#app/compat/idle-callback', () => ({
|
||||||
requestIdleCallback: (cb: Function) => cb()
|
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', () => {
|
describe('composables', () => {
|
||||||
it('are all tested', () => {
|
it('are all tested', () => {
|
||||||
const testedComposables: string[] = [
|
const testedComposables: string[] = [
|
||||||
@ -27,10 +43,13 @@ describe('composables', () => {
|
|||||||
'clearError',
|
'clearError',
|
||||||
'showError',
|
'showError',
|
||||||
'useError',
|
'useError',
|
||||||
|
'getAppManifest',
|
||||||
|
'getRouteRules',
|
||||||
'onNuxtReady',
|
'onNuxtReady',
|
||||||
'setResponseStatus',
|
'setResponseStatus',
|
||||||
'useRequestEvent',
|
'useRequestEvent',
|
||||||
'useRequestFetch',
|
'useRequestFetch',
|
||||||
|
'isPrerendered',
|
||||||
'useRequestHeaders',
|
'useRequestHeaders',
|
||||||
'clearNuxtState',
|
'clearNuxtState',
|
||||||
'useState',
|
'useState',
|
||||||
@ -43,7 +62,6 @@ describe('composables', () => {
|
|||||||
'defineNuxtRouteMiddleware',
|
'defineNuxtRouteMiddleware',
|
||||||
'definePayloadReducer',
|
'definePayloadReducer',
|
||||||
'definePayloadReviver',
|
'definePayloadReviver',
|
||||||
'isPrerendered',
|
|
||||||
'loadPayload',
|
'loadPayload',
|
||||||
'navigateTo',
|
'navigateTo',
|
||||||
'onBeforeRouteLeave',
|
'onBeforeRouteLeave',
|
||||||
@ -204,3 +222,39 @@ describe('url', () => {
|
|||||||
expect(url.protocol).toMatchInlineSnapshot('"http:"')
|
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'
|
import { defineVitestConfig } from 'nuxt-vitest/config'
|
||||||
|
|
||||||
export default defineVitestConfig({
|
export default defineVitestConfig({
|
||||||
|
// TODO: investigate
|
||||||
|
define: {
|
||||||
|
'import.meta.test': true
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
dir: './test/nuxt',
|
dir: './test/nuxt',
|
||||||
environment: 'nuxt'
|
environment: 'nuxt',
|
||||||
|
environmentOptions: {
|
||||||
|
nuxt: {
|
||||||
|
overrides: {
|
||||||
|
appConfig: {
|
||||||
|
nuxt: {
|
||||||
|
buildId: 'test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user