feat(nuxt,schema): add appId and improve chunk determinism (#27258)

This commit is contained in:
Daniel Roe 2024-05-16 22:41:31 -05:00 committed by GitHub
parent dad89c2b16
commit 3c42e13b68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 35 additions and 41 deletions

View File

@ -1,7 +1,6 @@
import type { MatcherExport, RouteMatcher } from 'radix3' import type { MatcherExport, RouteMatcher } from 'radix3'
import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3' import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import { defu } from 'defu' import { defu } from 'defu'
import { useAppConfig } from '../config'
import { useRuntimeConfig } from '../nuxt' import { useRuntimeConfig } from '../nuxt'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs' import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
@ -25,9 +24,7 @@ function fetchManifest () {
if (!isAppManifestEnabled) { if (!isAppManifestEnabled) {
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`') throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
} }
// @ts-expect-error private property manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${useRuntimeConfig().app.buildId}.json`))
const buildId = useAppConfig().nuxt?.buildId
manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${buildId}.json`))
manifest.then((m) => { manifest.then((m) => {
matcher = createMatcherFromExport(m.matcher) matcher = createMatcherFromExport(m.matcher)
}) })

View File

@ -3,7 +3,6 @@ import { parse } from 'devalue'
import { useHead } from '@unhead/vue' import { useHead } from '@unhead/vue'
import { getCurrentInstance } from 'vue' import { getCurrentInstance } from 'vue'
import { useNuxtApp, useRuntimeConfig } from '../nuxt' import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import { useAppConfig } from '../config'
import { useRoute } from './router' import { useRoute } from './router'
import { getAppManifest, getRouteRules } from './manifest' import { getAppManifest, getRouteRules } from './manifest'
@ -57,8 +56,9 @@ function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) { if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) {
throw new Error('Payload URL must not include hostname: ' + url) throw new Error('Payload URL must not include hostname: ' + url)
} }
const hash = opts.hash || (opts.fresh ? Date.now() : (useAppConfig().nuxt as any)?.buildId) const config = useRuntimeConfig()
return joinURL(useRuntimeConfig().app.baseURL, u.pathname, filename + (hash ? `?${hash}` : '')) const hash = opts.hash || (opts.fresh ? Date.now() : config.app.buildId)
return joinURL(config.app.baseURL, u.pathname, filename + (hash ? `?${hash}` : ''))
} }
async function _importPayload (payloadURL: string) { async function _importPayload (payloadURL: string) {

View File

@ -20,13 +20,13 @@ import type { LoadingIndicator } from '../app/composables/loading-indicator'
import type { RouteAnnouncer } from '../app/composables/route-announcer' import type { RouteAnnouncer } from '../app/composables/route-announcer'
import type { ViewTransition } from './plugins/view-transitions.client' import type { ViewTransition } from './plugins/view-transitions.client'
// @ts-expect-error virtual file
import { appId } from '#build/nuxt.config.mjs'
import type { NuxtAppLiterals } from '#app' import type { NuxtAppLiterals } from '#app'
// @ts-expect-error virtual import function getNuxtAppCtx (appName = appId || 'nuxt-app') {
import { buildId } from '#build/nuxt.config.mjs' return getContext<NuxtApp>(appName, {
function getNuxtAppCtx (appName?: string) {
return getContext<NuxtApp>(appName || buildId || 'nuxt-app', {
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server, asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
}) })
} }
@ -244,7 +244,7 @@ export interface CreateOptions {
export function createNuxtApp (options: CreateOptions) { export function createNuxtApp (options: CreateOptions) {
let hydratingCount = 0 let hydratingCount = 0
const nuxtApp: NuxtApp = { const nuxtApp: NuxtApp = {
name: buildId, _name: appId || 'nuxt-app',
_scope: effectScope(), _scope: effectScope(),
provide: undefined, provide: undefined,
globalName: 'nuxt', globalName: 'nuxt',

View File

@ -218,8 +218,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Add app manifest handler and prerender configuration // Add app manifest handler and prerender configuration
if (nuxt.options.experimental.appManifest) { if (nuxt.options.experimental.appManifest) {
// @ts-expect-error untyped nuxt property const buildId = nuxt.options.runtimeConfig.app.buildId ||=
const buildId = nuxt.options.appConfig.nuxt!.buildId ||=
(nuxt.options.dev ? 'dev' : nuxt.options.test ? 'test' : nuxt.options.buildId) (nuxt.options.dev ? 'dev' : nuxt.options.test ? 'test' : nuxt.options.buildId)
const buildTimestamp = Date.now() const buildTimestamp = Date.now()

View File

@ -21,7 +21,7 @@ import type { HeadEntryOptions } from '@unhead/schema'
import type { Link, Script, Style } from '@unhead/vue' import type { Link, Script, Style } from '@unhead/vue'
import { createServerHead } from '@unhead/vue' import { createServerHead } from '@unhead/vue'
import { defineRenderHandler, getRouteRules, useAppConfig, useRuntimeConfig, useStorage } from '#internal/nitro' import { defineRenderHandler, getRouteRules, useRuntimeConfig, useStorage } from '#internal/nitro'
import { useNitroApp } from '#internal/nitro/app' import { useNitroApp } from '#internal/nitro/app'
// @ts-expect-error virtual file // @ts-expect-error virtual file
@ -327,7 +327,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Whether we are prerendering route // Whether we are prerendering route
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + (useAppConfig().nuxt as any)?.buildId : undefined const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + ssrContext.runtimeConfig.app.buildId : undefined
if (import.meta.prerender) { if (import.meta.prerender) {
ssrContext.payload.prerenderedAt = Date.now() ssrContext.payload.prerenderedAt = Date.now()
} }

View File

@ -397,7 +397,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`, `export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`,
`export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`, `export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`,
`export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`, `export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
`export const buildId = ${JSON.stringify(ctx.nuxt.options.buildId)}`, `export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`,
].join('\n\n') ].join('\n\n')
}, },
} }

View File

@ -154,6 +154,13 @@ export default defineUntypedSchema({
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.nuxt'), $resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.nuxt'),
}, },
/**
* For multi-app projects, the unique name of the Nuxt application.
*/
appId: {
$resolve: (val: string) => val ?? 'nuxt-app',
},
/** /**
* A unique identifier matching the build. This may contain the hash of the current state of the project. * A unique identifier matching the build. This may contain the hash of the current state of the project.
*/ */
@ -528,11 +535,12 @@ export default defineUntypedSchema({
*/ */
runtimeConfig: { runtimeConfig: {
$resolve: async (val: RuntimeConfig, get): Promise<Record<string, unknown>> => { $resolve: async (val: RuntimeConfig, get): Promise<Record<string, unknown>> => {
const app = await get('app') as Record<string, string> const [app, buildId] = await Promise.all([get('app') as Promise<Record<string, string>>, get('buildId') as Promise<string>])
provideFallbackValues(val) provideFallbackValues(val)
return defu(val, { return defu(val, {
public: {}, public: {},
app: { app: {
buildId,
baseURL: app.baseURL, baseURL: app.baseURL,
buildAssetsDir: app.buildAssetsDir, buildAssetsDir: app.buildAssetsDir,
cdnURL: app.cdnURL, cdnURL: app.cdnURL,

View File

@ -2028,15 +2028,9 @@ describe('app config', () => {
fromLayer: true, fromLayer: true,
userConfig: 123, userConfig: 123,
} }
if (isTestingAppManifest) { expect.soft(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 })
}) })
}) })
@ -2649,23 +2643,23 @@ describe('defineNuxtComponent watch duplicate', () => {
}) })
describe('namespace access to useNuxtApp', () => { describe('namespace access to useNuxtApp', () => {
it('should return the nuxt instance when used with correct buildId', async () => { it('should return the nuxt instance when used with correct appId', async () => {
const { page, pageErrors } = await renderPage('/namespace-nuxt-app') const { page, pageErrors } = await renderPage('/namespace-nuxt-app')
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating) await page.waitForFunction(() => window.useNuxtApp?.() && !window.useNuxtApp?.().isHydrating)
// Defaulting to buildId // Defaulting to appId
await page.evaluate(() => window.useNuxtApp?.()) await page.evaluate(() => window.useNuxtApp?.())
// Using correct configured buildId // Using correct configured appId
// @ts-expect-error not public API yet // @ts-expect-error not public API yet
await page.evaluate(() => window.useNuxtApp?.('nuxt-app-basic')) await page.evaluate(() => window.useNuxtApp?.('nuxt-app-basic'))
await page.close() await page.close()
}) })
it('should throw an error when used with wrong buildId', async () => { it('should throw an error when used with wrong appId', async () => {
const { page, pageErrors } = await renderPage('/namespace-nuxt-app') const { page, pageErrors } = await renderPage('/namespace-nuxt-app')
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
@ -2674,7 +2668,7 @@ describe('namespace access to useNuxtApp', () => {
let error: unknown let error: unknown
try { try {
// Using wrong/unknown buildId // Using wrong/unknown appId
// @ts-expect-error not public API yet // @ts-expect-error not public API yet
await page.evaluate(() => window.useNuxtApp?.('nuxt-app-unknown')) await page.evaluate(() => window.useNuxtApp?.('nuxt-app-unknown'))
} catch (err) { } catch (err) {

View File

@ -534,7 +534,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 } nuxt: {}
fromLayer: boolean fromLayer: boolean
fromNuxtConfig: boolean fromNuxtConfig: boolean
nested: { nested: {

View File

@ -32,7 +32,7 @@ export default defineNuxtConfig({
}, },
buildDir: process.env.NITRO_BUILD_DIR, buildDir: process.env.NITRO_BUILD_DIR,
builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite', builder: process.env.TEST_BUILDER as 'webpack' | 'vite' ?? 'vite',
buildId: 'nuxt-app-basic', appId: 'nuxt-app-basic',
build: { build: {
transpile: [ transpile: [
(ctx) => { (ctx) => {

View File

@ -51,7 +51,7 @@ describe('config typings', () => {
it('appConfig', () => { it('appConfig', () => {
expectTypeOf(useAppConfig().foo).toEqualTypeOf<unknown>() expectTypeOf(useAppConfig().foo).toEqualTypeOf<unknown>()
expectTypeOf(useAppConfig()).toEqualTypeOf<{ expectTypeOf(useAppConfig()).toEqualTypeOf<{
nuxt: { buildId: string } nuxt: {}
[key: string]: unknown [key: string]: unknown
}>() }>()
}) })

View File

@ -30,9 +30,7 @@ describe('app config', () => {
const appConfig = useAppConfig() const appConfig = useAppConfig()
expect(appConfig).toMatchInlineSnapshot(` expect(appConfig).toMatchInlineSnapshot(`
{ {
"nuxt": { "nuxt": {},
"buildId": "override",
},
} }
`) `)
updateAppConfig({ updateAppConfig({
@ -44,7 +42,6 @@ describe('app config', () => {
{ {
"new": "value", "new": "value",
"nuxt": { "nuxt": {
"buildId": "override",
"nested": 42, "nested": 42,
}, },
} }

View File

@ -13,12 +13,11 @@ export default defineVitestConfig({
environmentOptions: { environmentOptions: {
nuxt: { nuxt: {
overrides: { overrides: {
buildId: 'nuxt-app',
experimental: { experimental: {
appManifest: process.env.TEST_MANIFEST !== 'manifest-off', appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
}, },
appConfig: { runtimeConfig: {
nuxt: { app: {
buildId: 'override', buildId: 'override',
}, },
}, },