Nuxt/packages/nuxt/src/app/composables/payload.ts

134 lines
3.8 KiB
TypeScript

import { hasProtocol, joinURL } from 'ufo'
import { parse } from 'devalue'
import { useHead } from '@unhead/vue'
import { getCurrentInstance } from 'vue'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
// @ts-expect-error virtual import
import { renderJsonPayloads } from '#build/nuxt.config.mjs'
interface LoadPayloadOptions {
fresh?: boolean
hash?: string
}
export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record<string, any> | Promise<Record<string, any>> | null {
if (process.server) { return null }
const payloadURL = _getPayloadURL(url, opts)
const nuxtApp = useNuxtApp()
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
if (cache[payloadURL]) {
return cache[payloadURL]
}
cache[payloadURL] = _importPayload(payloadURL).then((payload) => {
if (!payload) {
delete cache[payloadURL]
return null
}
return payload
})
return cache[payloadURL]
}
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) {
const payloadURL = _getPayloadURL(url, opts)
useHead({
link: [
{ rel: 'modulepreload', href: payloadURL }
]
})
}
// --- Internal ---
const extension = renderJsonPayloads ? 'json' : 'js'
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
const u = new URL(url, 'http://localhost')
if (u.search) {
throw new Error('Payload URL cannot contain search params: ' + url)
}
if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) {
throw new Error('Payload URL must not include hostname: ' + url)
}
const hash = opts.hash || (opts.fresh ? Date.now() : '')
return joinURL(useRuntimeConfig().app.baseURL, u.pathname, hash ? `_payload.${hash}.${extension}` : `_payload.${extension}`)
}
async function _importPayload (payloadURL: string) {
if (process.server) { return null }
try {
return renderJsonPayloads
? parsePayload(await fetch(payloadURL).then(res => res.text()))
: await import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r)
} catch (err) {
console.warn('[nuxt] Cannot load payload ', payloadURL, err)
}
return null
}
export function isPrerendered () {
// Note: Alternative for server is checking x-nitro-prerender header
const nuxtApp = useNuxtApp()
return !!nuxtApp.payload.prerenderedAt
}
let payloadCache: any = null
export async function getNuxtClientPayload () {
if (process.server) {
return
}
if (payloadCache) {
return payloadCache
}
const el = document.getElementById('__NUXT_DATA__')
if (!el) {
return {}
}
const inlineData = parsePayload(el.textContent || '')
const externalData = el.dataset.src ? await _importPayload(el.dataset.src) : undefined
payloadCache = {
...inlineData,
...externalData,
...window.__NUXT__
}
return payloadCache
}
export function parsePayload (payload: string) {
return parse(payload, useNuxtApp()._payloadRevivers)
}
/**
* This is an experimental function for configuring passing rich data from server -> client.
*/
export function definePayloadReducer (
name: string,
reduce: (data: any) => any
) {
if (process.server) {
useNuxtApp().ssrContext!._payloadReducers[name] = reduce
}
}
/**
* This is an experimental function for configuring passing rich data from server -> client.
*
* This function _must_ be called in a Nuxt plugin that is `unshift`ed to the beginning of the Nuxt plugins array.
*/
export function definePayloadReviver (
name: string,
revive: (data: string) => any | undefined
) {
if (process.dev && getCurrentInstance()) {
console.warn('[nuxt] [definePayloadReviver] This function must be called in a Nuxt plugin that is `unshift`ed to the beginning of the Nuxt plugins array.')
}
if (process.client) {
useNuxtApp()._payloadRevivers[name] = revive
}
}