@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/schemas/lenient.ts
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* fails the whole record over one numeric value, where the wire format wants
|
|
14
14
|
* that entry skipped and its neighbours kept.
|
|
15
15
|
*/
|
|
16
|
-
import * as Effect from "effect/Effect"
|
|
17
|
-
import * as Option from "effect/Option"
|
|
18
|
-
import * as Schema from "effect/Schema"
|
|
19
|
-
import * as Getter from "effect/SchemaGetter"
|
|
20
|
-
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import * as Option from "effect/Option";
|
|
18
|
+
import * as Schema from "effect/Schema";
|
|
19
|
+
import * as Getter from "effect/SchemaGetter";
|
|
20
|
+
|
|
21
|
+
import { isRecord } from "../utils/is-record.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* An optional field that goes *absent* rather than failing when it is
|
|
@@ -28,11 +29,11 @@ import { isRecord } from "../utils/is-record.js"
|
|
|
28
29
|
* costs the platform filter one signal; it should not cost the run the package.
|
|
29
30
|
*/
|
|
30
31
|
export const tolerant = <S extends Schema.Top>(schema: S) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
Schema.optional(
|
|
33
|
+
Schema.UndefinedOr(schema).pipe(
|
|
34
|
+
Schema.catchDecoding(() => Effect.succeed(Option.some(undefined))),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Keeps only the string-valued members of an untyped object.
|
|
@@ -42,26 +43,27 @@ export const tolerant = <S extends Schema.Top>(schema: S) =>
|
|
|
42
43
|
* a dependency block is no reason to forget the others.
|
|
43
44
|
*/
|
|
44
45
|
export const StringRecordSchema = Schema.Record(Schema.String, Schema.Unknown).pipe(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
46
|
+
Schema.decodeTo(Schema.Record(Schema.String, Schema.String), {
|
|
47
|
+
decode: Getter.transform((raw: Record<string, unknown>) => {
|
|
48
|
+
const out: Record<string, string> = {};
|
|
49
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
50
|
+
if (typeof value === "string") out[key] = value;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}),
|
|
54
|
+
encode: Getter.passthroughSubtype(),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
56
57
|
|
|
57
58
|
/** The same per-entry tolerance for a list: `["linux", 7]` decodes to `["linux"]`. */
|
|
58
59
|
export const StringArraySchema = Schema.Array(Schema.Unknown).pipe(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
60
|
+
Schema.decodeTo(Schema.Array(Schema.String), {
|
|
61
|
+
decode: Getter.transform((raw: ReadonlyArray<unknown>) =>
|
|
62
|
+
raw.flatMap((entry) => (typeof entry === "string" ? entry : [])),
|
|
63
|
+
),
|
|
64
|
+
encode: Getter.passthroughSubtype(),
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* `peerDependenciesMeta`, normalised.
|
|
@@ -71,17 +73,17 @@ export const StringArraySchema = Schema.Array(Schema.Unknown).pipe(
|
|
|
71
73
|
* value means "not optional".
|
|
72
74
|
*/
|
|
73
75
|
export const OptionalFlagRecordSchema = Schema.Record(Schema.String, Schema.Unknown).pipe(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
76
|
+
Schema.decodeTo(Schema.Record(Schema.String, Schema.Struct({ optional: Schema.Boolean })), {
|
|
77
|
+
decode: Getter.transform((raw: Record<string, unknown>) => {
|
|
78
|
+
const out: Record<string, { optional: boolean }> = {};
|
|
79
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
80
|
+
if (isRecord(value)) out[key] = { optional: value["optional"] === true };
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}),
|
|
84
|
+
encode: Getter.passthroughSubtype(),
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
85
87
|
|
|
86
88
|
/*
|
|
87
89
|
* The `Optional…` variants below decode an empty result to `undefined`.
|
|
@@ -93,31 +95,36 @@ export const OptionalFlagRecordSchema = Schema.Record(Schema.String, Schema.Unkn
|
|
|
93
95
|
*/
|
|
94
96
|
|
|
95
97
|
const absentWhenEmpty = <T extends Record<string, unknown>>(value: T): T | undefined =>
|
|
96
|
-
|
|
98
|
+
Object.keys(value).length > 0 ? value : undefined;
|
|
97
99
|
|
|
98
100
|
export const OptionalStringRecordSchema = StringRecordSchema.pipe(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
101
|
+
Schema.decodeTo(Schema.UndefinedOr(Schema.Record(Schema.String, Schema.String)), {
|
|
102
|
+
decode: Getter.transform(absentWhenEmpty<Record<string, string>>),
|
|
103
|
+
encode: Getter.transform((value: Record<string, string> | undefined) => value ?? {}),
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
104
106
|
|
|
105
107
|
export const OptionalStringArraySchema = StringArraySchema.pipe(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
108
|
+
Schema.decodeTo(Schema.UndefinedOr(Schema.Array(Schema.String)), {
|
|
109
|
+
decode: Getter.transform((value: ReadonlyArray<string>) =>
|
|
110
|
+
value.length > 0 ? value : undefined,
|
|
111
|
+
),
|
|
112
|
+
encode: Getter.transform((value: ReadonlyArray<string> | undefined) => value ?? []),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
112
115
|
|
|
113
116
|
export const OptionalFlagRecordOrAbsentSchema = OptionalFlagRecordSchema.pipe(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
117
|
+
Schema.decodeTo(
|
|
118
|
+
Schema.UndefinedOr(
|
|
119
|
+
Schema.Record(Schema.String, Schema.Struct({ optional: Schema.Boolean })),
|
|
120
|
+
),
|
|
121
|
+
{
|
|
122
|
+
decode: Getter.transform(
|
|
123
|
+
absentWhenEmpty<Record<string, { readonly optional: boolean }>>,
|
|
124
|
+
),
|
|
125
|
+
encode: Getter.transform(
|
|
126
|
+
(value: Record<string, { readonly optional: boolean }> | undefined) => value ?? {},
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
);
|
|
@@ -11,17 +11,18 @@
|
|
|
11
11
|
* is what `tolerant` buys: a `dependencies: "all of them"` loses one block, not
|
|
12
12
|
* the file.
|
|
13
13
|
*/
|
|
14
|
-
import * as Schema from "effect/Schema"
|
|
15
|
-
|
|
14
|
+
import * as Schema from "effect/Schema";
|
|
15
|
+
|
|
16
|
+
import { OptionalFlagRecordSchema, StringRecordSchema, tolerant } from "./lenient.js";
|
|
16
17
|
|
|
17
18
|
export const PackageJsonSchema = Schema.Struct({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
19
|
+
dependencies: tolerant(StringRecordSchema),
|
|
20
|
+
devDependencies: tolerant(StringRecordSchema),
|
|
21
|
+
optionalDependencies: tolerant(StringRecordSchema),
|
|
22
|
+
peerDependencies: tolerant(StringRecordSchema),
|
|
23
|
+
peerDependenciesMeta: tolerant(OptionalFlagRecordSchema),
|
|
24
|
+
});
|
|
24
25
|
|
|
25
|
-
export type PackageJson = typeof PackageJsonSchema.Type
|
|
26
|
+
export type PackageJson = typeof PackageJsonSchema.Type;
|
|
26
27
|
|
|
27
|
-
export const decodePackageJson = Schema.decodeUnknownOption(PackageJsonSchema)
|
|
28
|
+
export const decodePackageJson = Schema.decodeUnknownOption(PackageJsonSchema);
|
package/src/spec.ts
CHANGED
|
@@ -5,82 +5,83 @@
|
|
|
5
5
|
* Deliberately dependency-light and synchronous: parsing must never touch the
|
|
6
6
|
* network, so `--dry-run` and argument validation stay instant.
|
|
7
7
|
*/
|
|
8
|
-
import semver from "semver"
|
|
9
|
-
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
|
|
10
|
+
import { InvalidSpecError } from "./errors.js";
|
|
10
11
|
|
|
11
12
|
/** How a spec narrows down which version(s) of a package we want. */
|
|
12
13
|
export type Selector =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/** An exact, fully-qualified version: `react@18.2.0`. */
|
|
15
|
+
| { readonly _tag: "Exact"; readonly version: string }
|
|
16
|
+
/** A semver range: `react@^18`, `react@>=17 <19`, `react@*`. */
|
|
17
|
+
| { readonly _tag: "Range"; readonly range: string }
|
|
18
|
+
/** A dist-tag: `react@latest`, `typescript@next`. */
|
|
19
|
+
| { readonly _tag: "Tag"; readonly tag: string };
|
|
19
20
|
|
|
20
21
|
/** A parsed, validated package spec. */
|
|
21
22
|
export type PackageSpec = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
23
|
+
/** The npm package name, including scope. */
|
|
24
|
+
readonly name: string;
|
|
25
|
+
/** What the user asked for after the `@`. */
|
|
26
|
+
readonly selector: Selector;
|
|
27
|
+
/** The original string, kept verbatim for error messages and the manifest. */
|
|
28
|
+
readonly raw: string;
|
|
29
|
+
};
|
|
29
30
|
|
|
30
|
-
const MAX_NAME_LENGTH = 214
|
|
31
|
+
const MAX_NAME_LENGTH = 214;
|
|
31
32
|
|
|
32
33
|
// The registry accepts URL-safe characters plus `-`, `.` and `_`.
|
|
33
|
-
const SEGMENT_RE = /^[a-z0-9\-._~]
|
|
34
|
+
const SEGMENT_RE = /^[a-z0-9\-._~]+$/;
|
|
34
35
|
|
|
35
|
-
const DIST_TAG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]
|
|
36
|
+
const DIST_TAG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
36
37
|
|
|
37
38
|
/** Renders a spec back into the canonical `name@selector` form. */
|
|
38
39
|
export const formatSpec = (spec: PackageSpec): string =>
|
|
39
|
-
|
|
40
|
+
`${spec.name}@${formatSelector(spec.selector)}`;
|
|
40
41
|
|
|
41
42
|
/** Human-readable form of just the selector part. */
|
|
42
43
|
export const formatSelector = (selector: Selector): string => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
44
|
+
switch (selector._tag) {
|
|
45
|
+
case "Exact":
|
|
46
|
+
return selector.version;
|
|
47
|
+
case "Range":
|
|
48
|
+
return selector.range;
|
|
49
|
+
case "Tag":
|
|
50
|
+
return selector.tag;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Validates an npm package name against the rules the registry actually
|
|
55
56
|
* enforces. Returns `null` when valid, or the reason it is not.
|
|
56
57
|
*/
|
|
57
58
|
export const validatePackageName = (name: string): string | null => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
59
|
+
if (name.length === 0) return "name is empty";
|
|
60
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
61
|
+
return `name is longer than ${MAX_NAME_LENGTH} characters`;
|
|
62
|
+
}
|
|
63
|
+
if (name.trim() !== name) return "name has leading or trailing whitespace";
|
|
64
|
+
if (name.startsWith(".")) return "name cannot start with a period";
|
|
65
|
+
if (name.startsWith("_")) return "name cannot start with an underscore";
|
|
66
|
+
if (name !== name.toLowerCase()) return "name cannot contain capital letters";
|
|
67
|
+
|
|
68
|
+
if (name.startsWith("@")) {
|
|
69
|
+
const slash = name.indexOf("/");
|
|
70
|
+
if (slash === -1) return "scoped name is missing the '/name' part";
|
|
71
|
+
const scope = name.slice(1, slash);
|
|
72
|
+
const rest = name.slice(slash + 1);
|
|
73
|
+
if (scope.length === 0) return "scope is empty";
|
|
74
|
+
if (rest.length === 0) return "name after the scope is empty";
|
|
75
|
+
if (rest.includes("/")) return "name contains more than one '/'";
|
|
76
|
+
if (!SEGMENT_RE.test(scope)) return `scope "${scope}" contains illegal characters`;
|
|
77
|
+
if (!SEGMENT_RE.test(rest)) return `name "${rest}" contains illegal characters`;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (name.includes("/")) return "unscoped name cannot contain '/'";
|
|
82
|
+
if (!SEGMENT_RE.test(name)) return "name contains illegal characters";
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
87
|
* Parses a single spec string.
|
|
@@ -89,51 +90,51 @@ export const validatePackageName = (name: string): string | null => {
|
|
|
89
90
|
* look for the last `@` that is not at index 0.
|
|
90
91
|
*/
|
|
91
92
|
export const parseSpec = (raw: string): PackageSpec => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const input = raw.trim();
|
|
94
|
+
if (input.length === 0) {
|
|
95
|
+
throw new InvalidSpecError(raw, "spec is empty");
|
|
96
|
+
}
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
const at = input.lastIndexOf("@");
|
|
99
|
+
const hasSelector = at > 0;
|
|
100
|
+
const name = hasSelector ? input.slice(0, at) : input;
|
|
101
|
+
const selectorText = hasSelector ? input.slice(at + 1) : "";
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
const nameProblem = validatePackageName(name);
|
|
104
|
+
if (nameProblem !== null) {
|
|
105
|
+
throw new InvalidSpecError(raw, nameProblem);
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
if (!hasSelector || selectorText.length === 0) {
|
|
109
|
+
return { name, selector: { _tag: "Tag", tag: "latest" }, raw: input };
|
|
110
|
+
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
}
|
|
112
|
+
return { name, selector: parseSelector(raw, selectorText), raw: input };
|
|
113
|
+
};
|
|
113
114
|
|
|
114
115
|
/** Classifies the part after the `@` as an exact version, a range, or a dist-tag. */
|
|
115
116
|
export const parseSelector = (raw: string, text: string): Selector => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
117
|
+
// `semver.valid` only accepts a complete `x.y.z`, which is exactly the
|
|
118
|
+
// "pin me to this one version" case.
|
|
119
|
+
const exact = semver.valid(text, { loose: false });
|
|
120
|
+
if (exact !== null) {
|
|
121
|
+
return { _tag: "Exact", version: exact };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (semver.validRange(text, { loose: false }) !== null) {
|
|
125
|
+
return { _tag: "Range", range: text };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Anything left has to be a dist-tag. Tags may not look like versions and
|
|
129
|
+
// may not contain characters that would break a registry URL.
|
|
130
|
+
if (!DIST_TAG_RE.test(text)) {
|
|
131
|
+
throw new InvalidSpecError(
|
|
132
|
+
raw,
|
|
133
|
+
`"${text}" is not a valid version, semver range, or dist-tag`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return { _tag: "Tag", tag: text };
|
|
137
|
+
};
|
|
137
138
|
|
|
138
139
|
/**
|
|
139
140
|
* Parses many specs, collecting *every* failure rather than stopping at the
|
|
@@ -141,23 +142,26 @@ export const parseSelector = (raw: string, text: string): Selector => {
|
|
|
141
142
|
* in one go, not made to re-run four times.
|
|
142
143
|
*/
|
|
143
144
|
export const parseSpecs = (
|
|
144
|
-
|
|
145
|
-
): {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
145
|
+
inputs: ReadonlyArray<string>,
|
|
146
|
+
): {
|
|
147
|
+
readonly specs: ReadonlyArray<PackageSpec>;
|
|
148
|
+
readonly errors: ReadonlyArray<InvalidSpecError>;
|
|
149
|
+
} => {
|
|
150
|
+
const specs: Array<PackageSpec> = [];
|
|
151
|
+
const errors: Array<InvalidSpecError> = [];
|
|
152
|
+
for (const input of inputs) {
|
|
153
|
+
try {
|
|
154
|
+
specs.push(parseSpec(input));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof InvalidSpecError) {
|
|
157
|
+
errors.push(error);
|
|
158
|
+
} else {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { specs, errors };
|
|
164
|
+
};
|
|
161
165
|
|
|
162
166
|
/**
|
|
163
167
|
* Collapses duplicate specs, keeping the first occurrence.
|
|
@@ -166,13 +170,13 @@ export const parseSpecs = (
|
|
|
166
170
|
* download React twice.
|
|
167
171
|
*/
|
|
168
172
|
export const dedupeSpecs = (specs: ReadonlyArray<PackageSpec>): ReadonlyArray<PackageSpec> => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
173
|
+
const seen = new Set<string>();
|
|
174
|
+
const out: Array<PackageSpec> = [];
|
|
175
|
+
for (const spec of specs) {
|
|
176
|
+
const key = formatSpec(spec);
|
|
177
|
+
if (seen.has(key)) continue;
|
|
178
|
+
seen.add(key);
|
|
179
|
+
out.push(spec);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
};
|
package/src/types/value-of.ts
CHANGED
package/src/utils/is-record.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* package.json files, registry responses — without reaching for a cast.
|
|
6
6
|
*/
|
|
7
7
|
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
8
|
-
|
|
8
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|