@packall/core 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/{Archive.d.ts → archive.d.ts} +5 -5
  2. package/dist/archive.d.ts.map +1 -0
  3. package/dist/{Bundle.d.ts → bundle.d.ts} +51 -36
  4. package/dist/bundle.d.ts.map +1 -0
  5. package/dist/{DependencyRange.d.ts → dependency-range.d.ts} +2 -2
  6. package/dist/dependency-range.d.ts.map +1 -0
  7. package/dist/{Download.d.ts → download.d.ts} +7 -7
  8. package/dist/download.d.ts.map +1 -0
  9. package/dist/enums/artifact-kind.d.ts +8 -0
  10. package/dist/enums/artifact-kind.d.ts.map +1 -0
  11. package/dist/enums/edge-kind.d.ts +29 -0
  12. package/dist/enums/edge-kind.d.ts.map +1 -0
  13. package/dist/enums/input-file-kind.d.ts +12 -0
  14. package/dist/enums/input-file-kind.d.ts.map +1 -0
  15. package/dist/enums/layout.d.ts +23 -0
  16. package/dist/enums/layout.d.ts.map +1 -0
  17. package/dist/enums/lockfile-format.d.ts +9 -0
  18. package/dist/enums/lockfile-format.d.ts.map +1 -0
  19. package/dist/enums/phase.d.ts +11 -0
  20. package/dist/enums/phase.d.ts.map +1 -0
  21. package/dist/{Errors.d.ts → errors.d.ts} +49 -7
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/index.d.ts +44 -28
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2416 -1180
  26. package/dist/index.js.map +1 -1
  27. package/dist/input-file.d.ts +77 -0
  28. package/dist/input-file.d.ts.map +1 -0
  29. package/dist/{Integrity.d.ts → integrity.d.ts} +3 -3
  30. package/dist/integrity.d.ts.map +1 -0
  31. package/dist/{Layout.d.ts → layout.d.ts} +1 -1
  32. package/dist/{Layout.d.ts.map → layout.d.ts.map} +1 -1
  33. package/dist/locked-resolve.d.ts +47 -0
  34. package/dist/locked-resolve.d.ts.map +1 -0
  35. package/dist/lockfile/bun.d.ts +3 -0
  36. package/dist/lockfile/bun.d.ts.map +1 -0
  37. package/dist/lockfile/detect.d.ts +31 -0
  38. package/dist/lockfile/detect.d.ts.map +1 -0
  39. package/dist/lockfile/json.d.ts +23 -0
  40. package/dist/lockfile/json.d.ts.map +1 -0
  41. package/dist/lockfile/names.d.ts +16 -0
  42. package/dist/lockfile/names.d.ts.map +1 -0
  43. package/dist/lockfile/npm.d.ts +3 -0
  44. package/dist/lockfile/npm.d.ts.map +1 -0
  45. package/dist/lockfile/parse.d.ts +23 -0
  46. package/dist/lockfile/parse.d.ts.map +1 -0
  47. package/dist/lockfile/pnpm.d.ts +20 -0
  48. package/dist/lockfile/pnpm.d.ts.map +1 -0
  49. package/dist/lockfile/tree-builder.d.ts +70 -0
  50. package/dist/lockfile/tree-builder.d.ts.map +1 -0
  51. package/dist/lockfile/types.d.ts +70 -0
  52. package/dist/lockfile/types.d.ts.map +1 -0
  53. package/dist/{Manifest.d.ts → manifest.d.ts} +7 -15
  54. package/dist/manifest.d.ts.map +1 -0
  55. package/dist/{Options.d.ts → options.d.ts} +9 -27
  56. package/dist/options.d.ts.map +1 -0
  57. package/dist/{Platform.d.ts → platform.d.ts} +9 -9
  58. package/dist/platform.d.ts.map +1 -0
  59. package/dist/{Progress.d.ts → progress.d.ts} +10 -9
  60. package/dist/progress.d.ts.map +1 -0
  61. package/dist/{Registry.d.ts → registry.d.ts} +42 -44
  62. package/dist/registry.d.ts.map +1 -0
  63. package/dist/{Resolve.d.ts → resolve.d.ts} +25 -26
  64. package/dist/resolve.d.ts.map +1 -0
  65. package/dist/schemas/lenient.d.ts +38 -0
  66. package/dist/schemas/lenient.d.ts.map +1 -0
  67. package/dist/schemas/package-json.d.ts +44 -0
  68. package/dist/schemas/package-json.d.ts.map +1 -0
  69. package/dist/{Spec.d.ts → spec.d.ts} +4 -4
  70. package/dist/spec.d.ts.map +1 -0
  71. package/dist/types/value-of.d.ts +9 -0
  72. package/dist/types/value-of.d.ts.map +1 -0
  73. package/dist/utils/is-record.d.ts +8 -0
  74. package/dist/utils/is-record.d.ts.map +1 -0
  75. package/package.json +3 -2
  76. package/src/{Archive.ts → archive.ts} +92 -92
  77. package/src/{Bundle.ts → bundle.ts} +616 -570
  78. package/src/{DependencyRange.ts → dependency-range.ts} +115 -115
  79. package/src/{Download.ts → download.ts} +133 -132
  80. package/src/enums/artifact-kind.ts +9 -0
  81. package/src/enums/edge-kind.ts +32 -0
  82. package/src/enums/input-file-kind.ts +13 -0
  83. package/src/enums/layout.ts +25 -0
  84. package/src/enums/lockfile-format.ts +10 -0
  85. package/src/enums/phase.ts +12 -0
  86. package/src/{Errors.ts → errors.ts} +87 -5
  87. package/src/index.ts +133 -35
  88. package/src/{InputFile.ts → input-file.ts} +127 -44
  89. package/src/{Integrity.ts → integrity.ts} +121 -122
  90. package/src/locked-resolve.ts +461 -0
  91. package/src/lockfile/bun.ts +207 -0
  92. package/src/lockfile/detect.ts +80 -0
  93. package/src/lockfile/json.ts +132 -0
  94. package/src/lockfile/names.ts +20 -0
  95. package/src/lockfile/npm.ts +308 -0
  96. package/src/lockfile/parse.ts +56 -0
  97. package/src/lockfile/pnpm.ts +225 -0
  98. package/src/lockfile/tree-builder.ts +166 -0
  99. package/src/lockfile/types.ts +74 -0
  100. package/src/{Manifest.ts → manifest.ts} +133 -133
  101. package/src/{Options.ts → options.ts} +7 -27
  102. package/src/{Platform.ts → platform.ts} +23 -23
  103. package/src/{Progress.ts → progress.ts} +106 -106
  104. package/src/{Registry.ts → registry.ts} +115 -121
  105. package/src/{Resolve.ts → resolve.ts} +537 -528
  106. package/src/schemas/lenient.ts +123 -0
  107. package/src/schemas/package-json.ts +27 -0
  108. package/src/{Spec.ts → spec.ts} +11 -19
  109. package/src/types/value-of.ts +8 -0
  110. package/src/utils/is-record.ts +8 -0
  111. package/dist/Archive.d.ts.map +0 -1
  112. package/dist/Bundle.d.ts.map +0 -1
  113. package/dist/DependencyRange.d.ts.map +0 -1
  114. package/dist/Download.d.ts.map +0 -1
  115. package/dist/Errors.d.ts.map +0 -1
  116. package/dist/InputFile.d.ts +0 -46
  117. package/dist/InputFile.d.ts.map +0 -1
  118. package/dist/Integrity.d.ts.map +0 -1
  119. package/dist/Manifest.d.ts.map +0 -1
  120. package/dist/Options.d.ts.map +0 -1
  121. package/dist/Platform.d.ts.map +0 -1
  122. package/dist/Progress.d.ts.map +0 -1
  123. package/dist/Registry.d.ts.map +0 -1
  124. package/dist/Resolve.d.ts.map +0 -1
  125. package/dist/Spec.d.ts.map +0 -1
  126. /package/src/{Layout.ts → layout.ts} +0 -0
