monkeypatchlist 0.0.0-stage → 0.1.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.
Files changed (221) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +61 -0
  3. package/README.md +183 -2
  4. package/catalog/catalog.json +552211 -0
  5. package/catalog/catalog.schema.json +787 -0
  6. package/catalog/changelog.json +3 -0
  7. package/dist/bin/monkeypatchlist.d.ts +2 -0
  8. package/dist/bin/monkeypatchlist.js +3 -0
  9. package/dist/catalog.d.ts +561 -0
  10. package/dist/catalog.js +58 -0
  11. package/dist/cli.d.ts +84 -0
  12. package/dist/cli.js +470 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +1 -0
  15. package/dist/lockfile-input.d.ts +9 -0
  16. package/dist/lockfile-input.js +91 -0
  17. package/dist/packages/analysis/binding.d.ts +2 -0
  18. package/dist/packages/analysis/binding.js +5 -0
  19. package/dist/packages/analysis/index.d.ts +10 -0
  20. package/dist/packages/analysis/index.js +13 -0
  21. package/dist/packages/analysis/job.d.ts +33 -0
  22. package/dist/packages/analysis/job.js +85 -0
  23. package/dist/packages/analysis/lib/absence-tags.d.ts +34 -0
  24. package/dist/packages/analysis/lib/absence-tags.js +340 -0
  25. package/dist/packages/analysis/lib/ast.d.ts +28 -0
  26. package/dist/packages/analysis/lib/ast.js +64 -0
  27. package/dist/packages/analysis/lib/binding.d.ts +36 -0
  28. package/dist/packages/analysis/lib/binding.js +66 -0
  29. package/dist/packages/analysis/lib/budget.d.ts +80 -0
  30. package/dist/packages/analysis/lib/budget.js +31 -0
  31. package/dist/packages/analysis/lib/catch-arms.d.ts +19 -0
  32. package/dist/packages/analysis/lib/catch-arms.js +351 -0
  33. package/dist/packages/analysis/lib/chunk-loads.d.ts +160 -0
  34. package/dist/packages/analysis/lib/chunk-loads.js +882 -0
  35. package/dist/packages/analysis/lib/chunk-registry.d.ts +45 -0
  36. package/dist/packages/analysis/lib/chunk-registry.js +334 -0
  37. package/dist/packages/analysis/lib/code-routes.d.ts +52 -0
  38. package/dist/packages/analysis/lib/code-routes.js +921 -0
  39. package/dist/packages/analysis/lib/collisions.d.ts +22 -0
  40. package/dist/packages/analysis/lib/collisions.js +103 -0
  41. package/dist/packages/analysis/lib/definitions.d.ts +76 -0
  42. package/dist/packages/analysis/lib/definitions.js +782 -0
  43. package/dist/packages/analysis/lib/early-exits.d.ts +39 -0
  44. package/dist/packages/analysis/lib/early-exits.js +161 -0
  45. package/dist/packages/analysis/lib/effect.d.ts +138 -0
  46. package/dist/packages/analysis/lib/effect.js +800 -0
  47. package/dist/packages/analysis/lib/global-object.d.ts +581 -0
  48. package/dist/packages/analysis/lib/global-object.js +8240 -0
  49. package/dist/packages/analysis/lib/keys.d.ts +33 -0
  50. package/dist/packages/analysis/lib/keys.js +82 -0
  51. package/dist/packages/analysis/lib/line-starts.d.ts +41 -0
  52. package/dist/packages/analysis/lib/line-starts.js +79 -0
  53. package/dist/packages/analysis/lib/link-group.d.ts +51 -0
  54. package/dist/packages/analysis/lib/link-group.js +487 -0
  55. package/dist/packages/analysis/lib/module-links.d.ts +117 -0
  56. package/dist/packages/analysis/lib/module-links.js +1060 -0
  57. package/dist/packages/analysis/lib/nested-code.d.ts +38 -0
  58. package/dist/packages/analysis/lib/nested-code.js +200 -0
  59. package/dist/packages/analysis/lib/numeric.d.ts +16 -0
  60. package/dist/packages/analysis/lib/numeric.js +216 -0
  61. package/dist/packages/analysis/lib/once-flags.d.ts +12 -0
  62. package/dist/packages/analysis/lib/once-flags.js +190 -0
  63. package/dist/packages/analysis/lib/parameter-arguments.d.ts +12 -0
  64. package/dist/packages/analysis/lib/parameter-arguments.js +34 -0
  65. package/dist/packages/analysis/lib/parse.d.ts +10 -0
  66. package/dist/packages/analysis/lib/parse.js +39 -0
  67. package/dist/packages/analysis/lib/partial.d.ts +60 -0
  68. package/dist/packages/analysis/lib/partial.js +518 -0
  69. package/dist/packages/analysis/lib/print.d.ts +3 -0
  70. package/dist/packages/analysis/lib/print.js +150 -0
  71. package/dist/packages/analysis/lib/reachability.d.ts +137 -0
  72. package/dist/packages/analysis/lib/reachability.js +1295 -0
  73. package/dist/packages/analysis/lib/resolver.d.ts +13 -0
  74. package/dist/packages/analysis/lib/resolver.js +2886 -0
  75. package/dist/packages/analysis/lib/restores.d.ts +42 -0
  76. package/dist/packages/analysis/lib/restores.js +482 -0
  77. package/dist/packages/analysis/lib/rewritten-deletes.d.ts +6 -0
  78. package/dist/packages/analysis/lib/rewritten-deletes.js +142 -0
  79. package/dist/packages/analysis/lib/scope.d.ts +110 -0
  80. package/dist/packages/analysis/lib/scope.js +443 -0
  81. package/dist/packages/analysis/lib/script-loads.d.ts +56 -0
  82. package/dist/packages/analysis/lib/script-loads.js +359 -0
  83. package/dist/packages/analysis/lib/summaries.d.ts +177 -0
  84. package/dist/packages/analysis/lib/summaries.js +498 -0
  85. package/dist/packages/analysis/lib/types.d.ts +632 -0
  86. package/dist/packages/analysis/lib/types.js +128 -0
  87. package/dist/packages/analysis/lib/wrap.d.ts +34 -0
  88. package/dist/packages/analysis/lib/wrap.js +407 -0
  89. package/dist/packages/analysis/lib/writes.d.ts +358 -0
  90. package/dist/packages/analysis/lib/writes.js +1612 -0
  91. package/dist/packages/analysis/visit-order.d.ts +3 -0
  92. package/dist/packages/analysis/visit-order.js +23 -0
  93. package/dist/packages/attribution/index.d.ts +233 -0
  94. package/dist/packages/attribution/index.js +444 -0
  95. package/dist/packages/attribution/lib/containing-package.d.ts +76 -0
  96. package/dist/packages/attribution/lib/containing-package.js +304 -0
  97. package/dist/packages/attribution/lib/first-party.d.ts +37 -0
  98. package/dist/packages/attribution/lib/first-party.js +153 -0
  99. package/dist/packages/attribution/lib/lockfile-graph.d.ts +99 -0
  100. package/dist/packages/attribution/lib/lockfile-graph.js +158 -0
  101. package/dist/packages/attribution/lib/lockfile.d.ts +33 -0
  102. package/dist/packages/attribution/lib/lockfile.js +330 -0
  103. package/dist/packages/attribution/lib/path-rules.d.ts +29 -0
  104. package/dist/packages/attribution/lib/path-rules.js +84 -0
  105. package/dist/packages/attribution/lib/pnpm.d.ts +4 -0
  106. package/dist/packages/attribution/lib/pnpm.js +133 -0
  107. package/dist/packages/attribution/lib/remote-url.d.ts +7 -0
  108. package/dist/packages/attribution/lib/remote-url.js +24 -0
  109. package/dist/packages/attribution/lib/source-map.d.ts +51 -0
  110. package/dist/packages/attribution/lib/source-map.js +125 -0
  111. package/dist/packages/attribution/lib/yaml.d.ts +9 -0
  112. package/dist/packages/attribution/lib/yaml.js +277 -0
  113. package/dist/packages/attribution/lib/yarn.d.ts +7 -0
  114. package/dist/packages/attribution/lib/yarn.js +227 -0
  115. package/dist/packages/baseline/index.d.ts +45 -0
  116. package/dist/packages/baseline/index.js +41 -0
  117. package/dist/packages/baseline/lib/error.d.ts +15 -0
  118. package/dist/packages/baseline/lib/error.js +25 -0
  119. package/dist/packages/baseline/lib/format.d.ts +49 -0
  120. package/dist/packages/baseline/lib/format.js +22 -0
  121. package/dist/packages/baseline/lib/migrations.d.ts +19 -0
  122. package/dist/packages/baseline/lib/migrations.js +7 -0
  123. package/dist/packages/baseline/lib/previous.d.ts +23 -0
  124. package/dist/packages/baseline/lib/previous.js +64 -0
  125. package/dist/packages/baseline/lib/read.d.ts +39 -0
  126. package/dist/packages/baseline/lib/read.js +219 -0
  127. package/dist/packages/baseline/lib/report.d.ts +25 -0
  128. package/dist/packages/baseline/lib/report.js +104 -0
  129. package/dist/packages/baseline/lib/skew.d.ts +51 -0
  130. package/dist/packages/baseline/lib/skew.js +75 -0
  131. package/dist/packages/compare/index.d.ts +120 -0
  132. package/dist/packages/compare/index.js +274 -0
  133. package/dist/packages/compare/lib/catalog-notes.d.ts +73 -0
  134. package/dist/packages/compare/lib/catalog-notes.js +224 -0
  135. package/dist/packages/completeness/index.d.ts +246 -0
  136. package/dist/packages/completeness/index.js +249 -0
  137. package/dist/packages/confidence/index.d.ts +54 -0
  138. package/dist/packages/confidence/index.js +71 -0
  139. package/dist/packages/config/index.d.ts +107 -0
  140. package/dist/packages/config/index.js +95 -0
  141. package/dist/packages/config/lib/command-line.d.ts +23 -0
  142. package/dist/packages/config/lib/command-line.js +239 -0
  143. package/dist/packages/config/lib/config-file.d.ts +11 -0
  144. package/dist/packages/config/lib/config-file.js +119 -0
  145. package/dist/packages/config/lib/scan-config-id.d.ts +28 -0
  146. package/dist/packages/config/lib/scan-config-id.js +54 -0
  147. package/dist/packages/config/lib/schema.d.ts +2 -0
  148. package/dist/packages/config/lib/schema.js +31 -0
  149. package/dist/packages/config/lib/types.d.ts +64 -0
  150. package/dist/packages/config/lib/types.js +6 -0
  151. package/dist/packages/config/lib/urls.d.ts +6 -0
  152. package/dist/packages/config/lib/urls.js +9 -0
  153. package/dist/packages/discovery/index.d.ts +277 -0
  154. package/dist/packages/discovery/index.js +1950 -0
  155. package/dist/packages/discovery/lib/import-map.d.ts +68 -0
  156. package/dist/packages/discovery/lib/import-map.js +180 -0
  157. package/dist/packages/discovery/lib/requests.d.ts +58 -0
  158. package/dist/packages/discovery/lib/requests.js +591 -0
  159. package/dist/packages/discovery/lib/scopes.d.ts +6 -0
  160. package/dist/packages/discovery/lib/scopes.js +286 -0
  161. package/dist/packages/discovery/lib/source-maps.d.ts +56 -0
  162. package/dist/packages/discovery/lib/source-maps.js +209 -0
  163. package/dist/packages/discovery/lib/swept-code.d.ts +27 -0
  164. package/dist/packages/discovery/lib/swept-code.js +107 -0
  165. package/dist/packages/discovery/lib/syntax.d.ts +17 -0
  166. package/dist/packages/discovery/lib/syntax.js +39 -0
  167. package/dist/packages/findings/index.d.ts +106 -0
  168. package/dist/packages/findings/index.js +254 -0
  169. package/dist/packages/findings/lib/scope.d.ts +38 -0
  170. package/dist/packages/findings/lib/scope.js +110 -0
  171. package/dist/packages/parallel/index.d.ts +2 -0
  172. package/dist/packages/parallel/index.js +5 -0
  173. package/dist/packages/parallel/lib/protocol.d.ts +15 -0
  174. package/dist/packages/parallel/lib/protocol.js +2 -0
  175. package/dist/packages/parallel/lib/runner.d.ts +36 -0
  176. package/dist/packages/parallel/lib/runner.js +96 -0
  177. package/dist/packages/parallel/lib/schedule.d.ts +16 -0
  178. package/dist/packages/parallel/lib/schedule.js +52 -0
  179. package/dist/packages/parallel/lib/worker.d.ts +1 -0
  180. package/dist/packages/parallel/lib/worker.js +18 -0
  181. package/dist/packages/policy/index.d.ts +36 -0
  182. package/dist/packages/policy/index.js +22 -0
  183. package/dist/packages/report/explain.d.ts +26 -0
  184. package/dist/packages/report/explain.js +99 -0
  185. package/dist/packages/report/index.d.ts +210 -0
  186. package/dist/packages/report/index.js +388 -0
  187. package/dist/packages/report/terminal.d.ts +11 -0
  188. package/dist/packages/report/terminal.js +331 -0
  189. package/dist/packages/source-store/fixture.d.ts +22 -0
  190. package/dist/packages/source-store/fixture.js +58 -0
  191. package/dist/packages/source-store/index.d.ts +278 -0
  192. package/dist/packages/source-store/index.js +322 -0
  193. package/dist/packages/source-store/lib/addresses.d.ts +6 -0
  194. package/dist/packages/source-store/lib/addresses.js +91 -0
  195. package/dist/packages/source-store/lib/connection.d.ts +15 -0
  196. package/dist/packages/source-store/lib/connection.js +135 -0
  197. package/dist/packages/source-store/lib/content-types.d.ts +16 -0
  198. package/dist/packages/source-store/lib/content-types.js +75 -0
  199. package/dist/packages/source-store/lib/data-urls.d.ts +10 -0
  200. package/dist/packages/source-store/lib/data-urls.js +85 -0
  201. package/dist/packages/source-store/lib/deadline.d.ts +18 -0
  202. package/dist/packages/source-store/lib/deadline.js +44 -0
  203. package/dist/packages/source-store/lib/exchange.d.ts +29 -0
  204. package/dist/packages/source-store/lib/exchange.js +112 -0
  205. package/dist/packages/source-store/lib/integrity.d.ts +19 -0
  206. package/dist/packages/source-store/lib/integrity.js +86 -0
  207. package/dist/packages/source-store/lib/outcomes.d.ts +12 -0
  208. package/dist/packages/source-store/lib/outcomes.js +22 -0
  209. package/dist/packages/source-store/lib/proxy.d.ts +11 -0
  210. package/dist/packages/source-store/lib/proxy.js +53 -0
  211. package/dist/packages/source-store/lib/session.d.ts +12 -0
  212. package/dist/packages/source-store/lib/session.js +139 -0
  213. package/dist/packages/source-store/lib/urls.d.ts +6 -0
  214. package/dist/packages/source-store/lib/urls.js +15 -0
  215. package/dist/packages/source-store/resolve.d.ts +59 -0
  216. package/dist/packages/source-store/resolve.js +45 -0
  217. package/dist/scan.d.ts +65 -0
  218. package/dist/scan.js +592 -0
  219. package/package.json +95 -4
  220. package/schema/config.schema.json +133 -0
  221. package/schema/report.schema.json +1905 -0
