isolate-package 1.29.0 → 1.30.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.d.mts +7 -1
- package/dist/index.mjs +2 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{isolate-3GcdAuUK.mjs → isolate-CJy3YyKG.mjs} +261 -64
- package/dist/isolate-CJy3YyKG.mjs.map +1 -0
- package/dist/isolate-bin.mjs +3 -5
- package/dist/isolate-bin.mjs.map +1 -1
- package/package.json +21 -20
- package/src/get-internal-package-names.test.ts +0 -10
- package/src/index.ts +6 -0
- package/src/isolate.ts +38 -8
- package/src/lib/lockfile/helpers/generate-bun-lockfile.test.ts +619 -0
- package/src/lib/lockfile/helpers/generate-bun-lockfile.ts +354 -0
- package/src/lib/lockfile/helpers/generate-pnpm-lockfile.test.ts +387 -0
- package/src/lib/lockfile/helpers/index.ts +1 -0
- package/src/lib/lockfile/helpers/pnpm-map-importer.test.ts +183 -0
- package/src/lib/lockfile/process-lockfile.test.ts +238 -0
- package/src/lib/lockfile/process-lockfile.ts +6 -6
- package/src/lib/manifest/adapt-target-package-manifest.ts +6 -4
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.test.ts +49 -0
- package/src/lib/manifest/helpers/adapt-internal-package-manifests.ts +15 -3
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.test.ts +61 -0
- package/src/lib/manifest/helpers/adopt-pnpm-fields-from-root.ts +42 -3
- package/src/lib/patches/copy-patches.test.ts +49 -11
- package/src/lib/patches/copy-patches.ts +38 -17
- package/src/lib/types.ts +5 -0
- package/src/lib/utils/filter-patched-dependencies.test.ts +1 -11
- package/src/testing/setup.ts +13 -0
- package/dist/isolate-3GcdAuUK.mjs.map +0 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import { got } from "get-or-throw";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { useLogger } from "~/lib/logger";
|
|
5
|
+
import type { PackagesRegistry } from "~/lib/types";
|
|
6
|
+
import {
|
|
7
|
+
getErrorMessage,
|
|
8
|
+
getPackageName,
|
|
9
|
+
readTypedJsonSync,
|
|
10
|
+
} from "~/lib/utils";
|
|
11
|
+
|
|
12
|
+
type BunWorkspaceEntry = {
|
|
13
|
+
name?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
dependencies?: Record<string, string>;
|
|
16
|
+
devDependencies?: Record<string, string>;
|
|
17
|
+
optionalDependencies?: Record<string, string>;
|
|
18
|
+
peerDependencies?: Record<string, string>;
|
|
19
|
+
optionalPeers?: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type BunLockfile = {
|
|
23
|
+
lockfileVersion: number;
|
|
24
|
+
workspaces: Record<string, BunWorkspaceEntry>;
|
|
25
|
+
packages: Record<string, unknown[]>;
|
|
26
|
+
trustedDependencies?: string[];
|
|
27
|
+
patchedDependencies?: Record<string, string>;
|
|
28
|
+
overrides?: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serialize a value to JSON with trailing commas after every array element and
|
|
33
|
+
* object property, matching Bun's native bun.lock output format.
|
|
34
|
+
*/
|
|
35
|
+
export function serializeWithTrailingCommas(
|
|
36
|
+
value: unknown,
|
|
37
|
+
indent = 2,
|
|
38
|
+
): string {
|
|
39
|
+
const json = JSON.stringify(value, null, indent);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add trailing commas after values that precede a closing bracket/brace.
|
|
43
|
+
* Apply repeatedly because consecutive closing brackets (e.g. ]\n}) need
|
|
44
|
+
* multiple passes — the first pass adds a comma after the inner value, and
|
|
45
|
+
* subsequent passes handle the outer brackets.
|
|
46
|
+
*/
|
|
47
|
+
let result = json;
|
|
48
|
+
let previous: string;
|
|
49
|
+
do {
|
|
50
|
+
previous = result;
|
|
51
|
+
result = result.replace(/(["\d\w\]}-])\n(\s*[\]}])/g, "$1,\n$2");
|
|
52
|
+
} while (result !== previous);
|
|
53
|
+
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract dependency names from a workspace entry, optionally including
|
|
59
|
+
* devDependencies.
|
|
60
|
+
*/
|
|
61
|
+
function collectDependencyNames(
|
|
62
|
+
entry: BunWorkspaceEntry,
|
|
63
|
+
includeDevDependencies: boolean,
|
|
64
|
+
): string[] {
|
|
65
|
+
const names = new Set<string>();
|
|
66
|
+
|
|
67
|
+
for (const name of Object.keys(entry.dependencies ?? {})) {
|
|
68
|
+
names.add(name);
|
|
69
|
+
}
|
|
70
|
+
for (const name of Object.keys(entry.optionalDependencies ?? {})) {
|
|
71
|
+
names.add(name);
|
|
72
|
+
}
|
|
73
|
+
for (const name of Object.keys(entry.peerDependencies ?? {})) {
|
|
74
|
+
names.add(name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (includeDevDependencies) {
|
|
78
|
+
for (const name of Object.keys(entry.devDependencies ?? {})) {
|
|
79
|
+
names.add(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [...names];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check whether a package entry represents a workspace package by examining its
|
|
88
|
+
* identifier string (first element in the entry array).
|
|
89
|
+
*/
|
|
90
|
+
function isWorkspacePackageEntry(entry: unknown[]): boolean {
|
|
91
|
+
const ident = entry[0];
|
|
92
|
+
return typeof ident === "string" && ident.includes("@workspace:");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract the info object from a packages entry. The position varies by type:
|
|
97
|
+
* - npm packages: [ident, registry, info, checksum] -> index 2
|
|
98
|
+
* - workspace packages: [ident, info] -> index 1
|
|
99
|
+
* - git/github packages: [ident, info, checksum] -> index 1
|
|
100
|
+
*
|
|
101
|
+
* Detection: if the second element is a string (registry URL or checksum), the
|
|
102
|
+
* info object is deeper. Workspace entries have only 2 elements.
|
|
103
|
+
*/
|
|
104
|
+
function getPackageInfoObject(
|
|
105
|
+
entry: unknown[],
|
|
106
|
+
): Record<string, unknown> | undefined {
|
|
107
|
+
if (entry.length <= 1) return undefined;
|
|
108
|
+
|
|
109
|
+
/** Workspace entries: [ident, info] */
|
|
110
|
+
if (isWorkspacePackageEntry(entry)) {
|
|
111
|
+
return typeof entry[1] === "object"
|
|
112
|
+
? (entry[1] as Record<string, unknown>)
|
|
113
|
+
: undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* npm entries with registry URL: [ident, registryUrl, info, checksum].
|
|
118
|
+
* The second element is a string (the registry URL).
|
|
119
|
+
*/
|
|
120
|
+
if (typeof entry[1] === "string") {
|
|
121
|
+
return typeof entry[2] === "object"
|
|
122
|
+
? (entry[2] as Record<string, unknown>)
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** git/tarball entries: [ident, info, checksum] */
|
|
127
|
+
return typeof entry[1] === "object"
|
|
128
|
+
? (entry[1] as Record<string, unknown>)
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Recursively collect all package keys that are required, starting from a set
|
|
134
|
+
* of direct dependency names and walking through their transitive dependencies
|
|
135
|
+
* in the packages section.
|
|
136
|
+
*/
|
|
137
|
+
function collectRequiredPackages(
|
|
138
|
+
directDependencyNames: Set<string>,
|
|
139
|
+
packages: Record<string, unknown[]>,
|
|
140
|
+
): Set<string> {
|
|
141
|
+
const required = new Set<string>();
|
|
142
|
+
const queue = [...directDependencyNames];
|
|
143
|
+
|
|
144
|
+
while (queue.length > 0) {
|
|
145
|
+
const name = queue.pop()!;
|
|
146
|
+
|
|
147
|
+
if (required.has(name)) continue;
|
|
148
|
+
|
|
149
|
+
const entry = packages[name];
|
|
150
|
+
if (!entry) continue;
|
|
151
|
+
|
|
152
|
+
required.add(name);
|
|
153
|
+
|
|
154
|
+
const info = getPackageInfoObject(entry);
|
|
155
|
+
if (!info) continue;
|
|
156
|
+
|
|
157
|
+
/** Walk transitive dependencies from the info object */
|
|
158
|
+
for (const depField of [
|
|
159
|
+
"dependencies",
|
|
160
|
+
"optionalDependencies",
|
|
161
|
+
"peerDependencies",
|
|
162
|
+
]) {
|
|
163
|
+
const deps = info[depField];
|
|
164
|
+
if (deps && typeof deps === "object") {
|
|
165
|
+
for (const depName of Object.keys(deps as Record<string, unknown>)) {
|
|
166
|
+
if (!required.has(depName)) {
|
|
167
|
+
queue.push(depName);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return required;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function generateBunLockfile({
|
|
178
|
+
workspaceRootDir,
|
|
179
|
+
targetPackageDir,
|
|
180
|
+
isolateDir,
|
|
181
|
+
internalDepPackageNames,
|
|
182
|
+
packagesRegistry,
|
|
183
|
+
includeDevDependencies,
|
|
184
|
+
}: {
|
|
185
|
+
workspaceRootDir: string;
|
|
186
|
+
targetPackageDir: string;
|
|
187
|
+
isolateDir: string;
|
|
188
|
+
internalDepPackageNames: string[];
|
|
189
|
+
packagesRegistry: PackagesRegistry;
|
|
190
|
+
includeDevDependencies: boolean;
|
|
191
|
+
}) {
|
|
192
|
+
const log = useLogger();
|
|
193
|
+
|
|
194
|
+
log.debug("Generating Bun lockfile...");
|
|
195
|
+
|
|
196
|
+
const lockfilePath = path.join(workspaceRootDir, "bun.lock");
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
if (!fs.existsSync(lockfilePath)) {
|
|
200
|
+
throw new Error(`Failed to find bun.lock at ${lockfilePath}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lockfile = readTypedJsonSync<BunLockfile>(lockfilePath);
|
|
204
|
+
|
|
205
|
+
/** Compute workspace keys for the target and internal deps */
|
|
206
|
+
const targetWorkspaceKey = path
|
|
207
|
+
.relative(workspaceRootDir, targetPackageDir)
|
|
208
|
+
.split(path.sep)
|
|
209
|
+
.join(path.posix.sep);
|
|
210
|
+
|
|
211
|
+
const internalDepWorkspaceKeys = new Map<string, string>();
|
|
212
|
+
for (const name of internalDepPackageNames) {
|
|
213
|
+
const pkg = got(packagesRegistry, name);
|
|
214
|
+
/** Normalize to POSIX separators for matching bun.lock workspace keys */
|
|
215
|
+
const workspaceKey = pkg.rootRelativeDir
|
|
216
|
+
.split(path.sep)
|
|
217
|
+
.join(path.posix.sep);
|
|
218
|
+
internalDepWorkspaceKeys.set(name, workspaceKey);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Build the filtered workspaces object */
|
|
222
|
+
const filteredWorkspaces: Record<string, BunWorkspaceEntry> = {};
|
|
223
|
+
|
|
224
|
+
/** Remap the target workspace to root ("") */
|
|
225
|
+
const targetEntry = lockfile.workspaces[targetWorkspaceKey];
|
|
226
|
+
if (!targetEntry) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Target workspace "${targetWorkspaceKey}" not found in bun.lock. Available workspaces: ${Object.keys(lockfile.workspaces).join(", ")}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
const entry = { ...targetEntry };
|
|
234
|
+
if (!includeDevDependencies) {
|
|
235
|
+
delete entry.devDependencies;
|
|
236
|
+
}
|
|
237
|
+
filteredWorkspaces[""] = entry;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Add internal dependency workspaces */
|
|
241
|
+
for (const [, workspaceKey] of internalDepWorkspaceKeys) {
|
|
242
|
+
const entry = lockfile.workspaces[workspaceKey];
|
|
243
|
+
if (entry) {
|
|
244
|
+
/** Strip devDependencies from internal deps */
|
|
245
|
+
const filtered = { ...entry };
|
|
246
|
+
delete filtered.devDependencies;
|
|
247
|
+
filteredWorkspaces[workspaceKey] = filtered;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Collect all dependency names from filtered workspace entries, then
|
|
253
|
+
* recursively walk through the packages section to find all transitive
|
|
254
|
+
* dependencies.
|
|
255
|
+
*/
|
|
256
|
+
const directDependencyNames = new Set<string>();
|
|
257
|
+
for (const [workspaceKey, entry] of Object.entries(filteredWorkspaces)) {
|
|
258
|
+
const isTarget = workspaceKey === "";
|
|
259
|
+
const names = collectDependencyNames(
|
|
260
|
+
entry,
|
|
261
|
+
isTarget && includeDevDependencies,
|
|
262
|
+
);
|
|
263
|
+
for (const name of names) {
|
|
264
|
+
directDependencyNames.add(name);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const requiredPackages = collectRequiredPackages(
|
|
269
|
+
directDependencyNames,
|
|
270
|
+
lockfile.packages,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
/** Also include workspace package entries for kept internal deps */
|
|
274
|
+
const keptInternalDepNames = new Set(internalDepPackageNames);
|
|
275
|
+
|
|
276
|
+
/** Filter the packages section */
|
|
277
|
+
const filteredPackages: Record<string, unknown[]> = {};
|
|
278
|
+
for (const [key, entry] of Object.entries(lockfile.packages)) {
|
|
279
|
+
if (requiredPackages.has(key)) {
|
|
280
|
+
/**
|
|
281
|
+
* Skip workspace entries for packages that are not in our kept internal
|
|
282
|
+
* deps. This removes workspace references to packages outside the
|
|
283
|
+
* isolate.
|
|
284
|
+
*/
|
|
285
|
+
if (isWorkspacePackageEntry(entry) && !keptInternalDepNames.has(key)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
filteredPackages[key] = entry;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Also make sure workspace entries for kept internal deps are included */
|
|
293
|
+
for (const name of keptInternalDepNames) {
|
|
294
|
+
if (!filteredPackages[name] && lockfile.packages[name]) {
|
|
295
|
+
filteredPackages[name] = lockfile.packages[name];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Build the output lockfile preserving metadata */
|
|
300
|
+
const outputLockfile: BunLockfile = {
|
|
301
|
+
lockfileVersion: lockfile.lockfileVersion,
|
|
302
|
+
workspaces: filteredWorkspaces,
|
|
303
|
+
packages: filteredPackages,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (lockfile.overrides && Object.keys(lockfile.overrides).length > 0) {
|
|
307
|
+
outputLockfile.overrides = lockfile.overrides;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (
|
|
311
|
+
lockfile.trustedDependencies &&
|
|
312
|
+
lockfile.trustedDependencies.length > 0
|
|
313
|
+
) {
|
|
314
|
+
/** Filter to only include trusted dependencies that are in the output */
|
|
315
|
+
const outputTrusted = lockfile.trustedDependencies.filter(
|
|
316
|
+
(name) => filteredPackages[name] !== undefined,
|
|
317
|
+
);
|
|
318
|
+
if (outputTrusted.length > 0) {
|
|
319
|
+
outputLockfile.trustedDependencies = outputTrusted;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
lockfile.patchedDependencies &&
|
|
325
|
+
Object.keys(lockfile.patchedDependencies).length > 0
|
|
326
|
+
) {
|
|
327
|
+
/** Filter to only include patches for packages in the output */
|
|
328
|
+
const outputPatches: Record<string, string> = {};
|
|
329
|
+
for (const [spec, patchPath] of Object.entries(
|
|
330
|
+
lockfile.patchedDependencies,
|
|
331
|
+
)) {
|
|
332
|
+
const packageName = getPackageName(spec);
|
|
333
|
+
if (filteredPackages[packageName] !== undefined) {
|
|
334
|
+
outputPatches[spec] = patchPath;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (Object.keys(outputPatches).length > 0) {
|
|
338
|
+
outputLockfile.patchedDependencies = outputPatches;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const outputPath = path.join(isolateDir, "bun.lock");
|
|
343
|
+
/** Append trailing newline to match Bun's native output format */
|
|
344
|
+
await fs.writeFile(
|
|
345
|
+
outputPath,
|
|
346
|
+
serializeWithTrailingCommas(outputLockfile) + "\n",
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
log.debug("Created lockfile at", outputPath);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
log.error(`Failed to generate lockfile: ${getErrorMessage(err)}`);
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
}
|