@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/hints.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { developerPlainText, developerText } from './developer.js';
|
|
2
|
+
import { genericDefectText, InternalError, isAuthorFault, reasonOf } from './errors.js';
|
|
3
|
+
import { nodeAt } from './inspect.js';
|
|
4
|
+
import { reportPlainly } from './output.js';
|
|
5
|
+
import { pluginSentence } from './plugin.js';
|
|
6
|
+
import { brokenDestination, brokenFailureHook, brokenFailureView, viewCorrection, } from './rules.js';
|
|
7
|
+
import { ignoreRejection, isThenable } from './thenable.js';
|
|
8
|
+
import { describeFailure } from './view.js';
|
|
9
|
+
/** The generic defect message the first time a run writes it, and nothing after that. */
|
|
10
|
+
function genericOnce(build, application) {
|
|
11
|
+
if (build.generic) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
build.generic = true;
|
|
15
|
+
return genericDefectText(application);
|
|
16
|
+
}
|
|
17
|
+
/** The string one index of a returned list holds, read once, or `undefined` for a hole or a non-string. */
|
|
18
|
+
function heldString(listed, index) {
|
|
19
|
+
if (!Object.hasOwn(listed, index)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const held = listed[index];
|
|
23
|
+
return typeof held === 'string' ? held : undefined;
|
|
24
|
+
}
|
|
25
|
+
/** The first `length` strings a list holds, copied into a frozen array, or `undefined` at a gap. */
|
|
26
|
+
function copiedHints(listed, length) {
|
|
27
|
+
const hints = [];
|
|
28
|
+
for (let index = 0; index < length; index += 1) {
|
|
29
|
+
const hint = heldString(listed, index);
|
|
30
|
+
if (hint === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
hints.push(hint);
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze(hints);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Every hint a returned list holds, copied by index into a fresh frozen array, or `undefined` for a
|
|
39
|
+
* value that is not a list of strings alone. The copy calls no method on the returned value, so an
|
|
40
|
+
* array subclass or a proxy cannot answer a list other than the one it holds. A hole is not a hint.
|
|
41
|
+
* The length is read once, so a getter cannot grow the list while it is copied.
|
|
42
|
+
*/
|
|
43
|
+
function listedHints(returned) {
|
|
44
|
+
if (!Array.isArray(returned)) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const listed = returned;
|
|
48
|
+
// A proxy answers its own length, so a length no array can hold is not a list.
|
|
49
|
+
const { length } = listed;
|
|
50
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return copiedHints(listed, length);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The hints one returned value holds. A hook is synchronous, so a returned promise is a broken
|
|
57
|
+
* answer: it receives a rejection handler and is otherwise ignored, as a failure view's is.
|
|
58
|
+
*/
|
|
59
|
+
function readHints(returned) {
|
|
60
|
+
if (returned === undefined) {
|
|
61
|
+
return { hints: [], kind: 'hints' };
|
|
62
|
+
}
|
|
63
|
+
if (typeof returned === 'string') {
|
|
64
|
+
return { hints: [returned], kind: 'hints' };
|
|
65
|
+
}
|
|
66
|
+
if (isThenable(returned)) {
|
|
67
|
+
ignoreRejection(returned);
|
|
68
|
+
return {
|
|
69
|
+
cause: undefined,
|
|
70
|
+
kind: 'broken',
|
|
71
|
+
reason: 'The hook returned a promise instead of hints.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const hints = listedHints(returned);
|
|
75
|
+
return hints === undefined
|
|
76
|
+
? {
|
|
77
|
+
cause: undefined,
|
|
78
|
+
kind: 'broken',
|
|
79
|
+
reason: 'The hook returned a value that is not a string or an array of strings.',
|
|
80
|
+
}
|
|
81
|
+
: { hints, kind: 'hints' };
|
|
82
|
+
}
|
|
83
|
+
/** One hook's call, with a throw read as a broken answer. */
|
|
84
|
+
function callHook(hook, failure, context) {
|
|
85
|
+
try {
|
|
86
|
+
return readHints(hook(failure, context));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return { cause: error, kind: 'broken', reason: reasonOf(error) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The context every hook for one failure shares. `graph` and `command` stay getters, so a run
|
|
94
|
+
* whose hooks read neither builds no graph for them.
|
|
95
|
+
*/
|
|
96
|
+
function hookContext(scene, style) {
|
|
97
|
+
const { application, built, path } = scene;
|
|
98
|
+
let command = undefined;
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
application,
|
|
101
|
+
get command() {
|
|
102
|
+
command ??= nodeAt(built.inspected(), path);
|
|
103
|
+
return command;
|
|
104
|
+
},
|
|
105
|
+
get graph() {
|
|
106
|
+
return built.inspected();
|
|
107
|
+
},
|
|
108
|
+
path,
|
|
109
|
+
style,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Every installed plugin's hints for one failure, in installation order, and each hook that broke.
|
|
114
|
+
* Each hook receives the failure and its context alone, so hints accumulate and no hook sees
|
|
115
|
+
* another's.
|
|
116
|
+
*/
|
|
117
|
+
function collectHints(scene, failure, style) {
|
|
118
|
+
const { built } = scene;
|
|
119
|
+
if (built === undefined) {
|
|
120
|
+
return { broken: [], hints: Object.freeze([]) };
|
|
121
|
+
}
|
|
122
|
+
const context = hookContext({ ...scene, built }, style);
|
|
123
|
+
// Every hook runs here, in installation order, before any answer is read.
|
|
124
|
+
const answers = built.plugins.flatMap(({ identity, onFailure }) => onFailure === undefined ? [] : [{ answer: callHook(onFailure, failure, context), identity }]);
|
|
125
|
+
const hints = answers.flatMap(({ answer }) => (answer.kind === 'hints' ? answer.hints : []));
|
|
126
|
+
const broken = answers.flatMap(({ answer, identity }) => answer.kind === 'broken' ? [{ cause: answer.cause, identity, reason: answer.reason }] : []);
|
|
127
|
+
return { broken, hints: Object.freeze(hints) };
|
|
128
|
+
}
|
|
129
|
+
/** The defect one broken hook reports in a development build. */
|
|
130
|
+
function hookDefect({ cause, identity, reason }) {
|
|
131
|
+
return new InternalError(brokenFailureHook, {
|
|
132
|
+
cause,
|
|
133
|
+
correction: 'Return a string, an array of strings, or undefined from onFailure, synchronously.',
|
|
134
|
+
sentence: `${pluginSentence(identity)} failed in onFailure: ${reason}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** The defect a broken failure view reports in a development build. */
|
|
138
|
+
function viewDefect(report) {
|
|
139
|
+
return new InternalError(brokenFailureView, {
|
|
140
|
+
cause: report.cause,
|
|
141
|
+
correction: viewCorrection,
|
|
142
|
+
sentence: `Rendering the failure failed: ${report.reason}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* What one broken contract reports on the plain fallback path. A development build writes its
|
|
147
|
+
* Developer Diagnostic after a blank line; a distributed build writes the generic defect message
|
|
148
|
+
* at most once per run.
|
|
149
|
+
*/
|
|
150
|
+
function brokenContract(defect, build, scene) {
|
|
151
|
+
return build.development
|
|
152
|
+
? `\n${developerPlainText(defect, scene)}`
|
|
153
|
+
: genericOnce(build, scene.application);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* What the plain fallback path writes after one failure's diagnostic: core's default text when the
|
|
157
|
+
* view broke, then what each broken contract reports, the view first and each hook in installation
|
|
158
|
+
* order. Nothing here resolves markup or runs a plugin's code.
|
|
159
|
+
*/
|
|
160
|
+
function plainLines(report, broken, reporting) {
|
|
161
|
+
const { build, failure, scene } = reporting;
|
|
162
|
+
if (report.kind === 'rendered') {
|
|
163
|
+
return broken.map((hook) => brokenContract(hookDefect(hook), build, scene)).join('');
|
|
164
|
+
}
|
|
165
|
+
// `report.text` is core's default text, which already ends in `\n`; for a defect it is generic.
|
|
166
|
+
const own = isAuthorFault(failure) ? genericOnce(build, scene.application) : report.text;
|
|
167
|
+
return [viewDefect(report), ...broken.map(hookDefect)].reduce((text, defect) => `${text}${brokenContract(defect, build, scene)}`, own);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Renders one failure as the run's build decides. A development build renders a fault only the
|
|
171
|
+
* author can fix as its Developer Diagnostic, ahead of every override, with the hints under it,
|
|
172
|
+
* and after an earlier report of the run it opens with one blank line. Every other failure, and
|
|
173
|
+
* every failure in a distributed build, resolves through the view registry. Core's default text
|
|
174
|
+
* for a defect is the generic message, which the run writes at most once, so a later defect that
|
|
175
|
+
* core's own view renders writes nothing.
|
|
176
|
+
*/
|
|
177
|
+
async function renderFailure(sink, failure, scene) {
|
|
178
|
+
const { build } = sink;
|
|
179
|
+
if (build.development && isAuthorFault(failure)) {
|
|
180
|
+
const { style } = sink.output.context('stderr');
|
|
181
|
+
const text = developerText(failure, { ...scene.developer, hints: scene.hints, style });
|
|
182
|
+
await sink.output.report(build.reported ? `\n${text}` : text);
|
|
183
|
+
return { core: false, kind: 'rendered', text: '' };
|
|
184
|
+
}
|
|
185
|
+
const report = describeFailure(sink.registry, failure, failureContext(sink.output, scene));
|
|
186
|
+
if (report.kind === 'rendered' && !repeatsGeneric(build, report, failure)) {
|
|
187
|
+
// The view owns the trailing newline; output resolves its marked text.
|
|
188
|
+
await sink.output.report(report.text);
|
|
189
|
+
}
|
|
190
|
+
return report;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Whether a rendered report is the generic defect message a run wrote already, which it writes
|
|
194
|
+
* nothing for. Core's own view writes the generic message for a fault only the author can fix, and
|
|
195
|
+
* the first such report counts it.
|
|
196
|
+
*/
|
|
197
|
+
function repeatsGeneric(build, report, failure) {
|
|
198
|
+
if (report.kind !== 'rendered' || !report.core || !isAuthorFault(failure)) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const repeated = build.generic;
|
|
202
|
+
build.generic = true;
|
|
203
|
+
return repeated;
|
|
204
|
+
}
|
|
205
|
+
/** The context a failure view reads: the stderr view context, where the run was, and the hints. */
|
|
206
|
+
function failureContext(output, scene) {
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
...output.context('stderr'),
|
|
209
|
+
application: scene.developer.application,
|
|
210
|
+
hints: scene.hints,
|
|
211
|
+
path: scene.path,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Reports one failure. The hooks run first, so the diagnostic or the view receives their hints. A
|
|
216
|
+
* view that breaks leaves core's default text without hints on the plain fallback path, and each
|
|
217
|
+
* broken contract's report follows that whole diagnostic. It answers whether a view or a hook
|
|
218
|
+
* broke, which forces the run's code to 1 outside a cancelled run.
|
|
219
|
+
*/
|
|
220
|
+
async function reportFailure(sink, failure, scene) {
|
|
221
|
+
const { broken, hints } = collectHints(scene, failure, sink.output.context('stderr').style);
|
|
222
|
+
const developer = { application: scene.application, host: scene.host };
|
|
223
|
+
const report = await renderFailure(sink, failure, { developer, hints, path: scene.path });
|
|
224
|
+
const plain = plainLines(report, broken, { build: sink.build, failure, scene: developer });
|
|
225
|
+
if (plain !== '') {
|
|
226
|
+
await reportPlainly(sink.stderr, plain);
|
|
227
|
+
}
|
|
228
|
+
sink.build.reported = true;
|
|
229
|
+
return report.kind !== 'rendered' || broken.length > 0;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* What a run writes on the plain fallback path when a destination failed a write or reporting
|
|
233
|
+
* itself failed: the Developer Diagnostic of the broken destination in a development build, and
|
|
234
|
+
* the generic defect message, at most once per run, in a distributed one.
|
|
235
|
+
*/
|
|
236
|
+
function destinationReport(build, cause, scene) {
|
|
237
|
+
if (!build.development) {
|
|
238
|
+
return genericOnce(build, scene.application);
|
|
239
|
+
}
|
|
240
|
+
const defect = new InternalError(brokenDestination, {
|
|
241
|
+
cause,
|
|
242
|
+
correction: 'Give run() host streams that accept every write until the run resolves.',
|
|
243
|
+
sentence: 'Could not write invocation output.',
|
|
244
|
+
});
|
|
245
|
+
return `${build.reported ? '\n' : ''}${developerPlainText(defect, scene)}`;
|
|
246
|
+
}
|
|
247
|
+
export { destinationReport, reportFailure };
|
package/dist/host.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
2
|
import type { Host, RunOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The reader process capture supplies: one UTF-8 file, read synchronously, whose path lies under
|
|
5
|
+
* the working directory once both resolve through symbolic links. A stack can be forged, so a file
|
|
6
|
+
* outside `cwd`, a link that leaves it, a path that is not a regular file, such as a FIFO or a
|
|
7
|
+
* device, a file larger than 1 MiB, and any failure answer `undefined`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readSourceFile(path: string, cwd: string): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The roots a defect's frame may lie under: the working directory as the host names it, and the
|
|
12
|
+
* path it resolves to through symbolic links, because a runtime names a module by its resolved
|
|
13
|
+
* path. A directory that cannot be resolved is its own one root.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sourceRoots(cwd: string): readonly string[];
|
|
3
16
|
export declare function captureHost(overrides: RunOptions['host'], stderr: Writable): Host;
|
package/dist/host.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
1
3
|
// Process stream declarations assume a terminal, but pipes omit isTTY at runtime.
|
|
2
4
|
function isTerminal(stream) {
|
|
3
5
|
return stream.isTTY === true;
|
|
@@ -5,13 +7,53 @@ function isTerminal(stream) {
|
|
|
5
7
|
function dimension(value) {
|
|
6
8
|
return value === 0 ? undefined : value;
|
|
7
9
|
}
|
|
10
|
+
/** The largest source file the captured reader reads, so a frame cannot make it read without end. */
|
|
11
|
+
const sourceLimit = 1_048_576;
|
|
12
|
+
/**
|
|
13
|
+
* The reader process capture supplies: one UTF-8 file, read synchronously, whose path lies under
|
|
14
|
+
* the working directory once both resolve through symbolic links. A stack can be forged, so a file
|
|
15
|
+
* outside `cwd`, a link that leaves it, a path that is not a regular file, such as a FIFO or a
|
|
16
|
+
* device, a file larger than 1 MiB, and any failure answer `undefined`.
|
|
17
|
+
*/
|
|
18
|
+
export function readSourceFile(path, cwd) {
|
|
19
|
+
try {
|
|
20
|
+
const file = realpathSync(path);
|
|
21
|
+
const within = relative(realpathSync(cwd), file);
|
|
22
|
+
if (within === '' || within === '..' || within.startsWith(`..${sep}`) || isAbsolute(within)) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const stats = statSync(file);
|
|
26
|
+
if (!stats.isFile() || stats.size > sourceLimit) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return readFileSync(file, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The roots a defect's frame may lie under: the working directory as the host names it, and the
|
|
37
|
+
* path it resolves to through symbolic links, because a runtime names a module by its resolved
|
|
38
|
+
* path. A directory that cannot be resolved is its own one root.
|
|
39
|
+
*/
|
|
40
|
+
export function sourceRoots(cwd) {
|
|
41
|
+
try {
|
|
42
|
+
const resolved = realpathSync(cwd);
|
|
43
|
+
return resolved === cwd ? [cwd] : [cwd, resolved];
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [cwd];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
8
49
|
export function captureHost(overrides, stderr) {
|
|
9
|
-
const { argv, cwd, env, platform, stdin, stdout, terminal } = overrides ?? {};
|
|
50
|
+
const { argv, cwd, env, platform, readSource, stdin, stdout, terminal } = overrides ?? {};
|
|
10
51
|
return {
|
|
11
52
|
argv: [...(argv ?? process.argv.slice(2))],
|
|
12
53
|
cwd: cwd ?? process.cwd(),
|
|
13
54
|
env: { ...(env ?? process.env) },
|
|
14
55
|
platform: platform ?? process.platform,
|
|
56
|
+
readSource: readSource ?? readSourceFile,
|
|
15
57
|
stderr,
|
|
16
58
|
stdin: stdin ?? process.stdin,
|
|
17
59
|
stdout: stdout ?? process.stdout,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Finding } from './diagnostic-text.js';
|
|
2
|
+
import { DeclarationError } from './errors.js';
|
|
3
|
+
/** The calls that declare an identity. */
|
|
4
|
+
type IdentityCall = 'extension' | 'plugin' | 'view';
|
|
5
|
+
/** Whether a value is a plugin, extension, or view identity. */
|
|
6
|
+
declare function isIdentity(value: unknown): value is string;
|
|
7
|
+
/** Whether a value is a diagnostic rule identity: an identity and a kebab-case rule name. */
|
|
8
|
+
declare function isRuleIdentity(value: unknown): value is string;
|
|
9
|
+
/**
|
|
10
|
+
* The fault for one identity outside the grammar. `subject` opens the sentence and says who
|
|
11
|
+
* declares or holds it, such as `A plugin declares the identity`.
|
|
12
|
+
*/
|
|
13
|
+
declare function identityFault(subject: string, identity: unknown, findings: readonly Finding[]): DeclarationError;
|
|
14
|
+
/**
|
|
15
|
+
* Checks the identity one `plugin()`, `extension()`, or `view()` call declares, at the call, so a
|
|
16
|
+
* fault throws before the value exists. The finding marks the identity on the rebuilt call.
|
|
17
|
+
*/
|
|
18
|
+
declare function checkIdentity(call: IdentityCall, identity: unknown): asserts identity is string;
|
|
19
|
+
export { checkIdentity, identityFault, isIdentity, isRuleIdentity };
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { elided, spelled } from './diagnostic-text.js';
|
|
2
|
+
import { DeclarationError, quoted } from './errors.js';
|
|
3
|
+
import { invalidIdentity } from './plugin-rules.js';
|
|
4
|
+
/*
|
|
5
|
+
* The identity grammar plugins, extensions, views, and diagnostic rules share. Both grammars are
|
|
6
|
+
* built here from one source, so every rule identity's prefix is a valid plugin identity.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A package name as npm spells one, scoped or not: `help` or `@acme/config`. The group captures it,
|
|
10
|
+
* scope included, so its length is checked against npm's limit.
|
|
11
|
+
*/
|
|
12
|
+
const packageName = String.raw `(?<package>(?:@[a-z0-9-][a-z0-9._-]*\/)?[a-z0-9-][a-z0-9._-]*)`;
|
|
13
|
+
/** The longest package name npm accepts, scope included. */
|
|
14
|
+
const packageNameLimit = 214;
|
|
15
|
+
/** One segment after a `/`, of lowercase letters and digits in words joined by single hyphens. */
|
|
16
|
+
const kebabSegment = String.raw `\/[a-z0-9]+(?:-[a-z0-9]+)*`;
|
|
17
|
+
/**
|
|
18
|
+
* A package name, then zero or more kebab-case subpath segments: `@acme/config` or
|
|
19
|
+
* `@loomcli/plugins/help/page`. It is the grammar of a plugin, extension, or view identity.
|
|
20
|
+
*/
|
|
21
|
+
const identityGrammar = new RegExp(`^${packageName}(?:${kebabSegment})*$`, 'u');
|
|
22
|
+
/**
|
|
23
|
+
* An identity, then a mandatory kebab-case rule name: `@loomcli/core/spelling-taken` or
|
|
24
|
+
* `@loomcli/plugins/manifest/failure-name-conflict`. It is the grammar of a diagnostic rule's
|
|
25
|
+
* identity and of a validator package's issue codes.
|
|
26
|
+
*/
|
|
27
|
+
const ruleGrammar = new RegExp(`^${packageName}(?:${kebabSegment})+$`, 'u');
|
|
28
|
+
/** How a sentence names the declarer of each call. */
|
|
29
|
+
const declarers = {
|
|
30
|
+
extension: 'An extension',
|
|
31
|
+
plugin: 'A plugin',
|
|
32
|
+
view: 'A view',
|
|
33
|
+
};
|
|
34
|
+
/** Whether a value is a string one grammar matches, with a package name within npm's limit. */
|
|
35
|
+
function matches(grammar, value) {
|
|
36
|
+
if (typeof value !== 'string') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const name = grammar.exec(value)?.groups?.package;
|
|
40
|
+
return name !== undefined && name.length <= packageNameLimit;
|
|
41
|
+
}
|
|
42
|
+
/** Whether a value is a plugin, extension, or view identity. */
|
|
43
|
+
function isIdentity(value) {
|
|
44
|
+
return matches(identityGrammar, value);
|
|
45
|
+
}
|
|
46
|
+
/** Whether a value is a diagnostic rule identity: an identity and a kebab-case rule name. */
|
|
47
|
+
function isRuleIdentity(value) {
|
|
48
|
+
return matches(ruleGrammar, value);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The fault for one identity outside the grammar. `subject` opens the sentence and says who
|
|
52
|
+
* declares or holds it, such as `A plugin declares the identity`.
|
|
53
|
+
*/
|
|
54
|
+
function identityFault(subject, identity, findings) {
|
|
55
|
+
return new DeclarationError(invalidIdentity, {
|
|
56
|
+
correction: 'Name it <package>[/<subpath>...], such as "@acme/notes" or "@acme/notes/page".',
|
|
57
|
+
findings,
|
|
58
|
+
sentence: `${subject} ${quoted(identity)}, which is not a package name with optional kebab-case subpath segments.`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks the identity one `plugin()`, `extension()`, or `view()` call declares, at the call, so a
|
|
63
|
+
* fault throws before the value exists. The finding marks the identity on the rebuilt call.
|
|
64
|
+
*/
|
|
65
|
+
function checkIdentity(call, identity) {
|
|
66
|
+
if (!isIdentity(identity)) {
|
|
67
|
+
throw identityFault(`${declarers[call]} declares the identity`, identity, [
|
|
68
|
+
{ arguments: [identity, spelled(elided)], call, mark: '0' },
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export { checkIdentity, identityFault, isIdentity, isRuleIdentity };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
export { Application } from './application.js';
|
|
2
2
|
export { Command } from './command.js';
|
|
3
|
+
export { escapeControlCharacters } from './controls.js';
|
|
3
4
|
export { validationContext, validationContextKey } from './context.js';
|
|
5
|
+
export { diagnosticRule } from './diagnostic.js';
|
|
6
|
+
export { isRuleIdentity } from './identity.js';
|
|
7
|
+
export type { DiagnosticParts, DiagnosticRule, Finding } from './diagnostic-text.js';
|
|
4
8
|
export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ResultError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, } from './errors.js';
|
|
9
|
+
export { EX_CANTCREAT, EX_CONFIG, EX_DATAERR, EX_IOERR, EX_NOHOST, EX_NOINPUT, EX_NOPERM, EX_NOUSER, EX_OSERR, EX_OSFILE, EX_PROTOCOL, EX_SOFTWARE, EX_TEMPFAIL, EX_UNAVAILABLE, EX_USAGE, } from './exit-codes.js';
|
|
10
|
+
export type { FailureExitCode } from './exit-codes.js';
|
|
5
11
|
export { extension, readExtension } from './extension.js';
|
|
12
|
+
export { locate } from './locate.js';
|
|
6
13
|
export { incompleteResult, lanes } from './lanes.js';
|
|
7
14
|
export { override, view } from './view.js';
|
|
8
15
|
export { plugin } from './plugin.js';
|
|
16
|
+
export { translate } from './translators.js';
|
|
9
17
|
export { glyph } from './glyphs.generated.js';
|
|
10
18
|
export type { RenderingPolicy } from './rendering.js';
|
|
11
19
|
export type { ViewContext } from './types.js';
|
|
12
20
|
export { pad, style } from './style.js';
|
|
13
21
|
export { issuePath } from './validation.js';
|
|
14
22
|
export type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
|
|
15
|
-
export type { ApplicationMethod, ApplicationOptions } from './application.js';
|
|
23
|
+
export type { ApplicationMethod, ApplicationOptions, Packet } from './application.js';
|
|
16
24
|
export type { ChainOutcome, MiddlewareContext } from './chain.js';
|
|
17
25
|
export type { InputProblem, ResultFault } from './errors.js';
|
|
18
26
|
export type { AnyExtension, Extension, ExtensionValue } from './extension.js';
|
|
27
|
+
export type { FailureHook, FailureHookContext } from './hints.js';
|
|
19
28
|
export type { CommandMethod, CommandOptions } from './command.js';
|
|
20
29
|
export type { CancellationReason } from './signals.js';
|
|
30
|
+
export type { ErrorClass, Translation, Translator } from './translators.js';
|
|
21
31
|
export type { IncompleteResult } from './lanes.js';
|
|
22
|
-
export type {
|
|
32
|
+
export type { WordPosition } from './locate.js';
|
|
33
|
+
export type { AnyDeclaredView, DeclaredRowView, DeclaredView, DeclaredViewBrand, FailureClass, FailureView, FailureViewContext, ViewContribution, ViewOverride, } from './view.js';
|
|
23
34
|
export type { ArgumentNode, CommandGraph, CommandNode, OptionNode, ResultNode } from './inspect.js';
|
|
24
|
-
export type { Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionValues, } from './plugin.js';
|
|
35
|
+
export type { Middleware, OptionsOf, Plugin, PluginDefinition, PluginOptions, PluginOptionSpellings, PluginOptionValues, SourceAnswer, SourceContext, SourceResolver, } from './plugin.js';
|
|
25
36
|
export type { Action, ActionArgs, ActionContext, ActionHandler, ActionOptions, ArgumentConfig, AttachedCommand, BooleanOption, CommandAttachHook, ExitCode, Host, InputIdentity, InputTerminal, Out, OptionConfig, OutputTerminal, Request, ResultInput, ResultView, ResultViews, RunOptions, ScalarArgument, StringOption, SuppliedInputs, RowView, RowViews, ValidationContext, VariadicArgument, View, } from './types.js';
|
|
26
37
|
export type { ApplicationEnvironment, EnvironmentOf, Register, RegisteredEnvironment, } from './environment.js';
|
|
27
|
-
export type { Ansi16Color, Ansi256Fallbacks, ColorFallbacks, ConcreteStyle, Style, ThemeMapping, ThemeConstraint, } from './style.js';
|
|
38
|
+
export type { Ansi16Color, Ansi256Fallbacks, ColorFallbacks, ConcreteStyle, ContextualStyle, Style, ThemeMapping, ThemeConstraint, } from './style.js';
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
export { Application } from './application.js';
|
|
2
2
|
export { Command } from './command.js';
|
|
3
|
+
export { escapeControlCharacters } from './controls.js';
|
|
3
4
|
export { validationContext, validationContextKey } from './context.js';
|
|
5
|
+
export { diagnosticRule } from './diagnostic.js';
|
|
6
|
+
export { isRuleIdentity } from './identity.js';
|
|
4
7
|
export { DeclarationError, FatalError, InputError, InternalError, LoomError, MissingValueError, NonCallableCommandError, RepeatedOptionError, ResultError, ShortGroupError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, UsageError, } from './errors.js';
|
|
8
|
+
export { EX_CANTCREAT, EX_CONFIG, EX_DATAERR, EX_IOERR, EX_NOHOST, EX_NOINPUT, EX_NOPERM, EX_NOUSER, EX_OSERR, EX_OSFILE, EX_PROTOCOL, EX_SOFTWARE, EX_TEMPFAIL, EX_UNAVAILABLE, EX_USAGE, } from './exit-codes.js';
|
|
5
9
|
export { extension, readExtension } from './extension.js';
|
|
10
|
+
export { locate } from './locate.js';
|
|
6
11
|
export { incompleteResult, lanes } from './lanes.js';
|
|
7
12
|
export { override, view } from './view.js';
|
|
8
13
|
export { plugin } from './plugin.js';
|
|
14
|
+
export { translate } from './translators.js';
|
|
9
15
|
export { glyph } from './glyphs.generated.js';
|
|
10
16
|
export { pad, style } from './style.js';
|
|
11
17
|
export { issuePath } from './validation.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** An option declared with a type other than string or Boolean. */
|
|
2
|
+
declare const optionType: import("./diagnostic-text.js").DiagnosticRule;
|
|
3
|
+
/** A short alias that is not one ASCII letter. */
|
|
4
|
+
declare const shortAlias: import("./diagnostic-text.js").DiagnosticRule;
|
|
5
|
+
/**
|
|
6
|
+
* A yes-or-no declaration key, such as `required`, `hidden`, or an extension descriptor's
|
|
7
|
+
* `collect`, that holds a value other than a Boolean.
|
|
8
|
+
*/
|
|
9
|
+
declare const flagNotBoolean: import("./diagnostic-text.js").DiagnosticRule;
|
|
10
|
+
/** `shortOnly` on an option that declares no short alias. */
|
|
11
|
+
declare const shortOnlyWithoutShort: import("./diagnostic-text.js").DiagnosticRule;
|
|
12
|
+
/** `multiple` on a Boolean option. */
|
|
13
|
+
declare const booleanOptionMultiple: import("./diagnostic-text.js").DiagnosticRule;
|
|
14
|
+
/** `polarity` on a string option. */
|
|
15
|
+
declare const polarityOnString: import("./diagnostic-text.js").DiagnosticRule;
|
|
16
|
+
/** A polarity outside the three settings. */
|
|
17
|
+
declare const optionPolarity: import("./diagnostic-text.js").DiagnosticRule;
|
|
18
|
+
/** `polarity: 'both'` beside `shortOnly`. */
|
|
19
|
+
declare const shortOnlyBothPolarities: import("./diagnostic-text.js").DiagnosticRule;
|
|
20
|
+
/**
|
|
21
|
+
* One spelling that two options in one scope claim, the application's, a plugin's, or one a
|
|
22
|
+
* plugin's hook declared.
|
|
23
|
+
*/
|
|
24
|
+
declare const spellingTaken: import("./diagnostic-text.js").DiagnosticRule;
|
|
25
|
+
/**
|
|
26
|
+
* Two options with one declared name in one scope, the application's, a plugin's, or one a
|
|
27
|
+
* plugin's hook declared.
|
|
28
|
+
*/
|
|
29
|
+
declare const optionDeclaredTwice: import("./diagnostic-text.js").DiagnosticRule;
|
|
30
|
+
/**
|
|
31
|
+
* An input a plugin's `onCommandAttach` hook declares under a name that an input of the other kind
|
|
32
|
+
* already holds in the Command's scope: an argument under an option's name, or an option under an
|
|
33
|
+
* argument's name.
|
|
34
|
+
*/
|
|
35
|
+
declare const nameSharedAcrossKinds: import("./diagnostic-text.js").DiagnosticRule;
|
|
36
|
+
/** `required` or `validateOmitted` on a global option. */
|
|
37
|
+
declare const globalPresenceRule: import("./diagnostic-text.js").DiagnosticRule;
|
|
38
|
+
/** `globalOption()` after the application's own `command()` or `action()`. */
|
|
39
|
+
declare const globalOptionAfterCommand: import("./diagnostic-text.js").DiagnosticRule;
|
|
40
|
+
/** An environment binding on a multiple option. */
|
|
41
|
+
declare const envOnMultiple: import("./diagnostic-text.js").DiagnosticRule;
|
|
42
|
+
/** An environment binding whose name is outside the variable name grammar. */
|
|
43
|
+
declare const envName: import("./diagnostic-text.js").DiagnosticRule;
|
|
44
|
+
/** An environment binding on an argument. */
|
|
45
|
+
declare const envOnArgument: import("./diagnostic-text.js").DiagnosticRule;
|
|
46
|
+
/** One variable that two options in one invocation's scope bind. */
|
|
47
|
+
declare const variableBoundTwice: import("./diagnostic-text.js").DiagnosticRule;
|
|
48
|
+
/** `validateOmitted` on a declaration whose absence another rule already decides. */
|
|
49
|
+
declare const omissionAlreadyDecided: import("./diagnostic-text.js").DiagnosticRule;
|
|
50
|
+
/** `validateOmitted` on a declaration with no validator. */
|
|
51
|
+
declare const omissionWithoutValidator: import("./diagnostic-text.js").DiagnosticRule;
|
|
52
|
+
/** `validate`, `default`, `required`, or `validateOmitted` on a Boolean option. */
|
|
53
|
+
declare const booleanOptionValueRule: import("./diagnostic-text.js").DiagnosticRule;
|
|
54
|
+
/** A required input that also declares a default. */
|
|
55
|
+
declare const requiredWithDefault: import("./diagnostic-text.js").DiagnosticRule;
|
|
56
|
+
/** A `validate` value that is not a Standard Schema v1 object. */
|
|
57
|
+
declare const notAValidator: import("./diagnostic-text.js").DiagnosticRule;
|
|
58
|
+
/** A default of the wrong raw shape for its declaration. */
|
|
59
|
+
declare const defaultShape: import("./diagnostic-text.js").DiagnosticRule;
|
|
60
|
+
/** A declared default its validator rejected. */
|
|
61
|
+
declare const invalidDefault: import("./diagnostic-text.js").DiagnosticRule;
|
|
62
|
+
/** A validator's JSON Schema converter that threw or returned a value that is not a plain object. */
|
|
63
|
+
declare const schemaConverterFailed: import("./diagnostic-text.js").DiagnosticRule;
|
|
64
|
+
export { booleanOptionMultiple, booleanOptionValueRule, defaultShape, envName, envOnArgument, envOnMultiple, flagNotBoolean, globalOptionAfterCommand, globalPresenceRule, invalidDefault, notAValidator, omissionAlreadyDecided, nameSharedAcrossKinds, omissionWithoutValidator, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, requiredWithDefault, schemaConverterFailed, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, variableBoundTwice, };
|