fix production build

This commit is contained in:
Samuel Newman
2026-05-25 17:20:16 +03:00
parent fd2147919d
commit 2415343512
7 changed files with 91 additions and 67 deletions
+3
View File
@@ -134,3 +134,6 @@ bskyweb/static/media/*.svg
# superpowers plugin plans/specs — local-only workspace # superpowers plugin plans/specs — local-only workspace
docs/superpowers/ docs/superpowers/
# worktrees
.claude/worktrees/
+3 -8
View File
@@ -6,17 +6,12 @@ test-coverage.out
/embedr /embedr
# Don't accidentally commit JS-generated code # Don't accidentally commit JS-generated code
static/js/*.js static/_expo/
static/js/*.map static/assets/
static/js/*.js.LICENSE.txt
static/js/empty.txt
static/css/*.css
static/css/*.map
static/css/*.css.LICENSE.txt
static/css/empty.txt
static/media/*.png static/media/*.png
static/media/empty.txt static/media/empty.txt
templates/scripts.html templates/scripts.html
templates/fonts.html
templates/*-embed.html templates/*-embed.html
static/embed/*.html static/embed/*.html
static/embed/assets/*.js static/embed/assets/*.js
+2 -2
View File
@@ -260,8 +260,8 @@ func serve(cctx *cli.Context) error {
path := c.Request().URL.Path path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour maxAge := 1 * (60 * 60) // default is 1 hour
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time // all assets in /static/_expo, /static/assets, /static/media are content-hashed and can be cached for a long time
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") { if strings.HasPrefix(path, "/static/_expo/") || strings.HasPrefix(path, "/static/assets/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year maxAge = 365 * (60 * 60 * 24) // 1 year
} }
+4 -1
View File
@@ -2,7 +2,10 @@ package bskyweb
import "embed" import "embed"
//go:embed static/* // `all:` disables the default exclusion of files/dirs beginning with `_` or `.`,
// which is needed because Metro emits chunks like `__common-...js` and
// `__expo-metro-runtime-...js`.
//go:embed all:static
var StaticFS embed.FS var StaticFS embed.FS
//go:embed embedr-static/* //go:embed embedr-static/*
View File
+1 -15
View File
@@ -13,7 +13,7 @@
<!-- Hello Humans! API docs at https://atproto.com --> <!-- Hello Humans! API docs at https://atproto.com -->
<link rel="preload" as="font" type="font/woff2" href="{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin> {% include "fonts.html" %}
<style> <style>
/** /**
@@ -23,20 +23,6 @@
* *
* THIS NEEDS TO BE DUPLICATED IN `bskyweb/templates/base.html` * THIS NEEDS TO BE DUPLICATED IN `bskyweb/templates/base.html`
*/ */
@font-face {
font-family: 'InterVariable';
src: url("{{ staticCDNHost }}/static/media/InterVariable.c504db5c06caaf7cdfba.woff2") format('woff2');
font-weight: 300 1000;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'InterVariableItalic';
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.01dcbad1bac635f9c9cd.woff2") format('woff2');
font-weight: 300 1000;
font-style: italic;
font-display: swap;
}
html, html,
body { body {
margin: 0px; margin: 0px;
+78 -41
View File
@@ -5,64 +5,110 @@ const {execFileSync} = require('node:child_process')
const projectRoot = path.join(__dirname, '..') const projectRoot = path.join(__dirname, '..')
const distDir = path.join(projectRoot, 'dist') const distDir = path.join(projectRoot, 'dist')
const bskywebStatic = path.join(projectRoot, 'bskyweb', 'static') const bskywebStatic = path.join(projectRoot, 'bskyweb', 'static')
const templateFile = path.join( const templatesDir = path.join(projectRoot, 'bskyweb', 'templates')
projectRoot, const templateFile = path.join(templatesDir, 'scripts.html')
'bskyweb', const fontsTemplateFile = path.join(templatesDir, 'fonts.html')
'templates',
'scripts.html',
)
// Parse dist/index.html for entrypoint scripts and stylesheets injected by Metro. // Parse dist/index.html for entrypoint scripts and stylesheets injected by Metro.
// Metro injects <link rel="preload/stylesheet"> and <script> tags referencing /_expo/static/ paths. // Metro injects <link rel="preload/stylesheet"> and <script> tags referencing
// /_expo/static/ paths, optionally prefixed by `experiments.baseUrl` (e.g. /static).
// We capture from /_expo/static/ onward so the captured path is independent of baseUrl.
//
// We preserve the _expo/static/ structure when copying to bskyweb/static, because
// Metro's runtime chunk loader and source maps embed these paths. Rewriting them
// would mean rewriting the chunk loader and every .map file's references; not worth it.
const indexHtml = fs.readFileSync(path.join(distDir, 'index.html'), 'utf8') const indexHtml = fs.readFileSync(path.join(distDir, 'index.html'), 'utf8')
// Find all CSS stylesheet links pointing to _expo // Find all CSS stylesheet links pointing to _expo (skips preload links).
const cssEntries = [] const cssEntries = []
const cssRegex = /<link\b[^>]*href="(\/_expo\/static\/css\/[^"]+)"[^>]*>/g const cssRegex = /<link\b[^>]*href="[^"]*?(\/_expo\/static\/css\/[^"]+)"[^>]*>/g
let match let match
while ((match = cssRegex.exec(indexHtml)) !== null) { while ((match = cssRegex.exec(indexHtml)) !== null) {
if ( if (match[0].includes('rel="stylesheet"')) {
indexHtml
.substring(match.index, match.index + match[0].length)
.includes('stylesheet')
) {
cssEntries.push(match[1]) cssEntries.push(match[1])
} }
} }
// Find all script src entries pointing to _expo // Find all script src entries pointing to _expo
const jsEntries = [] const jsEntries = []
const jsRegex = /<script\b[^>]*src="(\/_expo\/static\/js\/[^"]+)"[^>]*>/g const jsRegex = /<script\b[^>]*src="[^"]*?(\/_expo\/static\/js\/[^"]+)"[^>]*>/g
while ((match = jsRegex.exec(indexHtml)) !== null) { while ((match = jsRegex.exec(indexHtml)) !== null) {
jsEntries.push(match[1]) jsEntries.push(match[1])
} }
if (jsEntries.length === 0) {
// Fail loudly: an empty scripts.html silently breaks the deployed app.
throw new Error(
'No JS entrypoints found in dist/index.html. ' +
'Metro may have changed its output format; update post-web-build.js.',
)
}
console.log(`Found ${jsEntries.length} script entrypoints`) console.log(`Found ${jsEntries.length} script entrypoints`)
console.log(`Found ${cssEntries.length} CSS entrypoints`) console.log(`Found ${cssEntries.length} CSS entrypoints`)
// Generate scripts.html template // Generate scripts.html template.
// Map /_expo/static/... to {{ staticCDNHost }}/static/... // Map /_expo/static/... to {{ staticCDNHost }}/static/_expo/static/... so the
// path matches what we copy into bskyweb/static below.
const outputLines = [] const outputLines = []
for (const href of cssEntries) { for (const href of cssEntries) {
const cdnPath = href.replace( const cdnPath = href.replace(/^\//, '{{ staticCDNHost }}/static/')
/^\/_expo\/static\//,
'{{ staticCDNHost }}/static/',
)
outputLines.push(`<link rel="stylesheet" href="${cdnPath}">`) outputLines.push(`<link rel="stylesheet" href="${cdnPath}">`)
} }
for (const src of jsEntries) { for (const src of jsEntries) {
const cdnPath = src.replace( const cdnPath = src.replace(/^\//, '{{ staticCDNHost }}/static/')
/^\/_expo\/static\//,
'{{ staticCDNHost }}/static/',
)
outputLines.push(`<script defer="defer" src="${cdnPath}"></script>`) outputLines.push(`<script defer="defer" src="${cdnPath}"></script>`)
} }
console.log(`Writing ${templateFile}`) console.log(`Writing ${templateFile}`)
fs.writeFileSync(templateFile, outputLines.join('\n')) 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 interRegularPath = `{{ staticCDNHost }}/static/assets/assets/fonts/inter/${interRegular}`
const interItalicPath = `{{ staticCDNHost }}/static/assets/assets/fonts/inter/${interItalic}`
const fontsHtml = `<link rel="preload" as="font" type="font/woff2" href="${interRegularPath}" crossorigin>
<style>
@font-face {
font-family: 'InterVariable';
src: url("${interRegularPath}") format('woff2');
font-weight: 300 1000;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'InterVariableItalic';
src: url("${interItalicPath}") format('woff2');
font-weight: 300 1000;
font-style: italic;
font-display: swap;
}
</style>
`
console.log(`Writing ${fontsTemplateFile}`)
fs.writeFileSync(fontsTemplateFile, fontsHtml)
// Clean previous build output to avoid stale files // Clean previous build output to avoid stale files
function cleanDir(dir) { function cleanDir(dir) {
if (fs.existsSync(dir)) { if (fs.existsSync(dir)) {
@@ -71,11 +117,10 @@ function cleanDir(dir) {
} }
} }
cleanDir(path.join(bskywebStatic, 'js', 'web')) cleanDir(path.join(bskywebStatic, '_expo'))
cleanDir(path.join(bskywebStatic, 'css'))
cleanDir(path.join(bskywebStatic, 'assets')) cleanDir(path.join(bskywebStatic, 'assets'))
// Recursively copy a directory, skipping source map files // Recursively copy a directory.
function copyDir(sourceDir, targetDir) { function copyDir(sourceDir, targetDir) {
if (!fs.existsSync(sourceDir)) { if (!fs.existsSync(sourceDir)) {
console.log(`Skipping ${sourceDir} (does not exist)`) console.log(`Skipping ${sourceDir} (does not exist)`)
@@ -88,26 +133,18 @@ function copyDir(sourceDir, targetDir) {
const targetPath = path.join(targetDir, entry.name) const targetPath = path.join(targetDir, entry.name)
if (entry.isDirectory()) { if (entry.isDirectory()) {
copyDir(sourcePath, targetPath) copyDir(sourcePath, targetPath)
} else if (!entry.name.endsWith('.map')) { } else {
fs.copyFileSync(sourcePath, targetPath) fs.copyFileSync(sourcePath, targetPath)
console.log(`Copied ${sourcePath} to ${targetPath}`) console.log(`Copied ${sourcePath} to ${targetPath}`)
} }
} }
} }
// Copy JS chunks // Copy Metro's _expo/ tree wholesale so JS chunks (including lazy chunks
copyDir( // referenced by hash) and source maps resolve at the same paths the runtime expects.
path.join(distDir, '_expo', 'static', 'js', 'web'), copyDir(path.join(distDir, '_expo'), path.join(bskywebStatic, '_expo'))
path.join(bskywebStatic, 'js', 'web'),
)
// Copy CSS // Copy assets (fonts, images, icons, etc.) referenced as /static/assets/... by the bundle.
copyDir(
path.join(distDir, '_expo', 'static', 'css'),
path.join(bskywebStatic, 'css'),
)
// Copy assets (fonts, images, icons, etc.)
copyDir(path.join(distDir, 'assets'), path.join(bskywebStatic, 'assets')) copyDir(path.join(distDir, 'assets'), path.join(bskywebStatic, 'assets'))
// Upload source maps to Sentry // Upload source maps to Sentry