@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
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ pnpm dlx @octanejs/cli doctor
|
|
|
25
25
|
| `octane analyze` | Compile the project and report every Octane compiler diagnostic, with its code and suggested edit. |
|
|
26
26
|
| `octane add <package>` | Install a binding, by its own name or by the React package it ports, and print its divergences. |
|
|
27
27
|
| `octane bindings [query]` | List and search the `@octanejs/*` bindings. |
|
|
28
|
-
| `octane explain <error>` | Decode a runtime error code, including the minified production message
|
|
28
|
+
| `octane explain <error>` | Decode a runtime error code, including the minified production message, or explain a Strong diagnostic such as `OCTANE_STRONG_RENDER_REF_READ`. |
|
|
29
29
|
| `octane info` | Environment and project details worth pasting into a bug report. |
|
|
30
30
|
| `octane mcp add` | Register the Octane MCP server with Claude Code, Codex, Cursor, or VS Code. |
|
|
31
31
|
|
|
@@ -66,15 +66,19 @@ Anything else is reported with the exact remedy rather than guessed at.
|
|
|
66
66
|
## `octane analyze`
|
|
67
67
|
|
|
68
68
|
Where `doctor` checks how the project is wired, `analyze` checks the code. It
|
|
69
|
-
compiles every `.tsrx
|
|
70
|
-
compiler found, so the results are
|
|
71
|
-
new compiler diagnostics show up here
|
|
69
|
+
compiles every `.tsrx`, and every `.tsx` whose JSX goes to Octane, through the
|
|
70
|
+
project's own `octane` and reports what the compiler found, so the results are
|
|
71
|
+
exactly what a build would warn about, and new compiler diagnostics show up here
|
|
72
|
+
without a CLI change. Every Strong violation in a file is reported, not only the
|
|
73
|
+
first one the build stops at.
|
|
72
74
|
|
|
73
75
|
```bash
|
|
74
|
-
octane analyze # every
|
|
76
|
+
octane analyze # every Octane module in the project
|
|
75
77
|
octane analyze src/App.tsrx # just these
|
|
76
78
|
octane analyze --code OCTANE_HYDRATE_SPLIT_STYLE
|
|
77
79
|
octane analyze --strict # warnings fail the run too
|
|
80
|
+
octane analyze --strong-preview # what Strong mode would reject, by code
|
|
81
|
+
octane analyze --fix # apply the compiler's suggested edits
|
|
78
82
|
```
|
|
79
83
|
|
|
80
84
|
```
|
|
@@ -88,6 +92,41 @@ A file that will not parse is reported as an error and does not stop the rest of
|
|
|
88
92
|
the run. Exit code is `3` when anything error-severity was found, or when
|
|
89
93
|
`--strict` and there were warnings.
|
|
90
94
|
|
|
95
|
+
Modules that `compiler.strong` in `octane.config.ts` reaches are analyzed in
|
|
96
|
+
Strong mode, as the build compiles them. A `.tsx` module whose leading
|
|
97
|
+
`@jsxImportSource` pragma, or the tsconfig's `jsxImportSource`, names another
|
|
98
|
+
library is left out unless you name it.
|
|
99
|
+
|
|
100
|
+
### Migrating to Strong mode
|
|
101
|
+
|
|
102
|
+
`--strong-preview` compiles every module as if Strong mode were on, reports what
|
|
103
|
+
it would reject, and ends with a count per code. Findings in modules that are not
|
|
104
|
+
Strong yet do not fail the run, so it works as an inventory before you opt in.
|
|
105
|
+
|
|
106
|
+
`--fix` applies the edits the compiler suggests and then reports what remains:
|
|
107
|
+
React's lazy ref initialization (`if (ref.current === null) ref.current = …`)
|
|
108
|
+
becomes `useLazyRef`, `useMemo(() => value, deps)` becomes `value`, and
|
|
109
|
+
`useCallback(fn, deps)` becomes `fn`. With `--dry-run` it reports the fixes
|
|
110
|
+
without writing them. Under `--json`, each finding with a fix carries its
|
|
111
|
+
`edits` as `{ start, end, text }` offsets into the file.
|
|
112
|
+
|
|
113
|
+
`octane explain <CODE>` prints what a Strong diagnostic detects, its
|
|
114
|
+
replacement, and the migration recipes for it. It also accepts a pasted compile
|
|
115
|
+
error or its docs link. Every code is documented at
|
|
116
|
+
[octanejs.dev/docs/strong-mode](https://octanejs.dev/docs/strong-mode#diagnostic-reference).
|
|
117
|
+
|
|
118
|
+
### Strong coverage
|
|
119
|
+
|
|
120
|
+
`octane analyze --strong-baseline init` records every module that compiles
|
|
121
|
+
without Strong mode in `octane-strong-baseline.json`. While that file exists,
|
|
122
|
+
every run fails on a module that is neither Strong nor listed
|
|
123
|
+
(`OCTANE_STRONG_COVERAGE_REGRESSION`), such as one whose `"use strong"` was
|
|
124
|
+
deleted, and on a listed name that is now Strong or gone
|
|
125
|
+
(`OCTANE_STRONG_COVERAGE_STALE`). `--strong-baseline update` removes stale
|
|
126
|
+
names and never adds one, so a new exception is always a reviewed edit to the
|
|
127
|
+
file. See
|
|
128
|
+
[Keeping modules Strong](https://github.com/octanejs/octane/blob/main/docs/strong-compiler-checks.md#keeping-modules-strong).
|
|
129
|
+
|
|
91
130
|
## For agents and CI
|
|
92
131
|
|
|
93
132
|
Every command is fully drivable by flags and emits a single JSON document under
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply the compiler's structured suggestions to a module's source.
|
|
3
|
+
*
|
|
4
|
+
* A suggestion's edits are one unit: the lazy-ref rewrite adds an import,
|
|
5
|
+
* replaces the `useRef` call, and deletes the `if`, and applying only part of
|
|
6
|
+
* that would leave the module broken. A suggestion is skipped whole when any
|
|
7
|
+
* of its edits overlaps an edit already accepted; running `--fix` again picks
|
|
8
|
+
* it up from the rewritten source. An edit identical to an accepted one, such
|
|
9
|
+
* as two rewrites adding the same import, is applied once.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {{ start: number, end: number, text: string }} Edit
|
|
12
|
+
* @typedef {{ code: string, edits: Edit[] }} Fix
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {Edit} a
|
|
17
|
+
* @param {Edit} b
|
|
18
|
+
*/
|
|
19
|
+
function overlaps(a, b) {
|
|
20
|
+
if (a.start === a.end && b.start === b.end) return a.start === b.start;
|
|
21
|
+
return a.start < b.end && b.start < a.end;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} source
|
|
26
|
+
* @param {readonly Fix[]} fixes
|
|
27
|
+
* @returns {{ text: string, applied: Fix[] }}
|
|
28
|
+
*/
|
|
29
|
+
export function applyFixes(source, fixes) {
|
|
30
|
+
/** @type {Edit[]} */
|
|
31
|
+
const accepted = [];
|
|
32
|
+
/** @type {Fix[]} */
|
|
33
|
+
const applied = [];
|
|
34
|
+
for (const fix of fixes) {
|
|
35
|
+
const fresh = fix.edits.filter(
|
|
36
|
+
(edit) =>
|
|
37
|
+
!accepted.some(
|
|
38
|
+
(other) =>
|
|
39
|
+
other.start === edit.start && other.end === edit.end && other.text === edit.text,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
if (fresh.some((edit) => accepted.some((other) => overlaps(edit, other)))) continue;
|
|
43
|
+
accepted.push(...fresh);
|
|
44
|
+
applied.push(fix);
|
|
45
|
+
}
|
|
46
|
+
let text = source;
|
|
47
|
+
for (const edit of accepted.sort((a, b) => b.start - a.start)) {
|
|
48
|
+
text = text.slice(0, edit.start) + edit.text + text.slice(edit.end);
|
|
49
|
+
}
|
|
50
|
+
return { text, applied };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The hooks a fix can replace, whose imports it may leave unused. */
|
|
54
|
+
const REPLACED_HOOKS = new Set(['useCallback', 'useMemo', 'useRef']);
|
|
55
|
+
|
|
56
|
+
const OCTANE_NAMED_IMPORT =
|
|
57
|
+
/import\s*\{([^}]*)\}\s*from\s*(['"])octane\2([^\S\n]*;)?([^\S\n]*\n)?/g;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Drop `useMemo`, `useCallback`, and `useRef` from the `octane` import when
|
|
61
|
+
* the fixes removed their last use. A name that still appears anywhere else,
|
|
62
|
+
* even in a comment, keeps its import: an unused import is harmless, a missing
|
|
63
|
+
* one is not.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} text
|
|
66
|
+
* @param {readonly string[]} candidates hook names a fix replaced
|
|
67
|
+
*/
|
|
68
|
+
export function pruneImports(text, candidates) {
|
|
69
|
+
const names = candidates.filter((name) => REPLACED_HOOKS.has(name));
|
|
70
|
+
if (names.length === 0) return text;
|
|
71
|
+
return text.replace(
|
|
72
|
+
OCTANE_NAMED_IMPORT,
|
|
73
|
+
(statement, /** @type {string} */ list, quote, semicolon = '', newline = '') => {
|
|
74
|
+
const specifiers = list
|
|
75
|
+
.split(',')
|
|
76
|
+
.map((specifier) => specifier.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
const kept = specifiers.filter((specifier) => {
|
|
79
|
+
if (!names.includes(specifier)) return true;
|
|
80
|
+
const uses = text.match(new RegExp(`\\b${specifier}\\b`, 'g'))?.length ?? 0;
|
|
81
|
+
return uses > 1;
|
|
82
|
+
});
|
|
83
|
+
if (kept.length === specifiers.length) return statement;
|
|
84
|
+
if (kept.length === 0) return '';
|
|
85
|
+
return `import { ${kept.join(', ')} } from ${quote}octane${quote}${semicolon}${newline}`;
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
}
|