zero-query 0.3.1 → 0.4.9
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 +42 -17
- package/cli/args.js +33 -0
- package/cli/commands/build.js +58 -0
- package/cli/commands/bundle.js +584 -0
- package/cli/commands/create.js +67 -0
- package/cli/commands/dev.js +516 -0
- package/cli/help.js +92 -0
- package/cli/index.js +53 -0
- package/cli/scaffold/LICENSE +21 -0
- package/cli/scaffold/index.html +62 -0
- package/cli/scaffold/scripts/app.js +101 -0
- package/cli/scaffold/scripts/components/about.js +119 -0
- package/cli/scaffold/scripts/components/api-demo.js +103 -0
- package/cli/scaffold/scripts/components/contacts/contacts.css +253 -0
- package/cli/scaffold/scripts/components/contacts/contacts.html +139 -0
- package/cli/scaffold/scripts/components/contacts/contacts.js +137 -0
- package/cli/scaffold/scripts/components/counter.js +65 -0
- package/cli/scaffold/scripts/components/home.js +137 -0
- package/cli/scaffold/scripts/components/not-found.js +16 -0
- package/cli/scaffold/scripts/components/todos.js +130 -0
- package/cli/scaffold/scripts/routes.js +13 -0
- package/cli/scaffold/scripts/store.js +96 -0
- package/cli/scaffold/styles/styles.css +556 -0
- package/cli/utils.js +122 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +431 -61
- package/dist/zquery.min.js +5 -5
- package/index.d.ts +206 -66
- package/index.js +5 -4
- package/package.json +8 -8
- package/src/component.js +408 -52
- package/src/core.js +16 -3
- package/src/router.js +2 -2
- package/cli.js +0 -1208
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/bundle.js — app bundler command
|
|
3
|
+
*
|
|
4
|
+
* Walks the ES module import graph starting from an entry file,
|
|
5
|
+
* strips import/export syntax, concatenates everything into a single
|
|
6
|
+
* IIFE with content-hashed filenames, and rewrites index.html for
|
|
7
|
+
* both server and local (file://) deployment.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
const { args, option } = require('../args');
|
|
17
|
+
const { minify, sizeKB } = require('../utils');
|
|
18
|
+
const buildLibrary = require('./build');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Module graph helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Resolve an import specifier relative to the importing file. */
|
|
25
|
+
function resolveImport(specifier, fromFile) {
|
|
26
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null;
|
|
27
|
+
let resolved = path.resolve(path.dirname(fromFile), specifier);
|
|
28
|
+
if (!path.extname(resolved) && fs.existsSync(resolved + '.js')) {
|
|
29
|
+
resolved += '.js';
|
|
30
|
+
}
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Extract import specifiers from a source file. */
|
|
35
|
+
function extractImports(code) {
|
|
36
|
+
const specifiers = [];
|
|
37
|
+
let m;
|
|
38
|
+
const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
39
|
+
while ((m = fromRe.exec(code)) !== null) specifiers.push(m[1]);
|
|
40
|
+
const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
|
|
41
|
+
while ((m = sideRe.exec(code)) !== null) {
|
|
42
|
+
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
43
|
+
}
|
|
44
|
+
return specifiers;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Walk the import graph — topological sort (leaves first). */
|
|
48
|
+
function walkImportGraph(entry) {
|
|
49
|
+
const visited = new Set();
|
|
50
|
+
const order = [];
|
|
51
|
+
|
|
52
|
+
function visit(file) {
|
|
53
|
+
const abs = path.resolve(file);
|
|
54
|
+
if (visited.has(abs)) return;
|
|
55
|
+
visited.add(abs);
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(abs)) {
|
|
58
|
+
console.warn(` ⚠ Missing file: ${abs}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const code = fs.readFileSync(abs, 'utf-8');
|
|
63
|
+
const imports = extractImports(code);
|
|
64
|
+
for (const spec of imports) {
|
|
65
|
+
const resolved = resolveImport(spec, abs);
|
|
66
|
+
if (resolved) visit(resolved);
|
|
67
|
+
}
|
|
68
|
+
order.push(abs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
visit(entry);
|
|
72
|
+
return order;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Strip ES module import/export syntax, keeping declarations. */
|
|
76
|
+
function stripModuleSyntax(code) {
|
|
77
|
+
code = code.replace(/^\s*import\s+[\s\S]*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
78
|
+
code = code.replace(/^\s*import\s+['"].*?['"];?\s*$/gm, '');
|
|
79
|
+
code = code.replace(/^(\s*)export\s+default\s+/gm, '$1');
|
|
80
|
+
code = code.replace(/^(\s*)export\s+(const|let|var|function|class|async\s+function)\s/gm, '$1$2 ');
|
|
81
|
+
code = code.replace(/^\s*export\s*\{[\s\S]*?\};?\s*$/gm, '');
|
|
82
|
+
return code;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Replace import.meta.url with a runtime equivalent. */
|
|
86
|
+
function replaceImportMeta(code, filePath, projectRoot) {
|
|
87
|
+
if (!code.includes('import.meta')) return code;
|
|
88
|
+
const rel = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
89
|
+
return code.replace(/import\.meta\.url/g, `(new URL('${rel}', document.baseURI).href)`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Rewrite templateUrl / styleUrl relative paths to project-relative paths.
|
|
94
|
+
* In a bundled IIFE, caller-base detection returns undefined (all stack
|
|
95
|
+
* frames point to the single bundle file), so relative URLs like
|
|
96
|
+
* 'contacts.html' never resolve to their original directory. By rewriting
|
|
97
|
+
* them to the same project-relative keys used in window.__zqInline, the
|
|
98
|
+
* runtime inline-resource lookup succeeds without a network fetch.
|
|
99
|
+
*/
|
|
100
|
+
function rewriteResourceUrls(code, filePath, projectRoot) {
|
|
101
|
+
const fileDir = path.dirname(filePath);
|
|
102
|
+
return code.replace(
|
|
103
|
+
/((?:templateUrl|styleUrl)\s*:\s*)(['"])([^'"]+)\2/g,
|
|
104
|
+
(match, prefix, quote, url) => {
|
|
105
|
+
if (url.startsWith('/') || url.includes('://')) return match;
|
|
106
|
+
const abs = path.resolve(fileDir, url);
|
|
107
|
+
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/');
|
|
108
|
+
return `${prefix}${quote}${rel}${quote}`;
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Scan bundled source files for external resource references
|
|
115
|
+
* (pages config, templateUrl, styleUrl) and return a map of
|
|
116
|
+
* { relativePath: fileContent } for inlining.
|
|
117
|
+
*/
|
|
118
|
+
function collectInlineResources(files, projectRoot) {
|
|
119
|
+
const inlineMap = {};
|
|
120
|
+
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
123
|
+
const fileDir = path.dirname(file);
|
|
124
|
+
|
|
125
|
+
// pages: config
|
|
126
|
+
const pagesMatch = code.match(/pages\s*:\s*\{[^}]*dir\s*:\s*['"]([^'"]+)['"]/s);
|
|
127
|
+
if (pagesMatch) {
|
|
128
|
+
const pagesDir = pagesMatch[1];
|
|
129
|
+
const ext = (code.match(/pages\s*:\s*\{[^}]*ext\s*:\s*['"]([^'"]+)['"]/s) || [])[1] || '.html';
|
|
130
|
+
const itemsMatch = code.match(/items\s*:\s*\[([\s\S]*?)\]/);
|
|
131
|
+
if (itemsMatch) {
|
|
132
|
+
const itemsBlock = itemsMatch[1];
|
|
133
|
+
const ids = [];
|
|
134
|
+
let m;
|
|
135
|
+
const strRe = /['"]([^'"]+)['"]/g;
|
|
136
|
+
while ((m = strRe.exec(itemsBlock)) !== null) {
|
|
137
|
+
const before = itemsBlock.substring(Math.max(0, m.index - 20), m.index);
|
|
138
|
+
if (/label\s*:\s*$/.test(before)) continue;
|
|
139
|
+
ids.push(m[1]);
|
|
140
|
+
}
|
|
141
|
+
const absPagesDir = path.join(fileDir, pagesDir);
|
|
142
|
+
for (const id of ids) {
|
|
143
|
+
const pagePath = path.join(absPagesDir, id + ext);
|
|
144
|
+
if (fs.existsSync(pagePath)) {
|
|
145
|
+
const relKey = path.relative(projectRoot, pagePath).replace(/\\/g, '/');
|
|
146
|
+
inlineMap[relKey] = fs.readFileSync(pagePath, 'utf-8');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// styleUrl:
|
|
153
|
+
const styleMatch = code.match(/styleUrl\s*:\s*['"]([^'"]+)['"]/);
|
|
154
|
+
if (styleMatch) {
|
|
155
|
+
const stylePath = path.join(fileDir, styleMatch[1]);
|
|
156
|
+
if (fs.existsSync(stylePath)) {
|
|
157
|
+
const relKey = path.relative(projectRoot, stylePath).replace(/\\/g, '/');
|
|
158
|
+
inlineMap[relKey] = fs.readFileSync(stylePath, 'utf-8');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// templateUrl:
|
|
163
|
+
const tmplMatch = code.match(/templateUrl\s*:\s*['"]([^'"]+)['"]/);
|
|
164
|
+
if (tmplMatch) {
|
|
165
|
+
const tmplPath = path.join(fileDir, tmplMatch[1]);
|
|
166
|
+
if (fs.existsSync(tmplPath)) {
|
|
167
|
+
const relKey = path.relative(projectRoot, tmplPath).replace(/\\/g, '/');
|
|
168
|
+
inlineMap[relKey] = fs.readFileSync(tmplPath, 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return inlineMap;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Auto-detect the app entry point.
|
|
178
|
+
*
|
|
179
|
+
* Strategy — ordered by precedence (first match wins):
|
|
180
|
+
* 1. HTML discovery: index.html first, then other .html files
|
|
181
|
+
* (root level + one directory deep).
|
|
182
|
+
* 2. Within each HTML file, prefer a module <script> whose src
|
|
183
|
+
* resolves to app.js, then fall back to the first module
|
|
184
|
+
* <script> tag regardless of name.
|
|
185
|
+
* 3. JS file scan: look for $.router first (entry point by design),
|
|
186
|
+
* then $.mount / $.store / mountAll.
|
|
187
|
+
* 4. Convention fallback paths.
|
|
188
|
+
*/
|
|
189
|
+
function detectEntry(projectRoot) {
|
|
190
|
+
// Matches all <script type="module" src="…"> tags (global)
|
|
191
|
+
const moduleScriptReG = /<script[^>]+type\s*=\s*["']module["'][^>]+src\s*=\s*["']([^"']+)["']/g;
|
|
192
|
+
|
|
193
|
+
// Collect HTML files: root level + one directory deep
|
|
194
|
+
const htmlFiles = [];
|
|
195
|
+
for (const entry of fs.readdirSync(projectRoot, { withFileTypes: true })) {
|
|
196
|
+
if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
197
|
+
htmlFiles.push(path.join(projectRoot, entry.name));
|
|
198
|
+
} else if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist') {
|
|
199
|
+
const sub = path.join(projectRoot, entry.name);
|
|
200
|
+
try {
|
|
201
|
+
for (const child of fs.readdirSync(sub, { withFileTypes: true })) {
|
|
202
|
+
if (child.isFile() && child.name.endsWith('.html')) {
|
|
203
|
+
htmlFiles.push(path.join(sub, child.name));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch { /* permission error — skip */ }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Sort: index.html first (at any depth), then alphabetical
|
|
211
|
+
htmlFiles.sort((a, b) => {
|
|
212
|
+
const aIsIndex = path.basename(a) === 'index.html' ? 0 : 1;
|
|
213
|
+
const bIsIndex = path.basename(b) === 'index.html' ? 0 : 1;
|
|
214
|
+
if (aIsIndex !== bIsIndex) return aIsIndex - bIsIndex;
|
|
215
|
+
return a.localeCompare(b);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 1. Parse module <script> tags from HTML files (index.html evaluated first).
|
|
219
|
+
// Within each file prefer a src ending in app.js, else the first module tag.
|
|
220
|
+
for (const htmlPath of htmlFiles) {
|
|
221
|
+
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
222
|
+
const htmlDir = path.dirname(htmlPath);
|
|
223
|
+
|
|
224
|
+
let appJsEntry = null; // src that resolves to app.js
|
|
225
|
+
let firstEntry = null; // first module script src
|
|
226
|
+
let m;
|
|
227
|
+
moduleScriptReG.lastIndex = 0;
|
|
228
|
+
while ((m = moduleScriptReG.exec(html)) !== null) {
|
|
229
|
+
const resolved = path.resolve(htmlDir, m[1]);
|
|
230
|
+
if (!fs.existsSync(resolved)) continue;
|
|
231
|
+
if (!firstEntry) firstEntry = resolved;
|
|
232
|
+
if (path.basename(resolved) === 'app.js') { appJsEntry = resolved; break; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (appJsEntry) return appJsEntry;
|
|
236
|
+
if (firstEntry) return firstEntry;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2. Search JS files for entry-point patterns.
|
|
240
|
+
// Pass 1 — $.router (the canonical entry point).
|
|
241
|
+
// Pass 2 — $.mount, $.store, mountAll (component-level, lower confidence).
|
|
242
|
+
const routerRe = /\$\.router\s*\(/;
|
|
243
|
+
const otherRe = /\$\.(mount|store)\s*\(|mountAll\s*\(/;
|
|
244
|
+
|
|
245
|
+
function collectJS(dir, depth = 0) {
|
|
246
|
+
const results = [];
|
|
247
|
+
if (depth > 2) return results;
|
|
248
|
+
try {
|
|
249
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
250
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') continue;
|
|
251
|
+
const full = path.join(dir, entry.name);
|
|
252
|
+
if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
253
|
+
results.push(full);
|
|
254
|
+
} else if (entry.isDirectory()) {
|
|
255
|
+
results.push(...collectJS(full, depth + 1));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch { /* skip */ }
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const jsFiles = collectJS(projectRoot);
|
|
263
|
+
|
|
264
|
+
// Pass 1: $.router
|
|
265
|
+
for (const file of jsFiles) {
|
|
266
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
267
|
+
if (routerRe.test(code)) return file;
|
|
268
|
+
}
|
|
269
|
+
// Pass 2: other entry-point signals
|
|
270
|
+
for (const file of jsFiles) {
|
|
271
|
+
const code = fs.readFileSync(file, 'utf-8');
|
|
272
|
+
if (otherRe.test(code)) return file;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. Convention fallbacks
|
|
276
|
+
const fallbacks = ['scripts/app.js', 'src/app.js', 'js/app.js', 'app.js', 'main.js'];
|
|
277
|
+
for (const f of fallbacks) {
|
|
278
|
+
const fp = path.join(projectRoot, f);
|
|
279
|
+
if (fs.existsSync(fp)) return fp;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// HTML rewriting
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Rewrite an HTML file to replace the module <script> with the bundle.
|
|
290
|
+
* Produces two variants:
|
|
291
|
+
* server/index.html — <base href="/"> for SPA deep routes
|
|
292
|
+
* local/index.html — relative paths for file:// access
|
|
293
|
+
*/
|
|
294
|
+
function rewriteHtml(projectRoot, htmlRelPath, bundleFile, includeLib, bundledFiles, serverDir, localDir) {
|
|
295
|
+
const htmlPath = path.resolve(projectRoot, htmlRelPath);
|
|
296
|
+
if (!fs.existsSync(htmlPath)) {
|
|
297
|
+
console.warn(` ⚠ HTML file not found: ${htmlRelPath}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const htmlDir = path.dirname(htmlPath);
|
|
302
|
+
let html = fs.readFileSync(htmlPath, 'utf-8');
|
|
303
|
+
|
|
304
|
+
// Collect asset references from HTML
|
|
305
|
+
const assetRe = /(?:<(?:link|script|img)[^>]*?\s(?:src|href)\s*=\s*["'])([^"']+)["']/gi;
|
|
306
|
+
const assets = new Set();
|
|
307
|
+
let m;
|
|
308
|
+
while ((m = assetRe.exec(html)) !== null) {
|
|
309
|
+
const ref = m[1];
|
|
310
|
+
if (ref.startsWith('http') || ref.startsWith('//') || ref.startsWith('data:') || ref.startsWith('#')) continue;
|
|
311
|
+
const refAbs = path.resolve(htmlDir, ref);
|
|
312
|
+
if (bundledFiles && bundledFiles.has(refAbs)) continue;
|
|
313
|
+
if (includeLib && /zquery(?:\.min)?\.js$/i.test(ref)) continue;
|
|
314
|
+
assets.add(ref);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Copy assets into both dist dirs
|
|
318
|
+
let copiedCount = 0;
|
|
319
|
+
for (const asset of assets) {
|
|
320
|
+
const srcFile = path.resolve(htmlDir, asset);
|
|
321
|
+
if (!fs.existsSync(srcFile)) continue;
|
|
322
|
+
for (const distDir of [serverDir, localDir]) {
|
|
323
|
+
const destFile = path.join(distDir, asset);
|
|
324
|
+
const destDir = path.dirname(destFile);
|
|
325
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
326
|
+
fs.copyFileSync(srcFile, destFile);
|
|
327
|
+
}
|
|
328
|
+
copiedCount++;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Copy CSS-referenced assets (fonts, images in url())
|
|
332
|
+
for (const asset of assets) {
|
|
333
|
+
const srcFile = path.resolve(htmlDir, asset);
|
|
334
|
+
if (!fs.existsSync(srcFile) || !asset.endsWith('.css')) continue;
|
|
335
|
+
const cssContent = fs.readFileSync(srcFile, 'utf-8');
|
|
336
|
+
const urlRe = /url\(\s*["']?([^"')]+?)["']?\s*\)/g;
|
|
337
|
+
let cm;
|
|
338
|
+
while ((cm = urlRe.exec(cssContent)) !== null) {
|
|
339
|
+
const ref = cm[1];
|
|
340
|
+
if (ref.startsWith('data:') || ref.startsWith('http') || ref.startsWith('//')) continue;
|
|
341
|
+
const cssSrcDir = path.dirname(srcFile);
|
|
342
|
+
const assetSrc = path.resolve(cssSrcDir, ref);
|
|
343
|
+
if (!fs.existsSync(assetSrc)) continue;
|
|
344
|
+
for (const distDir of [serverDir, localDir]) {
|
|
345
|
+
const cssDestDir = path.dirname(path.join(distDir, asset));
|
|
346
|
+
const assetDest = path.resolve(cssDestDir, ref);
|
|
347
|
+
if (!fs.existsSync(assetDest)) {
|
|
348
|
+
const dir = path.dirname(assetDest);
|
|
349
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
350
|
+
fs.copyFileSync(assetSrc, assetDest);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
copiedCount++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const bundleRel = path.relative(serverDir, bundleFile).replace(/\\/g, '/');
|
|
358
|
+
|
|
359
|
+
html = html.replace(
|
|
360
|
+
/<script\s+type\s*=\s*["']module["']\s+src\s*=\s*["'][^"']+["']\s*>\s*<\/script>/gi,
|
|
361
|
+
`<script defer src="${bundleRel}"></script>`
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
if (includeLib) {
|
|
365
|
+
html = html.replace(
|
|
366
|
+
/\s*<script\s+src\s*=\s*["'][^"']*zquery(?:\.min)?\.js["']\s*>\s*<\/script>/gi,
|
|
367
|
+
''
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const serverHtml = html;
|
|
372
|
+
const localHtml = html.replace(/<base\s+href\s*=\s*["']\/["'][^>]*>\s*\n?\s*/i, '');
|
|
373
|
+
|
|
374
|
+
const htmlName = path.basename(htmlRelPath);
|
|
375
|
+
fs.writeFileSync(path.join(serverDir, htmlName), serverHtml, 'utf-8');
|
|
376
|
+
fs.writeFileSync(path.join(localDir, htmlName), localHtml, 'utf-8');
|
|
377
|
+
console.log(` ✓ server/${htmlName} (with <base href="/">)`);
|
|
378
|
+
console.log(` ✓ local/${htmlName} (relative paths, file:// ready)`);
|
|
379
|
+
console.log(` ✓ Copied ${copiedCount} asset(s) into both dist dirs`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Main bundleApp function
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
function bundleApp() {
|
|
387
|
+
const projectRoot = process.cwd();
|
|
388
|
+
|
|
389
|
+
// Entry point — accepts a directory (auto-detects entry inside it) or a file
|
|
390
|
+
let entry = null;
|
|
391
|
+
let targetDir = null;
|
|
392
|
+
for (let i = 1; i < args.length; i++) {
|
|
393
|
+
if (!args[i].startsWith('-') && args[i - 1] !== '-o' && args[i - 1] !== '--out' && args[i - 1] !== '--html') {
|
|
394
|
+
const resolved = path.resolve(projectRoot, args[i]);
|
|
395
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
396
|
+
targetDir = resolved;
|
|
397
|
+
entry = detectEntry(resolved);
|
|
398
|
+
} else {
|
|
399
|
+
entry = resolved;
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (!entry) entry = detectEntry(projectRoot);
|
|
405
|
+
|
|
406
|
+
if (!entry || !fs.existsSync(entry)) {
|
|
407
|
+
console.error(`\n \u2717 Could not find entry file.`);
|
|
408
|
+
console.error(` Provide an app directory: zquery bundle my-app/\n`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const outPath = option('out', 'o', null);
|
|
413
|
+
|
|
414
|
+
// Auto-detect index.html
|
|
415
|
+
let htmlFile = option('html', null, null);
|
|
416
|
+
let htmlAbs = htmlFile ? path.resolve(projectRoot, htmlFile) : null;
|
|
417
|
+
if (!htmlFile) {
|
|
418
|
+
const htmlCandidates = [];
|
|
419
|
+
let entryDir = path.dirname(entry);
|
|
420
|
+
while (entryDir.length >= projectRoot.length) {
|
|
421
|
+
htmlCandidates.push(path.join(entryDir, 'index.html'));
|
|
422
|
+
const parent = path.dirname(entryDir);
|
|
423
|
+
if (parent === entryDir) break;
|
|
424
|
+
entryDir = parent;
|
|
425
|
+
}
|
|
426
|
+
htmlCandidates.push(path.join(projectRoot, 'index.html'));
|
|
427
|
+
htmlCandidates.push(path.join(projectRoot, 'public/index.html'));
|
|
428
|
+
for (const candidate of htmlCandidates) {
|
|
429
|
+
if (fs.existsSync(candidate)) {
|
|
430
|
+
htmlAbs = candidate;
|
|
431
|
+
htmlFile = path.relative(projectRoot, candidate);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Output directory
|
|
438
|
+
const entryRel = path.relative(projectRoot, entry);
|
|
439
|
+
const entryName = path.basename(entry, '.js');
|
|
440
|
+
let baseDistDir;
|
|
441
|
+
if (outPath) {
|
|
442
|
+
const resolved = path.resolve(projectRoot, outPath);
|
|
443
|
+
if (outPath.endsWith('/') || outPath.endsWith('\\') || !path.extname(outPath) ||
|
|
444
|
+
(fs.existsSync(resolved) && fs.statSync(resolved).isDirectory())) {
|
|
445
|
+
baseDistDir = resolved;
|
|
446
|
+
} else {
|
|
447
|
+
baseDistDir = path.dirname(resolved);
|
|
448
|
+
}
|
|
449
|
+
} else if (htmlAbs) {
|
|
450
|
+
baseDistDir = path.join(path.dirname(htmlAbs), 'dist');
|
|
451
|
+
} else {
|
|
452
|
+
baseDistDir = path.join(projectRoot, 'dist');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const serverDir = path.join(baseDistDir, 'server');
|
|
456
|
+
const localDir = path.join(baseDistDir, 'local');
|
|
457
|
+
|
|
458
|
+
console.log(`\n zQuery App Bundler`);
|
|
459
|
+
console.log(` Entry: ${entryRel}`);
|
|
460
|
+
console.log(` Output: ${path.relative(projectRoot, baseDistDir)}/server/ & local/`);
|
|
461
|
+
console.log(` Library: embedded`);
|
|
462
|
+
console.log(` HTML: ${htmlFile || 'not found (no index.html detected)'}`);
|
|
463
|
+
console.log('');
|
|
464
|
+
|
|
465
|
+
// ------ doBuild (inlined) ------
|
|
466
|
+
const start = Date.now();
|
|
467
|
+
|
|
468
|
+
if (!fs.existsSync(serverDir)) fs.mkdirSync(serverDir, { recursive: true });
|
|
469
|
+
if (!fs.existsSync(localDir)) fs.mkdirSync(localDir, { recursive: true });
|
|
470
|
+
|
|
471
|
+
const files = walkImportGraph(entry);
|
|
472
|
+
console.log(` Resolved ${files.length} module(s):`);
|
|
473
|
+
files.forEach(f => console.log(` • ${path.relative(projectRoot, f)}`));
|
|
474
|
+
|
|
475
|
+
const sections = files.map(file => {
|
|
476
|
+
let code = fs.readFileSync(file, 'utf-8');
|
|
477
|
+
code = stripModuleSyntax(code);
|
|
478
|
+
code = replaceImportMeta(code, file, projectRoot);
|
|
479
|
+
code = rewriteResourceUrls(code, file, projectRoot);
|
|
480
|
+
const rel = path.relative(projectRoot, file);
|
|
481
|
+
return `// --- ${rel} ${'—'.repeat(Math.max(1, 60 - rel.length))}\n${code.trim()}`;
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Embed zquery.min.js
|
|
485
|
+
let libSection = '';
|
|
486
|
+
{
|
|
487
|
+
// __dirname is cli/commands/, so the package root is two levels up
|
|
488
|
+
const pkgRoot = path.resolve(__dirname, '..', '..');
|
|
489
|
+
const pkgSrcDir = path.join(pkgRoot, 'src');
|
|
490
|
+
const pkgMinFile = path.join(pkgRoot, 'dist', 'zquery.min.js');
|
|
491
|
+
|
|
492
|
+
if (fs.existsSync(pkgSrcDir) && fs.existsSync(path.join(pkgRoot, 'index.js'))) {
|
|
493
|
+
console.log(`\n Building library from source...`);
|
|
494
|
+
const prevCwd = process.cwd();
|
|
495
|
+
try {
|
|
496
|
+
process.chdir(pkgRoot);
|
|
497
|
+
buildLibrary();
|
|
498
|
+
} finally {
|
|
499
|
+
process.chdir(prevCwd);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const htmlDir = htmlAbs ? path.dirname(htmlAbs) : null;
|
|
504
|
+
const libCandidates = [
|
|
505
|
+
path.join(pkgRoot, 'dist/zquery.min.js'),
|
|
506
|
+
htmlDir && path.join(htmlDir, 'scripts/vendor/zquery.min.js'),
|
|
507
|
+
htmlDir && path.join(htmlDir, 'vendor/zquery.min.js'),
|
|
508
|
+
path.join(projectRoot, 'scripts/vendor/zquery.min.js'),
|
|
509
|
+
path.join(projectRoot, 'vendor/zquery.min.js'),
|
|
510
|
+
path.join(projectRoot, 'lib/zquery.min.js'),
|
|
511
|
+
path.join(projectRoot, 'dist/zquery.min.js'),
|
|
512
|
+
path.join(projectRoot, 'zquery.min.js'),
|
|
513
|
+
].filter(Boolean);
|
|
514
|
+
const libPath = libCandidates.find(p => fs.existsSync(p));
|
|
515
|
+
|
|
516
|
+
if (libPath) {
|
|
517
|
+
const libBytes = fs.statSync(libPath).size;
|
|
518
|
+
libSection = `// --- zquery.min.js (library) ${'—'.repeat(34)}\n${fs.readFileSync(libPath, 'utf-8').trim()}\n\n`
|
|
519
|
+
+ `// --- Build-time metadata ————————————————————————————\nif(typeof $!=="undefined"){$.meta=Object.assign($.meta||{},{libSize:${libBytes}});}\n\n`;
|
|
520
|
+
console.log(` Embedded library from ${path.relative(projectRoot, libPath)} (${(libBytes / 1024).toFixed(1)} KB)`);
|
|
521
|
+
} else {
|
|
522
|
+
console.warn(`\n ⚠ Could not find zquery.min.js anywhere`);
|
|
523
|
+
console.warn(` Place zquery.min.js in scripts/vendor/, vendor/, lib/, or dist/`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const banner = `/**\n * App bundle — built by zQuery CLI\n * Entry: ${entryRel}\n * ${new Date().toISOString()}\n */`;
|
|
528
|
+
|
|
529
|
+
// Inline resources
|
|
530
|
+
const inlineMap = collectInlineResources(files, projectRoot);
|
|
531
|
+
let inlineSection = '';
|
|
532
|
+
if (Object.keys(inlineMap).length > 0) {
|
|
533
|
+
const entries = Object.entries(inlineMap).map(([key, content]) => {
|
|
534
|
+
const escaped = content
|
|
535
|
+
.replace(/\\/g, '\\\\')
|
|
536
|
+
.replace(/'/g, "\\'")
|
|
537
|
+
.replace(/\n/g, '\\n')
|
|
538
|
+
.replace(/\r/g, '');
|
|
539
|
+
return ` '${key}': '${escaped}'`;
|
|
540
|
+
});
|
|
541
|
+
inlineSection = `// --- Inlined resources (file:// support) ${'—'.repeat(20)}\nwindow.__zqInline = {\n${entries.join(',\n')}\n};\n\n`;
|
|
542
|
+
console.log(`\n Inlined ${Object.keys(inlineMap).length} external resource(s)`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const bundle = `${banner}\n(function() {\n 'use strict';\n\n${libSection}${inlineSection}${sections.join('\n\n')}\n\n})();\n`;
|
|
546
|
+
|
|
547
|
+
// Content-hashed filenames
|
|
548
|
+
const contentHash = crypto.createHash('sha256').update(bundle).digest('hex').slice(0, 8);
|
|
549
|
+
const bundleBase = `z-${entryName}.${contentHash}.js`;
|
|
550
|
+
const minBase = `z-${entryName}.${contentHash}.min.js`;
|
|
551
|
+
|
|
552
|
+
// Clean previous builds
|
|
553
|
+
const escName = entryName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
554
|
+
const cleanRe = new RegExp(`^z-${escName}\\.[a-f0-9]{8}\\.(?:min\\.)?js$`);
|
|
555
|
+
for (const dir of [serverDir, localDir]) {
|
|
556
|
+
if (fs.existsSync(dir)) {
|
|
557
|
+
for (const f of fs.readdirSync(dir)) {
|
|
558
|
+
if (cleanRe.test(f)) fs.unlinkSync(path.join(dir, f));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Write bundles
|
|
564
|
+
const bundleFile = path.join(serverDir, bundleBase);
|
|
565
|
+
const minFile = path.join(serverDir, minBase);
|
|
566
|
+
fs.writeFileSync(bundleFile, bundle, 'utf-8');
|
|
567
|
+
fs.writeFileSync(minFile, minify(bundle, banner), 'utf-8');
|
|
568
|
+
fs.copyFileSync(bundleFile, path.join(localDir, bundleBase));
|
|
569
|
+
fs.copyFileSync(minFile, path.join(localDir, minBase));
|
|
570
|
+
|
|
571
|
+
console.log(`\n ✓ ${bundleBase} (${sizeKB(fs.readFileSync(bundleFile))} KB)`);
|
|
572
|
+
console.log(` ✓ ${minBase} (${sizeKB(fs.readFileSync(minFile))} KB)`);
|
|
573
|
+
|
|
574
|
+
// Rewrite HTML
|
|
575
|
+
if (htmlFile) {
|
|
576
|
+
const bundledFileSet = new Set(files);
|
|
577
|
+
rewriteHtml(projectRoot, htmlFile, minFile, true, bundledFileSet, serverDir, localDir);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const elapsed = Date.now() - start;
|
|
581
|
+
console.log(` Done in ${elapsed}ms\n`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
module.exports = bundleApp;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// cli/commands/create.js — scaffold a new zQuery project
|
|
2
|
+
// Reads template files from cli/scaffold/, replaces {{NAME}} with the project
|
|
3
|
+
// name, and writes them into the target directory.
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Recursively collect every file under `dir`, returning paths relative to `dir`.
|
|
10
|
+
*/
|
|
11
|
+
function walkDir(dir, prefix = '') {
|
|
12
|
+
const entries = [];
|
|
13
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
entries.push(...walkDir(path.join(dir, entry.name), rel));
|
|
17
|
+
} else {
|
|
18
|
+
entries.push(rel);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return entries;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createProject(args) {
|
|
25
|
+
const target = args[1] ? path.resolve(args[1]) : process.cwd();
|
|
26
|
+
const name = path.basename(target);
|
|
27
|
+
|
|
28
|
+
// Guard: refuse to overwrite existing files
|
|
29
|
+
const conflicts = ['index.html', 'scripts'].filter(f =>
|
|
30
|
+
fs.existsSync(path.join(target, f))
|
|
31
|
+
);
|
|
32
|
+
if (conflicts.length) {
|
|
33
|
+
console.error(`\n \u2717 Directory already contains: ${conflicts.join(', ')}`);
|
|
34
|
+
console.error(` Aborting to avoid overwriting existing files.\n`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`\n zQuery \u2014 Create Project\n`);
|
|
39
|
+
console.log(` Scaffolding into ${target}\n`);
|
|
40
|
+
|
|
41
|
+
// Resolve the scaffold template directory
|
|
42
|
+
const scaffoldDir = path.resolve(__dirname, '..', 'scaffold');
|
|
43
|
+
|
|
44
|
+
// Walk the scaffold directory and copy each file
|
|
45
|
+
const templateFiles = walkDir(scaffoldDir);
|
|
46
|
+
|
|
47
|
+
for (const rel of templateFiles) {
|
|
48
|
+
const src = path.join(scaffoldDir, rel);
|
|
49
|
+
let content = fs.readFileSync(src, 'utf-8');
|
|
50
|
+
|
|
51
|
+
// Replace the {{NAME}} placeholder with the actual project name
|
|
52
|
+
content = content.replace(/\{\{NAME\}\}/g, name);
|
|
53
|
+
|
|
54
|
+
const dest = path.join(target, rel);
|
|
55
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
56
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
57
|
+
console.log(` \u2713 ${rel}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`
|
|
61
|
+
Done! Next steps:
|
|
62
|
+
|
|
63
|
+
npx zquery dev${target !== process.cwd() ? ` ${args[1]}` : ''}
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = createProject;
|