@packall/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/archive-6sqTn1C4.js +274 -0
  4. package/dist/archive-6sqTn1C4.js.map +1 -0
  5. package/dist/archive.d.ts +47 -15
  6. package/dist/archive.d.ts.map +1 -1
  7. package/dist/bundle.d.ts +6 -3
  8. package/dist/bundle.d.ts.map +1 -1
  9. package/dist/dependency-range.d.ts.map +1 -1
  10. package/dist/download.d.ts +2 -1
  11. package/dist/download.d.ts.map +1 -1
  12. package/dist/enums/artifact-kind.d.ts.map +1 -1
  13. package/dist/enums/edge-kind.d.ts.map +1 -1
  14. package/dist/enums/input-file-kind.d.ts.map +1 -1
  15. package/dist/enums/layout.d.ts.map +1 -1
  16. package/dist/enums/lockfile-format.d.ts.map +1 -1
  17. package/dist/enums/phase.d.ts.map +1 -1
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/index.d.ts +12 -10
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +169 -344
  22. package/dist/index.js.map +1 -1
  23. package/dist/input-file.d.ts.map +1 -1
  24. package/dist/integrity.d.ts +37 -5
  25. package/dist/integrity.d.ts.map +1 -1
  26. package/dist/layout.d.ts +41 -7
  27. package/dist/layout.d.ts.map +1 -1
  28. package/dist/locked-resolve.d.ts.map +1 -1
  29. package/dist/lockfile/bun.d.ts.map +1 -1
  30. package/dist/lockfile/detect.d.ts.map +1 -1
  31. package/dist/lockfile/json.d.ts.map +1 -1
  32. package/dist/lockfile/names.d.ts.map +1 -1
  33. package/dist/lockfile/npm.d.ts.map +1 -1
  34. package/dist/lockfile/parse.d.ts.map +1 -1
  35. package/dist/lockfile/pnpm.d.ts.map +1 -1
  36. package/dist/lockfile/tree-builder.d.ts.map +1 -1
  37. package/dist/lockfile/types.d.ts.map +1 -1
  38. package/dist/manifest.d.ts.map +1 -1
  39. package/dist/node/archiver.d.ts +5 -0
  40. package/dist/node/archiver.d.ts.map +1 -0
  41. package/dist/node/index.d.ts +13 -0
  42. package/dist/node/index.d.ts.map +1 -0
  43. package/dist/node/index.js +55 -0
  44. package/dist/node/index.js.map +1 -0
  45. package/dist/options.d.ts +3 -3
  46. package/dist/options.d.ts.map +1 -1
  47. package/dist/platform.d.ts +5 -10
  48. package/dist/platform.d.ts.map +1 -1
  49. package/dist/progress.d.ts.map +1 -1
  50. package/dist/registry.d.ts.map +1 -1
  51. package/dist/resolve.d.ts.map +1 -1
  52. package/dist/schemas/lenient.d.ts.map +1 -1
  53. package/dist/schemas/package-json.d.ts.map +1 -1
  54. package/dist/spec.d.ts.map +1 -1
  55. package/dist/types/value-of.d.ts.map +1 -1
  56. package/dist/utils/is-record.d.ts.map +1 -1
  57. package/package.json +11 -10
  58. package/src/archive.ts +95 -92
  59. package/src/bundle.ts +736 -616
  60. package/src/dependency-range.ts +121 -115
  61. package/src/download.ts +155 -133
  62. package/src/enums/artifact-kind.ts +5 -5
  63. package/src/enums/edge-kind.ts +13 -13
  64. package/src/enums/input-file-kind.ts +9 -9
  65. package/src/enums/layout.ts +20 -20
  66. package/src/enums/lockfile-format.ts +6 -6
  67. package/src/enums/phase.ts +8 -8
  68. package/src/errors.ts +200 -198
  69. package/src/index.ts +126 -113
  70. package/src/input-file.ts +246 -234
  71. package/src/integrity.ts +165 -121
  72. package/src/layout.ts +63 -29
  73. package/src/locked-resolve.ts +471 -461
  74. package/src/lockfile/bun.ts +214 -207
  75. package/src/lockfile/detect.ts +80 -80
  76. package/src/lockfile/json.ts +91 -92
  77. package/src/lockfile/names.ts +9 -9
  78. package/src/lockfile/npm.ts +328 -308
  79. package/src/lockfile/parse.ts +32 -32
  80. package/src/lockfile/pnpm.ts +230 -225
  81. package/src/lockfile/tree-builder.ts +116 -116
  82. package/src/lockfile/types.ts +52 -52
  83. package/src/manifest.ts +129 -133
  84. package/src/node/archiver.ts +85 -0
  85. package/src/node/index.ts +13 -0
  86. package/src/options.ts +84 -84
  87. package/src/platform.ts +97 -101
  88. package/src/progress.ts +105 -106
  89. package/src/registry.ts +116 -115
  90. package/src/resolve.ts +560 -537
  91. package/src/schemas/lenient.ts +67 -60
  92. package/src/schemas/package-json.ts +11 -10
  93. package/src/spec.ts +123 -119
  94. package/src/types/value-of.ts +1 -1
  95. package/src/utils/is-record.ts +1 -1
