@loomcli/core 0.1.1 → 0.3.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/dist/application.d.ts +73 -28
- package/dist/application.js +334 -99
- package/dist/chain.d.ts +68 -0
- package/dist/chain.js +372 -0
- package/dist/command.d.ts +201 -46
- package/dist/command.js +713 -57
- package/dist/environment.d.ts +22 -0
- package/dist/environment.js +1 -0
- package/dist/errors.d.ts +31 -51
- package/dist/errors.js +58 -90
- package/dist/extension.d.ts +99 -0
- package/dist/extension.js +330 -0
- package/dist/facts.d.ts +39 -0
- package/dist/facts.js +95 -0
- package/dist/globals.d.ts +49 -28
- package/dist/globals.js +104 -58
- package/dist/glyphs.generated.d.ts +464 -0
- package/dist/glyphs.generated.js +491 -0
- package/dist/host.js +2 -1
- package/dist/index.d.ts +21 -6
- package/dist/index.js +7 -2
- package/dist/inspect.d.ts +69 -12
- package/dist/inspect.js +83 -26
- package/dist/lanes.d.ts +26 -0
- package/dist/lanes.js +45 -0
- package/dist/options.d.ts +7 -0
- package/dist/options.js +9 -0
- package/dist/output.d.ts +93 -15
- package/dist/output.js +307 -34
- package/dist/plugin.d.ts +132 -0
- package/dist/plugin.js +278 -0
- package/dist/rendering.d.ts +21 -0
- package/dist/rendering.js +72 -0
- package/dist/sequence.d.ts +41 -0
- package/dist/sequence.js +225 -0
- package/dist/signals.d.ts +52 -0
- package/dist/signals.js +85 -0
- package/dist/style-ansi.d.ts +13 -0
- package/dist/style-ansi.js +306 -0
- package/dist/style-layout.d.ts +29 -0
- package/dist/style-layout.js +228 -0
- package/dist/style-resolve.d.ts +6 -0
- package/dist/style-resolve.js +26 -0
- package/dist/style-state.d.ts +14 -0
- package/dist/style-state.js +179 -0
- package/dist/style-wire.d.ts +31 -0
- package/dist/style-wire.js +201 -0
- package/dist/style.d.ts +86 -0
- package/dist/style.js +201 -0
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +22 -0
- package/dist/types.d.ts +222 -26
- package/dist/validation.d.ts +12 -3
- package/dist/validation.js +34 -17
- package/dist/view.d.ts +180 -0
- package/dist/view.js +307 -0
- package/package.json +2 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { DeclarationError, reasonOf } from './errors.js';
|
|
2
|
+
import { isPlainObject } from './facts.js';
|
|
3
|
+
/** Authored values register here, so the public brand publishes no state to reach or replace. */
|
|
4
|
+
const values = new WeakMap();
|
|
5
|
+
/**
|
|
6
|
+
* The descriptor that produced each stored output, keyed by the frozen record a node publishes. It
|
|
7
|
+
* lives beside the graph rather than on it, so a projection reads plain data while a typed read
|
|
8
|
+
* still compares descriptors by reference.
|
|
9
|
+
*/
|
|
10
|
+
const owners = new WeakMap();
|
|
11
|
+
/**
|
|
12
|
+
* The value one extension produces, branded with its target so a value on the wrong declaration is
|
|
13
|
+
* a compile error. It publishes the brand alone, so a value is never mistaken for the descriptor
|
|
14
|
+
* that produced it, and the input it carries stays private to this package.
|
|
15
|
+
*/
|
|
16
|
+
class ExtensionCarrier {
|
|
17
|
+
constructor(record) {
|
|
18
|
+
values.set(this, record);
|
|
19
|
+
Object.freeze(this);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One typed fact a plugin defines for one target. The descriptor is compared by reference wherever
|
|
24
|
+
* it appears, so one identity means one descriptor and a duplicated package copy is visible.
|
|
25
|
+
*/
|
|
26
|
+
function extension(identity, config) {
|
|
27
|
+
// `Object.assign` returns the same function object, so the value carries the descriptor itself.
|
|
28
|
+
function create(input) {
|
|
29
|
+
return new ExtensionCarrier({ descriptor, input });
|
|
30
|
+
}
|
|
31
|
+
const descriptor = Object.assign(create, {
|
|
32
|
+
identity,
|
|
33
|
+
schema: config.schema,
|
|
34
|
+
target: config.target,
|
|
35
|
+
});
|
|
36
|
+
Object.freeze(descriptor);
|
|
37
|
+
return descriptor;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The typed read of one extension value. It takes the node kind the descriptor targets, returns the
|
|
41
|
+
* stored output or `undefined`, compares the descriptor by reference with the one that produced the
|
|
42
|
+
* value, and runs no schema.
|
|
43
|
+
*/
|
|
44
|
+
function readExtension(node, descriptor) {
|
|
45
|
+
const record = node.extensions;
|
|
46
|
+
const owner = owners.get(record)?.get(descriptor.identity);
|
|
47
|
+
if (owner === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (owner !== descriptor) {
|
|
51
|
+
throw new DeclarationError(`Extension "${descriptor.identity}" was read through a descriptor that did not define the stored value. Install one copy of the package that defines it.`);
|
|
52
|
+
}
|
|
53
|
+
// Last resort: no typed path exists. The graph stores every output under a string identity, so
|
|
54
|
+
// The record reads back as `unknown` and no key relates one entry to a descriptor's schema.
|
|
55
|
+
// It holds because the reference test above proves this descriptor produced this output, and
|
|
56
|
+
// Build stored exactly what that descriptor's own schema returned.
|
|
57
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
58
|
+
return record[descriptor.identity];
|
|
59
|
+
}
|
|
60
|
+
/** How a diagnostic names the declarations one target covers. */
|
|
61
|
+
const applies = {
|
|
62
|
+
argument: 'arguments',
|
|
63
|
+
command: 'Commands',
|
|
64
|
+
option: 'options',
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The children one container contributes, or nothing when its own shape is not plain data. An
|
|
68
|
+
* accessor, a symbol key, and a non-enumerable own property are not plain data, and an `undefined`
|
|
69
|
+
* property value is absence, so it is dropped rather than stored. An array contributes its items,
|
|
70
|
+
* the way it reads back.
|
|
71
|
+
*/
|
|
72
|
+
function childrenOf(source) {
|
|
73
|
+
if (Array.isArray(source)) {
|
|
74
|
+
// `Array.from` reads a hole as the `undefined` it is, which the walk rejects like any other
|
|
75
|
+
// Value that is not plain data.
|
|
76
|
+
return Array.from(source, (value, index) => ({ key: String(index), value }));
|
|
77
|
+
}
|
|
78
|
+
if (!isPlainObject(source) || Object.getOwnPropertySymbols(source).length > 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const children = [];
|
|
82
|
+
for (const key of Object.getOwnPropertyNames(source)) {
|
|
83
|
+
const property = Object.getOwnPropertyDescriptor(source, key);
|
|
84
|
+
if (!property || !('value' in property) || !property.enumerable) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (property.value !== undefined) {
|
|
88
|
+
children.push({ key, value: property.value });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return children;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* One value the walk reached. A primitive is copied where it is read, and a container answers with
|
|
95
|
+
* the frame the walk fills. `ancestors` holds the containers on the path to this value, so a value
|
|
96
|
+
* that is one of them is a cycle.
|
|
97
|
+
*/
|
|
98
|
+
function openValue(value, key, ancestors) {
|
|
99
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
100
|
+
return { data: value, kind: 'leaf' };
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === 'number') {
|
|
103
|
+
return Number.isFinite(value) ? { data: value, kind: 'leaf' } : undefined;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value !== 'object' || ancestors.has(value)) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const children = childrenOf(value);
|
|
109
|
+
if (!children) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const isArray = Array.isArray(value);
|
|
113
|
+
return { frame: { children, entries: [], index: 0, isArray, key, source: value }, kind: 'frame' };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The frozen copy one finished container contributes. `Object.fromEntries` defines every key as an
|
|
117
|
+
* own data property, so an output key of `__proto__` is stored under that name and the copy keeps
|
|
118
|
+
* `Object.prototype`.
|
|
119
|
+
*/
|
|
120
|
+
function closeFrame(frame) {
|
|
121
|
+
return Object.freeze(frame.isArray ? frame.entries.map(([, value]) => value) : Object.fromEntries(frame.entries));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether a produced output is the plain data a node can freeze and every projection can read:
|
|
125
|
+
* strings, finite numbers, Booleans, null, arrays, and objects whose prototype is `Object.prototype`
|
|
126
|
+
* or null, to any depth and without cycles. It answers with the frozen copy, because the walk that
|
|
127
|
+
* proves the shape is the walk that builds it. The walk holds its own stack, so a deep output is
|
|
128
|
+
* bounded by the heap and never by the call stack. A declared default's snapshot answers a different
|
|
129
|
+
* question: it keeps a library object as it is, where an extension output holding one is rejected.
|
|
130
|
+
*/
|
|
131
|
+
function plainData(value) {
|
|
132
|
+
const ancestors = new Set();
|
|
133
|
+
const opened = openValue(value, '', ancestors);
|
|
134
|
+
if (!opened) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
if (opened.kind === 'leaf') {
|
|
138
|
+
return { data: opened.data };
|
|
139
|
+
}
|
|
140
|
+
const stack = [opened.frame];
|
|
141
|
+
ancestors.add(opened.frame.source);
|
|
142
|
+
for (;;) {
|
|
143
|
+
const frame = stack.at(-1);
|
|
144
|
+
if (!frame) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const child = frame.children[frame.index];
|
|
148
|
+
if (child) {
|
|
149
|
+
const next = openValue(child.value, child.key, ancestors);
|
|
150
|
+
if (!next) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
if (next.kind === 'leaf') {
|
|
154
|
+
frame.entries.push([child.key, next.data]);
|
|
155
|
+
frame.index += 1;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
stack.push(next.frame);
|
|
159
|
+
ancestors.add(next.frame.source);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
stack.pop();
|
|
164
|
+
ancestors.delete(frame.source);
|
|
165
|
+
const data = closeFrame(frame);
|
|
166
|
+
const parent = stack.at(-1);
|
|
167
|
+
if (!parent) {
|
|
168
|
+
return { data };
|
|
169
|
+
}
|
|
170
|
+
parent.entries.push([frame.key, data]);
|
|
171
|
+
parent.index += 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Whether a value is a descriptor: a callable object carrying an identity and a declared target. */
|
|
176
|
+
function isDescriptor(value) {
|
|
177
|
+
return (value !== null &&
|
|
178
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
179
|
+
'identity' in value &&
|
|
180
|
+
typeof value.identity === 'string' &&
|
|
181
|
+
'target' in value &&
|
|
182
|
+
(value.target === 'argument' || value.target === 'command' || value.target === 'option'));
|
|
183
|
+
}
|
|
184
|
+
/** One identity means one descriptor, wherever on the graph that descriptor appears. */
|
|
185
|
+
function registerDescriptor(descriptors, descriptor) {
|
|
186
|
+
const known = descriptors.get(descriptor.identity);
|
|
187
|
+
if (known === undefined) {
|
|
188
|
+
descriptors.set(descriptor.identity, descriptor);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (known !== descriptor) {
|
|
192
|
+
throw new DeclarationError(`Extension "${descriptor.identity}" is defined twice. Install one copy of the package that defines it.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Whether a value answers the Standard Schema v1 contract this build calls synchronously. */
|
|
196
|
+
function isSchema(value) {
|
|
197
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const standard = '~standard' in value ? value['~standard'] : undefined;
|
|
201
|
+
return (standard !== null &&
|
|
202
|
+
typeof standard === 'object' &&
|
|
203
|
+
'validate' in standard &&
|
|
204
|
+
typeof standard.validate === 'function');
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* One schema message as a sentence of its own. A schema author writes the message with or without a
|
|
208
|
+
* full stop, so the diagnostic supplies one only where the message carries none.
|
|
209
|
+
*/
|
|
210
|
+
function sentence(text) {
|
|
211
|
+
return text.endsWith('.') ? text : `${text}.`;
|
|
212
|
+
}
|
|
213
|
+
/** The message one rejected value reports, with the placeholder a silent schema earns. */
|
|
214
|
+
function issueText(issues) {
|
|
215
|
+
const first = Array.isArray(issues) ? issues[0] : undefined;
|
|
216
|
+
if (first !== null && typeof first === 'object' && 'message' in first) {
|
|
217
|
+
const { message } = first;
|
|
218
|
+
if (typeof message === 'string') {
|
|
219
|
+
return message;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// The sentence the caller composes ends the diagnostic, so this text carries no full stop.
|
|
223
|
+
return 'The schema rejected this value without an explanation';
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Whether one schema answered with a promise. A thenable object and a promise from another realm
|
|
227
|
+
* are as unwaitable here as a native one, so the test is the contract and not the class.
|
|
228
|
+
*/
|
|
229
|
+
function isThenable(value) {
|
|
230
|
+
return 'then' in value && typeof value.then === 'function';
|
|
231
|
+
}
|
|
232
|
+
/** The schema one descriptor answers with, which a JavaScript author can leave out. */
|
|
233
|
+
function schemaOf(subject, descriptor) {
|
|
234
|
+
const schema = 'schema' in descriptor ? descriptor.schema : undefined;
|
|
235
|
+
if (!isSchema(schema)) {
|
|
236
|
+
throw new DeclarationError(`${subject.sentence} holds extension "${descriptor.identity}", which declares no schema. Supply a Standard Schema v1 object that answers synchronously.`);
|
|
237
|
+
}
|
|
238
|
+
return schema;
|
|
239
|
+
}
|
|
240
|
+
/** The result one schema answered with, or the rejection its own throw is. */
|
|
241
|
+
function validated(subject, carried, schema) {
|
|
242
|
+
try {
|
|
243
|
+
return schema['~standard'].validate(carried.input);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
// A schema that throws rejected the value the only way it could, so it reads as a rejection.
|
|
247
|
+
throw new DeclarationError(`${subject.sentence} holds an invalid "${carried.descriptor.identity}" value: ${sentence(reasonOf(error))} Correct the value.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** Validates one carried value and answers the plain-data output the node stores under it. */
|
|
251
|
+
function validateValue(subject, carried) {
|
|
252
|
+
const { identity } = carried.descriptor;
|
|
253
|
+
const result = validated(subject, carried, schemaOf(subject, carried.descriptor));
|
|
254
|
+
if (result === null || typeof result !== 'object' || isThenable(result)) {
|
|
255
|
+
throw new DeclarationError(`Extension "${identity}" validates asynchronously. Supply a schema that answers synchronously.`);
|
|
256
|
+
}
|
|
257
|
+
const issues = 'issues' in result ? result.issues : undefined;
|
|
258
|
+
if (issues !== undefined) {
|
|
259
|
+
throw new DeclarationError(`${subject.sentence} holds an invalid "${identity}" value: ${sentence(issueText(issues))} Correct the value.`);
|
|
260
|
+
}
|
|
261
|
+
const output = plainData('value' in result ? result.value : undefined);
|
|
262
|
+
if (!output) {
|
|
263
|
+
throw new DeclarationError(`Extension "${identity}" produced a value that is not plain data ${subject.phrase}. Return strings, numbers, booleans, null, arrays, and plain objects.`);
|
|
264
|
+
}
|
|
265
|
+
return output.data;
|
|
266
|
+
}
|
|
267
|
+
/** An `extensions` slot holds a list of values, so anything else is the same declaration fault. */
|
|
268
|
+
function readList(subject, declared) {
|
|
269
|
+
if (declared === undefined) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
if (!Array.isArray(declared)) {
|
|
273
|
+
throw new DeclarationError(`${subject.sentence} holds a value that is not an extension value. Supply the value returned by calling an extension.`);
|
|
274
|
+
}
|
|
275
|
+
return declared;
|
|
276
|
+
}
|
|
277
|
+
/** The value one entry carries, under the rules its own slot's target sets. */
|
|
278
|
+
function carriedValue(subject, target, entry) {
|
|
279
|
+
const carried = typeof entry === 'object' && entry !== null ? values.get(entry) : undefined;
|
|
280
|
+
if (!carried) {
|
|
281
|
+
throw new DeclarationError(`${subject.sentence} holds a value that is not an extension value. Supply the value returned by calling an extension.`);
|
|
282
|
+
}
|
|
283
|
+
if (carried.descriptor.target !== target) {
|
|
284
|
+
throw new DeclarationError(`${subject.sentence} holds extension "${carried.descriptor.identity}", which applies to ${applies[carried.descriptor.target]}. Supply an extension that applies to ${applies[target]}.`);
|
|
285
|
+
}
|
|
286
|
+
return carried;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The frozen record one declaration publishes: each carried value validated once, synchronously,
|
|
290
|
+
* and stored under its extension's identity. The descriptors that produced them are recorded beside
|
|
291
|
+
* the record, so a typed read compares by reference without the graph carrying a reference.
|
|
292
|
+
*/
|
|
293
|
+
function buildExtensions(slot) {
|
|
294
|
+
const { subject } = slot;
|
|
295
|
+
const stored = [];
|
|
296
|
+
const defined = new Map();
|
|
297
|
+
for (const entry of readList(subject, slot.declared)) {
|
|
298
|
+
const carried = carriedValue(subject, slot.target, entry);
|
|
299
|
+
const { descriptor } = carried;
|
|
300
|
+
registerDescriptor(slot.descriptors, descriptor);
|
|
301
|
+
if (defined.has(descriptor.identity)) {
|
|
302
|
+
throw new DeclarationError(`${subject.sentence} holds extension "${descriptor.identity}" twice. Supply one value.`);
|
|
303
|
+
}
|
|
304
|
+
defined.set(descriptor.identity, descriptor);
|
|
305
|
+
stored.push([descriptor.identity, validateValue(subject, carried)]);
|
|
306
|
+
}
|
|
307
|
+
// `Object.fromEntries` defines each identity as an own data property, so an identity of
|
|
308
|
+
// `__proto__` is a key of the record and the record keeps `Object.prototype`.
|
|
309
|
+
const frozen = Object.freeze(Object.fromEntries(stored));
|
|
310
|
+
owners.set(frozen, defined);
|
|
311
|
+
return frozen;
|
|
312
|
+
}
|
|
313
|
+
/** Layers validate in authoring order; replacement updates existing keys without reinsertion. */
|
|
314
|
+
function buildCommandExtensions(slot) {
|
|
315
|
+
const stored = new Map();
|
|
316
|
+
const defined = new Map();
|
|
317
|
+
for (const declared of slot.layers) {
|
|
318
|
+
const layer = buildExtensions({ ...slot, declared, target: 'command' });
|
|
319
|
+
for (const [identity, value] of Object.entries(layer)) {
|
|
320
|
+
stored.set(identity, value);
|
|
321
|
+
}
|
|
322
|
+
for (const [identity, descriptor] of owners.get(layer) ?? []) {
|
|
323
|
+
defined.set(identity, descriptor);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const frozen = Object.freeze(Object.fromEntries(stored));
|
|
327
|
+
owners.set(frozen, defined);
|
|
328
|
+
return frozen;
|
|
329
|
+
}
|
|
330
|
+
export { buildCommandExtensions, buildExtensions, extension, isDescriptor, readExtension, registerDescriptor, };
|
package/dist/facts.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `description` core fact: one line of prose every projection reads. A value that is not a
|
|
3
|
+
* string fails the same way a blank one does, because the author reads one rule for one fact.
|
|
4
|
+
* An omitted description is absent, not a fault, so it passes through as `undefined`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkDescription(subject: string, value: unknown): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The `hidden` core fact: whether a listing omits this member. It is a Boolean, because a listing
|
|
9
|
+
* asks one question of it, and an omitted declaration reads `false`. Routing selects and parsing
|
|
10
|
+
* binds without reading it, so a hidden member behaves as any other.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkHidden(subject: string, value: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The `deprecated` core fact: the one-line migration message a listing shows beside the member.
|
|
15
|
+
* It answers the rule a description answers, because both are one line of prose a projection
|
|
16
|
+
* prints. A bare `true` is rejected with every other value that is not prose: a deprecation with
|
|
17
|
+
* no migration path leaves an operator or an agent with nothing to do.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkDeprecated(subject: string, value: unknown): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The declarations that carry neither listing fact: an argument, which cannot leave the grammar it
|
|
22
|
+
* sits in, and the root, which is every page's entry point. The types remove both keys there, and
|
|
23
|
+
* a JavaScript author, or a TypeScript author whose argument config is inferred from a value,
|
|
24
|
+
* reaches this rule instead.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkNoListingFacts(subject: string, declared: object): void;
|
|
27
|
+
/**
|
|
28
|
+
* The `version` core fact, which the Application alone carries. Core reads it as an opaque string,
|
|
29
|
+
* because the convention is the package manifest's own field and no scheme is imposed on it. An
|
|
30
|
+
* omitted version is `0.0.0`, which means unversioned, and core keeps no record of which one the
|
|
31
|
+
* author wrote.
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkVersion(value: unknown): string;
|
|
34
|
+
/**
|
|
35
|
+
* A structural value core reads as plain data: an object literal, and never a declaration that
|
|
36
|
+
* carries state of its own. The options slots read it to reject a value that is not an options
|
|
37
|
+
* object, and inspection reads it to copy a declared value faithfully.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
package/dist/facts.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DeclarationError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* One character outside Unicode `White_Space`, so a fact holds prose and not only spacing.
|
|
4
|
+
* The class covers the tab, the space, the line terminators, the no-break space, and every other
|
|
5
|
+
* space separator. A format character such as the zero-width space is outside it, so it is prose.
|
|
6
|
+
*/
|
|
7
|
+
const prose = /\P{White_Space}/u;
|
|
8
|
+
/**
|
|
9
|
+
* Every character that ends a line, so a fact a projection prints on one line holds none.
|
|
10
|
+
* The seven are LF, VT, FF, CR, NEL, LS, and PS, each of them `White_Space` too.
|
|
11
|
+
*/
|
|
12
|
+
const lineTerminator = /[\n\v\f\r\u0085\u2028\u2029]/u;
|
|
13
|
+
/**
|
|
14
|
+
* The `description` core fact: one line of prose every projection reads. A value that is not a
|
|
15
|
+
* string fails the same way a blank one does, because the author reads one rule for one fact.
|
|
16
|
+
* An omitted description is absent, not a fault, so it passes through as `undefined`.
|
|
17
|
+
*/
|
|
18
|
+
export function checkDescription(subject, value) {
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value !== 'string' || !prose.test(value) || lineTerminator.test(value)) {
|
|
23
|
+
throw new DeclarationError(`${subject} description must hold a character other than whitespace and no line terminator. Supply a one-line summary.`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The `hidden` core fact: whether a listing omits this member. It is a Boolean, because a listing
|
|
29
|
+
* asks one question of it, and an omitted declaration reads `false`. Routing selects and parsing
|
|
30
|
+
* binds without reading it, so a hidden member behaves as any other.
|
|
31
|
+
*/
|
|
32
|
+
export function checkHidden(subject, value) {
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value !== 'boolean') {
|
|
37
|
+
throw new DeclarationError(`${subject} hidden must be a Boolean. Supply true or false, or omit it.`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The `deprecated` core fact: the one-line migration message a listing shows beside the member.
|
|
43
|
+
* It answers the rule a description answers, because both are one line of prose a projection
|
|
44
|
+
* prints. A bare `true` is rejected with every other value that is not prose: a deprecation with
|
|
45
|
+
* no migration path leaves an operator or an agent with nothing to do.
|
|
46
|
+
*/
|
|
47
|
+
export function checkDeprecated(subject, value) {
|
|
48
|
+
if (value === undefined) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (typeof value !== 'string' || !prose.test(value) || lineTerminator.test(value)) {
|
|
52
|
+
throw new DeclarationError(`${subject} deprecated message must hold a character other than whitespace and no line terminator. Supply a one-line migration path, such as "Use get instead.".`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The declarations that carry neither listing fact: an argument, which cannot leave the grammar it
|
|
58
|
+
* sits in, and the root, which is every page's entry point. The types remove both keys there, and
|
|
59
|
+
* a JavaScript author, or a TypeScript author whose argument config is inferred from a value,
|
|
60
|
+
* reaches this rule instead.
|
|
61
|
+
*/
|
|
62
|
+
export function checkNoListingFacts(subject, declared) {
|
|
63
|
+
for (const fact of ['hidden', 'deprecated']) {
|
|
64
|
+
if (fact in declared) {
|
|
65
|
+
throw new DeclarationError(`${subject} declares ${fact}, which applies to named Commands and options alone. Remove it.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The `version` core fact, which the Application alone carries. Core reads it as an opaque string,
|
|
71
|
+
* because the convention is the package manifest's own field and no scheme is imposed on it. An
|
|
72
|
+
* omitted version is `0.0.0`, which means unversioned, and core keeps no record of which one the
|
|
73
|
+
* author wrote.
|
|
74
|
+
*/
|
|
75
|
+
export function checkVersion(value) {
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
return '0.0.0';
|
|
78
|
+
}
|
|
79
|
+
if (typeof value !== 'string' || !prose.test(value) || lineTerminator.test(value)) {
|
|
80
|
+
throw new DeclarationError('The Application version must be a string that holds a character other than whitespace and no line terminator. Supply a string such as "1.2.0".');
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A structural value core reads as plain data: an object literal, and never a declaration that
|
|
86
|
+
* carries state of its own. The options slots read it to reject a value that is not an options
|
|
87
|
+
* object, and inspection reads it to copy a declared value faithfully.
|
|
88
|
+
*/
|
|
89
|
+
export function isPlainObject(value) {
|
|
90
|
+
if (value === null || typeof value !== 'object') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const prototype = Object.getPrototypeOf(value);
|
|
94
|
+
return prototype === Object.prototype || prototype === null;
|
|
95
|
+
}
|
package/dist/globals.d.ts
CHANGED
|
@@ -1,36 +1,57 @@
|
|
|
1
|
+
import { DeclarationError } from './errors.js';
|
|
1
2
|
import { compileOptions } from './options.js';
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import type { BuiltPlugin, PluginBuild } from './plugin.js';
|
|
4
|
+
import type { OptionConfig, OptionValue } from './types.js';
|
|
5
|
+
import type { InputDeclaration, OptionInput, ValidatedInputs } from './validation.js';
|
|
6
|
+
/**
|
|
7
|
+
* Who declared one option that shares the globals table, or the Command that declares a local
|
|
8
|
+
* option colliding with it. Every collision sentence is derived from a pair of these, so the
|
|
9
|
+
* existing global-versus-local wording and the plugin wording read from one place.
|
|
10
|
+
*/
|
|
11
|
+
type OptionOwner = {
|
|
12
|
+
kind: 'application';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'local';
|
|
15
|
+
subject: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'plugin';
|
|
18
|
+
identity: string;
|
|
19
|
+
order: number;
|
|
20
|
+
};
|
|
21
|
+
/** One side of a collision: the option's declared name under the scope that declared it. */
|
|
22
|
+
interface OptionSite {
|
|
23
|
+
name: string;
|
|
24
|
+
owner: OptionOwner;
|
|
25
|
+
}
|
|
26
|
+
/** One key claimed twice, whichever two scopes claimed it. */
|
|
27
|
+
declare function keyCollision(name: string, first: OptionOwner, second: OptionOwner): DeclarationError;
|
|
28
|
+
/** One spelling claimed twice, whichever two scopes claimed it. */
|
|
29
|
+
declare function spellingCollision(spelling: string, first: OptionSite, second: OptionSite): DeclarationError;
|
|
30
|
+
/**
|
|
31
|
+
* The compiled global table: one spelling map shared by every Command in the graph. It holds the
|
|
32
|
+
* application's global options and every installed plugin's options, because the pre-scan reads one
|
|
33
|
+
* table. `inputs` holds the application's declarations alone, because a plugin option is never
|
|
34
|
+
* validated and never reaches an action.
|
|
35
|
+
*/
|
|
7
36
|
interface BuiltGlobals {
|
|
37
|
+
bind: (values: ValidatedInputs) => unknown;
|
|
8
38
|
inputs: readonly InputDeclaration[];
|
|
9
|
-
names:
|
|
39
|
+
names: ReadonlyMap<string, OptionOwner>;
|
|
10
40
|
options: ReturnType<typeof compileOptions>;
|
|
11
|
-
|
|
41
|
+
plugins: readonly BuiltPlugin[];
|
|
12
42
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): GlobalOptions<Options & Record<Name, OptionValue<Config>>>;
|
|
43
|
+
/** The Application's private global declarations and their schema-derived value binder. */
|
|
44
|
+
interface GlobalsState<Globals = unknown> {
|
|
45
|
+
bind: (values: ValidatedInputs) => Globals;
|
|
46
|
+
inputs: readonly OptionInput[];
|
|
18
47
|
}
|
|
48
|
+
declare function emptyGlobals(): GlobalsState<{}>;
|
|
49
|
+
declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, input: OptionInput<Name, Config>): GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
|
|
19
50
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*/
|
|
24
|
-
type GlobalOptions<Options = {}> = Pick<GlobalOptionsBuilder<Options>, typeof declaredTypes | 'option'>;
|
|
25
|
-
/**
|
|
26
|
-
* Whether a value is an authored GlobalOptions declaration, whichever call produced it. The
|
|
27
|
-
* registry is the test, because `option()` returns a new declaration of its own and the exported
|
|
28
|
-
* constructor is only the first of them.
|
|
51
|
+
* The one table the pre-scan reads: the application's global options in authoring order, then each
|
|
52
|
+
* installed plugin's options in installation order. Every collision between the two scopes, by key
|
|
53
|
+
* or by spelling, is reported here, so the pre-scan meets a table with one owner per name.
|
|
29
54
|
*/
|
|
30
|
-
declare function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
declare function bindGlobals<Options>(globals: GlobalOptions<Options> | undefined, values: ValidatedInputs): Options;
|
|
34
|
-
declare const GlobalOptions: new () => GlobalOptions;
|
|
35
|
-
export type { BuiltGlobals };
|
|
36
|
-
export { bindGlobals, buildGlobals, GlobalOptions, isGlobalOptions };
|
|
55
|
+
declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[], build: PluginBuild): BuiltGlobals;
|
|
56
|
+
export type { BuiltGlobals, GlobalsState, OptionOwner, OptionSite };
|
|
57
|
+
export { buildGlobals, declareGlobalOption, emptyGlobals, keyCollision, spellingCollision };
|