mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 10:25:42 +00:00
* fix(javascript): match source-map originals that name parent directories * fix(javascript): resolve parent segments against the source map's directory * fix(javascript): resolve source originals against their map location * fix(javascript): preserve URI and empty source map path semantics * fix(javascript): separate source URL paths from opaque URI payloads * fix(javascript): preserve filesystem names during URI classification * style(javascript): format source URI presence guard * fix(javascript): resolve source map references with URL semantics * fix(javascript): preserve literal UNC source map paths --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
import { posix } from "node:path";
|
|
|
|
import { compareCodePoints } from "../canonicalOrdering.js";
|
|
import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js";
|
|
import type { HistoricalSourceGraph } from "../referenceSourceGraph.js";
|
|
import {
|
|
SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS,
|
|
type SourceToBundleCandidate,
|
|
type SourceToBundleSignal,
|
|
} from "./sourceToBundleComparisonSchemas.js";
|
|
import { isDigest } from "../digests.js";
|
|
import { resolveJavaScriptSourceMapPath } from "./javascriptSourceMapPaths.js";
|
|
|
|
type SourceFile = Extract<
|
|
HistoricalSourceGraph["entries"][number],
|
|
{ readonly kind: "file" }
|
|
>;
|
|
|
|
type CurrentPathKind =
|
|
| "source-map-original"
|
|
| "canonical-path"
|
|
| "observation-path";
|
|
|
|
interface CurrentPath {
|
|
readonly kind: CurrentPathKind;
|
|
readonly value: string;
|
|
/** Artifact-relative map resolution identifies one path, not its suffixes. */
|
|
readonly allowSuffix: boolean;
|
|
}
|
|
|
|
interface CurrentProjection {
|
|
readonly node: ApplicationNode;
|
|
readonly digests: ReadonlySet<string>;
|
|
readonly paths: readonly CurrentPath[];
|
|
}
|
|
|
|
/** Indexed current-node facts used by deterministic source matching. */
|
|
export interface SourceToBundleCandidateIndex {
|
|
readonly nodes: ReadonlyMap<string, CurrentProjection>;
|
|
readonly byDigest: ReadonlyMap<string, ReadonlySet<string>>;
|
|
readonly byPathSuffix: ReadonlyMap<string, ReadonlySet<string>>;
|
|
readonly byBasename: ReadonlyMap<string, ReadonlySet<string>>;
|
|
}
|
|
|
|
const RELEVANT_NODE_KINDS = new Set([
|
|
"javascript-asset",
|
|
"javascript-module",
|
|
"source-module",
|
|
]);
|
|
const PATH_PROPERTIES = [
|
|
"path",
|
|
"source",
|
|
"original_source",
|
|
"module_path",
|
|
"resolved_path",
|
|
"declared_path",
|
|
] as const;
|
|
|
|
const signalWeight = (kind: SourceToBundleSignal["kind"]): number => {
|
|
const entry = SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS.find(
|
|
([signal]) => signal === kind,
|
|
);
|
|
if (entry === undefined)
|
|
throw new TypeError(`Missing source-to-bundle signal weight: ${kind}`);
|
|
return entry[1];
|
|
};
|
|
|
|
/** Keep source-bearing application nodes in stable identifier order. */
|
|
export const sourceBearingNodes = (
|
|
nodes: readonly ApplicationNode[],
|
|
): ApplicationNode[] =>
|
|
nodes
|
|
.filter(({ kind }) => RELEVANT_NODE_KINDS.has(kind))
|
|
.sort((left, right) => compareCodePoints(left.node_id, right.node_id));
|
|
|
|
/** Keep historical source files in stable path order. */
|
|
export const historicalSourceFiles = (
|
|
graph: HistoricalSourceGraph,
|
|
): SourceFile[] =>
|
|
graph.entries
|
|
.filter(
|
|
(entry): entry is SourceFile =>
|
|
entry.kind === "file" && entry.classifications.includes("source"),
|
|
)
|
|
.sort((left, right) => compareCodePoints(left.path, right.path));
|
|
|
|
/** Build deterministic candidate indices without assigning fuzzy matches. */
|
|
export const buildSourceToBundleCandidateIndex = (
|
|
nodes: readonly ApplicationNode[],
|
|
): SourceToBundleCandidateIndex => {
|
|
const projections = nodes.map(projectCurrentNode);
|
|
const byDigest = new Map<string, Set<string>>();
|
|
const byPathSuffix = new Map<string, Set<string>>();
|
|
const byBasename = new Map<string, Set<string>>();
|
|
for (const projection of projections) {
|
|
for (const digest of projection.digests)
|
|
addIndexValue(byDigest, digest, projection.node.node_id);
|
|
for (const path of projection.paths) {
|
|
for (const suffix of path.allowSuffix
|
|
? pathSuffixes(path.value)
|
|
: [path.value])
|
|
addIndexValue(byPathSuffix, suffix, projection.node.node_id);
|
|
addIndexValue(
|
|
byBasename,
|
|
posix.basename(path.value),
|
|
projection.node.node_id,
|
|
);
|
|
}
|
|
}
|
|
return {
|
|
nodes: new Map(
|
|
projections.map((projection) => [projection.node.node_id, projection]),
|
|
),
|
|
byDigest,
|
|
byPathSuffix,
|
|
byBasename,
|
|
};
|
|
};
|
|
|
|
/** Candidate identifiers ordered by strongest available signal, then ID. */
|
|
export const candidateIdsForSource = (
|
|
source: SourceFile,
|
|
index: SourceToBundleCandidateIndex,
|
|
): string[] => {
|
|
const groups = [
|
|
source.sha256 === null ? undefined : index.byDigest.get(source.sha256),
|
|
index.byPathSuffix.get(source.path),
|
|
index.byBasename.get(posix.basename(source.path)),
|
|
];
|
|
const seen = new Set<string>();
|
|
const output: string[] = [];
|
|
for (const group of groups) {
|
|
if (group === undefined) continue;
|
|
for (const nodeId of [...group].sort(compareCodePoints)) {
|
|
if (seen.has(nodeId)) continue;
|
|
seen.add(nodeId);
|
|
output.push(nodeId);
|
|
}
|
|
}
|
|
return output;
|
|
};
|
|
|
|
/** Whether an indexed candidate carries any content digest to compare. */
|
|
export const candidateHasDigest = (
|
|
index: SourceToBundleCandidateIndex,
|
|
nodeId: string,
|
|
): boolean => (index.nodes.get(nodeId)?.digests.size ?? 0) > 0;
|
|
|
|
/** Score one indexed candidate from explicit, caller-visible signals. */
|
|
export const scoreSourceToBundleCandidate = (
|
|
source: SourceFile,
|
|
nodeId: string,
|
|
index: SourceToBundleCandidateIndex,
|
|
): SourceToBundleCandidate | null => {
|
|
const projection = index.nodes.get(nodeId);
|
|
if (projection === undefined) return null;
|
|
const signals = candidateSignals(source, projection);
|
|
const score = signals.reduce((total, signal) => total + signal.weight, 0);
|
|
if (score < 20) return null;
|
|
return {
|
|
current_node_id: projection.node.node_id,
|
|
current_node_kind: projection.node.kind,
|
|
score,
|
|
confidence: candidateConfidence(signals),
|
|
signals,
|
|
};
|
|
};
|
|
|
|
const projectCurrentNode = (node: ApplicationNode): CurrentProjection => {
|
|
const digests = new Set<string>();
|
|
const paths: CurrentPath[] = [];
|
|
if (node.identity.strategy === "content-digest")
|
|
digests.add(node.identity.sha256);
|
|
if (node.identity.strategy === "source-map-original") {
|
|
if (node.identity.source_sha256 !== null)
|
|
digests.add(node.identity.source_sha256);
|
|
const mapLocations = node.observations.flatMap(({ evidence }) =>
|
|
evidence.extractor.operation === "parse-local-source-map" &&
|
|
evidence.location.available &&
|
|
evidence.location.value.kind === "artifact-path"
|
|
? [evidence.location.value.path]
|
|
: [],
|
|
);
|
|
if (mapLocations.length === 0)
|
|
addPath(paths, "source-map-original", node.identity.original_source);
|
|
for (const mapPath of mapLocations)
|
|
addPath(
|
|
paths,
|
|
"source-map-original",
|
|
node.identity.original_source,
|
|
mapPath,
|
|
);
|
|
}
|
|
if (node.identity.strategy === "canonical-path")
|
|
addPath(paths, "canonical-path", node.identity.path);
|
|
for (const observation of node.observations) {
|
|
const sourceDigest = observation.properties.source_sha256;
|
|
if (typeof sourceDigest === "string" && isDigest(sourceDigest))
|
|
digests.add(sourceDigest);
|
|
for (const key of PATH_PROPERTIES) {
|
|
const location = observation.evidence.location;
|
|
const mapPath =
|
|
node.identity.strategy === "source-map-original" &&
|
|
observation.evidence.extractor.operation === "parse-local-source-map" &&
|
|
(key === "source" || key === "original_source") &&
|
|
location.available &&
|
|
location.value.kind === "artifact-path"
|
|
? location.value.path
|
|
: undefined;
|
|
addJsonPath(paths, observation.properties[key], mapPath);
|
|
}
|
|
}
|
|
return {
|
|
node,
|
|
digests,
|
|
paths: uniquePaths(paths),
|
|
};
|
|
};
|
|
|
|
const candidateSignals = (
|
|
source: SourceFile,
|
|
current: CurrentProjection,
|
|
): SourceToBundleSignal[] => {
|
|
const signals: SourceToBundleSignal[] = [];
|
|
if (source.sha256 !== null && current.digests.has(source.sha256))
|
|
signals.push(signal("exact-source-digest", source.sha256, [source.sha256]));
|
|
const sourceMapPaths = matchingPaths(
|
|
source.path,
|
|
current.paths.filter(({ kind }) => kind === "source-map-original"),
|
|
);
|
|
if (sourceMapPaths.length > 0)
|
|
signals.push(
|
|
signal("source-map-original-path", source.path, sourceMapPaths),
|
|
);
|
|
const otherPaths = current.paths.filter(
|
|
({ kind }) => kind !== "source-map-original",
|
|
);
|
|
const exactPaths = otherPaths
|
|
.filter(({ value }) => value === source.path)
|
|
.map(({ value }) => value);
|
|
if (exactPaths.length > 0)
|
|
signals.push(signal("current-path-exact", source.path, exactPaths));
|
|
const suffixPaths = otherPaths
|
|
.filter(
|
|
({ value, allowSuffix }) =>
|
|
allowSuffix &&
|
|
value !== source.path &&
|
|
value.endsWith(`/${source.path}`),
|
|
)
|
|
.map(({ value }) => value);
|
|
if (suffixPaths.length > 0)
|
|
signals.push(signal("current-path-suffix", source.path, suffixPaths));
|
|
const basename = posix.basename(source.path);
|
|
const basenamePaths = current.paths
|
|
.filter(({ value }) => posix.basename(value) === basename)
|
|
.map(({ value }) => value);
|
|
if (basenamePaths.length > 0)
|
|
signals.push(signal("basename-match", basename, basenamePaths));
|
|
const extension = posix.extname(source.path).toLowerCase();
|
|
const extensionPaths = current.paths
|
|
.filter(({ value }) => posix.extname(value).toLowerCase() === extension)
|
|
.map(({ value }) => value);
|
|
if (extension !== "" && extensionPaths.length > 0)
|
|
signals.push(signal("language-extension", extension, extensionPaths));
|
|
return signals;
|
|
};
|
|
|
|
const signal = (
|
|
kind: SourceToBundleSignal["kind"],
|
|
sourceValue: string,
|
|
currentValues: readonly string[],
|
|
): SourceToBundleSignal => ({
|
|
kind,
|
|
weight: signalWeight(kind),
|
|
source_value: sourceValue,
|
|
current_values: [...new Set(currentValues)].sort(compareCodePoints),
|
|
});
|
|
|
|
const candidateConfidence = (
|
|
signals: readonly SourceToBundleSignal[],
|
|
): SourceToBundleCandidate["confidence"] => {
|
|
const kinds = new Set(signals.map(({ kind }) => kind));
|
|
if (kinds.has("exact-source-digest")) return "exact";
|
|
if (kinds.has("source-map-original-path") || kinds.has("current-path-exact"))
|
|
return "high";
|
|
return kinds.has("current-path-suffix") ? "medium" : "low";
|
|
};
|
|
|
|
const matchingPaths = (
|
|
sourcePath: string,
|
|
currentPaths: readonly CurrentPath[],
|
|
): string[] =>
|
|
currentPaths
|
|
.filter(
|
|
({ value, allowSuffix }) =>
|
|
value === sourcePath ||
|
|
(allowSuffix && value.endsWith(`/${sourcePath}`)),
|
|
)
|
|
.map(({ value }) => value);
|
|
|
|
const addJsonPath = (
|
|
paths: CurrentPath[],
|
|
value: unknown,
|
|
mapPath?: string,
|
|
): void => {
|
|
if (typeof value === "string")
|
|
addPath(paths, "observation-path", value, mapPath);
|
|
};
|
|
|
|
const addPath = (
|
|
paths: CurrentPath[],
|
|
kind: CurrentPathKind,
|
|
raw: string,
|
|
mapPath?: string,
|
|
): void => {
|
|
if (kind === "source-map-original" || mapPath !== undefined) {
|
|
const resolved = resolveJavaScriptSourceMapPath(raw, mapPath);
|
|
if (resolved !== null)
|
|
paths.push({
|
|
kind,
|
|
value: resolved.value,
|
|
allowSuffix: resolved.scope === "suffix",
|
|
});
|
|
return;
|
|
}
|
|
const value = normalizeCurrentPath(raw);
|
|
if (value !== null) paths.push({ kind, value, allowSuffix: true });
|
|
};
|
|
|
|
const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//iu;
|
|
const SCHEME_URL_PREFIX = /^[a-z][a-z0-9+.-]*:\/\/+/iu;
|
|
|
|
const normalizeCurrentPath = (raw: string): string | null => {
|
|
const withoutQuery = SCHEME_URL.test(raw)
|
|
? (raw.split(/[?#]/u, 1)[0] ?? "")
|
|
: raw;
|
|
const portable = withoutQuery
|
|
.replace(SCHEME_URL_PREFIX, "")
|
|
.replaceAll("\\", "/");
|
|
const parts: string[] = [];
|
|
for (const part of portable.split("/")) {
|
|
if (part === "" || part === ".") continue;
|
|
if (part === "..") {
|
|
if (parts.length === 0) return null;
|
|
parts.pop();
|
|
} else parts.push(part);
|
|
}
|
|
if (parts.length === 0) return null;
|
|
return parts.join("/");
|
|
};
|
|
|
|
const uniquePaths = (paths: readonly CurrentPath[]): CurrentPath[] =>
|
|
[
|
|
...new Map(
|
|
paths.map((path) => [
|
|
`${path.kind}\0${path.value}\0${String(path.allowSuffix)}`,
|
|
path,
|
|
]),
|
|
).values(),
|
|
].sort((left, right) =>
|
|
compareCodePoints(
|
|
`${left.kind}\0${left.value}`,
|
|
`${right.kind}\0${right.value}`,
|
|
),
|
|
);
|
|
|
|
const pathSuffixes = (path: string): string[] => {
|
|
const parts = path.split("/");
|
|
return parts.map((_, index) => parts.slice(index).join("/"));
|
|
};
|
|
|
|
const addIndexValue = (
|
|
index: Map<string, Set<string>>,
|
|
key: string,
|
|
nodeId: string,
|
|
): void => {
|
|
const values = index.get(key) ?? new Set<string>();
|
|
values.add(nodeId);
|
|
index.set(key, values);
|
|
};
|