package/src/input-file.ts CHANGED
@@ -13,102 +13,111 @@
13
13
  * A lockfile is checked for first, because `package-lock.json` and
14
14
  * `package.json` are both JSON objects and only the content tells them apart.
15
15
  */
16
- import * as Effect from "effect/Effect"
17
- import * as FileSystem from "effect/FileSystem"
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"
16
+ import * as Effect from "effect/Effect";
17
+ import * as FileSystem from "effect/FileSystem";
18
+ import * as Option from "effect/Option";
19
+
20
+ import { parseDependencyTarget } from "./dependency-range.js";
21
+ import { InputFileKind } from "./enums/input-file-kind.js";
22
+ import type { LockfileFormat } from "./enums/lockfile-format.js";
23
+ import type { InvalidSpecError, LockfileError } from "./errors.js";
24
+ import { InvalidInputFileError } from "./errors.js";
25
+ import { detectLockfile } from "./lockfile/detect.js";
26
+ import { parseLockfile } from "./lockfile/parse.js";
27
+ import type { LockedTree } from "./lockfile/types.js";
28
+ import type { PackageJson } from "./schemas/package-json.js";
29
+ import { decodePackageJson } from "./schemas/package-json.js";
30
+ import type { PackageSpec } from "./spec.js";
31
+ import { parseSpecs } from "./spec.js";
31
32
 
32
33
  export type InputFileResult = {
33
- readonly kind: InputFileKind
34
- readonly specs: ReadonlyArray<PackageSpec>
35
- /** Entries that were recognised but cannot be bundled from a registry. */
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
56
- }
34
+ readonly kind: InputFileKind;
35
+ readonly specs: ReadonlyArray<PackageSpec>;
36
+ /** Entries that were recognised but cannot be bundled from a registry. */
37
+ readonly warnings: ReadonlyArray<string>;
38
+ /**
39
+ * The pinned graph, set only for a lockfile.
40
+ *
41
+ * `specs` carries the same roots for display, but a lockfile run must resolve
42
+ * from this — see `locked-resolve` for why re-resolving the roots' ranges
43
+ * would quietly produce a different bundle.
44
+ */
45
+ readonly lockfile?: LockedTree | undefined;
46
+ /**
47
+ * Names from the blocks a package.json always installs — `dependencies` and,
48
+ * unless `--prod`, `devDependencies`.
49
+ *
50
+ * Used to check a lockfile against the manifest beside it. Deliberately
51
+ * excludes optional and peer entries: both are legitimately absent from a
52
+ * lockfile (an optional dependency that failed to build, a peer the consumer
53
+ * is expected to supply), so including them would turn a useful staleness
54
+ * check into a false alarm.
55
+ */
56
+ readonly required?: ReadonlyArray<string> | undefined;
57
+ };
57
58
 
58
59
  /** Which dependency blocks of a package.json become root specs. */
