isolate-package 1.33.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/isolate-bin.mjs +5 -6
- package/dist/isolate-bin.mjs.map +1 -1
- package/dist/{isolate-DyRD5Zd_.mjs → isolate-ts-Igq7C.mjs} +888 -271
- package/dist/isolate-ts-Igq7C.mjs.map +1 -0
- 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 +22 -17
- package/src/lib/config.test.ts +1 -1
- package/src/lib/config.ts +3 -3
- package/src/lib/lockfile/helpers/bun-lockfile.ts +153 -0
- package/src/lib/lockfile/helpers/generate-bun-lockfile.test.ts +3 -3
- package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +14 -146
- 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 +83 -2
- package/src/lib/lockfile/helpers/generate-pnpm-lockfile.ts +33 -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/adapt-target-package-manifest.ts +22 -13
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts +72 -3
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +22 -12
- 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 +154 -0
- package/src/lib/patches/collect-installed-names-bun.ts +87 -0
- package/src/lib/patches/collect-installed-names-pnpm.test.ts +316 -0
- package/src/lib/patches/collect-installed-names-pnpm.ts +365 -0
- package/src/lib/patches/copy-patches.test.ts +130 -13
- package/src/lib/patches/copy-patches.ts +47 -10
- 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-DyRD5Zd_.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;
|
|
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("-");
|
|
293
279
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
exec(`pnpm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout) => {
|
|
311
|
-
if (err) {
|
|
312
|
-
log.error(getErrorMessage(err));
|
|
313
|
-
return reject(err);
|
|
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,30 +452,106 @@ function resolveConfig(initialConfig) {
|
|
|
486
452
|
return config;
|
|
487
453
|
}
|
|
488
454
|
//#endregion
|
|
489
|
-
//#region src/lib/
|
|
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
|
+
}
|
|
490
533
|
/**
|
|
491
|
-
*
|
|
492
|
-
*
|
|
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.
|
|
493
537
|
*/
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
*
|
|
499
|
-
*
|
|
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.
|
|
500
544
|
*/
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
do {
|
|
504
|
-
previous = result;
|
|
505
|
-
result = result.replace(/(["\d\w\]}-])\n(\s*[\]}])/g, "$1,\n$2");
|
|
506
|
-
} while (result !== previous);
|
|
507
|
-
return result;
|
|
545
|
+
packageManager = inferFromManifest(workspaceRootDir) ?? inferFromFiles(workspaceRootDir);
|
|
546
|
+
return packageManager;
|
|
508
547
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
548
|
+
function shouldUsePnpmPack() {
|
|
549
|
+
const { name, majorVersion } = usePackageManager();
|
|
550
|
+
return name === "pnpm" && majorVersion >= 8;
|
|
551
|
+
}
|
|
552
|
+
//#endregion
|
|
553
|
+
//#region src/lib/lockfile/helpers/bun-lockfile.ts
|
|
554
|
+
/** Extract dependency names from a workspace entry. */
|
|
513
555
|
function collectDependencyNames(entry, includeDevDependencies) {
|
|
514
556
|
const names = /* @__PURE__ */ new Set();
|
|
515
557
|
for (const name of Object.keys(entry.dependencies ?? {})) names.add(name);
|
|
@@ -519,8 +561,8 @@ function collectDependencyNames(entry, includeDevDependencies) {
|
|
|
519
561
|
return [...names];
|
|
520
562
|
}
|
|
521
563
|
/**
|
|
522
|
-
* Check whether a package entry represents a workspace package by examining
|
|
523
|
-
* identifier string (first element in the entry array).
|
|
564
|
+
* Check whether a package entry represents a workspace package by examining
|
|
565
|
+
* its identifier string (first element in the entry array).
|
|
524
566
|
*/
|
|
525
567
|
function isWorkspacePackageEntry(entry) {
|
|
526
568
|
const ident = entry[0];
|
|
@@ -532,16 +574,16 @@ function isWorkspacePackageEntry(entry) {
|
|
|
532
574
|
* - workspace packages: [ident, info] -> index 1
|
|
533
575
|
* - git/github packages: [ident, info, checksum] -> index 1
|
|
534
576
|
*
|
|
535
|
-
* Detection: if the second element is a string (registry URL or checksum),
|
|
536
|
-
* info object is deeper. Workspace entries have only 2 elements.
|
|
577
|
+
* Detection: if the second element is a string (registry URL or checksum),
|
|
578
|
+
* the info object is deeper. Workspace entries have only 2 elements.
|
|
537
579
|
*/
|
|
538
580
|
function getPackageInfoObject(entry) {
|
|
539
581
|
if (entry.length <= 1) return void 0;
|
|
540
582
|
/** Workspace entries: [ident, info] */
|
|
541
583
|
if (isWorkspacePackageEntry(entry)) return typeof entry[1] === "object" ? entry[1] : void 0;
|
|
542
584
|
/**
|
|
543
|
-
* npm entries with registry URL: [ident, registryUrl, info, checksum].
|
|
544
|
-
*
|
|
585
|
+
* npm entries with registry URL: [ident, registryUrl, info, checksum]. The
|
|
586
|
+
* second element is a string (the registry URL).
|
|
545
587
|
*/
|
|
546
588
|
if (typeof entry[1] === "string") return typeof entry[2] === "object" ? entry[2] : void 0;
|
|
547
589
|
/** git/tarball entries: [ident, info, checksum] */
|
|
@@ -549,14 +591,14 @@ function getPackageInfoObject(entry) {
|
|
|
549
591
|
}
|
|
550
592
|
/**
|
|
551
593
|
* Recursively collect all package keys that are required, starting from a set
|
|
552
|
-
* of direct dependency names and walking through their transitive
|
|
553
|
-
* in the packages section.
|
|
594
|
+
* of direct dependency names and walking through their transitive
|
|
595
|
+
* dependencies in the packages section.
|
|
554
596
|
*/
|
|
555
597
|
function collectRequiredPackages(directDependencyNames, packages) {
|
|
556
598
|
const required = /* @__PURE__ */ new Set();
|
|
557
599
|
const queue = [...directDependencyNames];
|
|
558
|
-
|
|
559
|
-
|
|
600
|
+
let name;
|
|
601
|
+
while ((name = queue.pop()) !== void 0) {
|
|
560
602
|
if (required.has(name)) continue;
|
|
561
603
|
const entry = packages[name];
|
|
562
604
|
if (!entry) continue;
|
|
@@ -568,15 +610,40 @@ function collectRequiredPackages(directDependencyNames, packages) {
|
|
|
568
610
|
"dependencies",
|
|
569
611
|
"optionalDependencies",
|
|
570
612
|
"peerDependencies"
|
|
571
|
-
])
|
|
572
|
-
const deps = info[depField];
|
|
573
|
-
if (deps && typeof deps === "object") {
|
|
574
|
-
for (const depName of Object.keys(deps)) if (!required.has(depName)) queue.push(depName);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
613
|
+
]) enqueueDeps(info[depField], required, queue);
|
|
577
614
|
}
|
|
578
615
|
return required;
|
|
579
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
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/lib/lockfile/helpers/generate-bun-lockfile.ts
|
|
628
|
+
/**
|
|
629
|
+
* Serialize a value to JSON with trailing commas after every array element and
|
|
630
|
+
* object property, matching Bun's native bun.lock output format.
|
|
631
|
+
*/
|
|
632
|
+
function serializeWithTrailingCommas(value, indent = 2) {
|
|
633
|
+
/**
|
|
634
|
+
* Add trailing commas after values that precede a closing bracket/brace.
|
|
635
|
+
* Apply repeatedly because consecutive closing brackets (e.g. ]\n}) need
|
|
636
|
+
* multiple passes — the first pass adds a comma after the inner value, and
|
|
637
|
+
* subsequent passes handle the outer brackets.
|
|
638
|
+
*/
|
|
639
|
+
let result = JSON.stringify(value, null, indent);
|
|
640
|
+
let previous;
|
|
641
|
+
do {
|
|
642
|
+
previous = result;
|
|
643
|
+
result = result.replace(/(["\d\w\]}-])\n(\s*[\]}])/g, "$1,\n$2");
|
|
644
|
+
} while (result !== previous);
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
580
647
|
async function generateBunLockfile({ workspaceRootDir, targetPackageDir, isolateDir, internalDepPackageNames, packagesRegistry, includeDevDependencies }) {
|
|
581
648
|
const log = useLogger();
|
|
582
649
|
log.debug("Generating Bun lockfile...");
|
|
@@ -660,9 +727,9 @@ async function generateBunLockfile({ workspaceRootDir, targetPackageDir, isolate
|
|
|
660
727
|
/** Append trailing newline to match Bun's native output format */
|
|
661
728
|
await fs.writeFile(outputPath, serializeWithTrailingCommas(outputLockfile) + "\n");
|
|
662
729
|
log.debug("Created lockfile at", outputPath);
|
|
663
|
-
} catch (
|
|
664
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
665
|
-
throw
|
|
730
|
+
} catch (error) {
|
|
731
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
732
|
+
throw error;
|
|
666
733
|
}
|
|
667
734
|
}
|
|
668
735
|
//#endregion
|
|
@@ -710,9 +777,9 @@ async function generateNpmLockfile({ workspaceRootDir, isolateDir, targetPackage
|
|
|
710
777
|
});
|
|
711
778
|
}
|
|
712
779
|
log.debug("Created lockfile at", path.join(isolateDir, "package-lock.json"));
|
|
713
|
-
} catch (
|
|
714
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
715
|
-
throw
|
|
780
|
+
} catch (error) {
|
|
781
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
782
|
+
throw error;
|
|
716
783
|
}
|
|
717
784
|
}
|
|
718
785
|
async function generateFromRootLockfile({ workspaceRootDir, isolateDir, targetPackageName, targetPackageManifest, packagesRegistry, internalDepPackageNames }) {
|
|
@@ -728,7 +795,7 @@ async function generateFromRootLockfile({ workspaceRootDir, isolateDir, targetPa
|
|
|
728
795
|
const rootTree = await arborist.loadVirtual();
|
|
729
796
|
const targetImporterNode = arborist.workspaceNodes(rootTree, [targetPackageName])[0];
|
|
730
797
|
if (!targetImporterNode) throw new Error(`Target workspace "${targetPackageName}" not found in root package-lock.json`);
|
|
731
|
-
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`);
|
|
732
799
|
/**
|
|
733
800
|
* `workspaceDependencySet` walks `edgesOut` from each seed node. It does
|
|
734
801
|
* not add the seed node itself to the result, so ensure the target
|
|
@@ -767,6 +834,12 @@ async function generateFromRootLockfile({ workspaceRootDir, isolateDir, targetPa
|
|
|
767
834
|
srcData,
|
|
768
835
|
reachable,
|
|
769
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
|
+
*/
|
|
770
843
|
targetLinkLoc: `node_modules/${targetPackageName}`,
|
|
771
844
|
targetPackageManifest
|
|
772
845
|
});
|
|
@@ -796,10 +869,16 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
796
869
|
const srcPackages = srcData.packages;
|
|
797
870
|
if (!srcPackages[targetImporterLoc]) throw new Error(`Source lockfile has no entry for target importer "${targetImporterLoc}"`);
|
|
798
871
|
const targetNestedNodeModulesPrefix = `${targetImporterLoc}/node_modules/`;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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. */
|
|
802
880
|
const origLocByNewLoc = /* @__PURE__ */ new Map();
|
|
881
|
+
const collisions = [];
|
|
803
882
|
for (const node of reachable) {
|
|
804
883
|
const origLoc = node.location;
|
|
805
884
|
/** The target's self-link has no place in the isolate (root IS the target). */
|
|
@@ -815,20 +894,79 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
815
894
|
* `packages/app/lib/core`) are preserved verbatim because their disk
|
|
816
895
|
* location in the isolate is unchanged.
|
|
817
896
|
*/
|
|
818
|
-
|
|
819
|
-
if (origLoc === targetImporterLoc) newLoc = "";
|
|
820
|
-
else if (origLoc.startsWith(targetNestedNodeModulesPrefix)) newLoc = origLoc.slice(targetImporterLoc.length + 1);
|
|
821
|
-
else newLoc = origLoc;
|
|
897
|
+
const newLoc = remapToOutputLoc(origLoc);
|
|
822
898
|
const srcEntry = srcPackages[origLoc];
|
|
823
899
|
if (!srcEntry) throw new Error(`Reachable node "${origLoc}" has no entry in source lockfile packages`);
|
|
824
900
|
const existing = outPackages[newLoc];
|
|
825
901
|
if (existing && !entriesAreEquivalent(existing, srcEntry)) {
|
|
826
902
|
const previousOrigLoc = origLocByNewLoc.get(newLoc) ?? "<unknown>";
|
|
827
|
-
|
|
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.`);
|
|
828
928
|
}
|
|
829
929
|
outPackages[newLoc] = { ...srcEntry };
|
|
830
930
|
origLocByNewLoc.set(newLoc, origLoc);
|
|
831
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
|
+
}
|
|
832
970
|
/**
|
|
833
971
|
* If the target importer didn't make it into the reachable set for any
|
|
834
972
|
* reason (upstream Arborist bug, programmer error), bail loudly rather
|
|
@@ -836,8 +974,10 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
836
974
|
*/
|
|
837
975
|
if (!outPackages[""]) throw new Error(`Target importer "${targetImporterLoc}" was not present in the reachable node set; cannot construct isolate root entry`);
|
|
838
976
|
/** Overlay the isolate root with the adapted target manifest. */
|
|
839
|
-
const rootEntry = {
|
|
840
|
-
|
|
977
|
+
const rootEntry = {
|
|
978
|
+
...outPackages[""],
|
|
979
|
+
name: targetPackageManifest.name
|
|
980
|
+
};
|
|
841
981
|
if (targetPackageManifest.version) rootEntry.version = targetPackageManifest.version;
|
|
842
982
|
overlayManifestDeps(rootEntry, targetPackageManifest);
|
|
843
983
|
/** The isolate is no longer a workspace root. */
|
|
@@ -875,6 +1015,55 @@ function buildIsolatedLockfileJson({ srcData, reachable, targetImporterLoc, targ
|
|
|
875
1015
|
function entriesAreEquivalent(a, b) {
|
|
876
1016
|
return a.version === b.version && a.resolved === b.resolved && a.integrity === b.integrity && !!a.link === !!b.link;
|
|
877
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
|
+
}
|
|
878
1067
|
function overlayManifestDeps(entry, manifest) {
|
|
879
1068
|
for (const field of [
|
|
880
1069
|
"dependencies",
|
|
@@ -983,6 +1172,18 @@ async function generatePnpmLockfile({ workspaceRootDir, targetPackageDir, isolat
|
|
|
983
1172
|
/** Add packageExtensionsChecksum back to the pruned lockfile if present */
|
|
984
1173
|
if (lockfile.packageExtensionsChecksum) prunedLockfile.packageExtensionsChecksum = lockfile.packageExtensionsChecksum;
|
|
985
1174
|
/**
|
|
1175
|
+
* Pruning drops the catalogs snapshot, but the isolated importers keep
|
|
1176
|
+
* their "catalog:" specifiers (for pnpm we don't resolve catalog deps in
|
|
1177
|
+
* the manifest, since the output is itself a workspace). Restore it
|
|
1178
|
+
* verbatim — like overrides above — so it stays in sync with the importer
|
|
1179
|
+
* specifiers and the preserved pnpm-workspace.yaml catalog definitions,
|
|
1180
|
+
* which are themselves copied verbatim (see issue #198). pnpm tolerates
|
|
1181
|
+
* catalog entries that no retained importer references, so there is no need
|
|
1182
|
+
* to narrow the snapshot.
|
|
1183
|
+
*/
|
|
1184
|
+
const catalogs = lockfile.catalogs;
|
|
1185
|
+
if (catalogs) prunedLockfile.catalogs = catalogs;
|
|
1186
|
+
/**
|
|
986
1187
|
* Use pre-computed patched dependencies with transformed paths. The paths
|
|
987
1188
|
* are already adapted by copyPatches to match the isolated directory
|
|
988
1189
|
* structure, preserving the original folder structure (not flattened).
|
|
@@ -996,9 +1197,9 @@ async function generatePnpmLockfile({ workspaceRootDir, targetPackageDir, isolat
|
|
|
996
1197
|
patchedDependencies
|
|
997
1198
|
});
|
|
998
1199
|
log.debug("Created lockfile at", path.join(isolateDir, "pnpm-lock.yaml"));
|
|
999
|
-
} catch (
|
|
1000
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
1001
|
-
throw
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
1202
|
+
throw error;
|
|
1002
1203
|
}
|
|
1003
1204
|
}
|
|
1004
1205
|
//#endregion
|
|
@@ -1024,9 +1225,9 @@ async function generateYarnLockfile({ workspaceRootDir, isolateDir }) {
|
|
|
1024
1225
|
log.debug(`Running local install`);
|
|
1025
1226
|
execSync(`yarn install --cwd ${isolateDir}`);
|
|
1026
1227
|
log.debug("Generated lockfile at", newLockfilePath);
|
|
1027
|
-
} catch (
|
|
1028
|
-
log.error(`Failed to generate lockfile: ${getErrorMessage(
|
|
1029
|
-
throw
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(error)}`);
|
|
1230
|
+
throw error;
|
|
1030
1231
|
}
|
|
1031
1232
|
}
|
|
1032
1233
|
//#endregion
|
|
@@ -1103,7 +1304,7 @@ async function processLockfile({ workspaceRootDir, packagesRegistry, isolateDir,
|
|
|
1103
1304
|
//#endregion
|
|
1104
1305
|
//#region src/lib/manifest/io.ts
|
|
1105
1306
|
async function readManifest(packageDir) {
|
|
1106
|
-
return readTypedJson(path.join(packageDir, "package.json"));
|
|
1307
|
+
return await readTypedJson(path.join(packageDir, "package.json"));
|
|
1107
1308
|
}
|
|
1108
1309
|
async function writeManifest(outputDir, manifest) {
|
|
1109
1310
|
await fs.writeFile(path.join(outputDir, "package.json"), JSON.stringify(manifest, null, 2));
|
|
@@ -1139,30 +1340,83 @@ function adaptManifestInternalDeps({ manifest, packagesRegistry, parentRootRelat
|
|
|
1139
1340
|
}
|
|
1140
1341
|
//#endregion
|
|
1141
1342
|
//#region src/lib/manifest/helpers/resolve-catalog-dependencies.ts
|
|
1343
|
+
const catalogSourceCache = /* @__PURE__ */ new Map();
|
|
1344
|
+
/**
|
|
1345
|
+
* Loads catalog definitions by checking pnpm-workspace.yaml first (pnpm
|
|
1346
|
+
* format), then falling back to the root package.json (Bun format).
|
|
1347
|
+
*
|
|
1348
|
+
* Pnpm defines catalogs in pnpm-workspace.yaml:
|
|
1349
|
+
*
|
|
1350
|
+
* ```yaml
|
|
1351
|
+
* catalog:
|
|
1352
|
+
* react: ^18.3.1
|
|
1353
|
+
* catalogs:
|
|
1354
|
+
* react18:
|
|
1355
|
+
* react: ^18.3.1
|
|
1356
|
+
* ```
|
|
1357
|
+
*
|
|
1358
|
+
* Bun defines catalogs in package.json (at root level or under workspaces).
|
|
1359
|
+
*/
|
|
1360
|
+
async function loadCatalogSource(workspaceRootDir) {
|
|
1361
|
+
const cached = catalogSourceCache.get(workspaceRootDir);
|
|
1362
|
+
if (cached) return cached;
|
|
1363
|
+
/**
|
|
1364
|
+
* Drop the cache entry if loading fails so a subsequent call can retry
|
|
1365
|
+
* instead of immediately rethrowing the same rejection.
|
|
1366
|
+
*/
|
|
1367
|
+
const cachedLoadPromise = (async () => {
|
|
1368
|
+
const log = useLogger();
|
|
1369
|
+
/** Try pnpm-workspace.yaml first. */
|
|
1370
|
+
const workspaceYamlPath = path.join(workspaceRootDir, "pnpm-workspace.yaml");
|
|
1371
|
+
if (await fs.pathExists(workspaceYamlPath)) try {
|
|
1372
|
+
const rawContent = await fs.readFile(workspaceYamlPath, "utf-8");
|
|
1373
|
+
const yamlConfig = yaml.parse(rawContent);
|
|
1374
|
+
if (yamlConfig?.catalog || yamlConfig?.catalogs) return {
|
|
1375
|
+
catalog: yamlConfig.catalog,
|
|
1376
|
+
catalogs: yamlConfig.catalogs
|
|
1377
|
+
};
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
log.warn(`Failed to parse ${workspaceYamlPath}: ${error instanceof Error ? error.message : String(error)}. Falling back to package.json for catalog definitions.`);
|
|
1380
|
+
}
|
|
1381
|
+
const rootManifest = await readTypedJson(path.join(workspaceRootDir, "package.json"));
|
|
1382
|
+
return {
|
|
1383
|
+
catalog: rootManifest.catalog ?? rootManifest.workspaces?.catalog,
|
|
1384
|
+
catalogs: rootManifest.catalogs ?? rootManifest.workspaces?.catalogs
|
|
1385
|
+
};
|
|
1386
|
+
})().catch((error) => {
|
|
1387
|
+
catalogSourceCache.delete(workspaceRootDir);
|
|
1388
|
+
throw error;
|
|
1389
|
+
});
|
|
1390
|
+
catalogSourceCache.set(workspaceRootDir, cachedLoadPromise);
|
|
1391
|
+
return cachedLoadPromise;
|
|
1392
|
+
}
|
|
1142
1393
|
/**
|
|
1143
1394
|
* Resolves catalog dependencies by replacing "catalog:" specifiers with their
|
|
1144
|
-
* actual versions
|
|
1395
|
+
* actual versions.
|
|
1145
1396
|
*
|
|
1146
1397
|
* Supports both pnpm and Bun catalog formats:
|
|
1147
1398
|
*
|
|
1148
|
-
* - Pnpm: catalog
|
|
1149
|
-
* - Bun: catalog or catalogs at root level, or workspaces.catalog
|
|
1399
|
+
* - Pnpm: catalog/catalogs defined in pnpm-workspace.yaml
|
|
1400
|
+
* - Bun: catalog or catalogs at root level, or workspaces.catalog in
|
|
1401
|
+
* package.json
|
|
1150
1402
|
*/
|
|
1151
1403
|
async function resolveCatalogDependencies(dependencies, workspaceRootDir) {
|
|
1152
1404
|
if (!dependencies) return;
|
|
1153
1405
|
const log = useLogger();
|
|
1154
|
-
const
|
|
1155
|
-
const flatCatalog = rootManifest.catalog || rootManifest.workspaces?.catalog;
|
|
1156
|
-
const nestedCatalogs = rootManifest.catalogs || rootManifest.workspaces?.catalogs;
|
|
1406
|
+
const { catalog: flatCatalog, catalogs: nestedCatalogs } = await loadCatalogSource(workspaceRootDir);
|
|
1157
1407
|
if (!flatCatalog && !nestedCatalogs) return dependencies;
|
|
1158
1408
|
const resolved = { ...dependencies };
|
|
1159
1409
|
for (const [packageName, specifier] of Object.entries(dependencies)) if (specifier === "catalog:" || specifier.startsWith("catalog:")) {
|
|
1160
1410
|
let catalogVersion;
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1411
|
+
const groupName = specifier === "catalog:" ? "default" : specifier.slice(8);
|
|
1412
|
+
if (groupName === "default")
|
|
1413
|
+
/**
|
|
1414
|
+
* Per pnpm semantics, `catalog:` and `catalog:default` are
|
|
1415
|
+
* equivalent: the default catalog can live under the top-level
|
|
1416
|
+
* `catalog` field or under `catalogs.default`, so check both.
|
|
1417
|
+
*/
|
|
1418
|
+
catalogVersion = flatCatalog?.[packageName] ?? nestedCatalogs?.default?.[packageName];
|
|
1419
|
+
else catalogVersion = nestedCatalogs?.[groupName]?.[packageName];
|
|
1166
1420
|
if (catalogVersion) {
|
|
1167
1421
|
log.debug(`Resolving catalog dependency ${packageName}: "${specifier}" -> "${catalogVersion}"`);
|
|
1168
1422
|
resolved[packageName] = catalogVersion;
|
|
@@ -1179,6 +1433,14 @@ async function resolveCatalogDependencies(dependencies, workspaceRootDir) {
|
|
|
1179
1433
|
*/
|
|
1180
1434
|
async function adaptInternalPackageManifests({ internalPackageNames, packagesRegistry, isolateDir, forceNpm, workspaceRootDir }) {
|
|
1181
1435
|
const packageManager = usePackageManager();
|
|
1436
|
+
/**
|
|
1437
|
+
* For PNPM (non-forceNpm) the isolated output is itself a pnpm workspace with
|
|
1438
|
+
* its `pnpm-workspace.yaml` catalog definitions preserved, so "catalog:"
|
|
1439
|
+
* specifiers are kept verbatim to stay in sync with the lockfile importers
|
|
1440
|
+
* (see issue #198). For other package managers the catalog is not available
|
|
1441
|
+
* in the output, so we resolve the specifiers to versions.
|
|
1442
|
+
*/
|
|
1443
|
+
const isPnpmWorkspaceOutput = packageManager.name === "pnpm" && !forceNpm;
|
|
1182
1444
|
await Promise.all(internalPackageNames.map(async (packageName) => {
|
|
1183
1445
|
const { manifest, rootRelativeDir } = got(packagesRegistry, packageName);
|
|
1184
1446
|
/** Dev dependencies are never included for internal deps */
|
|
@@ -1191,13 +1453,12 @@ async function adaptInternalPackageManifests({ internalPackageNames, packagesReg
|
|
|
1191
1453
|
* setup (e.g. Prisma client generation).
|
|
1192
1454
|
*/
|
|
1193
1455
|
if (strippedManifest.scripts) strippedManifest.scripts = omit(strippedManifest.scripts, ["prepare"]);
|
|
1194
|
-
|
|
1195
|
-
const manifestWithResolvedCatalogs = {
|
|
1456
|
+
const preparedManifest = isPnpmWorkspaceOutput ? strippedManifest : {
|
|
1196
1457
|
...strippedManifest,
|
|
1197
1458
|
dependencies: await resolveCatalogDependencies(strippedManifest.dependencies, workspaceRootDir)
|
|
1198
1459
|
};
|
|
1199
|
-
const outputManifest = (packageManager.name === "pnpm" || packageManager.name === "bun") && !forceNpm ?
|
|
1200
|
-
manifest:
|
|
1460
|
+
const outputManifest = (packageManager.name === "pnpm" || packageManager.name === "bun") && !forceNpm ? preparedManifest : adaptManifestInternalDeps({
|
|
1461
|
+
manifest: preparedManifest,
|
|
1201
1462
|
packagesRegistry,
|
|
1202
1463
|
parentRootRelativeDir: rootRelativeDir
|
|
1203
1464
|
});
|
|
@@ -1213,7 +1474,7 @@ async function adaptInternalPackageManifests({ internalPackageNames, packagesReg
|
|
|
1213
1474
|
*/
|
|
1214
1475
|
async function adoptPnpmFieldsFromRoot(targetPackageManifest, workspaceRootDir) {
|
|
1215
1476
|
if (isRushWorkspace(workspaceRootDir)) return targetPackageManifest;
|
|
1216
|
-
const rootPackageManifest = await readTypedJson(path
|
|
1477
|
+
const rootPackageManifest = await readTypedJson(path.join(workspaceRootDir, "package.json"));
|
|
1217
1478
|
if (usePackageManager().name === "bun") return adoptBunFieldsFromRoot(targetPackageManifest, rootPackageManifest);
|
|
1218
1479
|
return adoptPnpmFieldsOnly(targetPackageManifest, rootPackageManifest);
|
|
1219
1480
|
}
|
|
@@ -1233,7 +1494,7 @@ function adoptBunFieldsFromRoot(targetPackageManifest, rootPackageManifest) {
|
|
|
1233
1494
|
}
|
|
1234
1495
|
/** Adopt pnpm-specific fields from the root manifest */
|
|
1235
1496
|
function adoptPnpmFieldsOnly(targetPackageManifest, rootPackageManifest) {
|
|
1236
|
-
const { overrides, onlyBuiltDependencies, ignoredBuiltDependencies } = rootPackageManifest.pnpm
|
|
1497
|
+
const { overrides, onlyBuiltDependencies, ignoredBuiltDependencies } = rootPackageManifest.pnpm ?? {};
|
|
1237
1498
|
/** If no pnpm fields are present, return the original manifest */
|
|
1238
1499
|
if (!overrides && !onlyBuiltDependencies && !ignoredBuiltDependencies) return targetPackageManifest;
|
|
1239
1500
|
const pnpmConfig = {};
|
|
@@ -1259,17 +1520,25 @@ async function adaptTargetPackageManifest({ manifest, packagesRegistry, workspac
|
|
|
1259
1520
|
const { includeDevDependencies, pickFromScripts, omitFromScripts, omitPackageManager, forceNpm } = config;
|
|
1260
1521
|
/** Dev dependencies are omitted by default */
|
|
1261
1522
|
const inputManifest = includeDevDependencies ? manifest : omit(manifest, ["devDependencies"]);
|
|
1262
|
-
|
|
1263
|
-
const manifestWithResolvedCatalogs = {
|
|
1523
|
+
const preparedManifest = packageManager.name === "pnpm" && !forceNpm ? inputManifest : {
|
|
1264
1524
|
...inputManifest,
|
|
1265
1525
|
dependencies: await resolveCatalogDependencies(inputManifest.dependencies, workspaceRootDir)
|
|
1266
1526
|
};
|
|
1267
1527
|
return {
|
|
1268
|
-
...(packageManager.name === "pnpm" || packageManager.name === "bun") && !forceNpm ? await adoptPnpmFieldsFromRoot(
|
|
1269
|
-
manifest:
|
|
1528
|
+
...(packageManager.name === "pnpm" || packageManager.name === "bun") && !forceNpm ? await adoptPnpmFieldsFromRoot(preparedManifest, workspaceRootDir) : adaptManifestInternalDeps({
|
|
1529
|
+
manifest: preparedManifest,
|
|
1270
1530
|
packagesRegistry
|
|
1271
1531
|
}),
|
|
1532
|
+
/**
|
|
1533
|
+
* Adopt the package manager definition from the root manifest if available.
|
|
1534
|
+
* The option to omit is there because some platforms might not handle it
|
|
1535
|
+
* properly (Cloud Run, April 24th 2024, does not handle pnpm v9)
|
|
1536
|
+
*/
|
|
1272
1537
|
packageManager: omitPackageManager ? void 0 : packageManager.packageManagerString,
|
|
1538
|
+
/**
|
|
1539
|
+
* Scripts are removed by default if not explicitly picked or omitted via
|
|
1540
|
+
* config.
|
|
1541
|
+
*/
|
|
1273
1542
|
scripts: pickFromScripts ? pick(manifest.scripts ?? {}, pickFromScripts) : omitFromScripts ? omit(manifest.scripts ?? {}, omitFromScripts) : {}
|
|
1274
1543
|
};
|
|
1275
1544
|
}
|
|
@@ -1300,8 +1569,8 @@ function validateManifestMandatoryFields(manifest, packagePath, requireFilesFiel
|
|
|
1300
1569
|
* packed
|
|
1301
1570
|
*/
|
|
1302
1571
|
if (requireFilesField && (!manifest.files || !Array.isArray(manifest.files) || manifest.files.length === 0)) missingFields.push("files");
|
|
1303
|
-
|
|
1304
|
-
|
|
1572
|
+
const [field] = missingFields;
|
|
1573
|
+
if (field) {
|
|
1305
1574
|
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`;
|
|
1306
1575
|
log.error(errorMessage);
|
|
1307
1576
|
throw new Error(errorMessage);
|
|
@@ -1310,7 +1579,7 @@ function validateManifestMandatoryFields(manifest, packagePath, requireFilesFiel
|
|
|
1310
1579
|
}
|
|
1311
1580
|
//#endregion
|
|
1312
1581
|
//#region src/lib/output/get-build-output-dir.ts
|
|
1313
|
-
|
|
1582
|
+
function getBuildOutputDir({ targetPackageDir, buildDirName, tsconfigPath }) {
|
|
1314
1583
|
const log = useLogger();
|
|
1315
1584
|
if (buildDirName) {
|
|
1316
1585
|
log.debug("Using buildDirName from config:", buildDirName);
|
|
@@ -1333,6 +1602,91 @@ async function getBuildOutputDir({ targetPackageDir, buildDirName, tsconfigPath
|
|
|
1333
1602
|
}
|
|
1334
1603
|
}
|
|
1335
1604
|
//#endregion
|
|
1605
|
+
//#region src/lib/utils/wait-for-complete-file.ts
|
|
1606
|
+
/**
|
|
1607
|
+
* Wait until the given file exists and its size has stopped changing across
|
|
1608
|
+
* two consecutive polls. Resolves once the file is considered fully written,
|
|
1609
|
+
* rejects with a timeout error otherwise.
|
|
1610
|
+
*
|
|
1611
|
+
* This is a cheap proxy for "the writer has finished flushing" without
|
|
1612
|
+
* inspecting file contents or relying on platform-specific signals. It is
|
|
1613
|
+
* intended for cases where an external process (e.g. `pnpm pack`) may report
|
|
1614
|
+
* completion before its output is fully visible on disk.
|
|
1615
|
+
*/
|
|
1616
|
+
async function waitForCompleteFile(filePath, { timeoutMs, pollMs }) {
|
|
1617
|
+
const deadline = Date.now() + timeoutMs;
|
|
1618
|
+
let lastSize = -1;
|
|
1619
|
+
while (Date.now() < deadline) {
|
|
1620
|
+
try {
|
|
1621
|
+
const { size } = await fs$1.promises.stat(filePath);
|
|
1622
|
+
if (size > 0 && size === lastSize) return;
|
|
1623
|
+
lastSize = size;
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
if (error.code !== "ENOENT") throw error;
|
|
1626
|
+
}
|
|
1627
|
+
await new Promise((resolve) => {
|
|
1628
|
+
setTimeout(resolve, pollMs);
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for file to be written: ${filePath}`);
|
|
1632
|
+
}
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/lib/utils/pack.ts
|
|
1635
|
+
/**
|
|
1636
|
+
* How long to wait for the packed tarball to appear and stop growing on disk
|
|
1637
|
+
* after `pnpm pack` / `npm pack` has exited.
|
|
1638
|
+
*/
|
|
1639
|
+
const PACK_FILE_READY_TIMEOUT_MS = 5e3;
|
|
1640
|
+
const PACK_FILE_READY_POLL_MS = 50;
|
|
1641
|
+
async function pack(srcDir, dstDir) {
|
|
1642
|
+
const log = useLogger();
|
|
1643
|
+
const execOptions = {
|
|
1644
|
+
cwd: srcDir,
|
|
1645
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1646
|
+
};
|
|
1647
|
+
/**
|
|
1648
|
+
* PNPM pack seems to be a lot faster than NPM pack, so when PNPM is detected
|
|
1649
|
+
* we use that instead.
|
|
1650
|
+
*/
|
|
1651
|
+
const packStdout = shouldUsePnpmPack() ? await new Promise((resolve, reject) => {
|
|
1652
|
+
exec(`pnpm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout) => {
|
|
1653
|
+
if (err) {
|
|
1654
|
+
log.error(getErrorMessage(err));
|
|
1655
|
+
reject(err);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
resolve(stdout);
|
|
1659
|
+
});
|
|
1660
|
+
}) : await new Promise((resolve, reject) => {
|
|
1661
|
+
exec(`npm pack --pack-destination "${dstDir}"`, execOptions, (err, stdout) => {
|
|
1662
|
+
if (err) {
|
|
1663
|
+
reject(err);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
resolve(stdout);
|
|
1667
|
+
});
|
|
1668
|
+
});
|
|
1669
|
+
const lastLine = packStdout.trim().split("\n").at(-1);
|
|
1670
|
+
assert(lastLine, `Failed to parse last line from stdout: ${packStdout.trim()}`);
|
|
1671
|
+
const fileName = path.basename(lastLine);
|
|
1672
|
+
assert(fileName, `Failed to parse file name from: ${lastLine}`);
|
|
1673
|
+
const filePath = path.join(dstDir, fileName);
|
|
1674
|
+
/**
|
|
1675
|
+
* `pnpm pack` (and occasionally `npm pack`) can return before the tarball is
|
|
1676
|
+
* fully visible/flushed to disk. A naive `existsSync` check is not enough:
|
|
1677
|
+
* the directory entry can appear before the file's data has been written,
|
|
1678
|
+
* which causes downstream consumers (gunzip + tar) to fail with
|
|
1679
|
+
* "unexpected end of file". Wait until the file exists and its size has
|
|
1680
|
+
* stopped changing across two consecutive polls before returning.
|
|
1681
|
+
*/
|
|
1682
|
+
await waitForCompleteFile(filePath, {
|
|
1683
|
+
timeoutMs: PACK_FILE_READY_TIMEOUT_MS,
|
|
1684
|
+
pollMs: PACK_FILE_READY_POLL_MS
|
|
1685
|
+
});
|
|
1686
|
+
log.debug(`Packed (temp)/${fileName}`);
|
|
1687
|
+
return filePath;
|
|
1688
|
+
}
|
|
1689
|
+
//#endregion
|
|
1336
1690
|
//#region src/lib/output/pack-dependencies.ts
|
|
1337
1691
|
/**
|
|
1338
1692
|
* Pack dependencies so that we extract only the files that are supposed to be
|
|
@@ -1361,18 +1715,13 @@ async function packDependencies({ packagesRegistry, internalPackageNames, packDe
|
|
|
1361
1715
|
}
|
|
1362
1716
|
//#endregion
|
|
1363
1717
|
//#region src/lib/output/process-build-output-files.ts
|
|
1364
|
-
const TIMEOUT_MS = 5e3;
|
|
1365
1718
|
async function processBuildOutputFiles({ targetPackageDir, tmpDir, isolateDir }) {
|
|
1366
|
-
const log = useLogger();
|
|
1367
1719
|
const packedFilePath = await pack(targetPackageDir, tmpDir);
|
|
1368
1720
|
const unpackDir = path.join(tmpDir, "target");
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
isWaitingYet = true;
|
|
1374
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1375
|
-
}
|
|
1721
|
+
/**
|
|
1722
|
+
* `pack` already waits for the tarball to be fully written before returning,
|
|
1723
|
+
* so it is safe to unpack immediately.
|
|
1724
|
+
*/
|
|
1376
1725
|
await unpack(packedFilePath, unpackDir);
|
|
1377
1726
|
await fs.copy(path.join(unpackDir, "package"), isolateDir);
|
|
1378
1727
|
}
|
|
@@ -1445,7 +1794,8 @@ function collectReachablePackageNames({ targetPackageManifest, packagesRegistry,
|
|
|
1445
1794
|
*/
|
|
1446
1795
|
function findPackagesGlobs(workspaceRootDir) {
|
|
1447
1796
|
const log = useLogger();
|
|
1448
|
-
|
|
1797
|
+
const packageManager = usePackageManager();
|
|
1798
|
+
switch (packageManager.name) {
|
|
1449
1799
|
case "pnpm": {
|
|
1450
1800
|
const workspaceConfig = readTypedYamlSync(path.join(workspaceRootDir, "pnpm-workspace.yaml"));
|
|
1451
1801
|
if (!workspaceConfig) throw new Error("pnpm-workspace.yaml file is empty. Please specify packages configuration.");
|
|
@@ -1461,17 +1811,16 @@ function findPackagesGlobs(workspaceRootDir) {
|
|
|
1461
1811
|
const { workspaces } = readTypedJsonSync(workspaceRootManifestPath);
|
|
1462
1812
|
if (!workspaces) throw new Error(`No workspaces field found in ${workspaceRootManifestPath}`);
|
|
1463
1813
|
if (Array.isArray(workspaces)) return workspaces;
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
return workspacesObject.packages;
|
|
1473
|
-
}
|
|
1814
|
+
/**
|
|
1815
|
+
* For Yarn, workspaces could be defined as an object with { packages: [],
|
|
1816
|
+
* nohoist: [] }. See
|
|
1817
|
+
* https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
|
|
1818
|
+
*/
|
|
1819
|
+
const workspacesObject = workspaces;
|
|
1820
|
+
assert(Array.isArray(workspacesObject.packages), "workspaces.packages must be an array");
|
|
1821
|
+
return workspacesObject.packages;
|
|
1474
1822
|
}
|
|
1823
|
+
default: throw new Error(`Unsupported package manager: ${packageManager.name}`);
|
|
1475
1824
|
}
|
|
1476
1825
|
}
|
|
1477
1826
|
//#endregion
|
|
@@ -1490,15 +1839,14 @@ async function createPackagesRegistry(workspaceRootDir, workspacePackagesOverrid
|
|
|
1490
1839
|
const manifestPath = path.join(absoluteDir, "package.json");
|
|
1491
1840
|
if (!fs.existsSync(manifestPath)) {
|
|
1492
1841
|
log.warn(`Ignoring directory ${rootRelativeDir} because it does not contain a package.json file`);
|
|
1493
|
-
return;
|
|
1494
|
-
} else {
|
|
1495
|
-
log.debug(`Registering package ${rootRelativeDir}`);
|
|
1496
|
-
return {
|
|
1497
|
-
manifest: await readTypedJson(path.join(absoluteDir, "package.json")),
|
|
1498
|
-
rootRelativeDir,
|
|
1499
|
-
absoluteDir
|
|
1500
|
-
};
|
|
1842
|
+
return null;
|
|
1501
1843
|
}
|
|
1844
|
+
log.debug(`Registering package ${rootRelativeDir}`);
|
|
1845
|
+
return {
|
|
1846
|
+
manifest: await readTypedJson(path.join(absoluteDir, "package.json")),
|
|
1847
|
+
rootRelativeDir,
|
|
1848
|
+
absoluteDir
|
|
1849
|
+
};
|
|
1502
1850
|
}))).reduce((acc, info) => {
|
|
1503
1851
|
if (info) acc[info.manifest.name] = info;
|
|
1504
1852
|
return acc;
|
|
@@ -1553,10 +1901,252 @@ function listInternalPackages(manifest, packagesRegistry, { includeDevDependenci
|
|
|
1553
1901
|
return [...new Set(result)];
|
|
1554
1902
|
}
|
|
1555
1903
|
//#endregion
|
|
1904
|
+
//#region src/lib/patches/collect-installed-names-bun.ts
|
|
1905
|
+
/**
|
|
1906
|
+
* Walk the workspace bun.lock starting from the target package and its
|
|
1907
|
+
* internal workspace dependencies, returning the set of every package name
|
|
1908
|
+
* that will end up installed in the isolate (including deep
|
|
1909
|
+
* external-to-external transitives).
|
|
1910
|
+
*
|
|
1911
|
+
* Used by `copyPatches` to preserve patches for transitive deps that aren't
|
|
1912
|
+
* directly listed on any internal manifest. Returns an empty set on any
|
|
1913
|
+
* failure so the caller falls back to manifest-based reachability.
|
|
1914
|
+
*/
|
|
1915
|
+
function collectInstalledNamesFromBunLockfile({ workspaceRootDir, targetPackageDir, internalDepPackageNames, packagesRegistry, includeDevDependencies }) {
|
|
1916
|
+
const log = useLogger();
|
|
1917
|
+
try {
|
|
1918
|
+
const lockfilePath = path.join(workspaceRootDir, "bun.lock");
|
|
1919
|
+
if (!fs.existsSync(lockfilePath)) {
|
|
1920
|
+
log.debug("No bun.lock available for installed-names walk");
|
|
1921
|
+
return /* @__PURE__ */ new Set();
|
|
1922
|
+
}
|
|
1923
|
+
const lockfile = readTypedJsonSync(lockfilePath);
|
|
1924
|
+
const targetWorkspaceKey = path.relative(workspaceRootDir, targetPackageDir).split(path.sep).join(path.posix.sep);
|
|
1925
|
+
const internalWorkspaceKeys = internalDepPackageNames.map((name) => {
|
|
1926
|
+
const pkg = packagesRegistry[name];
|
|
1927
|
+
if (!pkg) return null;
|
|
1928
|
+
return pkg.rootRelativeDir.split(path.sep).join(path.posix.sep);
|
|
1929
|
+
}).filter(Boolean);
|
|
1930
|
+
const directDependencyNames = /* @__PURE__ */ new Set();
|
|
1931
|
+
const targetEntry = lockfile.workspaces[targetWorkspaceKey];
|
|
1932
|
+
if (targetEntry) for (const name of collectDependencyNames(targetEntry, includeDevDependencies)) directDependencyNames.add(name);
|
|
1933
|
+
for (const workspaceKey of internalWorkspaceKeys) {
|
|
1934
|
+
const entry = lockfile.workspaces[workspaceKey];
|
|
1935
|
+
if (!entry) continue;
|
|
1936
|
+
/** Internal workspace deps never bring in their devDependencies */
|
|
1937
|
+
for (const name of collectDependencyNames(entry, false)) directDependencyNames.add(name);
|
|
1938
|
+
}
|
|
1939
|
+
return collectRequiredPackages(directDependencyNames, lockfile.packages);
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
log.debug(`Failed to walk bun.lock for installed names: ${error instanceof Error ? error.message : String(error)}`);
|
|
1942
|
+
return /* @__PURE__ */ new Set();
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
//#endregion
|
|
1946
|
+
//#region src/lib/patches/collect-installed-names-pnpm.ts
|
|
1947
|
+
/**
|
|
1948
|
+
* Walk the workspace pnpm lockfile starting from the target package and its
|
|
1949
|
+
* internal workspace dependencies, returning the set of every package name
|
|
1950
|
+
* that will end up installed in the isolate (including deep
|
|
1951
|
+
* external-to-external transitives).
|
|
1952
|
+
*
|
|
1953
|
+
* Used by `copyPatches` to preserve patches for transitive deps that aren't
|
|
1954
|
+
* directly listed on any internal manifest. Returns an empty set on any
|
|
1955
|
+
* failure so the caller falls back to manifest-based reachability. When the
|
|
1956
|
+
* lockfile is present but lacks a `packages` section, returns just the
|
|
1957
|
+
* direct importer dep names.
|
|
1958
|
+
*/
|
|
1959
|
+
async function collectInstalledNamesFromPnpmLockfile({ workspaceRootDir, targetPackageDir, internalDepPackageNames, packagesRegistry, majorVersion, includeDevDependencies }) {
|
|
1960
|
+
const log = useLogger();
|
|
1961
|
+
try {
|
|
1962
|
+
const useVersion9 = majorVersion >= 9;
|
|
1963
|
+
const isRush = isRushWorkspace(workspaceRootDir);
|
|
1964
|
+
const lockfileDir = isRush ? path.join(workspaceRootDir, "common/config/rush") : workspaceRootDir;
|
|
1965
|
+
const lockfile = useVersion9 ? await readWantedLockfile$1(lockfileDir, { ignoreIncompatible: false }) : await readWantedLockfile(lockfileDir, { ignoreIncompatible: false });
|
|
1966
|
+
if (!lockfile) {
|
|
1967
|
+
log.debug("No pnpm lockfile available for installed-names walk");
|
|
1968
|
+
return /* @__PURE__ */ new Set();
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Normalize separators to POSIX so Windows callers match the lockfile's
|
|
1972
|
+
* importer keys (mirrors generate-pnpm-lockfile.ts). Applied once here so
|
|
1973
|
+
* the `isTarget` equality check below compares apples-to-apples — without
|
|
1974
|
+
* this, on Windows the raw id with backslashes wouldn't match the
|
|
1975
|
+
* normalized id used as the importers map key.
|
|
1976
|
+
*/
|
|
1977
|
+
const targetImporterId = toLockfileImporterKey(useVersion9 ? getLockfileImporterId$1(workspaceRootDir, targetPackageDir) : getLockfileImporterId(workspaceRootDir, targetPackageDir), isRush);
|
|
1978
|
+
const importerIds = [targetImporterId, ...internalDepPackageNames.map((name) => packagesRegistry[name]?.rootRelativeDir).filter(Boolean).map((dir) => toLockfileImporterKey(dir, isRush))];
|
|
1979
|
+
const packages = lockfile.packages;
|
|
1980
|
+
if (!packages) {
|
|
1981
|
+
log.debug("Lockfile has no packages section to walk");
|
|
1982
|
+
return collectImporterDirectNames(lockfile.importers, importerIds, targetImporterId, includeDevDependencies);
|
|
1983
|
+
}
|
|
1984
|
+
const names = /* @__PURE__ */ new Set();
|
|
1985
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1986
|
+
const queue = [];
|
|
1987
|
+
for (const importerId of importerIds) {
|
|
1988
|
+
const importer = lockfile.importers[importerId];
|
|
1989
|
+
if (!importer) continue;
|
|
1990
|
+
enqueueImporterDeps({
|
|
1991
|
+
importer,
|
|
1992
|
+
names,
|
|
1993
|
+
queue,
|
|
1994
|
+
useVersion9,
|
|
1995
|
+
includeDevDependencies: importerId === targetImporterId && includeDevDependencies
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
let depPath;
|
|
1999
|
+
while ((depPath = queue.pop()) !== void 0) {
|
|
2000
|
+
if (seen.has(depPath)) continue;
|
|
2001
|
+
seen.add(depPath);
|
|
2002
|
+
names.add(extractPackageName(depPath));
|
|
2003
|
+
const pkg = packages[depPath];
|
|
2004
|
+
if (!pkg) continue;
|
|
2005
|
+
enqueueResolvedDeps(pkg.dependencies, names, queue, useVersion9, seen);
|
|
2006
|
+
enqueueResolvedDeps(pkg.optionalDependencies, names, queue, useVersion9, seen);
|
|
2007
|
+
/**
|
|
2008
|
+
* Peer requirement values are name → semver-range, not resolved depPaths.
|
|
2009
|
+
* Just record the names so a patch on a peer-only external transitive
|
|
2010
|
+
* survives filtering (mirrors the bun walker and the sister manifest
|
|
2011
|
+
* walker, which both include peerDependencies).
|
|
2012
|
+
*/
|
|
2013
|
+
collectNames(pkg.peerDependencies, names);
|
|
2014
|
+
}
|
|
2015
|
+
return names;
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
log.debug(`Failed to walk pnpm lockfile for installed names: ${error instanceof Error ? error.message : String(error)}`);
|
|
2018
|
+
return /* @__PURE__ */ new Set();
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Convert a raw importer id (as returned by `getLockfileImporterId` or a
|
|
2023
|
+
* package's rootRelativeDir) to the form actually used as a key in
|
|
2024
|
+
* `lockfile.importers`: POSIX separators, with the Rush `../../` prefix when
|
|
2025
|
+
* the workspace lives under `common/config/rush`. Lockfile keys are always
|
|
2026
|
+
* POSIX regardless of the host OS, so backslashes are normalized
|
|
2027
|
+
* unconditionally rather than relying on `path.sep`.
|
|
2028
|
+
*/
|
|
2029
|
+
function toLockfileImporterKey(importerId, isRush) {
|
|
2030
|
+
const posix = importerId.split(path.sep).join(path.posix.sep).replace(/\\/g, "/");
|
|
2031
|
+
return isRush ? `../../${posix}` : posix;
|
|
2032
|
+
}
|
|
2033
|
+
function enqueueImporterDeps({ importer, names, queue, useVersion9, includeDevDependencies }) {
|
|
2034
|
+
enqueueResolvedDeps(importer.dependencies, names, queue, useVersion9);
|
|
2035
|
+
enqueueResolvedDeps(importer.optionalDependencies, names, queue, useVersion9);
|
|
2036
|
+
if (includeDevDependencies) enqueueResolvedDeps(importer.devDependencies, names, queue, useVersion9);
|
|
2037
|
+
/**
|
|
2038
|
+
* Importer peerDependencies usually aren't a separate map in the lockfile
|
|
2039
|
+
* (autoInstallPeers folds them into `dependencies`), but record names if
|
|
2040
|
+
* they happen to be present.
|
|
2041
|
+
*/
|
|
2042
|
+
collectNames(importer.peerDependencies, names);
|
|
2043
|
+
}
|
|
2044
|
+
function enqueueResolvedDeps(deps, names, queue, useVersion9, seen) {
|
|
2045
|
+
if (!deps) return;
|
|
2046
|
+
for (const [alias, ref] of Object.entries(deps)) {
|
|
2047
|
+
/**
|
|
2048
|
+
* The alias is the name as listed in the parent's dependencies map. For
|
|
2049
|
+
* non-aliased installs this is also the resolved package name. We add it
|
|
2050
|
+
* to the set as a candidate name; visiting the actual depPath below
|
|
2051
|
+
* refines this with the true installed name.
|
|
2052
|
+
*/
|
|
2053
|
+
names.add(alias);
|
|
2054
|
+
const depPath = refToRelative(ref, alias, useVersion9);
|
|
2055
|
+
if (depPath && !seen?.has(depPath)) queue.push(depPath);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
function collectNames(deps, names) {
|
|
2059
|
+
if (!deps) return;
|
|
2060
|
+
for (const name of Object.keys(deps)) names.add(name);
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Mirrors `@pnpm/dependency-path`'s `refToRelative`. The depPath shape differs
|
|
2064
|
+
* between pnpm 8 (lockfile v6, normalized to v5 keys like `/foo/1.0.0`) and
|
|
2065
|
+
* pnpm 9 (lockfile v9 keys like `foo@1.0.0`). Returns the depPath used as a
|
|
2066
|
+
* key in `lockfile.packages`, or null if the ref points to a workspace link.
|
|
2067
|
+
*/
|
|
2068
|
+
function refToRelative(reference, pkgName, useVersion9) {
|
|
2069
|
+
if (!reference) return null;
|
|
2070
|
+
if (reference.startsWith("link:")) return null;
|
|
2071
|
+
return useVersion9 ? refToRelativeV9(reference, pkgName) : refToRelativeV8(reference, pkgName);
|
|
2072
|
+
}
|
|
2073
|
+
function refToRelativeV9(reference, pkgName) {
|
|
2074
|
+
if (reference.startsWith("@")) return reference;
|
|
2075
|
+
const atIndex = reference.indexOf("@");
|
|
2076
|
+
if (atIndex === -1) return `${pkgName}@${reference}`;
|
|
2077
|
+
const colonIndex = reference.indexOf(":");
|
|
2078
|
+
const bracketIndex = reference.indexOf("(");
|
|
2079
|
+
if ((colonIndex === -1 || atIndex < colonIndex) && (bracketIndex === -1 || atIndex < bracketIndex)) return reference;
|
|
2080
|
+
return `${pkgName}@${reference}`;
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* v8 form: pnpm 8 (lockfile v6) is normalized on read to v5-style depPaths
|
|
2084
|
+
* with leading slash and `/` separator between name and version. Plain
|
|
2085
|
+
* version refs build that key; refs already containing a `/` (peer-suffixed
|
|
2086
|
+
* or pre-formed) are returned verbatim. Mirrors `@pnpm/dependency-path@2.x`.
|
|
2087
|
+
*/
|
|
2088
|
+
function refToRelativeV8(reference, pkgName) {
|
|
2089
|
+
if (reference.startsWith("file:")) return reference;
|
|
2090
|
+
const slashIndex = reference.indexOf("/");
|
|
2091
|
+
const bracketIndex = reference.indexOf("(");
|
|
2092
|
+
const noSlashBeforeBracket = bracketIndex !== -1 && reference.lastIndexOf("/", bracketIndex) === -1;
|
|
2093
|
+
if (slashIndex === -1 || noSlashBeforeBracket) return `/${pkgName}/${reference}`;
|
|
2094
|
+
return reference;
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Extract the bare package name from a pnpm depPath. Strips the optional
|
|
2098
|
+
* peer-resolution suffix (e.g. `(react@18.0.0)`) before parsing. Handles
|
|
2099
|
+
* both v9 (`@scope/foo@1.0.0`) and v8 (`/@scope/foo/1.0.0`) shapes.
|
|
2100
|
+
*/
|
|
2101
|
+
function extractPackageName(depPath) {
|
|
2102
|
+
const peerStart = indexOfPeersSuffix(depPath);
|
|
2103
|
+
const trimmed = peerStart === -1 ? depPath : depPath.slice(0, peerStart);
|
|
2104
|
+
if (trimmed.startsWith("/")) {
|
|
2105
|
+
/** v8 v5-style: `/<name>/<version>` */
|
|
2106
|
+
const stripped = trimmed.slice(1);
|
|
2107
|
+
if (stripped.startsWith("@")) {
|
|
2108
|
+
const secondSlash = stripped.indexOf("/", stripped.indexOf("/") + 1);
|
|
2109
|
+
return secondSlash === -1 ? stripped : stripped.slice(0, secondSlash);
|
|
2110
|
+
}
|
|
2111
|
+
const firstSlash = stripped.indexOf("/");
|
|
2112
|
+
return firstSlash === -1 ? stripped : stripped.slice(0, firstSlash);
|
|
2113
|
+
}
|
|
2114
|
+
return getPackageName(trimmed);
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Mirrors `@pnpm/dependency-path`'s `indexOfPeersSuffix`. Returns the index
|
|
2118
|
+
* where the peer-resolution suffix starts, or -1 if there is none.
|
|
2119
|
+
*/
|
|
2120
|
+
function indexOfPeersSuffix(depPath) {
|
|
2121
|
+
if (!depPath.endsWith(")")) return -1;
|
|
2122
|
+
let open = 1;
|
|
2123
|
+
for (let i = depPath.length - 2; i >= 0; i--) if (depPath[i] === "(") open--;
|
|
2124
|
+
else if (depPath[i] === ")") open++;
|
|
2125
|
+
else if (!open) return i + 1;
|
|
2126
|
+
return -1;
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Fallback when the lockfile is missing `packages`: just return importer
|
|
2130
|
+
* direct dep names so we at least cover some of the graph.
|
|
2131
|
+
*/
|
|
2132
|
+
function collectImporterDirectNames(importers, importerIds, targetImporterId, includeDevDependencies) {
|
|
2133
|
+
const names = /* @__PURE__ */ new Set();
|
|
2134
|
+
for (const importerId of importerIds) {
|
|
2135
|
+
const importer = importers[importerId];
|
|
2136
|
+
if (!importer) continue;
|
|
2137
|
+
const isTarget = importerId === targetImporterId;
|
|
2138
|
+
for (const name of Object.keys(importer.dependencies ?? {})) names.add(name);
|
|
2139
|
+
for (const name of Object.keys(importer.optionalDependencies ?? {})) names.add(name);
|
|
2140
|
+
for (const name of Object.keys(importer.peerDependencies ?? {})) names.add(name);
|
|
2141
|
+
if (isTarget && includeDevDependencies) for (const name of Object.keys(importer.devDependencies ?? {})) names.add(name);
|
|
2142
|
+
}
|
|
2143
|
+
return names;
|
|
2144
|
+
}
|
|
2145
|
+
//#endregion
|
|
1556
2146
|
//#region src/lib/patches/copy-patches.ts
|
|
1557
|
-
async function copyPatches({ workspaceRootDir, targetPackageManifest, packagesRegistry, isolateDir, includeDevDependencies }) {
|
|
2147
|
+
async function copyPatches({ workspaceRootDir, targetPackageDir, targetPackageManifest, packagesRegistry, internalDepPackageNames, isolateDir, includeDevDependencies }) {
|
|
1558
2148
|
const log = useLogger();
|
|
1559
|
-
const { name: packageManagerName } = usePackageManager();
|
|
2149
|
+
const { name: packageManagerName, majorVersion } = usePackageManager();
|
|
1560
2150
|
let patchedDependencies;
|
|
1561
2151
|
/**
|
|
1562
2152
|
* Only try reading pnpm-workspace.yaml for pnpm workspaces. Bun workspaces
|
|
@@ -1594,6 +2184,28 @@ async function copyPatches({ workspaceRootDir, targetPackageManifest, packagesRe
|
|
|
1594
2184
|
packagesRegistry,
|
|
1595
2185
|
includeDevDependencies
|
|
1596
2186
|
});
|
|
2187
|
+
/**
|
|
2188
|
+
* Manifest-based reachability misses external→external transitives because
|
|
2189
|
+
* external manifests aren't loaded here. Walk the package-manager's
|
|
2190
|
+
* lockfile to also pick up those names, so a patch for a deeply-nested
|
|
2191
|
+
* external dep (e.g. `@react-pdf/render` reached via `@react-pdf/renderer`)
|
|
2192
|
+
* survives isolation.
|
|
2193
|
+
*/
|
|
2194
|
+
const lockfileInstalledNames = packageManagerName === "pnpm" ? await collectInstalledNamesFromPnpmLockfile({
|
|
2195
|
+
workspaceRootDir,
|
|
2196
|
+
targetPackageDir,
|
|
2197
|
+
internalDepPackageNames,
|
|
2198
|
+
packagesRegistry,
|
|
2199
|
+
majorVersion,
|
|
2200
|
+
includeDevDependencies
|
|
2201
|
+
}) : packageManagerName === "bun" ? collectInstalledNamesFromBunLockfile({
|
|
2202
|
+
workspaceRootDir,
|
|
2203
|
+
targetPackageDir,
|
|
2204
|
+
internalDepPackageNames,
|
|
2205
|
+
packagesRegistry,
|
|
2206
|
+
includeDevDependencies
|
|
2207
|
+
}) : /* @__PURE__ */ new Set();
|
|
2208
|
+
for (const name of lockfileInstalledNames) reachableDependencyNames.add(name);
|
|
1597
2209
|
const filteredPatches = filterPatchedDependencies({
|
|
1598
2210
|
patchedDependencies,
|
|
1599
2211
|
targetPackageManifest,
|
|
@@ -1698,16 +2310,16 @@ function writeIsolatePnpmWorkspace({ workspaceRootDir, isolateDir, copiedPatches
|
|
|
1698
2310
|
//#endregion
|
|
1699
2311
|
//#region src/isolate.ts
|
|
1700
2312
|
const __dirname = getDirname(import.meta.url);
|
|
1701
|
-
function createIsolator(
|
|
1702
|
-
const resolvedConfig = resolveConfig(
|
|
1703
|
-
return async function
|
|
2313
|
+
function createIsolator(initialConfig) {
|
|
2314
|
+
const resolvedConfig = resolveConfig(initialConfig);
|
|
2315
|
+
return async function runIsolate() {
|
|
1704
2316
|
const config = resolvedConfig;
|
|
1705
2317
|
setLogLevel(config.logLevel);
|
|
1706
2318
|
const log = useLogger();
|
|
1707
2319
|
const { version: libraryVersion } = await readTypedJson(path.join(path.join(__dirname, "..", "package.json")));
|
|
1708
2320
|
log.debug("Using isolate-package version", libraryVersion);
|
|
1709
2321
|
const { targetPackageDir, workspaceRootDir } = resolveWorkspacePaths(config);
|
|
1710
|
-
const buildOutputDir =
|
|
2322
|
+
const buildOutputDir = getBuildOutputDir({
|
|
1711
2323
|
targetPackageDir,
|
|
1712
2324
|
buildDirName: config.buildDirName,
|
|
1713
2325
|
tsconfigPath: config.tsconfigPath
|
|
@@ -1717,11 +2329,14 @@ function createIsolator(config) {
|
|
|
1717
2329
|
log.debug("Isolate target package", getRootRelativeLogPath(targetPackageDir, workspaceRootDir));
|
|
1718
2330
|
const isolateDir = path.join(targetPackageDir, config.isolateDirName);
|
|
1719
2331
|
log.debug("Isolate output directory", getRootRelativeLogPath(isolateDir, workspaceRootDir));
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
2332
|
+
/**
|
|
2333
|
+
* Place the trash sibling outside `targetPackageDir`, since that directory
|
|
2334
|
+
* is later packed by `processBuildOutputFiles`. Keeping the trash next to
|
|
2335
|
+
* the target package (still on the same filesystem) preserves the atomic
|
|
2336
|
+
* rename without risking that the trash gets picked up by `npm pack` or
|
|
2337
|
+
* races with that step.
|
|
2338
|
+
*/
|
|
2339
|
+
await resetIsolateDir(isolateDir, { trashParentDir: path.dirname(targetPackageDir) });
|
|
1725
2340
|
const tmpDir = path.join(isolateDir, "__tmp");
|
|
1726
2341
|
await fs.ensureDir(tmpDir);
|
|
1727
2342
|
const targetPackageManifest = await readTypedJson(path.join(targetPackageDir, "package.json"));
|
|
@@ -1794,8 +2409,10 @@ function createIsolator(config) {
|
|
|
1794
2409
|
await writeManifest(isolateDir, outputManifest);
|
|
1795
2410
|
const copiedPatches = (packageManager.name === "pnpm" || packageManager.name === "bun") && !config.forceNpm ? await copyPatches({
|
|
1796
2411
|
workspaceRootDir,
|
|
2412
|
+
targetPackageDir,
|
|
1797
2413
|
targetPackageManifest: outputManifest,
|
|
1798
2414
|
packagesRegistry,
|
|
2415
|
+
internalDepPackageNames: internalPackageNames,
|
|
1799
2416
|
isolateDir,
|
|
1800
2417
|
includeDevDependencies: config.includeDevDependencies
|
|
1801
2418
|
}) : {};
|
|
@@ -1824,7 +2441,7 @@ function createIsolator(config) {
|
|
|
1824
2441
|
const patchEntries = Object.fromEntries(Object.entries(copiedPatches).map(([spec, patchFile]) => [spec, patchFile.path]));
|
|
1825
2442
|
if (packageManager.name === "bun") manifest.patchedDependencies = patchEntries;
|
|
1826
2443
|
else {
|
|
1827
|
-
|
|
2444
|
+
manifest.pnpm ??= {};
|
|
1828
2445
|
manifest.pnpm.patchedDependencies = patchEntries;
|
|
1829
2446
|
}
|
|
1830
2447
|
log.debug(`Added ${Object.keys(copiedPatches).length} patches to isolated package.json`);
|
|
@@ -1891,6 +2508,6 @@ async function isolate(config) {
|
|
|
1891
2508
|
return createIsolator(config)();
|
|
1892
2509
|
}
|
|
1893
2510
|
//#endregion
|
|
1894
|
-
export {
|
|
2511
|
+
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 };
|
|
1895
2512
|
|
|
1896
|
-
//# sourceMappingURL=isolate-
|
|
2513
|
+
//# sourceMappingURL=isolate-ts-Igq7C.mjs.map
|