@mgks/docmd 0.3.6 → 0.3.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.
Files changed (53) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +86 -77
  3. package/bin/docmd.js +13 -16
  4. package/bin/postinstall.js +4 -4
  5. package/package.json +19 -16
  6. package/src/assets/css/docmd-highlight-dark.css +86 -1
  7. package/src/assets/css/docmd-highlight-light.css +86 -1
  8. package/src/assets/css/docmd-main.css +544 -464
  9. package/src/assets/css/docmd-theme-retro.css +105 -106
  10. package/src/assets/css/docmd-theme-ruby.css +92 -92
  11. package/src/assets/css/docmd-theme-sky.css +63 -64
  12. package/src/assets/favicon.ico +0 -0
  13. package/src/assets/images/docmd-logo-dark.png +0 -0
  14. package/src/assets/images/docmd-logo-light.png +0 -0
  15. package/src/assets/js/docmd-image-lightbox.js +2 -2
  16. package/src/assets/js/docmd-main.js +14 -6
  17. package/src/assets/js/docmd-mermaid.js +1 -1
  18. package/src/assets/js/docmd-search.js +1 -1
  19. package/src/commands/build.js +71 -370
  20. package/src/commands/dev.js +199 -72
  21. package/src/commands/init.js +135 -134
  22. package/src/commands/live.js +145 -0
  23. package/src/core/asset-manager.js +72 -0
  24. package/src/core/config-loader.js +2 -2
  25. package/src/core/config-validator.js +1 -1
  26. package/src/core/file-processor.js +13 -9
  27. package/src/core/fs-utils.js +40 -0
  28. package/src/core/html-formatter.js +97 -0
  29. package/src/core/html-generator.js +61 -65
  30. package/src/core/icon-renderer.js +1 -1
  31. package/src/core/logger.js +1 -1
  32. package/src/core/markdown/containers.js +1 -1
  33. package/src/core/markdown/renderers.js +1 -1
  34. package/src/core/markdown/rules.js +1 -2
  35. package/src/core/markdown/setup.js +1 -1
  36. package/src/core/navigation-helper.js +1 -1
  37. package/src/index.js +12 -0
  38. package/src/live/core.js +5 -1
  39. package/src/live/index.html +16 -1
  40. package/src/live/live.css +157 -68
  41. package/src/plugins/analytics.js +1 -1
  42. package/src/plugins/seo.js +26 -36
  43. package/src/plugins/sitemap.js +2 -2
  44. package/src/templates/layout.ejs +50 -81
  45. package/src/templates/navigation.ejs +23 -76
  46. package/src/templates/no-style.ejs +115 -129
  47. package/src/templates/partials/theme-init.js +1 -1
  48. package/src/templates/toc.ejs +6 -35
  49. package/docmd.config.js +0 -175
  50. package/scripts/build-live.js +0 -157
  51. package/scripts/test-live.js +0 -54
  52. package/src/assets/images/docmd-logo.png +0 -0
  53. package/src/live/templates.js +0 -9
@@ -1,16 +1,126 @@
1
- // Source file from the docmd project — https://github.com/mgks/docmd
1
+ // Source file from the docmd project — https://github.com/docmd-io/docmd
2
2
 
3
- const express = require('express');
4
3
  const http = require('http');
5
4
  const WebSocket = require('ws');
6
5
  const chokidar = require('chokidar');
7
6
  const path = require('path');
8
- const fs = require('fs-extra');
7
+ const fs = require('../core/fs-utils');
9
8
  const chalk = require('chalk');
10
- const os = require('os');
9
+ const os = require('os');
10
+ const readline = require('readline');
11
11
  const { buildSite } = require('./build');
12
12
  const { loadConfig } = require('../core/config-loader');
13
13
 
