@effected/workspaces 0.9.6 → 0.10.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.
@@ -1,8 +1,10 @@
1
1
  import { normalize } from "./internal/catalogs.js";
2
- import { Context, Effect, Layer, Predicate, Result } from "effect";
2
+ import { Context, Duration, Effect, Layer, Predicate, Result, Schema } from "effect";
3
3
  import { CatalogAssemblyError } from "@effected/npm";
4
4
  import { join } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import { Run } from "@effected/commands";
7
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
6
8
 
7
9
  //#region src/ConfigDependencyHooks.ts
8
10
  /** Whether `value` is a non-null, non-array object. */
@@ -97,6 +99,137 @@ const updateConfigOf = (mod) => {
97
99
  }
98
100
  };
99
101
  /**
102
+ * The subprocess replay program {@link ConfigDependencyHooks.layerSubprocess}
103
+ * hands to `node --input-type=module -e`.
104
+ *
105
+ * @remarks
106
+ * A **static** string constant, deliberately: a bundler compiles a *computed*
107
+ * dynamic `import()` into a context module that cannot resolve a runtime path
108
+ * (`Cannot find module 'file:///…'`), so the computed import has to run in a
109
+ * child process whose program text carries **no interpolated runtime value** —
110
+ * the workspace root, the seed and the dependency names all arrive via argv
111
+ * (`process.argv.slice(1)` under `-e`), never spliced into the script.
112
+ *
113
+ * The script mirrors this module's in-process semantics exactly so the two
114
+ * layers are drop-in interchangeable: `pnpmfile.mjs` before `pnpmfile.cjs`
115
+ * (pnpm 11's loader order), the `ERR_MODULE_NOT_FOUND`-for-the-candidate-itself
116
+ * skip discrimination (`err.url` equality), the same hook-locator shapes, the
117
+ * same tolerant threading of returned data (`configOf` / `finiteNumberOr` /
118
+ * `stringArrayOr` clones below), and the same synchronous hook call. Only the
119
+ * *mechanism* failures are reported: the script prints one final line of JSON —
120
+ * `{ ok: true, config }` on success, `{ ok: false, name, message, stack? }`
121
+ * naming the offending dependency on a load/replay failure — and exits through
122
+ * the write callback so the payload is flushed even if a hook left the event
123
+ * loop occupied. The final-line framing tolerates a hook's own `console.log`
124
+ * noise on stdout.
125
+ */
126
+ const REPLAY_SCRIPT = `
127
+ const [root, seedJson, ...names] = process.argv.slice(1);
128
+ const { pathToFileURL } = await import("node:url");
129
+ const { join } = await import("node:path");
130
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
131
+ const finiteNumberOr = (value, fallback) => (typeof value === "number" && Number.isFinite(value) ? value : fallback);
132
+ const stringArrayOr = (value, fallback) =>
133
+ Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : fallback;
134
+ const configOf = (value, fallback) =>
135
+ isObject(value)
136
+ ? {
137
+ catalog: isObject(value.catalog) ? value.catalog : fallback.catalog,
138
+ catalogs: isObject(value.catalogs) ? value.catalogs : fallback.catalogs,
139
+ minimumReleaseAge: finiteNumberOr(value.minimumReleaseAge, fallback.minimumReleaseAge),
140
+ minimumReleaseAgeExclude: stringArrayOr(value.minimumReleaseAgeExclude, fallback.minimumReleaseAgeExclude),
141
+ }
142
+ : fallback;
143
+ const failure = (name, cause) => ({
144
+ ok: false,
145
+ name,
146
+ message: cause instanceof Error ? cause.message : String(cause),
147
+ ...(cause instanceof Error && typeof cause.stack === "string" ? { stack: cause.stack } : {}),
148
+ });
149
+ const replay = async () => {
150
+ let catalog = {};
151
+ const catalogs = {};
152
+ for (const [name, entries] of Object.entries(JSON.parse(seedJson))) {
153
+ if (name === "default") catalog = { ...entries };
154
+ else catalogs[name] = { ...entries };
155
+ }
156
+ let config = { catalog, catalogs, minimumReleaseAge: undefined, minimumReleaseAgeExclude: undefined };
157
+ for (const name of names) {
158
+ let loaded;
159
+ let found = false;
160
+ for (const filename of ["pnpmfile.mjs", "pnpmfile.cjs"]) {
161
+ const candidate = pathToFileURL(join(root, "node_modules", ".pnpm-config", name, filename)).href;
162
+ try {
163
+ loaded = await import(candidate);
164
+ found = true;
165
+ break;
166
+ } catch (cause) {
167
+ const url =
168
+ isObject(cause) && cause.code === "ERR_MODULE_NOT_FOUND" && typeof cause.url === "string"
169
+ ? cause.url
170
+ : undefined;
171
+ if (url === candidate) continue;
172
+ return failure(name, cause);
173
+ }
174
+ }
175
+ if (!found) continue;
176
+ let hook;
177
+ for (const candidate of [loaded, isObject(loaded) ? loaded.default : undefined]) {
178
+ if (!isObject(candidate)) continue;
179
+ if (isObject(candidate.hooks) && typeof candidate.hooks.updateConfig === "function") {
180
+ hook = candidate.hooks.updateConfig;
181
+ break;
182
+ }
183
+ if (typeof candidate.updateConfig === "function") {
184
+ hook = candidate.updateConfig;
185
+ break;
186
+ }
187
+ }
188
+ if (hook === undefined) continue;
189
+ try {
190
+ config = configOf(hook(config), config);
191
+ } catch (cause) {
192
+ return failure(name, cause);
193
+ }
194
+ }
195
+ return { ok: true, config };
196
+ };
197
+ const payload = await replay();
198
+ process.stdout.write("\\n" + JSON.stringify(payload) + "\\n", () => process.exit(0));
199
+ `;
200
+ /**
201
+ * The ceiling on one subprocess replay. A config dependency's `updateConfig`
202
+ * reads and rewrites a config object — no install work, no network — so thirty
203
+ * seconds is generous. Without a ceiling a pnpmfile that loops or awaits a
204
+ * promise that never settles hangs `inject`, and through it the one memoized
205
+ * {@link WorkspaceCatalogs} assemble pass every catalog read blocks on. Expiry
206
+ * kills the child (`Run` scopes it) and surfaces as a `CommandFailedError`
207
+ * through the same typed transport-failure path as any other mechanism failure.
208
+ */
209
+ const REPLAY_TIMEOUT = Duration.seconds(30);
210
+ /**
211
+ * The subprocess protocol payload — the child's final stdout line, framed and
212
+ * parsed by `Run.jsonLine`. The envelope is strict (a payload without a usable
213
+ * `ok` discriminant is a mechanism failure, typed); the `config` slice inside a
214
+ * success stays `Unknown` because a hook's returned *data* is tolerantly
215
+ * threaded (`configOf`), never fatal.
216
+ */
217
+ const ReplayPayload = Schema.Union([Schema.Struct({
218
+ ok: Schema.Literal(true),
219
+ config: Schema.Unknown
220
+ }), Schema.Struct({
221
+ ok: Schema.Literal(false),
222
+ name: Schema.optionalKey(Schema.String),
223
+ message: Schema.optionalKey(Schema.String),
224
+ stack: Schema.optionalKey(Schema.String)
225
+ })]);
226
+ /** Rebuild the subprocess's serialized failure as an `Error`, preserving the child-side stack when it carried one. */
227
+ const replayFailureCause = (payload) => {
228
+ const error = new Error(payload.message ?? "config dependency hook replay failed");
229
+ if (payload.stack !== void 0) error.stack = payload.stack;
230
+ return error;
231
+ };
232
+ /**
100
233
  * Replays a workspace's `configDependencies` `updateConfig` hooks over the inline
101
234
  * catalogs — the opt-in seam that lets hook-injected catalogs participate in
102
235
  * assembly.
@@ -189,6 +322,95 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
189
322
  releaseAge: releaseAgeOf(config)
190
323
  };
191
324
  }) });
325
+ /**
326
+ * The subprocess layer: replays each config dependency's pnpmfile in a `node`
327
+ * child process instead of an in-process dynamic `import()`, with identical
328
+ * typed semantics to {@link ConfigDependencyHooks.layerLive} — the two are
329
+ * drop-in interchangeable.
330
+ *
331
+ * @remarks
332
+ * `layerLive` computes the `import()` path at runtime, and a bundler (rspack,
333
+ * for one) compiles a *computed* dynamic import into a context module that
334
+ * throws `Cannot find module 'file:///…'` at runtime — so in any bundled
335
+ * consumer, a GitHub Action above all, the in-process replay is unreachable.
336
+ * This layer keeps every computed load out of the bundle graph: the replay
337
+ * program is a **static** string constant passed via argv
338
+ * (`node --input-type=module -e <script> <root> <seed> <...names>`), and the
339
+ * child process performs the computed imports where no bundler rewrote them.
340
+ * A subprocess also keeps config-dependency code out of the consumer's own
341
+ * process.
342
+ *
343
+ * The contract's semantics are unchanged, not the downstream fail-open shape:
344
+ * an empty `configDependencies` returns the seed without spawning anything; a
345
+ * `..` path segment in a dependency name fails typed **before** any spawn; a
346
+ * missing pnpmfile (neither `.mjs` nor `.cjs`) is the one legitimate skip,
347
+ * discriminated inside the child by `err.url` equality exactly as
348
+ * `layerLive` discriminates in process; any other load failure — a syntax
349
+ * error, a throwing top level, an `ERR_MODULE_NOT_FOUND` for a module the
350
+ * pnpmfile itself imports — and a hook that throws when called are serialized
351
+ * back per-name and surface typed as a `hooks`-source `CatalogAssemblyError`
352
+ * naming that dependency. A hook's returned *data* stays tolerantly threaded
353
+ * (last well-formed write wins), never fatal. Spawn and transport failures —
354
+ * `node` absent, a non-zero exit without a result payload, unparseable
355
+ * output — fail typed too, never a defect and never a silent skip.
356
+ *
357
+ * Two bounds this layer imposes that `layerLive` cannot: the replay is
358
+ * given thirty seconds (a pnpmfile that loops or awaits forever fails typed
359
+ * instead of hanging the memoized assemble pass — a subprocess is killable,
360
+ * while `layerLive`'s in-process synchronous hook call is not interruptible
361
+ * by any means, so the asymmetry is inherent, not a parity violation), and
362
+ * the child's stdout is captured under `Run.jsonLine`'s 16 MiB default
363
+ * ceiling (a hook that logs more than that fails typed as `tooLarge`, where
364
+ * `layerLive` — which captures nothing — would succeed).
365
+ *
366
+ * Catalog folding and normalization stay in the **parent** (the same
367
+ * `@pnpm/catalogs`-derived path `layerLive` uses); the child returns only the
368
+ * raw threaded config slice, since the script cannot import kit code.
369
+ *
370
+ * Requires core's `ChildProcessSpawner`, resolved when the layer is built —
371
+ * the consumer provides it once at the edge (`@effect/platform-node`'s
372
+ * `NodeServices.layer`), the same discharge `@effected/git` uses. Wired by
373
+ * `WorkspaceCatalogs.layerWithConfigDependenciesSubprocess` /
374
+ * `Workspaces.layerWithConfigDependenciesSubprocess`.
375
+ */
376
+ static layerSubprocess = Layer.effect(ConfigDependencyHooks, Effect.gen(function* () {
377
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
378
+ return { inject: (root, configDependencies, seed) => Effect.gen(function* () {
379
+ const names = Object.keys(configDependencies);
380
+ if (names.length === 0) return {
381
+ catalogs: seed,
382
+ releaseAge: {}
383
+ };
384
+ for (const name of names) if (hasTraversalSegment(name)) return yield* Effect.fail(new CatalogAssemblyError({
385
+ source: "hooks",
386
+ path: name,
387
+ cause: /* @__PURE__ */ new Error(`config dependency name has a '..' path segment: ${name}`)
388
+ }));
389
+ const command = ChildProcess.make("node", [
390
+ "--input-type=module",
391
+ "-e",
392
+ REPLAY_SCRIPT,
393
+ root,
394
+ JSON.stringify(seed),
395
+ ...names
396
+ ]);
397
+ const payload = yield* Run.jsonLine(command, ReplayPayload, { timeout: REPLAY_TIMEOUT }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.catch((cause) => Effect.fail(new CatalogAssemblyError({
398
+ source: "hooks",
399
+ path: root,
400
+ cause
401
+ }))));
402
+ if (payload.ok === false) return yield* Effect.fail(new CatalogAssemblyError({
403
+ source: "hooks",
404
+ path: payload.name ?? root,
405
+ cause: replayFailureCause(payload)
406
+ }));
407
+ const config = configOf(payload.config, seedToConfig(seed));
408
+ return {
409
+ catalogs: configToEntries(config),
410
+ releaseAge: releaseAgeOf(config)
411
+ };
412
+ }) };
413
+ }));
192
414
  };
