diff --git a/bun.lock b/bun.lock index d93cfcba..acffb243 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ "@types/node": "^24.10.0", "@types/ws": "^8.18.1", "electron": "42.6.0", - "electron-builder": "26.15.3", + "electron-builder": "26.16.1", "esbuild": "^0.25.12", "eslint": "^10.8.0", "prettier": "^3.8.1", @@ -36,9 +36,9 @@ }, "overrides": { "@xmldom/xmldom": "0.8.15", - "app-builder-lib": "26.15.3", + "app-builder-lib": "26.16.1", "brace-expansion": "5.0.9", - "electron-builder-squirrel-windows": "26.15.3", + "electron-builder-squirrel-windows": "26.16.1", "fast-uri": "3.1.6", "form-data": "4.0.6", "ip-address": "10.2.0", @@ -181,7 +181,7 @@ "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], - "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="], @@ -243,7 +243,7 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "app-builder-lib": ["app-builder-lib@26.15.3", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^2.2.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.15.3", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.15.3", "electron-builder-squirrel-windows": "26.15.3" } }, "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow=="], + "app-builder-lib": ["app-builder-lib@26.16.1", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^1.8.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.16.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.16.1", "electron-builder-squirrel-windows": "26.16.1" } }, "sha512-FhaO6YOup01ZfQW0Z6gt3AyukJjv1gW4uFK47jTgwcHZKqyN/fSlK2LqPf9tAeZYLP2bRJLDzeOkRImsw2X4Pg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -273,7 +273,7 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "builder-util": ["builder-util@26.15.3", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw=="], + "builder-util": ["builder-util@26.16.0", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-RLyJhB7Si3YkzKR9ubQslWuXW3Vhs3CGe1i+SeixBZ0qTd1mk3XBmssvY22TlB6CS5blyko8Gu1JzpYk8UkYAg=="], "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], @@ -335,7 +335,7 @@ "discord-api-types": ["discord-api-types@0.38.49", "", {}, "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg=="], - "dmg-builder": ["dmg-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ=="], + "dmg-builder": ["dmg-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-pnI/3Qb24Uk+rMTgIUrsVUKosVgwmBUdF8Zeb8TexOSbpq8MWc7v6l+n+FrEqVkjNZwzBN+XpDS9ENgZ/rkWAw=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], @@ -349,11 +349,11 @@ "electron": ["electron@42.6.0", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-axGNgd+yCTg+vi1VEGrQqAj9WVWkePKwbICSAvMiT2eTaxhij9a/xhBHD6rXV8wrlW9ZfJzE5+xg752ImxrmTw=="], - "electron-builder": ["electron-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.3", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA=="], + "electron-builder": ["electron-builder@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.16.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-LrLK65QX5PUYYODXqp23FKrV7CILTtVY7mrJckNknO9jLNSMiqFkKbSMiDRw4CjOADMPVDdWLxY4mezOZWswxg=="], - "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "electron-winstaller": "5.4.0" } }, "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA=="], + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.16.1", "", { "dependencies": { "app-builder-lib": "26.16.1", "builder-util": "26.16.0", "electron-winstaller": "5.4.0" } }, "sha512-w0y44wSaT1l6R7CAGmeHn4nHPfvzDyCAU1xJyi1w9SbPYJpYn76SmHDzqHf8Y7l91cPWTdPYBpGQtB2T5mJ08A=="], - "electron-publish": ["electron-publish@26.15.3", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q=="], + "electron-publish": ["electron-publish@26.16.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.16.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-Vt3KzQIiw9BImvNOYtndg9Mjki+tl4+1sQiC/+G5j8khWaENOJFWodiB+sUl6yyHwtd37avehskdtPw7f8y/+Q=="], "electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="], diff --git a/package.json b/package.json index 59eacadf..d52d4d47 100644 --- a/package.json +++ b/package.json @@ -85,9 +85,9 @@ }, "overrides": { "@xmldom/xmldom": "0.8.15", - "app-builder-lib": "26.15.3", + "app-builder-lib": "26.16.1", "brace-expansion": "5.0.9", - "electron-builder-squirrel-windows": "26.15.3", + "electron-builder-squirrel-windows": "26.16.1", "fast-uri": "3.1.6", "form-data": "4.0.6", "ip-address": "10.2.0", @@ -129,7 +129,7 @@ "@types/node": "^24.10.0", "@types/ws": "^8.18.1", "electron": "42.6.0", - "electron-builder": "26.15.3", + "electron-builder": "26.16.1", "esbuild": "^0.25.12", "eslint": "^10.8.0", "prettier": "^3.8.1", diff --git a/scripts/package-audit.cjs b/scripts/package-audit.cjs index 9dc15757..2ebee9fb 100644 --- a/scripts/package-audit.cjs +++ b/scripts/package-audit.cjs @@ -51,10 +51,13 @@ function listFiles(root, prefix = '') { }); } +// asar resolves lookups with the platform separator, so stat with the listed +// native path and only normalize the reported name. function listAppFiles(archive) { return asar.listPackage(archive).flatMap((entry) => { - const name = entry.replaceAll('\\', '/').replace(/^\//, ''); - const stat = asar.statFile(archive, name); + const native = entry.replace(/^[\\/]/, ''); + const stat = asar.statFile(archive, native); + const name = native.replaceAll('\\', '/'); return 'size' in stat ? [{ path: name, bytes: stat.size }] : []; }); } diff --git a/scripts/package-audit.test.ts b/scripts/package-audit.test.ts index 0232b580..548c2440 100644 --- a/scripts/package-audit.test.ts +++ b/scripts/package-audit.test.ts @@ -134,10 +134,12 @@ test('archive inventory handles native files without counting them twice on disk mkdirSync(output); writeFileSync(path.join(input, 'main.js'), 'hello'); writeFileSync(path.join(input, 'native.node'), 'native'); + mkdirSync(path.join(input, 'dist', 'ai'), { recursive: true }); + writeFileSync(path.join(input, 'dist', 'ai', 'client.js'), 'nested'); const archive = path.join(output, 'app.asar'); await createPackageFromStreams( archive, - ['main.js', 'native.node'].map((name) => ({ + ['main.js', 'native.node', 'dist/ai/client.js'].map((name) => ({ path: name, type: 'file', unpacked: name.endsWith('.node'), @@ -148,6 +150,7 @@ test('archive inventory handles native files without counting them twice on disk assert.deepEqual(listAppFiles(archive), [ { path: 'main.js', bytes: 5 }, { path: 'native.node', bytes: 6 }, + { path: 'dist/ai/client.js', bytes: 6 }, ]); assert.equal( listFiles(output).reduce((sum: number, entry: { bytes: number }) => sum + entry.bytes, 0), diff --git a/scripts/run-package-smoke.mjs b/scripts/run-package-smoke.mjs index 4ef4d7b2..6a44335e 100644 --- a/scripts/run-package-smoke.mjs +++ b/scripts/run-package-smoke.mjs @@ -14,7 +14,12 @@ delete env.ELECTRON_RUN_AS_NODE; try { const result = spawnSync( require('electron'), - [fileURLToPath(new URL('./smoke-package.cjs', import.meta.url)), path.resolve(resources)], + [ + fileURLToPath(new URL('./smoke-package.cjs', import.meta.url)), + path.resolve(resources), + // CI runners lack a setuid chrome-sandbox; this harness never loads remote content. + ...(process.platform === 'linux' ? ['--no-sandbox'] : []), + ], { env, stdio: 'inherit', timeout: 75_000 }, ); if (result.error) throw result.error; diff --git a/scripts/smoke-package.cjs b/scripts/smoke-package.cjs index e0d39a67..8eb49fb4 100644 --- a/scripts/smoke-package.cjs +++ b/scripts/smoke-package.cjs @@ -1,6 +1,7 @@ // Run with the pinned Electron runtime against a finished app's resources folder. const { app, BrowserWindow, session } = require('electron'); const fs = require('node:fs'); +const http = require('node:http'); const path = require('node:path'); const { createRequire } = require('node:module'); const assert = require('node:assert/strict'); @@ -21,6 +22,39 @@ const timeout = setTimeout(() => { app.exit(1); }, 60_000); +const STATIC_TYPES = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.json': 'application/json', +}; + +// The stats dashboard is served by the stats HTTP server in the app, so load it +// over loopback HTTP from the packaged stats/dist and treat missing static +// assets as failures. API routes are not part of this smoke and may 404. +function serveStatsDist(root, failedRequests) { + const server = http.createServer((req, res) => { + const pathname = new URL(req.url, 'http://127.0.0.1').pathname; + const relative = pathname === '/' ? 'index.html' : pathname.slice(1); + try { + const body = fs.readFileSync(path.join(root, relative)); + res.writeHead(200, { + 'Content-Type': STATIC_TYPES[path.extname(relative)] ?? 'application/octet-stream', + }); + res.end(body); + } catch { + if (!pathname.startsWith('/api/')) failedRequests.push(`${req.url}: missing static asset`); + res.writeHead(404).end(); + } + }); + server.listen(0, '127.0.0.1'); + return server; +} + async function smoke() { await app.whenReady(); const packagedRequire = createRequire(path.join(archive, 'package.json')); @@ -50,10 +84,17 @@ async function smoke() { ); assert(extension.id, 'Yomitan extension failed to load'); const failedRequests = []; - session.defaultSession.webRequest.onErrorOccurred({ urls: ['file://*/*'] }, (details) => { - if (details.error !== 'net::ERR_ABORTED') - failedRequests.push(`${details.url}: ${details.error}`); - }); + session.defaultSession.webRequest.onErrorOccurred( + { urls: ['file://*/*', 'http://127.0.0.1/*'] }, + (details) => { + // Chromium probes the cache before fetching @font-face fonts; an uncached + // font reports ERR_CACHE_MISS and is then fetched normally. + if (!['net::ERR_ABORTED', 'net::ERR_CACHE_MISS'].includes(details.error)) + failedRequests.push(`${details.url}: ${details.error}`); + }, + ); + const statsServer = serveStatsDist(path.join(archive, 'stats', 'dist'), failedRequests); + await once(statsServer, 'listening'); for (const ui of ['renderer', 'settings', 'syncui', 'stats']) { const win = new BrowserWindow({ show: false, @@ -63,10 +104,12 @@ async function smoke() { }, }); try { - await win.loadFile( - path.join(archive, ui === 'stats' ? 'stats/dist/index.html' : `dist/${ui}/index.html`), - ); - if (ui !== 'stats') { + if (ui === 'stats') { + await win.loadURL(`http://127.0.0.1:${statsServer.address().port}/`); + // Let in-flight font requests settle before the window goes away. + await win.webContents.executeJavaScript('document.fonts.ready.then(() => true)'); + } else { + await win.loadFile(path.join(archive, `dist/${ui}/index.html`)); const loaded = await win.webContents.executeJavaScript( `document.fonts.load('400 16px "M PLUS 1"', '日本語').then(fonts => fonts.length > 0 && fonts.every(font => font.status === 'loaded'))`, ); @@ -76,6 +119,7 @@ async function smoke() { win.destroy(); } } + statsServer.close(); assert.deepEqual(failedRequests, [], 'Packaged UI resources failed to load'); console.log( 'Package smoke passed: SQLite, platform FFI, texthooker, Yomitan loading, UI pages, shared Japanese font.',