pulse-js-framework 1.7.5 → 1.7.8
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 +78 -392
- package/cli/dev.js +14 -0
- package/cli/docs-test.js +633 -0
- package/cli/index.js +313 -31
- package/cli/lint.js +13 -4
- package/cli/logger.js +32 -4
- package/cli/release.js +50 -20
- package/compiler/parser.js +1 -1
- package/package.json +11 -4
- package/runtime/dom-advanced.js +357 -0
- package/runtime/dom-binding.js +230 -0
- package/runtime/dom-conditional.js +133 -0
- package/runtime/dom-element.js +142 -0
- package/runtime/dom-lifecycle.js +178 -0
- package/runtime/dom-list.js +267 -0
- package/runtime/dom-selector.js +267 -0
- package/runtime/dom.js +119 -1279
- package/runtime/form.js +417 -22
- package/runtime/native.js +398 -52
- package/runtime/pulse.js +1 -1
- package/runtime/router.js +6 -5
- package/runtime/store.js +81 -6
- package/types/async.d.ts +310 -0
- package/types/form.d.ts +378 -0
- package/types/index.d.ts +44 -0
- /package/{core → runtime}/errors.js +0 -0
package/cli/docs-test.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI - Documentation Test
|
|
3
|
+
*
|
|
4
|
+
* Tests documentation without external dependencies:
|
|
5
|
+
* - JavaScript syntax validation (via vm.Script)
|
|
6
|
+
* - Import resolution verification
|
|
7
|
+
* - HTTP response testing via dev server
|
|
8
|
+
* - .pulse file compilation check
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
12
|
+
import { join, dirname, relative } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { createServer } from 'http';
|
|
15
|
+
import vm from 'vm';
|
|
16
|
+
import { compile } from '../compiler/index.js';
|
|
17
|
+
import { log } from './logger.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const root = join(__dirname, '..');
|
|
21
|
+
const docsDir = join(root, 'docs');
|
|
22
|
+
|
|
23
|
+
// Directories to skip during file collection
|
|
24
|
+
const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'coverage'];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Collect all JS files recursively
|
|
28
|
+
*/
|
|
29
|
+
function collectJsFiles(dir, files = []) {
|
|
30
|
+
if (!existsSync(dir)) return files;
|
|
31
|
+
|
|
32
|
+
for (const entry of readdirSync(dir)) {
|
|
33
|
+
// Skip common non-source directories
|
|
34
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
35
|
+
|
|
36
|
+
const fullPath = join(dir, entry);
|
|
37
|
+
const stat = statSync(fullPath);
|
|
38
|
+
|
|
39
|
+
if (stat.isDirectory()) {
|
|
40
|
+
collectJsFiles(fullPath, files);
|
|
41
|
+
} else if (entry.endsWith('.js')) {
|
|
42
|
+
files.push(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Collect all .pulse files recursively
|
|
51
|
+
*/
|
|
52
|
+
function collectPulseFiles(dir, files = []) {
|
|
53
|
+
if (!existsSync(dir)) return files;
|
|
54
|
+
|
|
55
|
+
for (const entry of readdirSync(dir)) {
|
|
56
|
+
// Skip common non-source directories
|
|
57
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
58
|
+
|
|
59
|
+
const fullPath = join(dir, entry);
|
|
60
|
+
const stat = statSync(fullPath);
|
|
61
|
+
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
collectPulseFiles(fullPath, files);
|
|
64
|
+
} else if (entry.endsWith('.pulse')) {
|
|
65
|
+
files.push(fullPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate JavaScript syntax
|
|
74
|
+
* Uses vm.Script but ignores ESM-specific errors (import/export)
|
|
75
|
+
* since vm.Script doesn't support ESM natively
|
|
76
|
+
*/
|
|
77
|
+
function validateJsSyntax(filePath) {
|
|
78
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
79
|
+
const errors = [];
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Transform ESM to CommonJS-like for syntax checking only
|
|
83
|
+
// This allows us to catch real syntax errors while ignoring ESM syntax
|
|
84
|
+
const transformedContent = content
|
|
85
|
+
// Remove import statements
|
|
86
|
+
.replace(/^import\s+.*$/gm, '// import removed')
|
|
87
|
+
// Replace "export default {" with "const _ = {"
|
|
88
|
+
.replace(/^export\s+default\s+\{/gm, 'const __default__ = {')
|
|
89
|
+
// Replace "export default function" with "function"
|
|
90
|
+
.replace(/^export\s+default\s+function/gm, 'function')
|
|
91
|
+
// Replace "export default class" with "class"
|
|
92
|
+
.replace(/^export\s+default\s+class/gm, 'class')
|
|
93
|
+
// Replace "export default expression" (covers other cases)
|
|
94
|
+
.replace(/^export\s+default\s+/gm, 'const __default__ = ')
|
|
95
|
+
// Replace "export const/let/var" with just "const/let/var"
|
|
96
|
+
.replace(/^export\s+(const|let|var)\s+/gm, '$1 ')
|
|
97
|
+
// Replace "export function" with "function"
|
|
98
|
+
.replace(/^export\s+function\s+/gm, 'function ')
|
|
99
|
+
// Replace "export class" with "class"
|
|
100
|
+
.replace(/^export\s+class\s+/gm, 'class ')
|
|
101
|
+
// Replace "export async function" with "async function"
|
|
102
|
+
.replace(/^export\s+async\s+function\s+/gm, 'async function ')
|
|
103
|
+
// Remove named export statements "export { ... }"
|
|
104
|
+
.replace(/^export\s*\{[^}]*\}.*$/gm, '// export removed');
|
|
105
|
+
|
|
106
|
+
new vm.Script(transformedContent, { filename: filePath });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// Ignore ESM-specific errors that slip through
|
|
109
|
+
const esmErrors = [
|
|
110
|
+
'Cannot use import statement',
|
|
111
|
+
"Unexpected token 'export'",
|
|
112
|
+
'Cannot use import.meta',
|
|
113
|
+
"Unexpected token 'import'"
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const isEsmError = esmErrors.some(msg => error.message.includes(msg));
|
|
117
|
+
|
|
118
|
+
if (!isEsmError) {
|
|
119
|
+
errors.push({
|
|
120
|
+
file: filePath,
|
|
121
|
+
line: error.lineNumber || null,
|
|
122
|
+
column: error.columnNumber || null,
|
|
123
|
+
message: error.message
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return errors;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract real import paths from JavaScript file
|
|
133
|
+
* Filters out imports inside template strings, comments, and code examples
|
|
134
|
+
*/
|
|
135
|
+
function extractImports(content, filePath) {
|
|
136
|
+
const imports = [];
|
|
137
|
+
|
|
138
|
+
// Check if this is a documentation page file (contains code examples)
|
|
139
|
+
const isDocsPage = filePath.includes('pages') && filePath.includes('Page.js');
|
|
140
|
+
|
|
141
|
+
// Remove template strings and their contents to avoid false positives
|
|
142
|
+
// Template strings often contain code examples with fake imports
|
|
143
|
+
let cleanContent = content;
|
|
144
|
+
|
|
145
|
+
if (isDocsPage) {
|
|
146
|
+
// For docs pages, only extract imports at the top of the file (real imports)
|
|
147
|
+
// Stop when we hit the first function declaration or export
|
|
148
|
+
const lines = content.split('\n');
|
|
149
|
+
const importLines = [];
|
|
150
|
+
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
// Stop at first non-import statement (excluding comments and empty lines)
|
|
154
|
+
if (trimmed && !trimmed.startsWith('import') && !trimmed.startsWith('//') && !trimmed.startsWith('/*')) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (trimmed.startsWith('import')) {
|
|
158
|
+
importLines.push(line);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cleanContent = importLines.join('\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Static imports: import x from './path'
|
|
166
|
+
const staticImportRegex = /import\s+(?:[\w{}\s,*]+\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
167
|
+
let match;
|
|
168
|
+
while ((match = staticImportRegex.exec(cleanContent)) !== null) {
|
|
169
|
+
imports.push(match[1]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Only check dynamic imports for non-docs pages to avoid code examples
|
|
173
|
+
if (!isDocsPage) {
|
|
174
|
+
const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
175
|
+
while ((match = dynamicImportRegex.exec(cleanContent)) !== null) {
|
|
176
|
+
imports.push(match[1]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return imports;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve import path relative to file
|
|
185
|
+
*/
|
|
186
|
+
function resolveImport(importPath, fromFile) {
|
|
187
|
+
// Skip external modules (no ./ or ../)
|
|
188
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
189
|
+
return null; // External module, skip
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const fromDir = dirname(fromFile);
|
|
193
|
+
let resolved = join(fromDir, importPath);
|
|
194
|
+
|
|
195
|
+
// Add .js extension if missing
|
|
196
|
+
if (!resolved.endsWith('.js') && !resolved.endsWith('.pulse')) {
|
|
197
|
+
if (existsSync(resolved + '.js')) {
|
|
198
|
+
resolved += '.js';
|
|
199
|
+
} else if (existsSync(resolved + '/index.js')) {
|
|
200
|
+
resolved = join(resolved, 'index.js');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Validate imports in a JavaScript file
|
|
209
|
+
*/
|
|
210
|
+
function validateImports(filePath) {
|
|
211
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
212
|
+
const imports = extractImports(content, filePath);
|
|
213
|
+
const errors = [];
|
|
214
|
+
|
|
215
|
+
for (const importPath of imports) {
|
|
216
|
+
const resolved = resolveImport(importPath, filePath);
|
|
217
|
+
|
|
218
|
+
// Skip external modules
|
|
219
|
+
if (resolved === null) continue;
|
|
220
|
+
|
|
221
|
+
// Skip runtime imports (resolved at runtime)
|
|
222
|
+
if (importPath.includes('/runtime/')) continue;
|
|
223
|
+
|
|
224
|
+
// Skip pulse-js-framework imports
|
|
225
|
+
if (importPath.includes('pulse-js-framework')) continue;
|
|
226
|
+
|
|
227
|
+
if (!existsSync(resolved)) {
|
|
228
|
+
errors.push({
|
|
229
|
+
file: filePath,
|
|
230
|
+
importPath,
|
|
231
|
+
resolved,
|
|
232
|
+
message: `Import not found: ${importPath}`
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return errors;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Compile and validate .pulse files
|
|
242
|
+
*/
|
|
243
|
+
function validatePulseFile(filePath) {
|
|
244
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
245
|
+
const errors = [];
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const result = compile(content, {
|
|
249
|
+
sourceMap: false,
|
|
250
|
+
sourceFileName: filePath
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (!result.success) {
|
|
254
|
+
for (const error of result.errors) {
|
|
255
|
+
errors.push({
|
|
256
|
+
file: filePath,
|
|
257
|
+
line: error.line || null,
|
|
258
|
+
column: error.column || null,
|
|
259
|
+
message: error.message
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
errors.push({
|
|
265
|
+
file: filePath,
|
|
266
|
+
message: error.message
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return errors;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Make HTTP request (native, no dependencies)
|
|
275
|
+
*/
|
|
276
|
+
function httpGet(url, timeout = 5000) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const urlObj = new URL(url);
|
|
279
|
+
const http = urlObj.protocol === 'https:'
|
|
280
|
+
? require('https')
|
|
281
|
+
: require('http');
|
|
282
|
+
|
|
283
|
+
const req = http.get(url, { timeout }, (res) => {
|
|
284
|
+
let data = '';
|
|
285
|
+
res.on('data', chunk => data += chunk);
|
|
286
|
+
res.on('end', () => resolve({ status: res.statusCode, data }));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
req.on('error', reject);
|
|
290
|
+
req.on('timeout', () => {
|
|
291
|
+
req.destroy();
|
|
292
|
+
reject(new Error('Request timeout'));
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Start a minimal test server for docs
|
|
299
|
+
*/
|
|
300
|
+
function startTestServer(port = 0) {
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
const server = createServer((req, res) => {
|
|
303
|
+
// Disable keep-alive for test server
|
|
304
|
+
res.setHeader('Connection', 'close');
|
|
305
|
+
|
|
306
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
307
|
+
let pathname = url.pathname;
|
|
308
|
+
|
|
309
|
+
if (pathname === '/') pathname = '/index.html';
|
|
310
|
+
|
|
311
|
+
const filePath = join(docsDir, pathname);
|
|
312
|
+
|
|
313
|
+
if (existsSync(filePath) && statSync(filePath).isFile()) {
|
|
314
|
+
const content = readFileSync(filePath);
|
|
315
|
+
const ext = pathname.split('.').pop();
|
|
316
|
+
const mimeTypes = {
|
|
317
|
+
'html': 'text/html',
|
|
318
|
+
'js': 'application/javascript',
|
|
319
|
+
'css': 'text/css',
|
|
320
|
+
'json': 'application/json'
|
|
321
|
+
};
|
|
322
|
+
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'text/plain' });
|
|
323
|
+
res.end(content);
|
|
324
|
+
} else {
|
|
325
|
+
res.writeHead(404);
|
|
326
|
+
res.end('Not Found');
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Disable keep-alive timeout
|
|
331
|
+
server.keepAliveTimeout = 0;
|
|
332
|
+
|
|
333
|
+
server.listen(port, '127.0.0.1', () => {
|
|
334
|
+
const addr = server.address();
|
|
335
|
+
resolve({ server, port: addr.port });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
server.on('error', reject);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Test HTTP responses for documentation pages
|
|
344
|
+
*/
|
|
345
|
+
async function testHttpResponses(port, routes) {
|
|
346
|
+
const errors = [];
|
|
347
|
+
const http = await import('http');
|
|
348
|
+
|
|
349
|
+
for (const route of routes) {
|
|
350
|
+
const url = `http://127.0.0.1:${port}${route}`;
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const result = await new Promise((resolve, reject) => {
|
|
354
|
+
const req = http.get(url, { timeout: 5000 }, (res) => {
|
|
355
|
+
let data = '';
|
|
356
|
+
res.on('data', chunk => data += chunk);
|
|
357
|
+
res.on('end', () => resolve({ status: res.statusCode, data }));
|
|
358
|
+
});
|
|
359
|
+
req.on('error', reject);
|
|
360
|
+
req.on('timeout', () => {
|
|
361
|
+
req.destroy();
|
|
362
|
+
reject(new Error('Timeout'));
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (result.status !== 200) {
|
|
367
|
+
errors.push({
|
|
368
|
+
route,
|
|
369
|
+
status: result.status,
|
|
370
|
+
message: `HTTP ${result.status} for ${route}`
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
errors.push({
|
|
375
|
+
route,
|
|
376
|
+
message: `Failed to fetch ${route}: ${error.message}`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return errors;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Run all documentation tests
|
|
386
|
+
* @param {Object} options
|
|
387
|
+
* @param {boolean} options.verbose - Show detailed output
|
|
388
|
+
* @param {boolean} options.httpTest - Run HTTP tests (starts server)
|
|
389
|
+
* @returns {Promise<{success: boolean, errors: Array}>}
|
|
390
|
+
*/
|
|
391
|
+
export async function runDocsTest(options = {}) {
|
|
392
|
+
const { verbose = false, httpTest = true } = options;
|
|
393
|
+
const allErrors = [];
|
|
394
|
+
let passed = 0;
|
|
395
|
+
let failed = 0;
|
|
396
|
+
|
|
397
|
+
log.info('');
|
|
398
|
+
log.info('Documentation Tests');
|
|
399
|
+
log.info('='.repeat(50));
|
|
400
|
+
|
|
401
|
+
// 1. Validate JavaScript syntax
|
|
402
|
+
log.info('');
|
|
403
|
+
log.info('Validating JavaScript syntax...');
|
|
404
|
+
|
|
405
|
+
const jsFiles = collectJsFiles(join(docsDir, 'src'));
|
|
406
|
+
|
|
407
|
+
for (const file of jsFiles) {
|
|
408
|
+
const relPath = relative(docsDir, file);
|
|
409
|
+
const errors = validateJsSyntax(file);
|
|
410
|
+
|
|
411
|
+
if (errors.length > 0) {
|
|
412
|
+
failed++;
|
|
413
|
+
log.error(` ✗ ${relPath}`);
|
|
414
|
+
for (const err of errors) {
|
|
415
|
+
log.error(` ${err.message}`);
|
|
416
|
+
allErrors.push({ type: 'syntax', ...err });
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
passed++;
|
|
420
|
+
if (verbose) log.info(` ✓ ${relPath}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
log.info(` Syntax: ${passed} passed, ${failed} failed`);
|
|
425
|
+
|
|
426
|
+
// 2. Validate imports
|
|
427
|
+
log.info('');
|
|
428
|
+
log.info('Validating imports...');
|
|
429
|
+
|
|
430
|
+
let importPassed = 0;
|
|
431
|
+
let importFailed = 0;
|
|
432
|
+
|
|
433
|
+
for (const file of jsFiles) {
|
|
434
|
+
const relPath = relative(docsDir, file);
|
|
435
|
+
const errors = validateImports(file);
|
|
436
|
+
|
|
437
|
+
if (errors.length > 0) {
|
|
438
|
+
importFailed++;
|
|
439
|
+
log.error(` ✗ ${relPath}`);
|
|
440
|
+
for (const err of errors) {
|
|
441
|
+
log.error(` ${err.message}`);
|
|
442
|
+
allErrors.push({ type: 'import', ...err });
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
importPassed++;
|
|
446
|
+
if (verbose) log.info(` ✓ ${relPath}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
log.info(` Imports: ${importPassed} passed, ${importFailed} failed`);
|
|
451
|
+
|
|
452
|
+
// 3. Check runtime files for package imports (won't work in browser)
|
|
453
|
+
log.info('');
|
|
454
|
+
log.info('Validating runtime imports (browser compatibility)...');
|
|
455
|
+
|
|
456
|
+
const runtimeDir = join(root, 'runtime');
|
|
457
|
+
const runtimeFiles = collectJsFiles(runtimeDir);
|
|
458
|
+
let runtimePassed = 0;
|
|
459
|
+
let runtimeFailed = 0;
|
|
460
|
+
|
|
461
|
+
for (const file of runtimeFiles) {
|
|
462
|
+
const relPath = relative(root, file);
|
|
463
|
+
const content = readFileSync(file, 'utf-8');
|
|
464
|
+
const errors = [];
|
|
465
|
+
|
|
466
|
+
// Find package imports (not starting with . / or /)
|
|
467
|
+
const importRegex = /^import\s+(?:[\w{}\s,*]+\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
468
|
+
let match;
|
|
469
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
470
|
+
const importPath = match[1];
|
|
471
|
+
// Package import = doesn't start with . or /
|
|
472
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
473
|
+
errors.push({
|
|
474
|
+
file,
|
|
475
|
+
importPath,
|
|
476
|
+
message: `Package import "${importPath}" won't work in browser without bundler`
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (errors.length > 0) {
|
|
482
|
+
runtimeFailed++;
|
|
483
|
+
log.error(` ✗ ${relPath}`);
|
|
484
|
+
for (const err of errors) {
|
|
485
|
+
log.error(` ${err.message}`);
|
|
486
|
+
allErrors.push({ type: 'runtime', ...err });
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
runtimePassed++;
|
|
490
|
+
if (verbose) log.info(` ✓ ${relPath}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
log.info(` Runtime: ${runtimePassed} passed, ${runtimeFailed} failed`);
|
|
495
|
+
|
|
496
|
+
// 4. Validate .pulse files (if any in docs)
|
|
497
|
+
const pulseFiles = collectPulseFiles(docsDir);
|
|
498
|
+
|
|
499
|
+
if (pulseFiles.length > 0) {
|
|
500
|
+
log.info('');
|
|
501
|
+
log.info('Validating .pulse files...');
|
|
502
|
+
|
|
503
|
+
let pulsePassed = 0;
|
|
504
|
+
let pulseFailed = 0;
|
|
505
|
+
|
|
506
|
+
for (const file of pulseFiles) {
|
|
507
|
+
const relPath = relative(docsDir, file);
|
|
508
|
+
const errors = validatePulseFile(file);
|
|
509
|
+
|
|
510
|
+
if (errors.length > 0) {
|
|
511
|
+
pulseFailed++;
|
|
512
|
+
log.error(` ✗ ${relPath}`);
|
|
513
|
+
for (const err of errors) {
|
|
514
|
+
log.error(` ${err.message}`);
|
|
515
|
+
allErrors.push({ type: 'pulse', ...err });
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
pulsePassed++;
|
|
519
|
+
if (verbose) log.info(` ✓ ${relPath}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
log.info(` Pulse: ${pulsePassed} passed, ${pulseFailed} failed`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 4. HTTP response tests
|
|
527
|
+
if (httpTest) {
|
|
528
|
+
log.info('');
|
|
529
|
+
log.info('Testing HTTP responses...');
|
|
530
|
+
|
|
531
|
+
let serverInfo = null;
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
serverInfo = await startTestServer();
|
|
535
|
+
const port = serverInfo.port;
|
|
536
|
+
|
|
537
|
+
// Test essential routes
|
|
538
|
+
const routes = [
|
|
539
|
+
'/index.html',
|
|
540
|
+
'/src/main.js',
|
|
541
|
+
'/src/state.js',
|
|
542
|
+
'/src/styles.js'
|
|
543
|
+
];
|
|
544
|
+
|
|
545
|
+
const httpErrors = await testHttpResponses(port, routes);
|
|
546
|
+
|
|
547
|
+
if (httpErrors.length > 0) {
|
|
548
|
+
for (const err of httpErrors) {
|
|
549
|
+
log.error(` ✗ ${err.route}: ${err.message}`);
|
|
550
|
+
allErrors.push({ type: 'http', ...err });
|
|
551
|
+
}
|
|
552
|
+
log.info(` HTTP: ${routes.length - httpErrors.length} passed, ${httpErrors.length} failed`);
|
|
553
|
+
} else {
|
|
554
|
+
log.info(` HTTP: ${routes.length} passed, 0 failed`);
|
|
555
|
+
}
|
|
556
|
+
} catch (error) {
|
|
557
|
+
log.warn(` HTTP tests skipped: ${error.message}`);
|
|
558
|
+
} finally {
|
|
559
|
+
if (serverInfo?.server) {
|
|
560
|
+
// Force close all connections and server
|
|
561
|
+
await new Promise((resolve) => {
|
|
562
|
+
serverInfo.server.close(() => resolve());
|
|
563
|
+
// Force close after 1 second if not closed
|
|
564
|
+
setTimeout(resolve, 1000);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 5. Compile examples .pulse files
|
|
571
|
+
const examplesDir = join(root, 'examples');
|
|
572
|
+
const examplePulseFiles = collectPulseFiles(examplesDir);
|
|
573
|
+
|
|
574
|
+
if (examplePulseFiles.length > 0) {
|
|
575
|
+
log.info('');
|
|
576
|
+
log.info('Validating example .pulse files...');
|
|
577
|
+
|
|
578
|
+
let examplePassed = 0;
|
|
579
|
+
let exampleFailed = 0;
|
|
580
|
+
|
|
581
|
+
for (const file of examplePulseFiles) {
|
|
582
|
+
const relPath = relative(root, file);
|
|
583
|
+
const errors = validatePulseFile(file);
|
|
584
|
+
|
|
585
|
+
if (errors.length > 0) {
|
|
586
|
+
exampleFailed++;
|
|
587
|
+
log.error(` ✗ ${relPath}`);
|
|
588
|
+
for (const err of errors) {
|
|
589
|
+
log.error(` ${err.message}`);
|
|
590
|
+
allErrors.push({ type: 'example', ...err });
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
examplePassed++;
|
|
594
|
+
if (verbose) log.info(` ✓ ${relPath}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
log.info(` Examples: ${examplePassed} passed, ${exampleFailed} failed`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Summary
|
|
602
|
+
log.info('');
|
|
603
|
+
log.info('-'.repeat(50));
|
|
604
|
+
|
|
605
|
+
const success = allErrors.length === 0;
|
|
606
|
+
|
|
607
|
+
if (success) {
|
|
608
|
+
log.success('All documentation tests passed!');
|
|
609
|
+
} else {
|
|
610
|
+
log.error(`Documentation tests failed: ${allErrors.length} error(s)`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
log.info('');
|
|
614
|
+
|
|
615
|
+
return { success, errors: allErrors };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* CLI entry point
|
|
620
|
+
*/
|
|
621
|
+
export async function runDocsTestCli(args) {
|
|
622
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
623
|
+
const noHttp = args.includes('--no-http');
|
|
624
|
+
|
|
625
|
+
const result = await runDocsTest({
|
|
626
|
+
verbose,
|
|
627
|
+
httpTest: !noHttp
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
if (!result.success) {
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
}
|