193
415
 
194
416
  //#endregion
package/README.md CHANGED
@@ -223,6 +223,7 @@ A name miss in the derived `getPackage` fails with the service's own typed `Pack
223
223
  ## Features
224
224
 
225
225
  - `Workspaces.layer` / `Workspaces.layerWithGit` / `Workspaces.resolvers` — the composite layers, split on requirements rather than feature flags: a filesystem, a filesystem plus a subprocess, and the two `@effected/npm` resolver contracts.
226
+ - `Workspaces.layerWithConfigDependencies` / `Workspaces.layerWithConfigDependenciesSubprocess` — opt in to replaying a pnpm config dependency's pnpmfile hooks, which is what lets catalogs and `releaseAgeGate()` see the entries a hook injects. The default layer runs no config-dependency code at all. The two spellings differ only in where the replay happens: in process, or in a `node` child process for a consumer whose code is bundled (a GitHub Action, say), where the in-process form's computed dynamic import cannot survive the bundler. The subprocess form asks for core's `ChildProcessSpawner`; `WorkspaceCatalogs` carries the same pair.
226
227
  - `Workspaces.resolverLayer` / `Workspaces.resolveManifest` — the one-call manifest-resolution path: a fresh, unmemoized layer per call so root discovery follows your cwd, and one-shot resolution of a whole `Manifest` against the real workspace.
