octane 0.2.11 → 0.2.13

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 (264) hide show
  1. package/README.md +5 -0
  2. package/dist/behavior-root.d.ts +13 -3
  3. package/dist/behavior-root.js +28 -5
  4. package/dist/cjs/behavior-root.cjs +32 -6
  5. package/dist/cjs/class-names.cjs +49 -0
  6. package/dist/cjs/constants.cjs +9 -0
  7. package/dist/cjs/context-epoch.cjs +38 -0
  8. package/dist/cjs/css.cjs +6 -27
  9. package/dist/cjs/data-encoding.cjs +160 -0
  10. package/dist/cjs/dom-binding-handoff.cjs +93 -0
  11. package/dist/cjs/dom-binding-protocol.cjs +115 -0
  12. package/dist/cjs/dom-binding-styles.cjs +292 -0
  13. package/dist/cjs/dom-bindings.cjs +700 -0
  14. package/dist/cjs/dom-focused-move.cjs +66 -0
  15. package/dist/cjs/dom-stage.cjs +896 -0
  16. package/dist/cjs/dom-tables.cjs +4 -94
  17. package/dist/cjs/error-codes.client.generated.cjs +33 -0
  18. package/dist/cjs/error-codes.server.generated.cjs +24 -0
  19. package/dist/cjs/hydration/control-capture.cjs +244 -0
  20. package/dist/cjs/hydration/event-capture.cjs +125 -43
  21. package/dist/cjs/hydration/interaction-config.cjs +50 -2
  22. package/dist/cjs/hydration-markers.cjs +37 -0
  23. package/dist/cjs/independent-hydration-protocol.cjs +106 -0
  24. package/dist/cjs/index.cjs +9 -0
  25. package/dist/cjs/runtime.cjs +6397 -1683
  26. package/dist/cjs/runtime.server.cjs +1465 -213
  27. package/dist/cjs/server/early-signals.cjs +45 -0
  28. package/dist/cjs/server/index.cjs +47 -0
  29. package/dist/cjs/server/rpc-batch.cjs +149 -0
  30. package/dist/cjs/server/rpc-stream.cjs +146 -0
  31. package/dist/cjs/server/rpc.cjs +34 -15
  32. package/dist/cjs/server/signal-query-observation.cjs +158 -0
  33. package/dist/cjs/server/streamed-region.cjs +52 -0
  34. package/dist/cjs/server/streamed-signals.cjs +512 -0
  35. package/dist/cjs/server-call.cjs +103 -0
  36. package/dist/cjs/server-rpc-batch-client.cjs +141 -0
  37. package/dist/cjs/server-rpc-client.cjs +94 -27
  38. package/dist/cjs/server-rpc-protocol.cjs +141 -0
  39. package/dist/cjs/server-rpc-stream-client.cjs +118 -0
  40. package/dist/cjs/signals/actions.cjs +474 -0
  41. package/dist/cjs/signals/client.cjs +4 -1
  42. package/dist/cjs/signals/computations.cjs +460 -0
  43. package/dist/cjs/signals/control-binding.cjs +368 -0
  44. package/dist/cjs/signals/derived-facade.cjs +50 -0
  45. package/dist/cjs/signals/document-owner.cjs +71 -0
  46. package/dist/cjs/signals/early-values.cjs +155 -0
  47. package/dist/cjs/signals/encoding.cjs +6 -95
  48. package/dist/cjs/signals/engine.cjs +276 -12
  49. package/dist/cjs/signals/errors.cjs +18 -0
  50. package/dist/cjs/signals/facade.cjs +477 -0
  51. package/dist/cjs/signals/graph.cjs +166 -20
  52. package/dist/cjs/signals/handle-protocol.cjs +36 -0
  53. package/dist/cjs/signals/index.cjs +81 -2
  54. package/dist/cjs/signals/native-read-client.cjs +10 -0
  55. package/dist/cjs/signals/owner-context.cjs +112 -0
  56. package/dist/cjs/signals/query-attempt-observer.cjs +68 -0
  57. package/dist/cjs/signals/query-facade.cjs +72 -0
  58. package/dist/cjs/signals/read-protocol.cjs +54 -0
  59. package/dist/cjs/signals/requests.cjs +287 -14
  60. package/dist/cjs/signals/scalar-computations.cjs +76 -0
  61. package/dist/cjs/signals/server.cjs +4 -1
  62. package/dist/cjs/signals/transition-candidate.cjs +567 -0
  63. package/dist/cjs/signals/transition-state.cjs +106 -0
  64. package/dist/cjs/signals/types.cjs +50 -0
  65. package/dist/cjs/stream-protocol.cjs +3 -0
  66. package/dist/cjs/streamed-signals-protocol.cjs +159 -0
  67. package/dist/cjs/style-values.cjs +122 -0
  68. package/dist/cjs/trusted-html.cjs +30 -0
  69. package/dist/cjs/version.cjs +1 -1
  70. package/dist/class-names.d.ts +5 -0
  71. package/dist/class-names.js +25 -0
  72. package/dist/compiler/bundler.js +51 -22
  73. package/dist/compiler/compile.js +2534 -581
  74. package/dist/compiler/dom-binding-program.js +935 -0
  75. package/dist/compiler/dom-binding-request.js +57 -0
  76. package/dist/compiler/dom-bindings.js +2245 -0
  77. package/dist/compiler/hook-deps.js +759 -10
  78. package/dist/compiler/hydrate-boundaries.js +358 -15
  79. package/dist/compiler/index.d.ts +39 -2
  80. package/dist/compiler/index.js +1 -0
  81. package/dist/compiler/native-attribute-reads.js +127 -0
  82. package/dist/compiler/native-change-diagnostics.js +32 -38
  83. package/dist/compiler/native-read-diagnostics.js +69 -8
  84. package/dist/compiler/native-read-types.js +80 -34
  85. package/dist/compiler/parser.node.js +1 -1
  86. package/dist/compiler/renderer-boundaries.js +40 -0
  87. package/dist/compiler/server-context.js +118 -0
  88. package/dist/compiler/signal-attempt-reads.js +479 -0
  89. package/dist/compiler/signal-declarations.js +557 -0
  90. package/dist/compiler/signal-start-reads.js +413 -0
  91. package/dist/compiler/slot-hooks.d.ts +3 -1
  92. package/dist/compiler/slot-hooks.js +168 -34
  93. package/dist/compiler/strong-auto-memo.js +90 -0
  94. package/dist/compiler/strong-html.js +143 -0
  95. package/dist/compiler/strong-mode.js +764 -66
  96. package/dist/compiler/strong-template-policy.js +307 -0
  97. package/dist/compiler/template-ir.js +200 -0
  98. package/dist/compiler/typescript.d.ts +2 -0
  99. package/dist/compiler/typescript.js +2 -0
  100. package/dist/compiler/vite.d.ts +3 -1
  101. package/dist/compiler/vite.js +122 -4
  102. package/dist/compiler/volar.LICENSES.txt +1 -1
  103. package/dist/compiler/volar.d.ts +6 -1
  104. package/dist/compiler/volar.js +750 -1433
  105. package/dist/constants.d.ts +1 -0
  106. package/dist/constants.js +10 -0
  107. package/dist/context-epoch.d.ts +2 -0
  108. package/dist/context-epoch.js +13 -0
  109. package/dist/css.d.ts +2 -5
  110. package/dist/css.js +2 -23
  111. package/dist/data-encoding.d.ts +9 -0
  112. package/dist/data-encoding.js +134 -0
  113. package/dist/dom-binding-classes.d.ts +15 -0
  114. package/dist/dom-binding-classes.js +129 -0
  115. package/dist/dom-binding-controls.d.ts +2 -0
  116. package/dist/dom-binding-controls.js +6 -0
  117. package/dist/dom-binding-handoff.d.ts +55 -0
  118. package/dist/dom-binding-handoff.js +62 -0
  119. package/dist/dom-binding-program.d.ts +300 -0
  120. package/dist/dom-binding-program.js +1625 -0
  121. package/dist/dom-binding-projections.d.ts +15 -0
  122. package/dist/dom-binding-projections.js +187 -0
  123. package/dist/dom-binding-protocol.d.ts +13 -0
  124. package/dist/dom-binding-protocol.js +88 -0
  125. package/dist/dom-binding-signals.d.ts +21 -0
  126. package/dist/dom-binding-signals.js +109 -0
  127. package/dist/dom-binding-styles.d.ts +17 -0
  128. package/dist/dom-binding-styles.js +272 -0
  129. package/dist/dom-bindings.d.ts +89 -0
  130. package/dist/dom-bindings.js +670 -0
  131. package/dist/dom-focused-move.d.ts +6 -0
  132. package/dist/dom-focused-move.js +42 -0
  133. package/dist/dom-stage.d.ts +52 -0
  134. package/dist/dom-stage.js +872 -0
  135. package/dist/dom-tables.d.ts +1 -3
  136. package/dist/dom-tables.js +1 -91
  137. package/dist/error-codes.client.generated.d.ts +9 -0
  138. package/dist/error-codes.client.generated.js +33 -0
  139. package/dist/error-codes.server.generated.d.ts +6 -0
  140. package/dist/error-codes.server.generated.js +24 -0
  141. package/dist/hydration/control-capture.d.ts +62 -0
  142. package/dist/hydration/control-capture.js +223 -0
  143. package/dist/hydration/document-lifecycle.d.ts +23 -0
  144. package/dist/hydration/document-lifecycle.js +80 -0
  145. package/dist/hydration/event-capture.d.ts +20 -1
  146. package/dist/hydration/event-capture.js +128 -42
  147. package/dist/hydration/independent-island.d.ts +45 -0
  148. package/dist/hydration/independent-island.js +233 -0
  149. package/dist/hydration/index.d.ts +8 -1
  150. package/dist/hydration/index.js +55 -1
  151. package/dist/hydration/interaction-config.d.ts +5 -0
  152. package/dist/hydration/interaction-config.js +44 -1
  153. package/dist/hydration/stream-delivery.d.ts +32 -0
  154. package/dist/hydration/stream-delivery.js +333 -0
  155. package/dist/hydration/stream-receiver.d.ts +26 -0
  156. package/dist/hydration/stream-receiver.js +204 -0
  157. package/dist/hydration/stream-result-receiver.d.ts +70 -0
  158. package/dist/hydration/stream-result-receiver.js +246 -0
  159. package/dist/hydration/streamed-signals.d.ts +44 -0
  160. package/dist/hydration/streamed-signals.js +186 -0
  161. package/dist/hydration/types.d.ts +6 -0
  162. package/dist/hydration-markers.d.ts +7 -0
  163. package/dist/hydration-markers.js +10 -0
  164. package/dist/independent-hydration-protocol.d.ts +31 -0
  165. package/dist/independent-hydration-protocol.js +80 -0
  166. package/dist/index.d.ts +3 -0
  167. package/dist/index.js +6 -0
  168. package/dist/internal/signal-read.d.ts +1 -0
  169. package/dist/internal/signal-read.js +4 -0
  170. package/dist/jsx-runtime-strong.d.ts +33 -0
  171. package/dist/jsx-runtime.d.ts +58 -2
  172. package/dist/public-types.d.ts +15 -1
  173. package/dist/runtime.d.ts +297 -23
  174. package/dist/runtime.js +6405 -1679
  175. package/dist/runtime.server.d.ts +110 -27
  176. package/dist/runtime.server.js +1458 -213
  177. package/dist/server/early-signals.d.ts +14 -0
  178. package/dist/server/early-signals.js +34 -0
  179. package/dist/server/index.d.ts +11 -2
  180. package/dist/server/index.js +50 -1
  181. package/dist/server/rpc-batch.d.ts +17 -0
  182. package/dist/server/rpc-batch.js +129 -0
  183. package/dist/server/rpc-stream.d.ts +9 -0
  184. package/dist/server/rpc-stream.js +125 -0
  185. package/dist/server/rpc.d.ts +5 -1
  186. package/dist/server/rpc.js +37 -13
  187. package/dist/server/signal-query-observation.d.ts +3 -0
  188. package/dist/server/signal-query-observation.js +134 -0
  189. package/dist/server/streamed-region.d.ts +15 -0
  190. package/dist/server/streamed-region.js +30 -0
  191. package/dist/server/streamed-signals.d.ts +45 -0
  192. package/dist/server/streamed-signals.js +488 -0
  193. package/dist/server-call.d.ts +41 -0
  194. package/dist/server-call.js +75 -0
  195. package/dist/server-rpc-batch-client.d.ts +7 -0
  196. package/dist/server-rpc-batch-client.js +119 -0
  197. package/dist/server-rpc-client.d.ts +3 -1
  198. package/dist/server-rpc-client.js +99 -27
  199. package/dist/server-rpc-protocol.d.ts +47 -0
  200. package/dist/server-rpc-protocol.js +109 -0
  201. package/dist/server-rpc-stream-client.d.ts +5 -0
  202. package/dist/server-rpc-stream-client.js +98 -0
  203. package/dist/signals/actions.d.ts +12 -0
  204. package/dist/signals/actions.js +466 -0
  205. package/dist/signals/client.d.ts +2 -0
  206. package/dist/signals/client.js +1 -0
  207. package/dist/signals/computations.d.ts +37 -0
  208. package/dist/signals/computations.js +455 -0
  209. package/dist/signals/control-binding.d.ts +31 -0
  210. package/dist/signals/control-binding.js +353 -0
  211. package/dist/signals/derived-facade.d.ts +3 -0
  212. package/dist/signals/derived-facade.js +25 -0
  213. package/dist/signals/document-owner.d.ts +10 -0
  214. package/dist/signals/document-owner.js +43 -0
  215. package/dist/signals/early-values.d.ts +24 -0
  216. package/dist/signals/early-values.js +124 -0
  217. package/dist/signals/encoding.d.ts +1 -3
  218. package/dist/signals/encoding.js +3 -93
  219. package/dist/signals/engine.d.ts +53 -4
  220. package/dist/signals/engine.js +275 -16
  221. package/dist/signals/errors.d.ts +7 -0
  222. package/dist/signals/errors.js +16 -0
  223. package/dist/signals/facade.d.ts +74 -0
  224. package/dist/signals/facade.js +452 -0
  225. package/dist/signals/graph.d.ts +52 -6
  226. package/dist/signals/graph.js +174 -19
  227. package/dist/signals/handle-protocol.d.ts +4 -0
  228. package/dist/signals/handle-protocol.js +11 -0
  229. package/dist/signals/index.d.ts +12 -3
  230. package/dist/signals/index.js +71 -2
  231. package/dist/signals/native-read-client.d.ts +3 -0
  232. package/dist/signals/native-read-client.js +10 -0
  233. package/dist/signals/owner-context.d.ts +22 -0
  234. package/dist/signals/owner-context.js +77 -0
  235. package/dist/signals/query-attempt-observer.d.ts +49 -0
  236. package/dist/signals/query-attempt-observer.js +41 -0
  237. package/dist/signals/query-facade.d.ts +3 -0
  238. package/dist/signals/query-facade.js +49 -0
  239. package/dist/signals/read-protocol.d.ts +37 -2
  240. package/dist/signals/read-protocol.js +44 -0
  241. package/dist/signals/requests.d.ts +38 -5
  242. package/dist/signals/requests.js +298 -15
  243. package/dist/signals/scalar-computations.d.ts +2 -0
  244. package/dist/signals/scalar-computations.js +60 -0
  245. package/dist/signals/server.d.ts +2 -0
  246. package/dist/signals/server.js +1 -0
  247. package/dist/signals/transition-candidate.d.ts +71 -0
  248. package/dist/signals/transition-candidate.js +557 -0
  249. package/dist/signals/transition-state.d.ts +51 -0
  250. package/dist/signals/transition-state.js +70 -0
  251. package/dist/signals/types.d.ts +95 -5
  252. package/dist/signals/types.js +20 -0
  253. package/dist/stream-protocol.d.ts +0 -5
  254. package/dist/stream-protocol.js +3 -0
  255. package/dist/streamed-signals-protocol.d.ts +60 -0
  256. package/dist/streamed-signals-protocol.js +130 -0
  257. package/dist/style-values.d.ts +3 -0
  258. package/dist/style-values.js +96 -0
  259. package/dist/trusted-html.d.ts +17 -0
  260. package/dist/trusted-html.js +6 -0
  261. package/dist/universal-core.d.ts +6 -0
  262. package/dist/universal-core.js +214 -202
  263. package/dist/version.js +1 -1
  264. package/package.json +48 -5
