473 lines
22 KiB
JavaScript
473 lines
22 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
import { dirname, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import postcss from 'postcss'
|
|
|
|
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
const exportDir = resolve(rootDir, '.tmp/rea-css-recovered')
|
|
const runtimePath = resolve(rootDir, '.tmp/rea-runtime-scripts.json')
|
|
const outputPath = resolve(rootDir, 'src/renderer/src/workbuddy/conversation/question-source.css')
|
|
|
|
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
|
const runtimeFile = 'src-0FGg1seX.js'
|
|
const runtimeSha256 = 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
|
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
|
const inlineResetFile = 'inline-90c5e4f0726a5658.css'
|
|
const inlineResetSha256 = '90c5e4f0726a56583f83dcffecc1fe87b5ad54ec6d0d593008ac033c8fe931c9'
|
|
const optionRuntimeClassOffset = 1043927
|
|
|
|
const sourceSpecs = [
|
|
{
|
|
file: 'src-CBaV_A3S.css',
|
|
sha256: '2090ed21045c8f8b0fc388d76424c8751bf18fcd065cbbd5d0ef485163a2e36c'
|
|
},
|
|
{
|
|
file: 'cb-bridge-jsj-8pWS.css',
|
|
sha256: '71007d54555800c62d65d6188c743d71481cd9034e0609d3c63bb2626f9d5446'
|
|
}
|
|
]
|
|
|
|
const classAliases = {
|
|
questionFloating: 'question-floating',
|
|
questionFloatingSlideUp: null,
|
|
header: 'question-floating__header',
|
|
headerLeft: 'question-floating__heading',
|
|
title: 'question-floating__title',
|
|
titleHelp: 'question-floating__hint',
|
|
headerRight: 'question-floating__header-actions',
|
|
pagination: 'question-floating__pagination',
|
|
pageBtn: 'question-floating__page-button',
|
|
pageInfo: 'question-floating__pagination-info',
|
|
skipAllBtn: 'question-floating__dismiss',
|
|
content: 'question-floating__content',
|
|
options: 'question-floating__options',
|
|
optionItem: 'question-floating__option',
|
|
optionArrow: 'question-floating__option-arrow',
|
|
optionNumber: 'question-floating__option-number',
|
|
selected: 'question-floating__option--selected',
|
|
checkbox: 'question-floating__checkbox',
|
|
disabled: 'question-floating__option--disabled',
|
|
optionText: 'question-floating__option-copy',
|
|
optionLabel: 'question-floating__option-label',
|
|
optionDesc: 'question-floating__option-description',
|
|
optionHelp: 'question-floating__option-help',
|
|
customInputRow: 'question-floating__custom',
|
|
customInputIcon: 'question-floating__custom-icon',
|
|
focused: 'question-floating__custom--focused',
|
|
customInput: 'question-floating__custom-input',
|
|
footer: 'question-floating__footer',
|
|
footerSingle: 'question-floating__footer--single',
|
|
selectedCount: 'question-floating__selected-count',
|
|
footerActions: 'question-floating__footer-actions',
|
|
skipBtn: 'question-floating__skip',
|
|
iconBtn: 'question-floating__advance',
|
|
iconBtnDisabled: 'question-floating__advance--disabled',
|
|
iconBtnActive: 'question-floating__advance--active',
|
|
iconBtnSend: 'question-floating__advance--send',
|
|
confirmVariant: 'question-floating--confirm'
|
|
}
|
|
|
|
function sha256(bytes) {
|
|
return createHash('sha256').update(bytes).digest('hex')
|
|
}
|
|
|
|
function sourceComment({ file, hash, offset, selectors, evidenceId, note }) {
|
|
return postcss.comment({
|
|
text: ` REA evidence=${evidenceId}; source=${file}; sha256=${hash}; offset=${offset}; original-selectors=${JSON.stringify(selectors)}${note ? `; ${note}` : ''} `
|
|
})
|
|
}
|
|
|
|
function isGlobalButtonOrUniversalSelector(selector) {
|
|
const normalized = selector
|
|
.replace(/:where\(\s*(button|input|select|textarea)\s*\)/gi, '$1')
|
|
.replace(/:is\(\s*(button|input|select|textarea)\s*\)/gi, '$1')
|
|
return normalized === '*' || /^(?:(?:html|body|:root)\s+)*(?:button|input|select|textarea)(?=$|[:[])/i.test(normalized)
|
|
}
|
|
|
|
function getClassMap(source) {
|
|
const footerOffset = source.indexOf('QuestionFloatingFooter=')
|
|
if (footerOffset < 0) throw new Error(`Runtime source is missing QuestionFloatingFooter: ${runtimeFile}`)
|
|
|
|
const candidates = [...source.matchAll(/cH=\{([^{}]+)\}/g)]
|
|
.filter(match => match.index < footerOffset && footerOffset - match.index < 12000)
|
|
const match = candidates.at(-1)
|
|
if (!match) throw new Error(`Runtime source is missing the QuestionFloating CSS module map: ${runtimeFile}`)
|
|
const componentOffset = source.indexOf('xH=(0,yH.forwardRef', match.index)
|
|
if (componentOffset < match.index) {
|
|
throw new Error(`Runtime source is missing the QuestionFloating component after its CSS module map: ${runtimeFile}`)
|
|
}
|
|
|
|
const bindings = Object.fromEntries(
|
|
[...match[1].matchAll(/([A-Za-z_$][\w$]*):([A-Za-z_$][\w$]*)/g)]
|
|
.map(item => [item[1], item[2]])
|
|
)
|
|
const classNames = {}
|
|
for (const [key, variable] of Object.entries(bindings)) {
|
|
const assignment = variable + '=`'
|
|
const position = source.lastIndexOf(assignment, match.index)
|
|
if (position < 0 || /[\w$]/.test(source[position - 1] || '')) {
|
|
throw new Error(`Runtime source is missing the CSS module binding ${variable} for ${key}`)
|
|
}
|
|
const start = position + assignment.length
|
|
const end = source.indexOf('`', start)
|
|
if (end < 0) throw new Error(`Runtime source has an unterminated CSS module binding for ${key}`)
|
|
classNames[key] = source.slice(start, end)
|
|
}
|
|
|
|
const missingAliases = Object.keys(bindings).filter(key => !(key in classAliases))
|
|
const extraAliases = Object.keys(classAliases).filter(key => !(key in bindings))
|
|
if (missingAliases.length || extraAliases.length) {
|
|
throw new Error(`QuestionFloating module map changed: missing aliases [${missingAliases.join(', ')}], stale aliases [${extraAliases.join(', ')}]`)
|
|
}
|
|
|
|
const rootClass = classNames.questionFloating
|
|
const moduleId = /_([A-Za-z0-9]+)_\d+$/.exec(rootClass)?.[1]
|
|
if (!moduleId || !rootClass.startsWith('_questionFloating_')) {
|
|
throw new Error(`Unexpected QuestionFloating module class: ${rootClass}`)
|
|
}
|
|
for (const [key, className] of Object.entries(classNames)) {
|
|
if (!className.includes(`_${moduleId}_`)) {
|
|
throw new Error(`QuestionFloating class ${key} is outside CSS module ${moduleId}: ${className}`)
|
|
}
|
|
}
|
|
|
|
return { componentOffset, footerOffset, moduleMapOffset: match.index, moduleId, classNames }
|
|
}
|
|
|
|
function classPattern(className) {
|
|
return new RegExp(`\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`)
|
|
}
|
|
|
|
function rewriteSelector(selector, classNames) {
|
|
let rewritten = selector
|
|
for (const [key, sourceClass] of Object.entries(classNames)) {
|
|
const alias = classAliases[key]
|
|
if (!alias) continue
|
|
rewritten = rewritten.replace(
|
|
new RegExp(`\\.${sourceClass.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`, 'g'),
|
|
`.${alias}`
|
|
)
|
|
}
|
|
return `.chat-pane.cr-theme ${rewritten}`
|
|
}
|
|
|
|
function projectModuleNode(node, source, classNames, selectedRules) {
|
|
if (node.type === 'rule') {
|
|
const selectors = postcss.list.comma(node.selector).map(selector => selector.trim())
|
|
const selected = selectors.filter(selector => Object.entries(classNames).some(([key, className]) => (
|
|
classAliases[key] && classPattern(className).test(selector)
|
|
)))
|
|
if (selected.length === 0) return []
|
|
|
|
const projected = selected.map(selector => rewriteSelector(selector, classNames))
|
|
selectedRules.push({ file: source.file, originalSelectors: selected, offset: node.source?.start?.offset ?? 0 })
|
|
return [
|
|
sourceComment({
|
|
file: source.file,
|
|
hash: source.sha256,
|
|
offset: node.source?.start?.offset ?? 0,
|
|
selectors: selected,
|
|
evidenceId: cssEvidenceId,
|
|
note: `runtime stylesheet ASAR comparison=${source.asarComparison}`
|
|
}),
|
|
node.clone({ selector: projected.join(',') })
|
|
]
|
|
}
|
|
|
|
if (node.type !== 'atrule' || !node.nodes) return []
|
|
const children = node.nodes.flatMap(child => projectModuleNode(child, source, classNames, selectedRules))
|
|
if (children.length === 0) return []
|
|
const projected = node.clone()
|
|
projected.removeAll()
|
|
projected.append(children)
|
|
return [projected]
|
|
}
|
|
|
|
function projectThemeTokens(root, source, output, records) {
|
|
root.walkRules(rule => {
|
|
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
|
const isDark = selectors.some(selector => /(?:^|[.:])(?:dark|cb-dark)(?:$|[.: ])/.test(selector))
|
|
const isLight = selectors.some(selector => /(?:^|[.:])(?:light|cb-light)(?:$|[.: ])/.test(selector))
|
|
const theme = isDark ? 'dark' : isLight ? 'light' : null
|
|
if (!theme) return
|
|
|
|
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--cb-question-floating-'))
|
|
if (declarations.length === 0) return
|
|
const selector = `.chat-pane.cr-theme[data-cr-theme=${theme}]`
|
|
const offset = rule.source?.start?.offset ?? 0
|
|
records.push({ file: source.file, originalSelectors: selectors, offset, projectedSelector: selector })
|
|
output.append(sourceComment({ file: source.file, hash: source.sha256, offset, selectors, evidenceId: cssEvidenceId, note: `theme token projection=${theme}` }))
|
|
output.append(postcss.rule({ selector, nodes: declarations.map(node => node.clone()) }))
|
|
})
|
|
}
|
|
|
|
function projectBridgeTokens(root, source, output, records) {
|
|
root.walkRules(rule => {
|
|
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
|
const declarations = rule.nodes.filter(node => node.type === 'decl' && node.prop.startsWith('--cb-question-floating-'))
|
|
if (declarations.length === 0) return
|
|
|
|
let projectedSelector
|
|
if (selectors.includes(':root') || selectors.some(selector => selector.startsWith(':root.'))) {
|
|
projectedSelector = '.chat-pane.cr-theme'
|
|
} else if (selectors.some(selector => selector.includes('[class*=questionFloating]'))) {
|
|
projectedSelector = '.chat-pane.cr-theme .question-floating'
|
|
} else if (selectors.some(selector => selector.includes('[class*=skipBtn]'))) {
|
|
projectedSelector = '.chat-pane.cr-theme .question-floating__skip'
|
|
}
|
|
if (!projectedSelector) return
|
|
|
|
const offset = rule.source?.start?.offset ?? 0
|
|
records.push({ file: source.file, originalSelectors: selectors, offset, projectedSelector })
|
|
output.append(sourceComment({ file: source.file, hash: source.sha256, offset, selectors, evidenceId: cssEvidenceId, note: `theme bridge projection=${projectedSelector}` }))
|
|
output.append(postcss.rule({ selector: projectedSelector, nodes: declarations.map(node => node.clone()) }))
|
|
})
|
|
}
|
|
|
|
function formatContainer(container, depth) {
|
|
if (!container.nodes) return
|
|
container.raws.semicolon = true
|
|
for (const child of container.nodes) {
|
|
child.raws.before = `\n${' '.repeat(Math.max(depth + 1, 0))}`
|
|
if (child.type === 'decl') {
|
|
child.raws.between = ': '
|
|
continue
|
|
}
|
|
if (child.nodes) {
|
|
child.raws.between = ' '
|
|
formatContainer(child, depth + 1)
|
|
}
|
|
}
|
|
container.raws.after = `\n${' '.repeat(Math.max(depth, 0))}`
|
|
}
|
|
|
|
const runtimeBytes = await readFile(runtimePath)
|
|
const runtimeExport = JSON.parse(runtimeBytes.toString('utf8'))
|
|
if (runtimeExport.evidence_id !== runtimeEvidenceId) {
|
|
throw new Error(`REA runtime Evidence ID changed: expected ${runtimeEvidenceId}, found ${runtimeExport.evidence_id ?? 'none'}`)
|
|
}
|
|
const runtimeItem = runtimeExport.items.find(item => item.file_path?.replaceAll('\\', '/').endsWith(`/assets/${runtimeFile}`))
|
|
if (!runtimeItem?.source?.artifact?.text) throw new Error(`REA runtime script is missing: ${runtimeFile}`)
|
|
const runtimeSource = runtimeItem.source.artifact.text
|
|
const actualRuntimeSha = sha256(Buffer.from(runtimeSource, 'utf8'))
|
|
if (runtimeItem.source.artifact.sha256 !== runtimeSha256 || actualRuntimeSha !== runtimeSha256) {
|
|
throw new Error(`REA runtime script digest mismatch for ${runtimeFile}: expected ${runtimeSha256}, export ${runtimeItem.source.artifact.sha256 ?? 'none'}, actual ${actualRuntimeSha}`)
|
|
}
|
|
|
|
const { componentOffset, footerOffset, moduleMapOffset, moduleId, classNames } = getClassMap(runtimeSource)
|
|
const cssManifest = JSON.parse(await readFile(resolve(exportDir, 'manifest.json'), 'utf8'))
|
|
if (cssManifest.sourceEvidenceId !== cssEvidenceId) {
|
|
throw new Error(`REA CSS Evidence ID changed: expected ${cssEvidenceId}, found ${cssManifest.sourceEvidenceId ?? 'none'}`)
|
|
}
|
|
|
|
const globalResetRules = []
|
|
const globalButtonTextAlignRules = []
|
|
for (const record of cssManifest.files ?? []) {
|
|
const bytes = await readFile(resolve(exportDir, record.file))
|
|
const actualHash = sha256(bytes)
|
|
if (!record.sha256 || actualHash !== record.sha256) {
|
|
throw new Error(`REA CSS digest mismatch during global reset scan for ${record.file}: manifest ${record.sha256 ?? 'none'}, actual ${actualHash}`)
|
|
}
|
|
const root = postcss.parse(bytes.toString('utf8'), { from: record.file })
|
|
root.walkRules(rule => {
|
|
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
|
const relevantSelectors = selectors.filter(isGlobalButtonOrUniversalSelector)
|
|
if (relevantSelectors.length === 0) return
|
|
const declarations = rule.nodes.filter(node => node.type === 'decl')
|
|
const textAlign = declarations.filter(node => node.prop.toLowerCase() === 'text-align')
|
|
if (relevantSelectors.includes('*')) {
|
|
globalResetRules.push({
|
|
file: record.file,
|
|
sha256: actualHash,
|
|
originalSelectors: selectors,
|
|
offset: rule.source?.start?.offset ?? 0,
|
|
declarations: declarations.map(node => node.clone())
|
|
})
|
|
}
|
|
if (textAlign.length) {
|
|
globalButtonTextAlignRules.push({
|
|
file: record.file,
|
|
sha256: actualHash,
|
|
originalSelectors: selectors,
|
|
relevantSelectors,
|
|
offset: rule.source?.start?.offset ?? 0,
|
|
declarations: textAlign.map(node => ({ property: node.prop, value: node.value }))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const sources = new Map()
|
|
for (const spec of sourceSpecs) {
|
|
const bytes = await readFile(resolve(exportDir, spec.file))
|
|
const manifestRecord = cssManifest.files?.find(record => record.file === spec.file)
|
|
const actualHash = sha256(bytes)
|
|
if (!manifestRecord?.sha256 || manifestRecord.sha256 !== spec.sha256 || actualHash !== spec.sha256) {
|
|
throw new Error(`REA CSS digest mismatch for ${spec.file}: expected ${spec.sha256}, manifest ${manifestRecord?.sha256 ?? 'none'}, actual ${actualHash}`)
|
|
}
|
|
sources.set(spec.file, {
|
|
file: spec.file,
|
|
sha256: actualHash,
|
|
asarComparison: manifestRecord.asarComparison?.status ?? 'not-compared',
|
|
root: postcss.parse(bytes.toString('utf8'), { from: spec.file })
|
|
})
|
|
}
|
|
|
|
const componentCss = sources.get('src-CBaV_A3S.css')
|
|
const bridgeCss = sources.get('cb-bridge-jsj-8pWS.css')
|
|
const optionClassName = classNames.optionItem
|
|
const optionCssRules = []
|
|
componentCss.root.walkRules(rule => {
|
|
const selectors = postcss.list.comma(rule.selector).map(selector => selector.trim())
|
|
if (selectors.includes(`.${optionClassName}`)) {
|
|
optionCssRules.push({
|
|
originalSelectors: selectors,
|
|
offset: rule.source?.start?.offset ?? 0,
|
|
textAlign: rule.nodes.filter(node => node.type === 'decl' && node.prop.toLowerCase() === 'text-align').map(node => node.value)
|
|
})
|
|
}
|
|
})
|
|
if (optionCssRules.length !== 1 || optionCssRules[0].offset !== 650985 || optionCssRules[0].textAlign.length > 0) {
|
|
throw new Error(`QuestionFloating option CSS evidence changed: ${JSON.stringify(optionCssRules)}`)
|
|
}
|
|
|
|
const inlineResetRule = globalResetRules.find(record => record.file === inlineResetFile && record.originalSelectors.includes('*'))
|
|
if (!inlineResetRule || inlineResetRule.sha256 !== inlineResetSha256 || inlineResetRule.offset !== 5) {
|
|
throw new Error(`REA global reset evidence changed: ${JSON.stringify(inlineResetRule ?? null)}`)
|
|
}
|
|
if (!inlineResetRule.declarations.some(node => node.prop === 'box-sizing' && node.value === 'border-box')) {
|
|
throw new Error('REA global universal reset no longer declares box-sizing: border-box')
|
|
}
|
|
|
|
const optionClassOffset = runtimeSource.indexOf('cH.optionItem')
|
|
if (optionClassOffset !== optionRuntimeClassOffset) {
|
|
throw new Error(`QuestionFloating option JSX offset changed: expected ${optionRuntimeClassOffset}, found ${optionClassOffset}`)
|
|
}
|
|
const sourceOptionStart = runtimeSource.lastIndexOf('(0,bH.jsxs)(`div`,{', optionClassOffset)
|
|
const sourceOptionSnippet = runtimeSource.slice(sourceOptionStart, optionClassOffset + 700)
|
|
if (sourceOptionStart < 0 || !sourceOptionSnippet.includes('role:`button`') || !sourceOptionSnippet.includes('tabIndex:O?-1:0') || !sourceOptionSnippet.includes('"aria-disabled":O||void 0') || !sourceOptionSnippet.includes('onKeyDown:')) {
|
|
throw new Error('REA QuestionFloating option no longer uses the verified div/button keyboard structure')
|
|
}
|
|
|
|
const output = postcss.root()
|
|
const optionUniversalResetRules = globalResetRules.filter(record => record.originalSelectors.includes('*'))
|
|
for (const reset of optionUniversalResetRules) {
|
|
output.append(sourceComment({
|
|
file: reset.file,
|
|
hash: reset.sha256,
|
|
offset: reset.offset,
|
|
selectors: reset.originalSelectors,
|
|
evidenceId: cssEvidenceId,
|
|
note: 'global universal reset projection=.chat-pane.cr-theme .question-floating__option'
|
|
}))
|
|
output.append(postcss.rule({
|
|
selector: '.chat-pane.cr-theme .question-floating__option',
|
|
nodes: reset.declarations.map(node => node.clone())
|
|
}))
|
|
}
|
|
const moduleRules = []
|
|
for (const node of componentCss.root.nodes) {
|
|
output.append(...projectModuleNode(node, componentCss, classNames, moduleRules))
|
|
}
|
|
|
|
const sourceModuleClasses = Object.entries(classNames)
|
|
.filter(([key]) => classAliases[key])
|
|
.map(([, className]) => className)
|
|
const missingRules = sourceModuleClasses.filter(className => !moduleRules.some(record => record.originalSelectors.some(selector => classPattern(className).test(selector))))
|
|
if (missingRules.length) throw new Error(`REA CSS is missing QuestionFloating module rules for: ${missingRules.join(', ')}`)
|
|
|
|
const animations = []
|
|
output.walkDecls(/^animation(?:-name)?$/, declaration => {
|
|
animations.push(...declaration.value.split(/[\s,]+/))
|
|
})
|
|
let projectedAnimationRuleCount = 0
|
|
componentCss.root.walkAtRules(/keyframes$/, atRule => {
|
|
if (!animations.includes(atRule.params)) return
|
|
const offset = atRule.source?.start?.offset ?? 0
|
|
output.append(sourceComment({
|
|
file: componentCss.file,
|
|
hash: componentCss.sha256,
|
|
offset,
|
|
selectors: [`@${atRule.name} ${atRule.params}`],
|
|
evidenceId: cssEvidenceId,
|
|
note: `runtime stylesheet ASAR comparison=${componentCss.asarComparison}`
|
|
}))
|
|
output.append(atRule.clone())
|
|
projectedAnimationRuleCount += 1
|
|
})
|
|
|
|
const themeRules = []
|
|
projectThemeTokens(componentCss.root, componentCss, output, themeRules)
|
|
const bridgeRules = []
|
|
projectBridgeTokens(bridgeCss.root, bridgeCss, output, bridgeRules)
|
|
|
|
const header = postcss.comment({
|
|
text: ` Generated by scripts/rea/recover-question-styles.mjs. Runtime component evidence=${runtimeEvidenceId}; source=${runtimeFile}; sha256=${runtimeSha256}; QuestionFloating offset=${componentOffset}; CSS module map offset=${moduleMapOffset}; footer offset=${footerOffset}; module=${moduleId}. CSS evidence=${cssEvidenceId}; ${componentCss.file} sha256=${componentCss.sha256}; ASAR comparison=${componentCss.asarComparison}; ${bridgeCss.file} sha256=${bridgeCss.sha256}; global reset=${inlineResetFile}@${inlineResetRule.offset}; global button text-align matches=${globalButtonTextAlignRules.length}. Selectors are scoped to .chat-pane.cr-theme and translated from the runtime CSS module map. `
|
|
})
|
|
output.prepend(header)
|
|
formatContainer(output, -1)
|
|
|
|
await writeFile(outputPath, output.toString() + '\n')
|
|
const localComponent = await readFile(resolve(rootDir, 'src/renderer/src/workbuddy/conversation/QuestionFloating.tsx'), 'utf8')
|
|
const localOptionClassOffset = localComponent.indexOf("'question-floating__option'")
|
|
const localOptionStart = localComponent.lastIndexOf('<div', localOptionClassOffset)
|
|
const localOptionSnippet = localComponent.slice(localOptionStart, localOptionClassOffset + 900)
|
|
if (localOptionStart < 0 || !localOptionSnippet.includes('role="button"') || !localOptionSnippet.includes('tabIndex=') || !localOptionSnippet.includes('aria-disabled=') || !localOptionSnippet.includes('onKeyDown=')) {
|
|
throw new Error('Local QuestionFloating option must preserve the recovered div/button keyboard structure')
|
|
}
|
|
const localClasses = [...new Set([...localComponent.matchAll(/['"](question-floating(?:__[\w-]+|--[\w-]+)?)['"]/g)].map(match => match[1]))]
|
|
const mappedClasses = new Set(Object.values(classAliases).filter(Boolean))
|
|
const unmappedLocalClasses = localClasses.filter(className => !mappedClasses.has(className))
|
|
const projectedRuleCount = moduleRules.length + themeRules.length + bridgeRules.length + projectedAnimationRuleCount + optionUniversalResetRules.length
|
|
console.log(JSON.stringify({
|
|
runtimeEvidenceId,
|
|
runtimeFile,
|
|
runtimeSha256,
|
|
componentOffset,
|
|
moduleMapOffset,
|
|
footerOffset,
|
|
cssEvidenceId,
|
|
moduleId,
|
|
projectedRuleCount,
|
|
projectedModuleRuleCount: moduleRules.length,
|
|
projectedThemeTokenRuleCount: themeRules.length,
|
|
projectedBridgeTokenRuleCount: bridgeRules.length,
|
|
projectedAnimationRuleCount,
|
|
projectedUniversalResetRuleCount: optionUniversalResetRules.length,
|
|
globalButtonTextAlignRules,
|
|
globalResetSource: {
|
|
evidenceId: cssEvidenceId,
|
|
file: inlineResetFile,
|
|
sha256: inlineResetSha256,
|
|
originalSelectors: inlineResetRule.originalSelectors,
|
|
offset: inlineResetRule.offset,
|
|
declarations: inlineResetRule.declarations.map(node => ({ property: node.prop, value: node.value }))
|
|
},
|
|
optionSource: {
|
|
runtimeEvidenceId,
|
|
file: runtimeFile,
|
|
sha256: runtimeSha256,
|
|
jsxClassOffset: optionClassOffset,
|
|
element: 'div',
|
|
role: 'button',
|
|
cssEvidenceId,
|
|
cssFile: componentCss.file,
|
|
cssSha256: componentCss.sha256,
|
|
cssOriginalSelectors: optionCssRules[0].originalSelectors,
|
|
cssOffset: optionCssRules[0].offset,
|
|
cssTextAlignDeclarations: optionCssRules[0].textAlign,
|
|
localElement: 'div',
|
|
localRole: 'button',
|
|
localClassOffset: localOptionClassOffset
|
|
},
|
|
globalCssFilesScanned: cssManifest.files?.length ?? 0,
|
|
sources: sourceSpecs.map(spec => ({
|
|
file: spec.file,
|
|
sha256: spec.sha256,
|
|
asarComparison: cssManifest.files.find(record => record.file === spec.file)?.asarComparison?.status ?? 'not-compared'
|
|
})),
|
|
unmappedLocalClasses,
|
|
outputPath
|
|
}, null, 2))
|