@clossys/launcher 0.3.1 → 0.4.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/README.md +1324 -60
- package/contracts/conversation-contract.md +2 -2
- package/contracts/product-ci-workflow.yml +74 -0
- package/contracts/repository-inventory.json +53 -0
- package/dist/admission-fixture.d.ts +168 -0
- package/dist/admission-fixture.d.ts.map +1 -0
- package/dist/admission-fixture.js +453 -0
- package/dist/admission-fixture.js.map +1 -0
- package/dist/admission.d.ts +124 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +799 -0
- package/dist/admission.js.map +1 -0
- package/dist/agents-guide.d.ts +9 -0
- package/dist/agents-guide.d.ts.map +1 -0
- package/dist/agents-guide.js +26 -0
- package/dist/agents-guide.js.map +1 -0
- package/dist/apply-command-options.check.d.ts +12 -0
- package/dist/apply-command-options.check.d.ts.map +1 -0
- package/dist/apply-command-options.check.js +20 -0
- package/dist/apply-command-options.check.js.map +1 -0
- package/dist/apply-plan-cli.d.ts +39 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +432 -15
- package/dist/apply-plan-cli.js.map +1 -1
- package/dist/apply-plan.d.ts +46 -59
- package/dist/apply-plan.d.ts.map +1 -1
- package/dist/apply-plan.js +112 -97
- package/dist/apply-plan.js.map +1 -1
- package/dist/apply-step-fixture.d.ts +87 -0
- package/dist/apply-step-fixture.d.ts.map +1 -0
- package/dist/apply-step-fixture.js +199 -0
- package/dist/apply-step-fixture.js.map +1 -0
- package/dist/apply-store.d.ts +93 -0
- package/dist/apply-store.d.ts.map +1 -0
- package/dist/apply-store.js +625 -0
- package/dist/apply-store.js.map +1 -0
- package/dist/approval-sheet.d.ts +21 -0
- package/dist/approval-sheet.d.ts.map +1 -0
- package/dist/approval-sheet.js +157 -0
- package/dist/approval-sheet.js.map +1 -0
- package/dist/body-command.d.ts +42 -0
- package/dist/body-command.d.ts.map +1 -0
- package/dist/body-command.js +143 -0
- package/dist/body-command.js.map +1 -0
- package/dist/change-set-contract.d.ts +381 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +738 -0
- package/dist/change-set-contract.js.map +1 -0
- package/dist/change-set-digest.d.ts +28 -0
- package/dist/change-set-digest.d.ts.map +1 -0
- package/dist/change-set-digest.js +65 -0
- package/dist/change-set-digest.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +14 -3
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +17 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -23
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +79 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +843 -268
- package/dist/core.js.map +1 -1
- package/dist/dry-materialize.d.ts +63 -0
- package/dist/dry-materialize.d.ts.map +1 -0
- package/dist/dry-materialize.js +330 -0
- package/dist/dry-materialize.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/generated/package-scope.generated.d.ts +6 -0
- package/dist/generated/package-scope.generated.d.ts.map +1 -0
- package/dist/generated/package-scope.generated.js +10 -0
- package/dist/generated/package-scope.generated.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts +3 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
- package/dist/generated/plan-contracts.generated.js +2840 -0
- package/dist/generated/plan-contracts.generated.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +11 -0
- package/dist/host.js.map +1 -1
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +48 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +24 -5
- package/dist/inventory-adoption.d.ts.map +1 -1
- package/dist/inventory-adoption.js +70 -25
- package/dist/inventory-adoption.js.map +1 -1
- package/dist/inventory-choice.d.ts +40 -0
- package/dist/inventory-choice.d.ts.map +1 -0
- package/dist/inventory-choice.js +156 -0
- package/dist/inventory-choice.js.map +1 -0
- package/dist/inventory-contract.d.ts +89 -0
- package/dist/inventory-contract.d.ts.map +1 -0
- package/dist/inventory-contract.js +121 -0
- package/dist/inventory-contract.js.map +1 -0
- package/dist/key-editor.d.ts +30 -0
- package/dist/key-editor.d.ts.map +1 -0
- package/dist/key-editor.js +445 -0
- package/dist/key-editor.js.map +1 -0
- package/dist/ledger-contract.d.ts +187 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +532 -0
- package/dist/ledger-contract.js.map +1 -0
- package/dist/ledger-trust.d.ts +90 -0
- package/dist/ledger-trust.d.ts.map +1 -0
- package/dist/ledger-trust.js +198 -0
- package/dist/ledger-trust.js.map +1 -0
- package/dist/lockfile-invariants.d.ts +48 -0
- package/dist/lockfile-invariants.d.ts.map +1 -0
- package/dist/lockfile-invariants.js +375 -0
- package/dist/lockfile-invariants.js.map +1 -0
- package/dist/lockfile-readers.d.ts +72 -0
- package/dist/lockfile-readers.d.ts.map +1 -0
- package/dist/lockfile-readers.js +713 -0
- package/dist/lockfile-readers.js.map +1 -0
- package/dist/lockfile-regen.d.ts +106 -0
- package/dist/lockfile-regen.d.ts.map +1 -0
- package/dist/lockfile-regen.js +760 -0
- package/dist/lockfile-regen.js.map +1 -0
- package/dist/lockfile-tool-env.d.ts +29 -0
- package/dist/lockfile-tool-env.d.ts.map +1 -0
- package/dist/lockfile-tool-env.js +111 -0
- package/dist/lockfile-tool-env.js.map +1 -0
- package/dist/materialize.d.ts +113 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +840 -0
- package/dist/materialize.js.map +1 -0
- package/dist/observe-repository.d.ts +90 -0
- package/dist/observe-repository.d.ts.map +1 -0
- package/dist/observe-repository.js +1367 -0
- package/dist/observe-repository.js.map +1 -0
- package/dist/plan-bundle-setup-fixture.d.ts +68 -0
- package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
- package/dist/plan-bundle-setup-fixture.js +167 -0
- package/dist/plan-bundle-setup-fixture.js.map +1 -0
- package/dist/plan-bundle.d.ts +250 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +827 -0
- package/dist/plan-bundle.js.map +1 -0
- package/dist/plan-command.d.ts +29 -0
- package/dist/plan-command.d.ts.map +1 -0
- package/dist/plan-command.js +493 -0
- package/dist/plan-command.js.map +1 -0
- package/dist/plan-contract.d.ts +153 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +61 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +25 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +106 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +23 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +177 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/planned-bundle.d.ts +20 -0
- package/dist/planned-bundle.d.ts.map +1 -0
- package/dist/planned-bundle.js +191 -0
- package/dist/planned-bundle.js.map +1 -0
- package/dist/product-repository.d.ts +4 -0
- package/dist/product-repository.d.ts.map +1 -1
- package/dist/product-repository.js +9 -1
- package/dist/product-repository.js.map +1 -1
- package/dist/provenance-gate.d.ts +48 -0
- package/dist/provenance-gate.d.ts.map +1 -0
- package/dist/provenance-gate.js +324 -0
- package/dist/provenance-gate.js.map +1 -0
- package/dist/pull-request-body.d.ts +45 -0
- package/dist/pull-request-body.d.ts.map +1 -0
- package/dist/pull-request-body.js +232 -0
- package/dist/pull-request-body.js.map +1 -0
- package/dist/registry-snapshot.d.ts +141 -0
- package/dist/registry-snapshot.d.ts.map +1 -0
- package/dist/registry-snapshot.js +483 -0
- package/dist/registry-snapshot.js.map +1 -0
- package/dist/release-age-edit.d.ts +52 -0
- package/dist/release-age-edit.d.ts.map +1 -0
- package/dist/release-age-edit.js +413 -0
- package/dist/release-age-edit.js.map +1 -0
- package/dist/root-entries.d.ts +36 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +80 -0
- package/dist/root-entries.js.map +1 -0
- package/dist/setup-template-scripts.d.ts +34 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +557 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +54 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +427 -0
- package/dist/setup-templates.js.map +1 -0
- package/dist/skills.d.ts +34 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +129 -17
- package/dist/skills.js.map +1 -1
- package/dist/status.d.ts +63 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +539 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +151 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +23 -7
- package/skill-catalogue/advisor/SKILL.md +59 -6
- package/skill-catalogue/architect/SKILL.md +2 -2
- package/skill-catalogue/bouncer/SKILL.md +2 -2
- package/skill-catalogue/builder/SKILL.md +2 -2
- package/skill-catalogue/butler/SKILL.md +2 -2
- package/skill-catalogue/controller/SKILL.md +2 -2
- package/skill-catalogue/customer/SKILL.md +2 -2
- package/skill-catalogue/designer/SKILL.md +4 -2
- package/skill-catalogue/giver/SKILL.md +2 -2
- package/skill-catalogue/influencer/SKILL.md +2 -2
- package/skill-catalogue/inspector/SKILL.md +2 -2
- package/skill-catalogue/integrator/SKILL.md +2 -2
- package/skill-catalogue/keeper/SKILL.md +2 -2
- package/skill-catalogue/launcher/SKILL.md +23 -7
- package/skill-catalogue/locksmith/SKILL.md +2 -2
- package/skill-catalogue/messenger/SKILL.md +2 -2
- package/skill-catalogue/observer/SKILL.md +2 -2
- package/skill-catalogue/publisher/SKILL.md +2 -2
- package/skill-catalogue/starter/SKILL.md +3 -2
- package/skill-catalogue/strategist/SKILL.md +12 -4
- package/skill-catalogue/writer/SKILL.md +2 -2
- package/src/admission-fixture.ts +572 -0
- package/src/admission.ts +816 -0
- package/src/agents-guide.ts +29 -0
- package/src/apply-command-options.check.ts +27 -0
- package/src/apply-plan-cli.ts +454 -14
- package/src/apply-plan.ts +112 -124
- package/src/apply-step-fixture.ts +236 -0
- package/src/apply-store.ts +584 -0
- package/src/approval-sheet.ts +164 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +937 -0
- package/src/change-set-digest.ts +70 -0
- package/src/check-cli.ts +14 -3
- package/src/cli.ts +90 -22
- package/src/core.ts +973 -275
- package/src/dry-materialize.ts +353 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +2840 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +72 -3
- package/src/inventory-adoption.ts +107 -29
- package/src/inventory-choice.ts +172 -0
- package/src/inventory-contract.ts +166 -0
- package/src/key-editor.ts +446 -0
- package/src/ledger-contract.ts +637 -0
- package/src/ledger-trust.ts +267 -0
- package/src/lockfile-invariants.ts +421 -0
- package/src/lockfile-readers.ts +749 -0
- package/src/lockfile-regen.ts +851 -0
- package/src/lockfile-tool-env.ts +131 -0
- package/src/materialize.ts +886 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +964 -0
- package/src/plan-command.ts +509 -0
- package/src/plan-contract.ts +179 -0
- package/src/plan-digest.ts +102 -0
- package/src/plan-rules.ts +188 -0
- package/src/planned-bundle.ts +211 -0
- package/src/product-repository.ts +10 -1
- package/src/provenance-gate.ts +352 -0
- package/src/pull-request-body.ts +261 -0
- package/src/registry-snapshot.ts +534 -0
- package/src/release-age-edit.ts +430 -0
- package/src/root-entries.ts +81 -0
- package/src/setup-template-scripts.ts +571 -0
- package/src/setup-templates.ts +471 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import { compareCodeUnits, compareTuples } from "./change-set-contract.js";
|
|
2
|
+
|
|
3
|
+
/** Lockfile formats this module can read. Yarn is not one of them (RFC section 4.3, Yarn note). */
|
|
4
|
+
export type LockfileFormat = "npm" | "pnpm";
|
|
5
|
+
|
|
6
|
+
/** The root placements the invariants inspect. npm's `peerDependencies` is read for npm only. */
|
|
7
|
+
export type RootPlacement = "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
|
|
8
|
+
|
|
9
|
+
/** One direct dependency of the repository root. */
|
|
10
|
+
export interface RootDependency {
|
|
11
|
+
readonly placement: RootPlacement;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** npm: `packages[""][placement][name]`; pnpm: `importers["."][placement][name].specifier`. */
|
|
14
|
+
readonly specifier: string;
|
|
15
|
+
/** npm: `packages["node_modules/<name>"].version`; pnpm: the `version` text before any `(` peer suffix. Null when absent. */
|
|
16
|
+
readonly version: string | null;
|
|
17
|
+
/** pnpm: the full `version` field text, before any peer-suffix stripping (fix 8: a `(patch_hash=...)` group must survive here even though `version` strips it). Always equal to `version` for npm. */
|
|
18
|
+
readonly rawVersion: string | null;
|
|
19
|
+
/** npm: `packages["node_modules/<name>"].integrity`; pnpm: `packages["<name>@<version>"].resolution.integrity`. Null when absent. */
|
|
20
|
+
readonly integrity: string | null;
|
|
21
|
+
/** npm: the `node_modules/<name>` entry has `link: true`; pnpm: `version` starts `link:` or `file:`. */
|
|
22
|
+
readonly link: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One resolved package entry, transitive included. */
|
|
26
|
+
export interface LockfileEntry {
|
|
27
|
+
/** npm: the `packages` path (`node_modules/a/node_modules/b`); pnpm: the `packages` key (`name@version`). */
|
|
28
|
+
readonly key: string;
|
|
29
|
+
/** The package's declared identity: npm `value.name` (falling back to the installed name); pnpm the `packages` key's name. */
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/** The name actually installed at `key` (fix 6): npm the text after the key's last `node_modules/`, regardless of a mismatched declared `name` (an alias); always equal to `name` for pnpm, which has no per-entry alias concept. */
|
|
32
|
+
readonly installedName: string;
|
|
33
|
+
/** Null for an npm link entry with no `version` field (fix 7: link entries are kept, not skipped). Always a string for pnpm. */
|
|
34
|
+
readonly version: string | null;
|
|
35
|
+
readonly integrity: string | null;
|
|
36
|
+
/** npm: `resolved` (also the link target for a link entry); pnpm: `resolution.tarball`. Null when absent. */
|
|
37
|
+
readonly tarball: string | null;
|
|
38
|
+
/** npm: `link: true`. Always false for pnpm. */
|
|
39
|
+
readonly link: boolean;
|
|
40
|
+
/** pnpm: every `resolution` key other than `integrity` and `tarball` (for example `type`, `directory`, `repo`, `commit`), sorted. Always empty for npm. */
|
|
41
|
+
readonly otherResolutionKeys: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One dependency reference inside an importer's or a snapshot's dependency map (fix 6, pnpm only). */
|
|
45
|
+
export interface LockfileDependencyRef {
|
|
46
|
+
/** Where this reference was read, for comparing the same location between base and regenerated -- for example `importers['.'].dependencies` or `snapshots['a@1.0.0'].dependencies`. */
|
|
47
|
+
readonly place: string;
|
|
48
|
+
/** The dependency's installed name (the map key). */
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/** The raw, unquoted `version` field text: a plain version (optionally with a `(...)` peer suffix) when not aliased, or `<other name>@<version>`, `link:...` or `file:...` when it is. */
|
|
51
|
+
readonly value: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The normalized view of one lockfile. */
|
|
55
|
+
export interface LockfileView {
|
|
56
|
+
readonly format: LockfileFormat;
|
|
57
|
+
/** npm: the number; pnpm: the string. */
|
|
58
|
+
readonly lockfileVersion: number | string;
|
|
59
|
+
/** Root direct dependencies, sorted by (placement, name) with compareCodeUnits. */
|
|
60
|
+
readonly root: readonly RootDependency[];
|
|
61
|
+
/** Every package entry, sorted by key with compareCodeUnits. */
|
|
62
|
+
readonly entries: readonly LockfileEntry[];
|
|
63
|
+
/** Every importer's and snapshot's dependency reference (fix 6). Always empty for npm. */
|
|
64
|
+
readonly dependencyRefs: readonly LockfileDependencyRef[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type LockfileUnreadableReason = "lockfile-format-unsupported" | "lockfile-unreadable";
|
|
68
|
+
|
|
69
|
+
export interface LockfileUnreadable {
|
|
70
|
+
readonly unreadable: true;
|
|
71
|
+
readonly reason: LockfileUnreadableReason;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type LockfileReadResult = LockfileView | LockfileUnreadable;
|
|
75
|
+
|
|
76
|
+
export function isUnreadable(result: LockfileReadResult): result is LockfileUnreadable {
|
|
77
|
+
return "unreadable" in result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function unreadableResult(reason: LockfileUnreadableReason): LockfileUnreadable {
|
|
81
|
+
return { unreadable: true, reason };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ROOT_PLACEMENTS: readonly RootPlacement[] = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// npm (package-lock.json): a strict JSON parser (JSON.parse silently accepts
|
|
88
|
+
// duplicate object keys, keeping the last -- this reader must refuse them),
|
|
89
|
+
// then a thin reading of the shape lockfileVersion 2/3 `packages` map holds.
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
class JsonSyntaxError extends Error {}
|
|
93
|
+
|
|
94
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
95
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A JSON parser that, unlike `JSON.parse`, refuses a document with a duplicate key in any object, anywhere in the tree. Throws `JsonSyntaxError` on anything else invalid JSON too. */
|
|
99
|
+
function parseStrictJson(source: string): unknown {
|
|
100
|
+
let i = 0;
|
|
101
|
+
const n = source.length;
|
|
102
|
+
|
|
103
|
+
function fail(message: string): never {
|
|
104
|
+
throw new JsonSyntaxError(message);
|
|
105
|
+
}
|
|
106
|
+
const isDigit = (c: string | undefined): boolean => c !== undefined && c >= "0" && c <= "9";
|
|
107
|
+
|
|
108
|
+
const skipWhitespace = (): void => {
|
|
109
|
+
for (;;) {
|
|
110
|
+
const c = source[i];
|
|
111
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") i += 1;
|
|
112
|
+
else break;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const parseLiteral = (literal: string, value: unknown): unknown => {
|
|
117
|
+
if (source.slice(i, i + literal.length) !== literal) fail(`expected "${literal}"`);
|
|
118
|
+
i += literal.length;
|
|
119
|
+
return value;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const parseNumber = (): number => {
|
|
123
|
+
const start = i;
|
|
124
|
+
if (source[i] === "-") i += 1;
|
|
125
|
+
if (source[i] === "0") {
|
|
126
|
+
i += 1;
|
|
127
|
+
} else if (isDigit(source[i])) {
|
|
128
|
+
i += 1;
|
|
129
|
+
while (isDigit(source[i])) i += 1;
|
|
130
|
+
} else {
|
|
131
|
+
fail("invalid number");
|
|
132
|
+
}
|
|
133
|
+
if (source[i] === ".") {
|
|
134
|
+
i += 1;
|
|
135
|
+
if (!isDigit(source[i])) fail("invalid number");
|
|
136
|
+
while (isDigit(source[i])) i += 1;
|
|
137
|
+
}
|
|
138
|
+
if (source[i] === "e" || source[i] === "E") {
|
|
139
|
+
i += 1;
|
|
140
|
+
const sign = source[i];
|
|
141
|
+
if (sign === "+" || sign === "-") i += 1;
|
|
142
|
+
if (!isDigit(source[i])) fail("invalid number");
|
|
143
|
+
while (isDigit(source[i])) i += 1;
|
|
144
|
+
}
|
|
145
|
+
return Number(source.slice(start, i));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const parseString = (): string => {
|
|
149
|
+
if (source[i] !== '"') fail("expected a string");
|
|
150
|
+
i += 1;
|
|
151
|
+
let out = "";
|
|
152
|
+
for (;;) {
|
|
153
|
+
const c = source[i];
|
|
154
|
+
if (c === undefined) fail("unterminated string");
|
|
155
|
+
if (c === '"') {
|
|
156
|
+
i += 1;
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
if (c === "\\") {
|
|
160
|
+
const esc = source[i + 1];
|
|
161
|
+
if (esc === undefined) fail("unterminated escape");
|
|
162
|
+
i += 2;
|
|
163
|
+
switch (esc) {
|
|
164
|
+
case '"':
|
|
165
|
+
out += '"';
|
|
166
|
+
break;
|
|
167
|
+
case "\\":
|
|
168
|
+
out += "\\";
|
|
169
|
+
break;
|
|
170
|
+
case "/":
|
|
171
|
+
out += "/";
|
|
172
|
+
break;
|
|
173
|
+
case "b":
|
|
174
|
+
out += "\b";
|
|
175
|
+
break;
|
|
176
|
+
case "f":
|
|
177
|
+
out += "\f";
|
|
178
|
+
break;
|
|
179
|
+
case "n":
|
|
180
|
+
out += "\n";
|
|
181
|
+
break;
|
|
182
|
+
case "r":
|
|
183
|
+
out += "\r";
|
|
184
|
+
break;
|
|
185
|
+
case "t":
|
|
186
|
+
out += "\t";
|
|
187
|
+
break;
|
|
188
|
+
case "u": {
|
|
189
|
+
const hex = source.slice(i, i + 4);
|
|
190
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail("invalid unicode escape");
|
|
191
|
+
out += String.fromCharCode(Number.parseInt(hex, 16));
|
|
192
|
+
i += 4;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
fail("invalid escape sequence");
|
|
197
|
+
}
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (c.charCodeAt(0) < 0x20) fail("control character in string");
|
|
201
|
+
out += c;
|
|
202
|
+
i += 1;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const parseObject = (): Record<string, unknown> => {
|
|
207
|
+
i += 1; // consume "{"
|
|
208
|
+
const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
209
|
+
const seen = new Set<string>();
|
|
210
|
+
skipWhitespace();
|
|
211
|
+
if (source[i] === "}") {
|
|
212
|
+
i += 1;
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
for (;;) {
|
|
216
|
+
skipWhitespace();
|
|
217
|
+
const key = parseString();
|
|
218
|
+
if (seen.has(key)) fail(`duplicate key "${key}"`);
|
|
219
|
+
seen.add(key);
|
|
220
|
+
skipWhitespace();
|
|
221
|
+
if (source[i] !== ":") fail('expected ":"');
|
|
222
|
+
i += 1;
|
|
223
|
+
result[key] = parseValue();
|
|
224
|
+
skipWhitespace();
|
|
225
|
+
if (source[i] === ",") {
|
|
226
|
+
i += 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (source[i] === "}") {
|
|
230
|
+
i += 1;
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
fail('expected "," or "}"');
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const parseArray = (): unknown[] => {
|
|
238
|
+
i += 1; // consume "["
|
|
239
|
+
const result: unknown[] = [];
|
|
240
|
+
skipWhitespace();
|
|
241
|
+
if (source[i] === "]") {
|
|
242
|
+
i += 1;
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
for (;;) {
|
|
246
|
+
result.push(parseValue());
|
|
247
|
+
skipWhitespace();
|
|
248
|
+
if (source[i] === ",") {
|
|
249
|
+
i += 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (source[i] === "]") {
|
|
253
|
+
i += 1;
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
fail('expected "," or "]"');
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
function parseValue(): unknown {
|
|
261
|
+
skipWhitespace();
|
|
262
|
+
const c = source[i];
|
|
263
|
+
if (c === undefined) fail("unexpected end of input");
|
|
264
|
+
if (c === "{") return parseObject();
|
|
265
|
+
if (c === "[") return parseArray();
|
|
266
|
+
if (c === '"') return parseString();
|
|
267
|
+
if (c === "t") return parseLiteral("true", true);
|
|
268
|
+
if (c === "f") return parseLiteral("false", false);
|
|
269
|
+
if (c === "n") return parseLiteral("null", null);
|
|
270
|
+
if (c === "-" || isDigit(c)) return parseNumber();
|
|
271
|
+
fail(`unexpected character "${c}"`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const value = parseValue();
|
|
275
|
+
skipWhitespace();
|
|
276
|
+
if (i !== n) fail("trailing content after the JSON value");
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Reads a `package-lock.json` of lockfileVersion 2 or 3. Never throws. */
|
|
281
|
+
export function readNpmLockfile(text: string): LockfileReadResult {
|
|
282
|
+
try {
|
|
283
|
+
let parsed: unknown;
|
|
284
|
+
try {
|
|
285
|
+
parsed = parseStrictJson(text);
|
|
286
|
+
} catch {
|
|
287
|
+
return unreadableResult("lockfile-unreadable");
|
|
288
|
+
}
|
|
289
|
+
if (!isPlainObject(parsed)) return unreadableResult("lockfile-unreadable");
|
|
290
|
+
|
|
291
|
+
if (!("lockfileVersion" in parsed)) return unreadableResult("lockfile-unreadable");
|
|
292
|
+
const lockfileVersion = parsed["lockfileVersion"];
|
|
293
|
+
if (lockfileVersion !== 2 && lockfileVersion !== 3) return unreadableResult("lockfile-format-unsupported");
|
|
294
|
+
|
|
295
|
+
const packages = parsed["packages"];
|
|
296
|
+
if (!isPlainObject(packages)) return unreadableResult("lockfile-unreadable");
|
|
297
|
+
const rootEntry = packages[""];
|
|
298
|
+
if (!isPlainObject(rootEntry)) return unreadableResult("lockfile-unreadable");
|
|
299
|
+
|
|
300
|
+
const root: RootDependency[] = [];
|
|
301
|
+
for (const placement of ROOT_PLACEMENTS) {
|
|
302
|
+
const block = rootEntry[placement];
|
|
303
|
+
if (block === undefined) continue;
|
|
304
|
+
if (!isPlainObject(block)) return unreadableResult("lockfile-unreadable");
|
|
305
|
+
for (const name of Object.keys(block)) {
|
|
306
|
+
const specifier = block[name];
|
|
307
|
+
if (typeof specifier !== "string") return unreadableResult("lockfile-unreadable");
|
|
308
|
+
const installed = packages[`node_modules/${name}`];
|
|
309
|
+
let version: string | null = null;
|
|
310
|
+
let integrity: string | null = null;
|
|
311
|
+
let link = false;
|
|
312
|
+
if (isPlainObject(installed)) {
|
|
313
|
+
version = typeof installed["version"] === "string" ? (installed["version"] as string) : null;
|
|
314
|
+
integrity = typeof installed["integrity"] === "string" ? (installed["integrity"] as string) : null;
|
|
315
|
+
link = installed["link"] === true;
|
|
316
|
+
}
|
|
317
|
+
root.push({ placement, name, specifier, version, rawVersion: version, integrity, link });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const entries: LockfileEntry[] = [];
|
|
322
|
+
const marker = "node_modules/";
|
|
323
|
+
for (const key of Object.keys(packages)) {
|
|
324
|
+
if (key === "") continue;
|
|
325
|
+
if (!key.startsWith(marker) && !key.includes(`/${marker}`)) continue; // a workspace member path, not an entry
|
|
326
|
+
const value = packages[key];
|
|
327
|
+
if (!isPlainObject(value)) continue;
|
|
328
|
+
const rawVersion = value["version"];
|
|
329
|
+
if (rawVersion !== undefined && typeof rawVersion !== "string") return unreadableResult("lockfile-unreadable");
|
|
330
|
+
const link = value["link"] === true;
|
|
331
|
+
const version = typeof rawVersion === "string" ? rawVersion : null;
|
|
332
|
+
if (version === null && !link) continue; // fix 7: keep a link entry even with no version; skip anything else unversioned
|
|
333
|
+
const installedName = key.slice(key.lastIndexOf(marker) + marker.length);
|
|
334
|
+
const name = typeof value["name"] === "string" ? (value["name"] as string) : installedName;
|
|
335
|
+
const integrity = typeof value["integrity"] === "string" ? (value["integrity"] as string) : null;
|
|
336
|
+
const tarball = typeof value["resolved"] === "string" ? (value["resolved"] as string) : null;
|
|
337
|
+
entries.push({ key, name, installedName, version, integrity, tarball, link, otherResolutionKeys: [] });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
root.sort((a, b) => compareTuples([a.placement, a.name], [b.placement, b.name]));
|
|
341
|
+
entries.sort((a, b) => compareCodeUnits(a.key, b.key));
|
|
342
|
+
|
|
343
|
+
return { format: "npm", lockfileVersion, root, entries, dependencyRefs: [] };
|
|
344
|
+
} catch {
|
|
345
|
+
return unreadableResult("lockfile-unreadable");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// pnpm (pnpm-lock.yaml): a restricted, purpose-built reader for exactly the
|
|
351
|
+
// importers/packages block-mapping shape pnpm 9/10 write for lockfileVersion
|
|
352
|
+
// '9.0'. Not a general YAML parser -- see the module doc comment above the
|
|
353
|
+
// reader for exactly what subset is supported and what makes a document
|
|
354
|
+
// unreadable. Precedent for style: packages/integrator's pnpm-lock.yaml
|
|
355
|
+
// reader, which this module does not import (a different package, and this
|
|
356
|
+
// module's needs -- resolution flow mappings, forbidden-construct detection
|
|
357
|
+
// -- are wider than that one's).
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
class LockfileFailure extends Error {
|
|
361
|
+
constructor(readonly reason: LockfileUnreadableReason) {
|
|
362
|
+
super(reason);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function fail(reason: LockfileUnreadableReason = "lockfile-unreadable"): never {
|
|
367
|
+
throw new LockfileFailure(reason);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function isBlankOrCommentLine(line: string): boolean {
|
|
371
|
+
const trimmed = line.trim();
|
|
372
|
+
return trimmed.length === 0 || trimmed.startsWith("#");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function indentOf(line: string): number {
|
|
376
|
+
return line.length - line.trimStart().length;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Strips one layer of single- or double-quoting. A single-quoted scalar decodes `''` to one `'`. A
|
|
381
|
+
* double-quoted scalar is refused outright the moment it contains a `\` (fix 9): this reader never
|
|
382
|
+
* interprets an escape (pnpm itself always writes single quotes), so it cannot tell a plain character
|
|
383
|
+
* from one hidden behind an escape sequence such as `\x66` or `\"`. Leaves a plain (unquoted) scalar as is.
|
|
384
|
+
*/
|
|
385
|
+
function unquoteYamlScalar(value: string): string {
|
|
386
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replace(/''/g, "'");
|
|
387
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
388
|
+
const inner = value.slice(1, -1);
|
|
389
|
+
if (inner.includes("\\")) fail();
|
|
390
|
+
return inner;
|
|
391
|
+
}
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The index of the colon separating a (possibly quoted) mapping key from its value on one trimmed line, or
|
|
397
|
+
* undefined when the line has none. For an unquoted key (fix 9), the separating colon is the first one
|
|
398
|
+
* followed by a space or the end of the line -- not simply the first colon anywhere, which a key holding
|
|
399
|
+
* its own colon (a URL such as `foo@https://host/path:`) would otherwise split in the wrong place.
|
|
400
|
+
*/
|
|
401
|
+
function findKeyColon(trimmedLine: string): number | undefined {
|
|
402
|
+
const first = trimmedLine[0];
|
|
403
|
+
if (first === "'" || first === '"') {
|
|
404
|
+
const quote = first;
|
|
405
|
+
let end = 1;
|
|
406
|
+
while (end < trimmedLine.length) {
|
|
407
|
+
if (trimmedLine[end] === quote) {
|
|
408
|
+
if (quote === "'" && trimmedLine[end + 1] === "'") {
|
|
409
|
+
end += 2;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
end += 1;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
end += 1;
|
|
416
|
+
}
|
|
417
|
+
const colonRelative = trimmedLine.slice(end).indexOf(":");
|
|
418
|
+
return colonRelative === -1 ? undefined : end + colonRelative;
|
|
419
|
+
}
|
|
420
|
+
for (let i = 0; i < trimmedLine.length; i += 1) {
|
|
421
|
+
if (trimmedLine[i] === ":" && (i + 1 === trimmedLine.length || trimmedLine[i + 1] === " ")) return i;
|
|
422
|
+
}
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Whether a value (or, on a keyless line, the whole line) begins, unquoted, with a YAML construct this reader refuses: an anchor, alias, tag, or block-scalar indicator. */
|
|
427
|
+
function hasForbiddenPlainStart(value: string): boolean {
|
|
428
|
+
const c = value[0];
|
|
429
|
+
return c === "&" || c === "*" || c === "!" || c === "|" || c === ">";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Fails on any anchor (`&name`), alias (`*name`), tag (`!`/`!!`), block
|
|
434
|
+
* scalar indicator (`|`/`>`), document marker (`---`/`...`), explicit key
|
|
435
|
+
* (`? `), merge key (`<<`), or tab used for indentation -- wherever in the
|
|
436
|
+
* file it appears, including inside sections this reader otherwise
|
|
437
|
+
* tolerates and never structurally parses (`settings`, a package's
|
|
438
|
+
* `engines`/`cpu`/`os`, and so on). Fix 9 widens this to a mapping *key*
|
|
439
|
+
* too, not only its value: a key spelled `&a`, `!tag`, `*alias`, `? ` or
|
|
440
|
+
* `<<` must refuse the file outright rather than silently fail to match
|
|
441
|
+
* the structural key (`importers`, `packages`, ...) it was disguising as,
|
|
442
|
+
* which would let that section read as merely absent.
|
|
443
|
+
*/
|
|
444
|
+
function scanForForbiddenYamlConstructs(lines: readonly string[]): void {
|
|
445
|
+
for (const line of lines) {
|
|
446
|
+
if (isBlankOrCommentLine(line)) continue;
|
|
447
|
+
if (line.slice(0, indentOf(line)).includes("\t")) fail();
|
|
448
|
+
let trimmed = line.trim();
|
|
449
|
+
if (trimmed === "---" || trimmed === "..." || trimmed.startsWith("--- ") || trimmed.startsWith("... ")) fail();
|
|
450
|
+
if (trimmed.startsWith("- ")) trimmed = trimmed.slice(2).trim();
|
|
451
|
+
else if (trimmed === "-") trimmed = "";
|
|
452
|
+
if (trimmed.startsWith("? ") || trimmed === "?") fail();
|
|
453
|
+
const colon = findKeyColon(trimmed);
|
|
454
|
+
if (colon !== undefined) {
|
|
455
|
+
const rawKey = trimmed.slice(0, colon);
|
|
456
|
+
if (rawKey === "<<") fail();
|
|
457
|
+
const keyFirst = rawKey[0];
|
|
458
|
+
if (keyFirst === "&" || keyFirst === "!" || keyFirst === "*") fail();
|
|
459
|
+
}
|
|
460
|
+
const value = colon === undefined ? trimmed : trimmed.slice(colon + 1).trim();
|
|
461
|
+
if (value.length > 0 && hasForbiddenPlainStart(value)) fail();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** One key's position within a parsed block-mapping level: its inline value (when the value sat on the key's own line) and the line range of its nested body (possibly empty), left unparsed until a caller recurses into it. */
|
|
466
|
+
interface MappingEntry {
|
|
467
|
+
readonly inlineValue: string | undefined;
|
|
468
|
+
readonly bodyStart: number;
|
|
469
|
+
readonly bodyEnd: number;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Parses exactly one level of a block mapping over `lines[start, end)`,
|
|
474
|
+
* whose entries all share `indent`. Does not recurse: a key's own nested
|
|
475
|
+
* content is only located by indentation here, never interpreted -- the
|
|
476
|
+
* caller parses a level's body only when it actually needs to read inside
|
|
477
|
+
* it, which is how this reader tolerates arbitrarily-shaped content it does
|
|
478
|
+
* not otherwise understand (a block sequence, deeper nesting, ...) in a
|
|
479
|
+
* section it does not read. Fails on a key duplicated at this level, a
|
|
480
|
+
* sequence item, or a line with no `key:`.
|
|
481
|
+
*/
|
|
482
|
+
function parseMappingLevel(lines: readonly string[], start: number, end: number, indent: number): Map<string, MappingEntry> {
|
|
483
|
+
const result = new Map<string, MappingEntry>();
|
|
484
|
+
let i = start;
|
|
485
|
+
while (i < end) {
|
|
486
|
+
const line = lines[i] as string;
|
|
487
|
+
if (isBlankOrCommentLine(line)) {
|
|
488
|
+
i += 1;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (indentOf(line) !== indent) fail();
|
|
492
|
+
const trimmed = line.trim();
|
|
493
|
+
if (trimmed.startsWith("- ") || trimmed === "-") fail();
|
|
494
|
+
const colon = findKeyColon(trimmed);
|
|
495
|
+
if (colon === undefined) fail();
|
|
496
|
+
const key = unquoteYamlScalar(trimmed.slice(0, colon).trim());
|
|
497
|
+
const rest = trimmed.slice(colon + 1).trim();
|
|
498
|
+
if (result.has(key)) fail();
|
|
499
|
+
let j = i + 1;
|
|
500
|
+
while (j < end) {
|
|
501
|
+
const next = lines[j] as string;
|
|
502
|
+
if (next.trim().length > 0 && indentOf(next) <= indent) break;
|
|
503
|
+
j += 1;
|
|
504
|
+
}
|
|
505
|
+
result.set(key, { inlineValue: rest.length > 0 ? rest : undefined, bodyStart: i + 1, bodyEnd: j });
|
|
506
|
+
i = j;
|
|
507
|
+
}
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** The indentation of the first non-blank, non-comment line in `lines[start, end)`, or undefined when the range holds none. */
|
|
512
|
+
function peekIndent(lines: readonly string[], start: number, end: number): number | undefined {
|
|
513
|
+
for (let i = start; i < end; i += 1) {
|
|
514
|
+
const line = lines[i] as string;
|
|
515
|
+
if (isBlankOrCommentLine(line)) continue;
|
|
516
|
+
return indentOf(line);
|
|
517
|
+
}
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Parses a one-level flow mapping (`{k: v, k2: v2}`) of plain or quoted scalars. Fails on nesting (`{`, `}`, `[`, `]` in a bare value) or anything else the syntax does not allow. */
|
|
522
|
+
function parseFlowMapping(raw: string): Map<string, string> {
|
|
523
|
+
const text = raw.trim();
|
|
524
|
+
if (!text.startsWith("{") || !text.endsWith("}")) fail();
|
|
525
|
+
const inner = text.slice(1, -1).trim();
|
|
526
|
+
const result = new Map<string, string>();
|
|
527
|
+
if (inner.length === 0) return result;
|
|
528
|
+
|
|
529
|
+
let i = 0;
|
|
530
|
+
const n = inner.length;
|
|
531
|
+
const skipWs = (): void => {
|
|
532
|
+
while (i < n && (inner[i] === " " || inner[i] === "\t")) i += 1;
|
|
533
|
+
};
|
|
534
|
+
const readQuoted = (): string => {
|
|
535
|
+
const quote = inner[i] as string;
|
|
536
|
+
i += 1;
|
|
537
|
+
let out = "";
|
|
538
|
+
while (i < n) {
|
|
539
|
+
const c = inner[i] as string;
|
|
540
|
+
// fix 9: a double-quoted scalar (key or value) refuses outright the moment it holds a `\` --
|
|
541
|
+
// this reader never interprets an escape, and pnpm never writes double quotes anyway.
|
|
542
|
+
if (quote === '"' && c === "\\") fail();
|
|
543
|
+
if (c === quote) {
|
|
544
|
+
if (quote === "'" && inner[i + 1] === "'") {
|
|
545
|
+
out += "'";
|
|
546
|
+
i += 2;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
i += 1;
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
out += c;
|
|
553
|
+
i += 1;
|
|
554
|
+
}
|
|
555
|
+
return fail();
|
|
556
|
+
};
|
|
557
|
+
// A bare value can itself contain `:` (a URL's scheme, say), so only the
|
|
558
|
+
// key reader stops there; the value reader stops only at the next `,`.
|
|
559
|
+
const readBare = (stop: string): string => {
|
|
560
|
+
const start = i;
|
|
561
|
+
while (i < n && inner[i] !== stop) i += 1;
|
|
562
|
+
const value = inner.slice(start, i).trim();
|
|
563
|
+
if (/[{}[\]]/.test(value)) fail();
|
|
564
|
+
return value;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
for (;;) {
|
|
568
|
+
skipWs();
|
|
569
|
+
if (i >= n) fail();
|
|
570
|
+
const kc = inner[i];
|
|
571
|
+
const key = kc === "'" || kc === '"' ? readQuoted() : readBare(":");
|
|
572
|
+
skipWs();
|
|
573
|
+
if (inner[i] !== ":") fail();
|
|
574
|
+
i += 1;
|
|
575
|
+
skipWs();
|
|
576
|
+
const vc = inner[i];
|
|
577
|
+
const value = vc === "'" || vc === '"' ? readQuoted() : readBare(",");
|
|
578
|
+
if (result.has(key)) fail();
|
|
579
|
+
result.set(key, value);
|
|
580
|
+
skipWs();
|
|
581
|
+
if (i >= n) break;
|
|
582
|
+
if (inner[i] === ",") {
|
|
583
|
+
i += 1;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
fail();
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Splits a `packages` key at its last `@`, the boundary pnpm uses between a (possibly scoped) name and its version. Undefined when there is no such `@` (not at index 0, so a scoped name's own leading `@` is never mistaken for it). */
|
|
592
|
+
function splitPackageKey(key: string): { readonly name: string; readonly version: string } | undefined {
|
|
593
|
+
for (let i = key.length - 1; i > 0; i -= 1) {
|
|
594
|
+
if (key[i] === "@") return { name: key.slice(0, i), version: key.slice(i + 1) };
|
|
595
|
+
}
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const PNPM_ROOT_PLACEMENTS = ["dependencies", "devDependencies", "optionalDependencies"] as const;
|
|
600
|
+
|
|
601
|
+
const SNAPSHOT_PLACEMENTS = ["dependencies", "optionalDependencies", "devDependencies"] as const;
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Refuses a structural key's inline value unless it is absent or exactly `{}` (fix 9). The structural keys
|
|
605
|
+
* are `importers`, `packages`, `snapshots`, an importer such as `.`, a placement block, a root dependency,
|
|
606
|
+
* and a package or snapshot entry's field block: each of these must be read as a nested block, never
|
|
607
|
+
* silently skipped just because its value sat inline as a flow mapping this reader does not expect there
|
|
608
|
+
* (which would otherwise let the whole section read as merely absent, switching an invariant off).
|
|
609
|
+
*/
|
|
610
|
+
function requireStructuralInline(entry: MappingEntry | undefined): void {
|
|
611
|
+
if (entry !== undefined && entry.inlineValue !== undefined && entry.inlineValue !== "{}") fail();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function readPnpmLockfileInner(text: string): LockfileReadResult {
|
|
615
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
616
|
+
scanForForbiddenYamlConstructs(lines);
|
|
617
|
+
|
|
618
|
+
const top = parseMappingLevel(lines, 0, lines.length, 0);
|
|
619
|
+
|
|
620
|
+
const versionEntry = top.get("lockfileVersion");
|
|
621
|
+
if (versionEntry === undefined || versionEntry.inlineValue === undefined) fail("lockfile-unreadable");
|
|
622
|
+
const lockfileVersion = unquoteYamlScalar(versionEntry.inlineValue);
|
|
623
|
+
if (lockfileVersion !== "9.0") fail("lockfile-format-unsupported");
|
|
624
|
+
|
|
625
|
+
// `packages` first: a root dependency's integrity is looked up from it.
|
|
626
|
+
const packageLookup = new Map<string, { readonly integrity: string | null }>();
|
|
627
|
+
const entries: LockfileEntry[] = [];
|
|
628
|
+
const packagesEntry = top.get("packages");
|
|
629
|
+
requireStructuralInline(packagesEntry);
|
|
630
|
+
if (packagesEntry !== undefined) {
|
|
631
|
+
const packagesIndent = peekIndent(lines, packagesEntry.bodyStart, packagesEntry.bodyEnd);
|
|
632
|
+
if (packagesIndent !== undefined) {
|
|
633
|
+
const packagesMap = parseMappingLevel(lines, packagesEntry.bodyStart, packagesEntry.bodyEnd, packagesIndent);
|
|
634
|
+
for (const [key, entry] of packagesMap) {
|
|
635
|
+
requireStructuralInline(entry);
|
|
636
|
+
const split = splitPackageKey(key);
|
|
637
|
+
if (split === undefined) fail("lockfile-unreadable");
|
|
638
|
+
let integrity: string | null = null;
|
|
639
|
+
let tarball: string | null = null;
|
|
640
|
+
let otherResolutionKeys: string[] = [];
|
|
641
|
+
const fieldsIndent = peekIndent(lines, entry.bodyStart, entry.bodyEnd);
|
|
642
|
+
if (fieldsIndent !== undefined) {
|
|
643
|
+
const fields = parseMappingLevel(lines, entry.bodyStart, entry.bodyEnd, fieldsIndent);
|
|
644
|
+
const resolution = fields.get("resolution");
|
|
645
|
+
if (resolution !== undefined) {
|
|
646
|
+
if (resolution.inlineValue === undefined) fail("lockfile-unreadable"); // a nested block, not a flow mapping
|
|
647
|
+
const flow = parseFlowMapping(resolution.inlineValue);
|
|
648
|
+
integrity = flow.get("integrity") ?? null;
|
|
649
|
+
tarball = flow.get("tarball") ?? null;
|
|
650
|
+
otherResolutionKeys = [...flow.keys()].filter((k) => k !== "integrity" && k !== "tarball").sort(compareCodeUnits);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
packageLookup.set(key, { integrity });
|
|
654
|
+
entries.push({ key, name: split.name, installedName: split.name, version: split.version, integrity, tarball, link: false, otherResolutionKeys });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const root: RootDependency[] = [];
|
|
660
|
+
const dependencyRefs: LockfileDependencyRef[] = [];
|
|
661
|
+
const importersEntry = top.get("importers");
|
|
662
|
+
requireStructuralInline(importersEntry);
|
|
663
|
+
if (importersEntry !== undefined) {
|
|
664
|
+
const importersIndent = peekIndent(lines, importersEntry.bodyStart, importersEntry.bodyEnd);
|
|
665
|
+
if (importersIndent === undefined) fail("lockfile-unreadable"); // importers present but no "." importer
|
|
666
|
+
const importersMap = parseMappingLevel(lines, importersEntry.bodyStart, importersEntry.bodyEnd, importersIndent);
|
|
667
|
+
if (importersMap.get(".") === undefined) fail("lockfile-unreadable");
|
|
668
|
+
for (const [importerId, importerEntry] of importersMap) {
|
|
669
|
+
requireStructuralInline(importerEntry);
|
|
670
|
+
const importerIndent = peekIndent(lines, importerEntry.bodyStart, importerEntry.bodyEnd);
|
|
671
|
+
if (importerIndent === undefined) continue;
|
|
672
|
+
const importerMap = parseMappingLevel(lines, importerEntry.bodyStart, importerEntry.bodyEnd, importerIndent);
|
|
673
|
+
for (const placement of PNPM_ROOT_PLACEMENTS) {
|
|
674
|
+
const block = importerMap.get(placement);
|
|
675
|
+
requireStructuralInline(block);
|
|
676
|
+
if (block === undefined) continue;
|
|
677
|
+
const depIndent = peekIndent(lines, block.bodyStart, block.bodyEnd);
|
|
678
|
+
if (depIndent === undefined) continue;
|
|
679
|
+
const depMap = parseMappingLevel(lines, block.bodyStart, block.bodyEnd, depIndent);
|
|
680
|
+
for (const [name, depEntry] of depMap) {
|
|
681
|
+
const fieldIndent = peekIndent(lines, depEntry.bodyStart, depEntry.bodyEnd);
|
|
682
|
+
if (fieldIndent === undefined) fail("lockfile-unreadable");
|
|
683
|
+
const fields = parseMappingLevel(lines, depEntry.bodyStart, depEntry.bodyEnd, fieldIndent);
|
|
684
|
+
const specifierField = fields.get("specifier");
|
|
685
|
+
const versionField = fields.get("version");
|
|
686
|
+
if (specifierField?.inlineValue === undefined || versionField?.inlineValue === undefined) fail("lockfile-unreadable");
|
|
687
|
+
const specifier = unquoteYamlScalar(specifierField.inlineValue);
|
|
688
|
+
const rawVersion = unquoteYamlScalar(versionField.inlineValue);
|
|
689
|
+
dependencyRefs.push({ place: `importers['${importerId}'].${placement}`, name, value: rawVersion });
|
|
690
|
+
if (importerId !== ".") continue;
|
|
691
|
+
const link = rawVersion.startsWith("link:") || rawVersion.startsWith("file:");
|
|
692
|
+
const parenIndex = rawVersion.indexOf("(");
|
|
693
|
+
const version = link || parenIndex === -1 ? rawVersion : rawVersion.slice(0, parenIndex);
|
|
694
|
+
const integrity = link ? null : (packageLookup.get(`${name}@${version}`)?.integrity ?? null);
|
|
695
|
+
root.push({ placement, name, specifier, version, rawVersion, integrity, link });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// `snapshots`: read only far enough to expose each dependency reference's raw value (fix 6). A
|
|
702
|
+
// snapshot's own resolution is looked up from `packages` by its key, not read here.
|
|
703
|
+
const snapshotsEntry = top.get("snapshots");
|
|
704
|
+
requireStructuralInline(snapshotsEntry);
|
|
705
|
+
if (snapshotsEntry !== undefined) {
|
|
706
|
+
const snapshotsIndent = peekIndent(lines, snapshotsEntry.bodyStart, snapshotsEntry.bodyEnd);
|
|
707
|
+
if (snapshotsIndent !== undefined) {
|
|
708
|
+
const snapshotsMap = parseMappingLevel(lines, snapshotsEntry.bodyStart, snapshotsEntry.bodyEnd, snapshotsIndent);
|
|
709
|
+
for (const [snapshotKey, snapshotEntry] of snapshotsMap) {
|
|
710
|
+
requireStructuralInline(snapshotEntry);
|
|
711
|
+
const fieldsIndent = peekIndent(lines, snapshotEntry.bodyStart, snapshotEntry.bodyEnd);
|
|
712
|
+
if (fieldsIndent === undefined) continue;
|
|
713
|
+
const fields = parseMappingLevel(lines, snapshotEntry.bodyStart, snapshotEntry.bodyEnd, fieldsIndent);
|
|
714
|
+
for (const placement of SNAPSHOT_PLACEMENTS) {
|
|
715
|
+
const block = fields.get(placement);
|
|
716
|
+
requireStructuralInline(block);
|
|
717
|
+
if (block === undefined) continue;
|
|
718
|
+
const depIndent = peekIndent(lines, block.bodyStart, block.bodyEnd);
|
|
719
|
+
if (depIndent === undefined) continue;
|
|
720
|
+
const depMap = parseMappingLevel(lines, block.bodyStart, block.bodyEnd, depIndent);
|
|
721
|
+
for (const [name, depEntry] of depMap) {
|
|
722
|
+
if (depEntry.inlineValue === undefined) fail("lockfile-unreadable");
|
|
723
|
+
dependencyRefs.push({ place: `snapshots['${snapshotKey}'].${placement}`, name, value: unquoteYamlScalar(depEntry.inlineValue) });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
root.sort((a, b) => compareTuples([a.placement, a.name], [b.placement, b.name]));
|
|
731
|
+
entries.sort((a, b) => compareCodeUnits(a.key, b.key));
|
|
732
|
+
|
|
733
|
+
return { format: "pnpm", lockfileVersion, root, entries, dependencyRefs };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Reads a `pnpm-lock.yaml` of lockfileVersion `'9.0'`. Never throws. */
|
|
737
|
+
export function readPnpmLockfile(text: string): LockfileReadResult {
|
|
738
|
+
try {
|
|
739
|
+
return readPnpmLockfileInner(text);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (error instanceof LockfileFailure) return unreadableResult(error.reason);
|
|
742
|
+
return unreadableResult("lockfile-unreadable");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Reads a lockfile of the given format. Never throws. */
|
|
747
|
+
export function readLockfile(format: LockfileFormat, text: string): LockfileReadResult {
|
|
748
|
+
return format === "npm" ? readNpmLockfile(text) : readPnpmLockfile(text);
|
|
749
|
+
}
|