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:
parent
74cbc8f6ae
commit
ae7efb6acf
16 changed files with 317 additions and 52 deletions
160
scripts/ci/verify-desktop-renderer-bundles.mjs
Normal file
160
scripts/ci/verify-desktop-renderer-bundles.mjs
Normal 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`)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue