zero-query 0.3.1 → 0.5.2

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/cli.js DELETED
@@ -1,1208 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * zQuery CLI
5
- *
6
- * Zero-dependency command-line tool for building the zQuery library,
7
- * bundling zQuery-based applications, and running a dev server with
8
- * live-reload.
9
- *
10
- * Usage:
11
- * zquery build Build the zQuery library (dist/)
12
- *
13
- * zquery bundle [entry] Bundle an app's ES modules into one file
14
- * zquery bundle scripts/app.js Specify entry explicitly
15
- * zquery bundle -o build/ Custom output directory
16
- * zquery bundle --html other.html Use a specific HTML file instead of auto-detected
17
- *
18
- * zquery dev [root] Start dev server with live-reload
19
- * zquery dev --port 8080 Custom port (default: 3100)
20
- *
21
- * Smart defaults (no flags needed for typical projects):
22
- * - Entry is auto-detected from index.html's <script type="module" src="...">
23
- * - zquery.min.js is always embedded (auto-built if not found)
24
- * - index.html is always rewritten and assets are copied
25
- * - Output goes to dist/ next to the detected index.html
26
- *
27
- * Examples:
28
- * cd my-app && npx zero-query bundle # just works!
29
- * npx zero-query bundle path/to/scripts/app.js # works from anywhere
30
- * cd my-app && npx zquery dev # dev server with live-reload
31
- */
32
-
33
- const fs = require('fs');
34
- const path = require('path');
35
- const crypto = require('crypto');
36
-
37
- // ---------------------------------------------------------------------------
38
- // CLI argument parsing
39
- // ---------------------------------------------------------------------------
40
-
41
- const args = process.argv.slice(2);
42
- const command = args[0];
43
-
44
- function flag(name, short) {
45
- const i = args.indexOf(`--${name}`);
46
- const j = short ? args.indexOf(`-${short}`) : -1;
47
- return i !== -1 || j !== -1;
48
- }
49
-
50
- function option(name, short, fallback) {
51
- let i = args.indexOf(`--${name}`);
52
- if (i === -1 && short) i = args.indexOf(`-${short}`);
53
- if (i !== -1 && i + 1 < args.length) return args[i + 1];
54
- return fallback;
55
- }
56
-
57
- // ---------------------------------------------------------------------------
58
- // Shared utilities
59
- // ---------------------------------------------------------------------------
60
-
61
- /**
62
- * Context-aware comment stripper — skips strings, templates, regex.
63
- * Reused from build.js.
64
- */
65
- function stripComments(code) {
66
- let out = '';
67
- let i = 0;
68
- while (i < code.length) {
69
- const ch = code[i];
70
- const next = code[i + 1];
71
-
72
- // String literals
73
- if (ch === '"' || ch === "'" || ch === '`') {
74
- const quote = ch;
75
- out += ch; i++;
76
- while (i < code.length) {
77
- if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
78
- out += code[i];
79
- if (code[i] === quote) { i++; break; }
80
- i++;
81
- }
82
- continue;
83
- }
84
-
85
- // Block comment
86
- if (ch === '/' && next === '*') {
87
- i += 2;
88
- while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) i++;
89
- i += 2;
90
- continue;
91
- }
92
-
93
- // Line comment
94
- if (ch === '/' && next === '/') {
95
- i += 2;
96
- while (i < code.length && code[i] !== '\n') i++;
97
- continue;
98
- }
99
-
100
- // Regex literal
101
- if (ch === '/') {
102
- const before = out.replace(/\s+$/, '');
103
- const last = before[before.length - 1];
104
- const isRegexCtx = !last || '=({[,;:!&|?~+-*/%^>'.includes(last)
105
- || before.endsWith('return') || before.endsWith('typeof')
106
- || before.endsWith('case') || before.endsWith('in')
107
- || before.endsWith('delete') || before.endsWith('void')
108
- || before.endsWith('throw') || before.endsWith('new');
109
- if (isRegexCtx) {
110
- out += ch; i++;
111
- let inCharClass = false;
112
- while (i < code.length) {
113
- const rc = code[i];
114
- if (rc === '\\') { out += rc + (code[i + 1] || ''); i += 2; continue; }
115
- if (rc === '[') inCharClass = true;
116
- if (rc === ']') inCharClass = false;
117
- out += rc; i++;
118
- if (rc === '/' && !inCharClass) {
119
- while (i < code.length && /[gimsuy]/.test(code[i])) { out += code[i]; i++; }
120
- break;
121
- }
122
- }
123
- continue;
124
- }
125
- }
126
-
127
- out += ch; i++;
128
- }
129
- return out;
130
- }
131
-
132
- /** Quick minification (same approach as build.js). */
133
- function minify(code, banner) {
134
- const body = stripComments(code.replace(banner, ''))
135
- .replace(/^\s*\n/gm, '')
136
- .replace(/\n\s+/g, '\n')
137
- .replace(/\s{2,}/g, ' ');
138
- return banner + '\n' + body;
139
- }
140
-
141
- function sizeKB(buf) {
142
- return (buf.length / 1024).toFixed(1);
143
- }
144
-
145
- // ---------------------------------------------------------------------------
146
- // "build" command — library build (mirrors build.js)
147
- // ---------------------------------------------------------------------------
148
-
149
- function buildLibrary() {
150
- const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
151
- const VERSION = pkg.version;
152
-
153
- const modules = [
154
- 'src/reactive.js', 'src/core.js', 'src/component.js',
155
- 'src/router.js', 'src/store.js', 'src/http.js', 'src/utils.js',
156
- ];
157
-
158
- const DIST = path.join(process.cwd(), 'dist');
159
- const OUT_FILE = path.join(DIST, 'zquery.js');
160
- const MIN_FILE = path.join(DIST, 'zquery.min.js');
161
-
162
- const start = Date.now();
163
- if (!fs.existsSync(DIST)) fs.mkdirSync(DIST, { recursive: true });
164
-
165
- const parts = modules.map(file => {
166
- let code = fs.readFileSync(path.join(process.cwd(), file), 'utf-8');
167
- code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
168
- code = code.replace(/^export\s+(default\s+)?/gm, '');
169
- code = code.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
170
- return `// --- ${file} ${'—'.repeat(60 - file.length)}\n${code.trim()}`;
171
- });
172
-
173
- let indexCode = fs.readFileSync(path.join(process.cwd(), 'index.js'), 'utf-8');
174
- indexCode = indexCode.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
175
- indexCode = indexCode.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
176
- indexCode = indexCode.replace(/^export\s+(default\s+)?/gm, '');
177
-
178
- const banner = `/**\n * zQuery (zeroQuery) v${VERSION}\n * Lightweight Frontend Library\n * https://github.com/tonywied17/zero-query\n * (c) ${new Date().getFullYear()} Anthony Wiedman — MIT License\n */`;
179
-
180
- const bundle = `${banner}\n(function(global) {\n 'use strict';\n\n${parts.join('\n\n')}\n\n// --- index.js (assembly) ${'—'.repeat(42)}\n${indexCode.trim().replace("'__VERSION__'", `'${VERSION}'`)}\n\n})(typeof window !== 'undefined' ? window : globalThis);\n`;
181
-
182
- fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
183
- fs.writeFileSync(MIN_FILE, minify(bundle, banner), 'utf-8');
184
-
185
- const elapsed = Date.now() - start;
186
- console.log(` ✓ dist/zquery.js (${sizeKB(fs.readFileSync(OUT_FILE))} KB)`);
187
- console.log(` ✓ dist/zquery.min.js (${sizeKB(fs.readFileSync(MIN_FILE))} KB)`);
188
- console.log(` Done in ${elapsed}ms\n`);
189
-
190
- return { DIST, OUT_FILE, MIN_FILE };
191
- }
192
-
193
-
194
- // ---------------------------------------------------------------------------
195
- // "bundle" command — app bundler
196
- // ---------------------------------------------------------------------------
197
-
198
- /**
199
- * Resolve an import specifier relative to the importing file.
200
- */
201
- function resolveImport(specifier, fromFile) {
202
- if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null; // bare specifier
203
- let resolved = path.resolve(path.dirname(fromFile), specifier);
204
- // If no extension and a .js file exists, add it
205
- if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
206
- resolved += '.js';
207
- }
208
- return resolved;
209
- }
210
-
211
- /**
212
- * Extract import specifiers from a source file.
213
- * Handles: import 'x', import x from 'x', import { a } from 'x', import * as x from 'x'
214
- * (including multi-line destructured imports)
215
- */
216
- function extractImports(code) {
217
- const specifiers = [];
218
- let m;
219
- // Pattern 1: import ... from 'specifier' (works for single & multi-line)
220
- const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
221
- while ((m = fromRe.exec(code)) !== null) {
222
- specifiers.push(m[1]);
223
- }
224
- // Pattern 2: side-effect imports — import './foo.js';
225
- const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
226
- while ((m = sideRe.exec(code)) !== null) {
227
- if (!specifiers.includes(m[1])) specifiers.push(m[1]);
228
- }
229
- return specifiers;
230
- }
231
-
232
- /**
233
- * Walk the import graph starting from `entry`, return files in dependency
234
- * order (leaves first — topological sort).
235
- */
236
- function walkImportGraph(entry) {
237
- const visited = new Set();
238
- const order = [];
239
-
240
- function visit(file) {
241
- const abs = path.resolve(file);
242
- if (visited.has(abs)) return;
243
- visited.add(abs);
244
-
245
- if (!fs.existsSync(abs)) {
246
- console.warn(` ⚠ Missing file: ${abs}`);
247
- return;
248
- }
249
-
250
- const code = fs.readFileSync(abs, 'utf-8');
251
- const imports = extractImports(code);
252
-
253
- for (const spec of imports) {
254
- const resolved = resolveImport(spec, abs);
255
- if (resolved) visit(resolved);
256
- }
257
-
258
- order.push(abs);
259
- }
260
-
261
- visit(entry);
262
- return order;
263
- }
264
-
265
- /**
266
- * Strip ES module import/export syntax from code, keeping declarations.
267
- */
268
- function stripModuleSyntax(code) {
269
- // Remove import lines (single-line and multi-line from ... )
270
- code = code.replace(/^\s*import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
271
- // Remove side-effect imports import './foo.js';
272
- code = code.replace(/^\s*import\s+['"].*?['"];?\s*$/gm, '');
273
- // Remove export default but keep the expression
274
- code = code.replace(/^(\s*)export\s+default\s+/gm, '$1');
275
- // Remove export keyword but keep declarations
276
- code = code.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/gm, '$1$2 ');
277
- // Remove standalone export { ... } blocks
278
- code = code.replace(/^\s*export\s*\{[\s\S]*?\};?\s*$/gm, '');
279
- return code;
280
- }
281
-
282
- /**
283
- * Replace `import.meta.url` with a runtime equivalent.
284
- * In a non-module <script>, import.meta doesn't exist, so we substitute
285
- * document.currentScript.src (set at load time) relative to the original
286
- * file's path inside the project.
287
- */
288
- function replaceImportMeta(code, filePath, projectRoot) {
289
- if (!code.includes('import.meta')) return code;
290
- // Compute the web-relative path of this file from the project root
291
- const rel = path.relative(projectRoot, filePath).replace(/\\/g, '/');
292
- // Replace import.meta.url with a constructed URL based on the page origin
293
- code = code.replace(
294
- /import\.meta\.url/g,
295
- `(new URL('${rel}', document.baseURI).href)`
296
- );
297
- return code;
298
- }
299
-
300
- /**
301
- * Scan bundled source files for external resource references
302
- * (pages config, templateUrl, styleUrl) and read those files so they
303
- * can be inlined into the bundle for file:// support.
304
- *
305
- * Returns a map of { relativePath: fileContent }.
306
- */
307
- function collectInlineResources(files, projectRoot) {
308
- const inlineMap = {};
309
-
310
- for (const file of files) {
311
- const code = fs.readFileSync(file, 'utf-8');
312
- const fileDir = path.dirname(file);
313
-
314
- // Detect `pages:` config — look for dir and items
315
- const pagesMatch = code.match(/pages\s*:\s*\{[^}]*dir\s*:\s*['"]([^'"]+)['"]/s);
316
- if (pagesMatch) {
317
- const pagesDir = pagesMatch[1];
318
- const ext = (code.match(/pages\s*:\s*\{[^}]*ext\s*:\s*['"]([^'"]+)['"]/s) || [])[1] || '.html';
319
-
320
- // Extract item IDs from the items array
321
- const itemsMatch = code.match(/items\s*:\s*\[([\s\S]*?)\]/);
322
- if (itemsMatch) {
323
- const itemsBlock = itemsMatch[1];
324
- const ids = [];
325
- // Match string items: 'getting-started'
326
- let m;
327
- const strRe = /['"]([^'"]+)['"]/g;
328
- const objIdRe = /id\s*:\s*['"]([^'"]+)['"]/g;
329
- // Collect all quoted strings that look like page IDs
330
- while ((m = strRe.exec(itemsBlock)) !== null) {
331
- // Skip labels (preceded by "label:")
332
- const before = itemsBlock.substring(Math.max(0, m.index - 20), m.index);
333
- if (/label\s*:\s*$/.test(before)) continue;
334
- ids.push(m[1]);
335
- }
336
-
337
- // Read each page file
338
- const absPagesDir = path.join(fileDir, pagesDir);
339
- for (const id of ids) {
340
- const pagePath = path.join(absPagesDir, id + ext);
341
- if (fs.existsSync(pagePath)) {
342
- const relKey = path.relative(projectRoot, pagePath).replace(/\\/g, '/');
343
- inlineMap[relKey] = fs.readFileSync(pagePath, 'utf-8');
344
- }
345
- }
346
- }
347
- }
348
-
349
- // Detect `styleUrl:` — single string
350
- const styleMatch = code.match(/styleUrl\s*:\s*['"]([^'"]+)['"]/);
351
- if (styleMatch) {
352
- const stylePath = path.join(fileDir, styleMatch[1]);
353
- if (fs.existsSync(stylePath)) {
354
- const relKey = path.relative(projectRoot, stylePath).replace(/\\/g, '/');
355
- inlineMap[relKey] = fs.readFileSync(stylePath, 'utf-8');
356
- }
357
- }
358
-
359
- // Detect `templateUrl:` — single string
360
- const tmplMatch = code.match(/templateUrl\s*:\s*['"]([^'"]+)['"]/);
361
- if (tmplMatch) {
362
- const tmplPath = path.join(fileDir, tmplMatch[1]);
363
- if (fs.existsSync(tmplPath)) {
364
- const relKey = path.relative(projectRoot, tmplPath).replace(/\\/g, '/');
365
- inlineMap[relKey] = fs.readFileSync(tmplPath, 'utf-8');
366
- }
367
- }
368
- }
369
-
370
- return inlineMap;
371
- }
372
-
373
- /**
374
- * Try to auto-detect the app entry point.
375
- * Looks for <script type="module" src="..."> in an index.html,
376
- * or falls back to common conventions.
377
- */
378
- function detectEntry(projectRoot) {
379
- const htmlCandidates = ['index.html', 'public/index.html'];
380
- for (const htmlFile of htmlCandidates) {
381
- const htmlPath = path.join(projectRoot, htmlFile);
382
- if (fs.existsSync(htmlPath)) {
383
- const html = fs.readFileSync(htmlPath, 'utf-8');
384
- const m = html.match(/<script[^>]+type\s*=\s*["']module["'][^>]+src\s*=\s*["']([^"']+)["']/);
385
- if (m) return path.join(projectRoot, m[1]);
386
- }
387
- }
388
- // Convention fallback
389
- const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
390
- for (const f of fallbacks) {
391
- const fp = path.join(projectRoot, f);
392
- if (fs.existsSync(fp)) return fp;
393
- }
394
- return null;
395
- }
396
-
397
-
398
- function bundleApp() {
399
- const projectRoot = process.cwd();
400
-
401
- // Entry point
402
- let entry = null;
403
- // First positional arg after "bundle" that doesn't start with -
404
- for (let i = 1; i < args.length; i++) {
405
- if (!args[i].startsWith('-') && args[i - 1] !== '-o' && args[i - 1] !== '--out' && args[i - 1] !== '--html') {
406
- entry = path.resolve(projectRoot, args[i]);
407
- break;
408
- }
409
- }
410
- if (!entry) entry = detectEntry(projectRoot);
411
-
412
- if (!entry || !fs.existsSync(entry)) {
413
- console.error(`\n ✗ Could not find entry file.`);
414
- console.error(` Provide one explicitly: zquery bundle scripts/app.js\n`);
415
- process.exit(1);
416
- }
417
-
418
- const outPath = option('out', 'o', null);
419
-
420
- // Auto-detect index.html by walking up from the entry file, then check cwd
421
- let htmlFile = option('html', null, null);
422
- let htmlAbs = htmlFile ? path.resolve(projectRoot, htmlFile) : null;
423
- if (!htmlFile) {
424
- const htmlCandidates = [];
425
- // Walk up from the entry file's directory to cwd looking for index.html
426
- let entryDir = path.dirname(entry);
427
- while (entryDir.length >= projectRoot.length) {
428
- htmlCandidates.push(path.join(entryDir, 'index.html'));
429
- const parent = path.dirname(entryDir);
430
- if (parent === entryDir) break;
431
- entryDir = parent;
432
- }
433
- // Also check cwd and public/
434
- htmlCandidates.push(path.join(projectRoot, 'index.html'));
435
- htmlCandidates.push(path.join(projectRoot, 'public/index.html'));
436
- for (const candidate of htmlCandidates) {
437
- if (fs.existsSync(candidate)) {
438
- htmlAbs = candidate;
439
- htmlFile = path.relative(projectRoot, candidate);
440
- break;
441
- }
442
- }
443
- }
444
-
445
- // Derive output directory:
446
- // -o flag → use that path
447
- // else → dist/ next to the detected HTML file (or cwd/dist/ as fallback)
448
- const entryRel = path.relative(projectRoot, entry);
449
- const entryName = path.basename(entry, '.js');
450
- let baseDistDir;
451
- if (outPath) {
452
- const resolved = path.resolve(projectRoot, outPath);
453
- if (outPath.endsWith('/') || outPath.endsWith('\\') || !path.extname(outPath) ||
454
- (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory())) {
455
- baseDistDir = resolved;
456
- } else {
457
- baseDistDir = path.dirname(resolved);
458
- }
459
- } else if (htmlAbs) {
460
- baseDistDir = path.join(path.dirname(htmlAbs), 'dist');
461
- } else {
462
- baseDistDir = path.join(projectRoot, 'dist');
463
- }
464
-
465
- // Two output sub-directories: server/ (with <base href="/">) and local/ (relative paths)
466
- const serverDir = path.join(baseDistDir, 'server');
467
- const localDir = path.join(baseDistDir, 'local');
468
-
469
- console.log(`\n zQuery App Bundler`);
470
- console.log(` Entry: ${entryRel}`);
471
- console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
472
- console.log(` Library: embedded`);
473
- console.log(` HTML: ${htmlFile || 'not found (no index.html detected)'}`);
474
- console.log('');
475
-
476
- function doBuild() {
477
- const start = Date.now();
478
-
479
- if (!fs.existsSync(serverDir)) fs.mkdirSync(serverDir, { recursive: true });
480
- if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
481
-
482
- // Walk the import graph
483
- const files = walkImportGraph(entry);
484
- console.log(` Resolved ${files.length} module(s):`);
485
- files.forEach(f => console.log(` • ${path.relative(projectRoot, f)}`));
486
-
487
- // Build concatenated source
488
- const sections = files.map(file => {
489
- let code = fs.readFileSync(file, 'utf-8');
490
- code = stripModuleSyntax(code);
491
- code = replaceImportMeta(code, file, projectRoot);
492
- const rel = path.relative(projectRoot, file);
493
- return `// --- ${rel} ${'—'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
494
- });
495
-
496
- // Embed zquery.min.js — always included
497
- let libSection = '';
498
- {
499
- const pkgSrcDir = path.join(__dirname, 'src');
500
- const pkgMinFile = path.join(__dirname, 'dist', 'zquery.min.js');
501
-
502
- // Always rebuild the library from source when running from the repo/package
503
- // so that dist/ stays current with the latest source changes.
504
- if (fs.existsSync(pkgSrcDir) && fs.existsSync(path.join(__dirname, 'index.js'))) {
505
- console.log(`\n Building library from source...`);
506
- const prevCwd = process.cwd();
507
- try {
508
- process.chdir(__dirname);
509
- buildLibrary();
510
- } finally {
511
- process.chdir(prevCwd);
512
- }
513
- }
514
-
515
- // Now look for the library in common locations
516
- const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
517
- const libCandidates = [
518
- // Prefer the freshly-built package dist
519
- path.join(__dirname, 'dist/zquery.min.js'),
520
- // Then check project-local locations
521
- htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
522
- htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
523
- path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
524
- path.join(projectRoot, 'vendor/zquery.min.js'),
525
- path.join(projectRoot, 'lib/zquery.min.js'),
526
- path.join(projectRoot, 'dist/zquery.min.js'),
527
- path.join(projectRoot, 'zquery.min.js'),
528
- ].filter(Boolean);
529
- const libPath = libCandidates.find(p => fs.existsSync(p));
530
-
531
- if (libPath) {
532
- const libBytes = fs.statSync(libPath).size;
533
- libSection = `// --- zquery.min.js (library) ${'—'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
534
- + `// --- Build-time metadata ————————————————————————————\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
535
- console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
536
- } else {
537
- console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
538
- console.warn(` Place zquery.min.js in scripts/vendor/, vendor/, lib/, or dist/`);
539
- }
540
- }
541
-
542
- const banner = `/**\n * App bundle — built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
543
-
544
- // Scan for external resources (pages, templateUrl, styleUrl) and inline them
545
- const inlineMap = collectInlineResources(files, projectRoot);
546
- let inlineSection = '';
547
- if (Object.keys(inlineMap).length > 0) {
548
- const entries = Object.entries(inlineMap).map(([key, content]) => {
549
- // Escape for embedding in a JS string literal
550
- const escaped = content
551
- .replace(/\\/g, '\\\\')
552
- .replace(/'/g, "\\'")
553
- .replace(/\n/g, '\\n')
554
- .replace(/\r/g, '');
555
- return ` '${key}': '${escaped}'`;
556
- });
557
- inlineSection = `// --- Inlined resources (file:// support) ${'—'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
558
- console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
559
- }
560
-
561
- const bundle = `${banner}\n(function() {\n 'use strict';\n\n${libSection}${inlineSection}${sections.join('\n\n')}\n\n})();\n`;
562
-
563
- // Content-hashed output filenames (z-<name>.<hash>.js)
564
- const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
565
- const bundleBase = `z-${entryName}.${contentHash}.js`;
566
- const minBase = `z-${entryName}.${contentHash}.min.js`;
567
-
568
- // Remove previous hashed builds from both output directories
569
- const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
570
- const cleanRe = new RegExp(`^z-${escName}\\.[a-f0-9]{8}\\.(?:min\\.)?js$`);
571
- for (const dir of [serverDir, localDir]) {
572
- if (fs.existsSync(dir)) {
573
- for (const f of fs.readdirSync(dir)) {
574
- if (cleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
575
- }
576
- }
577
- }
578
-
579
- // Write bundle into server/ (canonical), then copy to local/
580
- const bundleFile = path.join(serverDir, bundleBase);
581
- const minFile = path.join(serverDir, minBase);
582
- fs.writeFileSync(bundleFile, bundle, 'utf-8');
583
- fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
584
- fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
585
- fs.copyFileSync(minFile, path.join(localDir, minBase));
586
-
587
- console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
588
- console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
589
-
590
- // Rewrite index.html → two variants (server/ and local/)
591
- if (htmlFile) {
592
- const bundledFileSet = new Set(files);
593
- rewriteHtml(projectRoot, htmlFile, bundleFile, true, bundledFileSet, serverDir, localDir);
594
- }
595
-
596
- const elapsed = Date.now() - start;
597
- console.log(` Done in ${elapsed}ms\n`);
598
- }
599
-
600
- doBuild();
601
- }
602
-
603
- /**
604
- * Recursively copy a directory.
605
- */
606
- function copyDirSync(src, dest) {
607
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
608
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
609
- const srcPath = path.join(src, entry.name);
610
- const destPath = path.join(dest, entry.name);
611
- if (entry.isDirectory()) {
612
- copyDirSync(srcPath, destPath);
613
- } else {
614
- fs.copyFileSync(srcPath, destPath);
615
- }
616
- }
617
- }
618
-
619
- /**
620
- * Rewrite an HTML file to replace the module <script> with the bundle.
621
- * Copies all referenced assets into both server/ and local/ dist dirs,
622
- * then writes two index.html variants:
623
- * server/index.html — has <base href="/"> for SPA deep-route support
624
- * local/index.html — no <base>, relative paths for file:// access
625
- *
626
- * Both are fully static HTML with no dynamic loading.
627
- */
628
- function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
629
- const htmlPath = path.resolve(projectRoot, htmlRelPath);
630
- if (!fs.existsSync(htmlPath)) {
631
- console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
632
- return;
633
- }
634
-
635
- const htmlDir = path.dirname(htmlPath);
636
- let html = fs.readFileSync(htmlPath, 'utf-8');
637
-
638
- // Collect all asset references from the HTML (src=, href= on link/script/img)
639
- const assetRe = /(?:<(?:link|script|img)[^>]*?\s(?:src|href)\s*=\s*["'])([^"']+)["']/gi;
640
- const assets = new Set();
641
- let m;
642
- while ((m = assetRe.exec(html)) !== null) {
643
- const ref = m[1];
644
- if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:') || ref.startsWith('#')) continue;
645
- const refAbs = path.resolve(htmlDir, ref);
646
- if (bundledFiles && bundledFiles.has(refAbs)) continue;
647
- if (includeLib && /zquery(?:\.min)?\.js$/i.test(ref)) continue;
648
- assets.add(ref);
649
- }
650
-
651
- // Copy each referenced asset into BOTH dist dirs, preserving directory structure
652
- let copiedCount = 0;
653
- for (const asset of assets) {
654
- const srcFile = path.resolve(htmlDir, asset);
655
- if (!fs.existsSync(srcFile)) continue;
656
-
657
- for (const distDir of [serverDir, localDir]) {
658
- const destFile = path.join(distDir, asset);
659
- const destDir = path.dirname(destFile);
660
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
661
- fs.copyFileSync(srcFile, destFile);
662
- }
663
- copiedCount++;
664
- }
665
-
666
- // Also copy any CSS-referenced assets (fonts, images in url() etc.)
667
- for (const asset of assets) {
668
- const srcFile = path.resolve(htmlDir, asset);
669
- if (!fs.existsSync(srcFile) || !asset.endsWith('.css')) continue;
670
-
671
- const cssContent = fs.readFileSync(srcFile, 'utf-8');
672
- const urlRe = /url\(\s*["']?([^"')]+?)["']?\s*\)/g;
673
- let cm;
674
- while ((cm = urlRe.exec(cssContent)) !== null) {
675
- const ref = cm[1];
676
- if (ref.startsWith('data:') || ref.startsWith('http') || ref.startsWith('//')) continue;
677
- const cssSrcDir = path.dirname(srcFile);
678
- const assetSrc = path.resolve(cssSrcDir, ref);
679
- if (!fs.existsSync(assetSrc)) continue;
680
-
681
- for (const distDir of [serverDir, localDir]) {
682
- const cssDestDir = path.dirname(path.join(distDir, asset));
683
- const assetDest = path.resolve(cssDestDir, ref);
684
- if (!fs.existsSync(assetDest)) {
685
- const dir = path.dirname(assetDest);
686
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
687
- fs.copyFileSync(assetSrc, assetDest);
688
- }
689
- }
690
- copiedCount++;
691
- }
692
- }
693
-
694
- // Make the bundle filename relative (same name in both dirs)
695
- const bundleRel = path.relative(serverDir, bundleFile).replace(/\\/g, '/');
696
-
697
- // Replace <script type="module" src="..."> with the bundle (defer)
698
- html = html.replace(
699
- /<script\s+type\s*=\s*["']module["']\s+src\s*=\s*["'][^"']+["']\s*>\s*<\/script>/gi,
700
- `<script defer src="${bundleRel}"></script>`
701
- );
702
-
703
- // If library is embedded, remove the standalone zquery script tag
704
- if (includeLib) {
705
- html = html.replace(
706
- /\s*<script\s+src\s*=\s*["'][^"']*zquery(?:\.min)?\.js["']\s*>\s*<\/script>/gi,
707
- ''
708
- );
709
- }
710
-
711
- // ── server/index.html ──
712
- // Keep <base href="/"> as-is — the preload scanner sees it, all resources
713
- // resolve from root, deep-route refreshes work perfectly.
714
- const serverHtml = html;
715
-
716
- // ── local/index.html ──
717
- // Remove <base href="/"> so relative paths resolve from the HTML file's
718
- // directory — correct for file:// with zero console errors.
719
- const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
720
-
721
- // Write both
722
- const htmlName = path.basename(htmlRelPath);
723
- fs.writeFileSync(path.join(serverDir, htmlName), serverHtml, 'utf-8');
724
- fs.writeFileSync(path.join(localDir, htmlName), localHtml, 'utf-8');
725
- console.log(` ✓ server/${htmlName} (with <base href="/">)`);
726
- console.log(` ✓ local/${htmlName} (relative paths, file:// ready)`);
727
- console.log(` ✓ Copied ${copiedCount} asset(s) into both dist dirs`);
728
- }
729
-
730
-
731
- // ---------------------------------------------------------------------------
732
- // "dev" command — development server with live-reload
733
- // ---------------------------------------------------------------------------
734
-
735
- /**
736
- * SSE live-reload client script injected into served HTML.
737
- * Connects to /__zq_reload, reloads on 'reload' events,
738
- * and hot-swaps CSS on 'css' events without a full reload.
739
- */
740
- const LIVE_RELOAD_SNIPPET = `<script>
741
- (function(){
742
- var es, timer;
743
- function connect(){
744
- es = new EventSource('/__zq_reload');
745
- es.addEventListener('reload', function(){ location.reload(); });
746
- es.addEventListener('css', function(e){
747
- var sheets = document.querySelectorAll('link[rel="stylesheet"]');
748
- sheets.forEach(function(l){
749
- var href = l.getAttribute('href');
750
- if(!href) return;
751
- var sep = href.indexOf('?') >= 0 ? '&' : '?';
752
- l.setAttribute('href', href.replace(/[?&]_zqr=\\d+/, '') + sep + '_zqr=' + Date.now());
753
- });
754
- });
755
- es.onerror = function(){
756
- es.close();
757
- clearTimeout(timer);
758
- timer = setTimeout(connect, 2000);
759
- };
760
- }
761
- connect();
762
- })();
763
- </script>`;
764
-
765
- function devServer() {
766
- let zeroHttp;
767
- try {
768
- zeroHttp = require('zero-http');
769
- } catch (_) {
770
- console.error(`\n ✗ zero-http is required for the dev server.`);
771
- console.error(` Install it: npm install zero-http --save-dev\n`);
772
- process.exit(1);
773
- }
774
-
775
- const { createApp, static: serveStatic } = zeroHttp;
776
-
777
- // Determine the project root to serve
778
- let root = null;
779
- for (let i = 1; i < args.length; i++) {
780
- if (!args[i].startsWith('-') && args[i - 1] !== '-p' && args[i - 1] !== '--port') {
781
- root = path.resolve(process.cwd(), args[i]);
782
- break;
783
- }
784
- }
785
- if (!root) {
786
- // Auto-detect: look for index.html in cwd or common sub-dirs
787
- const candidates = [
788
- process.cwd(),
789
- path.join(process.cwd(), 'public'),
790
- path.join(process.cwd(), 'src'),
791
- ];
792
- for (const c of candidates) {
793
- if (fs.existsSync(path.join(c, 'index.html'))) { root = c; break; }
794
- }
795
- if (!root) root = process.cwd();
796
- }
797
-
798
- const PORT = parseInt(option('port', 'p', '3100'));
799
-
800
- // SSE clients for live-reload
801
- const sseClients = new Set();
802
-
803
- const app = createApp();
804
-
805
- // SSE endpoint — clients connect here for reload notifications
806
- app.get('/__zq_reload', (req, res) => {
807
- const sse = res.sse({ keepAlive: 30000, keepAliveComment: 'ping' });
808
- sseClients.add(sse);
809
- sse.on('close', () => sseClients.delete(sse));
810
- });
811
-
812
- // Auto-resolve zquery.min.js — serve the freshest version regardless of
813
- // what's on disk in the project. Priority:
814
- // 1. Package dist/ (when running from the repo after `npm run build`)
815
- // 2. node_modules/zero-query/dist/ (when installed as a dependency)
816
- // 3. Fall through to static serving (vendor copy on disk)
817
- // Registered as middleware so it runs BEFORE serveStatic.
818
- // Use --no-intercept to skip this and serve the on-disk vendor copy instead.
819
- const noIntercept = flag('no-intercept');
820
- app.use((req, res, next) => {
821
- if (noIntercept) return next();
822
- const basename = path.basename(req.url.split('?')[0]).toLowerCase();
823
- if (basename !== 'zquery.min.js') return next();
824
-
825
- const candidates = [
826
- path.join(__dirname, 'dist', 'zquery.min.js'), // package repo
827
- path.join(root, 'node_modules', 'zero-query', 'dist', 'zquery.min.js'), // npm dep
828
- ];
829
- for (const p of candidates) {
830
- if (fs.existsSync(p)) {
831
- res.set('Content-Type', 'application/javascript; charset=utf-8');
832
- res.set('Cache-Control', 'no-cache');
833
- res.send(fs.readFileSync(p, 'utf-8'));
834
- return;
835
- }
836
- }
837
- next(); // fall through to static / 404
838
- });
839
-
840
- // Static file serving
841
- app.use(serveStatic(root, { index: false, dotfiles: 'ignore' }));
842
-
843
- // SPA fallback — inject live-reload snippet into HTML
844
- app.get('*', (req, res) => {
845
- if (path.extname(req.url) && path.extname(req.url) !== '.html') {
846
- res.status(404).send('Not Found');
847
- return;
848
- }
849
- const indexPath = path.join(root, 'index.html');
850
- if (!fs.existsSync(indexPath)) {
851
- res.status(404).send('index.html not found');
852
- return;
853
- }
854
- let html = fs.readFileSync(indexPath, 'utf-8');
855
- // Inject live-reload snippet before </body> or at end
856
- if (html.includes('</body>')) {
857
- html = html.replace('</body>', LIVE_RELOAD_SNIPPET + '\n</body>');
858
- } else {
859
- html += LIVE_RELOAD_SNIPPET;
860
- }
861
- res.html(html);
862
- });
863
-
864
- // Broadcast a reload event to all connected SSE clients
865
- function broadcast(eventType, data) {
866
- for (const sse of sseClients) {
867
- try { sse.event(eventType, data || ''); } catch (_) { sseClients.delete(sse); }
868
- }
869
- }
870
-
871
- // File watcher — watch the project root for changes
872
- // All file types trigger a reload; CSS gets hot-swapped, everything else
873
- // triggers a full page reload. Only ignored directories are skipped.
874
- const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.cache']);
875
- let debounceTimer;
876
-
877
- function shouldWatch(filename) {
878
- if (!filename) return false;
879
- // Ignore dotfiles (e.g. .DS_Store)
880
- if (filename.startsWith('.')) return false;
881
- return true;
882
- }
883
-
884
- function isIgnored(filepath) {
885
- const parts = filepath.split(path.sep);
886
- return parts.some(p => IGNORE_DIRS.has(p));
887
- }
888
-
889
- // Collect directories to watch (walk root, skip ignored)
890
- function collectWatchDirs(dir) {
891
- const dirs = [dir];
892
- try {
893
- const entries = fs.readdirSync(dir, { withFileTypes: true });
894
- for (const entry of entries) {
895
- if (!entry.isDirectory()) continue;
896
- if (IGNORE_DIRS.has(entry.name)) continue;
897
- const sub = path.join(dir, entry.name);
898
- dirs.push(...collectWatchDirs(sub));
899
- }
900
- } catch (_) {}
901
- return dirs;
902
- }
903
-
904
- const watchDirs = collectWatchDirs(root);
905
- const watchers = [];
906
-
907
- for (const dir of watchDirs) {
908
- try {
909
- const watcher = fs.watch(dir, (eventType, filename) => {
910
- if (!shouldWatch(filename)) return;
911
- const fullPath = path.join(dir, filename || '');
912
- if (isIgnored(fullPath)) return;
913
-
914
- clearTimeout(debounceTimer);
915
- debounceTimer = setTimeout(() => {
916
- const rel = path.relative(root, fullPath).replace(/\\/g, '/');
917
- const ext = path.extname(filename).toLowerCase();
918
- const now = new Date().toLocaleTimeString();
919
-
920
- if (ext === '.css') {
921
- console.log(` ${now} \x1b[35m css \x1b[0m ${rel}`);
922
- broadcast('css', rel);
923
- } else {
924
- console.log(` ${now} \x1b[36m reload \x1b[0m ${rel}`);
925
- broadcast('reload', rel);
926
- }
927
- }, 100);
928
- });
929
- watchers.push(watcher);
930
- } catch (_) {}
931
- }
932
-
933
- app.listen(PORT, () => {
934
- console.log(`\n \x1b[1mzQuery Dev Server\x1b[0m`);
935
- console.log(` \x1b[2m${'─'.repeat(40)}\x1b[0m`);
936
- console.log(` Local: \x1b[36mhttp://localhost:${PORT}/\x1b[0m`);
937
- console.log(` Root: ${path.relative(process.cwd(), root) || '.'}`);
938
- console.log(` Live Reload: \x1b[32menabled\x1b[0m (SSE)`);
939
- if (noIntercept) console.log(` Intercept: \x1b[33mdisabled\x1b[0m (--no-intercept)`);
940
- console.log(` Watching: all files in ${watchDirs.length} director${watchDirs.length === 1 ? 'y' : 'ies'}`);
941
- console.log(` \x1b[2m${'─'.repeat(40)}\x1b[0m`);
942
- console.log(` Press Ctrl+C to stop\n`);
943
- });
944
-
945
- // Graceful shutdown
946
- process.on('SIGINT', () => {
947
- console.log('\n Shutting down...');
948
- watchers.forEach(w => w.close());
949
- for (const sse of sseClients) { try { sse.close(); } catch (_) {} }
950
- app.close(() => process.exit(0));
951
- setTimeout(() => process.exit(0), 1000);
952
- });
953
- }
954
-
955
-
956
- // ---------------------------------------------------------------------------
957
- // Create — scaffold a new zQuery project
958
- // ---------------------------------------------------------------------------
959
-
960
- function createProject() {
961
- const target = args[1] ? path.resolve(args[1]) : process.cwd();
962
- const name = path.basename(target);
963
-
964
- // Guard: refuse to overwrite existing files
965
- const conflicts = ['index.html', 'scripts'].filter(f =>
966
- fs.existsSync(path.join(target, f))
967
- );
968
- if (conflicts.length) {
969
- console.error(`\n ✗ Directory already contains: ${conflicts.join(', ')}`);
970
- console.error(` Aborting to avoid overwriting existing files.\n`);
971
- process.exit(1);
972
- }
973
-
974
- console.log(`\n zQuery — Create Project\n`);
975
- console.log(` Scaffolding into ${target}\n`);
976
-
977
- // ---- templates ----
978
-
979
- const indexHTML = `<!DOCTYPE html>
980
- <html lang="en">
981
- <head>
982
- <meta charset="UTF-8">
983
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
984
- <title>${name}</title>
985
- <link rel="stylesheet" href="styles/styles.css">
986
- <script src="scripts/vendor/zQuery.min.js"></script>
987
- <script type="module" src="scripts/app.js"></script>
988
- </head>
989
- <body>
990
- <nav>
991
- <a z-link="/">Home</a>
992
- <a z-link="/about">About</a>
993
- </nav>
994
- <div id="app"></div>
995
- </body>
996
- </html>`;
997
-
998
- const appJS = `// scripts/app.js — entry point
999
- import './components/home.js';
1000
- import './components/about.js';
1001
- import { routes } from './routes.js';
1002
-
1003
- $.router({ el: '#app', routes, fallback: 'not-found' });
1004
-
1005
- $.ready(() => {
1006
- console.log('zQuery v' + $.version + ' loaded');
1007
- });`;
1008
-
1009
- const routesJS = `// scripts/routes.js
1010
- export const routes = [
1011
- { path: '/', component: 'home-page' },
1012
- { path: '/about', component: 'about-page' },
1013
- ];`;
1014
-
1015
- const homeJS = `// scripts/components/home.js
1016
- $.component('home-page', {
1017
- state: () => ({ count: 0 }),
1018
-
1019
- increment() { this.state.count++; },
1020
-
1021
- render() {
1022
- return \`
1023
- <h1>Home</h1>
1024
- <p>Count: \${this.state.count}</p>
1025
- <button @click="increment">+1</button>
1026
- \`;
1027
- }
1028
- });`;
1029
-
1030
- const aboutJS = `// scripts/components/about.js
1031
- $.component('about-page', {
1032
- render() {
1033
- return \`
1034
- <h1>About</h1>
1035
- <p>Built with <strong>zQuery</strong> — a lightweight, zero-dependency frontend library.</p>
1036
- \`;
1037
- }
1038
- });`;
1039
-
1040
- const stylesCSS = `/* styles/styles.css */
1041
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1042
-
1043
- body {
1044
- font-family: system-ui, -apple-system, sans-serif;
1045
- line-height: 1.6;
1046
- color: #e6edf3;
1047
- background: #0d1117;
1048
- padding: 2rem;
1049
- }
1050
-
1051
- nav {
1052
- display: flex;
1053
- gap: 1rem;
1054
- margin-bottom: 2rem;
1055
- padding-bottom: 1rem;
1056
- border-bottom: 1px solid #30363d;
1057
- }
1058
-
1059
- nav a {
1060
- color: #58a6ff;
1061
- text-decoration: none;
1062
- font-weight: 500;
1063
- }
1064
-
1065
- nav a:hover { text-decoration: underline; }
1066
-
1067
- h1 { margin-bottom: 0.5rem; }
1068
-
1069
- button {
1070
- margin-top: 0.75rem;
1071
- padding: 0.5rem 1.25rem;
1072
- background: #238636;
1073
- color: #fff;
1074
- border: none;
1075
- border-radius: 6px;
1076
- cursor: pointer;
1077
- font-size: 0.95rem;
1078
- }
1079
-
1080
- button:hover { background: #2ea043; }`;
1081
-
1082
- // ---- write files ----
1083
-
1084
- const files = {
1085
- 'index.html': indexHTML,
1086
- 'scripts/app.js': appJS,
1087
- 'scripts/routes.js': routesJS,
1088
- 'scripts/components/home.js': homeJS,
1089
- 'scripts/components/about.js': aboutJS,
1090
- 'styles/styles.css': stylesCSS,
1091
- };
1092
-
1093
- for (const [rel, content] of Object.entries(files)) {
1094
- const abs = path.join(target, rel);
1095
- fs.mkdirSync(path.dirname(abs), { recursive: true });
1096
- fs.writeFileSync(abs, content, 'utf-8');
1097
- console.log(` ✓ ${rel}`);
1098
- }
1099
-
1100
- console.log(`
1101
- Done! Next steps:
1102
-
1103
- ${target !== process.cwd() ? `cd ${args[1]}\n ` : ''}npx zquery dev
1104
- `);
1105
- }
1106
-
1107
-
1108
- // ---------------------------------------------------------------------------
1109
- // Help
1110
- // ---------------------------------------------------------------------------
1111
-
1112
- function showHelp() {
1113
- console.log(`
1114
- zQuery CLI — create, dev, bundle & build
1115
-
1116
- COMMANDS
1117
-
1118
- create [dir] Scaffold a new zQuery project
1119
- Creates index.html, scripts/, styles/ in the target directory
1120
- (defaults to the current directory)
1121
-
1122
- dev [root] Start a dev server with live-reload
1123
- --port, -p <number> Port number (default: 3100)
1124
- --no-intercept Disable auto-resolution of zquery.min.js
1125
- (serve the on-disk vendor copy instead)
1126
-
1127
- bundle [entry] Bundle app ES modules into a single file
1128
- --out, -o <path> Output directory (default: dist/ next to index.html)
1129
- --html <file> Use a specific HTML file (default: auto-detected)
1130
-
1131
- build Build the zQuery library → dist/
1132
- (must be run from the project root where src/ lives)
1133
-
1134
- SMART DEFAULTS
1135
-
1136
- The bundler works with zero flags for typical projects:
1137
- • Entry is auto-detected from index.html <script type="module" src="...">
1138
- • zquery.min.js is always embedded (auto-built from source if not found)
1139
- • index.html is rewritten for both server and local (file://) use
1140
- • Output goes to dist/server/ and dist/local/ next to the detected index.html
1141
-
1142
- OUTPUT
1143
-
1144
- The bundler produces two self-contained sub-directories:
1145
-
1146
- dist/server/ deploy to your web server
1147
- index.html has <base href="/"> for SPA deep routes
1148
- z-<entry>.<hash>.js readable bundle
1149
- z-<entry>.<hash>.min.js minified bundle
1150
-
1151
- dist/local/ open from disk (file://)
1152
- index.html relative paths, no <base> tag
1153
- z-<entry>.<hash>.js same bundle
1154
-
1155
- Previous hashed builds are automatically cleaned on each rebuild.
1156
-
1157
- DEVELOPMENT
1158
-
1159
- zquery dev start a dev server with live-reload (port 3100)
1160
- zquery dev --port 8080 custom port
1161
-
1162
- EXAMPLES
1163
-
1164
- # Scaffold a new project and start developing
1165
- zquery create my-app && cd my-app && zquery dev
1166
-
1167
- # Start dev server with live-reload
1168
- cd my-app && zquery dev
1169
-
1170
- # Build the library only
1171
- zquery build
1172
-
1173
- # Bundle an app — auto-detects everything
1174
- cd my-app && zquery bundle
1175
-
1176
- # Point to an entry from a parent directory
1177
- zquery bundle path/to/scripts/app.js
1178
-
1179
- # Custom output directory
1180
- zquery bundle -o build/
1181
-
1182
- The bundler walks the ES module import graph starting from the entry
1183
- file, topologically sorts dependencies, strips import/export syntax,
1184
- and concatenates everything into a single IIFE with content-hashed
1185
- filenames for cache-busting. No dependencies needed — just Node.js.
1186
- `);
1187
- }
1188
-
1189
-
1190
- // ---------------------------------------------------------------------------
1191
- // Main
1192
- // ---------------------------------------------------------------------------
1193
-
1194
- if (!command || command === '--help' || command === '-h' || command === 'help') {
1195
- showHelp();
1196
- } else if (command === 'create') {
1197
- createProject();
1198
- } else if (command === 'build') {
1199
- console.log('\n zQuery Library Build\n');
1200
- buildLibrary();
1201
- } else if (command === 'bundle') {
1202
- bundleApp();
1203
- } else if (command === 'dev') {
1204
- devServer();
1205
- } else {
1206
- console.error(`\n Unknown command: ${command}\n Run "zquery --help" for usage.\n`);
1207
- process.exit(1);
1208
- }