@octanejs/mcp-server 0.2.3 → 0.2.6
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 +53 -12
- package/package.json +3 -2
- package/skills/bridge-react-package.md +35 -17
- package/skills/build-octane-software.md +69 -0
- package/skills/migrate-react-component.md +24 -6
- package/skills/react-divergences.md +66 -10
- package/skills/setup-ssr.md +48 -17
- package/src/bridge.js +209 -7
- package/src/bridge.test.js +125 -34
- package/src/index.js +190 -21
- package/src/index.test.js +127 -0
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@ MCP server for agents working with [Octane](https://github.com/octanejs/octane).
|
|
|
5
5
|
It serves two audiences:
|
|
6
6
|
|
|
7
7
|
- **Octane users** (any project): skills and tools for bridging React packages
|
|
8
|
-
to Octane,
|
|
9
|
-
|
|
10
|
-
the skills ship inside
|
|
8
|
+
to Octane, engineering production-grade applications and libraries, migrating
|
|
9
|
+
React components to `.tsrx`, understanding Octane's intentional divergences
|
|
10
|
+
from React, and setting up SSR. These work anywhere; the skills ship inside
|
|
11
|
+
this package.
|
|
11
12
|
- **Octane maintainers** (the octane monorepo): repo triage, validation
|
|
12
13
|
planning, benchmark and React-test-port automation. These tools register
|
|
13
14
|
only when the server detects an octane monorepo checkout at its root.
|
|
@@ -56,20 +57,50 @@ maintainer tools):
|
|
|
56
57
|
|
|
57
58
|
## Tools (always available)
|
|
58
59
|
|
|
60
|
+
The server initialization instructions direct coding agents to call
|
|
61
|
+
`octane_engineering_plan` and load `build-octane-software` before creating or
|
|
62
|
+
materially changing Octane code. This makes the correctness, performance
|
|
63
|
+
evidence, adversarial self-review, and handoff requirements available even when
|
|
64
|
+
the host does not automatically discover skills.
|
|
65
|
+
|
|
66
|
+
### `octane_engineering_plan`
|
|
67
|
+
|
|
68
|
+
Returns structured engineering gates for application, library, or
|
|
69
|
+
framework-core work. Framework-core plans always require hot-path analysis,
|
|
70
|
+
comparable baseline/candidate performance evidence, the maintainer core and
|
|
71
|
+
performance skills, a second review of the final diff, and explicit residual
|
|
72
|
+
risk reporting. In repo mode, the response also includes validation commands for
|
|
73
|
+
the supplied paths. A framework-core request outside repo mode returns a blocking
|
|
74
|
+
condition directing the client to configure `OCTANE_REPO_ROOT`, because the
|
|
75
|
+
required maintainer skills and repository validation are otherwise unavailable.
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"scope": "framework-core",
|
|
80
|
+
"changeKind": "performance",
|
|
81
|
+
"paths": ["packages/octane/src/runtime.ts"]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
59
85
|
### `octane_bridge_react_package`
|
|
60
86
|
|
|
61
87
|
Scans a React package (by name from `node_modules`, or any source directory by
|
|
62
88
|
path) for React API usage and returns an Octane compatibility report: which
|
|
63
|
-
APIs map one-to-one, which need rewrites (`forwardRef`,
|
|
64
|
-
|
|
65
|
-
core can be reused verbatim, whether an official
|
|
66
|
-
exists, an overall verdict (`bridgeable`,
|
|
67
|
-
`needs-rework`), and a step-by-step plan.
|
|
89
|
+
APIs map one-to-one, which need rewrites (`forwardRef`, class components,
|
|
90
|
+
synthetic `onChange`, `react-dom/server` imports), whether a
|
|
91
|
+
framework-agnostic core can be reused verbatim, whether an official
|
|
92
|
+
`@octanejs/*` binding already exists, an overall verdict (`bridgeable`,
|
|
93
|
+
`bridgeable-with-rewrites`, `needs-rework`), and a step-by-step plan.
|
|
68
94
|
|
|
69
95
|
```json
|
|
70
96
|
{ "package": "jotai", "projectRoot": "/path/to/my-app" }
|
|
71
97
|
```
|
|
72
98
|
|
|
99
|
+
The event scan is host-aware: it recommends `onInput` only for direct standard
|
|
100
|
+
text-host wiring that appears to mean “every edit.” It leaves component callbacks,
|
|
101
|
+
selects, checkboxes/radios, dynamic input types, and explicitly intentional native
|
|
102
|
+
text commits alone.
|
|
103
|
+
|
|
73
104
|
### `octane_bindings`
|
|
74
105
|
|
|
75
106
|
Returns the map of React packages with maintained `@octanejs/*` ports. The map
|
|
@@ -82,6 +113,8 @@ without registering its React-package mapping fails CI.
|
|
|
82
113
|
Returns a skill by name. Bundled skills (shipped with this package):
|
|
83
114
|
|
|
84
115
|
- `bridge-react-package` — the full workflow for porting a React library.
|
|
116
|
+
- `build-octane-software` — production engineering, performance, validation,
|
|
117
|
+
and adversarial self-review gates for Octane code.
|
|
85
118
|
- `migrate-react-component` — React JSX to `.tsrx` conversion reference.
|
|
86
119
|
- `react-divergences` — Octane's intentional differences from React.
|
|
87
120
|
- `setup-ssr` — server rendering and hydration setup.
|
|
@@ -101,11 +134,14 @@ invariants, and validation commands.
|
|
|
101
134
|
### `octane_triage_paths`
|
|
102
135
|
|
|
103
136
|
Classifies repository-relative paths by Octane area (compiler, core runtime,
|
|
104
|
-
SSR, ecosystem binding,
|
|
137
|
+
SSR, ecosystem binding, vite-plugin, deploy adapter, evals, website,
|
|
138
|
+
mcp-server, benchmark, docs, RuleSync source).
|
|
105
139
|
|
|
106
140
|
### `octane_validate_plan`
|
|
107
141
|
|
|
108
|
-
Recommends validation commands for changed paths and task kind.
|
|
142
|
+
Recommends validation commands for changed paths and task kind. Core task plans
|
|
143
|
+
include the quick benchmark ratio gate in addition to core tests, typechecking,
|
|
144
|
+
and repository-wide formatting.
|
|
109
145
|
|
|
110
146
|
### `octane_scaffold_react_port`
|
|
111
147
|
|
|
@@ -114,8 +150,13 @@ optionally writes the generated Vitest skeleton to an output file.
|
|
|
114
150
|
|
|
115
151
|
### `octane_benchmark`
|
|
116
152
|
|
|
117
|
-
Runs
|
|
118
|
-
`
|
|
153
|
+
Runs benchmark suites through the unified runner (`node benchmarks/bench.mjs`):
|
|
154
|
+
one manifest suite by name (`js-framework`, `todomvc`, `weather-app`,
|
|
155
|
+
`weather-app-lighthouse`, `chat-stream`, `dbmon`, `news`, `react-hosted-islands`,
|
|
156
|
+
`ssr-throughput`, `streaming-ssr`,
|
|
157
|
+
`codegen-size`, `bundle-size`, `three-renderer`, `three-bundle-size`, …) or every
|
|
158
|
+
suite with `all`; `quick` selects the reduced-iteration smoke pass. The suite
|
|
159
|
+
list mirrors the runner manifest and `node benchmarks/bench.mjs --list`.
|
|
119
160
|
|
|
120
161
|
### `octane_issue_context`
|
|
121
162
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"octane-mcp-server": "src/index.js"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
|
-
".": "./src/index.js"
|
|
31
|
+
".": "./src/index.js",
|
|
32
|
+
"./bridge": "./src/bridge.js"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@modelcontextprotocol/sdk": "^1.21.0",
|
|
@@ -12,20 +12,36 @@ of bridging by hand:
|
|
|
12
12
|
| `zustand` | `@octanejs/zustand` |
|
|
13
13
|
| `jotai` | `@octanejs/jotai` |
|
|
14
14
|
| `@apollo/client` | `@octanejs/apollo-client` |
|
|
15
|
+
| `@tanstack/ai-react` | `@octanejs/tanstack-ai` |
|
|
16
|
+
| `@tanstack/react-form` | `@octanejs/tanstack-form` |
|
|
15
17
|
| `@tanstack/react-query` | `@octanejs/tanstack-query` |
|
|
16
18
|
| `@tanstack/react-router` | `@octanejs/tanstack-router` |
|
|
19
|
+
| `@tanstack/react-store` | `@octanejs/tanstack-store` |
|
|
17
20
|
| `@tanstack/react-table` | `@octanejs/tanstack-table` |
|
|
18
21
|
| `@tanstack/react-virtual` | `@octanejs/tanstack-virtual` |
|
|
19
22
|
| `framer-motion` / `motion` | `@octanejs/motion` |
|
|
20
23
|
| `@stylexjs/stylex` | `@octanejs/stylex` |
|
|
24
|
+
| `styled-components` | `@octanejs/styled-components` |
|
|
21
25
|
| `react-router` / `react-router-dom` | `@octanejs/remix-router` |
|
|
22
26
|
| `@lexical/react` | `@octanejs/lexical` |
|
|
23
27
|
| `lucide-react` | `@octanejs/lucide` |
|
|
24
28
|
| `@floating-ui/react` | `@octanejs/floating-ui` |
|
|
25
29
|
| `radix-ui` | `@octanejs/radix` |
|
|
26
30
|
| `react-i18next` | `@octanejs/i18next` |
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
| `react-hook-form` | `@octanejs/hook-form` |
|
|
32
|
+
| `@base-ui-components/react` | `@octanejs/base-ui` |
|
|
33
|
+
| `@dnd-kit/react` | `@octanejs/dnd-kit` |
|
|
34
|
+
| `sonner` | `@octanejs/sonner` |
|
|
35
|
+
| `recharts` | `@octanejs/recharts` |
|
|
36
|
+
| `@react-three/fiber` | `@octanejs/three` |
|
|
37
|
+
| `@visx/*` | `@octanejs/visx` |
|
|
38
|
+
| `react-redux` | `@octanejs/redux` |
|
|
39
|
+
| `@reduxjs/toolkit` | `@octanejs/redux-toolkit` |
|
|
40
|
+
| `@testing-library/react` | `@octanejs/testing-library` |
|
|
41
|
+
| `@mdx-js/react` | `@octanejs/mdx` |
|
|
42
|
+
|
|
43
|
+
The `octane_bindings` tool returns the same map machine-readably. For anything
|
|
44
|
+
else, run the `octane_bridge_react_package` tool to get a scan of the
|
|
29
45
|
package's React API usage and a tailored plan, then follow the workflow below.
|
|
30
46
|
|
|
31
47
|
## Mental model
|
|
@@ -60,23 +76,25 @@ So a bridge never means "run the React package unchanged". It means:
|
|
|
60
76
|
`useCallback`, `useRef`, `useContext`, `useId`, `useImperativeHandle`,
|
|
61
77
|
`useSyncExternalStore` (full React 19 shape, including `getServerSnapshot`),
|
|
62
78
|
`useTransition`, `useDeferredValue`, `useActionState`, `useOptimistic`,
|
|
63
|
-
`useEffectEvent`, `
|
|
64
|
-
`
|
|
65
|
-
|
|
79
|
+
`useEffectEvent`, `useDebugValue` (accepted no-op), `use`, `startTransition`,
|
|
80
|
+
`memo`, `lazy` (also accepts a bare component from the loader),
|
|
81
|
+
`createContext`, `Suspense`, `createPortal`, `flushSync`, `createRoot`,
|
|
82
|
+
`hydrateRoot`. Everything imports from `octane` (no separate `react-dom`);
|
|
83
|
+
server rendering imports from `octane/server`, including the streaming
|
|
84
|
+
`renderToPipeableStream`/`renderToReadableStream`.
|
|
66
85
|
|
|
67
86
|
3. **Handle the gaps:**
|
|
68
87
|
- `forwardRef`: does not exist. Accept `ref` as a normal prop (React 19
|
|
69
88
|
style) and drop the wrapper.
|
|
70
|
-
- `useDebugValue`: shim as a no-op.
|
|
71
|
-
- `lazy`: use dynamic `import()` plus `use()` inside a `Suspense` boundary.
|
|
72
89
|
- Class components: rewrite as function components. Error boundary classes
|
|
73
90
|
become `<ErrorBoundary>` or the `@try { } @catch (e) { }` directive.
|
|
74
|
-
- Synthetic `onChange` on text
|
|
75
|
-
are native and delegated.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
- Synthetic `onChange` on standard text hosts: use native `onInput` when the
|
|
92
|
+
callback means every edit. Octane events are native and delegated.
|
|
93
|
+
Controlled `value`/`checked` follow React's semantics (the prop drives the
|
|
94
|
+
DOM property and reasserts on commits), so per-edit controlled-input logic
|
|
95
|
+
ports unchanged apart from the event name. Do not blanket-rewrite public
|
|
96
|
+
callbacks, selects, or checkbox/radio handlers. A deliberate uncontrolled
|
|
97
|
+
text commit may keep `onChange` with `suppressNativeChangeWarning`.
|
|
80
98
|
- StrictMode double-invoke: does not exist; delete test expectations that
|
|
81
99
|
count double renders.
|
|
82
100
|
|
|
@@ -118,7 +136,7 @@ So a bridge never means "run the React package unchanged". It means:
|
|
|
118
136
|
|
|
119
137
|
- `bridgeable`: only same-name hooks used; a mechanical rename of imports to
|
|
120
138
|
`octane` plus a `.tsrx` re-author of components is enough.
|
|
121
|
-
- `bridgeable-with-rewrites`: needs the `forwardRef` /
|
|
122
|
-
|
|
123
|
-
- `needs-rework`: class components, `
|
|
124
|
-
|
|
139
|
+
- `bridgeable-with-rewrites`: needs the `forwardRef` / event / `react-dom/server`
|
|
140
|
+
import rewrites above, but no architectural blockers.
|
|
141
|
+
- `needs-rework`: class components, `findDOMNode`, or React internals. Bridge
|
|
142
|
+
the core, redesign the binding.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Skill: Build production-grade Octane software
|
|
2
|
+
|
|
3
|
+
Use this before creating or materially changing an Octane application, component,
|
|
4
|
+
library binding, or framework integration. It is the quality baseline for work
|
|
5
|
+
performed with Octane's MCP tools; task-specific skills add syntax and migration
|
|
6
|
+
details but do not replace these gates.
|
|
7
|
+
|
|
8
|
+
## Start with the contract
|
|
9
|
+
|
|
10
|
+
1. State the user-visible behavior, inputs, failure states, accessibility needs,
|
|
11
|
+
and server/hydration expectations before choosing components or hooks.
|
|
12
|
+
2. Inspect the current project conventions, package versions, compiler/Vite
|
|
13
|
+
configuration, existing tests, and official `@octanejs/*` bindings. Do not
|
|
14
|
+
invent an API that current sources do not expose.
|
|
15
|
+
3. Identify the important user journey and its likely performance budget: initial
|
|
16
|
+
JavaScript, render/hydration work, interaction latency, network/data work, and
|
|
17
|
+
memory lifetime.
|
|
18
|
+
|
|
19
|
+
## Design for Octane's performance model
|
|
20
|
+
|
|
21
|
+
- Keep stable work outside reactive updates. Avoid state that can be derived,
|
|
22
|
+
effects that only mirror state, and subscriptions broader than the component
|
|
23
|
+
needs.
|
|
24
|
+
- Use keyed `@for` blocks for dynamic collections with stable domain keys. Keep
|
|
25
|
+
item components focused so an item update does not rebuild unrelated work.
|
|
26
|
+
- Prefer the platform and Octane's compiled directives over runtime abstraction
|
|
27
|
+
layers. Do not import React runtime packages or ship React-created JSX trees.
|
|
28
|
+
- Keep expensive parsing, sorting, formatting, and object construction off common
|
|
29
|
+
render paths; memoize only when the avoided work and invalidation rules justify
|
|
30
|
+
the retained state.
|
|
31
|
+
- Use native event semantics. `onInput` is the per-edit event for text controls;
|
|
32
|
+
do not add synthetic `onChange` compatibility or event wrapper allocation.
|
|
33
|
+
- For SSR, avoid client/server data divergence and duplicate fetches. Exercise
|
|
34
|
+
hydration with production-compiled output and preserve abort/error behavior.
|
|
35
|
+
- Treat bundle size and dependency cost as performance. Check for an official
|
|
36
|
+
binding before adding a compatibility layer or a second framework runtime.
|
|
37
|
+
|
|
38
|
+
## Validate behavior and performance
|
|
39
|
+
|
|
40
|
+
- Test realistic public behavior: rendered output, native events, focus,
|
|
41
|
+
accessibility state, errors, loading, cleanup, and hydration when used.
|
|
42
|
+
- Include empty, loading, error, repeated-interaction, and large-data cases that
|
|
43
|
+
can expose stale state, duplicate work, or unbounded retention.
|
|
44
|
+
- Use a production build for final validation. Measure the important user journey
|
|
45
|
+
before and after performance-sensitive changes under comparable conditions.
|
|
46
|
+
- Do not claim that code is faster because it is shorter, uses memoization, or
|
|
47
|
+
causes fewer apparent renders. Report measured results and the command or
|
|
48
|
+
procedure that produced them; call inconclusive measurements inconclusive.
|
|
49
|
+
|
|
50
|
+
## Adversarial self-review
|
|
51
|
+
|
|
52
|
+
Before handoff, reread the complete diff and try to reject it:
|
|
53
|
+
|
|
54
|
+
1. Can a simpler design preserve the contract with less state, indirection, or
|
|
55
|
+
retained data?
|
|
56
|
+
2. What happens for empty, large, rapid, nested, failing, aborted, and unmounted
|
|
57
|
+
cases?
|
|
58
|
+
3. Are effects cleaned up, async results made stale safely, and subscriptions
|
|
59
|
+
scoped to their consumers?
|
|
60
|
+
4. Does the same code work in development, production, SSR, and hydration modes
|
|
61
|
+
that the project supports?
|
|
62
|
+
5. Did an optimization move work to startup, the server, garbage collection, or
|
|
63
|
+
another component rather than remove it?
|
|
64
|
+
6. Are accessibility, security, diagnostics, and maintainability at least as
|
|
65
|
+
strong as before?
|
|
66
|
+
|
|
67
|
+
Fix findings, rerun the relevant checks, and review the final diff again. Report
|
|
68
|
+
validation, measured performance evidence, improvements made during self-review,
|
|
69
|
+
and any residual risk. Never hide an unverified path behind “all tests pass.”
|
|
@@ -42,11 +42,11 @@ locals, early returns) stays above it.
|
|
|
42
42
|
| `<Suspense fallback={...}>` | `<Suspense>` or `@try { } @pending { }` |
|
|
43
43
|
| Error boundary class | `<ErrorBoundary>` or `@try { } @catch (e) { }` |
|
|
44
44
|
| `forwardRef((props, ref) => ...)` | plain function; `ref` arrives as a prop |
|
|
45
|
-
| `<input onChange={...}>` | `<input onInput={...}>` (native event) |
|
|
46
|
-
| controlled `value={state}` |
|
|
45
|
+
| text `<input onChange={...}>` meaning every edit | `<input onInput={...}>` (native event) |
|
|
46
|
+
| controlled `value={state}` | keep it — React's controlled semantics apply; pair with `onInput` |
|
|
47
47
|
| `className={clsx(...)}` | `class={[...]}` composes clsx-style natively |
|
|
48
|
-
| `useDebugValue(x)` | delete
|
|
49
|
-
| `React.lazy(() => import(...))` |
|
|
48
|
+
| `useDebugValue(x)` | keep or delete — present as an accepted no-op |
|
|
49
|
+
| `React.lazy(() => import(...))` | `lazy()` works as-is (and also accepts a bare component from the loader) |
|
|
50
50
|
| `defaultProps` | parameter defaults / destructuring defaults |
|
|
51
51
|
|
|
52
52
|
## Text holes
|
|
@@ -64,8 +64,12 @@ treated as a renderable (component, element, coerced primitive).
|
|
|
64
64
|
## Hooks
|
|
65
65
|
|
|
66
66
|
The hook API matches React, and there are no rules of hooks: a hook may sit
|
|
67
|
-
behind a condition
|
|
68
|
-
|
|
67
|
+
behind a condition or after an early return, because identity comes from the
|
|
68
|
+
call site, not call order. The one exception is a plain JS loop — a slot-keyed
|
|
69
|
+
hook there is a compile error (every iteration would share one call-site slot);
|
|
70
|
+
use the keyed `@for` directive or extract a child component. Dependency arrays
|
|
71
|
+
may be omitted: the compiler infers them from lexical captures (explicit arrays
|
|
72
|
+
keep React's exact behavior; `null` means every render).
|
|
69
73
|
|
|
70
74
|
```tsx
|
|
71
75
|
export function Panel(props) @{
|
|
@@ -92,6 +96,20 @@ Events are native, delegated DOM events. `onClick`, `onInput`, `onSubmit`,
|
|
|
92
96
|
`onKeyDown` behave exactly like the platform. `onChange` on a text input fires
|
|
93
97
|
on commit (native change), not per keystroke.
|
|
94
98
|
|
|
99
|
+
`OCTANE_NATIVE_TEXT_ONCHANGE` reports likely React-style text-host wiring. Its
|
|
100
|
+
phase-preserving fixes are `onInput` and `onInputCapture`; it does not rewrite the
|
|
101
|
+
event. Keep component/library callbacks named `onChange`, and keep native change on
|
|
102
|
+
select and checkbox/radio hosts. If an uncontrolled text field intentionally saves
|
|
103
|
+
only on commit/blur, keep `onChange` and add the non-serialized host hint:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<input
|
|
107
|
+
defaultValue={draft}
|
|
108
|
+
onChange={(event) => save(event.currentTarget.value)}
|
|
109
|
+
suppressNativeChangeWarning
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
95
113
|
## Refs
|
|
96
114
|
|
|
97
115
|
React 19 style. `ref={cb}` with optional cleanup return, `ref={refObject}`, or
|
|
@@ -3,17 +3,50 @@
|
|
|
3
3
|
Use this when behavior differs from React and you need to decide whether it is a
|
|
4
4
|
bug or by design. Do not "fix" these toward React.
|
|
5
5
|
|
|
6
|
-
## No rules of hooks
|
|
6
|
+
## No rules of hooks — except plain JS loops
|
|
7
7
|
|
|
8
8
|
Hooks are tracked by compiler-assigned call-site slot, not call order. A hook may
|
|
9
|
-
sit behind a condition
|
|
10
|
-
|
|
9
|
+
sit behind a condition or after an early return; code that relies on hook-order
|
|
10
|
+
errors firing does not apply. The one restriction: a slot-keyed hook inside a
|
|
11
|
+
plain JS loop is a **compile error** (every iteration would share the one
|
|
12
|
+
call-site slot). Loop with the keyed `@for` template directive or extract a
|
|
13
|
+
child component instead. `use()` and `useContext` are exempt (call-order /
|
|
14
|
+
context-identity keyed, not slot-keyed).
|
|
11
15
|
|
|
12
|
-
##
|
|
16
|
+
## Dependency arrays are compiler-inferred when omitted
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
Omitting the array on `useEffect`, `useLayoutEffect`, `useInsertionEffect`,
|
|
19
|
+
`useMemo`, `useCallback`, or `useImperativeHandle` does not mean "every render"
|
|
20
|
+
— the compiler derives dependencies from lexical captures, omitting stable hook
|
|
21
|
+
results (state setters/dispatchers, refs, state getters, `useEffectEvent`
|
|
22
|
+
results). Locally declared custom hooks in full-compiled `.tsrx`/`.tsx` modules
|
|
23
|
+
also qualify when they transparently forward a callback and final dependency
|
|
24
|
+
parameter to one of those hooks; plain `.ts`/`.js`, imported/method, or
|
|
25
|
+
transforming wrappers require an explicit list. Explicit arrays keep React's
|
|
26
|
+
exact behavior and are never rewritten; `null` explicitly means run or
|
|
27
|
+
recompute after every render.
|
|
28
|
+
|
|
29
|
+
## State hooks expose a current-state getter
|
|
30
|
+
|
|
31
|
+
`useState` and `useReducer` have a stable third tuple member
|
|
32
|
+
(`[state, update, getState]`) that reads the latest scheduled hook-cell value.
|
|
33
|
+
Ordinary two-item destructures keep the allocation-free React shape.
|
|
34
|
+
|
|
35
|
+
## Controlled inputs match React — on native events
|
|
36
|
+
|
|
37
|
+
Controlled `value`/`checked` follow React's semantics exactly: the prop drives
|
|
38
|
+
the DOM property and reasserts on every commit and after discrete events;
|
|
39
|
+
`defaultValue`/`defaultChecked` are the uncontrolled escape hatch. But there is
|
|
40
|
+
no synthetic event layer: `onInput` is the per-keystroke handler for text
|
|
41
|
+
controls, and native `change` fires on blur/commit. Do not add a synthetic
|
|
42
|
+
`onChange` normalization.
|
|
43
|
+
|
|
44
|
+
The compiler warning `OCTANE_NATIVE_TEXT_ONCHANGE` points out a statically known
|
|
45
|
+
text host with `onChange`/`onChangeCapture` but no usable input handler; development
|
|
46
|
+
runtime checking covers unresolved final props on uncontrolled text controls. This
|
|
47
|
+
is migration guidance only. Deliberate native text commit behavior uses the
|
|
48
|
+
non-serialized `suppressNativeChangeWarning` host hint. Selects, checkables, custom
|
|
49
|
+
elements, and component callbacks remain quiet.
|
|
17
50
|
|
|
18
51
|
## Native delegated events
|
|
19
52
|
|
|
@@ -21,6 +54,16 @@ controlled-input value-reassertion model does not exist and must not be added.
|
|
|
21
54
|
synthetic layer. Timing, bubbling, and `event.target` semantics match the
|
|
22
55
|
platform, not React's wrapper.
|
|
23
56
|
|
|
57
|
+
## Parallel `use()` — no suspense waterfalls
|
|
58
|
+
|
|
59
|
+
The compiler unconditionally memoizes `use()`
|
|
60
|
+
argument creations per call site, starts provably-independent fetches together,
|
|
61
|
+
suspends once per stratum, and prefetches independent descendant fetch trees.
|
|
62
|
+
React runs the same code as a serial waterfall — do not "fix" fetch-start
|
|
63
|
+
timing, batch replay counts, or prefetch behavior toward React. True data
|
|
64
|
+
dependencies stay sequential; unwrap order, hydration-seed order, and rejection
|
|
65
|
+
routing match React.
|
|
66
|
+
|
|
24
67
|
## Keyed reconciler moves differ
|
|
25
68
|
|
|
26
69
|
Reconciliation is LIS-based (minimal DOM moves), not React's `lastPlacedIndex`.
|
|
@@ -28,6 +71,20 @@ The final DOM and survivor node identity are guaranteed identical to React; the
|
|
|
28
71
|
set of physically moved nodes is not. Tests asserting which nodes moved will
|
|
29
72
|
diverge; tests asserting final order and identity will pass.
|
|
30
73
|
|
|
74
|
+
## Synchronous first root mount and root API extensions
|
|
75
|
+
|
|
76
|
+
The first `root.render()` mounts synchronously, so render-then-unmount in one
|
|
77
|
+
outer batch can expose intermediate DOM that React's concurrent root elides.
|
|
78
|
+
`root.render(App, props)` is supported alongside `root.render(<App />)`. A root
|
|
79
|
+
whose managed DOM was externally removed unmounts safely instead of throwing
|
|
80
|
+
the browser's incidental `NotFoundError`.
|
|
81
|
+
|
|
82
|
+
## `lazy()` accepts bare components
|
|
83
|
+
|
|
84
|
+
React's `{ default }` module shape works, and Octane additionally accepts a
|
|
85
|
+
component directly from the loader. Suspense and ViewTransition are ordinary
|
|
86
|
+
components, so wrapping them in `lazy()` is valid; nested lazy wrappers are not.
|
|
87
|
+
|
|
31
88
|
## class / className composes clsx-style
|
|
32
89
|
|
|
33
90
|
Strings, numbers, arrays, objects, and nesting compose into a class string;
|
|
@@ -35,11 +92,10 @@ falsy parts drop out. React coerces an array to `"a,b"`; Octane yields `"a b"`.
|
|
|
35
92
|
|
|
36
93
|
## Not present at all
|
|
37
94
|
|
|
38
|
-
- Class components.
|
|
95
|
+
- Class components (rewrite as function components).
|
|
39
96
|
- Server Components / `'use client'` / `'use server'`.
|
|
40
97
|
- StrictMode double-invoke (renders and effects run once).
|
|
41
98
|
- `forwardRef` (refs are props, React 19 style).
|
|
42
|
-
- `useDebugValue` (shim as no-op).
|
|
43
99
|
- SuspenseList, Profiler, findDOMNode.
|
|
44
100
|
|
|
45
101
|
## Everything else matches
|
|
@@ -47,4 +103,4 @@ falsy parts drop out. React coerces an array to `"a,b"`; Octane yields `"a b"`.
|
|
|
47
103
|
Observable hook, effect, Suspense, and transition semantics match React,
|
|
48
104
|
including effect ordering (child-first on mount, parent-first cleanup on
|
|
49
105
|
deletion), `Object.is` state bailouts, batching, and `useId` stability across
|
|
50
|
-
server render and hydration.
|
|
106
|
+
server render and hydration. `useDebugValue` exists as an accepted no-op.
|
package/skills/setup-ssr.md
CHANGED
|
@@ -4,9 +4,11 @@ Use this when adding server-side rendering to an Octane app.
|
|
|
4
4
|
|
|
5
5
|
## The API
|
|
6
6
|
|
|
7
|
-
The entry points mirror React
|
|
8
|
-
`renderToString` (sync)
|
|
9
|
-
|
|
7
|
+
The entry points mirror React. `octane/server` (`react-dom/server`) has
|
|
8
|
+
`renderToString` (sync), `renderToStaticMarkup` (non-hydratable), and the two
|
|
9
|
+
streaming renderers `renderToPipeableStream` (Node streams) and
|
|
10
|
+
`renderToReadableStream` (web streams); `octane/static` (`react-dom/static`)
|
|
11
|
+
has `prerender` (async, awaits Suspense data). The buffered renderers return
|
|
10
12
|
`{ html, css }`.
|
|
11
13
|
|
|
12
14
|
```ts
|
|
@@ -28,9 +30,22 @@ const { html, css } = await prerender(App, props, {
|
|
|
28
30
|
leaves `@pending` fallbacks in place; use `prerender` to await the data.
|
|
29
31
|
- Options are optional: `nonce` stamps CSP nonces on the emitted inline tags (all
|
|
30
32
|
renderers); `signal` aborts a suspended render with the request and `timeoutMs`
|
|
31
|
-
bounds how long a `use(thenable)` may take to settle (async
|
|
33
|
+
bounds how long a `use(thenable)` may take to settle (async renders; global
|
|
32
34
|
default via `setSsrSuspenseTimeout`); `onError` observes render errors.
|
|
33
35
|
|
|
36
|
+
### Streaming
|
|
37
|
+
|
|
38
|
+
`renderToPipeableStream(App, props?, options?)` returns `{ pipe, abort }`; the
|
|
39
|
+
shell — the full page with `@pending` fallbacks for anything still suspended —
|
|
40
|
+
flushes immediately, then each Suspense boundary streams out of order as a
|
|
41
|
+
hidden segment plus an inline swap script when its data settles.
|
|
42
|
+
`renderToReadableStream` is the same engine over web streams: it resolves with
|
|
43
|
+
a `ReadableStream<Uint8Array>` once the shell is ready and rejects on a shell
|
|
44
|
+
error; consume the stream concurrently rather than awaiting its `allReady`
|
|
45
|
+
promise first. `StreamOptions` extends `RenderOptions` with `onShellReady()`,
|
|
46
|
+
`onShellError(err)`, and `onAllReady()`. `hydrateRoot` adopts streamed-in DOM
|
|
47
|
+
byte-for-byte, including per-boundary `use()` value or rejection seeds.
|
|
48
|
+
|
|
34
49
|
On the client:
|
|
35
50
|
|
|
36
51
|
```ts
|
|
@@ -43,24 +58,35 @@ hydration-stable; the client adopts server DOM instead of rebuilding it.
|
|
|
43
58
|
|
|
44
59
|
## Two integration paths
|
|
45
60
|
|
|
46
|
-
1. **Vite plugin (dev
|
|
47
|
-
from `octane.config.ts`,
|
|
48
|
-
`<!--ssr-head-->` /
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
1. **Vite plugin (routing + dev and production SSR)**: `@octanejs/vite-plugin`
|
|
62
|
+
matches routes from `octane.config.ts`, streams pages with
|
|
63
|
+
`renderToReadableStream()` into `index.html` around `<!--ssr-head-->` /
|
|
64
|
+
`<!--ssr-body-->`, and wires hydration automatically. In production,
|
|
65
|
+
`vite build` emits hashed client assets in `dist/client` plus a
|
|
66
|
+
self-contained SSR server at `dist/server/entry.js` (exports
|
|
67
|
+
`handler`/`nodeHandler`; preview with `octane-preview`);
|
|
68
|
+
`server.render: 'buffered'` switches it to the await-everything `prerender`.
|
|
69
|
+
A deploy adapter prepares the output for a host: `adapter: vercel()` from
|
|
70
|
+
`@octanejs/adapter-vercel` emits Vercel's Build Output API, while
|
|
71
|
+
`adapter: cloudflare()` from `@octanejs/adapter-cloudflare` emits a module
|
|
72
|
+
Worker at `dist/server/worker.js` for Workers Static Assets. Cloudflare apps
|
|
73
|
+
keep a user-owned `wrangler.jsonc` pointing `main` at that Worker and
|
|
74
|
+
`assets.directory` at `dist/client`, with `nodejs_compat` enabled. Leave
|
|
75
|
+
`assets.not_found_handling` unset or `"none"` so navigation misses reach SSR.
|
|
51
76
|
2. **Custom server**: write `entry-server.ts` exporting a function that calls
|
|
52
|
-
`prerender()` (or `renderToString()
|
|
53
|
-
`entry-client.ts` calling
|
|
54
|
-
|
|
55
|
-
before hydrating.
|
|
77
|
+
`prerender()` (or `renderToString()`, or a streaming renderer) and splices
|
|
78
|
+
the result into your HTML template, and `entry-client.ts` calling
|
|
79
|
+
`hydrateRoot`. Serialize app data (for example a dehydrated query-client
|
|
80
|
+
cache) into your own inline JSON script and read it before hydrating.
|
|
56
81
|
|
|
57
82
|
## Data and Suspense on the server
|
|
58
83
|
|
|
59
84
|
`use(promise)` suspends a pass; `prerender()` awaits it and re-renders, so
|
|
60
85
|
`@try { } @pending { }` boundaries resolve to their success arm in the emitted
|
|
61
|
-
HTML
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
HTML, while the streaming renderers flush the fallback in the shell and stream
|
|
87
|
+
the resolved boundary behind it. Resolved values serialize into the seed script
|
|
88
|
+
and hydration consumes them without re-fetching. For query-style data, prefetch
|
|
89
|
+
into a cache before rendering and dehydrate it yourself.
|
|
64
90
|
|
|
65
91
|
## Constraints to remember
|
|
66
92
|
|
|
@@ -68,6 +94,11 @@ rendering and dehydrate it yourself.
|
|
|
68
94
|
`useSyncExternalStore` uses `getServerSnapshot`.
|
|
69
95
|
- Server components must be compiled by the Octane compiler in server mode;
|
|
70
96
|
you cannot feed client-compiled output to the renderers.
|
|
71
|
-
-
|
|
97
|
+
- Hydration adopts the whole tree in one synchronous pass (no selective or
|
|
98
|
+
progressive hydration), and head elements hoisted from inside a streamed
|
|
99
|
+
Suspense boundary are re-created on hydration rather than shipped in the
|
|
100
|
+
stream.
|
|
72
101
|
- Render errors reject the promise unless an `ErrorBoundary`/`@catch` inside
|
|
73
102
|
the tree handles them; map rejections to HTTP status codes in your server.
|
|
103
|
+
With streaming, a recoverable error inside Suspense content keeps the emitted
|
|
104
|
+
fallback and marks only that boundary for client rendering.
|