mez-easyjs 0.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/LICENSE +21 -0
- package/README.md +40 -0
- package/bin/create-easy-app.js +60 -0
- package/bin/easy.js +7 -0
- package/compiler.d.ts +37 -0
- package/package.json +74 -0
- package/runtime.d.ts +103 -0
- package/src/commands/build.js +284 -0
- package/src/commands/create.js +476 -0
- package/src/commands/dev.js +434 -0
- package/src/commands/generate.js +104 -0
- package/src/commands/ws.js +157 -0
- package/src/compiler/codegen.js +1334 -0
- package/src/compiler/css.js +117 -0
- package/src/compiler/errors.js +65 -0
- package/src/compiler/expr.js +142 -0
- package/src/compiler/index.js +105 -0
- package/src/compiler/parse.js +115 -0
- package/src/compiler/strip-ts.js +39 -0
- package/src/compiler/template.js +419 -0
- package/src/index.js +128 -0
- package/src/runtime/app.js +115 -0
- package/src/runtime/component.js +222 -0
- package/src/runtime/css.js +35 -0
- package/src/runtime/errors.js +121 -0
- package/src/runtime/escape.js +62 -0
- package/src/runtime/events.js +151 -0
- package/src/runtime/hmr.js +157 -0
- package/src/runtime/index.js +40 -0
- package/src/runtime/overlay.js +95 -0
- package/src/runtime/reactive.js +172 -0
- package/src/runtime/router.js +423 -0
- package/src/runtime/scheduler.js +39 -0
- package/src/runtime/store.js +42 -0
- package/src/transform.js +106 -0
- package/templates/minimal/README.md +21 -0
- package/templates/minimal/easy.config.js +7 -0
- package/templates/minimal/index.html +13 -0
- package/templates/minimal/package.json +14 -0
- package/templates/minimal/src/App.easy +24 -0
- package/templates/minimal/src/main.js +4 -0
- package/templates/minimal/src/styles.css +14 -0
- package/templates/starter/README.md +18 -0
- package/templates/starter/easy.config.js +7 -0
- package/templates/starter/index.html +13 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/App.easy +69 -0
- package/templates/starter/src/components/Counter.easy +76 -0
- package/templates/starter/src/components/Greeting.easy +24 -0
- package/templates/starter/src/main.js +11 -0
- package/templates/starter/src/pages/About.easy +20 -0
- package/templates/starter/src/pages/Home.easy +69 -0
- package/templates/starter/src/styles.css +14 -0
- package/templates/tailwind/README.md +23 -0
- package/templates/tailwind/easy.config.js +7 -0
- package/templates/tailwind/index.html +19 -0
- package/templates/tailwind/package.json +14 -0
- package/templates/tailwind/src/App.easy +53 -0
- package/templates/tailwind/src/main.js +4 -0
- package/templates/tailwind/src/styles.css +4 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight HTML template parser → AST.
|
|
3
|
+
* Svelte-like: {expr}, onclick={handler}, {@html}, {#if}, {#each}
|
|
4
|
+
* Compat: {{expr}}, @click, e-if, e-for
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const VOID_TAGS = new Set([
|
|
8
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta',
|
|
9
|
+
'param', 'source', 'track', 'wbr'
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const EVENT_ATTRS = new Set([
|
|
13
|
+
'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout',
|
|
14
|
+
'onmousemove', 'onmouseenter', 'onmouseleave', 'onkeydown', 'onkeyup', 'onkeypress',
|
|
15
|
+
'oninput', 'onchange', 'onsubmit', 'onfocus', 'onblur', 'onscroll', 'onload',
|
|
16
|
+
'onerror', 'oncontextmenu', 'ontouchstart', 'ontouchend', 'ontouchmove'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function parseTemplate(html, options = {}) {
|
|
20
|
+
const expanded = expandControlBlocks(String(html || ''));
|
|
21
|
+
const tokens = tokenize(expanded);
|
|
22
|
+
const root = { type: 'Root', children: [] };
|
|
23
|
+
const stack = [root];
|
|
24
|
+
|
|
25
|
+
for (const token of tokens) {
|
|
26
|
+
const parent = stack[stack.length - 1];
|
|
27
|
+
|
|
28
|
+
if (token.type === 'text') {
|
|
29
|
+
const parts = splitInterpolations(token.value);
|
|
30
|
+
for (const part of parts) parent.children.push(part);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (token.type === 'comment') {
|
|
35
|
+
parent.children.push({ type: 'Comment', value: token.value });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (token.type === 'close') {
|
|
40
|
+
const name = token.name.toLowerCase();
|
|
41
|
+
if (stack.length > 1) {
|
|
42
|
+
const cur = stack[stack.length - 1];
|
|
43
|
+
if (cur.type === 'Element' && cur.tag.toLowerCase() === name) {
|
|
44
|
+
stack.pop();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (token.type === 'open' || token.type === 'self') {
|
|
51
|
+
const el = createElement(token);
|
|
52
|
+
parent.children.push(el);
|
|
53
|
+
if (token.type === 'open' && !VOID_TAGS.has(el.tag.toLowerCase()) && !token.selfClosing) {
|
|
54
|
+
stack.push(el);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return root;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* {#if expr}...{/if} → <div data-e-block="if" e-if="expr">...</div>
|
|
64
|
+
* {#each list as item}...{/each} → e-for
|
|
65
|
+
* {#each list as item, i}...{/each}
|
|
66
|
+
*/
|
|
67
|
+
function expandControlBlocks(html) {
|
|
68
|
+
let out = html;
|
|
69
|
+
|
|
70
|
+
// each first (may nest inside if)
|
|
71
|
+
out = out.replace(
|
|
72
|
+
/\{#each\s+([^}]+?)\s+as\s+([^}]+)\}([\s\S]*?)\{\/each\}/g,
|
|
73
|
+
(_, source, alias, body) => {
|
|
74
|
+
const parts = alias.split(',').map((s) => s.trim());
|
|
75
|
+
const item = parts[0];
|
|
76
|
+
const index = parts[1];
|
|
77
|
+
const forVal = index ? `${item}, ${index} in ${source.trim()}` : `${item} in ${source.trim()}`;
|
|
78
|
+
return `<div style="display:contents" e-for="${escapeAttr(forVal)}">${body}</div>`;
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
out = out.replace(
|
|
83
|
+
/\{#if\s+([^}]+)\}([\s\S]*?)\{\/if\}/g,
|
|
84
|
+
(_, cond, body) =>
|
|
85
|
+
`<div style="display:contents" e-if="${escapeAttr(cond.trim())}">${body}</div>`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createElement(token) {
|
|
92
|
+
const el = {
|
|
93
|
+
type: 'Element',
|
|
94
|
+
tag: token.name,
|
|
95
|
+
attrs: [],
|
|
96
|
+
directives: [],
|
|
97
|
+
events: [],
|
|
98
|
+
children: [],
|
|
99
|
+
isComponent: isComponentTag(token.name),
|
|
100
|
+
isRouterView: token.name.toLowerCase() === 'router-view'
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (const [rawName, rawValue] of Object.entries(token.attrs)) {
|
|
104
|
+
classifyAttr(el, rawName, rawValue === true ? '' : String(rawValue));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return el;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function classifyAttr(el, name, value) {
|
|
111
|
+
// Primary: onclick={handler} — brace form only (CSP). Quoted onclick="..." rejected.
|
|
112
|
+
if (EVENT_ATTRS.has(name.toLowerCase())) {
|
|
113
|
+
const raw = String(value || '');
|
|
114
|
+
if (!raw.startsWith('\0expr:')) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Inline event attribute ${name}="..." is not allowed. Use ${name}={handler} (compiles to data-e-on).`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const handler = stripBraces(value);
|
|
120
|
+
if (!isSafeHandler(handler)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Invalid event handler "${handler}". Use a method name like onclick={go} or onclick={go()}.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const event = name.toLowerCase().slice(2); // onclick → click
|
|
126
|
+
el.events.push({ event, handler, modifiers: [] });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// on:click={handler}
|
|
131
|
+
if (name.startsWith('on:') || name.startsWith('on-')) {
|
|
132
|
+
const handler = stripBraces(value);
|
|
133
|
+
if (!String(value || '').startsWith('\0expr:') && !isSafeHandler(handler)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Invalid event handler for ${name}. Use on:click={handler}.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!isSafeHandler(handler)) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Invalid event handler "${handler}". Use a method name like on:click={go}.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const event = name.slice(3);
|
|
144
|
+
el.events.push({ event, handler, modifiers: [] });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Compat: @click / e-on:click
|
|
149
|
+
if (name.startsWith('@') || name.startsWith('e-on:')) {
|
|
150
|
+
const handler = stripBraces(value);
|
|
151
|
+
if (!isSafeHandler(handler)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Invalid event handler "${handler}". Use @click={go} or e-on:click={go}.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const event = name.startsWith('@') ? name.slice(1) : name.slice(5);
|
|
157
|
+
el.events.push({ event, handler, modifiers: [] });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// bind:value={x} → model; bind:other={x} → attr bind (onclick etc. rejected in codegen)
|
|
162
|
+
if (name === 'bind:value' || name === 'bind:checked') {
|
|
163
|
+
el.directives.push({ name: 'model', value: stripBraces(value) });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (name.startsWith('bind:') && name.length > 5) {
|
|
167
|
+
el.directives.push({
|
|
168
|
+
name: 'bind',
|
|
169
|
+
arg: name.slice(5),
|
|
170
|
+
value: stripBraces(value)
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// :title / e-bind:title / title={expr} dynamic when braced originally
|
|
176
|
+
if ((name.startsWith(':') && name !== ':') || name.startsWith('e-bind:')) {
|
|
177
|
+
const attr = name.startsWith(':') ? name.slice(1) : name.slice(7);
|
|
178
|
+
el.directives.push({ name: 'bind', arg: attr, value: stripBraces(value) });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// value={expr} without bind: — treat as bind if looks like expression (no quotes in original)
|
|
183
|
+
// Handled via attr meta from tokenizer: valueMayBeExpr
|
|
184
|
+
if (name.endsWith('}') || false) {
|
|
185
|
+
/* noop */
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// e-model / model
|
|
189
|
+
if (name === 'e-model' || name === 'model') {
|
|
190
|
+
el.directives.push({ name: 'model', value: stripBraces(value) });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (name === 'e-html' || name === '@html') {
|
|
195
|
+
el.directives.push({ name: 'html', value: stripBraces(value), raw: true });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (name.startsWith('e-')) {
|
|
200
|
+
const dir = name.slice(2);
|
|
201
|
+
el.directives.push({ name: dir, value: stripBraces(value) });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Dynamic attr: name was parsed from foo={bar}
|
|
206
|
+
if (value.startsWith('\0expr:')) {
|
|
207
|
+
const expr = value.slice(6);
|
|
208
|
+
el.directives.push({ name: 'bind', arg: name, value: expr });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
el.attrs.push({ name, value, dynamic: false });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function stripBraces(value) {
|
|
216
|
+
const v = String(value || '').trim();
|
|
217
|
+
if (v.startsWith('\0expr:')) return v.slice(6).trim();
|
|
218
|
+
if (v.startsWith('{') && v.endsWith('}')) return v.slice(1, -1).trim();
|
|
219
|
+
return v.replace(/^['"]|['"]$/g, '');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Method name, optional empty call: go | go() */
|
|
223
|
+
function isSafeHandler(handler) {
|
|
224
|
+
return /^[A-Za-z_$][\w$]*(?:\s*\(\s*\))?$/.test(String(handler || '').trim());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isComponentTag(tag) {
|
|
228
|
+
return /^[A-Z]/.test(tag);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function splitInterpolations(text) {
|
|
232
|
+
const nodes = [];
|
|
233
|
+
// {@html expr} | {{ expr }} | { expr } (single braces last so {@html} wins)
|
|
234
|
+
const re = /\{@html\s+([\s\S]+?)\}|\{\{([\s\S]+?)\}\}|\{([^{}@][^{}]*)\}/g;
|
|
235
|
+
let last = 0;
|
|
236
|
+
let m;
|
|
237
|
+
while ((m = re.exec(text)) !== null) {
|
|
238
|
+
if (m.index > last) {
|
|
239
|
+
nodes.push({ type: 'Text', value: text.slice(last, m.index) });
|
|
240
|
+
}
|
|
241
|
+
if (m[1] != null) {
|
|
242
|
+
nodes.push({ type: 'RawHtml', expression: m[1].trim(), raw: true });
|
|
243
|
+
} else if (m[2] != null) {
|
|
244
|
+
nodes.push({ type: 'Interpolation', expression: m[2].trim(), raw: false });
|
|
245
|
+
} else {
|
|
246
|
+
const expr = m[3].trim();
|
|
247
|
+
// Skip CSS-like or empty; skip {# and {/
|
|
248
|
+
if (expr && !expr.startsWith('#') && !expr.startsWith('/')) {
|
|
249
|
+
nodes.push({ type: 'Interpolation', expression: expr, raw: false });
|
|
250
|
+
} else {
|
|
251
|
+
nodes.push({ type: 'Text', value: m[0] });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
last = m.index + m[0].length;
|
|
255
|
+
}
|
|
256
|
+
if (last < text.length) {
|
|
257
|
+
nodes.push({ type: 'Text', value: text.slice(last) });
|
|
258
|
+
}
|
|
259
|
+
return nodes.filter((n) => !(n.type === 'Text' && n.value === ''));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function tokenize(html) {
|
|
263
|
+
const tokens = [];
|
|
264
|
+
let i = 0;
|
|
265
|
+
|
|
266
|
+
while (i < html.length) {
|
|
267
|
+
if (html.startsWith('<!--', i)) {
|
|
268
|
+
const end = html.indexOf('-->', i + 4);
|
|
269
|
+
const value = html.slice(i + 4, end === -1 ? html.length : end);
|
|
270
|
+
tokens.push({ type: 'comment', value });
|
|
271
|
+
i = end === -1 ? html.length : end + 3;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (html[i] === '<') {
|
|
276
|
+
const close = html.startsWith('</', i);
|
|
277
|
+
const end = findTagEnd(html, i);
|
|
278
|
+
if (end === -1) break;
|
|
279
|
+
const raw = html.slice(i + (close ? 2 : 1), end);
|
|
280
|
+
const selfClosing = !close && /\/\s*$/.test(raw);
|
|
281
|
+
const cleaned = raw.replace(/\/\s*$/, '').trim();
|
|
282
|
+
const nameMatch = cleaned.match(/^([^\s/>]+)/);
|
|
283
|
+
if (!nameMatch) {
|
|
284
|
+
i = end + 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const name = nameMatch[1];
|
|
288
|
+
const attrs = close ? {} : parseTagAttrs(cleaned.slice(name.length));
|
|
289
|
+
tokens.push({
|
|
290
|
+
type: close ? 'close' : selfClosing || VOID_TAGS.has(name.toLowerCase()) ? 'self' : 'open',
|
|
291
|
+
name,
|
|
292
|
+
attrs,
|
|
293
|
+
selfClosing
|
|
294
|
+
});
|
|
295
|
+
i = end + 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const next = html.indexOf('<', i);
|
|
300
|
+
const value = html.slice(i, next === -1 ? html.length : next);
|
|
301
|
+
tokens.push({ type: 'text', value });
|
|
302
|
+
i = next === -1 ? html.length : next;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return tokens;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Find `>` not inside quotes or {...} */
|
|
309
|
+
function findTagEnd(html, start) {
|
|
310
|
+
let i = start + 1;
|
|
311
|
+
let quote = null;
|
|
312
|
+
let brace = 0;
|
|
313
|
+
while (i < html.length) {
|
|
314
|
+
const ch = html[i];
|
|
315
|
+
if (quote) {
|
|
316
|
+
if (ch === '\\') {
|
|
317
|
+
i += 2;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (ch === quote) quote = null;
|
|
321
|
+
i++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (ch === '"' || ch === "'") {
|
|
325
|
+
quote = ch;
|
|
326
|
+
i++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (ch === '{') {
|
|
330
|
+
brace++;
|
|
331
|
+
i++;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (ch === '}') {
|
|
335
|
+
brace = Math.max(0, brace - 1);
|
|
336
|
+
i++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (ch === '>' && brace === 0) return i;
|
|
340
|
+
i++;
|
|
341
|
+
}
|
|
342
|
+
return -1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Parse attributes including onclick={greet} and class="x".
|
|
347
|
+
* Brace values are marked with \0expr: prefix.
|
|
348
|
+
*/
|
|
349
|
+
function parseTagAttrs(str) {
|
|
350
|
+
const attrs = {};
|
|
351
|
+
let i = 0;
|
|
352
|
+
const s = str;
|
|
353
|
+
|
|
354
|
+
while (i < s.length) {
|
|
355
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
356
|
+
if (i >= s.length) break;
|
|
357
|
+
|
|
358
|
+
const nameStart = i;
|
|
359
|
+
while (i < s.length && /[^\s=]/.test(s[i])) i++;
|
|
360
|
+
const name = s.slice(nameStart, i);
|
|
361
|
+
if (!name) break;
|
|
362
|
+
|
|
363
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
364
|
+
|
|
365
|
+
if (s[i] !== '=') {
|
|
366
|
+
attrs[name] = true;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
i++; // =
|
|
370
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
371
|
+
|
|
372
|
+
if (s[i] === '{') {
|
|
373
|
+
let depth = 0;
|
|
374
|
+
const start = i;
|
|
375
|
+
while (i < s.length) {
|
|
376
|
+
if (s[i] === '{') depth++;
|
|
377
|
+
else if (s[i] === '}') {
|
|
378
|
+
depth--;
|
|
379
|
+
if (depth === 0) {
|
|
380
|
+
i++;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
i++;
|
|
385
|
+
}
|
|
386
|
+
const inner = s.slice(start + 1, i - 1).trim();
|
|
387
|
+
attrs[name] = '\0expr:' + inner;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (s[i] === '"' || s[i] === "'") {
|
|
392
|
+
const q = s[i];
|
|
393
|
+
i++;
|
|
394
|
+
let val = '';
|
|
395
|
+
while (i < s.length && s[i] !== q) {
|
|
396
|
+
if (s[i] === '\\') {
|
|
397
|
+
val += s[i + 1] || '';
|
|
398
|
+
i += 2;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
val += s[i];
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
if (s[i] === q) i++;
|
|
405
|
+
attrs[name] = val;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const start = i;
|
|
410
|
+
while (i < s.length && /[^\s>]/.test(s[i])) i++;
|
|
411
|
+
attrs[name] = s.slice(start, i);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return attrs;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function escapeAttr(s) {
|
|
418
|
+
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
419
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { createProject } from './commands/create.js';
|
|
4
|
+
import { runDev } from './commands/dev.js';
|
|
5
|
+
import { runBuild } from './commands/build.js';
|
|
6
|
+
import { generate } from './commands/generate.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export const PKG_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
export const MONOREPO_ROOT = path.resolve(__dirname, '../../..');
|
|
11
|
+
|
|
12
|
+
export async function run(argv) {
|
|
13
|
+
const { command, args, flags } = parseArgs(argv);
|
|
14
|
+
|
|
15
|
+
if (!command || command === 'help' || flags.help) {
|
|
16
|
+
printHelp();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
switch (command) {
|
|
21
|
+
case 'create':
|
|
22
|
+
case 'scaffold':
|
|
23
|
+
await createProject(args[0], flags);
|
|
24
|
+
break;
|
|
25
|
+
case 'dev':
|
|
26
|
+
await runDev(flags);
|
|
27
|
+
break;
|
|
28
|
+
case 'build':
|
|
29
|
+
await runBuild(flags);
|
|
30
|
+
break;
|
|
31
|
+
case 'generate':
|
|
32
|
+
case 'g':
|
|
33
|
+
await generate(args[0], args[1], flags);
|
|
34
|
+
break;
|
|
35
|
+
case 'version':
|
|
36
|
+
case '-v':
|
|
37
|
+
case '--version':
|
|
38
|
+
console.log('easy.js 0.1.0');
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
console.error(`Unknown command: ${command}`);
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const flags = {};
|
|
49
|
+
const args = [];
|
|
50
|
+
let command = null;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const a = argv[i];
|
|
54
|
+
if (a.startsWith('--')) {
|
|
55
|
+
const key = a.slice(2);
|
|
56
|
+
const next = argv[i + 1];
|
|
57
|
+
if (next && !next.startsWith('-')) {
|
|
58
|
+
flags[key] = next;
|
|
59
|
+
i++;
|
|
60
|
+
} else {
|
|
61
|
+
flags[key] = true;
|
|
62
|
+
}
|
|
63
|
+
} else if (a.startsWith('-') && a.length === 2) {
|
|
64
|
+
const key = { p: 'port', c: 'cwd', o: 'outDir' }[a[1]] || a[1];
|
|
65
|
+
const next = argv[i + 1];
|
|
66
|
+
if (next && !next.startsWith('-')) {
|
|
67
|
+
flags[key] = next;
|
|
68
|
+
i++;
|
|
69
|
+
} else {
|
|
70
|
+
flags[key] = true;
|
|
71
|
+
}
|
|
72
|
+
} else if (!command) {
|
|
73
|
+
command = a;
|
|
74
|
+
} else {
|
|
75
|
+
args.push(a);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { command, args, flags };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printHelp() {
|
|
83
|
+
console.log(`
|
|
84
|
+
Easy.js CLI v0.1.0
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
easy <command> [options]
|
|
88
|
+
create-easy-app [name] [options]
|
|
89
|
+
|
|
90
|
+
Commands:
|
|
91
|
+
create [name] Interactive project wizard (alias: scaffold)
|
|
92
|
+
dev Start dev server with HMR
|
|
93
|
+
build Compile for production → dist/assets/ (Vite-like)
|
|
94
|
+
generate <type> <n> Scaffold component|page (alias: g)
|
|
95
|
+
help Show this help
|
|
96
|
+
|
|
97
|
+
Create options:
|
|
98
|
+
--name <n> Project name (npm-valid)
|
|
99
|
+
--template <id> minimal | starter | tailwind
|
|
100
|
+
--lang js JavaScript only (TypeScript coming soon)
|
|
101
|
+
--yes Skip confirmation prompts
|
|
102
|
+
--skip-install Skip npm install
|
|
103
|
+
--skip-git Skip git init / commit
|
|
104
|
+
--cwd <path> Parent directory for the new project
|
|
105
|
+
|
|
106
|
+
Dev / build options:
|
|
107
|
+
--cwd <path> Project directory (default: .)
|
|
108
|
+
--port <n> Dev server port (default: 5173)
|
|
109
|
+
--outDir <path> Build output (default: dist)
|
|
110
|
+
--open Open browser on dev
|
|
111
|
+
--minify Minify build output (default: true)
|
|
112
|
+
--no-minify Skip minify
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
easy create
|
|
116
|
+
easy create my-app --template starter --yes
|
|
117
|
+
create-easy-app my-app --template tailwind --yes
|
|
118
|
+
easy dev --cwd examples/demo
|
|
119
|
+
easy build
|
|
120
|
+
easy generate component Counter
|
|
121
|
+
easy generate page About
|
|
122
|
+
|
|
123
|
+
Templates:
|
|
124
|
+
minimal Clean skeleton (App.easy + config)
|
|
125
|
+
starter Counter, routing, parent→child props
|
|
126
|
+
tailwind Tailwind Play CDN only (no PostCSS / npm tailwindcss)
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mountComponent, unmountComponent } from './component.js';
|
|
2
|
+
import { setupDelegation, createEventContext } from './events.js';
|
|
3
|
+
import { EasyError, reportError } from './errors.js';
|
|
4
|
+
import { installOverlay } from './overlay.js';
|
|
5
|
+
import { enableHmr } from './hmr.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create an Easy.js application.
|
|
9
|
+
*/
|
|
10
|
+
export function createApp(rootComponent) {
|
|
11
|
+
const context = {
|
|
12
|
+
plugins: [],
|
|
13
|
+
provides: Object.create(null),
|
|
14
|
+
router: null,
|
|
15
|
+
store: null,
|
|
16
|
+
rootInstance: null,
|
|
17
|
+
config: {
|
|
18
|
+
dev: typeof window !== 'undefined' && !!window.__EASY_DEV__
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const nodeContext = new WeakMap();
|
|
23
|
+
|
|
24
|
+
// Shared with router / child mounts so route pages can register event contexts.
|
|
25
|
+
context.registerContext = (node, ctx) => {
|
|
26
|
+
nodeContext.set(node, ctx);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (context.config.dev) {
|
|
30
|
+
installOverlay();
|
|
31
|
+
enableHmr();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const app = {
|
|
35
|
+
config: context.config,
|
|
36
|
+
_context: context,
|
|
37
|
+
|
|
38
|
+
use(plugin, options) {
|
|
39
|
+
if (plugin && typeof plugin.install === 'function') {
|
|
40
|
+
plugin.install(app, options);
|
|
41
|
+
} else if (typeof plugin === 'function') {
|
|
42
|
+
plugin(app, options);
|
|
43
|
+
}
|
|
44
|
+
context.plugins.push(plugin);
|
|
45
|
+
return app;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
provide(key, value) {
|
|
49
|
+
context.provides[key] = value;
|
|
50
|
+
return app;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
mount(selectorOrEl) {
|
|
54
|
+
const el =
|
|
55
|
+
typeof selectorOrEl === 'string'
|
|
56
|
+
? document.querySelector(selectorOrEl)
|
|
57
|
+
: selectorOrEl;
|
|
58
|
+
|
|
59
|
+
if (!el) {
|
|
60
|
+
const err = new EasyError(`Mount target not found: ${selectorOrEl}`, {
|
|
61
|
+
hint: 'Ensure index.html has <div id="app"></div> (or pass a valid selector).'
|
|
62
|
+
});
|
|
63
|
+
reportError(err);
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setupDelegation(el, (node) => {
|
|
68
|
+
let n = node;
|
|
69
|
+
while (n) {
|
|
70
|
+
if (nodeContext.has(n)) return nodeContext.get(n);
|
|
71
|
+
n = n.parentNode;
|
|
72
|
+
}
|
|
73
|
+
return context.rootInstance
|
|
74
|
+
? createEventContext(
|
|
75
|
+
context.rootInstance.methods,
|
|
76
|
+
context.rootInstance.state,
|
|
77
|
+
{ $router: context.router }
|
|
78
|
+
)
|
|
79
|
+
: null;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
context.rootInstance = mountComponent(rootComponent, el, {}, {
|
|
84
|
+
app,
|
|
85
|
+
...context
|
|
86
|
+
});
|
|
87
|
+
} catch (err) {
|
|
88
|
+
reportError(err, { component: 'Root' });
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (context.rootInstance) {
|
|
93
|
+
nodeContext.set(
|
|
94
|
+
el,
|
|
95
|
+
createEventContext(
|
|
96
|
+
context.rootInstance.methods,
|
|
97
|
+
context.rootInstance.state,
|
|
98
|
+
{ $router: context.router }
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return context.rootInstance;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
unmount() {
|
|
107
|
+
if (context.rootInstance) {
|
|
108
|
+
unmountComponent(context.rootInstance);
|
|
109
|
+
context.rootInstance = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return app;
|
|
115
|
+
}
|