@rse/specbook 1.5.2 → 1.6.1
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/LICENSE.txt +201 -0
- package/README.md +288 -43
- package/dst/specbook-api.d.ts +23 -3
- package/dst/specbook-api.js +133 -59
- package/dst/specbook-cli.js +93 -26
- package/dst/specbook-cmd-describe.js +4 -4
- package/dst/specbook-cmd-export-schema.d.ts +8 -0
- package/dst/specbook-cmd-export-schema.js +441 -0
- package/dst/specbook-cmd-export.js +23 -6
- package/dst/specbook-cmd-init-sample.d.ts +4 -0
- package/dst/specbook-cmd-init-sample.js +322 -0
- package/dst/specbook-cmd-init.d.ts +1 -0
- package/dst/specbook-cmd-init.js +42 -10
- package/dst/specbook-cmd-lint.js +46 -48
- package/dst/specbook-cmd-query.d.ts +30 -0
- package/dst/specbook-cmd-query.js +165 -0
- package/dst/specbook-config.d.ts +9 -1
- package/dst/specbook-config.js +135 -10
- package/dst/specbook-coverage.js +2 -2
- package/dst/specbook-diagram.d.ts +7 -5
- package/dst/specbook-diagram.js +546 -143
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +11 -8
- package/dst/specbook-export-code.js +3 -3
- package/dst/specbook-export-common.d.ts +1 -1
- package/dst/specbook-export-common.js +3 -2
- package/dst/specbook-export-diagram.d.ts +1 -1
- package/dst/specbook-export-diagram.js +25 -7
- package/dst/specbook-export-html-fold.d.ts +16 -6
- package/dst/specbook-export-html-fold.js +65 -44
- package/dst/specbook-export-html-render.js +107 -26
- package/dst/specbook-export-html-search.js +18 -20
- package/dst/specbook-export-html-template.js +38 -9
- package/dst/specbook-export-html-widget.d.ts +2 -0
- package/dst/specbook-export-html-widget.js +219 -19
- package/dst/specbook-export-html.css +208 -38
- package/dst/specbook-export-html.js +96 -53
- package/dst/specbook-export-md.d.ts +2 -1
- package/dst/specbook-export-md.js +7 -8
- package/dst/specbook-export-pdf.js +1 -1
- package/dst/specbook-files.d.ts +2 -0
- package/dst/specbook-files.js +41 -0
- package/dst/specbook-format-meta.yaml +401 -0
- package/dst/specbook-format-pm.yaml +1240 -0
- package/dst/specbook-format-schema.d.ts +33 -3
- package/dst/specbook-format-schema.js +49 -3
- package/dst/specbook-format.md +427 -47
- package/dst/specbook-format.yaml +524 -228
- package/dst/specbook-ignore.js +2 -2
- package/dst/specbook-link.d.ts +4 -0
- package/dst/specbook-link.js +61 -6
- package/dst/specbook-mcp.js +103 -19
- package/dst/specbook-parse-semantic.d.ts +1 -0
- package/dst/specbook-parse-semantic.js +11 -4
- package/dst/specbook-parse-syntax.d.ts +1 -0
- package/dst/specbook-parse-syntax.js +1 -1
- package/dst/specbook-parse.d.ts +1 -1
- package/dst/specbook-parse.js +3 -3
- package/dst/specbook-project.d.ts +2 -0
- package/dst/specbook-project.js +15 -5
- package/dst/specbook-remote.d.ts +28 -0
- package/dst/specbook-remote.js +429 -0
- package/package.json +32 -20
package/dst/specbook-api.js
CHANGED
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
6
|
import * as path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { glob } from "glob";
|
|
10
7
|
import { loadConfig, configStatistics } from "./specbook-config.js";
|
|
11
|
-
import { loadProject, writeProject, projectFile } from "./specbook-project.js";
|
|
8
|
+
import { loadProject, writeProject, projectFile, isRemote } from "./specbook-project.js";
|
|
9
|
+
import { version, standardConfig, metaConfig, pmConfig, isBundled, cacheDir, isCached, expandPattern, resolveConfig, reportOverlaps, search, fetchModes, parseFetchMode } from "./specbook-remote.js";
|
|
12
10
|
import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
|
|
13
11
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
14
12
|
import { lint } from "./specbook-cmd-lint.js";
|
|
13
|
+
import { isBelow } from "./specbook-files.js";
|
|
15
14
|
import { exportSpecification, watchSpecification, parseOutputSpec, formats, requireBrowser, parseOmit, omitAspects } from "./specbook-cmd-export.js";
|
|
15
|
+
import { lintSchema } from "./specbook-cmd-export-schema.js";
|
|
16
|
+
import { SpecBookDocument, renderNodes, queryFormats, parseQueryOutput } from "./specbook-cmd-query.js";
|
|
16
17
|
import { servePreview, previewAddr, previewPort } from "./specbook-cmd-preview.js";
|
|
17
18
|
import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel } from "./specbook-cmd-describe.js";
|
|
18
19
|
import { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity } from "./specbook-verbose.js";
|
|
@@ -24,13 +25,8 @@ export { projectFile };
|
|
|
24
25
|
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
25
26
|
export { compressLevels, parseCompressLevel };
|
|
26
27
|
export { renderDiagnostic, renderDiagnosticVerbose };
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
30
|
-
/* the bundled standard YAML schema configuration, which resides
|
|
31
|
-
alongside the compiled module and is used whenever no particular
|
|
32
|
-
schema configuration is given */
|
|
33
|
-
export const standardConfig = fileURLToPath(new URL("specbook-format.yaml", import.meta.url));
|
|
28
|
+
export { SpecBookDocument, renderNodes, queryFormats, parseQueryOutput };
|
|
29
|
+
export { version, standardConfig, metaConfig, pmConfig, cacheDir, fetchModes, parseFetchMode };
|
|
34
30
|
/* the SpecBook API */
|
|
35
31
|
export class SpecBook {
|
|
36
32
|
verbose;
|
|
@@ -64,32 +60,39 @@ export class SpecBook {
|
|
|
64
60
|
return {
|
|
65
61
|
file: project?.file,
|
|
66
62
|
config: config !== undefined && config.length > 0 ?
|
|
67
|
-
config.map((pattern) => pattern
|
|
68
|
-
project?.config?.map((pattern) => pattern
|
|
63
|
+
config.map((pattern) => isRemote(pattern) ? pattern : given(pattern)) :
|
|
64
|
+
project?.config?.map((pattern) => isRemote(pattern) ? pattern : derived(pattern)),
|
|
69
65
|
basedir: basedir !== undefined ? given(basedir) :
|
|
70
66
|
project?.basedir !== undefined ? derived(project.basedir) :
|
|
71
67
|
options.cwd !== undefined ? cwd : undefined
|
|
72
68
|
};
|
|
73
69
|
}
|
|
70
|
+
/* the options of the remote resolution of a command */
|
|
71
|
+
remoteOptions(options, verbose) {
|
|
72
|
+
return { fetch: parseFetchMode(options.fetch ?? "auto"),
|
|
73
|
+
cwd: path.resolve(options.cwd ?? "."), verbose };
|
|
74
|
+
}
|
|
74
75
|
/* determine the files of the YAML schema configuration out of the
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
pattern is given at all
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
patterns, in the order of the patterns and alphabetically within
|
|
77
|
+
a pattern, where a remote reference (the literals "std", "meta",
|
|
78
|
+
and "pm" included, naming the bundled schema configurations, the
|
|
79
|
+
standard one being also the fallback if no pattern is given at all) is
|
|
80
|
+
resolved into the files of the download cache, and optionally
|
|
81
|
+
collect the display label of every file: the remote reference it
|
|
82
|
+
stems from (plus its base name, if the reference yields several
|
|
83
|
+
files), or its local path */
|
|
84
|
+
async configFiles(patterns, access, labels) {
|
|
85
|
+
if (patterns === undefined || patterns.length === 0) {
|
|
86
|
+
labels?.set(standardConfig, "std");
|
|
81
87
|
return [standardConfig];
|
|
88
|
+
}
|
|
82
89
|
const files = new Array();
|
|
83
90
|
for (const pattern of patterns) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
windowsPathsNoEscape: process.platform === "win32" });
|
|
90
|
-
if (matches.length === 0)
|
|
91
|
-
throw new Error(`no configuration file matches "${pattern}"`);
|
|
92
|
-
files.push(...matches.sort());
|
|
91
|
+
const matches = isRemote(pattern) ? await resolveConfig(pattern, access) : await expandPattern(pattern);
|
|
92
|
+
for (const file of matches)
|
|
93
|
+
labels?.set(file, !isRemote(pattern) ? file :
|
|
94
|
+
matches.length === 1 ? pattern : `${pattern} (${path.basename(file)})`);
|
|
95
|
+
files.push(...matches);
|
|
93
96
|
}
|
|
94
97
|
return files;
|
|
95
98
|
}
|
|
@@ -97,31 +100,34 @@ export class SpecBook {
|
|
|
97
100
|
requireConfig(files, verbose) {
|
|
98
101
|
verbose("loading schema configuration " +
|
|
99
102
|
files.map((file) => `"${literal(file)}"`).join(", "));
|
|
100
|
-
const { config, diagnostics } = loadConfig(files);
|
|
103
|
+
const { config, diagnostics, overlaps } = loadConfig(files);
|
|
101
104
|
if (config === undefined)
|
|
102
105
|
throw new Error("invalid configuration:\n" +
|
|
103
106
|
diagnostics.map(renderDiagnostic).join("\n"));
|
|
107
|
+
reportOverlaps(overlaps, verbose);
|
|
104
108
|
const stats = configStatistics(config);
|
|
105
109
|
verbose(`loaded schema configuration specifying ${literal(stats.objects)} object kind(s) ` +
|
|
106
110
|
`and ${literal(stats.links)} link relationship(s)`);
|
|
107
111
|
return config;
|
|
108
112
|
}
|
|
109
113
|
/* initialize the configured specification artifact files below the
|
|
110
|
-
base directory
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
base directory (where "sample" scaffolds a sample object per
|
|
115
|
+
allowed object kind into them) and persist the effective project
|
|
116
|
+
options into a new project configuration file in the working
|
|
117
|
+
directory, unless one already applies (as a new one would shadow it) */
|
|
113
118
|
async init(options) {
|
|
114
119
|
const verbose = this.verboseOf("init");
|
|
115
120
|
const project = this.project(options, verbose);
|
|
116
|
-
const
|
|
117
|
-
|
|
121
|
+
const config = await this.configFiles(project.config, this.remoteOptions(options, verbose));
|
|
122
|
+
const files = initSpecification({ config: this.requireConfig(config, verbose),
|
|
123
|
+
basedir: project.basedir ?? ".", sample: options.sample === true, verbose });
|
|
118
124
|
if (project.file !== undefined) {
|
|
119
125
|
verbose(`skipping existing project configuration "${literal(project.file)}"`);
|
|
120
126
|
return { files };
|
|
121
127
|
}
|
|
122
128
|
const cwd = path.resolve(options.cwd ?? ".");
|
|
123
129
|
const relative = (value) => path.relative(cwd, path.resolve(cwd, value)) || ".";
|
|
124
|
-
const file = writeProject(cwd, project.config?.map((pattern) => pattern
|
|
130
|
+
const file = writeProject(cwd, project.config?.map((pattern) => isRemote(pattern) ? pattern : relative(pattern)) ?? ["std"], relative(project.basedir ?? "."));
|
|
125
131
|
verbose(`created project configuration "${literal(file)}"`);
|
|
126
132
|
return { files, project: options.cwd !== undefined ? file : path.relative(process.cwd(), file) };
|
|
127
133
|
}
|
|
@@ -131,9 +137,39 @@ export class SpecBook {
|
|
|
131
137
|
async lint(options) {
|
|
132
138
|
const verbose = this.verboseOf("lint");
|
|
133
139
|
const project = this.project(options, verbose);
|
|
134
|
-
return lint({ config: await this.configFiles(project.config),
|
|
140
|
+
return lint({ config: await this.configFiles(project.config, this.remoteOptions(options, verbose)),
|
|
135
141
|
basedir: project.basedir ?? ".", gitignore: options.gitignore === true, verbose });
|
|
136
142
|
}
|
|
143
|
+
/* load the specification Markdown files below the base directory (or,
|
|
144
|
+
under "schema", the effective schema configuration itself, described
|
|
145
|
+
as a specification of the bundled meta schema configuration) as a
|
|
146
|
+
document queryable through ASTq selectors, strictly like "export":
|
|
147
|
+
any error diagnostic prevents the loading, as a partial or invalid
|
|
148
|
+
specification must never be consumed, while the warnings are just
|
|
149
|
+
surfaced as unconditional verbose messages */
|
|
150
|
+
async load(options) {
|
|
151
|
+
const verbose = this.verboseOf("load");
|
|
152
|
+
if (options.schema === true && options.basedir !== undefined)
|
|
153
|
+
throw new Error("the base directory is not applicable to the export of the schema configuration");
|
|
154
|
+
const project = this.project(options, verbose);
|
|
155
|
+
const labels = new Map();
|
|
156
|
+
const config = await this.configFiles(project.config, this.remoteOptions(options, verbose), labels);
|
|
157
|
+
const result = this.parseSource(options, config, labels, project.basedir ?? ".", verbose);
|
|
158
|
+
if (result.config === undefined || result.diagnostics.some((diagnostic) => diagnostic.severity === "error"))
|
|
159
|
+
throw new Error("invalid specification:\n" +
|
|
160
|
+
result.diagnostics.map(renderDiagnostic).join("\n"));
|
|
161
|
+
for (const diagnostic of result.diagnostics)
|
|
162
|
+
verbose(renderDiagnostic(diagnostic), "none");
|
|
163
|
+
return new SpecBookDocument(result.specification, result.config, result.diagnostics, result.origins);
|
|
164
|
+
}
|
|
165
|
+
/* search the bundled schema configurations, the npm
|
|
166
|
+
registry, and GitHub for the shareable schema configurations
|
|
167
|
+
matching a term */
|
|
168
|
+
async search(options) {
|
|
169
|
+
const verbose = this.verboseOf("search");
|
|
170
|
+
verbose(`searching for schema configurations matching "${literal(options.term ?? "")}"`);
|
|
171
|
+
return search(options.term ?? "", this.remoteOptions(options, verbose));
|
|
172
|
+
}
|
|
137
173
|
/* render an already parsed specification into the requested formats
|
|
138
174
|
(strict: any error diagnostic prevents the export, as a partial or
|
|
139
175
|
invalid specification must never be emitted, while the warnings
|
|
@@ -163,26 +199,54 @@ export class SpecBook {
|
|
|
163
199
|
const cwd = options.cwd;
|
|
164
200
|
return cwd === undefined ? options.rebase : options.rebase?.map((dir) => dir !== undefined ? path.resolve(cwd, dir) : dir);
|
|
165
201
|
}
|
|
202
|
+
/* refuse a normalized Markdown output below the base directory (told
|
|
203
|
+
by its re-basing directory), as every ".md" file below the base
|
|
204
|
+
directory is a specification file and hence the output would be
|
|
205
|
+
parsed as one on the next run */
|
|
206
|
+
guardRebase(formats, rebase, basedir) {
|
|
207
|
+
for (const [i, format] of formats.entries()) {
|
|
208
|
+
const dir = rebase?.[i];
|
|
209
|
+
if (format === "md" && dir !== undefined && isBelow(dir, basedir))
|
|
210
|
+
throw new Error(`the Markdown output directory "${dir}" is at or below the base directory ` +
|
|
211
|
+
`"${basedir}", so the output would be parsed as a specification file`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/* parse the source of an export: the specification Markdown files
|
|
215
|
+
below the base directory, or, under "schema", the specification
|
|
216
|
+
generated out of the effective schema configuration itself (to
|
|
217
|
+
which no base directory applies) */
|
|
218
|
+
parseSource(options, config, labels, basedir, verbose) {
|
|
219
|
+
if (options.schema === true)
|
|
220
|
+
return lintSchema({ config, labels, verbose });
|
|
221
|
+
return lint({ config, basedir, gitignore: options.gitignore === true, verbose });
|
|
222
|
+
}
|
|
166
223
|
/* export the specification Markdown files below the base directory
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
224
|
+
(or, under "schema", the effective schema configuration itself,
|
|
225
|
+
described as a specification of the bundled meta schema
|
|
226
|
+
configuration) as JSON, JSON5, YAML, TOON, HTML, PDF, or
|
|
227
|
+
normalized Markdown, parsing the input just once and returning
|
|
228
|
+
one buffer per requested format, where "rebase" names, per
|
|
229
|
+
requested format, the directory of the output file, onto which
|
|
230
|
+
the image references of the normalized Markdown are re-based (an
|
|
231
|
+
absent one keeps them), and "omit" names the (comma-separated)
|
|
232
|
+
aspects the HTML, the PDF, and (the diagrams) the AST leave out
|
|
233
|
+
(see "omitAspects") */
|
|
174
234
|
async export(options) {
|
|
175
235
|
const verbose = this.verboseOf("export");
|
|
176
236
|
const requested = options.formats ?? ["json"];
|
|
177
237
|
const omit = parseOmit(options.omit);
|
|
238
|
+
if (options.schema === true && options.basedir !== undefined)
|
|
239
|
+
throw new Error("the base directory is not applicable to the export of the schema configuration");
|
|
178
240
|
const project = this.project(options, verbose);
|
|
179
|
-
|
|
241
|
+
if (options.schema !== true)
|
|
242
|
+
this.guardRebase(requested, this.rebaseOf(options), project.basedir ?? ".");
|
|
243
|
+
const labels = new Map();
|
|
244
|
+
const config = await this.configFiles(project.config, this.remoteOptions(options, verbose), labels);
|
|
180
245
|
/* a missing browser is an environment problem, so let the PDF
|
|
181
246
|
export fail before the specification is even parsed */
|
|
182
247
|
if (requested.includes("pdf"))
|
|
183
248
|
await requireBrowser(verbose);
|
|
184
|
-
const result =
|
|
185
|
-
basedir: project.basedir ?? ".", gitignore: options.gitignore === true, verbose });
|
|
249
|
+
const result = this.parseSource(options, config, labels, project.basedir ?? ".", verbose);
|
|
186
250
|
return this.renderFormats(result, requested, verbose, { realtime: options.realtime === true, omit }, this.rebaseOf(options));
|
|
187
251
|
}
|
|
188
252
|
/* keep an export in sync with its sources (the shared core of
|
|
@@ -196,8 +260,13 @@ export class SpecBook {
|
|
|
196
260
|
process alive afterwards */
|
|
197
261
|
async observe(options, verbose) {
|
|
198
262
|
const omit = parseOmit(options.omit);
|
|
263
|
+
if (options.schema === true && options.basedir !== undefined)
|
|
264
|
+
throw new Error("the base directory is not applicable to the export of the schema configuration");
|
|
199
265
|
const project = this.project(options, verbose);
|
|
200
|
-
|
|
266
|
+
if (options.schema !== true)
|
|
267
|
+
this.guardRebase(options.formats, this.rebaseOf(options), project.basedir ?? ".");
|
|
268
|
+
const labels = new Map();
|
|
269
|
+
const config = await this.configFiles(project.config, this.remoteOptions(options, verbose), labels);
|
|
201
270
|
/* a missing browser is an environment problem, so let the PDF
|
|
202
271
|
export fail before the specification is even parsed, but
|
|
203
272
|
after the usage problems (exactly like "export") */
|
|
@@ -206,11 +275,13 @@ export class SpecBook {
|
|
|
206
275
|
return watchSpecification(async () => {
|
|
207
276
|
/* the lint result carries the files to observe even for an
|
|
208
277
|
invalid specification, and the configuration files are
|
|
209
|
-
observed, too
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
278
|
+
observed, too (the only sources of the schema export), so
|
|
279
|
+
a failing export (even one due to an invalid configuration)
|
|
280
|
+
still keeps the observe loop fed, while the files of the
|
|
281
|
+
download cache never change */
|
|
282
|
+
const result = this.parseSource(options, config, labels, project.basedir ?? ".", verbose);
|
|
283
|
+
const files = [...config.map((file) => path.resolve(file)), ...result.files]
|
|
284
|
+
.filter((file) => !isCached(file));
|
|
214
285
|
/* an output which is itself an observed source would re-trigger
|
|
215
286
|
the observation with its own write and hence feed an endless
|
|
216
287
|
re-export loop, so refuse it before it is ever written */
|
|
@@ -241,7 +312,7 @@ export class SpecBook {
|
|
|
241
312
|
sources (see "observe") and every fresh export is pushed to the
|
|
242
313
|
connected browsers as an in-place document update, through the
|
|
243
314
|
client-side script the "realtime" export injects into the HTML,
|
|
244
|
-
where "omit"
|
|
315
|
+
where "omit" and "schema" are the ones of "export" */
|
|
245
316
|
async preview(options) {
|
|
246
317
|
const verbose = this.verboseOf("preview");
|
|
247
318
|
/* an invalid omit aspect is a usage problem, so let the preview
|
|
@@ -255,7 +326,7 @@ export class SpecBook {
|
|
|
255
326
|
try {
|
|
256
327
|
return await this.observe({ config: options.config, basedir: options.basedir, cwd: options.cwd,
|
|
257
328
|
formats: ["html"], realtime: true, omit: options.omit, gitignore: options.gitignore,
|
|
258
|
-
onExport: ([html]) => server.update(html) }, verbose);
|
|
329
|
+
schema: options.schema, onExport: ([html]) => server.update(html) }, verbose);
|
|
259
330
|
}
|
|
260
331
|
catch (err) {
|
|
261
332
|
/* a failing release must not mask the primary error */
|
|
@@ -275,13 +346,16 @@ export class SpecBook {
|
|
|
275
346
|
const project = this.project(options, verbose);
|
|
276
347
|
/* the schema-bearing parts require a schema configuration, so
|
|
277
348
|
they fall back onto the bundled standard one, which is always
|
|
278
|
-
embedded (also when named explicitly through "std"
|
|
279
|
-
bundled
|
|
349
|
+
embedded (also when named explicitly through "std", and so are
|
|
350
|
+
the bundled meta and project management ones named through
|
|
351
|
+
"meta" and "pm"), as its bundled file is no meaningful
|
|
352
|
+
reference, and so is a remote one, as its download cache file
|
|
353
|
+
is none either */
|
|
280
354
|
const given = project.config !== undefined;
|
|
281
355
|
const config = given || part === "all" || part === "schema" ?
|
|
282
|
-
await this.configFiles(project.config) : undefined;
|
|
283
|
-
const standard = config !== undefined && config.every(
|
|
284
|
-
const embed = options.embed === true || standard;
|
|
356
|
+
await this.configFiles(project.config, this.remoteOptions(options, verbose)) : undefined;
|
|
357
|
+
const standard = config !== undefined && config.every(isBundled);
|
|
358
|
+
const embed = options.embed === true || standard || (config?.some(isCached) ?? false);
|
|
285
359
|
const basedir = project.basedir ?? (part === "spec" ? "." : undefined);
|
|
286
360
|
const schema = config !== undefined ? this.requireConfig(config, verbose) : undefined;
|
|
287
361
|
verbose(`describing the "${literal(part)}" part of the SpecBook models and formats ` +
|
package/dst/specbook-cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from "node:fs";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { Command, CommanderError } from "commander";
|
|
10
10
|
import chalk from "chalk";
|
|
11
|
-
import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, previewAddr, previewPort, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, parseCompressLevel, parseVerbosity, verbosityOf, projectFile, omitAspects, version } from "./specbook-api.js";
|
|
11
|
+
import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, previewAddr, previewPort, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, parseCompressLevel, parseVerbosity, verbosityOf, projectFile, omitAspects, parseFetchMode, version, renderNodes, queryFormats, parseQueryOutput } from "./specbook-api.js";
|
|
12
12
|
import { serveMcp } from "./specbook-mcp.js";
|
|
13
13
|
/* route verbose messages to stderr, keeping stdout reserved for the
|
|
14
14
|
command outputs and the MCP protocol, and qualify every message with
|
|
@@ -67,14 +67,17 @@ const withVerboseOption = (command) => command
|
|
|
67
67
|
.option("-v, --verbose [level]", "print verbose processing information to stderr " +
|
|
68
68
|
"(0: none, 1: notices, 2: also the details like the coverage ratios, " +
|
|
69
69
|
"3: also the traces like the unreferenced objects; default: 0, bare flag: 1)", envDefault("verbose", "0"));
|
|
70
|
-
/* provide the repeatable schema configuration option, whose files
|
|
71
|
-
glob patterns are merged in order (with "std"
|
|
72
|
-
|
|
73
|
-
to the API, which resolves it through the
|
|
74
|
-
SPECBOOK_CONFIG and the project configuration file */
|
|
70
|
+
/* provide the repeatable schema configuration option, whose files,
|
|
71
|
+
glob patterns, or remote references are merged in order (with "std",
|
|
72
|
+
"meta", and "pm" naming the bundled ones), and determine its value, where
|
|
73
|
+
an absent one is left to the API, which resolves it through the
|
|
74
|
+
environment variable SPECBOOK_CONFIG and the project configuration file */
|
|
75
75
|
const withConfigOption = (command, fallback) => command
|
|
76
|
-
.option("-c, --config <yaml-file>", "YAML schema configuration file
|
|
77
|
-
"(
|
|
76
|
+
.option("-c, --config <yaml-file>", "YAML schema configuration file, glob pattern, or remote reference " +
|
|
77
|
+
"(\"npm:<pkg>[@<range>][/<path>]\", \"github:<user>/<repo>[/<path>][#<ref>]\", " +
|
|
78
|
+
"or an \"http[s]://...\" URL) " +
|
|
79
|
+
"(repeatable, merged in order, \"std\" for the bundled standard schema configuration, " +
|
|
80
|
+
"\"meta\" for the bundled meta one, \"pm\" for the bundled project management one; " +
|
|
78
81
|
`default: SPECBOOK_CONFIG, else the "config" entry of "${projectFile}", else ${fallback})`, (value, previous) => previous.concat(value), new Array());
|
|
79
82
|
const configOf = (opts) => opts.config.length > 0 ? opts.config : undefined;
|
|
80
83
|
/* provide the base directory option, where an absent one is left to
|
|
@@ -83,9 +86,16 @@ const configOf = (opts) => opts.config.length > 0 ? opts.config : undefined;
|
|
|
83
86
|
const withBasedirOption = (command, fallback) => command
|
|
84
87
|
.option("-b, --basedir <directory>", "base directory of the specification Markdown files " +
|
|
85
88
|
`(default: SPECBOOK_BASEDIR, else the "basedir" entry of "${projectFile}", else ${fallback})`);
|
|
89
|
+
/* provide the fetch option, which selects when the remote references
|
|
90
|
+
are fetched through the network, and determine its value */
|
|
91
|
+
const withFetchOption = (command) => command
|
|
92
|
+
.option("-F, --fetch <mode>", "when to fetch the remote references through the network (\"never\": " +
|
|
93
|
+
"the download cache only, \"auto\": whenever the download cache lacks them or is no longer fresh, " +
|
|
94
|
+
"\"always\": ignoring the download cache; default: auto)", envDefault("fetch", "auto"));
|
|
95
|
+
const fetchOf = (opts) => parseFetchMode(opts.fetch);
|
|
86
96
|
/* provide the common options of the specification processing sub-commands,
|
|
87
97
|
for which the YAML schema configuration falls back onto the standard one */
|
|
88
|
-
const withCommonOptions = (command) => withBasedirOption(withConfigOption(withVerboseOption(command), "the bundled standard schema configuration"), "\".\"");
|
|
98
|
+
const withCommonOptions = (command) => withFetchOption(withBasedirOption(withConfigOption(withVerboseOption(command), "the bundled standard schema configuration"), "\".\""));
|
|
89
99
|
/* provide the Git exclude option of the specification *reading*
|
|
90
100
|
sub-commands (not of "init", which creates artifact files instead of
|
|
91
101
|
reading them), which treats an artifact file Git excludes from its
|
|
@@ -93,6 +103,14 @@ const withCommonOptions = (command) => withBasedirOption(withConfigOption(withVe
|
|
|
93
103
|
const withGitignoreOption = (command) => command
|
|
94
104
|
.option("-g, --gitignore", "skip the artifact files excluded by the Git exclude rules " +
|
|
95
105
|
"(the \".gitignore\" files, \"info/exclude\", and the global excludes file)", envDefaultFlag("gitignore", false));
|
|
106
|
+
/* provide the schema option of the HTML-rendering sub-commands, which
|
|
107
|
+
exports the effective schema configuration itself (described as a
|
|
108
|
+
specification of the bundled meta schema configuration) instead of
|
|
109
|
+
the specification below the base directory */
|
|
110
|
+
const withSchemaOption = (command) => command
|
|
111
|
+
.option("-S, --schema", "export the schema configuration itself as a SpecBook document " +
|
|
112
|
+
"(described through the bundled meta schema configuration) instead of the specification " +
|
|
113
|
+
"(the base directory is not applicable then)", envDefaultFlag("schema", false));
|
|
96
114
|
/* provide the repeatable omit option of the HTML-rendering sub-commands,
|
|
97
115
|
whose aspects are left out of the output instead of being foldable,
|
|
98
116
|
and determine its value (validated by the API) */
|
|
@@ -130,13 +148,17 @@ withVerboseOption(program.command("mcp"))
|
|
|
130
148
|
verboseOf(opts)("mcp", "starting MCP server on stdio", "notice");
|
|
131
149
|
await serveMcp(verboseOf(opts, "mcp"));
|
|
132
150
|
});
|
|
133
|
-
/* the init command creates the configured artifact files
|
|
134
|
-
|
|
151
|
+
/* the init command creates the configured artifact files (optionally
|
|
152
|
+
with a sample object per allowed object kind) and, unless one already
|
|
153
|
+
applies, the project configuration file */
|
|
135
154
|
withCommonOptions(program.command("init"))
|
|
136
155
|
.description("initialize the configured specification artifact files below the base directory")
|
|
156
|
+
.option("-s, --sample", "scaffold a sample object per allowed object kind into the artifact files, " +
|
|
157
|
+
"with placeholder values satisfying the schema configuration", envDefaultFlag("sample", false))
|
|
137
158
|
.action(async (opts) => {
|
|
138
159
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
139
|
-
const result = await specbook.init({ config: configOf(opts), basedir: opts.basedir
|
|
160
|
+
const result = await specbook.init({ config: configOf(opts), basedir: opts.basedir,
|
|
161
|
+
fetch: fetchOf(opts), sample: opts.sample });
|
|
140
162
|
await writeStdout((result.files.length > 0 ?
|
|
141
163
|
`initialized artifact file(s): ${result.files.join(", ")}\n` :
|
|
142
164
|
"no artifact files were created\n") +
|
|
@@ -150,7 +172,7 @@ withGitignoreOption(withCommonOptions(program.command("lint")))
|
|
|
150
172
|
const verbose = verboseOf(opts);
|
|
151
173
|
const specbook = new SpecBook({ verbose });
|
|
152
174
|
const result = await specbook.lint({ config: configOf(opts), basedir: opts.basedir,
|
|
153
|
-
gitignore: opts.gitignore });
|
|
175
|
+
fetch: fetchOf(opts), gitignore: opts.gitignore });
|
|
154
176
|
const detailed = parseVerbosity(opts.verbose) > 0;
|
|
155
177
|
for (const diagnostic of result.diagnostics)
|
|
156
178
|
await writeStdout(detailed ?
|
|
@@ -162,9 +184,9 @@ withGitignoreOption(withCommonOptions(program.command("lint")))
|
|
|
162
184
|
verbose("lint", "specification valid", "notice");
|
|
163
185
|
});
|
|
164
186
|
/* the export command parses the input once and writes every output */
|
|
165
|
-
withOmitOption(withGitignoreOption(withCommonOptions(program.command("export"))))
|
|
187
|
+
withSchemaOption(withOmitOption(withGitignoreOption(withCommonOptions(program.command("export")))))
|
|
166
188
|
.description("export the specification Markdown files below the base directory " +
|
|
167
|
-
"as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
189
|
+
"(or the schema configuration itself) as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
168
190
|
.option("-w, --watch", "keep the outputs in sync by re-exporting on every source change", envDefaultFlag("watch", false))
|
|
169
191
|
.option("-o, --output [<format>:]<output-file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
170
192
|
"from the filename extension unless explicitly prefixed", (value, previous) => previous.concat(value), new Array())
|
|
@@ -195,17 +217,18 @@ withOmitOption(withGitignoreOption(withCommonOptions(program.command("export")))
|
|
|
195
217
|
const formats = distinct.map(({ format }) => format);
|
|
196
218
|
const rebase = distinct.map(rebaseOf);
|
|
197
219
|
if (opts.watch)
|
|
198
|
-
await specbook.watch({ config: configOf(opts), basedir: opts.basedir,
|
|
199
|
-
formats, rebase, omit: omitOf(opts), gitignore: opts.gitignore,
|
|
220
|
+
await specbook.watch({ config: configOf(opts), basedir: opts.basedir, fetch: fetchOf(opts),
|
|
221
|
+
formats, rebase, omit: omitOf(opts), gitignore: opts.gitignore, schema: opts.schema,
|
|
200
222
|
outputs: outputs.map(({ output }) => output), onExport: write });
|
|
201
223
|
else
|
|
202
|
-
await write(await specbook.export({ config: configOf(opts), basedir: opts.basedir,
|
|
203
|
-
formats, rebase, omit: omitOf(opts), gitignore: opts.gitignore }));
|
|
224
|
+
await write(await specbook.export({ config: configOf(opts), basedir: opts.basedir, fetch: fetchOf(opts),
|
|
225
|
+
formats, rebase, omit: omitOf(opts), gitignore: opts.gitignore, schema: opts.schema }));
|
|
204
226
|
});
|
|
205
227
|
/* the preview command serves the HTML export live in the browser */
|
|
206
|
-
withOmitOption(withGitignoreOption(withCommonOptions(program.command("preview"))))
|
|
228
|
+
withSchemaOption(withOmitOption(withGitignoreOption(withCommonOptions(program.command("preview")))))
|
|
207
229
|
.description("serve the HTML export of the specification Markdown files below the base " +
|
|
208
|
-
"directory as a live preview, re-exported and
|
|
230
|
+
"directory (or of the schema configuration itself) as a live preview, re-exported and " +
|
|
231
|
+
"reloaded on every source change")
|
|
209
232
|
.option("-a, --addr <ip-addr>", "IP address to listen on", envDefault("addr", previewAddr))
|
|
210
233
|
.option("-p, --port <tcp-port>", "TCP port to listen on", envDefault("port", String(previewPort)))
|
|
211
234
|
.action(async (opts) => {
|
|
@@ -213,17 +236,45 @@ withOmitOption(withGitignoreOption(withCommonOptions(program.command("preview"))
|
|
|
213
236
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
214
237
|
throw new Error(`invalid TCP port "${opts.port}"`);
|
|
215
238
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
216
|
-
await specbook.preview({ config: configOf(opts), basedir: opts.basedir,
|
|
217
|
-
omit: omitOf(opts), gitignore: opts.gitignore });
|
|
239
|
+
await specbook.preview({ config: configOf(opts), basedir: opts.basedir, fetch: fetchOf(opts),
|
|
240
|
+
addr: opts.addr, port, omit: omitOf(opts), gitignore: opts.gitignore, schema: opts.schema });
|
|
241
|
+
});
|
|
242
|
+
/* the query command loads the specification and writes the
|
|
243
|
+
nodes an ASTq selector matches as JSON, JSON5, YAML, or TOON */
|
|
244
|
+
withSchemaOption(withGitignoreOption(withCommonOptions(program.command("query"))))
|
|
245
|
+
.description("query the specification Markdown files below the base directory " +
|
|
246
|
+
"(or the schema configuration itself) with an ASTq selector, writing the matched nodes " +
|
|
247
|
+
"as JSON, JSON5, YAML, or TOON")
|
|
248
|
+
.argument("<selector>", "the ASTq selector of the nodes to write")
|
|
249
|
+
.option("-P, --param <name>=<value>", "value of a \"{name}\" parameter of the selector (repeatable)", (value, previous) => previous.concat(value), new Array())
|
|
250
|
+
.option("-f, --format <format>", `output format (${queryFormats.join(", ")}; ` +
|
|
251
|
+
"default: inferred from the output file like for \"export\", else json)", envDefault("format"))
|
|
252
|
+
.option("-o, --output [<format>:]<output-file>", "output file (\"-\" for stdout)", envDefault("output", "-"))
|
|
253
|
+
.action(async (selector, opts) => {
|
|
254
|
+
const verbose = verboseOf(opts);
|
|
255
|
+
const output = parseQueryOutput(opts.output, opts.format);
|
|
256
|
+
const fallback = envDefault("param");
|
|
257
|
+
const given = opts.param.length > 0 ? opts.param : (fallback !== undefined ? [fallback] : []);
|
|
258
|
+
const params = {};
|
|
259
|
+
for (const param of given) {
|
|
260
|
+
const m = (/^([^=]+)=(.*)$/s).exec(param);
|
|
261
|
+
if (m === null)
|
|
262
|
+
throw new Error(`invalid query parameter "${param}" (expected "<name>=<value>")`);
|
|
263
|
+
params[m[1]] = m[2];
|
|
264
|
+
}
|
|
265
|
+
const specbook = new SpecBook({ verbose });
|
|
266
|
+
const document = await specbook.load({ config: configOf(opts), basedir: opts.basedir,
|
|
267
|
+
fetch: fetchOf(opts), gitignore: opts.gitignore, schema: opts.schema });
|
|
268
|
+
await writeOutput(output.output, renderNodes(document.query(selector, params), output.format), "query", verbose);
|
|
218
269
|
});
|
|
219
270
|
/* the describe command also describes the generic SpecBook models and
|
|
220
271
|
formats alone, so its YAML schema configuration stays optional */
|
|
221
|
-
withBasedirOption(withConfigOption(withVerboseOption(program.command("describe")), "the bundled standard schema configuration, embedded"), "none")
|
|
272
|
+
withFetchOption(withBasedirOption(withConfigOption(withVerboseOption(program.command("describe")), "the bundled standard schema configuration, embedded"), "none"))
|
|
222
273
|
.description("describe the SpecBook models and formats as Markdown")
|
|
223
274
|
.option("-e, --embed", "embed the given YAML schema configuration instead of just referencing it", envDefaultFlag("embed", false))
|
|
224
275
|
.option("-z, --compress [level]", "compression level of the emitted YAML schema configuration " +
|
|
225
276
|
"(0: verbatim, 1: re-emitted with 2-space indentation and without comments, " +
|
|
226
|
-
"2: also without \"refs\", \"diagram\", and \"format\" fields, " +
|
|
277
|
+
"2: also without \"refs\", \"badge\", \"multiple\", \"diagram\", and \"format\" fields, " +
|
|
227
278
|
"3: also without \"desc\" fields; default and bare flag: 1)", envDefault("compress", "1"))
|
|
228
279
|
.option("-f, --format <format>", `output format (${describeFormats.join(", ")})`, envDefault("format", "md"))
|
|
229
280
|
.option("-p, --part <part>", `document part (${describeParts.join(", ")})`, envDefault("part", "all"))
|
|
@@ -232,10 +283,26 @@ withBasedirOption(withConfigOption(withVerboseOption(program.command("describe")
|
|
|
232
283
|
const verbose = verboseOf(opts);
|
|
233
284
|
const specbook = new SpecBook({ verbose });
|
|
234
285
|
const text = await specbook.describe({ config: configOf(opts), basedir: opts.basedir,
|
|
235
|
-
embed: opts.embed, compress: parseCompressLevel(opts.compress),
|
|
286
|
+
fetch: fetchOf(opts), embed: opts.embed, compress: parseCompressLevel(opts.compress),
|
|
236
287
|
format: parseDescribeFormat(opts.format), part: parseDescribePart(opts.part) });
|
|
237
288
|
await writeOutput(opts.output, text, "describe", verbose);
|
|
238
289
|
});
|
|
290
|
+
/* the search command finds the bundled schema configurations
|
|
291
|
+
and the shareable ones of the npm registry and GitHub */
|
|
292
|
+
withFetchOption(withVerboseOption(program.command("search")))
|
|
293
|
+
.description("search the bundled schema configurations, the npm registry, " +
|
|
294
|
+
"and GitHub for shareable schema configurations")
|
|
295
|
+
.argument("[term...]", "the words a matching schema configuration has to carry")
|
|
296
|
+
.action(async (term, opts) => {
|
|
297
|
+
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
298
|
+
const hits = await specbook.search({ term: term.join(" "), fetch: fetchOf(opts) });
|
|
299
|
+
if (hits.length === 0)
|
|
300
|
+
await writeStdout("no matching schema configurations found\n");
|
|
301
|
+
const width = Math.max(0, ...hits.map((hit) => hit.ref.length));
|
|
302
|
+
for (const hit of hits)
|
|
303
|
+
await writeStdout((`${hit.ref.padEnd(width)} ` +
|
|
304
|
+
`${(hit.version ?? "").padEnd(8)} ${hit.description ?? ""}`).trimEnd() + "\n");
|
|
305
|
+
});
|
|
239
306
|
/* run the command line program */
|
|
240
307
|
try {
|
|
241
308
|
await program.parseAsync();
|
|
@@ -21,9 +21,9 @@ export const parseDescribeFormat = (value) => parseChoice(describeFormats, "form
|
|
|
21
21
|
export const parseDescribePart = (value) => parseChoice(describeParts, "part", value);
|
|
22
22
|
/* the supported compression levels of the emitted YAML schema
|
|
23
23
|
configuration: verbatim (0), re-emitted with 2-space indentation and
|
|
24
|
-
without comments (1), additionally without its "refs", "
|
|
25
|
-
"format" fields (2), and additionally
|
|
26
|
-
objects and properties (3) */
|
|
24
|
+
without comments (1), additionally without its "refs", "badge",
|
|
25
|
+
"multiple", "diagram", and "format" fields (2), and additionally
|
|
26
|
+
without its "desc" fields of objects and properties (3) */
|
|
27
27
|
export const compressLevels = [0, 1, 2, 3];
|
|
28
28
|
/* parse and validate a compression level specification,
|
|
29
29
|
where a bare flag (or a boolean word) selects the plain re-emitting */
|
|
@@ -48,7 +48,7 @@ const compressYaml = (yaml, level) => {
|
|
|
48
48
|
if (level === 0)
|
|
49
49
|
return yaml;
|
|
50
50
|
const doc = parseDocument(yaml);
|
|
51
|
-
const drop = [["refs", "diagram", "format"], ["desc"]].slice(0, level - 1).flat();
|
|
51
|
+
const drop = [["refs", "badge", "multiple", "diagram", "format"], ["desc"]].slice(0, level - 1).flat();
|
|
52
52
|
doc.commentBefore = null;
|
|
53
53
|
doc.comment = null;
|
|
54
54
|
visit(doc, {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Verbose } from "./specbook-verbose.js";
|
|
2
|
+
import { type LintResult } from "./specbook-cmd-lint.js";
|
|
3
|
+
export interface SchemaLintOptions {
|
|
4
|
+
config: string[];
|
|
5
|
+
labels: Map<string, string>;
|
|
6
|
+
verbose: Verbose;
|
|
7
|
+
}
|
|
8
|
+
export declare const lintSchema: (options: SchemaLintOptions) => LintResult;
|