424 lines
18 KiB
JavaScript
424 lines
18 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { parse } from '@babel/parser'
|
|
import traverseModule from '@babel/traverse'
|
|
import postcss from 'postcss'
|
|
|
|
const EXPECTED_ROOT_SHA256 = '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918'
|
|
const MAIN_EVIDENCE_PATH = '.tmp/rea-selected-editor-evidence.json'
|
|
const DEPENDENCY_EVIDENCE_PATH = '.tmp/rea-selected-pierre-dependencies.json'
|
|
const LEAF_EVIDENCE_PATH = '.tmp/rea-selected-pierre-leaf-dependencies.json'
|
|
const LANGUAGE_EVIDENCE_PATH = '.tmp/rea-selected-pierre-ui-languages.json'
|
|
const OUTPUT_DIRECTORY = path.resolve('src/renderer/src/workbuddy/artifacts/recovered-pierre')
|
|
const VITE_PRELOAD_HELPER = '__vite__mapDeps'
|
|
const RECOVERED_PRELOAD_HELPER = '__ycodePierreMapDeps'
|
|
const traverse = traverseModule.default
|
|
|
|
const SOURCE_FILES = {
|
|
'renderer/assets/codeEditor-D3P9alks.js': 'main',
|
|
'renderer/assets/codeEditor-Bxmo_AI3.css': 'css',
|
|
'package.json': 'package',
|
|
'renderer/assets/rolldown-runtime-BJCASTsb.js': 'asset',
|
|
'renderer/assets/preload-helper-D30X3e06.js': 'asset',
|
|
'renderer/assets/dist-BAqWIeie.js': 'asset',
|
|
'renderer/assets/dist-BUGqZtx5.js': 'asset',
|
|
'renderer/assets/worker-portable-D1lwTqsk.js': 'asset',
|
|
'renderer/assets/zwitch-B8a51bts.js': 'asset',
|
|
'renderer/assets/stringify-entities-i2atvqYU.js': 'asset',
|
|
'renderer/assets/hast-util-whitespace-DbScqzu3.js': 'asset',
|
|
'renderer/assets/pierre-dark-Daa2n9wr.js': 'asset',
|
|
'renderer/assets/pierre-light-D_hZXvUj.js': 'asset',
|
|
'renderer/assets/javascript-AfZCJ9Uc.js': 'asset',
|
|
'renderer/assets/typescript-4sRsPriL.js': 'asset',
|
|
'renderer/assets/diff-lAxbRYdS.js': 'asset',
|
|
'renderer/assets/css-NFwnoVns.js': 'asset',
|
|
'renderer/assets/html-Dq1ixqYb.js': 'asset',
|
|
'renderer/assets/json-CKKhFIOr.js': 'asset',
|
|
'renderer/assets/markdown-BSrOIEaF.js': 'asset',
|
|
}
|
|
|
|
const COPYING_ASSETS = Object.keys(SOURCE_FILES).filter((sourcePath) => SOURCE_FILES[sourcePath] === 'asset')
|
|
const REACT_IMPORTS = new Map([
|
|
['./react-z__Ncn__.js', 's'],
|
|
['./react-dom-mnR6psRT.js', 'c'],
|
|
['./jsx-runtime-DM1VED__.js', 'f'],
|
|
])
|
|
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
|
|
const fail = (message) => {
|
|
throw new Error(message)
|
|
}
|
|
|
|
async function loadEvidence(filePath) {
|
|
const evidence = JSON.parse(await fs.readFile(filePath, 'utf8'))
|
|
if (evidence.authority !== 'shipped-artifact') fail(`Evidence is not shipped-artifact authority: ${filePath}`)
|
|
if (evidence.normalized_result?.root_sha256 !== EXPECTED_ROOT_SHA256) {
|
|
fail(`Evidence root SHA-256 does not match the selected WorkBuddy archive: ${filePath}`)
|
|
}
|
|
const files = new Map()
|
|
for (const file of evidence.normalized_result.files) {
|
|
const actualBytes = Buffer.byteLength(file.text, 'utf8')
|
|
const actualSha256 = sha256(file.text)
|
|
if (actualBytes !== file.size_bytes || actualSha256 !== file.sha256) {
|
|
fail(`Evidence text failed size/SHA validation: ${file.path}`)
|
|
}
|
|
files.set(file.path, file)
|
|
}
|
|
return { evidence, files }
|
|
}
|
|
|
|
function collectPatternNames(node, names = []) {
|
|
if (!node) return names
|
|
if (node.type === 'Identifier') names.push(node.name)
|
|
else if (node.type === 'RestElement') collectPatternNames(node.argument, names)
|
|
else if (node.type === 'AssignmentPattern') collectPatternNames(node.left, names)
|
|
else if (node.type === 'ArrayPattern') node.elements.forEach((entry) => collectPatternNames(entry, names))
|
|
else if (node.type === 'ObjectPattern') {
|
|
for (const property of node.properties) {
|
|
if (property.type === 'RestElement') collectPatternNames(property.argument, names)
|
|
else collectPatternNames(property.value, names)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
function collectDynamicImportPaths(source) {
|
|
const ast = parse(source, { sourceType: 'module' })
|
|
const paths = new Set()
|
|
traverse(ast, {
|
|
ImportExpression(pathRef) {
|
|
const sourceNode = pathRef.node.source
|
|
if (sourceNode.type === 'StringLiteral') paths.add(sourceNode.value)
|
|
else if (sourceNode.type === 'TemplateLiteral' && sourceNode.expressions.length === 0) {
|
|
paths.add(sourceNode.quasis[0].value.cooked)
|
|
}
|
|
},
|
|
})
|
|
return paths
|
|
}
|
|
|
|
function renameTopLevelBinding(source, fromName, toName, sourcePath) {
|
|
if (!source.includes(fromName)) return { source, renameCount: 0 }
|
|
if (source.includes(toName)) fail(`Reserved recovery identifier already exists in ${sourcePath}`)
|
|
|
|
const ast = parse(source, { sourceType: 'module' })
|
|
let foundBinding = false
|
|
traverse(ast, {
|
|
Program(pathRef) {
|
|
pathRef.scope.crawl()
|
|
if (pathRef.scope.getBinding(fromName)) {
|
|
pathRef.scope.rename(fromName, toName)
|
|
foundBinding = true
|
|
}
|
|
pathRef.stop()
|
|
},
|
|
})
|
|
if (!foundBinding) return { source, renameCount: 0 }
|
|
|
|
const edits = []
|
|
traverse(ast, {
|
|
Identifier(pathRef) {
|
|
const { start, end, name } = pathRef.node
|
|
if (name === toName && source.slice(start, end) === fromName) edits.push({ start, end })
|
|
},
|
|
})
|
|
if (edits.length < 2) fail(`Babel scope rename did not capture the binding and its references in ${sourcePath}`)
|
|
let renamedSource = source
|
|
for (const { start, end } of edits.sort((left, right) => right.start - left.start)) {
|
|
renamedSource = `${renamedSource.slice(0, start)}${toName}${renamedSource.slice(end)}`
|
|
}
|
|
return { source: renamedSource, renameCount: edits.length }
|
|
}
|
|
|
|
function closeBindings(source) {
|
|
const ast = parse(source, { sourceType: 'module' })
|
|
let programPath
|
|
traverse(ast, {
|
|
Program(pathRef) {
|
|
programPath = pathRef
|
|
pathRef.scope.crawl()
|
|
pathRef.stop()
|
|
},
|
|
})
|
|
|
|
const included = new Map()
|
|
const queue = []
|
|
const imported = new Map()
|
|
const enqueue = (binding) => {
|
|
if (!binding || binding.scope !== programPath.scope || included.has(binding.identifier.name)) return
|
|
included.set(binding.identifier.name, binding)
|
|
queue.push(binding)
|
|
}
|
|
const visitReferences = (pathRef) => {
|
|
pathRef.traverse({
|
|
Identifier(reference) {
|
|
if (!reference.isReferencedIdentifier() && !reference.isBindingIdentifier()) return
|
|
enqueue(reference.scope.getBinding(reference.node.name))
|
|
},
|
|
})
|
|
}
|
|
|
|
enqueue(programPath.scope.getBinding('Po'))
|
|
while (queue.length > 0) {
|
|
const binding = queue.shift()
|
|
if (binding.kind === 'module') {
|
|
const declaration = binding.path.findParent((candidate) => candidate.isImportDeclaration())
|
|
if (declaration) {
|
|
const locals = imported.get(declaration.node.source.value) ?? new Set()
|
|
locals.add(binding.identifier.name)
|
|
imported.set(declaration.node.source.value, locals)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (binding.path.isFunctionDeclaration() || binding.path.isClassDeclaration()) {
|
|
visitReferences(binding.path)
|
|
continue
|
|
}
|
|
if (!binding.path.isVariableDeclarator()) continue
|
|
|
|
if (binding.path.node.init) visitReferences(binding.path.get('init'))
|
|
else {
|
|
for (const violation of binding.constantViolations) {
|
|
visitReferences(violation)
|
|
const owner = violation.findParent((candidate) => candidate.isVariableDeclarator())
|
|
if (owner && owner !== binding.path && owner.node.id.type === 'Identifier') {
|
|
enqueue(programPath.scope.getBinding(owner.node.id.name))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const declarationChunks = []
|
|
for (const statement of ast.program.body) {
|
|
if (statement.type === 'ImportDeclaration') {
|
|
if (REACT_IMPORTS.has(statement.source.value)) continue
|
|
const names = imported.get(statement.source.value)
|
|
if (!names || names.size === 0) continue
|
|
const specifiers = statement.specifiers.filter((specifier) => names.has(specifier.local.name))
|
|
if (specifiers.length === 0) continue
|
|
const defaultImport = specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier')
|
|
const namespaceImport = specifiers.find((specifier) => specifier.type === 'ImportNamespaceSpecifier')
|
|
const namedImports = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier')
|
|
const parts = []
|
|
if (defaultImport) parts.push(defaultImport.local.name)
|
|
if (namespaceImport) parts.push(`* as ${namespaceImport.local.name}`)
|
|
if (namedImports.length > 0) {
|
|
parts.push(`{ ${namedImports.map((specifier) => {
|
|
const importedName = specifier.imported.name ?? specifier.imported.value
|
|
return importedName === specifier.local.name ? importedName : `${importedName} as ${specifier.local.name}`
|
|
}).join(', ')} }`)
|
|
}
|
|
declarationChunks.push(`import ${parts.join(', ')} from ${JSON.stringify(statement.source.value)};`)
|
|
continue
|
|
}
|
|
|
|
if (statement.type === 'VariableDeclaration') {
|
|
for (const declaration of statement.declarations) {
|
|
const names = collectPatternNames(declaration.id)
|
|
if (!names.some((name) => included.has(name))) continue
|
|
declarationChunks.push(`${statement.kind} ${source.slice(declaration.start, declaration.end)};`)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
|
|
included.has(statement.id?.name)) {
|
|
declarationChunks.push(source.slice(statement.start, statement.end))
|
|
}
|
|
}
|
|
|
|
const reactImports = [
|
|
`import * as __ycodeReact from 'react';`,
|
|
`import * as __ycodeReactDOM from 'react-dom';`,
|
|
`import * as __ycodeJsxRuntime from 'react/jsx-runtime';`,
|
|
`const s = () => __ycodeReact;`,
|
|
`const c = () => __ycodeReactDOM;`,
|
|
`const f = () => __ycodeJsxRuntime;`,
|
|
]
|
|
const prefix = [
|
|
`// Generated from REA shipped-artifact Evidence; edits belong in recover-pierre-diff.mjs.`,
|
|
`// Source: renderer/assets/codeEditor-D3P9alks.js; sha256: 5e9a16dca97c102afc3c73cd7dd32ba63daf5e7ddff12707ee1ea63e7a427b19.`,
|
|
...reactImports,
|
|
].join('\n')
|
|
const body = declarationChunks.join('\n')
|
|
const suffix = [
|
|
`Po();`,
|
|
`export { No as PierreDiffEditor, buildFileDiff, processFile, parseDiffFromFile, getFiletypeFromFileName };`,
|
|
].join('\n')
|
|
return { code: `${prefix}\n${body}\n${suffix}\n`, bindingCount: included.size, imports: new Set(imported.keys()) }
|
|
}
|
|
|
|
async function writeIfAbsentOrSame(filePath, text) {
|
|
try {
|
|
const previous = await fs.readFile(filePath, 'utf8')
|
|
if (previous === text) return
|
|
const generatedEngine = path.basename(filePath) === 'engine.mjs' &&
|
|
previous.startsWith('// Generated from REA shipped-artifact Evidence; edits belong in recover-pierre-diff.mjs.')
|
|
let generatedManifest = false
|
|
if (path.basename(filePath) === 'manifest.json') {
|
|
try {
|
|
generatedManifest = JSON.parse(previous).kind === 'rea-selected-pierre-diff-recovery'
|
|
} catch {
|
|
generatedManifest = false
|
|
}
|
|
}
|
|
if (!generatedEngine && !generatedManifest) fail(`Refusing to overwrite changed recovered artifact: ${filePath}`)
|
|
await fs.writeFile(filePath, text, 'utf8')
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') throw error
|
|
await fs.writeFile(filePath, text, 'utf8')
|
|
}
|
|
}
|
|
|
|
const [mainSource, dependencies, leafDependencies, uiLanguages] = await Promise.all([
|
|
loadEvidence(MAIN_EVIDENCE_PATH),
|
|
loadEvidence(DEPENDENCY_EVIDENCE_PATH),
|
|
loadEvidence(LEAF_EVIDENCE_PATH),
|
|
loadEvidence(LANGUAGE_EVIDENCE_PATH),
|
|
])
|
|
const dynamicEvidencePaths = (await fs.readdir('.tmp'))
|
|
.filter((fileName) => /^rea-selected-pierre-chunks-\d{3}\.json$/u.test(fileName))
|
|
.map((fileName) => path.join('.tmp', fileName))
|
|
.sort()
|
|
if (dynamicEvidencePaths.length === 0) fail('No REA-selected Pierre dynamic chunk Evidence was found')
|
|
const dynamicEvidence = await Promise.all(dynamicEvidencePaths.map(loadEvidence))
|
|
const evidenceSet = [mainSource, dependencies, leafDependencies, uiLanguages, ...dynamicEvidence]
|
|
const sources = new Map()
|
|
for (const evidence of evidenceSet) {
|
|
for (const [sourcePath, file] of evidence.files) {
|
|
const existing = sources.get(sourcePath)
|
|
if (existing && (existing.sha256 !== file.sha256 || existing.size_bytes !== file.size_bytes)) {
|
|
fail(`Conflicting REA Evidence for selected file ${sourcePath}`)
|
|
}
|
|
sources.set(sourcePath, file)
|
|
}
|
|
}
|
|
|
|
for (const [sourcePath, expectedKind] of Object.entries(SOURCE_FILES)) {
|
|
if (!sources.has(sourcePath)) fail(`Required selected REA source is missing (${expectedKind}): ${sourcePath}`)
|
|
}
|
|
const appPackage = JSON.parse(sources.get('package.json').text)
|
|
if (appPackage.version !== '5.8.2') fail(`Unexpected WorkBuddy app version: ${appPackage.version}`)
|
|
|
|
const mainText = sources.get('renderer/assets/codeEditor-D3P9alks.js').text
|
|
const dynamicImportPaths = collectDynamicImportPaths(mainText)
|
|
const dynamicAssetPaths = [...dynamicImportPaths].map((importPath) => {
|
|
if (!importPath.startsWith('./') || !importPath.endsWith('.js')) {
|
|
fail(`Unexpected non-local dynamic chunk path in CodeView: ${importPath}`)
|
|
}
|
|
return `renderer/assets/${importPath.slice(2)}`
|
|
})
|
|
for (const sourcePath of dynamicAssetPaths) {
|
|
if (!sources.has(sourcePath)) fail(`REA dynamic chunk evidence is missing ${sourcePath}`)
|
|
}
|
|
const extracted = closeBindings(mainText)
|
|
if (extracted.bindingCount < 700) fail(`Pierre closure is unexpectedly incomplete: ${extracted.bindingCount} bindings`)
|
|
if (extracted.imports.has('./src-0FGg1seX.js') || extracted.imports.has('./monaco-init-CE4MbM4x.js')) {
|
|
fail('Pierre closure unexpectedly depends on app shared code or Monaco initialization')
|
|
}
|
|
const renamedEngine = renameTopLevelBinding(
|
|
extracted.code,
|
|
VITE_PRELOAD_HELPER,
|
|
RECOVERED_PRELOAD_HELPER,
|
|
'recovered engine.mjs',
|
|
)
|
|
|
|
const cssSource = sources.get('renderer/assets/codeEditor-Bxmo_AI3.css').text
|
|
const cssAst = postcss.parse(cssSource)
|
|
const cssRules = []
|
|
cssAst.walkRules((rule) => {
|
|
if (rule.selectors?.some((selector) => selector.includes('_pierre-diff-editor'))) {
|
|
cssRules.push(cssSource.slice(rule.source.start.offset, rule.source.end.offset))
|
|
}
|
|
})
|
|
if (cssRules.length !== 8) fail(`Expected eight exact Pierre CSS module rules, found ${cssRules.length}`)
|
|
|
|
await fs.mkdir(OUTPUT_DIRECTORY, { recursive: true })
|
|
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, 'engine.mjs'), renamedEngine.source)
|
|
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, 'engine.css'), `${cssRules.join('\n')}\n`)
|
|
|
|
const assetRecords = []
|
|
const copyingAssetPaths = [...new Set([...COPYING_ASSETS, ...dynamicAssetPaths])]
|
|
for (const sourcePath of copyingAssetPaths) {
|
|
const source = sources.get(sourcePath)
|
|
if (!source) fail(`Required selected REA asset is missing: ${sourcePath}`)
|
|
const fileName = path.posix.basename(sourcePath)
|
|
const renamedAsset = renameTopLevelBinding(
|
|
source.text,
|
|
VITE_PRELOAD_HELPER,
|
|
RECOVERED_PRELOAD_HELPER,
|
|
sourcePath,
|
|
)
|
|
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, fileName), renamedAsset.source)
|
|
assetRecords.push({
|
|
path: fileName,
|
|
bytes: source.size_bytes,
|
|
sha256: source.sha256,
|
|
renamed_top_level_preload_helper_references: renamedAsset.renameCount,
|
|
})
|
|
}
|
|
|
|
const outputs = ['engine.mjs', 'engine.css', ...assetRecords.map((asset) => asset.path)]
|
|
const outputRecords = []
|
|
for (const fileName of outputs) {
|
|
const content = await fs.readFile(path.join(OUTPUT_DIRECTORY, fileName))
|
|
outputRecords.push({ path: fileName, bytes: content.length, sha256: sha256(content) })
|
|
}
|
|
const manifest = {
|
|
kind: 'rea-selected-pierre-diff-recovery',
|
|
target: { name: appPackage.name, version: appPackage.version, root_sha256: EXPECTED_ROOT_SHA256 },
|
|
evidence: {
|
|
renderer: mainSource.evidence.evidence_id,
|
|
dependencies: dependencies.evidence.evidence_id,
|
|
leaf_dependencies: leafDependencies.evidence.evidence_id,
|
|
ui_languages: uiLanguages.evidence.evidence_id,
|
|
dynamic_chunks: dynamicEvidence.map(({ evidence }) => evidence.evidence_id),
|
|
},
|
|
renderer_source: {
|
|
path: 'renderer/assets/codeEditor-D3P9alks.js',
|
|
sha256: '5e9a16dca97c102afc3c73cd7dd32ba63daf5e7ddff12707ee1ea63e7a427b19',
|
|
closure_program_bindings: extracted.bindingCount,
|
|
imports: [...extracted.imports].sort(),
|
|
dynamic_import_count: dynamicImportPaths.size,
|
|
dynamic_import_files: dynamicAssetPaths,
|
|
renamed_top_level_preload_helper: {
|
|
from: VITE_PRELOAD_HELPER,
|
|
to: RECOVERED_PRELOAD_HELPER,
|
|
references: renamedEngine.renameCount,
|
|
},
|
|
},
|
|
css_source: {
|
|
path: 'renderer/assets/codeEditor-Bxmo_AI3.css',
|
|
sha256: '1c8f2210c731a25f9051088df0db78b9de5b087c802b3a9e36828c1b6b54c353',
|
|
exact_pierre_rules: cssRules.length,
|
|
},
|
|
dynamic_chunk_assets: dynamicAssetPaths.length,
|
|
vite_preload_helper_rename: {
|
|
from: VITE_PRELOAD_HELPER,
|
|
to: RECOVERED_PRELOAD_HELPER,
|
|
affected_assets: assetRecords.filter((asset) => asset.renamed_top_level_preload_helper_references > 0)
|
|
.map((asset) => ({ path: asset.path, references: asset.renamed_top_level_preload_helper_references })),
|
|
},
|
|
limitations: [
|
|
'The original package manifest contains no Pierre or Shiki package version; this is a source-locked bundle recovery, not an npm package pin.',
|
|
'The optional shiki-wasm branch references renderer/assets/wasm-B9ZqxnKj.js, which the REA selector reports absent; the recovered component config uses the shiki-js engine.',
|
|
'Every static dynamic import in codeEditor-D3P9alks.js was selected through REA and copied; unselected app-shared and Monaco bundles are excluded from this component closure.',
|
|
],
|
|
assets: assetRecords,
|
|
outputs: outputRecords,
|
|
}
|
|
await writeIfAbsentOrSame(path.join(OUTPUT_DIRECTORY, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
|
|
console.log(JSON.stringify({
|
|
outputDirectory: OUTPUT_DIRECTORY,
|
|
closureProgramBindings: extracted.bindingCount,
|
|
closureBytes: Buffer.byteLength(extracted.code, 'utf8'),
|
|
renamedPreloadHelperReferences: renamedEngine.renameCount,
|
|
importedModules: [...extracted.imports].sort(),
|
|
dynamicChunks: dynamicImportPaths.size,
|
|
cssRules: cssRules.length,
|
|
assets: assetRecords.length,
|
|
manifest: path.join(OUTPUT_DIRECTORY, 'manifest.json'),
|
|
}, null, 2))
|