cajoa 0.2.0 → 0.4.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 +105 -43
- package/dist/index.cjs +125 -8
- package/dist/index.d.cts +42 -2
- package/dist/index.d.ts +42 -2
- package/dist/index.js +113 -7
- package/package.json +25 -4
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# Cajoa
|
|
2
2
|
|
|
3
|
-
A small, beginner-friendly frontend framework built on **signals**.
|
|
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.4 (early stage).** The API may still change. Not production-ready yet.
|
|
7
7
|
|
|
8
8
|
## Why Cajoa?
|
|
9
9
|
|
|
10
10
|
- **Components run once.** A component is just a function that builds DOM. Only the parts that depend on a signal update.
|
|
11
|
+
- **Plain JSX.** Write `<button onClick={...}>`, no special syntax to learn.
|
|
11
12
|
- **Tiny and readable.** The whole core is small enough to read in one sitting.
|
|
12
13
|
- **TypeScript-first**, ESM + CommonJS, zero runtime dependencies.
|
|
13
14
|
|
|
@@ -17,54 +18,94 @@ No virtual DOM, no hooks rules, no re-rendering components.
|
|
|
17
18
|
npm install cajoa
|
|
18
19
|
```
|
|
19
20
|
|
|
21
|
+
## Setup (JSX)
|
|
22
|
+
|
|
23
|
+
In your `tsconfig.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"compilerOptions": {
|
|
28
|
+
"jsx": "react-jsx",
|
|
29
|
+
"jsxImportSource": "cajoa"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If you use Vite and your JSX is not transformed, also set it in `vite.config.ts`
|
|
35
|
+
(`oxc: { jsx: { runtime: "automatic", importSource: "cajoa" } }` on newer Vite,
|
|
36
|
+
or `esbuild: { jsx: "automatic", jsxImportSource: "cajoa" }` on older ones).
|
|
37
|
+
|
|
20
38
|
## Quick start
|
|
21
39
|
|
|
22
|
-
```
|
|
23
|
-
import { createSignal,
|
|
40
|
+
```tsx
|
|
41
|
+
import { createSignal, render } from "cajoa";
|
|
24
42
|
|
|
25
43
|
function Counter() {
|
|
26
44
|
const [count, setCount] = createSignal(0);
|
|
27
45
|
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
count,
|
|
33
|
-
" times",
|
|
46
|
+
return (
|
|
47
|
+
<button onClick={() => setCount((c) => c + 1)}>
|
|
48
|
+
Clicked {count} times
|
|
49
|
+
</button>
|
|
34
50
|
);
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
render(Counter, document.getElementById("app")!);
|
|
38
54
|
```
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
## Two rules to remember
|
|
57
|
+
|
|
58
|
+
Cajoa has no compiler, so JSX expressions run exactly once. That gives two rules:
|
|
59
|
+
|
|
60
|
+
1. **Pass the signal, don't call it.**
|
|
61
|
+
`{count}` is reactive. `{count()}` is read once and never updates.
|
|
62
|
+
For computed values, pass a function: `{() => count() * 2}`.
|
|
63
|
+
2. **`Show` children are a function.**
|
|
64
|
+
`<Show when={on}>{() => <p>Hello</p>}</Show>`
|
|
65
|
+
Otherwise the element is created even when it is hidden.
|
|
41
66
|
|
|
42
67
|
## Lists and conditions
|
|
43
68
|
|
|
44
|
-
```
|
|
45
|
-
import { createSignal,
|
|
69
|
+
```tsx
|
|
70
|
+
import { createSignal, Show, For } from "cajoa";
|
|
46
71
|
|
|
47
72
|
const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
|
|
48
73
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
<Show when={() => todos().length > 0} fallback={() => <p>Nothing to do.</p>}>
|
|
75
|
+
{() => (
|
|
76
|
+
<ul>
|
|
77
|
+
<For each={todos}>{(todo) => <li>{todo.text}</li>}</For>
|
|
78
|
+
</ul>
|
|
79
|
+
)}
|
|
80
|
+
</Show>;
|
|
81
|
+
```
|
|
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")!,
|
|
65
104
|
);
|
|
66
105
|
```
|
|
67
106
|
|
|
107
|
+
Routes are matched in order, so put `path="*"` last.
|
|
108
|
+
|
|
68
109
|
## API
|
|
69
110
|
|
|
70
111
|
### Reactivity
|
|
@@ -76,14 +117,19 @@ h(
|
|
|
76
117
|
| `createMemo(fn)` | Cached computed value. Only notifies when the result changes. |
|
|
77
118
|
| `batch(fn)` | Groups several `set` calls into one update. |
|
|
78
119
|
|
|
79
|
-
###
|
|
120
|
+
### Components
|
|
121
|
+
|
|
122
|
+
| Component | What it does |
|
|
123
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `<Show when fallback>` | Conditional rendering. `children` and `fallback` are functions. |
|
|
125
|
+
| `<For each key?>` | List rendering. Reuses DOM nodes for unchanged items. The child is a function that returns a `Node`. |
|
|
80
126
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
127
|
+
### Rendering
|
|
128
|
+
|
|
129
|
+
| Function | What it does |
|
|
130
|
+
| ------------------------------ | ------------------------------------------------- |
|
|
131
|
+
| `render(component, container)` | Mounts a component. Returns a `dispose` function. |
|
|
132
|
+
| `h(tag, props, ...children)` | Low-level element creator. JSX compiles to this. |
|
|
87
133
|
|
|
88
134
|
### Lifecycle
|
|
89
135
|
|
|
@@ -101,30 +147,46 @@ h(
|
|
|
101
147
|
| `provide(ctx, value, fn)` | Runs `fn`; everything created inside can read `value`. |
|
|
102
148
|
| `useContext(ctx)` | Reads the nearest provided value (or the default). |
|
|
103
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
|
+
|
|
104
162
|
## Good to know
|
|
105
163
|
|
|
106
164
|
- `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.
|
|
107
165
|
- `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.
|
|
108
166
|
- Updating an object in place will not trigger effects. Set a new object instead.
|
|
167
|
+
- Fragments (`<>...</>`) work anywhere, but inside `<For>` the child must be a single element, because `For` reuses that node.
|
|
109
168
|
|
|
110
169
|
## Known limitations
|
|
111
170
|
|
|
112
|
-
- No JSX support yet (`h()` only).
|
|
113
171
|
- No SSR.
|
|
172
|
+
- Router: pathname only (no query/hash handling), no nested routes, no `base` path.
|
|
173
|
+
- `<Show>` and `<Router>` return a function, so they must sit inside an element, not be the root returned by `render`.
|
|
174
|
+
- Single-page apps need a server fallback to `index.html` for unknown paths.
|
|
114
175
|
- `createMemo` can show a brief inconsistent state when memos depend on each other.
|
|
115
176
|
- `set` calls are not batched automatically inside event handlers. Wrap them in `batch()`.
|
|
116
177
|
|
|
117
178
|
## Roadmap
|
|
118
179
|
|
|
119
180
|
- [x] Signals and effects
|
|
120
|
-
- [x] DOM bindings
|
|
121
|
-
- [x] `Show`, `For`
|
|
122
|
-
- [x] Lifecycle
|
|
181
|
+
- [x] DOM bindings and `render`
|
|
182
|
+
- [x] `Show`, `For` (keyed)
|
|
183
|
+
- [x] Lifecycle and disposal
|
|
123
184
|
- [x] `createMemo`, `batch`
|
|
124
|
-
- [x] Keyed `For`
|
|
125
185
|
- [x] Context
|
|
126
|
-
- [
|
|
127
|
-
- [
|
|
186
|
+
- [x] JSX support
|
|
187
|
+
- [x] Fragments
|
|
188
|
+
- [ ] Vite plugin (removes the two rules above)
|
|
189
|
+
- [x] Router
|
|
128
190
|
- [ ] SSR
|
|
129
191
|
|
|
130
192
|
## 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,
|
|
@@ -29,11 +33,18 @@ __export(index_exports, {
|
|
|
29
33
|
createRoot: () => createRoot,
|
|
30
34
|
createSignal: () => createSignal,
|
|
31
35
|
h: () => h,
|
|
36
|
+
jsx: () => jsx,
|
|
37
|
+
jsxDEV: () => jsxDEV,
|
|
38
|
+
jsxs: () => jsxs,
|
|
39
|
+
navigate: () => navigate,
|
|
32
40
|
onCleanup: () => onCleanup,
|
|
33
41
|
onMount: () => onMount,
|
|
34
42
|
provide: () => provide,
|
|
35
43
|
render: () => render,
|
|
36
|
-
|
|
44
|
+
untrack: () => untrack,
|
|
45
|
+
useContext: () => useContext,
|
|
46
|
+
useParams: () => useParams,
|
|
47
|
+
usePath: () => usePath
|
|
37
48
|
});
|
|
38
49
|
module.exports = __toCommonJS(index_exports);
|
|
39
50
|
|
|
@@ -169,6 +180,15 @@ function useContext(ctx) {
|
|
|
169
180
|
}
|
|
170
181
|
return ctx.defaultValue;
|
|
171
182
|
}
|
|
183
|
+
function untrack(fn) {
|
|
184
|
+
const prev = currentEffect;
|
|
185
|
+
currentEffect = null;
|
|
186
|
+
try {
|
|
187
|
+
return fn();
|
|
188
|
+
} finally {
|
|
189
|
+
currentEffect = prev;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
172
192
|
|
|
173
193
|
// src/dom/h.ts
|
|
174
194
|
function h(tag, props, ...children) {
|
|
@@ -251,9 +271,17 @@ function render(component, container) {
|
|
|
251
271
|
});
|
|
252
272
|
}
|
|
253
273
|
|
|
274
|
+
// src/reactivity/memo.ts
|
|
275
|
+
function createMemo(fn) {
|
|
276
|
+
const [value, setValue] = createSignal(void 0);
|
|
277
|
+
createEffect(() => setValue(() => fn()));
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
254
281
|
// src/components/show.ts
|
|
255
282
|
function Show(props) {
|
|
256
|
-
|
|
283
|
+
const visible = createMemo(() => !!props.when());
|
|
284
|
+
return () => visible() ? untrack(() => props.children()) : untrack(() => props.fallback?.() ?? null);
|
|
257
285
|
}
|
|
258
286
|
|
|
259
287
|
// src/components/for.ts
|
|
@@ -287,15 +315,97 @@ function For(props) {
|
|
|
287
315
|
};
|
|
288
316
|
}
|
|
289
317
|
|
|
290
|
-
// src/
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
318
|
+
// src/jsx-runtime.ts
|
|
319
|
+
function jsx(type, props) {
|
|
320
|
+
if (typeof type === "function") return type(props ?? {});
|
|
321
|
+
const { children, ...rest } = props ?? {};
|
|
322
|
+
const kids = children === void 0 ? [] : [children];
|
|
323
|
+
return h(type, rest, ...kids);
|
|
324
|
+
}
|
|
325
|
+
var jsxs = jsx;
|
|
326
|
+
var jsxDEV = jsx;
|
|
327
|
+
function Fragment(props) {
|
|
328
|
+
const frag = document.createDocumentFragment();
|
|
329
|
+
appendChildren(frag, props.children === void 0 ? [] : [props.children]);
|
|
330
|
+
return frag;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/router.ts
|
|
334
|
+
var read = () => typeof location === "undefined" ? "/" : location.pathname;
|
|
335
|
+
var [path, setPath] = createSignal(read());
|
|
336
|
+
var listening = false;
|
|
337
|
+
function listen() {
|
|
338
|
+
if (listening || typeof window === "undefined") return;
|
|
339
|
+
listening = true;
|
|
340
|
+
window.addEventListener("popstate", () => setPath(read()));
|
|
341
|
+
}
|
|
342
|
+
function navigate(to, options = {}) {
|
|
343
|
+
if (new URL(to, location.href).href === location.href) return;
|
|
344
|
+
history[options.replace ? "replaceState" : "pushState"](null, "", to);
|
|
345
|
+
setPath(read());
|
|
346
|
+
}
|
|
347
|
+
var usePath = () => path;
|
|
348
|
+
var Params = createContext({});
|
|
349
|
+
function useParams() {
|
|
350
|
+
return useContext(Params);
|
|
351
|
+
}
|
|
352
|
+
var split = (s) => s.split("/").filter(Boolean);
|
|
353
|
+
function decode(s) {
|
|
354
|
+
try {
|
|
355
|
+
return decodeURIComponent(s);
|
|
356
|
+
} catch {
|
|
357
|
+
return s;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function match(pattern, pathname) {
|
|
361
|
+
if (pattern === "*") return {};
|
|
362
|
+
const p = split(pattern);
|
|
363
|
+
const u = split(pathname);
|
|
364
|
+
if (p.length !== u.length) return null;
|
|
365
|
+
const params = {};
|
|
366
|
+
for (let i = 0; i < p.length; i++) {
|
|
367
|
+
if (p[i].startsWith(":")) params[p[i].slice(1)] = decode(u[i]);
|
|
368
|
+
else if (p[i] !== u[i]) return null;
|
|
369
|
+
}
|
|
370
|
+
return params;
|
|
371
|
+
}
|
|
372
|
+
function Route(props) {
|
|
373
|
+
return { __route: true, path: props.path, component: props.component };
|
|
374
|
+
}
|
|
375
|
+
function Router(props) {
|
|
376
|
+
listen();
|
|
377
|
+
const routes = [props.children].flat(Infinity).filter(
|
|
378
|
+
(r) => r?.__route === true
|
|
379
|
+
);
|
|
380
|
+
return () => {
|
|
381
|
+
const current = path();
|
|
382
|
+
for (const route of routes) {
|
|
383
|
+
const params = match(route.path, current);
|
|
384
|
+
if (params) {
|
|
385
|
+
return untrack(() => provide(Params, params, () => route.component()));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function Link(props) {
|
|
392
|
+
const { href, children, ...rest } = props;
|
|
393
|
+
const onClick = (e) => {
|
|
394
|
+
if (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return;
|
|
395
|
+
if (e.defaultPrevented || e.button !== 0) return;
|
|
396
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
navigate(href);
|
|
399
|
+
};
|
|
400
|
+
return h("a", { ...rest, href, onClick }, children);
|
|
295
401
|
}
|
|
296
402
|
// Annotate the CommonJS export names for ESM import in node:
|
|
297
403
|
0 && (module.exports = {
|
|
298
404
|
For,
|
|
405
|
+
Fragment,
|
|
406
|
+
Link,
|
|
407
|
+
Route,
|
|
408
|
+
Router,
|
|
299
409
|
Show,
|
|
300
410
|
batch,
|
|
301
411
|
createContext,
|
|
@@ -304,9 +414,16 @@ function createMemo(fn) {
|
|
|
304
414
|
createRoot,
|
|
305
415
|
createSignal,
|
|
306
416
|
h,
|
|
417
|
+
jsx,
|
|
418
|
+
jsxDEV,
|
|
419
|
+
jsxs,
|
|
420
|
+
navigate,
|
|
307
421
|
onCleanup,
|
|
308
422
|
onMount,
|
|
309
423
|
provide,
|
|
310
424
|
render,
|
|
311
|
-
|
|
425
|
+
untrack,
|
|
426
|
+
useContext,
|
|
427
|
+
useParams,
|
|
428
|
+
usePath
|
|
312
429
|
});
|
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;
|
|
@@ -32,5 +33,44 @@ declare function For<T>(props: {
|
|
|
32
33
|
|
|
33
34
|
declare function createMemo<T>(fn: () => T): () => T;
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
declare function jsx(type: any, props: Record<string, any> | null): any;
|
|
37
|
+
declare const jsxs: typeof jsx;
|
|
38
|
+
declare const jsxDEV: typeof jsx;
|
|
39
|
+
declare namespace JSX {
|
|
40
|
+
type Element = any;
|
|
41
|
+
interface IntrinsicElements {
|
|
42
|
+
[tag: string]: any;
|
|
43
|
+
}
|
|
44
|
+
interface ElementChildrenAttribute {
|
|
45
|
+
children: {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
declare function Fragment(props: {
|
|
49
|
+
children?: Child;
|
|
50
|
+
}): DocumentFragment;
|
|
51
|
+
|
|
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
|
+
export { For, Fragment, JSX, Link, Route, Router, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, jsx, jsxDEV, jsxs, navigate, onCleanup, onMount, provide, render, untrack, useContext, useParams, usePath };
|
|
76
|
+
export type { Child, Context, 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;
|
|
@@ -32,5 +33,44 @@ declare function For<T>(props: {
|
|
|
32
33
|
|
|
33
34
|
declare function createMemo<T>(fn: () => T): () => T;
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
declare function jsx(type: any, props: Record<string, any> | null): any;
|
|
37
|
+
declare const jsxs: typeof jsx;
|
|
38
|
+
declare const jsxDEV: typeof jsx;
|
|
39
|
+
declare namespace JSX {
|
|
40
|
+
type Element = any;
|
|
41
|
+
interface IntrinsicElements {
|
|
42
|
+
[tag: string]: any;
|
|
43
|
+
}
|
|
44
|
+
interface ElementChildrenAttribute {
|
|
45
|
+
children: {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
declare function Fragment(props: {
|
|
49
|
+
children?: Child;
|
|
50
|
+
}): DocumentFragment;
|
|
51
|
+
|
|
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
|
+
export { For, Fragment, JSX, Link, Route, Router, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, jsx, jsxDEV, jsxs, navigate, onCleanup, onMount, provide, render, untrack, useContext, useParams, usePath };
|
|
76
|
+
export type { Child, Context, RouteDef };
|
package/dist/index.js
CHANGED
|
@@ -130,6 +130,15 @@ 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
|
+
}
|
|
133
142
|
|
|
134
143
|
// src/dom/h.ts
|
|
135
144
|
function h(tag, props, ...children) {
|
|
@@ -212,9 +221,17 @@ function render(component, container) {
|
|
|
212
221
|
});
|
|
213
222
|
}
|
|
214
223
|
|
|
224
|
+
// src/reactivity/memo.ts
|
|
225
|
+
function createMemo(fn) {
|
|
226
|
+
const [value, setValue] = createSignal(void 0);
|
|
227
|
+
createEffect(() => setValue(() => fn()));
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
// src/components/show.ts
|
|
216
232
|
function Show(props) {
|
|
217
|
-
|
|
233
|
+
const visible = createMemo(() => !!props.when());
|
|
234
|
+
return () => visible() ? untrack(() => props.children()) : untrack(() => props.fallback?.() ?? null);
|
|
218
235
|
}
|
|
219
236
|
|
|
220
237
|
// src/components/for.ts
|
|
@@ -248,14 +265,96 @@ function For(props) {
|
|
|
248
265
|
};
|
|
249
266
|
}
|
|
250
267
|
|
|
251
|
-
// src/
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
268
|
+
// src/jsx-runtime.ts
|
|
269
|
+
function jsx(type, props) {
|
|
270
|
+
if (typeof type === "function") return type(props ?? {});
|
|
271
|
+
const { children, ...rest } = props ?? {};
|
|
272
|
+
const kids = children === void 0 ? [] : [children];
|
|
273
|
+
return h(type, rest, ...kids);
|
|
274
|
+
}
|
|
275
|
+
var jsxs = jsx;
|
|
276
|
+
var jsxDEV = jsx;
|
|
277
|
+
function Fragment(props) {
|
|
278
|
+
const frag = document.createDocumentFragment();
|
|
279
|
+
appendChildren(frag, props.children === void 0 ? [] : [props.children]);
|
|
280
|
+
return frag;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/router.ts
|
|
284
|
+
var read = () => typeof location === "undefined" ? "/" : location.pathname;
|
|
285
|
+
var [path, setPath] = createSignal(read());
|
|
286
|
+
var listening = false;
|
|
287
|
+
function listen() {
|
|
288
|
+
if (listening || typeof window === "undefined") return;
|
|
289
|
+
listening = true;
|
|
290
|
+
window.addEventListener("popstate", () => setPath(read()));
|
|
291
|
+
}
|
|
292
|
+
function navigate(to, options = {}) {
|
|
293
|
+
if (new URL(to, location.href).href === location.href) return;
|
|
294
|
+
history[options.replace ? "replaceState" : "pushState"](null, "", to);
|
|
295
|
+
setPath(read());
|
|
296
|
+
}
|
|
297
|
+
var usePath = () => path;
|
|
298
|
+
var Params = createContext({});
|
|
299
|
+
function useParams() {
|
|
300
|
+
return useContext(Params);
|
|
301
|
+
}
|
|
302
|
+
var split = (s) => s.split("/").filter(Boolean);
|
|
303
|
+
function decode(s) {
|
|
304
|
+
try {
|
|
305
|
+
return decodeURIComponent(s);
|
|
306
|
+
} catch {
|
|
307
|
+
return s;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function match(pattern, pathname) {
|
|
311
|
+
if (pattern === "*") return {};
|
|
312
|
+
const p = split(pattern);
|
|
313
|
+
const u = split(pathname);
|
|
314
|
+
if (p.length !== u.length) return null;
|
|
315
|
+
const params = {};
|
|
316
|
+
for (let i = 0; i < p.length; i++) {
|
|
317
|
+
if (p[i].startsWith(":")) params[p[i].slice(1)] = decode(u[i]);
|
|
318
|
+
else if (p[i] !== u[i]) return null;
|
|
319
|
+
}
|
|
320
|
+
return params;
|
|
321
|
+
}
|
|
322
|
+
function Route(props) {
|
|
323
|
+
return { __route: true, path: props.path, component: props.component };
|
|
324
|
+
}
|
|
325
|
+
function Router(props) {
|
|
326
|
+
listen();
|
|
327
|
+
const routes = [props.children].flat(Infinity).filter(
|
|
328
|
+
(r) => r?.__route === true
|
|
329
|
+
);
|
|
330
|
+
return () => {
|
|
331
|
+
const current = path();
|
|
332
|
+
for (const route of routes) {
|
|
333
|
+
const params = match(route.path, current);
|
|
334
|
+
if (params) {
|
|
335
|
+
return untrack(() => provide(Params, params, () => route.component()));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function Link(props) {
|
|
342
|
+
const { href, children, ...rest } = props;
|
|
343
|
+
const onClick = (e) => {
|
|
344
|
+
if (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(href)) return;
|
|
345
|
+
if (e.defaultPrevented || e.button !== 0) return;
|
|
346
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
347
|
+
e.preventDefault();
|
|
348
|
+
navigate(href);
|
|
349
|
+
};
|
|
350
|
+
return h("a", { ...rest, href, onClick }, children);
|
|
256
351
|
}
|
|
257
352
|
export {
|
|
258
353
|
For,
|
|
354
|
+
Fragment,
|
|
355
|
+
Link,
|
|
356
|
+
Route,
|
|
357
|
+
Router,
|
|
259
358
|
Show,
|
|
260
359
|
batch,
|
|
261
360
|
createContext,
|
|
@@ -264,9 +363,16 @@ export {
|
|
|
264
363
|
createRoot,
|
|
265
364
|
createSignal,
|
|
266
365
|
h,
|
|
366
|
+
jsx,
|
|
367
|
+
jsxDEV,
|
|
368
|
+
jsxs,
|
|
369
|
+
navigate,
|
|
267
370
|
onCleanup,
|
|
268
371
|
onMount,
|
|
269
372
|
provide,
|
|
270
373
|
render,
|
|
271
|
-
|
|
374
|
+
untrack,
|
|
375
|
+
useContext,
|
|
376
|
+
useParams,
|
|
377
|
+
usePath
|
|
272
378
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cajoa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A beginner-friendly, signals-based frontend framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -16,6 +16,26 @@
|
|
|
16
16
|
"types": "./dist/index.d.cts",
|
|
17
17
|
"default": "./dist/index.cjs"
|
|
18
18
|
}
|
|
19
|
+
},
|
|
20
|
+
"./jsx-runtime": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"./jsx-dev-runtime": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/index.d.cts",
|
|
37
|
+
"default": "./dist/index.cjs"
|
|
38
|
+
}
|
|
19
39
|
}
|
|
20
40
|
},
|
|
21
41
|
"files": [
|
|
@@ -31,7 +51,7 @@
|
|
|
31
51
|
},
|
|
32
52
|
"repository": {
|
|
33
53
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/
|
|
54
|
+
"url": "git+https://github.com/CajoaTS/Cajoa.git"
|
|
35
55
|
},
|
|
36
56
|
"keywords": [
|
|
37
57
|
"frontend",
|
|
@@ -44,10 +64,11 @@
|
|
|
44
64
|
"author": "cjmgrc",
|
|
45
65
|
"license": "Apache-2.0",
|
|
46
66
|
"bugs": {
|
|
47
|
-
"url": "https://github.com/
|
|
67
|
+
"url": "https://github.com/CajoaTS/Cajoa/issues"
|
|
48
68
|
},
|
|
49
|
-
"homepage": "https://github.com/
|
|
69
|
+
"homepage": "https://github.com/CajoaTS/Cajoa#readme",
|
|
50
70
|
"devDependencies": {
|
|
71
|
+
"@types/node": "^26.6.5",
|
|
51
72
|
"jsdom": "^29.1.1",
|
|
52
73
|
"tsup": "^9.0.0",
|
|
53
74
|
"typescript": "^5.9.3",
|