@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/plugin.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { checkEnvBinding, claimVariables } from './bindings.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { captureDeclaration, elidedRead, unreadableArgument, unreadableFault } from './capture.js';
|
|
3
|
+
import { notACommand } from './command-rules.js';
|
|
4
|
+
import { attach, commandCode, commandNode } from './command.js';
|
|
5
|
+
import { elided, quoteString, spelled } from './diagnostic-text.js';
|
|
6
|
+
import { DeclarationError, InternalError, quoted, reasonOf } from './errors.js';
|
|
4
7
|
import { appliesTo, buildExtensions, isDescriptor, registerDescriptor } from './extension.js';
|
|
5
|
-
import { checkDeprecated, checkDescription, checkHidden,
|
|
6
|
-
import { boundOptions } from './globals.js';
|
|
8
|
+
import { checkDeprecated, checkDescription, checkHidden, factFault, partFinding, pluginOptionSite, slotSite, } from './facts.js';
|
|
9
|
+
import { boundOptions, pluginSites } from './globals.js';
|
|
10
|
+
import { checkIdentity } from './identity.js';
|
|
7
11
|
import { coreViews } from './lanes.js';
|
|
8
12
|
import { booleanValue, compileOptions } from './options.js';
|
|
13
|
+
import { copyOwnKeys, decidePlain, declaring, isPlainObject, shallowList, shallowRecord, } from './plain.js';
|
|
14
|
+
import { foreignValue, middlewareActivation, notAFunction, notAList, notAnObject, pluginInstalledTwice, pluginOptionRule, signalClaimedTwice, slotTaken, sourceBinding, sourceBoundOwnOption, unknownSignal, } from './plugin-rules.js';
|
|
15
|
+
import { pluginLoaderFailed } from './rules.js';
|
|
9
16
|
import { isProcessSignal } from './signals.js';
|
|
10
17
|
import { buildTheme } from './theme.js';
|
|
11
|
-
import {
|
|
18
|
+
import { readTranslations } from './translators.js';
|
|
19
|
+
import { captureConfig, checkDeclarations, defaultDepthFault } from './validation.js';
|
|
12
20
|
import { buildViews, viewIdentities } from './view.js';
|
|
21
|
+
/** The slots of a definition that hold a list, each copied with its entries as they were built. */
|
|
22
|
+
const definitionLists = ['extensions', 'signals', 'commands', 'views', 'translators'];
|
|
13
23
|
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
14
24
|
const nodes = new WeakMap();
|
|
15
25
|
/**
|
|
@@ -25,110 +35,219 @@ class PluginDeclaration {
|
|
|
25
35
|
}
|
|
26
36
|
/**
|
|
27
37
|
* One plugin: an identity and the contributions it carries. Creating and installing the value runs
|
|
28
|
-
* none of its code:
|
|
29
|
-
*
|
|
30
|
-
* that
|
|
38
|
+
* none of its code: `onCommandAttach` runs at graph build, `onFailure` runs when `run()` renders a
|
|
39
|
+
* failure, and the middleware runs inside an invocation, so an installed plugin an invocation never
|
|
40
|
+
* reaches costs that invocation its hooks alone. The definition is read once, after the identity,
|
|
41
|
+
* and every rule that one definition carries on its own throws here, before the value exists.
|
|
31
42
|
*/
|
|
32
43
|
function plugin(identity, definition) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
return new PluginDeclaration(declaring(() => readPlugin(identity, definition)));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The one copy of a definition that `plugin()` reads: the definition, its theme mapping, options
|
|
48
|
+
* record, middleware with its activation list, and source, and each list it holds. An entry of a
|
|
49
|
+
* list is what its factory built and is not copied, and each option's config is captured where its
|
|
50
|
+
* own rules read it, so a fault about it names the option. A read that throws is the unreadable
|
|
51
|
+
* fault of the definition, and a definition that is not a plain object is the not-an-object fault.
|
|
52
|
+
*/
|
|
53
|
+
function captureDefinition(identity, definition) {
|
|
54
|
+
return captureDeclaration(definition, (copy, read) => {
|
|
55
|
+
read.nested(copy, 'theme', shallowRecord);
|
|
56
|
+
read.nested(copy, 'options', shallowRecord);
|
|
57
|
+
read.nested(copy, 'middleware', copyMiddleware);
|
|
58
|
+
read.nested(copy, 'source', shallowRecord);
|
|
59
|
+
for (const list of definitionLists) {
|
|
60
|
+
read.nested(copy, list, shallowList);
|
|
61
|
+
}
|
|
62
|
+
}, {
|
|
63
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
64
|
+
correction: 'Supply { options, middleware, extensions, views }.',
|
|
65
|
+
findings: [{ arguments: [identity, definition], call: 'plugin', mark: '1' }],
|
|
66
|
+
sentence: `${pluginSentence(identity)} declares a definition that is not an object.`,
|
|
67
|
+
}),
|
|
68
|
+
unreadable: unreadableArgument({ call: 'plugin', named: identity, subject: pluginSentence(identity) }, 'definition'),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** A middleware object's copy, its activation list copied too, or any other value as declared. */
|
|
72
|
+
function copyMiddleware(middleware) {
|
|
73
|
+
if (!decidePlain(middleware)) {
|
|
74
|
+
return middleware;
|
|
75
|
+
}
|
|
76
|
+
const copy = copyOwnKeys(middleware);
|
|
77
|
+
if (Object.hasOwn(copy, 'activate')) {
|
|
78
|
+
copy.activate = shallowList(copy.activate);
|
|
79
|
+
}
|
|
80
|
+
return copy;
|
|
40
81
|
}
|
|
41
82
|
/** How every plugin diagnostic names one plugin at the start of a sentence. */
|
|
42
83
|
function pluginSentence(identity) {
|
|
43
|
-
return `Plugin
|
|
84
|
+
return `Plugin ${quoted(identity)}`;
|
|
85
|
+
}
|
|
86
|
+
/** Where one slot of a plugin's definition sits, rebuilt as `plugin(identity, { slot })`. */
|
|
87
|
+
function pluginSlot(identity, slot, value) {
|
|
88
|
+
return slotSite({ call: 'plugin', named: identity, subject: pluginSentence(identity) }, slot, value);
|
|
89
|
+
}
|
|
90
|
+
/** The subject a plugin's `views` list reports under, and the call that declared it. */
|
|
91
|
+
function pluginViews(identity) {
|
|
92
|
+
return {
|
|
93
|
+
declares: true,
|
|
94
|
+
owner: { call: 'plugin', named: identity },
|
|
95
|
+
sentence: pluginSentence(identity),
|
|
96
|
+
};
|
|
44
97
|
}
|
|
45
|
-
/**
|
|
98
|
+
/** The declarations behind one installed value, or `undefined` for a value `plugin()` did not make. */
|
|
46
99
|
function nodeOf(value) {
|
|
47
|
-
|
|
48
|
-
if (!node) {
|
|
49
|
-
throw new DeclarationError('The Application holds a value that is not a plugin. Supply the value returned by plugin(identity, definition).');
|
|
50
|
-
}
|
|
51
|
-
return node;
|
|
100
|
+
return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
|
|
52
101
|
}
|
|
53
|
-
/**
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (identity === '') {
|
|
59
|
-
throw new DeclarationError('A plugin declares an empty identity. Supply a nonempty string, such as the package name.');
|
|
60
|
-
}
|
|
61
|
-
return identity;
|
|
102
|
+
/** One `plugins` entry as a finding prints it: a plugin as its `plugin()` call, elided. */
|
|
103
|
+
function entryCode(value) {
|
|
104
|
+
const node = nodeOf(value);
|
|
105
|
+
return node ? spelled(`plugin(${quoteString(node.identity)}, ${elided})`) : value;
|
|
62
106
|
}
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
/** How a second claim on one slot reads: its clause, the note on the owner's entry, and the fix. */
|
|
108
|
+
const slotWords = {
|
|
109
|
+
signals: {
|
|
110
|
+
clause: (owner) => `claims the signals slot, which plugin ${quoted(owner)} already holds.`,
|
|
111
|
+
correction: 'Install one owner.',
|
|
112
|
+
held: 'holds the signals slot',
|
|
113
|
+
},
|
|
114
|
+
source: {
|
|
115
|
+
clause: (owner) => `declares a configuration source, which plugin ${quoted(owner)} already declares.`,
|
|
116
|
+
correction: 'Install one source.',
|
|
117
|
+
held: 'declares the configuration source',
|
|
118
|
+
},
|
|
119
|
+
theme: {
|
|
120
|
+
clause: (owner) => `claims the theme slot, which plugin ${quoted(owner)} already holds.`,
|
|
121
|
+
correction: 'Install one owner.',
|
|
122
|
+
held: 'holds the theme slot',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
/** A second claim on one slot, which marks the owner's entry and the claimant's in `plugins`. */
|
|
126
|
+
function slotFault(site, slot, claim) {
|
|
127
|
+
const { claimant, installed, owner } = claim;
|
|
128
|
+
const words = slotWords[slot];
|
|
129
|
+
return new DeclarationError(slotTaken, {
|
|
130
|
+
correction: words.correction,
|
|
131
|
+
findings: [
|
|
132
|
+
partFinding(site, [owner], words.held),
|
|
133
|
+
partFinding(site, [claimant], 'claims it again'),
|
|
134
|
+
],
|
|
135
|
+
sentence: `${pluginSentence(installed[claimant]?.identity ?? '')} ${words.clause(installed[owner]?.identity ?? '')}`,
|
|
136
|
+
});
|
|
69
137
|
}
|
|
70
138
|
/**
|
|
71
139
|
* The installed list in composition order, with every rule that reads two plugins together: an
|
|
72
140
|
* identity installed twice, a second claim on the theme slot, the signals slot, or the
|
|
73
|
-
* configuration source, and two distinct descriptors under one identity. The slot is
|
|
74
|
-
*
|
|
75
|
-
* already ran at its `plugin()` call.
|
|
141
|
+
* configuration source, and two distinct descriptors under one identity. The slot is the copy the
|
|
142
|
+
* Application's constructor took, which a JavaScript author fills with any value. Each plugin's own
|
|
143
|
+
* rules already ran at its `plugin()` call.
|
|
76
144
|
*/
|
|
77
|
-
function installPlugins(plugins) {
|
|
145
|
+
function installPlugins(application, plugins) {
|
|
146
|
+
const printed = Array.isArray(plugins) ? Array.from(plugins, entryCode) : plugins;
|
|
147
|
+
const site = slotSite({ call: 'new Application', named: application, subject: 'The Application' }, 'plugins', printed);
|
|
78
148
|
if (!Array.isArray(plugins)) {
|
|
79
|
-
throw new DeclarationError(
|
|
149
|
+
throw new DeclarationError(notAList, {
|
|
150
|
+
correction: 'Supply a list of plugin values.',
|
|
151
|
+
findings: [partFinding(site, [])],
|
|
152
|
+
sentence: 'The Application declares plugins that are not an array.',
|
|
153
|
+
});
|
|
80
154
|
}
|
|
81
155
|
const list = plugins;
|
|
82
|
-
const installed = list.map((value) =>
|
|
83
|
-
|
|
156
|
+
const installed = list.map((value, index) => {
|
|
157
|
+
const node = nodeOf(value);
|
|
158
|
+
if (!node) {
|
|
159
|
+
throw new DeclarationError(foreignValue, {
|
|
160
|
+
correction: 'Supply the value returned by plugin(identity, definition).',
|
|
161
|
+
findings: [partFinding(site, [index])],
|
|
162
|
+
sentence: 'The Application holds a value that is not a plugin.',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return node;
|
|
166
|
+
});
|
|
167
|
+
const positions = new Map();
|
|
84
168
|
const descriptors = new Map();
|
|
85
169
|
// Each slot has one owner, so the first plugin to claim it names the second claimant's diagnostic.
|
|
86
|
-
const owners =
|
|
87
|
-
|
|
170
|
+
const owners = new Map();
|
|
171
|
+
const claim = (slot, index) => {
|
|
172
|
+
const owner = owners.get(slot);
|
|
173
|
+
if (owner !== undefined) {
|
|
174
|
+
throw slotFault(site, slot, { claimant: index, installed, owner });
|
|
175
|
+
}
|
|
176
|
+
owners.set(slot, index);
|
|
177
|
+
};
|
|
178
|
+
for (const [index, entry] of installed.entries()) {
|
|
88
179
|
const { identity } = entry;
|
|
89
|
-
|
|
90
|
-
|
|
180
|
+
const first = positions.get(identity);
|
|
181
|
+
if (first !== undefined) {
|
|
182
|
+
throw new DeclarationError(pluginInstalledTwice, {
|
|
183
|
+
correction: 'Install each plugin once.',
|
|
184
|
+
findings: [
|
|
185
|
+
partFinding(site, [first], 'the first installation'),
|
|
186
|
+
partFinding(site, [index], 'the second installation'),
|
|
187
|
+
],
|
|
188
|
+
sentence: `The Application installs plugin ${quoted(identity)} twice.`,
|
|
189
|
+
});
|
|
91
190
|
}
|
|
92
|
-
|
|
191
|
+
positions.set(identity, index);
|
|
93
192
|
if (entry.theme !== undefined) {
|
|
94
|
-
|
|
95
|
-
throw new DeclarationError(`${pluginSentence(identity)} claims the theme slot, which plugin "${owners.theme}" already holds. Install one owner.`);
|
|
96
|
-
}
|
|
97
|
-
owners.theme = identity;
|
|
193
|
+
claim('theme', index);
|
|
98
194
|
}
|
|
99
|
-
for (const descriptor of entry.descriptors
|
|
100
|
-
registerDescriptor(descriptors, descriptor
|
|
195
|
+
for (const [key, descriptor] of entry.descriptors) {
|
|
196
|
+
registerDescriptor(descriptors, descriptor, {
|
|
197
|
+
identity: key,
|
|
198
|
+
place: partFinding(site, [index]),
|
|
199
|
+
});
|
|
101
200
|
}
|
|
102
201
|
// An empty claim leaves the signals slot free.
|
|
103
202
|
if (entry.signals.length > 0) {
|
|
104
|
-
|
|
105
|
-
throw new DeclarationError(`${pluginSentence(identity)} claims the signals slot, which plugin "${owners.signals}" already holds. Install one owner.`);
|
|
106
|
-
}
|
|
107
|
-
owners.signals = identity;
|
|
203
|
+
claim('signals', index);
|
|
108
204
|
}
|
|
109
205
|
if (entry.source) {
|
|
110
|
-
|
|
111
|
-
throw new DeclarationError(`${pluginSentence(identity)} declares a configuration source, which plugin "${owners.source}" already declares. Install one source.`);
|
|
112
|
-
}
|
|
113
|
-
owners.source = identity;
|
|
206
|
+
claim('source', index);
|
|
114
207
|
}
|
|
115
208
|
}
|
|
116
209
|
return { descriptors, plugins: installed };
|
|
117
210
|
}
|
|
118
211
|
/** The keys a plugin option may not declare, in the order its diagnostic names them. */
|
|
119
212
|
const forbidden = ['validate', 'validateOmitted', 'required'];
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
213
|
+
/**
|
|
214
|
+
* The rules a plugin option answers before every rule an ordinary declaration carries, read from
|
|
215
|
+
* the one copy of its config that `captureConfig` takes, which it answers with for every later read.
|
|
216
|
+
* `siteOf` places the option with the value its entry prints as: the config as declared for a value
|
|
217
|
+
* that is not one, elided for a config whose read threw, and the copy for every later rule.
|
|
218
|
+
*/
|
|
219
|
+
function checkPluginOption(siteOf, declared) {
|
|
220
|
+
const sentence = siteOf(declared).subject;
|
|
221
|
+
const config = captureConfig(declared, {
|
|
222
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
223
|
+
correction: 'Supply { type, ... }.',
|
|
224
|
+
findings: [partFinding(siteOf(declared), [])],
|
|
225
|
+
sentence: `${sentence} is not an option declaration.`,
|
|
226
|
+
}),
|
|
227
|
+
// The default's walk stopped at the limit, so the finding prints it elided.
|
|
228
|
+
tooDeep: () => {
|
|
229
|
+
const { keys, shown } = elidedRead('default');
|
|
230
|
+
return defaultDepthFault(sentence, [partFinding(siteOf(shown), keys)]);
|
|
231
|
+
},
|
|
232
|
+
// A part whose read threw is never read again, so the finding prints it elided.
|
|
233
|
+
unreadable: (thrown, slot) => {
|
|
234
|
+
const { keys, shown } = elidedRead(slot);
|
|
235
|
+
return unreadableFault({ declared: 'config', findings: [partFinding(siteOf(shown), keys)], subject: sentence }, thrown);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const site = siteOf(config);
|
|
125
239
|
const rejected = forbidden.find((key) => key in config);
|
|
126
240
|
if (rejected !== undefined) {
|
|
127
|
-
throw
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
241
|
+
throw factFault(pluginOptionRule, site, {
|
|
242
|
+
correction: 'Remove it; a plugin option carries no validator or presence rule, and the middleware interprets the value.',
|
|
243
|
+
fact: rejected,
|
|
244
|
+
sentence: `${sentence} declares ${rejected}.`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
checkDescription(site, config.description);
|
|
248
|
+
checkHidden(site, config.hidden);
|
|
249
|
+
checkDeprecated(site, config.deprecated);
|
|
250
|
+
return config;
|
|
132
251
|
}
|
|
133
252
|
/**
|
|
134
253
|
* One plugin's option declarations, in declaration order. They join the globals table, so the rules
|
|
@@ -136,19 +255,36 @@ function checkPluginOption(sentence, config) {
|
|
|
136
255
|
*/
|
|
137
256
|
function readOptions(identity, declared, build) {
|
|
138
257
|
if (declared !== undefined && !isPlainObject(declared)) {
|
|
139
|
-
throw new DeclarationError(
|
|
258
|
+
throw new DeclarationError(notAnObject, {
|
|
259
|
+
correction: 'Supply a record of option declarations.',
|
|
260
|
+
findings: [partFinding(pluginSlot(identity, 'options', declared), [])],
|
|
261
|
+
sentence: `${pluginSentence(identity)} declares options that are not an object.`,
|
|
262
|
+
});
|
|
140
263
|
}
|
|
141
264
|
const inputs = [];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
265
|
+
// A finding prints each option's captured config once it is taken, and each later one elided.
|
|
266
|
+
const shown = Object.fromEntries(Object.keys(declared ?? {}).map((name) => [name, spelled(elided)]));
|
|
267
|
+
for (const [name, entry] of Object.entries(declared ?? {})) {
|
|
268
|
+
const sentence = `${pluginSentence(identity)} option ${quoted(name)}`;
|
|
269
|
+
// A computed key defines its own property even for `__proto__`, where assignment would not.
|
|
270
|
+
const siteOf = (value) => pluginOptionSite({ identity, options: { ...shown, [name]: value } }, name, sentence);
|
|
271
|
+
const config = checkPluginOption(siteOf, entry);
|
|
272
|
+
// Defining the key keeps its place, and holds even for a key of `__proto__`.
|
|
273
|
+
Object.defineProperty(shown, name, {
|
|
274
|
+
configurable: true,
|
|
275
|
+
enumerable: true,
|
|
276
|
+
value: config,
|
|
277
|
+
writable: true,
|
|
278
|
+
});
|
|
279
|
+
const site = siteOf(config);
|
|
280
|
+
checkEnvBinding(site, config);
|
|
281
|
+
const input = { config, kind: 'option', name };
|
|
147
282
|
// The shared rules name the plugin and the option, so a fault reads with its contributor.
|
|
148
|
-
checkDeclarations([input], sentence);
|
|
283
|
+
checkDeclarations([{ input, site }], sentence);
|
|
149
284
|
build.records.set(input, buildExtensions({
|
|
150
285
|
declared: config.extensions,
|
|
151
286
|
descriptors: build.descriptors,
|
|
287
|
+
site: { ...site, at: `${site.at}.extensions` },
|
|
152
288
|
subject: {
|
|
153
289
|
phrase: `on ${sentence.slice(0, 1).toLowerCase()}${sentence.slice(1)}`,
|
|
154
290
|
sentence,
|
|
@@ -158,30 +294,49 @@ function readOptions(identity, declared, build) {
|
|
|
158
294
|
inputs.push(input);
|
|
159
295
|
}
|
|
160
296
|
// Two options of one plugin meet in the one table the pre-scan reads, so they share its rules.
|
|
161
|
-
|
|
162
|
-
|
|
297
|
+
const siteOf = pluginSites(identity, inputs);
|
|
298
|
+
compileOptions(inputs, { siteOf, subject: `plugin ${quoted(identity)}` });
|
|
299
|
+
claimVariables(boundOptions(inputs, (input) => ({
|
|
300
|
+
phrase: `plugin ${quoted(identity)} option ${quoted(input.name)}`,
|
|
301
|
+
site: siteOf(input),
|
|
302
|
+
})));
|
|
163
303
|
return inputs;
|
|
164
304
|
}
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function readActivation(identity, declared, names) {
|
|
174
|
-
if (declared === 'always') {
|
|
305
|
+
/**
|
|
306
|
+
* The activation a middleware declares, checked against the options its own plugin declares. `site`
|
|
307
|
+
* holds the middleware object, and a fault marks its `activate` key, or the object itself when the
|
|
308
|
+
* key is absent.
|
|
309
|
+
*/
|
|
310
|
+
function readActivation(site, declared, names) {
|
|
311
|
+
const { activate } = declared;
|
|
312
|
+
if (activate === 'always') {
|
|
175
313
|
return 'always';
|
|
176
314
|
}
|
|
177
|
-
if (!Array.isArray(
|
|
178
|
-
throw new DeclarationError(
|
|
315
|
+
if (!Array.isArray(activate)) {
|
|
316
|
+
throw new DeclarationError(middlewareActivation, {
|
|
317
|
+
correction: "Supply activate: 'always' or a list of the plugin's own option names.",
|
|
318
|
+
findings: [partFinding(site, 'activate' in declared ? ['activate'] : [])],
|
|
319
|
+
sentence: `${site.subject} declares middleware with no activation.`,
|
|
320
|
+
});
|
|
179
321
|
}
|
|
180
|
-
const list =
|
|
322
|
+
const list = activate;
|
|
181
323
|
if (list.length === 0) {
|
|
182
|
-
throw new DeclarationError(
|
|
183
|
-
|
|
184
|
-
|
|
324
|
+
throw new DeclarationError(middlewareActivation, {
|
|
325
|
+
correction: "Name at least one of the plugin's options or use 'always'.",
|
|
326
|
+
findings: [partFinding(site, ['activate'])],
|
|
327
|
+
sentence: `${site.subject} declares middleware with an empty activation list.`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return list.map((name, index) => {
|
|
331
|
+
if (typeof name !== 'string' || !names.has(name)) {
|
|
332
|
+
throw new DeclarationError(middlewareActivation, {
|
|
333
|
+
correction: "Name one of the plugin's own options.",
|
|
334
|
+
findings: [partFinding(site, ['activate', index])],
|
|
335
|
+
sentence: `${site.subject} activates middleware on option ${quoted(name)}, which it does not declare.`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return name;
|
|
339
|
+
});
|
|
185
340
|
}
|
|
186
341
|
/**
|
|
187
342
|
* The loader a middleware declares. Core calls it with no arguments and reads whatever it resolves
|
|
@@ -201,28 +356,47 @@ async function loadDefault(identity, load, owed) {
|
|
|
201
356
|
module = await load();
|
|
202
357
|
}
|
|
203
358
|
catch (error) {
|
|
204
|
-
throw new InternalError(
|
|
359
|
+
throw new InternalError(pluginLoaderFailed, {
|
|
360
|
+
cause: error,
|
|
361
|
+
correction: loaderCorrection,
|
|
362
|
+
sentence: `Loading plugin ${quoted(identity)} failed: ${reasonOf(error)}`,
|
|
363
|
+
});
|
|
205
364
|
}
|
|
206
365
|
const exported = module !== null && typeof module === 'object' && 'default' in module
|
|
207
366
|
? module.default
|
|
208
367
|
: undefined;
|
|
209
368
|
if (!owed.guard(exported)) {
|
|
210
|
-
throw new InternalError(
|
|
369
|
+
throw new InternalError(pluginLoaderFailed, {
|
|
370
|
+
cause: undefined,
|
|
371
|
+
correction: loaderCorrection,
|
|
372
|
+
sentence: `Loading plugin ${quoted(identity)} failed: the module exports no default ${owed.noun} function.`,
|
|
373
|
+
});
|
|
211
374
|
}
|
|
212
375
|
return exported;
|
|
213
376
|
}
|
|
377
|
+
/** The fix every plugin loader fault shares. */
|
|
378
|
+
const loaderCorrection = "Make load() resolve to a module whose default export is the plugin's function, such as () => import('./middleware.js').";
|
|
214
379
|
/** One plugin's middleware, or `undefined` for a plugin that declares none. */
|
|
215
380
|
function readMiddleware(identity, declared, names) {
|
|
216
381
|
if (declared === undefined) {
|
|
217
382
|
return undefined;
|
|
218
383
|
}
|
|
384
|
+
const site = pluginSlot(identity, 'middleware', declared);
|
|
219
385
|
if (!isPlainObject(declared)) {
|
|
220
|
-
throw new DeclarationError(
|
|
386
|
+
throw new DeclarationError(notAnObject, {
|
|
387
|
+
correction: 'Supply { activate, load }.',
|
|
388
|
+
findings: [partFinding(site, [])],
|
|
389
|
+
sentence: `${pluginSentence(identity)} declares middleware that is not an object.`,
|
|
390
|
+
});
|
|
221
391
|
}
|
|
222
|
-
const activate = readActivation(
|
|
392
|
+
const activate = readActivation(site, declared, names);
|
|
223
393
|
const { load } = declared;
|
|
224
394
|
if (!isLoader(load)) {
|
|
225
|
-
throw new DeclarationError(
|
|
395
|
+
throw new DeclarationError(notAFunction, {
|
|
396
|
+
correction: "Supply load: () => import('./middleware.js').",
|
|
397
|
+
findings: [partFinding(site, 'load' in declared ? ['load'] : [])],
|
|
398
|
+
sentence: `${pluginSentence(identity)} declares middleware with no load function.`,
|
|
399
|
+
});
|
|
226
400
|
}
|
|
227
401
|
return { activate, load };
|
|
228
402
|
}
|
|
@@ -236,17 +410,42 @@ function readCommands(identity, declared) {
|
|
|
236
410
|
return [];
|
|
237
411
|
}
|
|
238
412
|
if (!Array.isArray(declared)) {
|
|
239
|
-
throw new DeclarationError(
|
|
413
|
+
throw new DeclarationError(notAList, {
|
|
414
|
+
correction: 'Supply a list of Command values.',
|
|
415
|
+
findings: [partFinding(pluginSlot(identity, 'commands', declared), [])],
|
|
416
|
+
sentence: `${pluginSentence(identity)} declares commands that are not an array.`,
|
|
417
|
+
});
|
|
240
418
|
}
|
|
241
419
|
const list = declared;
|
|
242
420
|
const commands = [];
|
|
421
|
+
// Each entry prints as the Command it is, so a finding rebuilds the list the plugin declared.
|
|
422
|
+
const entries = Array.from(list, (value) => {
|
|
423
|
+
const node = commandNode(value);
|
|
424
|
+
return node ? commandCode(node.name) : value;
|
|
425
|
+
});
|
|
243
426
|
// A for...of walk reads a hole as undefined, which the entry rule rejects, where map would skip it.
|
|
244
|
-
for (const value of list) {
|
|
427
|
+
for (const [index, value] of list.entries()) {
|
|
245
428
|
const node = commandNode(value);
|
|
429
|
+
const placement = {
|
|
430
|
+
arguments: [identity, { commands: entries }],
|
|
431
|
+
call: 'plugin',
|
|
432
|
+
mark: `1.commands.${String(index)}`,
|
|
433
|
+
};
|
|
246
434
|
if (!node) {
|
|
247
|
-
throw new DeclarationError(
|
|
435
|
+
throw new DeclarationError(notACommand, {
|
|
436
|
+
correction: 'Supply the value returned by new Command(name).',
|
|
437
|
+
findings: [placement],
|
|
438
|
+
sentence: `${pluginSentence(identity)} holds a value that is not a Command.`,
|
|
439
|
+
});
|
|
248
440
|
}
|
|
249
|
-
|
|
441
|
+
const parent = {
|
|
442
|
+
argument: undefined,
|
|
443
|
+
children: commands,
|
|
444
|
+
hasAction: false,
|
|
445
|
+
name: null,
|
|
446
|
+
path: [],
|
|
447
|
+
};
|
|
448
|
+
commands.push(attach(parent, node, placement));
|
|
250
449
|
}
|
|
251
450
|
return commands;
|
|
252
451
|
}
|
|
@@ -260,21 +459,39 @@ function readSignals(identity, declared) {
|
|
|
260
459
|
if (declared === undefined) {
|
|
261
460
|
return [];
|
|
262
461
|
}
|
|
462
|
+
const site = pluginSlot(identity, 'signals', declared);
|
|
263
463
|
if (!Array.isArray(declared)) {
|
|
264
|
-
throw new DeclarationError(
|
|
464
|
+
throw new DeclarationError(notAList, {
|
|
465
|
+
correction: 'Supply a list of signal names.',
|
|
466
|
+
findings: [partFinding(site, [])],
|
|
467
|
+
sentence: `${pluginSentence(identity)} declares signals that are not an array.`,
|
|
468
|
+
});
|
|
265
469
|
}
|
|
266
470
|
const list = declared;
|
|
267
|
-
|
|
268
|
-
|
|
471
|
+
// Each claimed signal's position, so a repeat marks both claims.
|
|
472
|
+
const claimed = new Map();
|
|
473
|
+
for (const [index, value] of list.entries()) {
|
|
269
474
|
if (!isProcessSignal(value)) {
|
|
270
|
-
throw new DeclarationError(
|
|
475
|
+
throw new DeclarationError(unknownSignal, {
|
|
476
|
+
correction: 'Claim SIGINT or SIGTERM.',
|
|
477
|
+
findings: [partFinding(site, [index])],
|
|
478
|
+
sentence: `${pluginSentence(identity)} claims signal ${quoted(value)}.`,
|
|
479
|
+
});
|
|
271
480
|
}
|
|
272
|
-
|
|
273
|
-
|
|
481
|
+
const first = claimed.get(value);
|
|
482
|
+
if (first !== undefined) {
|
|
483
|
+
throw new DeclarationError(signalClaimedTwice, {
|
|
484
|
+
correction: 'Claim each signal once.',
|
|
485
|
+
findings: [
|
|
486
|
+
partFinding(site, [first], 'the first claim'),
|
|
487
|
+
partFinding(site, [index], 'the second claim'),
|
|
488
|
+
],
|
|
489
|
+
sentence: `${pluginSentence(identity)} claims signal ${quoted(value)} twice.`,
|
|
490
|
+
});
|
|
274
491
|
}
|
|
275
|
-
claimed.
|
|
492
|
+
claimed.set(value, index);
|
|
276
493
|
}
|
|
277
|
-
return [...claimed];
|
|
494
|
+
return [...claimed.keys()];
|
|
278
495
|
}
|
|
279
496
|
/**
|
|
280
497
|
* A lifecycle hook is a function core calls at one named point, so being callable is the whole
|
|
@@ -289,7 +506,29 @@ function readHook(identity, declared) {
|
|
|
289
506
|
return undefined;
|
|
290
507
|
}
|
|
291
508
|
if (!isHook(declared)) {
|
|
292
|
-
throw new DeclarationError(
|
|
509
|
+
throw new DeclarationError(notAFunction, {
|
|
510
|
+
correction: 'Supply a function of the Command.',
|
|
511
|
+
findings: [partFinding(pluginSlot(identity, 'onCommandAttach', declared), [])],
|
|
512
|
+
sentence: `${pluginSentence(identity)} declares onCommandAttach that is not a function.`,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return declared;
|
|
516
|
+
}
|
|
517
|
+
/** Being callable is the whole claim, as it is for `onCommandAttach`; `run()` checks what it returns. */
|
|
518
|
+
function isFailureHook(value) {
|
|
519
|
+
return typeof value === 'function';
|
|
520
|
+
}
|
|
521
|
+
/** One plugin's `onFailure` hook, or `undefined` for a plugin that declares none. */
|
|
522
|
+
function readFailureHook(identity, declared) {
|
|
523
|
+
if (declared === undefined) {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
if (!isFailureHook(declared)) {
|
|
527
|
+
throw new DeclarationError(notAFunction, {
|
|
528
|
+
correction: 'Supply a function of the failure and its context.',
|
|
529
|
+
findings: [partFinding(pluginSlot(identity, 'onFailure', declared), [])],
|
|
530
|
+
sentence: `${pluginSentence(identity)} declares onFailure that is not a function.`,
|
|
531
|
+
});
|
|
293
532
|
}
|
|
294
533
|
return declared;
|
|
295
534
|
}
|
|
@@ -305,48 +544,90 @@ function readSource(identity, declaration, own) {
|
|
|
305
544
|
return undefined;
|
|
306
545
|
}
|
|
307
546
|
const sentence = pluginSentence(identity);
|
|
547
|
+
const site = pluginSlot(identity, 'source', declared);
|
|
308
548
|
if (!isPlainObject(declared)) {
|
|
309
|
-
throw new DeclarationError(
|
|
549
|
+
throw new DeclarationError(notAnObject, {
|
|
550
|
+
correction: 'Supply { binding, load }.',
|
|
551
|
+
findings: [partFinding(site, [])],
|
|
552
|
+
sentence: `${sentence} declares a source that is not an object.`,
|
|
553
|
+
});
|
|
310
554
|
}
|
|
311
555
|
const { binding, load } = declared;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
556
|
+
// A fault about one key marks the key, or the source itself when the key is absent.
|
|
557
|
+
const keyFinding = (key) => partFinding(site, key in declared ? [key] : []);
|
|
558
|
+
// The plugin's own extensions were admitted first, so a listed binding is in the registry.
|
|
559
|
+
// Its identity is read from there, where admission read it once, and never from the binding again.
|
|
560
|
+
const bound = [...own.build.descriptors].find(([, descriptor]) => descriptor === binding);
|
|
561
|
+
if (bound === undefined) {
|
|
562
|
+
throw new DeclarationError(sourceBinding, {
|
|
563
|
+
correction: 'Supply a descriptor the plugin lists under extensions.',
|
|
564
|
+
findings: [keyFinding('binding')],
|
|
565
|
+
sentence: `${sentence} declares a source binding that is not one of its extensions.`,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
const [bindingIdentity, { target }] = bound;
|
|
569
|
+
if (target !== 'option') {
|
|
570
|
+
throw new DeclarationError(sourceBinding, {
|
|
571
|
+
correction: 'Supply an extension that applies to options.',
|
|
572
|
+
findings: [keyFinding('binding')],
|
|
573
|
+
sentence: `${sentence} declares source binding ${quoted(bindingIdentity)}, which applies to ${appliesTo(target)}.`,
|
|
574
|
+
});
|
|
318
575
|
}
|
|
319
576
|
if (!isLoader(load)) {
|
|
320
|
-
throw new DeclarationError(
|
|
577
|
+
throw new DeclarationError(notAFunction, {
|
|
578
|
+
correction: "Supply load: () => import('./source.js').",
|
|
579
|
+
findings: [keyFinding('load')],
|
|
580
|
+
sentence: `${sentence} declares a source with no load function.`,
|
|
581
|
+
});
|
|
321
582
|
}
|
|
322
|
-
const carrier = own.inputs.find((input) => Object.hasOwn(own.build.records.get(input) ?? {},
|
|
583
|
+
const carrier = own.inputs.find((input) => Object.hasOwn(own.build.records.get(input) ?? {}, bindingIdentity));
|
|
323
584
|
if (carrier) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
585
|
+
// The record prints each option's captured config, which is what its rules read.
|
|
586
|
+
const option = pluginSites(identity, own.inputs)(carrier);
|
|
587
|
+
throw new DeclarationError(sourceBoundOwnOption, {
|
|
588
|
+
correction: "Remove the value; the source's own options resolve before it loads.",
|
|
589
|
+
findings: [partFinding(option, ['extensions'])],
|
|
590
|
+
sentence: `${option.subject} carries its own source binding.`,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return { binding: bindingIdentity, load };
|
|
327
594
|
}
|
|
328
595
|
/** A plugin's own list names the extensions it defines, before any declaration carries one. */
|
|
329
596
|
function defineExtensions(identity, declaration, build) {
|
|
330
597
|
const { extensions } = declaration;
|
|
598
|
+
const site = pluginSlot(identity, 'extensions', extensions);
|
|
331
599
|
if (extensions !== undefined && !Array.isArray(extensions)) {
|
|
332
|
-
throw new DeclarationError(
|
|
333
|
-
|
|
334
|
-
|
|
600
|
+
throw new DeclarationError(notAList, {
|
|
601
|
+
correction: 'Supply a list of extension descriptors.',
|
|
602
|
+
findings: [partFinding(site, [])],
|
|
603
|
+
sentence: `${pluginSentence(identity)} declares extensions that are not an array.`,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const list = Array.isArray(extensions) ? extensions : [];
|
|
607
|
+
for (const [index, descriptor] of list.entries()) {
|
|
335
608
|
if (!isDescriptor(descriptor)) {
|
|
336
|
-
throw new DeclarationError(
|
|
609
|
+
throw new DeclarationError(foreignValue, {
|
|
610
|
+
correction: 'Supply the value returned by extension(identity, config).',
|
|
611
|
+
findings: [partFinding(site, [index])],
|
|
612
|
+
sentence: `${pluginSentence(identity)} holds a value that is not an extension.`,
|
|
613
|
+
});
|
|
337
614
|
}
|
|
338
|
-
registerDescriptor(build.descriptors, descriptor
|
|
615
|
+
registerDescriptor(build.descriptors, descriptor, {
|
|
616
|
+
holder: pluginSentence(identity),
|
|
617
|
+
place: partFinding(site, [index]),
|
|
618
|
+
});
|
|
339
619
|
}
|
|
340
620
|
}
|
|
341
621
|
/**
|
|
342
|
-
* Every rule one definition carries on its own, in the order the definition's slots are read
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
622
|
+
* Every rule one definition carries on its own, in the order the definition's slots are read, each
|
|
623
|
+
* from the one copy `captureDefinition` takes once the identity is judged. The plugin's own
|
|
624
|
+
* extensions register before any declaration carries a value, so a duplicated package copy is
|
|
625
|
+
* reported from the list that defines it. The views list is read against core's view identities,
|
|
626
|
+
* and the Application reads the same copy again against every other contributor's.
|
|
346
627
|
*/
|
|
347
|
-
function readPlugin(
|
|
348
|
-
|
|
349
|
-
const declaration =
|
|
628
|
+
function readPlugin(named, definition) {
|
|
629
|
+
checkIdentity('plugin', named);
|
|
630
|
+
const declaration = captureDefinition(named, definition);
|
|
350
631
|
const theme = declaration.theme === undefined ? undefined : buildTheme(declaration.theme, named);
|
|
351
632
|
const build = { descriptors: new Map(), records: new Map() };
|
|
352
633
|
defineExtensions(named, declaration, build);
|
|
@@ -357,7 +638,9 @@ function readPlugin(identity, definition) {
|
|
|
357
638
|
const commands = readCommands(named, declaration.commands);
|
|
358
639
|
const middleware = readMiddleware(named, declaration.middleware, names);
|
|
359
640
|
const onCommandAttach = readHook(named, declaration.onCommandAttach);
|
|
360
|
-
|
|
641
|
+
const onFailure = readFailureHook(named, declaration.onFailure);
|
|
642
|
+
buildViews(pluginViews(named), declaration.views, viewIdentities(coreViews));
|
|
643
|
+
const translators = readTranslations(pluginSlot(named, 'translators', declaration.translators), declaration.translators);
|
|
361
644
|
return {
|
|
362
645
|
commands,
|
|
363
646
|
descriptors: build.descriptors,
|
|
@@ -365,10 +648,12 @@ function readPlugin(identity, definition) {
|
|
|
365
648
|
inputs,
|
|
366
649
|
middleware,
|
|
367
650
|
onCommandAttach,
|
|
651
|
+
onFailure,
|
|
368
652
|
records: build.records,
|
|
369
653
|
signals,
|
|
370
654
|
source,
|
|
371
655
|
theme,
|
|
656
|
+
translators,
|
|
372
657
|
views: declaration.views,
|
|
373
658
|
};
|
|
374
659
|
}
|
|
@@ -407,10 +692,10 @@ function pluginValues(inputs, values) {
|
|
|
407
692
|
/** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
|
|
408
693
|
function pluginSpellings(inputs, values) {
|
|
409
694
|
// Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
410
|
-
const
|
|
695
|
+
const supplied = inputs.flatMap(({ name }) => {
|
|
411
696
|
const spelling = values.spellings.get(name);
|
|
412
697
|
return spelling === undefined ? [] : [[name, spelling]];
|
|
413
698
|
});
|
|
414
|
-
return Object.freeze(Object.fromEntries(
|
|
699
|
+
return Object.freeze(Object.fromEntries(supplied));
|
|
415
700
|
}
|
|
416
|
-
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginValues, };
|
|
701
|
+
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, pluginValues, };
|