@packall/core 0.0.1 → 0.2.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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/archive-6sqTn1C4.js +274 -0
- package/dist/archive-6sqTn1C4.js.map +1 -0
- package/dist/archive.d.ts +64 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/{Bundle.d.ts → bundle.d.ts} +56 -38
- package/dist/bundle.d.ts.map +1 -0
- package/dist/{DependencyRange.d.ts → dependency-range.d.ts} +2 -2
- package/dist/dependency-range.d.ts.map +1 -0
- package/dist/{Download.d.ts → download.d.ts} +9 -8
- package/dist/download.d.ts.map +1 -0
- package/dist/enums/artifact-kind.d.ts +8 -0
- package/dist/enums/artifact-kind.d.ts.map +1 -0
- package/dist/enums/edge-kind.d.ts +29 -0
- package/dist/enums/edge-kind.d.ts.map +1 -0
- package/dist/enums/input-file-kind.d.ts +12 -0
- package/dist/enums/input-file-kind.d.ts.map +1 -0
- package/dist/enums/layout.d.ts +23 -0
- package/dist/enums/layout.d.ts.map +1 -0
- package/dist/enums/lockfile-format.d.ts +9 -0
- package/dist/enums/lockfile-format.d.ts.map +1 -0
- package/dist/enums/phase.d.ts +11 -0
- package/dist/enums/phase.d.ts.map +1 -0
- package/dist/{Errors.d.ts → errors.d.ts} +49 -7
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +46 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2121 -1060
- package/dist/index.js.map +1 -1
- package/dist/input-file.d.ts +77 -0
- package/dist/input-file.d.ts.map +1 -0
- package/dist/integrity.d.ts +80 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/layout.d.ts +87 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/locked-resolve.d.ts +47 -0
- package/dist/locked-resolve.d.ts.map +1 -0
- package/dist/lockfile/bun.d.ts +3 -0
- package/dist/lockfile/bun.d.ts.map +1 -0
- package/dist/lockfile/detect.d.ts +31 -0
- package/dist/lockfile/detect.d.ts.map +1 -0
- package/dist/lockfile/json.d.ts +23 -0
- package/dist/lockfile/json.d.ts.map +1 -0
- package/dist/lockfile/names.d.ts +16 -0
- package/dist/lockfile/names.d.ts.map +1 -0
- package/dist/lockfile/npm.d.ts +3 -0
- package/dist/lockfile/npm.d.ts.map +1 -0
- package/dist/lockfile/parse.d.ts +23 -0
- package/dist/lockfile/parse.d.ts.map +1 -0
- package/dist/lockfile/pnpm.d.ts +20 -0
- package/dist/lockfile/pnpm.d.ts.map +1 -0
- package/dist/lockfile/tree-builder.d.ts +70 -0
- package/dist/lockfile/tree-builder.d.ts.map +1 -0
- package/dist/lockfile/types.d.ts +70 -0
- package/dist/lockfile/types.d.ts.map +1 -0
- package/dist/{Manifest.d.ts → manifest.d.ts} +7 -15
- package/dist/manifest.d.ts.map +1 -0
- package/dist/node/archiver.d.ts +5 -0
- package/dist/node/archiver.d.ts.map +1 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +55 -0
- package/dist/node/index.js.map +1 -0
- package/dist/{Options.d.ts → options.d.ts} +12 -30
- package/dist/options.d.ts.map +1 -0
- package/dist/{Platform.d.ts → platform.d.ts} +14 -19
- package/dist/platform.d.ts.map +1 -0
- package/dist/{Progress.d.ts → progress.d.ts} +10 -9
- package/dist/progress.d.ts.map +1 -0
- package/dist/{Registry.d.ts → registry.d.ts} +42 -44
- package/dist/registry.d.ts.map +1 -0
- package/dist/{Resolve.d.ts → resolve.d.ts} +25 -26
- package/dist/resolve.d.ts.map +1 -0
- package/dist/schemas/lenient.d.ts +38 -0
- package/dist/schemas/lenient.d.ts.map +1 -0
- package/dist/schemas/package-json.d.ts +44 -0
- package/dist/schemas/package-json.d.ts.map +1 -0
- package/dist/{Spec.d.ts → spec.d.ts} +4 -4
- package/dist/spec.d.ts.map +1 -0
- package/dist/types/value-of.d.ts +9 -0
- package/dist/types/value-of.d.ts.map +1 -0
- package/dist/utils/is-record.d.ts +8 -0
- package/dist/utils/is-record.d.ts.map +1 -0
- package/package.json +13 -11
- package/src/archive.ts +95 -0
- package/src/bundle.ts +736 -0
- package/src/dependency-range.ts +121 -0
- package/src/download.ts +155 -0
- package/src/enums/artifact-kind.ts +9 -0
- package/src/enums/edge-kind.ts +32 -0
- package/src/enums/input-file-kind.ts +13 -0
- package/src/enums/layout.ts +25 -0
- package/src/enums/lockfile-format.ts +10 -0
- package/src/enums/phase.ts +12 -0
- package/src/errors.ts +300 -0
- package/src/index.ts +146 -35
- package/src/input-file.ts +324 -0
- package/src/integrity.ts +165 -0
- package/src/layout.ts +137 -0
- package/src/locked-resolve.ts +471 -0
- package/src/lockfile/bun.ts +214 -0
- package/src/lockfile/detect.ts +80 -0
- package/src/lockfile/json.ts +131 -0
- package/src/lockfile/names.ts +20 -0
- package/src/lockfile/npm.ts +328 -0
- package/src/lockfile/parse.ts +56 -0
- package/src/lockfile/pnpm.ts +230 -0
- package/src/lockfile/tree-builder.ts +166 -0
- package/src/lockfile/types.ts +74 -0
- package/src/manifest.ts +129 -0
- package/src/node/archiver.ts +85 -0
- package/src/node/index.ts +13 -0
- package/src/options.ts +103 -0
- package/src/platform.ts +178 -0
- package/src/progress.ts +105 -0
- package/src/registry.ts +116 -0
- package/src/resolve.ts +560 -0
- package/src/schemas/lenient.ts +130 -0
- package/src/schemas/package-json.ts +28 -0
- package/src/spec.ts +182 -0
- package/src/types/value-of.ts +8 -0
- package/src/utils/is-record.ts +8 -0
- package/dist/Archive.d.ts +0 -32
- package/dist/Archive.d.ts.map +0 -1
- package/dist/Bundle.d.ts.map +0 -1
- package/dist/DependencyRange.d.ts.map +0 -1
- package/dist/Download.d.ts.map +0 -1
- package/dist/Errors.d.ts.map +0 -1
- package/dist/InputFile.d.ts +0 -46
- package/dist/InputFile.d.ts.map +0 -1
- package/dist/Integrity.d.ts +0 -48
- package/dist/Integrity.d.ts.map +0 -1
- package/dist/Layout.d.ts +0 -53
- package/dist/Layout.d.ts.map +0 -1
- package/dist/Manifest.d.ts.map +0 -1
- package/dist/Options.d.ts.map +0 -1
- package/dist/Platform.d.ts.map +0 -1
- package/dist/Progress.d.ts.map +0 -1
- package/dist/Registry.d.ts.map +0 -1
- package/dist/Resolve.d.ts.map +0 -1
- package/dist/Spec.d.ts.map +0 -1
- package/src/Archive.ts +0 -92
- package/src/Bundle.ts +0 -570
- package/src/DependencyRange.ts +0 -115
- package/src/Download.ts +0 -132
- package/src/Errors.ts +0 -216
- package/src/InputFile.ts +0 -229
- package/src/Integrity.ts +0 -122
- package/src/Layout.ts +0 -103
- package/src/Manifest.ts +0 -133
- package/src/Options.ts +0 -123
- package/src/Platform.ts +0 -182
- package/src/Progress.ts +0 -106
- package/src/Registry.ts +0 -121
- package/src/Resolve.ts +0 -528
- package/src/Spec.ts +0 -186
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun.lock`.
|
|
3
|
+
*
|
|
4
|
+
* Like npm, bun records ranges on its edges and resolves them through the
|
|
5
|
+
* install-path nesting — but it nests with a plain `/`, and package names
|
|
6
|
+
* contain `/` too, so the parent of a key can only be found by asking which
|
|
7
|
+
* prefixes are themselves entries.
|
|
8
|
+
*/
|
|
9
|
+
import { EdgeKind, LockedRootKind } from "../enums/edge-kind.js";
|
|
10
|
+
import { LockfileFormat } from "../enums/lockfile-format.js";
|
|
11
|
+
import { isRecord } from "../utils/is-record.js";
|
|
12
|
+
import { optionalPeerNames, parseJsonObject, stringRecord } from "./json.js";
|
|
13
|
+
import {
|
|
14
|
+
asExactVersion,
|
|
15
|
+
fieldByRootKind,
|
|
16
|
+
importerKey,
|
|
17
|
+
notCovered,
|
|
18
|
+
TreeBuilder,
|
|
19
|
+
} from "./tree-builder.js";
|
|
20
|
+
import type { LockedEdge, LockedTree } from "./types.js";
|
|
21
|
+
|
|
22
|
+
/** One narrowed `packages` entry, keyed by its install path. */
|
|
23
|
+
type BunEntry = {
|
|
24
|
+
readonly key: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly version: string;
|
|
27
|
+
readonly dependencies: Readonly<Record<string, string>>;
|
|
28
|
+
readonly optionalDependencies: Readonly<Record<string, string>>;
|
|
29
|
+
readonly peerDependencies: Readonly<Record<string, string>>;
|
|
30
|
+
readonly optionalPeers: ReadonlySet<string>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Root blocks in bun's precedence order — the first mention of a name wins. */
|
|
34
|
+
const ROOT_KINDS = [
|
|
35
|
+
LockedRootKind.Prod,
|
|
36
|
+
LockedRootKind.Optional,
|
|
37
|
+
LockedRootKind.Dev,
|
|
38
|
+
LockedRootKind.Peer,
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export const parseBunLockfile = (
|
|
42
|
+
path: string,
|
|
43
|
+
content: string,
|
|
44
|
+
importer: string | undefined,
|
|
45
|
+
): LockedTree => {
|
|
46
|
+
const root = parseJsonObject(path, content);
|
|
47
|
+
const version = String(root["lockfileVersion"] ?? "?");
|
|
48
|
+
const builder = new TreeBuilder();
|
|
49
|
+
|
|
50
|
+
const packages = root["packages"];
|
|
51
|
+
const entries = isRecord(packages)
|
|
52
|
+
? collectEntries(builder, packages)
|
|
53
|
+
: new Map<string, BunEntry>();
|
|
54
|
+
|
|
55
|
+
const keys = new Set(entries.keys());
|
|
56
|
+
for (const entry of entries.values()) {
|
|
57
|
+
builder.add({
|
|
58
|
+
name: entry.name,
|
|
59
|
+
version: entry.version,
|
|
60
|
+
dependencies: edgesOf(builder, entries, keys, entry),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
collectRoots(builder, entries, keys, root["workspaces"], path, importer);
|
|
65
|
+
|
|
66
|
+
return builder.finish({ format: LockfileFormat.Bun, lockfileVersion: version, path, importer });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Narrows bun's tuple entries.
|
|
71
|
+
*
|
|
72
|
+
* The value is a positional array whose shape depends on where the package came
|
|
73
|
+
* from. A registry package is `[id, registry, info, integrity]`; a workspace
|
|
74
|
+
* member or a git dependency is shorter and its `id` carries the protocol.
|
|
75
|
+
*/
|
|
76
|
+
const collectEntries = (
|
|
77
|
+
builder: TreeBuilder,
|
|
78
|
+
packages: Record<string, unknown>,
|
|
79
|
+
): Map<string, BunEntry> => {
|
|
80
|
+
const entries = new Map<string, BunEntry>();
|
|
81
|
+
|
|
82
|
+
for (const [key, raw] of Object.entries(packages)) {
|
|
83
|
+
if (!Array.isArray(raw)) continue;
|
|
84
|
+
|
|
85
|
+
const [id, , info] = raw;
|
|
86
|
+
if (typeof id !== "string") continue;
|
|
87
|
+
|
|
88
|
+
const at = id.lastIndexOf("@");
|
|
89
|
+
if (at <= 0) continue;
|
|
90
|
+
const name = id.slice(0, at);
|
|
91
|
+
const spec = id.slice(at + 1);
|
|
92
|
+
|
|
93
|
+
const version = asExactVersion(name, spec);
|
|
94
|
+
if (version._tag === "Unsupported") {
|
|
95
|
+
builder.skip(name, spec, version.reason);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const details = isRecord(info) ? info : {};
|
|
100
|
+
|
|
101
|
+
entries.set(key, {
|
|
102
|
+
key,
|
|
103
|
+
name,
|
|
104
|
+
version: version.version,
|
|
105
|
+
dependencies: stringRecord(details["dependencies"]),
|
|
106
|
+
optionalDependencies: stringRecord(details["optionalDependencies"]),
|
|
107
|
+
peerDependencies: stringRecord(details["peerDependencies"]),
|
|
108
|
+
optionalPeers: collectOptionalPeers(details),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return entries;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** bun lists optional peers as a name array rather than npm's metadata object. */
|
|
116
|
+
const collectOptionalPeers = (details: Record<string, unknown>): ReadonlySet<string> => {
|
|
117
|
+
const listed = details["optionalPeers"];
|
|
118
|
+
const names = Array.isArray(listed)
|
|
119
|
+
? listed.flatMap((entry) => (typeof entry === "string" ? entry : []))
|
|
120
|
+
: [];
|
|
121
|
+
return new Set([...names, ...optionalPeerNames(details["peerDependenciesMeta"])]);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const edgesOf = (
|
|
125
|
+
builder: TreeBuilder,
|
|
126
|
+
entries: ReadonlyMap<string, BunEntry>,
|
|
127
|
+
keys: ReadonlySet<string>,
|
|
128
|
+
entry: BunEntry,
|
|
129
|
+
): ReadonlyArray<LockedEdge> => {
|
|
130
|
+
const edges: Array<LockedEdge> = [];
|
|
131
|
+
|
|
132
|
+
const add = (names: ReadonlyArray<string>, kind: EdgeKind): void => {
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
const target = findEntry(entries, keys, entry.key, name);
|
|
135
|
+
if (target === undefined) {
|
|
136
|
+
if (kind === EdgeKind.Prod)
|
|
137
|
+
builder.dangling(`${entry.name}@${entry.version}`, name);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
edges.push({ name: target.name, version: target.version, kind });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
add(Object.keys(entry.dependencies), EdgeKind.Prod);
|
|
145
|
+
add(Object.keys(entry.optionalDependencies), EdgeKind.Optional);
|
|
146
|
+
add(
|
|
147
|
+
Object.keys(entry.peerDependencies).filter((name) => !entry.optionalPeers.has(name)),
|
|
148
|
+
EdgeKind.Peer,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return edges;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const collectRoots = (
|
|
155
|
+
builder: TreeBuilder,
|
|
156
|
+
entries: ReadonlyMap<string, BunEntry>,
|
|
157
|
+
keys: ReadonlySet<string>,
|
|
158
|
+
workspaces: unknown,
|
|
159
|
+
path: string,
|
|
160
|
+
member: string | undefined,
|
|
161
|
+
): void => {
|
|
162
|
+
if (!isRecord(workspaces)) return;
|
|
163
|
+
|
|
164
|
+
const selected =
|
|
165
|
+
member === undefined
|
|
166
|
+
? Object.values(workspaces)
|
|
167
|
+
: Object.hasOwn(workspaces, importerKey(member))
|
|
168
|
+
? [workspaces[importerKey(member)]]
|
|
169
|
+
: notCovered(path, member, workspacePaths(workspaces));
|
|
170
|
+
|
|
171
|
+
for (const workspace of selected) {
|
|
172
|
+
if (!isRecord(workspace)) continue;
|
|
173
|
+
for (const kind of ROOT_KINDS) {
|
|
174
|
+
const declared = stringRecord(workspace[fieldByRootKind[kind]]);
|
|
175
|
+
for (const [name, specifier] of Object.entries(declared)) {
|
|
176
|
+
const target = findEntry(entries, keys, "", name);
|
|
177
|
+
if (target === undefined) continue;
|
|
178
|
+
builder.addRoot({ name: target.name, version: target.version, kind, specifier });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const workspacePaths = (workspaces: Record<string, unknown>): ReadonlyArray<string> =>
|
|
185
|
+
Object.keys(workspaces).map((key) => (key === "" ? "." : key));
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* bun nests with a plain `/`, and package names contain `/` too, so a parent
|
|
189
|
+
* key cannot be found by string-splitting alone — `@babel/core` would split
|
|
190
|
+
* into a scope that is not a key. `parentKey` only accepts a prefix that is
|
|
191
|
+
* itself an entry, which resolves the ambiguity.
|
|
192
|
+
*/
|
|
193
|
+
const findEntry = (
|
|
194
|
+
entries: ReadonlyMap<string, BunEntry>,
|
|
195
|
+
keys: ReadonlySet<string>,
|
|
196
|
+
fromKey: string,
|
|
197
|
+
name: string,
|
|
198
|
+
): BunEntry | undefined => {
|
|
199
|
+
let scope = fromKey;
|
|
200
|
+
for (;;) {
|
|
201
|
+
const found = entries.get(scope === "" ? name : `${scope}/${name}`);
|
|
202
|
+
if (found !== undefined) return found;
|
|
203
|
+
if (scope === "") return undefined;
|
|
204
|
+
scope = parentKey(keys, scope);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const parentKey = (keys: ReadonlySet<string>, key: string): string => {
|
|
209
|
+
for (let at = key.lastIndexOf("/"); at > 0; at = key.lastIndexOf("/", at - 1)) {
|
|
210
|
+
const prefix = key.slice(0, at);
|
|
211
|
+
if (keys.has(prefix)) return prefix;
|
|
212
|
+
}
|
|
213
|
+
return "";
|
|
214
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working out which lockfile — if any — some content is.
|
|
3
|
+
*
|
|
4
|
+
* Detection is by content, matching how `--file` treats everything else: a
|
|
5
|
+
* lockfile renamed to `deps.txt` still parses, and a `package-lock.json` that
|
|
6
|
+
* is actually a package.json still gets read as one.
|
|
7
|
+
*/
|
|
8
|
+
import { LockfileFormat } from "../enums/lockfile-format.js";
|
|
9
|
+
import { isRecord } from "../utils/is-record.js";
|
|
10
|
+
import { tryParseJsonc } from "./json.js";
|
|
11
|
+
|
|
12
|
+
/** What a blob of file content turned out to be. */
|
|
13
|
+
export type LockfileDetection =
|
|
14
|
+
| { readonly _tag: "Supported"; readonly format: LockfileFormat }
|
|
15
|
+
/**
|
|
16
|
+
* Recognisably a lockfile, but not one we can read. Named explicitly so the
|
|
17
|
+
* error says "yarn.lock is not supported yet" rather than leaving somebody to
|
|
18
|
+
* work out why their file parsed as a spec list.
|
|
19
|
+
*/
|
|
20
|
+
| { readonly _tag: "Unsupported"; readonly label: string; readonly hint: string }
|
|
21
|
+
| { readonly _tag: "NotALockfile" };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Deliberately cheap and total: it never throws, so `--file` can try this
|
|
25
|
+
* first and fall through to its other shapes when the answer is `NotALockfile`.
|
|
26
|
+
*/
|
|
27
|
+
export const detectLockfile = (content: string): LockfileDetection => {
|
|
28
|
+
const trimmed = content.trimStart();
|
|
29
|
+
|
|
30
|
+
// bun's binary lockfile leads with its own magic string. Worth naming,
|
|
31
|
+
// because `bun install` wrote this by default until 1.2 and the fix is one
|
|
32
|
+
// command.
|
|
33
|
+
if (trimmed.startsWith("bun-lockfile-format-v0")) {
|
|
34
|
+
return {
|
|
35
|
+
_tag: "Unsupported",
|
|
36
|
+
label: "a binary bun lockfile (bun.lockb)",
|
|
37
|
+
hint: "Run `bun install --save-text-lockfile` to produce a bun.lock, and pass that instead.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (/^__metadata:/m.test(trimmed) || /^# yarn lockfile v\d/m.test(trimmed)) {
|
|
42
|
+
return {
|
|
43
|
+
_tag: "Unsupported",
|
|
44
|
+
label: "a yarn lockfile",
|
|
45
|
+
hint: "yarn is not supported yet. Point --file at the package.json instead, or pass --lockfile off.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (trimmed.startsWith("{")) return detectJsonLockfile(trimmed);
|
|
50
|
+
|
|
51
|
+
// pnpm is the only YAML one, and it always declares its version first.
|
|
52
|
+
if (/^lockfileVersion:/m.test(trimmed) || /^(importers|snapshots):/m.test(trimmed)) {
|
|
53
|
+
return { _tag: "Supported", format: LockfileFormat.Pnpm };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { _tag: "NotALockfile" };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Separates the two JSON lockfiles from each other and from a package.json.
|
|
61
|
+
*
|
|
62
|
+
* Both npm and bun write a numeric `lockfileVersion`, so that field alone
|
|
63
|
+
* cannot decide it. bun is the one that carries a `workspaces` map keyed by the
|
|
64
|
+
* empty string; npm keeps the root project under `packages[""]` instead.
|
|
65
|
+
*/
|
|
66
|
+
const detectJsonLockfile = (trimmed: string): LockfileDetection => {
|
|
67
|
+
const parsed = tryParseJsonc(trimmed);
|
|
68
|
+
if (!isRecord(parsed)) return { _tag: "NotALockfile" };
|
|
69
|
+
|
|
70
|
+
const workspaces = parsed["workspaces"];
|
|
71
|
+
if (isRecord(workspaces) && Object.hasOwn(workspaces, "")) {
|
|
72
|
+
return { _tag: "Supported", format: LockfileFormat.Bun };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof parsed["lockfileVersion"] === "number") {
|
|
76
|
+
return { _tag: "Supported", format: LockfileFormat.Npm };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { _tag: "NotALockfile" };
|
|
80
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrowing helpers for the untyped JSON and YAML a lockfile parses into.
|
|
3
|
+
*
|
|
4
|
+
* Everything here takes `unknown` and hands back something narrowed, which is
|
|
5
|
+
* what keeps the parsers free of casts against files nobody controls. The
|
|
6
|
+
* shape-level narrowing is Effect Schema — the same decoders the registry
|
|
7
|
+
* response boundary uses — so "tolerated" means one thing across the codebase
|
|
8
|
+
* rather than being re-decided per parser.
|
|
9
|
+
*/
|
|
10
|
+
import * as Option from "effect/Option";
|
|
11
|
+
import * as Schema from "effect/Schema";
|
|
12
|
+
|
|
13
|
+
import { LockfileError } from "../errors.js";
|
|
14
|
+
import { OptionalFlagRecordSchema, StringRecordSchema } from "../schemas/lenient.js";
|
|
15
|
+
import { isRecord } from "../utils/is-record.js";
|
|
16
|
+
|
|
17
|
+
export const stringOr = (value: unknown, fallback: string): string =>
|
|
18
|
+
typeof value === "string" ? value : fallback;
|
|
19
|
+
|
|
20
|
+
const decodeStringRecord = Schema.decodeUnknownOption(StringRecordSchema);
|
|
21
|
+
|
|
22
|
+
/** Keeps only the string-valued members of an untyped object. */
|
|
23
|
+
export const stringRecord = (value: unknown): Readonly<Record<string, string>> =>
|
|
24
|
+
Option.getOrElse(decodeStringRecord(value), (): Record<string, string> => ({}));
|
|
25
|
+
|
|
26
|
+
const decodeOptionalFlags = Schema.decodeUnknownOption(OptionalFlagRecordSchema);
|
|
27
|
+
|
|
28
|
+
/** Names a package marked its peers optional with. */
|
|
29
|
+
export const optionalPeerNames = (value: unknown): ReadonlySet<string> => {
|
|
30
|
+
const flags = Option.getOrElse(
|
|
31
|
+
decodeOptionalFlags(value),
|
|
32
|
+
(): Record<string, { readonly optional: boolean }> => ({}),
|
|
33
|
+
);
|
|
34
|
+
return new Set(Object.entries(flags).flatMap(([name, meta]) => (meta.optional ? name : [])));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const parseJsonObject = (path: string, content: string): Record<string, unknown> => {
|
|
38
|
+
const parsed = tryParseJsonc(content);
|
|
39
|
+
if (parsed === undefined) {
|
|
40
|
+
throw new LockfileError(path, "not valid JSON");
|
|
41
|
+
}
|
|
42
|
+
if (!isRecord(parsed)) {
|
|
43
|
+
throw new LockfileError(path, "top level is not an object");
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parses JSON, tolerating the comments and trailing commas bun writes.
|
|
50
|
+
*
|
|
51
|
+
* `bun.lock` is JSONC by design — bun puts explanatory comments in it — so
|
|
52
|
+
* plain `JSON.parse` rejects real lockfiles. Returns `undefined` rather than
|
|
53
|
+
* throwing so detection can use it as a probe.
|
|
54
|
+
*/
|
|
55
|
+
export const tryParseJsonc = (content: string): unknown => {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(content);
|
|
58
|
+
} catch {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(stripJsonc(content));
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Removes comments and trailing commas.
|
|
69
|
+
*
|
|
70
|
+
* String-aware, because a `//` inside a tarball URL is not a comment and an
|
|
71
|
+
* escaped quote does not end a string. Both appear in every real lockfile.
|
|
72
|
+
*/
|
|
73
|
+
export const stripJsonc = (content: string): string => {
|
|
74
|
+
let out = "";
|
|
75
|
+
let index = 0;
|
|
76
|
+
let inString = false;
|
|
77
|
+
|
|
78
|
+
while (index < content.length) {
|
|
79
|
+
const char = content[index];
|
|
80
|
+
|
|
81
|
+
if (inString) {
|
|
82
|
+
out += char;
|
|
83
|
+
if (char === "\\") {
|
|
84
|
+
out += content[index + 1] ?? "";
|
|
85
|
+
index += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (char === '"') inString = false;
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (char === '"') {
|
|
94
|
+
inString = true;
|
|
95
|
+
out += char;
|
|
96
|
+
index += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (char === "/" && content[index + 1] === "/") {
|
|
101
|
+
const end = content.indexOf("\n", index);
|
|
102
|
+
index = end === -1 ? content.length : end;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (char === "/" && content[index + 1] === "*") {
|
|
107
|
+
const end = content.indexOf("*/", index + 2);
|
|
108
|
+
index = end === -1 ? content.length : end + 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A comma is trailing when the next thing that is not whitespace closes the
|
|
113
|
+
// structure it sits in.
|
|
114
|
+
if (char === ",") {
|
|
115
|
+
const next = content.slice(index + 1).search(/\S/);
|
|
116
|
+
const following = next === -1 ? "" : content[index + 1 + next];
|
|
117
|
+
if (following === "}" || following === "]") {
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
out += char;
|
|
124
|
+
index += 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return out;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const describeError = (error: unknown): string =>
|
|
131
|
+
error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LockfileFormat } from "../enums/lockfile-format.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The file name each supported manager writes, in detection order.
|
|
5
|
+
*
|
|
6
|
+
* Ordered: `package-lock.json` before `npm-shrinkwrap.json` before pnpm's and
|
|
7
|
+
* bun's, so a directory holding several is searched the way npm itself would.
|
|
8
|
+
*/
|
|
9
|
+
export const formatByLockfileName = {
|
|
10
|
+
"package-lock.json": LockfileFormat.Npm,
|
|
11
|
+
"npm-shrinkwrap.json": LockfileFormat.Npm,
|
|
12
|
+
"pnpm-lock.yaml": LockfileFormat.Pnpm,
|
|
13
|
+
"bun.lock": LockfileFormat.Bun,
|
|
14
|
+
} as const satisfies Record<string, LockfileFormat>;
|
|
15
|
+
|
|
16
|
+
/** The names a given manager writes. */
|
|
17
|
+
export const lockfileNamesFor = (format: LockfileFormat): ReadonlyArray<string> =>
|
|
18
|
+
Object.entries(formatByLockfileName).flatMap(([name, candidate]) =>
|
|
19
|
+
candidate === format ? name : [],
|
|
20
|
+
);
|