feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import net from 'node:net'
|
||||
import process from 'node:process'
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const host = '127.0.0.1'
|
||||
const port = 4230
|
||||
const playground = '/Users/Simon/Github/markstream-vue/playground'
|
||||
|
||||
function isOpen(portNumber) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.createConnection({ host, port: portNumber })
|
||||
socket.on('connect', () => {
|
||||
socket.end()
|
||||
resolve(true)
|
||||
})
|
||||
socket.on('error', () => {
|
||||
socket.destroy()
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForPort(portNumber, timeout = 30000) {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
if (await isOpen(portNumber))
|
||||
return
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
}
|
||||
throw new Error(`timeout waiting for ${portNumber}`)
|
||||
}
|
||||
|
||||
async function captureStaticState(page) {
|
||||
await page.waitForSelector('.stream-monaco-diff-root', { timeout: 120000 })
|
||||
await page.waitForSelector('.stream-monaco-diff-unchanged-bridge', { timeout: 120000 })
|
||||
|
||||
return page.evaluate(() => {
|
||||
function pickStyle(node) {
|
||||
if (!(node instanceof HTMLElement))
|
||||
return null
|
||||
const style = window.getComputedStyle(node)
|
||||
return {
|
||||
background: style.background,
|
||||
backgroundColor: style.backgroundColor,
|
||||
borderColor: style.borderColor,
|
||||
boxShadow: style.boxShadow,
|
||||
color: style.color,
|
||||
}
|
||||
}
|
||||
|
||||
const bridge = document.querySelector('.stream-monaco-diff-unchanged-bridge')
|
||||
const rail = bridge?.querySelector('.stream-monaco-unchanged-rail')
|
||||
const summary = bridge?.querySelector('.stream-monaco-unchanged-summary')
|
||||
const count = bridge?.querySelector('.stream-monaco-unchanged-count, .stream-monaco-unchanged-metadata-label')
|
||||
const reveal = bridge?.querySelector('.stream-monaco-unchanged-reveal')
|
||||
const diffOverview = document.querySelector('.monaco-diff-editor .diffOverview')
|
||||
const decorationsOverviewRuler = document.querySelector('.decorationsOverviewRuler')
|
||||
const firstLineNumber = document.querySelector('.monaco-diff-editor .editor.modified .line-numbers')
|
||||
const firstViewLine = document.querySelector('.monaco-diff-editor .editor.modified .view-line')
|
||||
const visibleRevealDirections = Array.from(
|
||||
bridge?.querySelectorAll('.stream-monaco-unchanged-reveal') ?? [],
|
||||
)
|
||||
.filter(node => node instanceof HTMLElement && !node.hidden)
|
||||
.map(node => node.dataset.direction ?? null)
|
||||
|
||||
return {
|
||||
bridge: pickStyle(bridge),
|
||||
rail: pickStyle(rail),
|
||||
railClasses: rail instanceof HTMLElement ? Array.from(rail.classList) : [],
|
||||
summary: pickStyle(summary),
|
||||
summaryPadding: summary instanceof HTMLElement
|
||||
? {
|
||||
left: window.getComputedStyle(summary).paddingLeft,
|
||||
right: window.getComputedStyle(summary).paddingRight,
|
||||
}
|
||||
: null,
|
||||
count: count ? window.getComputedStyle(count).color : null,
|
||||
reveal: pickStyle(reveal),
|
||||
visibleRevealDirections,
|
||||
overview: pickStyle(diffOverview),
|
||||
decorationsOverviewRuler: pickStyle(decorationsOverviewRuler),
|
||||
overviewMetrics: diffOverview instanceof HTMLElement
|
||||
? {
|
||||
width: Math.round(diffOverview.getBoundingClientRect().width),
|
||||
display: window.getComputedStyle(diffOverview).display,
|
||||
}
|
||||
: null,
|
||||
decorationsOverviewRulerMetrics: decorationsOverviewRuler instanceof HTMLElement
|
||||
? {
|
||||
width: Math.round(decorationsOverviewRuler.getBoundingClientRect().width),
|
||||
display: window.getComputedStyle(decorationsOverviewRuler).display,
|
||||
}
|
||||
: null,
|
||||
modifiedColumn: (
|
||||
firstLineNumber instanceof HTMLElement
|
||||
&& firstViewLine instanceof HTMLElement
|
||||
&& rail instanceof HTMLElement
|
||||
)
|
||||
? {
|
||||
lineNumberLeft: Math.round(firstLineNumber.getBoundingClientRect().left),
|
||||
lineNumberWidth: Math.round(firstLineNumber.getBoundingClientRect().width),
|
||||
codeLeft: Math.round(firstViewLine.getBoundingClientRect().left),
|
||||
railLeft: Math.round(rail.getBoundingClientRect().left),
|
||||
railWidth: Math.round(rail.getBoundingClientRect().width),
|
||||
}
|
||||
: null,
|
||||
text: summary?.textContent?.trim() ?? null,
|
||||
rootVars: bridge instanceof HTMLElement
|
||||
? {
|
||||
unchangedBg: bridge.style.getPropertyValue('--stream-monaco-unchanged-bg'),
|
||||
unchangedFg: bridge.style.getPropertyValue('--stream-monaco-unchanged-fg'),
|
||||
}
|
||||
: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function captureExpandedScrollState(page) {
|
||||
await page.click('.stream-monaco-diff-unchanged-bridge .stream-monaco-unchanged-summary')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
await page.evaluate(() => {
|
||||
const scrollable = document.querySelector(
|
||||
'.monaco-diff-editor .editor.modified .monaco-scrollable-element',
|
||||
)
|
||||
if (scrollable instanceof HTMLElement)
|
||||
scrollable.scrollTop = 220
|
||||
})
|
||||
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
return page.evaluate(() => {
|
||||
const bridges = Array.from(
|
||||
document.querySelectorAll('.stream-monaco-diff-unchanged-bridge'),
|
||||
)
|
||||
.filter(node => node instanceof HTMLElement)
|
||||
.map((node) => {
|
||||
const style = window.getComputedStyle(node)
|
||||
const rect = node.getBoundingClientRect()
|
||||
return {
|
||||
hiddenAttr: node.hasAttribute('hidden'),
|
||||
ariaHidden: node.getAttribute('aria-hidden'),
|
||||
display: style.display,
|
||||
visibility: style.visibility,
|
||||
opacity: style.opacity,
|
||||
height: Math.round(rect.height),
|
||||
width: Math.round(rect.width),
|
||||
text: node.textContent?.trim() ?? '',
|
||||
}
|
||||
})
|
||||
|
||||
const visibleBridges = bridges.filter(bridge =>
|
||||
!bridge.hiddenAttr
|
||||
&& bridge.ariaHidden !== 'true'
|
||||
&& bridge.display !== 'none'
|
||||
&& bridge.visibility !== 'hidden'
|
||||
&& bridge.opacity !== '0'
|
||||
&& bridge.height > 0
|
||||
&& bridge.width > 0,
|
||||
)
|
||||
|
||||
const modifiedScrollable = document.querySelector(
|
||||
'.monaco-diff-editor .editor.modified .monaco-scrollable-element',
|
||||
)
|
||||
|
||||
return {
|
||||
bridgeCount: bridges.length,
|
||||
visibleBridgeCount: visibleBridges.length,
|
||||
visibleBridges,
|
||||
modifiedScrollTop:
|
||||
modifiedScrollable instanceof HTMLElement ? modifiedScrollable.scrollTop : null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function captureRevealStepState(page) {
|
||||
const parseCount = (text) => {
|
||||
const match = text.match(/\d+/)
|
||||
return match ? Number.parseInt(match[0], 10) : Number.NaN
|
||||
}
|
||||
|
||||
const before = await page.evaluate(() => {
|
||||
const bridge = document.querySelector('.stream-monaco-diff-unchanged-bridge:not([hidden])')
|
||||
const summary = bridge?.querySelector('.stream-monaco-unchanged-summary')
|
||||
const button = bridge?.querySelector('.stream-monaco-unchanged-reveal:not([hidden])')
|
||||
return {
|
||||
label: summary?.textContent?.trim() ?? null,
|
||||
direction: button instanceof HTMLElement ? button.dataset.direction ?? null : null,
|
||||
}
|
||||
})
|
||||
|
||||
await page.click('.stream-monaco-diff-unchanged-bridge .stream-monaco-unchanged-reveal:not([hidden])')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
const after = await page.evaluate(() => {
|
||||
const bridge = document.querySelector('.stream-monaco-diff-unchanged-bridge:not([hidden])')
|
||||
const summary = bridge?.querySelector('.stream-monaco-unchanged-summary')
|
||||
return {
|
||||
label: summary?.textContent?.trim() ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
const beforeCount = parseCount(before.label ?? '')
|
||||
const afterCount = parseCount(after.label ?? '')
|
||||
|
||||
return {
|
||||
before,
|
||||
after,
|
||||
beforeCount,
|
||||
afterCount,
|
||||
delta:
|
||||
Number.isFinite(beforeCount) && Number.isFinite(afterCount)
|
||||
? beforeCount - afterCount
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const vite = spawn(
|
||||
'pnpm',
|
||||
['-C', playground, 'dev', '--host', host, '--port', String(port), '--strictPort'],
|
||||
{ stdio: ['ignore', 'pipe', 'pipe'], env: process.env },
|
||||
)
|
||||
|
||||
let logs = ''
|
||||
vite.stdout.on('data', (chunk) => {
|
||||
logs += String(chunk)
|
||||
})
|
||||
vite.stderr.on('data', (chunk) => {
|
||||
logs += String(chunk)
|
||||
})
|
||||
|
||||
try {
|
||||
await waitForPort(port)
|
||||
|
||||
const browser = await chromium.launch({ channel: 'chrome', headless: true })
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
|
||||
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('vmr-test-render-mode', 'monaco')
|
||||
localStorage.setItem('vmr-test-code-stream', 'true')
|
||||
localStorage.setItem('vmr-test-viewport-priority', 'false')
|
||||
localStorage.setItem('vmr-test-batch-rendering', 'false')
|
||||
localStorage.setItem('vmr-test-typewriter', 'false')
|
||||
localStorage.setItem('vmr-test-show-settings', 'true')
|
||||
localStorage.setItem('vmr-test-stream-speed', '4')
|
||||
localStorage.setItem('vmr-test-stream-interval', '24')
|
||||
})
|
||||
|
||||
console.log('goto')
|
||||
page.setDefaultNavigationTimeout(120000)
|
||||
await page.goto(`http://${host}:${port}/diff-line-info-regression`, { waitUntil: 'commit', timeout: 120000 })
|
||||
await page.waitForSelector('h1', { timeout: 120000 })
|
||||
console.log('page-loaded')
|
||||
|
||||
const staticState = await captureStaticState(page)
|
||||
console.log('static-captured')
|
||||
const revealStepState = await captureRevealStepState(page)
|
||||
console.log('reveal-step-captured')
|
||||
const expandedScrollState = await captureExpandedScrollState(page)
|
||||
console.log('expanded-scroll-captured')
|
||||
|
||||
console.log(JSON.stringify({ staticState, revealStepState, expandedScrollState }, null, 2))
|
||||
|
||||
await browser.close()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(String(error))
|
||||
console.error(logs)
|
||||
process.exitCode = 1
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
vite.kill('SIGTERM')
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
}
|
||||
|
||||
await main()
|
||||
Reference in New Issue
Block a user