Popup pages loaded their scripts as classic <script src> tags, which the
renderer build never bundles, so an installed app rendered only the static
markup: the recording tip stayed at 0:00 with no wave bars and live captions
showed nothing.
- declare popup scripts as modules so the build emits them, and fail
packaging when a renderer page references an asset that was never produced
- hold popup IPC until the renderer has loaded and re-assert visibility on
every show, so a popup hidden once still appears next time
- surface popup renderer console and load failures in the main log
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
160 lines
No EOL
5.9 KiB
JavaScript
160 lines
No EOL
5.9 KiB
JavaScript
// scripts/ci/verify-desktop-renderer-bundles.mjs
|
|
//
|
|
// Fails packaging when a renderer page ships without the assets it references.
|
|
//
|
|
// Why this exists: Vite only bundles <script type="module"> tags. A page that
|
|
// keeps a classic <script src="./script.js"> points at a file the build never
|
|
// emits, so the packaged window renders its static markup forever. That is how
|
|
// the recording overlay froze at 0:00 without wave bars and live captions never
|
|
// showed up. Comparing built HTML against disk catches the whole class of bug.
|
|
//
|
|
// Usage:
|
|
// node scripts/ci/verify-desktop-renderer-bundles.mjs
|
|
// node scripts/ci/verify-desktop-renderer-bundles.mjs --self-test
|
|
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
const repoRoot = path.resolve(scriptDir, '..', '..')
|
|
const desktopDir = path.join(repoRoot, 'apps', 'desktop')
|
|
const sourcePopupDir = path.join(desktopDir, 'src', 'renderer', 'popups')
|
|
const builtRendererDir = path.join(desktopDir, 'out', 'renderer')
|
|
|
|
/**
|
|
* Pages the packaged renderer must contain, relative to the renderer root.
|
|
* Popup directories are discovered from source so a new popup cannot be added
|
|
* without also being built.
|
|
*/
|
|
function expectedPages() {
|
|
const pages = ['index.html']
|
|
if (!existsSync(sourcePopupDir)) return pages
|
|
|
|
for (const entry of readdirSync(sourcePopupDir).sort()) {
|
|
const entryPath = path.join(sourcePopupDir, entry)
|
|
if (statSync(entryPath).isDirectory() && existsSync(path.join(entryPath, 'index.html'))) {
|
|
pages.push(path.posix.join('popups', entry, 'index.html'))
|
|
}
|
|
}
|
|
return pages
|
|
}
|
|
|
|
/** Local script/link references inside a built HTML page. */
|
|
function localReferences(html) {
|
|
const refs = []
|
|
const pattern = /<(?:script|link)\b[^>]*?\b(?:src|href)="([^"]+)"/g
|
|
let match
|
|
while ((match = pattern.exec(html)) !== null) {
|
|
const ref = match[1]
|
|
if (/^[a-z]+:/i.test(ref) || ref.startsWith('//') || ref.startsWith('#')) continue
|
|
refs.push(ref.split('?')[0])
|
|
}
|
|
return refs
|
|
}
|
|
|
|
/**
|
|
* @param {string} rendererDir built renderer root
|
|
* @param {string[]} pages page paths relative to that root
|
|
* @returns {string[]} problems, empty when the build is complete
|
|
*/
|
|
function collectProblems(rendererDir, pages) {
|
|
const problems = []
|
|
|
|
for (const page of pages) {
|
|
const pagePath = path.join(rendererDir, page)
|
|
if (!existsSync(pagePath)) {
|
|
problems.push(`missing built page: ${pagePath}`)
|
|
continue
|
|
}
|
|
|
|
const html = readFileSync(pagePath, 'utf8')
|
|
|
|
for (const ref of localReferences(html)) {
|
|
const assetPath = ref.startsWith('/')
|
|
? path.join(rendererDir, ref.slice(1))
|
|
: path.resolve(path.dirname(pagePath), ref)
|
|
if (!existsSync(assetPath)) {
|
|
problems.push(`${page} references a missing asset: ${ref} -> ${assetPath}`)
|
|
}
|
|
}
|
|
|
|
// A classic script tag is never emitted by the renderer build.
|
|
if (/<script\b(?![^>]*\btype="module")[^>]*\bsrc=/.test(html)) {
|
|
problems.push(`${page} loads a classic script; add type="module" so Vite bundles it`)
|
|
}
|
|
}
|
|
|
|
return problems
|
|
}
|
|
|
|
function selfTest() {
|
|
const tmpRoot = mkdtempSync(path.join(os.tmpdir(), 'd3ro-renderer-bundles-'))
|
|
const failures = []
|
|
|
|
try {
|
|
const bundlePage = (root, scriptTag) => {
|
|
mkdirSync(path.join(root, 'assets'), { recursive: true })
|
|
mkdirSync(path.join(root, 'popups', 'recording-tip'), { recursive: true })
|
|
writeFileSync(path.join(root, 'assets', 'app.js'), '')
|
|
writeFileSync(
|
|
path.join(root, 'popups', 'recording-tip', 'index.html'),
|
|
`<!DOCTYPE html>\n<html><body>${scriptTag}</body></html>\n`,
|
|
)
|
|
}
|
|
|
|
const pages = ['popups/recording-tip/index.html']
|
|
|
|
const goodRoot = path.join(tmpRoot, 'good')
|
|
bundlePage(goodRoot, '<script type="module" src="../../assets/app.js"></script>')
|
|
const goodProblems = collectProblems(goodRoot, pages)
|
|
if (goodProblems.length !== 0) {
|
|
failures.push(`complete build reported problems: ${goodProblems.join('; ')}`)
|
|
}
|
|
|
|
const brokenRoot = path.join(tmpRoot, 'broken')
|
|
bundlePage(brokenRoot, '<script src="./script.js"></script>')
|
|
const brokenProblems = collectProblems(brokenRoot, pages)
|
|
if (!brokenProblems.some((problem) => problem.includes('missing asset'))) {
|
|
failures.push('missing asset was not detected')
|
|
}
|
|
if (!brokenProblems.some((problem) => problem.includes('classic script'))) {
|
|
failures.push('classic script tag was not detected')
|
|
}
|
|
|
|
const missingPageProblems = collectProblems(goodRoot, ['popups/absent/index.html'])
|
|
if (!missingPageProblems.some((problem) => problem.startsWith('missing built page'))) {
|
|
failures.push('missing page was not detected')
|
|
}
|
|
} finally {
|
|
rmSync(tmpRoot, { recursive: true, force: true })
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error('verify-desktop-renderer-bundles self-test failed:')
|
|
for (const failure of failures) console.error(`- ${failure}`)
|
|
process.exit(1)
|
|
}
|
|
console.log('verify-desktop-renderer-bundles self-test: OK')
|
|
}
|
|
|
|
if (process.argv.slice(2).includes('--self-test')) {
|
|
selfTest()
|
|
} else if (!existsSync(builtRendererDir)) {
|
|
console.error(`Renderer build not found: ${builtRendererDir}`)
|
|
console.error(' build: npm run build --workspace=@d3ro/desktop')
|
|
process.exit(1)
|
|
} else {
|
|
const pages = expectedPages()
|
|
const problems = collectProblems(builtRendererDir, pages)
|
|
|
|
if (problems.length > 0) {
|
|
console.error('Desktop renderer bundle verification failed:')
|
|
for (const problem of problems) console.error(`- ${problem}`)
|
|
console.error(' rebuild: npm run build --workspace=@d3ro/desktop')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(`Desktop renderer bundle verification passed: ${pages.length} page(s) with all assets on disk`)
|
|
} |