Skip to content

Commit

Permalink
implement injectPreloadAssets() (#262, #419, #420, #510)
Browse files Browse the repository at this point in the history
  • Loading branch information
brillout committed Nov 22, 2022
1 parent 5e45e5e commit 833d3c5
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 49 deletions.
38 changes: 24 additions & 14 deletions vite-plugin-ssr/node/html/injectAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { injectAssets }
export { injectAssetsToStream }
export type { PageContextInjectAssets }

// TODO: BREAK THIS
async function injectAssets__public(htmlString: string, pageContext: Record<string, unknown>): Promise<string> {
assertWarning(false, '`_injectAssets()` is deprecated and will be removed.', { onlyOnce: true, showStackTrace: true })
assertUsage(
Expand All @@ -31,7 +32,7 @@ async function injectAssets__public(htmlString: string, pageContext: Record<stri
assertUsage(hasProp(pageContext, '__getPageAssets'), errMsg('`pageContext.__getPageAssets` is missing'))
assertUsage(hasProp(pageContext, '_passToClient', 'string[]'), errMsg('`pageContext._passToClient` is missing'))
castProp<() => Promise<PageAsset[]>, typeof pageContext, '__getPageAssets'>(pageContext, '__getPageAssets')
htmlString = await injectAssets(htmlString, pageContext as any)
htmlString = await injectAssets(htmlString, pageContext as any, false)
return htmlString
}

Expand All @@ -48,9 +49,13 @@ type PageContextInjectAssets = {
_baseUrl: string
is404: null | boolean
}
async function injectAssets(htmlString: string, pageContext: PageContextInjectAssets): Promise<string> {
async function injectAssets(
htmlString: string,
pageContext: PageContextInjectAssets,
disableAutoInjectPreloadTags: boolean
): Promise<string> {
const { injectAtStreamBegin, injectAtStreamEnd } = injectAssetsToStream(pageContext, null)
htmlString = await injectAtStreamBegin(htmlString)
htmlString = await injectAtStreamBegin(htmlString, disableAutoInjectPreloadTags)
htmlString = await injectAtStreamEnd(htmlString)
return htmlString
}
Expand All @@ -63,14 +68,19 @@ function injectAssetsToStream(pageContext: PageContextInjectAssets, injectToStre
injectAtStreamEnd
}

async function injectAtStreamBegin(htmlBegin: string): Promise<string> {
async function injectAtStreamBegin(htmlBegin: string, disableAutoInjectPreloadTags: boolean): Promise<string> {
assert([true, false].includes(pageContext._isHtmlOnly))
const isHtmlOnly = pageContext._isHtmlOnly

assert(pageContext._pageContextPromise === null || pageContext._pageContextPromise)
const injectJavaScriptDuringStream = pageContext._pageContextPromise === null && !!injectToStream

htmlSnippets = await getHtmlSnippets(pageContext, { isHtmlOnly, injectJavaScriptDuringStream })
htmlSnippets = await getHtmlSnippets(
pageContext,
injectJavaScriptDuringStream,
isHtmlOnly,
disableAutoInjectPreloadTags
)
const htmlSnippetsAtBegin = htmlSnippets.filter((snippet) => snippet.position !== 'DOCUMENT_END')

// Ensure existence of `<head>`
Expand Down Expand Up @@ -107,15 +117,15 @@ type HtmlSnippet = {
}
async function getHtmlSnippets(
pageContext: PageContextInjectAssets,
{
isHtmlOnly,
injectJavaScriptDuringStream
}: {
injectJavaScriptDuringStream: boolean
isHtmlOnly: boolean
}
injectJavaScriptDuringStream: boolean,
isHtmlOnly: boolean,
disableAutoInjectPreloadTags: boolean
) {
const pageAssets = await pageContext.__getPageAssets()
let pageAssets = await pageContext.__getPageAssets()

if (disableAutoInjectPreloadTags) {
pageAssets = pageAssets.filter(({ isPreload }) => !isPreload)
}

const htmlSnippets: HtmlSnippet[] = []

Expand Down Expand Up @@ -180,7 +190,7 @@ async function getMergedScriptTag(
pageAssets: PageAsset[],
pageContext: PageContextInjectAssets
): Promise<null | string> {
const scriptAssets = pageAssets.filter((pageAsset) => pageAsset.isEntry && pageAsset.assetType === 'script')
const scriptAssets = pageAssets.filter((pageAsset) => !pageAsset.isPreload && pageAsset.assetType === 'script')
const viteScripts = await getViteDevScripts(pageContext)
const scriptTagsHtml = `${viteScripts}${scriptAssets.map(inferAssetTag).join('')}`
const scriptTag = mergeScriptTags(scriptTagsHtml, pageContext)
Expand Down
105 changes: 75 additions & 30 deletions vite-plugin-ssr/node/html/renderHtml.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { assert, assertUsage, assertWarning, checkType, hasProp, isPromise, objectAssign } from '../utils'
import { assert, assertUsage, assertWarning, checkType, hasProp, isPromise, objectAssign, isObject } from '../utils'
import { injectAssets, injectAssetsToStream } from './injectAssets'
import type { PageContextInjectAssets } from './injectAssets'
import { processStream, isStream, Stream, streamToString, StreamTypePatch } from './stream'
import { isStreamReactStreaming } from './stream/react-streaming'
import type { InjectToStream } from 'react-streaming/server'
import type { PageAsset } from '../renderPage/getPageAssets'
import { inferPreloadTag } from './injectAssets/inferHtmlTags'

// Public
export { escapeInject }
export type { TemplateWrapped } // https://github.com/brillout/vite-plugin-ssr/issues/511
export { dangerouslySkipEscape }
export { injectPreloadTags }

// Private
export { renderHtml }
Expand All @@ -19,8 +22,15 @@ export type { HtmlRender }
type DocumentHtml = TemplateWrapped | EscapedString | Stream
type HtmlRender = string | Stream

type PageAssetPublic = {
src: PageAsset['src']
assetType: PageAsset['assetType']
mediaType: PageAsset['mediaType']
}
type InjectPreloadTags = { _injectPreloadTags: true | ((filter: PageAssetPublic[]) => PageAssetPublic[]) }

type TemplateStrings = TemplateStringsArray
type TemplateVariable = string | EscapedString | Stream | TemplateWrapped
type TemplateVariable = string | EscapedString | Stream | TemplateWrapped | InjectPreloadTags
type TemplateWrapped = {
_template: TemplateContent
}
Expand All @@ -37,6 +47,10 @@ function isDocumentHtml(something: unknown): something is DocumentHtml {
return false
}

type PageContextRenderHtml = PageContextInjectAssets & {
__getPageAssets: () => Promise<PageAsset[]>
}

async function renderHtml(
documentHtml: DocumentHtml,
pageContext: PageContextInjectAssets,
Expand All @@ -45,33 +59,33 @@ async function renderHtml(
): Promise<HtmlRender> {
if (isEscapedString(documentHtml)) {
let htmlString = getEscapedString(documentHtml)
htmlString = await injectAssets(htmlString, pageContext)
htmlString = await injectAssets(htmlString, pageContext, false)
return htmlString
}
if (isStream(documentHtml)) {
const stream = documentHtml
const streamWrapper = await renderHtmlStream(stream, {
pageContext,
onErrorWhileStreaming
})
const streamWrapper = await renderHtmlStream(stream, null, pageContext, onErrorWhileStreaming, false)
return streamWrapper
}
if (isTemplateWrapped(documentHtml)) {
const templateContent = documentHtml._template
const render = renderTemplate(templateContent, renderFilePath)
const render = await renderTemplate(templateContent, renderFilePath, pageContext)
if ('htmlString' in render) {
let { htmlString } = render
htmlString = await injectAssets(htmlString, pageContext)
let { htmlString, disableAutoInjectPreloadTags } = render
htmlString = await injectAssets(htmlString, pageContext, disableAutoInjectPreloadTags)
return htmlString
} else {
const streamWrapper = await renderHtmlStream(render.htmlStream, {
injectString: {
const { htmlStream, disableAutoInjectPreloadTags } = render
const streamWrapper = await renderHtmlStream(
htmlStream,
{
stringBegin: render.stringBegin,
stringEnd: render.stringEnd
},
pageContext,
onErrorWhileStreaming
})
onErrorWhileStreaming,
disableAutoInjectPreloadTags
)
return streamWrapper
}
}
Expand All @@ -81,15 +95,10 @@ async function renderHtml(

async function renderHtmlStream(
streamOriginal: Stream & { injectionBuffer?: string[] },
{
injectString,
pageContext,
onErrorWhileStreaming
}: {
injectString?: { stringBegin: string; stringEnd: string }
pageContext: PageContextInjectAssets & { enableEagerStreaming?: boolean }
onErrorWhileStreaming: (err: unknown) => void
}
injectString: null | { stringBegin: string; stringEnd: string },
pageContext: PageContextInjectAssets & { enableEagerStreaming?: boolean },
onErrorWhileStreaming: (err: unknown) => void,
disableAutoInjectPreloadTags: boolean
) {
const opts = {
onErrorWhileStreaming,
Expand All @@ -103,7 +112,7 @@ async function renderHtmlStream(
const { injectAtStreamBegin, injectAtStreamEnd } = injectAssetsToStream(pageContext, injectToStream)
objectAssign(opts, {
injectStringAtBegin: async () => {
return await injectAtStreamBegin(injectString.stringBegin)
return await injectAtStreamBegin(injectString.stringBegin, disableAutoInjectPreloadTags)
},
injectStringAtEnd: async () => {
return await injectAtStreamEnd(injectString.stringEnd)
Expand Down Expand Up @@ -169,13 +178,19 @@ function _dangerouslySkipEscape(arg: unknown): EscapedString {
return { _escaped: arg }
}

function renderTemplate(
async function renderTemplate(
templateContent: TemplateContent,
renderFilePath: string
): { htmlString: string } | { htmlStream: Stream; stringBegin: string; stringEnd: string } {
renderFilePath: string,
pageContext: PageContextRenderHtml
): Promise<
({ htmlString: string } | { htmlStream: Stream; stringBegin: string; stringEnd: string }) & {
disableAutoInjectPreloadTags: boolean
}
> {
let stringBegin = ''
let htmlStream: null | Stream = null
let stringEnd = ''
let disableAutoInjectPreloadTags = false

const addString = (str: string) => {
assert(typeof str === 'string')
Expand Down Expand Up @@ -210,7 +225,7 @@ function renderTemplate(
// Process `escapeInject` fragments
if (isTemplateWrapped(templateVar)) {
const templateContentInner = templateVar._template
const result = renderTemplate(templateContentInner, renderFilePath)
const result = await renderTemplate(templateContentInner, renderFilePath, pageContext)
if ('htmlString' in result) {
addString(result.htmlString)
} else {
Expand Down Expand Up @@ -244,6 +259,30 @@ function renderTemplate(
continue
}

if (isObject(templateVar) && '_injectPreloadTags' in templateVar) {
const pageAssets = await pageContext.__getPageAssets()
let pageAssetsToInject = pageAssets.filter(({ isPreload }) => isPreload)
const userFilter = templateVar._injectPreloadTags === true ? null : templateVar._injectPreloadTags
if (userFilter) {
const pageAssetsPublic = pageAssetsToInject.map((p) => {
return {
src: p.src,
assetType: p.assetType,
mediaType: p.mediaType
}
})
pageAssetsToInject = userFilter(pageAssetsPublic).map((pageAssetPublic: PageAssetPublic) => {
const pageAsset: PageAsset = { ...pageAssetPublic, isPreload: true }
return pageAsset
})
}
pageAssetsToInject.forEach((pageAsset) => {
addString(inferPreloadTag(pageAsset))
})
disableAutoInjectPreloadTags = true
continue
}

{
const varType = typeof templateVar
const streamNote = ['boolean', 'number', 'bigint', 'symbol'].includes(varType)
Expand All @@ -262,14 +301,16 @@ function renderTemplate(
if (htmlStream === null) {
assert(stringEnd === '')
return {
htmlString: stringBegin
htmlString: stringBegin,
disableAutoInjectPreloadTags
}
}

return {
htmlStream,
stringBegin,
stringEnd
stringEnd,
disableAutoInjectPreloadTags
}
}

Expand All @@ -294,3 +335,7 @@ async function getHtmlString(htmlRender: HtmlRender): Promise<string> {
checkType<never>(htmlRender)
assert(false)
}

function injectPreloadTags(filter?: (filter: PageAssetPublic[]) => PageAssetPublic[]): InjectPreloadTags {
return { _injectPreloadTags: filter ?? true }
}
1 change: 1 addition & 0 deletions vite-plugin-ssr/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { escapeInject, dangerouslySkipEscape } from './html/renderHtml'
export { pipeWebStream, pipeNodeStream, pipeStream, stampPipe } from './html/stream'
export { injectAssets__public as _injectAssets } from './html/injectAssets'
export { RenderErrorPage } from './renderPage/RenderErrorPage'
export { injectPreloadTags } from './html/renderHtml'

export type { PageContextBuiltIn } from './types'

Expand Down
2 changes: 1 addition & 1 deletion vite-plugin-ssr/node/renderPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ async function loadPageFilesServer(pageContext: { _pageId: string } & PageContex
}[] = []

;(await pageContextAddendum.__getPageAssets()).forEach((p) => {
if (p.assetType === 'script' && p.isEntry) {
if (p.assetType === 'script' && !p.isPreload) {
pageAssetsOldFormat.push({
src: p.src,
preloadType: null,
Expand Down
8 changes: 4 additions & 4 deletions vite-plugin-ssr/node/renderPage/getPageAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type PageAsset = {
src: string
assetType: null | NonNullable<MediaType>['assetType']
mediaType: null | NonNullable<MediaType>['mediaType']
isEntry: boolean
isPreload: boolean
}

async function getPageAssets(
Expand Down Expand Up @@ -63,7 +63,7 @@ async function getPageAssets(
src: clientEntrySrc,
assetType: 'script',
mediaType: 'text/javascript',
isEntry: true
isPreload: false
})
})
assetUrls.forEach((src) => {
Expand All @@ -84,8 +84,8 @@ async function getPageAssets(
src,
assetType,
mediaType,
// Vite automatically injects CSS, not only in development, but also in production (albeit FOUC). Therefore, strictly speaking, CSS aren't entries. We still, however, set `isEntry: true` for CSS, in order to denote page assets that should absolutely be injected in the HTML, regardless of preload strategy (not injecting CSS leads to FOUC).
isEntry: assetType === 'style'
// Vite automatically injects CSS, not only in development, but also in production (albeit FOUC). Therefore, strictly speaking, CSS aren't entries. We still, however, set `isPreload: false` for CSS, in order to denote page assets that should absolutely be injected in the HTML, regardless of preload strategy (not injecting CSS leads to FOUC).
isPreload: assetType !== 'style'
})
})

Expand Down

0 comments on commit 833d3c5

Please sign in to comment.