@goodbones/core 0.1.0-beta.2 → 0.1.0-beta.4
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/build/dts/core/graph.d.ts +1 -0
- package/build/dts/core/graph.d.ts.map +1 -1
- package/build/dts/domain/manifest-location.d.ts +2 -0
- package/build/dts/domain/manifest-location.d.ts.map +1 -1
- package/build/dts/index.d.ts +6 -4
- package/build/dts/index.d.ts.map +1 -1
- package/build/dts/infrastructure/manifest-file.d.ts +1 -0
- package/build/dts/infrastructure/manifest-file.d.ts.map +1 -1
- package/build/dts/infrastructure/manifest-include.d.ts +16 -0
- package/build/dts/infrastructure/manifest-include.d.ts.map +1 -0
- package/build/dts/infrastructure/walk.d.ts +6 -0
- package/build/dts/infrastructure/walk.d.ts.map +1 -1
- package/build/dts/manifest/infer.d.ts +55 -0
- package/build/dts/manifest/infer.d.ts.map +1 -0
- package/build/dts/manifest/json-schema.d.ts +2 -0
- package/build/dts/manifest/json-schema.d.ts.map +1 -1
- package/build/dts/manifest/manifest.d.ts +1 -1
- package/build/dts/manifest/manifest.d.ts.map +1 -1
- package/build/dts/ports/language.d.ts +2 -0
- package/build/dts/ports/language.d.ts.map +1 -1
- package/build/esm/core/graph.js +8 -0
- package/build/esm/core/graph.js.map +1 -1
- package/build/esm/domain/manifest-location.js +16 -1
- package/build/esm/domain/manifest-location.js.map +1 -1
- package/build/esm/index.js +6 -3
- package/build/esm/index.js.map +1 -1
- package/build/esm/infrastructure/manifest-file.js +31 -17
- package/build/esm/infrastructure/manifest-file.js.map +1 -1
- package/build/esm/infrastructure/manifest-include.js +187 -0
- package/build/esm/infrastructure/manifest-include.js.map +1 -0
- package/build/esm/infrastructure/walk.js +57 -1
- package/build/esm/infrastructure/walk.js.map +1 -1
- package/build/esm/manifest/infer.js +455 -0
- package/build/esm/manifest/infer.js.map +1 -0
- package/build/esm/manifest/json-schema.js +72 -17
- package/build/esm/manifest/json-schema.js.map +1 -1
- package/build/esm/manifest/manifest.js +7 -18
- package/build/esm/manifest/manifest.js.map +1 -1
- package/package.json +1 -1
- package/schema/architecture-node.schema.json +1518 -0
- package/schema/architecture.schema.json +1102 -691
- package/src/core/graph.ts +12 -0
- package/src/domain/manifest-location.ts +22 -0
- package/src/index.ts +33 -2
- package/src/infrastructure/manifest-file.ts +37 -17
- package/src/infrastructure/manifest-include.ts +318 -0
- package/src/infrastructure/walk.ts +70 -1
- package/src/manifest/infer.ts +643 -0
- package/src/manifest/json-schema.ts +83 -18
- package/src/manifest/manifest.ts +11 -20
- package/src/ports/language.ts +11 -0
|
@@ -7,13 +7,19 @@ import { Manifest } from "./manifest.js";
|
|
|
7
7
|
// comment and a JSON file in a `$schema` key; either way the editor completes
|
|
8
8
|
// keys and flags a misspelled one before the loader ever runs.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
// `{ use }` reference form that may stand in for any object
|
|
12
|
-
//
|
|
10
|
+
// Three things the codec does not know are added here: the `defs` map, the
|
|
11
|
+
// `{ use }` reference form that may stand in for any object, and the
|
|
12
|
+
// `{ include }` form that may stand in for any object or list — all belong to
|
|
13
|
+
// the passes that run before decoding.
|
|
13
14
|
|
|
14
15
|
export const MANIFEST_SCHEMA_ID =
|
|
15
16
|
"https://dataquail.github.io/goodbones/schema/architecture.schema.json";
|
|
16
17
|
|
|
18
|
+
// The schema an included file names: one node of the tree, with the two keys
|
|
19
|
+
// a file of its own may carry at the top.
|
|
20
|
+
export const MANIFEST_NODE_SCHEMA_ID =
|
|
21
|
+
"https://dataquail.github.io/goodbones/schema/architecture-node.schema.json";
|
|
22
|
+
|
|
17
23
|
type JsonValue = string | number | boolean | null | JsonObject | ReadonlyArray<JsonValue>;
|
|
18
24
|
type JsonObject = { readonly [key: string]: JsonValue };
|
|
19
25
|
|
|
@@ -23,11 +29,14 @@ const entriesOf = (value: JsonObject): ReadonlyArray<readonly [string, JsonValue
|
|
|
23
29
|
const isObject = (value: JsonValue): value is JsonObject =>
|
|
24
30
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
31
|
|
|
32
|
+
const isList = (value: JsonValue): value is ReadonlyArray<JsonValue> => Array.isArray(value);
|
|
33
|
+
|
|
26
34
|
// The generator names the recursive node after its own internal wrapper. A
|
|
27
35
|
// stable name is what a `$ref` in an error message or a docs page can point at.
|
|
28
36
|
const DEFINITION_NAMES: Readonly<Record<string, string>> = { Suspend_: "ManifestNode" };
|
|
29
37
|
|
|
30
38
|
const USE_REFERENCE = "#/$defs/Use";
|
|
39
|
+
const INCLUDE_REFERENCE = "#/$defs/Include";
|
|
31
40
|
|
|
32
41
|
const Use: JsonObject = {
|
|
33
42
|
type: "object",
|
|
@@ -37,9 +46,20 @@ const Use: JsonObject = {
|
|
|
37
46
|
required: ["use"],
|
|
38
47
|
};
|
|
39
48
|
|
|
49
|
+
const Include: JsonObject = {
|
|
50
|
+
type: "object",
|
|
51
|
+
description:
|
|
52
|
+
"A reference to another YAML or JSON file, relative to this one. Replaced by that file's whole value before the manifest is decoded; a list item naming a file that holds a list is spliced in. Nothing may be written beside `include`.",
|
|
53
|
+
properties: { include: { type: "string" } },
|
|
54
|
+
required: ["include"],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
};
|
|
57
|
+
|
|
40
58
|
// Every object schema below the root becomes "this object, or a `use` of a
|
|
41
|
-
// fragment shaped like it
|
|
42
|
-
//
|
|
59
|
+
// fragment shaped like it, or an `include` of a file holding one", and every
|
|
60
|
+
// list schema "this list, or an `include` of a file holding one". The
|
|
61
|
+
// expansion passes replace a reference wherever it stands, so the schema
|
|
62
|
+
// admits one wherever the value may stand.
|
|
43
63
|
const admitReferences = (value: JsonValue): JsonValue => {
|
|
44
64
|
if (Array.isArray(value)) return value.map(admitReferences);
|
|
45
65
|
if (!isObject(value)) return value;
|
|
@@ -53,8 +73,25 @@ const admitReferences = (value: JsonValue): JsonValue => {
|
|
|
53
73
|
rebuilt[key] = admitReferences(entry);
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
if (rebuilt.type === "object" && "properties" in rebuilt) {
|
|
77
|
+
return { anyOf: [{ $ref: USE_REFERENCE }, { $ref: INCLUDE_REFERENCE }, rebuilt] };
|
|
78
|
+
}
|
|
79
|
+
if (rebuilt.type === "array") {
|
|
80
|
+
return { anyOf: [{ $ref: INCLUDE_REFERENCE }, rebuilt] };
|
|
81
|
+
}
|
|
82
|
+
return rebuilt;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const DEFS_PROPERTY: JsonObject = {
|
|
86
|
+
type: "object",
|
|
87
|
+
description:
|
|
88
|
+
'Named fragments, referenced elsewhere in the manifest as `{ use: "<name>" }`. A fragment may itself contain `use`. Every file\'s `defs` share one namespace.',
|
|
89
|
+
additionalProperties: true,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const SCHEMA_PROPERTY: JsonObject = {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "For editors. Ignored by the loader.",
|
|
58
95
|
};
|
|
59
96
|
|
|
60
97
|
export const manifestJsonSchema = (): JsonObject => {
|
|
@@ -80,20 +117,48 @@ export const manifestJsonSchema = (): JsonObject => {
|
|
|
80
117
|
"One manifest of a repository's architecture, read by @goodbones/cli and @goodbones/oxlint. See https://dataquail.github.io/goodbones/architecture-rules/manifest/.",
|
|
81
118
|
...root,
|
|
82
119
|
properties: {
|
|
83
|
-
$schema:
|
|
84
|
-
|
|
85
|
-
description: "For editors. Ignored by the loader.",
|
|
86
|
-
},
|
|
87
|
-
defs: {
|
|
88
|
-
type: "object",
|
|
89
|
-
description:
|
|
90
|
-
'Named fragments, referenced elsewhere in the manifest as `{ use: "<name>" }`. A fragment may itself contain `use`.',
|
|
91
|
-
additionalProperties: true,
|
|
92
|
-
},
|
|
120
|
+
$schema: SCHEMA_PROPERTY,
|
|
121
|
+
defs: DEFS_PROPERTY,
|
|
93
122
|
...Object.fromEntries(
|
|
94
123
|
entriesOf(properties).map(([key, value]) => [key, admitReferences(value)]),
|
|
95
124
|
),
|
|
96
125
|
},
|
|
97
|
-
$defs: { ...definitions, Use },
|
|
126
|
+
$defs: { ...definitions, Use, Include },
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// One node of the tree as a file of its own — what a per-package
|
|
131
|
+
// `architecture.yaml` that the root manifest `include`s is shaped like. The
|
|
132
|
+
// node's object form, with the `$schema` and `defs` keys such a file may carry
|
|
133
|
+
// at the top, over the same definitions as the whole manifest.
|
|
134
|
+
export const manifestNodeJsonSchema = (): JsonObject => {
|
|
135
|
+
const whole = manifestJsonSchema();
|
|
136
|
+
const definitions = whole.$defs;
|
|
137
|
+
if (definitions === undefined || !isObject(definitions)) {
|
|
138
|
+
throw new Error("the manifest schema generated with no definitions");
|
|
139
|
+
}
|
|
140
|
+
const node = definitions.ManifestNode;
|
|
141
|
+
const variants = node !== undefined && isObject(node) ? node.anyOf : undefined;
|
|
142
|
+
const object =
|
|
143
|
+
variants !== undefined && isList(variants)
|
|
144
|
+
? variants.find((variant) => isObject(variant) && variant.type === "object")
|
|
145
|
+
: undefined;
|
|
146
|
+
if (object === undefined || !isObject(object)) {
|
|
147
|
+
throw new Error("the manifest schema generated with no object form of a node");
|
|
148
|
+
}
|
|
149
|
+
const { $id: _id, $schema: _schema, ...rest } = object;
|
|
150
|
+
return {
|
|
151
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
152
|
+
$id: MANIFEST_NODE_SCHEMA_ID,
|
|
153
|
+
title: "Architecture manifest node",
|
|
154
|
+
description:
|
|
155
|
+
"One node of an architecture manifest's tree, as a file the manifest includes. See https://dataquail.github.io/goodbones/architecture-rules/manifest/#splitting-the-manifest-include.",
|
|
156
|
+
...rest,
|
|
157
|
+
properties: {
|
|
158
|
+
$schema: SCHEMA_PROPERTY,
|
|
159
|
+
defs: DEFS_PROPERTY,
|
|
160
|
+
...(rest.properties !== undefined && isObject(rest.properties) ? rest.properties : {}),
|
|
161
|
+
},
|
|
162
|
+
$defs: definitions,
|
|
98
163
|
};
|
|
99
164
|
};
|
package/src/manifest/manifest.ts
CHANGED
|
@@ -4,7 +4,11 @@ import * as SchemaIssue from "effect/SchemaIssue";
|
|
|
4
4
|
|
|
5
5
|
import { DeclarationKind, ResolveConfig } from "../domain/architecture-config.js";
|
|
6
6
|
import { ConfigInvalid } from "../domain/architecture-error.js";
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
type ManifestLocator,
|
|
9
|
+
type ManifestPath,
|
|
10
|
+
renderManifestPath,
|
|
11
|
+
} from "../domain/manifest-location.js";
|
|
8
12
|
import { expandManifest, originOf, type Substitution } from "./expand.js";
|
|
9
13
|
|
|
10
14
|
// A manifest is a tree of nodes keyed by path pattern, where everything the
|
|
@@ -406,32 +410,19 @@ export type DecodeManifestOptions = {
|
|
|
406
410
|
readonly locate?: ManifestLocator | undefined;
|
|
407
411
|
};
|
|
408
412
|
|
|
409
|
-
const isIdentifier = (key: string): boolean => /^[A-Za-z_$][\w$]*$/.test(key);
|
|
410
|
-
|
|
411
|
-
// `tree["~/core/"].members[0].subject` — dotted where a key reads as a name,
|
|
412
|
-
// bracketed where it does not, so a node key that is a path pattern stays
|
|
413
|
-
// legible.
|
|
414
|
-
const renderPath = (path: ManifestPath): string =>
|
|
415
|
-
path.length === 0
|
|
416
|
-
? "(root)"
|
|
417
|
-
: path
|
|
418
|
-
.map((segment, index) => {
|
|
419
|
-
if (typeof segment === "number") return `[${String(segment)}]`;
|
|
420
|
-
const key = String(segment);
|
|
421
|
-
if (isIdentifier(key)) return index === 0 ? key : `.${key}`;
|
|
422
|
-
return `[${JSON.stringify(key)}]`;
|
|
423
|
-
})
|
|
424
|
-
.join("");
|
|
425
|
-
|
|
426
413
|
const fileLabelOf = (configPath: string): string => configPath.split(/[\\/]/).at(-1) ?? configPath;
|
|
427
414
|
|
|
415
|
+
// A position carries its own file when the value was written in a file the
|
|
416
|
+
// manifest `include`d; otherwise it is in the manifest file itself.
|
|
428
417
|
const positionOf = (
|
|
429
418
|
file: string,
|
|
430
419
|
locate: ManifestLocator | undefined,
|
|
431
420
|
path: ManifestPath,
|
|
432
421
|
): string | null => {
|
|
433
422
|
const found = locate?.(path) ?? null;
|
|
434
|
-
return found === null
|
|
423
|
+
return found === null
|
|
424
|
+
? null
|
|
425
|
+
: `${found.file ?? file}:${String(found.line)}:${String(found.column)}`;
|
|
435
426
|
};
|
|
436
427
|
|
|
437
428
|
// One line per issue: where in the file, which path, what was wrong — and,
|
|
@@ -452,7 +443,7 @@ const describeIssue = (
|
|
|
452
443
|
return `via \`use: ${JSON.stringify(name)}\`${position === null ? "" : ` at ${position}`}`;
|
|
453
444
|
});
|
|
454
445
|
return (
|
|
455
|
-
` ${at === null ? "" : `${at} `}${
|
|
446
|
+
` ${at === null ? "" : `${at} `}${renderManifestPath(origin.path)}: ${detail}` +
|
|
456
447
|
(via.length === 0 ? "" : ` (${via.join(", ")})`)
|
|
457
448
|
);
|
|
458
449
|
};
|
package/src/ports/language.ts
CHANGED
|
@@ -21,6 +21,17 @@ export type Language = {
|
|
|
21
21
|
// Files carrying one of those extensions that are not source — for
|
|
22
22
|
// TypeScript, a declaration file states types and no linter visits one.
|
|
23
23
|
readonly ignoredFiles: ReadonlyArray<RegExp>;
|
|
24
|
+
// The files whose presence makes a folder a package of this language —
|
|
25
|
+
// `package.json`, `go.mod`, `Cargo.toml`. `infer` counts its depth from a
|
|
26
|
+
// package rather than from the repository root, so a monorepo of twenty
|
|
27
|
+
// packages is described one package at a time. Empty if the ecosystem has no
|
|
28
|
+
// such marker.
|
|
29
|
+
readonly packageMarkers: ReadonlyArray<string>;
|
|
30
|
+
// The folder names a package of this language keeps its source under, in
|
|
31
|
+
// order of preference — `src` for TypeScript, nothing for Go, where source
|
|
32
|
+
// sits at the package root. The first one that exists is where `infer`
|
|
33
|
+
// starts counting.
|
|
34
|
+
readonly sourceRoots: ReadonlyArray<string>;
|
|
24
35
|
// Reads the facts out of one source text. The CLI reads every file through
|
|
25
36
|
// it, and a probe carrying a `source` snippet is parsed by it at load.
|
|
26
37
|
readonly extractor: FactExtractor;
|