vouchington-tooling 0.19.0 → 0.19.2

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/README.md CHANGED
@@ -96,9 +96,10 @@ Name mode takes one or more `--name <artifact>` flags (mutually exclusive with `
96
96
  the run's artifacts once, and extracts every requested name that is present into `<dir>/<name>`.
97
97
  Names are matched literally, in request order, with repeats downloaded once; empty names, `.`, `..`,
98
98
  and names containing `/`, `\`, or a line break are rejected before anything is listed. Requested
99
- names absent from the run are skipped with a single bounded `::warning::` (the absent count, the
100
- first three names, then `and N more`) and never one line per name. `availability=available` means at
101
- least one requested artifact was downloaded; `availability=unavailable` (exit 0) means none was
99
+ names absent from the run are expected (callers pass a superset such as retry-attempt variants) and
100
+ are skipped with a single bounded `::notice::` (the absent count, the first three names, then
101
+ `and N more`) and never one line per name. `availability=available` means at least one requested
102
+ artifact was downloaded; `availability=unavailable` (exit 0) means none was
102
103
  present. A present artifact whose download fails is a hard failure: the helper stops at the first
103
104
  one, prints `download failed artifact=<name> exit=<n>`, exits non-zero (a downloader exit of 3 is
104
105
  reported as 1 so it cannot be mistaken for absence), and writes no `availability` output.
@@ -327,7 +328,10 @@ or derive a configuration or documentation package spec from that manifest inste
327
328
 
328
329
  `dependency-license-policy` keeps legal policy in the consumer. `collectPnpmLicenseReport` creates
329
330
  an isolated, script-free temporary workspace and store, expands pnpm's supported architectures to
330
- every `os`, `cpu`, and `libc` selector represented in the lockfile, and validates the JSON report.
331
+ every `os`, `cpu`, and `libc` selector represented in the lockfile, drops every `engines` constraint
332
+ from the audit copy of the lockfile, and validates the JSON report. Dropping `engines` keeps
333
+ `pnpm fetch` from skipping optional packages that exclude the running Node.js; pnpm 12's `fetch`
334
+ ignores `force`, so their licenses would otherwise report as Unknown.
331
335
  Pass explicit denied SPDX IDs and prefixes, exact aliases, and justified allowlist scopes to
332
336
  `evaluatePnpmLicenseReport`. Unknown, malformed, and custom SPDX references fail closed. Allowlist
333
337
  scopes are either intentionally global or an exact package-name set; the library returns structured
@@ -26,7 +26,10 @@ export function collectPnpmLicenseReport(repoRoot, options = {}) {
26
26
  const auditWorkspace = prepareWorkspace(repoRoot, readFile(lockfilePath, 'utf8'), readFile(workspacePath, 'utf8'));
27
27
  try {
28
28
  const storeConfig = `--config.store-dir=${join(auditWorkspace.cwd, '.pnpm-store')}`;
29
- const fetchResult = execute('pnpm', [storeConfig, '--config.force=true', 'fetch', '--ignore-scripts'], { cwd: auditWorkspace.cwd, encoding: 'utf8' });
29
+ const fetchResult = execute('pnpm', [storeConfig, 'fetch', '--ignore-scripts'], {
30
+ cwd: auditWorkspace.cwd,
31
+ encoding: 'utf8',
32
+ });
30
33
  assertCommandSucceeded('pnpm fetch', fetchResult);
31
34
  const result = execute('pnpm', [storeConfig, 'licenses', 'list', '--json'], {
32
35
  cwd: auditWorkspace.cwd,
@@ -2,8 +2,21 @@ export interface PnpmLicenseAuditWorkspace {
2
2
  readonly cleanup: () => void;
3
3
  readonly cwd: string;
4
4
  }
5
- export declare function renderPnpmLicenseAuditWorkspace(lockfileSource: string, workspaceSource: string, paths: {
5
+ /** The rendered `pnpm-lock.yaml` and `pnpm-workspace.yaml` of a license audit workspace. */
6
+ export interface PnpmLicenseAuditFiles {
7
+ readonly lockfile: string;
8
+ readonly workspace: string;
9
+ }
10
+ /**
11
+ * Renders the audit copies of `pnpm-lock.yaml` and `pnpm-workspace.yaml` so `pnpm fetch` downloads
12
+ * every package in the lockfile, including optional ones that don't match the host.
13
+ *
14
+ * The workspace supports every `os`, `cpu`, and `libc` in the lockfile. The lockfile drops every
15
+ * `engines` constraint because pnpm 12's `fetch` ignores `force` and skips an optional package whose
16
+ * `engines` exclude the running Node.js, which would leave that package's license Unknown.
17
+ */
18
+ export declare function renderPnpmLicenseAuditFiles(lockfileSource: string, workspaceSource: string, paths: {
6
19
  lockfile: string;
7
20
  workspace: string;
8
- }): string;
21
+ }): PnpmLicenseAuditFiles;
9
22
  export declare function preparePnpmLicenseAuditWorkspace(repoRoot: string, lockfileSource: string, workspaceSource: string): PnpmLicenseAuditWorkspace;
@@ -1,20 +1,20 @@
1
1
  import { copyFileSync, existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
- import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
4
+ import { isMap, parseDocument, stringify as stringifyYaml } from 'yaml';
5
5
  const PLATFORM_KEYS = ['os', 'cpu', 'libc'];
6
- function parseYamlObject(source, path) {
7
- let parsed;
8
- try {
9
- parsed = parseYaml(source);
10
- }
11
- catch (error) {
6
+ function parseYamlDocument(source, path) {
7
+ const document = parseDocument(source);
8
+ const [error] = document.errors;
9
+ if (error)
12
10
  throw new Error(`failed to parse ${path}: ${String(error)}`, { cause: error });
13
- }
14
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
11
+ return document;
12
+ }
13
+ function assertYamlObject(value, path) {
14
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
15
15
  throw new Error(`expected ${path} to contain a YAML object`);
16
16
  }
17
- return parsed;
17
+ return value;
18
18
  }
19
19
  function getStringList(value, path) {
20
20
  if (typeof value === 'string')
@@ -24,13 +24,7 @@ function getStringList(value, path) {
24
24
  }
25
25
  return value;
26
26
  }
27
- export function renderPnpmLicenseAuditWorkspace(lockfileSource, workspaceSource, paths) {
28
- const lockfile = parseYamlObject(lockfileSource, paths.lockfile);
29
- const workspace = parseYamlObject(workspaceSource, paths.workspace);
30
- const packages = lockfile.packages;
31
- if (typeof packages !== 'object' || packages === null || Array.isArray(packages)) {
32
- throw new Error(`expected ${paths.lockfile} to contain a packages object`);
33
- }
27
+ function collectSupportedArchitectures(packages, lockfilePath) {
34
28
  const supportedArchitectures = {
35
29
  cpu: ['current'],
36
30
  libc: ['current'],
@@ -44,27 +38,52 @@ export function renderPnpmLicenseAuditWorkspace(lockfileSource, workspaceSource,
44
38
  const value = snapshot[key];
45
39
  if (value === undefined)
46
40
  continue;
47
- for (const platform of getStringList(value, `${paths.lockfile} packages.*.${key}`)) {
41
+ for (const platform of getStringList(value, `${lockfilePath} packages.*.${key}`)) {
48
42
  values.add(platform);
49
43
  }
50
44
  }
51
45
  supportedArchitectures[key].push(...[...values].filter((value) => value !== 'current').sort());
52
46
  }
53
- return stringifyYaml({ ...workspace, packages: [], supportedArchitectures });
47
+ return supportedArchitectures;
48
+ }
49
+ /**
50
+ * Renders the audit copies of `pnpm-lock.yaml` and `pnpm-workspace.yaml` so `pnpm fetch` downloads
51
+ * every package in the lockfile, including optional ones that don't match the host.
52
+ *
53
+ * The workspace supports every `os`, `cpu`, and `libc` in the lockfile. The lockfile drops every
54
+ * `engines` constraint because pnpm 12's `fetch` ignores `force` and skips an optional package whose
55
+ * `engines` exclude the running Node.js, which would leave that package's license Unknown.
56
+ */
57
+ export function renderPnpmLicenseAuditFiles(lockfileSource, workspaceSource, paths) {
58
+ const lockfileDocument = parseYamlDocument(lockfileSource, paths.lockfile);
59
+ const lockfile = assertYamlObject(lockfileDocument.toJS(), paths.lockfile);
60
+ const workspace = assertYamlObject(parseYamlDocument(workspaceSource, paths.workspace).toJS(), paths.workspace);
61
+ const packageNodes = lockfileDocument.get('packages');
62
+ if (!isMap(packageNodes)) {
63
+ throw new Error(`expected ${paths.lockfile} to contain a packages object`);
64
+ }
65
+ const supportedArchitectures = collectSupportedArchitectures(lockfile.packages, paths.lockfile);
66
+ for (const { value } of packageNodes.items)
67
+ if (isMap(value))
68
+ value.delete('engines');
69
+ return {
70
+ lockfile: lockfileDocument.toString({ lineWidth: 0 }),
71
+ workspace: stringifyYaml({ ...workspace, packages: [], supportedArchitectures }),
72
+ };
54
73
  }
55
74
  export function preparePnpmLicenseAuditWorkspace(repoRoot, lockfileSource, workspaceSource) {
56
75
  const auditRoot = mkdtempSync(join(tmpdir(), 'dependency-license-audit-'));
57
76
  try {
58
- for (const filename of ['package.json', 'pnpm-lock.yaml']) {
59
- copyFileSync(join(repoRoot, filename), join(auditRoot, filename));
60
- }
77
+ copyFileSync(join(repoRoot, 'package.json'), join(auditRoot, 'package.json'));
61
78
  const npmrc = join(repoRoot, '.npmrc');
62
79
  if (existsSync(npmrc))
63
80
  copyFileSync(npmrc, join(auditRoot, '.npmrc'));
64
- writeFileSync(join(auditRoot, 'pnpm-workspace.yaml'), renderPnpmLicenseAuditWorkspace(lockfileSource, workspaceSource, {
81
+ const files = renderPnpmLicenseAuditFiles(lockfileSource, workspaceSource, {
65
82
  lockfile: join(repoRoot, 'pnpm-lock.yaml'),
66
83
  workspace: join(repoRoot, 'pnpm-workspace.yaml'),
67
- }), 'utf8');
84
+ });
85
+ writeFileSync(join(auditRoot, 'pnpm-lock.yaml'), files.lockfile, 'utf8');
86
+ writeFileSync(join(auditRoot, 'pnpm-workspace.yaml'), files.workspace, 'utf8');
68
87
  }
69
88
  catch (error) {
70
89
  rmSync(auditRoot, { force: true, recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -126,14 +126,14 @@ download_pattern() {
126
126
  done
127
127
  }
128
128
 
129
- warn_absent() {
129
+ notice_absent() {
130
130
  local requested="$1" count=$(($# - 1)) list='' name
131
131
  shift
132
132
  for name in "${@:1:3}"; do
133
133
  list="${list:+$list, }$name"
134
134
  done
135
135
  [ "$count" -le 3 ] || list="$list and $((count - 3)) more"
136
- echo "::warning::Optional same-run artifacts absent: $count of $requested requested (${list//\%/%25})" >&2
136
+ echo "::notice::Optional same-run artifacts absent: $count of $requested requested (${list//\%/%25})" >&2
137
137
  }
138
138
 
139
139
  download_names() {
@@ -152,7 +152,7 @@ download_names() {
152
152
  if contains "$name" "${listed[@]}"; then present+=("$name"); else absent+=("$name"); fi
153
153
  done
154
154
  echo "[optional-run-artifacts] selection selector=name requested=${#names[@]} present=${#present[@]} absent=${#absent[@]}" >&2
155
- [ "${#absent[@]}" -eq 0 ] || warn_absent "${#names[@]}" "${absent[@]}"
155
+ [ "${#absent[@]}" -eq 0 ] || notice_absent "${#names[@]}" "${absent[@]}"
156
156
  [ "${#present[@]}" -gt 0 ] || return 3
157
157
  for name in "${present[@]}"; do
158
158
  echo "[optional-run-artifacts] attempt selector=name artifact=$name" >&2