babel-plugin-minibum-jsx 1.1.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 ADDED
@@ -0,0 +1,9 @@
1
+ Minibum JSX
2
+ ===========
3
+
4
+ Minibum JSX is a lightweight JSX plugin for the Minibum ecosystem. It enables
5
+ JSX syntax in application source files and transforms JSX elements into the
6
+ runtime calls required to create and compose UI components.
7
+
8
+ The plugin is intended to keep component markup concise while fitting into a
9
+ small, fast build pipeline.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "babel-plugin-minibum-jsx",
3
+ "version": "1.1.0",
4
+ "description": "A Babel plugin that transforms JSX into Minibum.",
5
+ "main": "src/index.js",
6
+ "types": "types/index.d.ts",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./types": "./types/index.d.ts"
10
+ },
11
+ "type": "module",
12
+ "files": [
13
+ "src",
14
+ "types"
15
+ ],
16
+ "scripts": {
17
+ "test": "vitest",
18
+ "prepublishOnly": "npm test -- --run"
19
+ },
20
+ "keywords": [
21
+ "babel-plugin",
22
+ "babel",
23
+ "jsx",
24
+ "minibum"
25
+ ],
26
+ "peerDependencies": {
27
+ "@babel/core": ">=7"
28
+ },
29
+ "devDependencies": {
30
+ "@babel/core": "^8.0.5",
31
+ "@babel/plugin-syntax-jsx": "^8.0.1",
32
+ "@types/convert-source-map": "^2.0.3",
33
+ "typescript": "^7.0.2",
34
+ "vitest": "^5.0.1"
35
+ }
36
+ }
package/src/index.js ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * babel-plugin-minibum-jsx
3
+ *
4
+ * Transforms classic JSX into MiniBum's fine-grained reactive API.
5
+ *
6
+ * Standard elements:
7
+ * <div className="c">{count}</div>
8
+ * → E.div({ className: "c", children: count })
9
+ *
10
+ * Control-flow:
11
+ * <For each={items}>{(item) => <li>{item}</li>}</For>
12
+ * → E.list(items, (item) => E.li({ children: item }))
13
+ *
14
+ * <Show when={visible}><p>Hi</p></Show>
15
+ * → E.cond(visible, () => E.p({ children: "Hi" }))
16
+ *
17
+ * <Show when={visible} fallback={<p>Loading</p>}>
18
+ * <p>Ready</p>
19
+ * </Show>
20
+ * → E.cond(visible, () => E.p({ children: "Ready" }), () => E.p({ children: "Loading" }))
21
+ *
22
+ * Also supports the aliases <List> and <Cond>.
23
+ */
24
+
25
+ "use strict";
26
+ import syntaxJsx from "@babel/plugin-syntax-jsx";
27
+
28
+ export default function minibumJsx({ types: t }) {
29
+ // ------------------------------------------------------------------
30
+ // Helpers
31
+ // ------------------------------------------------------------------
32
+
33
+ function isComponent(name) {
34
+ return typeof name === "string" && /^[A-Z]/.test(name);
35
+ }
36
+
37
+ function getName(node) {
38
+ if (t.isJSXIdentifier(node)) return node.name;
39
+ return null;
40
+ }
41
+
42
+ function convertAttrValue(value) {
43
+ if (value == null) return t.booleanLiteral(true);
44
+ if (t.isJSXExpressionContainer(value)) return value.expression;
45
+ return value;
46
+ }
47
+
48
+ function buildAttributes(attrs) {
49
+ if (!attrs || attrs.length === 0) return null;
50
+ const props = [];
51
+ for (const attr of attrs) {
52
+ if (t.isJSXSpreadAttribute(attr)) {
53
+ props.push(t.spreadElement(attr.argument));
54
+ continue;
55
+ }
56
+ const key = attr.name.name;
57
+ props.push(
58
+ t.objectProperty(
59
+ t.isValidIdentifier(key) ? t.identifier(key) : t.stringLiteral(key),
60
+ convertAttrValue(attr.value)
61
+ )
62
+ );
63
+ }
64
+ return t.objectExpression(props);
65
+ }
66
+
67
+ function convertChild(child) {
68
+ if (t.isJSXText(child)) {
69
+ const text = child.value.replace(/\s+/g, " ").trim();
70
+ return text ? t.stringLiteral(text) : null;
71
+ }
72
+ if (t.isJSXExpressionContainer(child)) {
73
+ return t.isJSXEmptyExpression(child.expression)
74
+ ? null
75
+ : child.expression;
76
+ }
77
+ if (t.isJSXElement(child) || t.isJSXFragment(child)) {
78
+ return transformNode(child);
79
+ }
80
+ if (t.isJSXSpreadChild(child)) {
81
+ return child.expression;
82
+ }
83
+ return child;
84
+ }
85
+
86
+ function buildChildren(children) {
87
+ const out = [];
88
+ for (const c of children) {
89
+ const v = convertChild(c);
90
+ if (v != null) out.push(v);
91
+ }
92
+ return out;
93
+ }
94
+
95
+ function buildConfig(attrsExpr, childrenArr) {
96
+ const props = [];
97
+ if (attrsExpr) {
98
+ if (t.isObjectExpression(attrsExpr)) {
99
+ props.push(...attrsExpr.properties);
100
+ } else {
101
+ props.push(t.spreadElement(attrsExpr));
102
+ }
103
+ }
104
+ if (childrenArr.length === 1) {
105
+ props.push(t.objectProperty(t.identifier("children"), childrenArr[0]));
106
+ } else if (childrenArr.length > 1) {
107
+ props.push(
108
+ t.objectProperty(
109
+ t.identifier("children"),
110
+ t.arrayExpression(childrenArr)
111
+ )
112
+ );
113
+ }
114
+ return t.objectExpression(props);
115
+ }
116
+
117
+ /** Extract a named prop from an attributes ObjectExpression (or null). */
118
+ function getProp(attrsExpr, propName) {
119
+ if (!attrsExpr || !t.isObjectExpression(attrsExpr)) return null;
120
+ for (const p of attrsExpr.properties) {
121
+ if (
122
+ t.isObjectProperty(p) &&
123
+ ((t.isIdentifier(p.key) && p.key.name === propName) ||
124
+ (t.isStringLiteral(p.key) && p.key.value === propName))
125
+ ) {
126
+ return p.value;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * Wrap an expression in an arrow function if it is not already a function.
134
+ * Used so that E.cond / E.list always receive a factory.
135
+ */
136
+ function ensureFunction(expr) {
137
+ if (
138
+ t.isArrowFunctionExpression(expr) ||
139
+ t.isFunctionExpression(expr)
140
+ ) {
141
+ return expr;
142
+ }
143
+ return t.arrowFunctionExpression([], expr);
144
+ }
145
+
146
+ // ------------------------------------------------------------------
147
+ // Control-flow transforms
148
+ // ------------------------------------------------------------------
149
+
150
+ /**
151
+ * <For each={sig}>{(item) => ...}</For>
152
+ * <List each={sig}>{(item) => ...}</List>
153
+ * → E.list(sig, mapFn)
154
+ */
155
+ function transformFor(opening, children) {
156
+ const attrs = buildAttributes(opening.attributes);
157
+ const eachExpr = getProp(attrs, "each");
158
+
159
+ if (!eachExpr) {
160
+ throw new Error(
161
+ `[minibum-jsx] <For>/<List> requires an "each" prop (the signal or array).`
162
+ );
163
+ }
164
+
165
+ const kids = buildChildren(children);
166
+ if (kids.length === 0) {
167
+ throw new Error(
168
+ `[minibum-jsx] <For>/<List> requires a map function as its child.`
169
+ );
170
+ }
171
+
172
+ let mapFn = kids[0];
173
+ if (kids.length > 1 || !t.isFunction(mapFn)) {
174
+ mapFn = t.arrowFunctionExpression(
175
+ [],
176
+ kids.length === 1 ? kids[0] : t.arrayExpression(kids)
177
+ );
178
+ }
179
+
180
+ return t.callExpression(
181
+ t.memberExpression(t.identifier("E"), t.identifier("list")),
182
+ [eachExpr, mapFn]
183
+ );
184
+ }
185
+
186
+
187
+ /**
188
+ * <Show when={sig}>...</Show>
189
+ * <Cond when={sig}>...</Cond>
190
+ * → E.cond(sig, () => content [, () => fallback])
191
+ */
192
+ function transformShow(opening, children) {
193
+ const attrs = buildAttributes(opening.attributes);
194
+ const whenExpr = getProp(attrs, "when");
195
+
196
+ if (!whenExpr) {
197
+ throw new Error(
198
+ `[minibum-jsx] <Show>/<Cond> requires a "when" prop (the condition signal).`
199
+ );
200
+ }
201
+
202
+ const fallbackExpr = getProp(attrs, "fallback");
203
+ const kids = buildChildren(children);
204
+
205
+ let thenExpr;
206
+
207
+ if (kids.length === 0) {
208
+ thenExpr = t.nullLiteral();
209
+ } else if (kids.length === 1) {
210
+ thenExpr = kids[0];
211
+ } else {
212
+ thenExpr = t.arrayExpression(kids);
213
+ }
214
+
215
+ let body = thenExpr;
216
+
217
+ if (fallbackExpr) {
218
+ body = t.conditionalExpression(
219
+ t.identifier("v"),
220
+ thenExpr,
221
+ fallbackExpr
222
+ );
223
+ } else {
224
+ body = t.logicalExpression(
225
+ "&&",
226
+ t.identifier("v"),
227
+ thenExpr
228
+ );
229
+ }
230
+
231
+ const callback = t.arrowFunctionExpression(
232
+ [t.identifier("v")],
233
+ body
234
+ );
235
+
236
+ return t.callExpression(
237
+ t.memberExpression(
238
+ t.identifier("E"),
239
+ t.identifier("cond")
240
+ ),
241
+ [whenExpr, callback]
242
+ );
243
+ }
244
+
245
+ // ------------------------------------------------------------------
246
+ // Main transform
247
+ // ------------------------------------------------------------------
248
+
249
+ function transformNode(node) {
250
+ // Fragment
251
+ if (t.isJSXFragment(node)) {
252
+ const kids = buildChildren(node.children);
253
+ return t.callExpression(
254
+ t.memberExpression(t.identifier("E"), t.identifier("fragment")),
255
+ kids
256
+ );
257
+ }
258
+
259
+ const opening = node.openingElement;
260
+ const name = getName(opening.name);
261
+
262
+ // Control-flow components
263
+ if (name === "For" || name === "List") {
264
+ return transformFor(opening, node.children);
265
+ }
266
+ if (name === "Show" || name === "Cond") {
267
+ return transformShow(opening, node.children);
268
+ }
269
+
270
+ // Normal element / component
271
+ const attrs = buildAttributes(opening.attributes);
272
+ const kids = buildChildren(node.children);
273
+ const config = buildConfig(attrs, kids);
274
+
275
+ // Member expression <E.something>
276
+ if (t.isJSXMemberExpression(opening.name)) {
277
+ const callee = opening.name;
278
+ return t.callExpression(
279
+ t.memberExpression(
280
+ t.identifier(callee.object.name),
281
+ t.identifier(callee.property.name)
282
+ ),
283
+ [config]
284
+ );
285
+ }
286
+
287
+ // Component (PascalCase)
288
+ if (isComponent(name)) {
289
+ return t.callExpression(t.identifier(name), [config]);
290
+ }
291
+
292
+ // HTML / SVG tag
293
+ return t.callExpression(
294
+ t.memberExpression(t.identifier("E"), t.identifier(name)),
295
+ [config]
296
+ );
297
+ }
298
+
299
+ // ------------------------------------------------------------------
300
+ // Visitor
301
+ // ------------------------------------------------------------------
302
+
303
+ return {
304
+ name: "babel-plugin-minibum-jsx",
305
+ // This removes the need for users to manually add @babel/plugin-syntax-jsx to their configs.
306
+ inherits: syntaxJsx,
307
+ visitor: {
308
+ Program: {
309
+ enter(path) {
310
+ // JSX output uses E.*; provide the runtime automatically unless
311
+ // the user already declares/imports E.
312
+ if (!path.scope.hasBinding("E")) {
313
+ path.unshiftContainer(
314
+ "body",
315
+ t.importDeclaration(
316
+ [t.importDefaultSpecifier(t.identifier("E"))],
317
+ t.stringLiteral("minibum")
318
+ )
319
+ );
320
+ }
321
+ },
322
+ },
323
+ JSXElement(path) {
324
+ path.replaceWith(transformNode(path.node));
325
+ },
326
+ JSXFragment(path) {
327
+ path.replaceWith(transformNode(path.node));
328
+ },
329
+ },
330
+ };
331
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * babel-plugin-minibum-jsx
3
+ *
4
+ * Transforms classic JSX into MiniBum's fine-grained reactive API.
5
+ *
6
+ * Standard elements:
7
+ * <div className="c">{count}</div>
8
+ * → E.div({ className: "c", children: count })
9
+ *
10
+ * Control-flow:
11
+ * <For each={items}>{(item) => <li>{item}</li>}</For>
12
+ * → E.list(items, (item) => E.li({ children: item }))
13
+ *
14
+ * <Show when={visible}><p>Hi</p></Show>
15
+ * → E.cond(visible, () => E.p({ children: "Hi" }))
16
+ *
17
+ * <Show when={visible} fallback={<p>Loading</p>}>
18
+ * <p>Ready</p>
19
+ * </Show>
20
+ * → E.cond(visible, () => E.p({ children: "Ready" }), () => E.p({ children: "Loading" }))
21
+ *
22
+ * Also supports the aliases <List> and <Cond>.
23
+ */
24
+ export default function minibumJsx({ types: t }: {
25
+ types: any;
26
+ }): {
27
+ name: string;
28
+ inherits: (api: import("@babel/core").PluginAPI, options: object, dirname: string) => import("@babel/core").PluginObject<object & import("@babel/core").PluginPass<object>>;
29
+ visitor: {
30
+ Program: {
31
+ enter(path: any): void;
32
+ };
33
+ JSXElement(path: any): void;
34
+ JSXFragment(path: any): void;
35
+ };
36
+ };