61 lines
10 KiB
JavaScript
61 lines
10 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { createHash } from 'node:crypto'
|
|
import { parse } from '@babel/parser'
|
|
import traverseModule from '@babel/traverse'
|
|
import postcss from 'postcss'
|
|
const traverse=traverseModule.default??traverseModule, evidence=JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json','utf8')),cache=new Map(),checkOnly=process.argv.includes('--check')
|
|
const HELP_FEEDBACK_EVIDENCE={id:'ev_2309de12a003fca2c863a223cbda5bccad36b3af73e29bc309fdaed94745cf9e',fileSha256:'87511827e6cf8f016c9c073fb654ffcc13520b0770dc2ef997564efc1ffe6ba7',pathLength:534,pathSha256:'d84f185b0d52e3b7eb61c96efc21a34887c213746b12132ddd3595d33e332287'}
|
|
const hash=(value)=>createHash('sha256').update(value).digest('hex')
|
|
function source(base){return evidence.items.find((item)=>item.file_path.endsWith(base))?.source.artifact.text}
|
|
function ast(base){if(!cache.has(base))cache.set(base,parse(source(base),{sourceType:'module'}));return cache.get(base)}
|
|
const context='contexts-BMeWSUHk.js',staticValue=(node)=>!node?undefined:node.type==='TemplateLiteral'&&!node.expressions.length?node.quasis[0].value.cooked:node.type==='StringLiteral'||node.type==='NumericLiteral'?node.value:undefined
|
|
function object(node){return Object.fromEntries(node.properties.filter((prop)=>prop.type==='ObjectProperty').map((prop)=>[prop.key.name??prop.key.value,staticValue(prop.value)]))}
|
|
let tabs,groups
|
|
traverse(ast(context),{AssignmentExpression(p){if(p.node.left.name==='DC')tabs=p.node.right.elements.map(object);if(p.node.left.name==='cXe')groups=p.node.right.elements.map(object)}})
|
|
const labels={};traverse(ast('zh-cn-Dfzn8myY.js'),{ObjectProperty(p){const key=p.node.key.value;if(typeof key==='string'&&/^(settings\.nav\.|settings\.search\.)/.test(key)){const value=staticValue(p.node.value);if(typeof value==='string')labels[key]=value}}})
|
|
const names={settings:'Hee',account:'ute',subscriptionUsage:'Ute',appearance:'qre',personalization:'bne',memory:'Na',extensions:'Une',models:'ModelNavIcon',claw:'ClawSettingsIcon',laboratory:'wre',deviceConnections:'MobileConnectIcon',dataManagement:'Kte',keyboardShortcuts:'Qne',buddyApps:'_ne',securityCenter:'Vte',systemPermissions:'$a',softwareConfig:'ro',helpFeedback:'co',getHelp:'One'}
|
|
function resolve(base,name,depth=0){
|
|
if(depth>8)return null
|
|
let imported,decl
|
|
traverse(ast(base),{ImportDeclaration(p){for(const spec of p.node.specifiers)if(spec.local.name===name&&spec.type==='ImportSpecifier')imported={base:p.node.source.value.replace('./',''),member:spec.imported.name}},FunctionDeclaration(p){if(p.node.id?.name===name)decl=p},AssignmentExpression(p){if(p.node.left.name===name)decl=p},VariableDeclarator(p){if(p.node.id?.name===name&&p.node.init)decl=p}})
|
|
if(imported&&source(imported.base)){let local;traverse(ast(imported.base),{ExportNamedDeclaration(p){for(const spec of p.node.specifiers)if(spec.exported.name===imported.member)local=spec.local.name}});if(local)return resolve(imported.base,local,depth+1)}
|
|
if(!decl)return null
|
|
const right=decl.node.right??decl.node.init
|
|
if(right?.type==='CallExpression'&&typeof staticValue(right.arguments[0])==='string'&&right.arguments[1]?.type==='Identifier') {
|
|
let nodes;traverse(ast(base),{AssignmentExpression(p){if(p.node.left.name===right.arguments[1].name&&p.node.right.type==='ArrayExpression')nodes=p.node.right.elements}})
|
|
if(nodes){let defaults={};traverse(ast('createLucideIcon-1goKJVIH.js'),{AssignmentExpression(p){if(p.node.left.name==='i'&&p.node.right.type==='ObjectExpression')defaults=object(p.node.right)}});return{props:defaults,children:nodes.map((entry)=>({tag:staticValue(entry.elements[0]),props:object(entry.elements[1])}))}}
|
|
}
|
|
if(right?.type==='CallExpression'&&right.arguments[0]?.type==='Identifier')return resolve(base,right.arguments[0].name,depth+1)
|
|
let svg=null,paths=[],next
|
|
decl.traverse({CallExpression(p){const tag=p.node.arguments[0];if(tag?.type==='Identifier'){next=tag.name;return}const kind=staticValue(tag);if(!['svg','path','circle','rect','line','polyline','polygon','ellipse'].includes(kind))return;const props={};for(const prop of p.node.arguments[1]?.properties??[]){if(prop.type!=='ObjectProperty')continue;const value=staticValue(prop.value);if(value!==undefined)props[prop.key.name??prop.key.value]=value}if(kind==='svg')svg=props;else paths.push({tag:kind,props})}})
|
|
if(svg&&paths.length)return{props:svg,children:paths}
|
|
return next?resolve(base,next,depth+1):null
|
|
}
|
|
const icons={};for(const[id,name]of Object.entries(names)){const svg=resolve(context,name);if(svg)icons[id]=svg}
|
|
const fixedEvidenceFile=path.join('.tmp','rea-css-evidence',HELP_FEEDBACK_EVIDENCE.id+'.json'),fixedEvidenceBytes=fs.readFileSync(fixedEvidenceFile),fixedEvidenceSha=hash(fixedEvidenceBytes)
|
|
if(fixedEvidenceSha!==HELP_FEEDBACK_EVIDENCE.fileSha256)throw new Error('Fixed settings navigation Evidence changed: '+fixedEvidenceSha)
|
|
const fixedEvidence=JSON.parse(fixedEvidenceBytes.toString('utf8'))
|
|
if(fixedEvidence.evidence_id!==HELP_FEEDBACK_EVIDENCE.id)throw new Error('Fixed settings navigation Evidence identity mismatch')
|
|
const fixedDocument=fixedEvidence.normalized_result.visuals.layout.documents.find((document)=>document.nodes.some((node)=>String(node.attributes?.class??'').split(/\s+/).includes('settings-navigation__group--about')))
|
|
if(!fixedDocument)throw new Error('settings-navigation__group--about is absent from fixed Evidence')
|
|
const fixedChildren=new Map();for(const node of fixedDocument.nodes){const list=fixedChildren.get(node.parent_index)??[];list.push(node);fixedChildren.set(node.parent_index,list)}
|
|
const descendants=(root)=>{const output=[],queue=[...(fixedChildren.get(root.node_index)??[])];while(queue.length){const node=queue.shift();output.push(node);queue.push(...(fixedChildren.get(node.node_index)??[]))}return output}
|
|
const aboutGroup=fixedDocument.nodes.find((node)=>String(node.attributes?.class??'').split(/\s+/).includes('settings-navigation__group--about')),aboutNodes=descendants(aboutGroup)
|
|
const aboutItem=aboutNodes.find((node)=>String(node.attributes?.class??'').split(/\s+/).includes('settings-navigation__item')),aboutSvg=aboutNodes.find((node)=>node.node_name.toLowerCase()==='svg')
|
|
if(!aboutItem||!aboutSvg)throw new Error('About navigation item or SVG is absent from fixed Evidence')
|
|
const fixedSvgRoot=fixedDocument.svg_nodes.find((node)=>node.node_index===aboutSvg.node_index&&node.node_name==='svg'),fixedPath=fixedDocument.svg_nodes.find((node)=>node.parent_index===aboutSvg.node_index&&node.node_name==='path')
|
|
if(!fixedSvgRoot)throw new Error('About navigation SVG root is absent from fixed Evidence')
|
|
if(!fixedPath||typeof fixedPath.attributes.d!=='string')throw new Error('About navigation SVG path is absent from fixed Evidence')
|
|
const pathLength=Buffer.byteLength(fixedPath.attributes.d,'utf16le')/2,pathSha=hash(fixedPath.attributes.d)
|
|
if(pathLength!==HELP_FEEDBACK_EVIDENCE.pathLength||pathSha!==HELP_FEEDBACK_EVIDENCE.pathSha256)throw new Error('About navigation path fingerprint mismatch: '+pathLength+' '+pathSha)
|
|
icons.helpFeedback={props:{viewBox:fixedSvgRoot.attributes.viewBox,fill:fixedSvgRoot.attributes.fill,xmlns:'http://www.w3.org/2000/svg'},children:[{tag:'path',props:{transform:fixedPath.attributes.transform,fill:fixedPath.attributes.fill,fillRule:fixedPath.attributes['fill-rule'],d:fixedPath.attributes.d}}]}
|
|
const generatedReference='// REA '+evidence.evidence_id+'; SettingsNavigation/DC/cXe and source SVG geometry only.\n// About path: REA '+HELP_FEEDBACK_EVIDENCE.id+'; evidence sha256='+fixedEvidenceSha+'; path sha256='+pathSha+'; length='+pathLength+'.\nexport const sourceSettingsTabs = '+JSON.stringify(tabs,null,2)+'\nexport const sourceSettingsGroups = '+JSON.stringify(groups,null,2)+'\nexport const sourceSettingsIcons: Record<string,{props:Record<string,string|number>;children:{tag:string;props:Record<string,string|number>}[]}> = '+JSON.stringify(icons,null,2)+'\nconst labels:Record<string,string> = '+JSON.stringify(labels,null,2)+'\nexport const sourceSettingsLabel=(key:string):string=>labels[key]??key\n'
|
|
const root=postcss.parse(fs.readFileSync('.tmp/rea-css-recovered/contexts-BkjwYSZT.css','utf8')),css=[]
|
|
root.walkRules((rule)=>{if(!/\.settings-(?:navigation|search)/.test(rule.selector))return;let copy=rule.clone();copy.selectors=copy.selectors.map((s)=>'.source-settings-navigation '+s);let tree=copy;for(let parent=rule.parent;parent?.type==='atrule';parent=parent.parent){const wrap=parent.clone({nodes:[]});wrap.append(tree);tree=wrap}css.push(tree.toString())})
|
|
const variables=new Set();const collect=(text)=>{for(const match of text.matchAll(/var\((--[\w-]+)/g))variables.add(match[1])};collect(css.join(''));const tokenRules=[];postcss.parse(fs.readFileSync('.tmp/rea-css-recovered/tokens-Df-i-zsO.css','utf8')).walkRules((rule)=>tokenRules.push(rule));for(let pass=0;pass<10;pass++)for(const rule of tokenRules)rule.walkDecls((decl)=>{if(variables.has(decl.prop))collect(decl.value)});const light=new Map(),dark=new Map();for(const rule of tokenRules){const target=rule.selector.includes('dark')?dark:rule.selector.includes(':root')?light:null;if(target)rule.walkDecls((decl)=>{if(variables.has(decl.prop))target.set(decl.prop,decl.value)})}const serialize=(map)=>[...map].map(([key,value])=>key+':'+value).join(';');css.unshift('.source-settings-navigation{'+serialize(light)+'}',':root[data-theme=dark] .source-settings-navigation{'+serialize(dark)+'}');
|
|
const generatedCss='/* REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94; media conditions retained. */\n'+css.join('\n')+'\n',outputs=new Map([['src/renderer/src/components/source-settings-navigation-reference.ts',generatedReference],['src/renderer/src/components/source-settings-navigation-recovered.css',generatedCss]]),drift=[]
|
|
for(const[file,content]of outputs){if(checkOnly){if(!fs.existsSync(file)||fs.readFileSync(file,'utf8')!==content)drift.push(file)}else fs.writeFileSync(file,content)}
|
|
if(drift.length){console.error('settings navigation recovery drift: '+drift.join(', '));process.exitCode=1}
|
|
console.log(JSON.stringify({check:checkOnly,idempotent:drift.length===0,tabs:tabs.length,groups:groups.length,icons:Object.keys(icons),unknownIcons:Object.keys(names).filter((key)=>!icons[key]),rules:css.length,helpFeedbackEvidence:HELP_FEEDBACK_EVIDENCE.id,helpFeedbackPathLength:pathLength,helpFeedbackPathSha256:pathSha}))
|