@packall/core 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/{archive-D1AFjtiX.js → archive-HuF2fp2A.js} +10 -2
  2. package/dist/archive-HuF2fp2A.js.map +1 -0
  3. package/dist/archive.d.ts +9 -18
  4. package/dist/archive.d.ts.map +1 -1
  5. package/dist/bundle.d.ts +0 -18
  6. package/dist/bundle.d.ts.map +1 -1
  7. package/dist/dependency-range.d.ts.map +1 -1
  8. package/dist/download.d.ts +0 -7
  9. package/dist/download.d.ts.map +1 -1
  10. package/dist/errors.d.ts +0 -12
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/index.d.ts +0 -10
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +29 -36
  15. package/dist/index.js.map +1 -1
  16. package/dist/input-file.d.ts +0 -15
  17. package/dist/input-file.d.ts.map +1 -1
  18. package/dist/integrity.d.ts +0 -14
  19. package/dist/integrity.d.ts.map +1 -1
  20. package/dist/layout.d.ts +22 -57
  21. package/dist/layout.d.ts.map +1 -1
  22. package/dist/locked-resolve.d.ts +0 -19
  23. package/dist/locked-resolve.d.ts.map +1 -1
  24. package/dist/lockfile/bun.d.ts.map +1 -1
  25. package/dist/lockfile/detect.d.ts +0 -7
  26. package/dist/lockfile/detect.d.ts.map +1 -1
  27. package/dist/lockfile/json.d.ts.map +1 -1
  28. package/dist/lockfile/npm.d.ts.map +1 -1
  29. package/dist/lockfile/parse.d.ts +0 -14
  30. package/dist/lockfile/parse.d.ts.map +1 -1
  31. package/dist/lockfile/pnpm.d.ts.map +1 -1
  32. package/dist/lockfile/tree-builder.d.ts.map +1 -1
  33. package/dist/lockfile/types.d.ts +0 -7
  34. package/dist/lockfile/types.d.ts.map +1 -1
  35. package/dist/manifest.d.ts.map +1 -1
  36. package/dist/node/archiver.d.ts.map +1 -1
  37. package/dist/node/index.d.ts +0 -11
  38. package/dist/node/index.d.ts.map +1 -1
  39. package/dist/node/index.js +1 -1
  40. package/dist/node/index.js.map +1 -1
  41. package/dist/node/platform.d.ts +0 -9
  42. package/dist/node/platform.d.ts.map +1 -1
  43. package/dist/options.d.ts +0 -7
  44. package/dist/options.d.ts.map +1 -1
  45. package/dist/platform.d.ts +0 -10
  46. package/dist/platform.d.ts.map +1 -1
  47. package/dist/progress.d.ts +0 -8
  48. package/dist/progress.d.ts.map +1 -1
  49. package/dist/registry.d.ts +5 -12
  50. package/dist/registry.d.ts.map +1 -1
  51. package/dist/resolve.d.ts +0 -18
  52. package/dist/resolve.d.ts.map +1 -1
  53. package/dist/schemas/lenient.d.ts.map +1 -1
  54. package/dist/schemas/package-json.d.ts +3 -12
  55. package/dist/schemas/package-json.d.ts.map +1 -1
  56. package/dist/spec.d.ts.map +1 -1
  57. package/package.json +12 -2
  58. package/src/archive.ts +9 -18
  59. package/src/bundle.ts +3 -18
  60. package/src/dependency-range.ts +0 -11
  61. package/src/download.ts +0 -7
  62. package/src/errors.ts +3 -13
  63. package/src/index.ts +2 -14
  64. package/src/input-file.ts +0 -15
  65. package/src/integrity.ts +0 -14
  66. package/src/layout.ts +22 -58
  67. package/src/locked-resolve.ts +5 -19
  68. package/src/lockfile/bun.ts +0 -8
  69. package/src/lockfile/detect.ts +0 -7
  70. package/src/lockfile/json.ts +0 -9
  71. package/src/lockfile/npm.ts +0 -8
  72. package/src/lockfile/parse.ts +0 -14
  73. package/src/lockfile/pnpm.ts +0 -7
  74. package/src/lockfile/tree-builder.ts +0 -6
  75. package/src/lockfile/types.ts +0 -7
  76. package/src/manifest.ts +0 -4
  77. package/src/node/archiver.ts +4 -12
  78. package/src/node/index.ts +0 -12
  79. package/src/node/platform.ts +0 -10
  80. package/src/options.ts +0 -7
  81. package/src/platform.ts +0 -11
  82. package/src/progress.ts +0 -8
  83. package/src/registry.ts +5 -12
  84. package/src/resolve.ts +4 -18
  85. package/src/schemas/lenient.ts +0 -15
  86. package/src/schemas/package-json.ts +4 -13
  87. package/src/spec.ts +0 -7
  88. package/dist/archive-D1AFjtiX.js.map +0 -1
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@packall/core",
3
- "version": "0.5.2",
4
- "description": "Registry-agnostic engine for bundling npm packages and their full dependency closure for offline import.",
3
+ "version": "0.5.3",
4
+ "description": "Registry-agnostic engine that resolves, downloads, verifies and packages an npm dependency closure for offline import.",
5
+ "keywords": [
6
+ "airgap",
7
+ "bundle",
8
+ "dependencies",
9
+ "effect",
10
+ "integrity",
11
+ "npm",
12
+ "offline",
13
+ "tarball"
14
+ ],
5
15
  "homepage": "https://packall.vercel.app",
