Skip to content

Commit

Permalink
feat: implement injectFilter() (closes #262, closes #419, closes #420
Browse files Browse the repository at this point in the history
…, closes #510)
  • Loading branch information
brillout committed Nov 22, 2022
1 parent f209277 commit e0785a6
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 46 deletions.
2 changes: 1 addition & 1 deletion test/preload/prod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('preload tags', () => {
<body>
<div id=\\"page-view\\"><div style=\\"display:flex;max-width:900px;margin:auto\\"><div style=\\"padding:20px;padding-top:20px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;line-height:1.8em\\"><div style=\\"margin-top:20px;margin-bottom:10px\\"><a href=\\"/\\"><img src=\\"/assets/logo.$HASH.svg\\" height=\\"64\\" width=\\"64\\"/></a></div><a class=\\"navitem\\" href=\\"/\\">Preload Default</a><a class=\\"navitem\\" href=\\"/preload-disabled\\">Preload Disabled</a><a class=\\"navitem\\" href=\\"/preload-font-only\\">Preload Only Font</a></div><div style=\\"padding:20px;padding-bottom:50px;border-left:2px solid #eee;min-height:100vh\\"><h1>Default</h1><p>This page showcases the default preloading strategy: in production, both the image and the font are preloaded.</p></div></div></div>
<script id=\\"vite-plugin-ssr_pageContext\\" type=\\"application/json\\">{\\"pageContext\\":{\\"_pageId\\":\\"/pages/index\\",\\"pageProps\\":\\"!undefined\\"}}</script><script type=\\"module\\" src=\\"/assets/entry-server-routing.$HASH.js\\" async></script><link rel=\\"preload\\" href=\\"/assets/logo.$HASH.svg\\" as=\\"image\\" type=\\"image/svg+xml\\"><link rel=\\"modulepreload\\" href=\\"/assets/entry-server-routing.$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/pages/index.page.$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/chunk-$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/renderer/_default.page.client.$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/chunk-$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"></body>
<link rel=\\"preload\\" href=\\"/assets/logo.$HASH.svg\\" as=\\"image\\" type=\\"image/svg+xml\\"><script id=\\"vite-plugin-ssr_pageContext\\" type=\\"application/json\\">{\\"pageContext\\":{\\"_pageId\\":\\"/pages/index\\",\\"pageProps\\":\\"!undefined\\"}}</script><script type=\\"module\\" src=\\"/assets/entry-server-routing.$HASH.js\\" async></script><link rel=\\"modulepreload\\" href=\\"/assets/entry-server-routing.$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/pages/index.page.$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/chunk-$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/renderer/_default.page.client.$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"><link rel=\\"modulepreload\\" href=\\"/assets/chunk-$HASH.js\\" as=\\"script\\" type=\\"text/javascript\\"></body>
</html>"
`
)
Expand Down
84 changes: 56 additions & 28 deletions vite-plugin-ssr/node/html/injectAssets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { injectHtmlTagsToString }
export { injectHtmlTagsToStream }
export type { PageContextInjectAssets }
export type { PreloadFilter }
export { injectAssets__public }

import { assert, assertUsage, assertWarning, castProp, hasProp } from '../utils'
Expand Down Expand Up @@ -33,7 +34,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 injectHtmlTagsToString([htmlString], pageContext as any, false)
htmlString = await injectHtmlTagsToString([htmlString], pageContext as any, false, null)
return htmlString
}

Expand All @@ -51,20 +52,34 @@ type PageContextInjectAssets = {
is404: null | boolean
}

type PreloadFilter = null | ((assets: PreloadFilterEntry[]) => PreloadFilterEntry[])
type PreloadFilterEntry = {
src: string
assetType: null | PageAsset['assetType']
mediaType: null | PageAsset['mediaType']
isPreload: boolean
inject: null | 'HTML_BEGIN' | 'HTML_END'
}

async function injectHtmlTagsToString(
htmlParts: HtmlPart[],
pageContext: PageContextInjectAssets,
disableAutoInjectPreloadTags: boolean
disableAutoInjectPreloadTags: boolean,
preloadFilter: PreloadFilter
): Promise<string> {
const htmlSnippets = await getHtmlSnippets(pageContext, null, disableAutoInjectPreloadTags)
const htmlSnippets = await getHtmlSnippets(pageContext, null, disableAutoInjectPreloadTags, preloadFilter)
const pageAssets = await pageContext.__getPageAssets()
let htmlString = htmlPartsToString(htmlParts, pageAssets)
htmlString = injectToHtmlBegin(htmlString, htmlSnippets, null)
htmlString = injectToHtmlEnd(htmlString, htmlSnippets)
return htmlString
}

function injectHtmlTagsToStream(pageContext: PageContextInjectAssets, injectToStream: null | InjectToStream) {
function injectHtmlTagsToStream(
pageContext: PageContextInjectAssets,
injectToStream: null | InjectToStream,
preloadFilter: PreloadFilter
) {
let htmlSnippets: HtmlSnippet[] | undefined

return {
Expand All @@ -76,7 +91,7 @@ function injectHtmlTagsToStream(pageContext: PageContextInjectAssets, injectToSt
htmlPartsBegin: HtmlPart[],
disableAutoInjectPreloadTags: boolean
): Promise<string> {
htmlSnippets = await getHtmlSnippets(pageContext, injectToStream, disableAutoInjectPreloadTags)
htmlSnippets = await getHtmlSnippets(pageContext, injectToStream, disableAutoInjectPreloadTags, preloadFilter)

const pageAssets = await pageContext.__getPageAssets()
let htmlBegin = htmlPartsToString(htmlPartsBegin, pageAssets)
Expand Down Expand Up @@ -130,12 +145,16 @@ async function resolvePageContextPromise(pageContext: {

type HtmlSnippet = {
htmlSnippet: string | (() => string)
// TODO: remove HEAD_CLOSING
position: 'HEAD_CLOSING' | 'HEAD_OPENING' | 'DOCUMENT_END' | 'STREAM'
}
// TODO: rename htmlSnippets => htmtTags
// TODO: move getHtmlSnippets to own module
async function getHtmlSnippets(
pageContext: PageContextInjectAssets,
injectToStream: null | InjectToStream,
disableAutoInjectPreloadTags: boolean
disableAutoInjectPreloadTags: boolean,
preloadFilter: PreloadFilter
) {
assert([true, false].includes(pageContext._isHtmlOnly))
const isHtmlOnly = pageContext._isHtmlOnly
Expand All @@ -145,12 +164,43 @@ async function getHtmlSnippets(

let pageAssets = await pageContext.__getPageAssets()

// TODO: remove
if (disableAutoInjectPreloadTags) {
pageAssets = pageAssets.filter(({ isPreload }) => !isPreload)
}

const htmlSnippets: HtmlSnippet[] = []

let preloadFilterEntries: PreloadFilterEntry[] = pageAssets
.filter((p) => p.assetType !== 'script')
.map((p) => ({
src: p.src,
assetType: p.assetType,
mediaType: p.mediaType,
isPreload: p.isPreload,
inject: p.assetType === 'style' || p.assetType === 'font' ? 'HTML_BEGIN' : 'HTML_END'
}))
if (preloadFilter) {
preloadFilterEntries = preloadFilter(preloadFilterEntries)
}
preloadFilterEntries.forEach((a) => {
if (a.assetType === 'style') {
// In development, Vite automatically inject styles, but we still inject `<link rel="stylesheet" type="text/css" href="${src}">` tags in order to avoid FOUC (flash of unstyled content).
// - https://github.com/vitejs/vite/issues/2282
// - https://github.com/brillout/vite-plugin-ssr/issues/261
// TODO: enforce `showStackTrace`
assertWarning(a.inject, `We recommend against not injecting ${a.src}`, { onlyOnce: true, showStackTrace: false })
}
})
for (const entry of preloadFilterEntries) {
if (entry.inject) {
const htmlSnippet = entry.isPreload ? inferPreloadTag(entry) : inferAssetTag(entry)
// TODO: align naming
const position = entry.inject === 'HTML_BEGIN' ? 'HEAD_OPENING' : 'DOCUMENT_END'
htmlSnippets.push({ htmlSnippet, position })
}
}

const positionJs = injectJavaScriptDuringStream ? 'STREAM' : 'DOCUMENT_END'

// Serialized pageContext
Expand All @@ -169,7 +219,6 @@ async function getHtmlSnippets(
position: positionJs
})
}

for (const pageAsset of pageAssets) {
const { assetType } = pageAsset

Expand All @@ -182,27 +231,6 @@ async function getHtmlSnippets(
}
continue
}

// CSS
if (assetType === 'style') {
// In development, Vite automatically inject styles, but we still inject `<link rel="stylesheet" type="text/css" href="${src}">` tags in order to avoid FOUC (flash of unstyled content).
// - https://github.com/vitejs/vite/issues/2282
// - https://github.com/brillout/vite-plugin-ssr/issues/261
const htmlSnippet = inferAssetTag(pageAsset)
htmlSnippets.push({ htmlSnippet, position: 'HEAD_OPENING' })
continue
}

// Fonts
if (assetType === 'font') {
const htmlSnippet = inferPreloadTag(pageAsset)
htmlSnippets.push({ htmlSnippet, position: 'HEAD_OPENING' })
continue
}

// Other (e.g. images)
const htmlSnippet = inferPreloadTag(pageAsset)
htmlSnippets.push({ htmlSnippet, position: 'DOCUMENT_END' })
}

return htmlSnippets
Expand Down
28 changes: 20 additions & 8 deletions vite-plugin-ssr/node/html/renderHtml.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, assertUsage, assertWarning, checkType, hasProp, isPromise, objectAssign, isObject } from '../utils'
import { injectHtmlTagsToString, injectHtmlTagsToStream } from './injectAssets'
import { injectHtmlTagsToString, injectHtmlTagsToStream, type PreloadFilter } from './injectAssets'
import type { PageContextInjectAssets } from './injectAssets'
import { processStream, isStream, Stream, streamToString, StreamTypePatch } from './stream'
import { isStreamReactStreaming } from './stream/react-streaming'
Expand Down Expand Up @@ -57,24 +57,30 @@ async function renderDocumentHtml(
_isProduction: boolean
},
renderFilePath: string,
onErrorWhileStreaming: (err: unknown) => void
onErrorWhileStreaming: (err: unknown) => void,
preloadFilter: PreloadFilter
): Promise<HtmlRender> {
if (isEscapedString(documentHtml)) {
let htmlString = getEscapedString(documentHtml)
htmlString = await injectHtmlTagsToString([htmlString], pageContext, false)
htmlString = await injectHtmlTagsToString([htmlString], pageContext, false, preloadFilter)
return htmlString
}
if (isStream(documentHtml)) {
const stream = documentHtml
const streamWrapper = await renderHtmlStream(stream, null, pageContext, onErrorWhileStreaming, false)
const streamWrapper = await renderHtmlStream(stream, null, pageContext, onErrorWhileStreaming, false, preloadFilter)
return streamWrapper
}
if (isTemplateWrapped(documentHtml)) {
const templateContent = documentHtml._template
const render = await renderTemplate(templateContent, renderFilePath, pageContext)
if (!('htmlStream' in render)) {
const { htmlPartsAll, disableAutoInjectPreloadTags } = render
const htmlString = await injectHtmlTagsToString(htmlPartsAll, pageContext, disableAutoInjectPreloadTags)
const htmlString = await injectHtmlTagsToString(
htmlPartsAll,
pageContext,
disableAutoInjectPreloadTags,
preloadFilter
)
return htmlString
} else {
const { htmlStream, disableAutoInjectPreloadTags } = render
Expand All @@ -86,7 +92,8 @@ async function renderDocumentHtml(
},
pageContext,
onErrorWhileStreaming,
disableAutoInjectPreloadTags
disableAutoInjectPreloadTags,
preloadFilter
)
return streamWrapper
}
Expand All @@ -100,7 +107,8 @@ async function renderHtmlStream(
injectString: null | { htmlPartsBegin: HtmlPart[]; htmlPartsEnd: HtmlPart[] },
pageContext: PageContextInjectAssets & { enableEagerStreaming?: boolean; _isProduction: boolean },
onErrorWhileStreaming: (err: unknown) => void,
disableAutoInjectPreloadTags: boolean
disableAutoInjectPreloadTags: boolean,
preloadFilter: PreloadFilter
) {
const opts = {
onErrorWhileStreaming,
Expand All @@ -111,7 +119,11 @@ async function renderHtmlStream(
if (isStreamReactStreaming(streamOriginal) && !streamOriginal.disabled) {
injectToStream = streamOriginal.injectToStream
}
const { injectAtStreamBegin, injectAtStreamEnd } = injectHtmlTagsToStream(pageContext, injectToStream)
const { injectAtStreamBegin, injectAtStreamEnd } = injectHtmlTagsToStream(
pageContext,
injectToStream,
preloadFilter
)
objectAssign(opts, {
injectStringAtBegin: async () => {
return await injectAtStreamBegin(injectString.htmlPartsBegin, disableAutoInjectPreloadTags)
Expand Down
29 changes: 25 additions & 4 deletions vite-plugin-ssr/node/renderPage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { getErrorPageId, route, isErrorPageId, RouteMatches } from '../shared/route'
import { type HtmlRender, isDocumentHtml, renderDocumentHtml, getHtmlString, type PageAssetPublic } from './html/renderHtml'
import {
type HtmlRender,
isDocumentHtml,
renderDocumentHtml,
getHtmlString,
type PageAssetPublic
} from './html/renderHtml'
import { PageFile, PageContextExports, getExportUnion, getPageFilesAll, ExportsAll } from '../shared/getPageFiles'
import { analyzePageClientSide, analyzePageClientSideInit } from '../shared/getPageFiles/analyzePageClientSide'
import { getHook } from '../shared/getHook'
Expand All @@ -20,7 +26,8 @@ import {
makeFirst,
isSameErrorMessage,
createDebugger,
callHookWithTimeout
callHookWithTimeout,
isCallable
} from './utils'
import { getPageAssets, type PageAsset } from './renderPage/getPageAssets'
import { sortPageContext } from '../shared/sortPageContext'
Expand Down Expand Up @@ -56,6 +63,7 @@ import { loadPageFilesServerSide } from '../shared/getPageFiles/analyzePageServe
import { handlePageContextRequestUrl } from './renderPage/handlePageContextRequestUrl'
import type { MediaType } from './html/inferMediaType'
import { inferEarlyHintLink } from './html/injectAssets/inferHtmlTags'
import type { PreloadFilter } from './html/injectAssets'

export { renderPage }
export { prerenderPage }
Expand Down Expand Up @@ -821,7 +829,7 @@ async function executeRenderHook(
preparePageContextForRelease(pageContext)
const result = await callHookWithTimeout(() => render(pageContext), 'render', hook.filePath)
if (isObject(result) && !isDocumentHtml(result)) {
assertHookResult(result, 'render', ['documentHtml', 'pageContext'] as const, renderFilePath)
assertHookResult(result, 'render', ['documentHtml', 'pageContext', 'preloadFilter'] as const, renderFilePath)
}
objectAssign(pageContext, { _renderHook: { hookFilePath: renderFilePath, hookName: 'render' as const } })

Expand Down Expand Up @@ -901,7 +909,20 @@ async function executeRenderHook(
})
*/
}
const htmlRender = await renderDocumentHtml(documentHtml, pageContext, renderFilePath, onErrorWhileStreaming)

let preloadFilter: PreloadFilter = null
if (hasProp(result, 'preloadFilter')) {
assertUsage(isCallable(result.preloadFilter), 'preloadFilter should be a function')
preloadFilter = result.preloadFilter
}

const htmlRender = await renderDocumentHtml(
documentHtml,
pageContext,
renderFilePath,
onErrorWhileStreaming,
preloadFilter
)
assert(typeof htmlRender === 'string' || isStream(htmlRender))
return { htmlRender, renderFilePath }
}
Expand Down
2 changes: 1 addition & 1 deletion vite-plugin-ssr/shared/getHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { assert, assertUsage, isCallable } from './utils'
function getHook(
pageContext: PageContextExports,
hookName: 'render' | 'onBeforeRender' | 'onBeforePrerender' | 'onBeforeRoute'
): null | { hook: Function; filePath: string } {
): null | { hook: (arg: unknown) => unknown; filePath: string } {
if (!(hookName in pageContext.exports)) {
return null
}
Expand Down
2 changes: 1 addition & 1 deletion vite-plugin-ssr/utils/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const globalObject = getGlobalObject<{ alreadyLogged: Set<string> }>('assert.ts'
function assertWarning(
condition: unknown,
errorMessage: string,
{ onlyOnce, showStackTrace }: { onlyOnce: boolean | string; showStackTrace?: true }
{ onlyOnce, showStackTrace }: { onlyOnce: boolean | string; showStackTrace?: boolean }
): void {
if (condition) {
return
Expand Down
4 changes: 1 addition & 3 deletions vite-plugin-ssr/utils/isCallable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export { isCallable }

function isCallable<T extends (...args: unknown[]) => unknown>(thing: T | unknown): thing is T {
export function isCallable<T extends (...args: any[]) => any>(thing: T | unknown): thing is T {
return thing instanceof Function || typeof thing === 'function'
}

0 comments on commit e0785a6

Please sign in to comment.