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,423 @@
|
|
|
1
|
+
import { reactive } from './reactive.js';
|
|
2
|
+
import { mountComponent, unmountComponent } from './component.js';
|
|
3
|
+
|
|
4
|
+
let activeRouter = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* History API SPA router with regex route matching, lazy components,
|
|
8
|
+
* and before/after navigation guards.
|
|
9
|
+
*/
|
|
10
|
+
export function createRouter(options = {}) {
|
|
11
|
+
const routes = normalizeRoutes(options.routes || []);
|
|
12
|
+
const base = options.base || '';
|
|
13
|
+
|
|
14
|
+
const state = reactive({
|
|
15
|
+
path: '/',
|
|
16
|
+
params: {},
|
|
17
|
+
query: {},
|
|
18
|
+
matched: null,
|
|
19
|
+
ready: false
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const beforeGuards = [];
|
|
23
|
+
const afterHooks = [];
|
|
24
|
+
let viewHost = null;
|
|
25
|
+
let currentInstance = null;
|
|
26
|
+
let app = null;
|
|
27
|
+
let pending = null;
|
|
28
|
+
|
|
29
|
+
const router = {
|
|
30
|
+
state,
|
|
31
|
+
options,
|
|
32
|
+
routes,
|
|
33
|
+
|
|
34
|
+
get currentRoute() {
|
|
35
|
+
return {
|
|
36
|
+
path: state.path,
|
|
37
|
+
params: state.params,
|
|
38
|
+
query: state.query,
|
|
39
|
+
matched: state.matched
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
install(appInstance) {
|
|
44
|
+
app = appInstance;
|
|
45
|
+
app._context.router = router;
|
|
46
|
+
activeRouter = router;
|
|
47
|
+
app.provide('router', router);
|
|
48
|
+
|
|
49
|
+
if (typeof window !== 'undefined') {
|
|
50
|
+
window.addEventListener('popstate', () => {
|
|
51
|
+
navigate(getLocation(), { replace: true, fromPop: true });
|
|
52
|
+
});
|
|
53
|
+
// Initial navigation after mount — deferred via microtask so RouterView can register
|
|
54
|
+
queueMicrotask(() => navigate(getLocation(), { replace: true, initial: true }));
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
beforeEach(fn) {
|
|
59
|
+
beforeGuards.push(fn);
|
|
60
|
+
return () => {
|
|
61
|
+
const i = beforeGuards.indexOf(fn);
|
|
62
|
+
if (i >= 0) beforeGuards.splice(i, 1);
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
afterEach(fn) {
|
|
67
|
+
afterHooks.push(fn);
|
|
68
|
+
return () => {
|
|
69
|
+
const i = afterHooks.indexOf(fn);
|
|
70
|
+
if (i >= 0) afterHooks.splice(i, 1);
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
push(to) {
|
|
75
|
+
return navigate(to, { replace: false });
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
replace(to) {
|
|
79
|
+
return navigate(to, { replace: true });
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
go(n) {
|
|
83
|
+
history.go(n);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
back() {
|
|
87
|
+
history.back();
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
forward() {
|
|
91
|
+
history.forward();
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
resolve(to) {
|
|
95
|
+
const loc = typeof to === 'string' ? parseLocation(to) : to;
|
|
96
|
+
const matched = matchRoute(routes, loc.path);
|
|
97
|
+
return { ...loc, matched };
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/** Called by RouterView placeholder */
|
|
101
|
+
_registerView(el) {
|
|
102
|
+
viewHost = el;
|
|
103
|
+
if (state.matched && state.ready) {
|
|
104
|
+
renderMatched();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
_unregisterView(el) {
|
|
109
|
+
if (viewHost === el) viewHost = null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function getLocation() {
|
|
114
|
+
const path = window.location.pathname.slice(base.length) || '/';
|
|
115
|
+
return {
|
|
116
|
+
path,
|
|
117
|
+
query: parseQuery(window.location.search),
|
|
118
|
+
hash: window.location.hash
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function navigate(to, navOpts = {}) {
|
|
123
|
+
const target = typeof to === 'string' ? parseLocation(to) : { ...to };
|
|
124
|
+
if (!target.path) target.path = '/';
|
|
125
|
+
|
|
126
|
+
const matched = matchRoute(routes, target.path);
|
|
127
|
+
const from = router.currentRoute;
|
|
128
|
+
const next = {
|
|
129
|
+
path: target.path,
|
|
130
|
+
query: target.query || {},
|
|
131
|
+
params: matched ? matched.params : {},
|
|
132
|
+
matched: matched ? matched.route : null,
|
|
133
|
+
fullPath: stringifyLocation(target)
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const runId = {};
|
|
137
|
+
pending = runId;
|
|
138
|
+
|
|
139
|
+
return runGuards(beforeGuards, toGuardCtx(next), toGuardCtx(from)).then((allowed) => {
|
|
140
|
+
if (pending !== runId) return;
|
|
141
|
+
if (allowed === false) return false;
|
|
142
|
+
|
|
143
|
+
const finalNext = typeof allowed === 'string' || (allowed && allowed.path)
|
|
144
|
+
? (() => {
|
|
145
|
+
const redir = typeof allowed === 'string' ? parseLocation(allowed) : allowed;
|
|
146
|
+
const m = matchRoute(routes, redir.path);
|
|
147
|
+
return {
|
|
148
|
+
path: redir.path,
|
|
149
|
+
query: redir.query || {},
|
|
150
|
+
params: m ? m.params : {},
|
|
151
|
+
matched: m ? m.route : null,
|
|
152
|
+
fullPath: stringifyLocation(redir)
|
|
153
|
+
};
|
|
154
|
+
})()
|
|
155
|
+
: next;
|
|
156
|
+
|
|
157
|
+
if (!navOpts.fromPop && typeof history !== 'undefined') {
|
|
158
|
+
const url = base + finalNext.fullPath;
|
|
159
|
+
if (navOpts.replace || navOpts.initial) {
|
|
160
|
+
history.replaceState({ path: finalNext.path }, '', url);
|
|
161
|
+
} else {
|
|
162
|
+
history.pushState({ path: finalNext.path }, '', url);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
state.path = finalNext.path;
|
|
167
|
+
state.params = finalNext.params;
|
|
168
|
+
state.query = finalNext.query;
|
|
169
|
+
// Store a plain snapshot — never put RegExp on reactive state
|
|
170
|
+
// (deep reactive wrapping would proxy regex and break later matches).
|
|
171
|
+
state.matched = finalNext.matched ? snapshotRoute(finalNext.matched) : null;
|
|
172
|
+
state.ready = true;
|
|
173
|
+
|
|
174
|
+
return renderMatched().then(() => {
|
|
175
|
+
for (const hook of afterHooks) {
|
|
176
|
+
try {
|
|
177
|
+
hook(finalNext, from);
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error('[easy] afterEach error:', e);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return finalNext;
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderMatched() {
|
|
188
|
+
if (!viewHost) return Promise.resolve();
|
|
189
|
+
|
|
190
|
+
if (currentInstance) {
|
|
191
|
+
unmountComponent(currentInstance);
|
|
192
|
+
currentInstance = null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
viewHost.innerHTML = '';
|
|
196
|
+
|
|
197
|
+
const route = state.matched;
|
|
198
|
+
if (!route) {
|
|
199
|
+
viewHost.innerHTML = `<div data-easy-miss>No route matched: ${escapeHtml(state.path)}</div>`;
|
|
200
|
+
return Promise.resolve();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let loader = route.component;
|
|
204
|
+
if (route.components && route.components.default) {
|
|
205
|
+
loader = route.components.default;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return Promise.resolve()
|
|
209
|
+
.then(() => {
|
|
210
|
+
if (typeof loader === 'function' && !loader.__isEasyComponent && !loader.setup) {
|
|
211
|
+
return loader();
|
|
212
|
+
}
|
|
213
|
+
return loader;
|
|
214
|
+
})
|
|
215
|
+
.then((mod) => {
|
|
216
|
+
const comp = mod?.default || mod;
|
|
217
|
+
// Pass full app._context (incl. registerContext) so page/child event
|
|
218
|
+
// handlers resolve — without it, clicks fall back to App-only methods.
|
|
219
|
+
const appCtx = app && app._context ? app._context : {};
|
|
220
|
+
currentInstance = mountComponent(
|
|
221
|
+
comp,
|
|
222
|
+
viewHost,
|
|
223
|
+
{ params: state.params, query: state.query },
|
|
224
|
+
{
|
|
225
|
+
...appCtx,
|
|
226
|
+
app,
|
|
227
|
+
router,
|
|
228
|
+
parent: null
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
return currentInstance;
|
|
232
|
+
})
|
|
233
|
+
.catch((err) => {
|
|
234
|
+
console.error('[easy] route load failed:', err);
|
|
235
|
+
viewHost.innerHTML = `<div data-easy-error>Failed to load page</div>`;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return router;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function useRouter() {
|
|
243
|
+
return activeRouter;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Lightweight RouterView marker — compiled apps call router._registerView(el).
|
|
248
|
+
* Can also be used as a programmatic helper.
|
|
249
|
+
*/
|
|
250
|
+
export function RouterView(host, router) {
|
|
251
|
+
const r = router || activeRouter;
|
|
252
|
+
if (!r) throw new Error('[easy] RouterView requires a router');
|
|
253
|
+
r._registerView(host);
|
|
254
|
+
return {
|
|
255
|
+
unmount() {
|
|
256
|
+
r._unregisterView(host);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeRoutes(routes, parentPath = '') {
|
|
262
|
+
const result = [];
|
|
263
|
+
for (const route of routes) {
|
|
264
|
+
const fullPath = joinPath(parentPath, route.path);
|
|
265
|
+
const record = {
|
|
266
|
+
...route,
|
|
267
|
+
path: fullPath,
|
|
268
|
+
regex: pathToRegex(fullPath),
|
|
269
|
+
keys: extractKeys(fullPath)
|
|
270
|
+
};
|
|
271
|
+
result.push(record);
|
|
272
|
+
if (route.children) {
|
|
273
|
+
result.push(...normalizeRoutes(route.children, fullPath));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Prefer static specificity: more segments / fewer params first
|
|
277
|
+
result.sort((a, b) => scoreRoute(b.path) - scoreRoute(a.path));
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function scoreRoute(path) {
|
|
282
|
+
const parts = path.split('/').filter(Boolean);
|
|
283
|
+
let score = parts.length * 10;
|
|
284
|
+
for (const p of parts) {
|
|
285
|
+
if (p.startsWith(':')) score -= 5;
|
|
286
|
+
else if (p === '*') score -= 8;
|
|
287
|
+
else score += 2;
|
|
288
|
+
}
|
|
289
|
+
return score;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function pathToRegex(path) {
|
|
293
|
+
if (path === '*') return /.*/;
|
|
294
|
+
const pattern =
|
|
295
|
+
'^' +
|
|
296
|
+
path
|
|
297
|
+
.replace(/\/\*$/, '(?:/.*)?')
|
|
298
|
+
.replace(/\//g, '\\/')
|
|
299
|
+
.replace(/:([A-Za-z_][\w]*)/g, '([^/]+)') +
|
|
300
|
+
'\\/?$';
|
|
301
|
+
return new RegExp(pattern);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function extractKeys(path) {
|
|
305
|
+
const keys = [];
|
|
306
|
+
path.replace(/:([A-Za-z_][\w]*)/g, (_, k) => {
|
|
307
|
+
keys.push(k);
|
|
308
|
+
return '';
|
|
309
|
+
});
|
|
310
|
+
return keys;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function matchRoute(routes, path) {
|
|
314
|
+
for (const route of routes) {
|
|
315
|
+
const m = path.match(route.regex);
|
|
316
|
+
if (m) {
|
|
317
|
+
const params = {};
|
|
318
|
+
route.keys.forEach((key, i) => {
|
|
319
|
+
params[key] = decodeURIComponent(m[i + 1] || '');
|
|
320
|
+
});
|
|
321
|
+
return { route, params };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseLocation(input) {
|
|
328
|
+
const [pathAndQuery, hash = ''] = input.split('#');
|
|
329
|
+
const [path, qs = ''] = pathAndQuery.split('?');
|
|
330
|
+
return {
|
|
331
|
+
path: path || '/',
|
|
332
|
+
query: parseQuery(qs ? `?${qs}` : ''),
|
|
333
|
+
hash: hash ? `#${hash}` : ''
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parseQuery(search) {
|
|
338
|
+
const query = {};
|
|
339
|
+
const s = search.startsWith('?') ? search.slice(1) : search;
|
|
340
|
+
if (!s) return query;
|
|
341
|
+
for (const part of s.split('&')) {
|
|
342
|
+
if (!part) continue;
|
|
343
|
+
const [k, v = ''] = part.split('=');
|
|
344
|
+
query[decodeURIComponent(k)] = decodeURIComponent(v);
|
|
345
|
+
}
|
|
346
|
+
return query;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function stringifyLocation({ path, query = {}, hash = '' }) {
|
|
350
|
+
const qs = Object.keys(query)
|
|
351
|
+
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(query[k])}`)
|
|
352
|
+
.join('&');
|
|
353
|
+
return `${path || '/'}${qs ? `?${qs}` : ''}${hash || ''}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function joinPath(parent, child) {
|
|
357
|
+
if (child.startsWith('/')) return child;
|
|
358
|
+
if (!parent || parent === '/') return `/${child}`.replace(/\/+/g, '/');
|
|
359
|
+
return `${parent}/${child}`.replace(/\/+/g, '/');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function snapshotRoute(route) {
|
|
363
|
+
if (!route) return null;
|
|
364
|
+
return {
|
|
365
|
+
path: route.path,
|
|
366
|
+
name: route.name,
|
|
367
|
+
meta: route.meta,
|
|
368
|
+
component: route.component,
|
|
369
|
+
components: route.components
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function toGuardCtx(route) {
|
|
374
|
+
return {
|
|
375
|
+
path: route.path,
|
|
376
|
+
params: route.params || {},
|
|
377
|
+
query: route.query || {},
|
|
378
|
+
matched: route.matched || null,
|
|
379
|
+
fullPath: route.fullPath || route.path
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function runGuards(guards, to, from) {
|
|
384
|
+
return guards.reduce(
|
|
385
|
+
(p, guard) =>
|
|
386
|
+
p.then((prev) => {
|
|
387
|
+
if (prev === false) return false;
|
|
388
|
+
if (typeof prev === 'string' || (prev && prev.path)) return prev;
|
|
389
|
+
return new Promise((resolve) => {
|
|
390
|
+
let settled = false;
|
|
391
|
+
const next = (val) => {
|
|
392
|
+
if (settled) return;
|
|
393
|
+
settled = true;
|
|
394
|
+
if (val === undefined || val === true) resolve(true);
|
|
395
|
+
else resolve(val);
|
|
396
|
+
};
|
|
397
|
+
try {
|
|
398
|
+
const result = guard(to, from, next);
|
|
399
|
+
if (result && typeof result.then === 'function') {
|
|
400
|
+
result.then((v) => {
|
|
401
|
+
if (!settled) next(v === undefined ? true : v);
|
|
402
|
+
}, () => next(false));
|
|
403
|
+
} else if (result !== undefined && guard.length < 3) {
|
|
404
|
+
next(result);
|
|
405
|
+
} else if (guard.length < 3 && !settled) {
|
|
406
|
+
next(true);
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
console.error('[easy] navigation guard error:', e);
|
|
410
|
+
next(false);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}),
|
|
414
|
+
Promise.resolve(true)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function escapeHtml(s) {
|
|
419
|
+
return String(s)
|
|
420
|
+
.replace(/&/g, '&')
|
|
421
|
+
.replace(/</g, '<')
|
|
422
|
+
.replace(/>/g, '>');
|
|
423
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const queue = new Set();
|
|
2
|
+
let flushing = false;
|
|
3
|
+
let pending = [];
|
|
4
|
+
|
|
5
|
+
export function queueJob(fn) {
|
|
6
|
+
queue.add(fn);
|
|
7
|
+
if (!flushing) {
|
|
8
|
+
flushing = true;
|
|
9
|
+
queueMicrotask(flushJobs);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function flushJobs() {
|
|
14
|
+
const jobs = [...queue];
|
|
15
|
+
queue.clear();
|
|
16
|
+
flushing = false;
|
|
17
|
+
for (const job of jobs) {
|
|
18
|
+
try {
|
|
19
|
+
job();
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error('[easy] scheduler error:', e);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const cbs = pending;
|
|
25
|
+
pending = [];
|
|
26
|
+
for (const cb of cbs) cb();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function nextTick(fn) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
pending.push(() => {
|
|
32
|
+
if (fn) fn();
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
if (!flushing && queue.size === 0) {
|
|
36
|
+
queueMicrotask(flushJobs);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { reactive } from './reactive.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple global store.
|
|
5
|
+
*
|
|
6
|
+
* const store = createStore({
|
|
7
|
+
* state: () => ({ count: 0 }),
|
|
8
|
+
* actions: {
|
|
9
|
+
* increment(state) { state.count++ }
|
|
10
|
+
* }
|
|
11
|
+
* });
|
|
12
|
+
*/
|
|
13
|
+
export function createStore(options = {}) {
|
|
14
|
+
const stateInit = typeof options.state === 'function' ? options.state() : (options.state || {});
|
|
15
|
+
const state = reactive(stateInit);
|
|
16
|
+
const actions = options.actions || {};
|
|
17
|
+
const getters = options.getters || {};
|
|
18
|
+
|
|
19
|
+
const api = {
|
|
20
|
+
get state() {
|
|
21
|
+
return state;
|
|
22
|
+
},
|
|
23
|
+
subscribe(key, fn) {
|
|
24
|
+
return state.$subscribe(key, fn);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (const [name, fn] of Object.entries(actions)) {
|
|
29
|
+
api[name] = (...args) => fn(state, ...args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [name, fn] of Object.entries(getters)) {
|
|
33
|
+
Object.defineProperty(api, name, {
|
|
34
|
+
get() {
|
|
35
|
+
return fn(state);
|
|
36
|
+
},
|
|
37
|
+
enumerable: true
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return api;
|
|
42
|
+
}
|
package/src/transform.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { compile } from './compiler/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Transform a module source for the browser:
|
|
8
|
+
* - compile .easy SFCs
|
|
9
|
+
* - rewrite mez-easyjs / mez-easyjs/runtime imports to served virtual paths
|
|
10
|
+
*/
|
|
11
|
+
export function transformFile(filePath, source, options = {}) {
|
|
12
|
+
const ext = path.extname(filePath);
|
|
13
|
+
let code = source;
|
|
14
|
+
let map = null;
|
|
15
|
+
|
|
16
|
+
if (ext === '.easy' || ext === '.ejs') {
|
|
17
|
+
const result = compile(source, {
|
|
18
|
+
filename: path.basename(filePath),
|
|
19
|
+
id: options.scopeId
|
|
20
|
+
});
|
|
21
|
+
code = result.code;
|
|
22
|
+
map = result.map;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (options.runtimeUrl) {
|
|
26
|
+
code = rewriteRuntimeImports(code, options.runtimeUrl);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.alias) {
|
|
30
|
+
for (const [from, to] of Object.entries(options.alias)) {
|
|
31
|
+
code = code.replaceAll(from, to);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { code, map };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function rewriteRuntimeImports(code, runtimeUrl) {
|
|
39
|
+
const base = runtimeUrl.replace(/\/index\.js$/, '');
|
|
40
|
+
return code
|
|
41
|
+
.replace(/(from\s+|import\s*\(\s*)['"]mez-easyjs['"]/g, `$1'${runtimeUrl}'`)
|
|
42
|
+
.replace(/(from\s+|import\s*\(\s*)['"]mez-easyjs\/runtime['"]/g, `$1'${runtimeUrl}'`)
|
|
43
|
+
.replace(
|
|
44
|
+
/(from\s+|import\s*\(\s*)['"]mez-easyjs\/runtime\/([^'"]+)['"]/g,
|
|
45
|
+
`$1'${base}/$2'`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveProject(cwd) {
|
|
50
|
+
const root = path.resolve(cwd || process.cwd());
|
|
51
|
+
const configPath = path.join(root, 'easy.config.js');
|
|
52
|
+
let config = {};
|
|
53
|
+
if (fs.existsSync(configPath)) {
|
|
54
|
+
// sync-ish: caller may await loadConfig
|
|
55
|
+
}
|
|
56
|
+
return { root, configPath, config };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function loadConfig(root) {
|
|
60
|
+
const configPath = path.join(root, 'easy.config.js');
|
|
61
|
+
if (!fs.existsSync(configPath)) {
|
|
62
|
+
return defaultConfig(root);
|
|
63
|
+
}
|
|
64
|
+
const mod = await import(pathToFileURL(configPath).href + '?t=' + Date.now());
|
|
65
|
+
return { ...defaultConfig(root), ...(mod.default || mod) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function defaultConfig(root) {
|
|
69
|
+
return {
|
|
70
|
+
root,
|
|
71
|
+
srcDir: 'src',
|
|
72
|
+
outDir: 'dist',
|
|
73
|
+
publicDir: 'public',
|
|
74
|
+
entry: 'src/main.js',
|
|
75
|
+
port: 5173
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function walkFiles(dir, exts = ['.js', '.easy', '.css', '.html']) {
|
|
80
|
+
const results = [];
|
|
81
|
+
if (!fs.existsSync(dir)) return results;
|
|
82
|
+
|
|
83
|
+
const stack = [dir];
|
|
84
|
+
while (stack.length) {
|
|
85
|
+
const cur = stack.pop();
|
|
86
|
+
for (const name of fs.readdirSync(cur)) {
|
|
87
|
+
if (name === 'node_modules' || name === 'dist' || name.startsWith('.')) continue;
|
|
88
|
+
const full = path.join(cur, name);
|
|
89
|
+
const st = fs.statSync(full);
|
|
90
|
+
if (st.isDirectory()) stack.push(full);
|
|
91
|
+
else if (exts.includes(path.extname(name))) results.push(full);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Tiny JS minifier — whitespace/comment strip (MVP, no dep). */
|
|
98
|
+
export function minifyJS(code) {
|
|
99
|
+
return code
|
|
100
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
101
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
102
|
+
.replace(/\n+/g, '\n')
|
|
103
|
+
.replace(/[ \t]+/g, ' ')
|
|
104
|
+
.replace(/\s*([{};,:()=<>+\-*/%&|!?])\s*/g, '$1')
|
|
105
|
+
.trim();
|
|
106
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# {{project_name}}
|
|
2
|
+
|
|
3
|
+
Minimal Easy.js app (scaffolded with `easy create`).
|
|
4
|
+
|
|
5
|
+
## Scripts
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev # http://localhost:5173
|
|
9
|
+
npm run build # production → dist/
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Dependencies
|
|
13
|
+
|
|
14
|
+
This project depends on [`mez-easyjs`](https://www.npmjs.com/package/mez-easyjs) (runtime + compiler + CLI).
|
|
15
|
+
|
|
16
|
+
When created from the Easy.js monorepo, `package.json` uses a `file:` link into `packages/easyjs`.
|
|
17
|
+
Published users get `"mez-easyjs": "^0.1.0"`.
|
|
18
|
+
|
|
19
|
+
## Language
|
|
20
|
+
|
|
21
|
+
JavaScript only for this template. TypeScript scaffolding is planned.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{project_name}}</title>
|
|
7
|
+
<link rel="stylesheet" href="/src/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.js"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let title = 'Welcome to Easy.js';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<main class="welcome">
|
|
6
|
+
<h1>{title}</h1>
|
|
7
|
+
<p>Edit <code>src/App.easy</code> and save — HMR updates in place.</p>
|
|
8
|
+
</main>
|
|
9
|
+
|
|
10
|
+
<style>
|
|
11
|
+
.welcome {
|
|
12
|
+
padding: 2rem 1.5rem;
|
|
13
|
+
max-width: 40rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
h1 {
|
|
17
|
+
letter-spacing: -0.03em;
|
|
18
|
+
margin: 0 0 0.75rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
code {
|
|
22
|
+
font-size: 0.95em;
|
|
23
|
+
}
|
|
24
|
+
</style>
|