@@ -0,0 +1,307 @@
1
+ import { createLexicalAnalysis } from './compile-universal.js';
2
+ import {
3
+ checkStrongHTMLValue,
4
+ strongHTMLSpreadWriter,
5
+ STRONG_UNTRUSTED_HTML,
6
+ STRONG_HTML_MESSAGE,
7
+ mayHaveStrongHTML,
8
+ } from './strong-html.js';
9
+ import { createRendererRegionResolver } from './renderer-boundaries.js';
10
+
11
+ const COMPAT_EXPORTS = new Set(['flushSync', 'unstable_batchedUpdates', 'StrictMode']);
12
+ const SUPPRESSION_PROPS = new Set(['suppressHydrationWarning', 'suppressNativeChangeWarning']);
13
+ const FUNCTIONS = new Set(['ArrowFunctionExpression', 'FunctionExpression', 'FunctionDeclaration']);
14
+ const SKIP_KEYS = new Set(['type', 'loc', 'start', 'end', 'range', 'metadata', 'parent']);
15
+
16
+ // These checks share the Strong visitor's lexical bindings. The policy itself
17
+ // never annotates the parser tree or changes the output of valid programs.
18
+ export function createStrongTemplatePolicy({
19
+ ast,
20
+ report,
21
+ resolve,
22
+ unwrap,
23
+ callableValue,
24
+ staticPrimitiveValue,
25
+ source,
26
+ filename,
27
+ options,
28
+ }) {
29
+ const pending = [];
30
+ const seen = new WeakMap();
31
+ const jsxReturns = new WeakMap();
32
+ // Import and property names may contain Unicode/hex/identity escapes or
33
+ // quoted line continuations. Newline string escapes cannot spell these names.
34
+ const mayHaveHTML =
35
+ mayHaveStrongHTML(source) &&
36
+ (source.includes('createElement') || /\\(?:u|x|[ceaElm]|[\r\n\u2028\u2029])/.test(source));
37
+ const templateDialect = /\.tsrx(?:[?#]|$)/.test(filename);
38
+ const domAt =
39
+ options.rendererRegionResolver ?? createRendererRegionResolver(ast, source, filename, options);
40
+ function add(code, node, message) {
41
+ let codes = seen.get(node);
42
+ if (codes?.has(code)) return;
43
+ if (!codes) seen.set(node, (codes = new Set()));
44
+ codes.add(code);
45
+ pending.push({ code, node, message });
46
+ }
47
+ function checkKey(expression, scope) {
48
+ const node = unwrap(expression);
49
+ if (!node || typeof node !== 'object' || FUNCTIONS.has(node.type)) return;
50
+ if (Array.isArray(node)) {
51
+ for (const child of node) checkKey(child, scope);
52
+ return;
53
+ }
54
+ if (node.type === 'Identifier' && resolve(scope, node.name)?.strongIndex === true) {
55
+ add(
56
+ 'OCTANE_STRONG_INDEX_KEY',
57
+ node,
58
+ 'An `@for` key must identify the item independently of its position. Use a stable item ID instead of the loop index.',
59
+ );
60
+ return;
61
+ }
62
+ // Looking up an item by position does not make the returned identity a
63
+ // positional key. Only the receiver, not the lookup index, contributes.
64
+ if (node.type === 'MemberExpression') {
65
+ checkKey(node.object, scope);
66
+ return;
67
+ }
68
+ if (node.type === 'Property') {
69
+ checkKey(node.value, scope);
70
+ return;
71
+ }
72
+ for (const key in node)
73
+ if (!SKIP_KEYS.has(key) && !key.startsWith('_octane')) checkKey(node[key], scope);
74
+ }
75
+ function expressionHasJSX(node, aliasIsJSX) {
76
+ const value = unwrap(node);
77
+ if (!value || FUNCTIONS.has(value.type)) return false;
78
+ if (value.type === 'Identifier' && aliasIsJSX) return aliasIsJSX(value);
79
+ if (
80
+ value.type === 'JSXElement' ||
81
+ value.type === 'JSXFragment' ||
82
+ value.type === 'JSXCodeBlock'
83
+ )
84
+ return true;
85
+ if (value.type === 'ConditionalExpression')
86
+ return (
87
+ expressionHasJSX(value.consequent, aliasIsJSX) ||
88
+ expressionHasJSX(value.alternate, aliasIsJSX)
89
+ );
90
+ if (value.type === 'LogicalExpression')
91
+ return expressionHasJSX(value.left, aliasIsJSX) || expressionHasJSX(value.right, aliasIsJSX);
92
+ if (value.type === 'SequenceExpression')
93
+ return expressionHasJSX(value.expressions.at(-1), aliasIsJSX);
94
+ if (value.type === 'ArrayExpression')
95
+ return value.elements.some((element) => expressionHasJSX(element, aliasIsJSX));
96
+ return false;
97
+ }
98
+ function returnsJSX(fn) {
99
+ if (jsxReturns.has(fn)) return jsxReturns.get(fn);
100
+ const constants = [];
101
+ const returnValues = [];
102
+ function returned(value) {
103
+ if (!value || typeof value !== 'object') return false;
104
+ if (Array.isArray(value)) return value.some(returned);
105
+ if (
106
+ FUNCTIONS.has(value.type) ||
107
+ value.type === 'ClassDeclaration' ||
108
+ value.type === 'ClassExpression'
109
+ )
110
+ return false;
111
+ if (value.type === 'ReturnStatement') {
112
+ returnValues.push(value.argument);
113
+ return expressionHasJSX(value.argument);
114
+ }
115
+ if (value.type === 'VariableDeclaration' && value.kind === 'const')
116
+ constants.push(...value.declarations);
117
+ for (const key in value)
118
+ if (!SKIP_KEYS.has(key) && !key.startsWith('_octane') && returned(value[key])) return true;
119
+ return false;
120
+ }
121
+ let result = fn.body.type === 'BlockStatement' ? returned(fn.body) : expressionHasJSX(fn.body);
122
+ // Resolve local const aliases only when a mapper contains a JSX-valued
123
+ // initializer. Ordinary data maps pay no additional lexical analysis.
124
+ if (!result && constants.some((declaration) => expressionHasJSX(declaration.init))) {
125
+ const lexical = createLexicalAnalysis(fn);
126
+ const values = new Map();
127
+ for (const declaration of constants) {
128
+ if (declaration.id.type !== 'Identifier') continue;
129
+ const owner = lexical.resolveBinding(
130
+ lexical.nodeScopes.get(declaration.id),
131
+ declaration.id.name,
132
+ )?.scope;
133
+ if (!owner) continue;
134
+ let bindings = values.get(owner);
135
+ if (!bindings) values.set(owner, (bindings = new Map()));
136
+ bindings.set(declaration.id.name, declaration.init);
137
+ }
138
+ const active = new Set();
139
+ function aliasIsJSX(identifier) {
140
+ const owner = lexical.resolveBinding(
141
+ lexical.nodeScopes.get(identifier),
142
+ identifier.name,
143
+ )?.scope;
144
+ const initializer = values.get(owner)?.get(identifier.name);
145
+ if (!initializer || active.has(initializer)) return false;
146
+ active.add(initializer);
147
+ const found = expressionHasJSX(initializer, aliasIsJSX);
148
+ active.delete(initializer);
149
+ return found;
150
+ }
151
+ result = returnValues.some((value) => expressionHasJSX(value, aliasIsJSX));
152
+ }
153
+ jsxReturns.set(fn, result);
154
+ return result;
155
+ }
156
+ function callbackReturnsJSX(callback) {
157
+ if (callback?.kind === 'callback') return returnsJSX(callback.node);
158
+ return callback?.kind === 'callback-choice' && callback.values.some(callbackReturnsJSX);
159
+ }
160
+ function suppression(name, node) {
161
+ if (SUPPRESSION_PROPS.has(name))
162
+ add(
163
+ 'OCTANE_STRONG_SUPPRESSION_PROP',
164
+ node,
165
+ `Remove \`${name}\` in Strong mode. Fix the hydration mismatch or use native per-edit \`onInput\` handling instead of suppressing the diagnostic.`,
166
+ );
167
+ }
168
+ function spreadSuppressions(expression, scope) {
169
+ const value = unwrap(expression);
170
+ if (value?.type === 'ObjectExpression') {
171
+ for (const property of value.properties) {
172
+ if (property.type === 'SpreadElement') spreadSuppressions(property.argument, scope);
173
+ else
174
+ suppression(
175
+ property.computed
176
+ ? staticPrimitiveValue(property.key, scope)
177
+ : (property.key.name ?? property.key.value),
178
+ property.key,
179
+ );
180
+ }
181
+ } else if (value?.type === 'ConditionalExpression') {
182
+ spreadSuppressions(value.consequent, scope);
183
+ spreadSuppressions(value.alternate, scope);
184
+ }
185
+ }
186
+ function visit(node, scope) {
187
+ if (
188
+ node.type === 'ImportDeclaration' &&
189
+ (node.source?.value === 'octane' || node.source?.value === 'octane/server') &&
190
+ node.importKind !== 'type'
191
+ ) {
192
+ for (const specifier of node.specifiers ?? []) {
193
+ const name = specifier.imported?.name ?? specifier.imported?.value;
194
+ if (specifier.importKind !== 'type' && COMPAT_EXPORTS.has(name))
195
+ add(
196
+ 'OCTANE_STRONG_COMPAT_IMPORT',
197
+ specifier,
198
+ `\`${name}\` is a compatibility API. Strong mode uses Octane's normal scheduling and component semantics; remove this import.`,
199
+ );
200
+ }
201
+ } else if (node.type === 'VariableDeclaration') {
202
+ for (const declaration of node.declarations) {
203
+ const value = unwrap(declaration.init);
204
+ if (
205
+ declaration.id.type !== 'ObjectPattern' ||
206
+ value?.type !== 'Identifier' ||
207
+ resolve(scope, value.name)?.kind !== 'namespace'
208
+ )
209
+ continue;
210
+ for (const property of declaration.id.properties) {
211
+ if (property.type !== 'Property') continue;
212
+ const name = property.computed
213
+ ? staticPrimitiveValue(property.key, scope)
214
+ : (property.key.name ?? property.key.value);
215
+ if (COMPAT_EXPORTS.has(name))
216
+ add(
217
+ 'OCTANE_STRONG_COMPAT_IMPORT',
218
+ property.key,
219
+ `\`${name}\` is a compatibility API. Remove this access in Strong mode.`,
220
+ );
221
+ }
222
+ }
223
+ } else if (node.type === 'MemberExpression') {
224
+ const object = unwrap(node.object);
225
+ const name = node.computed ? staticPrimitiveValue(node.property, scope) : node.property?.name;
226
+ if (
227
+ object?.type === 'Identifier' &&
228
+ resolve(scope, object.name)?.kind === 'namespace' &&
229
+ COMPAT_EXPORTS.has(name)
230
+ )
231
+ add(
232
+ 'OCTANE_STRONG_COMPAT_IMPORT',
233
+ node,
234
+ `\`${name}\` is a compatibility API. Remove this access in Strong mode.`,
235
+ );
236
+ } else if (node.type === 'CallExpression') {
237
+ const callee = unwrap(node.callee);
238
+ const calleeBinding =
239
+ mayHaveHTML && callee?.type === 'Identifier' ? resolve(scope, callee.name) : null;
240
+ const receiver = callee?.type === 'MemberExpression' ? unwrap(callee.object) : null;
241
+ const name =
242
+ callee?.type === 'MemberExpression'
243
+ ? callee.computed
244
+ ? staticPrimitiveValue(callee.property, scope)
245
+ : callee.property?.name
246
+ : null;
247
+ const isElementFactory =
248
+ mayHaveHTML &&
249
+ ((calleeBinding?.kind === 'hook' && calleeBinding.hook === 'createElement') ||
250
+ (name === 'createElement' &&
251
+ receiver?.type === 'Identifier' &&
252
+ resolve(scope, receiver.name)?.kind === 'namespace'));
253
+ if (
254
+ isElementFactory &&
255
+ typeof staticPrimitiveValue(node.arguments[0], scope) === 'string' &&
256
+ domAt(node)
257
+ ) {
258
+ checkStrongHTMLValue(strongHTMLSpreadWriter(node.arguments[1]), (value) =>
259
+ add(STRONG_UNTRUSTED_HTML, value, STRONG_HTML_MESSAGE),
260
+ );
261
+ }
262
+ if (
263
+ templateDialect &&
264
+ callee?.type === 'MemberExpression' &&
265
+ name === 'map' &&
266
+ callbackReturnsJSX(callableValue(node.arguments?.[0], scope))
267
+ )
268
+ add(
269
+ 'OCTANE_STRONG_MAP_JSX',
270
+ node,
271
+ 'Use a keyed `@for` block to render a list instead of `.map()` returning JSX. Choose a stable item identity for its key.',
272
+ );
273
+ } else if (node.type === 'JSXOpeningElement') {
274
+ let root = node.name;
275
+ let member = null;
276
+ while (root?.type === 'JSXMemberExpression') {
277
+ member = root;
278
+ root = root.object;
279
+ }
280
+ if (
281
+ member &&
282
+ root?.type === 'JSXIdentifier' &&
283
+ resolve(scope, root.name)?.kind === 'namespace' &&
284
+ COMPAT_EXPORTS.has(member.property.name)
285
+ )
286
+ add(
287
+ 'OCTANE_STRONG_COMPAT_IMPORT',
288
+ member,
289
+ `\`${member.property.name}\` is a compatibility API. Remove this access in Strong mode.`,
290
+ );
291
+ if (node.name?.type !== 'JSXIdentifier' || !/^[a-z]/.test(node.name.name) || !domAt(node))
292
+ return;
293
+ for (const attribute of node.attributes ?? []) {
294
+ if (attribute.type === 'JSXSpreadAttribute') spreadSuppressions(attribute.argument, scope);
295
+ else suppression(attribute.name?.name, attribute.name);
296
+ }
297
+ }
298
+ }
299
+ return {
300
+ visit,
301
+ enterKey: checkKey,
302
+ exitKey() {},
303
+ finish() {
304
+ for (const { code, node, message } of pending) report(code, node, message);
305
+ },
306
+ };
307
+ }
@@ -0,0 +1,200 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Template IR + origin recording (`inspect: true`). Static template HTML is
3
+ // runtime DATA, not JavaScript syntax: the browser-facing `template(html)` ABI
4
+ // intentionally stays a string. The compiler represents that data as a
5
+ // structured tree of elements and typed data chunks until allocTemplate
6
+ // serializes the completed template exactly once. Child templates compose by
7
+ // transferring nodes, so no intermediate HTML is joined and no origin offsets
8
+ // need to be shifted during construction. Origin spans remain local to their
9
+ // chunk; the one serializer shifts them into final logical-HTML coordinates.
10
+ //
11
+ // Origin collection stays gated on `ctx.inspect`. The normal compile path pays
12
+ // for the template chunks (the structural representation replacing string
13
+ // concatenation) but not for source-span arrays.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * @typedef {{ start: number, end: number, srcStart: number, srcEnd: number,
18
+ * kind: 'tag-open' | 'tag-close' | 'attr-name' | 'attr-value' | 'text' }} TemplateOrigin
19
+ */
20
+
21
+ /**
22
+ * @typedef {'syntax' | 'tag-open' | 'tag-close' | 'attribute' | 'text' |
23
+ * 'anchor' | 'fragment-open' | 'fragment-close' | 'raw'} TemplatePartKind
24
+ */
25
+
26
+ /**
27
+ * @typedef {{
28
+ * type: 'TemplatePart',
29
+ * kind: TemplatePartKind,
30
+ * value: string,
31
+ * origins: TemplateOrigin[] | null,
32
+ * length: number,
33
+ * }} TemplatePart
34
+ */
35
+
36
+ /**
37
+ * @typedef {{
38
+ * type: 'TemplateElement',
39
+ * tag: string,
40
+ * namespace: string,
41
+ * synthetic: boolean,
42
+ * opening: TemplatePart,
43
+ * attributes: TemplatePart[],
44
+ * openingEnd: TemplatePart,
45
+ * children: TemplateNode[],
46
+ * closing: TemplatePart | null,
47
+ * length: number,
48
+ * }} TemplateElement
49
+ */
50
+
51
+ /** @typedef {TemplatePart | TemplateElement} TemplateNode */
52
+
53
+ /**
54
+ * @typedef {{
55
+ * type: 'Template',
56
+ * parts: TemplateNode[],
57
+ * length: number,
58
+ * }} TemplateIR
59
+ */
60
+
61
+ /** @returns {TemplateIR} */
62
+ export function createTemplateIr() {
63
+ return { type: 'Template', parts: [], length: 0 };
64
+ }
65
+
66
+ /**
67
+ * @param {string} value
68
+ * @param {TemplatePartKind} [kind]
69
+ * @param {TemplateOrigin[] | null} [origins]
70
+ * @returns {TemplatePart}
71
+ */
72
+ export function createTemplatePart(value, kind = 'raw', origins = null) {
73
+ return { type: 'TemplatePart', kind, value, origins, length: value.length };
74
+ }
75
+
76
+ /** @param {TemplateIR} template @param {TemplateNode} node */
77
+ export function appendTemplateNode(template, node) {
78
+ template.parts.push(node);
79
+ template.length += node.length;
80
+ }
81
+
82
+ /**
83
+ * @param {TemplateIR} template
84
+ * @param {string} value
85
+ * @param {TemplatePartKind} [kind]
86
+ * @param {TemplateOrigin[] | null} [origins]
87
+ */
88
+ export function appendTemplatePart(template, value, kind = 'raw', origins = null) {
89
+ if (value === '') return;
90
+ appendTemplateNode(template, createTemplatePart(value, kind, origins));
91
+ }
92
+
93
+ /** @param {TemplateIR} into @param {TemplateIR} child */
94
+ export function appendTemplateIr(into, child) {
95
+ if (child.length === 0) return;
96
+ for (const part of child.parts) into.parts.push(part);
97
+ into.length += child.length;
98
+ }
99
+
100
+ /**
101
+ * @param {string} value
102
+ * @param {TemplatePartKind} [kind]
103
+ * @param {TemplateOrigin[] | null} [origins]
104
+ * @returns {TemplateIR}
105
+ */
106
+ export function templatePart(value, kind = 'raw', origins = null) {
107
+ const template = createTemplateIr();
108
+ appendTemplatePart(template, value, kind, origins);
109
+ return template;
110
+ }
111
+
112
+ /**
113
+ * @param {string} tag
114
+ * @param {string} namespace
115
+ * @param {boolean} voidElement
116
+ * @param {TemplateIR} attributes
117
+ * @param {TemplateIR} children
118
+ * @param {TemplateOrigin[] | null} openOrigins
119
+ * @param {TemplateOrigin[] | null} closeOrigins
120
+ * @param {boolean} [synthetic]
121
+ * @returns {TemplateIR}
122
+ */
123
+ export function templateElement(
124
+ tag,
125
+ namespace,
126
+ voidElement,
127
+ attributes,
128
+ children,
129
+ openOrigins,
130
+ closeOrigins,
131
+ synthetic = false,
132
+ ) {
133
+ const opening = createTemplatePart(`<${tag}`, 'tag-open', openOrigins);
134
+ const openingEnd = createTemplatePart(voidElement ? '/>' : '>', 'syntax');
135
+ const closing = voidElement ? null : createTemplatePart(`</${tag}>`, 'tag-close', closeOrigins);
136
+ const node = {
137
+ type: 'TemplateElement',
138
+ tag,
139
+ namespace,
140
+ synthetic,
141
+ opening,
142
+ attributes: /** @type {TemplatePart[]} */ (attributes.parts),
143
+ openingEnd,
144
+ children: children.parts,
145
+ closing,
146
+ length:
147
+ opening.value.length +
148
+ attributes.length +
149
+ openingEnd.value.length +
150
+ children.length +
151
+ (closing === null ? 0 : closing.value.length),
152
+ };
153
+ const template = createTemplateIr();
154
+ appendTemplateNode(template, node);
155
+ return template;
156
+ }
157
+
158
+ /**
159
+ * Serialize the completed template once. The IR already contains the exact
160
+ * runtime bytes; this pass only joins them and converts chunk-local origins to
161
+ * final logical-HTML offsets.
162
+ *
163
+ * @param {TemplateIR} template
164
+ */
165
+ export function serializeTemplateIr(template) {
166
+ const values = [];
167
+ let origins = null;
168
+ let offset = 0;
169
+ const appendPart = (part) => {
170
+ values.push(part.value);
171
+ if (part.origins !== null) {
172
+ origins ??= [];
173
+ for (const origin of part.origins) {
174
+ origins.push({
175
+ start: origin.start + offset,
176
+ end: origin.end + offset,
177
+ srcStart: origin.srcStart,
178
+ srcEnd: origin.srcEnd,
179
+ kind: origin.kind,
180
+ });
181
+ }
182
+ }
183
+ offset += part.value.length;
184
+ };
185
+ const visit = (node) => {
186
+ if (node.type === 'TemplatePart') {
187
+ appendPart(node);
188
+ return;
189
+ }
190
+ appendPart(node.opening);
191
+ for (const attribute of node.attributes) appendPart(attribute);
192
+ appendPart(node.openingEnd);
193
+ for (const child of node.children) visit(child);
194
+ if (node.closing !== null) appendPart(node.closing);
195
+ };
196
+ for (const node of template.parts) {
197
+ visit(node);
198
+ }
199
+ return { html: values.join(''), origins };
200
+ }
@@ -23,6 +23,8 @@ export interface TextTypeProjectOptions {
23
23
  root?: string;
24
24
  /** The same renderer configuration used by the Octane compiler. */
25
25
  renderers?: unknown;
26
+ /** The same native attribute contracts used by runtime compilation. */
27
+ knownAttributeSpreads?: readonly import('./index.js').KnownAttributeSpread[];
26
28
  }
