isolate-package 1.33.0-0 → 1.34.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/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{isolate-DTwgcMAN.mjs → isolate-DI3eUTci.mjs} +576 -242
- package/dist/isolate-DI3eUTci.mjs.map +1 -0
- package/dist/isolate-bin.mjs +5 -6
- package/dist/isolate-bin.mjs.map +1 -1
- package/package.json +23 -19
- package/src/get-internal-package-names.test.ts +1 -1
- package/src/get-internal-package-names.ts +2 -2
- package/src/isolate-bin.ts +5 -5
- package/src/isolate.ts +20 -17
- package/src/lib/config.test.ts +1 -1
- package/src/lib/config.ts +3 -3
- package/src/lib/lockfile/helpers/bun-lockfile.ts +21 -11
- package/src/lib/lockfile/helpers/generate-bun-lockfile.test.ts +3 -3
- package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +7 -7
- package/src/lib/lockfile/helpers/generate-npm-lockfile.integration.test.ts +1 -5
- package/src/lib/lockfile/helpers/generate-npm-lockfile.test.ts +311 -16
- package/src/lib/lockfile/helpers/generate-npm-lockfile.ts +193 -22
- package/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +2 -2
- package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +6 -6
- package/src/lib/lockfile/helpers/generate-yarn-lockfile.ts +5 -5
- package/src/lib/lockfile/process-lockfile.test.ts +2 -2
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts +3 -3
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +2 -2
- package/src/lib/manifest/helpers/adapt-manifest-internal-deps.ts +1 -1
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +4 -4
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +7 -7
- package/src/lib/manifest/helpers/resolve-catalog-dependencies.test.ts +410 -0
- package/src/lib/manifest/helpers/resolve-catalog-dependencies.ts +115 -27
- package/src/lib/manifest/io.ts +6 -2
- package/src/lib/manifest/validate-manifest.ts +2 -2
- package/src/lib/output/get-build-output-dir.ts +1 -1
- package/src/lib/output/pack-dependencies.ts +1 -1
- package/src/lib/output/process-build-output-files.ts +6 -17
- package/src/lib/package-manager/helpers/infer-from-files.ts +5 -5
- package/src/lib/package-manager/helpers/infer-from-manifest.ts +7 -8
- package/src/lib/package-manager/index.ts +1 -1
- package/src/lib/package-manager/names.ts +8 -10
- package/src/lib/patches/collect-installed-names-bun.test.ts +2 -2
- package/src/lib/patches/collect-installed-names-bun.ts +8 -8
- package/src/lib/patches/collect-installed-names-pnpm.test.ts +1 -1
- package/src/lib/patches/collect-installed-names-pnpm.ts +13 -12
- package/src/lib/patches/copy-patches.test.ts +5 -13
- package/src/lib/patches/copy-patches.ts +9 -9
- package/src/lib/patches/write-isolate-pnpm-workspace.test.ts +83 -3
- package/src/lib/patches/write-isolate-pnpm-workspace.ts +4 -4
- package/src/lib/registry/collect-reachable-package-names.test.ts +1 -1
- package/src/lib/registry/create-packages-registry.ts +34 -31
- package/src/lib/registry/helpers/find-packages-globs.ts +23 -19
- package/src/lib/registry/list-internal-packages.test.ts +2 -2
- package/src/lib/types.ts +2 -2
- package/src/lib/utils/filter-patched-dependencies.test.ts +1 -1
- package/src/lib/utils/filter-patched-dependencies.ts +2 -2
- package/src/lib/utils/get-dirname.ts +1 -1
- package/src/lib/utils/index.ts +1 -1
- package/src/lib/utils/json.ts +12 -14
- package/src/lib/utils/pack.ts +32 -22
- package/src/lib/utils/reset-isolate-dir.test.ts +165 -0
- package/src/lib/utils/reset-isolate-dir.ts +147 -0
- package/src/lib/utils/unpack.test.ts +76 -0
- package/src/lib/utils/unpack.ts +16 -10
- package/src/lib/utils/wait-for-complete-file.test.ts +105 -0
- package/src/lib/utils/wait-for-complete-file.ts +44 -0
- package/src/lib/utils/yaml.ts +8 -9
- package/src/testing/setup.ts +1 -1
- package/dist/isolate-DTwgcMAN.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { PatchFile } from "
|
|
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("
|
|
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("
|
|
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 "
|
|
4
|
-
import type { PatchFile, PnpmSettings } from "
|
|
5
|
-
import { readTypedYamlSync, writeTypedYamlSync } from "
|
|
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
|
|
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 "
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
path.join(absoluteDir, "package.json"),
|
|
47
|
-
);
|
|
43
|
+
log.debug(`Registering package ${rootRelativeDir}`);
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
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
|
|
52
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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 "
|
|
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("
|
|
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
|
|
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 "
|
|
2
|
+
import type { PackageManifest } from "#/lib/types";
|
|
3
3
|
import { filterPatchedDependencies } from "./filter-patched-dependencies";
|
|
4
4
|
|
|
5
5
|
describe("filterPatchedDependencies", () => {
|
package/src/lib/utils/index.ts
CHANGED
package/src/lib/utils/json.ts
CHANGED
|
@@ -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
|
|
6
|
+
export function readTypedJsonSync(filePath: string): unknown {
|
|
7
7
|
try {
|
|
8
8
|
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
9
|
-
|
|
9
|
+
return JSON.parse(
|
|
10
10
|
stripJsonComments(rawContent, { trailingCommas: true }),
|
|
11
|
-
) as
|
|
12
|
-
|
|
13
|
-
} catch (err) {
|
|
11
|
+
) as unknown;
|
|
12
|
+
} catch (error) {
|
|
14
13
|
throw new Error(
|
|
15
|
-
`Failed to read JSON from ${filePath}: ${getErrorMessage(
|
|
16
|
-
{ cause:
|
|
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
|
|
20
|
+
export async function readTypedJson(filePath: string): Promise<unknown> {
|
|
22
21
|
try {
|
|
23
22
|
const rawContent = await fs.readFile(filePath, "utf-8");
|
|
24
|
-
|
|
23
|
+
return JSON.parse(
|
|
25
24
|
stripJsonComments(rawContent, { trailingCommas: true }),
|
|
26
|
-
) as
|
|
27
|
-
|
|
28
|
-
} catch (err) {
|
|
25
|
+
) as unknown;
|
|
26
|
+
} catch (error) {
|
|
29
27
|
throw new Error(
|
|
30
|
-
`Failed to read JSON from ${filePath}: ${getErrorMessage(
|
|
31
|
-
{ cause:
|
|
28
|
+
`Failed to read JSON from ${filePath}: ${getErrorMessage(error)}`,
|
|
29
|
+
{ cause: error },
|
|
32
30
|
);
|
|
33
31
|
}
|
|
34
32
|
}
|
package/src/lib/utils/pack.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
59
|
+
const lastLine = packStdout.trim().split("\n").at(-1);
|
|
53
60
|
|
|
54
|
-
assert(
|
|
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
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { resetIsolateDir } from "./reset-isolate-dir";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wait until `predicate` returns true or the timeout elapses. Used for the
|
|
9
|
+
* fire-and-forget background delete, which we can't await directly.
|
|
10
|
+
*/
|
|
11
|
+
async function waitFor(
|
|
12
|
+
predicate: () => boolean | Promise<boolean>,
|
|
13
|
+
{
|
|
14
|
+
timeoutMs = 2000,
|
|
15
|
+
intervalMs = 20,
|
|
16
|
+
}: { timeoutMs?: number; intervalMs?: number } = {},
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const deadline = Date.now() + timeoutMs;
|
|
19
|
+
while (Date.now() < deadline) {
|
|
20
|
+
if (await predicate()) return;
|
|
21
|
+
await new Promise((resolve) => {
|
|
22
|
+
setTimeout(resolve, intervalMs);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for predicate`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("resetIsolateDir", () => {
|
|
29
|
+
let tempDir: string;
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "reset-isolate-dir-"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await fs.remove(tempDir).catch(() => {
|
|
37
|
+
/** Best-effort. */
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("creates an empty isolate dir when none exists", async () => {
|
|
42
|
+
const isolateDir = path.join(tempDir, "package", "isolate");
|
|
43
|
+
|
|
44
|
+
await resetIsolateDir(isolateDir);
|
|
45
|
+
|
|
46
|
+
expect(fs.existsSync(isolateDir)).toBe(true);
|
|
47
|
+
expect(await fs.readdir(isolateDir)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("empties an existing isolate dir and creates the trash sibling next to it by default", async () => {
|
|
51
|
+
const isolateDir = path.join(tempDir, "package", "isolate");
|
|
52
|
+
await fs.ensureDir(isolateDir);
|
|
53
|
+
await fs.writeFile(path.join(isolateDir, "stale.txt"), "stale");
|
|
54
|
+
|
|
55
|
+
await resetIsolateDir(isolateDir);
|
|
56
|
+
|
|
57
|
+
expect(fs.existsSync(isolateDir)).toBe(true);
|
|
58
|
+
expect(await fs.readdir(isolateDir)).toEqual([]);
|
|
59
|
+
|
|
60
|
+
/** Background delete eventually removes the trash sibling. */
|
|
61
|
+
await waitFor(async () => {
|
|
62
|
+
const entries = await fs.readdir(path.dirname(isolateDir));
|
|
63
|
+
return entries.every((entry) => !entry.includes(".trash-"));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("places trash in the provided trashParentDir instead of next to isolateDir", async () => {
|
|
68
|
+
const packageParent = path.join(tempDir, "packages");
|
|
69
|
+
const isolateDir = path.join(packageParent, "api", "isolate");
|
|
70
|
+
await fs.ensureDir(isolateDir);
|
|
71
|
+
await fs.writeFile(path.join(isolateDir, "stale.txt"), "stale");
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wrap `fs.rename` so we can read back the destination path the function
|
|
75
|
+
* picked — the trash dir name is randomised, so capturing the spy's
|
|
76
|
+
* arguments is the only way to assert where the rename targeted.
|
|
77
|
+
*/
|
|
78
|
+
const renameSpy = vi.spyOn(fs, "rename");
|
|
79
|
+
|
|
80
|
+
await resetIsolateDir(isolateDir, { trashParentDir: packageParent });
|
|
81
|
+
|
|
82
|
+
expect(renameSpy).toHaveBeenCalledTimes(1);
|
|
83
|
+
const [, renamedTo] = renameSpy.mock.calls[0]!;
|
|
84
|
+
expect(path.dirname(renamedTo as string)).toBe(packageParent);
|
|
85
|
+
expect(path.basename(renamedTo as string)).toMatch(
|
|
86
|
+
/^\.api-isolate\.trash-/,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
/** The original target package dir contains nothing but a fresh empty isolate dir. */
|
|
90
|
+
expect(await fs.readdir(path.dirname(isolateDir))).toEqual(["isolate"]);
|
|
91
|
+
expect(await fs.readdir(isolateDir)).toEqual([]);
|
|
92
|
+
|
|
93
|
+
renameSpy.mockRestore();
|
|
94
|
+
|
|
95
|
+
await waitFor(async () => {
|
|
96
|
+
const entries = await fs.readdir(packageParent);
|
|
97
|
+
return entries.every((entry) => !entry.includes(".trash-"));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("sweeps leftover trash from previous runs", async () => {
|
|
102
|
+
const packageParent = path.join(tempDir, "packages");
|
|
103
|
+
const isolateDir = path.join(packageParent, "api", "isolate");
|
|
104
|
+
await fs.ensureDir(isolateDir);
|
|
105
|
+
|
|
106
|
+
/** Simulate debris left behind by a previously killed run. */
|
|
107
|
+
const stale1 = path.join(packageParent, ".api-isolate.trash-9999-aabbccdd");
|
|
108
|
+
const stale2 = path.join(packageParent, ".api-isolate.trash-9998-eeff0011");
|
|
109
|
+
await fs.ensureDir(stale1);
|
|
110
|
+
await fs.ensureDir(stale2);
|
|
111
|
+
await fs.writeFile(path.join(stale1, "junk"), "junk");
|
|
112
|
+
|
|
113
|
+
/** Unrelated sibling that must be left alone. */
|
|
114
|
+
const sibling = path.join(packageParent, "web");
|
|
115
|
+
await fs.ensureDir(sibling);
|
|
116
|
+
|
|
117
|
+
await resetIsolateDir(isolateDir, { trashParentDir: packageParent });
|
|
118
|
+
|
|
119
|
+
/** Eventually both the stale entries and any new trash are gone. */
|
|
120
|
+
await waitFor(async () => {
|
|
121
|
+
const entries = await fs.readdir(packageParent);
|
|
122
|
+
return entries.every((entry) => !entry.includes(".trash-"));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(fs.existsSync(sibling)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("only sweeps trash matching this isolateDir's stem", async () => {
|
|
129
|
+
const packageParent = path.join(tempDir, "packages");
|
|
130
|
+
const isolateDir = path.join(packageParent, "api", "isolate");
|
|
131
|
+
await fs.ensureDir(isolateDir);
|
|
132
|
+
|
|
133
|
+
/** Trash from a different package's isolate run. */
|
|
134
|
+
const otherTrash = path.join(
|
|
135
|
+
packageParent,
|
|
136
|
+
".web-isolate.trash-1234-deadbeef",
|
|
137
|
+
);
|
|
138
|
+
await fs.ensureDir(otherTrash);
|
|
139
|
+
|
|
140
|
+
await resetIsolateDir(isolateDir, { trashParentDir: packageParent });
|
|
141
|
+
|
|
142
|
+
/** The sweep filter is keyed on the stem, so other packages' trash stays. */
|
|
143
|
+
expect(fs.existsSync(otherTrash)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("falls back to recursive delete when rename fails", async () => {
|
|
147
|
+
const isolateDir = path.join(tempDir, "package", "isolate");
|
|
148
|
+
await fs.ensureDir(isolateDir);
|
|
149
|
+
await fs.writeFile(path.join(isolateDir, "stale.txt"), "stale");
|
|
150
|
+
|
|
151
|
+
const renameSpy = vi
|
|
152
|
+
.spyOn(fs, "rename")
|
|
153
|
+
.mockRejectedValueOnce(
|
|
154
|
+
Object.assign(new Error("EXDEV"), { code: "EXDEV" }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
await resetIsolateDir(isolateDir);
|
|
158
|
+
|
|
159
|
+
expect(renameSpy).toHaveBeenCalledTimes(1);
|
|
160
|
+
expect(fs.existsSync(isolateDir)).toBe(true);
|
|
161
|
+
expect(await fs.readdir(isolateDir)).toEqual([]);
|
|
162
|
+
|
|
163
|
+
renameSpy.mockRestore();
|
|
164
|
+
});
|
|
165
|
+
});
|