cajoa 0.1.0 → 0.3.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 +109 -33
- package/dist/index.cjs +130 -14
- package/dist/index.d.cts +28 -4
- package/dist/index.d.ts +28 -4
- package/dist/index.js +121 -13
- package/package.json +78 -57
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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.3 (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
|
-
- **
|
|
11
|
+
- **Plain JSX.** Write `<button onClick={...}>`, no special syntax to learn.
|
|
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
|
|
|
14
15
|
## Install
|
|
@@ -17,60 +18,135 @@ 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.
|
|
66
|
+
|
|
67
|
+
## Lists and conditions
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { createSignal, Show, For } from "cajoa";
|
|
71
|
+
|
|
72
|
+
const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
|
|
73
|
+
|
|
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
|
+
```
|
|
41
82
|
|
|
42
83
|
## API
|
|
43
84
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
### Reactivity
|
|
86
|
+
|
|
87
|
+
| Function | What it does |
|
|
88
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `createSignal(value)` | Returns `[get, set]`. Reading `get()` inside an effect subscribes to it. `set` accepts a value or an updater function. |
|
|
90
|
+
| `createEffect(fn)` | Runs `fn` now and again whenever a signal it read changes. |
|
|
91
|
+
| `createMemo(fn)` | Cached computed value. Only notifies when the result changes. |
|
|
92
|
+
| `batch(fn)` | Groups several `set` calls into one update. |
|
|
93
|
+
|
|
94
|
+
### Components
|
|
95
|
+
|
|
96
|
+
| Component | What it does |
|
|
97
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `<Show when fallback>` | Conditional rendering. `children` and `fallback` are functions. |
|
|
99
|
+
| `<For each key?>` | List rendering. Reuses DOM nodes for unchanged items. The child is a function that returns a `Node`. |
|
|
100
|
+
|
|
101
|
+
### Rendering
|
|
102
|
+
|
|
103
|
+
| Function | What it does |
|
|
104
|
+
| ------------------------------ | ------------------------------------------------- |
|
|
105
|
+
| `render(component, container)` | Mounts a component. Returns a `dispose` function. |
|
|
106
|
+
| `h(tag, props, ...children)` | Low-level element creator. JSX compiles to this. |
|
|
107
|
+
|
|
108
|
+
### Lifecycle
|
|
109
|
+
|
|
110
|
+
| Function | What it does |
|
|
111
|
+
| ---------------- | ----------------------------------------------------------------- |
|
|
112
|
+
| `onMount(fn)` | Runs `fn` after the component is rendered. |
|
|
113
|
+
| `onCleanup(fn)` | Runs `fn` before an effect re-runs or when its owner is disposed. |
|
|
114
|
+
| `createRoot(fn)` | Creates a top-level owner you can dispose manually. |
|
|
115
|
+
|
|
116
|
+
### Context
|
|
117
|
+
|
|
118
|
+
| Function | What it does |
|
|
119
|
+
| ------------------------- | ------------------------------------------------------ |
|
|
120
|
+
| `createContext(default)` | Creates a context with a default value. |
|
|
121
|
+
| `provide(ctx, value, fn)` | Runs `fn`; everything created inside can read `value`. |
|
|
122
|
+
| `useContext(ctx)` | Reads the nearest provided value (or the default). |
|
|
123
|
+
|
|
124
|
+
## Good to know
|
|
125
|
+
|
|
126
|
+
- `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
|
+
- `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
|
+
- Updating an object in place will not trigger effects. Set a new object instead.
|
|
55
129
|
|
|
56
130
|
## Known limitations
|
|
57
131
|
|
|
58
|
-
-
|
|
59
|
-
- No `createMemo`, `batch`, or context yet.
|
|
60
|
-
- No JSX support yet (`h()` only).
|
|
132
|
+
- No Fragments (`<>...</>`) yet. Wrap siblings in an element.
|
|
61
133
|
- No SSR.
|
|
134
|
+
- `createMemo` can show a brief inconsistent state when memos depend on each other.
|
|
135
|
+
- `set` calls are not batched automatically inside event handlers. Wrap them in `batch()`.
|
|
62
136
|
|
|
63
137
|
## Roadmap
|
|
64
138
|
|
|
65
139
|
- [x] Signals and effects
|
|
66
|
-
- [x] DOM bindings
|
|
67
|
-
- [x] `Show`, `For`
|
|
68
|
-
- [x] Lifecycle
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
72
|
-
- [ ]
|
|
140
|
+
- [x] DOM bindings and `render`
|
|
141
|
+
- [x] `Show`, `For` (keyed)
|
|
142
|
+
- [x] Lifecycle and disposal
|
|
143
|
+
- [x] `createMemo`, `batch`
|
|
144
|
+
- [x] Context
|
|
145
|
+
- [x] JSX support
|
|
146
|
+
- [ ] Fragments
|
|
147
|
+
- [ ] Vite plugin (removes the two rules above)
|
|
73
148
|
- [ ] Router
|
|
149
|
+
- [ ] SSR
|
|
74
150
|
|
|
75
151
|
## Development
|
|
76
152
|
|
package/dist/index.cjs
CHANGED
|
@@ -22,13 +22,21 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
For: () => For,
|
|
24
24
|
Show: () => Show,
|
|
25
|
+
batch: () => batch,
|
|
26
|
+
createContext: () => createContext,
|
|
25
27
|
createEffect: () => createEffect,
|
|
28
|
+
createMemo: () => createMemo,
|
|
26
29
|
createRoot: () => createRoot,
|
|
27
30
|
createSignal: () => createSignal,
|
|
28
31
|
h: () => h,
|
|
32
|
+
jsx: () => jsx,
|
|
33
|
+
jsxDEV: () => jsxDEV,
|
|
34
|
+
jsxs: () => jsxs,
|
|
29
35
|
onCleanup: () => onCleanup,
|
|
30
36
|
onMount: () => onMount,
|
|
31
|
-
|
|
37
|
+
provide: () => provide,
|
|
38
|
+
render: () => render,
|
|
39
|
+
useContext: () => useContext
|
|
32
40
|
});
|
|
33
41
|
module.exports = __toCommonJS(index_exports);
|
|
34
42
|
|
|
@@ -45,6 +53,22 @@ function cleanComputation(c) {
|
|
|
45
53
|
c.deps.clear();
|
|
46
54
|
clean(c);
|
|
47
55
|
}
|
|
56
|
+
var batchDepth = 0;
|
|
57
|
+
var pending = /* @__PURE__ */ new Set();
|
|
58
|
+
function flush() {
|
|
59
|
+
const effects = [...pending];
|
|
60
|
+
pending.clear();
|
|
61
|
+
effects.forEach((effect) => effect.run());
|
|
62
|
+
}
|
|
63
|
+
function batch(fn) {
|
|
64
|
+
batchDepth++;
|
|
65
|
+
try {
|
|
66
|
+
return fn();
|
|
67
|
+
} finally {
|
|
68
|
+
batchDepth--;
|
|
69
|
+
if (batchDepth === 0) flush();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
48
72
|
function createSignal(value) {
|
|
49
73
|
const subs = /* @__PURE__ */ new Set();
|
|
50
74
|
const get = () => {
|
|
@@ -58,13 +82,18 @@ function createSignal(value) {
|
|
|
58
82
|
const v = typeof next === "function" ? next(value) : next;
|
|
59
83
|
if (Object.is(v, value)) return;
|
|
60
84
|
value = v;
|
|
61
|
-
[...subs]
|
|
85
|
+
for (const effect of [...subs]) {
|
|
86
|
+
if (batchDepth > 0)
|
|
87
|
+
pending.add(effect);
|
|
88
|
+
else effect.run();
|
|
89
|
+
}
|
|
62
90
|
};
|
|
63
91
|
return [get, set];
|
|
64
92
|
}
|
|
65
93
|
function createEffect(fn) {
|
|
66
94
|
const effect = {
|
|
67
95
|
cleanups: [],
|
|
96
|
+
parent: currentOwner,
|
|
68
97
|
deps: /* @__PURE__ */ new Set(),
|
|
69
98
|
disposed: false,
|
|
70
99
|
run() {
|
|
@@ -92,7 +121,7 @@ function onCleanup(fn) {
|
|
|
92
121
|
currentOwner?.cleanups.push(fn);
|
|
93
122
|
}
|
|
94
123
|
function createRoot(fn) {
|
|
95
|
-
const root = { cleanups: [] };
|
|
124
|
+
const root = { cleanups: [], parent: currentOwner };
|
|
96
125
|
const prevOwner = currentOwner;
|
|
97
126
|
const prevEffect = currentEffect;
|
|
98
127
|
currentOwner = root;
|
|
@@ -119,6 +148,30 @@ function onMount(fn) {
|
|
|
119
148
|
}
|
|
120
149
|
});
|
|
121
150
|
}
|
|
151
|
+
function createContext(defaultValue) {
|
|
152
|
+
return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
|
|
153
|
+
}
|
|
154
|
+
function provide(ctx, value, fn) {
|
|
155
|
+
const owner = {
|
|
156
|
+
cleanups: [],
|
|
157
|
+
parent: currentOwner,
|
|
158
|
+
context: /* @__PURE__ */ new Map([[ctx.id, value]])
|
|
159
|
+
};
|
|
160
|
+
onCleanup(() => clean(owner));
|
|
161
|
+
const prev = currentOwner;
|
|
162
|
+
currentOwner = owner;
|
|
163
|
+
try {
|
|
164
|
+
return fn();
|
|
165
|
+
} finally {
|
|
166
|
+
currentOwner = prev;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function useContext(ctx) {
|
|
170
|
+
for (let o = currentOwner; o; o = o.parent) {
|
|
171
|
+
if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
|
|
172
|
+
}
|
|
173
|
+
return ctx.defaultValue;
|
|
174
|
+
}
|
|
122
175
|
|
|
123
176
|
// src/dom/h.ts
|
|
124
177
|
function h(tag, props, ...children) {
|
|
@@ -152,12 +205,12 @@ function toNodes(value) {
|
|
|
152
205
|
return out;
|
|
153
206
|
}
|
|
154
207
|
function appendChildren(parent, children) {
|
|
155
|
-
const
|
|
156
|
-
while (
|
|
157
|
-
const child =
|
|
208
|
+
const pending2 = [...children].reverse();
|
|
209
|
+
while (pending2.length > 0) {
|
|
210
|
+
const child = pending2.pop();
|
|
158
211
|
if (Array.isArray(child)) {
|
|
159
212
|
for (let i = child.length - 1; i >= 0; i--) {
|
|
160
|
-
|
|
213
|
+
pending2.push(child[i]);
|
|
161
214
|
}
|
|
162
215
|
continue;
|
|
163
216
|
}
|
|
@@ -166,17 +219,29 @@ function appendChildren(parent, children) {
|
|
|
166
219
|
const end = document.createComment("");
|
|
167
220
|
parent.append(start, end);
|
|
168
221
|
createEffect(() => {
|
|
169
|
-
|
|
170
|
-
while (start.nextSibling && start.nextSibling !== end) {
|
|
171
|
-
start.nextSibling.remove();
|
|
172
|
-
}
|
|
173
|
-
end.before(...nodes);
|
|
222
|
+
reconcile(start, end, toNodes(child()));
|
|
174
223
|
});
|
|
175
224
|
} else {
|
|
176
225
|
parent.append(...toNodes(child));
|
|
177
226
|
}
|
|
178
227
|
}
|
|
179
228
|
}
|
|
229
|
+
function reconcile(start, end, next) {
|
|
230
|
+
const keep = new Set(next);
|
|
231
|
+
let cur = start.nextSibling;
|
|
232
|
+
while (cur && cur !== end) {
|
|
233
|
+
const after = cur.nextSibling;
|
|
234
|
+
if (!keep.has(cur)) cur.remove();
|
|
235
|
+
cur = after;
|
|
236
|
+
}
|
|
237
|
+
const parent = end.parentNode;
|
|
238
|
+
let ref = start.nextSibling;
|
|
239
|
+
for (const node of next) {
|
|
240
|
+
if (node === ref)
|
|
241
|
+
ref = ref.nextSibling;
|
|
242
|
+
else parent.insertBefore(node, ref);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
180
245
|
|
|
181
246
|
// src/dom/render.ts
|
|
182
247
|
function render(component, container) {
|
|
@@ -196,17 +261,68 @@ function Show(props) {
|
|
|
196
261
|
|
|
197
262
|
// src/components/for.ts
|
|
198
263
|
function For(props) {
|
|
199
|
-
|
|
264
|
+
let cache = /* @__PURE__ */ new Map();
|
|
265
|
+
onCleanup(() => {
|
|
266
|
+
cache.forEach((e) => e.dispose());
|
|
267
|
+
cache.clear();
|
|
268
|
+
});
|
|
269
|
+
return () => {
|
|
270
|
+
const items = props.each();
|
|
271
|
+
const next = /* @__PURE__ */ new Map();
|
|
272
|
+
const nodes = [];
|
|
273
|
+
items.forEach((item, i) => {
|
|
274
|
+
const k = props.key ? props.key(item, i) : item;
|
|
275
|
+
let entry = cache.get(k);
|
|
276
|
+
if (!entry) {
|
|
277
|
+
entry = createRoot((dispose) => ({
|
|
278
|
+
node: props.children(item, i),
|
|
279
|
+
dispose
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
next.set(k, entry);
|
|
283
|
+
nodes.push(entry.node);
|
|
284
|
+
});
|
|
285
|
+
cache.forEach((entry, k) => {
|
|
286
|
+
if (!next.has(k)) entry.dispose();
|
|
287
|
+
});
|
|
288
|
+
cache = next;
|
|
289
|
+
return nodes;
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
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
|
+
// src/jsx-runtime.ts
|
|
301
|
+
function jsx(type, props) {
|
|
302
|
+
if (typeof type === "function") return type(props ?? {});
|
|
303
|
+
const { children, ...rest } = props ?? {};
|
|
304
|
+
const kids = children === void 0 ? [] : [children];
|
|
305
|
+
return h(type, rest, ...kids);
|
|
200
306
|
}
|
|
307
|
+
var jsxs = jsx;
|
|
308
|
+
var jsxDEV = jsx;
|
|
201
309
|
// Annotate the CommonJS export names for ESM import in node:
|
|
202
310
|
0 && (module.exports = {
|
|
203
311
|
For,
|
|
204
312
|
Show,
|
|
313
|
+
batch,
|
|
314
|
+
createContext,
|
|
205
315
|
createEffect,
|
|
316
|
+
createMemo,
|
|
206
317
|
createRoot,
|
|
207
318
|
createSignal,
|
|
208
319
|
h,
|
|
320
|
+
jsx,
|
|
321
|
+
jsxDEV,
|
|
322
|
+
jsxs,
|
|
209
323
|
onCleanup,
|
|
210
324
|
onMount,
|
|
211
|
-
|
|
325
|
+
provide,
|
|
326
|
+
render,
|
|
327
|
+
useContext
|
|
212
328
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
type Cleanup = () => void;
|
|
2
|
+
declare function batch<T>(fn: () => T): T;
|
|
2
3
|
declare function createSignal<T>(value: T): readonly [() => T, (next: T | ((prev: T) => T)) => void];
|
|
3
4
|
declare function createEffect(fn: () => void): void;
|
|
4
5
|
declare function onCleanup(fn: Cleanup): void;
|
|
5
6
|
declare function createRoot<T>(fn: (dispose: () => void) => T): T;
|
|
6
7
|
declare function onMount(fn: () => void): void;
|
|
8
|
+
interface Context<T> {
|
|
9
|
+
readonly id: symbol;
|
|
10
|
+
readonly defaultValue: T;
|
|
11
|
+
}
|
|
12
|
+
declare function createContext<T>(defaultValue: T): Context<T>;
|
|
13
|
+
declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
|
|
14
|
+
declare function useContext<T>(ctx: Context<T>): T;
|
|
7
15
|
|
|
8
16
|
type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
|
|
9
17
|
declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
|
|
@@ -18,8 +26,24 @@ declare function Show<T>(props: {
|
|
|
18
26
|
|
|
19
27
|
declare function For<T>(props: {
|
|
20
28
|
each: () => T[];
|
|
21
|
-
children: (item: T, index: number) =>
|
|
22
|
-
|
|
29
|
+
children: (item: T, index: number) => Node;
|
|
30
|
+
key?: (item: T, index: number) => unknown;
|
|
31
|
+
}): () => Node[];
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
declare function createMemo<T>(fn: () => T): () => T;
|
|
34
|
+
|
|
35
|
+
declare function jsx(type: any, props: Record<string, any> | null): any;
|
|
36
|
+
declare const jsxs: typeof jsx;
|
|
37
|
+
declare const jsxDEV: typeof jsx;
|
|
38
|
+
declare namespace JSX {
|
|
39
|
+
type Element = any;
|
|
40
|
+
interface IntrinsicElements {
|
|
41
|
+
[tag: string]: any;
|
|
42
|
+
}
|
|
43
|
+
interface ElementChildrenAttribute {
|
|
44
|
+
children: {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { For, JSX, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, useContext };
|
|
49
|
+
export type { Child, Context };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
type Cleanup = () => void;
|
|
2
|
+
declare function batch<T>(fn: () => T): T;
|
|
2
3
|
declare function createSignal<T>(value: T): readonly [() => T, (next: T | ((prev: T) => T)) => void];
|
|
3
4
|
declare function createEffect(fn: () => void): void;
|
|
4
5
|
declare function onCleanup(fn: Cleanup): void;
|
|
5
6
|
declare function createRoot<T>(fn: (dispose: () => void) => T): T;
|
|
6
7
|
declare function onMount(fn: () => void): void;
|
|
8
|
+
interface Context<T> {
|
|
9
|
+
readonly id: symbol;
|
|
10
|
+
readonly defaultValue: T;
|
|
11
|
+
}
|
|
12
|
+
declare function createContext<T>(defaultValue: T): Context<T>;
|
|
13
|
+
declare function provide<T, R>(ctx: Context<T>, value: T, fn: () => R): R;
|
|
14
|
+
declare function useContext<T>(ctx: Context<T>): T;
|
|
7
15
|
|
|
8
16
|
type Child = string | number | boolean | null | undefined | Node | (() => unknown) | Child[];
|
|
9
17
|
declare function h(tag: string, props: Record<string, any> | null, ...children: Child[]): HTMLElement;
|
|
@@ -18,8 +26,24 @@ declare function Show<T>(props: {
|
|
|
18
26
|
|
|
19
27
|
declare function For<T>(props: {
|
|
20
28
|
each: () => T[];
|
|
21
|
-
children: (item: T, index: number) =>
|
|
22
|
-
|
|
29
|
+
children: (item: T, index: number) => Node;
|
|
30
|
+
key?: (item: T, index: number) => unknown;
|
|
31
|
+
}): () => Node[];
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
declare function createMemo<T>(fn: () => T): () => T;
|
|
34
|
+
|
|
35
|
+
declare function jsx(type: any, props: Record<string, any> | null): any;
|
|
36
|
+
declare const jsxs: typeof jsx;
|
|
37
|
+
declare const jsxDEV: typeof jsx;
|
|
38
|
+
declare namespace JSX {
|
|
39
|
+
type Element = any;
|
|
40
|
+
interface IntrinsicElements {
|
|
41
|
+
[tag: string]: any;
|
|
42
|
+
}
|
|
43
|
+
interface ElementChildrenAttribute {
|
|
44
|
+
children: {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { For, JSX, Show, batch, createContext, createEffect, createMemo, createRoot, createSignal, h, jsx, jsxDEV, jsxs, onCleanup, onMount, provide, render, useContext };
|
|
49
|
+
export type { Child, Context };
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,22 @@ function cleanComputation(c) {
|
|
|
11
11
|
c.deps.clear();
|
|
12
12
|
clean(c);
|
|
13
13
|
}
|
|
14
|
+
var batchDepth = 0;
|
|
15
|
+
var pending = /* @__PURE__ */ new Set();
|
|
16
|
+
function flush() {
|
|
17
|
+
const effects = [...pending];
|
|
18
|
+
pending.clear();
|
|
19
|
+
effects.forEach((effect) => effect.run());
|
|
20
|
+
}
|
|
21
|
+
function batch(fn) {
|
|
22
|
+
batchDepth++;
|
|
23
|
+
try {
|
|
24
|
+
return fn();
|
|
25
|
+
} finally {
|
|
26
|
+
batchDepth--;
|
|
27
|
+
if (batchDepth === 0) flush();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
function createSignal(value) {
|
|
15
31
|
const subs = /* @__PURE__ */ new Set();
|
|
16
32
|
const get = () => {
|
|
@@ -24,13 +40,18 @@ function createSignal(value) {
|
|
|
24
40
|
const v = typeof next === "function" ? next(value) : next;
|
|
25
41
|
if (Object.is(v, value)) return;
|
|
26
42
|
value = v;
|
|
27
|
-
[...subs]
|
|
43
|
+
for (const effect of [...subs]) {
|
|
44
|
+
if (batchDepth > 0)
|
|
45
|
+
pending.add(effect);
|
|
46
|
+
else effect.run();
|
|
47
|
+
}
|
|
28
48
|
};
|
|
29
49
|
return [get, set];
|
|
30
50
|
}
|
|
31
51
|
function createEffect(fn) {
|
|
32
52
|
const effect = {
|
|
33
53
|
cleanups: [],
|
|
54
|
+
parent: currentOwner,
|
|
34
55
|
deps: /* @__PURE__ */ new Set(),
|
|
35
56
|
disposed: false,
|
|
36
57
|
run() {
|
|
@@ -58,7 +79,7 @@ function onCleanup(fn) {
|
|
|
58
79
|
currentOwner?.cleanups.push(fn);
|
|
59
80
|
}
|
|
60
81
|
function createRoot(fn) {
|
|
61
|
-
const root = { cleanups: [] };
|
|
82
|
+
const root = { cleanups: [], parent: currentOwner };
|
|
62
83
|
const prevOwner = currentOwner;
|
|
63
84
|
const prevEffect = currentEffect;
|
|
64
85
|
currentOwner = root;
|
|
@@ -85,6 +106,30 @@ function onMount(fn) {
|
|
|
85
106
|
}
|
|
86
107
|
});
|
|
87
108
|
}
|
|
109
|
+
function createContext(defaultValue) {
|
|
110
|
+
return { id: /* @__PURE__ */ Symbol("cajoa-context"), defaultValue };
|
|
111
|
+
}
|
|
112
|
+
function provide(ctx, value, fn) {
|
|
113
|
+
const owner = {
|
|
114
|
+
cleanups: [],
|
|
115
|
+
parent: currentOwner,
|
|
116
|
+
context: /* @__PURE__ */ new Map([[ctx.id, value]])
|
|
117
|
+
};
|
|
118
|
+
onCleanup(() => clean(owner));
|
|
119
|
+
const prev = currentOwner;
|
|
120
|
+
currentOwner = owner;
|
|
121
|
+
try {
|
|
122
|
+
return fn();
|
|
123
|
+
} finally {
|
|
124
|
+
currentOwner = prev;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function useContext(ctx) {
|
|
128
|
+
for (let o = currentOwner; o; o = o.parent) {
|
|
129
|
+
if (o.context?.has(ctx.id)) return o.context.get(ctx.id);
|
|
130
|
+
}
|
|
131
|
+
return ctx.defaultValue;
|
|
132
|
+
}
|
|
88
133
|
|
|
89
134
|
// src/dom/h.ts
|
|
90
135
|
function h(tag, props, ...children) {
|
|
@@ -118,12 +163,12 @@ function toNodes(value) {
|
|
|
118
163
|
return out;
|
|
119
164
|
}
|
|
120
165
|
function appendChildren(parent, children) {
|
|
121
|
-
const
|
|
122
|
-
while (
|
|
123
|
-
const child =
|
|
166
|
+
const pending2 = [...children].reverse();
|
|
167
|
+
while (pending2.length > 0) {
|
|
168
|
+
const child = pending2.pop();
|
|
124
169
|
if (Array.isArray(child)) {
|
|
125
170
|
for (let i = child.length - 1; i >= 0; i--) {
|
|
126
|
-
|
|
171
|
+
pending2.push(child[i]);
|
|
127
172
|
}
|
|
128
173
|
continue;
|
|
129
174
|
}
|
|
@@ -132,17 +177,29 @@ function appendChildren(parent, children) {
|
|
|
132
177
|
const end = document.createComment("");
|
|
133
178
|
parent.append(start, end);
|
|
134
179
|
createEffect(() => {
|
|
135
|
-
|
|
136
|
-
while (start.nextSibling && start.nextSibling !== end) {
|
|
137
|
-
start.nextSibling.remove();
|
|
138
|
-
}
|
|
139
|
-
end.before(...nodes);
|
|
180
|
+
reconcile(start, end, toNodes(child()));
|
|
140
181
|
});
|
|
141
182
|
} else {
|
|
142
183
|
parent.append(...toNodes(child));
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
}
|
|
187
|
+
function reconcile(start, end, next) {
|
|
188
|
+
const keep = new Set(next);
|
|
189
|
+
let cur = start.nextSibling;
|
|
190
|
+
while (cur && cur !== end) {
|
|
191
|
+
const after = cur.nextSibling;
|
|
192
|
+
if (!keep.has(cur)) cur.remove();
|
|
193
|
+
cur = after;
|
|
194
|
+
}
|
|
195
|
+
const parent = end.parentNode;
|
|
196
|
+
let ref = start.nextSibling;
|
|
197
|
+
for (const node of next) {
|
|
198
|
+
if (node === ref)
|
|
199
|
+
ref = ref.nextSibling;
|
|
200
|
+
else parent.insertBefore(node, ref);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
146
203
|
|
|
147
204
|
// src/dom/render.ts
|
|
148
205
|
function render(component, container) {
|
|
@@ -162,16 +219,67 @@ function Show(props) {
|
|
|
162
219
|
|
|
163
220
|
// src/components/for.ts
|
|
164
221
|
function For(props) {
|
|
165
|
-
|
|
222
|
+
let cache = /* @__PURE__ */ new Map();
|
|
223
|
+
onCleanup(() => {
|
|
224
|
+
cache.forEach((e) => e.dispose());
|
|
225
|
+
cache.clear();
|
|
226
|
+
});
|
|
227
|
+
return () => {
|
|
228
|
+
const items = props.each();
|
|
229
|
+
const next = /* @__PURE__ */ new Map();
|
|
230
|
+
const nodes = [];
|
|
231
|
+
items.forEach((item, i) => {
|
|
232
|
+
const k = props.key ? props.key(item, i) : item;
|
|
233
|
+
let entry = cache.get(k);
|
|
234
|
+
if (!entry) {
|
|
235
|
+
entry = createRoot((dispose) => ({
|
|
236
|
+
node: props.children(item, i),
|
|
237
|
+
dispose
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
next.set(k, entry);
|
|
241
|
+
nodes.push(entry.node);
|
|
242
|
+
});
|
|
243
|
+
cache.forEach((entry, k) => {
|
|
244
|
+
if (!next.has(k)) entry.dispose();
|
|
245
|
+
});
|
|
246
|
+
cache = next;
|
|
247
|
+
return nodes;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
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
|
+
// src/jsx-runtime.ts
|
|
259
|
+
function jsx(type, props) {
|
|
260
|
+
if (typeof type === "function") return type(props ?? {});
|
|
261
|
+
const { children, ...rest } = props ?? {};
|
|
262
|
+
const kids = children === void 0 ? [] : [children];
|
|
263
|
+
return h(type, rest, ...kids);
|
|
166
264
|
}
|
|
265
|
+
var jsxs = jsx;
|
|
266
|
+
var jsxDEV = jsx;
|
|
167
267
|
export {
|
|
168
268
|
For,
|
|
169
269
|
Show,
|
|
270
|
+
batch,
|
|
271
|
+
createContext,
|
|
170
272
|
createEffect,
|
|
273
|
+
createMemo,
|
|
171
274
|
createRoot,
|
|
172
275
|
createSignal,
|
|
173
276
|
h,
|
|
277
|
+
jsx,
|
|
278
|
+
jsxDEV,
|
|
279
|
+
jsxs,
|
|
174
280
|
onCleanup,
|
|
175
281
|
onMount,
|
|
176
|
-
|
|
282
|
+
provide,
|
|
283
|
+
render,
|
|
284
|
+
useContext
|
|
177
285
|
};
|
package/package.json
CHANGED
|
@@ -1,57 +1,78 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "cajoa",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A beginner-friendly, signals-based frontend framework.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.cjs",
|
|
7
|
-
"module": "./dist/index.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"import": {
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
|
-
"default": "./dist/index.js"
|
|
14
|
-
},
|
|
15
|
-
"require": {
|
|
16
|
-
"types": "./dist/index.d.cts",
|
|
17
|
-
"default": "./dist/index.cjs"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
],
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "cajoa",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A beginner-friendly, signals-based frontend framework.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
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
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"sideEffects": false,
|
|
45
|
+
"scripts": {
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"dev": "vite --open /playground/",
|
|
48
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
49
|
+
"test": "vitest",
|
|
50
|
+
"prepublishOnly": "vitest run && npm run build"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/CajoaTS/Cajoa.git"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"frontend",
|
|
58
|
+
"framework",
|
|
59
|
+
"signals",
|
|
60
|
+
"reactive",
|
|
61
|
+
"ui",
|
|
62
|
+
"cajoa"
|
|
63
|
+
],
|
|
64
|
+
"author": "cjmgrc",
|
|
65
|
+
"license": "Apache-2.0",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/CajoaTS/Cajoa/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/CajoaTS/Cajoa#readme",
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/node": "^26.6.5",
|
|
72
|
+
"jsdom": "^29.1.1",
|
|
73
|
+
"tsup": "^9.0.0",
|
|
74
|
+
"typescript": "^5.9.3",
|
|
75
|
+
"vite": "^8.3.4",
|
|
76
|
+
"vitest": "^5.0.5"
|
|
77
|
+
}
|
|
78
|
+
}
|