227
228
  - `WorkspaceRoot` — root discovery from a `cwd`, over `WORKSPACE_MARKERS`.
228
229
  - `WorkspaceDiscovery` — package enumeration with a bounded descent for segment-crossing `packages/**` patterns, per-package lookup and the `makeTest` / `layerTest` in-memory test doubles.
@@ -428,6 +428,24 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
428
428
  */
429
429
  static layerWithConfigDependencies = (options) => Layer.effect(WorkspaceCatalogs, WorkspaceCatalogs.make(options)).pipe(Layer.provide(ConfigDependencyHooks.layerLive));
430
430
  /**
431
+ * The opt-in layer that replays config-dependency `pnpmfile` hooks in a
432
+ * `node` **child process**: it wires
433
+ * {@link ConfigDependencyHooks.layerSubprocess} in place of the in-process
434
+ * `layerLive`.
435
+ *
436
+ * @remarks
437
+ * Same typed semantics as {@link WorkspaceCatalogs.layerWithConfigDependencies}
438
+ * — the two hook layers are drop-in interchangeable — but the replay's
439
+ * computed dynamic `import()` runs in the subprocess, so it survives bundling
440
+ * (a bundler compiles a computed in-process `import()` into a context module
441
+ * that cannot resolve at runtime — every bundled GitHub Action hits this).
442
+ * The cost is one extra requirement: core's `ChildProcessSpawner`, provided
443
+ * once at the edge (`@effect/platform-node`'s `NodeServices.layer`) — the
444
+ * same sanctioned R-widening as `Workspaces.layerWithGit`. Parameterized, so
445
+ * bind it to a `const` and reuse it.
446
+ */
447
+ static layerWithConfigDependenciesSubprocess = (options) => Layer.effect(WorkspaceCatalogs, WorkspaceCatalogs.make(options)).pipe(Layer.provide(ConfigDependencyHooks.layerSubprocess));
448
+ /**
431
449
  * A test double satisfying the full {@link WorkspaceCatalogsShape} with no
432
450
  * filesystem, lockfile read, or hook replay.
433
451
  *
package/Workspaces.js CHANGED
@@ -26,6 +26,7 @@ const layerWithGit = (options) => {
26
26
  };
27
27
  const resolvers = Layer.mergeAll(WorkspaceCatalogs.catalogResolver, WorkspaceDiscovery.workspaceResolver);
28
28
  const layerWithConfigDependencies = (options) => compose(options, WorkspaceCatalogs.layerWithConfigDependencies);
29
+ const layerWithConfigDependenciesSubprocess = (options) => compose(options, WorkspaceCatalogs.layerWithConfigDependenciesSubprocess);
29
30
  const resolverLayer = (options) => resolvers.pipe(Layer.provide(layerWithConfigDependencies(options)));
30
31
  const resolveManifest = Effect.fn("Workspaces.resolveManifest")(function* (manifest, options) {
31
32
  return yield* manifest.resolve().pipe(Effect.provide(resolverLayer(options)));
@@ -107,6 +108,32 @@ var Workspaces = class {
107
108
  */
