@octanejs/mcp-server 0.2.23 → 0.2.25
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 +1 -1
- package/package.json +1 -1
- package/skills/build-octane-software.md +9 -0
- package/skills/migrate-react-component.md +1 -0
- package/skills/react-divergences.md +22 -0
- package/skills/setup-ssr.md +7 -3
- package/src/bridge.js +36 -6
- package/src/bridge.test.js +17 -0
- package/src/index.js +2 -0
package/README.md
CHANGED
|
@@ -167,7 +167,7 @@ one manifest suite by name (`js-framework`, `todomvc`, `weather-app`,
|
|
|
167
167
|
`floating-tree-navigation`, `manifest-cache-invalidation`, `vite-client-assets`, `activity`,
|
|
168
168
|
`streaming-ssr`, `streaming-backpressure`,
|
|
169
169
|
`compiler-throughput`, `tsrx-component-graph`, `codegen-size`, `hook-memo`,
|
|
170
|
-
`template-call-memo`, `tsrx-renderer-selection`, `bundle-size`, `bundle-reachability`, `three-renderer`,
|
|
170
|
+
`transition-hooks`, `template-call-memo`, `tsrx-renderer-selection`, `bundle-size`, `bundle-reachability`, `three-renderer`,
|
|
171
171
|
`three-bundle-size`, …)
|
|
172
172
|
or every suite with `all`; `quick` selects the reduced-iteration smoke pass. The
|
|
173
173
|
suite list mirrors the runner manifest and `node benchmarks/bench.mjs --list`.
|
package/package.json
CHANGED
|
@@ -30,6 +30,15 @@ details but do not replace these gates.
|
|
|
30
30
|
the retained state.
|
|
31
31
|
- Use native event semantics. `onInput` is the per-edit event for text controls;
|
|
32
32
|
do not add synthetic `onChange` compatibility or event wrapper allocation.
|
|
33
|
+
- Style with sibling-scoped `<style>` blocks and assigned theme blocks
|
|
34
|
+
(`const theme = <style>…</style>`, `class={theme.card}`,
|
|
35
|
+
`<style apply={theme} />`) instead of a CSS-in-JS runtime. A block styles
|
|
36
|
+
the items beside it and everything below them, never the element that
|
|
37
|
+
contains it: put it beside the element in a fragment
|
|
38
|
+
(`<><style>…</style><div>…</div></>`), inside `@{ … }` and directive bodies
|
|
39
|
+
too. Keep block CSS static and pass runtime values through custom
|
|
40
|
+
properties. A block inside a control-flow branch ships its CSS whichever
|
|
41
|
+
branch renders, so keep branch-only rules small.
|
|
33
42
|
- For SSR, avoid client/server data divergence and duplicate fetches. Exercise
|
|
34
43
|
hydration with production-compiled output and preserve abort/error behavior.
|
|
35
44
|
- Treat bundle size and dependency cost as performance. Check for an official
|
|
@@ -45,6 +45,7 @@ locals, early returns) stays above it.
|
|
|
45
45
|
| text `<input onChange={...}>` meaning every edit | `<input onInput={...}>` (native event) |
|
|
46
46
|
| controlled `value={state}` | keep it — React's controlled semantics apply; pair with `onInput` |
|
|
47
47
|
| `className={clsx(...)}` | `class={[...]}` composes clsx-style natively |
|
|
48
|
+
| CSS Modules `styles.card` / CSS-in-JS | `const theme = <style>…</style>` (a class map: `$class` plus one key per class) and `class={theme.card}`; a `<style>` block among an element's or fragment's children styles the items beside it and below (never its container), and `<style apply={theme} />` applies a theme to those same items |
|
|
48
49
|
| `useDebugValue(x)` | keep or delete — present as an accepted no-op |
|
|
49
50
|
| `React.lazy(() => import(...))` | `lazy()` works as-is (and also accepts a bare component from the loader) |
|
|
50
51
|
| `defaultProps` | parameter defaults / destructuring defaults |
|
|
@@ -85,6 +85,28 @@ React's `{ default }` module shape works, and Octane additionally accepts a
|
|
|
85
85
|
component directly from the loader. Suspense and ViewTransition are ordinary
|
|
86
86
|
components, so wrapping them in `lazy()` is valid; nested lazy wrappers are not.
|
|
87
87
|
|
|
88
|
+
## Scoped styles are sibling-scoped
|
|
89
|
+
|
|
90
|
+
A `<style>` block with raw CSS is TSRX template syntax, not a global stylesheet
|
|
91
|
+
as in React. It is a child of an element or a fragment and is scoped to its
|
|
92
|
+
siblings, not to the `@{ … }` body around it: it styles the items beside it and
|
|
93
|
+
everything below them — never the element that contains it —
|
|
94
|
+
selectors are rewritten with the hash of that children list and the hash is
|
|
95
|
+
stamped on those siblings and their descendants. To style an element, make the
|
|
96
|
+
block and the element fragment siblings (`<><style>…</style><div>…</div></>`);
|
|
97
|
+
a `@{ … }` or directive body holds one output node, so wrap the block and the
|
|
98
|
+
output in a fragment there too. Nested scopes stack their hashes outer to
|
|
99
|
+
inner; several blocks among the same children share one hash; `:global(…)` opts
|
|
100
|
+
out. `const theme = <style>…</style>` yields a class map (`$class` plus one key
|
|
101
|
+
per class) and `<style apply={theme} />` applies it to the items beside it,
|
|
102
|
+
with `apply={[a, b]}` composing. A theme must be declared before its applier.
|
|
103
|
+
The CSS of a control-flow branch is always emitted; only the stamping follows
|
|
104
|
+
the branch. A raw-CSS block is allowed only inside a `@{ … }` or
|
|
105
|
+
`@if`/`@for`/`@switch`/`@try` body; plain TSX keeps React's rule, where
|
|
106
|
+
`<style>{css}</style>` is an ordinary element passed through untouched. Only
|
|
107
|
+
`<style href precedence>` keeps React's Float semantics. Do not port this to
|
|
108
|
+
CSS Modules or a CSS-in-JS runtime.
|
|
109
|
+
|
|
88
110
|
## class / className composes clsx-style
|
|
89
111
|
|
|
90
112
|
Strings, numbers, arrays, objects, and nesting compose into a class string;
|
package/skills/setup-ssr.md
CHANGED
|
@@ -24,8 +24,10 @@ const { html, css } = await prerender(App, props, {
|
|
|
24
24
|
- `html`: rendered markup with hydration markers, plus an inline suspense seed
|
|
25
25
|
script when anything resolved. Hoisted `<title>/<meta>/<link>` fold in
|
|
26
26
|
(spliced into `<head>` if present, else prepended).
|
|
27
|
-
- `css`: deduped `<style data-octane>` tags from scoped styles
|
|
28
|
-
|
|
27
|
+
- `css`: deduped `<style data-octane>` tags from scoped styles, one per hash;
|
|
28
|
+
a component contributes one per style scope (nested `@{ … }` and
|
|
29
|
+
control-flow bodies have their own) plus one per assigned theme block. Place
|
|
30
|
+
them inside `<head>`.
|
|
29
31
|
- Use `renderToString` (from `octane/server`) for a single synchronous pass that
|
|
30
32
|
leaves `@pending` fallbacks in place; use `prerender` to await the data.
|
|
31
33
|
- Options are optional: `nonce` stamps CSP nonces on the emitted inline tags (all
|
|
@@ -54,7 +56,9 @@ hydrateRoot(document.getElementById('app')!, App, props);
|
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Pass the same component and props on both sides. `useId` and scoped styles are
|
|
57
|
-
hydration-stable
|
|
59
|
+
hydration-stable, including the stacked scope hashes and applied theme classes
|
|
60
|
+
(`<style apply={theme} />`, `theme.$class`); the client adopts server DOM
|
|
61
|
+
instead of rebuilding it.
|
|
58
62
|
|
|
59
63
|
## Two integration paths
|
|
60
64
|
|
package/src/bridge.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
// expected union from the workspace manifests, so publishing a new binding
|
|
7
7
|
// without registering it in either catalog fails the mcp-server tests.
|
|
8
8
|
export const KNOWN_BINDINGS = {
|
|
9
|
+
'react-is': '@octanejs/octane-is',
|
|
9
10
|
'@gsap/react': '@octanejs/gsap',
|
|
10
11
|
animejs: '@octanejs/animejs',
|
|
11
12
|
'usehooks-ts': '@octanejs/usehooks-ts',
|
|
@@ -67,6 +68,8 @@ export const KNOWN_BINDINGS = {
|
|
|
67
68
|
'react-hook-form': '@octanejs/hook-form',
|
|
68
69
|
'better-auth/react': '@octanejs/better-auth',
|
|
69
70
|
'@base-ui-components/react': '@octanejs/base-ui',
|
|
71
|
+
'@base-ui/react': '@octanejs/base-ui',
|
|
72
|
+
'@base-ui/utils': '@octanejs/base-ui-utils',
|
|
70
73
|
'@dnd-kit/react': '@octanejs/dnd-kit',
|
|
71
74
|
'embla-carousel-react': '@octanejs/embla-carousel',
|
|
72
75
|
'react-dropzone': '@octanejs/dropzone',
|
|
@@ -474,6 +477,20 @@ export async function collectSourceFiles(root, out = [], depth = 0) {
|
|
|
474
477
|
|
|
475
478
|
export function scanSource(source) {
|
|
476
479
|
const apis = new Map();
|
|
480
|
+
const symbolExports = new Map();
|
|
481
|
+
// Introspection libraries export element-kind symbols, not components.
|
|
482
|
+
// Require the right-hand identifier to resolve to an actual Symbol.for
|
|
483
|
+
// declaration; ordinary component exports/render calls remain API uses.
|
|
484
|
+
const symbols = new Set(
|
|
485
|
+
[...source.matchAll(/\b([A-Za-z_$][\w$]*)\s*=\s*Symbol\.for\(\s*['"][^'"]+['"]\s*\)/g)].map(
|
|
486
|
+
(match) => match[1],
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
for (const match of source.matchAll(
|
|
490
|
+
/\bexports\.([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*;/g,
|
|
491
|
+
)) {
|
|
492
|
+
if (symbols.has(match[2])) symbolExports.set(match[1], (symbolExports.get(match[1]) ?? 0) + 1);
|
|
493
|
+
}
|
|
477
494
|
for (const name of Object.keys(REACT_API_MAP)) {
|
|
478
495
|
if (name === 'onChange') continue;
|
|
479
496
|
const matches = source.match(new RegExp(`\\b${name}\\b`, 'g'));
|
|
@@ -493,13 +510,14 @@ export function scanSource(source) {
|
|
|
493
510
|
}
|
|
494
511
|
}
|
|
495
512
|
const classComponent = /\bextends\s+(React\.)?(Pure)?Component\b/.test(source);
|
|
496
|
-
return { apis, imports, classComponent };
|
|
513
|
+
return { apis, imports, classComponent, symbolExports };
|
|
497
514
|
}
|
|
498
515
|
|
|
499
516
|
export async function scanPath(root) {
|
|
500
517
|
const files = await collectSourceFiles(resolve(root));
|
|
501
518
|
const totals = new Map();
|
|
502
519
|
const imports = new Set();
|
|
520
|
+
const symbolExports = new Map();
|
|
503
521
|
let classComponents = false;
|
|
504
522
|
for (const file of files) {
|
|
505
523
|
let source;
|
|
@@ -513,14 +531,26 @@ export async function scanPath(root) {
|
|
|
513
531
|
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
514
532
|
}
|
|
515
533
|
for (const spec of result.imports) imports.add(spec);
|
|
534
|
+
for (const [name, count] of result.symbolExports) {
|
|
535
|
+
symbolExports.set(name, (symbolExports.get(name) ?? 0) + count);
|
|
536
|
+
}
|
|
516
537
|
classComponents ||= result.classComponent;
|
|
517
538
|
}
|
|
518
|
-
return { filesScanned: files.length, totals, imports, classComponents };
|
|
539
|
+
return { filesScanned: files.length, totals, imports, classComponents, symbolExports };
|
|
519
540
|
}
|
|
520
541
|
|
|
521
|
-
function apiRows(totals) {
|
|
542
|
+
function apiRows(totals, symbolExports) {
|
|
522
543
|
return [...totals.entries()]
|
|
523
|
-
.map(([name, count]) =>
|
|
544
|
+
.map(([name, count]) =>
|
|
545
|
+
symbolExports.get(name) === count
|
|
546
|
+
? {
|
|
547
|
+
name,
|
|
548
|
+
count,
|
|
549
|
+
status: 'rewrite',
|
|
550
|
+
note: 'Exported element-kind marker: map to the Octane kind; predicates for unsupported kinds remain false. This does not require rendering that component.',
|
|
551
|
+
}
|
|
552
|
+
: { name, count, ...REACT_API_MAP[name] },
|
|
553
|
+
)
|
|
524
554
|
.sort((a, b) => b.count - a.count);
|
|
525
555
|
}
|
|
526
556
|
|
|
@@ -581,7 +611,7 @@ export async function bridgeReport({ packageName, path, projectRoot }) {
|
|
|
581
611
|
}
|
|
582
612
|
|
|
583
613
|
const scan = await scanPath(scanRoot);
|
|
584
|
-
const rows = apiRows(scan.totals);
|
|
614
|
+
const rows = apiRows(scan.totals, scan.symbolExports);
|
|
585
615
|
report.filesScanned = scan.filesScanned;
|
|
586
616
|
report.reactImports = [...scan.imports];
|
|
587
617
|
report.classComponents = scan.classComponents;
|
|
@@ -604,7 +634,7 @@ export function bridgeReportFromSource(source, { packageName } = {}) {
|
|
|
604
634
|
report.vanillaCore = detectVanillaCore(packageName, null);
|
|
605
635
|
}
|
|
606
636
|
const scan = scanSource(source);
|
|
607
|
-
const rows = apiRows(scan.apis);
|
|
637
|
+
const rows = apiRows(scan.apis, scan.symbolExports);
|
|
608
638
|
report.reactImports = [...scan.imports];
|
|
609
639
|
report.classComponents = scan.classComponent;
|
|
610
640
|
report.apis = rows;
|
package/src/bridge.test.js
CHANGED
|
@@ -41,6 +41,23 @@ describe('scanSource', () => {
|
|
|
41
41
|
expect(scanSource('class Memoish extends PureComponent {}').classComponent).toBe(true);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('classifies symbol-kind exports without requiring the corresponding renderer', () => {
|
|
45
|
+
const report = bridgeReportFromSource(`
|
|
46
|
+
var REACT_PROFILER_TYPE = Symbol.for('react.profiler');
|
|
47
|
+
exports.Profiler = REACT_PROFILER_TYPE;
|
|
48
|
+
exports.isProfiler = value => value.type === REACT_PROFILER_TYPE;
|
|
49
|
+
`);
|
|
50
|
+
expect(report.apis.find((row) => row.name === 'Profiler').status).toBe('rewrite');
|
|
51
|
+
expect(report.verdict).toBe('bridgeable-with-rewrites');
|
|
52
|
+
const renderer = bridgeReportFromSource(`
|
|
53
|
+
import { Profiler } from 'react';
|
|
54
|
+
var REACT_PROFILER_TYPE = Symbol.for('react.profiler');
|
|
55
|
+
exports.Profiler = REACT_PROFILER_TYPE;
|
|
56
|
+
export const View = () => <Profiler />;
|
|
57
|
+
`);
|
|
58
|
+
expect(renderer.verdict).toBe('needs-rework');
|
|
59
|
+
});
|
|
60
|
+
|
|
44
61
|
it('targets only React-style text-host onChange wiring', () => {
|
|
45
62
|
const source = `
|
|
46
63
|
function Demo(props) {
|
package/src/index.js
CHANGED
|
@@ -79,6 +79,7 @@ export const BENCHMARK_SUITES = [
|
|
|
79
79
|
'radix-collection-order',
|
|
80
80
|
'router-dispatch',
|
|
81
81
|
'visx-categorical-scale',
|
|
82
|
+
'style-unitless',
|
|
82
83
|
'rspack-css-graph',
|
|
83
84
|
'floating-tree-navigation',
|
|
84
85
|
'ink-cursor-update',
|
|
@@ -114,6 +115,7 @@ export const BENCHMARK_SUITES = [
|
|
|
114
115
|
'lynx-bundle-size',
|
|
115
116
|
'codegen-size',
|
|
116
117
|
'hook-memo',
|
|
118
|
+
'transition-hooks',
|
|
117
119
|
'template-call-memo',
|
|
118
120
|
'compiler-throughput',
|
|
119
121
|
'tsrx-component-graph',
|