@loomcli/core 0.4.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/LICENSE +21 -0
- package/dist/application.d.ts +60 -31
- package/dist/application.js +356 -149
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +64 -0
- package/dist/chain.d.ts +26 -12
- package/dist/chain.js +59 -92
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +212 -93
- package/dist/command.js +1224 -454
- 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 +115 -26
- package/dist/errors.js +333 -54
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +44 -9
- package/dist/extension.js +147 -65
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +108 -25
- package/dist/globals.d.ts +63 -22
- package/dist/globals.js +164 -39
- 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 +15 -4
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +36 -5
- package/dist/inspect.js +125 -27
- package/dist/lanes.js +1 -1
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +96 -2
- package/dist/options.js +259 -71
- package/dist/output.d.ts +11 -2
- package/dist/output.js +23 -3
- 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 +121 -55
- package/dist/plugin.js +496 -126
- 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 +58 -0
- package/dist/sources.js +258 -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 +76 -21
- package/dist/validation.d.ts +65 -10
- package/dist/validation.js +314 -108
- package/dist/view.d.ts +49 -15
- package/dist/view.js +157 -79
- package/package.json +3 -2
package/dist/plugin.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { checkEnvBinding, claimVariables } from './bindings.js';
|
|
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';
|
|
6
|
+
import { appliesTo, buildExtensions, isDescriptor, registerDescriptor } from './extension.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';
|
|
10
|
+
import { coreViews } from './lanes.js';
|
|
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';
|
|
4
15
|
import { isProcessSignal } from './signals.js';
|
|
5
16
|
import { buildTheme } from './theme.js';
|
|
17
|
+
import { readTranslations } from './translators.js';
|
|
6
18
|
import { captureConfig, checkDeclarations } from './validation.js';
|
|
19
|
+
import { buildViews, viewIdentities } from './view.js';
|
|
7
20
|
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
8
21
|
const nodes = new WeakMap();
|
|
9
22
|
/**
|
|
@@ -19,8 +32,10 @@ class PluginDeclaration {
|
|
|
19
32
|
}
|
|
20
33
|
/**
|
|
21
34
|
* One plugin: an identity and the contributions it carries. Creating and installing the value runs
|
|
22
|
-
* none of its code:
|
|
23
|
-
*
|
|
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
|
|
38
|
+
* that one definition carries on its own throws here, before the value exists.
|
|
24
39
|
*/
|
|
25
40
|
function plugin(identity, definition) {
|
|
26
41
|
const captured = {
|
|
@@ -29,78 +44,171 @@ function plugin(identity, definition) {
|
|
|
29
44
|
? {}
|
|
30
45
|
: { theme: isPlainObject(definition.theme) ? { ...definition.theme } : definition.theme }),
|
|
31
46
|
};
|
|
32
|
-
return new PluginDeclaration(
|
|
33
|
-
definition: isPlainObject(definition) ? captured : definition,
|
|
34
|
-
identity,
|
|
35
|
-
});
|
|
47
|
+
return new PluginDeclaration(readPlugin(identity, isPlainObject(definition) ? captured : definition));
|
|
36
48
|
}
|
|
37
49
|
/** How every plugin diagnostic names one plugin at the start of a sentence. */
|
|
38
50
|
function pluginSentence(identity) {
|
|
39
|
-
return `Plugin
|
|
51
|
+
return `Plugin ${quoted(identity)}`;
|
|
52
|
+
}
|
|
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
|
+
};
|
|
40
64
|
}
|
|
41
|
-
/**
|
|
65
|
+
/** The declarations behind one installed value, or `undefined` for a value `plugin()` did not make. */
|
|
42
66
|
function nodeOf(value) {
|
|
43
|
-
|
|
44
|
-
if (!node) {
|
|
45
|
-
throw new DeclarationError('The Application holds a value that is not a plugin. Supply the value returned by plugin(identity, definition).');
|
|
46
|
-
}
|
|
47
|
-
return node;
|
|
67
|
+
return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
|
|
48
68
|
}
|
|
49
|
-
/**
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
throw new DeclarationError('A plugin declares an identity that is not a string. Supply a nonempty string, such as the package name.');
|
|
54
|
-
}
|
|
55
|
-
if (identity === '') {
|
|
56
|
-
throw new DeclarationError('A plugin declares an empty identity. Supply a nonempty string, such as the package name.');
|
|
57
|
-
}
|
|
58
|
-
if (installed.has(identity)) {
|
|
59
|
-
throw new DeclarationError(`The Application installs plugin "${identity}" twice. Install each plugin once.`);
|
|
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
|
-
function definitionOf(identity,
|
|
65
|
-
const { definition } = node;
|
|
75
|
+
function definitionOf(identity, definition) {
|
|
66
76
|
if (!isPlainObject(definition)) {
|
|
67
|
-
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
|
+
});
|
|
68
82
|
}
|
|
69
83
|
return definition;
|
|
70
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
|
+
}
|
|
71
116
|
/**
|
|
72
|
-
* The installed list in composition order, with
|
|
73
|
-
*
|
|
74
|
-
*
|
|
117
|
+
* The installed list in composition order, with every rule that reads two plugins together: an
|
|
118
|
+
* identity installed twice, a second claim on the theme slot, the signals slot, or the
|
|
119
|
+
* configuration source, and two distinct descriptors under one identity. The slot is read
|
|
120
|
+
* defensively, because a JavaScript author reaches it with any value. Each plugin's own rules
|
|
121
|
+
* already ran at its `plugin()` call.
|
|
75
122
|
*/
|
|
76
|
-
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);
|
|
77
126
|
if (!Array.isArray(plugins)) {
|
|
78
|
-
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
|
+
});
|
|
79
132
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
for (const value of plugins) {
|
|
133
|
+
const list = plugins;
|
|
134
|
+
const installed = list.map((value, index) => {
|
|
83
135
|
const node = nodeOf(value);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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();
|
|
146
|
+
const descriptors = new Map();
|
|
147
|
+
// Each slot has one owner, so the first plugin to claim it names the second claimant's diagnostic.
|
|
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()) {
|
|
157
|
+
const { identity } = entry;
|
|
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
|
+
});
|
|
168
|
+
}
|
|
169
|
+
positions.set(identity, index);
|
|
170
|
+
if (entry.theme !== undefined) {
|
|
171
|
+
claim('theme', index);
|
|
172
|
+
}
|
|
173
|
+
for (const [key, descriptor] of entry.descriptors) {
|
|
174
|
+
registerDescriptor(descriptors, descriptor, {
|
|
175
|
+
identity: key,
|
|
176
|
+
place: partFinding(site, [index]),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// An empty claim leaves the signals slot free.
|
|
180
|
+
if (entry.signals.length > 0) {
|
|
181
|
+
claim('signals', index);
|
|
182
|
+
}
|
|
183
|
+
if (entry.source) {
|
|
184
|
+
claim('source', index);
|
|
185
|
+
}
|
|
87
186
|
}
|
|
88
|
-
return installed;
|
|
187
|
+
return { descriptors, plugins: installed };
|
|
89
188
|
}
|
|
90
189
|
/** The keys a plugin option may not declare, in the order its diagnostic names them. */
|
|
91
190
|
const forbidden = ['validate', 'validateOmitted', 'required'];
|
|
92
191
|
/** The rules a plugin option answers before every rule an ordinary declaration carries. */
|
|
93
|
-
function checkPluginOption(
|
|
192
|
+
function checkPluginOption(site, config) {
|
|
193
|
+
const sentence = site.subject;
|
|
94
194
|
if (!isPlainObject(config)) {
|
|
95
|
-
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
|
+
});
|
|
96
200
|
}
|
|
97
201
|
const rejected = forbidden.find((key) => key in config);
|
|
98
202
|
if (rejected !== undefined) {
|
|
99
|
-
throw
|
|
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
|
+
});
|
|
100
208
|
}
|
|
101
|
-
checkDescription(
|
|
102
|
-
checkHidden(
|
|
103
|
-
checkDeprecated(
|
|
209
|
+
checkDescription(site, config.description);
|
|
210
|
+
checkHidden(site, config.hidden);
|
|
211
|
+
checkDeprecated(site, config.deprecated);
|
|
104
212
|
}
|
|
105
213
|
/**
|
|
106
214
|
* One plugin's option declarations, in declaration order. They join the globals table, so the rules
|
|
@@ -108,18 +216,25 @@ function checkPluginOption(sentence, config) {
|
|
|
108
216
|
*/
|
|
109
217
|
function readOptions(identity, declared, build) {
|
|
110
218
|
if (declared !== undefined && !isPlainObject(declared)) {
|
|
111
|
-
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
|
+
});
|
|
112
224
|
}
|
|
113
225
|
const inputs = [];
|
|
114
226
|
for (const [name, config] of Object.entries(declared ?? {})) {
|
|
115
|
-
const sentence = `${pluginSentence(identity)} option
|
|
116
|
-
|
|
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);
|
|
117
231
|
const input = { config: captureConfig(config), kind: 'option', name };
|
|
118
232
|
// The shared rules name the plugin and the option, so a fault reads with its contributor.
|
|
119
|
-
checkDeclarations([input], sentence);
|
|
120
|
-
build.
|
|
233
|
+
checkDeclarations([{ input, site }], sentence);
|
|
234
|
+
build.records.set(input, buildExtensions({
|
|
121
235
|
declared: config.extensions,
|
|
122
236
|
descriptors: build.descriptors,
|
|
237
|
+
site: { ...site, at: `${site.at}.extensions` },
|
|
123
238
|
subject: {
|
|
124
239
|
phrase: `on ${sentence.slice(0, 1).toLowerCase()}${sentence.slice(1)}`,
|
|
125
240
|
sentence,
|
|
@@ -128,28 +243,50 @@ function readOptions(identity, declared, build) {
|
|
|
128
243
|
}));
|
|
129
244
|
inputs.push(input);
|
|
130
245
|
}
|
|
246
|
+
// Two options of one plugin meet in the one table the pre-scan reads, so they share its rules.
|
|
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
|
+
})));
|
|
131
253
|
return inputs;
|
|
132
254
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function readActivation(identity, declared, names) {
|
|
142
|
-
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') {
|
|
143
263
|
return 'always';
|
|
144
264
|
}
|
|
145
|
-
if (!Array.isArray(
|
|
146
|
-
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
|
+
});
|
|
147
271
|
}
|
|
148
|
-
const list =
|
|
272
|
+
const list = activate;
|
|
149
273
|
if (list.length === 0) {
|
|
150
|
-
throw new DeclarationError(
|
|
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
|
+
});
|
|
151
279
|
}
|
|
152
|
-
return list.map((name) =>
|
|
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
|
+
});
|
|
153
290
|
}
|
|
154
291
|
/**
|
|
155
292
|
* The loader a middleware declares. Core calls it with no arguments and reads whatever it resolves
|
|
@@ -158,21 +295,110 @@ function readActivation(identity, declared, names) {
|
|
|
158
295
|
function isLoader(value) {
|
|
159
296
|
return typeof value === 'function';
|
|
160
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* The default export one plugin loader resolves to, checked by the guard its caller supplies. A
|
|
300
|
+
* loader that throws where it is called and one that rejects later are one failure, and a module
|
|
301
|
+
* without the export names the kind of function it owed, such as `middleware` or `source`.
|
|
302
|
+
*/
|
|
303
|
+
async function loadDefault(identity, load, owed) {
|
|
304
|
+
let module = undefined;
|
|
305
|
+
try {
|
|
306
|
+
module = await load();
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
throw new InternalError(pluginLoaderFailed, {
|
|
310
|
+
cause: error,
|
|
311
|
+
correction: loaderCorrection,
|
|
312
|
+
sentence: `Loading plugin ${quoted(identity)} failed: ${reasonOf(error)}`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const exported = module !== null && typeof module === 'object' && 'default' in module
|
|
316
|
+
? module.default
|
|
317
|
+
: undefined;
|
|
318
|
+
if (!owed.guard(exported)) {
|
|
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
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return exported;
|
|
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').";
|
|
161
329
|
/** One plugin's middleware, or `undefined` for a plugin that declares none. */
|
|
162
330
|
function readMiddleware(identity, declared, names) {
|
|
163
331
|
if (declared === undefined) {
|
|
164
332
|
return undefined;
|
|
165
333
|
}
|
|
334
|
+
const site = pluginSlot(identity, 'middleware', declared);
|
|
166
335
|
if (!isPlainObject(declared)) {
|
|
167
|
-
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
|
+
});
|
|
168
341
|
}
|
|
169
|
-
const activate = readActivation(
|
|
342
|
+
const activate = readActivation(site, declared, names);
|
|
170
343
|
const { load } = declared;
|
|
171
344
|
if (!isLoader(load)) {
|
|
172
|
-
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
|
+
});
|
|
173
350
|
}
|
|
174
351
|
return { activate, load };
|
|
175
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* The Commands one plugin attaches to the root, each checked by the attach the root applies, as a
|
|
355
|
+
* finished Command, against the plugin's own earlier Commands, and against the nesting cap. The
|
|
356
|
+
* Application attaches them again when it is constructed, against every other root child.
|
|
357
|
+
*/
|
|
358
|
+
function readCommands(identity, declared) {
|
|
359
|
+
if (declared === undefined) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
if (!Array.isArray(declared)) {
|
|
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
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const list = declared;
|
|
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
|
+
});
|
|
376
|
+
// A for...of walk reads a hole as undefined, which the entry rule rejects, where map would skip it.
|
|
377
|
+
for (const [index, value] of list.entries()) {
|
|
378
|
+
const node = commandNode(value);
|
|
379
|
+
const placement = {
|
|
380
|
+
arguments: [identity, { commands: entries }],
|
|
381
|
+
call: 'plugin',
|
|
382
|
+
mark: `1.commands.${String(index)}`,
|
|
383
|
+
};
|
|
384
|
+
if (!node) {
|
|
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
|
+
});
|
|
390
|
+
}
|
|
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));
|
|
399
|
+
}
|
|
400
|
+
return commands;
|
|
401
|
+
}
|
|
176
402
|
/**
|
|
177
403
|
* One plugin's claim on the signals slot, drawn from the closed set core installs listeners for.
|
|
178
404
|
* An empty list claims nothing, so it leaves the slot free for another plugin.
|
|
@@ -183,21 +409,39 @@ function readSignals(identity, declared) {
|
|
|
183
409
|
if (declared === undefined) {
|
|
184
410
|
return [];
|
|
185
411
|
}
|
|
412
|
+
const site = pluginSlot(identity, 'signals', declared);
|
|
186
413
|
if (!Array.isArray(declared)) {
|
|
187
|
-
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
|
+
});
|
|
188
419
|
}
|
|
189
420
|
const list = declared;
|
|
190
|
-
|
|
191
|
-
|
|
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()) {
|
|
192
424
|
if (!isProcessSignal(value)) {
|
|
193
|
-
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
|
+
});
|
|
194
430
|
}
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
});
|
|
197
441
|
}
|
|
198
|
-
claimed.
|
|
442
|
+
claimed.set(value, index);
|
|
199
443
|
}
|
|
200
|
-
return [...claimed];
|
|
444
|
+
return [...claimed.keys()];
|
|
201
445
|
}
|
|
202
446
|
/**
|
|
203
447
|
* A lifecycle hook is a function core calls at one named point, so being callable is the whole
|
|
@@ -212,67 +456,193 @@ function readHook(identity, declared) {
|
|
|
212
456
|
return undefined;
|
|
213
457
|
}
|
|
214
458
|
if (!isHook(declared)) {
|
|
215
|
-
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
|
+
});
|
|
216
464
|
}
|
|
217
465
|
return declared;
|
|
218
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
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return declared;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* The configuration source one plugin declares, or `undefined` for a plugin that declares none.
|
|
487
|
+
* The binding is one of the plugin's own option-target extensions, so core knows which options to
|
|
488
|
+
* ask about without knowing what the binding means. The plugin's own options resolve before the
|
|
489
|
+
* source loads, so none of them may carry its binding.
|
|
490
|
+
*/
|
|
491
|
+
function readSource(identity, declaration, own) {
|
|
492
|
+
const declared = declaration.source;
|
|
493
|
+
if (declared === undefined) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const sentence = pluginSentence(identity);
|
|
497
|
+
const site = pluginSlot(identity, 'source', declared);
|
|
498
|
+
if (!isPlainObject(declared)) {
|
|
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
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const { binding, load } = declared;
|
|
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
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (!isLoader(load)) {
|
|
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
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const carrier = own.inputs.find((input) => Object.hasOwn(own.build.records.get(input) ?? {}, bindingIdentity));
|
|
534
|
+
if (carrier) {
|
|
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 };
|
|
543
|
+
}
|
|
219
544
|
/** A plugin's own list names the extensions it defines, before any declaration carries one. */
|
|
220
545
|
function defineExtensions(identity, declaration, build) {
|
|
221
546
|
const { extensions } = declaration;
|
|
547
|
+
const site = pluginSlot(identity, 'extensions', extensions);
|
|
222
548
|
if (extensions !== undefined && !Array.isArray(extensions)) {
|
|
223
|
-
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
|
+
});
|
|
224
554
|
}
|
|
225
|
-
for (const descriptor of extensions ?? []) {
|
|
555
|
+
for (const [index, descriptor] of (extensions ?? []).entries()) {
|
|
226
556
|
if (!isDescriptor(descriptor)) {
|
|
227
|
-
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
|
+
});
|
|
228
562
|
}
|
|
229
|
-
registerDescriptor(build.descriptors, descriptor
|
|
563
|
+
registerDescriptor(build.descriptors, descriptor, {
|
|
564
|
+
holder: pluginSentence(identity),
|
|
565
|
+
place: partFinding(site, [index]),
|
|
566
|
+
});
|
|
230
567
|
}
|
|
231
568
|
}
|
|
232
569
|
/**
|
|
233
|
-
* Every
|
|
234
|
-
* before any declaration carries a value, so a duplicated package
|
|
235
|
-
* that
|
|
570
|
+
* Every rule one definition carries on its own, in the order the definition's slots are read. The
|
|
571
|
+
* plugin's own extensions register before any declaration carries a value, so a duplicated package
|
|
572
|
+
* copy is reported from the list that defines it. The views list is read against core's view
|
|
573
|
+
* identities, and the Application reads it again against every other contributor's.
|
|
236
574
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
signals,
|
|
269
|
-
theme,
|
|
270
|
-
views: declaration.views,
|
|
271
|
-
};
|
|
272
|
-
});
|
|
575
|
+
function readPlugin(named, definition) {
|
|
576
|
+
checkIdentity('plugin', named);
|
|
577
|
+
const declaration = definitionOf(named, definition);
|
|
578
|
+
const theme = declaration.theme === undefined ? undefined : buildTheme(declaration.theme, named);
|
|
579
|
+
const build = { descriptors: new Map(), records: new Map() };
|
|
580
|
+
defineExtensions(named, declaration, build);
|
|
581
|
+
const inputs = readOptions(named, declaration.options, build);
|
|
582
|
+
const names = new Set(inputs.map((input) => input.name));
|
|
583
|
+
const signals = readSignals(named, declaration.signals);
|
|
584
|
+
const source = readSource(named, declaration, { build, inputs });
|
|
585
|
+
const commands = readCommands(named, declaration.commands);
|
|
586
|
+
const middleware = readMiddleware(named, declaration.middleware, names);
|
|
587
|
+
const onCommandAttach = readHook(named, declaration.onCommandAttach);
|
|
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);
|
|
591
|
+
return {
|
|
592
|
+
commands,
|
|
593
|
+
descriptors: build.descriptors,
|
|
594
|
+
identity: named,
|
|
595
|
+
inputs,
|
|
596
|
+
middleware,
|
|
597
|
+
onCommandAttach,
|
|
598
|
+
onFailure,
|
|
599
|
+
records: build.records,
|
|
600
|
+
signals,
|
|
601
|
+
source,
|
|
602
|
+
theme,
|
|
603
|
+
translators,
|
|
604
|
+
views: declaration.views,
|
|
605
|
+
};
|
|
273
606
|
}
|
|
274
607
|
/** The signals the one slot owner claimed, or none when no installed plugin claims the slot. */
|
|
275
608
|
function ownedSignals(plugins) {
|
|
276
609
|
return plugins.find((entry) => entry.signals.length > 0)?.signals ?? [];
|
|
277
610
|
}
|
|
278
|
-
|
|
611
|
+
/** A collected value, or the declared array default, as this run's own copy. */
|
|
612
|
+
function collectedValue(collected, declared) {
|
|
613
|
+
if (collected) {
|
|
614
|
+
return [...collected];
|
|
615
|
+
}
|
|
616
|
+
return Array.isArray(declared) ? [...declared] : [];
|
|
617
|
+
}
|
|
618
|
+
/** One plugin option's value for one run: what a tier supplied, or the declared default. */
|
|
619
|
+
function pluginValue({ config, name }, values) {
|
|
620
|
+
const declared = config.default;
|
|
621
|
+
if (config.type === 'boolean') {
|
|
622
|
+
return booleanValue(values, name, config);
|
|
623
|
+
}
|
|
624
|
+
if (config.multiple === true) {
|
|
625
|
+
return collectedValue(values.lists.get(name), declared);
|
|
626
|
+
}
|
|
627
|
+
// Build already proved that a string option without a validator declares a string default.
|
|
628
|
+
return values.strings.get(name) ?? (typeof declared === 'string' ? declared : undefined);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* One plugin's own option values for one run: what argv or an input source supplied, or the
|
|
632
|
+
* declared default, filled without validation. A collected value and an array default are copied,
|
|
633
|
+
* so a plugin that writes to what it received changes neither the declaration nor the next run.
|
|
634
|
+
* Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
635
|
+
*/
|
|
636
|
+
function pluginValues(inputs, values) {
|
|
637
|
+
return Object.fromEntries(inputs.map((input) => [input.name, pluginValue(input, values)]));
|
|
638
|
+
}
|
|
639
|
+
/** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
|
|
640
|
+
function pluginSpellings(inputs, values) {
|
|
641
|
+
// Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
642
|
+
const supplied = inputs.flatMap(({ name }) => {
|
|
643
|
+
const spelling = values.spellings.get(name);
|
|
644
|
+
return spelling === undefined ? [] : [[name, spelling]];
|
|
645
|
+
});
|
|
646
|
+
return Object.freeze(Object.fromEntries(supplied));
|
|
647
|
+
}
|
|
648
|
+
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, pluginValues, };
|