@loomcli/core 0.5.0 → 0.7.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/NOTICE +34 -0
- package/dist/application.d.ts +18 -2
- package/dist/application.js +302 -75
- package/dist/bindings.d.ts +15 -10
- package/dist/bindings.js +34 -15
- package/dist/capture.d.ts +65 -0
- package/dist/capture.js +99 -0
- package/dist/chain.d.ts +15 -6
- package/dist/chain.js +40 -20
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +65 -23
- package/dist/command.js +734 -236
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +108 -24
- package/dist/errors.js +324 -53
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +41 -8
- package/dist/extension.js +142 -57
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +106 -23
- package/dist/globals.d.ts +29 -21
- package/dist/globals.js +117 -46
- package/dist/glyphs.generated.js +1 -1
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +68 -0
- package/dist/input-rules.js +152 -0
- package/dist/inspect.d.ts +12 -12
- package/dist/inspect.js +92 -48
- package/dist/lanes.js +1 -1
- package/dist/locate.js +4 -4
- package/dist/options.d.ts +30 -2
- package/dist/options.js +143 -46
- package/dist/output.d.ts +9 -2
- package/dist/output.js +18 -2
- package/dist/plain.d.ts +56 -0
- package/dist/plain.js +238 -0
- package/dist/plugin-rules.d.ts +68 -0
- package/dist/plugin-rules.js +164 -0
- package/dist/plugin-settings.d.ts +18 -0
- package/dist/plugin-settings.js +38 -0
- package/dist/plugin.d.ts +27 -15
- package/dist/plugin.js +429 -144
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +6 -4
- package/dist/sources.js +25 -16
- package/dist/style-layout.js +2 -2
- package/dist/style-width.d.ts +13 -0
- package/dist/style-width.js +170 -0
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +13 -3
- package/dist/unicode.generated.d.ts +27 -0
- package/dist/unicode.generated.js +1036 -0
- package/dist/validation.d.ts +69 -7
- package/dist/validation.js +211 -67
- package/dist/view.d.ts +61 -22
- package/dist/view.js +168 -81
- package/licenses/unicode-LICENSE.txt +41 -0
- package/licenses/uucode-LICENSE.md +35 -0
- package/package.json +9 -5
package/dist/validation.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { CaptureFaults } from './capture.js';
|
|
3
|
+
import type { Finding } from './diagnostic-text.js';
|
|
4
|
+
import { DeclarationError } from './errors.js';
|
|
5
|
+
import type { InputSite } from './facts.js';
|
|
2
6
|
import type { OptionValues } from './options.js';
|
|
3
7
|
import type { ArgumentConfig, ArgumentValue, Host, OptionConfig, OptionValue } from './types.js';
|
|
4
8
|
/**
|
|
@@ -73,12 +77,62 @@ declare class ValidatedInputs {
|
|
|
73
77
|
read(input: InputDeclaration): unknown;
|
|
74
78
|
}
|
|
75
79
|
export type { ValidatedInputs };
|
|
80
|
+
/** The faults a config's capture raises: those of every capture, and a default nested too deep. */
|
|
81
|
+
export interface ConfigFaults extends CaptureFaults {
|
|
82
|
+
/** The default holds a path deeper than `defaultLevels`; the finding prints it elided. */
|
|
83
|
+
readonly tooDeep: () => DeclarationError;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Authoring's one read of a config, through `captureDeclaration`: the prototype verdict, the copy of
|
|
87
|
+
* every own string key, the copy of its `extensions` list, and the snapshot of its default, inside
|
|
88
|
+
* one try. Every later check, the registry entry, and every sentence read the copy and never the
|
|
89
|
+
* author's object again, so a getter runs once and the caller's later changes reach nothing. The
|
|
90
|
+
* default is copied and frozen to `defaultLevels` levels, cycles included, and that copy is the
|
|
91
|
+
* value the graph publishes and a run validates; every other property is captured as declared,
|
|
92
|
+
* because core clones no library object. A read that throws, from a getter or a proxy trap, is the
|
|
93
|
+
* unreadable fault, named by the key it threw in, a default nested deeper is the too-deep fault,
|
|
94
|
+
* and a value that is not a plain object is the not-an-object fault.
|
|
95
|
+
*/
|
|
96
|
+
export declare function captureConfig<Config extends ArgumentConfig | OptionConfig>(config: Config, faults: ConfigFaults): Config;
|
|
97
|
+
/** The fault of a default nested deeper than `defaultLevels`, declared by `subject`. */
|
|
98
|
+
export declare function defaultDepthFault(subject: string, findings: readonly Finding[]): DeclarationError;
|
|
76
99
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
100
|
+
* The config every `argument()`, `option()`, and `globalOption()` call, and every input a lifecycle
|
|
101
|
+
* hook declares, reads through `captureConfig`. A call in JavaScript that supplies none, or a value
|
|
102
|
+
* of another kind, reports a declaration fault and not a TypeError, and so does a config whose read
|
|
103
|
+
* throws. Each fault marks the config on the call at `place`.
|
|
80
104
|
*/
|
|
81
|
-
export declare function
|
|
105
|
+
export declare function captureInputConfig<Config extends ArgumentConfig | OptionConfig>(declared: {
|
|
106
|
+
readonly config: Config;
|
|
107
|
+
readonly kind: InputDeclaration['kind'];
|
|
108
|
+
readonly name: string;
|
|
109
|
+
}, place: InputPlace): Config;
|
|
110
|
+
/**
|
|
111
|
+
* A declaration error names the declaration, because the author reads the declaration to fix it.
|
|
112
|
+
* An argument declares and reads under one name, so the two namings differ for options alone.
|
|
113
|
+
*/
|
|
114
|
+
export declare function declarationSubject(input: InputDeclaration): string;
|
|
115
|
+
/** Where the call that declared one input sits: the call's name and the Command it is on. */
|
|
116
|
+
export type InputPlace = Pick<Finding, 'call' | 'path'>;
|
|
117
|
+
/**
|
|
118
|
+
* Where one input was declared: a global option at its `globalOption()` call, and every other input
|
|
119
|
+
* at its own `argument()` or `option()` call on the Command at `path`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function inputPlace(input: InputDeclaration, scope: {
|
|
122
|
+
readonly global: boolean;
|
|
123
|
+
readonly path: readonly string[];
|
|
124
|
+
}): InputPlace;
|
|
125
|
+
/**
|
|
126
|
+
* The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
|
|
127
|
+
* finding for an input's own call starts from it, so each marks the call the same way. `subject`
|
|
128
|
+
* is the sentence's name for the input.
|
|
129
|
+
*/
|
|
130
|
+
export declare function declaringSite(input: InputDeclaration, place: InputPlace, subject?: string): InputSite;
|
|
131
|
+
/**
|
|
132
|
+
* One input's site with its config printed elided, for a fault judged before the config is read,
|
|
133
|
+
* such as an invalid name, so the fault reads none of the config.
|
|
134
|
+
*/
|
|
135
|
+
export declare function configUnread(site: InputSite): InputSite;
|
|
82
136
|
/**
|
|
83
137
|
* The declaration flag that sends an omitted value to its own validator. Every declaration reads it
|
|
84
138
|
* here, and the declaration rules below reject it wherever another rule already decides absence.
|
|
@@ -90,6 +144,11 @@ export declare function validatesOmission(input: InputDeclaration): boolean;
|
|
|
90
144
|
* helper, so a rejected item reads alike wherever its diagnostic is written.
|
|
91
145
|
*/
|
|
92
146
|
export declare function issuePath(issue: StandardSchemaV1.Issue): string | undefined;
|
|
147
|
+
/** One declaration and where it was declared, which its faults' findings rebuild. */
|
|
148
|
+
export interface SitedInput {
|
|
149
|
+
readonly input: InputDeclaration;
|
|
150
|
+
readonly site: InputSite;
|
|
151
|
+
}
|
|
93
152
|
/**
|
|
94
153
|
* Every declaration rule that reads the declaration alone. It is synchronous, so the call that
|
|
95
154
|
* declares an input applies it, and build applies it to an input a lifecycle hook declared; only
|
|
@@ -97,10 +156,13 @@ export declare function issuePath(issue: StandardSchemaV1.Issue): string | undef
|
|
|
97
156
|
* contributor that declares under its own name, such as a plugin, supplies the subject its
|
|
98
157
|
* diagnostics read with; every other caller is named by the declaration itself.
|
|
99
158
|
*/
|
|
100
|
-
export declare function checkDeclarations(inputs: readonly
|
|
159
|
+
export declare function checkDeclarations(inputs: readonly SitedInput[], named?: string): void;
|
|
160
|
+
/** The call that declared one input and the Command it sits on, which a default's fault rebuilds. */
|
|
161
|
+
export type InputPlaces = ReadonlyMap<InputDeclaration, InputPlace>;
|
|
101
162
|
/**
|
|
102
163
|
* Every declared default, validated before any token is read. The host is captured by then, so a
|
|
103
|
-
* default's validator reads the same Host its action will, under the `default` phase.
|
|
164
|
+
* default's validator reads the same Host its action will, under the `default` phase. `places`
|
|
165
|
+
* says where each declaration sits, which a rejected default's finding rebuilds.
|
|
104
166
|
*/
|
|
105
|
-
export declare function prepareInputs(inputs: ScopedInputs, host: Host): Promise<DefaultValues>;
|
|
167
|
+
export declare function prepareInputs(inputs: ScopedInputs, host: Host, places: InputPlaces): Promise<DefaultValues>;
|
|
106
168
|
export declare function validateValues(invocation: Invocation): Promise<ValidatedInputs>;
|
package/dist/validation.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { captureDeclaration, elidedRead, unreadableFault } from './capture.js';
|
|
1
2
|
import { schemaOptions } from './context.js';
|
|
2
|
-
import {
|
|
3
|
+
import { escapeControlCharacters } from './controls.js';
|
|
4
|
+
import { elided, spelled } from './diagnostic-text.js';
|
|
5
|
+
import { asSentence, DeclarationError, InputError, quoted, reasonOf } from './errors.js';
|
|
6
|
+
import { callSite, factFault, flagFault, partOf, siteFinding } from './facts.js';
|
|
7
|
+
import { booleanOptionValueRule, defaultDepth, defaultLevels, defaultShape, invalidDefault, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
|
|
3
8
|
import { booleanValue } from './options.js';
|
|
9
|
+
import { boundedSnapshot, NestedTooDeepError, shallowList } from './plain.js';
|
|
10
|
+
import { notAnObject } from './plugin-rules.js';
|
|
11
|
+
import { validatorFailed } from './rules.js';
|
|
4
12
|
/** Every declaration in validation order: the globals first, then the reading Command's own. */
|
|
5
13
|
function scoped(inputs) {
|
|
6
14
|
return [
|
|
@@ -47,20 +55,98 @@ class ValidatedInputs {
|
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
50
|
-
* Authoring's
|
|
51
|
-
*
|
|
52
|
-
*
|
|
58
|
+
* Authoring's one read of a config, through `captureDeclaration`: the prototype verdict, the copy of
|
|
59
|
+
* every own string key, the copy of its `extensions` list, and the snapshot of its default, inside
|
|
60
|
+
* one try. Every later check, the registry entry, and every sentence read the copy and never the
|
|
61
|
+
* author's object again, so a getter runs once and the caller's later changes reach nothing. The
|
|
62
|
+
* default is copied and frozen to `defaultLevels` levels, cycles included, and that copy is the
|
|
63
|
+
* value the graph publishes and a run validates; every other property is captured as declared,
|
|
64
|
+
* because core clones no library object. A read that throws, from a getter or a proxy trap, is the
|
|
65
|
+
* unreadable fault, named by the key it threw in, a default nested deeper is the too-deep fault,
|
|
66
|
+
* and a value that is not a plain object is the not-an-object fault.
|
|
53
67
|
*/
|
|
54
|
-
export function captureConfig(config) {
|
|
55
|
-
const
|
|
56
|
-
return
|
|
68
|
+
export function captureConfig(config, faults) {
|
|
69
|
+
const { notAnObject: notAnObjectFault, tooDeep, unreadable } = faults;
|
|
70
|
+
return captureDeclaration(config, (copy, read) => {
|
|
71
|
+
// Presence is the key, so a declared `default: undefined` stays a default.
|
|
72
|
+
read.nested(copy, 'default', (value) => boundedSnapshot(value, defaultLevels));
|
|
73
|
+
read.nested(copy, 'extensions', shallowList);
|
|
74
|
+
}, {
|
|
75
|
+
notAnObject: notAnObjectFault,
|
|
76
|
+
unreadable: (thrown, slot) => thrown instanceof NestedTooDeepError ? tooDeep() : unreadable(thrown, slot),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** The fault of a default nested deeper than `defaultLevels`, declared by `subject`. */
|
|
80
|
+
export function defaultDepthFault(subject, findings) {
|
|
81
|
+
return new DeclarationError(defaultDepth, {
|
|
82
|
+
correction: `Nest a default at most ${String(defaultLevels)} levels deep.`,
|
|
83
|
+
findings,
|
|
84
|
+
sentence: `${subject} default nests deeper than ${String(defaultLevels)} levels.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The config every `argument()`, `option()`, and `globalOption()` call, and every input a lifecycle
|
|
89
|
+
* hook declares, reads through `captureConfig`. A call in JavaScript that supplies none, or a value
|
|
90
|
+
* of another kind, reports a declaration fault and not a TypeError, and so does a config whose read
|
|
91
|
+
* throws. Each fault marks the config on the call at `place`.
|
|
92
|
+
*/
|
|
93
|
+
export function captureInputConfig(declared, place) {
|
|
94
|
+
const { config, kind, name } = declared;
|
|
95
|
+
const subject = `${kind === 'argument' ? 'Argument' : 'Option'} ${quoted(name)}`;
|
|
96
|
+
// The finding marks the config, or one key inside it, which `shown` stands for when the call prints.
|
|
97
|
+
const findings = (shown, keys = []) => [
|
|
98
|
+
siteFinding(callSite(subject, { arguments: [name, shown], ...place }), ['1', ...keys].join('.')),
|
|
99
|
+
];
|
|
100
|
+
return captureConfig(config, {
|
|
101
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
102
|
+
correction: `Supply ${kind === 'argument' ? 'an argument' : 'an option'} config object, such as ${kind === 'argument' ? '{}' : "{ type: 'string' }"}.`,
|
|
103
|
+
findings: findings(config),
|
|
104
|
+
sentence: `${subject} declares a config that is not an object.`,
|
|
105
|
+
}),
|
|
106
|
+
// The default's walk stopped at the limit, so the finding prints it elided.
|
|
107
|
+
tooDeep: () => {
|
|
108
|
+
const { keys, shown } = elidedRead('default');
|
|
109
|
+
return defaultDepthFault(subject, findings(shown, keys));
|
|
110
|
+
},
|
|
111
|
+
// A part whose read threw is never read again, so the finding prints it elided.
|
|
112
|
+
unreadable: (thrown, slot) => {
|
|
113
|
+
const { keys, shown } = elidedRead(slot);
|
|
114
|
+
return unreadableFault({ declared: 'config', findings: findings(shown, keys), subject }, thrown);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
57
117
|
}
|
|
58
118
|
/**
|
|
59
119
|
* A declaration error names the declaration, because the author reads the declaration to fix it.
|
|
60
120
|
* An argument declares and reads under one name, so the two namings differ for options alone.
|
|
61
121
|
*/
|
|
62
|
-
function
|
|
63
|
-
return input.kind === 'argument'
|
|
122
|
+
export function declarationSubject(input) {
|
|
123
|
+
return input.kind === 'argument'
|
|
124
|
+
? `Argument ${quoted(input.name)}`
|
|
125
|
+
: `Option ${quoted(input.name)}`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Where one input was declared: a global option at its `globalOption()` call, and every other input
|
|
129
|
+
* at its own `argument()` or `option()` call on the Command at `path`.
|
|
130
|
+
*/
|
|
131
|
+
export function inputPlace(input, scope) {
|
|
132
|
+
return scope.global ? { call: 'globalOption', path: [] } : { call: input.kind, path: scope.path };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
|
|
136
|
+
* finding for an input's own call starts from it, so each marks the call the same way. `subject`
|
|
137
|
+
* is the sentence's name for the input.
|
|
138
|
+
*/
|
|
139
|
+
export function declaringSite(input, place, subject = declarationSubject(input)) {
|
|
140
|
+
return callSite(subject, { arguments: [input.name, input.config], ...place });
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* One input's site with its config printed elided, for a fault judged before the config is read,
|
|
144
|
+
* such as an invalid name, so the fault reads none of the config.
|
|
145
|
+
*/
|
|
146
|
+
export function configUnread(site) {
|
|
147
|
+
const [name, config] = site.declaration.arguments;
|
|
148
|
+
const shown = config === undefined ? [name] : [name, spelled(elided)];
|
|
149
|
+
return { ...site, declaration: { ...site.declaration, arguments: shown } };
|
|
64
150
|
}
|
|
65
151
|
/**
|
|
66
152
|
* The token an operator would type for one declaration: `--file` for an option, `-F` when the
|
|
@@ -143,61 +229,88 @@ function holdsRawDefault(input) {
|
|
|
143
229
|
* rule that already decides absence rejects it, and the flag needs a validator to receive the
|
|
144
230
|
* omission.
|
|
145
231
|
*/
|
|
146
|
-
function checkOmissionValidation(input, subject) {
|
|
232
|
+
function checkOmissionValidation(input, site, subject) {
|
|
147
233
|
const { config } = input;
|
|
234
|
+
const decided = (sentence, correction) => factFault(omissionAlreadyDecided, site, { correction, fact: 'validateOmitted', sentence });
|
|
148
235
|
if (config.required) {
|
|
149
|
-
throw
|
|
236
|
+
throw decided(`${subject} is required and declares validateOmitted.`, 'Remove validateOmitted or make the input optional.');
|
|
150
237
|
}
|
|
151
238
|
if (hasDefault(input)) {
|
|
152
|
-
throw
|
|
239
|
+
throw decided(`${subject} declares a default and validateOmitted.`, 'Remove one; the default already fills an omitted value.');
|
|
153
240
|
}
|
|
154
241
|
if (collects(input)) {
|
|
155
|
-
throw
|
|
242
|
+
throw decided(`${subject} takes several values and declares validateOmitted.`, 'Remove validateOmitted; with no values the action receives an empty array and no validator runs.');
|
|
156
243
|
}
|
|
157
244
|
if (config.validate === undefined) {
|
|
158
|
-
throw
|
|
245
|
+
throw factFault(omissionWithoutValidator, site, {
|
|
246
|
+
correction: 'Add validate or remove validateOmitted.',
|
|
247
|
+
fact: 'validateOmitted',
|
|
248
|
+
sentence: `${subject} declares validateOmitted without a validator.`,
|
|
249
|
+
});
|
|
159
250
|
}
|
|
160
251
|
}
|
|
161
|
-
|
|
252
|
+
/** The value rules a Boolean option may not declare, in the order its diagnostic names them. */
|
|
253
|
+
const valueRules = ['validate', 'default', 'required', 'validateOmitted'];
|
|
254
|
+
/** Whether a `validate` value is a Standard Schema v1 object that core can call. */
|
|
255
|
+
function isStandardSchema(validator) {
|
|
256
|
+
if (validator === null || (typeof validator !== 'object' && typeof validator !== 'function')) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const props = Reflect.get(validator, '~standard');
|
|
260
|
+
return (typeof props === 'object' &&
|
|
261
|
+
props !== null &&
|
|
262
|
+
Reflect.get(props, 'version') === 1 &&
|
|
263
|
+
typeof Reflect.get(props, 'vendor') === 'string' &&
|
|
264
|
+
typeof Reflect.get(props, 'validate') === 'function');
|
|
265
|
+
}
|
|
266
|
+
function checkDeclaration(input, site, subject) {
|
|
162
267
|
const { config } = input;
|
|
163
268
|
if (input.kind === 'option' && input.config.type === 'boolean') {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
269
|
+
const declared = valueRules.find((key) => key in config);
|
|
270
|
+
if (declared !== undefined) {
|
|
271
|
+
throw factFault(booleanOptionValueRule, site, {
|
|
272
|
+
correction: `Remove ${declared}; use polarity to control its absent value.`,
|
|
273
|
+
fact: declared,
|
|
274
|
+
sentence: `${subject} is Boolean and declares ${declared}.`,
|
|
275
|
+
});
|
|
169
276
|
}
|
|
170
277
|
return;
|
|
171
278
|
}
|
|
172
279
|
if (config.required !== undefined && typeof config.required !== 'boolean') {
|
|
173
|
-
throw
|
|
280
|
+
throw flagFault(site, 'required');
|
|
174
281
|
}
|
|
175
282
|
if (input.kind === 'argument' &&
|
|
176
283
|
input.config.variadic !== undefined &&
|
|
177
284
|
typeof input.config.variadic !== 'boolean') {
|
|
178
|
-
throw
|
|
285
|
+
throw flagFault(site, 'variadic');
|
|
179
286
|
}
|
|
180
287
|
// The test reads presence, not truth, so a declared `undefined` is a declaration to reject.
|
|
181
288
|
if ('validateOmitted' in config && typeof config.validateOmitted !== 'boolean') {
|
|
182
|
-
throw
|
|
289
|
+
throw flagFault(site, 'validateOmitted');
|
|
183
290
|
}
|
|
184
291
|
if (config.required && Object.hasOwn(config, 'default')) {
|
|
185
|
-
throw
|
|
292
|
+
throw factFault(requiredWithDefault, site, {
|
|
293
|
+
correction: 'Remove the default or make the input optional.',
|
|
294
|
+
fact: 'default',
|
|
295
|
+
sentence: `${subject} is required and declares a default.`,
|
|
296
|
+
});
|
|
186
297
|
}
|
|
187
298
|
if (validatesOmission(input)) {
|
|
188
|
-
checkOmissionValidation(input, subject);
|
|
299
|
+
checkOmissionValidation(input, site, subject);
|
|
189
300
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
typeof validator['~standard'].vendor !== 'string' ||
|
|
197
|
-
typeof validator['~standard'].validate !== 'function')) {
|
|
198
|
-
throw new DeclarationError(`${subject} validate must be a Standard Schema v1 object. Supply a compatible validator.`);
|
|
301
|
+
if (config.validate !== undefined && !isStandardSchema(config.validate)) {
|
|
302
|
+
throw factFault(notAValidator, site, {
|
|
303
|
+
correction: 'Supply a compatible validator.',
|
|
304
|
+
fact: 'validate',
|
|
305
|
+
sentence: `${subject} validate must be a Standard Schema v1 object.`,
|
|
306
|
+
});
|
|
199
307
|
}
|
|
200
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* One issue a validator returned, checked where it enters. Core keeps every own enumerable field
|
|
311
|
+
* the issue carries and rewrites only `path`, reducing each segment to its key, so a field core
|
|
312
|
+
* never reads, such as a validator's issue code, reaches a failure view unchanged.
|
|
313
|
+
*/
|
|
201
314
|
function readIssue(issue) {
|
|
202
315
|
if (issue === null || typeof issue !== 'object' || !('message' in issue)) {
|
|
203
316
|
throw new Error('The validator returned an invalid Standard Schema issue.');
|
|
@@ -208,7 +321,7 @@ function readIssue(issue) {
|
|
|
208
321
|
}
|
|
209
322
|
const suppliedPath = 'path' in issue ? issue.path : undefined;
|
|
210
323
|
if (suppliedPath === undefined) {
|
|
211
|
-
return { message };
|
|
324
|
+
return { ...issue, message };
|
|
212
325
|
}
|
|
213
326
|
if (!Array.isArray(suppliedPath)) {
|
|
214
327
|
throw new Error('The validator returned an invalid Standard Schema issue path.');
|
|
@@ -220,13 +333,15 @@ function readIssue(issue) {
|
|
|
220
333
|
}
|
|
221
334
|
return key;
|
|
222
335
|
});
|
|
223
|
-
return { message, path };
|
|
336
|
+
return { ...issue, message, path };
|
|
224
337
|
}
|
|
225
338
|
/**
|
|
226
339
|
* A broken validator is a fault in the declaration, whichever value reached it, so its diagnostic
|
|
227
|
-
* names the declaration
|
|
340
|
+
* names the declaration and its finding marks the validator on the call at `place`. Returned issues
|
|
341
|
+
* belong to the value, so the caller names those.
|
|
228
342
|
*/
|
|
229
|
-
async function validate(input, raw,
|
|
343
|
+
async function validate(input, raw, call) {
|
|
344
|
+
const { context, place } = call;
|
|
230
345
|
const validator = input.config.validate;
|
|
231
346
|
if (validator === undefined) {
|
|
232
347
|
return { value: raw };
|
|
@@ -246,8 +361,13 @@ async function validate(input, raw, context) {
|
|
|
246
361
|
return { issues: Array.from(issues, readIssue) };
|
|
247
362
|
}
|
|
248
363
|
catch (error) {
|
|
249
|
-
|
|
250
|
-
|
|
364
|
+
// The reason is the author's detail: a distributed build shows the generic defect message.
|
|
365
|
+
const site = place === undefined ? undefined : declaringSite(input, place);
|
|
366
|
+
throw new DeclarationError(validatorFailed, {
|
|
367
|
+
correction: 'Fix the validator.',
|
|
368
|
+
findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
|
|
369
|
+
sentence: `${declarationSubject(input)} validator failed unexpectedly: ${asSentence(reasonOf(error))}`,
|
|
370
|
+
}, { cause: error });
|
|
251
371
|
}
|
|
252
372
|
}
|
|
253
373
|
/**
|
|
@@ -258,13 +378,13 @@ async function validate(input, raw, context) {
|
|
|
258
378
|
* next call. The host is the one captured object that every call and the action share.
|
|
259
379
|
*/
|
|
260
380
|
async function validateDeclared(input, raw, call) {
|
|
261
|
-
const { context, signal } = call;
|
|
381
|
+
const { context, place, signal } = call;
|
|
262
382
|
if (!collects(input) || input.config.validate === undefined) {
|
|
263
|
-
return validate(input, raw, context());
|
|
383
|
+
return validate(input, raw, { context: context(), place });
|
|
264
384
|
}
|
|
265
385
|
if (!Array.isArray(raw)) {
|
|
266
386
|
// The parser, the input sources, and the declaration rules only ever supply an array here.
|
|
267
|
-
throw new TypeError(`${
|
|
387
|
+
throw new TypeError(`${declarationSubject(input)} reached validation without an array of values.`);
|
|
268
388
|
}
|
|
269
389
|
const outputs = [];
|
|
270
390
|
const issues = [];
|
|
@@ -273,15 +393,15 @@ async function validateDeclared(input, raw, call) {
|
|
|
273
393
|
// A cancelled run starts no further call; the run resolves its cancellation code instead.
|
|
274
394
|
break;
|
|
275
395
|
}
|
|
276
|
-
const result = await validate(input, value, context());
|
|
396
|
+
const result = await validate(input, value, { context: context(), place });
|
|
277
397
|
if (result.issues === undefined) {
|
|
278
398
|
outputs.push(result.value);
|
|
279
399
|
}
|
|
280
400
|
else {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
path: [position, ...(issue.path ?? [])]
|
|
284
|
-
}
|
|
401
|
+
// The issue keeps its own fields, and only its path gains the value's position.
|
|
402
|
+
for (const issue of reported(result.issues)) {
|
|
403
|
+
issues.push({ ...issue, path: [position, ...(issue.path ?? [])] });
|
|
404
|
+
}
|
|
285
405
|
}
|
|
286
406
|
}
|
|
287
407
|
return issues.length === 0 ? { value: outputs } : { issues };
|
|
@@ -304,23 +424,33 @@ export function issuePath(issue) {
|
|
|
304
424
|
*/
|
|
305
425
|
function reported(issues) {
|
|
306
426
|
return issues.length === 0
|
|
307
|
-
? [
|
|
427
|
+
? [
|
|
428
|
+
{
|
|
429
|
+
message: 'The validator rejected this value without an explanation. Supply a different value.',
|
|
430
|
+
},
|
|
431
|
+
]
|
|
308
432
|
: issues;
|
|
309
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* One line per issue under the input's subject. A path segment can hold text the operator typed,
|
|
436
|
+
* such as a key of a record, so the line escapes the path; `issuePath` keeps it raw for a view.
|
|
437
|
+
*/
|
|
310
438
|
function messages(subject, issues) {
|
|
311
439
|
return issues.map((issue) => {
|
|
312
440
|
const path = issuePath(issue);
|
|
313
|
-
|
|
441
|
+
const position = path === undefined ? '' : ` at ${escapeControlCharacters(path)}`;
|
|
442
|
+
return `${subject}${position}: ${issue.message}`;
|
|
314
443
|
});
|
|
315
444
|
}
|
|
316
445
|
/** The fault a default of the wrong raw shape reports, by the shape its declaration expects. */
|
|
317
|
-
function defaultShapeFault(input, subject) {
|
|
446
|
+
function defaultShapeFault(input, site, subject) {
|
|
447
|
+
const fault = (sentence, correction) => factFault(defaultShape, site, { correction, fact: 'default', sentence });
|
|
318
448
|
if (!collects(input)) {
|
|
319
|
-
return `${subject} default must be a string without a validator
|
|
449
|
+
return fault(`${subject} default must be a string without a validator.`, 'Supply a string default.');
|
|
320
450
|
}
|
|
321
451
|
return input.config.validate === undefined
|
|
322
|
-
? `${subject} default must be an array of strings without a validator
|
|
323
|
-
: `${subject} default must be an array
|
|
452
|
+
? fault(`${subject} default must be an array of strings without a validator.`, 'Supply a string array default.')
|
|
453
|
+
: fault(`${subject} default must be an array.`, 'Supply an array of values.');
|
|
324
454
|
}
|
|
325
455
|
/** A declared `default: undefined` is a default, so presence is the key, never the value. */
|
|
326
456
|
function hasDefault(input) {
|
|
@@ -334,44 +464,57 @@ function hasDefault(input) {
|
|
|
334
464
|
* diagnostics read with; every other caller is named by the declaration itself.
|
|
335
465
|
*/
|
|
336
466
|
export function checkDeclarations(inputs, named) {
|
|
337
|
-
for (const input of inputs) {
|
|
338
|
-
checkDeclaration(input, named ??
|
|
467
|
+
for (const { input, site } of inputs) {
|
|
468
|
+
checkDeclaration(input, site, named ?? declarationSubject(input));
|
|
339
469
|
}
|
|
340
|
-
for (const input of inputs.filter((entry) => hasDefault(entry))) {
|
|
470
|
+
for (const { input, site } of inputs.filter((entry) => hasDefault(entry.input))) {
|
|
341
471
|
if (!holdsRawDefault(input)) {
|
|
342
|
-
|
|
343
|
-
throw new DeclarationError(defaultShapeFault(input, subject));
|
|
472
|
+
throw defaultShapeFault(input, site, named ?? declarationSubject(input));
|
|
344
473
|
}
|
|
345
474
|
}
|
|
346
475
|
}
|
|
347
476
|
/**
|
|
348
477
|
* Every declared default, validated before any token is read. The host is captured by then, so a
|
|
349
|
-
* default's validator reads the same Host its action will, under the `default` phase.
|
|
478
|
+
* default's validator reads the same Host its action will, under the `default` phase. `places`
|
|
479
|
+
* says where each declaration sits, which a rejected default's finding rebuilds.
|
|
350
480
|
*/
|
|
351
|
-
export async function prepareInputs(inputs, host) {
|
|
481
|
+
export async function prepareInputs(inputs, host, places) {
|
|
352
482
|
const declarations = scoped(inputs);
|
|
353
483
|
const defaults = new Map();
|
|
354
484
|
for (const entry of declarations.filter(({ input }) => hasDefault(input))) {
|
|
355
485
|
const { input } = entry;
|
|
356
|
-
const subject =
|
|
486
|
+
const subject = declarationSubject(input);
|
|
487
|
+
const place = places.get(input);
|
|
357
488
|
const result = await validateDeclared(input, input.config.default, {
|
|
358
489
|
context: () => ({ host, input: identityOf(entry), phase: 'default' }),
|
|
490
|
+
place,
|
|
359
491
|
});
|
|
360
492
|
if (result.issues !== undefined) {
|
|
361
|
-
|
|
493
|
+
const site = place === undefined ? undefined : declaringSite(input, place);
|
|
494
|
+
throw new DeclarationError(invalidDefault, {
|
|
495
|
+
correction: 'Fix the default or its validator.',
|
|
496
|
+
findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'default'))],
|
|
497
|
+
sentence: [
|
|
498
|
+
`${subject} has an invalid default.`,
|
|
499
|
+
...messages(subject, reported(result.issues)),
|
|
500
|
+
].join('\n'),
|
|
501
|
+
});
|
|
362
502
|
}
|
|
363
503
|
defaults.set(input, result.value);
|
|
364
504
|
}
|
|
365
505
|
return defaults;
|
|
366
506
|
}
|
|
367
507
|
/**
|
|
368
|
-
* An array default reaches the action as its own copy, so an action that mutates its array
|
|
508
|
+
* An array default reaches the action as its own mutable copy, so an action that mutates its array
|
|
369
509
|
* rewrites neither the declaration nor the next invocation. One prepared default serves every
|
|
370
|
-
* invocation of a run, and an unvalidated default is the
|
|
371
|
-
* copies it.
|
|
510
|
+
* invocation of a run, and an unvalidated default is the frozen snapshot the declaring call took,
|
|
511
|
+
* so each read copies it. The copy keeps a hole where the default has one, as the graph's does.
|
|
512
|
+
* Every other output passes through unchanged.
|
|
372
513
|
*/
|
|
373
514
|
function freshDefault(value) {
|
|
374
|
-
|
|
515
|
+
// A spread reads a hole as `undefined`, and `slice` keeps it.
|
|
516
|
+
// oxlint-disable-next-line unicorn/prefer-spread
|
|
517
|
+
return Array.isArray(value) ? value.slice() : value;
|
|
375
518
|
}
|
|
376
519
|
/**
|
|
377
520
|
* The raw tokens of one invocation, keyed by declared name. Every declared input of the routed
|
|
@@ -461,6 +604,7 @@ export async function validateValues(invocation) {
|
|
|
461
604
|
const accept = async (entry, raw, spelling) => {
|
|
462
605
|
const result = await validateDeclared(entry.input, raw, {
|
|
463
606
|
context: () => contextOf(entry),
|
|
607
|
+
place: inputPlace(entry.input, { global: entry.global, path: invocation.command }),
|
|
464
608
|
signal: invocation.signal,
|
|
465
609
|
});
|
|
466
610
|
if (result.issues === undefined) {
|