mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
feat(nuxt): add experimental.headNext
unhead integration (#22620)
This commit is contained in:
parent
bb83ab5b3f
commit
d50a416304
@ -58,9 +58,9 @@
|
||||
"@nuxt/telemetry": "^2.4.1",
|
||||
"@nuxt/ui-templates": "^1.3.1",
|
||||
"@nuxt/vite-builder": "workspace:../vite",
|
||||
"@unhead/ssr": "^1.2.2",
|
||||
"@unhead/vue": "^1.2.2",
|
||||
"@unhead/dom": "^1.2.2",
|
||||
"@unhead/ssr": "^1.3.2",
|
||||
"@unhead/vue": "^1.3.2",
|
||||
"@unhead/dom": "^1.3.2",
|
||||
"@vue/shared": "^3.3.4",
|
||||
"acorn": "8.10.0",
|
||||
"c12": "^1.4.2",
|
||||
|
@ -17,12 +17,15 @@ import { joinURL, withoutTrailingSlash } from 'ufo'
|
||||
import { renderToString as _renderToString } from 'vue/server-renderer'
|
||||
import { hash } from 'ohash'
|
||||
import { renderSSRHead } from '@unhead/ssr'
|
||||
import type { HeadEntryOptions } from '@unhead/schema'
|
||||
|
||||
import { defineRenderHandler, getRouteRules, useRuntimeConfig, useStorage } from '#internal/nitro'
|
||||
import { useNitroApp } from '#internal/nitro/app'
|
||||
|
||||
import type { Link, Script } from '@unhead/vue'
|
||||
import { createServerHead } from '@unhead/vue'
|
||||
// @ts-expect-error virtual file
|
||||
import unheadPlugins from '#internal/unhead-plugins.mjs'
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
|
||||
// @ts-expect-error virtual file
|
||||
@ -239,8 +242,12 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
// Get route options (currently to apply `ssr: false`)
|
||||
const routeOptions = getRouteRules(event)
|
||||
|
||||
const head = createServerHead()
|
||||
head.push(appHead)
|
||||
const head = createServerHead({
|
||||
plugins: unheadPlugins
|
||||
})
|
||||
// needed for hash hydration plugin to work
|
||||
const headEntryOptions: HeadEntryOptions = { mode: 'server' }
|
||||
head.push(appHead, headEntryOptions)
|
||||
|
||||
// Initialize ssr context
|
||||
const ssrContext: NuxtSSRContext = {
|
||||
@ -336,7 +343,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
? { rel: 'preload', as: 'fetch', crossorigin: 'anonymous', href: payloadURL }
|
||||
: { rel: 'modulepreload', href: payloadURL }
|
||||
]
|
||||
})
|
||||
}, headEntryOptions)
|
||||
}
|
||||
|
||||
// 2. Styles
|
||||
@ -346,17 +353,17 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
|
||||
),
|
||||
style: inlinedStyles
|
||||
})
|
||||
}, headEntryOptions)
|
||||
|
||||
if (!NO_SCRIPTS) {
|
||||
// 3. Resource Hints
|
||||
// TODO: add priorities based on Capo
|
||||
head.push({
|
||||
link: getPreloadLinks(ssrContext, renderer.rendererContext) as Link[]
|
||||
})
|
||||
}, headEntryOptions)
|
||||
head.push({
|
||||
link: getPrefetchLinks(ssrContext, renderer.rendererContext) as Link[]
|
||||
})
|
||||
}, headEntryOptions)
|
||||
// 4. Payloads
|
||||
head.push({
|
||||
script: _PAYLOAD_EXTRACTION
|
||||
@ -367,6 +374,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
|
||||
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
|
||||
}, {
|
||||
...headEntryOptions,
|
||||
// this should come before another end of body scripts
|
||||
tagPosition: 'bodyClose',
|
||||
tagPriority: 'high'
|
||||
@ -382,7 +390,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
defer: resource.module ? null : true,
|
||||
crossorigin: ''
|
||||
}))
|
||||
})
|
||||
}, headEntryOptions)
|
||||
}
|
||||
|
||||
// remove certain tags for nuxt islands
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { resolve } from 'pathe'
|
||||
import { addComponent, addImportsSources, addPlugin, defineNuxtModule, tryResolveModule } from '@nuxt/kit'
|
||||
import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, tryResolveModule } from '@nuxt/kit'
|
||||
import { distDir } from '../dirs'
|
||||
|
||||
const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head', 'Html', 'Body']
|
||||
@ -54,9 +54,26 @@ export default defineNuxtModule({
|
||||
addPlugin({ src: resolve(runtimeDir, 'plugins/vueuse-head-polyfill') })
|
||||
}
|
||||
|
||||
if (nuxt.options.experimental.headCapoPlugin) {
|
||||
addPlugin({ src: resolve(runtimeDir, 'plugins/capo') })
|
||||
}
|
||||
addTemplate({
|
||||
filename: 'unhead-plugins.mjs',
|
||||
getContents () {
|
||||
if (!nuxt.options.experimental.headNext) {
|
||||
return 'export default []'
|
||||
}
|
||||
// TODO don't use HashHydrationPlugin for SPA
|
||||
return `import { CapoPlugin, HashHydrationPlugin } from '@unhead/vue'
|
||||
const plugins = [HashHydrationPlugin()];
|
||||
if (process.server) {
|
||||
plugins.push(CapoPlugin({ track: true }));
|
||||
}
|
||||
export default plugins;`
|
||||
}
|
||||
})
|
||||
|
||||
// template is only exposed in nuxt context, expose in nitro context as well
|
||||
nuxt.hooks.hook('nitro:config', (config) => {
|
||||
config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins']
|
||||
})
|
||||
|
||||
// Add library-specific plugin
|
||||
addPlugin({ src: resolve(runtimeDir, 'plugins/unhead') })
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { CapoPlugin } from '@unhead/vue'
|
||||
import { defineNuxtPlugin } from '#app/nuxt'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt:head:capo',
|
||||
setup (nuxtApp) {
|
||||
nuxtApp.vueApp._context.provides.usehead.use(CapoPlugin({ track: true }))
|
||||
}
|
||||
})
|
@ -2,10 +2,17 @@ import { createHead as createClientHead } from '@unhead/vue'
|
||||
import { renderDOMHead } from '@unhead/dom'
|
||||
import { defineNuxtPlugin } from '#app/nuxt'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import unheadPlugins from '#build/unhead-plugins.mjs'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt:head',
|
||||
setup (nuxtApp) {
|
||||
const head = import.meta.server ? nuxtApp.ssrContext!.head : createClientHead()
|
||||
const head = import.meta.server
|
||||
? nuxtApp.ssrContext!.head
|
||||
: createClientHead({
|
||||
plugins: unheadPlugins
|
||||
})
|
||||
// nuxt.config appHead is set server-side within the renderer
|
||||
nuxtApp.vueApp.use(head)
|
||||
|
||||
|
@ -30,7 +30,7 @@
|
||||
"@types/file-loader": "5.0.1",
|
||||
"@types/pug": "2.0.6",
|
||||
"@types/sass-loader": "8.0.5",
|
||||
"@unhead/schema": "1.2.2",
|
||||
"@unhead/schema": "1.3.2",
|
||||
"@vitejs/plugin-vue": "4.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "3.0.1",
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
|
@ -232,10 +232,12 @@ export default defineUntypedSchema({
|
||||
asyncContext: false,
|
||||
|
||||
/**
|
||||
* Add the capo.js head plugin in order to render tags in of the head in a more performant way.
|
||||
* Use new experimental head optimisations:
|
||||
* - Add the capo.js head plugin in order to render tags in of the head in a more performant way.
|
||||
* - Uses the hash hydration plugin to reduce initial hydration
|
||||
*
|
||||
* @see https://rviscomi.github.io/capo.js/user/rules/
|
||||
* @see https://github.com/nuxt/nuxt/discussions/22632
|
||||
*/
|
||||
headCapoPlugin: false
|
||||
headNext: false
|
||||
}
|
||||
})
|
||||
|
@ -365,14 +365,14 @@ importers:
|
||||
specifier: ^14.18.0 || >=16.10.0
|
||||
version: 18.17.5
|
||||
'@unhead/dom':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2
|
||||
'@unhead/ssr':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2
|
||||
'@unhead/vue':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(vue@3.3.4)
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2(vue@3.3.4)
|
||||
'@vue/shared':
|
||||
specifier: ^3.3.4
|
||||
version: 3.3.4
|
||||
@ -577,8 +577,8 @@ importers:
|
||||
specifier: 8.0.5
|
||||
version: 8.0.5
|
||||
'@unhead/schema':
|
||||
specifier: 1.2.2
|
||||
version: 1.2.2
|
||||
specifier: 1.3.2
|
||||
version: 1.3.2
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 4.2.3
|
||||
version: 4.2.3(vite@4.4.9)(vue@3.3.4)
|
||||
@ -3239,41 +3239,41 @@ packages:
|
||||
eslint-visitor-keys: 3.4.3
|
||||
dev: true
|
||||
|
||||
/@unhead/dom@1.2.2:
|
||||
resolution: {integrity: sha512-ohganmg4i1Dd4wwQ2A9oLWEkJNpJRoERJNmFgzmScw9Vi3zMqoS4gPIofT20zUR5rhyyAsFojuDPojJ5vKcmqw==}
|
||||
/@unhead/dom@1.3.2:
|
||||
resolution: {integrity: sha512-iShW0eKzS4TvL0ATtmFNyRdx4JxFKiksoUSDAgkPrMaI8EhYtryU5IL0i5TVySSk4kIjMfLgd8uElOAfUHpTsQ==}
|
||||
dependencies:
|
||||
'@unhead/schema': 1.2.2
|
||||
'@unhead/shared': 1.2.2
|
||||
'@unhead/schema': 1.3.2
|
||||
'@unhead/shared': 1.3.2
|
||||
dev: false
|
||||
|
||||
/@unhead/schema@1.2.2:
|
||||
resolution: {integrity: sha512-cGtNvadL76eGl7QxGjWHZxFqLv9a2VrmRpeEb1d7sm0cvnN0bWngdXDTdUyXzn7RVv/Um+/yae6eiT6A+pyQOw==}
|
||||
/@unhead/schema@1.3.2:
|
||||
resolution: {integrity: sha512-RiJUPipN6wntwpJvHBS8+84/eQyQdnznWTY+AC3woWTUfPHz7M4Hzu6jEkdnzpTX77HQMIIuu734vohmo+L91A==}
|
||||
dependencies:
|
||||
hookable: 5.5.3
|
||||
zhead: 2.0.10
|
||||
|
||||
/@unhead/shared@1.2.2:
|
||||
resolution: {integrity: sha512-bWRjRyVzFsunih9GbHctvS8Aenj6KBe5ycql1JE4LawBL/NRYvCYUCPpdK5poVOqjYr0yDAf9m4JGaM2HwpVLw==}
|
||||
/@unhead/shared@1.3.2:
|
||||
resolution: {integrity: sha512-omOLfVnSkCpiIgikGKkgW6dzs+2jncAXtmPb+/IkFSkevbEfzyQlciDL12h9ChetRXjcWBZhu+OCCw0oY8W/Nw==}
|
||||
dependencies:
|
||||
'@unhead/schema': 1.2.2
|
||||
'@unhead/schema': 1.3.2
|
||||
dev: false
|
||||
|
||||
/@unhead/ssr@1.2.2:
|
||||
resolution: {integrity: sha512-mpWSNNbrQFJZolAfdVInPPiSGUva08bK9UbNV1zgDScUz+p+FnRg4cj77X+PpVeJ0+KPgjXfOsI8VQKYt+buYA==}
|
||||
/@unhead/ssr@1.3.2:
|
||||
resolution: {integrity: sha512-ygbvJcJoN6wzGZnRfc3QmBtHp1awxEy2IIPbLyqgNWmkDElUfC14xM2BvNAEybSTR/EeX8Sf492CGw+5I0RqPw==}
|
||||
dependencies:
|
||||
'@unhead/schema': 1.2.2
|
||||
'@unhead/shared': 1.2.2
|
||||
'@unhead/schema': 1.3.2
|
||||
'@unhead/shared': 1.3.2
|
||||
dev: false
|
||||
|
||||
/@unhead/vue@1.2.2(vue@3.3.4):
|
||||
resolution: {integrity: sha512-AxOmY5JPn4fS34ovaivPnqg2my+InIkZDNSxCKfRkmbBtstFre/Fyf0d92Qfx0u8PJiSRPOjthEHx5vKDgTEJQ==}
|
||||
/@unhead/vue@1.3.2(vue@3.3.4):
|
||||
resolution: {integrity: sha512-pM5SbKTTTzLroVHGfqBkDA8WCVk1qVWBe8sia5Rw1pSntp99VgDawJkiCdJJbyh/bOUOvCuAgUVHH1IRpB8QmQ==}
|
||||
peerDependencies:
|
||||
vue: '>=2.7 || >=3'
|
||||
dependencies:
|
||||
'@unhead/schema': 1.2.2
|
||||
'@unhead/shared': 1.2.2
|
||||
'@unhead/schema': 1.3.2
|
||||
'@unhead/shared': 1.3.2
|
||||
hookable: 5.5.3
|
||||
unhead: 1.2.2
|
||||
unhead: 1.3.2
|
||||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
@ -7183,7 +7183,7 @@ packages:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
/jstransformer@1.0.0:
|
||||
resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==}
|
||||
resolution: {integrity: sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=}
|
||||
dependencies:
|
||||
is-promise: 2.2.2
|
||||
promise: 7.3.1
|
||||
@ -10316,7 +10316,7 @@ packages:
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
/token-stream@1.0.0:
|
||||
resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==}
|
||||
resolution: {integrity: sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=}
|
||||
dev: false
|
||||
|
||||
/totalist@1.1.0:
|
||||
@ -10532,12 +10532,12 @@ packages:
|
||||
node-fetch-native: 1.2.0
|
||||
pathe: 1.1.1
|
||||
|
||||
/unhead@1.2.2:
|
||||
resolution: {integrity: sha512-9wDuiso7YWNe0BTA5NGsHR0dtqn0YrL/5+NumfuXDxxYykavc6N27pzZxTXiuvVHbod8tFicsxA6pC9WhQvzqg==}
|
||||
/unhead@1.3.2:
|
||||
resolution: {integrity: sha512-s4qW/Rcp6OD4GRBreAQYRD4B1ch7zhVt57IGUIGdn6xwT0tHJucHBv2GbqdpaTLmZcUOdblBIt2HXdOlbW2YHg==}
|
||||
dependencies:
|
||||
'@unhead/dom': 1.2.2
|
||||
'@unhead/schema': 1.2.2
|
||||
'@unhead/shared': 1.2.2
|
||||
'@unhead/dom': 1.3.2
|
||||
'@unhead/schema': 1.3.2
|
||||
'@unhead/shared': 1.3.2
|
||||
hookable: 5.5.3
|
||||
dev: false
|
||||
|
||||
|
@ -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('"97.4k"')
|
||||
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"95.0k"')
|
||||
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||
[
|
||||
"_nuxt/entry.js",
|
||||
@ -32,10 +32,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.5k"')
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.6k"')
|
||||
|
||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2342k"')
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2335k"')
|
||||
|
||||
const packages = modules.files
|
||||
.filter(m => m.endsWith('package.json'))
|
||||
@ -95,7 +95,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"370k"')
|
||||
|
||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"604k"')
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"597k"')
|
||||
|
||||
const packages = modules.files
|
||||
.filter(m => m.endsWith('package.json'))
|
||||
|
2
test/fixtures/basic/nuxt.config.ts
vendored
2
test/fixtures/basic/nuxt.config.ts
vendored
@ -195,7 +195,7 @@ export default defineNuxtConfig({
|
||||
treeshakeClientOnly: true,
|
||||
payloadExtraction: true,
|
||||
asyncContext: process.env.TEST_CONTEXT === 'async',
|
||||
headCapoPlugin: true
|
||||
headNext: true
|
||||
},
|
||||
appConfig: {
|
||||
fromNuxtConfig: true,
|
||||
|
Loading…
Reference in New Issue
Block a user