@matthesketh/utopia-runtime 0.0.1
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 +74 -0
- package/dist/index.cjs +425 -0
- package/dist/index.d.cts +172 -0
- package/dist/index.d.ts +172 -0
- package/dist/index.js +376 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Hesketh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @matthesketh/utopia-runtime
|
|
2
|
+
|
|
3
|
+
DOM renderer, directives, component lifecycle, scheduler, and hydration for UtopiaJS. This is the client-side runtime that compiled `.utopia` components import from.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @matthesketh/utopia-runtime
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { mount } from '@matthesketh/utopia-runtime';
|
|
15
|
+
import App from './App.utopia';
|
|
16
|
+
|
|
17
|
+
mount(App, '#app');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For hydrating server-rendered HTML:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { hydrate } from '@matthesketh/utopia-runtime';
|
|
24
|
+
import App from './App.utopia';
|
|
25
|
+
|
|
26
|
+
hydrate(App, '#app');
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
**DOM helpers** (used by compiled template output):
|
|
32
|
+
|
|
33
|
+
| Export | Description |
|
|
34
|
+
|--------|-------------|
|
|
35
|
+
| `createElement(tag)` | Create a DOM element |
|
|
36
|
+
| `createTextNode(text)` | Create a text node |
|
|
37
|
+
| `createComment(text)` | Create a comment node |
|
|
38
|
+
| `setText(node, text)` | Set text content |
|
|
39
|
+
| `setAttr(el, name, value)` | Set an attribute |
|
|
40
|
+
| `addEventListener(el, event, handler)` | Attach an event listener |
|
|
41
|
+
| `appendChild(parent, child)` | Append a child node |
|
|
42
|
+
| `insertBefore(parent, node, ref)` | Insert before a reference node |
|
|
43
|
+
| `removeNode(node)` | Remove a node from the DOM |
|
|
44
|
+
|
|
45
|
+
**Directives** (used by compiled control-flow):
|
|
46
|
+
|
|
47
|
+
| Export | Description |
|
|
48
|
+
|--------|-------------|
|
|
49
|
+
| `createIf(anchor, cond, trueBranch, falseBranch?)` | Conditional rendering |
|
|
50
|
+
| `createFor(anchor, list, renderFn)` | List rendering |
|
|
51
|
+
| `createComponent(def, props?)` | Component instantiation |
|
|
52
|
+
|
|
53
|
+
**Lifecycle:**
|
|
54
|
+
|
|
55
|
+
| Export | Description |
|
|
56
|
+
|--------|-------------|
|
|
57
|
+
| `mount(component, target)` | Mount a component to the DOM |
|
|
58
|
+
| `createComponentInstance(def, props?)` | Create a component instance |
|
|
59
|
+
| `hydrate(component, target)` | Hydrate server-rendered HTML |
|
|
60
|
+
|
|
61
|
+
**Scheduler:**
|
|
62
|
+
|
|
63
|
+
| Export | Description |
|
|
64
|
+
|--------|-------------|
|
|
65
|
+
| `queueJob(fn)` | Queue a microtask job |
|
|
66
|
+
| `nextTick()` | Wait for the next flush |
|
|
67
|
+
|
|
68
|
+
**Re-exports from `@matthesketh/utopia-core`:** `signal`, `computed`, `effect`, `batch`, `untrack`, `createEffect`.
|
|
69
|
+
|
|
70
|
+
See [docs/architecture.md](../../docs/architecture.md) and [docs/ssr.md](../../docs/ssr.md) for full details.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
addEventListener: () => addEventListener,
|
|
24
|
+
appendChild: () => appendChild,
|
|
25
|
+
batch: () => import_utopia_core2.batch,
|
|
26
|
+
computed: () => import_utopia_core2.computed,
|
|
27
|
+
createComment: () => createComment,
|
|
28
|
+
createComponent: () => createComponent,
|
|
29
|
+
createComponentInstance: () => createComponentInstance,
|
|
30
|
+
createEffect: () => import_utopia_core3.effect,
|
|
31
|
+
createElement: () => createElement,
|
|
32
|
+
createFor: () => createFor,
|
|
33
|
+
createIf: () => createIf,
|
|
34
|
+
createTextNode: () => createTextNode,
|
|
35
|
+
effect: () => import_utopia_core2.effect,
|
|
36
|
+
hydrate: () => hydrate,
|
|
37
|
+
insertBefore: () => insertBefore,
|
|
38
|
+
mount: () => mount,
|
|
39
|
+
nextTick: () => nextTick,
|
|
40
|
+
queueJob: () => queueJob,
|
|
41
|
+
removeNode: () => removeNode,
|
|
42
|
+
setAttr: () => setAttr,
|
|
43
|
+
setText: () => setText,
|
|
44
|
+
signal: () => import_utopia_core2.signal,
|
|
45
|
+
untrack: () => import_utopia_core2.untrack
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/component.ts
|
|
50
|
+
function createComponentInstance(definition, props) {
|
|
51
|
+
let styleElement = null;
|
|
52
|
+
const instance = {
|
|
53
|
+
el: null,
|
|
54
|
+
props: props ?? {},
|
|
55
|
+
slots: {},
|
|
56
|
+
mount(target, anchor) {
|
|
57
|
+
if (instance.el) {
|
|
58
|
+
target.insertBefore(instance.el, anchor ?? null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const ctx = definition.setup ? definition.setup(instance.props) : {};
|
|
62
|
+
const renderCtx = {
|
|
63
|
+
...ctx,
|
|
64
|
+
$slots: instance.slots
|
|
65
|
+
};
|
|
66
|
+
instance.el = definition.render(renderCtx);
|
|
67
|
+
target.insertBefore(instance.el, anchor ?? null);
|
|
68
|
+
if (definition.styles && !styleElement) {
|
|
69
|
+
styleElement = document.createElement("style");
|
|
70
|
+
styleElement.textContent = definition.styles;
|
|
71
|
+
document.head.appendChild(styleElement);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
unmount() {
|
|
75
|
+
if (instance.el && instance.el.parentNode) {
|
|
76
|
+
instance.el.parentNode.removeChild(instance.el);
|
|
77
|
+
}
|
|
78
|
+
instance.el = null;
|
|
79
|
+
if (styleElement && styleElement.parentNode) {
|
|
80
|
+
styleElement.parentNode.removeChild(styleElement);
|
|
81
|
+
styleElement = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return instance;
|
|
86
|
+
}
|
|
87
|
+
function mount(component, target) {
|
|
88
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
89
|
+
if (!el) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`[utopia] Mount target not found: ${typeof target === "string" ? target : "Element"}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const instance = createComponentInstance(component);
|
|
95
|
+
instance.mount(el);
|
|
96
|
+
return instance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/hydration.ts
|
|
100
|
+
var isHydrating = false;
|
|
101
|
+
var hydrateNode = null;
|
|
102
|
+
var cursorStack = [];
|
|
103
|
+
function claimNode() {
|
|
104
|
+
const node = hydrateNode;
|
|
105
|
+
if (node) {
|
|
106
|
+
hydrateNode = node.nextSibling;
|
|
107
|
+
}
|
|
108
|
+
return node;
|
|
109
|
+
}
|
|
110
|
+
function enterNode(el) {
|
|
111
|
+
cursorStack.push(hydrateNode);
|
|
112
|
+
hydrateNode = el.firstChild;
|
|
113
|
+
}
|
|
114
|
+
function exitNode() {
|
|
115
|
+
hydrateNode = cursorStack.pop() ?? null;
|
|
116
|
+
}
|
|
117
|
+
function hydrate(component, target) {
|
|
118
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
119
|
+
if (!el) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`[utopia] Hydration target not found: ${typeof target === "string" ? target : "Element"}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
isHydrating = true;
|
|
125
|
+
hydrateNode = el.firstChild;
|
|
126
|
+
try {
|
|
127
|
+
const instance = createComponentInstance(component);
|
|
128
|
+
const ctx = component.setup ? component.setup(instance.props) : {};
|
|
129
|
+
const renderCtx = {
|
|
130
|
+
...ctx,
|
|
131
|
+
$slots: instance.slots
|
|
132
|
+
};
|
|
133
|
+
instance.el = component.render(renderCtx);
|
|
134
|
+
if (component.styles) {
|
|
135
|
+
const style = document.createElement("style");
|
|
136
|
+
style.textContent = component.styles;
|
|
137
|
+
document.head.appendChild(style);
|
|
138
|
+
}
|
|
139
|
+
} finally {
|
|
140
|
+
isHydrating = false;
|
|
141
|
+
hydrateNode = null;
|
|
142
|
+
cursorStack.length = 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/dom.ts
|
|
147
|
+
function createElement(tag) {
|
|
148
|
+
if (isHydrating) {
|
|
149
|
+
const node = claimNode();
|
|
150
|
+
if (node && node.nodeType === 1) {
|
|
151
|
+
enterNode(node);
|
|
152
|
+
return node;
|
|
153
|
+
}
|
|
154
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
155
|
+
console.warn(`[utopia] Hydration mismatch: expected <${tag}>, got`, node);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return document.createElement(tag);
|
|
159
|
+
}
|
|
160
|
+
function createTextNode(text) {
|
|
161
|
+
if (isHydrating) {
|
|
162
|
+
const node = claimNode();
|
|
163
|
+
if (node && node.nodeType === 3) {
|
|
164
|
+
return node;
|
|
165
|
+
}
|
|
166
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
167
|
+
console.warn(`[utopia] Hydration mismatch: expected text node, got`, node);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return document.createTextNode(String(text));
|
|
171
|
+
}
|
|
172
|
+
function setText(node, value) {
|
|
173
|
+
const text = value == null ? "" : String(value);
|
|
174
|
+
if (node.data !== text) {
|
|
175
|
+
node.data = text;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function setAttr(el, name, value) {
|
|
179
|
+
if (name === "class") {
|
|
180
|
+
if (value == null || value === false) {
|
|
181
|
+
el.removeAttribute("class");
|
|
182
|
+
} else if (typeof value === "string") {
|
|
183
|
+
el.className = value;
|
|
184
|
+
} else if (typeof value === "object") {
|
|
185
|
+
const classes = [];
|
|
186
|
+
for (const key of Object.keys(value)) {
|
|
187
|
+
if (value[key]) {
|
|
188
|
+
classes.push(key);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
el.className = classes.join(" ");
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (name === "style") {
|
|
196
|
+
const htmlEl = el;
|
|
197
|
+
if (value == null || value === false) {
|
|
198
|
+
htmlEl.removeAttribute("style");
|
|
199
|
+
} else if (typeof value === "string") {
|
|
200
|
+
htmlEl.style.cssText = value;
|
|
201
|
+
} else if (typeof value === "object") {
|
|
202
|
+
htmlEl.style.cssText = "";
|
|
203
|
+
for (const prop of Object.keys(value)) {
|
|
204
|
+
const val = value[prop];
|
|
205
|
+
if (val != null) {
|
|
206
|
+
htmlEl.style.setProperty(
|
|
207
|
+
prop.replace(/([A-Z])/g, "-$1").toLowerCase(),
|
|
208
|
+
String(val)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
216
|
+
"disabled",
|
|
217
|
+
"checked",
|
|
218
|
+
"readonly",
|
|
219
|
+
"hidden",
|
|
220
|
+
"selected",
|
|
221
|
+
"required",
|
|
222
|
+
"multiple",
|
|
223
|
+
"autofocus",
|
|
224
|
+
"autoplay",
|
|
225
|
+
"controls",
|
|
226
|
+
"loop",
|
|
227
|
+
"muted",
|
|
228
|
+
"open",
|
|
229
|
+
"novalidate"
|
|
230
|
+
]);
|
|
231
|
+
if (BOOLEAN_ATTRS.has(name)) {
|
|
232
|
+
if (value) {
|
|
233
|
+
el.setAttribute(name, "");
|
|
234
|
+
if (name in el) {
|
|
235
|
+
el[name] = true;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
el.removeAttribute(name);
|
|
239
|
+
if (name in el) {
|
|
240
|
+
el[name] = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (name.startsWith("data-")) {
|
|
246
|
+
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
247
|
+
el.dataset[key] = value == null ? "" : String(value);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (value == null || value === false) {
|
|
251
|
+
el.removeAttribute(name);
|
|
252
|
+
} else {
|
|
253
|
+
el.setAttribute(name, value === true ? "" : String(value));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function addEventListener(el, event, handler) {
|
|
257
|
+
el.addEventListener(event, handler);
|
|
258
|
+
return () => {
|
|
259
|
+
el.removeEventListener(event, handler);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function insertBefore(parent, node, anchor) {
|
|
263
|
+
parent.insertBefore(node, anchor);
|
|
264
|
+
}
|
|
265
|
+
function removeNode(node) {
|
|
266
|
+
if (node.parentNode) {
|
|
267
|
+
node.parentNode.removeChild(node);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function appendChild(parent, child) {
|
|
271
|
+
if (isHydrating) {
|
|
272
|
+
if (child.nodeType === 1) {
|
|
273
|
+
exitNode();
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
parent.appendChild(child);
|
|
278
|
+
}
|
|
279
|
+
function createComment(text) {
|
|
280
|
+
if (isHydrating) {
|
|
281
|
+
const node = claimNode();
|
|
282
|
+
if (node && node.nodeType === 8) {
|
|
283
|
+
return node;
|
|
284
|
+
}
|
|
285
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
286
|
+
console.warn(`[utopia] Hydration mismatch: expected comment node, got`, node);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return document.createComment(text);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/directives.ts
|
|
293
|
+
var import_utopia_core = require("@matthesketh/utopia-core");
|
|
294
|
+
function clearNodes(nodes) {
|
|
295
|
+
for (const node of nodes) {
|
|
296
|
+
removeNode(node);
|
|
297
|
+
}
|
|
298
|
+
nodes.length = 0;
|
|
299
|
+
}
|
|
300
|
+
function createIf(anchor, condition, renderTrue, renderFalse) {
|
|
301
|
+
let currentNodes = [];
|
|
302
|
+
let lastConditionTruthy;
|
|
303
|
+
const parent = anchor.parentNode;
|
|
304
|
+
const dispose = (0, import_utopia_core.effect)(() => {
|
|
305
|
+
const truthy = !!condition();
|
|
306
|
+
if (truthy === lastConditionTruthy) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
lastConditionTruthy = truthy;
|
|
310
|
+
clearNodes(currentNodes);
|
|
311
|
+
if (truthy) {
|
|
312
|
+
const node = renderTrue();
|
|
313
|
+
currentNodes.push(node);
|
|
314
|
+
insertBefore(parent, node, anchor);
|
|
315
|
+
} else if (renderFalse) {
|
|
316
|
+
const node = renderFalse();
|
|
317
|
+
currentNodes.push(node);
|
|
318
|
+
insertBefore(parent, node, anchor);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
return () => {
|
|
322
|
+
dispose();
|
|
323
|
+
clearNodes(currentNodes);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function createFor(anchor, list, renderItem, key) {
|
|
327
|
+
let currentNodes = [];
|
|
328
|
+
const parent = anchor.parentNode;
|
|
329
|
+
void key;
|
|
330
|
+
const dispose = (0, import_utopia_core.effect)(() => {
|
|
331
|
+
const items = list();
|
|
332
|
+
clearNodes(currentNodes);
|
|
333
|
+
for (let i = 0; i < items.length; i++) {
|
|
334
|
+
const node = renderItem(items[i], i);
|
|
335
|
+
currentNodes.push(node);
|
|
336
|
+
insertBefore(parent, node, anchor);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
return () => {
|
|
340
|
+
dispose();
|
|
341
|
+
clearNodes(currentNodes);
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function createComponent(Component, props, children) {
|
|
345
|
+
const instance = createComponentInstance(Component, props);
|
|
346
|
+
if (children) {
|
|
347
|
+
for (const slotName of Object.keys(children)) {
|
|
348
|
+
instance.slots[slotName] = children[slotName];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const ctx = Component.setup ? Component.setup(instance.props) : {};
|
|
352
|
+
const renderCtx = {
|
|
353
|
+
...ctx,
|
|
354
|
+
$slots: instance.slots
|
|
355
|
+
};
|
|
356
|
+
instance.el = Component.render(renderCtx);
|
|
357
|
+
if (Component.styles) {
|
|
358
|
+
const style = document.createElement("style");
|
|
359
|
+
style.textContent = Component.styles;
|
|
360
|
+
document.head.appendChild(style);
|
|
361
|
+
}
|
|
362
|
+
return instance.el;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/scheduler.ts
|
|
366
|
+
var queue = /* @__PURE__ */ new Set();
|
|
367
|
+
var isFlushing = false;
|
|
368
|
+
var isFlushPending = false;
|
|
369
|
+
var resolvedPromise = Promise.resolve();
|
|
370
|
+
function queueJob(job) {
|
|
371
|
+
queue.add(job);
|
|
372
|
+
if (!isFlushPending && !isFlushing) {
|
|
373
|
+
isFlushPending = true;
|
|
374
|
+
resolvedPromise.then(flushJobs);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function nextTick() {
|
|
378
|
+
return resolvedPromise.then();
|
|
379
|
+
}
|
|
380
|
+
function flushJobs() {
|
|
381
|
+
isFlushPending = false;
|
|
382
|
+
isFlushing = true;
|
|
383
|
+
try {
|
|
384
|
+
for (const job of queue) {
|
|
385
|
+
queue.delete(job);
|
|
386
|
+
job();
|
|
387
|
+
}
|
|
388
|
+
} finally {
|
|
389
|
+
isFlushing = false;
|
|
390
|
+
if (queue.size > 0) {
|
|
391
|
+
isFlushPending = true;
|
|
392
|
+
resolvedPromise.then(flushJobs);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/index.ts
|
|
398
|
+
var import_utopia_core2 = require("@matthesketh/utopia-core");
|
|
399
|
+
var import_utopia_core3 = require("@matthesketh/utopia-core");
|
|
400
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
401
|
+
0 && (module.exports = {
|
|
402
|
+
addEventListener,
|
|
403
|
+
appendChild,
|
|
404
|
+
batch,
|
|
405
|
+
computed,
|
|
406
|
+
createComment,
|
|
407
|
+
createComponent,
|
|
408
|
+
createComponentInstance,
|
|
409
|
+
createEffect,
|
|
410
|
+
createElement,
|
|
411
|
+
createFor,
|
|
412
|
+
createIf,
|
|
413
|
+
createTextNode,
|
|
414
|
+
effect,
|
|
415
|
+
hydrate,
|
|
416
|
+
insertBefore,
|
|
417
|
+
mount,
|
|
418
|
+
nextTick,
|
|
419
|
+
queueJob,
|
|
420
|
+
removeNode,
|
|
421
|
+
setAttr,
|
|
422
|
+
setText,
|
|
423
|
+
signal,
|
|
424
|
+
untrack
|
|
425
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export { batch, computed, effect as createEffect, effect, signal, untrack } from '@matthesketh/utopia-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @matthesketh/utopia-runtime — Low-level DOM helpers
|
|
5
|
+
*
|
|
6
|
+
* These thin wrappers are the only layer between compiled .utopia template
|
|
7
|
+
* output and the real DOM. Keeping them minimal makes tree-shaking effective
|
|
8
|
+
* and keeps the runtime footprint small.
|
|
9
|
+
*/
|
|
10
|
+
/** Create a real DOM element for the given tag name. */
|
|
11
|
+
declare function createElement(tag: string): HTMLElement;
|
|
12
|
+
/** Create a DOM text node. */
|
|
13
|
+
declare function createTextNode(text: string): Text;
|
|
14
|
+
/**
|
|
15
|
+
* Set the text content of a Text node. The compiler wraps calls to this
|
|
16
|
+
* function inside an `effect()` so the DOM stays in sync with signals.
|
|
17
|
+
*/
|
|
18
|
+
declare function setText(node: Text, value: any): void;
|
|
19
|
+
/**
|
|
20
|
+
* Set an attribute on an element, handling the many special cases that arise
|
|
21
|
+
* in real-world templates:
|
|
22
|
+
*
|
|
23
|
+
* - **class**: accepts a string or an object `{ active: true, hidden: false }`
|
|
24
|
+
* - **style**: accepts a string or an object `{ color: 'red', fontSize: '14px' }`
|
|
25
|
+
* - **Boolean attributes** (`disabled`, `checked`, `readonly`, `hidden`,
|
|
26
|
+
* `selected`, `required`, `multiple`, `autofocus`, `autoplay`, `controls`,
|
|
27
|
+
* `loop`, `muted`, `open`, `novalidate`): set/remove the attribute based on
|
|
28
|
+
* truthiness, and also set the IDL property where applicable.
|
|
29
|
+
* - **data-* attributes**: set via `el.dataset`
|
|
30
|
+
* - Everything else: plain `setAttribute` / `removeAttribute`.
|
|
31
|
+
*/
|
|
32
|
+
declare function setAttr(el: Element, name: string, value: any): void;
|
|
33
|
+
/**
|
|
34
|
+
* Add an event listener to an element and return a cleanup function that
|
|
35
|
+
* removes it.
|
|
36
|
+
*/
|
|
37
|
+
declare function addEventListener(el: Element, event: string, handler: EventListener): () => void;
|
|
38
|
+
/** Insert `node` into `parent` before the given `anchor` (or append if null). */
|
|
39
|
+
declare function insertBefore(parent: Node, node: Node, anchor: Node | null): void;
|
|
40
|
+
/** Remove a node from its parent. No-op if the node has no parent. */
|
|
41
|
+
declare function removeNode(node: Node): void;
|
|
42
|
+
/** Append a child node to a parent. */
|
|
43
|
+
declare function appendChild(parent: Node, child: Node): void;
|
|
44
|
+
/** Create a DOM comment node. */
|
|
45
|
+
declare function createComment(text: string): Comment;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @matthesketh/utopia-runtime — Component lifecycle
|
|
49
|
+
*
|
|
50
|
+
* Provides the primitives for instantiating and mounting compiled .utopia
|
|
51
|
+
* component definitions.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* A ComponentDefinition is the object the compiler produces for each .utopia
|
|
55
|
+
* single-file component.
|
|
56
|
+
*/
|
|
57
|
+
interface ComponentDefinition {
|
|
58
|
+
/** The `<script>` block compiled into a setup function. */
|
|
59
|
+
setup?: (props: Record<string, any>) => Record<string, any>;
|
|
60
|
+
/** The `<template>` block compiled into a render function. */
|
|
61
|
+
render: (ctx: Record<string, any>) => Node;
|
|
62
|
+
/** Scoped CSS extracted from the `<style>` block, if any. */
|
|
63
|
+
styles?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A live instance of a mounted component.
|
|
67
|
+
*/
|
|
68
|
+
interface ComponentInstance {
|
|
69
|
+
/** The root DOM node produced by `render()`. */
|
|
70
|
+
el: Node | null;
|
|
71
|
+
/** The reactive props passed into this component. */
|
|
72
|
+
props: Record<string, any>;
|
|
73
|
+
/** Named slots (each value is a factory that returns a DOM subtree). */
|
|
74
|
+
slots: Record<string, () => Node>;
|
|
75
|
+
/** Mount the component's root node into the given target element. */
|
|
76
|
+
mount(target: Element, anchor?: Node): void;
|
|
77
|
+
/** Remove the component's root node from the DOM and clean up styles. */
|
|
78
|
+
unmount(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create a `ComponentInstance` from a compiled component definition.
|
|
82
|
+
*
|
|
83
|
+
* The instance is **not** automatically mounted — call `instance.mount()`
|
|
84
|
+
* to attach it to the DOM.
|
|
85
|
+
*/
|
|
86
|
+
declare function createComponentInstance(definition: ComponentDefinition, props?: Record<string, any>): ComponentInstance;
|
|
87
|
+
/**
|
|
88
|
+
* Mount a root component to the page.
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* import App from './App.utopia'
|
|
92
|
+
* import { mount } from '@matthesketh/utopia-runtime'
|
|
93
|
+
*
|
|
94
|
+
* mount(App, '#app')
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @param component The compiled root component definition.
|
|
98
|
+
* @param target A CSS selector string or a DOM Element to mount into.
|
|
99
|
+
* @returns The `ComponentInstance`, allowing later `unmount()`.
|
|
100
|
+
*/
|
|
101
|
+
declare function mount(component: ComponentDefinition, target: string | Element): ComponentInstance;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @matthesketh/utopia-runtime — Runtime directive implementations
|
|
105
|
+
*
|
|
106
|
+
* These functions are called by the code the compiler emits for control-flow
|
|
107
|
+
* constructs (`@if`, `@for`) and child components in .utopia templates.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Conditional rendering directive.
|
|
112
|
+
*
|
|
113
|
+
* @param anchor A Comment node already in the DOM that marks the insertion
|
|
114
|
+
* point. All branch nodes are inserted immediately before it.
|
|
115
|
+
* @param condition A function that returns a truthy/falsy value (typically
|
|
116
|
+
* reading a signal so the effect tracks it).
|
|
117
|
+
* @param renderTrue Factory that produces the DOM subtree for the "true" branch.
|
|
118
|
+
* @param renderFalse Optional factory for the "false" / else branch.
|
|
119
|
+
* @returns A dispose function that tears down the effect and removes nodes.
|
|
120
|
+
*/
|
|
121
|
+
declare function createIf(anchor: Comment, condition: () => any, renderTrue: () => Node, renderFalse?: () => Node): () => void;
|
|
122
|
+
/**
|
|
123
|
+
* List rendering directive.
|
|
124
|
+
*
|
|
125
|
+
* @param anchor Comment node marking the insertion point.
|
|
126
|
+
* @param list Function returning the current array (reads signals).
|
|
127
|
+
* @param renderItem Factory `(item, index) => Node` for each element.
|
|
128
|
+
* @param key Optional key extractor for future keyed-diffing optimisation.
|
|
129
|
+
* @returns A dispose function.
|
|
130
|
+
*/
|
|
131
|
+
declare function createFor<T>(anchor: Comment, list: () => T[], renderItem: (item: T, index: number) => Node, key?: (item: T, index: number) => any): () => void;
|
|
132
|
+
/**
|
|
133
|
+
* Mount a child component at the given anchor position.
|
|
134
|
+
*
|
|
135
|
+
* @param Component The compiled component definition (has `setup`, `render`,
|
|
136
|
+
* and optional `styles`).
|
|
137
|
+
* @param props Props object to pass to the component's setup function.
|
|
138
|
+
* @param children Optional slot/children map. Each key maps to a function
|
|
139
|
+
* that returns a DOM node for that slot.
|
|
140
|
+
* @returns The root DOM node of the mounted component.
|
|
141
|
+
*/
|
|
142
|
+
declare function createComponent(Component: ComponentDefinition, props?: Record<string, any>, children?: Record<string, () => Node>): Node;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @matthesketh/utopia-runtime — Microtask-based update scheduler
|
|
146
|
+
*
|
|
147
|
+
* Batches DOM update jobs so that multiple signal changes within the same
|
|
148
|
+
* synchronous tick only trigger a single DOM update pass.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* Queue a job for the next microtask flush. Duplicate references to the same
|
|
152
|
+
* function are automatically de-duplicated because we store them in a Set.
|
|
153
|
+
*/
|
|
154
|
+
declare function queueJob(job: () => void): void;
|
|
155
|
+
/**
|
|
156
|
+
* Returns a promise that resolves after the current pending flush completes.
|
|
157
|
+
* Useful for tests and any code that needs to wait for DOM updates.
|
|
158
|
+
*/
|
|
159
|
+
declare function nextTick(): Promise<void>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Hydrate a server-rendered component. Instead of creating new DOM nodes,
|
|
163
|
+
* the runtime claims the existing nodes in the target element and attaches
|
|
164
|
+
* event listeners and reactive effects.
|
|
165
|
+
*
|
|
166
|
+
* @param component - The compiled component definition
|
|
167
|
+
* @param target - A CSS selector string or DOM Element containing the
|
|
168
|
+
* server-rendered HTML
|
|
169
|
+
*/
|
|
170
|
+
declare function hydrate(component: ComponentDefinition, target: string | Element): void;
|
|
171
|
+
|
|
172
|
+
export { type ComponentDefinition, type ComponentInstance, addEventListener, appendChild, createComment, createComponent, createComponentInstance, createElement, createFor, createIf, createTextNode, hydrate, insertBefore, mount, nextTick, queueJob, removeNode, setAttr, setText };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export { batch, computed, effect as createEffect, effect, signal, untrack } from '@matthesketh/utopia-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @matthesketh/utopia-runtime — Low-level DOM helpers
|
|
5
|
+
*
|
|
6
|
+
* These thin wrappers are the only layer between compiled .utopia template
|
|
7
|
+
* output and the real DOM. Keeping them minimal makes tree-shaking effective
|
|
8
|
+
* and keeps the runtime footprint small.
|
|
9
|
+
*/
|
|
10
|
+
/** Create a real DOM element for the given tag name. */
|
|
11
|
+
declare function createElement(tag: string): HTMLElement;
|
|
12
|
+
/** Create a DOM text node. */
|
|
13
|
+
declare function createTextNode(text: string): Text;
|
|
14
|
+
/**
|
|
15
|
+
* Set the text content of a Text node. The compiler wraps calls to this
|
|
16
|
+
* function inside an `effect()` so the DOM stays in sync with signals.
|
|
17
|
+
*/
|
|
18
|
+
declare function setText(node: Text, value: any): void;
|
|
19
|
+
/**
|
|
20
|
+
* Set an attribute on an element, handling the many special cases that arise
|
|
21
|
+
* in real-world templates:
|
|
22
|
+
*
|
|
23
|
+
* - **class**: accepts a string or an object `{ active: true, hidden: false }`
|
|
24
|
+
* - **style**: accepts a string or an object `{ color: 'red', fontSize: '14px' }`
|
|
25
|
+
* - **Boolean attributes** (`disabled`, `checked`, `readonly`, `hidden`,
|
|
26
|
+
* `selected`, `required`, `multiple`, `autofocus`, `autoplay`, `controls`,
|
|
27
|
+
* `loop`, `muted`, `open`, `novalidate`): set/remove the attribute based on
|
|
28
|
+
* truthiness, and also set the IDL property where applicable.
|
|
29
|
+
* - **data-* attributes**: set via `el.dataset`
|
|
30
|
+
* - Everything else: plain `setAttribute` / `removeAttribute`.
|
|
31
|
+
*/
|
|
32
|
+
declare function setAttr(el: Element, name: string, value: any): void;
|
|
33
|
+
/**
|
|
34
|
+
* Add an event listener to an element and return a cleanup function that
|
|
35
|
+
* removes it.
|
|
36
|
+
*/
|
|
37
|
+
declare function addEventListener(el: Element, event: string, handler: EventListener): () => void;
|
|
38
|
+
/** Insert `node` into `parent` before the given `anchor` (or append if null). */
|
|
39
|
+
declare function insertBefore(parent: Node, node: Node, anchor: Node | null): void;
|
|
40
|
+
/** Remove a node from its parent. No-op if the node has no parent. */
|
|
41
|
+
declare function removeNode(node: Node): void;
|
|
42
|
+
/** Append a child node to a parent. */
|
|
43
|
+
declare function appendChild(parent: Node, child: Node): void;
|
|
44
|
+
/** Create a DOM comment node. */
|
|
45
|
+
declare function createComment(text: string): Comment;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @matthesketh/utopia-runtime — Component lifecycle
|
|
49
|
+
*
|
|
50
|
+
* Provides the primitives for instantiating and mounting compiled .utopia
|
|
51
|
+
* component definitions.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* A ComponentDefinition is the object the compiler produces for each .utopia
|
|
55
|
+
* single-file component.
|
|
56
|
+
*/
|
|
57
|
+
interface ComponentDefinition {
|
|
58
|
+
/** The `<script>` block compiled into a setup function. */
|
|
59
|
+
setup?: (props: Record<string, any>) => Record<string, any>;
|
|
60
|
+
/** The `<template>` block compiled into a render function. */
|
|
61
|
+
render: (ctx: Record<string, any>) => Node;
|
|
62
|
+
/** Scoped CSS extracted from the `<style>` block, if any. */
|
|
63
|
+
styles?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A live instance of a mounted component.
|
|
67
|
+
*/
|
|
68
|
+
interface ComponentInstance {
|
|
69
|
+
/** The root DOM node produced by `render()`. */
|
|
70
|
+
el: Node | null;
|
|
71
|
+
/** The reactive props passed into this component. */
|
|
72
|
+
props: Record<string, any>;
|
|
73
|
+
/** Named slots (each value is a factory that returns a DOM subtree). */
|
|
74
|
+
slots: Record<string, () => Node>;
|
|
75
|
+
/** Mount the component's root node into the given target element. */
|
|
76
|
+
mount(target: Element, anchor?: Node): void;
|
|
77
|
+
/** Remove the component's root node from the DOM and clean up styles. */
|
|
78
|
+
unmount(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Create a `ComponentInstance` from a compiled component definition.
|
|
82
|
+
*
|
|
83
|
+
* The instance is **not** automatically mounted — call `instance.mount()`
|
|
84
|
+
* to attach it to the DOM.
|
|
85
|
+
*/
|
|
86
|
+
declare function createComponentInstance(definition: ComponentDefinition, props?: Record<string, any>): ComponentInstance;
|
|
87
|
+
/**
|
|
88
|
+
* Mount a root component to the page.
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* import App from './App.utopia'
|
|
92
|
+
* import { mount } from '@matthesketh/utopia-runtime'
|
|
93
|
+
*
|
|
94
|
+
* mount(App, '#app')
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @param component The compiled root component definition.
|
|
98
|
+
* @param target A CSS selector string or a DOM Element to mount into.
|
|
99
|
+
* @returns The `ComponentInstance`, allowing later `unmount()`.
|
|
100
|
+
*/
|
|
101
|
+
declare function mount(component: ComponentDefinition, target: string | Element): ComponentInstance;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @matthesketh/utopia-runtime — Runtime directive implementations
|
|
105
|
+
*
|
|
106
|
+
* These functions are called by the code the compiler emits for control-flow
|
|
107
|
+
* constructs (`@if`, `@for`) and child components in .utopia templates.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Conditional rendering directive.
|
|
112
|
+
*
|
|
113
|
+
* @param anchor A Comment node already in the DOM that marks the insertion
|
|
114
|
+
* point. All branch nodes are inserted immediately before it.
|
|
115
|
+
* @param condition A function that returns a truthy/falsy value (typically
|
|
116
|
+
* reading a signal so the effect tracks it).
|
|
117
|
+
* @param renderTrue Factory that produces the DOM subtree for the "true" branch.
|
|
118
|
+
* @param renderFalse Optional factory for the "false" / else branch.
|
|
119
|
+
* @returns A dispose function that tears down the effect and removes nodes.
|
|
120
|
+
*/
|
|
121
|
+
declare function createIf(anchor: Comment, condition: () => any, renderTrue: () => Node, renderFalse?: () => Node): () => void;
|
|
122
|
+
/**
|
|
123
|
+
* List rendering directive.
|
|
124
|
+
*
|
|
125
|
+
* @param anchor Comment node marking the insertion point.
|
|
126
|
+
* @param list Function returning the current array (reads signals).
|
|
127
|
+
* @param renderItem Factory `(item, index) => Node` for each element.
|
|
128
|
+
* @param key Optional key extractor for future keyed-diffing optimisation.
|
|
129
|
+
* @returns A dispose function.
|
|
130
|
+
*/
|
|
131
|
+
declare function createFor<T>(anchor: Comment, list: () => T[], renderItem: (item: T, index: number) => Node, key?: (item: T, index: number) => any): () => void;
|
|
132
|
+
/**
|
|
133
|
+
* Mount a child component at the given anchor position.
|
|
134
|
+
*
|
|
135
|
+
* @param Component The compiled component definition (has `setup`, `render`,
|
|
136
|
+
* and optional `styles`).
|
|
137
|
+
* @param props Props object to pass to the component's setup function.
|
|
138
|
+
* @param children Optional slot/children map. Each key maps to a function
|
|
139
|
+
* that returns a DOM node for that slot.
|
|
140
|
+
* @returns The root DOM node of the mounted component.
|
|
141
|
+
*/
|
|
142
|
+
declare function createComponent(Component: ComponentDefinition, props?: Record<string, any>, children?: Record<string, () => Node>): Node;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @matthesketh/utopia-runtime — Microtask-based update scheduler
|
|
146
|
+
*
|
|
147
|
+
* Batches DOM update jobs so that multiple signal changes within the same
|
|
148
|
+
* synchronous tick only trigger a single DOM update pass.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* Queue a job for the next microtask flush. Duplicate references to the same
|
|
152
|
+
* function are automatically de-duplicated because we store them in a Set.
|
|
153
|
+
*/
|
|
154
|
+
declare function queueJob(job: () => void): void;
|
|
155
|
+
/**
|
|
156
|
+
* Returns a promise that resolves after the current pending flush completes.
|
|
157
|
+
* Useful for tests and any code that needs to wait for DOM updates.
|
|
158
|
+
*/
|
|
159
|
+
declare function nextTick(): Promise<void>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Hydrate a server-rendered component. Instead of creating new DOM nodes,
|
|
163
|
+
* the runtime claims the existing nodes in the target element and attaches
|
|
164
|
+
* event listeners and reactive effects.
|
|
165
|
+
*
|
|
166
|
+
* @param component - The compiled component definition
|
|
167
|
+
* @param target - A CSS selector string or DOM Element containing the
|
|
168
|
+
* server-rendered HTML
|
|
169
|
+
*/
|
|
170
|
+
declare function hydrate(component: ComponentDefinition, target: string | Element): void;
|
|
171
|
+
|
|
172
|
+
export { type ComponentDefinition, type ComponentInstance, addEventListener, appendChild, createComment, createComponent, createComponentInstance, createElement, createFor, createIf, createTextNode, hydrate, insertBefore, mount, nextTick, queueJob, removeNode, setAttr, setText };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// src/component.ts
|
|
2
|
+
function createComponentInstance(definition, props) {
|
|
3
|
+
let styleElement = null;
|
|
4
|
+
const instance = {
|
|
5
|
+
el: null,
|
|
6
|
+
props: props ?? {},
|
|
7
|
+
slots: {},
|
|
8
|
+
mount(target, anchor) {
|
|
9
|
+
if (instance.el) {
|
|
10
|
+
target.insertBefore(instance.el, anchor ?? null);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const ctx = definition.setup ? definition.setup(instance.props) : {};
|
|
14
|
+
const renderCtx = {
|
|
15
|
+
...ctx,
|
|
16
|
+
$slots: instance.slots
|
|
17
|
+
};
|
|
18
|
+
instance.el = definition.render(renderCtx);
|
|
19
|
+
target.insertBefore(instance.el, anchor ?? null);
|
|
20
|
+
if (definition.styles && !styleElement) {
|
|
21
|
+
styleElement = document.createElement("style");
|
|
22
|
+
styleElement.textContent = definition.styles;
|
|
23
|
+
document.head.appendChild(styleElement);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
unmount() {
|
|
27
|
+
if (instance.el && instance.el.parentNode) {
|
|
28
|
+
instance.el.parentNode.removeChild(instance.el);
|
|
29
|
+
}
|
|
30
|
+
instance.el = null;
|
|
31
|
+
if (styleElement && styleElement.parentNode) {
|
|
32
|
+
styleElement.parentNode.removeChild(styleElement);
|
|
33
|
+
styleElement = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return instance;
|
|
38
|
+
}
|
|
39
|
+
function mount(component, target) {
|
|
40
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
41
|
+
if (!el) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[utopia] Mount target not found: ${typeof target === "string" ? target : "Element"}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const instance = createComponentInstance(component);
|
|
47
|
+
instance.mount(el);
|
|
48
|
+
return instance;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/hydration.ts
|
|
52
|
+
var isHydrating = false;
|
|
53
|
+
var hydrateNode = null;
|
|
54
|
+
var cursorStack = [];
|
|
55
|
+
function claimNode() {
|
|
56
|
+
const node = hydrateNode;
|
|
57
|
+
if (node) {
|
|
58
|
+
hydrateNode = node.nextSibling;
|
|
59
|
+
}
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
function enterNode(el) {
|
|
63
|
+
cursorStack.push(hydrateNode);
|
|
64
|
+
hydrateNode = el.firstChild;
|
|
65
|
+
}
|
|
66
|
+
function exitNode() {
|
|
67
|
+
hydrateNode = cursorStack.pop() ?? null;
|
|
68
|
+
}
|
|
69
|
+
function hydrate(component, target) {
|
|
70
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
71
|
+
if (!el) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[utopia] Hydration target not found: ${typeof target === "string" ? target : "Element"}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
isHydrating = true;
|
|
77
|
+
hydrateNode = el.firstChild;
|
|
78
|
+
try {
|
|
79
|
+
const instance = createComponentInstance(component);
|
|
80
|
+
const ctx = component.setup ? component.setup(instance.props) : {};
|
|
81
|
+
const renderCtx = {
|
|
82
|
+
...ctx,
|
|
83
|
+
$slots: instance.slots
|
|
84
|
+
};
|
|
85
|
+
instance.el = component.render(renderCtx);
|
|
86
|
+
if (component.styles) {
|
|
87
|
+
const style = document.createElement("style");
|
|
88
|
+
style.textContent = component.styles;
|
|
89
|
+
document.head.appendChild(style);
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
isHydrating = false;
|
|
93
|
+
hydrateNode = null;
|
|
94
|
+
cursorStack.length = 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/dom.ts
|
|
99
|
+
function createElement(tag) {
|
|
100
|
+
if (isHydrating) {
|
|
101
|
+
const node = claimNode();
|
|
102
|
+
if (node && node.nodeType === 1) {
|
|
103
|
+
enterNode(node);
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
107
|
+
console.warn(`[utopia] Hydration mismatch: expected <${tag}>, got`, node);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return document.createElement(tag);
|
|
111
|
+
}
|
|
112
|
+
function createTextNode(text) {
|
|
113
|
+
if (isHydrating) {
|
|
114
|
+
const node = claimNode();
|
|
115
|
+
if (node && node.nodeType === 3) {
|
|
116
|
+
return node;
|
|
117
|
+
}
|
|
118
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
119
|
+
console.warn(`[utopia] Hydration mismatch: expected text node, got`, node);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return document.createTextNode(String(text));
|
|
123
|
+
}
|
|
124
|
+
function setText(node, value) {
|
|
125
|
+
const text = value == null ? "" : String(value);
|
|
126
|
+
if (node.data !== text) {
|
|
127
|
+
node.data = text;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function setAttr(el, name, value) {
|
|
131
|
+
if (name === "class") {
|
|
132
|
+
if (value == null || value === false) {
|
|
133
|
+
el.removeAttribute("class");
|
|
134
|
+
} else if (typeof value === "string") {
|
|
135
|
+
el.className = value;
|
|
136
|
+
} else if (typeof value === "object") {
|
|
137
|
+
const classes = [];
|
|
138
|
+
for (const key of Object.keys(value)) {
|
|
139
|
+
if (value[key]) {
|
|
140
|
+
classes.push(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
el.className = classes.join(" ");
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (name === "style") {
|
|
148
|
+
const htmlEl = el;
|
|
149
|
+
if (value == null || value === false) {
|
|
150
|
+
htmlEl.removeAttribute("style");
|
|
151
|
+
} else if (typeof value === "string") {
|
|
152
|
+
htmlEl.style.cssText = value;
|
|
153
|
+
} else if (typeof value === "object") {
|
|
154
|
+
htmlEl.style.cssText = "";
|
|
155
|
+
for (const prop of Object.keys(value)) {
|
|
156
|
+
const val = value[prop];
|
|
157
|
+
if (val != null) {
|
|
158
|
+
htmlEl.style.setProperty(
|
|
159
|
+
prop.replace(/([A-Z])/g, "-$1").toLowerCase(),
|
|
160
|
+
String(val)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
|
|
168
|
+
"disabled",
|
|
169
|
+
"checked",
|
|
170
|
+
"readonly",
|
|
171
|
+
"hidden",
|
|
172
|
+
"selected",
|
|
173
|
+
"required",
|
|
174
|
+
"multiple",
|
|
175
|
+
"autofocus",
|
|
176
|
+
"autoplay",
|
|
177
|
+
"controls",
|
|
178
|
+
"loop",
|
|
179
|
+
"muted",
|
|
180
|
+
"open",
|
|
181
|
+
"novalidate"
|
|
182
|
+
]);
|
|
183
|
+
if (BOOLEAN_ATTRS.has(name)) {
|
|
184
|
+
if (value) {
|
|
185
|
+
el.setAttribute(name, "");
|
|
186
|
+
if (name in el) {
|
|
187
|
+
el[name] = true;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
el.removeAttribute(name);
|
|
191
|
+
if (name in el) {
|
|
192
|
+
el[name] = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (name.startsWith("data-")) {
|
|
198
|
+
const key = name.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
199
|
+
el.dataset[key] = value == null ? "" : String(value);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (value == null || value === false) {
|
|
203
|
+
el.removeAttribute(name);
|
|
204
|
+
} else {
|
|
205
|
+
el.setAttribute(name, value === true ? "" : String(value));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function addEventListener(el, event, handler) {
|
|
209
|
+
el.addEventListener(event, handler);
|
|
210
|
+
return () => {
|
|
211
|
+
el.removeEventListener(event, handler);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function insertBefore(parent, node, anchor) {
|
|
215
|
+
parent.insertBefore(node, anchor);
|
|
216
|
+
}
|
|
217
|
+
function removeNode(node) {
|
|
218
|
+
if (node.parentNode) {
|
|
219
|
+
node.parentNode.removeChild(node);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function appendChild(parent, child) {
|
|
223
|
+
if (isHydrating) {
|
|
224
|
+
if (child.nodeType === 1) {
|
|
225
|
+
exitNode();
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
parent.appendChild(child);
|
|
230
|
+
}
|
|
231
|
+
function createComment(text) {
|
|
232
|
+
if (isHydrating) {
|
|
233
|
+
const node = claimNode();
|
|
234
|
+
if (node && node.nodeType === 8) {
|
|
235
|
+
return node;
|
|
236
|
+
}
|
|
237
|
+
if (typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
|
|
238
|
+
console.warn(`[utopia] Hydration mismatch: expected comment node, got`, node);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return document.createComment(text);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/directives.ts
|
|
245
|
+
import { effect } from "@matthesketh/utopia-core";
|
|
246
|
+
function clearNodes(nodes) {
|
|
247
|
+
for (const node of nodes) {
|
|
248
|
+
removeNode(node);
|
|
249
|
+
}
|
|
250
|
+
nodes.length = 0;
|
|
251
|
+
}
|
|
252
|
+
function createIf(anchor, condition, renderTrue, renderFalse) {
|
|
253
|
+
let currentNodes = [];
|
|
254
|
+
let lastConditionTruthy;
|
|
255
|
+
const parent = anchor.parentNode;
|
|
256
|
+
const dispose = effect(() => {
|
|
257
|
+
const truthy = !!condition();
|
|
258
|
+
if (truthy === lastConditionTruthy) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
lastConditionTruthy = truthy;
|
|
262
|
+
clearNodes(currentNodes);
|
|
263
|
+
if (truthy) {
|
|
264
|
+
const node = renderTrue();
|
|
265
|
+
currentNodes.push(node);
|
|
266
|
+
insertBefore(parent, node, anchor);
|
|
267
|
+
} else if (renderFalse) {
|
|
268
|
+
const node = renderFalse();
|
|
269
|
+
currentNodes.push(node);
|
|
270
|
+
insertBefore(parent, node, anchor);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return () => {
|
|
274
|
+
dispose();
|
|
275
|
+
clearNodes(currentNodes);
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function createFor(anchor, list, renderItem, key) {
|
|
279
|
+
let currentNodes = [];
|
|
280
|
+
const parent = anchor.parentNode;
|
|
281
|
+
void key;
|
|
282
|
+
const dispose = effect(() => {
|
|
283
|
+
const items = list();
|
|
284
|
+
clearNodes(currentNodes);
|
|
285
|
+
for (let i = 0; i < items.length; i++) {
|
|
286
|
+
const node = renderItem(items[i], i);
|
|
287
|
+
currentNodes.push(node);
|
|
288
|
+
insertBefore(parent, node, anchor);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return () => {
|
|
292
|
+
dispose();
|
|
293
|
+
clearNodes(currentNodes);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function createComponent(Component, props, children) {
|
|
297
|
+
const instance = createComponentInstance(Component, props);
|
|
298
|
+
if (children) {
|
|
299
|
+
for (const slotName of Object.keys(children)) {
|
|
300
|
+
instance.slots[slotName] = children[slotName];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const ctx = Component.setup ? Component.setup(instance.props) : {};
|
|
304
|
+
const renderCtx = {
|
|
305
|
+
...ctx,
|
|
306
|
+
$slots: instance.slots
|
|
307
|
+
};
|
|
308
|
+
instance.el = Component.render(renderCtx);
|
|
309
|
+
if (Component.styles) {
|
|
310
|
+
const style = document.createElement("style");
|
|
311
|
+
style.textContent = Component.styles;
|
|
312
|
+
document.head.appendChild(style);
|
|
313
|
+
}
|
|
314
|
+
return instance.el;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/scheduler.ts
|
|
318
|
+
var queue = /* @__PURE__ */ new Set();
|
|
319
|
+
var isFlushing = false;
|
|
320
|
+
var isFlushPending = false;
|
|
321
|
+
var resolvedPromise = Promise.resolve();
|
|
322
|
+
function queueJob(job) {
|
|
323
|
+
queue.add(job);
|
|
324
|
+
if (!isFlushPending && !isFlushing) {
|
|
325
|
+
isFlushPending = true;
|
|
326
|
+
resolvedPromise.then(flushJobs);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function nextTick() {
|
|
330
|
+
return resolvedPromise.then();
|
|
331
|
+
}
|
|
332
|
+
function flushJobs() {
|
|
333
|
+
isFlushPending = false;
|
|
334
|
+
isFlushing = true;
|
|
335
|
+
try {
|
|
336
|
+
for (const job of queue) {
|
|
337
|
+
queue.delete(job);
|
|
338
|
+
job();
|
|
339
|
+
}
|
|
340
|
+
} finally {
|
|
341
|
+
isFlushing = false;
|
|
342
|
+
if (queue.size > 0) {
|
|
343
|
+
isFlushPending = true;
|
|
344
|
+
resolvedPromise.then(flushJobs);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/index.ts
|
|
350
|
+
import { signal, computed, effect as effect2, batch, untrack } from "@matthesketh/utopia-core";
|
|
351
|
+
import { effect as effect3 } from "@matthesketh/utopia-core";
|
|
352
|
+
export {
|
|
353
|
+
addEventListener,
|
|
354
|
+
appendChild,
|
|
355
|
+
batch,
|
|
356
|
+
computed,
|
|
357
|
+
createComment,
|
|
358
|
+
createComponent,
|
|
359
|
+
createComponentInstance,
|
|
360
|
+
effect3 as createEffect,
|
|
361
|
+
createElement,
|
|
362
|
+
createFor,
|
|
363
|
+
createIf,
|
|
364
|
+
createTextNode,
|
|
365
|
+
effect2 as effect,
|
|
366
|
+
hydrate,
|
|
367
|
+
insertBefore,
|
|
368
|
+
mount,
|
|
369
|
+
nextTick,
|
|
370
|
+
queueJob,
|
|
371
|
+
removeNode,
|
|
372
|
+
setAttr,
|
|
373
|
+
setText,
|
|
374
|
+
signal,
|
|
375
|
+
untrack
|
|
376
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@matthesketh/utopia-runtime",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "DOM renderer and component lifecycle for UtopiaJS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Matt <matt@matthesketh.pro>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/wrxck/utopiajs.git",
|
|
11
|
+
"directory": "packages/runtime"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/wrxck/utopiajs/tree/main/packages/runtime",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"runtime",
|
|
16
|
+
"dom",
|
|
17
|
+
"renderer",
|
|
18
|
+
"lifecycle",
|
|
19
|
+
"directives",
|
|
20
|
+
"utopiajs"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20.0.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@matthesketh/utopia-core": "0.0.1"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
47
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
|
48
|
+
}
|
|
49
|
+
}
|