@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
@@ -15,6 +15,9 @@
15
15
  export type BundlerError =
16
16
  | InvalidSpecError
17
17
  | InvalidInputFileError
18
+ | LockfileError
19
+ | LockfileIncompleteError
20
+ | LockfileOutOfDateError
18
21
  | RegistryUnreachableError
19
22
  | PackageNotFoundError
20
23
  | VersionNotFoundError
@@ -27,8 +30,8 @@ export type BundlerError =
27
30
 
28
31
  abstract class BundlerErrorBase extends Error {
29
32
  abstract readonly _tag: string
30
- constructor(message: string, options?: { cause?: unknown }) {
31
- super(message, options as ErrorOptions)
33
+ constructor(message: string, options?: ErrorOptions) {
34
+ super(message, options)
32
35
  this.name = new.target.name
33
36
  }
34
37
  }
@@ -57,6 +60,85 @@ export class InvalidInputFileError extends BundlerErrorBase {
57
60
  }
58
61
  }
59
62
 
63
+ /** A lockfile could not be read, or is in a format we do not support. */
64
+ export class LockfileError extends BundlerErrorBase {
65
+ readonly _tag = "LockfileError" as const
66
+ readonly path: string
67
+ readonly reason: string
68
+ constructor(path: string, reason: string, options?: { cause?: unknown }) {
69
+ // Opens with "Lockfile" so the renderer's tag prefix does not stutter, and
70
+ // because not every reason is a read failure — "does not cover this
71
+ // package" is about a file that read perfectly well.
72
+ super(`Lockfile ${path}: ${reason}`, options)
73
+ this.path = path
74
+ this.reason = reason
75
+ }
76
+ }
77
+
78
+ /**
79
+ * The lockfile parsed, but does not pin everything the bundle needs.
80
+ *
81
+ * Falling back to a range here would defeat the entire point of reading a
82
+ * lockfile — you would get a bundle that is *mostly* what CI installed, with no
83
+ * indication of which parts were guessed. So this is fatal, and it names every
84
+ * gap at once so one `npm install` fixes all of them.
85
+ */
86
+ export class LockfileIncompleteError extends BundlerErrorBase {
87
+ readonly _tag = "LockfileIncompleteError" as const
88
+ readonly path: string
89
+ readonly missing: ReadonlyArray<string>
90
+ constructor(
91
+ path: string,
92
+ missing: ReadonlyArray<string>,
93
+ detail: string,
94
+ remedy: string
95
+ ) {
96
+ const shown = missing.slice(0, 10).map((entry) => ` ${entry}`).join("\n")
97
+ const rest = missing.length > 10 ? `\n … and ${missing.length - 10} more` : ""
98
+ super(
99
+ `${path} is incomplete — ${detail}:\n${shown}${rest}\n` +
100
+ ` ${remedy}\n` +
101
+ ` Or pass --lockfile off to resolve version ranges fresh instead.`
102
+ )
103
+ this.path = path
104
+ this.missing = missing
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The lockfile pins versions the registry no longer serves.
110
+ *
111
+ * Unpublished, or never mirrored into a private registry. Either way the
112
+ * bundle cannot be built as specified, and quietly substituting a nearby
113
+ * version would produce exactly the mismatch this feature exists to prevent.
114
+ */
115
+ export class LockfileOutOfDateError extends BundlerErrorBase {
116
+ readonly _tag = "LockfileOutOfDateError" as const
117
+ readonly path: string
118
+ readonly registry: string
119
+ readonly missing: ReadonlyArray<{ readonly name: string; readonly version: string }>
120
+ constructor(
121
+ path: string,
122
+ registry: string,
123
+ missing: ReadonlyArray<{ readonly name: string; readonly version: string; readonly detail: string }>
124
+ ) {
125
+ const shown = missing
126
+ .slice(0, 10)
127
+ .map((entry) => ` ${entry.name}@${entry.version} — ${entry.detail}`)
128
+ .join("\n")
129
+ const rest = missing.length > 10 ? `\n … and ${missing.length - 10} more` : ""
130
+ super(
131
+ `${path} pins ${missing.length} version${missing.length === 1 ? "" : "s"} ` +
132
+ `that ${registry} does not serve:\n${shown}${rest}\n` +
133
+ ` Refresh the lockfile against this registry, or pass --lockfile off to\n` +
134
+ ` resolve version ranges fresh instead.`
135
+ )
136
+ this.path = path
137
+ this.registry = registry
138
+ this.missing = missing.map(({ name, version }) => ({ name, version }))
139
+ }
140
+ }
141
+
60
142
  /**
61
143
  * The registry could not be reached at all — DNS failure, refused connection,
62
144
  * proxy blackhole, or a preflight that timed out.
@@ -67,11 +149,11 @@ export class InvalidInputFileError extends BundlerErrorBase {
67
149
  export class RegistryUnreachableError extends BundlerErrorBase {
68
150
  readonly _tag = "RegistryUnreachableError" as const
69
151
  readonly registry: string
70
- readonly timeoutMillis: number | undefined
152
+ readonly timeoutMs: number | undefined
71
153
  constructor(
72
154
  registry: string,
73
155
  detail: string,
74
- options?: { cause?: unknown; timeoutMillis?: number }
156
+ options?: { cause?: unknown; timeoutMs?: number }
75
157
  ) {
76
158
  super(
77
159
  `Cannot reach registry ${registry}: ${detail}.\n` +
@@ -79,7 +161,7 @@ export class RegistryUnreachableError extends BundlerErrorBase {
79
161
  options
80
162
  )
81
163
  this.registry = registry
82
- this.timeoutMillis = options?.timeoutMillis
164
+ this.timeoutMs = options?.timeoutMs
83
165
  }
84
166
  }
85
167
 
package/src/index.ts CHANGED
@@ -4,41 +4,139 @@
4
4
  * Nothing in here knows about npm specifically. Point it at any implementation
5
5
  * of the `Registry` service and it will resolve, download, verify and package
6
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.
7
10
  */
8
11
 
9
- export * as Archive from "./Archive.js"
10
- export * as Bundle from "./Bundle.js"
11
- export * as DependencyRange from "./DependencyRange.js"
12
- export * as Download from "./Download.js"
13
- export * as Integrity from "./Integrity.js"
14
- export * as InputFile from "./InputFile.js"
15
- export * as Layout from "./Layout.js"
16
- export * as Manifest from "./Manifest.js"
17
- export * as Options from "./Options.js"
18
- export * as Platform from "./Platform.js"
19
- export * as Progress from "./Progress.js"
20
- export * as Resolve from "./Resolve.js"
21
- export * as Spec from "./Spec.js"
22
-
23
- // Flat re-exports of the things a consumer reaches for constantly.
24
- export { bundle, bundleResolved, plan, plannedOutputs, summarize, withLayout } from "./Bundle.js"
25
- export type { BundleArtifact, BundleContext, BundleResult, PlanSummary } from "./Bundle.js"
26
- export { Registry } from "./Registry.js"
27
- export type { Packument, PackageDist, PackageManifest, RegistryService } from "./Registry.js"
28
- export { Progress as ProgressTag } from "./Progress.js"
29
- export type { ProgressEvent } from "./Progress.js"
30
- export { resolve } from "./Resolve.js"
31
- export type { Resolution, ResolvedPackage, RootResolution } from "./Resolve.js"
32
- export { parseSpec, parseSpecs } from "./Spec.js"
33
- export type { PackageSpec, Selector } from "./Spec.js"
34
- export type { BundleOptions, DependencyScope, Layout as LayoutKind, ResolveOptions } from "./Options.js"
12
+ export type * from "./errors.js"
13
+ export {
14
+ ArchiveError,
15
+ AuthenticationError,
16
+ IntegrityError,
17
+ InvalidInputFileError,
18
+ InvalidSpecError,
19
+ LockfileError,
20
+ LockfileIncompleteError,
21
+ LockfileOutOfDateError,
22
+ NoMatchingVersionsError,
23
+ OutputError,
24
+ PackageNotFoundError,
25
+ RegistryResponseError,
26
+ RegistryUnreachableError,
27
+ VersionNotFoundError
28
+ } from "./errors.js"
29
+
30
+ export { ArtifactKind } from "./enums/artifact-kind.js"
31
+ export { DirectOnlyKind, EdgeKind, LockedRootKind } from "./enums/edge-kind.js"
32
+ export { InputFileKind } from "./enums/input-file-kind.js"
33
+ export { Layout, Layouts } from "./enums/layout.js"
34
+ export { LockfileFormat } from "./enums/lockfile-format.js"
35
+ export { Phase } from "./enums/phase.js"
36
+
37
+ export type { ValueOf } from "./types/value-of.js"
38
+ export { isRecord } from "./utils/is-record.js"
39
+
40
+ // The tolerant decoders every boundary that reads untrusted JSON is built from
41
+ // — registry responses here, package.json and lockfiles inside the engine.
42
+ export {
43
+ OptionalFlagRecordOrAbsentSchema,
44
+ OptionalFlagRecordSchema,
45
+ OptionalStringArraySchema,
46
+ OptionalStringRecordSchema,
47
+ StringArraySchema,
48
+ StringRecordSchema,
49
+ tolerant
50
+ } from "./schemas/lenient.js"
51
+
52
+ export type { PackageSpec, Selector } from "./spec.js"
53
+ export { dedupeSpecs, formatSelector, formatSpec, parseSpec, parseSpecs } from "./spec.js"
54
+
55
+ export type { DependencyTarget } from "./dependency-range.js"
56
+ export { parseDependencyTarget } from "./dependency-range.js"
57
+
58
+ export type { PlatformConstraints, PlatformFilter, PlatformTarget } from "./platform.js"
59
+ export {
60
+ allPlatforms,
61
+ currentPlatform,
62
+ formatPlatformFilter,
63
+ isIncluded,
64
+ parsePlatformTarget,
65
+ platformTargets
66
+ } from "./platform.js"
67
+
68
+ export type { BundleOptions, DependencyScope, ResolveOptions } from "./options.js"
69
+ export { defaultBundleOptions, defaultResolveOptions, defaultScope } from "./options.js"
70
+
71
+ export type { Packument, PackageDist, PackageManifest } from "./registry.js"
72
+ // `Registry.Service` and `Progress.Service` travel with their class through
73
+ // declaration merging, so the one name carries the tag and its shape.
74
+ export { Registry } from "./registry.js"
75
+
76
+ export type { ProgressEvent } from "./progress.js"
77
+ // Aliased: `emit` and `layerSilent` say what they do inside `progress.ts`, and
78
+ // nothing at all on a package's public surface.
79
+ export {
80
+ emit as emitProgress,
81
+ layerCallback as layerCallbackProgress,
82
+ layerSilent as layerSilentProgress,
83
+ makeCollector as makeProgressCollector,
84
+ Progress
85
+ } from "./progress.js"
86
+
87
+ export type { IntegrityHash, VerificationResult } from "./integrity.js"
88
+ export { digestOf, hexDigestOf, parseIntegrity, verify as verifyIntegrity } from "./integrity.js"
89
+
90
+ export {
91
+ importGuide,
92
+ MANIFEST_FILE,
93
+ packagePath,
94
+ perSpecArchiveName,
95
+ README_FILE,
96
+ singleArchiveName,
97
+ splitName,
98
+ tarballFileName
99
+ } from "./layout.js"
100
+
101
+ export type { BundleManifest, ManifestEntry } from "./manifest.js"
102
+ export { buildManifest, MANIFEST_VERSION, serializeManifest } from "./manifest.js"
103
+
104
+ export type {
105
+ PackageKey,
106
+ Reason,
107
+ Resolution,
108
+ ResolutionWarning,
109
+ ResolvedPackage,
110
+ RootResolution
111
+ } from "./resolve.js"
112
+ export { indexPackages, packageKey, resolve, selectVersions } from "./resolve.js"
113
+
114
+ export type { LockedResolveOptions } from "./locked-resolve.js"
115
+ export { resolveLocked } from "./locked-resolve.js"
116
+
117
+ export type { LockfileDetection } from "./lockfile/detect.js"
118
+ export { detectLockfile } from "./lockfile/detect.js"
119
+ export type {
120
+ LockedEdge,
121
+ LockedPackage,
122
+ LockedRoot,
123
+ LockedTree,
124
+ LockfileParseOptions
125
+ } from "./lockfile/types.js"
126
+ export { formatByLockfileName, lockfileNamesFor } from "./lockfile/names.js"
127
+ export { parseLockfile } from "./lockfile/parse.js"
128
+
129
+ export type { InputFileOptions, InputFileResult } from "./input-file.js"
130
+ export { defaultInputFileOptions, parseInputFile, readInputFile } from "./input-file.js"
131
+
132
+ export type { BundleArtifact, BundleContext, BundleResult, PlannedOutput, PlanSummary } from "./bundle.js"
35
133
  export {
36
- defaultBundleOptions,
37
- defaultResolveOptions,
38
- defaultScope,
39
- layouts
40
- } from "./Options.js"
41
- export type { PlatformFilter, PlatformTarget } from "./Platform.js"
42
- export { allPlatforms, currentPlatform, parsePlatformTarget, platformTargets } from "./Platform.js"
43
-
44
- export * from "./Errors.js"
134
+ bundle,
135
+ bundleLocked,
136
+ bundleResolved,
137
+ plan,
138
+ planLocked,
139
+ plannedOutputs,
140
+ summarize,
141
+ withLayout
142
+ } from "./bundle.js"
@@ -1,34 +1,62 @@
1
1
  /**
2
2
  * `--file` handling.
3
3
  *
4
- * One flag, two accepted shapes, detected from the content rather than the
4
+ * One flag, three accepted shapes, detected from the content rather than the
5
5
  * extension so that `--file deps.json` and `--file my-packages` both do the
6
6
  * obvious thing:
7
7
  *
8
8
  * - a **package.json** — bundle everything it depends on
9
+ * - a **lockfile** — npm, pnpm or bun; bundle the exact versions it pins
9
10
  * - a **newline-delimited list** — one spec per line, `#` and `//` comments
10
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.
11
15
  */
12
16
  import * as Effect from "effect/Effect"
13
17
  import * as FileSystem from "effect/FileSystem"
14
- import { parseDependencyTarget } from "./DependencyRange.js"
15
- import { InvalidInputFileError } from "./Errors.js"
16
- import type { InvalidSpecError } from "./Errors.js"
17
- import type { PackageSpec } from "./Spec.js"
18
- import { parseSpecs } from "./Spec.js"
19
-
20
- /** Which of the two shapes a file turned out to be. */
21
- export type InputFileKind = "package.json" | "list"
18
+ import * as Option from "effect/Option"
19
+ import { parseDependencyTarget } from "./dependency-range.js"
20
+ import { InputFileKind } from "./enums/input-file-kind.js"
21
+ import type { LockfileFormat } from "./enums/lockfile-format.js"
22
+ import type { InvalidSpecError, LockfileError } from "./errors.js"
23
+ import { InvalidInputFileError } from "./errors.js"
24
+ import { detectLockfile } from "./lockfile/detect.js"
25
+ import { parseLockfile } from "./lockfile/parse.js"
26
+ import type { LockedTree } from "./lockfile/types.js"
27
+ import type { PackageJson } from "./schemas/package-json.js"
28
+ import { decodePackageJson } from "./schemas/package-json.js"
29
+ import type { PackageSpec } from "./spec.js"
30
+ import { parseSpecs } from "./spec.js"
22
31
 
23
- export interface InputFileResult {
32
+ export type InputFileResult = {
24
33
  readonly kind: InputFileKind
25
34
  readonly specs: ReadonlyArray<PackageSpec>
26
35
  /** Entries that were recognised but cannot be bundled from a registry. */
27
36
  readonly warnings: ReadonlyArray<string>
37
+ /**
38
+ * The pinned graph, set only for a lockfile.
39
+ *
40
+ * `specs` carries the same roots for display, but a lockfile run must resolve
41
+ * from this — see `locked-resolve` for why re-resolving the roots' ranges
42
+ * would quietly produce a different bundle.
43
+ */
44
+ readonly lockfile?: LockedTree | undefined
45
+ /**
46
+ * Names from the blocks a package.json always installs — `dependencies` and,
47
+ * unless `--prod`, `devDependencies`.
48
+ *
49
+ * Used to check a lockfile against the manifest beside it. Deliberately
50
+ * excludes optional and peer entries: both are legitimately absent from a
51
+ * lockfile (an optional dependency that failed to build, a peer the consumer
52
+ * is expected to supply), so including them would turn a useful staleness
53
+ * check into a false alarm.
54
+ */
55
+ readonly required?: ReadonlyArray<string> | undefined
28
56
  }
29
57
 
30
58
  /** Which dependency blocks of a package.json become root specs. */
31
- export interface InputFileOptions {
59
+ export type InputFileOptions = {
32
60
  /**
33
61
  * Include `devDependencies`.
34
62
  *
@@ -38,6 +66,13 @@ export interface InputFileOptions {
38
66
  * turns it off.
39
67
  */
40
68
  readonly includeDev: boolean
69
+ /**
70
+ * For a lockfile, which workspace member's dependencies are the roots.
71
+ *
72
+ * Set when a package.json found its lockfile by walking up: the file then
73
+ * covers the whole repository, and only one member of it was asked for.
74
+ */
75
+ readonly importer?: string | undefined
41
76
  }
42
77
 
43
78
  export const defaultInputFileOptions: InputFileOptions = { includeDev: true }
@@ -46,7 +81,7 @@ export const defaultInputFileOptions: InputFileOptions = { includeDev: true }
46
81
  export const readInputFile = (
47
82
  filePath: string,
48
83
  options: InputFileOptions = defaultInputFileOptions
49
- ): Effect.Effect<InputFileResult, InvalidInputFileError, FileSystem.FileSystem> =>
84
+ ): Effect.Effect<InputFileResult, InvalidInputFileError | LockfileError, FileSystem.FileSystem> =>
50
85
  Effect.gen(function*() {
51
86
  const fs = yield* FileSystem.FileSystem
52
87
 
@@ -64,12 +99,17 @@ export const readInputFile = (
64
99
  return yield* Effect.try({
65
100
  try: () => parseInputFile(filePath, content, options),
66
101
  catch: (error) =>
67
- error instanceof InvalidInputFileError
102
+ // A LockfileError already names the file and says what is wrong with
103
+ // it; wrapping it would only stutter.
104
+ error instanceof InvalidInputFileError || isLockfileError(error)
68
105
  ? error
69
- : new InvalidInputFileError(filePath, describe(error), { cause: error })
106
+ : new InvalidInputFileError(filePath, describeError(error), { cause: error })
70
107
  })
71
108
  })
72
109
 
110
+ const isLockfileError = (error: unknown): error is LockfileError =>
111
+ error instanceof Error && "_tag" in error && error._tag === "LockfileError"
112
+
73
113
  /**
74
114
  * Parses file content that has already been read.
75
115
  *
@@ -86,6 +126,17 @@ export const parseInputFile = (
86
126
  throw new InvalidInputFileError(filePath, "file is empty")
87
127
  }
88
128
 
129
+ // Checked first: a package-lock.json is also a JSON object whose top level
130
+ // looks a lot like a package.json, and reading one as the other would produce
131
+ // a bundle with none of the pinning that was asked for.
132
+ const detected = detectLockfile(trimmed)
133
+ if (detected._tag === "Unsupported") {
134
+ throw new InvalidInputFileError(filePath, `${detected.label} — ${detected.hint}`)
135
+ }
136
+ if (detected._tag === "Supported") {
137
+ return fromLockfile(filePath, content, detected.format, options)
138
+ }
139
+
89
140
  return looksLikeJson(trimmed)
90
141
  ? fromPackageJson(filePath, trimmed, options)
91
142
  : fromList(filePath, content)
@@ -94,33 +145,47 @@ export const parseInputFile = (
94
145
  const looksLikeJson = (trimmed: string): boolean => trimmed.startsWith("{")
95
146
 
96
147
  /* -------------------------------------------------------------------------- */
97
- /* package.json */
148
+ /* Lockfile */
98
149
  /* -------------------------------------------------------------------------- */
99
150
 
100
- interface PackageJsonShape {
101
- readonly dependencies?: Record<string, string>
102
- readonly devDependencies?: Record<string, string>
103
- readonly optionalDependencies?: Record<string, string>
104
- readonly peerDependencies?: Record<string, string>
105
- readonly peerDependenciesMeta?: Record<string, { optional?: boolean }>
151
+ const fromLockfile = (
152
+ filePath: string,
153
+ content: string,
154
+ format: LockfileFormat,
155
+ options: InputFileOptions
156
+ ): InputFileResult => {
157
+ const lockfile = parseLockfile(filePath, content, format, { importer: options.importer })
158
+
159
+ // The same devDependency rule as a package.json, applied to the pinned roots
160
+ // so that `specs` shows exactly what the run will bundle.
161
+ const roots = lockfile.roots.filter((root) => options.includeDev || root.kind !== "dev")
162
+
163
+ return {
164
+ kind: InputFileKind.Lockfile,
165
+ specs: roots.map((root) => ({
166
+ name: root.name,
167
+ selector: { _tag: "Exact", version: root.version },
168
+ raw: `${root.name}@${root.version}`
169
+ })),
170
+ warnings: lockfile.warnings,
171
+ lockfile
172
+ }
106
173
  }
107
174
 
175
+ /* -------------------------------------------------------------------------- */
176
+ /* package.json */
177
+ /* -------------------------------------------------------------------------- */
178
+
108
179
  const fromPackageJson = (
109
180
  filePath: string,
110
181
  content: string,
111
182
  options: InputFileOptions
112
183
  ): InputFileResult => {
113
- let parsed: PackageJsonShape
114
- try {
115
- parsed = JSON.parse(content) as PackageJsonShape
116
- } catch (error) {
117
- throw new InvalidInputFileError(filePath, `not valid JSON — ${describe(error)}`, {
118
- cause: error
119
- })
120
- }
184
+ const parsed = parsePackageJson(filePath, content)
121
185
 
122
186
  const specs: Array<PackageSpec> = []
123
187
  const warnings: Array<string> = []
188
+ const required: Array<string> = []
124
189
  const seen = new Set<string>()
125
190
 
126
191
  const collect = (
@@ -138,6 +203,9 @@ const fromPackageJson = (
138
203
  )
139
204
  continue
140
205
  }
206
+ // `target.name` rather than the key, so an `npm:` alias is recorded under
207
+ // the name a lockfile would actually pin it as.
208
+ if (label === "dependencies" || label === "devDependencies") required.push(target.name)
141
209
  const key = `${target.name}|${JSON.stringify(target.selector)}`
142
210
  if (seen.has(key)) continue
143
211
  seen.add(key)
@@ -166,7 +234,28 @@ const fromPackageJson = (
166
234
  )
167
235
  }
168
236
 
169
- return { kind: "package.json", specs, warnings }
237
+ return { kind: InputFileKind.PackageJson, specs, warnings, required }
238
+ }
239
+
240
+ /**
241
+ * Reads a package.json, or gives up on its contents rather than on the run.
242
+ *
243
+ * Invalid JSON is an error worth raising — somebody pointed `--file` at a
244
+ * broken file. A well-formed JSON document this schema cannot recognise is
245
+ * not: it decodes to no blocks, and the caller reports "declares no
246
+ * dependencies to bundle", which is both true and more useful.
247
+ */
248
+ const parsePackageJson = (filePath: string, content: string): PackageJson => {
249
+ let parsed: unknown
250
+ try {
251
+ parsed = JSON.parse(content)
252
+ } catch (error) {
253
+ throw new InvalidInputFileError(filePath, `not valid JSON — ${describeError(error)}`, {
254
+ cause: error
255
+ })
256
+ }
257
+
258
+ return Option.getOrElse(decodePackageJson(parsed), (): PackageJson => ({}))
170
259
  }
171
260
 
172
261
  /* -------------------------------------------------------------------------- */
@@ -174,14 +263,11 @@ const fromPackageJson = (
174
263
  /* -------------------------------------------------------------------------- */
175
264
 
176
265
  const fromList = (filePath: string, content: string): InputFileResult => {
177
- const lines: Array<string> = []
178
266
  const rawLines = content.split(/\r?\n/)
179
-
180
- for (const line of rawLines) {
267
+ const lines = rawLines.flatMap((line) => {
181
268
  const withoutComment = stripComment(line).trim()
182
- if (withoutComment.length === 0) continue
183
- lines.push(withoutComment)
184
- }
269
+ return withoutComment.length === 0 ? [] : withoutComment
270
+ })
185
271
 
186
272
  if (lines.length === 0) {
187
273
  throw new InvalidInputFileError(filePath, "no package specs found (only blank lines and comments)")
@@ -192,7 +278,7 @@ const fromList = (filePath: string, content: string): InputFileResult => {
192
278
  throw new InvalidInputFileError(filePath, formatSpecErrors(errors, rawLines))
193
279
  }
194
280
 
195
- return { kind: "list", specs, warnings: [] }
281
+ return { kind: InputFileKind.List, specs, warnings: [] }
196
282
  }
197
283
 
198
284
  /**
@@ -203,13 +289,10 @@ const fromList = (filePath: string, content: string): InputFileResult => {
203
289
  */
204
290
  const stripComment = (line: string): string => {
205
291
  const hash = line.indexOf("#")
206
- let result = hash === -1 ? line : line.slice(0, hash)
292
+ const result = hash === -1 ? line : line.slice(0, hash)
207
293
  const slashes = result.indexOf("//")
208
- if (slashes > 0 && result[slashes - 1] !== ":") {
209
- result = result.slice(0, slashes)
210
- } else if (slashes === 0) {
211
- result = ""
212
- }
294
+ if (slashes === 0) return ""
295
+ if (slashes > 0 && result[slashes - 1] !== ":") return result.slice(0, slashes)
213
296
  return result
214
297
  }
215
298
 
@@ -225,5 +308,5 @@ const formatSpecErrors = (
225
308
  return `${errors.length} invalid spec(s):\n${details.join("\n")}`
226
309
  }
227
310
 
228
- const describe = (error: unknown): string =>
311
+ const describeError = (error: unknown): string =>
229
312
  error instanceof Error ? error.message : String(error)