108
109
  static layerWithConfigDependencies = layerWithConfigDependencies;
109
110
  /**
111
+ * The git-free composite with config-dependency hook replay in a `node`
112
+ * **child process** —
113
+ * {@link WorkspaceCatalogs.layerWithConfigDependenciesSubprocess} in place of
114
+ * the in-process replay.
115
+ *
116
+ * @remarks
117
+ * Same typed semantics as {@link Workspaces.layerWithConfigDependencies}; the
118
+ * difference is mechanism, and it matters in exactly one environment class: a
119
+ * **bundled** consumer. The in-process replay's computed dynamic `import()`
120
+ * is compiled by bundlers (rspack among them) into a context module that
121
+ * throws `Cannot find module 'file:///…'` at runtime, which makes
122
+ * `WorkspaceCatalogs.releaseAgeGate()` unreachable from any bundled GitHub
123
+ * Action. Here the computed import runs inside a `node` child process whose
124
+ * program text is a static string handed over argv, so nothing computed
125
+ * enters the bundle graph.
126
+ *
127
+ * The extra requirement is core's `ChildProcessSpawner`, provided once at
128
+ * the edge (`@effect/platform-node`'s `NodeServices.layer`) — the same
129
+ * sanctioned R-widening as {@link Workspaces.layerWithGit}, and the reason
130
+ * this is a separate composite rather than a flag: a consumer that keeps the
131
+ * in-process replay should not have to be able to spawn a subprocess.
132
+ *
133
+ * **Bind the result to a `const`.**
134
+ */
135
+ static layerWithConfigDependenciesSubprocess = layerWithConfigDependenciesSubprocess;
136
+ /**
110
137
  * The git-free composite plus {@link ChangeDetector} and
111
138
  * {@link WorkspaceSnapshots}, over `@effected/git`'s `Git` service.
112
139
  *
package/index.d.ts CHANGED
@@ -4,8 +4,8 @@ import { CatalogAssemblyError, CatalogResolver, DependencyResolutionError, Manif
4
4
  import { GlobPattern } from "@effected/glob";
5
5
  import { Lockfile, LockfileFramingError, LockfileIntegrity, LockfileParseError, ResolvedPackage, WorkspaceManifest } from "@effected/lockfiles";
6
6
  import { Package } from "@effected/package-json";
7
- import { LocalExec } from "@effected/commands";
8
7
  import { ChildProcessSpawner } from "effect/unstable/process";
8
+ import { LocalExec } from "@effected/commands";
9
9
  //#region src/WorkspacePackage.d.ts
10
10
  declare const PublishConfig_base: Schema.Class<PublishConfig, Schema.Struct<{
11
11
  /** Scoped-package visibility. Its presence overrides `private`. */