59
60
  export type InputFileOptions = {
60
- /**
61
- * Include `devDependencies`.
62
- *
63
- * On by default: if you are pointing this tool at a package.json, you are
64
- * almost certainly trying to make that project installable behind the
65
- * firewall, and a project without its dev tooling does not build. `--prod`
66
- * turns it off.
67
- */
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
76
- }
77
-
78
- export const defaultInputFileOptions: InputFileOptions = { includeDev: true }
61
+ /**
62
+ * Include `devDependencies`.
63
+ *
64
+ * On by default: if you are pointing this tool at a package.json, you are
65
+ * almost certainly trying to make that project installable behind the
66
+ * firewall, and a project without its dev tooling does not build. `--prod`
67
+ * turns it off.
68
+ */
69
+ readonly includeDev: boolean;
70
+ /**
71
+ * For a lockfile, which workspace member's dependencies are the roots.
72
+ *
73
+ * Set when a package.json found its lockfile by walking up: the file then
74
+ * covers the whole repository, and only one member of it was asked for.
75
+ */
76
+ readonly importer?: string | undefined;
77
+ };
78
+
79
+ export const defaultInputFileOptions: InputFileOptions = { includeDev: true };
79
80
 
80
81
  /** Reads and parses an input file. */
81
82
  export const readInputFile = (
82
- filePath: string,
83
- options: InputFileOptions = defaultInputFileOptions
83
+ filePath: string,
84
+ options: InputFileOptions = defaultInputFileOptions,
84
85
  ): Effect.Effect<InputFileResult, InvalidInputFileError | LockfileError, FileSystem.FileSystem> =>
85
- Effect.gen(function*() {
86
- const fs = yield* FileSystem.FileSystem
87
-
88
- const exists = yield* fs.exists(filePath).pipe(
89
- Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause }))
90
- )
91
- if (!exists) {
92
- return yield* Effect.fail(new InvalidInputFileError(filePath, "file does not exist"))
93
- }
94
-
95
- const content = yield* fs.readFileString(filePath).pipe(
96
- Effect.mapError((cause) => new InvalidInputFileError(filePath, "could not be read", { cause }))
97
- )
98
-
99
- return yield* Effect.try({
100
- try: () => parseInputFile(filePath, content, options),
101
- catch: (error) =>
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)
105
- ? error
106
- : new InvalidInputFileError(filePath, describeError(error), { cause: error })
107
- })
108
- })
86
+ Effect.gen(function* () {
87
+ const fs = yield* FileSystem.FileSystem;
88
+
89
+ const exists = yield* fs
90
+ .exists(filePath)
91
+ .pipe(
92
+ Effect.mapError(
93
+ (cause) => new InvalidInputFileError(filePath, "could not be read", { cause }),
94
+ ),
95
+ );
96
+ if (!exists) {
97
+ return yield* Effect.fail(new InvalidInputFileError(filePath, "file does not exist"));
98
+ }
99
+
100
+ const content = yield* fs
101
+ .readFileString(filePath)
102
+ .pipe(
103
+ Effect.mapError(
104
+ (cause) => new InvalidInputFileError(filePath, "could not be read", { cause }),
105
+ ),
106
+ );
107
+
108
+ return yield* Effect.try({
109
+ try: () => parseInputFile(filePath, content, options),
110
+ catch: (error) =>
111
+ // A LockfileError already names the file and says what is wrong with
112
+ // it; wrapping it would only stutter.
113
+ error instanceof InvalidInputFileError || isLockfileError(error)
114
+ ? error
115
+ : new InvalidInputFileError(filePath, describeError(error), { cause: error }),
116
+ });
117
+ });
109
118
 
110
119
  const isLockfileError = (error: unknown): error is LockfileError =>
111
- error instanceof Error && "_tag" in error && error._tag === "LockfileError"
120
+ error instanceof Error && "_tag" in error && error._tag === "LockfileError";
112
121
 
113
122
  /**
114
123
  * Parses file content that has already been read.
@@ -117,125 +126,125 @@ const isLockfileError = (error: unknown): error is LockfileError =>
117
126
  * strings and no file system at all.
118
127
  */