@@ -1,115 +1,115 @@
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
- import semver from "semver"
13
- import type { Selector } from "./Spec.js"
14
-
15
- /** What a dependency edge points at. */
16
- export type DependencyTarget =
17
- /** A normal registry dependency. `name` may differ from the key under an alias. */
18
- | {
19
- readonly _tag: "Registry"
20
- readonly name: string
21
- readonly selector: Selector
22
- /** Set when the edge is an `npm:` alias, e.g. `"lodash4": "npm:lodash@^4"`. */
23
- readonly aliasOf?: string | undefined
24
- }
25
- /** Something we cannot fetch from a registry. */
26
- | { readonly _tag: "Unsupported"; readonly name: string; readonly raw: string; readonly reason: string }
27
-
28
- const UNSUPPORTED_PROTOCOLS: ReadonlyArray<readonly [string, string]> = [
29
- ["file:", "local file path"],
30
- ["link:", "local link"],
31
- ["workspace:", "workspace protocol"],
32
- ["portal:", "portal protocol"],
33
- ["patch:", "patch protocol"],
34
- ["git:", "git dependency"],
35
- ["git+", "git dependency"],
36
- ["github:", "GitHub shorthand"],
37
- ["gitlab:", "GitLab shorthand"],
38
- ["bitbucket:", "Bitbucket shorthand"],
39
- ["http:", "remote tarball URL"],
40
- ["https:", "remote tarball URL"]
41
- ]
42
-
43
- /**
44
- * Classifies one `name -> range` entry.
45
- *
46
- * An empty range, `*`, and `latest` all mean "any published version"; npm
47
- * treats them interchangeably and so do we.
48
- */
49
- export const parseDependencyTarget = (name: string, raw: string): DependencyTarget => {
50
- const text = raw.trim()
51
-
52
- if (text.length === 0 || text === "*" || text === "x" || text === "latest") {
53
- return { _tag: "Registry", name, selector: { _tag: "Range", range: "*" } }
54
- }
55
-
56
- if (text.startsWith("npm:")) {
57
- return parseAlias(name, text)
58
- }
59
-
60
- for (const [prefix, reason] of UNSUPPORTED_PROTOCOLS) {
61
- if (text.startsWith(prefix)) {
62
- return { _tag: "Unsupported", name, raw: text, reason }
63
- }
64
- }
65
-
66
- // Bare `owner/repo` is GitHub shorthand, but only when it is not a valid
67
- // range — `>=1.0.0/2` is not a thing, so this ordering is safe.
68
- if (semver.validRange(text, { loose: true }) === null && /^[\w.-]+\/[\w.-]+/.test(text)) {
69
- return { _tag: "Unsupported", name, raw: text, reason: "git shorthand" }
70
- }
71
-
72
- const exact = semver.valid(text, { loose: true })
73
- if (exact !== null) {
74
- return { _tag: "Registry", name, selector: { _tag: "Exact", version: exact } }
75
- }
76
-
77
- if (semver.validRange(text, { loose: true }) !== null) {
78
- return { _tag: "Registry", name, selector: { _tag: "Range", range: text } }
79
- }
80
-
81
- // Whatever is left is most plausibly a dist-tag published by a private
82
- // registry, e.g. `"@acme/sdk": "stable"`.
83
- if (/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(text)) {
84
- return { _tag: "Registry", name, selector: { _tag: "Tag", tag: text } }
85
- }
86
-
87
- return { _tag: "Unsupported", name, raw: text, reason: "unrecognised version specifier" }
88
- }
89
-
90
- /**
91
- * Parses `npm:<name>[@<range>]`.
92
- *
93
- * The nested name may itself be scoped, so the `@` split has the same
94
- * "not at index 0" caveat as top-level spec parsing.
95
- */
96
- const parseAlias = (key: string, text: string): DependencyTarget => {
97
- const body = text.slice("npm:".length)
98
- if (body.length === 0) {
99
- return { _tag: "Unsupported", name: key, raw: text, reason: "empty npm: alias" }
100
- }
101
-
102
- const at = body.lastIndexOf("@")
103
- const target = at > 0 ? body.slice(0, at) : body
104
- const rangeText = at > 0 ? body.slice(at + 1) : ""
105
-
106
- if (target.length === 0) {
107
- return { _tag: "Unsupported", name: key, raw: text, reason: "empty npm: alias target" }
108
- }
109
-
110
- const inner = parseDependencyTarget(target, rangeText)
111
- if (inner._tag === "Unsupported") {
112
- return { ...inner, name: key, raw: text }
113
- }
114
- return { _tag: "Registry", name: target, selector: inner.selector, aliasOf: key }
115
- }
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
+ import semver from "semver"
13
+ import type { Selector } from "./spec.js"
14
+
15
+ /** What a dependency edge points at. */
16
+ export type DependencyTarget =
17
+ /** A normal registry dependency. `name` may differ from the key under an alias. */
18
+ | {
19
+ readonly _tag: "Registry"
20
+ readonly name: string
21
+ readonly selector: Selector
22
+ /** Set when the edge is an `npm:` alias, e.g. `"lodash4": "npm:lodash@^4"`. */
23
+ readonly aliasOf?: string | undefined
24
+ }
25
+ /** Something we cannot fetch from a registry. */
26
+ | { readonly _tag: "Unsupported"; readonly name: string; readonly raw: string; readonly reason: string }
27
+
28
+ const UNSUPPORTED_PROTOCOLS: ReadonlyArray<readonly [string, string]> = [
29
+ ["file:", "local file path"],
30
+ ["link:", "local link"],
31
+ ["workspace:", "workspace protocol"],
32
+ ["portal:", "portal protocol"],
33
+ ["patch:", "patch protocol"],
34
+ ["git:", "git dependency"],
35
+ ["git+", "git dependency"],
36
+ ["github:", "GitHub shorthand"],
37
+ ["gitlab:", "GitLab shorthand"],
38
+ ["bitbucket:", "Bitbucket shorthand"],
39
+ ["http:", "remote tarball URL"],
40
+ ["https:", "remote tarball URL"]
41
+ ]
42
+
43
+ /**
44
+ * Classifies one `name -> range` entry.
45
+ *
46
+ * An empty range, `*`, and `latest` all mean "any published version"; npm
47
+ * treats them interchangeably and so do we.
48
+ */
49
+ export const parseDependencyTarget = (name: string, raw: string): DependencyTarget => {
50
+ const text = raw.trim()
51
+
52
+ if (text.length === 0 || text === "*" || text === "x" || text === "latest") {
53
+ return { _tag: "Registry", name, selector: { _tag: "Range", range: "*" } }
54
+ }
55
+
56
+ if (text.startsWith("npm:")) {
57
+ return parseAlias(name, text)
58
+ }
59
+
60
+ for (const [prefix, reason] of UNSUPPORTED_PROTOCOLS) {
61
+ if (text.startsWith(prefix)) {
62
+ return { _tag: "Unsupported", name, raw: text, reason }
63
+ }
64
+ }
65
+
66
+ // Bare `owner/repo` is GitHub shorthand, but only when it is not a valid
67
+ // range — `>=1.0.0/2` is not a thing, so this ordering is safe.
68
+ if (semver.validRange(text, { loose: true }) === null && /^[\w.-]+\/[\w.-]+/.test(text)) {
69
+ return { _tag: "Unsupported", name, raw: text, reason: "git shorthand" }
70
+ }
71
+
72
+ const exact = semver.valid(text, { loose: true })
73
+ if (exact !== null) {
74
+ return { _tag: "Registry", name, selector: { _tag: "Exact", version: exact } }
75
+ }
76
+
77
+ if (semver.validRange(text, { loose: true }) !== null) {
78
+ return { _tag: "Registry", name, selector: { _tag: "Range", range: text } }
79
+ }
80
+
81
+ // Whatever is left is most plausibly a dist-tag published by a private
82
+ // registry, e.g. `"@acme/sdk": "stable"`.
83
+ if (/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(text)) {
84
+ return { _tag: "Registry", name, selector: { _tag: "Tag", tag: text } }
85
+ }
86
+
87
+ return { _tag: "Unsupported", name, raw: text, reason: "unrecognised version specifier" }
88
+ }
89
+
90
+ /**
91
+ * Parses `npm:<name>[@<range>]`.
92
+ *
93
+ * The nested name may itself be scoped, so the `@` split has the same
94
+ * "not at index 0" caveat as top-level spec parsing.
95
+ */
96
+ const parseAlias = (key: string, text: string): DependencyTarget => {
97
+ const body = text.slice("npm:".length)
98
+ if (body.length === 0) {
99
+ return { _tag: "Unsupported", name: key, raw: text, reason: "empty npm: alias" }
100
+ }
101
+
102
+ const at = body.lastIndexOf("@")
103
+ const target = at > 0 ? body.slice(0, at) : body
104
+ const rangeText = at > 0 ? body.slice(at + 1) : ""
105
+
106
+ if (target.length === 0) {
107
+ return { _tag: "Unsupported", name: key, raw: text, reason: "empty npm: alias target" }
108
+ }
109
+
110
+ const inner = parseDependencyTarget(target, rangeText)
111
+ if (inner._tag === "Unsupported") {
112
+ return { ...inner, name: key, raw: text }
113
+ }
114
+ return { _tag: "Registry", name: target, selector: inner.selector, aliasOf: key }
115
+ }
@@ -1,132 +1,133 @@
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
- import * as Effect from "effect/Effect"
9
- import * as FileSystem from "effect/FileSystem"
10
- import * as Path from "effect/Path"
11
- import type { PlatformError } from "effect/PlatformError"
12
- import type { BundlerError } from "./Errors.js"
13
- import { IntegrityError, OutputError } from "./Errors.js"
14
- import * as Integrity from "./Integrity.js"
15
- import { packagePath } from "./Layout.js"
16
- import * as Progress from "./Progress.js"
17
- import { Registry } from "./Registry.js"
18
- import type { ResolvedPackage } from "./Resolve.js"
19
-
20
- /** What a completed download run produced. */
21
- export interface DownloadReport {
22
- /** `name@version` -> bytes written. */
23
- readonly sizes: ReadonlyMap<string, number>
24
- /** Packages the registry advertised no usable checksum for. */
25
- readonly unverified: ReadonlyArray<string>
26
- readonly totalBytes: number
27
- }
28
-
29
- /**
30
- * Downloads every package into `destDir`, laid out the way a registry serves
31
- * them.
32
- *
33
- * `verifyIntegrity` defaulting to on is deliberate: a corrupt tarball that
34
- * makes it into a corporate registry is far more expensive than a failed run.
35
- */
36
- export const downloadAll = (
37
- packages: ReadonlyArray<ResolvedPackage>,
38
- destDir: string,
39
- options: { readonly concurrency: number; readonly verifyIntegrity: boolean }
40
- ): Effect.Effect<
41
- DownloadReport,
42
- BundlerError | PlatformError,
43
- Registry | Progress.Progress | FileSystem.FileSystem | Path.Path
44
- > =>
45
- Effect.gen(function*() {
46
- const fs = yield* FileSystem.FileSystem
47
- const path = yield* Path.Path
48
- const registry = yield* Registry
49
-
50
- yield* Progress.emit({ _tag: "PhaseStarted", phase: "download", total: packages.length })
51
-
52
- let completed = 0
53
- const sizes = new Map<string, number>()
54
- const unverified: Array<string> = []
55
-
56
- const results = yield* Effect.forEach(
57
- packages,
58
- (pkg) =>
59
- Effect.gen(function*() {
60
- const key = `${pkg.name}@${pkg.version}`
61
-
62
- yield* Progress.emit({
63
- _tag: "DownloadStarted",
64
- name: pkg.name,
65
- version: pkg.version
66
- })
67
-
68
- const bytes = yield* registry.download(pkg.manifest)
69
-
70
- if (options.verifyIntegrity) {
71
- const result = Integrity.verify(bytes, pkg.manifest.dist)
72
- if (result._tag === "Mismatch") {
73
- return yield* Effect.fail(
74
- new IntegrityError(pkg.name, pkg.version, result.expected, result.actual)
75
- )
76
- }
77
- if (result._tag === "Unverifiable") {
78
- yield* Progress.emit({
79
- _tag: "Warning",
80
- message: `${key} could not be verified (${result.reason})`
81
- })
82
- }
83
- }
84
-
85
- const verifiable =
86
- (pkg.manifest.dist.integrity?.length ?? 0) > 0 ||
87
- (pkg.manifest.dist.shasum?.length ?? 0) > 0
88
-
89
- const relative = packagePath(pkg.name, pkg.version)
90
- const target = path.join(destDir, ...relative.split("/"))
91
-
92
- yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(
93
- Effect.mapError(
94
- (cause) =>
95
- new OutputError(path.dirname(target), "could not create directory", { cause })
96
- )
97
- )
98
- yield* fs.writeFile(target, bytes).pipe(
99
- Effect.mapError((cause) => new OutputError(target, "could not write tarball", { cause }))
100
- )
101
-
102
- // Counting here rather than inside the concurrent body would be
103
- // wrong; each fiber increments as it finishes, which is what makes
104
- // the progress counter monotonic.
105
- completed += 1
106
- yield* Progress.emit({
107
- _tag: "DownloadCompleted",
108
- name: pkg.name,
109
- version: pkg.version,
110
- bytes: bytes.byteLength,
111
- completedCount: completed,
112
- totalCount: packages.length
113
- })
114
-
115
- return { key, bytes: bytes.byteLength, verifiable } as const
116
- }),
117
- { concurrency: options.concurrency }
118
- )
119
-
120
- for (const result of results) {
121
- sizes.set(result.key, result.bytes)
122
- if (!result.verifiable) unverified.push(result.key)
123
- }
124
-
125
- yield* Progress.emit({ _tag: "PhaseCompleted", phase: "download" })
126
-
127
- return {
128
- sizes,
129
- unverified,
130
- totalBytes: results.reduce((sum, r) => sum + r.bytes, 0)
131
- }
132
- })
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
+ import * as Effect from "effect/Effect"
9
+ import * as FileSystem from "effect/FileSystem"
10
+ import * as Path from "effect/Path"
11
+ import type { PlatformError } from "effect/PlatformError"
12
+ import { Phase } from "./enums/phase.js"
13
+ import type { BundlerError } from "./errors.js"
14
+ import { IntegrityError, OutputError } from "./errors.js"
15
+ import * as Integrity from "./integrity.js"
16
+ import { packagePath } from "./layout.js"
17
+ import * as Progress from "./progress.js"
18
+ import { Registry } from "./registry.js"
19
+ import type { ResolvedPackage } from "./resolve.js"
20
+
21
+ /** What a completed download run produced. */
22
+ export type DownloadReport = {
23
+ /** `name@version` -> bytes written. */
24
+ readonly sizes: ReadonlyMap<string, number>
25
+ /** Packages the registry advertised no usable checksum for. */
26
+ readonly unverified: ReadonlyArray<string>
27
+ readonly totalBytes: number
28
+ }
29
+
30
+ /**
31
+ * Downloads every package into `destDir`, laid out the way a registry serves
32
+ * them.
33
+ *
34
+ * `verifyIntegrity` defaulting to on is deliberate: a corrupt tarball that
35
+ * makes it into a corporate registry is far more expensive than a failed run.
36
+ */
37
+ export const downloadAll = (
38
+ packages: ReadonlyArray<ResolvedPackage>,
39
+ destDir: string,
40
+ options: { readonly concurrency: number; readonly verifyIntegrity: boolean }
41
+ ): Effect.Effect<
42
+ DownloadReport,
43
+ BundlerError | PlatformError,
44
+ Registry | Progress.Progress | FileSystem.FileSystem | Path.Path
45
+ > =>
46
+ Effect.gen(function*() {
47
+ const fs = yield* FileSystem.FileSystem
48
+ const path = yield* Path.Path
49
+ const registry = yield* Registry
50
+
51
+ yield* Progress.emit({ _tag: "PhaseStarted", phase: Phase.Download, total: packages.length })
52
+
53
+ let completed = 0
54
+ const sizes = new Map<string, number>()
55
+ const unverified: Array<string> = []
56
+
57
+ const results = yield* Effect.forEach(
58
+ packages,
59
+ (pkg) =>
60
+ Effect.gen(function*() {
61
+ const key = `${pkg.name}@${pkg.version}`
62
+
63
+ yield* Progress.emit({
64
+ _tag: "DownloadStarted",
65
+ name: pkg.name,
66
+ version: pkg.version
67
+ })
68
+
69
+ const bytes = yield* registry.download(pkg.manifest)
70
+
71
+ if (options.verifyIntegrity) {
72
+ const result = Integrity.verify(bytes, pkg.manifest.dist)
73
+ if (result._tag === "Mismatch") {
74
+ return yield* Effect.fail(
75
+ new IntegrityError(pkg.name, pkg.version, result.expected, result.actual)
76
+ )
77
+ }
78
+ if (result._tag === "Unverifiable") {
79
+ yield* Progress.emit({
80
+ _tag: "Warning",
81
+ message: `${key} could not be verified (${result.reason})`
82
+ })
83
+ }
84
+ }
85
+
86
+ const verifiable =
87
+ (pkg.manifest.dist.integrity?.length ?? 0) > 0 ||
88
+ (pkg.manifest.dist.shasum?.length ?? 0) > 0
89
+
90
+ const relative = packagePath(pkg.name, pkg.version)
91
+ const target = path.join(destDir, ...relative.split("/"))
92
+
93
+ yield* fs.makeDirectory(path.dirname(target), { recursive: true }).pipe(
94
+ Effect.mapError(
95
+ (cause) =>
96
+ new OutputError(path.dirname(target), "could not create directory", { cause })
97
+ )
98
+ )
99
+ yield* fs.writeFile(target, bytes).pipe(
100
+ Effect.mapError((cause) => new OutputError(target, "could not write tarball", { cause }))
101
+ )
102
+
103
+ // Counting here rather than inside the concurrent body would be
104
+ // wrong; each fiber increments as it finishes, which is what makes
105
+ // the progress counter monotonic.
106
+ completed += 1
107
+ yield* Progress.emit({
108
+ _tag: "DownloadCompleted",
109
+ name: pkg.name,
110
+ version: pkg.version,
111
+ bytes: bytes.byteLength,
112
+ completedCount: completed,
113
+ totalCount: packages.length
114
+ })
115
+
116
+ return { key, bytes: bytes.byteLength, verifiable } as const
117
+ }),
118
+ { concurrency: options.concurrency }
119
+ )
120
+
121
+ for (const result of results) {
122
+ sizes.set(result.key, result.bytes)
123
+ if (!result.verifiable) unverified.push(result.key)
124
+ }
125
+
126
+ yield* Progress.emit({ _tag: "PhaseCompleted", phase: Phase.Download })
127
+
128
+ return {
129
+ sizes,
130
+ unverified,
131
+ totalBytes: results.reduce((sum, result) => sum + result.bytes, 0)
132
+ }
133
+ })
@@ -0,0 +1,9 @@
1
+ import type { ValueOf } from "../types/value-of.js"
2
+
3
+ /** What a bundle run wrote to the output directory. */
4
+ export const ArtifactKind = {
5
+ Archive: "archive",
6
+ Directory: "directory"
7
+ } as const
8
+
9
+ export type ArtifactKind = ValueOf<typeof ArtifactKind>
@@ -0,0 +1,32 @@
1
+ import type { ValueOf } from "../types/value-of.js"
2
+
3
+ /**
4
+ * Why one package depends on another.
5
+ *
6
+ * `EdgeKind` and `LockedRootKind` are one concept in two parts, which is why
7
+ * they share a file: `Dev` exists only at the top level, because lockfiles
8
+ * record dev dependencies for the project alone — a transitive devDependency is
9
+ * never installed, so it is never locked.
10
+ */
11
+ export const EdgeKind = {
12
+ Prod: "prod",
13
+ Optional: "optional",
14
+ Peer: "peer"
15
+ } as const
16
+
17
+ export type EdgeKind = ValueOf<typeof EdgeKind>
18
+
19
+ /** The kind that only a direct dependency of the locked project can have. */
20
+ export const DirectOnlyKind = {
21
+ Dev: "dev"
22
+ } as const
23
+
24
+ export type DirectOnlyKind = ValueOf<typeof DirectOnlyKind>
25
+
26
+ /** Why a direct dependency of the locked project is a root. */
27
+ export const LockedRootKind = {
28
+ ...EdgeKind,
29
+ ...DirectOnlyKind
30
+ } as const
31
+
32
+ export type LockedRootKind = ValueOf<typeof LockedRootKind>
@@ -0,0 +1,13 @@
1
+ import type { ValueOf } from "../types/value-of.js"
2
+
3
+ /** Which of the three shapes a `--file` input turned out to be. */
4
+ export const InputFileKind = {
5
+ /** Bundle everything a package.json depends on. */
6
+ PackageJson: "package.json",
7
+ /** Bundle the exact versions a lockfile pins. */
8
+ Lockfile: "lockfile",
9
+ /** A newline-delimited list of specs. */
10
+ List: "list"
11
+ } as const
12
+
13
+ export type InputFileKind = ValueOf<typeof InputFileKind>
@@ -0,0 +1,25 @@
1
+ import type { ValueOf } from "../types/value-of.js"
2
+
3
+ /** How the resulting tarball(s) are shaped. */
4
+ export const Layout = {
5
+ /**
6
+ * One tarball per requested spec, each carrying its own complete dependency
7
+ * closure. Shared dependencies repeat across tarballs, so the total is
8
+ * larger — but each one imports, rolls back and hands off independently.
9
+ */
10
+ PerSpec: "per-spec",
11
+ /**
12
+ * One tarball for everything, with the union of all closures deduplicated.
13
+ * Smallest output and a single import step; all-or-nothing to roll back.
14
+ */
15
+ Single: "single",
16
+ /**
17
+ * No archive at all — just the npm-layout directory tree, ready for
18
+ * `jf rt upload` or rsync.
19
+ */
20
+ Dir: "dir"
21
+ } as const
22
+
23
+ export type Layout = ValueOf<typeof Layout>
24
+
25
+ export const Layouts: ReadonlyArray<Layout> = Object.values(Layout)
@@ -0,0 +1,10 @@
1
+ import type { ValueOf } from "../types/value-of.js"
2
+
3
+ /** Which package manager wrote a lockfile. */
4
+ export const LockfileFormat = {
5
+ Npm: "npm",
6
+ Pnpm: "pnpm",
7
+ Bun: "bun"
8
+ } as const
9
+
10
+ export type LockfileFormat = ValueOf<typeof LockfileFormat>
@@ -0,0 +1,12 @@
1
+ import type { ValueOf } from "../types/value-of.js"
2
+
3
+ /** The phases a bundle run moves through, in order. */
4
+ export const Phase = {
5
+ Preflight: "preflight",
6
+ Resolve: "resolve",
7
+ Download: "download",
8
+ Archive: "archive",
9
+ Done: "done"
10
+ } as const
11
+
12
+ export type Phase = ValueOf<typeof Phase>