octane 0.2.12 → 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.
- package/README.md +5 -0
- package/dist/behavior-root.d.ts +13 -3
- package/dist/behavior-root.js +28 -5
- package/dist/cjs/behavior-root.cjs +32 -6
- package/dist/cjs/class-names.cjs +49 -0
- package/dist/cjs/constants.cjs +9 -0
- package/dist/cjs/css.cjs +6 -27
- package/dist/cjs/data-encoding.cjs +160 -0
- package/dist/cjs/dom-binding-handoff.cjs +93 -0
- package/dist/cjs/dom-binding-protocol.cjs +115 -0
- package/dist/cjs/dom-binding-styles.cjs +292 -0
- package/dist/cjs/dom-bindings.cjs +700 -0
- package/dist/cjs/dom-focused-move.cjs +66 -0
- package/dist/cjs/dom-tables.cjs +4 -94
- package/dist/cjs/error-codes.client.generated.cjs +33 -0
- package/dist/cjs/error-codes.server.generated.cjs +19 -0
- package/dist/cjs/hydration/control-capture.cjs +244 -0
- package/dist/cjs/hydration/event-capture.cjs +125 -43
- package/dist/cjs/hydration/interaction-config.cjs +50 -2
- package/dist/cjs/hydration-markers.cjs +37 -0
- package/dist/cjs/independent-hydration-protocol.cjs +106 -0
- package/dist/cjs/index.cjs +6 -0
- package/dist/cjs/runtime.cjs +2791 -215
- package/dist/cjs/runtime.server.cjs +716 -76
- package/dist/cjs/server/early-signals.cjs +45 -0
- package/dist/cjs/server/index.cjs +44 -0
- package/dist/cjs/server/rpc-batch.cjs +149 -0
- package/dist/cjs/server/rpc-stream.cjs +146 -0
- package/dist/cjs/server/rpc.cjs +34 -15
- package/dist/cjs/server/signal-query-observation.cjs +158 -0
- package/dist/cjs/server/streamed-region.cjs +52 -0
- package/dist/cjs/server/streamed-signals.cjs +512 -0
- package/dist/cjs/server-call.cjs +103 -0
- package/dist/cjs/server-rpc-batch-client.cjs +141 -0
- package/dist/cjs/server-rpc-client.cjs +94 -27
- package/dist/cjs/server-rpc-protocol.cjs +141 -0
- package/dist/cjs/server-rpc-stream-client.cjs +118 -0
- package/dist/cjs/signals/actions.cjs +474 -0
- package/dist/cjs/signals/client.cjs +4 -1
- package/dist/cjs/signals/computations.cjs +460 -0
- package/dist/cjs/signals/control-binding.cjs +368 -0
- package/dist/cjs/signals/derived-facade.cjs +50 -0
- package/dist/cjs/signals/document-owner.cjs +71 -0
- package/dist/cjs/signals/early-values.cjs +155 -0
- package/dist/cjs/signals/encoding.cjs +6 -95
- package/dist/cjs/signals/engine.cjs +276 -12
- package/dist/cjs/signals/errors.cjs +18 -0
- package/dist/cjs/signals/facade.cjs +477 -0
- package/dist/cjs/signals/graph.cjs +163 -20
- package/dist/cjs/signals/handle-protocol.cjs +36 -0
- package/dist/cjs/signals/index.cjs +81 -2
- package/dist/cjs/signals/native-read-client.cjs +10 -0
- package/dist/cjs/signals/owner-context.cjs +112 -0
- package/dist/cjs/signals/query-attempt-observer.cjs +68 -0
- package/dist/cjs/signals/query-facade.cjs +72 -0
- package/dist/cjs/signals/read-protocol.cjs +32 -0
- package/dist/cjs/signals/requests.cjs +287 -14
- package/dist/cjs/signals/scalar-computations.cjs +76 -0
- package/dist/cjs/signals/server.cjs +4 -1
- package/dist/cjs/signals/transition-candidate.cjs +567 -0
- package/dist/cjs/signals/transition-state.cjs +106 -0
- package/dist/cjs/signals/types.cjs +50 -0
- package/dist/cjs/stream-protocol.cjs +3 -0
- package/dist/cjs/streamed-signals-protocol.cjs +159 -0
- package/dist/cjs/style-values.cjs +122 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/class-names.d.ts +5 -0
- package/dist/class-names.js +25 -0
- package/dist/compiler/bundler.js +34 -6
- package/dist/compiler/compile.js +2234 -559
- package/dist/compiler/dom-binding-program.js +935 -0
- package/dist/compiler/dom-binding-request.js +57 -0
- package/dist/compiler/dom-bindings.js +2245 -0
- package/dist/compiler/hook-deps.js +26 -0
- package/dist/compiler/hydrate-boundaries.js +358 -15
- package/dist/compiler/index.d.ts +38 -1
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/native-attribute-reads.js +127 -0
- package/dist/compiler/native-read-diagnostics.js +48 -9
- package/dist/compiler/native-read-types.js +55 -32
- package/dist/compiler/server-context.js +118 -0
- package/dist/compiler/signal-attempt-reads.js +479 -0
- package/dist/compiler/signal-declarations.js +557 -0
- package/dist/compiler/signal-start-reads.js +413 -0
- package/dist/compiler/slot-hooks.js +57 -10
- package/dist/compiler/template-ir.js +200 -0
- package/dist/compiler/typescript.d.ts +2 -0
- package/dist/compiler/typescript.js +2 -0
- package/dist/compiler/vite.d.ts +3 -1
- package/dist/compiler/vite.js +122 -4
- package/dist/compiler/volar.d.ts +6 -1
- package/dist/compiler/volar.js +674 -143
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +10 -0
- package/dist/css.d.ts +2 -5
- package/dist/css.js +2 -23
- package/dist/data-encoding.d.ts +9 -0
- package/dist/data-encoding.js +134 -0
- package/dist/dom-binding-classes.d.ts +15 -0
- package/dist/dom-binding-classes.js +129 -0
- package/dist/dom-binding-controls.d.ts +2 -0
- package/dist/dom-binding-controls.js +6 -0
- package/dist/dom-binding-handoff.d.ts +55 -0
- package/dist/dom-binding-handoff.js +62 -0
- package/dist/dom-binding-program.d.ts +300 -0
- package/dist/dom-binding-program.js +1625 -0
- package/dist/dom-binding-projections.d.ts +15 -0
- package/dist/dom-binding-projections.js +187 -0
- package/dist/dom-binding-protocol.d.ts +13 -0
- package/dist/dom-binding-protocol.js +88 -0
- package/dist/dom-binding-signals.d.ts +21 -0
- package/dist/dom-binding-signals.js +109 -0
- package/dist/dom-binding-styles.d.ts +17 -0
- package/dist/dom-binding-styles.js +272 -0
- package/dist/dom-bindings.d.ts +89 -0
- package/dist/dom-bindings.js +670 -0
- package/dist/dom-focused-move.d.ts +6 -0
- package/dist/dom-focused-move.js +42 -0
- package/dist/dom-tables.d.ts +1 -3
- package/dist/dom-tables.js +1 -91
- package/dist/error-codes.client.generated.d.ts +9 -0
- package/dist/error-codes.client.generated.js +33 -0
- package/dist/error-codes.server.generated.d.ts +5 -0
- package/dist/error-codes.server.generated.js +19 -0
- package/dist/hydration/control-capture.d.ts +62 -0
- package/dist/hydration/control-capture.js +223 -0
- package/dist/hydration/document-lifecycle.d.ts +23 -0
- package/dist/hydration/document-lifecycle.js +80 -0
- package/dist/hydration/event-capture.d.ts +20 -1
- package/dist/hydration/event-capture.js +128 -42
- package/dist/hydration/independent-island.d.ts +45 -0
- package/dist/hydration/independent-island.js +233 -0
- package/dist/hydration/index.d.ts +8 -1
- package/dist/hydration/index.js +55 -1
- package/dist/hydration/interaction-config.d.ts +5 -0
- package/dist/hydration/interaction-config.js +44 -1
- package/dist/hydration/stream-delivery.d.ts +32 -0
- package/dist/hydration/stream-delivery.js +333 -0
- package/dist/hydration/stream-receiver.d.ts +26 -0
- package/dist/hydration/stream-receiver.js +204 -0
- package/dist/hydration/stream-result-receiver.d.ts +70 -0
- package/dist/hydration/stream-result-receiver.js +246 -0
- package/dist/hydration/streamed-signals.d.ts +44 -0
- package/dist/hydration/streamed-signals.js +186 -0
- package/dist/hydration/types.d.ts +6 -0
- package/dist/hydration-markers.d.ts +7 -0
- package/dist/hydration-markers.js +10 -0
- package/dist/independent-hydration-protocol.d.ts +31 -0
- package/dist/independent-hydration-protocol.js +80 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/internal/signal-read.d.ts +1 -0
- package/dist/internal/signal-read.js +4 -0
- package/dist/jsx-runtime.d.ts +45 -6
- package/dist/public-types.d.ts +15 -1
- package/dist/runtime.d.ts +259 -21
- package/dist/runtime.js +2812 -220
- package/dist/runtime.server.d.ts +86 -6
- package/dist/runtime.server.js +712 -76
- package/dist/server/early-signals.d.ts +14 -0
- package/dist/server/early-signals.js +34 -0
- package/dist/server/index.d.ts +10 -2
- package/dist/server/index.js +48 -1
- package/dist/server/rpc-batch.d.ts +17 -0
- package/dist/server/rpc-batch.js +129 -0
- package/dist/server/rpc-stream.d.ts +9 -0
- package/dist/server/rpc-stream.js +125 -0
- package/dist/server/rpc.d.ts +5 -1
- package/dist/server/rpc.js +37 -13
- package/dist/server/signal-query-observation.d.ts +3 -0
- package/dist/server/signal-query-observation.js +134 -0
- package/dist/server/streamed-region.d.ts +15 -0
- package/dist/server/streamed-region.js +30 -0
- package/dist/server/streamed-signals.d.ts +45 -0
- package/dist/server/streamed-signals.js +488 -0
- package/dist/server-call.d.ts +41 -0
- package/dist/server-call.js +75 -0
- package/dist/server-rpc-batch-client.d.ts +7 -0
- package/dist/server-rpc-batch-client.js +119 -0
- package/dist/server-rpc-client.d.ts +3 -1
- package/dist/server-rpc-client.js +99 -27
- package/dist/server-rpc-protocol.d.ts +47 -0
- package/dist/server-rpc-protocol.js +109 -0
- package/dist/server-rpc-stream-client.d.ts +5 -0
- package/dist/server-rpc-stream-client.js +98 -0
- package/dist/signals/actions.d.ts +12 -0
- package/dist/signals/actions.js +466 -0
- package/dist/signals/client.d.ts +2 -0
- package/dist/signals/client.js +1 -0
- package/dist/signals/computations.d.ts +37 -0
- package/dist/signals/computations.js +455 -0
- package/dist/signals/control-binding.d.ts +31 -0
- package/dist/signals/control-binding.js +353 -0
- package/dist/signals/derived-facade.d.ts +3 -0
- package/dist/signals/derived-facade.js +25 -0
- package/dist/signals/document-owner.d.ts +10 -0
- package/dist/signals/document-owner.js +43 -0
- package/dist/signals/early-values.d.ts +24 -0
- package/dist/signals/early-values.js +124 -0
- package/dist/signals/encoding.d.ts +1 -3
- package/dist/signals/encoding.js +3 -93
- package/dist/signals/engine.d.ts +53 -4
- package/dist/signals/engine.js +275 -16
- package/dist/signals/errors.d.ts +7 -0
- package/dist/signals/errors.js +16 -0
- package/dist/signals/facade.d.ts +74 -0
- package/dist/signals/facade.js +452 -0
- package/dist/signals/graph.d.ts +50 -5
- package/dist/signals/graph.js +170 -19
- package/dist/signals/handle-protocol.d.ts +4 -0
- package/dist/signals/handle-protocol.js +11 -0
- package/dist/signals/index.d.ts +12 -3
- package/dist/signals/index.js +71 -2
- package/dist/signals/native-read-client.d.ts +3 -0
- package/dist/signals/native-read-client.js +10 -0
- package/dist/signals/owner-context.d.ts +22 -0
- package/dist/signals/owner-context.js +77 -0
- package/dist/signals/query-attempt-observer.d.ts +49 -0
- package/dist/signals/query-attempt-observer.js +41 -0
- package/dist/signals/query-facade.d.ts +3 -0
- package/dist/signals/query-facade.js +49 -0
- package/dist/signals/read-protocol.d.ts +32 -3
- package/dist/signals/read-protocol.js +26 -0
- package/dist/signals/requests.d.ts +38 -5
- package/dist/signals/requests.js +298 -15
- package/dist/signals/scalar-computations.d.ts +2 -0
- package/dist/signals/scalar-computations.js +60 -0
- package/dist/signals/server.d.ts +2 -0
- package/dist/signals/server.js +1 -0
- package/dist/signals/transition-candidate.d.ts +71 -0
- package/dist/signals/transition-candidate.js +557 -0
- package/dist/signals/transition-state.d.ts +51 -0
- package/dist/signals/transition-state.js +70 -0
- package/dist/signals/types.d.ts +95 -5
- package/dist/signals/types.js +20 -0
- package/dist/stream-protocol.d.ts +0 -5
- package/dist/stream-protocol.js +3 -0
- package/dist/streamed-signals-protocol.d.ts +60 -0
- package/dist/streamed-signals-protocol.js +130 -0
- package/dist/style-values.d.ts +3 -0
- package/dist/style-values.js +96 -0
- package/dist/version.js +1 -1
- package/package.json +37 -1
|
@@ -0,0 +1,2245 @@
|
|
|
1
|
+
/** Explicit presentation views share authored SSR markup with a renderer-free program. */
|
|
2
|
+
import { builders as b, strongHash } from '@tsrx/core';
|
|
3
|
+
import { createLexicalAnalysis } from './compile-universal.js';
|
|
4
|
+
import { inheritHookMemoOrigin } from './inline-hook-memo.js';
|
|
5
|
+
import {
|
|
6
|
+
ATTRIBUTE_ALIASES,
|
|
7
|
+
BOOLEAN_ATTR_PROPS,
|
|
8
|
+
MUST_USE_PROPERTY_PROPS,
|
|
9
|
+
POSITIVE_NUMERIC_ATTR_PROPS,
|
|
10
|
+
SVG_ONLY_TAGS,
|
|
11
|
+
isUnitlessStyleProp,
|
|
12
|
+
isEnumeratedBooleanAttr,
|
|
13
|
+
hyphenateStyleName,
|
|
14
|
+
} from '../dom-tables.js';
|
|
15
|
+
import {
|
|
16
|
+
invalidHtmlNestingWithAncestor,
|
|
17
|
+
invalidHtmlNestingWithParent,
|
|
18
|
+
} from '../html-tree-validation.js';
|
|
19
|
+
import { shouldSanitizeURLAttribute } from '../sanitize-url.js';
|
|
20
|
+
import { needsBindingProgram, planBindingProgram } from './dom-binding-program.js';
|
|
21
|
+
import {
|
|
22
|
+
parseDomBindingRequest,
|
|
23
|
+
DOM_BINDINGS_QUERY,
|
|
24
|
+
DOM_BINDINGS_MOUNT_QUERY,
|
|
25
|
+
} from './dom-binding-request.js';
|
|
26
|
+
|
|
27
|
+
export { DOM_BINDINGS_QUERY, DOM_BINDINGS_MOUNT_QUERY } from './dom-binding-request.js';
|
|
28
|
+
export const DOM_BINDING_COMPILER_ABI_VERSION = 1;
|
|
29
|
+
const DIRECTIVE = 'use dom bindings';
|
|
30
|
+
const MARKER = 'data-octane-bindings';
|
|
31
|
+
const NODE_MARKER = 'data-octane-binding-node';
|
|
32
|
+
const SKIP = new Set([
|
|
33
|
+
'loc',
|
|
34
|
+
'start',
|
|
35
|
+
'end',
|
|
36
|
+
'range',
|
|
37
|
+
'metadata',
|
|
38
|
+
'parent',
|
|
39
|
+
'comments',
|
|
40
|
+
'leadingComments',
|
|
41
|
+
'trailingComments',
|
|
42
|
+
'innerComments',
|
|
43
|
+
'typeAnnotation',
|
|
44
|
+
'typeParameters',
|
|
45
|
+
]);
|
|
46
|
+
const UNWRAP = new Set([
|
|
47
|
+
'TSAsExpression',
|
|
48
|
+
'TSTypeAssertion',
|
|
49
|
+
'TSNonNullExpression',
|
|
50
|
+
'TSSatisfiesExpression',
|
|
51
|
+
'ParenthesizedExpression',
|
|
52
|
+
]);
|
|
53
|
+
const FORBIDDEN_TAGS = new Set([
|
|
54
|
+
'script',
|
|
55
|
+
'style',
|
|
56
|
+
'template',
|
|
57
|
+
'noscript',
|
|
58
|
+
'title',
|
|
59
|
+
'desc',
|
|
60
|
+
'meta',
|
|
61
|
+
'link',
|
|
62
|
+
'base',
|
|
63
|
+
'head',
|
|
64
|
+
'math',
|
|
65
|
+
'foreignObject',
|
|
66
|
+
'iframe',
|
|
67
|
+
'object',
|
|
68
|
+
'embed',
|
|
69
|
+
'plaintext',
|
|
70
|
+
'xmp',
|
|
71
|
+
]);
|
|
72
|
+
const FORBIDDEN_ATTRS = new Set([
|
|
73
|
+
'ref',
|
|
74
|
+
'key',
|
|
75
|
+
'children',
|
|
76
|
+
'dangerouslysetinnerhtml',
|
|
77
|
+
'innerhtml',
|
|
78
|
+
'innertext',
|
|
79
|
+
'textcontent',
|
|
80
|
+
'suppresshydrationwarning',
|
|
81
|
+
'suppressnativechangewarning',
|
|
82
|
+
'is',
|
|
83
|
+
'slot',
|
|
84
|
+
'download',
|
|
85
|
+
'capture',
|
|
86
|
+
'rowspan',
|
|
87
|
+
'start',
|
|
88
|
+
MARKER,
|
|
89
|
+
NODE_MARKER,
|
|
90
|
+
]);
|
|
91
|
+
// Initial form state and submission identity stay with normal SSR/native owners.
|
|
92
|
+
// A presentation binding must not reset a user's edit or change its form owner.
|
|
93
|
+
const EXTERNAL_ATTRS = new Set([
|
|
94
|
+
'autofocus',
|
|
95
|
+
'value',
|
|
96
|
+
'checked',
|
|
97
|
+
'defaultvalue',
|
|
98
|
+
'defaultchecked',
|
|
99
|
+
'selected',
|
|
100
|
+
'action',
|
|
101
|
+
'formaction',
|
|
102
|
+
'formmethod',
|
|
103
|
+
'formenctype',
|
|
104
|
+
'formtarget',
|
|
105
|
+
]);
|
|
106
|
+
const FORM_HOSTS = new Set(['input', 'textarea', 'select', 'button', 'option', 'optgroup', 'form']);
|
|
107
|
+
|
|
108
|
+
function externalAttribute(tag, name) {
|
|
109
|
+
return (
|
|
110
|
+
(EXTERNAL_ATTRS.has(name) && !(tag === 'button' && name === 'value')) ||
|
|
111
|
+
(MUST_USE_PROPERTY_PROPS.has(name) && !(tag === 'button' && name === 'value')) ||
|
|
112
|
+
POSITIVE_NUMERIC_ATTR_PROPS.has(name) ||
|
|
113
|
+
(FORM_HOSTS.has(tag) &&
|
|
114
|
+
(name === 'name' ||
|
|
115
|
+
name === 'id' ||
|
|
116
|
+
name === 'form' ||
|
|
117
|
+
name === 'multiple' ||
|
|
118
|
+
(tag === 'input' && (name === 'type' || name === 'list')) ||
|
|
119
|
+
(tag === 'form' && ['id', 'method', 'enctype', 'target', 'accept-charset'].includes(name))))
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bindingKind(tag, name) {
|
|
124
|
+
return name === 'class'
|
|
125
|
+
? 'class'
|
|
126
|
+
: name === 'style'
|
|
127
|
+
? 'styleAttribute'
|
|
128
|
+
: shouldSanitizeURLAttribute(tag, name)
|
|
129
|
+
? 'url'
|
|
130
|
+
: name.startsWith('aria-') ||
|
|
131
|
+
name.startsWith('data-') ||
|
|
132
|
+
isEnumeratedBooleanAttr(name.toLowerCase())
|
|
133
|
+
? 'aria'
|
|
134
|
+
: BOOLEAN_ATTR_PROPS.has(name.toLowerCase())
|
|
135
|
+
? 'boolean'
|
|
136
|
+
: 'attr';
|
|
137
|
+
}
|
|
138
|
+
// URL sinks with an existing native sanitizer can be projected. Other URL
|
|
139
|
+
// shapes remain static or externally owned rather than inventing a policy.
|
|
140
|
+
const URL_ATTRS = new Set([
|
|
141
|
+
'src',
|
|
142
|
+
'href',
|
|
143
|
+
'xlink:href',
|
|
144
|
+
'srcset',
|
|
145
|
+
'imagesrcset',
|
|
146
|
+
'poster',
|
|
147
|
+
'cite',
|
|
148
|
+
'background',
|
|
149
|
+
'data',
|
|
150
|
+
'ping',
|
|
151
|
+
'archive',
|
|
152
|
+
'codebase',
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
function error(filename, node, message) {
|
|
156
|
+
const error = new Error(
|
|
157
|
+
`Octane DOM bindings (${filename}:${node?.loc?.start?.line ?? 1}): ${message}`,
|
|
158
|
+
);
|
|
159
|
+
error.code = 'OCTANE_DOM_BINDINGS';
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function domBindingExportFromId(id) {
|
|
164
|
+
return parseDomBindingRequest(id)?.exportName ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function walk(node, visit, parent = null, key = null) {
|
|
168
|
+
if (!node || typeof node !== 'object') return;
|
|
169
|
+
if (Array.isArray(node)) {
|
|
170
|
+
for (const child of node) walk(child, visit, parent, key);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (typeof node.type !== 'string') return;
|
|
174
|
+
if (visit(node, parent, key) === false) return;
|
|
175
|
+
for (const [childKey, child] of Object.entries(node)) {
|
|
176
|
+
if (!SKIP.has(childKey)) walk(child, visit, node, childKey);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function mapCow(node, replacements) {
|
|
181
|
+
if (!node || typeof node !== 'object') return node;
|
|
182
|
+
if (replacements.has(node)) return replacements.get(node);
|
|
183
|
+
if (Array.isArray(node)) {
|
|
184
|
+
let result = null;
|
|
185
|
+
for (let index = 0; index < node.length; index++) {
|
|
186
|
+
const next = mapCow(node[index], replacements);
|
|
187
|
+
if (result === null && next !== node[index]) result = node.slice(0, index);
|
|
188
|
+
if (result !== null && next !== null) result.push(next);
|
|
189
|
+
}
|
|
190
|
+
return result ?? node;
|
|
191
|
+
}
|
|
192
|
+
let result = null;
|
|
193
|
+
for (const [key, child] of Object.entries(node)) {
|
|
194
|
+
if (SKIP.has(key)) continue;
|
|
195
|
+
const next = mapCow(child, replacements);
|
|
196
|
+
if (next !== child) {
|
|
197
|
+
result ??= { ...node };
|
|
198
|
+
result[key] = next;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return result ?? node;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function unwrap(node) {
|
|
205
|
+
while (UNWRAP.has(node?.type)) node = node.expression;
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function statements(fn) {
|
|
210
|
+
return Array.isArray(fn.body?.body) ? fn.body.body : [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isDirective(node) {
|
|
214
|
+
return node?.type === 'ExpressionStatement' && node.expression?.value === DIRECTIVE;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function attrName(attr) {
|
|
218
|
+
const name = attr.name;
|
|
219
|
+
return name?.type === 'JSXNamespacedName'
|
|
220
|
+
? `${name.namespace.name}:${name.name.name}`
|
|
221
|
+
: (name?.name ?? name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function attrValueForBinding(attr) {
|
|
225
|
+
return attr.value === null
|
|
226
|
+
? b.literal(true)
|
|
227
|
+
: unwrap(attr.value?.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function importedBindings(ast) {
|
|
231
|
+
const imports = new Map();
|
|
232
|
+
for (const declaration of ast.body) {
|
|
233
|
+
if (declaration.type !== 'ImportDeclaration' || declaration.importKind === 'type') continue;
|
|
234
|
+
for (const specifier of declaration.specifiers ?? []) {
|
|
235
|
+
if (specifier.importKind === 'type') continue;
|
|
236
|
+
imports.set(specifier.local.name, {
|
|
237
|
+
declaration,
|
|
238
|
+
specifier,
|
|
239
|
+
source: declaration.source.value,
|
|
240
|
+
imported: specifier.imported?.name ?? specifier.imported?.value ?? null,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return imports;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function importedProjectionCall(node, imports, lexical, parameterScope) {
|
|
248
|
+
const callee = unwrap(node?.callee);
|
|
249
|
+
let importedRoot = callee;
|
|
250
|
+
while (importedRoot?.type === 'MemberExpression' && !importedRoot.computed)
|
|
251
|
+
importedRoot = importedRoot.object;
|
|
252
|
+
const namespace =
|
|
253
|
+
importedRoot !== callee &&
|
|
254
|
+
importedRoot?.type === 'Identifier' &&
|
|
255
|
+
imports.get(importedRoot.name)?.specifier.type === 'ImportNamespaceSpecifier'
|
|
256
|
+
? importedRoot
|
|
257
|
+
: null;
|
|
258
|
+
const reference = namespace ?? callee;
|
|
259
|
+
const imported = reference?.type === 'Identifier' ? imports.get(reference.name) : null;
|
|
260
|
+
return (
|
|
261
|
+
imported != null &&
|
|
262
|
+
lexical.resolveBinding(lexical.nodeScopes.get(reference) ?? parameterScope, reference.name)
|
|
263
|
+
?.scope === lexical.rootScope &&
|
|
264
|
+
!/^use(?:[A-Z]|$)/.test(namespace ? callee.property.name : (imported.imported ?? '')) &&
|
|
265
|
+
imported.source !== 'octane' &&
|
|
266
|
+
!imported.source.startsWith('octane/')
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function nativeProjectionReference(node, imports, lexical, parameterScope) {
|
|
271
|
+
if (
|
|
272
|
+
node.type !== 'CallExpression' ||
|
|
273
|
+
node.optional ||
|
|
274
|
+
node.arguments.some((argument) => argument.type === 'SpreadElement')
|
|
275
|
+
)
|
|
276
|
+
return null;
|
|
277
|
+
const callee = unwrap(node.callee);
|
|
278
|
+
const member = callee?.type === 'MemberExpression' && !callee.computed && !callee.optional;
|
|
279
|
+
const reference = member ? unwrap(callee.object) : callee;
|
|
280
|
+
if (reference?.type !== 'Identifier') return null;
|
|
281
|
+
const binding = lexical.resolveBinding(
|
|
282
|
+
lexical.nodeScopes.get(reference) ?? parameterScope,
|
|
283
|
+
reference.name,
|
|
284
|
+
);
|
|
285
|
+
if (
|
|
286
|
+
binding === null &&
|
|
287
|
+
((!member && reference.name === 'String' && node.arguments.length <= 1) ||
|
|
288
|
+
(member && reference.name === 'Math' && callee.property.name === 'min'))
|
|
289
|
+
)
|
|
290
|
+
return reference;
|
|
291
|
+
const imported = imports.get(reference.name);
|
|
292
|
+
return binding?.scope === lexical.rootScope &&
|
|
293
|
+
imported?.source === 'octane/signals' &&
|
|
294
|
+
node.arguments.length === 1 &&
|
|
295
|
+
(member
|
|
296
|
+
? imported.specifier.type === 'ImportNamespaceSpecifier' &&
|
|
297
|
+
callee.property.name === 'isSignalHandle'
|
|
298
|
+
: imported.imported === 'isSignalHandle')
|
|
299
|
+
? reference
|
|
300
|
+
: null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function projectionFactoryConfiguration(expression, imports, lexical, parameterScope) {
|
|
304
|
+
const value = unwrap(expression);
|
|
305
|
+
return value?.type === 'CallExpression' &&
|
|
306
|
+
!value.optional &&
|
|
307
|
+
value.arguments.length === 1 &&
|
|
308
|
+
unwrap(value.arguments[0])?.type === 'ObjectExpression' &&
|
|
309
|
+
importedProjectionCall(value, imports, lexical, parameterScope)
|
|
310
|
+
? unwrap(value.arguments[0])
|
|
311
|
+
: null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function expressionProjection(node) {
|
|
315
|
+
return (
|
|
316
|
+
node?.type === 'ArrowFunctionExpression' &&
|
|
317
|
+
!node.async &&
|
|
318
|
+
node.body.type !== 'BlockStatement' &&
|
|
319
|
+
node.params.every((param) => param.type === 'Identifier')
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// The directive asserts imported projections are pure. Obvious writes, ambient
|
|
324
|
+
// reads, hooks and arbitrary calls remain diagnostics instead of silent one-shot work.
|
|
325
|
+
function assertProjection(
|
|
326
|
+
expression,
|
|
327
|
+
filename,
|
|
328
|
+
imports,
|
|
329
|
+
lexical,
|
|
330
|
+
parameterScope,
|
|
331
|
+
constants = new Set(),
|
|
332
|
+
) {
|
|
333
|
+
const configuration =
|
|
334
|
+
constants.size > 0
|
|
335
|
+
? projectionFactoryConfiguration(expression, imports, lexical, parameterScope)
|
|
336
|
+
: null;
|
|
337
|
+
const configurationArrows = configuration ? new Set() : null;
|
|
338
|
+
const collectConfiguration = (object) => {
|
|
339
|
+
for (const property of object.properties) {
|
|
340
|
+
if (
|
|
341
|
+
property.type !== 'Property' ||
|
|
342
|
+
property.kind !== 'init' ||
|
|
343
|
+
property.method ||
|
|
344
|
+
property.computed
|
|
345
|
+
)
|
|
346
|
+
continue;
|
|
347
|
+
const value = unwrap(property.value);
|
|
348
|
+
if (expressionProjection(value)) configurationArrows.add(value);
|
|
349
|
+
else if (value?.type === 'ObjectExpression') collectConfiguration(value);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
if (configuration) collectConfiguration(configuration);
|
|
353
|
+
const factoryProjection = (callee) => {
|
|
354
|
+
const path = [];
|
|
355
|
+
let receiver = callee;
|
|
356
|
+
while (receiver?.type === 'MemberExpression' && !receiver.computed && !receiver.optional) {
|
|
357
|
+
path.unshift(receiver.property.name);
|
|
358
|
+
receiver = unwrap(receiver.object);
|
|
359
|
+
}
|
|
360
|
+
if (
|
|
361
|
+
path.length === 0 ||
|
|
362
|
+
/^use(?:[A-Z]|$)/.test(path.at(-1)) ||
|
|
363
|
+
receiver?.type !== 'Identifier' ||
|
|
364
|
+
lexical.resolveBinding(lexical.nodeScopes.get(receiver) ?? parameterScope, receiver.name)
|
|
365
|
+
?.scope !== lexical.rootScope
|
|
366
|
+
)
|
|
367
|
+
return false;
|
|
368
|
+
let value = projectionFactoryConfiguration(
|
|
369
|
+
lexical.domBindingConstants?.get(receiver.name)?.init,
|
|
370
|
+
imports,
|
|
371
|
+
lexical,
|
|
372
|
+
parameterScope,
|
|
373
|
+
);
|
|
374
|
+
for (const name of path) {
|
|
375
|
+
if (
|
|
376
|
+
name === '__proto__' ||
|
|
377
|
+
value?.type !== 'ObjectExpression' ||
|
|
378
|
+
value.properties.some((property) => property.type !== 'Property' || property.computed)
|
|
379
|
+
)
|
|
380
|
+
return false;
|
|
381
|
+
const members = value.properties.filter(
|
|
382
|
+
(property) => (property.key.name ?? property.key.value) === name,
|
|
383
|
+
);
|
|
384
|
+
if (members.length !== 1 || members[0].kind !== 'init' || members[0].method) return false;
|
|
385
|
+
value = unwrap(members[0].value);
|
|
386
|
+
}
|
|
387
|
+
// The directive requires the provider to preserve its pure projections;
|
|
388
|
+
// this is an author/provider assertion, not an analysis of imported code.
|
|
389
|
+
// A result grants no arbitrary methods: the exact static member must come
|
|
390
|
+
// from a checked projection in the immutable literal configuration.
|
|
391
|
+
return expressionProjection(value);
|
|
392
|
+
};
|
|
393
|
+
// Like imported projections, these native reads rely on the directive's
|
|
394
|
+
// immutable-props/import contract. A string result cast does not admit arbitrary
|
|
395
|
+
// methods: every call in a chain must independently satisfy this boundary.
|
|
396
|
+
const readReceiver = (input) => {
|
|
397
|
+
let receiver = unwrap(input);
|
|
398
|
+
if (receiver?.type === 'ChainExpression') return readReceiver(receiver.expression);
|
|
399
|
+
if (receiver?.type === 'LogicalExpression')
|
|
400
|
+
return readReceiver(receiver.left) && readReceiver(receiver.right);
|
|
401
|
+
if (receiver?.type === 'ConditionalExpression')
|
|
402
|
+
return readReceiver(receiver.consequent) && readReceiver(receiver.alternate);
|
|
403
|
+
if (receiver?.type === 'Literal') return typeof receiver.value === 'string';
|
|
404
|
+
if (['CallExpression', 'OptionalCallExpression'].includes(receiver?.type))
|
|
405
|
+
return readMethod(receiver);
|
|
406
|
+
while (['MemberExpression', 'OptionalMemberExpression'].includes(receiver?.type))
|
|
407
|
+
receiver = unwrap(receiver.object);
|
|
408
|
+
if (receiver?.type !== 'Identifier') return false;
|
|
409
|
+
const binding = lexical.resolveBinding(
|
|
410
|
+
lexical.nodeScopes.get(receiver) ?? parameterScope,
|
|
411
|
+
receiver.name,
|
|
412
|
+
);
|
|
413
|
+
return binding != null && (binding.scope !== lexical.rootScope || imports.has(receiver.name));
|
|
414
|
+
};
|
|
415
|
+
const readMethod = (node) => {
|
|
416
|
+
const callee = unwrap(node?.callee);
|
|
417
|
+
const sampled =
|
|
418
|
+
(node?.type === 'CallExpression' || node?.type === 'OptionalCallExpression') &&
|
|
419
|
+
node.arguments.length === 0 &&
|
|
420
|
+
(callee?.computed ? callee.property?.value === 'get' : callee?.property?.name === 'get');
|
|
421
|
+
if (
|
|
422
|
+
!['MemberExpression', 'OptionalMemberExpression'].includes(callee?.type) ||
|
|
423
|
+
(!sampled &&
|
|
424
|
+
(callee.computed ||
|
|
425
|
+
!['includes', 'find', 'trim', 'slice', 'toUpperCase'].includes(callee.property.name)))
|
|
426
|
+
)
|
|
427
|
+
return false;
|
|
428
|
+
return readReceiver(callee.object);
|
|
429
|
+
};
|
|
430
|
+
const nativeReferences = new Set();
|
|
431
|
+
walk(expression, (node, parent, key) => {
|
|
432
|
+
if (node.type.startsWith('TS') && !UNWRAP.has(node.type)) return false;
|
|
433
|
+
if (
|
|
434
|
+
[
|
|
435
|
+
'AssignmentExpression',
|
|
436
|
+
'UpdateExpression',
|
|
437
|
+
'AwaitExpression',
|
|
438
|
+
'YieldExpression',
|
|
439
|
+
'NewExpression',
|
|
440
|
+
'TaggedTemplateExpression',
|
|
441
|
+
].includes(node.type) ||
|
|
442
|
+
(configurationArrows !== null &&
|
|
443
|
+
['ThisExpression', 'Super', 'MetaProperty', 'ImportExpression'].includes(node.type)) ||
|
|
444
|
+
(['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) &&
|
|
445
|
+
!configurationArrows?.has(node) &&
|
|
446
|
+
!(
|
|
447
|
+
key === 'arguments' &&
|
|
448
|
+
readMethod(parent) &&
|
|
449
|
+
['includes', 'find'].includes(unwrap(parent.callee)?.property?.name) &&
|
|
450
|
+
node.body.type !== 'BlockStatement' &&
|
|
451
|
+
!node.async &&
|
|
452
|
+
node.params.every((param) => param.type === 'Identifier')
|
|
453
|
+
)) ||
|
|
454
|
+
(node.type === 'UnaryExpression' && node.operator === 'delete') ||
|
|
455
|
+
/^JSX/.test(node.type)
|
|
456
|
+
)
|
|
457
|
+
error(filename, node, 'a binding value must be a pure props projection');
|
|
458
|
+
if (isRuntimeReference(node, lexical, parent, key)) {
|
|
459
|
+
const binding = lexical.resolveBinding(
|
|
460
|
+
lexical.nodeScopes.get(node) ?? lexical.rootScope,
|
|
461
|
+
node.name,
|
|
462
|
+
);
|
|
463
|
+
if (
|
|
464
|
+
binding === null &&
|
|
465
|
+
!nativeReferences.has(node) &&
|
|
466
|
+
!['undefined', 'NaN', 'Infinity'].includes(node.name)
|
|
467
|
+
) {
|
|
468
|
+
error(
|
|
469
|
+
filename,
|
|
470
|
+
node,
|
|
471
|
+
`ambient value ${JSON.stringify(node.name)} is not a props projection`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (binding?.scope === lexical.rootScope && !imports.has(node.name)) {
|
|
475
|
+
const declaration = lexical.domBindingConstants?.get(node.name);
|
|
476
|
+
if (!declaration)
|
|
477
|
+
error(
|
|
478
|
+
filename,
|
|
479
|
+
node,
|
|
480
|
+
'move module-local values into props or an imported pure projection',
|
|
481
|
+
);
|
|
482
|
+
if (constants.has(node.name))
|
|
483
|
+
error(filename, node, 'cyclic binding constants are not supported');
|
|
484
|
+
assertProjection(
|
|
485
|
+
declaration.init,
|
|
486
|
+
filename,
|
|
487
|
+
imports,
|
|
488
|
+
lexical,
|
|
489
|
+
parameterScope,
|
|
490
|
+
new Set([...constants, node.name]),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
if (lexical.domBindingCallback?.(node))
|
|
494
|
+
error(filename, node, 'local callbacks are supported only as native adapters');
|
|
495
|
+
}
|
|
496
|
+
if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') {
|
|
497
|
+
const callee = unwrap(node.callee);
|
|
498
|
+
const native = nativeProjectionReference(node, imports, lexical, parameterScope);
|
|
499
|
+
if (native) nativeReferences.add(native);
|
|
500
|
+
if (
|
|
501
|
+
native ||
|
|
502
|
+
node.metadata?.octaneNativeSignalRead ||
|
|
503
|
+
readMethod(node) ||
|
|
504
|
+
importedProjectionCall(node, imports, lexical, parameterScope) ||
|
|
505
|
+
factoryProjection(callee)
|
|
506
|
+
)
|
|
507
|
+
return;
|
|
508
|
+
error(
|
|
509
|
+
filename,
|
|
510
|
+
node,
|
|
511
|
+
'calls in bindings must be imported pure projections, not hooks or live accessors',
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function bindingParameterNames(parameter, filename) {
|
|
518
|
+
if (parameter?.type !== 'ObjectPattern') return [parameter?.name ?? null];
|
|
519
|
+
return parameter.properties.map((property, index) => {
|
|
520
|
+
if (
|
|
521
|
+
property.type === 'RestElement' &&
|
|
522
|
+
property.argument.type === 'Identifier' &&
|
|
523
|
+
index === parameter.properties.length - 1
|
|
524
|
+
)
|
|
525
|
+
return property.argument.name;
|
|
526
|
+
const value = property.value;
|
|
527
|
+
const binding = value?.type === 'AssignmentPattern' ? value.left : value;
|
|
528
|
+
if (
|
|
529
|
+
property.type !== 'Property' ||
|
|
530
|
+
property.kind !== 'init' ||
|
|
531
|
+
property.method ||
|
|
532
|
+
property.computed ||
|
|
533
|
+
!['Identifier', 'Literal'].includes(property.key.type) ||
|
|
534
|
+
binding?.type !== 'Identifier' ||
|
|
535
|
+
(value.type === 'AssignmentPattern' &&
|
|
536
|
+
(value.right.type !== 'Literal' ||
|
|
537
|
+
(value.right.value !== null &&
|
|
538
|
+
!['string', 'number', 'boolean'].includes(typeof value.right.value))))
|
|
539
|
+
)
|
|
540
|
+
error(
|
|
541
|
+
filename,
|
|
542
|
+
property,
|
|
543
|
+
'binding props support only flat static names, literal defaults, and rest',
|
|
544
|
+
);
|
|
545
|
+
return binding.name;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function bindingRender(fn, filename, required = true) {
|
|
550
|
+
const body = statements(fn);
|
|
551
|
+
const offset = isDirective(body[0]) ? 1 : 0;
|
|
552
|
+
if (required && offset === 0) error(filename, fn, `place '${DIRECTIVE}' first in the view body`);
|
|
553
|
+
const render =
|
|
554
|
+
fn.body.type === 'JSXCodeBlock'
|
|
555
|
+
? fn.body.render
|
|
556
|
+
: body.at(-1)?.type === 'ReturnStatement'
|
|
557
|
+
? unwrap(body.at(-1).argument)
|
|
558
|
+
: null;
|
|
559
|
+
const setup = body.slice(offset, fn.body.type === 'JSXCodeBlock' ? undefined : -1);
|
|
560
|
+
if (
|
|
561
|
+
!setup.every(
|
|
562
|
+
(statement) =>
|
|
563
|
+
statement.type === 'VariableDeclaration' &&
|
|
564
|
+
statement.kind === 'const' &&
|
|
565
|
+
statement.declarations.every((decl) => decl.id.type === 'Identifier' && decl.init),
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
error(filename, fn, 'binding setup supports only pure const aliases before its output');
|
|
569
|
+
if (
|
|
570
|
+
!render ||
|
|
571
|
+
fn.async ||
|
|
572
|
+
fn.generator ||
|
|
573
|
+
fn.params.length > 1 ||
|
|
574
|
+
(required && fn.params.length !== 1) ||
|
|
575
|
+
(fn.params.length === 1 && !['Identifier', 'ObjectPattern'].includes(fn.params[0].type))
|
|
576
|
+
) {
|
|
577
|
+
error(
|
|
578
|
+
filename,
|
|
579
|
+
fn,
|
|
580
|
+
'a binding view needs an ordinary props parameter and one template output, without early returns',
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
bindingParameterNames(fn.params[0], filename);
|
|
584
|
+
return render;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function bindingRestSpreads(fn, render, props, lexical, filename) {
|
|
588
|
+
const parameter = fn.params[0];
|
|
589
|
+
const rest =
|
|
590
|
+
parameter?.type === 'ObjectPattern'
|
|
591
|
+
? parameter.properties.find((property) => property.type === 'RestElement')?.argument
|
|
592
|
+
: null;
|
|
593
|
+
if (!rest) return { render, hasRest: false };
|
|
594
|
+
const scope = lexical.resolveBinding(lexical.nodeScopes.get(rest), rest.name)?.scope;
|
|
595
|
+
const consumed = new Set(
|
|
596
|
+
parameter.properties
|
|
597
|
+
.filter((property) => property.type === 'Property')
|
|
598
|
+
.map((property) => String(property.key.name ?? property.key.value)),
|
|
599
|
+
);
|
|
600
|
+
const keys = props?.filter((name) => !consumed.has(name));
|
|
601
|
+
const replacements = new Map();
|
|
602
|
+
const sites = new Map();
|
|
603
|
+
let nextSite = 0;
|
|
604
|
+
let conditional = keys?.every(
|
|
605
|
+
(name) =>
|
|
606
|
+
name === 'ref' || /^on[A-Z]/.test(name) || /^(?:aria|data)-[a-z][a-z0-9-]*$/.test(name),
|
|
607
|
+
);
|
|
608
|
+
let hasRest = false;
|
|
609
|
+
walk(render, (node) => {
|
|
610
|
+
if (
|
|
611
|
+
['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(node.type)
|
|
612
|
+
)
|
|
613
|
+
return false;
|
|
614
|
+
if (
|
|
615
|
+
!['JSXElement', 'Element'].includes(node.type) ||
|
|
616
|
+
!/^[a-z]/.test(node.openingElement?.name?.name ?? node.id?.name ?? '')
|
|
617
|
+
)
|
|
618
|
+
return;
|
|
619
|
+
const attributes = node.openingElement?.attributes ?? node.attributes ?? [];
|
|
620
|
+
const nativeSites = [];
|
|
621
|
+
let spread = 0;
|
|
622
|
+
for (const attribute of attributes) {
|
|
623
|
+
if (!['JSXSpreadAttribute', 'SpreadAttribute'].includes(attribute.type)) continue;
|
|
624
|
+
const ordinal = spread++;
|
|
625
|
+
const value = unwrap(attribute.argument);
|
|
626
|
+
if (
|
|
627
|
+
value?.type !== 'Identifier' ||
|
|
628
|
+
value.name !== rest.name ||
|
|
629
|
+
lexical.resolveBinding(lexical.nodeScopes.get(value), value.name)?.scope !== scope
|
|
630
|
+
)
|
|
631
|
+
continue;
|
|
632
|
+
hasRest = true;
|
|
633
|
+
nativeSites.push({ attribute, site: nextSite++, spread: ordinal, keys });
|
|
634
|
+
if (
|
|
635
|
+
['input', 'textarea', 'select'].includes(node.openingElement?.name?.name ?? node.id?.name)
|
|
636
|
+
)
|
|
637
|
+
conditional = false;
|
|
638
|
+
if (keys === undefined) continue;
|
|
639
|
+
if (keys.some((name) => ['class', 'className'].includes(name)))
|
|
640
|
+
error(filename, attribute, 'closed binding rest cannot supply class groups');
|
|
641
|
+
replacements.set(
|
|
642
|
+
attribute,
|
|
643
|
+
keys.map((name) =>
|
|
644
|
+
inheritHookMemoOrigin(
|
|
645
|
+
b.jsx_attribute(
|
|
646
|
+
b.jsx_id(name),
|
|
647
|
+
b.jsx_expression_container(b.member(value, b.literal(name), true)),
|
|
648
|
+
),
|
|
649
|
+
attribute,
|
|
650
|
+
),
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
if (nativeSites.length > 0) sites.set(attributes, nativeSites);
|
|
655
|
+
if (!attributes.some((attribute) => replacements.has(attribute))) return;
|
|
656
|
+
const expanded = attributes.flatMap((attribute) => replacements.get(attribute) ?? [attribute]);
|
|
657
|
+
sites.set(expanded, nativeSites);
|
|
658
|
+
const names = new Set();
|
|
659
|
+
for (const attribute of expanded) {
|
|
660
|
+
const raw = attrName(attribute);
|
|
661
|
+
if (typeof raw !== 'string') continue;
|
|
662
|
+
const name = (
|
|
663
|
+
raw === 'className'
|
|
664
|
+
? 'class'
|
|
665
|
+
: (ATTRIBUTE_ALIASES.get(raw) ??
|
|
666
|
+
raw.replace(
|
|
667
|
+
/^on(DoubleClick|Focus|Blur)(Capture)?$/,
|
|
668
|
+
(_, event, capture = '') =>
|
|
669
|
+
`on${event === 'DoubleClick' ? 'dblclick' : event === 'Focus' ? 'focusin' : 'focusout'}${capture}`,
|
|
670
|
+
))
|
|
671
|
+
).toLowerCase();
|
|
672
|
+
if (names.has(name))
|
|
673
|
+
error(
|
|
674
|
+
filename,
|
|
675
|
+
attribute,
|
|
676
|
+
'closed binding rest cannot collide with another native attribute',
|
|
677
|
+
);
|
|
678
|
+
names.add(name);
|
|
679
|
+
}
|
|
680
|
+
replacements.set(attributes, expanded);
|
|
681
|
+
});
|
|
682
|
+
// Only replace attribute lists. The original rest reference retains lexical
|
|
683
|
+
// provenance, and neither the authored function nor its parameter is rewritten.
|
|
684
|
+
for (const [node] of replacements) if (!Array.isArray(node)) replacements.delete(node);
|
|
685
|
+
return { render: mapCow(render, replacements), hasRest, sites, conditional };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function planView(fn, filename, source, imports, lexical, native = null) {
|
|
689
|
+
const render = native?.element ?? bindingRender(fn, filename);
|
|
690
|
+
const nodes = [];
|
|
691
|
+
const elements = [];
|
|
692
|
+
const bindings = [];
|
|
693
|
+
const values = [];
|
|
694
|
+
const projections = [];
|
|
695
|
+
const signalIndices = [];
|
|
696
|
+
const styleIndices = [];
|
|
697
|
+
const projectionGroups = [];
|
|
698
|
+
const classAttributes = new Map();
|
|
699
|
+
const id = `d:${strongHash(`octane:dom-bindings:2\0${filename}\0${fn.id.name}\0${source}`)}`;
|
|
700
|
+
const parameterScope = lexical.nodeScopes.get(fn.body) ?? lexical.rootScope;
|
|
701
|
+
const unbound = new Map();
|
|
702
|
+
let addressed = false;
|
|
703
|
+
const markUnbound = (expression, opaque = false) => {
|
|
704
|
+
const value = unwrap(expression);
|
|
705
|
+
const external =
|
|
706
|
+
value?.type === 'CallExpression' && value.callee.type === 'Identifier'
|
|
707
|
+
? imports.get(value.callee.name)
|
|
708
|
+
: null;
|
|
709
|
+
if (
|
|
710
|
+
external?.source !== 'octane/behavior' ||
|
|
711
|
+
external.imported !== 'unbound' ||
|
|
712
|
+
lexical.resolveBinding(lexical.nodeScopes.get(value.callee), value.callee.name)?.scope !==
|
|
713
|
+
lexical.rootScope
|
|
714
|
+
)
|
|
715
|
+
return false;
|
|
716
|
+
if (
|
|
717
|
+
value.arguments.length !== 1 ||
|
|
718
|
+
value.arguments[0].type === 'SpreadElement' ||
|
|
719
|
+
value.optional
|
|
720
|
+
)
|
|
721
|
+
error(filename, value, 'unbound requires one externally owned value');
|
|
722
|
+
// Opaque descendants remain ordinary authored output. Their expressions
|
|
723
|
+
// never execute in the selected artifact, so they need no projection proof.
|
|
724
|
+
if (!opaque) assertProjection(value.arguments[0], filename, imports, lexical, parameterScope);
|
|
725
|
+
unbound.set(value, value.arguments[0]);
|
|
726
|
+
return true;
|
|
727
|
+
};
|
|
728
|
+
const add = (node, kind, name, value, origin, unitless) => {
|
|
729
|
+
assertProjection(value, filename, imports, lexical, parameterScope);
|
|
730
|
+
// Static authored markup is not a behavior-owned channel. In particular,
|
|
731
|
+
// SVG paths remain in SSR rather than shipping again in every projector.
|
|
732
|
+
if (unwrap(value)?.type === 'Literal') return;
|
|
733
|
+
if (kind === 'styleObject') styleIndices.push(bindings.length);
|
|
734
|
+
else if (kind !== 'classToken' && kind !== 'control' && lexical.domBindingCanCarrySignal(value))
|
|
735
|
+
signalIndices.push(bindings.length);
|
|
736
|
+
bindings.push([node, kind, name, ...(unitless === undefined ? [] : [unitless])]);
|
|
737
|
+
values.push(value);
|
|
738
|
+
};
|
|
739
|
+
const addClassTokens = (index, expression) => {
|
|
740
|
+
const tokens = new Set();
|
|
741
|
+
const reserved = new Set();
|
|
742
|
+
const externalTokens = new Set();
|
|
743
|
+
const collectStaticTokens = (input) => {
|
|
744
|
+
const value = unwrap(input);
|
|
745
|
+
if (value?.type === 'Literal') {
|
|
746
|
+
if (typeof value.value === 'string' || typeof value.value === 'number') {
|
|
747
|
+
for (const token of String(value.value || '').split(/[\t\n\f\r ]+/))
|
|
748
|
+
externalTokens.add(token);
|
|
749
|
+
}
|
|
750
|
+
} else if (value?.type === 'ArrayExpression') {
|
|
751
|
+
for (const child of value.elements) collectStaticTokens(child);
|
|
752
|
+
} else if (value?.type === 'ObjectExpression') {
|
|
753
|
+
for (const property of value.properties) {
|
|
754
|
+
if (
|
|
755
|
+
property.type !== 'Property' ||
|
|
756
|
+
property.kind !== 'init' ||
|
|
757
|
+
property.computed ||
|
|
758
|
+
property.method
|
|
759
|
+
)
|
|
760
|
+
continue;
|
|
761
|
+
const key = property.key.name ?? property.key.value;
|
|
762
|
+
const condition = unwrap(property.value);
|
|
763
|
+
if (typeof key === 'string' && condition?.type === 'Literal' && condition.value) {
|
|
764
|
+
for (const token of key.split(/[\t\n\f\r ]+/)) externalTokens.add(token);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const visitClass = (input) => {
|
|
770
|
+
const value = unwrap(input);
|
|
771
|
+
if (markUnbound(value)) {
|
|
772
|
+
collectStaticTokens(value.arguments[0]);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (value?.type === 'ArrayExpression') {
|
|
776
|
+
for (const child of value.elements) {
|
|
777
|
+
if (child !== null) visitClass(child);
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (value?.type === 'Literal') {
|
|
782
|
+
collectStaticTokens(value);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (value?.type !== 'ObjectExpression')
|
|
786
|
+
error(
|
|
787
|
+
filename,
|
|
788
|
+
value,
|
|
789
|
+
'partial classes need fixed token objects and explicitly unbound values',
|
|
790
|
+
);
|
|
791
|
+
for (const property of value.properties) {
|
|
792
|
+
if (
|
|
793
|
+
property.type !== 'Property' ||
|
|
794
|
+
property.kind !== 'init' ||
|
|
795
|
+
property.computed ||
|
|
796
|
+
property.method
|
|
797
|
+
)
|
|
798
|
+
error(filename, property, 'partial classes need fixed ordinary token properties');
|
|
799
|
+
const token = property.key.name ?? property.key.value;
|
|
800
|
+
if (
|
|
801
|
+
typeof token !== 'string' ||
|
|
802
|
+
token === '' ||
|
|
803
|
+
/[\t\n\f\r ]/.test(token) ||
|
|
804
|
+
token === '__proto__'
|
|
805
|
+
)
|
|
806
|
+
error(filename, property, 'a class binding key must be one fixed nonempty class token');
|
|
807
|
+
if (tokens.has(token))
|
|
808
|
+
error(filename, property, `duplicate class token ${JSON.stringify(token)}`);
|
|
809
|
+
tokens.add(token);
|
|
810
|
+
if (unwrap(property.value)?.type !== 'Literal') reserved.add(token);
|
|
811
|
+
add(index, 'classToken', token, property.value, property);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
visitClass(expression);
|
|
815
|
+
for (const token of reserved) {
|
|
816
|
+
if (externalTokens.has(token))
|
|
817
|
+
error(
|
|
818
|
+
filename,
|
|
819
|
+
expression,
|
|
820
|
+
`unbound classes must not contribute owned token ${JSON.stringify(token)}`,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
const hasPartialClass = (input) => {
|
|
825
|
+
const value = unwrap(input);
|
|
826
|
+
return (
|
|
827
|
+
value?.type === 'ObjectExpression' ||
|
|
828
|
+
markUnbound(value) ||
|
|
829
|
+
(value?.type === 'ArrayExpression' && value.elements.some(hasPartialClass))
|
|
830
|
+
);
|
|
831
|
+
};
|
|
832
|
+
const addClassGroups = (index, attr, input) => {
|
|
833
|
+
const baseline = [];
|
|
834
|
+
const groups = [];
|
|
835
|
+
let needsGroup = false;
|
|
836
|
+
const collect = (expression) => {
|
|
837
|
+
const value = unwrap(expression);
|
|
838
|
+
if (markUnbound(value)) baseline.push(value.arguments[0]);
|
|
839
|
+
else if (value?.type === 'Literal') baseline.push(value);
|
|
840
|
+
else if (value?.type === 'ArrayExpression') {
|
|
841
|
+
for (const child of value.elements) if (child !== null) collect(child);
|
|
842
|
+
} else {
|
|
843
|
+
groups.push(value);
|
|
844
|
+
needsGroup ||=
|
|
845
|
+
value?.type !== 'ObjectExpression' ||
|
|
846
|
+
value.properties.some((property) => property.type !== 'Property' || property.computed);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
collect(input);
|
|
850
|
+
if (!needsGroup) return false;
|
|
851
|
+
const receipt = `data-octane-class-${id}-${index}`;
|
|
852
|
+
const value = b.array(groups);
|
|
853
|
+
assertProjection(value, filename, imports, lexical, parameterScope);
|
|
854
|
+
bindings.push([index, 'classGroup', receipt, 0]);
|
|
855
|
+
values.push(value);
|
|
856
|
+
classAttributes.set(attr, {
|
|
857
|
+
...attr,
|
|
858
|
+
_octaneBindingClassGroups: { receipt, baseline, groups: [value] },
|
|
859
|
+
});
|
|
860
|
+
return true;
|
|
861
|
+
};
|
|
862
|
+
const visit = (element, parent, namespace, ancestors) => {
|
|
863
|
+
if (element?.type !== 'JSXElement' && element?.type !== 'Element') {
|
|
864
|
+
error(
|
|
865
|
+
filename,
|
|
866
|
+
element,
|
|
867
|
+
'binding views support fixed native elements only, without text or structural holes',
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const tag = element.openingElement?.name?.name ?? element.id?.name;
|
|
871
|
+
if (typeof tag !== 'string' || !/^[a-z][a-zA-Z0-9]*$/.test(tag) || FORBIDDEN_TAGS.has(tag)) {
|
|
872
|
+
error(
|
|
873
|
+
filename,
|
|
874
|
+
element,
|
|
875
|
+
'component, custom, parser-sensitive and resource elements are not supported in binding views',
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (namespace === 0 && tag !== 'svg' && SVG_ONLY_TAGS.has(tag))
|
|
879
|
+
error(filename, element, 'SVG binding descendants need an explicit svg root');
|
|
880
|
+
const ns = tag === 'svg' ? 1 : namespace;
|
|
881
|
+
if (ns === 1 && tag !== 'svg' && tag !== 'a' && !SVG_ONLY_TAGS.has(tag))
|
|
882
|
+
error(filename, element, 'SVG binding descendants must be native SVG elements');
|
|
883
|
+
if (ns === 0 && ancestors.length > 0) {
|
|
884
|
+
const direct = invalidHtmlNestingWithParent(tag, ancestors.at(-1));
|
|
885
|
+
if (direct) error(filename, element, direct);
|
|
886
|
+
for (let i = 0; i < ancestors.length; i++) {
|
|
887
|
+
const invalid = invalidHtmlNestingWithAncestor(tag, ancestors.slice(i).reverse());
|
|
888
|
+
if (invalid) error(filename, element, invalid);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const authoredChildren = (native === null ? (element.children ?? []) : []).filter(
|
|
892
|
+
(child) =>
|
|
893
|
+
(child.type !== 'JSXText' || child.value.trim() !== '' || !/[\r\n]/.test(child.value)) &&
|
|
894
|
+
(child.type !== 'JSXExpressionContainer' ||
|
|
895
|
+
child.expression?.type !== 'JSXEmptyExpression'),
|
|
896
|
+
);
|
|
897
|
+
const children = authoredChildren.filter((child) => {
|
|
898
|
+
const expression = child.type === 'JSXExpressionContainer' ? child.expression : null;
|
|
899
|
+
return expression === null || !markUnbound(expression, true);
|
|
900
|
+
});
|
|
901
|
+
const opaqueChildren = authoredChildren.length !== children.length;
|
|
902
|
+
const openChildren = tag !== 'textarea' && opaqueChildren && children.length > 0;
|
|
903
|
+
if (tag === 'textarea' && children.some((child) => child.type !== 'JSXText'))
|
|
904
|
+
error(filename, element, 'textarea content must be static text or explicitly unbound');
|
|
905
|
+
if (openChildren) addressed = true;
|
|
906
|
+
const index = nodes.length;
|
|
907
|
+
// Textarea's initial value is serialized as text by the normal SSR path.
|
|
908
|
+
// Its children belong to the native control, even without an authored hole.
|
|
909
|
+
nodes.push([
|
|
910
|
+
parent,
|
|
911
|
+
tag,
|
|
912
|
+
ns,
|
|
913
|
+
(opaqueChildren && !openChildren) || tag === 'textarea' ? null : children.length,
|
|
914
|
+
...(openChildren ? [true] : []),
|
|
915
|
+
]);
|
|
916
|
+
elements.push(element);
|
|
917
|
+
const owned = new Set();
|
|
918
|
+
const externalNames = new Set();
|
|
919
|
+
const knownFields = new Set();
|
|
920
|
+
for (const attr of element.openingElement?.attributes ?? element.attributes ?? []) {
|
|
921
|
+
if (attr._octaneKnownAttributeSpread) {
|
|
922
|
+
const argument = attr.argument ?? attr.value.expression;
|
|
923
|
+
// The compiler verified the exact imported factory against its
|
|
924
|
+
// provider contract. Its arguments still need the ordinary proof.
|
|
925
|
+
for (const value of argument.arguments)
|
|
926
|
+
assertProjection(value, filename, imports, lexical, parameterScope);
|
|
927
|
+
const temporary = b.id(lexical.domBindingAllocateName('_bindingAttrs'));
|
|
928
|
+
const group = attr._octaneKnownAttributeSpread.projection ? [] : null;
|
|
929
|
+
if (group) projectionGroups.push(group);
|
|
930
|
+
else projections.push(inheritHookMemoOrigin(b.const(temporary, argument), attr));
|
|
931
|
+
for (const raw of attr._octaneKnownAttributeSpread.fields) {
|
|
932
|
+
let name = raw === 'className' ? 'class' : (ATTRIBUTE_ALIASES.get(raw) ?? raw);
|
|
933
|
+
if (ns === 0) name = name.toLowerCase();
|
|
934
|
+
const lower = name.toLowerCase();
|
|
935
|
+
if (
|
|
936
|
+
FORBIDDEN_ATTRS.has(lower) ||
|
|
937
|
+
lower.startsWith('on') ||
|
|
938
|
+
lower.startsWith('data-octane-class-') ||
|
|
939
|
+
externalAttribute(tag, lower) ||
|
|
940
|
+
((URL_ATTRS.has(lower) || name.includes(':')) && !shouldSanitizeURLAttribute(tag, name))
|
|
941
|
+
)
|
|
942
|
+
error(
|
|
943
|
+
filename,
|
|
944
|
+
attr,
|
|
945
|
+
`known spread field ${JSON.stringify(raw)} is not a presentation channel`,
|
|
946
|
+
);
|
|
947
|
+
if (owned.has(name) || externalNames.has(lower))
|
|
948
|
+
error(filename, attr, `known spread conflicts with attribute ${JSON.stringify(raw)}`);
|
|
949
|
+
owned.add(name);
|
|
950
|
+
knownFields.add(lower);
|
|
951
|
+
if (group) group.push([bindings.length, raw]);
|
|
952
|
+
if (name === 'style' && attr._octaneKnownAttributeSpread.style === 'object') {
|
|
953
|
+
if (!group) styleIndices.push(bindings.length);
|
|
954
|
+
bindings.push([index, 'styleObject', name]);
|
|
955
|
+
} else {
|
|
956
|
+
if (!group) signalIndices.push(bindings.length);
|
|
957
|
+
bindings.push([index, bindingKind(tag, name), name]);
|
|
958
|
+
}
|
|
959
|
+
values.push(
|
|
960
|
+
group
|
|
961
|
+
? inheritHookMemoOrigin(
|
|
962
|
+
group.length === 1 ? b.arrow([], argument) : b.literal(null),
|
|
963
|
+
attr,
|
|
964
|
+
)
|
|
965
|
+
: inheritHookMemoOrigin(
|
|
966
|
+
b.conditional(
|
|
967
|
+
b.binary('==', temporary, b.literal(null)),
|
|
968
|
+
b.unary('void', b.literal(0)),
|
|
969
|
+
b.member(temporary, b.literal(raw), true),
|
|
970
|
+
),
|
|
971
|
+
attr,
|
|
972
|
+
),
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
if (attr.type === 'JSXSpreadAttribute' || attr.type === 'SpreadAttribute') {
|
|
978
|
+
if (!markUnbound(attr.argument))
|
|
979
|
+
error(filename, attr, 'binding attribute spreads must be explicitly unbound');
|
|
980
|
+
if (bindings.some((binding) => binding[0] === index))
|
|
981
|
+
error(filename, attr, 'unbound attribute spreads must precede owned binding attributes');
|
|
982
|
+
const external = unwrap(unwrap(attr.argument).arguments[0]);
|
|
983
|
+
if (external?.type === 'ObjectExpression') {
|
|
984
|
+
for (const property of external.properties) {
|
|
985
|
+
if (property.type !== 'Property' || property.computed) continue;
|
|
986
|
+
const raw = property.key.name ?? property.key.value;
|
|
987
|
+
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);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (attr.type !== 'JSXAttribute' && attr.type !== 'Attribute')
|
|
1007
|
+
error(filename, attr, 'spread attributes are not supported in binding views');
|
|
1008
|
+
const raw = attrName(attr);
|
|
1009
|
+
let name = raw === 'className' ? 'class' : (ATTRIBUTE_ALIASES.get(raw) ?? raw);
|
|
1010
|
+
if (ns === 0) name = name.toLowerCase();
|
|
1011
|
+
const lower = name.toLowerCase();
|
|
1012
|
+
if (owned.has(name))
|
|
1013
|
+
error(
|
|
1014
|
+
filename,
|
|
1015
|
+
attr,
|
|
1016
|
+
`${knownFields.has(lower) ? 'known spread conflicts with' : 'duplicate binding'} attribute ${JSON.stringify(name)}`,
|
|
1017
|
+
);
|
|
1018
|
+
owned.add(name);
|
|
1019
|
+
if (lower === 'dangerouslysetinnerhtml' && markUnbound(attrValueForBinding(attr), true)) {
|
|
1020
|
+
if (authoredChildren.length > 0)
|
|
1021
|
+
error(filename, attr, 'an opaque HTML host cannot also declare binding children');
|
|
1022
|
+
nodes[index][3] = null;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (
|
|
1026
|
+
!/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(name) ||
|
|
1027
|
+
lower.startsWith('on') ||
|
|
1028
|
+
FORBIDDEN_ATTRS.has(lower) ||
|
|
1029
|
+
lower.startsWith('data-octane-class-')
|
|
1030
|
+
) {
|
|
1031
|
+
error(filename, attr, `attribute ${JSON.stringify(raw)} is not supported in binding views`);
|
|
1032
|
+
}
|
|
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;
|
|
1040
|
+
if (value?.type !== 'Literal' && externalNames.has(lower))
|
|
1041
|
+
error(
|
|
1042
|
+
filename,
|
|
1043
|
+
attr,
|
|
1044
|
+
`unbound spreads must not contribute owned attribute ${JSON.stringify(raw)}`,
|
|
1045
|
+
);
|
|
1046
|
+
if (externalAttribute(tag, lower)) {
|
|
1047
|
+
if (value?.type === 'Literal') continue;
|
|
1048
|
+
if (
|
|
1049
|
+
(lower === 'value' && ['input', 'textarea', 'select'].includes(tag)) ||
|
|
1050
|
+
(lower === 'checked' && tag === 'input')
|
|
1051
|
+
) {
|
|
1052
|
+
if (lower === 'checked') {
|
|
1053
|
+
const type = (element.openingElement?.attributes ?? element.attributes ?? []).find(
|
|
1054
|
+
(attribute) => attrName(attribute) === 'type',
|
|
1055
|
+
);
|
|
1056
|
+
const inputType = type && unwrap(attrValueForBinding(type));
|
|
1057
|
+
if (inputType?.type !== 'Literal' || !['checkbox', 'radio'].includes(inputType.value))
|
|
1058
|
+
error(
|
|
1059
|
+
filename,
|
|
1060
|
+
attr,
|
|
1061
|
+
'a checked binding requires a fixed checkbox or radio input type',
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
add(index, 'control', lower, value, attr);
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
error(
|
|
1068
|
+
filename,
|
|
1069
|
+
attr,
|
|
1070
|
+
`native state and identity attribute ${JSON.stringify(raw)} must be static or explicitly unbound`,
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if ((URL_ATTRS.has(lower) || name.includes(':')) && !shouldSanitizeURLAttribute(tag, name)) {
|
|
1074
|
+
if (value?.type === 'Literal') continue;
|
|
1075
|
+
error(
|
|
1076
|
+
filename,
|
|
1077
|
+
attr,
|
|
1078
|
+
'unsupported URL and namespaced attributes must be static or explicitly unbound',
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (name === 'class' && hasPartialClass(value)) {
|
|
1082
|
+
if (!addClassGroups(index, attr, value)) addClassTokens(index, value);
|
|
1083
|
+
} else if (name === 'style') {
|
|
1084
|
+
if (
|
|
1085
|
+
value?.type !== 'ObjectExpression' ||
|
|
1086
|
+
value.properties.some(
|
|
1087
|
+
(property) => property.type === 'SpreadElement' || property.computed,
|
|
1088
|
+
)
|
|
1089
|
+
) {
|
|
1090
|
+
add(
|
|
1091
|
+
index,
|
|
1092
|
+
value?.type === 'Literal' ? 'styleAttribute' : 'styleObject',
|
|
1093
|
+
'style',
|
|
1094
|
+
value,
|
|
1095
|
+
attr,
|
|
1096
|
+
);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const properties = new Set();
|
|
1100
|
+
for (const property of value.properties) {
|
|
1101
|
+
if (
|
|
1102
|
+
property.type !== 'Property' ||
|
|
1103
|
+
property.kind !== 'init' ||
|
|
1104
|
+
property.computed ||
|
|
1105
|
+
property.method
|
|
1106
|
+
)
|
|
1107
|
+
error(filename, property, 'binding styles need fixed ordinary properties');
|
|
1108
|
+
const key = property.key.name ?? property.key.value;
|
|
1109
|
+
if (typeof key !== 'string')
|
|
1110
|
+
error(filename, property, 'binding style property names must be strings');
|
|
1111
|
+
const cssName = hyphenateStyleName(key);
|
|
1112
|
+
if (!/^(?:--[A-Za-z_][\w-]*|-?[a-z][a-z-]*)$/.test(cssName))
|
|
1113
|
+
error(filename, property, 'invalid fixed binding style property');
|
|
1114
|
+
if (properties.has(cssName))
|
|
1115
|
+
error(filename, property, `duplicate style property ${JSON.stringify(cssName)}`);
|
|
1116
|
+
properties.add(cssName);
|
|
1117
|
+
add(
|
|
1118
|
+
index,
|
|
1119
|
+
'styleProperty',
|
|
1120
|
+
cssName,
|
|
1121
|
+
property.value,
|
|
1122
|
+
property,
|
|
1123
|
+
cssName.startsWith('--') || isUnitlessStyleProp(key),
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
} else {
|
|
1127
|
+
const kind =
|
|
1128
|
+
name === 'class'
|
|
1129
|
+
? 'class'
|
|
1130
|
+
: shouldSanitizeURLAttribute(tag, name)
|
|
1131
|
+
? 'url'
|
|
1132
|
+
: name.startsWith('aria-') ||
|
|
1133
|
+
name.startsWith('data-') ||
|
|
1134
|
+
isEnumeratedBooleanAttr(lower)
|
|
1135
|
+
? 'aria'
|
|
1136
|
+
: BOOLEAN_ATTR_PROPS.has(lower)
|
|
1137
|
+
? 'boolean'
|
|
1138
|
+
: 'attr';
|
|
1139
|
+
add(index, kind, name, value, attr);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (tag !== 'textarea') {
|
|
1143
|
+
for (const child of children) visit(child, index, ns, [...ancestors, tag]);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
visit(render, -1, native?.namespace ?? 0, native?.ancestors ?? []);
|
|
1147
|
+
return {
|
|
1148
|
+
fn,
|
|
1149
|
+
render,
|
|
1150
|
+
nodes,
|
|
1151
|
+
elements,
|
|
1152
|
+
addressed,
|
|
1153
|
+
bindings,
|
|
1154
|
+
values,
|
|
1155
|
+
projections,
|
|
1156
|
+
signalIndices,
|
|
1157
|
+
styleIndices,
|
|
1158
|
+
projectionGroups,
|
|
1159
|
+
unbound,
|
|
1160
|
+
classAttributes,
|
|
1161
|
+
id,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function literalData(value) {
|
|
1166
|
+
return Array.isArray(value) ? b.array(value.map(literalData)) : b.literal(value);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function scalarProperties(
|
|
1170
|
+
plan,
|
|
1171
|
+
project,
|
|
1172
|
+
classFactory,
|
|
1173
|
+
signalFactory = null,
|
|
1174
|
+
styleFactory = null,
|
|
1175
|
+
controlFactory = null,
|
|
1176
|
+
projectionFactory = null,
|
|
1177
|
+
) {
|
|
1178
|
+
return [
|
|
1179
|
+
b.prop('init', b.id('id'), b.literal(plan.id)),
|
|
1180
|
+
...(plan.addressed ? [b.prop('init', b.id('addressed'), b.literal(true))] : []),
|
|
1181
|
+
b.prop('init', b.id('nodes'), literalData(plan.nodes)),
|
|
1182
|
+
b.prop('init', b.id('bindings'), literalData(plan.bindings)),
|
|
1183
|
+
b.prop('init', b.id('project'), project),
|
|
1184
|
+
...(plan.signalIndices.length
|
|
1185
|
+
? [
|
|
1186
|
+
b.prop('init', b.id('signalIndices'), literalData(plan.signalIndices)),
|
|
1187
|
+
b.prop('init', b.id('connectSignal'), signalFactory),
|
|
1188
|
+
]
|
|
1189
|
+
: []),
|
|
1190
|
+
...(classFactory ? [b.prop('init', b.id('createClassGroup'), classFactory)] : []),
|
|
1191
|
+
...(styleFactory
|
|
1192
|
+
? [
|
|
1193
|
+
b.prop('init', b.id('styleIndices'), literalData(plan.styleIndices)),
|
|
1194
|
+
b.prop('init', b.id('connectStyle'), styleFactory),
|
|
1195
|
+
]
|
|
1196
|
+
: []),
|
|
1197
|
+
...(controlFactory ? [b.prop('init', b.id('createControls'), controlFactory)] : []),
|
|
1198
|
+
...(projectionFactory
|
|
1199
|
+
? [
|
|
1200
|
+
b.prop('init', b.id('projectionGroups'), literalData(plan.projectionGroups)),
|
|
1201
|
+
b.prop('init', b.id('connectProjection'), projectionFactory),
|
|
1202
|
+
]
|
|
1203
|
+
: []),
|
|
1204
|
+
];
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function projectionDependencies(plan, lexical) {
|
|
1208
|
+
const needed = new Set();
|
|
1209
|
+
const collect = (expression) =>
|
|
1210
|
+
walk(expression, (node, parent, key) => {
|
|
1211
|
+
if (node.type.startsWith('TS') && !UNWRAP.has(node.type)) return false;
|
|
1212
|
+
if (
|
|
1213
|
+
isRuntimeReference(node, lexical, parent, key) &&
|
|
1214
|
+
lexical.resolveBinding(lexical.nodeScopes.get(node) ?? lexical.rootScope, node.name)
|
|
1215
|
+
?.scope === lexical.rootScope
|
|
1216
|
+
) {
|
|
1217
|
+
if (needed.has(node.name)) return;
|
|
1218
|
+
needed.add(node.name);
|
|
1219
|
+
const declaration = lexical.domBindingConstants.get(node.name);
|
|
1220
|
+
if (declaration) collect(declaration.init);
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
collect(plan.expressions ?? plan.values);
|
|
1224
|
+
collect(plan.projections?.map((declaration) => declaration.declarations[0].init));
|
|
1225
|
+
return needed;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// A const declaration does not make its object immutable. Sharing is only safe
|
|
1229
|
+
// when every use terminates in a known style consumer, including uses through
|
|
1230
|
+
// local palettes/aliases. Unrelated exports and imperative callbacks stay local.
|
|
1231
|
+
function shareableConstants(ast, names, lexical, imports) {
|
|
1232
|
+
const parents = new WeakMap();
|
|
1233
|
+
const aliases = new Map();
|
|
1234
|
+
const references = new Map();
|
|
1235
|
+
const bindings = new WeakMap();
|
|
1236
|
+
const resolve = (node) => {
|
|
1237
|
+
const scope = lexical.resolveBinding(
|
|
1238
|
+
lexical.nodeScopes.get(node) ?? lexical.rootScope,
|
|
1239
|
+
node.name,
|
|
1240
|
+
)?.scope;
|
|
1241
|
+
if (!scope) return null;
|
|
1242
|
+
if (!bindings.has(scope)) bindings.set(scope, new Map());
|
|
1243
|
+
const names = bindings.get(scope);
|
|
1244
|
+
if (!names.has(node.name)) names.set(node.name, { scope, name: node.name });
|
|
1245
|
+
return names.get(node.name);
|
|
1246
|
+
};
|
|
1247
|
+
walk(ast, (node, parent, key) => {
|
|
1248
|
+
parents.set(node, { parent, key });
|
|
1249
|
+
if (
|
|
1250
|
+
node.type === 'VariableDeclarator' &&
|
|
1251
|
+
node.id.type === 'Identifier' &&
|
|
1252
|
+
parent.kind === 'const' &&
|
|
1253
|
+
parents.get(parent)?.parent?.type !== 'ExportNamedDeclaration'
|
|
1254
|
+
)
|
|
1255
|
+
aliases.set(node, resolve(node.id));
|
|
1256
|
+
if (isRuntimeReference(node, lexical, parent, key)) {
|
|
1257
|
+
const binding = resolve(node);
|
|
1258
|
+
if (!references.has(binding)) references.set(binding, []);
|
|
1259
|
+
references.get(binding).push(node);
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
const styleConsumer = (call) => {
|
|
1263
|
+
const callee = unwrap(call.callee);
|
|
1264
|
+
const member = callee?.type === 'MemberExpression' && !callee.computed;
|
|
1265
|
+
const provider = member ? unwrap(callee.object) : callee;
|
|
1266
|
+
const imported = provider?.type === 'Identifier' ? imports.get(provider.name) : null;
|
|
1267
|
+
return (
|
|
1268
|
+
imported &&
|
|
1269
|
+
['@octanejs/stylex', '@stylexjs/stylex'].includes(imported.source) &&
|
|
1270
|
+
resolve(provider)?.scope === lexical.rootScope &&
|
|
1271
|
+
['props', 'attrs'].includes(member ? callee.property.name : imported.imported)
|
|
1272
|
+
);
|
|
1273
|
+
};
|
|
1274
|
+
const checked = new Map();
|
|
1275
|
+
const dynamicRecipe = (call, reference) => {
|
|
1276
|
+
const callee = unwrap(call.callee);
|
|
1277
|
+
if (callee?.type !== 'MemberExpression' || callee.computed || callee.object !== reference)
|
|
1278
|
+
return false;
|
|
1279
|
+
const definition = lexical.domBindingConstants.get(reference.name)?.init;
|
|
1280
|
+
return (
|
|
1281
|
+
definition?.type === 'CallExpression' &&
|
|
1282
|
+
definition.arguments[0]?.properties?.some(
|
|
1283
|
+
(property) =>
|
|
1284
|
+
(property.key?.name ?? property.key?.value) === callee.property.name &&
|
|
1285
|
+
['ArrowFunctionExpression', 'FunctionExpression'].includes(property.value?.type),
|
|
1286
|
+
)
|
|
1287
|
+
);
|
|
1288
|
+
};
|
|
1289
|
+
const safe = (binding, visiting = new Set()) => {
|
|
1290
|
+
if (checked.has(binding)) return checked.get(binding);
|
|
1291
|
+
if (!binding || visiting.has(binding)) return false;
|
|
1292
|
+
const next = new Set(visiting).add(binding);
|
|
1293
|
+
const valid = (references.get(binding) ?? []).every((reference) => {
|
|
1294
|
+
let node = reference;
|
|
1295
|
+
while (parents.has(node)) {
|
|
1296
|
+
const { parent, key } = parents.get(node);
|
|
1297
|
+
if (!parent) return false;
|
|
1298
|
+
if (
|
|
1299
|
+
UNWRAP.has(parent.type) ||
|
|
1300
|
+
[
|
|
1301
|
+
'MemberExpression',
|
|
1302
|
+
'OptionalMemberExpression',
|
|
1303
|
+
'Property',
|
|
1304
|
+
'ObjectExpression',
|
|
1305
|
+
'ArrayExpression',
|
|
1306
|
+
'ConditionalExpression',
|
|
1307
|
+
'LogicalExpression',
|
|
1308
|
+
'JSXExpressionContainer',
|
|
1309
|
+
].includes(parent.type)
|
|
1310
|
+
) {
|
|
1311
|
+
node = parent;
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
if (parent.type === 'VariableDeclarator' && key === 'init')
|
|
1315
|
+
return safe(aliases.get(parent), next);
|
|
1316
|
+
if (parent.type === 'JSXAttribute') {
|
|
1317
|
+
const tag = parents.get(parent)?.parent?.name;
|
|
1318
|
+
return (
|
|
1319
|
+
attrName(parent) === 'sx' && tag?.type === 'JSXIdentifier' && /^[a-z]/.test(tag.name)
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
if (parent.type === 'CallExpression') {
|
|
1323
|
+
if (key !== 'callee') return styleConsumer(parent);
|
|
1324
|
+
if (!dynamicRecipe(parent, reference)) return false;
|
|
1325
|
+
// Dynamic recipes may return shared hoisted class data. Their
|
|
1326
|
+
// result must not escape to an imperative consumer either.
|
|
1327
|
+
node = parent;
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
return false;
|
|
1333
|
+
});
|
|
1334
|
+
checked.set(binding, valid);
|
|
1335
|
+
return valid;
|
|
1336
|
+
};
|
|
1337
|
+
return names.filter((name) => safe(resolve(lexical.domBindingConstants.get(name).id)));
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function projectProgram(ast, plan, filename, lexical) {
|
|
1341
|
+
const imports = importedBindings(ast);
|
|
1342
|
+
for (const statement of ast.body) {
|
|
1343
|
+
const declaration = statement.declaration ?? statement;
|
|
1344
|
+
if (
|
|
1345
|
+
statement.type === 'ImportDeclaration' ||
|
|
1346
|
+
statement.exportKind === 'type' ||
|
|
1347
|
+
declaration.type === 'TSInterfaceDeclaration' ||
|
|
1348
|
+
declaration.type === 'TSTypeAliasDeclaration' ||
|
|
1349
|
+
declaration.declare === true ||
|
|
1350
|
+
declaration.type === 'FunctionDeclaration'
|
|
1351
|
+
)
|
|
1352
|
+
continue;
|
|
1353
|
+
if (statement.type === 'ExpressionStatement' && typeof statement.directive === 'string')
|
|
1354
|
+
continue;
|
|
1355
|
+
if (
|
|
1356
|
+
declaration.type === 'VariableDeclaration' &&
|
|
1357
|
+
declaration.kind === 'const' &&
|
|
1358
|
+
declaration.declarations.every((item) => item.id.type === 'Identifier' && item.init)
|
|
1359
|
+
) {
|
|
1360
|
+
for (const item of declaration.declarations)
|
|
1361
|
+
assertProjection(
|
|
1362
|
+
item.init,
|
|
1363
|
+
filename,
|
|
1364
|
+
imports,
|
|
1365
|
+
lexical,
|
|
1366
|
+
lexical.rootScope,
|
|
1367
|
+
new Set([item.id.name]),
|
|
1368
|
+
);
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
error(
|
|
1372
|
+
filename,
|
|
1373
|
+
statement,
|
|
1374
|
+
'binding view modules cannot contain eager module initialization; pass state as props',
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
const needed = projectionDependencies(plan, lexical);
|
|
1378
|
+
const importNodes = ast.body.flatMap((node) => {
|
|
1379
|
+
const declaration = node.declaration ?? node;
|
|
1380
|
+
if (declaration.type === 'VariableDeclaration' && declaration.kind === 'const') {
|
|
1381
|
+
const declarations = declaration.declarations.filter((item) => needed.has(item.id.name));
|
|
1382
|
+
return declarations.length ? [{ ...declaration, declarations }] : [];
|
|
1383
|
+
}
|
|
1384
|
+
if (node.type !== 'ImportDeclaration' || node.importKind === 'type') return [];
|
|
1385
|
+
if (node.specifiers.length === 0) return [node];
|
|
1386
|
+
const specifiers = node.specifiers.filter(
|
|
1387
|
+
(specifier) => specifier.importKind !== 'type' && needed.has(specifier.local.name),
|
|
1388
|
+
);
|
|
1389
|
+
return specifiers.length === 0 ? [] : [{ ...node, specifiers }];
|
|
1390
|
+
});
|
|
1391
|
+
if (plan.root) {
|
|
1392
|
+
const names = new Set([
|
|
1393
|
+
...needed,
|
|
1394
|
+
...plan.dependencies.flatMap((node) =>
|
|
1395
|
+
node.specifiers.map((specifier) => specifier.local.name),
|
|
1396
|
+
),
|
|
1397
|
+
...plan.hoists.flatMap((node) => node.declarations.map((item) => item.id.name)),
|
|
1398
|
+
]);
|
|
1399
|
+
const allocate = (prefix) => {
|
|
1400
|
+
let name = prefix;
|
|
1401
|
+
for (let index = 1; names.has(name); index++) name = `${prefix}${index}`;
|
|
1402
|
+
names.add(name);
|
|
1403
|
+
return name;
|
|
1404
|
+
};
|
|
1405
|
+
const adopt = allocate('_$adoptBindingProgram');
|
|
1406
|
+
const mount = allocate('_$mountBindingProgram');
|
|
1407
|
+
const adoptScalar = plan.scalar ? allocate('_$adoptScalarBindings') : null;
|
|
1408
|
+
const signalFactory = plan.signals ? allocate('_bindingSignals') : null;
|
|
1409
|
+
const styleFactory = plan.styles ? allocate('_bindingStyles') : null;
|
|
1410
|
+
const controlFactory = plan.controls ? allocate('_bindingControls') : null;
|
|
1411
|
+
const hostCapability = plan.hostOperations ? allocate('_bindingHostOperations') : null;
|
|
1412
|
+
const listCapability = plan.lists ? allocate('_bindingList') : null;
|
|
1413
|
+
const projectionFactory = plan.projectionsEnabled ? allocate('_bindingProjections') : null;
|
|
1414
|
+
// Imported child artifacts carry their optional capabilities. Forward a
|
|
1415
|
+
// factory rather than loading every capability for every parent view.
|
|
1416
|
+
const capability = (name, local) => {
|
|
1417
|
+
const value = local
|
|
1418
|
+
? b.id(local)
|
|
1419
|
+
: [...plan.childPrograms]
|
|
1420
|
+
.map((child) =>
|
|
1421
|
+
name === 'list' || name === 'hostOperations'
|
|
1422
|
+
? b.logical(
|
|
1423
|
+
'??',
|
|
1424
|
+
b.member(b.id(child), name),
|
|
1425
|
+
b.member(b.member(b.id(child), 'adopt'), name),
|
|
1426
|
+
)
|
|
1427
|
+
: b.member(b.id(child), name),
|
|
1428
|
+
)
|
|
1429
|
+
.reduce((left, right) => (left ? b.logical('||', left, right) : right), null);
|
|
1430
|
+
return value ? [b.prop('init', b.id(name), value)] : [];
|
|
1431
|
+
};
|
|
1432
|
+
const root = plan.scalar ? b.id(allocate('_bindingRoot')) : null;
|
|
1433
|
+
const scalar =
|
|
1434
|
+
plan.scalar &&
|
|
1435
|
+
b.object(
|
|
1436
|
+
scalarProperties(
|
|
1437
|
+
plan.scalar,
|
|
1438
|
+
b.arrow(plan.fn.params, b.call(b.member(root, 'project'), b.array(plan.fn.params))),
|
|
1439
|
+
plan.scalar.bindings.some((binding) => binding[1] === 'classGroup')
|
|
1440
|
+
? b.member(root, 'createClassGroup')
|
|
1441
|
+
: null,
|
|
1442
|
+
signalFactory ? b.id(signalFactory) : null,
|
|
1443
|
+
styleFactory ? b.id(styleFactory) : null,
|
|
1444
|
+
controlFactory ? b.id(controlFactory) : null,
|
|
1445
|
+
projectionFactory ? b.id(projectionFactory) : null,
|
|
1446
|
+
),
|
|
1447
|
+
);
|
|
1448
|
+
return {
|
|
1449
|
+
...ast,
|
|
1450
|
+
body: [
|
|
1451
|
+
...importNodes,
|
|
1452
|
+
...plan.dependencies,
|
|
1453
|
+
...(projectionFactory
|
|
1454
|
+
? [
|
|
1455
|
+
inheritHookMemoOrigin(
|
|
1456
|
+
b.imports(
|
|
1457
|
+
[['__createBindingProjections', projectionFactory]],
|
|
1458
|
+
'octane/dom-binding-projections',
|
|
1459
|
+
),
|
|
1460
|
+
plan.fn,
|
|
1461
|
+
),
|
|
1462
|
+
]
|
|
1463
|
+
: []),
|
|
1464
|
+
...(styleFactory
|
|
1465
|
+
? [
|
|
1466
|
+
inheritHookMemoOrigin(
|
|
1467
|
+
b.imports([['__createBindingStyles', styleFactory]], 'octane/dom-binding-styles'),
|
|
1468
|
+
plan.fn,
|
|
1469
|
+
),
|
|
1470
|
+
]
|
|
1471
|
+
: []),
|
|
1472
|
+
...(controlFactory
|
|
1473
|
+
? [
|
|
1474
|
+
inheritHookMemoOrigin(
|
|
1475
|
+
b.imports(
|
|
1476
|
+
[['__createBindingControls', controlFactory]],
|
|
1477
|
+
'octane/dom-binding-controls',
|
|
1478
|
+
),
|
|
1479
|
+
plan.fn,
|
|
1480
|
+
),
|
|
1481
|
+
]
|
|
1482
|
+
: []),
|
|
1483
|
+
...(signalFactory
|
|
1484
|
+
? [
|
|
1485
|
+
inheritHookMemoOrigin(
|
|
1486
|
+
b.imports(
|
|
1487
|
+
[['__createBindingSignals', signalFactory]],
|
|
1488
|
+
'octane/dom-binding-signals',
|
|
1489
|
+
),
|
|
1490
|
+
plan.fn,
|
|
1491
|
+
),
|
|
1492
|
+
]
|
|
1493
|
+
: []),
|
|
1494
|
+
inheritHookMemoOrigin(
|
|
1495
|
+
b.imports(
|
|
1496
|
+
[
|
|
1497
|
+
['__adoptLeanBindingProgram', adopt],
|
|
1498
|
+
['__mountLeanBindingProgram', mount],
|
|
1499
|
+
...(listCapability ? [['__bindingList', listCapability]] : []),
|
|
1500
|
+
...(hostCapability ? [['__bindingProgramHostOperations', hostCapability]] : []),
|
|
1501
|
+
],
|
|
1502
|
+
'octane/dom-binding-program',
|
|
1503
|
+
),
|
|
1504
|
+
plan.fn,
|
|
1505
|
+
),
|
|
1506
|
+
...(adoptScalar
|
|
1507
|
+
? [
|
|
1508
|
+
inheritHookMemoOrigin(
|
|
1509
|
+
b.imports([['__adoptBindings', adoptScalar]], 'octane/dom-bindings'),
|
|
1510
|
+
plan.fn,
|
|
1511
|
+
),
|
|
1512
|
+
]
|
|
1513
|
+
: []),
|
|
1514
|
+
...plan.hoists,
|
|
1515
|
+
...(root ? [inheritHookMemoOrigin(b.const(root, plan.root), plan.fn)] : []),
|
|
1516
|
+
inheritHookMemoOrigin(
|
|
1517
|
+
b.export_default(
|
|
1518
|
+
b.object([
|
|
1519
|
+
b.prop('init', b.id('id'), b.literal(plan.id)),
|
|
1520
|
+
b.prop('init', b.id('root'), root ?? plan.root),
|
|
1521
|
+
...(plan.prepareProps
|
|
1522
|
+
? [b.prop('init', b.id('prepareProps'), plan.prepareProps)]
|
|
1523
|
+
: []),
|
|
1524
|
+
b.prop('init', b.id('adopt'), b.id(adopt)),
|
|
1525
|
+
b.prop('init', b.id('mount'), b.id(mount)),
|
|
1526
|
+
...(scalar ? [b.prop('init', b.id('scalar'), scalar)] : []),
|
|
1527
|
+
...(adoptScalar ? [b.prop('init', b.id('adoptScalar'), b.id(adoptScalar))] : []),
|
|
1528
|
+
...capability('connectStyle', styleFactory),
|
|
1529
|
+
...capability('connectProjection', projectionFactory),
|
|
1530
|
+
...capability('createControls', controlFactory),
|
|
1531
|
+
...capability('list', listCapability),
|
|
1532
|
+
...capability('hostOperations', hostCapability),
|
|
1533
|
+
...(signalFactory
|
|
1534
|
+
? [b.prop('init', b.id('connectSignal'), b.id(signalFactory))]
|
|
1535
|
+
: []),
|
|
1536
|
+
]),
|
|
1537
|
+
),
|
|
1538
|
+
plan.fn,
|
|
1539
|
+
),
|
|
1540
|
+
],
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
const project = inheritHookMemoOrigin(
|
|
1544
|
+
b.arrow(
|
|
1545
|
+
plan.fn.params,
|
|
1546
|
+
plan.projections.length > 0
|
|
1547
|
+
? b.block([...plan.projections, b.return(b.array(plan.values))])
|
|
1548
|
+
: b.array(plan.values),
|
|
1549
|
+
),
|
|
1550
|
+
plan.fn,
|
|
1551
|
+
);
|
|
1552
|
+
let classFactory = null;
|
|
1553
|
+
let signalFactory = null;
|
|
1554
|
+
let styleFactory = null;
|
|
1555
|
+
let controlFactory = null;
|
|
1556
|
+
let projectionFactory = null;
|
|
1557
|
+
if (plan.projectionGroups.length > 0) {
|
|
1558
|
+
projectionFactory = lexical.domBindingAllocateName('_bindingProjections');
|
|
1559
|
+
importNodes.push(
|
|
1560
|
+
inheritHookMemoOrigin(
|
|
1561
|
+
b.imports(
|
|
1562
|
+
[['__createBindingProjections', projectionFactory]],
|
|
1563
|
+
'octane/dom-binding-projections',
|
|
1564
|
+
),
|
|
1565
|
+
plan.fn,
|
|
1566
|
+
),
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
if (plan.styleIndices.length > 0) {
|
|
1570
|
+
styleFactory = lexical.domBindingAllocateName('_bindingStyles');
|
|
1571
|
+
importNodes.push(
|
|
1572
|
+
inheritHookMemoOrigin(
|
|
1573
|
+
b.imports([['__createBindingStyles', styleFactory]], 'octane/dom-binding-styles'),
|
|
1574
|
+
plan.fn,
|
|
1575
|
+
),
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
if (plan.bindings.some((binding) => binding[1] === 'control')) {
|
|
1579
|
+
controlFactory = lexical.domBindingAllocateName('_bindingControls');
|
|
1580
|
+
importNodes.push(
|
|
1581
|
+
inheritHookMemoOrigin(
|
|
1582
|
+
b.imports([['__createBindingControls', controlFactory]], 'octane/dom-binding-controls'),
|
|
1583
|
+
plan.fn,
|
|
1584
|
+
),
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
if (plan.signalIndices.length > 0) {
|
|
1588
|
+
signalFactory = lexical.domBindingAllocateName('_bindingSignals');
|
|
1589
|
+
importNodes.push(
|
|
1590
|
+
inheritHookMemoOrigin(
|
|
1591
|
+
b.imports([['__createBindingSignals', signalFactory]], 'octane/dom-binding-signals'),
|
|
1592
|
+
plan.fn,
|
|
1593
|
+
),
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
if (plan.bindings.some((binding) => binding[1] === 'classGroup')) {
|
|
1597
|
+
classFactory = '_$createBindingClassGroup';
|
|
1598
|
+
while (needed.has(classFactory)) classFactory += '$';
|
|
1599
|
+
importNodes.push(
|
|
1600
|
+
inheritHookMemoOrigin(
|
|
1601
|
+
b.imports([['createBindingClassGroup', classFactory]], 'octane/dom-binding-classes'),
|
|
1602
|
+
plan.fn,
|
|
1603
|
+
),
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
const adopt = lexical.domBindingAllocateName('_$adoptBindings');
|
|
1607
|
+
importNodes.push(
|
|
1608
|
+
inheritHookMemoOrigin(b.imports([['__adoptBindings', adopt]], 'octane/dom-bindings'), plan.fn),
|
|
1609
|
+
);
|
|
1610
|
+
return {
|
|
1611
|
+
...ast,
|
|
1612
|
+
body: [
|
|
1613
|
+
...importNodes,
|
|
1614
|
+
inheritHookMemoOrigin(
|
|
1615
|
+
b.export_default(
|
|
1616
|
+
b.object([
|
|
1617
|
+
b.prop('init', b.id('adopt'), b.id(adopt)),
|
|
1618
|
+
...scalarProperties(
|
|
1619
|
+
plan,
|
|
1620
|
+
project,
|
|
1621
|
+
classFactory ? b.id(classFactory) : null,
|
|
1622
|
+
signalFactory ? b.id(signalFactory) : null,
|
|
1623
|
+
styleFactory ? b.id(styleFactory) : null,
|
|
1624
|
+
controlFactory ? b.id(controlFactory) : null,
|
|
1625
|
+
projectionFactory ? b.id(projectionFactory) : null,
|
|
1626
|
+
),
|
|
1627
|
+
]),
|
|
1628
|
+
),
|
|
1629
|
+
plan.fn,
|
|
1630
|
+
),
|
|
1631
|
+
],
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function isRuntimeReference(node, lexical, parent, key) {
|
|
1636
|
+
if (node.type === 'JSXIdentifier')
|
|
1637
|
+
return (
|
|
1638
|
+
(parent?.type === 'JSXOpeningElement' || parent?.type === 'JSXClosingElement') &&
|
|
1639
|
+
key === 'name'
|
|
1640
|
+
);
|
|
1641
|
+
if (
|
|
1642
|
+
node.type !== 'Identifier' ||
|
|
1643
|
+
lexical.bindingNodes.has(node) ||
|
|
1644
|
+
lexical.nonReferenceNodes.has(node)
|
|
1645
|
+
)
|
|
1646
|
+
return false;
|
|
1647
|
+
if (parent?.type?.startsWith('Import')) return false;
|
|
1648
|
+
if (parent?.type === 'ExportSpecifier' && key === 'exported') return false;
|
|
1649
|
+
if (
|
|
1650
|
+
(parent?.type === 'MemberExpression' || parent?.type === 'OptionalMemberExpression') &&
|
|
1651
|
+
key === 'property' &&
|
|
1652
|
+
!parent.computed
|
|
1653
|
+
)
|
|
1654
|
+
return false;
|
|
1655
|
+
if (parent?.type === 'Property' && key === 'key' && !parent.computed) return false;
|
|
1656
|
+
return true;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/** Lower only statically proven imported calls; authored types keep the real view import. */
|
|
1660
|
+
function lowerAdoptions(ast, filename) {
|
|
1661
|
+
const imports = importedBindings(ast);
|
|
1662
|
+
const intrinsics = new Set(
|
|
1663
|
+
[...imports]
|
|
1664
|
+
.filter(
|
|
1665
|
+
([, value]) =>
|
|
1666
|
+
value.source === 'octane/behavior' &&
|
|
1667
|
+
['adoptBindings', 'mountBindings'].includes(value.imported),
|
|
1668
|
+
)
|
|
1669
|
+
.map(([name]) => name),
|
|
1670
|
+
);
|
|
1671
|
+
if (intrinsics.size === 0) return ast;
|
|
1672
|
+
const lexical = createLexicalAnalysis(ast);
|
|
1673
|
+
const replacements = new Map();
|
|
1674
|
+
const consumed = new Set();
|
|
1675
|
+
const added = [];
|
|
1676
|
+
const names = new Set();
|
|
1677
|
+
const constructionRequests = new Set();
|
|
1678
|
+
walk(ast, (node) => {
|
|
1679
|
+
if (node.type === 'Identifier') names.add(node.name);
|
|
1680
|
+
if (
|
|
1681
|
+
node.type !== 'CallExpression' ||
|
|
1682
|
+
node.callee?.type !== 'Identifier' ||
|
|
1683
|
+
!intrinsics.has(node.callee.name) ||
|
|
1684
|
+
imports.get(node.callee.name).imported !== 'mountBindings' ||
|
|
1685
|
+
lexical.resolveBinding(lexical.nodeScopes.get(node.callee), node.callee.name)?.scope !==
|
|
1686
|
+
lexical.rootScope
|
|
1687
|
+
)
|
|
1688
|
+
return;
|
|
1689
|
+
const view = unwrap(node.arguments[1]);
|
|
1690
|
+
const imported = view?.type === 'Identifier' ? imports.get(view.name) : null;
|
|
1691
|
+
if (
|
|
1692
|
+
imported?.specifier.type === 'ImportSpecifier' &&
|
|
1693
|
+
lexical.resolveBinding(lexical.nodeScopes.get(view), view.name)?.scope === lexical.rootScope
|
|
1694
|
+
)
|
|
1695
|
+
constructionRequests.add(
|
|
1696
|
+
`${imported.source}?${DOM_BINDINGS_QUERY}=${encodeURIComponent(imported.imported)}`,
|
|
1697
|
+
);
|
|
1698
|
+
});
|
|
1699
|
+
const allocate = (prefix) => {
|
|
1700
|
+
let name = prefix;
|
|
1701
|
+
for (let index = 1; names.has(name); index++) name = `${prefix}${index}`;
|
|
1702
|
+
names.add(name);
|
|
1703
|
+
return name;
|
|
1704
|
+
};
|
|
1705
|
+
const helpers = new Map();
|
|
1706
|
+
const queryLocals = new Map();
|
|
1707
|
+
walk(ast, (node) => {
|
|
1708
|
+
if (
|
|
1709
|
+
node.type !== 'CallExpression' ||
|
|
1710
|
+
node.callee?.type !== 'Identifier' ||
|
|
1711
|
+
!intrinsics.has(node.callee.name)
|
|
1712
|
+
)
|
|
1713
|
+
return;
|
|
1714
|
+
if (
|
|
1715
|
+
lexical.resolveBinding(lexical.nodeScopes.get(node.callee), node.callee.name)?.scope !==
|
|
1716
|
+
lexical.rootScope
|
|
1717
|
+
)
|
|
1718
|
+
return;
|
|
1719
|
+
const view = unwrap(node.arguments[1]);
|
|
1720
|
+
const intrinsic = imports.get(node.callee.name).imported;
|
|
1721
|
+
const imported = view?.type === 'Identifier' ? imports.get(view.name) : null;
|
|
1722
|
+
if (
|
|
1723
|
+
node.optional ||
|
|
1724
|
+
node.arguments.length < 3 ||
|
|
1725
|
+
node.arguments.length > 4 ||
|
|
1726
|
+
node.arguments.some((arg) => arg.type === 'SpreadElement') ||
|
|
1727
|
+
!imported?.imported ||
|
|
1728
|
+
imported.specifier.type !== 'ImportSpecifier' ||
|
|
1729
|
+
lexical.resolveBinding(lexical.nodeScopes.get(view), view.name)?.scope !==
|
|
1730
|
+
lexical.rootScope ||
|
|
1731
|
+
/[?#]/.test(imported.source)
|
|
1732
|
+
) {
|
|
1733
|
+
error(
|
|
1734
|
+
filename,
|
|
1735
|
+
node,
|
|
1736
|
+
'adoptBindings requires a directly imported named view and explicit root, source and options arguments',
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
let helper = helpers.get(intrinsic);
|
|
1740
|
+
if (intrinsic === 'mountBindings' && helper === undefined) {
|
|
1741
|
+
helper = allocate(`_$${intrinsic}`);
|
|
1742
|
+
helpers.set(intrinsic, helper);
|
|
1743
|
+
added.push(
|
|
1744
|
+
inheritHookMemoOrigin(b.imports([[`__${intrinsic}`, helper]], 'octane/dom-bindings'), node),
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
const adoptionRequest = `${imported.source}?${DOM_BINDINGS_QUERY}=${encodeURIComponent(imported.imported)}`;
|
|
1748
|
+
const request = `${adoptionRequest}${constructionRequests.has(adoptionRequest) ? `&${DOM_BINDINGS_MOUNT_QUERY}=1` : ''}`;
|
|
1749
|
+
let local = queryLocals.get(request);
|
|
1750
|
+
if (local === undefined) {
|
|
1751
|
+
local = allocate('_$bindingView');
|
|
1752
|
+
queryLocals.set(request, local);
|
|
1753
|
+
added.push(inheritHookMemoOrigin(b.imports([['default', local]], request), node));
|
|
1754
|
+
}
|
|
1755
|
+
consumed.add(node.callee);
|
|
1756
|
+
consumed.add(view);
|
|
1757
|
+
// Replace leaves so nested adoptions in source/options remain traversable.
|
|
1758
|
+
// The artifact selects its own implementation. A generic adopter would
|
|
1759
|
+
// retain fixed-layout machinery even when every view is structural.
|
|
1760
|
+
replacements.set(
|
|
1761
|
+
node.callee,
|
|
1762
|
+
inheritHookMemoOrigin(
|
|
1763
|
+
intrinsic === 'adoptBindings' ? b.member(b.id(local), 'adopt') : b.id(helper),
|
|
1764
|
+
node.callee,
|
|
1765
|
+
),
|
|
1766
|
+
);
|
|
1767
|
+
replacements.set(view, inheritHookMemoOrigin(b.id(local), view));
|
|
1768
|
+
});
|
|
1769
|
+
const remaining = new Set();
|
|
1770
|
+
walk(ast, (node, parent, key) => {
|
|
1771
|
+
if (node.type.startsWith('TS') && !UNWRAP.has(node.type)) return false;
|
|
1772
|
+
if (
|
|
1773
|
+
isRuntimeReference(node, lexical, parent, key) &&
|
|
1774
|
+
!consumed.has(node) &&
|
|
1775
|
+
lexical.resolveBinding(lexical.nodeScopes.get(node), node.name)?.scope === lexical.rootScope
|
|
1776
|
+
)
|
|
1777
|
+
remaining.add(node.name);
|
|
1778
|
+
});
|
|
1779
|
+
for (const name of intrinsics) {
|
|
1780
|
+
if (remaining.has(name))
|
|
1781
|
+
error(
|
|
1782
|
+
filename,
|
|
1783
|
+
imports.get(name).specifier,
|
|
1784
|
+
'adoptBindings must be called directly in a compiler-owned .tsrx/.tsx module',
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
const candidates = new Set([...consumed].map((node) => node.name));
|
|
1788
|
+
for (const node of ast.body) {
|
|
1789
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
1790
|
+
const specifiers = node.specifiers.filter(
|
|
1791
|
+
(specifier) => !candidates.has(specifier.local.name) || remaining.has(specifier.local.name),
|
|
1792
|
+
);
|
|
1793
|
+
if (specifiers.length !== node.specifiers.length)
|
|
1794
|
+
replacements.set(node, specifiers.length === 0 ? null : { ...node, specifiers });
|
|
1795
|
+
}
|
|
1796
|
+
const lowered = mapCow(ast, replacements);
|
|
1797
|
+
return { ...lowered, body: [...added, ...lowered.body] };
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/** Annotate normal SSR/client output, or select a pure adoption descriptor Program. */
|
|
1801
|
+
export function prepareDomBindings(ast, source, filename, selectedExport, helpers) {
|
|
1802
|
+
const imports = importedBindings(ast);
|
|
1803
|
+
const lexical = createLexicalAnalysis(ast);
|
|
1804
|
+
lexical.domBindingConstants = new Map(
|
|
1805
|
+
ast.body.flatMap((statement) => {
|
|
1806
|
+
const declaration = statement.declaration ?? statement;
|
|
1807
|
+
return declaration.type === 'VariableDeclaration' && declaration.kind === 'const'
|
|
1808
|
+
? declaration.declarations
|
|
1809
|
+
.filter((item) => item.id.type === 'Identifier' && item.init)
|
|
1810
|
+
.map((item) => [item.id.name, item])
|
|
1811
|
+
: [];
|
|
1812
|
+
}),
|
|
1813
|
+
);
|
|
1814
|
+
const plans = new Map();
|
|
1815
|
+
const replacements = new Map();
|
|
1816
|
+
const localFunctions = new Map(
|
|
1817
|
+
ast.body.flatMap((statement) => {
|
|
1818
|
+
const fn = statement.declaration ?? statement;
|
|
1819
|
+
return fn.type === 'FunctionDeclaration' && fn.id ? [[fn.id.name, fn]] : [];
|
|
1820
|
+
}),
|
|
1821
|
+
);
|
|
1822
|
+
const inProgress = new Set();
|
|
1823
|
+
const programPlans = new Map();
|
|
1824
|
+
const programNames = new Set(imports.keys());
|
|
1825
|
+
const localConstants = new Map();
|
|
1826
|
+
walk(ast, (node) => {
|
|
1827
|
+
if (node.type === 'Identifier') programNames.add(node.name);
|
|
1828
|
+
if (node.type === 'VariableDeclaration' && node.kind === 'const') {
|
|
1829
|
+
for (const declaration of node.declarations) {
|
|
1830
|
+
if (declaration.id.type !== 'Identifier' || !declaration.init) continue;
|
|
1831
|
+
const scope = lexical.resolveBinding(
|
|
1832
|
+
lexical.nodeScopes.get(declaration.id),
|
|
1833
|
+
declaration.id.name,
|
|
1834
|
+
)?.scope;
|
|
1835
|
+
if (!scope || scope === lexical.rootScope) continue;
|
|
1836
|
+
if (!localConstants.has(scope)) localConstants.set(scope, new Map());
|
|
1837
|
+
localConstants.get(scope).set(declaration.id.name, declaration);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
const localDeclaration = (node) =>
|
|
1842
|
+
node?.type === 'Identifier'
|
|
1843
|
+
? localConstants
|
|
1844
|
+
.get(lexical.resolveBinding(lexical.nodeScopes.get(node), node.name)?.scope)
|
|
1845
|
+
?.get(node.name)
|
|
1846
|
+
: null;
|
|
1847
|
+
const callbackFor = (expression) => {
|
|
1848
|
+
let value = unwrap(expression);
|
|
1849
|
+
const seen = new Set();
|
|
1850
|
+
while (value?.type === 'Identifier') {
|
|
1851
|
+
const declaration = localDeclaration(value);
|
|
1852
|
+
if (!declaration || seen.has(declaration)) return null;
|
|
1853
|
+
seen.add(declaration);
|
|
1854
|
+
value = unwrap(declaration.init);
|
|
1855
|
+
}
|
|
1856
|
+
return ['ArrowFunctionExpression', 'FunctionExpression'].includes(value?.type) ? value : null;
|
|
1857
|
+
};
|
|
1858
|
+
lexical.domBindingCallback = callbackFor;
|
|
1859
|
+
const allocateProgramName = (prefix) => {
|
|
1860
|
+
let name = prefix;
|
|
1861
|
+
for (let i = 1; programNames.has(name); i++) name = `${prefix}${i}`;
|
|
1862
|
+
programNames.add(name);
|
|
1863
|
+
return name;
|
|
1864
|
+
};
|
|
1865
|
+
lexical.domBindingAllocateName = allocateProgramName;
|
|
1866
|
+
lexical.domBindingCanCarrySignal = helpers.canCarryDirectSignalHandle;
|
|
1867
|
+
const refCallbacks = [];
|
|
1868
|
+
walk(ast, (node) => {
|
|
1869
|
+
if ((node.type === 'JSXAttribute' || node.type === 'Attribute') && attrName(node) === 'ref') {
|
|
1870
|
+
const callback = callbackFor(attrValueForBinding(node));
|
|
1871
|
+
if (callback) refCallbacks.push(callback);
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
const refDependencies = refCallbacks.length
|
|
1875
|
+
? helpers.analyzeCallbackDependencies(ast, refCallbacks)
|
|
1876
|
+
: new Map();
|
|
1877
|
+
const isUnbound = (expression) => {
|
|
1878
|
+
const value = unwrap(expression);
|
|
1879
|
+
if (value?.type !== 'CallExpression' || value.callee.type !== 'Identifier') return false;
|
|
1880
|
+
const imported = imports.get(value.callee.name);
|
|
1881
|
+
return (
|
|
1882
|
+
imported?.source === 'octane/behavior' &&
|
|
1883
|
+
imported.imported === 'unbound' &&
|
|
1884
|
+
lexical.resolveBinding(lexical.nodeScopes.get(value.callee), value.callee.name)?.scope ===
|
|
1885
|
+
lexical.rootScope
|
|
1886
|
+
);
|
|
1887
|
+
};
|
|
1888
|
+
const programFor = (fn, render, props = null, annotationsOnly = false) => {
|
|
1889
|
+
const key = annotationsOnly ? 'annotations' : JSON.stringify(props);
|
|
1890
|
+
if (programPlans.get(fn)?.has(key)) return programPlans.get(fn).get(key);
|
|
1891
|
+
if (inProgress.has(fn))
|
|
1892
|
+
error(filename, fn, 'recursive binding child programs are not supported');
|
|
1893
|
+
inProgress.add(fn);
|
|
1894
|
+
const rest = bindingRestSpreads(fn, render, props, lexical, filename);
|
|
1895
|
+
const setup = statements(fn)
|
|
1896
|
+
.filter((statement) => statement.type === 'VariableDeclaration')
|
|
1897
|
+
.flatMap((statement) => statement.declarations);
|
|
1898
|
+
for (const declaration of annotationsOnly ? [] : setup)
|
|
1899
|
+
if (!callbackFor(declaration.init))
|
|
1900
|
+
assertProjection(
|
|
1901
|
+
declaration.init,
|
|
1902
|
+
filename,
|
|
1903
|
+
imports,
|
|
1904
|
+
lexical,
|
|
1905
|
+
lexical.nodeScopes.get(fn.body) ?? lexical.rootScope,
|
|
1906
|
+
);
|
|
1907
|
+
const setupBindings = new Map(
|
|
1908
|
+
setup.map((declaration) => [
|
|
1909
|
+
declaration.id.name,
|
|
1910
|
+
{
|
|
1911
|
+
scope: lexical.resolveBinding(
|
|
1912
|
+
lexical.nodeScopes.get(declaration.id) ?? lexical.nodeScopes.get(fn.body),
|
|
1913
|
+
declaration.id.name,
|
|
1914
|
+
)?.scope,
|
|
1915
|
+
declaration,
|
|
1916
|
+
},
|
|
1917
|
+
]),
|
|
1918
|
+
);
|
|
1919
|
+
const projectionBody = (value, expressions, temporaries = []) => {
|
|
1920
|
+
const required = new Set();
|
|
1921
|
+
const visit = (expression) =>
|
|
1922
|
+
walk(expression, (node, parent, key) => {
|
|
1923
|
+
if (!isRuntimeReference(node, lexical, parent, key)) return;
|
|
1924
|
+
const binding = setupBindings.get(node.name);
|
|
1925
|
+
const declaration =
|
|
1926
|
+
binding?.scope ===
|
|
1927
|
+
lexical.resolveBinding(lexical.nodeScopes.get(node) ?? lexical.rootScope, node.name)
|
|
1928
|
+
?.scope
|
|
1929
|
+
? binding?.declaration
|
|
1930
|
+
: undefined;
|
|
1931
|
+
if (!declaration || required.has(declaration)) return;
|
|
1932
|
+
required.add(declaration);
|
|
1933
|
+
visit(declaration.init);
|
|
1934
|
+
});
|
|
1935
|
+
visit(value);
|
|
1936
|
+
for (const temporary of temporaries) {
|
|
1937
|
+
const expression = temporary.declarations[0].init;
|
|
1938
|
+
visit(expression);
|
|
1939
|
+
expressions.push(expression);
|
|
1940
|
+
}
|
|
1941
|
+
if (required.size === 0 && temporaries.length === 0) return value;
|
|
1942
|
+
const declarations = setup.filter((declaration) => required.has(declaration));
|
|
1943
|
+
expressions.push(...declarations.map((declaration) => declaration.init));
|
|
1944
|
+
return b.block([
|
|
1945
|
+
...declarations.map((declaration) =>
|
|
1946
|
+
inheritHookMemoOrigin(b.const(declaration.id, declaration.init), declaration),
|
|
1947
|
+
),
|
|
1948
|
+
...temporaries,
|
|
1949
|
+
b.return(value),
|
|
1950
|
+
]);
|
|
1951
|
+
};
|
|
1952
|
+
const context = {
|
|
1953
|
+
source,
|
|
1954
|
+
filename,
|
|
1955
|
+
helpers,
|
|
1956
|
+
isUnbound,
|
|
1957
|
+
mapCow,
|
|
1958
|
+
imports,
|
|
1959
|
+
lexical,
|
|
1960
|
+
allocateProgramName,
|
|
1961
|
+
projectionBody,
|
|
1962
|
+
annotationsOnly,
|
|
1963
|
+
restSites: rest.sites,
|
|
1964
|
+
parameterNames: bindingParameterNames(fn.params[0], filename),
|
|
1965
|
+
refDependencies: (expression) => {
|
|
1966
|
+
const records = refDependencies.get(callbackFor(expression)) ?? null;
|
|
1967
|
+
if (records?.some((record) => callbackFor(record.node)))
|
|
1968
|
+
error(filename, expression, 'native ref callbacks cannot capture local callbacks');
|
|
1969
|
+
return records;
|
|
1970
|
+
},
|
|
1971
|
+
isChildSlot: (expression) => {
|
|
1972
|
+
const value = unwrap(expression);
|
|
1973
|
+
const parameter = fn.params[0];
|
|
1974
|
+
const member =
|
|
1975
|
+
value?.type === 'MemberExpression' &&
|
|
1976
|
+
!value.optional &&
|
|
1977
|
+
(value.computed ? value.property.value : value.property.name) === 'children';
|
|
1978
|
+
let reference = member ? value.object : null;
|
|
1979
|
+
let binding = parameter;
|
|
1980
|
+
if (parameter?.type === 'ObjectPattern') {
|
|
1981
|
+
const children = parameter.properties.find(
|
|
1982
|
+
(property) =>
|
|
1983
|
+
property.type === 'Property' &&
|
|
1984
|
+
(property.key.name ?? property.key.value) === 'children' &&
|
|
1985
|
+
(member ||
|
|
1986
|
+
(property.value.type === 'AssignmentPattern' ? property.value.left : property.value)
|
|
1987
|
+
.name === value?.name),
|
|
1988
|
+
);
|
|
1989
|
+
if (member) {
|
|
1990
|
+
if (children) return false;
|
|
1991
|
+
binding = parameter.properties.find(
|
|
1992
|
+
(property) => property.type === 'RestElement',
|
|
1993
|
+
)?.argument;
|
|
1994
|
+
} else {
|
|
1995
|
+
reference = value;
|
|
1996
|
+
binding = children?.value;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
const fallback = binding?.type === 'AssignmentPattern' ? binding.right : null;
|
|
2000
|
+
if (fallback) binding = binding.left;
|
|
2001
|
+
const matches =
|
|
2002
|
+
reference?.type === 'Identifier' &&
|
|
2003
|
+
reference.name === binding?.name &&
|
|
2004
|
+
lexical.resolveBinding(lexical.nodeScopes.get(reference), reference.name)?.scope ===
|
|
2005
|
+
lexical.resolveBinding(lexical.nodeScopes.get(binding), binding.name)?.scope;
|
|
2006
|
+
if (matches && fallback && fallback.value !== null)
|
|
2007
|
+
error(filename, fallback, 'binding child slot defaults must be null');
|
|
2008
|
+
return matches;
|
|
2009
|
+
},
|
|
2010
|
+
canCarryValue: (expression) => {
|
|
2011
|
+
if (!helpers.canCarryDirectSignalHandle(expression)) return false;
|
|
2012
|
+
let renderable = false;
|
|
2013
|
+
walk(expression, (node) => {
|
|
2014
|
+
if (node.type.startsWith('JSX')) renderable = true;
|
|
2015
|
+
});
|
|
2016
|
+
return !renderable;
|
|
2017
|
+
},
|
|
2018
|
+
localProgram: (name, props, annotationsOnly = false) => {
|
|
2019
|
+
const child = localFunctions.get(name);
|
|
2020
|
+
return child
|
|
2021
|
+
? programFor(child, bindingRender(child, filename, false), props, annotationsOnly)
|
|
2022
|
+
: null;
|
|
2023
|
+
},
|
|
2024
|
+
nativePlan: (element, namespace, ancestors) =>
|
|
2025
|
+
planView(fn, filename, source, imports, lexical, { element, namespace, ancestors }),
|
|
2026
|
+
assertProjection: (expression) =>
|
|
2027
|
+
assertProjection(
|
|
2028
|
+
expression,
|
|
2029
|
+
filename,
|
|
2030
|
+
imports,
|
|
2031
|
+
lexical,
|
|
2032
|
+
lexical.nodeScopes.get(fn.body) ?? lexical.rootScope,
|
|
2033
|
+
),
|
|
2034
|
+
assertAdapter: (expression) => {
|
|
2035
|
+
const seen = new Set();
|
|
2036
|
+
const visit = (value) =>
|
|
2037
|
+
walk(value, (node, parent, key) => {
|
|
2038
|
+
if (node.type.startsWith('TS') && !UNWRAP.has(node.type)) return false;
|
|
2039
|
+
if (!isRuntimeReference(node, lexical, parent, key)) return;
|
|
2040
|
+
if (
|
|
2041
|
+
lexical.resolveBinding(lexical.nodeScopes.get(node) ?? lexical.rootScope, node.name)
|
|
2042
|
+
?.scope === lexical.rootScope &&
|
|
2043
|
+
!imports.has(node.name)
|
|
2044
|
+
)
|
|
2045
|
+
error(
|
|
2046
|
+
filename,
|
|
2047
|
+
node,
|
|
2048
|
+
'native adapters must receive module-local callbacks through props or imports',
|
|
2049
|
+
);
|
|
2050
|
+
const declaration = localDeclaration(node);
|
|
2051
|
+
if (declaration && callbackFor(declaration.init) && !seen.has(declaration)) {
|
|
2052
|
+
seen.add(declaration);
|
|
2053
|
+
visit(declaration.init);
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
visit(expression);
|
|
2057
|
+
},
|
|
2058
|
+
};
|
|
2059
|
+
const plan = planBindingProgram(fn, rest.render, context);
|
|
2060
|
+
inProgress.delete(fn);
|
|
2061
|
+
if (!programPlans.has(fn)) programPlans.set(fn, new Map());
|
|
2062
|
+
programPlans.get(fn).set(key, plan);
|
|
2063
|
+
const authored =
|
|
2064
|
+
!annotationsOnly && rest.hasRest
|
|
2065
|
+
? planBindingProgram(fn, render, { ...context, annotationsOnly: true })
|
|
2066
|
+
: plan;
|
|
2067
|
+
if (annotationsOnly) {
|
|
2068
|
+
const structuralPresentation =
|
|
2069
|
+
!rest.hasRest && plan.structural && fn.body.type === 'JSXCodeBlock';
|
|
2070
|
+
replacements.set(fn, {
|
|
2071
|
+
...mapCow(fn, authored.replacements),
|
|
2072
|
+
_octaneBindingView: { id: plan.id },
|
|
2073
|
+
_octanePresentationHydration: {
|
|
2074
|
+
id: plan.id,
|
|
2075
|
+
supported: structuralPresentation,
|
|
2076
|
+
...(structuralPresentation ? { structural: true } : {}),
|
|
2077
|
+
...(rest.hasRest && fn.body.type === 'JSXCodeBlock'
|
|
2078
|
+
? { structural: true, conditionalRest: true }
|
|
2079
|
+
: {}),
|
|
2080
|
+
},
|
|
2081
|
+
});
|
|
2082
|
+
return plan;
|
|
2083
|
+
}
|
|
2084
|
+
const field = (name) =>
|
|
2085
|
+
plan.root.properties.find((property) => property.key.name === name)?.value;
|
|
2086
|
+
const fixedPresentation =
|
|
2087
|
+
!rest.hasRest &&
|
|
2088
|
+
fn.body.type === 'JSXCodeBlock' &&
|
|
2089
|
+
!plan.controls &&
|
|
2090
|
+
field('regions').elements.length === 0 &&
|
|
2091
|
+
field('bindings').elements.every((binding) => binding.elements[1].value !== 'text');
|
|
2092
|
+
const conditionalRest =
|
|
2093
|
+
rest.hasRest && rest.conditional && plan.structural && fn.body.type === 'JSXCodeBlock';
|
|
2094
|
+
plan.structural &&= !rest.hasRest && fn.body.type === 'JSXCodeBlock';
|
|
2095
|
+
const structuralPresentation = !fixedPresentation && plan.structural;
|
|
2096
|
+
let createHandoff;
|
|
2097
|
+
if (structuralPresentation || conditionalRest) {
|
|
2098
|
+
createHandoff = allocateProgramName('_bindingHandoff');
|
|
2099
|
+
plan.dependencies.push(
|
|
2100
|
+
inheritHookMemoOrigin(
|
|
2101
|
+
b.imports(
|
|
2102
|
+
[['__createStructuralBindingHandoff', createHandoff]],
|
|
2103
|
+
'octane/dom-binding-program',
|
|
2104
|
+
),
|
|
2105
|
+
fn,
|
|
2106
|
+
),
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
plan.root = {
|
|
2110
|
+
...plan.root,
|
|
2111
|
+
properties: [
|
|
2112
|
+
...plan.root.properties,
|
|
2113
|
+
b.prop(
|
|
2114
|
+
'init',
|
|
2115
|
+
b.id('handoff'),
|
|
2116
|
+
b.literal(structuralPresentation ? 'structural' : fixedPresentation),
|
|
2117
|
+
),
|
|
2118
|
+
...(createHandoff ? [b.prop('init', b.id('createHandoff'), b.id(createHandoff))] : []),
|
|
2119
|
+
// A receipt is not a lease capability. The runtime must explicitly
|
|
2120
|
+
// validate the caller/range and prepare its host writers before takeover.
|
|
2121
|
+
...(rest.hasRest && props !== null
|
|
2122
|
+
? [b.prop('init', b.id('closedProps'), b.array(props.map((name) => b.literal(name))))]
|
|
2123
|
+
: []),
|
|
2124
|
+
...(conditionalRest ? [b.prop('init', b.id('conditionalRest'), b.literal(true))] : []),
|
|
2125
|
+
],
|
|
2126
|
+
};
|
|
2127
|
+
replacements.set(fn, {
|
|
2128
|
+
...mapCow(fn, authored.replacements),
|
|
2129
|
+
_octaneBindingView: { id: plan.id },
|
|
2130
|
+
// Normal renderer ownership transfer is narrower than binding-program
|
|
2131
|
+
// adoption. Keep its proof separate from the existing SSR view stamp.
|
|
2132
|
+
_octanePresentationHydration: {
|
|
2133
|
+
id: plan.id,
|
|
2134
|
+
supported: fixedPresentation || structuralPresentation,
|
|
2135
|
+
...(structuralPresentation || conditionalRest ? { structural: true } : {}),
|
|
2136
|
+
...(conditionalRest ? { conditionalRest: true } : {}),
|
|
2137
|
+
},
|
|
2138
|
+
});
|
|
2139
|
+
return plan;
|
|
2140
|
+
};
|
|
2141
|
+
walk(ast, (node, parent) => {
|
|
2142
|
+
if (
|
|
2143
|
+
!['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(node.type)
|
|
2144
|
+
)
|
|
2145
|
+
return;
|
|
2146
|
+
if (!statements(node).some(isDirective)) return;
|
|
2147
|
+
if (
|
|
2148
|
+
node.type !== 'FunctionDeclaration' ||
|
|
2149
|
+
parent?.type !== 'ExportNamedDeclaration' ||
|
|
2150
|
+
!ast.body.includes(parent) ||
|
|
2151
|
+
!node.id
|
|
2152
|
+
) {
|
|
2153
|
+
error(filename, node, 'binding views must be named top-level exported functions');
|
|
2154
|
+
}
|
|
2155
|
+
const render = bindingRender(node, filename);
|
|
2156
|
+
if (
|
|
2157
|
+
selectedExport !== node.id.name &&
|
|
2158
|
+
bindingRestSpreads(node, render, null, lexical, filename).hasRest
|
|
2159
|
+
) {
|
|
2160
|
+
programFor(node, render, null, true);
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2163
|
+
const structural =
|
|
2164
|
+
node.params[0]?.type === 'ObjectPattern' ||
|
|
2165
|
+
needsBindingProgram(render, isUnbound) ||
|
|
2166
|
+
statements(node).some((statement) => statement.type === 'VariableDeclaration');
|
|
2167
|
+
if (structural || (helpers.mount && selectedExport === node.id.name)) {
|
|
2168
|
+
const plan = programFor(node, render, selectedExport === node.id.name ? helpers.props : null);
|
|
2169
|
+
if (!structural) plan.scalar = planView(node, filename, source, imports, lexical);
|
|
2170
|
+
plans.set(node.id.name, plan);
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
const plan = planView(node, filename, source, imports, lexical);
|
|
2174
|
+
plans.set(node.id.name, plan);
|
|
2175
|
+
const rewritten = new Map([...plan.unbound, ...plan.classAttributes]);
|
|
2176
|
+
// Rewrite from leaves to root so parent replacements retain every child
|
|
2177
|
+
// address and stripped ownership intrinsic without mutating the parser AST.
|
|
2178
|
+
for (let index = plan.elements.length - 1; index >= 0; index--) {
|
|
2179
|
+
if (index !== 0 && !plan.addressed) continue;
|
|
2180
|
+
const element = plan.elements[index];
|
|
2181
|
+
const markers = [];
|
|
2182
|
+
if (index === 0) {
|
|
2183
|
+
markers.push(
|
|
2184
|
+
inheritHookMemoOrigin(b.jsx_attribute(b.jsx_id(MARKER), b.literal(plan.id)), element),
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
if (plan.addressed) {
|
|
2188
|
+
markers.push(
|
|
2189
|
+
inheritHookMemoOrigin(
|
|
2190
|
+
b.jsx_attribute(b.jsx_id(NODE_MARKER), b.literal(`${plan.id}:${index}`)),
|
|
2191
|
+
element,
|
|
2192
|
+
),
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
const target = mapCow(element, rewritten);
|
|
2196
|
+
rewritten.set(
|
|
2197
|
+
element,
|
|
2198
|
+
target.openingElement
|
|
2199
|
+
? {
|
|
2200
|
+
...target,
|
|
2201
|
+
openingElement: {
|
|
2202
|
+
...target.openingElement,
|
|
2203
|
+
attributes: [...target.openingElement.attributes, ...markers],
|
|
2204
|
+
},
|
|
2205
|
+
}
|
|
2206
|
+
: { ...target, attributes: [...target.attributes, ...markers] },
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
for (const [node, replacement] of rewritten) replacements.set(node, replacement);
|
|
2210
|
+
});
|
|
2211
|
+
if (selectedExport !== null) {
|
|
2212
|
+
const plan = plans.get(selectedExport);
|
|
2213
|
+
if (!plan)
|
|
2214
|
+
error(
|
|
2215
|
+
filename,
|
|
2216
|
+
ast,
|
|
2217
|
+
`export ${JSON.stringify(selectedExport)} is not an opted-in static binding view`,
|
|
2218
|
+
);
|
|
2219
|
+
helpers.collectConstants?.(
|
|
2220
|
+
shareableConstants(
|
|
2221
|
+
ast,
|
|
2222
|
+
[...projectionDependencies(plan, lexical)].filter((name) =>
|
|
2223
|
+
lexical.domBindingConstants.has(name),
|
|
2224
|
+
),
|
|
2225
|
+
lexical,
|
|
2226
|
+
imports,
|
|
2227
|
+
),
|
|
2228
|
+
);
|
|
2229
|
+
return projectProgram(ast, plan, filename, lexical);
|
|
2230
|
+
}
|
|
2231
|
+
if (helpers.collectConstants) {
|
|
2232
|
+
const names = new Set();
|
|
2233
|
+
for (const fn of new Set(
|
|
2234
|
+
[...plans.values()].map((plan) => plan.fn).concat([...programPlans.keys()]),
|
|
2235
|
+
)) {
|
|
2236
|
+
// Open rest-prop views only need annotations in the ordinary renderer;
|
|
2237
|
+
// their authored body still identifies the shared module constants.
|
|
2238
|
+
for (const name of projectionDependencies({ values: fn }, lexical)) {
|
|
2239
|
+
if (lexical.domBindingConstants.has(name)) names.add(name);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
helpers.collectConstants(shareableConstants(ast, [...names], lexical, imports));
|
|
2243
|
+
}
|
|
2244
|
+
return lowerAdoptions(mapCow(ast, replacements), filename);
|
|
2245
|
+
}
|