@packall/core 0.3.3 → 0.4.1
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/README.md +2 -2
- package/dist/bundle.d.ts +7 -6
- package/dist/bundle.d.ts.map +1 -1
- package/dist/enums/artifact-kind.d.ts +6 -0
- package/dist/enums/artifact-kind.d.ts.map +1 -1
- package/dist/enums/layout.d.ts +6 -0
- package/dist/enums/layout.d.ts.map +1 -1
- package/dist/index.js +135 -28
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +48 -12
- package/dist/layout.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bundle.ts +140 -7
- package/src/enums/artifact-kind.ts +6 -0
- package/src/enums/layout.ts +6 -0
- package/src/layout.ts +83 -12
- package/src/manifest.ts +2 -2
package/dist/layout.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The on-disk shape of a bundle.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Under every layout but `flat`, everything is laid out exactly the way a
|
|
5
|
+
* registry serves it:
|
|
5
6
|
*
|
|
6
7
|
* lodash/-/lodash-4.17.21.tgz
|
|
7
8
|
* @babel/core/-/core-7.24.0.tgz
|
|
@@ -13,7 +14,10 @@
|
|
|
13
14
|
* The one shape worth noticing before editing anything here: a scoped
|
|
14
15
|
* package's file name drops the scope, so `@babel/core` becomes
|
|
15
16
|
* `core-7.24.0.tgz` under an `@babel/core/-/` directory.
|
|
17
|
+
*
|
|
18
|
+
* `flat` layout is the exception; `bundlePath` is where the two part company.
|
|
16
19
|
*/
|
|
20
|
+
import { Layout } from "./enums/layout.js";
|
|
17
21
|
/** Splits `@scope/name` into its parts. `scope` is undefined when unscoped. */
|
|
18
22
|
export declare const splitName: (name: string) => {
|
|
19
23
|
readonly scope: string | undefined;
|
|
@@ -32,7 +36,7 @@ export declare const MANIFEST_FILE = "bundle-manifest.json";
|
|
|
32
36
|
/** File name of the short import guide that ships inside every bundle. */
|
|
33
37
|
export declare const README_FILE = "IMPORT.md";
|
|
34
38
|
/**
|
|
35
|
-
*
|
|
39
|
+
* A flat, collision-free file name for one `name@version`.
|
|
36
40
|
*
|
|
37
41
|
* It is the spec, with the one character a file name cannot hold taken out:
|
|
38
42
|
* `@types/react@19.2.18` becomes `@types-react@19.2.18.tgz`. So the name of the
|
|
@@ -62,26 +66,58 @@ export declare const README_FILE = "IMPORT.md";
|
|
|
62
66
|
* has to cross-reference against a manifest.
|
|
63
67
|
*
|
|
64
68
|
* It does not make every collision impossible — `@a/b-c` and `@a-b/c` still
|
|
65
|
-
* meet — so
|
|
66
|
-
* scoped-against-unscoped one is `@babel/core` beside
|
|
67
|
-
* real migration somebody could be bundling.
|
|
69
|
+
* meet — so the run still checks before downloading anything. That pair is
|
|
70
|
+
* pathological; the scoped-against-unscoped one is `@babel/core` beside
|
|
71
|
+
* `babel-core`, which is a real migration somebody could be bundling.
|
|
68
72
|
*
|
|
69
73
|
* The leading `@` is safe here in a way it is not inside the archive: tar's
|
|
70
74
|
* splice convention applies to the *file list* it is given, which is what
|
|
71
|
-
* `archive.ts` guards with `./`.
|
|
75
|
+
* `archive.ts` guards with `./`. These are output paths, not entries.
|
|
76
|
+
*/
|
|
77
|
+
export declare const flatName: (name: string, version: string) => string;
|
|
78
|
+
/**
|
|
79
|
+
* Name of the archive produced for one root spec in `per-spec` layout.
|
|
80
|
+
*
|
|
81
|
+
* The same rule as {@link flatName}, which is where the reasoning lives — a
|
|
82
|
+
* per-spec archive and a `flat` package tarball are both "one file in a
|
|
83
|
+
* directory, named for what is in it", and there is no reason for a directory
|
|
84
|
+
* of bundles and a directory of packages to be read differently.
|
|
85
|
+
*/
|
|
86
|
+
export declare const perSpecArchiveName: typeof flatName;
|
|
87
|
+
/**
|
|
88
|
+
* The path of one package tarball within a bundle. The one place the two
|
|
89
|
+
* shapes are chosen between, so a manifest and the tree it describes cannot
|
|
90
|
+
* drift apart.
|
|
91
|
+
*
|
|
92
|
+
* `flat` keeps a file name and no directories, and uses {@link flatName}
|
|
93
|
+
* rather than the registry file name: with the directories gone that name is
|
|
94
|
+
* all there is to tell two packages apart, and the registry one drops the
|
|
95
|
+
* scope, which would put `@types/node` and `node` on the same file. The name
|
|
96
|
+
* is free to differ — `npm publish <tarball>` reads a package's name and
|
|
97
|
+
* version from the `package.json` inside it, not from what the file is
|
|
98
|
+
* called.
|
|
72
99
|
*/
|
|
73
|
-
export declare const
|
|
100
|
+
export declare const bundlePath: (layout: Layout, name: string, version: string) => string;
|
|
74
101
|
/** Name of the archive produced in `single` layout. */
|
|
75
102
|
export declare const singleArchiveName: (base?: string) => string;
|
|
103
|
+
/** Shared by both guides. */
|
|
104
|
+
type GuideOptions = {
|
|
105
|
+
readonly packageCount: number;
|
|
106
|
+
readonly createdAt: string;
|
|
107
|
+
readonly toolVersion: string;
|
|
108
|
+
readonly layout: Layout;
|
|
109
|
+
};
|
|
76
110
|
/**
|
|
77
111
|
* The import guide written into each bundle.
|
|
78
112
|
*
|
|
79
113
|
* Kept short and copy-pasteable on purpose: whoever opens this is mid-task on a
|
|
80
114
|
* restricted network and does not want prose.
|
|
115
|
+
*
|
|
116
|
+
* Layout-aware, because the instructions genuinely differ — a registry-layout
|
|
117
|
+
* tree uploads into Artifactory as it stands and a flat directory does not, and
|
|
118
|
+
* a guide that said otherwise would be discovered to be wrong by somebody who
|
|
119
|
+
* can no longer look anything up.
|
|
81
120
|
*/
|
|
82
|
-
export declare const importGuide: (options:
|
|
83
|
-
|
|
84
|
-
readonly createdAt: string;
|
|
85
|
-
readonly toolVersion: string;
|
|
86
|
-
}) => string;
|
|
121
|
+
export declare const importGuide: (options: GuideOptions) => string;
|
|
122
|
+
export {};
|
|
87
123
|
//# sourceMappingURL=layout.d.ts.map
|
package/dist/layout.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,+EAA+E;AAC/E,eAAO,MAAM,SAAS,SACf,MAAM,KACV;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAK7D,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,eAAe,SAAU,MAAM,WAAW,MAAM,KAAG,MAG/D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,SAAU,MAAM,WAAW,MAAM,KAAG,MACd,CAAC;AAE/C,gEAAgE;AAChE,eAAO,MAAM,aAAa,yBAAyB,CAAC;AAEpD,0EAA0E;AAC1E,eAAO,MAAM,WAAW,cAAc,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,QAAQ,SAAU,MAAM,WAAW,MAAM,KAAG,MAIxD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,iBAAW,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,WAAY,MAAM,QAAQ,MAAM,WAAW,MAAM,KAAG,MACG,CAAC;AAE/E,uDAAuD;AACvD,eAAO,MAAM,iBAAiB,qBAAsB,MAAuB,CAAC;AAE5E,6BAA6B;AAC7B,KAAK,YAAY,GAAG;IACnB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,YAAa,YAAY,KAAG,MACqB,CAAC"}
|
package/package.json
CHANGED
package/src/bundle.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { Phase } from "./enums/phase.js";
|
|
|
32
32
|
import type { BundlerError } from "./errors.js";
|
|
33
33
|
import { OutputError } from "./errors.js";
|
|
34
34
|
import {
|
|
35
|
+
flatName,
|
|
35
36
|
importGuide,
|
|
36
37
|
MANIFEST_FILE,
|
|
37
38
|
packagePath,
|
|
@@ -57,7 +58,7 @@ export type BundleArtifact = {
|
|
|
57
58
|
readonly path: string;
|
|
58
59
|
readonly bytes: number;
|
|
59
60
|
readonly packageCount: number;
|
|
60
|
-
/** Which spec this artifact covers.
|
|
61
|
+
/** Which spec this artifact covers. Only `per-spec` sets it. */
|
|
61
62
|
readonly spec?: string | undefined;
|
|
62
63
|
readonly version?: string | undefined;
|
|
63
64
|
};
|
|
@@ -99,11 +100,12 @@ export type PlanSummary = {
|
|
|
99
100
|
/**
|
|
100
101
|
* The files a run will write, known before anything is downloaded.
|
|
101
102
|
*
|
|
102
|
-
* `dir`
|
|
103
|
-
*
|
|
104
|
-
* version
|
|
105
|
-
*
|
|
106
|
-
* the
|
|
103
|
+
* `dir` and `flat` are the odd ones: they merge a package tree into `outDir`
|
|
104
|
+
* rather than writing an archive. Either way a package's path is derived from
|
|
105
|
+
* its name and version, so the same package at the same version is the same
|
|
106
|
+
* bytes at the same path — an overlap is idempotent rather than destructive.
|
|
107
|
+
* Only the two summary files are genuinely replaced, and those are the ones
|
|
108
|
+
* worth guarding.
|
|
107
109
|
*/
|
|
108
110
|
export type PlannedOutput = {
|
|
109
111
|
readonly file: string;
|
|
@@ -294,7 +296,9 @@ export const plannedOutputs = (
|
|
|
294
296
|
resolution: Resolution,
|
|
295
297
|
options: BundleContext,
|
|
296
298
|
): ReadonlyArray<PlannedOutput> => {
|
|
297
|
-
if (options.layout === Layout.Dir
|
|
299
|
+
if (options.layout === Layout.Dir || options.layout === Layout.Flat) {
|
|
300
|
+
return [{ file: MANIFEST_FILE }, { file: README_FILE }];
|
|
301
|
+
}
|
|
298
302
|
if (options.layout === Layout.Single) {
|
|
299
303
|
return [{ file: singleArchiveName(options.archiveName ?? "bundle") }];
|
|
300
304
|
}
|
|
@@ -371,6 +375,8 @@ const emit = (
|
|
|
371
375
|
return emitDirectory(input);
|
|
372
376
|
case Layout.PerSpec:
|
|
373
377
|
return emitPerSpec(input, staging);
|
|
378
|
+
case Layout.Flat:
|
|
379
|
+
return emitFlat(input, staging);
|
|
374
380
|
}
|
|
375
381
|
};
|
|
376
382
|
|
|
@@ -460,6 +466,89 @@ const emitDirectory = (
|
|
|
460
466
|
];
|
|
461
467
|
});
|
|
462
468
|
|
|
469
|
+
/**
|
|
470
|
+
* The flat directory: every tarball side by side, no archive, no nesting.
|
|
471
|
+
*
|
|
472
|
+
* The tarballs are the untouched registry ones — only their names are ours,
|
|
473
|
+
* and `bundlePath` says why.
|
|
474
|
+
*/
|
|
475
|
+
const emitFlat = (
|
|
476
|
+
input: EmitInput,
|
|
477
|
+
staging: string,
|
|
478
|
+
): Effect.Effect<
|
|
479
|
+
ReadonlyArray<BundleArtifact>,
|
|
480
|
+
BundlerError | PlatformError,
|
|
481
|
+
FileSystem.FileSystem | Path.Path
|
|
482
|
+
> =>
|
|
483
|
+
Effect.gen(function* () {
|
|
484
|
+
const fs = yield* FileSystem.FileSystem;
|
|
485
|
+
const path = yield* Path.Path;
|
|
486
|
+
|
|
487
|
+
// Staged and then copied rather than written straight into `outDir`, so a
|
|
488
|
+
// failure partway through leaves the output directory as it was.
|
|
489
|
+
const flatDir = path.join(staging, "flat");
|
|
490
|
+
yield* fs.makeDirectory(flatDir, { recursive: true });
|
|
491
|
+
|
|
492
|
+
for (const pkg of input.resolution.packages) {
|
|
493
|
+
const parts = packagePath(pkg.name, pkg.version).split("/");
|
|
494
|
+
yield* linkOrCopy(
|
|
495
|
+
path.join(input.packagesDir, ...parts),
|
|
496
|
+
path.join(flatDir, flatName(pkg.name, pkg.version)),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
yield* writeBundleMetadata({
|
|
501
|
+
dir: flatDir,
|
|
502
|
+
resolution: input.resolution,
|
|
503
|
+
included: input.resolution.packages,
|
|
504
|
+
options: input.options,
|
|
505
|
+
registryInfo: input.registryInfo,
|
|
506
|
+
sizes: input.report.sizes,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
yield* fs.copy(flatDir, input.options.outDir, { overwrite: true }).pipe(
|
|
510
|
+
Effect.mapError(
|
|
511
|
+
(cause) =>
|
|
512
|
+
new OutputError(input.options.outDir, "could not write output tree", {
|
|
513
|
+
cause,
|
|
514
|
+
}),
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// One artifact per file, not one for the directory.
|
|
519
|
+
//
|
|
520
|
+
// `dir` reports its tree as a single artifact because the tree is the
|
|
521
|
+
// unit: the paths are what make it importable, so half of it is worth
|
|
522
|
+
// nothing. Flat has no paths to preserve and every file stands on its
|
|
523
|
+
// own — `npm publish` takes them one at a time, in any order. Reporting
|
|
524
|
+
// one artifact there contradicted the directory it had just written, and
|
|
525
|
+
// the run said "1 artifact" over 56 files.
|
|
526
|
+
//
|
|
527
|
+
// The manifest and the import guide are counted too: under this layout
|
|
528
|
+
// they are loose files the run produced, and a count that omitted them
|
|
529
|
+
// would not match what `ls` shows.
|
|
530
|
+
const tarballs: ReadonlyArray<BundleArtifact> = input.resolution.packages.map((pkg) => ({
|
|
531
|
+
kind: ArtifactKind.Archive,
|
|
532
|
+
path: path.join(input.options.outDir, flatName(pkg.name, pkg.version)),
|
|
533
|
+
bytes: input.report.sizes.get(`${pkg.name}@${pkg.version}`) ?? 0,
|
|
534
|
+
packageCount: 1,
|
|
535
|
+
}));
|
|
536
|
+
|
|
537
|
+
const summaries = yield* Effect.forEach([MANIFEST_FILE, README_FILE], (file) =>
|
|
538
|
+
Effect.gen(function* () {
|
|
539
|
+
const info = yield* fs.stat(path.join(flatDir, file));
|
|
540
|
+
return {
|
|
541
|
+
kind: ArtifactKind.File,
|
|
542
|
+
path: path.join(input.options.outDir, file),
|
|
543
|
+
bytes: Number(info.size),
|
|
544
|
+
packageCount: 0,
|
|
545
|
+
} satisfies BundleArtifact;
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
return [...tarballs, ...summaries];
|
|
550
|
+
});
|
|
551
|
+
|
|
463
552
|
/**
|
|
464
553
|
* One tarball per resolved root *version*.
|
|
465
554
|
*
|
|
@@ -597,6 +686,7 @@ const writeBundleMetadata = (input: {
|
|
|
597
686
|
packageCount: input.included.length,
|
|
598
687
|
createdAt: createdAt.toISOString(),
|
|
599
688
|
toolVersion: input.options.toolVersion,
|
|
689
|
+
layout: input.options.layout,
|
|
600
690
|
}),
|
|
601
691
|
);
|
|
602
692
|
});
|
|
@@ -611,6 +701,48 @@ const topLevelEntries = (
|
|
|
611
701
|
return entries.toSorted();
|
|
612
702
|
});
|
|
613
703
|
|
|
704
|
+
/**
|
|
705
|
+
* `assertDistinctOutputs`, over package tarballs instead of archives.
|
|
706
|
+
*
|
|
707
|
+
* `flat` is the only layout with nothing but a file name to tell two packages
|
|
708
|
+
* apart — everywhere else a tarball keeps its unique `name/-/` directory. What
|
|
709
|
+
* is left to catch is the pathological pair, `@a/b-c` against `@a-b/c`; the
|
|
710
|
+
* plausible one is impossible by construction, which is what `flatName` buys.
|
|
711
|
+
*/
|
|
712
|
+
const assertDistinctFlatNames = (
|
|
713
|
+
resolution: Resolution,
|
|
714
|
+
options: BundleContext,
|
|
715
|
+
): Effect.Effect<void, OutputError> =>
|
|
716
|
+
Effect.gen(function* () {
|
|
717
|
+
if (options.layout !== Layout.Flat) return;
|
|
718
|
+
|
|
719
|
+
const owners = new Map<string, Array<string>>();
|
|
720
|
+
for (const pkg of resolution.packages) {
|
|
721
|
+
const file = flatName(pkg.name, pkg.version);
|
|
722
|
+
const claimed = owners.get(file);
|
|
723
|
+
|
|
724
|
+
if (claimed === undefined) owners.set(file, [`${pkg.name}@${pkg.version}`]);
|
|
725
|
+
else claimed.push(`${pkg.name}@${pkg.version}`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const collisions = [...owners].filter(([, packages]) => packages.length > 1);
|
|
729
|
+
if (collisions.length === 0) return;
|
|
730
|
+
|
|
731
|
+
const described = collisions
|
|
732
|
+
.map(([file, packages]) => `${packages.join(" and ")} would both be ${file}`)
|
|
733
|
+
.join("; ");
|
|
734
|
+
|
|
735
|
+
return yield* Effect.fail(
|
|
736
|
+
new OutputError(
|
|
737
|
+
options.outDir,
|
|
738
|
+
`two packages want the same file name — ${described}. \`flat\` layout has ` +
|
|
739
|
+
`only the file name to tell them apart, and the \`/\` in a scope becomes ` +
|
|
740
|
+
`a \`-\` to make one. Use --layout dir, which keeps each package in its ` +
|
|
741
|
+
`own directory.`,
|
|
742
|
+
),
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
|
|
614
746
|
/**
|
|
615
747
|
* Two different packages must not want the same file name.
|
|
616
748
|
*
|
|
@@ -701,6 +833,7 @@ const prepareOutputDir = (
|
|
|
701
833
|
const outDir = options.outDir;
|
|
702
834
|
|
|
703
835
|
yield* assertDistinctOutputs(resolution, options);
|
|
836
|
+
yield* assertDistinctFlatNames(resolution, options);
|
|
704
837
|
|
|
705
838
|
if (!options.force) {
|
|
706
839
|
const clashes: Array<string> = [];
|
|
@@ -4,6 +4,12 @@ import type { ValueOf } from "../types/value-of.js";
|
|
|
4
4
|
export const ArtifactKind = {
|
|
5
5
|
Archive: "archive",
|
|
6
6
|
Directory: "directory",
|
|
7
|
+
/**
|
|
8
|
+
* A loose file that is not an archive — the manifest and the import guide,
|
|
9
|
+
* which `dir` and `flat` write beside the tarballs rather than sealing
|
|
10
|
+
* inside one.
|
|
11
|
+
*/
|
|
12
|
+
File: "file",
|
|
7
13
|
} as const;
|
|
8
14
|
|
|
9
15
|
export type ArtifactKind = ValueOf<typeof ArtifactKind>;
|
package/src/enums/layout.ts
CHANGED
|
@@ -18,6 +18,12 @@ export const Layout = {
|
|
|
18
18
|
* `jf rt upload` or rsync.
|
|
19
19
|
*/
|
|
20
20
|
Dir: "dir",
|
|
21
|
+
/**
|
|
22
|
+
* Every tarball side by side in one directory — no archive, and no
|
|
23
|
+
* `name/-/` nesting. For tooling that globs `*.tgz` out of one directory
|
|
24
|
+
* rather than walking a registry tree.
|
|
25
|
+
*/
|
|
26
|
+
Flat: "flat",
|
|
21
27
|
} as const;
|
|
22
28
|
|
|
23
29
|
export type Layout = ValueOf<typeof Layout>;
|
package/src/layout.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The on-disk shape of a bundle.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Under every layout but `flat`, everything is laid out exactly the way a
|
|
5
|
+
* registry serves it:
|
|
5
6
|
*
|
|
6
7
|
* lodash/-/lodash-4.17.21.tgz
|
|
7
8
|
* @babel/core/-/core-7.24.0.tgz
|
|
@@ -13,8 +14,12 @@
|
|
|
13
14
|
* The one shape worth noticing before editing anything here: a scoped
|
|
14
15
|
* package's file name drops the scope, so `@babel/core` becomes
|
|
15
16
|
* `core-7.24.0.tgz` under an `@babel/core/-/` directory.
|
|
17
|
+
*
|
|
18
|
+
* `flat` layout is the exception; `bundlePath` is where the two part company.
|
|
16
19
|
*/
|
|
17
20
|
|
|
21
|
+
import { Layout } from "./enums/layout.js";
|
|
22
|
+
|
|
18
23
|
/** Splits `@scope/name` into its parts. `scope` is undefined when unscoped. */
|
|
19
24
|
export const splitName = (
|
|
20
25
|
name: string,
|
|
@@ -46,7 +51,7 @@ export const MANIFEST_FILE = "bundle-manifest.json";
|
|
|
46
51
|
export const README_FILE = "IMPORT.md";
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
|
-
*
|
|
54
|
+
* A flat, collision-free file name for one `name@version`.
|
|
50
55
|
*
|
|
51
56
|
* It is the spec, with the one character a file name cannot hold taken out:
|
|
52
57
|
* `@types/react@19.2.18` becomes `@types-react@19.2.18.tgz`. So the name of the
|
|
@@ -76,34 +81,72 @@ export const README_FILE = "IMPORT.md";
|
|
|
76
81
|
* has to cross-reference against a manifest.
|
|
77
82
|
*
|
|
78
83
|
* It does not make every collision impossible — `@a/b-c` and `@a-b/c` still
|
|
79
|
-
* meet — so
|
|
80
|
-
* scoped-against-unscoped one is `@babel/core` beside
|
|
81
|
-
* real migration somebody could be bundling.
|
|
84
|
+
* meet — so the run still checks before downloading anything. That pair is
|
|
85
|
+
* pathological; the scoped-against-unscoped one is `@babel/core` beside
|
|
86
|
+
* `babel-core`, which is a real migration somebody could be bundling.
|
|
82
87
|
*
|
|
83
88
|
* The leading `@` is safe here in a way it is not inside the archive: tar's
|
|
84
89
|
* splice convention applies to the *file list* it is given, which is what
|
|
85
|
-
* `archive.ts` guards with `./`.
|
|
90
|
+
* `archive.ts` guards with `./`. These are output paths, not entries.
|
|
86
91
|
*/
|
|
87
|
-
export const
|
|
92
|
+
export const flatName = (name: string, version: string): string => {
|
|
88
93
|
const { scope, bare } = splitName(name);
|
|
89
94
|
const prefix = scope === undefined ? bare : `${scope}-${bare}`;
|
|
90
95
|
return `${prefix}@${version}.tgz`;
|
|
91
96
|
};
|
|
92
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Name of the archive produced for one root spec in `per-spec` layout.
|
|
100
|
+
*
|
|
101
|
+
* The same rule as {@link flatName}, which is where the reasoning lives — a
|
|
102
|
+
* per-spec archive and a `flat` package tarball are both "one file in a
|
|
103
|
+
* directory, named for what is in it", and there is no reason for a directory
|
|
104
|
+
* of bundles and a directory of packages to be read differently.
|
|
105
|
+
*/
|
|
106
|
+
export const perSpecArchiveName = flatName;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The path of one package tarball within a bundle. The one place the two
|
|
110
|
+
* shapes are chosen between, so a manifest and the tree it describes cannot
|
|
111
|
+
* drift apart.
|
|
112
|
+
*
|
|
113
|
+
* `flat` keeps a file name and no directories, and uses {@link flatName}
|
|
114
|
+
* rather than the registry file name: with the directories gone that name is
|
|
115
|
+
* all there is to tell two packages apart, and the registry one drops the
|
|
116
|
+
* scope, which would put `@types/node` and `node` on the same file. The name
|
|
117
|
+
* is free to differ — `npm publish <tarball>` reads a package's name and
|
|
118
|
+
* version from the `package.json` inside it, not from what the file is
|
|
119
|
+
* called.
|
|
120
|
+
*/
|
|
121
|
+
export const bundlePath = (layout: Layout, name: string, version: string): string =>
|
|
122
|
+
layout === Layout.Flat ? flatName(name, version) : packagePath(name, version);
|
|
123
|
+
|
|
93
124
|
/** Name of the archive produced in `single` layout. */
|
|
94
125
|
export const singleArchiveName = (base = "bundle"): string => `${base}.tgz`;
|
|
95
126
|
|
|
127
|
+
/** Shared by both guides. */
|
|
128
|
+
type GuideOptions = {
|
|
129
|
+
readonly packageCount: number;
|
|
130
|
+
readonly createdAt: string;
|
|
131
|
+
readonly toolVersion: string;
|
|
132
|
+
readonly layout: Layout;
|
|
133
|
+
};
|
|
134
|
+
|
|
96
135
|
/**
|
|
97
136
|
* The import guide written into each bundle.
|
|
98
137
|
*
|
|
99
138
|
* Kept short and copy-pasteable on purpose: whoever opens this is mid-task on a
|
|
100
139
|
* restricted network and does not want prose.
|
|
140
|
+
*
|
|
141
|
+
* Layout-aware, because the instructions genuinely differ — a registry-layout
|
|
142
|
+
* tree uploads into Artifactory as it stands and a flat directory does not, and
|
|
143
|
+
* a guide that said otherwise would be discovered to be wrong by somebody who
|
|
144
|
+
* can no longer look anything up.
|
|
101
145
|
*/
|
|
102
|
-
export const importGuide = (options:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}): string =>
|
|
146
|
+
export const importGuide = (options: GuideOptions): string =>
|
|
147
|
+
options.layout === Layout.Flat ? flatGuide(options) : treeGuide(options);
|
|
148
|
+
|
|
149
|
+
const treeGuide = (options: GuideOptions): string =>
|
|
107
150
|
`# Importing this bundle
|
|
108
151
|
|
|
109
152
|
${options.packageCount} package tarball(s), laid out exactly as an npm registry serves them:
|
|
@@ -135,3 +178,31 @@ Publish each tarball individually:
|
|
|
135
178
|
Every entry in ${MANIFEST_FILE} carries the integrity string the source registry
|
|
136
179
|
advertised, and each tarball was checked against it at download time.
|
|
137
180
|
`;
|
|
181
|
+
|
|
182
|
+
const flatGuide = (options: GuideOptions): string =>
|
|
183
|
+
`# Importing this bundle
|
|
184
|
+
|
|
185
|
+
${options.packageCount} package tarball(s), side by side in this one directory:
|
|
186
|
+
|
|
187
|
+
<name>@<version>.tgz
|
|
188
|
+
@<scope>-<name>@<version>.tgz
|
|
189
|
+
|
|
190
|
+
Created ${options.createdAt} by packall ${options.toolVersion}.
|
|
191
|
+
See ${MANIFEST_FILE} for the full list with checksums.
|
|
192
|
+
|
|
193
|
+
## Publishing into a registry
|
|
194
|
+
|
|
195
|
+
\`npm publish\` reads each package's name and version from the \`package.json\`
|
|
196
|
+
inside its tarball, so neither the file names nor the order matter:
|
|
197
|
+
|
|
198
|
+
for f in *.tgz; do npm publish --registry <url> "$f"; done
|
|
199
|
+
|
|
200
|
+
For a tree that uploads into Artifactory as it stands, use --layout dir instead.
|
|
201
|
+
|
|
202
|
+
## Verifying before import
|
|
203
|
+
|
|
204
|
+
node -e "const m=require('./${MANIFEST_FILE}');console.log(m.packages.length+' packages')"
|
|
205
|
+
|
|
206
|
+
Every entry in ${MANIFEST_FILE} carries the integrity string the source registry
|
|
207
|
+
advertised, and each tarball was checked against it at download time.
|
|
208
|
+
`;
|
package/src/manifest.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The manifest written into every bundle — the audit trail, answerable without
|
|
3
3
|
* unpacking a single tarball. See `notes/requirements.md` § FR-10.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { bundlePath } from "./layout.js";
|
|
6
6
|
import type { BundleOptions } from "./options.js";
|
|
7
7
|
import { formatPlatformFilter } from "./platform.js";
|
|
8
8
|
import type { Resolution, ResolvedPackage } from "./resolve.js";
|
|
@@ -82,7 +82,7 @@ export const buildManifest = (input: {
|
|
|
82
82
|
return {
|
|
83
83
|
name: pkg.name,
|
|
84
84
|
version: pkg.version,
|
|
85
|
-
path:
|
|
85
|
+
path: bundlePath(input.options.layout, pkg.name, pkg.version),
|
|
86
86
|
tarball: pkg.manifest.dist.tarball,
|
|
87
87
|
integrity: pkg.manifest.dist.integrity,
|
|
88
88
|
shasum: pkg.manifest.dist.shasum,
|