6
16
  "bugs": {
7
17
  "url": "https://gitlab.com/yelev/packall/-/issues"
package/src/archive.ts CHANGED
@@ -1,20 +1,3 @@
1
- /**
2
- * Turning a staged directory tree into a gzipped tarball.
3
- *
4
- * A service rather than a function, because packing is the one step that has
5
- * to be implemented differently per platform. Everywhere else the engine
6
- * reaches the disk through `effect/FileSystem`, which is what lets an in-memory
7
- * filesystem carry the whole pipeline — but a tar writer is not filesystem
8
- * access, it is a format, and `node-tar` reads the paths it is given with
9
- * Node's own `fs` rather than through the injected service. Left as a direct
10
- * call, packing would quietly bypass the abstraction the rest of the engine
11
- * depends on, which stays invisible until something other than Node provides
12
- * the filesystem.
13
- *
14
- * The implementations live outside this module — `@packall/core/node` for
15
- * Node, a Web Streams writer in the browser — so nothing platform-specific is
16
- * reachable from the engine's own graph.
17
- */
18
1
  import * as Context from "effect/Context";
19
2
  import * as Effect from "effect/Effect";
20
3
  import type * as FileSystem from "effect/FileSystem";
@@ -56,7 +39,15 @@ export declare namespace Archiver {
56
39
  };
57
40
  }
58
41
 
59
- /** Service tag for archive creation. */
42
+ /**
43
+ * Service tag for archive creation.
44
+ *
45
+ * Packing is a service rather than a plain function because it is the one step
46
+ * that cannot go through `effect/FileSystem`: a tar writer reads the paths it
47
+ * is handed with the host's own file APIs, so a direct call would bypass the
48
+ * injected filesystem the rest of the engine runs on. Provide `layerArchiver`
49
+ * from `@packall/core/node`, or an implementation of your own.
50
+ */
60
51
  export class Archiver extends Context.Service<Archiver, Archiver.Service>()(
61
52
  "@packall/core/Archiver",
62
53
  ) {}
package/src/bundle.ts CHANGED
@@ -1,21 +1,3 @@
1
- /**
2
- * End-to-end orchestration: specs in, tarballs out.
3
- *
4
- * Two properties this module is responsible for — see `notes/requirements.md`
5
- * § FR-5 and § NFR-9 for why they are requirements, and the mechanisms below
6
- * for why the code reads as it does:
7
- *
8
- * 1. **Nothing is written outside the output directory.** Every intermediate
9
- * file lives in a scoped temp directory obtained from
10
- * `makeTempDirectoryScoped`, so the scope closing — on success, on failure,
11
- * or on Ctrl-C — takes the whole staging tree with it. The working
12
- * directory is never touched.
13
- *
14
- * 2. **Each package is downloaded exactly once**, no matter how many bundles
15
- * it ends up in. Per-spec archives are assembled by hard-linking out of one
16
- * shared download tree, so `per-spec` costs extra disk in the *output*, not
17
- * extra network.
18
- */
19
1
  import type * as Crypto from "effect/Crypto";
