@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/mcp-server",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.22.2"
@@ -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;
@@ -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; place inside
28
- `<head>`.
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; the client adopts server DOM instead of rebuilding it.
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]) => ({ name, count, ...REACT_API_MAP[name] }))
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;
@@ -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',