@effected/schemastore-cli 0.11.0 → 0.13.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/AjvValidator.js +103 -0
- package/ConfigLoader.js +2 -2
- package/README.md +87 -42
- package/Report.js +5 -0
- package/Runner.js +132 -8
- package/cli/commands/check.js +5 -2
- package/cli/execute.js +25 -9
- package/cli/program.js +1 -1
- package/index.d.ts +56 -0
- package/index.js +3 -0
- package/main.js +2 -2
- package/package.json +9 -2
- package/tsdoc-metadata.json +11 -0
package/AjvValidator.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { KeywordFamilies, SchemaValidator, SchemaValidatorError, ValidationFinding } from "@effected/schemastore";
|
|
2
|
+
import { Ajv } from "ajv";
|
|
3
|
+
import ajvFormats from "ajv-formats";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/AjvValidator.ts
|
|
7
|
+
const addFormats = ajvFormats.default;
|
|
8
|
+
const MAX_KEYWORD_WALK_DEPTH = 256;
|
|
9
|
+
const collectDeclaredKeywords = (node, into, depth) => {
|
|
10
|
+
if (depth >= MAX_KEYWORD_WALK_DEPTH || typeof node !== "object" || node === null) return;
|
|
11
|
+
if (Array.isArray(node)) {
|
|
12
|
+
for (const element of node) collectDeclaredKeywords(element, into, depth + 1);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
for (const [key, value] of Object.entries(node)) {
|
|
16
|
+
if (KeywordFamilies.isDeclared(key)) {
|
|
17
|
+
into.add(key);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
collectDeclaredKeywords(value, into, depth + 1);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const findingFromAjvError = (error) => ValidationFinding.make({
|
|
24
|
+
path: error.instancePath,
|
|
25
|
+
message: error.message ?? "schema is not valid",
|
|
26
|
+
keyword: error.keyword
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* The shipped `SchemaValidator` implementation: ajv strict mode over the
|
|
30
|
+
* Draft-07 meta-schema, with every declared `KeywordFamilies` keyword and
|
|
31
|
+
* the standard `ajv-formats` vocabulary registered.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* Lives in the CLI, not the library, so `ajv` is a cost only the command
|
|
35
|
+
* pays: `@effected/schemastore` owns the `SchemaValidator` contract
|
|
36
|
+
* and its doubles, an application that imports it at runtime never pulls an
|
|
37
|
+
* engine, and the `schemastore` command composes this layer at its edge. It
|
|
38
|
+
* is exported for a program that drives `SchemaPipeline` itself and wants
|
|
39
|
+
* the same verdict the command gives.
|
|
40
|
+
*
|
|
41
|
+
* `validate` checks the document against the Draft-07 meta-schema and then
|
|
42
|
+
* compiles it, reporting BOTH as `ValidationFinding` values: meta-schema
|
|
43
|
+
* failures keep ajv's structured `instancePath` and `keyword`, while a
|
|
44
|
+
* rejection ajv raises by *throwing* becomes a root-pathed finding — both a
|
|
45
|
+
* strict-mode compile failure and a declared keyword whose NAME ajv's own
|
|
46
|
+
* grammar (`/^[a-z_$][a-z0-9_$:-]*$/i`) refuses, such as an `x-ai-*` key
|
|
47
|
+
* carrying a dot or a space. The error channel stays reserved for the engine
|
|
48
|
+
* failing as a mechanism (`SchemaValidatorError`).
|
|
49
|
+
*
|
|
50
|
+
* `strict` defaults to `true` — SchemaStore's gate. Each call builds its own
|
|
51
|
+
* ajv instance, so documents sharing an `$id` never collide. Registering the
|
|
52
|
+
* declared families keeps the engine's verdict consistent with
|
|
53
|
+
* `DocumentLint`'s through the same predicate, so the two cannot drift; the
|
|
54
|
+
* plugin's `formatMaximum` / `formatMinimum` limit keywords are deliberately
|
|
55
|
+
* NOT registered, because the lint answers those as unknown keywords.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { SchemaValidator } from "@effected/schemastore";
|
|
60
|
+
* import { AjvValidator } from "@effected/schemastore-cli";
|
|
61
|
+
* import { Effect } from "effect";
|
|
62
|
+
*
|
|
63
|
+
* const program = Effect.gen(function* () {
|
|
64
|
+
* const validator = yield* SchemaValidator;
|
|
65
|
+
* return yield* validator.validate({ type: "object" });
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* Effect.runPromise(Effect.provide(program, AjvValidator.layer));
|
|
69
|
+
* // => []
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
var AjvValidator = class {
|
|
75
|
+
constructor() {}
|
|
76
|
+
/** The engine, as a `SchemaValidator` layer. */
|
|
77
|
+
static layer = Layer.succeed(SchemaValidator, { validate: (document, options) => Effect.try({
|
|
78
|
+
try: () => {
|
|
79
|
+
const ajv = new Ajv({
|
|
80
|
+
strict: options?.strict ?? true,
|
|
81
|
+
allErrors: true
|
|
82
|
+
});
|
|
83
|
+
addFormats(ajv, { keywords: false });
|
|
84
|
+
const declared = /* @__PURE__ */ new Set();
|
|
85
|
+
collectDeclaredKeywords(document, declared, 0);
|
|
86
|
+
try {
|
|
87
|
+
for (const keyword of declared) ajv.addKeyword({ keyword });
|
|
88
|
+
if (!ajv.validateSchema(document)) return (ajv.errors ?? []).map(findingFromAjvError);
|
|
89
|
+
ajv.compile(document);
|
|
90
|
+
} catch (cause) {
|
|
91
|
+
return [ValidationFinding.make({
|
|
92
|
+
path: "",
|
|
93
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
94
|
+
})];
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
},
|
|
98
|
+
catch: (cause) => SchemaValidatorError.make({ cause })
|
|
99
|
+
}) });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { AjvValidator };
|
package/ConfigLoader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Effect, FileSystem, Path, Predicate, Schema } from "effect";
|
|
2
1
|
import { isSchemastoreConfig } from "@effected/schemastore";
|
|
2
|
+
import { Effect, FileSystem, Path, Predicate, Schema } from "effect";
|
|
3
3
|
import { createJiti } from "jiti";
|
|
4
4
|
|
|
5
5
|
//#region src/ConfigLoader.ts
|
|
@@ -45,7 +45,7 @@ const describeMalformed = (config) => {
|
|
|
45
45
|
if (!Predicate.isObject(schema) || typeof schema.name !== "string" || !Array.isArray(schema.frozen) || typeof schema.drift !== "string") return `schemas[${index}] is not a resolved schema (missing name/target/frozen/drift)`;
|
|
46
46
|
if (!isTargetShaped(schema.target)) return `schemas[${index}].target is not a SchemaTarget (missing schema/$id/path/published)`;
|
|
47
47
|
if (schema.catalog !== void 0 && !isCatalogEntryShaped(schema.catalog)) return `schemas[${index}].catalog is not a catalog entry (missing name/description/fileMatch/url)`;
|
|
48
|
-
for (const [j, frozen] of schema.frozen.entries()) if (!Predicate.isObject(frozen) || typeof frozen.version !== "string" || typeof frozen.path !== "string" || typeof frozen.url !== "string") return `schemas[${index}].frozen[${j}] is not a frozen version (missing version/path/url)`;
|
|
48
|
+
for (const [j, frozen] of schema.frozen.entries()) if (!Predicate.isObject(frozen) || typeof frozen.version !== "string" || typeof frozen.path !== "string" || typeof frozen.$id !== "string" || typeof frozen.url !== "string") return `schemas[${index}].frozen[${j}] is not a frozen version (missing version/path/$id/url)`;
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
const describeDuplicatePath = (config) => {
|
package/README.md
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
The `schemastore` command: build and check SchemaStore-shaped JSON Schema documents from a `schemastore.config.ts`. It is the
|
|
9
|
-
|
|
10
|
-
It is not a library: nothing is importable from it. Every type a config file needs comes from `@effected/schemastore`, which the CLI declares as a peer so your config and the pipeline share one `effect` and one `@effected/schemastore` instance.
|
|
8
|
+
The `schemastore` command: build and check SchemaStore-shaped JSON Schema documents from a `schemastore.config.ts`. It is the companion to [`@effected/schemastore`](https://www.npmjs.com/package/@effected/schemastore), which owns the pipeline and every type a config needs; this package ships the plumbing every consumer used to write by hand — the config loader, the drift policy, the frozen-label checks, the catalog file, exit codes, a GitHub step summary — once, as a `bin`. It is also where the validation engine lives: `AjvValidator`, ajv in strict mode, composed by the command and exported for a program that drives the pipeline itself, so the library stays free of ajv.
|
|
11
9
|
|
|
12
10
|
> **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
|
|
13
11
|
> development against a single pinned Effect v4 prerelease. Packages graduate to
|
|
@@ -23,57 +21,90 @@ It is not a library: nothing is importable from it. Every type a config file nee
|
|
|
23
21
|
|
|
24
22
|
## Install
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
The two packages release together at one version. The library is a regular dependency (the application reads its schema's identity from it at runtime); the command is a devDependency.
|
|
27
25
|
|
|
28
26
|
```bash
|
|
29
|
-
|
|
27
|
+
pnpm add @effected/schemastore effect
|
|
28
|
+
pnpm add -D @effected/schemastore-cli
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
pnpm add -D @effected/schemastore-cli @effected/schemastore effect
|
|
34
|
-
```
|
|
31
|
+
`effect` and `@effected/schemastore` are peers of the command, so your config and the pipeline share one instance of each.
|
|
35
32
|
|
|
36
33
|
## Configure
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
Declare each schema's hosted identity once, in the application, next to the schema — the application writes `$schema` from it, and the config hands the same value to `defineConfig` so nothing is spelled twice:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// src/schema/output.ts
|
|
39
|
+
import { HostedSchema } from "@effected/schemastore";
|
|
40
|
+
import { Schema } from "effect";
|
|
41
|
+
|
|
42
|
+
export const OUTPUT_SCHEMA_VERSION = "5.2";
|
|
43
|
+
|
|
44
|
+
export const OutputSchemaIdentity = HostedSchema.github({
|
|
45
|
+
repo: "savvy-web/silk-release-action",
|
|
46
|
+
path: "schemas",
|
|
47
|
+
name: "silk-release-action.output",
|
|
48
|
+
versions: [OUTPUT_SCHEMA_VERSION],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const ReleaseOutput = Schema.Struct({
|
|
52
|
+
$schema: Schema.Literal(OutputSchemaIdentity.$id),
|
|
53
|
+
status: Schema.Literals(["released", "skipped"]),
|
|
54
|
+
});
|
|
55
|
+
```
|
|
39
56
|
|
|
40
57
|
```ts
|
|
58
|
+
// schemastore.config.ts (also .mts, .js, .mjs; found by walking upward, or passed as the positional argument)
|
|
41
59
|
import { defineConfig } from "@effected/schemastore";
|
|
42
|
-
import {
|
|
60
|
+
import { OutputSchemaIdentity, ReleaseOutput } from "./src/schema/output.js";
|
|
43
61
|
|
|
44
62
|
export default defineConfig({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
schema: OkfitConfig,
|
|
50
|
-
versions: ["1.0"],
|
|
51
|
-
published: true,
|
|
52
|
-
catalog: { description: "okfit configuration", fileMatch: ["okfit.toml", ".okfit.toml"] },
|
|
63
|
+
// Relative paths resolve against this file's directory.
|
|
64
|
+
outputDir: "schemas",
|
|
65
|
+
schemas: {
|
|
66
|
+
[OutputSchemaIdentity.name]: { schema: ReleaseOutput, hosted: OutputSchemaIdentity },
|
|
53
67
|
},
|
|
54
|
-
},
|
|
55
68
|
});
|
|
56
69
|
```
|
|
57
70
|
|
|
58
|
-
A second label is appended to `versions` only once the first is published
|
|
59
|
-
and its file already exists on disk — see "The lifecycle" in the
|
|
60
|
-
`building-schemastore-schemas` skill's `drift-and-versioning.md` reference.
|
|
61
|
-
A first-run config should declare a single label; naming an extra one before
|
|
62
|
-
its file exists fails the build with `FrozenVersionMissingError`.
|
|
63
|
-
|
|
64
|
-
`schemas` is keyed by file base name — the key IS the schema's `name`, and `$id`, the write `path` and every catalog URL derive from it, `outputDir` and `baseUrl`; there is no `$id` override. `versions` lists every label the catalog advertises; `current` (default: the newest) is the one generated at `path`/`$id`, and every other label becomes a **frozen** file the CLI verifies still exists on disk but never regenerates — advertising a frozen label with nothing on disk fails the build before anything is written. `published` (default `false`) marks a version other people already depend on. `baseUrl: "schemastore"` expands `$id` to `https://json.schemastore.org/…` and the catalog URL to `https://www.schemastore.org/…`; any other value is one `https://` base for both. `outputDir` and `onDrift` are top-level only; `baseUrl` and `drift` are top-level defaults an entry may override. `catalog` is required under `baseUrl: "schemastore"` and optional under a custom host. Every schema's declared `catalog` entry lands in ONE file at `catalogPath` (default `<outputDir>/catalog.json`) — never one file per schema.
|
|
65
|
-
|
|
66
|
-
Then add two scripts:
|
|
67
|
-
|
|
68
71
|
```json
|
|
69
72
|
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
"scripts": {
|
|
74
|
+
"schema:build": "schemastore build",
|
|
75
|
+
"schema:check": "schemastore check"
|
|
76
|
+
}
|
|
74
77
|
}
|
|
75
78
|
```
|
|
76
79
|
|
|
80
|
+
`schema:build` writes `schemas/5.2/silk-release-action.output-5.2.json`; `schema:check` in CI fails whenever a build would write anything. Bumping the version is one constant, and once a label has shipped it stays in `versions` as a frozen file the command verifies but never regenerates.
|
|
81
|
+
|
|
82
|
+
A schema published to SchemaStore itself uses `HostedSchema.schemastore` and declares its catalog entry; the command assembles every entry into one `catalog.json`:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
outputDir: "schemas",
|
|
87
|
+
schemas: {
|
|
88
|
+
okfit: {
|
|
89
|
+
schema: OkfitConfig,
|
|
90
|
+
hosted: HostedSchema.schemastore({ name: "okfit", versions: ["1.0"] }),
|
|
91
|
+
published: true,
|
|
92
|
+
catalog: { description: "okfit configuration", fileMatch: ["okfit.toml", ".okfit.toml"] },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### The entry contract
|
|
99
|
+
|
|
100
|
+
- The key IS the schema's `name`; `$id`, the write `path` and every catalog URL derive from it and the identity. There is no `$id` override. With `hosted`, the key must equal `hosted.name` and `baseUrl` / `versions` / `current` / `layout` must not be spelled beside it; without `hosted`, spell those fields (and a top-level `baseUrl` default) and the command builds the identity for you.
|
|
101
|
+
- `versions` lists every label the catalog advertises; `current` (default: the newest) is the one generated at `path` / `$id`. Every other label is **frozen**: it must exist on disk and declare the `$id` the config derives for it, or the build fails before anything is written (`FrozenVersionMissingError`, `FrozenVersionIdMismatchError`). A `repo` / `branch` / `path` / `baseUrl` change is therefore a re-publish event for every frozen label. Declare a single label on a first run; append the next only once the first has shipped.
|
|
102
|
+
- `appendVersion` (default `true`) keeps SchemaStore's `-<version>` file suffix; `false` lets the version directory name the file alone (`schemas/6.0/output.json`) and requires the `"versioned"` layout. Like `baseUrl`, it belongs to `hosted` when that is given, and flipping it is a re-publish event for every frozen label.
|
|
103
|
+
- `published` (default `false`) marks a version other people already depend on: an unpublished schema regenerates in place through any change, a published one is held to the drift policy.
|
|
104
|
+
- `drift` (`strict` / `semantic` / `allow`, default `semantic`) and `onDrift` (`error` / `warn`) are top-level defaults; `drift`, `published`, `jsonSchema` and `rootAnnotations` may be set per entry. Objects are emitted closed (`additionalProperties: false`); `jsonSchema: { onExcessProperty: "ignore" }` reopens one document.
|
|
105
|
+
- `catalog` is required under SchemaStore hosting and optional under a custom host; every entry lands in ONE file at `catalogPath` (default `<outputDir>/catalog.json`).
|
|
106
|
+
- A typo'd key anywhere in the config is named and rejected, never ignored, and every issue on an entry is reported at once.
|
|
107
|
+
|
|
77
108
|
## Commands
|
|
78
109
|
|
|
79
110
|
```text
|
|
@@ -81,22 +112,36 @@ schemastore build [config] [--drift=strict|semantic|allow] [--on-drift=error|war
|
|
|
81
112
|
schemastore check [config] [--drift=strict|semantic|allow] [--on-drift=error|warn] [--force] [--format=human|json]
|
|
82
113
|
```
|
|
83
114
|
|
|
84
|
-
- Before anything is generated, every
|
|
85
|
-
- `build` generates every schema, runs the gates (structural
|
|
86
|
-
- `check` is the identical walk with no writes: it reports what `build` would do under the same flags and exits
|
|
87
|
-
- `--drift` and `--on-drift` override the config
|
|
88
|
-
- `--format=json` emits one JSON document on stdout (
|
|
89
|
-
|
|
115
|
+
- Before anything is generated, every frozen label is verified — present on disk, and self-identified by the derived `$id`.
|
|
116
|
+
- `build` generates every schema, runs the gates (the structural lint and ajv strict mode), applies the drift policy, and writes what passes — content-compared, so an unchanged file is untouched — plus the catalog file when any entry declares one.
|
|
117
|
+
- `check` is the identical walk with no writes: it reports what `build` would do under the same flags and exits the same way, and also fails (exit `1`) whenever a build would write anything — a stale or missing document is fixed by running `schemastore build` and committing the result. A `catalog.json` left behind after the last `catalog` block was removed is reported `orphaned` and fails `check` the same way, but `build` never deletes it: delete the file by hand, or restore a `catalog` block. So is a document left behind under an old derived name — an `appendVersion` flip or a `layout` change moved its path, and nothing claims the old file any more: both commands probe the sibling shapes (`<name>.json`, `<name>-<v>.json`, `<v>/<name>.json`, `<v>/<name>-<v>.json`) of every label the config still declares and report each one that exists as an orphaned document, failed by `check`, never deleted by `build`. Nothing else in `outputDir` is looked at, so sharing it with another config, a deploy folder, or the repository root is safe (unless two configs derive the same schema name and version under different layouts into it); a `name` change or a dropped label leaves a file the command cannot know about — delete those by hand.
|
|
118
|
+
- `--drift` and `--on-drift` override the config for one run; `--force` is sugar for `--drift=allow` (combined with a different explicit `--drift` it is a usage error).
|
|
119
|
+
- `--format=json` emits one JSON document on stdout (per-schema outcome and effective tolerance, the catalog outcome, the `orphaned` document paths when any, `drift: { onDrift, policy? }`); human text moves to stderr. When `GITHUB_STEP_SUMMARY` is set, both commands append a markdown table.
|
|
120
|
+
|
|
121
|
+
## The engine, as a library export
|
|
122
|
+
|
|
123
|
+
The command validates with ajv in strict mode — SchemaStore's own gate — registering the keyword families `@effected/schemastore` declares and the standard `ajv-formats` vocabulary (formats only, never the `formatMaximum` family), one fresh instance per document. The same layer is this package's one export, for a program composing the pipeline directly:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { SchemaFile, SchemaPipeline } from "@effected/schemastore";
|
|
127
|
+
import { AjvValidator } from "@effected/schemastore-cli";
|
|
128
|
+
import { NodeServices } from "@effect/platform-node";
|
|
129
|
+
import { Effect, Layer } from "effect";
|
|
130
|
+
|
|
131
|
+
const AppLayer = Layer.mergeAll(SchemaFile.layer, AjvValidator.layer).pipe(Layer.provide(NodeServices.layer));
|
|
132
|
+
|
|
133
|
+
const program = SchemaPipeline.run(targets).pipe(Effect.provide(AppLayer));
|
|
134
|
+
```
|
|
90
135
|
|
|
91
|
-
|
|
136
|
+
Findings come back as values; the error channel carries `SchemaValidatorError` only when the engine fails as a mechanism.
|
|
92
137
|
|
|
93
138
|
## Exit codes
|
|
94
139
|
|
|
95
140
|
| code | meaning |
|
|
96
141
|
| ---- | -------------------------------------------------------------------------- |
|
|
97
142
|
| 0 | success, including drift under `onDrift: warn` |
|
|
98
|
-
| 1 | drift under `onDrift: error` (
|
|
99
|
-
| 2 | config not found, failed to load, or failed `
|
|
143
|
+
| 1 | drift under `onDrift: error` (one line per drifting schema: `$id`, change, current and next version), a gate failure, a missing or mis-identified frozen version, or — for `check` — anything `build` would write or an output nothing claims (an orphaned catalog file, or an orphaned document at a sibling shape of a derived path) |
|
|
144
|
+
| 2 | config not found, failed to load, or failed `defineConfig` validation |
|
|
100
145
|
| 3 | infrastructure failure |
|
|
101
146
|
| 64 | usage error |
|
|
102
147
|
|
package/Report.js
CHANGED
|
@@ -23,9 +23,11 @@ const catalogLine = (entry) => {
|
|
|
23
23
|
case "unchanged": return `unchanged catalog ${entry.path} (${entry.entries} entries)`;
|
|
24
24
|
case "would-write": return `would write catalog ${entry.path} (${entry.entries} entries)`;
|
|
25
25
|
case "held": return `held catalog ${entry.path} (${entry.entries} entries)`;
|
|
26
|
+
case "orphaned": return `orphaned catalog ${entry.path} (no schema declares a catalog)`;
|
|
26
27
|
default: return entry.outcome;
|
|
27
28
|
}
|
|
28
29
|
};
|
|
30
|
+
const orphanedLine = (orphan) => `orphaned document ${orphan} (no target, frozen version, or catalog entry claims it — delete it by hand; build never will)`;
|
|
29
31
|
const driftClause = (report) => `drift ${report.policy !== void 0 ? `${report.policy} (flag)` : "per schema (config)"}, on-drift ${report.onDrift}`;
|
|
30
32
|
const summaryLine = (report) => {
|
|
31
33
|
const written = report.schemas.filter((schema) => schema.outcome === "written").length;
|
|
@@ -57,6 +59,7 @@ var Report = class {
|
|
|
57
59
|
const lines = [];
|
|
58
60
|
for (const schema of report.schemas) lines.push(...schemaLines(schema, report));
|
|
59
61
|
if (report.catalog !== void 0) lines.push(catalogLine(report.catalog));
|
|
62
|
+
for (const orphan of report.orphaned ?? []) lines.push(orphanedLine(orphan));
|
|
60
63
|
lines.push(summaryLine(report));
|
|
61
64
|
return lines;
|
|
62
65
|
}
|
|
@@ -103,6 +106,7 @@ var Report = class {
|
|
|
103
106
|
entries: report.catalog.entries,
|
|
104
107
|
outcome: report.catalog.outcome
|
|
105
108
|
} } : {},
|
|
109
|
+
...report.orphaned !== void 0 ? { orphaned: report.orphaned } : {},
|
|
106
110
|
drifted: report.drifted,
|
|
107
111
|
gateFailed: report.gateFailed,
|
|
108
112
|
wrote: report.wrote
|
|
@@ -148,6 +152,7 @@ var Report = class {
|
|
|
148
152
|
String(report.catalog.entries),
|
|
149
153
|
report.catalog.outcome
|
|
150
154
|
]));
|
|
155
|
+
if (report.orphaned !== void 0) lines.push("", tableRow(["orphaned document", "claimed by"]), tableRow(["---", "---"]), ...report.orphaned.map((orphan) => tableRow([orphan, "nothing — delete by hand"])));
|
|
151
156
|
lines.push("");
|
|
152
157
|
if (report.gateFailed) {
|
|
153
158
|
const failed = report.schemas.filter((schema) => schema.outcome === "gate-failed").length;
|
package/Runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
1
|
import { CanonicalJson, CatalogEntry, DriftPolicy, SchemaPipeline, SchemaVersioning } from "@effected/schemastore";
|
|
2
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
//#region src/Runner.ts
|
|
5
5
|
/**
|
|
@@ -26,9 +26,61 @@ missing: Schema.Array(Schema.Struct({
|
|
|
26
26
|
return `${this.missing.length} frozen version(s) advertised but not on disk; nothing was written.\n${lines.join("\n")}`;
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Indicates that one or more frozen files exist but do not declare the
|
|
31
|
+
* `$id` their {@link ResolvedSchema.frozen} entry derives — the file is
|
|
32
|
+
* absent an `$id`, declares a different one, or does not parse. Raised by
|
|
33
|
+
* {@link Runner.run} before anything is generated: a frozen document is
|
|
34
|
+
* the one file the derivation does not own, so it is the one place `$id`
|
|
35
|
+
* and the advertised URL can still disagree (a `baseUrl` change leaves
|
|
36
|
+
* every frozen file carrying the old host). Every mismatch is collected.
|
|
37
|
+
*
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
var FrozenVersionIdMismatchError = class extends Schema.TaggedError()("FrozenVersionIdMismatchError", {
|
|
41
|
+
/** One entry per frozen file whose `$id` is not the derived one. */
|
|
42
|
+
mismatched: Schema.Array(Schema.Struct({
|
|
43
|
+
/** The schema's key in the config. */
|
|
44
|
+
name: Schema.String,
|
|
45
|
+
/** The frozen version label. */
|
|
46
|
+
version: Schema.String,
|
|
47
|
+
/** The frozen file. */
|
|
48
|
+
path: Schema.String,
|
|
49
|
+
/** The `$id` the config derives for this label. */
|
|
50
|
+
expected: Schema.String,
|
|
51
|
+
/** The `$id` the file declares; absent when it declares none or does not parse. */
|
|
52
|
+
actual: Schema.optionalKey(Schema.String),
|
|
53
|
+
/** Why the file fails: a different `$id`, no `$id` at all, or text that is not JSON. */
|
|
54
|
+
reason: Schema.Literals([
|
|
55
|
+
"mismatch",
|
|
56
|
+
"absent",
|
|
57
|
+
"unparseable"
|
|
58
|
+
])
|
|
59
|
+
})) }) {
|
|
60
|
+
get message() {
|
|
61
|
+
const lines = this.mismatched.map((entry) => {
|
|
62
|
+
const detail = entry.reason === "mismatch" ? `declares $id ${entry.actual}, expected ${entry.expected}` : entry.reason === "absent" ? `declares no $id, expected ${entry.expected}` : `does not parse as JSON, expected $id ${entry.expected}`;
|
|
63
|
+
return ` schema "${entry.name}" version ${entry.version}: ${entry.path} ${detail}`;
|
|
64
|
+
});
|
|
65
|
+
return `${this.mismatched.length} frozen version(s) on disk do not carry their derived $id; nothing was written.\n${lines.join("\n")}`;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
29
68
|
const pipelineOptions = { contractChanges: "allow" };
|
|
30
69
|
const orNone = (read) => read.pipe(Effect.map(Option.some), Effect.catchIf((error) => error.reason._tag === "NotFound", () => Effect.succeed(Option.none())));
|
|
31
70
|
const pendingOutcome = (wouldWrite, refused) => !wouldWrite ? "unchanged" : refused ? "held" : "would-write";
|
|
71
|
+
const declaredId = (text) => {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(text);
|
|
75
|
+
} catch {
|
|
76
|
+
return { reason: "unparseable" };
|
|
77
|
+
}
|
|
78
|
+
const $id = typeof parsed === "object" && parsed !== null ? parsed.$id : void 0;
|
|
79
|
+
return typeof $id === "string" ? {
|
|
80
|
+
reason: "ok",
|
|
81
|
+
$id
|
|
82
|
+
} : { reason: "absent" };
|
|
83
|
+
};
|
|
32
84
|
const parsesEqual = (existing, text) => {
|
|
33
85
|
try {
|
|
34
86
|
return CanonicalJson.equals(JSON.parse(existing), JSON.parse(text));
|
|
@@ -48,7 +100,11 @@ const parsesEqual = (existing, text) => {
|
|
|
48
100
|
* is reported at once: a build fails typed with
|
|
49
101
|
* {@link FrozenVersionMissingError} listing every label with no file on disk
|
|
50
102
|
* — nothing is written — a catalog that points a label at a 404 is a worse
|
|
51
|
-
* failure than an early refusal.
|
|
103
|
+
* failure than an early refusal. A file that is there is read, and must
|
|
104
|
+
* declare the `$id` its entry derives, else the run fails typed with
|
|
105
|
+
* {@link FrozenVersionIdMismatchError} (a different `$id`, none, or text
|
|
106
|
+
* that is not JSON) — a `baseUrl` change is a re-publish event for every
|
|
107
|
+
* frozen label, not a silent re-advertisement.
|
|
52
108
|
*
|
|
53
109
|
* **Drift is classified per schema, under that schema's own
|
|
54
110
|
* {@link ResolvedSchema.drift} tolerance — unless `options.policy` is set,
|
|
@@ -66,8 +122,26 @@ const parsesEqual = (existing, text) => {
|
|
|
66
122
|
* **Every catalog entry the config declares lands in ONE file** at
|
|
67
123
|
* `config.catalogPath` — never one file per schema — serialized canonically
|
|
68
124
|
* and compared by parsed content against the file on disk, written only
|
|
69
|
-
* when different and only when the run is writing.
|
|
70
|
-
*
|
|
125
|
+
* when different and only when the run is writing. When no schema declares
|
|
126
|
+
* one, nothing is written; a file still at `catalogPath` is reported
|
|
127
|
+
* `orphaned` (stale under `check`) and left in place, and the report omits
|
|
128
|
+
* `catalog` entirely only when there is no such file either.
|
|
129
|
+
*
|
|
130
|
+
* **A moved path leaves an orphan the derivation cannot see**: an
|
|
131
|
+
* `appendVersion` flip or a `layout` change renames a document's derived
|
|
132
|
+
* path, and the previously written file stays on disk under the old name —
|
|
133
|
+
* for a `published` label, its advertised URL keeps serving a stale
|
|
134
|
+
* document with no report. The derivation has exactly four shapes for a
|
|
135
|
+
* name and label (`<name>.json`, `<name>-<v>.json`, `<v>/<name>.json`,
|
|
136
|
+
* `<v>/<name>-<v>.json`), so both modes probe the sibling shapes of every
|
|
137
|
+
* label the config still knows and report each one that exists as a FILE
|
|
138
|
+
* and that no target, frozen version, or `catalogPath` claims in
|
|
139
|
+
* {@link RunReport.orphaned}. Nothing else on disk is looked at: an
|
|
140
|
+
* `outputDir` shared with another config, a deploy folder, or the
|
|
141
|
+
* repository root holds documents this config cannot tell from its own
|
|
142
|
+
* leftovers, so they are never reported. A `name` change is therefore not
|
|
143
|
+
* caught either — the old name is unknowable. Orphans are reported, never
|
|
144
|
+
* deleted: the CLI may not have written them.
|
|
71
145
|
*
|
|
72
146
|
* @public
|
|
73
147
|
*/
|
|
@@ -77,15 +151,37 @@ var Runner = class {
|
|
|
77
151
|
const fs = yield* FileSystem.FileSystem;
|
|
78
152
|
const path = yield* Path.Path;
|
|
79
153
|
const missing = [];
|
|
154
|
+
const mismatched = [];
|
|
80
155
|
for (const schema of config.schemas) for (const frozen of schema.frozen) {
|
|
81
156
|
const info = yield* orNone(fs.stat(frozen.path));
|
|
82
|
-
if (Option.isNone(info) || info.value.type !== "File")
|
|
157
|
+
if (Option.isNone(info) || info.value.type !== "File") {
|
|
158
|
+
missing.push({
|
|
159
|
+
name: schema.name,
|
|
160
|
+
version: frozen.version,
|
|
161
|
+
path: frozen.path
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const text = yield* fs.readFileString(frozen.path);
|
|
166
|
+
const declared = declaredId(text);
|
|
167
|
+
if (declared.reason !== "ok") mismatched.push({
|
|
168
|
+
name: schema.name,
|
|
169
|
+
version: frozen.version,
|
|
170
|
+
path: frozen.path,
|
|
171
|
+
expected: frozen.$id,
|
|
172
|
+
reason: declared.reason
|
|
173
|
+
});
|
|
174
|
+
else if (declared.$id !== frozen.$id) mismatched.push({
|
|
83
175
|
name: schema.name,
|
|
84
176
|
version: frozen.version,
|
|
85
|
-
path: frozen.path
|
|
177
|
+
path: frozen.path,
|
|
178
|
+
expected: frozen.$id,
|
|
179
|
+
actual: declared.$id,
|
|
180
|
+
reason: "mismatch"
|
|
86
181
|
});
|
|
87
182
|
}
|
|
88
183
|
if (missing.length > 0) return yield* Effect.fail(new FrozenVersionMissingError({ missing }));
|
|
184
|
+
if (mismatched.length > 0) return yield* Effect.fail(new FrozenVersionIdMismatchError({ mismatched }));
|
|
89
185
|
const targets = config.schemas.map((schema) => schema.target);
|
|
90
186
|
const checks = yield* SchemaPipeline.check(targets, pipelineOptions);
|
|
91
187
|
const gateFailed = checks.some((check) => check.blocked);
|
|
@@ -131,7 +227,14 @@ var Runner = class {
|
|
|
131
227
|
});
|
|
132
228
|
const entries = config.schemas.flatMap((schema) => schema.catalog !== void 0 ? [schema.catalog] : []);
|
|
133
229
|
let catalog;
|
|
134
|
-
if (entries.length
|
|
230
|
+
if (entries.length === 0) {
|
|
231
|
+
const info = yield* orNone(fs.stat(config.catalogPath));
|
|
232
|
+
if (Option.isSome(info)) catalog = {
|
|
233
|
+
path: config.catalogPath,
|
|
234
|
+
entries: 0,
|
|
235
|
+
outcome: "orphaned"
|
|
236
|
+
};
|
|
237
|
+
} else {
|
|
135
238
|
const text = yield* CanonicalJson.serialize(entries.map((entry) => Schema.encodeSync(CatalogEntry)(entry)));
|
|
136
239
|
const existing = yield* orNone(fs.readFileString(config.catalogPath));
|
|
137
240
|
const same = Option.isSome(existing) && parsesEqual(existing.value, text);
|
|
@@ -146,6 +249,26 @@ var Runner = class {
|
|
|
146
249
|
outcome
|
|
147
250
|
};
|
|
148
251
|
}
|
|
252
|
+
const claimed = /* @__PURE__ */ new Set([path.normalize(config.catalogPath)]);
|
|
253
|
+
for (const schema of config.schemas) {
|
|
254
|
+
claimed.add(path.normalize(schema.target.path));
|
|
255
|
+
for (const frozen of schema.frozen) claimed.add(path.normalize(frozen.path));
|
|
256
|
+
}
|
|
257
|
+
const orphaned = [];
|
|
258
|
+
for (const schema of config.schemas) {
|
|
259
|
+
const labels = [schema.target.version, ...schema.frozen.map((frozen) => frozen.version)];
|
|
260
|
+
const shapes = [SchemaVersioning.fileName(schema.name), ...labels.flatMap((version) => version === void 0 ? [] : [
|
|
261
|
+
SchemaVersioning.fileName(schema.name, version, "flat"),
|
|
262
|
+
SchemaVersioning.fileName(schema.name, version, "versioned"),
|
|
263
|
+
SchemaVersioning.fileName(schema.name, version, "versioned", false)
|
|
264
|
+
])];
|
|
265
|
+
for (const shape of shapes) {
|
|
266
|
+
const file = path.normalize(path.join(config.outputDir, shape));
|
|
267
|
+
if (claimed.has(file)) continue;
|
|
268
|
+
const info = yield* orNone(fs.stat(file));
|
|
269
|
+
if (Option.isSome(info) && info.value.type === "File") orphaned.push(file);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
149
272
|
return {
|
|
150
273
|
mode: options.mode,
|
|
151
274
|
configPath: options.configPath,
|
|
@@ -153,6 +276,7 @@ var Runner = class {
|
|
|
153
276
|
...options.policy !== void 0 ? { policy: options.policy } : {},
|
|
154
277
|
schemas,
|
|
155
278
|
...catalog !== void 0 ? { catalog } : {},
|
|
279
|
+
...orphaned.length > 0 ? { orphaned } : {},
|
|
156
280
|
drifted,
|
|
157
281
|
gateFailed,
|
|
158
282
|
wrote: schemas.some((s) => s.outcome === "written") || catalog?.outcome === "written"
|
|
@@ -161,4 +285,4 @@ var Runner = class {
|
|
|
161
285
|
};
|
|
162
286
|
|
|
163
287
|
//#endregion
|
|
164
|
-
export { FrozenVersionMissingError, Runner };
|
|
288
|
+
export { FrozenVersionIdMismatchError, FrozenVersionMissingError, Runner };
|
package/cli/commands/check.js
CHANGED
|
@@ -6,11 +6,14 @@ import { Command } from "effect/unstable/cli";
|
|
|
6
6
|
/**
|
|
7
7
|
* `schemastore check`: the same walk as `build`, reported and never written.
|
|
8
8
|
* The CI drift gate: it also fails when the committed documents are stale,
|
|
9
|
-
* i.e. whenever `build` would write anything
|
|
9
|
+
* i.e. whenever `build` would write anything — and on the outputs nothing
|
|
10
|
+
* claims (an orphaned catalog file, or a document left behind at a sibling
|
|
11
|
+
* shape of a derived path), which `build` never deletes: remove them by
|
|
12
|
+
* hand.
|
|
10
13
|
*
|
|
11
14
|
* @public
|
|
12
15
|
*/
|
|
13
|
-
const makeCheckCommand = (deps) => Command.make("check", commandFlags, (input) => execute("check", input, deps)).pipe(Command.withDescription("Report what build would do, fail when it would write anything or refuse to, and write nothing"));
|
|
16
|
+
const makeCheckCommand = (deps) => Command.make("check", commandFlags, (input) => execute("check", input, deps)).pipe(Command.withDescription("Report what build would do, fail when it would write anything or refuse to, or when a catalog file or document is orphaned, and write nothing"));
|
|
14
17
|
|
|
15
18
|
//#endregion
|
|
16
19
|
export { makeCheckCommand };
|
package/cli/execute.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { AjvValidator } from "../AjvValidator.js";
|
|
1
2
|
import { ConfigLoader } from "../ConfigLoader.js";
|
|
2
3
|
import { Report } from "../Report.js";
|
|
3
4
|
import { Runner } from "../Runner.js";
|
|
4
5
|
import { StepSummary } from "../StepSummary.js";
|
|
5
|
-
import {
|
|
6
|
+
import { SchemaFile } from "@effected/schemastore";
|
|
6
7
|
import { Console, Effect, Option, Schema } from "effect";
|
|
7
|
-
import {
|
|
8
|
+
import { CliRuntime } from "@effected/cli";
|
|
8
9
|
|
|
9
10
|
//#region src/cli/execute.ts
|
|
10
11
|
const DriftedSchema = Schema.Struct({
|
|
@@ -45,14 +46,22 @@ var GateError = class extends Schema.TaggedError()("GateError", { count: Schema.
|
|
|
45
46
|
};
|
|
46
47
|
/**
|
|
47
48
|
* `check` found committed documents that differ from what the config
|
|
48
|
-
* generates (or are missing), so a `build` would write
|
|
49
|
-
*
|
|
49
|
+
* generates (or are missing), so a `build` would write — or outputs
|
|
50
|
+
* nothing claims (an orphaned catalog file, an orphaned document), which
|
|
51
|
+
* `build` never deletes. `check` is the CI drift gate, so a stale tree
|
|
52
|
+
* fails it. Exit `1`. `count` is every finding; `orphaned` the part of it
|
|
53
|
+
* a build cannot clear, so the message names both remedies.
|
|
50
54
|
*
|
|
51
55
|
* @public
|
|
52
56
|
*/
|
|
53
|
-
var StaleError = class extends Schema.TaggedError()("StaleError", {
|
|
57
|
+
var StaleError = class extends Schema.TaggedError()("StaleError", {
|
|
58
|
+
count: Schema.Number,
|
|
59
|
+
orphaned: Schema.optionalKey(Schema.Number)
|
|
60
|
+
}) {
|
|
54
61
|
get message() {
|
|
55
|
-
|
|
62
|
+
const orphaned = this.orphaned ?? 0;
|
|
63
|
+
const stale = this.count - orphaned;
|
|
64
|
+
return [...stale > 0 ? [`${stale} document(s) are stale; run \`schemastore build\` and commit the result.`] : [], ...orphaned > 0 ? [`${orphaned} orphaned output(s) must be deleted by hand; build never will.`] : []].join(" ");
|
|
56
65
|
}
|
|
57
66
|
};
|
|
58
67
|
/**
|
|
@@ -111,7 +120,10 @@ const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
|
|
|
111
120
|
mode,
|
|
112
121
|
configPath: loaded.path,
|
|
113
122
|
...drift
|
|
114
|
-
}).pipe(Effect.provide(SchemaFile.layer), Effect.provide(deps.validator ??
|
|
123
|
+
}).pipe(Effect.provide(SchemaFile.layer), Effect.provide(deps.validator ?? AjvValidator.layer), Effect.catchTags({
|
|
124
|
+
FrozenVersionMissingError: (error) => Effect.fail(CliRuntime.reported(error, 1)),
|
|
125
|
+
FrozenVersionIdMismatchError: (error) => Effect.fail(CliRuntime.reported(error, 1))
|
|
126
|
+
}));
|
|
115
127
|
yield* emit(report, input.format);
|
|
116
128
|
yield* StepSummary.append(Report.markdown(report));
|
|
117
129
|
if (report.gateFailed) {
|
|
@@ -128,8 +140,12 @@ const execute = Effect.fn("schemastore.execute")(function* (mode, input, deps) {
|
|
|
128
140
|
return yield* Effect.fail(CliRuntime.reported(new DriftError({ drifted }), 1));
|
|
129
141
|
}
|
|
130
142
|
if (mode === "check") {
|
|
131
|
-
const
|
|
132
|
-
|
|
143
|
+
const orphaned = (report.catalog?.outcome === "orphaned" ? 1 : 0) + (report.orphaned?.length ?? 0);
|
|
144
|
+
const count = report.schemas.filter((schema) => schema.outcome === "would-write").length + (report.catalog?.outcome === "would-write" ? 1 : 0) + orphaned;
|
|
145
|
+
if (count > 0) return yield* Effect.fail(CliRuntime.reported(new StaleError({
|
|
146
|
+
count,
|
|
147
|
+
...orphaned > 0 ? { orphaned } : {}
|
|
148
|
+
}), 1));
|
|
133
149
|
}
|
|
134
150
|
});
|
|
135
151
|
|
package/cli/program.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConfigLoadError } from "../ConfigLoader.js";
|
|
2
2
|
import { makeCommands } from "./root.js";
|
|
3
|
-
import { CliLogger, CliRuntime } from "@effected/cli";
|
|
4
3
|
import { Effect } from "effect";
|
|
4
|
+
import { CliLogger, CliRuntime } from "@effected/cli";
|
|
5
5
|
import { Command } from "effect/unstable/cli";
|
|
6
6
|
|
|
7
7
|
//#region src/cli/program.ts
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { SchemaValidator } from "@effected/schemastore";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
//#region src/AjvValidator.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The shipped `SchemaValidator` implementation: ajv strict mode over the
|
|
6
|
+
* Draft-07 meta-schema, with every declared `KeywordFamilies` keyword and
|
|
7
|
+
* the standard `ajv-formats` vocabulary registered.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Lives in the CLI, not the library, so `ajv` is a cost only the command
|
|
11
|
+
* pays: `@effected/schemastore` owns the `SchemaValidator` contract
|
|
12
|
+
* and its doubles, an application that imports it at runtime never pulls an
|
|
13
|
+
* engine, and the `schemastore` command composes this layer at its edge. It
|
|
14
|
+
* is exported for a program that drives `SchemaPipeline` itself and wants
|
|
15
|
+
* the same verdict the command gives.
|
|
16
|
+
*
|
|
17
|
+
* `validate` checks the document against the Draft-07 meta-schema and then
|
|
18
|
+
* compiles it, reporting BOTH as `ValidationFinding` values: meta-schema
|
|
19
|
+
* failures keep ajv's structured `instancePath` and `keyword`, while a
|
|
20
|
+
* rejection ajv raises by *throwing* becomes a root-pathed finding — both a
|
|
21
|
+
* strict-mode compile failure and a declared keyword whose NAME ajv's own
|
|
22
|
+
* grammar (`/^[a-z_$][a-z0-9_$:-]*$/i`) refuses, such as an `x-ai-*` key
|
|
23
|
+
* carrying a dot or a space. The error channel stays reserved for the engine
|
|
24
|
+
* failing as a mechanism (`SchemaValidatorError`).
|
|
25
|
+
*
|
|
26
|
+
* `strict` defaults to `true` — SchemaStore's gate. Each call builds its own
|
|
27
|
+
* ajv instance, so documents sharing an `$id` never collide. Registering the
|
|
28
|
+
* declared families keeps the engine's verdict consistent with
|
|
29
|
+
* `DocumentLint`'s through the same predicate, so the two cannot drift; the
|
|
30
|
+
* plugin's `formatMaximum` / `formatMinimum` limit keywords are deliberately
|
|
31
|
+
* NOT registered, because the lint answers those as unknown keywords.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { SchemaValidator } from "@effected/schemastore";
|
|
36
|
+
* import { AjvValidator } from "@effected/schemastore-cli";
|
|
37
|
+
* import { Effect } from "effect";
|
|
38
|
+
*
|
|
39
|
+
* const program = Effect.gen(function* () {
|
|
40
|
+
* const validator = yield* SchemaValidator;
|
|
41
|
+
* return yield* validator.validate({ type: "object" });
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* Effect.runPromise(Effect.provide(program, AjvValidator.layer));
|
|
45
|
+
* // => []
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export declare class AjvValidator {
|
|
51
|
+
private constructor();
|
|
52
|
+
/** The engine, as a `SchemaValidator` layer. */
|
|
53
|
+
static readonly layer: Layer.Layer<SchemaValidator>;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.js
ADDED
package/main.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { loggerLayer, program } from "./cli/program.js";
|
|
2
|
+
import { Effect } from "effect";
|
|
2
3
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
3
4
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
4
5
|
import { CliRuntime } from "@effected/cli";
|
|
5
|
-
import { Effect } from "effect";
|
|
6
6
|
import { CliError } from "effect/unstable/cli";
|
|
7
7
|
|
|
8
8
|
//#region src/main.ts
|
|
@@ -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.13.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.13.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": [
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"type": "module",
|
|
31
31
|
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./index.d.ts",
|
|
34
|
+
"import": "./index.js",
|
|
35
|
+
"default": "./index.js"
|
|
36
|
+
},
|
|
32
37
|
"./package.json": "./package.json"
|
|
33
38
|
},
|
|
34
39
|
"bin": {
|
|
@@ -37,10 +42,12 @@
|
|
|
37
42
|
"dependencies": {
|
|
38
43
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
39
44
|
"@effected/cli": "^0.5.1",
|
|
45
|
+
"ajv": "^8.20.0",
|
|
46
|
+
"ajv-formats": "^3.0.1",
|
|
40
47
|
"jiti": "^2.6.0"
|
|
41
48
|
},
|
|
42
49
|
"peerDependencies": {
|
|
43
|
-
"@effected/schemastore": "0.
|
|
50
|
+
"@effected/schemastore": "0.13.0",
|
|
44
51
|
"effect": "4.0.0-rc.115"
|
|
45
52
|
},
|
|
46
53
|
"engines": {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.59.1"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|