@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/integrity.ts CHANGED
@@ -1,121 +1,165 @@
1
- /**
2
- * Checksum verification for downloaded tarballs.
3
- *
4
- * A bundle is a supply-chain artifact that gets imported wholesale into a
5
- * corporate registry, so "the bytes I got are the bytes the registry promised"
6
- * is the one property worth being strict about. A failed check discards the
7
- * download rather than warning and carrying on.
8
- *
9
- * Implemented directly against `node:crypto` rather than pulling in `ssri`,
10
- * because the subset of Subresource Integrity that npm actually publishes is
11
- * small and entirely mechanical.
12
- */
13
- import { createHash, timingSafeEqual } from "node:crypto"
14
-
15
- /** One parsed SRI hash. */
16
- export type IntegrityHash = {
17
- readonly algorithm: string
18
- /** Base64 digest, exactly as written in the SRI string. */
19
- readonly digest: string
20
- }
21
-
22
- const SUPPORTED = new Set(["sha512", "sha384", "sha256", "sha1"])
23
-
24
- /**
25
- * Parses an SRI string such as `sha512-abc...==`.
26
- *
27
- * npm permits several space-separated hashes; we keep every one we can verify
28
- * and ignore algorithms Node does not implement.
29
- */
30
- export const parseIntegrity = (integrity: string): ReadonlyArray<IntegrityHash> => {
31
- const hashes: Array<IntegrityHash> = []
32
- for (const token of integrity.trim().split(/\s+/)) {
33
- if (token.length === 0) continue
34
- const dash = token.indexOf("-")
35
- if (dash <= 0) continue
36
- const algorithm = token.slice(0, dash).toLowerCase()
37
- const digest = token.slice(dash + 1)
38
- if (!SUPPORTED.has(algorithm) || digest.length === 0) continue
39
- hashes.push({ algorithm, digest })
40
- }
41
- return hashes
42
- }
43
-
44
- /** Computes the base64 digest of some bytes under one algorithm. */
45
- export const digestOf = (data: Uint8Array, algorithm: string): string =>
46
- createHash(algorithm).update(data).digest("base64")
47
-
48
- /** Computes the hex digest of some bytes — the form legacy `shasum` uses. */
49
- export const hexDigestOf = (data: Uint8Array, algorithm: string): string =>
50
- createHash(algorithm).update(data).digest("hex")
51
-
52
- /** Result of checking a download. */
53
- export type VerificationResult =
54
- /** Verified against at least one checksum. */
55
- | { readonly _tag: "Verified"; readonly using: string }
56
- /** The registry advertised no checksum we could use. */
57
- | { readonly _tag: "Unverifiable"; readonly reason: string }
58
- /** A checksum was present and did not match. */
59
- | { readonly _tag: "Mismatch"; readonly expected: string; readonly actual: string }
60
-
61
- /** Constant-time comparison of two digest strings of equal length. */
62
- const digestsEqual = (a: string, b: string): boolean => {
63
- const left = Buffer.from(a)
64
- const right = Buffer.from(b)
65
- // `timingSafeEqual` throws on unequal lengths, which is itself a mismatch.
66
- return left.length === right.length && timingSafeEqual(left, right)
67
- }
68
-
69
- /**
70
- * Checks bytes against whichever checksums the registry supplied.
71
- *
72
- * SRI is preferred; `shasum` is the fallback for versions published before
73
- * integrity strings existed. If the registry gave us neither, that is reported
74
- * as `Unverifiable` rather than silently treated as a pass — the caller decides
75
- * whether to tolerate it.
76
- */
77
- export const verify = (
78
- data: Uint8Array,
79
- dist: { readonly integrity?: string | undefined; readonly shasum?: string | undefined }
80
- ): VerificationResult => {
81
- if (dist.integrity !== undefined && dist.integrity.length > 0) {
82
- // Strongest first, so a mismatch is reported against the best hash present.
83
- const [best] = parseIntegrity(dist.integrity).toSorted(
84
- (a, b) => strength(b.algorithm) - strength(a.algorithm)
85
- )
86
- if (best === undefined) {
87
- return { _tag: "Unverifiable", reason: `no supported algorithm in "${dist.integrity}"` }
88
- }
89
-
90
- const actual = digestOf(data, best.algorithm)
91
- return digestsEqual(actual, best.digest)
92
- ? { _tag: "Verified", using: best.algorithm }
93
- : {
94
- _tag: "Mismatch",
95
- expected: `${best.algorithm}-${best.digest}`,
96
- actual: `${best.algorithm}-${actual}`
97
- }
98
- }
99
-
100
- if (dist.shasum !== undefined && dist.shasum.length > 0) {
101
- const actual = hexDigestOf(data, "sha1")
102
- return digestsEqual(actual.toLowerCase(), dist.shasum.toLowerCase())
103
- ? { _tag: "Verified", using: "sha1" }
104
- : { _tag: "Mismatch", expected: `sha1-${dist.shasum}`, actual: `sha1-${actual}` }
105
- }
106
-
107
- return { _tag: "Unverifiable", reason: "registry advertised no integrity or shasum" }
108
- }
109
-
110
- const strength = (algorithm: string): number => {
111
- switch (algorithm) {
112
- case "sha512":
113
- return 4
114
- case "sha384":
115
- return 3
116
- case "sha256":
117
- return 2
118
- default:
119
- return 1
120
- }
121
- }
1
+ /**
2
+ * Checksum verification for downloaded tarballs.
3
+ *
4
+ * A failed check discards the download rather than warning and carrying on —
5
+ * see `notes/requirements.md` § FR-9 for why that strictness is a requirement
6
+ * rather than a preference.
7
+ *
8
+ * Built on Effect's `Crypto` service rather than `node:crypto` directly. The
9
+ * four digests `Crypto` offers SHA-1, SHA-256, SHA-384, SHA-512 — are exactly
10
+ * the four npm publishes, so nothing is given up, and the same verification
11
+ * then runs unchanged against WebCrypto in a browser. Parsing stays here
12
+ * because the subset of Subresource Integrity npm actually emits is small and
13
+ * entirely mechanical; `ssri` would be a dependency for a regex.
14
+ */
15
+ import * as Crypto from "effect/Crypto";
16
+ import * as Effect from "effect/Effect";
17
+ import * as Encoding from "effect/Encoding";
18
+ import type { PlatformError } from "effect/PlatformError";
19
+
20
+ /**
21
+ * The digests npm publishes, mapped to what `Crypto.digest` calls them.
22
+ *
23
+ * Doubles as the supported-algorithm list: an SRI token whose algorithm is not
24
+ * a key here is one we cannot check, and is dropped during parsing.
25
+ */
26
+ const digestAlgorithmByName = {
27
+ sha512: "SHA-512",
28
+ sha384: "SHA-384",
29
+ sha256: "SHA-256",
30
+ sha1: "SHA-1",
31
+ } as const satisfies Record<string, Crypto.DigestAlgorithm>;
32
+
33
+ /** An algorithm this module can verify against. */
34
+ export type IntegrityAlgorithm = keyof typeof digestAlgorithmByName;
35
+
36
+ const isSupported = (algorithm: string): algorithm is IntegrityAlgorithm =>
37
+ Object.hasOwn(digestAlgorithmByName, algorithm);
38
+
39
+ /** One parsed SRI hash. */
40
+ export type IntegrityHash = {
41
+ readonly algorithm: IntegrityAlgorithm;
42
+ /** Base64 digest, exactly as written in the SRI string. */
43
+ readonly digest: string;
44
+ };
45
+
46
+ /**
47
+ * Parses an SRI string such as `sha512-abc...==`.
48
+ *
49
+ * npm permits several space-separated hashes; we keep every one we can verify
50
+ * and ignore algorithms we cannot.
51
+ */
52
+ export const parseIntegrity = (integrity: string): ReadonlyArray<IntegrityHash> => {
53
+ const hashes: Array<IntegrityHash> = [];
54
+ for (const token of integrity.trim().split(/\s+/)) {
55
+ if (token.length === 0) continue;
56
+ const dash = token.indexOf("-");
57
+ if (dash <= 0) continue;
58
+ const algorithm = token.slice(0, dash).toLowerCase();
59
+ const digest = token.slice(dash + 1);
60
+ if (!isSupported(algorithm) || digest.length === 0) continue;
61
+ hashes.push({ algorithm, digest });
62
+ }
63
+ return hashes;
64
+ };
65
+
66
+ const digestBytes = (
67
+ data: Uint8Array,
68
+ algorithm: IntegrityAlgorithm,
69
+ ): Effect.Effect<Uint8Array, PlatformError, Crypto.Crypto> =>
70
+ Effect.gen(function* () {
71
+ const crypto = yield* Crypto.Crypto;
72
+ return yield* crypto.digest(digestAlgorithmByName[algorithm], data);
73
+ });
74
+
75
+ /** Computes the base64 digest of some bytes under one algorithm. */
76
+ export const digestOf = (
77
+ data: Uint8Array,
78
+ algorithm: IntegrityAlgorithm,
79
+ ): Effect.Effect<string, PlatformError, Crypto.Crypto> =>
80
+ Effect.map(digestBytes(data, algorithm), Encoding.encodeBase64);
81
+
82
+ /** Computes the hex digest of some bytes the form legacy `shasum` uses. */
83
+ export const hexDigestOf = (
84
+ data: Uint8Array,
85
+ algorithm: IntegrityAlgorithm,
86
+ ): Effect.Effect<string, PlatformError, Crypto.Crypto> =>
87
+ Effect.map(digestBytes(data, algorithm), Encoding.encodeHex);
88
+
89
+ /** Result of checking a download. */
90
+ export type VerificationResult =
91
+ /** Verified against at least one checksum. */
92
+ | { readonly _tag: "Verified"; readonly using: string }
93
+ /** The registry advertised no checksum we could use. */
94
+ | { readonly _tag: "Unverifiable"; readonly reason: string }
95
+ /** A checksum was present and did not match. */
96
+ | { readonly _tag: "Mismatch"; readonly expected: string; readonly actual: string };
97
+
98
+ /**
99
+ * Constant-time comparison of two digest strings.
100
+ *
101
+ * Written by hand rather than with `node:crypto`'s `timingSafeEqual`, which
102
+ * needs `Buffer` and would pin this module to Node. Both digests here are
103
+ * public knowledge, so the timing property is close to theatre — but it costs
104
+ * four lines, and the alternative is explaining in a review why it was dropped.
105
+ */
106
+ const digestsEqual = (a: string, b: string): boolean => {
107
+ if (a.length !== b.length) return false;
108
+ let difference = 0;
109
+ for (let index = 0; index < a.length; index++) {
110
+ difference |= a.charCodeAt(index) ^ b.charCodeAt(index);
111
+ }
112
+ return difference === 0;
113
+ };
114
+
115
+ /**
116
+ * Checks bytes against whichever checksums the registry supplied.
117
+ *
118
+ * SRI is preferred; `shasum` is the fallback for versions published before
119
+ * integrity strings existed. If the registry gave us neither, that is reported
120
+ * as `Unverifiable` rather than silently treated as a pass — the caller decides
121
+ * whether to tolerate it.
122
+ */
123
+ export const verify = (
124
+ data: Uint8Array,
125
+ dist: { readonly integrity?: string | undefined; readonly shasum?: string | undefined },
126
+ ): Effect.Effect<VerificationResult, PlatformError, Crypto.Crypto> =>
127
+ Effect.gen(function* () {
128
+ if (dist.integrity !== undefined && dist.integrity.length > 0) {
129
+ // Strongest first, so a mismatch is reported against the best hash present.
130
+ const [best] = parseIntegrity(dist.integrity).toSorted(
131
+ (a, b) => strengthByAlgorithm[b.algorithm] - strengthByAlgorithm[a.algorithm],
132
+ );
133
+ if (best === undefined) {
134
+ return {
135
+ _tag: "Unverifiable",
136
+ reason: `no supported algorithm in "${dist.integrity}"`,
137
+ };
138
+ }
139
+
140
+ const actual = yield* digestOf(data, best.algorithm);
141
+ return digestsEqual(actual, best.digest)
142
+ ? { _tag: "Verified", using: best.algorithm }
143
+ : {
144
+ _tag: "Mismatch",
145
+ expected: `${best.algorithm}-${best.digest}`,
146
+ actual: `${best.algorithm}-${actual}`,
147
+ };
148
+ }
149
+
150
+ if (dist.shasum !== undefined && dist.shasum.length > 0) {
151
+ const actual = yield* hexDigestOf(data, "sha1");
152
+ return digestsEqual(actual.toLowerCase(), dist.shasum.toLowerCase())
153
+ ? { _tag: "Verified", using: "sha1" }
154
+ : { _tag: "Mismatch", expected: `sha1-${dist.shasum}`, actual: `sha1-${actual}` };
155
+ }
156
+
157
+ return { _tag: "Unverifiable", reason: "registry advertised no integrity or shasum" };
158
+ });
159
+
160
+ const strengthByAlgorithm = {
161
+ sha512: 4,
162
+ sha384: 3,
163
+ sha256: 2,
164
+ sha1: 1,
165
+ } as const satisfies Record<IntegrityAlgorithm, number>;
package/src/layout.ts CHANGED
@@ -7,27 +7,29 @@
7
7
  * @babel/core/-/core-7.24.0.tgz
