@effected/workspaces 0.1.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/CatalogAssemblyError.js +43 -0
- package/ChangeDetector.js +154 -0
- package/ConfigDependencyHooks.js +153 -0
- package/DependencyGraph.js +241 -0
- package/LICENSE +21 -0
- package/LockfileReader.js +118 -0
- package/PackageManagerName.js +248 -0
- package/Publishability.js +73 -0
- package/README.md +165 -0
- package/WorkspaceCatalogs.js +392 -0
- package/WorkspaceDiscovery.js +338 -0
- package/WorkspacePackage.js +247 -0
- package/WorkspaceRoot.js +90 -0
- package/WorkspaceSnapshots.js +0 -0
- package/WorkspaceStateSnapshot.js +165 -0
- package/Workspaces.js +113 -0
- package/WorkspacesSync.js +205 -0
- package/index.d.ts +1559 -0
- package/index.js +17 -0
- package/internal/catalogs.js +70 -0
- package/internal/enumerate.js +75 -0
- package/internal/limits.js +20 -0
- package/internal/patterns.js +62 -0
- package/internal/traverse.js +94 -0
- package/package.json +61 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { CatalogAssemblyError } from "./CatalogAssemblyError.js";
|
|
2
|
+
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
3
|
+
import { inlineCatalogs, merge, normalize, rangeOf } from "./internal/catalogs.js";
|
|
4
|
+
import { ConfigDependencyHooks } from "./ConfigDependencyHooks.js";
|
|
5
|
+
import { LockfileReader } from "./LockfileReader.js";
|
|
6
|
+
import { Context, Duration, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError, Schema } from "effect";
|
|
7
|
+
import { CatalogResolver, DependencyResolutionError } from "@effected/npm";
|
|
8
|
+
import { Yaml } from "@effected/yaml";
|
|
9
|
+
|
|
10
|
+
//#region src/WorkspaceCatalogs.ts
|
|
11
|
+
/**
|
|
12
|
+
* An immutable, fully-normalized catalog collection — the one catalog
|
|
13
|
+
* resolution semantic in the package.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* `entries` is catalog name → dependency → range, with pnpm's unnamed
|
|
17
|
+
* top-level `catalog:` block normalized under the key `"default"`. Lockfile
|
|
18
|
+
* catalog entries, which pnpm records as `{ specifier, version }`, are
|
|
19
|
+
* normalized to their `specifier` — the declared range, which is what a catalog
|
|
20
|
+
* resolves to.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
var CatalogSet = class CatalogSet extends Schema.Class("CatalogSet")({
|
|
25
|
+
/** Catalog name → dependency name → version range. */
|
|
26
|
+
entries: Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.String)) }) {
|
|
27
|
+
/** The empty set — a workspace with no catalogs. */
|
|
28
|
+
static empty() {
|
|
29
|
+
return CatalogSet.make({ entries: {} });
|
|
30
|
+
}
|
|
31
|
+
/** Wrap a pnpm `Catalogs` map, dropping unusable entries. */
|
|
32
|
+
static fromCatalogs(catalogs) {
|
|
33
|
+
return CatalogSet.make({ entries: normalize(catalogs) });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The `catalog:` and `catalogs:` blocks of a `pnpm-workspace.yaml` document.
|
|
37
|
+
*
|
|
38
|
+
* @param text - The raw YAML text.
|
|
39
|
+
*/
|
|
40
|
+
static fromWorkspaceYaml = Effect.fn("CatalogSet.fromWorkspaceYaml")(function* (text) {
|
|
41
|
+
const document = yield* Yaml.parse(text).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
42
|
+
source: "manifest",
|
|
43
|
+
path: "pnpm-workspace.yaml",
|
|
44
|
+
cause
|
|
45
|
+
})));
|
|
46
|
+
return CatalogSet.fromCatalogs(inlineCatalogs(catalogBlocksOf(document)));
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* The `catalogs:` section of a pnpm lockfile, whose entries are either a bare
|
|
50
|
+
* range or a `{ specifier, version }` pair.
|
|
51
|
+
*/
|
|
52
|
+
static fromLockfileCatalogs(raw) {
|
|
53
|
+
return CatalogSet.make({ entries: normalize(raw) });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The catalog set a parsed lockfile records, PM-aware.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Both pnpm and bun record catalogs in the lockfile, in different shapes:
|
|
60
|
+
* pnpm under `extension.catalogs`, bun under `extension.catalog` /
|
|
61
|
+
* `extension.catalogs`. Assembly reads whichever the parsed lockfile carries
|
|
62
|
+
* rather than assuming the pnpm extension. A lockfile with no extension, or a
|
|
63
|
+
* pnpm one with no catalogs, yields the empty set.
|
|
64
|
+
*
|
|
65
|
+
* @param lockfile - A parsed lockfile from `@effected/lockfiles`.
|
|
66
|
+
*/
|
|
67
|
+
static fromLockfile(lockfile) {
|
|
68
|
+
const ext = lockfile.extension;
|
|
69
|
+
if (ext === void 0) return CatalogSet.empty();
|
|
70
|
+
if (ext._tag === "pnpm") return ext.catalogs !== void 0 ? CatalogSet.fromLockfileCatalogs(ext.catalogs) : CatalogSet.empty();
|
|
71
|
+
return CatalogSet.fromBunBlocks({
|
|
72
|
+
catalog: ext.catalog,
|
|
73
|
+
catalogs: ext.catalogs
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A catalog set from bun's `{ catalog, catalogs }` blocks — used for the
|
|
78
|
+
* `bun.lock` extension and for the root `package.json` `workspaces` block. The
|
|
79
|
+
* unnamed default catalog normalizes under `"default"`. Tolerant: unusable
|
|
80
|
+
* values are dropped, never fatal.
|
|
81
|
+
*
|
|
82
|
+
* @param blocks - The `catalog` (default) and `catalogs` (named) blocks.
|
|
83
|
+
*/
|
|
84
|
+
static fromBunBlocks(blocks) {
|
|
85
|
+
const raw = {};
|
|
86
|
+
if (isObject(blocks.catalogs)) Object.assign(raw, blocks.catalogs);
|
|
87
|
+
if (isObject(blocks.catalog)) raw.default = {
|
|
88
|
+
...isObject(raw.default) ? raw.default : {},
|
|
89
|
+
...blocks.catalog
|
|
90
|
+
};
|
|
91
|
+
return CatalogSet.fromCatalogs(raw);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The `catalog` / `catalogs` blocks of a root `package.json` `workspaces` field
|
|
95
|
+
* — bun's package.json analogue of pnpm's `pnpm-workspace.yaml` blocks.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* **Hard-fail by design**, preserving the semantics catalog output is
|
|
99
|
+
* load-bearing for: a present-but-malformed `workspaces` shape (a number, a
|
|
100
|
+
* string, an object with a malformed `packages` / `catalog` / `catalogs`), or
|
|
101
|
+
* the default catalog declared twice — once as `workspaces.catalog` and again
|
|
102
|
+
* as `workspaces.catalogs.default` — fails with {@link CatalogAssemblyError}
|
|
103
|
+
* naming what was wrong. An absent `workspaces` field, one explicitly `null`,
|
|
104
|
+
* or the plain array form (npm/yarn patterns, which carry no catalogs) yields
|
|
105
|
+
* the empty set. Presence is checked structurally, so an explicitly-declared
|
|
106
|
+
* empty `catalog: {}` still counts as a declaration.
|
|
107
|
+
*
|
|
108
|
+
* @param text - The raw root `package.json` text.
|
|
109
|
+
*/
|
|
110
|
+
static fromManifestWorkspaces = Effect.fn("CatalogSet.fromManifestWorkspaces")(function* (text) {
|
|
111
|
+
const manifest = yield* Effect.try({
|
|
112
|
+
try: () => JSON.parse(text),
|
|
113
|
+
catch: (cause) => new CatalogAssemblyError({
|
|
114
|
+
source: "manifest",
|
|
115
|
+
path: "package.json",
|
|
116
|
+
cause
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
if (!isObject(manifest)) return yield* Effect.fail(new CatalogAssemblyError({
|
|
120
|
+
source: "manifest",
|
|
121
|
+
path: "package.json",
|
|
122
|
+
cause: /* @__PURE__ */ new Error("package.json is not a JSON object")
|
|
123
|
+
}));
|
|
124
|
+
const blocks = yield* manifestCatalogBlocks(manifest.workspaces);
|
|
125
|
+
return CatalogSet.fromBunBlocks(blocks);
|
|
126
|
+
});
|
|
127
|
+
/** Merge sets. Later sets win per dependency within a catalog. */
|
|
128
|
+
static merge(...sets) {
|
|
129
|
+
return CatalogSet.fromCatalogs(merge(...sets.map((set) => set.entries)));
|
|
130
|
+
}
|
|
131
|
+
/** Whether any catalog declares anything. */
|
|
132
|
+
get isEmpty() {
|
|
133
|
+
return Object.keys(this.entries).length === 0;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The range a `catalog:` specifier resolves to.
|
|
137
|
+
*
|
|
138
|
+
* @remarks
|
|
139
|
+
* Total: an unmatched dependency, an unknown catalog name, or a non-catalog
|
|
140
|
+
* specifier all yield `Option.none()`. A *malformed* catalog — one pnpm
|
|
141
|
+
* itself rejects — also yields `Option.none()` here; the fallible surface is
|
|
142
|
+
* {@link WorkspaceCatalogs}, which fails typed on assembly instead.
|
|
143
|
+
*
|
|
144
|
+
* @param dependency - The package name being resolved.
|
|
145
|
+
* @param specifier - The declared specifier, e.g. `catalog:` or `catalog:build`.
|
|
146
|
+
*/
|
|
147
|
+
resolveSpecifier(dependency, specifier) {
|
|
148
|
+
const resolved = rangeOf(this.entries, dependency, specifier);
|
|
149
|
+
return typeof resolved === "string" ? Option.some(resolved) : Option.none();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The range `dependency` carries in a named catalog, or in the default
|
|
153
|
+
* catalog when `catalog` is `Option.none()`.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* The shape `@effected/npm`'s `CatalogResolver` contract asks for.
|
|
157
|
+
*/
|
|
158
|
+
rangeOf(dependency, catalog) {
|
|
159
|
+
const name = Option.getOrElse(catalog, () => "default");
|
|
160
|
+
return Option.fromUndefinedOr(this.entries[name]?.[dependency]);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
/** Whether `value` is a non-null, non-array object. */
|
|
164
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
165
|
+
/** Whether every value in an object is a string — a usable `dependency → range` catalog. */
|
|
166
|
+
const isStringRecord = (value) => isObject(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
167
|
+
/** The `catalog` / `catalogs` blocks of a parsed pnpm-workspace document. */
|
|
168
|
+
const catalogBlocksOf = (document) => {
|
|
169
|
+
if (document === null || typeof document !== "object") return {};
|
|
170
|
+
const raw = document;
|
|
171
|
+
return {
|
|
172
|
+
catalog: raw.catalog,
|
|
173
|
+
catalogs: raw.catalogs
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
/** The `configDependencies` map (name → version+integrity) of a parsed pnpm-workspace document. */
|
|
177
|
+
const configDependenciesOf = (document) => {
|
|
178
|
+
if (!isObject(document) || !isObject(document.configDependencies)) return {};
|
|
179
|
+
const out = {};
|
|
180
|
+
for (const [name, spec] of Object.entries(document.configDependencies)) if (typeof spec === "string") out[name] = spec;
|
|
181
|
+
return out;
|
|
182
|
+
};
|
|
183
|
+
/** A hard-fail catalog-assembly failure naming the malformed part of a `workspaces` field. */
|
|
184
|
+
const malformed = (source, path, detail) => new CatalogAssemblyError({
|
|
185
|
+
source,
|
|
186
|
+
path,
|
|
187
|
+
cause: new Error(detail)
|
|
188
|
+
});
|
|
189
|
+
/**
|
|
190
|
+
* Validate a `catalog` (default) / `catalogs` (named) block pair, hard-failing on
|
|
191
|
+
* a malformed shape or the default catalog declared twice.
|
|
192
|
+
*
|
|
193
|
+
* @remarks
|
|
194
|
+
* Shared by the bun `package.json` `workspaces` reader
|
|
195
|
+
* ({@link CatalogSet.fromManifestWorkspaces}) and the pnpm `pnpm-workspace.yaml`
|
|
196
|
+
* reader, so both fail typed on exactly the same conditions rather than one
|
|
197
|
+
* hard-failing and the other silently normalizing to `{}` — the "every dependency
|
|
198
|
+
* looks newly added" bug. `labels` names the two blocks in the diagnostics so the
|
|
199
|
+
* error reads in the caller's vocabulary (`workspaces.catalog` vs. `catalog`).
|
|
200
|
+
*/
|
|
201
|
+
const validatedCatalogBlocks = (catalog, catalogs, labels) => {
|
|
202
|
+
let validCatalog;
|
|
203
|
+
if (catalog !== void 0 && catalog !== null) {
|
|
204
|
+
if (!isStringRecord(catalog)) return Effect.fail(malformed("catalog", labels.catalog, `"${labels.catalog}" must map dependency names to version strings`));
|
|
205
|
+
validCatalog = catalog;
|
|
206
|
+
}
|
|
207
|
+
let validCatalogs;
|
|
208
|
+
if (catalogs !== void 0 && catalogs !== null) {
|
|
209
|
+
if (!isObject(catalogs)) return Effect.fail(malformed("catalog", labels.catalogs, `"${labels.catalogs}" must map catalog names to catalogs`));
|
|
210
|
+
for (const [name, entries] of Object.entries(catalogs)) if (!isStringRecord(entries)) return Effect.fail(malformed("catalog", `${labels.catalogs}.${name}`, `"${labels.catalogs}.${name}" must map dependency names to version strings`));
|
|
211
|
+
validCatalogs = catalogs;
|
|
212
|
+
}
|
|
213
|
+
if (validCatalog !== void 0 && validCatalogs !== void 0 && "default" in validCatalogs) return Effect.fail(malformed("catalog", "default", `The default catalog is declared twice: as "${labels.catalog}" and as "${labels.catalogs}.default"`));
|
|
214
|
+
return Effect.succeed({
|
|
215
|
+
...validCatalog !== void 0 ? { catalog: validCatalog } : {},
|
|
216
|
+
...validCatalogs !== void 0 ? { catalogs: validCatalogs } : {}
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* The `catalog` / `catalogs` blocks of a root `package.json` `workspaces` field,
|
|
221
|
+
* hard-failing on a malformed shape or the default catalog declared twice.
|
|
222
|
+
*/
|
|
223
|
+
const manifestCatalogBlocks = (workspaces) => {
|
|
224
|
+
if (workspaces === void 0 || workspaces === null || Array.isArray(workspaces)) return Effect.succeed({});
|
|
225
|
+
if (!isObject(workspaces)) return Effect.fail(malformed("manifest", "package.json", `"workspaces" must be an array or object, got ${typeof workspaces}`));
|
|
226
|
+
if (workspaces.packages !== void 0 && workspaces.packages !== null) {
|
|
227
|
+
const packages = workspaces.packages;
|
|
228
|
+
if (!Array.isArray(packages) || !packages.every((entry) => typeof entry === "string")) return Effect.fail(malformed("manifest", "package.json", `"workspaces.packages" must be an array of strings`));
|
|
229
|
+
}
|
|
230
|
+
return validatedCatalogBlocks(workspaces.catalog, workspaces.catalogs, {
|
|
231
|
+
catalog: "workspaces.catalog",
|
|
232
|
+
catalogs: "workspaces.catalogs"
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Validate the inline `catalog` / `catalogs` blocks of a parsed
|
|
237
|
+
* `pnpm-workspace.yaml` document, hard-failing on the same malformed-shape and
|
|
238
|
+
* duplicate-default conditions as the bun `package.json` path.
|
|
239
|
+
*
|
|
240
|
+
* @remarks
|
|
241
|
+
* The live pnpm reader in {@link WorkspaceCatalogs.make} runs this BEFORE
|
|
242
|
+
* normalizing with `inlineCatalogs`, so an invalid inline catalog fails typed
|
|
243
|
+
* rather than normalizing to `{}` and reading as ABSENT. A non-object document,
|
|
244
|
+
* or one with no catalog blocks, validates to `{}` — absent/empty still yields
|
|
245
|
+
* empty. The return is discarded; normalization stays on the shared
|
|
246
|
+
* `inlineCatalogs` path.
|
|
247
|
+
*/
|
|
248
|
+
const validatePnpmWorkspaceCatalogs = (document) => {
|
|
249
|
+
if (!isObject(document)) return Effect.void;
|
|
250
|
+
return Effect.asVoid(validatedCatalogBlocks(document.catalog, document.catalogs, {
|
|
251
|
+
catalog: "catalog",
|
|
252
|
+
catalogs: "catalogs"
|
|
253
|
+
}));
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* Assembles a workspace's catalogs, package-manager-aware.
|
|
257
|
+
*
|
|
258
|
+
* @remarks
|
|
259
|
+
* The inline source is picked by **file presence**, the same rule
|
|
260
|
+
* `internal/patterns.ts` uses for globs: a `pnpm-workspace.yaml` selects the pnpm
|
|
261
|
+
* path (its `catalog:` / `catalogs:` blocks); its absence selects the root
|
|
262
|
+
* `package.json` `workspaces.catalog` / `workspaces.catalogs` (bun's analogue).
|
|
263
|
+
* The lockfile source is PM-aware too — whichever extension the parsed lockfile
|
|
264
|
+
* carries (pnpm or bun) contributes its recorded catalogs.
|
|
265
|
+
*
|
|
266
|
+
* Precedence, lowest first: catalogs recorded in the lockfile, then the inline
|
|
267
|
+
* declaration, then — only under {@link WorkspaceCatalogs.layerWithConfigDependencies}
|
|
268
|
+
* — the config-dependency `pnpmfile.cjs` hooks, each layer winning per dependency
|
|
269
|
+
* within a catalog. An unreadable or absent lockfile degrades to no lockfile
|
|
270
|
+
* catalogs rather than failing (the inline declaration is the source of truth, the
|
|
271
|
+
* lockfile a record of what was installed); a malformed inline source **fails
|
|
272
|
+
* typed** with {@link CatalogAssemblyError}, because a silently-empty catalog read
|
|
273
|
+
* is the "every dependency looks newly added" bug.
|
|
274
|
+
*
|
|
275
|
+
* Assembly is deferred to the first call and memoized success-only, matching
|
|
276
|
+
* {@link WorkspaceDiscovery}.
|
|
277
|
+
*
|
|
278
|
+
* @public
|
|
279
|
+
*/
|
|
280
|
+
var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effected/workspaces/WorkspaceCatalogs") {
|
|
281
|
+
/** Builds the service. */
|
|
282
|
+
static make = (options) => Effect.gen(function* () {
|
|
283
|
+
const roots = yield* WorkspaceRoot;
|
|
284
|
+
const lockfiles = yield* LockfileReader;
|
|
285
|
+
const hooks = yield* ConfigDependencyHooks;
|
|
286
|
+
const fs = yield* FileSystem.FileSystem;
|
|
287
|
+
const path = yield* Path.Path;
|
|
288
|
+
const probeExists = (target) => fs.exists(target).pipe(Effect.catchTag("PlatformError", (error) => {
|
|
289
|
+
const reason = error.reason;
|
|
290
|
+
return reason instanceof PlatformError.SystemError && reason._tag === "NotFound" ? Effect.succeed(false) : Effect.fail(new CatalogAssemblyError({
|
|
291
|
+
source: "manifest",
|
|
292
|
+
path: target,
|
|
293
|
+
cause: error
|
|
294
|
+
}));
|
|
295
|
+
}));
|
|
296
|
+
const assemble = Effect.gen(function* () {
|
|
297
|
+
const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
|
|
298
|
+
const fromLockfile = yield* lockfiles.read().pipe(Effect.map(CatalogSet.fromLockfile), Effect.catch((_failure) => Effect.succeed(CatalogSet.empty())));
|
|
299
|
+
const workspaceYaml = path.join(root, "pnpm-workspace.yaml");
|
|
300
|
+
const hasPnpmWorkspace = yield* probeExists(workspaceYaml);
|
|
301
|
+
let inline;
|
|
302
|
+
let injected;
|
|
303
|
+
if (hasPnpmWorkspace) {
|
|
304
|
+
const text = yield* fs.readFileString(workspaceYaml).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
305
|
+
source: "manifest",
|
|
306
|
+
path: workspaceYaml,
|
|
307
|
+
cause
|
|
308
|
+
})));
|
|
309
|
+
const document = yield* Yaml.parse(text).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
310
|
+
source: "manifest",
|
|
311
|
+
path: "pnpm-workspace.yaml",
|
|
312
|
+
cause
|
|
313
|
+
})));
|
|
314
|
+
yield* validatePnpmWorkspaceCatalogs(document);
|
|
315
|
+
inline = CatalogSet.fromCatalogs(inlineCatalogs(catalogBlocksOf(document)));
|
|
316
|
+
const injectedEntries = yield* hooks.inject(root, configDependenciesOf(document), inline.entries);
|
|
317
|
+
injected = CatalogSet.fromCatalogs(injectedEntries);
|
|
318
|
+
} else {
|
|
319
|
+
const manifestPath = path.join(root, "package.json");
|
|
320
|
+
if (!(yield* probeExists(manifestPath))) return fromLockfile;
|
|
321
|
+
const text = yield* fs.readFileString(manifestPath).pipe(Effect.mapError((cause) => new CatalogAssemblyError({
|
|
322
|
+
source: "manifest",
|
|
323
|
+
path: manifestPath,
|
|
324
|
+
cause
|
|
325
|
+
})));
|
|
326
|
+
inline = yield* CatalogSet.fromManifestWorkspaces(text);
|
|
327
|
+
injected = CatalogSet.empty();
|
|
328
|
+
}
|
|
329
|
+
const assembled = CatalogSet.merge(fromLockfile, inline, injected);
|
|
330
|
+
yield* Effect.logDebug("Catalogs assembled").pipe(Effect.annotateLogs({
|
|
331
|
+
"workspace.root": root,
|
|
332
|
+
"workspace.catalogs": Object.keys(assembled.entries).join(",")
|
|
333
|
+
}));
|
|
334
|
+
return assembled;
|
|
335
|
+
});
|
|
336
|
+
const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(assemble, Duration.infinity);
|
|
337
|
+
const memo = Effect.onExit(resolveOnce, (exit) => Exit.isSuccess(exit) ? Effect.void : invalidate);
|
|
338
|
+
return {
|
|
339
|
+
set: Effect.fn("WorkspaceCatalogs.set")(function* () {
|
|
340
|
+
return yield* memo;
|
|
341
|
+
}),
|
|
342
|
+
resolveSpecifier: Effect.fn("WorkspaceCatalogs.resolveSpecifier")(function* (dependency, specifier) {
|
|
343
|
+
return (yield* memo).resolveSpecifier(dependency, specifier);
|
|
344
|
+
})
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* The live layer — the default, which **never executes config-dependency
|
|
349
|
+
* code**: it wires {@link ConfigDependencyHooks.layerNoop}, whose hooks return
|
|
350
|
+
* the inline-catalog seed untouched.
|
|
351
|
+
*
|
|
352
|
+
* @remarks
|
|
353
|
+
* Parameterized, so it mints a fresh reference per call — bind it to a
|
|
354
|
+
* `const` and reuse it, or layer memoization does not apply.
|
|
355
|
+
*/
|
|
356
|
+
static layer = (options) => Layer.effect(WorkspaceCatalogs, WorkspaceCatalogs.make(options)).pipe(Layer.provide(ConfigDependencyHooks.layerNoop));
|
|
357
|
+
/**
|
|
358
|
+
* The opt-in live layer that **does** replay config-dependency `pnpmfile.cjs`
|
|
359
|
+
* hooks: it wires {@link ConfigDependencyHooks.layerLive}, which dynamically
|
|
360
|
+
* imports and runs each config dependency's `updateConfig` in process.
|
|
361
|
+
*
|
|
362
|
+
* @remarks
|
|
363
|
+
* Same output and requirement set as {@link WorkspaceCatalogs.layer} — the only
|
|
364
|
+
* difference is that config-dependency code is executed. Use it deliberately;
|
|
365
|
+
* the default catalog path stays free of any config-dependency execution.
|
|
366
|
+
* Parameterized, so bind it to a `const` and reuse it.
|
|
367
|
+
*/
|
|
368
|
+
static layerWithConfigDependencies = (options) => Layer.effect(WorkspaceCatalogs, WorkspaceCatalogs.make(options)).pipe(Layer.provide(ConfigDependencyHooks.layerLive));
|
|
369
|
+
/**
|
|
370
|
+
* The real implementation of `@effected/npm`'s `CatalogResolver` contract —
|
|
371
|
+
* the one `@effected/package-json` declares but cannot fill.
|
|
372
|
+
*
|
|
373
|
+
* @remarks
|
|
374
|
+
* `rangeOf` returns `Option.none()` for a dependency no catalog declares, per
|
|
375
|
+
* the contract's convention; `DependencyResolutionError` is reserved for a
|
|
376
|
+
* failure of the resolution *mechanism* — an unfindable workspace root, an
|
|
377
|
+
* unreadable or malformed `pnpm-workspace.yaml`.
|
|
378
|
+
*/
|
|
379
|
+
static catalogResolver = Layer.effect(CatalogResolver, Effect.gen(function* () {
|
|
380
|
+
const catalogs = yield* WorkspaceCatalogs;
|
|
381
|
+
return { rangeOf: (packageName, catalog) => catalogs.set().pipe(Effect.map((set) => set.rangeOf(packageName, catalog)), Effect.mapError((cause) => new DependencyResolutionError({
|
|
382
|
+
specifier: Option.match(catalog, {
|
|
383
|
+
onNone: () => "catalog:",
|
|
384
|
+
onSome: (name) => `catalog:${name}`
|
|
385
|
+
}),
|
|
386
|
+
cause
|
|
387
|
+
}))) };
|
|
388
|
+
}));
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
//#endregion
|
|
392
|
+
export { CatalogSet, WorkspaceCatalogs };
|