@effected/schemastore 0.10.0 → 0.11.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 CHANGED
@@ -98,7 +98,9 @@ var CatalogEntry = class CatalogEntry extends Schema.Class("CatalogEntry")({
98
98
  * Assembles an entry from a catalog identity plus
99
99
  * {@link SchemaVersioning.catalogUrls}' inputs: pass `versions` for the
100
100
  * versioned mode (the `versions` map and latest-pointing `url` are
101
- * derived), omit it for the unversioned mode. Throws an `Error` naming
101
+ * derived), omit it for the unversioned mode. `layout` (default
102
+ * `"flat"`) and `current` (default: the newest label) are forwarded to
103
+ * {@link SchemaVersioning.catalogUrls} verbatim. Throws an `Error` naming
102
104
  * both spellings when two labels compare equal under
103
105
  * {@link SchemaVersioning.Order} (`1.2` and `1.2.0`): each would be its
104
106
  * own key and URL for one document.
@@ -108,7 +110,9 @@ var CatalogEntry = class CatalogEntry extends Schema.Class("CatalogEntry")({
108
110
  const urls = SchemaVersioning.catalogUrls({
109
111
  baseUrl: options.baseUrl,
110
112
  name: options.fileBaseName ?? options.name,
111
- ...options.versions !== void 0 ? { versions: options.versions } : {}
113
+ ...options.versions !== void 0 ? { versions: options.versions } : {},
114
+ ...options.layout !== void 0 ? { layout: options.layout } : {},
115
+ ...options.current !== void 0 ? { current: options.current } : {}
112
116
  });
113
117
  return CatalogEntry.make({
114
118
  name: options.name,
@@ -73,7 +73,7 @@ const bumpNext = (current, parsed, components) => {
73
73
  }
74
74
  };
75
75
  const assertSimpleName = (name) => {
76
- if (name.length === 0 || /[/\\\s]/.test(name)) throw new Error(`Schema name must be a non-empty simple file base name, got "${name}"`);
76
+ if (!SchemaVersioning.isSimpleName(name)) throw new Error(`Schema name must be a non-empty simple file base name, got "${name}"`);
77
77
  };
78
78
  const joinUrl = (baseUrl, file) => {
79
79
  let end = baseUrl.length;
@@ -182,22 +182,35 @@ var SchemaVersioning = class SchemaVersioning {
182
182
  return reparsed.success;
183
183
  }
184
184
  /**
185
+ * Whether a name is a simple file base name — non-empty, no path
186
+ * separators, no whitespace — the rule every schema name is held to
187
+ * ({@link SchemaVersioning.fileName} throws on anything else; `defineConfig`
188
+ * rejects a schema key the same way). One predicate so the two cannot drift.
189
+ */
190
+ static isSimpleName(name) {
191
+ return name.length > 0 && !/[/\\\s]/.test(name);
192
+ }
193
+ /**
185
194
  * Derives the schema file name for a catalog name: `name.json`
186
- * unversioned, `name-<version>.json` versioned.
195
+ * unversioned, `name-<version>.json` versioned under the `"flat"`
196
+ * layout (the default and the only shape SchemaStore serves), or
197
+ * `<version>/name-<version>.json` under `"versioned"`.
187
198
  *
188
199
  * The name must be a simple file base name (no separators, no
189
200
  * whitespace); anything else is a wiring mistake and throws.
190
201
  */
191
- static fileName(name, version) {
202
+ static fileName(name, version, layout = "flat") {
192
203
  assertSimpleName(name);
193
- return version === void 0 ? `${name}.json` : `${name}-${version}.json`;
204
+ if (version === void 0) return `${name}.json`;
205
+ const file = `${name}-${version}.json`;
206
+ return layout === "versioned" ? `${version}/${file}` : file;
194
207
  }
195
208
  /**
196
209
  * The canonical URL a schema file is hosted at: `baseUrl` joined with
197
210
  * {@link SchemaVersioning.fileName}.
198
211
  */
199
- static schemaUrl(baseUrl, name, version) {
200
- return joinUrl(baseUrl, SchemaVersioning.fileName(name, version));
212
+ static schemaUrl(baseUrl, name, version, layout = "flat") {
213
+ return joinUrl(baseUrl, SchemaVersioning.fileName(name, version, layout));
201
214
  }
202
215
  /**
203
216
  * Assembles the `url`/`versions` half of a catalog entry.
@@ -205,9 +218,19 @@ var SchemaVersioning = class SchemaVersioning {
205
218
  * Omitting `versions` selects the unversioned mode (`url` only,
206
219
  * pointing at the plain `name.json`). Providing them selects the
207
220
  * versioned mode: the `versions` map carries every label, and `url`
208
- * points at the latest version's file. An **empty** `versions` array is
221
+ * points at `current` (default: the newest label under
222
+ * {@link SchemaVersioning.Order}). An **empty** `versions` array is
209
223
  * a contradiction (versioned mode with no versions) and throws — pass
210
- * `undefined` for the unversioned mode.
224
+ * `undefined` for the unversioned mode. `current`, when given, must
225
+ * compare equal under {@link SchemaVersioning.Order} to a member of
226
+ * `versions` or this throws; `url` is built from that member's own
227
+ * spelling (the `versions` map's key), not from the `current` argument
228
+ * verbatim — so a differently-spelled equivalent (`"1.2"` matching a
229
+ * `"1.2.0"` member) still points `url` at the same file the map does.
230
+ *
231
+ * `layout` (default `"flat"`) is forwarded to every URL derivation, so
232
+ * `"versioned"` nests every map value and `url` under its own version
233
+ * directory.
211
234
  *
212
235
  * Labels are inserted in ascending {@link SchemaVersioning.Order}; see
213
236
  * {@link CatalogUrls.versions} for why a bare-major key's serialized
@@ -215,14 +238,18 @@ var SchemaVersioning = class SchemaVersioning {
215
238
  */
216
239
  static catalogUrls(options) {
217
240
  const { baseUrl, name, versions } = options;
218
- if (versions === void 0) return { url: SchemaVersioning.schemaUrl(baseUrl, name) };
241
+ const layout = options.layout ?? "flat";
242
+ if (versions === void 0) return { url: SchemaVersioning.schemaUrl(baseUrl, name, void 0, layout) };
219
243
  if (versions.length === 0) throw new Error(`catalogUrls received an empty versions array for "${name}": pass undefined for the unversioned mode`);
220
244
  const ascending = [...versions].sort(SchemaVersioning.Order);
221
245
  const map = {};
222
- for (const version of ascending) map[version] = SchemaVersioning.schemaUrl(baseUrl, name, version);
246
+ for (const version of ascending) map[version] = SchemaVersioning.schemaUrl(baseUrl, name, version, layout);
223
247
  const newest = ascending[ascending.length - 1];
248
+ const requested = options.current ?? newest;
249
+ const current = ascending.find((v) => SchemaVersioning.Order(v, requested) === 0);
250
+ if (current === void 0) throw new Error(`catalogUrls: current "${requested}" is not one of the versions of "${name}"`);
224
251
  return {
225
- url: SchemaVersioning.schemaUrl(baseUrl, name, newest),
252
+ url: SchemaVersioning.schemaUrl(baseUrl, name, current, layout),
226
253
  versions: map
227
254
  };
228
255
  }
@@ -1,42 +1,147 @@
1
1
  import { SchemaVersioning } from "./SchemaVersioning.js";
2
2
  import { CatalogEntry } from "./CatalogEntry.js";
3
3
  import { DriftPolicy } from "./DriftPolicy.js";
4
- import { Schema } from "effect";
4
+ import { SchemaTarget } from "./SchemaTarget.js";
5
+ import { Option, Predicate, Result, Schema } from "effect";
5
6
 
6
7
  //#region src/SchemastoreConfig.ts
7
8
  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;
9
+ /** The host SchemaStore-hosted documents declare in `$id`. @public */
10
+ const SCHEMASTORE_ID_BASE = "https://json.schemastore.org";
11
+ /** The host SchemaStore's `catalog.json` points `url` at. @public */
12
+ const SCHEMASTORE_CATALOG_BASE = "https://www.schemastore.org";
13
+ const DRIFT_TOLERANCES = [
14
+ "strict",
15
+ "semantic",
16
+ "allow"
17
+ ];
18
+ const ON_DRIFT = ["error", "warn"];
19
+ const LAYOUTS = ["flat", "versioned"];
20
+ const fail = (message) => {
21
+ throw new Error(`defineConfig: ${message}`);
27
22
  };
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) ?? [];
23
+ const trimSlashes = (dir) => {
24
+ let end = dir.length;
25
+ while (end > 1 && dir.charCodeAt(end - 1) === 47) end -= 1;
26
+ return dir.slice(0, end);
27
+ };
28
+ const resolveHosting = (name, baseUrl, layout) => {
29
+ if (baseUrl === void 0) return fail(`schema "${name}" has no baseUrl and the config declares no default`);
30
+ if (typeof baseUrl !== "string") return fail(`schema "${name}" has a baseUrl that is not a string`);
31
+ if (baseUrl.length === 0) return fail(`schema "${name}" has no baseUrl and the config declares no default`);
32
+ if (layout !== void 0 && !LAYOUTS.includes(layout)) return fail(`schema "${name}" has an invalid layout "${String(layout)}"; expected "flat" or "versioned"`);
33
+ if (baseUrl === "schemastore") {
34
+ if (layout !== void 0) return fail(`schema "${name}" declares layout "${layout}" under baseUrl "schemastore", which serves only the flat layout`);
35
+ return {
36
+ idBase: SCHEMASTORE_ID_BASE,
37
+ catalogBase: SCHEMASTORE_CATALOG_BASE,
38
+ layout: "flat"
39
+ };
40
+ }
41
+ if (!baseUrl.startsWith("https://") || baseUrl.length === 8) return fail(`schema "${name}" has baseUrl "${baseUrl}"; expected "schemastore" or an https:// URL`);
42
+ return {
43
+ idBase: baseUrl,
44
+ catalogBase: baseUrl,
45
+ layout: layout ?? "versioned"
46
+ };
47
+ };
48
+ const parseLabel = (name, label) => {
49
+ if (typeof label !== "string") return fail(`schema "${name}" has a version label that is not a string: ${String(label)}`);
50
+ return Result.getOrThrowWith(SchemaVersioning.parseResult(label), (error) => /* @__PURE__ */ new Error(`defineConfig: schema "${name}" has an invalid version label "${label}": ${error.message}`));
51
+ };
52
+ const resolveVersions = (name, entry) => {
53
+ if (entry.versions === void 0) {
54
+ if (entry.current !== void 0) return fail(`schema "${name}" declares current "${entry.current}" without versions`);
55
+ return;
56
+ }
57
+ if (!Array.isArray(entry.versions)) return fail(`schema "${name}" declares versions that is not an array`);
58
+ if (entry.versions.length === 0) return fail(`schema "${name}" declares versions as an empty array; omit versions for an unversioned schema`);
59
+ const versions = [];
60
+ for (const label of entry.versions) {
61
+ const version = parseLabel(name, label);
34
62
  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}"`);
63
+ if (duplicate !== void 0) return fail(`schema "${name}" declares the same version twice, as "${duplicate}" and "${version}"`);
36
64
  versions.push(version);
37
- map.set(target.name, versions);
38
65
  }
39
- return map;
66
+ const newest = Option.getOrThrow(SchemaVersioning.latest(versions));
67
+ if (entry.current === void 0) return {
68
+ versions,
69
+ current: newest
70
+ };
71
+ const current = parseLabel(name, entry.current);
72
+ const match = versions.find((v) => SchemaVersioning.Order(v, current) === 0);
73
+ if (match === void 0) return fail(`schema "${name}" declares current "${entry.current}" which is not one of its versions`);
74
+ return {
75
+ versions,
76
+ current: match
77
+ };
78
+ };
79
+ const isDriftTolerance = (value) => DRIFT_TOLERANCES.includes(value);
80
+ const resolveConfigDrift = (value) => {
81
+ if (value === void 0) return DriftPolicy.defaults.policy;
82
+ return isDriftTolerance(value) ? value : fail(`config has an invalid drift tolerance "${String(value)}"`);
83
+ };
84
+ const resolveSchemaDrift = (name, value, fallback) => {
85
+ if (value === void 0) return fallback;
86
+ return isDriftTolerance(value) ? value : fail(`schema "${name}" has an invalid drift tolerance "${String(value)}"`);
87
+ };
88
+ const resolveEntry = (name, entry, defaults, outputDir) => {
89
+ if (!SchemaVersioning.isSimpleName(name)) return fail(`schema "${name}" must be keyed by a simple file base name (no separators, no whitespace)`);
90
+ if (!Predicate.isObject(entry)) return fail(`schema "${name}" must be an object`);
91
+ if (!Schema.isSchema(entry.schema)) return fail(`schema "${name}" has a schema that is not an Effect Schema`);
92
+ if (entry.published !== void 0 && typeof entry.published !== "boolean") return fail(`schema "${name}" has a published that is not a boolean`);
93
+ const baseUrl = entry.baseUrl ?? defaults.baseUrl;
94
+ const hosting = resolveHosting(name, baseUrl, entry.layout);
95
+ const versioned = resolveVersions(name, entry);
96
+ if (baseUrl === "schemastore" && entry.catalog === void 0) return fail(`schema "${name}" must declare a catalog block under baseUrl "schemastore"`);
97
+ if (entry.catalog !== void 0 && (!Predicate.isObject(entry.catalog) || !Array.isArray(entry.catalog.fileMatch) || typeof entry.catalog.description !== "string")) return fail(`schema "${name}" declares an invalid catalog block (expected { description, fileMatch[] })`);
98
+ if (entry.catalog !== void 0 && entry.catalog.fileMatch.length === 0) return fail(`schema "${name}" declares a catalog with an empty fileMatch`);
99
+ const file = (version) => `${outputDir}/${SchemaVersioning.fileName(name, version, hosting.layout)}`;
100
+ const urlOf = (base) => (version) => SchemaVersioning.schemaUrl(base, name, version, hosting.layout);
101
+ const idOf = urlOf(hosting.idBase);
102
+ const catalogUrlOf = urlOf(hosting.catalogBase);
103
+ const current = versioned?.current;
104
+ const target = current === void 0 ? SchemaTarget.make({
105
+ schema: entry.schema,
106
+ $id: idOf(current),
107
+ name,
108
+ path: file(current),
109
+ published: entry.published ?? false,
110
+ ...entry.jsonSchema !== void 0 ? { jsonSchema: entry.jsonSchema } : {},
111
+ ...entry.rootAnnotations !== void 0 ? { rootAnnotations: entry.rootAnnotations } : {}
112
+ }) : SchemaTarget.make({
113
+ schema: entry.schema,
114
+ $id: idOf(current),
115
+ name,
116
+ path: file(current),
117
+ version: current,
118
+ published: entry.published ?? false,
119
+ ...entry.jsonSchema !== void 0 ? { jsonSchema: entry.jsonSchema } : {},
120
+ ...entry.rootAnnotations !== void 0 ? { rootAnnotations: entry.rootAnnotations } : {}
121
+ });
122
+ const frozen = versioned === void 0 ? [] : versioned.versions.filter((v) => v !== versioned.current).map((version) => ({
123
+ version,
124
+ path: file(version),
125
+ url: catalogUrlOf(version)
126
+ }));
127
+ const catalog = entry.catalog === void 0 ? void 0 : CatalogEntry.assemble({
128
+ name,
129
+ description: entry.catalog.description,
130
+ fileMatch: entry.catalog.fileMatch,
131
+ baseUrl: hosting.catalogBase,
132
+ layout: hosting.layout,
133
+ ...versioned !== void 0 ? {
134
+ versions: versioned.versions,
135
+ current: versioned.current
136
+ } : {}
137
+ });
138
+ return {
139
+ name,
140
+ target,
141
+ frozen,
142
+ drift: resolveSchemaDrift(name, entry.drift, defaults.drift),
143
+ ...catalog !== void 0 ? { catalog } : {}
144
+ };
40
145
  };
41
146
  const normalizePath = (raw) => {
42
147
  const absolute = raw.startsWith("/");
@@ -52,11 +157,11 @@ const normalizePath = (raw) => {
52
157
  }
53
158
  return `${absolute ? "/" : ""}${out.join("/")}`;
54
159
  };
55
- const assertUniquePaths = (schemas, catalog) => {
160
+ const assertUniquePaths = (paths) => {
56
161
  const seen = /* @__PURE__ */ new Set();
57
- for (const p of [...schemas.map((target) => target.path), ...catalog.map((entry) => entry.path)]) {
162
+ for (const p of paths) {
58
163
  const normalized = normalizePath(p);
59
- if (seen.has(normalized)) throw new Error(`defineConfig: output path "${p}" is declared twice`);
164
+ if (seen.has(normalized)) fail(`output path "${p}" is declared twice`);
60
165
  seen.add(normalized);
61
166
  }
62
167
  };
@@ -64,49 +169,57 @@ const assertUniquePaths = (schemas, catalog) => {
64
169
  * Validate and assemble a `schemastore.config.ts` value.
65
170
  *
66
171
  * @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.
172
+ * Pure: no IO, no Effect. `$id`, the write `path` and every catalog URL are
173
+ * derived from ONE layout (`outputDir`, `baseUrl` and `layout`) so they
174
+ * cannot disagree with each other. `versions` names every label a schema
175
+ * advertises; `current` (default: the newest under
176
+ * {@link SchemaVersioning.Order}) is the one generated at `target`, and every
177
+ * other label becomes a {@link FrozenVersion} the CLI verifies but does not
178
+ * regenerate. `baseUrl: "schemastore"` expands to
179
+ * {@link SCHEMASTORE_ID_BASE} for `$id` and {@link SCHEMASTORE_CATALOG_BASE}
180
+ * for the catalog URL, forcing the `"flat"` layout; any other `baseUrl` is
181
+ * used as one base for both, defaulting to the `"versioned"` layout.
182
+ *
183
+ * Throws a plain `Error` (never a raw `TypeError`) naming the offending
184
+ * schema on: a non-object `input`; an empty `schemas` record; a
185
+ * missing/empty `outputDir`; a schema key that is not a simple file base
186
+ * name; a schema whose `schema` is not an Effect Schema; a schema with no
187
+ * `baseUrl` anywhere, or a `baseUrl` that is not a string; a `baseUrl` that
188
+ * is neither `"schemastore"` nor an `https://` URL; a `versions` that is not
189
+ * an array, or an empty `versions` array; a version label (or `current`)
190
+ * that is not a string, or an otherwise invalid version label; two labels
191
+ * spelling the same version; `current` given without `versions`, or naming
192
+ * one not among them; a non-boolean `published`; `layout` declared under
193
+ * `baseUrl: "schemastore"`; a missing `catalog` under
194
+ * `baseUrl: "schemastore"`, or one with an empty `fileMatch`; an invalid
195
+ * `drift` or top-level `onDrift`; and an output path (a target, a frozen
196
+ * file, or the catalog path) declared twice, compared after a lexical
197
+ * normalisation (`./`, `..`, trailing `/`) — the CLI's loader re-checks on
198
+ * the resolved absolute paths. Branding the result lets a loader recognise a
199
+ * config module's default export via {@link isSchemastoreConfig}.
76
200
  *
77
201
  * @public
78
202
  */
79
203
  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 rawCatalog = input.catalog ?? [];
84
- if (!Array.isArray(rawCatalog)) throw new Error("defineConfig: invalid catalog: expected an array of catalog entries");
85
- const catalog = rawCatalog.map((raw) => {
86
- const label = typeof raw === "object" && raw !== null ? String(raw.name) : String(raw);
87
- const config = decodeOrThrow(CatalogConfigSchema, raw, `catalog entry "${label}"`);
88
- const found = versions.get(config.name);
89
- if (found === void 0) throw new Error(`defineConfig: catalog entry "${config.name}" matches no versioned schema`);
90
- return {
91
- config,
92
- entry: CatalogEntry.assemble({
93
- name: config.name,
94
- description: config.description,
95
- fileMatch: config.fileMatch,
96
- baseUrl: config.baseUrl,
97
- versions: found
98
- })
99
- };
100
- });
101
- assertUniquePaths(input.schemas, catalog.map((c) => c.config));
204
+ if (!Predicate.isObject(input)) return fail("expected a config object");
205
+ if (typeof input.outputDir !== "string" || input.outputDir.length === 0) return fail("outputDir is required");
206
+ const outputDir = trimSlashes(input.outputDir);
207
+ if (!Predicate.isObject(input.schemas) || Object.keys(input.schemas).length === 0) return fail("at least one schema is required");
208
+ if (input.onDrift !== void 0 && !ON_DRIFT.includes(input.onDrift)) return fail(`invalid onDrift "${String(input.onDrift)}"`);
209
+ if (input.catalogPath !== void 0 && (typeof input.catalogPath !== "string" || input.catalogPath.length === 0)) return fail("catalogPath must be a non-empty string when given");
210
+ const defaults = {
211
+ baseUrl: input.baseUrl,
212
+ drift: resolveConfigDrift(input.drift)
213
+ };
214
+ const schemas = Object.entries(input.schemas).map(([name, entry]) => resolveEntry(name, entry, defaults, outputDir));
215
+ const catalogPath = input.catalogPath ?? `${outputDir}/catalog.json`;
216
+ assertUniquePaths([...schemas.flatMap((s) => [s.target.path, ...s.frozen.map((f) => f.path)]), catalogPath]);
102
217
  return {
103
218
  [ConfigBrand]: true,
104
- schemas: input.schemas,
105
- catalog,
106
- drift: {
107
- policy: drift.policy ?? DriftPolicy.defaults.policy,
108
- onDrift: drift.onDrift ?? DriftPolicy.defaults.onDrift
109
- }
219
+ outputDir,
220
+ onDrift: input.onDrift ?? DriftPolicy.defaults.onDrift,
221
+ catalogPath,
222
+ schemas
110
223
  };
111
224
  };
112
225
  /**
@@ -118,4 +231,4 @@ const defineConfig = (input) => {
118
231
  const isSchemastoreConfig = (value) => typeof value === "object" && value !== null && value[ConfigBrand] === true;
119
232
 
120
233
  //#endregion
121
- export { defineConfig, isSchemastoreConfig };
234
+ export { SCHEMASTORE_CATALOG_BASE, SCHEMASTORE_ID_BASE, defineConfig, isSchemastoreConfig };
package/index.d.ts CHANGED
@@ -650,6 +650,15 @@ export declare const SchemaVersion: Schema.brand<Schema.String, "SchemaVersion">
650
650
  * @public
651
651
  */
652
652
  export type SchemaVersion = typeof SchemaVersion.Type;
653
+ /**
654
+ * Where a versioned document sits relative to its base: `flat` is
655
+ * `<name>-<version>.json` (the only shape SchemaStore serves), `versioned`
656
+ * nests it as `<version>/<name>-<version>.json`. An unversioned document is
657
+ * `<name>.json` under either.
658
+ *
659
+ * @public
660
+ */
661
+ type SchemaLayout = "flat" | "versioned";
653
662
  /**
654
663
  * The `url`/`versions` half of a catalog entry, as assembled by
655
664
  * {@link SchemaVersioning.catalogUrls}.
@@ -756,28 +765,47 @@ export declare class SchemaVersioning {
756
765
  * the label and the ceiling rather than `SemVer.make`'s bare schema failure.
757
766
  */
758
767
  static next(current: SchemaVersion, change: WriteChange): SchemaVersion;
768
+ /**
769
+ * Whether a name is a simple file base name — non-empty, no path
770
+ * separators, no whitespace — the rule every schema name is held to
771
+ * ({@link SchemaVersioning.fileName} throws on anything else; `defineConfig`
772
+ * rejects a schema key the same way). One predicate so the two cannot drift.
773
+ */
774
+ static isSimpleName(name: string): boolean;
759
775
  /**
760
776
  * Derives the schema file name for a catalog name: `name.json`
761
- * unversioned, `name-<version>.json` versioned.
777
+ * unversioned, `name-<version>.json` versioned under the `"flat"`
778
+ * layout (the default and the only shape SchemaStore serves), or
779
+ * `<version>/name-<version>.json` under `"versioned"`.
762
780
  *
763
781
  * The name must be a simple file base name (no separators, no
764
782
  * whitespace); anything else is a wiring mistake and throws.
765
783
  */
766
- static fileName(name: string, version?: SchemaVersion): string;
784
+ static fileName(name: string, version?: SchemaVersion, layout?: SchemaLayout): string;
767
785
  /**
768
786
  * The canonical URL a schema file is hosted at: `baseUrl` joined with
769
787
  * {@link SchemaVersioning.fileName}.
770
788
  */
771
- static schemaUrl(baseUrl: string, name: string, version?: SchemaVersion): string;
789
+ static schemaUrl(baseUrl: string, name: string, version?: SchemaVersion, layout?: SchemaLayout): string;
772
790
  /**
773
791
  * Assembles the `url`/`versions` half of a catalog entry.
774
792
  *
775
793
  * Omitting `versions` selects the unversioned mode (`url` only,
776
794
  * pointing at the plain `name.json`). Providing them selects the
777
795
  * versioned mode: the `versions` map carries every label, and `url`
778
- * points at the latest version's file. An **empty** `versions` array is
796
+ * points at `current` (default: the newest label under
797
+ * {@link SchemaVersioning.Order}). An **empty** `versions` array is
779
798
  * a contradiction (versioned mode with no versions) and throws — pass
780
- * `undefined` for the unversioned mode.
799
+ * `undefined` for the unversioned mode. `current`, when given, must
800
+ * compare equal under {@link SchemaVersioning.Order} to a member of
801
+ * `versions` or this throws; `url` is built from that member's own
802
+ * spelling (the `versions` map's key), not from the `current` argument
803
+ * verbatim — so a differently-spelled equivalent (`"1.2"` matching a
804
+ * `"1.2.0"` member) still points `url` at the same file the map does.
805
+ *
806
+ * `layout` (default `"flat"`) is forwarded to every URL derivation, so
807
+ * `"versioned"` nests every map value and `url` under its own version
808
+ * directory.
781
809
  *
782
810
  * Labels are inserted in ascending {@link SchemaVersioning.Order}; see
783
811
  * {@link CatalogUrls.versions} for why a bare-major key's serialized
@@ -787,6 +815,8 @@ export declare class SchemaVersioning {
787
815
  readonly baseUrl: string;
788
816
  readonly name: string;
789
817
  readonly versions?: ReadonlyArray<SchemaVersion>;
818
+ readonly layout?: SchemaLayout;
819
+ readonly current?: SchemaVersion;
790
820
  }): CatalogUrls;
791
821
  }
792
822
  //#endregion
@@ -836,7 +866,9 @@ export declare class CatalogEntry extends CatalogEntry_base {
836
866
  * Assembles an entry from a catalog identity plus
837
867
  * {@link SchemaVersioning.catalogUrls}' inputs: pass `versions` for the
838
868
  * versioned mode (the `versions` map and latest-pointing `url` are
839
- * derived), omit it for the unversioned mode. Throws an `Error` naming
869
+ * derived), omit it for the unversioned mode. `layout` (default
870
+ * `"flat"`) and `current` (default: the newest label) are forwarded to
871
+ * {@link SchemaVersioning.catalogUrls} verbatim. Throws an `Error` naming
840
872
  * both spellings when two labels compare equal under
841
873
  * {@link SchemaVersioning.Order} (`1.2` and `1.2.0`): each would be its
842
874
  * own key and URL for one document.
@@ -848,6 +880,8 @@ export declare class CatalogEntry extends CatalogEntry_base {
848
880
  readonly baseUrl: string;
849
881
  readonly fileBaseName?: string;
850
882
  readonly versions?: ReadonlyArray<SchemaVersion>;
883
+ readonly layout?: SchemaLayout;
884
+ readonly current?: SchemaVersion;
851
885
  }): CatalogEntry;
852
886
  /**
853
887
  * The fileMatch hygiene lint over this entry's patterns — pure shape
@@ -1630,42 +1664,144 @@ export declare class SchemaPipeline {
1630
1664
  //#endregion
1631
1665
  //#region src/SchemastoreConfig.d.ts
1632
1666
  declare const ConfigBrand: unique symbol;
1667
+ /** The host SchemaStore-hosted documents declare in `$id`. @public */
1668
+ export declare const SCHEMASTORE_ID_BASE = "https://json.schemastore.org";
1669
+ /** The host SchemaStore's `catalog.json` points `url` at. @public */
1670
+ export declare const SCHEMASTORE_CATALOG_BASE = "https://www.schemastore.org";
1633
1671
  /**
1634
- * One catalog entry a `schemastore.config.ts` declares: the SchemaStore
1635
- * `catalog.json` fields plus where to write the assembled entry. The entry's
1636
- * `versions` and `url` are derived by {@link defineConfig} from every
1637
- * versioned schema of the same `name`.
1672
+ * The SchemaStore `catalog.json` fields a schema entry declares, minus
1673
+ * `url`/`versions` — those are derived from the entry's `baseUrl`, `layout`
1674
+ * and `versions`/`current` by {@link defineConfig}, so they cannot disagree
1675
+ * with the schema's own identity.
1638
1676
  *
1639
1677
  * @public
1640
1678
  */
1641
- interface CatalogConfig {
1642
- readonly name: string;
1679
+ interface CatalogInput {
1680
+ /** The catalog description. */
1643
1681
  readonly description: string;
1682
+ /** Glob patterns editors match files against; must be non-empty. */
1644
1683
  readonly fileMatch: ReadonlyArray<string>;
1645
- readonly baseUrl: string;
1646
- /** Where to write the assembled entry; relative paths are resolved by the loader against the config file's directory. */
1647
- readonly path: string;
1648
1684
  }
1649
1685
  /**
1650
- * What a `schemastore.config.ts` hands to {@link defineConfig}: the schema
1651
- * targets, an optional catalog block and an optional partial drift block.
1686
+ * One schema a `schemastore.config.ts` declares, keyed by its own file base
1687
+ * name in {@link SchemastoreConfigInput.schemas}. `$id`, the write `path` and
1688
+ * every catalog URL are derived from `outputDir`, `baseUrl` (this entry's, or
1689
+ * the config's default) and `layout` — never spelled out by hand.
1690
+ *
1691
+ * @public
1692
+ */
1693
+ interface SchemaEntryInput {
1694
+ /** The Effect Schema source the document is generated from. */
1695
+ readonly schema: Schema.Constraint;
1696
+ /**
1697
+ * Every version label this schema advertises. Omit for an unversioned
1698
+ * schema (`<name>.json`). An empty array is rejected — omit the field
1699
+ * instead. Two labels that compare equal under
1700
+ * {@link SchemaVersioning.Order} (`"1.2"` and `"1.2.0"`) are rejected as
1701
+ * one version spelled twice.
1702
+ */
1703
+ readonly versions?: ReadonlyArray<string>;
1704
+ /**
1705
+ * Which of `versions` is the one generated at this entry's `path`/`$id`;
1706
+ * the rest become {@link ResolvedSchema.frozen} files the CLI verifies
1707
+ * but does not regenerate. Defaults to the newest label under
1708
+ * {@link SchemaVersioning.Order}. Requires `versions`, and must name one
1709
+ * of them.
1710
+ */
1711
+ readonly current?: string;
1712
+ /**
1713
+ * Whether a consumer already depends on this document at this label —
1714
+ * forwarded to {@link (SchemaTarget:class).(make:1)}. Defaults to `false`.
1715
+ */
1716
+ readonly published?: boolean;
1717
+ /**
1718
+ * Where this schema is hosted: the literal `"schemastore"` (the only
1719
+ * value that expands `$id` to {@link SCHEMASTORE_ID_BASE} and the catalog
1720
+ * URL to {@link SCHEMASTORE_CATALOG_BASE}, and forces the `"flat"`
1721
+ * layout) or an `https://` URL used as ONE base for both `$id` and the
1722
+ * catalog URL. Falls back to {@link SchemastoreConfigInput.baseUrl} when
1723
+ * omitted; an entry with neither is rejected.
1724
+ */
1725
+ readonly baseUrl?: string;
1726
+ /**
1727
+ * How a versioned document's path/URL nests relative to its base — see
1728
+ * {@link SchemaLayout}. Defaults to `"versioned"` for a custom `baseUrl`,
1729
+ * and is rejected outright under `baseUrl: "schemastore"`, which serves
1730
+ * only the flat layout.
1731
+ */
1732
+ readonly layout?: SchemaLayout;
1733
+ /** Overrides {@link SchemastoreConfigInput.drift} for this schema. */
1734
+ readonly drift?: DriftTolerance;
1735
+ /**
1736
+ * The catalog entry to assemble for this schema. Required under
1737
+ * `baseUrl: "schemastore"` (every SchemaStore-hosted document is
1738
+ * cataloged); optional under a custom host.
1739
+ */
1740
+ readonly catalog?: CatalogInput;
1741
+ /** Forwarded to {@link (SchemaTarget:class).(make:1)}'s `jsonSchema`. */
1742
+ readonly jsonSchema?: Schema.ToJsonSchemaOptions;
1743
+ /** Forwarded to {@link (SchemaTarget:class).(make:1)}'s `rootAnnotations`. */
1744
+ readonly rootAnnotations?: Readonly<Record<string, unknown>>;
1745
+ }
1746
+ /**
1747
+ * What a `schemastore.config.ts` hands to {@link defineConfig}: a directory
1748
+ * every derived path is written under, top-level defaults for `baseUrl` and
1749
+ * `drift`, and the keyed set of schemas to derive.
1652
1750
  *
1653
1751
  * @public
1654
1752
  */
1655
1753
  interface SchemastoreConfigInput {
1656
- readonly schemas: ReadonlyArray<SchemaTarget>;
1657
- readonly catalog?: ReadonlyArray<CatalogConfig>;
1658
- readonly drift?: Partial<DriftOptions>;
1754
+ /** The directory every derived `path` is written under; a trailing slash is trimmed. */
1755
+ readonly outputDir: string;
1756
+ /** The default {@link SchemaEntryInput.baseUrl} for an entry that declares none. */
1757
+ readonly baseUrl?: string;
1758
+ /** The default {@link SchemaEntryInput.drift} for an entry that declares none. Defaults to `"semantic"`. */
1759
+ readonly drift?: DriftTolerance;
1760
+ /** What a build does when it finds drift. Defaults to `"error"`. */
1761
+ readonly onDrift?: OnDrift;
1762
+ /** Where the assembled catalog is written. Defaults to `<outputDir>/catalog.json`. */
1763
+ readonly catalogPath?: string;
1764
+ /**
1765
+ * The schemas to derive, keyed by file base name — the key IS the
1766
+ * `name` every derived path and URL is built from, so it must be a
1767
+ * simple file base name (no separators, no whitespace).
1768
+ */
1769
+ readonly schemas: Readonly<Record<string, SchemaEntryInput>>;
1770
+ }
1771
+ /**
1772
+ * One version of a schema that is advertised (via `versions`) but not the
1773
+ * one currently generated — a file the CLI verifies exists on disk before
1774
+ * any write (existence only — content is never compared), never
1775
+ * regenerates.
1776
+ *
1777
+ * @public
1778
+ */
1779
+ interface FrozenVersion {
1780
+ /** The frozen version label. */
1781
+ readonly version: SchemaVersion;
1782
+ /** The path the frozen file lives at. */
1783
+ readonly path: string;
1784
+ /** The catalog URL the frozen file is hosted at. */
1785
+ readonly url: string;
1659
1786
  }
1660
1787
  /**
1661
- * A validated catalog declaration paired with the `CatalogEntry` assembled
1662
- * from it.
1788
+ * One `defineConfig` schema entry, resolved: the {@link (SchemaTarget:interface)} to
1789
+ * generate, its frozen predecessor versions, its effective drift tolerance,
1790
+ * and its assembled catalog entry, if any.
1663
1791
  *
1664
1792
  * @public
1665
1793
  */
1666
- interface CatalogTarget {
1667
- readonly config: CatalogConfig;
1668
- readonly entry: CatalogEntry;
1794
+ interface ResolvedSchema {
1795
+ /** The schema's key in {@link SchemastoreConfigInput.schemas}. */
1796
+ readonly name: string;
1797
+ /** The target to generate at the current version (or the sole, unversioned target). */
1798
+ readonly target: SchemaTarget;
1799
+ /** Every OTHER advertised version, as a frozen file to verify. */
1800
+ readonly frozen: ReadonlyArray<FrozenVersion>;
1801
+ /** This entry's effective drift tolerance, after falling back to the config default. */
1802
+ readonly drift: DriftTolerance;
1803
+ /** The assembled catalog entry, when {@link SchemaEntryInput.catalog} was given. */
1804
+ readonly catalog?: CatalogEntry;
1669
1805
  }
1670
1806
  /**
1671
1807
  * The validated, defaults-filled config {@link defineConfig} answers and the
@@ -1675,23 +1811,47 @@ interface CatalogTarget {
1675
1811
  */
1676
1812
  interface SchemastoreConfig {
1677
1813
  readonly [ConfigBrand]: true;
1678
- readonly schemas: ReadonlyArray<SchemaTarget>;
1679
- readonly catalog: ReadonlyArray<CatalogTarget>;
1680
- readonly drift: DriftOptions;
1814
+ /** The directory every derived `path` is written under, trailing slash trimmed. */
1815
+ readonly outputDir: string;
1816
+ /** What a build does when it finds drift. */
1817
+ readonly onDrift: OnDrift;
1818
+ /** Where the assembled catalog is written. */
1819
+ readonly catalogPath: string;
1820
+ /** Every schema, resolved. */
1821
+ readonly schemas: ReadonlyArray<ResolvedSchema>;
1681
1822
  }
1682
1823
  /**
1683
1824
  * Validate and assemble a `schemastore.config.ts` value.
1684
1825
  *
1685
1826
  * @remarks
1686
- * Pure: no IO, no Effect. Identity-with-validation over the input, filling
1687
- * drift defaults, deriving each catalog entry's `versions` from EVERY
1688
- * versioned schema of that name (published or not — the entry is what gets
1689
- * submitted to become published), and branding the result so a loader can
1690
- * recognise a config module's default export. Throws a plain `Error` on a
1691
- * bad input; the CLI wraps it into its typed config-load error. Rejects an
1692
- * output `path` declared twice across schemas and catalog entries, compared
1693
- * after a lexical normalisation (`./`, `..`, trailing `/`); the CLI's loader
1694
- * re-checks on the resolved absolute paths.
1827
+ * Pure: no IO, no Effect. `$id`, the write `path` and every catalog URL are
1828
+ * derived from ONE layout (`outputDir`, `baseUrl` and `layout`) so they
1829
+ * cannot disagree with each other. `versions` names every label a schema
1830
+ * advertises; `current` (default: the newest under
1831
+ * {@link SchemaVersioning.Order}) is the one generated at `target`, and every
1832
+ * other label becomes a {@link FrozenVersion} the CLI verifies but does not
1833
+ * regenerate. `baseUrl: "schemastore"` expands to
1834
+ * {@link SCHEMASTORE_ID_BASE} for `$id` and {@link SCHEMASTORE_CATALOG_BASE}
1835
+ * for the catalog URL, forcing the `"flat"` layout; any other `baseUrl` is
1836
+ * used as one base for both, defaulting to the `"versioned"` layout.
1837
+ *
1838
+ * Throws a plain `Error` (never a raw `TypeError`) naming the offending
1839
+ * schema on: a non-object `input`; an empty `schemas` record; a
1840
+ * missing/empty `outputDir`; a schema key that is not a simple file base
1841
+ * name; a schema whose `schema` is not an Effect Schema; a schema with no
1842
+ * `baseUrl` anywhere, or a `baseUrl` that is not a string; a `baseUrl` that
1843
+ * is neither `"schemastore"` nor an `https://` URL; a `versions` that is not
1844
+ * an array, or an empty `versions` array; a version label (or `current`)
1845
+ * that is not a string, or an otherwise invalid version label; two labels
1846
+ * spelling the same version; `current` given without `versions`, or naming
1847
+ * one not among them; a non-boolean `published`; `layout` declared under
1848
+ * `baseUrl: "schemastore"`; a missing `catalog` under
1849
+ * `baseUrl: "schemastore"`, or one with an empty `fileMatch`; an invalid
1850
+ * `drift` or top-level `onDrift`; and an output path (a target, a frozen
1851
+ * file, or the catalog path) declared twice, compared after a lexical
1852
+ * normalisation (`./`, `..`, trailing `/`) — the CLI's loader re-checks on
1853
+ * the resolved absolute paths. Branding the result lets a loader recognise a
1854
+ * config module's default export via {@link isSchemastoreConfig}.
1695
1855
  *
1696
1856
  * @public
1697
1857
  */
@@ -1704,5 +1864,5 @@ export declare const defineConfig: (input: SchemastoreConfigInput) => Schemastor
1704
1864
  */
1705
1865
  export declare const isSchemastoreConfig: (value: unknown) => value is SchemastoreConfig;
1706
1866
  //#endregion
1707
- 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 };
1867
+ export type { CanonicalJsonError, CanonicalJsonOptions, CatalogInput, CatalogUrls, CheckResult, ContractChangePolicy, DriftOptions, DriftTolerance, DriftVerdict, FrozenVersion, OnDrift, PipelineCheckResult, PipelineResult, ResolvedSchema, SchemaChange, SchemaEntryInput, SchemaFileShape, SchemaLayout, SchemaPipelineOptions, SchemaValidatorOptions, SchemaValidatorShape, SchemaWriteOptions, SchemastoreConfig, SchemastoreConfigInput, StoreDocumentOptions, WriteChange, WriteOutcome, WriteResult };
1708
1868
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -9,7 +9,7 @@ import { SchemaFile, SchemaFileNotFoundError, SchemaFileReadError, SchemaFileWri
9
9
  import { SchemaValidator, SchemaValidatorError, ValidationFinding } from "./SchemaValidator.js";
10
10
  import { DRAFT_07_META_SCHEMA, SchemaConversionError, StoreDocument, UndeclaredAnnotationKeyError } from "./StoreDocument.js";
11
11
  import { ContractChangeTarget, PipelineFinding, SchemaContractChangeError, SchemaGateError, SchemaPipeline } from "./SchemaPipeline.js";
12
- import { defineConfig, isSchemastoreConfig } from "./SchemastoreConfig.js";
13
12
  import { SchemaTarget } from "./SchemaTarget.js";
13
+ import { SCHEMASTORE_CATALOG_BASE, SCHEMASTORE_ID_BASE, defineConfig, isSchemastoreConfig } from "./SchemastoreConfig.js";
14
14
 
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 };
15
+ export { CanonicalJson, CatalogEntry, CatalogLintFinding, ContractChangeTarget, DRAFT_07_META_SCHEMA, DocumentDiff, DocumentLint, DocumentLintFinding, DriftPolicy, InvalidSchemaVersionError, JsonDepthExceededError, KeywordFamilies, NonJsonValueError, PipelineFinding, SCHEMASTORE_CATALOG_BASE, SCHEMASTORE_ID_BASE, 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.10.0",
3
+ "version": "0.11.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": [