@effected/workspaces 0.1.0 → 0.2.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/ChangeDetector.js +1 -1
- package/ConfigDependencyHooks.js +2 -2
- package/README.md +40 -7
- package/WorkspaceCatalogs.js +11 -9
- package/WorkspaceDiscovery.js +1 -0
- package/WorkspacePackage.js +16 -1
- package/WorkspaceSnapshots.js +0 -0
- package/WorkspaceStateSnapshot.js +6 -0
- package/Workspaces.js +74 -1
- package/WorkspacesSync.js +75 -44
- package/index.d.ts +171 -58
- package/index.js +1 -2
- package/package.json +4 -4
- package/CatalogAssemblyError.js +0 -43
package/ChangeDetector.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
2
2
|
import { DependencyGraph } from "./DependencyGraph.js";
|
|
3
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
3
|
import { Git } from "@effected/git";
|
|
4
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
5
5
|
|
|
6
6
|
//#region src/ChangeDetector.ts
|
|
7
7
|
/**
|
package/ConfigDependencyHooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CatalogAssemblyError } from "./CatalogAssemblyError.js";
|
|
2
1
|
import { normalize } from "./internal/catalogs.js";
|
|
3
2
|
import { Context, Effect, Layer, Predicate, Result } from "effect";
|
|
3
|
+
import { CatalogAssemblyError } from "@effected/npm";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
@@ -95,7 +95,7 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
|
|
|
95
95
|
* (in process, no subprocess) and replays its `updateConfig` hook over the
|
|
96
96
|
* seed, in declaration order. A dependency without a `pnpmfile.cjs` contributes
|
|
97
97
|
* nothing; a dependency whose file fails to load or replay fails typed with a
|
|
98
|
-
* `hooks`-source
|
|
98
|
+
* `hooks`-source `CatalogAssemblyError`, never a silent skip.
|
|
99
99
|
*
|
|
100
100
|
* @remarks
|
|
101
101
|
* Node-coupled by design — the `node:fs` / `node:path` / `node:url` imports are
|
package/README.md
CHANGED
|
@@ -110,20 +110,52 @@ const Resolvers = Workspaces.resolvers.pipe(Layer.provide(WorkspacesLayer));
|
|
|
110
110
|
// Layer<CatalogResolver | WorkspaceResolver, never, FileSystem | Path>
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
`Workspaces.resolverLayer(options?)` is that wiring in one call: the two contracts over a full workspace stack, needing only `FileSystem` and `Path` from you. A fresh layer per call is the point — root discovery re-runs each time, including the `process.cwd()` read when `options.cwd` is omitted, so a build tool that changes directory between manifests stays correct. It wires the config-dependency replay path; compose `Workspaces.resolvers` with `Workspaces.layer` yourself if config-dependency code must not run.
|
|
114
|
+
|
|
115
|
+
For whole manifests, `Workspaces.resolveManifest` is the one-shot path over `@effected/npm`'s tolerant `Manifest` model:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { Manifest } from "@effected/npm";
|
|
119
|
+
import { Workspaces } from "@effected/workspaces";
|
|
120
|
+
import { Effect } from "effect";
|
|
121
|
+
|
|
122
|
+
const program = Effect.gen(function* () {
|
|
123
|
+
const manifest = yield* Manifest.decode({ dependencies: { effect: "catalog:" } });
|
|
124
|
+
const resolved = manifest.needsResolution ? yield* Workspaces.resolveManifest(manifest) : manifest;
|
|
125
|
+
return resolved.toRecord();
|
|
126
|
+
});
|
|
127
|
+
// needsResolution is pure — checking it first skips catalog assembly entirely
|
|
128
|
+
// when no dependency field carries a catalog: or workspace: specifier
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A specifier the workspace cannot answer fails typed as `UnresolvedDependencyError`: at the manifest level "no catalog entry" means the manifest cannot be projected to concrete ranges.
|
|
132
|
+
|
|
113
133
|
## The synchronous escape hatch
|
|
114
134
|
|
|
115
|
-
Vitest's config-time project discovery cannot await. Two functions exist for exactly that case, and they
|
|
135
|
+
Vitest's config-time project discovery cannot await. Two functions exist for exactly that case, and they run synchronously over file and path operations you supply — the module itself imports nothing platform-shaped, and Node's built-ins satisfy the operations one-liner each:
|
|
116
136
|
|
|
117
137
|
```ts
|
|
138
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
139
|
+
import * as path from "node:path";
|
|
118
140
|
import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
|
|
119
141
|
|
|
120
|
-
const
|
|
121
|
-
|
|
142
|
+
const options = {
|
|
143
|
+
fileSystem: {
|
|
144
|
+
exists: existsSync,
|
|
145
|
+
readFile: (p: string) => readFileSync(p, "utf8"),
|
|
146
|
+
readDirectory: (p: string) => readdirSync(p),
|
|
147
|
+
isDirectory: (p: string) => statSync(p).isDirectory(),
|
|
148
|
+
},
|
|
149
|
+
path, // node:path satisfies SyncPath verbatim
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const root = findWorkspaceRootSync(options);
|
|
153
|
+
const packages = root === null ? [] : getWorkspacePackagesSync(root, options);
|
|
122
154
|
// root: the workspace root path, or null when none is found above the cwd
|
|
123
155
|
// packages: the discovered workspace packages, empty when there is no root
|
|
124
156
|
```
|
|
125
157
|
|
|
126
|
-
Both entry points drive one traversal state machine
|
|
158
|
+
Windows correctness is the operations you pass — `node:path` on Windows is already win32-appropriate. Both entry points drive one traversal state machine (the same dequeue order, depth rule, visit budget and `node_modules` prune), so the sync and Effect surfaces can never disagree about what a pattern means. The one deliberate difference is at a bound: the Effect enumerator fails typed, the sync one truncates. Prefer the Effect API everywhere you can run one.
|
|
127
159
|
|
|
128
160
|
## Error handling
|
|
129
161
|
|
|
@@ -144,21 +176,22 @@ const program = Effect.gen(function* () {
|
|
|
144
176
|
);
|
|
145
177
|
```
|
|
146
178
|
|
|
147
|
-
`WorkspaceRootNotFoundError`, `WorkspaceDiscoveryError`, `WorkspacePatternError`, `PackageNotFoundError`, `WorkspaceManifestError`, `PackageManagerDetectionError`, `CatalogAssemblyError`, `LockfileReadError`, `CyclicDependencyError` and `ChangeDetectionError` each name one thing that can actually go wrong, and each method's error channel is narrowed to the ones it can produce. Change detection additionally surfaces `@effected/git`'s typed git errors, such as `NotARepositoryError`.
|
|
179
|
+
`WorkspaceRootNotFoundError`, `WorkspaceDiscoveryError`, `WorkspacePatternError`, `PackageNotFoundError`, `WorkspaceManifestError`, `PackageManagerDetectionError`, `CatalogAssemblyError`, `LockfileReadError`, `CyclicDependencyError` and `ChangeDetectionError` each name one thing that can actually go wrong, and each method's error channel is narrowed to the ones it can produce. `CatalogAssemblyError` is defined in `@effected/npm`, beside the resolver contract that names it in its channel — import it from there. Change detection additionally surfaces `@effected/git`'s typed git errors, such as `NotARepositoryError`.
|
|
148
180
|
|
|
149
181
|
## Features
|
|
150
182
|
|
|
151
183
|
- `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.
|
|
184
|
+
- `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.
|
|
152
185
|
- `WorkspaceRoot` — root discovery from a `cwd`, over `WORKSPACE_MARKERS`.
|
|
153
186
|
- `WorkspaceDiscovery` — package enumeration with a bounded descent for segment-crossing `packages/**` patterns, plus per-package lookup.
|
|
154
|
-
- `WorkspacePackage` — a deliberately tolerant manifest model, so one member with an odd version cannot fail discovery for the whole repo. `WorkspacePackage.manifest(pkg)` is the opt-in bridge to `@effected/package-json`'s strict `Package`.
|
|
187
|
+
- `WorkspacePackage` — a deliberately tolerant manifest model, so one member with an odd version cannot fail discovery for the whole repo. `manifestRecord` keeps the as-read `package.json` for tolerant access to fields outside the typed slice without a second read; `WorkspacePackage.manifest(pkg)` re-reads and is the opt-in bridge to `@effected/package-json`'s strict `Package`.
|
|
155
188
|
- `DependencyGraph` — a value class over discovered packages: `levels()` for parallel build tiers, the flattened topological order, and `CyclicDependencyError` when there isn't one.
|
|
156
189
|
- `PackageManagerDetector` — npm, pnpm, yarn or bun from lockfiles and the `packageManager` field.
|
|
157
190
|
- `WorkspaceCatalogs` — pnpm catalog assembly and `catalog:` resolution, on pnpm's own catalog packages.
|
|
158
191
|
- `LockfileReader` — locate and parse the workspace's lockfile through `@effected/lockfiles`.
|
|
159
192
|
- `ChangeDetector` — git-range change detection over `@effected/git`'s `Git` service; swap the layer to mock it with no repository.
|
|
160
193
|
- `PublishabilityDetector` — whether a package publishes and to where, as a `PublishTarget` (registry, directory, access, provenance). The default layer implements npm's semantics; swap the layer if yours differ.
|
|
161
|
-
- `findWorkspaceRootSync` / `getWorkspacePackagesSync` — the
|
|
194
|
+
- `findWorkspaceRootSync` / `getWorkspacePackagesSync` — the synchronous escape hatch for config-time callers that cannot await, over file and path operations you supply.
|
|
162
195
|
|
|
163
196
|
## License
|
|
164
197
|
|
package/WorkspaceCatalogs.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { CatalogAssemblyError } from "./CatalogAssemblyError.js";
|
|
2
1
|
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
3
2
|
import { inlineCatalogs, merge, normalize, rangeOf } from "./internal/catalogs.js";
|
|
4
3
|
import { ConfigDependencyHooks } from "./ConfigDependencyHooks.js";
|
|
5
4
|
import { LockfileReader } from "./LockfileReader.js";
|
|
6
5
|
import { Context, Duration, Effect, Exit, FileSystem, Layer, Option, Path, PlatformError, Schema } from "effect";
|
|
7
|
-
import { CatalogResolver, DependencyResolutionError } from "@effected/npm";
|
|
6
|
+
import { CatalogAssemblyError, CatalogResolver, DependencyResolutionError } from "@effected/npm";
|
|
8
7
|
import { Yaml } from "@effected/yaml";
|
|
9
8
|
|
|
10
9
|
//#region src/WorkspaceCatalogs.ts
|
|
@@ -99,7 +98,7 @@ entries: Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.String
|
|
|
99
98
|
* load-bearing for: a present-but-malformed `workspaces` shape (a number, a
|
|
100
99
|
* string, an object with a malformed `packages` / `catalog` / `catalogs`), or
|
|
101
100
|
* the default catalog declared twice — once as `workspaces.catalog` and again
|
|
102
|
-
* as `workspaces.catalogs.default` — fails with
|
|
101
|
+
* as `workspaces.catalogs.default` — fails with `CatalogAssemblyError`
|
|
103
102
|
* naming what was wrong. An absent `workspaces` field, one explicitly `null`,
|
|
104
103
|
* or the plain array form (npm/yarn patterns, which carry no catalogs) yields
|
|
105
104
|
* the empty set. Presence is checked structurally, so an explicitly-declared
|
|
@@ -269,7 +268,7 @@ const validatePnpmWorkspaceCatalogs = (document) => {
|
|
|
269
268
|
* within a catalog. An unreadable or absent lockfile degrades to no lockfile
|
|
270
269
|
* catalogs rather than failing (the inline declaration is the source of truth, the
|
|
271
270
|
* lockfile a record of what was installed); a malformed inline source **fails
|
|
272
|
-
* typed** with
|
|
271
|
+
* typed** with `CatalogAssemblyError`, because a silently-empty catalog read
|
|
273
272
|
* is the "every dependency looks newly added" bug.
|
|
274
273
|
*
|
|
275
274
|
* Assembly is deferred to the first call and memoized success-only, matching
|
|
@@ -372,19 +371,22 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
|
|
|
372
371
|
*
|
|
373
372
|
* @remarks
|
|
374
373
|
* `rangeOf` returns `Option.none()` for a dependency no catalog declares, per
|
|
375
|
-
* the contract's convention
|
|
376
|
-
*
|
|
377
|
-
*
|
|
374
|
+
* the contract's convention. A failed catalog *assembly* — an unreadable or
|
|
375
|
+
* malformed `pnpm-workspace.yaml`, a broken config-dependency hook — passes
|
|
376
|
+
* through **typed** as the contract's `CatalogAssemblyError` (it used to be
|
|
377
|
+
* folded into `DependencyResolutionError`'s defect `cause`, which forced
|
|
378
|
+
* consumers to `_tag`-sniff `unknown`); only the remaining mechanism failure,
|
|
379
|
+
* an unfindable workspace root, is wrapped as `DependencyResolutionError`.
|
|
378
380
|
*/
|
|
379
381
|
static catalogResolver = Layer.effect(CatalogResolver, Effect.gen(function* () {
|
|
380
382
|
const catalogs = yield* WorkspaceCatalogs;
|
|
381
|
-
return { rangeOf: (packageName, catalog) => catalogs.set().pipe(Effect.map((set) => set.rangeOf(packageName, catalog)), Effect.
|
|
383
|
+
return { rangeOf: (packageName, catalog) => catalogs.set().pipe(Effect.map((set) => set.rangeOf(packageName, catalog)), Effect.catchTag("WorkspaceRootNotFoundError", (cause) => Effect.fail(new DependencyResolutionError({
|
|
382
384
|
specifier: Option.match(catalog, {
|
|
383
385
|
onNone: () => "catalog:",
|
|
384
386
|
onSome: (name) => `catalog:${name}`
|
|
385
387
|
}),
|
|
386
388
|
cause
|
|
387
|
-
}))) };
|
|
389
|
+
})))) };
|
|
388
390
|
}));
|
|
389
391
|
};
|
|
390
392
|
|
package/WorkspaceDiscovery.js
CHANGED
|
@@ -184,6 +184,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
|
|
|
184
184
|
path: directory,
|
|
185
185
|
packageJsonPath,
|
|
186
186
|
relativePath,
|
|
187
|
+
manifestRecord: raw,
|
|
187
188
|
...typeof raw.private === "boolean" ? { private: raw.private } : {},
|
|
188
189
|
...isStringRecord(raw.dependencies) ? { dependencies: raw.dependencies } : {},
|
|
189
190
|
...isStringRecord(raw.devDependencies) ? { devDependencies: raw.devDependencies } : {},
|
package/WorkspacePackage.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Package } from "@effected/package-json";
|
|
|
5
5
|
|
|
6
6
|
//#region src/WorkspacePackage.ts
|
|
7
7
|
const EMPTY = Object.freeze(Object.create(null));
|
|
8
|
+
const EMPTY_MANIFEST = Object.freeze(Object.create(null));
|
|
8
9
|
/**
|
|
9
10
|
* The `publishConfig` fields workspace tooling reads.
|
|
10
11
|
*
|
|
@@ -100,7 +101,21 @@ var WorkspacePackage = class WorkspacePackage extends Schema.Class("WorkspacePac
|
|
|
100
101
|
/** Optional dependencies. */
|
|
101
102
|
optionalDependencies: DependencyMap,
|
|
102
103
|
/** The `publishConfig` block, when present. */
|
|
103
|
-
publishConfig: Schema.optionalKey(PublishConfig)
|
|
104
|
+
publishConfig: Schema.optionalKey(PublishConfig),
|
|
105
|
+
/**
|
|
106
|
+
* The package's `package.json` as read — tolerant access to every field
|
|
107
|
+
* outside the typed discovery slice (`scripts`, `exports`, …) without a
|
|
108
|
+
* second file read.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* Values are `unknown` and exactly what discovery parsed; nothing here is
|
|
112
|
+
* validated beyond being a record. For the strict typed model use
|
|
113
|
+
* `manifest()`, which deliberately **re-reads** the file — a point-in-time
|
|
114
|
+
* refresh this captured record cannot provide. Defaults to `{}` for
|
|
115
|
+
* construction sites and previously-serialized values that predate the
|
|
116
|
+
* field.
|
|
117
|
+
*/
|
|
118
|
+
manifestRecord: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.withDecodingDefaultKey(Effect.succeed(EMPTY_MANIFEST)), Schema.withConstructorDefault(Effect.succeed(EMPTY_MANIFEST)))
|
|
104
119
|
}) {
|
|
105
120
|
/** Whether this is the workspace root package. */
|
|
106
121
|
get isRootWorkspace() {
|
package/WorkspaceSnapshots.js
CHANGED
|
Binary file
|
|
@@ -140,6 +140,12 @@ var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot"
|
|
|
140
140
|
* THIS snapshot's catalog set — so code written to the contract resolves
|
|
141
141
|
* `catalog:` specifiers as of this ref. Built once per instance and cached, so
|
|
142
142
|
* it memoizes by reference.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* The contract's error channel (`CatalogAssemblyError` /
|
|
146
|
+
* `DependencyResolutionError`) is satisfied vacuously: a snapshot's catalogs
|
|
147
|
+
* were already assembled when it was captured, so this resolver is total —
|
|
148
|
+
* `rangeOf` never fails.
|
|
143
149
|
*/
|
|
144
150
|
get catalogResolver() {
|
|
145
151
|
if (this.#catalogResolver === void 0) this.#catalogResolver = Layer.succeed(CatalogResolver, { rangeOf: (packageName, catalog) => Effect.succeed(this.catalogs.rangeOf(packageName, catalog)) });
|
package/Workspaces.js
CHANGED
|
@@ -6,8 +6,8 @@ import { LockfileReader } from "./LockfileReader.js";
|
|
|
6
6
|
import { PublishabilityDetector } from "./Publishability.js";
|
|
7
7
|
import { WorkspaceCatalogs } from "./WorkspaceCatalogs.js";
|
|
8
8
|
import { WorkspaceSnapshots } from "./WorkspaceSnapshots.js";
|
|
9
|
-
import { Layer } from "effect";
|
|
10
9
|
import { Git } from "@effected/git";
|
|
10
|
+
import { Effect, Layer } from "effect";
|
|
11
11
|
|
|
12
12
|
//#region src/Workspaces.ts
|
|
13
13
|
/**
|
|
@@ -98,6 +98,77 @@ const resolvers = Layer.mergeAll(WorkspaceCatalogs.catalogResolver, WorkspaceDis
|
|
|
98
98
|
*/
|
|
99
99
|
const layerWithConfigDependencies = (options) => compose(options, WorkspaceCatalogs.layerWithConfigDependencies);
|
|
100
100
|
/**
|
|
101
|
+
* The one-call resolver factory: {@link Workspaces.resolvers} pre-wired over
|
|
102
|
+
* {@link Workspaces.layerWithConfigDependencies}, so the two `@effected/npm`
|
|
103
|
+
* contracts (`CatalogResolver`, `WorkspaceResolver`) need only a platform
|
|
104
|
+
* (`FileSystem` + `Path`) from the consumer.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* This is deliberately a **parameterized layer function, and the fresh layer
|
|
108
|
+
* per call is the feature**: layers memoize by reference, so each call mints
|
|
109
|
+
* an unmemoized layer whose root discovery re-runs — including a per-call
|
|
110
|
+
* `process.cwd()` read when `options.cwd` is omitted. A build tool that
|
|
111
|
+
* changes directory between manifests gets a correct re-discovery each time
|
|
112
|
+
* precisely because nothing is shared across calls. When you *want* sharing,
|
|
113
|
+
* bind one call's result to a `const` and provide that; the memoization rule
|
|
114
|
+
* is unchanged, this factory just refuses to hide it.
|
|
115
|
+
*
|
|
116
|
+
* Catalog assembly replays config-dependency `pnpmfile` hooks (the
|
|
117
|
+
* `layerWithConfigDependencies` path) — the semantics a real pnpm install
|
|
118
|
+
* has. Compose {@link Workspaces.resolvers} with {@link Workspaces.layer}
|
|
119
|
+
* yourself if config-dependency code must not run in process.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
124
|
+
* import { Effect } from "effect";
|
|
125
|
+
*
|
|
126
|
+
* const program = doSomethingWithResolvers.pipe(
|
|
127
|
+
* Effect.provide(Workspaces.resolverLayer()),
|
|
128
|
+
* );
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @public
|
|
132
|
+
*/
|
|
133
|
+
const resolverLayer = (options) => resolvers.pipe(Layer.provide(layerWithConfigDependencies(options)));
|
|
134
|
+
/**
|
|
135
|
+
* Resolve every `catalog:` and `workspace:` specifier in one `Manifest`
|
|
136
|
+
* against the real workspace, in one call — the 90% path. Decode stays at the
|
|
137
|
+
* consumer's edge: build the `Manifest` with `Manifest.decode` (from
|
|
138
|
+
* `@effected/npm`), hand it here, and get a new `Manifest` back with concrete
|
|
139
|
+
* ranges; `toRecord()` returns to the wire shape.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* Composes `manifest.resolve()` with a fresh {@link Workspaces.resolverLayer}
|
|
143
|
+
* per call, so the workspace root is re-discovered from `options.cwd` (or the
|
|
144
|
+
* current `process.cwd()`) on every invocation. Consumers processing many
|
|
145
|
+
* manifests should check `manifest.needsResolution` first and skip the call
|
|
146
|
+
* entirely when no dependency field carries a `catalog:`/`workspace:`
|
|
147
|
+
* specifier — that predicate is pure and avoids catalog assembly altogether.
|
|
148
|
+
*
|
|
149
|
+
* A specifier the workspace cannot answer fails typed as
|
|
150
|
+
* `UnresolvedDependencyError`; assembly and mechanism failures surface as
|
|
151
|
+
* `CatalogAssemblyError` / `DependencyResolutionError`.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* import { Manifest } from "@effected/npm";
|
|
156
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
157
|
+
* import { Effect } from "effect";
|
|
158
|
+
*
|
|
159
|
+
* const program = Effect.gen(function* () {
|
|
160
|
+
* const manifest = yield* Manifest.decode({ dependencies: { effect: "catalog:" } });
|
|
161
|
+
* const resolved = manifest.needsResolution ? yield* Workspaces.resolveManifest(manifest) : manifest;
|
|
162
|
+
* return resolved.toRecord();
|
|
163
|
+
* });
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* @public
|
|
167
|
+
*/
|
|
168
|
+
const resolveManifest = Effect.fn("Workspaces.resolveManifest")(function* (manifest, options) {
|
|
169
|
+
return yield* manifest.resolve().pipe(Effect.provide(resolverLayer(options)));
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
101
172
|
* The composite layers.
|
|
102
173
|
*
|
|
103
174
|
* @public
|
|
@@ -106,6 +177,8 @@ const Workspaces = {
|
|
|
106
177
|
layer,
|
|
107
178
|
layerWithConfigDependencies,
|
|
108
179
|
layerWithGit,
|
|
180
|
+
resolveManifest,
|
|
181
|
+
resolverLayer,
|
|
109
182
|
resolvers
|
|
110
183
|
};
|
|
111
184
|
|
package/WorkspacesSync.js
CHANGED
|
@@ -4,8 +4,6 @@ import { PublishConfig, WorkspacePackage } from "./WorkspacePackage.js";
|
|
|
4
4
|
import { Effect, Exit, Schema } from "effect";
|
|
5
5
|
import { GlobSet } from "@effected/glob";
|
|
6
6
|
import { Yaml } from "@effected/yaml";
|
|
7
|
-
import { dirname, join, resolve } from "node:path";
|
|
8
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
7
|
|
|
10
8
|
//#region src/WorkspacesSync.ts
|
|
11
9
|
/**
|
|
@@ -17,68 +15,84 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
17
15
|
* only on `undefined` sails straight into `raw.name` and throws a `TypeError`.
|
|
18
16
|
* These functions are documented as total and are reached from a Vitest config,
|
|
19
17
|
* which has nowhere to put a crash: malformed input must be *skipped*, never a
|
|
20
|
-
* defect.
|
|
18
|
+
* defect. A throwing consumer `readFile` lands in the same catch as a JSON
|
|
19
|
+
* syntax error — an unusable manifest either way.
|
|
21
20
|
*/
|
|
22
|
-
const readJson = (file) => {
|
|
21
|
+
const readJson = (fileSystem, file) => {
|
|
23
22
|
let parsed;
|
|
24
23
|
try {
|
|
25
|
-
parsed = JSON.parse(
|
|
24
|
+
parsed = JSON.parse(fileSystem.readFile(file));
|
|
26
25
|
} catch {
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
30
29
|
};
|
|
31
|
-
/** Whether `dir` is a directory. Never throws
|
|
32
|
-
const isDirectory = (dir) => {
|
|
30
|
+
/** Whether `dir` is a directory. Never throws — a throwing consumer op reads as `false`. */
|
|
31
|
+
const isDirectory = (fileSystem, dir) => {
|
|
33
32
|
try {
|
|
34
|
-
return
|
|
33
|
+
return fileSystem.isDirectory(dir);
|
|
35
34
|
} catch {
|
|
36
35
|
return false;
|
|
37
36
|
}
|
|
38
37
|
};
|
|
39
38
|
/** Whether `dir` holds a `package.json`. */
|
|
40
|
-
const isPackage = (dir) =>
|
|
39
|
+
const isPackage = (options, dir) => options.fileSystem.exists(options.path.join(dir, "package.json"));
|
|
41
40
|
/**
|
|
42
|
-
* The nearest workspace root at or above `cwd`, or `null`.
|
|
41
|
+
* The nearest workspace root at or above `options.cwd`, or `null`.
|
|
43
42
|
*
|
|
44
43
|
* @remarks
|
|
45
|
-
* **Synchronous
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* **Synchronous.** The Effect surface is `WorkspaceRoot`; reach for this one
|
|
45
|
+
* only where you genuinely cannot run an Effect — a Vitest config being the
|
|
46
|
+
* motivating case. The file and path operations are the caller's
|
|
47
|
+
* ({@link FindWorkspaceRootSyncOptions}); this module imports no `node:*` and
|
|
48
|
+
* assumes no posix.
|
|
48
49
|
*
|
|
49
50
|
* Markers match the async service exactly: a `pnpm-workspace.yaml`, or a
|
|
50
51
|
* `package.json` carrying a `workspaces` field.
|
|
51
52
|
*
|
|
52
|
-
* @param
|
|
53
|
+
* @param options - The consumer-supplied file and path operations, plus the
|
|
54
|
+
* optional `cwd` to start from (defaulting to `process.cwd()`).
|
|
53
55
|
*
|
|
54
56
|
* @example
|
|
55
57
|
* ```ts
|
|
58
|
+
* import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
59
|
+
* import * as path from "node:path";
|
|
56
60
|
* import { findWorkspaceRootSync } from "@effected/workspaces";
|
|
57
61
|
*
|
|
58
|
-
* const root = findWorkspaceRootSync(
|
|
62
|
+
* const root = findWorkspaceRootSync({
|
|
63
|
+
* fileSystem: {
|
|
64
|
+
* exists: existsSync,
|
|
65
|
+
* readFile: (p) => readFileSync(p, "utf8"),
|
|
66
|
+
* readDirectory: (p) => readdirSync(p),
|
|
67
|
+
* isDirectory: (p) => statSync(p).isDirectory(),
|
|
68
|
+
* },
|
|
69
|
+
* path,
|
|
70
|
+
* });
|
|
59
71
|
* ```
|
|
60
72
|
*
|
|
61
73
|
* @public
|
|
62
74
|
*/
|
|
63
|
-
const findWorkspaceRootSync = (
|
|
64
|
-
|
|
75
|
+
const findWorkspaceRootSync = (options) => {
|
|
76
|
+
const { fileSystem, path } = options;
|
|
77
|
+
let current = path.resolve(options.cwd ?? process.cwd());
|
|
65
78
|
for (let depth = 0; depth < 32 * 8; depth++) {
|
|
66
|
-
if (
|
|
67
|
-
const manifest = readJson(join(current, "package.json"));
|
|
79
|
+
if (fileSystem.exists(path.join(current, "pnpm-workspace.yaml"))) return current;
|
|
80
|
+
const manifest = readJson(fileSystem, path.join(current, "package.json"));
|
|
68
81
|
if (manifest?.workspaces !== void 0 && manifest.workspaces !== null) return current;
|
|
69
|
-
const parent = dirname(current);
|
|
82
|
+
const parent = path.dirname(current);
|
|
70
83
|
if (parent === current) return null;
|
|
71
84
|
current = parent;
|
|
72
85
|
}
|
|
73
86
|
return null;
|
|
74
87
|
};
|
|
75
88
|
/** The workspace `packages:` patterns for `root`, matching `internal/patterns.ts`'s precedence. */
|
|
76
|
-
const readPatternsSync = (root) => {
|
|
77
|
-
const
|
|
78
|
-
|
|
89
|
+
const readPatternsSync = (options, root) => {
|
|
90
|
+
const { fileSystem, path } = options;
|
|
91
|
+
const workspaceYaml = path.join(root, "pnpm-workspace.yaml");
|
|
92
|
+
if (fileSystem.exists(workspaceYaml)) {
|
|
79
93
|
let text = "";
|
|
80
94
|
try {
|
|
81
|
-
text =
|
|
95
|
+
text = fileSystem.readFile(workspaceYaml);
|
|
82
96
|
} catch {
|
|
83
97
|
text = "";
|
|
84
98
|
}
|
|
@@ -88,12 +102,12 @@ const readPatternsSync = (root) => {
|
|
|
88
102
|
if (patterns.length > 0) return patterns;
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
|
-
return manifestPatternsOf(readJson(join(root, "package.json")) ?? {});
|
|
105
|
+
return manifestPatternsOf(readJson(fileSystem, path.join(root, "package.json")) ?? {});
|
|
92
106
|
};
|
|
93
107
|
/** Build a `WorkspacePackage` from a directory, or `null` if its manifest is unusable. */
|
|
94
|
-
const readPackageSync = (directory, relativePath) => {
|
|
95
|
-
const packageJsonPath = join(directory, "package.json");
|
|
96
|
-
const raw = readJson(packageJsonPath);
|
|
108
|
+
const readPackageSync = (options, directory, relativePath) => {
|
|
109
|
+
const packageJsonPath = options.path.join(directory, "package.json");
|
|
110
|
+
const raw = readJson(options.fileSystem, packageJsonPath);
|
|
97
111
|
if (raw === void 0) return null;
|
|
98
112
|
const name = raw.name;
|
|
99
113
|
const version = raw.version;
|
|
@@ -113,6 +127,7 @@ const readPackageSync = (directory, relativePath) => {
|
|
|
113
127
|
devDependencies: stringRecord(raw.devDependencies) ?? {},
|
|
114
128
|
peerDependencies: stringRecord(raw.peerDependencies) ?? {},
|
|
115
129
|
optionalDependencies: stringRecord(raw.optionalDependencies) ?? {},
|
|
130
|
+
manifestRecord: raw,
|
|
116
131
|
...config !== void 0 && Exit.isSuccess(config) ? { publishConfig: config.value } : {}
|
|
117
132
|
});
|
|
118
133
|
};
|
|
@@ -120,7 +135,10 @@ const readPackageSync = (directory, relativePath) => {
|
|
|
120
135
|
* Every workspace package under `root`, root package first.
|
|
121
136
|
*
|
|
122
137
|
* @remarks
|
|
123
|
-
* **Synchronous
|
|
138
|
+
* **Synchronous.** The Effect surface is `WorkspaceDiscovery`. The file and
|
|
139
|
+
* path operations are the caller's ({@link GetWorkspacePackagesSyncOptions}
|
|
140
|
+
* extends {@link WorkspacesSyncOptions}); this module imports no `node:*` and
|
|
141
|
+
* assumes no posix.
|
|
124
142
|
*
|
|
125
143
|
* Pattern semantics are not merely "shared" with the Effect enumerator — both
|
|
126
144
|
* drive the **same traversal state machine** (`internal/traverse.ts`), so the
|
|
@@ -137,54 +155,67 @@ const readPackageSync = (directory, relativePath) => {
|
|
|
137
155
|
* the enumerator's defect.
|
|
138
156
|
*
|
|
139
157
|
* @param root - The workspace root, from {@link findWorkspaceRootSync}.
|
|
140
|
-
* @param options -
|
|
158
|
+
* @param options - The consumer-supplied operations and traversal bounds; see
|
|
159
|
+
* {@link GetWorkspacePackagesSyncOptions}.
|
|
141
160
|
*
|
|
142
161
|
* @example
|
|
143
162
|
* ```ts
|
|
163
|
+
* import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
164
|
+
* import * as path from "node:path";
|
|
144
165
|
* import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
|
|
145
166
|
*
|
|
146
|
-
* const
|
|
147
|
-
*
|
|
167
|
+
* const ops = {
|
|
168
|
+
* fileSystem: {
|
|
169
|
+
* exists: existsSync,
|
|
170
|
+
* readFile: (p: string) => readFileSync(p, "utf8"),
|
|
171
|
+
* readDirectory: (p: string) => readdirSync(p),
|
|
172
|
+
* isDirectory: (p: string) => statSync(p).isDirectory(),
|
|
173
|
+
* },
|
|
174
|
+
* path,
|
|
175
|
+
* };
|
|
176
|
+
* const root = findWorkspaceRootSync(ops);
|
|
177
|
+
* const packages = root === null ? [] : getWorkspacePackagesSync(root, ops);
|
|
148
178
|
* ```
|
|
149
179
|
*
|
|
150
180
|
* @public
|
|
151
181
|
*/
|
|
152
182
|
const getWorkspacePackagesSync = (root, options) => {
|
|
153
|
-
const
|
|
183
|
+
const { fileSystem, path } = options;
|
|
184
|
+
const maxDepth = options.maxDepth ?? 32;
|
|
154
185
|
if (!isValidMaxDepth(maxDepth)) throw new RangeError(`getWorkspacePackagesSync: ${badMaxDepthMessage(maxDepth)}`);
|
|
155
|
-
const patterns = readPatternsSync(root);
|
|
186
|
+
const patterns = readPatternsSync(options, root);
|
|
156
187
|
const compiled = Effect.runSyncExit(GlobSet.compile(patterns));
|
|
157
188
|
if (Exit.isFailure(compiled)) return [];
|
|
158
189
|
const globs = compiled.value;
|
|
159
190
|
const included = /* @__PURE__ */ new Map();
|
|
160
191
|
for (const literal of globs.literals) {
|
|
161
|
-
const absolute = join(root, literal);
|
|
162
|
-
if (isPackage(absolute)) included.set(literal, absolute);
|
|
192
|
+
const absolute = path.join(root, literal);
|
|
193
|
+
if (isPackage(options, absolute)) included.set(literal, absolute);
|
|
163
194
|
}
|
|
164
195
|
for (const wildcard of globs.wildcards) {
|
|
165
196
|
const base = wildcard.enumerationPrefix.replace(/\/$/, "");
|
|
166
|
-
const absoluteBase = join(root, base);
|
|
167
|
-
if (!isDirectory(absoluteBase)) continue;
|
|
197
|
+
const absoluteBase = path.join(root, base);
|
|
198
|
+
if (!isDirectory(fileSystem, absoluteBase)) continue;
|
|
168
199
|
const traversal = new Traversal(base, absoluteBase, maxDepth);
|
|
169
200
|
let stopped = false;
|
|
170
201
|
for (let current = traversal.next(); current !== void 0 && !stopped; current = traversal.next()) {
|
|
171
202
|
if (traversal.charge() !== void 0) break;
|
|
172
203
|
let entries = [];
|
|
173
204
|
try {
|
|
174
|
-
entries =
|
|
205
|
+
entries = fileSystem.readDirectory(current.absolute);
|
|
175
206
|
} catch {
|
|
176
207
|
continue;
|
|
177
208
|
}
|
|
178
209
|
for (const entry of entries) {
|
|
179
210
|
if (isPruned(entry)) continue;
|
|
180
211
|
const relative = joinRelative(current.relative, entry);
|
|
181
|
-
const absolute = join(current.absolute, entry);
|
|
182
|
-
if (!isDirectory(absolute)) continue;
|
|
212
|
+
const absolute = path.join(current.absolute, entry);
|
|
213
|
+
if (!isDirectory(fileSystem, absolute)) continue;
|
|
183
214
|
if (wildcard.crossesSegments && !traversal.admits(current)) {
|
|
184
215
|
stopped = true;
|
|
185
216
|
break;
|
|
186
217
|
}
|
|
187
|
-
if (wildcard.matches(relative) && isPackage(absolute)) included.set(relative, absolute);
|
|
218
|
+
if (wildcard.matches(relative) && isPackage(options, absolute)) included.set(relative, absolute);
|
|
188
219
|
if (!wildcard.crossesSegments) continue;
|
|
189
220
|
traversal.push(current, relative, absolute);
|
|
190
221
|
}
|
|
@@ -194,10 +225,10 @@ const getWorkspacePackagesSync = (root, options) => {
|
|
|
194
225
|
for (const [relativePath, absolute] of [...included.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
195
226
|
if (relativePath === "." || absolute === root) continue;
|
|
196
227
|
if (globs.excludes.some((exclude) => exclude.matches(relativePath))) continue;
|
|
197
|
-
const pkg = readPackageSync(absolute, relativePath);
|
|
228
|
+
const pkg = readPackageSync(options, absolute, relativePath);
|
|
198
229
|
if (pkg !== null) members.push(pkg);
|
|
199
230
|
}
|
|
200
|
-
const rootPackage = readPackageSync(root, ".");
|
|
231
|
+
const rootPackage = readPackageSync(options, root, ".");
|
|
201
232
|
return rootPackage === null ? members : [rootPackage, ...members];
|
|
202
233
|
};
|
|
203
234
|
|
package/index.d.ts
CHANGED
|
@@ -1,44 +1,10 @@
|
|
|
1
|
-
import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
2
1
|
import { Git, GitCommandError, NotARepositoryError, UnknownRefError } from "@effected/git";
|
|
3
|
-
import {
|
|
2
|
+
import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
3
|
+
import { CatalogAssemblyError, CatalogResolver, DependencyResolutionError, Manifest, UnresolvedDependencyError, WorkspaceResolver } from "@effected/npm";
|
|
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
7
|
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
8
|
-
//#region src/CatalogAssemblyError.d.ts
|
|
9
|
-
declare const CatalogAssemblyError_base: Schema.Class<CatalogAssemblyError, Schema.TaggedStruct<"CatalogAssemblyError", {
|
|
10
|
-
/**
|
|
11
|
-
* Which input failed: `manifest` for a file-level or top-level shape problem,
|
|
12
|
-
* `catalog` for a malformed catalog block or the double-default duplication,
|
|
13
|
-
* `hooks` for a config-dependency `pnpmfile.cjs` load or replay failure.
|
|
14
|
-
*/
|
|
15
|
-
readonly source: Schema.Literals<readonly ["manifest", "catalog", "hooks"]>;
|
|
16
|
-
/** The file, the catalog name, or the config dependency name. */
|
|
17
|
-
readonly path: Schema.String;
|
|
18
|
-
/** The originating failure. */
|
|
19
|
-
readonly cause: Schema.Defect;
|
|
20
|
-
}>, import("effect/Cause").YieldableError>;
|
|
21
|
-
/**
|
|
22
|
-
* Raised when a workspace's catalogs cannot be assembled — a `pnpm-workspace.yaml`
|
|
23
|
-
* that is unreadable or not valid YAML, a root `package.json` `workspaces` field
|
|
24
|
-
* whose shape or catalog blocks are malformed in a way pnpm itself rejects
|
|
25
|
-
* (including the default catalog declared twice), or a config dependency whose
|
|
26
|
-
* `pnpmfile.cjs` cannot be loaded or replayed.
|
|
27
|
-
*
|
|
28
|
-
* @remarks
|
|
29
|
-
* A *missing* `pnpm-workspace.yaml`, an *absent* `workspaces` field, or one
|
|
30
|
-
* explicitly `null` is not an error: there is simply nothing to misread, so
|
|
31
|
-
* assembly yields the empty set. The reader is otherwise **hard-fail by design** —
|
|
32
|
-
* a silently-empty catalog read is the "every dependency looks newly added" bug,
|
|
33
|
-
* because catalog output is load-bearing for snapshot diffing.
|
|
34
|
-
*
|
|
35
|
-
* @public
|
|
36
|
-
*/
|
|
37
|
-
declare class CatalogAssemblyError extends CatalogAssemblyError_base {
|
|
38
|
-
/** Renders the failing source into a one-line message. */
|
|
39
|
-
get message(): string;
|
|
40
|
-
}
|
|
41
|
-
//#endregion
|
|
42
8
|
//#region src/WorkspacePackage.d.ts
|
|
43
9
|
declare const PublishConfig_base: Schema.Class<PublishConfig, Schema.Struct<{
|
|
44
10
|
/** Scoped-package visibility. Its presence overrides `private`. */
|
|
@@ -129,6 +95,20 @@ declare const WorkspacePackage_base: Schema.Class<WorkspacePackage, Schema.Struc
|
|
|
129
95
|
readonly optionalDependencies: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>>;
|
|
130
96
|
/** The `publishConfig` block, when present. */
|
|
131
97
|
readonly publishConfig: Schema.optionalKey<typeof PublishConfig>;
|
|
98
|
+
/**
|
|
99
|
+
* The package's `package.json` as read — tolerant access to every field
|
|
100
|
+
* outside the typed discovery slice (`scripts`, `exports`, …) without a
|
|
101
|
+
* second file read.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* Values are `unknown` and exactly what discovery parsed; nothing here is
|
|
105
|
+
* validated beyond being a record. For the strict typed model use
|
|
106
|
+
* `manifest()`, which deliberately **re-reads** the file — a point-in-time
|
|
107
|
+
* refresh this captured record cannot provide. Defaults to `{}` for
|
|
108
|
+
* construction sites and previously-serialized values that predate the
|
|
109
|
+
* field.
|
|
110
|
+
*/
|
|
111
|
+
readonly manifestRecord: Schema.withConstructorDefault<Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.Unknown>, never>>;
|
|
132
112
|
}>, {}>;
|
|
133
113
|
/**
|
|
134
114
|
* A single package inside a workspace: the discovery-relevant slice of its
|
|
@@ -648,7 +628,7 @@ declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
|
|
|
648
628
|
* (in process, no subprocess) and replays its `updateConfig` hook over the
|
|
649
629
|
* seed, in declaration order. A dependency without a `pnpmfile.cjs` contributes
|
|
650
630
|
* nothing; a dependency whose file fails to load or replay fails typed with a
|
|
651
|
-
* `hooks`-source
|
|
631
|
+
* `hooks`-source `CatalogAssemblyError`, never a silent skip.
|
|
652
632
|
*
|
|
653
633
|
* @remarks
|
|
654
634
|
* Node-coupled by design — the `node:fs` / `node:path` / `node:url` imports are
|
|
@@ -1107,7 +1087,7 @@ declare class CatalogSet extends CatalogSet_base {
|
|
|
1107
1087
|
* load-bearing for: a present-but-malformed `workspaces` shape (a number, a
|
|
1108
1088
|
* string, an object with a malformed `packages` / `catalog` / `catalogs`), or
|
|
1109
1089
|
* the default catalog declared twice — once as `workspaces.catalog` and again
|
|
1110
|
-
* as `workspaces.catalogs.default` — fails with
|
|
1090
|
+
* as `workspaces.catalogs.default` — fails with `CatalogAssemblyError`
|
|
1111
1091
|
* naming what was wrong. An absent `workspaces` field, one explicitly `null`,
|
|
1112
1092
|
* or the plain array form (npm/yarn patterns, which carry no catalogs) yields
|
|
1113
1093
|
* the empty set. Presence is checked structurally, so an explicitly-declared
|
|
@@ -1190,7 +1170,7 @@ declare const WorkspaceCatalogs_base: Context.ServiceClass<WorkspaceCatalogs, "@
|
|
|
1190
1170
|
* within a catalog. An unreadable or absent lockfile degrades to no lockfile
|
|
1191
1171
|
* catalogs rather than failing (the inline declaration is the source of truth, the
|
|
1192
1172
|
* lockfile a record of what was installed); a malformed inline source **fails
|
|
1193
|
-
* typed** with
|
|
1173
|
+
* typed** with `CatalogAssemblyError`, because a silently-empty catalog read
|
|
1194
1174
|
* is the "every dependency looks newly added" bug.
|
|
1195
1175
|
*
|
|
1196
1176
|
* Assembly is deferred to the first call and memoized success-only, matching
|
|
@@ -1229,9 +1209,12 @@ declare class WorkspaceCatalogs extends WorkspaceCatalogs_base {
|
|
|
1229
1209
|
*
|
|
1230
1210
|
* @remarks
|
|
1231
1211
|
* `rangeOf` returns `Option.none()` for a dependency no catalog declares, per
|
|
1232
|
-
* the contract's convention
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1212
|
+
* the contract's convention. A failed catalog *assembly* — an unreadable or
|
|
1213
|
+
* malformed `pnpm-workspace.yaml`, a broken config-dependency hook — passes
|
|
1214
|
+
* through **typed** as the contract's `CatalogAssemblyError` (it used to be
|
|
1215
|
+
* folded into `DependencyResolutionError`'s defect `cause`, which forced
|
|
1216
|
+
* consumers to `_tag`-sniff `unknown`); only the remaining mechanism failure,
|
|
1217
|
+
* an unfindable workspace root, is wrapped as `DependencyResolutionError`.
|
|
1235
1218
|
*/
|
|
1236
1219
|
static readonly catalogResolver: Layer.Layer<CatalogResolver, never, WorkspaceCatalogs>;
|
|
1237
1220
|
}
|
|
@@ -1342,6 +1325,12 @@ declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
|
|
|
1342
1325
|
* THIS snapshot's catalog set — so code written to the contract resolves
|
|
1343
1326
|
* `catalog:` specifiers as of this ref. Built once per instance and cached, so
|
|
1344
1327
|
* it memoizes by reference.
|
|
1328
|
+
*
|
|
1329
|
+
* @remarks
|
|
1330
|
+
* The contract's error channel (`CatalogAssemblyError` /
|
|
1331
|
+
* `DependencyResolutionError`) is satisfied vacuously: a snapshot's catalogs
|
|
1332
|
+
* were already assembled when it was captured, so this resolver is total —
|
|
1333
|
+
* `rangeOf` never fails.
|
|
1345
1334
|
*/
|
|
1346
1335
|
get catalogResolver(): Layer.Layer<CatalogResolver>;
|
|
1347
1336
|
/**
|
|
@@ -1365,7 +1354,7 @@ declare class WorkspaceStateSnapshot extends WorkspaceStateSnapshot_base {
|
|
|
1365
1354
|
* filesystem, so no `WorkspaceDiscoveryError` / `WorkspacePatternError` appears —
|
|
1366
1355
|
* and a malformed *lockfile* at the ref degrades to no catalogs (a lockfile is a
|
|
1367
1356
|
* record, not a source of truth), so only the inline source raises
|
|
1368
|
-
*
|
|
1357
|
+
* `CatalogAssemblyError`.
|
|
1369
1358
|
*
|
|
1370
1359
|
* @public
|
|
1371
1360
|
*/
|
|
@@ -1479,39 +1468,148 @@ declare const Workspaces: {
|
|
|
1479
1468
|
readonly layer: (options?: WorkspacesOptions) => Layer.Layer<WorkspacesServices, never, FileSystem.FileSystem | Path.Path>;
|
|
1480
1469
|
readonly layerWithConfigDependencies: (options?: WorkspacesOptions) => Layer.Layer<WorkspacesServices, never, FileSystem.FileSystem | Path.Path>;
|
|
1481
1470
|
readonly layerWithGit: (options?: WorkspacesOptions) => Layer.Layer<WorkspacesServices | ChangeDetector | WorkspaceSnapshots | Git, never, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
|
|
1471
|
+
readonly resolveManifest: (manifest: Manifest, options?: WorkspacesOptions) => Effect.Effect<Manifest, CatalogAssemblyError | DependencyResolutionError | UnresolvedDependencyError, FileSystem.FileSystem | Path.Path>;
|
|
1472
|
+
readonly resolverLayer: (options?: WorkspacesOptions) => Layer.Layer<CatalogResolver | WorkspaceResolver, never, FileSystem.FileSystem | Path.Path>;
|
|
1482
1473
|
readonly resolvers: Layer.Layer<CatalogResolver | WorkspaceResolver, never, WorkspaceCatalogs | WorkspaceDiscovery>;
|
|
1483
1474
|
};
|
|
1484
1475
|
//#endregion
|
|
1485
1476
|
//#region src/WorkspacesSync.d.ts
|
|
1486
1477
|
/**
|
|
1487
|
-
* The
|
|
1478
|
+
* The synchronous file operations the sync entry points need, supplied by the
|
|
1479
|
+
* consumer. Node's built-ins satisfy it directly:
|
|
1480
|
+
*
|
|
1481
|
+
* ```ts
|
|
1482
|
+
* import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
1483
|
+
*
|
|
1484
|
+
* const fileSystem: SyncFileSystem = {
|
|
1485
|
+
* exists: existsSync,
|
|
1486
|
+
* readFile: (p) => readFileSync(p, "utf8"),
|
|
1487
|
+
* readDirectory: (p) => readdirSync(p),
|
|
1488
|
+
* isDirectory: (p) => statSync(p).isDirectory(),
|
|
1489
|
+
* };
|
|
1490
|
+
* ```
|
|
1491
|
+
*
|
|
1492
|
+
* `exists` must return a boolean and not throw (Node's `existsSync` never
|
|
1493
|
+
* does). The other three may throw — `statSync` on a missing path, a
|
|
1494
|
+
* permission error mid-read — and every throw is absorbed into the documented
|
|
1495
|
+
* degraded-skip semantics: a throwing `readFile` reads as an unusable
|
|
1496
|
+
* manifest, a throwing `readDirectory` as an unreadable directory, a throwing
|
|
1497
|
+
* `isDirectory` as "not a directory". Nothing propagates.
|
|
1498
|
+
*
|
|
1499
|
+
* @public
|
|
1500
|
+
*/
|
|
1501
|
+
interface SyncFileSystem {
|
|
1502
|
+
/** Whether a file or directory exists at `path`. Must not throw. */
|
|
1503
|
+
readonly exists: (path: string) => boolean;
|
|
1504
|
+
/** Read the file at `path` as text. May throw; a throw degrades to a skip. */
|
|
1505
|
+
readonly readFile: (path: string) => string;
|
|
1506
|
+
/** The entry names inside the directory at `path`. May throw; a throw skips the directory. */
|
|
1507
|
+
readonly readDirectory: (path: string) => ReadonlyArray<string>;
|
|
1508
|
+
/** Whether `path` is a directory. May throw; a throw reads as `false`. */
|
|
1509
|
+
readonly isDirectory: (path: string) => boolean;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* The synchronous path operations the sync entry points need, supplied by the
|
|
1513
|
+
* consumer. Deliberately a structural subset of `node:path`, so the built-in
|
|
1514
|
+
* module (and its `win32` / `posix` variants, or a Bun / Deno equivalent)
|
|
1515
|
+
* satisfies it verbatim:
|
|
1516
|
+
*
|
|
1517
|
+
* ```ts
|
|
1518
|
+
* import * as path from "node:path";
|
|
1519
|
+
*
|
|
1520
|
+
* const options: WorkspacesSyncOptions = {
|
|
1521
|
+
* fileSystem: { exists: existsSync, readFile: (p) => readFileSync(p, "utf8"), readDirectory: (p) => readdirSync(p), isDirectory: (p) => statSync(p).isDirectory() },
|
|
1522
|
+
* path, // node:path IS a SyncPath
|
|
1523
|
+
* };
|
|
1524
|
+
* ```
|
|
1525
|
+
*
|
|
1526
|
+
* These operations shape only the ABSOLUTE paths handed back to the consumer
|
|
1527
|
+
* (and to its own `fileSystem`); workspace-relative pattern matching is POSIX
|
|
1528
|
+
* by the `packages:` contract and never routes through here. Windows
|
|
1529
|
+
* correctness comes from supplying a win32-appropriate implementation, not
|
|
1530
|
+
* from anything in this module.
|
|
1531
|
+
*
|
|
1532
|
+
* @public
|
|
1533
|
+
*/
|
|
1534
|
+
interface SyncPath {
|
|
1535
|
+
/** Join segments with the implementation's separator (like `path.join`). */
|
|
1536
|
+
readonly join: (...segments: ReadonlyArray<string>) => string;
|
|
1537
|
+
/** The directory portion of `p` (like `path.dirname`). */
|
|
1538
|
+
readonly dirname: (p: string) => string;
|
|
1539
|
+
/** Resolve segments to an absolute path (rightmost-wins, like `path.resolve`). */
|
|
1540
|
+
readonly resolve: (...segments: ReadonlyArray<string>) => string;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* The consumer-supplied operations backing one sync call: the file operations
|
|
1544
|
+
* and the path implementation. Both are required — this package never imports
|
|
1545
|
+
* `node:*` and never assumes posix, so the platform binding is entirely the
|
|
1546
|
+
* caller's.
|
|
1547
|
+
*
|
|
1548
|
+
* @public
|
|
1549
|
+
*/
|
|
1550
|
+
interface WorkspacesSyncOptions {
|
|
1551
|
+
/** The synchronous file operations (Node: `existsSync` / `readFileSync` / `readdirSync` / `statSync`). */
|
|
1552
|
+
readonly fileSystem: SyncFileSystem;
|
|
1553
|
+
/** The synchronous path implementation (Node: the `node:path` module itself). */
|
|
1554
|
+
readonly path: SyncPath;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Options for {@link findWorkspaceRootSync}: the required consumer-supplied
|
|
1558
|
+
* operations plus where to start the ascent.
|
|
1559
|
+
*
|
|
1560
|
+
* @public
|
|
1561
|
+
*/
|
|
1562
|
+
interface FindWorkspaceRootSyncOptions extends WorkspacesSyncOptions {
|
|
1563
|
+
/**
|
|
1564
|
+
* Where to start the ascent, matching the Effect layers' `{ cwd }` option.
|
|
1565
|
+
*
|
|
1566
|
+
* @defaultValue `process.cwd()`, read lazily per call.
|
|
1567
|
+
*/
|
|
1568
|
+
readonly cwd?: string;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* The nearest workspace root at or above `options.cwd`, or `null`.
|
|
1488
1572
|
*
|
|
1489
1573
|
* @remarks
|
|
1490
|
-
* **Synchronous
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1574
|
+
* **Synchronous.** The Effect surface is `WorkspaceRoot`; reach for this one
|
|
1575
|
+
* only where you genuinely cannot run an Effect — a Vitest config being the
|
|
1576
|
+
* motivating case. The file and path operations are the caller's
|
|
1577
|
+
* ({@link FindWorkspaceRootSyncOptions}); this module imports no `node:*` and
|
|
1578
|
+
* assumes no posix.
|
|
1493
1579
|
*
|
|
1494
1580
|
* Markers match the async service exactly: a `pnpm-workspace.yaml`, or a
|
|
1495
1581
|
* `package.json` carrying a `workspaces` field.
|
|
1496
1582
|
*
|
|
1497
|
-
* @param
|
|
1583
|
+
* @param options - The consumer-supplied file and path operations, plus the
|
|
1584
|
+
* optional `cwd` to start from (defaulting to `process.cwd()`).
|
|
1498
1585
|
*
|
|
1499
1586
|
* @example
|
|
1500
1587
|
* ```ts
|
|
1588
|
+
* import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
1589
|
+
* import * as path from "node:path";
|
|
1501
1590
|
* import { findWorkspaceRootSync } from "@effected/workspaces";
|
|
1502
1591
|
*
|
|
1503
|
-
* const root = findWorkspaceRootSync(
|
|
1592
|
+
* const root = findWorkspaceRootSync({
|
|
1593
|
+
* fileSystem: {
|
|
1594
|
+
* exists: existsSync,
|
|
1595
|
+
* readFile: (p) => readFileSync(p, "utf8"),
|
|
1596
|
+
* readDirectory: (p) => readdirSync(p),
|
|
1597
|
+
* isDirectory: (p) => statSync(p).isDirectory(),
|
|
1598
|
+
* },
|
|
1599
|
+
* path,
|
|
1600
|
+
* });
|
|
1504
1601
|
* ```
|
|
1505
1602
|
*
|
|
1506
1603
|
* @public
|
|
1507
1604
|
*/
|
|
1508
|
-
declare const findWorkspaceRootSync: (
|
|
1605
|
+
declare const findWorkspaceRootSync: (options: FindWorkspaceRootSyncOptions) => string | null;
|
|
1509
1606
|
/**
|
|
1510
|
-
* Options for {@link getWorkspacePackagesSync}
|
|
1607
|
+
* Options for {@link getWorkspacePackagesSync}: the required consumer-supplied
|
|
1608
|
+
* operations plus the traversal bound.
|
|
1511
1609
|
*
|
|
1512
1610
|
* @public
|
|
1513
1611
|
*/
|
|
1514
|
-
interface GetWorkspacePackagesSyncOptions {
|
|
1612
|
+
interface GetWorkspacePackagesSyncOptions extends WorkspacesSyncOptions {
|
|
1515
1613
|
/**
|
|
1516
1614
|
* Descent cap below a wildcard's enumeration prefix, mirroring
|
|
1517
1615
|
* `WorkspaceDiscovery.layer({ maxDepth })`.
|
|
@@ -1524,7 +1622,10 @@ interface GetWorkspacePackagesSyncOptions {
|
|
|
1524
1622
|
* Every workspace package under `root`, root package first.
|
|
1525
1623
|
*
|
|
1526
1624
|
* @remarks
|
|
1527
|
-
* **Synchronous
|
|
1625
|
+
* **Synchronous.** The Effect surface is `WorkspaceDiscovery`. The file and
|
|
1626
|
+
* path operations are the caller's ({@link GetWorkspacePackagesSyncOptions}
|
|
1627
|
+
* extends {@link WorkspacesSyncOptions}); this module imports no `node:*` and
|
|
1628
|
+
* assumes no posix.
|
|
1528
1629
|
*
|
|
1529
1630
|
* Pattern semantics are not merely "shared" with the Effect enumerator — both
|
|
1530
1631
|
* drive the **same traversal state machine** (`internal/traverse.ts`), so the
|
|
@@ -1541,19 +1642,31 @@ interface GetWorkspacePackagesSyncOptions {
|
|
|
1541
1642
|
* the enumerator's defect.
|
|
1542
1643
|
*
|
|
1543
1644
|
* @param root - The workspace root, from {@link findWorkspaceRootSync}.
|
|
1544
|
-
* @param options -
|
|
1645
|
+
* @param options - The consumer-supplied operations and traversal bounds; see
|
|
1646
|
+
* {@link GetWorkspacePackagesSyncOptions}.
|
|
1545
1647
|
*
|
|
1546
1648
|
* @example
|
|
1547
1649
|
* ```ts
|
|
1650
|
+
* import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
1651
|
+
* import * as path from "node:path";
|
|
1548
1652
|
* import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
|
|
1549
1653
|
*
|
|
1550
|
-
* const
|
|
1551
|
-
*
|
|
1654
|
+
* const ops = {
|
|
1655
|
+
* fileSystem: {
|
|
1656
|
+
* exists: existsSync,
|
|
1657
|
+
* readFile: (p: string) => readFileSync(p, "utf8"),
|
|
1658
|
+
* readDirectory: (p: string) => readdirSync(p),
|
|
1659
|
+
* isDirectory: (p: string) => statSync(p).isDirectory(),
|
|
1660
|
+
* },
|
|
1661
|
+
* path,
|
|
1662
|
+
* };
|
|
1663
|
+
* const root = findWorkspaceRootSync(ops);
|
|
1664
|
+
* const packages = root === null ? [] : getWorkspacePackagesSync(root, ops);
|
|
1552
1665
|
* ```
|
|
1553
1666
|
*
|
|
1554
1667
|
* @public
|
|
1555
1668
|
*/
|
|
1556
|
-
declare const getWorkspacePackagesSync: (root: string, options
|
|
1669
|
+
declare const getWorkspacePackagesSync: (root: string, options: GetWorkspacePackagesSyncOptions) => ReadonlyArray<WorkspacePackage>;
|
|
1557
1670
|
//#endregion
|
|
1558
|
-
export {
|
|
1671
|
+
export { type CatalogAssemblyFailure, CatalogSet, ChangeDetectionError, type ChangeDetectionFailure, ChangeDetectionOptions, ChangeDetector, type ChangeDetectorShape, ConfigDependencyHooks, type ConfigDependencyHooksShape, CyclicDependencyError, type DependencyDiff, DependencyGraph, DetectedPackageManager, type FindWorkspaceRootSyncOptions, type GetWorkspacePackagesSyncOptions, LockfileReadError, type LockfileReadFailure, LockfileReader, type LockfileReaderOptions, type LockfileReaderShape, PackageManagerDetectionError, type PackageManagerDetectionFailure, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, type SyncFileSystem, type SyncPath, WORKSPACE_MARKERS, WorkspaceCatalogs, type WorkspaceCatalogsOptions, type WorkspaceCatalogsShape, WorkspaceDiscovery, WorkspaceDiscoveryError, type WorkspaceDiscoveryFailure, type WorkspaceDiscoveryOptions, type WorkspaceDiscoveryShape, WorkspaceInfo, type WorkspaceLookupFailure, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, type WorkspaceSnapshotAtFailure, type WorkspaceSnapshotWorktreeFailure, WorkspaceSnapshots, type WorkspaceSnapshotsOptions, type WorkspaceSnapshotsShape, WorkspaceStateSnapshot, Workspaces, type WorkspacesOptions, type WorkspacesServices, type WorkspacesSyncOptions, findWorkspaceRootSync, getWorkspacePackagesSync };
|
|
1559
1672
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { CatalogAssemblyError } from "./CatalogAssemblyError.js";
|
|
2
1
|
import { PublishConfig, WorkspaceManifestError, WorkspacePackage } from "./WorkspacePackage.js";
|
|
3
2
|
import { WORKSPACE_MARKERS, WorkspaceRoot, WorkspaceRootNotFoundError } from "./WorkspaceRoot.js";
|
|
4
3
|
import { PackageNotFoundError, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspacePatternError } from "./WorkspaceDiscovery.js";
|
|
@@ -14,4 +13,4 @@ import { WorkspaceSnapshots } from "./WorkspaceSnapshots.js";
|
|
|
14
13
|
import { Workspaces } from "./Workspaces.js";
|
|
15
14
|
import { findWorkspaceRootSync, getWorkspacePackagesSync } from "./WorkspacesSync.js";
|
|
16
15
|
|
|
17
|
-
export {
|
|
16
|
+
export { CatalogSet, ChangeDetectionError, ChangeDetectionOptions, ChangeDetector, ConfigDependencyHooks, CyclicDependencyError, DependencyGraph, DetectedPackageManager, LockfileReadError, LockfileReader, PackageManagerDetectionError, PackageManagerDetector, PackageManagerName, PackageNotFoundError, PackageStateSnapshot, PublishConfig, PublishTarget, PublishabilityDetector, WORKSPACE_MARKERS, WorkspaceCatalogs, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspaceManifestError, WorkspacePackage, WorkspacePatternError, WorkspaceRoot, WorkspaceRootNotFoundError, WorkspaceSnapshots, WorkspaceStateSnapshot, Workspaces, findWorkspaceRootSync, getWorkspacePackagesSync };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": [
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@effected/git": "0.1.0",
|
|
44
44
|
"@effected/glob": "0.1.0",
|
|
45
|
-
"@effected/lockfiles": "0.1.
|
|
46
|
-
"@effected/npm": "0.
|
|
47
|
-
"@effected/package-json": "0.
|
|
45
|
+
"@effected/lockfiles": "0.1.1",
|
|
46
|
+
"@effected/npm": "0.2.0",
|
|
47
|
+
"@effected/package-json": "0.2.0",
|
|
48
48
|
"@effected/walker": "0.1.0",
|
|
49
49
|
"@effected/yaml": "0.1.0",
|
|
50
50
|
"@pnpm/catalogs.config": "^1100.0.0",
|
package/CatalogAssemblyError.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
|
|
3
|
-
//#region src/CatalogAssemblyError.ts
|
|
4
|
-
/**
|
|
5
|
-
* Raised when a workspace's catalogs cannot be assembled — a `pnpm-workspace.yaml`
|
|
6
|
-
* that is unreadable or not valid YAML, a root `package.json` `workspaces` field
|
|
7
|
-
* whose shape or catalog blocks are malformed in a way pnpm itself rejects
|
|
8
|
-
* (including the default catalog declared twice), or a config dependency whose
|
|
9
|
-
* `pnpmfile.cjs` cannot be loaded or replayed.
|
|
10
|
-
*
|
|
11
|
-
* @remarks
|
|
12
|
-
* A *missing* `pnpm-workspace.yaml`, an *absent* `workspaces` field, or one
|
|
13
|
-
* explicitly `null` is not an error: there is simply nothing to misread, so
|
|
14
|
-
* assembly yields the empty set. The reader is otherwise **hard-fail by design** —
|
|
15
|
-
* a silently-empty catalog read is the "every dependency looks newly added" bug,
|
|
16
|
-
* because catalog output is load-bearing for snapshot diffing.
|
|
17
|
-
*
|
|
18
|
-
* @public
|
|
19
|
-
*/
|
|
20
|
-
var CatalogAssemblyError = class extends Schema.TaggedErrorClass()("CatalogAssemblyError", {
|
|
21
|
-
/**
|
|
22
|
-
* Which input failed: `manifest` for a file-level or top-level shape problem,
|
|
23
|
-
* `catalog` for a malformed catalog block or the double-default duplication,
|
|
24
|
-
* `hooks` for a config-dependency `pnpmfile.cjs` load or replay failure.
|
|
25
|
-
*/
|
|
26
|
-
source: Schema.Literals([
|
|
27
|
-
"manifest",
|
|
28
|
-
"catalog",
|
|
29
|
-
"hooks"
|
|
30
|
-
]),
|
|
31
|
-
/** The file, the catalog name, or the config dependency name. */
|
|
32
|
-
path: Schema.String,
|
|
33
|
-
/** The originating failure. */
|
|
34
|
-
cause: Schema.Defect()
|
|
35
|
-
}) {
|
|
36
|
-
/** Renders the failing source into a one-line message. */
|
|
37
|
-
get message() {
|
|
38
|
-
return `Failed to assemble catalogs from ${this.source} ${this.path}`;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
//#endregion
|
|
43
|
-
export { CatalogAssemblyError };
|