@octanejs/mcp-server 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/LICENSE +21 -0
- package/README.md +129 -0
- package/package.json +41 -0
- package/skills/bridge-react-package.md +117 -0
- package/skills/migrate-react-component.md +98 -0
- package/skills/react-divergences.md +50 -0
- package/skills/setup-ssr.md +73 -0
- package/src/bridge.js +314 -0
- package/src/bridge.test.js +154 -0
- package/src/index.js +428 -0
- package/src/index.test.js +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @octanejs/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for agents working with [Octane](https://github.com/octanejs/octane).
|
|
4
|
+
|
|
5
|
+
It serves two audiences:
|
|
6
|
+
|
|
7
|
+
- **Octane users** (any project): skills and tools for bridging React packages
|
|
8
|
+
to Octane, migrating React components to `.tsrx`, understanding Octane's
|
|
9
|
+
intentional divergences from React, and setting up SSR. These work anywhere;
|
|
10
|
+
the skills ship inside this package.
|
|
11
|
+
- **Octane maintainers** (the octane monorepo): repo triage, validation
|
|
12
|
+
planning, benchmark and React-test-port automation. These tools register
|
|
13
|
+
only when the server detects an octane monorepo checkout at its root.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @octanejs/mcp-server
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For local development inside the octane repository:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm --filter @octanejs/mcp-server start
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## MCP transport
|
|
28
|
+
|
|
29
|
+
The server uses stdio transport.
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"octane": {
|
|
35
|
+
"command": "octane-mcp-server"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Set `OCTANE_REPO_ROOT` to point the server at an octane checkout (enables the
|
|
42
|
+
maintainer tools):
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"octane": {
|
|
48
|
+
"command": "octane-mcp-server",
|
|
49
|
+
"env": {
|
|
50
|
+
"OCTANE_REPO_ROOT": "/path/to/octane"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Tools (always available)
|
|
58
|
+
|
|
59
|
+
### `octane_bridge_react_package`
|
|
60
|
+
|
|
61
|
+
Scans a React package (by name from `node_modules`, or any source directory by
|
|
62
|
+
path) for React API usage and returns an Octane compatibility report: which
|
|
63
|
+
APIs map one-to-one, which need rewrites (`forwardRef`, `useDebugValue`,
|
|
64
|
+
`lazy`, class components, synthetic `onChange`), whether a framework-agnostic
|
|
65
|
+
core can be reused verbatim, whether an official `@octanejs/*` binding already
|
|
66
|
+
exists, an overall verdict (`bridgeable`, `bridgeable-with-rewrites`,
|
|
67
|
+
`needs-rework`), and a step-by-step plan.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{ "package": "jotai", "projectRoot": "/path/to/my-app" }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `octane_bindings`
|
|
74
|
+
|
|
75
|
+
Returns the map of React packages with maintained `@octanejs/*` ports
|
|
76
|
+
(zustand, query, motion, stylex, router, lexical, floating-ui, radix).
|
|
77
|
+
|
|
78
|
+
### `octane_skill`
|
|
79
|
+
|
|
80
|
+
Returns a skill by name. Bundled skills (shipped with this package):
|
|
81
|
+
|
|
82
|
+
- `bridge-react-package` — the full workflow for porting a React library.
|
|
83
|
+
- `migrate-react-component` — React JSX to `.tsrx` conversion reference.
|
|
84
|
+
- `react-divergences` — Octane's intentional differences from React.
|
|
85
|
+
- `setup-ssr` — server rendering and hydration setup.
|
|
86
|
+
|
|
87
|
+
When running inside the octane monorepo, the maintainer skills from
|
|
88
|
+
`.ai/skills` are also available: `react-library-port`, `bug-hunter`,
|
|
89
|
+
`create-a-pr`, `handle-issue`, `octane-core-extend`, `triage`,
|
|
90
|
+
`performance-audit`.
|
|
91
|
+
|
|
92
|
+
## Tools (octane monorepo only)
|
|
93
|
+
|
|
94
|
+
### `octane_project_map`
|
|
95
|
+
|
|
96
|
+
Returns `.ai/project-map.md` with package layout, authoritative sources,
|
|
97
|
+
invariants, and validation commands.
|
|
98
|
+
|
|
99
|
+
### `octane_triage_paths`
|
|
100
|
+
|
|
101
|
+
Classifies repository-relative paths by Octane area (compiler, core runtime,
|
|
102
|
+
SSR, ecosystem binding, mcp-server, benchmark, docs, RuleSync source).
|
|
103
|
+
|
|
104
|
+
### `octane_validate_plan`
|
|
105
|
+
|
|
106
|
+
Recommends validation commands for changed paths and task kind.
|
|
107
|
+
|
|
108
|
+
### `octane_scaffold_react_port`
|
|
109
|
+
|
|
110
|
+
Runs `scripts/scaffold-react-port.mjs` for an upstream React test file and
|
|
111
|
+
optionally writes the generated Vitest skeleton to an output file.
|
|
112
|
+
|
|
113
|
+
### `octane_benchmark`
|
|
114
|
+
|
|
115
|
+
Runs a known benchmark workspace (`news`, `js-framework`, `recursive-context`,
|
|
116
|
+
`signal-favoring`, `dbmon`) or all benchmarks.
|
|
117
|
+
|
|
118
|
+
### `octane_issue_context`
|
|
119
|
+
|
|
120
|
+
Uses the GitHub CLI (`gh`) to fetch an issue and returns structured issue
|
|
121
|
+
context plus lightweight triage hints. Requires `gh` installed and
|
|
122
|
+
authenticated.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm --filter @octanejs/mcp-server test
|
|
128
|
+
pnpm --filter @octanejs/mcp-server start
|
|
129
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/mcp-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server exposing Octane repository automation for coding agents.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Dominic Gannaway",
|
|
9
|
+
"email": "dg@domgan.com"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
14
|
+
"directory": "packages/octane-mcp-server"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"skills",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"octane-mcp-server": "src/index.js"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.21.0",
|
|
32
|
+
"zod": "^4.4.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vitest": "^4.1.9"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"start": "node src/index.js",
|
|
39
|
+
"test": "cd ../.. && vitest run --project octane-mcp-server"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Skill: Bridge a React package to Octane
|
|
2
|
+
|
|
3
|
+
Use this when a user wants a React ecosystem library to work in their Octane app.
|
|
4
|
+
|
|
5
|
+
## Check for an official binding first
|
|
6
|
+
|
|
7
|
+
These libraries already have maintained Octane ports. Install the binding instead
|
|
8
|
+
of bridging by hand:
|
|
9
|
+
|
|
10
|
+
| React package | Octane binding |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `zustand` | `@octanejs/zustand` |
|
|
13
|
+
| `@tanstack/react-query` | `@octanejs/query` |
|
|
14
|
+
| `framer-motion` / `motion` | `@octanejs/motion` |
|
|
15
|
+
| `@stylexjs/stylex` | `@octanejs/stylex` |
|
|
16
|
+
| `react-router` / `react-router-dom` | `@octanejs/router` |
|
|
17
|
+
| `@lexical/react` | `@octanejs/lexical` |
|
|
18
|
+
| `@floating-ui/react` | `@octanejs/floating-ui` |
|
|
19
|
+
| `radix-ui` | `@octanejs/radix` |
|
|
20
|
+
|
|
21
|
+
For anything else, run the `octane_bridge_react_package` tool to get a scan of the
|
|
22
|
+
package's React API usage and a tailored plan, then follow the workflow below.
|
|
23
|
+
|
|
24
|
+
## Mental model
|
|
25
|
+
|
|
26
|
+
Octane is a compiler framework, not a runtime VDOM. Two consequences drive
|
|
27
|
+
everything:
|
|
28
|
+
|
|
29
|
+
1. Compiled React JSX (`jsx()` / `createElement` trees) cannot render on Octane.
|
|
30
|
+
Components must be authored in `.tsrx` (or `.tsx` compiled by the Octane
|
|
31
|
+
compiler).
|
|
32
|
+
2. Every Octane hook call is bound to a compiler-injected slot. A slotless
|
|
33
|
+
`useState(0)` coming from a React build throws immediately.
|
|
34
|
+
|
|
35
|
+
So a bridge never means "run the React package unchanged". It means:
|
|
36
|
+
|
|
37
|
+
- Reuse the package's framework-agnostic core verbatim (store, query client,
|
|
38
|
+
state machine, form engine). Code with zero `react` imports runs on Octane
|
|
39
|
+
as-is.
|
|
40
|
+
- Re-implement the thin React binding layer (usually a handful of hooks) against
|
|
41
|
+
Octane's identically named hooks.
|
|
42
|
+
- Re-author any shipped JSX components in `.tsrx`.
|
|
43
|
+
|
|
44
|
+
## Workflow
|
|
45
|
+
|
|
46
|
+
1. **Classify the library.** Find its vanilla core (`zustand/vanilla`,
|
|
47
|
+
`@tanstack/query-core`, `jotai/vanilla`, `xstate`, `@floating-ui/dom`, a
|
|
48
|
+
`*-core` dependency, or a pure internal module). Identify the React surface:
|
|
49
|
+
hooks, components, providers, portals, refs.
|
|
50
|
+
|
|
51
|
+
2. **Map the React APIs.** Same-name and same-semantics in Octane: `useState`,
|
|
52
|
+
`useReducer`, `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useMemo`,
|
|
53
|
+
`useCallback`, `useRef`, `useContext`, `useId`, `useImperativeHandle`,
|
|
54
|
+
`useSyncExternalStore` (full React 19 shape, including `getServerSnapshot`),
|
|
55
|
+
`useTransition`, `useDeferredValue`, `useActionState`, `useOptimistic`,
|
|
56
|
+
`useEffectEvent`, `use`, `startTransition`, `memo`, `createContext`,
|
|
57
|
+
`Suspense`, `createPortal`, `flushSync`, `createRoot`, `hydrateRoot`.
|
|
58
|
+
Everything imports from `octane` (no separate `react-dom`).
|
|
59
|
+
|
|
60
|
+
3. **Handle the gaps:**
|
|
61
|
+
- `forwardRef`: does not exist. Accept `ref` as a normal prop (React 19
|
|
62
|
+
style) and drop the wrapper.
|
|
63
|
+
- `useDebugValue`: shim as a no-op.
|
|
64
|
+
- `lazy`: use dynamic `import()` plus `use()` inside a `Suspense` boundary.
|
|
65
|
+
- Class components: rewrite as function components. Error boundary classes
|
|
66
|
+
become `<ErrorBoundary>` or the `@try { } @catch (e) { }` directive.
|
|
67
|
+
- Synthetic `onChange` on text inputs: use native `onInput`. Octane events
|
|
68
|
+
are native and delegated.
|
|
69
|
+
- Controlled inputs: Octane inputs are uncontrolled and native; `value` and
|
|
70
|
+
`checked` are plain attributes. Port controlled-input logic to
|
|
71
|
+
read-from-DOM plus explicit writes, or keep state in the store and write
|
|
72
|
+
the attribute on change.
|
|
73
|
+
- StrictMode double-invoke: does not exist; delete test expectations that
|
|
74
|
+
count double renders.
|
|
75
|
+
|
|
76
|
+
4. **Custom hooks in plain `.ts` files.** Octane's compiler auto-slots hook
|
|
77
|
+
calls in files it compiles. A binding published as plain `.ts` that calls
|
|
78
|
+
hooks internally must forward the caller's slot: accept a trailing `slot`
|
|
79
|
+
argument and derive stable child slots per call site. The convention used by
|
|
80
|
+
the official bindings:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { useMemo, useRef } from 'octane';
|
|
84
|
+
|
|
85
|
+
export function subSlot(slot: symbol | undefined, tag: string) {
|
|
86
|
+
return slot !== undefined ? Symbol.for((slot.description ?? '') + ':' + tag) : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function useControllableState(opts, slot?: symbol) {
|
|
90
|
+
const valueRef = useRef(opts.defaultValue, subSlot(slot, 'value'));
|
|
91
|
+
return useMemo(() => build(valueRef), [opts.value], subSlot(slot, 'memo'));
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Callers compiled from `.tsrx`/`.tsx` pass their injected slot automatically as
|
|
96
|
+
the trailing argument when the hook file itself is excluded from the compiler's
|
|
97
|
+
auto-slotting pass. The simpler alternative: keep the binding in compiled
|
|
98
|
+
files so slots are injected for you.
|
|
99
|
+
|
|
100
|
+
5. **Re-author shipped components in `.tsrx`.** `props.children` works, refs are
|
|
101
|
+
props, lists use `@for (const x of xs; key x.id) { }`, conditionals use
|
|
102
|
+
`@if`, dynamic text holes use `{expr as string}` unless the expression is
|
|
103
|
+
provably a string.
|
|
104
|
+
|
|
105
|
+
6. **Validate.** Drive real DOM events against the bridged binding and, where
|
|
106
|
+
possible, run the same fixture against the React original and compare
|
|
107
|
+
rendered HTML after each step. Also test what HTML comparison cannot see:
|
|
108
|
+
render counts, subscription add/remove, effect ordering, ref lifecycle.
|
|
109
|
+
|
|
110
|
+
## Verdict guide for the scan tool
|
|
111
|
+
|
|
112
|
+
- `bridgeable`: only same-name hooks used; a mechanical rename of imports to
|
|
113
|
+
`octane` plus a `.tsrx` re-author of components is enough.
|
|
114
|
+
- `bridgeable-with-rewrites`: needs the `forwardRef` / `useDebugValue` / `lazy` /
|
|
115
|
+
event rewrites above, but no architectural blockers.
|
|
116
|
+
- `needs-rework`: class components, `renderToPipeableStream`, `findDOMNode`, or
|
|
117
|
+
React internals. Bridge the core, redesign the binding.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Skill: Migrate a React component to Octane
|
|
2
|
+
|
|
3
|
+
Use this when converting React component source (JSX/TSX) into an Octane `.tsrx`
|
|
4
|
+
component.
|
|
5
|
+
|
|
6
|
+
## Imports
|
|
7
|
+
|
|
8
|
+
Everything comes from `octane`. Replace `react`, `react-dom`, and
|
|
9
|
+
`react-dom/client` imports:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { useState, useEffect, createPortal, createRoot, hydrateRoot } from 'octane';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Component shape
|
|
16
|
+
|
|
17
|
+
Any function used at a `<Foo/>` site is a component. Two equivalent forms:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
export function Counter() @{
|
|
21
|
+
const [count, setCount] = useState(0);
|
|
22
|
+
<button onClick={() => setCount(count + 1)}>{'Count: ' + count}</button>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Counter() {
|
|
26
|
+
const [count, setCount] = useState(0);
|
|
27
|
+
return <button onClick={() => setCount(count + 1)}>{'Count: ' + count}</button>;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The `@{ ... }` body must end with exactly one output node. Setup code (hooks,
|
|
32
|
+
locals, early returns) stays above it.
|
|
33
|
+
|
|
34
|
+
## Conversion table
|
|
35
|
+
|
|
36
|
+
| React pattern | Octane pattern |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `items.map(x => <li key={x.id}>...` | `@for (const x of items; key x.id) { <li>... }` with optional `@empty { }` |
|
|
39
|
+
| `cond ? <A/> : <B/>` in JSX | `@if (cond) { <A/> } @else { <B/> }` |
|
|
40
|
+
| `{cond && <A/>}` | `@if (cond) { <A/> }` |
|
|
41
|
+
| switch on a value | `@switch (v) { @case (a) { } @default { } }` |
|
|
42
|
+
| `<Suspense fallback={...}>` | `<Suspense>` or `@try { } @pending { }` |
|
|
43
|
+
| Error boundary class | `<ErrorBoundary>` or `@try { } @catch (e) { }` |
|
|
44
|
+
| `forwardRef((props, ref) => ...)` | plain function; `ref` arrives as a prop |
|
|
45
|
+
| `<input onChange={...}>` | `<input onInput={...}>` (native event) |
|
|
46
|
+
| controlled `value={state}` | uncontrolled; `value` is a plain attribute, read the DOM in handlers |
|
|
47
|
+
| `className={clsx(...)}` | `class={[...]}` composes clsx-style natively |
|
|
48
|
+
| `useDebugValue(x)` | delete it |
|
|
49
|
+
| `React.lazy(() => import(...))` | dynamic `import()` + `use()` under Suspense |
|
|
50
|
+
| `defaultProps` | parameter defaults / destructuring defaults |
|
|
51
|
+
|
|
52
|
+
## Text holes
|
|
53
|
+
|
|
54
|
+
A dynamic text hole needs `{expr as string}` unless the compiler can prove the
|
|
55
|
+
expression is a string (string literal, template literal, `+` concatenation with
|
|
56
|
+
a string, or a tracked local). A bare `{expr}` that is not provably a string is
|
|
57
|
+
treated as a renderable (component, element, coerced primitive).
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<p>{'Elapsed: ' + seconds}</p>
|
|
61
|
+
<p>{seconds as string}</p>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Hooks
|
|
65
|
+
|
|
66
|
+
The hook API matches React, and there are no rules of hooks: a hook may sit
|
|
67
|
+
behind a condition, after an early return, or in a loop, because identity comes
|
|
68
|
+
from the call site, not call order.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
export function Panel(props) @{
|
|
72
|
+
const [n, setN] = useState(0);
|
|
73
|
+
if (props.hidden) return;
|
|
74
|
+
useEffect(() => log(n), [n]);
|
|
75
|
+
<button onClick={() => setN(n + 1)}>{'count: ' + n}</button>
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## What does not port
|
|
80
|
+
|
|
81
|
+
- Class components (rewrite as functions).
|
|
82
|
+
- StrictMode double-invoke expectations (there is no double render; delete
|
|
83
|
+
render-count workarounds).
|
|
84
|
+
- Server Components / `'use client'` directives.
|
|
85
|
+
- Synthetic event pooling or `e.persist()` (events are native).
|
|
86
|
+
- `React.Children` traversal over arbitrary VDOM (children are descriptors, not
|
|
87
|
+
a VDOM tree; prefer explicit props over children introspection).
|
|
88
|
+
|
|
89
|
+
## Events
|
|
90
|
+
|
|
91
|
+
Events are native, delegated DOM events. `onClick`, `onInput`, `onSubmit`,
|
|
92
|
+
`onKeyDown` behave exactly like the platform. `onChange` on a text input fires
|
|
93
|
+
on commit (native change), not per keystroke.
|
|
94
|
+
|
|
95
|
+
## Refs
|
|
96
|
+
|
|
97
|
+
React 19 style. `ref={cb}` with optional cleanup return, `ref={refObject}`, or
|
|
98
|
+
an array `ref={[a, b]}` to compose. No `forwardRef` anywhere.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Skill: Octane's intentional divergences from React
|
|
2
|
+
|
|
3
|
+
Use this when behavior differs from React and you need to decide whether it is a
|
|
4
|
+
bug or by design. Do not "fix" these toward React.
|
|
5
|
+
|
|
6
|
+
## No rules of hooks
|
|
7
|
+
|
|
8
|
+
Hooks are tracked by compiler-assigned call-site slot, not call order. A hook may
|
|
9
|
+
sit behind a condition, after an early return, or in a loop. Code that relies on
|
|
10
|
+
hook-order errors firing does not apply.
|
|
11
|
+
|
|
12
|
+
## No controlled components, no synthetic onChange
|
|
13
|
+
|
|
14
|
+
`value` and `checked` are plain attributes; inputs are uncontrolled and native.
|
|
15
|
+
There is no per-keystroke synthetic `onChange`; use native `onInput`. React's
|
|
16
|
+
controlled-input value-reassertion model does not exist and must not be added.
|
|
17
|
+
|
|
18
|
+
## Native delegated events
|
|
19
|
+
|
|
20
|
+
`onClick`, `onInput`, `onSubmit` etc. are real DOM events via delegation, not a
|
|
21
|
+
synthetic layer. Timing, bubbling, and `event.target` semantics match the
|
|
22
|
+
platform, not React's wrapper.
|
|
23
|
+
|
|
24
|
+
## Keyed reconciler moves differ
|
|
25
|
+
|
|
26
|
+
Reconciliation is LIS-based (minimal DOM moves), not React's `lastPlacedIndex`.
|
|
27
|
+
The final DOM and survivor node identity are guaranteed identical to React; the
|
|
28
|
+
set of physically moved nodes is not. Tests asserting which nodes moved will
|
|
29
|
+
diverge; tests asserting final order and identity will pass.
|
|
30
|
+
|
|
31
|
+
## class / className composes clsx-style
|
|
32
|
+
|
|
33
|
+
Strings, numbers, arrays, objects, and nesting compose into a class string;
|
|
34
|
+
falsy parts drop out. React coerces an array to `"a,b"`; Octane yields `"a b"`.
|
|
35
|
+
|
|
36
|
+
## Not present at all
|
|
37
|
+
|
|
38
|
+
- Class components.
|
|
39
|
+
- Server Components / `'use client'` / `'use server'`.
|
|
40
|
+
- StrictMode double-invoke (renders and effects run once).
|
|
41
|
+
- `forwardRef` (refs are props, React 19 style).
|
|
42
|
+
- `useDebugValue` (shim as no-op).
|
|
43
|
+
- SuspenseList, Profiler, findDOMNode.
|
|
44
|
+
|
|
45
|
+
## Everything else matches
|
|
46
|
+
|
|
47
|
+
Observable hook, effect, Suspense, and transition semantics match React,
|
|
48
|
+
including effect ordering (child-first on mount, parent-first cleanup on
|
|
49
|
+
deletion), `Object.is` state bailouts, batching, and `useId` stability across
|
|
50
|
+
server render and hydration.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Skill: Set up Octane SSR and hydration
|
|
2
|
+
|
|
3
|
+
Use this when adding server-side rendering to an Octane app.
|
|
4
|
+
|
|
5
|
+
## The API
|
|
6
|
+
|
|
7
|
+
The entry points mirror React: `octane/server` (`react-dom/server`) has
|
|
8
|
+
`renderToString` (sync) and `renderToStaticMarkup`; `octane/static`
|
|
9
|
+
(`react-dom/static`) has `prerender` (async, awaits Suspense data). All return
|
|
10
|
+
`{ html, css }`.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { prerender } from 'octane/static';
|
|
14
|
+
|
|
15
|
+
const { html, css } = await prerender(App, props, {
|
|
16
|
+
signal: request.signal,
|
|
17
|
+
nonce: cspNonce,
|
|
18
|
+
timeoutMs: 5000,
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `html`: rendered markup with hydration markers, plus an inline suspense seed
|
|
23
|
+
script when anything resolved. Hoisted `<title>/<meta>/<link>` fold in
|
|
24
|
+
(spliced into `<head>` if present, else prepended).
|
|
25
|
+
- `css`: deduped `<style data-octane>` tags from scoped styles; place inside
|
|
26
|
+
`<head>`.
|
|
27
|
+
- Use `renderToString` (from `octane/server`) for a single synchronous pass that
|
|
28
|
+
leaves `@pending` fallbacks in place; use `prerender` to await the data.
|
|
29
|
+
- Options are optional: `nonce` stamps CSP nonces on the emitted inline tags (all
|
|
30
|
+
renderers); `signal` aborts a suspended render with the request and `timeoutMs`
|
|
31
|
+
bounds how long a `use(thenable)` may take to settle (async `prerender`; global
|
|
32
|
+
default via `setSsrSuspenseTimeout`); `onError` observes render errors.
|
|
33
|
+
|
|
34
|
+
On the client:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { hydrateRoot } from 'octane';
|
|
38
|
+
hydrateRoot(document.getElementById('app')!, App, props);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Pass the same component and props on both sides. `useId` and scoped styles are
|
|
42
|
+
hydration-stable; the client adopts server DOM instead of rebuilding it.
|
|
43
|
+
|
|
44
|
+
## Two integration paths
|
|
45
|
+
|
|
46
|
+
1. **Vite plugin (dev SSR + routing)**: `@octanejs/vite-plugin` matches routes
|
|
47
|
+
from `octane.config.ts`, renders pages into `index.html` at
|
|
48
|
+
`<!--ssr-head-->` / `<!--ssr-body-->`, and wires hydration automatically.
|
|
49
|
+
Production server output is not generated yet; for production SSR today use
|
|
50
|
+
path 2.
|
|
51
|
+
2. **Custom server**: write `entry-server.ts` exporting a function that calls
|
|
52
|
+
`prerender()` (or `renderToString()`) and splices the result into your HTML template, and
|
|
53
|
+
`entry-client.ts` calling `hydrateRoot`. Serialize app data (for example a
|
|
54
|
+
dehydrated query-client cache) into your own inline JSON script and read it
|
|
55
|
+
before hydrating.
|
|
56
|
+
|
|
57
|
+
## Data and Suspense on the server
|
|
58
|
+
|
|
59
|
+
`use(promise)` suspends a pass; `prerender()` awaits it and re-renders, so
|
|
60
|
+
`@try { } @pending { }` boundaries resolve to their success arm in the emitted
|
|
61
|
+
HTML. Resolved values serialize into the seed script and hydration consumes
|
|
62
|
+
them without re-fetching. For query-style data, prefetch into a cache before
|
|
63
|
+
rendering and dehydrate it yourself.
|
|
64
|
+
|
|
65
|
+
## Constraints to remember
|
|
66
|
+
|
|
67
|
+
- Effects never run on the server; state hooks return initial values;
|
|
68
|
+
`useSyncExternalStore` uses `getServerSnapshot`.
|
|
69
|
+
- Server components must be compiled by the Octane compiler in server mode;
|
|
70
|
+
you cannot feed client-compiled output to the renderers.
|
|
71
|
+
- Output is buffered, not streamed: send it as one response.
|
|
72
|
+
- Render errors reject the promise unless an `ErrorBoundary`/`@catch` inside
|
|
73
|
+
the tree handles them; map rejections to HTTP status codes in your server.
|