isolate-package 1.33.0 → 1.35.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 (68) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/index.mjs.map +1 -1
  3. package/dist/isolate-bin.mjs +5 -6
  4. package/dist/isolate-bin.mjs.map +1 -1
  5. package/dist/{isolate-DyRD5Zd_.mjs → isolate-ts-Igq7C.mjs} +888 -271
  6. package/dist/isolate-ts-Igq7C.mjs.map +1 -0
  7. package/package.json +23 -19
  8. package/src/get-internal-package-names.test.ts +1 -1
  9. package/src/get-internal-package-names.ts +2 -2
  10. package/src/isolate-bin.ts +5 -5
  11. package/src/isolate.ts +22 -17
  12. package/src/lib/config.test.ts +1 -1
  13. package/src/lib/config.ts +3 -3
  14. package/src/lib/lockfile/helpers/bun-lockfile.ts +153 -0
  15. package/src/lib/lockfile/helpers/generate-bun-lockfile.test.ts +3 -3
  16. package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +14 -146
  17. package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +1 -5
  18. package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +311 -16
  19. package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +193 -22
  20. package/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +83 -2
  21. package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +33 -6
  22. package/src/lib/lockfile/helpers/generate-yarn-lockfile.ts +5 -5
  23. package/src/lib/lockfile/process-lockfile.test.ts +2 -2
  24. package/src/lib/manifest/adapt-target-package-manifest.ts +22 -13
  25. package/src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts +72 -3
  26. package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +22 -12
  27. package/src/lib/manifest/helpers/adapt-manifest-internal-deps.ts +1 -1
  28. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +4 -4
  29. package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +7 -7
  30. package/src/lib/manifest/helpers/resolve-catalog-dependencies.test.ts +410 -0
  31. package/src/lib/manifest/helpers/resolve-catalog-dependencies.ts +115 -27
  32. package/src/lib/manifest/io.ts +6 -2
  33. package/src/lib/manifest/validate-manifest.ts +2 -2
  34. package/src/lib/output/get-build-output-dir.ts +1 -1
  35. package/src/lib/output/pack-dependencies.ts +1 -1
  36. package/src/lib/output/process-build-output-files.ts +6 -17
  37. package/src/lib/package-manager/helpers/infer-from-files.ts +5 -5
  38. package/src/lib/package-manager/helpers/infer-from-manifest.ts +7 -8
  39. package/src/lib/package-manager/index.ts +1 -1
  40. package/src/lib/package-manager/names.ts +8 -10
  41. package/src/lib/patches/collect-installed-names-bun.test.ts +154 -0
  42. package/src/lib/patches/collect-installed-names-bun.ts +87 -0
  43. package/src/lib/patches/collect-installed-names-pnpm.test.ts +316 -0
  44. package/src/lib/patches/collect-installed-names-pnpm.ts +365 -0
  45. package/src/lib/patches/copy-patches.test.ts +130 -13
  46. package/src/lib/patches/copy-patches.ts +47 -10
  47. package/src/lib/patches/write-isolate-pnpm-workspace.test.ts +83 -3
  48. package/src/lib/patches/write-isolate-pnpm-workspace.ts +4 -4
  49. package/src/lib/registry/collect-reachable-package-names.test.ts +1 -1
  50. package/src/lib/registry/create-packages-registry.ts +34 -31
  51. package/src/lib/registry/helpers/find-packages-globs.ts +23 -19
  52. package/src/lib/registry/list-internal-packages.test.ts +2 -2
  53. package/src/lib/types.ts +2 -2
  54. package/src/lib/utils/filter-patched-dependencies.test.ts +1 -1
  55. package/src/lib/utils/filter-patched-dependencies.ts +2 -2
  56. package/src/lib/utils/get-dirname.ts +1 -1
  57. package/src/lib/utils/index.ts +1 -1
  58. package/src/lib/utils/json.ts +12 -14
  59. package/src/lib/utils/pack.ts +32 -22
  60. package/src/lib/utils/reset-isolate-dir.test.ts +165 -0
  61. package/src/lib/utils/reset-isolate-dir.ts +147 -0
  62. package/src/lib/utils/unpack.test.ts +76 -0
  63. package/src/lib/utils/unpack.ts +16 -10
  64. package/src/lib/utils/wait-for-complete-file.test.ts +105 -0
  65. package/src/lib/utils/wait-for-complete-file.ts +44 -0
  66. package/src/lib/utils/yaml.ts +8 -9
  67. package/src/testing/setup.ts +1 -1
  68. package/dist/isolate-DyRD5Zd_.mjs.map +0 -1