119
128
  export const parseInputFile = (
120
- filePath: string,
121
- content: string,
122
- options: InputFileOptions = defaultInputFileOptions
129
+ filePath: string,
130
+ content: string,
131
+ options: InputFileOptions = defaultInputFileOptions,
123
132
  ): InputFileResult => {
124
- const trimmed = content.trim()
125
- if (trimmed.length === 0) {
126
- throw new InvalidInputFileError(filePath, "file is empty")
127
- }
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
-
140
- return looksLikeJson(trimmed)
141
- ? fromPackageJson(filePath, trimmed, options)
142
- : fromList(filePath, content)
143
- }
144
-
145
- const looksLikeJson = (trimmed: string): boolean => trimmed.startsWith("{")
133
+ const trimmed = content.trim();
134
+ if (trimmed.length === 0) {
135
+ throw new InvalidInputFileError(filePath, "file is empty");
136
+ }
137
+
138
+ // Checked first: a package-lock.json is also a JSON object whose top level
139
+ // looks a lot like a package.json, and reading one as the other would produce
140
+ // a bundle with none of the pinning that was asked for.
141
+ const detected = detectLockfile(trimmed);
142
+ if (detected._tag === "Unsupported") {
143
+ throw new InvalidInputFileError(filePath, `${detected.label} — ${detected.hint}`);
144
+ }
145
+ if (detected._tag === "Supported") {
146
+ return fromLockfile(filePath, content, detected.format, options);
147
+ }
148
+
149
+ return looksLikeJson(trimmed)
150
+ ? fromPackageJson(filePath, trimmed, options)
151
+ : fromList(filePath, content);
152
+ };
153
+
154
+ const looksLikeJson = (trimmed: string): boolean => trimmed.startsWith("{");
146
155
 
147
156
  /* -------------------------------------------------------------------------- */
148
157
  /* Lockfile */
149
158
  /* -------------------------------------------------------------------------- */
150
159
 
151
160
  const fromLockfile = (
152
- filePath: string,
153
- content: string,
154
- format: LockfileFormat,
155
- options: InputFileOptions
161
+ filePath: string,
162
+ content: string,
163
+ format: LockfileFormat,
164
+ options: InputFileOptions,
156
165
  ): 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
- }
173
- }
166
+ const lockfile = parseLockfile(filePath, content, format, { importer: options.importer });
167
+
168
+ // The same devDependency rule as a package.json, applied to the pinned roots
169
+ // so that `specs` shows exactly what the run will bundle.
170
+ const roots = lockfile.roots.filter((root) => options.includeDev || root.kind !== "dev");
171
+
172
+ return {
173
+ kind: InputFileKind.Lockfile,
174
+ specs: roots.map((root) => ({
175
+ name: root.name,
176
+ selector: { _tag: "Exact", version: root.version },
177
+ raw: `${root.name}@${root.version}`,
178
+ })),
179
+ warnings: lockfile.warnings,
180
+ lockfile,
181
+ };
182
+ };
174
183
 
175
184
  /* -------------------------------------------------------------------------- */
176
185
  /* package.json */
177
186
  /* -------------------------------------------------------------------------- */
178
187
 
