@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.
@@ -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;
@@ -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 code, or the whole minified\n' +
44
- 'message a production build prints, arguments included.',
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 (changes.length === 0 && dependencies.length === 0 && devDependencies.length === 0) {
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 (options.install !== false && dependencies.length + devDependencies.length > 0) {
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
- manual.push(
509
- `Install: ${[...dependencies, ...devDependencies].join(' ')}`,
510
- 'then typescript, at the range @tsrx/typescript-plugin declares as its peer.',
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);