@packall/core 0.1.0 → 0.3.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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/archive-DmF9XNl7.js +284 -0
  4. package/dist/archive-DmF9XNl7.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 +11 -1
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/index.d.ts +12 -10
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +169 -344
  23. package/dist/index.js.map +1 -1
  24. package/dist/input-file.d.ts.map +1 -1
  25. package/dist/integrity.d.ts +37 -5
  26. package/dist/integrity.d.ts.map +1 -1
  27. package/dist/layout.d.ts +41 -7
  28. package/dist/layout.d.ts.map +1 -1
  29. package/dist/locked-resolve.d.ts.map +1 -1
  30. package/dist/lockfile/bun.d.ts.map +1 -1
  31. package/dist/lockfile/detect.d.ts.map +1 -1
  32. package/dist/lockfile/json.d.ts.map +1 -1
  33. package/dist/lockfile/names.d.ts.map +1 -1
  34. package/dist/lockfile/npm.d.ts.map +1 -1
  35. package/dist/lockfile/parse.d.ts.map +1 -1
  36. package/dist/lockfile/pnpm.d.ts.map +1 -1
  37. package/dist/lockfile/tree-builder.d.ts.map +1 -1
  38. package/dist/lockfile/types.d.ts.map +1 -1
  39. package/dist/manifest.d.ts.map +1 -1
  40. package/dist/node/archiver.d.ts +5 -0
  41. package/dist/node/archiver.d.ts.map +1 -0
  42. package/dist/node/index.d.ts +13 -0
  43. package/dist/node/index.d.ts.map +1 -0
  44. package/dist/node/index.js +55 -0
  45. package/dist/node/index.js.map +1 -0
  46. package/dist/options.d.ts +3 -3
  47. package/dist/options.d.ts.map +1 -1
  48. package/dist/platform.d.ts +5 -10
  49. package/dist/platform.d.ts.map +1 -1
  50. package/dist/progress.d.ts.map +1 -1
  51. package/dist/registry.d.ts.map +1 -1
  52. package/dist/resolve.d.ts.map +1 -1
  53. package/dist/schemas/lenient.d.ts.map +1 -1
  54. package/dist/schemas/package-json.d.ts.map +1 -1
  55. package/dist/spec.d.ts.map +1 -1
  56. package/dist/types/value-of.d.ts.map +1 -1
  57. package/dist/utils/is-record.d.ts.map +1 -1
  58. package/package.json +11 -10
  59. package/src/archive.ts +95 -92
  60. package/src/bundle.ts +736 -616
  61. package/src/dependency-range.ts +121 -115
  62. package/src/download.ts +155 -133
  63. package/src/enums/artifact-kind.ts +5 -5
  64. package/src/enums/edge-kind.ts +13 -13
  65. package/src/enums/input-file-kind.ts +9 -9
  66. package/src/enums/layout.ts +20 -20
  67. package/src/enums/lockfile-format.ts +6 -6
  68. package/src/enums/phase.ts +8 -8
  69. package/src/errors.ts +211 -199
  70. package/src/index.ts +126 -113
  71. package/src/input-file.ts +246 -234
  72. package/src/integrity.ts +165 -121
  73. package/src/layout.ts +63 -29
  74. package/src/locked-resolve.ts +471 -461
  75. package/src/lockfile/bun.ts +214 -207
  76. package/src/lockfile/detect.ts +80 -80
  77. package/src/lockfile/json.ts +91 -92
  78. package/src/lockfile/names.ts +9 -9
  79. package/src/lockfile/npm.ts +328 -308
  80. package/src/lockfile/parse.ts +32 -32
  81. package/src/lockfile/pnpm.ts +230 -225
  82. package/src/lockfile/tree-builder.ts +116 -116
  83. package/src/lockfile/types.ts +52 -52
  84. package/src/manifest.ts +129 -133
  85. package/src/node/archiver.ts +85 -0
  86. package/src/node/index.ts +13 -0
  87. package/src/options.ts +84 -84
  88. package/src/platform.ts +97 -101
  89. package/src/progress.ts +105 -106
  90. package/src/registry.ts +116 -115
  91. package/src/resolve.ts +560 -537
  92. package/src/schemas/lenient.ts +67 -60
  93. package/src/schemas/package-json.ts +11 -10
  94. package/src/spec.ts +123 -119
  95. package/src/types/value-of.ts +1 -1
  96. package/src/utils/is-record.ts +1 -1
