@effected/workspaces 0.28.0-next-20260927062352 → 0.28.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/ConfigDependencyHooks.js +13 -1
- package/LockfileReader.js +2 -1
- package/WorkspaceCatalogs.js +2 -1
- package/WorkspaceDiscovery.js +2 -1
- package/WorkspaceRoot.js +4 -1
- package/WorkspaceSnapshots.js +2 -1
- package/Workspaces.js +6 -2
- package/index.d.ts +116 -5
- package/internal/configDependencyFetch.js +39 -6
- package/internal/configDependencyResolution.js +1 -0
- package/internal/layerRoot.js +14 -0
- package/package.json +4 -4
- package/testing.d.ts +27 -0
package/ConfigDependencyHooks.js
CHANGED
|
@@ -482,7 +482,19 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
|
|
|
482
482
|
* with `reason: "integrityUnavailable"` — nothing is fetched in either case
|
|
483
483
|
* — and a fetch that fails with `reason: "fetchFailed"`, keeping the
|
|
484
484
|
* not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
|
|
485
|
-
* registry access, and is bounded at two minutes.
|
|
485
|
+
* registry access, and is bounded at two minutes.
|
|
486
|
+
*
|
|
487
|
+
* The scratch workspace fetches through the same registries a
|
|
488
|
+
* `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
|
|
489
|
+
* registries, mirrors, auth) is copied into it as-is, with `${VAR}`
|
|
490
|
+
* references left for pnpm to expand, never read or logged, and removed
|
|
491
|
+
* with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
|
|
492
|
+
* `registries` keys are carried into the scratch's. That is the CURRENT
|
|
493
|
+
* checkout's registry config for both sides of a diff — a base ref's
|
|
494
|
+
* `.npmrc` is not read through git — and relative paths inside the `.npmrc`
|
|
495
|
+
* (a `cafile=./ca.pem`) resolve against the scratch, not the root.
|
|
496
|
+
*
|
|
497
|
+
* An empty `configDependencies`, or one whose dependencies all
|
|
486
498
|
* ship no pnpmfile, returns the seed without spawning anything; a `..` path
|
|
487
499
|
* segment in a dependency name fails typed **before** any spawn; a
|
|
488
500
|
* load failure in the child — a syntax error, a throwing top level, an
|
package/LockfileReader.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findLayerRoot } from "./internal/layerRoot.js";
|
|
1
2
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
2
3
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
3
4
|
import { PackageManagerDetector } from "./PackageManagerName.js";
|
|
@@ -60,7 +61,7 @@ var LockfileReader = class LockfileReader extends Context.Service()("@effected/w
|
|
|
60
61
|
const fs = yield* FileSystem.FileSystem;
|
|
61
62
|
const path = yield* Path.Path;
|
|
62
63
|
const init = Effect.gen(function* () {
|
|
63
|
-
const root = yield*
|
|
64
|
+
const root = yield* findLayerRoot(roots, options);
|
|
64
65
|
const format = (yield* detector.detect(root)).name;
|
|
65
66
|
const lockfilePath = path.join(root, filenameFor(format));
|
|
66
67
|
const content = yield* fs.readFileString(lockfilePath).pipe(Effect.mapError((cause) => new LockfileReadError({
|
package/WorkspaceCatalogs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findLayerRoot } from "./internal/layerRoot.js";
|
|
1
2
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
2
3
|
import { inlineCatalogs, merge, normalize, rangeOf } from "./internal/catalogs.js";
|
|
3
4
|
import { ConfigDependencyHooks, NoPeerDependencyRules } from "./ConfigDependencyHooks.js";
|
|
@@ -342,7 +343,7 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
342
343
|
}));
|
|
343
344
|
}));
|
|
344
345
|
const assemble = Effect.gen(function* () {
|
|
345
|
-
const root = yield*
|
|
346
|
+
const root = yield* findLayerRoot(roots, options);
|
|
346
347
|
const lockfileOutputs = yield* lockfiles.read().pipe(Effect.map((lockfile) => ({
|
|
347
348
|
catalogs: CatalogSet.fromLockfile(lockfile),
|
|
348
349
|
importerVersions: importerVersionsOf(lockfile)
|
package/WorkspaceDiscovery.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { enumerate } from "./internal/enumerate.js";
|
|
2
|
+
import { findLayerRoot } from "./internal/layerRoot.js";
|
|
2
3
|
import { readPatterns } from "./internal/patterns.js";
|
|
3
4
|
import { WorkspacePackage } from "./WorkspacePackage.js";
|
|
4
5
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
@@ -256,7 +257,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
256
257
|
};
|
|
257
258
|
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
258
259
|
const discover = Effect.gen(function* () {
|
|
259
|
-
const root = yield*
|
|
260
|
+
const root = yield* findLayerRoot(roots, options);
|
|
260
261
|
return yield* discoverAt(root);
|
|
261
262
|
});
|
|
262
263
|
const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(discover, Duration.infinity);
|
package/WorkspaceRoot.js
CHANGED
|
@@ -8,6 +8,8 @@ import { Walker } from "@effected/walker";
|
|
|
8
8
|
* @public
|
|
9
9
|
*/
|
|
10
10
|
const WORKSPACE_MARKERS = ["pnpm-workspace.yaml", "package.json"];
|
|
11
|
+
/** How the not-found message names the `package.json` marker: only one with a `workspaces` field counts. */
|
|
12
|
+
const PACKAGE_JSON_MARKER_PROSE = "package.json with a \"workspaces\" field";
|
|
11
13
|
/**
|
|
12
14
|
* Raised when no workspace root can be found by ascending from a directory.
|
|
13
15
|
*
|
|
@@ -36,7 +38,8 @@ var WorkspaceRootNotFoundError = class extends Schema.TaggedError()("WorkspaceRo
|
|
|
36
38
|
/** Renders the search path, probed markers and any ceiling into a one-line message. */
|
|
37
39
|
get message() {
|
|
38
40
|
const bound = this.stopAt === void 0 ? "" : ` up to ${this.stopAt}`;
|
|
39
|
-
|
|
41
|
+
const looked = this.markers.map((marker) => marker === "package.json" ? PACKAGE_JSON_MARKER_PROSE : marker);
|
|
42
|
+
return `No workspace root above ${this.searchPath}${bound} (looked for ${looked.join(", ")})`;
|
|
40
43
|
}
|
|
41
44
|
};
|
|
42
45
|
/**
|
package/WorkspaceSnapshots.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findLayerRoot } from "./internal/layerRoot.js";
|
|
1
2
|
import { manifestPatternsOf, pnpmPatternsOf } from "./internal/patterns.js";
|
|
2
3
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
3
4
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
@@ -210,7 +211,7 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
|
|
|
210
211
|
const atCaches = /* @__PURE__ */ new Map();
|
|
211
212
|
return {
|
|
212
213
|
at: Effect.fn("WorkspaceSnapshots.at")(function* (ref) {
|
|
213
|
-
const root = yield*
|
|
214
|
+
const root = yield* findLayerRoot(roots, options);
|
|
214
215
|
const key = `${root}\0${ref}`;
|
|
215
216
|
let memo = atCaches.get(key);
|
|
216
217
|
if (memo === void 0) {
|
package/Workspaces.js
CHANGED
|
@@ -48,7 +48,8 @@ const localExecLayer = (options) => Layer.effect(LocalExec, Effect.gen(function*
|
|
|
48
48
|
const detector = yield* PackageManagerDetector;
|
|
49
49
|
return { context: Effect.gen(function* () {
|
|
50
50
|
const cwd = options?.cwd ?? globalThis.process?.cwd?.() ?? "/";
|
|
51
|
-
const
|
|
51
|
+
const stopAt = options?.stopAt;
|
|
52
|
+
const root = yield* roots.find(cwd, stopAt === void 0 ? void 0 : { stopAt }).pipe(Effect.asSome, Effect.orElseSucceed(Option.none));
|
|
52
53
|
if (Option.isNone(root)) return Option.none();
|
|
53
54
|
const detected = yield* detector.detect(root.value).pipe(Effect.asSome, Effect.catchTag("PackageManagerDetectionError", () => Effect.succeed(Option.none())), Effect.mapError((cause) => new LocalExecError({
|
|
54
55
|
directory: root.value,
|
|
@@ -289,7 +290,10 @@ var Workspaces = class {
|
|
|
289
290
|
* verbatim.
|
|
290
291
|
*
|
|
291
292
|
* `directory` is the resolved **workspace root**, not the caller's cwd: a
|
|
292
|
-
* project-local launcher has to run where the workspace is.
|
|
293
|
+
* project-local launcher has to run where the workspace is. `stopAt` caps
|
|
294
|
+
* that ascent exactly as {@link WorkspacesOptions.stopAt} does, and a
|
|
295
|
+
* ceiling with no root at or below it is the same `Option.none()` as no
|
|
296
|
+
* root at all.
|
|
293
297
|
*
|
|
294
298
|
* A consumer with no monorepo never needs this layer, and therefore never
|
|
295
299
|
* installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
|
package/index.d.ts
CHANGED
|
@@ -541,6 +541,33 @@ interface WorkspaceDiscoveryOptions {
|
|
|
541
541
|
* honoured.
|
|
542
542
|
*/
|
|
543
543
|
readonly cwd?: string;
|
|
544
|
+
/**
|
|
545
|
+
* A ceiling for the layer-bound root ascent from `cwd`, passed straight
|
|
546
|
+
* through to the `stopAt` of {@link WorkspaceRoot}'s `find`.
|
|
547
|
+
*
|
|
548
|
+
* @remarks
|
|
549
|
+
* Inclusive: the ceiling itself is probed, so a `cwd` that is its own
|
|
550
|
+
* workspace root still resolves with `stopAt: cwd`. A relative ceiling is
|
|
551
|
+
* resolved against the process working directory, exactly as `cwd` is. When
|
|
552
|
+
* no root is found at or below the ceiling, the methods fail with
|
|
553
|
+
* {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt`, rather
|
|
554
|
+
* than adopting an enclosing directory's workspace — pass `stopAt: cwd` for
|
|
555
|
+
* a checkout nested inside someone else's monorepo.
|
|
556
|
+
*
|
|
557
|
+
* Applies ONLY to the layer-bound methods (`info`, `listPackages`,
|
|
558
|
+
* `importerMap`, `getPackage`, `resolveFile`, `resolveFiles`). The per-call
|
|
559
|
+
* `infoIn`, `listPackagesIn` and `refreshIn` resolve from an arbitrary
|
|
560
|
+
* directory the caller names, which a single layer-level ceiling cannot
|
|
561
|
+
* sensibly bound, so they ascend unbounded as before.
|
|
562
|
+
*
|
|
563
|
+
* `LockfileReader`, `WorkspaceCatalogs` and `WorkspaceSnapshots` each
|
|
564
|
+
* take the same option; give them the same
|
|
565
|
+
* value when wiring by hand, or let a `Workspaces.*` composite forward one
|
|
566
|
+
* `stopAt` to all of them, so no service adopts a root another refused.
|
|
567
|
+
*
|
|
568
|
+
* @defaultValue no ceiling — the ascent runs to the filesystem root.
|
|
569
|
+
*/
|
|
570
|
+
readonly stopAt?: string | undefined;
|
|
544
571
|
/** Descent cap for segment-crossing patterns. Defaults to 32. */
|
|
545
572
|
readonly maxDepth?: number;
|
|
546
573
|
}
|
|
@@ -1230,7 +1257,19 @@ export declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
|
|
|
1230
1257
|
* with `reason: "integrityUnavailable"` — nothing is fetched in either case
|
|
1231
1258
|
* — and a fetch that fails with `reason: "fetchFailed"`, keeping the
|
|
1232
1259
|
* not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
|
|
1233
|
-
* registry access, and is bounded at two minutes.
|
|
1260
|
+
* registry access, and is bounded at two minutes.
|
|
1261
|
+
*
|
|
1262
|
+
* The scratch workspace fetches through the same registries a
|
|
1263
|
+
* `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
|
|
1264
|
+
* registries, mirrors, auth) is copied into it as-is, with `${VAR}`
|
|
1265
|
+
* references left for pnpm to expand, never read or logged, and removed
|
|
1266
|
+
* with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
|
|
1267
|
+
* `registries` keys are carried into the scratch's. That is the CURRENT
|
|
1268
|
+
* checkout's registry config for both sides of a diff — a base ref's
|
|
1269
|
+
* `.npmrc` is not read through git — and relative paths inside the `.npmrc`
|
|
1270
|
+
* (a `cafile=./ca.pem`) resolve against the scratch, not the root.
|
|
1271
|
+
*
|
|
1272
|
+
* An empty `configDependencies`, or one whose dependencies all
|
|
1234
1273
|
* ship no pnpmfile, returns the seed without spawning anything; a `..` path
|
|
1235
1274
|
* segment in a dependency name fails typed **before** any spawn; a
|
|
1236
1275
|
* load failure in the child — a syntax error, a throwing top level, an
|
|
@@ -2032,6 +2071,21 @@ interface LockfileReaderOptions {
|
|
|
2032
2071
|
* @defaultValue `process.cwd()`, read lazily on first use.
|
|
2033
2072
|
*/
|
|
2034
2073
|
readonly cwd?: string;
|
|
2074
|
+
/**
|
|
2075
|
+
* A ceiling for the root ascent from `cwd`, passed straight through to the
|
|
2076
|
+
* `stopAt` of {@link WorkspaceRoot}'s `find`.
|
|
2077
|
+
*
|
|
2078
|
+
* @remarks
|
|
2079
|
+
* Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
|
|
2080
|
+
* root is found at or below the ceiling, `read` fails with
|
|
2081
|
+
* {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
|
|
2082
|
+
* than adopting an enclosing directory's workspace. Pass the same value as
|
|
2083
|
+
* {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
|
|
2084
|
+
* root; the `Workspaces.*` composites forward one `stopAt` to all of them.
|
|
2085
|
+
*
|
|
2086
|
+
* @defaultValue no ceiling — the ascent runs to the filesystem root.
|
|
2087
|
+
*/
|
|
2088
|
+
readonly stopAt?: string | undefined;
|
|
2035
2089
|
}
|
|
2036
2090
|
declare const LockfileReader_base: Context.ServiceClass<LockfileReader, "@effected/workspaces/LockfileReader", LockfileReaderShape>;
|
|
2037
2091
|
/**
|
|
@@ -3109,6 +3163,21 @@ interface WorkspaceCatalogsOptions {
|
|
|
3109
3163
|
* @defaultValue `process.cwd()`, read lazily on first use.
|
|
3110
3164
|
*/
|
|
3111
3165
|
readonly cwd?: string;
|
|
3166
|
+
/**
|
|
3167
|
+
* A ceiling for the root ascent from `cwd`, passed straight through to the
|
|
3168
|
+
* `stopAt` of {@link WorkspaceRoot}'s `find`.
|
|
3169
|
+
*
|
|
3170
|
+
* @remarks
|
|
3171
|
+
* Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
|
|
3172
|
+
* root is found at or below the ceiling, catalog assembly fails with
|
|
3173
|
+
* {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
|
|
3174
|
+
* than adopting an enclosing directory's workspace. Pass the same value as
|
|
3175
|
+
* {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
|
|
3176
|
+
* root; the `Workspaces.*` composites forward one `stopAt` to all of them.
|
|
3177
|
+
*
|
|
3178
|
+
* @defaultValue no ceiling — the ascent runs to the filesystem root.
|
|
3179
|
+
*/
|
|
3180
|
+
readonly stopAt?: string | undefined;
|
|
3112
3181
|
}
|
|
3113
3182
|
declare const WorkspaceCatalogs_base: Context.ServiceClass<WorkspaceCatalogs, "@effected/workspaces/WorkspaceCatalogs", WorkspaceCatalogsShape>;
|
|
3114
3183
|
/**
|
|
@@ -3662,6 +3731,21 @@ interface WorkspaceSnapshotsOptions {
|
|
|
3662
3731
|
* first call is honoured.
|
|
3663
3732
|
*/
|
|
3664
3733
|
readonly cwd?: string;
|
|
3734
|
+
/**
|
|
3735
|
+
* A ceiling for the root ascent from `cwd`, passed straight through to the
|
|
3736
|
+
* `stopAt` of {@link WorkspaceRoot}'s `find`.
|
|
3737
|
+
*
|
|
3738
|
+
* @remarks
|
|
3739
|
+
* Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
|
|
3740
|
+
* root is found at or below the ceiling, `at(ref)` fails with
|
|
3741
|
+
* {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
|
|
3742
|
+
* than adopting an enclosing directory's workspace. Pass the same value as
|
|
3743
|
+
* {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
|
|
3744
|
+
* root; the `Workspaces.*` composites forward one `stopAt` to all of them.
|
|
3745
|
+
*
|
|
3746
|
+
* @defaultValue no ceiling — the ascent runs to the filesystem root.
|
|
3747
|
+
*/
|
|
3748
|
+
readonly stopAt?: string | undefined;
|
|
3665
3749
|
/**
|
|
3666
3750
|
* Catalogs every snapshot this service produces carries as its
|
|
3667
3751
|
* `seededCatalogs` — consulted only where the snapshot's own catalogs cannot
|
|
@@ -3829,9 +3913,17 @@ export declare class WorkspaceSnapshots extends WorkspaceSnapshots_base {
|
|
|
3829
3913
|
/**
|
|
3830
3914
|
* Options shared by the composite layers.
|
|
3831
3915
|
*
|
|
3916
|
+
* @remarks
|
|
3917
|
+
* Derived from the option shapes of the services a composite builds —
|
|
3918
|
+
* {@link WorkspaceDiscoveryOptions}, {@link LockfileReaderOptions} and
|
|
3919
|
+
* {@link WorkspaceCatalogsOptions} — and handed to each of them unchanged, so a
|
|
3920
|
+
* root-resolving option one service grows cannot silently stop at the
|
|
3921
|
+
* composite. `cwd` and `stopAt` are re-declared here only to document them as
|
|
3922
|
+
* the one concern they are across the composite.
|
|
3923
|
+
*
|
|
3832
3924
|
* @public
|
|
3833
3925
|
*/
|
|
3834
|
-
interface WorkspacesOptions {
|
|
3926
|
+
interface WorkspacesOptions extends WorkspaceDiscoveryOptions, LockfileReaderOptions, WorkspaceCatalogsOptions {
|
|
3835
3927
|
/**
|
|
3836
3928
|
* The directory every root-consuming service resolves the workspace root
|
|
3837
3929
|
* from — one explicit concern, applied uniformly.
|
|
@@ -3839,8 +3931,23 @@ interface WorkspacesOptions {
|
|
|
3839
3931
|
* @defaultValue `process.cwd()`, read lazily on first use.
|
|
3840
3932
|
*/
|
|
3841
3933
|
readonly cwd?: string;
|
|
3842
|
-
/**
|
|
3843
|
-
|
|
3934
|
+
/**
|
|
3935
|
+
* A ceiling for the root ascent from `cwd`, forwarded to EVERY
|
|
3936
|
+
* root-resolving service the composite builds — {@link WorkspaceDiscovery},
|
|
3937
|
+
* {@link LockfileReader}, {@link WorkspaceCatalogs} and, on the git
|
|
3938
|
+
* composites, {@link WorkspaceSnapshots}.
|
|
3939
|
+
*
|
|
3940
|
+
* @remarks
|
|
3941
|
+
* Inclusive and resolved to an absolute path, as {@link WorkspaceRoot}'s
|
|
3942
|
+
* `find` does. Pass `stopAt: cwd` for a checkout nested inside someone
|
|
3943
|
+
* else's workspace: every service then fails with
|
|
3944
|
+
* {@link WorkspaceRootNotFoundError} consistently, instead of discovery
|
|
3945
|
+
* refusing the enclosing workspace while the lockfile and catalog reads
|
|
3946
|
+
* adopt it. A checkout that is itself a workspace root still resolves.
|
|
3947
|
+
*
|
|
3948
|
+
* @defaultValue no ceiling — the ascent runs to the filesystem root.
|
|
3949
|
+
*/
|
|
3950
|
+
readonly stopAt?: string | undefined;
|
|
3844
3951
|
}
|
|
3845
3952
|
/**
|
|
3846
3953
|
* Options for the composites that additionally provide {@link WorkspaceSnapshots}
|
|
@@ -4086,7 +4193,10 @@ export declare class Workspaces {
|
|
|
4086
4193
|
* verbatim.
|
|
4087
4194
|
*
|
|
4088
4195
|
* `directory` is the resolved **workspace root**, not the caller's cwd: a
|
|
4089
|
-
* project-local launcher has to run where the workspace is.
|
|
4196
|
+
* project-local launcher has to run where the workspace is. `stopAt` caps
|
|
4197
|
+
* that ascent exactly as {@link WorkspacesOptions.stopAt} does, and a
|
|
4198
|
+
* ceiling with no root at or below it is the same `Option.none()` as no
|
|
4199
|
+
* root at all.
|
|
4090
4200
|
*
|
|
4091
4201
|
* A consumer with no monorepo never needs this layer, and therefore never
|
|
4092
4202
|
* installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
|
|
@@ -4115,6 +4225,7 @@ export declare class Workspaces {
|
|
|
4115
4225
|
*/
|
|
4116
4226
|
static readonly localExecLayer: (options?: {
|
|
4117
4227
|
readonly cwd?: string;
|
|
4228
|
+
readonly stopAt?: string | undefined;
|
|
4118
4229
|
}) => Layer.Layer<LocalExec, never, PackageManagerDetector | WorkspaceRoot>;
|
|
4119
4230
|
/**
|
|
4120
4231
|
* Resolve every `catalog:` and `workspace:` specifier in one `Manifest`
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { carries, manifestVersion, messageOf, sideLabel } from "./configDependencyShared.js";
|
|
2
2
|
import { splitConfigDependencySpec } from "./configDependencySpecGrammar.js";
|
|
3
|
-
import { Duration, Effect } from "effect";
|
|
3
|
+
import { Duration, Effect, Predicate } from "effect";
|
|
4
4
|
import { IntegrityHash } from "@effected/npm";
|
|
5
|
+
import { Yaml } from "@effected/yaml";
|
|
5
6
|
import { Run } from "@effected/commands";
|
|
6
7
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
7
|
-
import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { copyFile, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
10
|
import { basename, dirname, join } from "node:path";
|
|
10
11
|
|
|
@@ -47,12 +48,36 @@ const expectedIntegrity = (request) => Effect.gen(function* () {
|
|
|
47
48
|
if (integrity === void 0) return yield* Effect.fail(fetchFailure("integrityUnavailable", `config dependency ${key} must be fetched, but ${sideLabel(side)} records no integrity for it (no inline integrity in pnpm-workspace.yaml and no pnpm-lock.yaml entry for that version), so a fetched copy could not be verified and nothing was fetched.`));
|
|
48
49
|
return integrity;
|
|
49
50
|
});
|
|
51
|
+
/** The {@link RegistrySettings} of a parsed `pnpm-workspace.yaml` document. */
|
|
52
|
+
const registrySettingsOf = (document) => {
|
|
53
|
+
if (!Predicate.isObject(document)) return { registries: {} };
|
|
54
|
+
const registries = {};
|
|
55
|
+
if (Predicate.isObject(document.registries)) {
|
|
56
|
+
for (const [scope, url] of Object.entries(document.registries)) if (typeof url === "string") registries[scope] = url;
|
|
57
|
+
}
|
|
58
|
+
return typeof document.registry === "string" ? {
|
|
59
|
+
registry: document.registry,
|
|
60
|
+
registries
|
|
61
|
+
} : { registries };
|
|
62
|
+
};
|
|
50
63
|
/**
|
|
51
|
-
* The scratch workspace's `pnpm-workspace.yaml
|
|
64
|
+
* The scratch workspace's `pnpm-workspace.yaml`: the one config dependency,
|
|
65
|
+
* plus the declaring workspace's registry settings. Every key and scalar is a
|
|
52
66
|
* JSON-quoted string, which YAML reads verbatim, so no declared text can
|
|
53
67
|
* change the document's structure.
|
|
54
68
|
*/
|
|
55
|
-
const scratchWorkspaceYaml = (name, version
|
|
69
|
+
const scratchWorkspaceYaml = (name, version, settings = { registries: {} }) => {
|
|
70
|
+
const lines = ["configDependencies:", ` ${JSON.stringify(name)}: ${JSON.stringify(version)}`];
|
|
71
|
+
if (settings.registry !== void 0) lines.push(`registry: ${JSON.stringify(settings.registry)}`);
|
|
72
|
+
const scopes = Object.entries(settings.registries);
|
|
73
|
+
if (scopes.length > 0) {
|
|
74
|
+
lines.push("registries:");
|
|
75
|
+
for (const [scope, url] of scopes) lines.push(` ${JSON.stringify(scope)}: ${JSON.stringify(url)}`);
|
|
76
|
+
}
|
|
77
|
+
return `${lines.join("\n")}\n`;
|
|
78
|
+
};
|
|
79
|
+
/** Whether `error` is a node filesystem "no such file" failure. */
|
|
80
|
+
const isNotFound = (error) => Predicate.isObject(error) && error.code === "ENOENT";
|
|
56
81
|
/**
|
|
57
82
|
* The scratch workspace's `pnpm-lock.yaml`: the env preamble pnpm 11 and 12
|
|
58
83
|
* write for a lone config dependency, pinned to `integrity`, followed by the
|
|
@@ -123,14 +148,22 @@ const makeFetchConfigDependency = (spawner) => (request) => Effect.scoped(Effect
|
|
|
123
148
|
catch: (cause) => failed(`${what} failed: ${messageOf(cause)}`, cause)
|
|
124
149
|
});
|
|
125
150
|
const integrity = yield* expectedIntegrity(request);
|
|
151
|
+
const rootYaml = yield* io("reading the workspace pnpm-workspace.yaml", () => readFile(join(request.root, "pnpm-workspace.yaml"), "utf8").catch((error) => {
|
|
152
|
+
if (isNotFound(error)) return void 0;
|
|
153
|
+
throw error;
|
|
154
|
+
}));
|
|
155
|
+
const settings = rootYaml === void 0 ? registrySettingsOf(void 0) : registrySettingsOf(yield* Yaml.parse(rootYaml).pipe(Effect.mapError((cause) => failed(`reading the workspace pnpm-workspace.yaml failed: ${messageOf(cause)}`, cause))));
|
|
126
156
|
const scratch = yield* Effect.acquireRelease(io("creating a scratch workspace", () => mkdtemp(join(tmpdir(), "effected-config-dependency-"))), (dir) => Effect.promise(() => rm(dir, {
|
|
127
157
|
recursive: true,
|
|
128
158
|
force: true
|
|
129
159
|
}).catch(() => void 0)));
|
|
130
160
|
const lockfile = scratchLockfile(name, version, integrity);
|
|
131
161
|
yield* io("writing the scratch workspace", async () => {
|
|
132
|
-
await writeFile(join(scratch, "pnpm-workspace.yaml"), scratchWorkspaceYaml(name, version));
|
|
162
|
+
await writeFile(join(scratch, "pnpm-workspace.yaml"), scratchWorkspaceYaml(name, version, settings));
|
|
133
163
|
await writeFile(join(scratch, "pnpm-lock.yaml"), lockfile);
|
|
164
|
+
await copyFile(join(request.root, ".npmrc"), join(scratch, ".npmrc")).catch((error) => {
|
|
165
|
+
if (!isNotFound(error)) throw error;
|
|
166
|
+
});
|
|
134
167
|
});
|
|
135
168
|
const command = ChildProcess.make("pnpm", fetchArgs(scratch, request.stores), { cwd: scratch });
|
|
136
169
|
yield* Run.text(command, { timeout: FETCH_TIMEOUT }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.mapError((cause) => failed(`fetching ${key} with \`pnpm ${fetchArgs("<scratch>", request.stores).join(" ")}\` failed: ${messageOf(cause)}`, cause)));
|
|
@@ -143,4 +176,4 @@ const makeFetchConfigDependency = (spawner) => (request) => Effect.scoped(Effect
|
|
|
143
176
|
}));
|
|
144
177
|
|
|
145
178
|
//#endregion
|
|
146
|
-
export { expectedIntegrity, fetchArgs, makeFetchConfigDependency, storeDirArgument };
|
|
179
|
+
export { expectedIntegrity, fetchArgs, makeFetchConfigDependency, registrySettingsOf, scratchWorkspaceYaml, storeDirArgument };
|
|
@@ -266,6 +266,7 @@ const resolveDirectory = (root, entry, stores, locks, options) => Effect.gen(fun
|
|
|
266
266
|
if (options.fetch === void 0) return yield* Effect.fail(notInstalled(Option.none()));
|
|
267
267
|
return {
|
|
268
268
|
dir: yield* options.fetch({
|
|
269
|
+
root,
|
|
269
270
|
name,
|
|
270
271
|
version: declared,
|
|
271
272
|
spec: entry.spec,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/layerRoot.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the workspace root a layer's options name. `Effect.suspend` so the
|
|
6
|
+
* ambient `process.cwd()` is read at first use, not at layer construction.
|
|
7
|
+
*/
|
|
8
|
+
const findLayerRoot = (roots, options) => Effect.suspend(() => {
|
|
9
|
+
const stopAt = options?.stopAt;
|
|
10
|
+
return roots.find(options?.cwd ?? process.cwd(), stopAt === void 0 ? void 0 : { stopAt });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { findLayerRoot };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.28.0
|
|
3
|
+
"version": "0.28.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": [
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"@effected/git": "^0.17.0",
|
|
56
56
|
"@effected/glob": "^0.8.0",
|
|
57
57
|
"@effected/jsonc": "^0.13.0",
|
|
58
|
-
"@effected/lockfiles": "^0.12.0
|
|
59
|
-
"@effected/npm": "^0.17.0
|
|
60
|
-
"@effected/package-json": "^0.18.0
|
|
58
|
+
"@effected/lockfiles": "^0.12.0",
|
|
59
|
+
"@effected/npm": "^0.17.0",
|
|
60
|
+
"@effected/package-json": "^0.18.0",
|
|
61
61
|
"@effected/semver": "^0.9.0",
|
|
62
62
|
"@effected/walker": "^0.12.0",
|
|
63
63
|
"@effected/yaml": "^0.17.0",
|
package/testing.d.ts
CHANGED
|
@@ -633,6 +633,33 @@ interface WorkspaceDiscoveryOptions {
|
|
|
633
633
|
* honoured.
|
|
634
634
|
*/
|
|
635
635
|
readonly cwd?: string;
|
|
636
|
+
/**
|
|
637
|
+
* A ceiling for the layer-bound root ascent from `cwd`, passed straight
|
|
638
|
+
* through to the `stopAt` of {@link WorkspaceRoot}'s `find`.
|
|
639
|
+
*
|
|
640
|
+
* @remarks
|
|
641
|
+
* Inclusive: the ceiling itself is probed, so a `cwd` that is its own
|
|
642
|
+
* workspace root still resolves with `stopAt: cwd`. A relative ceiling is
|
|
643
|
+
* resolved against the process working directory, exactly as `cwd` is. When
|
|
644
|
+
* no root is found at or below the ceiling, the methods fail with
|
|
645
|
+
* {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt`, rather
|
|
646
|
+
* than adopting an enclosing directory's workspace — pass `stopAt: cwd` for
|
|
647
|
+
* a checkout nested inside someone else's monorepo.
|
|
648
|
+
*
|
|
649
|
+
* Applies ONLY to the layer-bound methods (`info`, `listPackages`,
|
|
650
|
+
* `importerMap`, `getPackage`, `resolveFile`, `resolveFiles`). The per-call
|
|
651
|
+
* `infoIn`, `listPackagesIn` and `refreshIn` resolve from an arbitrary
|
|
652
|
+
* directory the caller names, which a single layer-level ceiling cannot
|
|
653
|
+
* sensibly bound, so they ascend unbounded as before.
|
|
654
|
+
*
|
|
655
|
+
* `LockfileReader`, `WorkspaceCatalogs` and `WorkspaceSnapshots` each
|
|
656
|
+
* take the same option; give them the same
|
|
657
|
+
* value when wiring by hand, or let a `Workspaces.*` composite forward one
|
|
658
|
+
* `stopAt` to all of them, so no service adopts a root another refused.
|
|
659
|
+
*
|
|
660
|
+
* @defaultValue no ceiling — the ascent runs to the filesystem root.
|
|
661
|
+
*/
|
|
662
|
+
readonly stopAt?: string | undefined;
|
|
636
663
|
/** Descent cap for segment-crossing patterns. Defaults to 32. */
|
|
637
664
|
readonly maxDepth?: number;
|
|
638
665
|
}
|