@effected/schemastore-cli 0.9.1 → 0.10.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/ConfigLoader.js +13 -0
- package/README.md +2 -2
- package/Report.js +9 -1
- package/Runner.js +5 -13
- package/cli/execute.js +48 -12
- package/cli/program.js +2 -1
- package/main.js +1 -1
- package/package.json +3 -3
package/ConfigLoader.js
CHANGED
|
@@ -41,6 +41,14 @@ const describeMalformedTarget = (schemas) => {
|
|
|
41
41
|
if (!(record !== void 0 && Schema.isSchema(record.schema) && typeof record.$id === "string" && typeof record.path === "string" && typeof record.published === "boolean")) return `schemas[${index}] is not a SchemaTarget (missing schema/$id/path/published)`;
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
+
const describeMalformedCatalog = (catalog) => {
|
|
45
|
+
if (!Array.isArray(catalog)) return "catalog is not an array";
|
|
46
|
+
for (const [index, entry] of catalog.entries()) {
|
|
47
|
+
const record = typeof entry === "object" && entry !== null ? entry : void 0;
|
|
48
|
+
const config = record !== void 0 && typeof record.config === "object" && record.config !== null ? record.config : void 0;
|
|
49
|
+
if (config === void 0 || typeof config.path !== "string") return `catalog[${index}] is not a catalog entry (missing config.path)`;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
44
52
|
const describeDuplicatePath = (config) => {
|
|
45
53
|
const seen = /* @__PURE__ */ new Set();
|
|
46
54
|
const paths = [...config.schemas.map((target) => target.path), ...config.catalog.map((c) => c.config.path)];
|
|
@@ -141,6 +149,11 @@ var ConfigLoader = class ConfigLoader {
|
|
|
141
149
|
path: configPath,
|
|
142
150
|
reason: malformed
|
|
143
151
|
}));
|
|
152
|
+
const malformedCatalog = describeMalformedCatalog(exported.catalog);
|
|
153
|
+
if (malformedCatalog !== void 0) return yield* Effect.fail(new ConfigLoadError({
|
|
154
|
+
path: configPath,
|
|
155
|
+
reason: malformedCatalog
|
|
156
|
+
}));
|
|
144
157
|
const directory = path.dirname(configPath);
|
|
145
158
|
const config = yield* ConfigLoader.resolvePaths(exported, directory);
|
|
146
159
|
const duplicate = describeDuplicatePath(config);
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ schemastore check [config] [--drift=strict|semantic|allow] [--on-drift=error|war
|
|
|
90
90
|
|
|
91
91
|
- `build` generates every schema, runs the gates (structural lints and ajv strict mode), applies the drift policy, and writes what passes — content-compared, so unchanged files are untouched — along with each catalog entry.
|
|
92
92
|
- `check` is the identical walk with no writes: it reports what `build` would do under the same flags and exits under the same conditions. It is the CI gate, so it also fails (exit `1`) whenever a build would write anything — a committed schema or catalog entry that differs from what the config generates, or is missing, is stale; run `schemastore build` and commit the result.
|
|
93
|
-
- `--drift` and `--on-drift` override the config's `drift` block for one run; `--force` is sugar for `--drift=allow
|
|
93
|
+
- `--drift` and `--on-drift` override the config's `drift` block for one run; `--force` is sugar for `--drift=allow` and nothing else — combined with an explicit non-`allow` `--drift` it is a usage error (exit 64), not a precedence question; `--force --drift=allow` is accepted.
|
|
94
94
|
- `--format=json` emits one JSON document on stdout (config path, per-schema outcome, per-catalog-entry outcome, effective drift policy and its source); human text moves to stderr.
|
|
95
95
|
- When `GITHUB_STEP_SUMMARY` is set, both commands append a markdown summary table.
|
|
96
96
|
|
|
@@ -101,7 +101,7 @@ An unpublished schema is never drift: a contract change at a pinned but unpublis
|
|
|
101
101
|
| code | meaning |
|
|
102
102
|
| ---- | -------------------------------------------------------------------------- |
|
|
103
103
|
| 0 | success, including drift under `onDrift: warn` |
|
|
104
|
-
| 1 | drift under `onDrift: error`, a gate failure, or — for `check` — any document `build` would write |
|
|
104
|
+
| 1 | drift under `onDrift: error` (the error lists one line per drifting schema: `$id`, change, current and next version), a gate failure, or — for `check` — any document `build` would write |
|
|
105
105
|
| 2 | config not found, failed to load, or failed `SchemastoreConfig` validation |
|
|
106
106
|
| 3 | infrastructure failure |
|
|
107
107
|
| 64 | usage error |
|
package/Report.js
CHANGED
|
@@ -40,7 +40,15 @@ const tableRow = (columns) => `| ${columns.join(" | ")} |`;
|
|
|
40
40
|
*/
|
|
41
41
|
var Report = class {
|
|
42
42
|
constructor() {}
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* stdout lines.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* The summary's `drift` count is the number of schemas whose VERDICT is
|
|
48
|
+
* `"drift"`, independent of `written`/`unchanged`: under
|
|
49
|
+
* `onDrift: "warn"` a drifting schema is written AND counted as drift,
|
|
50
|
+
* so the four counts need not sum to the schema total.
|
|
51
|
+
*/
|
|
44
52
|
static human(report) {
|
|
45
53
|
const lines = [];
|
|
46
54
|
for (const schema of report.schemas) lines.push(...schemaLines(schema, report));
|
package/Runner.js
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
1
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
2
|
import { CanonicalJson, CatalogEntry, DriftPolicy, SchemaPipeline, SchemaVersioning } from "@effected/schemastore";
|
|
3
3
|
|
|
4
4
|
//#region src/Runner.ts
|
|
5
5
|
const pipelineOptions = { contractChanges: "allow" };
|
|
6
6
|
const catalogText = (target) => CanonicalJson.serialize(Schema.encodeSync(CatalogEntry)(target.entry));
|
|
7
|
-
const
|
|
8
|
-
if (a === b) return true;
|
|
9
|
-
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
|
|
10
|
-
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
11
|
-
const left = a;
|
|
12
|
-
const right = b;
|
|
13
|
-
const keys = Object.keys(left);
|
|
14
|
-
return keys.length === Object.keys(right).length && keys.every((k) => Object.hasOwn(right, k) && jsonEqual(left[k], right[k]));
|
|
15
|
-
};
|
|
16
|
-
const sameJson = (existing, text) => {
|
|
7
|
+
const parsesEqual = (existing, text) => {
|
|
17
8
|
try {
|
|
18
|
-
return
|
|
9
|
+
return CanonicalJson.equals(JSON.parse(existing), JSON.parse(text));
|
|
19
10
|
} catch {
|
|
20
11
|
return false;
|
|
21
12
|
}
|
|
@@ -88,7 +79,8 @@ var Runner = class {
|
|
|
88
79
|
for (const entry of config.catalog) {
|
|
89
80
|
const { name, path: file } = entry.config;
|
|
90
81
|
const text = yield* catalogText(entry);
|
|
91
|
-
|
|
82
|
+
const existing = yield* fs.readFileString(file).pipe(Effect.map(Option.some), Effect.catchIf((error) => error.reason._tag === "NotFound", () => Effect.succeed(Option.none())));
|
|
83
|
+
if (Option.isSome(existing) && parsesEqual(existing.value, text)) catalog.push({
|
|
92
84
|
name,
|
|
93
85
|
path: file,
|
|
94
86
|
outcome: "unchanged"
|
package/cli/execute.js
CHANGED
|
@@ -7,15 +7,30 @@ import { Console, Effect, Option, Schema } from "effect";
|
|
|
7
7
|
import { SchemaFile, SchemaValidator } from "@effected/schemastore";
|
|
8
8
|
|
|
9
9
|
//#region src/cli/execute.ts
|
|
10
|
+
const DriftedSchema = Schema.Struct({
|
|
11
|
+
$id: Schema.String,
|
|
12
|
+
change: Schema.Literals([
|
|
13
|
+
"none",
|
|
14
|
+
"created",
|
|
15
|
+
"annotations",
|
|
16
|
+
"contract"
|
|
17
|
+
]),
|
|
18
|
+
version: Schema.optionalKey(Schema.String),
|
|
19
|
+
nextVersion: Schema.optionalKey(Schema.String)
|
|
20
|
+
});
|
|
10
21
|
/**
|
|
11
22
|
* A published schema drifted under `onDrift: "error"`, so nothing was
|
|
12
23
|
* written. Exit `1`.
|
|
13
24
|
*
|
|
14
25
|
* @public
|
|
15
26
|
*/
|
|
16
|
-
var DriftError = class extends Schema.TaggedError()("DriftError", {
|
|
27
|
+
var DriftError = class extends Schema.TaggedError()("DriftError", { drifted: Schema.Array(DriftedSchema) }) {
|
|
28
|
+
get count() {
|
|
29
|
+
return this.drifted.length;
|
|
30
|
+
}
|
|
17
31
|
get message() {
|
|
18
|
-
|
|
32
|
+
const lines = this.drifted.map((s) => ` ${s.$id}: ${s.change}${s.version !== void 0 ? ` at published ${s.version}` : ""}${s.nextVersion !== void 0 ? ` → suggest ${s.nextVersion}` : ""}`);
|
|
33
|
+
return `${this.count} published schema(s) drifted; nothing was written.\n${lines.join("\n")}\nBump the drifting versions in the config, or re-run with --force to write anyway.`;
|
|
19
34
|
}
|
|
20
35
|
};
|
|
21
36
|
/**
|
|
@@ -40,6 +55,18 @@ var StaleError = class extends Schema.TaggedError()("StaleError", { count: Schem
|
|
|
40
55
|
return `${this.count} document(s) are stale; run \`schemastore build\` and commit the result.`;
|
|
41
56
|
}
|
|
42
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* `--force` (shorthand for `--drift=allow`) was combined with an explicit
|
|
60
|
+
* `--drift` that is not `allow`. Contradictory, so refused as a usage
|
|
61
|
+
* error rather than silently resolving to `allow`. Exit `64`.
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
var ConflictingFlagsError = class extends Schema.TaggedError()("ConflictingFlagsError", { policy: Schema.String }) {
|
|
66
|
+
get message() {
|
|
67
|
+
return `--force conflicts with --drift=${this.policy}: --force means --drift=allow`;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
43
70
|
const effectiveDrift = (configured, input) => {
|
|
44
71
|
return {
|
|
45
72
|
policy: input.force ? "allow" : Option.getOrElse(input.drift, () => configured.policy),
|
|
@@ -58,17 +85,21 @@ const emit = Effect.fn("schemastore.emit")(function* (report, format) {
|
|
|
58
85
|
* Run one `build` or `check`.
|
|
59
86
|
*
|
|
60
87
|
* @remarks
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* `
|
|
88
|
+
* Before anything is loaded, `--force` combined with an explicit `--drift`
|
|
89
|
+
* other than `allow` short-circuits with `ConflictingFlagsError` at exit
|
|
90
|
+
* `64` — a usage error, not a run outcome. Otherwise loads the config,
|
|
91
|
+
* applies the flag overrides, runs the shared walk, emits the report in the
|
|
92
|
+
* requested format, appends the step summary, and fails typed —
|
|
93
|
+
* `GateError`, then `DriftError`, then (for `check` only) `StaleError`,
|
|
94
|
+
* each carrying exit `1` — when the report says the run refused to write
|
|
95
|
+
* or, under `check`, that a build would write. `SchemaFile` is built here
|
|
96
|
+
* over the environment's `FileSystem`; the validator is `deps.validator` or
|
|
97
|
+
* the real engine.
|
|
68
98
|
*
|
|
69
99
|
* @public
|
|
70
100
|
*/
|
|
71
101
|
const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
|
|
102
|
+
if (input.force && Option.isSome(input.drift) && input.drift.value !== "allow") return yield* Effect.fail(CliRuntime.reported(new ConflictingFlagsError({ policy: input.drift.value }), 64));
|
|
72
103
|
const loaded = yield* ConfigLoader.load({
|
|
73
104
|
cwd: deps.cwd,
|
|
74
105
|
...Option.isSome(input.config) ? { explicit: input.config.value } : {},
|
|
@@ -88,8 +119,13 @@ const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
|
|
|
88
119
|
return yield* Effect.fail(CliRuntime.reported(new GateError({ count }), 1));
|
|
89
120
|
}
|
|
90
121
|
if (report.drifted && drift.onDrift === "error") {
|
|
91
|
-
const
|
|
92
|
-
|
|
122
|
+
const drifted = report.schemas.filter((schema) => schema.verdict === "drift").map((schema) => ({
|
|
123
|
+
$id: schema.$id,
|
|
124
|
+
change: schema.change,
|
|
125
|
+
...schema.version !== void 0 ? { version: schema.version } : {},
|
|
126
|
+
...schema.nextVersion !== void 0 ? { nextVersion: schema.nextVersion } : {}
|
|
127
|
+
}));
|
|
128
|
+
return yield* Effect.fail(CliRuntime.reported(new DriftError({ drifted }), 1));
|
|
93
129
|
}
|
|
94
130
|
if (mode === "check") {
|
|
95
131
|
const count = report.schemas.filter((schema) => schema.outcome === "would-write").length + report.catalog.filter((entry) => entry.outcome === "would-write").length;
|
|
@@ -98,4 +134,4 @@ const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
|
|
|
98
134
|
});
|
|
99
135
|
|
|
100
136
|
//#endregion
|
|
101
|
-
export { DriftError, GateError, StaleError, execute };
|
|
137
|
+
export { ConflictingFlagsError, DriftError, GateError, StaleError, execute };
|
package/cli/program.js
CHANGED
|
@@ -28,7 +28,8 @@ const trimLoadError = Effect.fn("schemastore.trimLoadError")(function* (error) {
|
|
|
28
28
|
* Fails with the marked error the runtime maps to the exit code:
|
|
29
29
|
* `ShowHelp` is `64` with parse errors and `0` without (help itself was
|
|
30
30
|
* already rendered); `ConfigNotFoundError` / `ConfigLoadError` are `2`;
|
|
31
|
-
* `
|
|
31
|
+
* `ConflictingFlagsError` arrives already marked `64`; `DriftError` /
|
|
32
|
+
* `GateError` / `StaleError` arrive already marked `1`.
|
|
32
33
|
*
|
|
33
34
|
* @public
|
|
34
35
|
*/
|
package/main.js
CHANGED
|
@@ -15,7 +15,7 @@ const render = (error) => CliError.isCliError(error) && error._tag === "ShowHelp
|
|
|
15
15
|
const main = () => {
|
|
16
16
|
const run = program(process.argv.slice(2), {
|
|
17
17
|
cwd: process.cwd(),
|
|
18
|
-
version: "0.
|
|
18
|
+
version: "0.10.0"
|
|
19
19
|
}).pipe(Effect.provide(NodeServices.layer), CliRuntime.reportFailures({
|
|
20
20
|
exitCode: 3,
|
|
21
21
|
render
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/schemastore-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The schemastore command: build and check SchemaStore-shaped JSON Schema documents from a schemastore.config.ts, with a per-schema published flag and a drift policy.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
39
|
-
"@effected/cli": "^0.5.
|
|
39
|
+
"@effected/cli": "^0.5.1",
|
|
40
40
|
"jiti": "^2.6.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@effected/schemastore": "0.
|
|
43
|
+
"@effected/schemastore": "0.10.0",
|
|
44
44
|
"effect": "4.0.0-rc.115"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|