8
8
  *
9
9
  * That is not an Artifactory convention — it is the path structure in every
10
- * `dist.tarball` URL npm publishes, which is why the same tree imports into
11
- * Artifactory, Nexus and Verdaccio without translation. Note that a scoped
12
- * package's file name drops the scope: `@babel/core` becomes `core-7.24.0.tgz`
13
- * under an `@babel/core/-/` directory.
10
+ * `dist.tarball` URL npm publishes. See `notes/requirements.md` § FR-12 for
11
+ * what that buys.
12
+ *
13
+ * The one shape worth noticing before editing anything here: a scoped
14
+ * package's file name drops the scope, so `@babel/core` becomes
15
+ * `core-7.24.0.tgz` under an `@babel/core/-/` directory.
14
16
  */
15
17
 
16
18
  /** Splits `@scope/name` into its parts. `scope` is undefined when unscoped. */
17
19
  export const splitName = (
18
- name: string
20
+ name: string,
19
21
  ): { readonly scope: string | undefined; readonly bare: string } => {
20
- if (!name.startsWith("@")) return { scope: undefined, bare: name }
21
- const slash = name.indexOf("/")
22
- if (slash === -1) return { scope: undefined, bare: name }
23
- return { scope: name.slice(0, slash), bare: name.slice(slash + 1) }
24
- }
22
+ if (!name.startsWith("@")) return { scope: undefined, bare: name };
23
+ const slash = name.indexOf("/");
24
+ if (slash === -1) return { scope: undefined, bare: name };
25
+ return { scope: name.slice(0, slash), bare: name.slice(slash + 1) };
26
+ };
25
27
 
