@vertekum/schema-builder 0.1.7 → 0.2.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/CHANGELOG.md +22 -0
- package/README.md +6 -2
- package/docs/build.md +31 -6
- package/docs/emission.md +110 -48
- package/docs/expressions.md +12 -2
- package/docs/format.md +75 -0
- package/docs/language.md +41 -26
- package/docs/lint.md +23 -2
- package/docs/modules.md +8 -5
- package/docs/scales.md +29 -2
- package/grammar/dfn.tmLanguage.json +33 -4
- package/package.json +4 -4
- package/src/api.ts +16 -3
- package/src/ast.ts +23 -9
- package/src/build.ts +177 -25
- package/src/cli.ts +360 -32
- package/src/emit.ts +189 -65
- package/src/format.ts +273 -0
- package/src/index.ts +27 -1
- package/src/lint.ts +21 -4
- package/src/parser.ts +98 -13
- package/src/resolve.ts +12 -5
- package/src/tokenizer.ts +56 -4
package/src/cli.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
3
3
|
import type { CommandDescriptor, CommandResult } from '@vertekum/core';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
assertOpenSetsAreNameSets,
|
|
6
|
+
build,
|
|
7
|
+
evaluateProduction,
|
|
8
|
+
type PatternRef,
|
|
9
|
+
type TreeNode,
|
|
10
|
+
} from './build';
|
|
5
11
|
import { emit, isStamped } from './emit';
|
|
12
|
+
import { DfnError } from './error';
|
|
13
|
+
import { fixSource, formatSource, resolveIndent } from './format';
|
|
6
14
|
import { lintModule } from './lint';
|
|
7
|
-
import { resolveModule } from './resolve';
|
|
15
|
+
import { type ResolvedModule, resolveModule } from './resolve';
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* `vertekum schema build [module]` (ADR-0030): expand `.dfn` modules and DECLARE the built JSON
|
|
@@ -17,6 +25,32 @@ import { resolveModule } from './resolve';
|
|
|
17
25
|
/** The slice of the CLI's Project this command reads. */
|
|
18
26
|
interface ProjectDir {
|
|
19
27
|
projectDir: string;
|
|
28
|
+
/** The consumer's `format.indent`, when set — fmt honors it (JSON writes already do). */
|
|
29
|
+
indent?: string | number;
|
|
30
|
+
/** The kernel's config store — where this extension's own settings slice lives. */
|
|
31
|
+
kernel?: { config: { get<T>(id: string): T } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The extension's configured input/output pair, defaults included. */
|
|
35
|
+
function settingsOf(ctx: { project: unknown }): {
|
|
36
|
+
source: string;
|
|
37
|
+
out?: string;
|
|
38
|
+
link: boolean;
|
|
39
|
+
schemaId?: string;
|
|
40
|
+
} {
|
|
41
|
+
const kernel = (ctx.project as ProjectDir).kernel;
|
|
42
|
+
const slice = kernel?.config.get<{
|
|
43
|
+
source?: string;
|
|
44
|
+
out?: string;
|
|
45
|
+
link?: boolean;
|
|
46
|
+
schemaId?: string;
|
|
47
|
+
}>('vtk.schema-builder');
|
|
48
|
+
return {
|
|
49
|
+
source: slice?.source ?? './schemas',
|
|
50
|
+
out: slice?.out,
|
|
51
|
+
link: slice?.link === true,
|
|
52
|
+
schemaId: slice?.schemaId,
|
|
53
|
+
};
|
|
20
54
|
}
|
|
21
55
|
|
|
22
56
|
function modulesUnder(dir: string): string[] {
|
|
@@ -26,22 +60,125 @@ function modulesUnder(dir: string): string[] {
|
|
|
26
60
|
.map((f) => join(dir, f));
|
|
27
61
|
}
|
|
28
62
|
|
|
29
|
-
|
|
30
|
-
|
|
63
|
+
/**
|
|
64
|
+
* What a command's `[module]` argument means: nothing → the default sweep (`./schemas`);
|
|
65
|
+
* a directory → sweep it; a file → that module alone (`explicitFile` — build's rootless
|
|
66
|
+
* error applies only there). A path that exists as neither is an error up front.
|
|
67
|
+
*/
|
|
68
|
+
function resolveModules(
|
|
69
|
+
projectDir: string,
|
|
70
|
+
arg: string | undefined,
|
|
71
|
+
sourceDefault: string,
|
|
72
|
+
): { modules: string[]; explicitFile: boolean; root: string } {
|
|
73
|
+
if (!arg) {
|
|
74
|
+
const root = join(projectDir, sourceDefault);
|
|
75
|
+
return { modules: modulesUnder(root), explicitFile: false, root };
|
|
76
|
+
}
|
|
77
|
+
const target = join(projectDir, arg);
|
|
78
|
+
if (!existsSync(target)) {
|
|
79
|
+
throw new Error(`no such file or directory: ${arg}`);
|
|
80
|
+
}
|
|
81
|
+
if (statSync(target).isDirectory()) {
|
|
82
|
+
return { modules: modulesUnder(target), explicitFile: false, root: target };
|
|
83
|
+
}
|
|
84
|
+
return { modules: [target], explicitFile: true, root: dirname(target) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A module's NATURE: the `scope` pragma, defaulted by root presence. */
|
|
88
|
+
export function natureOf(
|
|
89
|
+
resolved: ResolvedModule,
|
|
90
|
+
): 'document' | 'def' | 'inline' {
|
|
91
|
+
return (
|
|
92
|
+
resolved.module.meta.scope ?? (resolved.module.root ? 'document' : 'def')
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface BuildModuleOptions {
|
|
31
97
|
/** The module name the provenance stamp records; default: the basename. Pass a relative
|
|
32
98
|
* path when modules nest, so same-named modules stamp distinguishably. */
|
|
33
|
-
|
|
99
|
+
label?: string;
|
|
100
|
+
/** Where the artifact lands; default: beside the module. */
|
|
101
|
+
targetDir?: string;
|
|
102
|
+
/** Derived `$id` for this artifact (configured base + path); the `id` pragma wins. */
|
|
103
|
+
schemaId?: string;
|
|
104
|
+
/** Linked emission: resolve a module to the artifact path a cross-file `$ref` should use. */
|
|
105
|
+
linkResolve?: (module: ResolvedModule) => string | undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildModule(
|
|
109
|
+
modulePath: string,
|
|
110
|
+
options: BuildModuleOptions = {},
|
|
34
111
|
): {
|
|
35
112
|
target: string;
|
|
36
113
|
content: string;
|
|
37
114
|
} {
|
|
38
115
|
const resolved = resolveModule(modulePath);
|
|
39
116
|
assertOpenSetsAreNameSets(resolved);
|
|
40
|
-
const
|
|
41
|
-
|
|
117
|
+
const nature = natureOf(resolved);
|
|
118
|
+
if (nature === 'inline') {
|
|
119
|
+
throw new DfnError(
|
|
120
|
+
`${resolved.name}.dfn is scope "inline" — it is never emitted`,
|
|
121
|
+
1,
|
|
122
|
+
1,
|
|
123
|
+
resolved.path,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (nature === 'document' && !resolved.module.root) {
|
|
127
|
+
throw new DfnError(
|
|
128
|
+
`scope "document" requires a root — ${resolved.name}.dfn declares none`,
|
|
129
|
+
1,
|
|
130
|
+
1,
|
|
131
|
+
resolved.path,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const tree = resolved.module.root ? build(resolved) : undefined;
|
|
136
|
+
|
|
137
|
+
// Public patterns in declaration order, bare-evaluated; plus a memoized provider for
|
|
138
|
+
// emission's deep-checks (covering cross-module patterns and def-scope roots).
|
|
139
|
+
const patterns = new Map<string, TreeNode>();
|
|
140
|
+
for (const name of resolved.module.productions.keys()) {
|
|
141
|
+
if (resolved.module.private.has(name)) continue;
|
|
142
|
+
patterns.set(name, evaluateProduction(resolved, name));
|
|
143
|
+
}
|
|
144
|
+
const bareMemo = new Map<string, TreeNode | undefined>();
|
|
145
|
+
const bare = (ref: PatternRef): TreeNode | undefined => {
|
|
146
|
+
const key = ref.module
|
|
147
|
+
? `@${ref.module.path}/${ref.root ? '/root' : ref.name}`
|
|
148
|
+
: ref.name;
|
|
149
|
+
if (!bareMemo.has(key)) {
|
|
150
|
+
let value: TreeNode | undefined;
|
|
151
|
+
if (!ref.module) value = patterns.get(ref.name);
|
|
152
|
+
else if (ref.root) value = build(ref.module);
|
|
153
|
+
else value = evaluateProduction(ref.module, ref.name);
|
|
154
|
+
bareMemo.set(key, value);
|
|
155
|
+
}
|
|
156
|
+
return bareMemo.get(key);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const moduleFile = options.label ?? basename(modulePath);
|
|
160
|
+
const meta = resolved.module.meta;
|
|
42
161
|
return {
|
|
43
|
-
target: join(
|
|
44
|
-
|
|
162
|
+
target: join(
|
|
163
|
+
options.targetDir ?? dirname(modulePath),
|
|
164
|
+
`${resolved.name}.json`,
|
|
165
|
+
),
|
|
166
|
+
content: emit(tree, {
|
|
167
|
+
moduleFile,
|
|
168
|
+
id: meta.id,
|
|
169
|
+
schemaId: options.schemaId,
|
|
170
|
+
title: meta.title,
|
|
171
|
+
description: meta.description,
|
|
172
|
+
scopeKind: nature,
|
|
173
|
+
// A def file is pattern-natured: its body stays unsealed unless the author seals it,
|
|
174
|
+
// so consumers can whole-file-compose it. A document validates alone: sealed.
|
|
175
|
+
sealed: meta.sealed ?? nature === 'document',
|
|
176
|
+
patterns,
|
|
177
|
+
rootDefName:
|
|
178
|
+
nature === 'def' && resolved.module.root ? 'root' : undefined,
|
|
179
|
+
bare,
|
|
180
|
+
linkResolve: options.linkResolve,
|
|
181
|
+
}),
|
|
45
182
|
};
|
|
46
183
|
}
|
|
47
184
|
|
|
@@ -59,41 +196,169 @@ export const schemaLintCommand: CommandDescriptor = {
|
|
|
59
196
|
{
|
|
60
197
|
name: 'module',
|
|
61
198
|
required: false,
|
|
62
|
-
description:
|
|
199
|
+
description:
|
|
200
|
+
"a .dfn file or directory; default: every module under './schemas'",
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
options: [
|
|
204
|
+
{
|
|
205
|
+
flag: '--fix',
|
|
206
|
+
description: "apply mechanical repairs (a misplaced '*') before linting",
|
|
63
207
|
},
|
|
64
208
|
],
|
|
65
209
|
run(ctx): CommandResult {
|
|
66
210
|
const { projectDir } = ctx.project as ProjectDir;
|
|
67
|
-
const modules =
|
|
68
|
-
|
|
69
|
-
|
|
211
|
+
const { modules } = resolveModules(
|
|
212
|
+
projectDir,
|
|
213
|
+
ctx.args.module,
|
|
214
|
+
settingsOf(ctx).source,
|
|
215
|
+
);
|
|
70
216
|
if (modules.length === 0) {
|
|
71
217
|
return { summary: 'no .dfn modules found' };
|
|
72
218
|
}
|
|
73
219
|
|
|
74
220
|
const project = (file: string) =>
|
|
75
221
|
isAbsolute(file) ? relative(projectDir, file) : file;
|
|
76
|
-
|
|
77
|
-
|
|
222
|
+
|
|
223
|
+
// --fix first: repaired content is linted IN MEMORY (the runner owns writing), so the
|
|
224
|
+
// remaining diagnostics describe the state the fix would leave on disk.
|
|
225
|
+
const files: Array<{ path: string; content: string }> = [];
|
|
226
|
+
const fixed: string[] = [];
|
|
227
|
+
const sources = new Map<string, string>();
|
|
228
|
+
if (ctx.options.fix === true) {
|
|
229
|
+
for (const modulePath of modules) {
|
|
230
|
+
if (!existsSync(modulePath)) continue;
|
|
231
|
+
const source = readFileSync(modulePath, 'utf8');
|
|
232
|
+
const result = fixSource(source);
|
|
233
|
+
if (result.fixes.length === 0) continue;
|
|
234
|
+
sources.set(modulePath, result.content);
|
|
235
|
+
files.push({ path: project(modulePath), content: result.content });
|
|
236
|
+
for (const fix of result.fixes) {
|
|
237
|
+
fixed.push(
|
|
238
|
+
`${project(modulePath)}:${fix.line}:${fix.column} ${fix.message}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const found = modules.flatMap((modulePath) =>
|
|
245
|
+
lintModule(modulePath, sources).map((d) => ({
|
|
246
|
+
...d,
|
|
247
|
+
file: project(d.file),
|
|
248
|
+
})),
|
|
78
249
|
);
|
|
250
|
+
const warningLine = (d: (typeof found)[number]) =>
|
|
251
|
+
` ${d.file}:${d.line}:${d.column} warning: ${d.message}`;
|
|
252
|
+
const warnings = found.filter((d) => d.severity === 'warning');
|
|
253
|
+
const diagnostics = found.filter((d) => d.severity !== 'warning');
|
|
79
254
|
|
|
80
255
|
if (diagnostics.length > 0) {
|
|
81
256
|
throw new Error(
|
|
82
257
|
[
|
|
258
|
+
...(fixed.length > 0
|
|
259
|
+
? [`fixed ${fixed.length}:`, ...fixed.map((f) => ` ${f}`)]
|
|
260
|
+
: []),
|
|
83
261
|
`${diagnostics.length} problem(s) in the .dfn modules:`,
|
|
84
262
|
...diagnostics.map(
|
|
85
263
|
(d) => ` ${d.file}:${d.line}:${d.column} ${d.message}`,
|
|
86
264
|
),
|
|
265
|
+
...warnings.map(warningLine),
|
|
87
266
|
].join('\n'),
|
|
88
267
|
);
|
|
89
268
|
}
|
|
269
|
+
const notes = [
|
|
270
|
+
...(fixed.length > 0
|
|
271
|
+
? [`fixed ${fixed.length} problem(s):`, ...fixed.map((f) => ` ${f}`)]
|
|
272
|
+
: []),
|
|
273
|
+
...warnings.map(warningLine),
|
|
274
|
+
`${modules.length} module(s) clean${warnings.length > 0 ? `, ${warnings.length} warning(s)` : ''}`,
|
|
275
|
+
];
|
|
90
276
|
return {
|
|
91
|
-
summary:
|
|
92
|
-
|
|
277
|
+
summary: notes.join('\n'),
|
|
278
|
+
files,
|
|
279
|
+
data: { modules: modules.map(project), fixed, warnings },
|
|
93
280
|
};
|
|
94
281
|
},
|
|
95
282
|
};
|
|
96
283
|
|
|
284
|
+
/**
|
|
285
|
+
* `vertekum schema fmt [module]`: canonical formatting for the `.dfn` sources — block indent
|
|
286
|
+
* from `format.indent`/.editorconfig, canonical spacing, hygiene. A module whose grammar cannot
|
|
287
|
+
* be lexed is skipped with a notice (broken grammar is lint's report — fmt never rewrites what
|
|
288
|
+
* it cannot read). `--check` verifies instead of writing: the CI gate.
|
|
289
|
+
*/
|
|
290
|
+
export const schemaFmtCommand: CommandDescriptor = {
|
|
291
|
+
name: 'schema fmt',
|
|
292
|
+
description: 'format .dfn vocabulary modules canonically',
|
|
293
|
+
args: [
|
|
294
|
+
{
|
|
295
|
+
name: 'module',
|
|
296
|
+
required: false,
|
|
297
|
+
description:
|
|
298
|
+
"a .dfn file or directory; default: every module under './schemas'",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
options: [
|
|
302
|
+
{
|
|
303
|
+
flag: '--check',
|
|
304
|
+
description: 'verify formatting; write nothing',
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
run(ctx): CommandResult {
|
|
308
|
+
const { projectDir, indent } = ctx.project as ProjectDir;
|
|
309
|
+
const { modules } = resolveModules(
|
|
310
|
+
projectDir,
|
|
311
|
+
ctx.args.module,
|
|
312
|
+
settingsOf(ctx).source,
|
|
313
|
+
);
|
|
314
|
+
if (modules.length === 0) {
|
|
315
|
+
return { summary: 'no .dfn modules found', files: [] };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const project = (file: string) =>
|
|
319
|
+
isAbsolute(file) ? relative(projectDir, file) : file;
|
|
320
|
+
const files: Array<{ path: string; content: string }> = [];
|
|
321
|
+
const skipped: string[] = [];
|
|
322
|
+
const unformatted: string[] = [];
|
|
323
|
+
|
|
324
|
+
for (const modulePath of modules) {
|
|
325
|
+
const source = readFileSync(modulePath, 'utf8');
|
|
326
|
+
let formatted: string;
|
|
327
|
+
try {
|
|
328
|
+
formatted = formatSource(source, {
|
|
329
|
+
indent: resolveIndent(modulePath, indent),
|
|
330
|
+
});
|
|
331
|
+
} catch (error) {
|
|
332
|
+
skipped.push(
|
|
333
|
+
`${project(modulePath)} does not lex (${error instanceof Error ? error.message : String(error)}) — run \`vertekum schema lint\``,
|
|
334
|
+
);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (formatted === source) continue;
|
|
338
|
+
if (ctx.options.check === true) {
|
|
339
|
+
unformatted.push(project(modulePath));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
files.push({ path: project(modulePath), content: formatted });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (unformatted.length > 0) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`unformatted .dfn modules: ${unformatted.join(', ')} — run \`vertekum schema fmt\``,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const notes = [
|
|
351
|
+
ctx.options.check === true
|
|
352
|
+
? `${modules.length - skipped.length} module(s) formatted`
|
|
353
|
+
: files.length > 0
|
|
354
|
+
? `formatted ${files.length} module(s)`
|
|
355
|
+
: `${modules.length - skipped.length} module(s) already formatted`,
|
|
356
|
+
...skipped,
|
|
357
|
+
];
|
|
358
|
+
return { summary: notes.join('\n'), files, data: { skipped } };
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
97
362
|
export const schemaBuildCommand: CommandDescriptor = {
|
|
98
363
|
name: 'schema build',
|
|
99
364
|
description: 'build .dfn vocabulary modules into JSON Schema files',
|
|
@@ -101,7 +366,14 @@ export const schemaBuildCommand: CommandDescriptor = {
|
|
|
101
366
|
{
|
|
102
367
|
name: 'module',
|
|
103
368
|
required: false,
|
|
104
|
-
description:
|
|
369
|
+
description:
|
|
370
|
+
"a .dfn file or directory; default: the configured source ('./schemas')",
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'out',
|
|
374
|
+
required: false,
|
|
375
|
+
description:
|
|
376
|
+
'output directory for this run; default: the configured out, else beside each module',
|
|
105
377
|
},
|
|
106
378
|
],
|
|
107
379
|
options: [
|
|
@@ -112,30 +384,86 @@ export const schemaBuildCommand: CommandDescriptor = {
|
|
|
112
384
|
],
|
|
113
385
|
run(ctx): CommandResult {
|
|
114
386
|
const { projectDir } = ctx.project as ProjectDir;
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
387
|
+
const settings = settingsOf(ctx);
|
|
388
|
+
const { modules, explicitFile, root } = resolveModules(
|
|
389
|
+
projectDir,
|
|
390
|
+
ctx.args.module,
|
|
391
|
+
settings.source,
|
|
392
|
+
);
|
|
118
393
|
if (modules.length === 0) {
|
|
119
394
|
return { summary: 'no .dfn modules found', files: [] };
|
|
120
395
|
}
|
|
121
396
|
|
|
397
|
+
// Where a module's artifact lands: the positional `[out]` pairs with THIS invocation's
|
|
398
|
+
// module root; the configured `out` pairs with the configured `source`; a module outside
|
|
399
|
+
// its pair's root builds beside itself — input and output are a pair, not a redirect.
|
|
400
|
+
const outArg = ctx.args.out as string | undefined;
|
|
401
|
+
const outDir = outArg
|
|
402
|
+
? join(projectDir, outArg)
|
|
403
|
+
: settings.out
|
|
404
|
+
? join(projectDir, settings.out)
|
|
405
|
+
: undefined;
|
|
406
|
+
const pairRoot = outArg ? root : join(projectDir, settings.source);
|
|
407
|
+
const targetDirFor = (modulePath: string): string | undefined => {
|
|
408
|
+
if (!outDir) return undefined;
|
|
409
|
+
if (outArg && explicitFile) return outDir;
|
|
410
|
+
const within = relative(pairRoot, dirname(modulePath));
|
|
411
|
+
if (within.startsWith('..') || isAbsolute(within)) return undefined;
|
|
412
|
+
return join(outDir, within);
|
|
413
|
+
};
|
|
414
|
+
|
|
122
415
|
const files: Array<{ path: string; content: string }> = [];
|
|
123
416
|
const skipped: string[] = [];
|
|
124
417
|
const fragments: string[] = [];
|
|
125
418
|
const stale: string[] = [];
|
|
126
419
|
|
|
127
420
|
for (const modulePath of modules) {
|
|
128
|
-
// A
|
|
421
|
+
// A `scope "inline"` module is never emitted — the sweep skips it with a notice;
|
|
129
422
|
// naming one explicitly stays an error (buildModule throws), since silence there
|
|
130
|
-
// would hide
|
|
131
|
-
if (!
|
|
132
|
-
|
|
133
|
-
|
|
423
|
+
// would hide the wrong pragma.
|
|
424
|
+
if (!explicitFile) {
|
|
425
|
+
const resolved = resolveModule(modulePath);
|
|
426
|
+
const nature =
|
|
427
|
+
resolved.module.meta.scope ??
|
|
428
|
+
(resolved.module.root ? 'document' : 'def');
|
|
429
|
+
if (nature === 'inline') {
|
|
430
|
+
fragments.push(relative(projectDir, modulePath));
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
134
433
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
434
|
+
const parentTargetDir = targetDirFor(modulePath) ?? dirname(modulePath);
|
|
435
|
+
// Linked mode: a child links only when THIS project builds it — its artifact must be
|
|
436
|
+
// computable through the same source/out mapping. Package modules inline.
|
|
437
|
+
const linkResolve = settings.link
|
|
438
|
+
? (child: ResolvedModule): string | undefined => {
|
|
439
|
+
const within = relative(projectDir, child.path);
|
|
440
|
+
if (within.startsWith('..') || within.includes('node_modules')) {
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
const childDir = targetDirFor(child.path) ?? dirname(child.path);
|
|
444
|
+
const ref = relative(
|
|
445
|
+
parentTargetDir,
|
|
446
|
+
join(childDir, `${child.name}.json`),
|
|
447
|
+
);
|
|
448
|
+
return ref.startsWith('.') ? ref : `./${ref}`;
|
|
449
|
+
}
|
|
450
|
+
: undefined;
|
|
451
|
+
const derivedId = settings.schemaId
|
|
452
|
+
? settings.schemaId.replace(/\/?$/, '/') +
|
|
453
|
+
relative(
|
|
454
|
+
outDir ?? join(projectDir, settings.source),
|
|
455
|
+
join(
|
|
456
|
+
parentTargetDir,
|
|
457
|
+
`${basename(modulePath).replace(/\.dfn$/, '')}.json`,
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
: undefined;
|
|
461
|
+
const { target, content } = buildModule(modulePath, {
|
|
462
|
+
label: relative(projectDir, modulePath),
|
|
463
|
+
targetDir: targetDirFor(modulePath),
|
|
464
|
+
schemaId: derivedId,
|
|
465
|
+
linkResolve,
|
|
466
|
+
});
|
|
139
467
|
const existing = existsSync(target)
|
|
140
468
|
? readFileSync(target, 'utf8')
|
|
141
469
|
: undefined;
|
|
@@ -161,7 +489,7 @@ export const schemaBuildCommand: CommandDescriptor = {
|
|
|
161
489
|
ctx.options.check === true
|
|
162
490
|
? `${modules.length - fragments.length} module(s) current`
|
|
163
491
|
: `built ${files.length} module(s)`,
|
|
164
|
-
...fragments.map((f) => `${f}
|
|
492
|
+
...fragments.map((f) => `${f} is scope "inline" — skipped`),
|
|
165
493
|
...skipped.map((f) => `${f} has local edits (no stamp) — left as is`),
|
|
166
494
|
];
|
|
167
495
|
return { summary: notes.join('\n'), files, data: { skipped, fragments } };
|