package/dist/cli.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { type CatalogChangelog, type SerializedCatalog } from './catalog.ts';
2
+ import { type Budget, type Resolver } from './packages/analysis/index.ts';
3
+ import { type Migrations } from './packages/baseline/index.ts';
4
+ import { type GroupRunner } from './packages/parallel/index.ts';
5
+ import { type Fetcher } from './packages/source-store/index.ts';
6
+ import type { ResolvedReference } from './packages/discovery/index.ts';
7
+ /** Exit codes (§16). */
8
+ export declare const exitCodes: {
9
+ readonly success: 0;
10
+ readonly gateFailed: 1;
11
+ readonly usage: 2;
12
+ readonly incomplete: 3;
13
+ readonly internal: 4;
14
+ };
15
+ /** The CLI's usage text: the commands the config module reads, and `--version`, which the CLI answers itself. */
16
+ export declare const usage: string;
17
+ /**
18
+ * The wall-clock crash guard (§13, §18): a scan still analysing this long after it started runs no further link group,
19
+ * and is `incomplete` with no findings. It guards against a runaway scan, far above the cost target (§18), and no
20
+ * result depends on when it fired.
21
+ */
22
+ export declare const crashGuardMs: number;
23
+ export interface Io {
24
+ /** `isTTY` is true when stdout is a terminal: only then is the terminal report colored (§15 Formatting). */
25
+ stdout: {
26
+ write(text: string): void;
27
+ isTTY?: boolean;
28
+ };
29
+ stderr: {
30
+ write(text: string): void;
31
+ };
32
+ /** The environment: `NO_COLOR` turns color off (§15 Formatting). */
33
+ env?: Readonly<Record<string, string | undefined>>;
34
+ /** The working directory: relative paths, and the default `monkeypatchlist.config.json`, are found from it. */
35
+ cwd(): string;
36
+ }
37
+ /** What the CLI plugs into the pipeline. Tests may swap any of them. */
38
+ export interface Deps {
39
+ resolver: Resolver;
40
+ loadCatalog: () => SerializedCatalog;
41
+ /** Reads the catalog changelog shipped with the catalog (§4 Catalog versions). */
42
+ loadChangelog: () => CatalogChangelog;
43
+ /**
44
+ * Makes the fetcher `--mode resolve` reads remote URLs through (§19 Source store and retrieval), once per scan, or is
45
+ * null when this build has none. `offline` always uses the offline fetcher, and never calls this, so no command
46
+ * touches the network unless `--mode resolve` is given (§16).
47
+ */
48
+ resolveFetcher: (() => Fetcher) | null;
49
+ /** A monotonic clock in milliseconds: the `run` block's duration and the crash guard read it. */
50
+ clock: () => number;
51
+ /** The wall clock: the `run` block's `startedAt`, and when each fetch finished. */
52
+ now: () => Date;
53
+ /** How a baseline in an older format or fingerprint scheme is read (§4 Version skew, §11 Format versions). */
54
+ migrations: Migrations;
55
+ /**
56
+ * The step budget per source byte of a link group, and the caps (§18): the shipped constants. They are never a flag;
57
+ * the cost benchmark swaps in multiplied ones for its raised-budget rerun.
58
+ */
59
+ budget: Budget;
60
+ /**
61
+ * How many link groups may be analyzed at once (§18 Parallelism), an internal setting and never a flag. At 1, the
62
+ * default, they run one after another in process. Above 1, they run in worker threads with the shipped resolver, so
63
+ * `resolver` must be it, while the groups' estimated peak heaps fit `memoryBudget`. Either way the report is the same.
64
+ */
65
+ workers: number;
66
+ /** The memory, in bytes, the estimated peak heaps of the groups running at once must fit in (§18 Parallelism). */
67
+ memoryBudget: number;
68
+ /**
69
+ * The references only the resolver can see that a scan's first round starts from (§19), never a flag: none by
70
+ * default. A test seeds every one up front to show the report doesn't depend on the rounds.
71
+ */
72
+ seed: readonly ResolvedReference[];
73
+ /**
74
+ * Makes what runs a scan's link groups, given the catalog and budget it runs with, in place of the runner `workers`
75
+ * chooses, or null for that runner. Internal and never a flag: the cost benchmark swaps in one that measures each
76
+ * group's time and peak heap (§18 Target). Whatever runs the groups, the report is the same.
77
+ */
78
+ groups: ((setup: {
79
+ catalog: SerializedCatalog;
80
+ budget: Budget;
81
+ }) => GroupRunner) | null;
82
+ }
83
+ /** Runs `monkeypatchlist <argv…>`. Returns the exit code. The terminal report goes to stdout, and diagnostics to stderr. */
84
+ export declare function main(argv: readonly string[], io: Io, deps?: Partial<Deps>): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,470 @@
1
+ // The CLI's composition root: loads the configuration, wires the modules together, reads the clock for the report's
2
+ // `run` block, writes the output and chooses the exit code (§16).
3
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { loadCatalog, loadChangelog } from "./catalog.js";
6
+ import { defaultBudget, jsResolver } from "./packages/analysis/index.js";
7
+ import { BaselineError, buildBaseline, formatVersion, migrations, newerVersions, readBaseline, readPrevious, renderBaseline, versionSkew, writtenVersions, } from "./packages/baseline/index.js";
8
+ import { Ajv2020 } from 'ajv/dist/2020.js';
9
+ import { differingScanConfigFields, loadConfig, loadExplainConfig, usage as commandUsage, UsageError, } from "./packages/config/index.js";
10
+ import { fingerprintScheme } from "./packages/findings/index.js";
11
+ import { findExplained, renderCandidates, renderExplanation } from "./packages/report/explain.js";
12
+ import { buildReport, renderAccepted, renderJson, reportVersion } from "./packages/report/index.js";
13
+ import { defaultMemoryBudget, workerRunner } from "./packages/parallel/index.js";
14
+ import { renderTerminal } from "./packages/report/terminal.js";
15
+ import { createSourceStore, offlineFetcher } from "./packages/source-store/index.js";
16
+ import { resolveFetcher as makeResolveFetcher } from "./packages/source-store/resolve.js";
17
+ import { readLockfileInput } from "./lockfile-input.js";
18
+ import { scan } from "./scan.js";
19
+ /** Exit codes (§16). */
20
+ export const exitCodes = { success: 0, gateFailed: 1, usage: 2, incomplete: 3, internal: 4 };
21
+ /** The flags that print the versions (§16 Flags). */
22
+ const versionFlags = ['--version', '-v'];
23
+ /** The CLI's usage text: the commands the config module reads, and `--version`, which the CLI answers itself. */
24
+ export const usage = `${commandUsage}\n monkeypatchlist --version | -v`;
25
+ /**
26
+ * The wall-clock crash guard (§13, §18): a scan still analysing this long after it started runs no further link group,
27
+ * and is `incomplete` with no findings. It guards against a runaway scan, far above the cost target (§18), and no
28
+ * result depends on when it fired.
29
+ */
30
+ export const crashGuardMs = 30 * 60 * 1000;
31
+ const defaultDeps = {
32
+ resolver: jsResolver,
33
+ loadCatalog,
34
+ loadChangelog,
35
+ // A fresh fetcher for each scan, so that nothing it caches or counts outlives the scan (§17 Caching).
36
+ resolveFetcher: () => makeResolveFetcher({ version: monkeypatchlistVersion(), env: process.env }),
37
+ clock: () => performance.now(),
38
+ now: () => new Date(),
39
+ migrations,
40
+ budget: defaultBudget,
41
+ workers: 1,
42
+ memoryBudget: defaultMemoryBudget,
43
+ seed: [],
44
+ groups: null,
45
+ };
46
+ /** Runs `monkeypatchlist <argv…>`. Returns the exit code. The terminal report goes to stdout, and diagnostics to stderr. */
47
+ export async function main(argv, io, deps = {}) {
48
+ const { resolver, loadCatalog, loadChangelog, resolveFetcher, clock, now, migrations, budget, workers, memoryBudget, groups, seed, } = {
49
+ ...defaultDeps,
50
+ ...deps,
51
+ };
52
+ try {
53
+ const cwd = io.cwd();
54
+ // `--version` prints the versions and reads no config file or build directory (§16 Flags).
55
+ if (versionFlags.includes(argv[0] ?? ''))
56
+ return version(argv, io, { loadCatalog, resolver });
57
+ // `explain` reads a saved report and never scans (§16 Commands).
58
+ if (argv[0] === 'explain')
59
+ return explain(argv.slice(1), io);
60
+ const config = loadConfig(argv, { cwd });
61
+ if (config.mode === 'resolve' && resolveFetcher === null) {
62
+ throw new UsageError("--mode resolve isn't available: this build of monkeypatchlist has no resolve fetcher");
63
+ }
64
+ const buildDir = resolve(cwd, config.buildDir);
65
+ if (!statSync(buildDir, { throwIfNoEntry: false })?.isDirectory()) {
66
+ throw new UsageError(`build directory '${config.buildDir}' is not a directory`);
67
+ }
68
+ const catalog = loadCatalog();
69
+ const versions = {
70
+ formatVersion,
71
+ fingerprintScheme,
72
+ catalogVersion: catalog.metadata.catalogVersion,
73
+ monkeypatchlistVersion: monkeypatchlistVersion(),
74
+ };
75
+ const baseline = comparedBaseline({ cwd, config, versions, migrations }, io.stderr);
76
+ const replaced = replacedNewer({ cwd, config, versions, migrations });
77
+ const startedAt = now();
78
+ const start = clock();
79
+ const fetcher = config.mode === 'resolve' && resolveFetcher !== null ? resolveFetcher() : offlineFetcher;
80
+ // Each source-map directory is relative to the working directory, and is read under the name it was declared by.
81
+ const sourceMapDirs = new Map(config.sourceMaps.map((dir) => [dir, resolve(cwd, dir)]));
82
+ const store = createSourceStore({ buildDir, mappings: config.mappings, fetcher, now, sourceMapDirs });
83
+ const result = await scan(config, {
84
+ store,
85
+ catalog,
86
+ changelog: loadChangelog(),
87
+ resolver,
88
+ monkeypatchlistVersion: versions.monkeypatchlistVersion,
89
+ baseline,
90
+ crashGuard: { fired: () => clock() - start >= crashGuardMs },
91
+ budget,
92
+ lockfile: config.lockfile === null ? null : readLockfileInput(cwd, config.lockfile),
93
+ ...(groups !== null
94
+ ? { groups: groups({ catalog, budget }) }
95
+ : workers > 1
96
+ ? { groups: parallelGroups({ resolver, workers, memoryBudget, catalog, budget }) }
97
+ : {}),
98
+ }, seed);
99
+ const report = buildReport({
100
+ ...result,
101
+ run: {
102
+ startedAt: startedAt.toISOString(),
103
+ durationMs: Math.round(clock() - start),
104
+ retrievals: store.retrievals(),
105
+ },
106
+ });
107
+ const json = renderJson(report);
108
+ if (config.json === '-')
109
+ io.stdout.write(json);
110
+ else {
111
+ // Color only on a TTY, and never under a non-empty `NO_COLOR` or `--no-color` (§15 Formatting, no-color.org).
112
+ const noColor = io.env?.NO_COLOR;
113
+ const color = io.stdout.isTTY === true && (noColor === undefined || noColor === '') && config.color;
114
+ io.stdout.write(renderTerminal(report, { color, verbose: config.verbose, listIneffective: config.listIneffective }));
115
+ if (config.json !== null)
116
+ writeFileSync(resolve(cwd, config.json), json);
117
+ }
118
+ // Under `--fail-on`, `--allow-incomplete` downgrades an `incomplete` scan's exit code 3 to a warning (§13, §16).
119
+ const { level } = result.completeness;
120
+ if (config.command === 'scan' && config.failOn.length > 0 && config.allowIncomplete && level === 'incomplete') {
121
+ io.stderr.write('monkeypatchlist: warning: the scan is incomplete, so the gate saw only what it analyzed; --allow-incomplete ' +
122
+ 'downgrades exit code 3 to this warning\n');
123
+ }
124
+ // `baseline init` and `baseline update` accept the whole current finding set (§11 Commands).
125
+ if (config.command !== 'scan') {
126
+ const fired = result.completeness.counters.statistics.crashGuard === 'fired';
127
+ // `baseline init` refuses a scan whose crash guard fired (exit 3), even with `--allow-incomplete`, and writes
128
+ // nothing (§13): such a scan reports no findings, and the guard never changes results.
129
+ if (config.command === 'baseline init' && fired) {
130
+ io.stderr.write(`monkeypatchlist: baseline init refuses a scan whose crash guard fired, even under --allow-incomplete, so ` +
131
+ `${config.out} is left as it was: a scan whose guard fired reports no findings. Run it again\n`);
132
+ return exitCodes.incomplete;
133
+ }
134
+ // `baseline init` and `baseline update` refuse an `incomplete` scan unless `--allow-incomplete` is given (§11
135
+ // Commands, §13).
136
+ if (level === 'incomplete' && !config.allowIncomplete) {
137
+ io.stderr.write(`monkeypatchlist: ${config.command} refuses an incomplete scan, so ${config.out} is left as it was: the report's ` +
138
+ 'completeness says what was not analyzed. Pass --allow-incomplete to write it anyway\n');
139
+ return exitCodes.incomplete;
140
+ }
141
+ // A scan whose crash guard fired reports no findings, so `baseline update --allow-incomplete` leaves the baseline
142
+ // unchanged (§13): every entry is `unconfirmed`, and the header keeps the versions and level it was written
143
+ // under.
144
+ if (config.command === 'baseline update' && fired) {
145
+ io.stderr.write(`monkeypatchlist: warning: the crash guard fired, so baseline update left ${config.out} as it was: a scan whose ` +
146
+ 'guard fired reports no findings, and every baseline entry is unconfirmed\n');
147
+ return report.gate.exitCode;
148
+ }
149
+ const baseline = buildBaseline({
150
+ scanConfigId: config.scanConfigId,
151
+ surface: config.surface,
152
+ tool: result.tool,
153
+ completeness: level,
154
+ findings: result.findings,
155
+ packages: result.packages,
156
+ });
157
+ writeFileSync(resolve(cwd, config.out), renderBaseline(baseline));
158
+ // Once the file is written, `baseline update` prints what it accepted (§11 Commands): after the terminal report,
159
+ // or on stderr when stdout carries the JSON report instead.
160
+ if (config.command === 'baseline update') {
161
+ const accepted = renderAccepted(report, config.out);
162
+ if (config.json === '-')
163
+ io.stderr.write(accepted);
164
+ else
165
+ io.stdout.write(`\n${accepted}`);
166
+ }
167
+ // `baseline init` writes on an `incomplete` scan under `--allow-incomplete`, with a warning (§11 Commands, §13).
168
+ if (config.command === 'baseline init' && level === 'incomplete') {
169
+ io.stderr.write("monkeypatchlist: warning: baseline init wrote a baseline from a scan that is incomplete (the report's " +
170
+ 'completeness says what was not analyzed), so it lacks whatever the scan could not analyze\n');
171
+ }
172
+ // `baseline update` writes on a `complete-with-limitations` scan, and on an `incomplete` one under
173
+ // `--allow-incomplete`, with a warning (§11 Commands, §13). The entries the scan couldn't confirm are carried
174
+ // forward unchanged.
175
+ if (config.command === 'baseline update' && level !== 'complete') {
176
+ const unconfirmed = result.findings.filter((finding) => finding.baselineState === 'unconfirmed').length;
177
+ const entries = `${unconfirmed} unconfirmed ${unconfirmed === 1 ? 'entry' : 'entries'}`;
178
+ io.stderr.write(`monkeypatchlist: warning: baseline update wrote a baseline from a scan that is ${level} (the report's completeness ` +
179
+ `says what was not analyzed), and carried ${entries} forward unchanged\n`);
180
+ }
181
+ // Under `--allow-downgrade`, `baseline init` replaced a baseline a newer version wrote (§4 Version skew).
182
+ if (replaced.length > 0) {
183
+ io.stderr.write(`monkeypatchlist: warning: baseline init replaced '${config.out}', which a newer version wrote ` +
184
+ `(${newerThan(replaced)}), with a baseline this one wrote\n`);
185
+ }
186
+ const count = baseline.entries.length;
187
+ io.stderr.write(`monkeypatchlist: wrote ${config.out} (${count} ${count === 1 ? 'entry' : 'entries'})\n`);
188
+ }
189
+ return report.gate.exitCode;
190
+ }
191
+ catch (error) {
192
+ if (error instanceof UsageError) {
193
+ io.stderr.write(`monkeypatchlist: ${error.message}\n${usage}\n`);
194
+ return exitCodes.usage;
195
+ }
196
+ io.stderr.write(`monkeypatchlist: internal error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
197
+ return exitCodes.internal;
198
+ }
199
+ }
200
+ /**
201
+ * `monkeypatchlist --version` (§16 Flags): one line on stdout, `monkeypatchlist <version> · catalog <n> · analyzer
202
+ * <analyzerVersion>`, with the values a scan's report records in its `tool` block, read from the same places: the
203
+ * package version, the catalog the CLI loads and the resolver it runs. Anything after the flag is a usage error.
204
+ */
205
+ function version(argv, io, { loadCatalog, resolver }) {
206
+ if (argv.length > 1)
207
+ throw new UsageError(`${argv[0]} takes no other arguments`);
208
+ const catalogVersion = loadCatalog().metadata.catalogVersion;
209
+ io.stdout.write(`monkeypatchlist ${monkeypatchlistVersion()} · catalog ${catalogVersion} · analyzer ${resolver.analyzerVersion}\n`);
210
+ return exitCodes.success;
211
+ }
212
+ /**
213
+ * `monkeypatchlist explain <fingerprint|target> --report <file>` (§16 Commands): renders one finding from a saved JSON report,
214
+ * with every occurrence and its trace (§14). It reads only the report, never the build directory, and runs no scan. A
215
+ * fingerprint or target the report doesn't hold is a usage error (exit 2), and so is a target that names several
216
+ * findings, which lists them with their fingerprints.
217
+ */
218
+ function explain(argv, io) {
219
+ const config = loadExplainConfig(argv, { cwd: io.cwd() });
220
+ const report = readReport(resolve(io.cwd(), config.report), config.report);
221
+ const explained = findExplained(report, config.query);
222
+ if (explained.kind === 'unknown') {
223
+ io.stderr.write(`monkeypatchlist: no finding in '${config.report}' has the fingerprint or target '${config.query}'\n`);
224
+ return exitCodes.usage;
225
+ }
226
+ if (explained.kind === 'ambiguous') {
227
+ const count = explained.candidates.length;
228
+ io.stderr.write(`monkeypatchlist: '${config.query}' names ${count} findings in '${config.report}'; explain one by its fingerprint:\n` +
229
+ renderCandidates(explained.candidates));
230
+ return exitCodes.usage;
231
+ }
232
+ io.stdout.write(renderExplanation(report, explained.finding));
233
+ return exitCodes.success;
234
+ }
235
+ let validateReport;
236
+ /** Reads a saved JSON report (§15), or throws a `UsageError` when it can't be read or doesn't follow the report schema. */
237
+ function readReport(path, given) {
238
+ let text;
239
+ try {
240
+ text = readFileSync(path, 'utf8');
241
+ }
242
+ catch (error) {
243
+ throw new UsageError(`report '${given}' can't be read: ${error.code ?? error}`);
244
+ }
245
+ let value;
246
+ try {
247
+ value = JSON.parse(text.startsWith('\uFEFF') ? text.slice(1) : text);
248
+ }
249
+ catch (error) {
250
+ throw new UsageError(`report '${given}' is not valid JSON: ${error.message}`);
251
+ }
252
+ validateReport ??= new Ajv2020({ allErrors: true, strict: true }).compile(JSON.parse(readFileSync(new URL('../schema/report.schema.json', import.meta.url), 'utf8')));
253
+ if (!validateReport(value)) {
254
+ const [first] = validateReport.errors ?? [];
255
+ const where = first === undefined ? '' : `: ${first.instancePath || '(top level)'} ${first.message ?? ''}`;
256
+ throw new UsageError(`report '${given}' is not a monkeypatchlist JSON report this version reads${where}`);
257
+ }
258
+ return value;
259
+ }
260
+ /**
261
+ * The baseline the scan compares with, its path as given, and its version-skew note if it has one (§4), or null for
262
+ * none (§11):
263
+ *
264
+ * - `scan`'s `--baseline`. Without `--fail-on`, one that can't be used leaves the scan without a baseline, with a
265
+ * warning (§11 No baseline). Under `--fail-on` it is a usage error, so a mistyped path never passes CI.
266
+ * - The file `baseline update` rewrites (`--out`), so that it can print what it accepts (§11 Commands). It must be a
267
+ * readable baseline: `baseline init` writes a new one.
268
+ *
269
+ * With it come the previous scan's values for the reachability and install-context notes (§11 Never `changed`): those
270
+ * of `--previous`, which override a report baseline's, or those of a report baseline, or none.
271
+ */
272
+ function comparedBaseline(context, stderr) {
273
+ const { config, versions } = context;
274
+ let path;
275
+ let read;
276
+ if (config.command === 'baseline update') {
277
+ path = config.out;
278
+ read = loadBaseline(context, path, (problem) => {
279
+ throw new UsageError('baseline update needs a readable baseline to rewrite (after a merge conflict, check out either side and run ' +
280
+ `it again, or write a new one with monkeypatchlist baseline init): ${problem}`);
281
+ });
282
+ }
283
+ else {
284
+ if (config.command !== 'scan')
285
+ return null;
286
+ if (config.baseline === null) {
287
+ warnPreviousUnused(config, stderr);
288
+ return null;
289
+ }
290
+ const { failOn } = config;
291
+ path = config.baseline;
292
+ read = loadBaseline(context, path, (problem) => {
293
+ if (failOn.length > 0) {
294
+ throw new UsageError(`--fail-on needs a readable baseline (pass --baseline <path>, or write one with monkeypatchlist baseline init): ${problem}`);
295
+ }
296
+ stderr.write(`monkeypatchlist: warning: scanning without a baseline, so findings have no baseline state: ${problem}\n`);
297
+ return null;
298
+ });
299
+ }
300
+ if (read === null) {
301
+ if (config.command === 'scan')
302
+ warnPreviousUnused(config, stderr);
303
+ return null;
304
+ }
305
+ // A baseline written by another version scans with a note naming both versions (§4 Version skew).
306
+ const skew = versionSkew(read.written, versions);
307
+ if (skew !== null)
308
+ heedSkew(skew, path, config, stderr);
309
+ // An explicit `--previous` overrides a report baseline as the source of the previous values.
310
+ const explicit = config.command === 'scan' && config.previous !== null ? loadPrevious(context, config.previous) : null;
311
+ return { path, baseline: read.baseline, notes: skew === null ? [] : [skew], previous: explicit ?? read.previous };
312
+ }
313
+ /** `--previous` without a baseline to compare with gives no notes, and a warning (§16). */
314
+ function warnPreviousUnused(config, stderr) {
315
+ if (config.previous === null)
316
+ return;
317
+ stderr.write(`monkeypatchlist: warning: --previous '${config.previous}' gives no reachability or install-context notes without a ` +
318
+ 'baseline: the notes follow the baseline pairing, so pass --baseline <path> as well\n');
319
+ }
320
+ /**
321
+ * The previous scan's values in the report `path` names (§16 `--previous`). A report that can't be read, isn't a scan
322
+ * report this monkeypatchlist reads, or was written for another scan configuration is a usage error.
323
+ */
324
+ function loadPrevious({ cwd, config }, path) {
325
+ let text;
326
+ try {
327
+ text = readFileSync(resolve(cwd, path), 'utf8');
328
+ }
329
+ catch (error) {
330
+ throw new UsageError(`previous report '${path}' can't be read: ${error.code ?? error}`);
331
+ }
332
+ let read;
333
+ try {
334
+ read = readPrevious(text, reportVersion);
335
+ }
336
+ catch (error) {
337
+ if (!(error instanceof BaselineError))
338
+ throw error;
339
+ throw new UsageError(`previous report '${path}' can't be used: ${error.message}`);
340
+ }
341
+ // A previous report is compared only with a scan of the configuration it was written from, as a baseline is.
342
+ const differing = differingScanConfigFields(config.scanConfigId, read.scanConfigId);
343
+ if (differing.length > 0) {
344
+ throw new UsageError(`previous report '${path}' was written for another scan configuration: it differs from this scan in its ` +
345
+ `${fieldList(differing)}. Pass a report of a scan with the same settings`);
346
+ }
347
+ return read.values;
348
+ }
349
+ /** Field names as `a`, `a and b` or `a, b and c`. */
350
+ function fieldList(fields) {
351
+ return fields.length === 1 ? fields[0] : `${fields.slice(0, -1).join(', ')} and ${fields.at(-1)}`;
352
+ }
353
+ /**
354
+ * What a baseline written by another version means for the command, beyond the note (§4 Version skew, §11 Format
355
+ * versions, ADR 0008):
356
+ *
357
+ * - `baseline update` refuses to rewrite one written by a newer version unless `--allow-downgrade` is given.
358
+ * - An older format or fingerprint scheme was migrated in memory, with a warning.
359
+ * - One written by a newer version (a stale install) scans with a warning.
360
+ */
361
+ function heedSkew(skew, path, config, stderr) {
362
+ const [baseline, scan] = [versionsOf(skew, 'baseline'), versionsOf(skew, 'scan')];
363
+ if (skew.newer === 'baseline' && config.command === 'baseline update' && !config.allowDowngrade) {
364
+ throw new UsageError(`baseline update won't rewrite '${path}': it was written by a newer version (${baseline}) than this one ` +
365
+ `(${scan}). Upgrade monkeypatchlist, or pass --allow-downgrade to rewrite it with this one`);
366
+ }
367
+ for (const version of ['formatVersion', 'fingerprintScheme']) {
368
+ const { baseline: written, scan: current } = skew[version];
369
+ if (written < current) {
370
+ stderr.write(`monkeypatchlist: warning: baseline '${path}' is in ${versionLabels[version]} ${written}, older than this ` +
371
+ `monkeypatchlist's (${current}): it was migrated in memory, and baseline update rewrites it\n`);
372
+ }
373
+ }
374
+ if (skew.newer === 'baseline') {
375
+ stderr.write(`monkeypatchlist: warning: baseline '${path}' was written by a newer version (${baseline}) than this one (${scan}): ` +
376
+ "findings carry no explainedBy, and a delta may come from the newer version's catalog or analyzer. Upgrade " +
377
+ 'monkeypatchlist to compare like with like\n');
378
+ }
379
+ }
380
+ /**
381
+ * The versions of the baseline `baseline init` replaces (`--out`) that are newer than this monkeypatchlist's (§4 Version
382
+ * skew), or none for any other command. One with any was written by a newer monkeypatchlist or catalog, and `init` refuses to
383
+ * replace it unless `--allow-downgrade` is given, before it scans. A file it can't read (a missing one, say) has none,
384
+ * and so does a file that records no version it can read, such as one left with merge-conflict markers: both are
385
+ * replaced, and a write that fails is an internal error.
386
+ */
387
+ function replacedNewer({ cwd, config, versions, migrations }) {
388
+ if (config.command !== 'baseline init')
389
+ return [];
390
+ let text;
391
+ try {
392
+ text = readFileSync(resolve(cwd, config.out), 'utf8');
393
+ }
394
+ catch {
395
+ return [];
396
+ }
397
+ const newer = newerVersions(writtenVersions(text, migrations), versions);
398
+ if (newer.length > 0 && !config.allowDowngrade) {
399
+ throw new UsageError(`baseline init won't replace '${config.out}': it was written by a newer version (${newerThan(newer)}). ` +
400
+ 'Upgrade monkeypatchlist, or pass --allow-downgrade to replace it with a baseline this one writes');
401
+ }
402
+ return newer;
403
+ }
404
+ /** How a message names each version a baseline header records. */
405
+ const versionLabels = {
406
+ formatVersion: 'formatVersion',
407
+ fingerprintScheme: 'fingerprint scheme',
408
+ catalogVersion: 'catalog',
409
+ monkeypatchlistVersion: 'monkeypatchlist',
410
+ };
411
+ /** Each newer version and this monkeypatchlist's, as `catalog 5, newer than this one's 4`. */
412
+ function newerThan(newer) {
413
+ return newer
414
+ .map(({ name, baseline, scan }) => `${versionLabels[name]} ${baseline}, newer than this one's ${scan}`)
415
+ .join('; ');
416
+ }
417
+ /** The monkeypatchlist and catalog versions on one side of a version skew, as `monkeypatchlist 1.2.0 · catalog 3`. */
418
+ function versionsOf(skew, side) {
419
+ return `monkeypatchlist ${skew.monkeypatchlistVersion[side]} · catalog ${skew.catalogVersion[side]}`;
420
+ }
421
+ /**
422
+ * Reads a baseline to compare with (§11), or null when there is none to compare with.
423
+ *
424
+ * - A file that can't be read, or isn't a baseline, is passed to `unreadable`, which says what that means for the
425
+ * command: a usage error, or a scan without a baseline (§11 No baseline).
426
+ * - A baseline in a newer format, or written for another scan configuration, is a usage error either way (§11 Format
427
+ * versions, §16 Scan configuration identity).
428
+ */
429
+ function loadBaseline({ cwd, config, migrations }, path, unreadable) {
430
+ let text;
431
+ try {
432
+ text = readFileSync(resolve(cwd, path), 'utf8');
433
+ }
434
+ catch (error) {
435
+ return unreadable(`baseline '${path}' can't be read: ${error.code ?? error}`);
436
+ }
437
+ let read;
438
+ try {
439
+ // `scan --baseline` also reads a full scan report (§11 Baseline, ADR 0020); `baseline update` rewrites a baseline file.
440
+ const report = config.command === 'scan' ? { reportVersion, allowIncomplete: config.allowIncomplete } : undefined;
441
+ read = readBaseline(text, migrations, report === undefined ? {} : { report });
442
+ }
443
+ catch (error) {
444
+ if (!(error instanceof BaselineError))
445
+ throw error;
446
+ const problem = `baseline '${path}' can't be used: ${error.message}`;
447
+ if (error.newerFormat)
448
+ throw new UsageError(problem);
449
+ return unreadable(problem);
450
+ }
451
+ // A baseline is compared only with a scan of the configuration it was written from (§16, ADR 0017).
452
+ const differing = differingScanConfigFields(config.scanConfigId, read.baseline.scanConfigId);
453
+ if (differing.length > 0) {
454
+ throw new UsageError(`baseline '${path}' was written for another scan configuration: it differs from this scan in its ${fieldList(differing)}. ` +
455
+ 'Scan with the settings it was written with, or write a new baseline with monkeypatchlist baseline init');
456
+ }
457
+ return read;
458
+ }
459
+ function monkeypatchlistVersion() {
460
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
461
+ }
462
+ /**
463
+ * The worker-thread group runner (§18 Parallelism). Its workers run the shipped resolver, so a scan that swaps in
464
+ * another one may not ask for workers.
465
+ */
466
+ function parallelGroups(options) {
467
+ if (options.resolver !== jsResolver)
468
+ throw new Error('parallel link groups run only the shipped resolver');
469
+ return workerRunner(options);
470
+ }
@@ -0,0 +1,2 @@
1
+ export { loadCatalog, loadChangelog, realms, surfaces } from './catalog.ts';
2
+ export type { AssignKind, CatalogChangelog, CatalogHolder, CatalogTarget, ChangeRecord, Descriptor, Drop, EngineExtension, EnginePrototype, EsAddition, EsProseProperty, Exclusion, Holder, InstanceData, InstanceMember, MemberData, MemberKind, PropertyKind, ProseMember, ProtectedEntry, Realm, Retention, SerializedCatalog, Singleton, Surface, SurfaceMap, } from './catalog.ts';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { loadCatalog, loadChangelog, realms, surfaces } from "./catalog.js";
@@ -0,0 +1,9 @@
1
+ import { type LockfileInput } from './packages/attribution/index.ts';
2
+ /**
3
+ * The lockfile input of `lockfile` (as given, relative to `cwd`): its text and, for pnpm and Yarn, the manifests in its
4
+ * directory and in the workspace folders it names, or for Yarn v1 those its own manifest's `workspaces` globs match.
5
+ */
6
+ export declare function readLockfileInput(cwd: string, lockfile: {
7
+ path: string;
8
+ importer: string | null;
9
+ }): LockfileInput;
@@ -0,0 +1,91 @@
1
+ // Reads the lockfile a scan is given (§10 Introducing dependencies) and the `package.json` manifests beside it (§2
2
+ // Inputs), so that attribution stays pure: it gets their texts. A missing file is never a usage error: a missing
3
+ // lockfile gives `no-lockfile`, and a missing manifest `importer-unresolved`, and neither affects completeness.
4
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { lockfileManifests } from "./packages/attribution/index.js";
7
+ /** A file's text, or null when it can't be read. */
8
+ function textAt(path) {
9
+ try {
10
+ return readFileSync(path, 'utf8');
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ /**
17
+ * The lockfile input of `lockfile` (as given, relative to `cwd`): its text and, for pnpm and Yarn, the manifests in its
18
+ * directory and in the workspace folders it names, or for Yarn v1 those its own manifest's `workspaces` globs match.
19
+ */
20
+ export function readLockfileInput(cwd, lockfile) {
21
+ const full = resolve(cwd, lockfile.path);
22
+ const text = textAt(full);
23
+ const input = { ...lockfile, text };
24
+ if (lockfileManifests(full, text, null) === null)
25
+ return input;
26
+ const directory = dirname(full);
27
+ const root = textAt(join(directory, 'package.json'));
28
+ const wanted = lockfileManifests(full, text, root) ?? { folders: [], globs: [] };
29
+ const folders = [...new Set([...wanted.folders, ...matchingFolders(directory, wanted.globs)])].sort();
30
+ const manifests = { '': root };
31
+ for (const folder of folders)
32
+ manifests[folder] = textAt(join(directory, folder, 'package.json'));
33
+ return { ...input, manifests };
34
+ }
35
+ /**
36
+ * The folders under `directory` that Yarn v1 `workspaces` globs name: those holding a `package.json` that a pattern
37
+ * matches (`*` within a segment, `**` across any), less those a `!` pattern matches. `node_modules` and dot folders
38
+ * are never entered.
39
+ */
40
+ function matchingFolders(directory, globs) {
41
+ if (globs.length === 0)
42
+ return [];
43
+ const include = globs.filter((glob) => !glob.startsWith('!')).map(pattern);
44
+ const exclude = globs.filter((glob) => glob.startsWith('!')).map((glob) => pattern(glob.slice(1)));
45
+ const out = [];
46
+ const walk = (relative, depth) => {
47
+ if (depth > 8)
48
+ return;
49
+ let entries;
50
+ try {
51
+ entries = readdirSync(join(directory, relative)).sort();
52
+ }
53
+ catch {
54
+ return;
55
+ }
56
+ for (const entry of entries) {
57
+ if (entry === 'node_modules' || entry.startsWith('.'))
58
+ continue;
59
+ const folder = relative === '' ? entry : `${relative}/${entry}`;
60
+ try {
61
+ if (!statSync(join(directory, folder)).isDirectory())
62
+ continue;
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ if (include.some((each) => each.test(folder)) && !exclude.some((each) => each.test(folder))) {
68
+ try {
69
+ if (statSync(join(directory, folder, 'package.json')).isFile())
70
+ out.push(folder);
71
+ }
72
+ catch {
73
+ // No manifest: no workspace.
74
+ }
75
+ }
76
+ walk(folder, depth + 1);
77
+ }
78
+ };
79
+ walk('', 0);
80
+ return out;
81
+ }
82
+ /** A workspace glob as a regular expression over a folder relative to the lockfile's directory. */
83
+ function pattern(glob) {
84
+ const segments = glob.replace(/^\.\//, '').replace(/\/+$/, '').split('/');
85
+ const source = segments
86
+ .map((segment) => segment === '**'
87
+ ? '(?:[^/]+/)*'
88
+ : `${segment.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]')}/`)
89
+ .join('');
90
+ return new RegExp(`^${source.replace(/\/$/, '').replace(/\(\?:\[\^\/\]\+\/\)\*$/, '(?:[^/]+/)*[^/]+')}$`);
91
+ }
@@ -0,0 +1,2 @@
1
+ export { eslintScopeBindings, scopeKinds, type BindingResolver, type DeclarationNode, type IdentifierNode, type OxcProgram, type Resolution, type ScopeKind, } from './lib/binding.ts';
2
+ export { createJsResolver } from './lib/resolver.ts';