@barefootjs/jsx 0.17.0 → 0.17.1
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/adapters/env-signal.d.ts +42 -7
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +47 -2
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +921 -726
- package/dist/ssr-seed-plan.d.ts +84 -0
- package/dist/ssr-seed-plan.d.ts.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/expression-parser.test.ts +11 -1
- package/src/__tests__/free-identifiers.test.ts +55 -0
- package/src/__tests__/materialize-getter-calls.test.ts +58 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +19 -0
- package/src/__tests__/ssr-seed-plan.test.ts +212 -0
- package/src/adapters/env-signal.ts +57 -9
- package/src/compiler.ts +6 -1
- package/src/expression-parser.ts +199 -9
- package/src/index.ts +7 -2
- package/src/ssr-seed-plan.ts +146 -0
- package/src/types.ts +11 -0
package/dist/index.js
CHANGED
|
@@ -352,6 +352,7 @@ function tsNodeToParsedExpr(node) {
|
|
|
352
352
|
}
|
|
353
353
|
var CALLBACK_METHODS = new Set([
|
|
354
354
|
"filter",
|
|
355
|
+
"map",
|
|
355
356
|
"every",
|
|
356
357
|
"some",
|
|
357
358
|
"find",
|
|
@@ -1813,6 +1814,57 @@ function stringifyParsedExpr(expr) {
|
|
|
1813
1814
|
return expr.raw;
|
|
1814
1815
|
}
|
|
1815
1816
|
}
|
|
1817
|
+
function materializeGetterCalls(expr, names) {
|
|
1818
|
+
const rw = (e) => materializeGetterCalls(e, names);
|
|
1819
|
+
switch (expr.kind) {
|
|
1820
|
+
case "call":
|
|
1821
|
+
if (expr.args.length === 0 && expr.callee.kind === "identifier" && names.has(expr.callee.name)) {
|
|
1822
|
+
return { kind: "identifier", name: expr.callee.name };
|
|
1823
|
+
}
|
|
1824
|
+
return { kind: "call", callee: rw(expr.callee), args: expr.args.map(rw) };
|
|
1825
|
+
case "binary":
|
|
1826
|
+
return { kind: "binary", op: expr.op, left: rw(expr.left), right: rw(expr.right) };
|
|
1827
|
+
case "logical":
|
|
1828
|
+
return { kind: "logical", op: expr.op, left: rw(expr.left), right: rw(expr.right) };
|
|
1829
|
+
case "unary":
|
|
1830
|
+
return { kind: "unary", op: expr.op, argument: rw(expr.argument) };
|
|
1831
|
+
case "conditional":
|
|
1832
|
+
return {
|
|
1833
|
+
kind: "conditional",
|
|
1834
|
+
test: rw(expr.test),
|
|
1835
|
+
consequent: rw(expr.consequent),
|
|
1836
|
+
alternate: rw(expr.alternate)
|
|
1837
|
+
};
|
|
1838
|
+
case "member":
|
|
1839
|
+
return { kind: "member", object: rw(expr.object), property: expr.property, computed: expr.computed };
|
|
1840
|
+
case "index-access":
|
|
1841
|
+
return { kind: "index-access", object: rw(expr.object), index: rw(expr.index) };
|
|
1842
|
+
case "template-literal":
|
|
1843
|
+
return {
|
|
1844
|
+
kind: "template-literal",
|
|
1845
|
+
parts: expr.parts.map((p) => p.type === "string" ? p : { type: "expression", expr: rw(p.expr) })
|
|
1846
|
+
};
|
|
1847
|
+
case "array-literal":
|
|
1848
|
+
return { kind: "array-literal", elements: expr.elements.map(rw) };
|
|
1849
|
+
case "array-method":
|
|
1850
|
+
if (expr.method === "flat")
|
|
1851
|
+
return { ...expr, object: rw(expr.object) };
|
|
1852
|
+
return { ...expr, object: rw(expr.object), args: expr.args.map(rw) };
|
|
1853
|
+
case "object-literal":
|
|
1854
|
+
return {
|
|
1855
|
+
kind: "object-literal",
|
|
1856
|
+
raw: expr.raw,
|
|
1857
|
+
properties: expr.properties.map((p) => ({ ...p, value: rw(p.value) }))
|
|
1858
|
+
};
|
|
1859
|
+
case "arrow":
|
|
1860
|
+
return { kind: "arrow", params: expr.params, body: rw(expr.body) };
|
|
1861
|
+
case "identifier":
|
|
1862
|
+
case "literal":
|
|
1863
|
+
case "regex":
|
|
1864
|
+
case "unsupported":
|
|
1865
|
+
return expr;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1816
1868
|
function serializeParsedExpr(expr) {
|
|
1817
1869
|
const node = toEvalNode(expr);
|
|
1818
1870
|
return node === null ? null : JSON.stringify(node);
|
|
@@ -1862,8 +1914,13 @@ function freeVarsInBody(body, params) {
|
|
|
1862
1914
|
for (const p of e.properties)
|
|
1863
1915
|
visit(p.value);
|
|
1864
1916
|
return;
|
|
1865
|
-
case "literal":
|
|
1866
1917
|
case "array-method":
|
|
1918
|
+
if (e.method === "includes") {
|
|
1919
|
+
visit(e.object);
|
|
1920
|
+
e.args.forEach(visit);
|
|
1921
|
+
}
|
|
1922
|
+
return;
|
|
1923
|
+
case "literal":
|
|
1867
1924
|
case "arrow":
|
|
1868
1925
|
case "regex":
|
|
1869
1926
|
case "unsupported":
|
|
@@ -1873,6 +1930,72 @@ function freeVarsInBody(body, params) {
|
|
|
1873
1930
|
visit(body);
|
|
1874
1931
|
return [...found].sort();
|
|
1875
1932
|
}
|
|
1933
|
+
function freeIdentifiers(expr) {
|
|
1934
|
+
const free = new Set;
|
|
1935
|
+
function visit(e, bound) {
|
|
1936
|
+
switch (e.kind) {
|
|
1937
|
+
case "literal":
|
|
1938
|
+
case "regex":
|
|
1939
|
+
return true;
|
|
1940
|
+
case "identifier":
|
|
1941
|
+
if (!bound.has(e.name))
|
|
1942
|
+
free.add(e.name);
|
|
1943
|
+
return true;
|
|
1944
|
+
case "call": {
|
|
1945
|
+
const isBuiltinCallee = evalBuiltinCalleeName(e.callee) !== null;
|
|
1946
|
+
if (!isBuiltinCallee && !visit(e.callee, bound))
|
|
1947
|
+
return false;
|
|
1948
|
+
for (const a of e.args)
|
|
1949
|
+
if (!visit(a, bound))
|
|
1950
|
+
return false;
|
|
1951
|
+
return true;
|
|
1952
|
+
}
|
|
1953
|
+
case "member":
|
|
1954
|
+
return visit(e.object, bound);
|
|
1955
|
+
case "index-access":
|
|
1956
|
+
return visit(e.object, bound) && visit(e.index, bound);
|
|
1957
|
+
case "binary":
|
|
1958
|
+
case "logical":
|
|
1959
|
+
return visit(e.left, bound) && visit(e.right, bound);
|
|
1960
|
+
case "unary":
|
|
1961
|
+
return visit(e.argument, bound);
|
|
1962
|
+
case "conditional":
|
|
1963
|
+
return visit(e.test, bound) && visit(e.consequent, bound) && visit(e.alternate, bound);
|
|
1964
|
+
case "template-literal":
|
|
1965
|
+
for (const p of e.parts) {
|
|
1966
|
+
if (p.type === "expression" && !visit(p.expr, bound))
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
return true;
|
|
1970
|
+
case "array-literal":
|
|
1971
|
+
for (const el of e.elements)
|
|
1972
|
+
if (!visit(el, bound))
|
|
1973
|
+
return false;
|
|
1974
|
+
return true;
|
|
1975
|
+
case "array-method":
|
|
1976
|
+
if (!visit(e.object, bound))
|
|
1977
|
+
return false;
|
|
1978
|
+
for (const a of e.args)
|
|
1979
|
+
if (!visit(a, bound))
|
|
1980
|
+
return false;
|
|
1981
|
+
return true;
|
|
1982
|
+
case "object-literal":
|
|
1983
|
+
for (const p of e.properties)
|
|
1984
|
+
if (!visit(p.value, bound))
|
|
1985
|
+
return false;
|
|
1986
|
+
return true;
|
|
1987
|
+
case "arrow": {
|
|
1988
|
+
const inner = new Set(bound);
|
|
1989
|
+
for (const p of e.params)
|
|
1990
|
+
inner.add(p);
|
|
1991
|
+
return visit(e.body, inner);
|
|
1992
|
+
}
|
|
1993
|
+
case "unsupported":
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return visit(expr, new Set) ? free : null;
|
|
1998
|
+
}
|
|
1876
1999
|
var EVAL_BINARY_OPS = new Set([
|
|
1877
2000
|
"+",
|
|
1878
2001
|
"-",
|
|
@@ -1998,7 +2121,14 @@ function toEvalNode(e) {
|
|
|
1998
2121
|
}
|
|
1999
2122
|
return { kind: "object-literal", properties };
|
|
2000
2123
|
}
|
|
2001
|
-
case "array-method":
|
|
2124
|
+
case "array-method": {
|
|
2125
|
+
if (e.method === "includes" && e.args.length === 1) {
|
|
2126
|
+
const object = toEvalNode(e.object);
|
|
2127
|
+
const arg = toEvalNode(e.args[0]);
|
|
2128
|
+
return object && arg ? { kind: "array-method", method: "includes", object, args: [arg] } : null;
|
|
2129
|
+
}
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2002
2132
|
case "arrow":
|
|
2003
2133
|
case "regex":
|
|
2004
2134
|
case "unsupported":
|
|
@@ -3582,8 +3712,8 @@ function propResolvesUnsafe(prop, env, unsafeLocalNames) {
|
|
|
3582
3712
|
}
|
|
3583
3713
|
if (!source)
|
|
3584
3714
|
return false;
|
|
3585
|
-
const { freeIdentifiers } = csrSubstitute(source, env);
|
|
3586
|
-
return setIntersects(
|
|
3715
|
+
const { freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
|
|
3716
|
+
return setIntersects(freeIdentifiers2, unsafeLocalNames);
|
|
3587
3717
|
}
|
|
3588
3718
|
function computeDeferredChildSlots(node, ctx, inlinableConstants, unsafeLocalNames, propsObjectName) {
|
|
3589
3719
|
const deferred = new Set;
|
|
@@ -3636,8 +3766,8 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
3636
3766
|
const source = templateExpr ?? expr;
|
|
3637
3767
|
if (!source)
|
|
3638
3768
|
return source;
|
|
3639
|
-
const { rewritten, freeIdentifiers } = csrSubstitute(source, env);
|
|
3640
|
-
if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(
|
|
3769
|
+
const { rewritten, freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
|
|
3770
|
+
if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(freeIdentifiers2, unsafeLocalNames)) {
|
|
3641
3771
|
return UNSAFE_TEMPLATE_EXPR;
|
|
3642
3772
|
}
|
|
3643
3773
|
return applyPropsRewrite(rewritten, propsObjectName ?? null);
|
|
@@ -6301,7 +6431,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
|
|
|
6301
6431
|
const baseValue = `${propsName}.${sourceKey}`;
|
|
6302
6432
|
const value2 = defaultValueExpr ? `${baseValue} ?? ${defaultValueExpr}` : baseValue;
|
|
6303
6433
|
const containsArrow2 = el.initializer ? nodeContainsArrow(el.initializer) : false;
|
|
6304
|
-
const
|
|
6434
|
+
const freeIdentifiers3 = el.initializer ? extractFreeIdentifiersFromNode(el.initializer) : new Set([propsName]);
|
|
6305
6435
|
ctx.localConstants.push({
|
|
6306
6436
|
name: localName,
|
|
6307
6437
|
value: value2,
|
|
@@ -6309,7 +6439,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
|
|
|
6309
6439
|
isExported,
|
|
6310
6440
|
type: null,
|
|
6311
6441
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
6312
|
-
freeIdentifiers:
|
|
6442
|
+
freeIdentifiers: freeIdentifiers3,
|
|
6313
6443
|
containsArrow: containsArrow2 || undefined,
|
|
6314
6444
|
origin: { phase: "hydrate", scope: "init", effect: "pure" }
|
|
6315
6445
|
});
|
|
@@ -6393,7 +6523,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
|
|
|
6393
6523
|
} else if (value) {
|
|
6394
6524
|
type = inferTypeFromValue(value);
|
|
6395
6525
|
}
|
|
6396
|
-
const
|
|
6526
|
+
const freeIdentifiers2 = node.initializer ? extractFreeIdentifiersFromNode(node.initializer) : undefined;
|
|
6397
6527
|
const containsArrow = node.initializer ? nodeContainsArrow(node.initializer) : false;
|
|
6398
6528
|
const systemConstructKind = node.initializer ? getSystemConstructKind(node.initializer) : undefined;
|
|
6399
6529
|
let templateValue;
|
|
@@ -6442,7 +6572,7 @@ function collectConstant(node, ctx, _isModule, declarationKind = "const", isExpo
|
|
|
6442
6572
|
isModule: isModule || undefined,
|
|
6443
6573
|
type,
|
|
6444
6574
|
loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath),
|
|
6445
|
-
freeIdentifiers,
|
|
6575
|
+
freeIdentifiers: freeIdentifiers2,
|
|
6446
6576
|
isJsx,
|
|
6447
6577
|
isJsxFunction: isJsxFunction || undefined,
|
|
6448
6578
|
containsArrow: containsArrow || undefined,
|
|
@@ -9843,14 +9973,14 @@ function processAttributes(attributes, ctx) {
|
|
|
9843
9973
|
clientOnly = true;
|
|
9844
9974
|
}
|
|
9845
9975
|
}
|
|
9846
|
-
const
|
|
9976
|
+
const freeIdentifiers2 = attrFreeIdentifiers(attr);
|
|
9847
9977
|
attrs.push({
|
|
9848
9978
|
name,
|
|
9849
9979
|
value,
|
|
9850
9980
|
clientOnly,
|
|
9851
9981
|
loc: getSourceLocation(attr, ctx.sourceFile, ctx.filePath),
|
|
9852
9982
|
...computeReactivityFlags(attr, ctx),
|
|
9853
|
-
...
|
|
9983
|
+
...freeIdentifiers2 !== undefined && { freeIdentifiers: freeIdentifiers2 }
|
|
9854
9984
|
});
|
|
9855
9985
|
}
|
|
9856
9986
|
return { attrs, events, ref };
|
|
@@ -10173,14 +10303,14 @@ function processComponentProps(attributes, ctx) {
|
|
|
10173
10303
|
clientOnly = true;
|
|
10174
10304
|
}
|
|
10175
10305
|
}
|
|
10176
|
-
const
|
|
10306
|
+
const freeIdentifiers2 = attrFreeIdentifiers(attr);
|
|
10177
10307
|
props.push({
|
|
10178
10308
|
name,
|
|
10179
10309
|
value,
|
|
10180
10310
|
clientOnly,
|
|
10181
10311
|
loc: getSourceLocation(attr, ctx.sourceFile, ctx.filePath),
|
|
10182
10312
|
...computeReactivityFlags(attr, ctx),
|
|
10183
|
-
...
|
|
10313
|
+
...freeIdentifiers2 !== undefined && { freeIdentifiers: freeIdentifiers2 }
|
|
10184
10314
|
});
|
|
10185
10315
|
}
|
|
10186
10316
|
return props;
|
|
@@ -10596,7 +10726,7 @@ function decideWrapForChildProp(expandedValue, ctx, prop) {
|
|
|
10596
10726
|
return { wrap: true, reason: "props-access" };
|
|
10597
10727
|
return decideWrapForAttr(expandedValue, ctx, prop);
|
|
10598
10728
|
}
|
|
10599
|
-
function needsEffectWrapper(expr, ctx,
|
|
10729
|
+
function needsEffectWrapper(expr, ctx, freeIdentifiers2) {
|
|
10600
10730
|
for (const signal of ctx.signals) {
|
|
10601
10731
|
if (new RegExp(`\\b${signal.getter}\\s*\\(`).test(expr)) {
|
|
10602
10732
|
return true;
|
|
@@ -10610,7 +10740,7 @@ function needsEffectWrapper(expr, ctx, freeIdentifiers) {
|
|
|
10610
10740
|
for (const prop of ctx.propsParams) {
|
|
10611
10741
|
if (prop.name === "children")
|
|
10612
10742
|
continue;
|
|
10613
|
-
if (
|
|
10743
|
+
if (freeIdentifiers2 ? freeIdentifiers2.has(prop.name) : tokenContainsIdent(expr, prop.name)) {
|
|
10614
10744
|
return true;
|
|
10615
10745
|
}
|
|
10616
10746
|
}
|
|
@@ -10621,8 +10751,8 @@ function needsEffectWrapper(expr, ctx, freeIdentifiers) {
|
|
|
10621
10751
|
}
|
|
10622
10752
|
return false;
|
|
10623
10753
|
}
|
|
10624
|
-
function classifyReactivity(expr, ctx, loopParam, loopParamBindings,
|
|
10625
|
-
const has = (name) =>
|
|
10754
|
+
function classifyReactivity(expr, ctx, loopParam, loopParamBindings, freeIdentifiers2) {
|
|
10755
|
+
const has = (name) => freeIdentifiers2 ? freeIdentifiers2.has(name) : tokenContainsIdent(expr, name);
|
|
10626
10756
|
if (loopParamBindings && loopParamBindings.length > 0) {
|
|
10627
10757
|
for (const b of loopParamBindings) {
|
|
10628
10758
|
if (has(b.name)) {
|
|
@@ -10632,7 +10762,7 @@ function classifyReactivity(expr, ctx, loopParam, loopParamBindings, freeIdentif
|
|
|
10632
10762
|
} else if (loopParam && has(loopParam)) {
|
|
10633
10763
|
return { kind: "loop-param", param: loopParam };
|
|
10634
10764
|
}
|
|
10635
|
-
if (needsEffectWrapper(expr, ctx,
|
|
10765
|
+
if (needsEffectWrapper(expr, ctx, freeIdentifiers2)) {
|
|
10636
10766
|
return { kind: "signal-or-memo-or-prop" };
|
|
10637
10767
|
}
|
|
10638
10768
|
return { kind: "none" };
|
|
@@ -12634,9 +12764,9 @@ function populateCsrInlinable(ctx, relocateEnv) {
|
|
|
12634
12764
|
continue;
|
|
12635
12765
|
const env = buildEnvWithConsts();
|
|
12636
12766
|
const source = c.value.trim();
|
|
12637
|
-
const { rewritten, freeIdentifiers } = csrSubstitute(source, env);
|
|
12767
|
+
const { rewritten, freeIdentifiers: freeIdentifiers2 } = csrSubstitute(source, env);
|
|
12638
12768
|
let pendingDependency = false;
|
|
12639
|
-
for (const id of
|
|
12769
|
+
for (const id of freeIdentifiers2) {
|
|
12640
12770
|
if (id === c.name)
|
|
12641
12771
|
continue;
|
|
12642
12772
|
const dep = ctx.localConstants.find((o) => o.name === id);
|
|
@@ -12652,7 +12782,7 @@ function populateCsrInlinable(ctx, relocateEnv) {
|
|
|
12652
12782
|
ctx.csrInlinable.set(c.name, null);
|
|
12653
12783
|
} else {
|
|
12654
12784
|
const bridgedRewritten = inlineResult.rewrittenValue;
|
|
12655
|
-
const bridgedFreeIdentifiers = recomputeFreeIdentifiers(bridgedRewritten,
|
|
12785
|
+
const bridgedFreeIdentifiers = recomputeFreeIdentifiers(bridgedRewritten, freeIdentifiers2);
|
|
12656
12786
|
ctx.csrInlinable.set(c.name, { rewrittenValue: bridgedRewritten, freeIdentifiers: bridgedFreeIdentifiers });
|
|
12657
12787
|
constSubs.set(c.name, {
|
|
12658
12788
|
kind: "identifier",
|
|
@@ -13199,14 +13329,26 @@ function sortDeclarations(declarations, declNameSet, graph) {
|
|
|
13199
13329
|
var ENV_SIGNAL_CLIENT_FACTORY = {
|
|
13200
13330
|
search: "createSearchParams"
|
|
13201
13331
|
};
|
|
13202
|
-
|
|
13332
|
+
var ENV_SIGNAL_READERS = new Map([
|
|
13333
|
+
["search", { key: "search", canonicalName: "searchParams", methods: new Set(["get"]) }]
|
|
13334
|
+
]);
|
|
13335
|
+
function envSignalReaderFor(key) {
|
|
13336
|
+
if (key === undefined)
|
|
13337
|
+
return null;
|
|
13338
|
+
return ENV_SIGNAL_READERS.get(key) ?? null;
|
|
13339
|
+
}
|
|
13340
|
+
function envSignalLocalNames(metadata, key) {
|
|
13203
13341
|
const names = new Set;
|
|
13204
13342
|
for (const s of metadata.signals) {
|
|
13205
|
-
if (s.envReader ===
|
|
13343
|
+
if (s.envReader !== undefined && (key === undefined || s.envReader === key)) {
|
|
13206
13344
|
names.add(s.getter);
|
|
13345
|
+
}
|
|
13207
13346
|
}
|
|
13208
13347
|
return names;
|
|
13209
13348
|
}
|
|
13349
|
+
function searchParamsLocalNames(metadata) {
|
|
13350
|
+
return envSignalLocalNames(metadata, "search");
|
|
13351
|
+
}
|
|
13210
13352
|
function importsSearchParams(metadata) {
|
|
13211
13353
|
return searchParamsLocalNames(metadata).size > 0;
|
|
13212
13354
|
}
|
|
@@ -17897,123 +18039,503 @@ function evalNode(node, ctx) {
|
|
|
17897
18039
|
return UNRESOLVED;
|
|
17898
18040
|
}
|
|
17899
18041
|
|
|
17900
|
-
// src/
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
const
|
|
17904
|
-
const
|
|
17905
|
-
const
|
|
17906
|
-
|
|
17907
|
-
const seenOther = new Set;
|
|
17908
|
-
const fold = (src, rawNames, idx, names, render) => {
|
|
17909
|
-
if (!idx.has(src)) {
|
|
17910
|
-
idx.set(src, result.length);
|
|
17911
|
-
names.set(src, new Set);
|
|
17912
|
-
result.push("");
|
|
17913
|
-
}
|
|
17914
|
-
const set = names.get(src);
|
|
17915
|
-
for (const n of rawNames.split(",").map((s) => s.trim()).filter(Boolean))
|
|
17916
|
-
set.add(n);
|
|
17917
|
-
result[idx.get(src)] = render(src, set);
|
|
17918
|
-
};
|
|
17919
|
-
for (const raw of lines) {
|
|
17920
|
-
const line = raw.trim();
|
|
17921
|
-
if (!line)
|
|
18042
|
+
// src/augment-inherited-props.ts
|
|
18043
|
+
import ts17 from "typescript";
|
|
18044
|
+
function collectContextConsumers(metadata) {
|
|
18045
|
+
const constants = metadata.localConstants ?? [];
|
|
18046
|
+
const contextDefaults = new Map;
|
|
18047
|
+
for (const c of constants) {
|
|
18048
|
+
if (c.systemConstructKind !== "createContext" || c.value === undefined)
|
|
17922
18049
|
continue;
|
|
17923
|
-
|
|
17924
|
-
const valueMatch = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
|
|
17925
|
-
if (valueMatch) {
|
|
17926
|
-
fold(valueMatch[2], valueMatch[1], valueIdx, valueNames, (s, n) => `import { ${[...n].join(", ")} } from '${s}'`);
|
|
17927
|
-
} else if (typeMatch) {
|
|
17928
|
-
fold(typeMatch[2], typeMatch[1], typeIdx, typeNames, (s, n) => `import type { ${[...n].join(", ")} } from '${s}'`);
|
|
17929
|
-
} else if (!seenOther.has(line)) {
|
|
17930
|
-
seenOther.add(line);
|
|
17931
|
-
result.push(line);
|
|
17932
|
-
}
|
|
18050
|
+
contextDefaults.set(c.name, parseCreateContextDefault(c.value));
|
|
17933
18051
|
}
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
const errors = [];
|
|
17940
|
-
const adapter = options.adapter;
|
|
17941
|
-
const entries = [];
|
|
17942
|
-
const program = options.program ?? (needsTypeBasedDetection(source) ? createProgramForFile(source, filePath)?.program : undefined);
|
|
17943
|
-
for (const componentName of componentNames) {
|
|
17944
|
-
const ctx = analyzeComponent(source, filePath, componentName, program);
|
|
17945
|
-
if (!ctx.jsxReturn) {
|
|
17946
|
-
errors.push(...ctx.errors);
|
|
18052
|
+
if (contextDefaults.size === 0)
|
|
18053
|
+
return [];
|
|
18054
|
+
const consumers = [];
|
|
18055
|
+
for (const c of constants) {
|
|
18056
|
+
if (c.value === undefined)
|
|
17947
18057
|
continue;
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
errors.push(...ctx.errors);
|
|
17951
|
-
if (!ir)
|
|
18058
|
+
const ctxName = parseUseContextArg(c.value);
|
|
18059
|
+
if (ctxName === null || !contextDefaults.has(ctxName))
|
|
17952
18060
|
continue;
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
};
|
|
17959
|
-
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
|
|
17960
|
-
if (options.cssLayerPrefix) {
|
|
17961
|
-
applyCssLayerPrefix(componentIR, options.cssLayerPrefix);
|
|
17962
|
-
}
|
|
17963
|
-
entries.push({ componentIR, ctx });
|
|
17964
|
-
}
|
|
17965
|
-
if (options.outputIR) {
|
|
17966
|
-
for (const { componentIR } of entries) {
|
|
17967
|
-
const componentName = componentIR.metadata.componentName;
|
|
17968
|
-
files.push({
|
|
17969
|
-
path: filePath.replace(/\.tsx?$/, `.${componentName}.ir.json`),
|
|
17970
|
-
content: JSON.stringify(componentIR, null, 2),
|
|
17971
|
-
type: "ir"
|
|
17972
|
-
});
|
|
17973
|
-
}
|
|
17974
|
-
}
|
|
17975
|
-
const allOutputs = [];
|
|
17976
|
-
const defaultExportName = entries.find((e) => e.componentIR.metadata.hasDefaultExport)?.componentIR.metadata.componentName;
|
|
17977
|
-
const fileWideInlineExported = new Set;
|
|
17978
|
-
for (const { componentIR } of entries) {
|
|
17979
|
-
for (const name of collectInlineExportedNames(componentIR)) {
|
|
17980
|
-
fileWideInlineExported.add(name);
|
|
17981
|
-
}
|
|
17982
|
-
}
|
|
17983
|
-
const moduleConstantsSet = new Set;
|
|
17984
|
-
const moduleConstantsOrdered = [];
|
|
17985
|
-
const fileScope = computeFileScope(filePath);
|
|
17986
|
-
const nonExportedSiblings = new Set;
|
|
17987
|
-
for (const { componentIR } of entries) {
|
|
17988
|
-
if (!componentIR.metadata.isExported) {
|
|
17989
|
-
nonExportedSiblings.add(componentIR.metadata.componentName);
|
|
17990
|
-
}
|
|
18061
|
+
consumers.push({
|
|
18062
|
+
localName: c.name,
|
|
18063
|
+
contextName: ctxName,
|
|
18064
|
+
defaultValue: contextDefaults.get(ctxName) ?? null
|
|
18065
|
+
});
|
|
17991
18066
|
}
|
|
17992
|
-
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
18067
|
+
return consumers;
|
|
18068
|
+
}
|
|
18069
|
+
function parseUseContextArg(source) {
|
|
18070
|
+
const expr = parseSingleExpression(source);
|
|
18071
|
+
if (!expr || !ts17.isCallExpression(expr))
|
|
18072
|
+
return null;
|
|
18073
|
+
if (!ts17.isIdentifier(expr.expression) || expr.expression.text !== "useContext")
|
|
18074
|
+
return null;
|
|
18075
|
+
if (expr.arguments.length !== 1)
|
|
18076
|
+
return null;
|
|
18077
|
+
const arg = expr.arguments[0];
|
|
18078
|
+
return ts17.isIdentifier(arg) ? arg.text : null;
|
|
18079
|
+
}
|
|
18080
|
+
function parseCreateContextDefault(source) {
|
|
18081
|
+
const expr = parseSingleExpression(source);
|
|
18082
|
+
if (!expr || !ts17.isCallExpression(expr))
|
|
18083
|
+
return null;
|
|
18084
|
+
if (expr.arguments.length === 0)
|
|
18085
|
+
return null;
|
|
18086
|
+
const arg = expr.arguments[0];
|
|
18087
|
+
if (ts17.isStringLiteral(arg) || ts17.isNoSubstitutionTemplateLiteral(arg))
|
|
18088
|
+
return arg.text;
|
|
18089
|
+
if (ts17.isNumericLiteral(arg))
|
|
18090
|
+
return Number(arg.text);
|
|
18091
|
+
if (arg.kind === ts17.SyntaxKind.TrueKeyword)
|
|
18092
|
+
return true;
|
|
18093
|
+
if (arg.kind === ts17.SyntaxKind.FalseKeyword)
|
|
18094
|
+
return false;
|
|
18095
|
+
return null;
|
|
18096
|
+
}
|
|
18097
|
+
function parseSingleExpression(source) {
|
|
18098
|
+
const sf = ts17.createSourceFile("__ctx.ts", `(${source})`, ts17.ScriptTarget.Latest, false);
|
|
18099
|
+
const stmt = sf.statements[0];
|
|
18100
|
+
if (!stmt || !ts17.isExpressionStatement(stmt))
|
|
18101
|
+
return null;
|
|
18102
|
+
let e = stmt.expression;
|
|
18103
|
+
while (ts17.isParenthesizedExpression(e))
|
|
18104
|
+
e = e.expression;
|
|
18105
|
+
return e;
|
|
18106
|
+
}
|
|
18107
|
+
function augmentInheritedPropAccesses(ir) {
|
|
18108
|
+
const propsObj = ir.metadata.propsObjectName;
|
|
18109
|
+
if (!propsObj)
|
|
18110
|
+
return;
|
|
18111
|
+
const existing = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
18112
|
+
const bareRefProps = new Set;
|
|
18113
|
+
const booleanAttrProps = new Set;
|
|
18114
|
+
const accessed = new Set;
|
|
18115
|
+
const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
|
|
18116
|
+
const scan = (s) => {
|
|
18117
|
+
if (!s)
|
|
18118
|
+
return;
|
|
18119
|
+
for (const m of s.matchAll(accessRe))
|
|
18120
|
+
accessed.add(m[1]);
|
|
17996
18121
|
};
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
18122
|
+
for (const memo of ir.metadata.memos)
|
|
18123
|
+
scan(memo.computation);
|
|
18124
|
+
for (const signal of ir.metadata.signals)
|
|
18125
|
+
scan(signal.initialValue);
|
|
18126
|
+
for (const stmt of ir.metadata.initStatements ?? [])
|
|
18127
|
+
scan(stmt.body);
|
|
18128
|
+
for (const eff of ir.metadata.effects ?? [])
|
|
18129
|
+
scan(eff.body);
|
|
18130
|
+
for (const c of ir.metadata.localConstants ?? []) {
|
|
18131
|
+
if (c.isModule)
|
|
18132
|
+
continue;
|
|
18133
|
+
scan(c.value);
|
|
18134
|
+
}
|
|
18135
|
+
const walk = (node) => {
|
|
18136
|
+
if (!node)
|
|
18137
|
+
return;
|
|
18138
|
+
const el = node;
|
|
18139
|
+
for (const attr of el.attrs ?? []) {
|
|
18140
|
+
const v = attr.value;
|
|
18141
|
+
if (v?.parts) {
|
|
18142
|
+
for (const part of v.parts) {
|
|
18143
|
+
if (part.type === "string")
|
|
18144
|
+
scan(part.value);
|
|
18145
|
+
else if (part.type === "ternary") {
|
|
18146
|
+
scan(part.condition);
|
|
18147
|
+
scan(part.whenTrue);
|
|
18148
|
+
scan(part.whenFalse);
|
|
18149
|
+
} else if (part.type === "lookup")
|
|
18150
|
+
scan(part.key);
|
|
18151
|
+
}
|
|
18152
|
+
}
|
|
18153
|
+
if (v?.kind === "expression" && typeof v.expr === "string") {
|
|
18154
|
+
scan(v.expr);
|
|
18155
|
+
const expr = v.expr.trim();
|
|
18156
|
+
const prefix = `${propsObj}.`;
|
|
18157
|
+
if (isBooleanAttr(attr.name) || v.presenceOrUndefined) {
|
|
18158
|
+
const m = expr.match(new RegExp(`^${propsObj}\\.([A-Za-z_$][\\w$]*)`));
|
|
18159
|
+
if (m)
|
|
18160
|
+
booleanAttrProps.add(m[1]);
|
|
18161
|
+
} else if (expr.startsWith(prefix)) {
|
|
18162
|
+
const rest = expr.slice(prefix.length);
|
|
18163
|
+
if (/^[A-Za-z_$][\w$]*$/.test(rest))
|
|
18164
|
+
bareRefProps.add(rest);
|
|
18165
|
+
}
|
|
18166
|
+
}
|
|
18167
|
+
}
|
|
18168
|
+
for (const child of el.children ?? []) {
|
|
18169
|
+
const c = child;
|
|
18170
|
+
walk(c.element ?? child);
|
|
18171
|
+
}
|
|
18172
|
+
const branchy = node;
|
|
18173
|
+
walk(branchy.whenTrue);
|
|
18174
|
+
walk(branchy.whenFalse);
|
|
18175
|
+
walk(branchy.consequent);
|
|
18176
|
+
walk(branchy.alternate);
|
|
18177
|
+
};
|
|
18178
|
+
walk(ir.root);
|
|
18179
|
+
for (const name of accessed) {
|
|
18180
|
+
if (existing.has(name))
|
|
18181
|
+
continue;
|
|
18182
|
+
let raw;
|
|
18183
|
+
if (booleanAttrProps.has(name))
|
|
18184
|
+
raw = "boolean";
|
|
18185
|
+
else if (bareRefProps.has(name))
|
|
18186
|
+
raw = "unknown";
|
|
18187
|
+
else
|
|
18188
|
+
raw = "string";
|
|
18189
|
+
const type = raw === "boolean" ? { kind: "primitive", raw: "boolean", primitive: "boolean" } : raw === "string" ? { kind: "primitive", raw: "string", primitive: "string" } : { kind: "unknown", raw: "unknown" };
|
|
18190
|
+
ir.metadata.propsParams.push({ name, type, optional: true });
|
|
18191
|
+
existing.add(name);
|
|
18192
|
+
}
|
|
18193
|
+
}
|
|
18194
|
+
function parseStaticStringConst(source) {
|
|
18195
|
+
const sf = ts17.createSourceFile("__const.ts", `const __x = (${source});`, ts17.ScriptTarget.Latest, false);
|
|
18196
|
+
const stmt = sf.statements[0];
|
|
18197
|
+
if (!stmt || !ts17.isVariableStatement(stmt))
|
|
18198
|
+
return null;
|
|
18199
|
+
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
18200
|
+
while (init && ts17.isParenthesizedExpression(init))
|
|
18201
|
+
init = init.expression;
|
|
18202
|
+
if (!init)
|
|
18203
|
+
return null;
|
|
18204
|
+
if (ts17.isStringLiteral(init) || ts17.isNoSubstitutionTemplateLiteral(init)) {
|
|
18205
|
+
return init.text;
|
|
18206
|
+
}
|
|
18207
|
+
return evalStringArrayJoin(source);
|
|
18208
|
+
}
|
|
18209
|
+
function evalTemplateOfStringConsts(source, resolved) {
|
|
18210
|
+
const sf = ts17.createSourceFile("__const.ts", `const __x = (${source});`, ts17.ScriptTarget.Latest, false);
|
|
18211
|
+
const stmt = sf.statements[0];
|
|
18212
|
+
if (!stmt || !ts17.isVariableStatement(stmt))
|
|
18213
|
+
return null;
|
|
18214
|
+
let init = stmt.declarationList.declarations[0]?.initializer;
|
|
18215
|
+
while (init && ts17.isParenthesizedExpression(init))
|
|
18216
|
+
init = init.expression;
|
|
18217
|
+
if (!init || !ts17.isTemplateExpression(init))
|
|
18218
|
+
return null;
|
|
18219
|
+
let out = init.head.text;
|
|
18220
|
+
for (const span of init.templateSpans) {
|
|
18221
|
+
if (!ts17.isIdentifier(span.expression))
|
|
18222
|
+
return null;
|
|
18223
|
+
const value = resolved.get(span.expression.text);
|
|
18224
|
+
if (value === undefined)
|
|
18225
|
+
return null;
|
|
18226
|
+
out += value + span.literal.text;
|
|
18227
|
+
}
|
|
18228
|
+
return out;
|
|
18229
|
+
}
|
|
18230
|
+
function collectModuleStringConsts(constants) {
|
|
18231
|
+
const map = new Map;
|
|
18232
|
+
const candidates = (constants ?? []).filter((c) => c.isModule && c.value !== undefined);
|
|
18233
|
+
let progressed = true;
|
|
18234
|
+
while (progressed) {
|
|
18235
|
+
progressed = false;
|
|
18236
|
+
for (const c of candidates) {
|
|
18237
|
+
if (map.has(c.name))
|
|
18238
|
+
continue;
|
|
18239
|
+
const literal = parseStaticStringConst(c.value) ?? evalTemplateOfStringConsts(c.value, map);
|
|
18240
|
+
if (literal !== null) {
|
|
18241
|
+
map.set(c.name, literal);
|
|
18242
|
+
progressed = true;
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18245
|
+
}
|
|
18246
|
+
return map;
|
|
18247
|
+
}
|
|
18248
|
+
function lookupStaticRecordLiteral(objectName, key, constants) {
|
|
18249
|
+
const constInfo = (constants ?? []).find((c) => c.name === objectName && c.isModule);
|
|
18250
|
+
if (constInfo?.value === undefined)
|
|
18251
|
+
return null;
|
|
18252
|
+
const sf = ts17.createSourceFile("__rec.ts", `(${constInfo.value})`, ts17.ScriptTarget.Latest, true);
|
|
18253
|
+
if (sf.statements.length !== 1)
|
|
18254
|
+
return null;
|
|
18255
|
+
const stmt = sf.statements[0];
|
|
18256
|
+
if (!ts17.isExpressionStatement(stmt))
|
|
18257
|
+
return null;
|
|
18258
|
+
let parsed = stmt.expression;
|
|
18259
|
+
while (ts17.isParenthesizedExpression(parsed))
|
|
18260
|
+
parsed = parsed.expression;
|
|
18261
|
+
if (!ts17.isObjectLiteralExpression(parsed))
|
|
18262
|
+
return null;
|
|
18263
|
+
for (const prop of parsed.properties) {
|
|
18264
|
+
if (!ts17.isPropertyAssignment(prop))
|
|
18265
|
+
continue;
|
|
18266
|
+
const name = prop.name;
|
|
18267
|
+
const propKey = ts17.isIdentifier(name) || ts17.isStringLiteral(name) || ts17.isNoSubstitutionTemplateLiteral(name) ? name.text : null;
|
|
18268
|
+
if (propKey !== key)
|
|
18269
|
+
continue;
|
|
18270
|
+
let v = prop.initializer;
|
|
18271
|
+
while (ts17.isParenthesizedExpression(v))
|
|
18272
|
+
v = v.expression;
|
|
18273
|
+
if (ts17.isNumericLiteral(v))
|
|
18274
|
+
return { kind: "number", text: v.text };
|
|
18275
|
+
if (ts17.isStringLiteral(v) || ts17.isNoSubstitutionTemplateLiteral(v)) {
|
|
18276
|
+
return { kind: "string", text: v.text };
|
|
18277
|
+
}
|
|
18278
|
+
return null;
|
|
18279
|
+
}
|
|
18280
|
+
return null;
|
|
18281
|
+
}
|
|
18282
|
+
function evalStringArrayJoin(source) {
|
|
18283
|
+
const sf = ts17.createSourceFile("__join.ts", `const __x = (${source});`, ts17.ScriptTarget.Latest, false);
|
|
18284
|
+
const stmt = sf.statements[0];
|
|
18285
|
+
if (!stmt || !ts17.isVariableStatement(stmt))
|
|
18286
|
+
return null;
|
|
18287
|
+
let node = stmt.declarationList.declarations[0]?.initializer;
|
|
18288
|
+
while (node && ts17.isParenthesizedExpression(node))
|
|
18289
|
+
node = node.expression;
|
|
18290
|
+
if (!node || !ts17.isCallExpression(node))
|
|
18291
|
+
return null;
|
|
18292
|
+
const callee = node.expression;
|
|
18293
|
+
if (!ts17.isPropertyAccessExpression(callee))
|
|
18294
|
+
return null;
|
|
18295
|
+
if (callee.name.text !== "join")
|
|
18296
|
+
return null;
|
|
18297
|
+
let recv = callee.expression;
|
|
18298
|
+
while (ts17.isParenthesizedExpression(recv))
|
|
18299
|
+
recv = recv.expression;
|
|
18300
|
+
if (!ts17.isArrayLiteralExpression(recv))
|
|
18301
|
+
return null;
|
|
18302
|
+
const parts = [];
|
|
18303
|
+
for (const el of recv.elements) {
|
|
18304
|
+
if (ts17.isStringLiteral(el) || ts17.isNoSubstitutionTemplateLiteral(el)) {
|
|
18305
|
+
parts.push(el.text);
|
|
18306
|
+
} else {
|
|
18307
|
+
return null;
|
|
18308
|
+
}
|
|
18309
|
+
}
|
|
18310
|
+
let sep2 = ",";
|
|
18311
|
+
if (node.arguments.length >= 1) {
|
|
18312
|
+
const arg = node.arguments[0];
|
|
18313
|
+
if (ts17.isStringLiteral(arg) || ts17.isNoSubstitutionTemplateLiteral(arg))
|
|
18314
|
+
sep2 = arg.text;
|
|
18315
|
+
else
|
|
18316
|
+
return null;
|
|
18317
|
+
}
|
|
18318
|
+
return parts.join(sep2);
|
|
18319
|
+
}
|
|
18320
|
+
function parseRecordIndexAccess(val, localConstants, propsParams, resolveKey) {
|
|
18321
|
+
if (!ts17.isElementAccessExpression(val))
|
|
18322
|
+
return null;
|
|
18323
|
+
const obj = val.expression;
|
|
18324
|
+
const arg = val.argumentExpression;
|
|
18325
|
+
if (!ts17.isIdentifier(obj) || !ts17.isIdentifier(arg))
|
|
18326
|
+
return null;
|
|
18327
|
+
let indexPropName;
|
|
18328
|
+
let defaultKey;
|
|
18329
|
+
const resolved = resolveKey?.(arg.text);
|
|
18330
|
+
if (resolved) {
|
|
18331
|
+
indexPropName = resolved.propName;
|
|
18332
|
+
defaultKey = resolved.defaultLiteral;
|
|
18333
|
+
} else if (propsParams.some((p) => p.name === arg.text)) {
|
|
18334
|
+
indexPropName = arg.text;
|
|
18335
|
+
} else {
|
|
18336
|
+
return null;
|
|
18337
|
+
}
|
|
18338
|
+
const constInfo = localConstants.find((c) => c.name === obj.text && c.isModule);
|
|
18339
|
+
if (constInfo?.value === undefined)
|
|
18340
|
+
return null;
|
|
18341
|
+
const sf = ts17.createSourceFile("__rec.ts", `(${constInfo.value})`, ts17.ScriptTarget.Latest, true);
|
|
18342
|
+
if (sf.statements.length !== 1)
|
|
18343
|
+
return null;
|
|
18344
|
+
const stmt = sf.statements[0];
|
|
18345
|
+
if (!ts17.isExpressionStatement(stmt))
|
|
18346
|
+
return null;
|
|
18347
|
+
let parsed = stmt.expression;
|
|
18348
|
+
while (ts17.isParenthesizedExpression(parsed))
|
|
18349
|
+
parsed = parsed.expression;
|
|
18350
|
+
if (!ts17.isObjectLiteralExpression(parsed))
|
|
18351
|
+
return null;
|
|
18352
|
+
const entries = [];
|
|
18353
|
+
for (const prop of parsed.properties) {
|
|
18354
|
+
if (!ts17.isPropertyAssignment(prop))
|
|
18355
|
+
return null;
|
|
18356
|
+
let key;
|
|
18357
|
+
if (ts17.isIdentifier(prop.name)) {
|
|
18358
|
+
key = prop.name.text;
|
|
18359
|
+
} else if (ts17.isStringLiteral(prop.name) || ts17.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
18360
|
+
key = prop.name.text;
|
|
18361
|
+
} else {
|
|
18362
|
+
return null;
|
|
18363
|
+
}
|
|
18364
|
+
let v = prop.initializer;
|
|
18365
|
+
while (ts17.isParenthesizedExpression(v))
|
|
18366
|
+
v = v.expression;
|
|
18367
|
+
if (ts17.isNumericLiteral(v)) {
|
|
18368
|
+
entries.push({ key, value: { kind: "number", text: v.text } });
|
|
18369
|
+
} else if (ts17.isStringLiteral(v) || ts17.isNoSubstitutionTemplateLiteral(v)) {
|
|
18370
|
+
entries.push({ key, value: { kind: "string", text: v.text } });
|
|
18371
|
+
} else {
|
|
18372
|
+
return null;
|
|
18373
|
+
}
|
|
18374
|
+
}
|
|
18375
|
+
return { indexPropName, entries, defaultKey };
|
|
18376
|
+
}
|
|
18377
|
+
|
|
18378
|
+
// src/ssr-seed-plan.ts
|
|
18379
|
+
function classify2(name, origin, expr, parsed, available) {
|
|
18380
|
+
if (!isSupported(parsed).supported)
|
|
18381
|
+
return { kind: "opaque", name, origin };
|
|
18382
|
+
const frees = freeIdentifiers(parsed);
|
|
18383
|
+
if (frees === null)
|
|
18384
|
+
return { kind: "opaque", name, origin };
|
|
18385
|
+
for (const free of frees) {
|
|
18386
|
+
if (!available.has(free))
|
|
18387
|
+
return { kind: "opaque", name, origin };
|
|
18388
|
+
}
|
|
18389
|
+
return { kind: "derived", name, origin, expr, parsed, frees: [...frees] };
|
|
18390
|
+
}
|
|
18391
|
+
function computeSsrSeedPlan(metadata) {
|
|
18392
|
+
const baseScope = metadata.propsParams.map((p) => p.name);
|
|
18393
|
+
if (metadata.propsObjectName)
|
|
18394
|
+
baseScope.push(metadata.propsObjectName);
|
|
18395
|
+
for (const name of collectModuleStringConsts(metadata.localConstants).keys()) {
|
|
18396
|
+
baseScope.push(name);
|
|
18397
|
+
}
|
|
18398
|
+
const available = new Set(baseScope);
|
|
18399
|
+
const steps = [];
|
|
18400
|
+
for (const signal of metadata.signals) {
|
|
18401
|
+
if (signal.envReader) {
|
|
18402
|
+
const reader = envSignalReaderFor(signal.envReader);
|
|
18403
|
+
if (reader) {
|
|
18404
|
+
steps.push({ kind: "env-reader", name: signal.getter, reader });
|
|
18405
|
+
available.add(signal.getter);
|
|
18406
|
+
continue;
|
|
18407
|
+
}
|
|
18408
|
+
}
|
|
18409
|
+
const expr = signal.initialValue.trim();
|
|
18410
|
+
steps.push(expr === "" ? { kind: "opaque", name: signal.getter, origin: "signal" } : classify2(signal.getter, "signal", expr, parseExpression(expr), available));
|
|
18411
|
+
available.add(signal.getter);
|
|
18412
|
+
}
|
|
18413
|
+
for (const memo of metadata.memos) {
|
|
18414
|
+
const body = extractArrowBodyExpression(memo.computation);
|
|
18415
|
+
const expr = body?.trim() ?? "";
|
|
18416
|
+
steps.push(expr === "" ? { kind: "opaque", name: memo.name, origin: "memo" } : classify2(memo.name, "memo", expr, memo.parsed ?? parseExpression(expr), available));
|
|
18417
|
+
available.add(memo.name);
|
|
18418
|
+
}
|
|
18419
|
+
return { baseScope, steps };
|
|
18420
|
+
}
|
|
18421
|
+
|
|
18422
|
+
// src/compiler.ts
|
|
18423
|
+
function mergeTemplateImports(lines) {
|
|
18424
|
+
const result = [];
|
|
18425
|
+
const valueIdx = new Map;
|
|
18426
|
+
const valueNames = new Map;
|
|
18427
|
+
const typeIdx = new Map;
|
|
18428
|
+
const typeNames = new Map;
|
|
18429
|
+
const seenOther = new Set;
|
|
18430
|
+
const fold = (src, rawNames, idx, names, render) => {
|
|
18431
|
+
if (!idx.has(src)) {
|
|
18432
|
+
idx.set(src, result.length);
|
|
18433
|
+
names.set(src, new Set);
|
|
18434
|
+
result.push("");
|
|
18435
|
+
}
|
|
18436
|
+
const set = names.get(src);
|
|
18437
|
+
for (const n of rawNames.split(",").map((s) => s.trim()).filter(Boolean))
|
|
18438
|
+
set.add(n);
|
|
18439
|
+
result[idx.get(src)] = render(src, set);
|
|
18440
|
+
};
|
|
18441
|
+
for (const raw of lines) {
|
|
18442
|
+
const line = raw.trim();
|
|
18443
|
+
if (!line)
|
|
18444
|
+
continue;
|
|
18445
|
+
const typeMatch = line.match(/^import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
|
|
18446
|
+
const valueMatch = line.match(/^import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?$/);
|
|
18447
|
+
if (valueMatch) {
|
|
18448
|
+
fold(valueMatch[2], valueMatch[1], valueIdx, valueNames, (s, n) => `import { ${[...n].join(", ")} } from '${s}'`);
|
|
18449
|
+
} else if (typeMatch) {
|
|
18450
|
+
fold(typeMatch[2], typeMatch[1], typeIdx, typeNames, (s, n) => `import type { ${[...n].join(", ")} } from '${s}'`);
|
|
18451
|
+
} else if (!seenOther.has(line)) {
|
|
18452
|
+
seenOther.add(line);
|
|
18453
|
+
result.push(line);
|
|
18454
|
+
}
|
|
18455
|
+
}
|
|
18456
|
+
return result.filter(Boolean).join(`
|
|
18457
|
+
`);
|
|
18458
|
+
}
|
|
18459
|
+
function compileMultipleComponents(source, filePath, componentNames, options) {
|
|
18460
|
+
const files = [];
|
|
18461
|
+
const errors = [];
|
|
18462
|
+
const adapter = options.adapter;
|
|
18463
|
+
const entries = [];
|
|
18464
|
+
const program = options.program ?? (needsTypeBasedDetection(source) ? createProgramForFile(source, filePath)?.program : undefined);
|
|
18465
|
+
for (const componentName of componentNames) {
|
|
18466
|
+
const ctx = analyzeComponent(source, filePath, componentName, program);
|
|
18467
|
+
if (!ctx.jsxReturn) {
|
|
18468
|
+
errors.push(...ctx.errors);
|
|
18469
|
+
continue;
|
|
18470
|
+
}
|
|
18471
|
+
const ir = jsxToIR(ctx);
|
|
18472
|
+
errors.push(...ctx.errors);
|
|
18473
|
+
if (!ir)
|
|
18474
|
+
continue;
|
|
18475
|
+
const componentIR = {
|
|
18476
|
+
version: "0.1",
|
|
18477
|
+
metadata: buildMetadata(ctx),
|
|
18478
|
+
root: ir,
|
|
18479
|
+
errors: []
|
|
18480
|
+
};
|
|
18481
|
+
componentIR.metadata.clientAnalysis = analyzeClientNeeds(componentIR);
|
|
18482
|
+
if (options.cssLayerPrefix) {
|
|
18483
|
+
applyCssLayerPrefix(componentIR, options.cssLayerPrefix);
|
|
18484
|
+
}
|
|
18485
|
+
entries.push({ componentIR, ctx });
|
|
18486
|
+
}
|
|
18487
|
+
if (options.outputIR) {
|
|
18488
|
+
for (const { componentIR } of entries) {
|
|
18489
|
+
const componentName = componentIR.metadata.componentName;
|
|
18490
|
+
files.push({
|
|
18491
|
+
path: filePath.replace(/\.tsx?$/, `.${componentName}.ir.json`),
|
|
18492
|
+
content: JSON.stringify(componentIR, null, 2),
|
|
18493
|
+
type: "ir"
|
|
18494
|
+
});
|
|
18495
|
+
}
|
|
18496
|
+
}
|
|
18497
|
+
const allOutputs = [];
|
|
18498
|
+
const defaultExportName = entries.find((e) => e.componentIR.metadata.hasDefaultExport)?.componentIR.metadata.componentName;
|
|
18499
|
+
const fileWideInlineExported = new Set;
|
|
18500
|
+
for (const { componentIR } of entries) {
|
|
18501
|
+
for (const name of collectInlineExportedNames(componentIR)) {
|
|
18502
|
+
fileWideInlineExported.add(name);
|
|
18503
|
+
}
|
|
18504
|
+
}
|
|
18505
|
+
const moduleConstantsSet = new Set;
|
|
18506
|
+
const moduleConstantsOrdered = [];
|
|
18507
|
+
const fileScope = computeFileScope(filePath);
|
|
18508
|
+
const nonExportedSiblings = new Set;
|
|
18509
|
+
for (const { componentIR } of entries) {
|
|
18510
|
+
if (!componentIR.metadata.isExported) {
|
|
18511
|
+
nonExportedSiblings.add(componentIR.metadata.componentName);
|
|
18512
|
+
}
|
|
18513
|
+
}
|
|
18514
|
+
setActiveComponentScope({ fileScope, nonExportedSiblings });
|
|
18515
|
+
const multiAdapterCaps = {
|
|
18516
|
+
templatePrimitives: options.adapter.templatePrimitives,
|
|
18517
|
+
acceptsTemplateCall: options.adapter.acceptsTemplateCall
|
|
18518
|
+
};
|
|
18519
|
+
try {
|
|
18520
|
+
for (const { componentIR } of entries) {
|
|
18521
|
+
const scriptBaseName = options.scriptBaseName ?? (!componentIR.metadata.hasDefaultExport && defaultExportName ? defaultExportName : undefined);
|
|
18522
|
+
const adapterOutput = adapter.generate(componentIR, {
|
|
18523
|
+
scriptBaseName,
|
|
18524
|
+
siblingTemplatesRegistered: options.siblingTemplatesRegistered,
|
|
18525
|
+
rewriteRelativeImport: options.rewriteRelativeImport
|
|
18526
|
+
});
|
|
18527
|
+
const moduleExports = generateModuleExports(componentIR, fileWideInlineExported, options.rewriteRelativeImport);
|
|
18528
|
+
const s = adapterOutput.sections;
|
|
18529
|
+
const imports = s.imports;
|
|
18530
|
+
const types = s.types;
|
|
18531
|
+
const component = s.component + (s.defaultExport || "");
|
|
18532
|
+
const mc = s.moduleConstants;
|
|
18533
|
+
if (mc && !moduleConstantsSet.has(mc)) {
|
|
18534
|
+
moduleConstantsSet.add(mc);
|
|
18535
|
+
moduleConstantsOrdered.push(mc);
|
|
18536
|
+
}
|
|
18537
|
+
allOutputs.push({
|
|
18538
|
+
componentName: componentIR.metadata.componentName,
|
|
18017
18539
|
rawTemplate: adapterOutput.template,
|
|
18018
18540
|
imports,
|
|
18019
18541
|
types,
|
|
@@ -18209,7 +18731,7 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
|
|
|
18209
18731
|
return { files, errors };
|
|
18210
18732
|
}
|
|
18211
18733
|
function buildMetadata(ctx) {
|
|
18212
|
-
|
|
18734
|
+
const metadata = {
|
|
18213
18735
|
componentName: ctx.componentName || "Unknown",
|
|
18214
18736
|
hasDefaultExport: ctx.hasDefaultExport,
|
|
18215
18737
|
isExported: ctx.isExported,
|
|
@@ -18231,6 +18753,8 @@ function buildMetadata(ctx) {
|
|
|
18231
18753
|
localFunctions: ctx.localFunctions,
|
|
18232
18754
|
localConstants: ctx.localConstants
|
|
18233
18755
|
};
|
|
18756
|
+
metadata.ssrSeedPlan = computeSsrSeedPlan(metadata);
|
|
18757
|
+
return metadata;
|
|
18234
18758
|
}
|
|
18235
18759
|
function compileJSX(source, filePath, options) {
|
|
18236
18760
|
const files = [];
|
|
@@ -18393,7 +18917,7 @@ function compileJSX(source, filePath, options) {
|
|
|
18393
18917
|
return { files, errors };
|
|
18394
18918
|
}
|
|
18395
18919
|
// src/shared-program.ts
|
|
18396
|
-
import
|
|
18920
|
+
import ts18 from "typescript";
|
|
18397
18921
|
function commonParent(paths) {
|
|
18398
18922
|
if (paths.length === 0)
|
|
18399
18923
|
return process.cwd();
|
|
@@ -18414,10 +18938,10 @@ function commonParent(paths) {
|
|
|
18414
18938
|
function createProgramForCorpus(files, options = {}) {
|
|
18415
18939
|
const baseUrl = options.baseUrl ?? commonParent(files);
|
|
18416
18940
|
const compilerOptions = {
|
|
18417
|
-
target:
|
|
18418
|
-
module:
|
|
18419
|
-
moduleResolution:
|
|
18420
|
-
jsx:
|
|
18941
|
+
target: ts18.ScriptTarget.Latest,
|
|
18942
|
+
module: ts18.ModuleKind.ESNext,
|
|
18943
|
+
moduleResolution: ts18.ModuleResolutionKind.Bundler,
|
|
18944
|
+
jsx: ts18.JsxEmit.ReactJSX,
|
|
18421
18945
|
strict: true,
|
|
18422
18946
|
skipLibCheck: true,
|
|
18423
18947
|
noEmit: true,
|
|
@@ -18427,7 +18951,7 @@ function createProgramForCorpus(files, options = {}) {
|
|
|
18427
18951
|
...options.compilerOptions
|
|
18428
18952
|
};
|
|
18429
18953
|
const absolute = files.map((f) => path_default.resolve(f));
|
|
18430
|
-
return
|
|
18954
|
+
return ts18.createProgram(absolute, compilerOptions, undefined, options.oldProgram);
|
|
18431
18955
|
}
|
|
18432
18956
|
// src/adapters/interface.ts
|
|
18433
18957
|
class BaseAdapter {
|
|
@@ -19095,7 +19619,7 @@ function emitAttrValue(value, emitter, name) {
|
|
|
19095
19619
|
}
|
|
19096
19620
|
}
|
|
19097
19621
|
// src/combine-client-js.ts
|
|
19098
|
-
import
|
|
19622
|
+
import ts19 from "typescript";
|
|
19099
19623
|
var CHILD_PLACEHOLDER_RE = /import '\/\* @bf-child:(\w+) \*\/'/g;
|
|
19100
19624
|
function combineParentChildClientJs(files) {
|
|
19101
19625
|
const result = new Map;
|
|
@@ -19152,10 +19676,10 @@ function combineParentChildClientJs(files) {
|
|
|
19152
19676
|
return result;
|
|
19153
19677
|
}
|
|
19154
19678
|
function parseAndMerge(content, importsBySource, otherImports, codeSections) {
|
|
19155
|
-
const sourceFile =
|
|
19679
|
+
const sourceFile = ts19.createSourceFile("combine.js", content, ts19.ScriptTarget.Latest, false, ts19.ScriptKind.JS);
|
|
19156
19680
|
const importSpans = [];
|
|
19157
19681
|
for (const stmt of sourceFile.statements) {
|
|
19158
|
-
if (!
|
|
19682
|
+
if (!ts19.isImportDeclaration(stmt))
|
|
19159
19683
|
continue;
|
|
19160
19684
|
const start = stmt.getStart(sourceFile);
|
|
19161
19685
|
const end = stmt.getEnd();
|
|
@@ -19165,8 +19689,8 @@ function parseAndMerge(content, importsBySource, otherImports, codeSections) {
|
|
|
19165
19689
|
continue;
|
|
19166
19690
|
const clause = stmt.importClause;
|
|
19167
19691
|
const bindings = clause?.namedBindings;
|
|
19168
|
-
const specifier =
|
|
19169
|
-
if (clause && !clause.name && bindings &&
|
|
19692
|
+
const specifier = ts19.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
|
|
19693
|
+
if (clause && !clause.name && bindings && ts19.isNamedImports(bindings)) {
|
|
19170
19694
|
if (!importsBySource.has(specifier)) {
|
|
19171
19695
|
importsBySource.set(specifier, new Set);
|
|
19172
19696
|
}
|
|
@@ -19325,7 +19849,7 @@ function escapeRe(s) {
|
|
|
19325
19849
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19326
19850
|
}
|
|
19327
19851
|
// src/debug.ts
|
|
19328
|
-
import
|
|
19852
|
+
import ts20 from "typescript";
|
|
19329
19853
|
function buildComponentGraph(source, filePath, componentName) {
|
|
19330
19854
|
const ctx = analyzeComponent(source, filePath, componentName);
|
|
19331
19855
|
if (!ctx.jsxReturn) {
|
|
@@ -20610,7 +21134,7 @@ function truncateExpr(expr, max = 40) {
|
|
|
20610
21134
|
function exprReadsPropMember(expr, propsObjectName) {
|
|
20611
21135
|
let sf;
|
|
20612
21136
|
try {
|
|
20613
|
-
sf =
|
|
21137
|
+
sf = ts20.createSourceFile("__attr.tsx", `(${expr})`, ts20.ScriptTarget.Latest, true, ts20.ScriptKind.TSX);
|
|
20614
21138
|
} catch {
|
|
20615
21139
|
return false;
|
|
20616
21140
|
}
|
|
@@ -20618,11 +21142,11 @@ function exprReadsPropMember(expr, propsObjectName) {
|
|
|
20618
21142
|
const visit3 = (n) => {
|
|
20619
21143
|
if (found)
|
|
20620
21144
|
return;
|
|
20621
|
-
if (
|
|
21145
|
+
if (ts20.isPropertyAccessExpression(n) && ts20.isIdentifier(n.expression) && n.expression.text === propsObjectName && n.name.text !== "children") {
|
|
20622
21146
|
found = true;
|
|
20623
21147
|
return;
|
|
20624
21148
|
}
|
|
20625
|
-
|
|
21149
|
+
ts20.forEachChild(n, visit3);
|
|
20626
21150
|
};
|
|
20627
21151
|
visit3(sf);
|
|
20628
21152
|
return found;
|
|
@@ -20692,7 +21216,7 @@ function findSourceFile2(meta) {
|
|
|
20692
21216
|
return null;
|
|
20693
21217
|
}
|
|
20694
21218
|
// src/profiler.ts
|
|
20695
|
-
import
|
|
21219
|
+
import ts21 from "typescript";
|
|
20696
21220
|
var PROFILE_SCHEMA_VERSION = 1;
|
|
20697
21221
|
var DEFAULT_FANOUT_THRESHOLD = 8;
|
|
20698
21222
|
function buildStaticBudget(source, filePath, componentName, options = {}) {
|
|
@@ -20962,15 +21486,15 @@ function joinProfilerEvents(events, index) {
|
|
|
20962
21486
|
return { joined, unattributed, diagnostics };
|
|
20963
21487
|
}
|
|
20964
21488
|
function findUninstrumentedEffects(source, filePath, instrumentedLines) {
|
|
20965
|
-
const sf =
|
|
21489
|
+
const sf = ts21.createSourceFile(filePath, source, ts21.ScriptTarget.Latest, true, ts21.ScriptKind.TSX);
|
|
20966
21490
|
const out = [];
|
|
20967
21491
|
const visit3 = (node) => {
|
|
20968
|
-
if (
|
|
21492
|
+
if (ts21.isCallExpression(node) && ts21.isIdentifier(node.expression) && node.expression.text === "createEffect") {
|
|
20969
21493
|
const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
20970
21494
|
if (!instrumentedLines.has(line))
|
|
20971
21495
|
out.push({ file: filePath, line });
|
|
20972
21496
|
}
|
|
20973
|
-
|
|
21497
|
+
ts21.forEachChild(node, visit3);
|
|
20974
21498
|
};
|
|
20975
21499
|
visit3(sf);
|
|
20976
21500
|
out.sort((a, b) => a.line - b.line);
|
|
@@ -21278,13 +21802,13 @@ function assessBatchSafety(args) {
|
|
|
21278
21802
|
const signalGetters = new Set(args.graph.signals.map((s) => s.name));
|
|
21279
21803
|
let sf;
|
|
21280
21804
|
try {
|
|
21281
|
-
sf =
|
|
21805
|
+
sf = ts21.createSourceFile("__h.ts", `const __h = ${args.handler}`, ts21.ScriptTarget.Latest, true);
|
|
21282
21806
|
} catch {
|
|
21283
21807
|
return "unverified";
|
|
21284
21808
|
}
|
|
21285
21809
|
const calls = [];
|
|
21286
21810
|
const visit3 = (node) => {
|
|
21287
|
-
if (
|
|
21811
|
+
if (ts21.isCallExpression(node) && ts21.isIdentifier(node.expression)) {
|
|
21288
21812
|
const name = node.expression.text;
|
|
21289
21813
|
if (setters.has(name))
|
|
21290
21814
|
calls.push({ pos: node.getStart(sf), kind: "write" });
|
|
@@ -21293,7 +21817,7 @@ function assessBatchSafety(args) {
|
|
|
21293
21817
|
else if (!signalGetters.has(name) && !memoNames.has(name))
|
|
21294
21818
|
calls.push({ pos: node.getStart(sf), kind: "risky" });
|
|
21295
21819
|
}
|
|
21296
|
-
|
|
21820
|
+
ts21.forEachChild(node, visit3);
|
|
21297
21821
|
};
|
|
21298
21822
|
visit3(sf);
|
|
21299
21823
|
calls.sort((a, b) => a.pos - b.pos);
|
|
@@ -21664,602 +22188,267 @@ function computeMetrics(graph, eventSummary, hydrated) {
|
|
|
21664
22188
|
}
|
|
21665
22189
|
function computeMaxMemoChainDepth(graph) {
|
|
21666
22190
|
if (graph.memos.length === 0)
|
|
21667
|
-
return 0;
|
|
21668
|
-
const memoSet = new Set(graph.memos.map((m) => m.name));
|
|
21669
|
-
const memoDeps = new Map;
|
|
21670
|
-
for (const memo of graph.memos) {
|
|
21671
|
-
memoDeps.set(memo.name, memo.deps.filter((d) => memoSet.has(d)));
|
|
21672
|
-
}
|
|
21673
|
-
const cache = new Map;
|
|
21674
|
-
function depth(name, visited) {
|
|
21675
|
-
if (cache.has(name))
|
|
21676
|
-
return cache.get(name);
|
|
21677
|
-
if (visited.has(name))
|
|
21678
|
-
return 0;
|
|
21679
|
-
const children = memoDeps.get(name) ?? [];
|
|
21680
|
-
if (children.length === 0) {
|
|
21681
|
-
cache.set(name, 1);
|
|
21682
|
-
return 1;
|
|
21683
|
-
}
|
|
21684
|
-
visited.add(name);
|
|
21685
|
-
const d = 1 + Math.max(...children.map((c) => depth(c, new Set(visited))));
|
|
21686
|
-
cache.set(name, d);
|
|
21687
|
-
return d;
|
|
21688
|
-
}
|
|
21689
|
-
let max = 0;
|
|
21690
|
-
for (const memo of graph.memos) {
|
|
21691
|
-
const d = depth(memo.name, new Set);
|
|
21692
|
-
if (d > max)
|
|
21693
|
-
max = d;
|
|
21694
|
-
}
|
|
21695
|
-
return max;
|
|
21696
|
-
}
|
|
21697
|
-
function computeFindings(metrics, graph, eventSummary) {
|
|
21698
|
-
const findings = [];
|
|
21699
|
-
for (const signal of graph.signals) {
|
|
21700
|
-
if (signal.consumers.length > THRESHOLDS.highFanOut) {
|
|
21701
|
-
findings.push({
|
|
21702
|
-
kind: "high-fan-out",
|
|
21703
|
-
severity: "warning",
|
|
21704
|
-
signal: signal.name,
|
|
21705
|
-
message: `${signal.name} has ${signal.consumers.length} consumers (fan-out > ${THRESHOLDS.highFanOut})`,
|
|
21706
|
-
suggestion: `Split ${signal.name} into finer-grained signals, or add a createMemo to shield downstream consumers from unrelated updates`,
|
|
21707
|
-
loc: { file: signal.loc.file, line: signal.loc.line }
|
|
21708
|
-
});
|
|
21709
|
-
}
|
|
21710
|
-
}
|
|
21711
|
-
if (metrics.maxMemoChainDepth > THRESHOLDS.deepMemoChain) {
|
|
21712
|
-
findings.push({
|
|
21713
|
-
kind: "deep-memo-chain",
|
|
21714
|
-
severity: "warning",
|
|
21715
|
-
depth: metrics.maxMemoChainDepth,
|
|
21716
|
-
message: `Memo chain depth ${metrics.maxMemoChainDepth} (threshold: ${THRESHOLDS.deepMemoChain}) — a single signal update cascades through ${metrics.maxMemoChainDepth} memo levels`,
|
|
21717
|
-
suggestion: "Flatten intermediate memos that do not cache expensive computations; deep chains increase propagation latency"
|
|
21718
|
-
});
|
|
21719
|
-
}
|
|
21720
|
-
const batchSeen = new Set;
|
|
21721
|
-
for (const event of eventSummary.events) {
|
|
21722
|
-
const distinct = new Set;
|
|
21723
|
-
const setterNames = [];
|
|
21724
|
-
for (const sc of event.setterCalls) {
|
|
21725
|
-
if (sc.signal) {
|
|
21726
|
-
distinct.add(sc.signal);
|
|
21727
|
-
setterNames.push(sc.setter);
|
|
21728
|
-
}
|
|
21729
|
-
}
|
|
21730
|
-
if (distinct.size >= THRESHOLDS.batchMinSignals) {
|
|
21731
|
-
const dedupeKey = `${event.eventName}|${event.loc.file ?? ""}|${event.loc.start.line}|${[...distinct].sort().join(",")}`;
|
|
21732
|
-
if (batchSeen.has(dedupeKey))
|
|
21733
|
-
continue;
|
|
21734
|
-
batchSeen.add(dedupeKey);
|
|
21735
|
-
findings.push({
|
|
21736
|
-
kind: "batch-candidate",
|
|
21737
|
-
severity: "info",
|
|
21738
|
-
signals: [...distinct],
|
|
21739
|
-
message: `${event.eventName} on <${event.elementContext}> sets ${distinct.size} signals (${[...distinct].join(", ")}) — triggers ${distinct.size} separate update cycles (static; verify setters are not in separate if/else branches)`,
|
|
21740
|
-
suggestion: `If all listed setters fire unconditionally in the same handler path, wrap in batch(() => { ${setterNames.join("; ")}; }) to collapse ${distinct.size} cycles into 1`,
|
|
21741
|
-
loc: event.loc.file ? { file: event.loc.file, line: event.loc.start.line } : undefined
|
|
21742
|
-
});
|
|
21743
|
-
}
|
|
21744
|
-
}
|
|
21745
|
-
if (metrics.dynamicBindings > 0 && metrics.fallbacks >= THRESHOLDS.fallbackHeavyMin && metrics.fallbacks / metrics.dynamicBindings > THRESHOLDS.fallbackHeavyRatio) {
|
|
21746
|
-
findings.push({
|
|
21747
|
-
kind: "fallback-heavy",
|
|
21748
|
-
severity: "info",
|
|
21749
|
-
message: `${metrics.fallbacks}/${metrics.dynamicBindings} bindings (${Math.round(metrics.fallbacks / metrics.dynamicBindings * 100)}%) are fallback-wrapped — reactivity not statically provable`,
|
|
21750
|
-
suggestion: "Run `bf debug fallbacks` to see each expression and fix them so the compiler can prove reactivity without the fallback wrapper"
|
|
21751
|
-
});
|
|
21752
|
-
}
|
|
21753
|
-
return findings;
|
|
21754
|
-
}
|
|
21755
|
-
function diffProfiles(before, after) {
|
|
21756
|
-
const worseWhenHigher = new Set([
|
|
21757
|
-
"fallbacks",
|
|
21758
|
-
"maxSignalFanOut",
|
|
21759
|
-
"maxMemoChainDepth",
|
|
21760
|
-
"totalSubscriptions",
|
|
21761
|
-
"batchCandidateCount"
|
|
21762
|
-
]);
|
|
21763
|
-
const numericKeys = [
|
|
21764
|
-
"signals",
|
|
21765
|
-
"memos",
|
|
21766
|
-
"effects",
|
|
21767
|
-
"loops",
|
|
21768
|
-
"eventHandlers",
|
|
21769
|
-
"dynamicBindings",
|
|
21770
|
-
"fallbacks",
|
|
21771
|
-
"conditionals",
|
|
21772
|
-
"maxSignalFanOut",
|
|
21773
|
-
"maxMemoChainDepth",
|
|
21774
|
-
"totalSubscriptions",
|
|
21775
|
-
"batchCandidateCount"
|
|
21776
|
-
];
|
|
21777
|
-
const regressions = [];
|
|
21778
|
-
const improvements = [];
|
|
21779
|
-
const neutral = [];
|
|
21780
|
-
for (const key of numericKeys) {
|
|
21781
|
-
const b = before[key];
|
|
21782
|
-
const a = after[key];
|
|
21783
|
-
if (a === b)
|
|
21784
|
-
continue;
|
|
21785
|
-
const entry = { metric: key, before: b, after: a, delta: a - b };
|
|
21786
|
-
if (worseWhenHigher.has(key)) {
|
|
21787
|
-
if (a > b)
|
|
21788
|
-
regressions.push(entry);
|
|
21789
|
-
else
|
|
21790
|
-
improvements.push(entry);
|
|
21791
|
-
} else {
|
|
21792
|
-
neutral.push(entry);
|
|
21793
|
-
}
|
|
21794
|
-
}
|
|
21795
|
-
return { componentName: after.componentName, before, after, regressions, improvements, neutral };
|
|
21796
|
-
}
|
|
21797
|
-
function formatSingleProfile(profile) {
|
|
21798
|
-
const m = profile.metrics;
|
|
21799
|
-
const lines = [];
|
|
21800
|
-
lines.push(`${m.componentName} — reactive profile (static)`);
|
|
21801
|
-
if (m.sourceFile)
|
|
21802
|
-
lines.push(` source: ${m.sourceFile}`);
|
|
21803
|
-
lines.push(` hydrated: ${m.hydrated ? "yes" : "no"}`);
|
|
21804
|
-
lines.push("");
|
|
21805
|
-
lines.push(" Counts:");
|
|
21806
|
-
lines.push(` signals: ${m.signals}`);
|
|
21807
|
-
lines.push(` memos: ${m.memos}`);
|
|
21808
|
-
if (m.effects > 0)
|
|
21809
|
-
lines.push(` effects: ${m.effects}`);
|
|
21810
|
-
lines.push(` dynamic bindings: ${m.dynamicBindings}`);
|
|
21811
|
-
if (m.fallbacks > 0)
|
|
21812
|
-
lines.push(` fallbacks: ${m.fallbacks}`);
|
|
21813
|
-
if (m.loops > 0)
|
|
21814
|
-
lines.push(` loops: ${m.loops}`);
|
|
21815
|
-
if (m.conditionals > 0)
|
|
21816
|
-
lines.push(` conditionals: ${m.conditionals}`);
|
|
21817
|
-
if (m.eventHandlers > 0)
|
|
21818
|
-
lines.push(` event handlers: ${m.eventHandlers}`);
|
|
21819
|
-
lines.push("");
|
|
21820
|
-
lines.push(" Reactive budget (SR5):");
|
|
21821
|
-
const fanOutSuffix = m.hotSignal ? ` (${m.hotSignal})` : "";
|
|
21822
|
-
lines.push(` max signal fan-out: ${m.maxSignalFanOut}${fanOutSuffix}`);
|
|
21823
|
-
lines.push(` max memo chain depth: ${m.maxMemoChainDepth}`);
|
|
21824
|
-
lines.push(` total subscriptions: ${m.totalSubscriptions}`);
|
|
21825
|
-
if (m.batchCandidateCount > 0) {
|
|
21826
|
-
lines.push(` batch candidates: ${m.batchCandidateCount} handler(s) set ≥2 signals`);
|
|
21827
|
-
}
|
|
21828
|
-
if (profile.findings.length > 0) {
|
|
21829
|
-
lines.push("");
|
|
21830
|
-
lines.push(" Findings:");
|
|
21831
|
-
for (const f of profile.findings) {
|
|
21832
|
-
const icon = f.severity === "warning" ? "⚠" : "→";
|
|
21833
|
-
lines.push(` ${icon} [${f.kind}] ${f.message}`);
|
|
21834
|
-
lines.push(` fix: ${f.suggestion}`);
|
|
21835
|
-
if (f.loc) {
|
|
21836
|
-
const file = f.loc.file.split("/").pop() ?? f.loc.file;
|
|
21837
|
-
lines.push(` at ${file}:${f.loc.line}`);
|
|
21838
|
-
}
|
|
21839
|
-
}
|
|
21840
|
-
} else {
|
|
21841
|
-
lines.push("");
|
|
21842
|
-
lines.push(" No findings — component is within all thresholds.");
|
|
21843
|
-
}
|
|
21844
|
-
return lines.join(`
|
|
21845
|
-
`);
|
|
21846
|
-
}
|
|
21847
|
-
function formatProfileTable(profiles) {
|
|
21848
|
-
if (profiles.length === 0)
|
|
21849
|
-
return "No components found.";
|
|
21850
|
-
const sorted = [...profiles].sort((a, b) => b.metrics.totalSubscriptions - a.metrics.totalSubscriptions);
|
|
21851
|
-
const lines = [];
|
|
21852
|
-
lines.push("Component sig memo bind fall fanOut chain subs batch findings");
|
|
21853
|
-
lines.push("─".repeat(90));
|
|
21854
|
-
for (const p of sorted) {
|
|
21855
|
-
const m = p.metrics;
|
|
21856
|
-
const name = m.componentName.padEnd(23).slice(0, 23);
|
|
21857
|
-
const findingStr = p.findings.length > 0 ? p.findings.map((f) => f.kind.replace(/-/g, "_")).join(",") : "—";
|
|
21858
|
-
const row = [
|
|
21859
|
-
name,
|
|
21860
|
-
String(m.signals).padStart(3),
|
|
21861
|
-
String(m.memos).padStart(5),
|
|
21862
|
-
String(m.dynamicBindings).padStart(5),
|
|
21863
|
-
String(m.fallbacks).padStart(5),
|
|
21864
|
-
String(m.maxSignalFanOut).padStart(7),
|
|
21865
|
-
String(m.maxMemoChainDepth).padStart(6),
|
|
21866
|
-
String(m.totalSubscriptions).padStart(5),
|
|
21867
|
-
String(m.batchCandidateCount).padStart(6),
|
|
21868
|
-
` ${findingStr}`
|
|
21869
|
-
].join(" ");
|
|
21870
|
-
lines.push(row);
|
|
21871
|
-
}
|
|
21872
|
-
const allFindings = sorted.flatMap((p) => p.findings.map((f) => ({ component: p.metrics.componentName, finding: f })));
|
|
21873
|
-
if (allFindings.length > 0) {
|
|
21874
|
-
lines.push("");
|
|
21875
|
-
lines.push("Findings:");
|
|
21876
|
-
for (const { component, finding } of allFindings) {
|
|
21877
|
-
const icon = finding.severity === "warning" ? "⚠" : "→";
|
|
21878
|
-
lines.push(` ${icon} ${component}: ${finding.message}`);
|
|
21879
|
-
lines.push(` fix: ${finding.suggestion}`);
|
|
21880
|
-
if (finding.loc) {
|
|
21881
|
-
const file = finding.loc.file.split("/").pop() ?? finding.loc.file;
|
|
21882
|
-
lines.push(` at ${file}:${finding.loc.line}`);
|
|
21883
|
-
}
|
|
21884
|
-
}
|
|
21885
|
-
} else {
|
|
21886
|
-
lines.push("");
|
|
21887
|
-
lines.push("No findings across all components.");
|
|
21888
|
-
}
|
|
21889
|
-
return lines.join(`
|
|
21890
|
-
`);
|
|
21891
|
-
}
|
|
21892
|
-
function formatProfileDiff(diff) {
|
|
21893
|
-
const lines = [];
|
|
21894
|
-
lines.push(`${diff.componentName} — reactive profile diff (before → after)`);
|
|
21895
|
-
lines.push("");
|
|
21896
|
-
if (diff.regressions.length === 0 && diff.improvements.length === 0 && diff.neutral.length === 0) {
|
|
21897
|
-
lines.push(" No changes in reactive metrics.");
|
|
21898
|
-
return lines.join(`
|
|
21899
|
-
`);
|
|
21900
|
-
}
|
|
21901
|
-
if (diff.regressions.length > 0) {
|
|
21902
|
-
lines.push(" Regressions (reactive cost increased):");
|
|
21903
|
-
for (const e of diff.regressions) {
|
|
21904
|
-
lines.push(` ${e.metric}: ${e.before} → ${e.after} (+${e.delta})`);
|
|
21905
|
-
}
|
|
22191
|
+
return 0;
|
|
22192
|
+
const memoSet = new Set(graph.memos.map((m) => m.name));
|
|
22193
|
+
const memoDeps = new Map;
|
|
22194
|
+
for (const memo of graph.memos) {
|
|
22195
|
+
memoDeps.set(memo.name, memo.deps.filter((d) => memoSet.has(d)));
|
|
21906
22196
|
}
|
|
21907
|
-
|
|
21908
|
-
|
|
21909
|
-
|
|
21910
|
-
|
|
22197
|
+
const cache = new Map;
|
|
22198
|
+
function depth(name, visited) {
|
|
22199
|
+
if (cache.has(name))
|
|
22200
|
+
return cache.get(name);
|
|
22201
|
+
if (visited.has(name))
|
|
22202
|
+
return 0;
|
|
22203
|
+
const children = memoDeps.get(name) ?? [];
|
|
22204
|
+
if (children.length === 0) {
|
|
22205
|
+
cache.set(name, 1);
|
|
22206
|
+
return 1;
|
|
21911
22207
|
}
|
|
22208
|
+
visited.add(name);
|
|
22209
|
+
const d = 1 + Math.max(...children.map((c) => depth(c, new Set(visited))));
|
|
22210
|
+
cache.set(name, d);
|
|
22211
|
+
return d;
|
|
21912
22212
|
}
|
|
21913
|
-
|
|
21914
|
-
|
|
21915
|
-
|
|
21916
|
-
|
|
21917
|
-
|
|
21918
|
-
}
|
|
22213
|
+
let max = 0;
|
|
22214
|
+
for (const memo of graph.memos) {
|
|
22215
|
+
const d = depth(memo.name, new Set);
|
|
22216
|
+
if (d > max)
|
|
22217
|
+
max = d;
|
|
21919
22218
|
}
|
|
21920
|
-
return
|
|
21921
|
-
`);
|
|
21922
|
-
}
|
|
21923
|
-
function profileToJSON(profile) {
|
|
21924
|
-
return {
|
|
21925
|
-
metrics: profile.metrics,
|
|
21926
|
-
findings: profile.findings
|
|
21927
|
-
};
|
|
22219
|
+
return max;
|
|
21928
22220
|
}
|
|
21929
|
-
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
22221
|
+
function computeFindings(metrics, graph, eventSummary) {
|
|
22222
|
+
const findings = [];
|
|
22223
|
+
for (const signal of graph.signals) {
|
|
22224
|
+
if (signal.consumers.length > THRESHOLDS.highFanOut) {
|
|
22225
|
+
findings.push({
|
|
22226
|
+
kind: "high-fan-out",
|
|
22227
|
+
severity: "warning",
|
|
22228
|
+
signal: signal.name,
|
|
22229
|
+
message: `${signal.name} has ${signal.consumers.length} consumers (fan-out > ${THRESHOLDS.highFanOut})`,
|
|
22230
|
+
suggestion: `Split ${signal.name} into finer-grained signals, or add a createMemo to shield downstream consumers from unrelated updates`,
|
|
22231
|
+
loc: { file: signal.loc.file, line: signal.loc.line }
|
|
22232
|
+
});
|
|
22233
|
+
}
|
|
21938
22234
|
}
|
|
21939
|
-
if (
|
|
21940
|
-
|
|
21941
|
-
|
|
21942
|
-
|
|
21943
|
-
|
|
21944
|
-
|
|
21945
|
-
|
|
21946
|
-
if (ctxName === null || !contextDefaults.has(ctxName))
|
|
21947
|
-
continue;
|
|
21948
|
-
consumers.push({
|
|
21949
|
-
localName: c.name,
|
|
21950
|
-
contextName: ctxName,
|
|
21951
|
-
defaultValue: contextDefaults.get(ctxName) ?? null
|
|
22235
|
+
if (metrics.maxMemoChainDepth > THRESHOLDS.deepMemoChain) {
|
|
22236
|
+
findings.push({
|
|
22237
|
+
kind: "deep-memo-chain",
|
|
22238
|
+
severity: "warning",
|
|
22239
|
+
depth: metrics.maxMemoChainDepth,
|
|
22240
|
+
message: `Memo chain depth ${metrics.maxMemoChainDepth} (threshold: ${THRESHOLDS.deepMemoChain}) — a single signal update cascades through ${metrics.maxMemoChainDepth} memo levels`,
|
|
22241
|
+
suggestion: "Flatten intermediate memos that do not cache expensive computations; deep chains increase propagation latency"
|
|
21952
22242
|
});
|
|
21953
22243
|
}
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
if (expr.arguments.length !== 1)
|
|
21963
|
-
return null;
|
|
21964
|
-
const arg = expr.arguments[0];
|
|
21965
|
-
return ts21.isIdentifier(arg) ? arg.text : null;
|
|
21966
|
-
}
|
|
21967
|
-
function parseCreateContextDefault(source) {
|
|
21968
|
-
const expr = parseSingleExpression(source);
|
|
21969
|
-
if (!expr || !ts21.isCallExpression(expr))
|
|
21970
|
-
return null;
|
|
21971
|
-
if (expr.arguments.length === 0)
|
|
21972
|
-
return null;
|
|
21973
|
-
const arg = expr.arguments[0];
|
|
21974
|
-
if (ts21.isStringLiteral(arg) || ts21.isNoSubstitutionTemplateLiteral(arg))
|
|
21975
|
-
return arg.text;
|
|
21976
|
-
if (ts21.isNumericLiteral(arg))
|
|
21977
|
-
return Number(arg.text);
|
|
21978
|
-
if (arg.kind === ts21.SyntaxKind.TrueKeyword)
|
|
21979
|
-
return true;
|
|
21980
|
-
if (arg.kind === ts21.SyntaxKind.FalseKeyword)
|
|
21981
|
-
return false;
|
|
21982
|
-
return null;
|
|
21983
|
-
}
|
|
21984
|
-
function parseSingleExpression(source) {
|
|
21985
|
-
const sf = ts21.createSourceFile("__ctx.ts", `(${source})`, ts21.ScriptTarget.Latest, false);
|
|
21986
|
-
const stmt = sf.statements[0];
|
|
21987
|
-
if (!stmt || !ts21.isExpressionStatement(stmt))
|
|
21988
|
-
return null;
|
|
21989
|
-
let e = stmt.expression;
|
|
21990
|
-
while (ts21.isParenthesizedExpression(e))
|
|
21991
|
-
e = e.expression;
|
|
21992
|
-
return e;
|
|
21993
|
-
}
|
|
21994
|
-
function augmentInheritedPropAccesses(ir) {
|
|
21995
|
-
const propsObj = ir.metadata.propsObjectName;
|
|
21996
|
-
if (!propsObj)
|
|
21997
|
-
return;
|
|
21998
|
-
const existing = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
21999
|
-
const bareRefProps = new Set;
|
|
22000
|
-
const booleanAttrProps = new Set;
|
|
22001
|
-
const accessed = new Set;
|
|
22002
|
-
const accessRe = new RegExp(`(?:^|[^\\w$.])${propsObj}\\.([A-Za-z_$][\\w$]*)`, "g");
|
|
22003
|
-
const scan = (s) => {
|
|
22004
|
-
if (!s)
|
|
22005
|
-
return;
|
|
22006
|
-
for (const m of s.matchAll(accessRe))
|
|
22007
|
-
accessed.add(m[1]);
|
|
22008
|
-
};
|
|
22009
|
-
for (const memo of ir.metadata.memos)
|
|
22010
|
-
scan(memo.computation);
|
|
22011
|
-
for (const signal of ir.metadata.signals)
|
|
22012
|
-
scan(signal.initialValue);
|
|
22013
|
-
for (const stmt of ir.metadata.initStatements ?? [])
|
|
22014
|
-
scan(stmt.body);
|
|
22015
|
-
for (const eff of ir.metadata.effects ?? [])
|
|
22016
|
-
scan(eff.body);
|
|
22017
|
-
for (const c of ir.metadata.localConstants ?? []) {
|
|
22018
|
-
if (c.isModule)
|
|
22019
|
-
continue;
|
|
22020
|
-
scan(c.value);
|
|
22021
|
-
}
|
|
22022
|
-
const walk = (node) => {
|
|
22023
|
-
if (!node)
|
|
22024
|
-
return;
|
|
22025
|
-
const el = node;
|
|
22026
|
-
for (const attr of el.attrs ?? []) {
|
|
22027
|
-
const v = attr.value;
|
|
22028
|
-
if (v?.parts) {
|
|
22029
|
-
for (const part of v.parts) {
|
|
22030
|
-
if (part.type === "string")
|
|
22031
|
-
scan(part.value);
|
|
22032
|
-
else if (part.type === "ternary") {
|
|
22033
|
-
scan(part.condition);
|
|
22034
|
-
scan(part.whenTrue);
|
|
22035
|
-
scan(part.whenFalse);
|
|
22036
|
-
} else if (part.type === "lookup")
|
|
22037
|
-
scan(part.key);
|
|
22038
|
-
}
|
|
22039
|
-
}
|
|
22040
|
-
if (v?.kind === "expression" && typeof v.expr === "string") {
|
|
22041
|
-
scan(v.expr);
|
|
22042
|
-
const expr = v.expr.trim();
|
|
22043
|
-
const prefix = `${propsObj}.`;
|
|
22044
|
-
if (isBooleanAttr(attr.name) || v.presenceOrUndefined) {
|
|
22045
|
-
const m = expr.match(new RegExp(`^${propsObj}\\.([A-Za-z_$][\\w$]*)`));
|
|
22046
|
-
if (m)
|
|
22047
|
-
booleanAttrProps.add(m[1]);
|
|
22048
|
-
} else if (expr.startsWith(prefix)) {
|
|
22049
|
-
const rest = expr.slice(prefix.length);
|
|
22050
|
-
if (/^[A-Za-z_$][\w$]*$/.test(rest))
|
|
22051
|
-
bareRefProps.add(rest);
|
|
22052
|
-
}
|
|
22244
|
+
const batchSeen = new Set;
|
|
22245
|
+
for (const event of eventSummary.events) {
|
|
22246
|
+
const distinct = new Set;
|
|
22247
|
+
const setterNames = [];
|
|
22248
|
+
for (const sc of event.setterCalls) {
|
|
22249
|
+
if (sc.signal) {
|
|
22250
|
+
distinct.add(sc.signal);
|
|
22251
|
+
setterNames.push(sc.setter);
|
|
22053
22252
|
}
|
|
22054
22253
|
}
|
|
22055
|
-
|
|
22056
|
-
const
|
|
22057
|
-
|
|
22254
|
+
if (distinct.size >= THRESHOLDS.batchMinSignals) {
|
|
22255
|
+
const dedupeKey = `${event.eventName}|${event.loc.file ?? ""}|${event.loc.start.line}|${[...distinct].sort().join(",")}`;
|
|
22256
|
+
if (batchSeen.has(dedupeKey))
|
|
22257
|
+
continue;
|
|
22258
|
+
batchSeen.add(dedupeKey);
|
|
22259
|
+
findings.push({
|
|
22260
|
+
kind: "batch-candidate",
|
|
22261
|
+
severity: "info",
|
|
22262
|
+
signals: [...distinct],
|
|
22263
|
+
message: `${event.eventName} on <${event.elementContext}> sets ${distinct.size} signals (${[...distinct].join(", ")}) — triggers ${distinct.size} separate update cycles (static; verify setters are not in separate if/else branches)`,
|
|
22264
|
+
suggestion: `If all listed setters fire unconditionally in the same handler path, wrap in batch(() => { ${setterNames.join("; ")}; }) to collapse ${distinct.size} cycles into 1`,
|
|
22265
|
+
loc: event.loc.file ? { file: event.loc.file, line: event.loc.start.line } : undefined
|
|
22266
|
+
});
|
|
22058
22267
|
}
|
|
22059
|
-
const branchy = node;
|
|
22060
|
-
walk(branchy.whenTrue);
|
|
22061
|
-
walk(branchy.whenFalse);
|
|
22062
|
-
walk(branchy.consequent);
|
|
22063
|
-
walk(branchy.alternate);
|
|
22064
|
-
};
|
|
22065
|
-
walk(ir.root);
|
|
22066
|
-
for (const name of accessed) {
|
|
22067
|
-
if (existing.has(name))
|
|
22068
|
-
continue;
|
|
22069
|
-
let raw;
|
|
22070
|
-
if (booleanAttrProps.has(name))
|
|
22071
|
-
raw = "boolean";
|
|
22072
|
-
else if (bareRefProps.has(name))
|
|
22073
|
-
raw = "unknown";
|
|
22074
|
-
else
|
|
22075
|
-
raw = "string";
|
|
22076
|
-
const type = raw === "boolean" ? { kind: "primitive", raw: "boolean", primitive: "boolean" } : raw === "string" ? { kind: "primitive", raw: "string", primitive: "string" } : { kind: "unknown", raw: "unknown" };
|
|
22077
|
-
ir.metadata.propsParams.push({ name, type, optional: true });
|
|
22078
|
-
existing.add(name);
|
|
22079
22268
|
}
|
|
22269
|
+
if (metrics.dynamicBindings > 0 && metrics.fallbacks >= THRESHOLDS.fallbackHeavyMin && metrics.fallbacks / metrics.dynamicBindings > THRESHOLDS.fallbackHeavyRatio) {
|
|
22270
|
+
findings.push({
|
|
22271
|
+
kind: "fallback-heavy",
|
|
22272
|
+
severity: "info",
|
|
22273
|
+
message: `${metrics.fallbacks}/${metrics.dynamicBindings} bindings (${Math.round(metrics.fallbacks / metrics.dynamicBindings * 100)}%) are fallback-wrapped — reactivity not statically provable`,
|
|
22274
|
+
suggestion: "Run `bf debug fallbacks` to see each expression and fix them so the compiler can prove reactivity without the fallback wrapper"
|
|
22275
|
+
});
|
|
22276
|
+
}
|
|
22277
|
+
return findings;
|
|
22080
22278
|
}
|
|
22081
|
-
function
|
|
22082
|
-
const
|
|
22083
|
-
|
|
22084
|
-
|
|
22085
|
-
|
|
22086
|
-
|
|
22087
|
-
|
|
22088
|
-
|
|
22089
|
-
|
|
22090
|
-
|
|
22091
|
-
|
|
22092
|
-
|
|
22279
|
+
function diffProfiles(before, after) {
|
|
22280
|
+
const worseWhenHigher = new Set([
|
|
22281
|
+
"fallbacks",
|
|
22282
|
+
"maxSignalFanOut",
|
|
22283
|
+
"maxMemoChainDepth",
|
|
22284
|
+
"totalSubscriptions",
|
|
22285
|
+
"batchCandidateCount"
|
|
22286
|
+
]);
|
|
22287
|
+
const numericKeys = [
|
|
22288
|
+
"signals",
|
|
22289
|
+
"memos",
|
|
22290
|
+
"effects",
|
|
22291
|
+
"loops",
|
|
22292
|
+
"eventHandlers",
|
|
22293
|
+
"dynamicBindings",
|
|
22294
|
+
"fallbacks",
|
|
22295
|
+
"conditionals",
|
|
22296
|
+
"maxSignalFanOut",
|
|
22297
|
+
"maxMemoChainDepth",
|
|
22298
|
+
"totalSubscriptions",
|
|
22299
|
+
"batchCandidateCount"
|
|
22300
|
+
];
|
|
22301
|
+
const regressions = [];
|
|
22302
|
+
const improvements = [];
|
|
22303
|
+
const neutral = [];
|
|
22304
|
+
for (const key of numericKeys) {
|
|
22305
|
+
const b = before[key];
|
|
22306
|
+
const a = after[key];
|
|
22307
|
+
if (a === b)
|
|
22308
|
+
continue;
|
|
22309
|
+
const entry = { metric: key, before: b, after: a, delta: a - b };
|
|
22310
|
+
if (worseWhenHigher.has(key)) {
|
|
22311
|
+
if (a > b)
|
|
22312
|
+
regressions.push(entry);
|
|
22313
|
+
else
|
|
22314
|
+
improvements.push(entry);
|
|
22315
|
+
} else {
|
|
22316
|
+
neutral.push(entry);
|
|
22317
|
+
}
|
|
22093
22318
|
}
|
|
22094
|
-
return
|
|
22319
|
+
return { componentName: after.componentName, before, after, regressions, improvements, neutral };
|
|
22095
22320
|
}
|
|
22096
|
-
function
|
|
22097
|
-
const
|
|
22098
|
-
const
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22111
|
-
|
|
22112
|
-
|
|
22113
|
-
|
|
22321
|
+
function formatSingleProfile(profile) {
|
|
22322
|
+
const m = profile.metrics;
|
|
22323
|
+
const lines = [];
|
|
22324
|
+
lines.push(`${m.componentName} — reactive profile (static)`);
|
|
22325
|
+
if (m.sourceFile)
|
|
22326
|
+
lines.push(` source: ${m.sourceFile}`);
|
|
22327
|
+
lines.push(` hydrated: ${m.hydrated ? "yes" : "no"}`);
|
|
22328
|
+
lines.push("");
|
|
22329
|
+
lines.push(" Counts:");
|
|
22330
|
+
lines.push(` signals: ${m.signals}`);
|
|
22331
|
+
lines.push(` memos: ${m.memos}`);
|
|
22332
|
+
if (m.effects > 0)
|
|
22333
|
+
lines.push(` effects: ${m.effects}`);
|
|
22334
|
+
lines.push(` dynamic bindings: ${m.dynamicBindings}`);
|
|
22335
|
+
if (m.fallbacks > 0)
|
|
22336
|
+
lines.push(` fallbacks: ${m.fallbacks}`);
|
|
22337
|
+
if (m.loops > 0)
|
|
22338
|
+
lines.push(` loops: ${m.loops}`);
|
|
22339
|
+
if (m.conditionals > 0)
|
|
22340
|
+
lines.push(` conditionals: ${m.conditionals}`);
|
|
22341
|
+
if (m.eventHandlers > 0)
|
|
22342
|
+
lines.push(` event handlers: ${m.eventHandlers}`);
|
|
22343
|
+
lines.push("");
|
|
22344
|
+
lines.push(" Reactive budget (SR5):");
|
|
22345
|
+
const fanOutSuffix = m.hotSignal ? ` (${m.hotSignal})` : "";
|
|
22346
|
+
lines.push(` max signal fan-out: ${m.maxSignalFanOut}${fanOutSuffix}`);
|
|
22347
|
+
lines.push(` max memo chain depth: ${m.maxMemoChainDepth}`);
|
|
22348
|
+
lines.push(` total subscriptions: ${m.totalSubscriptions}`);
|
|
22349
|
+
if (m.batchCandidateCount > 0) {
|
|
22350
|
+
lines.push(` batch candidates: ${m.batchCandidateCount} handler(s) set ≥2 signals`);
|
|
22114
22351
|
}
|
|
22115
|
-
|
|
22116
|
-
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
22125
|
-
continue;
|
|
22126
|
-
const literal = parseStaticStringConst(c.value) ?? evalTemplateOfStringConsts(c.value, map);
|
|
22127
|
-
if (literal !== null) {
|
|
22128
|
-
map.set(c.name, literal);
|
|
22129
|
-
progressed = true;
|
|
22352
|
+
if (profile.findings.length > 0) {
|
|
22353
|
+
lines.push("");
|
|
22354
|
+
lines.push(" Findings:");
|
|
22355
|
+
for (const f of profile.findings) {
|
|
22356
|
+
const icon = f.severity === "warning" ? "⚠" : "→";
|
|
22357
|
+
lines.push(` ${icon} [${f.kind}] ${f.message}`);
|
|
22358
|
+
lines.push(` fix: ${f.suggestion}`);
|
|
22359
|
+
if (f.loc) {
|
|
22360
|
+
const file = f.loc.file.split("/").pop() ?? f.loc.file;
|
|
22361
|
+
lines.push(` at ${file}:${f.loc.line}`);
|
|
22130
22362
|
}
|
|
22131
22363
|
}
|
|
22364
|
+
} else {
|
|
22365
|
+
lines.push("");
|
|
22366
|
+
lines.push(" No findings — component is within all thresholds.");
|
|
22132
22367
|
}
|
|
22133
|
-
return
|
|
22368
|
+
return lines.join(`
|
|
22369
|
+
`);
|
|
22134
22370
|
}
|
|
22135
|
-
function
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
|
|
22139
|
-
const
|
|
22140
|
-
|
|
22141
|
-
|
|
22142
|
-
const
|
|
22143
|
-
|
|
22144
|
-
|
|
22145
|
-
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
22157
|
-
|
|
22158
|
-
|
|
22159
|
-
v = v.expression;
|
|
22160
|
-
if (ts21.isNumericLiteral(v))
|
|
22161
|
-
return { kind: "number", text: v.text };
|
|
22162
|
-
if (ts21.isStringLiteral(v) || ts21.isNoSubstitutionTemplateLiteral(v)) {
|
|
22163
|
-
return { kind: "string", text: v.text };
|
|
22164
|
-
}
|
|
22165
|
-
return null;
|
|
22371
|
+
function formatProfileTable(profiles) {
|
|
22372
|
+
if (profiles.length === 0)
|
|
22373
|
+
return "No components found.";
|
|
22374
|
+
const sorted = [...profiles].sort((a, b) => b.metrics.totalSubscriptions - a.metrics.totalSubscriptions);
|
|
22375
|
+
const lines = [];
|
|
22376
|
+
lines.push("Component sig memo bind fall fanOut chain subs batch findings");
|
|
22377
|
+
lines.push("─".repeat(90));
|
|
22378
|
+
for (const p of sorted) {
|
|
22379
|
+
const m = p.metrics;
|
|
22380
|
+
const name = m.componentName.padEnd(23).slice(0, 23);
|
|
22381
|
+
const findingStr = p.findings.length > 0 ? p.findings.map((f) => f.kind.replace(/-/g, "_")).join(",") : "—";
|
|
22382
|
+
const row = [
|
|
22383
|
+
name,
|
|
22384
|
+
String(m.signals).padStart(3),
|
|
22385
|
+
String(m.memos).padStart(5),
|
|
22386
|
+
String(m.dynamicBindings).padStart(5),
|
|
22387
|
+
String(m.fallbacks).padStart(5),
|
|
22388
|
+
String(m.maxSignalFanOut).padStart(7),
|
|
22389
|
+
String(m.maxMemoChainDepth).padStart(6),
|
|
22390
|
+
String(m.totalSubscriptions).padStart(5),
|
|
22391
|
+
String(m.batchCandidateCount).padStart(6),
|
|
22392
|
+
` ${findingStr}`
|
|
22393
|
+
].join(" ");
|
|
22394
|
+
lines.push(row);
|
|
22166
22395
|
}
|
|
22167
|
-
|
|
22168
|
-
|
|
22169
|
-
|
|
22170
|
-
|
|
22171
|
-
|
|
22172
|
-
|
|
22173
|
-
|
|
22174
|
-
|
|
22175
|
-
|
|
22176
|
-
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
const callee = node.expression;
|
|
22180
|
-
if (!ts21.isPropertyAccessExpression(callee))
|
|
22181
|
-
return null;
|
|
22182
|
-
if (callee.name.text !== "join")
|
|
22183
|
-
return null;
|
|
22184
|
-
let recv = callee.expression;
|
|
22185
|
-
while (ts21.isParenthesizedExpression(recv))
|
|
22186
|
-
recv = recv.expression;
|
|
22187
|
-
if (!ts21.isArrayLiteralExpression(recv))
|
|
22188
|
-
return null;
|
|
22189
|
-
const parts = [];
|
|
22190
|
-
for (const el of recv.elements) {
|
|
22191
|
-
if (ts21.isStringLiteral(el) || ts21.isNoSubstitutionTemplateLiteral(el)) {
|
|
22192
|
-
parts.push(el.text);
|
|
22193
|
-
} else {
|
|
22194
|
-
return null;
|
|
22396
|
+
const allFindings = sorted.flatMap((p) => p.findings.map((f) => ({ component: p.metrics.componentName, finding: f })));
|
|
22397
|
+
if (allFindings.length > 0) {
|
|
22398
|
+
lines.push("");
|
|
22399
|
+
lines.push("Findings:");
|
|
22400
|
+
for (const { component, finding } of allFindings) {
|
|
22401
|
+
const icon = finding.severity === "warning" ? "⚠" : "→";
|
|
22402
|
+
lines.push(` ${icon} ${component}: ${finding.message}`);
|
|
22403
|
+
lines.push(` fix: ${finding.suggestion}`);
|
|
22404
|
+
if (finding.loc) {
|
|
22405
|
+
const file = finding.loc.file.split("/").pop() ?? finding.loc.file;
|
|
22406
|
+
lines.push(` at ${file}:${finding.loc.line}`);
|
|
22407
|
+
}
|
|
22195
22408
|
}
|
|
22409
|
+
} else {
|
|
22410
|
+
lines.push("");
|
|
22411
|
+
lines.push("No findings across all components.");
|
|
22196
22412
|
}
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
const arg = node.arguments[0];
|
|
22200
|
-
if (ts21.isStringLiteral(arg) || ts21.isNoSubstitutionTemplateLiteral(arg))
|
|
22201
|
-
sep2 = arg.text;
|
|
22202
|
-
else
|
|
22203
|
-
return null;
|
|
22204
|
-
}
|
|
22205
|
-
return parts.join(sep2);
|
|
22413
|
+
return lines.join(`
|
|
22414
|
+
`);
|
|
22206
22415
|
}
|
|
22207
|
-
function
|
|
22208
|
-
|
|
22209
|
-
|
|
22210
|
-
|
|
22211
|
-
|
|
22212
|
-
|
|
22213
|
-
return
|
|
22214
|
-
|
|
22215
|
-
let defaultKey;
|
|
22216
|
-
const resolved = resolveKey?.(arg.text);
|
|
22217
|
-
if (resolved) {
|
|
22218
|
-
indexPropName = resolved.propName;
|
|
22219
|
-
defaultKey = resolved.defaultLiteral;
|
|
22220
|
-
} else if (propsParams.some((p) => p.name === arg.text)) {
|
|
22221
|
-
indexPropName = arg.text;
|
|
22222
|
-
} else {
|
|
22223
|
-
return null;
|
|
22416
|
+
function formatProfileDiff(diff) {
|
|
22417
|
+
const lines = [];
|
|
22418
|
+
lines.push(`${diff.componentName} — reactive profile diff (before → after)`);
|
|
22419
|
+
lines.push("");
|
|
22420
|
+
if (diff.regressions.length === 0 && diff.improvements.length === 0 && diff.neutral.length === 0) {
|
|
22421
|
+
lines.push(" No changes in reactive metrics.");
|
|
22422
|
+
return lines.join(`
|
|
22423
|
+
`);
|
|
22224
22424
|
}
|
|
22225
|
-
|
|
22226
|
-
|
|
22227
|
-
|
|
22228
|
-
|
|
22229
|
-
if (sf.statements.length !== 1)
|
|
22230
|
-
return null;
|
|
22231
|
-
const stmt = sf.statements[0];
|
|
22232
|
-
if (!ts21.isExpressionStatement(stmt))
|
|
22233
|
-
return null;
|
|
22234
|
-
let parsed = stmt.expression;
|
|
22235
|
-
while (ts21.isParenthesizedExpression(parsed))
|
|
22236
|
-
parsed = parsed.expression;
|
|
22237
|
-
if (!ts21.isObjectLiteralExpression(parsed))
|
|
22238
|
-
return null;
|
|
22239
|
-
const entries = [];
|
|
22240
|
-
for (const prop of parsed.properties) {
|
|
22241
|
-
if (!ts21.isPropertyAssignment(prop))
|
|
22242
|
-
return null;
|
|
22243
|
-
let key;
|
|
22244
|
-
if (ts21.isIdentifier(prop.name)) {
|
|
22245
|
-
key = prop.name.text;
|
|
22246
|
-
} else if (ts21.isStringLiteral(prop.name) || ts21.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
22247
|
-
key = prop.name.text;
|
|
22248
|
-
} else {
|
|
22249
|
-
return null;
|
|
22425
|
+
if (diff.regressions.length > 0) {
|
|
22426
|
+
lines.push(" Regressions (reactive cost increased):");
|
|
22427
|
+
for (const e of diff.regressions) {
|
|
22428
|
+
lines.push(` ${e.metric}: ${e.before} → ${e.after} (+${e.delta})`);
|
|
22250
22429
|
}
|
|
22251
|
-
|
|
22252
|
-
|
|
22253
|
-
|
|
22254
|
-
|
|
22255
|
-
|
|
22256
|
-
} else if (ts21.isStringLiteral(v) || ts21.isNoSubstitutionTemplateLiteral(v)) {
|
|
22257
|
-
entries.push({ key, value: { kind: "string", text: v.text } });
|
|
22258
|
-
} else {
|
|
22259
|
-
return null;
|
|
22430
|
+
}
|
|
22431
|
+
if (diff.improvements.length > 0) {
|
|
22432
|
+
lines.push(" Improvements (reactive cost decreased):");
|
|
22433
|
+
for (const e of diff.improvements) {
|
|
22434
|
+
lines.push(` ${e.metric}: ${e.before} → ${e.after} (${e.delta})`);
|
|
22260
22435
|
}
|
|
22261
22436
|
}
|
|
22262
|
-
|
|
22437
|
+
if (diff.neutral.length > 0) {
|
|
22438
|
+
lines.push(" Structural changes (count changes, no clear direction):");
|
|
22439
|
+
for (const e of diff.neutral) {
|
|
22440
|
+
const sign = (e.delta ?? 0) > 0 ? "+" : "";
|
|
22441
|
+
lines.push(` ${e.metric}: ${e.before} → ${e.after} (${sign}${e.delta})`);
|
|
22442
|
+
}
|
|
22443
|
+
}
|
|
22444
|
+
return lines.join(`
|
|
22445
|
+
`);
|
|
22446
|
+
}
|
|
22447
|
+
function profileToJSON(profile) {
|
|
22448
|
+
return {
|
|
22449
|
+
metrics: profile.metrics,
|
|
22450
|
+
findings: profile.findings
|
|
22451
|
+
};
|
|
22263
22452
|
}
|
|
22264
22453
|
|
|
22265
22454
|
// src/index.ts
|
|
@@ -22292,6 +22481,7 @@ export {
|
|
|
22292
22481
|
parseBlockBodyTolerant,
|
|
22293
22482
|
parseBlockBody,
|
|
22294
22483
|
needsTypeBasedDetection,
|
|
22484
|
+
materializeGetterCalls,
|
|
22295
22485
|
matchSearchParamsMethodCall,
|
|
22296
22486
|
matchQueryHrefCall,
|
|
22297
22487
|
matchLoweringCall,
|
|
@@ -22315,6 +22505,7 @@ export {
|
|
|
22315
22505
|
generateClientJsWithSourceMap,
|
|
22316
22506
|
generateClientJs,
|
|
22317
22507
|
freeVarsInBody,
|
|
22508
|
+
freeIdentifiers,
|
|
22318
22509
|
formatWhyUpdate,
|
|
22319
22510
|
formatWastedReReruns,
|
|
22320
22511
|
formatUpdatePath,
|
|
@@ -22343,6 +22534,8 @@ export {
|
|
|
22343
22534
|
exprToString,
|
|
22344
22535
|
evaluateProfileGates,
|
|
22345
22536
|
evalStringArrayJoin,
|
|
22537
|
+
envSignalReaderFor,
|
|
22538
|
+
envSignalLocalNames,
|
|
22346
22539
|
enableCompilerInstrumentation,
|
|
22347
22540
|
emitParsedExpr,
|
|
22348
22541
|
emitIRNode,
|
|
@@ -22355,6 +22548,7 @@ export {
|
|
|
22355
22548
|
createProgramForCorpus,
|
|
22356
22549
|
createError,
|
|
22357
22550
|
containsHigherOrder,
|
|
22551
|
+
computeSsrSeedPlan,
|
|
22358
22552
|
compileJSX,
|
|
22359
22553
|
combineParentChildClientJs,
|
|
22360
22554
|
collectModuleStringConsts,
|
|
@@ -22390,6 +22584,7 @@ export {
|
|
|
22390
22584
|
PROFILE_SCHEMA_VERSION,
|
|
22391
22585
|
JsxAdapter,
|
|
22392
22586
|
ErrorCodes,
|
|
22587
|
+
ENV_SIGNAL_READERS,
|
|
22393
22588
|
CALLBACK_METHODS,
|
|
22394
22589
|
BaseAdapter,
|
|
22395
22590
|
BUILTIN_LOWERING_PLUGINS,
|