@effected/workspaces 0.25.0 → 0.27.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/LayerPolicy.js ADDED
@@ -0,0 +1,133 @@
1
+ import { ALL_DEPENDENCY_FIELDS } from "./internal/dependencyFields.js";
2
+ import { Effect, FileSystem, Result, Schema } from "effect";
3
+ import { GlobSet } from "@effected/glob";
4
+ import { DependencyField } from "@effected/npm";
5
+
6
+ //#region src/LayerPolicy.ts
7
+ const REQUIRED_EDGE = /^\S+ -> \S+$/;
8
+ /** `input` without `keys`, when it is a plain object; anything else passes through for the schema to reject. */
9
+ const withoutKeys = (input, keys) => {
10
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
11
+ return Object.fromEntries(Object.entries(input).filter(([key]) => !keys.includes(key)));
12
+ };
13
+ /**
14
+ * Raised when a layer policy cannot be read, parsed or decoded.
15
+ *
16
+ * @public
17
+ */
18
+ var LayerPolicyError = class extends Schema.TaggedError()("LayerPolicyError", {
19
+ /** `read`: the file could not be read. `json`: it is not JSON. `decode`: it is not a `LayerPolicy`. */
20
+ reason: Schema.Literals([
21
+ "read",
22
+ "json",
23
+ "decode"
24
+ ]),
25
+ /** The file, when the policy came from one. */
26
+ path: Schema.optionalKey(Schema.String),
27
+ /** The originating failure. */
28
+ cause: Schema.Defect()
29
+ }) {
30
+ /** Renders the failure kind and the file into one line. */
31
+ get message() {
32
+ const at = this.path === void 0 ? "" : ` at ${this.path}`;
33
+ switch (this.reason) {
34
+ case "read": return `Could not read the layer policy${at}`;
35
+ case "json": return `The layer policy${at} is not valid JSON`;
36
+ default: return `The layer policy${at} does not match the LayerPolicy shape${this.cause instanceof Error && this.cause.message !== "" ? `: ${this.cause.message}` : ""}`;
37
+ }
38
+ }
39
+ };
40
+ /**
41
+ * A committed dependency-layering policy (`layers.json`).
42
+ *
43
+ * @remarks
44
+ * `layers` is top-down: an edge may only point from a layer to one BELOW it
45
+ * (a higher index), or into `tooling`. `tooling` packages may depend on each
46
+ * other but never reach a layer. `unconstrained` holds globs (through
47
+ * `@effected/glob`) for packages whose own edges are not checked (a test
48
+ * harness, the private root). `fields` narrows which dependency maps count,
49
+ * defaulting to all four. `requiredEdges` (`"a -> b"`) are edges that must
50
+ * exist in a checked field: the non-vacuity guard against a discovery that
51
+ * quietly drops real edges.
52
+ *
53
+ * Every entry matches a package's `name`, never its `relativePath`: `layers`
54
+ * and `tooling` list exact names, and `unconstrained` globs match names. A
55
+ * private root named `my-monorepo` at `relativePath` `"."` is classified as
56
+ * `"my-monorepo"`; an entry of `"."` or `"packages/*"` classifies nothing.
57
+ *
58
+ * Decoding is strict: a key the policy does not model fails `decode`, and the
59
+ * message names every such key. A typo on an optional key would otherwise be
60
+ * silently dropped: `requiredEdge` would remove the non-vacuity guard and
61
+ * leave the report green. `$schema` is always accepted. A file that carries
62
+ * keys of its own, such as systems' `harness`, passes them in `allowKeys`.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import { NodeServices } from "@effect/platform-node";
67
+ * import { LayerPolicy } from "@effected/workspaces/testing";
68
+ * import { Effect } from "effect";
69
+ *
70
+ * const policy = LayerPolicy.load("/repo/layers.json").pipe(Effect.provide(NodeServices.layer));
71
+ * ```
72
+ *
73
+ * @public
74
+ */
75
+ var LayerPolicy = class LayerPolicy extends Schema.Class("LayerPolicy")({
76
+ /** Exact package names (never relative paths) per layer, top layer first. */
77
+ layers: Schema.Array(Schema.Array(Schema.String)),
78
+ /** Exact package names (never relative paths) any layer may depend on that never depend on a layer. */
79
+ tooling: Schema.Array(Schema.String),
80
+ /** Globs matched against package names (never relative paths) for packages whose own edges are not checked. */
81
+ unconstrained: Schema.Array(Schema.String).check(Schema.makeFilter((patterns) => Result.isSuccess(GlobSet.compileResult(patterns)), { title: "compilable glob patterns" })),
82
+ /** The dependency maps to check. Absent means all four. */
83
+ fields: Schema.optionalKey(Schema.Array(DependencyField)),
84
+ /** Edges that must exist, written `"a -> b"`. */
85
+ requiredEdges: Schema.optionalKey(Schema.Array(Schema.String.check(Schema.isPattern(REQUIRED_EDGE))))
86
+ }) {
87
+ /** The dependency maps a check reads: `fields`, or all four. */
88
+ get effectiveFields() {
89
+ return this.fields ?? ALL_DEPENDENCY_FIELDS;
90
+ }
91
+ /**
92
+ * Decode a policy from a parsed JSON value.
93
+ *
94
+ * @remarks
95
+ * Every key the policy does not model fails, except `$schema` and the
96
+ * keys named in `options.allowKeys`, which are dropped before decoding.
97
+ * `options.path` names the file in the error.
98
+ */
99
+ static decode = Effect.fn("LayerPolicy.decode")((input, options) => {
100
+ const path = options?.path;
101
+ return Schema.decodeUnknownEffect(LayerPolicy)(withoutKeys(input, ["$schema", ...options?.allowKeys ?? []]), {
102
+ onExcessProperty: "error",
103
+ errors: "all"
104
+ }).pipe(Effect.mapError((cause) => new LayerPolicyError({
105
+ reason: "decode",
106
+ cause,
107
+ ...path === void 0 ? {} : { path }
108
+ })));
109
+ });
110
+ /** Read, parse and decode the policy file at `path`; `options.allowKeys` is passed to `decode`. */
111
+ static load = Effect.fn("LayerPolicy.load")(function* (path, options) {
112
+ const text = yield* (yield* FileSystem.FileSystem).readFileString(path).pipe(Effect.mapError((cause) => new LayerPolicyError({
113
+ reason: "read",
114
+ path,
115
+ cause
116
+ })));
117
+ const json = yield* Effect.try({
118
+ try: () => JSON.parse(text),
119
+ catch: (cause) => new LayerPolicyError({
120
+ reason: "json",
121
+ path,
122
+ cause
123
+ })
124
+ });
125
+ return yield* LayerPolicy.decode(json, {
126
+ path,
127
+ allowKeys: options?.allowKeys
128
+ });
129
+ });
130
+ };
131
+
132
+ //#endregion
133
+ export { LayerPolicy, LayerPolicyError };