cajoa 0.8.0 → 0.10.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 +61 -25
- package/dist/vite.cjs +219 -0
- package/dist/vite.d.cts +9 -0
- package/dist/vite.d.ts +9 -0
- package/dist/vite.js +184 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -30,22 +30,24 @@ Or add Cajoa to an existing project:
|
|
|
30
30
|
npm install cajoa
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
### Manual
|
|
33
|
+
### Manual setup
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Add the plugin to `vite.config.ts`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { defineConfig } from "vite";
|
|
39
|
+
import { cajoa } from "cajoa/vite";
|
|
40
|
+
|
|
41
|
+
export default defineConfig({ plugins: [cajoa()] });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
And set the JSX types in `tsconfig.json`:
|
|
36
45
|
|
|
37
46
|
```json
|
|
38
|
-
{
|
|
39
|
-
"compilerOptions": {
|
|
40
|
-
"jsx": "react-jsx",
|
|
41
|
-
"jsxImportSource": "cajoa"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
47
|
+
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "cajoa" } }
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
(`oxc: { jsx: { runtime: "automatic", importSource: "cajoa" } }` on newer Vite,
|
|
48
|
-
or `esbuild: { jsx: "automatic", jsxImportSource: "cajoa" }` on older ones).
|
|
50
|
+
The plugin also sets Vite's JSX transform for you. Without the plugin, set `oxc: { jsx: { runtime: "automatic", importSource: "cajoa" } }` (Vite 8) or `esbuild: { jsx: "automatic", jsxImportSource: "cajoa" }` (older Vite) yourself.
|
|
49
51
|
|
|
50
52
|
## Quick start
|
|
51
53
|
|
|
@@ -65,16 +67,33 @@ function Counter() {
|
|
|
65
67
|
render(Counter, document.getElementById("app")!);
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
##
|
|
70
|
+
## Reactive JSX
|
|
71
|
+
|
|
72
|
+
With the Vite plugin, write plain JSX. Calls to signals are reactive wherever they appear in a DOM element, and `Show` children are created lazily:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<p>Double: {count() * 2}</p>
|
|
76
|
+
<p class={count() % 2 ? "odd" : "even"} style={{ color: color() }}>…</p>
|
|
77
|
+
|
|
78
|
+
<Show when={count() > 3} fallback={<p>Keep clicking</p>}>
|
|
79
|
+
<p>More than 3!</p>
|
|
80
|
+
</Show>
|
|
81
|
+
|
|
82
|
+
<For each={todos()}>{(todo) => <li>{todo.text}</li>}</For>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The plugin rewrites your JSX at build time. When an expression contains a function call, it wraps it in a function (`{count() * 2}` becomes `{() => (count() * 2)}`) for:
|
|
86
|
+
|
|
87
|
+
- children and attributes of DOM elements (but not `ref`, `key` and `on*` props)
|
|
88
|
+
- children of your own components
|
|
89
|
+
- `when` and `each` of `Show` and `For`, and the `fallback` and children of `Show`
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
It does **not** change the props of your own components. They receive plain values. To pass a live value, pass the signal itself (`<Greeting name={name} />`) or a function.
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
2. **`Show` children are a function
|
|
76
|
-
`<Show when={on}>{() => <p>Hello</p>}</Show>`
|
|
77
|
-
Otherwise the element is created even when it is hidden.
|
|
93
|
+
Without the plugin, two rules apply:
|
|
94
|
+
|
|
95
|
+
1. **Pass the signal, don't call it.** `{count}` is reactive, `{count()}` is read once. For computed values, pass a function: `{() => count() * 2}`.
|
|
96
|
+
2. **`Show` children are a function:** `<Show when={on}>{() => <p>Hello</p>}</Show>`.
|
|
78
97
|
|
|
79
98
|
## Lists and conditions
|
|
80
99
|
|
|
@@ -96,7 +115,7 @@ const [todos, setTodos] = createSignal([{ id: 1, text: "Learn Cajoa" }]);
|
|
|
96
115
|
|
|
97
116
|
## Forms, styles and events
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+
````tsx
|
|
100
119
|
const [name, setName] = createSignal("");
|
|
101
120
|
const [agree, setAgree] = createSignal(false);
|
|
102
121
|
|
|
@@ -118,12 +137,29 @@ const [agree, setAgree] = createSignal(false);
|
|
|
118
137
|
Hello, {() => name() || "stranger"}!
|
|
119
138
|
</p>
|
|
120
139
|
</form>;
|
|
121
|
-
|
|
140
|
+
|
|
141
|
+
## Refs and SVG
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
function SearchBox() {
|
|
145
|
+
let input: HTMLInputElement | undefined;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div>
|
|
149
|
+
<input ref={(el: HTMLInputElement) => (input = el)} />
|
|
150
|
+
<button onClick={() => input?.focus()}>Focus</button>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
````
|
|
122
156
|
|
|
123
157
|
- `value`, `checked` and `selected` are set as DOM properties, so they stay in sync even after the user typed. Write `value` after `type`, `min` and `max`.
|
|
124
158
|
- `style` takes a string or an object (camelCase keys, kebab-case keys, or custom properties like `--accent`). Numbers are not converted to `px`, so write `"12px"`. For a reactive style, pass a function: `style={() => ({ color: color() })}`. An object with signals inside, like `style={{ color: color() }}`, is read once.
|
|
125
159
|
- Events are native DOM events. `onChange` fires when a field loses focus, so use `onInput` for every keystroke. `onDoubleClick` listens to `dblclick`.
|
|
126
160
|
- `className` and `htmlFor` work. Inline string handlers like `onclick="..."` are ignored.
|
|
161
|
+
- `ref` is a function that receives the element once it is created. Its props and children are ready, but it is not in the page yet, so use `onMount` if you need to measure it right away. Signals read inside `ref` are not tracked.
|
|
162
|
+
- SVG works: `<svg>`, `<path>`, `<circle>`, gradients, filters and the other SVG tags are created in the SVG namespace. Write attributes as `stroke-width="2"` or `strokeWidth="2"`.
|
|
127
163
|
|
|
128
164
|
```tsx
|
|
129
165
|
import { render, Router, Route, Link, useParams } from "cajoa";
|
|
@@ -233,12 +269,12 @@ Routes are matched in order, so put `path="*"` last.
|
|
|
233
269
|
- Router: pathname only (no query/hash handling), no nested routes, no `base` path.
|
|
234
270
|
- `<Show>` and `<Router>` return a function, so they must sit inside an element, not be the root returned by `render`.
|
|
235
271
|
- Single-page apps need a server fallback to `index.html` for unknown paths.
|
|
236
|
-
- SVG is not supported yet: `<svg>` is created as an HTML element, so it does not render.
|
|
237
|
-
- No `ref` prop yet.
|
|
238
272
|
- `<select value>` works only when the `<option>` elements are direct children of the `<select>`.
|
|
239
273
|
- `createMemo` can show a brief inconsistent state when memos depend on each other.
|
|
240
274
|
- `set` calls are not batched automatically inside event handlers. Wrap them in `batch()`.
|
|
241
275
|
- Types in the core use a loose `HostNode` type for now.
|
|
276
|
+
- Inside `<svg>`, the tags `<a>`, `<title>`, `<style>` and `<script>` are created as HTML elements (they share their names with HTML tags).
|
|
277
|
+
- The compiler works on `.tsx` and `.jsx` files in Vite projects. `Show` and `For` are recognised only when imported from `cajoa` or `@cajoa/core`.
|
|
242
278
|
|
|
243
279
|
## Roadmap
|
|
244
280
|
|
|
@@ -250,8 +286,8 @@ Routes are matched in order, so put `path="*"` last.
|
|
|
250
286
|
- [x] Renderer abstraction, split into packages
|
|
251
287
|
- [x] Automated releases
|
|
252
288
|
- [x] Form, style and event handling in the DOM renderer
|
|
253
|
-
- [
|
|
254
|
-
- [
|
|
289
|
+
- [x] SVG and `ref` support
|
|
290
|
+
- [x] Vite plugin (removes the two rules above)
|
|
255
291
|
- [ ] Better form and style handling in the DOM renderer
|
|
256
292
|
- [ ] Native renderers (mobile, desktop)
|
|
257
293
|
- [ ] SSR
|
package/dist/vite.cjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/vite.ts
|
|
31
|
+
var vite_exports = {};
|
|
32
|
+
__export(vite_exports, {
|
|
33
|
+
cajoa: () => cajoa
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(vite_exports);
|
|
36
|
+
var import_vite = require("vite");
|
|
37
|
+
|
|
38
|
+
// src/compiler.ts
|
|
39
|
+
var import_parser = require("@babel/parser");
|
|
40
|
+
var import_magic_string = __toESM(require("magic-string"), 1);
|
|
41
|
+
var CORE_MODULES = /* @__PURE__ */ new Set(["cajoa", "@cajoa/core"]);
|
|
42
|
+
var SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
43
|
+
"loc",
|
|
44
|
+
"extra",
|
|
45
|
+
"leadingComments",
|
|
46
|
+
"trailingComments",
|
|
47
|
+
"innerComments"
|
|
48
|
+
]);
|
|
49
|
+
var CALL_TYPES = /* @__PURE__ */ new Set([
|
|
50
|
+
"CallExpression",
|
|
51
|
+
"OptionalCallExpression",
|
|
52
|
+
"TaggedTemplateExpression"
|
|
53
|
+
]);
|
|
54
|
+
var STOP_TYPES = /* @__PURE__ */ new Set([
|
|
55
|
+
"ArrowFunctionExpression",
|
|
56
|
+
"FunctionExpression",
|
|
57
|
+
"FunctionDeclaration",
|
|
58
|
+
"ObjectMethod",
|
|
59
|
+
"ClassMethod",
|
|
60
|
+
"ClassPrivateMethod",
|
|
61
|
+
"JSXElement",
|
|
62
|
+
"JSXFragment"
|
|
63
|
+
]);
|
|
64
|
+
var PASS_THROUGH = /* @__PURE__ */ new Set([
|
|
65
|
+
"ArrowFunctionExpression",
|
|
66
|
+
"FunctionExpression",
|
|
67
|
+
"Identifier",
|
|
68
|
+
"MemberExpression",
|
|
69
|
+
"OptionalMemberExpression"
|
|
70
|
+
]);
|
|
71
|
+
function isNode(value) {
|
|
72
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
73
|
+
}
|
|
74
|
+
function childNodes(node) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const key of Object.keys(node)) {
|
|
77
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
78
|
+
const value = node[key];
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
for (const item of value) if (isNode(item)) out.push(item);
|
|
81
|
+
} else if (isNode(value)) {
|
|
82
|
+
out.push(value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function containsCall(node) {
|
|
88
|
+
if (CALL_TYPES.has(node.type)) return true;
|
|
89
|
+
if (STOP_TYPES.has(node.type)) return false;
|
|
90
|
+
return childNodes(node).some(containsCall);
|
|
91
|
+
}
|
|
92
|
+
function exprOf(node) {
|
|
93
|
+
if (!node || node.type !== "JSXExpressionContainer") return null;
|
|
94
|
+
return node.expression.type === "JSXEmptyExpression" ? null : node.expression;
|
|
95
|
+
}
|
|
96
|
+
function collectControls(program) {
|
|
97
|
+
const controls = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const node of program.body) {
|
|
99
|
+
if (node.type !== "ImportDeclaration" || !CORE_MODULES.has(node.source.value))
|
|
100
|
+
continue;
|
|
101
|
+
for (const spec of node.specifiers) {
|
|
102
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
103
|
+
const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
104
|
+
if (imported === "Show" || imported === "For")
|
|
105
|
+
controls.set(spec.local.name, imported);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return controls;
|
|
109
|
+
}
|
|
110
|
+
function transformJsx(code, id) {
|
|
111
|
+
const file = id.split("?")[0];
|
|
112
|
+
if (!/\.[jt]sx$/.test(file) || /[\\/]node_modules[\\/]/.test(file))
|
|
113
|
+
return null;
|
|
114
|
+
if (!code.includes("<")) return null;
|
|
115
|
+
let ast;
|
|
116
|
+
try {
|
|
117
|
+
ast = (0, import_parser.parse)(code, {
|
|
118
|
+
sourceType: "module",
|
|
119
|
+
plugins: file.endsWith(".tsx") ? ["jsx", "typescript"] : ["jsx"]
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const controls = collectControls(ast.program);
|
|
125
|
+
const s = new import_magic_string.default(code);
|
|
126
|
+
let changed = false;
|
|
127
|
+
const wrap = (expr) => {
|
|
128
|
+
s.prependRight(expr.start, "() => (");
|
|
129
|
+
s.appendLeft(expr.end, ")");
|
|
130
|
+
changed = true;
|
|
131
|
+
};
|
|
132
|
+
const wrapChildren = (parent) => {
|
|
133
|
+
for (const kid of parent.children) {
|
|
134
|
+
const expr = exprOf(kid);
|
|
135
|
+
if (expr && containsCall(expr)) wrap(expr);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const wrapShowChildren = (el) => {
|
|
139
|
+
const kids = el.children.filter(
|
|
140
|
+
(k) => !(k.type === "JSXText" && k.value.trim() === "") && !(k.type === "JSXExpressionContainer" && k.expression.type === "JSXEmptyExpression")
|
|
141
|
+
);
|
|
142
|
+
if (kids.length === 0) return;
|
|
143
|
+
if (kids.length === 1) {
|
|
144
|
+
const only = kids[0];
|
|
145
|
+
if (only.type === "JSXElement" || only.type === "JSXFragment") {
|
|
146
|
+
s.prependRight(only.start, "{() => (");
|
|
147
|
+
s.appendLeft(only.end, ")}");
|
|
148
|
+
changed = true;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const expr = exprOf(only);
|
|
152
|
+
if (expr) {
|
|
153
|
+
if (!PASS_THROUGH.has(expr.type)) wrap(expr);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
s.prependRight(kids[0].start, "{() => (<>");
|
|
158
|
+
s.appendLeft(kids[kids.length - 1].end, "</>)}");
|
|
159
|
+
changed = true;
|
|
160
|
+
wrapChildren(el);
|
|
161
|
+
};
|
|
162
|
+
const handleElement = (el) => {
|
|
163
|
+
const nameNode = el.openingElement.name;
|
|
164
|
+
const name = nameNode.type === "JSXIdentifier" ? nameNode.name : null;
|
|
165
|
+
const isHost = name !== null && /^[a-z]/.test(name);
|
|
166
|
+
const control = name !== null && !isHost ? controls.get(name) : void 0;
|
|
167
|
+
for (const attr of el.openingElement.attributes) {
|
|
168
|
+
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier")
|
|
169
|
+
continue;
|
|
170
|
+
const expr = exprOf(attr.value);
|
|
171
|
+
if (!expr) continue;
|
|
172
|
+
const prop = attr.name.name;
|
|
173
|
+
if (isHost) {
|
|
174
|
+
if (prop === "ref" || prop === "key" || prop.startsWith("on")) continue;
|
|
175
|
+
if (containsCall(expr)) wrap(expr);
|
|
176
|
+
} else if (control === "Show") {
|
|
177
|
+
if (prop === "when" && containsCall(expr)) wrap(expr);
|
|
178
|
+
else if (prop === "fallback" && !PASS_THROUGH.has(expr.type))
|
|
179
|
+
wrap(expr);
|
|
180
|
+
} else if (control === "For") {
|
|
181
|
+
if (prop === "each" && containsCall(expr)) wrap(expr);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (control === "Show") wrapShowChildren(el);
|
|
185
|
+
else if (control !== "For") wrapChildren(el);
|
|
186
|
+
};
|
|
187
|
+
const visit = (node) => {
|
|
188
|
+
if (node.type === "JSXElement") handleElement(node);
|
|
189
|
+
else if (node.type === "JSXFragment") wrapChildren(node);
|
|
190
|
+
for (const child of childNodes(node)) visit(child);
|
|
191
|
+
};
|
|
192
|
+
visit(ast.program);
|
|
193
|
+
if (!changed) return null;
|
|
194
|
+
return {
|
|
195
|
+
code: s.toString(),
|
|
196
|
+
map: s.generateMap({ hires: true, source: file, includeContent: true })
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/vite.ts
|
|
201
|
+
function cajoa(options = {}) {
|
|
202
|
+
const importSource = options.importSource ?? "cajoa";
|
|
203
|
+
return {
|
|
204
|
+
name: "cajoa",
|
|
205
|
+
enforce: "pre",
|
|
206
|
+
config() {
|
|
207
|
+
const major = Number.parseInt(import_vite.version, 10);
|
|
208
|
+
return major >= 8 ? { oxc: { jsx: { runtime: "automatic", importSource } } } : { esbuild: { jsx: "automatic", jsxImportSource: importSource } };
|
|
209
|
+
},
|
|
210
|
+
transform(code, id) {
|
|
211
|
+
const result = transformJsx(code, id);
|
|
212
|
+
return result ? { code: result.code, map: result.map } : null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
217
|
+
0 && (module.exports = {
|
|
218
|
+
cajoa
|
|
219
|
+
});
|
package/dist/vite.d.cts
ADDED
package/dist/vite.d.ts
ADDED
package/dist/vite.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// src/vite.ts
|
|
2
|
+
import { version } from "vite";
|
|
3
|
+
|
|
4
|
+
// src/compiler.ts
|
|
5
|
+
import { parse } from "@babel/parser";
|
|
6
|
+
import MagicString from "magic-string";
|
|
7
|
+
var CORE_MODULES = /* @__PURE__ */ new Set(["cajoa", "@cajoa/core"]);
|
|
8
|
+
var SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
9
|
+
"loc",
|
|
10
|
+
"extra",
|
|
11
|
+
"leadingComments",
|
|
12
|
+
"trailingComments",
|
|
13
|
+
"innerComments"
|
|
14
|
+
]);
|
|
15
|
+
var CALL_TYPES = /* @__PURE__ */ new Set([
|
|
16
|
+
"CallExpression",
|
|
17
|
+
"OptionalCallExpression",
|
|
18
|
+
"TaggedTemplateExpression"
|
|
19
|
+
]);
|
|
20
|
+
var STOP_TYPES = /* @__PURE__ */ new Set([
|
|
21
|
+
"ArrowFunctionExpression",
|
|
22
|
+
"FunctionExpression",
|
|
23
|
+
"FunctionDeclaration",
|
|
24
|
+
"ObjectMethod",
|
|
25
|
+
"ClassMethod",
|
|
26
|
+
"ClassPrivateMethod",
|
|
27
|
+
"JSXElement",
|
|
28
|
+
"JSXFragment"
|
|
29
|
+
]);
|
|
30
|
+
var PASS_THROUGH = /* @__PURE__ */ new Set([
|
|
31
|
+
"ArrowFunctionExpression",
|
|
32
|
+
"FunctionExpression",
|
|
33
|
+
"Identifier",
|
|
34
|
+
"MemberExpression",
|
|
35
|
+
"OptionalMemberExpression"
|
|
36
|
+
]);
|
|
37
|
+
function isNode(value) {
|
|
38
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
39
|
+
}
|
|
40
|
+
function childNodes(node) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const key of Object.keys(node)) {
|
|
43
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
44
|
+
const value = node[key];
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
for (const item of value) if (isNode(item)) out.push(item);
|
|
47
|
+
} else if (isNode(value)) {
|
|
48
|
+
out.push(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function containsCall(node) {
|
|
54
|
+
if (CALL_TYPES.has(node.type)) return true;
|
|
55
|
+
if (STOP_TYPES.has(node.type)) return false;
|
|
56
|
+
return childNodes(node).some(containsCall);
|
|
57
|
+
}
|
|
58
|
+
function exprOf(node) {
|
|
59
|
+
if (!node || node.type !== "JSXExpressionContainer") return null;
|
|
60
|
+
return node.expression.type === "JSXEmptyExpression" ? null : node.expression;
|
|
61
|
+
}
|
|
62
|
+
function collectControls(program) {
|
|
63
|
+
const controls = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const node of program.body) {
|
|
65
|
+
if (node.type !== "ImportDeclaration" || !CORE_MODULES.has(node.source.value))
|
|
66
|
+
continue;
|
|
67
|
+
for (const spec of node.specifiers) {
|
|
68
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
69
|
+
const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
70
|
+
if (imported === "Show" || imported === "For")
|
|
71
|
+
controls.set(spec.local.name, imported);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return controls;
|
|
75
|
+
}
|
|
76
|
+
function transformJsx(code, id) {
|
|
77
|
+
const file = id.split("?")[0];
|
|
78
|
+
if (!/\.[jt]sx$/.test(file) || /[\\/]node_modules[\\/]/.test(file))
|
|
79
|
+
return null;
|
|
80
|
+
if (!code.includes("<")) return null;
|
|
81
|
+
let ast;
|
|
82
|
+
try {
|
|
83
|
+
ast = parse(code, {
|
|
84
|
+
sourceType: "module",
|
|
85
|
+
plugins: file.endsWith(".tsx") ? ["jsx", "typescript"] : ["jsx"]
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const controls = collectControls(ast.program);
|
|
91
|
+
const s = new MagicString(code);
|
|
92
|
+
let changed = false;
|
|
93
|
+
const wrap = (expr) => {
|
|
94
|
+
s.prependRight(expr.start, "() => (");
|
|
95
|
+
s.appendLeft(expr.end, ")");
|
|
96
|
+
changed = true;
|
|
97
|
+
};
|
|
98
|
+
const wrapChildren = (parent) => {
|
|
99
|
+
for (const kid of parent.children) {
|
|
100
|
+
const expr = exprOf(kid);
|
|
101
|
+
if (expr && containsCall(expr)) wrap(expr);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const wrapShowChildren = (el) => {
|
|
105
|
+
const kids = el.children.filter(
|
|
106
|
+
(k) => !(k.type === "JSXText" && k.value.trim() === "") && !(k.type === "JSXExpressionContainer" && k.expression.type === "JSXEmptyExpression")
|
|
107
|
+
);
|
|
108
|
+
if (kids.length === 0) return;
|
|
109
|
+
if (kids.length === 1) {
|
|
110
|
+
const only = kids[0];
|
|
111
|
+
if (only.type === "JSXElement" || only.type === "JSXFragment") {
|
|
112
|
+
s.prependRight(only.start, "{() => (");
|
|
113
|
+
s.appendLeft(only.end, ")}");
|
|
114
|
+
changed = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const expr = exprOf(only);
|
|
118
|
+
if (expr) {
|
|
119
|
+
if (!PASS_THROUGH.has(expr.type)) wrap(expr);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
s.prependRight(kids[0].start, "{() => (<>");
|
|
124
|
+
s.appendLeft(kids[kids.length - 1].end, "</>)}");
|
|
125
|
+
changed = true;
|
|
126
|
+
wrapChildren(el);
|
|
127
|
+
};
|
|
128
|
+
const handleElement = (el) => {
|
|
129
|
+
const nameNode = el.openingElement.name;
|
|
130
|
+
const name = nameNode.type === "JSXIdentifier" ? nameNode.name : null;
|
|
131
|
+
const isHost = name !== null && /^[a-z]/.test(name);
|
|
132
|
+
const control = name !== null && !isHost ? controls.get(name) : void 0;
|
|
133
|
+
for (const attr of el.openingElement.attributes) {
|
|
134
|
+
if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier")
|
|
135
|
+
continue;
|
|
136
|
+
const expr = exprOf(attr.value);
|
|
137
|
+
if (!expr) continue;
|
|
138
|
+
const prop = attr.name.name;
|
|
139
|
+
if (isHost) {
|
|
140
|
+
if (prop === "ref" || prop === "key" || prop.startsWith("on")) continue;
|
|
141
|
+
if (containsCall(expr)) wrap(expr);
|
|
142
|
+
} else if (control === "Show") {
|
|
143
|
+
if (prop === "when" && containsCall(expr)) wrap(expr);
|
|
144
|
+
else if (prop === "fallback" && !PASS_THROUGH.has(expr.type))
|
|
145
|
+
wrap(expr);
|
|
146
|
+
} else if (control === "For") {
|
|
147
|
+
if (prop === "each" && containsCall(expr)) wrap(expr);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (control === "Show") wrapShowChildren(el);
|
|
151
|
+
else if (control !== "For") wrapChildren(el);
|
|
152
|
+
};
|
|
153
|
+
const visit = (node) => {
|
|
154
|
+
if (node.type === "JSXElement") handleElement(node);
|
|
155
|
+
else if (node.type === "JSXFragment") wrapChildren(node);
|
|
156
|
+
for (const child of childNodes(node)) visit(child);
|
|
157
|
+
};
|
|
158
|
+
visit(ast.program);
|
|
159
|
+
if (!changed) return null;
|
|
160
|
+
return {
|
|
161
|
+
code: s.toString(),
|
|
162
|
+
map: s.generateMap({ hires: true, source: file, includeContent: true })
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/vite.ts
|
|
167
|
+
function cajoa(options = {}) {
|
|
168
|
+
const importSource = options.importSource ?? "cajoa";
|
|
169
|
+
return {
|
|
170
|
+
name: "cajoa",
|
|
171
|
+
enforce: "pre",
|
|
172
|
+
config() {
|
|
173
|
+
const major = Number.parseInt(version, 10);
|
|
174
|
+
return major >= 8 ? { oxc: { jsx: { runtime: "automatic", importSource } } } : { esbuild: { jsx: "automatic", jsxImportSource: importSource } };
|
|
175
|
+
},
|
|
176
|
+
transform(code, id) {
|
|
177
|
+
const result = transformJsx(code, id);
|
|
178
|
+
return result ? { code: result.code, map: result.map } : null;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
cajoa
|
|
184
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cajoa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A small, beginner-friendly frontend framework built on signals and JSX.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -35,6 +35,16 @@
|
|
|
35
35
|
"require": {
|
|
36
36
|
"types": "./dist/index.d.cts",
|
|
37
37
|
"default": "./dist/index.cjs"
|
|
38
|
+
},
|
|
39
|
+
"./vite": {
|
|
40
|
+
"import": {
|
|
41
|
+
"types": "./dist/vite.d.ts",
|
|
42
|
+
"default": "./dist/vite.js"
|
|
43
|
+
},
|
|
44
|
+
"require": {
|
|
45
|
+
"types": "./dist/vite.d.cts",
|
|
46
|
+
"default": "./dist/vite.cjs"
|
|
47
|
+
}
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
},
|
|
@@ -45,6 +55,9 @@
|
|
|
45
55
|
],
|
|
46
56
|
"jsx-dev-runtime": [
|
|
47
57
|
"./dist/index.d.ts"
|
|
58
|
+
],
|
|
59
|
+
"vite": [
|
|
60
|
+
"./dist/vite.d.ts"
|
|
48
61
|
]
|
|
49
62
|
}
|
|
50
63
|
},
|
|
@@ -56,11 +69,21 @@
|
|
|
56
69
|
],
|
|
57
70
|
"sideEffects": true,
|
|
58
71
|
"dependencies": {
|
|
59
|
-
"@
|
|
60
|
-
"@cajoa/
|
|
72
|
+
"@babel/parser": "^8.0.7",
|
|
73
|
+
"@cajoa/core": "^0.10.0",
|
|
74
|
+
"@cajoa/dom": "^0.10.0",
|
|
75
|
+
"magic-string": "^0.30.21"
|
|
76
|
+
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"vite": ">=5.0.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"vite": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
61
84
|
},
|
|
62
85
|
"scripts": {
|
|
63
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
86
|
+
"build": "tsup src/index.ts src/vite.ts --format esm,cjs --dts --clean",
|
|
64
87
|
"check:package": "publint && attw --pack ."
|
|
65
88
|
},
|
|
66
89
|
"repository": {
|