octane 0.2.6 → 0.2.7

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,7 +81,11 @@ 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,
@@ -444,8 +448,8 @@ function attrBindingHelper(bind) {
444
448
  // repeating the guard and previous-value publication in every component.
445
449
  // Certified repeated host rows select the ordinary writer and emit an inline
446
450
  // 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.
451
+ // Fresh classes compare their composed string in emitBindingUpdate; controlled
452
+ // form properties must reassert the live DOM value on every render.
449
453
  function attrBindingUpdateHelper(bind, inlineBindingGuards = false) {
450
454
  const helper = attrBindingHelper(bind);
451
455
  if (inlineBindingGuards) return helper;
@@ -456,7 +460,11 @@ function attrBindingUpdateHelper(bind, inlineBindingGuards = false) {
456
460
  case 'ariaAttr':
457
461
  return `${helper}IfChanged`;
458
462
  case 'class':
459
- return bind.fresh ? helper : `${helper}IfChanged`;
463
+ return bind.fresh
464
+ ? bind.ns && bind.ns !== 'html'
465
+ ? 'updateFreshClassAttr'
466
+ : 'updateFreshClassName'
467
+ : `${helper}IfChanged`;
460
468
  default:
461
469
  return helper;
462
470
  }
@@ -1172,6 +1180,8 @@ const INTERNAL_CLIENT_RUNTIME_HELPERS = new Set([
1172
1180
  'setAriaAttributeIfChanged',
1173
1181
  'setClassNameIfChanged',
1174
1182
  'setClassAttrIfChanged',
1183
+ 'updateFreshClassName',
1184
+ 'updateFreshClassAttr',
1175
1185
  'textHoleUpdate',
1176
1186
  'childTextHoleUpdate',
1177
1187
  ...HOOK_MEMO_RUNTIME_HELPERS,
@@ -8586,7 +8596,7 @@ function instrumentProfileComponents(ast, ctx) {
8586
8596
  * Compile a .tsrx source string into JS targeting `octane`.
8587
8597
  * @param {string} source
8588
8598
  * @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] —
8599
+ * @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
8600
  * `dev: true` emits client hydration source-location metadata (per-component
8591
8601
  * `__s.locs`/`__s.locFile`) and, in server mode, source-located native-element
8592
8602
  * scopes for invalid HTML nesting diagnostics. Both are strictly gated so
@@ -8634,7 +8644,6 @@ export function compileForBundler(source, filename, options) {
8634
8644
  }
8635
8645
 
8636
8646
  function compileAuthored(source, filename, options, bundlerMetadata) {
8637
- assertNativeReadOptions(options);
8638
8647
  const mode = (options && options.mode) || 'client';
8639
8648
  if (mode !== 'client' && mode !== 'server') {
8640
8649
  throw new Error(`Unknown compile mode "${mode}" — expected 'client' or 'server'.`);
@@ -8645,6 +8654,7 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8645
8654
  );
8646
8655
  analyzeTsrx(analyzedAst, cleanFilename);
8647
8656
  adoptParserAst(analyzedAst);
8657
+ options = nativeReadOptions(analyzedAst, options);
8648
8658
  assertNativeReadDiagnostics(analyzedAst, source, cleanFilename, options);
8649
8659
  const strongModeEnabled =
8650
8660
  assertStrongMode(analyzedAst, source, cleanFilename, options)?.enabled === true;
@@ -11050,7 +11060,7 @@ function ssrCompileBodyWithMapTemps(
11050
11060
  // produce mergeable text nodes:
11051
11061
  // 'static' — a static string-literal Text (bakes / serializes as literal text)
11052
11062
  // 'empty' — a static literal that renders NOTHING (adjacency-transparent)
11053
- // 'dyn' — a known-string dynamic text hole (client `<!>` + htextSwap,
11063
+ // 'dyn' — a proven primitive text hole (client `<!>` + htextSwap,
11054
11064
  // server markerless ssrText)
11055
11065
  // 'other' — everything else (elements, renderable `{expr}` holes, control
11056
11066
  // flow — all serialize elements or `<!--[-->…<!--]-->` ranges
@@ -11059,9 +11069,7 @@ function textAdjacencyKind(node, ctx) {
11059
11069
  if (node.type !== 'Text') return 'other';
11060
11070
  const lit = staticTextLiteral(node.expression);
11061
11071
  if (lit !== null) return lit === '' ? 'empty' : 'static';
11062
- return isKnownStringChildExpression(node.expression, ctx.knownStringChildLocals)
11063
- ? 'dyn'
11064
- : 'other';
11072
+ return isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals) ? 'dyn' : 'other';
11065
11073
  }
11066
11074
 
11067
11075
  // Does the child at `i` have a text-producing sibling next to it (looking
@@ -11361,13 +11369,14 @@ function ssrEmitNode(
11361
11369
  : null,
11362
11370
  );
11363
11371
  }
11364
- // `{x as string}` / literals / templates / `+`-concats → definite TEXT.
11372
+ // String assertions / templates / string concats / proven numeric results
11373
+ // → a text binding on both client and server.
11365
11374
  // Everything else (`{children}`, `{<Comp/>}`, possibly-renderable values)
11366
11375
  // → ssrChild, which RENDERS a component/element child (and coerces a
11367
11376
  // primitive to text) — mirrors Ripple's `{expr}` vs `{expr as string}`.
11368
11377
  // rewriteHookCalls: a `use(thenable)` in this hole bypasses the setup
11369
11378
  // rewrite, so key it here too (else it collides with sibling/nested use()).
11370
- if (isKnownStringChildExpression(expr, ctx.knownStringChildLocals)) {
11379
+ if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
11371
11380
  // ssrTextPre = ssrText + the runtime leading-'\n' protection (the value
11372
11381
  // isn't known at compile time here).
11373
11382
  const fn =
@@ -12253,7 +12262,7 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12253
12262
  // markerless `childTextHole` mount: a primitive is the host's bare text, an object
12254
12263
  // still gets a `<!--[-->…<!--]-->` block). Must match the client's only-child
12255
12264
  // 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
12265
+ // child that is neither a static literal (baked into HTML) nor proven text
12257
12266
  // (emitted via `ssrText`).
12258
12267
  const onlyChild0 =
12259
12268
  normChildren.length === 1 && normChildren[0].type === 'Text' ? normChildren[0] : null;
@@ -12265,7 +12274,7 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
12265
12274
  htmlSources.length === 0 &&
12266
12275
  onlyChild0 !== null &&
12267
12276
  staticTextLiteral(onlyChild0.expression) === null &&
12268
- !isKnownStringChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
12277
+ !isKnownTextChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
12269
12278
  ) {
12270
12279
  const childHelper =
12271
12280
  tag === 'pre' || tag === 'textarea' || tag === 'listing' ? 'ssrChildTextPre' : 'ssrChildText';
@@ -14929,10 +14938,8 @@ function isMountStableInlineHandler(node, ctx) {
14929
14938
  return true;
14930
14939
  }
14931
14940
 
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.
14941
+ // Object/array/function literals allocate a new identity on every evaluation.
14942
+ // Class bindings compare their composed string instead of that fresh identity.
14936
14943
  function isFreshBindingExpr(node) {
14937
14944
  const value = unwrapTsExpr(node);
14938
14945
  return (
@@ -18348,22 +18355,24 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18348
18355
  }
18349
18356
  const hn = `h${holeProps.length}`;
18350
18357
  if (expr && expr.type === 'TSAsExpression') {
18351
- // Preserve the `as T` cast in the renderer (it marks a dynamic TEXT hole).
18358
+ // Preserve explicit string assertions. Numeric assertions and typed
18359
+ // primitive proofs need a synthetic text assertion on the renderer's
18360
+ // `props.hN` because it cannot see the original expression's proof.
18352
18361
  holeProps.push(objectProp(hn, rewriteExtractedFragmentHole(expr.expression, ctx, childNs)));
18362
+ const annotation = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18363
+ ? b.ts_keyword_type('string')
18364
+ : expr.typeAnnotation;
18353
18365
  newChildren.push(
18354
- b.jsx_expression_container(
18355
- b.ts_as(memberProps(hn, expr.expression), expr.typeAnnotation),
18356
- ),
18366
+ b.jsx_expression_container(b.ts_as(memberProps(hn, expr.expression), annotation)),
18357
18367
  );
18358
18368
  } else {
18359
18369
  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.
18370
+ // The renderer only sees `props.hN`, so it cannot rediscover an inline
18371
+ // string or primitive-number proof. This synthetic cast preserves the
18372
+ // text binding across extraction; it is stripped from emitted JS and
18373
+ // does not change the authored expression or its runtime value.
18365
18374
  const member = memberProps(hn, expr);
18366
- const rendered = isKnownStringChildExpression(expr, ctx.knownStringChildLocals)
18375
+ const rendered = isKnownTextChildExpression(expr, ctx.knownStringChildLocals)
18367
18376
  ? b.ts_as(member, b.ts_keyword_type('string'))
18368
18377
  : member;
18369
18378
  newChildren.push(b.jsx_expression_container(rendered));
@@ -20563,15 +20572,14 @@ function headElementArgNodes(node, index, ctx) {
20563
20572
  ];
20564
20573
  }
20565
20574
 
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).
20575
+ // Float resources contain global CSS. Preserve the parser's CSS source rather
20576
+ // than sending it through the scoped stylesheet renderer, which prunes unused
20577
+ // selectors and renames keyframes even without the scoping analysis pass.
20570
20578
  /** @param {any} el @returns {any} */
20571
20579
  function styleResourceCssExpression(el) {
20572
20580
  const sheet = (el.children || []).find((c) => c && c.type === 'StyleSheet');
20573
20581
  if (!sheet) return b.literal('');
20574
- const css = renderStylesheets([cloneAstNode(sheet)]);
20582
+ const css = sheet.source;
20575
20583
  return b.literal(css, JSON.stringify(css), el);
20576
20584
  }
20577
20585
 
@@ -21180,17 +21188,30 @@ function staticTextLiteral(node) {
21180
21188
  // to inherit a type proof. Ordinary copy-on-write expression rewrites preserve
21181
21189
  // the metadata, and extractFragment explicitly carries it onto its props hole.
21182
21190
  function applyStringChildProofs(ast, source, filename, facts) {
21183
- const typedRanges = createTextTypeFactsLookup(facts, filename, source);
21191
+ const typeRanges = createTextTypeFactsLookup(facts, filename, source);
21192
+ const typedRanges = typeRanges?.stringRanges ?? null;
21193
+ const primitiveRanges = typeRanges?.primitiveRanges ?? null;
21184
21194
  // 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 = [];
21195
+ // Escaped identifiers can spell these built-ins too. The AST remains the
21196
+ // authority for calls; these substring checks only skip common files.
21197
+ const inspectIntrinsicCalls =
21198
+ source.includes('String') ||
21199
+ source.includes('Number') ||
21200
+ source.includes('BigInt') ||
21201
+ source.includes('Date') ||
21202
+ source.includes('\\u');
21203
+ if (
21204
+ (typedRanges === null || typedRanges.size === 0) &&
21205
+ (primitiveRanges === null || primitiveRanges.size === 0) &&
21206
+ !inspectIntrinsicCalls
21207
+ ) {
21208
+ return ast;
21209
+ }
21210
+ const stringProofs = new Set();
21211
+ const primitiveProofs = new Set();
21212
+ const intrinsicCalls = [];
21192
21213
  const writes = [];
21193
- let ambientStringValue = false;
21214
+ const ambientIntrinsicValues = new Set();
21194
21215
  const seen = new WeakSet();
21195
21216
  const collect = (node, parent = null, key = null) => {
21196
21217
  if (node === null || typeof node !== 'object') return;
@@ -21201,7 +21222,7 @@ function applyStringChildProofs(ast, source, filename, facts) {
21201
21222
  if (seen.has(node)) return;
21202
21223
  seen.add(node);
21203
21224
  if (
21204
- remainingRanges !== null &&
21225
+ (typedRanges !== null || primitiveRanges !== null) &&
21205
21226
  node.type === 'JSXExpressionContainer' &&
21206
21227
  ((key === 'children' &&
21207
21228
  (parent?.type === 'JSXElement' ||
@@ -21212,16 +21233,17 @@ function applyStringChildProofs(ast, source, filename, facts) {
21212
21233
  ) {
21213
21234
  const expr = node.expression;
21214
21235
  const range = `${expr?.start}:${expr?.end}`;
21215
- if (remainingRanges.delete(range)) proofs.add(expr);
21236
+ if (typedRanges?.delete(range)) stringProofs.add(expr);
21237
+ if (primitiveRanges?.delete(range)) primitiveProofs.add(expr);
21216
21238
  }
21217
- if (inspectStringCalls) {
21239
+ if (inspectIntrinsicCalls) {
21218
21240
  if (
21219
21241
  node.type === 'CallExpression' &&
21220
21242
  node.optional !== true &&
21221
21243
  node.callee?.type === 'Identifier' &&
21222
- node.callee.name === 'String'
21244
+ TEXT_INTRINSICS.has(node.callee.name)
21223
21245
  ) {
21224
- stringCalls.push(node);
21246
+ intrinsicCalls.push(node);
21225
21247
  }
21226
21248
  if (node.type === 'AssignmentExpression') writes.push(node.left);
21227
21249
  else if (node.type === 'UpdateExpression') writes.push(node.argument);
@@ -21236,11 +21258,13 @@ function applyStringChildProofs(ast, source, filename, facts) {
21236
21258
  // Ambient VALUE declarations can describe a replaced global. Type-only
21237
21259
  // imports/interfaces do not shadow the JavaScript built-in.
21238
21260
  if (node.type === 'TSDeclareFunction' || node.declare === true) {
21239
- if (node.id?.name === 'String') ambientStringValue = true;
21261
+ if (TEXT_INTRINSICS.has(node.id?.name)) ambientIntrinsicValues.add(node.id.name);
21240
21262
  if (node.type === 'VariableDeclaration') {
21241
21263
  const names = new Set();
21242
21264
  for (const declaration of node.declarations || []) collectBindings(declaration.id, names);
21243
- if (names.has('String')) ambientStringValue = true;
21265
+ for (const name of names) {
21266
+ if (TEXT_INTRINSICS.has(name)) ambientIntrinsicValues.add(name);
21267
+ }
21244
21268
  }
21245
21269
  }
21246
21270
  }
@@ -21251,33 +21275,43 @@ function applyStringChildProofs(ast, source, filename, facts) {
21251
21275
  }
21252
21276
  };
21253
21277
  collect(ast);
21254
- if (remainingRanges !== null && remainingRanges.size !== 0) {
21278
+ const remainingRange =
21279
+ typedRanges?.values().next().value ?? primitiveRanges?.values().next().value;
21280
+ if (remainingRange !== undefined) {
21255
21281
  throw new Error(
21256
- `Invalid textTypeFacts for ${JSON.stringify(filename)}: range ${remainingRanges.values().next().value} is not an authored JSX child expression.`,
21282
+ `Invalid textTypeFacts for ${JSON.stringify(filename)}: range ${remainingRange} is not an authored JSX child expression.`,
21257
21283
  );
21258
21284
  }
21259
21285
  let lexical = null;
21260
- if (stringCalls.length > 0 || (proofs.size > 0 && writes.length > 0)) {
21286
+ if (
21287
+ intrinsicCalls.length > 0 ||
21288
+ ((stringProofs.size > 0 || primitiveProofs.size > 0) && writes.length > 0)
21289
+ ) {
21261
21290
  // The existing scope analysis understands var hoisting, parameter defaults,
21262
21291
  // imports, catch bindings, and TSRX @for/@try scopes. Pay for it only when
21263
21292
  // an intrinsic candidate or a possible proof-invalidating write needs it.
21264
21293
  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
21294
+ if (writes.some((target) => writesGlobalTextIntrinsic(target, lexical))) {
21295
+ // TypeScript still uses a built-in constructor's declared primitive
21296
+ // return type after an asserted replacement. Decline all new proofs in this
21268
21297
  // rare module rather than attempting incomplete value-taint analysis.
21269
21298
  // Supplied ranges were still validated above; explicit text syntax keeps
21270
21299
  // its existing coercion meaning through isKnownStringExpression.
21271
21300
  return ast;
21272
21301
  }
21273
21302
  }
21274
- if (lexical !== null && !ambientStringValue) {
21275
- for (const call of stringCalls) {
21303
+ if (lexical !== null) {
21304
+ for (const call of intrinsicCalls) {
21305
+ const name = call.callee.name;
21306
+ if (ambientIntrinsicValues.has(name)) continue;
21276
21307
  const scope = lexical.nodeScopes.get(call);
21277
- if (scope !== undefined && !lexical.isBound(scope, 'String')) proofs.add(call);
21308
+ if (scope !== undefined && !lexical.isBound(scope, name)) {
21309
+ if (name === 'String' || name === 'Date') stringProofs.add(call);
21310
+ else primitiveProofs.add(call);
21311
+ }
21278
21312
  }
21279
21313
  }
21280
- if (proofs.size === 0) return ast;
21314
+ if (stringProofs.size === 0 && primitiveProofs.size === 0) return ast;
21281
21315
  const rewritten = new WeakMap();
21282
21316
  const rewrite = (node) => {
21283
21317
  if (node === null || typeof node !== 'object') return node;
@@ -21303,8 +21337,10 @@ function applyStringChildProofs(ast, source, filename, facts) {
21303
21337
  out[key] = next;
21304
21338
  }
21305
21339
  }
21306
- if (proofs.has(node)) {
21340
+ if (stringProofs.has(node)) {
21307
21341
  out = { ...out, metadata: { ...out.metadata, octane_string_child: true } };
21342
+ } else if (primitiveProofs.has(node)) {
21343
+ out = { ...out, metadata: { ...out.metadata, octane_primitive_text_child: true } };
21308
21344
  }
21309
21345
  }
21310
21346
  rewritten.set(node, out);
@@ -21313,10 +21349,12 @@ function applyStringChildProofs(ast, source, filename, facts) {
21313
21349
  return rewrite(ast);
21314
21350
  }
21315
21351
 
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) {
21352
+ const TEXT_INTRINSICS = new Set(['String', 'Number', 'BigInt', 'Date']);
21353
+
21354
+ // A visible replacement of a global constructor invalidates inferred text
21355
+ // proofs for the module. Calls still evaluate the authored callee: a replacement
21356
+ // may return an element rather than the built-in's primitive result.
21357
+ function writesGlobalTextIntrinsic(target, lexical) {
21320
21358
  if (!target || typeof target !== 'object') return false;
21321
21359
  if (
21322
21360
  target.type === 'TSAsExpression' ||
@@ -21324,20 +21362,20 @@ function writesGlobalString(target, lexical) {
21324
21362
  target.type === 'TSNonNullExpression' ||
21325
21363
  target.type === 'ParenthesizedExpression'
21326
21364
  ) {
21327
- return writesGlobalString(target.expression, lexical);
21365
+ return writesGlobalTextIntrinsic(target.expression, lexical);
21328
21366
  }
21329
21367
  const unbound = (node, name) => {
21330
21368
  const scope = lexical.nodeScopes.get(node);
21331
21369
  return scope !== undefined && !lexical.isBound(scope, name);
21332
21370
  };
21333
21371
  if (target.type === 'Identifier') {
21334
- return target.name === 'String' && unbound(target, 'String');
21372
+ return TEXT_INTRINSICS.has(target.name) && unbound(target, target.name);
21335
21373
  }
21336
21374
  if (target.type === 'MemberExpression') {
21337
21375
  const name = target.computed ? target.property?.value : target.property?.name;
21338
21376
  const object = target.object;
21339
21377
  return (
21340
- name === 'String' &&
21378
+ TEXT_INTRINSICS.has(name) &&
21341
21379
  object?.type === 'Identifier' &&
21342
21380
  (object.name === 'globalThis' ||
21343
21381
  object.name === 'window' ||
@@ -21346,14 +21384,14 @@ function writesGlobalString(target, lexical) {
21346
21384
  unbound(object, object.name)
21347
21385
  );
21348
21386
  }
21349
- if (target.type === 'AssignmentPattern') return writesGlobalString(target.left, lexical);
21350
- if (target.type === 'RestElement') return writesGlobalString(target.argument, lexical);
21387
+ if (target.type === 'AssignmentPattern') return writesGlobalTextIntrinsic(target.left, lexical);
21388
+ if (target.type === 'RestElement') return writesGlobalTextIntrinsic(target.argument, lexical);
21351
21389
  if (target.type === 'ArrayPattern') {
21352
- return (target.elements || []).some((element) => writesGlobalString(element, lexical));
21390
+ return (target.elements || []).some((element) => writesGlobalTextIntrinsic(element, lexical));
21353
21391
  }
21354
21392
  if (target.type === 'ObjectPattern') {
21355
21393
  return (target.properties || []).some((property) =>
21356
- writesGlobalString(property.argument ?? property.value, lexical),
21394
+ writesGlobalTextIntrinsic(property.argument ?? property.value, lexical),
21357
21395
  );
21358
21396
  }
21359
21397
  return false;
@@ -21366,6 +21404,7 @@ function writesGlobalString(target, lexical) {
21366
21404
  // Recognised shapes:
21367
21405
  // - String Literal: 'foo' / "bar"
21368
21406
  // - TemplateLiteral: `${x}-${y}` (always coerces to string)
21407
+ // - `typeof x`: always a string, even for unknown x
21369
21408
  // - `as string` / `<string>x`: user-asserted string-typed expression
21370
21409
  // - `satisfies string`: same intent
21371
21410
  // - Wrappers (`!`, instantiation): peel and check inside
@@ -21380,6 +21419,7 @@ function isKnownStringExpression(node, locals, childProofs = false) {
21380
21419
  return typeof node.value === 'string';
21381
21420
  }
21382
21421
  if (node.type === 'TemplateLiteral') return true;
21422
+ if (node.type === 'UnaryExpression' && node.operator === 'typeof') return true;
21383
21423
  // An identifier the compiler has tracked back to a string in this component's
21384
21424
  // scope: a `const` bound to a provably-string expression, a `const x: string`,
21385
21425
  // or a `string`-typed param — see collectKnownStringLocals. `locals` is
@@ -21423,6 +21463,15 @@ function isKnownStringExpression(node, locals, childProofs = false) {
21423
21463
  isKnownStringExpression(node.alternate, locals, childProofs)
21424
21464
  );
21425
21465
  }
21466
+ if (node.type === 'LogicalExpression') {
21467
+ return (
21468
+ isKnownStringExpression(node.left, locals, childProofs) &&
21469
+ isKnownStringExpression(node.right, locals, childProofs)
21470
+ );
21471
+ }
21472
+ if (node.type === 'SequenceExpression') {
21473
+ return isKnownStringExpression(node.expressions.at(-1), locals, childProofs);
21474
+ }
21426
21475
  return false;
21427
21476
  }
21428
21477
 
@@ -21433,6 +21482,75 @@ function isKnownStringChildExpression(node, locals) {
21433
21482
  return isKnownStringExpression(node, locals, true);
21434
21483
  }
21435
21484
 
21485
+ // Only child positions may use the primitive-text binding for numbers and
21486
+ // bigints. These expressions are primitives (or throw before returning) under
21487
+ // JavaScript's own operators. Keep the proof distinct from "known string":
21488
+ // numeric results cannot establish that `value + anotherValue` concatenates.
21489
+ function isKnownTextChildExpression(node, stringLocals) {
21490
+ if (isKnownStringChildExpression(node, stringLocals)) return true;
21491
+ if (node == null || typeof node !== 'object') return false;
21492
+ if (node.metadata?.octane_primitive_text_child === true) return true;
21493
+ if (node.type === 'Literal' || node.type === 'NumericLiteral' || node.type === 'BigIntLiteral') {
21494
+ return (
21495
+ typeof node.value === 'number' ||
21496
+ typeof node.value === 'bigint' ||
21497
+ node.type === 'BigIntLiteral'
21498
+ );
21499
+ }
21500
+ if (node.type === 'UnaryExpression') {
21501
+ return node.operator === '+' || node.operator === '-' || node.operator === '~';
21502
+ }
21503
+ if (node.type === 'BinaryExpression') {
21504
+ if (node.operator === '+') {
21505
+ return (
21506
+ isKnownTextChildExpression(node.left, stringLocals) &&
21507
+ isKnownTextChildExpression(node.right, stringLocals)
21508
+ );
21509
+ }
21510
+ return NUMERIC_TEXT_OPERATORS.has(node.operator);
21511
+ }
21512
+ if (node.type === 'ConditionalExpression') {
21513
+ return (
21514
+ isKnownTextChildExpression(node.consequent, stringLocals) &&
21515
+ isKnownTextChildExpression(node.alternate, stringLocals)
21516
+ );
21517
+ }
21518
+ if (node.type === 'LogicalExpression') {
21519
+ return (
21520
+ isKnownTextChildExpression(node.left, stringLocals) &&
21521
+ isKnownTextChildExpression(node.right, stringLocals)
21522
+ );
21523
+ }
21524
+ if (node.type === 'SequenceExpression') {
21525
+ return isKnownTextChildExpression(node.expressions.at(-1), stringLocals);
21526
+ }
21527
+ if (
21528
+ node.type === 'TSAsExpression' ||
21529
+ node.type === 'TSTypeAssertion' ||
21530
+ node.type === 'TSSatisfiesExpression' ||
21531
+ node.type === 'TSNonNullExpression' ||
21532
+ node.type === 'TSInstantiationExpression' ||
21533
+ node.type === 'ParenthesizedExpression'
21534
+ ) {
21535
+ return isKnownTextChildExpression(node.expression, stringLocals);
21536
+ }
21537
+ return false;
21538
+ }
21539
+
21540
+ const NUMERIC_TEXT_OPERATORS = new Set([
21541
+ '-',
21542
+ '*',
21543
+ '/',
21544
+ '%',
21545
+ '**',
21546
+ '<<',
21547
+ '>>',
21548
+ '>>>',
21549
+ '|',
21550
+ '&',
21551
+ '^',
21552
+ ]);
21553
+
21436
21554
  // Annotation check: does a TS type annotation resolve to `string`? Accepts both a
21437
21555
  // bare type node and a `TSTypeAnnotation` wrapper (`x: string`).
21438
21556
  function isStringTypeAnnotation(ann) {
@@ -22020,7 +22138,7 @@ function planJsx(
22020
22138
  const coalesceChildRoot =
22021
22139
  jsxNodes.length === 1 &&
22022
22140
  ((jsxNodes[0].type === 'Text' &&
22023
- !isKnownStringChildExpression(jsxNodes[0].expression, ctx.knownStringChildLocals)) ||
22141
+ !isKnownTextChildExpression(jsxNodes[0].expression, ctx.knownStringChildLocals)) ||
22024
22142
  jsxNodes[0].type === 'TSRXExpression');
22025
22143
  // Top-level control-flow directives (@if/@for/@switch/@try/<Activity>). In a
22026
22144
  // body that ALSO has static template roots, each construct emits a `<!>`
@@ -22467,6 +22585,8 @@ function planJsx(
22467
22585
  // a mount-only import for deferred writes.
22468
22586
  const attrHelper = attrBindingHelper(b);
22469
22587
  if (attrHelper !== null) {
22588
+ if (b.kind === 'class' && b.fresh && (!b.deferred || (!b.mountOnly && inlineBindingGuards)))
22589
+ ctx.runtimeNeeded.add('normalizeClass');
22470
22590
  if (!b.deferred) {
22471
22591
  ctx.runtimeNeeded.add(attrHelper);
22472
22592
  registerAttrLoweringOrigin(ctx, b.nameOrigin, attrHelper, b.name);
@@ -23876,12 +23996,10 @@ function commitSourceRows(sources, valueOf) {
23876
23996
  // is byte-identical to the old unconditional mount write).
23877
23997
  function emitDeferredMount(bind, elVar, bag) {
23878
23998
  // 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
- }
23999
+ bag.constField(
24000
+ bind.kind === 'style' ? `_sty$${bind.id}` : `_prev$${bind.id}`,
24001
+ bind.kind === 'styleProperty' ? 'style-unset' : 'undefined',
24002
+ );
23885
24003
  const key = `_el$${bind.id}`;
23886
24004
  if (!bag.host(key, elVar)) return null;
23887
24005
  return inheritOriginLoc(
@@ -24089,9 +24207,15 @@ function emitBindingMount(bind, elVar, bag) {
24089
24207
  }
24090
24208
  case 'class': {
24091
24209
  // 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())));
24210
+ // to setAttribute. A fresh literal is truthy even when it composes to
24211
+ // an empty string, so caching its composition preserves class="".
24212
+ const value = bind.fresh ? b.call('_$normalizeClass', bind.expr) : bind.expr;
24213
+ const body = [
24214
+ b.const('_v', value),
24215
+ b.stmt(b.call(callee(), el(), V())),
24216
+ ...mountHost(),
24217
+ b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
24218
+ ];
24095
24219
  return st(b.block(body));
24096
24220
  }
24097
24221
  case 'style': {
@@ -24386,13 +24510,14 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
24386
24510
  return st(b.stmt(b.call(callee(), F('_el'), bind.expr)));
24387
24511
  }
24388
24512
  case 'class': {
24389
- if (bind.fresh) {
24390
- return st(b.stmt(b.call(callee(), F('_el'), bind.expr)));
24391
- }
24513
+ // Recompose fresh literal values on every render, including when their
24514
+ // members have getters, but skip the DOM write for an unchanged class.
24515
+ const value =
24516
+ bind.fresh && inlineBindingGuards ? b.call('_$normalizeClass', bind.expr) : bind.expr;
24392
24517
  if (inlineBindingGuards) {
24393
24518
  return st(
24394
24519
  b.block([
24395
- b.const('_v', bind.expr),
24520
+ b.const('_v', value),
24396
24521
  b.if(
24397
24522
  b.binary('!==', F('_prev'), V()),
24398
24523
  b.block([
@@ -24405,7 +24530,7 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
24405
24530
  );
24406
24531
  }
24407
24532
  return st(
24408
- b.stmt(b.assignment('=', F('_prev'), b.call(callee(), bind.expr, F('_prev'), F('_el')))),
24533
+ b.stmt(b.assignment('=', F('_prev'), b.call(callee(), value, F('_prev'), F('_el')))),
24409
24534
  );
24410
24535
  }
24411
24536
  case 'style': {
@@ -24847,12 +24972,11 @@ function emitNodeHtml(
24847
24972
  cssHash = null,
24848
24973
  ) {
24849
24974
  if (node.type === 'Text') {
24850
- if (isKnownStringChildExpression(node.expression, ctx.knownStringChildLocals)) {
24975
+ if (isKnownTextChildExpression(node.expression, ctx.knownStringChildLocals)) {
24851
24976
  bindings.push({
24852
24977
  id: bindings.length,
24853
24978
  kind: 'text',
24854
24979
  expr: resolveStyleExpr(node.expression, cssHash),
24855
- knownString: true,
24856
24980
  path: path.slice(0, -1),
24857
24981
  childIndex: path[path.length - 1],
24858
24982
  });
@@ -26050,12 +26174,11 @@ function emitElementHtml(
26050
26174
  ]
26051
26175
  : null;
26052
26176
  appendTemplatePart(html, escaped, 'text', origins);
26053
- } else if (isKnownStringChildExpression(txtChild.expression, ctx.knownStringChildLocals)) {
26177
+ } else if (isKnownTextChildExpression(txtChild.expression, ctx.knownStringChildLocals)) {
26054
26178
  bindings.push({
26055
26179
  id: bindings.length,
26056
26180
  kind: 'textOnlyChild',
26057
26181
  expr: resolveStyleExpr(txtChild.expression, cssHash),
26058
- knownString: true,
26059
26182
  path,
26060
26183
  });
26061
26184
  // The element stays empty in the template — runtime appends a Text node.
@@ -26224,12 +26347,11 @@ function emitElementHtml(
26224
26347
  appendTemplatePart(html, escaped, 'text', origins);
26225
26348
  prevBakedText = true;
26226
26349
  if (!prevBaked) childIdx++;
26227
- } else if (isKnownStringChildExpression(child.expression, ctx.knownStringChildLocals)) {
26350
+ } else if (isKnownTextChildExpression(child.expression, ctx.knownStringChildLocals)) {
26228
26351
  bindings.push({
26229
26352
  id: bindings.length,
26230
26353
  kind: 'text',
26231
26354
  expr: resolveStyleExpr(child.expression, cssHash),
26232
- knownString: true,
26233
26355
  path,
26234
26356
  childIndex: childIdx,
26235
26357
  // A text-producing neighbour → the server emits a `<!-- -->`
@@ -26396,7 +26518,7 @@ function emitElementHtml(
26396
26518
  // - `{createPortal(BODY, TARGET, PROPS?)}` → portal() call
26397
26519
  // - `{cond ? <JSX/> : <JSX/>}` → lowered to ifBlock (so the branches
26398
26520
  // mount real DOM, not stringified text)
26399
- // - a known-string expression → text-hole binding
26521
+ // - a proven primitive text expression → text-hole binding
26400
26522
  // - anything else → renderable childSlot hole
26401
26523
  const expr = child.expression;
26402
26524
  if (isCreatePortalCall(expr)) {
@@ -26424,12 +26546,11 @@ function emitElementHtml(
26424
26546
  ifCalls.push(ic);
26425
26547
  appendTemplatePart(html, '<!>', 'anchor');
26426
26548
  childIdx++;
26427
- } else if (isKnownStringChildExpression(expr, ctx.knownStringChildLocals)) {
26549
+ } else if (isKnownTextChildExpression(expr, ctx.knownStringChildLocals)) {
26428
26550
  bindings.push({
26429
26551
  id: bindings.length,
26430
26552
  kind: 'text',
26431
26553
  expr: tsrxExprNode(resolveStyleExpr(expr, cssHash), ctx, componentName, inlinedSubs),
26432
- knownString: true,
26433
26554
  path,
26434
26555
  childIndex: childIdx,
26435
26556
  });
@@ -26555,7 +26676,7 @@ function helperCaptures(ctx, stmts, params) {
26555
26676
 
26556
26677
  // A construct's helpers (then+else, all switch cases, try+pending+catch,
26557
26678
  // 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
26679
+ // every helper reads from the same layout — so the emitted array is the
26559
26680
  // sorted UNION of each body's captures. Null propagates (no component
26560
26681
  // context → all of the construct's helpers stay inline).
26561
26682
  function unionEnv(ctx, bodies) {
@@ -26578,7 +26699,7 @@ function unionEnv(ctx, bodies) {
26578
26699
  // `__extra` ABI slot (renderBlock forwards `block.extra` as the third body
26579
26700
  // arg; the compiled call site passes the current values every parent render):
26580
26701
  //
26581
- // function __then$0(__props, __s, __extra) { const [label] = __extra; … }
26702
+ // function __then$0(__props, __s, __extra) { const label = __extra[0]; … }
26582
26703
  //
26583
26704
  // `envNames: null` keeps the legacy placement — the helper is emitted INSIDE
26584
26705
  // the component function so its closures capture the parent's locals
@@ -26603,25 +26724,27 @@ function hoistBodyHelper(
26603
26724
  const ownEnv = new Set(ownEnvNames || []);
26604
26725
  let bodyStmts = stmts;
26605
26726
  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.
26727
+ // Read only this helper's captures at their positions in the construct's
26728
+ // shared UNION tuple. Union-only names must not become local bindings: an
26729
+ // arm may have a same-named local shadowing another arm's capture. Direct
26730
+ // indexed reads avoid iterator work before these helpers optimize.
26731
+ // Each binding maps to the construct body it feeds.
26611
26732
  const envOrigin =
26612
26733
  (Array.isArray(stmts) && stmts.find((s) => s != null && s.loc != null)) ||
26613
26734
  (params || []).find((p) => p != null && p.loc != null) ||
26614
26735
  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'),
26736
+ const captures = [];
26737
+ for (let i = 0; i < envNames.length; i++) {
26738
+ const name = envNames[i];
26739
+ if (!ownEnv.has(name)) continue;
26740
+ captures.push(
26741
+ inheritOriginLoc(
26742
+ b.const(b.id(name), b.member(b.id('__extra'), b.literal(i), true)),
26743
+ envOrigin,
26620
26744
  ),
26621
- envOrigin,
26622
- ),
26623
- ...stmts,
26624
- ];
26745
+ );
26746
+ }
26747
+ bodyStmts = [...captures, ...stmts];
26625
26748
  }
26626
26749
  // The synthetic helper shell maps to the construct body it hoists.
26627
26750
  const fakeOrigin =
@@ -26650,7 +26773,7 @@ function hoistBodyHelper(
26650
26773
  // Module-scope placement. Nested constructs compiled INSIDE this body
26651
26774
  // compute THEIR captures against the names visible at their call sites —
26652
26775
  // 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
26776
+ // this helper's params, env bindings, and body-level locals. (A nested
26654
26777
  // helper's env values are emitted as plain identifiers at its call site.)
26655
26778
  const prevLocals = ctx.currentComponentLocals;
26656
26779
  const prevInvariantLocals = ctx.currentInvariantLocals;
@@ -28249,7 +28372,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28249
28372
  // event-row captures in the same order improves the compressed fragment.
28250
28373
  envNames = [envNames[1], envNames[0], ...envNames.slice(2)];
28251
28374
  }
28252
- // The helper destructures only component-local captures (`envNames`) from the
28375
+ // The helper reads only component-local captures (`envNames`) from the
28253
28376
  // tuple prefix. Compiler memoization may additionally need live imported
28254
28377
  // bindings as dependency witnesses; append them without disturbing that ABI.
28255
28378
  const runtimeDepNames =