Files
orca/config/scripts/xterm-sourcemap-source-contract.mjs
T

103 lines
3.8 KiB
JavaScript

import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { splitPatchEntries } from './xterm-patch-text.mjs'
function mappedSourcePath(source) {
return source.match(/^(?:webpack:\/\/@xterm\/xterm\/\.\/|\.\.\/\.\.\/)(src\/.+)$/)?.[1]
}
export function mappedSourceFiles(map) {
const files = new Map()
for (const [index, source] of map.sources.entries()) {
const relative = mappedSourcePath(source)
if (!relative) {
continue
}
if (relative.split('/').includes('..')) {
throw new Error(`Unsafe mapped source: ${relative}`)
}
if (files.has(relative)) {
throw new Error(`Duplicate mapped source: ${relative}`)
}
if (typeof map.sourcesContent[index] !== 'string') {
throw new Error(`Missing mapped content: ${relative}`)
}
files.set(relative, map.sourcesContent[index])
}
if (files.size === 0) {
throw new Error('Published source map contains no upstream sources')
}
return files
}
// Headless publishes source only inside its map; require exact-byte provenance.
export function assertMappedSourcesMatch(mapPath, checkoutRoot) {
const files = mappedSourceFiles(JSON.parse(readFileSync(mapPath, 'utf8')))
for (const [relative, content] of files) {
if (readFileSync(path.join(checkoutRoot, relative), 'utf8') !== content) {
throw new Error(`Mapped source differs from pinned checkout: ${relative}`)
}
}
}
export function assertMappedPatchDerivation(sourcePatch, generatedPatch, sourceMap) {
const entry = splitPatchEntries(generatedPatch).find((entry) => entry.path === sourceMap)
const readSide = (sign) => {
const lines = entry?.text.split('\n') ?? []
const hunks = lines.filter((line) => line.startsWith('@@'))
if (hunks.length !== 1 || !/^@@ -1(?:,\d+)? \+1(?:,\d+)? @@/.test(hunks[0])) {
throw new Error(`Source map patch must contain one complete JSON hunk: ${sourceMap}`)
}
const body = lines
.slice(lines.indexOf(hunks[0]) + 1)
.filter((line) => line.startsWith(sign) || line.startsWith(' '))
.map((line) => line.slice(1))
.join('\n')
return JSON.parse(body)
}
const beforeMap = readSide('-'),
afterMap = readSide('+')
if (JSON.stringify(beforeMap.sources) !== JSON.stringify(afterMap.sources)) {
throw new Error('Source map source names changed')
}
const before = mappedSourceFiles(beforeMap),
after = mappedSourceFiles(afterMap)
for (const [index, source] of beforeMap.sources.entries()) {
if (
!mappedSourcePath(source) &&
beforeMap.sourcesContent[index] !== afterMap.sourcesContent[index]
) {
throw new Error(`Unrecognized mapped source changed: ${source}`)
}
}
const directory = mkdtempSync(path.join(tmpdir(), 'orca-xterm-mapped-source-'))
try {
for (const [relative, content] of before) {
mkdirSync(path.dirname(path.join(directory, relative)), { recursive: true })
writeFileSync(path.join(directory, relative), content)
}
execFileSync('git', ['apply', '--whitespace=nowarn', '-'], {
cwd: directory,
input: sourcePatch,
stdio: ['pipe', 'pipe', 'pipe']
})
for (const entry of splitPatchEntries(sourcePatch)) {
if (!after.has(entry.path)) {
throw new Error(`Changed source missing from generated map: ${entry.path}`)
}
}
if (before.size !== after.size) {
throw new Error('Source map source set changed')
}
for (const [relative, content] of after) {
if (readFileSync(path.join(directory, relative), 'utf8') !== content) {
throw new Error(`Source patch and generated map disagree: ${relative}`)
}
}
} finally {
rmSync(directory, { recursive: true, force: true })
}
}