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
|
@@ -5,14 +5,15 @@ import path, { join } from "node:path";
|
|
|
5
5
|
import { isEmpty, omit, pick, unique } from "remeda";
|
|
6
6
|
import { exec, execFileSync, execSync } from "node:child_process";
|
|
7
7
|
import { detectMonorepo } from "detect-monorepo";
|
|
8
|
-
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { createConsola } from "consola";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
10
|
import { inspect } from "node:util";
|
|
12
|
-
import fs$1 from "node:fs";
|
|
11
|
+
import fs$1, { createReadStream } from "node:fs";
|
|
13
12
|
import stripJsonComments from "strip-json-comments";
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { pipeline } from "node:stream/promises";
|
|
15
|
+
import { createGunzip } from "node:zlib";
|
|
16
|
+
import { extract } from "tar-fs";
|
|
16
17
|
import yaml from "yaml";
|
|
17
18
|
import Arborist from "@npmcli/arborist";
|
|
18
19
|
import Config from "@npmcli/config";
|
|
@@ -21,7 +22,6 @@ import { getLockfileImporterId, readWantedLockfile, writeWantedLockfile } from "
|
|
|
21
22
|
import { getLockfileImporterId as getLockfileImporterId$1, readWantedLockfile as readWantedLockfile$1, writeWantedLockfile as writeWantedLockfile$1 } from "pnpm_lockfile_file_v9";
|
|
22
23
|
import { pruneLockfile } from "pnpm_prune_lockfile_v8";
|
|
23
24
|
import { pruneLockfile as pruneLockfile$1 } from "pnpm_prune_lockfile_v9";
|
|
24
|
-
import path$1 from "path";
|
|
25
25
|
import { getTsconfig } from "get-tsconfig";
|
|
26
26
|
import outdent$1 from "outdent";
|
|
27
27
|
import { globSync } from "glob";
|
|
@@ -178,16 +178,16 @@ function readTypedJsonSync(filePath) {
|
|
|
178
178
|
try {
|
|
179
179
|
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
180
180
|
return JSON.parse(stripJsonComments(rawContent, { trailingCommas: true }));
|
|
181
|
-
} catch (
|
|
182
|
-
throw new Error(`Failed to read JSON from ${filePath}: ${getErrorMessage(
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`Failed to read JSON from ${filePath}: ${getErrorMessage(error)}`, { cause: error });
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
async function readTypedJson(filePath) {
|
|
186
186
|
try {
|
|
187
187
|
const rawContent = await fs.readFile(filePath, "utf-8");
|
|
188
188
|
return JSON.parse(stripJsonComments(rawContent, { trailingCommas: true }));
|
|
189
|
-
} catch (
|
|
190
|
-
throw new Error(`Failed to read JSON from ${filePath}: ${getErrorMessage(
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw new Error(`Failed to read JSON from ${filePath}: ${getErrorMessage(error)}`, { cause: error });
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
//#endregion
|
|
@@ -199,162 +199,128 @@ function getIsolateRelativeLogPath(path, isolatePath) {
|
|
|
199
199
|
return join("(isolate)", path.replace(isolatePath, ""));
|
|
200
200
|
}
|
|
201
201
|
//#endregion
|
|
202
|
-
//#region src/lib/utils/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
} catch (err) {
|
|
235
|
-
throw new Error(`Failed to find package manager version for ${name}: ${getErrorMessage(err)}`, { cause: err });
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
/** If no lockfile was found, it could be that there is an npm shrinkwrap file. */
|
|
239
|
-
if (fs.existsSync(path.join(workspaceRoot, "npm-shrinkwrap.json"))) {
|
|
240
|
-
const version = getVersion("npm");
|
|
241
|
-
return {
|
|
242
|
-
name: "npm",
|
|
243
|
-
version,
|
|
244
|
-
majorVersion: getMajorVersion(version)
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
throw new Error(`Failed to detect package manager`);
|
|
248
|
-
}
|
|
249
|
-
function getVersion(packageManagerName) {
|
|
250
|
-
return execSync(`${packageManagerName} --version`).toString().trim();
|
|
251
|
-
}
|
|
252
|
-
//#endregion
|
|
253
|
-
//#region src/lib/package-manager/helpers/infer-from-manifest.ts
|
|
254
|
-
function inferFromManifest(workspaceRoot) {
|
|
202
|
+
//#region src/lib/utils/reset-isolate-dir.ts
|
|
203
|
+
/**
|
|
204
|
+
* Prefix used for trash directories created when resetting the isolate output
|
|
205
|
+
* directory. The leading dot keeps it hidden in file explorers and `ls`
|
|
206
|
+
* without `-a`, so users don't see a flash of two folders while the old
|
|
207
|
+
* contents are being reaped in the background.
|
|
208
|
+
*/
|
|
209
|
+
const TRASH_PREFIX = ".";
|
|
210
|
+
const TRASH_INFIX = ".trash-";
|
|
211
|
+
/**
|
|
212
|
+
* Reset the isolate output directory to a fresh empty directory, avoiding the
|
|
213
|
+
* `ENOTEMPTY` race that occurs when another process (e.g. the Firebase
|
|
214
|
+
* functions emulator, a file watcher) writes into the directory while it is
|
|
215
|
+
* being recursively deleted.
|
|
216
|
+
*
|
|
217
|
+
* Strategy:
|
|
218
|
+
*
|
|
219
|
+
* 1. Sweep any leftover trash directories from previous runs that may have
|
|
220
|
+
* been killed mid-cleanup. Best-effort: ignore errors.
|
|
221
|
+
* 2. If the isolate directory exists, atomically `rename` it to a hidden
|
|
222
|
+
* sibling on the same filesystem. The rename is atomic, so the moment it
|
|
223
|
+
* returns the original path is free for a fresh empty directory and
|
|
224
|
+
* nothing a concurrent writer does inside the old tree can affect the
|
|
225
|
+
* new one.
|
|
226
|
+
* 3. Kick off the recursive delete of the trash directory in the background.
|
|
227
|
+
* We don't await it: it is the slowest part of an isolate run, and any
|
|
228
|
+
* failure (e.g. another process still holding files open) is harmless
|
|
229
|
+
* because the logical state is already correct. Stale trash dirs are
|
|
230
|
+
* reaped by the next run's sweep in step 1.
|
|
231
|
+
* 4. Ensure the (now-vacant) isolate directory exists.
|
|
232
|
+
*/
|
|
233
|
+
async function resetIsolateDir(isolateDir, options = {}) {
|
|
255
234
|
const log = useLogger();
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
235
|
+
const trashParentDir = options.trashParentDir ?? path.dirname(isolateDir);
|
|
236
|
+
const trashGlobPrefix = `${TRASH_PREFIX}${buildTrashStem(trashParentDir, isolateDir)}${TRASH_INFIX}`;
|
|
237
|
+
/** Best-effort sweep of leftover trash from previously killed runs. */
|
|
238
|
+
await sweepStaleTrash(trashParentDir, trashGlobPrefix);
|
|
239
|
+
if (fs.existsSync(isolateDir)) {
|
|
240
|
+
const trashDir = path.join(trashParentDir, `${trashGlobPrefix}${process.pid}-${randomBytes(4).toString("hex")}`);
|
|
241
|
+
try {
|
|
242
|
+
await fs.ensureDir(trashParentDir);
|
|
243
|
+
await fs.rename(isolateDir, trashDir);
|
|
244
|
+
log.debug("Moved existing isolate output directory to trash for cleanup");
|
|
245
|
+
/**
|
|
246
|
+
* Fire-and-forget. A concurrent writer can cause `ENOTEMPTY` or
|
|
247
|
+
* `EBUSY` here, but the logical state is already correct: the real
|
|
248
|
+
* `isolateDir` is gone. Any debris left behind will be swept on the
|
|
249
|
+
* next run.
|
|
250
|
+
*/
|
|
251
|
+
fs.remove(trashDir).catch((error) => {
|
|
252
|
+
log.debug("Background cleanup of trashed isolate directory did not complete:", error instanceof Error ? error.message : String(error));
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
/**
|
|
256
|
+
* `rename` can fail with `EXDEV` if `trashParentDir` ends up on a
|
|
257
|
+
* different filesystem from `isolateDir`, or with `EPERM` on platforms
|
|
258
|
+
* that disallow renaming busy directories. Fall back to the original
|
|
259
|
+
* behaviour: a straight recursive delete. This preserves correctness
|
|
260
|
+
* at the cost of the race the rename was meant to avoid.
|
|
261
|
+
*/
|
|
262
|
+
log.debug("Could not rename existing isolate output directory, falling back to recursive delete:", error instanceof Error ? error.message : String(error));
|
|
263
|
+
await fs.remove(isolateDir);
|
|
264
|
+
}
|
|
260
265
|
}
|
|
261
|
-
|
|
262
|
-
assert(supportedPackageManagerNames.includes(name), `Package manager "${name}" is not currently supported`);
|
|
263
|
-
const lockfileName = getLockfileFileName(name);
|
|
264
|
-
assert(fs.existsSync(path.join(workspaceRoot, lockfileName)), `Manifest declares ${name} to be the packageManager, but failed to find ${lockfileName} in workspace root`);
|
|
265
|
-
return {
|
|
266
|
-
name,
|
|
267
|
-
version,
|
|
268
|
-
majorVersion: getMajorVersion(version),
|
|
269
|
-
packageManagerString
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
//#endregion
|
|
273
|
-
//#region src/lib/package-manager/index.ts
|
|
274
|
-
let packageManager;
|
|
275
|
-
function usePackageManager() {
|
|
276
|
-
if (!packageManager) throw Error("No package manager detected. Make sure to call detectPackageManager() before usePackageManager()");
|
|
277
|
-
return packageManager;
|
|
266
|
+
await fs.ensureDir(isolateDir);
|
|
278
267
|
}
|
|
279
268
|
/**
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
269
|
+
* Build a stable name stem for the trash directory. When `trashParentDir` is
|
|
270
|
+
* the direct parent of `isolateDir` (the default), this is just the isolate
|
|
271
|
+
* dir's basename. When it isn't (e.g. callers placing trash outside the
|
|
272
|
+
* packed dir), include the relative path so multiple isolate dirs sharing
|
|
273
|
+
* the same trash parent don't collide in the sweep filter.
|
|
283
274
|
*/
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
* Disable infer from manifest for now. I doubt it is useful after all but
|
|
289
|
-
* I'll keep the code as a reminder.
|
|
290
|
-
*/
|
|
291
|
-
packageManager = inferFromManifest(workspaceRootDir) ?? inferFromFiles(workspaceRootDir);
|
|
292
|
-
return packageManager;
|
|
293
|
-
}
|
|
294
|
-
function shouldUsePnpmPack() {
|
|
295
|
-
const { name, majorVersion } = usePackageManager();
|
|
296
|
-
return name === "pnpm" && majorVersion >= 8;
|
|
275
|
+
function buildTrashStem(trashParentDir, isolateDir) {
|
|
276
|
+
const relative = path.relative(trashParentDir, isolateDir);
|
|
277
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return path.basename(isolateDir);
|
|
278
|
+
return relative.split(path.sep).filter(Boolean).join("-");
|
|
297
279
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
resolve(stdout);
|
|
316
|
-
});
|
|
317
|
-
}) : await new Promise((resolve, reject) => {
|
|
318
|
-
exec(`npm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout) => {
|
|
319
|
-
if (err) return reject(err);
|
|
320
|
-
resolve(stdout);
|
|
280
|
+
/**
|
|
281
|
+
* Best-effort sweep of leftover trash directories matching this isolate dir's
|
|
282
|
+
* stem. Failures are swallowed: stale trash is debris, not state, and a
|
|
283
|
+
* subsequent run will get another chance to reap it.
|
|
284
|
+
*/
|
|
285
|
+
async function sweepStaleTrash(parentDir, trashGlobPrefix) {
|
|
286
|
+
let entries;
|
|
287
|
+
try {
|
|
288
|
+
entries = await fs.readdir(parentDir);
|
|
289
|
+
} catch {
|
|
290
|
+
/** Parent doesn't exist yet; nothing to sweep. */
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
await Promise.all(entries.filter((entry) => entry.startsWith(trashGlobPrefix)).map(async (entry) => {
|
|
294
|
+
await fs.remove(path.join(parentDir, entry)).catch(() => {
|
|
295
|
+
/** Best-effort. */
|
|
321
296
|
});
|
|
322
|
-
});
|
|
323
|
-
const lastLine = stdout.trim().split("\n").at(-1);
|
|
324
|
-
assert(lastLine, `Failed to parse last line from stdout: ${stdout.trim()}`);
|
|
325
|
-
const fileName = path.basename(lastLine);
|
|
326
|
-
assert(fileName, `Failed to parse file name from: ${lastLine}`);
|
|
327
|
-
const filePath = path.join(dstDir, fileName);
|
|
328
|
-
if (!fs$1.existsSync(filePath)) log.error(`The response from pack could not be resolved to an existing file: ${filePath}`);
|
|
329
|
-
else log.debug(`Packed (temp)/${fileName}`);
|
|
330
|
-
process.chdir(previousCwd);
|
|
331
|
-
/**
|
|
332
|
-
* Return the path anyway even if it doesn't validate. A later stage will wait
|
|
333
|
-
* for the file to occur still. Not sure if this makes sense. Maybe we should
|
|
334
|
-
* stop at the validation error...
|
|
335
|
-
*/
|
|
336
|
-
return filePath;
|
|
297
|
+
}));
|
|
337
298
|
}
|
|
338
299
|
//#endregion
|
|
339
300
|
//#region src/lib/utils/unpack.ts
|
|
301
|
+
/**
|
|
302
|
+
* Extract a gzipped tar archive into the given directory.
|
|
303
|
+
*
|
|
304
|
+
* Uses `stream/promises.pipeline` so that errors at any stage (file read,
|
|
305
|
+
* gunzip, tar extract) propagate as a rejected promise rather than crashing
|
|
306
|
+
* the process as unhandled stream errors.
|
|
307
|
+
*/
|
|
340
308
|
async function unpack(filePath, unpackDir) {
|
|
341
|
-
await
|
|
342
|
-
fs.createReadStream(filePath).pipe(createGunzip()).pipe(tar.extract(unpackDir)).on("finish", () => resolve()).on("error", (err) => reject(err));
|
|
343
|
-
});
|
|
309
|
+
await pipeline(createReadStream(filePath), createGunzip(), extract(unpackDir));
|
|
344
310
|
}
|
|
345
311
|
//#endregion
|
|
346
312
|
//#region src/lib/utils/yaml.ts
|
|
313
|
+
/** @todo Add some zod validation maybe */
|
|
347
314
|
function readTypedYamlSync(filePath) {
|
|
348
315
|
try {
|
|
349
316
|
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
350
|
-
/** @todo Add some zod validation maybe */
|
|
351
317
|
return yaml.parse(rawContent);
|
|
352
|
-
} catch (
|
|
353
|
-
throw new Error(`Failed to read YAML from ${filePath}: ${getErrorMessage(
|
|
318
|
+
} catch (error) {
|
|
319
|
+
throw new Error(`Failed to read YAML from ${filePath}: ${getErrorMessage(error)}`, { cause: error });
|
|
354
320
|
}
|
|
355
321
|
}
|
|
322
|
+
/** @todo Add some zod validation maybe */
|
|
356
323
|
function writeTypedYamlSync(filePath, content) {
|
|
357
|
-
/** @todo Add some zod validation maybe */
|
|
358
324
|
fs.writeFileSync(filePath, yaml.stringify(content), "utf-8");
|
|
359
325
|
}
|
|
360
326
|
//#endregion
|
|
@@ -486,6 +452,104 @@ function resolveConfig(initialConfig) {
|
|
|
486
452
|
return config;
|
|
487
453
|
}
|
|
488
454
|
//#endregion
|
|
455
|
+
//#region src/lib/utils/get-major-version.ts
|
|
456
|
+
function getMajorVersion(version) {
|
|
457
|
+
return parseInt(version.split(".").at(0) ?? "0", 10);
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/lib/package-manager/names.ts
|
|
461
|
+
const supportedPackageManagerNames = [
|
|
462
|
+
"pnpm",
|
|
463
|
+
"yarn",
|
|
464
|
+
"npm",
|
|
465
|
+
"bun"
|
|
466
|
+
];
|
|
467
|
+
const lockfileFileNamesByPackageManager = {
|
|
468
|
+
bun: "bun.lock",
|
|
469
|
+
pnpm: "pnpm-lock.yaml",
|
|
470
|
+
yarn: "yarn.lock",
|
|
471
|
+
npm: "package-lock.json"
|
|
472
|
+
};
|
|
473
|
+
function getLockfileFileName(name) {
|
|
474
|
+
return lockfileFileNamesByPackageManager[name];
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/lib/package-manager/helpers/infer-from-files.ts
|
|
478
|
+
function inferFromFiles(workspaceRoot) {
|
|
479
|
+
for (const name of supportedPackageManagerNames) {
|
|
480
|
+
const lockfileName = getLockfileFileName(name);
|
|
481
|
+
if (fs.existsSync(path.join(workspaceRoot, lockfileName))) try {
|
|
482
|
+
const version = getVersion(name);
|
|
483
|
+
return {
|
|
484
|
+
name,
|
|
485
|
+
version,
|
|
486
|
+
majorVersion: getMajorVersion(version)
|
|
487
|
+
};
|
|
488
|
+
} catch (error) {
|
|
489
|
+
throw new Error(`Failed to find package manager version for ${name}: ${getErrorMessage(error)}`, { cause: error });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** If no lockfile was found, it could be that there is an npm shrinkwrap file. */
|
|
493
|
+
if (fs.existsSync(path.join(workspaceRoot, "npm-shrinkwrap.json"))) {
|
|
494
|
+
const version = getVersion("npm");
|
|
495
|
+
return {
|
|
496
|
+
name: "npm",
|
|
497
|
+
version,
|
|
498
|
+
majorVersion: getMajorVersion(version)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
throw new Error(`Failed to detect package manager`);
|
|
502
|
+
}
|
|
503
|
+
function getVersion(packageManagerName) {
|
|
504
|
+
return execSync(`${packageManagerName} --version`).toString().trim();
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/lib/package-manager/helpers/infer-from-manifest.ts
|
|
508
|
+
function inferFromManifest(workspaceRoot) {
|
|
509
|
+
const log = useLogger();
|
|
510
|
+
const { packageManager: packageManagerString } = readTypedJsonSync(path.join(workspaceRoot, "package.json"));
|
|
511
|
+
if (!packageManagerString) {
|
|
512
|
+
log.debug("No packageManager field found in root manifest");
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const [name, version = "*"] = packageManagerString.split("@");
|
|
516
|
+
assert(supportedPackageManagerNames.includes(name), `Package manager "${name}" is not currently supported`);
|
|
517
|
+
const lockfileName = getLockfileFileName(name);
|
|
518
|
+
assert(fs.existsSync(path.join(workspaceRoot, lockfileName)), `Manifest declares ${name} to be the packageManager, but failed to find ${lockfileName} in workspace root`);
|
|
519
|
+
return {
|
|
520
|
+
name,
|
|
521
|
+
version,
|
|
522
|
+
majorVersion: getMajorVersion(version),
|
|
523
|
+
packageManagerString
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/lib/package-manager/index.ts
|
|
528
|
+
let packageManager;
|
|
529
|
+
function usePackageManager() {
|
|
530
|
+
if (!packageManager) throw new Error("No package manager detected. Make sure to call detectPackageManager() before usePackageManager()");
|
|
531
|
+
return packageManager;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* First we check if the package manager is declared in the manifest. If it is,
|
|
535
|
+
* we get the name and version from there. Otherwise we'll search for the
|
|
536
|
+
* different lockfiles and ask the OS to report the installed version.
|
|
537
|
+
*/
|
|
538
|
+
function detectPackageManager(workspaceRootDir) {
|
|
539
|
+
if (isRushWorkspace(workspaceRootDir)) packageManager = inferFromFiles(path.join(workspaceRootDir, "common/config/rush"));
|
|
540
|
+
else
|
|
541
|
+
/**
|
|
542
|
+
* Disable infer from manifest for now. I doubt it is useful after all but
|
|
543
|
+
* I'll keep the code as a reminder.
|
|
544
|
+
*/
|
|
545
|
+
packageManager = inferFromManifest(workspaceRootDir) ?? inferFromFiles(workspaceRootDir);
|
|
546
|
+
return packageManager;
|
|
547
|
+
}
|
|
548
|
+
function shouldUsePnpmPack() {
|
|
549
|
+
const { name, majorVersion } = usePackageManager();
|
|
550
|
+
return name === "pnpm" && majorVersion >= 8;
|
|
551
|
+
}
|
|
552
|
+
//#endregion
|
|
489
553
|
//#region src/lib/lockfile/helpers/bun-lockfile.ts
|
|
490
554
|
/** Extract dependency names from a workspace entry. */
|
|
491
555
|
function collectDependencyNames(entry, includeDevDependencies) {
|
|
@@ -533,8 +597,8 @@ function getPackageInfoObject(entry) {
|
|
|
533
597
|
function collectRequiredPackages(directDependencyNames, packages) {
|
|
534
598
|
const required = /* @__PURE__ */ new Set();
|
|
535
599
|
const queue = [...directDependencyNames];
|
|
536
|
-
|
|
537
|
-
|
|
600
|
+
let name;
|
|
601
|
+
while ((name = queue.pop()) !== void 0) {
|
|
538
602
|
if (required.has(name)) continue;
|
|
539
603
|
const entry = packages[name];
|
|
540
604
|
if (!entry) continue;
|
|
@@ -546,15 +610,19 @@ function collectRequiredPackages(directDependencyNames, packages) {
|
|
|
546
610
|
"dependencies",
|
|
547
611
|
"optionalDependencies",
|
|
548
612
|
"peerDependencies"
|
|
549
|
-
])
|
|
550
|
-
const deps = info[depField];
|
|
551
|
-
if (deps && typeof deps === "object") {
|
|
552
|
-
for (const depName of Object.keys(deps)) if (!required.has(depName)) queue.push(depName);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
613
|
+
]) enqueueDeps(info[depField], required, queue);
|
|
555
614
|
}
|
|
556
615
|
return required;
|
|
557
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Push any names from a dependency map onto the BFS queue, skipping anything
|
|
619
|
+
* already marked required so we don't revisit it. `deps` is typed as `unknown`
|
|
620
|
+
* because it comes from a freshly-parsed lockfile entry with no schema.
|
|
621
|
+
*/
|
|
622
|
+
function enqueueDeps(deps, required, queue) {
|
|
623
|
+
if (!deps || typeof deps !== "object") return;
|
|
624
|
+
for (const depName of Object.keys(deps)) if (!required.has(depName)) queue.push(depName);
|
|
625
|
+
}
|
|
558
626
|
//#endregion
|
|
559
627
|
//#region src/lib/lockfile/helpers/generate-bun-lockfile.ts
|
|
560
628
|
/**
|
|
@@ -659,9 +727,9 @@ async function generateBunLockfile({ workspaceRootDir, targetPackageDir, isolate
|
|
|
659
727
|
/** Append trailing newline to match Bun's native output format */
|
|
660
728
|
await fs.writeFile(outputPath, serializeWithTrailingCommas(outputLockfile) + "\n");
|
|
661
729
|
log.debug("Created lockfile at", outputPath);
|
|
662
|
-
} catch (
|
|
663
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
664
|
-
throw
|
|
730
|
+
} catch (error) {
|
|
731
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
732
|
+
throw error;
|
|
665
733
|
}
|
|
666
734
|
}
|
|
667
735
|
//#endregion
|
|
@@ -709,9 +777,9 @@ async function generateNpmLockfile({ workspaceRootDir, isolateDir, targetPackage
|
|
|
709
777
|
});
|
|
710
778
|
}
|
|
711
779
|
log.debug("Created lockfile at", path.join(isolateDir, "package-lock.json"));
|
|
712
|
-
} catch (
|
|
713
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
714
|
-
throw
|
|
780
|
+
} catch (error) {
|
|
781
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
782
|
+
throw error;
|
|
715
783
|
}
|
|
716
784
|
}
|
|
717
785
|
async function generateFromRootLockfile({ workspaceRootDir, isolateDir, targetPackageName, targetPackageManifest, packagesRegistry, internalDepPackageNames }) {
|
|
@@ -727,7 +795,7 @@ async function generateFromRootLockfile({ workspaceRootDir, isolateDir, targetPa
|
|
|
727
795
|
const rootTree = await arborist.loadVirtual();
|
|
728
796
|
const targetImporterNode = arborist.workspaceNodes(rootTree, [targetPackageName])[0];
|
|
729
797
|
if (!targetImporterNode) throw new Error(`Target workspace "${targetPackageName}" not found in root package-lock.json`);
|
|
730
|
-
if (typeof targetImporterNode.location !== "string") throw new
|
|
798
|
+
if (typeof targetImporterNode.location !== "string") throw new TypeError(`Target workspace "${targetPackageName}" resolved to a node without a location`);
|
|
731
799
|
/**
|
|
732
800
|
* `workspaceDependencySet` walks `edgesOut` from each seed node. It does
|
|
733
801
|
* not add the seed node itself to the result, so ensure the target
|
|
@@ -766,6 +834,12 @@ async function generateFromRootLockfile({ workspaceRootDir, isolateDir, targetPa
|
|
|
766
834
|
srcData,
|
|
767
835
|
reachable,
|
|
768
836
|
targetImporterLoc: targetImporterNode.location,
|
|
837
|
+
/**
|
|
838
|
+
* npm's lockfile exposes each workspace as a Link at
|
|
839
|
+
* `node_modules/<name>`. This link is pointless in the isolate (the
|
|
840
|
+
* target becomes the root), so filter it out if it shows up in the
|
|
841
|
+
* reachable set.
|
|
842
|
+
*/
|
|
769
843
|
targetLinkLoc: `node_modules/${targetPackageName}`,
|
|
770
844
|
targetPackageManifest
|
|
771
845
|
});
|
|
@@ -795,10 +869,16 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
795
869
|
const srcPackages = srcData.packages;
|
|
796
870
|
if (!srcPackages[targetImporterLoc]) throw new Error(`Source lockfile has no entry for target importer "${targetImporterLoc}"`);
|
|
797
871
|
const targetNestedNodeModulesPrefix = `${targetImporterLoc}/node_modules/`;
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
872
|
+
const remapToOutputLoc = (origLoc) => {
|
|
873
|
+
if (origLoc === targetImporterLoc) return "";
|
|
874
|
+
if (origLoc.startsWith(targetNestedNodeModulesPrefix)) return origLoc.slice(targetImporterLoc.length + 1);
|
|
875
|
+
return origLoc;
|
|
876
|
+
};
|
|
877
|
+
const isTargetNested = (origLoc) => origLoc === targetImporterLoc || origLoc.startsWith(targetNestedNodeModulesPrefix);
|
|
878
|
+
/** Track the source location each output entry came from, used to identify
|
|
879
|
+
* the displaced entry when a collision occurs. */
|
|
801
880
|
const origLocByNewLoc = /* @__PURE__ */ new Map();
|
|
881
|
+
const collisions = [];
|
|
802
882
|
for (const node of reachable) {
|
|
803
883
|
const origLoc = node.location;
|
|
804
884
|
/** The target's self-link has no place in the isolate (root IS the target). */
|
|
@@ -814,20 +894,79 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
814
894
|
* `packages/app/lib/core`) are preserved verbatim because their disk
|
|
815
895
|
* location in the isolate is unchanged.
|
|
816
896
|
*/
|
|
817
|
-
|
|
818
|
-
if (origLoc === targetImporterLoc) newLoc = "";
|
|
819
|
-
else if (origLoc.startsWith(targetNestedNodeModulesPrefix)) newLoc = origLoc.slice(targetImporterLoc.length + 1);
|
|
820
|
-
else newLoc = origLoc;
|
|
897
|
+
const newLoc = remapToOutputLoc(origLoc);
|
|
821
898
|
const srcEntry = srcPackages[origLoc];
|
|
822
899
|
if (!srcEntry) throw new Error(`Reachable node "${origLoc}" has no entry in source lockfile packages`);
|
|
823
900
|
const existing = outPackages[newLoc];
|
|
824
901
|
if (existing && !entriesAreEquivalent(existing, srcEntry)) {
|
|
825
902
|
const previousOrigLoc = origLocByNewLoc.get(newLoc) ?? "<unknown>";
|
|
826
|
-
|
|
903
|
+
const incomingIsTargetNested = isTargetNested(origLoc);
|
|
904
|
+
const previousIsTargetNested = isTargetNested(previousOrigLoc);
|
|
905
|
+
if (incomingIsTargetNested && !previousIsTargetNested) {
|
|
906
|
+
/** The target-nested entry wins. The previously-stored hoisted entry
|
|
907
|
+
* is displaced and must be re-nested under its consumers. */
|
|
908
|
+
collisions.push({
|
|
909
|
+
loserOrigLoc: previousOrigLoc,
|
|
910
|
+
loserEntry: existing
|
|
911
|
+
});
|
|
912
|
+
outPackages[newLoc] = { ...srcEntry };
|
|
913
|
+
origLocByNewLoc.set(newLoc, origLoc);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (!incomingIsTargetNested && previousIsTargetNested) {
|
|
917
|
+
/** The previously-stored target-nested entry wins; the incoming
|
|
918
|
+
* hoisted entry is the loser. */
|
|
919
|
+
collisions.push({
|
|
920
|
+
loserOrigLoc: origLoc,
|
|
921
|
+
loserEntry: { ...srcEntry }
|
|
922
|
+
});
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
/** Neither side is the target's nested version, or both are — we have
|
|
926
|
+
* no rule to pick a winner. Bail loudly. */
|
|
927
|
+
throw new Error(`Path collision at "${newLoc}": source locations "${previousOrigLoc}" and "${origLoc}" both map there with conflicting entries and no rule applies to pick a winner (neither is a target-nested override, or both are). Please report a reproduction at https://github.com/0x80/isolate-package/issues.`);
|
|
827
928
|
}
|
|
828
929
|
outPackages[newLoc] = { ...srcEntry };
|
|
829
930
|
origLocByNewLoc.set(newLoc, origLoc);
|
|
830
931
|
}
|
|
932
|
+
/** Re-nest each displaced entry under the reachable consumers that
|
|
933
|
+
* originally resolved to it via node_modules walk-up. */
|
|
934
|
+
for (const collision of collisions) {
|
|
935
|
+
const loserName = extractPackageNameFromLockfileLoc(collision.loserOrigLoc);
|
|
936
|
+
if (!loserName) continue;
|
|
937
|
+
for (const consumer of reachable) {
|
|
938
|
+
const consumerSrcLoc = consumer.location;
|
|
939
|
+
if (consumerSrcLoc === collision.loserOrigLoc) continue;
|
|
940
|
+
if (consumerSrcLoc === targetLinkLoc) continue;
|
|
941
|
+
const consumerEntry = srcPackages[consumerSrcLoc];
|
|
942
|
+
if (!consumerEntry) continue;
|
|
943
|
+
/** Workspace links carry dependency metadata on the importer entry,
|
|
944
|
+
* not the link entry itself. Skip the link side. */
|
|
945
|
+
if (consumerEntry.link) continue;
|
|
946
|
+
if (!entryDependsOn(consumerEntry, loserName)) continue;
|
|
947
|
+
if (resolveDepInSrcLockfile(consumerSrcLoc, loserName, srcPackages) !== collision.loserOrigLoc) continue;
|
|
948
|
+
const consumerNewLoc = remapToOutputLoc(consumerSrcLoc);
|
|
949
|
+
/** Consumer maps to the isolate root (the target itself). The root
|
|
950
|
+
* slot is already taken by the winning version. The target's own
|
|
951
|
+
* dependencies use that version — we cannot serve a different one
|
|
952
|
+
* here without nesting under the target, which would be its own
|
|
953
|
+
* collision. Accept the resolution shift. */
|
|
954
|
+
if (consumerNewLoc === "") continue;
|
|
955
|
+
/** If the consumer was itself displaced by another collision (its
|
|
956
|
+
* src-side entry doesn't match the entry we actually placed at its
|
|
957
|
+
* new location), the consumer isn't really present in the isolate.
|
|
958
|
+
* Its original dep needs are irrelevant here. */
|
|
959
|
+
const consumerOutEntry = outPackages[consumerNewLoc];
|
|
960
|
+
if (!consumerOutEntry || !entriesAreEquivalent(consumerOutEntry, consumerEntry)) continue;
|
|
961
|
+
const nestedLoc = `${consumerNewLoc}/node_modules/${loserName}`;
|
|
962
|
+
const existingNested = outPackages[nestedLoc];
|
|
963
|
+
if (existingNested) {
|
|
964
|
+
if (entriesAreEquivalent(existingNested, collision.loserEntry)) continue;
|
|
965
|
+
throw new Error(`Cannot re-nest displaced "${loserName}" under "${consumerNewLoc}": the slot "${nestedLoc}" already contains a different entry. Please report a reproduction at https://github.com/0x80/isolate-package/issues.`);
|
|
966
|
+
}
|
|
967
|
+
outPackages[nestedLoc] = { ...collision.loserEntry };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
831
970
|
/**
|
|
832
971
|
* If the target importer didn't make it into the reachable set for any
|
|
833
972
|
* reason (upstream Arborist bug, programmer error), bail loudly rather
|
|
@@ -835,8 +974,10 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
835
974
|
*/
|
|
836
975
|
if (!outPackages[""]) throw new Error(`Target importer "${targetImporterLoc}" was not present in the reachable node set; cannot construct isolate root entry`);
|
|
837
976
|
/** Overlay the isolate root with the adapted target manifest. */
|
|
838
|
-
const rootEntry = {
|
|
839
|
-
|
|
977
|
+
const rootEntry = {
|
|
978
|
+
...outPackages[""],
|
|
979
|
+
name: targetPackageManifest.name
|
|
980
|
+
};
|
|
840
981
|
if (targetPackageManifest.version) rootEntry.version = targetPackageManifest.version;
|
|
841
982
|
overlayManifestDeps(rootEntry, targetPackageManifest);
|
|
842
983
|
/** The isolate is no longer a workspace root. */
|
|
@@ -874,6 +1015,55 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
874
1015
|
function entriesAreEquivalent(a, b) {
|
|
875
1016
|
return a.version === b.version && a.resolved === b.resolved && a.integrity === b.integrity && !!a.link === !!b.link;
|
|
876
1017
|
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Extracts the package name from a lockfile install location. Handles scoped
|
|
1020
|
+
* packages, where the name is two segments after the last `node_modules/`.
|
|
1021
|
+
*
|
|
1022
|
+
* "node_modules/foo" -> "foo"
|
|
1023
|
+
* "node_modules/@scope/foo" -> "@scope/foo"
|
|
1024
|
+
* "node_modules/a/node_modules/b" -> "b"
|
|
1025
|
+
* "node_modules/a/node_modules/@scope/b" -> "@scope/b"
|
|
1026
|
+
*
|
|
1027
|
+
* Returns null for locations that don't contain `node_modules/`.
|
|
1028
|
+
*/
|
|
1029
|
+
function extractPackageNameFromLockfileLoc(loc) {
|
|
1030
|
+
const lastIdx = loc.lastIndexOf("node_modules/");
|
|
1031
|
+
if (lastIdx < 0) return null;
|
|
1032
|
+
const tail = loc.slice(lastIdx + 13);
|
|
1033
|
+
if (tail.startsWith("@")) {
|
|
1034
|
+
const slashIdx = tail.indexOf("/");
|
|
1035
|
+
if (slashIdx < 0) return tail;
|
|
1036
|
+
/** Stop at the second `/` so we don't include any further nesting. */
|
|
1037
|
+
const secondSlash = tail.indexOf("/", slashIdx + 1);
|
|
1038
|
+
return secondSlash < 0 ? tail : tail.slice(0, secondSlash);
|
|
1039
|
+
}
|
|
1040
|
+
const slashIdx = tail.indexOf("/");
|
|
1041
|
+
return slashIdx < 0 ? tail : tail.slice(0, slashIdx);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Returns true if the lockfile entry lists `depName` in any of its dependency
|
|
1045
|
+
* fields. Includes peer/optional/dev because any of them may have been the
|
|
1046
|
+
* reason for the dep being installed at runtime.
|
|
1047
|
+
*/
|
|
1048
|
+
function entryDependsOn(entry, depName) {
|
|
1049
|
+
return entry.dependencies?.[depName] !== void 0 || entry.devDependencies?.[depName] !== void 0 || entry.peerDependencies?.[depName] !== void 0 || entry.optionalDependencies?.[depName] !== void 0;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Resolves `depName` against `srcPackages` from the perspective of a consumer
|
|
1053
|
+
* at `consumerLoc`, mirroring Node.js `node_modules` walk-up. Returns the
|
|
1054
|
+
* lockfile key of the entry the consumer would load at runtime, or null when
|
|
1055
|
+
* no candidate exists.
|
|
1056
|
+
*/
|
|
1057
|
+
function resolveDepInSrcLockfile(consumerLoc, depName, srcPackages) {
|
|
1058
|
+
let scope = consumerLoc;
|
|
1059
|
+
while (true) {
|
|
1060
|
+
const candidate = scope === "" ? `node_modules/${depName}` : `${scope}/node_modules/${depName}`;
|
|
1061
|
+
if (srcPackages[candidate]) return candidate;
|
|
1062
|
+
if (scope === "") return null;
|
|
1063
|
+
const idx = scope.lastIndexOf("/node_modules/");
|
|
1064
|
+
scope = idx < 0 ? "" : scope.slice(0, idx);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
877
1067
|
function overlayManifestDeps(entry, manifest) {
|
|
878
1068
|
for (const field of [
|
|
879
1069
|
"dependencies",
|
|
@@ -995,9 +1185,9 @@ async function generatePnpmLockfile({ workspaceRootDir, targetPackageDir, isolat
|
|
|
995
1185
|
patchedDependencies
|
|
996
1186
|
});
|
|
997
1187
|
log.debug("Created lockfile at", path.join(isolateDir, "pnpm-lock.yaml"));
|
|
998
|
-
} catch (
|
|
999
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
1000
|
-
throw
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
1190
|
+
throw error;
|
|
1001
1191
|
}
|
|
1002
1192
|
}
|
|
1003
1193
|
//#endregion
|
|
@@ -1023,9 +1213,9 @@ async function generateYarnLockfile({ workspaceRootDir, isolateDir }) {
|
|
|
1023
1213
|
log.debug(`Running local install`);
|
|
1024
1214
|
execSync(`yarn install --cwd ${isolateDir}`);
|
|
1025
1215
|
log.debug("Generated lockfile at", newLockfilePath);
|
|
1026
|
-
} catch (
|
|
1027
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
1028
|
-
throw
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
1218
|
+
throw error;
|
|
1029
1219
|
}
|
|
1030
1220
|
}
|
|
1031
1221
|
//#endregion
|
|
@@ -1102,7 +1292,7 @@ async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir,
|
|
|
1102
1292
|
//#endregion
|
|
1103
1293
|
//#region src/lib/manifest/io.ts
|
|
1104
1294
|
async function readManifest(packageDir) {
|
|
1105
|
-
return readTypedJson(path.join(packageDir, "package.json"));
|
|
1295
|
+
return await readTypedJson(path.join(packageDir, "package.json"));
|
|
1106
1296
|
}
|
|
1107
1297
|
async function writeManifest(outputDir, manifest) {
|
|
1108
1298
|
await fs.writeFile(path.join(outputDir, "package.json"), JSON.stringify(manifest, null, 2));
|
|
@@ -1138,30 +1328,83 @@ function adaptManifestInternalDeps({ manifest, packagesRegistry, parentRootRelat
|
|
|
1138
1328
|
}
|
|
1139
1329
|
//#endregion
|
|
1140
1330
|
//#region src/lib/manifest/helpers/resolve-catalog-dependencies.ts
|
|
1331
|
+
const catalogSourceCache = /* @__PURE__ */ new Map();
|
|
1332
|
+
/**
|
|
1333
|
+
* Loads catalog definitions by checking pnpm-workspace.yaml first (pnpm
|
|
1334
|
+
* format), then falling back to the root package.json (Bun format).
|
|
1335
|
+
*
|
|
1336
|
+
* Pnpm defines catalogs in pnpm-workspace.yaml:
|
|
1337
|
+
*
|
|
1338
|
+
* ```yaml
|
|
1339
|
+
* catalog:
|
|
1340
|
+
* react: ^18.3.1
|
|
1341
|
+
* catalogs:
|
|
1342
|
+
* react18:
|
|
1343
|
+
* react: ^18.3.1
|
|
1344
|
+
* ```
|
|
1345
|
+
*
|
|
1346
|
+
* Bun defines catalogs in package.json (at root level or under workspaces).
|
|
1347
|
+
*/
|
|
1348
|
+
async function loadCatalogSource(workspaceRootDir) {
|
|
1349
|
+
const cached = catalogSourceCache.get(workspaceRootDir);
|
|
1350
|
+
if (cached) return cached;
|
|
1351
|
+
/**
|
|
1352
|
+
* Drop the cache entry if loading fails so a subsequent call can retry
|
|
1353
|
+
* instead of immediately rethrowing the same rejection.
|
|
1354
|
+
*/
|
|
1355
|
+
const cachedLoadPromise = (async () => {
|
|
1356
|
+
const log = useLogger();
|
|
1357
|
+
/** Try pnpm-workspace.yaml first. */
|
|
1358
|
+
const workspaceYamlPath = path.join(workspaceRootDir, "pnpm-workspace.yaml");
|
|
1359
|
+
if (await fs.pathExists(workspaceYamlPath)) try {
|
|
1360
|
+
const rawContent = await fs.readFile(workspaceYamlPath, "utf-8");
|
|
1361
|
+
const yamlConfig = yaml.parse(rawContent);
|
|
1362
|
+
if (yamlConfig?.catalog || yamlConfig?.catalogs) return {
|
|
1363
|
+
catalog: yamlConfig.catalog,
|
|
1364
|
+
catalogs: yamlConfig.catalogs
|
|
1365
|
+
};
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
log.warn(`Failed to parse ${workspaceYamlPath}: ${error instanceof Error ? error.message : String(error)}. Falling back to package.json for catalog definitions.`);
|
|
1368
|
+
}
|
|
1369
|
+
const rootManifest = await readTypedJson(path.join(workspaceRootDir, "package.json"));
|
|
1370
|
+
return {
|
|
1371
|
+
catalog: rootManifest.catalog ?? rootManifest.workspaces?.catalog,
|
|
1372
|
+
catalogs: rootManifest.catalogs ?? rootManifest.workspaces?.catalogs
|
|
1373
|
+
};
|
|
1374
|
+
})().catch((error) => {
|
|
1375
|
+
catalogSourceCache.delete(workspaceRootDir);
|
|
1376
|
+
throw error;
|
|
1377
|
+
});
|
|
1378
|
+
catalogSourceCache.set(workspaceRootDir, cachedLoadPromise);
|
|
1379
|
+
return cachedLoadPromise;
|
|
1380
|
+
}
|
|
1141
1381
|
/**
|
|
1142
1382
|
* Resolves catalog dependencies by replacing "catalog:" specifiers with their
|
|
1143
|
-
* actual versions
|
|
1383
|
+
* actual versions.
|
|
1144
1384
|
*
|
|
1145
1385
|
* Supports both pnpm and Bun catalog formats:
|
|
1146
1386
|
*
|
|
1147
|
-
* - Pnpm: catalog
|
|
1148
|
-
* - Bun: catalog or catalogs at root level, or workspaces.catalog
|
|
1387
|
+
* - Pnpm: catalog/catalogs defined in pnpm-workspace.yaml
|
|
1388
|
+
* - Bun: catalog or catalogs at root level, or workspaces.catalog in
|
|
1389
|
+
* package.json
|
|
1149
1390
|
*/
|
|
1150
1391
|
async function resolveCatalogDependencies(dependencies, workspaceRootDir) {
|
|
1151
1392
|
if (!dependencies) return;
|
|
1152
1393
|
const log = useLogger();
|
|
1153
|
-
const
|
|
1154
|
-
const flatCatalog = rootManifest.catalog || rootManifest.workspaces?.catalog;
|
|
1155
|
-
const nestedCatalogs = rootManifest.catalogs || rootManifest.workspaces?.catalogs;
|
|
1394
|
+
const { catalog: flatCatalog, catalogs: nestedCatalogs } = await loadCatalogSource(workspaceRootDir);
|
|
1156
1395
|
if (!flatCatalog && !nestedCatalogs) return dependencies;
|
|
1157
1396
|
const resolved = { ...dependencies };
|
|
1158
1397
|
for (const [packageName, specifier] of Object.entries(dependencies)) if (specifier === "catalog:" || specifier.startsWith("catalog:")) {
|
|
1159
1398
|
let catalogVersion;
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1399
|
+
const groupName = specifier === "catalog:" ? "default" : specifier.slice(8);
|
|
1400
|
+
if (groupName === "default")
|
|
1401
|
+
/**
|
|
1402
|
+
* Per pnpm semantics, `catalog:` and `catalog:default` are
|
|
1403
|
+
* equivalent: the default catalog can live under the top-level
|
|
1404
|
+
* `catalog` field or under `catalogs.default`, so check both.
|
|
1405
|
+
*/
|
|
1406
|
+
catalogVersion = flatCatalog?.[packageName] ?? nestedCatalogs?.default?.[packageName];
|
|
1407
|
+
else catalogVersion = nestedCatalogs?.[groupName]?.[packageName];
|
|
1165
1408
|
if (catalogVersion) {
|
|
1166
1409
|
log.debug(`Resolving catalog dependency ${packageName}: "${specifier}" -> "${catalogVersion}"`);
|
|
1167
1410
|
resolved[packageName] = catalogVersion;
|
|
@@ -1212,7 +1455,7 @@ async function adaptInternalPackageManifests({ internalPackageNames, packagesReg
|
|
|
1212
1455
|
*/
|
|
1213
1456
|
async function adoptPnpmFieldsFromRoot(targetPackageManifest, workspaceRootDir) {
|
|
1214
1457
|
if (isRushWorkspace(workspaceRootDir)) return targetPackageManifest;
|
|
1215
|
-
const rootPackageManifest = await readTypedJson(path
|
|
1458
|
+
const rootPackageManifest = await readTypedJson(path.join(workspaceRootDir, "package.json"));
|
|
1216
1459
|
if (usePackageManager().name === "bun") return adoptBunFieldsFromRoot(targetPackageManifest, rootPackageManifest);
|
|
1217
1460
|
return adoptPnpmFieldsOnly(targetPackageManifest, rootPackageManifest);
|
|
1218
1461
|
}
|
|
@@ -1232,7 +1475,7 @@ function adoptBunFieldsFromRoot(targetPackageManifest, rootPackageManifest) {
|
|
|
1232
1475
|
}
|
|
1233
1476
|
/** Adopt pnpm-specific fields from the root manifest */
|
|
1234
1477
|
function adoptPnpmFieldsOnly(targetPackageManifest, rootPackageManifest) {
|
|
1235
|
-
const { overrides, onlyBuiltDependencies, ignoredBuiltDependencies } = rootPackageManifest.pnpm
|
|
1478
|
+
const { overrides, onlyBuiltDependencies, ignoredBuiltDependencies } = rootPackageManifest.pnpm ?? {};
|
|
1236
1479
|
/** If no pnpm fields are present, return the original manifest */
|
|
1237
1480
|
if (!overrides && !onlyBuiltDependencies && !ignoredBuiltDependencies) return targetPackageManifest;
|
|
1238
1481
|
const pnpmConfig = {};
|
|
@@ -1268,7 +1511,16 @@ async function adaptTargetPackageManifest({ manifest, packagesRegistry, workspac
|
|
|
1268
1511
|
manifest: manifestWithResolvedCatalogs,
|
|
1269
1512
|
packagesRegistry
|
|
1270
1513
|
}),
|
|
1514
|
+
/**
|
|
1515
|
+
* Adopt the package manager definition from the root manifest if available.
|
|
1516
|
+
* The option to omit is there because some platforms might not handle it
|
|
1517
|
+
* properly (Cloud Run, April 24th 2024, does not handle pnpm v9)
|
|
1518
|
+
*/
|
|
1271
1519
|
packageManager: omitPackageManager ? void 0 : packageManager.packageManagerString,
|
|
1520
|
+
/**
|
|
1521
|
+
* Scripts are removed by default if not explicitly picked or omitted via
|
|
1522
|
+
* config.
|
|
1523
|
+
*/
|
|
1272
1524
|
scripts: pickFromScripts ? pick(manifest.scripts ?? {}, pickFromScripts) : omitFromScripts ? omit(manifest.scripts ?? {}, omitFromScripts) : {}
|
|
1273
1525
|
};
|
|
1274
1526
|
}
|
|
@@ -1299,8 +1551,8 @@ function validateManifestMandatoryFields(manifest, packagePath, requireFilesFiel
|
|
|
1299
1551
|
* packed
|
|
1300
1552
|
*/
|
|
1301
1553
|
if (requireFilesField && (!manifest.files || !Array.isArray(manifest.files) || manifest.files.length === 0)) missingFields.push("files");
|
|
1302
|
-
|
|
1303
|
-
|
|
1554
|
+
const [field] = missingFields;
|
|
1555
|
+
if (field) {
|
|
1304
1556
|
const errorMessage = missingFields.length === 1 ? `Package at ${packagePath} is missing the "${field}" field in its package.json. See ${fieldDocUrls[field] ?? "https://isolate-package.codecompose.dev/getting-started#prerequisites"}` : `Package at ${packagePath} is missing mandatory fields in its package.json: ${missingFields.join(", ")}. See https://isolate-package.codecompose.dev/getting-started#prerequisites`;
|
|
1305
1557
|
log.error(errorMessage);
|
|
1306
1558
|
throw new Error(errorMessage);
|
|
@@ -1309,7 +1561,7 @@ function validateManifestMandatoryFields(manifest, packagePath, requireFilesFiel
|
|
|
1309
1561
|
}
|
|
1310
1562
|
//#endregion
|
|
1311
1563
|
//#region src/lib/output/get-build-output-dir.ts
|
|
1312
|
-
|
|
1564
|
+
function getBuildOutputDir({ targetPackageDir, buildDirName, tsconfigPath }) {
|
|
1313
1565
|
const log = useLogger();
|
|
1314
1566
|
if (buildDirName) {
|
|
1315
1567
|
log.debug("Using buildDirName from config:", buildDirName);
|
|
@@ -1332,6 +1584,91 @@ async function getBuildOutputDir({ targetPackageDir, buildDirName, tsconfigPath
|
|
|
1332
1584
|
}
|
|
1333
1585
|
}
|
|
1334
1586
|
//#endregion
|
|
1587
|
+
//#region src/lib/utils/wait-for-complete-file.ts
|
|
1588
|
+
/**
|
|
1589
|
+
* Wait until the given file exists and its size has stopped changing across
|
|
1590
|
+
* two consecutive polls. Resolves once the file is considered fully written,
|
|
1591
|
+
* rejects with a timeout error otherwise.
|
|
1592
|
+
*
|
|
1593
|
+
* This is a cheap proxy for "the writer has finished flushing" without
|
|
1594
|
+
* inspecting file contents or relying on platform-specific signals. It is
|
|
1595
|
+
* intended for cases where an external process (e.g. `pnpm pack`) may report
|
|
1596
|
+
* completion before its output is fully visible on disk.
|
|
1597
|
+
*/
|
|
1598
|
+
async function waitForCompleteFile(filePath, { timeoutMs, pollMs }) {
|
|
1599
|
+
const deadline = Date.now() + timeoutMs;
|
|
1600
|
+
let lastSize = -1;
|
|
1601
|
+
while (Date.now() < deadline) {
|
|
1602
|
+
try {
|
|
1603
|
+
const { size } = await fs$1.promises.stat(filePath);
|
|
1604
|
+
if (size > 0 && size === lastSize) return;
|
|
1605
|
+
lastSize = size;
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
if (error.code !== "ENOENT") throw error;
|
|
1608
|
+
}
|
|
1609
|
+
await new Promise((resolve) => {
|
|
1610
|
+
setTimeout(resolve, pollMs);
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for file to be written: ${filePath}`);
|
|
1614
|
+
}
|
|
1615
|
+
//#endregion
|
|
1616
|
+
//#region src/lib/utils/pack.ts
|
|
1617
|
+
/**
|
|
1618
|
+
* How long to wait for the packed tarball to appear and stop growing on disk
|
|
1619
|
+
* after `pnpm pack` / `npm pack` has exited.
|
|
1620
|
+
*/
|
|
1621
|
+
const PACK_FILE_READY_TIMEOUT_MS = 5e3;
|
|
1622
|
+
const PACK_FILE_READY_POLL_MS = 50;
|
|
1623
|
+
async function pack(srcDir, dstDir) {
|
|
1624
|
+
const log = useLogger();
|
|
1625
|
+
const execOptions = {
|
|
1626
|
+
cwd: srcDir,
|
|
1627
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1628
|
+
};
|
|
1629
|
+
/**
|
|
1630
|
+
* PNPM pack seems to be a lot faster than NPM pack, so when PNPM is detected
|
|
1631
|
+
* we use that instead.
|
|
1632
|
+
*/
|
|
1633
|
+
const packStdout = shouldUsePnpmPack() ? await new Promise((resolve, reject) => {
|
|
1634
|
+
exec(`pnpm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout) => {
|
|
1635
|
+
if (err) {
|
|
1636
|
+
log.error(getErrorMessage(err));
|
|
1637
|
+
reject(err);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
resolve(stdout);
|
|
1641
|
+
});
|
|
1642
|
+
}) : await new Promise((resolve, reject) => {
|
|
1643
|
+
exec(`npm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout) => {
|
|
1644
|
+
if (err) {
|
|
1645
|
+
reject(err);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
resolve(stdout);
|
|
1649
|
+
});
|
|
1650
|
+
});
|
|
1651
|
+
const lastLine = packStdout.trim().split("\n").at(-1);
|
|
1652
|
+
assert(lastLine, `Failed to parse last line from stdout: ${packStdout.trim()}`);
|
|
1653
|
+
const fileName = path.basename(lastLine);
|
|
1654
|
+
assert(fileName, `Failed to parse file name from: ${lastLine}`);
|
|
1655
|
+
const filePath = path.join(dstDir, fileName);
|
|
1656
|
+
/**
|
|
1657
|
+
* `pnpm pack` (and occasionally `npm pack`) can return before the tarball is
|
|
1658
|
+
* fully visible/flushed to disk. A naive `existsSync` check is not enough:
|
|
1659
|
+
* the directory entry can appear before the file's data has been written,
|
|
1660
|
+
* which causes downstream consumers (gunzip + tar) to fail with
|
|
1661
|
+
* "unexpected end of file". Wait until the file exists and its size has
|
|
1662
|
+
* stopped changing across two consecutive polls before returning.
|
|
1663
|
+
*/
|
|
1664
|
+
await waitForCompleteFile(filePath, {
|
|
1665
|
+
timeoutMs: PACK_FILE_READY_TIMEOUT_MS,
|
|
1666
|
+
pollMs: PACK_FILE_READY_POLL_MS
|
|
1667
|
+
});
|
|
1668
|
+
log.debug(`Packed (temp)/${fileName}`);
|
|
1669
|
+
return filePath;
|
|
1670
|
+
}
|
|
1671
|
+
//#endregion
|
|
1335
1672
|
//#region src/lib/output/pack-dependencies.ts
|
|
1336
1673
|
/**
|
|
1337
1674
|
* Pack dependencies so that we extract only the files that are supposed to be
|
|
@@ -1360,18 +1697,13 @@ async function packDependencies({ packagesRegistry, internalPackageNames, packDe
|
|
|
1360
1697
|
}
|
|
1361
1698
|
//#endregion
|
|
1362
1699
|
//#region src/lib/output/process-build-output-files.ts
|
|
1363
|
-
const TIMEOUT_MS = 5e3;
|
|
1364
1700
|
async function processBuildOutputFiles({ targetPackageDir, tmpDir, isolateDir }) {
|
|
1365
|
-
const log = useLogger();
|
|
1366
1701
|
const packedFilePath = await pack(targetPackageDir, tmpDir);
|
|
1367
1702
|
const unpackDir = path.join(tmpDir, "target");
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
isWaitingYet = true;
|
|
1373
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1374
|
-
}
|
|
1703
|
+
/**
|
|
1704
|
+
* `pack` already waits for the tarball to be fully written before returning,
|
|
1705
|
+
* so it is safe to unpack immediately.
|
|
1706
|
+
*/
|
|
1375
1707
|
await unpack(packedFilePath, unpackDir);
|
|
1376
1708
|
await fs.copy(path.join(unpackDir, "package"), isolateDir);
|
|
1377
1709
|
}
|
|
@@ -1444,7 +1776,8 @@ function collectReachablePackageNames({ targetPackageManifest, packagesRegistry,
|
|
|
1444
1776
|
*/
|
|
1445
1777
|
function findPackagesGlobs(workspaceRootDir) {
|
|
1446
1778
|
const log = useLogger();
|
|
1447
|
-
|
|
1779
|
+
const packageManager = usePackageManager();
|
|
1780
|
+
switch (packageManager.name) {
|
|
1448
1781
|
case "pnpm": {
|
|
1449
1782
|
const workspaceConfig = readTypedYamlSync(path.join(workspaceRootDir, "pnpm-workspace.yaml"));
|
|
1450
1783
|
if (!workspaceConfig) throw new Error("pnpm-workspace.yaml file is empty. Please specify packages configuration.");
|
|
@@ -1460,17 +1793,16 @@ function findPackagesGlobs(workspaceRootDir) {
|
|
|
1460
1793
|
const { workspaces } = readTypedJsonSync(workspaceRootManifestPath);
|
|
1461
1794
|
if (!workspaces) throw new Error(`No workspaces field found in ${workspaceRootManifestPath}`);
|
|
1462
1795
|
if (Array.isArray(workspaces)) return workspaces;
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
return workspacesObject.packages;
|
|
1472
|
-
}
|
|
1796
|
+
/**
|
|
1797
|
+
* For Yarn, workspaces could be defined as an object with { packages: [],
|
|
1798
|
+
* nohoist: [] }. See
|
|
1799
|
+
* https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
|
|
1800
|
+
*/
|
|
1801
|
+
const workspacesObject = workspaces;
|
|
1802
|
+
assert(Array.isArray(workspacesObject.packages), "workspaces.packages must be an array");
|
|
1803
|
+
return workspacesObject.packages;
|
|
1473
1804
|
}
|
|
1805
|
+
default: throw new Error(`Unsupported package manager: ${packageManager.name}`);
|
|
1474
1806
|
}
|
|
1475
1807
|
}
|
|
1476
1808
|
//#endregion
|
|
@@ -1489,15 +1821,14 @@ async function createPackagesRegistry(workspaceRootDir, workspacePackagesOverrid
|
|
|
1489
1821
|
const manifestPath = path.join(absoluteDir, "package.json");
|
|
1490
1822
|
if (!fs.existsSync(manifestPath)) {
|
|
1491
1823
|
log.warn(`Ignoring directory ${rootRelativeDir} because it does not contain a package.json file`);
|
|
1492
|
-
return;
|
|
1493
|
-
} else {
|
|
1494
|
-
log.debug(`Registering package ${rootRelativeDir}`);
|
|
1495
|
-
return {
|
|
1496
|
-
manifest: await readTypedJson(path.join(absoluteDir, "package.json")),
|
|
1497
|
-
rootRelativeDir,
|
|
1498
|
-
absoluteDir
|
|
1499
|
-
};
|
|
1824
|
+
return null;
|
|
1500
1825
|
}
|
|
1826
|
+
log.debug(`Registering package ${rootRelativeDir}`);
|
|
1827
|
+
return {
|
|
1828
|
+
manifest: await readTypedJson(path.join(absoluteDir, "package.json")),
|
|
1829
|
+
rootRelativeDir,
|
|
1830
|
+
absoluteDir
|
|
1831
|
+
};
|
|
1501
1832
|
}))).reduce((acc, info) => {
|
|
1502
1833
|
if (info) acc[info.manifest.name] = info;
|
|
1503
1834
|
return acc;
|
|
@@ -1577,7 +1908,7 @@ function collectInstalledNamesFromBunLockfile({ workspaceRootDir, targetPackageD
|
|
|
1577
1908
|
const pkg = packagesRegistry[name];
|
|
1578
1909
|
if (!pkg) return null;
|
|
1579
1910
|
return pkg.rootRelativeDir.split(path.sep).join(path.posix.sep);
|
|
1580
|
-
}).filter(
|
|
1911
|
+
}).filter(Boolean);
|
|
1581
1912
|
const directDependencyNames = /* @__PURE__ */ new Set();
|
|
1582
1913
|
const targetEntry = lockfile.workspaces[targetWorkspaceKey];
|
|
1583
1914
|
if (targetEntry) for (const name of collectDependencyNames(targetEntry, includeDevDependencies)) directDependencyNames.add(name);
|
|
@@ -1588,8 +1919,8 @@ function collectInstalledNamesFromBunLockfile({ workspaceRootDir, targetPackageD
|
|
|
1588
1919
|
for (const name of collectDependencyNames(entry, false)) directDependencyNames.add(name);
|
|
1589
1920
|
}
|
|
1590
1921
|
return collectRequiredPackages(directDependencyNames, lockfile.packages);
|
|
1591
|
-
} catch (
|
|
1592
|
-
log.debug(`Failed to walk bun.lock for installed names: ${
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
log.debug(`Failed to walk bun.lock for installed names: ${error instanceof Error ? error.message : String(error)}`);
|
|
1593
1924
|
return /* @__PURE__ */ new Set();
|
|
1594
1925
|
}
|
|
1595
1926
|
}
|
|
@@ -1626,7 +1957,7 @@ async function collectInstalledNamesFromPnpmLockfile({ workspaceRootDir, targetP
|
|
|
1626
1957
|
* normalized id used as the importers map key.
|
|
1627
1958
|
*/
|
|
1628
1959
|
const targetImporterId = toLockfileImporterKey(useVersion9 ? getLockfileImporterId$1(workspaceRootDir, targetPackageDir) : getLockfileImporterId(workspaceRootDir, targetPackageDir), isRush);
|
|
1629
|
-
const importerIds = [targetImporterId, ...internalDepPackageNames.map((name) => packagesRegistry[name]?.rootRelativeDir).filter(
|
|
1960
|
+
const importerIds = [targetImporterId, ...internalDepPackageNames.map((name) => packagesRegistry[name]?.rootRelativeDir).filter(Boolean).map((dir) => toLockfileImporterKey(dir, isRush))];
|
|
1630
1961
|
const packages = lockfile.packages;
|
|
1631
1962
|
if (!packages) {
|
|
1632
1963
|
log.debug("Lockfile has no packages section to walk");
|
|
@@ -1646,8 +1977,8 @@ async function collectInstalledNamesFromPnpmLockfile({ workspaceRootDir, targetP
|
|
|
1646
1977
|
includeDevDependencies: importerId === targetImporterId && includeDevDependencies
|
|
1647
1978
|
});
|
|
1648
1979
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1980
|
+
let depPath;
|
|
1981
|
+
while ((depPath = queue.pop()) !== void 0) {
|
|
1651
1982
|
if (seen.has(depPath)) continue;
|
|
1652
1983
|
seen.add(depPath);
|
|
1653
1984
|
names.add(extractPackageName(depPath));
|
|
@@ -1664,8 +1995,8 @@ async function collectInstalledNamesFromPnpmLockfile({ workspaceRootDir, targetP
|
|
|
1664
1995
|
collectNames(pkg.peerDependencies, names);
|
|
1665
1996
|
}
|
|
1666
1997
|
return names;
|
|
1667
|
-
} catch (
|
|
1668
|
-
log.debug(`Failed to walk pnpm lockfile for installed names: ${
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
log.debug(`Failed to walk pnpm lockfile for installed names: ${error instanceof Error ? error.message : String(error)}`);
|
|
1669
2000
|
return /* @__PURE__ */ new Set();
|
|
1670
2001
|
}
|
|
1671
2002
|
}
|
|
@@ -1751,7 +2082,7 @@ function refToRelativeV8(reference, pkgName) {
|
|
|
1751
2082
|
*/
|
|
1752
2083
|
function extractPackageName(depPath) {
|
|
1753
2084
|
const peerStart = indexOfPeersSuffix(depPath);
|
|
1754
|
-
const trimmed = peerStart === -1 ? depPath : depPath.
|
|
2085
|
+
const trimmed = peerStart === -1 ? depPath : depPath.slice(0, peerStart);
|
|
1755
2086
|
if (trimmed.startsWith("/")) {
|
|
1756
2087
|
/** v8 v5-style: `/<name>/<version>` */
|
|
1757
2088
|
const stripped = trimmed.slice(1);
|
|
@@ -1961,16 +2292,16 @@ function writeIsolatePnpmWorkspace({ workspaceRootDir, isolateDir, copiedPatches
|
|
|
1961
2292
|
//#endregion
|
|
1962
2293
|
//#region src/isolate.ts
|
|
1963
2294
|
const __dirname = getDirname(import.meta.url);
|
|
1964
|
-
function createIsolator(
|
|
1965
|
-
const resolvedConfig = resolveConfig(
|
|
1966
|
-
return async function
|
|
2295
|
+
function createIsolator(initialConfig) {
|
|
2296
|
+
const resolvedConfig = resolveConfig(initialConfig);
|
|
2297
|
+
return async function runIsolate() {
|
|
1967
2298
|
const config = resolvedConfig;
|
|
1968
2299
|
setLogLevel(config.logLevel);
|
|
1969
2300
|
const log = useLogger();
|
|
1970
2301
|
const { version: libraryVersion } = await readTypedJson(path.join(path.join(__dirname, "..", "package.json")));
|
|
1971
2302
|
log.debug("Using isolate-package version", libraryVersion);
|
|
1972
2303
|
const { targetPackageDir, workspaceRootDir } = resolveWorkspacePaths(config);
|
|
1973
|
-
const buildOutputDir =
|
|
2304
|
+
const buildOutputDir = getBuildOutputDir({
|
|
1974
2305
|
targetPackageDir,
|
|
1975
2306
|
buildDirName: config.buildDirName,
|
|
1976
2307
|
tsconfigPath: config.tsconfigPath
|
|
@@ -1980,11 +2311,14 @@ function createIsolator(config) {
|
|
|
1980
2311
|
log.debug("Isolate target package", getRootRelativeLogPath(targetPackageDir, workspaceRootDir));
|
|
1981
2312
|
const isolateDir = path.join(targetPackageDir, config.isolateDirName);
|
|
1982
2313
|
log.debug("Isolate output directory", getRootRelativeLogPath(isolateDir, workspaceRootDir));
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2314
|
+
/**
|
|
2315
|
+
* Place the trash sibling outside `targetPackageDir`, since that directory
|
|
2316
|
+
* is later packed by `processBuildOutputFiles`. Keeping the trash next to
|
|
2317
|
+
* the target package (still on the same filesystem) preserves the atomic
|
|
2318
|
+
* rename without risking that the trash gets picked up by `npm pack` or
|
|
2319
|
+
* races with that step.
|
|
2320
|
+
*/
|
|
2321
|
+
await resetIsolateDir(isolateDir, { trashParentDir: path.dirname(targetPackageDir) });
|
|
1988
2322
|
const tmpDir = path.join(isolateDir, "__tmp");
|
|
1989
2323
|
await fs.ensureDir(tmpDir);
|
|
1990
2324
|
const targetPackageManifest = await readTypedJson(path.join(targetPackageDir, "package.json"));
|
|
@@ -2089,7 +2423,7 @@ function createIsolator(config) {
|
|
|
2089
2423
|
const patchEntries = Object.fromEntries(Object.entries(copiedPatches).map(([spec, patchFile]) => [spec, patchFile.path]));
|
|
2090
2424
|
if (packageManager.name === "bun") manifest.patchedDependencies = patchEntries;
|
|
2091
2425
|
else {
|
|
2092
|
-
|
|
2426
|
+
manifest.pnpm ??= {};
|
|
2093
2427
|
manifest.pnpm.patchedDependencies = patchEntries;
|
|
2094
2428
|
}
|
|
2095
2429
|
log.debug(`Added ${Object.keys(copiedPatches).length} patches to isolated package.json`);
|
|
@@ -2156,6 +2490,6 @@ async function isolate(config) {
|
|
|
2156
2490
|
return createIsolator(config)();
|
|
2157
2491
|
}
|
|
2158
2492
|
//#endregion
|
|
2159
|
-
export {
|
|
2493
|
+
export { defineConfig as a, resolveWorkspacePaths as c, detectPackageManager as i, readTypedJson as l, listInternalPackages as n, loadConfigFromFile as o, createPackagesRegistry as r, resolveConfig as s, isolate as t, filterObjectUndefined as u };
|
|
2160
2494
|
|
|
2161
|
-
//# sourceMappingURL=isolate-
|
|
2495
|
+
//# sourceMappingURL=isolate-DI3eUTci.mjs.map
|