mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 02:32:54 +00:00
* feat(javascript): recover code-split bundles from multiple inputs recover_javascript_sources accepted a single path, and validateModuleSet bound every module's provenance to that one snapshot. A code-split app (runtime + chunks, or an Electron main/preload/renderer set) had to be recovered one file per call, and modules recovered alone could not resolve imports across chunks: cross-chunk require(id) references stayed as numeric ids with property reads. Add an optional additional_paths input. The listed files are concatenated, in order after path, into the single engine snapshot, so one Wakaru run sees every module and cross-chunk imports resolve. validateModuleSet's single-snapshot binding is unchanged. Module provenance is attributed back to the input file its first extraction range starts in, with byte ranges rebased to that file's own offsets; a module whose ranges cross an input boundary keeps the concatenated snapshot identity and offsets, recorded as an explicit limitation. Single-path behavior is byte-for-byte unchanged (one segment, offset 0). Fixes #1605 * fix(javascript): preserve source identity in multi-input recovery --------- Co-authored-by: JingHao-Leon <w15909130000@gmail.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
import { join } from "node:path";
|
|
import {
|
|
createAnalysisExecution,
|
|
type ExecutionOptions,
|
|
} from "../../application/AnalysisProvider.js";
|
|
import type { SafeOutputTree } from "../../artifacts/SafeOutputTree.js";
|
|
import {
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisOutputError,
|
|
} from "../../domain/analysisErrorCore.js";
|
|
import type { JavaScriptRecoveryInput } from "../../domain/javascript/javascriptRecovery.js";
|
|
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
|
import {
|
|
fingerprintRecoveryFile,
|
|
inventoryRecoveryFiles,
|
|
readRecoveryFile,
|
|
type snapshotRecoveryInputs,
|
|
} from "./RecoveryFiles.js";
|
|
import {
|
|
runWakaruCommand,
|
|
type resolveWakaruCommand,
|
|
type WakaruLauncher,
|
|
} from "./WakaruCommand.js";
|
|
import { parseWakaruReports } from "./WakaruReport.js";
|
|
import { publishWakaruArtifacts } from "./WakaruPublication.js";
|
|
import { RECOVERY_LIMITS, WAKARU_PROVIDER_IDENTITY } from "./WakaruRelease.js";
|
|
import { admitWakaruVersion } from "./WakaruVersion.js";
|
|
|
|
const OPERATION = "recover_javascript_sources";
|
|
const MODES = {
|
|
structural: "strict",
|
|
heuristic: "auto",
|
|
inspection: "inspect",
|
|
} as const;
|
|
|
|
type RecoveryWorkspace = {
|
|
root: string;
|
|
tree: SafeOutputTree;
|
|
input: JavaScriptRecoveryInput;
|
|
sources: Awaited<ReturnType<typeof snapshotRecoveryInputs>>;
|
|
engine: Awaited<ReturnType<typeof resolveWakaruCommand>>;
|
|
environment: Readonly<Record<string, string | undefined>>;
|
|
launcher: WakaruLauncher | undefined;
|
|
options: ExecutionOptions | undefined;
|
|
deadline: number;
|
|
};
|
|
|
|
/** Consume the pinned producer representation in a private workspace. */
|
|
const executeWakaruRecovery = async (context: RecoveryWorkspace) => {
|
|
const { root, input, sources, engine, environment, launcher, options } =
|
|
context;
|
|
const staging = join(root, "modules");
|
|
const commandContext = {
|
|
...engine,
|
|
cwd: root,
|
|
deadline: context.deadline,
|
|
environment: environment,
|
|
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
...(launcher === undefined ? {} : { launcher: launcher }),
|
|
};
|
|
const version = await runWakaruCommand({
|
|
...commandContext,
|
|
arguments: ["--version"],
|
|
});
|
|
const admitted =
|
|
version.exit_code === 0
|
|
? admitWakaruVersion(version.stdout.trim())
|
|
: undefined;
|
|
if (admitted === undefined || admitted.status === "unsupported") {
|
|
// The message names the mismatch; the reason also keeps the tool's stderr.
|
|
const mismatch = `${admitted?.message ?? `Wakaru --version failed; reported version: ${version.stdout.trim()}`}; exit: ${String(version.exit_code)}`;
|
|
throw new AnalysisCapabilityUnavailableError(
|
|
"wakaru",
|
|
OPERATION,
|
|
`${mismatch}; stderr: ${version.stderr}`,
|
|
{ userMessage: `${mismatch}.` },
|
|
);
|
|
}
|
|
const run = await runWakaruCommand({
|
|
...commandContext,
|
|
staging,
|
|
arguments: [
|
|
...sources.map((source) => source.snapshot_path),
|
|
`--unpack=${MODES[input.extraction_mode]}`,
|
|
`--level=${input.rewrite_level}`,
|
|
"--provenance",
|
|
"--emit-source-map",
|
|
"--json",
|
|
"-o",
|
|
staging,
|
|
],
|
|
});
|
|
const files = await inventoryRecoveryFiles(staging, options?.signal);
|
|
const snapshots = new Map<string, Buffer>();
|
|
for (const source of sources)
|
|
snapshots.set(
|
|
source.snapshot_path,
|
|
await readRecoveryFile(
|
|
source.snapshot_path,
|
|
source.bytes,
|
|
options?.signal,
|
|
),
|
|
);
|
|
const parsed = await parseWakaruReports({
|
|
execution: run,
|
|
provenancePath: join(staging, "provenance.json"),
|
|
snapshots,
|
|
files,
|
|
});
|
|
return { version, admitted, run, parsed };
|
|
};
|
|
|
|
/** Publish validated recovery and retain exact source and engine bindings. */
|
|
export const prepareWakaruExecution = async (context: RecoveryWorkspace) => {
|
|
const { root, tree, input, sources, engine, options } = context;
|
|
const [source] = sources;
|
|
const staging = join(root, "modules");
|
|
const { version, admitted, run, parsed } =
|
|
await executeWakaruRecovery(context);
|
|
const result = await publishWakaruArtifacts({
|
|
tree,
|
|
staging,
|
|
input,
|
|
sources,
|
|
engine,
|
|
admitted,
|
|
parsed,
|
|
execution: run,
|
|
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
});
|
|
for (const selected of sources) {
|
|
const current = await fingerprintRecoveryFile(
|
|
selected.path,
|
|
RECOVERY_LIMITS.inputBytes,
|
|
options?.signal,
|
|
);
|
|
if (current.sha256 !== selected.sha256 || current.bytes !== selected.bytes)
|
|
throw new AnalysisOutputError(
|
|
OPERATION,
|
|
`Original input changed during recovery: ${selected.path}`,
|
|
);
|
|
}
|
|
if (
|
|
(
|
|
await fingerprintRecoveryFile(
|
|
engine.command,
|
|
RECOVERY_LIMITS.outputBytes,
|
|
options?.signal,
|
|
)
|
|
).sha256 !== engine.sha256
|
|
)
|
|
throw new AnalysisOutputError(
|
|
OPERATION,
|
|
"Configured engine bytes changed during recovery",
|
|
);
|
|
return createAnalysisExecution(
|
|
result,
|
|
{ ...WAKARU_PROVIDER_IDENTITY, version: admitted.version },
|
|
{
|
|
rawResult: jsonValueSchema.parse({
|
|
version,
|
|
execution: run,
|
|
report: parsed.report,
|
|
provenance: parsed.provenance,
|
|
}),
|
|
subject: {
|
|
path: source.path,
|
|
sha256: source.sha256,
|
|
format: "javascript",
|
|
},
|
|
locations: [
|
|
...sources.map((selected) => ({
|
|
kind: "artifact-path" as const,
|
|
path: selected.path,
|
|
})),
|
|
...result.modules.map((module) => ({
|
|
kind: "artifact-path" as const,
|
|
path: module.artifact.path,
|
|
})),
|
|
],
|
|
limitations: result.limitations,
|
|
},
|
|
);
|
|
};
|