const path = require('node:path')
const fs = require('node:fs')
const {execFileSync} = require('node:child_process')
const projectRoot = path.join(__dirname, '..')
require('dotenv').config({path: path.join(projectRoot, '.env')})
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'))
/* The splash and Go templates share the fonts served by Expo's public folder. */
const interFontPath = '{{ staticCDNHost }}/static/fonts/'
const interRegularPath = `${interFontPath}InterVariable.woff2`
const interItalicPath = `${interFontPath}InterVariable-Italic.woff2`
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 releasePackage = 'xyz.blueskyweb.app'
const release =
process.env.SENTRY_RELEASE ||
`${releasePackage}@${require(path.join(projectRoot, 'package.json')).version}`
/*
* The runtime reports `xyz.blueskyweb.app@` + (EXPO_PUBLIC_RELEASE_VERSION
* || package.json version) (SENTRY_RELEASE in src/env/common.ts), so maps
* uploaded under a bogus release never match incoming events. "null" and
* "undefined" catch `jq -r '.version'` stringifying a missing package.json
* version field, and the empty case catches an unset
* EXPO_PUBLIC_RELEASE_VERSION in the Dockerfile.
*/
const version = release.startsWith(`${releasePackage}@`)
? release.slice(releasePackage.length + 1)
: ''
if (!version || version === 'null' || version === 'undefined') {
throw new Error(
`Refusing to upload source maps under invalid Sentry release ${JSON.stringify(release)}. ` +
`Set SENTRY_RELEASE to "${releasePackage}@" 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)')
}
for (const [source, target] of [
['bskyweb/static/iframe', 'iframe'],
['bskyweb/static/.well-known', '.well-known'],
['witchsky-static-about', 'about'],
['web/_headers', '_headers'],
['web/_routes.json', '_routes.json'],
]) {
const destination = path.join(distDir, target)
fs.mkdirSync(path.dirname(destination), {recursive: true})
fs.cpSync(path.join(projectRoot, source), destination, {recursive: true})
}
for (const directory of [distDir, bskywebStatic]) {
require('./generate-oauth-metadata').generateOAuthMetadata(directory)
}
copyDir(
path.join(projectRoot, 'public', 'static', 'fonts'),
path.join(bskywebStatic, 'fonts'),
)
/* Metro embeds /static in chunk and asset URLs for the Go server. Pages needs
* that same directory layout when dist is served directly. */
for (const directory of ['_expo', 'assets', 'favicon.ico']) {
const destination = path.join(distDir, 'static', directory)
cleanDir(destination)
fs.renameSync(path.join(distDir, directory), destination)
}