@octanejs/cli 0.0.12 → 0.2.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/README.md +44 -5
- package/package.json +1 -1
- package/src/commands/analyze/fix.js +88 -0
- package/src/commands/analyze/index.js +685 -0
- package/src/commands/analyze/strong-coverage.js +324 -0
- package/src/commands/doctor/checks/config.js +1 -41
- package/src/commands/explain.js +73 -2
- package/src/data/octane-data.json +1745 -117
- package/src/kernel/octane-config.js +42 -0
- package/src/kernel/project.js +1 -1
- package/src/kernel/registry.js +1 -1
- package/src/commands/analyze.js +0 -283
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { CliError } from '../../kernel/errors.js';
|
|
6
|
+
import { resolveConfigLoader } from '../../kernel/octane-config.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The checked-in list of modules allowed to compile without Strong mode.
|
|
10
|
+
*
|
|
11
|
+
* Strong is opt-in per module or per application, so the cheapest way around
|
|
12
|
+
* a Strong diagnostic is to stop being Strong: delete `"use strong"`, turn
|
|
13
|
+
* `compiler.strong` off, or move the code into another package. Each of those
|
|
14
|
+
* is a one-line change that is easy to miss in review. With this file in the
|
|
15
|
+
* project, every module Octane compiles has to be Strong or be named here, and
|
|
16
|
+
* `octane analyze` only ever removes names. A new exception therefore shows up
|
|
17
|
+
* as an edit to this one file, which a team can route to an owner.
|
|
18
|
+
*/
|
|
19
|
+
export const BASELINE_FILE = 'octane-strong-baseline.json';
|
|
20
|
+
|
|
21
|
+
export const REGRESSION = 'OCTANE_STRONG_COVERAGE_REGRESSION';
|
|
22
|
+
export const STALE = 'OCTANE_STRONG_COVERAGE_STALE';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {{ strong: boolean, directive: boolean, config: boolean }} StrongStatus
|
|
26
|
+
*
|
|
27
|
+
* @typedef {Object} StrongPolicy
|
|
28
|
+
* @property {boolean} configured `compiler.strong` from octane.config
|
|
29
|
+
* @property {(source: string, absolute: string) => StrongStatus} status
|
|
30
|
+
* @property {(source: string) => string | null} jsxPragma the leading
|
|
31
|
+
* `@jsxImportSource` module, read by the compiler's own scanner
|
|
32
|
+
*
|
|
33
|
+
* @typedef {Object} CoverageModule
|
|
34
|
+
* @property {string} file project-relative, `/`-separated
|
|
35
|
+
* @property {string} absolute
|
|
36
|
+
* @property {StrongStatus} status
|
|
37
|
+
*
|
|
38
|
+
* @typedef {Object} Baseline
|
|
39
|
+
* @property {string} path
|
|
40
|
+
* @property {string} text
|
|
41
|
+
* @property {string[]} exceptions
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The project's Strong policy, decided by the project's own compiler.
|
|
46
|
+
*
|
|
47
|
+
* `compiler.strong` comes from evaluating octane.config, and the per-module
|
|
48
|
+
* answer comes from the installed bundler compiler's `strongModuleStatus`, the
|
|
49
|
+
* same decision its `transform` makes. Nothing here re-derives the directive
|
|
50
|
+
* prologue or the package-boundary rule.
|
|
51
|
+
*
|
|
52
|
+
* `required` is set when the answer is about to gate a run (a baseline exists
|
|
53
|
+
* or one is being written). A policy that cannot be determined then fails the
|
|
54
|
+
* run. Otherwise the policy only decides whether `compiler.strong` reaches a
|
|
55
|
+
* module, so a project without it needs nothing more (`{ policy: null }`), and
|
|
56
|
+
* one whose setting cannot be applied gets the `reason` to show beside the
|
|
57
|
+
* diagnostics it could still produce.
|
|
58
|
+
*
|
|
59
|
+
* @param {import('../../kernel/project.js').Project} project
|
|
60
|
+
* @param {{ required: boolean }} options
|
|
61
|
+
* @returns {Promise<{ policy: StrongPolicy | null, reason?: string }>}
|
|
62
|
+
*/
|
|
63
|
+
export async function loadStrongPolicy(project, { required }) {
|
|
64
|
+
/** @param {string} reason */
|
|
65
|
+
const unavailable = (reason) => {
|
|
66
|
+
if (required) throw new CliError(reason);
|
|
67
|
+
return { policy: null, reason };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let configured = false;
|
|
71
|
+
if (project.octaneConfigPath !== null) {
|
|
72
|
+
const file = path.basename(project.octaneConfigPath);
|
|
73
|
+
const load = await resolveConfigLoader(project.root);
|
|
74
|
+
if (!load) {
|
|
75
|
+
return unavailable(
|
|
76
|
+
`${file} could not be evaluated because @octanejs/app-core is not installed, so its compiler.strong setting is unknown.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
configured = (await load(project.root)).compiler?.strong === true;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return unavailable(
|
|
83
|
+
`${file} failed to load, so its compiler.strong setting is unknown: ${
|
|
84
|
+
error instanceof Error ? error.message : String(error)
|
|
85
|
+
}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!required && !configured) return { policy: null };
|
|
91
|
+
|
|
92
|
+
let bundler;
|
|
93
|
+
try {
|
|
94
|
+
const require = createRequire(path.join(project.root, 'noop.js'));
|
|
95
|
+
bundler = await import(pathToFileURL(require.resolve('octane/compiler/bundler')).href);
|
|
96
|
+
} catch {
|
|
97
|
+
return unavailable('Could not resolve `octane/compiler/bundler` from this project.');
|
|
98
|
+
}
|
|
99
|
+
const compiler = bundler.createOctaneCompiler?.({ root: project.root, strong: configured });
|
|
100
|
+
if (
|
|
101
|
+
typeof compiler?.strongModuleStatus !== 'function' ||
|
|
102
|
+
typeof bundler.findLeadingJsxImportSourcePragma !== 'function'
|
|
103
|
+
) {
|
|
104
|
+
return unavailable(
|
|
105
|
+
'The installed octane cannot report which modules compile under Strong mode. Update octane.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
policy: {
|
|
111
|
+
configured,
|
|
112
|
+
status: (source, absolute) => compiler.strongModuleStatus(source, absolute),
|
|
113
|
+
jsxPragma: bundler.findLeadingJsxImportSourcePragma,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The installed compiler's leading `@jsxImportSource` scanner, for deciding
|
|
120
|
+
* which `.tsx` modules are Octane JSX when no Strong policy was loaded. `null`
|
|
121
|
+
* when the installed octane predates it.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} root
|
|
124
|
+
* @returns {Promise<Pick<StrongPolicy, 'jsxPragma'> | null>}
|
|
125
|
+
*/
|
|
126
|
+
export async function loadJsxPragma(root) {
|
|
127
|
+
try {
|
|
128
|
+
const require = createRequire(path.join(root, 'noop.js'));
|
|
129
|
+
const bundler = await import(pathToFileURL(require.resolve('octane/compiler/bundler')).href);
|
|
130
|
+
return typeof bundler.findLeadingJsxImportSourcePragma === 'function'
|
|
131
|
+
? { jsxPragma: bundler.findLeadingJsxImportSourcePragma }
|
|
132
|
+
: null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An import of octane or one of its subpaths, capturing its clause. The clause
|
|
140
|
+
* may span lines but never reaches into the next `import`, so a semicolon-free
|
|
141
|
+
* module cannot borrow a later import's specifier.
|
|
142
|
+
*/
|
|
143
|
+
const OCTANE_IMPORT =
|
|
144
|
+
/^\s*import\s+((?:(?!\bimport\b)[^;])*?)\s*\bfrom\s*['"]octane(?:\/[^'"]*)?['"]/gm;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Does the module import a runtime binding from octane? TypeScript erases an
|
|
148
|
+
* `import type` clause and a named clause whose every specifier is
|
|
149
|
+
* `type`-marked, so neither leaves anything for the compiler to slot. A
|
|
150
|
+
* default or namespace binding, or any unmarked specifier, survives. Comments
|
|
151
|
+
* in the clause are not specifiers, so they are dropped before it is read.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} source
|
|
154
|
+
*/
|
|
155
|
+
function importsOctaneAtRuntime(source) {
|
|
156
|
+
for (const [, authored] of source.matchAll(OCTANE_IMPORT)) {
|
|
157
|
+
const clause = authored.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, ' ').trim();
|
|
158
|
+
if (/^type\b/.test(clause)) continue;
|
|
159
|
+
const named = /^\{([\s\S]*)\}$/.exec(clause);
|
|
160
|
+
if (named === null) return true;
|
|
161
|
+
const specifiers = named[1]
|
|
162
|
+
.split(',')
|
|
163
|
+
.map((specifier) => specifier.trim())
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
if (!specifiers.every((specifier) => /^type\s/.test(specifier))) return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string | undefined} source a JSX import source
|
|
172
|
+
*/
|
|
173
|
+
function isOctaneJsxSource(source) {
|
|
174
|
+
return source === 'octane' || source === 'octane/strong' || !!source?.startsWith('@octanejs/');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Is this a module Octane compiles, and so one Strong can reach?
|
|
179
|
+
*
|
|
180
|
+
* `.tsrx` always is. A `.tsx` module is unless its own leading pragma, or the
|
|
181
|
+
* tsconfig `jsxImportSource`, hands its JSX to another library, as a
|
|
182
|
+
* React-hosted project does. A plain `.ts`/`.js` module is when it imports
|
|
183
|
+
* octane at runtime, which is when the compiler slots its hooks. Declaration
|
|
184
|
+
* files and other extensions never are.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} absolute
|
|
187
|
+
* @param {string} source
|
|
188
|
+
* @param {string | undefined} jsxImportSource from tsconfig
|
|
189
|
+
* @param {Pick<StrongPolicy, 'jsxPragma'>} policy
|
|
190
|
+
*/
|
|
191
|
+
export function isOctaneModule(absolute, source, jsxImportSource, policy) {
|
|
192
|
+
if (absolute.endsWith('.d.ts')) return false;
|
|
193
|
+
const extension = path.extname(absolute);
|
|
194
|
+
if (extension === '.tsrx') return true;
|
|
195
|
+
if (extension === '.tsx') {
|
|
196
|
+
const pragma = policy.jsxPragma(source);
|
|
197
|
+
if (pragma !== null) return isOctaneJsxSource(pragma);
|
|
198
|
+
return jsxImportSource === undefined || isOctaneJsxSource(jsxImportSource);
|
|
199
|
+
}
|
|
200
|
+
if (extension === '.ts' || extension === '.js') return importsOctaneAtRuntime(source);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {string} root
|
|
206
|
+
* @param {string} absolute
|
|
207
|
+
*/
|
|
208
|
+
export function projectPath(root, absolute) {
|
|
209
|
+
return path.relative(root, absolute).split(path.sep).join('/');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} root
|
|
214
|
+
* @returns {Baseline | null}
|
|
215
|
+
*/
|
|
216
|
+
export function readBaseline(root) {
|
|
217
|
+
const file = path.join(root, BASELINE_FILE);
|
|
218
|
+
if (!existsSync(file)) return null;
|
|
219
|
+
const text = readFileSync(file, 'utf8');
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(text);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new CliError(
|
|
225
|
+
`${BASELINE_FILE} is not valid JSON: ${error instanceof Error ? error.message : error}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
parsed?.version !== 1 ||
|
|
230
|
+
!Array.isArray(parsed.exceptions) ||
|
|
231
|
+
parsed.exceptions.some((/** @type {unknown} */ entry) => typeof entry !== 'string')
|
|
232
|
+
) {
|
|
233
|
+
throw new CliError(
|
|
234
|
+
`${BASELINE_FILE} must be { "version": 1, "exceptions": [<project-relative paths>] }.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return { path: file, text, exceptions: parsed.exceptions };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Sorted and deduplicated, so the file only changes when its contents do.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} root
|
|
244
|
+
* @param {Iterable<string>} exceptions
|
|
245
|
+
*/
|
|
246
|
+
export function writeBaseline(root, exceptions) {
|
|
247
|
+
const body = { version: 1, exceptions: [...new Set(exceptions)].sort() };
|
|
248
|
+
writeFileSync(path.join(root, BASELINE_FILE), `${JSON.stringify(body, null, '\t')}\n`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Compare the measured modules with the recorded exceptions.
|
|
253
|
+
*
|
|
254
|
+
* A non-Strong module that is not listed is a regression: something new, or
|
|
255
|
+
* something that stopped being Strong. A listed module that is Strong, gone, or
|
|
256
|
+
* no longer compiled by Octane is stale, and is an error too, because a stale
|
|
257
|
+
* name is a pre-approved slot for the next regression. Staleness is only known
|
|
258
|
+
* when every module was measured.
|
|
259
|
+
*
|
|
260
|
+
* @param {{
|
|
261
|
+
* modules: CoverageModule[],
|
|
262
|
+
* baseline: Baseline,
|
|
263
|
+
* policy: StrongPolicy,
|
|
264
|
+
* complete: boolean,
|
|
265
|
+
* unmeasured: ReadonlySet<string>,
|
|
266
|
+
* display: (absolute: string) => string,
|
|
267
|
+
* }} input
|
|
268
|
+
*/
|
|
269
|
+
export function compareCoverage({ modules, baseline, policy, complete, unmeasured, display }) {
|
|
270
|
+
const listed = new Set(baseline.exceptions);
|
|
271
|
+
const measured = new Map(modules.map((module) => [module.file, module]));
|
|
272
|
+
const baselineDisplay = display(baseline.path);
|
|
273
|
+
|
|
274
|
+
/** @type {import('./index.js').Finding[]} */
|
|
275
|
+
const findings = [];
|
|
276
|
+
/** @type {string[]} */
|
|
277
|
+
const regressions = [];
|
|
278
|
+
for (const module of modules) {
|
|
279
|
+
if (module.status.strong || listed.has(module.file)) continue;
|
|
280
|
+
regressions.push(module.file);
|
|
281
|
+
const fix =
|
|
282
|
+
policy.configured && !module.status.config
|
|
283
|
+
? 'compiler.strong in octane.config does not reach modules of another package, so add "use strong" before its imports'
|
|
284
|
+
: 'Add "use strong" before its imports, or enable compiler.strong in octane.config,';
|
|
285
|
+
findings.push({
|
|
286
|
+
file: display(module.absolute),
|
|
287
|
+
line: 1,
|
|
288
|
+
column: 1,
|
|
289
|
+
severity: 'error',
|
|
290
|
+
code: REGRESSION,
|
|
291
|
+
message: `This module compiles without Strong mode and is not a recorded exception. ${fix} and fix what Strong then reports. A new exception is a reviewed edit to ${BASELINE_FILE}; octane analyze never adds one.`,
|
|
292
|
+
suggestions: [],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** @type {string[]} */
|
|
297
|
+
const stale = [];
|
|
298
|
+
if (complete) {
|
|
299
|
+
const lines = baseline.text.split('\n');
|
|
300
|
+
for (const entry of baseline.exceptions) {
|
|
301
|
+
const module = measured.get(entry);
|
|
302
|
+
if (unmeasured.has(entry) || (module !== undefined && !module.status.strong)) continue;
|
|
303
|
+
stale.push(entry);
|
|
304
|
+
const why =
|
|
305
|
+
module !== undefined
|
|
306
|
+
? 'now compiles with Strong mode'
|
|
307
|
+
: existsSync(path.join(path.dirname(baseline.path), entry))
|
|
308
|
+
? 'is not a module Octane compiles'
|
|
309
|
+
: 'no longer exists';
|
|
310
|
+
const index = lines.findIndex((line) => line.includes(JSON.stringify(entry)));
|
|
311
|
+
findings.push({
|
|
312
|
+
file: baselineDisplay,
|
|
313
|
+
line: index === -1 ? 1 : index + 1,
|
|
314
|
+
column: 1,
|
|
315
|
+
severity: 'error',
|
|
316
|
+
code: STALE,
|
|
317
|
+
message: `${entry} ${why}, so it is no longer an exception. Run \`octane analyze --strong-baseline update\` to remove it.`,
|
|
318
|
+
suggestions: [],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { findings, regressions, stale };
|
|
324
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
3
|
+
import { resolveConfigLoader } from '../../../kernel/octane-config.js';
|
|
5
4
|
import { readInstalled } from '../../../kernel/project.js';
|
|
6
5
|
import { fail, pass, skip, warn } from '../check.js';
|
|
7
6
|
|
|
@@ -10,45 +9,6 @@ const ADAPTERS = {
|
|
|
10
9
|
cloudflare: '@octanejs/adapter-cloudflare',
|
|
11
10
|
};
|
|
12
11
|
|
|
13
|
-
/**
|
|
14
|
-
* Load the project's own config loader rather than shipping a second one.
|
|
15
|
-
*
|
|
16
|
-
* `@octanejs/app-core` is usually a transitive dependency of the bundler
|
|
17
|
-
* plugin, not a direct one, so a plain resolve from the project root misses it.
|
|
18
|
-
* Falling back to a resolve rooted at the plugin reproduces exactly the lookup
|
|
19
|
-
* the plugin itself performs.
|
|
20
|
-
*
|
|
21
|
-
* @param {string} root
|
|
22
|
-
* @returns {Promise<((root: string) => Promise<any>) | null>}
|
|
23
|
-
*/
|
|
24
|
-
async function resolveConfigLoader(root) {
|
|
25
|
-
const fromProject = createRequire(path.join(root, 'noop.js'));
|
|
26
|
-
const specifier = '@octanejs/app-core/config-loader';
|
|
27
|
-
|
|
28
|
-
/** @type {string | null} */
|
|
29
|
-
let entry = null;
|
|
30
|
-
try {
|
|
31
|
-
entry = fromProject.resolve(specifier);
|
|
32
|
-
} catch {
|
|
33
|
-
for (const plugin of [
|
|
34
|
-
'@octanejs/vite-plugin',
|
|
35
|
-
'@octanejs/rspack-plugin',
|
|
36
|
-
'@octanejs/rsbuild-plugin',
|
|
37
|
-
]) {
|
|
38
|
-
try {
|
|
39
|
-
entry = createRequire(fromProject.resolve(plugin)).resolve(specifier);
|
|
40
|
-
break;
|
|
41
|
-
} catch {
|
|
42
|
-
// Try the next plugin.
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!entry) return null;
|
|
48
|
-
const module = await import(pathToFileURL(entry).href);
|
|
49
|
-
return module.loadOctaneConfig ?? null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
12
|
/**
|
|
53
13
|
* Loading the config means running esbuild over `octane.config.ts`. All three
|
|
54
14
|
* config checks need the same result, so it is computed once instead of three
|
package/src/commands/explain.js
CHANGED
|
@@ -28,6 +28,69 @@ export function parseErrorReference(input) {
|
|
|
28
28
|
return { code: code[1] ?? code[2], args };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Find a Strong compiler diagnostic in what the user pasted: the code itself,
|
|
33
|
+
* the code without its `OCTANE_STRONG_` prefix, a whole compile error, or its
|
|
34
|
+
* documentation link.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} input
|
|
37
|
+
* @returns {string | null} the full code, whether or not it is known
|
|
38
|
+
*/
|
|
39
|
+
export function parseStrongReference(input) {
|
|
40
|
+
const code = /\bOCTANE_[A-Z0-9_]+\b/i.exec(input);
|
|
41
|
+
if (code) return code[0].toUpperCase();
|
|
42
|
+
const anchor = /#(octane-[a-z0-9-]+)/i.exec(input);
|
|
43
|
+
if (anchor) return anchor[1].toUpperCase().replaceAll('-', '_');
|
|
44
|
+
const short = /^\s*([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\s*$/i.exec(input);
|
|
45
|
+
return short ? `OCTANE_STRONG_${short[1].toUpperCase()}` : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {import('../kernel/context.js').Ctx} ctx
|
|
50
|
+
* @param {string} code
|
|
51
|
+
*/
|
|
52
|
+
function explainStrong(ctx, code) {
|
|
53
|
+
const entry = DATA.strongDiagnostics?.diagnostics?.[code];
|
|
54
|
+
if (!entry) {
|
|
55
|
+
throw new CliError(`Unknown Strong diagnostic ${code}.`, {
|
|
56
|
+
hint: 'Upgrade @octanejs/cli if the code is newer, or see https://octanejs.dev/docs/strong-mode#diagnostic-reference',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** @typedef {{ id: string, title: string, react: string, strong: string, codes: string[] }} Recipe */
|
|
60
|
+
/** @type {Recipe[]} */
|
|
61
|
+
const recipes = (DATA.strongDiagnostics.recipes ?? []).filter((/** @type {Recipe} */ recipe) =>
|
|
62
|
+
recipe.codes.includes(code),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
ctx.ui.intro(code);
|
|
66
|
+
ctx.ui.log('');
|
|
67
|
+
ctx.ui.log(` ${entry.detects}`);
|
|
68
|
+
ctx.ui.log('');
|
|
69
|
+
ctx.ui.log(` ${ctx.ui.colors.bold('Replacement:')} ${entry.replacement}`);
|
|
70
|
+
for (const recipe of recipes) {
|
|
71
|
+
ctx.ui.log('');
|
|
72
|
+
ctx.ui.log(` ${ctx.ui.colors.bold(recipe.title)}`);
|
|
73
|
+
ctx.ui.log(` ${ctx.ui.colors.dim('React: ')} ${recipe.react}`);
|
|
74
|
+
ctx.ui.log(` ${ctx.ui.colors.dim('Strong:')} ${recipe.strong}`);
|
|
75
|
+
}
|
|
76
|
+
if (entry.severity === 'hint') {
|
|
77
|
+
ctx.ui.log('');
|
|
78
|
+
ctx.ui.log(` ${ctx.ui.colors.dim('severity: hint')}`);
|
|
79
|
+
}
|
|
80
|
+
ctx.ui.outro(entry.url);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
json: {
|
|
84
|
+
code,
|
|
85
|
+
severity: entry.severity,
|
|
86
|
+
detects: entry.detects,
|
|
87
|
+
replacement: entry.replacement,
|
|
88
|
+
recipes,
|
|
89
|
+
url: entry.url,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
31
94
|
/**
|
|
32
95
|
* @param {string} template
|
|
33
96
|
* @param {string[]} args
|
|
@@ -40,8 +103,9 @@ function fill(template, args) {
|
|
|
40
103
|
|
|
41
104
|
export default defineCommand({
|
|
42
105
|
description:
|
|
43
|
-
'Look up an Octane runtime error. Accepts a bare
|
|
44
|
-
'message a production build prints, arguments
|
|
106
|
+
'Look up an Octane runtime error or Strong compiler diagnostic. Accepts a bare\n' +
|
|
107
|
+
'code, the whole minified message a production build prints, arguments\n' +
|
|
108
|
+
'included, or a Strong code such as OCTANE_STRONG_RENDER_REF_READ.',
|
|
45
109
|
positionals: [
|
|
46
110
|
{ name: 'error', description: 'An error code, URL, or pasted message.', required: true },
|
|
47
111
|
],
|
|
@@ -50,6 +114,13 @@ export default defineCommand({
|
|
|
50
114
|
const raw = input.positionals.join(' ').trim();
|
|
51
115
|
if (!raw) throw usageError('Nothing to explain.', 'Try: octane explain 3');
|
|
52
116
|
|
|
117
|
+
// Runtime errors are numbered and Strong diagnostics are named, so a
|
|
118
|
+
// pasted message names at most one kind.
|
|
119
|
+
const strong = parseStrongReference(raw);
|
|
120
|
+
if (strong !== null && (/\bOCTANE_/i.test(raw) || parseErrorReference(raw) === null)) {
|
|
121
|
+
return explainStrong(ctx, strong);
|
|
122
|
+
}
|
|
123
|
+
|
|
53
124
|
const reference = parseErrorReference(raw);
|
|
54
125
|
if (!reference) {
|
|
55
126
|
throw usageError(`Could not find an error code in "${raw}".`, 'Try: octane explain 3');
|