Skip to content
File

Blob: src/worker/sites/manifest.ts

typescript135 lines
1import {
2 SITES_BROWSER_ENTRY,
3 SITES_FONTS_ENTRY,
4 SITES_FONTS_SOURCE,
5 SITES_STYLES_ENTRY,
6 SITES_STYLES_SOURCE,
7} from "@/shared/sites/entrypoints";
8import type { SiteDocumentAssets } from "@/sites/types";
9 
10interface ViteManifestEntry {
11 file: string;
12 css?: string[];
13 assets?: string[];
14 imports?: string[];
15 dynamicImports?: string[];
16}
17 
18type ViteManifest = Record<string, ViteManifestEntry>;
19 
20export interface SitesAssetIndex {
21 // Request-time document asset URLs. These are deploy-specific, but they are
22 // not persisted in Sites artifacts now that R2 stores render JSON instead of
23 // full HTML.
24 documentAssets: SiteDocumentAssets;
25}
26 
27// Source path served by the Vite dev server during `npm run dev` (no build
28// manifest exists), so Vite's CSS pipeline processes @imports and Tailwind in
29// flight.
30const SITES_MANIFEST_PATH = "/sites-manifest.json";
31 
32let cachedIndexPromise: Promise<SitesAssetIndex | null> | null = null;
33 
34function toPathname(path: string | undefined): string | null {
35 if (!path) return null;
36 return path.startsWith("/") ? path : `/${path}`;
37}
38 
39function readStylesheetPath(entry: ViteManifestEntry | undefined): string | undefined {
40 if (!entry) return undefined;
41 return entry.css?.[0] ?? (entry.file.endsWith(".css") ? entry.file : undefined);
42}
43 
44function getChunk(manifest: ViteManifest, key: string | undefined): ViteManifestEntry | null {
45 if (!key) return null;
46 return manifest[key] ?? null;
47}
48 
49function collectScriptGraph(
50 manifest: ViteManifest,
51 key: string,
52): { scriptSrc: string | null; modulePreloadHrefs: string[] } {
53 const visited = new Set<string>();
54 const preloadHrefs: string[] = [];
55 const scriptEntry = getChunk(manifest, key);
56 const scriptSrc = toPathname(scriptEntry?.file);
57 
58 function visitImport(importKey: string, preload: boolean): void {
59 if (visited.has(importKey)) return;
60 visited.add(importKey);
61 
62 const entry = getChunk(manifest, importKey);
63 if (!entry) return;
64 
65 for (const child of entry.imports ?? []) visitImport(child, preload);
66 for (const child of entry.dynamicImports ?? []) visitImport(child, false);
67 
68 const href = toPathname(entry.file);
69 if (preload && href) preloadHrefs.push(href);
70 }
71 
72 for (const importKey of scriptEntry?.imports ?? []) visitImport(importKey, true);
73 for (const importKey of scriptEntry?.dynamicImports ?? []) visitImport(importKey, false);
74 
75 return { scriptSrc, modulePreloadHrefs: preloadHrefs };
76}
77 
78async function loadIndex(env: Pick<Env, "ASSETS">): Promise<SitesAssetIndex | null> {
79 const response = await env.ASSETS.fetch(new Request(`https://assets.local${SITES_MANIFEST_PATH}`));
80 if (!response.ok) return null;
81 
82 const manifest = (await response.json()) as ViteManifest;
83 const stylesEntry = manifest[SITES_STYLES_ENTRY];
84 const fontsEntry = manifest[SITES_FONTS_ENTRY];
85 const cssRel = readStylesheetPath(stylesEntry);
86 const fontCssRel = readStylesheetPath(fontsEntry);
87 if (!cssRel || !fontCssRel) return null;
88 
89 const stylesheetHref = toPathname(cssRel);
90 const fontStylesheetHref = toPathname(fontCssRel);
91 if (!stylesheetHref || !fontStylesheetHref) return null;
92 
93 const { scriptSrc, modulePreloadHrefs } = collectScriptGraph(manifest, SITES_BROWSER_ENTRY);
94 if (!scriptSrc) return null;
95 
96 return {
97 documentAssets: {
98 stylesheetHref,
99 fontStylesheetHref,
100 scriptSrc,
101 modulePreloadHrefs,
102 },
103 };
104}
105 
106export function getSitesAssetIndex(env: Pick<Env, "ASSETS">): Promise<SitesAssetIndex | null> {
107 if (!cachedIndexPromise) {
108 cachedIndexPromise = loadIndex(env).catch((err) => {
109 cachedIndexPromise = null;
110 throw err;
111 });
112 }
113 return cachedIndexPromise;
114}
115 
116export function resetSitesManifestCacheForTests(): void {
117 cachedIndexPromise = null;
118}
119 
120function getDevDocumentAssets(): SiteDocumentAssets {
121 return {
122 stylesheetHref: `/${SITES_STYLES_SOURCE}?direct`,
123 fontStylesheetHref: `/${SITES_FONTS_SOURCE}?direct`,
124 scriptSrc: `/${SITES_BROWSER_ENTRY}`,
125 modulePreloadHrefs: [],
126 };
127}
128 
129export async function resolveSitesDocumentAssets(env: Pick<Env, "ASSETS">): Promise<SiteDocumentAssets | null> {
130 const index = await getSitesAssetIndex(env);
131 if (index) return index.documentAssets;
132 if (import.meta.env.DEV) return getDevDocumentAssets();
133 return null;
134}