fix(desktop): render popup overlays in packaged builds

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
This commit is contained in:
Yun Chan 2026-09-19 08:24:03 +09:00
parent 74cbc8f6ae
commit ae7efb6acf
16 changed files with 317 additions and 52 deletions

View file

@ -0,0 +1,160 @@
// 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`)
}