-
Notifications
You must be signed in to change notification settings - Fork 5.9k
fix: preserve real prefix when middle ID segment is a reserved word #513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
bfe10ee
6d54111
0a0ffd2
67c6f19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,8 +27,17 @@ const TYPE_TO_PREFIX: Record<string, string> = { | |
| /** | ||
| * Strips all non-valid prefixes from an ID, returning the bare path | ||
| * and the first valid prefix found (if any). | ||
| * | ||
| * `expectedPrefix` is the canonical prefix for the node's declared type | ||
| * (e.g. "file" for a file node). It disambiguates a reserved word that | ||
| * appears before the expected prefix — a spurious project-name prefix that | ||
| * happens to collide with a reserved word — from a reserved word that is a | ||
| * legitimate middle path segment. | ||
| */ | ||
| function stripToValidPrefix(id: string): { prefix: string | null; path: string } { | ||
| function stripToValidPrefix( | ||
| id: string, | ||
| expectedPrefix?: string, | ||
| ): { prefix: string | null; path: string } { | ||
| let remaining = id; | ||
|
|
||
| // Peel off colon-separated segments until we find a valid prefix or run out | ||
|
|
@@ -38,11 +47,22 @@ function stripToValidPrefix(id: string): { prefix: string | null; path: string } | |
|
|
||
| const segment = remaining.slice(0, colonIdx); | ||
| if (VALID_PREFIXES.has(segment)) { | ||
| // Check for double valid prefix (e.g., "file:file:src/foo.ts") | ||
| // Collapse the outer prefix only when the next segment is either: | ||
| // - the SAME reserved word — a true duplicate ("file:file:src/foo.ts"), or | ||
| // - the node's expected prefix — a spurious project-name prefix that | ||
| // collides with a reserved word ("service:file:src/foo.ts" for a file | ||
| // node), which must resolve to the canonical "file:src/foo.ts". | ||
| // A different reserved word that is NOT the expected prefix | ||
| // ("endpoint:service:x" for an endpoint node) is a real path segment and | ||
| // must be preserved. | ||
| const rest = remaining.slice(colonIdx + 1); | ||
| const innerColonIdx = rest.indexOf(":"); | ||
| if (innerColonIdx > 0 && VALID_PREFIXES.has(rest.slice(0, innerColonIdx))) { | ||
| // Double-prefixed — skip the outer, recurse on inner | ||
| const innerSegment = innerColonIdx > 0 ? rest.slice(0, innerColonIdx) : ""; | ||
| if ( | ||
| innerColonIdx > 0 && | ||
| (innerSegment === segment || innerSegment === expectedPrefix) | ||
| ) { | ||
| // Skip the outer prefix, recurse on the inner one | ||
| remaining = rest; | ||
| continue; | ||
| } | ||
|
|
@@ -69,7 +89,7 @@ export function normalizeNodeId( | |
| if (!trimmed) return trimmed; | ||
|
|
||
| const expectedPrefix = TYPE_TO_PREFIX[node.type]; | ||
| const { prefix, path } = stripToValidPrefix(trimmed); | ||
| const { prefix, path } = stripToValidPrefix(trimmed, expectedPrefix); | ||
|
|
||
| if (prefix) { | ||
| // For step nodes with filePath, reconstruct as step:flowSlug:filePath:stepSlug. | ||
|
|
@@ -191,6 +211,40 @@ function inferTypeFromId(id: string): string { | |
| return "file"; | ||
| } | ||
|
|
||
| /** | ||
| * Best-effort repair of an edge endpoint that matches no node ID. | ||
| * | ||
| * Tries the prefix-inferred type first (preserving the common case), then | ||
| * each subsequent leading reserved-word segment as a candidate type. This | ||
| * recovers a reserved-word project prefix — e.g. an edge endpoint | ||
| * `service:file:src/foo.ts` pointing at the canonical node `file:src/foo.ts`, | ||
| * where `inferTypeFromId` would treat the spurious `service` as the type and | ||
| * fail to strip it, leaving the edge dangling. Returns the original id | ||
| * unchanged when nothing resolves to an existing node. | ||
| */ | ||
| function resolveEdgeEndpoint(id: string, validNodeIds: Set<string>): string { | ||
| const candidateTypes: string[] = [inferTypeFromId(id)]; | ||
|
|
||
| // Add each leading valid-prefix segment's type as an additional candidate, | ||
| // so a spurious outer reserved word can be skipped in favour of the real one. | ||
| let rest = id; | ||
| while (true) { | ||
| const colonIdx = rest.indexOf(":"); | ||
| if (colonIdx <= 0) break; | ||
| const segment = rest.slice(0, colonIdx); | ||
| if (!(segment in PREFIX_TO_TYPE)) break; | ||
| const type = PREFIX_TO_TYPE[segment]; | ||
| if (!candidateTypes.includes(type)) candidateTypes.push(type); | ||
| rest = rest.slice(colonIdx + 1); | ||
| } | ||
|
|
||
| for (const type of candidateTypes) { | ||
| const normalized = normalizeNodeId(id, { type }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an edge endpoint has more than one reserved prefix before the real node prefix, e.g. Useful? React with 👍 / 👎. |
||
| if (validNodeIds.has(normalized)) return normalized; | ||
| } | ||
| return id; | ||
| } | ||
|
|
||
| /** | ||
| * Normalizes a merged batch output: fixes node IDs and numeric complexity, | ||
| * rewrites edge references, deduplicates nodes and edges, and drops dangling edges. | ||
|
|
@@ -280,18 +334,14 @@ export function normalizeBatchOutput(data: { | |
| let newSource = idMap.get(oldSource) ?? oldSource; | ||
| let newTarget = idMap.get(oldTarget) ?? oldTarget; | ||
|
|
||
| // Fallback: if endpoint not found in idMap, normalize it directly | ||
| // (handles cross-variant malformed IDs between nodes and edges). | ||
| // Try the edge's implied type first (from prefix), then fall back to "file". | ||
| // Fallback: if an endpoint isn't found in idMap, repair it directly | ||
| // (handles cross-variant malformed IDs between nodes and edges, including | ||
| // reserved-word project prefixes that inferTypeFromId alone can't resolve). | ||
| if (!validNodeIds.has(newSource)) { | ||
| const inferredType = inferTypeFromId(newSource); | ||
| const normalized = normalizeNodeId(newSource, { type: inferredType }); | ||
| if (validNodeIds.has(normalized)) newSource = normalized; | ||
| newSource = resolveEdgeEndpoint(newSource, validNodeIds); | ||
| } | ||
| if (!validNodeIds.has(newTarget)) { | ||
| const inferredType = inferTypeFromId(newTarget); | ||
| const normalized = normalizeNodeId(newTarget, { type: inferredType }); | ||
| if (validNodeIds.has(normalized)) newTarget = normalized; | ||
| newTarget = resolveEdgeEndpoint(newTarget, validNodeIds); | ||
| } | ||
|
|
||
| if (newSource !== oldSource || newTarget !== oldTarget) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
normalizeBatchOutputfalls back to normalizing an edge endpoint that is not present inidMap, it infers the type from the malformed endpoint itself. For a project named like a reserved prefix, e.g. an edge endpointservice:file:src/foo.tspointing to an existing canonical nodefile:src/foo.ts,inferTypeFromIdsuppliesservice, so this check does not strip the outerservicesegment and the edge remains dangling and is dropped. This regresses the cross-variant edge repair path for the same reserved-word project-prefix case the node normalization now handles.Useful? React with 👍 / 👎.