#!/usr/bin/env node /** * Browser regression check for UI assets that must remain local. * * This deliberately builds a throwaway Vite app from the production * components. It does not inspect source strings: Chromium must load the * bundled fonts, Monaco worker, and Leaflet marker images, edit and save a * file, and render a map. OSM tiles are intercepted and fulfilled locally; * every other cross-origin request is treated as a failure. */ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { createRequire } from 'node:module'; const scriptDir = dirname(fileURLToPath(import.meta.url)); process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; const repoRoot = resolve(scriptDir, '../..'); const uiRoot = join(repoRoot, 'ui'); const cspMode = process.argv.includes('--csp'); const uiNodeModules = join(uiRoot, 'node_modules'); const importFrom = async (path) => import(pathToFileURL(path).href); const vite = await importFrom(join(uiNodeModules, 'vite/dist/node/index.js')); const reactPluginModule = await importFrom(join(uiNodeModules, '@vitejs/plugin-react/dist/index.js')); const require = createRequire(import.meta.url); const playwrightModule = await importFrom(require.resolve('playwright')); const { chromium } = playwrightModule.chromium ? playwrightModule : playwrightModule.default; const reactPlugin = reactPluginModule.default; const localModule = (name) => join(uiNodeModules, name); const MONACO = join(uiRoot, 'src/components/userfolder/MonacoFileEditor.tsx'); const MAP = join(uiRoot, 'src/components/embed/MapPlacesDetail.tsx'); const CSS = join(uiRoot, 'src/index.css'); const POSTCSS = join(uiRoot, 'postcss.config.js'); class ExternalRequestDetected extends Error { constructor(urls) { super(`external requests blocked: ${urls.join(', ')}`); this.name = 'ExternalRequestDetected'; } } const EXPECTED_CDN_URL = 'https://fonts.googleapis.com/css2?family=Inter:wght@400&display=swap'; function assertNoExternal(urls) { if (urls.length > 0) throw new ExternalRequestDetected(urls); } const TILE_PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64', ); function fixtureSource() { return `
`; } function fixtureAppSource() { return `import React, { useState } from 'react'; import { createRoot } from 'react-dom/client'; import { I18nextProvider } from 'react-i18next'; import i18next from 'i18next'; import { MonacoFileEditor } from ${JSON.stringify(MONACO)}; import { MapPlacesDetail } from ${JSON.stringify(MAP)}; import ${JSON.stringify(CSS)}; import jaUserfolder from ${JSON.stringify(join(uiRoot, "src/i18n/locales/ja/userfolder.json"))}; import enUserfolder from ${JSON.stringify(join(uiRoot, "src/i18n/locales/en/userfolder.json"))}; const i18n = i18next.createInstance(); await i18n.init({ lng: 'ja', fallbackLng: 'en', resources: { ja: { userfolder: jaUserfolder, embed: { searchResults: { map: '地図検索結果: 「{{query}}」' }, viewOn: { osm: 'OpenStreetMap で開く' } } }, en: { userfolder: enUserfolder, embed: { searchResults: { map: 'Map search results: “{{query}}”' }, viewOn: { osm: 'Open in OpenStreetMap' } } }, }, }); const MAP_DATA = { query: '東京駅', places: [ { name: '東京駅', address: '東京都千代田区', lat: 35.681236, lon: 139.767125, type: 'station', details: '中央駅', mapUrl: 'https://www.openstreetmap.org/?mlat=35.681236&mlon=139.767125#map=16/35.681236/139.767125' }, { name: '皇居', address: '東京都千代田区', lat: 35.685175, lon: 139.752799, type: 'park', details: '公園', mapUrl: 'https://www.openstreetmap.org/' }, ], }; function App() { const [saved, setSaved] = useState(''); const [dirty, setDirty] = useState(false); return

Local UI asset probe

日本語フォント
IBM mono 0123
JetBrains mono 0123
{ setSaved(value); await new Promise(resolve => setTimeout(resolve, 150)); }} onDirtyChange={setDirty} />
{String(dirty)} {saved}
; } createRoot(document.getElementById('root')).render(); `; } async function makeFixture() { const root = await mkdtemp(join(tmpdir(), 'maestro-local-ui-assets-')); await mkdir(join(root, 'src'), { recursive: true }); await writeFile(join(root, 'index.html'), fixtureSource()); await writeFile(join(root, 'src/main.tsx'), fixtureAppSource()); return root; } async function buildFixture(root, outDir) { const previousCwd = process.cwd(); process.chdir(uiRoot); try { await vite.build({ configFile: false, root, base: '/ui/', plugins: [reactPlugin()], resolve: { dedupe: ['react', 'react-dom'], alias: [ { find: /^react$/, replacement: localModule('react') }, { find: /^react\/(.*)$/, replacement: `${localModule('react')}/$1` }, { find: /^react-dom$/, replacement: localModule('react-dom') }, { find: /^react-dom\/(.*)$/, replacement: `${localModule('react-dom')}/$1` }, { find: /^react-i18next$/, replacement: localModule('react-i18next') }, { find: /^i18next$/, replacement: localModule('i18next') }, ], }, css: { postcss: POSTCSS }, logLevel: 'silent', build: { outDir, emptyOutDir: true, rollupOptions: { input: join(root, 'index.html') }, }, }); } finally { process.chdir(previousCwd); } } async function closePreview(previewInfo) { const preview = previewInfo?.preview ?? previewInfo; if (!preview) return; if (typeof preview.close === 'function') { await preview.close(); return; } if (preview.httpServer) { await new Promise((resolveClose) => preview.httpServer.close(resolveClose)); } } async function runPreview(root) { const headers = {}; if (cspMode) { const { buildBrowserCsp } = await importFrom(join(repoRoot, 'dist/bridge/egress-csp.js')); headers['Content-Security-Policy'] = buildBrowserCsp({ mode: 'enforce', connectOrigins: [], imageOrigins: ['https://tile.openstreetmap.org'], frameOrigins: [], }); } const preview = await vite.preview({ configFile: false, root, base: '/ui/', preview: { host: '127.0.0.1', port: 0, strictPort: false, headers }, }); const address = preview.httpServer.address(); if (!address || typeof address === 'string') throw new Error('Vite preview did not expose a TCP address'); return { preview, origin: `http://127.0.0.1:${address.port}` }; } async function browserCheck() { const root = await makeFixture(); const outDir = join(root, 'dist'); let preview; let browser; let context; try { browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); context = await browser.newContext({ serviceWorkers: 'block' }); await buildFixture(root, outDir); preview = await runPreview(root); const page = await context.newPage(); const requests = []; const external = []; const osm = []; const localFailures = []; const pageErrors = []; const consoleErrors = []; const workerResponses = []; const local = new URL(preview.origin); context.on('requestfailed', (request) => { const url = new URL(request.url()); if (url.origin === local.origin) localFailures.push(`request failed ${request.url()}: ${request.failure()?.errorText ?? 'unknown'}`); }); context.on('response', (response) => { const url = new URL(response.url()); if (url.origin === local.origin && response.status() >= 400) { localFailures.push(`HTTP ${response.status()} ${response.url()}`); } if (url.origin === local.origin && /(?:worker|\.worker)[^/]*\.js/i.test(url.pathname) && response.ok()) { workerResponses.push(response.url()); } }); page.on('pageerror', (error) => pageErrors.push(String(error))); page.on('console', (message) => { if (message.type() === 'error') consoleErrors.push(message.text()); }); // This static fixture needs no WebSocket connection. Intercept before any // server connection so a future dependency cannot bypass the HTTP guard. await context.routeWebSocket('**/*', socket => { external.push(socket.url()); socket.close(); }); await context.route('**/*', async (route) => { const url = new URL(route.request().url()); requests.push(url.toString()); if (url.origin === local.origin) return route.continue(); if (url.hostname === 'tile.openstreetmap.org') { osm.push(url.toString()); return route.fulfill({ status: 200, contentType: 'image/png', body: TILE_PNG }); } external.push(url.toString()); return route.abort('blockedbyclient'); }); await page.goto(`${preview.origin}/ui/`, { waitUntil: 'domcontentloaded' }); assertNoExternal(external); if (localFailures.length > 0 || pageErrors.length > 0 || consoleErrors.length > 0) { throw new Error(`local UI asset failures: ${JSON.stringify({ localFailures, pageErrors, consoleErrors })}`); } await page.locator('[data-testid="monaco-file-editor"]').waitFor(); await page.locator('.leaflet-container').waitFor(); try { await page.waitForFunction(() => Boolean(document.querySelector('.monaco-editor [role="textbox"]') || document.querySelector('[data-testid="monaco-load-error"]')), undefined, { timeout: 45000 }); } catch (error) { const editorState = await page.locator('[data-testid="monaco-file-editor"]').evaluate(element => ({ html: element.innerHTML, text: element.textContent })); throw new Error(`Monaco did not mount: ${JSON.stringify({ editorState, localFailures, pageErrors, consoleErrors, requests: requests.slice(-30) })}`, { cause: error }); } if (await page.locator('[data-testid="monaco-load-error"]').count() > 0) { throw new Error(`Monaco lazy load failed: ${JSON.stringify({ localFailures, pageErrors, consoleErrors })}`); } await page.waitForFunction(() => document.fonts.status === 'loaded', undefined, { timeout: 45000 }); await page.waitForTimeout(500); const fontFamilies = await page.evaluate(async () => { const names = ['IBM Plex Sans JP', 'IBM Plex Mono', 'JetBrains Mono']; await Promise.all(names.map(name => document.fonts.load(`16px "${name}"`))); return Object.fromEntries(names.map(name => [name, document.fonts.check(`16px "${name}"`)])); }); if (Object.values(fontFamilies).some(value => value !== true)) { throw new Error(`font faces did not load: ${JSON.stringify(fontFamilies)}`); } const fontRequests = requests.filter(url => /(?:plex|jetbrains)/i.test(url) && /\.(?:woff2?|ttf)(?:[?#]|$)/i.test(url)); if (new Set(fontRequests.map(url => /ibm-plex-sans-jp|ibm-plex-mono|jetbrains-mono/i.exec(url)?.[0])).size < 3) { throw new Error(`expected all three local font families, got: ${fontRequests.join(', ')}`); } const editor = page.locator('.monaco-editor [role="textbox"]').first(); await editor.waitFor(); await editor.focus(); await page.keyboard.press('ControlOrMeta+A'); await page.keyboard.type('# edited locally\\n'); await page.getByTestId('monaco-save').click(); // onSave can report persisted content before the child's dirty-state // notification reaches the parent. Wait for both observable outcomes. await page.waitForFunction(() => document.querySelector('[data-testid="save-result"]')?.textContent?.includes('edited locally') && document.querySelector('[data-testid="dirty-state"]')?.textContent === 'false'); await page.waitForFunction(() => document.querySelectorAll('.leaflet-marker-icon').length === 2); const markerCount = await page.locator('.leaflet-marker-icon').count(); if (markerCount !== 2) throw new Error(`expected two rendered map markers, got ${markerCount}`); await page.waitForFunction(() => [...document.querySelectorAll('.leaflet-marker-icon')].every(element => element instanceof HTMLImageElement && element.naturalWidth > 0 && element.naturalHeight > 0), undefined, { timeout: 30000 }); const markerSources = await page.locator('.leaflet-marker-icon').evaluateAll(elements => elements.map(element => element.getAttribute('src') ?? '')); const markerUrls = markerSources.map((src) => src.startsWith('data:') ? src : new URL(src, `${local.origin}/ui/`).href); if (markerUrls.some(url => !url.startsWith(`${local.origin}/ui/`) && !url.startsWith('data:image/'))) { throw new Error(`marker image escaped same-origin asset path: ${markerUrls.join(', ')}`); } const markerSizes = await page.locator('.leaflet-marker-icon').evaluateAll(elements => elements.map(element => ({ width: element instanceof HTMLImageElement ? element.naturalWidth : 0, height: element instanceof HTMLImageElement ? element.naturalHeight : 0, }))); if (markerSizes.some(size => size.width <= 0 || size.height <= 0)) { throw new Error(`marker images did not decode: ${JSON.stringify(markerSizes)}`); } if (osm.length === 0) throw new Error('Leaflet did not request an OSM tile'); if (workerResponses.length === 0) { await context.waitForEvent('response', { predicate: response => { const url = new URL(response.url()); return url.origin === local.origin && /(?:worker|\.worker)[^/]*\.js/i.test(url.pathname) && response.ok(); }, timeout: 10000, }); } assertNoExternal(external); if (localFailures.length > 0 || pageErrors.length > 0 || consoleErrors.length > 0) { throw new Error(`local UI asset failures: ${JSON.stringify({ localFailures, pageErrors, consoleErrors })}`); } const positive = { requests: [...requests], external: [], osm: [...osm], fontRequests, markerSources: markerUrls.map(url => url.startsWith('data:') ? 'data:image' : new URL(url).pathname), }; // Inject only after the real components finish loading: this exercises // the final guard, including requests that start after DOMContentLoaded. await page.evaluate(url => { document.addEventListener('securitypolicyviolation', event => { if ((event.blockedURI === url || event.blockedURI === new URL(url).origin) && event.effectiveDirective === 'style-src-elem' && event.disposition === 'enforce') { document.body.dataset.cspProbeBlocked = 'true'; } }); const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; link.onload = link.onerror = () => { document.body.dataset.cdnProbeFinished = 'true'; }; document.head.append(link); }, EXPECTED_CDN_URL); await page.waitForFunction(() => document.body.dataset.cdnProbeFinished === 'true', undefined, { timeout: 10000 }); let negativeCaught = false; if (cspMode) { await page.waitForFunction(() => document.body.dataset.cspProbeBlocked === 'true', undefined, { timeout: 10000 }); assertNoExternal(external); negativeCaught = true; } else try { assertNoExternal(external); } catch (error) { if (!(error instanceof ExternalRequestDetected && external.length === 1 && external[0] === EXPECTED_CDN_URL)) throw error; negativeCaught = true; } if (!negativeCaught) throw new Error('external CDN negative fixture was not rejected by the browser network guard'); return { ...positive, negativeExternalCdnDetected: negativeCaught }; } finally { await context?.close(); await browser?.close(); await closePreview(preview); await rm(root, { recursive: true, force: true }); } } const positive = await browserCheck(); console.log(JSON.stringify({ ok: true, cspEnforced: cspMode, sameOriginRequests: positive.requests.length - positive.external.length - positive.osm.length, localFontFiles: positive.fontRequests.length, osmTilesMocked: positive.osm.length, markerSources: positive.markerSources, negativeExternalCdnDetected: positive.negativeExternalCdnDetected, }, null, 2));