octane 0.3.0 → 0.3.2

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 (55) hide show
  1. package/dist/cjs/constants.cjs +18 -25
  2. package/dist/cjs/css.cjs +6 -0
  3. package/dist/cjs/dom-binding-protocol.cjs +37 -14
  4. package/dist/cjs/hydration/control-capture.cjs +12 -4
  5. package/dist/cjs/hydration/event-capture.cjs +6 -7
  6. package/dist/cjs/hydration-markers.cjs +40 -2
  7. package/dist/cjs/index.cjs +2 -0
  8. package/dist/cjs/runtime-tags.cjs +55 -0
  9. package/dist/cjs/runtime.cjs +235 -148
  10. package/dist/cjs/runtime.server.cjs +110 -80
  11. package/dist/cjs/server/streamed-signals.cjs +102 -64
  12. package/dist/cjs/signals/control-binding.cjs +2 -1
  13. package/dist/cjs/stream-protocol.cjs +9 -8
  14. package/dist/cjs/version.cjs +1 -1
  15. package/dist/compiler/compile.js +550 -35
  16. package/dist/compiler/dom-bindings.js +5 -0
  17. package/dist/compiler/hydrate-boundaries.js +118 -0
  18. package/dist/compiler/private-context.js +160 -0
  19. package/dist/constants.d.ts +7 -18
  20. package/dist/constants.js +11 -9
  21. package/dist/css.d.ts +7 -0
  22. package/dist/css.js +4 -0
  23. package/dist/dom-binding-program.js +12 -5
  24. package/dist/dom-binding-protocol.d.ts +14 -1
  25. package/dist/dom-binding-protocol.js +40 -14
  26. package/dist/hydration/control-capture.d.ts +2 -0
  27. package/dist/hydration/control-capture.js +18 -5
  28. package/dist/hydration/event-capture.js +1 -2
  29. package/dist/hydration/independent-island.js +5 -5
  30. package/dist/hydration/stream-delivery.d.ts +1 -1
  31. package/dist/hydration/stream-delivery.js +16 -9
  32. package/dist/hydration/stream-result-receiver.d.ts +1 -0
  33. package/dist/hydration/stream-result-receiver.js +8 -4
  34. package/dist/hydration-markers.d.ts +30 -0
  35. package/dist/hydration-markers.js +27 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/react/index.js +1 -1
  39. package/dist/react/react-compat-shared.js +5 -3
  40. package/dist/react/shared.d.ts +1 -3
  41. package/dist/react/shared.js +1 -1
  42. package/dist/runtime-tags.d.ts +34 -0
  43. package/dist/runtime-tags.js +22 -0
  44. package/dist/runtime.d.ts +9 -14
  45. package/dist/runtime.js +213 -106
  46. package/dist/runtime.server.d.ts +10 -5
  47. package/dist/runtime.server.js +101 -56
  48. package/dist/server/streamed-signals.d.ts +1 -0
  49. package/dist/server/streamed-signals.js +102 -64
  50. package/dist/signals/control-binding.js +4 -2
  51. package/dist/stream-protocol.js +20 -9
  52. package/dist/universal-core.js +8 -4
  53. package/dist/universal-native.js +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +3 -3
@@ -2300,6 +2300,11 @@ export function prepareDomBindings(ast, source, filename, selectedExport, helper
2300
2300
  );
2301
2301
  }
2302
2302
  for (const [node, replacement] of rewritten) replacements.set(node, replacement);
2303
+ if (!plan.hostHandoff)
2304
+ replacements.set(node, {
2305
+ ...mapCow(node, rewritten),
2306
+ _octaneScalarBindingClaims: true,
2307
+ });
2303
2308
  if (plan.hostHandoff)
