177 lines
5.7 KiB
JavaScript
177 lines
5.7 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
import { dirname, resolve } from 'node:path'
|
|
|
|
const routeEvidencePath = resolve('.tmp/rea-todo20-scheduled-create.json')
|
|
const outputPath = resolve('src/renderer/src/automations/automation-source-assets.ts')
|
|
const checkOnly = process.argv.includes('--check')
|
|
|
|
const expected = {
|
|
automation: {
|
|
sha256: '687f7b062b0df0f353dafd1f17b00cb939685da5a12c8d3ce865b920b94d6710',
|
|
bytes: 131_537
|
|
},
|
|
src: {
|
|
sha256: '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0',
|
|
bytes: 1_062_959
|
|
}
|
|
}
|
|
|
|
function fail(message) {
|
|
throw new Error(`automation UI recovery: ${message}`)
|
|
}
|
|
|
|
function sha256(value) {
|
|
return createHash('sha256').update(value).digest('hex')
|
|
}
|
|
|
|
function assertArtifact(label, artifact, sourceText) {
|
|
const expectedArtifact = expected[label]
|
|
if (!artifact || !sourceText) fail(`${label} source artifact is missing`)
|
|
if (artifact.bytes !== expectedArtifact.bytes) {
|
|
fail(`${label} byte count changed: ${artifact.bytes} != ${expectedArtifact.bytes}`)
|
|
}
|
|
if (artifact.sha256 !== expectedArtifact.sha256 || sha256(sourceText) !== expectedArtifact.sha256) {
|
|
fail(`${label} SHA-256 does not match the pinned REA evidence`)
|
|
}
|
|
}
|
|
|
|
function findScript(evidence, suffix) {
|
|
return evidence.normalized_result?.scripts?.items?.find((item) =>
|
|
item.file_path?.endsWith(suffix)
|
|
)
|
|
}
|
|
|
|
function extractDataUri(source, variable) {
|
|
const escaped = variable.replaceAll('$', '\\$')
|
|
const match = new RegExp(`(?<![\\w$])${escaped}\\s*=\\s*\\x60(data:image/svg\\+xml,[^\\x60]+)\\x60`).exec(source)
|
|
if (!match) fail(`could not recover ${variable} SVG data URI`)
|
|
return match[1]
|
|
}
|
|
|
|
function parseJsxPath(attributesSource) {
|
|
const attributes = Object.fromEntries(
|
|
[...attributesSource.matchAll(/([A-Za-z][\w-]*)\s*:\s*`([^`]*)`/g)]
|
|
.map((match) => [match[1], match[2]])
|
|
)
|
|
if (!attributes.d) fail('a recovered path is missing its d attribute')
|
|
return attributes
|
|
}
|
|
|
|
function attributeName(name) {
|
|
return name
|
|
.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
|
|
}
|
|
|
|
function escapeAttribute(value) {
|
|
return value
|
|
.replaceAll('&', '&')
|
|
.replaceAll('"', '"')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
}
|
|
|
|
function pathsToSvg(paths, width, height, viewBox) {
|
|
const markup = paths.map((attributes) => {
|
|
const serialized = Object.entries(attributes)
|
|
.filter(([name]) => name !== 'd')
|
|
.map(([name, value]) => `${attributeName(name)}="${escapeAttribute(value)}"`)
|
|
.join(' ')
|
|
return `<path ${serialized} d="${escapeAttribute(attributes.d)}"/>`
|
|
}).join('')
|
|
return `data:image/svg+xml,${encodeURIComponent(
|
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox}" fill="black">${markup}</svg>`
|
|
)}`
|
|
}
|
|
|
|
function extractPaths(source, startMarker, endMarker, jsxFactory) {
|
|
const start = source.indexOf(startMarker)
|
|
const end = source.indexOf(endMarker, start + startMarker.length)
|
|
if (start < 0 || end < 0) fail(`could not locate ${startMarker}`)
|
|
const block = source.slice(start, end)
|
|
const pattern = new RegExp(`\\(0,${jsxFactory}\\.jsx\\)\\(\\x60path\\x60,\\s*\\{([^}]*)\\}\\)`, 'g')
|
|
const paths = [...block.matchAll(pattern)].map((match) => parseJsxPath(match[1]))
|
|
if (!paths.length) fail(`no SVG paths recovered from ${startMarker}`)
|
|
return paths
|
|
}
|
|
|
|
const evidence = JSON.parse(await readFile(routeEvidencePath, 'utf8'))
|
|
const srcItem = findScript(evidence, 'src-DPDj7_qD.js')
|
|
const automationItem = findScript(evidence, 'automation-qDy4JNlv.js')
|
|
|
|
assertArtifact('src', srcItem?.source?.artifact, srcItem?.source?.artifact?.text)
|
|
assertArtifact(
|
|
'automation',
|
|
automationItem?.source?.artifact,
|
|
automationItem?.source?.artifact?.text
|
|
)
|
|
|
|
const src = srcItem.source.artifact.text
|
|
const automation = automationItem.source.artifact.text
|
|
|
|
const templateIcons = {
|
|
news: extractDataUri(src, 'xb'),
|
|
languages: extractDataUri(src, 'Yy'),
|
|
moon: extractDataUri(src, '_b'),
|
|
'weekly-report': extractDataUri(src, 'Tb'),
|
|
film: extractDataUri(src, 'Ly'),
|
|
calendar: extractDataUri(src, 'Ny'),
|
|
lightbulb: extractDataUri(src, '$y'),
|
|
image: extractDataUri(src, 'Gy'),
|
|
hospital: extractDataUri(src, 'Vy'),
|
|
'messages-square': extractDataUri(src, 'sb'),
|
|
'list-todo': extractDataUri(src, 'rb')
|
|
}
|
|
|
|
const clockPaths = extractPaths(
|
|
src,
|
|
'Gc=(0,Uc.forwardRef)',
|
|
'Gc.displayName',
|
|
'Wc'
|
|
)
|
|
templateIcons['alarm-clock'] = pathsToSvg(clockPaths, 16, 16, '0 0 16 16')
|
|
|
|
const emptyIllustrationPaths = extractPaths(
|
|
automation,
|
|
'function TaskEmptyIllustration',
|
|
'var Ta,Ea',
|
|
'Ta'
|
|
)
|
|
const emptyIllustration = pathsToSvg(
|
|
emptyIllustrationPaths,
|
|
64,
|
|
64,
|
|
'0 0 64 64'
|
|
)
|
|
|
|
const output = `/**
|
|
* Generated by scripts/rea/recover-automation-ui.mjs.
|
|
*
|
|
* Source: fixed WorkBuddy 5.8.3 REA evidence
|
|
* ${evidence.evidence_id}
|
|
*
|
|
* Do not edit by hand. Re-run the recovery script to verify or refresh the assets.
|
|
*/
|
|
export const AUTOMATION_EVIDENCE = {
|
|
id: ${JSON.stringify(evidence.evidence_id)},
|
|
automationSha256: ${JSON.stringify(expected.automation.sha256)},
|
|
srcSha256: ${JSON.stringify(expected.src.sha256)}
|
|
} as const
|
|
|
|
export const AUTOMATION_TEMPLATE_ICON_URLS = ${JSON.stringify(templateIcons, null, 2)} as const
|
|
|
|
export type AutomationTemplateIconKey = keyof typeof AUTOMATION_TEMPLATE_ICON_URLS
|
|
|
|
export const AUTOMATION_EMPTY_ILLUSTRATION_URL = ${JSON.stringify(emptyIllustration)}
|
|
`
|
|
|
|
if (checkOnly) {
|
|
const current = await readFile(outputPath, 'utf8').catch(() => '')
|
|
if (current !== output) fail('generated asset file is stale')
|
|
console.log(`PASS automation UI assets ${expected.automation.sha256}`)
|
|
} else {
|
|
await mkdir(dirname(outputPath), { recursive: true })
|
|
await writeFile(outputPath, output, 'utf8')
|
|
console.log(`Wrote ${outputPath}`)
|
|
}
|