@rse/specbook 1.0.3 → 1.0.4
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/README.md +106 -19
- package/dst/specbook-api.d.ts +29 -9
- package/dst/specbook-api.js +108 -33
- package/dst/specbook-cli.js +84 -25
- package/dst/specbook-cmd-describe.d.ts +7 -1
- package/dst/specbook-cmd-describe.js +56 -11
- package/dst/specbook-cmd-export.d.ts +2 -1
- package/dst/specbook-cmd-export.js +46 -3
- package/dst/specbook-cmd-lint.d.ts +2 -1
- package/dst/specbook-cmd-lint.js +12 -8
- package/dst/specbook-cmd-preview.d.ts +12 -0
- package/dst/specbook-cmd-preview.js +57 -0
- package/dst/specbook-config.d.ts +1 -1
- package/dst/specbook-config.js +187 -37
- package/dst/specbook-diagnostic.d.ts +2 -0
- package/dst/specbook-diagnostic.js +4 -2
- package/dst/specbook-diagram.d.ts +1 -0
- package/dst/specbook-diagram.js +0 -0
- package/dst/specbook-export-ast.js +4 -2
- package/dst/specbook-export-common.js +5 -3
- package/dst/specbook-export-html-search.js +118 -30
- package/dst/specbook-export-html.css +1140 -233
- package/dst/specbook-export-html.d.ts +1 -1
- package/dst/specbook-export-html.js +764 -120
- package/dst/specbook-export-md.js +4 -2
- package/dst/specbook-format-schema.d.ts +33 -1
- package/dst/specbook-format-schema.js +34 -2
- package/dst/specbook-format.md +193 -25
- package/dst/specbook-format.yaml +2291 -299
- package/dst/specbook-link.d.ts +2 -1
- package/dst/specbook-link.js +43 -3
- package/dst/specbook-mcp.js +19 -10
- package/dst/specbook-parse-common.d.ts +4 -2
- package/dst/specbook-parse-common.js +6 -3
- package/dst/specbook-parse-semantic.js +290 -30
- package/dst/specbook-parse-syntax.js +3 -1
- package/dst/specbook-parse-value.d.ts +1 -0
- package/dst/specbook-parse-value.js +17 -3
- package/dst/specbook-parse.js +1 -1
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ About
|
|
|
13
13
|
format* which can be configured for the specifications of particular
|
|
14
14
|
contexts through a YAML-based *schema configuration*. SpecBook allows
|
|
15
15
|
specifications to be *initialized*, *linted*, *exported* (JSON, JSON5,
|
|
16
|
-
YAML, TOON, HTML, PDF, and normalized Markdown),
|
|
17
|
-
LLMs.
|
|
16
|
+
YAML, TOON, HTML, PDF, and normalized Markdown), *previewed* (HTML, live
|
|
17
|
+
in the browser), and *described* to LLMs.
|
|
18
18
|
|
|
19
19
|
**SpecBook** ships with an API class with methods `<xxx>()`, a
|
|
20
20
|
CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
@@ -44,8 +44,9 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
44
44
|
The YAML schema configuration strictly defines the allowed object
|
|
45
45
|
kinds, hierarchies, and properties, whose values are constrained by an
|
|
46
46
|
expression language (regex, enum, tags, list, and reference). Violations
|
|
47
|
-
are reported as file- and line-precise diagnostics
|
|
48
|
-
|
|
47
|
+
are reported as file- and line-precise diagnostics, while the reference
|
|
48
|
+
coverage an object kind demands is reported as a warning only. A bundled
|
|
49
|
+
standard schema configuration applies if no particular one is given.
|
|
49
50
|
|
|
50
51
|
- **Object Model Diagram Visualisation**:
|
|
51
52
|
Object kinds can declare "graph", "hub", or "grid" diagrams in the
|
|
@@ -68,7 +69,8 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
68
69
|
|
|
69
70
|
- **HTML Export for Developers**:
|
|
70
71
|
The HTML export is a self-contained single document with a
|
|
71
|
-
table-of-contents
|
|
72
|
+
table-of-contents (also as a slide-in side panel for quick navigation),
|
|
73
|
+
full-text search, embedded images, and a light/dark
|
|
72
74
|
theme toggle. It is intended for the day-to-day online reading during
|
|
73
75
|
development. The HTML export can map all object kinds to nested
|
|
74
76
|
sections or compact tables, or an automatic mixture which collapses
|
|
@@ -107,31 +109,55 @@ export class SpecBook {
|
|
|
107
109
|
/* initialize the configured specification artifact files below the
|
|
108
110
|
base directory and return the list of the generated files */
|
|
109
111
|
init (options: {
|
|
110
|
-
config?: string,
|
|
112
|
+
config?: string[], /* YAML schema configuration files or glob patterns, merged
|
|
113
|
+
in order (default: bundled standard one, named "std") */
|
|
111
114
|
basedir?: string /* base directory (default: ".") */
|
|
112
115
|
}): Promise<string[]>
|
|
113
116
|
|
|
114
117
|
/* parse and validate the specification Markdown files below the base directory */
|
|
115
118
|
lint (options: {
|
|
116
|
-
config?: string,
|
|
119
|
+
config?: string[],
|
|
117
120
|
basedir?: string
|
|
118
121
|
}): Promise<LintResult>
|
|
119
122
|
|
|
120
123
|
/* export the specification, parsing the input just once and
|
|
121
|
-
returning one buffer per requested format (strict: any
|
|
122
|
-
rejects the export with an "Error"
|
|
124
|
+
returning one buffer per requested format (strict: any error
|
|
125
|
+
diagnostic rejects the export with an "Error", while the warnings
|
|
126
|
+
are surfaced as "notice" verbose messages) */
|
|
123
127
|
export (options: {
|
|
124
|
-
config?: string,
|
|
128
|
+
config?: string[],
|
|
125
129
|
basedir?: string,
|
|
126
|
-
formats?: ExportFormat[]
|
|
130
|
+
formats?: ExportFormat[], /* requested output formats (default: [ "json" ]) */
|
|
131
|
+
realtime?: boolean /* inject the live preview script into the HTML */
|
|
127
132
|
}): Promise<Buffer[]>
|
|
128
133
|
|
|
134
|
+
/* export the specification like "export" and then keep it in sync by
|
|
135
|
+
re-exporting it on every change of a specification file (a failed
|
|
136
|
+
re-export is reported and leaves the observe loop intact) */
|
|
137
|
+
watch (options: {
|
|
138
|
+
config?: string[],
|
|
139
|
+
basedir?: string,
|
|
140
|
+
formats?: ExportFormat[],
|
|
141
|
+
realtime?: boolean,
|
|
142
|
+
onExport: (buffers: Buffer[]) => void | Promise<void>
|
|
143
|
+
}): Promise<void>
|
|
144
|
+
|
|
145
|
+
/* serve the HTML export as a live preview on "http://<addr>:<port>/",
|
|
146
|
+
kept in sync like "watch" and updated in the browser on every change */
|
|
147
|
+
preview (options: {
|
|
148
|
+
config?: string[],
|
|
149
|
+
basedir?: string,
|
|
150
|
+
addr?: string, /* listening IP address (default: "127.0.0.1") */
|
|
151
|
+
port?: number /* listening TCP port (default: 12345) */
|
|
152
|
+
}): Promise<void>
|
|
153
|
+
|
|
129
154
|
/* describe the SpecBook models and formats, optionally pointing to
|
|
130
155
|
the artifacts of the particular project */
|
|
131
156
|
describe (options: {
|
|
132
|
-
config?: string,
|
|
157
|
+
config?: string[],
|
|
133
158
|
basedir?: string,
|
|
134
159
|
embed?: boolean, /* embed the schema configuration instead of referencing it */
|
|
160
|
+
compress?: CompressLevel, /* compression level of the schema configuration (default: 1) */
|
|
135
161
|
format?: DescribeFormat, /* output format (default: "md") */
|
|
136
162
|
part?: DescribePart /* document part (default: "all") */
|
|
137
163
|
}): Promise<string>
|
|
@@ -149,21 +175,25 @@ export interface LintResult {
|
|
|
149
175
|
specification: Spec
|
|
150
176
|
diagnostics: Diagnostic[]
|
|
151
177
|
config?: Schema
|
|
178
|
+
files: string[] /* the artifact files plus their embedded assets */
|
|
152
179
|
}
|
|
153
180
|
export interface Diagnostic {
|
|
154
181
|
file: string
|
|
155
182
|
line: number
|
|
156
183
|
column: number
|
|
184
|
+
severity: "error" | "warning"
|
|
157
185
|
message: string
|
|
158
186
|
}
|
|
159
187
|
|
|
160
|
-
/* the supported export formats and describe formats/parts */
|
|
188
|
+
/* the supported export formats and describe formats/parts/compression levels */
|
|
161
189
|
export const formats: readonly [ "json", "json5", "yaml", "toon", "html", "pdf", "md" ]
|
|
162
190
|
export const describeFormats: readonly [ "md", "raw" ]
|
|
163
191
|
export const describeParts: readonly [ "all", "meta", "schema", "spec" ]
|
|
192
|
+
export const compressLevels: readonly [ 0, 1, 2, 3 ]
|
|
164
193
|
export type ExportFormat = typeof formats[number]
|
|
165
194
|
export type DescribeFormat = typeof describeFormats[number]
|
|
166
195
|
export type DescribePart = typeof describeParts[number]
|
|
196
|
+
export type CompressLevel = typeof compressLevels[number]
|
|
167
197
|
|
|
168
198
|
/* the Abstract Syntax Tree (AST) of a parsed specification */
|
|
169
199
|
export type Spec = {
|
|
@@ -251,13 +281,23 @@ $ specbook export \
|
|
|
251
281
|
[-v|--verbose] \
|
|
252
282
|
[-c|--config <schema-yaml-file>] \
|
|
253
283
|
[-b|--basedir <spec-md-file-basedir>] \
|
|
254
|
-
[-o|--output [<format>:]<output-file>]
|
|
284
|
+
[-o|--output [<format>:]<output-file>] \
|
|
285
|
+
[-w|--watch] \
|
|
286
|
+
[...]
|
|
287
|
+
|
|
288
|
+
$ specbook preview \
|
|
289
|
+
[-v|--verbose] \
|
|
290
|
+
[-c|--config <schema-yaml-file>] \
|
|
291
|
+
[-b|--basedir <spec-md-file-basedir>] \
|
|
292
|
+
[-a|--addr <ip-addr>] \
|
|
293
|
+
[-p|--port <tcp-port>]
|
|
255
294
|
|
|
256
295
|
$ specbook describe \
|
|
257
296
|
[-v|--verbose] \
|
|
258
297
|
[-c|--config <schema-yaml-file>] \
|
|
259
298
|
[-b|--basedir <spec-md-file-basedir>] \
|
|
260
299
|
[-e|--embed] \
|
|
300
|
+
[-z|--compress [<level>]] \
|
|
261
301
|
[-f|--format <format>] \
|
|
262
302
|
[-p|--part <part>] \
|
|
263
303
|
[-o|--output <output-file>]
|
|
@@ -279,8 +319,22 @@ Options:
|
|
|
279
319
|
A referenced file which is absent is reported, unless all of its
|
|
280
320
|
artifacts are `optional`, and so is an artifact absent from its present
|
|
281
321
|
file, unless it is `optional` itself. Both `lint` and `export` report
|
|
282
|
-
all diagnostics and fail on any
|
|
283
|
-
|
|
322
|
+
all diagnostics and fail on any error among them (a warning, like a
|
|
323
|
+
lapse of the reference coverage a `referenced` object kind demands,
|
|
324
|
+
is reported only), so a partial or invalid specification is never
|
|
325
|
+
exported.
|
|
326
|
+
The option accepts glob patterns and can be given multiple times: the
|
|
327
|
+
matching files (in the order of the patterns and alphabetically within
|
|
328
|
+
a pattern, where the literal `std` names the bundled standard schema
|
|
329
|
+
configuration, and where a pattern matching no file is an error) are
|
|
330
|
+
merged in order into one effective schema configuration, the later
|
|
331
|
+
files into the earlier ones. The objects merge deeply, while the
|
|
332
|
+
elements of the lists are matched by identity (artifacts by `kind`
|
|
333
|
+
plus `id`/`name`, nested objects by `kind`, properties by `name`, and
|
|
334
|
+
scalar entries by value): a matching element is merged into its
|
|
335
|
+
counterpart, an unmatched one is appended. Every file has to be valid
|
|
336
|
+
YAML on its own, while the merged result alone is validated against
|
|
337
|
+
the schema of the configuration.
|
|
284
338
|
|
|
285
339
|
- `-b|--basedir <spec-md-file-basedir>`:
|
|
286
340
|
The base directory (default: `.`) is the directory the referenced
|
|
@@ -295,18 +349,48 @@ Options:
|
|
|
295
349
|
The `md` export format normalizes the entire corpus into a *single*
|
|
296
350
|
Markdown document.
|
|
297
351
|
|
|
352
|
+
- `-w|--watch` (`export` only):
|
|
353
|
+
Keep the outputs in sync with their sources: after the regular export,
|
|
354
|
+
the referenced artifact files and all assets they embed are observed,
|
|
355
|
+
and every change re-exports the specification once the sources stayed
|
|
356
|
+
silent for one second. A failed re-export is reported and leaves the
|
|
357
|
+
observe loop intact, so a transiently invalid specification does not
|
|
358
|
+
end the watch. The outputs have to be regular files, as `-` (stdout)
|
|
359
|
+
cannot receive a repeated export.
|
|
360
|
+
|
|
361
|
+
- `-a|--addr <ip-addr>`, `-p|--port <tcp-port>` (`preview` only):
|
|
362
|
+
The IP address (default: `127.0.0.1`) and TCP port (default: `12345`)
|
|
363
|
+
the live preview listens on. The HTML export is served on
|
|
364
|
+
`http://<ip-addr>:<tcp-port>/`, kept in sync with its sources exactly
|
|
365
|
+
like `export --watch`, and updated in the browser after every change
|
|
366
|
+
through a WebSocket connection the served page keeps open, where the
|
|
367
|
+
document is replaced in place, so the scroll position survives (a
|
|
368
|
+
failed re-export keeps the previous HTML in place). A status icon
|
|
369
|
+
left of the theme switcher shows the connection state: grey while
|
|
370
|
+
connected, in the search highlight color while disconnected, and
|
|
371
|
+
blinking for 2s after every update.
|
|
372
|
+
|
|
298
373
|
- `-o|--output <output-file>` (`describe` only):
|
|
299
374
|
The output file (default: `-` for stdout) receives the described
|
|
300
375
|
Markdown document.
|
|
301
376
|
|
|
302
377
|
- `-e|--embed` (`describe` only):
|
|
303
|
-
Embed the given YAML schema configuration
|
|
378
|
+
Embed the given YAML schema configuration itself instead of just
|
|
304
379
|
referencing it, so the resulting document describes the specification
|
|
305
380
|
format entirely on its own.
|
|
306
381
|
|
|
382
|
+
- `-z|--compress [<level>]` (`describe` only):
|
|
383
|
+
The compression level (default and bare flag: `1`) of the YAML
|
|
384
|
+
schema configuration (embedded into the Markdown or emitted as the
|
|
385
|
+
raw file content), so the configuration costs fewer tokens: `0`
|
|
386
|
+
emits it verbatim, `1` re-emits it with 2-space indentation, unwrapped
|
|
387
|
+
lines, and without comments, `2` additionally leaves out its `refs` fields, and
|
|
388
|
+
`3` additionally leaves out its `desc` fields of objects and properties.
|
|
389
|
+
|
|
307
390
|
- `-f|--format <format>` (`describe` only):
|
|
308
391
|
The output format (default: `md`) switches from the rendered Markdown
|
|
309
|
-
onto the raw original file content
|
|
392
|
+
onto the raw original file content (the `schema` one compressed by the
|
|
393
|
+
`-z|--compress` level) with `raw`, which is available for
|
|
310
394
|
the file-backed parts `meta` and `schema` only.
|
|
311
395
|
|
|
312
396
|
- `-p|--part <part>` (`describe` only):
|
|
@@ -319,7 +403,10 @@ Options:
|
|
|
319
403
|
The default value of every CLI option `--xxx` can be overridden
|
|
320
404
|
by a corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
321
405
|
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
|
322
|
-
`SPECBOOK_VERBOSE`), while an
|
|
406
|
+
`SPECBOOK_VERBOSE`, `SPECBOOK_ADDR`, `SPECBOOK_PORT`), while an
|
|
407
|
+
explicitly supplied option always wins. As `-c|--config` is repeatable,
|
|
408
|
+
`SPECBOOK_CONFIG` carries a list of patterns separated by the path
|
|
409
|
+
delimiter of the platform (`:` on Unix, `;` on Windows).
|
|
323
410
|
|
|
324
411
|
Example:
|
|
325
412
|
|
package/dst/specbook-api.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic } from "./specbook-diagnostic.js";
|
|
1
|
+
import { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic, type DiagnosticSeverity } from "./specbook-diagnostic.js";
|
|
2
2
|
import { type LintResult } from "./specbook-cmd-lint.js";
|
|
3
3
|
import { parseOutputSpec, formats, type ExportFormat } from "./specbook-cmd-export.js";
|
|
4
|
-
import {
|
|
4
|
+
import { previewAddr, previewPort } from "./specbook-cmd-preview.js";
|
|
5
|
+
import { describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel, type DescribeFormat, type DescribePart, type CompressLevel } from "./specbook-cmd-describe.js";
|
|
5
6
|
import { literal, renderVerbose, type Verbose, type VerboseLevel } from "./specbook-verbose.js";
|
|
6
7
|
export { literal, renderVerbose, type Verbose, type VerboseLevel };
|
|
7
8
|
export { formats, parseOutputSpec, type ExportFormat };
|
|
9
|
+
export { previewAddr, previewPort };
|
|
8
10
|
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
+
export { compressLevels, parseCompressLevel };
|
|
12
|
+
export { type DescribeFormat, type DescribePart, type CompressLevel };
|
|
13
|
+
export { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic, type DiagnosticSeverity };
|
|
11
14
|
export { type LintResult };
|
|
12
15
|
export type { Spec, SpecArtifact, SpecObject, SpecDescription, SpecProperty } from "./specbook-format-spec.js";
|
|
13
16
|
export type { Schema, SchemaObject, SchemaProperty } from "./specbook-format-schema.js";
|
|
@@ -21,25 +24,42 @@ export declare class SpecBook {
|
|
|
21
24
|
private verbose;
|
|
22
25
|
constructor(options?: SpecBookOptions);
|
|
23
26
|
private verboseOf;
|
|
24
|
-
private
|
|
27
|
+
private configFiles;
|
|
25
28
|
private requireConfig;
|
|
26
29
|
init(options: {
|
|
27
|
-
config?: string;
|
|
30
|
+
config?: string[];
|
|
28
31
|
basedir?: string;
|
|
29
32
|
}): Promise<string[]>;
|
|
30
33
|
lint(options: {
|
|
31
|
-
config?: string;
|
|
34
|
+
config?: string[];
|
|
32
35
|
basedir?: string;
|
|
33
36
|
}): Promise<LintResult>;
|
|
37
|
+
private renderFormats;
|
|
34
38
|
export(options: {
|
|
35
|
-
config?: string;
|
|
39
|
+
config?: string[];
|
|
36
40
|
basedir?: string;
|
|
37
41
|
formats?: ExportFormat[];
|
|
42
|
+
realtime?: boolean;
|
|
38
43
|
}): Promise<Buffer[]>;
|
|
44
|
+
private observe;
|
|
45
|
+
watch(options: {
|
|
46
|
+
config?: string[];
|
|
47
|
+
basedir?: string;
|
|
48
|
+
formats?: ExportFormat[];
|
|
49
|
+
realtime?: boolean;
|
|
50
|
+
onExport: (buffers: Buffer[]) => void | Promise<void>;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
preview(options: {
|
|
53
|
+
config?: string[];
|
|
54
|
+
basedir?: string;
|
|
55
|
+
addr?: string;
|
|
56
|
+
port?: number;
|
|
57
|
+
}): Promise<void>;
|
|
39
58
|
describe(options: {
|
|
40
|
-
config?: string;
|
|
59
|
+
config?: string[];
|
|
41
60
|
basedir?: string;
|
|
42
61
|
embed?: boolean;
|
|
62
|
+
compress?: CompressLevel;
|
|
43
63
|
format?: DescribeFormat;
|
|
44
64
|
part?: DescribePart;
|
|
45
65
|
}): Promise<string>;
|
package/dst/specbook-api.js
CHANGED
|
@@ -5,18 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { glob } from "glob";
|
|
8
9
|
import { loadConfig } from "./specbook-config.js";
|
|
9
10
|
import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
|
|
10
11
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
11
12
|
import { lint } from "./specbook-cmd-lint.js";
|
|
12
|
-
import { exportSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
|
|
13
|
-
import {
|
|
13
|
+
import { exportSpecification, watchSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
|
|
14
|
+
import { servePreview, previewAddr, previewPort } from "./specbook-cmd-preview.js";
|
|
15
|
+
import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel } from "./specbook-cmd-describe.js";
|
|
14
16
|
import { requireBrowser } from "./specbook-export-pdf.js";
|
|
15
17
|
import { literal, renderVerbose } from "./specbook-verbose.js";
|
|
16
18
|
/* re-export the central types for API consumers */
|
|
17
19
|
export { literal, renderVerbose };
|
|
18
20
|
export { formats, parseOutputSpec };
|
|
21
|
+
export { previewAddr, previewPort };
|
|
19
22
|
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
23
|
+
export { compressLevels, parseCompressLevel };
|
|
20
24
|
export { renderDiagnostic, renderDiagnosticVerbose };
|
|
21
25
|
/* our own version, taken from the package manifest, which resides one
|
|
22
26
|
level above both the source and the compiled module directory */
|
|
@@ -35,16 +39,32 @@ export class SpecBook {
|
|
|
35
39
|
verboseOf(cmd) {
|
|
36
40
|
return (msg, level = "debug") => this.verbose(cmd, msg, level);
|
|
37
41
|
}
|
|
38
|
-
/* determine the
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/* determine the files of the YAML schema configuration out of the
|
|
43
|
+
glob patterns, in the order of the patterns and alphabetically
|
|
44
|
+
within a pattern, where the literal "std" names the bundled
|
|
45
|
+
standard schema configuration, which is also the fallback if no
|
|
46
|
+
pattern is given at all */
|
|
47
|
+
async configFiles(patterns) {
|
|
48
|
+
if (patterns === undefined || patterns.length === 0)
|
|
49
|
+
return [standardConfig];
|
|
50
|
+
const files = new Array();
|
|
51
|
+
for (const pattern of patterns) {
|
|
52
|
+
if (pattern === "std") {
|
|
53
|
+
files.push(standardConfig);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const matches = await glob(pattern, { nodir: true });
|
|
57
|
+
if (matches.length === 0)
|
|
58
|
+
throw new Error(`no configuration file matches "${pattern}"`);
|
|
59
|
+
files.push(...matches.sort());
|
|
60
|
+
}
|
|
61
|
+
return files;
|
|
42
62
|
}
|
|
43
|
-
/* load
|
|
44
|
-
requireConfig(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const { config, diagnostics } = loadConfig(
|
|
63
|
+
/* load the YAML schema configuration out of its files, failing on any problem */
|
|
64
|
+
requireConfig(files, verbose) {
|
|
65
|
+
verbose("loading schema configuration " +
|
|
66
|
+
files.map((file) => `"${literal(file)}"`).join(", "));
|
|
67
|
+
const { config, diagnostics } = loadConfig(files);
|
|
48
68
|
if (config === undefined)
|
|
49
69
|
throw new Error("invalid configuration:\n" +
|
|
50
70
|
diagnostics.map(renderDiagnostic).join("\n"));
|
|
@@ -54,19 +74,36 @@ export class SpecBook {
|
|
|
54
74
|
below the base directory */
|
|
55
75
|
async init(options) {
|
|
56
76
|
const verbose = this.verboseOf("init");
|
|
57
|
-
return initSpecification({ config: this.requireConfig(options.config, verbose),
|
|
77
|
+
return initSpecification({ config: this.requireConfig(await this.configFiles(options.config), verbose),
|
|
58
78
|
basedir: options.basedir ?? ".", verbose });
|
|
59
79
|
}
|
|
60
80
|
/* lint the specification Markdown files below the base directory */
|
|
61
81
|
async lint(options) {
|
|
62
|
-
return lint({ config: this.
|
|
82
|
+
return lint({ config: await this.configFiles(options.config),
|
|
63
83
|
basedir: options.basedir ?? ".", verbose: this.verboseOf("lint") });
|
|
64
84
|
}
|
|
85
|
+
/* render an already parsed specification into the requested formats
|
|
86
|
+
(strict: any error diagnostic prevents the export, as a partial or
|
|
87
|
+
invalid specification must never be emitted, while the warnings
|
|
88
|
+
are just surfaced as notices), where "realtime" injects the
|
|
89
|
+
client-side script of the live preview into the HTML */
|
|
90
|
+
async renderFormats(result, formats, verbose, realtime) {
|
|
91
|
+
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error"))
|
|
92
|
+
throw new Error("invalid specification:\n" +
|
|
93
|
+
result.diagnostics.map(renderDiagnostic).join("\n"));
|
|
94
|
+
for (const diagnostic of result.diagnostics)
|
|
95
|
+
verbose(renderDiagnostic(diagnostic), "notice");
|
|
96
|
+
if (result.specification.artifacts.length === 0)
|
|
97
|
+
throw new Error("unexportable specification: no artifacts found");
|
|
98
|
+
const buffers = new Array();
|
|
99
|
+
for (const format of formats)
|
|
100
|
+
buffers.push(await exportSpecification(result.specification, format, verbose, result.config, realtime));
|
|
101
|
+
return buffers;
|
|
102
|
+
}
|
|
65
103
|
/* export the specification Markdown files below the base directory
|
|
66
104
|
as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown,
|
|
67
105
|
parsing the input just once and returning one buffer per
|
|
68
|
-
requested format
|
|
69
|
-
as a partial or invalid specification must never be emitted) */
|
|
106
|
+
requested format */
|
|
70
107
|
async export(options) {
|
|
71
108
|
const verbose = this.verboseOf("export");
|
|
72
109
|
const requested = options.formats ?? ["json"];
|
|
@@ -74,23 +111,62 @@ export class SpecBook {
|
|
|
74
111
|
export fail before the specification is even parsed */
|
|
75
112
|
if (requested.includes("pdf"))
|
|
76
113
|
await requireBrowser(verbose);
|
|
77
|
-
|
|
78
|
-
basedir: options.basedir ?? ".", verbose });
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
return this.renderFormats(lint({ config: await this.configFiles(options.config),
|
|
115
|
+
basedir: options.basedir ?? ".", verbose }), requested, verbose, options.realtime === true);
|
|
116
|
+
}
|
|
117
|
+
/* keep an export in sync with its sources (the shared core of
|
|
118
|
+
"watch" and "preview"): every change of an artifact file or of one
|
|
119
|
+
of its embedded assets re-exports the specification and hands the
|
|
120
|
+
buffers to "onExport" once the sources fell silent again. A failed
|
|
121
|
+
re-export is reported and leaves the observe loop intact, so a
|
|
122
|
+
transiently invalid specification does not end the watch. The
|
|
123
|
+
returned promise settles once the initial export is done, while
|
|
124
|
+
the active watcher keeps the process alive afterwards */
|
|
125
|
+
async observe(options, verbose) {
|
|
126
|
+
const config = await this.configFiles(options.config);
|
|
127
|
+
return watchSpecification(async () => {
|
|
128
|
+
/* the lint result carries the files to observe even for an
|
|
129
|
+
invalid specification, so a failing export still keeps the
|
|
130
|
+
observe loop fed with an up-to-date file set */
|
|
131
|
+
const result = lint({ config, basedir: options.basedir ?? ".", verbose });
|
|
132
|
+
try {
|
|
133
|
+
await options.onExport(await this.renderFormats(result, options.formats, verbose, options.realtime));
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
verbose("export failed: " +
|
|
137
|
+
(err instanceof Error ? err.message : String(err)), "notice");
|
|
138
|
+
}
|
|
139
|
+
return result.files;
|
|
140
|
+
}, verbose);
|
|
141
|
+
}
|
|
142
|
+
/* export the specification like "export" and then keep the export
|
|
143
|
+
in sync with its sources (see "observe") */
|
|
144
|
+
async watch(options) {
|
|
145
|
+
const verbose = this.verboseOf("export");
|
|
146
|
+
const requested = options.formats ?? ["json"];
|
|
147
|
+
if (requested.includes("pdf"))
|
|
148
|
+
await requireBrowser(verbose);
|
|
149
|
+
return this.observe({ ...options, formats: requested,
|
|
150
|
+
realtime: options.realtime === true }, verbose);
|
|
151
|
+
}
|
|
152
|
+
/* serve the HTML export of the specification as a live preview on
|
|
153
|
+
"http://<addr>:<port>/": the export is kept in sync with its
|
|
154
|
+
sources (see "observe") and every fresh export is pushed to the
|
|
155
|
+
connected browsers as an in-place document update, through the
|
|
156
|
+
client-side script the "realtime" export injects into the HTML */
|
|
157
|
+
async preview(options) {
|
|
158
|
+
const verbose = this.verboseOf("preview");
|
|
159
|
+
const server = await servePreview({ addr: options.addr ?? previewAddr,
|
|
160
|
+
port: options.port ?? previewPort, verbose });
|
|
161
|
+
return this.observe({ config: options.config, basedir: options.basedir,
|
|
162
|
+
formats: ["html"], realtime: true,
|
|
163
|
+
onExport: ([html]) => server.update(html) }, verbose);
|
|
88
164
|
}
|
|
89
165
|
/* describe the generic SpecBook models and formats as Markdown (or
|
|
90
166
|
as the raw original file content), either entirely or reduced to
|
|
91
167
|
a single part, optionally pointing to the artifacts of the
|
|
92
168
|
particular project, whose YAML schema configuration is validated
|
|
93
|
-
before use */
|
|
169
|
+
before use and optionally emitted compressed by level */
|
|
94
170
|
async describe(options) {
|
|
95
171
|
const verbose = this.verboseOf("describe");
|
|
96
172
|
const format = options.format ?? "md";
|
|
@@ -98,15 +174,14 @@ export class SpecBook {
|
|
|
98
174
|
/* the schema-bearing parts require a schema configuration, so
|
|
99
175
|
they fall back onto the bundled standard one, which is always
|
|
100
176
|
embedded, as its bundled file is no meaningful reference */
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
const config =
|
|
177
|
+
const given = options.config !== undefined && options.config.length > 0;
|
|
178
|
+
const standard = !given && (part === "all" || part === "schema" || options.embed === true);
|
|
179
|
+
const config = given || standard ? await this.configFiles(options.config) : undefined;
|
|
104
180
|
const embed = options.embed === true || standard;
|
|
105
181
|
const basedir = options.basedir ?? (part === "spec" ? "." : undefined);
|
|
106
|
-
|
|
107
|
-
this.requireConfig(config, verbose);
|
|
182
|
+
const schema = config !== undefined ? this.requireConfig(config, verbose) : undefined;
|
|
108
183
|
verbose(`describing the "${literal(part)}" part of the SpecBook models and formats ` +
|
|
109
184
|
`in the "${literal(format)}" format`);
|
|
110
|
-
return describeFormat({ ...options, config, basedir, embed, standard, format, part });
|
|
185
|
+
return describeFormat({ ...options, config, schema, basedir, embed, standard, format, part });
|
|
111
186
|
}
|
|
112
187
|
}
|