octane 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -149,6 +149,21 @@ for (const name of [
149
149
  ]) {
150
150
  KNOWN_SVG_PROPERTY_SPELLINGS.set(name.toLowerCase(), name);
151
151
  }
152
+ const NON_NATIVE_LOWERCASE_PROPERTIES = /* @__PURE__ */ new Set([
153
+ "autoFocus",
154
+ "defaultValue",
155
+ "defaultChecked",
156
+ "dangerouslySetInnerHTML",
157
+ "className",
158
+ "suppressContentEditableWarning",
159
+ "suppressHydrationWarning",
160
+ "suppressNativeChangeWarning",
161
+ "__octaneNativeChangeDiagnostic",
162
+ "acceptCharset",
163
+ "htmlFor",
164
+ "httpEquiv",
165
+ "viewBox"
166
+ ]);
152
167
  function hostPropertyWarning(name, value, tag, isSvg = false) {
153
168
  const lower = name.toLowerCase();
154
169
  if (name === "for" && tag === "label") return null;
@@ -164,6 +179,9 @@ function hostPropertyWarning(name, value, tag, isSvg = false) {
164
179
  if (name.startsWith("aria-") || /^aria[A-Z]/.test(name) || name.startsWith("data-")) return null;
165
180
  const known = KNOWN_PROPERTY_SPELLINGS.get(lower) ?? (isSvg ? KNOWN_SVG_PROPERTY_SPELLINGS.get(lower) : void 0);
166
181
  if (known !== void 0) {
182
+ if (!isSvg && name === lower && // Hyphenated/namespaced presentation aliases belong to SVG, not HTML.
183
+ !name.includes("-") && !name.includes(":") && !NON_NATIVE_LOWERCASE_PROPERTIES.has(known) && (import_constants.ATTRIBUTE_ALIASES.get(known) ?? lower) === lower)
184
+ return null;
167
185
  if (isSvg && import_constants.ATTRIBUTE_ALIASES.get(known) === name) return null;
168
186
  return name !== known ? `Invalid DOM property \`${name}\`. Did you mean \`${known}\`?` : null;
169
187
  }
@@ -24550,12 +24550,43 @@ function preservesCommittedSuspense(block) {
24550
24550
  }
24551
24551
  return false;
24552
24552
  }
24553
+ function markerlessBranchRoots(scope, domParent, after, end = null) {
24554
+ let roots = 0;
24555
+ const slots = scope._slots;
24556
+ if (slots !== null) {
24557
+ for (let i = 0; i < slots.length; i++) {
24558
+ const slot = slots[i];
24559
+ if (slot.anchor === after && (slot.__kind === "ifBlockSlot" || slot.__kind === "switchBlockSlot" || slot.__kind === "componentSlotSlot" && slot.block?.parentNode === domParent)) {
24560
+ roots++;
24561
+ if (end !== null) {
24562
+ journalRootProperty(slot, "anchor", slot.anchor);
24563
+ slot.anchor = end;
24564
+ }
24565
+ }
24566
+ }
24567
+ }
24568
+ const children = scope.children;
24569
+ if (children !== null) {
24570
+ for (let i = 0; i < children.length; i++) {
24571
+ const child2 = children[i].scope;
24572
+ const block = child2.block;
24573
+ if (block !== scope.block && block.parentNode === domParent && block.endMarker === after) {
24574
+ roots += markerlessBranchRoots(child2, domParent, after, end) || 1;
24575
+ if (end !== null) {
24576
+ journalRootProperty(block, "endMarker", block.endMarker);
24577
+ block.endMarker = end;
24578
+ }
24579
+ }
24580
+ }
24581
+ }
24582
+ return roots;
24583
+ }
24553
24584
  function finalizeMarkerlessBranch(state, domParent, block, marker, before, after) {
24554
24585
  state.markerlessBefore = void 0;
24555
24586
  if (state.borrowed && state.start === null) return;
24556
24587
  const first = before ? getNextSibling(before) : getFirstChild(domParent);
24557
24588
  const last = after ? (STAGED_DOM?.view(after) ?? after).previousSibling : (STAGED_DOM?.view(domParent) ?? domParent).lastChild;
24558
- if (last !== null && first === last && first.nodeType === 1) {
24589
+ if (last !== null && first === last && first.nodeType === 1 && markerlessBranchRoots(block, domParent, after) <= 1) {
24559
24590
  block.startMarker = first;
24560
24591
  block.endMarker = first;
24561
24592
  state.end = first;
@@ -24569,6 +24600,7 @@ function finalizeMarkerlessBranch(state, domParent, block, marker, before, after
24569
24600
  block.exclusiveMarkers = true;
24570
24601
  state.start = start;
24571
24602
  state.end = end;
24603
+ markerlessBranchRoots(block, domParent, after, end);
24572
24604
  }
24573
24605
  }
24574
24606
  function replaceSharedBlockBoundary(parent, oldStart, oldEnd, newStart, newEnd) {
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  version: () => version
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const version = "0.4.0";
24
+ const version = "0.4.1";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  version
@@ -14366,6 +14366,7 @@ function ssrEmitActivity(node, ctx, name, inlinedSubs, parentNs, cssHash, compon
14366
14366
 
14367
14367
  function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs) {
14368
14368
  const bindingSite = node._octaneBindingSite;
14369
+ const itemBody = forItemTemplateBody(node, ctx);
14369
14370
  // rewriteHookCalls: key any `use(thenable)` in the @for iterable expression.
14370
14371
  const itemsExpr = rewriteHookCalls(node.right, ctx, name);
14371
14372
  const mapCall = node.nativeArrayMap || null;
@@ -14373,7 +14374,7 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
14373
14374
  const params = [itemId];
14374
14375
  if (node.index) params.push(node.index);
14375
14376
  const itemSub = ssrCompileSub(
14376
- node.body.body,
14377
+ itemBody,
14377
14378
  ctx,
14378
14379
  '__sitem',
14379
14380
  params,
@@ -14461,8 +14462,8 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
14461
14462
  // attribute serialization stay on the allocation-free path, matching the
14462
14463
  // client forBlock's existing PURE-body proof.
14463
14464
  const itemNeedsIdentity =
14464
- containsComponentCallOrControlFlow(node.body.body) ||
14465
- containsRenderCall(node.body.body) ||
14465
+ containsComponentCallOrControlFlow(itemBody) ||
14466
+ containsRenderCall(itemBody) ||
14466
14467
  ssrAstCallsAny(itemSub.fn, ['_$ssrChild', '_$ssrComponent']);
14467
14468
  const itemCall = ssrSubCall(
14468
14469
  itemSub.fnName,
@@ -25141,6 +25142,7 @@ function planJsx(
25141
25142
  b.deferred =
25142
25143
  DEFERRABLE_MOUNT_KINDS.has(b.kind) &&
25143
25144
  !sharesSpreadHost &&
25145
+ !b.beforeConnect &&
25144
25146
  b.name !== 'dangerouslySetInnerHTML';
25145
25147
  }
25146
25148
 
@@ -25213,7 +25215,9 @@ function planJsx(
25213
25215
  // these text mounts and flush them AFTER every walk has been emitted, so all navigation
25214
25216
  // happens on the intact template. (Regression: StoryRow's `.meta` interleaves text holes
25215
25217
  // with `<Link>` components.)
25216
- const deferredTextMounts = [];
25218
+ // Connection-sensitive attributes also wait for every mount flag (including
25219
+ // suppressHydrationWarning) before writing, but still run before insertion.
25220
+ const postWalkMounts = [];
25217
25221
  let nativeStyleSlots = 0;
25218
25222
  // Emit per-binding mount code.
25219
25223
  for (const b of elementBindings) {
@@ -25329,7 +25333,7 @@ function planJsx(
25329
25333
  // `htextSwap` (sibling text hole) detaches its `<!>`; defer it past all walks (see above).
25330
25334
  const mountEmit = emitBindingMount(b, elVar, bag);
25331
25335
  if (mountEmit !== null) {
25332
- const target = b.kind === 'text' ? deferredTextMounts : mountLines;
25336
+ const target = b.kind === 'text' || b.beforeConnect ? postWalkMounts : mountLines;
25333
25337
  if (Array.isArray(mountEmit)) target.push(...mountEmit);
25334
25338
  else target.push(mountEmit);
25335
25339
  }
@@ -25395,9 +25399,9 @@ function planJsx(
25395
25399
  anchorKey: 'portalAnchor',
25396
25400
  stampLoc: false,
25397
25401
  });
25398
- // Flush the deferred sibling-text-hole mounts now that every element walk is emitted —
25399
- // `htextSwap` can safely detach its `<!>` placeholder without breaking later navigation.
25400
- for (const line of deferredTextMounts) mountLines.push(line);
25402
+ // Every element walk and mount flag is ready: text holes can replace their
25403
+ // placeholders and iframe sources can initialize before the root is inserted.
25404
+ for (const line of postWalkMounts) mountLines.push(line);
25401
25405
 
25402
25406
  if (!noTemplate) {
25403
25407
  // Allocate + insert + commit in ONE shared-factory call, LAST — see the
@@ -29349,6 +29353,9 @@ function emitElementHtml(
29349
29353
  id: bindings.length,
29350
29354
  kind: 'attr',
29351
29355
  attributeHelper: writer?.helper,
29356
+ // Connecting an iframe without its source creates an initial
29357
+ // document before the intended navigation and sandbox checks.
29358
+ beforeConnect: tag === 'iframe' && attrName.toLowerCase() === 'src',
29352
29359
  name:
29353
29360
  ctx.dev && (rawAttrName === 'tabIndex' || rawAttrName === 'htmlFor')
29354
29361
  ? rawAttrName
@@ -31580,6 +31587,79 @@ function keyedSelectionDepIndex(itemName, keyBody, subStmts, runtimeDepNames, ct
31580
31587
  return runtimeDepNames.indexOf(selected.name);
31581
31588
  }
31582
31589
 
31590
+ // The first root's legacy key already owns the entire row through keyFn.
31591
+ // Avoid a second descriptor boundary for that same key. Only consume a host
31592
+ // key when the row cannot change a collected key before a later child reads it.
31593
+ // Opaque rendering, spreads and duplicate keys retain the existing boundary.
31594
+ // Component keys and nested host keys retain their own reconciliation ranges.
31595
+ function forItemTemplateBody(node, ctx) {
31596
+ const body = node.body.body;
31597
+ if (ctx._universalRuntimeUnit != null || body.length !== 1) return body;
31598
+ const root = body[0];
31599
+ if (!isPlainHostRoot(root) || isActivityLongForm(root, ctx) || isFragmentLongForm(root, ctx))
31600
+ return body;
31601
+ const attrs = root.attributes || root.openingElement?.attributes || [];
31602
+ let keyIndex = -1;
31603
+ for (let i = 0; i < attrs.length; i++) {
31604
+ const attr = attrs[i];
31605
+ if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') return body;
31606
+ if (jsxAttrRawName(attr) !== 'key') continue;
31607
+ if (keyIndex !== -1 || attr.value == null) return body;
31608
+ keyIndex = i;
31609
+ }
31610
+ if (keyIndex === -1) return body;
31611
+ const value = attrs[keyIndex].value;
31612
+ const key = value.type === 'JSXExpressionContainer' ? value.expression : value;
31613
+ if (!isDeferralSafeBundleArg(key)) return body;
31614
+ // Destructuring must not invoke defaults, computed reads or an iterator.
31615
+ if (!isAutoMemoPropsParam(node.left.declarations[0].id)) return body;
31616
+ function stableHost(host) {
31617
+ if (!isPlainHostRoot(host) || isActivityLongForm(host, ctx) || isFragmentLongForm(host, ctx))
31618
+ return false;
31619
+ const tag = host.id?.name ?? host.openingElement?.name?.name;
31620
+ if (tag.includes('-') || tag === 'script') return false;
31621
+ const attributes = host.attributes || host.openingElement?.attributes || [];
31622
+ if (
31623
+ attributes.some(
31624
+ (attr) =>
31625
+ attr.type === 'SpreadAttribute' ||
31626
+ attr.type === 'JSXSpreadAttribute' ||
31627
+ jsxAttrRawName(attr) === 'is' ||
31628
+ jsxAttrRawName(attr) === 'children',
31629
+ )
31630
+ )
31631
+ return false;
31632
+ // Inspect values so deferred event/ref callbacks remain eligible.
31633
+ const values = attributes.map((attr) => attr.value);
31634
+ if (containsRenderCall(values) || containsAutoMemoUnsafeStructure(values)) return false;
31635
+ for (const child of host.children || []) {
31636
+ if (child.type === 'JSXText') continue;
31637
+ if (child.type === 'Text' || child.type === 'JSXExpressionContainer') {
31638
+ const expr = child.expression;
31639
+ if (!expr || expr.type === 'JSXEmptyExpression') continue;
31640
+ if (
31641
+ !isKnownTextChildExpression(expr) ||
31642
+ containsRenderCall([expr]) ||
31643
+ containsAutoMemoUnsafeStructure([expr])
31644
+ )
31645
+ return false;
31646
+ } else if (!stableHost(child)) return false;
31647
+ }
31648
+ return true;
31649
+ }
31650
+ if (!stableHost(root)) return body;
31651
+ const kept = attrs.filter((_, index) => index !== keyIndex);
31652
+ return [
31653
+ root.openingElement
31654
+ ? {
31655
+ ...root,
31656
+ ...(root.attributes === undefined ? {} : { attributes: kept }),
31657
+ openingElement: { ...root.openingElement, attributes: kept },
31658
+ }
31659
+ : { ...root, attributes: kept },
31660
+ ];
31661
+ }
31662
+
31583
31663
  function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
31584
31664
  // `@for await (...)` (async iteration) has no meaning for the runtime's
31585
31665
  // synchronous keyed reconciler. The TSRX parser currently rejects the surface
@@ -31613,7 +31693,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
31613
31693
  // we synthesize a fresh name and emit the destructuring inside the body so
31614
31694
  // the keyFn still gets the whole item and the body still sees the fields.
31615
31695
  const itemName = isDestructured ? '_item' : leftDeclId.name;
31616
- const subStmts = node.body.body;
31696
+ const subStmts = forItemTemplateBody(node, ctx);
31617
31697
 
31618
31698
  // Key resolution priority (matches @tsrx/core's build_hoisted_for_of_with_hooks):
31619
31699
  // 1. `key={…}` attribute on the first Element child (legacy / explicit).
@@ -31638,7 +31718,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
31638
31718
  // body for the first Element and pull its `key=` attr if any. Accept both
31639
31719
  // the old `Element` IR and the raw new `JSXElement` shape that's reached
31640
31720
  // here when the body wasn't routed through normalizeChildren.
31641
- const firstEl = subStmts.find((n) => n.type === 'Element' || n.type === 'JSXElement');
31721
+ const firstEl = node.body.body.find((n) => n.type === 'Element' || n.type === 'JSXElement');
31642
31722
  if (firstEl) {
31643
31723
  const keyAttr = (firstEl.attributes || firstEl.openingElement?.attributes || []).find(
31644
31724
  (a) => (a.name?.name || a.name) === 'key',
@@ -31848,18 +31928,17 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
31848
31928
  const regionStmts = emptyStmts ? [...subStmts, ...emptyStmts] : subStmts;
31849
31929
  const regionAst = b.block(regionStmts);
31850
31930
  const regionFree = collectFreeIdentifiers(regionAst, bodyScope);
31851
- if (node.key) {
31852
- for (const name of collectFreeIdentifiers(node.key, bodyScope)) regionFree.add(name);
31853
- }
31931
+ for (const name of collectFreeIdentifiers(keyBody, bodyScope)) regionFree.add(name);
31854
31932
  let listSafe =
31855
31933
  ctx.autoMemo === true &&
31856
31934
  !hasPerItemEventClosure &&
31857
31935
  isAutoMemoCalculationDependency(node.right) &&
31858
31936
  !hasHook &&
31859
31937
  !containsRenderCall(regionStmts, ctx) &&
31860
- !containsRenderCall(node.key ? [node.key] : [], ctx) &&
31938
+ !containsRenderCall([keyBody], ctx) &&
31861
31939
  !containsAutoMemoUnsafeStructure(regionStmts, ctx) &&
31862
- !containsAutoMemoUnsafeStructure(node.key ? [node.key] : [], ctx) &&
31940
+ !containsAutoMemoUnsafeStructure([keyBody], ctx) &&
31941
+ !containsImportedMemberRead(keyBody, ctx.importedNames) &&
31863
31942
  !containsImportedMemberRead(regionAst, ctx.importedNames);
31864
31943
  const listDeps = new Set();
31865
31944
  const witnesses = collectImportedComponentReferences(regionAst, ctx.importedNames);
@@ -122,6 +122,21 @@ for (const name of [
122
122
  ]) {
123
123
  KNOWN_SVG_PROPERTY_SPELLINGS.set(name.toLowerCase(), name);
124
124
  }
125
+ const NON_NATIVE_LOWERCASE_PROPERTIES = /* @__PURE__ */ new Set([
126
+ "autoFocus",
127
+ "defaultValue",
128
+ "defaultChecked",
129
+ "dangerouslySetInnerHTML",
130
+ "className",
131
+ "suppressContentEditableWarning",
132
+ "suppressHydrationWarning",
133
+ "suppressNativeChangeWarning",
134
+ "__octaneNativeChangeDiagnostic",
135
+ "acceptCharset",
136
+ "htmlFor",
137
+ "httpEquiv",
138
+ "viewBox"
139
+ ]);
125
140
  function hostPropertyWarning(name, value, tag, isSvg = false) {
126
141
  const lower = name.toLowerCase();
127
142
  if (name === "for" && tag === "label") return null;
@@ -137,6 +152,9 @@ function hostPropertyWarning(name, value, tag, isSvg = false) {
137
152
  if (name.startsWith("aria-") || /^aria[A-Z]/.test(name) || name.startsWith("data-")) return null;
138
153
  const known = KNOWN_PROPERTY_SPELLINGS.get(lower) ?? (isSvg ? KNOWN_SVG_PROPERTY_SPELLINGS.get(lower) : void 0);
139
154
  if (known !== void 0) {
155
+ if (!isSvg && name === lower && // Hyphenated/namespaced presentation aliases belong to SVG, not HTML.
156
+ !name.includes("-") && !name.includes(":") && !NON_NATIVE_LOWERCASE_PROPERTIES.has(known) && (ATTRIBUTE_ALIASES.get(known) ?? lower) === lower)
157
+ return null;
140
158
  if (isSvg && ATTRIBUTE_ALIASES.get(known) === name) return null;
141
159
  return name !== known ? `Invalid DOM property \`${name}\`. Did you mean \`${known}\`?` : null;
142
160
  }
package/dist/runtime.js CHANGED
@@ -24412,12 +24412,43 @@ function preservesCommittedSuspense(block) {
24412
24412
  }
24413
24413
  return false;
24414
24414
  }
24415
+ function markerlessBranchRoots(scope, domParent, after, end = null) {
24416
+ let roots = 0;
24417
+ const slots = scope._slots;
24418
+ if (slots !== null) {
24419
+ for (let i = 0; i < slots.length; i++) {
24420
+ const slot = slots[i];
24421
+ if (slot.anchor === after && (slot.__kind === "ifBlockSlot" || slot.__kind === "switchBlockSlot" || slot.__kind === "componentSlotSlot" && slot.block?.parentNode === domParent)) {
24422
+ roots++;
24423
+ if (end !== null) {
24424
+ journalRootProperty(slot, "anchor", slot.anchor);
24425
+ slot.anchor = end;
24426
+ }
24427
+ }
24428
+ }
24429
+ }
24430
+ const children = scope.children;
24431
+ if (children !== null) {
24432
+ for (let i = 0; i < children.length; i++) {
24433
+ const child2 = children[i].scope;
24434
+ const block = child2.block;
24435
+ if (block !== scope.block && block.parentNode === domParent && block.endMarker === after) {
24436
+ roots += markerlessBranchRoots(child2, domParent, after, end) || 1;
24437
+ if (end !== null) {
24438
+ journalRootProperty(block, "endMarker", block.endMarker);
24439
+ block.endMarker = end;
24440
+ }
24441
+ }
24442
+ }
24443
+ }
24444
+ return roots;
24445
+ }
24415
24446
  function finalizeMarkerlessBranch(state, domParent, block, marker, before, after) {
24416
24447
  state.markerlessBefore = void 0;
24417
24448
  if (state.borrowed && state.start === null) return;
24418
24449
  const first = before ? getNextSibling(before) : getFirstChild(domParent);
24419
24450
  const last = after ? (STAGED_DOM?.view(after) ?? after).previousSibling : (STAGED_DOM?.view(domParent) ?? domParent).lastChild;
24420
- if (last !== null && first === last && first.nodeType === 1) {
24451
+ if (last !== null && first === last && first.nodeType === 1 && markerlessBranchRoots(block, domParent, after) <= 1) {
24421
24452
  block.startMarker = first;
24422
24453
  block.endMarker = first;
24423
24454
  state.end = first;
@@ -24431,6 +24462,7 @@ function finalizeMarkerlessBranch(state, domParent, block, marker, before, after
24431
24462
  block.exclusiveMarkers = true;
24432
24463
  state.start = start;
24433
24464
  state.end = end;
24465
+ markerlessBranchRoots(block, domParent, after, end);
24434
24466
  }
24435
24467
  }
24436
24468
  function replaceSharedBlockBoundary(parent, oldStart, oldEnd, newStart, newEnd) {
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- const version = "0.4.0";
1
+ const version = "0.4.1";
2
2
  export {
3
3
  version
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octane",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": [