@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,165 @@
|
|
|
1
|
+
import { CatalogSet } from "./WorkspaceCatalogs.js";
|
|
2
|
+
import { Effect, Exit, Layer, Option, Schema } from "effect";
|
|
3
|
+
import { CatalogResolver, DependencySpecifier, WorkspaceResolver } from "@effected/npm";
|
|
4
|
+
|
|
5
|
+
//#region src/WorkspaceStateSnapshot.ts
|
|
6
|
+
const EMPTY = Object.freeze(Object.create(null));
|
|
7
|
+
const DependencyMap = Schema.Record(Schema.String, Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(EMPTY)), Schema.withConstructorDefault(Effect.succeed(EMPTY)));
|
|
8
|
+
/**
|
|
9
|
+
* One workspace member as captured in a {@link WorkspaceStateSnapshot} — the
|
|
10
|
+
* serializable slice a snapshot diff reads: identity, version, location, and the
|
|
11
|
+
* four dependency records.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Deliberately narrower than {@link WorkspacePackage}: a snapshot is a value to
|
|
15
|
+
* store and diff, not a located member to act on, so it carries no absolute
|
|
16
|
+
* paths, `publishConfig`, or `private` flag. The four records are keyed by the
|
|
17
|
+
* standard manifest field names, which are exactly `@effected/npm`'s
|
|
18
|
+
* `DependencyField` values.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
var PackageStateSnapshot = class extends Schema.Class("PackageStateSnapshot")({
|
|
23
|
+
/** The package name. */
|
|
24
|
+
name: Schema.NonEmptyString,
|
|
25
|
+
/** The raw `version` string, as recorded at the captured moment. */
|
|
26
|
+
version: Schema.String,
|
|
27
|
+
/** POSIX path relative to the workspace root; `"."` for the root package. */
|
|
28
|
+
relativePath: Schema.String,
|
|
29
|
+
/** Production dependencies. */
|
|
30
|
+
dependencies: DependencyMap,
|
|
31
|
+
/** Development dependencies. */
|
|
32
|
+
devDependencies: DependencyMap,
|
|
33
|
+
/** Peer dependencies. */
|
|
34
|
+
peerDependencies: DependencyMap,
|
|
35
|
+
/** Optional dependencies. */
|
|
36
|
+
optionalDependencies: DependencyMap
|
|
37
|
+
}) {
|
|
38
|
+
/**
|
|
39
|
+
* Every dependency, merged across the four kinds.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Precedence on a name declared in several kinds runs
|
|
43
|
+
* `dependencies` \> `devDependencies` \> `peerDependencies` \>
|
|
44
|
+
* `optionalDependencies`.
|
|
45
|
+
*/
|
|
46
|
+
get allDependencies() {
|
|
47
|
+
return {
|
|
48
|
+
...this.optionalDependencies,
|
|
49
|
+
...this.peerDependencies,
|
|
50
|
+
...this.devDependencies,
|
|
51
|
+
...this.dependencies
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The state of a whole workspace at one moment — its packages and its assembled
|
|
57
|
+
* catalog set — as a serializable value.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* Produced by `WorkspaceSnapshots.at` (a git ref, read with no checkout)
|
|
61
|
+
* or `WorkspaceSnapshots.worktree` (the live tree). The lookup and
|
|
62
|
+
* resolution surfaces (`versions`, `package`, `resolve`, the resolver layers)
|
|
63
|
+
* are backed by `#private` indexes built lazily on first use and never encoded —
|
|
64
|
+
* the `DependencyGraph` edge-index precedent.
|
|
65
|
+
*
|
|
66
|
+
* `resolve` and the resolver layers answer specifiers against THIS snapshot's
|
|
67
|
+
* own captured state, so a consumer can ask "what did `catalog:` /
|
|
68
|
+
* `workspace:*` mean as of that ref". An unmatched specifier is always
|
|
69
|
+
* `Option.none()`, never an error.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { WorkspaceSnapshots } from "@effected/workspaces";
|
|
74
|
+
* import { Effect } from "effect";
|
|
75
|
+
*
|
|
76
|
+
* const program = Effect.gen(function* () {
|
|
77
|
+
* const snapshots = yield* WorkspaceSnapshots;
|
|
78
|
+
* const before = yield* snapshots.at("origin/main");
|
|
79
|
+
* return before.resolve("effect", "catalog:");
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
var WorkspaceStateSnapshot = class extends Schema.Class("WorkspaceStateSnapshot")({
|
|
86
|
+
/** Every workspace package captured at this moment. */
|
|
87
|
+
packages: Schema.Array(PackageStateSnapshot),
|
|
88
|
+
/** The catalog set assembled at this moment. */
|
|
89
|
+
catalogs: CatalogSet
|
|
90
|
+
}) {
|
|
91
|
+
#versionIndex;
|
|
92
|
+
#packageIndex;
|
|
93
|
+
#catalogResolver;
|
|
94
|
+
#workspaceResolver;
|
|
95
|
+
#resolvers;
|
|
96
|
+
#versions() {
|
|
97
|
+
if (this.#versionIndex === void 0) this.#versionIndex = new Map(this.packages.map((pkg) => [pkg.name, pkg.version]));
|
|
98
|
+
return this.#versionIndex;
|
|
99
|
+
}
|
|
100
|
+
#packages() {
|
|
101
|
+
if (this.#packageIndex === void 0) this.#packageIndex = new Map(this.packages.map((pkg) => [pkg.name, pkg]));
|
|
102
|
+
return this.#packageIndex;
|
|
103
|
+
}
|
|
104
|
+
/** Every captured package's name → version. Total; O(1) after the first call. */
|
|
105
|
+
get versions() {
|
|
106
|
+
return this.#versions();
|
|
107
|
+
}
|
|
108
|
+
/** A single captured package by name, or `Option.none()`. Total. */
|
|
109
|
+
package(name) {
|
|
110
|
+
return Option.fromUndefinedOr(this.#packages().get(name));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The concrete range or version a specifier resolved to AS OF this snapshot.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* The specifier is classified through `@effected/npm`'s
|
|
117
|
+
* `DependencySpecifier.FromString` — never by prefix-sniffing.
|
|
118
|
+
* A `workspace:` specifier resolves to the captured version of `dependency`; a
|
|
119
|
+
* `catalog:` specifier resolves against the captured catalog set. Every other
|
|
120
|
+
* form — a plain range, a dist-tag, a `file:`/git/url specifier, or an
|
|
121
|
+
* unparseable string — is `Option.none()`, because there is no indirection to
|
|
122
|
+
* resolve. Total.
|
|
123
|
+
*
|
|
124
|
+
* @param dependency - The dependency's package name (what `workspace:` /
|
|
125
|
+
* `catalog:` resolve for).
|
|
126
|
+
* @param specifier - The raw specifier string.
|
|
127
|
+
*/
|
|
128
|
+
resolve(dependency, specifier) {
|
|
129
|
+
const exit = Schema.decodeUnknownExit(DependencySpecifier.FromString)(specifier);
|
|
130
|
+
if (!Exit.isSuccess(exit)) return Option.none();
|
|
131
|
+
const classified = exit.value;
|
|
132
|
+
switch (classified._tag) {
|
|
133
|
+
case "catalog": return this.catalogs.rangeOf(dependency, classified.name);
|
|
134
|
+
case "workspace": return Option.fromUndefinedOr(this.#versions().get(dependency));
|
|
135
|
+
default: return Option.none();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A `CatalogResolver` layer implementing `@effected/npm`'s contract against
|
|
140
|
+
* THIS snapshot's catalog set — so code written to the contract resolves
|
|
141
|
+
* `catalog:` specifiers as of this ref. Built once per instance and cached, so
|
|
142
|
+
* it memoizes by reference.
|
|
143
|
+
*/
|
|
144
|
+
get catalogResolver() {
|
|
145
|
+
if (this.#catalogResolver === void 0) this.#catalogResolver = Layer.succeed(CatalogResolver, { rangeOf: (packageName, catalog) => Effect.succeed(this.catalogs.rangeOf(packageName, catalog)) });
|
|
146
|
+
return this.#catalogResolver;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A `WorkspaceResolver` layer implementing `@effected/npm`'s contract against
|
|
150
|
+
* THIS snapshot's captured versions — so code written to the contract resolves
|
|
151
|
+
* `workspace:` specifiers as of this ref. Built once per instance and cached.
|
|
152
|
+
*/
|
|
153
|
+
get workspaceResolver() {
|
|
154
|
+
if (this.#workspaceResolver === void 0) this.#workspaceResolver = Layer.succeed(WorkspaceResolver, { versionOf: (packageName) => Effect.succeed(Option.fromUndefinedOr(this.#versions().get(packageName))) });
|
|
155
|
+
return this.#workspaceResolver;
|
|
156
|
+
}
|
|
157
|
+
/** Both snapshot-scoped resolver layers merged. Built once per instance and cached. */
|
|
158
|
+
get resolvers() {
|
|
159
|
+
if (this.#resolvers === void 0) this.#resolvers = Layer.mergeAll(this.catalogResolver, this.workspaceResolver);
|
|
160
|
+
return this.#resolvers;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
export { PackageStateSnapshot, WorkspaceStateSnapshot };
|
package/Workspaces.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { WorkspaceRoot } from "./WorkspaceRoot.js";
|
|
2
|
+
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
3
|
+
import { ChangeDetector } from "./ChangeDetector.js";
|
|
4
|
+
import { PackageManagerDetector } from "./PackageManagerName.js";
|
|
5
|
+
import { LockfileReader } from "./LockfileReader.js";
|
|
6
|
+
import { PublishabilityDetector } from "./Publishability.js";
|
|
7
|
+
import { WorkspaceCatalogs } from "./WorkspaceCatalogs.js";
|
|
8
|
+
import { WorkspaceSnapshots } from "./WorkspaceSnapshots.js";
|
|
9
|
+
import { Layer } from "effect";
|
|
10
|
+
import { Git } from "@effected/git";
|
|
11
|
+
|
|
12
|
+
//#region src/Workspaces.ts
|
|
13
|
+
/**
|
|
14
|
+
* Every service that needs only a filesystem: root, package-manager detection,
|
|
15
|
+
* discovery, lockfile reading, catalogs and publishability.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* Requires core `FileSystem` and `Path`, which the consumer provides at the
|
|
19
|
+
* edge (`@effect/platform-node`, `@effect/platform-bun`, or a test's
|
|
20
|
+
* `FileSystem.layerNoop`).
|
|
21
|
+
*
|
|
22
|
+
* **Bind the result to a `const`.** This is a parameterized factory and layers
|
|
23
|
+
* memoize by reference, so calling it twice builds everything twice.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
28
|
+
* import { Layer } from "effect";
|
|
29
|
+
*
|
|
30
|
+
* const WorkspacesLayer = Workspaces.layer();
|
|
31
|
+
* const AppLayer = Layer.provide(WorkspacesLayer, PlatformLayer);
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @public
|
|
35
|
+
*/
|
|
36
|
+
const compose = (options, catalogsFactory) => {
|
|
37
|
+
const roots = WorkspaceRoot.layer;
|
|
38
|
+
const detector = PackageManagerDetector.layer;
|
|
39
|
+
const discovery = WorkspaceDiscovery.layer(options).pipe(Layer.provide(roots));
|
|
40
|
+
const lockfiles = LockfileReader.layer(options).pipe(Layer.provide(roots), Layer.provide(detector), Layer.provide(discovery));
|
|
41
|
+
const catalogs = catalogsFactory(options).pipe(Layer.provide(roots), Layer.provide(lockfiles));
|
|
42
|
+
return Layer.mergeAll(roots, detector, discovery, lockfiles, catalogs, PublishabilityDetector.layer);
|
|
43
|
+
};
|
|
44
|
+
const layer = (options) => compose(options, WorkspaceCatalogs.layer);
|
|
45
|
+
/**
|
|
46
|
+
* The git-free composite plus {@link ChangeDetector} and
|
|
47
|
+
* {@link WorkspaceSnapshots}, over `@effected/git`'s `Git` service.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* The extra requirement is core's `ChildProcessSpawner` (behind `Git`), which
|
|
51
|
+
* is why it is a separate layer rather than a flag: a consumer that never
|
|
52
|
+
* detects changes or reads at a ref should not have to be able to spawn a
|
|
53
|
+
* subprocess. The consumer provides `ChildProcessSpawner` once at the edge
|
|
54
|
+
* (`@effect/platform-node`'s `NodeServices.layer`); a test provides
|
|
55
|
+
* `Layer.succeed(Git, …)` and needs no repository on disk.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
const layerWithGit = (options) => {
|
|
60
|
+
const core = layer(options);
|
|
61
|
+
const git = Git.layer;
|
|
62
|
+
return Layer.mergeAll(core, git, ChangeDetector.layer.pipe(Layer.provide(git), Layer.provide(core)), WorkspaceSnapshots.layer(options).pipe(Layer.provide(git), Layer.provide(core)));
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The two `@effected/npm` resolver contracts, implemented for real.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* Provide this alongside `@effected/package-json`'s `Package.resolve` and a
|
|
69
|
+
* manifest's `catalog:` and `workspace:` specifiers resolve against the actual
|
|
70
|
+
* workspace instead of the no-op layers' `Option.none()`.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { Package } from "@effected/package-json";
|
|
75
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
76
|
+
* import { Layer } from "effect";
|
|
77
|
+
*
|
|
78
|
+
* const WorkspacesLayer = Workspaces.layer();
|
|
79
|
+
* const Resolvers = Workspaces.resolvers.pipe(Layer.provide(WorkspacesLayer));
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
const resolvers = Layer.mergeAll(WorkspaceCatalogs.catalogResolver, WorkspaceDiscovery.workspaceResolver);
|
|
85
|
+
/**
|
|
86
|
+
* The git-free composite, but with catalog assembly that **replays config
|
|
87
|
+
* dependency `pnpmfile.cjs` hooks** — {@link WorkspaceCatalogs.layerWithConfigDependencies}
|
|
88
|
+
* in place of the default no-op catalogs layer.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* Identical requirement set to {@link Workspaces.layer}; the only difference is
|
|
92
|
+
* that config-dependency code is executed in process. Opt in deliberately — the
|
|
93
|
+
* default {@link Workspaces.layer} never executes config-dependency code.
|
|
94
|
+
*
|
|
95
|
+
* **Bind the result to a `const`.**
|
|
96
|
+
*
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
const layerWithConfigDependencies = (options) => compose(options, WorkspaceCatalogs.layerWithConfigDependencies);
|
|
100
|
+
/**
|
|
101
|
+
* The composite layers.
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
const Workspaces = {
|
|
106
|
+
layer,
|
|
107
|
+
layerWithConfigDependencies,
|
|
108
|
+
layerWithGit,
|
|
109
|
+
resolvers
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
export { Workspaces };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Traversal, badMaxDepthMessage, isPruned, isValidMaxDepth, joinRelative } from "./internal/traverse.js";
|
|
2
|
+
import { manifestPatternsOf, pnpmPatternsOf } from "./internal/patterns.js";
|
|
3
|
+
import { PublishConfig, WorkspacePackage } from "./WorkspacePackage.js";
|
|
4
|
+
import { Effect, Exit, Schema } from "effect";
|
|
5
|
+
import { GlobSet } from "@effected/glob";
|
|
6
|
+
import { Yaml } from "@effected/yaml";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
//#region src/WorkspacesSync.ts
|
|
11
|
+
/**
|
|
12
|
+
* Read and JSON-parse a file into a plain object, or `undefined`. Never throws.
|
|
13
|
+
*
|
|
14
|
+
* The non-object check is load-bearing, not defensive noise. `JSON.parse`
|
|
15
|
+
* returns `undefined` for *nothing* — a `package.json` whose entire content is
|
|
16
|
+
* `null`, `42` or `"x"` parses successfully to that value, so a caller guarding
|
|
17
|
+
* only on `undefined` sails straight into `raw.name` and throws a `TypeError`.
|
|
18
|
+
* These functions are documented as total and are reached from a Vitest config,
|
|
19
|
+
* which has nowhere to put a crash: malformed input must be *skipped*, never a
|
|
20
|
+
* defect.
|
|
21
|
+
*/
|
|
22
|
+
const readJson = (file) => {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
30
|
+
};
|
|
31
|
+
/** Whether `dir` is a directory. Never throws. */
|
|
32
|
+
const isDirectory = (dir) => {
|
|
33
|
+
try {
|
|
34
|
+
return statSync(dir).isDirectory();
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** Whether `dir` holds a `package.json`. */
|
|
40
|
+
const isPackage = (dir) => existsSync(join(dir, "package.json"));
|
|
41
|
+
/**
|
|
42
|
+
* The nearest workspace root at or above `cwd`, or `null`.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* **Synchronous and Node-only.** The Effect surface is `WorkspaceRoot`; reach
|
|
46
|
+
* for this one only where you genuinely cannot run an Effect — a Vitest config
|
|
47
|
+
* being the motivating case.
|
|
48
|
+
*
|
|
49
|
+
* Markers match the async service exactly: a `pnpm-workspace.yaml`, or a
|
|
50
|
+
* `package.json` carrying a `workspaces` field.
|
|
51
|
+
*
|
|
52
|
+
* @param cwd - Where to start the ascent. Defaults to `process.cwd()`.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { findWorkspaceRootSync } from "@effected/workspaces";
|
|
57
|
+
*
|
|
58
|
+
* const root = findWorkspaceRootSync();
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
const findWorkspaceRootSync = (cwd) => {
|
|
64
|
+
let current = resolve(cwd ?? process.cwd());
|
|
65
|
+
for (let depth = 0; depth < 32 * 8; depth++) {
|
|
66
|
+
if (existsSync(join(current, "pnpm-workspace.yaml"))) return current;
|
|
67
|
+
const manifest = readJson(join(current, "package.json"));
|
|
68
|
+
if (manifest?.workspaces !== void 0 && manifest.workspaces !== null) return current;
|
|
69
|
+
const parent = dirname(current);
|
|
70
|
+
if (parent === current) return null;
|
|
71
|
+
current = parent;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
/** The workspace `packages:` patterns for `root`, matching `internal/patterns.ts`'s precedence. */
|
|
76
|
+
const readPatternsSync = (root) => {
|
|
77
|
+
const workspaceYaml = join(root, "pnpm-workspace.yaml");
|
|
78
|
+
if (existsSync(workspaceYaml)) {
|
|
79
|
+
let text = "";
|
|
80
|
+
try {
|
|
81
|
+
text = readFileSync(workspaceYaml, "utf8");
|
|
82
|
+
} catch {
|
|
83
|
+
text = "";
|
|
84
|
+
}
|
|
85
|
+
const exit = Effect.runSyncExit(Yaml.parse(text));
|
|
86
|
+
if (Exit.isSuccess(exit)) {
|
|
87
|
+
const patterns = pnpmPatternsOf(exit.value);
|
|
88
|
+
if (patterns.length > 0) return patterns;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return manifestPatternsOf(readJson(join(root, "package.json")) ?? {});
|
|
92
|
+
};
|
|
93
|
+
/** 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);
|
|
97
|
+
if (raw === void 0) return null;
|
|
98
|
+
const name = raw.name;
|
|
99
|
+
const version = raw.version;
|
|
100
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
101
|
+
if (typeof version !== "string" || version.length === 0) return null;
|
|
102
|
+
const stringRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string") ? value : void 0;
|
|
103
|
+
const publishConfig = raw.publishConfig;
|
|
104
|
+
const config = publishConfig !== null && typeof publishConfig === "object" ? Effect.runSyncExit(Schema.decodeUnknownEffect(PublishConfig)(publishConfig)) : void 0;
|
|
105
|
+
return WorkspacePackage.make({
|
|
106
|
+
name,
|
|
107
|
+
version,
|
|
108
|
+
path: directory,
|
|
109
|
+
packageJsonPath,
|
|
110
|
+
relativePath,
|
|
111
|
+
private: raw.private === true,
|
|
112
|
+
dependencies: stringRecord(raw.dependencies) ?? {},
|
|
113
|
+
devDependencies: stringRecord(raw.devDependencies) ?? {},
|
|
114
|
+
peerDependencies: stringRecord(raw.peerDependencies) ?? {},
|
|
115
|
+
optionalDependencies: stringRecord(raw.optionalDependencies) ?? {},
|
|
116
|
+
...config !== void 0 && Exit.isSuccess(config) ? { publishConfig: config.value } : {}
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Every workspace package under `root`, root package first.
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* **Synchronous and Node-only.** The Effect surface is `WorkspaceDiscovery`.
|
|
124
|
+
*
|
|
125
|
+
* Pattern semantics are not merely "shared" with the Effect enumerator — both
|
|
126
|
+
* drive the **same traversal state machine** (`internal/traverse.ts`), so the
|
|
127
|
+
* dequeue order, the depth rule, the visit budget and the prune list cannot
|
|
128
|
+
* drift apart. A `packages/**` finds exactly the same packages here as there,
|
|
129
|
+
* including at the depth boundary.
|
|
130
|
+
*
|
|
131
|
+
* The one deliberate difference is what happens at a bound: the Effect surface
|
|
132
|
+
* fails typed (`depthExceeded` / `budgetExceeded`), while this one is **total**
|
|
133
|
+
* and truncates. An unenumerable pattern or an unreadable manifest is skipped,
|
|
134
|
+
* not raised — a function with no error channel should not pretend to have one,
|
|
135
|
+
* and a Vitest config has nowhere to put a failure. Totality covers *data*, not
|
|
136
|
+
* caller mistakes: a `maxDepth` that is not a positive integer throws, matching
|
|
137
|
+
* the enumerator's defect.
|
|
138
|
+
*
|
|
139
|
+
* @param root - The workspace root, from {@link findWorkspaceRootSync}.
|
|
140
|
+
* @param options - Traversal bounds; see {@link GetWorkspacePackagesSyncOptions}.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
|
|
145
|
+
*
|
|
146
|
+
* const root = findWorkspaceRootSync();
|
|
147
|
+
* const packages = root === null ? [] : getWorkspacePackagesSync(root);
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* @public
|
|
151
|
+
*/
|
|
152
|
+
const getWorkspacePackagesSync = (root, options) => {
|
|
153
|
+
const maxDepth = options?.maxDepth ?? 32;
|
|
154
|
+
if (!isValidMaxDepth(maxDepth)) throw new RangeError(`getWorkspacePackagesSync: ${badMaxDepthMessage(maxDepth)}`);
|
|
155
|
+
const patterns = readPatternsSync(root);
|
|
156
|
+
const compiled = Effect.runSyncExit(GlobSet.compile(patterns));
|
|
157
|
+
if (Exit.isFailure(compiled)) return [];
|
|
158
|
+
const globs = compiled.value;
|
|
159
|
+
const included = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const literal of globs.literals) {
|
|
161
|
+
const absolute = join(root, literal);
|
|
162
|
+
if (isPackage(absolute)) included.set(literal, absolute);
|
|
163
|
+
}
|
|
164
|
+
for (const wildcard of globs.wildcards) {
|
|
165
|
+
const base = wildcard.enumerationPrefix.replace(/\/$/, "");
|
|
166
|
+
const absoluteBase = join(root, base);
|
|
167
|
+
if (!isDirectory(absoluteBase)) continue;
|
|
168
|
+
const traversal = new Traversal(base, absoluteBase, maxDepth);
|
|
169
|
+
let stopped = false;
|
|
170
|
+
for (let current = traversal.next(); current !== void 0 && !stopped; current = traversal.next()) {
|
|
171
|
+
if (traversal.charge() !== void 0) break;
|
|
172
|
+
let entries = [];
|
|
173
|
+
try {
|
|
174
|
+
entries = readdirSync(current.absolute);
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (isPruned(entry)) continue;
|
|
180
|
+
const relative = joinRelative(current.relative, entry);
|
|
181
|
+
const absolute = join(current.absolute, entry);
|
|
182
|
+
if (!isDirectory(absolute)) continue;
|
|
183
|
+
if (wildcard.crossesSegments && !traversal.admits(current)) {
|
|
184
|
+
stopped = true;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (wildcard.matches(relative) && isPackage(absolute)) included.set(relative, absolute);
|
|
188
|
+
if (!wildcard.crossesSegments) continue;
|
|
189
|
+
traversal.push(current, relative, absolute);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const members = [];
|
|
194
|
+
for (const [relativePath, absolute] of [...included.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
195
|
+
if (relativePath === "." || absolute === root) continue;
|
|
196
|
+
if (globs.excludes.some((exclude) => exclude.matches(relativePath))) continue;
|
|
197
|
+
const pkg = readPackageSync(absolute, relativePath);
|
|
198
|
+
if (pkg !== null) members.push(pkg);
|
|
199
|
+
}
|
|
200
|
+
const rootPackage = readPackageSync(root, ".");
|
|
201
|
+
return rootPackage === null ? members : [rootPackage, ...members];
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
export { findWorkspaceRootSync, getWorkspacePackagesSync };
|