@packall/core 0.3.2 → 0.4.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/dist/layout.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * The on-disk shape of a bundle.
3
3
  *
4
- * Everything is laid out exactly the way a registry serves it:
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
- * Name of the archive produced for one root spec in `per-spec` layout.
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 `assertDistinctOutputs` still runs. That pair is pathological; the
66
- * scoped-against-unscoped one is `@babel/core` beside `babel-core`, which is a
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 `./`. This is the output path, not an entry.
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 perSpecArchiveName: (name: string, version: string) => string;
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
- readonly packageCount: number;
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
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,+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,kBAAkB,SAAU,MAAM,WAAW,MAAM,KAAG,MAIlE,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,iBAAiB,qBAAsB,MAAuB,CAAC;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,WAAW,YAAa;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B,KAAG,MA+BH,CAAC"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@packall/core",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Registry-agnostic engine for bundling npm packages and their full dependency closure for offline import.",
5
5
  "homepage": "https://packall.vercel.app",
6
6
  "bugs": {
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. Absent for `single` and `dir`. */
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` layout is the odd one: it merges a package tree into `outDir`, and
103
- * package paths are `name/-/name-version.tgz` the same package at the same
104
- * version is the same bytes, so an overlap there is idempotent rather than
105
- * destructive. Only the two summary files are genuinely replaced, and those are
106
- * the ones worth guarding.
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) return [{ file: MANIFEST_FILE }, { file: README_FILE }];
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>;
@@ -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
- * Everything is laid out exactly the way a registry serves it:
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
- * Name of the archive produced for one root spec in `per-spec` layout.
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 `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.
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 `./`. This is the output path, not an entry.
90
+ * `archive.ts` guards with `./`. These are output paths, not entries.
86
91
  */
87
- export const perSpecArchiveName = (name: string, version: string): string => {
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
- readonly packageCount: number;
104
- readonly createdAt: string;
105
- readonly toolVersion: string;
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 { packagePath } from "./layout.js";
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: packagePath(pkg.name, pkg.version),
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,