octane 0.2.6 → 0.2.8

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.
@@ -81,13 +81,18 @@ import { assertNoLiveClientOnlyImports } from './client-only-server.js';
81
81
  import { nsForChildren, nsForSelf } from './jsx-namespace.js';
82
82
  import { analyzeNativeChangeDiagnostics } from './native-change-diagnostics.js';
83
83
  import { assertStrongMode } from './strong-mode.js';
84
- import { assertNativeReadDiagnostics, assertNativeReadOptions } from './native-read-diagnostics.js';
84
+ import {
85
+ assertNativeReadDiagnostics,
86
+ assertNativeReadOptions,
87
+ nativeReadOptions,
88
+ } from './native-read-diagnostics.js';
85
89
  import {
86
90
  captureNativeReadWitness,
87
91
  wrapNativeReadScope,
88
92
  wrapNativeWarmScope,
89
93
  } from './native-read-codegen.js';
90
94
  import { createTextTypeFactsLookup } from './text-type-facts.js';
95
+ import { collectProvenContextBindings, isProvenContextUse } from './context-use.js';
91
96
  import { applyCssModuleConstants } from './css-module-constants.js';
92
97
  import { assertUniversalRuntimeTarget, normalizeUniversalRuntime } from './universal-runtime.js';
93
98
 
