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,476 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const PKG_ROOT = path.resolve(__dirname, '../..');
9
+ const MONOREPO_ROOT = path.resolve(PKG_ROOT, '../..');
10
+ const TEMPLATES_ROOT = path.resolve(PKG_ROOT, 'templates');
11
+
12
+ const TEMPLATES = {
13
+ minimal: {
14
+ id: 'minimal',
15
+ label: 'Minimal / Empty',
16
+ description: 'Clean skeleton — easy.config.js, App.easy, least deps'
17
+ },
18
+ starter: {
19
+ id: 'starter',
20
+ label: 'Starter / Example',
21
+ description: 'Counter, routing, parent→child props — ready for npm run dev'
22
+ },
23
+ tailwind: {
24
+ id: 'tailwind',
25
+ label: 'Tailwind (CDN)',
26
+ description: 'Play CDN utilities in markup — no PostCSS / npm tailwindcss'
27
+ }
28
+ };
29
+
30
+ const NPM_NAME_RE =
31
+ /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
32
+
33
+ const c = {
34
+ reset: '\x1b[0m',
35
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
36
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
37
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
38
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
39
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
40
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
41
+ magenta: (s) => `\x1b[35m${s}\x1b[0m`
42
+ };
43
+
44
+ /**
45
+ * Scaffold a new Easy.js project (interactive wizard by default).
46
+ *
47
+ * Flags:
48
+ * --name <n> Project name (also positional arg)
49
+ * --template <id> minimal | starter | tailwind
50
+ * --lang <js|ts> Language (only js supported in MVP)
51
+ * --cwd <dir> Parent directory for the project
52
+ * --yes / -y Accept defaults; skip prompts when possible
53
+ * --skip-install Do not run npm install
54
+ * --skip-git Do not git init / commit
55
+ */
56
+ export async function createProject(nameArg, flags = {}) {
57
+ const parentDir = path.resolve(flags.cwd || process.cwd());
58
+ const interactive = process.stdin.isTTY && !flags.yes && !flags.y;
59
+
60
+ console.log(`
61
+ ${c.cyan(c.bold(' easy.js'))} ${c.dim('— create a new project')}
62
+ `);
63
+
64
+ let projectName =
65
+ flags.name ||
66
+ nameArg ||
67
+ (interactive ? await promptName() : null) ||
68
+ 'easy-app';
69
+
70
+ projectName = String(projectName).trim();
71
+ const nameCheck = validateNpmName(projectName);
72
+ if (!nameCheck.ok) {
73
+ throw new Error(nameCheck.error);
74
+ }
75
+
76
+ let language =
77
+ normalizeLang(flags.lang || flags.language) ||
78
+ (interactive ? await promptLanguage() : 'js');
79
+ if (language === 'ts') {
80
+ console.log(
81
+ c.yellow(
82
+ ' TypeScript scaffolding is not available yet — using JavaScript.\n'
83
+ )
84
+ );
85
+ language = 'js';
86
+ }
87
+
88
+ let templateId =
89
+ normalizeTemplate(flags.template || flags.t) ||
90
+ (interactive ? await promptTemplate() : 'starter');
91
+
92
+ if (!TEMPLATES[templateId]) {
93
+ throw new Error(
94
+ `Unknown template "${templateId}". Use: minimal | starter | tailwind`
95
+ );
96
+ }
97
+
98
+ const target = path.resolve(parentDir, projectName);
99
+ // Path traversal protection — project must stay under the chosen parent.
100
+ const relToParent = path.relative(parentDir, target);
101
+ if (
102
+ !relToParent ||
103
+ relToParent.startsWith('..') ||
104
+ path.isAbsolute(relToParent)
105
+ ) {
106
+ throw new Error('Invalid project path (path traversal blocked).');
107
+ }
108
+ if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
109
+ throw new Error(`Directory not empty: ${target}`);
110
+ }
111
+
112
+ if (interactive) {
113
+ const ok = await confirm(
114
+ `Create ${c.bold(projectName)} (${TEMPLATES[templateId].label}, ${language}) in\n ${target}?`
115
+ );
116
+ if (!ok) {
117
+ console.log(c.dim(' Cancelled.'));
118
+ return;
119
+ }
120
+ }
121
+
122
+ const templateDir = path.join(TEMPLATES_ROOT, templateId);
123
+ if (!fs.existsSync(templateDir)) {
124
+ throw new Error(`Template files missing: ${templateDir}`);
125
+ }
126
+
127
+ fs.mkdirSync(target, { recursive: true });
128
+ const vars = {
129
+ project_name: projectName,
130
+ projectName,
131
+ PROJECT_NAME: projectName
132
+ };
133
+ copyDir(templateDir, target, vars);
134
+
135
+ writePackageJson(target, projectName, templateId);
136
+ writeGitignore(target);
137
+
138
+ if (!flags['skip-git'] && !flags.skipGit) {
139
+ initGit(target);
140
+ }
141
+
142
+ if (!flags['skip-install'] && !flags.skipInstall) {
143
+ console.log(c.dim('\n Installing dependencies…\n'));
144
+ const install = runNpmInstall(target);
145
+ if (!install.ok) {
146
+ console.log(
147
+ c.yellow(
148
+ ` npm install failed (you can run it manually in ${projectName}).\n ${install.error || ''}`
149
+ )
150
+ );
151
+ }
152
+ }
153
+
154
+ if (!flags['skip-git'] && !flags.skipGit) {
155
+ initialCommit(target, projectName, templateId);
156
+ }
157
+
158
+ printSuccess({ projectName, templateId, language, target });
159
+ }
160
+
161
+ export function validateNpmName(name) {
162
+ if (!name || !String(name).trim()) {
163
+ return { ok: false, error: 'Project name cannot be empty.' };
164
+ }
165
+ const n = String(name).trim();
166
+ if (n === '.' || n === '..' || n.includes('..')) {
167
+ return { ok: false, error: 'Invalid project name.' };
168
+ }
169
+ if (/[\\]/.test(n) || /[\0]/.test(n)) {
170
+ return { ok: false, error: 'Project name cannot contain path separators.' };
171
+ }
172
+ // Allow a single / only for scoped packages (@scope/name).
173
+ const slashCount = (n.match(/\//g) || []).length;
174
+ if (slashCount > (n.startsWith('@') ? 1 : 0)) {
175
+ return { ok: false, error: 'Invalid project name (too many path segments).' };
176
+ }
177
+ if (/\s/.test(n)) {
178
+ return { ok: false, error: 'Project name cannot contain spaces.' };
179
+ }
180
+ if (/[A-Z]/.test(n)) {
181
+ return {
182
+ ok: false,
183
+ error: 'Project name must be lowercase (npm package rules).'
184
+ };
185
+ }
186
+ if (!NPM_NAME_RE.test(n)) {
187
+ return {
188
+ ok: false,
189
+ error:
190
+ 'Invalid npm package name. Use lowercase letters, digits, hyphens, or @scope/name.'
191
+ };
192
+ }
193
+ if (n.startsWith('.') || n.startsWith('_')) {
194
+ return { ok: false, error: 'Project name cannot start with . or _.' };
195
+ }
196
+ return { ok: true };
197
+ }
198
+
199
+ function normalizeTemplate(raw) {
200
+ if (!raw || raw === true) return null;
201
+ const id = String(raw).toLowerCase().trim();
202
+ if (id === 'empty') return 'minimal';
203
+ if (id === 'example' || id === 'demo') return 'starter';
204
+ if (id === 'tw') return 'tailwind';
205
+ return TEMPLATES[id] ? id : id;
206
+ }
207
+
208
+ function normalizeLang(raw) {
209
+ if (!raw || raw === true) return null;
210
+ const v = String(raw).toLowerCase().trim();
211
+ if (v === 'javascript' || v === 'js') return 'js';
212
+ if (v === 'typescript' || v === 'ts') return 'ts';
213
+ return v;
214
+ }
215
+
216
+ async function promptName() {
217
+ const answer = await ask(` ${c.bold('Project name:')} `, 'easy-app');
218
+ const check = validateNpmName(answer);
219
+ if (!check.ok) {
220
+ console.log(c.red(` ✖ ${check.error}`));
221
+ return promptName();
222
+ }
223
+ return answer.trim();
224
+ }
225
+
226
+ async function promptLanguage() {
227
+ console.log(`
228
+ ${c.bold('Language')}
229
+ ${c.cyan('1)')} JavaScript ${c.green('(available)')}
230
+ ${c.dim('2) TypeScript — coming soon (not available yet)')}
231
+ `);
232
+ const answer = await ask(' Choose [1]: ', '1');
233
+ if (answer.trim() === '2' || /^ts/i.test(answer)) {
234
+ return 'ts';
235
+ }
236
+ return 'js';
237
+ }
238
+
239
+ async function promptTemplate() {
240
+ const ids = Object.keys(TEMPLATES);
241
+ console.log(`\n ${c.bold('Template')}`);
242
+ ids.forEach((id, i) => {
243
+ const t = TEMPLATES[id];
244
+ console.log(
245
+ ` ${c.cyan(`${i + 1})`)} ${t.label} ${c.dim(`— ${t.description}`)}`
246
+ );
247
+ });
248
+ console.log();
249
+ const answer = await ask(' Choose [2]: ', '2');
250
+ const n = Number(answer.trim());
251
+ if (n >= 1 && n <= ids.length) return ids[n - 1];
252
+ const byId = normalizeTemplate(answer);
253
+ if (TEMPLATES[byId]) return byId;
254
+ console.log(c.yellow(' Using starter.'));
255
+ return 'starter';
256
+ }
257
+
258
+ async function confirm(message) {
259
+ console.log(`\n ${message}`);
260
+ const answer = await ask(` ${c.bold('Continue?')} [Y/n] `, 'y');
261
+ return !/^(n|no)$/i.test(answer.trim());
262
+ }
263
+
264
+ function ask(question, defaultValue = '') {
265
+ const rl = readline.createInterface({
266
+ input: process.stdin,
267
+ output: process.stdout
268
+ });
269
+ return new Promise((resolve) => {
270
+ rl.question(question, (answer) => {
271
+ rl.close();
272
+ const v = answer.trim();
273
+ resolve(v || defaultValue);
274
+ });
275
+ });
276
+ }
277
+
278
+ function copyDir(src, dest, vars) {
279
+ fs.mkdirSync(dest, { recursive: true });
280
+ for (const name of fs.readdirSync(src)) {
281
+ if (name === '.git' || name === 'node_modules') continue;
282
+ const from = path.join(src, name);
283
+ const to = path.join(dest, name);
284
+ const st = fs.statSync(from);
285
+ if (st.isDirectory()) {
286
+ copyDir(from, to, vars);
287
+ } else {
288
+ let content = fs.readFileSync(from);
289
+ const textLike = /\.(html?|js|mjs|cjs|json|css|md|easy|ejs|txt|svg)$/i.test(
290
+ name
291
+ );
292
+ if (textLike) {
293
+ let text = content.toString('utf8');
294
+ text = applyVars(text, vars);
295
+ fs.writeFileSync(to, text);
296
+ } else {
297
+ fs.writeFileSync(to, content);
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ function applyVars(text, vars) {
304
+ let out = text;
305
+ for (const [key, value] of Object.entries(vars)) {
306
+ out = out.split(`{{${key}}}`).join(value);
307
+ }
308
+ return out;
309
+ }
310
+
311
+ function writePackageJson(target, projectName, templateId) {
312
+ const pkgPath = path.join(target, 'package.json');
313
+ let pkg;
314
+ if (fs.existsSync(pkgPath)) {
315
+ pkg = JSON.parse(applyVars(fs.readFileSync(pkgPath, 'utf8'), { projectName, project_name: projectName }));
316
+ } else {
317
+ pkg = {
318
+ name: projectName,
319
+ version: '0.1.0',
320
+ private: true,
321
+ type: 'module',
322
+ scripts: {
323
+ dev: 'easy dev',
324
+ build: 'easy build'
325
+ }
326
+ };
327
+ }
328
+
329
+ pkg.name = projectName;
330
+ // Prefer file: links into this monorepo when available; else version pins.
331
+ pkg.dependencies = resolveEasyDependencies(target);
332
+
333
+ if (!pkg.scripts) pkg.scripts = {};
334
+ if (!pkg.scripts.dev) pkg.scripts.dev = 'easy dev';
335
+ if (!pkg.scripts.build) pkg.scripts.build = 'easy build';
336
+
337
+ pkg.easyTemplate = templateId;
338
+
339
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
340
+ }
341
+
342
+ /**
343
+ * Prefer file: links into this monorepo when present (local development).
344
+ * Fall back to published version numbers for consumers of published packages.
345
+ */
346
+ /** Semver range used when scaffolding outside this monorepo (published path). */
347
+ const PUBLISHED_RANGE = '^0.1.0';
348
+
349
+ /**
350
+ * Prefer file: links into this monorepo when present (local development).
351
+ * Fall back to published semver ranges for consumers of published packages.
352
+ */
353
+ function resolveEasyDependencies(targetDir) {
354
+ const local = path.join(MONOREPO_ROOT, 'packages', 'easyjs');
355
+ const pkgJson = path.join(local, 'package.json');
356
+ if (fs.existsSync(pkgJson)) {
357
+ let rel = path.relative(targetDir, local);
358
+ rel = rel.split(path.sep).join('/');
359
+ if (!rel.startsWith('.')) rel = `./${rel}`;
360
+ return { 'mez-easyjs': `file:${rel}` };
361
+ }
362
+ return { 'mez-easyjs': PUBLISHED_RANGE };
363
+ }
364
+
365
+ function writeGitignore(target) {
366
+ const dest = path.join(target, '.gitignore');
367
+ if (fs.existsSync(dest)) return;
368
+ fs.writeFileSync(
369
+ dest,
370
+ `# easy.js
371
+ node_modules/
372
+ dist/
373
+ .easy/
374
+ .cache/
375
+ coverage/
376
+ *.log
377
+ .DS_Store
378
+ Thumbs.db
379
+ *.local
380
+ .env
381
+ .env.*
382
+ `
383
+ );
384
+ }
385
+
386
+ function hasGit() {
387
+ const r = spawnSync('git', ['--version'], { encoding: 'utf8' });
388
+ return r.status === 0;
389
+ }
390
+
391
+ function initGit(target) {
392
+ if (!hasGit()) {
393
+ console.log(c.dim(' git not available — skipping git init'));
394
+ return;
395
+ }
396
+ const r = spawnSync('git', ['init'], { cwd: target, encoding: 'utf8' });
397
+ if (r.status !== 0) {
398
+ console.log(c.dim(' git init failed — skipping'));
399
+ }
400
+ }
401
+
402
+ function initialCommit(target, projectName, templateId) {
403
+ if (!hasGit()) return;
404
+ if (!fs.existsSync(path.join(target, '.git'))) return;
405
+
406
+ spawnSync('git', ['add', '-A'], { cwd: target, encoding: 'utf8' });
407
+ const msg = `chore: initial commit from easy create (${templateId})\n\nScaffolded ${projectName} with the ${templateId} template.`;
408
+ const r = spawnSync('git', ['commit', '-m', msg], {
409
+ cwd: target,
410
+ encoding: 'utf8',
411
+ env: {
412
+ ...process.env,
413
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || 'easy create',
414
+ GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || 'easy@localhost',
415
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || 'easy create',
416
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || 'easy@localhost'
417
+ }
418
+ });
419
+ if (r.status !== 0) {
420
+ console.log(
421
+ c.dim(
422
+ ' git commit skipped (configure user.name / user.email, or commit manually)'
423
+ )
424
+ );
425
+ }
426
+ }
427
+
428
+ function runNpmInstall(target) {
429
+ const isWin = process.platform === 'win32';
430
+ const npmCmd = isWin ? 'npm.cmd' : 'npm';
431
+ const r = spawnSync(npmCmd, ['install'], {
432
+ cwd: target,
433
+ encoding: 'utf8',
434
+ stdio: 'inherit',
435
+ // Avoid shell:true (DEP0190); npm.cmd is invokable directly on Windows.
436
+ windowsHide: true
437
+ });
438
+ return {
439
+ ok: r.status === 0,
440
+ error: r.status === 0 ? null : `exit ${r.status}`
441
+ };
442
+ }
443
+
444
+ function printSuccess({ projectName, templateId, language, target }) {
445
+ const art = `
446
+ ${c.cyan(' ___ __ _ ___ _ _')}
447
+ ${c.cyan(' / _ \\/ _` / __| | | |')}
448
+ ${c.cyan(' | __/ (_| \\__ \\ |_| |')}
449
+ ${c.cyan(' \\___|\\__,_|___/\\__, |')}
450
+ ${c.cyan(' |___/ ')}${c.bold('.js')}
451
+ `;
452
+
453
+ console.log(art);
454
+ console.log(c.green(c.bold(' ✔ Project ready!\n')));
455
+ console.log(` ${c.bold('Name:')} ${projectName}`);
456
+ console.log(` ${c.bold('Template:')} ${TEMPLATES[templateId].label} (${templateId})`);
457
+ console.log(` ${c.bold('Language:')} ${language === 'js' ? 'JavaScript' : language}`);
458
+ console.log(` ${c.bold('Path:')} ${target}`);
459
+ if (templateId === 'tailwind') {
460
+ console.log(
461
+ `\n ${c.dim('Tailwind via Play CDN (@tailwindcss/browser) — no PostCSS.')}`
462
+ );
463
+ }
464
+ console.log(`
465
+ ${c.bold('Next steps:')}
466
+
467
+ ${c.cyan(`cd ${projectName}`)}
468
+ ${c.cyan('npm run dev')}
469
+
470
+ ${c.dim('Docs: https://github.com/easyjs/easy-js')}
471
+ ${c.dim('Issues: https://github.com/easyjs/easy-js/issues')}
472
+ `);
473
+ }
474
+
475
+ // Re-export registry for tests / help
476
+ export { TEMPLATES, TEMPLATES_ROOT, PKG_ROOT };