@effected/workspaces 0.32.1 → 0.33.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/ImportGraph.js +525 -0
- package/README.md +54 -2
- package/SourceBoundary.js +15 -40
- package/internal/importPosition.js +642 -0
- package/internal/sourceText.js +1 -1
- package/internal/sourceWalk.js +58 -0
- package/package.json +12 -12
- package/testing.d.ts +418 -104
- package/testing.js +3 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Effect, FileSystem, Option, Path } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/sourceWalk.ts
|
|
4
|
+
/** The file extensions a walk reads when the caller names none. */
|
|
5
|
+
const DEFAULT_EXTENSIONS = [
|
|
6
|
+
".ts",
|
|
7
|
+
".mts",
|
|
8
|
+
".cts",
|
|
9
|
+
".js",
|
|
10
|
+
".mjs",
|
|
11
|
+
".cjs"
|
|
12
|
+
];
|
|
13
|
+
/** Declaration files: always skipped, whatever extensions the caller names. */
|
|
14
|
+
const DECLARATION = /\.d\.[cm]?ts$/;
|
|
15
|
+
/**
|
|
16
|
+
* Every source file under `root`, in walk order.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Walks with an explicit stack, visiting each real directory once (via
|
|
20
|
+
* `realPath`), so a symlink loop terminates and a linked directory is not
|
|
21
|
+
* read twice. `node_modules` is never entered. A missing root fails; it never
|
|
22
|
+
* walks nothing. A dangling symlink under the root is skipped, having nothing
|
|
23
|
+
* to read; any other entry that cannot be read fails the walk.
|
|
24
|
+
*/
|
|
25
|
+
const walkSources = Effect.fn("walkSources")(function* (options) {
|
|
26
|
+
const fs = yield* FileSystem.FileSystem;
|
|
27
|
+
const path = yield* Path.Path;
|
|
28
|
+
const posix = (relative) => relative.split(path.sep).join("/");
|
|
29
|
+
const found = [];
|
|
30
|
+
const visited = /* @__PURE__ */ new Set();
|
|
31
|
+
const pending = [options.root];
|
|
32
|
+
while (pending.length > 0) {
|
|
33
|
+
const directory = pending.pop();
|
|
34
|
+
if (directory === void 0) break;
|
|
35
|
+
const real = yield* fs.realPath(directory);
|
|
36
|
+
if (visited.has(real)) continue;
|
|
37
|
+
visited.add(real);
|
|
38
|
+
for (const name of yield* fs.readDirectory(directory)) {
|
|
39
|
+
const full = path.join(directory, name);
|
|
40
|
+
const entry = yield* fs.stat(full).pipe(Effect.map(Option.some), Effect.catch((error) => error.reason._tag === "NotFound" ? fs.readLink(full).pipe(Effect.as(Option.none()), Effect.mapError(() => error)) : Effect.fail(error)));
|
|
41
|
+
if (Option.isNone(entry)) continue;
|
|
42
|
+
const info = entry.value;
|
|
43
|
+
if (info.type === "Directory") {
|
|
44
|
+
if (name !== "node_modules") pending.push(full);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (info.type !== "File" || DECLARATION.test(name) || !options.extensions.some((extension) => name.endsWith(extension))) continue;
|
|
48
|
+
found.push({
|
|
49
|
+
file: posix(path.relative(options.root, full)),
|
|
50
|
+
path: full
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return found;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { DECLARATION, DEFAULT_EXTENSIONS, walkSources };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/workspaces",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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": [
|
|
@@ -51,23 +51,23 @@
|
|
|
51
51
|
"./package.json": "./package.json"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@effected/commands": "^0.
|
|
55
|
-
"@effected/git": "^0.
|
|
56
|
-
"@effected/glob": "^0.
|
|
57
|
-
"@effected/jsonc": "^0.
|
|
58
|
-
"@effected/lockfiles": "^0.
|
|
59
|
-
"@effected/npm": "^0.
|
|
60
|
-
"@effected/package-json": "^0.
|
|
61
|
-
"@effected/semver": "^0.
|
|
62
|
-
"@effected/walker": "^0.
|
|
63
|
-
"@effected/yaml": "^0.
|
|
54
|
+
"@effected/commands": "^0.12.0",
|
|
55
|
+
"@effected/git": "^0.21.0",
|
|
56
|
+
"@effected/glob": "^0.11.0",
|
|
57
|
+
"@effected/jsonc": "^0.16.0",
|
|
58
|
+
"@effected/lockfiles": "^0.16.0",
|
|
59
|
+
"@effected/npm": "^0.21.0",
|
|
60
|
+
"@effected/package-json": "^0.21.0",
|
|
61
|
+
"@effected/semver": "^0.12.0",
|
|
62
|
+
"@effected/walker": "^0.16.0",
|
|
63
|
+
"@effected/yaml": "^0.20.0",
|
|
64
64
|
"@pnpm/catalogs.config": "^1100.0.5",
|
|
65
65
|
"@pnpm/catalogs.protocol-parser": "^1100.0.0",
|
|
66
66
|
"@pnpm/catalogs.resolver": "^1100.0.0",
|
|
67
67
|
"@pnpm/catalogs.types": "^1100.0.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"effect": "4.0.
|
|
70
|
+
"effect": "^4.0.0"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=24.11.0"
|
package/testing.d.ts
CHANGED
|
@@ -5,92 +5,6 @@ import { WorkspaceManifest } from "@effected/lockfiles";
|
|
|
5
5
|
import { Package } from "@effected/package-json";
|
|
6
6
|
import { CommandOutput } from "@effected/commands";
|
|
7
7
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
8
|
-
//#region src/LayerPolicy.d.ts
|
|
9
|
-
declare const LayerPolicyError_base: Schema.Class<LayerPolicyError, Schema.TaggedStruct<"LayerPolicyError", {
|
|
10
|
-
/** `read`: the file could not be read. `json`: it is not JSON. `decode`: it is not a `LayerPolicy`. */
|
|
11
|
-
readonly reason: Schema.Literals<readonly ["read", "json", "decode"]>;
|
|
12
|
-
/** The file, when the policy came from one. */
|
|
13
|
-
readonly path: Schema.optionalKey<Schema.String>;
|
|
14
|
-
/** The originating failure. */
|
|
15
|
-
readonly cause: Schema.Defect;
|
|
16
|
-
}>, import("effect/Cause").YieldableError>;
|
|
17
|
-
/**
|
|
18
|
-
* Raised when a layer policy cannot be read, parsed or decoded.
|
|
19
|
-
*
|
|
20
|
-
* @public
|
|
21
|
-
*/
|
|
22
|
-
export declare class LayerPolicyError extends LayerPolicyError_base {
|
|
23
|
-
/** Renders the failure kind and the file into one line. */
|
|
24
|
-
get message(): string;
|
|
25
|
-
}
|
|
26
|
-
declare const LayerPolicy_base: Schema.Class<LayerPolicy, Schema.Struct<{
|
|
27
|
-
/** Exact package names (never relative paths) per layer, top layer first. */
|
|
28
|
-
readonly layers: Schema.$Array<Schema.$Array<Schema.String>>;
|
|
29
|
-
/** Exact package names (never relative paths) any layer may depend on that never depend on a layer. */
|
|
30
|
-
readonly tooling: Schema.$Array<Schema.String>;
|
|
31
|
-
/** Globs matched against package names (never relative paths) for packages whose own edges are not checked. */
|
|
32
|
-
readonly unconstrained: Schema.$Array<Schema.String>;
|
|
33
|
-
/** The dependency maps to check. Absent means all four. */
|
|
34
|
-
readonly fields: Schema.optionalKey<Schema.$Array<Schema.Literals<readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]>>>;
|
|
35
|
-
/** Edges that must exist, written `"a -> b"`. */
|
|
36
|
-
readonly requiredEdges: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
37
|
-
}>, {}>;
|
|
38
|
-
/**
|
|
39
|
-
* A committed dependency-layering policy (`layers.json`).
|
|
40
|
-
*
|
|
41
|
-
* @remarks
|
|
42
|
-
* `layers` is top-down: an edge may only point from a layer to one BELOW it
|
|
43
|
-
* (a higher index), or into `tooling`. `tooling` packages may depend on each
|
|
44
|
-
* other but never reach a layer. `unconstrained` holds globs (through
|
|
45
|
-
* `@effected/glob`) for packages whose own edges are not checked (a test
|
|
46
|
-
* harness, the private root). `fields` narrows which dependency maps count,
|
|
47
|
-
* defaulting to all four. `requiredEdges` (`"a -> b"`) are edges that must
|
|
48
|
-
* exist in a checked field: the non-vacuity guard against a discovery that
|
|
49
|
-
* quietly drops real edges.
|
|
50
|
-
*
|
|
51
|
-
* Every entry matches a package's `name`, never its `relativePath`: `layers`
|
|
52
|
-
* and `tooling` list exact names, and `unconstrained` globs match names. A
|
|
53
|
-
* private root named `my-monorepo` at `relativePath` `"."` is classified as
|
|
54
|
-
* `"my-monorepo"`; an entry of `"."` or `"packages/*"` classifies nothing.
|
|
55
|
-
*
|
|
56
|
-
* Decoding is strict: a key the policy does not model fails `decode`, and the
|
|
57
|
-
* message names every such key. A typo on an optional key would otherwise be
|
|
58
|
-
* silently dropped: `requiredEdge` would remove the non-vacuity guard and
|
|
59
|
-
* leave the report green. `$schema` is always accepted. A file that carries
|
|
60
|
-
* keys of its own, such as a `harness` block, passes them in `allowKeys`.
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```ts
|
|
64
|
-
* import { NodeServices } from "@effect/platform-node";
|
|
65
|
-
* import { LayerPolicy } from "@effected/workspaces/testing";
|
|
66
|
-
* import { Effect } from "effect";
|
|
67
|
-
*
|
|
68
|
-
* const policy = LayerPolicy.load("/repo/layers.json").pipe(Effect.provide(NodeServices.layer));
|
|
69
|
-
* ```
|
|
70
|
-
*
|
|
71
|
-
* @public
|
|
72
|
-
*/
|
|
73
|
-
export declare class LayerPolicy extends LayerPolicy_base {
|
|
74
|
-
/** The dependency maps a check reads: `fields`, or all four. */
|
|
75
|
-
get effectiveFields(): ReadonlyArray<DependencyField>;
|
|
76
|
-
/**
|
|
77
|
-
* Decode a policy from a parsed JSON value.
|
|
78
|
-
*
|
|
79
|
-
* @remarks
|
|
80
|
-
* Every key the policy does not model fails, except `$schema` and the
|
|
81
|
-
* keys named in `options.allowKeys`, which are dropped before decoding.
|
|
82
|
-
* `options.path` names the file in the error.
|
|
83
|
-
*/
|
|
84
|
-
static readonly decode: (input: unknown, options?: {
|
|
85
|
-
readonly path?: string | undefined;
|
|
86
|
-
readonly allowKeys?: ReadonlyArray<string> | undefined;
|
|
87
|
-
} | undefined) => Effect.Effect<LayerPolicy, LayerPolicyError, never>;
|
|
88
|
-
/** Read, parse and decode the policy file at `path`; `options.allowKeys` is passed to `decode`. */
|
|
89
|
-
static readonly load: (path: string, options?: {
|
|
90
|
-
readonly allowKeys?: ReadonlyArray<string> | undefined;
|
|
91
|
-
} | undefined) => Effect.Effect<LayerPolicy, LayerPolicyError, FileSystem.FileSystem>;
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
8
|
//#region src/WorkspacePackage.d.ts
|
|
95
9
|
declare const PublishConfig_base: Schema.Class<PublishConfig, Schema.Struct<{
|
|
96
10
|
/** Scoped-package visibility. Its presence overrides `private`. */
|
|
@@ -328,20 +242,6 @@ export declare class WorkspacePackage extends WorkspacePackage_base {
|
|
|
328
242
|
manifest(): Effect.Effect<Package, WorkspaceManifestError, FileSystem.FileSystem>;
|
|
329
243
|
}
|
|
330
244
|
//#endregion
|
|
331
|
-
//#region src/PackageManagerName.d.ts
|
|
332
|
-
/**
|
|
333
|
-
* The four package managers this package understands.
|
|
334
|
-
*
|
|
335
|
-
* @public
|
|
336
|
-
*/
|
|
337
|
-
export declare const PackageManagerName: Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>;
|
|
338
|
-
/**
|
|
339
|
-
* The decoded type of {@link (PackageManagerName:variable)}: `"npm" | "pnpm" | "yarn" | "bun"`.
|
|
340
|
-
*
|
|
341
|
-
* @public
|
|
342
|
-
*/
|
|
343
|
-
export type PackageManagerName = typeof PackageManagerName.Type;
|
|
344
|
-
//#endregion
|
|
345
245
|
//#region src/WorkspaceRoot.d.ts
|
|
346
246
|
/**
|
|
347
247
|
* Options for {@link WorkspaceRoot}'s `find`.
|
|
@@ -888,6 +788,419 @@ export declare class WorkspaceDiscovery extends WorkspaceDiscovery_base {
|
|
|
888
788
|
static readonly workspaceResolver: Layer.Layer<WorkspaceResolver, never, WorkspaceDiscovery>;
|
|
889
789
|
}
|
|
890
790
|
//#endregion
|
|
791
|
+
//#region src/ImportGraph.d.ts
|
|
792
|
+
declare const NoEntriesError_base: Schema.Class<NoEntriesError, Schema.TaggedStruct<"NoEntriesError", {
|
|
793
|
+
/** The root the walk would have reported against. */
|
|
794
|
+
readonly root: Schema.String;
|
|
795
|
+
}>, import("effect/Cause").YieldableError>;
|
|
796
|
+
/**
|
|
797
|
+
* The entries list named no file to start from, so a walk would prove nothing.
|
|
798
|
+
*
|
|
799
|
+
* @public
|
|
800
|
+
*/
|
|
801
|
+
export declare class NoEntriesError extends NoEntriesError_base {
|
|
802
|
+
/** Renders the root into a one-line message. */
|
|
803
|
+
get message(): string;
|
|
804
|
+
}
|
|
805
|
+
declare const EntryNotFoundError_base: Schema.Class<EntryNotFoundError, Schema.TaggedStruct<"EntryNotFoundError", {
|
|
806
|
+
/** The entry, verbatim. */
|
|
807
|
+
readonly entry: Schema.String;
|
|
808
|
+
/** The root it was resolved against. */
|
|
809
|
+
readonly root: Schema.String;
|
|
810
|
+
}>, import("effect/Cause").YieldableError>;
|
|
811
|
+
/**
|
|
812
|
+
* An entry named a file no resolution candidate could find. The walk fails
|
|
813
|
+
* rather than starting from nothing: a typo'd entry must never read as clean.
|
|
814
|
+
*
|
|
815
|
+
* @public
|
|
816
|
+
*/
|
|
817
|
+
export declare class EntryNotFoundError extends EntryNotFoundError_base {
|
|
818
|
+
/** Renders the entry and root into a one-line message. */
|
|
819
|
+
get message(): string;
|
|
820
|
+
}
|
|
821
|
+
declare const UnresolvedImport_base: Schema.Class<UnresolvedImport, Schema.Struct<{
|
|
822
|
+
/** The importing file, relative to the root, with `/` separators. */
|
|
823
|
+
readonly file: Schema.String;
|
|
824
|
+
/** The specifier, verbatim. */
|
|
825
|
+
readonly specifier: Schema.String;
|
|
826
|
+
}>, {}>;
|
|
827
|
+
/**
|
|
828
|
+
* One relative import no file candidate resolved: a link of the graph the
|
|
829
|
+
* walk could not follow, so whatever it would have reached is unproven.
|
|
830
|
+
*
|
|
831
|
+
* @public
|
|
832
|
+
*/
|
|
833
|
+
export declare class UnresolvedImport extends UnresolvedImport_base {
|
|
834
|
+
/** `file specifier`, the form an assertion message reads best in. */
|
|
835
|
+
get label(): string;
|
|
836
|
+
}
|
|
837
|
+
declare const ReachOffence_base: Schema.Class<ReachOffence, Schema.Struct<{
|
|
838
|
+
/** The importing file, relative to the root, with `/` separators. */
|
|
839
|
+
readonly file: Schema.String;
|
|
840
|
+
/** The 1-based line. */
|
|
841
|
+
readonly line: Schema.Number;
|
|
842
|
+
/** The 1-based column, in UTF-16 code units. */
|
|
843
|
+
readonly column: Schema.Number;
|
|
844
|
+
/** The forbidden specifier, verbatim. */
|
|
845
|
+
readonly specifier: Schema.String;
|
|
846
|
+
/**
|
|
847
|
+
* The files the walk took from an entry to `file`, entry first and `file`
|
|
848
|
+
* last, each relative to the root with `/` separators. Breadth-first, so
|
|
849
|
+
* this is a shortest chain; it is the chain the walk first reached `file`
|
|
850
|
+
* by, and a file is visited once whatever else reaches it.
|
|
851
|
+
*/
|
|
852
|
+
readonly chain: Schema.$Array<Schema.String>;
|
|
853
|
+
}>, {}>;
|
|
854
|
+
/**
|
|
855
|
+
* One forbidden import a walk from the entries reached.
|
|
856
|
+
*
|
|
857
|
+
* @public
|
|
858
|
+
*/
|
|
859
|
+
export declare class ReachOffence extends ReachOffence_base {
|
|
860
|
+
/** `file:line:column specifier via chain`, the form an assertion message reads best in. */
|
|
861
|
+
get label(): string;
|
|
862
|
+
}
|
|
863
|
+
declare const ReachabilityScan_base: Schema.Class<ReachabilityScan, Schema.Struct<{
|
|
864
|
+
/**
|
|
865
|
+
* Every file the walk reached, entries included, relative to the root with
|
|
866
|
+
* `/` separators, sorted. Assert it is non-empty: a walk that reached
|
|
867
|
+
* nothing proves nothing.
|
|
868
|
+
*/
|
|
869
|
+
readonly files: Schema.$Array<Schema.String>;
|
|
870
|
+
/**
|
|
871
|
+
* Every relative specifier no file candidate resolved, one per
|
|
872
|
+
* file-and-specifier pair, sorted by file then specifier. Assert it is
|
|
873
|
+
* empty, or exactly the generated files you expect: an unresolved link is
|
|
874
|
+
* a part of the graph the walk could not prove anything about.
|
|
875
|
+
*/
|
|
876
|
+
readonly unresolved: Schema.$Array<typeof UnresolvedImport>;
|
|
877
|
+
/** Every forbidden import the walk reached, sorted by file, then line, then column. */
|
|
878
|
+
readonly offences: Schema.$Array<typeof ReachOffence>;
|
|
879
|
+
}>, {}>;
|
|
880
|
+
/**
|
|
881
|
+
* What a reachability walk read and found.
|
|
882
|
+
*
|
|
883
|
+
* @public
|
|
884
|
+
*/
|
|
885
|
+
export declare class ReachabilityScan extends ReachabilityScan_base {
|
|
886
|
+
/** One `file:line:column specifier via chain` label per offence: `[]` means the entries reach nothing forbidden. */
|
|
887
|
+
get violations(): ReadonlyArray<string>;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Options for {@link ImportGraph.reachability}.
|
|
891
|
+
*
|
|
892
|
+
* @public
|
|
893
|
+
*/
|
|
894
|
+
interface ReachabilityOptions {
|
|
895
|
+
/** The directory every entry and reported path is relative to. */
|
|
896
|
+
readonly root: string;
|
|
897
|
+
/**
|
|
898
|
+
* The files the walk starts from, relative to `root` with `/` separators,
|
|
899
|
+
* or absolute. Each resolves like a relative import (see `extensions`);
|
|
900
|
+
* one that resolves to no file fails `EntryNotFoundError`, and an empty
|
|
901
|
+
* list fails `NoEntriesError` — the walk never proves nothing silently.
|
|
902
|
+
*/
|
|
903
|
+
readonly entries: ReadonlyArray<string>;
|
|
904
|
+
/**
|
|
905
|
+
* Specifiers no reached file may import, matched like a `SourceBoundary`
|
|
906
|
+
* `forbidImports` entry: equal to the specifier, a subpath of it, or its
|
|
907
|
+
* text before a trailing `*`.
|
|
908
|
+
*/
|
|
909
|
+
readonly forbid: ReadonlyArray<string>;
|
|
910
|
+
/**
|
|
911
|
+
* File extensions a relative specifier resolves through: `./a.js` also
|
|
912
|
+
* tries `./a` with each extension, and `./a/index` with each. Defaults to
|
|
913
|
+
* `.ts`, `.mts`, `.cts`, `.js`, `.mjs` and `.cjs`; declaration files are
|
|
914
|
+
* never a candidate.
|
|
915
|
+
*/
|
|
916
|
+
readonly extensions?: ReadonlyArray<string> | undefined;
|
|
917
|
+
/**
|
|
918
|
+
* The workspace-package extension point: called with every bare specifier
|
|
919
|
+
* a reached file imports, answering the file to continue the walk from
|
|
920
|
+
* (relative to `root` with `/` separators, or absolute, resolved like an
|
|
921
|
+
* entry), or `undefined` to stop at the specifier. A path that resolves to
|
|
922
|
+
* no file is reported in `unresolved` under the specifier it was reached
|
|
923
|
+
* by. The default stops at every bare specifier: the walk follows relative
|
|
924
|
+
* imports only.
|
|
925
|
+
*/
|
|
926
|
+
readonly followPackage?: ((specifier: string) => string | undefined) | undefined;
|
|
927
|
+
}
|
|
928
|
+
declare const UndeclaredImport_base: Schema.Class<UndeclaredImport, Schema.Struct<{
|
|
929
|
+
/** The name of the package that owns the importing file. */
|
|
930
|
+
readonly package: Schema.String;
|
|
931
|
+
/** The importing file, relative to its package, with `/` separators. */
|
|
932
|
+
readonly file: Schema.String;
|
|
933
|
+
/** The 1-based line. */
|
|
934
|
+
readonly line: Schema.Number;
|
|
935
|
+
/** The 1-based column, in UTF-16 code units. */
|
|
936
|
+
readonly column: Schema.Number;
|
|
937
|
+
/** The specifier, verbatim. */
|
|
938
|
+
readonly specifier: Schema.String;
|
|
939
|
+
/** The package name the specifier names: `effect` for `effect/Schema`. */
|
|
940
|
+
readonly dependency: Schema.String;
|
|
941
|
+
}>, {}>;
|
|
942
|
+
/**
|
|
943
|
+
* One runtime import the owning package's manifest does not declare.
|
|
944
|
+
*
|
|
945
|
+
* @public
|
|
946
|
+
*/
|
|
947
|
+
export declare class UndeclaredImport extends UndeclaredImport_base {
|
|
948
|
+
/** `package file:line:column undeclared dependency`, the form an assertion message reads best in. */
|
|
949
|
+
get label(): string;
|
|
950
|
+
}
|
|
951
|
+
declare const UndeclaredScan_base: Schema.Class<UndeclaredScan, Schema.Struct<{
|
|
952
|
+
/** Every package whose manifest the check read, sorted by name. Assert it is non-empty. */
|
|
953
|
+
readonly packages: Schema.$Array<Schema.String>;
|
|
954
|
+
/**
|
|
955
|
+
* Every file the check read, relative to the workspace root with `/`
|
|
956
|
+
* separators, sorted. Each file appears once, listed under its innermost
|
|
957
|
+
* package even when a wider package's glob also walked it. Assert it is
|
|
958
|
+
* non-empty: a check that read no file proves nothing, and a mistyped
|
|
959
|
+
* `include` shows up here.
|
|
960
|
+
*/
|
|
961
|
+
readonly files: Schema.$Array<Schema.String>;
|
|
962
|
+
/** Every undeclared runtime import, sorted by package, then file, then line, then column. */
|
|
963
|
+
readonly undeclared: Schema.$Array<typeof UndeclaredImport>;
|
|
964
|
+
}>, {}>;
|
|
965
|
+
/**
|
|
966
|
+
* What an undeclared-import check read and found.
|
|
967
|
+
*
|
|
968
|
+
* @public
|
|
969
|
+
*/
|
|
970
|
+
export declare class UndeclaredScan extends UndeclaredScan_base {
|
|
971
|
+
/** One `package file:line:column undeclared dependency` label per import: `[]` means every runtime import is declared. */
|
|
972
|
+
get violations(): ReadonlyArray<string>;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Options for {@link ImportGraph.undeclared}.
|
|
976
|
+
*
|
|
977
|
+
* @public
|
|
978
|
+
*/
|
|
979
|
+
interface UndeclaredOptions {
|
|
980
|
+
/**
|
|
981
|
+
* Bare Node built-ins (`"fs"`, `"child_process"`), never flagged. A
|
|
982
|
+
* `node:`-prefixed specifier is always a built-in; no bare list ships
|
|
983
|
+
* here, because one would drift with Node releases — spread
|
|
984
|
+
* `builtinModules` from `node:module` in your test file. A bare built-in
|
|
985
|
+
* not on this list is flagged like any undeclared package.
|
|
986
|
+
*/
|
|
987
|
+
readonly builtins?: ReadonlyArray<string> | undefined;
|
|
988
|
+
/**
|
|
989
|
+
* Globs, matched against each package-relative path with `/` separators,
|
|
990
|
+
* naming the runtime source files to read. Defaults to `["src/**"]`: the
|
|
991
|
+
* check compares against `dependencies`, `peerDependencies` and
|
|
992
|
+
* `optionalDependencies`, so test files importing devDependencies are out
|
|
993
|
+
* of scope by design. Each glob is walked from its static prefix (the text
|
|
994
|
+
* before its first pattern character), so the default walks `src/` and
|
|
995
|
+
* never `dist/` or `node_modules`; a glob with no static prefix walks the
|
|
996
|
+
* package directory. A file under a nested workspace package belongs to
|
|
997
|
+
* that innermost package alone: a root glob starting with `**` walks the
|
|
998
|
+
* whole repository but checks each nested file only against its own
|
|
999
|
+
* package's manifest.
|
|
1000
|
+
*/
|
|
1001
|
+
readonly include?: ReadonlyArray<string> | undefined;
|
|
1002
|
+
/**
|
|
1003
|
+
* File extensions to read. Defaults to `.ts`, `.mts`, `.cts`, `.js`,
|
|
1004
|
+
* `.mjs` and `.cjs`; declaration files are always skipped.
|
|
1005
|
+
*/
|
|
1006
|
+
readonly extensions?: ReadonlyArray<string> | undefined;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Import-graph checks: properties of what the imports of a set of files
|
|
1010
|
+
* reach, which no one-file rule can express.
|
|
1011
|
+
*
|
|
1012
|
+
* @remarks
|
|
1013
|
+
* {@link SourceBoundary} checks one file at a time. Two guards consumers want
|
|
1014
|
+
* are properties of the import graph itself:
|
|
1015
|
+
*
|
|
1016
|
+
* - {@link ImportGraph.reachability} answers "nothing reachable from entry X
|
|
1017
|
+
* imports Y". A directory rule approximates that, but a refactor which adds
|
|
1018
|
+
* a new static path from the root entry into an allowed directory still
|
|
1019
|
+
* passes it; a reachability walk follows the real import edges and reports
|
|
1020
|
+
* the chain that reaches the forbidden specifier.
|
|
1021
|
+
* - {@link ImportGraph.undeclared} answers "every runtime import is
|
|
1022
|
+
* declared". Workspace hoisting resolves an import of a package the
|
|
1023
|
+
* manifest never declared, and only a packed install catches that today —
|
|
1024
|
+
* far too slow for the edit loop. This check is static and fast: each
|
|
1025
|
+
* package's literal specifiers against its own `dependencies`,
|
|
1026
|
+
* `peerDependencies` and `optionalDependencies`.
|
|
1027
|
+
*
|
|
1028
|
+
* Both are static analysis of literal specifiers: static imports,
|
|
1029
|
+
* re-exports, and `import()`/`require()` calls whose argument is one string
|
|
1030
|
+
* literal. There is no dynamic-import guarantee: a computed specifier
|
|
1031
|
+
* (`import("./ui/" + name)`) is invisible to both, and a packed install
|
|
1032
|
+
* (`PackedInstall`) remains the proof of what actually resolves at
|
|
1033
|
+
* runtime. Both reuse the `SourceBoundary` lexer, so a specifier in a
|
|
1034
|
+
* comment, a string, template text or a regex body is never an import, and
|
|
1035
|
+
* both refuse to pass vacuously: every result reports what it read.
|
|
1036
|
+
*
|
|
1037
|
+
* @example
|
|
1038
|
+
* ```ts
|
|
1039
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
1040
|
+
* import { ImportGraph } from "@effected/workspaces/testing";
|
|
1041
|
+
* import { Effect } from "effect";
|
|
1042
|
+
*
|
|
1043
|
+
* // Nothing reachable from the root entry may import ink or react.
|
|
1044
|
+
* const walk = ImportGraph.reachability({
|
|
1045
|
+
* root: "/repo/packages/ui/src",
|
|
1046
|
+
* entries: ["index.ts"],
|
|
1047
|
+
* forbid: ["ink", "react"],
|
|
1048
|
+
* }).pipe(Effect.provide(NodeServices.layer));
|
|
1049
|
+
* ```
|
|
1050
|
+
*
|
|
1051
|
+
* @public
|
|
1052
|
+
*/
|
|
1053
|
+
export declare class ImportGraph {
|
|
1054
|
+
private constructor();
|
|
1055
|
+
/**
|
|
1056
|
+
* Walk the import graph from `entries` through relative imports and report
|
|
1057
|
+
* every forbidden specifier a reached file imports.
|
|
1058
|
+
*
|
|
1059
|
+
* @remarks
|
|
1060
|
+
* The walk is breadth-first, so a violation's chain is a shortest path
|
|
1061
|
+
* from an entry, and each file is visited once through its `realPath`, so
|
|
1062
|
+
* a symlink loop terminates and a diamond reports one chain. The walk is
|
|
1063
|
+
* conservative: it follows every literal specifier, type-only imports
|
|
1064
|
+
* included, so a forbidden import only a type reaches is still reported.
|
|
1065
|
+
* A bare specifier is checked against `forbid` — the same matcher
|
|
1066
|
+
* `SourceBoundary.check` uses — but only walked into when `followPackage`
|
|
1067
|
+
* names a file for it. A relative specifier that resolves to no file is
|
|
1068
|
+
* reported in `unresolved`, never dropped: the part of the graph behind it
|
|
1069
|
+
* is unproven.
|
|
1070
|
+
*/
|
|
1071
|
+
static readonly reachability: (options: ReachabilityOptions) => Effect.Effect<ReachabilityScan, EntryNotFoundError | NoEntriesError | import("effect/PlatformError").PlatformError, FileSystem.FileSystem | Path.Path>;
|
|
1072
|
+
/**
|
|
1073
|
+
* Check every workspace package's runtime sources for imports its own
|
|
1074
|
+
* manifest does not declare.
|
|
1075
|
+
*
|
|
1076
|
+
* @remarks
|
|
1077
|
+
* For each discovered package, reads the files its `include` globs name
|
|
1078
|
+
* (`src/**` by default) and compares every runtime specifier against the
|
|
1079
|
+
* package's `dependencies`, `peerDependencies` and `optionalDependencies`.
|
|
1080
|
+
* `devDependencies` never declares: an import of one from a runtime source
|
|
1081
|
+
* is exactly the hoisting bug this check exists for. `optionalDependencies`
|
|
1082
|
+
* does declare: npm and pnpm install one by default, so a packed install
|
|
1083
|
+
* resolves the import. A relative,
|
|
1084
|
+
* absolute, `#`-subpath or URL-scheme specifier is never a package import;
|
|
1085
|
+
* a `node:` specifier is always a built-in, and a bare built-in is one only
|
|
1086
|
+
* when `builtins` lists it. A package importing its own name (a
|
|
1087
|
+
* self-reference through `exports`) is declared by definition. A file
|
|
1088
|
+
* under two discovered packages (a nested package under the root) is
|
|
1089
|
+
* checked against its innermost package's manifest only, so a root-wide
|
|
1090
|
+
* `include` never double-reports it. Type-only imports are erased by the
|
|
1091
|
+
* compiler and never flagged: `import type { X } from`, `export type`
|
|
1092
|
+
* re-exports, and an `import()` in a type position (`typeof import("x")`,
|
|
1093
|
+
* an annotation, a type alias right-hand side, a generic argument). The
|
|
1094
|
+
* position is judged by the construct enclosing the call, not by the one
|
|
1095
|
+
* token before it, so a runtime `import("x")` in a typed initializer, an
|
|
1096
|
+
* object literal, a ternary branch or a call argument counts like any
|
|
1097
|
+
* static import. One `import type from "x"` is runtime too — TypeScript
|
|
1098
|
+
* reads it as a default import of a binding named `type`. What the static
|
|
1099
|
+
* pass cannot see — a computed specifier — only `PackedInstall` proves.
|
|
1100
|
+
*/
|
|
1101
|
+
static readonly undeclared: (options?: UndeclaredOptions | undefined) => Effect.Effect<UndeclaredScan, import("@effected/glob").GlobPatternError | import("effect/PlatformError").PlatformError | WorkspaceDiscoveryFailure, FileSystem.FileSystem | Path.Path | WorkspaceDiscovery>;
|
|
1102
|
+
}
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region src/LayerPolicy.d.ts
|
|
1105
|
+
declare const LayerPolicyError_base: Schema.Class<LayerPolicyError, Schema.TaggedStruct<"LayerPolicyError", {
|
|
1106
|
+
/** `read`: the file could not be read. `json`: it is not JSON. `decode`: it is not a `LayerPolicy`. */
|
|
1107
|
+
readonly reason: Schema.Literals<readonly ["read", "json", "decode"]>;
|
|
1108
|
+
/** The file, when the policy came from one. */
|
|
1109
|
+
readonly path: Schema.optionalKey<Schema.String>;
|
|
1110
|
+
/** The originating failure. */
|
|
1111
|
+
readonly cause: Schema.Defect;
|
|
1112
|
+
}>, import("effect/Cause").YieldableError>;
|
|
1113
|
+
/**
|
|
1114
|
+
* Raised when a layer policy cannot be read, parsed or decoded.
|
|
1115
|
+
*
|
|
1116
|
+
* @public
|
|
1117
|
+
*/
|
|
1118
|
+
export declare class LayerPolicyError extends LayerPolicyError_base {
|
|
1119
|
+
/** Renders the failure kind and the file into one line. */
|
|
1120
|
+
get message(): string;
|
|
1121
|
+
}
|
|
1122
|
+
declare const LayerPolicy_base: Schema.Class<LayerPolicy, Schema.Struct<{
|
|
1123
|
+
/** Exact package names (never relative paths) per layer, top layer first. */
|
|
1124
|
+
readonly layers: Schema.$Array<Schema.$Array<Schema.String>>;
|
|
1125
|
+
/** Exact package names (never relative paths) any layer may depend on that never depend on a layer. */
|
|
1126
|
+
readonly tooling: Schema.$Array<Schema.String>;
|
|
1127
|
+
/** Globs matched against package names (never relative paths) for packages whose own edges are not checked. */
|
|
1128
|
+
readonly unconstrained: Schema.$Array<Schema.String>;
|
|
1129
|
+
/** The dependency maps to check. Absent means all four. */
|
|
1130
|
+
readonly fields: Schema.optionalKey<Schema.$Array<Schema.Literals<readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]>>>;
|
|
1131
|
+
/** Edges that must exist, written `"a -> b"`. */
|
|
1132
|
+
readonly requiredEdges: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
1133
|
+
}>, {}>;
|
|
1134
|
+
/**
|
|
1135
|
+
* A committed dependency-layering policy (`layers.json`).
|
|
1136
|
+
*
|
|
1137
|
+
* @remarks
|
|
1138
|
+
* `layers` is top-down: an edge may only point from a layer to one BELOW it
|
|
1139
|
+
* (a higher index), or into `tooling`. `tooling` packages may depend on each
|
|
1140
|
+
* other but never reach a layer. `unconstrained` holds globs (through
|
|
1141
|
+
* `@effected/glob`) for packages whose own edges are not checked (a test
|
|
1142
|
+
* harness, the private root). `fields` narrows which dependency maps count,
|
|
1143
|
+
* defaulting to all four. `requiredEdges` (`"a -> b"`) are edges that must
|
|
1144
|
+
* exist in a checked field: the non-vacuity guard against a discovery that
|
|
1145
|
+
* quietly drops real edges.
|
|
1146
|
+
*
|
|
1147
|
+
* Every entry matches a package's `name`, never its `relativePath`: `layers`
|
|
1148
|
+
* and `tooling` list exact names, and `unconstrained` globs match names. A
|
|
1149
|
+
* private root named `my-monorepo` at `relativePath` `"."` is classified as
|
|
1150
|
+
* `"my-monorepo"`; an entry of `"."` or `"packages/*"` classifies nothing.
|
|
1151
|
+
*
|
|
1152
|
+
* Decoding is strict: a key the policy does not model fails `decode`, and the
|
|
1153
|
+
* message names every such key. A typo on an optional key would otherwise be
|
|
1154
|
+
* silently dropped: `requiredEdge` would remove the non-vacuity guard and
|
|
1155
|
+
* leave the report green. `$schema` is always accepted. A file that carries
|
|
1156
|
+
* keys of its own, such as a `harness` block, passes them in `allowKeys`.
|
|
1157
|
+
*
|
|
1158
|
+
* @example
|
|
1159
|
+
* ```ts
|
|
1160
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
1161
|
+
* import { LayerPolicy } from "@effected/workspaces/testing";
|
|
1162
|
+
* import { Effect } from "effect";
|
|
1163
|
+
*
|
|
1164
|
+
* const policy = LayerPolicy.load("/repo/layers.json").pipe(Effect.provide(NodeServices.layer));
|
|
1165
|
+
* ```
|
|
1166
|
+
*
|
|
1167
|
+
* @public
|
|
1168
|
+
*/
|
|
1169
|
+
export declare class LayerPolicy extends LayerPolicy_base {
|
|
1170
|
+
/** The dependency maps a check reads: `fields`, or all four. */
|
|
1171
|
+
get effectiveFields(): ReadonlyArray<DependencyField>;
|
|
1172
|
+
/**
|
|
1173
|
+
* Decode a policy from a parsed JSON value.
|
|
1174
|
+
*
|
|
1175
|
+
* @remarks
|
|
1176
|
+
* Every key the policy does not model fails, except `$schema` and the
|
|
1177
|
+
* keys named in `options.allowKeys`, which are dropped before decoding.
|
|
1178
|
+
* `options.path` names the file in the error.
|
|
1179
|
+
*/
|
|
1180
|
+
static readonly decode: (input: unknown, options?: {
|
|
1181
|
+
readonly path?: string | undefined;
|
|
1182
|
+
readonly allowKeys?: ReadonlyArray<string> | undefined;
|
|
1183
|
+
} | undefined) => Effect.Effect<LayerPolicy, LayerPolicyError, never>;
|
|
1184
|
+
/** Read, parse and decode the policy file at `path`; `options.allowKeys` is passed to `decode`. */
|
|
1185
|
+
static readonly load: (path: string, options?: {
|
|
1186
|
+
readonly allowKeys?: ReadonlyArray<string> | undefined;
|
|
1187
|
+
} | undefined) => Effect.Effect<LayerPolicy, LayerPolicyError, FileSystem.FileSystem>;
|
|
1188
|
+
}
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/PackageManagerName.d.ts
|
|
1191
|
+
/**
|
|
1192
|
+
* The four package managers this package understands.
|
|
1193
|
+
*
|
|
1194
|
+
* @public
|
|
1195
|
+
*/
|
|
1196
|
+
export declare const PackageManagerName: Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>;
|
|
1197
|
+
/**
|
|
1198
|
+
* The decoded type of {@link (PackageManagerName:variable)}: `"npm" | "pnpm" | "yarn" | "bun"`.
|
|
1199
|
+
*
|
|
1200
|
+
* @public
|
|
1201
|
+
*/
|
|
1202
|
+
export type PackageManagerName = typeof PackageManagerName.Type;
|
|
1203
|
+
//#endregion
|
|
891
1204
|
//#region src/PackedInstall.d.ts
|
|
892
1205
|
/**
|
|
893
1206
|
* Where each closure package is packed from.
|
|
@@ -1870,9 +2183,10 @@ export declare class SourceBoundary {
|
|
|
1870
2183
|
* Check every source file under `root` against `rules`.
|
|
1871
2184
|
*
|
|
1872
2185
|
* @remarks
|
|
1873
|
-
* Walks with
|
|
2186
|
+
* Walks with the shared source walker (`internal/sourceWalk.ts`, which the
|
|
2187
|
+
* `ImportGraph` checks also use): each real directory is visited once (via
|
|
1874
2188
|
* `realPath`), so a symlink loop terminates and a linked directory is not
|
|
1875
|
-
* scanned twice
|
|
2189
|
+
* scanned twice, and `node_modules` is never entered. Paths come back relative
|
|
1876
2190
|
* and `/`-separated whatever the platform's separator, and that is also
|
|
1877
2191
|
* what `allow` globs match against. A missing root fails; it never scans
|
|
1878
2192
|
* nothing. A dangling symlink under the root is skipped, having nothing to
|
|
@@ -1890,7 +2204,7 @@ export declare class SourceBoundary {
|
|
|
1890
2204
|
* }).pipe(Effect.provide(NodeServices.layer));
|
|
1891
2205
|
* ```
|
|
1892
2206
|
*/
|
|
1893
|
-
static readonly scan: (options: ScanOptions) => Effect.Effect<SourceScan, import("@effected/glob").GlobPatternError | import("effect/PlatformError").PlatformError, FileSystem.FileSystem | Path.Path>;
|
|
2207
|
+
static readonly scan: (options: ScanOptions) => Effect.Effect<SourceScan, import("@effected/glob").GlobPatternError | import("effect/PlatformError").PlatformError, FileSystem.FileSystem | import("effect/Path").Path>;
|
|
1894
2208
|
/** The shipped positive- and negative-control snippets, at least one of each per rule kind. */
|
|
1895
2209
|
static readonly fixtures: ReadonlyArray<BoundaryFixture>;
|
|
1896
2210
|
/** The names of shipped fixtures the scanner gets wrong; `[]` means it still flags and spares what it must. */
|
|
@@ -1992,5 +2306,5 @@ export declare class WorkspaceLayering {
|
|
|
1992
2306
|
static readonly checkWorkspace: (policy: LayerPolicy) => Effect.Effect<LayeringReport, WorkspaceDiscoveryFailure, WorkspaceDiscovery>;
|
|
1993
2307
|
}
|
|
1994
2308
|
//#endregion
|
|
1995
|
-
export type { BinCommandOptions, BinProvenance, BoundaryFixture, BoundaryRule, DependencyDiff, FindWorkspaceRootOptions, LayeringGraph, OffenceRule, PackSource, PackedInstallBudget, PackedInstallClosureOptions, PackedInstallGate, PackedInstallOptions, PackedInstallPreflight, PackedInstallPreflightOptions, ReferenceOptions, RunBinOptions, ScanOptions, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceLookupFailure, WorkspaceRootShape };
|
|
2309
|
+
export type { BinCommandOptions, BinProvenance, BoundaryFixture, BoundaryRule, DependencyDiff, FindWorkspaceRootOptions, LayeringGraph, OffenceRule, PackSource, PackedInstallBudget, PackedInstallClosureOptions, PackedInstallGate, PackedInstallOptions, PackedInstallPreflight, PackedInstallPreflightOptions, ReachabilityOptions, ReferenceOptions, RunBinOptions, ScanOptions, UndeclaredOptions, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceLookupFailure, WorkspaceRootShape };
|
|
1996
2310
|
//# sourceMappingURL=testing.d.ts.map
|