@effected/schemastore 0.8.0 → 0.9.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/CatalogEntry.js +13 -1
- package/DriftPolicy.js +30 -0
- package/README.md +1 -1
- package/SchemaTarget.js +9 -2
- package/SchemaVersioning.js +67 -30
- package/SchemastoreConfig.js +118 -0
- package/index.d.ts +194 -29
- package/index.js +3 -1
- package/package.json +1 -1
package/CatalogEntry.js
CHANGED
|
@@ -39,6 +39,14 @@ const GENERIC_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
39
39
|
"settings"
|
|
40
40
|
]);
|
|
41
41
|
const COMPLEX_GLOB = /[{}[\]]|[?*+@!]\(|^!/;
|
|
42
|
+
const assertDistinctVersions = (name, versions) => {
|
|
43
|
+
const seen = [];
|
|
44
|
+
for (const version of versions) {
|
|
45
|
+
const duplicate = seen.find((v) => SchemaVersioning.Order(v, version) === 0);
|
|
46
|
+
if (duplicate !== void 0) throw new Error(`CatalogEntry.assemble: "${name}" declares the same version twice, as "${duplicate}" and "${version}"`);
|
|
47
|
+
seen.push(version);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
42
50
|
const lintPattern = (pattern) => {
|
|
43
51
|
const findings = [];
|
|
44
52
|
const basename = pattern.slice(pattern.lastIndexOf("/") + 1);
|
|
@@ -90,9 +98,13 @@ var CatalogEntry = class CatalogEntry extends Schema.Class("CatalogEntry")({
|
|
|
90
98
|
* Assembles an entry from a catalog identity plus
|
|
91
99
|
* {@link SchemaVersioning.catalogUrls}' inputs: pass `versions` for the
|
|
92
100
|
* versioned mode (the `versions` map and latest-pointing `url` are
|
|
93
|
-
* derived), omit it for the unversioned mode.
|
|
101
|
+
* derived), omit it for the unversioned mode. Throws an `Error` naming
|
|
102
|
+
* both spellings when two labels compare equal under
|
|
103
|
+
* {@link SchemaVersioning.Order} (`1.2` and `1.2.0`): each would be its
|
|
104
|
+
* own key and URL for one document.
|
|
94
105
|
*/
|
|
95
106
|
static assemble(options) {
|
|
107
|
+
if (options.versions !== void 0) assertDistinctVersions(options.name, options.versions);
|
|
96
108
|
const urls = SchemaVersioning.catalogUrls({
|
|
97
109
|
baseUrl: options.baseUrl,
|
|
98
110
|
name: options.fileBaseName ?? options.name,
|
package/DriftPolicy.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/DriftPolicy.ts
|
|
2
|
+
/**
|
|
3
|
+
* Classifies one target's change against a drift tolerance.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Pure and synchronous. The lifecycle rule is fixed here so no caller
|
|
7
|
+
* relitigates it: an unpublished target is NEVER drift — it is regenerated
|
|
8
|
+
* in place until someone depends on its label. Gate failures (lint warnings,
|
|
9
|
+
* validator findings) are not drift either and are not this module's
|
|
10
|
+
* concern.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
var DriftPolicy = class {
|
|
15
|
+
constructor() {}
|
|
16
|
+
/** `{ policy: "semantic", onDrift: "error" }` — what a config gets when it says nothing. */
|
|
17
|
+
static defaults = {
|
|
18
|
+
policy: "semantic",
|
|
19
|
+
onDrift: "error"
|
|
20
|
+
};
|
|
21
|
+
static classify(input, policy) {
|
|
22
|
+
if (!input.published || policy === "allow") return "write";
|
|
23
|
+
if (input.change === "contract") return "drift";
|
|
24
|
+
if (input.change === "annotations" && policy === "strict") return "drift";
|
|
25
|
+
return "write";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { DriftPolicy };
|
package/README.md
CHANGED
|
@@ -114,7 +114,7 @@ One thing the package does *not* do to a declared-family value is walk inside it
|
|
|
114
114
|
|
|
115
115
|
## Catalog entries and versioning
|
|
116
116
|
|
|
117
|
-
`CatalogEntry` is the `catalog.json` entry as a `Schema.Class`, so decoding an existing entry and encoding one for submission are the same artifact. `SchemaVersion` is a **
|
|
117
|
+
`CatalogEntry` is the `catalog.json` entry as a `Schema.Class`, so decoding an existing entry and encoding one for submission are the same artifact. `SchemaVersion` is a **one-to-three-component** label — `major`, `major.minor` or `major.minor.patch` with an optional prerelease, matched by a grammar regex and then validated by `@effected/semver` over the label padded to three components — so ordering is plain SemVer precedence (`1.10.0` above `1.9.0`; `1`, `1.0` and `1.0.0` compare equal) and the label round-trips verbatim. Build metadata is rejected (`1.0.0+build.5` does not parse): SemVer precedence ignores it, so two labels differing only in build would compare equal and both claim to be the latest. Surrounding whitespace is rejected for the same round-tripping reason. The file-name convention is SchemaStore's own `<name>-<version>.json`, and partial labels like `1.2` — common in the store's corpus — are accepted as written; `defineConfig` refuses two spellings of one version under one name so a file name always maps back to one label.
|
|
118
118
|
|
|
119
119
|
`SchemaVersioning.isPinned(version)` answers whether a label names a published document rather than a prerelease — SemVer §9 makes a prerelease's own instability explicit, so a contract change inside one breaks nobody's pin. It is the one predicate the pipeline's contract gate and `SchemaVersioning.next` both read, so the two can never disagree about the same label. `next(current, change)` is the version a `WriteChange` classification calls for: identity for anything but a `"contract"` change on a pinned label, otherwise a MINOR bump on the 0.x line (0.x treats MINOR as the breaking axis) or MAJOR above it — always strictly greater, never a minted prerelease.
|
|
120
120
|
|
package/SchemaTarget.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { SchemaVersioning } from "./SchemaVersioning.js";
|
|
2
|
+
import { Result } from "effect";
|
|
3
|
+
|
|
1
4
|
//#region src/SchemaTarget.ts
|
|
2
5
|
/**
|
|
3
6
|
* Constructors for `SchemaTarget` values.
|
|
@@ -10,18 +13,22 @@ var SchemaTarget = class {
|
|
|
10
13
|
* Builds a target. `$id` and `path` must be non-empty — an empty
|
|
11
14
|
* identity is a wiring mistake and throws, as does an empty `name` when
|
|
12
15
|
* one is given. The `name`-with-`version` invariant is enforced by the
|
|
13
|
-
* overloads above; the runtime check remains for untyped callers.
|
|
16
|
+
* overloads above; the runtime check remains for untyped callers. A
|
|
17
|
+
* string `version` that fails {@link SchemaVersioning.parseResult} throws,
|
|
18
|
+
* naming the invalid label.
|
|
14
19
|
*/
|
|
15
20
|
static make(options) {
|
|
16
21
|
for (const key of ["$id", "path"]) if (options[key].length === 0) throw new Error(`SchemaTarget.make requires a non-empty "${key}"`);
|
|
17
22
|
if (options.name !== void 0 && options.name.length === 0) throw new Error("SchemaTarget.make requires a non-empty \"name\" when one is given");
|
|
18
23
|
if (options.version !== void 0 && options.name === void 0) throw new Error("SchemaTarget.make requires a \"name\" when \"version\" is given (catalog naming is name-<version>.json)");
|
|
24
|
+
const version = options.version === void 0 ? void 0 : Result.getOrThrowWith(SchemaVersioning.parseResult(options.version), (error) => /* @__PURE__ */ new Error(`SchemaTarget.make received an invalid version label "${options.version}": ${error.message}`));
|
|
19
25
|
return {
|
|
20
26
|
schema: options.schema,
|
|
21
27
|
$id: options.$id,
|
|
22
28
|
path: options.path,
|
|
29
|
+
published: options.published ?? false,
|
|
23
30
|
...options.name !== void 0 ? { name: options.name } : {},
|
|
24
|
-
...
|
|
31
|
+
...version !== void 0 ? { version } : {},
|
|
25
32
|
...options.jsonSchema !== void 0 ? { jsonSchema: options.jsonSchema } : {}
|
|
26
33
|
};
|
|
27
34
|
}
|
package/SchemaVersioning.js
CHANGED
|
@@ -2,9 +2,27 @@ import { Effect, Option, Order, Result, Schema } from "effect";
|
|
|
2
2
|
import { SemVer } from "@effected/semver";
|
|
3
3
|
|
|
4
4
|
//#region src/SchemaVersioning.ts
|
|
5
|
+
const LABEL = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*)){0,2}(-[0-9A-Za-z.-]+)?$/;
|
|
6
|
+
const split = (input) => {
|
|
7
|
+
if (!LABEL.test(input)) return;
|
|
8
|
+
const dash = input.indexOf("-");
|
|
9
|
+
const core = dash === -1 ? input : input.slice(0, dash);
|
|
10
|
+
const prerelease = dash === -1 ? "" : input.slice(dash);
|
|
11
|
+
return {
|
|
12
|
+
core: core.split("."),
|
|
13
|
+
prerelease
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/** Pad a 1–3 component core to `major.minor.patch` so SemVer can order it. */
|
|
17
|
+
const padded = (parts) => `${[
|
|
18
|
+
...parts.core,
|
|
19
|
+
"0",
|
|
20
|
+
"0"
|
|
21
|
+
].slice(0, 3).join(".")}${parts.prerelease}`;
|
|
5
22
|
const isVersionLabel = (input) => {
|
|
6
|
-
|
|
7
|
-
|
|
23
|
+
const parts = split(input);
|
|
24
|
+
if (parts === void 0) return false;
|
|
25
|
+
const parsed = SemVer.parseResult(padded(parts));
|
|
8
26
|
return Result.isSuccess(parsed) && parsed.success.build.length === 0;
|
|
9
27
|
};
|
|
10
28
|
/**
|
|
@@ -22,29 +40,34 @@ input: Schema.String }) {
|
|
|
22
40
|
}
|
|
23
41
|
};
|
|
24
42
|
/**
|
|
25
|
-
* A schema version label: a branded string holding
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `1.2` and `1` are NOT accepted, though SchemaStore's own corpus uses such
|
|
30
|
-
* labels: requiring all three components makes a label unambiguous to split
|
|
31
|
-
* back out of `<name>-<version>.json` or its URL, which is what consumers
|
|
32
|
-
* do with it. The file-name convention around the label stays SchemaStore's.
|
|
43
|
+
* A schema version label: a branded string holding one, two or three
|
|
44
|
+
* dot-separated numeric components — `major`, `major.minor` or
|
|
45
|
+
* `major.minor.patch` — with an optional SemVer prerelease. Build metadata
|
|
46
|
+
* is rejected (see below).
|
|
33
47
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
48
|
+
* This matches SchemaStore's own corpus, which commonly uses partial labels
|
|
49
|
+
* like `agripparc-1.2.json`. The label is preserved VERBATIM — it is the
|
|
50
|
+
* file name and the URL — and a missing component is read as zero only for
|
|
51
|
+
* ordering (see {@link SchemaVersioning.Order}), never rewritten into the
|
|
52
|
+
* label itself.
|
|
36
53
|
*
|
|
37
54
|
* @public
|
|
38
55
|
*/
|
|
39
|
-
const SchemaVersion = Schema.String.check(Schema.makeFilter((value) => isVersionLabel(value) ? void 0 : "must be a
|
|
56
|
+
const SchemaVersion = Schema.String.check(Schema.makeFilter((value) => isVersionLabel(value) ? void 0 : "must be a major, major.minor or major.minor.patch version label")).pipe(Schema.brand("SchemaVersion"));
|
|
40
57
|
const orderingKey = (label) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
58
|
+
const parts = split(label);
|
|
59
|
+
const result = parts === void 0 ? void 0 : SemVer.parseResult(padded(parts));
|
|
60
|
+
if (result === void 0 || Result.isFailure(result)) throw new Error(`SchemaVersion ordering invariant violated for label "${label}"`);
|
|
43
61
|
return result.success;
|
|
44
62
|
};
|
|
45
|
-
const
|
|
63
|
+
const bumpNext = (current, parsed, components) => {
|
|
46
64
|
try {
|
|
47
|
-
|
|
65
|
+
const bumped = components === 1 ? parsed.bump.major() : parsed.bump.minor();
|
|
66
|
+
return [
|
|
67
|
+
bumped.major,
|
|
68
|
+
bumped.minor,
|
|
69
|
+
bumped.patch
|
|
70
|
+
].slice(0, components).join(".");
|
|
48
71
|
} catch (cause) {
|
|
49
72
|
throw new Error(`SchemaVersion bump invariant violated: "${current}" cannot be bumped past Number.MAX_SAFE_INTEGER (${Number.MAX_SAFE_INTEGER})`, { cause });
|
|
50
73
|
}
|
|
@@ -63,8 +86,10 @@ const joinUrl = (baseUrl, file) => {
|
|
|
63
86
|
* — SchemaStore's own suffix convention — a `versions` map, and `url`
|
|
64
87
|
* pointing at the latest version).
|
|
65
88
|
*
|
|
66
|
-
* Version labels are
|
|
67
|
-
*
|
|
89
|
+
* Version labels are 1–3 dot-separated numeric components with an optional
|
|
90
|
+
* prerelease; ordering reads a missing component as zero and otherwise
|
|
91
|
+
* follows plain SemVer precedence: `1.10` above `1.9`, `2.0.0-beta` below
|
|
92
|
+
* `2.0.0`.
|
|
68
93
|
*
|
|
69
94
|
* @public
|
|
70
95
|
*/
|
|
@@ -112,6 +137,15 @@ var SchemaVersioning = class SchemaVersioning {
|
|
|
112
137
|
return orderingKey(version).prerelease.length === 0;
|
|
113
138
|
}
|
|
114
139
|
/**
|
|
140
|
+
* The number of dot-separated numeric components in a label's core:
|
|
141
|
+
* `1` for `"1"` (or `"1-beta"`), `2` for `"1.2"`, `3` for `"1.2.3"`.
|
|
142
|
+
*/
|
|
143
|
+
static components(version) {
|
|
144
|
+
const parts = split(version);
|
|
145
|
+
if (parts === void 0) throw new Error(`SchemaVersion components invariant violated for label "${version}"`);
|
|
146
|
+
return parts.core.length;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
115
149
|
* The version label a change classification calls for. Pure and
|
|
116
150
|
* synchronous; total over validated labels — a non-label input is a wiring
|
|
117
151
|
* bug and dies as a defect, the same as {@link SchemaVersioning.Order}.
|
|
@@ -123,13 +157,15 @@ var SchemaVersioning = class SchemaVersioning {
|
|
|
123
157
|
* declares its own instability; the pipeline's `"block-versioned"`
|
|
124
158
|
* policy uses the same {@link SchemaVersioning.isPinned}, so the gate
|
|
125
159
|
* and the bump agree.
|
|
126
|
-
* -
|
|
127
|
-
*
|
|
128
|
-
*
|
|
160
|
+
* - otherwise → a MINOR bump, preserving the component count
|
|
161
|
+
* ({@link SchemaVersioning.components}): `1.2.3` → `1.3.0`, `1.2` →
|
|
162
|
+
* `1.3`, `0.4` → `0.5`. On a one-component label the only axis IS
|
|
163
|
+
* major: `1` → `2`.
|
|
129
164
|
*
|
|
130
165
|
* The bump's job is to be strictly greater and conspicuous, NOT to encode
|
|
131
166
|
* SemVer compatibility: `DocumentDiff` cannot tell an added optional
|
|
132
167
|
* property from a removed required one, so every contract change reads as
|
|
168
|
+
* breaking — the author bumps MAJOR by hand when they know the change is
|
|
133
169
|
* breaking. Each label is its own file and URL, so an over-bump costs a
|
|
134
170
|
* file; an under-bump would overwrite a pinned document. `next` never
|
|
135
171
|
* introduces a prerelease from a stable input.
|
|
@@ -140,7 +176,7 @@ var SchemaVersioning = class SchemaVersioning {
|
|
|
140
176
|
*/
|
|
141
177
|
static next(current, change) {
|
|
142
178
|
if (change !== "contract" || !SchemaVersioning.isPinned(current)) return current;
|
|
143
|
-
const label =
|
|
179
|
+
const label = bumpNext(current, orderingKey(current), SchemaVersioning.components(current));
|
|
144
180
|
const reparsed = SchemaVersioning.parseResult(label);
|
|
145
181
|
if (Result.isFailure(reparsed)) throw new Error(`SchemaVersion bump invariant violated: "${current}" bumped to "${label}"`);
|
|
146
182
|
return reparsed.success;
|
|
@@ -173,13 +209,14 @@ var SchemaVersioning = class SchemaVersioning {
|
|
|
173
209
|
* a contradiction (versioned mode with no versions) and throws — pass
|
|
174
210
|
* `undefined` for the unversioned mode.
|
|
175
211
|
*
|
|
176
|
-
* Labels are inserted in ascending {@link SchemaVersioning.Order}
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* (as {@link SchemaVersioning.latest} does) is still the
|
|
212
|
+
* Labels are inserted in ascending {@link SchemaVersioning.Order}, but a
|
|
213
|
+
* bare-major label (`"2"`) is array-index-like, so JavaScript enumerates
|
|
214
|
+
* it FIRST regardless of insertion order — the serialized order of the
|
|
215
|
+
* `versions` map is not meaningful when such a label is present. A two-
|
|
216
|
+
* or three-component label is never integer-like and keeps insertion
|
|
217
|
+
* order through serialization. Deriving ordering from the labels
|
|
218
|
+
* themselves (as {@link SchemaVersioning.latest} does) is still the
|
|
219
|
+
* robust read.
|
|
183
220
|
*/
|
|
184
221
|
static catalogUrls(options) {
|
|
185
222
|
const { baseUrl, name, versions } = options;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { SchemaVersioning } from "./SchemaVersioning.js";
|
|
2
|
+
import { CatalogEntry } from "./CatalogEntry.js";
|
|
3
|
+
import { DriftPolicy } from "./DriftPolicy.js";
|
|
4
|
+
import { Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/SchemastoreConfig.ts
|
|
7
|
+
const ConfigBrand = Symbol.for("@effected/schemastore/SchemastoreConfig");
|
|
8
|
+
const DriftSchema = Schema.Struct({
|
|
9
|
+
policy: Schema.optionalKey(Schema.Literals([
|
|
10
|
+
"strict",
|
|
11
|
+
"semantic",
|
|
12
|
+
"allow"
|
|
13
|
+
])),
|
|
14
|
+
onDrift: Schema.optionalKey(Schema.Literals(["error", "warn"]))
|
|
15
|
+
});
|
|
16
|
+
const CatalogConfigSchema = Schema.Struct({
|
|
17
|
+
name: Schema.String.check(Schema.isMinLength(1)),
|
|
18
|
+
description: Schema.String,
|
|
19
|
+
fileMatch: Schema.Array(Schema.String),
|
|
20
|
+
baseUrl: Schema.String.check(Schema.isMinLength(1)),
|
|
21
|
+
path: Schema.String.check(Schema.isMinLength(1))
|
|
22
|
+
});
|
|
23
|
+
const decodeOrThrow = (schema, value, what) => {
|
|
24
|
+
const result = Schema.decodeUnknownResult(schema)(value);
|
|
25
|
+
if (result._tag === "Failure") throw new Error(`defineConfig: invalid ${what}: ${String(result.failure)}`);
|
|
26
|
+
return result.success;
|
|
27
|
+
};
|
|
28
|
+
const versionsByName = (schemas) => {
|
|
29
|
+
const map = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const target of schemas) {
|
|
31
|
+
if (target.name === void 0 || target.version === void 0) continue;
|
|
32
|
+
const version = target.version;
|
|
33
|
+
const versions = map.get(target.name) ?? [];
|
|
34
|
+
const duplicate = versions.find((v) => SchemaVersioning.Order(v, version) === 0);
|
|
35
|
+
if (duplicate !== void 0) throw new Error(`defineConfig: schema "${target.name}" declares the same version twice, as "${duplicate}" and "${version}"`);
|
|
36
|
+
versions.push(version);
|
|
37
|
+
map.set(target.name, versions);
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
40
|
+
};
|
|
41
|
+
const normalizePath = (raw) => {
|
|
42
|
+
const absolute = raw.startsWith("/");
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const segment of raw.split("/")) {
|
|
45
|
+
if (segment === "" || segment === ".") continue;
|
|
46
|
+
if (segment === "..") {
|
|
47
|
+
if (out.length > 0 && out[out.length - 1] !== "..") out.pop();
|
|
48
|
+
else if (!absolute) out.push(segment);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
out.push(segment);
|
|
52
|
+
}
|
|
53
|
+
return `${absolute ? "/" : ""}${out.join("/")}`;
|
|
54
|
+
};
|
|
55
|
+
const assertUniquePaths = (schemas, catalog) => {
|
|
56
|
+
const seen = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const p of [...schemas.map((target) => target.path), ...catalog.map((entry) => entry.path)]) {
|
|
58
|
+
const normalized = normalizePath(p);
|
|
59
|
+
if (seen.has(normalized)) throw new Error(`defineConfig: output path "${p}" is declared twice`);
|
|
60
|
+
seen.add(normalized);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Validate and assemble a `schemastore.config.ts` value.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* Pure: no IO, no Effect. Identity-with-validation over the input, filling
|
|
68
|
+
* drift defaults, deriving each catalog entry's `versions` from EVERY
|
|
69
|
+
* versioned schema of that name (published or not — the entry is what gets
|
|
70
|
+
* submitted to become published), and branding the result so a loader can
|
|
71
|
+
* recognise a config module's default export. Throws a plain `Error` on a
|
|
72
|
+
* bad input; the CLI wraps it into its typed config-load error. Rejects an
|
|
73
|
+
* output `path` declared twice across schemas and catalog entries, compared
|
|
74
|
+
* after a lexical normalisation (`./`, `..`, trailing `/`); the CLI's loader
|
|
75
|
+
* re-checks on the resolved absolute paths.
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
const defineConfig = (input) => {
|
|
80
|
+
if (!Array.isArray(input.schemas) || input.schemas.length === 0) throw new Error("defineConfig: at least one schema is required");
|
|
81
|
+
const drift = decodeOrThrow(DriftSchema, input.drift ?? {}, "drift block");
|
|
82
|
+
const versions = versionsByName(input.schemas);
|
|
83
|
+
const catalog = (input.catalog ?? []).map((raw) => {
|
|
84
|
+
const config = decodeOrThrow(CatalogConfigSchema, raw, `catalog entry "${String(raw.name)}"`);
|
|
85
|
+
const found = versions.get(config.name);
|
|
86
|
+
if (found === void 0) throw new Error(`defineConfig: catalog entry "${config.name}" matches no versioned schema`);
|
|
87
|
+
return {
|
|
88
|
+
config,
|
|
89
|
+
entry: CatalogEntry.assemble({
|
|
90
|
+
name: config.name,
|
|
91
|
+
description: config.description,
|
|
92
|
+
fileMatch: config.fileMatch,
|
|
93
|
+
baseUrl: config.baseUrl,
|
|
94
|
+
versions: found
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
assertUniquePaths(input.schemas, catalog.map((c) => c.config));
|
|
99
|
+
return {
|
|
100
|
+
[ConfigBrand]: true,
|
|
101
|
+
schemas: input.schemas,
|
|
102
|
+
catalog,
|
|
103
|
+
drift: {
|
|
104
|
+
policy: drift.policy ?? DriftPolicy.defaults.policy,
|
|
105
|
+
onDrift: drift.onDrift ?? DriftPolicy.defaults.onDrift
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Whether a value is a config produced by {@link defineConfig} — the check a
|
|
111
|
+
* loader runs on a config module's default export.
|
|
112
|
+
*
|
|
113
|
+
* @public
|
|
114
|
+
*/
|
|
115
|
+
const isSchemastoreConfig = (value) => typeof value === "object" && value !== null && value[ConfigBrand] === true;
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { defineConfig, isSchemastoreConfig };
|
package/index.d.ts
CHANGED
|
@@ -577,17 +577,16 @@ export declare class InvalidSchemaVersionError extends InvalidSchemaVersionError
|
|
|
577
577
|
get message(): string;
|
|
578
578
|
}
|
|
579
579
|
/**
|
|
580
|
-
* A schema version label: a branded string holding
|
|
581
|
-
*
|
|
582
|
-
*
|
|
580
|
+
* A schema version label: a branded string holding one, two or three
|
|
581
|
+
* dot-separated numeric components — `major`, `major.minor` or
|
|
582
|
+
* `major.minor.patch` — with an optional SemVer prerelease. Build metadata
|
|
583
|
+
* is rejected (see below).
|
|
583
584
|
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* The label round-trips verbatim into file names and catalog `versions`
|
|
590
|
-
* keys; ordering parses it directly (see {@link SchemaVersioning.Order}).
|
|
585
|
+
* This matches SchemaStore's own corpus, which commonly uses partial labels
|
|
586
|
+
* like `agripparc-1.2.json`. The label is preserved VERBATIM — it is the
|
|
587
|
+
* file name and the URL — and a missing component is read as zero only for
|
|
588
|
+
* ordering (see {@link SchemaVersioning.Order}), never rewritten into the
|
|
589
|
+
* label itself.
|
|
591
590
|
*
|
|
592
591
|
* @public
|
|
593
592
|
*/
|
|
@@ -608,9 +607,14 @@ interface CatalogUrls {
|
|
|
608
607
|
/** The catalog `url` — the unversioned file, or the latest versioned file. */
|
|
609
608
|
readonly url: string;
|
|
610
609
|
/**
|
|
611
|
-
* The versioned catalog's `versions` map (label → url), inserted
|
|
612
|
-
*
|
|
613
|
-
*
|
|
610
|
+
* The versioned catalog's `versions` map (label → url), inserted in
|
|
611
|
+
* ascending version order.
|
|
612
|
+
*
|
|
613
|
+
* A bare-major label (`"2"`) is array-index-like, so JavaScript
|
|
614
|
+
* enumerates it FIRST regardless of insertion order — the serialized
|
|
615
|
+
* order of such a key is therefore not meaningful. A two- or
|
|
616
|
+
* three-component label is never integer-like and keeps insertion order
|
|
617
|
+
* through serialization.
|
|
614
618
|
*/
|
|
615
619
|
readonly versions?: Readonly<Record<string, string>>;
|
|
616
620
|
}
|
|
@@ -620,8 +624,10 @@ interface CatalogUrls {
|
|
|
620
624
|
* — SchemaStore's own suffix convention — a `versions` map, and `url`
|
|
621
625
|
* pointing at the latest version).
|
|
622
626
|
*
|
|
623
|
-
* Version labels are
|
|
624
|
-
*
|
|
627
|
+
* Version labels are 1–3 dot-separated numeric components with an optional
|
|
628
|
+
* prerelease; ordering reads a missing component as zero and otherwise
|
|
629
|
+
* follows plain SemVer precedence: `1.10` above `1.9`, `2.0.0-beta` below
|
|
630
|
+
* `2.0.0`.
|
|
625
631
|
*
|
|
626
632
|
* @public
|
|
627
633
|
*/
|
|
@@ -662,6 +668,11 @@ export declare class SchemaVersioning {
|
|
|
662
668
|
* deadlock.
|
|
663
669
|
*/
|
|
664
670
|
static isPinned(version: SchemaVersion): boolean;
|
|
671
|
+
/**
|
|
672
|
+
* The number of dot-separated numeric components in a label's core:
|
|
673
|
+
* `1` for `"1"` (or `"1-beta"`), `2` for `"1.2"`, `3` for `"1.2.3"`.
|
|
674
|
+
*/
|
|
675
|
+
static components(version: SchemaVersion): 1 | 2 | 3;
|
|
665
676
|
/**
|
|
666
677
|
* The version label a change classification calls for. Pure and
|
|
667
678
|
* synchronous; total over validated labels — a non-label input is a wiring
|
|
@@ -674,13 +685,15 @@ export declare class SchemaVersioning {
|
|
|
674
685
|
* declares its own instability; the pipeline's `"block-versioned"`
|
|
675
686
|
* policy uses the same {@link SchemaVersioning.isPinned}, so the gate
|
|
676
687
|
* and the bump agree.
|
|
677
|
-
* -
|
|
678
|
-
*
|
|
679
|
-
*
|
|
688
|
+
* - otherwise → a MINOR bump, preserving the component count
|
|
689
|
+
* ({@link SchemaVersioning.components}): `1.2.3` → `1.3.0`, `1.2` →
|
|
690
|
+
* `1.3`, `0.4` → `0.5`. On a one-component label the only axis IS
|
|
691
|
+
* major: `1` → `2`.
|
|
680
692
|
*
|
|
681
693
|
* The bump's job is to be strictly greater and conspicuous, NOT to encode
|
|
682
694
|
* SemVer compatibility: `DocumentDiff` cannot tell an added optional
|
|
683
695
|
* property from a removed required one, so every contract change reads as
|
|
696
|
+
* breaking — the author bumps MAJOR by hand when they know the change is
|
|
684
697
|
* breaking. Each label is its own file and URL, so an over-bump costs a
|
|
685
698
|
* file; an under-bump would overwrite a pinned document. `next` never
|
|
686
699
|
* introduces a prerelease from a stable input.
|
|
@@ -713,13 +726,14 @@ export declare class SchemaVersioning {
|
|
|
713
726
|
* a contradiction (versioned mode with no versions) and throws — pass
|
|
714
727
|
* `undefined` for the unversioned mode.
|
|
715
728
|
*
|
|
716
|
-
* Labels are inserted in ascending {@link SchemaVersioning.Order}
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
* (as {@link SchemaVersioning.latest} does) is still the
|
|
729
|
+
* Labels are inserted in ascending {@link SchemaVersioning.Order}, but a
|
|
730
|
+
* bare-major label (`"2"`) is array-index-like, so JavaScript enumerates
|
|
731
|
+
* it FIRST regardless of insertion order — the serialized order of the
|
|
732
|
+
* `versions` map is not meaningful when such a label is present. A two-
|
|
733
|
+
* or three-component label is never integer-like and keeps insertion
|
|
734
|
+
* order through serialization. Deriving ordering from the labels
|
|
735
|
+
* themselves (as {@link SchemaVersioning.latest} does) is still the
|
|
736
|
+
* robust read.
|
|
723
737
|
*/
|
|
724
738
|
static catalogUrls(options: {
|
|
725
739
|
readonly baseUrl: string;
|
|
@@ -774,7 +788,10 @@ export declare class CatalogEntry extends CatalogEntry_base {
|
|
|
774
788
|
* Assembles an entry from a catalog identity plus
|
|
775
789
|
* {@link SchemaVersioning.catalogUrls}' inputs: pass `versions` for the
|
|
776
790
|
* versioned mode (the `versions` map and latest-pointing `url` are
|
|
777
|
-
* derived), omit it for the unversioned mode.
|
|
791
|
+
* derived), omit it for the unversioned mode. Throws an `Error` naming
|
|
792
|
+
* both spellings when two labels compare equal under
|
|
793
|
+
* {@link SchemaVersioning.Order} (`1.2` and `1.2.0`): each would be its
|
|
794
|
+
* own key and URL for one document.
|
|
778
795
|
*/
|
|
779
796
|
static assemble(options: {
|
|
780
797
|
readonly name: string;
|
|
@@ -844,6 +861,64 @@ export declare class DocumentLint {
|
|
|
844
861
|
static lint(document: StoreDocument): ReadonlyArray<DocumentLintFinding>;
|
|
845
862
|
}
|
|
846
863
|
//#endregion
|
|
864
|
+
//#region src/DriftPolicy.d.ts
|
|
865
|
+
/**
|
|
866
|
+
* How much change a PUBLISHED schema document may absorb before a build is
|
|
867
|
+
* refused.
|
|
868
|
+
*
|
|
869
|
+
* @remarks
|
|
870
|
+
* - `"strict"` — any content change (`annotations` or `contract`) is drift.
|
|
871
|
+
* - `"semantic"` — only a `contract` change is drift; documentation-only
|
|
872
|
+
* keywords rewrite in place.
|
|
873
|
+
* - `"allow"` — nothing is drift; the document rewrites in place.
|
|
874
|
+
*
|
|
875
|
+
* @public
|
|
876
|
+
*/
|
|
877
|
+
type DriftTolerance = "strict" | "semantic" | "allow";
|
|
878
|
+
/**
|
|
879
|
+
* What a build does when it finds drift: refuse to write anything, or write
|
|
880
|
+
* and warn.
|
|
881
|
+
*
|
|
882
|
+
* @public
|
|
883
|
+
*/
|
|
884
|
+
type OnDrift = "error" | "warn";
|
|
885
|
+
/**
|
|
886
|
+
* The drift settings a config declares and a CLI flag may override.
|
|
887
|
+
*
|
|
888
|
+
* @public
|
|
889
|
+
*/
|
|
890
|
+
interface DriftOptions {
|
|
891
|
+
readonly policy: DriftTolerance;
|
|
892
|
+
readonly onDrift: OnDrift;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* The verdict for one target: write it, or hold it as drift.
|
|
896
|
+
*
|
|
897
|
+
* @public
|
|
898
|
+
*/
|
|
899
|
+
type DriftVerdict = "write" | "drift";
|
|
900
|
+
/**
|
|
901
|
+
* Classifies one target's change against a drift tolerance.
|
|
902
|
+
*
|
|
903
|
+
* @remarks
|
|
904
|
+
* Pure and synchronous. The lifecycle rule is fixed here so no caller
|
|
905
|
+
* relitigates it: an unpublished target is NEVER drift — it is regenerated
|
|
906
|
+
* in place until someone depends on its label. Gate failures (lint warnings,
|
|
907
|
+
* validator findings) are not drift either and are not this module's
|
|
908
|
+
* concern.
|
|
909
|
+
*
|
|
910
|
+
* @public
|
|
911
|
+
*/
|
|
912
|
+
export declare class DriftPolicy {
|
|
913
|
+
private constructor();
|
|
914
|
+
/** `{ policy: "semantic", onDrift: "error" }` — what a config gets when it says nothing. */
|
|
915
|
+
static readonly defaults: DriftOptions;
|
|
916
|
+
static classify(input: {
|
|
917
|
+
readonly published: boolean;
|
|
918
|
+
readonly change: WriteChange;
|
|
919
|
+
}, policy: DriftTolerance): DriftVerdict;
|
|
920
|
+
}
|
|
921
|
+
//#endregion
|
|
847
922
|
//#region src/KeywordFamilies.d.ts
|
|
848
923
|
/**
|
|
849
924
|
* The one owner of the declared non-standard keyword families, in two
|
|
@@ -969,6 +1044,17 @@ export interface SchemaTarget {
|
|
|
969
1044
|
* rewritten in place.
|
|
970
1045
|
*/
|
|
971
1046
|
readonly version?: SchemaVersion;
|
|
1047
|
+
/**
|
|
1048
|
+
* Whether a consumer already depends on this document at this label.
|
|
1049
|
+
*
|
|
1050
|
+
* @remarks
|
|
1051
|
+
* The lifecycle switch the drift policy reads: an unpublished target is
|
|
1052
|
+
* always regenerated in place, a published one is held to the configured
|
|
1053
|
+
* drift tolerance. Defaults to `false`. The library's own
|
|
1054
|
+
* `contractChanges: "block-versioned"` policy keys on a pinned `version`,
|
|
1055
|
+
* not on this flag — the CLI is what reads it.
|
|
1056
|
+
*/
|
|
1057
|
+
readonly published: boolean;
|
|
972
1058
|
/**
|
|
973
1059
|
* Options passed through to {@link StoreDocument.fromSchema} (and, from
|
|
974
1060
|
* there, core's `Schema.toJsonSchemaDocument`).
|
|
@@ -999,20 +1085,23 @@ export declare class SchemaTarget {
|
|
|
999
1085
|
readonly $id: string;
|
|
1000
1086
|
readonly name?: string;
|
|
1001
1087
|
readonly path: string;
|
|
1088
|
+
readonly published?: boolean;
|
|
1002
1089
|
readonly jsonSchema?: Schema.ToJsonSchemaOptions;
|
|
1003
1090
|
}): SchemaTarget;
|
|
1004
1091
|
/**
|
|
1005
1092
|
* Builds a versioned target. `name` is **required** here: versioned
|
|
1006
1093
|
* catalog naming is `name-<version>.json`, so a version without a name
|
|
1007
1094
|
* cannot be resolved — the overload pair makes that unrepresentable
|
|
1008
|
-
* rather than a runtime throw.
|
|
1095
|
+
* rather than a runtime throw. `version` also accepts a plain string
|
|
1096
|
+
* label, parsed via {@link SchemaVersioning.parseResult}.
|
|
1009
1097
|
*/
|
|
1010
1098
|
static make(options: {
|
|
1011
1099
|
readonly schema: Schema.Constraint;
|
|
1012
1100
|
readonly $id: string;
|
|
1013
1101
|
readonly name: string;
|
|
1014
1102
|
readonly path: string;
|
|
1015
|
-
readonly version: SchemaVersion;
|
|
1103
|
+
readonly version: SchemaVersion | string;
|
|
1104
|
+
readonly published?: boolean;
|
|
1016
1105
|
readonly jsonSchema?: Schema.ToJsonSchemaOptions;
|
|
1017
1106
|
}): SchemaTarget;
|
|
1018
1107
|
}
|
|
@@ -1475,5 +1564,81 @@ export declare class SchemaPipeline {
|
|
|
1475
1564
|
static checkOne(target: SchemaTarget, options?: SchemaPipelineOptions): Effect.Effect<PipelineCheckResult, SchemaConversionError | UndeclaredAnnotationKeyError | SchemaValidatorError | CanonicalJsonError | SchemaFileReadError, SchemaFile | SchemaValidator>;
|
|
1476
1565
|
}
|
|
1477
1566
|
//#endregion
|
|
1478
|
-
|
|
1567
|
+
//#region src/SchemastoreConfig.d.ts
|
|
1568
|
+
declare const ConfigBrand: unique symbol;
|
|
1569
|
+
/**
|
|
1570
|
+
* One catalog entry a `schemastore.config.ts` declares: the SchemaStore
|
|
1571
|
+
* `catalog.json` fields plus where to write the assembled entry. The entry's
|
|
1572
|
+
* `versions` and `url` are derived by {@link defineConfig} from every
|
|
1573
|
+
* versioned schema of the same `name`.
|
|
1574
|
+
*
|
|
1575
|
+
* @public
|
|
1576
|
+
*/
|
|
1577
|
+
interface CatalogConfig {
|
|
1578
|
+
readonly name: string;
|
|
1579
|
+
readonly description: string;
|
|
1580
|
+
readonly fileMatch: ReadonlyArray<string>;
|
|
1581
|
+
readonly baseUrl: string;
|
|
1582
|
+
/** Where to write the assembled entry; relative paths are resolved by the loader against the config file's directory. */
|
|
1583
|
+
readonly path: string;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* What a `schemastore.config.ts` hands to {@link defineConfig}: the schema
|
|
1587
|
+
* targets, an optional catalog block and an optional partial drift block.
|
|
1588
|
+
*
|
|
1589
|
+
* @public
|
|
1590
|
+
*/
|
|
1591
|
+
interface SchemastoreConfigInput {
|
|
1592
|
+
readonly schemas: ReadonlyArray<SchemaTarget>;
|
|
1593
|
+
readonly catalog?: ReadonlyArray<CatalogConfig>;
|
|
1594
|
+
readonly drift?: Partial<DriftOptions>;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* A validated catalog declaration paired with the `CatalogEntry` assembled
|
|
1598
|
+
* from it.
|
|
1599
|
+
*
|
|
1600
|
+
* @public
|
|
1601
|
+
*/
|
|
1602
|
+
interface CatalogTarget {
|
|
1603
|
+
readonly config: CatalogConfig;
|
|
1604
|
+
readonly entry: CatalogEntry;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* The validated, defaults-filled config {@link defineConfig} answers and the
|
|
1608
|
+
* CLI consumes. Recognisable via {@link isSchemastoreConfig}.
|
|
1609
|
+
*
|
|
1610
|
+
* @public
|
|
1611
|
+
*/
|
|
1612
|
+
interface SchemastoreConfig {
|
|
1613
|
+
readonly [ConfigBrand]: true;
|
|
1614
|
+
readonly schemas: ReadonlyArray<SchemaTarget>;
|
|
1615
|
+
readonly catalog: ReadonlyArray<CatalogTarget>;
|
|
1616
|
+
readonly drift: DriftOptions;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Validate and assemble a `schemastore.config.ts` value.
|
|
1620
|
+
*
|
|
1621
|
+
* @remarks
|
|
1622
|
+
* Pure: no IO, no Effect. Identity-with-validation over the input, filling
|
|
1623
|
+
* drift defaults, deriving each catalog entry's `versions` from EVERY
|
|
1624
|
+
* versioned schema of that name (published or not — the entry is what gets
|
|
1625
|
+
* submitted to become published), and branding the result so a loader can
|
|
1626
|
+
* recognise a config module's default export. Throws a plain `Error` on a
|
|
1627
|
+
* bad input; the CLI wraps it into its typed config-load error. Rejects an
|
|
1628
|
+
* output `path` declared twice across schemas and catalog entries, compared
|
|
1629
|
+
* after a lexical normalisation (`./`, `..`, trailing `/`); the CLI's loader
|
|
1630
|
+
* re-checks on the resolved absolute paths.
|
|
1631
|
+
*
|
|
1632
|
+
* @public
|
|
1633
|
+
*/
|
|
1634
|
+
export declare const defineConfig: (input: SchemastoreConfigInput) => SchemastoreConfig;
|
|
1635
|
+
/**
|
|
1636
|
+
* Whether a value is a config produced by {@link defineConfig} — the check a
|
|
1637
|
+
* loader runs on a config module's default export.
|
|
1638
|
+
*
|
|
1639
|
+
* @public
|
|
1640
|
+
*/
|
|
1641
|
+
export declare const isSchemastoreConfig: (value: unknown) => value is SchemastoreConfig;
|
|
1642
|
+
//#endregion
|
|
1643
|
+
export type { CanonicalJsonError, CanonicalJsonOptions, CatalogConfig, CatalogTarget, CatalogUrls, CheckResult, ContractChangePolicy, DriftOptions, DriftTolerance, DriftVerdict, OnDrift, PipelineCheckResult, PipelineResult, SchemaChange, SchemaFileShape, SchemaPipelineOptions, SchemaValidatorOptions, SchemaValidatorShape, SchemaWriteOptions, SchemastoreConfig, SchemastoreConfigInput, StoreDocumentOptions, WriteChange, WriteOutcome, WriteResult };
|
|
1479
1644
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -4,10 +4,12 @@ import { CatalogEntry, CatalogLintFinding } from "./CatalogEntry.js";
|
|
|
4
4
|
import { KeywordFamilies } from "./KeywordFamilies.js";
|
|
5
5
|
import { DocumentDiff } from "./DocumentDiff.js";
|
|
6
6
|
import { DocumentLint, DocumentLintFinding } from "./DocumentLint.js";
|
|
7
|
+
import { DriftPolicy } from "./DriftPolicy.js";
|
|
7
8
|
import { SchemaFile, SchemaFileNotFoundError, SchemaFileReadError, SchemaFileWriteError } from "./SchemaFile.js";
|
|
8
9
|
import { SchemaValidator, SchemaValidatorError, ValidationFinding } from "./SchemaValidator.js";
|
|
9
10
|
import { DRAFT_07_META_SCHEMA, SchemaConversionError, StoreDocument, UndeclaredAnnotationKeyError } from "./StoreDocument.js";
|
|
10
11
|
import { ContractChangeTarget, PipelineFinding, SchemaContractChangeError, SchemaGateError, SchemaPipeline } from "./SchemaPipeline.js";
|
|
12
|
+
import { defineConfig, isSchemastoreConfig } from "./SchemastoreConfig.js";
|
|
11
13
|
import { SchemaTarget } from "./SchemaTarget.js";
|
|
12
14
|
|
|
13
|
-
export { CanonicalJson, CatalogEntry, CatalogLintFinding, ContractChangeTarget, DRAFT_07_META_SCHEMA, DocumentDiff, DocumentLint, DocumentLintFinding, InvalidSchemaVersionError, JsonDepthExceededError, KeywordFamilies, NonJsonValueError, PipelineFinding, SchemaContractChangeError, SchemaConversionError, SchemaFile, SchemaFileNotFoundError, SchemaFileReadError, SchemaFileWriteError, SchemaGateError, SchemaPipeline, SchemaTarget, SchemaValidator, SchemaValidatorError, SchemaVersion, SchemaVersioning, StoreDocument, UndeclaredAnnotationKeyError, ValidationFinding };
|
|
15
|
+
export { CanonicalJson, CatalogEntry, CatalogLintFinding, ContractChangeTarget, DRAFT_07_META_SCHEMA, DocumentDiff, DocumentLint, DocumentLintFinding, DriftPolicy, InvalidSchemaVersionError, JsonDepthExceededError, KeywordFamilies, NonJsonValueError, PipelineFinding, SchemaContractChangeError, SchemaConversionError, SchemaFile, SchemaFileNotFoundError, SchemaFileReadError, SchemaFileWriteError, SchemaGateError, SchemaPipeline, SchemaTarget, SchemaValidator, SchemaValidatorError, SchemaVersion, SchemaVersioning, StoreDocument, UndeclaredAnnotationKeyError, ValidationFinding, defineConfig, isSchemastoreConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/schemastore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Build, validate, version and publish SchemaStore-shaped Draft-07 JSON Schema documents from Effect Schema sources: document assembly, ajv strict-mode validation, structural lints, catalog entries, canonical JSON and a content-comparing emit pipeline.",
|
|
6
6
|
"keywords": [
|