zero-query 0.1.2 → 0.2.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/README.md +132 -16
- package/cli.js +833 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2605 -0
- package/dist/zquery.min.js +17 -0
- package/index.js +1 -0
- package/package.json +10 -3
- package/src/component.js +12 -0
- package/src/router.js +40 -6
package/cli.js
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* zQuery CLI
|
|
5
|
+
*
|
|
6
|
+
* Zero-dependency command-line tool for building the zQuery library
|
|
7
|
+
* and bundling zQuery-based applications into a single file.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* zquery build Build the zQuery library (dist/)
|
|
11
|
+
*
|
|
12
|
+
* zquery bundle [entry] Bundle an app's ES modules into one file
|
|
13
|
+
* zquery bundle scripts/app.js Specify entry explicitly
|
|
14
|
+
* zquery bundle -o build/ Custom output directory
|
|
15
|
+
* zquery bundle --html other.html Use a specific HTML file instead of auto-detected
|
|
16
|
+
* zquery bundle --watch Watch & rebuild on changes
|
|
17
|
+
*
|
|
18
|
+
* Smart defaults (no flags needed for typical projects):
|
|
19
|
+
* - Entry is auto-detected from index.html's <script type="module" src="...">
|
|
20
|
+
* - zquery.min.js is always embedded (auto-built if not found)
|
|
21
|
+
* - index.html is always rewritten and assets are copied
|
|
22
|
+
* - Output goes to dist/ next to the detected index.html
|
|
23
|
+
*
|
|
24
|
+
* Examples:
|
|
25
|
+
* cd my-app && npx zero-query bundle # just works!
|
|
26
|
+
* npx zero-query bundle path/to/scripts/app.js # works from anywhere
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const crypto = require('crypto');
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// CLI argument parsing
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const command = args[0];
|
|
39
|
+
|
|
40
|
+
function flag(name, short) {
|
|
41
|
+
const i = args.indexOf(`--${name}`);
|
|
42
|
+
const j = short ? args.indexOf(`-${short}`) : -1;
|
|
43
|
+
return i !== -1 || j !== -1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function option(name, short, fallback) {
|
|
47
|
+
let i = args.indexOf(`--${name}`);
|
|
48
|
+
if (i === -1 && short) i = args.indexOf(`-${short}`);
|
|
49
|
+
if (i !== -1 && i + 1 < args.length) return args[i + 1];
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Shared utilities
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Context-aware comment stripper — skips strings, templates, regex.
|
|
59
|
+
* Reused from build.js.
|
|
60
|
+
*/
|
|
61
|
+
function stripComments(code) {
|
|
62
|
+
let out = '';
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < code.length) {
|
|
65
|
+
const ch = code[i];
|
|
66
|
+
const next = code[i + 1];
|
|
67
|
+
|
|
68
|
+
// String literals
|
|
69
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
70
|
+
const quote = ch;
|
|
71
|
+
out += ch; i++;
|
|
72
|
+
while (i < code.length) {
|
|
73
|
+
if (code[i] === '\\') { out += code[i] + (code[i + 1] || ''); i += 2; continue; }
|
|
74
|
+
out += code[i];
|
|
75
|
+
if (code[i] === quote) { i++; break; }
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Block comment
|
|
82
|
+
if (ch === '/' && next === '*') {
|
|
83
|
+
i += 2;
|
|
84
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) i++;
|
|
85
|
+
i += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Line comment
|
|
90
|
+
if (ch === '/' && next === '/') {
|
|
91
|
+
i += 2;
|
|
92
|
+
while (i < code.length && code[i] !== '\n') i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Regex literal
|
|
97
|
+
if (ch === '/') {
|
|
98
|
+
const before = out.replace(/\s+$/, '');
|
|
99
|
+
const last = before[before.length - 1];
|
|
100
|
+
const isRegexCtx = !last || '=({[,;:!&|?~+-*/%^>'.includes(last)
|
|
101
|
+
|| before.endsWith('return') || before.endsWith('typeof')
|
|
102
|
+
|| before.endsWith('case') || before.endsWith('in')
|
|
103
|
+
|| before.endsWith('delete') || before.endsWith('void')
|
|
104
|
+
|| before.endsWith('throw') || before.endsWith('new');
|
|
105
|
+
if (isRegexCtx) {
|
|
106
|
+
out += ch; i++;
|
|
107
|
+
let inCharClass = false;
|
|
108
|
+
while (i < code.length) {
|
|
109
|
+
const rc = code[i];
|
|
110
|
+
if (rc === '\\') { out += rc + (code[i + 1] || ''); i += 2; continue; }
|
|
111
|
+
if (rc === '[') inCharClass = true;
|
|
112
|
+
if (rc === ']') inCharClass = false;
|
|
113
|
+
out += rc; i++;
|
|
114
|
+
if (rc === '/' && !inCharClass) {
|
|
115
|
+
while (i < code.length && /[gimsuy]/.test(code[i])) { out += code[i]; i++; }
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
out += ch; i++;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Quick minification (same approach as build.js). */
|
|
129
|
+
function minify(code, banner) {
|
|
130
|
+
const body = stripComments(code.replace(banner, ''))
|
|
131
|
+
.replace(/^\s*\n/gm, '')
|
|
132
|
+
.replace(/\n\s+/g, '\n')
|
|
133
|
+
.replace(/\s{2,}/g, ' ');
|
|
134
|
+
return banner + '\n' + body;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sizeKB(buf) {
|
|
138
|
+
return (buf.length / 1024).toFixed(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// "build" command — library build (mirrors build.js)
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
function buildLibrary() {
|
|
146
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
|
147
|
+
const VERSION = pkg.version;
|
|
148
|
+
|
|
149
|
+
const modules = [
|
|
150
|
+
'src/reactive.js', 'src/core.js', 'src/component.js',
|
|
151
|
+
'src/router.js', 'src/store.js', 'src/http.js', 'src/utils.js',
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const DIST = path.join(process.cwd(), 'dist');
|
|
155
|
+
const OUT_FILE = path.join(DIST, 'zquery.js');
|
|
156
|
+
const MIN_FILE = path.join(DIST, 'zquery.min.js');
|
|
157
|
+
|
|
158
|
+
const start = Date.now();
|
|
159
|
+
if (!fs.existsSync(DIST)) fs.mkdirSync(DIST, { recursive: true });
|
|
160
|
+
|
|
161
|
+
const parts = modules.map(file => {
|
|
162
|
+
let code = fs.readFileSync(path.join(process.cwd(), file), 'utf-8');
|
|
163
|
+
code = code.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
164
|
+
code = code.replace(/^export\s+(default\s+)?/gm, '');
|
|
165
|
+
code = code.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
|
|
166
|
+
return `// --- ${file} ${'—'.repeat(60 - file.length)}\n${code.trim()}`;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
let indexCode = fs.readFileSync(path.join(process.cwd(), 'index.js'), 'utf-8');
|
|
170
|
+
indexCode = indexCode.replace(/^import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
171
|
+
indexCode = indexCode.replace(/^export\s*\{[\s\S]*?\};\s*$/gm, '');
|
|
172
|
+
indexCode = indexCode.replace(/^export\s+(default\s+)?/gm, '');
|
|
173
|
+
|
|
174
|
+
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 */`;
|
|
175
|
+
|
|
176
|
+
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`;
|
|
177
|
+
|
|
178
|
+
fs.writeFileSync(OUT_FILE, bundle, 'utf-8');
|
|
179
|
+
fs.writeFileSync(MIN_FILE, minify(bundle, banner), 'utf-8');
|
|
180
|
+
|
|
181
|
+
const elapsed = Date.now() - start;
|
|
182
|
+
console.log(` ✓ dist/zquery.js (${sizeKB(fs.readFileSync(OUT_FILE))} KB)`);
|
|
183
|
+
console.log(` ✓ dist/zquery.min.js (${sizeKB(fs.readFileSync(MIN_FILE))} KB)`);
|
|
184
|
+
console.log(` Done in ${elapsed}ms\n`);
|
|
185
|
+
|
|
186
|
+
return { DIST, OUT_FILE, MIN_FILE };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// "bundle" command — app bundler
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolve an import specifier relative to the importing file.
|
|
196
|
+
*/
|
|
197
|
+
function resolveImport(specifier, fromFile) {
|
|
198
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null; // bare specifier
|
|
199
|
+
let resolved = path.resolve(path.dirname(fromFile), specifier);
|
|
200
|
+
// If no extension and a .js file exists, add it
|
|
201
|
+
if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
|
|
202
|
+
resolved += '.js';
|
|
203
|
+
}
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract import specifiers from a source file.
|
|
209
|
+
* Handles: import 'x', import x from 'x', import { a } from 'x', import * as x from 'x'
|
|
210
|
+
* (including multi-line destructured imports)
|
|
211
|
+
*/
|
|
212
|
+
function extractImports(code) {
|
|
213
|
+
const specifiers = [];
|
|
214
|
+
let m;
|
|
215
|
+
// Pattern 1: import ... from 'specifier' (works for single & multi-line)
|
|
216
|
+
const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
217
|
+
while ((m = fromRe.exec(code)) !== null) {
|
|
218
|
+
specifiers.push(m[1]);
|
|
219
|
+
}
|
|
220
|
+
// Pattern 2: side-effect imports — import './foo.js';
|
|
221
|
+
const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
|
|
222
|
+
while ((m = sideRe.exec(code)) !== null) {
|
|
223
|
+
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
224
|
+
}
|
|
225
|
+
return specifiers;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Walk the import graph starting from `entry`, return files in dependency
|
|
230
|
+
* order (leaves first — topological sort).
|
|
231
|
+
*/
|
|
232
|
+
function walkImportGraph(entry) {
|
|
233
|
+
const visited = new Set();
|
|
234
|
+
const order = [];
|
|
235
|
+
|
|
236
|
+
function visit(file) {
|
|
237
|
+
const abs = path.resolve(file);
|
|
238
|
+
if (visited.has(abs)) return;
|
|
239
|
+
visited.add(abs);
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(abs)) {
|
|
242
|
+
console.warn(` ⚠ Missing file: ${abs}`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const code = fs.readFileSync(abs, 'utf-8');
|
|
247
|
+
const imports = extractImports(code);
|
|
248
|
+
|
|
249
|
+
for (const spec of imports) {
|
|
250
|
+
const resolved = resolveImport(spec, abs);
|
|
251
|
+
if (resolved) visit(resolved);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
order.push(abs);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
visit(entry);
|
|
258
|
+
return order;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Strip ES module import/export syntax from code, keeping declarations.
|
|
263
|
+
*/
|
|
264
|
+
function stripModuleSyntax(code) {
|
|
265
|
+
// Remove import lines (single-line and multi-line from ... )
|
|
266
|
+
code = code.replace(/^\s*import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
267
|
+
// Remove side-effect imports import './foo.js';
|
|
268
|
+
code = code.replace(/^\s*import\s+['"].*?['"];?\s*$/gm, '');
|
|
269
|
+
// Remove export default but keep the expression
|
|
270
|
+
code = code.replace(/^(\s*)export\s+default\s+/gm, '$1');
|
|
271
|
+
// Remove export keyword but keep declarations
|
|
272
|
+
code = code.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/gm, '$1$2 ');
|
|
273
|
+
// Remove standalone export { ... } blocks
|
|
274
|
+
code = code.replace(/^\s*export\s*\{[\s\S]*?\};?\s*$/gm, '');
|
|
275
|
+
return code;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Replace `import.meta.url` with a runtime equivalent.
|
|
280
|
+
* In a non-module <script>, import.meta doesn't exist, so we substitute
|
|
281
|
+
* document.currentScript.src (set at load time) relative to the original
|
|
282
|
+
* file's path inside the project.
|
|
283
|
+
*/
|
|
284
|
+
function replaceImportMeta(code, filePath, projectRoot) {
|
|
285
|
+
if (!code.includes('import.meta')) return code;
|
|
286
|
+
// Compute the web-relative path of this file from the project root
|
|
287
|
+
const rel = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
288
|
+
// Replace import.meta.url with a constructed URL based on the page origin
|
|
289
|
+
code = code.replace(
|
|
290
|
+
/import\.meta\.url/g,
|
|
291
|
+
`(new URL('${rel}', document.baseURI).href)`
|
|
292
|
+
);
|
|
293
|
+
return code;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Scan bundled source files for external resource references
|
|
298
|
+
* (pages config, templateUrl, styleUrl) and read those files so they
|
|
299
|
+
* can be inlined into the bundle for file:// support.
|
|
300
|
+
*
|
|
301
|
+
* Returns a map of { relativePath: fileContent }.
|
|
302
|
+
*/
|
|
303
|
+
function collectInlineResources(files, projectRoot) {
|
|
304
|
+
const inlineMap = {};
|
|
305
|
+
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
308
|
+
const fileDir = path.dirname(file);
|
|
309
|
+
|
|
310
|
+
// Detect `pages:` config — look for dir and items
|
|
311
|
+
const pagesMatch = code.match(/pages\s*:\s*\{[^}]*dir\s*:\s*['"]([^'"]+)['"]/s);
|
|
312
|
+
if (pagesMatch) {
|
|
313
|
+
const pagesDir = pagesMatch[1];
|
|
314
|
+
const ext = (code.match(/pages\s*:\s*\{[^}]*ext\s*:\s*['"]([^'"]+)['"]/s) || [])[1] || '.html';
|
|
315
|
+
|
|
316
|
+
// Extract item IDs from the items array
|
|
317
|
+
const itemsMatch = code.match(/items\s*:\s*\[([\s\S]*?)\]/);
|
|
318
|
+
if (itemsMatch) {
|
|
319
|
+
const itemsBlock = itemsMatch[1];
|
|
320
|
+
const ids = [];
|
|
321
|
+
// Match string items: 'getting-started'
|
|
322
|
+
let m;
|
|
323
|
+
const strRe = /['"]([^'"]+)['"]/g;
|
|
324
|
+
const objIdRe = /id\s*:\s*['"]([^'"]+)['"]/g;
|
|
325
|
+
// Collect all quoted strings that look like page IDs
|
|
326
|
+
while ((m = strRe.exec(itemsBlock)) !== null) {
|
|
327
|
+
// Skip labels (preceded by "label:")
|
|
328
|
+
const before = itemsBlock.substring(Math.max(0, m.index - 20), m.index);
|
|
329
|
+
if (/label\s*:\s*$/.test(before)) continue;
|
|
330
|
+
ids.push(m[1]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Read each page file
|
|
334
|
+
const absPagesDir = path.join(fileDir, pagesDir);
|
|
335
|
+
for (const id of ids) {
|
|
336
|
+
const pagePath = path.join(absPagesDir, id + ext);
|
|
337
|
+
if (fs.existsSync(pagePath)) {
|
|
338
|
+
const relKey = path.relative(projectRoot, pagePath).replace(/\\/g, '/');
|
|
339
|
+
inlineMap[relKey] = fs.readFileSync(pagePath, 'utf-8');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Detect `styleUrl:` — single string
|
|
346
|
+
const styleMatch = code.match(/styleUrl\s*:\s*['"]([^'"]+)['"]/);
|
|
347
|
+
if (styleMatch) {
|
|
348
|
+
const stylePath = path.join(fileDir, styleMatch[1]);
|
|
349
|
+
if (fs.existsSync(stylePath)) {
|
|
350
|
+
const relKey = path.relative(projectRoot, stylePath).replace(/\\/g, '/');
|
|
351
|
+
inlineMap[relKey] = fs.readFileSync(stylePath, 'utf-8');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Detect `templateUrl:` — single string
|
|
356
|
+
const tmplMatch = code.match(/templateUrl\s*:\s*['"]([^'"]+)['"]/);
|
|
357
|
+
if (tmplMatch) {
|
|
358
|
+
const tmplPath = path.join(fileDir, tmplMatch[1]);
|
|
359
|
+
if (fs.existsSync(tmplPath)) {
|
|
360
|
+
const relKey = path.relative(projectRoot, tmplPath).replace(/\\/g, '/');
|
|
361
|
+
inlineMap[relKey] = fs.readFileSync(tmplPath, 'utf-8');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return inlineMap;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Try to auto-detect the app entry point.
|
|
371
|
+
* Looks for <script type="module" src="..."> in an index.html,
|
|
372
|
+
* or falls back to common conventions.
|
|
373
|
+
*/
|
|
374
|
+
function detectEntry(projectRoot) {
|
|
375
|
+
const htmlCandidates = ['index.html', 'public/index.html'];
|
|
376
|
+
for (const htmlFile of htmlCandidates) {
|
|
377
|
+
const htmlPath = path.join(projectRoot, htmlFile);
|
|
378
|
+
if (fs.existsSync(htmlPath)) {
|
|
379
|
+
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
380
|
+
const m = html.match(/<script[^>]+type\s*=\s*["']module["'][^>]+src\s*=\s*["']([^"']+)["']/);
|
|
381
|
+
if (m) return path.join(projectRoot, m[1]);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Convention fallback
|
|
385
|
+
const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
386
|
+
for (const f of fallbacks) {
|
|
387
|
+
const fp = path.join(projectRoot, f);
|
|
388
|
+
if (fs.existsSync(fp)) return fp;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
function bundleApp() {
|
|
395
|
+
const projectRoot = process.cwd();
|
|
396
|
+
|
|
397
|
+
// Entry point
|
|
398
|
+
let entry = null;
|
|
399
|
+
// First positional arg after "bundle" that doesn't start with -
|
|
400
|
+
for (let i = 1; i < args.length; i++) {
|
|
401
|
+
if (!args[i].startsWith('-') && args[i - 1] !== '-o' && args[i - 1] !== '--out' && args[i - 1] !== '--html') {
|
|
402
|
+
entry = path.resolve(projectRoot, args[i]);
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!entry) entry = detectEntry(projectRoot);
|
|
407
|
+
|
|
408
|
+
if (!entry || !fs.existsSync(entry)) {
|
|
409
|
+
console.error(`\n ✗ Could not find entry file.`);
|
|
410
|
+
console.error(` Provide one explicitly: zquery bundle scripts/app.js\n`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const outPath = option('out', 'o', null);
|
|
415
|
+
const watchMode = flag('watch', 'w');
|
|
416
|
+
|
|
417
|
+
// Auto-detect index.html by walking up from the entry file, then check cwd
|
|
418
|
+
let htmlFile = option('html', null, null);
|
|
419
|
+
let htmlAbs = htmlFile ? path.resolve(projectRoot, htmlFile) : null;
|
|
420
|
+
if (!htmlFile) {
|
|
421
|
+
const htmlCandidates = [];
|
|
422
|
+
// Walk up from the entry file's directory to cwd looking for index.html
|
|
423
|
+
let entryDir = path.dirname(entry);
|
|
424
|
+
while (entryDir.length >= projectRoot.length) {
|
|
425
|
+
htmlCandidates.push(path.join(entryDir, 'index.html'));
|
|
426
|
+
const parent = path.dirname(entryDir);
|
|
427
|
+
if (parent === entryDir) break;
|
|
428
|
+
entryDir = parent;
|
|
429
|
+
}
|
|
430
|
+
// Also check cwd and public/
|
|
431
|
+
htmlCandidates.push(path.join(projectRoot, 'index.html'));
|
|
432
|
+
htmlCandidates.push(path.join(projectRoot, 'public/index.html'));
|
|
433
|
+
for (const candidate of htmlCandidates) {
|
|
434
|
+
if (fs.existsSync(candidate)) {
|
|
435
|
+
htmlAbs = candidate;
|
|
436
|
+
htmlFile = path.relative(projectRoot, candidate);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Derive output directory:
|
|
443
|
+
// -o flag → use that path
|
|
444
|
+
// else → dist/ next to the detected HTML file (or cwd/dist/ as fallback)
|
|
445
|
+
const entryRel = path.relative(projectRoot, entry);
|
|
446
|
+
const entryName = path.basename(entry, '.js');
|
|
447
|
+
let baseDistDir;
|
|
448
|
+
if (outPath) {
|
|
449
|
+
const resolved = path.resolve(projectRoot, outPath);
|
|
450
|
+
if (outPath.endsWith('/') || outPath.endsWith('\\') || !path.extname(outPath) ||
|
|
451
|
+
(fs.existsSync(resolved) && fs.statSync(resolved).isDirectory())) {
|
|
452
|
+
baseDistDir = resolved;
|
|
453
|
+
} else {
|
|
454
|
+
baseDistDir = path.dirname(resolved);
|
|
455
|
+
}
|
|
456
|
+
} else if (htmlAbs) {
|
|
457
|
+
baseDistDir = path.join(path.dirname(htmlAbs), 'dist');
|
|
458
|
+
} else {
|
|
459
|
+
baseDistDir = path.join(projectRoot, 'dist');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Two output sub-directories: server/ (with <base href="/">) and local/ (relative paths)
|
|
463
|
+
const serverDir = path.join(baseDistDir, 'server');
|
|
464
|
+
const localDir = path.join(baseDistDir, 'local');
|
|
465
|
+
|
|
466
|
+
console.log(`\n zQuery App Bundler`);
|
|
467
|
+
console.log(` Entry: ${entryRel}`);
|
|
468
|
+
console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
|
|
469
|
+
console.log(` Library: embedded`);
|
|
470
|
+
console.log(` HTML: ${htmlFile || 'not found (no index.html detected)'}`);
|
|
471
|
+
console.log('');
|
|
472
|
+
|
|
473
|
+
function doBuild() {
|
|
474
|
+
const start = Date.now();
|
|
475
|
+
|
|
476
|
+
if (!fs.existsSync(serverDir)) fs.mkdirSync(serverDir, { recursive: true });
|
|
477
|
+
if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
|
|
478
|
+
|
|
479
|
+
// Walk the import graph
|
|
480
|
+
const files = walkImportGraph(entry);
|
|
481
|
+
console.log(` Resolved ${files.length} module(s):`);
|
|
482
|
+
files.forEach(f => console.log(` • ${path.relative(projectRoot, f)}`));
|
|
483
|
+
|
|
484
|
+
// Build concatenated source
|
|
485
|
+
const sections = files.map(file => {
|
|
486
|
+
let code = fs.readFileSync(file, 'utf-8');
|
|
487
|
+
code = stripModuleSyntax(code);
|
|
488
|
+
code = replaceImportMeta(code, file, projectRoot);
|
|
489
|
+
const rel = path.relative(projectRoot, file);
|
|
490
|
+
return `// --- ${rel} ${'—'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Embed zquery.min.js — always included
|
|
494
|
+
let libSection = '';
|
|
495
|
+
{
|
|
496
|
+
const pkgSrcDir = path.join(__dirname, 'src');
|
|
497
|
+
const pkgMinFile = path.join(__dirname, 'dist', 'zquery.min.js');
|
|
498
|
+
|
|
499
|
+
// Always rebuild the library from source when running from the repo/package
|
|
500
|
+
// so that dist/ stays current with the latest source changes.
|
|
501
|
+
if (fs.existsSync(pkgSrcDir) && fs.existsSync(path.join(__dirname, 'index.js'))) {
|
|
502
|
+
console.log(`\n Building library from source...`);
|
|
503
|
+
const prevCwd = process.cwd();
|
|
504
|
+
try {
|
|
505
|
+
process.chdir(__dirname);
|
|
506
|
+
buildLibrary();
|
|
507
|
+
} finally {
|
|
508
|
+
process.chdir(prevCwd);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Now look for the library in common locations
|
|
513
|
+
const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
|
|
514
|
+
const libCandidates = [
|
|
515
|
+
// Prefer the freshly-built package dist
|
|
516
|
+
path.join(__dirname, 'dist/zquery.min.js'),
|
|
517
|
+
// Then check project-local locations
|
|
518
|
+
htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
|
|
519
|
+
htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
|
|
520
|
+
path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
|
|
521
|
+
path.join(projectRoot, 'vendor/zquery.min.js'),
|
|
522
|
+
path.join(projectRoot, 'lib/zquery.min.js'),
|
|
523
|
+
path.join(projectRoot, 'dist/zquery.min.js'),
|
|
524
|
+
path.join(projectRoot, 'zquery.min.js'),
|
|
525
|
+
].filter(Boolean);
|
|
526
|
+
const libPath = libCandidates.find(p => fs.existsSync(p));
|
|
527
|
+
|
|
528
|
+
if (libPath) {
|
|
529
|
+
const libBytes = fs.statSync(libPath).size;
|
|
530
|
+
libSection = `// --- zquery.min.js (library) ${'—'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
|
|
531
|
+
+ `// --- Build-time metadata ————————————————————————————\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
|
|
532
|
+
console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
|
|
533
|
+
} else {
|
|
534
|
+
console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
|
|
535
|
+
console.warn(` Place zquery.min.js in scripts/vendor/, vendor/, lib/, or dist/`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const banner = `/**\n * App bundle — built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
|
|
540
|
+
|
|
541
|
+
// Scan for external resources (pages, templateUrl, styleUrl) and inline them
|
|
542
|
+
const inlineMap = collectInlineResources(files, projectRoot);
|
|
543
|
+
let inlineSection = '';
|
|
544
|
+
if (Object.keys(inlineMap).length > 0) {
|
|
545
|
+
const entries = Object.entries(inlineMap).map(([key, content]) => {
|
|
546
|
+
// Escape for embedding in a JS string literal
|
|
547
|
+
const escaped = content
|
|
548
|
+
.replace(/\\/g, '\\\\')
|
|
549
|
+
.replace(/'/g, "\\'")
|
|
550
|
+
.replace(/\n/g, '\\n')
|
|
551
|
+
.replace(/\r/g, '');
|
|
552
|
+
return ` '${key}': '${escaped}'`;
|
|
553
|
+
});
|
|
554
|
+
inlineSection = `// --- Inlined resources (file:// support) ${'—'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
|
|
555
|
+
console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const bundle = `${banner}\n(function() {\n 'use strict';\n\n${libSection}${inlineSection}${sections.join('\n\n')}\n\n})();\n`;
|
|
559
|
+
|
|
560
|
+
// Content-hashed output filenames (z-<name>.<hash>.js)
|
|
561
|
+
const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
|
|
562
|
+
const bundleBase = `z-${entryName}.${contentHash}.js`;
|
|
563
|
+
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
564
|
+
|
|
565
|
+
// Remove previous hashed builds from both output directories
|
|
566
|
+
const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
567
|
+
const cleanRe = new RegExp(`^z-${escName}\\.[a-f0-9]{8}\\.(?:min\\.)?js$`);
|
|
568
|
+
for (const dir of [serverDir, localDir]) {
|
|
569
|
+
if (fs.existsSync(dir)) {
|
|
570
|
+
for (const f of fs.readdirSync(dir)) {
|
|
571
|
+
if (cleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Write bundle into server/ (canonical), then copy to local/
|
|
577
|
+
const bundleFile = path.join(serverDir, bundleBase);
|
|
578
|
+
const minFile = path.join(serverDir, minBase);
|
|
579
|
+
fs.writeFileSync(bundleFile, bundle, 'utf-8');
|
|
580
|
+
fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
|
|
581
|
+
fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
|
|
582
|
+
fs.copyFileSync(minFile, path.join(localDir, minBase));
|
|
583
|
+
|
|
584
|
+
console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
|
|
585
|
+
console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
|
|
586
|
+
|
|
587
|
+
// Rewrite index.html → two variants (server/ and local/)
|
|
588
|
+
if (htmlFile) {
|
|
589
|
+
const bundledFileSet = new Set(files);
|
|
590
|
+
rewriteHtml(projectRoot, htmlFile, bundleFile, true, bundledFileSet, serverDir, localDir);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const elapsed = Date.now() - start;
|
|
594
|
+
console.log(` Done in ${elapsed}ms\n`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
doBuild();
|
|
598
|
+
|
|
599
|
+
// Watch mode
|
|
600
|
+
if (watchMode) {
|
|
601
|
+
const watchDirs = new Set();
|
|
602
|
+
const files = walkImportGraph(entry);
|
|
603
|
+
files.forEach(f => watchDirs.add(path.dirname(f)));
|
|
604
|
+
|
|
605
|
+
console.log(' Watching for changes...\n');
|
|
606
|
+
let debounceTimer;
|
|
607
|
+
for (const dir of watchDirs) {
|
|
608
|
+
fs.watch(dir, { recursive: true }, (_, filename) => {
|
|
609
|
+
if (!filename || !filename.endsWith('.js')) return;
|
|
610
|
+
clearTimeout(debounceTimer);
|
|
611
|
+
debounceTimer = setTimeout(() => {
|
|
612
|
+
console.log(` Changed: ${filename} — rebuilding...`);
|
|
613
|
+
try { doBuild(); } catch (e) { console.error(` ✗ ${e.message}`); }
|
|
614
|
+
}, 200);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Recursively copy a directory.
|
|
622
|
+
*/
|
|
623
|
+
function copyDirSync(src, dest) {
|
|
624
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
625
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
626
|
+
const srcPath = path.join(src, entry.name);
|
|
627
|
+
const destPath = path.join(dest, entry.name);
|
|
628
|
+
if (entry.isDirectory()) {
|
|
629
|
+
copyDirSync(srcPath, destPath);
|
|
630
|
+
} else {
|
|
631
|
+
fs.copyFileSync(srcPath, destPath);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Rewrite an HTML file to replace the module <script> with the bundle.
|
|
638
|
+
* Copies all referenced assets into both server/ and local/ dist dirs,
|
|
639
|
+
* then writes two index.html variants:
|
|
640
|
+
* server/index.html — has <base href="/"> for SPA deep-route support
|
|
641
|
+
* local/index.html — no <base>, relative paths for file:// access
|
|
642
|
+
*
|
|
643
|
+
* Both are fully static HTML with no dynamic loading.
|
|
644
|
+
*/
|
|
645
|
+
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
|
|
646
|
+
const htmlPath = path.resolve(projectRoot, htmlRelPath);
|
|
647
|
+
if (!fs.existsSync(htmlPath)) {
|
|
648
|
+
console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const htmlDir = path.dirname(htmlPath);
|
|
653
|
+
let html = fs.readFileSync(htmlPath, 'utf-8');
|
|
654
|
+
|
|
655
|
+
// Collect all asset references from the HTML (src=, href= on link/script/img)
|
|
656
|
+
const assetRe = /(?:<(?:link|script|img)[^>]*?\s(?:src|href)\s*=\s*["'])([^"']+)["']/gi;
|
|
657
|
+
const assets = new Set();
|
|
658
|
+
let m;
|
|
659
|
+
while ((m = assetRe.exec(html)) !== null) {
|
|
660
|
+
const ref = m[1];
|
|
661
|
+
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:') || ref.startsWith('#')) continue;
|
|
662
|
+
const refAbs = path.resolve(htmlDir, ref);
|
|
663
|
+
if (bundledFiles && bundledFiles.has(refAbs)) continue;
|
|
664
|
+
if (includeLib && /zquery(?:\.min)?\.js$/i.test(ref)) continue;
|
|
665
|
+
assets.add(ref);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Copy each referenced asset into BOTH dist dirs, preserving directory structure
|
|
669
|
+
let copiedCount = 0;
|
|
670
|
+
for (const asset of assets) {
|
|
671
|
+
const srcFile = path.resolve(htmlDir, asset);
|
|
672
|
+
if (!fs.existsSync(srcFile)) continue;
|
|
673
|
+
|
|
674
|
+
for (const distDir of [serverDir, localDir]) {
|
|
675
|
+
const destFile = path.join(distDir, asset);
|
|
676
|
+
const destDir = path.dirname(destFile);
|
|
677
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
678
|
+
fs.copyFileSync(srcFile, destFile);
|
|
679
|
+
}
|
|
680
|
+
copiedCount++;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Also copy any CSS-referenced assets (fonts, images in url() etc.)
|
|
684
|
+
for (const asset of assets) {
|
|
685
|
+
const srcFile = path.resolve(htmlDir, asset);
|
|
686
|
+
if (!fs.existsSync(srcFile) || !asset.endsWith('.css')) continue;
|
|
687
|
+
|
|
688
|
+
const cssContent = fs.readFileSync(srcFile, 'utf-8');
|
|
689
|
+
const urlRe = /url\(\s*["']?([^"')]+?)["']?\s*\)/g;
|
|
690
|
+
let cm;
|
|
691
|
+
while ((cm = urlRe.exec(cssContent)) !== null) {
|
|
692
|
+
const ref = cm[1];
|
|
693
|
+
if (ref.startsWith('data:') || ref.startsWith('http') || ref.startsWith('//')) continue;
|
|
694
|
+
const cssSrcDir = path.dirname(srcFile);
|
|
695
|
+
const assetSrc = path.resolve(cssSrcDir, ref);
|
|
696
|
+
if (!fs.existsSync(assetSrc)) continue;
|
|
697
|
+
|
|
698
|
+
for (const distDir of [serverDir, localDir]) {
|
|
699
|
+
const cssDestDir = path.dirname(path.join(distDir, asset));
|
|
700
|
+
const assetDest = path.resolve(cssDestDir, ref);
|
|
701
|
+
if (!fs.existsSync(assetDest)) {
|
|
702
|
+
const dir = path.dirname(assetDest);
|
|
703
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
704
|
+
fs.copyFileSync(assetSrc, assetDest);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
copiedCount++;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Make the bundle filename relative (same name in both dirs)
|
|
712
|
+
const bundleRel = path.relative(serverDir, bundleFile).replace(/\\/g, '/');
|
|
713
|
+
|
|
714
|
+
// Replace <script type="module" src="..."> with the bundle (defer)
|
|
715
|
+
html = html.replace(
|
|
716
|
+
/<script\s+type\s*=\s*["']module["']\s+src\s*=\s*["'][^"']+["']\s*>\s*<\/script>/gi,
|
|
717
|
+
`<script defer src="${bundleRel}"></script>`
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
// If library is embedded, remove the standalone zquery script tag
|
|
721
|
+
if (includeLib) {
|
|
722
|
+
html = html.replace(
|
|
723
|
+
/\s*<script\s+src\s*=\s*["'][^"']*zquery(?:\.min)?\.js["']\s*>\s*<\/script>/gi,
|
|
724
|
+
''
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ── server/index.html ──
|
|
729
|
+
// Keep <base href="/"> as-is — the preload scanner sees it, all resources
|
|
730
|
+
// resolve from root, deep-route refreshes work perfectly.
|
|
731
|
+
const serverHtml = html;
|
|
732
|
+
|
|
733
|
+
// ── local/index.html ──
|
|
734
|
+
// Remove <base href="/"> so relative paths resolve from the HTML file's
|
|
735
|
+
// directory — correct for file:// with zero console errors.
|
|
736
|
+
const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
|
|
737
|
+
|
|
738
|
+
// Write both
|
|
739
|
+
const htmlName = path.basename(htmlRelPath);
|
|
740
|
+
fs.writeFileSync(path.join(serverDir, htmlName), serverHtml, 'utf-8');
|
|
741
|
+
fs.writeFileSync(path.join(localDir, htmlName), localHtml, 'utf-8');
|
|
742
|
+
console.log(` ✓ server/${htmlName} (with <base href="/">)`);
|
|
743
|
+
console.log(` ✓ local/${htmlName} (relative paths, file:// ready)`);
|
|
744
|
+
console.log(` ✓ Copied ${copiedCount} asset(s) into both dist dirs`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
// ---------------------------------------------------------------------------
|
|
749
|
+
// Help
|
|
750
|
+
// ---------------------------------------------------------------------------
|
|
751
|
+
|
|
752
|
+
function showHelp() {
|
|
753
|
+
console.log(`
|
|
754
|
+
zQuery CLI — build & bundle tool
|
|
755
|
+
|
|
756
|
+
COMMANDS
|
|
757
|
+
|
|
758
|
+
build Build the zQuery library → dist/
|
|
759
|
+
(must be run from the project root where src/ lives)
|
|
760
|
+
|
|
761
|
+
bundle [entry] Bundle app ES modules into a single file
|
|
762
|
+
--out, -o <path> Output directory (default: dist/ next to index.html)
|
|
763
|
+
--html <file> Use a specific HTML file (default: auto-detected)
|
|
764
|
+
--watch, -w Watch source files and rebuild on changes
|
|
765
|
+
|
|
766
|
+
SMART DEFAULTS
|
|
767
|
+
|
|
768
|
+
The bundler works with zero flags for typical projects:
|
|
769
|
+
• Entry is auto-detected from index.html <script type="module" src="...">
|
|
770
|
+
• zquery.min.js is always embedded (auto-built from source if not found)
|
|
771
|
+
• index.html is rewritten for both server and local (file://) use
|
|
772
|
+
• Output goes to dist/server/ and dist/local/ next to the detected index.html
|
|
773
|
+
|
|
774
|
+
OUTPUT
|
|
775
|
+
|
|
776
|
+
The bundler produces two self-contained sub-directories:
|
|
777
|
+
|
|
778
|
+
dist/server/ deploy to your web server
|
|
779
|
+
index.html has <base href="/"> for SPA deep routes
|
|
780
|
+
z-<entry>.<hash>.js readable bundle
|
|
781
|
+
z-<entry>.<hash>.min.js minified bundle
|
|
782
|
+
|
|
783
|
+
dist/local/ open from disk (file://)
|
|
784
|
+
index.html relative paths, no <base> tag
|
|
785
|
+
z-<entry>.<hash>.js same bundle
|
|
786
|
+
|
|
787
|
+
Previous hashed builds are automatically cleaned on each rebuild.
|
|
788
|
+
|
|
789
|
+
DEVELOPMENT
|
|
790
|
+
|
|
791
|
+
npm run serve start a local dev server (zero-http, SPA routing)
|
|
792
|
+
npm run dev watch mode — auto-rebuild bundle on source changes
|
|
793
|
+
|
|
794
|
+
EXAMPLES
|
|
795
|
+
|
|
796
|
+
# Build the library only
|
|
797
|
+
zquery build
|
|
798
|
+
|
|
799
|
+
# Bundle an app — auto-detects everything
|
|
800
|
+
cd my-app && zquery bundle
|
|
801
|
+
|
|
802
|
+
# Point to an entry from a parent directory
|
|
803
|
+
zquery bundle path/to/scripts/app.js
|
|
804
|
+
|
|
805
|
+
# Custom output directory
|
|
806
|
+
zquery bundle -o build/
|
|
807
|
+
|
|
808
|
+
# Watch mode
|
|
809
|
+
zquery bundle --watch
|
|
810
|
+
|
|
811
|
+
The bundler walks the ES module import graph starting from the entry
|
|
812
|
+
file, topologically sorts dependencies, strips import/export syntax,
|
|
813
|
+
and concatenates everything into a single IIFE with content-hashed
|
|
814
|
+
filenames for cache-busting. No dependencies needed — just Node.js.
|
|
815
|
+
`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// Main
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
824
|
+
showHelp();
|
|
825
|
+
} else if (command === 'build') {
|
|
826
|
+
console.log('\n zQuery Library Build\n');
|
|
827
|
+
buildLibrary();
|
|
828
|
+
} else if (command === 'bundle') {
|
|
829
|
+
bundleApp();
|
|
830
|
+
} else {
|
|
831
|
+
console.error(`\n Unknown command: ${command}\n Run "zquery --help" for usage.\n`);
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|