@octanejs/mcp-server 0.2.23 → 0.2.24

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.24",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.22.2"
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',
@@ -474,6 +475,20 @@ export async function collectSourceFiles(root, out = [], depth = 0) {
474
475
 
475
476
  export function scanSource(source) {
476
477
  const apis = new Map();
478
+ const symbolExports = new Map();
479
+ // Introspection libraries export element-kind symbols, not components.
480
+ // Require the right-hand identifier to resolve to an actual Symbol.for
481
+ // declaration; ordinary component exports/render calls remain API uses.
482
+ const symbols = new Set(
483
+ [...source.matchAll(/\b([A-Za-z_$][\w$]*)\s*=\s*Symbol\.for\(\s*['"][^'"]+['"]\s*\)/g)].map(
484
+ (match) => match[1],
485
+ ),
486
+ );
487
+ for (const match of source.matchAll(
488
+ /\bexports\.([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*;/g,
489
+ )) {
490
+ if (symbols.has(match[2])) symbolExports.set(match[1], (symbolExports.get(match[1]) ?? 0) + 1);
491
+ }
477
492
  for (const name of Object.keys(REACT_API_MAP)) {
478
493
  if (name === 'onChange') continue;
479
494
  const matches = source.match(new RegExp(`\\b${name}\\b`, 'g'));
@@ -493,13 +508,14 @@ export function scanSource(source) {
493
508
  }
494
509
  }
495
510
  const classComponent = /\bextends\s+(React\.)?(Pure)?Component\b/.test(source);
496
- return { apis, imports, classComponent };
511
+ return { apis, imports, classComponent, symbolExports };
497
512
  }
498
513
 
499
514
  export async function scanPath(root) {
500
515
  const files = await collectSourceFiles(resolve(root));
501
516
  const totals = new Map();
502
517
  const imports = new Set();
518
+ const symbolExports = new Map();
503
519
  let classComponents = false;
504
520
  for (const file of files) {
505
521
  let source;
@@ -513,14 +529,26 @@ export async function scanPath(root) {
513
529
  totals.set(name, (totals.get(name) ?? 0) + count);
514
530
  }
515
531
  for (const spec of result.imports) imports.add(spec);
532
+ for (const [name, count] of result.symbolExports) {
533
+ symbolExports.set(name, (symbolExports.get(name) ?? 0) + count);
534
+ }
516
535
  classComponents ||= result.classComponent;
517
536
  }
518
- return { filesScanned: files.length, totals, imports, classComponents };
537
+ return { filesScanned: files.length, totals, imports, classComponents, symbolExports };
519
538
  }
520
539
 
521
- function apiRows(totals) {
540
+ function apiRows(totals, symbolExports) {
522
541
  return [...totals.entries()]
523
- .map(([name, count]) => ({ name, count, ...REACT_API_MAP[name] }))
542
+ .map(([name, count]) =>
543
+ symbolExports.get(name) === count
544
+ ? {
545
+ name,
546
+ count,
547
+ status: 'rewrite',
548
+ note: 'Exported element-kind marker: map to the Octane kind; predicates for unsupported kinds remain false. This does not require rendering that component.',
549
+ }
550
+ : { name, count, ...REACT_API_MAP[name] },
551
+ )
524
552
  .sort((a, b) => b.count - a.count);
525
553
  }
526
554
 
@@ -581,7 +609,7 @@ export async function bridgeReport({ packageName, path, projectRoot }) {
581
609
  }
582
610
 
583
611
  const scan = await scanPath(scanRoot);
584
- const rows = apiRows(scan.totals);
612
+ const rows = apiRows(scan.totals, scan.symbolExports);
585
613
  report.filesScanned = scan.filesScanned;
586
614
  report.reactImports = [...scan.imports];
587
615
  report.classComponents = scan.classComponents;
@@ -604,7 +632,7 @@ export function bridgeReportFromSource(source, { packageName } = {}) {
604
632
  report.vanillaCore = detectVanillaCore(packageName, null);
605
633
  }
606
634
  const scan = scanSource(source);
607
- const rows = apiRows(scan.apis);
635
+ const rows = apiRows(scan.apis, scan.symbolExports);
608
636
  report.reactImports = [...scan.imports];
609
637
  report.classComponents = scan.classComponent;
610
638
  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
@@ -114,6 +114,7 @@ export const BENCHMARK_SUITES = [
114
114
  'lynx-bundle-size',
115
115
  'codegen-size',
116
116
  'hook-memo',
117
+ 'transition-hooks',
117
118
  'template-call-memo',
118
119
  'compiler-throughput',
119
120
  'tsrx-component-graph',