const path = require('node:path') const fs = require('node:fs') const {execFileSync} = require('node:child_process') const projectRoot = path.join(__dirname, '..') const distDir = path.join(projectRoot, 'dist') const bskywebStatic = path.join(projectRoot, 'bskyweb', 'static') const templatesDir = path.join(projectRoot, 'bskyweb', 'templates') const templateFile = path.join(templatesDir, 'scripts.html') const fontsTemplateFile = path.join(templatesDir, 'fonts.html') // Parse dist/index.html for entrypoint scripts and stylesheets injected by Metro. // Metro injects and `) } console.log(`Writing ${templateFile}`) fs.writeFileSync(templateFile, outputLines.join('\n')) // Generate fonts.html — preload + @font-face for the splash, using the same // content-hashed paths Metro emits for the bundle. Avoids shipping a duplicate // font copy under /static/media/ and ensures the preload is actually used. const fontsDir = path.join(distDir, 'assets', 'assets', 'fonts', 'inter') function findFontHash(prefix) { if (!fs.existsSync(fontsDir)) return null const match = fs .readdirSync(fontsDir) .find(name => name.startsWith(prefix) && name.endsWith('.woff2')) return match || null } const interRegular = findFontHash('InterVariable.') const interItalic = findFontHash('InterVariable-Italic.') if (!interRegular || !interItalic) { throw new Error( `Could not find Inter font files in ${fontsDir}. ` + 'Update post-web-build.js if the font emit path changed.', ) } const interFontPath = '/assets/assets/fonts/inter/' /* * The static index.html references the fonts by their unhashed names, but * Metro emits them content-hashed. Rewrite dist/index.html in place so the * font preload and @font-face URLs resolve when serving dist/ directly. */ const fixedIndexHtml = indexHtml .replaceAll( '/assets/assets/fonts/inter/InterVariable.woff2', `${interFontPath}${interRegular}`, ) .replaceAll( '/assets/assets/fonts/inter/InterVariable-Italic.woff2', `${interFontPath}${interItalic}`, ) if (fixedIndexHtml !== indexHtml) { console.log('Rewriting font paths in dist/index.html to hashed filenames') fs.writeFileSync(path.join(distDir, 'index.html'), fixedIndexHtml) } const interRegularPath = `{{ staticCDNHost }}/static${interFontPath}${interRegular}` const interItalicPath = `{{ staticCDNHost }}/static${interFontPath}${interItalic}` const fontsHtml = ` ` console.log(`Writing ${fontsTemplateFile}`) fs.writeFileSync(fontsTemplateFile, fontsHtml) // Clean previous build output to avoid stale files function cleanDir(dir) { if (fs.existsSync(dir)) { fs.rmSync(dir, {recursive: true}) console.log(`Cleaned ${dir}`) } } cleanDir(path.join(bskywebStatic, '_expo')) cleanDir(path.join(bskywebStatic, 'assets')) // Recursively copy a directory. function copyDir(sourceDir, targetDir) { if (!fs.existsSync(sourceDir)) { console.log(`Skipping ${sourceDir} (does not exist)`) return } fs.mkdirSync(targetDir, {recursive: true}) const entries = fs.readdirSync(sourceDir, {withFileTypes: true}) for (const entry of entries) { const sourcePath = path.join(sourceDir, entry.name) const targetPath = path.join(targetDir, entry.name) if (entry.isDirectory()) { copyDir(sourcePath, targetPath) } else { fs.copyFileSync(sourcePath, targetPath) console.log(`Copied ${sourcePath} to ${targetPath}`) } } } // Copy Metro's _expo/ tree wholesale so JS chunks (including lazy chunks // referenced by hash) and source maps resolve at the same paths the runtime expects. copyDir(path.join(distDir, '_expo'), path.join(bskywebStatic, '_expo')) // Copy assets (fonts, images, icons, etc.) referenced as /static/assets/... by the bundle. copyDir(path.join(distDir, 'assets'), path.join(bskywebStatic, 'assets')) // Upload source maps to Sentry if ( process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_AUTH_TOKEN != 'unknown' ) { const release = process.env.SENTRY_RELEASE || require(path.join(projectRoot, 'package.json')).version /* * The runtime reports EXPO_PUBLIC_RELEASE_VERSION || package.json version * (src/env/common.ts), so maps uploaded under a bogus release never match * incoming events. "null"/"undefined" catch `jq -r '.version'` stringifying * a missing package.json version field. */ if (!release || release === 'null' || release === 'undefined') { throw new Error( `Refusing to upload source maps under invalid Sentry release ${JSON.stringify(release)}. ` + 'Set SENTRY_RELEASE or restore the "version" field in package.json.', ) } const dist = process.env.SENTRY_DIST || undefined const sourceMapDir = path.join(distDir, '_expo', 'static') console.log(`Uploading source maps to Sentry (release: ${release})`) const args = [ 'sourcemaps', 'upload', '--org', 'blueskyweb', '--project', 'app', '--release', release, ...(dist ? ['--dist', dist] : []), sourceMapDir, ] try { execFileSync('sentry-cli', args, {stdio: 'inherit', cwd: projectRoot}) console.log('Sentry source map upload complete') } catch (e) { console.error('Sentry source map upload failed:', e.message) process.exit(1) } } else { console.log('Skipping Sentry source map upload (SENTRY_AUTH_TOKEN not set)') }