octane 0.1.21 → 0.1.23
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/dist/compiler/compile.js +165 -6
- package/dist/runtime.js +5 -2
- package/package.json +4 -1
package/dist/compiler/compile.js
CHANGED
|
@@ -3936,6 +3936,78 @@ function containsAutoMemoUnsafeStructure(stmts) {
|
|
|
3936
3936
|
return found;
|
|
3937
3937
|
}
|
|
3938
3938
|
|
|
3939
|
+
/**
|
|
3940
|
+
* Bundling MOVES an argument's evaluation out of the arrow body and into the
|
|
3941
|
+
* component body, so it now runs on mount and on every update instead of once
|
|
3942
|
+
* per event. That rewrite is only sound for an expression that is
|
|
3943
|
+
* side-effect-free and O(1)-ish to evaluate: `() => setData(makeData(1000))`
|
|
3944
|
+
* otherwise rebuilds the whole dataset on every unrelated render, and the user
|
|
3945
|
+
* never sees a call they can attribute to the click that did not happen.
|
|
3946
|
+
*
|
|
3947
|
+
* The whitelist is deliberately narrower than "pure", for two reasons.
|
|
3948
|
+
*
|
|
3949
|
+
* A fresh identity per evaluation defeats the runtime arg diff the bundle
|
|
3950
|
+
* exists for — an `ArrayExpression`/`ObjectExpression`/arrow arg can never
|
|
3951
|
+
* compare equal, so it would pay a per-render allocation to skip nothing.
|
|
3952
|
+
* Regex is rejected for the same reason `isInvariantLiteral` rejects it.
|
|
3953
|
+
*
|
|
3954
|
+
* The line is drawn at VALUE STABILITY, not at "provably pure". An accepted
|
|
3955
|
+
* expression must yield at render time what it would have yielded at click
|
|
3956
|
+
* time, and must not do unbounded or author-visible work to get there. A
|
|
3957
|
+
* property read can reach a getter and `a + b` can reach `valueOf` — but that
|
|
3958
|
+
* is the standing premise of the optimization, not a new risk: `select(row.id)`
|
|
3959
|
+
* is the shape it was built for, and refusing property reads would leave it
|
|
3960
|
+
* with nothing to optimize. Refusing arithmetic while accepting `row.id` would
|
|
3961
|
+
* draw the same line in two places, so both stay.
|
|
3962
|
+
*
|
|
3963
|
+
* What cannot stay are the expressions that break value stability outright:
|
|
3964
|
+
* a CALL does unbounded work and can be observed happening (`makeData(1000)`
|
|
3965
|
+
* rebuilt a whole dataset per render), a fresh array/object/regex allocates an
|
|
3966
|
+
* identity that can never compare equal, and an assignment or `++` mutates.
|
|
3967
|
+
*
|
|
3968
|
+
* `.current` is rejected because a ref genuinely returns the WRONG VALUE here,
|
|
3969
|
+
* not merely an early one: `queueRefAttach` runs AFTER the mount that reads it,
|
|
3970
|
+
* so a hoisted `ref.current` hands the first click the `null` it held before
|
|
3971
|
+
* the ref was attached. Computed members fail closed for the same reason —
|
|
3972
|
+
* `ref[key]` can spell `current` without saying so, and
|
|
3973
|
+
* `isAutoMemoCalculationDependency` already refuses every computed key.
|
|
3974
|
+
*/
|
|
3975
|
+
function isDeferralSafeBundleArg(node) {
|
|
3976
|
+
const value = unwrapTsExpr(node);
|
|
3977
|
+
if (!value) return false;
|
|
3978
|
+
switch (value.type) {
|
|
3979
|
+
case 'Literal':
|
|
3980
|
+
return isInvariantLiteral(value);
|
|
3981
|
+
case 'Identifier':
|
|
3982
|
+
return true;
|
|
3983
|
+
case 'ChainExpression':
|
|
3984
|
+
return isDeferralSafeBundleArg(value.expression);
|
|
3985
|
+
case 'MemberExpression':
|
|
3986
|
+
// Computed keys fail closed: the key is only known at runtime, so
|
|
3987
|
+
// `ref[k]` can reach `.current` without naming it.
|
|
3988
|
+
if (value.computed) return false;
|
|
3989
|
+
if (value.property?.name === 'current') return false;
|
|
3990
|
+
return isDeferralSafeBundleArg(value.object);
|
|
3991
|
+
case 'TemplateLiteral':
|
|
3992
|
+
// A fresh string still compares by VALUE, so the arg diff works.
|
|
3993
|
+
return (value.expressions || []).every(isDeferralSafeBundleArg);
|
|
3994
|
+
case 'UnaryExpression':
|
|
3995
|
+
// `delete` mutates; the rest only read their operand.
|
|
3996
|
+
return value.operator !== 'delete' && isDeferralSafeBundleArg(value.argument);
|
|
3997
|
+
case 'BinaryExpression':
|
|
3998
|
+
case 'LogicalExpression':
|
|
3999
|
+
return isDeferralSafeBundleArg(value.left) && isDeferralSafeBundleArg(value.right);
|
|
4000
|
+
case 'ConditionalExpression':
|
|
4001
|
+
return (
|
|
4002
|
+
isDeferralSafeBundleArg(value.test) &&
|
|
4003
|
+
isDeferralSafeBundleArg(value.consequent) &&
|
|
4004
|
+
isDeferralSafeBundleArg(value.alternate)
|
|
4005
|
+
);
|
|
4006
|
+
default:
|
|
4007
|
+
return false;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
|
|
3939
4011
|
/**
|
|
3940
4012
|
* `() => fn(a, b, …)` — a zero-param arrow whose body is a single
|
|
3941
4013
|
* function call. Returns `{ callee, args }` if so, else null. Used to compile
|
|
@@ -3967,8 +4039,9 @@ function detectStableEventBundle(node) {
|
|
|
3967
4039
|
if (!body || body.type !== 'CallExpression') return null;
|
|
3968
4040
|
// Identifier callees only — see the receiver-loss note above.
|
|
3969
4041
|
if (!body.callee || body.callee.type !== 'Identifier') return null;
|
|
3970
|
-
// Bail if any arg is a spread — bundle args are positional only.
|
|
3971
|
-
|
|
4042
|
+
// Bail if any arg is a spread — bundle args are positional only. Everything
|
|
4043
|
+
// else has to survive being hoisted to render time; see the note above.
|
|
4044
|
+
if (!body.arguments.every(isDeferralSafeBundleArg)) return null;
|
|
3972
4045
|
return { callee: body.callee, args: body.arguments };
|
|
3973
4046
|
}
|
|
3974
4047
|
|
|
@@ -4331,6 +4404,31 @@ export function hasOwnValueReturn(node) {
|
|
|
4331
4404
|
return walk(body.body || []);
|
|
4332
4405
|
}
|
|
4333
4406
|
|
|
4407
|
+
/**
|
|
4408
|
+
* Whether a statement list always completes abruptly, so control can never fall
|
|
4409
|
+
* past its end. Lets an outputless `@{ … }` body drop the tail return it would
|
|
4410
|
+
* otherwise synthesize, because the body's own returns already cover every path.
|
|
4411
|
+
*
|
|
4412
|
+
* Deliberately syntactic: `return`, `throw`, a block that ends abruptly, and an
|
|
4413
|
+
* if/else whose arms both do. Anything subtler keeps the tail, which is always
|
|
4414
|
+
* safe — the runtime reads a fallen-through `undefined` as "this body already
|
|
4415
|
+
* emitted its template", so the tail must stay wherever reachability is unproven.
|
|
4416
|
+
*/
|
|
4417
|
+
function alwaysCompletesAbruptly(statements) {
|
|
4418
|
+
const last = statements[statements.length - 1];
|
|
4419
|
+
if (!last) return false;
|
|
4420
|
+
if (last.type === 'ReturnStatement' || last.type === 'ThrowStatement') return true;
|
|
4421
|
+
if (last.type === 'BlockStatement') return alwaysCompletesAbruptly(last.body || []);
|
|
4422
|
+
if (last.type === 'IfStatement') {
|
|
4423
|
+
return (
|
|
4424
|
+
!!last.alternate &&
|
|
4425
|
+
alwaysCompletesAbruptly([last.consequent]) &&
|
|
4426
|
+
alwaysCompletesAbruptly([last.alternate])
|
|
4427
|
+
);
|
|
4428
|
+
}
|
|
4429
|
+
return false;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4334
4432
|
/**
|
|
4335
4433
|
* A mixed shorthand's early return must always reach renderReturnedValue. The
|
|
4336
4434
|
* runtime reserves `undefined` for a compiled-void body that already emitted its
|
|
@@ -6073,6 +6171,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6073
6171
|
nextHookMemoCacheId: 0, // unique non-index slots property per compiled render function
|
|
6074
6172
|
currentInvariantLocals: null, // Set<string> of component-lifetime-stable local values
|
|
6075
6173
|
currentEventInvariantLocals: null, // Set<string> safe to retain in native event slots
|
|
6174
|
+
currentBodyIsComponentScope: false, // planning the component body itself, not a nested arm
|
|
6076
6175
|
currentProfileComponentId: null,
|
|
6077
6176
|
knownStringLocals: null, // Set<string> of provably-string locals (text-hole inference)
|
|
6078
6177
|
nextHookSymId: 0,
|
|
@@ -10463,6 +10562,11 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10463
10562
|
}
|
|
10464
10563
|
ctx.currentInvariantLocals = invariantLocals;
|
|
10465
10564
|
ctx.currentEventInvariantLocals = eventInvariantLocals;
|
|
10565
|
+
// Same gate `findMountEventCallbackSinks` uses: the lifetime proof below is
|
|
10566
|
+
// defined relative to the COMPONENT's scope, so it is only sound while
|
|
10567
|
+
// planning that scope's own JSX.
|
|
10568
|
+
const prevBodyIsComponentScope = ctx.currentBodyIsComponentScope;
|
|
10569
|
+
ctx.currentBodyIsComponentScope = options?.autoCallback === true;
|
|
10466
10570
|
// M3 inherit-range: only a real `@{ … }` (JSXCodeBlock) component body spans
|
|
10467
10571
|
// its block's whole range — synthetic sub-bodies (@if/@for/@try arms,
|
|
10468
10572
|
// children render-fns) pass statement arrays and stay unflagged. planJsx
|
|
@@ -10483,10 +10587,20 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10483
10587
|
const shellOrigin = node.loc ? node : node.id?.loc ? node.id : prevFnOrigin;
|
|
10484
10588
|
let plan = null;
|
|
10485
10589
|
let returnedExpression = null;
|
|
10486
|
-
if (returnedOutput) {
|
|
10487
|
-
|
|
10590
|
+
if (returnedOutput && jsxNodes.length === 0) {
|
|
10591
|
+
// A `@{ … }` body can carry value returns with NO trailing output node —
|
|
10592
|
+
// `@{ … return null }` while a component is being written, or a React-shaped
|
|
10593
|
+
// `return <jsx>` inside the block. There is no template to lower: the body's
|
|
10594
|
+
// own returns are the whole output, so the tail is a plain `null` covering
|
|
10595
|
+
// the fall-through path. When the body provably never falls through, that
|
|
10596
|
+
// tail is unreachable and is dropped. Statement returns already normalized
|
|
10597
|
+
// to `?? null`.
|
|
10598
|
+
if (!alwaysCompletesAbruptly(rewrittenStatements)) {
|
|
10599
|
+
returnedExpression = b.literal(null, 'null', node);
|
|
10600
|
+
}
|
|
10601
|
+
} else if (returnedOutput) {
|
|
10488
10602
|
returnedExpression = lowerReturnJsx(
|
|
10489
|
-
rewriteHookCalls(
|
|
10603
|
+
rewriteHookCalls(jsxNodes[0], ctx, name, options?.localHookSlots === true),
|
|
10490
10604
|
ctx,
|
|
10491
10605
|
inlinedSubs,
|
|
10492
10606
|
cssHash,
|
|
@@ -10496,6 +10610,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10496
10610
|
}
|
|
10497
10611
|
ctx.currentInvariantLocals = prevInvariantLocals;
|
|
10498
10612
|
ctx.currentEventInvariantLocals = prevEventInvariantLocals;
|
|
10613
|
+
ctx.currentBodyIsComponentScope = prevBodyIsComponentScope;
|
|
10499
10614
|
ctx._inheritBody = prevInheritBody;
|
|
10500
10615
|
ctx._fnOrigin = prevFnOrigin;
|
|
10501
10616
|
ctx._foldedDirectiveCalls = prevFDC;
|
|
@@ -11067,6 +11182,49 @@ function isEventHandlerInvariantExpr(node, ctx) {
|
|
|
11067
11182
|
);
|
|
11068
11183
|
}
|
|
11069
11184
|
|
|
11185
|
+
/**
|
|
11186
|
+
* An inline `onClick={() => …}` arrow is rebuilt and reassigned to its DOM slot
|
|
11187
|
+
* on every render. When nothing the arrow reads can change, that write is dead
|
|
11188
|
+
* work: the handler can be installed once at mount and left alone, which is
|
|
11189
|
+
* already what a NAMED handler gets through `findMountEventCallbackSinks`.
|
|
11190
|
+
* Recognising the inline form closes the gap between the two spellings.
|
|
11191
|
+
*
|
|
11192
|
+
* "Nothing it reads can change" means every free identifier is either proven
|
|
11193
|
+
* event-invariant for this component (a useState setter, a ref object, a
|
|
11194
|
+
* useEffectEvent wrapper, …) or is not a component local at all — module scope,
|
|
11195
|
+
* an import, or a global, each fixed for the module's lifetime. That second
|
|
11196
|
+
* clause is the same inference `isArrowStableOver` makes.
|
|
11197
|
+
*
|
|
11198
|
+
* Sound only while planning the component body's own JSX.
|
|
11199
|
+
* `collectComponentLocals` deliberately ignores nested blocks, so inside a
|
|
11200
|
+
* `@for` item body the loop variable is absent from the set and would read as
|
|
11201
|
+
* module scope — and a keyed survivor can be handed a different item without
|
|
11202
|
+
* remounting, which would freeze the first item's capture in the slot forever.
|
|
11203
|
+
*/
|
|
11204
|
+
function isMountStableInlineHandler(node, ctx) {
|
|
11205
|
+
if (ctx.hmr || ctx.profile || !ctx.currentBodyIsComponentScope) return false;
|
|
11206
|
+
const value = unwrapTsExpr(node);
|
|
11207
|
+
// A FunctionExpression is reachable through its own binding name and carries
|
|
11208
|
+
// its own `this`/`arguments`; only the arrow form is a pure lexical capture.
|
|
11209
|
+
if (value?.type !== 'ArrowFunctionExpression') return false;
|
|
11210
|
+
const locals = ctx.currentComponentLocals;
|
|
11211
|
+
if (!locals) return false;
|
|
11212
|
+
const paramScope = new Set();
|
|
11213
|
+
for (const p of value.params || []) collectBindings(p, paramScope);
|
|
11214
|
+
// Params are walked alongside the body: their names are already bound in
|
|
11215
|
+
// `paramScope`, but a default (`(e, x = n) => …`) is an ordinary expression
|
|
11216
|
+
// that runs per call and can reach a changing local.
|
|
11217
|
+
for (const name of collectFreeIdentifiers([value.body, ...(value.params || [])], paramScope)) {
|
|
11218
|
+
// `arguments` is the render call's own, and a DIRECT `eval` resolves
|
|
11219
|
+
// component locals this walk cannot see — either would tie the installed
|
|
11220
|
+
// closure to whatever the first render happened to hold.
|
|
11221
|
+
if (name === 'arguments' || name === 'eval') return false;
|
|
11222
|
+
if (!locals.has(name)) continue;
|
|
11223
|
+
if (ctx.currentEventInvariantLocals?.has(name) !== true) return false;
|
|
11224
|
+
}
|
|
11225
|
+
return true;
|
|
11226
|
+
}
|
|
11227
|
+
|
|
11070
11228
|
// Object/array/function literals allocate a new identity on every evaluation,
|
|
11071
11229
|
// so an identity diff can never skip their update. This currently feeds the
|
|
11072
11230
|
// class binding path, where dropping the dead previous-value field preserves
|
|
@@ -19490,7 +19648,8 @@ function emitElementHtml(
|
|
|
19490
19648
|
slotKey,
|
|
19491
19649
|
ns: hostNs,
|
|
19492
19650
|
dev: ctx.dev,
|
|
19493
|
-
mountOnly:
|
|
19651
|
+
mountOnly:
|
|
19652
|
+
isEventHandlerInvariantExpr(inner, ctx) || isMountStableInlineHandler(inner, ctx),
|
|
19494
19653
|
});
|
|
19495
19654
|
}
|
|
19496
19655
|
} else if (attrName === 'class') {
|
package/dist/runtime.js
CHANGED
|
@@ -8056,7 +8056,8 @@ function createScopedValue(readElement) {
|
|
|
8056
8056
|
const resolve = () => {
|
|
8057
8057
|
const scope = CURRENT_SCOPE;
|
|
8058
8058
|
const epoch = COMPILER_CACHE_CONTEXT_EPOCH;
|
|
8059
|
-
|
|
8059
|
+
const sameScope = resolvedScope === scope || resolvedScope !== null && scope !== null && scope.block.parentBlock === resolvedScope.block && scope.$$ctxValues === null;
|
|
8060
|
+
if (resolved === void 0 || !sameScope || resolvedEpoch !== epoch) {
|
|
8060
8061
|
const next = readElement();
|
|
8061
8062
|
if (next.key === null && KEYED_ELEMENT_DESCRIPTORS.has(next)) {
|
|
8062
8063
|
KEYED_ELEMENT_DESCRIPTORS.add(descriptor);
|
|
@@ -8064,6 +8065,8 @@ function createScopedValue(readElement) {
|
|
|
8064
8065
|
resolvedScope = scope;
|
|
8065
8066
|
resolvedEpoch = epoch;
|
|
8066
8067
|
resolved = next;
|
|
8068
|
+
} else if (resolvedScope !== scope) {
|
|
8069
|
+
resolvedScope = scope;
|
|
8067
8070
|
}
|
|
8068
8071
|
return resolved;
|
|
8069
8072
|
};
|
|
@@ -8102,7 +8105,7 @@ function createScopedElement(type, props, readChildren) {
|
|
|
8102
8105
|
const children = () => {
|
|
8103
8106
|
const scope = CURRENT_SCOPE;
|
|
8104
8107
|
const epoch = COMPILER_CACHE_CONTEXT_EPOCH;
|
|
8105
|
-
const sameScope = resolvedScope === scope || resolvedScope !== null && scope !== null && scope.block.parentBlock === resolvedScope.block;
|
|
8108
|
+
const sameScope = resolvedScope === scope || resolvedScope !== null && scope !== null && scope.block.parentBlock === resolvedScope.block && scope.$$ctxValues === null;
|
|
8106
8109
|
if (!resolved || !sameScope || resolvedEpoch !== epoch) {
|
|
8107
8110
|
const nextChildren = readChildren();
|
|
8108
8111
|
resolvedScope = scope;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octane",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -119,11 +119,14 @@
|
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
121
121
|
"@tsrx/react": "^0.2.56",
|
|
122
|
+
"@wagmi/connectors": "8.0.25",
|
|
123
|
+
"@wagmi/core": "3.6.4",
|
|
122
124
|
"esbuild": "^0.28.1",
|
|
123
125
|
"happy-dom": "^20.11.0",
|
|
124
126
|
"playwright": "^1.61.1",
|
|
125
127
|
"react": "^19.2.7",
|
|
126
128
|
"react-dom": "^19.2.7",
|
|
129
|
+
"viem": "2.55.10",
|
|
127
130
|
"vite": "^8.1.5",
|
|
128
131
|
"vitest": "^4.1.10"
|
|
129
132
|
},
|