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