feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,205 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = resolve(scriptDir, '..', '..')
|
||||
const tmpDir = join(repoRoot, '.tmp')
|
||||
|
||||
const evidenceFiles = {
|
||||
home: join(tmpDir, 'rea-four-modules-library-stable-20261017.json'),
|
||||
local: join(tmpDir, 'rea-four-modules-library-local-20261017.json')
|
||||
}
|
||||
|
||||
const prefixes = [
|
||||
'knowledge-sidebar',
|
||||
'main-layout-content',
|
||||
'workspace-tree',
|
||||
'wb-home',
|
||||
'doc-feed',
|
||||
'wb-feed',
|
||||
'my-files'
|
||||
]
|
||||
|
||||
const styleKeys = [
|
||||
'display',
|
||||
'position',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-weight',
|
||||
'line-height',
|
||||
'color',
|
||||
'background-color',
|
||||
'border-radius',
|
||||
'border-top-width',
|
||||
'border-top-color',
|
||||
'padding-top',
|
||||
'padding-right',
|
||||
'padding-bottom',
|
||||
'padding-left',
|
||||
'margin-top',
|
||||
'margin-right',
|
||||
'margin-bottom',
|
||||
'margin-left',
|
||||
'gap',
|
||||
'align-items',
|
||||
'justify-content',
|
||||
'box-shadow',
|
||||
'overflow',
|
||||
'max-width',
|
||||
'min-height',
|
||||
'transition-property',
|
||||
'transition-duration',
|
||||
'transition-timing-function',
|
||||
'opacity',
|
||||
'transform'
|
||||
]
|
||||
|
||||
function stripComments(source) {
|
||||
return source.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
}
|
||||
|
||||
function cssRules(source) {
|
||||
const css = stripComments(source)
|
||||
const stack = []
|
||||
const rules = []
|
||||
let boundary = 0
|
||||
let quote = ''
|
||||
let escaped = false
|
||||
|
||||
for (let index = 0; index < css.length; index += 1) {
|
||||
const char = css[index]
|
||||
if (quote) {
|
||||
if (escaped) escaped = false
|
||||
else if (char === '\\') escaped = true
|
||||
else if (char === quote) quote = ''
|
||||
continue
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char
|
||||
continue
|
||||
}
|
||||
if (char === '{') {
|
||||
const prelude = css.slice(boundary, index).trim()
|
||||
stack.push({ prelude, bodyStart: index + 1 })
|
||||
boundary = index + 1
|
||||
continue
|
||||
}
|
||||
if (char === '}') {
|
||||
const entry = stack.pop()
|
||||
if (entry) {
|
||||
const body = css.slice(entry.bodyStart, index).trim()
|
||||
if (body && !entry.prelude.startsWith('@')) {
|
||||
rules.push({
|
||||
context: stack.map((item) => item.prelude).filter((item) => item.startsWith('@')),
|
||||
selector: entry.prelude,
|
||||
body
|
||||
})
|
||||
}
|
||||
}
|
||||
boundary = index + 1
|
||||
continue
|
||||
}
|
||||
if (char === ';' && stack.length === 0) boundary = index + 1
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
function matchesPrefix(selector) {
|
||||
return prefixes.some((prefix) => selector.includes(prefix))
|
||||
}
|
||||
|
||||
function relevantNode(node) {
|
||||
const bounds = node.bounds ?? []
|
||||
if (bounds.length < 4 || bounds[2] <= 0 || bounds[3] <= 0) return false
|
||||
if (!node.attributes || !Object.keys(node.attributes).length) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function layoutNode(node) {
|
||||
const styles = node.styles ?? {}
|
||||
return {
|
||||
nodeIndex: node.node_index,
|
||||
parentIndex: node.parent_index,
|
||||
tag: node.node_name,
|
||||
attributes: node.attributes,
|
||||
bounds: node.bounds,
|
||||
styles: Object.fromEntries(styleKeys.map((key) => [key, styles[key]]))
|
||||
}
|
||||
}
|
||||
|
||||
async function readEvidence(path) {
|
||||
return JSON.parse(await fs.readFile(path, 'utf8'))
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const evidence = {}
|
||||
for (const [name, path] of Object.entries(evidenceFiles)) {
|
||||
evidence[name] = await readEvidence(path)
|
||||
}
|
||||
|
||||
const seenRules = new Set()
|
||||
const recovered = []
|
||||
const styleSources = new Map()
|
||||
|
||||
for (const [name, entry] of Object.entries(evidence)) {
|
||||
for (const sheet of entry.normalized_result.styles.items ?? []) {
|
||||
const text = sheet.source?.artifact?.text
|
||||
if (!text || !prefixes.some((prefix) => text.includes(prefix))) continue
|
||||
const sheetName = basename(sheet.file_path ?? 'inline.css')
|
||||
styleSources.set(sheetName, {
|
||||
path: sheet.file_path,
|
||||
bytes: sheet.source.artifact.bytes,
|
||||
sha256: sheet.source.artifact.sha256
|
||||
})
|
||||
for (const rule of cssRules(text)) {
|
||||
if (!matchesPrefix(rule.selector)) continue
|
||||
const key = `${rule.context.join('>')}|${rule.selector}|${rule.body}`
|
||||
if (seenRules.has(key)) continue
|
||||
seenRules.add(key)
|
||||
recovered.push({
|
||||
evidence: name,
|
||||
sheet: sheetName,
|
||||
...rule
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const css = recovered
|
||||
.map((rule) => {
|
||||
const prefix = rule.context.length ? `${rule.context.join(' { ')} { ` : ''
|
||||
const suffix = rule.context.length ? ` ${'}'.repeat(rule.context.length)}` : ''
|
||||
return `/* ${rule.evidence}:${rule.sheet} */\n${prefix}${rule.selector} { ${rule.body} }${suffix}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const layout = {}
|
||||
for (const [name, entry] of Object.entries(evidence)) {
|
||||
layout[name] = (entry.normalized_result.visuals.layout.documents ?? [])
|
||||
.flatMap((document) => document.nodes ?? [])
|
||||
.filter(relevantNode)
|
||||
.map(layoutNode)
|
||||
}
|
||||
|
||||
await fs.mkdir(tmpDir, { recursive: true })
|
||||
await Promise.all([
|
||||
fs.writeFile(join(tmpDir, 'library-recovered-rules.css'), `${css}\n`, 'utf8'),
|
||||
fs.writeFile(join(tmpDir, 'library-layout-summary.json'), JSON.stringify({
|
||||
styleSources: Object.fromEntries(styleSources),
|
||||
layout
|
||||
}, null, 2), 'utf8')
|
||||
])
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
rules: recovered.length,
|
||||
css: join(tmpDir, 'library-recovered-rules.css'),
|
||||
layout: join(tmpDir, 'library-layout-summary.json'),
|
||||
styleSources
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
Reference in new issue
Block a user