@@ -2,39 +2,45 @@ import fs from "fs-extra";
2
2
  import path from "node:path";
3
3
  import { readWantedLockfile as readWantedLockfile_v8 } from "pnpm_lockfile_file_v8";
4
4
  import { readWantedLockfile as readWantedLockfile_v9 } from "pnpm_lockfile_file_v9";
5
- import { useLogger } from "~/lib/logger";
6
- import { usePackageManager } from "~/lib/package-manager";
7
- import { collectReachablePackageNames } from "~/lib/registry";
5
+ import { useLogger } from "#/lib/logger";
6
+ import { usePackageManager } from "#/lib/package-manager";
7
+ import { collectReachablePackageNames } from "#/lib/registry";
8
8
  import type {
9
9
  PackageManifest,
10
10
  PackagesRegistry,
11
11
  PatchFile,
12
12
  PnpmSettings,
13
- } from "~/lib/types";
13
+ } from "#/lib/types";
14
14
  import {
15
15
  filterPatchedDependencies,
16
16
  getRootRelativeLogPath,
17
17
  isRushWorkspace,
18
18
  readTypedJson,
19
19
  readTypedYamlSync,
20
- } from "~/lib/utils";
20
+ } from "#/lib/utils";
21
+ import { collectInstalledNamesFromBunLockfile } from "./collect-installed-names-bun";
22
+ import { collectInstalledNamesFromPnpmLockfile } from "./collect-installed-names-pnpm";
21
23
 