20
2
  import * as Effect from "effect/Effect";
21
3
  import * as FileSystem from "effect/FileSystem";
@@ -320,6 +302,9 @@ const runBundle = (
320
302
 
321
303
  yield* prepareOutputDir(resolution, options);
322
304
 
305
+ // Scoped, not just temporary: the scope closing — on success, on failure,
306
+ // or on Ctrl-C — takes the whole staging tree with it, which is what keeps
307
+ // a crashed run from leaving half a bundle beside the user's own files.
323
308
  const staging = yield* fs.makeTempDirectoryScoped({ prefix: "packall-" });
324
309
  const packagesDir = path.join(staging, "packages");
325
310
  yield* fs.makeDirectory(packagesDir, { recursive: true });
@@ -1,14 +1,3 @@
1
- /**
2
- * Classification of the right-hand side of a dependency entry.
3
- *
4
- * `"dependencies": { "x": "^1.0.0" }` is the easy case. Real package.json files
5
- * also contain aliases, git URLs, tarball URLs, `file:` links and
6
- * `workspace:` protocols — none of which can be fetched from a registry.
7
- *
8
- * Rather than crashing or silently producing a bundle that will not install,
9
- * we classify each edge and let the caller report precisely what was skipped
10
- * and why.
11
- */
12
1
  import semver from "semver";
13
2
 
14
3
  import type { Selector } from "./spec.js";
package/src/download.ts CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * Fetching tarballs into the staging tree.
3
- *
4
- * Downloads run concurrently, every one is checksum-verified before it is
5
- * written, and the destination is always the scoped temp directory — nothing
6
- * lands next to the user's own files.
7
- */
8
1
  import type * as Crypto from "effect/Crypto";
9
2
  import * as Effect from "effect/Effect";
10
3
  import * as FileSystem from "effect/FileSystem";
package/src/errors.ts CHANGED
@@ -1,16 +1,3 @@
1
- /**
2
- * Failure types for the bundler.
3
- *
4
- * These are deliberately plain classes carrying a literal `_tag` rather than
5
- * `Data.TaggedError`. They work with `Effect.catchTag` / `Effect.catchTags`
6
- * exactly the same way, and they keep the public error surface independent of
7
- * any single Effect release — useful while Effect v4 is still in beta.
8
- *
9
- * Every error carries enough context to be actionable without a stack trace,
10
- * because the primary consumer is somebody staring at a terminal on a
11
- * locked-down network trying to work out why a download did not happen.
12
- */
13
-
14
1
  /** Discriminant union of every failure the bundler can produce. */
15
2
  export type BundlerError =
16
3
  | InvalidSpecError
@@ -29,6 +16,9 @@ export type BundlerError =
29
16
  | ArchiveError
30
17
  | OutputError;
31
18
 
19
+ // Plain classes with a literal `_tag` rather than `Data.TaggedError`. They work
20
+ // with `catchTag` / `catchTags` identically, and this keeps the public error
21
+ // surface off a helper that may still move while Effect v4 is in beta.
32
22
  abstract class BundlerErrorBase extends Error {
33
23
  abstract readonly _tag: string;
34
24
  constructor(message: string, options?: ErrorOptions) {
package/src/index.ts CHANGED
@@ -1,14 +1,3 @@
1
- /**
2
- * `@packall/core` — the registry-agnostic bundling engine.
3
- *
4
- * Nothing in here knows about npm specifically. Point it at any implementation
5
- * of the `Registry` service and it will resolve, download, verify and package
6
- * a dependency closure.
7
- *
8
- * This is the package's whole public API. Everything not named here is an
9
- * implementation detail, including modules that happen to be reachable.
10
- */
11
-
12
1
  export type * from "./errors.js";
13
2
  export {
14
3
  ArchiveError,
@@ -88,9 +77,8 @@ export type { IntegrityAlgorithm, IntegrityHash, VerificationResult } from "./in
88
77
  export { digestOf, hexDigestOf, parseIntegrity, verify as verifyIntegrity } from "./integrity.js";
89
78
 
90
79
  // The packing backend. The Node implementation is deliberately *not* re-exported
91
- // here — it lives behind `@packall/core/node`, because it pulls in `node-tar` and
92
- // reaching it has to be an explicit act so importing the engine stays safe in a
93
- // browser.
80
+ // here — it pulls in `node-tar`, so reaching it has to be an explicit import of
81
+ // `@packall/core/node`.
94
82
  export type { ArchiveResult, CreateArchiveOptions } from "./archive.js";
95
83
  export { Archiver, createArchive } from "./archive.js";
96
84
 
package/src/input-file.ts CHANGED
@@ -1,18 +1,3 @@
1
- /**
2
- * `--file` handling.
3
- *
4
- * One flag, three accepted shapes, detected from the content rather than the
5
- * extension so that `--file deps.json` and `--file my-packages` both do the
6
- * obvious thing:
7
- *
8
- * - a **package.json** — bundle everything it depends on
9
- * - a **lockfile** — npm, pnpm or bun; bundle the exact versions it pins
10
- * - a **newline-delimited list** — one spec per line, `#` and `//` comments
11
- * allowed, blank lines ignored
12
- *
13
- * A lockfile is checked for first, because `package-lock.json` and
14
- * `package.json` are both JSON objects and only the content tells them apart.
15
- */
16
1
  import * as Effect from "effect/Effect";
17
2
  import * as FileSystem from "effect/FileSystem";
18
3
  import * as Option from "effect/Option";
package/src/integrity.ts CHANGED
@@ -1,17 +1,3 @@
1
- /**
2
- * Checksum verification for downloaded tarballs.
3
- *
4
- * A failed check discards the download rather than warning and carrying on —
5
- * see `notes/requirements.md` § FR-9 for why that strictness is a requirement
6
- * rather than a preference.
7
- *
8
- * Built on Effect's `Crypto` service rather than `node:crypto` directly. The
9
- * four digests `Crypto` offers — SHA-1, SHA-256, SHA-384, SHA-512 — are exactly
10
- * the four npm publishes, so nothing is given up, and the same verification
11
- * then runs unchanged against WebCrypto in a browser. Parsing stays here
12
- * because the subset of Subresource Integrity npm actually emits is small and
13
- * entirely mechanical; `ssri` would be a dependency for a regex.
14
- */
15
1
  import * as Crypto from "effect/Crypto";
16
2
  import * as Effect from "effect/Effect";
17
3
  import * as Encoding from "effect/Encoding";
package/src/layout.ts CHANGED
@@ -1,23 +1,3 @@
1
- /**
2
- * The on-disk shape of a bundle.
3
- *
4
- * Under every layout but `flat`, everything is laid out exactly the way a
5
- * registry serves it:
6
- *
7
- * lodash/-/lodash-4.17.21.tgz
8
- * @babel/core/-/core-7.24.0.tgz
9
- *
10
- * That is not an Artifactory convention — it is the path structure in every
11
- * `dist.tarball` URL npm publishes. See `notes/requirements.md` § FR-12 for
12
- * what that buys.
13
- *
14
- * The one shape worth noticing before editing anything here: a scoped
15
- * package's file name drops the scope, so `@babel/core` becomes
16
- * `core-7.24.0.tgz` under an `@babel/core/-/` directory.
17
- *
18
- * `flat` layout is the exception; `bundlePath` is where the two part company.
19
- */
20
-
21
1
  import { Layout } from "./enums/layout.js";
22
2
 
23
3
  /** Splits `@scope/name` into its parts. `scope` is undefined when unscoped. */
@@ -30,7 +10,12 @@ export const splitName = (
30
10
  return { scope: name.slice(0, slash), bare: name.slice(slash + 1) };
31
11
  };
32
12
 
33
- /** `@babel/core` + `7.24.0` -> `core-7.24.0.tgz` */
13
+ /**
14
+ * `@babel/core` + `7.24.0` -> `core-7.24.0.tgz`
15
+ *
16
+ * The scope is dropped, because that is what the registry serves. It is also
17
+ * why a flat directory of tarballs cannot use this name — see {@link flatName}.
18
+ */
34
19
  export const tarballFileName = (name: string, version: string): string => {
35
20
  const { bare } = splitName(name);
36
21
  return `${bare}-${version}.tgz`;
@@ -51,43 +36,22 @@ export const MANIFEST_FILE = "bundle-manifest.json";
51
36
  export const README_FILE = "IMPORT.md";
52
37
 
53
38
  /**
54
- * A flat, collision-free file name for one `name@version`.
55
- *
56
- * It is the spec, with the one character a file name cannot hold taken out:
57
- * `@types/react@19.2.18` becomes `@types-react@19.2.18.tgz`. So the name of the
58
- * file answers the question somebody has when they are looking at a directory
59
- * of them which package, and which version in the notation they would have
60
- * typed to ask for it.
61
- *
62
- * The version is joined with `@` rather than `-` for the same reason. A
63
- * hyphen made `foo-bar-1.0.0` ambiguous by eye: package `foo` at `bar-1.0.0`,
64
- * or `foo-bar` at `1.0.0`? npm's own syntax has no such problem, because a
65
- * package name may not contain `@` beyond its scope.
66
- *
67
- * **The scope's `@` stays**, and it is the difference between a correct bundle
68
- * and a quietly incomplete one.
69
- *
70
- * Dropping it — which this did, matching what `npm pack` produces — meant
71
- * `@something/somewhat` and `something-somewhat` were both
72
- * `something-somewhat-1.0.0.tgz`. Two different packages, one path: the second
73
- * landed on the first, and the run reported two artifacts for the one file that
74
- * survived. A bundle short a package, called complete, found at the far end of
75
- * an air gap.
76
- *
77
- * Keeping the `@` makes that impossible by construction rather than by a check,
78
- * because an unscoped npm name may not begin with one. It is also the more
79
- * honest name: the scope is part of what the package *is*, and a directory of
80
- * archives that has forgotten which ones were scoped is a directory somebody
81
- * has to cross-reference against a manifest.
82
- *
83
- * It does not make every collision impossible — `@a/b-c` and `@a-b/c` still
84
- * meet — so the run still checks before downloading anything. That pair is
85
- * pathological; the scoped-against-unscoped one is `@babel/core` beside
86
- * `babel-core`, which is a real migration somebody could be bundling.
87
- *
88
- * The leading `@` is safe here in a way it is not inside the archive: tar's
89
- * splice convention applies to the *file list* it is given, which is what
90
- * `archive.ts` guards with `./`. These are output paths, not entries.
39
+ * A flat, collision-free file name for one `name@version`:
40
+ * `@types/react@19.2.18` becomes `@types-react@19.2.18.tgz`.
41
+ *
42
+ * **The scope's `@` stays.** Dropping it which this did, matching `npm pack`
43
+ * put `@something/somewhat` and `something-somewhat` on the same file name,
44
+ * and the run reported two artifacts for the one file that survived. Keeping it
45
+ * makes that impossible by construction rather than by a check, because an
46
+ * unscoped npm name may not begin with `@`.
47
+ *
48
+ * The version joins with `@` rather than `-` because `foo-bar-1.0.0` is
49
+ * ambiguous by eye: `foo` at `bar-1.0.0`, or `foo-bar` at `1.0.0`?
50
+ *
51
+ * `@a/b-c` against `@a-b/c` still collides, so callers check before
52
+ * downloading. The leading `@` is safe here in a way it is not inside an
53
+ * archive: tar's splice convention applies to the entry list, which the
54
+ * archiver guards with `./`. These are output paths, not entries.
91
55
  */
92
56
  export const flatName = (name: string, version: string): string => {
93
57
  const { scope, bare } = splitName(name);
@@ -1,22 +1,3 @@
1
- /**
2
- * Resolution driven by a lockfile instead of by semver.
3
- *
4
- * The distinction matters more than it first looks. Feeding a lockfile's
5
- * versions in as exact root specs and letting the ordinary walk run would still
6
- * produce the wrong bundle: every *transitive* edge is a range, and
7
- * `Resolve.walk` answers a range with whatever is newest today. Lock `tslib` at
8
- * 2.6.0 and let something depending on `tslib@^2` be re-resolved, and the
9
- * bundle grows a second copy at 2.8.1 that CI never installed.
10
- *
11
- * So the lockfile is treated as the graph, not as a set of hints. The package
12
- * set *is* what the file pins, and every edge is satisfied from inside it.
13
- * Nothing here calls `selectVersions`; the only thing the registry is asked for
14
- * is the metadata that a lockfile does not record — `dist`, and the `os`/`cpu`
15
- * constraints that `--platform` narrows on.
16
- *
17
- * The result is an ordinary `Resolution`, so layout, archiving, the manifest
18
- * and the summary cannot tell which path produced it.
19
- */
20
1
  import * as Effect from "effect/Effect";
21
2
  import semver from "semver";
22
3
 
@@ -78,6 +59,11 @@ export const resolveLocked = (
78
59
  // Everything the lockfile can reach, before the platform filter has had a
79
60
  // say. Cheap — no network at all — and it is what decides the fetch list.
80
61
  const reachable = reachableKeys(lock, roots, options);
62
+ // Nothing here re-resolves a range. Handing these versions to `Resolve.walk`
63
+ // instead would answer every *transitive* range with whatever is newest
64
+ // today, so a lockfile pinning `tslib@2.6.0` would still gain a 2.8.1 that
65
+ // CI never installed. The registry is asked only for what a lockfile does
66
+ // not record.
81
67
  const manifests = yield* fetchManifests(lock, reachable, options);
82
68
 
83
69
  const warnings: Array<ResolutionWarning> = lock.warnings.map((message) => ({ message }));
@@ -1,11 +1,3 @@
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
1
  import { EdgeKind, LockedRootKind } from "../enums/edge-kind.js";
10
2
  import { LockfileFormat } from "../enums/lockfile-format.js";
11
3
  import { isRecord } from "../utils/is-record.js";
@@ -1,10 +1,3 @@
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
1
  import { LockfileFormat } from "../enums/lockfile-format.js";
9
2
  import { isRecord } from "../utils/is-record.js";
10
3
  import { tryParseJsonc } from "./json.js";
@@ -1,12 +1,3 @@
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
1
  import * as Option from "effect/Option";
11
2
  import * as Schema from "effect/Schema";
12
3
 
@@ -1,11 +1,3 @@
1
- /**
2
- * `package-lock.json` / `npm-shrinkwrap.json`.
3
- *
4
- * npm records *ranges* on its edges and relies on the directory nesting to say
5
- * which copy of a package a range resolved to, so the node resolution walk-up —
6
- * look beside me, then in my parent, then my grandparent — is reimplemented
7
- * here.
8
- */
9
1
  import { EdgeKind, LockedRootKind } from "../enums/edge-kind.js";
10
2
  import { LockfileFormat } from "../enums/lockfile-format.js";
11
3
  import { isRecord } from "../utils/is-record.js";
@@ -1,17 +1,3 @@
1
- /**
2
- * Lockfile parsing — npm, pnpm and bun.
3
- *
4
- * `--file package.json` reads *ranges*, and a range resolves to whatever is
5
- * newest at the moment the command runs. That is the right answer for "bundle
6
- * me something installable" and the wrong one for "bundle exactly what CI
7
- * installed" — which is what a lockfile records and what this module reads.
8
- *
9
- * Three formats, one output shape. Each parser's real job is the same: turn a
10
- * manager-specific encoding of "package X at path P depends on Y" into a flat
11
- * graph keyed by `name@version`, with every edge already pointing at a concrete
12
- * version. Once that is done, nothing downstream has to know which manager
13
- * wrote the file.
14
- */
15
1
  import { LockfileFormat } from "../enums/lockfile-format.js";
16
2
  import { LockfileError } from "../errors.js";
17
3
  import { parseBunLockfile } from "./bun.js";
@@ -1,10 +1,3 @@
1
- /**
2
- * `pnpm-lock.yaml`.
3
- *
4
- * The easy one: pnpm's snapshots already name exact versions, so there is no
5
- * nesting walk-up to reimplement. The fiddly parts are the three package-key
6
- * encodings still in the wild and the parenthesised peer-resolution suffix.
7
- */
8
1
  import semver from "semver";
9
2
  import { parse as parseYaml } from "yaml";
10
3
 
@@ -1,9 +1,3 @@
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
1
  import semver from "semver";
8
2
 
9
3
  import { parseDependencyTarget } from "../dependency-range.js";
@@ -1,10 +1,3 @@
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
1
  import type { EdgeKind, LockedRootKind } from "../enums/edge-kind.js";
9
2
  import type { LockfileFormat } from "../enums/lockfile-format.js";
10
3
 
package/src/manifest.ts CHANGED
@@ -1,7 +1,3 @@
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
1
  import { bundlePath } from "./layout.js";
6
2
  import type { BundleOptions } from "./options.js";
7
3
  import { formatPlatformFilter } from "./platform.js";
@@ -1,15 +1,3 @@
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
1
  import * as Effect from "effect/Effect";
14
2
  import * as FileSystem from "effect/FileSystem";
15
3
  import * as Layer from "effect/Layer";
@@ -20,6 +8,10 @@ import type { ArchiveResult, CreateArchiveOptions } from "../archive.js";
20
8
  import { Archiver } from "../archive.js";
21
9
  import { ArchiveError } from "../errors.js";
22
10
 
11
+ // node-tar is what npm itself packs with, and the dependency is load-bearing
12
+ // rather than convenient: scoped package paths routinely exceed tar's 100-byte
13
+ // name field, so anything hand-written here would have to get prefix-splitting
14
+ // and PAX headers right before npm could read the result.
23
15
  const createArchive = (
24
16
  options: CreateArchiveOptions,
25
17
  ): Effect.Effect<ArchiveResult, ArchiveError | PlatformError, FileSystem.FileSystem> =>
package/src/node/index.ts CHANGED
@@ -1,14 +1,2 @@
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
1
  export { layerArchiver } from "./archiver.js";
14
2
  export { currentPlatform } from "./platform.js";
@@ -1,13 +1,3 @@
1
- /**
2
- * The machine we are currently running on.
3
- *
4
- * One function, in its own file, for the reason the whole `/node` subpath
5
- * exists: it reads `process`, and importing `@packall/core` has to stay safe in
6
- * a browser. Everything else in `platform.ts` is string work over what a
7
- * manifest declares — it has no host and needs none, so it stays on the main
8
- * entry where the engine can reach it.
9
- */
10
-
11
1
  import type { PlatformTarget } from "../platform.js";
12
2
 
13
3
  /** The machine we are currently running on. */
package/src/options.ts CHANGED
@@ -1,10 +1,3 @@
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
1
  import { Layout } from "./enums/layout.js";
9
2
  import type { PlatformFilter } from "./platform.js";
10
3
  import { allPlatforms } from "./platform.js";
package/src/platform.ts CHANGED
@@ -1,14 +1,3 @@
1
- /**
2
- * Platform matching for optional dependencies.
3
- *
4
- * The default is "keep every platform" and narrowing is opt-in — see
5
- * `notes/requirements.md` § FR-13 for the failure mode that decides it.
6
- *
7
- * The mechanics that decision rests on are here rather than there: a package
8
- * declares `os`, `cpu` and `libc` in its own manifest, and this module keeps a
9
- * package when the filter *could* install it, not when it matches exactly.
10
- */
11
-
12
1
  /** A target to keep, e.g. `linux-x64`, `linux-x64-musl`, or just `linux`. */
13
2
  export type PlatformTarget = {
14
3
  readonly os: string;
package/src/progress.ts CHANGED
@@ -1,11 +1,3 @@
1
- /**
2
- * Progress reporting.
3
- *
4
- * The engine emits structured events; it never writes to a terminal. That
5
- * separation is what lets the CLI draw a live TTY view, CI print plain lines,
6
- * and the test suite assert on an array of events without any of them
7
- * interfering with each other.
8
- */
9
1
  import * as Context from "effect/Context";
10
2
  import * as Effect from "effect/Effect";
11
3
  import * as Layer from "effect/Layer";
package/src/registry.ts CHANGED
@@ -1,15 +1,3 @@
1
- /**
2
- * The registry seam.
3
- *
4
- * This is the **only** place in the engine that knows a registry exists.
5
- * Everything downstream — range resolution, the dependency walk, layout,
6
- * archiving — is expressed against these three operations, so swapping npm for
7
- * something else means implementing this interface and nothing more.
8
- *
9
- * `@packall/registry-npm` provides the implementation that speaks the plain
10
- * npm registry protocol, which is what Artifactory, Nexus, Verdaccio, GitHub
11
- * Packages and registry.npmjs.org all serve.
12
- */
13
1
  import * as Context from "effect/Context";
14
2
  import type * as Effect from "effect/Effect";
15
3
 
@@ -100,6 +88,11 @@ export declare namespace Registry {
100
88
  /**
101
89
  * Service tag for the registry backend.
102
90
  *
91
+ * The engine's only route to a registry. Version selection, the dependency
92
+ * walk, layout and archiving are all written against these four operations, so
93
+ * backing a registry that does not speak the npm protocol means implementing
94
+ * this interface and nothing else.
95
+ *
103
96
  * @example
104
97
  * ```ts
105
98
  * import { Effect } from "effect"
package/src/resolve.ts CHANGED
@@ -1,21 +1,3 @@
1
- /**
2
- * Version selection and the dependency closure walk.
3
- *
4
- * This is the part that decides *what* ends up in a bundle. Its only side
5
- * effect is asking the `Registry` service for metadata, which makes the whole
6
- * of it testable against a fake registry with no network at all.
7
- *
8
- * The walk is breadth-first in waves: resolve the current frontier
9
- * concurrently, collect the newly discovered edges, repeat. Wave-based BFS
10
- * gives real parallelism without a dynamic worker pool, and because each wave
11
- * merges its results serially there is no shared mutable state being written
12
- * from more than one fiber.
13
- *
14
- * One subtlety worth calling out: metadata is cached across walks, but each
15
- * walk still traverses the full graph. That is what lets `per-spec` layout ask
16
- * "what does *this* root version depend on" many times over while the network
17
- * is touched exactly once per package.
18
- */
19
1
  import * as Effect from "effect/Effect";
20
2
  import semver from "semver";
21
3
 
@@ -352,6 +334,10 @@ const walk = (
352
334
  }
353
335
  if (wave.length === 0) break;
354
336
 
337
+ // A wave resolves concurrently and then merges serially below. That is
338
+ // what gives real parallelism without a worker pool, and it is why
339
+ // nothing writes to `state` from more than one fiber — moving the merge
340
+ // inside `resolveTask` would reintroduce exactly that.
355
341
  const results = yield* Effect.forEach(
356
342
  wave,
357
343
  (task) => resolveTask(state, task, options),
@@ -1,18 +1,3 @@
1
- /**
2
- * Schemas for JSON nobody controls.
3
- *
4
- * Registry responses, lockfiles and hand-edited package.json files all arrive
5
- * as untyped JSON, and the rule at every one of those boundaries is the same:
6
- * drop what cannot be understood, keep the rest. A bundler that rejects a
7
- * packument because one published version carries a field it has never seen is
8
- * useless against exactly the private registries it exists to serve.
9
- *
10
- * Effect Schema does the narrowing, which is what keeps those boundaries free
11
- * of casts and hand-rolled `typeof` ladders. What this module adds on top of
12
- * the stock combinators is that tolerance: `Schema.Record(String, String)`
13
- * fails the whole record over one numeric value, where the wire format wants
14
- * that entry skipped and its neighbours kept.
15
- */
16
1
  import * as Effect from "effect/Effect";
17
2
  import * as Option from "effect/Option";
18
3
  import * as Schema from "effect/Schema";