octane 0.1.21 → 0.1.22
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 +127 -3
- 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
|
|
|
@@ -6073,6 +6146,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6073
6146
|
nextHookMemoCacheId: 0, // unique non-index slots property per compiled render function
|
|
6074
6147
|
currentInvariantLocals: null, // Set<string> of component-lifetime-stable local values
|
|
6075
6148
|
currentEventInvariantLocals: null, // Set<string> safe to retain in native event slots
|
|
6149
|
+
currentBodyIsComponentScope: false, // planning the component body itself, not a nested arm
|
|
6076
6150
|
currentProfileComponentId: null,
|
|
6077
6151
|
knownStringLocals: null, // Set<string> of provably-string locals (text-hole inference)
|
|
6078
6152
|
nextHookSymId: 0,
|
|
@@ -10463,6 +10537,11 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10463
10537
|
}
|
|
10464
10538
|
ctx.currentInvariantLocals = invariantLocals;
|
|
10465
10539
|
ctx.currentEventInvariantLocals = eventInvariantLocals;
|
|
10540
|
+
// Same gate `findMountEventCallbackSinks` uses: the lifetime proof below is
|
|
10541
|
+
// defined relative to the COMPONENT's scope, so it is only sound while
|
|
10542
|
+
// planning that scope's own JSX.
|
|
10543
|
+
const prevBodyIsComponentScope = ctx.currentBodyIsComponentScope;
|
|
10544
|
+
ctx.currentBodyIsComponentScope = options?.autoCallback === true;
|
|
10466
10545
|
// M3 inherit-range: only a real `@{ … }` (JSXCodeBlock) component body spans
|
|
10467
10546
|
// its block's whole range — synthetic sub-bodies (@if/@for/@try arms,
|
|
10468
10547
|
// children render-fns) pass statement arrays and stay unflagged. planJsx
|
|
@@ -10496,6 +10575,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10496
10575
|
}
|
|
10497
10576
|
ctx.currentInvariantLocals = prevInvariantLocals;
|
|
10498
10577
|
ctx.currentEventInvariantLocals = prevEventInvariantLocals;
|
|
10578
|
+
ctx.currentBodyIsComponentScope = prevBodyIsComponentScope;
|
|
10499
10579
|
ctx._inheritBody = prevInheritBody;
|
|
10500
10580
|
ctx._fnOrigin = prevFnOrigin;
|
|
10501
10581
|
ctx._foldedDirectiveCalls = prevFDC;
|
|
@@ -11067,6 +11147,49 @@ function isEventHandlerInvariantExpr(node, ctx) {
|
|
|
11067
11147
|
);
|
|
11068
11148
|
}
|
|
11069
11149
|
|
|
11150
|
+
/**
|
|
11151
|
+
* An inline `onClick={() => …}` arrow is rebuilt and reassigned to its DOM slot
|
|
11152
|
+
* on every render. When nothing the arrow reads can change, that write is dead
|
|
11153
|
+
* work: the handler can be installed once at mount and left alone, which is
|
|
11154
|
+
* already what a NAMED handler gets through `findMountEventCallbackSinks`.
|
|
11155
|
+
* Recognising the inline form closes the gap between the two spellings.
|
|
11156
|
+
*
|
|
11157
|
+
* "Nothing it reads can change" means every free identifier is either proven
|
|
11158
|
+
* event-invariant for this component (a useState setter, a ref object, a
|
|
11159
|
+
* useEffectEvent wrapper, …) or is not a component local at all — module scope,
|
|
11160
|
+
* an import, or a global, each fixed for the module's lifetime. That second
|
|
11161
|
+
* clause is the same inference `isArrowStableOver` makes.
|
|
11162
|
+
*
|
|
11163
|
+
* Sound only while planning the component body's own JSX.
|
|
11164
|
+
* `collectComponentLocals` deliberately ignores nested blocks, so inside a
|
|
11165
|
+
* `@for` item body the loop variable is absent from the set and would read as
|
|
11166
|
+
* module scope — and a keyed survivor can be handed a different item without
|
|
11167
|
+
* remounting, which would freeze the first item's capture in the slot forever.
|
|
11168
|
+
*/
|
|
11169
|
+
function isMountStableInlineHandler(node, ctx) {
|
|
11170
|
+
if (ctx.hmr || ctx.profile || !ctx.currentBodyIsComponentScope) return false;
|
|
11171
|
+
const value = unwrapTsExpr(node);
|
|
11172
|
+
// A FunctionExpression is reachable through its own binding name and carries
|
|
11173
|
+
// its own `this`/`arguments`; only the arrow form is a pure lexical capture.
|
|
11174
|
+
if (value?.type !== 'ArrowFunctionExpression') return false;
|
|
11175
|
+
const locals = ctx.currentComponentLocals;
|
|
11176
|
+
if (!locals) return false;
|
|
11177
|
+
const paramScope = new Set();
|
|
11178
|
+
for (const p of value.params || []) collectBindings(p, paramScope);
|
|
11179
|
+
// Params are walked alongside the body: their names are already bound in
|
|
11180
|
+
// `paramScope`, but a default (`(e, x = n) => …`) is an ordinary expression
|
|
11181
|
+
// that runs per call and can reach a changing local.
|
|
11182
|
+
for (const name of collectFreeIdentifiers([value.body, ...(value.params || [])], paramScope)) {
|
|
11183
|
+
// `arguments` is the render call's own, and a DIRECT `eval` resolves
|
|
11184
|
+
// component locals this walk cannot see — either would tie the installed
|
|
11185
|
+
// closure to whatever the first render happened to hold.
|
|
11186
|
+
if (name === 'arguments' || name === 'eval') return false;
|
|
11187
|
+
if (!locals.has(name)) continue;
|
|
11188
|
+
if (ctx.currentEventInvariantLocals?.has(name) !== true) return false;
|
|
11189
|
+
}
|
|
11190
|
+
return true;
|
|
11191
|
+
}
|
|
11192
|
+
|
|
11070
11193
|
// Object/array/function literals allocate a new identity on every evaluation,
|
|
11071
11194
|
// so an identity diff can never skip their update. This currently feeds the
|
|
11072
11195
|
// class binding path, where dropping the dead previous-value field preserves
|
|
@@ -19490,7 +19613,8 @@ function emitElementHtml(
|
|
|
19490
19613
|
slotKey,
|
|
19491
19614
|
ns: hostNs,
|
|
19492
19615
|
dev: ctx.dev,
|
|
19493
|
-
mountOnly:
|
|
19616
|
+
mountOnly:
|
|
19617
|
+
isEventHandlerInvariantExpr(inner, ctx) || isMountStableInlineHandler(inner, ctx),
|
|
19494
19618
|
});
|
|
19495
19619
|
}
|
|
19496
19620
|
} 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.22",
|
|
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
|
},
|