@vertekum/schema-builder 0.1.0 → 0.1.2

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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @vertekum/schema-builder
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [`579276d`](https://github.com/moros-oxus/vertekum/commit/579276d8c1ab7215f291037cebcad0e03ae8d170) Thanks [@tschemmer](https://github.com/tschemmer)! - `schema build` with no argument now skips fragment modules (files declaring no `root` — imports, noted in the summary) instead of failing; naming one explicitly is still an error. References gain a qualified form, `<@module/production>`, addressing one import's production by module basename — the resolver for name collisions across imports, which the ambiguity error now suggests.
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [[`34b1157`](https://github.com/moros-oxus/vertekum/commit/34b1157546027442bf98437699ea3b9da45ebe86)]:
14
+ - @vertekum/core@0.1.1
package/README.md CHANGED
@@ -41,7 +41,9 @@ npx vertekum check # the built schema now governs your token nam
41
41
 
42
42
  `schema build` accepts a single module argument, defaults to every `.dfn` under
43
43
  `./schemas`, and supports `--dry-run` (list without writing), `--json`, and `--check`
44
- (verify built files are current; exit 1 when stale — the CI guard).
44
+ (verify built files are current; exit 1 when stale — the CI guard). Only modules with a
45
+ `root` build: the sweep skips fragment modules (denotation files that exist to be `use`d),
46
+ while naming one explicitly is an error.
45
47
 
46
48
  ## The definition language
47
49
 
@@ -67,6 +69,7 @@ indented line continues the statement above it.
67
69
  | `[ … ]` | grouping; groups may hold full sub-paths (branches) |
68
70
  | `<name>` | reference a local production |
69
71
  | `<@name>` | reference an imported production; an imported module's *root* goes by its basename |
72
+ | `<@module/name>` | qualified: that import's production alone — the resolver when two imports share a name |
70
73
  | `<name [a, b]>` | **pick** — only the listed members of the set |
71
74
  | `<name ![a, b]>` | **omit** — the set minus the listed members |
72
75
  | `<name*>` / `[a \| b *]` | **open set** — additions beyond the listed names are permitted, and every member (listed or added) takes the same tail |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertekum/schema-builder",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Declare token vocabularies as grammar; build them into JSON Schema files",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -35,13 +35,13 @@
35
35
  "./cli": "./src/cli.ts"
36
36
  },
37
37
  "peerDependencies": {
38
- "@vertekum/core": "0.1.0"
38
+ "@vertekum/core": "0.1.1"
39
39
  },
40
40
  "dependencies": {
41
41
  "zod": "^3.25.76"
42
42
  },
43
43
  "devDependencies": {
44
- "@vertekum/core": "0.1.0",
44
+ "@vertekum/core": "0.1.1",
45
45
  "@vertekum/schema-vocabulary": "0.1.0"
46
46
  }
47
47
  }
package/src/ast.ts CHANGED
@@ -73,6 +73,8 @@ export interface Ref {
73
73
  kind: 'ref';
74
74
  name: string;
75
75
  imported: boolean;
76
+ /** `<@module/production>` — resolve `name` in this import alone (the collision resolver). */
77
+ from?: string;
76
78
  open: boolean;
77
79
  pick: string[];
78
80
  omit: string[];
package/src/build.ts CHANGED
@@ -42,6 +42,22 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
42
42
  return { node: production, scope };
43
43
  }
44
44
 
45
+ // Qualified: `<@module/production>` looks in that import alone — the collision resolver.
46
+ if (ref.from) {
47
+ const imported = scope.module.imports.get(ref.from);
48
+ if (!imported) {
49
+ throw new DfnError(`no import named '${ref.from}'`, 1, 1);
50
+ }
51
+ const production = imported.module.productions.get(ref.name);
52
+ if (!production) {
53
+ throw new DfnError(`'${ref.from}' has no production '${ref.name}'`, 1, 1);
54
+ }
55
+ return {
56
+ node: production,
57
+ scope: { module: imported, expanding: new Set() },
58
+ };
59
+ }
60
+
45
61
  const hits: Array<{ node: Node; scope: Scope }> = [];
46
62
  for (const imported of scope.module.imports.values()) {
47
63
  const importedScope: Scope = { module: imported, expanding: new Set() };
@@ -55,7 +71,11 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
55
71
  throw new DfnError(`no import provides '<@${ref.name}>'`, 1, 1);
56
72
  }
57
73
  if (hits.length > 1) {
58
- throw new DfnError(`'<@${ref.name}>' is ambiguous across imports`, 1, 1);
74
+ throw new DfnError(
75
+ `'<@${ref.name}>' is ambiguous across imports — qualify it: <@module/${ref.name}>`,
76
+ 1,
77
+ 1,
78
+ );
59
79
  }
60
80
  return hits[0];
61
81
  }
