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,1334 @@
1
+ /**
2
+ * Generate optimized runtime JS from a template AST.
3
+ * CSP-safe: no eval / new Function. XSS-safe: escape by default; {@html} opt-in.
4
+ */
5
+
6
+ import { compileExpression } from './expr.js';
7
+ import { CompileError, locate } from './errors.js';
8
+
9
+ let uid = 0;
10
+
11
+ export function generate(ast, options = {}) {
12
+ uid = 0;
13
+ const ctx = {
14
+ scopeId: options.scopeId,
15
+ filename: options.filename || 'component.easy',
16
+ styles: options.styles || '',
17
+ script: options.script || '',
18
+ source: options.source || '',
19
+ bindings: [],
20
+ fors: [],
21
+ ifs: [],
22
+ models: [],
23
+ components: [],
24
+ routerViews: [],
25
+ boundaries: [],
26
+ dependencies: []
27
+ };
28
+
29
+ const bodyHtml = genChildren(ast.children, ctx, {});
30
+ const scriptInfo = analyzeScript(options.script || '', {
31
+ lang: options.lang || 'js'
32
+ });
33
+ ctx.dependencies = scriptInfo.imports.map((i) => i.source);
34
+
35
+ const code = emitModule(ctx, scriptInfo, bodyHtml);
36
+ return { code, dependencies: [...new Set(ctx.dependencies)] };
37
+ }
38
+
39
+ function genChildren(children, ctx, meta) {
40
+ return (children || []).map((child) => genNode(child, ctx, meta)).join('');
41
+ }
42
+
43
+ function genNode(node, ctx, meta) {
44
+ if (node.type === 'Text') return escapeHtml(node.value);
45
+
46
+ if (node.type === 'Comment') return `<!--${node.value}-->`;
47
+
48
+ if (node.type === 'Interpolation') {
49
+ const id = nextId();
50
+ const { keys } = compileExpression(node.expression);
51
+ ctx.bindings.push({
52
+ id,
53
+ keys,
54
+ kind: 'text',
55
+ expression: node.expression,
56
+ raw: false
57
+ });
58
+ return `<span data-e="${id}"></span>`;
59
+ }
60
+
61
+ if (node.type === 'RawHtml') {
62
+ const id = nextId();
63
+ const { keys } = compileExpression(node.expression);
64
+ ctx.bindings.push({
65
+ id,
66
+ keys,
67
+ kind: 'rawHtml',
68
+ expression: node.expression,
69
+ raw: true
70
+ });
71
+ return `<span data-e="${id}" data-e-raw="1"></span>`;
72
+ }
73
+
74
+ if (node.type === 'Element') return genElement(node, ctx, meta);
75
+ return '';
76
+ }
77
+
78
+ function genElement(el, ctx, meta) {
79
+ const tagLower = el.tag.toLowerCase();
80
+
81
+ if (el.isRouterView || tagLower === 'router-view') {
82
+ const id = nextId();
83
+ ctx.routerViews.push({ id });
84
+ return `<div data-e-router="${id}"></div>`;
85
+ }
86
+
87
+ if (tagLower === 'error-boundary' || el.tag === 'ErrorBoundary') {
88
+ return genErrorBoundary(el, ctx, meta);
89
+ }
90
+
91
+ const dirFor = el.directives.find((d) => d.name === 'for');
92
+ const dirIf = el.directives.find((d) => d.name === 'if');
93
+ const dirElseIf = el.directives.find((d) => d.name === 'else-if');
94
+ const dirElse = el.directives.find((d) => d.name === 'else');
95
+ const dirShow = el.directives.find((d) => d.name === 'show');
96
+ const dirText = el.directives.find((d) => d.name === 'text');
97
+ const dirHtml = el.directives.find((d) => d.name === 'html');
98
+ const dirModel = el.directives.find((d) => d.name === 'model');
99
+ const dirBoundary = el.directives.find((d) => d.name === 'error-boundary');
100
+ const binds = el.directives.filter((d) => d.name === 'bind');
101
+
102
+ if (dirFor) {
103
+ const id = nextId();
104
+ const parsed = parseFor(dirFor.value);
105
+ const { keys } = compileExpression(parsed.source);
106
+ const innerEl = { ...el, directives: el.directives.filter((d) => d.name !== 'for') };
107
+ const itemTemplate = genRawTemplate(innerEl, ctx.scopeId);
108
+ ctx.fors.push({ id, ...parsed, keys, itemTemplate });
109
+ return `<div data-e-for="${id}" style="display:contents"></div>`;
110
+ }
111
+
112
+ if (dirIf) {
113
+ const id = nextId();
114
+ const { keys } = compileExpression(dirIf.value);
115
+ const inner = { ...el, directives: el.directives.filter((d) => d.name !== 'if') };
116
+ const html = genElementWithoutStructural(inner, ctx, meta);
117
+ ctx.ifs.push({ id, condition: dirIf.value, keys, html, kind: 'if' });
118
+ return `<template data-e-if="${id}"></template>`;
119
+ }
120
+
121
+ if (dirElseIf || dirElse) {
122
+ const id = nextId();
123
+ const condition = dirElseIf ? dirElseIf.value : 'true';
124
+ const { keys } = compileExpression(condition);
125
+ const inner = {
126
+ ...el,
127
+ directives: el.directives.filter((d) => d.name !== 'else' && d.name !== 'else-if')
128
+ };
129
+ const html = genElementWithoutStructural(inner, ctx, meta);
130
+ ctx.ifs.push({
131
+ id,
132
+ condition,
133
+ keys,
134
+ html,
135
+ kind: dirElse ? 'else' : 'else-if'
136
+ });
137
+ return `<template data-e-if="${id}"></template>`;
138
+ }
139
+
140
+ if (dirBoundary) {
141
+ return genErrorBoundary(el, ctx, meta, dirBoundary.value || 'Something went wrong.');
142
+ }
143
+
144
+ return genElementWithoutStructural(el, ctx, meta);
145
+ }
146
+
147
+ function genErrorBoundary(el, ctx, meta, fallbackAttr) {
148
+ const id = nextId();
149
+ const fallback =
150
+ fallbackAttr ||
151
+ (el.attrs.find((a) => a.name === 'fallback') || {}).value ||
152
+ 'Something went wrong in this section.';
153
+ const childrenHtml = genChildren(el.children, ctx, { ...meta, inBoundary: id });
154
+ ctx.boundaries.push({ id, fallback });
155
+ return `<div data-e-boundary="${id}">${childrenHtml}</div>`;
156
+ }
157
+
158
+ function genElementWithoutStructural(el, ctx, meta) {
159
+ const dirShow = el.directives.find((d) => d.name === 'show');
160
+ const dirText = el.directives.find((d) => d.name === 'text');
161
+ const dirHtml = el.directives.find((d) => d.name === 'html');
162
+ const dirModel = el.directives.find((d) => d.name === 'model');
163
+ const binds = el.directives.filter((d) => d.name === 'bind');
164
+
165
+ if (el.isComponent) {
166
+ const id = nextId();
167
+ const propBinds = {};
168
+ for (const b of binds) propBinds[b.arg] = b.value;
169
+ // Static attrs as literal props
170
+ for (const a of el.attrs) {
171
+ if (!(a.name in propBinds)) propBinds[a.name] = JSON.stringify(a.value);
172
+ }
173
+ ctx.components.push({
174
+ id,
175
+ name: el.tag,
176
+ props: propBinds,
177
+ keys: binds.flatMap((b) => compileExpression(b.value).keys),
178
+ children: el.children || []
179
+ });
180
+ return `<div data-e-comp="${id}" style="display:contents"></div>`;
181
+ }
182
+
183
+ const id = nextId();
184
+ const tag = el.tag;
185
+ const attrParts = [];
186
+ const scopeAttr = ctx.scopeId ? ` ${ctx.scopeId}` : '';
187
+
188
+ for (const a of el.attrs) {
189
+ const staticAttr = emitStaticAttr(a, ctx);
190
+ if (staticAttr) attrParts.push(staticAttr);
191
+ }
192
+
193
+ for (const b of binds) {
194
+ if (isDangerousAttrName(b.arg)) {
195
+ throw new CompileError({
196
+ message: `Refusing to bind dangerous attribute "${b.arg}"`,
197
+ filename: ctx.filename,
198
+ ...locate(ctx.source || '', b.arg),
199
+ source: ctx.source || '',
200
+ hint: 'Use onclick={handler} (compiles to data-e-on). Never bind on* or srcdoc.'
201
+ });
202
+ }
203
+ const bid = nextId();
204
+ ctx.bindings.push({
205
+ id: bid,
206
+ keys: compileExpression(b.value).keys,
207
+ kind: 'attr',
208
+ attr: b.arg,
209
+ expression: b.value,
210
+ ownerHint: id
211
+ });
212
+ attrParts.push(` data-e-bind-${b.arg}="${bid}"`);
213
+ }
214
+
215
+ if (el.events.length) {
216
+ // data-e-on only — never inline onclick= (CSP)
217
+ const spec = el.events.map((e) => `${e.event}:${stripCall(e.handler)}`).join(';');
218
+ attrParts.push(` data-e-on="${escapeAttr(spec)}"`);
219
+ }
220
+
221
+ if (dirModel) {
222
+ const mid = nextId();
223
+ ctx.models.push({
224
+ id: mid,
225
+ keys: compileExpression(dirModel.value).keys,
226
+ expression: dirModel.value,
227
+ ownerId: id
228
+ });
229
+ attrParts.push(` data-e-model="${mid}"`);
230
+ attrParts.push(` data-e-on="input:__model_${mid}"`);
231
+ }
232
+
233
+ if (dirShow || dirText || dirHtml || binds.length || el.events.length || dirModel) {
234
+ attrParts.push(` data-e-node="${id}"`);
235
+ }
236
+
237
+ if (dirShow) {
238
+ ctx.bindings.push({
239
+ id,
240
+ keys: compileExpression(dirShow.value).keys,
241
+ kind: 'show',
242
+ expression: dirShow.value
243
+ });
244
+ }
245
+
246
+ if (dirText) {
247
+ ctx.bindings.push({
248
+ id,
249
+ keys: compileExpression(dirText.value).keys,
250
+ kind: 'textContent',
251
+ expression: dirText.value,
252
+ raw: false
253
+ });
254
+ }
255
+
256
+ if (dirHtml) {
257
+ // Explicit opt-in only (e-html / @html attr)
258
+ ctx.bindings.push({
259
+ id,
260
+ keys: compileExpression(dirHtml.value).keys,
261
+ kind: 'rawHtml',
262
+ expression: dirHtml.value,
263
+ raw: true
264
+ });
265
+ }
266
+
267
+ const open = `<${tag}${scopeAttr}${attrParts.join('')}>`;
268
+ if (isVoid(tag)) return open;
269
+
270
+ const children =
271
+ dirText || dirHtml ? '' : genChildren(el.children, ctx, meta);
272
+ return `${open}${children}</${tag}>`;
273
+ }
274
+
275
+ function genRawTemplate(el, scopeId) {
276
+ if (el.type === 'Text') return escapeHtml(el.value);
277
+ if (el.type === 'Interpolation') return `{${el.expression}}`;
278
+ if (el.type === 'RawHtml') return `{@html ${el.expression}}`;
279
+ if (el.type === 'Comment') return `<!--${el.value}-->`;
280
+ if (el.type !== 'Element') return '';
281
+
282
+ const tag = el.tag;
283
+ const parts = [];
284
+ const scopeAttr = scopeId ? ` ${scopeId}` : '';
285
+ for (const a of el.attrs) {
286
+ const staticAttr = emitStaticAttr(a, { filename: 'template' });
287
+ if (staticAttr) parts.push(staticAttr);
288
+ }
289
+ if (el.events.length) {
290
+ const spec = el.events.map((e) => `${e.event}:${stripCall(e.handler)}`).join(';');
291
+ parts.push(` data-e-on="${escapeAttr(spec)}"`);
292
+ }
293
+ const open = `<${tag}${scopeAttr}${parts.join('')}>`;
294
+ if (isVoid(tag)) return open;
295
+ const children = (el.children || [])
296
+ .map((c) => {
297
+ if (c.type === 'Element') return genRawTemplate(c, scopeId);
298
+ if (c.type === 'Interpolation') return `{${c.expression}}`;
299
+ if (c.type === 'RawHtml') return `{@html ${c.expression}}`;
300
+ if (c.type === 'Text') return escapeHtml(c.value);
301
+ return '';
302
+ })
303
+ .join('');
304
+ return `${open}${children}</${tag}>`;
305
+ }
306
+
307
+ function emitModule(ctx, scriptInfo, bodyHtml) {
308
+ const { scopeId, styles, filename } = ctx;
309
+ const htmlLit = JSON.stringify(bodyHtml);
310
+ const importLines = scriptInfo.imports
311
+ .map((imp) => formatImport(imp))
312
+ .join('\n');
313
+
314
+ const setupBody = scriptInfo.setupBody || defaultSetupBody();
315
+
316
+ return `/* Compiled by Easy.js from ${filename} */
317
+ import {
318
+ defineComponent, reactive, injectStyles, replaceStyles,
319
+ mountComponent, unmountComponent,
320
+ escapeHtml, sanitizeUrl,
321
+ createBoundary, pushBoundary, reportError, EasyError,
322
+ registerHmrInstance, createEventContext
323
+ } from 'mez-easyjs';
324
+ ${importLines}
325
+
326
+ const __SCOPE = ${JSON.stringify(scopeId)};
327
+ const __STYLES = ${JSON.stringify(styles)};
328
+ const __HTML = ${htmlLit};
329
+ const __FILE = ${JSON.stringify(filename)};
330
+ const __HMR_ID = import.meta.url;
331
+
332
+ ${scriptInfo.prelude}
333
+
334
+ export default defineComponent({
335
+ name: ${JSON.stringify(filename.replace(/\\.easy$/i, ''))},
336
+ scopeId: __SCOPE,
337
+ styles: __STYLES,
338
+ filename: __FILE,
339
+ setup(props, ctx) {
340
+ ${indent(setupBody, 4)}
341
+
342
+ const __cleanups = [];
343
+ const __childInstances = [];
344
+ let __root = null;
345
+ const __nodes = Object.create(null);
346
+
347
+ function __read(exprCode) {
348
+ /* exprCode is precompiled — kept for clarity in errors */
349
+ return exprCode;
350
+ }
351
+
352
+ function __qs(sel) {
353
+ return __root.querySelector(sel);
354
+ }
355
+
356
+ function __subscribe(keys, fn) {
357
+ const run = () => {
358
+ try { fn(); }
359
+ catch (err) {
360
+ reportError(err instanceof EasyError ? err : new EasyError(err.message || String(err), {
361
+ component: __FILE,
362
+ filename: __FILE,
363
+ cause: err,
364
+ hint: 'Check the template expression and component state.'
365
+ }));
366
+ }
367
+ };
368
+ for (const k of keys) {
369
+ if (state && state.$subscribe) __cleanups.push(state.$subscribe(k, run));
370
+ }
371
+ run();
372
+ }
373
+
374
+ function __ctxMethods() {
375
+ return { ...methods, ...(__modelHandlers || {}) };
376
+ }
377
+
378
+ ${indent(emitUpdaters(ctx), 4)}
379
+
380
+ ${indent(emitModelMethods(ctx), 4)}
381
+
382
+ function mount(el) {
383
+ __root = el;
384
+ replaceStyles(__SCOPE, __STYLES);
385
+ el.innerHTML = __HTML;
386
+
387
+ ${indent(emitNodeCache(ctx), 6)}
388
+
389
+ if (ctx.appContext && ctx.appContext.registerContext) {
390
+ const router = ctx.appContext.router;
391
+ // Live proxy — do NOT spread state (stale snapshot breaks this.x mutations
392
+ // and can desync handlers from reactive subscribers / DOM updaters).
393
+ const __eventCtx = createEventContext(__ctxMethods(), state, {
394
+ $router: router,
395
+ $emit: ctx.emit
396
+ });
397
+ ctx.appContext.registerContext(el, __eventCtx);
398
+ // display:contents hosts are skipped in some browsers' event paths;
399
+ // also register on the first real child so clicks still resolve.
400
+ if (el.firstElementChild) {
401
+ ctx.appContext.registerContext(el.firstElementChild, __eventCtx);
402
+ }
403
+ }
404
+
405
+ if (typeof registerHmrInstance === 'function' && __HMR_ID) {
406
+ __cleanups.push(registerHmrInstance(__HMR_ID, {
407
+ get state() { return state; },
408
+ get props() { return props; },
409
+ get $el() { return __root; },
410
+ get appContext() { return ctx.appContext; },
411
+ unmount
412
+ }, { filename: __FILE, scopeId: __SCOPE }));
413
+ }
414
+
415
+ ${indent(emitSubscriptions(ctx), 6)}
416
+ ${indent(emitRouterViews(ctx), 6)}
417
+ ${indent(emitBoundaries(ctx), 6)}
418
+ ${indent(emitChildComponents(ctx), 6)}
419
+ }
420
+
421
+ function unmount() {
422
+ for (const c of __childInstances) {
423
+ if (c && c.unmount) c.unmount();
424
+ }
425
+ __childInstances.length = 0;
426
+ for (const fn of __cleanups) fn();
427
+ __cleanups.length = 0;
428
+ if (__root) __root.innerHTML = '';
429
+ __root = null;
430
+ }
431
+
432
+ // $router is resolved via __self / createBoundThis — do not put it on methods
433
+ // (runtime binds every methods entry with .bind, so non-functions break mount).
434
+
435
+ const api = { state, methods, mount, unmount, props };
436
+ ctx.expose(api);
437
+ return api;
438
+ }
439
+ });
440
+
441
+ // HMR accept hint for Easy.js client
442
+ export const __easy_hmr = { scopeId: __SCOPE, file: __FILE };
443
+ `;
444
+ }
445
+
446
+ function emitExpr(expression, locals = []) {
447
+ const { code, keys } = compileExpression(expression, { locals });
448
+ return { code, keys };
449
+ }
450
+
451
+ function emitUpdaters(ctx) {
452
+ const lines = [];
453
+
454
+ for (const b of ctx.bindings) {
455
+ const { code } = emitExpr(b.expression);
456
+ if (b.kind === 'text' || b.kind === 'textContent') {
457
+ lines.push(`function __upd_${b.id}() {
458
+ const n = __nodes[${JSON.stringify(String(b.id))}];
459
+ if (!n) return;
460
+ const __s = state, __m = methods, __p = props;
461
+ const v = (${code});
462
+ // textContent never parses HTML — XSS-safe by default (no innerHTML)
463
+ n.textContent = v == null ? '' : String(v);
464
+ }`);
465
+ } else if (b.kind === 'rawHtml') {
466
+ lines.push(`function __upd_${b.id}() {
467
+ const n = __nodes[${JSON.stringify(String(b.id))}];
468
+ if (!n) return;
469
+ const __s = state, __m = methods, __p = props;
470
+ const v = (${code});
471
+ n.innerHTML = v == null ? '' : String(v);
472
+ }`);
473
+ } else if (b.kind === 'show') {
474
+ lines.push(`function __upd_${b.id}() {
475
+ const n = __nodes[${JSON.stringify(String(b.id))}];
476
+ if (!n) return;
477
+ const __s = state, __m = methods, __p = props;
478
+ n.style.display = (${code}) ? '' : 'none';
479
+ }`);
480
+ } else if (b.kind === 'attr') {
481
+ lines.push(`function __upd_${b.id}() {
482
+ const n = __root.querySelector('[data-e-bind-${b.attr}="${b.id}"]') || __nodes[${JSON.stringify(String(b.ownerHint))}];
483
+ if (!n) return;
484
+ const __s = state, __m = methods, __p = props;
485
+ let v = (${code});
486
+ ${attrSetter(b.attr)}
487
+ }`);
488
+ }
489
+ }
490
+
491
+ // Fix rawHtml double evaluation
492
+ // (rewritten below in loop — let me fix rawHtml properly)
493
+ // I'll replace rawHtml blocks:
494
+
495
+ for (const block of ctx.ifs) {
496
+ const { code } = emitExpr(block.condition);
497
+ lines.push(`function __upd_if_${block.id}() {
498
+ const anchor = __nodes[${JSON.stringify('if_' + block.id)}];
499
+ if (!anchor) return;
500
+ const __s = state, __m = methods, __p = props;
501
+ const show = !!(${code});
502
+ let node = anchor.__mounted;
503
+ if (show && !node) {
504
+ const tpl = document.createElement('template');
505
+ tpl.innerHTML = ${JSON.stringify(block.html)};
506
+ const wrap = document.createElement('div');
507
+ wrap.style.display = 'contents';
508
+ wrap.appendChild(tpl.content);
509
+ anchor.parentNode.insertBefore(wrap, anchor);
510
+ anchor.__mounted = wrap;
511
+ } else if (!show && node) {
512
+ if (node.parentNode) node.parentNode.removeChild(node);
513
+ anchor.__mounted = null;
514
+ }
515
+ }`);
516
+ }
517
+
518
+ for (const f of ctx.fors) {
519
+ const { code: srcCode } = emitExpr(f.source);
520
+ const locals = [f.item, f.index || 'index'].filter(Boolean);
521
+ lines.push(`function __upd_for_${f.id}() {
522
+ const host = __nodes[${JSON.stringify('for_' + f.id)}];
523
+ if (!host) return;
524
+ const __s = state, __m = methods, __p = props;
525
+ const list = (${srcCode}) || [];
526
+ host.innerHTML = '';
527
+ const frag = document.createDocumentFragment();
528
+ const tplSrc = ${JSON.stringify(f.itemTemplate)};
529
+ for (let __i = 0; __i < list.length; __i++) {
530
+ const ${f.item} = list[__i];
531
+ const ${f.index || 'index'} = __i;
532
+ const html = tplSrc
533
+ .replace(/\\{@html\\s+([\\s\\S]+?)\\}/g, (_, expr) => {
534
+ const __locals = { ${f.item}: ${f.item}, ${f.index || 'index'}: ${f.index || 'index'} };
535
+ const v = __evalLocal(expr.trim(), __locals);
536
+ return v == null ? '' : String(v);
537
+ })
538
+ .replace(/\\{\\{([\\s\\S]+?)\\}\\}/g, (_, expr) => {
539
+ const __locals = { ${f.item}: ${f.item}, ${f.index || 'index'}: ${f.index || 'index'} };
540
+ const v = __evalLocal(expr.trim(), __locals);
541
+ return escapeHtml(v);
542
+ })
543
+ .replace(/\\{([^{}@][^}]*)\\}/g, (_, expr) => {
544
+ const __locals = { ${f.item}: ${f.item}, ${f.index || 'index'}: ${f.index || 'index'} };
545
+ const v = __evalLocal(expr.trim(), __locals);
546
+ return escapeHtml(v);
547
+ });
548
+ const tpl = document.createElement('template');
549
+ tpl.innerHTML = html;
550
+ frag.appendChild(tpl.content);
551
+ }
552
+ host.appendChild(frag);
553
+ }
554
+
555
+ function __evalLocal(expr, locals) {
556
+ // CSP-safe local evaluation via precompiled map for for-loops is limited;
557
+ // resolve simple member paths against locals + state.
558
+ return __pathRead(expr, locals);
559
+ }
560
+
561
+ function __pathRead(expr, locals) {
562
+ const trimmed = String(expr).trim();
563
+ // simple identifier or a.b.c — note: \\\\w in this template → \\w in output source
564
+ if (/^[\\w$]+(?:\\.[\\w$]+)*$/.test(trimmed)) {
565
+ const parts = trimmed.split('.');
566
+ let cur = Object.prototype.hasOwnProperty.call(locals, parts[0])
567
+ ? locals[parts[0]]
568
+ : (Object.prototype.hasOwnProperty.call(state, parts[0]) ? state[parts[0]] : undefined);
569
+ for (let i = 1; i < parts.length; i++) {
570
+ if (cur == null) return undefined;
571
+ cur = cur[parts[i]];
572
+ }
573
+ return cur;
574
+ }
575
+ reportError(new EasyError('Bad list item expression: ' + trimmed, {
576
+ filename: __FILE,
577
+ hint: 'Keep e-for item interpolations to simple paths like {{ todo }} or {{ item.name }}'
578
+ }));
579
+ return '';
580
+ }`);
581
+ }
582
+
583
+ return lines.join('\n\n');
584
+ }
585
+
586
+ const URL_ATTRS = new Set([
587
+ 'href',
588
+ 'src',
589
+ 'xlink:href',
590
+ 'formaction',
591
+ 'action',
592
+ 'poster',
593
+ 'cite',
594
+ 'background',
595
+ 'data'
596
+ ]);
597
+
598
+ function isDangerousAttrName(name) {
599
+ const n = String(name || '').toLowerCase();
600
+ return /^on[a-z]/.test(n) || n === 'srcdoc';
601
+ }
602
+
603
+ function emitStaticAttr(a, ctx) {
604
+ const name = String(a.name || '');
605
+ const lower = name.toLowerCase();
606
+ if (isDangerousAttrName(name)) {
607
+ throw new CompileError({
608
+ message: `Inline event / dangerous attribute "${name}" is not allowed`,
609
+ filename: ctx.filename || 'component.easy',
610
+ ...locate(ctx.source || '', name),
611
+ source: ctx.source || '',
612
+ hint: 'Use onclick={handler} — Easy.js compiles to delegated data-e-on (CSP-safe).'
613
+ });
614
+ }
615
+ let value = a.value;
616
+ if (URL_ATTRS.has(lower)) {
617
+ value = sanitizeUrlStatic(value);
618
+ if (!value) return '';
619
+ }
620
+ return ` ${name}="${escapeAttr(value)}"`;
621
+ }
622
+
623
+ /** Compile-time URL sanitize (mirrors runtime sanitizeUrl). */
624
+ function sanitizeUrlStatic(value) {
625
+ if (value == null) return '';
626
+ let s = String(value).trim();
627
+ s = s.replace(/[\u0000-\u001F\u007F\s]+/g, '');
628
+ s = s.replace(/&#0*58;|&#x0*3a;|&colon;/gi, ':');
629
+ if (/^(javascript|vbscript|data):/i.test(s)) return '';
630
+ return String(value).trim();
631
+ }
632
+
633
+ function attrSetter(attr) {
634
+ const lower = String(attr || '').toLowerCase();
635
+ if (isDangerousAttrName(attr)) {
636
+ return `/* blocked dangerous attr ${JSON.stringify(attr)} */`;
637
+ }
638
+ if (attr === 'class' || attr === 'className') {
639
+ return `n.className = v == null ? '' : String(v);`;
640
+ }
641
+ if (attr === 'style') {
642
+ return `if (typeof v === 'object' && v) { Object.assign(n.style, v); } else { n.setAttribute('style', v == null ? '' : String(v)); }`;
643
+ }
644
+ if (attr === 'value') {
645
+ return `n.value = v == null ? '' : v;`;
646
+ }
647
+ if (URL_ATTRS.has(lower)) {
648
+ return `v = sanitizeUrl(v); if (!v) n.removeAttribute(${JSON.stringify(attr)}); else n.setAttribute(${JSON.stringify(attr)}, v);`;
649
+ }
650
+ if (attr === 'checked' || attr === 'disabled' || attr === 'selected' || attr === 'hidden') {
651
+ return `n[${JSON.stringify(attr)}] = !!v;`;
652
+ }
653
+ return `if (v == null || v === false) n.removeAttribute(${JSON.stringify(attr)}); else n.setAttribute(${JSON.stringify(attr)}, v === true ? '' : String(v));`;
654
+ }
655
+
656
+ function emitModelMethods(ctx) {
657
+ const lines = ['const __modelHandlers = {};'];
658
+ for (const m of ctx.models) {
659
+ // state.foo = ... via compiled assignment target (identifier or path)
660
+ const target = m.expression.trim();
661
+ if (!/^[\w$]+(?:\.[\w$]+)*$/.test(target)) {
662
+ throw new CompileError({
663
+ message: `e-model must be a simple path, got "${target}"`,
664
+ filename: ctx.filename,
665
+ ...locate(ctx.source || ctx.script, target),
666
+ source: ctx.source || ctx.script,
667
+ hint: 'Use e-model="draft" or e-model="user.name"'
668
+ });
669
+ }
670
+ const parts = target.split('.');
671
+ const assign =
672
+ parts.length === 1
673
+ ? `state[${JSON.stringify(parts[0])}] = val;`
674
+ : (() => {
675
+ let s = 'state';
676
+ for (let i = 0; i < parts.length - 1; i++) s += `[${JSON.stringify(parts[i])}]`;
677
+ return `${s}[${JSON.stringify(parts[parts.length - 1])}] = val;`;
678
+ })();
679
+ lines.push(`methods.__model_${m.id} = function(e) {
680
+ const val = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
681
+ ${assign}
682
+ };`);
683
+ lines.push(`__modelHandlers.__model_${m.id} = methods.__model_${m.id};`);
684
+ }
685
+ return lines.join('\n');
686
+ }
687
+
688
+ function emitNodeCache(ctx) {
689
+ const lines = [];
690
+ for (const b of ctx.bindings) {
691
+ if (b.kind === 'text' || b.kind === 'rawHtml') {
692
+ lines.push(`__nodes[${JSON.stringify(String(b.id))}] = __qs('[data-e="${b.id}"]');`);
693
+ } else if (b.kind === 'attr') {
694
+ lines.push(`__nodes[${JSON.stringify(String(b.id))}] = __qs('[data-e-bind-${b.attr}="${b.id}"]');`);
695
+ } else if (b.kind === 'show' || b.kind === 'textContent') {
696
+ lines.push(`__nodes[${JSON.stringify(String(b.id))}] = __qs('[data-e-node="${b.id}"]');`);
697
+ }
698
+ }
699
+ for (const block of ctx.ifs) {
700
+ lines.push(`__nodes[${JSON.stringify('if_' + block.id)}] = __qs('[data-e-if="${block.id}"]');`);
701
+ }
702
+ for (const f of ctx.fors) {
703
+ lines.push(`__nodes[${JSON.stringify('for_' + f.id)}] = __qs('[data-e-for="${f.id}"]');`);
704
+ }
705
+ for (const c of ctx.components) {
706
+ lines.push(`__nodes[${JSON.stringify('comp_' + c.id)}] = __qs('[data-e-comp="${c.id}"]');`);
707
+ }
708
+ for (const r of ctx.routerViews) {
709
+ lines.push(`__nodes[${JSON.stringify('router_' + r.id)}] = __qs('[data-e-router="${r.id}"]');`);
710
+ }
711
+ for (const b of ctx.boundaries) {
712
+ lines.push(`__nodes[${JSON.stringify('boundary_' + b.id)}] = __qs('[data-e-boundary="${b.id}"]');`);
713
+ }
714
+ for (const m of ctx.models) {
715
+ lines.push(`__nodes[${JSON.stringify('model_' + m.id)}] = __qs('[data-e-model="${m.id}"]');`);
716
+ }
717
+ return lines.join('\n');
718
+ }
719
+
720
+ function emitSubscriptions(ctx) {
721
+ const lines = [];
722
+ for (const b of ctx.bindings) {
723
+ const keys = b.keys.length ? b.keys : ['*'];
724
+ lines.push(`__subscribe(${JSON.stringify(keys)}, __upd_${b.id});`);
725
+ }
726
+ for (const block of ctx.ifs) {
727
+ const keys = block.keys.length ? block.keys : ['*'];
728
+ lines.push(`__subscribe(${JSON.stringify(keys)}, __upd_if_${block.id});`);
729
+ }
730
+ for (const f of ctx.fors) {
731
+ const keys = f.keys.length ? f.keys : ['*'];
732
+ lines.push(`__subscribe(${JSON.stringify(keys)}, () => __upd_for_${f.id}());`);
733
+ }
734
+ for (const m of ctx.models) {
735
+ const keys = m.keys.length ? m.keys : ['*'];
736
+ const { code } = emitExpr(m.expression);
737
+ lines.push(`__subscribe(${JSON.stringify(keys)}, () => {
738
+ const n = __nodes[${JSON.stringify('model_' + m.id)}];
739
+ if (!n) return;
740
+ const __s = state, __m = methods, __p = props;
741
+ const v = (${code});
742
+ if (n.type === 'checkbox') n.checked = !!v;
743
+ else if (n.value !== String(v ?? '')) n.value = v ?? '';
744
+ });`);
745
+ }
746
+ return lines.join('\n');
747
+ }
748
+
749
+ function emitRouterViews(ctx) {
750
+ if (!ctx.routerViews.length) return '';
751
+ return ctx.routerViews
752
+ .map(
753
+ (r) => `{
754
+ const host = __nodes[${JSON.stringify('router_' + r.id)}];
755
+ const router = ctx.appContext && ctx.appContext.router;
756
+ if (host && router) {
757
+ router._registerView(host);
758
+ __cleanups.push(() => router._unregisterView(host));
759
+ }
760
+ }`
761
+ )
762
+ .join('\n');
763
+ }
764
+
765
+ function emitBoundaries(ctx) {
766
+ if (!ctx.boundaries.length) return '';
767
+ return ctx.boundaries
768
+ .map(
769
+ (b) => `{
770
+ const host = __nodes[${JSON.stringify('boundary_' + b.id)}];
771
+ if (host) {
772
+ const boundary = createBoundary(host, {
773
+ fallback: ${JSON.stringify(b.fallback)},
774
+ name: 'ErrorBoundary',
775
+ hint: 'A child component threw. Fix the error and save — HMR will retry.'
776
+ });
777
+ const stop = pushBoundary(boundary);
778
+ __cleanups.push(() => { stop(); boundary.dispose(); });
779
+ host.__easyBoundary = boundary;
780
+ }
781
+ }`
782
+ )
783
+ .join('\n');
784
+ }
785
+
786
+ function emitChildComponents(ctx) {
787
+ if (!ctx.components.length) return '';
788
+ return ctx.components
789
+ .map((c) => {
790
+ const propsEntries = Object.entries(c.props).map(([k, v]) => {
791
+ // Static JSON.stringify values from attrs start with quote from JSON.stringify already
792
+ if (/^["']/.test(v) || v === 'true' || v === 'false' || /^[\d.]+$/.test(v)) {
793
+ // If we JSON.stringified in genElement, v is already '"foo"'
794
+ if (v.startsWith('"') || v.startsWith("'")) {
795
+ return `${JSON.stringify(k)}: ${v}`;
796
+ }
797
+ }
798
+ // Dynamic expression
799
+ const { code } = emitExpr(v);
800
+ return `${JSON.stringify(k)}: (() => { const __s = state, __m = methods, __p = props; return (${code}); })()`;
801
+ });
802
+ return `{
803
+ const host = __nodes[${JSON.stringify('comp_' + c.id)}];
804
+ const Comp = typeof ${c.name} !== 'undefined' ? ${c.name} : null;
805
+ if (!Comp) {
806
+ reportError(new EasyError('Unknown component <${c.name}>', {
807
+ filename: __FILE,
808
+ hint: 'Import the component in <script> and use PascalCase in the template.'
809
+ }));
810
+ } else if (host) {
811
+ const apply = () => {
812
+ try {
813
+ if (host.__inst) unmountComponent(host.__inst);
814
+ host.__inst = mountComponent(Comp, host, { ${propsEntries.join(', ')} }, {
815
+ ...ctx.appContext,
816
+ parent: { filename: __FILE }
817
+ });
818
+ } catch (err) {
819
+ const easyErr = err instanceof EasyError ? err : new EasyError(err.message || String(err), {
820
+ component: '${c.name}',
821
+ filename: __FILE,
822
+ cause: err,
823
+ hint: 'Check props passed to <${c.name}>.'
824
+ });
825
+ reportError(easyErr);
826
+ let p = host.parentElement;
827
+ while (p) {
828
+ if (p.__easyBoundary) { p.__easyBoundary.capture(easyErr, { filename: __FILE }); break; }
829
+ p = p.parentElement;
830
+ }
831
+ }
832
+ };
833
+ apply();
834
+ __childInstances.push({ unmount: () => host.__inst && unmountComponent(host.__inst) });
835
+ ${
836
+ c.keys.length
837
+ ? `__subscribe(${JSON.stringify(c.keys)}, () => { apply(); });`
838
+ : `/* static child <${c.name}> — mount once, do not remount on parent state */`
839
+ }
840
+ }
841
+ }`;
842
+ })
843
+ .join('\n');
844
+ }
845
+
846
+ /* ── Script analysis ─────────────────────────────────────────── */
847
+
848
+ function analyzeScript(script, options = {}) {
849
+ let raw = script || '';
850
+ if (options.lang === 'ts' || options.lang === 'typescript') {
851
+ // strip applied in compile(); keep defensive
852
+ raw = raw;
853
+ }
854
+
855
+ const imports = [];
856
+ let body = raw;
857
+ const importRe = /^import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"]\s*;?[ \t]*$/gm;
858
+ let m;
859
+ const importSpans = [];
860
+ while ((m = importRe.exec(raw)) !== null) {
861
+ importSpans.push(m[0]);
862
+ imports.push(parseImportClause(m[1], m[2]));
863
+ }
864
+ for (const span of importSpans) body = body.replace(span, '');
865
+ body = body.trim();
866
+
867
+ let setupBody = null;
868
+ let prelude = '';
869
+
870
+ const defaultExportMatch = body.match(/export\s+default\s+(\{[\s\S]*})\s*;?\s*$/);
871
+ if (defaultExportMatch) {
872
+ const obj = defaultExportMatch[1];
873
+ prelude = body.slice(0, defaultExportMatch.index).trim();
874
+ setupBody = optionsApiToSetup(obj);
875
+ } else if (/export\s+default\b/.test(body)) {
876
+ prelude = body.replace(/export\s+default\s+/, 'const __options = ');
877
+ setupBody = optionsApiToSetup('__options');
878
+ } else if (looksLikeSvelteScript(body)) {
879
+ const svelte = svelteScriptToSetup(body);
880
+ prelude = svelte.prelude;
881
+ setupBody = svelte.setupBody;
882
+ } else {
883
+ prelude = body;
884
+ setupBody = defaultSetupBody();
885
+ }
886
+
887
+ return { imports, setupBody, prelude };
888
+ }
889
+
890
+ function looksLikeSvelteScript(body) {
891
+ if (!body.trim()) return true;
892
+ return (
893
+ /^\s*(let|const|var|function)\b/m.test(body) ||
894
+ /^\s*(?:async\s+)?function\b/m.test(body)
895
+ );
896
+ }
897
+
898
+ /**
899
+ * Svelte-like script:
900
+ * let count = 0;
901
+ * function inc() { count++; }
902
+ * → reactive state + methods with rewritten refs
903
+ */
904
+ function svelteScriptToSetup(body) {
905
+ const stateFields = [];
906
+ const fns = [];
907
+ let work = body;
908
+
909
+ // 1) function declarations
910
+ {
911
+ const fnRe = /(?:export\s+)?(async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
912
+ const pieces = [];
913
+ let last = 0;
914
+ let fm;
915
+ const temp = work;
916
+ while ((fm = fnRe.exec(temp)) !== null) {
917
+ pieces.push(temp.slice(last, fm.index));
918
+ const braceStart = fm.index + fm[0].length - 1;
919
+ const braceEnd = findMatchingBrace(temp, braceStart);
920
+ fns.push({
921
+ name: fm[2],
922
+ params: fm[3].trim(),
923
+ body: temp.slice(braceStart + 1, braceEnd),
924
+ async: !!fm[1]
925
+ });
926
+ last = braceEnd + 1;
927
+ }
928
+ pieces.push(temp.slice(last));
929
+ work = pieces.join('\n');
930
+ }
931
+
932
+ // 2) arrow functions with block body — before let/const extraction
933
+ {
934
+ const arrowRe =
935
+ /(?:export\s+)?(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(async\s*)?\(([^)]*)\)\s*=>\s*\{/g;
936
+ let am;
937
+ const src = work;
938
+ const keep = [];
939
+ let cursor = 0;
940
+ while ((am = arrowRe.exec(src)) !== null) {
941
+ keep.push(src.slice(cursor, am.index));
942
+ const braceStart = am.index + am[0].length - 1;
943
+ const braceEnd = findMatchingBrace(src, braceStart);
944
+ fns.push({
945
+ name: am[1],
946
+ params: am[3].trim(),
947
+ body: src.slice(braceStart + 1, braceEnd),
948
+ async: !!am[2]
949
+ });
950
+ cursor = braceEnd + 1;
951
+ if (src[cursor] === ';') cursor++;
952
+ }
953
+ keep.push(src.slice(cursor));
954
+ work = keep.join('\n');
955
+ }
956
+
957
+ // 3) expression-bodied arrows
958
+ work = work.replace(
959
+ /(?:export\s+)?(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(async\s*)?\(([^)]*)\)\s*=>\s*([^;{][^;]*);/g,
960
+ (_, name, asyncKw, params, expr) => {
961
+ fns.push({
962
+ name,
963
+ params: params.trim(),
964
+ body: `return (${expr.trim()});`,
965
+ async: !!asyncKw
966
+ });
967
+ return '';
968
+ }
969
+ );
970
+
971
+ // 3b) onMount(() => { ... }) / onMount(function(){...})
972
+ let onMountBody = null;
973
+ {
974
+ const om = work.match(/onMount\s*\(\s*(?:function\s*\([^)]*\)\s*|([^)]*)\s*=>\s*)\{/);
975
+ if (om) {
976
+ const start = work.indexOf(om[0]) + om[0].length - 1;
977
+ const end = findMatchingBrace(work, start);
978
+ onMountBody = work.slice(start + 1, end);
979
+ let after = end + 1;
980
+ // consume closing );
981
+ const tail = work.slice(after).match(/^\s*\)\s*;?/);
982
+ if (tail) after += tail[0].length;
983
+ work = work.slice(0, work.indexOf(om[0])) + work.slice(after);
984
+ }
985
+ }
986
+
987
+ // 4) let / const state
988
+ work = work.replace(
989
+ /(?:export\s+)?(let|const|var)\s+([A-Za-z_$][\w$]*)(\s*=\s*([^;]+))?;/g,
990
+ (_, kind, name, _eq, init) => {
991
+ stateFields.push({ name, init: (init || 'undefined').trim(), kind });
992
+ return '';
993
+ }
994
+ );
995
+
996
+ const stateKeys = stateFields.map((f) => f.name);
997
+ const stateObj = stateFields
998
+ .map((f) => ` ${JSON.stringify(f.name)}: ${f.init}`)
999
+ .join(',\n');
1000
+
1001
+ const methodNames = fns.map((fn) => fn.name);
1002
+
1003
+ const methodEntries = fns.map((fn) => {
1004
+ const locals = fn.params
1005
+ .split(',')
1006
+ .map((p) => p.trim().split(/[\s=]/)[0])
1007
+ .filter(Boolean);
1008
+ const rewritten = rewriteStateRefs(fn.body, stateKeys, locals, methodNames);
1009
+ const asyncKw = fn.async ? 'async ' : '';
1010
+ return ` ${JSON.stringify(fn.name)}: ${asyncKw}function(${fn.params}) {\n${indent(rewritten, 4)}\n }`;
1011
+ });
1012
+
1013
+ const prelude = work.replace(/\bonMount\b/g, '/* onMount handled */').trim();
1014
+
1015
+ const onMountSnippet = onMountBody
1016
+ ? `
1017
+ queueMicrotask(() => {
1018
+ try {
1019
+ (function() {\n${indent(rewriteStateRefs(onMountBody, stateKeys, [], methodNames), 4)}\n }).call(__self);
1020
+ } catch (e) {
1021
+ reportError(e, { filename: __FILE });
1022
+ }
1023
+ });
1024
+ `
1025
+ : '';
1026
+
1027
+ const setupBody = `
1028
+ const state = reactive({
1029
+ ${stateObj || ' /* no let bindings */'}
1030
+ });
1031
+ const methods = {
1032
+ ${methodEntries.join(',\n') || ' /* no functions */'}
1033
+ };
1034
+ const __self = new Proxy({}, {
1035
+ get(_, key) {
1036
+ if (key === '$router') return ctx.appContext && ctx.appContext.router;
1037
+ if (key === '$emit') return ctx.emit;
1038
+ if (key === '$props') return props;
1039
+ if (key in methods) return methods[key];
1040
+ if (state && key in state) return state[key];
1041
+ return undefined;
1042
+ },
1043
+ set(_, key, value) {
1044
+ if (state) { state[key] = value; return true; }
1045
+ return false;
1046
+ }
1047
+ });
1048
+ for (const __name of Object.keys(methods)) {
1049
+ const __fn = methods[__name];
1050
+ methods[__name] = function (...__args) {
1051
+ try {
1052
+ return __fn.apply(__self, __args);
1053
+ } catch (err) {
1054
+ reportError(new EasyError(err.message || String(err), {
1055
+ filename: __FILE,
1056
+ component: __FILE,
1057
+ cause: err,
1058
+ hint: 'Error in "' + __name + '".'
1059
+ }));
1060
+ throw err;
1061
+ }
1062
+ };
1063
+ }
1064
+ ${onMountSnippet}
1065
+ `;
1066
+
1067
+ return { prelude, setupBody };
1068
+ }
1069
+
1070
+ function findMatchingBrace(src, openIdx) {
1071
+ let depth = 0;
1072
+ for (let i = openIdx; i < src.length; i++) {
1073
+ const ch = src[i];
1074
+ if (ch === '"' || ch === "'" || ch === '`') {
1075
+ const q = ch;
1076
+ i++;
1077
+ while (i < src.length) {
1078
+ if (src[i] === '\\') {
1079
+ i += 2;
1080
+ continue;
1081
+ }
1082
+ if (src[i] === q) break;
1083
+ i++;
1084
+ }
1085
+ continue;
1086
+ }
1087
+ if (ch === '{') depth++;
1088
+ else if (ch === '}') {
1089
+ depth--;
1090
+ if (depth === 0) return i;
1091
+ }
1092
+ }
1093
+ return src.length - 1;
1094
+ }
1095
+
1096
+ function rewriteStateRefs(code, stateKeys, locals = [], methodNames = []) {
1097
+ const localSet = new Set(locals);
1098
+ const keySet = new Set(stateKeys);
1099
+ const methodSet = new Set(methodNames);
1100
+ if (!keySet.size && !methodSet.size) return code;
1101
+ let i = 0;
1102
+ let out = '';
1103
+ const src = code;
1104
+
1105
+ while (i < src.length) {
1106
+ const ch = src[i];
1107
+
1108
+ if (ch === '"' || ch === "'" || ch === '`') {
1109
+ const q = ch;
1110
+ out += ch;
1111
+ i++;
1112
+ while (i < src.length) {
1113
+ out += src[i];
1114
+ if (src[i] === '\\') {
1115
+ i++;
1116
+ if (i < src.length) out += src[i];
1117
+ i++;
1118
+ continue;
1119
+ }
1120
+ if (src[i] === q) {
1121
+ i++;
1122
+ break;
1123
+ }
1124
+ i++;
1125
+ }
1126
+ continue;
1127
+ }
1128
+
1129
+ if (ch === '/' && src[i + 1] === '/') {
1130
+ while (i < src.length && src[i] !== '\n') {
1131
+ out += src[i];
1132
+ i++;
1133
+ }
1134
+ continue;
1135
+ }
1136
+
1137
+ if (ch === '/' && src[i + 1] === '*') {
1138
+ out += ch;
1139
+ i++;
1140
+ while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) {
1141
+ out += src[i];
1142
+ i++;
1143
+ }
1144
+ continue;
1145
+ }
1146
+
1147
+ if (/[A-Za-z_$]/.test(ch)) {
1148
+ const start = i;
1149
+ i++;
1150
+ while (i < src.length && /[\w$]/.test(src[i])) i++;
1151
+ const id = src.slice(start, i);
1152
+ const prev = previousNonSpace(out);
1153
+ if (prev === '.') {
1154
+ out += id;
1155
+ continue;
1156
+ }
1157
+ if (localSet.has(id)) {
1158
+ out += id;
1159
+ continue;
1160
+ }
1161
+ if (keySet.has(id)) {
1162
+ out += `state[${JSON.stringify(id)}]`;
1163
+ continue;
1164
+ }
1165
+ // Bare method refs (e.g. syncParams.call(this) inside onMount)
1166
+ if (methodSet.has(id)) {
1167
+ out += `methods[${JSON.stringify(id)}]`;
1168
+ continue;
1169
+ }
1170
+ out += id;
1171
+ continue;
1172
+ }
1173
+
1174
+ out += ch;
1175
+ i++;
1176
+ }
1177
+
1178
+ return out;
1179
+ }
1180
+
1181
+ function previousNonSpace(s) {
1182
+ for (let i = s.length - 1; i >= 0; i--) {
1183
+ if (!/\s/.test(s[i])) return s[i];
1184
+ }
1185
+ return '';
1186
+ }
1187
+
1188
+ function formatImport(imp) {
1189
+ if (imp.namespaced) return `import * as ${imp.local} from ${JSON.stringify(imp.source)};`;
1190
+ if (imp.defaultName && imp.specifiers.length) {
1191
+ return `import ${imp.defaultName}, { ${imp.specifiers.map((s) => (s.local === s.imported ? s.imported : `${s.imported} as ${s.local}`)).join(', ')} } from ${JSON.stringify(imp.source)};`;
1192
+ }
1193
+ if (imp.defaultName) return `import ${imp.defaultName} from ${JSON.stringify(imp.source)};`;
1194
+ if (imp.specifiers.length) {
1195
+ return `import { ${imp.specifiers.map((s) => (s.local === s.imported ? s.imported : `${s.imported} as ${s.local}`)).join(', ')} } from ${JSON.stringify(imp.source)};`;
1196
+ }
1197
+ return `import ${JSON.stringify(imp.source)};`;
1198
+ }
1199
+
1200
+ function parseImportClause(clause, source) {
1201
+ const result = { source, defaultName: null, namespaced: null, local: null, specifiers: [] };
1202
+ const trimmed = clause.trim();
1203
+ if (trimmed.startsWith('*')) {
1204
+ const m = trimmed.match(/\*\s+as\s+(\w+)/);
1205
+ result.namespaced = true;
1206
+ result.local = m ? m[1] : 'mod';
1207
+ return result;
1208
+ }
1209
+ for (const part of splitImportParts(trimmed)) {
1210
+ if (part.startsWith('{')) {
1211
+ const inner = part.slice(1, -1);
1212
+ for (const s of inner.split(',')) {
1213
+ const t = s.trim();
1214
+ if (!t) continue;
1215
+ const [imported, local] = t.split(/\s+as\s+/).map((x) => x.trim());
1216
+ result.specifiers.push({ imported, local: local || imported });
1217
+ }
1218
+ } else {
1219
+ result.defaultName = part.trim();
1220
+ }
1221
+ }
1222
+ return result;
1223
+ }
1224
+
1225
+ function splitImportParts(clause) {
1226
+ const parts = [];
1227
+ let depth = 0;
1228
+ let cur = '';
1229
+ for (const ch of clause) {
1230
+ if (ch === '{') depth++;
1231
+ if (ch === '}') depth--;
1232
+ if (ch === ',' && depth === 0) {
1233
+ parts.push(cur.trim());
1234
+ cur = '';
1235
+ } else cur += ch;
1236
+ }
1237
+ if (cur.trim()) parts.push(cur.trim());
1238
+ return parts;
1239
+ }
1240
+
1241
+ function optionsApiToSetup(objExpr) {
1242
+ const isLiteral = objExpr.trim().startsWith('{');
1243
+ return `
1244
+ const __options = ${isLiteral ? objExpr : objExpr};
1245
+ const __data = typeof __options.data === 'function' ? __options.data() : (__options.data || {});
1246
+ const state = reactive(__data);
1247
+ const methods = {};
1248
+ const __self = new Proxy({}, {
1249
+ get(_, key) {
1250
+ if (key === '$router') return ctx.appContext && ctx.appContext.router;
1251
+ if (key === '$emit') return ctx.emit;
1252
+ if (key === '$props') return props;
1253
+ if (key in methods) return methods[key];
1254
+ if (state && key in state) return state[key];
1255
+ return undefined;
1256
+ },
1257
+ set(_, key, value) {
1258
+ if (state) { state[key] = value; return true; }
1259
+ return false;
1260
+ }
1261
+ });
1262
+ for (const [__name, __fn] of Object.entries(__options.methods || {})) {
1263
+ methods[__name] = function (...__args) {
1264
+ try {
1265
+ return __fn.apply(__self, __args);
1266
+ } catch (err) {
1267
+ reportError(new EasyError(err.message || String(err), {
1268
+ filename: __FILE,
1269
+ component: __FILE,
1270
+ cause: err,
1271
+ hint: 'Error in method "' + __name + '".'
1272
+ }));
1273
+ throw err;
1274
+ }
1275
+ };
1276
+ }
1277
+ if (__options.created) __options.created.call(__self);
1278
+ const __mountedHook = __options.mounted;
1279
+ if (__mountedHook) {
1280
+ queueMicrotask(() => { try { __mountedHook.call(__self); } catch (e) { reportError(e, { filename: __FILE }); } });
1281
+ }
1282
+ `;
1283
+ }
1284
+
1285
+ function defaultSetupBody() {
1286
+ return `const state = reactive({});\nconst methods = {};\n`;
1287
+ }
1288
+
1289
+ function parseFor(value) {
1290
+ const m = value
1291
+ .trim()
1292
+ .match(/^\(?\s*([\w$]+)(?:\s*,\s*([\w$]+))?\s*\)?\s+(?:in|of)\s+(.+)$/);
1293
+ if (!m) return { item: 'item', index: 'index', source: value.trim() };
1294
+ return { item: m[1], index: m[2] || null, source: m[3].trim() };
1295
+ }
1296
+
1297
+ function stripCall(handler) {
1298
+ return String(handler || '')
1299
+ .trim()
1300
+ .replace(/\(\s*\)$/, '');
1301
+ }
1302
+
1303
+ function nextId() {
1304
+ return uid++;
1305
+ }
1306
+
1307
+ function escapeHtml(s) {
1308
+ return String(s)
1309
+ .replace(/&/g, '&amp;')
1310
+ .replace(/</g, '&lt;')
1311
+ .replace(/>/g, '&gt;');
1312
+ }
1313
+
1314
+ function escapeAttr(s) {
1315
+ return String(s)
1316
+ .replace(/&/g, '&amp;')
1317
+ .replace(/"/g, '&quot;')
1318
+ .replace(/</g, '&lt;');
1319
+ }
1320
+
1321
+ function isVoid(tag) {
1322
+ return new Set([
1323
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta',
1324
+ 'param', 'source', 'track', 'wbr'
1325
+ ]).has(String(tag).toLowerCase());
1326
+ }
1327
+
1328
+ function indent(str, n) {
1329
+ const pad = ' '.repeat(n);
1330
+ return String(str)
1331
+ .split('\n')
1332
+ .map((l) => (l ? pad + l : l))
1333
+ .join('\n');
1334
+ }