Files
ycode/scripts/rea/recover-action-icons.mjs
T

205 lines
8.4 KiB
JavaScript

// Recover literal SVG JSX from the existing REA export without executing captured code.
import crypto from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import { parse } from '@babel/parser'
const inputPath = '.tmp/rea-runtime-scripts.json'
const outputPath = 'src/renderer/src/workbuddy/message-actions/SourceActionIcons.tsx'
const bundle = JSON.parse(await fs.readFile(inputPath, 'utf8'))
const mainItem = bundle.items.find(item => item.file_path?.endsWith('src-0FGg1seX.js'))
const chevronItem = bundle.items.find(item => item.file_path?.endsWith('chevron-down-icon-Ve_gE8h-.js'))
if (!mainItem?.source?.included || !chevronItem?.source?.included) {
throw new Error('The REA export is missing one or more captured icon sources')
}
function verifiedSource(item) {
const source = item.source.artifact.text
const sha256 = crypto.createHash('sha256').update(source).digest('hex')
if (sha256 !== item.source.artifact.sha256) throw new Error(`REA source digest mismatch: ${item.file_path}`)
return { source, sha256 }
}
function findAssignment(ast, name) {
const matches = []
function visit(node) {
if (!node || typeof node !== 'object') return
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && node.left.name === name) {
matches.push(node)
}
for (const value of Object.values(node)) {
if (Array.isArray(value)) value.forEach(visit)
else if (value && typeof value === 'object') visit(value)
}
}
visit(ast.program)
if (matches.length !== 1) throw new Error(`Expected one assignment for ${name}, found ${matches.length}`)
return matches[0]
}
function keyName(node) {
if (node.type === 'Identifier') return node.name
if (node.type === 'StringLiteral' || node.type === 'NumericLiteral') return String(node.value)
return undefined
}
function staticValue(node) {
if (node.type === 'StringLiteral') return { kind: 'string', value: node.value }
if (node.type === 'NumericLiteral') return { kind: 'number', value: node.value }
if (node.type === 'BooleanLiteral') return { kind: 'boolean', value: node.value }
if (node.type === 'NullLiteral') return { kind: 'null' }
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
return { kind: 'string', value: node.quasis[0].value.cooked ?? node.quasis[0].value.raw }
}
return undefined
}
function escapeAttribute(value) {
return String(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;')
}
function isFactoryCall(node) {
if (node.type !== 'CallExpression' || node.arguments.length < 2) return false
const callee = node.callee
return callee.type === 'SequenceExpression'
|| callee.type === 'MemberExpression'
|| callee.type === 'Identifier'
}
function renderChild(node, depth, bindings) {
if (!node || node.type === 'NullLiteral') return ''
if (node.type === 'ArrayExpression') return node.elements.map(child => renderChild(child, depth, bindings)).filter(Boolean).join('\n')
if (node.type === 'StringLiteral') return `${' '.repeat(depth)}${node.value}`
if (!isFactoryCall(node)) throw new Error(`Unsupported captured SVG child node: ${node.type}`)
const tagNode = node.arguments[0]
const propsNode = node.arguments[1]
const tag = staticValue(tagNode)
if (tag?.kind !== 'string' || propsNode.type !== 'ObjectExpression') {
throw new Error('Captured SVG element is not a literal JSX factory call')
}
const attrs = []
let children
for (const property of propsNode.properties) {
if (property.type === 'SpreadElement') {
const binding = bindings.get(property.argument.name)
if (!binding || binding.kind !== 'spread') throw new Error('Unexpected dynamic SVG props spread')
attrs.push(`{...${binding.value}}`)
continue
}
if (property.type !== 'ObjectProperty') throw new Error('Unsupported SVG props member')
const name = keyName(property.key)
if (!name || name === 'key') continue
if (name === 'children') {
children = property.value
continue
}
const value = staticValue(property.value)
if (value?.kind === 'string') {
attrs.push(`${name}="${escapeAttribute(value.value)}"`)
} else if (value?.kind === 'number') {
attrs.push(`${name}={${value.value}}`)
} else if (value?.kind === 'boolean') {
attrs.push(value.value ? name : `${name}={false}`)
} else if (value?.kind === 'null') {
attrs.push(`${name}={null}`)
} else if (property.value.type === 'Identifier') {
const binding = bindings.get(property.value.name)
if (!binding || binding.kind !== 'expression') throw new Error(`Unexpected dynamic SVG prop: ${name}`)
attrs.push(`${name}={${binding.value}}`)
} else {
throw new Error(`Unsupported captured SVG attribute value: ${name}`)
}
}
const pad = ' '.repeat(depth)
const attrText = attrs.map(attribute => `${' '.repeat(depth + 1)}${attribute}`).join('\n')
const renderedChildren = children ? renderChild(children, depth + 1, bindings) : ''
if (!renderedChildren) return `${pad}<${tag.value}${attrs.length ? `\n${attrText}\n${pad}` : ''} />`
return `${pad}<${tag.value}${attrs.length ? `\n${attrText}\n${pad}` : ''}>\n${renderedChildren}\n${pad}</${tag.value}>`
}
function componentSource(assignment, exportedName, sourceLabel, sha256) {
const arrow = assignment.right
if (arrow.type !== 'ArrowFunctionExpression' || arrow.params.length !== 1) {
throw new Error(`Unexpected icon component form: ${assignment.left.name}`)
}
const param = arrow.params[0]
let signature
let bindings
if (param.type === 'ObjectPattern') {
const propsBindings = new Map()
for (const property of param.properties) {
if (property.type === 'ObjectProperty' && property.value.type === 'Identifier') {
const key = keyName(property.key)
if (key === 'className' || key === 'style') propsBindings.set(property.value.name, { kind: 'expression', value: key })
} else if (property.type === 'RestElement' && property.argument.type === 'Identifier') {
propsBindings.set(property.argument.name, { kind: 'spread', value: 'props' })
}
}
signature = '({ className, style, ...props }: IconProps)'
bindings = propsBindings
} else if (param.type === 'Identifier') {
signature = '(props: IconProps)'
bindings = new Map([[param.name, { kind: 'spread', value: 'props' }]])
} else {
throw new Error(`Unsupported icon component parameter: ${assignment.left.name}`)
}
const body = arrow.body
if (!isFactoryCall(body)) throw new Error(`Unsupported icon component body: ${assignment.left.name}`)
const svg = renderChild(body, 2, bindings)
return `/** REA ${bundle.evidence_id}; ${sourceLabel} SHA-256 ${sha256}; UTF-16 [${assignment.start}, ${assignment.end}). */\nexport function ${exportedName}${signature}: JSX.Element {\n return (\n${svg}\n )\n}\n`
}
const main = verifiedSource(mainItem)
const chevron = verifiedSource(chevronItem)
const mainAst = parse(main.source, { sourceType: 'module', plugins: ['jsx'] })
const chevronAst = parse(chevron.source, { sourceType: 'module', plugins: ['jsx'] })
const mainIcons = [
['ThumbsUpIcon', 'SourceThumbsUpIcon'],
['ThumbsUpSolidIcon', 'SourceThumbsUpSolidIcon'],
['ThumbsDownIcon', 'SourceThumbsDownIcon'],
['ThumbsDownSolidIcon', 'SourceThumbsDownSolidIcon'],
['ResendIcon', 'SourceResendIcon'],
['RevertIcon', 'SourceRevertIcon']
]
const snippets = [
"import type { JSX, SVGProps } from 'react'",
'',
'type IconProps = SVGProps<SVGSVGElement>',
'',
'// No dedicated regenerate icon declaration was found in the captured bundle.',
''
]
for (const [sourceName, exportedName] of mainIcons) {
const assignment = findAssignment(mainAst, sourceName)
snippets.push(componentSource(
assignment,
exportedName,
'src-0FGg1seX.js',
main.sha256
))
}
const chevronAssignment = findAssignment(chevronAst, 'ChevronDownIconImpl')
snippets.push(componentSource(
chevronAssignment,
'SourceFoldChevronIcon',
'chevron-down-icon-Ve_gE8h-.js',
chevron.sha256
))
await fs.mkdir(path.dirname(outputPath), { recursive: true })
await fs.writeFile(outputPath, `${snippets.join('\n')}\n`)
console.log(JSON.stringify({
outputPath,
evidenceId: bundle.evidence_id,
sources: [
{ file: 'src-0FGg1seX.js', sha256: main.sha256, icons: mainIcons.length },
{ file: 'chevron-down-icon-Ve_gE8h-.js', sha256: chevron.sha256, icons: 1 }
],
regenerate: 'no dedicated icon declaration found'
}, null, 2))