@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 +14 -0
- package/README.md +4 -1
- package/package.json +3 -3
- package/src/ast.ts +2 -0
- package/src/build.ts +21 -1
- package/src/cli.ts +11 -2
- package/src/parser.ts +25 -2
- package/src/tokenizer.ts +2 -0
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.
|
|
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.
|
|
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.
|
|
44
|
+
"@vertekum/core": "0.1.1",
|
|
45
45
|
"@vertekum/schema-vocabulary": "0.1.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/ast.ts
CHANGED
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(
|
|
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
|
-
|
|
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 {
|
|
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);
|