mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-12 10:11:42 -04:00
* chore: update all dependencies to latest versions Update all outdated dependencies across the monorepo catalog and fix breaking changes from major version bumps. Notable updates: - ESLint 9 → 10 (fix custom rule API, disable new no-useless-assignment) - @stylistic/eslint-plugin 4 → 5 (auto-fixed indent changes) - @cyclonedx/cyclonedx-library 9 → 10 (adapt to removed SPDX API) - esbuild 0.25 → 0.27 - TypeScript 5.9.2 → 5.9.3 - Various @types packages, test utilities, and build tools Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update unified/remark/mdast imports for v11/v4 API changes Update imports in get-release-text for the new ESM named exports: - mdast-util-to-string: default → { toString } - unified: default → { unified } Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve typecheck errors from dependency updates - isexe v4: use named import { sync } instead of default export - remark-parse/remark-stringify v11: add vfile as packageExtension dependency so TypeScript can resolve type declarations - get-release-text: remove unused @ts-expect-error directives Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert runtime dependency major version bumps Revert major version bumps for runtime dependencies that are bundled into pnpm to fix test failures where pnpm add silently fails: - bin-links: keep ^5.0.0 (was ^6.0.0) - cli-truncate: keep ^4.0.0 (was ^5.2.0) - delay: keep ^6.0.0 (was ^7.0.0) - filenamify: keep ^6.0.0 (was ^7.0.1) - find-up: keep ^7.0.0 (was ^8.0.0) - isexe: keep 2.0.0 (was 4.0.0) - normalize-newline: keep 4.1.0 (was 5.0.0) - p-queue: keep ^8.1.0 (was ^9.1.0) - ps-list: keep ^8.1.1 (was ^9.0.0) - string-length: keep ^6.0.0 (was ^7.0.1) - symlink-dir: keep ^7.0.0 (was ^9.0.0) - terminal-link: keep ^4.0.0 (was ^5.0.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore runtime dependency major version bumps Re-apply all runtime dependency major version bumps that were previously reverted. All packages maintain their default exports except isexe v4 which needs named imports. Updated runtime deps: - bin-links: ^5.0.0 → ^6.0.0 - cli-truncate: ^4.0.0 → ^5.2.0 - delay: ^6.0.0 → ^7.0.0 - filenamify: ^6.0.0 → ^7.0.1 - find-up: ^7.0.0 → ^8.0.0 - isexe: 2.0.0 → 4.0.0 (fix: use named import { sync }) - normalize-newline: 4.1.0 → 5.0.0 - p-queue: ^8.1.0 → ^9.1.0 - ps-list: ^8.1.1 → ^9.0.0 - string-length: ^6.0.0 → ^7.0.1 - symlink-dir: ^7.0.0 → ^9.0.0 - terminal-link: ^4.0.0 → ^5.0.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert tempy to 3.0.0 to fix bundle hang tempy 3.2.0 pulls in temp-dir 3.0.0 which uses async fs.realpath() inside its module init. When bundled by esbuild into the __esm lazy init pattern, this causes a deadlock during module initialization, making the pnpm binary hang silently on startup. Keeping tempy at 3.0.0 which uses temp-dir 2.x (sync fs.realpathSync). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add comment explaining why tempy cannot be upgraded Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert nock to 13.3.4 for node-fetch compatibility nock 14 changed its HTTP interception mechanism in a way that doesn't properly intercept node-fetch requests, causing audit tests to hang waiting for responses that are never intercepted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add comment explaining why nock cannot be upgraded Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update symlink-dir imports for v10 ESM named exports symlink-dir v10 removed the default export and switched to named exports: { symlinkDir, symlinkDirSync }. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert @typescript/native-preview to working version Newer tsgo dev builds (>= 20260318) have a regression where @types/node cannot be resolved, breaking all node built-in types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: vulnerabilities * fix: align comment indentation in runLifecycleHook Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pin msgpackr to 1.11.8 for TypeScript 5.9 compatibility msgpackr 1.11.9 has broken type definitions that use Iterable/Iterator without required type arguments, causing compile errors with TS 5.9. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
247 lines
7.6 KiB
TypeScript
247 lines
7.6 KiB
TypeScript
import yaml from 'yaml'
|
|
|
|
export interface PatchDocumentOptions {
|
|
/**
|
|
* Updating aliases is inherently ambiguous since they're not a concept in
|
|
* JSON. The default is to unwrap and remove aliases since that's the most
|
|
* correct behavior.
|
|
*
|
|
* Aliases can be configured to "follow", which updates the anchor node.
|
|
* However, this can result in surprising behavior when values in the target
|
|
* JSON between the anchor and the alias don't match.
|
|
*
|
|
* @default 'unwrap'
|
|
*/
|
|
readonly aliases?: 'unwrap' | 'follow'
|
|
}
|
|
|
|
interface PatchContext extends PatchDocumentOptions {
|
|
readonly document: yaml.Document
|
|
readonly aliases: 'unwrap' | 'follow'
|
|
}
|
|
|
|
/**
|
|
* Recursively update a YAML document (in-place) to match the contents of a
|
|
* target value.
|
|
*
|
|
* Comments are preserved on a best-effort basis. There are several cases where
|
|
* ambiguity arises. See this package's README.md for details.
|
|
*/
|
|
export function patchDocument (document: yaml.Document, target: unknown, options?: PatchDocumentOptions): void {
|
|
// Documents with errors can't be stringified and have unpredictable ASTs.
|
|
if (document.errors.length > 0) {
|
|
throw new Error('Document with errors cannot be patched')
|
|
}
|
|
|
|
document.contents = patchNode(document.contents, target, {
|
|
document,
|
|
aliases: options?.aliases ?? 'unwrap',
|
|
})
|
|
}
|
|
|
|
function patchNode (node: yaml.Node | null | undefined, target: unknown, ctx: PatchContext): yaml.Node | null {
|
|
if (node == null) {
|
|
return ctx.document.createNode(target)
|
|
}
|
|
|
|
if (target == null) {
|
|
return null
|
|
}
|
|
|
|
if (yaml.isAlias(node)) {
|
|
return patchAlias(node, target, ctx)
|
|
}
|
|
|
|
if (yaml.isScalar(node)) {
|
|
return patchScalar(node, target, ctx)
|
|
}
|
|
|
|
if (yaml.isMap(node)) {
|
|
return patchMap(node, target, ctx)
|
|
}
|
|
|
|
if (yaml.isSeq(node)) {
|
|
return patchSeq(node, target, ctx)
|
|
}
|
|
|
|
const _never: never = node
|
|
throw new Error('Unrecognized yaml node: ' + String(node))
|
|
}
|
|
|
|
function patchAlias (alias: yaml.Alias, target: unknown, ctx: PatchContext): yaml.Node | null {
|
|
const resolved = alias.resolve(ctx.document)
|
|
|
|
// This should only happen if the document was corrupted after it was parsed.
|
|
// Unresolved aliases should fail at the parsing stage.
|
|
if (resolved == null) {
|
|
throw new Error('Failed to resolve yaml alias: ' + alias.source)
|
|
}
|
|
|
|
switch (ctx.aliases) {
|
|
case 'follow': {
|
|
// This can result in surprising behavior since the anchor node will end up
|
|
// with the contents of the last encountered alias. The default is to
|
|
// "unwrap" for this reason.
|
|
patchNode(resolved, target, ctx)
|
|
return alias
|
|
}
|
|
|
|
case 'unwrap': {
|
|
const copy = resolved.clone() as typeof resolved
|
|
copy.anchor = undefined
|
|
patchNode(copy, target, ctx)
|
|
return copy
|
|
}
|
|
}
|
|
}
|
|
|
|
function patchScalar (scalar: yaml.Scalar, target: unknown, ctx: PatchContext): yaml.Node {
|
|
if (scalar.value === target) {
|
|
return scalar
|
|
}
|
|
|
|
if (typeof target === 'boolean' || typeof target === 'string' || typeof target === 'number') {
|
|
scalar.value = target
|
|
return scalar
|
|
}
|
|
|
|
return ctx.document.createNode(target)
|
|
}
|
|
|
|
function patchMap (map: yaml.YAMLMap, target: unknown, ctx: PatchContext): yaml.Node | null {
|
|
if (!isRecord(target)) {
|
|
return ctx.document.createNode(target)
|
|
}
|
|
|
|
// Intentionally return null on empty maps as well. This recursively clears
|
|
// empty maps in the final document.
|
|
if (target == null || Object.keys(target).length === 0) {
|
|
return null
|
|
}
|
|
|
|
const mapKeyToExistingPair = new Map<string, yaml.Pair>()
|
|
|
|
for (const pair of map.items) {
|
|
// We can't update non-node types. Pairs should only contain values that are
|
|
// non-nodes if the yaml document was modified manually after parsing.
|
|
if (!yaml.isScalar(pair.key) || typeof pair.key.value !== 'string') {
|
|
throw new Error('Encountered unexpected non-node value: ' + String(pair.key))
|
|
}
|
|
|
|
mapKeyToExistingPair.set(pair.key.value, pair)
|
|
}
|
|
|
|
map.items = Object.entries(target)
|
|
.map(([key, value]) => {
|
|
const existingPair = mapKeyToExistingPair.get(key)
|
|
|
|
if (existingPair == null) {
|
|
return ctx.document.createPair(key, value)
|
|
}
|
|
|
|
if (!yaml.isNode(existingPair.value)) {
|
|
throw new Error('Encountered unexpected non-node value: ' + String(existingPair.value))
|
|
}
|
|
|
|
existingPair.value = patchNode(existingPair.value, value, ctx)
|
|
return existingPair
|
|
})
|
|
.filter((pair) => pair.value != null)
|
|
|
|
return map
|
|
}
|
|
|
|
function patchSeq (seq: yaml.YAMLSeq, target: unknown, ctx: PatchContext): yaml.Node {
|
|
if (!Array.isArray(target)) {
|
|
return ctx.document.createNode(target)
|
|
}
|
|
|
|
// Primitive lists can benefit from a more correct reconciliation process
|
|
// since it's possible to uniquely identify items.
|
|
//
|
|
// Reconciling lists with objects is more complex since we don't know which
|
|
// objects in the source list semantically correspond to the same object in
|
|
// the target list. This is the same problem that virtual DOM frameworks (e.g.
|
|
// React) have. We have to go by indexes in the complex case. If solving this
|
|
// problem becomes important in the future, it may be worth making callers to
|
|
// pass in a getKeyForNode() function.
|
|
return isPrimitiveList(target)
|
|
? patchSeqPrimitive(seq, target)
|
|
: patchSeqComplex(seq, target, ctx)
|
|
}
|
|
|
|
function patchSeqPrimitive (seq: yaml.YAMLSeq, target: Array<boolean | number | string | null | undefined>): yaml.Node {
|
|
// Keep track of existing nodes to reuse when building up the final list from
|
|
// the target list. These nodes will have comments attached to them, so it's
|
|
// important to reuse them when possible.
|
|
const valueToNodesMap = new Map<boolean | number | string, yaml.Scalar[]>()
|
|
|
|
for (const item of seq.items) {
|
|
if (item != null && !yaml.isNode(item)) {
|
|
throw new Error('Encountered unexpected non-node value: ' + String(item))
|
|
}
|
|
|
|
// We know all items in the target list are scalars. If there's a non-scalar
|
|
// in the source list, it needs to be removed. Skip over this item so it's
|
|
// not added to the final list.
|
|
if (!yaml.isScalar(item) || !isPrimitive(item.value) || item.value == null) {
|
|
continue
|
|
}
|
|
|
|
const nodeList = valueToNodesMap.get(item.value) ?? []
|
|
nodeList.push(item)
|
|
|
|
valueToNodesMap.set(item.value, nodeList)
|
|
}
|
|
|
|
seq.items = target.filter(item => item != null).map((item): yaml.Scalar => {
|
|
const existingNodesList = valueToNodesMap.get(item)
|
|
const firstExistingItem = existingNodesList?.shift()
|
|
|
|
// If the list is now empty as a result of removing the first item, clean up
|
|
// the map.
|
|
if (existingNodesList?.length === 0) {
|
|
valueToNodesMap.delete(item)
|
|
}
|
|
|
|
return firstExistingItem ?? new yaml.Scalar(item)
|
|
})
|
|
|
|
return seq
|
|
}
|
|
|
|
function patchSeqComplex (seq: yaml.YAMLSeq, target: unknown[], ctx: PatchContext): yaml.Node {
|
|
const nextItems: yaml.Node[] = []
|
|
|
|
for (let i = 0; i < Math.max(seq.items.length, target.length); i++) {
|
|
const existingItem = seq.items[i]
|
|
const targetItem = target[i]
|
|
|
|
if (existingItem != null && !yaml.isNode(existingItem)) {
|
|
throw new Error('Encountered unexpected non-node value: ' + String(existingItem))
|
|
}
|
|
|
|
const nextItem = patchNode(existingItem, targetItem, ctx)
|
|
if (nextItem == null) {
|
|
continue
|
|
}
|
|
|
|
nextItems.push(nextItem)
|
|
}
|
|
|
|
seq.items = nextItems
|
|
return seq
|
|
}
|
|
|
|
function isRecord (value: unknown): value is Record<string, unknown> {
|
|
return value != null && typeof value === 'object' && !Array.isArray(value)
|
|
}
|
|
|
|
function isPrimitiveList (arr: unknown[]) {
|
|
return arr.every(isPrimitive)
|
|
}
|
|
|
|
function isPrimitive (value: unknown) {
|
|
return value == null || typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number'
|
|
}
|