@@ -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
- import { isRecord } from "../utils/is-record.js"
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
- Schema.optional(
32
- Schema.UndefinedOr(schema).pipe(
33
- Schema.catchDecoding(() => Effect.succeed(Option.some(undefined)))
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
- Schema.decodeTo(Schema.Record(Schema.String, Schema.String), {
46
- decode: Getter.transform((raw: Record<string, unknown>) => {
47
- const out: Record<string, string> = {}
48
- for (const [key, value] of Object.entries(raw)) {
49
- if (typeof value === "string") out[key] = value
50
- }
51
- return out
52
- }),
53
- encode: Getter.passthroughSubtype()
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
- Schema.decodeTo(Schema.Array(Schema.String), {
60
- decode: Getter.transform((raw: ReadonlyArray<unknown>) =>
61
- raw.flatMap((entry) => (typeof entry === "string" ? entry : []))),
62
- encode: Getter.passthroughSubtype()
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
- Schema.decodeTo(Schema.Record(Schema.String, Schema.Struct({ optional: Schema.Boolean })), {
75
- decode: Getter.transform((raw: Record<string, unknown>) => {
76
- const out: Record<string, { optional: boolean }> = {}
77
- for (const [key, value] of Object.entries(raw)) {
78
- if (isRecord(value)) out[key] = { optional: value["optional"] === true }
79
- }
80
- return out
81
- }),
82
- encode: Getter.passthroughSubtype()
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
- Object.keys(value).length > 0 ? value : undefined
98
+ Object.keys(value).length > 0 ? value : undefined;
97
99
 
98
100
  export const OptionalStringRecordSchema = StringRecordSchema.pipe(
99
- Schema.decodeTo(Schema.UndefinedOr(Schema.Record(Schema.String, Schema.String)), {
100
- decode: Getter.transform(absentWhenEmpty<Record<string, string>>),
101
- encode: Getter.transform((value: Record<string, string> | undefined) => value ?? {})
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
- Schema.decodeTo(Schema.UndefinedOr(Schema.Array(Schema.String)), {
107
- decode: Getter.transform((value: ReadonlyArray<string>) =>
108
- value.length > 0 ? value : undefined),
109
- encode: Getter.transform((value: ReadonlyArray<string> | undefined) => value ?? [])
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
- Schema.decodeTo(
115
- Schema.UndefinedOr(Schema.Record(Schema.String, Schema.Struct({ optional: Schema.Boolean }))),
116
- {
117
- decode: Getter.transform(absentWhenEmpty<Record<string, { readonly optional: boolean }>>),
118
- encode: Getter.transform(
119
- (value: Record<string, { readonly optional: boolean }> | undefined) => value ?? {}
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
- import { OptionalFlagRecordSchema, StringRecordSchema, tolerant } from "./lenient.js"
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
- dependencies: tolerant(StringRecordSchema),
19
- devDependencies: tolerant(StringRecordSchema),
20
- optionalDependencies: tolerant(StringRecordSchema),
21
- peerDependencies: tolerant(StringRecordSchema),
22
- peerDependenciesMeta: tolerant(OptionalFlagRecordSchema)
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
- import { InvalidSpecError } from "./errors.js"
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
- /** An exact, fully-qualified version: `react@18.2.0`. */
14
- | { readonly _tag: "Exact"; readonly version: string }
15
- /** A semver range: `react@^18`, `react@>=17 <19`, `react@*`. */
16
- | { readonly _tag: "Range"; readonly range: string }
17
- /** A dist-tag: `react@latest`, `typescript@next`. */
18
- | { readonly _tag: "Tag"; readonly tag: string }
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
- /** The npm package name, including scope. */
23
- readonly name: string
24
- /** What the user asked for after the `@`. */
25
- readonly selector: Selector
26
- /** The original string, kept verbatim for error messages and the manifest. */
27
- readonly raw: string
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
- `${spec.name}@${formatSelector(spec.selector)}`
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
- switch (selector._tag) {
44
- case "Exact":
45
- return selector.version
46
- case "Range":
47
- return selector.range
48
- case "Tag":
49
- return selector.tag
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
- if (name.length === 0) return "name is empty"
59
- if (name.length > MAX_NAME_LENGTH) {
60
- return `name is longer than ${MAX_NAME_LENGTH} characters`
61
- }
62
- if (name.trim() !== name) return "name has leading or trailing whitespace"
63
- if (name.startsWith(".")) return "name cannot start with a period"
64
- if (name.startsWith("_")) return "name cannot start with an underscore"
65
- if (name !== name.toLowerCase()) return "name cannot contain capital letters"
66
-
67
- if (name.startsWith("@")) {
68
- const slash = name.indexOf("/")
69
- if (slash === -1) return "scoped name is missing the '/name' part"
70
- const scope = name.slice(1, slash)
71
- const rest = name.slice(slash + 1)
72
- if (scope.length === 0) return "scope is empty"
73
- if (rest.length === 0) return "name after the scope is empty"
74
- if (rest.includes("/")) return "name contains more than one '/'"
75
- if (!SEGMENT_RE.test(scope)) return `scope "${scope}" contains illegal characters`
76
- if (!SEGMENT_RE.test(rest)) return `name "${rest}" contains illegal characters`
77
- return null
78
- }
79
-
80
- if (name.includes("/")) return "unscoped name cannot contain '/'"
81
- if (!SEGMENT_RE.test(name)) return "name contains illegal characters"
82
- return null
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
- const input = raw.trim()
93
- if (input.length === 0) {
94
- throw new InvalidSpecError(raw, "spec is empty")
95
- }
93
+ const input = raw.trim();
94
+ if (input.length === 0) {
95
+ throw new InvalidSpecError(raw, "spec is empty");
96
+ }
96
97
 
97
- const at = input.lastIndexOf("@")
98
- const hasSelector = at > 0
99
- const name = hasSelector ? input.slice(0, at) : input
100
- const selectorText = hasSelector ? input.slice(at + 1) : ""
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
- const nameProblem = validatePackageName(name)
103
- if (nameProblem !== null) {
104
- throw new InvalidSpecError(raw, nameProblem)
105
- }
103
+ const nameProblem = validatePackageName(name);
104
+ if (nameProblem !== null) {
105
+ throw new InvalidSpecError(raw, nameProblem);
106
+ }
106
107
 
107
- if (!hasSelector || selectorText.length === 0) {
108
- return { name, selector: { _tag: "Tag", tag: "latest" }, raw: input }
109
- }
108
+ if (!hasSelector || selectorText.length === 0) {
109
+ return { name, selector: { _tag: "Tag", tag: "latest" }, raw: input };
110
+ }
110
111
 
111
- return { name, selector: parseSelector(raw, selectorText), raw: input }
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
- // `semver.valid` only accepts a complete `x.y.z`, which is exactly the
117
- // "pin me to this one version" case.
118
- const exact = semver.valid(text, { loose: false })
119
- if (exact !== null) {
120
- return { _tag: "Exact", version: exact }
121
- }
122
-
123
- if (semver.validRange(text, { loose: false }) !== null) {
124
- return { _tag: "Range", range: text }
125
- }
126
-
127
- // Anything left has to be a dist-tag. Tags may not look like versions and
128
- // may not contain characters that would break a registry URL.
129
- if (!DIST_TAG_RE.test(text)) {
130
- throw new InvalidSpecError(
131
- raw,
132
- `"${text}" is not a valid version, semver range, or dist-tag`
133
- )
134
- }
135
- return { _tag: "Tag", tag: text }
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
- inputs: ReadonlyArray<string>
145
- ): { readonly specs: ReadonlyArray<PackageSpec>; readonly errors: ReadonlyArray<InvalidSpecError> } => {
146
- const specs: Array<PackageSpec> = []
147
- const errors: Array<InvalidSpecError> = []
148
- for (const input of inputs) {
149
- try {
150
- specs.push(parseSpec(input))
151
- } catch (error) {
152
- if (error instanceof InvalidSpecError) {
153
- errors.push(error)
154
- } else {
155
- throw error
156
- }
157
- }
158
- }
159
- return { specs, errors }
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
- const seen = new Set<string>()
170
- const out: Array<PackageSpec> = []
171
- for (const spec of specs) {
172
- const key = formatSpec(spec)
173
- if (seen.has(key)) continue
174
- seen.add(key)
175
- out.push(spec)
176
- }
177
- return out
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
+ };
@@ -5,4 +5,4 @@
5
5
  * package: the object is the value, `ValueOf<typeof X>` is the type, and the two
6
6
  * share one name.
7
7
  */
8
- export type ValueOf<T> = T[keyof T]
8
+ export type ValueOf<T> = T[keyof T];
@@ -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
- typeof value === "object" && value !== null && !Array.isArray(value)
8
+ typeof value === "object" && value !== null && !Array.isArray(value);