14
+ // --- 1. Native Static File Server ---
15
+ const MIME_TYPES = {
16
+ '.html': 'text/html',
17
+ '.js': 'text/javascript',
18
+ '.css': 'text/css',
19
+ '.json': 'application/json',
20
+ '.png': 'image/png',
21
+ '.jpg': 'image/jpg',
22
+ '.jpeg': 'image/jpg',
23
+ '.gif': 'image/gif',
24
+ '.svg': 'image/svg+xml',
25
+ '.ico': 'image/x-icon',
26
+ '.woff': 'application/font-woff',
27
+ '.woff2': 'font/woff2',
28
+ '.ttf': 'application/font-ttf',
29
+ '.txt': 'text/plain',
30
+ };
31
+
32
+ async function serveStatic(req, res, rootDir) {
33
+ // Normalize path and remove query strings
34
+ let safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, '').split('?')[0].split('#')[0];
35
+ if (safePath === '/' || safePath === '\\') safePath = 'index.html';
36
+
37
+ let filePath = path.join(rootDir, safePath);
38
+
39
+ try {
40
+ let stats;
41
+ try {
42
+ stats = await fs.stat(filePath);
43
+ } catch (e) {
44
+ // If direct path fails, try appending .html (clean URLs support)
45
+ if (path.extname(filePath) === '') {
46
+ filePath += '.html';
47
+ stats = await fs.stat(filePath);
48
+ } else {
49
+ throw e;
50
+ }
51
+ }
52
+
53
+ if (stats.isDirectory()) {
54
+ filePath = path.join(filePath, 'index.html');
55
+ await fs.stat(filePath);
56
+ }
57
+
58
+ const ext = path.extname(filePath).toLowerCase();
59
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
60
+ const content = await fs.readFile(filePath);
61
+
62
+ res.writeHead(200, { 'Content-Type': contentType });
63
+
64
+ // Inject Live Reload Script into HTML files only
65
+ if (contentType === 'text/html') {
66
+ const htmlStr = content.toString('utf-8');
67
+ const liveReloadScript = `
68
+ <script>
69
+ (function() {
70
+ let socket;
71
+ let retryCount = 0;
72
+ const maxRetries = 50;
73
+
74
+ function connect() {
75
+ // Avoid connecting if already connected
76
+ if (socket && (socket.readyState === 0 || socket.readyState === 1)) return;
77
+
78
+ socket = new WebSocket('ws://' + window.location.host);
79
+
80
+ socket.onopen = () => {
81
+ console.log('⚡ docmd connected');
82
+ retryCount = 0;
83
+ };
84
+
85
+ socket.onmessage = (e) => {
86
+ if(e.data === 'reload') window.location.reload();
87
+ };
88
+
89
+ socket.onclose = () => {
90
+ // Exponential backoff for reconnection
91
+ if (retryCount < maxRetries) {
92
+ retryCount++;
93
+ const delay = Math.min(1000 * (1.5 ** retryCount), 5000);
94
+ setTimeout(connect, delay);
95
+ }
96
+ };
97
+
98
+ socket.onerror = (err) => {
99
+ // Ignore errors, let onclose handle retry
100
+ };
101
+ }
102
+ // Delay initial connection slightly to ensure page load
103
+ setTimeout(connect, 500);
104
+ })();
105
+ </script></body>`;
106
+ res.end(htmlStr.replace('</body>', liveReloadScript));
107
+ } else {
108
+ res.end(content);
109
+ }
110
+
111
+ } catch (err) {
112
+ if (err.code === 'ENOENT') {
113
+ res.writeHead(404, { 'Content-Type': 'text/html' });
114
+ res.end('<h1>404 Not Found</h1><p>docmd dev server</p>');
115
+ } else {
116
+ res.writeHead(500);
117
+ res.end(`Server Error: ${err.code}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ // --- 2. Helper Utilities ---
123
+
14
124
  function formatPathForDisplay(absolutePath, cwd) {
15
125
  const relativePath = path.relative(cwd, absolutePath);
16
126
  if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
@@ -31,11 +141,13 @@ function getNetworkIp() {
31
141
  return null;
32
142
  }
33
143
 
144
+ // --- 3. Main Dev Function ---
145
+
34
146
  async function startDevServer(configPathOption, options = { preserve: false, port: undefined }) {
35
147
  let config = await loadConfig(configPathOption);
36
148
  const CWD = process.cwd();
37
149
 
38
- // Config Fallback for Watcher
150
+ // Config Fallback Logic
39
151
  let actualConfigPath = path.resolve(CWD, configPathOption);
40
152
  if (configPathOption === 'docmd.config.js' && !await fs.pathExists(actualConfigPath)) {
41
153
  const legacyPath = path.resolve(CWD, 'config.js');
@@ -56,58 +168,24 @@ async function startDevServer(configPathOption, options = { preserve: false, por
56
168
  let paths = resolveConfigPaths(config);
57
169
  const DOCMD_ROOT = path.resolve(__dirname, '..');
58
170
 
59
- const app = express();
60
- const server = http.createServer(app);
61
- const wss = new WebSocket.Server({ server });
171
+ // --- Create Native Server ---
172
+ const server = http.createServer((req, res) => {
173
+ serveStatic(req, res, paths.outputDir);
174
+ });
175
+
176
+ let wss; // WebSocket instance (initialized later)
62
177
 
63
178
  function broadcastReload() {
64
- wss.clients.forEach((client) => {
65
- if (client.readyState === WebSocket.OPEN) {
66
- client.send('reload');
67
- }
68
- });
69
- }
70
-
71
- // Inject live reload script
72
- app.use((req, res, next) => {
73
- if (req.path.endsWith('.html') || req.path === '/' || !req.path.includes('.')) {
74
- const originalSend = res.send;
75
- res.send = function(body) {
76
- if (typeof body === 'string' && body.includes('</body>')) {
77
- const liveReloadScript = `
78
- <script>
79
- (function() {
80
- let socket;
81
- let reconnectTimer;
82
- function connect() {
83
- socket = new WebSocket('ws://' + window.location.host);
84
- socket.onopen = function() {
85
- console.log('⚡ docmd live reload connected');
86
- if (reconnectTimer) clearInterval(reconnectTimer);
87
- };
88
- socket.onmessage = function(event) {
89
- if (event.data === 'reload') window.location.reload();
90
- };
91
- socket.onclose = function() {
92
- reconnectTimer = setTimeout(connect, 1000);
93
- };
94
- }
95
- connect();
96
- })();
97
- </script>
98
- `;
99
- body = body.replace('</body>', `${liveReloadScript}</body>`);
179
+ if (wss) {
180
+ wss.clients.forEach((client) => {
181
+ if (client.readyState === WebSocket.OPEN) {
182
+ client.send('reload');
100
183
  }
101
- originalSend.call(this, body);
102
- };
184
+ });
103
185
  }
104
- next();
105
- });
106
-
107
- let staticMiddleware = express.static(paths.outputDir);
108
- app.use((req, res, next) => staticMiddleware(req, res, next));
186
+ }
109
187
 
110
- // --- 1. Initial Build ---
188
+ // --- Initial Build ---
111
189
  console.log(chalk.blue('🚀 Performing initial build...'));
112
190
  try {
113
191
  await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
@@ -115,7 +193,7 @@ async function startDevServer(configPathOption, options = { preserve: false, por
115
193
  console.error(chalk.red('❌ Initial build failed:'), error.message);
116
194
  }
117
195
 
118
- // --- 2. Setup Watcher & Logs ---
196
+ // --- Watcher Setup ---
119
197
  const userAssetsDirExists = await fs.pathExists(paths.userAssetsDir);
120
198
  const watchedPaths = [paths.srcDirToWatch, paths.configFileToWatch];
121
199
  if (userAssetsDirExists) watchedPaths.push(paths.userAssetsDir);
@@ -129,16 +207,12 @@ async function startDevServer(configPathOption, options = { preserve: false, por
129
207
  );
130
208
  }
131
209
 
132
- // LOGS: Explicitly print what we are watching
133
210
  console.log(chalk.dim('\n👀 Watching for changes in:'));
134
211
  console.log(chalk.dim(` - Source: ${chalk.cyan(formatPathForDisplay(paths.srcDirToWatch, CWD))}`));
135
212
  console.log(chalk.dim(` - Config: ${chalk.cyan(formatPathForDisplay(paths.configFileToWatch, CWD))}`));
136
213
  if (userAssetsDirExists) {
137
214
  console.log(chalk.dim(` - Assets: ${chalk.cyan(formatPathForDisplay(paths.userAssetsDir, CWD))}`));
138
215
  }
139
- if (process.env.DOCMD_DEV === 'true') {
140
- console.log(chalk.dim(` - docmd Internal: ${chalk.magenta(formatPathForDisplay(DOCMD_ROOT, CWD))}`));
141
- }
142
216
  console.log('');
143
217
 
144
218
  const watcher = chokidar.watch(watchedPaths, {
@@ -155,11 +229,9 @@ async function startDevServer(configPathOption, options = { preserve: false, por
155
229
  try {
156
230
  if (filePath === paths.configFileToWatch) {
157
231
  config = await loadConfig(configPathOption);
158
- const newPaths = resolveConfigPaths(config);
159
- if (newPaths.outputDir !== paths.outputDir) {
160
- staticMiddleware = express.static(newPaths.outputDir);
161
- }
162
- paths = newPaths;
232
+ // Note: With native server, we don't need to restart middleware,
233
+ // serveStatic reads from disk dynamically on every request.
234
+ paths = resolveConfigPaths(config);
163
235
  }
164
236
 
165
237
  await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
@@ -171,17 +243,52 @@ async function startDevServer(configPathOption, options = { preserve: false, por
171
243
  }
172
244
  });
173
245
 
174
- const PORT = options.port || process.env.PORT || 3000;
246
+ // --- Server Startup Logic (Port Checking) ---
247
+ const PORT = parseInt(options.port || process.env.PORT || 3000, 10);
175
248
  const MAX_PORT_ATTEMPTS = 10;
249
+
250
+ function checkPortInUse(port) {
251
+ return new Promise((resolve) => {
252
+ const tester = http.createServer()
253
+ .once('error', (err) => {
254
+ if (err.code === 'EADDRINUSE') resolve(true);
255
+ else resolve(false);
256
+ })
257
+ .once('listening', () => {
258
+ tester.close(() => resolve(false));
259
+ })
260
+ .listen(port, '0.0.0.0');
261
+ });
262
+ }
263
+
264
+ function askUserConfirmation() {
265
+ return new Promise((resolve) => {
266
+ const rl = readline.createInterface({
267
+ input: process.stdin,
268
+ output: process.stdout
269
+ });
270
+
271
+ console.log(chalk.yellow(`\n⚠️ Port ${PORT} is already in use.`));
272
+ console.log(chalk.yellow(` Another instance of docmd (or another app) might be running.`));
273
+
274
+ rl.question(' Do you want to start another instance on a different port? (Y/n) ', (answer) => {
275
+ rl.close();
276
+ const isYes = answer.trim().toLowerCase() === 'y' || answer.trim() === '';
277
+ resolve(isYes);
278
+ });
279
+ });
280
+ }
176
281
 
177
282
  function tryStartServer(port, attempt = 1) {
178
- // 0.0.0.0 allows network access
179
283
  server.listen(port, '0.0.0.0')
180
284
  .on('listening', async () => {
285
+ // Initialize WebSocket Server only AFTER successful listen
286
+ wss = new WebSocket.Server({ server });
287
+ wss.on('error', (e) => console.error('WebSocket Error:', e.message));
288
+
181
289
  const indexHtmlPath = path.join(paths.outputDir, 'index.html');
182
290
  const networkIp = getNetworkIp();
183
291
 
184
- // Use 127.0.0.1 explicitly
185
292
  const localUrl = `http://127.0.0.1:${port}`;
186
293
  const networkUrl = networkIp ? `http://${networkIp}:${port}` : null;
187
294
 
@@ -203,16 +310,37 @@ async function startDevServer(configPathOption, options = { preserve: false, por
203
310
  }
204
311
  })
205
312
  .on('error', (err) => {
206
- if (err.code === 'EADDRINUSE' && attempt < MAX_PORT_ATTEMPTS) {
207
- tryStartServer(port + 1, attempt + 1);
313
+ if (err.code === 'EADDRINUSE') {
314
+ server.close();
315
+ tryStartServer(port + 1);
208
316
  } else {
209
- console.error(chalk.red(`Failed to start server: ${err.message}`));
210
- process.exit(1);
317
+ console.error(chalk.red(`Failed to start server: ${err.message}`));
318
+ process.exit(1);
211
319
  }
212
320
  });
213
321
  }
214
-
215
- tryStartServer(parseInt(PORT, 10));
322
+
323
+ // --- Main Execution Flow ---
324
+ (async () => {
325
+ // Skip check if user manually specified port flag
326
+ if (options.port) {
327
+ tryStartServer(PORT);
328
+ return;
329
+ }
330
+
331
+ const isBusy = await checkPortInUse(PORT);
332
+
333
+ if (isBusy) {
334
+ const shouldProceed = await askUserConfirmation();
335
+ if (!shouldProceed) {
336
+ console.log(chalk.dim('Cancelled.'));
337
+ process.exit(0);
338
+ }
339
+ tryStartServer(PORT + 1);
340
+ } else {
341
+ tryStartServer(PORT);
342
+ }
343
+ })();
216
344
 
217
345
  process.on('SIGINT', () => {
218
346
  console.log(chalk.yellow('\n🛑 Shutting down...'));
@@ -221,5 +349,4 @@ async function startDevServer(configPathOption, options = { preserve: false, por
221
349
  });
222
350
  }
223
351
 
224
- // Ensure this export is here!
225
352
  module.exports = { startDevServer };