22
24
  export async function copyPatches({
23
25
  workspaceRootDir,
26
+ targetPackageDir,
24
27
  targetPackageManifest,
25
28
  packagesRegistry,
29
+ internalDepPackageNames,
26
30
  isolateDir,
27
31
  includeDevDependencies,
28
32
  }: {
29
33
  workspaceRootDir: string;
34
+ targetPackageDir: string;
30
35
  targetPackageManifest: PackageManifest;
31
36
  packagesRegistry: PackagesRegistry;
37
+ internalDepPackageNames: string[];
32
38
  isolateDir: string;
33
39
  includeDevDependencies: boolean;
34
40
  }): Promise<Record<string, PatchFile>> {
35
41
  const log = useLogger();
36
42
 
37
- const { name: packageManagerName } = usePackageManager();
43
+ const { name: packageManagerName, majorVersion } = usePackageManager();
38
44
 
39
45
  let patchedDependencies: Record<string, string> | undefined;
40
46
 
@@ -44,9 +50,9 @@ export async function copyPatches({
44
50
  */
45
51
  if (packageManagerName === "pnpm") {
46
52
  try {
47
- const pnpmSettings = readTypedYamlSync<PnpmSettings>(
53
+ const pnpmSettings = readTypedYamlSync(
48
54
  path.join(workspaceRootDir, "pnpm-workspace.yaml"),
49
- );
55
+ ) as PnpmSettings | undefined;
50
56
  patchedDependencies = pnpmSettings?.patchedDependencies;
51
57
  } catch (error) {
52
58
  log.warn(
@@ -67,9 +73,9 @@ export async function copyPatches({
67
73
  }
68
74
 
69
75
  try {
70
- const workspaceRootManifest = await readTypedJson<PackageManifest>(
76
+ const workspaceRootManifest = (await readTypedJson(
71
77
  path.join(workspaceRootDir, "package.json"),
72
- );
78
+ )) as PackageManifest;
73
79
  /** PNPM stores patches under pnpm.patchedDependencies, Bun at the top level */
74
80
  patchedDependencies =
75
81
  workspaceRootManifest?.pnpm?.patchedDependencies ??
@@ -102,6 +108,37 @@ export async function copyPatches({
102
108
  includeDevDependencies,
103
109
  });
104
110
 
111
+ /**
112
+ * Manifest-based reachability misses external→external transitives because
113
+ * external manifests aren't loaded here. Walk the package-manager's
114
+ * lockfile to also pick up those names, so a patch for a deeply-nested
115
+ * external dep (e.g. `@react-pdf/render` reached via `@react-pdf/renderer`)
116
+ * survives isolation.
117
+ */
118
+ const lockfileInstalledNames =
119
+ packageManagerName === "pnpm"
120
+ ? await collectInstalledNamesFromPnpmLockfile({
121
+ workspaceRootDir,
122
+ targetPackageDir,
123
+ internalDepPackageNames,
124
+ packagesRegistry,
125
+ majorVersion,
126
+ includeDevDependencies,
127
+ })
128
+ : packageManagerName === "bun"
129
+ ? collectInstalledNamesFromBunLockfile({
130
+ workspaceRootDir,
131
+ targetPackageDir,
132
+ internalDepPackageNames,
133
+ packagesRegistry,
134
+ includeDevDependencies,
135
+ })
136
+ : new Set<string>();
137
+
138
+ for (const name of lockfileInstalledNames) {
139
+ reachableDependencyNames.add(name);
140
+ }
141
+
105
142
  const filteredPatches = filterPatchedDependencies({
106
143
  patchedDependencies,
107
144
  targetPackageManifest,
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import type { PatchFile } from "~/lib/types";
2
+ import type { PatchFile } from "#/lib/types";
3
3
  import { writeIsolatePnpmWorkspace } from "./write-isolate-pnpm-workspace";
4
4
 
5
5
  vi.mock("fs-extra", () => ({
@@ -8,14 +8,14 @@ vi.mock("fs-extra", () => ({
8
8
  },
9
9
  }));
10
10
 
11
- vi.mock("~/lib/utils", () => ({
11
+ vi.mock("#/lib/utils", () => ({
12
12
  readTypedYamlSync: vi.fn(),
13
13
  writeTypedYamlSync: vi.fn(),
14
14
  }));
15
15
 
16
16
  const fs = vi.mocked((await import("fs-extra")).default);
17
17
  const { readTypedYamlSync, writeTypedYamlSync } = vi.mocked(
18
- await import("~/lib/utils"),
18
+ await import("#/lib/utils"),
19
19
  );
20
20
 
21
21
  const workspaceRootDir = "/workspace";
@@ -139,6 +139,86 @@ describe("writeIsolatePnpmWorkspace", () => {
139
139
  );
140
140
  });
141
141
 
142
+ /**
143
+ * Regression test for issue #189: pnpm 11 expresses the build-script policy
144
+ * via `allowBuilds` in pnpm-workspace.yaml (and removes the older
145
+ * `pnpm.onlyBuiltDependencies` / `ignoredBuiltDependencies` fields from
146
+ * package.json). The verbatim copy must carry that field — along with other
147
+ * workspace-level settings like `minimumReleaseAge` — into the isolate
148
+ * output so downstream `pnpm install` honors the same policy.
149
+ */
150
+ it("preserves pnpm 11 workspace settings (allowBuilds, minimumReleaseAge) when no patches are involved", () => {
151
+ readTypedYamlSync.mockReturnValue({
152
+ packages: ["apps/*", "packages/*"],
153
+ allowBuilds: {
154
+ puppeteer: true,
155
+ esbuild: true,
156
+ },
157
+ minimumReleaseAge: 10_080,
158
+ });
159
+
160
+ writeIsolatePnpmWorkspace({
161
+ workspaceRootDir,
162
+ isolateDir,
163
+ copiedPatches: {},
164
+ });
165
+
166
+ /**
167
+ * With no patchedDependencies in the source yaml, the file is copied
168
+ * verbatim — preserving `allowBuilds` and any other top-level settings.
169
+ */
170
+ expect(writeTypedYamlSync).not.toHaveBeenCalled();
171
+ expect(fs.copyFileSync).toHaveBeenCalledWith(
172
+ "/workspace/pnpm-workspace.yaml",
173
+ "/workspace/isolate/pnpm-workspace.yaml",
174
+ );
175
+ });
176
+
177
+ /**
178
+ * When patches are being filtered, the rewrite path must still carry
179
+ * `allowBuilds` into the output yaml — otherwise pnpm 11's build-script
180
+ * policy is silently dropped.
181
+ */
182
+ it("preserves allowBuilds when rewriting to filter patchedDependencies", () => {
183
+ readTypedYamlSync.mockReturnValue({
184
+ packages: ["apps/*", "packages/*"],
185
+ allowBuilds: {
186
+ puppeteer: true,
187
+ },
188
+ patchedDependencies: {
189
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
190
+ "axios@1.6.0": "patches/axios@1.6.0.patch",
191
+ },
192
+ });
193
+
194
+ const copiedPatches: Record<string, PatchFile> = {
195
+ "lodash@4.17.21": {
196
+ path: "patches/lodash@4.17.21.patch",
197
+ hash: "abc",
198
+ },
199
+ };
200
+
201
+ writeIsolatePnpmWorkspace({
202
+ workspaceRootDir,
203
+ isolateDir,
204
+ copiedPatches,
205
+ });
206
+
207
+ expect(fs.copyFileSync).not.toHaveBeenCalled();
208
+ expect(writeTypedYamlSync).toHaveBeenCalledWith(
209
+ "/workspace/isolate/pnpm-workspace.yaml",
210
+ {
211
+ packages: ["apps/*", "packages/*"],
212
+ allowBuilds: {
213
+ puppeteer: true,
214
+ },
215
+ patchedDependencies: {
216
+ "lodash@4.17.21": "patches/lodash@4.17.21.patch",
217
+ },
218
+ },
219
+ );
220
+ });
221
+
142
222
  it("copies verbatim when every patch is kept (preserving comments and order)", () => {
143
223
  readTypedYamlSync.mockReturnValue({
144
224
  packages: ["packages/*"],
@@ -1,8 +1,8 @@
1
1
  import fs from "fs-extra";
2
2
  import path from "node:path";
3
- import { useLogger } from "~/lib/logger";
4
- import type { PatchFile, PnpmSettings } from "~/lib/types";
5
- import { readTypedYamlSync, writeTypedYamlSync } from "~/lib/utils";
3
+ import { useLogger } from "#/lib/logger";
4
+ import type { PatchFile, PnpmSettings } from "#/lib/types";
5
+ import { readTypedYamlSync, writeTypedYamlSync } from "#/lib/utils";
6
6
 
7
7
  /**
8
8
  * Copy `pnpm-workspace.yaml` from the workspace root to the isolate directory,
@@ -39,7 +39,7 @@ export function writeIsolatePnpmWorkspace({
39
39
  let settings: PnpmSettings | undefined;
40
40
 
41
41
  try {
42
- settings = readTypedYamlSync<PnpmSettings>(sourcePath);
42
+ settings = readTypedYamlSync(sourcePath) as PnpmSettings | undefined;
43
43
  } catch (error) {
44
44
  log.warn(
45
45
  `Could not read pnpm-workspace.yaml, falling back to verbatim copy: ${error instanceof Error ? error.message : String(error)}`,
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { PackageManifest, PackagesRegistry } from "~/lib/types";
2
+ import type { PackageManifest, PackagesRegistry } from "#/lib/types";
3
3
  import { collectReachablePackageNames } from "./collect-reachable-package-names";
4
4
 
5
5
  function entry(manifest: PackageManifest) {
@@ -28,38 +28,41 @@ export async function createPackagesRegistry(
28
28
  workspaceRootDir,
29
29
  );
30
30
 
31
- const registry: PackagesRegistry = (
32
- await Promise.all(
33
- allPackages.map(async (rootRelativeDir) => {
34
- const absoluteDir = path.join(workspaceRootDir, rootRelativeDir);
35
- const manifestPath = path.join(absoluteDir, "package.json");
31
+ const entries = await Promise.all(
32
+ allPackages.map(async (rootRelativeDir) => {
33
+ const absoluteDir = path.join(workspaceRootDir, rootRelativeDir);
34
+ const manifestPath = path.join(absoluteDir, "package.json");
36
35
 
37
- if (!fs.existsSync(manifestPath)) {
38
- log.warn(
39
- `Ignoring directory ${rootRelativeDir} because it does not contain a package.json file`,
40
- );
41
- return;
42
- } else {
43
- log.debug(`Registering package ${rootRelativeDir}`);
36
+ if (!fs.existsSync(manifestPath)) {
37
+ log.warn(
38
+ `Ignoring directory ${rootRelativeDir} because it does not contain a package.json file`,
39
+ );
40
+ return null;
41
+ }
44
42
 
45
- const manifest = await readTypedJson<PackageManifest>(
46
- path.join(absoluteDir, "package.json"),
47
- );
43
+ log.debug(`Registering package ${rootRelativeDir}`);
48
44
 
49
- return {
50
- manifest,
51
- rootRelativeDir,
52
- absoluteDir,
53
- };
54
- }
55
- }),
56
- )
57
- ).reduce<PackagesRegistry>((acc, info) => {
58
- if (info) {
59
- acc[info.manifest.name] = info;
60
- }
61
- return acc;
62
- }, {});
45
+ const manifest = (await readTypedJson(
46
+ path.join(absoluteDir, "package.json"),
47
+ )) as PackageManifest;
48
+
49
+ return {
50
+ manifest,
51
+ rootRelativeDir,
52
+ absoluteDir,
53
+ };
54
+ }),
55
+ );
56
+
57
+ const registry: PackagesRegistry = entries.reduce<PackagesRegistry>(
58
+ (acc, info) => {
59
+ if (info) {
60
+ acc[info.manifest.name] = info;
61
+ }
62
+ return acc;
63
+ },
64
+ {},
65
+ );
63
66
 
64
67
  return registry;
65
68
  }
@@ -73,9 +76,9 @@ function listWorkspacePackages(
73
76
  workspaceRootDir: string,
74
77
  ) {
75
78
  if (isRushWorkspace(workspaceRootDir)) {
76
- const rushConfig = readTypedJsonSync<RushConfig>(
79
+ const rushConfig = readTypedJsonSync(
77
80
  path.join(workspaceRootDir, "rush.json"),
78
- );
81
+ ) as RushConfig;
79
82
 
80
83
  return rushConfig.projects.map(({ projectFolder }) => projectFolder);
81
84
  } else {
@@ -13,16 +13,16 @@ import {
13
13
  * monorepo. This configuration is dependent on the package manager used, and I
14
14
  * don't know if we're covering all cases yet...
15
15
  */
16
- export function findPackagesGlobs(workspaceRootDir: string) {
16
+ export function findPackagesGlobs(workspaceRootDir: string): string[] {
17
17
  const log = useLogger();
18
18
 
19
19
  const packageManager = usePackageManager();
20
20
 
21
21
  switch (packageManager.name) {
22
22
  case "pnpm": {
23
- const workspaceConfig = readTypedYamlSync<{ packages: string[] }>(
23
+ const workspaceConfig = readTypedYamlSync(
24
24
  path.join(workspaceRootDir, "pnpm-workspace.yaml"),
25
- );
25
+ ) as { packages: string[] } | undefined;
26
26
 
27
27
  if (!workspaceConfig) {
28
28
  throw new Error(
@@ -48,9 +48,9 @@ export function findPackagesGlobs(workspaceRootDir: string) {
48
48
  "package.json",
49
49
  );
50
50
 
51
- const { workspaces } = readTypedJsonSync<{ workspaces: string[] }>(
52
- workspaceRootManifestPath,
53
- );
51
+ const { workspaces } = readTypedJsonSync(workspaceRootManifestPath) as {
52
+ workspaces: string[];
53
+ };
54
54
 
55
55
  if (!workspaces) {
56
56
  throw new Error(
@@ -60,21 +60,25 @@ export function findPackagesGlobs(workspaceRootDir: string) {
60
60
 
61
61
  if (Array.isArray(workspaces)) {
62
62
  return workspaces;
63
- } else {
64
- /**
65
- * For Yarn, workspaces could be defined as an object with { packages:
66
- * [], nohoist: [] }. See
67
- * https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
68
- */
69
- const workspacesObject = workspaces as { packages?: string[] };
63
+ }
70
64
 
71
- assert(
72
- workspacesObject.packages,
73
- "workspaces.packages must be an array",
74
- );
65
+ /**
66
+ * For Yarn, workspaces could be defined as an object with { packages: [],
67
+ * nohoist: [] }. See
68
+ * https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
69
+ */
70
+ const workspacesObject = workspaces as { packages?: string[] };
75
71
 
76
- return workspacesObject.packages;
77
- }
72
+ assert(
73
+ Array.isArray(workspacesObject.packages),
74
+ "workspaces.packages must be an array",
75
+ );
76
+
77
+ return workspacesObject.packages;
78
78
  }
79
+ default:
80
+ throw new Error(
81
+ `Unsupported package manager: ${packageManager.name as string}`,
82
+ );
79
83
  }
80
84
  }
@@ -1,10 +1,10 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
- import type { PackageManifest, PackagesRegistry } from "~/lib/types";
2
+ import type { PackageManifest, PackagesRegistry } from "#/lib/types";
3
3
  import { listInternalPackages } from "./list-internal-packages";
4
4
 
5
5
  const mockWarn = vi.fn();
6
6
 
7
- vi.mock("~/lib/logger", () => ({
7
+ vi.mock("#/lib/logger", () => ({
8
8
  useLogger: () => ({
9
9
  debug: vi.fn(),
10
10
  info: vi.fn(),
package/src/lib/types.ts CHANGED
@@ -6,10 +6,10 @@ export type { PnpmSettings } from "@pnpm/types";
6
6
  * Represents a patch file entry in the pnpm lockfile. Contains the path to the
7
7
  * patch file and its content hash.
8
8
  */
9
- export interface PatchFile {
9
+ export type PatchFile = {
10
10
  path: string;
11
11
  hash: string;
12
- }
12
+ };
13
13
 
14
14
  export type PackageManifest = PnpmPackageManifest & {
15
15
  packageManager?: string;
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { PackageManifest } from "~/lib/types";
2
+ import type { PackageManifest } from "#/lib/types";
3
3
  import { filterPatchedDependencies } from "./filter-patched-dependencies";
4
4
 
5
5
  describe("filterPatchedDependencies", () => {
@@ -1,5 +1,5 @@
1
- import { useLogger } from "~/lib/logger";
2
- import type { PackageManifest } from "~/lib/types";
1
+ import { useLogger } from "#/lib/logger";
2
+ import type { PackageManifest } from "#/lib/types";
3
3
  import { getPackageName } from "./get-package-name";
4
4
 
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { fileURLToPath } from "url";
1
+ import { fileURLToPath } from "node:url";
2
2
 
3
3
  /**
4
4
  * Calling context should pass in import.meta.url and the function will return
@@ -8,6 +8,6 @@ export * from "./is-present";
8
8
  export * from "./is-rush-workspace";
9
9
  export * from "./json";
10
10
  export * from "./log-paths";
11
- export * from "./pack";
11
+ export * from "./reset-isolate-dir";
12
12
  export * from "./unpack";
13
13
  export * from "./yaml";
@@ -3,32 +3,30 @@ import stripJsonComments from "strip-json-comments";
3
3
  import { getErrorMessage } from "./get-error-message";
4
4
 
5
5
  /** @todo Pass in zod schema and validate */
6
- export function readTypedJsonSync<T>(filePath: string) {
6
+ export function readTypedJsonSync(filePath: string): unknown {
7
7
  try {
8
8
  const rawContent = fs.readFileSync(filePath, "utf-8");
9
- const data = JSON.parse(
9
+ return JSON.parse(
10
10
  stripJsonComments(rawContent, { trailingCommas: true }),
11
- ) as T;
12
- return data;
13
- } catch (err) {
11
+ ) as unknown;
12
+ } catch (error) {
14
13
  throw new Error(
15
- `Failed to read JSON from ${filePath}: ${getErrorMessage(err)}`,
16
- { cause: err },
14
+ `Failed to read JSON from ${filePath}: ${getErrorMessage(error)}`,
15
+ { cause: error },
17
16
  );
18
17
  }
19
18
  }
20
19
 
21
- export async function readTypedJson<T>(filePath: string) {
20
+ export async function readTypedJson(filePath: string): Promise<unknown> {
22
21
  try {
23
22
  const rawContent = await fs.readFile(filePath, "utf-8");
24
- const data = JSON.parse(
23
+ return JSON.parse(
25
24
  stripJsonComments(rawContent, { trailingCommas: true }),
26
- ) as T;
27
- return data;
28
- } catch (err) {
25
+ ) as unknown;
26
+ } catch (error) {
29
27
  throw new Error(
30
- `Failed to read JSON from ${filePath}: ${getErrorMessage(err)}`,
31
- { cause: err },
28
+ `Failed to read JSON from ${filePath}: ${getErrorMessage(error)}`,
29
+ { cause: error },
32
30
  );
33
31
  }
34
32
  }
@@ -1,26 +1,31 @@
1
1
  import assert from "node:assert";
2
2
  import { exec } from "node:child_process";
3
- import fs from "node:fs";
4
3
  import path from "node:path";
5
4
  import { useLogger } from "../logger";
6
5
  import { shouldUsePnpmPack } from "../package-manager";
7
6
  import { getErrorMessage } from "./get-error-message";
7
+ import { waitForCompleteFile } from "./wait-for-complete-file";
8
+
9
+ /**
10
+ * How long to wait for the packed tarball to appear and stop growing on disk
11
+ * after `pnpm pack` / `npm pack` has exited.
12
+ */
13
+ const PACK_FILE_READY_TIMEOUT_MS = 5000;
14
+ const PACK_FILE_READY_POLL_MS = 50;
8
15
 
9
16
  export async function pack(srcDir: string, dstDir: string) {
10
17
  const log = useLogger();
11
18
 
12
19
  const execOptions = {
20
+ cwd: srcDir,
13
21
  maxBuffer: 10 * 1024 * 1024,
14
22
  };
15
23
 
16
- const previousCwd = process.cwd();
17
- process.chdir(srcDir);
18
-
19
24
  /**
20
25
  * PNPM pack seems to be a lot faster than NPM pack, so when PNPM is detected
21
26
  * we use that instead.
22
27
  */
23
- const stdout = shouldUsePnpmPack()
28
+ const packStdout = shouldUsePnpmPack()
24
29
  ? await new Promise<string>((resolve, reject) => {
25
30
  exec(
26
31
  `pnpm pack --pack-destination "${dstDir}"`,
@@ -28,7 +33,8 @@ export async function pack(srcDir: string, dstDir: string) {
28
33
  (err, stdout) => {
29
34
  if (err) {
30
35
  log.error(getErrorMessage(err));
31
- return reject(err);
36
+ reject(err);
37
+ return;
32
38
  }
33
39
 
34
40
  resolve(stdout);
@@ -41,7 +47,8 @@ export async function pack(srcDir: string, dstDir: string) {
41
47
  execOptions,
42
48
  (err, stdout) => {
43
49
  if (err) {
44
- return reject(err);
50
+ reject(err);
51
+ return;
45
52
  }
46
53
 
47
54
  resolve(stdout);
@@ -49,9 +56,12 @@ export async function pack(srcDir: string, dstDir: string) {
49
56
  );
50
57
  });
51
58
 
52
- const lastLine = stdout.trim().split("\n").at(-1);
59
+ const lastLine = packStdout.trim().split("\n").at(-1);
53
60
 
54
- assert(lastLine, `Failed to parse last line from stdout: ${stdout.trim()}`);
61
+ assert(
62
+ lastLine,
63
+ `Failed to parse last line from stdout: ${packStdout.trim()}`,
64
+ );
55
65
 
56
66
  const fileName = path.basename(lastLine);
57
67
 
@@ -59,20 +69,20 @@ export async function pack(srcDir: string, dstDir: string) {
59
69
 
60
70
  const filePath = path.join(dstDir, fileName);
61
71
 
62
- if (!fs.existsSync(filePath)) {
63
- log.error(
64
- `The response from pack could not be resolved to an existing file: ${filePath}`,
65
- );
66
- } else {
67
- log.debug(`Packed (temp)/${fileName}`);
68
- }
69
-
70
- process.chdir(previousCwd);
71
-
72
72
  /**
73
- * Return the path anyway even if it doesn't validate. A later stage will wait
74
- * for the file to occur still. Not sure if this makes sense. Maybe we should
75
- * stop at the validation error...
73
+ * `pnpm pack` (and occasionally `npm pack`) can return before the tarball is
74
+ * fully visible/flushed to disk. A naive `existsSync` check is not enough:
75
+ * the directory entry can appear before the file's data has been written,
76
+ * which causes downstream consumers (gunzip + tar) to fail with
77
+ * "unexpected end of file". Wait until the file exists and its size has
78
+ * stopped changing across two consecutive polls before returning.
76
79
  */
80
+ await waitForCompleteFile(filePath, {
81
+ timeoutMs: PACK_FILE_READY_TIMEOUT_MS,
82
+ pollMs: PACK_FILE_READY_POLL_MS,
83
+ });
84
+
85
+ log.debug(`Packed (temp)/${fileName}`);
86
+
77
87
  return filePath;
78
88
  }