@@ -896,6 +896,58 @@ declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
896
896
  * `WorkspaceCatalogs.layerWithConfigDependencies`.
897
897
  */
898
898
  static readonly layerLive: Layer.Layer<ConfigDependencyHooks>;
899
+ /**
900
+ * The subprocess layer: replays each config dependency's pnpmfile in a `node`
901
+ * child process instead of an in-process dynamic `import()`, with identical
902
+ * typed semantics to {@link ConfigDependencyHooks.layerLive} — the two are
903
+ * drop-in interchangeable.
904
+ *
905
+ * @remarks
906
+ * `layerLive` computes the `import()` path at runtime, and a bundler (rspack,
907
+ * for one) compiles a *computed* dynamic import into a context module that
908
+ * throws `Cannot find module 'file:///…'` at runtime — so in any bundled
909
+ * consumer, a GitHub Action above all, the in-process replay is unreachable.
910
+ * This layer keeps every computed load out of the bundle graph: the replay
911
+ * program is a **static** string constant passed via argv
912
+ * (`node --input-type=module -e <script> <root> <seed> <...names>`), and the
913
+ * child process performs the computed imports where no bundler rewrote them.
914
+ * A subprocess also keeps config-dependency code out of the consumer's own
915
+ * process.
916
+ *
917
+ * The contract's semantics are unchanged, not the downstream fail-open shape:
918
+ * an empty `configDependencies` returns the seed without spawning anything; a
919
+ * `..` path segment in a dependency name fails typed **before** any spawn; a
920
+ * missing pnpmfile (neither `.mjs` nor `.cjs`) is the one legitimate skip,
921
+ * discriminated inside the child by `err.url` equality exactly as
922
+ * `layerLive` discriminates in process; any other load failure — a syntax
923
+ * error, a throwing top level, an `ERR_MODULE_NOT_FOUND` for a module the
924
+ * pnpmfile itself imports — and a hook that throws when called are serialized
925
+ * back per-name and surface typed as a `hooks`-source `CatalogAssemblyError`
926
+ * naming that dependency. A hook's returned *data* stays tolerantly threaded
927
+ * (last well-formed write wins), never fatal. Spawn and transport failures —
928
+ * `node` absent, a non-zero exit without a result payload, unparseable
929
+ * output — fail typed too, never a defect and never a silent skip.
930
+ *
931
+ * Two bounds this layer imposes that `layerLive` cannot: the replay is
932
+ * given thirty seconds (a pnpmfile that loops or awaits forever fails typed
933
+ * instead of hanging the memoized assemble pass — a subprocess is killable,
934
+ * while `layerLive`'s in-process synchronous hook call is not interruptible
935
+ * by any means, so the asymmetry is inherent, not a parity violation), and
936
+ * the child's stdout is captured under `Run.jsonLine`'s 16 MiB default
937
+ * ceiling (a hook that logs more than that fails typed as `tooLarge`, where
938
+ * `layerLive` — which captures nothing — would succeed).
939
+ *
940
+ * Catalog folding and normalization stay in the **parent** (the same
941
+ * `@pnpm/catalogs`-derived path `layerLive` uses); the child returns only the
942
+ * raw threaded config slice, since the script cannot import kit code.
943
+ *
944
+ * Requires core's `ChildProcessSpawner`, resolved when the layer is built —
945
+ * the consumer provides it once at the edge (`@effect/platform-node`'s
946
+ * `NodeServices.layer`), the same discharge `@effected/git` uses. Wired by
947
+ * `WorkspaceCatalogs.layerWithConfigDependenciesSubprocess` /
948
+ * `Workspaces.layerWithConfigDependenciesSubprocess`.
949
+ */
950
+ static readonly layerSubprocess: Layer.Layer<ConfigDependencyHooks, never, ChildProcessSpawner.ChildProcessSpawner>;
899
951
  }
