From 454f052bbbd5c37affa45497c45fb9c4db9b2b27 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 24 Sep 2022 12:25:05 +0100 Subject: [PATCH 1/2] fix(nuxt): only fetch payloads for prerendered routes --- packages/nuxt/src/app/composables/payload.ts | 9 +++++++++ packages/nuxt/src/app/nuxt.ts | 6 ++++++ .../nuxt/src/app/plugins/payload.client.ts | 18 ++++++++++++------ packages/nuxt/src/core/nitro.ts | 14 ++++++++++++++ packages/nuxt/src/core/nuxt.ts | 2 +- .../nuxt/src/core/runtime/nitro/manifest.ts | 8 ++++++++ .../nuxt/src/core/runtime/nitro/renderer.ts | 1 + packages/schema/src/config/experimental.ts | 4 +++- 8 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 packages/nuxt/src/core/runtime/nitro/manifest.ts diff --git a/packages/nuxt/src/app/composables/payload.ts b/packages/nuxt/src/app/composables/payload.ts index f53d06d7ab8..4213a0ed39c 100644 --- a/packages/nuxt/src/app/composables/payload.ts +++ b/packages/nuxt/src/app/composables/payload.ts @@ -9,6 +9,8 @@ interface LoadPayloadOptions { export function loadPayload (url: string, opts: LoadPayloadOptions = {}) { if (process.server) { return null } + if (!_hasPayload(url)) { return null } + const payloadURL = _getPayloadURL(url, opts) const nuxtApp = useNuxtApp() const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {} @@ -26,6 +28,8 @@ export function loadPayload (url: string, opts: LoadPayloadOptions = {}) { } export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) { + if (!_hasPayload(url)) { return } + const payloadURL = _getPayloadURL(url, opts) useHead({ link: [ @@ -45,6 +49,11 @@ function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) { return joinURL(parsed.pathname, hash ? `_payload.${hash}.js` : '_payload.js') } +function _hasPayload (url: string) { + const nuxtApp = useNuxtApp() + return nuxtApp._manifest.static.some(route => typeof route === 'string' ? route === url : route.test(url)) +} + async function _importPayload (payloadURL: string) { if (process.server) { return null } const res = await import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).catch((err) => { diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 881d9a248a0..202630e077a 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -55,6 +55,10 @@ export interface NuxtSSRContext extends SSRContext { renderMeta?: () => Promise | NuxtMeta } +export interface RouteManifest { + static: Array +} + interface _NuxtApp { vueApp: App globalName: string @@ -72,6 +76,8 @@ interface _NuxtApp { error: Ref } | undefined>, + _manifest: RouteManifest + ssrContext?: NuxtSSRContext payload: { serverRendered?: boolean diff --git a/packages/nuxt/src/app/plugins/payload.client.ts b/packages/nuxt/src/app/plugins/payload.client.ts index c13acdb001b..3ffada8b0ac 100644 --- a/packages/nuxt/src/app/plugins/payload.client.ts +++ b/packages/nuxt/src/app/plugins/payload.client.ts @@ -1,12 +1,18 @@ -import { defineNuxtPlugin, loadPayload, isPrerendered, useRouter } from '#app' +import escapeRE from 'escape-string-regexp' +import { defineNuxtPlugin, loadPayload, useRouter } from '#app' -export default defineNuxtPlugin((nuxtApp) => { - // Only enable behavior if initial page is prerendered - // TOOD: Support hybrid and dev - if (!isPrerendered()) { - return +export default defineNuxtPlugin(async (nuxtApp) => { + const manifest = await $fetch<{ static: string[] }>('/manifest.json', { + cache: 'no-cache' + }).catch(() => ({ static: [] })) + + nuxtApp._manifest = { + ...manifest, + static: manifest.static.map(r => r.endsWith('/**') ? new RegExp(escapeRE(r.slice(0, -3)) + '.*') : r) } + if (nuxtApp._manifest.static.length === 0) { return } + // Load payload into cache nuxtApp.hooks.hook('link:prefetch', to => loadPayload(to)) diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index 572eece60a2..a09f02a7a02 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -55,6 +55,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { ], prerender: { crawlLinks: nuxt.options._generate ? nuxt.options.generate.crawler : false, + ignore: ['/manifest.json'], routes: ([] as string[]) .concat(nuxt.options._generate ? ['/', '/200.html', ...nuxt.options.generate.routes] : []) .concat(nuxt.options.ssr === false ? ['/index.html', '/200.html', '/404.html'] : []) @@ -119,6 +120,14 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { nitroConfig.virtual!['#build/dist/server/styles.mjs'] = 'export default {}' } + // Add manifest handler + if (nuxt.options.experimental.payloadExtraction) { + nitroConfig.handlers!.push({ + route: '/manifest.json', + handler: resolve(distDir, 'core/runtime/nitro/manifest') + }) + } + // Register nuxt protection patterns nitroConfig.rollupConfig!.plugins!.push(ImportProtectionPlugin.rollup({ rootDir: nuxt.options.rootDir, @@ -176,6 +185,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { if (!nuxt.options._generate) { await build(nitro) } else { + await fsp.writeFile(join(nitro.options.output.publicDir, 'manifest.json'), JSON.stringify({ + routes: nitro._prerenderedRoutes + ?.filter(r => r.route.endsWith('_payload.js')) + .map(r => r.route.replace(/\/_payload\.js$/, '') || '/') + })) const distDir = resolve(nuxt.options.rootDir, 'dist') if (!existsSync(distDir)) { await fsp.symlink(nitro.options.output.publicDir, distDir, 'junction').catch(() => {}) diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 81305b82691..e64be04e2fc 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -174,7 +174,7 @@ async function initNuxt (nuxt: Nuxt) { }) // Add prerender payload support - if (!nuxt.options.dev && nuxt.options.experimental.payloadExtraction) { + if (nuxt.options.experimental.payloadExtraction) { addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client')) } diff --git a/packages/nuxt/src/core/runtime/nitro/manifest.ts b/packages/nuxt/src/core/runtime/nitro/manifest.ts new file mode 100644 index 00000000000..2322765679e --- /dev/null +++ b/packages/nuxt/src/core/runtime/nitro/manifest.ts @@ -0,0 +1,8 @@ +import { defineCachedEventHandler } from '#internal/nitro' + +export default defineCachedEventHandler(() => { + return { + // TODO: support route rules for payload extraction + static: ['/**'] + } +}) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index ed2cc418366..86405ab2f36 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -196,6 +196,7 @@ export default defineRenderHandler(async (event) => { head: normalizeChunks([ renderedMeta.headTags, _PAYLOAD_EXTRACTION ? `` : null, + process.env.NUXT_PAYLOAD_EXTRACTION ? '' : null, _rendered.renderResourceHints(), _rendered.renderStyles(), inlinedStyles, diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 9fc13cf2998..6a2e6f07967 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -78,6 +78,8 @@ export default defineUntypedSchema({ /** * When this option is enabled (by default) payload of pages generated with `nuxt generate` are extracted */ - payloadExtraction: true, + payloadExtraction: { + $resolve: async (val, get) => val ?? !(await get('dev')) + }, } }) From 109922edae0a754178424ed71ec60a15e2b36fd5 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 24 Sep 2022 12:46:11 +0100 Subject: [PATCH 2/2] test: exclude `` from href extraction --- test/basic.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index 2fe9c019dba..927cae59ab3 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -493,7 +493,7 @@ describe.runIf(process.env.NUXT_TEST_DEV)('detecting invalid root nodes', () => describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { it('should work with no overrides', async () => { const html: string = await $fetch('/assets') - for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { + for (const match of html.matchAll(/<(?!link)[^>]*(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { const url = match[2] || match[3] expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy() } @@ -523,7 +523,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { await startServer() const html = await $fetch('/foo/assets') - for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { + for (const match of html.matchAll(/<(?!link)[^>]*(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { const url = match[2] || match[3] expect( url.startsWith('/foo/_other/') || @@ -540,7 +540,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { await startServer() const html = await $fetch('/assets') - for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { + for (const match of html.matchAll(/<(?!link)[^>]*(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { const url = match[2] || match[3] expect( url.startsWith('./_nuxt/') || @@ -568,7 +568,7 @@ describe.skipIf(process.env.NUXT_TEST_DEV)('dynamic paths', () => { await startServer() const html = await $fetch('/foo/assets') - for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { + for (const match of html.matchAll(/<(?!link)[^>]*(href|src)="(.*?)"|url\(([^)]*?)\)/g)) { const url = match[2] || match[3] expect( url.startsWith('https://example.com/_cdn/') ||