zero-query 1.0.9 → 1.1.1
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/commands/build-api.js +442 -0
- package/cli/commands/build.js +33 -2
- package/cli/commands/bundle.js +41 -0
- package/cli/commands/dev/server.js +56 -3
- package/cli/scaffold/default/app/components/contacts/contacts.css +9 -9
- package/cli/scaffold/default/app/components/playground/playground.css +1 -1
- package/cli/scaffold/default/app/components/playground/playground.html +5 -5
- package/cli/scaffold/default/app/components/playground/playground.js +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +3 -3
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +4 -4
- package/cli/utils.js +6 -6
- package/dist/API.md +6603 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +387 -25
- package/dist/zquery.min.js +47 -17
- package/index.d.ts +9 -3
- package/index.js +10 -2
- package/package.json +2 -1
- package/src/component.js +243 -6
- package/src/reactive.js +4 -3
- package/src/router.js +79 -9
- package/src/store.js +49 -3
- package/tests/cli.test.js +80 -0
- package/tests/compare.test.js +486 -0
- package/tests/dev-server.test.js +489 -0
- package/tests/docs.test.js +1650 -0
- package/tests/electron-features.test.js +864 -0
- package/types/misc.d.ts +7 -7
- package/types/reactive.d.ts +1 -1
- package/types/store.d.ts +2 -1
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/build-api.js - auto-generate API.md from docs section data
|
|
3
|
+
*
|
|
4
|
+
* Reads the structured docs sections (zquery-website/app/components/docs/sections),
|
|
5
|
+
* converts their HTML content to clean Markdown, and writes API.md.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node cli/commands/build-api.js
|
|
8
|
+
* npm run build:api
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { pathToFileURL } = require('url');
|
|
16
|
+
|
|
17
|
+
// Sections to include in API.md and their order.
|
|
18
|
+
// Maps section ID → { file, export } for direct import (avoids pulling in
|
|
19
|
+
// getting-started.js which depends on the website store / globalThis.$).
|
|
20
|
+
const API_SECTIONS = [
|
|
21
|
+
{ id: 'router', file: 'router.js', exportName: 'routerSec' },
|
|
22
|
+
{ id: 'components', file: 'components.js', exportName: 'components' },
|
|
23
|
+
{ id: 'directives', file: 'directives.js', exportName: 'directives' },
|
|
24
|
+
{ id: 'store', file: 'store.js', exportName: 'storeSec' },
|
|
25
|
+
{ id: 'http', file: 'http.js', exportName: 'http' },
|
|
26
|
+
{ id: 'reactive', file: 'reactive.js', exportName: 'reactive' },
|
|
27
|
+
{ id: 'selectors', file: 'selectors.js', exportName: 'selectors' },
|
|
28
|
+
{ id: 'utils', file: 'utils.js', exportName: 'utils' },
|
|
29
|
+
{ id: 'error-handling', file: 'error-handling.js', exportName: 'errorHandling'},
|
|
30
|
+
{ id: 'ssr', file: 'ssr.js', exportName: 'ssr' },
|
|
31
|
+
{ id: 'security', file: 'security.js', exportName: 'security' },
|
|
32
|
+
{ id: 'environment', file: 'environment.js', exportName: 'environment' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// HTML entity decoding
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function unesc(str) {
|
|
41
|
+
return str
|
|
42
|
+
.replace(/&/g, '&')
|
|
43
|
+
.replace(/</g, '<')
|
|
44
|
+
.replace(/>/g, '>')
|
|
45
|
+
.replace(/"/g, '"')
|
|
46
|
+
.replace(/'/g, "'")
|
|
47
|
+
.replace(/—/g, '\u2014')
|
|
48
|
+
.replace(/–/g, '\u2013')
|
|
49
|
+
.replace(/→/g, '\u2192')
|
|
50
|
+
.replace(/←/g, '\u2190')
|
|
51
|
+
.replace(/ /g, ' ')
|
|
52
|
+
.replace(/ /g, ' ')
|
|
53
|
+
.replace(/ /g, ' ')
|
|
54
|
+
.replace(/…/g, '\u2026')
|
|
55
|
+
.replace(/×/g, '\u00D7')
|
|
56
|
+
.replace(/©/g, '\u00A9')
|
|
57
|
+
.replace(/’/g, '\u2019')
|
|
58
|
+
.replace(/‘/g, '\u2018')
|
|
59
|
+
.replace(/”/g, '\u201D')
|
|
60
|
+
.replace(/“/g, '\u201C')
|
|
61
|
+
.replace(/•/g, '\u2022')
|
|
62
|
+
.replace(/·/g, '\u00B7')
|
|
63
|
+
.replace(/™/g, '\u2122')
|
|
64
|
+
.replace(/›/g, '\u203A')
|
|
65
|
+
.replace(/‹/g, '\u2039')
|
|
66
|
+
.replace(/↔/g, '\u2194')
|
|
67
|
+
.replace(/'/g, "'")
|
|
68
|
+
.replace(/ /g, '\u200A')
|
|
69
|
+
.replace(/ /g, '\u2009')
|
|
70
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Inline HTML → Markdown
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert inline HTML elements to their Markdown equivalents.
|
|
80
|
+
* Handles: code, strong/b, em/i, a, br, spans (dots, badges, etc.)
|
|
81
|
+
*/
|
|
82
|
+
function inlineMd(html) {
|
|
83
|
+
if (!html) return '';
|
|
84
|
+
let md = html;
|
|
85
|
+
|
|
86
|
+
// Strip decorative dots (colored circles in table cells)
|
|
87
|
+
md = md.replace(/<span class="docs-dot"[^>]*><\/span>\s*/g, '');
|
|
88
|
+
md = md.replace(/<span class="docs-legend-dot"[^>]*><\/span>\s*/g, '');
|
|
89
|
+
md = md.replace(/<span class="docs-legend-item">[\s\S]*?<\/span>/g, '');
|
|
90
|
+
|
|
91
|
+
// ZQueryCollection SVG badge → backtick text
|
|
92
|
+
md = md.replace(/<span class="zq-badge">[\s\S]*?<\/span>/g, '`ZQueryCollection`');
|
|
93
|
+
|
|
94
|
+
// Status badges (colored pills) → plain text
|
|
95
|
+
md = md.replace(/<span\s+style="[^"]*background:[^"]*"[^>]*>([\s\S]*?)<\/span>/g, '$1');
|
|
96
|
+
|
|
97
|
+
// Convert <a> links (before code, since links can wrap code elements)
|
|
98
|
+
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g, '[$2]($1)');
|
|
99
|
+
|
|
100
|
+
// Convert <code> (both inline and zq-inline variants)
|
|
101
|
+
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/g, (_, c) => '`' + unesc(c) + '`');
|
|
102
|
+
|
|
103
|
+
// Convert <strong>/<b>
|
|
104
|
+
md = md.replace(/<(?:strong|b)>([\s\S]*?)<\/(?:strong|b)>/g, '**$1**');
|
|
105
|
+
|
|
106
|
+
// Convert <em>/<i>
|
|
107
|
+
md = md.replace(/<(?:em|i)>([\s\S]*?)<\/(?:em|i)>/g, '*$1*');
|
|
108
|
+
|
|
109
|
+
// Convert <br>
|
|
110
|
+
md = md.replace(/<br\s*\/?>/g, '\n');
|
|
111
|
+
|
|
112
|
+
// Strip remaining <span> (keep content)
|
|
113
|
+
md = md.replace(/<span[^>]*>([\s\S]*?)<\/span>/g, '$1');
|
|
114
|
+
|
|
115
|
+
return md;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// HTML table → Markdown table
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function convertTable(tableHtml) {
|
|
124
|
+
let html = tableHtml;
|
|
125
|
+
|
|
126
|
+
// Strip legend rows (decorative colored-dot rows)
|
|
127
|
+
html = html.replace(/<tr class="docs-table-legend-row">[\s\S]*?<\/tr>/g, '');
|
|
128
|
+
|
|
129
|
+
const headers = [];
|
|
130
|
+
const rows = [];
|
|
131
|
+
|
|
132
|
+
// --- Extract header cells from <thead> ---
|
|
133
|
+
const theadMatch = html.match(/<thead>([\s\S]*?)<\/thead>/);
|
|
134
|
+
if (theadMatch) {
|
|
135
|
+
const thRe = /<th[^>]*>([\s\S]*?)<\/th>/g;
|
|
136
|
+
let m;
|
|
137
|
+
while ((m = thRe.exec(theadMatch[1])) !== null) {
|
|
138
|
+
headers.push(unesc(inlineMd(m[1])).trim());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Extract body rows from <tbody> ---
|
|
143
|
+
const tbodyMatch = html.match(/<tbody>([\s\S]*?)<\/tbody>/);
|
|
144
|
+
if (tbodyMatch) {
|
|
145
|
+
const trRe = /<tr>([\s\S]*?)<\/tr>/g;
|
|
146
|
+
let m;
|
|
147
|
+
while ((m = trRe.exec(tbodyMatch[1])) !== null) {
|
|
148
|
+
const cells = [];
|
|
149
|
+
const tdRe = /<td>([\s\S]*?)<\/td>/g;
|
|
150
|
+
let td;
|
|
151
|
+
while ((td = tdRe.exec(m[1])) !== null) {
|
|
152
|
+
let cell = unesc(inlineMd(td[1])).trim();
|
|
153
|
+
cell = cell.replace(/\n+/g, ' '); // flatten to single line
|
|
154
|
+
cell = cell.replace(/\|/g, '\\|'); // escape pipe chars
|
|
155
|
+
cells.push(cell);
|
|
156
|
+
}
|
|
157
|
+
rows.push(cells);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!headers.length && !rows.length) return '';
|
|
162
|
+
|
|
163
|
+
const colCount = Math.max(headers.length, rows[0]?.length || 0);
|
|
164
|
+
|
|
165
|
+
// Pad short rows
|
|
166
|
+
while (headers.length < colCount) headers.push('');
|
|
167
|
+
rows.forEach(r => { while (r.length < colCount) r.push(''); });
|
|
168
|
+
|
|
169
|
+
const lines = [];
|
|
170
|
+
lines.push('| ' + headers.join(' | ') + ' |');
|
|
171
|
+
lines.push('| ' + headers.map(() => '---').join(' | ') + ' |');
|
|
172
|
+
rows.forEach(r => lines.push('| ' + r.join(' | ') + ' |'));
|
|
173
|
+
|
|
174
|
+
return '\n' + lines.join('\n') + '\n';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Full section HTML → Markdown
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
function htmlToMarkdown(html) {
|
|
183
|
+
let md = html;
|
|
184
|
+
|
|
185
|
+
// ---- Pass 1: Extract code blocks → placeholders ----
|
|
186
|
+
// Matches: optional code-header div, then <pre z-skip><code class="language-xxx">...</code></pre>
|
|
187
|
+
const codeBlocks = [];
|
|
188
|
+
md = md.replace(
|
|
189
|
+
/(?:<div class="code-header">[\s\S]*?<\/div>\s*)?<pre[^>]*>\s*<code class="language-(\w+)">([\s\S]*?)<\/code>\s*<\/pre>/g,
|
|
190
|
+
(_, lang, src) => {
|
|
191
|
+
const idx = codeBlocks.length;
|
|
192
|
+
codeBlocks.push({ lang, src: unesc(src).trim() });
|
|
193
|
+
return `\n__CODEBLOCK_${idx}__\n`;
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// ---- Pass 2: Convert tables ----
|
|
198
|
+
md = md.replace(
|
|
199
|
+
/<div class="docs-table-wrap">\s*<table[^>]*>([\s\S]*?)<\/table>\s*<\/div>/g,
|
|
200
|
+
(_, content) => convertTable(content)
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// ---- Pass 3: Convert callouts, tips, warnings ----
|
|
204
|
+
md = md.replace(/<div class="docs-callout"[^>]*>([\s\S]*?)<\/div>/g, (_, c) => {
|
|
205
|
+
const text = unesc(inlineMd(c)).trim();
|
|
206
|
+
const lines = text.split('\n').map(l => '> ' + l);
|
|
207
|
+
return '\n' + lines.join('\n') + '\n';
|
|
208
|
+
});
|
|
209
|
+
md = md.replace(/<div class="docs-tip">([\s\S]*?)<\/div>/g, (_, c) => {
|
|
210
|
+
return '\n> **Tip:** ' + unesc(inlineMd(c)).trim() + '\n';
|
|
211
|
+
});
|
|
212
|
+
md = md.replace(/<div class="docs-warning">([\s\S]*?)<\/div>/g, (_, c) => {
|
|
213
|
+
return '\n> **Warning:** ' + unesc(inlineMd(c)).trim() + '\n';
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ---- Pass 4: Strip legend bars & file trees ----
|
|
217
|
+
md = md.replace(/<div class="docs-legend-bar">[\s\S]*?<\/div>/g, '');
|
|
218
|
+
// File trees (only in non-API sections, but strip just in case)
|
|
219
|
+
md = md.replace(/<div class="file-tree">[\s\S]*?<\/div>(?:\s*<\/div>)*/g, '');
|
|
220
|
+
|
|
221
|
+
// ---- Pass 5: Convert headings ----
|
|
222
|
+
md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/g, (_, c) => '\n## ' + unesc(inlineMd(c)).trim() + '\n');
|
|
223
|
+
md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/g, (_, c) => '\n### ' + unesc(inlineMd(c)).trim() + '\n');
|
|
224
|
+
md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/g, (_, c) => '\n#### ' + unesc(inlineMd(c)).trim() + '\n');
|
|
225
|
+
|
|
226
|
+
// ---- Pass 6: Convert lists ----
|
|
227
|
+
md = md.replace(/<ul>([\s\S]*?)<\/ul>/g, (_, content) => {
|
|
228
|
+
let result = '\n';
|
|
229
|
+
const liRe = /<li>([\s\S]*?)<\/li>/g;
|
|
230
|
+
let li;
|
|
231
|
+
while ((li = liRe.exec(content)) !== null) {
|
|
232
|
+
result += '- ' + unesc(inlineMd(li[1])).trim() + '\n';
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
});
|
|
236
|
+
md = md.replace(/<ol>([\s\S]*?)<\/ol>/g, (_, content) => {
|
|
237
|
+
let result = '\n';
|
|
238
|
+
let i = 1;
|
|
239
|
+
const liRe = /<li>([\s\S]*?)<\/li>/g;
|
|
240
|
+
let li;
|
|
241
|
+
while ((li = liRe.exec(content)) !== null) {
|
|
242
|
+
result += `${i++}. ` + unesc(inlineMd(li[1])).trim() + '\n';
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ---- Pass 7: Convert paragraphs ----
|
|
248
|
+
md = md.replace(/<p>([\s\S]*?)<\/p>/g, (_, c) => '\n' + unesc(inlineMd(c)).trim() + '\n');
|
|
249
|
+
|
|
250
|
+
// ---- Pass 8: Convert <hr> ----
|
|
251
|
+
md = md.replace(/<hr\s*\/?>/g, '\n---\n');
|
|
252
|
+
|
|
253
|
+
// ---- Pass 9: Remaining inline conversion ----
|
|
254
|
+
md = inlineMd(md);
|
|
255
|
+
|
|
256
|
+
// ---- Pass 10: Strip remaining HTML tags ----
|
|
257
|
+
md = md.replace(/<[^>]+>/g, '');
|
|
258
|
+
|
|
259
|
+
// ---- Pass 11: Decode remaining entities ----
|
|
260
|
+
md = unesc(md);
|
|
261
|
+
|
|
262
|
+
// ---- Pass 12: Clean up whitespace ----
|
|
263
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
264
|
+
md = md.trim();
|
|
265
|
+
|
|
266
|
+
// ---- Pass 13: Restore code blocks ----
|
|
267
|
+
codeBlocks.forEach((block, i) => {
|
|
268
|
+
md = md.replace(
|
|
269
|
+
`__CODEBLOCK_${i}__`,
|
|
270
|
+
'\n```' + block.lang + '\n' + block.src + '\n```\n'
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Final whitespace cleanup
|
|
275
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
276
|
+
|
|
277
|
+
return md;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// TOC builder
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function slugify(text) {
|
|
286
|
+
return text
|
|
287
|
+
.toLowerCase()
|
|
288
|
+
.replace(/`/g, '')
|
|
289
|
+
.replace(/\$/g, '')
|
|
290
|
+
.replace(/\(\)/g, '')
|
|
291
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
292
|
+
.replace(/\s+/g, '-')
|
|
293
|
+
.replace(/-+/g, '-')
|
|
294
|
+
.replace(/^-|-$/g, '');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildToc(sections) {
|
|
298
|
+
const lines = [];
|
|
299
|
+
for (const section of sections) {
|
|
300
|
+
// Get the section title from the first <h2> in content
|
|
301
|
+
const html = section.content();
|
|
302
|
+
const h2Match = html.match(/<h2[^>]*>([\s\S]*?)<\/h2>/);
|
|
303
|
+
const title = h2Match ? unesc(inlineMd(h2Match[1])).trim() : section.label;
|
|
304
|
+
lines.push(`- [${title}](#${slugify(title)})`);
|
|
305
|
+
for (const h of section.headings) {
|
|
306
|
+
const hText = unesc(h.text);
|
|
307
|
+
lines.push(` - [${hText}](#${slugify(hText)})`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return lines.join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// ES Module Exports section (generated from index.js)
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
function buildEsmSection(root) {
|
|
319
|
+
const indexSrc = fs.readFileSync(path.join(root, 'index.js'), 'utf-8');
|
|
320
|
+
|
|
321
|
+
// Extract the export block
|
|
322
|
+
const exportMatch = indexSrc.match(/export\s*\{([\s\S]*?)\}/);
|
|
323
|
+
if (!exportMatch) return '';
|
|
324
|
+
|
|
325
|
+
const exports = exportMatch[1]
|
|
326
|
+
.split(',')
|
|
327
|
+
.map(e => e.trim())
|
|
328
|
+
.filter(Boolean)
|
|
329
|
+
.map(e => {
|
|
330
|
+
const parts = e.split(/\s+as\s+/);
|
|
331
|
+
return parts.length > 1
|
|
332
|
+
? { name: parts[1].trim(), alias: parts[0].trim() }
|
|
333
|
+
: { name: parts[0].trim() };
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const importNames = exports.map(e => {
|
|
337
|
+
return e.alias ? `${e.alias} as ${e.name}` : e.name;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return [
|
|
341
|
+
'## ES Module Exports (for npm/bundler usage)',
|
|
342
|
+
'',
|
|
343
|
+
'When used as an ES module (not the built bundle), the library provides named exports for every public API:',
|
|
344
|
+
'',
|
|
345
|
+
'```js',
|
|
346
|
+
'import {',
|
|
347
|
+
' ' + importNames.join(',\n '),
|
|
348
|
+
"} from 'zero-query';",
|
|
349
|
+
'```',
|
|
350
|
+
'',
|
|
351
|
+
'> The SSR module has its own entry point: `import { createSSRApp, renderToString } from \'zero-query/ssr\';`',
|
|
352
|
+
].join('\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Main
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
async function buildApi() {
|
|
361
|
+
const root = process.cwd();
|
|
362
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8'));
|
|
363
|
+
|
|
364
|
+
console.log('\n zQuery API.md Generator\n');
|
|
365
|
+
|
|
366
|
+
// Import sections individually (ESM dynamic import)
|
|
367
|
+
// We avoid importing sections/index.js because it pulls in getting-started.js
|
|
368
|
+
// which depends on the website store ($.libSize).
|
|
369
|
+
const sectionsDir = path.join(root, 'zquery-website', 'app', 'components', 'docs', 'sections');
|
|
370
|
+
|
|
371
|
+
const apiSections = [];
|
|
372
|
+
for (const entry of API_SECTIONS) {
|
|
373
|
+
const fileUrl = pathToFileURL(path.join(sectionsDir, entry.file)).href;
|
|
374
|
+
const mod = await import(fileUrl);
|
|
375
|
+
const section = mod[entry.exportName];
|
|
376
|
+
if (section) {
|
|
377
|
+
apiSections.push(section);
|
|
378
|
+
} else {
|
|
379
|
+
console.warn(` ⚠ Section "${entry.id}" not found in ${entry.file} (export: ${entry.exportName})`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
console.log(` Processing ${apiSections.length} sections...`);
|
|
384
|
+
|
|
385
|
+
// --- Header ---
|
|
386
|
+
const header = [
|
|
387
|
+
'# zQuery (zeroQuery) - Full API Reference',
|
|
388
|
+
'',
|
|
389
|
+
'Complete API documentation for every module, method, option, and type in zQuery. All examples assume the global `$` is available via the built `zquery.min.js` bundle. For getting started, project setup, the dev server, and the CLI bundler, see [README.md](README.md).',
|
|
390
|
+
'',
|
|
391
|
+
'> **Editor Support:** Install the [zQuery for VS Code](https://marketplace.visualstudio.com/items?itemName=zQuery.zquery-vs-code) extension for autocomplete, hover docs, directive support, and 185+ code snippets.',
|
|
392
|
+
'',
|
|
393
|
+
'---',
|
|
394
|
+
].join('\n');
|
|
395
|
+
|
|
396
|
+
// --- Table of Contents ---
|
|
397
|
+
const toc = buildToc(apiSections);
|
|
398
|
+
|
|
399
|
+
// --- Convert each section ---
|
|
400
|
+
const sectionMds = apiSections.map(s => htmlToMarkdown(s.content()));
|
|
401
|
+
|
|
402
|
+
// --- ES Module Exports ---
|
|
403
|
+
const esm = buildEsmSection(root);
|
|
404
|
+
|
|
405
|
+
// --- Assemble ---
|
|
406
|
+
const parts = [header, '', '## Table of Contents', '', toc, '', '---'];
|
|
407
|
+
|
|
408
|
+
for (const md of sectionMds) {
|
|
409
|
+
parts.push('');
|
|
410
|
+
parts.push(md);
|
|
411
|
+
parts.push('');
|
|
412
|
+
parts.push('---');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
parts.push('');
|
|
416
|
+
parts.push(esm);
|
|
417
|
+
|
|
418
|
+
let output = parts.join('\n');
|
|
419
|
+
output = output.replace(/\n{4,}/g, '\n\n\n');
|
|
420
|
+
output = output.replace(/\n+$/, '\n');
|
|
421
|
+
|
|
422
|
+
// --- Write ---
|
|
423
|
+
const outPath = path.join(root, 'API.md');
|
|
424
|
+
fs.writeFileSync(outPath, output, 'utf-8');
|
|
425
|
+
|
|
426
|
+
const lines = output.split('\n').length;
|
|
427
|
+
const kb = Math.round(Buffer.from(output).byteLength / 1024);
|
|
428
|
+
console.log(` \u2713 API.md generated (${lines} lines, ${kb} KB)`);
|
|
429
|
+
console.log();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
// Allow both require() (from CLI) and direct execution
|
|
434
|
+
if (require.main === module) {
|
|
435
|
+
buildApi().catch(err => {
|
|
436
|
+
console.error(' \u2717 Failed to generate API.md:', err.message);
|
|
437
|
+
console.error(err.stack);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
module.exports = buildApi;
|
package/cli/commands/build.js
CHANGED
|
@@ -24,6 +24,15 @@ function buildLibrary() {
|
|
|
24
24
|
'src/utils.js',
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
// Guard: ensure we're inside the zero-query source repo
|
|
28
|
+
const missing = modules.filter(m => !fs.existsSync(path.join(process.cwd(), m)));
|
|
29
|
+
if (missing.length > 0) {
|
|
30
|
+
console.error(` ✗ "zquery build" must be run from the zero-query source repository.`);
|
|
31
|
+
console.error(` Missing source files: ${missing.join(', ')}`);
|
|
32
|
+
console.error(` To bundle your app for production, use "zquery bundle" instead.\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
const DIST = path.join(process.cwd(), 'dist');
|
|
28
37
|
const OUT_FILE = path.join(DIST, 'zquery.js');
|
|
29
38
|
const MIN_FILE = path.join(DIST, 'zquery.min.js');
|
|
@@ -111,13 +120,35 @@ function buildLibrary() {
|
|
|
111
120
|
console.log(` ✓ dist/zquery.min.js (${sizeKB(fs.readFileSync(MIN_FILE))} KB)`);
|
|
112
121
|
console.log(` Done in ${elapsed}ms\n`);
|
|
113
122
|
|
|
114
|
-
// ---
|
|
123
|
+
// --- Generate API.md before zipping --------------------------------------
|
|
115
124
|
const root = process.cwd();
|
|
125
|
+
try {
|
|
126
|
+
const buildApi = require('./build-api');
|
|
127
|
+
// buildApi() is async (dynamic imports), so we run it synchronously via
|
|
128
|
+
// a child process to keep the build function synchronous.
|
|
129
|
+
execSync('node cli/commands/build-api.js', {
|
|
130
|
+
cwd: root,
|
|
131
|
+
stdio: 'inherit',
|
|
132
|
+
timeout: 60000,
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.log(' ⚠ API.md generation skipped:', err.message || 'unknown error');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Copy API.md into dist -----------------------------------------------
|
|
139
|
+
const apiSrc = path.join(root, 'API.md');
|
|
140
|
+
const apiDist = path.join(DIST, 'API.md');
|
|
141
|
+
if (fs.existsSync(apiSrc)) {
|
|
142
|
+
fs.copyFileSync(apiSrc, apiDist);
|
|
143
|
+
console.log(` ✓ dist/API.md (${sizeKB(fs.readFileSync(apiDist))} KB)`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Create dist/zquery.dist.zip -----------------------------------------
|
|
116
147
|
const zipFiles = [
|
|
117
148
|
{ src: OUT_FILE, name: 'zquery.js' },
|
|
118
149
|
{ src: MIN_FILE, name: 'zquery.min.js' },
|
|
119
150
|
{ src: path.join(root, 'LICENSE'), name: 'LICENSE' },
|
|
120
|
-
{ src:
|
|
151
|
+
{ src: apiDist, name: 'API.md' },
|
|
121
152
|
{ src: path.join(root, 'README.md'), name: 'README.md' },
|
|
122
153
|
];
|
|
123
154
|
|
package/cli/commands/bundle.js
CHANGED
|
@@ -304,6 +304,27 @@ function minifyCSS(css) {
|
|
|
304
304
|
return css.trim();
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Heuristic: is the next '/' the start of a regex literal (vs division)?
|
|
309
|
+
* Used by minifyTemplateLiterals to avoid misinterpreting backticks
|
|
310
|
+
* inside regex patterns as template literal delimiters.
|
|
311
|
+
*/
|
|
312
|
+
function _isRegexCtxTpl(out) {
|
|
313
|
+
let end = out.length - 1;
|
|
314
|
+
while (end >= 0 && (out[end] === ' ' || out[end] === '\t' || out[end] === '\n' || out[end] === '\r')) end--;
|
|
315
|
+
if (end < 0) return true;
|
|
316
|
+
const last = out[end];
|
|
317
|
+
if ('=({[,;:!&|?~+-*/%^>'.includes(last)) return true;
|
|
318
|
+
const tail = out.substring(Math.max(0, end - 7), end + 1);
|
|
319
|
+
for (const kw of ['return', 'typeof', 'case', 'in', 'delete', 'void', 'throw', 'new']) {
|
|
320
|
+
if (tail.endsWith(kw)) {
|
|
321
|
+
const pos = end - kw.length;
|
|
322
|
+
if (pos < 0 || !/[a-zA-Z0-9_$]/.test(out[pos])) return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
307
328
|
/**
|
|
308
329
|
* Walk JS source and minify the HTML/CSS inside template literals.
|
|
309
330
|
* Handles ${…} interpolations (with nesting) and preserves <pre> blocks.
|
|
@@ -340,6 +361,25 @@ function minifyTemplateLiterals(code) {
|
|
|
340
361
|
continue;
|
|
341
362
|
}
|
|
342
363
|
|
|
364
|
+
// Regex literal: copy verbatim (prevents backticks inside regex from
|
|
365
|
+
// being mistaken for template literals, e.g. /`([^`]+)`/g)
|
|
366
|
+
if (ch === '/' && _isRegexCtxTpl(out)) {
|
|
367
|
+
out += ch; i++;
|
|
368
|
+
let inCharClass = false;
|
|
369
|
+
while (i < code.length) {
|
|
370
|
+
const rc = code[i];
|
|
371
|
+
if (rc === '\\') { out += rc + (code[i + 1] || ''); i += 2; continue; }
|
|
372
|
+
if (rc === '[') inCharClass = true;
|
|
373
|
+
if (rc === ']') inCharClass = false;
|
|
374
|
+
out += rc; i++;
|
|
375
|
+
if (rc === '/' && !inCharClass) {
|
|
376
|
+
while (i < code.length && /[gimsuy]/.test(code[i])) { out += code[i]; i++; }
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
343
383
|
// Template literal: extract, minify HTML, and emit
|
|
344
384
|
if (ch === '`') {
|
|
345
385
|
out += _minifyTemplate(code, i);
|
|
@@ -1181,3 +1221,4 @@ function bundleApp() {
|
|
|
1181
1221
|
|
|
1182
1222
|
module.exports = bundleApp;
|
|
1183
1223
|
module.exports.stripModuleSyntax = stripModuleSyntax;
|
|
1224
|
+
module.exports.minifyTemplateLiterals = minifyTemplateLiterals;
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* Creates the zero-http app, serves static files, injects the
|
|
5
5
|
* error-overlay snippet into HTML responses, and manages the
|
|
6
6
|
* SSE connection pool for live-reload events.
|
|
7
|
+
*
|
|
8
|
+
* Uses zero-http middleware:
|
|
9
|
+
* - helmet() → security headers (relaxed CSP for dev inline scripts)
|
|
10
|
+
* - compress() → brotli/gzip/deflate response compression
|
|
11
|
+
* - cors() → allow cross-origin requests in development
|
|
12
|
+
* - serveStatic() → static file serving with ETag & Cache-Control
|
|
13
|
+
* - SSE with keepAlive, retry, pad for proxy compatibility
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
16
|
'use strict';
|
|
@@ -22,6 +29,7 @@ class SSEPool {
|
|
|
22
29
|
this._clients = new Set();
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
/** @param {import('zero-http').SSEStream} sse */
|
|
25
33
|
add(sse) {
|
|
26
34
|
this._clients.add(sse);
|
|
27
35
|
sse.on('close', () => this._clients.delete(sse));
|
|
@@ -33,6 +41,9 @@ class SSEPool {
|
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
/** Number of connected SSE clients. */
|
|
45
|
+
get size() { return this._clients.size; }
|
|
46
|
+
|
|
36
47
|
closeAll() {
|
|
37
48
|
for (const sse of this._clients) {
|
|
38
49
|
try { sse.close(); } catch { /* ignore */ }
|
|
@@ -92,15 +103,54 @@ async function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
|
92
103
|
zeroHttp = require('zero-http');
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
const {
|
|
106
|
+
const {
|
|
107
|
+
createApp,
|
|
108
|
+
static: serveStatic,
|
|
109
|
+
helmet,
|
|
110
|
+
compress,
|
|
111
|
+
cors,
|
|
112
|
+
debug,
|
|
113
|
+
} = zeroHttp;
|
|
114
|
+
|
|
96
115
|
debug.level('silent');
|
|
97
116
|
|
|
98
117
|
const app = createApp();
|
|
99
118
|
const pool = new SSEPool();
|
|
100
119
|
|
|
120
|
+
// ---- Security headers (dev-friendly CSP) ----
|
|
121
|
+
app.use(helmet({
|
|
122
|
+
contentSecurityPolicy: {
|
|
123
|
+
directives: {
|
|
124
|
+
defaultSrc: ["'self'"],
|
|
125
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
|
126
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
127
|
+
imgSrc: ["'self'", 'data:', 'blob:'],
|
|
128
|
+
connectSrc: ["'self'", 'ws:', 'wss:'],
|
|
129
|
+
fontSrc: ["'self'", 'data:'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
// SPA dev server runs over plain HTTP
|
|
133
|
+
hsts: false,
|
|
134
|
+
// Allow framing for devtools panel
|
|
135
|
+
frameguard: false,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
// ---- CORS (allow cross-origin during development) ----
|
|
139
|
+
app.use(cors());
|
|
140
|
+
|
|
141
|
+
// ---- Compression (brotli > gzip > deflate) ----
|
|
142
|
+
app.use(compress({
|
|
143
|
+
threshold: 1024,
|
|
144
|
+
}));
|
|
145
|
+
|
|
101
146
|
// ---- SSE endpoint ----
|
|
102
147
|
app.get('/__zq_reload', (req, res) => {
|
|
103
|
-
const sse = res.sse({
|
|
148
|
+
const sse = res.sse({
|
|
149
|
+
keepAlive: 30000,
|
|
150
|
+
keepAliveComment: 'ping',
|
|
151
|
+
retry: 3000,
|
|
152
|
+
pad: 2048,
|
|
153
|
+
});
|
|
104
154
|
pool.add(sse);
|
|
105
155
|
});
|
|
106
156
|
|
|
@@ -158,7 +208,10 @@ async function createServer({ root, htmlEntry, port, noIntercept }) {
|
|
|
158
208
|
});
|
|
159
209
|
|
|
160
210
|
function listen(cb) {
|
|
161
|
-
app.listen(port, cb);
|
|
211
|
+
const server = app.listen(port, cb);
|
|
212
|
+
server.keepAliveTimeout = 65000;
|
|
213
|
+
server.headersTimeout = 66000;
|
|
214
|
+
return server;
|
|
162
215
|
}
|
|
163
216
|
|
|
164
217
|
return { app, pool, listen };
|