octane 0.13.2 → 0.13.3
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/cjs/version.cjs +1 -1
- package/dist/compiler/compile.js +107 -18
- package/dist/compiler/hook-deps.js +2 -11
- package/dist/compiler/strong-diagnostics.js +2 -32
- package/dist/compiler/strong-fixes.js +0 -63
- package/dist/compiler/strong-mode.js +1 -6
- package/dist/node/version.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cjs/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
version: () => version
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const version = "0.13.
|
|
24
|
+
const version = "0.13.3";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
version
|
package/dist/compiler/compile.js
CHANGED
|
@@ -3666,7 +3666,10 @@ function autoCalculationOf(expression, selfName, componentLocals, ctx, immutable
|
|
|
3666
3666
|
const init = unwrapTsExpr(expression);
|
|
3667
3667
|
// An arrow/function init is auto-callback's job, not ours.
|
|
3668
3668
|
if (FN_TYPES.has(init?.type)) return null;
|
|
3669
|
-
|
|
3669
|
+
const literal =
|
|
3670
|
+
ctx.strongLiteralMemo === true &&
|
|
3671
|
+
(init?.type === 'ObjectExpression' || init?.type === 'ArrayExpression');
|
|
3672
|
+
if (!isPropCreationExpr(init, ctx, literal)) return null;
|
|
3670
3673
|
// Every call reached during render must satisfy the same projection contract
|
|
3671
3674
|
// the region cache uses. Compatibility-mode member calls fail closed.
|
|
3672
3675
|
const immutableProjection = immutableArrayProjection(init, immutableStates);
|
|
@@ -3693,7 +3696,7 @@ function autoCalculationOf(expression, selfName, componentLocals, ctx, immutable
|
|
|
3693
3696
|
return null;
|
|
3694
3697
|
}
|
|
3695
3698
|
}
|
|
3696
|
-
return { init, deps, immutableProjection };
|
|
3699
|
+
return { init, deps, immutableProjection, literal };
|
|
3697
3700
|
}
|
|
3698
3701
|
|
|
3699
3702
|
// The single-name `const` declarator a calculation can replace, or null.
|
|
@@ -3726,6 +3729,8 @@ function closeCalculationReads(
|
|
|
3726
3729
|
componentLocals,
|
|
3727
3730
|
ctx,
|
|
3728
3731
|
immutableStates = null,
|
|
3732
|
+
literalReads = null,
|
|
3733
|
+
renderRoots = [],
|
|
3729
3734
|
) {
|
|
3730
3735
|
if (renderReadNames.size === 0) return renderReadNames;
|
|
3731
3736
|
const declaredAt = new Map();
|
|
@@ -3750,6 +3755,24 @@ function closeCalculationReads(
|
|
|
3750
3755
|
if (decl === undefined) continue;
|
|
3751
3756
|
const calculation = autoCalculationOf(decl.init, name, componentLocals, ctx, immutableStates);
|
|
3752
3757
|
if (calculation === null) continue;
|
|
3758
|
+
// An allocated local can still be mutated or escape during setup.
|
|
3759
|
+
// Cache only literals read directly as eligible props/refs; other reads
|
|
3760
|
+
// (including callback captures and alias creation) keep their JS lifetime.
|
|
3761
|
+
if (calculation.literal) {
|
|
3762
|
+
const allowed = literalReads?.get(name);
|
|
3763
|
+
if (
|
|
3764
|
+
!allowed ||
|
|
3765
|
+
statements.some(
|
|
3766
|
+
(statement) =>
|
|
3767
|
+
statement.declarations?.[0] !== decl &&
|
|
3768
|
+
collectFreeIdentifiers(statement, [], allowed).has(name),
|
|
3769
|
+
) ||
|
|
3770
|
+
collectFreeIdentifiers(renderRoots, [], allowed).has(name)
|
|
3771
|
+
) {
|
|
3772
|
+
forward.push(name);
|
|
3773
|
+
continue;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3753
3776
|
const at = declaredAt.get(name);
|
|
3754
3777
|
if (calculation.deps.some((dependency) => declaredAt.get(dependency) > at)) {
|
|
3755
3778
|
forward.push(name);
|
|
@@ -3782,7 +3805,14 @@ function rewriteAutoCalculation(
|
|
|
3782
3805
|
immutableStates,
|
|
3783
3806
|
);
|
|
3784
3807
|
if (calculation === null) return stmt;
|
|
3785
|
-
const { init, deps, immutableProjection } = calculation;
|
|
3808
|
+
const { init, deps, immutableProjection, literal } = calculation;
|
|
3809
|
+
// An unrelated update may supply a new props bag with the same fields.
|
|
3810
|
+
// Literal identities depend on the inputs they read, not on that bag.
|
|
3811
|
+
const dependencyNodes = literal
|
|
3812
|
+
? collectDepPaths(init, null, moduleBoundCheckForDeps(ctx, init), () =>
|
|
3813
|
+
requireRuntimeForContext(ctx, METHOD_DEP_IMPORT),
|
|
3814
|
+
)
|
|
3815
|
+
: deps.map((n) => b.id(n));
|
|
3786
3816
|
return {
|
|
3787
3817
|
...stmt,
|
|
3788
3818
|
declarations: [
|
|
@@ -3799,7 +3829,7 @@ function rewriteAutoCalculation(
|
|
|
3799
3829
|
: { _octaneImmutableArrayFilter: immutableProjection }),
|
|
3800
3830
|
},
|
|
3801
3831
|
b.arrow([], init),
|
|
3802
|
-
b.array(
|
|
3832
|
+
b.array(dependencyNodes),
|
|
3803
3833
|
),
|
|
3804
3834
|
init,
|
|
3805
3835
|
),
|
|
@@ -3843,20 +3873,24 @@ const CALCULATION_VALUE_PROOFS = [
|
|
|
3843
3873
|
* unobservable. Positions whose evaluation is conditional or deferred keep
|
|
3844
3874
|
* their expression: directive arms, component children (the child decides
|
|
3845
3875
|
* whether and when they render), and built-in boundaries. An expression holding
|
|
3846
|
-
* JSX keeps its list and descriptor lowering, and event handlers
|
|
3847
|
-
*
|
|
3876
|
+
* JSX keeps its list and descriptor lowering, and event handlers and keys keep
|
|
3877
|
+
* their wiring. Ref arrays use the same identity proof as props. A render tree that writes to anything keeps its whole
|
|
3848
3878
|
* evaluation order.
|
|
3849
3879
|
*
|
|
3850
3880
|
* Returns null when nothing moves; otherwise the copy-on-write render nodes,
|
|
3851
3881
|
* the declarations to append, and their names.
|
|
3852
3882
|
*/
|
|
3853
3883
|
function hoistStrongRenderCalculations(jsxNodes, ctx, immutableStates) {
|
|
3854
|
-
if (ctx.strongMemo
|
|
3884
|
+
if ((!ctx.strongMemo && !ctx.strongLiteralMemo) || ctx.nativeReads || !ctx.currentComponentLocals)
|
|
3885
|
+
return null;
|
|
3855
3886
|
if (renderTreeWrites(jsxNodes)) return null;
|
|
3856
3887
|
const declarations = [];
|
|
3857
3888
|
const names = [];
|
|
3889
|
+
const literalReads = new Map();
|
|
3858
3890
|
const nodes = mapNodes(jsxNodes, visitNode);
|
|
3859
|
-
return declarations.length === 0
|
|
3891
|
+
return declarations.length === 0 && literalReads.size === 0
|
|
3892
|
+
? null
|
|
3893
|
+
: { jsxNodes: nodes, declarations, names, literalReads };
|
|
3860
3894
|
|
|
3861
3895
|
function visitNode(node) {
|
|
3862
3896
|
switch (node?.type) {
|
|
@@ -3894,7 +3928,10 @@ function hoistStrongRenderCalculations(jsxNodes, ctx, immutableStates) {
|
|
|
3894
3928
|
return attribute;
|
|
3895
3929
|
}
|
|
3896
3930
|
const name = attribute.name.name;
|
|
3897
|
-
if (name === 'key' || name === '
|
|
3931
|
+
if (name === 'key' || name === 'children') return attribute;
|
|
3932
|
+
const inner = unwrapTsExpr(attribute.value?.expression);
|
|
3933
|
+
if (name === 'ref' && inner?.type !== 'ArrayExpression' && inner?.type !== 'Identifier')
|
|
3934
|
+
return attribute;
|
|
3898
3935
|
// The host handoff prepares unbound fields through the ordinary renderer.
|
|
3899
3936
|
// Hoisting one outside that preparation makes its writer ineligible.
|
|
3900
3937
|
if (
|
|
@@ -3905,7 +3942,7 @@ function hoistStrongRenderCalculations(jsxNodes, ctx, immutableStates) {
|
|
|
3905
3942
|
return attribute;
|
|
3906
3943
|
if (!component && /^on[A-Z]/.test(name)) return attribute;
|
|
3907
3944
|
if (attribute.value?.type !== 'JSXExpressionContainer') return attribute;
|
|
3908
|
-
const value = visitContainer(attribute.value);
|
|
3945
|
+
const value = visitContainer(attribute.value, true);
|
|
3909
3946
|
return value === attribute.value ? attribute : { ...attribute, value };
|
|
3910
3947
|
});
|
|
3911
3948
|
// Component children render inside the child, if at all.
|
|
@@ -3918,11 +3955,27 @@ function hoistStrongRenderCalculations(jsxNodes, ctx, immutableStates) {
|
|
|
3918
3955
|
};
|
|
3919
3956
|
}
|
|
3920
3957
|
|
|
3921
|
-
function visitContainer(container) {
|
|
3958
|
+
function visitContainer(container, prop = false) {
|
|
3922
3959
|
const expression = container.expression;
|
|
3923
3960
|
const inner = unwrapTsExpr(expression);
|
|
3924
3961
|
if (!inner || inner.type === 'JSXEmptyExpression' || containsJsxNode(inner)) return container;
|
|
3925
|
-
if (
|
|
3962
|
+
if (prop && inner.type === 'Identifier') {
|
|
3963
|
+
let nodes = literalReads.get(inner.name);
|
|
3964
|
+
if (!nodes) literalReads.set(inner.name, (nodes = new Set()));
|
|
3965
|
+
nodes.add(inner);
|
|
3966
|
+
}
|
|
3967
|
+
const calculation = autoCalculationOf(
|
|
3968
|
+
inner,
|
|
3969
|
+
null,
|
|
3970
|
+
ctx.currentComponentLocals,
|
|
3971
|
+
ctx,
|
|
3972
|
+
immutableStates,
|
|
3973
|
+
);
|
|
3974
|
+
if (
|
|
3975
|
+
calculation === null ||
|
|
3976
|
+
(!ctx.strongMemo && !calculation.literal) ||
|
|
3977
|
+
(calculation.literal && !prop)
|
|
3978
|
+
) {
|
|
3926
3979
|
return container;
|
|
3927
3980
|
}
|
|
3928
3981
|
const name = allocCompilerName(ctx, '__calc');
|
|
@@ -3931,6 +3984,7 @@ function hoistStrongRenderCalculations(jsxNodes, ctx, immutableStates) {
|
|
|
3931
3984
|
// A child warm plan runs outside this body, so it keeps the authored prop.
|
|
3932
3985
|
(ctx.hoistedRenderCalculations ??= new Map()).set(name, expression);
|
|
3933
3986
|
const reference = b.id(name, inner);
|
|
3987
|
+
if (prop) literalReads.set(name, new Set([reference]));
|
|
3934
3988
|
for (const proof of CALCULATION_VALUE_PROOFS) {
|
|
3935
3989
|
if (inner.metadata?.[proof] === true) reference.metadata[proof] = true;
|
|
3936
3990
|
}
|
|
@@ -3957,9 +4011,10 @@ function establishesRenderScope(statement) {
|
|
|
3957
4011
|
// `return <jsx>` reached after the render-scope proof above gets the same
|
|
3958
4012
|
// calculations, declared immediately before it. Returns null when nothing moves.
|
|
3959
4013
|
function hoistStrongReturnCalculations(statements, ctx) {
|
|
3960
|
-
if (ctx.strongMemo
|
|
4014
|
+
if ((!ctx.strongMemo && !ctx.strongLiteralMemo) || ctx.nativeReads) return null;
|
|
3961
4015
|
let out = null;
|
|
3962
4016
|
const names = [];
|
|
4017
|
+
const literalReads = new Map();
|
|
3963
4018
|
let established = false;
|
|
3964
4019
|
for (let index = 0; index < statements.length; index++) {
|
|
3965
4020
|
const statement = statements[index];
|
|
@@ -3969,13 +4024,18 @@ function hoistStrongReturnCalculations(statements, ctx) {
|
|
|
3969
4024
|
out ??= statements.slice(0, index);
|
|
3970
4025
|
out.push(...hoisted.declarations, { ...statement, argument: hoisted.jsxNodes[0] });
|
|
3971
4026
|
names.push(...hoisted.names);
|
|
4027
|
+
for (const [name, reads] of hoisted.literalReads) {
|
|
4028
|
+
let nodes = literalReads.get(name);
|
|
4029
|
+
if (!nodes) literalReads.set(name, (nodes = new Set()));
|
|
4030
|
+
for (const read of reads) nodes.add(read);
|
|
4031
|
+
}
|
|
3972
4032
|
continue;
|
|
3973
4033
|
}
|
|
3974
4034
|
}
|
|
3975
4035
|
out?.push(statement);
|
|
3976
4036
|
established ||= establishesRenderScope(statement);
|
|
3977
4037
|
}
|
|
3978
|
-
return out === null ? null : { statements: out, names };
|
|
4038
|
+
return out === null ? null : { statements: out, names, literalReads };
|
|
3979
4039
|
}
|
|
3980
4040
|
|
|
3981
4041
|
function mapNodes(nodes, map) {
|
|
@@ -4928,7 +4988,9 @@ const CALLBACK_IDENTITY_OWNING_HOOKS = new Set(
|
|
|
4928
4988
|
);
|
|
4929
4989
|
|
|
4930
4990
|
function hookOwnsCallbackIdentity(call) {
|
|
4931
|
-
|
|
4991
|
+
// After dependency inference, use the resolved hook even when an import
|
|
4992
|
+
// alias looks like a custom hook. Earlier passes still use the spelling.
|
|
4993
|
+
const name = stableHookCallName(call) ?? hookShapedCalleeName(call);
|
|
4932
4994
|
return name !== null && CALLBACK_IDENTITY_OWNING_HOOKS.has(name);
|
|
4933
4995
|
}
|
|
4934
4996
|
|
|
@@ -11776,6 +11838,14 @@ function compileInternal(
|
|
|
11776
11838
|
splitModuleFunctions,
|
|
11777
11839
|
autoMemo: autoMemoEnabled,
|
|
11778
11840
|
strongMemo: strongMemoEnabled,
|
|
11841
|
+
// Ref and effect lifetimes must not churn on an unrelated dev/HMR render.
|
|
11842
|
+
// Only proven literal props use these caches; region/call caching stays prod-only.
|
|
11843
|
+
strongLiteralMemo:
|
|
11844
|
+
strongModeEnabled &&
|
|
11845
|
+
options?.autoMemo !== false &&
|
|
11846
|
+
mode === 'client' &&
|
|
11847
|
+
options?.__universal == null &&
|
|
11848
|
+
options?.nativeReads !== true,
|
|
11779
11849
|
nativeReads: options?.nativeReads === true,
|
|
11780
11850
|
opaqueSignalHandles:
|
|
11781
11851
|
options?.opaqueSignalHandles === true || options?.__signalModuleImport === true,
|
|
@@ -17892,6 +17962,8 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
17892
17962
|
ctx.currentComponentLocals,
|
|
17893
17963
|
ctx,
|
|
17894
17964
|
immutableStateProvenance,
|
|
17965
|
+
hoisted?.literalReads,
|
|
17966
|
+
jsxNodes,
|
|
17895
17967
|
);
|
|
17896
17968
|
if (renderReadNames.size > 0) {
|
|
17897
17969
|
workingStatements = workingStatements.map((statement) => {
|
|
@@ -19270,8 +19342,8 @@ function isTrivialUseArg(n) {
|
|
|
19270
19342
|
// the lowering passes) and hook-shaped calls (`use` / `use[A-Z]…` / a tracked
|
|
19271
19343
|
// `use` import alias — their slot/occurrence bookkeeping must execute on the
|
|
19272
19344
|
// body proper every pass, so they can never sit behind a cache hit).
|
|
19273
|
-
function isPropCreationExpr(expr, ctx) {
|
|
19274
|
-
let found =
|
|
19345
|
+
function isPropCreationExpr(expr, ctx, includesLiteral = false) {
|
|
19346
|
+
let found = includesLiteral;
|
|
19275
19347
|
let blocked = false;
|
|
19276
19348
|
walk(expr);
|
|
19277
19349
|
return found && !blocked;
|
|
@@ -19305,7 +19377,21 @@ function isPropCreationExpr(expr, ctx) {
|
|
|
19305
19377
|
blocked = true;
|
|
19306
19378
|
return;
|
|
19307
19379
|
}
|
|
19308
|
-
if (FN_TYPES.has(n.type))
|
|
19380
|
+
if (FN_TYPES.has(n.type)) {
|
|
19381
|
+
// A regular function can mutate a containing literal via its receiver,
|
|
19382
|
+
// or retain per-render state on its own function object. Reuse the
|
|
19383
|
+
// callback movement proof before stabilizing either kind of identity.
|
|
19384
|
+
if (
|
|
19385
|
+
includesLiteral &&
|
|
19386
|
+
n.type !== 'ArrowFunctionExpression' &&
|
|
19387
|
+
(!isMovableHookCallback(n) ||
|
|
19388
|
+
hasInlineMemoDirectEval(n) ||
|
|
19389
|
+
(n.id && collectFreeIdentifiers({ ...n, id: null }, []).has(n.id.name)))
|
|
19390
|
+
) {
|
|
19391
|
+
blocked = true;
|
|
19392
|
+
}
|
|
19393
|
+
return; // does not run during render
|
|
19394
|
+
}
|
|
19309
19395
|
// Same render-time-call inventory as containsRenderCall: plain and
|
|
19310
19396
|
// optional calls, constructions, tagged templates, and dynamic
|
|
19311
19397
|
// import() (ESTree flavor; the Babel flavor is a CallExpression).
|
|
@@ -21973,6 +22059,9 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
21973
22059
|
collectRenderReadNames(renderedRoots, ctx),
|
|
21974
22060
|
ctx.currentComponentLocals,
|
|
21975
22061
|
ctx,
|
|
22062
|
+
null,
|
|
22063
|
+
hoisted?.literalReads,
|
|
22064
|
+
renderedRoots,
|
|
21976
22065
|
);
|
|
21977
22066
|
let autoCalculatedDeclarations = null;
|
|
21978
22067
|
let renderScopeEstablished = false;
|
|
@@ -2341,17 +2341,8 @@ export function analyzeStrongHookPolicies(ast, options = {}) {
|
|
|
2341
2341
|
for (const { call, scope } of analysis.calls) {
|
|
2342
2342
|
const name = callNames.get(call);
|
|
2343
2343
|
const config = DEPENDENCY_HOOKS.get(name);
|
|
2344
|
-
|
|
2345
|
-
if (name === 'useMemo' || name === 'useCallback')
|
|
2346
|
-
diagnostics.push({
|
|
2347
|
-
code: 'OCTANE_STRONG_MANUAL_MEMO',
|
|
2348
|
-
node: call,
|
|
2349
|
-
hook: name,
|
|
2350
|
-
severity: 'error',
|
|
2351
|
-
message: `Strong mode owns calculation and callback caching. Replace ${name} with a const declaration and let the compiler track its inputs.`,
|
|
2352
|
-
});
|
|
2353
|
-
continue;
|
|
2354
|
-
}
|
|
2344
|
+
// Authored memo hooks own their dependencies; effect policies do not apply to them.
|
|
2345
|
+
if (config === undefined || name === 'useMemo' || name === 'useCallback') continue;
|
|
2355
2346
|
const argument = call.arguments[config.deps];
|
|
2356
2347
|
if (argument === undefined || isUnshadowedUndefined(argument, scope)) {
|
|
2357
2348
|
// Inference would run this hook on every render, which is the
|
|
@@ -131,7 +131,7 @@ export const STRONG_DIAGNOSTICS = [
|
|
|
131
131
|
section: 'effects',
|
|
132
132
|
severity: 'error',
|
|
133
133
|
detects:
|
|
134
|
-
"An
|
|
134
|
+
"An effect or imperative handle dependency argument differs from the compiler's inferred inputs, or cannot be proven equivalent. An array the compiler proves equivalent is reported as a **hint** instead, and its authored behavior is preserved.",
|
|
135
135
|
replacement: 'Omit the dependency argument.',
|
|
136
136
|
},
|
|
137
137
|
{
|
|
@@ -139,18 +139,10 @@ export const STRONG_DIAGNOSTICS = [
|
|
|
139
139
|
section: 'effects',
|
|
140
140
|
severity: 'error',
|
|
141
141
|
detects:
|
|
142
|
-
'
|
|
142
|
+
'An effect or imperative handle receives `null` dependencies, or omits them while its callback reads a binding declared after the hook, or a variable the component assigns after the hook or from a nested function. Inference cannot read that value where the hook is called, so the hook would run on every render.',
|
|
143
143
|
replacement:
|
|
144
144
|
'Omit the argument so the compiler tracks reactive inputs. Declare and finish assigning what the hook reads before calling it, or keep a changing value in state or a ref.',
|
|
145
145
|
},
|
|
146
|
-
{
|
|
147
|
-
code: 'OCTANE_STRONG_MANUAL_MEMO',
|
|
148
|
-
section: 'effects',
|
|
149
|
-
severity: 'error',
|
|
150
|
-
detects: "A call to Octane's `useMemo` or `useCallback`, including known import aliases.",
|
|
151
|
-
replacement:
|
|
152
|
-
'Write a normal calculation or callback declaration and let Strong compilation cache eligible declarations. `octane analyze --fix` rewrites `useMemo(() => value, deps)` to `value` and `useCallback(fn, deps)` to `fn`.',
|
|
153
|
-
},
|
|
154
146
|
{
|
|
155
147
|
code: 'OCTANE_STRONG_EFFECT_EVENT_DEPENDENCY',
|
|
156
148
|
section: 'effects',
|
|
@@ -533,28 +525,6 @@ export function Label({ text }: { text: string }) {
|
|
|
533
525
|
`,
|
|
534
526
|
note: 'A callback ref runs while the element commits, so a state update there renders the component a second time before paint. `useLayoutSnapshot` reads the element after layout on every commit and re-renders only when the measurement changes. To follow resizes that happen without a commit, attach a `ResizeObserver` from the ref or an effect; its callback may update state.',
|
|
535
527
|
},
|
|
536
|
-
{
|
|
537
|
-
id: 'manual-memo',
|
|
538
|
-
title: 'Drop useMemo and useCallback',
|
|
539
|
-
react: 'const total = useMemo(() => sum(items), [items]);',
|
|
540
|
-
strong: 'const total = sum(items);',
|
|
541
|
-
codes: ['OCTANE_STRONG_MANUAL_MEMO'],
|
|
542
|
-
before: `import { useCallback, useMemo } from 'octane';
|
|
543
|
-
|
|
544
|
-
export function Total({ items, onPick }: { items: number[]; onPick: (total: number) => void }) {
|
|
545
|
-
const total = useMemo(() => items.reduce((sum, item) => sum + item, 0), [items]);
|
|
546
|
-
const pick = useCallback(() => onPick(total), [onPick, total]);
|
|
547
|
-
return <button onClick={pick}>{String(total)}</button>;
|
|
548
|
-
}
|
|
549
|
-
`,
|
|
550
|
-
after: `export function Total({ items, onPick }: { items: number[]; onPick: (total: number) => void }) {
|
|
551
|
-
const total = items.reduce((sum, item) => sum + item, 0);
|
|
552
|
-
const pick = () => onPick(total);
|
|
553
|
-
return <button onClick={pick}>{String(total)}</button>;
|
|
554
|
-
}
|
|
555
|
-
`,
|
|
556
|
-
note: 'Strong production builds memoize render calculations from their inputs, and eligible callbacks keep a stable identity in every build until their inferred inputs change. `octane analyze --fix` applies this rewrite.',
|
|
557
|
-
},
|
|
558
528
|
{
|
|
559
529
|
id: 'latest-ref',
|
|
560
530
|
title: 'Read the latest props from an effect',
|
|
@@ -244,69 +244,6 @@ export function lazyRefSuggestion(idiom, ast, source) {
|
|
|
244
244
|
return { ...suggestion, edits };
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
const PRIMARY_EXPRESSIONS = new Set([
|
|
248
|
-
'ArrayExpression',
|
|
249
|
-
'CallExpression',
|
|
250
|
-
'Identifier',
|
|
251
|
-
'JSXElement',
|
|
252
|
-
'JSXFragment',
|
|
253
|
-
'Literal',
|
|
254
|
-
'MemberExpression',
|
|
255
|
-
'ParenthesizedExpression',
|
|
256
|
-
'TaggedTemplateExpression',
|
|
257
|
-
'TemplateLiteral',
|
|
258
|
-
]);
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Replace `useMemo(() => value, deps)` with `value` and
|
|
262
|
-
* `useCallback(fn, deps)` with `fn`. A block-bodied memo factory other than a
|
|
263
|
-
* single `return` keeps its message only.
|
|
264
|
-
*
|
|
265
|
-
* @param {any} call
|
|
266
|
-
* @param {'useMemo' | 'useCallback'} name
|
|
267
|
-
* @param {string} source
|
|
268
|
-
*/
|
|
269
|
-
export function manualMemoSuggestion(call, name, source) {
|
|
270
|
-
const callback = unwrap(call.arguments?.[0]);
|
|
271
|
-
const message =
|
|
272
|
-
name === 'useMemo'
|
|
273
|
-
? 'Write the calculation as a plain expression; Strong compilation caches it.'
|
|
274
|
-
: 'Pass the function directly; Strong compilation keeps its identity stable.';
|
|
275
|
-
if (callback == null || call.arguments.some((argument) => argument.type === 'SpreadElement')) {
|
|
276
|
-
return { message };
|
|
277
|
-
}
|
|
278
|
-
if (name === 'useCallback') {
|
|
279
|
-
return {
|
|
280
|
-
message,
|
|
281
|
-
edits: [
|
|
282
|
-
{ start: call.start, end: call.end, text: source.slice(callback.start, callback.end) },
|
|
283
|
-
],
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
if (
|
|
287
|
-
callback.type !== 'ArrowFunctionExpression' ||
|
|
288
|
-
callback.async === true ||
|
|
289
|
-
callback.params.length > 0
|
|
290
|
-
) {
|
|
291
|
-
return { message };
|
|
292
|
-
}
|
|
293
|
-
let body = callback.body;
|
|
294
|
-
if (body?.type === 'BlockStatement') {
|
|
295
|
-
const only = body.body?.length === 1 ? body.body[0] : null;
|
|
296
|
-
if (only?.type !== 'ReturnStatement' || only.argument == null) return { message };
|
|
297
|
-
body = only.argument;
|
|
298
|
-
}
|
|
299
|
-
const text = source.slice(body.start, body.end);
|
|
300
|
-
if (FUNCTION_SCOPE_WORDS.test(text)) return { message };
|
|
301
|
-
// The call was a primary expression, so anything that binds less tightly
|
|
302
|
-
// keeps that grouping in parentheses: `useMemo(() => !a, []).b` is `(!a).b`.
|
|
303
|
-
const parenthesize = !PRIMARY_EXPRESSIONS.has(body.type);
|
|
304
|
-
return {
|
|
305
|
-
message,
|
|
306
|
-
edits: [{ start: call.start, end: call.end, text: parenthesize ? `(${text})` : text }],
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
247
|
// Properties and calls that read the committed layout. A state update in an
|
|
311
248
|
// effect that reads one of these is a measurement, which useLayoutSnapshot owns.
|
|
312
249
|
const LAYOUT_PROPERTIES = new Set([
|
|
@@ -21,7 +21,6 @@ import { createStrongRenderPolicy } from './strong-render-policy.js';
|
|
|
21
21
|
import {
|
|
22
22
|
collectLazyRefIdioms,
|
|
23
23
|
lazyRefSuggestion,
|
|
24
|
-
manualMemoSuggestion,
|
|
25
24
|
readsLayout,
|
|
26
25
|
strongDocsUrl,
|
|
27
26
|
} from './strong-fixes.js';
|
|
@@ -6259,12 +6258,8 @@ export function analyzeStrongMode(ast, source, filename, options = {}) {
|
|
|
6259
6258
|
...analyzeStrongExternalStore(ast, strongHookAnalysis, stateDiagnostic),
|
|
6260
6259
|
);
|
|
6261
6260
|
for (const policy of analyzeStrongHookPolicies(ast, options)) {
|
|
6262
|
-
const suggestions =
|
|
6263
|
-
policy.hook === 'useMemo' || policy.hook === 'useCallback'
|
|
6264
|
-
? [manualMemoSuggestion(policy.node, policy.hook, source)]
|
|
6265
|
-
: [];
|
|
6266
6261
|
diagnostics.push({
|
|
6267
|
-
...diagnostic(policy.code, filename, policy.node, policy.message
|
|
6262
|
+
...diagnostic(policy.code, filename, policy.node, policy.message),
|
|
6268
6263
|
severity: policy.severity,
|
|
6269
6264
|
});
|
|
6270
6265
|
}
|
package/dist/node/version.js
CHANGED
package/dist/version.js
CHANGED