ohrisk 0.127.1 → 0.128.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.128.0 - 2026-06-20
4
+
5
+ ### Added
6
+
7
+ - Yarn Berry `yarn.lock` files are now parsed alongside Yarn classic lockfiles,
8
+ including `npm:` protocol descriptors, patched npm packages, and workspace
9
+ package roots.
10
+
11
+ ### Fixed
12
+
13
+ - Real-world Yarn workspace scans now ignore local `workspace:` packages as npm
14
+ package evidence while still scanning each workspace package manifest as a
15
+ dependency root.
16
+
3
17
  ## 0.127.1 - 2026-06-20
4
18
 
5
19
  ### Fixed
package/README.md CHANGED
@@ -62,18 +62,18 @@ Ohrisk is distributed as an npm package, and the packaged CLI runs on Node.js
62
62
  `>=20.0.0`. Bun is used for Ohrisk development, tests, and packaging, but users
63
63
  do not need Bun installed to run the published CLI.
64
64
 
65
- Ohrisk scans Bun, npm package-lock/shrinkwrap, pnpm, Deno npm, and Yarn v1
65
+ Ohrisk scans Bun, npm package-lock/shrinkwrap, pnpm, Deno npm, and Yarn
66
66
  lockfiles regardless of which package manager you use to install the CLI.
67
67
 
68
68
  ## Current Scope
69
69
 
70
70
  The current implementation is the first npm-style vertical slice:
71
71
 
72
- - Bun `bun.lock`, npm `package-lock.json`, npm `npm-shrinkwrap.json`, pnpm `pnpm-lock.yaml`, Deno `deno.lock`, and Yarn v1 `yarn.lock` project discovery
72
+ - Bun `bun.lock`, npm `package-lock.json`, npm `npm-shrinkwrap.json`, pnpm `pnpm-lock.yaml`, Deno `deno.lock`, and Yarn classic/Berry `yarn.lock` project discovery
73
73
  - Node-compatible packaged CLI entrypoint for npm, pnpm, Yarn, npx, pnpm dlx, and yarn dlx users
74
74
  - explicit lockfile selection with `--lockfile <path>` for projects that contain more than one supported lockfile
75
75
  - direct and transitive dependency graph extraction
76
- - Bun, npm, pnpm, and Yarn v1 workspace projects are scanned from every workspace/importer package root
76
+ - Bun, npm, pnpm, and Yarn classic/Berry workspace projects are scanned from every workspace/importer package root
77
77
  - Deno `deno.lock` projects are scanned for npm package dependencies recorded in `npm:` specifiers; remote URL imports and JSR packages are not scanned yet
78
78
  - npm alias dependency resolution, including pnpm alias package keys, with alias context preserved in dependency paths
79
79
  - production, development, optional, and peer dependency classification
@@ -159,7 +159,7 @@ Supported lockfiles:
159
159
  - `npm-shrinkwrap.json` with the same package-lock parser support
160
160
  - `pnpm-lock.yaml` with `importers`, `packages`, and `snapshots` sections
161
161
  - `deno.lock` npm package entries from Deno v3/v4-style lockfiles
162
- - Yarn v1 `yarn.lock` with root and workspace dependency sets from `package.json` manifests
162
+ - Yarn classic/Berry `yarn.lock` with root and workspace dependency sets from `package.json` manifests
163
163
 
164
164
  Select a specific lockfile when a project contains more than one supported lockfile:
165
165
 
package/dist/cli.js CHANGED
@@ -15175,7 +15175,7 @@ function parseDiffArgs(argv) {
15175
15175
  }
15176
15176
 
15177
15177
  // src/cli/version.ts
15178
- var OHRISK_VERSION = "0.127.1";
15178
+ var OHRISK_VERSION = "0.128.0";
15179
15179
 
15180
15180
  // src/diff/compare.ts
15181
15181
  function diffRiskFindings(input) {
@@ -17266,6 +17266,17 @@ function resolveNpmDependencyReference(requestedName, range) {
17266
17266
  aliased: alias.name !== requestedName
17267
17267
  };
17268
17268
  }
