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,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, '&amp;')
421
+ .replace(/</g, '&lt;')
422
+ .replace(/>/g, '&gt;');
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
+ }
@@ -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,7 @@
1
+ export default {
2
+ srcDir: 'src',
3
+ outDir: 'dist',
4
+ publicDir: 'public',
5
+ entry: 'src/main.js',
6
+ port: 5173
7
+ };
@@ -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,14 @@
1
+ {
2
+ "name": "{{project_name}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "easy dev",
8
+ "build": "easy build",
9
+ "generate": "easy generate"
10
+ },
11
+ "dependencies": {
12
+ "mez-easyjs": "^0.1.0"
13
+ }
14
+ }
@@ -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>
@@ -0,0 +1,4 @@
1
+ import { createApp } from 'mez-easyjs';
2
+ import App from './App.easy';
3
+
4
+ createApp(App).mount('#app');