179
188
  const fromPackageJson = (
180
- filePath: string,
181
- content: string,
182
- options: InputFileOptions
189
+ filePath: string,
190
+ content: string,
191
+ options: InputFileOptions,
183
192
  ): InputFileResult => {
184
- const parsed = parsePackageJson(filePath, content)
185
-
186
- const specs: Array<PackageSpec> = []
187
- const warnings: Array<string> = []
188
- const required: Array<string> = []
189
- const seen = new Set<string>()
190
-
191
- const collect = (
192
- entries: Record<string, string> | undefined,
193
- label: string,
194
- skip?: (name: string) => boolean
195
- ): void => {
196
- if (entries === undefined) return
197
- for (const [name, raw] of Object.entries(entries)) {
198
- if (skip?.(name)) continue
199
- const target = parseDependencyTarget(name, raw)
200
- if (target._tag === "Unsupported") {
201
- warnings.push(
202
- `${label}: skipped ${name}@${target.raw} (${target.reason}) — not fetchable from a registry`
203
- )
204
- continue
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)
209
- const key = `${target.name}|${JSON.stringify(target.selector)}`
210
- if (seen.has(key)) continue
211
- seen.add(key)
212
- specs.push({
213
- name: target.name,
214
- selector: target.selector,
215
- raw: `${target.name}@${raw}`
216
- })
217
- }
218
- }
219
-
220
- collect(parsed.dependencies, "dependencies")
221
- collect(parsed.optionalDependencies, "optionalDependencies")
222
- if (options.includeDev) {
223
- collect(parsed.devDependencies, "devDependencies")
224
- }
225
- const meta = parsed.peerDependenciesMeta ?? {}
226
- collect(parsed.peerDependencies, "peerDependencies", (name) => meta[name]?.optional === true)
227
-
228
- if (specs.length === 0) {
229
- throw new InvalidInputFileError(
230
- filePath,
231
- options.includeDev
232
- ? "package.json declares no dependencies to bundle"
233
- : "package.json declares no non-dev dependencies to bundle (drop --prod to include devDependencies)"
234
- )
235
- }
236
-
237
- return { kind: InputFileKind.PackageJson, specs, warnings, required }
238
- }
193
+ const parsed = parsePackageJson(filePath, content);
194
+
195
+ const specs: Array<PackageSpec> = [];
196
+ const warnings: Array<string> = [];
197
+ const required: Array<string> = [];
198
+ const seen = new Set<string>();
199
+
200
+ const collect = (
201
+ entries: Record<string, string> | undefined,
202
+ label: string,
203
+ skip?: (name: string) => boolean,
204
+ ): void => {
205
+ if (entries === undefined) return;
206
+ for (const [name, raw] of Object.entries(entries)) {
207
+ if (skip?.(name)) continue;
208
+ const target = parseDependencyTarget(name, raw);
209
+ if (target._tag === "Unsupported") {
210
+ warnings.push(
211
+ `${label}: skipped ${name}@${target.raw} (${target.reason}) — not fetchable from a registry`,
212
+ );
213
+ continue;
214
+ }
215
+ // `target.name` rather than the key, so an `npm:` alias is recorded under
216
+ // the name a lockfile would actually pin it as.
217
+ if (label === "dependencies" || label === "devDependencies") required.push(target.name);
218
+ const key = `${target.name}|${JSON.stringify(target.selector)}`;
219
+ if (seen.has(key)) continue;
220
+ seen.add(key);
221
+ specs.push({
222
+ name: target.name,
223
+ selector: target.selector,
224
+ raw: `${target.name}@${raw}`,
225
+ });
226
+ }
227
+ };
228
+
229
+ collect(parsed.dependencies, "dependencies");
230
+ collect(parsed.optionalDependencies, "optionalDependencies");
231
+ if (options.includeDev) {
232
+ collect(parsed.devDependencies, "devDependencies");
233
+ }
234
+ const meta = parsed.peerDependenciesMeta ?? {};
235
+ collect(parsed.peerDependencies, "peerDependencies", (name) => meta[name]?.optional === true);
236
+
237
+ if (specs.length === 0) {
238
+ throw new InvalidInputFileError(
239
+ filePath,
240
+ options.includeDev
241
+ ? "package.json declares no dependencies to bundle"
242
+ : "package.json declares no non-dev dependencies to bundle (drop --prod to include devDependencies)",
243
+ );
244
+ }
245
+
246
+ return { kind: InputFileKind.PackageJson, specs, warnings, required };
247
+ };
239
248
 
240
249
  /**
241
250
  * Reads a package.json, or gives up on its contents rather than on the run.
@@ -246,40 +255,43 @@ const fromPackageJson = (
246
255
  * dependencies to bundle", which is both true and more useful.
247
256
  */
248
257
  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 => ({}))
259
- }
258
+ let parsed: unknown;
259
+ try {
260
+ parsed = JSON.parse(content);
261
+ } catch (error) {
262
+ throw new InvalidInputFileError(filePath, `not valid JSON — ${describeError(error)}`, {
263
+ cause: error,
264
+ });
265
+ }
266
+
267
+ return Option.getOrElse(decodePackageJson(parsed), (): PackageJson => ({}));
268
+ };
260
269
 
