@lightspeed-js/core 0.1.0
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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/compile.js +1494 -0
- package/dist/index.js +3060 -0
- package/dist/ssg.js +3620 -0
- package/dist/types/build.d.ts +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/src/agent/index.d.ts +175 -0
- package/dist/types/src/cli/commands/agent.d.ts +9 -0
- package/dist/types/src/cli/commands/build.d.ts +2 -0
- package/dist/types/src/cli/commands/create.d.ts +2 -0
- package/dist/types/src/cli/commands/dev.d.ts +2 -0
- package/dist/types/src/cli/commands/index.d.ts +3 -0
- package/dist/types/src/cli/index.d.ts +2 -0
- package/dist/types/src/cli/types/commands.d.ts +11 -0
- package/dist/types/src/cli/types/index.d.ts +1 -0
- package/dist/types/src/cli/utils/commands.d.ts +5 -0
- package/dist/types/src/cli/utils/index.d.ts +1 -0
- package/dist/types/src/compile/expressions.d.ts +76 -0
- package/dist/types/src/compile/index.d.ts +44 -0
- package/dist/types/src/compile/logic.d.ts +101 -0
- package/dist/types/src/manifest.d.ts +148 -0
- package/dist/types/src/marker.d.ts +85 -0
- package/dist/types/src/parser/index.d.ts +2 -0
- package/dist/types/src/parser/layout.d.ts +32 -0
- package/dist/types/src/parser/lsp.d.ts +56 -0
- package/dist/types/src/parser/route.d.ts +87 -0
- package/dist/types/src/parser/style-source-node.d.ts +19 -0
- package/dist/types/src/parser/style.d.ts +20 -0
- package/dist/types/src/parser/template.d.ts +32 -0
- package/dist/types/src/parser/test.d.ts +1 -0
- package/dist/types/src/runtime/bindings/BindingEvaluator.d.ts +174 -0
- package/dist/types/src/runtime/bindings/index.d.ts +2 -0
- package/dist/types/src/runtime/compiler/ImportTransformer.d.ts +11 -0
- package/dist/types/src/runtime/compiler/index.d.ts +1 -0
- package/dist/types/src/runtime/components/ComponentInstance.d.ts +110 -0
- package/dist/types/src/runtime/components/ComponentRegistry.d.ts +11 -0
- package/dist/types/src/runtime/components/index.d.ts +3 -0
- package/dist/types/src/runtime/components/types/index.d.ts +34 -0
- package/dist/types/src/runtime/core.d.ts +251 -0
- package/dist/types/src/runtime/diagnostics.d.ts +33 -0
- package/dist/types/src/runtime/dom/ForBlock.d.ts +127 -0
- package/dist/types/src/runtime/dom/Mounter.d.ts +223 -0
- package/dist/types/src/runtime/dom/StyleManager.d.ts +57 -0
- package/dist/types/src/runtime/dom/hydration.d.ts +35 -0
- package/dist/types/src/runtime/dom/index.d.ts +7 -0
- package/dist/types/src/runtime/events/EventBinder.d.ts +18 -0
- package/dist/types/src/runtime/events/index.d.ts +1 -0
- package/dist/types/src/runtime/globals/GlobalFunctions.d.ts +24 -0
- package/dist/types/src/runtime/globals/index.d.ts +1 -0
- package/dist/types/src/runtime/helpers.d.ts +141 -0
- package/dist/types/src/runtime/index.d.ts +16 -0
- package/dist/types/src/runtime/reactive/ReactiveProxy.d.ts +63 -0
- package/dist/types/src/runtime/reactive/UpdateScheduler.d.ts +9 -0
- package/dist/types/src/runtime/reactive/index.d.ts +2 -0
- package/dist/types/src/runtime/signals/SignalManager.d.ts +10 -0
- package/dist/types/src/runtime/signals/index.d.ts +3 -0
- package/dist/types/src/runtime/signals/signals.d.ts +16 -0
- package/dist/types/src/runtime/signals/types/index.d.ts +1 -0
- package/dist/types/src/runtime/signals/types/signals.d.ts +7 -0
- package/dist/types/src/runtime/state/Cell.d.ts +68 -0
- package/dist/types/src/runtime/state/CellManager.d.ts +101 -0
- package/dist/types/src/runtime/state/index.d.ts +4 -0
- package/dist/types/src/specifiers.d.ts +16 -0
- package/dist/types/src/ssg/index.d.ts +21 -0
- package/dist/types/src/ssg/rebuild.d.ts +49 -0
- package/dist/types/src/ssg/render.d.ts +104 -0
- package/dist/types/src/types/lsp.d.ts +22 -0
- package/dist/types/src/types/optional.d.ts +14 -0
- package/dist/types/src/types/virtual.d.ts +4 -0
- package/dist/types/src/vite/csp.d.ts +12 -0
- package/dist/types/src/vite/index.d.ts +2 -0
- package/dist/types/src/vite/layout.d.ts +17 -0
- package/dist/types/src/vite/plugin.d.ts +69 -0
- package/dist/types/src/vite/routes.d.ts +139 -0
- package/dist/types/test/adopted-styles.test.d.ts +1 -0
- package/dist/types/test/adoption.test.d.ts +1 -0
- package/dist/types/test/agent.test.d.ts +1 -0
- package/dist/types/test/audit.test.d.ts +1 -0
- package/dist/types/test/bindings.test.d.ts +1 -0
- package/dist/types/test/branches-in-loops.test.d.ts +1 -0
- package/dist/types/test/build.test.d.ts +1 -0
- package/dist/types/test/buildtime.test.d.ts +1 -0
- package/dist/types/test/built.test.d.ts +1 -0
- package/dist/types/test/cell-props.test.d.ts +1 -0
- package/dist/types/test/comments.test.d.ts +1 -0
- package/dist/types/test/compile-expressions.test.d.ts +1 -0
- package/dist/types/test/compile-logic.test.d.ts +1 -0
- package/dist/types/test/components.test.d.ts +1 -0
- package/dist/types/test/composition.test.d.ts +1 -0
- package/dist/types/test/declared-needs.test.d.ts +1 -0
- package/dist/types/test/diagnostics.test.d.ts +1 -0
- package/dist/types/test/dynamic.test.d.ts +1 -0
- package/dist/types/test/example.test.d.ts +1 -0
- package/dist/types/test/faq.test.d.ts +1 -0
- package/dist/types/test/flatten-nested.test.d.ts +1 -0
- package/dist/types/test/html-binding.test.d.ts +1 -0
- package/dist/types/test/hydration-report.test.d.ts +1 -0
- package/dist/types/test/hydration.test.d.ts +1 -0
- package/dist/types/test/layout-resolution.test.d.ts +1 -0
- package/dist/types/test/layout.test.d.ts +1 -0
- package/dist/types/test/lazy.test.d.ts +1 -0
- package/dist/types/test/manifest.test.d.ts +1 -0
- package/dist/types/test/model-props.test.d.ts +1 -0
- package/dist/types/test/model.test.d.ts +1 -0
- package/dist/types/test/mounting.test.d.ts +1 -0
- package/dist/types/test/needs.test.d.ts +1 -0
- package/dist/types/test/nested-rows.test.d.ts +1 -0
- package/dist/types/test/on-mounted.test.d.ts +1 -0
- package/dist/types/test/on-update.test.d.ts +1 -0
- package/dist/types/test/page-island.test.d.ts +1 -0
- package/dist/types/test/pages.test.d.ts +1 -0
- package/dist/types/test/reactivity.test.d.ts +1 -0
- package/dist/types/test/rebuild-keeps-logic.test.d.ts +1 -0
- package/dist/types/test/rebuild.test.d.ts +1 -0
- package/dist/types/test/reenumerate.test.d.ts +1 -0
- package/dist/types/test/refresh.test.d.ts +1 -0
- package/dist/types/test/refs.test.d.ts +1 -0
- package/dist/types/test/reorder.test.d.ts +1 -0
- package/dist/types/test/route.test.d.ts +1 -0
- package/dist/types/test/routes-build.test.d.ts +1 -0
- package/dist/types/test/routes-dev.test.d.ts +1 -0
- package/dist/types/test/routes-emit.test.d.ts +1 -0
- package/dist/types/test/routes-runtime.test.d.ts +1 -0
- package/dist/types/test/setup.d.ts +59 -0
- package/dist/types/test/show.test.d.ts +1 -0
- package/dist/types/test/skeleton-adoption.test.d.ts +1 -0
- package/dist/types/test/slots.test.d.ts +1 -0
- package/dist/types/test/specifiers.test.d.ts +1 -0
- package/dist/types/test/ssg.test.d.ts +1 -0
- package/dist/types/test/state.test.d.ts +1 -0
- package/dist/types/test/static-islands.test.d.ts +1 -0
- package/dist/types/test/stats.test.d.ts +1 -0
- package/dist/types/test/structural.test.d.ts +1 -0
- package/dist/types/test/style-sources.test.d.ts +1 -0
- package/dist/types/test/styles.test.d.ts +1 -0
- package/dist/types/test/surgical-rebuild.test.d.ts +1 -0
- package/dist/types/test/tables.test.d.ts +1 -0
- package/dist/types/test/unmount.test.d.ts +1 -0
- package/dist/types/test/verbatim.test.d.ts +1 -0
- package/dist/vite.js +5655 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3060 @@
|
|
|
1
|
+
// src/runtime/diagnostics.ts
|
|
2
|
+
var PREFIX = "[lightspeed]";
|
|
3
|
+
function isDev() {
|
|
4
|
+
try {
|
|
5
|
+
return Boolean(import.meta.env?.DEV);
|
|
6
|
+
} catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function warn(message, ...detail) {
|
|
11
|
+
console.warn(`${PREFIX} ${message}`, ...detail);
|
|
12
|
+
}
|
|
13
|
+
function fail(message, ...detail) {
|
|
14
|
+
console.error(`${PREFIX} ${message}`, ...detail);
|
|
15
|
+
}
|
|
16
|
+
function debug(message, ...args) {
|
|
17
|
+
if (isDev())
|
|
18
|
+
console.log(`${PREFIX} ${message}`, ...args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/runtime/signals/SignalManager.ts
|
|
22
|
+
class SignalManager {
|
|
23
|
+
listeners = new Map;
|
|
24
|
+
history = new Map;
|
|
25
|
+
emit(event, data) {
|
|
26
|
+
if (!this.history.has(event)) {
|
|
27
|
+
this.history.set(event, []);
|
|
28
|
+
}
|
|
29
|
+
this.history.get(event).push(data);
|
|
30
|
+
const eventListeners = this.listeners.get(event) || [];
|
|
31
|
+
eventListeners.forEach((listener) => {
|
|
32
|
+
try {
|
|
33
|
+
listener(data);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
fail(`Error in signal listener for ${event}:`, error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
on(event, listener) {
|
|
40
|
+
if (!this.listeners.has(event)) {
|
|
41
|
+
this.listeners.set(event, []);
|
|
42
|
+
}
|
|
43
|
+
this.listeners.get(event).push(listener);
|
|
44
|
+
}
|
|
45
|
+
off(event, listener) {
|
|
46
|
+
const eventListeners = this.listeners.get(event);
|
|
47
|
+
if (eventListeners) {
|
|
48
|
+
const index = eventListeners.indexOf(listener);
|
|
49
|
+
if (index > -1) {
|
|
50
|
+
eventListeners.splice(index, 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
getHistory(event) {
|
|
55
|
+
return this.history.get(event) || [];
|
|
56
|
+
}
|
|
57
|
+
getLatest(event) {
|
|
58
|
+
const history = this.history.get(event);
|
|
59
|
+
if (!history || history.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
return history[history.length - 1];
|
|
63
|
+
}
|
|
64
|
+
clear() {
|
|
65
|
+
this.listeners.clear();
|
|
66
|
+
this.history.clear();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// src/runtime/signals/signals.ts
|
|
70
|
+
function createSignalAPI(signalManager) {
|
|
71
|
+
return {
|
|
72
|
+
emit: (event, data) => signalManager.emit(event, data),
|
|
73
|
+
on: (event, listener) => signalManager.on(event, listener),
|
|
74
|
+
off: (event, listener) => signalManager.off(event, listener),
|
|
75
|
+
getHistory: (event) => signalManager.getHistory(event),
|
|
76
|
+
getLatest: (event) => signalManager.getLatest(event)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createScopedSignalAPI(signalManager, registerCleanup) {
|
|
80
|
+
return {
|
|
81
|
+
emit: (event, data) => signalManager.emit(event, data),
|
|
82
|
+
on: (event, listener) => {
|
|
83
|
+
signalManager.on(event, listener);
|
|
84
|
+
registerCleanup(() => signalManager.off(event, listener));
|
|
85
|
+
},
|
|
86
|
+
off: (event, listener) => signalManager.off(event, listener),
|
|
87
|
+
getHistory: (event) => signalManager.getHistory(event),
|
|
88
|
+
getLatest: (event) => signalManager.getLatest(event)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// src/runtime/reactive/ReactiveProxy.ts
|
|
92
|
+
function isWrappable(value) {
|
|
93
|
+
if (value === null || typeof value !== "object")
|
|
94
|
+
return false;
|
|
95
|
+
if (Array.isArray(value))
|
|
96
|
+
return true;
|
|
97
|
+
const prototype = Object.getPrototypeOf(value);
|
|
98
|
+
return prototype === Object.prototype || prototype === null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ReactiveProxy {
|
|
102
|
+
onChange;
|
|
103
|
+
caches = new WeakMap;
|
|
104
|
+
ownerless = {};
|
|
105
|
+
rawTargets = new WeakMap;
|
|
106
|
+
observers = new WeakMap;
|
|
107
|
+
constructor(onChange) {
|
|
108
|
+
this.onChange = onChange;
|
|
109
|
+
}
|
|
110
|
+
cacheFor(owner) {
|
|
111
|
+
const key = owner ?? this.ownerless;
|
|
112
|
+
let cache = this.caches.get(key);
|
|
113
|
+
if (!cache) {
|
|
114
|
+
cache = new WeakMap;
|
|
115
|
+
this.caches.set(key, cache);
|
|
116
|
+
}
|
|
117
|
+
return cache;
|
|
118
|
+
}
|
|
119
|
+
create(target, owner = null) {
|
|
120
|
+
if (this.rawTargets.has(target))
|
|
121
|
+
return target;
|
|
122
|
+
if (owner)
|
|
123
|
+
this.observe(target, owner);
|
|
124
|
+
const cache = this.cacheFor(owner);
|
|
125
|
+
const existing = cache.get(target);
|
|
126
|
+
if (existing)
|
|
127
|
+
return existing;
|
|
128
|
+
const proxy = new Proxy(target, {
|
|
129
|
+
get: (obj, prop, receiver) => {
|
|
130
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
131
|
+
return isWrappable(value) ? this.create(value, owner) : value;
|
|
132
|
+
},
|
|
133
|
+
set: (obj, prop, value) => {
|
|
134
|
+
const next = this.rawTargets.get(value) ?? value;
|
|
135
|
+
const previous = obj[prop];
|
|
136
|
+
obj[prop] = next;
|
|
137
|
+
if (!Object.is(previous, next)) {
|
|
138
|
+
this.notify(obj, owner);
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
},
|
|
142
|
+
deleteProperty: (obj, prop) => {
|
|
143
|
+
const existed = prop in obj;
|
|
144
|
+
delete obj[prop];
|
|
145
|
+
if (existed) {
|
|
146
|
+
this.notify(obj, owner);
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
cache.set(target, proxy);
|
|
152
|
+
this.rawTargets.set(proxy, target);
|
|
153
|
+
return proxy;
|
|
154
|
+
}
|
|
155
|
+
share(value, observer) {
|
|
156
|
+
const target = this.rawTargets.get(value) ?? value;
|
|
157
|
+
if (!isWrappable(target))
|
|
158
|
+
return;
|
|
159
|
+
this.observe(target, observer);
|
|
160
|
+
}
|
|
161
|
+
observe(target, owner) {
|
|
162
|
+
let set = this.observers.get(target);
|
|
163
|
+
if (!set) {
|
|
164
|
+
set = new Set;
|
|
165
|
+
this.observers.set(target, set);
|
|
166
|
+
}
|
|
167
|
+
for (const ref of set) {
|
|
168
|
+
const held = ref.deref();
|
|
169
|
+
if (!held)
|
|
170
|
+
set.delete(ref);
|
|
171
|
+
else if (held === owner)
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
set.add(new WeakRef(owner));
|
|
175
|
+
}
|
|
176
|
+
notify(target, owner) {
|
|
177
|
+
this.onChange(owner);
|
|
178
|
+
const set = this.observers.get(target);
|
|
179
|
+
if (!set)
|
|
180
|
+
return;
|
|
181
|
+
for (const ref of set) {
|
|
182
|
+
const observer = ref.deref();
|
|
183
|
+
if (!observer) {
|
|
184
|
+
set.delete(ref);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (observer !== owner)
|
|
188
|
+
this.onChange(observer);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
raw(value) {
|
|
192
|
+
return this.rawTargets.get(value) ?? value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// src/runtime/reactive/UpdateScheduler.ts
|
|
196
|
+
class UpdateScheduler {
|
|
197
|
+
queue = new Set;
|
|
198
|
+
scheduled = false;
|
|
199
|
+
flushCallback;
|
|
200
|
+
constructor(flushCallback) {
|
|
201
|
+
this.flushCallback = flushCallback;
|
|
202
|
+
}
|
|
203
|
+
queueUpdate(owner) {
|
|
204
|
+
this.queue.add(owner);
|
|
205
|
+
if (!this.scheduled) {
|
|
206
|
+
this.scheduled = true;
|
|
207
|
+
requestAnimationFrame(() => this.flushUpdates());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
flushUpdates() {
|
|
211
|
+
this.scheduled = false;
|
|
212
|
+
const batch = [...this.queue];
|
|
213
|
+
this.queue.clear();
|
|
214
|
+
this.flushCallback(batch);
|
|
215
|
+
}
|
|
216
|
+
clear() {
|
|
217
|
+
this.queue.clear();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// src/runtime/components/ComponentRegistry.ts
|
|
221
|
+
class ComponentRegistry {
|
|
222
|
+
components = new Map;
|
|
223
|
+
register(component) {
|
|
224
|
+
this.components.set(component.id, component);
|
|
225
|
+
if (import.meta.env?.DEV) {
|
|
226
|
+
debug(`registered ${component.id}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
update(id, component) {
|
|
230
|
+
this.components.set(id, component);
|
|
231
|
+
if (import.meta.env?.DEV) {
|
|
232
|
+
debug(`hot updated ${id}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
get(id) {
|
|
236
|
+
return this.components.get(id);
|
|
237
|
+
}
|
|
238
|
+
has(id) {
|
|
239
|
+
return this.components.has(id);
|
|
240
|
+
}
|
|
241
|
+
getAllIds() {
|
|
242
|
+
return Array.from(this.components.keys());
|
|
243
|
+
}
|
|
244
|
+
getAll() {
|
|
245
|
+
return Array.from(this.components.values());
|
|
246
|
+
}
|
|
247
|
+
clear() {
|
|
248
|
+
this.components.clear();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// src/runtime/components/ComponentInstance.ts
|
|
252
|
+
class ComponentInstance {
|
|
253
|
+
id;
|
|
254
|
+
componentId;
|
|
255
|
+
parent;
|
|
256
|
+
children = [];
|
|
257
|
+
rootNodes = [];
|
|
258
|
+
refs = {};
|
|
259
|
+
mountedCallbacks = [];
|
|
260
|
+
logicCleanup = [];
|
|
261
|
+
bindings = [];
|
|
262
|
+
blocks = [];
|
|
263
|
+
scopeId;
|
|
264
|
+
props = {};
|
|
265
|
+
state = {};
|
|
266
|
+
handlers = new Map;
|
|
267
|
+
updateCallbacks = [];
|
|
268
|
+
handlerNames = [];
|
|
269
|
+
signals;
|
|
270
|
+
cleanup = [];
|
|
271
|
+
listeners = 0;
|
|
272
|
+
models = 0;
|
|
273
|
+
wantsMount = false;
|
|
274
|
+
runtimeCells = new Set;
|
|
275
|
+
constructor(id, componentId, signals, parent = null) {
|
|
276
|
+
this.id = id;
|
|
277
|
+
this.componentId = componentId;
|
|
278
|
+
this.signals = signals;
|
|
279
|
+
this.parent = parent;
|
|
280
|
+
}
|
|
281
|
+
addHandler(name, fn) {
|
|
282
|
+
this.handlers.set(name, fn);
|
|
283
|
+
}
|
|
284
|
+
getHandler(name) {
|
|
285
|
+
return this.handlers.get(name);
|
|
286
|
+
}
|
|
287
|
+
hasHandler(name) {
|
|
288
|
+
return this.handlers.has(name);
|
|
289
|
+
}
|
|
290
|
+
addCleanup(cleanup) {
|
|
291
|
+
this.cleanup.push(cleanup);
|
|
292
|
+
}
|
|
293
|
+
addLogicCleanup(cleanup) {
|
|
294
|
+
this.logicCleanup.push(cleanup);
|
|
295
|
+
}
|
|
296
|
+
releaseDom() {
|
|
297
|
+
this.blocks.forEach((block) => block.destroy());
|
|
298
|
+
this.blocks.length = 0;
|
|
299
|
+
this.cleanup.forEach((cleanup) => cleanup());
|
|
300
|
+
this.cleanup.length = 0;
|
|
301
|
+
this.bindings.length = 0;
|
|
302
|
+
this.rootNodes.length = 0;
|
|
303
|
+
for (const name of Object.keys(this.refs))
|
|
304
|
+
delete this.refs[name];
|
|
305
|
+
}
|
|
306
|
+
addBinding(binding) {
|
|
307
|
+
this.bindings.push(binding);
|
|
308
|
+
}
|
|
309
|
+
addBlock(block) {
|
|
310
|
+
this.blocks.push(block);
|
|
311
|
+
}
|
|
312
|
+
destroy() {
|
|
313
|
+
this.children.forEach((child) => child.destroy());
|
|
314
|
+
this.children.length = 0;
|
|
315
|
+
this.blocks.forEach((block) => block.destroy());
|
|
316
|
+
this.blocks.length = 0;
|
|
317
|
+
this.cleanup.forEach((cleanup) => cleanup());
|
|
318
|
+
this.cleanup.length = 0;
|
|
319
|
+
this.logicCleanup.forEach((cleanup) => cleanup());
|
|
320
|
+
this.logicCleanup.length = 0;
|
|
321
|
+
this.handlers.clear();
|
|
322
|
+
this.bindings.length = 0;
|
|
323
|
+
this.rootNodes.length = 0;
|
|
324
|
+
for (const name of Object.keys(this.refs))
|
|
325
|
+
delete this.refs[name];
|
|
326
|
+
this.mountedCallbacks.length = 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// src/runtime/bindings/BindingEvaluator.ts
|
|
330
|
+
class BindingScope {
|
|
331
|
+
instance;
|
|
332
|
+
locals;
|
|
333
|
+
cleanup;
|
|
334
|
+
constructor(instance, locals, cleanup) {
|
|
335
|
+
this.instance = instance;
|
|
336
|
+
this.locals = locals;
|
|
337
|
+
this.cleanup = cleanup;
|
|
338
|
+
}
|
|
339
|
+
get id() {
|
|
340
|
+
return this.instance.id;
|
|
341
|
+
}
|
|
342
|
+
get state() {
|
|
343
|
+
return this.instance.state;
|
|
344
|
+
}
|
|
345
|
+
get props() {
|
|
346
|
+
return this.instance.props;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
var BOOLEAN_ATTRIBUTES = new Set([
|
|
350
|
+
"allowfullscreen",
|
|
351
|
+
"async",
|
|
352
|
+
"autofocus",
|
|
353
|
+
"autoplay",
|
|
354
|
+
"checked",
|
|
355
|
+
"controls",
|
|
356
|
+
"default",
|
|
357
|
+
"defer",
|
|
358
|
+
"disabled",
|
|
359
|
+
"formnovalidate",
|
|
360
|
+
"hidden",
|
|
361
|
+
"inert",
|
|
362
|
+
"ismap",
|
|
363
|
+
"itemscope",
|
|
364
|
+
"loop",
|
|
365
|
+
"multiple",
|
|
366
|
+
"muted",
|
|
367
|
+
"novalidate",
|
|
368
|
+
"open",
|
|
369
|
+
"playsinline",
|
|
370
|
+
"readonly",
|
|
371
|
+
"required",
|
|
372
|
+
"reversed",
|
|
373
|
+
"selected"
|
|
374
|
+
]);
|
|
375
|
+
var NO_LOCALS = [];
|
|
376
|
+
var SKIP = Symbol("skip");
|
|
377
|
+
var IDENTITY = (raw) => raw;
|
|
378
|
+
var TO_NUMBER = (raw) => raw === "" || raw == null ? undefined : Number(raw);
|
|
379
|
+
function resolveModelTarget(el) {
|
|
380
|
+
const tag = el.tagName;
|
|
381
|
+
if (tag === "SELECT")
|
|
382
|
+
return { prop: "value", event: "change", coerce: IDENTITY };
|
|
383
|
+
if (tag === "TEXTAREA")
|
|
384
|
+
return { prop: "value", event: "input", coerce: IDENTITY };
|
|
385
|
+
if (tag === "INPUT") {
|
|
386
|
+
const type = el.type;
|
|
387
|
+
if (type === "checkbox")
|
|
388
|
+
return { prop: "checked", event: "change", coerce: Boolean };
|
|
389
|
+
if (type === "radio")
|
|
390
|
+
return { prop: "value", event: "change", coerce: IDENTITY, radio: true };
|
|
391
|
+
if (type === "number" || type === "range")
|
|
392
|
+
return { prop: "value", event: "input", coerce: TO_NUMBER };
|
|
393
|
+
}
|
|
394
|
+
return { prop: "value", event: "input", coerce: IDENTITY };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
class BindingEvaluator {
|
|
398
|
+
cells;
|
|
399
|
+
onPropChanged;
|
|
400
|
+
compiledCache = new Map;
|
|
401
|
+
writerCache = new Map;
|
|
402
|
+
constructor(cells, onPropChanged = () => {
|
|
403
|
+
}) {
|
|
404
|
+
this.cells = cells;
|
|
405
|
+
this.onPropChanged = onPropChanged;
|
|
406
|
+
}
|
|
407
|
+
expressions = new Map;
|
|
408
|
+
writers = new Map;
|
|
409
|
+
registerExpressions(expressions = {}, writers = {}) {
|
|
410
|
+
for (const [key, fn] of Object.entries(expressions)) {
|
|
411
|
+
this.expressions.set(key, fn);
|
|
412
|
+
}
|
|
413
|
+
for (const [key, fn] of Object.entries(writers)) {
|
|
414
|
+
this.writers.set(key, fn);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
compile(expr, params) {
|
|
418
|
+
const compiled = this.expressions.get(`${params.join(",")}|${expr}`);
|
|
419
|
+
if (compiled)
|
|
420
|
+
return compiled;
|
|
421
|
+
fail(`No compiled expression for ${JSON.stringify(expr)}` + (params.length ? ` in scope [${params.join(", ")}]` : ""));
|
|
422
|
+
return () => {
|
|
423
|
+
return;
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
compileWriter(expr, params) {
|
|
427
|
+
const writer = this.writers.get(`${params.join(",")}|${expr}`);
|
|
428
|
+
if (writer)
|
|
429
|
+
return writer;
|
|
430
|
+
warn(`$model needs an assignable expression, got "${expr}" - it will render but not write back.`);
|
|
431
|
+
return () => {
|
|
432
|
+
return;
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
createBinding(el, kind, expr, attr, params = []) {
|
|
436
|
+
return { el, kind, attr, expr, compiled: this.compile(expr, params) };
|
|
437
|
+
}
|
|
438
|
+
createPropBinding(target, name, expr, params = []) {
|
|
439
|
+
return {
|
|
440
|
+
kind: "prop",
|
|
441
|
+
target,
|
|
442
|
+
name,
|
|
443
|
+
expr,
|
|
444
|
+
compiled: this.compile(expr, params)
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
bindCellProp(target, name, key) {
|
|
448
|
+
const cell = this.cells.get(key);
|
|
449
|
+
if (!cell.pending)
|
|
450
|
+
target.props[name] = cell.value;
|
|
451
|
+
target.runtimeCells.add(key);
|
|
452
|
+
const unsubscribe = cell.subscribe((value) => {
|
|
453
|
+
if (Object.is(target.props[name], value))
|
|
454
|
+
return;
|
|
455
|
+
target.props[name] = value;
|
|
456
|
+
this.onPropChanged(target);
|
|
457
|
+
});
|
|
458
|
+
target.addLogicCleanup(unsubscribe);
|
|
459
|
+
}
|
|
460
|
+
createModelBinding(el, expr, params = []) {
|
|
461
|
+
return {
|
|
462
|
+
kind: "model",
|
|
463
|
+
el,
|
|
464
|
+
expr,
|
|
465
|
+
target: resolveModelTarget(el),
|
|
466
|
+
compiled: this.compile(expr, params),
|
|
467
|
+
write: this.compileWriter(expr, params)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
createForBinding(block, expr, params = []) {
|
|
471
|
+
return { kind: "for", block, expr, compiled: this.compile(expr, params) };
|
|
472
|
+
}
|
|
473
|
+
readProxy() {
|
|
474
|
+
return this.cells.createReadProxy();
|
|
475
|
+
}
|
|
476
|
+
pendingProbe(onRead) {
|
|
477
|
+
return (key) => {
|
|
478
|
+
onRead?.(key);
|
|
479
|
+
return this.cells.get(key).pending;
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
get cellManager() {
|
|
483
|
+
return this.cells;
|
|
484
|
+
}
|
|
485
|
+
compileExpression(expr, params = []) {
|
|
486
|
+
return this.compile(expr, params);
|
|
487
|
+
}
|
|
488
|
+
evaluate(expr, instance, locals = []) {
|
|
489
|
+
try {
|
|
490
|
+
return this.compile(expr, [])(instance.state, instance.props, this.cells.createReadProxy(), this.pendingProbe(), ...locals);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
warn(`Error evaluating "${expr}" in ${instance.id}:`, error);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
applyBinding(binding, scope) {
|
|
497
|
+
this.apply(binding, scope);
|
|
498
|
+
}
|
|
499
|
+
updateInstance(instance) {
|
|
500
|
+
const scope = new BindingScope(instance, NO_LOCALS, instance);
|
|
501
|
+
for (const binding of instance.bindings) {
|
|
502
|
+
this.apply(binding, scope);
|
|
503
|
+
}
|
|
504
|
+
for (const block of instance.blocks) {
|
|
505
|
+
this.updateBlock(block);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
updateRow(instance, row) {
|
|
509
|
+
const scope = new BindingScope(instance, row.locals, row);
|
|
510
|
+
for (const binding of row.bindings) {
|
|
511
|
+
this.apply(binding, scope);
|
|
512
|
+
}
|
|
513
|
+
for (const block of row.blocks) {
|
|
514
|
+
this.updateBlock(block);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
updateBlock(block) {
|
|
518
|
+
block.rows.forEach((row) => {
|
|
519
|
+
for (let i = 0;i < block.parentLocals.length; i++) {
|
|
520
|
+
row.locals[i] = block.parentLocals[i];
|
|
521
|
+
}
|
|
522
|
+
this.updateRow(block.owner, row);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
updateTree(instance) {
|
|
526
|
+
this.updateInstance(instance);
|
|
527
|
+
for (const child of instance.children) {
|
|
528
|
+
this.updateTree(child);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
attachModel(binding, scope) {
|
|
532
|
+
const listener = () => {
|
|
533
|
+
const next = this.readFromDom(binding);
|
|
534
|
+
if (next === SKIP)
|
|
535
|
+
return;
|
|
536
|
+
try {
|
|
537
|
+
binding.write(scope.state, scope.props, this.cells.createReadProxy(), this.pendingProbe(), next, ...scope.locals);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
warn(`Failed writing $model="${binding.expr}":`, error);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
binding.el.addEventListener(binding.target.event, listener);
|
|
543
|
+
scope.cleanup.addCleanup(() => binding.el.removeEventListener(binding.target.event, listener));
|
|
544
|
+
}
|
|
545
|
+
readFromDom(binding) {
|
|
546
|
+
const el = binding.el;
|
|
547
|
+
if (binding.target.radio) {
|
|
548
|
+
return el.checked ? el.value : SKIP;
|
|
549
|
+
}
|
|
550
|
+
return binding.target.coerce(el[binding.target.prop]);
|
|
551
|
+
}
|
|
552
|
+
apply(binding, scope) {
|
|
553
|
+
const reads = new Set;
|
|
554
|
+
const shared = this.cells.createReadProxy((key) => reads.add(key));
|
|
555
|
+
const pending = this.pendingProbe((key) => reads.add(key));
|
|
556
|
+
let value;
|
|
557
|
+
try {
|
|
558
|
+
value = binding.compiled(scope.state, scope.props, shared, pending, ...scope.locals);
|
|
559
|
+
this.syncCellSubscriptions(binding, scope, reads);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
warn(`Error evaluating "${binding.expr}" in ${scope.id}:`, error);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (binding.kind === "for") {
|
|
565
|
+
binding.block.schedule(binding.block.toItems(value));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (binding.kind === "model") {
|
|
569
|
+
this.applyModel(binding, value);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (binding.kind === "prop") {
|
|
573
|
+
const target = binding.target;
|
|
574
|
+
if (Object.is(target.props[binding.name], value))
|
|
575
|
+
return;
|
|
576
|
+
target.props[binding.name] = value;
|
|
577
|
+
this.onPropChanged(target);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
switch (binding.kind) {
|
|
581
|
+
case "text":
|
|
582
|
+
binding.el.textContent = value == null ? "" : String(value);
|
|
583
|
+
break;
|
|
584
|
+
case "class":
|
|
585
|
+
this.updateClasses(binding.el, value);
|
|
586
|
+
break;
|
|
587
|
+
case "show":
|
|
588
|
+
setHidden(binding.el, !value);
|
|
589
|
+
break;
|
|
590
|
+
case "html":
|
|
591
|
+
binding.el.innerHTML = value == null ? "" : String(value);
|
|
592
|
+
break;
|
|
593
|
+
case "attr": {
|
|
594
|
+
const attr = binding.attr;
|
|
595
|
+
const aria = attr.startsWith("aria-");
|
|
596
|
+
if (BOOLEAN_ATTRIBUTES.has(attr) && !aria) {
|
|
597
|
+
if (value)
|
|
598
|
+
binding.el.setAttribute(attr, "");
|
|
599
|
+
else
|
|
600
|
+
binding.el.removeAttribute(attr);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
if (value == null || value === false && !aria) {
|
|
604
|
+
binding.el.removeAttribute(attr);
|
|
605
|
+
} else if (aria) {
|
|
606
|
+
binding.el.setAttribute(attr, String(value));
|
|
607
|
+
} else {
|
|
608
|
+
binding.el.setAttribute(attr, value === true ? "" : String(value));
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
applyModel(binding, value) {
|
|
615
|
+
const el = binding.el;
|
|
616
|
+
const { prop, radio, coerce } = binding.target;
|
|
617
|
+
if (radio) {
|
|
618
|
+
const shouldCheck = String(value) === el.value;
|
|
619
|
+
if (el.checked !== shouldCheck)
|
|
620
|
+
el.checked = shouldCheck;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (Object.is(coerce(el[prop]), value))
|
|
624
|
+
return;
|
|
625
|
+
if (prop === "checked") {
|
|
626
|
+
el.checked = Boolean(value);
|
|
627
|
+
} else {
|
|
628
|
+
el.value = value == null ? "" : String(value);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
syncCellSubscriptions(binding, scope, reads) {
|
|
632
|
+
const previous = binding.cellKeys;
|
|
633
|
+
if (previous && sameKeys(previous, reads))
|
|
634
|
+
return;
|
|
635
|
+
binding.cellUnsubscribes?.forEach((unsubscribe) => unsubscribe());
|
|
636
|
+
binding.cellKeys = reads;
|
|
637
|
+
binding.cellUnsubscribes = [...reads].map((key) => this.cells.get(key).subscribe(() => this.apply(binding, scope)));
|
|
638
|
+
if (!binding.cleanupRegistered) {
|
|
639
|
+
binding.cleanupRegistered = true;
|
|
640
|
+
scope.cleanup.addCleanup(() => {
|
|
641
|
+
binding.cellUnsubscribes?.forEach((unsubscribe) => unsubscribe());
|
|
642
|
+
binding.cellUnsubscribes = [];
|
|
643
|
+
binding.cellKeys = undefined;
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
appliedClasses = new WeakMap;
|
|
648
|
+
updateClasses(element, classObj) {
|
|
649
|
+
if (typeof classObj === "string") {
|
|
650
|
+
const previous = this.appliedClasses.get(element) ?? [];
|
|
651
|
+
const next = classObj.split(/\s+/).filter(Boolean);
|
|
652
|
+
for (const name of previous) {
|
|
653
|
+
if (!next.includes(name))
|
|
654
|
+
element.classList.remove(name);
|
|
655
|
+
}
|
|
656
|
+
for (const name of next)
|
|
657
|
+
element.classList.add(name);
|
|
658
|
+
this.appliedClasses.set(element, next);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (typeof classObj === "object" && classObj !== null) {
|
|
662
|
+
Object.entries(classObj).forEach(([className, shouldAdd]) => {
|
|
663
|
+
if (shouldAdd) {
|
|
664
|
+
element.classList.add(className);
|
|
665
|
+
} else {
|
|
666
|
+
element.classList.remove(className);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function sameKeys(a, b) {
|
|
673
|
+
if (a.size !== b.size)
|
|
674
|
+
return false;
|
|
675
|
+
for (const key of a)
|
|
676
|
+
if (!b.has(key))
|
|
677
|
+
return false;
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
var HIDDEN_ATTR = "data-lsp-hidden";
|
|
681
|
+
function setHidden(el, hidden) {
|
|
682
|
+
if (hidden)
|
|
683
|
+
el.setAttribute(HIDDEN_ATTR, "");
|
|
684
|
+
else
|
|
685
|
+
el.removeAttribute(HIDDEN_ATTR);
|
|
686
|
+
}
|
|
687
|
+
// src/runtime/events/EventBinder.ts
|
|
688
|
+
class EventBinder {
|
|
689
|
+
bind(el, eventType, handlerName, owner, cleanupTarget = owner) {
|
|
690
|
+
this.listen(el, eventType, (event) => {
|
|
691
|
+
const handler = owner.getHandler(handlerName);
|
|
692
|
+
if (!handler) {
|
|
693
|
+
warn(`Handler not found: ${handlerName} (on ${owner.componentId})`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
handler(event);
|
|
697
|
+
}, cleanupTarget, `${eventType} handler ${handlerName}`);
|
|
698
|
+
}
|
|
699
|
+
bindExpression(el, eventType, invoke, cleanupTarget, label) {
|
|
700
|
+
this.listen(el, eventType, invoke, cleanupTarget, label);
|
|
701
|
+
}
|
|
702
|
+
listen(el, eventType, run, cleanupTarget, label) {
|
|
703
|
+
const listener = (event) => {
|
|
704
|
+
try {
|
|
705
|
+
run(event);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
fail(`Error in ${label}:`, error);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
el.addEventListener(eventType, listener);
|
|
711
|
+
cleanupTarget.addCleanup(() => el.removeEventListener(eventType, listener));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// src/runtime/globals/GlobalFunctions.ts
|
|
715
|
+
class GlobalFunctions {
|
|
716
|
+
reactiveProxy;
|
|
717
|
+
globalSignals;
|
|
718
|
+
constructor(reactiveProxy, globalSignals) {
|
|
719
|
+
this.reactiveProxy = reactiveProxy;
|
|
720
|
+
this.globalSignals = globalSignals;
|
|
721
|
+
}
|
|
722
|
+
setup() {
|
|
723
|
+
window.defineProps = this.defineProps.bind(this);
|
|
724
|
+
window.reactive = this.reactive.bind(this);
|
|
725
|
+
window.defineSignal = this.defineSignal.bind(this);
|
|
726
|
+
window.onMounted = this.onMounted.bind(this);
|
|
727
|
+
}
|
|
728
|
+
defineProps(propsDefinition) {
|
|
729
|
+
const props = {};
|
|
730
|
+
if (propsDefinition && typeof propsDefinition === "object") {
|
|
731
|
+
Object.entries(propsDefinition).forEach(([key, definition]) => {
|
|
732
|
+
if (definition && typeof definition === "object" && "default" in definition) {
|
|
733
|
+
props[key] = definition.default;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
return props;
|
|
738
|
+
}
|
|
739
|
+
reactive(target) {
|
|
740
|
+
return this.reactiveProxy.create(target);
|
|
741
|
+
}
|
|
742
|
+
defineSignal() {
|
|
743
|
+
return createSignalAPI(this.globalSignals);
|
|
744
|
+
}
|
|
745
|
+
onMounted(callback) {
|
|
746
|
+
setTimeout(callback, 0);
|
|
747
|
+
}
|
|
748
|
+
cleanup() {
|
|
749
|
+
delete window.defineProps;
|
|
750
|
+
delete window.reactive;
|
|
751
|
+
delete window.defineSignal;
|
|
752
|
+
delete window.onMounted;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// src/runtime/state/Cell.ts
|
|
756
|
+
class Cell {
|
|
757
|
+
key;
|
|
758
|
+
currentValue;
|
|
759
|
+
isPending = true;
|
|
760
|
+
listeners = new Set;
|
|
761
|
+
persist;
|
|
762
|
+
constructor(key, options = {}) {
|
|
763
|
+
this.key = key;
|
|
764
|
+
this.persist = options.persist ?? false;
|
|
765
|
+
if (this.persist) {
|
|
766
|
+
const stored = readStored(key);
|
|
767
|
+
if (stored !== undefined) {
|
|
768
|
+
this.currentValue = stored;
|
|
769
|
+
this.isPending = false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
get value() {
|
|
774
|
+
return this.currentValue;
|
|
775
|
+
}
|
|
776
|
+
set value(next) {
|
|
777
|
+
this.set(next);
|
|
778
|
+
}
|
|
779
|
+
get pending() {
|
|
780
|
+
return this.isPending;
|
|
781
|
+
}
|
|
782
|
+
set(next) {
|
|
783
|
+
if (!this.isPending && Object.is(next, this.currentValue))
|
|
784
|
+
return;
|
|
785
|
+
this.currentValue = next;
|
|
786
|
+
this.isPending = false;
|
|
787
|
+
if (this.persist)
|
|
788
|
+
writeStored(this.key, next);
|
|
789
|
+
this.listeners.forEach((listener) => {
|
|
790
|
+
try {
|
|
791
|
+
listener(next);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
fail(`Error in cell listener for ${this.key}:`, error);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
enablePersistence() {
|
|
798
|
+
if (this.persist)
|
|
799
|
+
return;
|
|
800
|
+
this.persist = true;
|
|
801
|
+
const stored = readStored(this.key);
|
|
802
|
+
if (this.isPending) {
|
|
803
|
+
if (stored !== undefined)
|
|
804
|
+
this.set(stored);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
writeStored(this.key, this.currentValue);
|
|
808
|
+
}
|
|
809
|
+
subscribe(listener) {
|
|
810
|
+
this.listeners.add(listener);
|
|
811
|
+
return () => this.listeners.delete(listener);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
var STORAGE_PREFIX = "lsp:";
|
|
815
|
+
function readStored(key) {
|
|
816
|
+
try {
|
|
817
|
+
const raw = globalThis.localStorage?.getItem(STORAGE_PREFIX + key);
|
|
818
|
+
return raw == null ? undefined : JSON.parse(raw);
|
|
819
|
+
} catch {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function writeStored(key, value) {
|
|
824
|
+
try {
|
|
825
|
+
if (value === undefined) {
|
|
826
|
+
globalThis.localStorage?.removeItem(STORAGE_PREFIX + key);
|
|
827
|
+
} else {
|
|
828
|
+
globalThis.localStorage?.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// src/runtime/state/CellManager.ts
|
|
834
|
+
class CellManager {
|
|
835
|
+
ssg;
|
|
836
|
+
cells = new Map;
|
|
837
|
+
initialised = new Set;
|
|
838
|
+
declaredDefaults = new Map;
|
|
839
|
+
refreshKeys = new Set;
|
|
840
|
+
refreshed = new Set;
|
|
841
|
+
buildKeys = new Set;
|
|
842
|
+
buildWork = [];
|
|
843
|
+
constructor(ssg = false) {
|
|
844
|
+
this.ssg = ssg;
|
|
845
|
+
}
|
|
846
|
+
get(key, init, options = {}) {
|
|
847
|
+
let cell = this.cells.get(key);
|
|
848
|
+
if (!cell) {
|
|
849
|
+
cell = new Cell(key, options);
|
|
850
|
+
this.cells.set(key, cell);
|
|
851
|
+
} else if (options.persist) {
|
|
852
|
+
cell.enablePersistence();
|
|
853
|
+
}
|
|
854
|
+
if (init !== undefined) {
|
|
855
|
+
if (typeof init === "function") {
|
|
856
|
+
this.runInitialiser(key, cell, init, options);
|
|
857
|
+
} else {
|
|
858
|
+
this.applyDefault(key, cell, init);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return cell;
|
|
862
|
+
}
|
|
863
|
+
applyDefault(key, cell, value) {
|
|
864
|
+
if (import.meta.env?.DEV) {
|
|
865
|
+
if (this.declaredDefaults.has(key) && !Object.is(this.declaredDefaults.get(key), value)) {
|
|
866
|
+
warn(`Conflicting defaults for cell "${key}": ${JSON.stringify(this.declaredDefaults.get(key))} vs ${JSON.stringify(value)}. First one wins, and which is first can vary.`);
|
|
867
|
+
}
|
|
868
|
+
this.declaredDefaults.set(key, value);
|
|
869
|
+
}
|
|
870
|
+
if (cell.pending)
|
|
871
|
+
cell.set(value);
|
|
872
|
+
}
|
|
873
|
+
runInitialiser(key, cell, initialiser, options = {}) {
|
|
874
|
+
const atBuild = options.fetchAt === "build";
|
|
875
|
+
if (this.ssg && !atBuild)
|
|
876
|
+
return;
|
|
877
|
+
if (atBuild)
|
|
878
|
+
this.buildKeys.add(key);
|
|
879
|
+
if (atBuild && options.refresh)
|
|
880
|
+
this.refreshKeys.add(key);
|
|
881
|
+
if (this.initialised.has(key)) {
|
|
882
|
+
if (!this.ssg && options.refresh && !this.refreshed.has(key)) {
|
|
883
|
+
this.refreshed.add(key);
|
|
884
|
+
this.fetch(key, cell, initialiser, false);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
this.initialised.add(key);
|
|
889
|
+
this.fetch(key, cell, initialiser, this.ssg);
|
|
890
|
+
}
|
|
891
|
+
fetch(key, cell, initialiser, tracked) {
|
|
892
|
+
try {
|
|
893
|
+
const result = initialiser();
|
|
894
|
+
if (result instanceof Promise) {
|
|
895
|
+
const settled = result.then((value) => cell.set(value));
|
|
896
|
+
if (tracked) {
|
|
897
|
+
settled.catch(() => {
|
|
898
|
+
});
|
|
899
|
+
this.buildWork.push(settled);
|
|
900
|
+
} else {
|
|
901
|
+
settled.catch((error) => fail(`Cell initialiser failed for "${key}":`, error));
|
|
902
|
+
}
|
|
903
|
+
} else {
|
|
904
|
+
cell.set(result);
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
fail(`Cell initialiser failed for "${key}":`, error);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
whenReady(keys, onReady) {
|
|
911
|
+
const outstanding = keys.filter((key) => this.get(key).pending);
|
|
912
|
+
if (!outstanding.length) {
|
|
913
|
+
onReady();
|
|
914
|
+
return () => {
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const unsubscribes = [];
|
|
918
|
+
const cancel = () => {
|
|
919
|
+
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
920
|
+
unsubscribes.length = 0;
|
|
921
|
+
};
|
|
922
|
+
const check = () => {
|
|
923
|
+
if (outstanding.some((key) => this.get(key).pending))
|
|
924
|
+
return;
|
|
925
|
+
cancel();
|
|
926
|
+
onReady();
|
|
927
|
+
};
|
|
928
|
+
outstanding.forEach((key) => unsubscribes.push(this.get(key).subscribe(check)));
|
|
929
|
+
return cancel;
|
|
930
|
+
}
|
|
931
|
+
pendingKeys(keys) {
|
|
932
|
+
return keys.filter((key) => this.get(key).pending);
|
|
933
|
+
}
|
|
934
|
+
async settleBuildData() {
|
|
935
|
+
while (this.buildWork.length) {
|
|
936
|
+
const pending = this.buildWork;
|
|
937
|
+
this.buildWork = [];
|
|
938
|
+
await Promise.all(pending);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
isBuildKey(key) {
|
|
942
|
+
return this.buildKeys.has(key);
|
|
943
|
+
}
|
|
944
|
+
isBakedOnly(key) {
|
|
945
|
+
return this.buildKeys.has(key) && !this.refreshKeys.has(key);
|
|
946
|
+
}
|
|
947
|
+
hasBuildData() {
|
|
948
|
+
return this.buildKeys.size > 0;
|
|
949
|
+
}
|
|
950
|
+
buildPayload() {
|
|
951
|
+
const payload = {};
|
|
952
|
+
for (const key of this.buildKeys) {
|
|
953
|
+
const cell = this.cells.get(key);
|
|
954
|
+
if (cell && !cell.pending)
|
|
955
|
+
payload[key] = cell.value;
|
|
956
|
+
}
|
|
957
|
+
return payload;
|
|
958
|
+
}
|
|
959
|
+
seed(values) {
|
|
960
|
+
for (const [key, value] of Object.entries(values)) {
|
|
961
|
+
this.initialised.add(key);
|
|
962
|
+
this.get(key).set(value);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
has(key) {
|
|
966
|
+
return this.cells.has(key);
|
|
967
|
+
}
|
|
968
|
+
keys() {
|
|
969
|
+
return [...this.cells.keys()];
|
|
970
|
+
}
|
|
971
|
+
createReadProxy(onRead) {
|
|
972
|
+
return new Proxy({}, {
|
|
973
|
+
get: (_target, prop) => {
|
|
974
|
+
if (typeof prop !== "string")
|
|
975
|
+
return;
|
|
976
|
+
const key = prop;
|
|
977
|
+
onRead?.(key);
|
|
978
|
+
return this.get(key).value;
|
|
979
|
+
},
|
|
980
|
+
set: (_target, prop, value) => {
|
|
981
|
+
if (typeof prop !== "string")
|
|
982
|
+
return false;
|
|
983
|
+
this.get(prop).set(value);
|
|
984
|
+
return true;
|
|
985
|
+
},
|
|
986
|
+
has: () => true
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
clear() {
|
|
990
|
+
this.cells.clear();
|
|
991
|
+
this.initialised.clear();
|
|
992
|
+
this.declaredDefaults.clear();
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// src/runtime/dom/ForBlock.ts
|
|
996
|
+
class Row {
|
|
997
|
+
locals;
|
|
998
|
+
key = "";
|
|
999
|
+
nodes = [];
|
|
1000
|
+
bindings = [];
|
|
1001
|
+
blocks = [];
|
|
1002
|
+
cleanup = [];
|
|
1003
|
+
instances = [];
|
|
1004
|
+
constructor(locals) {
|
|
1005
|
+
this.locals = locals;
|
|
1006
|
+
}
|
|
1007
|
+
addBinding(binding) {
|
|
1008
|
+
this.bindings.push(binding);
|
|
1009
|
+
}
|
|
1010
|
+
addCleanup(cleanup) {
|
|
1011
|
+
this.cleanup.push(cleanup);
|
|
1012
|
+
}
|
|
1013
|
+
addBlock(block) {
|
|
1014
|
+
this.blocks.push(block);
|
|
1015
|
+
}
|
|
1016
|
+
get item() {
|
|
1017
|
+
return this.locals[this.locals.length - 1];
|
|
1018
|
+
}
|
|
1019
|
+
set item(value) {
|
|
1020
|
+
this.locals[this.locals.length - 1] = value;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function toIterable(value) {
|
|
1024
|
+
if (value == null)
|
|
1025
|
+
return [];
|
|
1026
|
+
if (Array.isArray(value))
|
|
1027
|
+
return value;
|
|
1028
|
+
if (typeof value === "number") {
|
|
1029
|
+
return Array.from({ length: Math.max(0, Math.floor(value)) }, (_, i) => i);
|
|
1030
|
+
}
|
|
1031
|
+
if (typeof value[Symbol.iterator] === "function")
|
|
1032
|
+
return [...value];
|
|
1033
|
+
if (typeof value === "object")
|
|
1034
|
+
return Object.values(value);
|
|
1035
|
+
return [];
|
|
1036
|
+
}
|
|
1037
|
+
var nextObjectId = 1;
|
|
1038
|
+
var objectIds = new WeakMap;
|
|
1039
|
+
function identityOf(value) {
|
|
1040
|
+
if (typeof value === "symbol")
|
|
1041
|
+
return value.toString();
|
|
1042
|
+
if (value !== null && typeof value === "object") {
|
|
1043
|
+
let id = objectIds.get(value);
|
|
1044
|
+
if (id === undefined) {
|
|
1045
|
+
id = nextObjectId++;
|
|
1046
|
+
objectIds.set(value, id);
|
|
1047
|
+
}
|
|
1048
|
+
return `o${id}`;
|
|
1049
|
+
}
|
|
1050
|
+
return `${typeof value}:${String(value)}`;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
class ForBlock {
|
|
1054
|
+
owner;
|
|
1055
|
+
anchor;
|
|
1056
|
+
templates;
|
|
1057
|
+
alias;
|
|
1058
|
+
params;
|
|
1059
|
+
parentLocals;
|
|
1060
|
+
keyFor;
|
|
1061
|
+
renderer;
|
|
1062
|
+
conditional;
|
|
1063
|
+
itemsExpr;
|
|
1064
|
+
rows = new Map;
|
|
1065
|
+
order = [];
|
|
1066
|
+
pendingItems = null;
|
|
1067
|
+
running = false;
|
|
1068
|
+
warnedDuplicates = false;
|
|
1069
|
+
constructor(owner, anchor, templates, alias, params, parentLocals, keyFor, renderer, conditional = false, itemsExpr = "") {
|
|
1070
|
+
this.owner = owner;
|
|
1071
|
+
this.anchor = anchor;
|
|
1072
|
+
this.templates = templates;
|
|
1073
|
+
this.alias = alias;
|
|
1074
|
+
this.params = params;
|
|
1075
|
+
this.parentLocals = parentLocals;
|
|
1076
|
+
this.keyFor = keyFor;
|
|
1077
|
+
this.renderer = renderer;
|
|
1078
|
+
this.conditional = conditional;
|
|
1079
|
+
this.itemsExpr = itemsExpr;
|
|
1080
|
+
}
|
|
1081
|
+
toItems(value) {
|
|
1082
|
+
if (this.conditional) {
|
|
1083
|
+
return Number.isInteger(value) && value >= 0 ? [value] : [];
|
|
1084
|
+
}
|
|
1085
|
+
return toIterable(value);
|
|
1086
|
+
}
|
|
1087
|
+
templateFor(item) {
|
|
1088
|
+
return this.conditional ? this.templates[item] : this.templates[0];
|
|
1089
|
+
}
|
|
1090
|
+
schedule(items) {
|
|
1091
|
+
this.pendingItems = items;
|
|
1092
|
+
if (this.running)
|
|
1093
|
+
return;
|
|
1094
|
+
this.running = true;
|
|
1095
|
+
this.drain();
|
|
1096
|
+
}
|
|
1097
|
+
async drain() {
|
|
1098
|
+
try {
|
|
1099
|
+
while (this.pendingItems !== null) {
|
|
1100
|
+
const items = this.pendingItems;
|
|
1101
|
+
this.pendingItems = null;
|
|
1102
|
+
await this.reconcile(items);
|
|
1103
|
+
}
|
|
1104
|
+
} finally {
|
|
1105
|
+
this.running = false;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
keysFor(items) {
|
|
1109
|
+
const occurrences = new Map;
|
|
1110
|
+
return items.map((item) => {
|
|
1111
|
+
const locals = [...this.parentLocals, item];
|
|
1112
|
+
const base = identityOf(this.keyFor ? this.keyFor(locals) : item);
|
|
1113
|
+
const seen = occurrences.get(base) ?? 0;
|
|
1114
|
+
occurrences.set(base, seen + 1);
|
|
1115
|
+
if (seen > 0 && !this.keyFor && !this.warnedDuplicates) {
|
|
1116
|
+
this.warnedDuplicates = true;
|
|
1117
|
+
{
|
|
1118
|
+
warn(`$for over duplicate values in "${this.alias}" - rows are matched by position among equal items. Add $key for exact identity.`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return `${base}#${seen}`;
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
async reconcile(items) {
|
|
1125
|
+
const keys = this.keysFor(items);
|
|
1126
|
+
const next = new Map;
|
|
1127
|
+
for (let i = 0;i < items.length; i++) {
|
|
1128
|
+
const key = keys[i];
|
|
1129
|
+
const existing = this.rows.get(key);
|
|
1130
|
+
if (existing) {
|
|
1131
|
+
this.rows.delete(key);
|
|
1132
|
+
if (this.alias)
|
|
1133
|
+
existing.item = items[i];
|
|
1134
|
+
this.renderer.refreshRow(this, existing);
|
|
1135
|
+
next.set(key, existing);
|
|
1136
|
+
} else {
|
|
1137
|
+
const row = await this.renderer.createRow(this, items[i]);
|
|
1138
|
+
row.key = key;
|
|
1139
|
+
next.set(key, row);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
this.rows.forEach((row) => this.renderer.destroyRow(this, row));
|
|
1143
|
+
this.rows = next;
|
|
1144
|
+
this.order = keys;
|
|
1145
|
+
this.position();
|
|
1146
|
+
}
|
|
1147
|
+
position() {
|
|
1148
|
+
const parent = this.anchor.parentNode;
|
|
1149
|
+
if (!parent)
|
|
1150
|
+
return;
|
|
1151
|
+
let reference = this.anchor;
|
|
1152
|
+
for (let i = this.order.length - 1;i >= 0; i--) {
|
|
1153
|
+
const row = this.rows.get(this.order[i]);
|
|
1154
|
+
if (!row)
|
|
1155
|
+
continue;
|
|
1156
|
+
for (let j = row.nodes.length - 1;j >= 0; j--) {
|
|
1157
|
+
const node = row.nodes[j];
|
|
1158
|
+
if (node.nextSibling !== reference || node.parentNode !== parent) {
|
|
1159
|
+
parent.insertBefore(node, reference);
|
|
1160
|
+
}
|
|
1161
|
+
reference = node;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (parent.isConnected)
|
|
1165
|
+
this.renderer.connected?.();
|
|
1166
|
+
}
|
|
1167
|
+
destroy() {
|
|
1168
|
+
this.pendingItems = null;
|
|
1169
|
+
this.rows.forEach((row) => this.renderer.destroyRow(this, row));
|
|
1170
|
+
this.rows.clear();
|
|
1171
|
+
this.order = [];
|
|
1172
|
+
this.anchor.parentNode?.removeChild(this.anchor);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/runtime/dom/hydration.ts
|
|
1177
|
+
var TRIGGERS = new Set([
|
|
1178
|
+
"load",
|
|
1179
|
+
"idle",
|
|
1180
|
+
"visible",
|
|
1181
|
+
"interaction",
|
|
1182
|
+
"never",
|
|
1183
|
+
"always"
|
|
1184
|
+
]);
|
|
1185
|
+
function parseTrigger(value) {
|
|
1186
|
+
if (!value)
|
|
1187
|
+
return "load";
|
|
1188
|
+
const trigger = value.trim().toLowerCase();
|
|
1189
|
+
if (TRIGGERS.has(trigger)) {
|
|
1190
|
+
return trigger;
|
|
1191
|
+
}
|
|
1192
|
+
warn(`Unknown $hydrate="${value}" - expected load, idle, visible, interaction, never or always. Falling back to load.`);
|
|
1193
|
+
return "load";
|
|
1194
|
+
}
|
|
1195
|
+
function scheduleHydration(trigger, element, run) {
|
|
1196
|
+
if (trigger === "load") {
|
|
1197
|
+
run();
|
|
1198
|
+
return () => {
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
if (trigger === "idle") {
|
|
1202
|
+
const idle = globalThis.requestIdleCallback;
|
|
1203
|
+
if (typeof idle === "function") {
|
|
1204
|
+
const handle2 = idle(run);
|
|
1205
|
+
return () => globalThis.cancelIdleCallback?.(handle2);
|
|
1206
|
+
}
|
|
1207
|
+
const handle = setTimeout(run, 1);
|
|
1208
|
+
return () => clearTimeout(handle);
|
|
1209
|
+
}
|
|
1210
|
+
if (trigger === "interaction") {
|
|
1211
|
+
const events = ["pointerdown", "keydown", "focusin"];
|
|
1212
|
+
const cancel = () => {
|
|
1213
|
+
events.forEach((event) => element.removeEventListener(event, fire));
|
|
1214
|
+
};
|
|
1215
|
+
const fire = () => {
|
|
1216
|
+
cancel();
|
|
1217
|
+
run();
|
|
1218
|
+
};
|
|
1219
|
+
events.forEach((event) => element.addEventListener(event, fire));
|
|
1220
|
+
return cancel;
|
|
1221
|
+
}
|
|
1222
|
+
const Observer = globalThis.IntersectionObserver;
|
|
1223
|
+
if (typeof Observer !== "function") {
|
|
1224
|
+
warn('$hydrate="visible" needs IntersectionObserver; hydrating immediately.');
|
|
1225
|
+
run();
|
|
1226
|
+
return () => {
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
const observer = new Observer((entries) => {
|
|
1230
|
+
if (!entries.some((entry) => entry.isIntersecting))
|
|
1231
|
+
return;
|
|
1232
|
+
observer.disconnect();
|
|
1233
|
+
run();
|
|
1234
|
+
});
|
|
1235
|
+
observer.observe(element);
|
|
1236
|
+
return () => observer.disconnect();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/marker.ts
|
|
1240
|
+
var START = "lsp ";
|
|
1241
|
+
var END = "/lsp";
|
|
1242
|
+
var MARKER_END = `<!--${END}-->`;
|
|
1243
|
+
function isMarkerStart(node) {
|
|
1244
|
+
return node.nodeType === 8 && node.data.startsWith(START);
|
|
1245
|
+
}
|
|
1246
|
+
function isMarkerEnd(node) {
|
|
1247
|
+
return node.nodeType === 8 && node.data.trim() === END;
|
|
1248
|
+
}
|
|
1249
|
+
function parseMarker(node) {
|
|
1250
|
+
const data = node.data.slice(START.length);
|
|
1251
|
+
try {
|
|
1252
|
+
const payload = JSON.parse(data);
|
|
1253
|
+
return typeof payload?.c === "string" ? payload : null;
|
|
1254
|
+
} catch {
|
|
1255
|
+
warn("Malformed island marker:", data);
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function findMarkerEnd(nodes, start) {
|
|
1260
|
+
let depth = 0;
|
|
1261
|
+
for (let i = start;i < nodes.length; i++) {
|
|
1262
|
+
if (isMarkerStart(nodes[i]))
|
|
1263
|
+
depth++;
|
|
1264
|
+
else if (isMarkerEnd(nodes[i])) {
|
|
1265
|
+
depth--;
|
|
1266
|
+
if (depth === 0)
|
|
1267
|
+
return i;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return -1;
|
|
1271
|
+
}
|
|
1272
|
+
function slotNodesFor(payload, between) {
|
|
1273
|
+
if (!payload.ssr)
|
|
1274
|
+
return between;
|
|
1275
|
+
const stash = between.find((node) => node.nodeType === 1 && node.tagName === "TEMPLATE" && node.hasAttribute("data-lsp-slot"));
|
|
1276
|
+
return stash ? Array.from(stash.content.childNodes) : [];
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/runtime/dom/Mounter.ts
|
|
1280
|
+
class Mounter {
|
|
1281
|
+
registry;
|
|
1282
|
+
bindings;
|
|
1283
|
+
events;
|
|
1284
|
+
initInstance;
|
|
1285
|
+
releaseInstance;
|
|
1286
|
+
resolve;
|
|
1287
|
+
metadata;
|
|
1288
|
+
ssg;
|
|
1289
|
+
styles;
|
|
1290
|
+
constructor(registry, bindings, events, initInstance, releaseInstance = () => {
|
|
1291
|
+
}, resolve = async (id) => null, metadata = () => null, ssg = false, styles = null) {
|
|
1292
|
+
this.registry = registry;
|
|
1293
|
+
this.bindings = bindings;
|
|
1294
|
+
this.events = events;
|
|
1295
|
+
this.initInstance = initInstance;
|
|
1296
|
+
this.releaseInstance = releaseInstance;
|
|
1297
|
+
this.resolve = resolve;
|
|
1298
|
+
this.metadata = metadata;
|
|
1299
|
+
this.ssg = ssg;
|
|
1300
|
+
this.styles = styles;
|
|
1301
|
+
}
|
|
1302
|
+
async mountRoot(componentId, container, props = {}, slotNodes = []) {
|
|
1303
|
+
const instance = await this.mount(componentId, null, props, slotNodes);
|
|
1304
|
+
if (!instance)
|
|
1305
|
+
return null;
|
|
1306
|
+
container.innerHTML = "";
|
|
1307
|
+
const frag = document.createDocumentFragment();
|
|
1308
|
+
instance.rootNodes.forEach((node) => frag.appendChild(node));
|
|
1309
|
+
container.appendChild(frag);
|
|
1310
|
+
return instance;
|
|
1311
|
+
}
|
|
1312
|
+
async mount(componentId, parent, props, slotNodes, preserved, adopt, rescue) {
|
|
1313
|
+
const definition = await this.resolve(componentId);
|
|
1314
|
+
if (!definition)
|
|
1315
|
+
return null;
|
|
1316
|
+
const instance = await this.initInstance(componentId, parent, props);
|
|
1317
|
+
if (!instance)
|
|
1318
|
+
return null;
|
|
1319
|
+
let fragment = this.parseTemplate(definition.template);
|
|
1320
|
+
const markers = [];
|
|
1321
|
+
const context = {
|
|
1322
|
+
owner: instance,
|
|
1323
|
+
holder: instance,
|
|
1324
|
+
params: [],
|
|
1325
|
+
locals: [],
|
|
1326
|
+
preserved
|
|
1327
|
+
};
|
|
1328
|
+
if (adopt) {
|
|
1329
|
+
const adopted = [];
|
|
1330
|
+
if (this.adoptNodes(Array.from(fragment.childNodes), adopt, context, adopted)) {
|
|
1331
|
+
for (const marker of adopted) {
|
|
1332
|
+
await this.mountMarker(marker, instance, context);
|
|
1333
|
+
}
|
|
1334
|
+
instance.rootNodes = significant(adopt);
|
|
1335
|
+
const adoptedRoot = instance.rootNodes.find((node) => node.nodeType === 1);
|
|
1336
|
+
adoptedRoot?.setAttribute("data-lsp-instance", instance.id);
|
|
1337
|
+
parent?.children.push(instance);
|
|
1338
|
+
return instance;
|
|
1339
|
+
}
|
|
1340
|
+
warn(`Could not adopt server-rendered DOM for <${componentId}> - rebuilding. Something rendered at build differs from what the client expects.`);
|
|
1341
|
+
instance.releaseDom();
|
|
1342
|
+
fragment = this.parseTemplate(definition.template);
|
|
1343
|
+
}
|
|
1344
|
+
const parentOwned = this.fillSlots(fragment, slotNodes, parent !== null);
|
|
1345
|
+
if (rescue)
|
|
1346
|
+
context.preserved = rescue();
|
|
1347
|
+
this.collect(fragment, context, parentOwned, markers);
|
|
1348
|
+
for (const marker of markers) {
|
|
1349
|
+
await this.mountMarker(marker, instance, context);
|
|
1350
|
+
}
|
|
1351
|
+
instance.rootNodes = Array.from(fragment.childNodes);
|
|
1352
|
+
const rootElement = instance.rootNodes.find((node) => node.nodeType === 1);
|
|
1353
|
+
rootElement?.setAttribute("data-lsp-instance", instance.id);
|
|
1354
|
+
parent?.children.push(instance);
|
|
1355
|
+
return instance;
|
|
1356
|
+
}
|
|
1357
|
+
unmount(instance) {
|
|
1358
|
+
const nodes = [...instance.rootNodes];
|
|
1359
|
+
instance.destroy();
|
|
1360
|
+
nodes.forEach((node) => node.remove?.());
|
|
1361
|
+
const siblings = instance.parent?.children;
|
|
1362
|
+
if (siblings) {
|
|
1363
|
+
const index = siblings.indexOf(instance);
|
|
1364
|
+
if (index > -1)
|
|
1365
|
+
siblings.splice(index, 1);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
parseTemplate(html) {
|
|
1369
|
+
const template = document.createElement("template");
|
|
1370
|
+
template.innerHTML = html.trim();
|
|
1371
|
+
return template.content;
|
|
1372
|
+
}
|
|
1373
|
+
fillSlots(fragment, slotNodes, ownedByParent = true) {
|
|
1374
|
+
const outlets = Array.from(fragment.querySelectorAll("slot"));
|
|
1375
|
+
const placed = expandSlotFills(slotNodes);
|
|
1376
|
+
const parentOwned = ownedByParent ? new Set(placed) : new Set;
|
|
1377
|
+
if (outlets.length === 0)
|
|
1378
|
+
return parentOwned;
|
|
1379
|
+
const byName = new Map;
|
|
1380
|
+
for (const node of slotNodes) {
|
|
1381
|
+
const name = node.nodeType === 1 ? node.getAttribute("slot") ?? "" : "";
|
|
1382
|
+
const group = byName.get(name) ?? [];
|
|
1383
|
+
group.push(node);
|
|
1384
|
+
byName.set(name, group);
|
|
1385
|
+
}
|
|
1386
|
+
const filled = new Set;
|
|
1387
|
+
for (const outlet of outlets) {
|
|
1388
|
+
const name = outlet.getAttribute("name") ?? "";
|
|
1389
|
+
if (filled.has(name)) {
|
|
1390
|
+
warn(`Duplicate <slot${name ? ` name="${name}"` : ""}> - only the first is filled.`);
|
|
1391
|
+
outlet.remove();
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
filled.add(name);
|
|
1395
|
+
const supplied = byName.get(name);
|
|
1396
|
+
byName.delete(name);
|
|
1397
|
+
const holder = document.createDocumentFragment();
|
|
1398
|
+
if (supplied?.length) {
|
|
1399
|
+
supplied.forEach((node) => {
|
|
1400
|
+
if (isSlotTemplate(node)) {
|
|
1401
|
+
holder.appendChild(node.content);
|
|
1402
|
+
} else {
|
|
1403
|
+
if (node.nodeType === 1)
|
|
1404
|
+
node.removeAttribute("slot");
|
|
1405
|
+
holder.appendChild(node);
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
if (this.ssg) {
|
|
1409
|
+
holder.insertBefore(document.createComment(`${SLOT_MARK}${name}`), holder.firstChild);
|
|
1410
|
+
holder.appendChild(document.createComment(SLOT_END));
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
Array.from(outlet.childNodes).forEach((node) => holder.appendChild(node));
|
|
1414
|
+
}
|
|
1415
|
+
outlet.replaceWith(holder);
|
|
1416
|
+
}
|
|
1417
|
+
for (const [name, nodes] of byName) {
|
|
1418
|
+
if (!nodes.length)
|
|
1419
|
+
continue;
|
|
1420
|
+
warn(name ? `No <slot name="${name}"> to receive this content - it was dropped.` : "Content passed to a component with no unnamed <slot> - it was dropped.");
|
|
1421
|
+
}
|
|
1422
|
+
return parentOwned;
|
|
1423
|
+
}
|
|
1424
|
+
collect(node, context, skip, markers) {
|
|
1425
|
+
if (skip.has(node))
|
|
1426
|
+
return;
|
|
1427
|
+
if (node.nodeType === 1) {
|
|
1428
|
+
const el = node;
|
|
1429
|
+
if (el.hasAttribute("data-bind-for")) {
|
|
1430
|
+
this.createBlock(el, context, "for");
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (el.hasAttribute("data-bind-if")) {
|
|
1434
|
+
this.createBlock(el, context, "if");
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (el.hasAttribute("data-bind-else") || el.hasAttribute("data-bind-else-if")) {
|
|
1438
|
+
warn("$else / $else-if must directly follow a $if or $else-if sibling", el);
|
|
1439
|
+
el.remove();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
if (el.hasAttribute("data-lsp-needs")) {
|
|
1443
|
+
this.collectNeeds(el, context, markers);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
this.collectElement(el, context);
|
|
1447
|
+
}
|
|
1448
|
+
this.collectChildren(node, context, skip, markers);
|
|
1449
|
+
}
|
|
1450
|
+
collectNeeds(el, context, markers) {
|
|
1451
|
+
const keys = (el.getAttribute("data-lsp-needs") ?? "").split(",").map((key) => key.trim()).filter(Boolean);
|
|
1452
|
+
const shimmer = el.getAttribute("data-lsp-shimmer");
|
|
1453
|
+
el.removeAttribute("data-lsp-needs");
|
|
1454
|
+
el.removeAttribute("data-lsp-shimmer");
|
|
1455
|
+
this.collectElement(el, context);
|
|
1456
|
+
if (!keys.length) {
|
|
1457
|
+
warn(`$needs on <${el.tagName.toLowerCase()}> must name at least one cell - bare $needs is only meaningful on a component usage.`);
|
|
1458
|
+
this.collectChildren(el, context, new Set, markers);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (!this.bindings.cellManager.pendingKeys(keys).length) {
|
|
1462
|
+
this.collectChildren(el, context, new Set, markers);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const held = document.createDocumentFragment();
|
|
1466
|
+
Array.from(el.childNodes).forEach((child) => held.appendChild(child));
|
|
1467
|
+
el.setAttribute("data-lsp-pending", "");
|
|
1468
|
+
if (shimmer !== null) {
|
|
1469
|
+
el.setAttribute("data-lsp-shimmer", "");
|
|
1470
|
+
if (shimmer)
|
|
1471
|
+
el.setAttribute("style", joinStyle(el, `--lsp-pending-width: ${shimmer}`));
|
|
1472
|
+
this.styles?.ensurePendingStyles();
|
|
1473
|
+
}
|
|
1474
|
+
const cancel = this.bindings.cellManager.whenReady(keys, () => {
|
|
1475
|
+
el.removeAttribute("data-lsp-pending");
|
|
1476
|
+
el.removeAttribute("data-lsp-shimmer");
|
|
1477
|
+
Array.from(held.childNodes).forEach((child) => el.appendChild(child));
|
|
1478
|
+
const nested = [];
|
|
1479
|
+
this.collectChildren(el, context, new Set, nested);
|
|
1480
|
+
nested.forEach((range) => {
|
|
1481
|
+
this.mountMarker(range, context.owner, context);
|
|
1482
|
+
});
|
|
1483
|
+
this.bindings.updateInstance(context.owner);
|
|
1484
|
+
});
|
|
1485
|
+
context.holder.addCleanup(cancel);
|
|
1486
|
+
}
|
|
1487
|
+
collectChildren(node, context, skip, markers) {
|
|
1488
|
+
const children = Array.from(node.childNodes);
|
|
1489
|
+
for (let i = 0;i < children.length; i++) {
|
|
1490
|
+
const child = children[i];
|
|
1491
|
+
if (!isMarkerStart(child)) {
|
|
1492
|
+
this.collect(child, context, skip, markers);
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
const endIndex = findMarkerEnd(children, i);
|
|
1496
|
+
if (endIndex === -1) {
|
|
1497
|
+
warn("Unterminated island marker:", child.data);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const payload = parseMarker(child);
|
|
1501
|
+
const between = children.slice(i + 1, endIndex);
|
|
1502
|
+
const slotNodes = payload ? slotNodesFor(payload, between) : between;
|
|
1503
|
+
expandSlotFills(slotNodes).forEach((slotNode) => this.collect(slotNode, context, skip, markers));
|
|
1504
|
+
if (payload) {
|
|
1505
|
+
markers.push({
|
|
1506
|
+
start: child,
|
|
1507
|
+
end: children[endIndex],
|
|
1508
|
+
payload,
|
|
1509
|
+
slotNodes: liveSlotNodes(payload, child, children[endIndex], slotNodes)
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
i = endIndex;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
rescueRendered(range) {
|
|
1516
|
+
const rescued = new Map;
|
|
1517
|
+
const counts = new Map;
|
|
1518
|
+
const children = [];
|
|
1519
|
+
let current = range.start.nextSibling;
|
|
1520
|
+
while (current && current !== range.end) {
|
|
1521
|
+
children.push(current);
|
|
1522
|
+
current = current.nextSibling;
|
|
1523
|
+
}
|
|
1524
|
+
const walk = (nodes) => {
|
|
1525
|
+
for (let i = 0;i < nodes.length; i++) {
|
|
1526
|
+
const node = nodes[i];
|
|
1527
|
+
if (!isMarkerStart(node)) {
|
|
1528
|
+
walk(Array.from(node.childNodes));
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
const endIndex = findMarkerEnd(nodes, i);
|
|
1532
|
+
if (endIndex === -1)
|
|
1533
|
+
continue;
|
|
1534
|
+
const payload = parseMarker(node);
|
|
1535
|
+
const between = nodes.slice(i + 1, endIndex);
|
|
1536
|
+
if (payload) {
|
|
1537
|
+
const ordinal = counts.get(payload.c) ?? 0;
|
|
1538
|
+
counts.set(payload.c, ordinal + 1);
|
|
1539
|
+
const held = document.createDocumentFragment();
|
|
1540
|
+
between.forEach((child) => held.appendChild(child));
|
|
1541
|
+
rescued.set(`${payload.c}#${ordinal}`, Array.from(held.childNodes));
|
|
1542
|
+
}
|
|
1543
|
+
i = endIndex;
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
walk(children);
|
|
1547
|
+
return rescued;
|
|
1548
|
+
}
|
|
1549
|
+
takeRescued(range, context) {
|
|
1550
|
+
if (!context.preserved)
|
|
1551
|
+
return null;
|
|
1552
|
+
const seen = context.seen ??= new Map;
|
|
1553
|
+
const ordinal = seen.get(range.payload.c) ?? 0;
|
|
1554
|
+
seen.set(range.payload.c, ordinal + 1);
|
|
1555
|
+
return context.preserved.get(`${range.payload.c}#${ordinal}`) ?? null;
|
|
1556
|
+
}
|
|
1557
|
+
adoptNodes(templateNodes, domNodes, context, markers) {
|
|
1558
|
+
const wanted = significant(templateNodes);
|
|
1559
|
+
const found = significant(domNodes);
|
|
1560
|
+
let d = 0;
|
|
1561
|
+
for (let i = 0;i < wanted.length; i++) {
|
|
1562
|
+
const node = wanted[i];
|
|
1563
|
+
if (node.nodeType === 1) {
|
|
1564
|
+
const el = node;
|
|
1565
|
+
if (el.hasAttribute("data-bind-for")) {
|
|
1566
|
+
const consumed = this.adoptBlock(el, found, d, context, "for");
|
|
1567
|
+
if (!consumed)
|
|
1568
|
+
return false;
|
|
1569
|
+
d += consumed.dom;
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
if (el.hasAttribute("data-bind-if")) {
|
|
1573
|
+
const consumed = this.adoptBlock(el, found, d, context, "if");
|
|
1574
|
+
if (!consumed)
|
|
1575
|
+
return false;
|
|
1576
|
+
d += consumed.dom;
|
|
1577
|
+
i += consumed.templates - 1;
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
if (el.tagName === "SLOT") {
|
|
1581
|
+
if (!found[d] || !isSlotStart(found[d])) {
|
|
1582
|
+
if (!this.adoptNodes(Array.from(el.childNodes), found.slice(d), context, markers)) {
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
d = found.length;
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
while (d < found.length && !isSlotEnd(found[d]))
|
|
1589
|
+
d++;
|
|
1590
|
+
d++;
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
const target = found[d++];
|
|
1594
|
+
if (!target || target.nodeType !== 1)
|
|
1595
|
+
return false;
|
|
1596
|
+
if (target.tagName !== el.tagName)
|
|
1597
|
+
return false;
|
|
1598
|
+
if (el.tagName === "NOSCRIPT")
|
|
1599
|
+
continue;
|
|
1600
|
+
this.collectElement(target, context);
|
|
1601
|
+
if (!this.adoptNodes(Array.from(el.childNodes), Array.from(target.childNodes), context, markers)) {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (isMarkerStart(node)) {
|
|
1607
|
+
const end = findMarkerEnd(wanted, i);
|
|
1608
|
+
if (end === -1)
|
|
1609
|
+
return false;
|
|
1610
|
+
const domStart = found[d];
|
|
1611
|
+
if (!domStart || !isMarkerStart(domStart))
|
|
1612
|
+
return false;
|
|
1613
|
+
const domEnd = findMarkerEnd(found, d);
|
|
1614
|
+
if (domEnd === -1)
|
|
1615
|
+
return false;
|
|
1616
|
+
const payload = parseMarker(domStart);
|
|
1617
|
+
if (!payload)
|
|
1618
|
+
return false;
|
|
1619
|
+
const templateFills = wanted.slice(i + 1, end);
|
|
1620
|
+
const between = found.slice(d + 1, domEnd);
|
|
1621
|
+
const regions = templateFills.length ? findSlotRegions(between) : new Map;
|
|
1622
|
+
let slotNodes = templateFills;
|
|
1623
|
+
let slotsAdopted = regions.size > 0;
|
|
1624
|
+
if (slotsAdopted) {
|
|
1625
|
+
const placed = [];
|
|
1626
|
+
for (const [name, fills] of groupSlotFills(templateFills)) {
|
|
1627
|
+
const region = regions.get(name);
|
|
1628
|
+
if (!region || !this.adoptNodes(fills, region, context, markers)) {
|
|
1629
|
+
slotsAdopted = false;
|
|
1630
|
+
break;
|
|
1631
|
+
}
|
|
1632
|
+
placed.push(...region);
|
|
1633
|
+
}
|
|
1634
|
+
if (slotsAdopted)
|
|
1635
|
+
slotNodes = placed;
|
|
1636
|
+
}
|
|
1637
|
+
if (!slotsAdopted) {
|
|
1638
|
+
expandSlotFills(templateFills).forEach((slotNode) => this.collect(slotNode, context, new Set, markers));
|
|
1639
|
+
slotNodes = templateFills;
|
|
1640
|
+
}
|
|
1641
|
+
markers.push({
|
|
1642
|
+
start: domStart,
|
|
1643
|
+
end: found[domEnd],
|
|
1644
|
+
payload,
|
|
1645
|
+
slotNodes,
|
|
1646
|
+
adopted: templateFills.length === 0 || slotsAdopted
|
|
1647
|
+
});
|
|
1648
|
+
i = end;
|
|
1649
|
+
d = domEnd + 1;
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
d++;
|
|
1653
|
+
}
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
adoptBlock(el, found, from, context, kind) {
|
|
1657
|
+
let anchorIndex = -1;
|
|
1658
|
+
for (let i = from;i < found.length; i++) {
|
|
1659
|
+
if (isBlockAnchor(found[i])) {
|
|
1660
|
+
anchorIndex = i;
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
if (!isRowMark(found[i]) && i === from)
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (anchorIndex === -1)
|
|
1667
|
+
return null;
|
|
1668
|
+
const block = this.createBlock(el, context, kind, found[anchorIndex]);
|
|
1669
|
+
if (!block)
|
|
1670
|
+
return null;
|
|
1671
|
+
const slices = anchorIndex === from ? [] : sliceRows(found, from, anchorIndex);
|
|
1672
|
+
if (!slices)
|
|
1673
|
+
return null;
|
|
1674
|
+
if (!this.adoptRows(block, slices, context))
|
|
1675
|
+
return null;
|
|
1676
|
+
return {
|
|
1677
|
+
dom: anchorIndex - from + 1,
|
|
1678
|
+
templates: block.templates.length
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
onConnected;
|
|
1682
|
+
fillRange(range, nodes) {
|
|
1683
|
+
const parent = range.start.parentNode;
|
|
1684
|
+
if (!parent)
|
|
1685
|
+
return;
|
|
1686
|
+
let current = range.start.nextSibling;
|
|
1687
|
+
while (current && current !== range.end) {
|
|
1688
|
+
const next = current.nextSibling;
|
|
1689
|
+
parent.removeChild(current);
|
|
1690
|
+
current = next;
|
|
1691
|
+
}
|
|
1692
|
+
const fragment = document.createDocumentFragment();
|
|
1693
|
+
nodes.forEach((node) => fragment.appendChild(node));
|
|
1694
|
+
parent.insertBefore(fragment, range.end);
|
|
1695
|
+
if (parent.isConnected)
|
|
1696
|
+
this.onConnected?.();
|
|
1697
|
+
}
|
|
1698
|
+
collectElement(el, context) {
|
|
1699
|
+
addScope(el, context.owner.scopeId);
|
|
1700
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1701
|
+
this.applyDirective(el, attr.name, attr.value, context);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
createBlock(el, context, kind, existingAnchor) {
|
|
1705
|
+
const conditional = kind === "if";
|
|
1706
|
+
const rawExpression = el.getAttribute(`data-bind-${kind}`) ?? "";
|
|
1707
|
+
const branches = conditional ? this.gatherBranches(el) : [];
|
|
1708
|
+
const expression = conditional ? composeBranchExpression(branches) : rawExpression;
|
|
1709
|
+
const parsed = conditional ? { alias: null, items: expression } : parseForExpression(rawExpression);
|
|
1710
|
+
if (!parsed) {
|
|
1711
|
+
warn(`Could not parse $for="${expression}" - expected "item in items"`);
|
|
1712
|
+
el.remove();
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
if (!conditional && el.hasAttribute("data-bind-if")) {
|
|
1716
|
+
warn(`$for and $if on the same element are not supported (${rawExpression}). Wrap one in an element.`);
|
|
1717
|
+
}
|
|
1718
|
+
const keyExpr = conditional ? null : el.getAttribute("data-bind-key");
|
|
1719
|
+
el.removeAttribute(`data-bind-${kind}`);
|
|
1720
|
+
el.removeAttribute("data-bind-key");
|
|
1721
|
+
let anchor;
|
|
1722
|
+
if (existingAnchor) {
|
|
1723
|
+
anchor = existingAnchor;
|
|
1724
|
+
el.remove();
|
|
1725
|
+
} else {
|
|
1726
|
+
anchor = document.createComment(` ${kind}: ${expression} `);
|
|
1727
|
+
el.replaceWith(anchor);
|
|
1728
|
+
}
|
|
1729
|
+
const templates = conditional ? branches.map((b) => b.el) : [el];
|
|
1730
|
+
const params = parsed.alias ? [...context.params, parsed.alias] : context.params;
|
|
1731
|
+
const keyFor = keyExpr ? (locals) => {
|
|
1732
|
+
const compiled = this.bindings.compileExpression(keyExpr, params);
|
|
1733
|
+
return compiled(context.owner.state, context.owner.props, this.cellsProxy(), this.bindings.pendingProbe(), ...locals);
|
|
1734
|
+
} : null;
|
|
1735
|
+
const block = new ForBlock(context.owner, anchor, templates, parsed.alias, context.params, context.locals, keyFor, this, conditional, parsed.items);
|
|
1736
|
+
context.holder.addBlock(block);
|
|
1737
|
+
context.holder.addBinding(this.bindings.createForBinding(block, parsed.items, context.params));
|
|
1738
|
+
return block;
|
|
1739
|
+
}
|
|
1740
|
+
adoptRows(block, slices, context) {
|
|
1741
|
+
const value = this.bindings.evaluate(block.itemsExpr, context.owner, context.locals);
|
|
1742
|
+
const items = block.toItems(value);
|
|
1743
|
+
if (items.length !== slices.length)
|
|
1744
|
+
return false;
|
|
1745
|
+
const keys = block.keysFor(items);
|
|
1746
|
+
for (let i = 0;i < items.length; i++) {
|
|
1747
|
+
const item = items[i];
|
|
1748
|
+
const row = new Row(block.alias ? [...block.parentLocals, item] : [...block.parentLocals]);
|
|
1749
|
+
row.key = keys[i];
|
|
1750
|
+
row.nodes = slices[i];
|
|
1751
|
+
const rowContext = {
|
|
1752
|
+
owner: block.owner,
|
|
1753
|
+
holder: row,
|
|
1754
|
+
params: block.alias ? [...block.params, block.alias] : block.params,
|
|
1755
|
+
locals: row.locals
|
|
1756
|
+
};
|
|
1757
|
+
const template = block.templateFor(item);
|
|
1758
|
+
const templateNodes = template.tagName === "TEMPLATE" ? Array.from(template.content.cloneNode(true).childNodes) : [template.cloneNode(true)];
|
|
1759
|
+
const markers = [];
|
|
1760
|
+
const content = slices[i].filter((node) => !isRowMark(node));
|
|
1761
|
+
if (!this.adoptNodes(templateNodes, content, rowContext, markers)) {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
for (const marker of markers) {
|
|
1765
|
+
this.mountMarker(marker, block.owner, rowContext).then((child) => {
|
|
1766
|
+
if (child)
|
|
1767
|
+
row.instances.push(child);
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
block.rows.set(keys[i], row);
|
|
1771
|
+
block.order.push(keys[i]);
|
|
1772
|
+
}
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
gatherBranches(el) {
|
|
1776
|
+
const branches = [
|
|
1777
|
+
{ condition: el.getAttribute("data-bind-if"), el }
|
|
1778
|
+
];
|
|
1779
|
+
let sibling = el.nextElementSibling;
|
|
1780
|
+
while (sibling) {
|
|
1781
|
+
const next = sibling.nextElementSibling;
|
|
1782
|
+
if (sibling.hasAttribute("data-bind-else-if")) {
|
|
1783
|
+
branches.push({
|
|
1784
|
+
condition: sibling.getAttribute("data-bind-else-if"),
|
|
1785
|
+
el: sibling
|
|
1786
|
+
});
|
|
1787
|
+
sibling.removeAttribute("data-bind-else-if");
|
|
1788
|
+
sibling.remove();
|
|
1789
|
+
sibling = next;
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
if (sibling.hasAttribute("data-bind-else")) {
|
|
1793
|
+
branches.push({ condition: null, el: sibling });
|
|
1794
|
+
sibling.removeAttribute("data-bind-else");
|
|
1795
|
+
sibling.remove();
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
break;
|
|
1799
|
+
}
|
|
1800
|
+
return branches;
|
|
1801
|
+
}
|
|
1802
|
+
async createRow(block, item) {
|
|
1803
|
+
const row = new Row(block.alias ? [...block.parentLocals, item] : [...block.parentLocals]);
|
|
1804
|
+
const fragment = document.createDocumentFragment();
|
|
1805
|
+
const source = block.templateFor(item);
|
|
1806
|
+
fragment.appendChild(source.tagName === "TEMPLATE" ? source.content.cloneNode(true) : source.cloneNode(true));
|
|
1807
|
+
const context = {
|
|
1808
|
+
owner: block.owner,
|
|
1809
|
+
holder: row,
|
|
1810
|
+
params: block.alias ? [...block.params, block.alias] : block.params,
|
|
1811
|
+
locals: row.locals
|
|
1812
|
+
};
|
|
1813
|
+
const markers = [];
|
|
1814
|
+
this.collect(fragment, context, new Set, markers);
|
|
1815
|
+
for (const marker of markers) {
|
|
1816
|
+
const child = await this.mountMarker(marker, block.owner, context);
|
|
1817
|
+
if (child)
|
|
1818
|
+
row.instances.push(child);
|
|
1819
|
+
}
|
|
1820
|
+
if (this.ssg) {
|
|
1821
|
+
fragment.insertBefore(document.createComment(ROW_MARK), fragment.firstChild);
|
|
1822
|
+
}
|
|
1823
|
+
row.nodes = Array.from(fragment.childNodes);
|
|
1824
|
+
this.bindings.updateRow(block.owner, row);
|
|
1825
|
+
row.instances.forEach((child) => this.bindings.updateTree(child));
|
|
1826
|
+
return row;
|
|
1827
|
+
}
|
|
1828
|
+
connected() {
|
|
1829
|
+
this.onConnected?.();
|
|
1830
|
+
}
|
|
1831
|
+
refreshRow(block, row) {
|
|
1832
|
+
this.bindings.updateRow(block.owner, row);
|
|
1833
|
+
}
|
|
1834
|
+
destroyRow(block, row) {
|
|
1835
|
+
row.blocks.forEach((nested) => nested.destroy());
|
|
1836
|
+
row.blocks.length = 0;
|
|
1837
|
+
row.instances.forEach((instance) => {
|
|
1838
|
+
const siblings = block.owner.children;
|
|
1839
|
+
const index = siblings.indexOf(instance);
|
|
1840
|
+
if (index > -1)
|
|
1841
|
+
siblings.splice(index, 1);
|
|
1842
|
+
this.releaseInstance(instance);
|
|
1843
|
+
instance.destroy();
|
|
1844
|
+
});
|
|
1845
|
+
row.instances.length = 0;
|
|
1846
|
+
row.cleanup.forEach((cleanup) => cleanup());
|
|
1847
|
+
row.cleanup.length = 0;
|
|
1848
|
+
row.bindings.length = 0;
|
|
1849
|
+
row.nodes.forEach((node) => node.remove?.());
|
|
1850
|
+
row.nodes.length = 0;
|
|
1851
|
+
}
|
|
1852
|
+
cellsProxy() {
|
|
1853
|
+
return this.bindings.readProxy();
|
|
1854
|
+
}
|
|
1855
|
+
applyDirective(el, name, value, context) {
|
|
1856
|
+
const { holder, params } = context;
|
|
1857
|
+
if (name === "data-lsp-ref") {
|
|
1858
|
+
if (context.owner.refs[value]) {
|
|
1859
|
+
warn(`Duplicate $ref="${value}" in <${context.owner.componentId}> - a ref names one element, and inside a $for every row would claim it. The last one wins.`);
|
|
1860
|
+
}
|
|
1861
|
+
context.owner.refs[value] = el;
|
|
1862
|
+
return true;
|
|
1863
|
+
}
|
|
1864
|
+
if (name === "data-text") {
|
|
1865
|
+
holder.addBinding(this.bindings.createBinding(el, "text", value, undefined, params));
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
if (name === "data-bind-show") {
|
|
1869
|
+
holder.addBinding(this.bindings.createBinding(el, "show", value, undefined, params));
|
|
1870
|
+
return true;
|
|
1871
|
+
}
|
|
1872
|
+
if (name === "data-bind-htmlfor") {
|
|
1873
|
+
holder.addBinding(this.bindings.createBinding(el, "attr", value, "for", params));
|
|
1874
|
+
return true;
|
|
1875
|
+
}
|
|
1876
|
+
if (name === "data-bind-html") {
|
|
1877
|
+
holder.addBinding(this.bindings.createBinding(el, "html", value, undefined, params));
|
|
1878
|
+
return true;
|
|
1879
|
+
}
|
|
1880
|
+
if (name === "data-bind-class") {
|
|
1881
|
+
holder.addBinding(this.bindings.createBinding(el, "class", value, undefined, params));
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
if (name === "data-bind-model") {
|
|
1885
|
+
context.owner.models++;
|
|
1886
|
+
const binding = this.bindings.createModelBinding(el, value, params);
|
|
1887
|
+
holder.addBinding(binding);
|
|
1888
|
+
this.bindings.attachModel(binding, new BindingScope(context.owner, context.locals, holder));
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1891
|
+
if (name === "data-bind-key" || name === "data-bind-if" || name === "data-bind-else" || name === "data-bind-else-if" || name === "data-bind-hydrate") {
|
|
1892
|
+
return true;
|
|
1893
|
+
}
|
|
1894
|
+
if (name.startsWith("data-bind-")) {
|
|
1895
|
+
holder.addBinding(this.bindings.createBinding(el, "attr", value, name.slice(10), params));
|
|
1896
|
+
return true;
|
|
1897
|
+
}
|
|
1898
|
+
if (name.startsWith("data-on-")) {
|
|
1899
|
+
this.bindEvent(el, name.slice(8), value, context);
|
|
1900
|
+
return true;
|
|
1901
|
+
}
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
bindEvent(el, eventType, source, context) {
|
|
1905
|
+
const expression = source.trim();
|
|
1906
|
+
const { owner, holder, params, locals } = context;
|
|
1907
|
+
owner.listeners++;
|
|
1908
|
+
if (/^[A-Za-z_$][\w$]*$/.test(expression)) {
|
|
1909
|
+
this.events.bind(el, eventType, expression, owner, holder);
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
const handlerNames = owner.handlerNames.filter((name) => !params.includes(name));
|
|
1913
|
+
const compiled = this.bindings.compileExpression(expression, [
|
|
1914
|
+
...params,
|
|
1915
|
+
...handlerNames,
|
|
1916
|
+
"$event"
|
|
1917
|
+
]);
|
|
1918
|
+
this.events.bindExpression(el, eventType, (event) => {
|
|
1919
|
+
compiled(owner.state, owner.props, this.bindings.readProxy(), this.bindings.pendingProbe(), ...locals, ...handlerNames.map((name) => owner.getHandler(name)), event);
|
|
1920
|
+
}, holder, `${eventType} expression "${expression}"`);
|
|
1921
|
+
}
|
|
1922
|
+
async mountMarker(range, owner, context) {
|
|
1923
|
+
if (range.payload.dyn && !range.payload.c) {
|
|
1924
|
+
const id = this.resolveDynamicId(range.payload.dyn, context);
|
|
1925
|
+
if (!id) {
|
|
1926
|
+
warn(`<Dynamic $is="${range.payload.dyn}"> resolved to nothing, so no component was mounted.`);
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
range.payload.c = id;
|
|
1930
|
+
}
|
|
1931
|
+
const { needs } = range.payload;
|
|
1932
|
+
const trigger = parseTrigger(range.payload.hydrate ?? null);
|
|
1933
|
+
if (this.ssg) {
|
|
1934
|
+
const waiting = needs && this.bindings.cellManager.pendingKeys(needs).length > 0;
|
|
1935
|
+
if (needs && (waiting || needs.length === 0)) {
|
|
1936
|
+
this.renderSkeleton(range);
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
return this.completeMount(range, owner, context);
|
|
1940
|
+
}
|
|
1941
|
+
if (trigger === "never")
|
|
1942
|
+
return null;
|
|
1943
|
+
if (needs || trigger !== "load" && trigger !== "always") {
|
|
1944
|
+
this.deferMarker(range, owner, context, trigger, needs);
|
|
1945
|
+
return null;
|
|
1946
|
+
}
|
|
1947
|
+
return this.completeMount(range, owner, context);
|
|
1948
|
+
}
|
|
1949
|
+
deferMarker(range, owner, context, trigger, needs) {
|
|
1950
|
+
const held = document.createDocumentFragment();
|
|
1951
|
+
range.slotNodes.forEach((node) => held.appendChild(node));
|
|
1952
|
+
const rescued = this.takeRescued(range, context);
|
|
1953
|
+
if (rescued)
|
|
1954
|
+
this.fillRange(range, rescued);
|
|
1955
|
+
else if (!range.payload.ssr && !range.adopted)
|
|
1956
|
+
this.renderSkeleton(range);
|
|
1957
|
+
let triggered = false;
|
|
1958
|
+
let ready = !needs;
|
|
1959
|
+
const attempt = () => {
|
|
1960
|
+
if (!triggered || !ready)
|
|
1961
|
+
return;
|
|
1962
|
+
this.completeMount(range, owner, context);
|
|
1963
|
+
};
|
|
1964
|
+
const cancelTrigger = scheduleHydration(trigger, triggerTarget(range), () => {
|
|
1965
|
+
triggered = true;
|
|
1966
|
+
attempt();
|
|
1967
|
+
});
|
|
1968
|
+
context.holder.addCleanup(cancelTrigger);
|
|
1969
|
+
if (needs) {
|
|
1970
|
+
const cancelReady = this.bindings.cellManager.whenReady(needs, () => {
|
|
1971
|
+
ready = true;
|
|
1972
|
+
attempt();
|
|
1973
|
+
});
|
|
1974
|
+
context.holder.addCleanup(cancelReady);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
resolveDynamicId(expr, context) {
|
|
1978
|
+
const scope = new BindingScope(context.owner, context.locals, context.holder);
|
|
1979
|
+
try {
|
|
1980
|
+
const value = this.bindings.compileExpression(expr, context.params)(scope.state, scope.props, this.bindings.readProxy(), this.bindings.pendingProbe(), ...scope.locals);
|
|
1981
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
warn(`Error evaluating <Dynamic $is="${expr}">:`, error);
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
renderSkeleton(range) {
|
|
1988
|
+
const definition = this.registry.get(range.payload.c);
|
|
1989
|
+
const fallback = definition ? null : this.metadata(range.payload.c);
|
|
1990
|
+
const skeleton = definition?.skeleton ?? fallback?.skeleton ?? "";
|
|
1991
|
+
const scopeId = definition?.scopeId ?? fallback?.scope;
|
|
1992
|
+
const template = document.createElement("template");
|
|
1993
|
+
template.innerHTML = skeleton;
|
|
1994
|
+
if (scopeId) {
|
|
1995
|
+
Array.from(template.content.querySelectorAll("*")).forEach((node) => addScope(node, scopeId));
|
|
1996
|
+
}
|
|
1997
|
+
this.fillRange(range, Array.from(template.content.childNodes));
|
|
1998
|
+
}
|
|
1999
|
+
async completeMount(range, owner, context) {
|
|
2000
|
+
const { payload } = range;
|
|
2001
|
+
const props = { ...payload.props ?? {} };
|
|
2002
|
+
const scope = new BindingScope(owner, context.locals, context.holder);
|
|
2003
|
+
const propBindings = Object.entries(payload.bind ?? {}).filter(([name]) => name !== "class" && name !== "style");
|
|
2004
|
+
for (const [name, expr] of propBindings) {
|
|
2005
|
+
props[name] = this.bindings.compileExpression(expr, context.params)(scope.state, scope.props, this.bindings.readProxy(), this.bindings.pendingProbe(), ...scope.locals);
|
|
2006
|
+
}
|
|
2007
|
+
const cellProps = Object.entries(payload.cells ?? {});
|
|
2008
|
+
for (const [name, key] of cellProps) {
|
|
2009
|
+
const cell = this.bindings.cellManager.get(key);
|
|
2010
|
+
if (!cell.pending)
|
|
2011
|
+
props[name] = cell.value;
|
|
2012
|
+
}
|
|
2013
|
+
const holdsSkeleton = Boolean(payload.needs) && !payload.ssr;
|
|
2014
|
+
const child = await this.mount(payload.c, owner, props, range.slotNodes, undefined, (payload.ssr || range.adopted) && !holdsSkeleton ? nodesBetween(range.start, range.end) : undefined, payload.ssr ? () => this.rescueRendered(range) : undefined);
|
|
2015
|
+
if (!child)
|
|
2016
|
+
return null;
|
|
2017
|
+
for (const [name, key] of cellProps) {
|
|
2018
|
+
this.bindings.bindCellProp(child, name, key);
|
|
2019
|
+
}
|
|
2020
|
+
for (const [name, expr] of propBindings) {
|
|
2021
|
+
const binding = this.bindings.createPropBinding(child, name, expr, context.params);
|
|
2022
|
+
context.holder.addBinding(binding);
|
|
2023
|
+
this.bindings.applyBinding(binding, scope);
|
|
2024
|
+
}
|
|
2025
|
+
const rootElement = child.rootNodes.find((node) => node.nodeType === 1);
|
|
2026
|
+
if (rootElement)
|
|
2027
|
+
this.applyForwarded(rootElement, payload, context);
|
|
2028
|
+
this.fillRange(range, child.rootNodes);
|
|
2029
|
+
this.bindings.updateTree(child);
|
|
2030
|
+
return child;
|
|
2031
|
+
}
|
|
2032
|
+
findIslands(scope) {
|
|
2033
|
+
const comments = [];
|
|
2034
|
+
const walker = document.createTreeWalker(scope, 128);
|
|
2035
|
+
while (walker.nextNode())
|
|
2036
|
+
comments.push(walker.currentNode);
|
|
2037
|
+
const islands = [];
|
|
2038
|
+
let depth = 0;
|
|
2039
|
+
let openIndex = -1;
|
|
2040
|
+
for (let i = 0;i < comments.length; i++) {
|
|
2041
|
+
if (isMarkerStart(comments[i])) {
|
|
2042
|
+
if (depth === 0)
|
|
2043
|
+
openIndex = i;
|
|
2044
|
+
depth++;
|
|
2045
|
+
} else if (isMarkerEnd(comments[i])) {
|
|
2046
|
+
depth--;
|
|
2047
|
+
if (depth === 0 && openIndex >= 0) {
|
|
2048
|
+
const payload = parseMarker(comments[openIndex]);
|
|
2049
|
+
if (payload) {
|
|
2050
|
+
islands.push({
|
|
2051
|
+
start: comments[openIndex],
|
|
2052
|
+
end: comments[i],
|
|
2053
|
+
payload,
|
|
2054
|
+
slotNodes: slotNodesFor(payload, nodesBetween(comments[openIndex], comments[i]))
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
openIndex = -1;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return islands;
|
|
2062
|
+
}
|
|
2063
|
+
async mountRange(range, props, slotNodes) {
|
|
2064
|
+
const child = await this.mount(range.payload.c, null, props, slotNodes, undefined, range.payload.ssr ? nodesBetween(range.start, range.end) : undefined, range.payload.ssr ? () => this.rescueRendered(range) : undefined);
|
|
2065
|
+
if (!child)
|
|
2066
|
+
return null;
|
|
2067
|
+
const root = child.rootNodes.find((node) => node.nodeType === 1);
|
|
2068
|
+
if (root) {
|
|
2069
|
+
for (const [name, value] of Object.entries(range.payload.attrs ?? {})) {
|
|
2070
|
+
if (name === "class") {
|
|
2071
|
+
const existing = root.getAttribute("class") ?? "";
|
|
2072
|
+
const merged = new Set(`${existing} ${value}`.split(/\s+/).filter(Boolean));
|
|
2073
|
+
root.setAttribute("class", [...merged].join(" "));
|
|
2074
|
+
} else {
|
|
2075
|
+
root.setAttribute(name, value);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
this.fillRange(range, child.rootNodes);
|
|
2080
|
+
this.bindings.updateTree(child);
|
|
2081
|
+
return child;
|
|
2082
|
+
}
|
|
2083
|
+
applyForwarded(root, payload, context) {
|
|
2084
|
+
for (const [name, value] of Object.entries(payload.attrs ?? {})) {
|
|
2085
|
+
if (name === "class") {
|
|
2086
|
+
const existing = root.getAttribute("class") ?? "";
|
|
2087
|
+
const merged = new Set(`${existing} ${value}`.split(/\s+/).filter(Boolean));
|
|
2088
|
+
root.setAttribute("class", [...merged].join(" "));
|
|
2089
|
+
} else if (name === "style") {
|
|
2090
|
+
const existing = root.getAttribute("style");
|
|
2091
|
+
root.setAttribute("style", existing ? `${existing};${value}` : value);
|
|
2092
|
+
} else {
|
|
2093
|
+
root.setAttribute(name, value);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
for (const [event, handler] of Object.entries(payload.on ?? {})) {
|
|
2097
|
+
this.bindEvent(root, event, handler, context);
|
|
2098
|
+
}
|
|
2099
|
+
for (const [name, expr] of Object.entries(payload.bind ?? {})) {
|
|
2100
|
+
if (name !== "class" && name !== "style")
|
|
2101
|
+
continue;
|
|
2102
|
+
this.applyDirective(root, `data-bind-${name}`, expr, context);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
function addScope(el, scopeId) {
|
|
2107
|
+
if (!scopeId)
|
|
2108
|
+
return;
|
|
2109
|
+
const existing = el.getAttribute("data-lsp");
|
|
2110
|
+
if (!existing) {
|
|
2111
|
+
el.setAttribute("data-lsp", scopeId);
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (!existing.split(/\s+/).includes(scopeId)) {
|
|
2115
|
+
el.setAttribute("data-lsp", `${existing} ${scopeId}`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function parseForExpression(expression) {
|
|
2119
|
+
const match = expression.match(/^\s*(.+?)\s+(?:in|of)\s+(.+?)\s*$/);
|
|
2120
|
+
if (!match)
|
|
2121
|
+
return null;
|
|
2122
|
+
const alias = match[1].trim();
|
|
2123
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(alias)) {
|
|
2124
|
+
warn(`$for alias must be a plain identifier, got "${alias}"`);
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
return { alias, items: match[2].trim() };
|
|
2128
|
+
}
|
|
2129
|
+
function composeBranchExpression(branches) {
|
|
2130
|
+
const elseIndex = branches.findIndex((branch) => branch.condition === null);
|
|
2131
|
+
let expression = elseIndex >= 0 ? String(elseIndex) : "-1";
|
|
2132
|
+
for (let i = branches.length - 1;i >= 0; i--) {
|
|
2133
|
+
const { condition } = branches[i];
|
|
2134
|
+
if (condition === null)
|
|
2135
|
+
continue;
|
|
2136
|
+
expression = `((${condition}) ? ${i} : ${expression})`;
|
|
2137
|
+
}
|
|
2138
|
+
return expression;
|
|
2139
|
+
}
|
|
2140
|
+
function liveSlotNodes(payload, start, end, collected) {
|
|
2141
|
+
if (payload.ssr)
|
|
2142
|
+
return collected;
|
|
2143
|
+
return nodesBetween(start, end);
|
|
2144
|
+
}
|
|
2145
|
+
function groupSlotFills(fills) {
|
|
2146
|
+
const byName = new Map;
|
|
2147
|
+
for (const node of fills) {
|
|
2148
|
+
const name = node.nodeType === 1 ? node.getAttribute("slot") ?? "" : "";
|
|
2149
|
+
const group = byName.get(name) ?? [];
|
|
2150
|
+
group.push(...isSlotTemplate(node) ? Array.from(node.content.childNodes) : [node]);
|
|
2151
|
+
byName.set(name, group);
|
|
2152
|
+
}
|
|
2153
|
+
return byName;
|
|
2154
|
+
}
|
|
2155
|
+
function isSlotTemplate(node) {
|
|
2156
|
+
return node.nodeType === 1 && node.tagName === "TEMPLATE" && node.hasAttribute("slot");
|
|
2157
|
+
}
|
|
2158
|
+
function expandSlotFills(slotNodes) {
|
|
2159
|
+
const placed = [];
|
|
2160
|
+
for (const node of slotNodes) {
|
|
2161
|
+
if (isSlotTemplate(node)) {
|
|
2162
|
+
placed.push(...Array.from(node.content.childNodes));
|
|
2163
|
+
} else {
|
|
2164
|
+
placed.push(node);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return placed;
|
|
2168
|
+
}
|
|
2169
|
+
function nodesBetween(start, end) {
|
|
2170
|
+
const nodes = [];
|
|
2171
|
+
let current = start.nextSibling;
|
|
2172
|
+
while (current && current !== end) {
|
|
2173
|
+
nodes.push(current);
|
|
2174
|
+
current = current.nextSibling;
|
|
2175
|
+
}
|
|
2176
|
+
return nodes;
|
|
2177
|
+
}
|
|
2178
|
+
function sliceRows(found, from, anchorIndex) {
|
|
2179
|
+
const slices = [];
|
|
2180
|
+
let current = null;
|
|
2181
|
+
for (let i = from;i < anchorIndex; i++) {
|
|
2182
|
+
const node = found[i];
|
|
2183
|
+
if (isRowMark(node)) {
|
|
2184
|
+
current = [node];
|
|
2185
|
+
slices.push(current);
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
if (!current)
|
|
2189
|
+
return null;
|
|
2190
|
+
current.push(node);
|
|
2191
|
+
}
|
|
2192
|
+
return slices;
|
|
2193
|
+
}
|
|
2194
|
+
var SLOT_MARK = "s:";
|
|
2195
|
+
var SLOT_END = "/s";
|
|
2196
|
+
function isSlotStart(node) {
|
|
2197
|
+
return node.nodeType === 8 && node.data.startsWith(SLOT_MARK);
|
|
2198
|
+
}
|
|
2199
|
+
function isSlotEnd(node) {
|
|
2200
|
+
return node.nodeType === 8 && node.data === SLOT_END;
|
|
2201
|
+
}
|
|
2202
|
+
function findSlotRegions(nodes, into = new Map) {
|
|
2203
|
+
for (const node of nodes) {
|
|
2204
|
+
if (isMarkerStart(node))
|
|
2205
|
+
continue;
|
|
2206
|
+
if (isSlotStart(node)) {
|
|
2207
|
+
const name = node.data.slice(SLOT_MARK.length);
|
|
2208
|
+
const region = [];
|
|
2209
|
+
let current = node.nextSibling;
|
|
2210
|
+
while (current && !isSlotEnd(current)) {
|
|
2211
|
+
region.push(current);
|
|
2212
|
+
current = current.nextSibling;
|
|
2213
|
+
}
|
|
2214
|
+
into.set(name, region);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
if (node.childNodes?.length) {
|
|
2218
|
+
findSlotRegions(Array.from(node.childNodes), into);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return into;
|
|
2222
|
+
}
|
|
2223
|
+
var ROW_MARK = "|";
|
|
2224
|
+
function isRowMark(node) {
|
|
2225
|
+
return node.nodeType === 8 && node.data === ROW_MARK;
|
|
2226
|
+
}
|
|
2227
|
+
function isBlockAnchor(node) {
|
|
2228
|
+
if (node.nodeType !== 8)
|
|
2229
|
+
return false;
|
|
2230
|
+
const data = node.data.trim();
|
|
2231
|
+
return data.startsWith("for:") || data.startsWith("if:");
|
|
2232
|
+
}
|
|
2233
|
+
function isInsignificant(node) {
|
|
2234
|
+
return node.nodeType === 3 && !(node.textContent ?? "").trim();
|
|
2235
|
+
}
|
|
2236
|
+
function significant(nodes) {
|
|
2237
|
+
return nodes.filter((node) => !isInsignificant(node));
|
|
2238
|
+
}
|
|
2239
|
+
function joinStyle(el, declaration) {
|
|
2240
|
+
const existing = el.getAttribute("style")?.trim() ?? "";
|
|
2241
|
+
if (!existing)
|
|
2242
|
+
return declaration;
|
|
2243
|
+
return existing.endsWith(";") ? `${existing} ${declaration}` : `${existing}; ${declaration}`;
|
|
2244
|
+
}
|
|
2245
|
+
function triggerTarget(range) {
|
|
2246
|
+
const first = range.start.nextSibling;
|
|
2247
|
+
return first && first.nodeType === 1 ? first : range.start.parentNode;
|
|
2248
|
+
}
|
|
2249
|
+
// src/manifest.ts
|
|
2250
|
+
var HIDDEN_RULE = "[data-lsp-hidden]{display:none!important}";
|
|
2251
|
+
|
|
2252
|
+
// src/runtime/dom/StyleManager.ts
|
|
2253
|
+
var PENDING_STYLES = `
|
|
2254
|
+
:where([data-lsp-pending][data-lsp-shimmer]) {
|
|
2255
|
+
display: inline-block;
|
|
2256
|
+
min-width: var(--lsp-pending-width, 4ch);
|
|
2257
|
+
border-radius: 0.25em;
|
|
2258
|
+
background-image: linear-gradient(90deg, #f0f0f0 25%, rgba(0,0,0,0) 37%, #f0f0f0 63%);
|
|
2259
|
+
background-size: 400% 100%;
|
|
2260
|
+
animation: lsp-pending 1.4s ease infinite;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
@keyframes lsp-pending {
|
|
2264
|
+
0% { background-position: 100% 50%; }
|
|
2265
|
+
100% { background-position: 0% 50%; }
|
|
2266
|
+
}
|
|
2267
|
+
`;
|
|
2268
|
+
|
|
2269
|
+
class StyleManager {
|
|
2270
|
+
ssg;
|
|
2271
|
+
componentRegistry;
|
|
2272
|
+
styleElement = null;
|
|
2273
|
+
sheet = null;
|
|
2274
|
+
adopted = [];
|
|
2275
|
+
pendingStylesUsed = false;
|
|
2276
|
+
covered = new Set;
|
|
2277
|
+
deferredCovered = new Set;
|
|
2278
|
+
constructor(componentRegistry, ssg = false) {
|
|
2279
|
+
this.ssg = ssg;
|
|
2280
|
+
this.componentRegistry = componentRegistry;
|
|
2281
|
+
}
|
|
2282
|
+
ensureElement() {
|
|
2283
|
+
if (this.styleElement)
|
|
2284
|
+
return this.styleElement;
|
|
2285
|
+
this.styleElement = document.getElementById("lightspeed-styles");
|
|
2286
|
+
if (!this.styleElement) {
|
|
2287
|
+
this.styleElement = document.createElement("style");
|
|
2288
|
+
this.styleElement.id = "lightspeed-styles";
|
|
2289
|
+
this.styleElement.textContent = HIDDEN_RULE;
|
|
2290
|
+
document.head.appendChild(this.styleElement);
|
|
2291
|
+
}
|
|
2292
|
+
seed(this.covered, this.styleElement.getAttribute("data-lsp-covers"));
|
|
2293
|
+
seed(this.deferredCovered, document.getElementById("lightspeed-styles-deferred")?.getAttribute("data-lsp-covers") ?? null);
|
|
2294
|
+
return this.styleElement;
|
|
2295
|
+
}
|
|
2296
|
+
updateStyles() {
|
|
2297
|
+
const element = this.ensureElement();
|
|
2298
|
+
const additions = [];
|
|
2299
|
+
if (this.pendingStylesUsed && !this.covered.has("@pending")) {
|
|
2300
|
+
this.covered.add("@pending");
|
|
2301
|
+
additions.push(PENDING_STYLES);
|
|
2302
|
+
}
|
|
2303
|
+
for (const component of this.componentRegistry.getAll()) {
|
|
2304
|
+
if (component.style && !this.covered.has(component.id)) {
|
|
2305
|
+
this.covered.add(component.id);
|
|
2306
|
+
additions.push(component.style);
|
|
2307
|
+
}
|
|
2308
|
+
if (!this.ssg && component.styleDeferred && !this.deferredCovered.has(component.id)) {
|
|
2309
|
+
this.deferredCovered.add(component.id);
|
|
2310
|
+
additions.push(component.styleDeferred);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (!additions.length)
|
|
2314
|
+
return;
|
|
2315
|
+
this.emit(element, additions);
|
|
2316
|
+
element.setAttribute("data-lsp-covers", [...this.covered].join(","));
|
|
2317
|
+
debug("styles updated");
|
|
2318
|
+
}
|
|
2319
|
+
ensurePendingStyles() {
|
|
2320
|
+
if (this.pendingStylesUsed)
|
|
2321
|
+
return;
|
|
2322
|
+
this.pendingStylesUsed = true;
|
|
2323
|
+
this.updateStyles();
|
|
2324
|
+
}
|
|
2325
|
+
coveredComponents() {
|
|
2326
|
+
return [...this.covered].filter((id) => id !== "@pending");
|
|
2327
|
+
}
|
|
2328
|
+
removeStyles() {
|
|
2329
|
+
if (this.styleElement) {
|
|
2330
|
+
this.styleElement.remove();
|
|
2331
|
+
this.styleElement = null;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
addComponentStyle(componentId, style) {
|
|
2335
|
+
this.updateStyles();
|
|
2336
|
+
}
|
|
2337
|
+
removeComponentStyle(componentId) {
|
|
2338
|
+
this.updateStyles();
|
|
2339
|
+
}
|
|
2340
|
+
emit(element, additions) {
|
|
2341
|
+
if (this.ssg || !this.adopts()) {
|
|
2342
|
+
element.textContent = [element.textContent ?? "", ...additions].filter(Boolean).join(`
|
|
2343
|
+
|
|
2344
|
+
`);
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
if (!this.sheet) {
|
|
2348
|
+
this.sheet = new (this.sheetConstructor());
|
|
2349
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, this.sheet];
|
|
2350
|
+
}
|
|
2351
|
+
this.adopted.push(...additions);
|
|
2352
|
+
this.sheet.replaceSync(this.adopted.join(`
|
|
2353
|
+
|
|
2354
|
+
`));
|
|
2355
|
+
}
|
|
2356
|
+
sheetConstructor() {
|
|
2357
|
+
const view = document?.defaultView;
|
|
2358
|
+
return view?.CSSStyleSheet ?? globalThis.CSSStyleSheet;
|
|
2359
|
+
}
|
|
2360
|
+
adopts() {
|
|
2361
|
+
return typeof document !== "undefined" && "adoptedStyleSheets" in document && typeof this.sheetConstructor() === "function";
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
function seed(target, list) {
|
|
2365
|
+
if (!list)
|
|
2366
|
+
return;
|
|
2367
|
+
list.split(",").map((id) => id.trim()).filter(Boolean).forEach((id) => target.add(id));
|
|
2368
|
+
}
|
|
2369
|
+
// src/parser/route.ts
|
|
2370
|
+
function parseRoute(pattern) {
|
|
2371
|
+
if (!pattern.startsWith("/")) {
|
|
2372
|
+
throw new Error(`[lightspeed] Route pattern "${pattern}" must start with "/".`);
|
|
2373
|
+
}
|
|
2374
|
+
const segments = [];
|
|
2375
|
+
const params = [];
|
|
2376
|
+
let fixedDepth = true;
|
|
2377
|
+
for (const raw of pattern.split("/").filter(Boolean)) {
|
|
2378
|
+
if (raw === "*") {
|
|
2379
|
+
fixedDepth = false;
|
|
2380
|
+
segments.push({ value: "*", param: true });
|
|
2381
|
+
params.push("*");
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
if (raw.startsWith(":")) {
|
|
2385
|
+
const name = raw.slice(1);
|
|
2386
|
+
if (!name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
2387
|
+
throw new Error(`[lightspeed] Route pattern "${pattern}" has an invalid parameter "${raw}".`);
|
|
2388
|
+
}
|
|
2389
|
+
if (params.includes(name)) {
|
|
2390
|
+
throw new Error(`[lightspeed] Route pattern "${pattern}" declares ":${name}" twice.`);
|
|
2391
|
+
}
|
|
2392
|
+
segments.push({ value: name, param: true });
|
|
2393
|
+
params.push(name);
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
segments.push({ value: raw, param: false });
|
|
2397
|
+
}
|
|
2398
|
+
if (!params.length) {
|
|
2399
|
+
throw new Error(`[lightspeed] Route pattern "${pattern}" has no parameters. A page with ` + `a fixed URL does not need one - its filename already decides its path.`);
|
|
2400
|
+
}
|
|
2401
|
+
return { pattern, segments, params, fixedDepth };
|
|
2402
|
+
}
|
|
2403
|
+
function matchPath(route, pathname) {
|
|
2404
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
2405
|
+
const params = {};
|
|
2406
|
+
for (let i = 0;i < route.segments.length; i++) {
|
|
2407
|
+
const segment = route.segments[i];
|
|
2408
|
+
if (segment.value === "*" && segment.param) {
|
|
2409
|
+
const rest = parts.slice(i);
|
|
2410
|
+
if (!rest.length)
|
|
2411
|
+
return null;
|
|
2412
|
+
params["*"] = rest.map(decodeURIComponent).join("/");
|
|
2413
|
+
return params;
|
|
2414
|
+
}
|
|
2415
|
+
const part = parts[i];
|
|
2416
|
+
if (part === undefined)
|
|
2417
|
+
return null;
|
|
2418
|
+
if (segment.param) {
|
|
2419
|
+
params[segment.value] = decodeURIComponent(part);
|
|
2420
|
+
} else if (part !== segment.value) {
|
|
2421
|
+
return null;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
if (parts.length !== route.segments.length)
|
|
2425
|
+
return null;
|
|
2426
|
+
return params;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/runtime/core.ts
|
|
2430
|
+
var PAGE_SCOPE = {
|
|
2431
|
+
id: "page",
|
|
2432
|
+
state: {},
|
|
2433
|
+
props: {},
|
|
2434
|
+
addCleanup: () => {
|
|
2435
|
+
},
|
|
2436
|
+
addBinding: () => {
|
|
2437
|
+
},
|
|
2438
|
+
addBlock: () => {
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
var pendingPageExpressions = [];
|
|
2442
|
+
function registerPageExpressions(expressions = {}, writers = {}) {
|
|
2443
|
+
pendingPageExpressions.push({ expressions, writers });
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
class LightspeedRuntime {
|
|
2447
|
+
componentRegistry = new ComponentRegistry;
|
|
2448
|
+
instances = new Map;
|
|
2449
|
+
globalSignals = new SignalManager;
|
|
2450
|
+
cells;
|
|
2451
|
+
initialised = false;
|
|
2452
|
+
instanceCounter = 0;
|
|
2453
|
+
loaders = new Map;
|
|
2454
|
+
loading = new Map;
|
|
2455
|
+
pageMetadata = {};
|
|
2456
|
+
roots = new Map;
|
|
2457
|
+
rootCounter = 0;
|
|
2458
|
+
ssg = false;
|
|
2459
|
+
pendingIslands = [];
|
|
2460
|
+
reactiveProxy;
|
|
2461
|
+
updateScheduler;
|
|
2462
|
+
bindingEvaluator;
|
|
2463
|
+
eventBinder;
|
|
2464
|
+
globalFunctions;
|
|
2465
|
+
pendingMounts = [];
|
|
2466
|
+
flushing = false;
|
|
2467
|
+
mounter;
|
|
2468
|
+
styleManager;
|
|
2469
|
+
constructor(options = {}) {
|
|
2470
|
+
this.cells = new CellManager(options.ssg ?? false);
|
|
2471
|
+
this.updateScheduler = new UpdateScheduler((owners) => this.handleUpdate(owners));
|
|
2472
|
+
this.reactiveProxy = new ReactiveProxy((owner) => {
|
|
2473
|
+
if (owner && this.updating.has(owner))
|
|
2474
|
+
return;
|
|
2475
|
+
this.updateScheduler.queueUpdate(owner);
|
|
2476
|
+
});
|
|
2477
|
+
this.bindingEvaluator = new BindingEvaluator(this.cells, (instance) => {
|
|
2478
|
+
this.shareObjectProps(instance);
|
|
2479
|
+
this.updateScheduler.queueUpdate(instance);
|
|
2480
|
+
});
|
|
2481
|
+
this.eventBinder = new EventBinder;
|
|
2482
|
+
this.globalFunctions = new GlobalFunctions(this.reactiveProxy, this.globalSignals);
|
|
2483
|
+
this.styleManager = new StyleManager(this.componentRegistry, options.ssg ?? false);
|
|
2484
|
+
this.mounter = new Mounter(this.componentRegistry, this.bindingEvaluator, this.eventBinder, (componentId, parent, props) => this.createInstance(componentId, parent, props), (instance) => this.unregisterSubtree(instance), (componentId) => this.resolveComponent(componentId), (componentId) => this.pageMetadata[componentId] ?? null, options.ssg ?? false, this.styleManager);
|
|
2485
|
+
this.mounter.onConnected = () => this.flushMounted();
|
|
2486
|
+
this.ssg = options.ssg ?? false;
|
|
2487
|
+
this.readPageMetadata();
|
|
2488
|
+
for (const table of pendingPageExpressions) {
|
|
2489
|
+
this.bindingEvaluator.registerExpressions(table.expressions, table.writers);
|
|
2490
|
+
}
|
|
2491
|
+
this.readPageState();
|
|
2492
|
+
this.readPageRoute();
|
|
2493
|
+
this.globalFunctions.setup();
|
|
2494
|
+
}
|
|
2495
|
+
updating = new Set;
|
|
2496
|
+
runUpdateCallbacks(instance) {
|
|
2497
|
+
if (!instance.updateCallbacks.length)
|
|
2498
|
+
return;
|
|
2499
|
+
if (this.updating.has(instance))
|
|
2500
|
+
return;
|
|
2501
|
+
this.updating.add(instance);
|
|
2502
|
+
try {
|
|
2503
|
+
for (const callback of instance.updateCallbacks) {
|
|
2504
|
+
try {
|
|
2505
|
+
callback();
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
warn(`onUpdate failed in ${instance.id}: ${String(error)}`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
} finally {
|
|
2511
|
+
this.updating.delete(instance);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
handleUpdate(owners) {
|
|
2515
|
+
for (const owner of owners) {
|
|
2516
|
+
if (owner) {
|
|
2517
|
+
this.runUpdateCallbacks(owner);
|
|
2518
|
+
this.bindingEvaluator.updateInstance(owner);
|
|
2519
|
+
} else {
|
|
2520
|
+
this.roots.forEach((root) => this.bindingEvaluator.updateTree(root.instance));
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
registerComponent(component) {
|
|
2525
|
+
this.bindingEvaluator.registerExpressions(component.expressions, component.writers);
|
|
2526
|
+
this.componentRegistry.register(component);
|
|
2527
|
+
}
|
|
2528
|
+
readPageMetadata() {
|
|
2529
|
+
const element = typeof document !== "undefined" ? document.getElementById("lightspeed-manifest") : null;
|
|
2530
|
+
if (!element?.textContent)
|
|
2531
|
+
return;
|
|
2532
|
+
try {
|
|
2533
|
+
this.pageMetadata = JSON.parse(element.textContent);
|
|
2534
|
+
} catch {
|
|
2535
|
+
warn("Malformed inlined component metadata");
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
readPageRoute() {
|
|
2539
|
+
if (this.ssg || typeof document === "undefined")
|
|
2540
|
+
return;
|
|
2541
|
+
const element = document.getElementById("lightspeed-route");
|
|
2542
|
+
if (!element?.textContent)
|
|
2543
|
+
return;
|
|
2544
|
+
try {
|
|
2545
|
+
const { pattern } = JSON.parse(element.textContent);
|
|
2546
|
+
const path = document.location.pathname;
|
|
2547
|
+
const params = matchPath(parseRoute(pattern), path);
|
|
2548
|
+
if (!params) {
|
|
2549
|
+
warn(`Route "${pattern}" does not match ${path} - the ` + `server rewrote a URL onto this page that the page does not claim.`);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
this.cells.seed({
|
|
2553
|
+
route: { path, pattern, params }
|
|
2554
|
+
});
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
warn(`Malformed route declaration: ${error.message}`);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
readPageState() {
|
|
2560
|
+
const element = typeof document !== "undefined" ? document.getElementById("lightspeed-state") : null;
|
|
2561
|
+
if (!element?.textContent)
|
|
2562
|
+
return;
|
|
2563
|
+
try {
|
|
2564
|
+
this.cells.seed(JSON.parse(element.textContent));
|
|
2565
|
+
} catch {
|
|
2566
|
+
warn("Malformed build-time state payload");
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
useComponents(loaders, page) {
|
|
2570
|
+
if (page) {
|
|
2571
|
+
this.bindingEvaluator.registerExpressions(page.expressions, page.writers);
|
|
2572
|
+
}
|
|
2573
|
+
for (const [id, loader] of Object.entries(loaders)) {
|
|
2574
|
+
this.loaders.set(id, loader);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
async resolveComponent(componentId) {
|
|
2578
|
+
const registered = this.componentRegistry.get(componentId);
|
|
2579
|
+
if (registered)
|
|
2580
|
+
return registered;
|
|
2581
|
+
const inFlight = this.loading.get(componentId);
|
|
2582
|
+
if (inFlight)
|
|
2583
|
+
return inFlight;
|
|
2584
|
+
const loader = this.loaders.get(componentId);
|
|
2585
|
+
if (!loader) {
|
|
2586
|
+
warn(`Component not found: ${componentId}`);
|
|
2587
|
+
return null;
|
|
2588
|
+
}
|
|
2589
|
+
const request = loader().then((module) => {
|
|
2590
|
+
const definition = module?.default ?? module;
|
|
2591
|
+
if (!definition?.id) {
|
|
2592
|
+
warn(`Component module for "${componentId}" has no definition`);
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
this.registerComponent(definition);
|
|
2596
|
+
this.styleManager.updateStyles();
|
|
2597
|
+
return definition;
|
|
2598
|
+
}).catch((error) => {
|
|
2599
|
+
fail(`Failed to load component "${componentId}":`, error);
|
|
2600
|
+
return null;
|
|
2601
|
+
}).finally(() => this.loading.delete(componentId));
|
|
2602
|
+
this.loading.set(componentId, request);
|
|
2603
|
+
return request;
|
|
2604
|
+
}
|
|
2605
|
+
updateComponent(id, component) {
|
|
2606
|
+
this.componentRegistry.update(id, component);
|
|
2607
|
+
if (this.initialised) {
|
|
2608
|
+
this.remountAll();
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
shareObjectProps(instance) {
|
|
2612
|
+
const props = instance.props;
|
|
2613
|
+
if (!props)
|
|
2614
|
+
return;
|
|
2615
|
+
for (const key of Object.keys(props)) {
|
|
2616
|
+
this.reactiveProxy.share(props[key], instance);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
async createInstance(componentId, parent, props) {
|
|
2620
|
+
const component = await this.resolveComponent(componentId);
|
|
2621
|
+
if (!component)
|
|
2622
|
+
return null;
|
|
2623
|
+
const instanceId = `${componentId}#${++this.instanceCounter}`;
|
|
2624
|
+
const instance = new ComponentInstance(instanceId, componentId, this.globalSignals, parent);
|
|
2625
|
+
instance.props = props ?? {};
|
|
2626
|
+
this.shareObjectProps(instance);
|
|
2627
|
+
instance.scopeId = component.scopeId;
|
|
2628
|
+
instance.handlerNames = component.handlers ?? [];
|
|
2629
|
+
if (component.setup) {
|
|
2630
|
+
try {
|
|
2631
|
+
component.setup(this.createExecutionContext(instance));
|
|
2632
|
+
debug(`mounted ${instance.id}`);
|
|
2633
|
+
} catch (error) {
|
|
2634
|
+
fail(`Error executing logic for ${instance.id}:`, error);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
this.instances.set(instanceId, instance);
|
|
2638
|
+
return instance;
|
|
2639
|
+
}
|
|
2640
|
+
createExecutionContext(instance) {
|
|
2641
|
+
const runtimeHelpers = {
|
|
2642
|
+
reactive: (target) => this.reactiveProxy.create(target, instance),
|
|
2643
|
+
defineSignal: () => createScopedSignalAPI(this.globalSignals, (cleanup) => instance.addLogicCleanup(cleanup)),
|
|
2644
|
+
shared: (key, init, options) => {
|
|
2645
|
+
if (options?.fetchAt !== "build" || options?.refresh) {
|
|
2646
|
+
instance.runtimeCells.add(key);
|
|
2647
|
+
}
|
|
2648
|
+
return this.scopedCell(key, init, options, instance);
|
|
2649
|
+
},
|
|
2650
|
+
defineRefs: () => instance.refs,
|
|
2651
|
+
onMounted: (callback) => {
|
|
2652
|
+
if (this.ssg) {
|
|
2653
|
+
instance.wantsMount = true;
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
instance.mountedCallbacks.push(callback);
|
|
2657
|
+
if (!this.pendingMounts.includes(instance)) {
|
|
2658
|
+
this.pendingMounts.push(instance);
|
|
2659
|
+
}
|
|
2660
|
+
},
|
|
2661
|
+
onUnmounted: (callback) => {
|
|
2662
|
+
if (this.ssg)
|
|
2663
|
+
return;
|
|
2664
|
+
instance.addLogicCleanup(callback);
|
|
2665
|
+
},
|
|
2666
|
+
onUpdate: (callback) => {
|
|
2667
|
+
if (this.ssg)
|
|
2668
|
+
return;
|
|
2669
|
+
instance.updateCallbacks.push(callback);
|
|
2670
|
+
},
|
|
2671
|
+
defineProps: (propsDefinition) => {
|
|
2672
|
+
const props = {
|
|
2673
|
+
...this.defaultProps(propsDefinition),
|
|
2674
|
+
...instance.props
|
|
2675
|
+
};
|
|
2676
|
+
instance.props = props;
|
|
2677
|
+
return props;
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2680
|
+
return {
|
|
2681
|
+
_componentInstance: instance,
|
|
2682
|
+
__lightspeedRuntime: runtimeHelpers,
|
|
2683
|
+
_components: this.componentRegistry,
|
|
2684
|
+
_getComponent: (id) => this.componentRegistry.get(id),
|
|
2685
|
+
console,
|
|
2686
|
+
_addHandler: (name, fn) => {
|
|
2687
|
+
instance.addHandler(name, fn);
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
hasBuildData() {
|
|
2692
|
+
return this.cells.hasBuildData();
|
|
2693
|
+
}
|
|
2694
|
+
settleBuildData() {
|
|
2695
|
+
return this.cells.settleBuildData();
|
|
2696
|
+
}
|
|
2697
|
+
buildPayload() {
|
|
2698
|
+
return this.cells.buildPayload();
|
|
2699
|
+
}
|
|
2700
|
+
hydrationReport(options = {}) {
|
|
2701
|
+
const rows = [];
|
|
2702
|
+
const visit = (instance) => {
|
|
2703
|
+
instance.children.forEach(visit);
|
|
2704
|
+
for (const block of instance.blocks) {
|
|
2705
|
+
for (const row of block.rows.values()) {
|
|
2706
|
+
row.instances.forEach(visit);
|
|
2707
|
+
for (const nested of row.blocks) {
|
|
2708
|
+
for (const nestedRow of nested.rows.values()) {
|
|
2709
|
+
nestedRow.instances.forEach(visit);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
const reasons = [];
|
|
2715
|
+
const bound = new Set;
|
|
2716
|
+
const collect = (bindings) => {
|
|
2717
|
+
for (const binding of bindings) {
|
|
2718
|
+
for (const key of binding.cellKeys ?? [])
|
|
2719
|
+
bound.add(key);
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
collect(instance.bindings);
|
|
2723
|
+
for (const block of instance.blocks) {
|
|
2724
|
+
for (const row of block.rows.values()) {
|
|
2725
|
+
collect(row.bindings);
|
|
2726
|
+
for (const nested of row.blocks) {
|
|
2727
|
+
for (const nestedRow of nested.rows.values())
|
|
2728
|
+
collect(nestedRow.bindings);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
const live = [...bound].filter((key) => !this.cells.isBakedOnly(key) && (key !== "route" || options.routeIsLive));
|
|
2733
|
+
if (live.length)
|
|
2734
|
+
reasons.push(`binds ${live.join(", ")}`);
|
|
2735
|
+
if (instance.listeners)
|
|
2736
|
+
reasons.push(`${instance.listeners} event handler(s)`);
|
|
2737
|
+
if (instance.models)
|
|
2738
|
+
reasons.push(`${instance.models} two-way binding(s)`);
|
|
2739
|
+
if (instance.wantsMount)
|
|
2740
|
+
reasons.push("onMounted");
|
|
2741
|
+
if (instance.updateCallbacks.length)
|
|
2742
|
+
reasons.push("onUpdate");
|
|
2743
|
+
const cells = [...instance.runtimeCells].filter((key) => !this.cells.isBakedOnly(key) && (key !== "route" || options.routeIsLive));
|
|
2744
|
+
if (cells.length)
|
|
2745
|
+
reasons.push(`reads ${cells.join(", ")}`);
|
|
2746
|
+
const isLive = reasons.length > 0;
|
|
2747
|
+
rows.push({
|
|
2748
|
+
id: instance.id,
|
|
2749
|
+
componentId: instance.componentId,
|
|
2750
|
+
live: isLive,
|
|
2751
|
+
reasons
|
|
2752
|
+
});
|
|
2753
|
+
return isLive;
|
|
2754
|
+
};
|
|
2755
|
+
this.roots.forEach((root) => visit(root.instance));
|
|
2756
|
+
return rows;
|
|
2757
|
+
}
|
|
2758
|
+
seedState(values) {
|
|
2759
|
+
this.cells.seed(values);
|
|
2760
|
+
}
|
|
2761
|
+
shared(key, init, options) {
|
|
2762
|
+
const cell = this.cells.get(key, init, options);
|
|
2763
|
+
return {
|
|
2764
|
+
key: cell.key,
|
|
2765
|
+
get value() {
|
|
2766
|
+
return cell.value;
|
|
2767
|
+
},
|
|
2768
|
+
set value(next) {
|
|
2769
|
+
cell.set(next);
|
|
2770
|
+
},
|
|
2771
|
+
get pending() {
|
|
2772
|
+
return cell.pending;
|
|
2773
|
+
},
|
|
2774
|
+
set: (next) => cell.set(next),
|
|
2775
|
+
subscribe: (listener) => cell.subscribe(listener)
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
scopedCell(key, init, options, instance) {
|
|
2779
|
+
const cell = this.cells.get(key, init, options);
|
|
2780
|
+
return {
|
|
2781
|
+
key: cell.key,
|
|
2782
|
+
get value() {
|
|
2783
|
+
return cell.value;
|
|
2784
|
+
},
|
|
2785
|
+
set value(next) {
|
|
2786
|
+
cell.set(next);
|
|
2787
|
+
},
|
|
2788
|
+
get pending() {
|
|
2789
|
+
return cell.pending;
|
|
2790
|
+
},
|
|
2791
|
+
set: (next) => cell.set(next),
|
|
2792
|
+
subscribe: (listener) => {
|
|
2793
|
+
const unsubscribe = cell.subscribe(listener);
|
|
2794
|
+
instance.addLogicCleanup(unsubscribe);
|
|
2795
|
+
return unsubscribe;
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
defaultProps(propsDefinition) {
|
|
2800
|
+
const props = {};
|
|
2801
|
+
if (propsDefinition && typeof propsDefinition === "object") {
|
|
2802
|
+
Object.entries(propsDefinition).forEach(([key, definition]) => {
|
|
2803
|
+
if (definition && typeof definition === "object" && "default" in definition) {
|
|
2804
|
+
props[key] = definition.default;
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
return props;
|
|
2809
|
+
}
|
|
2810
|
+
async mount(componentId, target, props = {}, slotHtml) {
|
|
2811
|
+
const container = this.resolveTarget(target);
|
|
2812
|
+
if (!container)
|
|
2813
|
+
return null;
|
|
2814
|
+
const instance = await this.mounter.mountRoot(componentId, container, props, this.parseSlotHtml(slotHtml));
|
|
2815
|
+
if (!instance)
|
|
2816
|
+
return null;
|
|
2817
|
+
const handle = {
|
|
2818
|
+
id: `root#${++this.rootCounter}`,
|
|
2819
|
+
componentId,
|
|
2820
|
+
container,
|
|
2821
|
+
instance,
|
|
2822
|
+
props,
|
|
2823
|
+
slotHtml
|
|
2824
|
+
};
|
|
2825
|
+
this.roots.set(handle.id, handle);
|
|
2826
|
+
this.styleManager.updateStyles();
|
|
2827
|
+
this.bindingEvaluator.updateTree(instance);
|
|
2828
|
+
if (import.meta.env?.DEV) {
|
|
2829
|
+
debug(`mounted ${componentId} → ${handle.id}`);
|
|
2830
|
+
}
|
|
2831
|
+
return handle;
|
|
2832
|
+
}
|
|
2833
|
+
unmount(handle) {
|
|
2834
|
+
const root = this.roots.get(handle.id);
|
|
2835
|
+
if (!root)
|
|
2836
|
+
return;
|
|
2837
|
+
this.unregisterSubtree(root.instance);
|
|
2838
|
+
this.mounter.unmount(root.instance);
|
|
2839
|
+
this.roots.delete(root.id);
|
|
2840
|
+
}
|
|
2841
|
+
async remountAll() {
|
|
2842
|
+
for (const root of [...this.roots.values()]) {
|
|
2843
|
+
this.unregisterSubtree(root.instance);
|
|
2844
|
+
this.mounter.unmount(root.instance);
|
|
2845
|
+
const instance = root.container ? await this.mounter.mountRoot(root.componentId, root.container, root.props, this.parseSlotHtml(root.slotHtml)) : await this.mounter.mountRange(root.range, root.props, this.parseSlotHtml(root.slotHtml));
|
|
2846
|
+
if (instance) {
|
|
2847
|
+
root.instance = instance;
|
|
2848
|
+
this.bindingEvaluator.updateTree(instance);
|
|
2849
|
+
} else {
|
|
2850
|
+
this.roots.delete(root.id);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
this.styleManager.updateStyles();
|
|
2854
|
+
}
|
|
2855
|
+
async hydrate(scope = document) {
|
|
2856
|
+
const mounted = new Set;
|
|
2857
|
+
this.roots.forEach((root) => {
|
|
2858
|
+
if (root.range)
|
|
2859
|
+
mounted.add(root.range.start);
|
|
2860
|
+
if (root.container)
|
|
2861
|
+
mounted.add(root.container);
|
|
2862
|
+
});
|
|
2863
|
+
const handles = [];
|
|
2864
|
+
for (const range of this.mounter.findIslands(scope)) {
|
|
2865
|
+
if (mounted.has(range.start))
|
|
2866
|
+
continue;
|
|
2867
|
+
const { payload } = range;
|
|
2868
|
+
if (payload.on && Object.keys(payload.on).length) {
|
|
2869
|
+
warn(`Ignoring @${Object.keys(payload.on).join(", @")} on top-level <${payload.c}>: no parent scope to resolve a handler from`);
|
|
2870
|
+
}
|
|
2871
|
+
const trigger = parseTrigger(payload.hydrate ?? null);
|
|
2872
|
+
const slotHtml = serializeNodes(range.slotNodes);
|
|
2873
|
+
const { needs } = payload;
|
|
2874
|
+
if (this.ssg) {
|
|
2875
|
+
if (needs) {
|
|
2876
|
+
this.mounter.renderSkeleton(range);
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
const handle2 = await this.mountIsland(range, slotHtml);
|
|
2880
|
+
if (handle2)
|
|
2881
|
+
handles.push(handle2);
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
if (needs || trigger !== "load") {
|
|
2885
|
+
if (!payload.ssr)
|
|
2886
|
+
this.mounter.renderSkeleton(range);
|
|
2887
|
+
const first = range.start.nextSibling;
|
|
2888
|
+
const target = first && first.nodeType === 1 ? first : range.start.parentNode;
|
|
2889
|
+
let triggered = false;
|
|
2890
|
+
let ready = !needs;
|
|
2891
|
+
const attempt = () => {
|
|
2892
|
+
if (!triggered || !ready)
|
|
2893
|
+
return;
|
|
2894
|
+
this.mountIsland(range, slotHtml);
|
|
2895
|
+
};
|
|
2896
|
+
this.pendingIslands.push(scheduleHydration(trigger, target, () => {
|
|
2897
|
+
triggered = true;
|
|
2898
|
+
attempt();
|
|
2899
|
+
}));
|
|
2900
|
+
if (needs) {
|
|
2901
|
+
this.pendingIslands.push(this.cells.whenReady(needs, () => {
|
|
2902
|
+
ready = true;
|
|
2903
|
+
attempt();
|
|
2904
|
+
}));
|
|
2905
|
+
}
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
const handle = await this.mountIsland(range, slotHtml);
|
|
2909
|
+
if (handle)
|
|
2910
|
+
handles.push(handle);
|
|
2911
|
+
}
|
|
2912
|
+
this.initialised = true;
|
|
2913
|
+
this.flushMounted();
|
|
2914
|
+
return handles;
|
|
2915
|
+
}
|
|
2916
|
+
flushMounted() {
|
|
2917
|
+
if (this.flushing)
|
|
2918
|
+
return;
|
|
2919
|
+
this.flushing = true;
|
|
2920
|
+
try {
|
|
2921
|
+
let progressed = true;
|
|
2922
|
+
while (progressed) {
|
|
2923
|
+
progressed = false;
|
|
2924
|
+
for (const instance of [...this.pendingMounts]) {
|
|
2925
|
+
if (!instance.rootNodes.some((node) => node.isConnected))
|
|
2926
|
+
continue;
|
|
2927
|
+
const at = this.pendingMounts.indexOf(instance);
|
|
2928
|
+
if (at >= 0)
|
|
2929
|
+
this.pendingMounts.splice(at, 1);
|
|
2930
|
+
progressed = true;
|
|
2931
|
+
const callbacks = instance.mountedCallbacks.splice(0);
|
|
2932
|
+
for (const callback of callbacks) {
|
|
2933
|
+
try {
|
|
2934
|
+
callback();
|
|
2935
|
+
} catch (error) {
|
|
2936
|
+
fail(`Error in onMounted for ${instance.id}:`, error);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
} finally {
|
|
2942
|
+
this.flushing = false;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
async mountIsland(range, slotHtml) {
|
|
2946
|
+
const props = { ...range.payload.props ?? {} };
|
|
2947
|
+
const dynamic = Object.entries(range.payload.bind ?? {}).filter(([name]) => name !== "class" && name !== "style");
|
|
2948
|
+
for (const [name, expr] of dynamic) {
|
|
2949
|
+
props[name] = this.bindingEvaluator.evaluate(expr, PAGE_SCOPE);
|
|
2950
|
+
}
|
|
2951
|
+
const cellProps = Object.entries(range.payload.cells ?? {});
|
|
2952
|
+
for (const [name, key] of cellProps) {
|
|
2953
|
+
const cell = this.cells.get(key);
|
|
2954
|
+
if (!cell.pending)
|
|
2955
|
+
props[name] = cell.value;
|
|
2956
|
+
}
|
|
2957
|
+
const instance = await this.mounter.mountRange(range, props, this.parseSlotHtml(slotHtml));
|
|
2958
|
+
if (!instance)
|
|
2959
|
+
return null;
|
|
2960
|
+
for (const [name, key] of cellProps) {
|
|
2961
|
+
this.bindingEvaluator.bindCellProp(instance, name, key);
|
|
2962
|
+
}
|
|
2963
|
+
for (const [name, expr] of dynamic) {
|
|
2964
|
+
const binding = this.bindingEvaluator.createPropBinding(instance, name, expr);
|
|
2965
|
+
instance.addBinding(binding);
|
|
2966
|
+
this.bindingEvaluator.applyBinding(binding, new BindingScope(PAGE_SCOPE, [], instance));
|
|
2967
|
+
}
|
|
2968
|
+
const handle = {
|
|
2969
|
+
id: `root#${++this.rootCounter}`,
|
|
2970
|
+
componentId: range.payload.c,
|
|
2971
|
+
instance,
|
|
2972
|
+
props,
|
|
2973
|
+
range,
|
|
2974
|
+
slotHtml
|
|
2975
|
+
};
|
|
2976
|
+
this.roots.set(handle.id, handle);
|
|
2977
|
+
this.styleManager.updateStyles();
|
|
2978
|
+
this.bindingEvaluator.updateTree(instance);
|
|
2979
|
+
this.flushMounted();
|
|
2980
|
+
return handle;
|
|
2981
|
+
}
|
|
2982
|
+
parseSlotHtml(html) {
|
|
2983
|
+
if (!html)
|
|
2984
|
+
return [];
|
|
2985
|
+
const template = document.createElement("template");
|
|
2986
|
+
template.innerHTML = html;
|
|
2987
|
+
return Array.from(template.content.childNodes);
|
|
2988
|
+
}
|
|
2989
|
+
resolveTarget(target) {
|
|
2990
|
+
if (typeof target !== "string")
|
|
2991
|
+
return target;
|
|
2992
|
+
const element = document.querySelector(target);
|
|
2993
|
+
if (!element) {
|
|
2994
|
+
warn(`Mount target not found: ${target}`);
|
|
2995
|
+
}
|
|
2996
|
+
return element;
|
|
2997
|
+
}
|
|
2998
|
+
unregisterSubtree(instance) {
|
|
2999
|
+
this.instances.delete(instance.id);
|
|
3000
|
+
instance.children.forEach((child) => this.unregisterSubtree(child));
|
|
3001
|
+
}
|
|
3002
|
+
async init(rootComponentId = "home", target = "#app") {
|
|
3003
|
+
if (import.meta.env?.DEV) {
|
|
3004
|
+
debug("runtime ready");
|
|
3005
|
+
}
|
|
3006
|
+
await this.mount(rootComponentId, target);
|
|
3007
|
+
this.flushMounted();
|
|
3008
|
+
this.initialised = true;
|
|
3009
|
+
}
|
|
3010
|
+
destroy() {
|
|
3011
|
+
this.initialised = false;
|
|
3012
|
+
this.pendingIslands.forEach((cancel) => cancel());
|
|
3013
|
+
this.pendingIslands.length = 0;
|
|
3014
|
+
for (const root of [...this.roots.values()]) {
|
|
3015
|
+
this.unregisterSubtree(root.instance);
|
|
3016
|
+
this.mounter.unmount(root.instance);
|
|
3017
|
+
}
|
|
3018
|
+
this.roots.clear();
|
|
3019
|
+
this.instances.clear();
|
|
3020
|
+
this.globalSignals.clear();
|
|
3021
|
+
this.cells.clear();
|
|
3022
|
+
this.updateScheduler.clear();
|
|
3023
|
+
this.styleManager.removeStyles();
|
|
3024
|
+
this.globalFunctions.cleanup();
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
function createLightspeedApp(options) {
|
|
3028
|
+
return new LightspeedRuntime(options);
|
|
3029
|
+
}
|
|
3030
|
+
function serializeNodes(nodes) {
|
|
3031
|
+
return nodes.map((node) => node.nodeType === 1 ? node.outerHTML : node.textContent ?? "").join("");
|
|
3032
|
+
}
|
|
3033
|
+
// src/runtime/helpers.ts
|
|
3034
|
+
function reactive(target) {
|
|
3035
|
+
return target;
|
|
3036
|
+
}
|
|
3037
|
+
function defineSignal() {
|
|
3038
|
+
return {};
|
|
3039
|
+
}
|
|
3040
|
+
function onMounted(callback) {
|
|
3041
|
+
}
|
|
3042
|
+
function defineProps(propsDefinition) {
|
|
3043
|
+
return {};
|
|
3044
|
+
}
|
|
3045
|
+
function onUnmounted(callback) {
|
|
3046
|
+
}
|
|
3047
|
+
function shared(key, init, options) {
|
|
3048
|
+
return {};
|
|
3049
|
+
}
|
|
3050
|
+
export {
|
|
3051
|
+
shared,
|
|
3052
|
+
registerPageExpressions,
|
|
3053
|
+
reactive,
|
|
3054
|
+
onUnmounted,
|
|
3055
|
+
onMounted,
|
|
3056
|
+
defineSignal,
|
|
3057
|
+
defineProps,
|
|
3058
|
+
createLightspeedApp,
|
|
3059
|
+
LightspeedRuntime
|
|
3060
|
+
};
|