import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import ts from "typescript"; // Tokens have only type imports. Compile them with the project's TypeScript // dependency so this works even when Node was built without type stripping. const { outputText } = ts.transpileModule( await readFile( new URL("../../components/src/lib/theme/tokens.ts", import.meta.url), "utf8", ), { compilerOptions: { module: ts.ModuleKind.ESNext } }, ); const { borderRadius, colors, loadingIndicator, spacing, surfaces, textAlphas, touchTargets, typeScale, } = await import( `data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}` ); // Expo's shared chunk is eager even when every app screen is lazy. Keep its // generated bundles intact, but let a tiny bootloader start them after paint. const output = process.argv[2] ?? "dist"; const htmlPath = path.join(output, "index.html"); let html = await readFile(htmlPath, "utf8"); const scripts = []; html = html.replace( /]*\bsrc="([^"]+)"[^>]*>\s*<\/script>/g, (_, src) => { scripts.push(src); return ""; }, ); if (!scripts.length || !html.includes('
')) { throw new Error("Expected Expo SPA HTML with scripts and an empty #root"); } const code = await readFile(new URL("./web-bootstrap.js", import.meta.url)); const hash = createHash("sha256").update(code).digest("hex").slice(0, 16); const filename = `bootstrap-${hash}.js`; await writeFile(path.join(output, "_expo/static/js/web", filename), code); const css = ` #web-bootstrap { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: ${spacing[4]}px; padding: ${spacing[6]}px; background: var(--bootstrap-background, ${surfaces.dark[0]}); color: ${textAlphas.dark[1]}; font: ${typeScale.base.fontSize}px/${typeScale.base.lineHeight}px system-ui, sans-serif; text-align: center; } #bootstrap-spinner { width: ${loadingIndicator.size}px; height: ${loadingIndicator.size}px; color: var(--bootstrap-primary, ${colors.primary[500]}); animation: bootstrap-spin ${loadingIndicator.duration}ms linear infinite; } #web-bootstrap [hidden] { display: none; } html,body { background-color: var(--bootstrap-background, ${surfaces.dark[0]}); } #bootstrap-retry { font: inherit; color: inherit; background: ${surfaces.dark[2]}; padding: ${spacing[2]}px ${spacing[4]}px; border: none; border-radius: ${borderRadius.md}px; min-height: ${touchTargets.minimum}px; cursor: pointer; } #bootstrap-retry:focus-visible { outline: ${spacing[1] / 2}px solid currentColor; outline-offset: ${spacing[1] / 2}px; } @keyframes bootstrap-spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { #bootstrap-spinner { animation: none; } } `; html = html.replace("", ``); html = html.replace( '
', `
`, ); // Escape '<' so a filename cannot terminate the JSON script element. const manifest = JSON.stringify(scripts).replace(/", ` `, ); await writeFile(htmlPath, html); console.log( `Web bootstrap: ${code.length} initial JS bytes; ${scripts.length} Expo scripts deferred until after paint`, );