@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.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/archive-6sqTn1C4.js +274 -0
- package/dist/archive-6sqTn1C4.js.map +1 -0
- package/dist/archive.d.ts +47 -15
- package/dist/archive.d.ts.map +1 -1
- package/dist/bundle.d.ts +6 -3
- package/dist/bundle.d.ts.map +1 -1
- package/dist/dependency-range.d.ts.map +1 -1
- package/dist/download.d.ts +2 -1
- package/dist/download.d.ts.map +1 -1
- package/dist/enums/artifact-kind.d.ts.map +1 -1
- package/dist/enums/edge-kind.d.ts.map +1 -1
- package/dist/enums/input-file-kind.d.ts.map +1 -1
- package/dist/enums/layout.d.ts.map +1 -1
- package/dist/enums/lockfile-format.d.ts.map +1 -1
- package/dist/enums/phase.d.ts.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +169 -344
- package/dist/index.js.map +1 -1
- package/dist/input-file.d.ts.map +1 -1
- package/dist/integrity.d.ts +37 -5
- package/dist/integrity.d.ts.map +1 -1
- package/dist/layout.d.ts +41 -7
- package/dist/layout.d.ts.map +1 -1
- package/dist/locked-resolve.d.ts.map +1 -1
- package/dist/lockfile/bun.d.ts.map +1 -1
- package/dist/lockfile/detect.d.ts.map +1 -1
- package/dist/lockfile/json.d.ts.map +1 -1
- package/dist/lockfile/names.d.ts.map +1 -1
- package/dist/lockfile/npm.d.ts.map +1 -1
- package/dist/lockfile/parse.d.ts.map +1 -1
- package/dist/lockfile/pnpm.d.ts.map +1 -1
- package/dist/lockfile/tree-builder.d.ts.map +1 -1
- package/dist/lockfile/types.d.ts.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/node/archiver.d.ts +5 -0
- package/dist/node/archiver.d.ts.map +1 -0
- package/dist/node/index.d.ts +13 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +55 -0
- package/dist/node/index.js.map +1 -0
- package/dist/options.d.ts +3 -3
- package/dist/options.d.ts.map +1 -1
- package/dist/platform.d.ts +5 -10
- package/dist/platform.d.ts.map +1 -1
- package/dist/progress.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/schemas/lenient.d.ts.map +1 -1
- package/dist/schemas/package-json.d.ts.map +1 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/types/value-of.d.ts.map +1 -1
- package/dist/utils/is-record.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/archive.ts +95 -92
- package/src/bundle.ts +736 -616
- package/src/dependency-range.ts +121 -115
- package/src/download.ts +155 -133
- package/src/enums/artifact-kind.ts +5 -5
- package/src/enums/edge-kind.ts +13 -13
- package/src/enums/input-file-kind.ts +9 -9
- package/src/enums/layout.ts +20 -20
- package/src/enums/lockfile-format.ts +6 -6
- package/src/enums/phase.ts +8 -8
- package/src/errors.ts +200 -198
- package/src/index.ts +126 -113
- package/src/input-file.ts +246 -234
- package/src/integrity.ts +165 -121
- package/src/layout.ts +63 -29
- package/src/locked-resolve.ts +471 -461
- package/src/lockfile/bun.ts +214 -207
- package/src/lockfile/detect.ts +80 -80
- package/src/lockfile/json.ts +91 -92
- package/src/lockfile/names.ts +9 -9
- package/src/lockfile/npm.ts +328 -308
- package/src/lockfile/parse.ts +32 -32
- package/src/lockfile/pnpm.ts +230 -225
- package/src/lockfile/tree-builder.ts +116 -116
- package/src/lockfile/types.ts +52 -52
- package/src/manifest.ts +129 -133
- package/src/node/archiver.ts +85 -0
- package/src/node/index.ts +13 -0
- package/src/options.ts +84 -84
- package/src/platform.ts +97 -101
- package/src/progress.ts +105 -106
- package/src/registry.ts +116 -115
- package/src/resolve.ts +560 -537
- package/src/schemas/lenient.ts +67 -60
- package/src/schemas/package-json.ts +11 -10
- package/src/spec.ts +123 -119
- package/src/types/value-of.ts +1 -1
- 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
|
-
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
import type {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
27
|
-
import type {
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
30
|
-
import {
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
83
|
-
|
|
83
|
+
filePath: string,
|
|
84
|
+
options: InputFileOptions = defaultInputFileOptions,
|
|
84
85
|
): Effect.Effect<InputFileResult, InvalidInputFileError | LockfileError, FileSystem.FileSystem> =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
filePath: string,
|
|
130
|
+
content: string,
|
|
131
|
+
options: InputFileOptions = defaultInputFileOptions,
|
|
123
132
|
): InputFileResult => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
filePath: string,
|
|
162
|
+
content: string,
|
|
163
|
+
format: LockfileFormat,
|
|
164
|
+
options: InputFileOptions,
|
|
156
165
|
): InputFileResult => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
filePath: string,
|
|
190
|
+
content: string,
|
|
191
|
+
options: InputFileOptions,
|
|
183
192
|
): InputFileResult => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
301
|
-
|
|
312
|
+
errors: ReadonlyArray<InvalidSpecError>,
|
|
313
|
+
rawLines: ReadonlyArray<string>,
|
|
302
314
|
): string => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
324
|
+
error instanceof Error ? error.message : String(error);
|