@loomcli/core 0.6.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.js +47 -15
- package/dist/capture.d.ts +65 -0
- package/dist/capture.js +99 -0
- package/dist/command.js +73 -44
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +8 -1
- package/dist/globals.js +9 -7
- package/dist/glyphs.generated.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/input-rules.d.ts +5 -1
- package/dist/input-rules.js +8 -1
- package/dist/inspect.d.ts +0 -8
- package/dist/inspect.js +5 -21
- package/dist/options.d.ts +7 -0
- package/dist/options.js +13 -7
- package/dist/plain.d.ts +52 -2
- package/dist/plain.js +228 -2
- package/dist/plugin-rules.d.ts +7 -1
- package/dist/plugin-rules.js +11 -2
- package/dist/plugin-settings.d.ts +18 -0
- package/dist/plugin-settings.js +38 -0
- package/dist/plugin.d.ts +6 -6
- package/dist/plugin.js +98 -45
- package/dist/sources.js +4 -4
- package/dist/style-layout.js +2 -2
- package/dist/style-width.d.ts +13 -0
- package/dist/style-width.js +170 -0
- package/dist/types.d.ts +2 -2
- package/dist/unicode.generated.d.ts +27 -0
- package/dist/unicode.generated.js +1036 -0
- package/dist/validation.d.ts +33 -12
- package/dist/validation.js +73 -28
- package/dist/view.d.ts +16 -11
- package/dist/view.js +13 -4
- package/licenses/unicode-LICENSE.txt +41 -0
- package/licenses/uucode-LICENSE.md +35 -0
- package/package.json +9 -5
package/NOTICE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@loomcli/core
|
|
2
|
+
|
|
3
|
+
This package includes material from uucode and from the Unicode Character
|
|
4
|
+
Database. Its license field reads "MIT AND Unicode-3.0": the Unicode tables
|
|
5
|
+
named below stay under the Unicode License v3, and every other file is under
|
|
6
|
+
the MIT License in LICENSE or, for the ported width iterator, in
|
|
7
|
+
licenses/uucode-LICENSE.md. Neither license asks anything of an application
|
|
8
|
+
that installs this package beyond keeping these notices with the files they
|
|
9
|
+
cover.
|
|
10
|
+
|
|
11
|
+
Unicode Character Database
|
|
12
|
+
Copyright © 1991-2025 Unicode, Inc.
|
|
13
|
+
Licensed under the Unicode License v3.
|
|
14
|
+
A copy of the License is in licenses/unicode-LICENSE.txt, and at
|
|
15
|
+
https://www.unicode.org/license.txt
|
|
16
|
+
|
|
17
|
+
uucode
|
|
18
|
+
Copyright (c) 2026 Tim Culverhouse
|
|
19
|
+
Licensed under the MIT License.
|
|
20
|
+
A copy of the License is in licenses/uucode-LICENSE.md.
|
|
21
|
+
Source: @rockorager/uucode 2.2.1 on npm
|
|
22
|
+
|
|
23
|
+
The Unicode tables core measures text with, in dist/unicode.generated.js, are
|
|
24
|
+
generated by the Loom repository's scripts/generate-unicode-tables.mjs from
|
|
25
|
+
the data files of @rockorager/uucode 2.2.1, which derive from Unicode 17 data.
|
|
26
|
+
The generator writes the same values as a JavaScript module, so a bundle
|
|
27
|
+
carries them.
|
|
28
|
+
|
|
29
|
+
The grapheme width iterator in dist/style-width.js is ported from the
|
|
30
|
+
@rockorager/uucode 2.2.1 width module. Changes made to it:
|
|
31
|
+
|
|
32
|
+
- It is written in TypeScript and reads the generated tables module.
|
|
33
|
+
- It yields each grapheme's start, end, and width, and no longer yields the
|
|
34
|
+
grapheme's text or offers clone, peek, or a standalone string width.
|
package/dist/application.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { captureDeclaration, unreadableArgument } from './capture.js';
|
|
1
2
|
import { runInvocation } from './chain.js';
|
|
2
3
|
import { portableName } from './command-rules.js';
|
|
3
4
|
import { attachToRoot, callArguments, childNode, buildGraph, checkDeclaredOptions, collectInputs, commandPlacement, inputPlaces, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, isPortableName, layerOf, portableNameCorrection, } from './command.js';
|
|
4
5
|
import { escapeControlCharacters } from './controls.js';
|
|
6
|
+
import { elided, spelled } from './diagnostic-text.js';
|
|
5
7
|
import { DeclarationError, exitCodeOf, InternalError, quoted, reasonOf, toFailure, } from './errors.js';
|
|
6
8
|
import { storeCommandLayers } from './extension.js';
|
|
7
9
|
import { checkDescription, checkNoListingFacts, checkVersion, partFinding, slotSite, } from './facts.js';
|
|
@@ -12,7 +14,7 @@ import { globalOptionAfterCommand } from './input-rules.js';
|
|
|
12
14
|
import { inspectGraph } from './inspect.js';
|
|
13
15
|
import { coreViews } from './lanes.js';
|
|
14
16
|
import { Output, reportPlainly } from './output.js';
|
|
15
|
-
import { isPlainObject } from './plain.js';
|
|
17
|
+
import { declaring, isPlainObject, shallowList, shallowRecord } from './plain.js';
|
|
16
18
|
import { invalidPacket, notAnObject, retiredApplicationOption } from './plugin-rules.js';
|
|
17
19
|
import { installPlugins, ownedSignals, pluginViews } from './plugin.js';
|
|
18
20
|
import { renderingPolicy } from './rendering.js';
|
|
@@ -133,11 +135,17 @@ class ApplicationBuilder {
|
|
|
133
135
|
}
|
|
134
136
|
globalOption(name, config) {
|
|
135
137
|
// The plugins' Commands attach at construction, so only the application's own calls close it.
|
|
138
|
+
// The config is judged after the order, so the finding prints it elided and reads none of it.
|
|
136
139
|
if (this.#config.composed) {
|
|
137
140
|
throw new DeclarationError(globalOptionAfterCommand, {
|
|
138
141
|
correction: 'Declare global options before attaching Commands or registering an action.',
|
|
139
142
|
findings: [
|
|
140
|
-
{
|
|
143
|
+
{
|
|
144
|
+
arguments: callArguments(name, config === undefined ? undefined : spelled(elided)),
|
|
145
|
+
call: 'globalOption',
|
|
146
|
+
mark: '0',
|
|
147
|
+
path: [],
|
|
148
|
+
},
|
|
141
149
|
],
|
|
142
150
|
sentence: `The Application declares global option ${quoted(name)} after command() or action().`,
|
|
143
151
|
});
|
|
@@ -481,7 +489,10 @@ function runRendering(rendering) {
|
|
|
481
489
|
subject: 'The run',
|
|
482
490
|
};
|
|
483
491
|
}
|
|
484
|
-
/**
|
|
492
|
+
/**
|
|
493
|
+
* Reject obsolete wiring before silently losing options that invocations depend on. `options` is the
|
|
494
|
+
* copy `captureOptions` took, or `undefined` for an Application declared without options.
|
|
495
|
+
*/
|
|
485
496
|
function checkOptions(name, options) {
|
|
486
497
|
const site = {
|
|
487
498
|
at: '1',
|
|
@@ -491,13 +502,6 @@ function checkOptions(name, options) {
|
|
|
491
502
|
if (options === undefined) {
|
|
492
503
|
return { description: undefined, version: checkVersion(site, undefined) };
|
|
493
504
|
}
|
|
494
|
-
if (!isPlainObject(options)) {
|
|
495
|
-
throw new DeclarationError(notAnObject, {
|
|
496
|
-
correction: 'Supply an Application options object.',
|
|
497
|
-
findings: [{ ...site.declaration, mark: '1' }],
|
|
498
|
-
sentence: 'The Application declares options that are not an object.',
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
505
|
for (const [key, correction] of retired) {
|
|
502
506
|
if (key in options) {
|
|
503
507
|
const retiredSite = optionSite(name, key, Reflect.get(options, key));
|
|
@@ -546,15 +550,43 @@ function readPacket(name, packet) {
|
|
|
546
550
|
sentence: found,
|
|
547
551
|
});
|
|
548
552
|
}
|
|
553
|
+
/** The parts of the Application options that are lists, each copied with its entries as built. */
|
|
554
|
+
const optionLists = ['plugins', 'views', 'translators', 'extensions'];
|
|
555
|
+
/**
|
|
556
|
+
* The one copy of the options that `new Application(name, options)` reads: the options object, its
|
|
557
|
+
* packet and rendering policy, and each list it holds. An entry of a list is what its factory built
|
|
558
|
+
* and is not copied. A read that throws is the unreadable fault of the options, and a value that is
|
|
559
|
+
* not a plain object is the not-an-object fault. An Application declared without options has none
|
|
560
|
+
* to copy.
|
|
561
|
+
*/
|
|
562
|
+
function captureOptions(name, options) {
|
|
563
|
+
if (options === undefined) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
return captureDeclaration(options, (copy, read) => {
|
|
567
|
+
read.nested(copy, 'packet', shallowRecord);
|
|
568
|
+
read.nested(copy, 'rendering', shallowRecord);
|
|
569
|
+
for (const list of optionLists) {
|
|
570
|
+
read.nested(copy, list, shallowList);
|
|
571
|
+
}
|
|
572
|
+
}, {
|
|
573
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
574
|
+
correction: 'Supply an Application options object.',
|
|
575
|
+
findings: [{ arguments: [name, options], call: 'new Application', mark: '1' }],
|
|
576
|
+
sentence: 'The Application declares options that are not an object.',
|
|
577
|
+
}),
|
|
578
|
+
unreadable: unreadableArgument({ call: 'new Application', named: name, subject: 'The Application' }, 'options'),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
549
581
|
/**
|
|
550
582
|
* Every rule `new Application(name, options)` applies, in the order it reads the slot: the
|
|
551
583
|
* application's own view overrides, its translations, the rendering policy, the options slot and
|
|
552
584
|
* its facts, the installed list and every rule between two plugins, the root's extension values,
|
|
553
585
|
* and then each plugin's Commands, which attach to the root first, in installation order and list
|
|
554
|
-
* order.
|
|
586
|
+
* order. Each reads the one copy of the options `captureOptions` takes.
|
|
555
587
|
*/
|
|
556
|
-
function declareApplication(name,
|
|
557
|
-
const slot =
|
|
588
|
+
function declareApplication(name, declared) {
|
|
589
|
+
const slot = captureOptions(name, declared);
|
|
558
590
|
const identities = viewIdentities(coreViews);
|
|
559
591
|
const views = buildViews({
|
|
560
592
|
declares: false,
|
|
@@ -563,7 +595,7 @@ function declareApplication(name, options) {
|
|
|
563
595
|
}, slot?.views, identities);
|
|
564
596
|
const translations = readTranslations(optionSite(name, 'translators', slot?.translators), slot?.translators);
|
|
565
597
|
const rendering = renderingPolicy(slot?.rendering, optionSite(name, 'rendering', slot?.rendering));
|
|
566
|
-
const facts = checkOptions(name,
|
|
598
|
+
const facts = checkOptions(name, slot);
|
|
567
599
|
const development = readPacket(name, slot?.packet);
|
|
568
600
|
const installed = installPlugins(name, slot?.plugins ?? []);
|
|
569
601
|
const { plugins } = installed;
|
|
@@ -624,7 +656,7 @@ class ApplicationDeclaration extends ApplicationBuilder {
|
|
|
624
656
|
constructor(name, options) {
|
|
625
657
|
// The arguments evaluate in order, so the name is checked before any option is read.
|
|
626
658
|
const checked = checkApplicationName(name);
|
|
627
|
-
super(checked, declareApplication(checked, options));
|
|
659
|
+
super(checked, declaring(() => declareApplication(checked, options)));
|
|
628
660
|
}
|
|
629
661
|
}
|
|
630
662
|
export const Application = ApplicationDeclaration;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Finding } from './diagnostic-text.js';
|
|
2
|
+
import { DeclarationError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* One declaring call's read of the object an author passed it, under way. It remembers the slot it
|
|
5
|
+
* is reading, so a read that throws names the slot it threw in, or no slot when the object itself
|
|
6
|
+
* threw.
|
|
7
|
+
*/
|
|
8
|
+
declare class DeclarationRead {
|
|
9
|
+
/** The top-level key being read, or `undefined` while the object itself is read. */
|
|
10
|
+
slot: string | undefined;
|
|
11
|
+
/** The copy of every own string key of one object, each read under its own slot. */
|
|
12
|
+
record(declared: object): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Replaces the value one key of a copy holds with `copy` of it, read under that key's slot. An
|
|
15
|
+
* absent key stays absent, so a finding prints the copy with the keys the author wrote.
|
|
16
|
+
*/
|
|
17
|
+
nested(captured: Record<string, unknown>, key: string, copy: (value: unknown) => unknown): void;
|
|
18
|
+
}
|
|
19
|
+
/** The two faults one declaring call raises when it cannot capture what it was given. */
|
|
20
|
+
interface CaptureFaults {
|
|
21
|
+
/** The value is not a plain object, so it has no keys for core to read. */
|
|
22
|
+
readonly notAnObject: () => DeclarationError;
|
|
23
|
+
/** A read threw the value it receives, in the top-level slot it names, or in the object itself. */
|
|
24
|
+
readonly unreadable: (thrown: unknown, slot: string | undefined) => DeclarationError;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Authoring's one read of an object a declaring call receives, inside one try: the verdict on its
|
|
28
|
+
* prototype, the copy of every own string key, and the copies `nested` takes of the parts it holds,
|
|
29
|
+
* each judged plain once by `decidePlain`. Every later check, stored value, and finding reads that
|
|
30
|
+
* copy and never the author's object again, so a getter runs once and a read that threw is never
|
|
31
|
+
* repeated. A value that is not a plain object is the not-an-object fault, and a read that throws,
|
|
32
|
+
* from a getter or a proxy trap, is the unreadable fault, named by the slot it threw in.
|
|
33
|
+
*/
|
|
34
|
+
declare function captureDeclaration<Declared>(declared: Declared, nested: (copy: Record<string, unknown>, read: DeclarationRead) => void, faults: CaptureFaults): Declared & Record<string, unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* What a finding prints for a declaration whose read threw: the slot it threw in, elided, under the
|
|
37
|
+
* keys that lead to it, or the whole declaration elided. A read that threw is never repeated.
|
|
38
|
+
*/
|
|
39
|
+
declare function elidedRead(slot: string | undefined): {
|
|
40
|
+
keys: readonly string[];
|
|
41
|
+
shown: unknown;
|
|
42
|
+
};
|
|
43
|
+
/** What one unreadable declaration is: an input's config, a plugin's definition, or an options object. */
|
|
44
|
+
type Unreadable = 'config' | 'definition' | 'options';
|
|
45
|
+
/**
|
|
46
|
+
* The fault for a declaration whose read threw, named by `subject` and marked by `findings`. The
|
|
47
|
+
* thrown value's reason ends the sentence and the value itself is the fault's cause.
|
|
48
|
+
*/
|
|
49
|
+
declare function unreadableFault(report: {
|
|
50
|
+
readonly declared: Unreadable;
|
|
51
|
+
readonly findings: readonly Finding[];
|
|
52
|
+
readonly subject: string;
|
|
53
|
+
}, thrown: unknown): DeclarationError;
|
|
54
|
+
/**
|
|
55
|
+
* The unreadable fault of the object a `plugin()` call or a constructor receives as its second
|
|
56
|
+
* argument. Its finding marks the top-level slot whose read threw, or the whole argument when the
|
|
57
|
+
* object itself threw, and prints that part elided.
|
|
58
|
+
*/
|
|
59
|
+
declare function unreadableArgument(declaration: {
|
|
60
|
+
readonly call: string;
|
|
61
|
+
readonly named: unknown;
|
|
62
|
+
readonly subject: string;
|
|
63
|
+
}, declared: 'definition' | 'options'): (thrown: unknown, slot: string | undefined) => DeclarationError;
|
|
64
|
+
export type { CaptureFaults };
|
|
65
|
+
export { captureDeclaration, elidedRead, unreadableArgument, unreadableFault };
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { elided, spelled } from './diagnostic-text.js';
|
|
2
|
+
import { asSentence, DeclarationError, reasonOf } from './errors.js';
|
|
3
|
+
import { copyOwnKeys, decidePlain } from './plain.js';
|
|
4
|
+
import { unreadableDeclaration } from './plugin-rules.js';
|
|
5
|
+
/**
|
|
6
|
+
* One declaring call's read of the object an author passed it, under way. It remembers the slot it
|
|
7
|
+
* is reading, so a read that throws names the slot it threw in, or no slot when the object itself
|
|
8
|
+
* threw.
|
|
9
|
+
*/
|
|
10
|
+
class DeclarationRead {
|
|
11
|
+
/** The top-level key being read, or `undefined` while the object itself is read. */
|
|
12
|
+
slot = undefined;
|
|
13
|
+
/** The copy of every own string key of one object, each read under its own slot. */
|
|
14
|
+
record(declared) {
|
|
15
|
+
const copy = copyOwnKeys(declared, (key) => {
|
|
16
|
+
this.slot = key;
|
|
17
|
+
});
|
|
18
|
+
this.slot = undefined;
|
|
19
|
+
return copy;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Replaces the value one key of a copy holds with `copy` of it, read under that key's slot. An
|
|
23
|
+
* absent key stays absent, so a finding prints the copy with the keys the author wrote.
|
|
24
|
+
*/
|
|
25
|
+
nested(captured, key, copy) {
|
|
26
|
+
if (Object.hasOwn(captured, key)) {
|
|
27
|
+
this.slot = key;
|
|
28
|
+
captured[key] = copy(captured[key]);
|
|
29
|
+
this.slot = undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Authoring's one read of an object a declaring call receives, inside one try: the verdict on its
|
|
35
|
+
* prototype, the copy of every own string key, and the copies `nested` takes of the parts it holds,
|
|
36
|
+
* each judged plain once by `decidePlain`. Every later check, stored value, and finding reads that
|
|
37
|
+
* copy and never the author's object again, so a getter runs once and a read that threw is never
|
|
38
|
+
* repeated. A value that is not a plain object is the not-an-object fault, and a read that throws,
|
|
39
|
+
* from a getter or a proxy trap, is the unreadable fault, named by the slot it threw in.
|
|
40
|
+
*/
|
|
41
|
+
function captureDeclaration(declared, nested, faults) {
|
|
42
|
+
const read = new DeclarationRead();
|
|
43
|
+
let copy = undefined;
|
|
44
|
+
try {
|
|
45
|
+
if (decidePlain(declared)) {
|
|
46
|
+
copy = read.record(declared);
|
|
47
|
+
nested(copy, read);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw faults.unreadable(error, read.slot);
|
|
52
|
+
}
|
|
53
|
+
if (copy === undefined) {
|
|
54
|
+
throw faults.notAnObject();
|
|
55
|
+
}
|
|
56
|
+
// Last resort: no typed path exists.
|
|
57
|
+
// The copy is built key by key, so the compiler types it as a record of unknown values.
|
|
58
|
+
// A spread would keep the declared type, but it reads enumerable keys alone and names no slot.
|
|
59
|
+
// It holds because the copy holds every own string key of the declared plain object.
|
|
60
|
+
// Each holds the value read from it once, or that value's copy of the same kind.
|
|
61
|
+
// No declaration type declares a symbol key, and none reads its prototype.
|
|
62
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
63
|
+
return copy;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* What a finding prints for a declaration whose read threw: the slot it threw in, elided, under the
|
|
67
|
+
* keys that lead to it, or the whole declaration elided. A read that threw is never repeated.
|
|
68
|
+
*/
|
|
69
|
+
function elidedRead(slot) {
|
|
70
|
+
return slot === undefined
|
|
71
|
+
? { keys: [], shown: spelled(elided) }
|
|
72
|
+
: { keys: [slot], shown: Object.fromEntries([[slot, spelled(elided)]]) };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The fault for a declaration whose read threw, named by `subject` and marked by `findings`. The
|
|
76
|
+
* thrown value's reason ends the sentence and the value itself is the fault's cause.
|
|
77
|
+
*/
|
|
78
|
+
function unreadableFault(report, thrown) {
|
|
79
|
+
const { declared, findings, subject } = report;
|
|
80
|
+
return new DeclarationError(unreadableDeclaration, {
|
|
81
|
+
correction: `Declare the ${declared} as a plain object literal whose properties read without throwing.`,
|
|
82
|
+
findings,
|
|
83
|
+
sentence: `${subject} ${declared} could not be read: ${asSentence(reasonOf(thrown))}`,
|
|
84
|
+
}, { cause: thrown });
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The unreadable fault of the object a `plugin()` call or a constructor receives as its second
|
|
88
|
+
* argument. Its finding marks the top-level slot whose read threw, or the whole argument when the
|
|
89
|
+
* object itself threw, and prints that part elided.
|
|
90
|
+
*/
|
|
91
|
+
function unreadableArgument(declaration, declared) {
|
|
92
|
+
const { call, named, subject } = declaration;
|
|
93
|
+
return (thrown, slot) => {
|
|
94
|
+
const { keys, shown } = elidedRead(slot);
|
|
95
|
+
const finding = { arguments: [named, shown], call, mark: ['1', ...keys].join('.') };
|
|
96
|
+
return unreadableFault({ declared, findings: [finding], subject }, thrown);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export { captureDeclaration, elidedRead, unreadableArgument, unreadableFault };
|
package/dist/command.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
var _a, _b;
|
|
2
2
|
import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
|
|
3
|
+
import { captureDeclaration, unreadableArgument } from './capture.js';
|
|
3
4
|
import { aliasWithoutNames, argumentDeclaredTwice, argumentsBesideChildren, commandAttachedTwice, commandGlobals, commandWithoutAction, declaredAfterAction, declaredName, groupOption, multipleActions, multipleResults, nestingDepth, notACommand, optionalArgumentLast, portableName, repeatedAlias, resultWithoutAction, resultWithoutViews, rowViewOnValue, siblingNameTaken, unknownDefaultView, variadicArgumentLast, viewName, viewShape, viewsWithoutResult, } from './command-rules.js';
|
|
4
|
-
import { quoteString, spelled } from './diagnostic-text.js';
|
|
5
|
+
import { elided, quoteString, spelled } from './diagnostic-text.js';
|
|
5
6
|
import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
|
|
6
7
|
import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
|
|
7
|
-
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
|
|
8
|
+
import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, declarerNote, siteFinding, } from './facts.js';
|
|
8
9
|
import { buildGlobals, checkLocalOptions } from './globals.js';
|
|
9
10
|
import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
|
|
10
|
-
import { graphMismatch, nodeAt, resultNode
|
|
11
|
+
import { graphMismatch, nodeAt, resultNode } from './inspect.js';
|
|
11
12
|
import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
|
|
12
|
-
import { isPlainObject } from './plain.js';
|
|
13
|
+
import { declaring, isPlainObject, shallowList, snapshot } from './plain.js';
|
|
13
14
|
import { brokenAttachHook, notAnObject } from './plugin-rules.js';
|
|
14
15
|
import { fillInputs } from './sources.js';
|
|
15
|
-
import {
|
|
16
|
+
import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
|
|
16
17
|
/**
|
|
17
18
|
* The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
|
|
18
19
|
* A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
|
|
@@ -73,16 +74,30 @@ export function commandPlacement(path, child) {
|
|
|
73
74
|
function constructorFinding(name, options, mark) {
|
|
74
75
|
return { arguments: callArguments(name, options), call: 'new Command', mark };
|
|
75
76
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The one copy of the options that `new Command(name, options)` reads, taken once the name is
|
|
79
|
+
* judged: the options object and its `extensions` list, whose entries are what their factory built
|
|
80
|
+
* and are not copied. A read that throws is the unreadable fault of the options, a value that is not
|
|
81
|
+
* a plain object is the not-an-object fault, and a Command declared without options has none.
|
|
82
|
+
*/
|
|
83
|
+
function captureCommandOptions(name, options) {
|
|
84
|
+
if (options === undefined) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return captureDeclaration(options, (copy, read) => {
|
|
88
|
+
read.nested(copy, 'extensions', shallowList);
|
|
89
|
+
}, {
|
|
90
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
80
91
|
correction: 'Supply a Command options object.',
|
|
81
92
|
findings: [constructorFinding(name, options, '1')],
|
|
82
93
|
sentence: `${commandSentence(name)} declares options that are not an object.`,
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
}),
|
|
95
|
+
unreadable: unreadableArgument({ call: 'new Command', named: name, subject: commandSentence(name) }, 'options'),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** A named Command's options never carry the retired globals wiring. */
|
|
99
|
+
function checkCommandOptions(name, options) {
|
|
100
|
+
if ('globals' in options) {
|
|
86
101
|
throw new DeclarationError(commandGlobals, {
|
|
87
102
|
correction: 'Declare globals on the Application and register its environment.',
|
|
88
103
|
findings: [constructorFinding(name, options, '1.globals')],
|
|
@@ -149,23 +164,28 @@ export function freshState(declaration) {
|
|
|
149
164
|
};
|
|
150
165
|
}
|
|
151
166
|
/**
|
|
152
|
-
* A named Command's own declaration, checked before the value exists: the name, the
|
|
153
|
-
* the core facts, and the extension values
|
|
154
|
-
* the author passed changes nothing the declaration holds.
|
|
167
|
+
* A named Command's own declaration, checked before the value exists: the name, then the one copy
|
|
168
|
+
* of the options slot, its core facts, and the extension values it carries. A later change to the
|
|
169
|
+
* options object the author passed changes nothing the declaration holds.
|
|
155
170
|
*/
|
|
156
171
|
function namedState(name, options) {
|
|
157
172
|
if (!isPortableName(name)) {
|
|
173
|
+
// The options are read after the name is judged, so the name's finding prints them elided.
|
|
158
174
|
throw new DeclarationError(portableName, {
|
|
159
175
|
correction: portableNameCorrection,
|
|
160
|
-
findings: [
|
|
176
|
+
findings: [
|
|
177
|
+
constructorFinding(name, options === undefined ? undefined : spelled(elided), '0'),
|
|
178
|
+
],
|
|
161
179
|
sentence: `Command name ${quoted(name)} is invalid.`,
|
|
162
180
|
});
|
|
163
181
|
}
|
|
164
|
-
|
|
165
|
-
|
|
182
|
+
const slot = captureCommandOptions(name, options);
|
|
183
|
+
if (slot !== undefined) {
|
|
184
|
+
checkCommandOptions(name, slot);
|
|
185
|
+
}
|
|
166
186
|
const site = {
|
|
167
187
|
at: '1',
|
|
168
|
-
declaration: { arguments: [name,
|
|
188
|
+
declaration: { arguments: [name, slot], call: 'new Command' },
|
|
169
189
|
subject: commandSentence(name),
|
|
170
190
|
};
|
|
171
191
|
// The facts are read in the order their diagnostics have always ranked.
|
|
@@ -221,6 +241,14 @@ function inputFinding(path, input, note) {
|
|
|
221
241
|
const site = declaringSite(input, inputPlace(input, { global: false, path }));
|
|
222
242
|
return siteFinding(site, site.named, note);
|
|
223
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* The finding for an input whose name is invalid, marking the name. The config is read after the
|
|
246
|
+
* name is judged, so it prints elided.
|
|
247
|
+
*/
|
|
248
|
+
function nameFinding(path, input) {
|
|
249
|
+
const site = configUnread(declaringSite(input, inputPlace(input, { global: false, path })));
|
|
250
|
+
return siteFinding(site, site.named);
|
|
251
|
+
}
|
|
224
252
|
/** The scope one Command's own options compile under: its subject, and each option's call. */
|
|
225
253
|
function localScope(name, path) {
|
|
226
254
|
return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
|
|
@@ -297,7 +325,7 @@ function checkArgumentName(command, declared, input) {
|
|
|
297
325
|
if (!isDeclaredName(input.name)) {
|
|
298
326
|
throw new DeclarationError(declaredName, {
|
|
299
327
|
correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
|
|
300
|
-
findings: [
|
|
328
|
+
findings: [nameFinding(path, input)],
|
|
301
329
|
sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
|
|
302
330
|
});
|
|
303
331
|
}
|
|
@@ -317,11 +345,13 @@ function checkArgumentName(command, declared, input) {
|
|
|
317
345
|
export function declareArgument(state, declared) {
|
|
318
346
|
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
319
347
|
// A name of another kind then reports as a declared name instead of failing to print in the order diagnostic.
|
|
320
|
-
// The config is
|
|
348
|
+
// The config is read once right after its name is judged, and every later check reads that copy.
|
|
321
349
|
const { name } = state;
|
|
322
350
|
checkArgumentName({ name, path: pathOf(name), subject: commandSubject(name) }, [], declared);
|
|
323
|
-
|
|
324
|
-
|
|
351
|
+
const input = {
|
|
352
|
+
...declared,
|
|
353
|
+
config: captureInputConfig(declared, { call: 'argument', path: pathOf(name) }),
|
|
354
|
+
};
|
|
325
355
|
checkOpen(state, {
|
|
326
356
|
arguments: [input.name, input.config],
|
|
327
357
|
call: 'argument',
|
|
@@ -350,11 +380,14 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
|
|
|
350
380
|
*/
|
|
351
381
|
export function declareOption(state, declared, table = noGlobals) {
|
|
352
382
|
// The call's own input is judged before the receiver's state, as alias() judges its names.
|
|
353
|
-
// The config is
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const input = {
|
|
357
|
-
|
|
383
|
+
// The config is read once right after its name is judged, and every later check reads that copy.
|
|
384
|
+
const path = pathOf(state.name);
|
|
385
|
+
checkOptionName(declared.name, configUnread(inputSite(state.name, path, declared)));
|
|
386
|
+
const input = {
|
|
387
|
+
...declared,
|
|
388
|
+
config: captureInputConfig(declared, { call: 'option', path }),
|
|
389
|
+
};
|
|
390
|
+
const site = inputSite(state.name, path, input);
|
|
358
391
|
checkOpen(state, {
|
|
359
392
|
arguments: [input.name, input.config],
|
|
360
393
|
call: 'option',
|
|
@@ -632,7 +665,7 @@ function checkGroup(state, place) {
|
|
|
632
665
|
throw new DeclarationError(groupOption, {
|
|
633
666
|
correction: 'Register an action or remove the option.',
|
|
634
667
|
findings: [inputFinding(place.path, option, 'no action reads it')],
|
|
635
|
-
sentence: `${commandSentence(name)} declares option
|
|
668
|
+
sentence: `${commandSentence(name)} declares option ${quoted(option.name)} but registers no action to receive it.`,
|
|
636
669
|
});
|
|
637
670
|
}
|
|
638
671
|
}
|
|
@@ -1045,13 +1078,13 @@ class AttachedCommandValue {
|
|
|
1045
1078
|
checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
|
|
1046
1079
|
}
|
|
1047
1080
|
else {
|
|
1048
|
-
checkOptionName(raw.name, inputSite(declared.name, path, raw));
|
|
1081
|
+
checkOptionName(raw.name, configUnread(inputSite(declared.name, path, raw)));
|
|
1049
1082
|
}
|
|
1050
|
-
checkInputConfig(raw, { call: raw.kind, path });
|
|
1051
1083
|
// Each kind captures its own config, so the input keeps the pairing its kind declares.
|
|
1084
|
+
const place = { call: raw.kind, path };
|
|
1052
1085
|
const input = raw.kind === 'argument'
|
|
1053
|
-
? { ...raw, config:
|
|
1054
|
-
: { ...raw, config:
|
|
1086
|
+
? { ...raw, config: captureInputConfig(raw, place) }
|
|
1087
|
+
: { ...raw, config: captureInputConfig(raw, place) };
|
|
1055
1088
|
return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
|
|
1056
1089
|
}
|
|
1057
1090
|
#derive(call, declared) {
|
|
@@ -1166,16 +1199,12 @@ function nameCollisionRule(declared, held) {
|
|
|
1166
1199
|
}
|
|
1167
1200
|
return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
|
|
1168
1201
|
}
|
|
1169
|
-
/** The note a finding for one hook-declared input carries. */
|
|
1170
|
-
function hookNote(identity) {
|
|
1171
|
-
return `declared by plugin ${quoted(identity)}`;
|
|
1172
|
-
}
|
|
1173
1202
|
/** The clause and the remedy an input another plugin's hook already declared earns. */
|
|
1174
1203
|
function hookClause(earlier, path) {
|
|
1175
1204
|
const { identity, input } = earlier;
|
|
1176
1205
|
return {
|
|
1177
1206
|
clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
|
|
1178
|
-
held: inputFinding(path, input,
|
|
1207
|
+
held: inputFinding(path, input, declarerNote(identity)),
|
|
1179
1208
|
kind: input.kind,
|
|
1180
1209
|
remedy: 'Install one of them.',
|
|
1181
1210
|
};
|
|
@@ -1285,14 +1314,14 @@ function checkAttachedSpelling(declared, named) {
|
|
|
1285
1314
|
throw new DeclarationError(spellingTaken, {
|
|
1286
1315
|
correction: 'Change one of the two spellings or omit the plugin.',
|
|
1287
1316
|
findings: [
|
|
1288
|
-
spellingPlace(site, option.role,
|
|
1317
|
+
spellingPlace(site, option.role, declarerNote(identity)),
|
|
1289
1318
|
...(used.finding === undefined ? [] : [used.finding]),
|
|
1290
1319
|
],
|
|
1291
1320
|
sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
|
|
1292
1321
|
});
|
|
1293
1322
|
}
|
|
1294
1323
|
}
|
|
1295
|
-
readSpellings(table, claimed, (_name, role) => spellingPlace(site, role,
|
|
1324
|
+
readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, declarerNote(identity)));
|
|
1296
1325
|
}
|
|
1297
1326
|
/** The declarations of one kind, keyed by name, the first of each name winning. */
|
|
1298
1327
|
function byName(inputs) {
|
|
@@ -1314,7 +1343,7 @@ function tableSpellings(globals) {
|
|
|
1314
1343
|
const { owner, site } = entry;
|
|
1315
1344
|
const note = owner.kind === 'plugin'
|
|
1316
1345
|
? `an option of plugin ${quoted(owner.identity)}`
|
|
1317
|
-
: `the global option
|
|
1346
|
+
: `the global option ${quoted(name)}`;
|
|
1318
1347
|
return spellingPlace(site, role, note);
|
|
1319
1348
|
};
|
|
1320
1349
|
}
|
|
@@ -1346,7 +1375,7 @@ function checkAttachedInputs(declared, attached, place) {
|
|
|
1346
1375
|
const local = locals.get(name);
|
|
1347
1376
|
return local === undefined || local.kind !== 'option'
|
|
1348
1377
|
? undefined
|
|
1349
|
-
: spellingPlace(scope.siteOf(local), role, `the local option
|
|
1378
|
+
: spellingPlace(scope.siteOf(local), role, `the local option ${quoted(name)}`);
|
|
1350
1379
|
});
|
|
1351
1380
|
for (const entry of attached) {
|
|
1352
1381
|
const { identity, input } = entry;
|
|
@@ -1354,7 +1383,7 @@ function checkAttachedInputs(declared, attached, place) {
|
|
|
1354
1383
|
if (collision) {
|
|
1355
1384
|
throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
|
|
1356
1385
|
correction: collision.remedy,
|
|
1357
|
-
findings: [inputFinding(path, input,
|
|
1386
|
+
findings: [inputFinding(path, input, declarerNote(identity)), collision.held],
|
|
1358
1387
|
sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
|
|
1359
1388
|
});
|
|
1360
1389
|
}
|
|
@@ -1594,7 +1623,7 @@ _b = CommandBuilder;
|
|
|
1594
1623
|
*/
|
|
1595
1624
|
class CommandDeclaration extends CommandBuilder {
|
|
1596
1625
|
constructor(name, options) {
|
|
1597
|
-
const declared = namedState(name, options);
|
|
1626
|
+
const declared = declaring(() => namedState(name, options));
|
|
1598
1627
|
super(declared.name, declared.state);
|
|
1599
1628
|
}
|
|
1600
1629
|
}
|
package/dist/facts.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export interface FactSite {
|
|
|
9
9
|
readonly declaration: Omit<Finding, 'mark' | 'note'>;
|
|
10
10
|
readonly at: string;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* The note a finding carries for what a plugin declared on the author's behalf, such as an input
|
|
14
|
+
* its hook declares or the settings its factory takes, so the diagnostic names the declarer.
|
|
15
|
+
*/
|
|
16
|
+
export declare function declarerNote(identity: string): string;
|
|
12
17
|
/** The finding for the call one site holds, marking one part of it, with a note when given. */
|
|
13
18
|
export declare function siteFinding(site: FactSite, mark: string, note?: string): Finding;
|
|
14
19
|
/**
|
package/dist/facts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { misplacedListingFact, notOneLine } from './command-rules.js';
|
|
2
|
-
import { DeclarationError } from './errors.js';
|
|
2
|
+
import { DeclarationError, quoted } from './errors.js';
|
|
3
3
|
import { flagNotBoolean } from './input-rules.js';
|
|
4
4
|
/**
|
|
5
5
|
* One character outside Unicode `White_Space`, so a fact holds prose and not only spacing.
|
|
@@ -12,6 +12,13 @@ const prose = /\P{White_Space}/u;
|
|
|
12
12
|
* The seven are LF, VT, FF, CR, NEL, LS, and PS, each of them `White_Space` too.
|
|
13
13
|
*/
|
|
14
14
|
const lineTerminator = /[\n\v\f\r\u0085\u2028\u2029]/u;
|
|
15
|
+
/**
|
|
16
|
+
* The note a finding carries for what a plugin declared on the author's behalf, such as an input
|
|
17
|
+
* its hook declares or the settings its factory takes, so the diagnostic names the declarer.
|
|
18
|
+
*/
|
|
19
|
+
export function declarerNote(identity) {
|
|
20
|
+
return `declared by plugin ${quoted(identity)}`;
|
|
21
|
+
}
|
|
15
22
|
/** The finding for the call one site holds, marking one part of it, with a note when given. */
|
|
16
23
|
export function siteFinding(site, mark, note) {
|
|
17
24
|
const finding = { ...site.declaration, mark };
|