@@ -444,8 +449,8 @@ function attrBindingHelper(bind) {
444
449
  // repeating the guard and previous-value publication in every component.
445
450
  // Certified repeated host rows select the ordinary writer and emit an inline
446
451
  // guard instead, avoiding a call and cache publication on unchanged rows.
447
- // Fresh classes deliberately remain unguarded; controlled form properties
448
- // likewise must reassert the live DOM value on every render.
452
+ // Fresh classes compare their composed string in emitBindingUpdate; controlled
453
+ // form properties must reassert the live DOM value on every render.
449
454
  function attrBindingUpdateHelper(bind, inlineBindingGuards = false) {
450
455
  const helper = attrBindingHelper(bind);
451
456
  if (inlineBindingGuards) return helper;
@@ -456,7 +461,11 @@ function attrBindingUpdateHelper(bind, inlineBindingGuards = false) {
456
461
  case 'ariaAttr':
457
462
  return `${helper}IfChanged`;
458
463
  case 'class':
459
- return bind.fresh ? helper : `${helper}IfChanged`;
464
+ return bind.fresh
465
+ ? bind.ns && bind.ns !== 'html'
466
+ ? 'updateFreshClassAttr'
467
+ : 'updateFreshClassName'
468
+ : `${helper}IfChanged`;
460
469
  default:
461
470
  return helper;
462
471
  }
@@ -1172,6 +1181,8 @@ const INTERNAL_CLIENT_RUNTIME_HELPERS = new Set([
1172
1181
  'setAriaAttributeIfChanged',
1173
1182
  'setClassNameIfChanged',
1174
1183
  'setClassAttrIfChanged',
1184
+ 'updateFreshClassName',
1185
+ 'updateFreshClassAttr',
1175
1186
  'textHoleUpdate',
1176
1187
  'childTextHoleUpdate',
1177
1188
  ...HOOK_MEMO_RUNTIME_HELPERS,
@@ -5100,7 +5111,9 @@ function collectImmutableModuleFunctions(body) {
5100
5111
  const declared = new Map();
5101
5112
  for (const statement of body) {
5102
5113
  const declaration =
5103
- statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
5114
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
5115
+ ? statement.declaration
5116
+ : statement;
5104
5117
  if (declaration?.type === 'FunctionDeclaration' && declaration.id?.type === 'Identifier') {
5105
5118
  declared.set(declaration.id.name, declaration);
5106
5119
  }
@@ -5115,10 +5128,31 @@ function collectImmutableModuleFunctions(body) {
5115
5128
  }
5116
5129
  if (seen.has(n)) return;
5117
5130
  seen.add(n);
5118
- if (n.type === 'AssignmentExpression' && n.left?.type === 'Identifier') {
5119
- declared.delete(n.left.name);
5120
- } else if (n.type === 'UpdateExpression' && n.argument?.type === 'Identifier') {
5121
- declared.delete(n.argument.name);
5131
+ const writeTarget =
5132
+ n.type === 'AssignmentExpression'
5133
+ ? n.left
5134
+ : n.type === 'UpdateExpression'
5135
+ ? n.argument
5136
+ : n.type === 'VariableDeclarator'
5137
+ ? n.id
5138
+ : (n.type === 'ForOfStatement' || n.type === 'ForInStatement') &&
5139
+ n.left?.type !== 'VariableDeclaration'
5140
+ ? n.left
5141
+ : null;
5142
+ if (writeTarget?.type === 'Identifier') {
5143
+ declared.delete(writeTarget.name);
5144
+ } else if (writeTarget !== null) {
5145
+ const names = new Set();
5146
+ collectPatternNames(unwrapTsExpr(writeTarget), names);
5147
+ for (const name of names) declared.delete(name);
5148
+ } else if (
5149
+ n.type === 'CallExpression' &&
5150
+ unwrapTsExpr(n.callee)?.type === 'Identifier' &&
5151
+ unwrapTsExpr(n.callee).name === 'eval'
5152
+ ) {
5153
+ // Direct eval can write a module function binding without a visible
5154
+ // assignment node. Exclude those names from identity proofs.
5155
+ declared.clear();
5122
5156
  }
5123
5157
  for (const key in n) {
5124
5158
  if (AST_WALK_SKIP_KEYS.has(key)) continue;
@@ -8586,7 +8620,7 @@ function instrumentProfileComponents(ast, ctx) {
8586
8620
  * Compile a .tsrx source string into JS targeting `octane`.
8587
8621
  * @param {string} source
8588
8622
  * @param {string} filename
8589
- * @param {{ hmr?: boolean | 'vite' | 'webpack', mode?: 'client' | 'server', dev?: boolean, strong?: boolean, profile?: boolean, profileFilename?: string, autoMemo?: boolean, inlineHookMemo?: boolean, dataCallbackHooks?: readonly string[], valdiWriterFacts?: import('./compile-valdi.js').ValdiWriterFacts, textTypeFacts?: { version: 1, filename: string, sourceVersion: string, projectVersion: string, stringChildRanges: readonly (readonly [number, number])[] }, renderer?: { id: string, module: string, target: 'dom' | 'universal' | 'valdi', server?: string }, rendererBoundaries?: Readonly<Record<string, Readonly<Record<string, { ownerRenderer: string, childRenderer: string, prop: string, server?: string }>>>>, rendererRegistry?: Readonly<Record<string, { module: string, target: 'dom' | 'universal' | 'valdi', server?: string }>>, clientOnlyImports?: readonly unknown[], __hydratePrepared?: boolean, __hydrateBoundaryModule?: boolean, __nativeChangeDiagnostics?: readonly unknown[], __nativeChangeAnalysis?: { diagnostics: readonly unknown[], classifications: Map<number, string> } }} [options] —
8623
+ * @param {{ hmr?: boolean | 'vite' | 'webpack', mode?: 'client' | 'server', dev?: boolean, strong?: boolean, profile?: boolean, profileFilename?: string, autoMemo?: boolean, inlineHookMemo?: boolean, dataCallbackHooks?: readonly string[], valdiWriterFacts?: import('./compile-valdi.js').ValdiWriterFacts, textTypeFacts?: { version: 1, filename: string, sourceVersion: string, projectVersion: string, stringChildRanges: readonly (readonly [number, number])[], primitiveTextChildRanges?: readonly (readonly [number, number])[] }, renderer?: { id: string, module: string, target: 'dom' | 'universal' | 'valdi', server?: string }, rendererBoundaries?: Readonly<Record<string, Readonly<Record<string, { ownerRenderer: string, childRenderer: string, prop: string, server?: string }>>>>, rendererRegistry?: Readonly<Record<string, { module: string, target: 'dom' | 'universal' | 'valdi', server?: string }>>, clientOnlyImports?: readonly unknown[], __hydratePrepared?: boolean, __hydrateBoundaryModule?: boolean, __nativeChangeDiagnostics?: readonly unknown[], __nativeChangeAnalysis?: { diagnostics: readonly unknown[], classifications: Map<number, string> } }} [options] —
8590
8624
  * `dev: true` emits client hydration source-location metadata (per-component
8591
8625
  * `__s.locs`/`__s.locFile`) and, in server mode, source-located native-element
8592
8626
  * scopes for invalid HTML nesting diagnostics. Both are strictly gated so
@@ -8634,7 +8668,6 @@ export function compileForBundler(source, filename, options) {
8634
8668
  }
8635
8669
 
8636
8670
  function compileAuthored(source, filename, options, bundlerMetadata) {
8637
- assertNativeReadOptions(options);
8638
8671
  const mode = (options && options.mode) || 'client';
8639
8672
  if (mode !== 'client' && mode !== 'server') {
8640
8673
  throw new Error(`Unknown compile mode "${mode}" — expected 'client' or 'server'.`);
@@ -8645,6 +8678,7 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8645
8678
  );
8646
8679
  analyzeTsrx(analyzedAst, cleanFilename);
8647
8680
  adoptParserAst(analyzedAst);
8681
+ options = nativeReadOptions(analyzedAst, options);
8648
8682
  assertNativeReadDiagnostics(analyzedAst, source, cleanFilename, options);
8649
8683
  const strongModeEnabled =
8650
8684
  assertStrongMode(analyzedAst, source, cleanFilename, options)?.enabled === true;
@@ -9175,6 +9209,7 @@ function compileInternal(
9175
9209
  ctx.octaneImportLocals = imports.locals;
9176
9210
  ctx.octaneImportNamespaces = imports.namespaces;
9177
9211
  ctx.foreignImportLocals = imports.foreignLocals;
9212
+ ctx.provenContextBindings = collectProvenContextBindings(ast);
9178
9213
  ctx.activityModuleAst = ast;
9179
9214
  }
9180
9215
  if (ctx.dev) {
@@ -10446,6 +10481,7 @@ function compileServer(
10446
10481
  ctx.octaneImportLocals = imports.locals;
10447
10482
  ctx.octaneImportNamespaces = imports.namespaces;
10448
10483
  ctx.foreignImportLocals = imports.foreignLocals;
10484
+ ctx.provenContextBindings = collectProvenContextBindings(ast);
10449
10485
  ctx.activityModuleAst = ast;
10450
10486
  }
10451
10487
  // M3 inherit-range exclusion set — must match the client compile's
@@ -11050,7 +11086,7 @@ function ssrCompileBodyWithMapTemps(
11050
11086
  // produce mergeable text nodes:
11051
11087
  // 'static' — a static string-literal Text (bakes / serializes as literal text)
11052
11088
  // 'empty' — a static literal that renders NOTHING (adjacency-transparent)
11053
- // 'dyn' — a known-string dynamic text hole (client `<!>` + htextSwap,
11089
+ // 'dyn' — a proven primitive text hole (client `<!>` + htextSwap,
11054
11090
  // server markerless ssrText)
11055
11091
  // 'other' — everything else (elements, renderable `{expr}` holes, control
11056
11092
  // flow — all serialize elements or `<!--[-->…<!--]-->` ranges
@@ -11059,9 +11095,7 @@ function textAdjacencyKind(node, ctx) {
11059
11095
  if (node.type !== 'Text') return 'other';
11060
11096
  const lit = staticTextLiteral(node.expression);
11061
11097
  if (lit !== null) return lit === '' ? 'empty' : 'static';
11062
- return isKnownStringChildExpression(node.expression, ctx.knownStringChildLocals)
11063
- ? 'dyn'
11064
- : 'other';
11098
+ return isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals) ? 'dyn' : 'other';
11065
11099
  }
11066
11100
 
11067
11101
  // Does the child at `i` have a text-producing sibling next to it (looking
@@ -11361,13 +11395,14 @@ function ssrEmitNode(
11361
11395
  : null,
11362
11396
  );
11363
11397
  }
11364
- // `{x as string}` / literals / templates / `+`-concats → definite TEXT.
11398
+ // String assertions / templates / string concats / proven numeric results
11399
+ // → a text binding on both client and server.
11365
11400
  // Everything else (`{children}`, `{<Comp/>}`, possibly-renderable values)
11366
11401
  // → ssrChild, which RENDERS a component/element child (and coerces a
11367
11402
  // primitive to text) — mirrors Ripple's `{expr}` vs `{expr as string}`.
11368
11403
  // rewriteHookCalls: a `use(thenable)` in this hole bypasses the setup
11369
11404
  // rewrite, so key it here too (else it collides with sibling/nested use()).
11370
- if (isKnownStringChildExpression(expr, ctx.knownStringChildLocals)) {
11405
+ if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
11371
11406
  // ssrTextPre = ssrText + the runtime leading-'\n' protection (the value
11372
11407
  // isn't known at compile time here).
11373
11408
  const fn =
@@ -12253,7 +12288,7 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12253
12288
  // markerless `childTextHole` mount: a primitive is the host's bare text, an object
12254
12289
  // still gets a `<!--[-->…<!--]-->` block). Must match the client's only-child
12255
12290
  // markerless condition exactly so both sides agree for hydration: a single `Text`
12256
- // child that is neither a static literal (baked into HTML) nor a known string
12291
+ // child that is neither a static literal (baked into HTML) nor proven text
12257
12292
  // (emitted via `ssrText`).
12258
12293
  const onlyChild0 =
12259
12294
  normChildren.length === 1 && normChildren[0].type === 'Text' ? normChildren[0] : null;
@@ -12265,7 +12300,7 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12265
12300
  htmlSources.length === 0 &&
12266
12301
  onlyChild0 !== null &&
12267
12302
  staticTextLiteral(onlyChild0.expression) === null &&
12268
- !isKnownStringChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
12303
+ !isKnownTextChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
12269
12304
  ) {
12270
12305
  const childHelper =
12271
12306
  tag === 'pre' || tag === 'textarea' || tag === 'listing' ? 'ssrChildTextPre' : 'ssrChildText';
@@ -14027,6 +14062,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14027
14062
  ctx.inlineHookMemo && ctx.mode !== 'server' && ctx._universalRuntimeUnit == null;
14028
14063
  ctx._puInlineLowering = inlineLowering;
14029
14064
  let warmThunk = null;
14065
+ let staticChildOnly = false;
14030
14066
  if (options && options.autoCallback) {
14031
14067
  const creations = [];
14032
14068
  const warmChildren = [];
@@ -14085,9 +14121,21 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14085
14121
  jsxNodes = parallelUseWalkJsx(jsxNodes, ctx, name, creations, warmChildren, [], new Set());
14086
14122
  const warm = buildWarmArtifacts(node, ctx, name, creations, warmChildren);
14087
14123
  warmThunk = warm.thunk;
14124
+ staticChildOnly = warm.staticChildOnly === true;
14088
14125
  ctx._pendingWarm = warm.warmNode;
14089
14126
  }
14090
- workingStatements = rewriteParallelUse(workingStatements, ctx, name, warmThunk);
14127
+ workingStatements = rewriteParallelUse(
14128
+ workingStatements,
14129
+ ctx,
14130
+ name,
14131
+ warmThunk,
14132
+ staticChildOnly &&
14133
+ ctx.mode !== 'server' &&
14134
+ ctx._universalRuntimeUnit == null &&
14135
+ ctx.moduleFunctionDeclarations.get(name)?.start === node.start
14136
+ ? staticChildWarmPlanProps(node, name)
14137
+ : null,
14138
+ );
14091
14139
 
14092
14140
  // De-callback the hook memo tier (production client compile). Authored and
14093
14141
  // auto-generated useMemo/useCallback declarations become inline flat-cache
@@ -14813,6 +14861,7 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
14813
14861
  // Names bound by a binding pattern (params, declarator ids). Mirrors the
14814
14862
  // pattern handling of collectFreeIdentifiers' collectBindings.
14815
14863
  function collectPatternNames(pat, into) {
14864
+ pat = unwrapTsExpr(pat);
14816
14865
  if (!pat) return into;
14817
14866
  switch (pat.type) {
14818
14867
  case 'Identifier':
@@ -14929,10 +14978,8 @@ function isMountStableInlineHandler(node, ctx) {
14929
14978
  return true;
14930
14979
  }
14931
14980
 
14932
- // Object/array/function literals allocate a new identity on every evaluation,
14933
- // so an identity diff can never skip their update. This currently feeds the
14934
- // class binding path, where dropping the dead previous-value field preserves
14935
- // the exact setter frequency while shrinking both code and the binding bag.
14981
+ // Object/array/function literals allocate a new identity on every evaluation.
14982
+ // Class bindings compare their composed string instead of that fresh identity.
14936
14983
  function isFreshBindingExpr(node) {
14937
14984
  const value = unwrapTsExpr(node);
14938
14985
  return (
@@ -16114,8 +16161,61 @@ function isWarmSafeExpr(expr, paramNames, componentLocals, armLocals) {
16114
16161
  return true;
16115
16162
  }
16116
16163
 
16164
+ // The registration records props now and invokes the already-attached module
16165
+ // plan only if a descendant suspends. A reassigned parameter would differ from
16166
+ // the old thunk's live lexical capture, including writes in JSX expressions or
16167
+ // nested callbacks. A same-name write in a shadowed scope merely declines the
16168
+ // optimization. Dynamic bindings are conservatively excluded as well.
16169
+ function staticChildWarmPlanProps(node, componentName) {
16170
+ const params = node.params || [];
16171
+ if (params.length > 1 || (params.length === 1 && params[0].type !== 'Identifier')) {
16172
+ return null;
16173
+ }
16174
+ const name = params[0]?.name ?? null;
16175
+ if (name === componentName) return null;
16176
+ const seen = new WeakSet();
16177
+ const bindsName = (pattern, binding) => {
16178
+ if (binding === null || pattern === null) return false;
16179
+ if (pattern.type === 'Identifier') return pattern.name === binding;
16180
+ const names = new Set();
16181
+ collectPatternNames(unwrapTsExpr(pattern), names);
16182
+ return names.has(binding);
16183
+ };
16184
+ const unsafe = (value) => {
16185
+ if (value === null || typeof value !== 'object') return false;
16186
+ if (Array.isArray(value)) return value.some(unsafe);
16187
+ if (seen.has(value)) return false;
16188
+ seen.add(value);
16189
+ if (
16190
+ (value.type === 'AssignmentExpression' && bindsName(value.left, name)) ||
16191
+ (value.type === 'UpdateExpression' && bindsName(value.argument, name)) ||
16192
+ (value.type === 'VariableDeclarator' &&
16193
+ (bindsName(value.id, name) || bindsName(value.id, componentName))) ||
16194
+ ((value.type === 'FunctionDeclaration' || value.type === 'ClassDeclaration') &&
16195
+ value.id?.name === componentName) ||
16196
+ (value.type === 'CatchClause' && bindsName(value.param, componentName)) ||
16197
+ ((value.type === 'ForOfStatement' || value.type === 'ForInStatement') &&
16198
+ bindsName(value.left, name)) ||
16199
+ (value.type === 'CallExpression' &&
16200
+ unwrapTsExpr(value.callee)?.type === 'Identifier' &&
16201
+ unwrapTsExpr(value.callee).name === 'eval') ||
16202
+ (value.type === 'Identifier' && value.name === 'arguments') ||
16203
+ value.type === 'ThisExpression' ||
16204
+ value.type === 'Super' ||
16205
+ value.type === 'MetaProperty'
16206
+ ) {
16207
+ return true;
16208
+ }
16209
+ for (const key in value) {
16210
+ if (!AST_WALK_SKIP_KEYS.has(key) && unsafe(value[key])) return true;
16211
+ }
16212
+ return false;
16213
+ };
16214
+ return unsafe(node.body) ? null : name === null ? b.unary('void', b.literal(0, '0')) : b.id(name);
16215
+ }
16216
+
16117
16217
  // ── Pass B: run detection + hoist + batch ───────────────────────────────────
16118
- function rewriteParallelUse(statements, ctx, componentName, warmThunk) {
16218
+ function rewriteParallelUse(statements, ctx, componentName, warmThunk, staticPlanProps = null) {
16119
16219
  let firstBatch = true;
16120
16220
  const output = transformList(statements);
16121
16221
  if (warmThunk) {
@@ -16125,17 +16225,57 @@ function rewriteParallelUse(statements, ctx, componentName, warmThunk) {
16125
16225
  // setup preserves early-return reachability: an alternate returned tree must
16126
16226
  // not activate the final template's plan. The first direct batch receives the
16127
16227
  // same thunk below so it can still warm before setup reaches this registration.
16128
- const batchHelper = ctx.mode === 'server' ? 'puBatch' : 'useBatch';
16129
- const batchAlias = requireRuntimeForContext(ctx, batchHelper);
16130
- const registration = inheritOriginLoc(
16131
- b.stmt(b.call(batchAlias, b.array([]), warmThunk)),
16228
+ // Only the DOM client has registerWarmPlan. Its two-argument form pushes
16229
+ // the same block, thunk, and undefined props as an empty useBatch call,
16230
+ // without creating an array on every render. Server and universal units
16231
+ // continue to use their own batch helpers.
16232
+ const fallbackHelper =
16233
+ ctx.mode === 'client' && ctx._universalRuntimeUnit == null
16234
+ ? 'registerWarmPlan'
16235
+ : ctx.mode === 'server'
16236
+ ? 'puBatch'
16237
+ : 'useBatch';
16238
+ const fallback = inheritOriginLoc(
16239
+ b.stmt(
16240
+ fallbackHelper === 'registerWarmPlan'
16241
+ ? b.call(
16242
+ requireRuntimeForContext(ctx, fallbackHelper),
16243
+ warmThunk,
16244
+ b.unary('void', b.literal(0, '0')),
16245
+ )
16246
+ : b.call(requireRuntimeForContext(ctx, fallbackHelper), b.array([]), warmThunk),
16247
+ ),
16132
16248
  warmThunk,
16133
16249
  );
16250
+ let registration = [fallback];
16251
+ if (staticPlanProps !== null) {
16252
+ // Hoisted functions can render before the module-tail markWarm stamp,
16253
+ // and route extraction can leave that stamp behind. Only the common
16254
+ // attached-plan path may omit the in-body closure and array.
16255
+ const planName = allocCompilerName(ctx, '__warmPlan');
16256
+ registration = [
16257
+ inheritOriginLoc(b.const(planName, b.member(b.id(componentName), '__warm')), warmThunk),
16258
+ inheritOriginLoc(
16259
+ b.if(
16260
+ b.binary('===', b.unary('typeof', b.id(planName)), b.literal('function', "'function'")),
16261
+ b.stmt(
16262
+ b.call(
16263
+ requireRuntimeForContext(ctx, 'registerWarmPlan'),
16264
+ b.id(planName),
16265
+ staticPlanProps,
16266
+ ),
16267
+ ),
16268
+ fallback,
16269
+ ),
16270
+ warmThunk,
16271
+ ),
16272
+ ];
16273
+ }
16134
16274
  const finalIndex =
16135
16275
  output.length > 0 && output[output.length - 1].type === 'ReturnStatement'
16136
16276
  ? output.length - 1
16137
16277
  : output.length;
16138
- output.splice(finalIndex, 0, registration);
16278
+ output.splice(finalIndex, 0, ...registration);
16139
16279
  }
16140
16280
  return output;
16141
16281
 
@@ -16229,6 +16369,19 @@ function rewriteParallelUse(statements, ctx, componentName, warmThunk) {
16229
16369
  for (const m of run.members) out.push(m.stmt);
16230
16370
  return;
16231
16371
  }
16372
+ if (
16373
+ uses.every(
16374
+ (m) =>
16375
+ m.call._octaneImportedHook === 'use' &&
16376
+ isProvenContextUse(m.creation, ctx.provenContextBindings),
16377
+ )
16378
+ ) {
16379
+ // Context reads cannot suspend. Preserve the authored statements in
16380
+ // their original order, and leave the first warm thunk available for
16381
+ // the next real data batch. The trailing child-only registration stays.
16382
+ for (const m of run.members) out.push(m.stmt);
16383
+ return;
16384
+ }
16232
16385
  // Hoist each creation into a temp, batch, then re-emit the original
16233
16386
  // statements with creations swapped for the temps. Unwrap order (and
16234
16387
  // hydration-seed order with it) is untouched.
@@ -16419,7 +16572,19 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
16419
16572
  node,
16420
16573
  );
16421
16574
  }
16422
- return { thunk, warmNode };
16575
+ return {
16576
+ thunk,
16577
+ warmNode,
16578
+ // The module plan contains own creations before child calls. Reuse it
16579
+ // for this component's registration only when it is child-only like the
16580
+ // original thunk; otherwise a descendant suspend could refetch its own
16581
+ // earlier memo. Memoized child props stay on the existing path too.
16582
+ staticChildOnly:
16583
+ warmNode !== null &&
16584
+ thunk !== null &&
16585
+ warmMemos.length === 0 &&
16586
+ warmKids.every((child) => child.props.every((prop) => prop.memo == null)),
16587
+ };
16423
16588
  }
16424
16589
 
16425
16590
  // ===========================================================================
@@ -18348,22 +18513,24 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18348
18513
  }
18349
18514
  const hn = `h${holeProps.length}`;
18350
18515
  if (expr && expr.type === 'TSAsExpression') {
18351
- // Preserve the `as T` cast in the renderer (it marks a dynamic TEXT hole).
18516
+ // Preserve explicit string assertions. Numeric assertions and typed
18517
+ // primitive proofs need a synthetic text assertion on the renderer's
18518
+ // `props.hN` because it cannot see the original expression's proof.
18352
18519
  holeProps.push(objectProp(hn, rewriteExtractedFragmentHole(expr.expression, ctx, childNs)));
18520
+ const annotation = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18521
+ ? b.ts_keyword_type('string')
18522
+ : expr.typeAnnotation;
18353
18523
  newChildren.push(
18354
- b.jsx_expression_container(
18355
- b.ts_as(memberProps(hn, expr.expression), expr.typeAnnotation),
18356
- ),
18524
+ b.jsx_expression_container(b.ts_as(memberProps(hn, expr.expression), annotation)),
18357
18525
  );
18358
18526
  } else {
18359
18527
  holeProps.push(objectProp(hn, rewriteExtractedFragmentHole(expr, ctx, childNs)));
18360
- // A hole the compiler proved is a string (concat / template / tracked
18361
- // local) is a TEXT hole — but the renderer only sees `props.hN`, which it
18362
- // can't prove. Re-assert it with an `as string` cast so the renderer keeps
18363
- // the text-binding classification (htext, markerless) instead of falling to
18364
- // a renderable childSlot, preserving byte-equality with the inline form.
18528
+ // The renderer only sees `props.hN`, so it cannot rediscover an inline
18529
+ // string or primitive-number proof. This synthetic cast preserves the
18530
+ // text binding across extraction; it is stripped from emitted JS and
18531
+ // does not change the authored expression or its runtime value.
18365
18532
  const member = memberProps(hn, expr);
18366
- const rendered = isKnownStringChildExpression(expr, ctx.knownStringChildLocals)
18533
+ const rendered = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18367
18534
  ? b.ts_as(member, b.ts_keyword_type('string'))
18368
18535
  : member;
18369
18536
  newChildren.push(b.jsx_expression_container(rendered));
@@ -20563,15 +20730,14 @@ function headElementArgNodes(node, index, ctx) {
20563
20730
  ];
20564
20731
  }
20565
20732
 
20566
- // A Float style resource ships its authored CSS verbatim-by-meaning: the parsed
20567
- // StyleSheet re-renders WITHOUT the scoping pipeline (no analyze/prepare), so
20568
- // selectors and rules come out unscoped. Serialization normalizes whitespace,
20569
- // which is invisible to the resource contract (identity is the href).
20733
+ // Float resources contain global CSS. Preserve the parser's CSS source rather
20734
+ // than sending it through the scoped stylesheet renderer, which prunes unused
20735
+ // selectors and renames keyframes even without the scoping analysis pass.
20570
20736
  /** @param {any} el @returns {any} */
20571
20737
  function styleResourceCssExpression(el) {
20572
20738
  const sheet = (el.children || []).find((c) => c && c.type === 'StyleSheet');
20573
20739
  if (!sheet) return b.literal('');
20574
- const css = renderStylesheets([cloneAstNode(sheet)]);
20740
+ const css = sheet.source;
20575
20741
  return b.literal(css, JSON.stringify(css), el);
20576
20742
  }
20577
20743
 
@@ -21180,17 +21346,30 @@ function staticTextLiteral(node) {
21180
21346
  // to inherit a type proof. Ordinary copy-on-write expression rewrites preserve
21181
21347
  // the metadata, and extractFragment explicitly carries it onto its props hole.
21182
21348
  function applyStringChildProofs(ast, source, filename, facts) {
21183
- const typedRanges = createTextTypeFactsLookup(facts, filename, source);
21349
+ const typeRanges = createTextTypeFactsLookup(facts, filename, source);
21350
+ const typedRanges = typeRanges?.stringRanges ?? null;
21351
+ const primitiveRanges = typeRanges?.primitiveRanges ?? null;
21184
21352
  // Avoid another AST walk for the overwhelmingly common syntax-only module.
21185
- // Escaped identifiers can spell String too; declining a module without either
21186
- // spelling is safe, while the AST remains the authority for actual calls.
21187
- const inspectStringCalls = source.includes('String') || source.includes('\\u');
21188
- if ((typedRanges === null || typedRanges.size === 0) && !inspectStringCalls) return ast;
21189
- const remainingRanges = typedRanges;
21190
- const proofs = new Set();
21191
- const stringCalls = [];
21353
+ // Escaped identifiers can spell these built-ins too. The AST remains the
21354
+ // authority for calls; these substring checks only skip common files.
21355
+ const inspectIntrinsicCalls =
21356
+ source.includes('String') ||
21357
+ source.includes('Number') ||
21358
+ source.includes('BigInt') ||
21359
+ source.includes('Date') ||
21360
+ source.includes('\\u');
21361
+ if (
21362
+ (typedRanges === null || typedRanges.size === 0) &&
21363
+ (primitiveRanges === null || primitiveRanges.size === 0) &&
21364
+ !inspectIntrinsicCalls
21365
+ ) {
21366
+ return ast;
21367
+ }
21368
+ const stringProofs = new Set();
21369
+ const primitiveProofs = new Set();
21370
+ const intrinsicCalls = [];
21192
21371
  const writes = [];
21193
- let ambientStringValue = false;
21372
+ const ambientIntrinsicValues = new Set();
21194
21373
  const seen = new WeakSet();
21195
21374
  const collect = (node, parent = null, key = null) => {
21196
21375
  if (node === null || typeof node !== 'object') return;
@@ -21201,7 +21380,7 @@ function applyStringChildProofs(ast, source, filename, facts) {
21201
21380
  if (seen.has(node)) return;
21202
21381
  seen.add(node);
21203
21382
  if (
21204
- remainingRanges !== null &&
21383
+ (typedRanges !== null || primitiveRanges !== null) &&
21205
21384
  node.type === 'JSXExpressionContainer' &&
21206
21385
  ((key === 'children' &&
21207
21386
  (parent?.type === 'JSXElement' ||
@@ -21212,16 +21391,17 @@ function applyStringChildProofs(ast, source, filename, facts) {
21212
21391
  ) {
21213
21392
  const expr = node.expression;
21214
21393
  const range = `${expr?.start}:${expr?.end}`;
21215
- if (remainingRanges.delete(range)) proofs.add(expr);
21394
+ if (typedRanges?.delete(range)) stringProofs.add(expr);
21395
+ if (primitiveRanges?.delete(range)) primitiveProofs.add(expr);
21216
21396
  }
21217
- if (inspectStringCalls) {
21397
+ if (inspectIntrinsicCalls) {
21218
21398
  if (
21219
21399
  node.type === 'CallExpression' &&
21220
21400
  node.optional !== true &&
21221
21401
  node.callee?.type === 'Identifier' &&
21222
- node.callee.name === 'String'
21402
+ TEXT_INTRINSICS.has(node.callee.name)
21223
21403
  ) {
21224
- stringCalls.push(node);
21404
+ intrinsicCalls.push(node);
21225
21405
  }
21226
21406
  if (node.type === 'AssignmentExpression') writes.push(node.left);
21227
21407
  else if (node.type === 'UpdateExpression') writes.push(node.argument);
@@ -21236,11 +21416,13 @@ function applyStringChildProofs(ast, source, filename, facts) {
21236
21416
  // Ambient VALUE declarations can describe a replaced global. Type-only
21237
21417
  // imports/interfaces do not shadow the JavaScript built-in.
21238
21418
  if (node.type === 'TSDeclareFunction' || node.declare === true) {
21239
- if (node.id?.name === 'String') ambientStringValue = true;
21419
+ if (TEXT_INTRINSICS.has(node.id?.name)) ambientIntrinsicValues.add(node.id.name);
21240
21420
  if (node.type === 'VariableDeclaration') {
21241
21421
  const names = new Set();
21242
21422
  for (const declaration of node.declarations || []) collectBindings(declaration.id, names);
21243
- if (names.has('String')) ambientStringValue = true;
21423
+ for (const name of names) {
21424
+ if (TEXT_INTRINSICS.has(name)) ambientIntrinsicValues.add(name);
21425
+ }
21244
21426
  }
21245
21427
  }
21246
21428
  }
@@ -21251,33 +21433,43 @@ function applyStringChildProofs(ast, source, filename, facts) {
21251
21433
  }
21252
21434
  };
21253
21435
  collect(ast);
21254
- if (remainingRanges !== null && remainingRanges.size !== 0) {
21436
+ const remainingRange =
21437
+ typedRanges?.values().next().value ?? primitiveRanges?.values().next().value;
21438
+ if (remainingRange !== undefined) {
21255
21439
  throw new Error(
21256
- `Invalid textTypeFacts for ${JSON.stringify(filename)}: range ${remainingRanges.values().next().value} is not an authored JSX child expression.`,
21440
+ `Invalid textTypeFacts for ${JSON.stringify(filename)}: range ${remainingRange} is not an authored JSX child expression.`,
21257
21441
  );
21258
21442
  }
21259
21443
  let lexical = null;
21260
- if (stringCalls.length > 0 || (proofs.size > 0 && writes.length > 0)) {
21444
+ if (
21445
+ intrinsicCalls.length > 0 ||
21446
+ ((stringProofs.size > 0 || primitiveProofs.size > 0) && writes.length > 0)
21447
+ ) {
21261
21448
  // The existing scope analysis understands var hoisting, parameter defaults,
21262
21449
  // imports, catch bindings, and TSRX @for/@try scopes. Pay for it only when
21263
21450
  // an intrinsic candidate or a possible proof-invalidating write needs it.
21264
21451
  lexical = createLexicalAnalysis(ast);
21265
- if (writes.some((target) => writesGlobalString(target, lexical))) {
21266
- // TypeScript still calls the global constructor's return type `string`
21267
- // after an asserted replacement. Decline ALL new child proofs in this
21452
+ if (writes.some((target) => writesGlobalTextIntrinsic(target, lexical))) {
21453
+ // TypeScript still uses a built-in constructor's declared primitive
21454
+ // return type after an asserted replacement. Decline all new proofs in this
21268
21455
  // rare module rather than attempting incomplete value-taint analysis.
21269
21456
  // Supplied ranges were still validated above; explicit text syntax keeps
21270
21457
  // its existing coercion meaning through isKnownStringExpression.
21271
21458
  return ast;
21272
21459
  }
21273
21460
  }
21274
- if (lexical !== null && !ambientStringValue) {
21275
- for (const call of stringCalls) {
21461
+ if (lexical !== null) {
21462
+ for (const call of intrinsicCalls) {
21463
+ const name = call.callee.name;
21464
+ if (ambientIntrinsicValues.has(name)) continue;
21276
21465
  const scope = lexical.nodeScopes.get(call);
21277
- if (scope !== undefined && !lexical.isBound(scope, 'String')) proofs.add(call);
21466
+ if (scope !== undefined && !lexical.isBound(scope, name)) {
21467
+ if (name === 'String' || name === 'Date') stringProofs.add(call);
21468
+ else primitiveProofs.add(call);
21469
+ }
21278
21470
  }
21279
21471
  }
21280
- if (proofs.size === 0) return ast;
21472
+ if (stringProofs.size === 0 && primitiveProofs.size === 0) return ast;
21281
21473
  const rewritten = new WeakMap();
21282
21474
  const rewrite = (node) => {
21283
21475
  if (node === null || typeof node !== 'object') return node;
@@ -21303,8 +21495,10 @@ function applyStringChildProofs(ast, source, filename, facts) {
21303
21495
  out[key] = next;
21304
21496
  }
21305
21497
  }
21306
- if (proofs.has(node)) {
21498
+ if (stringProofs.has(node)) {
21307
21499
  out = { ...out, metadata: { ...out.metadata, octane_string_child: true } };
21500
+ } else if (primitiveProofs.has(node)) {
21501
+ out = { ...out, metadata: { ...out.metadata, octane_primitive_text_child: true } };
21308
21502
  }
21309
21503
  }
21310
21504
  rewritten.set(node, out);
@@ -21313,10 +21507,12 @@ function applyStringChildProofs(ast, source, filename, facts) {
21313
21507
  return rewrite(ast);
21314
21508
  }
21315
21509
 
21316
- // A visible replacement of the global constructor invalidates intrinsic
21317
- // recognition for the whole module. Calls still evaluate the authored callee;
21318
- // do not silently turn a replacement's returned element into its string form.
21319
- function writesGlobalString(target, lexical) {
21510
+ const TEXT_INTRINSICS = new Set(['String', 'Number', 'BigInt', 'Date']);
21511
+
21512
+ // A visible replacement of a global constructor invalidates inferred text
21513
+ // proofs for the module. Calls still evaluate the authored callee: a replacement
21514
+ // may return an element rather than the built-in's primitive result.
21515
+ function writesGlobalTextIntrinsic(target, lexical) {
21320
21516
  if (!target || typeof target !== 'object') return false;
21321
21517
  if (
21322
21518
  target.type === 'TSAsExpression' ||
@@ -21324,20 +21520,20 @@ function writesGlobalString(target, lexical) {
21324
21520
  target.type === 'TSNonNullExpression' ||
21325
21521
  target.type === 'ParenthesizedExpression'
21326
21522
  ) {
21327
- return writesGlobalString(target.expression, lexical);
21523
+ return writesGlobalTextIntrinsic(target.expression, lexical);
21328
21524
  }
21329
21525
  const unbound = (node, name) => {
21330
21526
  const scope = lexical.nodeScopes.get(node);
21331
21527
  return scope !== undefined && !lexical.isBound(scope, name);
21332
21528
  };
21333
21529
  if (target.type === 'Identifier') {
21334
- return target.name === 'String' && unbound(target, 'String');
21530
+ return TEXT_INTRINSICS.has(target.name) && unbound(target, target.name);
21335
21531
  }
21336
21532
  if (target.type === 'MemberExpression') {
21337
21533
  const name = target.computed ? target.property?.value : target.property?.name;
21338
21534
  const object = target.object;
21339
21535
  return (
21340
- name === 'String' &&
21536
+ TEXT_INTRINSICS.has(name) &&
21341
21537
  object?.type === 'Identifier' &&
21342
21538
  (object.name === 'globalThis' ||
21343
21539
  object.name === 'window' ||
@@ -21346,14 +21542,14 @@ function writesGlobalString(target, lexical) {
21346
21542
  unbound(object, object.name)
21347
21543
  );
21348
21544
  }
21349
- if (target.type === 'AssignmentPattern') return writesGlobalString(target.left, lexical);
21350
- if (target.type === 'RestElement') return writesGlobalString(target.argument, lexical);
21545
+ if (target.type === 'AssignmentPattern') return writesGlobalTextIntrinsic(target.left, lexical);
21546
+ if (target.type === 'RestElement') return writesGlobalTextIntrinsic(target.argument, lexical);
21351
21547
  if (target.type === 'ArrayPattern') {
21352
- return (target.elements || []).some((element) => writesGlobalString(element, lexical));
21548
+ return (target.elements || []).some((element) => writesGlobalTextIntrinsic(element, lexical));
21353
21549
  }
21354
21550
  if (target.type === 'ObjectPattern') {
21355
21551
  return (target.properties || []).some((property) =>
21356
- writesGlobalString(property.argument ?? property.value, lexical),
21552
+ writesGlobalTextIntrinsic(property.argument ?? property.value, lexical),
21357
21553
  );
21358
21554
  }
21359
21555
  return false;
@@ -21366,6 +21562,7 @@ function writesGlobalString(target, lexical) {
21366
21562
  // Recognised shapes:
21367
21563
  // - String Literal: 'foo' / "bar"
21368
21564
  // - TemplateLiteral: `${x}-${y}` (always coerces to string)
21565
+ // - `typeof x`: always a string, even for unknown x
21369
21566
  // - `as string` / `<string>x`: user-asserted string-typed expression
21370
21567
  // - `satisfies string`: same intent
21371
21568
  // - Wrappers (`!`, instantiation): peel and check inside
@@ -21380,6 +21577,7 @@ function isKnownStringExpression(node, locals, childProofs = false) {
21380
21577
  return typeof node.value === 'string';
21381
21578
  }
21382
21579
  if (node.type === 'TemplateLiteral') return true;
21580
+ if (node.type === 'UnaryExpression' && node.operator === 'typeof') return true;
21383
21581
  // An identifier the compiler has tracked back to a string in this component's
21384
21582
  // scope: a `const` bound to a provably-string expression, a `const x: string`,
21385
21583
  // or a `string`-typed param — see collectKnownStringLocals. `locals` is
@@ -21423,6 +21621,15 @@ function isKnownStringExpression(node, locals, childProofs = false) {
21423
21621
  isKnownStringExpression(node.alternate, locals, childProofs)
21424
21622
  );
21425
21623
  }
21624
+ if (node.type === 'LogicalExpression') {
21625
+ return (
21626
+ isKnownStringExpression(node.left, locals, childProofs) &&
21627
+ isKnownStringExpression(node.right, locals, childProofs)
21628
+ );
21629
+ }
21630
+ if (node.type === 'SequenceExpression') {
21631
+ return isKnownStringExpression(node.expressions.at(-1), locals, childProofs);
21632
+ }
21426
21633
  return false;
21427
21634
  }
21428
21635
 
@@ -21433,6 +21640,75 @@ function isKnownStringChildExpression(node, locals) {
21433
21640
  return isKnownStringExpression(node, locals, true);
21434
21641
  }
21435
21642
 
21643
+ // Only child positions may use the primitive-text binding for numbers and
21644
+ // bigints. These expressions are primitives (or throw before returning) under
21645
+ // JavaScript's own operators. Keep the proof distinct from "known string":
21646
+ // numeric results cannot establish that `value + anotherValue` concatenates.
21647
+ function isKnownTextChildExpression(node, stringLocals) {
21648
+ if (isKnownStringChildExpression(node, stringLocals)) return true;
21649
+ if (node == null || typeof node !== 'object') return false;
21650
+ if (node.metadata?.octane_primitive_text_child === true) return true;
21651
+ if (node.type === 'Literal' || node.type === 'NumericLiteral' || node.type === 'BigIntLiteral') {
21652
+ return (
21653
+ typeof node.value === 'number' ||
21654
+ typeof node.value === 'bigint' ||
21655
+ node.type === 'BigIntLiteral'
21656
+ );
21657
+ }
21658
+ if (node.type === 'UnaryExpression') {
21659
+ return node.operator === '+' || node.operator === '-' || node.operator === '~';
21660
+ }
21661
+ if (node.type === 'BinaryExpression') {
21662
+ if (node.operator === '+') {
21663
+ return (
21664
+ isKnownTextChildExpression(node.left, stringLocals) &&
21665
+ isKnownTextChildExpression(node.right, stringLocals)
21666
+ );
21667
+ }
21668
+ return NUMERIC_TEXT_OPERATORS.has(node.operator);
21669
+ }
21670
+ if (node.type === 'ConditionalExpression') {
21671
+ return (
21672
+ isKnownTextChildExpression(node.consequent, stringLocals) &&
21673
+ isKnownTextChildExpression(node.alternate, stringLocals)
21674
+ );
21675
+ }
21676
+ if (node.type === 'LogicalExpression') {
21677
+ return (
21678
+ isKnownTextChildExpression(node.left, stringLocals) &&
21679
+ isKnownTextChildExpression(node.right, stringLocals)
21680
+ );
21681
+ }
21682
+ if (node.type === 'SequenceExpression') {
21683
+ return isKnownTextChildExpression(node.expressions.at(-1), stringLocals);
21684
+ }
21685
+ if (
21686
+ node.type === 'TSAsExpression' ||
21687
+ node.type === 'TSTypeAssertion' ||
21688
+ node.type === 'TSSatisfiesExpression' ||
21689
+ node.type === 'TSNonNullExpression' ||
21690
+ node.type === 'TSInstantiationExpression' ||
21691
+ node.type === 'ParenthesizedExpression'
21692
+ ) {
21693
+ return isKnownTextChildExpression(node.expression, stringLocals);
21694
+ }
21695
+ return false;
21696
+ }
21697
+
21698
+ const NUMERIC_TEXT_OPERATORS = new Set([
21699
+ '-',
21700
+ '*',
21701
+ '/',
21702
+ '%',
21703
+ '**',
21704
+ '<<',
21705
+ '>>',
21706
+ '>>>',
21707
+ '|',
21708
+ '&',
21709
+ '^',
21710
+ ]);
21711
+
21436
21712
  // Annotation check: does a TS type annotation resolve to `string`? Accepts both a
21437
21713
  // bare type node and a `TSTypeAnnotation` wrapper (`x: string`).
21438
21714
  function isStringTypeAnnotation(ann) {
@@ -22020,7 +22296,7 @@ function planJsx(
22020
22296
  const coalesceChildRoot =
22021
22297
  jsxNodes.length === 1 &&
22022
22298
  ((jsxNodes[0].type === 'Text' &&
22023
- !isKnownStringChildExpression(jsxNodes[0].expression, ctx.knownStringChildLocals)) ||
22299
+ !isKnownTextChildExpression(jsxNodes[0].expression, ctx.knownStringChildLocals)) ||
22024
22300
  jsxNodes[0].type === 'TSRXExpression');
22025
22301
  // Top-level control-flow directives (@if/@for/@switch/@try/<Activity>). In a
22026
22302
  // body that ALSO has static template roots, each construct emits a `<!>`
@@ -22467,6 +22743,8 @@ function planJsx(
22467
22743
  // a mount-only import for deferred writes.
22468
22744
  const attrHelper = attrBindingHelper(b);
22469
22745
  if (attrHelper !== null) {
22746
+ if (b.kind === 'class' && b.fresh && (!b.deferred || (!b.mountOnly && inlineBindingGuards)))
22747
+ ctx.runtimeNeeded.add('normalizeClass');
22470
22748
  if (!b.deferred) {
22471
22749
  ctx.runtimeNeeded.add(attrHelper);
22472
22750
  registerAttrLoweringOrigin(ctx, b.nameOrigin, attrHelper, b.name);
@@ -23876,12 +24154,10 @@ function commitSourceRows(sources, valueOf) {
23876
24154
  // is byte-identical to the old unconditional mount write).
23877
24155
  function emitDeferredMount(bind, elVar, bag) {
23878
24156
  // Whole-object `style` diffs on `_sty`; scalar styles and other values on `_prev`.
23879
- if (!(bind.kind === 'class' && bind.fresh)) {
23880
- bag.constField(
23881
- bind.kind === 'style' ? `_sty$${bind.id}` : `_prev$${bind.id}`,
23882
- bind.kind === 'styleProperty' ? 'style-unset' : 'undefined',
23883
- );
23884
- }
24157
+ bag.constField(
24158
+ bind.kind === 'style' ? `_sty$${bind.id}` : `_prev$${bind.id}`,
24159
+ bind.kind === 'styleProperty' ? 'style-unset' : 'undefined',
24160
+ );
23885
24161
  const key = `_el$${bind.id}`;
23886
24162
  if (!bag.host(key, elVar)) return null;
23887
24163
  return inheritOriginLoc(
@@ -24089,9 +24365,15 @@ function emitBindingMount(bind, elVar, bag) {
24089
24365
  }
24090
24366
  case 'class': {
24091
24367
  // On SVG/MathML hosts the `className` property is read-only — fall back
24092
- // to setAttribute. Compile-time choice, zero runtime branching.
24093
- const body = [b.const('_v', bind.expr), b.stmt(b.call(callee(), el(), V())), ...mountHost()];
24094
- if (!bind.fresh) body.push(b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())));
24368
+ // to setAttribute. A fresh literal is truthy even when it composes to
24369
+ // an empty string, so caching its composition preserves class="".
24370
+ const value = bind.fresh ? b.call('_$normalizeClass', bind.expr) : bind.expr;
24371
+ const body = [
24372
+ b.const('_v', value),
24373
+ b.stmt(b.call(callee(), el(), V())),
24374
+ ...mountHost(),
24375
+ b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
24376
+ ];
24095
24377
  return st(b.block(body));
24096
24378
  }
24097
24379
  case 'style': {
@@ -24386,13 +24668,14 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
24386
24668
  return st(b.stmt(b.call(callee(), F('_el'), bind.expr)));
24387
24669
  }
24388
24670
  case 'class': {
24389
- if (bind.fresh) {
24390
- return st(b.stmt(b.call(callee(), F('_el'), bind.expr)));
24391
- }
24671
+ // Recompose fresh literal values on every render, including when their
24672
+ // members have getters, but skip the DOM write for an unchanged class.
24673
+ const value =
24674
+ bind.fresh && inlineBindingGuards ? b.call('_$normalizeClass', bind.expr) : bind.expr;
24392
24675
  if (inlineBindingGuards) {
24393
24676
  return st(
24394
24677
  b.block([
24395
- b.const('_v', bind.expr),
24678
+ b.const('_v', value),
24396
24679
  b.if(
24397
24680
  b.binary('!==', F('_prev'), V()),
24398
24681
  b.block([
@@ -24405,7 +24688,7 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
24405
24688
  );
24406
24689
  }
24407
24690
  return st(
24408
- b.stmt(b.assignment('=', F('_prev'), b.call(callee(), bind.expr, F('_prev'), F('_el')))),
24691
+ b.stmt(b.assignment('=', F('_prev'), b.call(callee(), value, F('_prev'), F('_el')))),
24409
24692
  );
24410
24693
  }
24411
24694
  case 'style': {
@@ -24847,12 +25130,11 @@ function emitNodeHtml(
24847
25130
  cssHash = null,
24848
25131
  ) {
24849
25132
  if (node.type === 'Text') {
24850
- if (isKnownStringChildExpression(node.expression, ctx.knownStringChildLocals)) {
25133
+ if (isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals)) {
24851
25134
  bindings.push({
24852
25135
  id: bindings.length,
24853
25136
  kind: 'text',
24854
25137
  expr: resolveStyleExpr(node.expression, cssHash),
24855
- knownString: true,
24856
25138
  path: path.slice(0, -1),
24857
25139
  childIndex: path[path.length - 1],
24858
25140
  });
@@ -26050,12 +26332,11 @@ function emitElementHtml(
26050
26332
  ]
26051
26333
  : null;
26052
26334
  appendTemplatePart(html, escaped, 'text', origins);
26053
- } else if (isKnownStringChildExpression(txtChild.expression, ctx.knownStringChildLocals)) {
26335
+ } else if (isKnownTextChildExpression(txtChild.expression, ctx.knownStringChildLocals)) {
26054
26336
  bindings.push({
26055
26337
  id: bindings.length,
26056
26338
  kind: 'textOnlyChild',
26057
26339
  expr: resolveStyleExpr(txtChild.expression, cssHash),
26058
- knownString: true,
26059
26340
  path,
26060
26341
  });
26061
26342
  // The element stays empty in the template — runtime appends a Text node.
@@ -26224,12 +26505,11 @@ function emitElementHtml(
26224
26505
  appendTemplatePart(html, escaped, 'text', origins);
26225
26506
  prevBakedText = true;
26226
26507
  if (!prevBaked) childIdx++;
26227
- } else if (isKnownStringChildExpression(child.expression, ctx.knownStringChildLocals)) {
26508
+ } else if (isKnownTextChildExpression(child.expression, ctx.knownStringChildLocals)) {
26228
26509
  bindings.push({
26229
26510
  id: bindings.length,
26230
26511
  kind: 'text',
26231
26512
  expr: resolveStyleExpr(child.expression, cssHash),
26232
- knownString: true,
26233
26513
  path,
26234
26514
  childIndex: childIdx,
26235
26515
  // A text-producing neighbour → the server emits a `<!-- -->`
@@ -26396,7 +26676,7 @@ function emitElementHtml(
26396
26676
  // - `{createPortal(BODY, TARGET, PROPS?)}` → portal() call
26397
26677
  // - `{cond ? <JSX/> : <JSX/>}` → lowered to ifBlock (so the branches
26398
26678
  // mount real DOM, not stringified text)
26399
- // - a known-string expression → text-hole binding
26679
+ // - a proven primitive text expression → text-hole binding
26400
26680
  // - anything else → renderable childSlot hole
26401
26681
  const expr = child.expression;
26402
26682
  if (isCreatePortalCall(expr)) {
@@ -26424,12 +26704,11 @@ function emitElementHtml(
26424
26704
  ifCalls.push(ic);
26425
26705
  appendTemplatePart(html, '<!>', 'anchor');
26426
26706
  childIdx++;
26427
- } else if (isKnownStringChildExpression(expr, ctx.knownStringChildLocals)) {
26707
+ } else if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
26428
26708
  bindings.push({
26429
26709
  id: bindings.length,
26430
26710
  kind: 'text',
26431
26711
  expr: tsrxExprNode(resolveStyleExpr(expr, cssHash), ctx, componentName, inlinedSubs),
26432
- knownString: true,
26433
26712
  path,
26434
26713
  childIndex: childIdx,
26435
26714
  });
@@ -26555,7 +26834,7 @@ function helperCaptures(ctx, stmts, params) {
26555
26834
 
26556
26835
  // A construct's helpers (then+else, all switch cases, try+pending+catch,
26557
26836
  // item+empty) share ONE env tuple — `block.extra` is per construct block and
26558
- // every helper destructures the same layout — so the emitted array is the
26837
+ // every helper reads from the same layout — so the emitted array is the
26559
26838
  // sorted UNION of each body's captures. Null propagates (no component
26560
26839
  // context → all of the construct's helpers stay inline).
26561
26840
  function unionEnv(ctx, bodies) {
@@ -26578,7 +26857,7 @@ function unionEnv(ctx, bodies) {
26578
26857
  // `__extra` ABI slot (renderBlock forwards `block.extra` as the third body
26579
26858
  // arg; the compiled call site passes the current values every parent render):
26580
26859
  //
26581
- // function __then$0(__props, __s, __extra) { const [label] = __extra; … }
26860
+ // function __then$0(__props, __s, __extra) { const label = __extra[0]; … }
26582
26861
  //
26583
26862
  // `envNames: null` keeps the legacy placement — the helper is emitted INSIDE
26584
26863
  // the component function so its closures capture the parent's locals
@@ -26603,25 +26882,27 @@ function hoistBodyHelper(
26603
26882
  const ownEnv = new Set(ownEnvNames || []);
26604
26883
  let bodyStmts = stmts;
26605
26884
  if (envNames && envNames.length > 0) {
26606
- // Destructure the construct's shared env tuple. The layout is the UNION
26607
- // across the construct's helpers. An arm leaves holes for union-only names:
26608
- // binding them could collide with a same-named local that shadows a capture
26609
- // used only by another arm.
26610
- // The env destructure maps to the construct body it feeds.
26885
+ // Read only this helper's captures at their positions in the construct's
26886
+ // shared UNION tuple. Union-only names must not become local bindings: an
26887
+ // arm may have a same-named local shadowing another arm's capture. Direct
26888
+ // indexed reads avoid iterator work before these helpers optimize.
26889
+ // Each binding maps to the construct body it feeds.
26611
26890
  const envOrigin =
26612
26891
  (Array.isArray(stmts) && stmts.find((s) => s != null && s.loc != null)) ||
26613
26892
  (params || []).find((p) => p != null && p.loc != null) ||
26614
26893
  null;
26615
- bodyStmts = [
26616
- inheritOriginLoc(
26617
- b.const(
26618
- b.array_pattern(envNames.map((n) => (ownEnv.has(n) ? b.id(n) : null))),
26619
- b.id('__extra'),
26894
+ const captures = [];
26895
+ for (let i = 0; i < envNames.length; i++) {
26896
+ const name = envNames[i];
26897
+ if (!ownEnv.has(name)) continue;
26898
+ captures.push(
26899
+ inheritOriginLoc(
26900
+ b.const(b.id(name), b.member(b.id('__extra'), b.literal(i), true)),
26901
+ envOrigin,
26620
26902
  ),
26621
- envOrigin,
26622
- ),
26623
- ...stmts,
26624
- ];
26903
+ );
26904
+ }
26905
+ bodyStmts = [...captures, ...stmts];
26625
26906
  }
26626
26907
  // The synthetic helper shell maps to the construct body it hoists.
26627
26908
  const fakeOrigin =
@@ -26650,7 +26931,7 @@ function hoistBodyHelper(
26650
26931
  // Module-scope placement. Nested constructs compiled INSIDE this body
26651
26932
  // compute THEIR captures against the names visible at their call sites —
26652
26933
  // which live in THIS compiled body: the component's locals extended with
26653
- // this helper's params, env destructure, and body-level locals. (A nested
26934
+ // this helper's params, env bindings, and body-level locals. (A nested
26654
26935
  // helper's env values are emitted as plain identifiers at its call site.)
26655
26936
  const prevLocals = ctx.currentComponentLocals;
26656
26937
  const prevInvariantLocals = ctx.currentInvariantLocals;
@@ -28249,7 +28530,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28249
28530
  // event-row captures in the same order improves the compressed fragment.
28250
28531
  envNames = [envNames[1], envNames[0], ...envNames.slice(2)];
28251
28532
  }
28252
- // The helper destructures only component-local captures (`envNames`) from the
28533
+ // The helper reads only component-local captures (`envNames`) from the
28253
28534
  // tuple prefix. Compiler memoization may additionally need live imported
28254
28535
  // bindings as dependency witnesses; append them without disturbing that ABI.
28255
28536
  const runtimeDepNames =