octane 0.2.13 → 0.2.15

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.
Files changed (36) hide show
  1. package/dist/cjs/constants.cjs +7 -7
  2. package/dist/cjs/dom-binding-claims.cjs +28 -0
  3. package/dist/cjs/dom-binding-handoff.cjs +2 -2
  4. package/dist/cjs/dom-bindings.cjs +105 -13
  5. package/dist/cjs/runtime.cjs +311 -55
  6. package/dist/cjs/server/streamed-signals.cjs +3 -2
  7. package/dist/cjs/signals/control-binding.cjs +33 -9
  8. package/dist/cjs/signals/control-handoff.cjs +39 -0
  9. package/dist/cjs/stream-protocol.cjs +6 -0
  10. package/dist/cjs/version.cjs +1 -1
  11. package/dist/compiler/compile.js +86 -24
  12. package/dist/compiler/dom-binding-program.js +30 -3
  13. package/dist/compiler/dom-bindings.js +115 -34
  14. package/dist/compiler/volar.js +58 -17
  15. package/dist/constants.d.ts +2 -2
  16. package/dist/constants.js +2 -2
  17. package/dist/dom-binding-claims.d.ts +1 -0
  18. package/dist/dom-binding-claims.js +4 -0
  19. package/dist/dom-binding-handoff.d.ts +6 -4
  20. package/dist/dom-binding-handoff.js +1 -1
  21. package/dist/dom-binding-program.d.ts +2 -1
  22. package/dist/dom-binding-program.js +16 -1
  23. package/dist/dom-bindings.d.ts +5 -2
  24. package/dist/dom-bindings.js +105 -13
  25. package/dist/runtime.d.ts +34 -3
  26. package/dist/runtime.js +314 -55
  27. package/dist/server/streamed-signals.js +3 -2
  28. package/dist/signals/control-binding.d.ts +9 -5
  29. package/dist/signals/control-binding.js +38 -6
  30. package/dist/signals/control-handoff.d.ts +26 -0
  31. package/dist/signals/control-handoff.js +12 -0
  32. package/dist/signals/index.d.ts +1 -0
  33. package/dist/stream-protocol.d.ts +4 -0
  34. package/dist/stream-protocol.js +4 -0
  35. package/dist/version.js +1 -1
  36. package/package.json +1 -1
