Skip to content

Commit

Permalink
feat(nuxt): allow configuring spa loading indicator (#21640)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Jun 20, 2023
1 parent 343a46d commit c66c82e
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 6 deletions.
5 changes: 5 additions & 0 deletions docs/2.guide/1.concepts/3.rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export default defineNuxtConfig({
})
```

::alert{type=info}
If you do use `ssr: false`, you should also place an HTML file in `~/app/spa-loading-template.html` with some HTML you would like to use to render a loading screen that will be rendered until your app is hydrated.
:ReadMore{link="/docs/api/configuration/nuxt-config#spaloadingindicator"}
::

## Hybrid Rendering

Hybrid rendering allows different caching rules per route using **Route Rules** and decides how the server should respond to a new request on a given URL.
Expand Down
21 changes: 19 additions & 2 deletions packages/nuxt/src/core/nitro.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync, promises as fsp } from 'node:fs'
import { existsSync, promises as fsp, readFileSync } from 'node:fs'
import { join, relative, resolve } from 'pathe'
import { build, copyPublicAssets, createDevServer, createNitro, prepare, prerender, scanHandlers, writeTypes } from 'nitropack'
import type { Nitro, NitroConfig } from 'nitropack'
Expand All @@ -10,6 +10,8 @@ import { dynamicEventHandler } from 'h3'
import { createHeadCore } from '@unhead/vue'
import { renderSSRHead } from '@unhead/ssr'
import type { Nuxt } from 'nuxt/schema'
// @ts-expect-error TODO: add legacy type support for subpath imports
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'

import { distDir } from '../dirs'
import { ImportProtectionPlugin } from './plugins/import-protection'
Expand All @@ -29,6 +31,13 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
? [new RegExp(`node_modules\\/(?!${excludePaths.join('|')})`)]
: [/node_modules/]

const spaLoadingTemplatePath = nuxt.options.spaLoadingTemplate ?? resolve(nuxt.options.srcDir, 'app/spa-loading-template.html')
if (spaLoadingTemplatePath !== false && !existsSync(spaLoadingTemplatePath)) {
if (nuxt.options.spaLoadingTemplate) {
console.warn(`[nuxt] Could not load custom \`spaLoadingTemplate\` path as it does not exist: \`${spaLoadingTemplatePath}\`.`)
}
}

const nitroConfig: NitroConfig = defu(_nitroConfig, <NitroConfig>{
debug: nuxt.options.debug,
rootDir: nuxt.options.rootDir,
Expand Down Expand Up @@ -75,7 +84,15 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
devHandlers: [],
baseURL: nuxt.options.app.baseURL,
virtual: {
'#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config']
'#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config'],
'#spa-template': () => {
try {
if (spaLoadingTemplatePath) {
return `export const template = ${JSON.stringify(readFileSync(spaLoadingTemplatePath, 'utf-8'))}`
}
} catch {}
return `export const template = ${JSON.stringify(defaultSpaLoadingTemplate({}))}`
}
},
routeRules: {
'/__nuxt_error': { cache: false }
Expand Down
5 changes: 4 additions & 1 deletion packages/nuxt/src/core/runtime/nitro/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,12 @@ const getSSRRenderer = lazyCachedFunction(async () => {
const getSPARenderer = lazyCachedFunction(async () => {
const manifest = await getClientManifest()

// @ts-expect-error virtual file
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')

const options = {
manifest,
renderToString: () => `<${appRootTag} id="${appRootId}"></${appRootTag}>`,
renderToString: () => `<${appRootTag} id="${appRootId}">${spaTemplate}</${appRootTag}>`,
buildAssetsURL
}
// Create SPA renderer and cache the result for all requests
Expand Down
60 changes: 60 additions & 0 deletions packages/schema/src/config/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineUntypedSchema } from 'untyped'
import { defu } from 'defu'
import { resolve } from 'pathe'
import type { AppHeadMetaObject } from '../types/head'

export default defineUntypedSchema({
Expand Down Expand Up @@ -177,6 +178,65 @@ export default defineUntypedSchema({
rootTag: 'div',
},

/** A path to an HTML file, the contents of which will be inserted into any HTML page
* rendered with `ssr: false`.
*
* By default Nuxt will look in `~/app/spa-loading-template.html` for this file.
*
* You can set this to `false` to disable any loading indicator.
*
* Some good sources for spinners are [SpinKit](https://github.com/tobiasahlin/SpinKit) or [SVG Spinners](https://icones.js.org/collection/svg-spinners).
*
* @example ~/app/spa-loading-template.html
* ```html
* <!-- https://github.com/barelyhuman/snips/blob/dev/pages/css-loader.md -->
* <div class="loader"></div>
* <style>
* .loader {
* display: block;
* position: fixed;
* z-index: 1031;
* top: 50%;
* left: 50%;
* transform: translate(-50%, -50%);
* width: 18px;
* height: 18px;
* box-sizing: border-box;
* border: solid 2px transparent;
* border-top-color: #000;
* border-left-color: #000;
* border-bottom-color: #efefef;
* border-right-color: #efefef;
* border-radius: 50%;
* -webkit-animation: loader 400ms linear infinite;
* animation: loader 400ms linear infinite;
* }
*
* \@-webkit-keyframes loader {
* 0% {
* -webkit-transform: rotate(0deg);
* }
* 100% {
* -webkit-transform: rotate(360deg);
* }
* }
* \@keyframes loader {
* 0% {
* transform: rotate(0deg);
* }
* 100% {
* transform: rotate(360deg);
* }
* }
* </style>
* ```
*
* @type {string | false}
*/
spaLoadingTemplate: {
$resolve: async (val, get) => typeof val === 'string' ? resolve(await get('srcDir'), val) : (val ?? null)
},

/**
* An array of nuxt app plugins.
*
Expand Down
15 changes: 13 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM

it('default server bundle size', async () => {
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"61.3k"')
expect.soft(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"62.1k"')

const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2295k"')
Expand Down

0 comments on commit c66c82e

Please sign in to comment.