import type { Route } from '@anansi/router'; import { Request } from 'express'; import React from 'react'; import { StatsChunkGroup } from 'webpack'; import { buildPolicy, joinNonce, type CSPolicy } from './csp.js'; import Document from './DocumentComponent.js'; import type { ServerSpout } from './types.js'; type NeededNext = { matchedRoutes: Route[]; title?: string; scripts?: React.ReactNode[]; extraStyle?: React.ReactNode[]; }; export { type CSPolicy }; export default function DocumentSpout(options: { head?: React.ReactNode; title: string; lang?: string; rootId?: string; charSet?: string; csPolicy?: CSPolicy; }): ServerSpout, Record, NeededNext> { return next => async props => { const nextProps = await next(props); const publicPath = props.clientManifest.publicPath; if ( Object.keys(props.clientManifest?.entrypoints ?? {}).length < 1 || publicPath === undefined ) throw new Error('Manifest missing entries needed'); // TODO: consider using this package for build stats in future: // https://github.com/facebook/react/tree/main/packages/react-server-dom-webpack const assetMap = (assets: { name: string; size?: number }[]) => assets.map(({ name }) => `${publicPath}${name}`); const assetList: string[] = []; Object.values(props.clientManifest?.entrypoints ?? {}).forEach( entrypoint => { assetList.push(...assetMap(entrypoint.assets ?? [])); }, ); new Set( assetMap( Object.values(props.clientManifest.namedChunkGroups ?? {}) .filter(({ name }) => nextProps.matchedRoutes.some(route => name?.includes(route.name)), ) .flatMap(chunk => [ ...(chunk.assets ?? []), // any chunk preloads ...childrenAssets(chunk), ]), ), ).forEach(asset => assetList.push(asset)); // find additional assets to preload based on matched route const assets: { href: string; as?: string | undefined; rel?: string | undefined; }[] = assetList .filter(asset => !asset.endsWith('.hot-update.js')) .map(asset => asset.endsWith('.css') ? { href: asset, rel: 'stylesheet' } : asset.endsWith('.js') ? { href: asset, as: 'script' } : { href: asset }, ); if (options.csPolicy) { const httpEquiv = process.env.NODE_ENV === 'production' ? 'Content-Security-Policy' : 'Content-Security-Policy-Report-Only'; props.res.setHeader( httpEquiv, buildPolicy(joinNonce(options.csPolicy, props.nonce)), ); } return { ...nextProps, app: ( {nextProps.app} ), }; }; } function childrenAssets(chunk: StatsChunkGroup) { return chunk.children ? Object.values(chunk.children).flatMap(preload => preload.flatMap(c => c.assets ?? []), ) : []; }