@@ -248,7 +248,7 @@ function importedProjectionCall(node, imports, lexical, parameterScope) {
248
248
  const callee = unwrap(node?.callee);
249
249
  let importedRoot = callee;
250
250
  while (importedRoot?.type === 'MemberExpression' && !importedRoot.computed)
251
- importedRoot = importedRoot.object;
251
+ importedRoot = unwrap(importedRoot.object);
252
252
  const namespace =
253
253
  importedRoot !== callee &&
254
254
  importedRoot?.type === 'Identifier' &&
@@ -695,6 +695,8 @@ function planView(fn, filename, source, imports, lexical, native = null) {
695
695
  const signalIndices = [];
696
696
  const styleIndices = [];
697
697
  const projectionGroups = [];
698
+ const providerBindings = new Set();
699
+ const unboundAttributes = new Set();
698
700
  const classAttributes = new Map();
699
701
  const id = `d:${strongHash(`octane:dom-bindings:2\0${filename}\0${fn.id.name}\0${source}`)}`;
700
702
  const parameterScope = lexical.nodeScopes.get(fn.body) ?? lexical.rootScope;
@@ -702,14 +704,15 @@ function planView(fn, filename, source, imports, lexical, native = null) {
702
704
  let addressed = false;
703
705
  const markUnbound = (expression, opaque = false) => {
704
706
  const value = unwrap(expression);
707
+ const callee = unwrap(value?.callee);
705
708
  const external =
706
- value?.type === 'CallExpression' && value.callee.type === 'Identifier'
707
- ? imports.get(value.callee.name)
709
+ value?.type === 'CallExpression' && callee?.type === 'Identifier'
710
+ ? imports.get(callee.name)
708
711
  : null;
709
712
  if (
710
713
  external?.source !== 'octane/behavior' ||
711
714
  external.imported !== 'unbound' ||
712
- lexical.resolveBinding(lexical.nodeScopes.get(value.callee), value.callee.name)?.scope !==
715
+ lexical.resolveBinding(lexical.nodeScopes.get(callee), callee.name)?.scope !==
713
716
  lexical.rootScope
714
717
  )
715
718
  return false;
@@ -898,6 +901,18 @@ function planView(fn, filename, source, imports, lexical, native = null) {
898
901
  const expression = child.type === 'JSXExpressionContainer' ? child.expression : null;
899
902
  return expression === null || !markUnbound(expression, true);
900
903
  });
904
+ // A scalar leaf has one stable Text node (or no node for empty SSR).
905
+ // Structural or opaque children retain their existing compiler contract.
906
+ const child = children.length === 1 ? children[0] : null;
907
+ const expression = child?.type === 'JSXExpressionContainer' ? child.expression : null;
908
+ const text =
909
+ tag !== 'textarea' &&
910
+ authoredChildren.length === children.length &&
911
+ (child?.type === 'JSXText' ||
912
+ (expression?.type === 'Literal' && typeof expression.value === 'string') ||
913
+ (expression?.type === 'TSAsExpression' &&
914
+ (expression.typeAnnotation.type === 'TSStringKeyword' ||
915
+ expression.typeAnnotation.type === 'TSNumberKeyword')));
901
916
  const opaqueChildren = authoredChildren.length !== children.length;
902
917
  const openChildren = tag !== 'textarea' && opaqueChildren && children.length > 0;
903
918
  if (tag === 'textarea' && children.some((child) => child.type !== 'JSXText'))
@@ -910,15 +925,17 @@ function planView(fn, filename, source, imports, lexical, native = null) {
910
925
  parent,
911
926
  tag,
912
927
  ns,
913
- (opaqueChildren && !openChildren) || tag === 'textarea' ? null : children.length,
914
- ...(openChildren ? [true] : []),
928
+ (opaqueChildren && !openChildren) || tag === 'textarea' ? null : text ? 0 : children.length,
929
+ ...(text ? [null, 1] : openChildren ? [true] : []),
915
930
  ]);
916
931
  elements.push(element);
917
932
  const owned = new Set();
918
933
  const externalNames = new Set();
919
934
  const knownFields = new Set();
920
935
  for (const attr of element.openingElement?.attributes ?? element.attributes ?? []) {
921
- if (attr._octaneKnownAttributeSpread) {
936
+ // A provider proves normal-renderer fields, not early ownership. External
937
+ // spreads retain the ordinary unbound checks and are stripped below.
938
+ if (attr._octaneKnownAttributeSpread && !attr._octaneKnownAttributeSpread.unbound) {
922
939
  const argument = attr.argument ?? attr.value.expression;
923
940
  // The compiler verified the exact imported factory against its
924
941
  // provider contract. Its arguments still need the ordinary proof.
@@ -956,6 +973,7 @@ function planView(fn, filename, source, imports, lexical, native = null) {
956
973
  if (!group) signalIndices.push(bindings.length);
957
974
  bindings.push([index, bindingKind(tag, name), name]);
958
975
  }
976
+ providerBindings.add(bindings.length - 1);
959
977
  values.push(
960
978
  group
961
979
  ? inheritHookMemoOrigin(
@@ -979,26 +997,31 @@ function planView(fn, filename, source, imports, lexical, native = null) {
979
997
  error(filename, attr, 'binding attribute spreads must be explicitly unbound');
980
998
  if (bindings.some((binding) => binding[0] === index))
981
999
  error(filename, attr, 'unbound attribute spreads must precede owned binding attributes');
1000
+ const addExternalName = (raw, node) => {
1001
+ const name = (
1002
+ raw === 'className' ? 'class' : (ATTRIBUTE_ALIASES.get(raw) ?? raw)
1003
+ ).toLowerCase();
1004
+ if (
1005
+ FORBIDDEN_ATTRS.has(name) ||
1006
+ name.startsWith('on') ||
1007
+ name.startsWith('data-octane-class-')
1008
+ )
1009
+ error(
1010
+ filename,
1011
+ node,
1012
+ `unbound spreads cannot supply reserved or structural attribute ${JSON.stringify(raw)}`,
1013
+ );
1014
+ externalNames.add(name);
1015
+ };
982
1016
  const external = unwrap(unwrap(attr.argument).arguments[0]);
983
- if (external?.type === 'ObjectExpression') {
1017
+ if (attr._octaneKnownAttributeSpread) {
1018
+ for (const raw of attr._octaneKnownAttributeSpread.fields) addExternalName(raw, attr);
1019
+ } else if (external?.type === 'ObjectExpression') {
984
1020
  for (const property of external.properties) {
985
1021
  if (property.type !== 'Property' || property.computed) continue;
986
1022
  const raw = property.key.name ?? property.key.value;
987
1023
  if (typeof raw !== 'string') continue;
988
- const name = (
989
- raw === 'className' ? 'class' : (ATTRIBUTE_ALIASES.get(raw) ?? raw)
990
- ).toLowerCase();
991
- if (
992
- FORBIDDEN_ATTRS.has(name) ||
993
- name.startsWith('on') ||
994
- name.startsWith('data-octane-class-')
995
- )
996
- error(
997
- filename,
998
- property,
999
- `unbound spreads cannot supply reserved or structural attribute ${JSON.stringify(raw)}`,
1000
- );
1001
- externalNames.add(name);
1024
+ addExternalName(raw, property);
1002
1025
  }
1003
1026
  }
1004
1027
  continue;
@@ -1022,6 +1045,16 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1022
1045
  nodes[index][3] = null;
1023
1046
  continue;
1024
1047
  }
1048
+ const value =
1049
+ attr.value === null
1050
+ ? b.literal(true)
1051
+ : unwrap(
1052
+ attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value,
1053
+ );
1054
+ if ((lower === 'ref' || /^on[A-Z]/.test(raw)) && markUnbound(value)) {
1055
+ unboundAttributes.add(lower);
1056
+ continue;
1057
+ }
1025
1058
  if (
1026
1059
  !/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(name) ||
1027
1060
  lower.startsWith('on') ||
@@ -1030,13 +1063,10 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1030
1063
  ) {
1031
1064
  error(filename, attr, `attribute ${JSON.stringify(raw)} is not supported in binding views`);
1032
1065
  }
1033
- const value =
1034
- attr.value === null
1035
- ? b.literal(true)
1036
- : unwrap(
1037
- attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value,
1038
- );
1039
- if (markUnbound(value)) continue;
1066
+ if (markUnbound(value)) {
1067
+ unboundAttributes.add(lower);
1068
+ continue;
1069
+ }
1040
1070
  if (value?.type !== 'Literal' && externalNames.has(lower))
1041
1071
  error(
1042
1072
  filename,
@@ -1139,11 +1169,35 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1139
1169
  add(index, kind, name, value, attr);
1140
1170
  }
1141
1171
  }
1142
- if (tag !== 'textarea') {
1172
+ if (text) {
1173
+ if (expression) add(index, 'text', '#text', expression, child);
1174
+ } else if (tag !== 'textarea') {
1143
1175
  for (const child of children) visit(child, index, ns, [...ancestors, tag]);
1144
1176
  }
1145
1177
  };
1146
1178
  visit(render, -1, native?.namespace ?? 0, native?.ancestors ?? []);
1179
+ // Opaque descendants are outside this receipt. Only a scalar native host's
1180
+ // declared presentation channels can transfer independently of those children.
1181
+ const hostHandoff =
1182
+ nodes.length === 1 &&
1183
+ nodes[0][3] === null &&
1184
+ !addressed &&
1185
+ !['textarea', 'input', 'select'].includes(nodes[0][1]) &&
1186
+ bindings.length > 0 &&
1187
+ bindings.every(
1188
+ (binding, index) =>
1189
+ binding[0] === 0 &&
1190
+ (['class', 'styleProperty', 'styleAttribute', 'styleObject'].includes(binding[1]) ||
1191
+ providerBindings.has(index)),
1192
+ ) &&
1193
+ [...(render.openingElement?.attributes ?? render.attributes ?? [])].every(
1194
+ (attr) =>
1195
+ !['JSXSpreadAttribute', 'SpreadAttribute'].includes(attr.type) ||
1196
+ (attr._octaneKnownAttributeSpread && !attr._octaneKnownAttributeSpread.unbound),
1197
+ ) &&
1198
+ !(render.openingElement?.attributes ?? render.attributes ?? []).some(
1199
+ (attr) => attrName(attr)?.toLowerCase() === 'dangerouslysetinnerhtml',
1200
+ );
1147
1201
  return {
1148
1202
  fn,
1149
1203
  render,
@@ -1159,6 +1213,8 @@ function planView(fn, filename, source, imports, lexical, native = null) {
1159
1213
  unbound,
1160
1214
  classAttributes,
1161
1215
  id,
1216
+ hostHandoff,
1217
+ unboundAttributes,
1162
1218
  };
1163
1219
  }
1164
1220
 
@@ -1181,6 +1237,7 @@ function scalarProperties(
1181
1237
  b.prop('init', b.id('nodes'), literalData(plan.nodes)),
1182
1238
  b.prop('init', b.id('bindings'), literalData(plan.bindings)),
1183
1239
  b.prop('init', b.id('project'), project),
1240
+ ...(plan.hostHandoff ? [b.prop('init', b.id('handoff'), b.literal('host'))] : []),
1184
1241
  ...(plan.signalIndices.length
1185
1242
  ? [
1186
1243
  b.prop('init', b.id('signalIndices'), literalData(plan.signalIndices)),
@@ -1221,6 +1278,8 @@ function projectionDependencies(plan, lexical) {
1221
1278
  }
1222
1279
  });
1223
1280
  collect(plan.expressions ?? plan.values);
1281
+ collect(plan.scalar?.values);
1282
+ collect(plan.scalar?.projections?.map((declaration) => declaration.declarations[0].init));
1224
1283
  collect(plan.projections?.map((declaration) => declaration.declarations[0].init));
1225
1284
  return needed;
1226
1285
  }
@@ -1435,7 +1494,15 @@ function projectProgram(ast, plan, filename, lexical) {
1435
1494
  b.object(
1436
1495
  scalarProperties(
1437
1496
  plan.scalar,
1438
- b.arrow(plan.fn.params, b.call(b.member(root, 'project'), b.array(plan.fn.params))),
1497
+ inheritHookMemoOrigin(
1498
+ b.arrow(
1499
+ plan.fn.params,
1500
+ plan.scalar.projections.length > 0
1501
+ ? b.block([...plan.scalar.projections, b.return(b.array(plan.scalar.values))])
1502
+ : b.array(plan.scalar.values),
1503
+ ),
1504
+ plan.fn,
1505
+ ),
1439
1506
  plan.scalar.bindings.some((binding) => binding[1] === 'classGroup')
1440
1507
  ? b.member(root, 'createClassGroup')
1441
1508
  : null,
@@ -1876,12 +1943,13 @@ export function prepareDomBindings(ast, source, filename, selectedExport, helper
1876
1943
  : new Map();
1877
1944
  const isUnbound = (expression) => {
1878
1945
  const value = unwrap(expression);
1879
- if (value?.type !== 'CallExpression' || value.callee.type !== 'Identifier') return false;
1880
- const imported = imports.get(value.callee.name);
1946
+ const callee = unwrap(value?.callee);
1947
+ if (value?.type !== 'CallExpression' || callee?.type !== 'Identifier') return false;
1948
+ const imported = imports.get(callee.name);
1881
1949
  return (
1882
1950
  imported?.source === 'octane/behavior' &&
1883
1951
  imported.imported === 'unbound' &&
1884
- lexical.resolveBinding(lexical.nodeScopes.get(value.callee), value.callee.name)?.scope ===
1952
+ lexical.resolveBinding(lexical.nodeScopes.get(callee), callee.name)?.scope ===
1885
1953
  lexical.rootScope
1886
1954
  );
1887
1955
  };
@@ -2132,6 +2200,9 @@ export function prepareDomBindings(ast, source, filename, selectedExport, helper
2132
2200
  _octanePresentationHydration: {
2133
2201
  id: plan.id,
2134
2202
  supported: fixedPresentation || structuralPresentation,
2203
+ ...(field('nodes').elements.some((node) => node.elements[5]?.value === 'value')
2204
+ ? { nativeControl: true }
2205
+ : {}),
2135
2206
  ...(structuralPresentation || conditionalRest ? { structural: true } : {}),
2136
2207
  ...(conditionalRest ? { conditionalRest: true } : {}),
2137
2208
  },
@@ -2207,6 +2278,16 @@ export function prepareDomBindings(ast, source, filename, selectedExport, helper
2207
2278
  );
2208
2279
  }
2209
2280
  for (const [node, replacement] of rewritten) replacements.set(node, replacement);
2281
+ if (plan.hostHandoff)
2282
+ replacements.set(node, {
2283
+ ...mapCow(node, rewritten),
2284
+ _octanePresentationHydration: {
2285
+ id: plan.id,
2286
+ supported: true,
2287
+ host: true,
2288
+ unboundAttributes: plan.unboundAttributes,
2289
+ },
2290
+ });
2210
2291
  });
2211
2292
  if (selectedExport !== null) {
2212
2293
  const plan = plans.get(selectedExport);
@@ -31829,11 +31829,10 @@ var OCTANE_PLATFORM = {
31829
31829
  }
31830
31830
  };
31831
31831
  var octaneTransform = createJsxTransform(OCTANE_PLATFORM);
31832
- function projectNativeAttributeReads(ast, contracts) {
31832
+ function projectNativeDomReads(ast, contracts) {
31833
31833
  const attributes = new Set(
31834
31834
  contracts?.flatMap((contract) => contract.jsxAttribute ? [contract.jsxAttribute] : [])
31835
31835
  );
31836
- if (attributes.size === 0) return ast;
31837
31836
  const names = /* @__PURE__ */ new Set();
31838
31837
  const walk2 = (node, replace) => {
31839
31838
  if (!node || typeof node !== "object") return node;
@@ -31858,20 +31857,54 @@ function projectNativeAttributeReads(ast, contracts) {
31858
31857
  let helper = "__octane_nativeAttributeValue";
31859
31858
  while (names.has(helper)) helper += "_";
31860
31859
  let used = false;
31860
+ const read = (value) => {
31861
+ used = true;
31862
+ return inheritHookMemoOrigin(builders_exports.call(builders_exports.id(helper), value), value);
31863
+ };
31864
+ const projectText = (node) => {
31865
+ if (!node) return node;
31866
+ if (["TSAsExpression", "TSTypeAssertion", "TSSatisfiesExpression"].includes(node.type) && node.typeAnnotation?.type === "TSStringKeyword") {
31867
+ used = true;
31868
+ const expression = projectText(node.expression);
31869
+ return {
31870
+ ...node,
31871
+ expression: expression === node.expression ? builders_exports.call(builders_exports.id(helper), expression) : expression
31872
+ };
31873
+ }
31874
+ if ([
31875
+ "TSAsExpression",
31876
+ "TSTypeAssertion",
31877
+ "TSNonNullExpression",
31878
+ "TSSatisfiesExpression",
31879
+ "TSInstantiationExpression",
31880
+ "ParenthesizedExpression"
31881
+ ].includes(node.type)) {
31882
+ const expression = projectText(node.expression);
31883
+ return expression === node.expression ? node : { ...node, expression };
31884
+ }
31885
+ return node;
31886
+ };
31861
31887
  const projected = walk2(ast, (node) => {
31862
- if (node.type !== "JSXOpeningElement" && node.type !== "Element") return null;
31888
+ if (["JSXElement", "JSXFragment", "Element", "Fragment"].includes(node.type)) {
31889
+ node = {
31890
+ ...node,
31891
+ children: node.children.map((child) => {
31892
+ if (child.type !== "JSXExpressionContainer") return child;
31893
+ const expression = projectText(child.expression);
31894
+ return expression === child.expression ? child : { ...child, expression };
31895
+ })
31896
+ };
31897
+ }
31898
+ if (node.type !== "JSXOpeningElement" && node.type !== "Element") return node;
31863
31899
  const name = node.type === "Element" ? node.id : node.name;
31864
31900
  if (name?.type !== "JSXIdentifier" && name?.type !== "Identifier" || typeof name.name !== "string" || name.name[0] !== name.name[0].toLowerCase())
31865
- return null;
31901
+ return node;
31866
31902
  return {
31867
31903
  ...node,
31868
31904
  attributes: node.attributes.map((attribute) => {
31869
31905
  if (!attributes.has(attribute.name?.name ?? attribute.name) || attribute.value?.type !== "JSXExpressionContainer")
31870
31906
  return attribute;
31871
- const result = lowerNativeAttributeReads(attribute.value.expression, (value) => {
31872
- used = true;
31873
- return inheritHookMemoOrigin(builders_exports.call(builders_exports.id(helper), value), value);
31874
- });
31907
+ const result = lowerNativeAttributeReads(attribute.value.expression, read);
31875
31908
  return { ...attribute, value: { ...attribute.value, expression: result.expression } };
31876
31909
  })
31877
31910
  };
@@ -32099,7 +32132,7 @@ function compileToVolarMappings(source, filename, options) {
32099
32132
  const transform = selectOctaneTransform(ast);
32100
32133
  const transformed = transform(
32101
32134
  projectServerContextCalls(
32102
- renderer.target === "dom" ? projectNativeAttributeReads(transformAst, options?.knownAttributeSpreads) : transformAst,
32135
+ renderer.target === "dom" ? projectNativeDomReads(transformAst, options?.knownAttributeSpreads) : transformAst,
32103
32136
  filename
32104
32137
  ),
32105
32138
  source,
@@ -32251,14 +32284,22 @@ function compileTypesInspection(source, filename, options) {
32251
32284
  const renderer = resolveRendererForFile(rendererConfig, filename ?? "untitled.tsrx");
32252
32285
  const rendererPragma = authoredLeadingPragma(ast, comments) ? null : createRendererTypePragma(renderer, ast);
32253
32286
  const transform = selectOctaneTransform(ast);
32254
- const transformed = transform(projectServerContextCalls(ast, filename), source, filename, {
32255
- collect: true,
32256
- loose: true,
32257
- typeOnly: true,
32258
- inspect: true,
32259
- errors,
32260
- comments: rendererPragma === null ? comments : [rendererPragma, ...comments]
32261
- });
32287
+ const transformed = transform(
32288
+ projectServerContextCalls(
32289
+ renderer.target === "dom" ? projectNativeDomReads(ast) : ast,
32290
+ filename
32291
+ ),
32292
+ source,
32293
+ filename,
32294
+ {
32295
+ collect: true,
32296
+ loose: true,
32297
+ typeOnly: true,
32298
+ inspect: true,
32299
+ errors,
32300
+ comments: rendererPragma === null ? comments : [rendererPragma, ...comments]
32301
+ }
32302
+ );
32262
32303
  markNativeTemplateBodies(ast);
32263
32304
  return {
32264
32305
  code: transformed.code,
@@ -50,7 +50,7 @@ export declare const HYDRATION_TEXT_SEP = " ";
50
50
  * render order) so a hydrating boundary returns synchronously instead of
51
51
  * re-suspending. Shared so server emit and client read stay byte-identical.
52
52
  */
53
- export declare const SUSPENSE_SCRIPT_ATTR = "data-octane-suspense";
53
+ export { SUSPENSE_SCRIPT_ATTR } from './stream-protocol.js';
54
54
  export declare const SUSPENSE_RESOLVED_COMMENT = "oct-suspense:";
55
55
  export declare const SUSPENSE_RESOLVED_SEED_ATTR = "data-octane-suspense-seeds";
56
56
  export declare const SUSPENSE_RESOLVED_NATIVE_ATTR = "data-octane-suspense-signals";
@@ -105,7 +105,7 @@ export declare const STREAM_SEGMENT_ATTR = "data-oct-s";
105
105
  /** Per-boundary seed-JSON script attribute (inside the segment). */
106
106
  export declare const STREAM_SEED_ATTR = "data-oct-seed";
107
107
  /** Renderer-owned executable/data scripts emitted by the streaming protocol. */
108
- export declare const STREAM_SCRIPT_ATTR = "data-octane-stream";
108
+ export { STREAM_SCRIPT_ATTR } from './stream-protocol.js';
109
109
  /** Hidden carrier for Float sheet resources discovered after the shell; the
110
110
  * inline `$OCTRH` call hoists its tags into document.head. */
111
111
  export declare const STREAM_RESOURCE_ATTR = "data-oct-fr";
package/dist/constants.js CHANGED
@@ -8,7 +8,7 @@ const FOR_BLOCK_OPEN_EMPTY = `<!--${HYDRATION_FOR_EMPTY}-->`;
8
8
  const FOR_BLOCK_OPEN_ITEMS = `<!--${HYDRATION_FOR_ITEMS}-->`;
9
9
  const EMPTY_COMMENT = "<!---->";
10
10
  const HYDRATION_TEXT_SEP = " ";
11
- const SUSPENSE_SCRIPT_ATTR = "data-octane-suspense";
11
+ import { SUSPENSE_SCRIPT_ATTR } from "./stream-protocol.js";
12
12
  const SUSPENSE_RESOLVED_COMMENT = "oct-suspense:";
13
13
  const SUSPENSE_RESOLVED_SEED_ATTR = "data-octane-suspense-seeds";
14
14
  const SUSPENSE_RESOLVED_NATIVE_ATTR = "data-octane-suspense-signals";
@@ -37,7 +37,7 @@ import {
37
37
  import { STREAM_BOUNDARY_ATTR } from "./stream-protocol.js";
38
38
  const STREAM_SEGMENT_ATTR = "data-oct-s";
39
39
  const STREAM_SEED_ATTR = "data-oct-seed";
40
- const STREAM_SCRIPT_ATTR = "data-octane-stream";
40
+ import { STREAM_SCRIPT_ATTR } from "./stream-protocol.js";
41
41
  const STREAM_RESOURCE_ATTR = "data-oct-fr";
42
42
  const STREAM_SEED_COMMENT = "oct-seed:";
43
43
  import {
@@ -0,0 +1 @@
1
+ export declare const domBindingClaims: WeakMap<Element, Map<string, string | null | undefined>>;
@@ -0,0 +1,4 @@
1
+ const domBindingClaims = /* @__PURE__ */ new WeakMap();
2
+ export {
3
+ domBindingClaims
4
+ };
@@ -1,5 +1,5 @@
1
- /** Renderer-independent capability carried by an adopted, compiler-proven view. */
2
- export declare const BINDING_HANDOFF: unique symbol;
1
+ import { BINDING_HANDOFF } from './signals/control-handoff.js';
2
+ export { BINDING_HANDOFF };
3
3
  /** Current early-owned range, not a license to render a different shape. */
4
4
  export interface BindingHandoffRange {
5
5
  readonly kind: 'if' | 'view' | 'slot' | 'text';
@@ -24,13 +24,15 @@ export interface BindingHandoff {
24
24
  readonly root: Node;
25
25
  readonly anchor: Node;
26
26
  readonly end?: Node;
27
- /** Allocated only for a claimed structural presentation. -1 means publication is in progress. */
27
+ /** A scalar host's declared native channels; descendants belong to other owners. */
28
+ readonly host?: ReadonlySet<string>;
29
+ /** Revisioned presentations return -1 while publication is in progress. */
28
30
  revision?(): number;
29
31
  ranges?(): ReadonlyMap<Node, BindingHandoffRange>;
30
32
  view?(root: Node): BindingHandoffView | undefined;
31
33
  rest?(element: Element, site: number): BindingHandoffRest | undefined;
32
34
  valid?(): boolean;
33
- /** One deferred retry; only structural leases allocate publication listeners. */
35
+ /** One deferred retry; listener storage is allocated only when requested. */
34
36
  afterPublication?(callback: () => void): () => void;
35
37
  active(): boolean;
36
38
  retire(publish?: () => void): void;
@@ -1,4 +1,4 @@
1
- const BINDING_HANDOFF = /* @__PURE__ */ Symbol.for("octane.binding-handoff");
1
+ import { BINDING_HANDOFF } from "./signals/control-handoff.js";
2
2
  const events = /* @__PURE__ */ new WeakMap();
3
3
  const delivered = /* @__PURE__ */ new WeakMap();
4
4
  function registerBindingEvent(node, type) {
@@ -15,7 +15,8 @@ export type BindingProgramNode = readonly [
15
15
  kind: 'element',
16
16
  tag: string,
17
17
  namespace: 0 | 1,
18
- children: number | null
18
+ children: number | null,
19
+ nativeControlContent?: 'value'
19
20
  ] | readonly [parent: number, kind: 'text', text: string] | readonly [parent: number, kind: 'region', site: string];
20
21
  export interface BindingRange {
21
22
  readonly start: Comment;
@@ -1250,15 +1250,30 @@ function bindProgram(root, descriptor, source, options, mount, list = descriptor
1250
1250
  dispose,
1251
1251
  () => !busy
1252
1252
  );
1253
- if (mount || descriptor.root.handoff !== true || !instance || descriptor.root.regions.length !== 0 || descriptor.root.bindings.some((binding) => binding[1] === "control") || descriptor.root.nodes.some((node) => node[1] === "element" && node[4] === null))
1253
+ if (mount || descriptor.root.handoff !== true || !instance || descriptor.root.regions.length !== 0 || descriptor.root.bindings.some((binding) => binding[1] === "control") || descriptor.root.nodes.some(
1254
+ (node, index) => node[1] === "element" && node[4] === null && !(node[2] === "textarea" && node[3] === 0 && node[5] === "value" && Array.from(instance.nodes[index].childNodes).every((child) => child.nodeType === 3))
1255
+ ))
1254
1256
  throw new Error(
1255
1257
  "Hydration binding leases require an adopted fixed native view without structural regions or controls."
1256
1258
  );
1259
+ const topology = handoff === void 0 && descriptor.root.nodes.some((node) => node[1] === "element" && node[5] === "value") ? [instance.range.start, ...instance.nodes, instance.range.end].map(
1260
+ (node) => [node, node.parentNode, node.previousSibling, node.nextSibling]
1261
+ ) : void 0;
1257
1262
  return handoff ??= {
1258
1263
  id: descriptor.id,
1259
1264
  root: instance.range.start,
1260
1265
  anchor: instance.nodes.find((node) => node.nodeType === 1) ?? instance.range.start,
1261
1266
  active: () => !transaction.disposed,
1267
+ ...topology === void 0 ? {} : {
1268
+ end: instance.range.end,
1269
+ valid: () => topology.every(
1270
+ ([node, parent, before, after]) => node.parentNode === parent && (node === instance.range.start || node.previousSibling === before) && (node === instance.range.end || node.nextSibling === after)
1271
+ ) && descriptor.root.nodes.every(
1272
+ (proof, index) => proof[1] !== "element" || (proof[4] === null ? Array.from(instance.nodes[index].childNodes).every(
1273
+ (child) => child.nodeType === 3
1274
+ ) : instance.nodes[index].childNodes.length === proof[4])
1275
+ )
1276
+ },
1262
1277
  retire: (publish) => {
1263
1278
  transaction.preservePresentation = true;
1264
1279
  dispose(void 0, publish);
@@ -45,12 +45,13 @@ export type BindingNode = readonly [
45
45
  tag: string,
46
46
  namespace: 0 | 1,
47
47
  children: number | null,
48
- openChildren?: true
48
+ openChildren?: true | null,
49
+ text?: 1
49
50
  ];
50
51
  /** @internal Names and numeric CSS units are resolved by the compiler. */
51
52
  export type BindingOperation = readonly [
52
53
  node: number,
53
- kind: 'attr' | 'boolean' | 'aria' | 'class' | 'classToken' | 'classGroup' | 'styleProperty' | 'styleAttribute' | 'styleObject' | 'control' | 'url',
54
+ kind: 'attr' | 'boolean' | 'aria' | 'class' | 'classToken' | 'classGroup' | 'styleProperty' | 'styleAttribute' | 'styleObject' | 'control' | 'url' | 'text',
54
55
  name: string,
55
56
  unitlessOrGroup?: boolean | number
56
57
  ];
@@ -59,6 +60,8 @@ export type BindingValue = string | null | BindingStyleSnapshot;
59
60
  /** @internal No application function is invoked to discover the DOM topology. */
60
61
  export interface CompiledBindings<Props> {
61
62
  readonly id: string;
63
+ /** Compiler-proven single native host, with no descendant ownership. */
64
+ readonly handoff?: 'host';
62
65
  /** Targets carry compiler-issued addresses when unbound siblings may change. */
63
66
  readonly addressed?: true;
64
67
  readonly nodes: readonly BindingNode[];