@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/options.js
CHANGED
|
@@ -1,52 +1,129 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { declaredName } from './command-rules.js';
|
|
2
|
+
import { valueCode } from './diagnostic-text.js';
|
|
3
|
+
import { DeclarationError, MissingValueError, quoted, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
|
|
4
|
+
import { factFault, flagFault, siteFinding } from './facts.js';
|
|
5
|
+
import { booleanOptionMultiple, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, } from './input-rules.js';
|
|
2
6
|
/** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
|
|
3
7
|
export function emptyValues() {
|
|
4
8
|
return { booleans: new Map(), lists: new Map(), spellings: new Map(), strings: new Map() };
|
|
5
9
|
}
|
|
6
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The part of one declaration that yields a spelling of the given role, which a spelling fault
|
|
12
|
+
* marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
|
|
13
|
+
* generates a negative form.
|
|
14
|
+
*/
|
|
15
|
+
export function spellingMark(site, role) {
|
|
16
|
+
if (role === 'long') {
|
|
17
|
+
return site.named;
|
|
18
|
+
}
|
|
19
|
+
return `${site.at}.${role === 'short' ? 'short' : 'polarity'}`;
|
|
20
|
+
}
|
|
21
|
+
/** The declared name answers the declared-name rule an argument's name answers. */
|
|
22
|
+
export function checkOptionName(name, site) {
|
|
23
|
+
const findings = [siteFinding(site, site.named)];
|
|
7
24
|
if (typeof name !== 'string') {
|
|
8
|
-
throw new DeclarationError(
|
|
25
|
+
throw new DeclarationError(declaredName, {
|
|
26
|
+
correction: 'Supply a string name.',
|
|
27
|
+
findings,
|
|
28
|
+
sentence: `Option name ${valueCode(name)} is not a string.`,
|
|
29
|
+
});
|
|
9
30
|
}
|
|
10
31
|
if (!name || name.startsWith('-') || /[\s=]/u.test(name)) {
|
|
11
|
-
throw new DeclarationError(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
throw new DeclarationError(declaredName, {
|
|
33
|
+
correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
|
|
34
|
+
findings,
|
|
35
|
+
sentence: `Option name ${quoted(name)} is invalid.`,
|
|
36
|
+
});
|
|
15
37
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
}
|
|
39
|
+
/** Whether a declared short alias is one ASCII letter, the rule every short spelling answers. */
|
|
40
|
+
export function isShortAlias(short) {
|
|
41
|
+
return typeof short === 'string' && /^[A-Za-z]$/u.test(short);
|
|
42
|
+
}
|
|
43
|
+
/** The sentence and correction of the short-alias fault for the option `subject` names. */
|
|
44
|
+
export function shortAliasText(subject) {
|
|
45
|
+
return {
|
|
46
|
+
correction: 'Supply one ASCII letter.',
|
|
47
|
+
sentence: `${subject} declares a short alias that is not one ASCII letter.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The short alias, and `shortOnly`, which leaves the option that alias alone. */
|
|
51
|
+
function checkShortForms(config, site, subject) {
|
|
52
|
+
if (config.short !== undefined && !isShortAlias(config.short)) {
|
|
53
|
+
throw factFault(shortAlias, site, { ...shortAliasText(subject), fact: 'short' });
|
|
19
54
|
}
|
|
20
55
|
if (config.shortOnly !== undefined && typeof config.shortOnly !== 'boolean') {
|
|
21
|
-
throw
|
|
56
|
+
throw flagFault(site, 'shortOnly');
|
|
22
57
|
}
|
|
23
58
|
if (config.shortOnly && config.short === undefined) {
|
|
24
|
-
throw
|
|
59
|
+
throw factFault(shortOnlyWithoutShort, site, {
|
|
60
|
+
correction: 'Add short or remove shortOnly.',
|
|
61
|
+
fact: 'shortOnly',
|
|
62
|
+
sentence: `${subject} declares shortOnly and no short alias.`,
|
|
63
|
+
});
|
|
25
64
|
}
|
|
65
|
+
}
|
|
66
|
+
/** A Boolean option's polarity, which a string option does not declare. */
|
|
67
|
+
function checkPolarity(config, site, subject) {
|
|
68
|
+
if (config.polarity === undefined) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (config.type !== 'boolean') {
|
|
72
|
+
throw factFault(polarityOnString, site, {
|
|
73
|
+
correction: 'Remove polarity or use type "boolean".',
|
|
74
|
+
fact: 'polarity',
|
|
75
|
+
sentence: `${subject} declares polarity but is not Boolean.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!['positive', 'both', 'negative'].includes(config.polarity)) {
|
|
79
|
+
throw factFault(optionPolarity, site, {
|
|
80
|
+
correction: 'Use "positive", "both", or "negative".',
|
|
81
|
+
fact: 'polarity',
|
|
82
|
+
sentence: `${subject} has an invalid polarity.`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (config.polarity === 'both' && config.shortOnly) {
|
|
86
|
+
throw factFault(shortOnlyBothPolarities, site, {
|
|
87
|
+
correction: 'Enable long forms or select one polarity.',
|
|
88
|
+
fact: 'shortOnly',
|
|
89
|
+
sentence: `${subject} cannot express both polarities with shortOnly.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Every rule one option declaration answers alone, before the table meets it. */
|
|
94
|
+
function validateDeclaration({ name, config }, site) {
|
|
95
|
+
checkOptionName(name, site);
|
|
96
|
+
const subject = `Option ${quoted(name)}`;
|
|
97
|
+
if (!['string', 'boolean'].includes(config.type)) {
|
|
98
|
+
throw factFault(optionType, site, {
|
|
99
|
+
correction: 'Use "string" or "boolean".',
|
|
100
|
+
fact: 'type',
|
|
101
|
+
sentence: `${subject} has an invalid type.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
checkShortForms(config, site, subject);
|
|
26
105
|
if (config.type === 'boolean' && config.multiple !== undefined) {
|
|
27
|
-
throw
|
|
106
|
+
throw factFault(booleanOptionMultiple, site, {
|
|
107
|
+
correction: 'Remove multiple or declare a string option.',
|
|
108
|
+
fact: 'multiple',
|
|
109
|
+
sentence: `${subject} is a boolean option and declares multiple.`,
|
|
110
|
+
});
|
|
28
111
|
}
|
|
29
112
|
if (config.multiple !== undefined && typeof config.multiple !== 'boolean') {
|
|
30
|
-
throw
|
|
31
|
-
}
|
|
32
|
-
if (config.polarity !== undefined) {
|
|
33
|
-
if (config.type !== 'boolean') {
|
|
34
|
-
throw new DeclarationError(`Option "${name}" declares polarity but is not Boolean. Remove polarity or use type "boolean".`);
|
|
35
|
-
}
|
|
36
|
-
if (!['positive', 'both', 'negative'].includes(config.polarity)) {
|
|
37
|
-
throw new DeclarationError(`Option "${name}" has an invalid polarity. Use "positive", "both", or "negative".`);
|
|
38
|
-
}
|
|
39
|
-
if (config.polarity === 'both' && config.shortOnly) {
|
|
40
|
-
throw new DeclarationError(`Option "${name}" cannot express both polarities with shortOnly. Enable long forms or select one polarity.`);
|
|
41
|
-
}
|
|
113
|
+
throw flagFault(site, 'multiple');
|
|
42
114
|
}
|
|
115
|
+
checkPolarity(config, site, subject);
|
|
43
116
|
}
|
|
44
|
-
function addSpelling(
|
|
45
|
-
const existing =
|
|
117
|
+
function addSpelling(claims, spelling, claim) {
|
|
118
|
+
const existing = claims.get(spelling);
|
|
46
119
|
if (existing) {
|
|
47
|
-
throw new DeclarationError(
|
|
120
|
+
throw new DeclarationError(spellingTaken, {
|
|
121
|
+
correction: 'Change one declaration.',
|
|
122
|
+
findings: [existing, claim].map(({ option, site }) => siteFinding(site, spellingMark(site, option.role))),
|
|
123
|
+
sentence: `Option spelling ${quoted(spelling)} is used by both ${quoted(existing.option.name)} and ${quoted(claim.option.name)}.`,
|
|
124
|
+
});
|
|
48
125
|
}
|
|
49
|
-
|
|
126
|
+
claims.set(spelling, claim);
|
|
50
127
|
}
|
|
51
128
|
/**
|
|
52
129
|
* One Boolean option's value for one invocation: the value the parser consumed, or the value its
|
|
@@ -57,37 +134,50 @@ function addSpelling(spellings, spelling, option) {
|
|
|
57
134
|
export function booleanValue(values, name, config) {
|
|
58
135
|
return values.booleans.get(name) ?? config.polarity === 'negative';
|
|
59
136
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
137
|
+
/**
|
|
138
|
+
* One scope's options compiled into the spelling table the parser reads, with every rule one
|
|
139
|
+
* declaration answers alone and every rule two of them answer together.
|
|
140
|
+
*/
|
|
141
|
+
export function compileOptions(declarations, scope) {
|
|
142
|
+
const claims = new Map();
|
|
143
|
+
const names = new Map();
|
|
144
|
+
for (const declaration of declarations) {
|
|
145
|
+
const { name, config } = declaration;
|
|
146
|
+
const site = scope.siteOf(declaration);
|
|
147
|
+
validateDeclaration({ config, name }, site);
|
|
148
|
+
const first = names.get(name);
|
|
149
|
+
if (first) {
|
|
150
|
+
const earlier = scope.siteOf(first);
|
|
151
|
+
throw new DeclarationError(optionDeclaredTwice, {
|
|
152
|
+
correction: 'Remove or rename the duplicate.',
|
|
153
|
+
findings: [
|
|
154
|
+
siteFinding(earlier, earlier.named, 'the first declaration'),
|
|
155
|
+
siteFinding(site, site.named, 'the second declaration'),
|
|
156
|
+
],
|
|
157
|
+
sentence: `Option ${quoted(name)} is declared more than once on ${scope.subject}.`,
|
|
158
|
+
});
|
|
67
159
|
}
|
|
68
|
-
names.
|
|
160
|
+
names.set(name, declaration);
|
|
69
161
|
const positive = config.type === 'string'
|
|
70
162
|
? { multiple: config.multiple === true, name, type: 'string' }
|
|
71
163
|
: { name, type: 'boolean', value: config.polarity !== 'negative' };
|
|
72
164
|
if (!config.shortOnly) {
|
|
73
165
|
if (config.type === 'string' || config.polarity !== 'negative') {
|
|
74
|
-
addSpelling(
|
|
166
|
+
addSpelling(claims, `--${name}`, { option: { ...positive, role: 'long' }, site });
|
|
75
167
|
}
|
|
76
168
|
if (config.type === 'boolean' &&
|
|
77
169
|
(config.polarity === 'both' || config.polarity === 'negative')) {
|
|
78
|
-
addSpelling(
|
|
79
|
-
name,
|
|
80
|
-
|
|
81
|
-
type: 'boolean',
|
|
82
|
-
value: false,
|
|
170
|
+
addSpelling(claims, `--no-${name}`, {
|
|
171
|
+
option: { name, role: 'negative', type: 'boolean', value: false },
|
|
172
|
+
site,
|
|
83
173
|
});
|
|
84
174
|
}
|
|
85
175
|
}
|
|
86
176
|
if (config.short !== undefined) {
|
|
87
|
-
addSpelling(
|
|
177
|
+
addSpelling(claims, `-${config.short}`, { option: { ...positive, role: 'short' }, site });
|
|
88
178
|
}
|
|
89
179
|
}
|
|
90
|
-
return
|
|
180
|
+
return new Map([...claims].map(([spelling, { option }]) => [spelling, option]));
|
|
91
181
|
}
|
|
92
182
|
/**
|
|
93
183
|
* Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
|
|
@@ -199,6 +289,9 @@ function readOption(spellings, input, state) {
|
|
|
199
289
|
/**
|
|
200
290
|
* A hyphen token belongs to the globals when its long spelling or every short letter does. The
|
|
201
291
|
* pre-scan reads the globals alone, so a letter it does not own is only "not a global option".
|
|
292
|
+
* The scan stops at a global value option, because the letters after it may be the value the
|
|
293
|
+
* operator meant to pass: the token then belongs to the globals, and parsing reports the
|
|
294
|
+
* value-position fault that names that option alone.
|
|
202
295
|
*/
|
|
203
296
|
function isGlobalToken(spellings, token) {
|
|
204
297
|
const long = longToken(token);
|
|
@@ -210,8 +303,12 @@ function isGlobalToken(spellings, token) {
|
|
|
210
303
|
let other = '';
|
|
211
304
|
for (let index = 0; index < group.length; index += 1) {
|
|
212
305
|
const letter = group.charAt(index);
|
|
213
|
-
|
|
306
|
+
const option = spellings.get(`-${letter}`);
|
|
307
|
+
if (option) {
|
|
214
308
|
global = global === '' ? letter : global;
|
|
309
|
+
if (option.type === 'string') {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
215
312
|
}
|
|
216
313
|
else {
|
|
217
314
|
other = other === '' ? letter : other;
|
package/dist/output.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { RenderingPolicy } from './rendering.js';
|
|
|
4
4
|
import type { Palette } from './style-state.js';
|
|
5
5
|
import type { ActionChannel, Host, OpenResult, Out, ResultBinding, ViewContext } from './types.js';
|
|
6
6
|
import type { ViewRegistry } from './view.js';
|
|
7
|
-
type WriteState = {
|
|
7
|
+
export type WriteState = {
|
|
8
8
|
kind: 'ok';
|
|
9
9
|
} | {
|
|
10
10
|
kind: 'failed';
|
|
@@ -19,6 +19,7 @@ export declare class Output {
|
|
|
19
19
|
private readonly signal;
|
|
20
20
|
private readonly destinations;
|
|
21
21
|
private renderFault;
|
|
22
|
+
private readonly viewFaults;
|
|
22
23
|
private readonly stops;
|
|
23
24
|
private route;
|
|
24
25
|
/**
|
|
@@ -56,7 +57,7 @@ export declare class Output {
|
|
|
56
57
|
private resultFault;
|
|
57
58
|
/** The registry one invocation resolves through, republished as each contributor is read. */
|
|
58
59
|
useViews(registry: ViewRegistry): void;
|
|
59
|
-
/** The
|
|
60
|
+
/** The path routing walked, updated as each name routes, which an incomplete sequence names. */
|
|
60
61
|
useRoute(path: readonly string[]): void;
|
|
61
62
|
/** What this invocation's output raised beside its calls, in the order it was raised. */
|
|
62
63
|
get stopped(): readonly unknown[];
|
|
@@ -112,6 +113,12 @@ export declare class Output {
|
|
|
112
113
|
* awaits the call must not end the process with an unhandled rejection.
|
|
113
114
|
*/
|
|
114
115
|
private renderFailed;
|
|
116
|
+
/**
|
|
117
|
+
* Whether one value is what a view or a message check failed with during this invocation. A
|
|
118
|
+
* broken view is a defect in the code that broke the view contract, so an action or a source that
|
|
119
|
+
* lets its rejection propagate reports it as that defect.
|
|
120
|
+
*/
|
|
121
|
+
raisedByView(value: unknown): boolean;
|
|
115
122
|
/** What a view failed with during this invocation, if one did. */
|
|
116
123
|
get fault(): {
|
|
117
124
|
cause: unknown;
|
package/dist/output.js
CHANGED
|
@@ -145,10 +145,13 @@ export class Output {
|
|
|
145
145
|
signal;
|
|
146
146
|
destinations = new Map();
|
|
147
147
|
renderFault = undefined;
|
|
148
|
+
// Every value a view failed with.
|
|
149
|
+
// A caller that lets one propagate never offers it to a translator.
|
|
150
|
+
viewFaults = new WeakSet();
|
|
148
151
|
// Every fault the output path raised beside its calls, reported after the primary outcome.
|
|
149
152
|
// A source a sequence stopped on and a results-lane fault are both such faults.
|
|
150
153
|
stops = [];
|
|
151
|
-
// The
|
|
154
|
+
// The path routing walked, which an incomplete sequence names, updated as each name routes.
|
|
152
155
|
route = [];
|
|
153
156
|
/**
|
|
154
157
|
* The channel every caller writes through. It is typed with the result left open, because the
|
|
@@ -263,7 +266,7 @@ export class Output {
|
|
|
263
266
|
useViews(registry) {
|
|
264
267
|
this.registry = registry;
|
|
265
268
|
}
|
|
266
|
-
/** The
|
|
269
|
+
/** The path routing walked, updated as each name routes, which an incomplete sequence names. */
|
|
267
270
|
useRoute(path) {
|
|
268
271
|
this.route = path;
|
|
269
272
|
}
|
|
@@ -395,10 +398,23 @@ export class Output {
|
|
|
395
398
|
*/
|
|
396
399
|
renderFailed(cause) {
|
|
397
400
|
this.renderFault ??= { cause };
|
|
401
|
+
if ((typeof cause === 'object' || typeof cause === 'function') && cause !== null) {
|
|
402
|
+
this.viewFaults.add(cause);
|
|
403
|
+
}
|
|
398
404
|
const rejection = Promise.reject(cause);
|
|
399
405
|
void rejection.catch(() => undefined);
|
|
400
406
|
return rejection;
|
|
401
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Whether one value is what a view or a message check failed with during this invocation. A
|
|
410
|
+
* broken view is a defect in the code that broke the view contract, so an action or a source that
|
|
411
|
+
* lets its rejection propagate reports it as that defect.
|
|
412
|
+
*/
|
|
413
|
+
raisedByView(value) {
|
|
414
|
+
return ((typeof value === 'object' || typeof value === 'function') &&
|
|
415
|
+
value !== null &&
|
|
416
|
+
this.viewFaults.has(value));
|
|
417
|
+
}
|
|
402
418
|
/** What a view failed with during this invocation, if one did. */
|
|
403
419
|
get fault() {
|
|
404
420
|
return this.renderFault;
|
package/dist/plain.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs one declaring call with verdicts of its own, dropped when it returns or throws. A call made
|
|
3
|
+
* inside another, such as a `plugin()` a getter makes, is part of the outer read and shares them.
|
|
4
|
+
*/
|
|
5
|
+
declare function declaring<Result>(call: () => Result): Result;
|
|
6
|
+
/**
|
|
7
|
+
* A structural value core reads as plain data: an object literal, and never a declaration that
|
|
8
|
+
* carries state of its own. The options slots read it to reject a value that is not an options
|
|
9
|
+
* object, and `snapshot` reads it to copy a declared value faithfully. A value a declaring call
|
|
10
|
+
* already judged answers with that verdict.
|
|
11
|
+
*/
|
|
12
|
+
declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a declaring call reads one part of a declaration as plain data, decided once: the first
|
|
15
|
+
* verdict is recorded, and `isPlainObject` answers with it for the same value from then on.
|
|
16
|
+
*/
|
|
17
|
+
declare function decidePlain(value: unknown): value is Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
|
|
20
|
+
* consumer cannot reach the source through the copy. A value that holds itself is copied with the
|
|
21
|
+
* same cycle, because each source object is judged and copied once and every path to it reaches
|
|
22
|
+
* that copy. Primitives and library objects, such as a class instance or a `Date` a schema
|
|
23
|
+
* produced, are reported as they are, because core cannot copy them meaningfully. Build reads it
|
|
24
|
+
* for a converter's schema, and the chain for the request one middleware holds.
|
|
25
|
+
*/
|
|
26
|
+
declare function snapshot(value: unknown): unknown;
|
|
27
|
+
/** The snapshot of one plain object, under the record type the caller already established. */
|
|
28
|
+
declare function snapshotRecord(value: Record<string, unknown>): Readonly<Record<string, unknown>>;
|
|
29
|
+
/**
|
|
30
|
+
* The snapshot `snapshot` takes, of a value whose paths may hold at most `levels` arrays and plain
|
|
31
|
+
* objects, the value itself included. Every path through a container the value holds twice counts
|
|
32
|
+
* it, and a value that holds itself has a path without end. Either kind of path past `levels`
|
|
33
|
+
* throws `NestedTooDeepError` before the walk goes deeper than `levels`, so neither the walk nor
|
|
34
|
+
* any later reader of the copy goes deeper either. A declaring call reads it for a declared default.
|
|
35
|
+
*/
|
|
36
|
+
declare function boundedSnapshot(value: unknown, levels: number): unknown;
|
|
37
|
+
/** What `boundedSnapshot` throws for a value with a path longer than its limit. */
|
|
38
|
+
declare class NestedTooDeepError extends Error {
|
|
39
|
+
name: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A copy of every own string key of one object, enumerable or not, each described once and read
|
|
43
|
+
* once, in the order the object lists its keys. `entering` hears each key before it is read, so a
|
|
44
|
+
* read that throws can name it. A symbol key is not copied, because no declaration declares one.
|
|
45
|
+
*/
|
|
46
|
+
declare function copyOwnKeys(declared: object, entering?: (key: string) => void): Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* The copy of one part of a declaration that `decidePlain` judges plain, by `copyOwnKeys`. Any other
|
|
49
|
+
* value is answered as it is, so the rule for its slot reports it.
|
|
50
|
+
*/
|
|
51
|
+
declare function shallowRecord(value: unknown): unknown;
|
|
52
|
+
/** A copy of one list's entries by `fillList`. Each entry is what its factory built, so it is kept. */
|
|
53
|
+
declare function copyList<Entry>(list: readonly Entry[]): Entry[];
|
|
54
|
+
/** The copy of a value that is a list, by `copyList`. Any other value is answered as it is. */
|
|
55
|
+
declare function shallowList(value: unknown): unknown;
|
|
56
|
+
export { boundedSnapshot, copyList, copyOwnKeys, decidePlain, declaring, isPlainObject, NestedTooDeepError, shallowList, shallowRecord, snapshot, snapshotRecord, };
|
package/dist/plain.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdict the declaring call under way reached on each part of a declaration it captured, so
|
|
3
|
+
* every later rule of that call reads that one verdict and never asks the part's prototype again.
|
|
4
|
+
* No call is under way outside `declaring`, so a run and every later call judge afresh.
|
|
5
|
+
*/
|
|
6
|
+
let verdicts = undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Runs one declaring call with verdicts of its own, dropped when it returns or throws. A call made
|
|
9
|
+
* inside another, such as a `plugin()` a getter makes, is part of the outer read and shares them.
|
|
10
|
+
*/
|
|
11
|
+
function declaring(call) {
|
|
12
|
+
if (verdicts !== undefined) {
|
|
13
|
+
return call();
|
|
14
|
+
}
|
|
15
|
+
verdicts = new WeakMap();
|
|
16
|
+
try {
|
|
17
|
+
return call();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
verdicts = undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A structural value core reads as plain data: an object literal, and never a declaration that
|
|
25
|
+
* carries state of its own. The options slots read it to reject a value that is not an options
|
|
26
|
+
* object, and `snapshot` reads it to copy a declared value faithfully. A value a declaring call
|
|
27
|
+
* already judged answers with that verdict.
|
|
28
|
+
*/
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
if (value === null || typeof value !== 'object') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return verdicts?.get(value) ?? hasPlainPrototype(value);
|
|
34
|
+
}
|
|
35
|
+
/** Whether one object's prototype, read once, is `Object.prototype` or `null`. */
|
|
36
|
+
function hasPlainPrototype(value) {
|
|
37
|
+
const prototype = Object.getPrototypeOf(value);
|
|
38
|
+
return prototype === Object.prototype || prototype === null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether a declaring call reads one part of a declaration as plain data, decided once: the first
|
|
42
|
+
* verdict is recorded, and `isPlainObject` answers with it for the same value from then on.
|
|
43
|
+
*/
|
|
44
|
+
function decidePlain(value) {
|
|
45
|
+
if (value === null || typeof value !== 'object') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const known = verdicts?.get(value);
|
|
49
|
+
if (known !== undefined) {
|
|
50
|
+
return known;
|
|
51
|
+
}
|
|
52
|
+
const verdict = hasPlainPrototype(value);
|
|
53
|
+
verdicts?.set(value, verdict);
|
|
54
|
+
return verdict;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
|
|
58
|
+
* consumer cannot reach the source through the copy. A value that holds itself is copied with the
|
|
59
|
+
* same cycle, because each source object is judged and copied once and every path to it reaches
|
|
60
|
+
* that copy. Primitives and library objects, such as a class instance or a `Date` a schema
|
|
61
|
+
* produced, are reported as they are, because core cannot copy them meaningfully. Build reads it
|
|
62
|
+
* for a converter's schema, and the chain for the request one middleware holds.
|
|
63
|
+
*/
|
|
64
|
+
function snapshot(value) {
|
|
65
|
+
return copied(value, copying(Number.POSITIVE_INFINITY), 1);
|
|
66
|
+
}
|
|
67
|
+
/** The snapshot of one plain object, under the record type the caller already established. */
|
|
68
|
+
function snapshotRecord(value) {
|
|
69
|
+
return copiedRecord(value, copying(Number.POSITIVE_INFINITY), 1);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The snapshot `snapshot` takes, of a value whose paths may hold at most `levels` arrays and plain
|
|
73
|
+
* objects, the value itself included. Every path through a container the value holds twice counts
|
|
74
|
+
* it, and a value that holds itself has a path without end. Either kind of path past `levels`
|
|
75
|
+
* throws `NestedTooDeepError` before the walk goes deeper than `levels`, so neither the walk nor
|
|
76
|
+
* any later reader of the copy goes deeper either. A declaring call reads it for a declared default.
|
|
77
|
+
*/
|
|
78
|
+
function boundedSnapshot(value, levels) {
|
|
79
|
+
return copied(value, copying(levels), 1);
|
|
80
|
+
}
|
|
81
|
+
/** What `boundedSnapshot` throws for a value with a path longer than its limit. */
|
|
82
|
+
class NestedTooDeepError extends Error {
|
|
83
|
+
name = 'NestedTooDeepError';
|
|
84
|
+
}
|
|
85
|
+
/** A snapshot about to start, whose paths may hold at most `levels` containers. */
|
|
86
|
+
function copying(levels) {
|
|
87
|
+
return { copies: new Map(), heights: new Map(), levels };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One value of a snapshot in progress, reached at `level`, where the value itself is level 1. An
|
|
91
|
+
* object the walk reached already answers as it did then, so its prototype is read once.
|
|
92
|
+
*/
|
|
93
|
+
function copied(value, walk, level) {
|
|
94
|
+
if (typeof value !== 'object' || value === null) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
if (walk.copies.has(value)) {
|
|
98
|
+
return walk.copies.get(value);
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return copiedList(value, walk, level);
|
|
102
|
+
}
|
|
103
|
+
if (isPlainObject(value)) {
|
|
104
|
+
return copiedRecord(value, walk, level);
|
|
105
|
+
}
|
|
106
|
+
return kept(value, walk);
|
|
107
|
+
}
|
|
108
|
+
/** An object core cannot copy, kept as it is, which every later reach answers with unread. */
|
|
109
|
+
function kept(value, walk) {
|
|
110
|
+
walk.copies.set(value, value);
|
|
111
|
+
walk.heights.set(value, 0);
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* How many containers the longest path from one entry holds, once the walk has copied the entry of
|
|
116
|
+
* a container reached at `level`. A container the walk filled counts every container below it,
|
|
117
|
+
* even one it reached first by a shorter path. A container still being filled is one this entry
|
|
118
|
+
* leads back into, so the path through it has no end, and any other value holds none.
|
|
119
|
+
*/
|
|
120
|
+
function checkedHeight(entry, walk, level) {
|
|
121
|
+
const height = typeof entry === 'object' && entry !== null
|
|
122
|
+
? (walk.heights.get(entry) ?? Number.POSITIVE_INFINITY)
|
|
123
|
+
: 0;
|
|
124
|
+
if (level + height > walk.levels) {
|
|
125
|
+
throw new NestedTooDeepError();
|
|
126
|
+
}
|
|
127
|
+
return height;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The copier of each entry of one container reached at `level`, which counts how many containers
|
|
131
|
+
* the longest path from that container holds, itself included.
|
|
132
|
+
*/
|
|
133
|
+
function entryCopier(walk, level) {
|
|
134
|
+
let height = 1;
|
|
135
|
+
return {
|
|
136
|
+
copy: (entry) => {
|
|
137
|
+
const copy = copied(entry, walk, level + 1);
|
|
138
|
+
height = Math.max(height, checkedHeight(entry, walk, level) + 1);
|
|
139
|
+
return copy;
|
|
140
|
+
},
|
|
141
|
+
height: () => height,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Throws `NestedTooDeepError` for a container the walk first reaches past its limit. */
|
|
145
|
+
function checkLevel(walk, level) {
|
|
146
|
+
if (level > walk.levels) {
|
|
147
|
+
throw new NestedTooDeepError();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* One array's copy. It joins `copies` before its entries are read, so an entry that leads back to
|
|
152
|
+
* it reaches the copy, and it is frozen once it is filled. A hole stays a hole.
|
|
153
|
+
*/
|
|
154
|
+
function copiedList(list, walk, level) {
|
|
155
|
+
checkLevel(walk, level);
|
|
156
|
+
const copy = [];
|
|
157
|
+
walk.copies.set(list, copy);
|
|
158
|
+
const entries = entryCopier(walk, level);
|
|
159
|
+
fillList(copy, list, entries.copy);
|
|
160
|
+
walk.heights.set(list, entries.height());
|
|
161
|
+
return Object.freeze(copy);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* One plain object's copy on `Object.prototype`, filled and frozen as an array's copy is. Each key
|
|
165
|
+
* is defined as an own data property, so a key of `__proto__` is stored under that name.
|
|
166
|
+
*/
|
|
167
|
+
function copiedRecord(record, walk, level) {
|
|
168
|
+
checkLevel(walk, level);
|
|
169
|
+
const copy = {};
|
|
170
|
+
walk.copies.set(record, copy);
|
|
171
|
+
const entries = entryCopier(walk, level);
|
|
172
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
173
|
+
defineEntry(copy, key, entries.copy(entry));
|
|
174
|
+
}
|
|
175
|
+
walk.heights.set(record, entries.height());
|
|
176
|
+
return Object.freeze(copy);
|
|
177
|
+
}
|
|
178
|
+
/** Defines one key as an own data property, so a key of `__proto__` is stored under that name. */
|
|
179
|
+
function defineEntry(target, key, value) {
|
|
180
|
+
Object.defineProperty(target, key, {
|
|
181
|
+
configurable: true,
|
|
182
|
+
enumerable: true,
|
|
183
|
+
value,
|
|
184
|
+
writable: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A copy of every own string key of one object, enumerable or not, each described once and read
|
|
189
|
+
* once, in the order the object lists its keys. `entering` hears each key before it is read, so a
|
|
190
|
+
* read that throws can name it. A symbol key is not copied, because no declaration declares one.
|
|
191
|
+
*/
|
|
192
|
+
function copyOwnKeys(declared, entering = () => undefined) {
|
|
193
|
+
const copy = {};
|
|
194
|
+
for (const key of Reflect.ownKeys(declared)) {
|
|
195
|
+
if (typeof key === 'string') {
|
|
196
|
+
entering(key);
|
|
197
|
+
if (Reflect.getOwnPropertyDescriptor(declared, key) !== undefined) {
|
|
198
|
+
defineEntry(copy, key, Reflect.get(declared, key));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return copy;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The copy of one part of a declaration that `decidePlain` judges plain, by `copyOwnKeys`. Any other
|
|
206
|
+
* value is answered as it is, so the rule for its slot reports it.
|
|
207
|
+
*/
|
|
208
|
+
function shallowRecord(value) {
|
|
209
|
+
return decidePlain(value) ? copyOwnKeys(value) : value;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Fills `copy` with what `convert` makes of each entry of `list`, read by its length once and then
|
|
213
|
+
* index by index, so none of the list's own methods runs. A hole stays a hole.
|
|
214
|
+
*/
|
|
215
|
+
function fillList(copy, list, convert) {
|
|
216
|
+
const { length } = list;
|
|
217
|
+
for (let index = 0; index < length; index += 1) {
|
|
218
|
+
if (Object.hasOwn(list, index)) {
|
|
219
|
+
defineEntry(copy, index, convert(list[index]));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
copy.length = length;
|
|
223
|
+
}
|
|
224
|
+
/** A copy of one list's entries by `fillList`. Each entry is what its factory built, so it is kept. */
|
|
225
|
+
function copyList(list) {
|
|
226
|
+
const copy = [];
|
|
227
|
+
fillList(copy, list, (entry) => entry);
|
|
228
|
+
return copy;
|
|
229
|
+
}
|
|
230
|
+
/** The copy of a value that is a list, by `copyList`. Any other value is answered as it is. */
|
|
231
|
+
function shallowList(value) {
|
|
232
|
+
if (!Array.isArray(value)) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
const list = value;
|
|
236
|
+
return copyList(list);
|
|
237
|
+
}
|
|
238
|
+
export { boundedSnapshot, copyList, copyOwnKeys, decidePlain, declaring, isPlainObject, NestedTooDeepError, shallowList, shallowRecord, snapshot, snapshotRecord, };
|