17269
+ if (range.startsWith("npm:")) {
17270
+ const bareRange = range.slice("npm:".length);
17271
+ if (bareRange !== "") {
17272
+ return {
17273
+ requestedName,
17274
+ lookupName: requestedName,
17275
+ lookupRange: bareRange,
17276
+ aliased: false
17277
+ };
17278
+ }
17279
+ }
17269
17280
  return {
17270
17281
  requestedName,
17271
17282
  lookupName: requestedName,
@@ -19141,17 +19152,20 @@ function readPackageName2(packageJson) {
19141
19152
  return typeof packageJson.name === "string" && packageJson.name !== "" ? packageJson.name : undefined;
19142
19153
  }
19143
19154
  function parseLockfile(input, lockfilePath) {
19155
+ if (hasMergeConflictMarkers(input)) {
19156
+ return err(createError({
19157
+ code: "YARN_LOCK_PARSE_FAILED",
19158
+ category: "unsupported_input",
19159
+ message: "Failed to parse yarn.lock because it contains unresolved merge conflicts.",
19160
+ details: {
19161
+ lockfilePath
19162
+ }
19163
+ }));
19164
+ }
19165
+ if (isYarnBerryLockfile(input)) {
19166
+ return parseBerryLockfile(input, lockfilePath);
19167
+ }
19144
19168
  try {
19145
- if (hasMergeConflictMarkers(input)) {
19146
- return err(createError({
19147
- code: "YARN_LOCK_PARSE_FAILED",
19148
- category: "unsupported_input",
19149
- message: "Failed to parse yarn.lock because it contains unresolved merge conflicts.",
19150
- details: {
19151
- lockfilePath
19152
- }
19153
- }));
19154
- }
19155
19169
  const parsed = yarnLockfile.parse(input);
19156
19170
  if (parsed.type === "conflict") {
19157
19171
  return err(createError({
@@ -19163,12 +19177,47 @@ function parseLockfile(input, lockfilePath) {
19163
19177
  }
19164
19178
  }));
19165
19179
  }
19166
- return ok(parsed.object);
19180
+ return ok({
19181
+ format: "classic",
19182
+ entries: parsed.object
19183
+ });
19184
+ } catch (cause) {
19185
+ return err(createError({
19186
+ code: "YARN_LOCK_PARSE_FAILED",
19187
+ category: "unsupported_input",
19188
+ message: "Failed to parse yarn.lock. Ohrisk expects a Yarn classic or Berry lockfile.",
19189
+ details: {
19190
+ lockfilePath,
19191
+ cause: cause instanceof Error ? cause.message : String(cause)
19192
+ }
19193
+ }));
19194
+ }
19195
+ }
19196
+ function isYarnBerryLockfile(input) {
19197
+ return /^__metadata:\s*$/m.test(input);
19198
+ }
19199
+ function parseBerryLockfile(input, lockfilePath) {
19200
+ try {
19201
+ const parsed = $parse(input);
19202
+ if (!isObjectRecord8(parsed)) {
19203
+ throw new Error("Expected a YAML mapping at the document root.");
19204
+ }
19205
+ const entries = {};
19206
+ for (const [key, value] of Object.entries(parsed)) {
19207
+ if (key === "__metadata" || !isObjectRecord8(value)) {
19208
+ continue;
19209
+ }
19210
+ entries[key] = value;
19211
+ }
19212
+ return ok({
19213
+ format: "berry",
19214
+ entries
19215
+ });
19167
19216
  } catch (cause) {
19168
19217
  return err(createError({
19169
19218
  code: "YARN_LOCK_PARSE_FAILED",
19170
19219
  category: "unsupported_input",
19171
- message: "Failed to parse yarn.lock. Ohrisk currently supports Yarn v1 lockfiles.",
19220
+ message: "Failed to parse Yarn Berry lockfile.",
19172
19221
  details: {
19173
19222
  lockfilePath,
19174
19223
  cause: cause instanceof Error ? cause.message : String(cause)
@@ -19181,12 +19230,12 @@ function hasMergeConflictMarkers(input) {
19181
19230
  }
19182
19231
  function parsePackageRecords4(lockfile) {
19183
19232
  const records = [];
19184
- for (const [key, entry] of Object.entries(lockfile)) {
19233
+ for (const [key, entry] of Object.entries(lockfile.entries)) {
19185
19234
  if (typeof entry.version !== "string" || entry.version === "") {
19186
19235
  continue;
19187
19236
  }
19188
19237
  const descriptors = splitDescriptorKey(key);
19189
- const identity2 = descriptors.map(parseDescriptor).find(Boolean);
19238
+ const identity2 = lockfile.format === "berry" ? readBerryPackageIdentity({ key, entry }) : descriptors.map(parseDescriptor).find(Boolean);
19190
19239
  if (!identity2) {
19191
19240
  continue;
19192
19241
  }
@@ -19194,7 +19243,10 @@ function parsePackageRecords4(lockfile) {
19194
19243
  const integrity = typeof entry.integrity === "string" && entry.integrity !== "" ? entry.integrity : undefined;
19195
19244
  records.push({
19196
19245
  key,
19197
- descriptors,
19246
+ descriptors: descriptorIndexKeys({
19247
+ descriptors,
19248
+ format: lockfile.format
19249
+ }),
19198
19250
  name: identity2.name,
19199
19251
  version: entry.version,
19200
19252
  id: `${identity2.name}@${entry.version}`,
@@ -19208,8 +19260,26 @@ function parsePackageRecords4(lockfile) {
19208
19260
  function splitDescriptorKey(key) {
19209
19261
  return key.split(/,\s*/).map((descriptor) => descriptor.trim()).filter(Boolean);
19210
19262
  }
19263
+ function descriptorIndexKeys(input) {
19264
+ const keys = new Set;
19265
+ for (const descriptor of input.descriptors) {
19266
+ const unquoted = unquoteDescriptor(descriptor);
19267
+ keys.add(unquoted);
19268
+ const parsed = input.format === "berry" ? parseBerryDescriptor(unquoted) : parseDescriptor(unquoted);
19269
+ if (!parsed) {
19270
+ continue;
19271
+ }
19272
+ keys.add(`${parsed.name}@${parsed.range}`);
19273
+ keys.add(`${parsed.name}@npm:${parsed.range}`);
19274
+ }
19275
+ return [...keys];
19276
+ }
19211
19277
  function parseDescriptor(descriptor) {
19212
- const unquoted = descriptor.replace(/^"|"$/g, "");
19278
+ const unquoted = unquoteDescriptor(descriptor);
19279
+ const berryDescriptor = parseBerryDescriptor(unquoted);
19280
+ if (berryDescriptor) {
19281
+ return berryDescriptor;
19282
+ }
19213
19283
  const aliasMarker = "@npm:";
19214
19284
  const aliasIndex = unquoted.indexOf(aliasMarker);
19215
19285
  if (aliasIndex > 0) {
@@ -19225,6 +19295,89 @@ function parseDescriptor(descriptor) {
19225
19295
  }
19226
19296
  return { name: parsed.name, range: parsed.reference };
19227
19297
  }
19298
+ function parseBerryDescriptor(descriptor) {
19299
+ const npmLocator = parseBerryNpmLocator(descriptor);
19300
+ if (npmLocator) {
19301
+ const alias = parseNpmPackageReference(`npm:${npmLocator.reference}`);
19302
+ return alias ? { name: alias.name, range: alias.reference } : { name: npmLocator.name, range: npmLocator.reference };
19303
+ }
19304
+ const patchLocator = parseBerryPatchLocator(descriptor);
19305
+ if (patchLocator) {
19306
+ return patchLocator;
19307
+ }
19308
+ return;
19309
+ }
19310
+ function readBerryPackageIdentity(input) {
19311
+ const resolution = typeof input.entry.resolution === "string" ? input.entry.resolution : undefined;
19312
+ const parsedResolution = resolution ? parseBerryResolution(resolution) : undefined;
19313
+ if (parsedResolution) {
19314
+ return parsedResolution;
19315
+ }
19316
+ const descriptorIdentity = splitDescriptorKey(input.key).map((descriptor) => parseBerryDescriptor(unquoteDescriptor(descriptor))).find(Boolean);
19317
+ return descriptorIdentity ? { name: descriptorIdentity.name, version: descriptorIdentity.range } : undefined;
19318
+ }
19319
+ function parseBerryResolution(value) {
19320
+ const unquoted = unquoteDescriptor(value);
19321
+ const npmLocator = parseBerryNpmLocator(unquoted);
19322
+ if (npmLocator) {
19323
+ return {
19324
+ name: npmLocator.name,
19325
+ version: npmLocator.reference
19326
+ };
19327
+ }
19328
+ const patchLocator = parseBerryPatchLocator(unquoted);
19329
+ if (patchLocator) {
19330
+ return {
19331
+ name: patchLocator.name,
19332
+ version: patchLocator.range
19333
+ };
19334
+ }
19335
+ return;
19336
+ }
19337
+ function parseBerryNpmLocator(value) {
19338
+ const marker = "@npm:";
19339
+ const index = value.indexOf(marker);
19340
+ if (index <= 0) {
19341
+ return;
19342
+ }
19343
+ const name = value.slice(0, index);
19344
+ const reference = value.slice(index + marker.length);
19345
+ if (!isValidNpmPackageName(name) || reference === "") {
19346
+ return;
19347
+ }
19348
+ return { name, reference };
19349
+ }
19350
+ function parseBerryPatchLocator(value) {
19351
+ const marker = "@patch:";
19352
+ const index = value.indexOf(marker);
19353
+ if (index <= 0) {
19354
+ return;
19355
+ }
19356
+ const patchedLocator = value.slice(index + marker.length).split("#")[0]?.split("::")[0];
19357
+ if (!patchedLocator) {
19358
+ return;
19359
+ }
19360
+ const decodedLocator = safeDecodeURIComponent(patchedLocator);
19361
+ const npmLocator = parseBerryNpmLocator(decodedLocator);
19362
+ if (!npmLocator) {
19363
+ return;
19364
+ }
19365
+ const alias = parseNpmPackageReference(`npm:${npmLocator.reference}`);
19366
+ return alias ? { name: alias.name, range: alias.reference } : { name: npmLocator.name, range: npmLocator.reference };
19367
+ }
19368
+ function safeDecodeURIComponent(value) {
19369
+ try {
19370
+ return decodeURIComponent(value);
19371
+ } catch {
19372
+ return value;
19373
+ }
19374
+ }
19375
+ function unquoteDescriptor(value) {
19376
+ return value.replace(/^"|"$/g, "");
19377
+ }
19378
+ function isValidNpmPackageName(value) {
19379
+ return /^(?:@[^/]+\/)?[^/@][^@]*$/.test(value);
19380
+ }
19228
19381
  function collectRootDependencies5(packageJson) {
19229
19382
  return [
19230
19383
  ...dependencyEntries4(packageJson.dependencies, "production"),
@@ -19277,10 +19430,28 @@ function indexPackagesByName3(records) {
19277
19430
  return index;
19278
19431
  }
19279
19432
  function resolvePackageRecord4(input) {
19280
- const descriptor = `${input.name}@${input.range}`;
19281
19433
  const reference = resolveNpmDependencyReference(input.name, input.range);
19282
19434
  const candidates = input.nameIndex.get(reference.lookupName) ?? [];
19283
- return input.descriptorIndex.get(descriptor) ?? (candidates.length === 1 ? candidates[0] : undefined) ?? candidates.find((candidate) => candidate.version === reference.lookupRange) ?? undefined;
19435
+ return dependencyDescriptorCandidates({
19436
+ name: input.name,
19437
+ range: input.range,
19438
+ reference
19439
+ }).map((descriptor) => input.descriptorIndex.get(descriptor)).find((record) => record !== undefined) ?? (candidates.length === 1 ? candidates[0] : undefined) ?? candidates.find((candidate) => candidate.version === reference.lookupRange) ?? undefined;
19440
+ }
19441
+ function dependencyDescriptorCandidates(input) {
19442
+ const candidates = new Set;
19443
+ candidates.add(`${input.name}@${input.range}`);
19444
+ candidates.add(`${input.reference.lookupName}@${input.reference.lookupRange}`);
19445
+ candidates.add(`${input.name}@npm:${input.range}`);
19446
+ candidates.add(`${input.reference.lookupName}@npm:${input.reference.lookupRange}`);
19447
+ if (input.range.startsWith("npm:")) {
19448
+ const bareRange = input.range.slice("npm:".length);
19449
+ candidates.add(`${input.name}@${bareRange}`);
19450
+ candidates.add(`${input.name}@npm:${bareRange}`);
19451
+ candidates.add(`${input.reference.lookupName}@${bareRange}`);
19452
+ candidates.add(`${input.reference.lookupName}@npm:${bareRange}`);
19453
+ }
19454
+ return [...candidates].filter((candidate) => !candidate.endsWith("@"));
19284
19455
  }
19285
19456
  function walkDependency5(input) {
19286
19457
  if (input.seen.has(input.record.key)) {
@@ -21200,7 +21371,7 @@ var KNOWN_PROJECT_MANIFESTS = [
21200
21371
  "deno.json",
21201
21372
  "deno.jsonc"
21202
21373
  ];
21203
- var SUPPORTED_LOCKFILE_MESSAGE = "Ohrisk currently supports bun.lock, package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, deno.lock, and Yarn v1 yarn.lock.";
21374
+ var SUPPORTED_LOCKFILE_MESSAGE = "Ohrisk currently supports bun.lock, package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, deno.lock, and Yarn classic/Berry yarn.lock.";
21204
21375
  function discoverProject(options = {}) {
21205
21376
  const startDir = path12.resolve(options.cwd ?? process.cwd());
21206
21377
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohrisk",
3
- "version": "0.127.1",
3
+ "version": "0.128.0",
4
4
  "description": "Catch open-source license risk before your PR ships.",
5
5
  "license": "MIT",
6
6
  "type": "module",