26
28
  /** `@babel/core` + `7.24.0` -> `core-7.24.0.tgz` */
27
29
  export const tarballFileName = (name: string, version: string): string => {
28
- const { bare } = splitName(name)
29
- return `${bare}-${version}.tgz`
30
- }
30
+ const { bare } = splitName(name);
31
+ return `${bare}-${version}.tgz`;
32
+ };
31
33
 
32
34
  /**
33
35
  * The path of one package tarball within a bundle, using `/` separators
@@ -35,29 +37,61 @@ export const tarballFileName = (name: string, version: string): string => {
35
37
  * are always POSIX.
36
38
  */
37
39
  export const packagePath = (name: string, version: string): string =>
38
- `${name}/-/${tarballFileName(name, version)}`
40
+ `${name}/-/${tarballFileName(name, version)}`;
39
41
 
40
42
  /** File name of the manifest that ships inside every bundle. */
41
- export const MANIFEST_FILE = "bundle-manifest.json"
43
+ export const MANIFEST_FILE = "bundle-manifest.json";
42
44
 
43
45
  /** File name of the short import guide that ships inside every bundle. */
44
- export const README_FILE = "IMPORT.md"
46
+ export const README_FILE = "IMPORT.md";
45
47
 
46
48
  /**
47
49
  * Name of the archive produced for one root spec in `per-spec` layout.
48
50
  *
49
- * Scoped names are flattened (`@babel/core` -> `babel-core`) because a `/` in a
50
- * file name is not a thing, and `@` confuses enough shells and web UIs to be
51
- * worth avoiding.
51
+ * It is the spec, with the one character a file name cannot hold taken out:
52
+ * `@types/react@19.2.18` becomes `@types-react@19.2.18.tgz`. So the name of the
53
+ * file answers the question somebody has when they are looking at a directory
54
+ * of them — which package, and which version — in the notation they would have
55
+ * typed to ask for it.
56
+ *
57
+ * The version is joined with `@` rather than `-` for the same reason. A
58
+ * hyphen made `foo-bar-1.0.0` ambiguous by eye: package `foo` at `bar-1.0.0`,
59
+ * or `foo-bar` at `1.0.0`? npm's own syntax has no such problem, because a
60
+ * package name may not contain `@` beyond its scope.
61
+ *
62
+ * **The scope's `@` stays**, and it is the difference between a correct bundle
63
+ * and a quietly incomplete one.
64
+ *
65
+ * Dropping it — which this did, matching what `npm pack` produces — meant
66
+ * `@something/somewhat` and `something-somewhat` were both
67
+ * `something-somewhat-1.0.0.tgz`. Two different packages, one path: the second
68
+ * landed on the first, and the run reported two artifacts for the one file that
69
+ * survived. A bundle short a package, called complete, found at the far end of
70
+ * an air gap.
71
+ *
72
+ * Keeping the `@` makes that impossible by construction rather than by a check,
73
+ * because an unscoped npm name may not begin with one. It is also the more
74
+ * honest name: the scope is part of what the package *is*, and a directory of
75
+ * archives that has forgotten which ones were scoped is a directory somebody
76
+ * has to cross-reference against a manifest.
77
+ *
78
+ * It does not make every collision impossible — `@a/b-c` and `@a-b/c` still
79
+ * meet — so `assertDistinctOutputs` still runs. That pair is pathological; the
80
+ * scoped-against-unscoped one is `@babel/core` beside `babel-core`, which is a
81
+ * real migration somebody could be bundling.
82
+ *
83
+ * The leading `@` is safe here in a way it is not inside the archive: tar's
84
+ * splice convention applies to the *file list* it is given, which is what
85
+ * `archive.ts` guards with `./`. This is the output path, not an entry.
52
86
  */