27
29
 
28
30
  export interface TextTypeProject {
@@ -421,6 +421,7 @@ export function createTextTypeProject(options) {
421
421
  try {
422
422
  compilation = compileToVolarMappings(source, rendererFilename(file, rendererRoot), {
423
423
  renderers,
424
+ knownAttributeSpreads: options.knownAttributeSpreads,
424
425
  });
425
426
  } catch {
426
427
  // Broken authored syntax is not evidence. Passing the authored text
@@ -495,6 +496,7 @@ export function createTextTypeProject(options) {
495
496
  options: program.getCompilerOptions(),
496
497
  references: loadConfig().projectReferences ?? [],
497
498
  renderers: renderers.signature,
499
+ knownAttributeSpreads: options.knownAttributeSpreads,
498
500
  rendererRoot,
499
501
  roots: roots(),
500
502
  inputs,
@@ -1,5 +1,5 @@
1
1
  import type { Plugin } from 'vite';
2
- import type { OctaneCssModuleConstants } from './index.js';
2
+ import type { KnownAttributeSpread, OctaneCssModuleConstants } from './index.js';
3
3
 
4
4
  export type { OctaneCssModuleConstants } from './index.js';
5
5
 
@@ -87,6 +87,8 @@ export interface OctaneVitePluginOptions {
87
87
  * @default false
88
88
  */
89
89
  strong?: boolean;
90
+ /** Trusted adapter output shapes; imported calls keep one evaluation per native host. */
91
+ knownAttributeSpreads?: readonly KnownAttributeSpread[];
90
92
  /**
91
93
  * @experimental Infer primitive child text from a TypeScript project in
92
94
  * one-shot production builds. Serve and watched builds retain syntax