@octanejs/cli 0.1.0 → 0.2.1
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 +30 -6
- package/package.json +1 -1
- package/src/commands/analyze/fix.js +88 -0
- package/src/commands/analyze/index.js +291 -64
- package/src/commands/analyze/strong-coverage.js +21 -1
- package/src/commands/explain.js +73 -2
- package/src/commands/init/index.js +39 -8
- package/src/data/octane-data.json +494 -117
|
@@ -115,6 +115,26 @@ export async function loadStrongPolicy(project, { required }) {
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
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
|
+
|
|
118
138
|
/**
|
|
119
139
|
* An import of octane or one of its subpaths, capturing its clause. The clause
|
|
120
140
|
* may span lines but never reaches into the next `import`, so a semicolon-free
|
|
@@ -166,7 +186,7 @@ function isOctaneJsxSource(source) {
|
|
|
166
186
|
* @param {string} absolute
|
|
167
187
|
* @param {string} source
|
|
168
188
|
* @param {string | undefined} jsxImportSource from tsconfig
|
|
169
|
-
* @param {StrongPolicy} policy
|
|
189
|
+
* @param {Pick<StrongPolicy, 'jsxPragma'>} policy
|
|
170
190
|
*/
|
|
171
191
|
export function isOctaneModule(absolute, source, jsxImportSource, policy) {
|
|
172
192
|
if (absolute.endsWith('.d.ts')) return false;
|
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');
|
|
@@ -4,6 +4,7 @@ import { defineCommand } from '../../kernel/command.js';
|
|
|
4
4
|
import { setCompilerOption } from '../../kernel/edit.js';
|
|
5
5
|
import { CliError, EXIT } from '../../kernel/errors.js';
|
|
6
6
|
import { PACKAGE_MANAGERS, PNPM_ALLOWED_BUILDS, installPackages } from '../../kernel/install.js';
|
|
7
|
+
import { readInstalled } from '../../kernel/project.js';
|
|
7
8
|
import {
|
|
8
9
|
MODES,
|
|
9
10
|
PRETTIER_CONFIG_FILES,
|
|
@@ -425,10 +426,16 @@ export async function applyInit(ctx, project, options) {
|
|
|
425
426
|
const declared = project.declaredDependencies;
|
|
426
427
|
const dependencies = integration.dependencies.filter((name) => !declared[name]);
|
|
427
428
|
const devDependencies = integration.devDependencies.filter((name) => !declared[name]);
|
|
429
|
+
const needsParser = !declared['@tsrx/oxc'];
|
|
428
430
|
|
|
429
431
|
ctx.ui.intro(options.title);
|
|
430
432
|
|
|
431
|
-
if (
|
|
433
|
+
if (
|
|
434
|
+
changes.length === 0 &&
|
|
435
|
+
dependencies.length === 0 &&
|
|
436
|
+
devDependencies.length === 0 &&
|
|
437
|
+
!needsParser
|
|
438
|
+
) {
|
|
432
439
|
ctx.ui.outro('Already set up. Run `octane doctor` to confirm.');
|
|
433
440
|
return { json: { ok: true, mode, changes: [], installed: [] } };
|
|
434
441
|
}
|
|
@@ -439,10 +446,11 @@ export async function applyInit(ctx, project, options) {
|
|
|
439
446
|
? changes.map((c) => `${c.file} ${ctx.ui.colors.dim(c.summary)}`)
|
|
440
447
|
: ['nothing'],
|
|
441
448
|
);
|
|
442
|
-
if (dependencies.length + devDependencies.length > 0) {
|
|
449
|
+
if (dependencies.length + devDependencies.length > 0 || needsParser) {
|
|
443
450
|
ctx.ui.note('Will install', [
|
|
444
451
|
...dependencies,
|
|
445
452
|
...devDependencies.map((name) => `${name} (dev)`),
|
|
453
|
+
...(needsParser ? ['@tsrx/oxc (dev)'] : []),
|
|
446
454
|
]);
|
|
447
455
|
}
|
|
448
456
|
|
|
@@ -463,7 +471,10 @@ export async function applyInit(ctx, project, options) {
|
|
|
463
471
|
const warn = (/** @type {string[]} */ lines) => {
|
|
464
472
|
for (const line of lines) if (!manual.includes(line)) manual.push(line);
|
|
465
473
|
};
|
|
466
|
-
if (
|
|
474
|
+
if (
|
|
475
|
+
options.install !== false &&
|
|
476
|
+
(dependencies.length + devDependencies.length > 0 || needsParser)
|
|
477
|
+
) {
|
|
467
478
|
const spinner = ctx.ui.spinner(`Installing with ${installing.packageManager ?? 'npm'}`);
|
|
468
479
|
try {
|
|
469
480
|
for (const [names, dev] of [
|
|
@@ -479,6 +490,23 @@ export async function applyInit(ctx, project, options) {
|
|
|
479
490
|
installed.push(.../** @type {string[]} */ (names));
|
|
480
491
|
}
|
|
481
492
|
|
|
493
|
+
if (needsParser) {
|
|
494
|
+
const octane = readInstalled(project.root, 'octane');
|
|
495
|
+
const range =
|
|
496
|
+
octane?.manifest.peerDependencies?.['@tsrx/oxc'] ??
|
|
497
|
+
octane?.manifest.dependencies?.['@tsrx/oxc'];
|
|
498
|
+
if (typeof range !== 'string' || range === '' || range.startsWith('catalog:')) {
|
|
499
|
+
manual.push('Install @tsrx/oxc at the range declared by octane.');
|
|
500
|
+
} else {
|
|
501
|
+
const ran = await installPackages(ctx, installing, [`@tsrx/oxc@${range}`], {
|
|
502
|
+
dev: true,
|
|
503
|
+
allowBuilds: PNPM_ALLOWED_BUILDS,
|
|
504
|
+
});
|
|
505
|
+
warn(ran.warnings);
|
|
506
|
+
installed.push('@tsrx/oxc');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
482
510
|
// Now that the plugin is on disk it can say which TypeScript it runs
|
|
483
511
|
// under. A package manager that installs required peers has already
|
|
484
512
|
// put one there and this pins the same range explicitly; one that does
|
|
@@ -501,14 +529,17 @@ export async function applyInit(ctx, project, options) {
|
|
|
501
529
|
throw error;
|
|
502
530
|
}
|
|
503
531
|
spinner.stop(`Installed ${installed.length} package(s)`);
|
|
504
|
-
} else if (dependencies.length + devDependencies.length > 0) {
|
|
532
|
+
} else if (dependencies.length + devDependencies.length > 0 || needsParser) {
|
|
505
533
|
// The range lives in the plugin, which is not installed yet, so it cannot
|
|
506
534
|
// be quoted here. Naming the package without one would send people to the
|
|
507
535
|
// newest major, which `tsrx-tsc` cannot start under.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
536
|
+
if (dependencies.length + devDependencies.length > 0) {
|
|
537
|
+
manual.push(
|
|
538
|
+
`Install: ${[...dependencies, ...devDependencies].join(' ')}`,
|
|
539
|
+
'then typescript, at the range @tsrx/typescript-plugin declares as its peer.',
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (needsParser) manual.push('Install @tsrx/oxc at the range declared by octane.');
|
|
512
543
|
}
|
|
513
544
|
|
|
514
545
|
if (manual.length > 0) ctx.ui.note('Do this by hand', manual);
|