cajoa 0.3.0 → 0.5.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/README.md +58 -5
- package/dist/index.cjs +192 -59
- package/dist/index.d.cts +48 -2
- package/dist/index.d.ts +48 -2
- package/dist/index.js +180 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A small, beginner-friendly frontend framework built on **signals** and **JSX**.
|
|
4
4
|
No virtual DOM, no hooks rules, no re-rendering components.
|
|
5
5
|
|
|
6
|
-
> ⚠️ **v0.
|
|
6
|
+
> ⚠️ **v0.5 (early stage).** The API may still change. Not production-ready yet.
|
|
7
7
|
|
|
8
8
|
## Why Cajoa?
|
|
9
9
|
|
|
@@ -15,7 +15,7 @@ No virtual DOM, no hooks rules, no re-rendering components.
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm
|
|
18
|
+
npm create cajoa@latest my-app
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Setup (JSX)
|
|
@@ -80,6 +80,32 @@ const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
|
|
|
80
80
|
</Show>;
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
## Routing
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { render, Router, Route, Link, useParams } from "cajoa";
|
|
87
|
+
|
|
88
|
+
const User = () => <h1>User {useParams().id}</h1>;
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
() => (
|
|
92
|
+
<div>
|
|
93
|
+
<nav>
|
|
94
|
+
<Link href="/">Home</Link> <Link href="/users/7">User 7</Link>
|
|
95
|
+
</nav>
|
|
96
|
+
<Router>
|
|
97
|
+
<Route path="/" component={() => <h1>Home</h1>} />
|
|
98
|
+
<Route path="/users/:id" component={User} />
|
|
99
|
+
<Route path="*" component={() => <h1>Not found</h1>} />
|
|
100
|
+
</Router>
|
|
101
|
+
</div>
|
|
102
|
+
),
|
|
103
|
+
document.getElementById("app")!,
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Routes are matched in order, so put `path="*"` last.
|
|
108
|
+
|
|
83
109
|
## API
|
|
84
110
|
|
|
85
111
|
### Reactivity
|
|
@@ -121,18 +147,42 @@ const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
|
|
|
121
147
|
| `provide(ctx, value, fn)` | Runs `fn`; everything created inside can read `value`. |
|
|
122
148
|
| `useContext(ctx)` | Reads the nearest provided value (or the default). |
|
|
123
149
|
|
|
150
|
+
### Router
|
|
151
|
+
|
|
152
|
+
| Function | What it does |
|
|
153
|
+
| ---------------------------- | ---------------------------------------------------------- |
|
|
154
|
+
| `<Router>` | Renders the first `<Route>` that matches the current path. |
|
|
155
|
+
| `<Route path component>` | Maps a path (`/users/:id`, `*`) to a component. |
|
|
156
|
+
| `<Link href>` | An `<a>` that navigates without reloading the page. |
|
|
157
|
+
| `navigate(to, { replace? })` | Navigates programmatically. |
|
|
158
|
+
| `useParams()` | Reads the route params (`{ id: "7" }`). |
|
|
159
|
+
| `usePath()` | Returns a signal getter for the current pathname. |
|
|
160
|
+
| `untrack(fn)` | Runs `fn` without subscribing to signals. |
|
|
161
|
+
|
|
162
|
+
### Renderer
|
|
163
|
+
|
|
164
|
+
| Function | What it does |
|
|
165
|
+
| ----------------------- | ------------------------------------------------------------------ |
|
|
166
|
+
| `setRenderer(renderer)` | Swaps the renderer (default: DOM). Used to target other platforms. |
|
|
167
|
+
| `getRenderer()` | Returns the current renderer. |
|
|
168
|
+
| `domRenderer` | The built-in DOM renderer. |
|
|
169
|
+
|
|
124
170
|
## Good to know
|
|
125
171
|
|
|
126
172
|
- `For` uses the item itself as the key by default. If you pass `key`, items with the same key are **not** re-rendered when their data changes, so keys must be unique and the item should hold its own signals if it needs to update.
|
|
127
173
|
- `useContext` only works while a component is being created (or inside `onMount` / effects). Read it at the top of your component and use the value later in handlers.
|
|
128
174
|
- Updating an object in place will not trigger effects. Set a new object instead.
|
|
175
|
+
- Fragments (`<>...</>`) work anywhere, but inside `<For>` the child must be a single element, because `For` reuses that node.
|
|
129
176
|
|
|
130
177
|
## Known limitations
|
|
131
178
|
|
|
132
|
-
- No Fragments (`<>...</>`) yet. Wrap siblings in an element.
|
|
133
179
|
- No SSR.
|
|
180
|
+
- Router: pathname only (no query/hash handling), no nested routes, no `base` path.
|
|
181
|
+
- `<Show>` and `<Router>` return a function, so they must sit inside an element, not be the root returned by `render`.
|
|
182
|
+
- Single-page apps need a server fallback to `index.html` for unknown paths.
|
|
134
183
|
- `createMemo` can show a brief inconsistent state when memos depend on each other.
|
|
135
184
|
- `set` calls are not batched automatically inside event handlers. Wrap them in `batch()`.
|
|
185
|
+
- The Router and event names (`onClick`) are still web-specific for now.
|
|
136
186
|
|
|
137
187
|
## Roadmap
|
|
138
188
|
|
|
@@ -143,9 +193,12 @@ const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
|
|
|
143
193
|
- [x] `createMemo`, `batch`
|
|
144
194
|
- [x] Context
|
|
145
195
|
- [x] JSX support
|
|
146
|
-
- [
|
|
196
|
+
- [x] Fragments
|
|
197
|
+
- [x] Router
|
|
198
|
+
- [x] Renderer abstraction (core no longer depends on DOM)
|
|
199
|
+
- [ ] Split into packages ('@cajoa/core', '@cajoa/dom')
|
|
147
200
|
- [ ] Vite plugin (removes the two rules above)
|
|
148
|
-
- [ ]
|
|
201
|
+
- [ ] Native renderer (mobile, desktop)
|
|
149
202
|
- [ ] SSR
|
|
150
203
|
|
|
151
204
|
## Development
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
For: () => For,
|
|
24
|
+
Fragment: () => Fragment,
|
|
25
|
+
Link: () => Link,
|
|
26
|
+
Route: () => Route,
|
|
27
|
+
Router: () => Router,
|
|
24
28
|
Show: () => Show,
|
|
25
29
|
batch: () => batch,
|
|
26
30
|
createContext: () => createContext,
|
|
@@ -28,15 +32,22 @@ __export(index_exports, {
|
|
|
28
32
|
createMemo: () => createMemo,
|
|
29
33
|
createRoot: () => createRoot,
|
|
30
34
|
createSignal: () => createSignal,
|
|
35
|
+
domRenderer: () => domRenderer,
|
|
36
|
+
getRenderer: () => getRenderer,
|
|
31
37
|
h: () => h,
|
|
32
38
|
jsx: () => jsx,
|
|
33
39
|
jsxDEV: () => jsxDEV,
|
|
34
40
|
jsxs: () => jsxs,
|
|
41
|
+
navigate: () => navigate,
|
|
35
42
|
onCleanup: () => onCleanup,
|
|
36
43
|
onMount: () => onMount,
|
|
37
44
|
provide: () => provide,
|
|
38
45
|
render: () => render,
|
|
39
|
-
|
|
46
|
+
setRenderer: () => setRenderer,
|
|
47
|
+
untrack: () => untrack,
|
|
48
|
+
useContext: () => useContext,
|
|
49
|
+
useParams: () => useParams,
|
|
50
|
+
usePath: () => usePath
|
|
40
51
|
});
|
|
41
52
|
module.exports = __toCommonJS(index_exports);
|
|
42
53
|
|
|
@@ -172,91 +183,134 @@ function useContext(ctx) {
|
|
|
172
183
|
}
|
|
173
184
|
return ctx.defaultValue;
|
|
174
185
|
}
|
|
186
|
+
function untrack(fn) {
|
|
187
|
+
const prev = currentEffect;
|
|
188
|
+
currentEffect = null;
|
|
189
|
+
try {
|
|
190
|
+
return fn();
|
|
191
|
+
} finally {
|
|
192
|
+
currentEffect = prev;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/renderer/dom.ts
|
|
197
|
+
var domRenderer = {
|
|
198
|
+
createElement: (tag) => document.createElement(tag),
|
|
199
|
+
createText: (value) => document.createTextNode(value),
|
|
200
|
+
createMarker: () => document.createComment(""),
|
|
201
|
+
createFragment: () => document.createDocumentFragment(),
|
|
202
|
+
isNode: (v) => typeof Node !== "undefined" && v instanceof Node,
|
|
203
|
+
insert: (parent, node, anchor = null) => {
|
|
204
|
+
parent.insertBefore(node, anchor);
|
|
205
|
+
},
|
|
206
|
+
remove: (node) => {
|
|
207
|
+
node.remove();
|
|
208
|
+
},
|
|
209
|
+
nextSibling: (node) => node.nextSibling,
|
|
210
|
+
parentNode: (node) => node.parentNode,
|
|
211
|
+
clear: (parent) => {
|
|
212
|
+
parent.replaceChildren();
|
|
213
|
+
},
|
|
214
|
+
setProp(el, key, value) {
|
|
215
|
+
const e = el;
|
|
216
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
217
|
+
e.addEventListener(key.slice(2).toLowerCase(), value);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const name = key === "className" ? "class" : key;
|
|
221
|
+
if (value == null || value === false) e.removeAttribute(name);
|
|
222
|
+
else e.setAttribute(name, value === true ? "" : String(value));
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/renderer/index.ts
|
|
227
|
+
var current = domRenderer;
|
|
228
|
+
function setRenderer(renderer) {
|
|
229
|
+
current = renderer;
|
|
230
|
+
}
|
|
231
|
+
function getRenderer() {
|
|
232
|
+
return current;
|
|
233
|
+
}
|
|
175
234
|
|
|
176
235
|
// src/dom/h.ts
|
|
177
236
|
function h(tag, props, ...children) {
|
|
178
|
-
const
|
|
237
|
+
const r = getRenderer();
|
|
238
|
+
const el = r.createElement(tag);
|
|
179
239
|
for (const [key, value] of Object.entries(props ?? {})) {
|
|
180
|
-
|
|
240
|
+
if (typeof value === "function" && !key.startsWith("on")) {
|
|
241
|
+
createEffect(() => r.setProp(el, key, value()));
|
|
242
|
+
} else {
|
|
243
|
+
r.setProp(el, key, value);
|
|
244
|
+
}
|
|
181
245
|
}
|
|
182
246
|
appendChildren(el, children);
|
|
183
247
|
return el;
|
|
184
248
|
}
|
|
185
|
-
function setProp(el, key, value) {
|
|
186
|
-
if (key.startsWith("on") && typeof value === "function") {
|
|
187
|
-
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
188
|
-
} else if (typeof value === "function") {
|
|
189
|
-
createEffect(() => applyAttr(el, key, value()));
|
|
190
|
-
} else {
|
|
191
|
-
applyAttr(el, key, value);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function applyAttr(el, key, value) {
|
|
195
|
-
const name = key === "className" ? "class" : key;
|
|
196
|
-
if (value == null || value === false) el.removeAttribute(name);
|
|
197
|
-
else el.setAttribute(name, value === true ? "" : String(value));
|
|
198
|
-
}
|
|
199
249
|
function toNodes(value) {
|
|
250
|
+
const r = getRenderer();
|
|
200
251
|
const out = [];
|
|
201
252
|
for (const v of [value].flat(Infinity)) {
|
|
202
253
|
if (v == null || typeof v === "boolean") continue;
|
|
203
|
-
out.push(v
|
|
254
|
+
out.push(r.isNode(v) ? v : r.createText(String(v)));
|
|
204
255
|
}
|
|
205
256
|
return out;
|
|
206
257
|
}
|
|
207
|
-
function appendChildren(parent, children) {
|
|
208
|
-
const pending2 = [...children].reverse();
|
|
209
|
-
while (pending2.length > 0) {
|
|
210
|
-
const child = pending2.pop();
|
|
211
|
-
if (Array.isArray(child)) {
|
|
212
|
-
for (let i = child.length - 1; i >= 0; i--) {
|
|
213
|
-
pending2.push(child[i]);
|
|
214
|
-
}
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (typeof child === "function") {
|
|
218
|
-
const start = document.createComment("");
|
|
219
|
-
const end = document.createComment("");
|
|
220
|
-
parent.append(start, end);
|
|
221
|
-
createEffect(() => {
|
|
222
|
-
reconcile(start, end, toNodes(child()));
|
|
223
|
-
});
|
|
224
|
-
} else {
|
|
225
|
-
parent.append(...toNodes(child));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
258
|
function reconcile(start, end, next) {
|
|
259
|
+
const r = getRenderer();
|
|
230
260
|
const keep = new Set(next);
|
|
231
|
-
let cur =
|
|
261
|
+
let cur = r.nextSibling(start);
|
|
232
262
|
while (cur && cur !== end) {
|
|
233
|
-
const after =
|
|
234
|
-
if (!keep.has(cur))
|
|
263
|
+
const after = r.nextSibling(cur);
|
|
264
|
+
if (!keep.has(cur)) r.remove(cur);
|
|
235
265
|
cur = after;
|
|
236
266
|
}
|
|
237
|
-
const parent =
|
|
238
|
-
let ref =
|
|
267
|
+
const parent = r.parentNode(end);
|
|
268
|
+
let ref = r.nextSibling(start);
|
|
239
269
|
for (const node of next) {
|
|
240
|
-
if (node === ref)
|
|
241
|
-
|
|
242
|
-
|
|
270
|
+
if (node === ref) ref = r.nextSibling(ref);
|
|
271
|
+
else r.insert(parent, node, ref);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function appendChildren(parent, children) {
|
|
275
|
+
const r = getRenderer();
|
|
276
|
+
for (const child of children.flat(Infinity)) {
|
|
277
|
+
if (typeof child === "function") {
|
|
278
|
+
const start = r.createMarker();
|
|
279
|
+
const end = r.createMarker();
|
|
280
|
+
r.insert(parent, start);
|
|
281
|
+
r.insert(parent, end);
|
|
282
|
+
createEffect(() => reconcile(start, end, toNodes(child())));
|
|
283
|
+
} else {
|
|
284
|
+
for (const node of toNodes(child)) r.insert(parent, node);
|
|
285
|
+
}
|
|
243
286
|
}
|
|
244
287
|
}
|
|
245
288
|
|
|
246
289
|
// src/dom/render.ts
|
|
247
290
|
function render(component, container) {
|
|
248
291
|
return createRoot((dispose) => {
|
|
249
|
-
|
|
292
|
+
const r = getRenderer();
|
|
293
|
+
const node = component();
|
|
294
|
+
r.clear(container);
|
|
295
|
+
r.insert(container, node);
|
|
250
296
|
return () => {
|
|
251
297
|
dispose();
|
|
252
|
-
|
|
298
|
+
r.clear(container);
|
|
253
299
|
};
|
|
254
300
|
});
|
|
255
301
|
}
|
|
256
302
|
|
|
303
|
+
// src/reactivity/memo.ts
|
|
304
|
+
function createMemo(fn) {
|
|
305
|
+
const [value, setValue] = createSignal(void 0);
|
|
306
|
+
createEffect(() => setValue(() => fn()));
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
|
|
257
310
|
// src/components/show.ts
|
|
258
311
|
function Show(props) {
|
|
259
|
-
|
|
312
|
+
const visible = createMemo(() => !!props.when());
|
|
313
|
+
return () => visible() ? untrack(() => props.children()) : untrack(() => props.fallback?.() ?? null);
|
|
260
314
|
}
|
|
261
315
|
|
|
262
316
|
// src/components/for.ts
|
|
@@ -290,13 +344,6 @@ function For(props) {
|
|
|
290
344
|
};
|
|
291
345
|
}
|
|
292
346
|
|
|
293
|
-
// src/reactivity/memo.ts
|
|
294
|
-
function createMemo(fn) {
|
|
295
|
-
const [value, setValue] = createSignal(void 0);
|
|
296
|
-
createEffect(() => setValue(() => fn()));
|
|
297
|
-
return value;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
347
|
// src/jsx-runtime.ts
|
|
301
348
|
function jsx(type, props) {
|
|
302
349
|
if (typeof type === "function") return type(props ?? {});
|
|
@@ -306,9 +353,88 @@ function jsx(type, props) {
|
|
|
306
353
|
}
|
|
307
354
|
var jsxs = jsx;
|
|
308
355
|
var jsxDEV = jsx;
|
|
356
|
+
function Fragment(props) {
|
|
357
|
+
const frag = getRenderer().createFragment();
|
|
358
|
+
appendChildren(frag, props.children === void 0 ? [] : [props.children]);
|
|
359
|
+
return frag;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/router.ts
|
|
363
|
+
var read = () => typeof location === "undefined" ? "/" : location.pathname;
|
|
364
|
+
var [path, setPath] = createSignal(read());
|
|
365
|
+
var listening = false;
|
|
366
|
+
function listen() {
|
|
367
|
+
if (listening || typeof window === "undefined") return;
|
|
368
|
+
listening = true;
|
|
369
|
+
window.addEventListener("popstate", () => setPath(read()));
|
|
370
|
+
}
|
|
371
|
+
function navigate(to, options = {}) {
|
|
372
|
+
if (new URL(to, location.href).href === location.href) return;
|
|
373
|
+
history[options.replace ? "replaceState" : "pushState"](null, "", to);
|
|
374
|
+
setPath(read());
|
|
375
|
+
}
|
|
376
|
+
var usePath = () => path;
|
|
377
|
+
var Params = createContext({});
|
|
378
|
+
function useParams() {
|
|
379
|
+
return useContext(Params);
|
|
380
|
+
}
|
|
381
|
+
var split = (s) => s.split("/").filter(Boolean);
|
|
382
|
+
function decode(s) {
|
|
383
|
+
try {
|
|
384
|
+
return decodeURIComponent(s);
|
|
385
|
+
} catch {
|
|
386
|
+
return s;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function match(pattern, pathname) {
|
|
390
|
+
if (pattern === "*") return {};
|
|
391
|
+
const p = split(pattern);
|
|
392
|
+
const u = split(pathname);
|
|
393
|
+
if (p.length !== u.length) return null;
|
|
394
|
+
const params = {};
|
|
395
|
+
for (let i = 0; i < p.length; i++) {
|
|
396
|
+
if (p[i].startsWith(":")) params[p[i].slice(1)] = decode(u[i]);
|
|
397
|
+
else if (p[i] !== u[i]) return null;
|
|
398
|
+
}
|
|
399
|
+
return params;
|
|
400
|
+
}
|
|
401
|
+
function Route(props) {
|
|
402
|
+
return { __route: true, path: props.path, component: props.component };
|
|
403
|
+
}
|
|
404
|
+
function Router(props) {
|
|
405
|
+
listen();
|
|
406
|
+
const routes = [props.children].flat(Infinity).filter(
|
|
407
|
+
(r) => r?.__route === true
|
|
408
|
+
);
|
|
409
|
+
return () => {
|
|
410
|
+
const current2 = path();
|
|
411
|
+
for (const route of routes) {
|
|
412
|
+
const params = match(route.path, current2);
|
|
413
|
+
if (params) {
|
|
414
|
+
return untrack(() => provide(Params, params, () => route.component()));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function Link(props) {
|
|
421
|
+
const { href, children, ...rest } = props;
|
|
422
|
+
const onClick = (e) => {
|
|
423
|
+
if (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return;
|
|
424
|
+
if (e.defaultPrevented || e.button !== 0) return;
|
|
425
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
426
|
+
e.preventDefault();
|
|
427
|
+
navigate(href);
|
|
428
|
+
};
|
|
429
|
+
return h("a", { ...rest, href, onClick }, children);
|
|
430
|
+
}
|
|
309
431
|
// Annotate the CommonJS export names for ESM import in node:
|
|
310
432
|
0 && (module.exports = {
|
|
311
433
|
For,
|
|
434
|
+
Fragment,
|
|
435
|
+
Link,
|
|
436
|
+
Route,
|
|
437
|
+
Router,
|
|
312
438
|
Show,
|
|
313
439
|
batch,
|
|
314
440
|
createContext,
|
|
@@ -316,13 +442,20 @@ var jsxDEV = jsx;
|
|
|
316
442
|
createMemo,
|
|
317
443
|
createRoot,
|
|
318
444
|
createSignal,
|
|
445
|
+
domRenderer,
|
|
446
|
+
getRenderer,
|
|
319
447
|
h,
|
|
320
448
|
jsx,
|
|
321
449
|
jsxDEV,
|
|
322
450
|
jsxs,
|
|
451
|
+
navigate,
|
|
323
452
|
onCleanup,
|
|
324
453
|
onMount,
|
|
325
454
|
provide,
|
|
326
455
|
render,
|
|
327
|
-
|
|
456
|
+
setRenderer,
|
|
457
|
+
untrack,
|
|
458
|
+
useContext,
|
|
459
|
+
useParams,
|
|
460
|
+
usePath
|
|
328
461
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -12,6 +12,7 @@ interface Context<T> {
|
|
|
12
12
|
declare function createContext<T>(defaultValue: T): Context<T>;
|
|
13
13
|
declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
|
|
14
14
|
declare function useContext<T>(ctx: Context<T>): T;
|
|
15
|
+
declare function untrack<T>(fn: () => T): T;
|
|
15
16
|
|
|
16
17
|
type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
|
|
17
18
|
declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
|
|
@@ -44,6 +45,51 @@ declare namespace JSX {
|
|
|
44
45
|
children: {};
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
declare function Fragment(props: {
|
|
49
|
+
children?: Child;
|
|
50
|
+
}): any;
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
declare function navigate(to: string, options?: {
|
|
53
|
+
replace?: boolean;
|
|
54
|
+
}): void;
|
|
55
|
+
declare const usePath: () => () => string;
|
|
56
|
+
declare function useParams(): Record<string, string>;
|
|
57
|
+
interface RouteDef {
|
|
58
|
+
__route: true;
|
|
59
|
+
path: string;
|
|
60
|
+
component: () => unknown;
|
|
61
|
+
}
|
|
62
|
+
declare function Route(props: {
|
|
63
|
+
path: string;
|
|
64
|
+
component: () => unknown;
|
|
65
|
+
}): RouteDef;
|
|
66
|
+
declare function Router(props: {
|
|
67
|
+
children?: unknown;
|
|
68
|
+
}): () => unknown;
|
|
69
|
+
declare function Link(props: {
|
|
70
|
+
href: string;
|
|
71
|
+
children?: unknown;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}): HTMLElement;
|
|
74
|
+
|
|
75
|
+
interface Renderer<N = any> {
|
|
76
|
+
createElement(tag: string): N;
|
|
77
|
+
createText(value: string): N;
|
|
78
|
+
createMarker(): N;
|
|
79
|
+
createFragment(): N;
|
|
80
|
+
isNode(value: unknown): value is N;
|
|
81
|
+
insert(parent: N, node: N, anchor?: N | null): void;
|
|
82
|
+
remove(node: N): void;
|
|
83
|
+
nextSibling(node: N): N | null;
|
|
84
|
+
parentNode(node: N): N | null;
|
|
85
|
+
clear(parent: N): void;
|
|
86
|
+
setProp(el: N, name: string, value: unknown): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
declare function setRenderer(renderer: Renderer): void;
|
|
90
|
+
declare function getRenderer(): Renderer;
|
|
91
|
+
|
|
92
|
+
declare const domRenderer: Renderer<Node>;
|
|
93
|
+
|
|
94
|
+
export { For, Fragment, JSX, Link, Route, Router, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, domRenderer, getRenderer, h, jsx, jsxDEV, jsxs, navigate, onCleanup, onMount, provide, render, setRenderer, untrack, useContext, useParams, usePath };
|
|
95
|
+
export type { Child, Context, Renderer, RouteDef };
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ interface Context<T> {
|
|
|
12
12
|
declare function createContext<T>(defaultValue: T): Context<T>;
|
|
13
13
|
declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
|
|
14
14
|
declare function useContext<T>(ctx: Context<T>): T;
|
|
15
|
+
declare function untrack<T>(fn: () => T): T;
|
|
15
16
|
|
|
16
17
|
type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
|
|
17
18
|
declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
|
|
@@ -44,6 +45,51 @@ declare namespace JSX {
|
|
|
44
45
|
children: {};
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
declare function Fragment(props: {
|
|
49
|
+
children?: Child;
|
|
50
|
+
}): any;
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
declare function navigate(to: string, options?: {
|
|
53
|
+
replace?: boolean;
|
|
54
|
+
}): void;
|
|
55
|
+
declare const usePath: () => () => string;
|
|
56
|
+
declare function useParams(): Record<string, string>;
|
|
57
|
+
interface RouteDef {
|
|
58
|
+
__route: true;
|
|
59
|
+
path: string;
|
|
60
|
+
component: () => unknown;
|
|
61
|
+
}
|
|
62
|
+
declare function Route(props: {
|
|
63
|
+
path: string;
|
|
64
|
+
component: () => unknown;
|
|
65
|
+
}): RouteDef;
|
|
66
|
+
declare function Router(props: {
|
|
67
|
+
children?: unknown;
|
|
68
|
+
}): () => unknown;
|
|
69
|
+
declare function Link(props: {
|
|
70
|
+
href: string;
|
|
71
|
+
children?: unknown;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}): HTMLElement;
|
|
74
|
+
|
|
75
|
+
interface Renderer<N = any> {
|
|
76
|
+
createElement(tag: string): N;
|
|
77
|
+
createText(value: string): N;
|
|
78
|
+
createMarker(): N;
|
|
79
|
+
createFragment(): N;
|
|
80
|
+
isNode(value: unknown): value is N;
|
|
81
|
+
insert(parent: N, node: N, anchor?: N | null): void;
|
|
82
|
+
remove(node: N): void;
|
|
83
|
+
nextSibling(node: N): N | null;
|
|
84
|
+
parentNode(node: N): N | null;
|
|
85
|
+
clear(parent: N): void;
|
|
86
|
+
setProp(el: N, name: string, value: unknown): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
declare function setRenderer(renderer: Renderer): void;
|
|
90
|
+
declare function getRenderer(): Renderer;
|
|
91
|
+
|
|
92
|
+
declare const domRenderer: Renderer<Node>;
|
|
93
|
+
|
|
94
|
+
export { For, Fragment, JSX, Link, Route, Router, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, domRenderer, getRenderer, h, jsx, jsxDEV, jsxs, navigate, onCleanup, onMount, provide, render, setRenderer, untrack, useContext, useParams, usePath };
|
|
95
|
+
export type { Child, Context, Renderer, RouteDef };
|
package/dist/index.js
CHANGED
|
@@ -130,91 +130,134 @@ function useContext(ctx) {
|
|
|
130
130
|
}
|
|
131
131
|
return ctx.defaultValue;
|
|
132
132
|
}
|
|
133
|
+
function untrack(fn) {
|
|
134
|
+
const prev = currentEffect;
|
|
135
|
+
currentEffect = null;
|
|
136
|
+
try {
|
|
137
|
+
return fn();
|
|
138
|
+
} finally {
|
|
139
|
+
currentEffect = prev;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/renderer/dom.ts
|
|
144
|
+
var domRenderer = {
|
|
145
|
+
createElement: (tag) => document.createElement(tag),
|
|
146
|
+
createText: (value) => document.createTextNode(value),
|
|
147
|
+
createMarker: () => document.createComment(""),
|
|
148
|
+
createFragment: () => document.createDocumentFragment(),
|
|
149
|
+
isNode: (v) => typeof Node !== "undefined" && v instanceof Node,
|
|
150
|
+
insert: (parent, node, anchor = null) => {
|
|
151
|
+
parent.insertBefore(node, anchor);
|
|
152
|
+
},
|
|
153
|
+
remove: (node) => {
|
|
154
|
+
node.remove();
|
|
155
|
+
},
|
|
156
|
+
nextSibling: (node) => node.nextSibling,
|
|
157
|
+
parentNode: (node) => node.parentNode,
|
|
158
|
+
clear: (parent) => {
|
|
159
|
+
parent.replaceChildren();
|
|
160
|
+
},
|
|
161
|
+
setProp(el, key, value) {
|
|
162
|
+
const e = el;
|
|
163
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
164
|
+
e.addEventListener(key.slice(2).toLowerCase(), value);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const name = key === "className" ? "class" : key;
|
|
168
|
+
if (value == null || value === false) e.removeAttribute(name);
|
|
169
|
+
else e.setAttribute(name, value === true ? "" : String(value));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/renderer/index.ts
|
|
174
|
+
var current = domRenderer;
|
|
175
|
+
function setRenderer(renderer) {
|
|
176
|
+
current = renderer;
|
|
177
|
+
}
|
|
178
|
+
function getRenderer() {
|
|
179
|
+
return current;
|
|
180
|
+
}
|
|
133
181
|
|
|
134
182
|
// src/dom/h.ts
|
|
135
183
|
function h(tag, props, ...children) {
|
|
136
|
-
const
|
|
184
|
+
const r = getRenderer();
|
|
185
|
+
const el = r.createElement(tag);
|
|
137
186
|
for (const [key, value] of Object.entries(props ?? {})) {
|
|
138
|
-
|
|
187
|
+
if (typeof value === "function" && !key.startsWith("on")) {
|
|
188
|
+
createEffect(() => r.setProp(el, key, value()));
|
|
189
|
+
} else {
|
|
190
|
+
r.setProp(el, key, value);
|
|
191
|
+
}
|
|
139
192
|
}
|
|
140
193
|
appendChildren(el, children);
|
|
141
194
|
return el;
|
|
142
195
|
}
|
|
143
|
-
function setProp(el, key, value) {
|
|
144
|
-
if (key.startsWith("on") && typeof value === "function") {
|
|
145
|
-
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
146
|
-
} else if (typeof value === "function") {
|
|
147
|
-
createEffect(() => applyAttr(el, key, value()));
|
|
148
|
-
} else {
|
|
149
|
-
applyAttr(el, key, value);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
function applyAttr(el, key, value) {
|
|
153
|
-
const name = key === "className" ? "class" : key;
|
|
154
|
-
if (value == null || value === false) el.removeAttribute(name);
|
|
155
|
-
else el.setAttribute(name, value === true ? "" : String(value));
|
|
156
|
-
}
|
|
157
196
|
function toNodes(value) {
|
|
197
|
+
const r = getRenderer();
|
|
158
198
|
const out = [];
|
|
159
199
|
for (const v of [value].flat(Infinity)) {
|
|
160
200
|
if (v == null || typeof v === "boolean") continue;
|
|
161
|
-
out.push(v
|
|
201
|
+
out.push(r.isNode(v) ? v : r.createText(String(v)));
|
|
162
202
|
}
|
|
163
203
|
return out;
|
|
164
204
|
}
|
|
165
|
-
function appendChildren(parent, children) {
|
|
166
|
-
const pending2 = [...children].reverse();
|
|
167
|
-
while (pending2.length > 0) {
|
|
168
|
-
const child = pending2.pop();
|
|
169
|
-
if (Array.isArray(child)) {
|
|
170
|
-
for (let i = child.length - 1; i >= 0; i--) {
|
|
171
|
-
pending2.push(child[i]);
|
|
172
|
-
}
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (typeof child === "function") {
|
|
176
|
-
const start = document.createComment("");
|
|
177
|
-
const end = document.createComment("");
|
|
178
|
-
parent.append(start, end);
|
|
179
|
-
createEffect(() => {
|
|
180
|
-
reconcile(start, end, toNodes(child()));
|
|
181
|
-
});
|
|
182
|
-
} else {
|
|
183
|
-
parent.append(...toNodes(child));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
205
|
function reconcile(start, end, next) {
|
|
206
|
+
const r = getRenderer();
|
|
188
207
|
const keep = new Set(next);
|
|
189
|
-
let cur =
|
|
208
|
+
let cur = r.nextSibling(start);
|
|
190
209
|
while (cur && cur !== end) {
|
|
191
|
-
const after =
|
|
192
|
-
if (!keep.has(cur))
|
|
210
|
+
const after = r.nextSibling(cur);
|
|
211
|
+
if (!keep.has(cur)) r.remove(cur);
|
|
193
212
|
cur = after;
|
|
194
213
|
}
|
|
195
|
-
const parent =
|
|
196
|
-
let ref =
|
|
214
|
+
const parent = r.parentNode(end);
|
|
215
|
+
let ref = r.nextSibling(start);
|
|
197
216
|
for (const node of next) {
|
|
198
|
-
if (node === ref)
|
|
199
|
-
|
|
200
|
-
|
|
217
|
+
if (node === ref) ref = r.nextSibling(ref);
|
|
218
|
+
else r.insert(parent, node, ref);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function appendChildren(parent, children) {
|
|
222
|
+
const r = getRenderer();
|
|
223
|
+
for (const child of children.flat(Infinity)) {
|
|
224
|
+
if (typeof child === "function") {
|
|
225
|
+
const start = r.createMarker();
|
|
226
|
+
const end = r.createMarker();
|
|
227
|
+
r.insert(parent, start);
|
|
228
|
+
r.insert(parent, end);
|
|
229
|
+
createEffect(() => reconcile(start, end, toNodes(child())));
|
|
230
|
+
} else {
|
|
231
|
+
for (const node of toNodes(child)) r.insert(parent, node);
|
|
232
|
+
}
|
|
201
233
|
}
|
|
202
234
|
}
|
|
203
235
|
|
|
204
236
|
// src/dom/render.ts
|
|
205
237
|
function render(component, container) {
|
|
206
238
|
return createRoot((dispose) => {
|
|
207
|
-
|
|
239
|
+
const r = getRenderer();
|
|
240
|
+
const node = component();
|
|
241
|
+
r.clear(container);
|
|
242
|
+
r.insert(container, node);
|
|
208
243
|
return () => {
|
|
209
244
|
dispose();
|
|
210
|
-
|
|
245
|
+
r.clear(container);
|
|
211
246
|
};
|
|
212
247
|
});
|
|
213
248
|
}
|
|
214
249
|
|
|
250
|
+
// src/reactivity/memo.ts
|
|
251
|
+
function createMemo(fn) {
|
|
252
|
+
const [value, setValue] = createSignal(void 0);
|
|
253
|
+
createEffect(() => setValue(() => fn()));
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
215
257
|
// src/components/show.ts
|
|
216
258
|
function Show(props) {
|
|
217
|
-
|
|
259
|
+
const visible = createMemo(() => !!props.when());
|
|
260
|
+
return () => visible() ? untrack(() => props.children()) : untrack(() => props.fallback?.() ?? null);
|
|
218
261
|
}
|
|
219
262
|
|
|
220
263
|
// src/components/for.ts
|
|
@@ -248,13 +291,6 @@ function For(props) {
|
|
|
248
291
|
};
|
|
249
292
|
}
|
|
250
293
|
|
|
251
|
-
// src/reactivity/memo.ts
|
|
252
|
-
function createMemo(fn) {
|
|
253
|
-
const [value, setValue] = createSignal(void 0);
|
|
254
|
-
createEffect(() => setValue(() => fn()));
|
|
255
|
-
return value;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
294
|
// src/jsx-runtime.ts
|
|
259
295
|
function jsx(type, props) {
|
|
260
296
|
if (typeof type === "function") return type(props ?? {});
|
|
@@ -264,8 +300,87 @@ function jsx(type, props) {
|
|
|
264
300
|
}
|
|
265
301
|
var jsxs = jsx;
|
|
266
302
|
var jsxDEV = jsx;
|
|
303
|
+
function Fragment(props) {
|
|
304
|
+
const frag = getRenderer().createFragment();
|
|
305
|
+
appendChildren(frag, props.children === void 0 ? [] : [props.children]);
|
|
306
|
+
return frag;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/router.ts
|
|
310
|
+
var read = () => typeof location === "undefined" ? "/" : location.pathname;
|
|
311
|
+
var [path, setPath] = createSignal(read());
|
|
312
|
+
var listening = false;
|
|
313
|
+
function listen() {
|
|
314
|
+
if (listening || typeof window === "undefined") return;
|
|
315
|
+
listening = true;
|
|
316
|
+
window.addEventListener("popstate", () => setPath(read()));
|
|
317
|
+
}
|
|
318
|
+
function navigate(to, options = {}) {
|
|
319
|
+
if (new URL(to, location.href).href === location.href) return;
|
|
320
|
+
history[options.replace ? "replaceState" : "pushState"](null, "", to);
|
|
321
|
+
setPath(read());
|
|
322
|
+
}
|
|
323
|
+
var usePath = () => path;
|
|
324
|
+
var Params = createContext({});
|
|
325
|
+
function useParams() {
|
|
326
|
+
return useContext(Params);
|
|
327
|
+
}
|
|
328
|
+
var split = (s) => s.split("/").filter(Boolean);
|
|
329
|
+
function decode(s) {
|
|
330
|
+
try {
|
|
331
|
+
return decodeURIComponent(s);
|
|
332
|
+
} catch {
|
|
333
|
+
return s;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function match(pattern, pathname) {
|
|
337
|
+
if (pattern === "*") return {};
|
|
338
|
+
const p = split(pattern);
|
|
339
|
+
const u = split(pathname);
|
|
340
|
+
if (p.length !== u.length) return null;
|
|
341
|
+
const params = {};
|
|
342
|
+
for (let i = 0; i < p.length; i++) {
|
|
343
|
+
if (p[i].startsWith(":")) params[p[i].slice(1)] = decode(u[i]);
|
|
344
|
+
else if (p[i] !== u[i]) return null;
|
|
345
|
+
}
|
|
346
|
+
return params;
|
|
347
|
+
}
|
|
348
|
+
function Route(props) {
|
|
349
|
+
return { __route: true, path: props.path, component: props.component };
|
|
350
|
+
}
|
|
351
|
+
function Router(props) {
|
|
352
|
+
listen();
|
|
353
|
+
const routes = [props.children].flat(Infinity).filter(
|
|
354
|
+
(r) => r?.__route === true
|
|
355
|
+
);
|
|
356
|
+
return () => {
|
|
357
|
+
const current2 = path();
|
|
358
|
+
for (const route of routes) {
|
|
359
|
+
const params = match(route.path, current2);
|
|
360
|
+
if (params) {
|
|
361
|
+
return untrack(() => provide(Params, params, () => route.component()));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function Link(props) {
|
|
368
|
+
const { href, children, ...rest } = props;
|
|
369
|
+
const onClick = (e) => {
|
|
370
|
+
if (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return;
|
|
371
|
+
if (e.defaultPrevented || e.button !== 0) return;
|
|
372
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
373
|
+
e.preventDefault();
|
|
374
|
+
navigate(href);
|
|
375
|
+
};
|
|
376
|
+
return h("a", { ...rest, href, onClick }, children);
|
|
377
|
+
}
|
|
267
378
|
export {
|
|
268
379
|
For,
|
|
380
|
+
Fragment,
|
|
381
|
+
Link,
|
|
382
|
+
Route,
|
|
383
|
+
Router,
|
|
269
384
|
Show,
|
|
270
385
|
batch,
|
|
271
386
|
createContext,
|
|
@@ -273,13 +388,20 @@ export {
|
|
|
273
388
|
createMemo,
|
|
274
389
|
createRoot,
|
|
275
390
|
createSignal,
|
|
391
|
+
domRenderer,
|
|
392
|
+
getRenderer,
|
|
276
393
|
h,
|
|
277
394
|
jsx,
|
|
278
395
|
jsxDEV,
|
|
279
396
|
jsxs,
|
|
397
|
+
navigate,
|
|
280
398
|
onCleanup,
|
|
281
399
|
onMount,
|
|
282
400
|
provide,
|
|
283
401
|
render,
|
|
284
|
-
|
|
402
|
+
setRenderer,
|
|
403
|
+
untrack,
|
|
404
|
+
useContext,
|
|
405
|
+
useParams,
|
|
406
|
+
usePath
|
|
285
407
|
};
|