@vltpkg/graph
Advanced tools
+2
-0
@@ -74,2 +74,3 @@ import type { InspectOptions } from 'node:util'; | ||
| cpu?: string[] | string; | ||
| libc?: string[] | string; | ||
| } | undefined; | ||
@@ -98,2 +99,3 @@ buildState: "none" | "needed" | "built" | "failed"; | ||
| cpu?: string[] | string; | ||
| libc?: string[] | string; | ||
| } | undefined; | ||
@@ -100,0 +102,0 @@ buildState: "none" | "needed" | "built" | "failed"; |
+5
-1
@@ -83,4 +83,8 @@ import { error } from '@vltpkg/error-cause'; | ||
| this.edges.delete.add(fromEdge); | ||
| if (edge.to) | ||
| if (edge.to) { | ||
| this.edges.add.add(edge); | ||
| if (!edge.optional) { | ||
| this.optionalOnly = false; | ||
| } | ||
| } | ||
| } | ||
@@ -87,0 +91,0 @@ for (const edge of this.from.edges) { |
+10
-2
@@ -133,2 +133,8 @@ import { getId, joinDepIDTuple, splitExtra } from '@vltpkg/dep-id'; | ||
| this.manifests.set(wsNode.id, wsNode.manifest); | ||
| // set bins for workspace nodes so they can be linked | ||
| // when another importer depends on this workspace | ||
| /* c8 ignore next 3 - tested by integration tests */ | ||
| if (wsNode.manifest.bin) { | ||
| wsNode.bins = wsNode.manifest.bin; | ||
| } | ||
| } | ||
@@ -316,5 +322,5 @@ this.importers.add(wsNode); | ||
| if (manifest) { | ||
| const { bin, engines, os, cpu } = manifest; | ||
| const { bin, engines, os, cpu, libc } = manifest; | ||
| // add platform info if available | ||
| if (engines || os || cpu) { | ||
| if (engines || os || cpu || libc) { | ||
| const platform = {}; | ||
@@ -327,2 +333,4 @@ if (engines) | ||
| platform.cpu = cpu; | ||
| if (libc) | ||
| platform.libc = libc; | ||
| toNode.platform = platform; | ||
@@ -329,0 +337,0 @@ } |
@@ -9,3 +9,3 @@ import { joinDepIDTuple, joinExtra } from '@vltpkg/dep-id'; | ||
| import { extractNode } from "../reify/extract-node.js"; | ||
| import { endPeerPlacement, postPlacementPeerCheck, startPeerPlacement, } from "./peers.js"; | ||
| import { checkPeerEdgesCompatible, endPeerPlacement, forkPeerContext, postPlacementPeerCheck, startPeerPlacement, } from "./peers.js"; | ||
| /** | ||
@@ -71,2 +71,15 @@ * Only install devDeps for git dependencies and importers | ||
| const existingNode = graph.findResolution(spec, fromNode, queryModifier); | ||
| // Check if existing node's peer edges are compatible with new parent | ||
| const peerCompatResult = existingNode ? | ||
| checkPeerEdgesCompatible(existingNode, fromNode, peerContext, graph) | ||
| : { compatible: true }; | ||
| // Fork peer context if incompatible peer edges detected | ||
| let effectivePeerContext = peerContext; | ||
| /* c8 ignore start */ | ||
| if (!peerCompatResult.compatible && peerCompatResult.forkEntry) { | ||
| effectivePeerContext = forkPeerContext(graph, peerContext, [ | ||
| peerCompatResult.forkEntry, | ||
| ]); | ||
| } | ||
| /* c8 ignore stop */ | ||
| // defines what nodes are eligible to be reused | ||
@@ -78,7 +91,9 @@ const validExistingNode = existingNode && | ||
| (!peer || | ||
| // otherwise reusing peer deps only in case of a peerSetHash matche | ||
| existingNode.peerSetHash === fromNode.peerSetHash); | ||
| // otherwise reusing peer deps only in case of a peerSetHash match | ||
| existingNode.peerSetHash === fromNode.peerSetHash) && | ||
| // Check if existing node's peer edges are compatible with new parent | ||
| peerCompatResult.compatible; | ||
| /* c8 ignore stop */ | ||
| if (validExistingNode || | ||
| // importers are handled at the ./add-nodes.ts top-level | ||
| // importers are handled at the ./refresh-ideal-graph.ts top-level | ||
| // so we should just skip whenever we find one | ||
@@ -120,3 +135,3 @@ existingNode?.importer) { | ||
| depth, | ||
| peerContext, | ||
| peerContext: effectivePeerContext, | ||
| }; | ||
@@ -123,0 +138,0 @@ fetchTasks.push(fetchTask); |
@@ -0,3 +1,4 @@ | ||
| import { Spec } from '@vltpkg/spec'; | ||
| import type { PeerContext, PeerContextEntry, PeerContextEntryInput, ProcessPlacementResult } from './types.ts'; | ||
| import type { Spec, SpecOptions } from '@vltpkg/spec'; | ||
| import type { SpecOptions } from '@vltpkg/spec'; | ||
| import type { DependencySaveType, Manifest } from '@vltpkg/types'; | ||
@@ -9,2 +10,22 @@ import type { Monorepo } from '@vltpkg/workspaces'; | ||
| /** | ||
| * Result of checking if an existing node's peer edges are compatible | ||
| * with a new parent's context. The `forkEntry` property is optional | ||
| * and will only be present if the node's peer edges are incompatible. | ||
| */ | ||
| type PeerEdgeCompatResult = { | ||
| compatible: boolean; | ||
| /** When incompatible, entry to add to forked context */ | ||
| forkEntry?: { | ||
| spec: Spec; | ||
| target: Node; | ||
| type: DependencySaveType; | ||
| }; | ||
| }; | ||
| /** | ||
| * Check if an existing node's peer edges would still resolve to the same | ||
| * targets from a new parent's context. Returns incompatible info if any | ||
| * peer would resolve differently, meaning the node should NOT be reused. | ||
| */ | ||
| export declare const checkPeerEdgesCompatible: (existingNode: Node, fromNode: Node, peerContext: PeerContext, graph: Graph) => PeerEdgeCompatResult; | ||
| /** | ||
| * Retrieve a unique hash value for a given peer context set. | ||
@@ -66,5 +87,4 @@ */ | ||
| * values from the current peer context set. | ||
| * @param {PeerContext} currentContext The current peer context (may be forked from original) | ||
| */ | ||
| resolvePeerDeps: (currentContext: PeerContext) => void; | ||
| resolvePeerDeps: () => void; | ||
| }; | ||
@@ -79,2 +99,3 @@ /** | ||
| export declare const postPlacementPeerCheck: (graph: Graph, sortedLevelResults: ProcessPlacementResult[]) => void; | ||
| export {}; | ||
| //# sourceMappingURL=peers.d.ts.map |
+119
-43
@@ -5,5 +5,102 @@ // helpers for managing peer dependency resolution | ||
| import { satisfies } from '@vltpkg/satisfies'; | ||
| import { getDependencies } from "../dependencies.js"; | ||
| import { Spec } from '@vltpkg/spec'; | ||
| import { getDependencies, shorten } from "../dependencies.js"; | ||
| import { getOrderedDependencies } from "./get-ordered-dependencies.js"; | ||
| import { longDependencyTypes } from '@vltpkg/types'; | ||
| /** | ||
| * Check if an existing node's peer edges would still resolve to the same | ||
| * targets from a new parent's context. Returns incompatible info if any | ||
| * peer would resolve differently, meaning the node should NOT be reused. | ||
| */ | ||
| export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, graph) => { | ||
| // No peer deps means always compatible | ||
| if (!existingNode.manifest?.peerDependencies || | ||
| Object.keys(existingNode.manifest.peerDependencies).length === 0) | ||
| return { compatible: true }; | ||
| const peerDeps = existingNode.manifest.peerDependencies; | ||
| for (const [peerName, peerSpec] of Object.entries(peerDeps)) { | ||
| const existingEdge = existingNode.edgesOut.get(peerName); | ||
| if (!existingEdge?.to) | ||
| continue; // dangling peer, skip | ||
| // Check the peer context for what this parent's context would provide | ||
| const contextEntry = peerContext.get(peerName); | ||
| // If context has a different target for this peer, not compatible | ||
| if (contextEntry?.target && | ||
| contextEntry.target.id !== existingEdge.to.id) { | ||
| // Verify the context target would actually satisfy the peer spec | ||
| const spec = Spec.parse(peerName, peerSpec, { | ||
| ...graph.mainImporter.options, | ||
| registry: fromNode.registry, | ||
| }); | ||
| if (satisfies(contextEntry.target.id, spec, fromNode.location, fromNode.projectRoot, graph.monorepo)) { | ||
| return { | ||
| compatible: false, | ||
| forkEntry: { | ||
| spec, | ||
| target: contextEntry.target, | ||
| type: contextEntry.type, | ||
| }, | ||
| }; | ||
| } | ||
| } | ||
| // Also check parent's already-placed siblings | ||
| const siblingEdge = fromNode.edgesOut.get(peerName); | ||
| if (siblingEdge?.to && siblingEdge.to.id !== existingEdge.to.id) { | ||
| const spec = Spec.parse(peerName, peerSpec, { | ||
| ...graph.mainImporter.options, | ||
| registry: fromNode.registry, | ||
| }); | ||
| if (satisfies(siblingEdge.to.id, spec, fromNode.location, fromNode.projectRoot, graph.monorepo)) { | ||
| return { | ||
| compatible: false, | ||
| forkEntry: { | ||
| spec, | ||
| target: siblingEdge.to, | ||
| type: siblingEdge.type, | ||
| }, | ||
| }; | ||
| } | ||
| } | ||
| // Check parent's manifest for not-yet-placed siblings | ||
| // This handles the case where sibling hasn't been placed yet but will be | ||
| const parentManifest = fromNode.manifest; | ||
| if (parentManifest) { | ||
| for (const depType of longDependencyTypes) { | ||
| const depRecord = parentManifest[depType]; | ||
| if (depRecord?.[peerName]) { | ||
| // Parent declares this peer as a dependency | ||
| // Check if there's an existing graph node that would satisfy it differently | ||
| const parentSpec = Spec.parse(peerName, depRecord[peerName], { | ||
| ...graph.mainImporter.options, | ||
| registry: fromNode.registry, | ||
| }); | ||
| // Look for a node in the graph that satisfies parent's spec but differs from existing edge | ||
| for (const candidateNode of graph.nodes.values()) { | ||
| if (candidateNode.name === peerName && | ||
| candidateNode.id !== existingEdge.to.id && | ||
| satisfies(candidateNode.id, parentSpec, fromNode.location, fromNode.projectRoot, graph.monorepo)) { | ||
| // Also verify this candidate satisfies the peer spec | ||
| const peerSpecParsed = Spec.parse(peerName, peerSpec, { | ||
| ...graph.mainImporter.options, | ||
| registry: fromNode.registry, | ||
| }); | ||
| if (satisfies(candidateNode.id, peerSpecParsed, fromNode.location, fromNode.projectRoot, graph.monorepo)) { | ||
| return { | ||
| compatible: false, | ||
| forkEntry: { | ||
| spec: peerSpecParsed, | ||
| target: candidateNode, | ||
| type: shorten(depType), | ||
| }, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return { compatible: true }; | ||
| }; | ||
| /** | ||
| * Retrieve a unique hash value for a given peer context set. | ||
@@ -25,3 +122,4 @@ */ | ||
| if (entry.specs.size > 0) { | ||
| for (const s of entry.specs) { | ||
| for (const s_ of entry.specs) { | ||
| const s = s_.final; | ||
| if ( | ||
@@ -70,3 +168,3 @@ // only able to check range intersections for registry types | ||
| // validate if the provided spec is compatible with existing specs | ||
| if (incompatibleSpecs(spec, entry)) { | ||
| if (incompatibleSpecs(spec.final, entry)) { | ||
| return true; | ||
@@ -113,4 +211,5 @@ } | ||
| // case of adding sibling deps that conflicts with one another | ||
| if (incompatibleSpecs(spec, entry)) | ||
| if (incompatibleSpecs(spec.final, entry)) { | ||
| return true; | ||
| } | ||
| if (target && | ||
@@ -120,21 +219,13 @@ [...entry.specs].every(s => satisfies(target.id, s, fromNode.location, fromNode.projectRoot, monorepo))) { | ||
| target.version !== entry.target?.version) { | ||
| // Check if the existing target also satisfies the new spec. | ||
| // If it does, we should keep the existing target rather than | ||
| // switching to the new one. This preserves pinned versions | ||
| // and prevents unnecessary target changes. | ||
| const existingTargetSatisfiesNewSpec = entry.target && | ||
| satisfies(entry.target.id, spec, fromNode.location, fromNode.projectRoot, monorepo); | ||
| if (!existingTargetSatisfiesNewSpec) { | ||
| // Only update all dependents to point to the new target if | ||
| // the existing target doesn't satisfy the new spec | ||
| for (const dependents of entry.contextDependents) { | ||
| const edge = dependents.edgesOut.get(name); | ||
| if (edge?.to && edge.to !== target) { | ||
| edge.to.edgesIn.delete(edge); | ||
| edge.to = target; | ||
| target.edgesIn.add(edge); | ||
| } | ||
| // we have a compatible entry that has a new, compatible target | ||
| // so we need to update all dependents to point to the new target | ||
| for (const dependents of entry.contextDependents) { | ||
| const edge = dependents.edgesOut.get(name); | ||
| if (edge?.to && edge.to !== target) { | ||
| edge.to.edgesIn.delete(edge); | ||
| edge.to = target; | ||
| target.edgesIn.add(edge); | ||
| } | ||
| entry.target = target; | ||
| } | ||
| entry.target = target; | ||
| } | ||
@@ -162,8 +253,3 @@ // otherwise sets the value in case it was nullish | ||
| // to note that specs and contextDependents are new objects so that changes | ||
| // to those in the new context do not affect the previous one. | ||
| // IMPORTANT: We preserve the target from the parent context so that packages | ||
| // in the forked context can still resolve peer deps to the same version as | ||
| // the parent context. This fixes an issue where forked contexts would lose | ||
| // track of already-resolved peer dependencies (like a pinned typescript version) | ||
| // and resolve to different versions. | ||
| // to those in the new context do not affect the previous one | ||
| for (const [name, entry] of peerContext.entries()) { | ||
@@ -173,3 +259,3 @@ nextPeerContext.set(name, { | ||
| specs: new Set(entry.specs), | ||
| target: entry.target, | ||
| target: undefined, | ||
| type: entry.type, | ||
@@ -184,13 +270,6 @@ contextDependents: new Set(entry.contextDependents), | ||
| const name = target?.name /* c8 ignore next */ ?? spec.final.name; | ||
| // IMPORTANT: If the new entry has no target but the parent context had one, | ||
| // preserve the parent's target. This ensures that when a fork happens due to | ||
| // spec "incompatibility" (e.g., pinned version vs range), we don't lose the | ||
| // already-resolved target. The satisfies check in resolvePeerDeps will later | ||
| // verify if the preserved target actually satisfies the new spec. | ||
| const existingEntry = nextPeerContext.get(name); | ||
| const preservedTarget = !target && existingEntry?.target ? existingEntry.target : target; | ||
| const newEntry = { | ||
| active: true, | ||
| specs: new Set([spec]), | ||
| target: preservedTarget, | ||
| target, | ||
| type, | ||
@@ -286,5 +365,4 @@ contextDependents: dependent ? new Set([dependent]) : new Set(), | ||
| * values from the current peer context set. | ||
| * @param {PeerContext} currentContext The current peer context (may be forked from original) | ||
| */ | ||
| resolvePeerDeps: (currentContext) => { | ||
| resolvePeerDeps: () => { | ||
| // iterate on the set of peer dependencies of the current node | ||
@@ -314,4 +392,4 @@ // and try to resolve them from the existing peer context set, | ||
| // THEN: Try to retrieve an entry for that peer dep from | ||
| // the current peer context set (which may have been forked) | ||
| const entry = currentContext.get(spec.final.name); | ||
| // the current peer context set | ||
| const entry = peerContext.get(spec.final.name); | ||
| if (!node.edgesOut.has(spec.final.name) && | ||
@@ -387,5 +465,3 @@ entry?.target && | ||
| for (const childDep of sortedChildDeps) { | ||
| // Pass the current peerContext (which may have been forked) | ||
| // so resolvePeerDeps uses the correct context with preserved targets | ||
| childDep.updateContext.resolvePeerDeps(childDep.peerContext); | ||
| childDep.updateContext.resolvePeerDeps(); | ||
| childDep.deps = getOrderedDependencies(childDep.deps); | ||
@@ -392,0 +468,0 @@ } |
@@ -8,2 +8,8 @@ import { appendNodes } from "./append-nodes.js"; | ||
| const orderedImporters = [...graph.importers].sort((a, b) => { | ||
| // mainImporter always comes first | ||
| /* c8 ignore next */ | ||
| if (a === graph.mainImporter) | ||
| return -1; | ||
| if (b === graph.mainImporter) | ||
| return 1; | ||
| // sorts importers first by usage of peer deps | ||
@@ -10,0 +16,0 @@ const aIsPeer = (a.manifest?.peerDependencies && |
@@ -67,3 +67,3 @@ import type { PackageInfoClient } from '@vltpkg/package-info'; | ||
| }[] | undefined; | ||
| resolvePeerDeps: (currentContext: PeerContext) => void; | ||
| resolvePeerDeps: () => void; | ||
| }; | ||
@@ -70,0 +70,0 @@ }; |
@@ -11,2 +11,3 @@ import type { DepID } from '@vltpkg/dep-id'; | ||
| cpu?: string[] | string; | ||
| libc?: string[] | string; | ||
| }; | ||
@@ -13,0 +14,0 @@ /** |
+2
-0
@@ -120,2 +120,3 @@ import type { PathScurry } from 'path-scurry'; | ||
| cpu?: string[] | string; | ||
| libc?: string[] | string; | ||
| }; | ||
@@ -227,2 +228,3 @@ /** | ||
| cpu?: string[] | string; | ||
| libc?: string[] | string; | ||
| } | undefined; | ||
@@ -229,0 +231,0 @@ buildState: "none" | "needed" | "built" | "failed"; |
@@ -6,2 +6,3 @@ import type { EdgeLike, NodeLike } from '@vltpkg/types'; | ||
| nodes: NodeLike[]; | ||
| highlightSelection?: boolean; | ||
| }; | ||
@@ -17,3 +18,3 @@ /** | ||
| */ | ||
| export declare function mermaidOutput({ edges, importers, nodes, }: MermaidOutputGraph): string; | ||
| export declare function mermaidOutput(options: MermaidOutputGraph): string; | ||
| //# sourceMappingURL=mermaid-output.d.ts.map |
| import { Edge } from "../edge.js"; | ||
| import { Node } from "../node.js"; | ||
| let missingCount = 0; | ||
| const isSelected = (options, edge, node) => (!node || options.nodes.includes(node)) && | ||
| /* c8 ignore next */ (!edge || options.edges.includes(edge)); | ||
| /** | ||
@@ -61,11 +63,15 @@ * Generates a short identifier for a given index following the pattern: | ||
| */ | ||
| const nodeRef = (node, labeledNodes, depIdMapping) => { | ||
| const nodeRef = (node, labeledNodes, depIdMapping, options) => { | ||
| const shortId = depIdMapping.get(node.id) /* c8 ignore next - should not be possible */ ?? ''; | ||
| const selectedClass = (options.highlightSelection && | ||
| isSelected(options, undefined, node)) ? | ||
| ':::selected' | ||
| : ''; | ||
| if (labeledNodes.has(node.id)) { | ||
| return shortId; | ||
| return shortId + selectedClass; | ||
| } | ||
| labeledNodes.add(node.id); | ||
| return `${shortId}("${String(node).replaceAll('@', '#64;')}")`; | ||
| return `${shortId}("${String(node).replaceAll('@', '#64;')}")${selectedClass}`; | ||
| }; | ||
| function parseNode(seenNodes, labeledNodes, includedItems, depIdMapping, node, isImporter = false) { | ||
| function parseNode(seenNodes, labeledNodes, includedItems, depIdMapping, options, node, isImporter = false) { | ||
| if (seenNodes.has(node.id) || !includedItems.get(node)) { | ||
@@ -77,3 +83,5 @@ return ''; | ||
| // since they appear at the top of the graph. Non-importer nodes are labeled inline as part of edge definitions. | ||
| const nodeLabel = isImporter ? nodeRef(node, labeledNodes, depIdMapping) : ''; | ||
| const nodeLabel = isImporter ? | ||
| nodeRef(node, labeledNodes, depIdMapping, options) | ||
| : ''; | ||
| // Include both regular edges and workspace edges (if any) | ||
@@ -85,3 +93,3 @@ const allEdges = [ | ||
| const edges = allEdges | ||
| .map(e => parseEdge(seenNodes, labeledNodes, includedItems, depIdMapping, e)) | ||
| .map(e => parseEdge(seenNodes, labeledNodes, includedItems, depIdMapping, options, e)) | ||
| .filter(Boolean) | ||
@@ -96,3 +104,3 @@ .join('\n'); | ||
| } | ||
| function parseEdge(seenNodes, labeledNodes, includedItems, depIdMapping, edge) { | ||
| function parseEdge(seenNodes, labeledNodes, includedItems, depIdMapping, options, edge) { | ||
| if (!includedItems.get(edge)) { | ||
@@ -102,3 +110,3 @@ return ''; | ||
| const edgeType = edge.type === 'prod' ? '' : ` (${edge.type})`; | ||
| const edgeResult = nodeRef(edge.from, labeledNodes, depIdMapping) + | ||
| const edgeResult = nodeRef(edge.from, labeledNodes, depIdMapping, options) + | ||
| ` -->|"${String(edge.spec).replaceAll('@', '#64;')}${edgeType}"| `; | ||
@@ -111,4 +119,4 @@ const missingLabel = edge.type.endsWith('ptional') ? 'Missing Optional' : 'Missing'; | ||
| // it will use the short identifier instead of repeating the full label. | ||
| const toRef = nodeRef(edge.to, labeledNodes, depIdMapping); | ||
| const childEdges = parseNode(seenNodes, labeledNodes, includedItems, depIdMapping, edge.to); | ||
| const toRef = nodeRef(edge.to, labeledNodes, depIdMapping, options); | ||
| const childEdges = parseNode(seenNodes, labeledNodes, includedItems, depIdMapping, options, edge.to); | ||
| return edgeResult + toRef + (childEdges ? '\n' + childEdges : ''); | ||
@@ -119,3 +127,4 @@ } | ||
| */ | ||
| export function mermaidOutput({ edges, importers, nodes, }) { | ||
| export function mermaidOutput(options) { | ||
| const { edges, importers, nodes, highlightSelection } = options; | ||
| const seen = new Set(); | ||
@@ -165,8 +174,11 @@ const includedItems = new Map(); | ||
| const seenNodes = new Set(); | ||
| return ('flowchart TD\n' + | ||
| [...importers] | ||
| .map(i => parseNode(seenNodes, labeledNodes, includedItems, depIdMapping, i, true)) | ||
| .filter(Boolean) | ||
| .join('\n')); | ||
| const graphOutput = [...importers] | ||
| .map(i => parseNode(seenNodes, labeledNodes, includedItems, depIdMapping, options, i, true)) | ||
| .filter(Boolean) | ||
| .join('\n'); | ||
| const styleDefinition = highlightSelection ? | ||
| '\nclassDef selected fill:gold,color:#242424' | ||
| : ''; | ||
| return 'flowchart TD\n' + graphOutput + styleDefinition; | ||
| } | ||
| //# sourceMappingURL=mermaid-output.js.map |
+22
-22
| { | ||
| "name": "@vltpkg/graph", | ||
| "description": "A library that helps understanding & expressing what happens on an install", | ||
| "version": "1.0.0-rc.13", | ||
| "version": "1.0.0-rc.14", | ||
| "repository": { | ||
@@ -14,22 +14,22 @@ "type": "git", | ||
| "promise-call-limit": "^3.0.2", | ||
| "@vltpkg/cmd-shim": "1.0.0-rc.13", | ||
| "@vltpkg/dep-id": "1.0.0-rc.13", | ||
| "@vltpkg/error-cause": "1.0.0-rc.13", | ||
| "@vltpkg/fast-split": "1.0.0-rc.13", | ||
| "@vltpkg/dss-breadcrumb": "1.0.0-rc.13", | ||
| "@vltpkg/graph-run": "1.0.0-rc.13", | ||
| "@vltpkg/output": "1.0.0-rc.13", | ||
| "@vltpkg/init": "1.0.0-rc.13", | ||
| "@vltpkg/package-info": "1.0.0-rc.13", | ||
| "@vltpkg/package-json": "1.0.0-rc.13", | ||
| "@vltpkg/pick-manifest": "1.0.0-rc.13", | ||
| "@vltpkg/rollback-remove": "1.0.0-rc.13", | ||
| "@vltpkg/query": "1.0.0-rc.13", | ||
| "@vltpkg/run": "1.0.0-rc.13", | ||
| "@vltpkg/satisfies": "1.0.0-rc.13", | ||
| "@vltpkg/security-archive": "1.0.0-rc.13", | ||
| "@vltpkg/spec": "1.0.0-rc.13", | ||
| "@vltpkg/types": "1.0.0-rc.13", | ||
| "@vltpkg/vlt-json": "1.0.0-rc.13", | ||
| "@vltpkg/workspaces": "1.0.0-rc.13" | ||
| "@vltpkg/cmd-shim": "1.0.0-rc.14", | ||
| "@vltpkg/dep-id": "1.0.0-rc.14", | ||
| "@vltpkg/dss-breadcrumb": "1.0.0-rc.14", | ||
| "@vltpkg/error-cause": "1.0.0-rc.14", | ||
| "@vltpkg/graph-run": "1.0.0-rc.14", | ||
| "@vltpkg/init": "1.0.0-rc.14", | ||
| "@vltpkg/fast-split": "1.0.0-rc.14", | ||
| "@vltpkg/package-info": "1.0.0-rc.14", | ||
| "@vltpkg/output": "1.0.0-rc.14", | ||
| "@vltpkg/package-json": "1.0.0-rc.14", | ||
| "@vltpkg/pick-manifest": "1.0.0-rc.14", | ||
| "@vltpkg/query": "1.0.0-rc.14", | ||
| "@vltpkg/rollback-remove": "1.0.0-rc.14", | ||
| "@vltpkg/run": "1.0.0-rc.14", | ||
| "@vltpkg/satisfies": "1.0.0-rc.14", | ||
| "@vltpkg/security-archive": "1.0.0-rc.14", | ||
| "@vltpkg/types": "1.0.0-rc.14", | ||
| "@vltpkg/spec": "1.0.0-rc.14", | ||
| "@vltpkg/workspaces": "1.0.0-rc.14", | ||
| "@vltpkg/vlt-json": "1.0.0-rc.14" | ||
| }, | ||
@@ -46,3 +46,3 @@ "devDependencies": { | ||
| "typescript-eslint": "^8.49.0", | ||
| "@vltpkg/vlt-json": "1.0.0-rc.13" | ||
| "@vltpkg/vlt-json": "1.0.0-rc.14" | ||
| }, | ||
@@ -49,0 +49,0 @@ "license": "BSD-2-Clause-Patent", |
+97
-22
@@ -44,8 +44,9 @@  | ||
| - Modifiers: Configuration for selectively altering dependency | ||
| resolution; Ideal/Actual builders support skipping node loads when | ||
| modifiers change. | ||
| resolution via DSS queries in `vlt.json`. | ||
| - Peer Contexts: Isolation mechanism for peer dependencies that allows | ||
| multiple versions of the same package when peer requirements differ. | ||
| ## API | ||
| ### `actual.load({ projectRoot: string }): Graph` | ||
| ### `actual.load(options): Graph` | ||
@@ -56,30 +57,55 @@ Recursively loads the `node_modules` folder found at `projectRoot` in | ||
| ### `async ideal.build({ projectRoot: string }): Promise<Graph>` | ||
| ### `ideal.build(options): Promise<Graph>` | ||
| This method returns a new `Graph` object, reading from the | ||
| `package.json` file located at `projectRoot` dir and building up the | ||
| graph representation of nodes and edges from the files read from the | ||
| local file system. | ||
| Builds the ideal dependency graph by loading from lockfile (preferred) | ||
| or actual graph, then expanding dependencies by fetching manifests. | ||
| Requires `packageInfo` and `remover` in addition to standard options. | ||
| ### `lockfile.load({ mainManifest: Manifest, projectRoot: string }): Graph` | ||
| ### `lockfile.load(options): Graph` | ||
| Loads the lockfile file found at `projectRoot` and returns the graph. | ||
| ### `reify(options): Promise<Diff>` | ||
| ### `lockfile.save(options): void` | ||
| Saves the graph to `vlt-lock.json`. | ||
| ### `reify(options): Promise<ReifyResult>` | ||
| Computes a `Diff` between the Actual and Ideal graphs and applies the | ||
| minimal filesystem changes (creating/deleting links, writing | ||
| lockfiles, hoisting, lifecycle scripts) to make the on-disk install | ||
| match the Ideal graph. | ||
| match the Ideal graph. Returns `{ diff, buildQueue }`. | ||
| ### `install(options, add?): Promise<{ graph, diff, buildQueue }>` | ||
| High-level install orchestration that handles graph building, reify, | ||
| and lockfile management. Supports `--frozen-lockfile`, | ||
| `--clean-install`, and `--lockfile-only` modes. | ||
| ### `mermaidOutput(graph): string` | ||
| Generates Mermaid flowchart syntax from graph data. | ||
| ### `humanReadableOutput(graph, options): string` | ||
| Generates ASCII tree output with optional colors. Used in `vlt ls`. | ||
| ### `jsonOutput(graph): JSONOutputItem[]` | ||
| Returns array of `{name, fromID, spec, type, to, overridden}` items. | ||
| ## Usage | ||
| Here's a quick example of how to use the `@vltpkg/graph.ideal.build` | ||
| method to build a graph representation of the install defined at the | ||
| `projectRoot` directory. | ||
| ### High-Level Install | ||
| ``` | ||
| import { ideal } from '@vltpkg/graph' | ||
| ```ts | ||
| import { install } from '@vltpkg/graph' | ||
| const graph = await ideal.build({ projectRoot: process.cwd() }) | ||
| const { graph, diff, buildQueue } = await install({ | ||
| projectRoot: process.cwd(), | ||
| packageInfo, | ||
| packageJson, | ||
| scurry, | ||
| allowScripts: '*', | ||
| }) | ||
| ``` | ||
@@ -91,3 +117,6 @@ | ||
| import { actual, ideal, reify } from '@vltpkg/graph' | ||
| import { RollbackRemove } from '@vltpkg/rollback-remove' | ||
| const remover = new RollbackRemove() | ||
| // Load current on-disk state | ||
@@ -98,2 +127,3 @@ const from = actual.load({ | ||
| scurry, | ||
| loadManifests: true, | ||
| }) | ||
@@ -107,6 +137,7 @@ | ||
| scurry, | ||
| remover, | ||
| }) | ||
| // Apply minimal changes to match Ideal | ||
| await reify({ | ||
| const { diff, buildQueue } = await reify({ | ||
| graph: to, | ||
@@ -117,2 +148,4 @@ actual: from, | ||
| scurry, | ||
| remover, | ||
| allowScripts: '*', | ||
| }) | ||
@@ -127,13 +160,46 @@ ``` | ||
| // Load virtual graph from vlt-lock.json | ||
| const g = lockfile.load({ | ||
| const graph = lockfile.load({ | ||
| projectRoot, | ||
| mainManifest, | ||
| packageJson, | ||
| scurry, | ||
| }) | ||
| // Save both lockfile formats | ||
| lockfile.save({ graph: g, projectRoot, packageJson, scurry }) | ||
| // Save to vlt-lock.json | ||
| lockfile.save({ graph }) | ||
| ``` | ||
| ### Graph Visualization | ||
| ```ts | ||
| import { | ||
| mermaidOutput, | ||
| humanReadableOutput, | ||
| jsonOutput, | ||
| } from '@vltpkg/graph' | ||
| // Mermaid flowchart (for docs, dashboards) | ||
| const mermaid = mermaidOutput({ | ||
| edges: [...graph.edges], | ||
| nodes: [...graph.nodes.values()], | ||
| importers: graph.importers, | ||
| }) | ||
| // ASCII tree with colors (used in `vlt ls`) | ||
| const tree = humanReadableOutput( | ||
| { | ||
| edges: [...graph.edges], | ||
| nodes: [...graph.nodes.values()], | ||
| importers: graph.importers, | ||
| }, | ||
| { colors: true }, | ||
| ) | ||
| // JSON array of dependency items | ||
| const json = jsonOutput({ | ||
| edges: [...graph.edges], | ||
| nodes: [...graph.nodes.values()], | ||
| importers: graph.importers, | ||
| }) | ||
| ``` | ||
| ## Architecture | ||
@@ -147,2 +213,3 @@ | ||
| - Hidden lockfile: `node_modules/.vlt-lock.json` for faster loads | ||
| - 📖 [Lockfile README](./src/lockfile/README.md) | ||
@@ -161,2 +228,3 @@ - Actual Graphs (filesystem-based) | ||
| existing nodes that satisfy specs | ||
| - 📖 [Ideal README](./src/ideal/README.md) | ||
@@ -166,2 +234,5 @@ Finally, `src/diff.ts` computes changes and `src/reify/` applies them | ||
| - 📖 [Reify README](./src/reify/README.md) | ||
| - 📖 [Architecture Guide](./ARCHITECTURE.md) | ||
| ## Related Workspaces | ||
@@ -173,2 +244,3 @@ | ||
| - `@vltpkg/semver`: Semantic version parsing/comparison | ||
| - `@vltpkg/satisfies`: Check if a DepID satisfies a Spec | ||
| - `@vltpkg/package-info`: Fetch remote manifests and artifacts | ||
@@ -178,2 +250,5 @@ (registry, git, tarball) | ||
| - `@vltpkg/workspaces`: Monorepo workspace discovery and grouping | ||
| - `@vltpkg/rollback-remove`: Safe file removal with rollback | ||
| capability | ||
| - `@vltpkg/vlt-json`: Load `vlt.json` configuration (modifiers, etc.) | ||
@@ -180,0 +255,0 @@ ## References |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
836850
2.24%7836
1.93%250
42.86%18
-14.29%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated