@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/index.js ADDED
@@ -0,0 +1,17 @@
1
+ import { CatalogAssemblyError } from "./CatalogAssemblyError.js";
2
+ import { PublishConfig, WorkspaceManifestError, WorkspacePackage } from "./WorkspacePackage.js";
3
+ import { WORKSPACE_MARKERS, WorkspaceRoot, WorkspaceRootNotFoundError } from "./WorkspaceRoot.js";
4
+ import { PackageNotFoundError, WorkspaceDiscovery, WorkspaceDiscoveryError, WorkspaceInfo, WorkspacePatternError } from "./WorkspaceDiscovery.js";
5
+ import { CyclicDependencyError, DependencyGraph } from "./DependencyGraph.js";
6
+ import { ChangeDetectionError, ChangeDetectionOptions, ChangeDetector } from "./ChangeDetector.js";
7
+ import { ConfigDependencyHooks } from "./ConfigDependencyHooks.js";
8
+ import { DetectedPackageManager, PackageManagerDetectionError, PackageManagerDetector, PackageManagerName } from "./PackageManagerName.js";
9
+ import { LockfileReadError, LockfileReader } from "./LockfileReader.js";
10
+ import { PublishTarget, PublishabilityDetector } from "./Publishability.js";
11
+ import { CatalogSet, WorkspaceCatalogs } from "./WorkspaceCatalogs.js";
12
+ import { PackageStateSnapshot, WorkspaceStateSnapshot } from "./WorkspaceStateSnapshot.js";
13
+ import { WorkspaceSnapshots } from "./WorkspaceSnapshots.js";
14
+ import { Workspaces } from "./Workspaces.js";
15
+ import { findWorkspaceRootSync, getWorkspacePackagesSync } from "./WorkspacesSync.js";
16
+
17
+ export { CatalogAssemblyError, 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 };
@@ -0,0 +1,70 @@
1
+ import { getCatalogsFromWorkspaceManifest, mergeCatalogs } from "@pnpm/catalogs.config";
2
+ import { parseCatalogProtocol } from "@pnpm/catalogs.protocol-parser";
3
+ import { matchCatalogResolveResult, resolveFromCatalog } from "@pnpm/catalogs.resolver";
4
+
5
+ //#region src/internal/catalogs.ts
6
+ /** Project a pnpm-workspace manifest's `catalog` / `catalogs` fields into a `Catalogs` map. */
7
+ const inlineCatalogs = (manifest) => {
8
+ if (manifest.catalog === void 0 && manifest.catalogs === void 0) return {};
9
+ try {
10
+ return getCatalogsFromWorkspaceManifest({
11
+ catalog: manifest.catalog,
12
+ catalogs: manifest.catalogs
13
+ });
14
+ } catch {
15
+ return {};
16
+ }
17
+ };
18
+ /** Merge catalog sources; later sources win per dependency within a catalog. */
19
+ const merge = (...sources) => mergeCatalogs(...sources);
20
+ /** Whether `specifier` is a `catalog:` protocol reference, and which catalog it names. */
21
+ const catalogNameOf = (specifier) => parseCatalogProtocol(specifier);
22
+ /** Normalize the arbitrary shape of a catalog map into `CatalogEntries`, dropping anything unusable. */
23
+ const normalize = (raw) => {
24
+ if (raw === null || typeof raw !== "object") return {};
25
+ const entries = {};
26
+ for (const [catalogName, catalog] of Object.entries(raw)) {
27
+ if (catalog === null || typeof catalog !== "object") continue;
28
+ const clean = {};
29
+ for (const [dependency, value] of Object.entries(catalog)) if (typeof value === "string") define(clean, dependency, value);
30
+ else if (value !== null && typeof value === "object" && "specifier" in value) {
31
+ const specifier = value.specifier;
32
+ if (typeof specifier === "string") define(clean, dependency, specifier);
33
+ }
34
+ define(entries, catalogName, clean);
35
+ }
36
+ return entries;
37
+ };
38
+ const define = (target, key, value) => {
39
+ if (key === "__proto__") Object.defineProperty(target, key, {
40
+ value,
41
+ writable: true,
42
+ enumerable: true,
43
+ configurable: true
44
+ });
45
+ else target[key] = value;
46
+ };
47
+ /**
48
+ * Resolve one `catalog:` specifier against an assembled catalog set.
49
+ *
50
+ * Returns the range on a hit, `undefined` on a miss (the specifier names no
51
+ * catalog entry — an ordinary `Option.none()` to the caller), and a
52
+ * `CatalogMisconfiguration` when pnpm reports the catalog itself is malformed.
53
+ */
54
+ const rangeOf = (catalogs, dependency, specifier) => {
55
+ if (catalogNameOf(specifier) === null) return void 0;
56
+ return matchCatalogResolveResult(resolveFromCatalog(catalogs, {
57
+ alias: dependency,
58
+ bareSpecifier: specifier
59
+ }), {
60
+ found: (hit) => hit.resolution.specifier,
61
+ misconfiguration: (bad) => ({
62
+ catalogName: bad.catalogName,
63
+ detail: bad.error.message
64
+ }),
65
+ unused: () => void 0
66
+ });
67
+ };
68
+
69
+ //#endregion
70
+ export { catalogNameOf, inlineCatalogs, merge, normalize, rangeOf };
@@ -0,0 +1,75 @@
1
+ import { Traversal, badMaxDepthMessage, isPruned, isValidMaxDepth, joinRelative } from "./traverse.js";
2
+ import { Effect, FileSystem, Path } from "effect";
3
+
4
+ //#region src/internal/enumerate.ts
5
+ /** Strip a trailing slash from `GlobPattern.enumerationPrefix` to get a relative directory. */
6
+ const baseOf = (pattern) => pattern.enumerationPrefix.replace(/\/$/, "");
7
+ /**
8
+ * Enumerate the workspace directories a compiled `packages:` set selects.
9
+ *
10
+ * Every returned directory holds a `package.json`, matches at least one include
11
+ * (literal or wildcard), and is rejected by no exclude. Results are sorted by
12
+ * relative path.
13
+ */
14
+ const enumerate = (root, globs, options) => Effect.gen(function* () {
15
+ const fs = yield* FileSystem.FileSystem;
16
+ const path = yield* Path.Path;
17
+ const maxDepth = options?.maxDepth ?? 32;
18
+ if (!isValidMaxDepth(maxDepth)) return yield* Effect.die(/* @__PURE__ */ new Error(`enumerate: ${badMaxDepthMessage(maxDepth)}`));
19
+ const isPackage = (absolute) => fs.exists(path.join(absolute, "package.json")).pipe(Effect.orElseSucceed(() => false));
20
+ const isDirectory = (absolute) => fs.stat(absolute).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false));
21
+ const included = /* @__PURE__ */ new Map();
22
+ /** A shared-traversal stop, materialized as this module's failure record. */
23
+ const failureOf = (stop, pattern) => ({
24
+ kind: stop.kind,
25
+ pattern,
26
+ detail: stop.detail
27
+ });
28
+ for (const literal of globs.literals) {
29
+ const absolute = path.join(root, literal);
30
+ if (yield* isPackage(absolute)) included.set(literal, absolute);
31
+ }
32
+ for (const wildcard of globs.wildcards) {
33
+ const base = baseOf(wildcard);
34
+ const absoluteBase = path.join(root, base);
35
+ if (!(yield* isDirectory(absoluteBase))) return yield* Effect.fail({
36
+ kind: "missingBaseDir",
37
+ pattern: wildcard.source,
38
+ detail: base === "" ? root : base
39
+ });
40
+ const traversal = new Traversal(base, absoluteBase, maxDepth);
41
+ for (let current = traversal.next(); current !== void 0; current = traversal.next()) {
42
+ const spent = traversal.charge();
43
+ if (spent !== void 0) return yield* Effect.fail(failureOf(spent, wildcard.source));
44
+ const frame = current;
45
+ const entries = yield* fs.readDirectory(frame.absolute).pipe(Effect.catch((error) => error.reason._tag === "NotFound" ? Effect.succeed([]) : Effect.fail({
46
+ kind: "unreadableDirectory",
47
+ pattern: wildcard.source,
48
+ detail: frame.relative === "" ? root : frame.relative
49
+ })));
50
+ for (const entry of entries) {
51
+ if (isPruned(entry)) continue;
52
+ const relative = joinRelative(frame.relative, entry);
53
+ const absolute = path.join(frame.absolute, entry);
54
+ if (!(yield* isDirectory(absolute))) continue;
55
+ if (wildcard.crossesSegments && !traversal.admits(frame)) return yield* Effect.fail(failureOf(traversal.depthStop(), wildcard.source));
56
+ if (wildcard.matches(relative) && (yield* isPackage(absolute))) included.set(relative, absolute);
57
+ if (!wildcard.crossesSegments) continue;
58
+ traversal.push(frame, relative, absolute);
59
+ }
60
+ }
61
+ }
62
+ const results = [];
63
+ for (const [relativePath, absolute] of included) {
64
+ if (globs.excludes.some((exclude) => exclude.matches(relativePath))) continue;
65
+ results.push({
66
+ relativePath,
67
+ path: absolute
68
+ });
69
+ }
70
+ results.sort((a, b) => a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0);
71
+ return results;
72
+ });
73
+
74
+ //#endregion
75
+ export { enumerate };
@@ -0,0 +1,20 @@
1
+ //#region src/internal/limits.ts
2
+ /**
3
+ * Hard ceiling on directories the enumerator will visit for one pattern set.
4
+ * Guards the pathological case a depth cap alone does not: a wide, shallow
5
+ * tree. Exceeding it fails typed rather than hanging.
6
+ *
7
+ * @internal
8
+ */
9
+ const MAX_ENUMERATION_ENTRIES = 1e5;
10
+ /**
11
+ * Directory names never descended into. pnpm, npm, yarn and bun all ignore
12
+ * `node_modules` when expanding workspace globs; without the prune a
13
+ * `packages/**` in an installed repo would walk the entire store.
14
+ *
15
+ * @internal
16
+ */
17
+ const PRUNED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
18
+
19
+ //#endregion
20
+ export { MAX_ENUMERATION_ENTRIES, PRUNED_DIRECTORIES };
@@ -0,0 +1,62 @@
1
+ import { Effect, FileSystem, Path } from "effect";
2
+ import { Yaml } from "@effected/yaml";
3
+
4
+ //#region src/internal/patterns.ts
5
+ const stringsOf = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : void 0;
6
+ /** The `packages:` list of a `pnpm-workspace.yaml` document. Total on a parsed document. */
7
+ const pnpmPatternsOf = (document) => {
8
+ if (document === null || typeof document !== "object") return [];
9
+ return stringsOf(document.packages) ?? [];
10
+ };
11
+ /** The `workspaces` field of a root package.json, in either supported shape. */
12
+ const manifestPatternsOf = (manifest) => {
13
+ if (manifest === null || typeof manifest !== "object") return [];
14
+ const workspaces = manifest.workspaces;
15
+ const direct = stringsOf(workspaces);
16
+ if (direct !== void 0) return direct;
17
+ if (workspaces !== null && typeof workspaces === "object" && "packages" in workspaces) return stringsOf(workspaces.packages) ?? [];
18
+ return [];
19
+ };
20
+ /**
21
+ * The workspace `packages:` patterns for `root`: `pnpm-workspace.yaml` first,
22
+ * then the root package.json `workspaces` field. An absent config is a
23
+ * standalone package, not an error — it yields an empty list.
24
+ */
25
+ const readPatterns = (root) => Effect.gen(function* () {
26
+ const fs = yield* FileSystem.FileSystem;
27
+ const path = yield* Path.Path;
28
+ const pnpmWorkspacePath = path.join(root, "pnpm-workspace.yaml");
29
+ if (yield* fs.exists(pnpmWorkspacePath).pipe(Effect.orElseSucceed(() => false))) {
30
+ const content = yield* fs.readFileString(pnpmWorkspacePath).pipe(Effect.mapError((cause) => ({
31
+ path: pnpmWorkspacePath,
32
+ kind: "read",
33
+ cause
34
+ })));
35
+ const document = yield* Yaml.parse(content).pipe(Effect.mapError((cause) => ({
36
+ path: pnpmWorkspacePath,
37
+ kind: "invalidYaml",
38
+ cause
39
+ })));
40
+ const patterns = pnpmPatternsOf(document);
41
+ if (patterns.length > 0) return patterns;
42
+ }
43
+ const manifestPath = path.join(root, "package.json");
44
+ if (!(yield* fs.exists(manifestPath).pipe(Effect.orElseSucceed(() => false)))) return [];
45
+ const content = yield* fs.readFileString(manifestPath).pipe(Effect.mapError((cause) => ({
46
+ path: manifestPath,
47
+ kind: "read",
48
+ cause
49
+ })));
50
+ const manifest = yield* Effect.try({
51
+ try: () => JSON.parse(content),
52
+ catch: (cause) => ({
53
+ path: manifestPath,
54
+ kind: "invalidJson",
55
+ cause
56
+ })
57
+ });
58
+ return manifestPatternsOf(manifest);
59
+ });
60
+
61
+ //#endregion
62
+ export { manifestPatternsOf, pnpmPatternsOf, readPatterns };
@@ -0,0 +1,94 @@
1
+ import { MAX_ENUMERATION_ENTRIES, PRUNED_DIRECTORIES } from "./limits.js";
2
+
3
+ //#region src/internal/traverse.ts
4
+ /** Directory names never descended into. */
5
+ const isPruned = (entry) => PRUNED_DIRECTORIES.has(entry);
6
+ /** Join root-relative POSIX segments; `""` is the root itself. */
7
+ const joinRelative = (base, entry) => base === "" ? entry : `${base}/${entry}`;
8
+ /**
9
+ * Whether `maxDepth` is a usable bound.
10
+ *
11
+ * `NaN < 1` is `false` and so is `2.5 < 1` — a bare relational guard admits both,
12
+ * and a `NaN` bound then runs the loop zero times and returns an empty result
13
+ * indistinguishable from a legitimate one. Integrality first. A bad bound is a
14
+ * PROGRAMMER error, not a data condition, so both entry points treat it as a
15
+ * defect (an `Effect.die` / a thrown `RangeError`) rather than a typed failure.
16
+ */
17
+ const isValidMaxDepth = (maxDepth) => Number.isInteger(maxDepth) && maxDepth >= 1;
18
+ /** The message both entry points use when `maxDepth` is not a positive integer. */
19
+ const badMaxDepthMessage = (maxDepth) => `maxDepth must be a positive integer, received ${String(maxDepth)}`;
20
+ /**
21
+ * The shared worklist for one wildcard's descent.
22
+ *
23
+ * A worklist, not a recursion: it cannot overflow the stack, so there is no cap
24
+ * to get wrong.
25
+ */
26
+ var Traversal = class {
27
+ #frames = [];
28
+ #head = 0;
29
+ #visited = 0;
30
+ #base;
31
+ #maxDepth;
32
+ #maxEntries;
33
+ constructor(base, absoluteBase, maxDepth, maxEntries = MAX_ENUMERATION_ENTRIES) {
34
+ this.#base = base;
35
+ this.#maxDepth = maxDepth;
36
+ this.#maxEntries = maxEntries;
37
+ this.#frames.push({
38
+ relative: base,
39
+ absolute: absoluteBase,
40
+ depth: 0
41
+ });
42
+ }
43
+ /**
44
+ * The next directory to read, or `undefined` when the worklist is drained.
45
+ *
46
+ * A head index, never `Array.shift()`. `shift()` re-indexes the whole array on
47
+ * every dequeue, so draining a worklist anywhere near `MAX_ENUMERATION_ENTRIES`
48
+ * (100,000) is quadratic — the very budget that bounds the walk would become
49
+ * the slow path.
50
+ */
51
+ next() {
52
+ if (this.#head >= this.#frames.length) return void 0;
53
+ const frame = this.#frames[this.#head];
54
+ this.#head += 1;
55
+ return frame;
56
+ }
57
+ /** Charge one directory read against the visit budget. */
58
+ charge() {
59
+ this.#visited += 1;
60
+ if (this.#visited > this.#maxEntries) return {
61
+ kind: "budgetExceeded",
62
+ detail: `visited more than ${this.#maxEntries} directories`
63
+ };
64
+ }
65
+ /**
66
+ * Whether a child of `parent` lies within the depth cap.
67
+ *
68
+ * Callers must consult this BEFORE accepting a child as a package, not merely
69
+ * before descending into it. The cap bounds what the traversal *enumerates*;
70
+ * a directory beyond it is out of scope entirely, and gating only the descent
71
+ * is what let a package one level past the cap slip into the sync results.
72
+ */
73
+ admits(parent) {
74
+ return parent.depth + 1 <= this.#maxDepth;
75
+ }
76
+ /** The stop a child beyond the depth cap produces. */
77
+ depthStop() {
78
+ return {
79
+ kind: "depthExceeded",
80
+ detail: `descended past ${this.#maxDepth} levels below "${this.#base}"`
81
+ };
82
+ }
83
+ /** Queue a child of `parent` for descent. Only call when {@link Traversal.admits} holds. */
84
+ push(parent, relative, absolute) {
85
+ this.#frames.push({
86
+ relative,
87
+ absolute,
88
+ depth: parent.depth + 1
89
+ });
90
+ }
91
+ };
92
+
93
+ //#endregion
94
+ export { Traversal, badMaxDepthMessage, isPruned, isValidMaxDepth, joinRelative };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@effected/workspaces",
3
+ "version": "0.1.0",
4
+ "private": false,
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
+ "keywords": [
7
+ "monorepo",
8
+ "workspaces",
9
+ "pnpm",
10
+ "npm",
11
+ "yarn",
12
+ "bun",
13
+ "dependency-graph",
14
+ "catalog",
15
+ "effect",
16
+ "effected"
17
+ ],
18
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/workspaces#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/spencerbeggs/effected/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/spencerbeggs/effected.git",
25
+ "directory": "packages/workspaces"
26
+ },
27
+ "license": "MIT",
28
+ "author": {
29
+ "name": "C. Spencer Beggs",
30
+ "email": "spencer@beggs.codes",
31
+ "url": "https://spencerbeg.gs"
32
+ },
33
+ "sideEffects": false,
34
+ "type": "module",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./index.d.ts",
38
+ "import": "./index.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "dependencies": {
43
+ "@effected/git": "0.1.0",
44
+ "@effected/glob": "0.1.0",
45
+ "@effected/lockfiles": "0.1.0",
46
+ "@effected/npm": "0.1.0",
47
+ "@effected/package-json": "0.1.0",
48
+ "@effected/walker": "0.1.0",
49
+ "@effected/yaml": "0.1.0",
50
+ "@pnpm/catalogs.config": "^1100.0.0",
51
+ "@pnpm/catalogs.protocol-parser": "^1100.0.0",
52
+ "@pnpm/catalogs.resolver": "^1100.0.0",
53
+ "@pnpm/catalogs.types": "^1100.0.0"
54
+ },
55
+ "peerDependencies": {
56
+ "effect": "4.0.0-beta.98"
57
+ },
58
+ "engines": {
59
+ "node": ">=24.11.0"
60
+ }
61
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.9"
9
+ }
10
+ ]
11
+ }