261
270
  /* -------------------------------------------------------------------------- */
262
271
  /* Spec list */
263
272
  /* -------------------------------------------------------------------------- */
264
273
 
265
274
  const fromList = (filePath: string, content: string): InputFileResult => {
266
- const rawLines = content.split(/\r?\n/)
267
- const lines = rawLines.flatMap((line) => {
268
- const withoutComment = stripComment(line).trim()
269
- return withoutComment.length === 0 ? [] : withoutComment
270
- })
271
-
272
- if (lines.length === 0) {
273
- throw new InvalidInputFileError(filePath, "no package specs found (only blank lines and comments)")
274
- }
275
-
276
- const { specs, errors } = parseSpecs(lines)
277
- if (errors.length > 0) {
278
- throw new InvalidInputFileError(filePath, formatSpecErrors(errors, rawLines))
279
- }
280
-
281
- return { kind: InputFileKind.List, specs, warnings: [] }
282
- }
275
+ const rawLines = content.split(/\r?\n/);
276
+ const lines = rawLines.flatMap((line) => {
277
+ const withoutComment = stripComment(line).trim();
278
+ return withoutComment.length === 0 ? [] : withoutComment;
279
+ });
280
+
281
+ if (lines.length === 0) {
282
+ throw new InvalidInputFileError(
283
+ filePath,
284
+ "no package specs found (only blank lines and comments)",
285
+ );
286
+ }
287
+
288
+ const { specs, errors } = parseSpecs(lines);
289
+ if (errors.length > 0) {
290
+ throw new InvalidInputFileError(filePath, formatSpecErrors(errors, rawLines));
291
+ }
292
+
293
+ return { kind: InputFileKind.List, specs, warnings: [] };
294
+ };
283
295
 
284
296
  /**
285
297
  * Strips `#` and `//` comments.
@@ -288,25 +300,25 @@ const fromList = (filePath: string, content: string): InputFileResult => {
288
300
  * `https://…` does not get silently truncated.
289
301
  */
290
302
  const stripComment = (line: string): string => {
291
- const hash = line.indexOf("#")
292
- const result = hash === -1 ? line : line.slice(0, hash)
293
- const slashes = result.indexOf("//")
294
- if (slashes === 0) return ""
295
- if (slashes > 0 && result[slashes - 1] !== ":") return result.slice(0, slashes)
296
- return result
297
- }
303
+ const hash = line.indexOf("#");
304
+ const result = hash === -1 ? line : line.slice(0, hash);
305
+ const slashes = result.indexOf("//");
306
+ if (slashes === 0) return "";
307
+ if (slashes > 0 && result[slashes - 1] !== ":") return result.slice(0, slashes);
308
+ return result;
309
+ };
298
310
 
299
311
  const formatSpecErrors = (
300
- errors: ReadonlyArray<InvalidSpecError>,
301
- rawLines: ReadonlyArray<string>
312
+ errors: ReadonlyArray<InvalidSpecError>,
313
+ rawLines: ReadonlyArray<string>,
302
314
  ): string => {
303
- const details = errors.map((error) => {
304
- const index = rawLines.findIndex((line) => stripComment(line).trim() === error.spec)
305
- const where = index === -1 ? "" : ` (line ${index + 1})`
306
- return ` ${error.spec}${where}: ${error.reason}`
307
- })
308
- return `${errors.length} invalid spec(s):\n${details.join("\n")}`
309
- }
315
+ const details = errors.map((error) => {
316
+ const index = rawLines.findIndex((line) => stripComment(line).trim() === error.spec);
317
+ const where = index === -1 ? "" : ` (line ${index + 1})`;
318
+ return ` ${error.spec}${where}: ${error.reason}`;
319
+ });
320
+ return `${errors.length} invalid spec(s):\n${details.join("\n")}`;
321
+ };
310
322
 
311
323
  const describeError = (error: unknown): string =>
312
- error instanceof Error ? error.message : String(error)
324
+ error instanceof Error ? error.message : String(error);