pulse-js-framework 1.7.11 → 1.7.13
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 +80 -8
- package/cli/docs.js +712 -0
- package/cli/doctor.js +702 -0
- package/cli/index.js +338 -65
- package/cli/scaffold.js +1037 -0
- package/cli/test.js +455 -0
- package/package.json +19 -2
- package/runtime/a11y.js +824 -1
- package/runtime/context.js +374 -0
- package/runtime/graphql.js +1356 -0
- package/runtime/index.js +6 -0
- package/runtime/logger.js +2 -1
- package/runtime/websocket.js +874 -0
- package/types/context.d.ts +171 -0
- package/types/graphql.d.ts +490 -0
- package/types/index.d.ts +15 -0
- package/types/websocket.d.ts +347 -0
package/cli/docs.js
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI - Docs Command
|
|
3
|
+
* Generate API documentation from JSDoc comments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
7
|
+
import { join, relative, dirname, basename, extname } from 'path';
|
|
8
|
+
import { log } from './logger.js';
|
|
9
|
+
import { parseArgs, formatBytes } from './utils/file-utils.js';
|
|
10
|
+
import { createTimer, formatDuration } from './utils/cli-ui.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* JSDoc tag types
|
|
14
|
+
*/
|
|
15
|
+
const JSDOC_TAGS = [
|
|
16
|
+
'param', 'parameter', 'arg', 'argument',
|
|
17
|
+
'returns', 'return',
|
|
18
|
+
'type', 'typedef',
|
|
19
|
+
'property', 'prop',
|
|
20
|
+
'example',
|
|
21
|
+
'throws', 'exception',
|
|
22
|
+
'deprecated',
|
|
23
|
+
'see', 'link',
|
|
24
|
+
'since', 'version',
|
|
25
|
+
'author',
|
|
26
|
+
'module', 'namespace',
|
|
27
|
+
'exports', 'export',
|
|
28
|
+
'class', 'constructor',
|
|
29
|
+
'function', 'func', 'method',
|
|
30
|
+
'private', 'protected', 'public',
|
|
31
|
+
'static',
|
|
32
|
+
'async',
|
|
33
|
+
'callback',
|
|
34
|
+
'template',
|
|
35
|
+
'default', 'defaultvalue'
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse JSDoc comment block
|
|
40
|
+
* @param {string} comment - Raw JSDoc comment
|
|
41
|
+
* @returns {Object} Parsed JSDoc data
|
|
42
|
+
*/
|
|
43
|
+
function parseJSDocComment(comment) {
|
|
44
|
+
// Remove comment markers
|
|
45
|
+
const lines = comment
|
|
46
|
+
.replace(/^\/\*\*\s*/, '')
|
|
47
|
+
.replace(/\s*\*\/$/, '')
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map(line => line.replace(/^\s*\*\s?/, ''));
|
|
50
|
+
|
|
51
|
+
const result = {
|
|
52
|
+
description: '',
|
|
53
|
+
tags: []
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let currentTag = null;
|
|
57
|
+
let descriptionLines = [];
|
|
58
|
+
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const tagMatch = line.match(/^@(\w+)\s*(.*)?$/);
|
|
61
|
+
|
|
62
|
+
if (tagMatch) {
|
|
63
|
+
// Save previous tag
|
|
64
|
+
if (currentTag) {
|
|
65
|
+
result.tags.push(currentTag);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Start new tag
|
|
69
|
+
const [, tagName, tagContent] = tagMatch;
|
|
70
|
+
currentTag = {
|
|
71
|
+
tag: tagName,
|
|
72
|
+
content: tagContent || ''
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Parse specific tag formats
|
|
76
|
+
if (['param', 'parameter', 'arg', 'argument'].includes(tagName)) {
|
|
77
|
+
const paramMatch = tagContent?.match(/^\{([^}]+)\}\s*(\[?[\w.]+\]?)\s*-?\s*(.*)$/);
|
|
78
|
+
if (paramMatch) {
|
|
79
|
+
const [, type, name, desc] = paramMatch;
|
|
80
|
+
const optional = name.startsWith('[') && name.endsWith(']');
|
|
81
|
+
currentTag.type = type;
|
|
82
|
+
currentTag.name = name.replace(/^\[|\]$/g, '').split('=')[0];
|
|
83
|
+
currentTag.description = desc;
|
|
84
|
+
currentTag.optional = optional;
|
|
85
|
+
if (name.includes('=')) {
|
|
86
|
+
currentTag.default = name.split('=')[1].replace(/\]$/, '');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else if (['returns', 'return'].includes(tagName)) {
|
|
90
|
+
const returnMatch = tagContent?.match(/^\{([^}]+)\}\s*(.*)$/);
|
|
91
|
+
if (returnMatch) {
|
|
92
|
+
const [, type, desc] = returnMatch;
|
|
93
|
+
currentTag.type = type;
|
|
94
|
+
currentTag.description = desc;
|
|
95
|
+
}
|
|
96
|
+
} else if (['type', 'typedef'].includes(tagName)) {
|
|
97
|
+
const typeMatch = tagContent?.match(/^\{([^}]+)\}\s*(.*)$/);
|
|
98
|
+
if (typeMatch) {
|
|
99
|
+
currentTag.type = typeMatch[1];
|
|
100
|
+
currentTag.name = typeMatch[2];
|
|
101
|
+
}
|
|
102
|
+
} else if (['property', 'prop'].includes(tagName)) {
|
|
103
|
+
const propMatch = tagContent?.match(/^\{([^}]+)\}\s*([\w.]+)\s*-?\s*(.*)$/);
|
|
104
|
+
if (propMatch) {
|
|
105
|
+
const [, type, name, desc] = propMatch;
|
|
106
|
+
currentTag.type = type;
|
|
107
|
+
currentTag.name = name;
|
|
108
|
+
currentTag.description = desc;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else if (currentTag) {
|
|
112
|
+
// Continue current tag
|
|
113
|
+
currentTag.content += '\n' + line;
|
|
114
|
+
if (currentTag.description !== undefined) {
|
|
115
|
+
currentTag.description += '\n' + line;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Description line
|
|
119
|
+
descriptionLines.push(line);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Save last tag
|
|
124
|
+
if (currentTag) {
|
|
125
|
+
result.tags.push(currentTag);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Clean up description
|
|
129
|
+
result.description = descriptionLines.join('\n').trim();
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract JSDoc comments from source code
|
|
136
|
+
* @param {string} source - Source code
|
|
137
|
+
* @returns {Array} Array of JSDoc entries
|
|
138
|
+
*/
|
|
139
|
+
function extractJSDocEntries(source) {
|
|
140
|
+
const entries = [];
|
|
141
|
+
const jsdocRegex = /\/\*\*[\s\S]*?\*\/\s*(?:export\s+)?(?:(async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=|class\s+(\w+)|(\w+)\s*[:(])/g;
|
|
142
|
+
|
|
143
|
+
let match;
|
|
144
|
+
while ((match = jsdocRegex.exec(source)) !== null) {
|
|
145
|
+
const fullMatch = match[0];
|
|
146
|
+
const commentEnd = fullMatch.indexOf('*/') + 2;
|
|
147
|
+
const comment = fullMatch.slice(0, commentEnd);
|
|
148
|
+
const afterComment = fullMatch.slice(commentEnd).trim();
|
|
149
|
+
|
|
150
|
+
// Determine the name and type of the documented item
|
|
151
|
+
let name = match[2] || match[3] || match[4] || match[5];
|
|
152
|
+
let kind = 'unknown';
|
|
153
|
+
|
|
154
|
+
if (match[2]) {
|
|
155
|
+
kind = match[1] ? 'async function' : 'function';
|
|
156
|
+
} else if (match[4]) {
|
|
157
|
+
kind = 'class';
|
|
158
|
+
} else if (match[3]) {
|
|
159
|
+
// Check what's assigned
|
|
160
|
+
if (afterComment.includes('function') || afterComment.includes('=>')) {
|
|
161
|
+
kind = 'function';
|
|
162
|
+
} else if (afterComment.includes('class')) {
|
|
163
|
+
kind = 'class';
|
|
164
|
+
} else {
|
|
165
|
+
kind = 'const';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const parsed = parseJSDocComment(comment);
|
|
170
|
+
|
|
171
|
+
entries.push({
|
|
172
|
+
name,
|
|
173
|
+
kind,
|
|
174
|
+
...parsed,
|
|
175
|
+
line: source.slice(0, match.index).split('\n').length
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return entries;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Process a JavaScript/TypeScript file
|
|
184
|
+
* @param {string} filePath - Path to file
|
|
185
|
+
* @returns {Object} Processed file data
|
|
186
|
+
*/
|
|
187
|
+
function processFile(filePath) {
|
|
188
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
189
|
+
const entries = extractJSDocEntries(source);
|
|
190
|
+
|
|
191
|
+
// Find module-level JSDoc
|
|
192
|
+
const moduleDocMatch = source.match(/^\/\*\*[\s\S]*?\*\/\s*\n(?!\s*(export|function|class|const|let|var))/);
|
|
193
|
+
let moduleDoc = null;
|
|
194
|
+
|
|
195
|
+
if (moduleDocMatch) {
|
|
196
|
+
moduleDoc = parseJSDocComment(moduleDocMatch[0]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
path: filePath,
|
|
201
|
+
name: basename(filePath, extname(filePath)),
|
|
202
|
+
moduleDoc,
|
|
203
|
+
entries
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Find JavaScript files in directory
|
|
209
|
+
* @param {string} dir - Directory to search
|
|
210
|
+
* @param {Object} options - Search options
|
|
211
|
+
* @returns {string[]} Array of file paths
|
|
212
|
+
*/
|
|
213
|
+
function findJsFiles(dir, options = {}) {
|
|
214
|
+
const { extensions = ['.js', '.mjs', '.ts'], exclude = ['node_modules', '.git', 'dist', 'coverage'] } = options;
|
|
215
|
+
const files = [];
|
|
216
|
+
|
|
217
|
+
function walk(currentDir) {
|
|
218
|
+
try {
|
|
219
|
+
const entries = readdirSync(currentDir);
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
if (exclude.includes(entry) || entry.startsWith('.')) continue;
|
|
222
|
+
|
|
223
|
+
const fullPath = join(currentDir, entry);
|
|
224
|
+
try {
|
|
225
|
+
const stat = statSync(fullPath);
|
|
226
|
+
if (stat.isDirectory()) {
|
|
227
|
+
walk(fullPath);
|
|
228
|
+
} else if (extensions.some(ext => entry.endsWith(ext))) {
|
|
229
|
+
files.push(fullPath);
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
// Skip inaccessible files
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
// Skip inaccessible directories
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
walk(dir);
|
|
241
|
+
return files;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate Markdown documentation
|
|
246
|
+
* @param {Object} fileData - Processed file data
|
|
247
|
+
* @param {Object} options - Generation options
|
|
248
|
+
* @returns {string} Markdown content
|
|
249
|
+
*/
|
|
250
|
+
function generateMarkdown(fileData, options = {}) {
|
|
251
|
+
const { includeSource = false } = options;
|
|
252
|
+
let md = '';
|
|
253
|
+
|
|
254
|
+
// Module header
|
|
255
|
+
md += `# ${fileData.name}\n\n`;
|
|
256
|
+
|
|
257
|
+
if (fileData.moduleDoc) {
|
|
258
|
+
md += `${fileData.moduleDoc.description}\n\n`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
md += `**File:** \`${relative(process.cwd(), fileData.path)}\`\n\n`;
|
|
262
|
+
|
|
263
|
+
// Table of contents
|
|
264
|
+
if (fileData.entries.length > 0) {
|
|
265
|
+
md += `## Table of Contents\n\n`;
|
|
266
|
+
for (const entry of fileData.entries) {
|
|
267
|
+
const anchor = entry.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
268
|
+
md += `- [${entry.name}](#${anchor})\n`;
|
|
269
|
+
}
|
|
270
|
+
md += '\n---\n\n';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Document each entry
|
|
274
|
+
for (const entry of fileData.entries) {
|
|
275
|
+
md += `## ${entry.name}\n\n`;
|
|
276
|
+
|
|
277
|
+
// Kind badge
|
|
278
|
+
const kindBadge = `\`${entry.kind}\``;
|
|
279
|
+
md += `${kindBadge}\n\n`;
|
|
280
|
+
|
|
281
|
+
// Description
|
|
282
|
+
if (entry.description) {
|
|
283
|
+
md += `${entry.description}\n\n`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Parameters
|
|
287
|
+
const params = entry.tags.filter(t => ['param', 'parameter', 'arg', 'argument'].includes(t.tag));
|
|
288
|
+
if (params.length > 0) {
|
|
289
|
+
md += `### Parameters\n\n`;
|
|
290
|
+
md += `| Name | Type | Description |\n`;
|
|
291
|
+
md += `|------|------|-------------|\n`;
|
|
292
|
+
for (const param of params) {
|
|
293
|
+
const optional = param.optional ? ' (optional)' : '';
|
|
294
|
+
const defaultVal = param.default ? ` = \`${param.default}\`` : '';
|
|
295
|
+
md += `| \`${param.name}\`${optional}${defaultVal} | \`${param.type || 'any'}\` | ${(param.description || '').replace(/\n/g, ' ')} |\n`;
|
|
296
|
+
}
|
|
297
|
+
md += '\n';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Returns
|
|
301
|
+
const returns = entry.tags.find(t => ['returns', 'return'].includes(t.tag));
|
|
302
|
+
if (returns) {
|
|
303
|
+
md += `### Returns\n\n`;
|
|
304
|
+
md += `\`${returns.type || 'void'}\``;
|
|
305
|
+
if (returns.description) {
|
|
306
|
+
md += ` - ${returns.description.replace(/\n/g, ' ')}`;
|
|
307
|
+
}
|
|
308
|
+
md += '\n\n';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Throws
|
|
312
|
+
const throws = entry.tags.filter(t => ['throws', 'exception'].includes(t.tag));
|
|
313
|
+
if (throws.length > 0) {
|
|
314
|
+
md += `### Throws\n\n`;
|
|
315
|
+
for (const t of throws) {
|
|
316
|
+
md += `- ${t.content}\n`;
|
|
317
|
+
}
|
|
318
|
+
md += '\n';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Examples
|
|
322
|
+
const examples = entry.tags.filter(t => t.tag === 'example');
|
|
323
|
+
if (examples.length > 0) {
|
|
324
|
+
md += `### Example${examples.length > 1 ? 's' : ''}\n\n`;
|
|
325
|
+
for (const example of examples) {
|
|
326
|
+
md += `\`\`\`javascript\n${example.content.trim()}\n\`\`\`\n\n`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Deprecated
|
|
331
|
+
const deprecated = entry.tags.find(t => t.tag === 'deprecated');
|
|
332
|
+
if (deprecated) {
|
|
333
|
+
md += `> **Deprecated:** ${deprecated.content || 'This is deprecated.'}\n\n`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// See also
|
|
337
|
+
const seeAlso = entry.tags.filter(t => ['see', 'link'].includes(t.tag));
|
|
338
|
+
if (seeAlso.length > 0) {
|
|
339
|
+
md += `### See Also\n\n`;
|
|
340
|
+
for (const see of seeAlso) {
|
|
341
|
+
md += `- ${see.content}\n`;
|
|
342
|
+
}
|
|
343
|
+
md += '\n';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
md += '---\n\n';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return md;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Generate JSON documentation
|
|
354
|
+
* @param {Object[]} filesData - Array of processed file data
|
|
355
|
+
* @returns {Object} JSON documentation
|
|
356
|
+
*/
|
|
357
|
+
function generateJson(filesData) {
|
|
358
|
+
return {
|
|
359
|
+
generated: new Date().toISOString(),
|
|
360
|
+
version: '1.0.0',
|
|
361
|
+
modules: filesData.map(f => ({
|
|
362
|
+
name: f.name,
|
|
363
|
+
path: relative(process.cwd(), f.path),
|
|
364
|
+
description: f.moduleDoc?.description || null,
|
|
365
|
+
exports: f.entries.map(e => ({
|
|
366
|
+
name: e.name,
|
|
367
|
+
kind: e.kind,
|
|
368
|
+
description: e.description,
|
|
369
|
+
line: e.line,
|
|
370
|
+
params: e.tags
|
|
371
|
+
.filter(t => ['param', 'parameter'].includes(t.tag))
|
|
372
|
+
.map(p => ({
|
|
373
|
+
name: p.name,
|
|
374
|
+
type: p.type,
|
|
375
|
+
description: p.description,
|
|
376
|
+
optional: p.optional || false,
|
|
377
|
+
default: p.default || null
|
|
378
|
+
})),
|
|
379
|
+
returns: (() => {
|
|
380
|
+
const ret = e.tags.find(t => ['returns', 'return'].includes(t.tag));
|
|
381
|
+
return ret ? { type: ret.type, description: ret.description } : null;
|
|
382
|
+
})(),
|
|
383
|
+
examples: e.tags
|
|
384
|
+
.filter(t => t.tag === 'example')
|
|
385
|
+
.map(t => t.content.trim()),
|
|
386
|
+
deprecated: e.tags.find(t => t.tag === 'deprecated')?.content || null,
|
|
387
|
+
since: e.tags.find(t => t.tag === 'since')?.content || null
|
|
388
|
+
}))
|
|
389
|
+
}))
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Generate HTML documentation
|
|
395
|
+
* @param {Object[]} filesData - Array of processed file data
|
|
396
|
+
* @param {Object} options - Generation options
|
|
397
|
+
* @returns {string} HTML content
|
|
398
|
+
*/
|
|
399
|
+
function generateHtml(filesData, options = {}) {
|
|
400
|
+
const { title = 'API Documentation' } = options;
|
|
401
|
+
|
|
402
|
+
let html = `<!DOCTYPE html>
|
|
403
|
+
<html lang="en">
|
|
404
|
+
<head>
|
|
405
|
+
<meta charset="UTF-8">
|
|
406
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
407
|
+
<title>${title}</title>
|
|
408
|
+
<style>
|
|
409
|
+
:root {
|
|
410
|
+
--bg: #ffffff;
|
|
411
|
+
--text: #1a1a1a;
|
|
412
|
+
--code-bg: #f5f5f5;
|
|
413
|
+
--border: #e0e0e0;
|
|
414
|
+
--accent: #646cff;
|
|
415
|
+
--link: #646cff;
|
|
416
|
+
}
|
|
417
|
+
@media (prefers-color-scheme: dark) {
|
|
418
|
+
:root {
|
|
419
|
+
--bg: #1a1a1a;
|
|
420
|
+
--text: #ffffff;
|
|
421
|
+
--code-bg: #2d2d2d;
|
|
422
|
+
--border: #404040;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
426
|
+
body {
|
|
427
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
428
|
+
background: var(--bg);
|
|
429
|
+
color: var(--text);
|
|
430
|
+
line-height: 1.6;
|
|
431
|
+
}
|
|
432
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
433
|
+
h1 { font-size: 2.5rem; margin-bottom: 1rem; color: var(--accent); }
|
|
434
|
+
h2 { font-size: 1.8rem; margin: 2rem 0 1rem; border-bottom: 2px solid var(--border); padding-bottom: 0.5rem; }
|
|
435
|
+
h3 { font-size: 1.3rem; margin: 1.5rem 0 0.5rem; }
|
|
436
|
+
h4 { font-size: 1.1rem; margin: 1rem 0 0.5rem; color: var(--accent); }
|
|
437
|
+
p { margin: 0.5rem 0; }
|
|
438
|
+
code { background: var(--code-bg); padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
|
|
439
|
+
pre { background: var(--code-bg); padding: 1rem; border-radius: 8px; overflow-x: auto; margin: 1rem 0; }
|
|
440
|
+
pre code { padding: 0; background: none; }
|
|
441
|
+
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
|
442
|
+
th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid var(--border); }
|
|
443
|
+
th { background: var(--code-bg); font-weight: 600; }
|
|
444
|
+
.badge { display: inline-block; padding: 0.2em 0.6em; border-radius: 4px; background: var(--accent); color: white; font-size: 0.8em; margin-left: 0.5rem; }
|
|
445
|
+
.deprecated { background: #dc3545; }
|
|
446
|
+
.nav { position: fixed; left: 0; top: 0; width: 250px; height: 100vh; background: var(--code-bg); padding: 1rem; overflow-y: auto; border-right: 1px solid var(--border); }
|
|
447
|
+
.nav h3 { margin-bottom: 0.5rem; }
|
|
448
|
+
.nav ul { list-style: none; }
|
|
449
|
+
.nav li { margin: 0.25rem 0; }
|
|
450
|
+
.nav a { color: var(--link); text-decoration: none; }
|
|
451
|
+
.nav a:hover { text-decoration: underline; }
|
|
452
|
+
.main { margin-left: 270px; }
|
|
453
|
+
@media (max-width: 768px) { .nav { display: none; } .main { margin-left: 0; } }
|
|
454
|
+
.entry { margin-bottom: 3rem; }
|
|
455
|
+
</style>
|
|
456
|
+
</head>
|
|
457
|
+
<body>
|
|
458
|
+
<nav class="nav">
|
|
459
|
+
<h3>Modules</h3>
|
|
460
|
+
<ul>
|
|
461
|
+
`;
|
|
462
|
+
|
|
463
|
+
// Navigation
|
|
464
|
+
for (const file of filesData) {
|
|
465
|
+
html += ` <li><strong>${file.name}</strong>\n <ul>\n`;
|
|
466
|
+
for (const entry of file.entries) {
|
|
467
|
+
const anchor = `${file.name}-${entry.name}`.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
468
|
+
html += ` <li><a href="#${anchor}">${entry.name}</a></li>\n`;
|
|
469
|
+
}
|
|
470
|
+
html += ` </ul>\n </li>\n`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
html += ` </ul>
|
|
474
|
+
</nav>
|
|
475
|
+
<main class="main">
|
|
476
|
+
<div class="container">
|
|
477
|
+
<h1>${title}</h1>
|
|
478
|
+
<p>Generated: ${new Date().toLocaleString()}</p>
|
|
479
|
+
`;
|
|
480
|
+
|
|
481
|
+
// Content
|
|
482
|
+
for (const file of filesData) {
|
|
483
|
+
html += ` <section>
|
|
484
|
+
<h2 id="${file.name.toLowerCase()}">${file.name}</h2>
|
|
485
|
+
<p><code>${relative(process.cwd(), file.path)}</code></p>
|
|
486
|
+
`;
|
|
487
|
+
|
|
488
|
+
if (file.moduleDoc?.description) {
|
|
489
|
+
html += ` <p>${escapeHtml(file.moduleDoc.description)}</p>\n`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (const entry of file.entries) {
|
|
493
|
+
const anchor = `${file.name}-${entry.name}`.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
494
|
+
const deprecated = entry.tags.find(t => t.tag === 'deprecated');
|
|
495
|
+
|
|
496
|
+
html += ` <div class="entry" id="${anchor}">
|
|
497
|
+
<h3>${entry.name}<span class="badge">${entry.kind}</span>${deprecated ? '<span class="badge deprecated">deprecated</span>' : ''}</h3>
|
|
498
|
+
`;
|
|
499
|
+
|
|
500
|
+
if (entry.description) {
|
|
501
|
+
html += ` <p>${escapeHtml(entry.description)}</p>\n`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Parameters
|
|
505
|
+
const params = entry.tags.filter(t => ['param', 'parameter'].includes(t.tag));
|
|
506
|
+
if (params.length > 0) {
|
|
507
|
+
html += ` <h4>Parameters</h4>
|
|
508
|
+
<table>
|
|
509
|
+
<tr><th>Name</th><th>Type</th><th>Description</th></tr>
|
|
510
|
+
`;
|
|
511
|
+
for (const p of params) {
|
|
512
|
+
const opt = p.optional ? ' (optional)' : '';
|
|
513
|
+
const def = p.default ? ` = <code>${escapeHtml(p.default)}</code>` : '';
|
|
514
|
+
html += ` <tr><td><code>${p.name}</code>${opt}${def}</td><td><code>${escapeHtml(p.type || 'any')}</code></td><td>${escapeHtml(p.description || '')}</td></tr>\n`;
|
|
515
|
+
}
|
|
516
|
+
html += ` </table>\n`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Returns
|
|
520
|
+
const returns = entry.tags.find(t => ['returns', 'return'].includes(t.tag));
|
|
521
|
+
if (returns) {
|
|
522
|
+
html += ` <h4>Returns</h4>
|
|
523
|
+
<p><code>${escapeHtml(returns.type || 'void')}</code>${returns.description ? ` - ${escapeHtml(returns.description)}` : ''}</p>\n`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Examples
|
|
527
|
+
const examples = entry.tags.filter(t => t.tag === 'example');
|
|
528
|
+
if (examples.length > 0) {
|
|
529
|
+
html += ` <h4>Example${examples.length > 1 ? 's' : ''}</h4>\n`;
|
|
530
|
+
for (const ex of examples) {
|
|
531
|
+
html += ` <pre><code>${escapeHtml(ex.content.trim())}</code></pre>\n`;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
html += ` </div>\n`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
html += ` </section>\n`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
html += ` </div>
|
|
542
|
+
</main>
|
|
543
|
+
</body>
|
|
544
|
+
</html>`;
|
|
545
|
+
|
|
546
|
+
return html;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Escape HTML special characters
|
|
551
|
+
*/
|
|
552
|
+
function escapeHtml(str) {
|
|
553
|
+
if (!str) return '';
|
|
554
|
+
return str
|
|
555
|
+
.replace(/&/g, '&')
|
|
556
|
+
.replace(/</g, '<')
|
|
557
|
+
.replace(/>/g, '>')
|
|
558
|
+
.replace(/"/g, '"');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Main docs command handler
|
|
563
|
+
*/
|
|
564
|
+
export async function runDocs(args) {
|
|
565
|
+
const { options, patterns } = parseArgs(args);
|
|
566
|
+
|
|
567
|
+
const generate = options.generate || options.g || false;
|
|
568
|
+
const format = options.format || options.f || 'markdown';
|
|
569
|
+
const output = options.output || options.o || 'docs/api';
|
|
570
|
+
const title = options.title || 'API Documentation';
|
|
571
|
+
const verbose = options.verbose || options.v || false;
|
|
572
|
+
|
|
573
|
+
// If not generating, show help
|
|
574
|
+
if (!generate) {
|
|
575
|
+
showDocsHelp();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const timer = createTimer();
|
|
580
|
+
|
|
581
|
+
// Find source files
|
|
582
|
+
const inputPaths = patterns.length > 0 ? patterns : ['src', 'runtime'];
|
|
583
|
+
const allFiles = [];
|
|
584
|
+
|
|
585
|
+
for (const inputPath of inputPaths) {
|
|
586
|
+
const fullPath = join(process.cwd(), inputPath);
|
|
587
|
+
if (!existsSync(fullPath)) {
|
|
588
|
+
log.warn(`Path not found: ${inputPath}`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const stat = statSync(fullPath);
|
|
593
|
+
if (stat.isDirectory()) {
|
|
594
|
+
allFiles.push(...findJsFiles(fullPath));
|
|
595
|
+
} else if (stat.isFile()) {
|
|
596
|
+
allFiles.push(fullPath);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (allFiles.length === 0) {
|
|
601
|
+
log.error('No JavaScript files found to document.');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
log.info(`Processing ${allFiles.length} file(s)...\n`);
|
|
606
|
+
|
|
607
|
+
// Process files
|
|
608
|
+
const filesData = [];
|
|
609
|
+
let totalEntries = 0;
|
|
610
|
+
|
|
611
|
+
for (const file of allFiles) {
|
|
612
|
+
const data = processFile(file);
|
|
613
|
+
if (data.entries.length > 0) {
|
|
614
|
+
filesData.push(data);
|
|
615
|
+
totalEntries += data.entries.length;
|
|
616
|
+
|
|
617
|
+
if (verbose) {
|
|
618
|
+
log.info(` ${relative(process.cwd(), file)}: ${data.entries.length} entries`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (filesData.length === 0) {
|
|
624
|
+
log.warn('No documented exports found.');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Create output directory
|
|
629
|
+
const outputPath = join(process.cwd(), output);
|
|
630
|
+
if (!existsSync(outputPath)) {
|
|
631
|
+
mkdirSync(outputPath, { recursive: true });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Generate documentation
|
|
635
|
+
if (format === 'json') {
|
|
636
|
+
const json = generateJson(filesData);
|
|
637
|
+
const jsonPath = join(outputPath, 'api.json');
|
|
638
|
+
writeFileSync(jsonPath, JSON.stringify(json, null, 2));
|
|
639
|
+
log.success(`Generated: ${relative(process.cwd(), jsonPath)}`);
|
|
640
|
+
} else if (format === 'html') {
|
|
641
|
+
const html = generateHtml(filesData, { title });
|
|
642
|
+
const htmlPath = join(outputPath, 'index.html');
|
|
643
|
+
writeFileSync(htmlPath, html);
|
|
644
|
+
log.success(`Generated: ${relative(process.cwd(), htmlPath)}`);
|
|
645
|
+
} else {
|
|
646
|
+
// Default: Markdown (one file per module)
|
|
647
|
+
for (const fileData of filesData) {
|
|
648
|
+
const md = generateMarkdown(fileData, options);
|
|
649
|
+
const mdPath = join(outputPath, `${fileData.name}.md`);
|
|
650
|
+
writeFileSync(mdPath, md);
|
|
651
|
+
log.success(`Generated: ${relative(process.cwd(), mdPath)}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Generate index
|
|
655
|
+
let indexMd = `# API Documentation\n\n`;
|
|
656
|
+
indexMd += `Generated: ${new Date().toLocaleString()}\n\n`;
|
|
657
|
+
indexMd += `## Modules\n\n`;
|
|
658
|
+
|
|
659
|
+
for (const fileData of filesData) {
|
|
660
|
+
indexMd += `- [${fileData.name}](./${fileData.name}.md)`;
|
|
661
|
+
if (fileData.moduleDoc?.description) {
|
|
662
|
+
const shortDesc = fileData.moduleDoc.description.split('\n')[0].slice(0, 80);
|
|
663
|
+
indexMd += ` - ${shortDesc}`;
|
|
664
|
+
}
|
|
665
|
+
indexMd += '\n';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
writeFileSync(join(outputPath, 'README.md'), indexMd);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const elapsed = formatDuration(timer.elapsed());
|
|
672
|
+
log.info(`\nDocumented ${totalEntries} exports from ${filesData.length} module(s) in ${elapsed}`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Show docs help
|
|
677
|
+
*/
|
|
678
|
+
function showDocsHelp() {
|
|
679
|
+
log.info(`
|
|
680
|
+
Pulse Docs - Generate API documentation from JSDoc
|
|
681
|
+
|
|
682
|
+
Usage: pulse docs --generate [paths...] [options]
|
|
683
|
+
|
|
684
|
+
Options:
|
|
685
|
+
--generate, -g Generate documentation (required)
|
|
686
|
+
--format, -f Output format: markdown, json, html (default: markdown)
|
|
687
|
+
--output, -o Output directory (default: docs/api)
|
|
688
|
+
--title Documentation title (for HTML)
|
|
689
|
+
--verbose, -v Show detailed output
|
|
690
|
+
|
|
691
|
+
Examples:
|
|
692
|
+
pulse docs --generate # Document src/ and runtime/
|
|
693
|
+
pulse docs --generate src/ # Document specific directory
|
|
694
|
+
pulse docs --generate --format html # Generate HTML docs
|
|
695
|
+
pulse docs --generate --format json # Generate JSON docs
|
|
696
|
+
pulse docs -g -f html -o docs/ # HTML docs to docs/
|
|
697
|
+
|
|
698
|
+
Output:
|
|
699
|
+
markdown -> One .md file per module + README.md index
|
|
700
|
+
html -> Single index.html with navigation
|
|
701
|
+
json -> Single api.json with structured data
|
|
702
|
+
|
|
703
|
+
JSDoc Tags Supported:
|
|
704
|
+
@param {type} name - description
|
|
705
|
+
@returns {type} description
|
|
706
|
+
@example
|
|
707
|
+
@throws {Error}
|
|
708
|
+
@deprecated
|
|
709
|
+
@see reference
|
|
710
|
+
@since version
|
|
711
|
+
`);
|
|
712
|
+
}
|