53
87
  export const perSpecArchiveName = (name: string, version: string): string => {
54
- const { scope, bare } = splitName(name)
55
- const prefix = scope === undefined ? bare : `${scope.slice(1)}-${bare}`
56
- return `${prefix}-${version}.tgz`
57
- }
88
+ const { scope, bare } = splitName(name);
89
+ const prefix = scope === undefined ? bare : `${scope}-${bare}`;
90
+ return `${prefix}@${version}.tgz`;
91
+ };
58
92
 
59
93
  /** Name of the archive produced in `single` layout. */
60
- export const singleArchiveName = (base = "bundle"): string => `${base}.tgz`
94
+ export const singleArchiveName = (base = "bundle"): string => `${base}.tgz`;
61
95
 
62
96
  /**
63
97
  * The import guide written into each bundle.
@@ -66,11 +100,11 @@ export const singleArchiveName = (base = "bundle"): string => `${base}.tgz`
66
100
  * restricted network and does not want prose.
67
101
  */
68
102
  export const importGuide = (options: {
69
- readonly packageCount: number
70
- readonly createdAt: string
71
- readonly toolVersion: string
103
+ readonly packageCount: number;
104
+ readonly createdAt: string;
105
+ readonly toolVersion: string;
72
106
  }): string =>
73
- `# Importing this bundle
107
+ `# Importing this bundle
74
108
 
75
109
  ${options.packageCount} package tarball(s), laid out exactly as an npm registry serves them:
76
110
 
@@ -100,4 +134,4 @@ Publish each tarball individually:
100
134
 
101
135
  Every entry in ${MANIFEST_FILE} carries the integrity string the source registry
102
136
  advertised, and each tarball was checked against it at download time.
103
- `
137
+ `;