package/src/cli.ts CHANGED
@@ -66,9 +66,17 @@ export const schemaBuildCommand: CommandDescriptor = {
66
66
 
67
67
  const files: Array<{ path: string; content: string }> = [];
68
68
  const skipped: string[] = [];
69
+ const fragments: string[] = [];
69
70
  const stale: string[] = [];
70
71
 
71
72
  for (const modulePath of modules) {
73
+ // A rootless module is a FRAGMENT — imports for other modules. The sweep skips it;
74
+ // naming one explicitly stays an error (buildModule throws), since silence there
75
+ // would hide a typo'd `root`.
76
+ if (!ctx.args.module && !resolveModule(modulePath).module.root) {
77
+ fragments.push(relative(projectDir, modulePath));
78
+ continue;
79
+ }
72
80
  const { target, content } = buildModule(modulePath);
73
81
  const existing = existsSync(target)
74
82
  ? readFileSync(target, 'utf8')
@@ -93,10 +101,11 @@ export const schemaBuildCommand: CommandDescriptor = {
93
101
  }
94
102
  const notes = [
95
103
  ctx.options.check === true
96
- ? `${modules.length} module(s) current`
104
+ ? `${modules.length - fragments.length} module(s) current`
97
105
  : `built ${files.length} module(s)`,
106
+ ...fragments.map((f) => `${f} declares no root (a fragment) — skipped`),
98
107
  ...skipped.map((f) => `${f} has local edits (no stamp) — left as is`),
99
108
  ];
100
- return { summary: notes.join('\n'), files, data: { skipped } };
109
+ return { summary: notes.join('\n'), files, data: { skipped, fragments } };
101
110
  },
102
111
  };
package/src/parser.ts CHANGED
@@ -192,7 +192,22 @@ class Parser {
192
192
  case 'langle': {
193
193
  const imported = this.peek().kind === 'at';
194
194
  if (imported) this.next();
195
- const name = this.expect('ident', 'a production name');
195
+ let name = this.expect('ident', 'a production name');
196
+ // `<@module/production>` — qualified access to one import's production.
197
+ let from: string | undefined;
198
+ if (this.peek().kind === 'slash') {
199
+ if (!imported) {
200
+ const bad = this.peek();
201
+ throw new DfnError(
202
+ 'a qualified reference is an import — write <@module/name>',
203
+ bad.line,
204
+ bad.column,
205
+ );
206
+ }
207
+ this.next();
208
+ from = name.value;
209
+ name = this.expect('ident', 'a production name');
210
+ }
196
211
  // Set modifiers: `[a, b]` picks only the listed members; `![a, b]` omits them.
197
212
  const pick: string[] = [];
198
213
  const omit: string[] = [];
@@ -218,7 +233,15 @@ class Parser {
218
233
  const open = this.peek().kind === 'star';
219
234
  if (open) this.next();
220
235
  this.expect('rangle', "'>'");
221
- return { kind: 'ref', name: name.value, imported, open, pick, omit };
236
+ return {
237
+ kind: 'ref',
238
+ name: name.value,
239
+ imported,
240
+ from,
241
+ open,
242
+ pick,
243
+ omit,
244
+ };
222
245
  }
223
246
  case 'lbracket': {
224
247
  const node = this.alternation();
package/src/tokenizer.ts CHANGED
@@ -25,6 +25,7 @@ export type TokenKind =
25
25
  | 'question'
26
26
  | 'comma'
27
27
  | 'bang'
28
+ | 'slash'
28
29
  | 'newline'
29
30
  | 'eof';
30
31
 
@@ -126,6 +127,7 @@ export function tokenize(source: string): Token[] {
126
127
  '?': 'question',
127
128
  ',': 'comma',
128
129
  '!': 'bang',
130
+ '/': 'slash',
129
131
  };
130
132
  const kind = single[text[0]];
131
133
  if (!kind) throw new DfnError(`unexpected '${text[0]}'`, line, column);