@effected/workspaces 0.26.0 → 0.27.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/PackedInstall.js +530 -51
- package/README.md +67 -18
- package/SourceBoundary.js +87 -3
- package/internal/packedInstallPlan.js +73 -8
- package/internal/sourceText.js +32 -6
- package/package.json +1 -1
- package/testing.d.ts +484 -29
package/PackedInstall.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
2
2
|
import { PackageManagerName } from "./PackageManagerName.js";
|
|
3
|
-
import { closureOf, consumerFiles, installArgs,
|
|
4
|
-
import { Duration, Effect, FileSystem, Option, Path, Result, Schema } from "effect";
|
|
3
|
+
import { binConflict, binTargetOf, closureOf, consumerFiles, fileOverridesOf, installArgs, overridePath, readPackedManifest, scrubEnv, versionOf } from "./internal/packedInstallPlan.js";
|
|
4
|
+
import { Duration, Effect, FileSystem, Option, Path, Redacted, Result, Schema } from "effect";
|
|
5
|
+
import { Yaml } from "@effected/yaml";
|
|
5
6
|
import { Run } from "@effected/commands";
|
|
6
7
|
import { ChildProcess } from "effect/unstable/process";
|
|
7
8
|
|
|
@@ -21,8 +22,12 @@ var PackedInstallError = class extends Schema.TaggedError()("PackedInstallError"
|
|
|
21
22
|
"PackSourceMissing",
|
|
22
23
|
"PackFailed",
|
|
23
24
|
"UnresolvedProtocol",
|
|
25
|
+
"InvalidOverride",
|
|
26
|
+
"BinConflict",
|
|
24
27
|
"InstallFailed",
|
|
25
28
|
"MissingBin",
|
|
29
|
+
"BinFailed",
|
|
30
|
+
"UnownedBin",
|
|
26
31
|
"Discovery",
|
|
27
32
|
"Io"
|
|
28
33
|
]),
|
|
@@ -37,6 +42,14 @@ var PackedInstallError = class extends Schema.TaggedError()("PackedInstallError"
|
|
|
37
42
|
/** The originating failure. */
|
|
38
43
|
cause: Schema.optionalKey(Schema.Defect())
|
|
39
44
|
}) {};
|
|
45
|
+
const DEFAULT_BIN_TIMEOUT = "1 minute";
|
|
46
|
+
/**
|
|
47
|
+
* Whether a failed `readLink` means "this entry is not a symlink". Node reports
|
|
48
|
+
* that as `EINVAL`, which its platform layer tags `Unknown` with the errno on
|
|
49
|
+
* the cause, and `@effected/memfs` raises the same shape. Anything else,
|
|
50
|
+
* `PermissionDenied` and `BadResource` included, is a real failure.
|
|
51
|
+
*/
|
|
52
|
+
const isNotALink = (error) => error.reason._tag === "Unknown" && typeof error.reason.cause === "object" && error.reason.cause !== null && error.reason.cause.code === "EINVAL";
|
|
40
53
|
/**
|
|
41
54
|
* One scratch project, outside the workspace, with the carrier installed.
|
|
42
55
|
*
|
|
@@ -48,13 +61,249 @@ var InstalledConsumer = class extends Schema.Class("InstalledConsumer")({
|
|
|
48
61
|
/** The version it reported and was pinned to. */
|
|
49
62
|
managerVersion: Schema.String,
|
|
50
63
|
/** The consumer project directory (realpath'd). */
|
|
51
|
-
directory: Schema.String
|
|
64
|
+
directory: Schema.String,
|
|
65
|
+
/**
|
|
66
|
+
* The scrubbed environment the install ran under, which {@link InstalledConsumer.runBin}
|
|
67
|
+
* starts from. Redacted, so printing a consumer never prints a token.
|
|
68
|
+
* `PackedInstall.run` always sets it; a hand-made consumer without it runs
|
|
69
|
+
* its bins under only `RunBinOptions.env`.
|
|
70
|
+
*/
|
|
71
|
+
env: Schema.optionalKey(Schema.Redacted(Schema.Record(Schema.String, Schema.String))),
|
|
72
|
+
/**
|
|
73
|
+
* The carrier the consumer depends on, which {@link InstalledConsumer.carrierCommand}
|
|
74
|
+
* resolves bins through. `PackedInstall.run` always sets it.
|
|
75
|
+
*/
|
|
76
|
+
carrier: Schema.optionalKey(Schema.String)
|
|
52
77
|
}) {
|
|
53
78
|
/** The installed bin `name`, in `node_modules/.bin`. POSIX: `PackedInstall` runs only there. */
|
|
54
79
|
binPath(name) {
|
|
55
80
|
return `${this.directory}/node_modules/.bin/${name}`;
|
|
56
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Which installed package the `node_modules/.bin` entry `name` resolves into.
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* Under a flat layout (npm, bun, and Yarn with the `node-modules` linker
|
|
87
|
+
* this run configures), a hoisted bin of the same name from another package
|
|
88
|
+
* can shadow the carrier's, and running it cannot tell you which one ran.
|
|
89
|
+
* Those managers write each `.bin` entry as a symlink, so this reads the
|
|
90
|
+
* link, realpaths its target, and walks up to the nearest `package.json`
|
|
91
|
+
* with a string `name`, staying inside the consumer directory (itself
|
|
92
|
+
* realpath'd first, so a `/var` alias of `/private/var` or a trailing
|
|
93
|
+
* slash does not move the bound). A manifest that is valid JSON but not an
|
|
94
|
+
* object, or has no string `name` (a `dist/package.json` carrying only
|
|
95
|
+
* `type`), is passed over. Assert `package` is your carrier.
|
|
96
|
+
*
|
|
97
|
+
* `undefined` means one thing: the entry exists and is not a symlink.
|
|
98
|
+
* pnpm writes `.bin` entries as shell shims, whose target this does not
|
|
99
|
+
* parse; its isolated layout links only the consumer's direct dependencies
|
|
100
|
+
* at the top level, so a shadowing bin needs a direct dependency there.
|
|
101
|
+
*
|
|
102
|
+
* An entry that does not exist, or a link whose target does not, fails
|
|
103
|
+
* `MissingBin`. A link into no named package inside the consumer fails
|
|
104
|
+
* `UnownedBin`, naming the target. Any other read failure, a
|
|
105
|
+
* `package.json` that is not JSON included, fails `Io`.
|
|
106
|
+
*
|
|
107
|
+
* @param name - The bin, as named in `node_modules/.bin`.
|
|
108
|
+
*/
|
|
109
|
+
binProvenance(name) {
|
|
110
|
+
const bin = this.binPath(name);
|
|
111
|
+
const directory = this.directory;
|
|
112
|
+
const manager = this.manager;
|
|
113
|
+
const entry = `node_modules/.bin/${name}`;
|
|
114
|
+
return Effect.gen(function* () {
|
|
115
|
+
const fs = yield* FileSystem.FileSystem;
|
|
116
|
+
const path = yield* Path.Path;
|
|
117
|
+
const io = (message) => (cause) => failure("Io", message, {
|
|
118
|
+
manager,
|
|
119
|
+
cause
|
|
120
|
+
});
|
|
121
|
+
const missing = (why) => failure("MissingBin", `${manager}: ${entry} ${why}`, { manager });
|
|
122
|
+
if (!(yield* fs.readLink(bin).pipe(Effect.as(true), Effect.catch((error) => isNotALink(error) ? Effect.succeed(false) : error.reason._tag === "NotFound" ? Effect.fail(missing("does not exist")) : Effect.fail(io(`could not read the link ${bin}`)(error)))))) {
|
|
123
|
+
if (!(yield* fs.exists(bin).pipe(Effect.mapError(io(`could not inspect ${bin}`))))) return yield* missing("does not exist");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const target = yield* fs.realPath(bin).pipe(Effect.catch((error) => error.reason._tag === "NotFound" ? Effect.fail(missing("is a link to nothing")) : Effect.fail(io(`could not resolve ${bin}`)(error))));
|
|
127
|
+
const root = yield* fs.realPath(directory).pipe(Effect.mapError(io(`could not resolve ${directory}`)));
|
|
128
|
+
for (let dir = path.dirname(target); dir.startsWith(`${root}/`); dir = path.dirname(dir)) {
|
|
129
|
+
const manifest = path.join(dir, "package.json");
|
|
130
|
+
if (!(yield* fs.exists(manifest).pipe(Effect.mapError(io(`could not inspect ${manifest}`))))) continue;
|
|
131
|
+
const text = yield* fs.readFileString(manifest).pipe(Effect.mapError(io(`could not read ${manifest}`)));
|
|
132
|
+
const parsed = yield* Effect.try({
|
|
133
|
+
try: () => JSON.parse(text),
|
|
134
|
+
catch: io(`${manifest} is not JSON`)
|
|
135
|
+
});
|
|
136
|
+
const named = typeof parsed === "object" && parsed !== null ? parsed.name : void 0;
|
|
137
|
+
if (typeof named === "string") return {
|
|
138
|
+
package: named,
|
|
139
|
+
target
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return yield* failure("UnownedBin", `${manager}: ${entry} links to ${target}, which lies in no named package inside ${root}`, { manager });
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The command {@link InstalledConsumer.runBin} spawns for the bin `name`,
|
|
147
|
+
* for a caller that drives the child itself, such as `McpProbe.initialize`
|
|
148
|
+
* from `@effected/mcp/testing`.
|
|
149
|
+
*
|
|
150
|
+
* @remarks
|
|
151
|
+
* {@link InstalledConsumer.binPath}, from the consumer's directory, under
|
|
152
|
+
* the install's scrubbed environment with `options.env` layered over it
|
|
153
|
+
* after the scrub, and nothing inherited beyond that. `runBin` builds its
|
|
154
|
+
* command here and then ignores stdin; this one leaves stdin as the
|
|
155
|
+
* spawner's default pipe, so a probe can write to it. Spawn it inside the
|
|
156
|
+
* scope that ran `PackedInstall.run`: the scratch directory is removed
|
|
157
|
+
* when that scope closes.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* import { McpProbe } from "@effected/mcp/testing";
|
|
162
|
+
* import type { InstalledConsumer } from "@effected/workspaces/testing";
|
|
163
|
+
*
|
|
164
|
+
* declare const consumer: InstalledConsumer;
|
|
165
|
+
* declare const xdg: string;
|
|
166
|
+
*
|
|
167
|
+
* const probe = McpProbe.initialize(consumer.command("my-tool-mcp", [], { env: { XDG_DATA_HOME: xdg } }));
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @param name - The bin, as named in `node_modules/.bin`.
|
|
171
|
+
* @param args - Its arguments.
|
|
172
|
+
* @param options - Extra environment and working directory.
|
|
173
|
+
*/
|
|
174
|
+
command(name, args = [], options = {}) {
|
|
175
|
+
return ChildProcess.make(this.binPath(name), args, {
|
|
176
|
+
cwd: options.cwd ?? this.directory,
|
|
177
|
+
env: layeredEnv(this, options),
|
|
178
|
+
extendEnv: false
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Run the installed bin `name` to completion and collect what it wrote.
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* Spawns {@link InstalledConsumer.command} with stdin ignored: the bin
|
|
186
|
+
* from the consumer's directory, under the install's scrubbed environment
|
|
187
|
+
* with `options.env` layered over it after the scrub, and nothing
|
|
188
|
+
* inherited beyond that. A
|
|
189
|
+
* non-zero exit is a result, read from `exitCode`, never a failure. A bin
|
|
190
|
+
* that cannot spawn, outlives `options.timeout` or floods its output fails
|
|
191
|
+
* `BinFailed` naming the manager and the bin. Run it inside the scope that
|
|
192
|
+
* ran `PackedInstall.run`: the scratch directory is removed when that
|
|
193
|
+
* scope closes.
|
|
194
|
+
*
|
|
195
|
+
* @param name - The bin, as named in `node_modules/.bin`.
|
|
196
|
+
* @param args - Its arguments.
|
|
197
|
+
* @param options - Extra environment, working directory and ceiling.
|
|
198
|
+
*/
|
|
199
|
+
runBin(name, args = [], options = {}) {
|
|
200
|
+
return collectBin(this.command(name, args, options), this.manager, name, options.timeout);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The command that runs the CARRIER's own bin `name`, whichever package
|
|
204
|
+
* won the `node_modules/.bin` slot.
|
|
205
|
+
*
|
|
206
|
+
* @remarks
|
|
207
|
+
* With `allowSharedBins`, a flat layout (npm, bun, Yarn's `node-modules`
|
|
208
|
+
* linker) can link a front end's same-named bin into `.bin`, so
|
|
209
|
+
* {@link InstalledConsumer.runBin} proves only that SOME package's bin runs.
|
|
210
|
+
* This reads the carrier's installed `node_modules/<carrier>/package.json`,
|
|
211
|
+
* takes `name` from its `bin` map (a `bin` string answers to the unscoped
|
|
212
|
+
* package name), and returns `node <that file> ...args`: the carrier's shim
|
|
213
|
+
* itself, run by the `node` on the environment's `PATH`.
|
|
214
|
+
*
|
|
215
|
+
* Running it through `node` has three limits. It assumes a Node script, so
|
|
216
|
+
* a non-Node shim misruns. It drops any flags in the shim's shebang (a
|
|
217
|
+
* `#!/usr/bin/env -S node --enable-source-maps` runs without them). And it
|
|
218
|
+
* bypasses the file's executable bit, so under shared bins it does not
|
|
219
|
+
* prove the carrier's shim is executable, only that it runs: the manager
|
|
220
|
+
* that linked a front end's bin into the slot may never have made the
|
|
221
|
+
* carrier's target executable. Environment and working directory are built as
|
|
222
|
+
* {@link InstalledConsumer.command} builds them, stdin left open for a probe
|
|
223
|
+
* such as `McpProbe.initialize`.
|
|
224
|
+
*
|
|
225
|
+
* A consumer with no `carrier`, a carrier that is not installed or does not
|
|
226
|
+
* declare `name`, or a declared file that does not exist fails
|
|
227
|
+
* `MissingBin`; a manifest that cannot be read or is not JSON fails `Io`.
|
|
228
|
+
*
|
|
229
|
+
* @param name - The bin, as the carrier's `bin` map names it.
|
|
230
|
+
* @param args - Its arguments.
|
|
231
|
+
* @param options - Extra environment and working directory.
|
|
232
|
+
*/
|
|
233
|
+
carrierCommand(name, args = [], options = {}) {
|
|
234
|
+
const consumer = this;
|
|
235
|
+
const manager = this.manager;
|
|
236
|
+
return Effect.gen(function* () {
|
|
237
|
+
const fs = yield* FileSystem.FileSystem;
|
|
238
|
+
const path = yield* Path.Path;
|
|
239
|
+
const missing = (why) => failure("MissingBin", `${manager}: the carrier's bin ${name} ${why}`, {
|
|
240
|
+
manager,
|
|
241
|
+
...consumer.carrier === void 0 ? {} : { package: consumer.carrier }
|
|
242
|
+
});
|
|
243
|
+
if (consumer.carrier === void 0) return yield* missing("cannot be found: this consumer records no carrier");
|
|
244
|
+
const root = path.join(consumer.directory, "node_modules", ...consumer.carrier.split("/"));
|
|
245
|
+
const manifestPath = path.join(root, "package.json");
|
|
246
|
+
const text = yield* fs.readFileString(manifestPath).pipe(Effect.catch((error) => error.reason._tag === "NotFound" ? Effect.fail(missing(`cannot be found: ${consumer.carrier} is not installed at ${root}`)) : Effect.fail(failure("Io", `could not read ${manifestPath}`, {
|
|
247
|
+
manager,
|
|
248
|
+
cause: error
|
|
249
|
+
}))));
|
|
250
|
+
const manifest = readPackedManifest(text);
|
|
251
|
+
if (Result.isFailure(manifest)) return yield* failure("Io", `${manifestPath} is not a JSON object`, {
|
|
252
|
+
manager,
|
|
253
|
+
cause: manifest.failure
|
|
254
|
+
});
|
|
255
|
+
const target = binTargetOf(text, name);
|
|
256
|
+
if (target === void 0) return yield* missing(`is not declared by ${consumer.carrier}`);
|
|
257
|
+
const file = path.join(root, target);
|
|
258
|
+
if (!(yield* fs.exists(file).pipe(Effect.mapError((cause) => failure("Io", `could not inspect ${file}`, {
|
|
259
|
+
manager,
|
|
260
|
+
cause
|
|
261
|
+
}))))) return yield* missing(`points at ${file}, which does not exist`);
|
|
262
|
+
return ChildProcess.make("node", [file, ...args], {
|
|
263
|
+
cwd: options.cwd ?? consumer.directory,
|
|
264
|
+
env: layeredEnv(consumer, options),
|
|
265
|
+
extendEnv: false
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Run the carrier's own bin `name` to completion, whichever package won
|
|
271
|
+
* the `node_modules/.bin` slot.
|
|
272
|
+
*
|
|
273
|
+
* @remarks
|
|
274
|
+
* Spawns {@link InstalledConsumer.carrierCommand} with stdin ignored, and
|
|
275
|
+
* reports as {@link InstalledConsumer.runBin} does: a non-zero exit is a
|
|
276
|
+
* result, and a bin that cannot spawn or outlives `options.timeout` (one
|
|
277
|
+
* minute by default) fails `BinFailed`. Use it beside `runBin` under
|
|
278
|
+
* `allowSharedBins`: `runBin` proves what a user typing the bin name gets,
|
|
279
|
+
* this proves the carrier's shim itself works, within `carrierCommand`'s
|
|
280
|
+
* limits (a Node script, shebang flags dropped, executable bit not
|
|
281
|
+
* checked).
|
|
282
|
+
*
|
|
283
|
+
* @param name - The bin, as the carrier's `bin` map names it.
|
|
284
|
+
* @param args - Its arguments.
|
|
285
|
+
* @param options - Extra environment, working directory and ceiling.
|
|
286
|
+
*/
|
|
287
|
+
runCarrierBin(name, args = [], options = {}) {
|
|
288
|
+
const manager = this.manager;
|
|
289
|
+
return this.carrierCommand(name, args, options).pipe(Effect.flatMap((command) => collectBin(command, manager, name, options.timeout)));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
/** The install's scrubbed environment with the caller's entries layered on after the scrub: a deliberate CI=true must survive it. */
|
|
293
|
+
const layeredEnv = (consumer, options) => {
|
|
294
|
+
const env = scrubEnv(consumer.env === void 0 ? {} : Redacted.value(consumer.env));
|
|
295
|
+
for (const [key, value] of Object.entries(options.env ?? {})) if (value === void 0) delete env[key];
|
|
296
|
+
else env[key] = value;
|
|
297
|
+
return env;
|
|
57
298
|
};
|
|
299
|
+
/** Run a bin's command to completion, stdin ignored, mapping a failed spawn or an expired ceiling to `BinFailed`. */
|
|
300
|
+
const collectBin = (command, manager, name, timeout = DEFAULT_BIN_TIMEOUT) => Run.collect(ChildProcess.make(command.command, command.args, {
|
|
301
|
+
...command.options,
|
|
302
|
+
stdin: "ignore"
|
|
303
|
+
}), { timeout }).pipe(Effect.mapError((cause) => failure("BinFailed", cause._tag === "CommandFailedError" && cause.kind === "timeout" ? `${manager}: ${name} timed out after ${describeDuration(timeout)}` : `${manager}: ${name} could not run`, {
|
|
304
|
+
manager,
|
|
305
|
+
cause
|
|
306
|
+
})));
|
|
58
307
|
/**
|
|
59
308
|
* What a packed install produced.
|
|
60
309
|
*
|
|
@@ -66,13 +315,31 @@ var PackedInstallResult = class extends Schema.Class("PackedInstallResult")({
|
|
|
66
315
|
/** The requested managers that did not answer `--version`. */
|
|
67
316
|
unavailable: Schema.Array(PackageManagerName),
|
|
68
317
|
/** Every packed package: name to absolute tarball path. */
|
|
69
|
-
tarballs: Schema.Record(Schema.String, Schema.String)
|
|
318
|
+
tarballs: Schema.Record(Schema.String, Schema.String),
|
|
319
|
+
/**
|
|
320
|
+
* The scratch root (realpath'd) holding the tarballs and every consumer.
|
|
321
|
+
* It is removed when the scope that ran `PackedInstall.run` closes, so a
|
|
322
|
+
* directory made under it, such as an `XDG_DATA_HOME` for the bins, is
|
|
323
|
+
* cleaned up with it.
|
|
324
|
+
*/
|
|
325
|
+
scratch: Schema.String
|
|
70
326
|
}) {};
|
|
71
327
|
const DEFAULT_PACK_FROM = { directory: "dist/prod/npm/pkg" };
|
|
72
328
|
const WORKSPACE_PROTOCOL_NOT_INSTALLED = "ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL";
|
|
73
329
|
const TAIL = 2e3;
|
|
74
330
|
const tail = (text) => text.length <= TAIL ? text : text.slice(-2e3);
|
|
75
331
|
const DEFAULT_INSTALL_TIMEOUT = "4 minutes";
|
|
332
|
+
const PROBE_TIMEOUT = "30 seconds";
|
|
333
|
+
const DEFAULT_PACK_TIMEOUT = "2 minutes";
|
|
334
|
+
const MANIFEST_TIMEOUT = "30 seconds";
|
|
335
|
+
/** The untimed steps a budget allows for: discovery, the consumer files and the bin checks. */
|
|
336
|
+
const UNTIMED_SLACK = "30 seconds";
|
|
337
|
+
/**
|
|
338
|
+
* Cleanup a budget allows for: removing the scratch root, every consumer's
|
|
339
|
+
* `node_modules` included, when the scope closes, and killing and reaping a
|
|
340
|
+
* child after its ceiling interrupts it.
|
|
341
|
+
*/
|
|
342
|
+
const CLEANUP_ALLOWANCE = "1 minute";
|
|
76
343
|
/** A ceiling as a person reads it: `"4m"` for `"4 minutes"`, the raw input if it does not decode. */
|
|
77
344
|
const describeDuration = (input) => Option.match(Duration.fromInput(input), {
|
|
78
345
|
onNone: () => String(input),
|
|
@@ -83,6 +350,70 @@ const failure = (reason, message, extra = {}) => new PackedInstallError({
|
|
|
83
350
|
message,
|
|
84
351
|
...extra
|
|
85
352
|
});
|
|
353
|
+
const namesOf = (plan) => [...plan.closure.map((pkg) => pkg.name), ...plan.replacements.map((replacement) => replacement.name)];
|
|
354
|
+
/**
|
|
355
|
+
* The one implementation of "what does this run pack", behind both
|
|
356
|
+
* `PackedInstall.closure` and `PackedInstall.run`, so the two cannot drift.
|
|
357
|
+
*/
|
|
358
|
+
const planClosure = (carrier, options) => Effect.gen(function* () {
|
|
359
|
+
const fs = yield* FileSystem.FileSystem;
|
|
360
|
+
const path = yield* Path.Path;
|
|
361
|
+
const discovery = yield* WorkspaceDiscovery;
|
|
362
|
+
const discoveryFailed = (cause) => failure("Discovery", "workspace discovery failed", { cause });
|
|
363
|
+
const packages = yield* discovery.listPackages().pipe(Effect.mapError(discoveryFailed));
|
|
364
|
+
const closure = closureOf(packages, carrier, options.closure ?? "auto");
|
|
365
|
+
if (Result.isFailure(closure)) return yield* failure("UnknownPackage", `${closure.failure} is not a package of this workspace`, { package: closure.failure });
|
|
366
|
+
const requested = Object.entries(options.overrides ?? {}).map(([name, spec]) => [name, overridePath(spec)]);
|
|
367
|
+
const root = options.workspaceOverrides === true || requested.some(([, spec]) => !path.isAbsolute(spec)) ? (yield* discovery.info().pipe(Effect.mapError(discoveryFailed))).root : "/";
|
|
368
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
369
|
+
if (options.workspaceOverrides === true) {
|
|
370
|
+
const file = path.join(root, "pnpm-workspace.yaml");
|
|
371
|
+
const text = yield* fs.readFileString(file).pipe(Effect.mapError((cause) => failure("InvalidOverride", `workspaceOverrides reads ${file}, which could not be read`, { cause })));
|
|
372
|
+
const document = yield* Yaml.parse(text).pipe(Effect.mapError((cause) => failure("InvalidOverride", `${file} is not valid YAML`, { cause })));
|
|
373
|
+
for (const [name, spec] of Object.entries(fileOverridesOf(document))) wanted.set(name, spec);
|
|
374
|
+
}
|
|
375
|
+
for (const [name, spec] of requested) wanted.set(name, spec);
|
|
376
|
+
const packed = new Set(closure.success.map((pkg) => pkg.name));
|
|
377
|
+
const replacements = [];
|
|
378
|
+
for (const name of [...wanted.keys()].sort()) {
|
|
379
|
+
const invalid = (message, cause) => failure("InvalidOverride", `override ${name}: ${message}`, {
|
|
380
|
+
package: name,
|
|
381
|
+
...cause === void 0 ? {} : { cause }
|
|
382
|
+
});
|
|
383
|
+
if (packed.has(name)) return yield* invalid(`${name === carrier ? "the carrier" : "a closure member"} is always packed from the workspace; overrides replace only packages outside it`);
|
|
384
|
+
const spec = wanted.get(name) ?? "";
|
|
385
|
+
const absolute = path.isAbsolute(spec) ? spec : path.resolve(root, spec);
|
|
386
|
+
const source = yield* fs.realPath(absolute).pipe(Effect.catch((error) => error.reason._tag === "NotFound" ? Effect.fail(invalid(`${absolute} does not exist`)) : Effect.fail(failure("Io", `could not resolve ${absolute}`, {
|
|
387
|
+
package: name,
|
|
388
|
+
cause: error
|
|
389
|
+
}))));
|
|
390
|
+
const info = yield* fs.stat(source).pipe(Effect.mapError((cause) => failure("Io", `could not inspect ${source}`, {
|
|
391
|
+
package: name,
|
|
392
|
+
cause
|
|
393
|
+
})));
|
|
394
|
+
if (info.type === "Directory") {
|
|
395
|
+
const manifest = path.join(source, "package.json");
|
|
396
|
+
if (!(yield* fs.exists(manifest).pipe(Effect.mapError((cause) => failure("Io", `could not inspect ${manifest}`, {
|
|
397
|
+
package: name,
|
|
398
|
+
cause
|
|
399
|
+
}))))) return yield* invalid(`${source} has no package.json to pack`);
|
|
400
|
+
replacements.push({
|
|
401
|
+
name,
|
|
402
|
+
source,
|
|
403
|
+
kind: "directory"
|
|
404
|
+
});
|
|
405
|
+
} else if (info.type === "File" && source.endsWith(".tgz")) replacements.push({
|
|
406
|
+
name,
|
|
407
|
+
source,
|
|
408
|
+
kind: "tarball"
|
|
409
|
+
});
|
|
410
|
+
else return yield* invalid(`${source} is neither a package directory nor a .tgz`);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
closure: closure.success,
|
|
414
|
+
replacements
|
|
415
|
+
};
|
|
416
|
+
});
|
|
86
417
|
/**
|
|
87
418
|
* Prove a carrier's bins reach a consumer that is not part of the workspace,
|
|
88
419
|
* once per available package manager.
|
|
@@ -94,8 +425,8 @@ const failure = (reason, message, extra = {}) => new PackedInstallError({
|
|
|
94
425
|
* pin in the repo cannot refuse it), it writes a consumer whose only direct
|
|
95
426
|
* dependency is the carrier tarball, steers the closure to its tarballs
|
|
96
427
|
* through the manager's own override field, pins the probed version (as
|
|
97
|
-
* `packageManager`, or for pnpm as `devEngines.packageManager` with
|
|
98
|
-
* "ignore"`, since pnpm resolves a `packageManager` pin from the registry even
|
|
428
|
+
* `packageManager`, or for pnpm as `devEngines.packageManager` with
|
|
429
|
+
* `onFail: "ignore"`, since pnpm resolves a `packageManager` pin from the registry even
|
|
99
430
|
* when it names the running version), installs with lifecycle scripts skipped,
|
|
100
431
|
* and checks every
|
|
101
432
|
* expected bin is present and executable. A packed manifest that still
|
|
@@ -103,18 +434,18 @@ const failure = (reason, message, extra = {}) => new PackedInstallError({
|
|
|
103
434
|
* fails `UnresolvedProtocol` before any install.
|
|
104
435
|
*
|
|
105
436
|
* It asserts nothing about what the bins DO: run them from the test through
|
|
106
|
-
* `InstalledConsumer.
|
|
107
|
-
* `@effected/mcp/testing`, inside the same
|
|
108
|
-
*
|
|
437
|
+
* `InstalledConsumer.runBin`, or for an MCP bin through `McpProbe` from
|
|
438
|
+
* `@effected/mcp/testing` at `InstalledConsumer.binPath`, inside the same
|
|
439
|
+
* scope, because the scratch directory (`PackedInstallResult.scratch`) is
|
|
440
|
+
* removed when the scope closes. Size the test's outer timeout with
|
|
441
|
+
* {@link PackedInstall.timeoutBudget}. POSIX only.
|
|
109
442
|
*
|
|
110
443
|
* @example
|
|
111
444
|
* ```ts
|
|
112
445
|
* import { NodeServices } from "@effect/platform-node";
|
|
113
|
-
* import { Run } from "@effected/commands";
|
|
114
446
|
* import { Workspaces } from "@effected/workspaces";
|
|
115
447
|
* import { PackedInstall } from "@effected/workspaces/testing";
|
|
116
448
|
* import { Effect, Layer } from "effect";
|
|
117
|
-
* import { ChildProcess } from "effect/unstable/process";
|
|
118
449
|
*
|
|
119
450
|
* const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
|
|
120
451
|
*
|
|
@@ -128,22 +459,141 @@ const failure = (reason, message, extra = {}) => new PackedInstallError({
|
|
|
128
459
|
* bins: ["my-tool"],
|
|
129
460
|
* env: process.env,
|
|
130
461
|
* });
|
|
131
|
-
*
|
|
462
|
+
* // Keep the tool's data inside the scratch root, removed with it.
|
|
463
|
+
* const env = { XDG_DATA_HOME: `${result.scratch}/xdg` };
|
|
132
464
|
* for (const consumer of result.consumers) {
|
|
133
|
-
* const
|
|
134
|
-
*
|
|
135
|
-
* );
|
|
136
|
-
* console.log(consumer.manager, version.trim());
|
|
465
|
+
* const { stdout, exitCode } = yield* consumer.runBin("my-tool", ["--version"], { env });
|
|
466
|
+
* console.log(consumer.manager, exitCode, stdout.trim());
|
|
137
467
|
* }
|
|
138
468
|
* }).pipe(Effect.scoped, Effect.provide(Live));
|
|
139
469
|
* ```
|
|
140
470
|
*
|
|
141
471
|
* @public
|
|
142
472
|
*/
|
|
143
|
-
var PackedInstall = class {
|
|
473
|
+
var PackedInstall = class PackedInstall {
|
|
144
474
|
constructor() {}
|
|
145
475
|
/** `env` without undefined values or the parent manager's context: the environment the installs run under. Reuse it to run the installed bins. */
|
|
146
476
|
static scrubEnv = scrubEnv;
|
|
477
|
+
/**
|
|
478
|
+
* The ceiling a test's outer timeout should cover so that every one of the
|
|
479
|
+
* run's own ceilings fires first, as a named `PackedInstallError`, rather
|
|
480
|
+
* than the outer guard's `TimeoutError` that names nothing.
|
|
481
|
+
*
|
|
482
|
+
* @remarks
|
|
483
|
+
* The worst case of the run's own ceilings, taken in sequence as the run
|
|
484
|
+
* takes them: each manager's `--version` probe (30 seconds), install
|
|
485
|
+
* (`installTimeout`) and `perConsumer` (one minute by default, one `runBin`
|
|
486
|
+
* at its default ceiling), plus each package's pack
|
|
487
|
+
* (`packTimeout`, two minutes by default) and manifest read (30 seconds),
|
|
488
|
+
* plus 30 seconds for the untimed steps and one minute for cleanup:
|
|
489
|
+
* removing the scratch root when the scope closes, and killing a child
|
|
490
|
+
* whose ceiling interrupted it. It reads the same defaults the run does.
|
|
491
|
+
* Pass `packages` as the names {@link PackedInstall.closure} returns to
|
|
492
|
+
* size it before the run. A vitest test timeout must sit above it, since
|
|
493
|
+
* vitest's own guard should not pre-empt the Effect's.
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```ts
|
|
497
|
+
* import { PackedInstall } from "@effected/workspaces/testing";
|
|
498
|
+
* import { Duration } from "effect";
|
|
499
|
+
*
|
|
500
|
+
* const budget = PackedInstall.timeoutBudget({
|
|
501
|
+
* managers: ["npm", "pnpm", "yarn", "bun"],
|
|
502
|
+
* installTimeout: "3 minutes",
|
|
503
|
+
* packTimeout: "30 seconds",
|
|
504
|
+
* packages: ["my-tool", "my-tool-cli", "my-tool-core"],
|
|
505
|
+
* perConsumer: "2 minutes",
|
|
506
|
+
* });
|
|
507
|
+
* console.log(Duration.format(budget));
|
|
508
|
+
* // => 26m 30s
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
static timeoutBudget = (budget) => {
|
|
512
|
+
const perManager = [
|
|
513
|
+
PROBE_TIMEOUT,
|
|
514
|
+
budget.installTimeout ?? DEFAULT_INSTALL_TIMEOUT,
|
|
515
|
+
budget.perConsumer ?? DEFAULT_BIN_TIMEOUT
|
|
516
|
+
].map(Duration.fromInputUnsafe).reduce((total, step) => Duration.sum(total, step), Duration.zero);
|
|
517
|
+
const perPackage = Duration.sum(Duration.fromInputUnsafe(budget.packTimeout ?? DEFAULT_PACK_TIMEOUT), Duration.fromInputUnsafe(MANIFEST_TIMEOUT));
|
|
518
|
+
const packages = typeof budget.packages === "number" ? budget.packages : budget.packages.length;
|
|
519
|
+
return Duration.sum(Duration.sum(Duration.times(perManager, new Set(budget.managers).size), Duration.times(perPackage, Math.max(1, packages))), Duration.sum(Duration.fromInputUnsafe(UNTIMED_SLACK), Duration.fromInputUnsafe(CLEANUP_ALLOWANCE)));
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* The packages a run packs, in the order it packs them, without packing
|
|
523
|
+
* anything: the carrier, the rest of the closure, then the `overrides`
|
|
524
|
+
* packages by name.
|
|
525
|
+
*
|
|
526
|
+
* @remarks
|
|
527
|
+
* The same implementation {@link PackedInstall.run} plans with, so for the
|
|
528
|
+
* same workspace and options the result equals the keys of
|
|
529
|
+
* `PackedInstallResult.tarballs`, in order. `options.closure` defaults to
|
|
530
|
+
* `"auto"`, and the run's own options object is accepted as it is. It
|
|
531
|
+
* fails as the run would before packing: `UnknownPackage`, `Discovery`,
|
|
532
|
+
* `InvalidOverride` or `Io`. Use it to size
|
|
533
|
+
* {@link PackedInstall.timeoutBudget} before the test is declared.
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* ```ts
|
|
537
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
538
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
539
|
+
* import { PackedInstall } from "@effected/workspaces/testing";
|
|
540
|
+
* import { Effect, Layer } from "effect";
|
|
541
|
+
*
|
|
542
|
+
* const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
|
|
543
|
+
* const packages = await Effect.runPromise(PackedInstall.closure("my-tool").pipe(Effect.provide(Live)));
|
|
544
|
+
* const budget = PackedInstall.timeoutBudget({ managers: ["npm", "pnpm"], packages });
|
|
545
|
+
* ```
|
|
546
|
+
*
|
|
547
|
+
* @param carrier - The carrier, as the run's `carrier`.
|
|
548
|
+
* @param options - The closure and override options, as the run takes them.
|
|
549
|
+
*/
|
|
550
|
+
static closure = (carrier, options = {}) => planClosure(carrier, options).pipe(Effect.map(namesOf), Effect.withSpan("PackedInstall.closure"));
|
|
551
|
+
/**
|
|
552
|
+
* {@link PackedInstall.timeoutBudget} for exactly the run `options`
|
|
553
|
+
* describes, planned from the options themselves.
|
|
554
|
+
*
|
|
555
|
+
* @remarks
|
|
556
|
+
* Plans the closure with {@link PackedInstall.closure} (the run's own
|
|
557
|
+
* planner, `overrides` and `workspaceOverrides` included) and takes
|
|
558
|
+
* `managers`, `installTimeout` and `packTimeout` from the same object, so
|
|
559
|
+
* nothing is written twice. `perConsumer` is the one input a run's options
|
|
560
|
+
* do not carry. It is an Effect because planning reads the workspace; in a
|
|
561
|
+
* vitest file, await it at module evaluation, since a test's timeout is
|
|
562
|
+
* fixed when the test is declared. Fails as `closure` does.
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* ```ts
|
|
566
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
567
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
568
|
+
* import type { PackedInstallOptions } from "@effected/workspaces/testing";
|
|
569
|
+
* import { PackedInstall } from "@effected/workspaces/testing";
|
|
570
|
+
* import { Duration, Effect, Layer } from "effect";
|
|
571
|
+
*
|
|
572
|
+
* const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
|
|
573
|
+
* const RUN: PackedInstallOptions = {
|
|
574
|
+
* carrier: "my-tool",
|
|
575
|
+
* closure: "auto",
|
|
576
|
+
* managers: ["npm", "pnpm"],
|
|
577
|
+
* bins: ["my-tool"],
|
|
578
|
+
* env: process.env,
|
|
579
|
+
* packTimeout: "30 seconds",
|
|
580
|
+
* };
|
|
581
|
+
* const budget = await Effect.runPromise(
|
|
582
|
+
* PackedInstall.timeoutBudgetFor(RUN, { perConsumer: "1 minute" }).pipe(Effect.provide(Live)),
|
|
583
|
+
* );
|
|
584
|
+
* const TEST_TIMEOUT_MS = Duration.toMillis(budget) + 60_000;
|
|
585
|
+
* ```
|
|
586
|
+
*
|
|
587
|
+
* @param options - The run's options, the same object passed to `run`.
|
|
588
|
+
* @param extra - What the test does with each consumer afterwards.
|
|
589
|
+
*/
|
|
590
|
+
static timeoutBudgetFor = (options, extra = {}) => PackedInstall.closure(options.carrier, options).pipe(Effect.map((packages) => PackedInstall.timeoutBudget({
|
|
591
|
+
managers: options.managers,
|
|
592
|
+
packages,
|
|
593
|
+
...options.installTimeout === void 0 ? {} : { installTimeout: options.installTimeout },
|
|
594
|
+
...options.packTimeout === void 0 ? {} : { packTimeout: options.packTimeout },
|
|
595
|
+
...extra.perConsumer === void 0 ? {} : { perConsumer: extra.perConsumer }
|
|
596
|
+
})));
|
|
147
597
|
/** Pack, then install under every available manager. */
|
|
148
598
|
static run = Effect.fn("PackedInstall.run")(function* (options) {
|
|
149
599
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -160,7 +610,7 @@ var PackedInstall = class {
|
|
|
160
610
|
stdin: "ignore"
|
|
161
611
|
});
|
|
162
612
|
const scratch = yield* fs.makeTempDirectoryScoped({ prefix: "effected-packed-install-" }).pipe(Effect.flatMap((directory) => fs.realPath(directory)), Effect.mapError(io("could not create the scratch directory")));
|
|
163
|
-
const probes = yield* Effect.forEach(managers, (manager) => Run.collect(command(manager, ["--version"], scratch), { timeout:
|
|
613
|
+
const probes = yield* Effect.forEach(managers, (manager) => Run.collect(command(manager, ["--version"], scratch), { timeout: PROBE_TIMEOUT }).pipe(Effect.map((output) => output.succeeded ? versionOf(output.stdout) : void 0), Effect.catch(() => Effect.succeed(void 0)), Effect.map((version) => ({
|
|
164
614
|
manager,
|
|
165
615
|
version
|
|
166
616
|
}))));
|
|
@@ -172,18 +622,13 @@ var PackedInstall = class {
|
|
|
172
622
|
if (available.length === 0) return yield* failure("NoManagerAvailable", `none of ${managers.join(", ")} answered --version from ${scratch}`);
|
|
173
623
|
const missing = unavailable[0];
|
|
174
624
|
if (options.require === "all" && missing !== void 0) return yield* failure("ManagerUnavailable", `${missing} is required but did not answer --version`, { manager: missing });
|
|
175
|
-
const
|
|
176
|
-
const closure = closureOf(packages, options.carrier, options.closure);
|
|
177
|
-
if (Result.isFailure(closure)) return yield* failure("UnknownPackage", `${closure.failure} is not a package of this workspace`, { package: closure.failure });
|
|
625
|
+
const plan = yield* planClosure(options.carrier, options);
|
|
178
626
|
const source = options.packFrom ?? DEFAULT_PACK_FROM;
|
|
179
|
-
const
|
|
627
|
+
const packTimeout = options.packTimeout ?? DEFAULT_PACK_TIMEOUT;
|
|
628
|
+
/** `npm pack` or `pnpm pack` in `cwd` into an empty `destination`; the one tarball it wrote. */
|
|
629
|
+
const pack = (name, cwd, packer, destination) => Effect.gen(function* () {
|
|
180
630
|
yield* fs.makeDirectory(destination, { recursive: true }).pipe(Effect.mapError(io(`could not create ${destination}`)));
|
|
181
|
-
const
|
|
182
|
-
if (source !== "source") {
|
|
183
|
-
if (!(yield* fs.exists(path.join(cwd, "package.json")).pipe(Effect.mapError(io(`could not inspect ${cwd}`))))) return yield* failure("PackSourceMissing", `${cwd} has no package.json; build ${pkg.name} before packing it`, { package: pkg.name });
|
|
184
|
-
}
|
|
185
|
-
const packer = source === "source" ? "pnpm" : "npm";
|
|
186
|
-
const args = source === "source" ? [
|
|
631
|
+
const args = packer === "pnpm" ? [
|
|
187
632
|
"pack",
|
|
188
633
|
"--pack-destination",
|
|
189
634
|
destination,
|
|
@@ -194,44 +639,75 @@ var PackedInstall = class {
|
|
|
194
639
|
"--pack-destination",
|
|
195
640
|
destination
|
|
196
641
|
];
|
|
197
|
-
const output = yield* Run.collect(command(packer, args, cwd), { timeout:
|
|
198
|
-
package:
|
|
642
|
+
const output = yield* Run.collect(command(packer, args, cwd), { timeout: packTimeout }).pipe(Effect.mapError((cause) => failure("PackFailed", cause._tag === "CommandFailedError" && cause.kind === "timeout" ? `${packer} pack timed out after ${describeDuration(packTimeout)} for ${name}` : `${packer} pack could not run for ${name}`, {
|
|
643
|
+
package: name,
|
|
199
644
|
cause
|
|
200
645
|
})));
|
|
201
646
|
if (!output.succeeded) {
|
|
202
|
-
const notInstalled =
|
|
203
|
-
return yield* failure("PackFailed", notInstalled ? `pnpm pack could not rewrite the workspace: specifiers of ${
|
|
204
|
-
package:
|
|
647
|
+
const notInstalled = packer === "pnpm" && `${output.stdout}${output.stderr}`.includes(WORKSPACE_PROTOCOL_NOT_INSTALLED);
|
|
648
|
+
return yield* failure("PackFailed", notInstalled ? `pnpm pack could not rewrite the workspace: specifiers of ${name} because the workspace is not installed; run pnpm install in the workspace first` : `${packer} pack exited ${output.exitCode} for ${name}`, {
|
|
649
|
+
package: name,
|
|
205
650
|
output: tail(`${output.stdout}\n${output.stderr}`)
|
|
206
651
|
});
|
|
207
652
|
}
|
|
208
|
-
const found = (yield* fs.readDirectory(destination).pipe(Effect.mapError(io(`could not list ${destination}`)))).filter((
|
|
653
|
+
const found = (yield* fs.readDirectory(destination).pipe(Effect.mapError(io(`could not list ${destination}`)))).filter((file) => file.endsWith(".tgz"));
|
|
209
654
|
const [only] = found;
|
|
210
|
-
if (only === void 0 || found.length !== 1) return yield* failure("PackFailed", `expected exactly one tarball for ${
|
|
211
|
-
|
|
212
|
-
|
|
655
|
+
if (only === void 0 || found.length !== 1) return yield* failure("PackFailed", `expected exactly one tarball for ${name} in ${destination}, found ${found.length}`, { package: name });
|
|
656
|
+
return path.join(destination, only);
|
|
657
|
+
});
|
|
658
|
+
/** The packed manifest, read with `tar` from `cwd`, refused if only a workspace could resolve it. */
|
|
659
|
+
const inspect = (name, tarball, cwd) => Effect.gen(function* () {
|
|
660
|
+
const text = yield* Run.text(command("tar", [
|
|
213
661
|
"-xzOf",
|
|
214
662
|
tarball,
|
|
215
663
|
"package/package.json"
|
|
216
|
-
],
|
|
217
|
-
package:
|
|
664
|
+
], cwd), { timeout: MANIFEST_TIMEOUT }).pipe(Effect.mapError((cause) => failure("PackFailed", `could not read package/package.json out of ${tarball}`, {
|
|
665
|
+
package: name,
|
|
218
666
|
cause
|
|
219
667
|
})));
|
|
220
|
-
const
|
|
221
|
-
if (Result.isFailure(
|
|
222
|
-
package:
|
|
223
|
-
cause:
|
|
668
|
+
const manifest = readPackedManifest(text);
|
|
669
|
+
if (Result.isFailure(manifest)) return yield* failure("PackFailed", `the packed package.json of ${name} is not a JSON object`, {
|
|
670
|
+
package: name,
|
|
671
|
+
cause: manifest.failure
|
|
224
672
|
});
|
|
225
|
-
if (
|
|
226
|
-
return
|
|
673
|
+
if (manifest.success.unresolved.length > 0) return yield* failure("UnresolvedProtocol", `${name}'s packed manifest still carries ${manifest.success.unresolved.join(", ")}, which no consumer outside the workspace can resolve; pack from "source" so pnpm rewrites workspace: and catalog:, or fix the build`, { package: name });
|
|
674
|
+
return manifest.success;
|
|
227
675
|
});
|
|
228
676
|
const packed = [];
|
|
229
|
-
for (const [index, pkg] of closure.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
677
|
+
for (const [index, pkg] of plan.closure.entries()) {
|
|
678
|
+
const destination = path.join(scratch, "tarballs", String(index));
|
|
679
|
+
const cwd = source === "source" ? pkg.path : path.join(pkg.path, source.directory);
|
|
680
|
+
if (source !== "source") {
|
|
681
|
+
if (!(yield* fs.exists(path.join(cwd, "package.json")).pipe(Effect.mapError(io(`could not inspect ${cwd}`))))) return yield* failure("PackSourceMissing", `${cwd} has no package.json; build ${pkg.name} before packing it`, { package: pkg.name });
|
|
682
|
+
}
|
|
683
|
+
const tarball = yield* pack(pkg.name, cwd, source === "source" ? "pnpm" : "npm", destination);
|
|
684
|
+
packed.push({
|
|
685
|
+
name: pkg.name,
|
|
686
|
+
tarball,
|
|
687
|
+
manifest: yield* inspect(pkg.name, tarball, destination)
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
for (const [offset, replacement] of plan.replacements.entries()) {
|
|
691
|
+
const destination = path.join(scratch, "tarballs", String(plan.closure.length + offset));
|
|
692
|
+
const tarball = replacement.kind === "tarball" ? replacement.source : yield* pack(replacement.name, replacement.source, "npm", destination);
|
|
693
|
+
const manifest = yield* inspect(replacement.name, tarball, scratch);
|
|
694
|
+
if (manifest.name !== replacement.name) return yield* failure("InvalidOverride", `override ${replacement.name}: ${replacement.source} packs ${manifest.name ?? "a package with no name"}, not ${replacement.name}`, { package: replacement.name });
|
|
695
|
+
packed.push({
|
|
696
|
+
name: replacement.name,
|
|
697
|
+
tarball,
|
|
698
|
+
manifest
|
|
699
|
+
});
|
|
700
|
+
}
|
|
233
701
|
const [carrier, ...rest] = packed;
|
|
234
702
|
if (carrier === void 0) return yield* failure("UnknownPackage", `${options.carrier} was not packed`, { package: options.carrier });
|
|
703
|
+
const conflict = options.allowSharedBins === true ? void 0 : binConflict({
|
|
704
|
+
name: carrier.name,
|
|
705
|
+
bins: carrier.manifest.bins
|
|
706
|
+
}, rest.map(({ name, manifest }) => ({
|
|
707
|
+
name,
|
|
708
|
+
bins: manifest.bins
|
|
709
|
+
})));
|
|
710
|
+
if (conflict !== void 0) return yield* failure("BinConflict", `${conflict.package} declares the bin ${conflict.bin}, which the carrier ${carrier.name} declares; under a flat layout (npm, bun, Yarn's node-modules linker) either can take node_modules/.bin/${conflict.bin} and the carrier's provenance is lost there; drop the bin from ${conflict.package}, or pass allowSharedBins: true to share it deliberately`, { package: conflict.package });
|
|
235
711
|
const overrides = Object.fromEntries(rest.map(({ name, tarball }) => [name, tarball]));
|
|
236
712
|
const consumers = yield* Effect.forEach(available, ({ manager, version }) => Effect.gen(function* () {
|
|
237
713
|
const directory = path.join(scratch, `consumer-${manager}`);
|
|
@@ -255,7 +731,9 @@ var PackedInstall = class {
|
|
|
255
731
|
const consumer = InstalledConsumer.make({
|
|
256
732
|
manager,
|
|
257
733
|
managerVersion: version,
|
|
258
|
-
directory
|
|
734
|
+
directory,
|
|
735
|
+
env: Redacted.make(env),
|
|
736
|
+
carrier: carrier.name
|
|
259
737
|
});
|
|
260
738
|
for (const bin of options.bins) {
|
|
261
739
|
const info = yield* Effect.option(fs.stat(consumer.binPath(bin)));
|
|
@@ -273,7 +751,8 @@ var PackedInstall = class {
|
|
|
273
751
|
return PackedInstallResult.make({
|
|
274
752
|
consumers,
|
|
275
753
|
unavailable,
|
|
276
|
-
tarballs: Object.fromEntries(packed.map(({ name, tarball }) => [name, tarball]))
|
|
754
|
+
tarballs: Object.fromEntries(packed.map(({ name, tarball }) => [name, tarball])),
|
|
755
|
+
scratch
|
|
277
756
|
});
|
|
278
757
|
});
|
|
279
758
|
};
|