octane 0.1.4 → 0.1.5
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 +5 -3
- package/dist/compiler/compile.js +118 -8
- package/dist/compiler/hook-deps.js +724 -0
- package/dist/compiler/slot-hooks.js +117 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -0
- package/dist/runtime.d.ts +15 -8
- package/dist/runtime.js +27 -6
- package/dist/runtime.server.d.ts +7 -3
- package/dist/runtime.server.js +28 -3
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
Octane is a fast, TypeScript-first UI framework, and the successor to
|
|
8
8
|
[Inferno](https://github.com/infernojs/inferno). It gives you the React API you
|
|
9
|
-
already know, a compiler that keeps the runtime small and fast,
|
|
10
|
-
hooks,
|
|
11
|
-
and the compiler
|
|
9
|
+
already know, a compiler that keeps the runtime small and fast, no rules of
|
|
10
|
+
hooks, and no hand-maintained dependency arrays in the common case. Omit a hook's
|
|
11
|
+
dependency list and the compiler derives it from the closure; explicit arrays
|
|
12
|
+
retain React semantics, while `null` means every render. This package ships both
|
|
13
|
+
the runtime and compiler, with the compiler exposed at `octane/compiler`.
|
|
12
14
|
|
|
13
15
|
For the full story, see the
|
|
14
16
|
[main README](https://github.com/octanejs/octane#readme).
|
package/dist/compiler/compile.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from '@tsrx/core';
|
|
37
37
|
import { print as esrapPrint } from 'esrap';
|
|
38
38
|
import esrapTsx from 'esrap/languages/tsx';
|
|
39
|
+
import { applyHookDependencies } from './hook-deps.js';
|
|
39
40
|
|
|
40
41
|
// DOM truth tables shared with the client/server runtimes (via constants.ts) —
|
|
41
42
|
// static bakes and dynamic writes MUST agree on which attributes render, under
|
|
@@ -575,7 +576,7 @@ function collectComponentLocals(componentNode) {
|
|
|
575
576
|
* memoising on.
|
|
576
577
|
*
|
|
577
578
|
* Stability sources:
|
|
578
|
-
* - useState / useReducer setters (second
|
|
579
|
+
* - useState / useReducer setters and state getters (second/third slots)
|
|
579
580
|
* - useRef returns (the ref object itself, not .current)
|
|
580
581
|
* - useCallback / useEffectEvent returns
|
|
581
582
|
* - Arrows previously declared in this body whose free vars are themselves
|
|
@@ -605,8 +606,20 @@ function computeStableLocals(statements, componentLocals) {
|
|
|
605
606
|
decl.id.elements[1].type === 'Identifier'
|
|
606
607
|
) {
|
|
607
608
|
stable.add(decl.id.elements[1].name);
|
|
608
|
-
continue;
|
|
609
609
|
}
|
|
610
|
+
// [_, _, getX] = useState(...) — the compiler-generated getter
|
|
611
|
+
// closes over the hook cell and is stable for that cell's lifetime.
|
|
612
|
+
if (
|
|
613
|
+
(callName === 'useState' || callName === 'useReducer') &&
|
|
614
|
+
decl.id.type === 'ArrayPattern' &&
|
|
615
|
+
decl.id.elements &&
|
|
616
|
+
decl.id.elements.length >= 3 &&
|
|
617
|
+
decl.id.elements[2] &&
|
|
618
|
+
decl.id.elements[2].type === 'Identifier'
|
|
619
|
+
) {
|
|
620
|
+
stable.add(decl.id.elements[2].name);
|
|
621
|
+
}
|
|
622
|
+
if (callName === 'useState' || callName === 'useReducer') continue;
|
|
610
623
|
// x = useRef(...) / useCallback(...) / useEffectEvent(...) — the
|
|
611
624
|
// return value is stable for the lifetime of the component.
|
|
612
625
|
if (
|
|
@@ -1430,6 +1443,11 @@ export function compile(source, filename, options) {
|
|
|
1430
1443
|
// Normalize arrow-function components (`const X = () => @{…}`) to
|
|
1431
1444
|
// FunctionDeclaration form so the component pipeline recognizes them.
|
|
1432
1445
|
normalizeArrowComponents(ast);
|
|
1446
|
+
// Omitted dependency lists are compiler-owned: infer reactive captures
|
|
1447
|
+
// before any component splitting/hoisting so every lexical binding is still
|
|
1448
|
+
// visible to the shared TSRX/TSX analysis. Explicit arrays and `null` pass
|
|
1449
|
+
// through untouched.
|
|
1450
|
+
applyHookDependencies(ast, { filename });
|
|
1433
1451
|
const hmrEnabled = !!(options && options.hmr);
|
|
1434
1452
|
// Dev mode: emit dev-only hydration source-location metadata (a per-component
|
|
1435
1453
|
// `__s.locs` table of structured {line,column} keyed by slot index + the module file
|
|
@@ -1897,6 +1915,10 @@ function compileServer(source, filename, options) {
|
|
|
1897
1915
|
// Normalize arrow-function components (`const X = () => @{…}`) to
|
|
1898
1916
|
// FunctionDeclaration form so the component pipeline recognizes them.
|
|
1899
1917
|
normalizeArrowComponents(ast);
|
|
1918
|
+
// Mirror the client transform exactly. Effects are server no-ops, but
|
|
1919
|
+
// useMemo/useCallback execute during SSR and must receive the same inferred
|
|
1920
|
+
// dependency shape as hydration's client compile.
|
|
1921
|
+
applyHookDependencies(ast, { filename });
|
|
1900
1922
|
const ctx = {
|
|
1901
1923
|
filename,
|
|
1902
1924
|
mode: 'server',
|
|
@@ -4152,7 +4174,86 @@ function rejectHookInJsLoop(loop, ctx, componentName) {
|
|
|
4152
4174
|
walk(loop);
|
|
4153
4175
|
}
|
|
4154
4176
|
|
|
4177
|
+
const STATE_GETTER_HELPERS = {
|
|
4178
|
+
useState: '__useStateWithGetter',
|
|
4179
|
+
useReducer: '__useReducerWithGetter',
|
|
4180
|
+
};
|
|
4181
|
+
|
|
4182
|
+
function arrayPatternObservesStateGetter(pattern) {
|
|
4183
|
+
const elements = pattern.elements || [];
|
|
4184
|
+
if (elements[2] != null) return true;
|
|
4185
|
+
// A rest before/at index 2 observes the getter even without an explicit
|
|
4186
|
+
// third binding: `[state, ...rest]` includes both update and getState.
|
|
4187
|
+
for (let i = 0; i <= 2 && i < elements.length; i++) {
|
|
4188
|
+
if (elements[i]?.type === 'RestElement') return true;
|
|
4189
|
+
}
|
|
4190
|
+
return false;
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
function isTransparentStateTupleWrapper(node, child) {
|
|
4194
|
+
if (!node) return false;
|
|
4195
|
+
if (
|
|
4196
|
+
node.type === 'TSAsExpression' ||
|
|
4197
|
+
node.type === 'TSTypeAssertion' ||
|
|
4198
|
+
node.type === 'TSNonNullExpression' ||
|
|
4199
|
+
node.type === 'ParenthesizedExpression' ||
|
|
4200
|
+
node.type === 'ChainExpression'
|
|
4201
|
+
) {
|
|
4202
|
+
return node.expression === child;
|
|
4203
|
+
}
|
|
4204
|
+
return false;
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
// Source-level useState/useReducer tuples have a third getState member. Mark
|
|
4208
|
+
// calls that can observe it so rewriteHookCalls can select a specialized helper;
|
|
4209
|
+
// the ordinary two-item runtime path remains byte-for-byte for proven-dead
|
|
4210
|
+
// getters. Any escaping/ambiguous tuple is conservative and gets the full shape.
|
|
4211
|
+
function markStateGetterUsage(root) {
|
|
4212
|
+
const ancestors = [];
|
|
4213
|
+
function walk(node) {
|
|
4214
|
+
if (node == null || typeof node !== 'object') return;
|
|
4215
|
+
if (Array.isArray(node)) {
|
|
4216
|
+
for (const child of node) walk(child);
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
if (
|
|
4220
|
+
node.type === 'CallExpression' &&
|
|
4221
|
+
node.callee?.type === 'Identifier' &&
|
|
4222
|
+
STATE_GETTER_HELPERS[node.callee.name]
|
|
4223
|
+
) {
|
|
4224
|
+
let child = node;
|
|
4225
|
+
let i = ancestors.length - 1;
|
|
4226
|
+
while (i >= 0 && isTransparentStateTupleWrapper(ancestors[i], child)) {
|
|
4227
|
+
child = ancestors[i--];
|
|
4228
|
+
}
|
|
4229
|
+
const parent = i >= 0 ? ancestors[i] : null;
|
|
4230
|
+
let observed = true;
|
|
4231
|
+
if (parent?.type === 'VariableDeclarator' && parent.init === child) {
|
|
4232
|
+
observed = parent.id.type !== 'ArrayPattern' || arrayPatternObservesStateGetter(parent.id);
|
|
4233
|
+
} else if (parent?.type === 'AssignmentExpression' && parent.right === child) {
|
|
4234
|
+
observed =
|
|
4235
|
+
parent.left.type !== 'ArrayPattern' || arrayPatternObservesStateGetter(parent.left);
|
|
4236
|
+
} else if (parent?.type === 'MemberExpression' && parent.object === child) {
|
|
4237
|
+
const p = parent.property;
|
|
4238
|
+
const index = parent.computed && p?.type === 'Literal' ? Number(p.value) : NaN;
|
|
4239
|
+
observed = index !== 0 && index !== 1;
|
|
4240
|
+
} else if (parent?.type === 'ExpressionStatement') {
|
|
4241
|
+
observed = false;
|
|
4242
|
+
}
|
|
4243
|
+
node._octaneStateGetter = observed;
|
|
4244
|
+
}
|
|
4245
|
+
ancestors.push(node);
|
|
4246
|
+
for (const key in node) {
|
|
4247
|
+
if (AST_WALK_SKIP_KEYS.has(key) || key === '_octaneStateGetter') continue;
|
|
4248
|
+
walk(node[key]);
|
|
4249
|
+
}
|
|
4250
|
+
ancestors.pop();
|
|
4251
|
+
}
|
|
4252
|
+
walk(root);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4155
4255
|
function rewriteHookCalls(node, ctx, componentName) {
|
|
4256
|
+
markStateGetterUsage(node);
|
|
4156
4257
|
return mapAst(node, (n) => {
|
|
4157
4258
|
// A plain JS loop must not contain slot-keyed hook calls — reject before
|
|
4158
4259
|
// slotting. (A template `@for` is also a ForOfStatement; its JSX body tells
|
|
@@ -4183,6 +4284,7 @@ function rewriteHookCalls(node, ctx, componentName) {
|
|
|
4183
4284
|
const isCustom = /^use[A-Z]/.test(name) && name !== 'useContext';
|
|
4184
4285
|
const isServerUse = name === 'use' && ctx.mode === 'server';
|
|
4185
4286
|
if (isBuiltin || isCustom || isServerUse) {
|
|
4287
|
+
const getterHelper = n._octaneStateGetter ? STATE_GETTER_HELPERS[name] : null;
|
|
4186
4288
|
// A builtin hook call site is USER code (the user's own identifier), so
|
|
4187
4289
|
// its import stays bare — EXCEPT compiler-inserted calls (auto-callback's
|
|
4188
4290
|
// `useCallback`), whose callee is renamed to the `_$` alias below so a
|
|
@@ -4190,6 +4292,7 @@ function rewriteHookCalls(node, ctx, componentName) {
|
|
|
4190
4292
|
if (isBuiltin) {
|
|
4191
4293
|
if (n.callee._octaneGenerated) ctx.runtimeNeeded.add(name);
|
|
4192
4294
|
else ctx.userRuntimeNames.add(name);
|
|
4295
|
+
if (getterHelper !== null) ctx.runtimeNeeded.add(getterHelper);
|
|
4193
4296
|
}
|
|
4194
4297
|
if (isServerUse) ctx.userRuntimeNames.add('use');
|
|
4195
4298
|
const debug = isServerUse
|
|
@@ -4226,9 +4329,12 @@ function rewriteHookCalls(node, ctx, componentName) {
|
|
|
4226
4329
|
}
|
|
4227
4330
|
return {
|
|
4228
4331
|
...n,
|
|
4229
|
-
callee:
|
|
4230
|
-
|
|
4231
|
-
|
|
4332
|
+
callee:
|
|
4333
|
+
getterHelper !== null
|
|
4334
|
+
? { type: 'Identifier', name: rtAlias(getterHelper) }
|
|
4335
|
+
: n.callee._octaneGenerated
|
|
4336
|
+
? { type: 'Identifier', name: rtAlias(name) }
|
|
4337
|
+
: n.callee,
|
|
4232
4338
|
arguments: [...args, { type: 'Identifier', name: symVar }],
|
|
4233
4339
|
};
|
|
4234
4340
|
}
|
|
@@ -6315,7 +6421,7 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
6315
6421
|
const chv = `_b.${bag.letter(`_chv$${cc.id}`)}`;
|
|
6316
6422
|
pushAfter(
|
|
6317
6423
|
cc.id,
|
|
6318
|
-
` { const _v = (${cc.valueExpr}); if (${chp} !== _v) { ${chp} = _v; const _t = ${chv}; if (_t != null &&
|
|
6424
|
+
` { const _v = (${cc.valueExpr}); const _o = _v !== null && (typeof _v === 'object' || typeof _v === 'function'); if (_o || ${chp} !== _v) { ${chp} = _v; const _t = ${chv}; if (_t != null && !_o && _v !== null) _$setText(_t, _v); else ${chv} = _$childTextHole(__s, ${slotIndex}, ${hostExpr}, _v, _t); } }`,
|
|
6319
6425
|
);
|
|
6320
6426
|
continue;
|
|
6321
6427
|
}
|
|
@@ -6338,7 +6444,11 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
6338
6444
|
// unchanged-skippable primitive already backed by a text node, do a direct
|
|
6339
6445
|
// `setText` — exactly like a `.tsrx` `{… as string}` text binding. Objects /
|
|
6340
6446
|
// functions (component / element / array), the first render, and mode
|
|
6341
|
-
// switches go through `textHole` → the full `childSlot
|
|
6447
|
+
// switches go through `textHole` → the full `childSlot` — INCLUDING when
|
|
6448
|
+
// the value is identity-UNCHANGED: only childSlot's bail path refreshes
|
|
6449
|
+
// changed-context consumers below (a stable `{children}` passthrough under
|
|
6450
|
+
// a re-rendering Provider), so an inline identity skip would strand them.
|
|
6451
|
+
// Only unchanged primitives/null (no consumers possible) skip the call.
|
|
6342
6452
|
ctx.runtimeNeeded.add('setText');
|
|
6343
6453
|
ctx.runtimeNeeded.add('textHole');
|
|
6344
6454
|
// When the slot has its OWN `<!>` placeholder, tell textHole/childSlot to
|
|
@@ -6348,7 +6458,7 @@ function planJsx(jsxNodesRaw, ctx, componentName, inlinedSubs, parentNs = 'html'
|
|
|
6348
6458
|
const chv = `_b.${bag.letter(`_chv$${cc.id}`)}`;
|
|
6349
6459
|
pushAfter(
|
|
6350
6460
|
cc.id,
|
|
6351
|
-
` { const _v = (${cc.valueExpr}); if (${chp} !== _v) { ${chp} = _v; const _t = ${chv}; if (_t != null &&
|
|
6461
|
+
` { const _v = (${cc.valueExpr}); const _o = _v !== null && (typeof _v === 'object' || typeof _v === 'function'); if (_o || ${chp} !== _v) { ${chp} = _v; const _t = ${chv}; if (_t != null && !_o && _v !== null) _$setText(_t, _v); else ${chv} = _$textHole(__s, ${slotIndex}, ${hostExpr}, _v, ${anchorExpr}${ownEndArg}); } }`,
|
|
6352
6462
|
);
|
|
6353
6463
|
continue;
|
|
6354
6464
|
}
|