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,157 @@
1
+ /**
2
+ * Hot Module Replacement — preserve reactive state across component swaps.
3
+ */
4
+
5
+ import { injectStyles, removeStyles, replaceStyles } from './css.js';
6
+ import { raw } from './reactive.js';
7
+
8
+ const records = new Map(); // moduleId -> { instances: Set, meta }
9
+ let hotEnabled = false;
10
+
11
+ export function enableHmr() {
12
+ hotEnabled = true;
13
+ if (typeof window !== 'undefined') window.__EASY_HMR__ = api;
14
+ return api;
15
+ }
16
+
17
+ export function isHmrEnabled() {
18
+ return hotEnabled;
19
+ }
20
+
21
+ function normalizeId(id) {
22
+ if (!id) return '';
23
+ try {
24
+ const u = new URL(id, typeof location !== 'undefined' ? location.href : 'http://local/');
25
+ return u.pathname;
26
+ } catch {
27
+ return String(id).split('?')[0];
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Register a mounted instance for a module URL/id.
33
+ */
34
+ export function registerHmrInstance(moduleId, instance, meta = {}) {
35
+ if (!hotEnabled || !moduleId) return () => {};
36
+ const id = normalizeId(moduleId);
37
+ let rec = records.get(id);
38
+ if (!rec) {
39
+ rec = { instances: new Set(), meta: {} };
40
+ records.set(id, rec);
41
+ }
42
+ Object.assign(rec.meta, meta);
43
+ rec.instances.add(instance);
44
+ instance.__hmrId = id;
45
+ return () => {
46
+ rec.instances.delete(instance);
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Apply a newly imported module (default export = component def).
52
+ * Preserves state snapshots on existing instances.
53
+ */
54
+ export async function applyHmrUpdate(moduleId, newModule, info = {}) {
55
+ const defn = newModule?.default || newModule;
56
+ if (!defn) return { ok: false, reason: 'no-default' };
57
+
58
+ const id = normalizeId(moduleId);
59
+
60
+ if (defn.scopeId && defn.styles != null) {
61
+ replaceStyles(defn.scopeId, defn.styles);
62
+ }
63
+
64
+ // Style-only: skip remount when server says so
65
+ if (info.styleOnly) {
66
+ console.log(`[easy hmr] styles updated for ${id}`);
67
+ return { ok: true, remounted: 0, stylesOnly: true };
68
+ }
69
+
70
+ const rec = records.get(id);
71
+ if (!rec || rec.instances.size === 0) {
72
+ records.set(id, { instances: new Set(), meta: { defn }, pending: defn });
73
+ return { ok: true, remounted: 0 };
74
+ }
75
+
76
+ const instances = [...rec.instances];
77
+ let remounted = 0;
78
+
79
+ for (const inst of instances) {
80
+ const snapshot = snapshotState(inst.state);
81
+ const host = inst.$el;
82
+ const props = inst.props ? { ...raw(inst.props) } : {};
83
+ const appContext = inst.appContext || {};
84
+
85
+ rec.instances.delete(inst);
86
+ try {
87
+ if (typeof inst.unmount === 'function') inst.unmount();
88
+ } catch (e) {
89
+ console.warn('[easy hmr] unmount failed', e);
90
+ }
91
+
92
+ if (!host) continue;
93
+
94
+ try {
95
+ const { mountComponent } = await import('./component.js');
96
+ const next = mountComponent(defn, host, props, {
97
+ ...appContext,
98
+ hmrModuleId: id
99
+ });
100
+ if (next?.state && snapshot) restoreState(next.state, snapshot);
101
+ remounted++;
102
+ } catch (e) {
103
+ console.error('[easy hmr] remount failed', e);
104
+ }
105
+ }
106
+
107
+ rec.meta.defn = defn;
108
+ console.log(`[easy hmr] ${id} → ${remounted} instance(s), state preserved`);
109
+ return { ok: true, remounted };
110
+ }
111
+
112
+ export function replaceScopedStyles(scopeId, cssText) {
113
+ replaceStyles(scopeId, cssText);
114
+ }
115
+
116
+ export function snapshotState(state) {
117
+ if (!state) return null;
118
+ try {
119
+ const base = raw(state) || state;
120
+ return JSON.parse(JSON.stringify(base));
121
+ } catch {
122
+ const out = {};
123
+ for (const k of Object.keys(state)) {
124
+ try {
125
+ out[k] = JSON.parse(JSON.stringify(raw(state[k]) ?? state[k]));
126
+ } catch {
127
+ out[k] = state[k];
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+ }
133
+
134
+ export function restoreState(state, snapshot) {
135
+ if (!state || !snapshot) return;
136
+ for (const [k, v] of Object.entries(snapshot)) {
137
+ try {
138
+ state[k] = Array.isArray(v) ? v.slice() : v;
139
+ } catch (_) {
140
+ /* ignore */
141
+ }
142
+ }
143
+ if (typeof state.$notify === 'function') {
144
+ for (const k of Object.keys(snapshot)) state.$notify(k);
145
+ }
146
+ }
147
+
148
+ const api = {
149
+ register: registerHmrInstance,
150
+ apply: applyHmrUpdate,
151
+ replaceStyles: replaceScopedStyles,
152
+ snapshot: snapshotState,
153
+ restore: restoreState,
154
+ records
155
+ };
156
+
157
+ export default api;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Easy.js runtime — zero external dependencies.
3
+ * Surgical DOM updates via compile-time subscriptions; no Virtual DOM.
4
+ */
5
+
6
+ export { reactive, isReactive, raw } from './reactive.js';
7
+ export { createApp } from './app.js';
8
+ export { defineComponent, mountComponent, unmountComponent } from './component.js';
9
+ export { createRouter, useRouter, RouterView } from './router.js';
10
+ export { createStore } from './store.js';
11
+ export { injectStyles, removeStyles, replaceStyles } from './css.js';
12
+ export { setupDelegation, createEventContext, on, off } from './events.js';
13
+ export { nextTick, queueJob } from './scheduler.js';
14
+ export {
15
+ escapeHtml,
16
+ escapeAttr,
17
+ sanitizeUrl,
18
+ isEventAttr,
19
+ isUrlAttr,
20
+ URL_ATTRS
21
+ } from './escape.js';
22
+ export {
23
+ EasyError,
24
+ reportError,
25
+ setOverlayHandler,
26
+ pushBoundary,
27
+ peekBoundary,
28
+ withErrorBoundary,
29
+ createBoundary
30
+ } from './errors.js';
31
+ export { installOverlay, showOverlay, hideOverlay } from './overlay.js';
32
+ export {
33
+ enableHmr,
34
+ isHmrEnabled,
35
+ registerHmrInstance,
36
+ applyHmrUpdate,
37
+ replaceScopedStyles,
38
+ snapshotState,
39
+ restoreState
40
+ } from './hmr.js';
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Dev-mode error overlay — message + optional code frame.
3
+ * Enabled when import.meta / __EASY_DEV__ is set by the CLI.
4
+ */
5
+
6
+ import { setOverlayHandler } from './errors.js';
7
+ import { escapeHtml } from './escape.js';
8
+
9
+ let root = null;
10
+
11
+ export function installOverlay() {
12
+ if (typeof document === 'undefined') return;
13
+ if (window.__EASY_OVERLAY_INSTALLED__) return;
14
+ window.__EASY_OVERLAY_INSTALLED__ = true;
15
+
16
+ setOverlayHandler((err) => showOverlay(err));
17
+
18
+ window.addEventListener('error', (e) => {
19
+ showOverlay(e.error || e.message, { source: 'window.onerror' });
20
+ });
21
+ window.addEventListener('unhandledrejection', (e) => {
22
+ showOverlay(e.reason, { source: 'unhandledrejection' });
23
+ });
24
+ }
25
+
26
+ export function showOverlay(err, meta = {}) {
27
+ if (typeof document === 'undefined') return;
28
+ ensureRoot();
29
+
30
+ const message = err?.message || String(err);
31
+ const component = err?.component || meta.component || '';
32
+ const filename = err?.filename || meta.filename || '';
33
+ const hint = err?.hint || '';
34
+ const frame = err?.frame || meta.frame || '';
35
+ const stack = err?.stack || (err?.cause && err.cause.stack) || '';
36
+
37
+ root.innerHTML = `
38
+ <div data-easy-overlay-panel>
39
+ <div data-easy-overlay-top>
40
+ <strong>Easy.js Error</strong>
41
+ <button type="button" data-easy-overlay-close aria-label="Close">×</button>
42
+ </div>
43
+ <div data-easy-overlay-loc>${escapeHtml([component, filename].filter(Boolean).join(' · '))}</div>
44
+ <pre data-easy-overlay-msg>${escapeHtml(message)}</pre>
45
+ ${hint ? `<p data-easy-overlay-hint>${escapeHtml(hint)}</p>` : ''}
46
+ ${frame ? `<pre data-easy-overlay-frame>${escapeHtml(frame)}</pre>` : ''}
47
+ ${stack ? `<pre data-easy-overlay-stack>${escapeHtml(String(stack).split('\n').slice(0, 8).join('\n'))}</pre>` : ''}
48
+ </div>
49
+ `;
50
+ root.style.display = 'block';
51
+ root.querySelector('[data-easy-overlay-close]')?.addEventListener('click', hideOverlay);
52
+ }
53
+
54
+ export function hideOverlay() {
55
+ if (root) root.style.display = 'none';
56
+ }
57
+
58
+ function ensureRoot() {
59
+ if (root) return;
60
+ root = document.createElement('div');
61
+ root.setAttribute('data-easy-overlay', '');
62
+ root.innerHTML = '';
63
+ Object.assign(root.style, {
64
+ display: 'none',
65
+ position: 'fixed',
66
+ inset: '0',
67
+ zIndex: '2147483646',
68
+ background: 'rgba(8,10,14,0.72)',
69
+ backdropFilter: 'blur(4px)',
70
+ padding: '2rem',
71
+ overflow: 'auto',
72
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
73
+ });
74
+
75
+ const style = document.createElement('style');
76
+ style.textContent = `
77
+ [data-easy-overlay-panel]{
78
+ max-width:720px;margin:5vh auto;background:#141a22;color:#e8eef6;
79
+ border:1px solid rgba(240,160,96,.45);border-radius:12px;padding:1.25rem 1.35rem;
80
+ box-shadow:0 24px 64px rgba(0,0,0,.45);
81
+ }
82
+ [data-easy-overlay-top]{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}
83
+ [data-easy-overlay-top] strong{color:#f0a060;font-size:14px;letter-spacing:.04em;text-transform:uppercase}
84
+ [data-easy-overlay-close]{background:transparent;border:0;color:#8b9bb0;font-size:22px;cursor:pointer;line-height:1}
85
+ [data-easy-overlay-loc]{color:#8b9bb0;font-size:12px;margin-bottom:.5rem}
86
+ [data-easy-overlay-msg]{white-space:pre-wrap;font-size:14px;line-height:1.45;margin:0 0 .75rem;color:#fff}
87
+ [data-easy-overlay-hint]{color:#3ddea8;font-size:13px;margin:0 0 .75rem}
88
+ [data-easy-overlay-frame],[data-easy-overlay-stack]{
89
+ background:#0c1016;border-radius:8px;padding:.75rem;font-size:12px;line-height:1.4;
90
+ overflow:auto;color:#c5d0de;margin:0 0 .5rem
91
+ }
92
+ `;
93
+ document.head.appendChild(style);
94
+ document.body.appendChild(root);
95
+ }
@@ -0,0 +1,172 @@
1
+ const REACTIVE = Symbol('reactive');
2
+ const RAW = Symbol('raw');
3
+ const SUBS = Symbol('subs');
4
+
5
+ /**
6
+ * Create a reactive object with key-level subscriptions.
7
+ * Compiled components call state.$subscribe(key, updater) so only
8
+ * the pre-generated DOM updaters for that key run on change.
9
+ */
10
+ export function reactive(target, options = {}) {
11
+ if (target == null || typeof target !== 'object') return target;
12
+ if (target[REACTIVE]) return target;
13
+ // Never proxy built-ins (RegExp, Date, …) — wrapping RegExp breaks String.match
14
+ // with "RegExp.prototype.hasIndices getter called on non-RegExp object".
15
+ if (!isReactiveTarget(target)) return target;
16
+
17
+ const deep = options.deep !== false;
18
+ const subs = new Map(); // key -> Set<fn>
19
+ const anySubs = new Set(); // listeners for any key
20
+
21
+ const notify = (key) => {
22
+ const set = subs.get(key);
23
+ if (set) {
24
+ for (const fn of [...set]) fn(key);
25
+ }
26
+ for (const fn of [...anySubs]) fn(key);
27
+ };
28
+
29
+ const handler = {
30
+ get(obj, key, receiver) {
31
+ if (key === REACTIVE) return true;
32
+ if (key === RAW) return obj;
33
+ if (key === SUBS) return subs;
34
+
35
+ if (key === '$subscribe') {
36
+ return (k, fn) => {
37
+ if (k === '*') {
38
+ anySubs.add(fn);
39
+ return () => anySubs.delete(fn);
40
+ }
41
+ if (!subs.has(k)) subs.set(k, new Set());
42
+ subs.get(k).add(fn);
43
+ return () => {
44
+ const s = subs.get(k);
45
+ if (s) {
46
+ s.delete(fn);
47
+ if (!s.size) subs.delete(k);
48
+ }
49
+ };
50
+ };
51
+ }
52
+
53
+ if (key === '$notify') {
54
+ return (k) => notify(k);
55
+ }
56
+
57
+ if (key === '$raw') {
58
+ return obj;
59
+ }
60
+
61
+ const value = Reflect.get(obj, key, receiver);
62
+ return value;
63
+ },
64
+
65
+ set(obj, key, value, receiver) {
66
+ if (key === REACTIVE || key === RAW || key === SUBS) return false;
67
+
68
+ const old = obj[key];
69
+ let next = value;
70
+
71
+ if (deep && next != null && typeof next === 'object' && !next[REACTIVE]) {
72
+ if (Array.isArray(next)) {
73
+ next = reactiveArray(next, () => notify(key));
74
+ } else if (isReactiveTarget(next)) {
75
+ next = reactive(next, options);
76
+ }
77
+ }
78
+
79
+ if (Object.is(old, next)) return true;
80
+
81
+ const result = Reflect.set(obj, key, next, receiver);
82
+ notify(key);
83
+ return result;
84
+ },
85
+
86
+ deleteProperty(obj, key) {
87
+ if (!(key in obj)) return true;
88
+ const result = Reflect.deleteProperty(obj, key);
89
+ notify(key);
90
+ return result;
91
+ }
92
+ };
93
+
94
+ // Deep-wrap existing nested objects/arrays (skip built-ins like RegExp)
95
+ if (deep) {
96
+ for (const key of Object.keys(target)) {
97
+ const v = target[key];
98
+ if (v != null && typeof v === 'object' && !v[REACTIVE]) {
99
+ if (Array.isArray(v)) {
100
+ const k = key;
101
+ target[key] = reactiveArray(v, () => notify(k));
102
+ } else if (isReactiveTarget(v)) {
103
+ target[key] = reactive(v, options);
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ return new Proxy(target, handler);
110
+ }
111
+
112
+ /** Plain objects / arrays only — never RegExp, Date, Map, DOM nodes, etc. */
113
+ function isReactiveTarget(value) {
114
+ if (value == null || typeof value !== 'object') return false;
115
+ if (Array.isArray(value)) return true;
116
+ if (value instanceof RegExp) return false;
117
+ if (value instanceof Date) return false;
118
+ if (value instanceof Promise) return false;
119
+ if (value instanceof Error) return false;
120
+ if (typeof Map !== 'undefined' && value instanceof Map) return false;
121
+ if (typeof Set !== 'undefined' && value instanceof Set) return false;
122
+ if (typeof WeakMap !== 'undefined' && value instanceof WeakMap) return false;
123
+ if (typeof WeakSet !== 'undefined' && value instanceof WeakSet) return false;
124
+ if (typeof Element !== 'undefined' && value instanceof Element) return false;
125
+ if (typeof Node !== 'undefined' && value instanceof Node) return false;
126
+ const proto = Object.getPrototypeOf(value);
127
+ return proto === Object.prototype || proto === null;
128
+ }
129
+
130
+ /**
131
+ * Array mutations trigger a parent-key notification via onMutate.
132
+ */
133
+ function reactiveArray(arr, onMutate) {
134
+ const mutating = new Set([
135
+ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin'
136
+ ]);
137
+
138
+ return new Proxy(arr, {
139
+ get(target, key, receiver) {
140
+ if (key === REACTIVE) return true;
141
+ if (key === RAW) return target;
142
+ const val = Reflect.get(target, key, receiver);
143
+ if (typeof val === 'function' && mutating.has(key)) {
144
+ return function (...args) {
145
+ const result = val.apply(target, args);
146
+ onMutate();
147
+ return result;
148
+ };
149
+ }
150
+ return val;
151
+ },
152
+ set(target, key, value, receiver) {
153
+ const old = target[key];
154
+ const result = Reflect.set(target, key, value, receiver);
155
+ if (!Object.is(old, value)) onMutate();
156
+ return result;
157
+ },
158
+ deleteProperty(target, key) {
159
+ const result = Reflect.deleteProperty(target, key);
160
+ onMutate();
161
+ return result;
162
+ }
163
+ });
164
+ }
165
+
166
+ export function isReactive(value) {
167
+ return !!(value && value[REACTIVE]);
168
+ }
169
+
170
+ export function raw(value) {
171
+ return (value && value[RAW]) || value;
172
+ }