@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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bookkeeping every lockfile parser shares.
|
|
3
|
+
*
|
|
4
|
+
* Packages, edges and skip warnings accumulate here while a parser walks a
|
|
5
|
+
* file, which is what lets each parser stay focused on its own encoding.
|
|
6
|
+
*/
|
|
7
|
+
import semver from "semver";
|
|
8
|
+
|
|
9
|
+
import { parseDependencyTarget } from "../dependency-range.js";
|
|
10
|
+
import { LockedRootKind } from "../enums/edge-kind.js";
|
|
11
|
+
import type { LockfileFormat } from "../enums/lockfile-format.js";
|
|
12
|
+
import { LockfileError } from "../errors.js";
|
|
13
|
+
import { packageKey } from "../resolve.js";
|
|
14
|
+
import type { LockedEdge, LockedPackage, LockedRoot, LockedTree } from "./types.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Which manifest block each root kind is declared in.
|
|
18
|
+
*
|
|
19
|
+
* The block names are the same across npm, pnpm and bun; only which of them a
|
|
20
|
+
* given format writes differs, which is why each parser supplies its own order
|
|
21
|
+
* rather than iterating this.
|
|
22
|
+
*/
|
|
23
|
+
export const fieldByRootKind = {
|
|
24
|
+
[LockedRootKind.Prod]: "dependencies",
|
|
25
|
+
[LockedRootKind.Optional]: "optionalDependencies",
|
|
26
|
+
[LockedRootKind.Dev]: "devDependencies",
|
|
27
|
+
[LockedRootKind.Peer]: "peerDependencies",
|
|
28
|
+
} as const satisfies Record<LockedRootKind, string>;
|
|
29
|
+
|
|
30
|
+
export class TreeBuilder {
|
|
31
|
+
readonly packages = new Map<string, LockedPackage>();
|
|
32
|
+
readonly roots: Array<LockedRoot> = [];
|
|
33
|
+
readonly warnings: Array<string> = [];
|
|
34
|
+
readonly incomplete: Array<string> = [];
|
|
35
|
+
/** Names dropped on purpose, so an edge into one is explained, not alarming. */
|
|
36
|
+
private readonly skipped = new Set<string>();
|
|
37
|
+
private readonly seenWarnings = new Set<string>();
|
|
38
|
+
|
|
39
|
+
add(pkg: LockedPackage): void {
|
|
40
|
+
const key = packageKey(pkg.name, pkg.version);
|
|
41
|
+
const existing = this.packages.get(key);
|
|
42
|
+
if (existing === undefined) {
|
|
43
|
+
this.packages.set(key, pkg);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// The same package at the same version can appear at several paths in a
|
|
47
|
+
// nested layout. They are the same bytes; merge the edge sets so a copy
|
|
48
|
+
// recorded with fewer dependencies does not lose any.
|
|
49
|
+
this.packages.set(key, {
|
|
50
|
+
...existing,
|
|
51
|
+
dependencies: mergeEdges(existing.dependencies, pkg.dependencies),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
addRoot(root: LockedRoot): void {
|
|
56
|
+
if (this.roots.some((existing) => existing.name === root.name)) return;
|
|
57
|
+
this.roots.push(root);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
warn(message: string): void {
|
|
61
|
+
if (this.seenWarnings.has(message)) return;
|
|
62
|
+
this.seenWarnings.add(message);
|
|
63
|
+
this.warnings.push(message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Notes an entry that is real but cannot come from a registry. */
|
|
67
|
+
skip(name: string, spec: string, reason: string): void {
|
|
68
|
+
this.skipped.add(name);
|
|
69
|
+
this.warn(`skipped ${name}@${spec} (${reason}) — not fetchable from a registry`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Records a required edge that resolved to nothing.
|
|
74
|
+
*
|
|
75
|
+
* An edge into something skipped on purpose is expected — you cannot bundle a
|
|
76
|
+
* `workspace:` sibling from a registry — so it stays a warning. Anything else
|
|
77
|
+
* means the file does not pin what it claims to.
|
|
78
|
+
*/
|
|
79
|
+
dangling(from: string, name: string): void {
|
|
80
|
+
if (this.skipped.has(name)) {
|
|
81
|
+
this.warn(`${from} requires ${name}, which is not fetchable from a registry`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.incomplete.push(`${name} (required by ${from})`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
finish(input: {
|
|
88
|
+
readonly format: LockfileFormat;
|
|
89
|
+
readonly lockfileVersion: string;
|
|
90
|
+
readonly path: string;
|
|
91
|
+
readonly importer?: string | undefined;
|
|
92
|
+
}): LockedTree {
|
|
93
|
+
if (this.packages.size === 0) {
|
|
94
|
+
throw new LockfileError(input.path, "lockfile pins no packages that can be bundled");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
format: input.format,
|
|
98
|
+
lockfileVersion: input.lockfileVersion,
|
|
99
|
+
path: input.path,
|
|
100
|
+
importer: input.importer,
|
|
101
|
+
packages: this.packages,
|
|
102
|
+
roots: this.roots,
|
|
103
|
+
warnings: this.warnings,
|
|
104
|
+
incomplete: this.incomplete,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const mergeEdges = (
|
|
110
|
+
a: ReadonlyArray<LockedEdge>,
|
|
111
|
+
b: ReadonlyArray<LockedEdge>,
|
|
112
|
+
): ReadonlyArray<LockedEdge> => {
|
|
113
|
+
const merged = new Map<string, LockedEdge>();
|
|
114
|
+
for (const edge of [...a, ...b]) {
|
|
115
|
+
merged.set(`${edge.name}@${edge.version}|${edge.kind}`, edge);
|
|
116
|
+
}
|
|
117
|
+
return [...merged.values()];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reads the right-hand side of a locked entry as an exact version.
|
|
122
|
+
*
|
|
123
|
+
* Returns the version when it is one, or the reason it is not — `file:../x`,
|
|
124
|
+
* `workspace:*`, a git URL. `parseDependencyTarget` already classifies every
|
|
125
|
+
* one of those for package.json parsing, so the vocabulary of skip reasons
|
|
126
|
+
* stays identical between the two paths.
|
|
127
|
+
*/
|
|
128
|
+
export const asExactVersion = (
|
|
129
|
+
name: string,
|
|
130
|
+
raw: string,
|
|
131
|
+
):
|
|
132
|
+
| { readonly _tag: "Version"; readonly version: string }
|
|
133
|
+
| { readonly _tag: "Unsupported"; readonly reason: string } => {
|
|
134
|
+
const exact = semver.valid(raw, { loose: true });
|
|
135
|
+
if (exact !== null) return { _tag: "Version", version: exact };
|
|
136
|
+
|
|
137
|
+
const target = parseDependencyTarget(name, raw);
|
|
138
|
+
if (target._tag === "Unsupported") return { _tag: "Unsupported", reason: target.reason };
|
|
139
|
+
return { _tag: "Unsupported", reason: "not an exact version" };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Raised when a lockfile found by walking up turns out not to cover the
|
|
144
|
+
* package.json that went looking for it.
|
|
145
|
+
*
|
|
146
|
+
* Worth distinguishing from "no lockfile at all": the file exists and is
|
|
147
|
+
* readable, it simply belongs to a different project, and bundling it would
|
|
148
|
+
* produce someone else's dependency set.
|
|
149
|
+
*/
|
|
150
|
+
export const notCovered = (path: string, importer: string, known: ReadonlyArray<string>): never => {
|
|
151
|
+
const listed = known
|
|
152
|
+
.slice(0, 8)
|
|
153
|
+
.map((entry) => ` ${entry}`)
|
|
154
|
+
.join("\n");
|
|
155
|
+
throw new LockfileError(
|
|
156
|
+
path,
|
|
157
|
+
`it does not cover ${importer}.\n` +
|
|
158
|
+
(known.length === 0
|
|
159
|
+
? " It records no workspace members."
|
|
160
|
+
: ` Workspace members it does record:\n${listed}` +
|
|
161
|
+
(known.length > 8 ? `\n … and ${known.length - 8} more` : "")),
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** `""` is how npm and bun spell the project at the lockfile's own level. */
|
|
166
|
+
export const importerKey = (importer: string): string => (importer === "." ? "" : importer);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one output shape every lockfile parser produces.
|
|
3
|
+
*
|
|
4
|
+
* npm, pnpm and bun encode "package X at path P depends on Y" in three
|
|
5
|
+
* different ways; once flattened into this graph, nothing downstream has to
|
|
6
|
+
* know which manager wrote the file.
|
|
7
|
+
*/
|
|
8
|
+
import type { EdgeKind, LockedRootKind } from "../enums/edge-kind.js";
|
|
9
|
+
import type { LockfileFormat } from "../enums/lockfile-format.js";
|
|
10
|
+
|
|
11
|
+
/** One edge out of a locked package, already pinned to an exact version. */
|
|
12
|
+
export type LockedEdge = {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly version: string;
|
|
15
|
+
readonly kind: EdgeKind;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** One package pinned by a lockfile. */
|
|
19
|
+
export type LockedPackage = {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly version: string;
|
|
22
|
+
readonly dependencies: ReadonlyArray<LockedEdge>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A direct dependency of the locked project. */
|
|
26
|
+
export type LockedRoot = {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly version: string;
|
|
29
|
+
readonly kind: LockedRootKind;
|
|
30
|
+
/** The range the manifest asked for, kept for reporting. */
|
|
31
|
+
readonly specifier?: string | undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Options for reading a lockfile. */
|
|
35
|
+
export type LockfileParseOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* Which workspace member's dependencies become the roots, as a path relative
|
|
38
|
+
* to the lockfile in POSIX form — `"packages/core"`, or `"."` for the project
|
|
39
|
+
* at the lockfile's own level.
|
|
40
|
+
*
|
|
41
|
+
* Left undefined, every workspace member in the file contributes, which is
|
|
42
|
+
* what pointing `--file` straight at a lockfile should mean. It matters when
|
|
43
|
+
* a package.json found a lockfile by walking *up*: the file then pins the
|
|
44
|
+
* whole repository, and only one member of it was asked for.
|
|
45
|
+
*/
|
|
46
|
+
readonly importer?: string | undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A whole lockfile, flattened. */
|
|
50
|
+
export type LockedTree = {
|
|
51
|
+
readonly format: LockfileFormat;
|
|
52
|
+
/** As the file declares it: `3`, `'9.0'`, `1`. */
|
|
53
|
+
readonly lockfileVersion: string;
|
|
54
|
+
/** Where it was read from, for error messages. */
|
|
55
|
+
readonly path: string;
|
|
56
|
+
/** The workspace member the roots came from, when one was selected. */
|
|
57
|
+
readonly importer?: string | undefined;
|
|
58
|
+
/** Every pinned package, keyed by `name@version`. */
|
|
59
|
+
readonly packages: ReadonlyMap<string, LockedPackage>;
|
|
60
|
+
/** The project's direct dependencies. */
|
|
61
|
+
readonly roots: ReadonlyArray<LockedRoot>;
|
|
62
|
+
/** Entries that were recognised but cannot be fetched from a registry. */
|
|
63
|
+
readonly warnings: ReadonlyArray<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Required edges pointing at nothing the file pins.
|
|
66
|
+
*
|
|
67
|
+
* Distinct from `warnings`: a `workspace:` dependency that cannot be bundled
|
|
68
|
+
* is expected and gets a warning, while an ordinary registry dependency with
|
|
69
|
+
* no entry means the lockfile is genuinely partial and the bundle it produces
|
|
70
|
+
* would be missing a package. The caller decides what to do about it, but the
|
|
71
|
+
* two must not be confused.
|
|
72
|
+
*/
|
|
73
|
+
readonly incomplete: ReadonlyArray<string>;
|
|
74
|
+
};
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The manifest written into every bundle — the audit trail, answerable without
|
|
3
|
+
* unpacking a single tarball. See `notes/requirements.md` § FR-10.
|
|
4
|
+
*/
|
|
5
|
+
import { packagePath } from "./layout.js";
|
|
6
|
+
import type { BundleOptions } from "./options.js";
|
|
7
|
+
import { formatPlatformFilter } from "./platform.js";
|
|
8
|
+
import type { Resolution, ResolvedPackage } from "./resolve.js";
|
|
9
|
+
import { formatSpec } from "./spec.js";
|
|
10
|
+
|
|
11
|
+
/** Schema version, bumped when the shape changes incompatibly. */
|
|
12
|
+
export const MANIFEST_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
export type ManifestEntry = {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly version: string;
|
|
17
|
+
/** Path of this tarball inside the bundle. */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
/** The URL it was downloaded from. */
|
|
20
|
+
readonly tarball: string;
|
|
21
|
+
/** SRI integrity string as advertised by the source registry, when present. */
|
|
22
|
+
readonly integrity?: string | undefined;
|
|
23
|
+
/** Legacy sha1, present on older published versions. */
|
|
24
|
+
readonly shasum?: string | undefined;
|
|
25
|
+
/** Actual size on disk, in bytes. */
|
|
26
|
+
readonly bytes?: number | undefined;
|
|
27
|
+
/** Human-readable justifications: `root:react@18.2.0`, `prod:scheduler@0.23.0`. */
|
|
28
|
+
readonly reasons: ReadonlyArray<string>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type BundleManifest = {
|
|
32
|
+
readonly manifestVersion: number;
|
|
33
|
+
readonly tool: { readonly name: string; readonly version: string };
|
|
34
|
+
readonly createdAt: string;
|
|
35
|
+
readonly registry: { readonly kind: string; readonly url: string };
|
|
36
|
+
/** What the user asked for, verbatim. */
|
|
37
|
+
readonly requested: ReadonlyArray<string>;
|
|
38
|
+
/** What each request expanded to. */
|
|
39
|
+
readonly roots: ReadonlyArray<{
|
|
40
|
+
readonly spec: string;
|
|
41
|
+
readonly versions: ReadonlyArray<string>;
|
|
42
|
+
readonly packageCount: number;
|
|
43
|
+
}>;
|
|
44
|
+
readonly options: {
|
|
45
|
+
readonly layout: string;
|
|
46
|
+
readonly optionalDependencies: boolean;
|
|
47
|
+
readonly peerDependencies: boolean;
|
|
48
|
+
readonly platforms: string;
|
|
49
|
+
readonly allVersions: boolean;
|
|
50
|
+
readonly includePrerelease: boolean;
|
|
51
|
+
readonly integrityVerified: boolean;
|
|
52
|
+
};
|
|
53
|
+
readonly packages: ReadonlyArray<ManifestEntry>;
|
|
54
|
+
readonly warnings: ReadonlyArray<string>;
|
|
55
|
+
readonly totals: {
|
|
56
|
+
readonly packages: number;
|
|
57
|
+
readonly bytes: number;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Renders a package's reasons into short, greppable strings. */
|
|
62
|
+
export const formatReasons = (pkg: ResolvedPackage): ReadonlyArray<string> =>
|
|
63
|
+
pkg.reasons.map((reason) =>
|
|
64
|
+
reason._tag === "Root" ? `root:${reason.spec}` : `${reason.kind}:${reason.from}`,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/** Builds the manifest for a completed (or dry) run. */
|
|
68
|
+
export const buildManifest = (input: {
|
|
69
|
+
readonly resolution: Resolution;
|
|
70
|
+
/** Only the packages actually included in *this* archive. */
|
|
71
|
+
readonly included: ReadonlyArray<ResolvedPackage>;
|
|
72
|
+
readonly options: BundleOptions;
|
|
73
|
+
readonly registry: { readonly kind: string; readonly url: string };
|
|
74
|
+
readonly toolVersion: string;
|
|
75
|
+
readonly sizes: ReadonlyMap<string, number>;
|
|
76
|
+
readonly createdAt?: Date | undefined;
|
|
77
|
+
}): BundleManifest => {
|
|
78
|
+
const includedNames = new Set(input.included.map((pkg) => `${pkg.name}@${pkg.version}`));
|
|
79
|
+
|
|
80
|
+
const packages: ReadonlyArray<ManifestEntry> = input.included.map((pkg) => {
|
|
81
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
82
|
+
return {
|
|
83
|
+
name: pkg.name,
|
|
84
|
+
version: pkg.version,
|
|
85
|
+
path: packagePath(pkg.name, pkg.version),
|
|
86
|
+
tarball: pkg.manifest.dist.tarball,
|
|
87
|
+
integrity: pkg.manifest.dist.integrity,
|
|
88
|
+
shasum: pkg.manifest.dist.shasum,
|
|
89
|
+
bytes: input.sizes.get(key),
|
|
90
|
+
reasons: formatReasons(pkg),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const totalBytes = packages.reduce((sum, entry) => sum + (entry.bytes ?? 0), 0);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
manifestVersion: MANIFEST_VERSION,
|
|
98
|
+
tool: { name: "packall", version: input.toolVersion },
|
|
99
|
+
createdAt: (input.createdAt ?? new Date()).toISOString(),
|
|
100
|
+
registry: input.registry,
|
|
101
|
+
requested: input.resolution.roots.map((root) => formatSpec(root.spec)),
|
|
102
|
+
roots: input.resolution.roots.map((root) => ({
|
|
103
|
+
spec: formatSpec(root.spec),
|
|
104
|
+
versions: root.versions,
|
|
105
|
+
packageCount: root.closure.filter((key) => includedNames.has(key)).length,
|
|
106
|
+
})),
|
|
107
|
+
options: {
|
|
108
|
+
layout: input.options.layout,
|
|
109
|
+
optionalDependencies: input.options.scope.optional,
|
|
110
|
+
peerDependencies: input.options.scope.peer,
|
|
111
|
+
platforms: formatPlatformFilter(input.options.scope.platforms),
|
|
112
|
+
allVersions: input.options.allVersions,
|
|
113
|
+
includePrerelease: input.options.includePrerelease,
|
|
114
|
+
integrityVerified: input.options.verifyIntegrity,
|
|
115
|
+
},
|
|
116
|
+
packages,
|
|
117
|
+
warnings: input.resolution.warnings.map((warning) =>
|
|
118
|
+
warning.from === undefined ? warning.message : `${warning.from}: ${warning.message}`,
|
|
119
|
+
),
|
|
120
|
+
totals: {
|
|
121
|
+
packages: packages.length,
|
|
122
|
+
bytes: totalBytes,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Serialises a manifest with stable key order and a trailing newline. */
|
|
128
|
+
export const serializeManifest = (manifest: BundleManifest): string =>
|
|
129
|
+
`${JSON.stringify(manifest, null, 2)}\n`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node `Archiver`, backed by `node-tar`.
|
|
3
|
+
*
|
|
4
|
+
* Behind `@packall/core/node` rather than the main barrel: importing
|
|
5
|
+
* `@packall/core` must not drag `node-tar` — and through it `node:fs` — into a
|
|
6
|
+
* browser bundle. Anything running on Node reaches for this explicitly.
|
|
7
|
+
*
|
|
8
|
+
* Uses `node-tar`, the same library npm itself packs with. Writing a USTAR
|
|
9
|
+
* encoder by hand would be a fun afternoon and a bad idea: scoped package
|
|
10
|
+
* paths routinely exceed the 100-byte name field, so correctness depends on
|
|
11
|
+
* prefix-splitting and PAX headers that `tar` already gets right.
|
|
12
|
+
*/
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as FileSystem from "effect/FileSystem";
|
|
15
|
+
import * as Layer from "effect/Layer";
|
|
16
|
+
import type { PlatformError } from "effect/PlatformError";
|
|
17
|
+
import { create } from "tar";
|
|
18
|
+
|
|
19
|
+
import type { ArchiveResult, CreateArchiveOptions } from "../archive.js";
|
|
20
|
+
import { Archiver } from "../archive.js";
|
|
21
|
+
import { ArchiveError } from "../errors.js";
|
|
22
|
+
|
|
23
|
+
const createArchive = (
|
|
24
|
+
options: CreateArchiveOptions,
|
|
25
|
+
): Effect.Effect<ArchiveResult, ArchiveError | PlatformError, FileSystem.FileSystem> =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const fs = yield* FileSystem.FileSystem;
|
|
28
|
+
|
|
29
|
+
yield* Effect.tryPromise({
|
|
30
|
+
try: () =>
|
|
31
|
+
create(
|
|
32
|
+
{
|
|
33
|
+
gzip: options.gzipLevel === undefined ? true : { level: options.gzipLevel },
|
|
34
|
+
file: options.outPath,
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
// Normalises uid/gid/mtime so the same inputs produce byte-identical
|
|
37
|
+
// output — which matters when a security team wants to diff two
|
|
38
|
+
// bundles or re-derive one from a manifest.
|
|
39
|
+
portable: true,
|
|
40
|
+
// Undo the `./` added below, so recorded entry names stay exactly
|
|
41
|
+
// the registry paths the manifest lists.
|
|
42
|
+
onWriteEntry: (entry) => {
|
|
43
|
+
entry.path = entry.path.replace(/^\.\//, "");
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
// Sorted so archive order is stable across runs and platforms.
|
|
47
|
+
options.entries.toSorted().map(dotSlash),
|
|
48
|
+
),
|
|
49
|
+
catch: (cause) => new ArchiveError(options.outPath, describe(cause), { cause }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const info = yield* fs.stat(options.outPath);
|
|
53
|
+
|
|
54
|
+
return { path: options.outPath, bytes: Number(info.size) };
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** The Node packing backend. */
|
|
58
|
+
export const layerArchiver: Layer.Layer<Archiver> = Layer.succeed(Archiver)({
|
|
59
|
+
create: createArchive,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Guards every entry against tar's `@` convention.
|
|
64
|
+
*
|
|
65
|
+
* In a tar file list, a leading `@` means "splice in the entries of this other
|
|
66
|
+
* archive" — a GNU convention `node-tar` implements by stripping the `@` and
|
|
67
|
+
* looking for what is left. Every scoped package is a top-level entry starting
|
|
68
|
+
* with `@`, so this hit the bundler in both possible ways:
|
|
69
|
+
*
|
|
70
|
+
* - `@oxc-project` became `oxc-project`, which does not exist, and the run died
|
|
71
|
+
* with an ENOENT naming a path that appears nowhere in the staging tree.
|
|
72
|
+
* - `@esbuild` became `esbuild`, which *does* exist — the unscoped package of
|
|
73
|
+
* the same name sitting right next to it. No error, and every scoped tarball
|
|
74
|
+
* silently missing from a bundle that reported success. That is the dangerous
|
|
75
|
+
* one: you would not find out until the install failed behind the firewall.
|
|
76
|
+
*
|
|
77
|
+
* `./@scope` is not subject to the convention and resolves identically.
|
|
78
|
+
*
|
|
79
|
+
* Any other backend has to answer this question too — the browser writer must
|
|
80
|
+
* be held to the same test.
|
|
81
|
+
*/
|
|
82
|
+
const dotSlash = (entry: string): string => (entry.startsWith("./") ? entry : `./${entry}`);
|
|
83
|
+
|
|
84
|
+
const describe = (cause: unknown): string =>
|
|
85
|
+
cause instanceof Error ? cause.message : String(cause);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@packall/core/node` — the engine's Node-specific layers.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is separated from the main barrel for one reason: importing
|
|
5
|
+
* `@packall/core` must stay safe in a browser. The engine itself reaches the
|
|
6
|
+
* outside world only through services — `Registry`, `Progress`, `Archiver`,
|
|
7
|
+
* `FileSystem`, `Path`, `Crypto` — so the platform lives at the edges, and this
|
|
8
|
+
* is the Node edge. A browser supplies its own.
|
|
9
|
+
*
|
|
10
|
+
* This is the entry point's whole public API.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { layerArchiver } from "./archiver.js";
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Option types shared by the resolver and the bundler, plus their defaults.
|
|
3
|
+
*
|
|
4
|
+
* The rule the defaults below are derived from is stated once, beside the flags
|
|
5
|
+
* that expose them — see `descriptors.ts` in `@packall/cli`, and
|
|
6
|
+
* `notes/requirements.md` § FR-13.
|
|
7
|
+
*/
|
|
8
|
+
import { Layout } from "./enums/layout.js";
|
|
9
|
+
import type { PlatformFilter } from "./platform.js";
|
|
10
|
+
import { allPlatforms } from "./platform.js";
|
|
11
|
+
|
|
12
|
+
/** Which dependency edges the resolver follows. */
|
|
13
|
+
export type DependencyScope = {
|
|
14
|
+
/**
|
|
15
|
+
* Follow `optionalDependencies`. On by default — this is where per-platform
|
|
16
|
+
* native binaries live, and omitting them is the most common reason an
|
|
17
|
+
* offline install fails on a machine other than the one that built it.
|
|
18
|
+
*/
|
|
19
|
+
readonly optional: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Follow non-optional `peerDependencies`. On by default, because npm 7+
|
|
22
|
+
* auto-installs peers and will reach for the network if they are absent.
|
|
23
|
+
*/
|
|
24
|
+
readonly peer: boolean;
|
|
25
|
+
/** Which platforms optional dependencies are kept for. */
|
|
26
|
+
readonly platforms: PlatformFilter;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const defaultScope: DependencyScope = {
|
|
30
|
+
optional: true,
|
|
31
|
+
peer: true,
|
|
32
|
+
platforms: allPlatforms,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Options governing version selection and the dependency walk. */
|
|
36
|
+
export type ResolveOptions = {
|
|
37
|
+
readonly scope: DependencyScope;
|
|
38
|
+
/**
|
|
39
|
+
* When a root spec is a range, take *every* published version that satisfies
|
|
40
|
+
* it rather than only the best match.
|
|
41
|
+
*
|
|
42
|
+
* `npmb react@^18` bundles one version; `npmb --all-versions react@^18`
|
|
43
|
+
* bundles all of them.
|
|
44
|
+
*/
|
|
45
|
+
readonly allVersions: boolean;
|
|
46
|
+
/** With `allVersions`, keep at most this many (newest first). */
|
|
47
|
+
readonly maxVersions?: number | undefined;
|
|
48
|
+
/** Let prereleases satisfy ranges that would not normally admit them. */
|
|
49
|
+
readonly includePrerelease: boolean;
|
|
50
|
+
/** How many registry requests to have in flight at once. */
|
|
51
|
+
readonly concurrency: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const defaultResolveOptions: ResolveOptions = {
|
|
55
|
+
scope: defaultScope,
|
|
56
|
+
allVersions: false,
|
|
57
|
+
includePrerelease: false,
|
|
58
|
+
concurrency: 10,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Options for a full bundle run. */
|
|
62
|
+
export type BundleOptions = ResolveOptions & {
|
|
63
|
+
readonly layout: Layout;
|
|
64
|
+
/** Directory the finished artifacts are written to. */
|
|
65
|
+
readonly outDir: string;
|
|
66
|
+
/** Base name for the archive in `single` layout. Defaults to `bundle`. */
|
|
67
|
+
readonly archiveName?: string | undefined;
|
|
68
|
+
/** Resolve and report, but download nothing and write nothing. */
|
|
69
|
+
readonly dryRun: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Verify every tarball against the checksum the registry advertised.
|
|
72
|
+
*
|
|
73
|
+
* On by default and there is essentially no good reason to turn it off; the
|
|
74
|
+
* escape hatch exists only for registries that serve broken metadata.
|
|
75
|
+
*/
|
|
76
|
+
readonly verifyIntegrity: boolean;
|
|
77
|
+
/** Overwrite existing files in `outDir` instead of refusing. */
|
|
78
|
+
readonly force: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Planned output files to leave untouched, by file name.
|
|
81
|
+
*
|
|
82
|
+
* The engine never asks anything; deciding is the caller's job. The CLI's
|
|
83
|
+
* `--force prompt` resolves its per-file answers into this set, so "keep the
|
|
84
|
+
* existing one" means that artifact is simply not produced, and the rest of
|
|
85
|
+
* the run continues.
|
|
86
|
+
*/
|
|
87
|
+
readonly skipExisting?: ReadonlySet<string> | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Planned output files that may be replaced, by file name.
|
|
90
|
+
*
|
|
91
|
+
* A narrower `force`: `--force tsdown` says "replace tsdown's bundle, and
|
|
92
|
+
* still ask about anything else".
|
|
93
|
+
*/
|
|
94
|
+
readonly overwrite?: ReadonlySet<string> | undefined;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const defaultBundleOptions: Omit<BundleOptions, "outDir"> = {
|
|
98
|
+
...defaultResolveOptions,
|
|
99
|
+
layout: Layout.PerSpec,
|
|
100
|
+
dryRun: false,
|
|
101
|
+
verifyIntegrity: true,
|
|
102
|
+
force: false,
|
|
103
|
+
};
|