From 344940a36dd72b1e1ca991b02c955b8102396693 Mon Sep 17 00:00:00 2001 From: nikolay_koskarev Date: Tue, 5 Nov 2024 01:52:37 +0700 Subject: [PATCH 1/5] fix(nuxt, schema) Keep showing the spa-loading-template until suspense:resolve (#21721) --- packages/nuxt/src/app/entry.ts | 7 +- .../nuxt/src/core/runtime/nitro/renderer.ts | 11 ++- packages/schema/src/config/app.ts | 22 ++++- pnpm-lock.yaml | 90 ++++++++++++++++--- test/fixtures/spa-loader/app.vue | 16 ++++ .../spa-loader/app/spa-loading-template.html | 1 + test/fixtures/spa-loader/nuxt.config.ts | 6 ++ test/fixtures/spa-loader/package.json | 12 +++ test/fixtures/spa-loader/server/api/test.ts | 3 + test/fixtures/spa-loader/server/tsconfig.json | 3 + test/fixtures/spa-loader/tsconfig.json | 3 + test/spa-loader-no-ssr.test.ts | 36 ++++++++ test/spa-loader-ssr.test.ts | 36 ++++++++ 13 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 test/fixtures/spa-loader/app.vue create mode 100644 test/fixtures/spa-loader/app/spa-loading-template.html create mode 100644 test/fixtures/spa-loader/nuxt.config.ts create mode 100644 test/fixtures/spa-loader/package.json create mode 100644 test/fixtures/spa-loader/server/api/test.ts create mode 100644 test/fixtures/spa-loader/server/tsconfig.json create mode 100644 test/fixtures/spa-loader/tsconfig.json create mode 100644 test/spa-loader-no-ssr.test.ts create mode 100644 test/spa-loader-ssr.test.ts diff --git a/packages/nuxt/src/app/entry.ts b/packages/nuxt/src/app/entry.ts index dac40f4b97..1e55e2cda1 100644 --- a/packages/nuxt/src/app/entry.ts +++ b/packages/nuxt/src/app/entry.ts @@ -17,7 +17,7 @@ import plugins from '#build/plugins' // @ts-expect-error virtual file import RootComponent from '#build/root-component.mjs' // @ts-expect-error virtual file -import { appId, multiApp, vueAppRootContainer } from '#build/nuxt.config.mjs' +import { appId, appSpaLoaderAttrs, multiApp, vueAppRootContainer } from '#build/nuxt.config.mjs' let entry: (ssrContext?: CreateOptions['ssrContext']) => Promise> @@ -72,6 +72,11 @@ if (import.meta.client) { if (vueApp.config.errorHandler === handleVueError) { vueApp.config.errorHandler = undefined } }) + // Remove spa loader if present + nuxt.hook('app:suspense:resolve', () => { + if (!isSSR && appSpaLoaderAttrs.id) { document.getElementById(appSpaLoaderAttrs.id)?.remove() } + }) + try { await applyPlugins(nuxt, plugins) } catch (err) { diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 4a7037ae44..6e735c8a16 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -30,7 +30,7 @@ import { renderSSRHeadOptions } from '#internal/unhead.config.mjs' import type { NuxtPayload, NuxtSSRContext } from '#app' // @ts-expect-error virtual file -import { appHead, appId, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp } from '#internal/nuxt.config.mjs' +import { appHead, appId, appRootAttrs, appRootTag, appSpaLoaderAttrs, appSpaLoaderTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp } from '#internal/nuxt.config.mjs' // @ts-expect-error virtual file import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths' @@ -144,7 +144,11 @@ const getSPARenderer = lazyCachedFunction(async () => { // @ts-expect-error virtual file const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '') - .then(r => APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG) + .then((r) => { + const appTemplate = APP_ROOT_OPEN_TAG + APP_ROOT_CLOSE_TAG + const loaderTemplate = r ? APP_SPA_LOADER_OPEN_TAG + r + APP_SPA_LOADER_CLOSE_TAG : '' + return appTemplate + loaderTemplate + }) const options = { manifest, @@ -222,6 +226,9 @@ async function getIslandContext (event: H3Event): Promise { return ctx } +const APP_SPA_LOADER_OPEN_TAG = `<${appSpaLoaderTag}${propsToString(appSpaLoaderAttrs)}>` +const APP_SPA_LOADER_CLOSE_TAG = `` + const HAS_APP_TELEPORTS = !!(appTeleportTag && appTeleportAttrs.id) const APP_TELEPORT_OPEN_TAG = HAS_APP_TELEPORTS ? `<${appTeleportTag}${propsToString(appTeleportAttrs)}>` : '' const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `` : '' diff --git a/packages/schema/src/config/app.ts b/packages/schema/src/config/app.ts index 9f5d8aa0b5..406c0d6e54 100644 --- a/packages/schema/src/config/app.ts +++ b/packages/schema/src/config/app.ts @@ -235,7 +235,7 @@ export default defineUntypedSchema({ }, /** - * Customize Nuxt root element tag. + * Customize Nuxt Teleport element tag. */ teleportTag: { $resolve: val => val || 'div', @@ -262,6 +262,26 @@ export default defineUntypedSchema({ }) }, }, + + /** + * Customize Nuxt SpaLoader element tag. + */ + spaLoaderTag: { + $resolve: val => val || 'div', + }, + + /** + * Customize Nuxt Nuxt SpaLoader element attributes. + * @type {typeof import('@unhead/schema').HtmlAttributes} + */ + spaLoaderAttrs: { + $resolve: async (val: undefined | null | Record, get) => { + const spaLoaderId = await get('app.spaLoaderId') + return defu(val, { + id: spaLoaderId === false ? undefined : (spaLoaderId || '__nuxt-spa-loader'), + }) + }, + }, }, /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 251e75968b..7c500b6ccb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1189,6 +1189,12 @@ importers: specifier: workspace:* version: link:../../../packages/nuxt + test/fixtures/spa-loader: + dependencies: + nuxt: + specifier: workspace:* + version: link:../../../packages/nuxt + test/fixtures/suspense: dependencies: nuxt: @@ -2273,30 +2279,35 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-glibc@2.4.1': resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.4.1': resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.4.1': resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.4.1': resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-wasm@2.4.1': resolution: {integrity: sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA==} @@ -2484,46 +2495,55 @@ packages: resolution: {integrity: sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.24.3': resolution: {integrity: sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.24.3': resolution: {integrity: sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.24.3': resolution: {integrity: sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.24.3': resolution: {integrity: sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.24.3': resolution: {integrity: sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.24.3': resolution: {integrity: sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.24.3': resolution: {integrity: sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.24.3': resolution: {integrity: sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.24.3': resolution: {integrity: sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==} @@ -2554,21 +2574,25 @@ packages: resolution: {integrity: sha512-JogYtL3VQS9wJ3p3FNhDqinm7avrMsdwz4erP7YCjD7idob93GYAE7dPrHUzSNVnCBYXRaHJYZHDQs7lKVcYZw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rspack/binding-linux-arm64-musl@1.0.14': resolution: {integrity: sha512-qgybhxI/nnoa8CUz7zKTC0Oh37NZt9uRxsSV7+ZYrfxqbrVCoNVuutPpY724uUHy1M6W34kVEm1uT1N4Ka5cZg==} cpu: [arm64] os: [linux] + libc: [musl] '@rspack/binding-linux-x64-gnu@1.0.14': resolution: {integrity: sha512-5vzaDRw3/sGKo3ax/1cU3/cxqNjajwlt2LU288vXNe1/n8oe/pcDfYcTugpOe/A1DqzadanudJszLpFcKsaFtQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rspack/binding-linux-x64-musl@1.0.14': resolution: {integrity: sha512-4U6QD9xVS1eGme52DuJr6Fg/KdcUfJ+iKwH49Up460dZ/fLvGylnVGA+V0mzPlKi8gfy7NwFuYXZdu3Pwi1YYg==} cpu: [x64] os: [linux] + libc: [musl] '@rspack/binding-win32-arm64-msvc@1.0.14': resolution: {integrity: sha512-SjeYw7qqRHYZ5RPClu+ffKZsShQdU3amA1OwC3M0AS6dbfEcji8482St3Y8Z+QSzYRapCEZij9LMM/9ypEhISg==} @@ -2894,18 +2918,27 @@ packages: '@unhead/dom@1.11.11': resolution: {integrity: sha512-4YwziCH5CmjvUzSGdZ4Klj6BqhLSTNZooA9kt47yDxj4Qw9uHqVnXwWWupYsVdIYPNsw1tR2AkHveg82y1Fn3A==} + '@unhead/dom@1.11.9': + resolution: {integrity: sha512-AOoCt05sLbkmp7ipCAs2JQdV0auLc5lCkLbCZj19kuPmWcFOoHNByQAG/AFKuSvi297OYp8abKGCStIgyz2x4A==} + '@unhead/schema@1.11.10': resolution: {integrity: sha512-lXh7cm5XtFaw3gc+ZVXTSfIHXiBpAywbjtEiOsz5TR4GxOjj2rtfOAl4C3Difk1yupP6L2otYmOZdn/i8EXSJg==} '@unhead/schema@1.11.11': resolution: {integrity: sha512-xSGsWHPBYcMV/ckQeImbrVu6ddeRnrdDCgXUKv3xIjGBY+ob/96V80lGX8FKWh8GwdFSwhblISObKlDAt5K9ZQ==} + '@unhead/schema@1.11.9': + resolution: {integrity: sha512-0V37bxG4sQuiLw3M5DMD+b99ndOOngecMlekQ122TDvBb24W8rWwkHhXvAu5eFg6bQXPdQF1A0U0PuRMcCj/ZA==} + '@unhead/shared@1.11.10': resolution: {integrity: sha512-YQgZcOyo1id7drUeDPGn0R83pirvIcV+Car3/m7ZfCLL1Syab6uXmRckVRd69yVbUL4eirIm9IzzmvzM/OuGuw==} '@unhead/shared@1.11.11': resolution: {integrity: sha512-RfdvUskPn90ipO+PmR98jKZ8Lsx1uuzscOenO5xcrMrtWGhlLWaEBIrbvFOvX5PZ/u8/VNMJChTXGDUjEtHmlg==} + '@unhead/shared@1.11.9': + resolution: {integrity: sha512-Df6Td9d87NM5EWf4ylAN98zwf50DwfMg3xoy6ofz3Qg1jSXewEIMD1w1C0/Q6KdpLo01TuoQ0RfpSyVtxt7oEA==} + '@unhead/ssr@1.11.10': resolution: {integrity: sha512-tj5zeJtCbSktNNqsdL+6h6OIY7dYO+2HSiC1VbofGYsoG7nDNXMypkrW/cTMqZVr5/gWhKaUgFQALjm28CflYg==} @@ -2914,6 +2947,11 @@ packages: peerDependencies: vue: 3.5.12 + '@unhead/vue@1.11.9': + resolution: {integrity: sha512-vdl3H1bwJNindhRplMun7zhtNFggP8QqpPwc1e7kd2a0ORp776+QpFXKdYHFSlX+eAMmDVv8LQ+VL0N++pXxNg==} + peerDependencies: + vue: 3.5.12 + '@unocss/astro@0.62.4': resolution: {integrity: sha512-98KfkbrNhBLx2+uYxMiGsldIeIZ6/PbL4yaGRHeHoiHd7p4HmIyCF+auYe4Psntx3Yr8kU+XSIAhGDYebvTidQ==} peerDependencies: @@ -3504,7 +3542,7 @@ packages: engines: {node: '>= 0.4'} asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + resolution: {integrity: sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=} assert-never@1.3.0: resolution: {integrity: sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==} @@ -3692,7 +3730,7 @@ packages: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} character-parser@2.2.0: - resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + resolution: {integrity: sha1-x84o821LzZdE5f/CxfzeHHMmH8A=} character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} @@ -4138,7 +4176,7 @@ packages: engines: {node: '>=6.0.0'} doctypes@1.1.0: - resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + resolution: {integrity: sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=} dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -4705,7 +4743,6 @@ packages: glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -5244,7 +5281,7 @@ packages: engines: {node: '>=0.10.0'} js-stringify@1.0.2: - resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + resolution: {integrity: sha1-Fzb939lyTyijaCrcYjCufk6Weds=} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5307,7 +5344,7 @@ packages: engines: {node: '>=0.10.0'} jstransformer@1.0.0: - resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + resolution: {integrity: sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=} keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -6549,7 +6586,7 @@ packages: resolution: {integrity: sha512-6tJUH4xHFcdO85CZRwAcEtHNCzjZ9V9S0VZLgo1pzbN04qy8jiVCZ3oAxDmBVG3Rth5b1xFTDet5WG/UYZeJLQ==} relateurl@0.2.7: - resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + resolution: {integrity: sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=} engines: {node: '>= 0.10'} remark-emoji@5.0.1: @@ -7079,7 +7116,7 @@ packages: engines: {node: '>=0.6'} token-stream@1.0.0: - resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + resolution: {integrity: sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=} totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} @@ -7212,6 +7249,9 @@ packages: unhead@1.11.10: resolution: {integrity: sha512-hypXrAI47wE3wIhkze0RMPGAWcoo45Q1+XzdqLD/OnTCzjFXQrpuE4zBy8JRexyrqp+Ud2+nFTUNf/mjfFSymw==} + unhead@1.11.9: + resolution: {integrity: sha512-EwEGMjbXVVn2O5vNfXUHiAjHWFHngPjkAx0yVZZsrTgqzs7+A/YvJ90TLvBna874+HCKZWtufo7QAI7luU2CgA==} + unicode-emoji-modifier-base@1.0.0: resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} engines: {node: '>=4'} @@ -7549,7 +7589,7 @@ packages: optional: true void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + resolution: {integrity: sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=} engines: {node: '>=0.10.0'} vscode-jsonrpc@6.0.0: @@ -8753,7 +8793,7 @@ snapshots: '@types/google.maps': 3.58.1 '@types/vimeo__player': 2.18.3 '@types/youtube': 0.1.0 - '@unhead/vue': 1.11.10(vue@3.5.12(typescript@5.6.3)) + '@unhead/vue': 1.11.9(vue@3.5.12(typescript@5.6.3)) '@vueuse/core': 11.1.0(vue@3.5.12(typescript@5.6.3)) consola: 3.2.3 defu: 6.1.4 @@ -9617,6 +9657,11 @@ snapshots: '@unhead/schema': 1.11.11 '@unhead/shared': 1.11.11 + '@unhead/dom@1.11.9': + dependencies: + '@unhead/schema': 1.11.9 + '@unhead/shared': 1.11.9 + '@unhead/schema@1.11.10': dependencies: hookable: 5.5.3 @@ -9627,6 +9672,11 @@ snapshots: hookable: 5.5.3 zhead: 2.2.4 + '@unhead/schema@1.11.9': + dependencies: + hookable: 5.5.3 + zhead: 2.2.4 + '@unhead/shared@1.11.10': dependencies: '@unhead/schema': 1.11.10 @@ -9635,6 +9685,10 @@ snapshots: dependencies: '@unhead/schema': 1.11.11 + '@unhead/shared@1.11.9': + dependencies: + '@unhead/schema': 1.11.9 + '@unhead/ssr@1.11.10': dependencies: '@unhead/schema': 1.11.10 @@ -9649,6 +9703,15 @@ snapshots: unhead: 1.11.10 vue: 3.5.12(typescript@5.6.3) + '@unhead/vue@1.11.9(vue@3.5.12(typescript@5.6.3))': + dependencies: + '@unhead/schema': 1.11.9 + '@unhead/shared': 1.11.9 + defu: 6.1.4 + hookable: 5.5.3 + unhead: 1.11.9 + vue: 3.5.12(typescript@5.6.3) + '@unocss/astro@0.62.4(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.7)(sass@1.78.0)(terser@5.32.0))': dependencies: '@unocss/core': 0.62.4 @@ -14910,6 +14973,13 @@ snapshots: '@unhead/shared': 1.11.10 hookable: 5.5.3 + unhead@1.11.9: + dependencies: + '@unhead/dom': 1.11.9 + '@unhead/schema': 1.11.9 + '@unhead/shared': 1.11.9 + hookable: 5.5.3 + unicode-emoji-modifier-base@1.0.0: {} unicorn-magic@0.1.0: {} diff --git a/test/fixtures/spa-loader/app.vue b/test/fixtures/spa-loader/app.vue new file mode 100644 index 0000000000..b654005857 --- /dev/null +++ b/test/fixtures/spa-loader/app.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/test/fixtures/spa-loader/app/spa-loading-template.html b/test/fixtures/spa-loader/app/spa-loading-template.html new file mode 100644 index 0000000000..b683d1e597 --- /dev/null +++ b/test/fixtures/spa-loader/app/spa-loading-template.html @@ -0,0 +1 @@ +
loading...
diff --git a/test/fixtures/spa-loader/nuxt.config.ts b/test/fixtures/spa-loader/nuxt.config.ts new file mode 100644 index 0000000000..bc4c9c7869 --- /dev/null +++ b/test/fixtures/spa-loader/nuxt.config.ts @@ -0,0 +1,6 @@ +export default defineNuxtConfig({ + ssr: false, + devtools: { enabled: false }, + spaLoadingTemplate: true, + compatibilityDate: '2024-06-28', +}) diff --git a/test/fixtures/spa-loader/package.json b/test/fixtures/spa-loader/package.json new file mode 100644 index 0000000000..7316c14927 --- /dev/null +++ b/test/fixtures/spa-loader/package.json @@ -0,0 +1,12 @@ +{ + "name": "nuxt-playground", + "private": true, + "scripts": { + "dev": "nuxi dev", + "build": "nuxi build", + "start": "nuxi preview" + }, + "dependencies": { + "nuxt": "workspace:*" + } +} diff --git a/test/fixtures/spa-loader/server/api/test.ts b/test/fixtures/spa-loader/server/api/test.ts new file mode 100644 index 0000000000..16be9e4121 --- /dev/null +++ b/test/fixtures/spa-loader/server/api/test.ts @@ -0,0 +1,3 @@ +export default eventHandler((_event) => { + return 'Hello!' +}) diff --git a/test/fixtures/spa-loader/server/tsconfig.json b/test/fixtures/spa-loader/server/tsconfig.json new file mode 100644 index 0000000000..b9ed69c19e --- /dev/null +++ b/test/fixtures/spa-loader/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/test/fixtures/spa-loader/tsconfig.json b/test/fixtures/spa-loader/tsconfig.json new file mode 100644 index 0000000000..4b34df1571 --- /dev/null +++ b/test/fixtures/spa-loader/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/test/spa-loader-no-ssr.test.ts b/test/spa-loader-no-ssr.test.ts new file mode 100644 index 0000000000..87d85415ea --- /dev/null +++ b/test/spa-loader-no-ssr.test.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { isWindows } from 'std-env' +import { getBrowser, setup, url } from '@nuxt/test-utils' + +const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack' + +await setup({ + rootDir: fileURLToPath(new URL('./fixtures/spa-loader', import.meta.url)), + dev: process.env.TEST_ENV === 'dev', + server: true, + browser: true, + setupTimeout: (isWindows ? 360 : 120) * 1000, + nuxtConfig: { + builder: isWebpack ? 'webpack' : 'vite', + ssr: false, + spaLoadingTemplate: true, + }, +}) + +describe('spa-loader with SPA', () => { + it('should render spa-loader', async () => { + const browser = await getBrowser() + const page = await browser.newPage({}) + await page.goto(url('/'), { waitUntil: 'domcontentloaded' }) + + const loader = page.getByTestId('loader') + expect(await loader.isVisible()).toBeTruthy() + + const content = page.getByTestId('content') + await content.waitFor({ state: 'visible' }) + expect(await loader.isHidden()).toBeTruthy() + + await page.close() + }, 60_000) +}) diff --git a/test/spa-loader-ssr.test.ts b/test/spa-loader-ssr.test.ts new file mode 100644 index 0000000000..b876ebbe74 --- /dev/null +++ b/test/spa-loader-ssr.test.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { isWindows } from 'std-env' +import { getBrowser, setup, url } from '@nuxt/test-utils' + +const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack' + +await setup({ + rootDir: fileURLToPath(new URL('./fixtures/spa-loader', import.meta.url)), + dev: process.env.TEST_ENV === 'dev', + server: true, + browser: true, + setupTimeout: (isWindows ? 360 : 120) * 1000, + nuxtConfig: { + builder: isWebpack ? 'webpack' : 'vite', + ssr: true, + spaLoadingTemplate: true, + }, +}) + +describe('spa-loader with SSR', () => { + it('should render content without spa-loader', async () => { + const browser = await getBrowser() + const page = await browser.newPage({}) + await page.goto(url('/'), { waitUntil: 'domcontentloaded' }) + + const loader = page.getByTestId('__nuxt-spa-loader') + expect(await loader.isVisible()).toBeFalsy() + + const content = page.getByTestId('content') + await content.waitFor({ state: 'visible' }) + expect(await loader.isHidden()).toBeTruthy() + + await page.close() + }, 60_000) +}) From 096b6d6be36658a95b100e0b84f15bec8b60d8b1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:00:58 +0000 Subject: [PATCH 2/5] [autofix.ci] apply automated fixes --- test/bundle.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index e13a526698..0374c26ae5 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -37,7 +37,7 @@ 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(`"208k"`) + expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"209k"`) const modules = await analyzeSizes(['node_modules/**/*'], serverDir) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1394k"`) @@ -78,7 +78,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM const serverDir = join(rootDir, '.output-inline/server') const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) - expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"557k"`) + expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"558k"`) const modules = await analyzeSizes(['node_modules/**/*'], serverDir) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"94.4k"`) From ffe807bf3974fa061b8d48683f081a86f866be84 Mon Sep 17 00:00:00 2001 From: nikolay_koskarev Date: Thu, 7 Nov 2024 23:45:21 +0700 Subject: [PATCH 3/5] add experimental.spaPreloaderOutside flag --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 12 ++++++++---- packages/nuxt/src/core/templates.ts | 1 + packages/schema/src/config/experimental.ts | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 6e735c8a16..88b06d8526 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -30,7 +30,7 @@ import { renderSSRHeadOptions } from '#internal/unhead.config.mjs' import type { NuxtPayload, NuxtSSRContext } from '#app' // @ts-expect-error virtual file -import { appHead, appId, appRootAttrs, appRootTag, appSpaLoaderAttrs, appSpaLoaderTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp } from '#internal/nuxt.config.mjs' +import { appHead, appId, appRootAttrs, appRootTag, appSpaLoaderAttrs, appSpaLoaderTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp, spaPreloaderOutside } from '#internal/nuxt.config.mjs' // @ts-expect-error virtual file import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths' @@ -145,9 +145,13 @@ const getSPARenderer = lazyCachedFunction(async () => { // @ts-expect-error virtual file const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '') .then((r) => { - const appTemplate = APP_ROOT_OPEN_TAG + APP_ROOT_CLOSE_TAG - const loaderTemplate = r ? APP_SPA_LOADER_OPEN_TAG + r + APP_SPA_LOADER_CLOSE_TAG : '' - return appTemplate + loaderTemplate + if (spaPreloaderOutside) { + const appTemplate = APP_ROOT_OPEN_TAG + APP_ROOT_CLOSE_TAG + const loaderTemplate = r ? APP_SPA_LOADER_OPEN_TAG + r + APP_SPA_LOADER_CLOSE_TAG : '' + return appTemplate + loaderTemplate + } else { + return APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG + } }) const options = { diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index a21352deba..d657db731e 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -524,6 +524,7 @@ export const nuxtConfigTemplate: NuxtTemplate = { `export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`, `export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`, `export const crawlLinks = ${!!((ctx.nuxt as any)._nitro as Nitro).options.prerender.crawlLinks}`, + `export const spaPreloaderOutside = ${ctx.nuxt.options.experimental.spaPreloaderOutside}`, ].join('\n\n') }, } diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 00ae9e2e0c..9396527ef4 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -407,5 +407,11 @@ export default defineUntypedSchema({ return val ?? ((await get('future') as Record).compatibilityVersion === 4) }, }, + + /** + * Keep showing the spa-loading-template until suspense:resolve + * @see [Nuxt Issues #24770](https://github.com/nuxt/nuxt/issues/21721) + */ + spaPreloaderOutside: false, }, }) From b4a279dec1ec283bd647f7d88b3f9722fc6ddb99 Mon Sep 17 00:00:00 2001 From: nikolay_koskarev Date: Fri, 8 Nov 2024 01:09:29 +0700 Subject: [PATCH 4/5] add tests for sapPreloaderOutside --- test/fixtures/spa-loader/nuxt.config.ts | 8 +++- test/spa-loader-ssr.test.ts | 36 ---------------- .../spa-preloader-outside-disabled.test.ts | 41 +++++++++++++++++++ .../spa-preloader-outside-enabled.test.ts} | 26 +++++++++--- 4 files changed, 69 insertions(+), 42 deletions(-) delete mode 100644 test/spa-loader-ssr.test.ts create mode 100644 test/spa-loader/spa-preloader-outside-disabled.test.ts rename test/{spa-loader-no-ssr.test.ts => spa-loader/spa-preloader-outside-enabled.test.ts} (54%) diff --git a/test/fixtures/spa-loader/nuxt.config.ts b/test/fixtures/spa-loader/nuxt.config.ts index bc4c9c7869..e40b0471eb 100644 --- a/test/fixtures/spa-loader/nuxt.config.ts +++ b/test/fixtures/spa-loader/nuxt.config.ts @@ -1,6 +1,12 @@ export default defineNuxtConfig({ - ssr: false, devtools: { enabled: false }, spaLoadingTemplate: true, + routeRules: { + '/spa': { ssr: false }, + '/ssr': { ssr: true }, + }, + experimental: { + spaPreloaderOutside: false, + }, compatibilityDate: '2024-06-28', }) diff --git a/test/spa-loader-ssr.test.ts b/test/spa-loader-ssr.test.ts deleted file mode 100644 index b876ebbe74..0000000000 --- a/test/spa-loader-ssr.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' -import { isWindows } from 'std-env' -import { getBrowser, setup, url } from '@nuxt/test-utils' - -const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack' - -await setup({ - rootDir: fileURLToPath(new URL('./fixtures/spa-loader', import.meta.url)), - dev: process.env.TEST_ENV === 'dev', - server: true, - browser: true, - setupTimeout: (isWindows ? 360 : 120) * 1000, - nuxtConfig: { - builder: isWebpack ? 'webpack' : 'vite', - ssr: true, - spaLoadingTemplate: true, - }, -}) - -describe('spa-loader with SSR', () => { - it('should render content without spa-loader', async () => { - const browser = await getBrowser() - const page = await browser.newPage({}) - await page.goto(url('/'), { waitUntil: 'domcontentloaded' }) - - const loader = page.getByTestId('__nuxt-spa-loader') - expect(await loader.isVisible()).toBeFalsy() - - const content = page.getByTestId('content') - await content.waitFor({ state: 'visible' }) - expect(await loader.isHidden()).toBeTruthy() - - await page.close() - }, 60_000) -}) diff --git a/test/spa-loader/spa-preloader-outside-disabled.test.ts b/test/spa-loader/spa-preloader-outside-disabled.test.ts new file mode 100644 index 0000000000..83259d0ba5 --- /dev/null +++ b/test/spa-loader/spa-preloader-outside-disabled.test.ts @@ -0,0 +1,41 @@ +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { isWindows } from 'std-env' +import { $fetch, getBrowser, setup, url } from '@nuxt/test-utils' + +const isWebpack = + process.env.TEST_BUILDER === 'webpack' || + process.env.TEST_BUILDER === 'rspack' + +await setup({ + rootDir: fileURLToPath(new URL('../fixtures/spa-loader', import.meta.url)), + dev: process.env.TEST_ENV === 'dev', + server: true, + browser: true, + setupTimeout: (isWindows ? 360 : 120) * 1000, + nuxtConfig: { + builder: isWebpack ? 'webpack' : 'vite', + spaLoadingTemplate: true, + experimental: { + spaPreloaderOutside: false, + }, + }, +}) + +describe('spaPreloaderOutside flag is disabled', () => { + it('shoul be render loader inside appTag', async () => { + const html = await $fetch('/spa') + expect(html).toContain(`
loading...
\n
`) + }) + + it('spa-loader does not appear while the app is mounting', async () => { + const browser = await getBrowser() + const page = await browser.newPage({}) + await page.goto(url('/spa'), { waitUntil: 'domcontentloaded' }) + + const loader = page.getByTestId('__nuxt-spa-loader') + expect(await loader.isHidden()).toBeTruthy() + + await page.close() + }, 60_000) +}) diff --git a/test/spa-loader-no-ssr.test.ts b/test/spa-loader/spa-preloader-outside-enabled.test.ts similarity index 54% rename from test/spa-loader-no-ssr.test.ts rename to test/spa-loader/spa-preloader-outside-enabled.test.ts index 87d85415ea..71c873bd74 100644 --- a/test/spa-loader-no-ssr.test.ts +++ b/test/spa-loader/spa-preloader-outside-enabled.test.ts @@ -6,24 +6,25 @@ import { getBrowser, setup, url } from '@nuxt/test-utils' const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack' await setup({ - rootDir: fileURLToPath(new URL('./fixtures/spa-loader', import.meta.url)), + rootDir: fileURLToPath(new URL('../fixtures/spa-loader', import.meta.url)), dev: process.env.TEST_ENV === 'dev', server: true, browser: true, setupTimeout: (isWindows ? 360 : 120) * 1000, nuxtConfig: { builder: isWebpack ? 'webpack' : 'vite', - ssr: false, spaLoadingTemplate: true, + experimental: { + spaPreloaderOutside: true, + }, }, }) -describe('spa-loader with SPA', () => { +describe('spaPreloaderOutside flag is enabled', () => { it('should render spa-loader', async () => { const browser = await getBrowser() const page = await browser.newPage({}) - await page.goto(url('/'), { waitUntil: 'domcontentloaded' }) - + await page.goto(url('/spa'), { waitUntil: 'domcontentloaded' }) const loader = page.getByTestId('loader') expect(await loader.isVisible()).toBeTruthy() @@ -33,4 +34,19 @@ describe('spa-loader with SPA', () => { await page.close() }, 60_000) + + it('should render content without spa-loader', async () => { + const browser = await getBrowser() + const page = await browser.newPage({}) + await page.goto(url('/ssr'), { waitUntil: 'domcontentloaded' }) + + const loader = page.getByTestId('__nuxt-spa-loader') + expect(await loader.isHidden()).toBeTruthy() + + const content = page.getByTestId('content') + await content.waitFor({ state: 'visible' }) + expect(await loader.isHidden()).toBeTruthy() + + await page.close() + }, 60_000) }) From 45a5a540bba32bb20cd663f3ab413055d2e30fd6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:13:10 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- test/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 0374c26ae5..30855689da 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -78,7 +78,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM const serverDir = join(rootDir, '.output-inline/server') const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) - expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"558k"`) + expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"557k"`) const modules = await analyzeSizes(['node_modules/**/*'], serverDir) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"94.4k"`)