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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/bin/create-easy-app.js +60 -0
  4. package/bin/easy.js +7 -0
  5. package/compiler.d.ts +37 -0
  6. package/package.json +74 -0
  7. package/runtime.d.ts +103 -0
  8. package/src/commands/build.js +284 -0
  9. package/src/commands/create.js +476 -0
  10. package/src/commands/dev.js +434 -0
  11. package/src/commands/generate.js +104 -0
  12. package/src/commands/ws.js +157 -0
  13. package/src/compiler/codegen.js +1334 -0
  14. package/src/compiler/css.js +117 -0
  15. package/src/compiler/errors.js +65 -0
  16. package/src/compiler/expr.js +142 -0
  17. package/src/compiler/index.js +105 -0
  18. package/src/compiler/parse.js +115 -0
  19. package/src/compiler/strip-ts.js +39 -0
  20. package/src/compiler/template.js +419 -0
  21. package/src/index.js +128 -0
  22. package/src/runtime/app.js +115 -0
  23. package/src/runtime/component.js +222 -0
  24. package/src/runtime/css.js +35 -0
  25. package/src/runtime/errors.js +121 -0
  26. package/src/runtime/escape.js +62 -0
  27. package/src/runtime/events.js +151 -0
  28. package/src/runtime/hmr.js +157 -0
  29. package/src/runtime/index.js +40 -0
  30. package/src/runtime/overlay.js +95 -0
  31. package/src/runtime/reactive.js +172 -0
  32. package/src/runtime/router.js +423 -0
  33. package/src/runtime/scheduler.js +39 -0
  34. package/src/runtime/store.js +42 -0
  35. package/src/transform.js +106 -0
  36. package/templates/minimal/README.md +21 -0
  37. package/templates/minimal/easy.config.js +7 -0
  38. package/templates/minimal/index.html +13 -0
  39. package/templates/minimal/package.json +14 -0
  40. package/templates/minimal/src/App.easy +24 -0
  41. package/templates/minimal/src/main.js +4 -0
  42. package/templates/minimal/src/styles.css +14 -0
  43. package/templates/starter/README.md +18 -0
  44. package/templates/starter/easy.config.js +7 -0
  45. package/templates/starter/index.html +13 -0
  46. package/templates/starter/package.json +14 -0
  47. package/templates/starter/src/App.easy +69 -0
  48. package/templates/starter/src/components/Counter.easy +76 -0
  49. package/templates/starter/src/components/Greeting.easy +24 -0
  50. package/templates/starter/src/main.js +11 -0
  51. package/templates/starter/src/pages/About.easy +20 -0
  52. package/templates/starter/src/pages/Home.easy +69 -0
  53. package/templates/starter/src/styles.css +14 -0
  54. package/templates/tailwind/README.md +23 -0
  55. package/templates/tailwind/easy.config.js +7 -0
  56. package/templates/tailwind/index.html +19 -0
  57. package/templates/tailwind/package.json +14 -0
  58. package/templates/tailwind/src/App.easy +53 -0
  59. package/templates/tailwind/src/main.js +4 -0
  60. 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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
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
+ }