900
952
  //#endregion
901
953
  //#region src/DependencyGraph.d.ts
@@ -2097,6 +2149,24 @@ declare class WorkspaceCatalogs extends WorkspaceCatalogs_base {
2097
2149
  * Parameterized, so bind it to a `const` and reuse it.
2098
2150
  */
2099
2151
  static readonly layerWithConfigDependencies: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path>;
2152
+ /**
2153
+ * The opt-in layer that replays config-dependency `pnpmfile` hooks in a
2154
+ * `node` **child process**: it wires
2155
+ * {@link ConfigDependencyHooks.layerSubprocess} in place of the in-process
2156
+ * `layerLive`.
2157
+ *
2158
+ * @remarks
2159
+ * Same typed semantics as {@link WorkspaceCatalogs.layerWithConfigDependencies}
2160
+ * — the two hook layers are drop-in interchangeable — but the replay's
2161
+ * computed dynamic `import()` runs in the subprocess, so it survives bundling
2162
+ * (a bundler compiles a computed in-process `import()` into a context module
2163
+ * that cannot resolve at runtime — every bundled GitHub Action hits this).
2164
+ * The cost is one extra requirement: core's `ChildProcessSpawner`, provided
2165
+ * once at the edge (`@effect/platform-node`'s `NodeServices.layer`) — the
2166
+ * same sanctioned R-widening as `Workspaces.layerWithGit`. Parameterized, so
2167
+ * bind it to a `const` and reuse it.
2168
+ */
2169
+ static readonly layerWithConfigDependenciesSubprocess: (options?: WorkspaceCatalogsOptions) => Layer.Layer<WorkspaceCatalogs, never, WorkspaceRoot | LockfileReader | FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
2100
2170
  /**
2101
2171
  * A test double satisfying the full {@link WorkspaceCatalogsShape} with no
2102
2172
  * filesystem, lockfile read, or hook replay.
@@ -2569,6 +2639,32 @@ declare class Workspaces {
2569
2639
  * **Bind the result to a `const`.**
2570
2640
  */
2571
2641
  static readonly layerWithConfigDependencies: (options?: WorkspacesOptions) => Layer.Layer<WorkspacesServices, never, FileSystem.FileSystem | Path.Path>;
2642
+ /**
2643
+ * The git-free composite with config-dependency hook replay in a `node`
2644
+ * **child process** —
2645
+ * {@link WorkspaceCatalogs.layerWithConfigDependenciesSubprocess} in place of
2646
+ * the in-process replay.
2647
+ *
2648
+ * @remarks
2649
+ * Same typed semantics as {@link Workspaces.layerWithConfigDependencies}; the
2650
+ * difference is mechanism, and it matters in exactly one environment class: a
2651
+ * **bundled** consumer. The in-process replay's computed dynamic `import()`
2652
+ * is compiled by bundlers (rspack among them) into a context module that
2653
+ * throws `Cannot find module 'file:///…'` at runtime, which makes
2654
+ * `WorkspaceCatalogs.releaseAgeGate()` unreachable from any bundled GitHub
2655
+ * Action. Here the computed import runs inside a `node` child process whose
2656
+ * program text is a static string handed over argv, so nothing computed
2657
+ * enters the bundle graph.
2658
+ *
2659
+ * The extra requirement is core's `ChildProcessSpawner`, provided once at
2660
+ * the edge (`@effect/platform-node`'s `NodeServices.layer`) — the same
2661
+ * sanctioned R-widening as {@link Workspaces.layerWithGit}, and the reason
2662
+ * this is a separate composite rather than a flag: a consumer that keeps the
2663
+ * in-process replay should not have to be able to spawn a subprocess.
2664
+ *
2665
+ * **Bind the result to a `const`.**
2666
+ */
2667
+ static readonly layerWithConfigDependenciesSubprocess: (options?: WorkspacesOptions) => Layer.Layer<WorkspacesServices, never, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
2572
2668
  /**
2573
2669
  * The git-free composite plus {@link ChangeDetector} and
2574
2670
  * {@link WorkspaceSnapshots}, over `@effected/git`'s `Git` service.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Monorepo workspace tooling as Effect services — root discovery, package enumeration, the dependency graph, package-manager detection, pnpm catalog resolution, lockfile IO and git-based change detection.",
6
6
  "keywords": [
@@ -46,11 +46,11 @@
46
46
  "./package.json": "./package.json"
47
47
  },
48
48
  "dependencies": {
49
- "@effected/commands": "^0.2.1",
49
+ "@effected/commands": "^0.3.0",
50
50
  "@effected/git": "^0.5.2",
51
51
  "@effected/glob": "^0.2.2",
52
52
  "@effected/lockfiles": "^0.3.2",
53
- "@effected/npm": "^0.8.2",
53
+ "@effected/npm": "^0.8.3",
54
54
  "@effected/package-json": "^0.7.3",
55
55
  "@effected/semver": "^0.3.2",
56
56
  "@effected/walker": "^0.3.4",