@octanejs/cli 0.1.0 → 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.
@@ -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');