2304
2309
  replacements.set(node, {
2305
2310
  ...mapCow(node, rewritten),
@@ -14,8 +14,110 @@ import {
14
14
  isIdentifierReference,
15
15
  } from './compile-universal.js';
16
16
 
17
+ import { findPrivateCompiledContextProofs } from './private-context.js';
18
+
17
19
  export const HYDRATE_QUERY_PARAM = 'octane-hydrate';
18
20
 
21
+ // Only this extraction pass can certify the loader/child ABI. Keep provenance
22
+ // off authored attributes and parser metadata; COW rewrites preserve tag nodes.
23
+ const compiledSplitHydrateTags = new WeakMap();
24
+
25
+ export function compiledSplitHydrateTagsForAst(ast) {
26
+ return compiledSplitHydrateTags.get(ast);
27
+ }
28
+
29
+ function isCompiledSplitHydrateBoundary(boundary) {
30
+ if (boundary.disabled || boundary.independent || boundary.permanentStatic) return false;
31
+ const opening = boundary.node.openingElement;
32
+ if (opening.name?.type !== 'JSXIdentifier') return false;
33
+ if (
34
+ (opening.attributes ?? []).some((attribute) => {
35
+ const name = attribute.name?.name;
36
+ return (
37
+ attribute.type !== 'JSXAttribute' ||
38
+ attribute.name?.type !== 'JSXIdentifier' ||
39
+ typeof name !== 'string' ||
40
+ name === 'children' ||
41
+ name === 'fallback' ||
42
+ name.startsWith('__')
43
+ );
44
+ })
45
+ )
46
+ return false;
47
+ return !(boundary.node.children ?? []).some((child) => {
48
+ const expression = unwrapExpression(
49
+ child.type === 'JSXExpressionContainer' ? child.expression : null,
50
+ );
51
+ return ['ArrowFunctionExpression', 'FunctionExpression'].includes(expression?.type);
52
+ });
53
+ }
54
+
55
+ // Generated captures preserve a proven private Context identity, not an authored
56
+ // escape. Certificates belong only to this prepared AST and exact parser tags.
57
+ const privateSplitContexts = new WeakMap();
58
+ export function privateCompiledContextsForHydrateAst(ast) {
59
+ return privateSplitContexts.get(ast);
60
+ }
61
+
62
+ function privateContextCaptureBoundary(boundary) {
63
+ if (boundary.disabled || boundary.independent || boundary.permanentStatic) return false;
64
+ const opening = boundary.node.openingElement;
65
+ if (opening?.name?.type !== 'JSXIdentifier') return false;
66
+ return (opening.attributes ?? []).every((attribute) => {
67
+ const name = attribute.name?.name;
68
+ return (
69
+ attribute.type === 'JSXAttribute' &&
70
+ attribute.name?.type === 'JSXIdentifier' &&
71
+ typeof name === 'string' &&
72
+ name !== 'children' &&
73
+ !name.startsWith('__')
74
+ );
75
+ });
76
+ }
77
+
78
+ function originalSplitContextProofs(analysis, moduleMovePlan) {
79
+ const contexts = findPrivateCompiledContextProofs(analysis.ast);
80
+ if (contexts.size === 0) return contexts;
81
+ const captured = new Set();
82
+ for (const boundary of analysis.boundaries) {
83
+ if (boundary.disabled || hasPermanentStaticAncestor(boundary)) continue;
84
+ const moduleBindings = moduleMovePlan.bindingsByPath.get(boundary.path) ?? new Set();
85
+ const names = collectCaptures(
86
+ boundary.node.children,
87
+ analysis.imports.importBindings,
88
+ boundary.shadowedImports,
89
+ moduleBindings.size === 0 ? null : moduleBindings,
90
+ );
91
+ for (const name of names) {
92
+ if (!contexts.has(name)) continue;
93
+ if (!privateContextCaptureBoundary(boundary)) contexts.delete(name);
94
+ else captured.add(name);
95
+ }
96
+ }
97
+ return new Map([...contexts].filter(([name]) => captured.has(name)));
98
+ }
99
+
100
+ function certifySplitContexts(ast, contexts, captureBindings) {
101
+ if (contexts.size === 0) return;
102
+ const byId = new Map(),
103
+ byTag = new Map(),
104
+ present = new WeakSet();
105
+ for (const [name, context] of contexts) {
106
+ byId.set(context.id, context.callee);
107
+ for (const tag of context.providerTags) byTag.set(tag, captureBindings.get(name) ?? context.id);
108
+ }
109
+ const callees = new Set(),
110
+ providers = new Map();
111
+ mapAstCow(ast, (node) => {
112
+ present.add(node);
113
+ if (byId.has(node)) callees.add(byId.get(node));
114
+ if (byTag.has(node)) providers.set(node, byTag.get(node));
115
+ return undefined;
116
+ });
117
+ for (const [tag, binding] of providers) if (!present.has(binding)) providers.delete(tag);
118
+ if (callees.size > 0 || providers.size > 0) privateSplitContexts.set(ast, { callees, providers });
119
+ }
120
+
19
121
  const SKIP_KEYS = new Set(['type', 'loc', 'start', 'end', 'range', 'metadata', 'parent']);
20
122
  const TRANSPARENT_TS_EXPRESSIONS = new Set([
21
123
  'ParenthesizedExpression',
@@ -1755,6 +1857,7 @@ function extractedModuleAst(
1755
1857
  request,
1756
1858
  moduleBindingsByPath = new Map(),
1757
1859
  moduleDeclarationsByPath = new Map(),
1860
+ captureBindings = new Map(),
1758
1861
  ) {
1759
1862
  if (boundary.disabled) {
1760
1863
  throw extractionError(
@@ -1803,6 +1906,11 @@ function extractedModuleAst(
1803
1906
  boundary.node,
1804
1907
  ),
1805
1908
  ];
1909
+ if (setup.length > 0) {
1910
+ const pattern = setup[0].declarations[0].id;
1911
+ for (let index = 0; index < captures.length; index++)
1912
+ captureBindings.set(captures[index], pattern.elements[index]);
1913
+ }
1806
1914
  const codeBlock = inheritGeneratedOrigin(
1807
1915
  {
1808
1916
  type: 'JSXCodeBlock',
@@ -2139,6 +2247,8 @@ export function prepareHydrateBoundaries(source, filename, boundaryPath = null,
2139
2247
  }
2140
2248
  const request = sameSourceRequest(filename);
2141
2249
  const moduleMovePlan = createModuleMovePlanAst(analysis, request);
2250
+ const contexts = originalSplitContextProofs(analysis, moduleMovePlan);
2251
+ const captureBindings = new Map();
2142
2252
  const independentWidgets = independentWidgetMetadata(analysis, filename, moduleMovePlan);
2143
2253
  let independentIndex = 0;
2144
2254
  for (const boundary of analysis.boundaries) {
@@ -2174,8 +2284,16 @@ export function prepareHydrateBoundaries(source, filename, boundaryPath = null,
2174
2284
  request,
2175
2285
  moduleMovePlan.bindingsByPath,
2176
2286
  moduleMovePlan.declarationsByPath,
2287
+ captureBindings,
2177
2288
  );
2178
2289
  }
2290
+ const templateTags = new Set(
2291
+ analysis.boundaries
2292
+ .filter(isCompiledSplitHydrateBoundary)
2293
+ .map((boundary) => boundary.node.openingElement.name),
2294
+ );
2295
+ if (templateTags.size > 0) compiledSplitHydrateTags.set(ast, templateTags);
2296
+ certifySplitContexts(ast, contexts, captureBindings);
2179
2297
  return {
2180
2298
  ast,
2181
2299
  boundaryPath,
@@ -0,0 +1,160 @@
1
+ import {
2
+ createLexicalAnalysis,
3
+ forEachRuntimeAstChild,
4
+ isIdentifierReference,
5
+ } from './compile-universal.js';
6
+
7
+ /** Only private contexts whose complete authored lifetime stays in template bodies. */
8
+ export function findPrivateCompiledContexts(ast) {
9
+ return new Map(
10
+ [...findPrivateCompiledContextProofs(ast)].map(([name, context]) => [name, context.callee]),
11
+ );
12
+ }
13
+
14
+ /** Definition and exact provider tags accepted by the same closed-module proof. */
15
+ export function findPrivateCompiledContextProofs(ast) {
16
+ const imports = new Map();
17
+ for (const statement of ast.body ?? []) {
18
+ if (
19
+ statement.type !== 'ImportDeclaration' ||
20
+ statement.source?.value !== 'octane' ||
21
+ statement.importKind === 'type'
22
+ )
23
+ continue;
24
+ for (const specifier of statement.specifiers ?? []) {
25
+ const name = specifier.imported?.name ?? specifier.imported?.value;
26
+ if (
27
+ specifier.type === 'ImportSpecifier' &&
28
+ specifier.importKind !== 'type' &&
29
+ ['createContext', 'use', 'useContext'].includes(name)
30
+ )
31
+ imports.set(specifier.local.name, name);
32
+ }
33
+ }
34
+ if (![...imports.values()].includes('createContext')) return new Map();
35
+ if (
36
+ !(ast.body ?? []).some(
37
+ (statement) =>
38
+ statement.type === 'VariableDeclaration' &&
39
+ statement.kind === 'const' &&
40
+ statement.declarations.some(
41
+ (entry) => imports.get(entry.init?.callee?.name) === 'createContext',
42
+ ),
43
+ )
44
+ )
45
+ return new Map();
46
+ const lexical = createLexicalAnalysis(ast);
47
+ const moduleBinding = (node) =>
48
+ node != null &&
49
+ lexical.resolveBinding(lexical.nodeScopes.get(node), node.name)?.scope === lexical.rootScope;
50
+ const contexts = new Map();
51
+ for (const statement of ast.body ?? []) {
52
+ if (statement.type !== 'VariableDeclaration' || statement.kind !== 'const') continue;
53
+ for (const entry of statement.declarations ?? []) {
54
+ const call = entry.init;
55
+ if (
56
+ entry.id?.type === 'Identifier' &&
57
+ call?.type === 'CallExpression' &&
58
+ call.optional !== true &&
59
+ call.callee?.type === 'Identifier' &&
60
+ imports.get(call.callee.name) === 'createContext' &&
61
+ moduleBinding(call.callee) &&
62
+ call.arguments.length === 1 &&
63
+ call.arguments[0].type !== 'SpreadElement'
64
+ )
65
+ contexts.set(entry.id.name, {
66
+ id: entry.id,
67
+ callee: call.callee,
68
+ providerTags: new Set(),
69
+ valid: true,
70
+ });
71
+ }
72
+ }
73
+ if (contexts.size === 0) return new Map();
74
+ let opaque = false;
75
+ const providerChildren = (element) =>
76
+ !(element.openingElement.attributes ?? []).some(
77
+ (attribute) =>
78
+ attribute.type === 'JSXSpreadAttribute' ||
79
+ attribute.name?.type !== 'JSXIdentifier' ||
80
+ attribute.name.name === 'children' ||
81
+ attribute.name.name.startsWith('__'),
82
+ ) &&
83
+ !(element.children ?? []).some((child) => {
84
+ let expression = child.type === 'JSXExpressionContainer' ? child.expression : null;
85
+ while (
86
+ [
87
+ 'TSAsExpression',
88
+ 'TSTypeAssertion',
89
+ 'TSSatisfiesExpression',
90
+ 'TSNonNullExpression',
91
+ 'TSInstantiationExpression',
92
+ 'ParenthesizedExpression',
93
+ ].includes(expression?.type)
94
+ )
95
+ expression = expression.expression;
96
+ return ['ArrowFunctionExpression', 'FunctionExpression'].includes(expression?.type);
97
+ });
98
+ const inspect = (node, parent, key, grandparent, direct = false) => {
99
+ if (node === null || typeof node !== 'object') return;
100
+ const identifier = node.type === 'Identifier';
101
+ const reference = identifier && isIdentifierReference(node, parent, key, lexical);
102
+ if (
103
+ node.type === 'WithStatement' ||
104
+ (reference && node.name === 'eval') ||
105
+ ((node.type === 'CallExpression' || node.type === 'OptionalCallExpression') &&
106
+ ['MemberExpression', 'OptionalMemberExpression'].includes(node.callee?.type) &&
107
+ (node.callee.property?.name ?? node.callee.property?.value) === 'eval')
108
+ )
109
+ opaque = true;
110
+ const context = contexts.get(node.name);
111
+ if (context !== undefined) {
112
+ // A module-wide name is used at emitted component calls. Any shadow
113
+ // declines that name rather than guessing a generated body's scope.
114
+ if (lexical.bindingNodes.has(node) && node !== context.id) context.valid = false;
115
+ const tag =
116
+ node.type === 'JSXIdentifier' &&
117
+ (((parent?.type === 'JSXOpeningElement' || parent?.type === 'JSXClosingElement') &&
118
+ key === 'name') ||
119
+ (parent?.type === 'JSXMemberExpression' && key === 'object'));
120
+ const exported = parent?.type === 'ExportSpecifier' && key === 'local';
121
+ if (reference || tag || exported) {
122
+ if (lexical.nodeScopes.get(node) === undefined) context.valid = false;
123
+ else if (moduleBinding(node)) {
124
+ const read =
125
+ reference &&
126
+ parent?.type === 'CallExpression' &&
127
+ parent.optional !== true &&
128
+ parent.arguments[0] === node &&
129
+ parent.arguments.length === 1 &&
130
+ parent.callee?.type === 'Identifier' &&
131
+ ['use', 'useContext'].includes(imports.get(parent.callee.name)) &&
132
+ moduleBinding(parent.callee);
133
+ const provider =
134
+ tag &&
135
+ direct &&
136
+ (parent?.type === 'JSXOpeningElement' || parent?.type === 'JSXClosingElement') &&
137
+ grandparent?.type === 'JSXElement' &&
138
+ providerChildren(grandparent);
139
+ if (!read && !provider) context.valid = false;
140
+ if (provider) context.providerTags.add(node);
141
+ }
142
+ }
143
+ }
144
+ forEachRuntimeAstChild(node, (child, childKey) => {
145
+ const childDirect =
146
+ node.type === 'JSXCodeBlock'
147
+ ? childKey === 'render'
148
+ : direct &&
149
+ ((node.type === 'JSXElement' &&
150
+ ['children', 'openingElement', 'closingElement'].includes(childKey)) ||
151
+ (node.type === 'JSXFragment' && childKey === 'children') ||
152
+ ((node.type === 'JSXOpeningElement' || node.type === 'JSXClosingElement') &&
153
+ childKey === 'name'));
154
+ inspect(child, node, childKey, parent, childDirect);
155
+ });
156
+ };
157
+ inspect(ast);
158
+ if (opaque) return new Map();
159
+ return new Map([...contexts].filter(([, context]) => context.valid));
160
+ }
@@ -10,16 +10,13 @@
10
10
  *
11
11
  * The server emit (runtime.server `ssrBlock` and friends) writes these markers
12
12
  * around every dynamic site, and the client `hydrateRoot` cursor scans for them
13
- * to align with the server output. This module is the shared home both import.
13
+ * to align with the server output. This module is the public home both import;
14
+ * the marker payloads and boundary attribute names themselves are declared in
15
+ * `hydration-markers.js`, which stays off the `dom-tables.js` graph so the
16
+ * pre-root capture bundle can share them, and are re-exported here unchanged.
14
17
  */
15
- /** Single-character payload of a block-open comment. */
16
- export declare const HYDRATION_START = "[";
17
- /** Single-character payload of a block-close comment. */
18
- export declare const HYDRATION_END = "]";
19
- /** @for outer-open payload: the server rendered its @empty arm. */
20
- export declare const HYDRATION_FOR_EMPTY = "[f0";
21
- /** @for outer-open payload: the server rendered one or more direct-host items. */
22
- export declare const HYDRATION_FOR_ITEMS = "[f1";
18
+ import { HYDRATION_START, HYDRATION_END, HYDRATION_FOR_EMPTY, HYDRATION_FOR_ITEMS } from './hydration-markers.js';
19
+ export { HYDRATION_START, HYDRATION_END, HYDRATION_FOR_EMPTY, HYDRATION_FOR_ITEMS };
23
20
  /** Opens a hydratable block (component output / control-flow branch). */
24
21
  export declare const BLOCK_OPEN = "<!--[-->";
25
22
  /** Closes a hydratable block. */
@@ -87,17 +84,9 @@ export declare const HYDRATION_RANGE_BOUNDARY: unique symbol;
87
84
  export declare const HYDRATE_STATIC_ID_COUNT_PREFIX = "octane-static-hydrate:";
88
85
  /** Closing comment for a wrapper-free permanent-static range. */
89
86
  export declare const HYDRATE_STATIC_END = "/octane-static-hydrate";
90
- /** Stable id of a server-rendered deferred hydration boundary. */
91
- export declare const HYDRATE_ID_ATTR = "data-octane-hydrate-id";
92
- /** Serialized strategy kind (`visible`, `idle`, `dynamic`, …). */
93
- export declare const HYDRATE_WHEN_ATTR = "data-octane-hydrate-when";
94
- /** Number of `useId()` slots consumed while rendering the deferred child. */
95
- export declare const HYDRATE_ID_COUNT_ATTR = "data-octane-hydrate-id-count";
96
87
  /** Opaque renderer stream token authenticating pending descendants owned by this boundary. */
97
88
  export { HYDRATE_STREAM_TOKEN_ATTR } from './stream-protocol.js';
98
- /** Direct-child JSON script carrying this boundary's `use()` seed slice. */
99
- export declare const HYDRATE_SEED_ATTR = "data-octane-hydrate-seed";
100
- export { INDEPENDENT_HYDRATE_MANIFEST_ATTR, HYDRATE_INDEPENDENT_ATTR, HYDRATE_INPUT_ATTR, SIGNAL_CONTROL_ATTR, } from './hydration-markers.js';
89
+ export { INDEPENDENT_HYDRATE_MANIFEST_ATTR, HYDRATE_INDEPENDENT_ATTR, HYDRATE_INPUT_ATTR, SIGNAL_CONTROL_ATTR, HYDRATE_ID_ATTR, HYDRATE_WHEN_ATTR, HYDRATE_ID_COUNT_ATTR, HYDRATE_SEED_ATTR, } from './hydration-markers.js';
101
90
  /** Sentinel <template> attribute marking a pending streamed boundary. */
102
91
  export { STREAM_BOUNDARY_ATTR } from './stream-protocol.js';
103
92
  /** Hidden segment container attribute carrying a completed boundary's content. */
package/dist/constants.js CHANGED
@@ -1,7 +1,9 @@
1
- const HYDRATION_START = "[";
2
- const HYDRATION_END = "]";
3
- const HYDRATION_FOR_EMPTY = "[f0";
4
- const HYDRATION_FOR_ITEMS = "[f1";
1
+ import {
2
+ HYDRATION_START,
3
+ HYDRATION_END,
4
+ HYDRATION_FOR_EMPTY,
5
+ HYDRATION_FOR_ITEMS
6
+ } from "./hydration-markers.js";
5
7
  const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
6
8
  const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
7
9
  const FOR_BLOCK_OPEN_EMPTY = `<!--${HYDRATION_FOR_EMPTY}-->`;
@@ -23,16 +25,16 @@ const HYDRATION_RANGE_BOUNDARY = /* @__PURE__ */ Symbol.for(
23
25
  );
24
26
  const HYDRATE_STATIC_ID_COUNT_PREFIX = "octane-static-hydrate:";
25
27
  const HYDRATE_STATIC_END = "/octane-static-hydrate";
26
- const HYDRATE_ID_ATTR = "data-octane-hydrate-id";
27
- const HYDRATE_WHEN_ATTR = "data-octane-hydrate-when";
28
- const HYDRATE_ID_COUNT_ATTR = "data-octane-hydrate-id-count";
29
28
  import { HYDRATE_STREAM_TOKEN_ATTR } from "./stream-protocol.js";
30
- const HYDRATE_SEED_ATTR = "data-octane-hydrate-seed";
31
29
  import {
32
30
  INDEPENDENT_HYDRATE_MANIFEST_ATTR,
33
31
  HYDRATE_INDEPENDENT_ATTR,
34
32
  HYDRATE_INPUT_ATTR,
35
- SIGNAL_CONTROL_ATTR
33
+ SIGNAL_CONTROL_ATTR,
34
+ HYDRATE_ID_ATTR,
35
+ HYDRATE_WHEN_ATTR,
36
+ HYDRATE_ID_COUNT_ATTR,
37
+ HYDRATE_SEED_ATTR
36
38
  } from "./hydration-markers.js";
37
39
  import { STREAM_BOUNDARY_ATTR } from "./stream-protocol.js";
38
40
  const STREAM_SEGMENT_ATTR = "data-oct-s";
package/dist/css.d.ts CHANGED
@@ -12,3 +12,10 @@ export declare function styleName(name: string): string;
12
12
  export declare function devWarnStyleProperty(name: string, value: unknown, server: boolean): void;
13
13
  /** @internal Development-only guidance after an existing CSS coercion fails. */
14
14
  export declare function devWarnStyleCoercion(name: string, value: unknown): void;
15
+ /**
16
+ * The one stylesheet element-scoped `<ViewTransition>` needs. The client
17
+ * dedupes the injection by this id and the SSR serializer emits the same pair,
18
+ * so spelling either half twice ships the rule twice or drops it on one side.
19
+ */
20
+ export declare const VIEW_TRANSITION_SCOPE_STYLE_ID = "octane-view-transition-scope";
21
+ export declare const VIEW_TRANSITION_SCOPE_CSS = "[vt-scope=\"element\"]{view-transition-scope:all!important}";
package/dist/css.js CHANGED
@@ -67,7 +67,11 @@ function devWarnStyleCoercion(name, value) {
67
67
  `The provided \`${name}\` CSS property is an unsupported type ${valueType}. This value must be coerced to a string before using it here.`
68
68
  );
69
69
  }
70
+ const VIEW_TRANSITION_SCOPE_STYLE_ID = "octane-view-transition-scope";
71
+ const VIEW_TRANSITION_SCOPE_CSS = '[vt-scope="element"]{view-transition-scope:all!important}';
70
72
  export {
73
+ VIEW_TRANSITION_SCOPE_CSS,
74
+ VIEW_TRANSITION_SCOPE_STYLE_ID,
71
75
  devWarnStyleCoercion,
72
76
  devWarnStyleProperty,
73
77
  mergeClass,
@@ -5,7 +5,13 @@ import {
5
5
  __releaseBinding,
6
6
  __writeBinding
7
7
  } from "./dom-bindings.js";
8
- import { encodeBindingKey, parseBindingMarker } from "./dom-binding-protocol.js";
8
+ import {
9
+ bindingRootMarker,
10
+ BINDING_OPEN_PREFIX,
11
+ encodeBindingKey,
12
+ parseBindingMarker
13
+ } from "./dom-binding-protocol.js";
14
+ import { HYDRATION_FOR_PREFIX } from "./hydration-markers.js";
9
15
  import { moveNativeNodeBefore } from "./dom-focused-move.js";
10
16
  import { rendererRangeClose } from "./stream-protocol.js";
11
17
  import {
@@ -527,7 +533,7 @@ function createFragment(definition, id, document, transaction) {
527
533
  const template = document.createElement("template");
528
534
  template.innerHTML = definition.ns === 1 ? `<svg>${definition.html}</svg>` : definition.html;
529
535
  const content = document.createDocumentFragment();
530
- const start = document.createComment(`[b;${id};root`);
536
+ const start = document.createComment(bindingRootMarker(id));
531
537
  const end = document.createComment("]");
532
538
  content.appendChild(start);
533
539
  if (definition.ns === 1) {
@@ -875,7 +881,8 @@ function commitRegion(plan, id, transaction) {
875
881
  transaction
876
882
  );
877
883
  }
878
- if (definition.kind === "if") region.range.start.data = `[b;${id};${region.site};${plan.arm}`;
884
+ if (definition.kind === "if")
885
+ region.range.start.data = `${BINDING_OPEN_PREFIX}${id};${region.site};${plan.arm}`;
879
886
  }
880
887
  function writeText(region, value) {
881
888
  if (region.text === null) {
@@ -1469,13 +1476,13 @@ function commitList(plan, id, transaction) {
1469
1476
  }
1470
1477
  let anchor = region.range.end;
1471
1478
  for (const [key, item] of [...plan.items].reverse()) {
1472
- item.instance.range.start.data = `[b;${id};${region.site};k;${key}`;
1479
+ item.instance.range.start.data = `${BINDING_OPEN_PREFIX}${id};${region.site};k;${key}`;
1473
1480
  moveRange(item.instance.range, region.range.end.parentNode, anchor, transaction);
1474
1481
  if (transaction.disposed) return;
1475
1482
  anchor = item.instance.range.start;
1476
1483
  }
1477
1484
  }
1478
- region.range.start.data = `[f${plan.arm};b;${id};${region.site}`;
1485
+ region.range.start.data = `${HYDRATION_FOR_PREFIX}${plan.arm};b;${id};${region.site}`;
1479
1486
  return;
1480
1487
  }
1481
1488
  function claimProgramControl(instance, index, node, transaction) {
@@ -1,5 +1,12 @@
1
- /** Compiler-owned DOM presentation markers. This leaf is shared by client and SSR. */
2
1
  export type BindingKey = string | number;
2
+ /** Payload prefix opening a presentation-binding range. */
3
+ export declare const BINDING_OPEN_PREFIX: string;
4
+ /**
5
+ * Comment payload opening an authored presentation view's own component range.
6
+ * The client runtime and the SSR serializer both stamp this onto the view they
7
+ * bind, so the two must never spell it separately.
8
+ */
9
+ export declare function bindingRootMarker(id: string): string;
3
10
  export declare function encodeBindingKey(key: BindingKey): string;
4
11
  export declare function decodeBindingKey(encoded: string): BindingKey;
5
12
  export interface BindingMarker {
@@ -11,3 +18,9 @@ export interface BindingMarker {
11
18
  }
12
19
  export declare function parseBindingMarker(data: string): BindingMarker | null;
13
20
  export declare function isBindingOpenComment(data: string): boolean;
21
+ /**
22
+ * True when a binding-open payload carries an `@for` outer arm. The prefix test
23
+ * alone is not a decision: `isBindingOpenComment` still requires the full
24
+ * receipt, so this only spares callers from spelling the arm prefixes again.
25
+ */
26
+ export declare function isForBindingOpenComment(data: string): boolean;
@@ -1,3 +1,21 @@
1
+ import {
2
+ HYDRATION_FOR_ARM_INDEX,
3
+ HYDRATION_FOR_EMPTY,
4
+ HYDRATION_FOR_ITEMS,
5
+ HYDRATION_FOR_PREFIX,
6
+ HYDRATION_START
7
+ } from "./hydration-markers.js";
8
+ const BINDING_OPEN_PREFIX = HYDRATION_START + "b;";
9
+ const BINDING_RECEIPT = ";b;";
10
+ const BINDING_FOR_EMPTY_PREFIX = HYDRATION_FOR_EMPTY + BINDING_RECEIPT;
11
+ const BINDING_FOR_ITEMS_PREFIX = HYDRATION_FOR_ITEMS + BINDING_RECEIPT;
12
+ const BINDING_FOR_ID_START = BINDING_FOR_EMPTY_PREFIX.length;
13
+ const BINDING_ID_START = BINDING_OPEN_PREFIX.length;
14
+ const BINDING_ROOT_SITE = "root";
15
+ const BINDING_ROOT_SUFFIX = ";" + BINDING_ROOT_SITE;
16
+ function bindingRootMarker(id) {
17
+ return BINDING_OPEN_PREFIX + id + BINDING_ROOT_SUFFIX;
18
+ }
1
19
  function encodeBindingKey(key) {
2
20
  if (typeof key === "string")
3
21
  return "s:" + JSON.stringify(key).replace(/-/g, "\\u002d").replace(/</g, "\\u003c");
@@ -17,21 +35,22 @@ function decodeBindingKey(encoded) {
17
35
  throw new TypeError("A DOM presentation key has invalid serialized identity.");
18
36
  }
19
37
  function parseBindingMarker(data) {
20
- if (data.startsWith("[f0;b;") || data.startsWith("[f1;b;")) {
21
- const separator = data.indexOf(";", 6);
22
- if (separator <= 6 || separator === data.length - 1) return null;
38
+ if (data.startsWith(BINDING_FOR_EMPTY_PREFIX) || data.startsWith(BINDING_FOR_ITEMS_PREFIX)) {
39
+ const separator = data.indexOf(";", BINDING_FOR_ID_START);
40
+ if (separator <= BINDING_FOR_ID_START || separator === data.length - 1) return null;
23
41
  return {
24
- id: data.slice(6, separator),
42
+ id: data.slice(BINDING_FOR_ID_START, separator),
25
43
  site: data.slice(separator + 1),
26
44
  kind: "for",
27
- arm: data.charCodeAt(2) - 48
45
+ arm: data.charCodeAt(HYDRATION_FOR_ARM_INDEX) - 48
28
46
  };
29
47
  }
30
- if (!data.startsWith("[b;")) return null;
31
- const idEnd = data.indexOf(";", 3);
32
- if (idEnd <= 3 || idEnd === data.length - 1) return null;
33
- const id = data.slice(3, idEnd);
34
- if (data.slice(idEnd + 1) === "root") return { id, site: "root", kind: "root" };
48
+ if (!data.startsWith(BINDING_OPEN_PREFIX)) return null;
49
+ const idEnd = data.indexOf(";", BINDING_ID_START);
50
+ if (idEnd <= BINDING_ID_START || idEnd === data.length - 1) return null;
51
+ const id = data.slice(BINDING_ID_START, idEnd);
52
+ if (data.slice(idEnd + 1) === BINDING_ROOT_SITE)
53
+ return { id, site: BINDING_ROOT_SITE, kind: "root" };
35
54
  const siteEnd = data.indexOf(";", idEnd + 1);
36
55
  if (siteEnd <= idEnd + 1 || siteEnd === data.length - 1) return null;
37
56
  const site = data.slice(idEnd + 1, siteEnd);
@@ -56,13 +75,14 @@ function parseBindingMarker(data) {
56
75
  return null;
57
76
  }
58
77
  function isBindingOpenComment(data) {
59
- const list = data.startsWith("[f0;b;") || data.startsWith("[f1;b;");
60
- if (!list && !data.startsWith("[b;")) return false;
61
- const idStart = list ? 6 : 3;
78
+ const list = data.startsWith(BINDING_FOR_EMPTY_PREFIX) || data.startsWith(BINDING_FOR_ITEMS_PREFIX);
79
+ if (!list && !data.startsWith(BINDING_OPEN_PREFIX)) return false;
80
+ const idStart = list ? BINDING_FOR_ID_START : BINDING_ID_START;
62
81
  const idEnd = data.indexOf(";", idStart);
63
82
  if (idEnd <= idStart || idEnd === data.length - 1) return false;
64
83
  if (list) return data.indexOf(";", idEnd + 1) === -1;
65
- if (data.length - idEnd === 5 && data.endsWith(";root")) return true;
84
+ if (data.length - idEnd === BINDING_ROOT_SUFFIX.length && data.endsWith(BINDING_ROOT_SUFFIX))
85
+ return true;
66
86
  const siteEnd = data.indexOf(";", idEnd + 1);
67
87
  if (siteEnd <= idEnd + 1 || siteEnd === data.length - 1) return false;
68
88
  const tail = siteEnd + 1;
@@ -80,9 +100,15 @@ function isBindingOpenComment(data) {
80
100
  }
81
101
  return true;
82
102
  }
103
+ function isForBindingOpenComment(data) {
104
+ return data.startsWith(HYDRATION_FOR_PREFIX) && isBindingOpenComment(data);
105
+ }
83
106
  export {
107
+ BINDING_OPEN_PREFIX,
108
+ bindingRootMarker,
84
109
  decodeBindingKey,
85
110
  encodeBindingKey,
86
111
  isBindingOpenComment,
112
+ isForBindingOpenComment,
87
113
  parseBindingMarker
88
114
  };
@@ -45,6 +45,8 @@ export interface HydrationControlCandidateValue {
45
45
  * the state changed while ownership was being transferred.
46
46
  */
47
47
  export declare function snapshotHydrationControl(control: Element): HydrationControlSnapshot | null;
48
+ /** @internal Only unchanged server state may yield to an eventless textarea restore. */
49
+ export declare function isRestoredHydrationTextarea(control: Element, value: unknown): boolean;
48
50
  /**
49
51
  * Capture the exact DOM/edit authority an async storage read is allowed to replace.
50
52
  * A focus, selection, composition, input (including clear), node replacement, or
@@ -1,4 +1,10 @@
1
- import { HYDRATE_INDEPENDENT_ATTR, HYDRATE_INPUT_ATTR } from "../hydration-markers.js";
1
+ import {
2
+ HYDRATE_ID_ATTR,
3
+ HYDRATE_INDEPENDENT_ATTR,
4
+ HYDRATE_INPUT_ATTR,
5
+ HYDRATE_MARKER_SELECTOR,
6
+ HYDRATE_WHEN_ATTR
7
+ } from "../hydration-markers.js";
2
8
  import {
3
9
  EARLY_HYDRATION_INTENTS_KEY,
4
10
  HYDRATE_INTERACTION_EVENTS_ATTR
@@ -9,10 +15,9 @@ import {
9
15
  publishHydrationControlSignalValues,
10
16
  readEarlyHydrationControlRevision
11
17
  } from "../signals/early-values.js";
12
- const HYDRATE_MARKER_SELECTOR = "[data-octane-hydrate-id]";
13
18
  const HYDRATE_CONTROL_DOCUMENTS = /* @__PURE__ */ new WeakSet();
14
19
  function isEarlyHydrationIntentCurrent([event, target, boundary, id, when, events], ownerDocument) {
15
- return event.target === target && target.isConnected && boundary.isConnected && target.ownerDocument === ownerDocument && boundary.ownerDocument === ownerDocument && target.closest(`[${HYDRATE_INDEPENDENT_ATTR}]`) === boundary && boundary.getAttribute("data-octane-hydrate-id") === id && boundary.getAttribute("data-octane-hydrate-when") === when && boundary.getAttribute(HYDRATE_INTERACTION_EVENTS_ATTR) === events;
20
+ return event.target === target && target.isConnected && boundary.isConnected && target.ownerDocument === ownerDocument && boundary.ownerDocument === ownerDocument && target.closest(`[${HYDRATE_INDEPENDENT_ATTR}]`) === boundary && boundary.getAttribute(HYDRATE_ID_ATTR) === id && boundary.getAttribute(HYDRATE_WHEN_ATTR) === when && boundary.getAttribute(HYDRATE_INTERACTION_EVENTS_ATTR) === events;
16
21
  }
17
22
  function isHydrationElement(target) {
18
23
  return target !== null && target.nodeType === 1;
@@ -108,6 +113,13 @@ function snapshotHydrationControl(control) {
108
113
  composing: record?.composing ?? false
109
114
  };
110
115
  }
116
+ function isRestoredHydrationTextarea(control, value) {
117
+ if (control.localName !== "textarea" || typeof value !== "string" || HYDRATE_CONTROL_RECORDS.get(control)?.composing === true)
118
+ return false;
119
+ const textarea = control;
120
+ const baseline = textarea.defaultValue.replace(/\r\n?/g, "\n");
121
+ return value.replace(/\r\n?/g, "\n") === baseline && textarea.value !== baseline;
122
+ }
111
123
  function captureHydrationControlCandidate(control) {
112
124
  const snapshot = snapshotHydrationControl(control);
113
125
  if (snapshot === null) return null;
@@ -115,7 +127,7 @@ function captureHydrationControlCandidate(control) {
115
127
  return {
116
128
  control,
117
129
  bindingId: control.getAttribute(HYDRATE_INPUT_ATTR),
118
- boundaryId: control.closest(HYDRATE_MARKER_SELECTOR)?.getAttribute("data-octane-hydrate-id") ?? null,
130
+ boundaryId: control.closest(HYDRATE_MARKER_SELECTOR)?.getAttribute(HYDRATE_ID_ATTR) ?? null,
119
131
  snapshot
120
132
  };
121
133
  }
@@ -130,7 +142,7 @@ function sameHydrationControlValue(left, right) {
130
142
  }
131
143
  function applyHydrationControlCandidate(candidate, value) {
132
144
  const control = candidate.control;
133
- if (!control.isConnected || control.getAttribute(HYDRATE_INPUT_ATTR) !== candidate.bindingId || (control.closest(HYDRATE_MARKER_SELECTOR)?.getAttribute("data-octane-hydrate-id") ?? null) !== candidate.boundaryId) {
145
+ if (!control.isConnected || control.getAttribute(HYDRATE_INPUT_ATTR) !== candidate.bindingId || (control.closest(HYDRATE_MARKER_SELECTOR)?.getAttribute(HYDRATE_ID_ATTR) ?? null) !== candidate.boundaryId) {
134
146
  return false;
135
147
  }
136
148
  const current = snapshotHydrationControl(control);
@@ -219,5 +231,6 @@ export {
219
231
  consumeHydrationControl,
220
232
  initializeHydrationControlCapture,
221
233
  isEarlyHydrationIntentCurrent,
234
+ isRestoredHydrationTextarea,
222
235
  snapshotHydrationControl
223
236
  };