@mgks/docmd 0.3.9 โ 0.4.0
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 +15 -160
- package/bin/docmd.js +6 -69
- package/package.json +6 -79
- package/bin/postinstall.js +0 -14
- package/src/assets/css/docmd-highlight-dark.css +0 -86
- package/src/assets/css/docmd-highlight-light.css +0 -86
- package/src/assets/css/docmd-main.css +0 -1736
- package/src/assets/css/docmd-theme-retro.css +0 -867
- package/src/assets/css/docmd-theme-ruby.css +0 -629
- package/src/assets/css/docmd-theme-sky.css +0 -617
- package/src/assets/favicon.ico +0 -0
- package/src/assets/images/docmd-logo-dark.png +0 -0
- package/src/assets/images/docmd-logo-light.png +0 -0
- package/src/assets/js/docmd-image-lightbox.js +0 -74
- package/src/assets/js/docmd-main.js +0 -260
- package/src/assets/js/docmd-mermaid.js +0 -205
- package/src/assets/js/docmd-search.js +0 -218
- package/src/commands/build.js +0 -237
- package/src/commands/dev.js +0 -352
- package/src/commands/init.js +0 -277
- package/src/commands/live.js +0 -145
- package/src/core/asset-manager.js +0 -72
- package/src/core/config-loader.js +0 -58
- package/src/core/config-validator.js +0 -80
- package/src/core/file-processor.js +0 -103
- package/src/core/fs-utils.js +0 -40
- package/src/core/html-generator.js +0 -184
- package/src/core/icon-renderer.js +0 -106
- package/src/core/logger.js +0 -21
- package/src/core/markdown/containers.js +0 -94
- package/src/core/markdown/renderers.js +0 -90
- package/src/core/markdown/rules.js +0 -402
- package/src/core/markdown/setup.js +0 -113
- package/src/core/navigation-helper.js +0 -74
- package/src/index.js +0 -12
- package/src/live/core.js +0 -67
- package/src/live/index.html +0 -216
- package/src/live/live.css +0 -256
- package/src/live/shims.js +0 -1
- package/src/plugins/analytics.js +0 -48
- package/src/plugins/seo.js +0 -107
- package/src/plugins/sitemap.js +0 -127
- package/src/templates/layout.ejs +0 -187
- package/src/templates/navigation.ejs +0 -87
- package/src/templates/no-style.ejs +0 -166
- package/src/templates/partials/theme-init.js +0 -30
- package/src/templates/toc.ejs +0 -38
package/src/commands/build.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
// Source file from the docmd project โ https://github.com/docmd-io/docmd
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('../core/fs-utils');
|
|
5
|
-
const MiniSearch = require('minisearch');
|
|
6
|
-
const { loadConfig } = require('../core/config-loader');
|
|
7
|
-
const { createMarkdownItInstance, findMarkdownFiles, processMarkdownFile } = require('../core/file-processor');
|
|
8
|
-
const { generateHtmlPage, generateNavigationHtml } = require('../core/html-generator');
|
|
9
|
-
const { clearWarnedIcons } = require('../core/icon-renderer');
|
|
10
|
-
const { findPageNeighbors } = require('../core/navigation-helper');
|
|
11
|
-
const { generateSitemap } = require('../plugins/sitemap');
|
|
12
|
-
const { processAssets } = require('../core/asset-manager');
|
|
13
|
-
|
|
14
|
-
// Add a global or scoped flag to track if the warning has been shown in the current dev session
|
|
15
|
-
let highlightWarningShown = false;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Format paths for display to make them relative to CWD
|
|
19
|
-
*/
|
|
20
|
-
function formatPathForDisplay(absolutePath, cwd) {
|
|
21
|
-
const relativePath = path.relative(cwd, absolutePath);
|
|
22
|
-
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
23
|
-
return `./${relativePath}`;
|
|
24
|
-
}
|
|
25
|
-
return relativePath;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function buildSite(configPath, options = { isDev: false, preserve: false, noDoubleProcessing: false }) {
|
|
29
|
-
clearWarnedIcons(); // Clear warnings at the start of every build
|
|
30
|
-
|
|
31
|
-
const config = await loadConfig(configPath);
|
|
32
|
-
const CWD = process.cwd();
|
|
33
|
-
const SRC_DIR = path.resolve(CWD, config.srcDir);
|
|
34
|
-
const OUTPUT_DIR = path.resolve(CWD, config.outputDir);
|
|
35
|
-
const USER_ASSETS_DIR = path.resolve(CWD, 'assets');
|
|
36
|
-
const md = createMarkdownItInstance(config);
|
|
37
|
-
const shouldMinify = !options.isDev && config.minify !== false;
|
|
38
|
-
const searchIndexData = [];
|
|
39
|
-
const isOfflineMode = options.offline === true;
|
|
40
|
-
|
|
41
|
-
if (!await fs.exists(SRC_DIR)) {
|
|
42
|
-
throw new Error(`Source directory not found: ${formatPathForDisplay(SRC_DIR, CWD)}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Create output directory if it doesn't exist
|
|
46
|
-
await fs.ensureDir(OUTPUT_DIR);
|
|
47
|
-
|
|
48
|
-
// Clean HTML files
|
|
49
|
-
if (await fs.exists(OUTPUT_DIR)) {
|
|
50
|
-
const cleanupFiles = await findFilesToCleanup(OUTPUT_DIR);
|
|
51
|
-
for (const file of cleanupFiles) {
|
|
52
|
-
await fs.remove(file);
|
|
53
|
-
}
|
|
54
|
-
if (!options.isDev) {
|
|
55
|
-
console.log(`๐งน Cleaned HTML files from output directory: ${formatPathForDisplay(OUTPUT_DIR, CWD)}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --- ASSET PROCESSING ---
|
|
60
|
-
const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
|
|
61
|
-
await fs.ensureDir(assetsDestDir);
|
|
62
|
-
|
|
63
|
-
// 1. Process Internal Assets First (Base)
|
|
64
|
-
const assetsSrcDir = path.join(__dirname, '..', 'assets');
|
|
65
|
-
if (await fs.exists(assetsSrcDir)) {
|
|
66
|
-
if (!options.isDev) console.log(`๐ Processing docmd assets...`);
|
|
67
|
-
await processAssets(assetsSrcDir, assetsDestDir, { minify: shouldMinify });
|
|
68
|
-
} else {
|
|
69
|
-
console.warn(`โ ๏ธ Assets source directory not found: ${formatPathForDisplay(assetsSrcDir, CWD)}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 2. Process User Assets Second (Overrides Internal)
|
|
73
|
-
if (await fs.exists(USER_ASSETS_DIR)) {
|
|
74
|
-
if (!options.isDev) console.log(`๐ Processing user assets...`);
|
|
75
|
-
await processAssets(USER_ASSETS_DIR, assetsDestDir, { minify: shouldMinify });
|
|
76
|
-
|
|
77
|
-
// Copy favicon to root if exists
|
|
78
|
-
const userFavicon = path.join(USER_ASSETS_DIR, 'favicon.ico');
|
|
79
|
-
if (await fs.exists(userFavicon)) {
|
|
80
|
-
await fs.copy(userFavicon, path.join(OUTPUT_DIR, 'favicon.ico'));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Check for Highlight.js themes presence (sanity check)
|
|
85
|
-
const lightThemePath = path.join(__dirname, '..', 'assets', 'css', 'docmd-highlight-light.css');
|
|
86
|
-
if (!await fs.exists(lightThemePath)) {
|
|
87
|
-
if (!options.isDev || (options.isDev && !highlightWarningShown)) {
|
|
88
|
-
console.warn(`โ ๏ธ Highlight.js themes not found in assets. Syntax highlighting may break.`);
|
|
89
|
-
if (options.isDev) highlightWarningShown = true;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 3. Process Markdown Content
|
|
94
|
-
const processedPages = [];
|
|
95
|
-
const processedFiles = new Set();
|
|
96
|
-
const markdownFiles = await findMarkdownFiles(SRC_DIR);
|
|
97
|
-
if (!options.isDev) console.log(`๐ Found ${markdownFiles.length} markdown files.`);
|
|
98
|
-
|
|
99
|
-
for (const filePath of markdownFiles) {
|
|
100
|
-
try {
|
|
101
|
-
const relativePath = path.relative(SRC_DIR, filePath);
|
|
102
|
-
|
|
103
|
-
// Skip file if already processed in this dev build cycle
|
|
104
|
-
if (options.noDoubleProcessing && processedFiles.has(relativePath)) continue;
|
|
105
|
-
processedFiles.add(relativePath);
|
|
106
|
-
|
|
107
|
-
const processedData = await processMarkdownFile(filePath, md, config);
|
|
108
|
-
if (!processedData) continue;
|
|
109
|
-
|
|
110
|
-
const { frontmatter: pageFrontmatter, htmlContent, headings, searchData } = processedData;
|
|
111
|
-
const isIndexFile = path.basename(relativePath) === 'index.md';
|
|
112
|
-
|
|
113
|
-
let outputHtmlPath;
|
|
114
|
-
if (isIndexFile) {
|
|
115
|
-
outputHtmlPath = path.join(path.dirname(relativePath), 'index.html');
|
|
116
|
-
} else {
|
|
117
|
-
outputHtmlPath = relativePath.replace(/\.md$/, '/index.html');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const finalOutputHtmlPath = path.join(OUTPUT_DIR, outputHtmlPath);
|
|
121
|
-
|
|
122
|
-
let relativePathToRoot = path.relative(path.dirname(finalOutputHtmlPath), OUTPUT_DIR);
|
|
123
|
-
relativePathToRoot = relativePathToRoot === '' ? './' : relativePathToRoot.replace(/\\/g, '/') + '/';
|
|
124
|
-
|
|
125
|
-
let normalizedPath = path.relative(SRC_DIR, filePath).replace(/\\/g, '/');
|
|
126
|
-
if (path.basename(normalizedPath) === 'index.md') {
|
|
127
|
-
normalizedPath = path.dirname(normalizedPath);
|
|
128
|
-
if (normalizedPath === '.') normalizedPath = '';
|
|
129
|
-
} else {
|
|
130
|
-
normalizedPath = normalizedPath.replace(/\.md$/, '');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!normalizedPath.startsWith('/')) normalizedPath = '/' + normalizedPath;
|
|
134
|
-
if (normalizedPath.length > 1 && !normalizedPath.endsWith('/')) normalizedPath += '/';
|
|
135
|
-
|
|
136
|
-
const navigationHtml = await generateNavigationHtml(
|
|
137
|
-
config.navigation, normalizedPath, relativePathToRoot, config, isOfflineMode
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const { prevPage, nextPage } = findPageNeighbors(config.navigation, normalizedPath);
|
|
141
|
-
|
|
142
|
-
if (prevPage) prevPage.url = relativePathToRoot + prevPage.path.substring(1);
|
|
143
|
-
if (nextPage) nextPage.url = relativePathToRoot + nextPage.path.substring(1);
|
|
144
|
-
|
|
145
|
-
const pageDataForTemplate = {
|
|
146
|
-
content: htmlContent,
|
|
147
|
-
pageTitle: pageFrontmatter.title || 'Untitled',
|
|
148
|
-
siteTitle: config.siteTitle,
|
|
149
|
-
navigationHtml, relativePathToRoot, config, frontmatter: pageFrontmatter,
|
|
150
|
-
outputPath: outputHtmlPath.replace(/\\/g, '/'),
|
|
151
|
-
prevPage, nextPage, currentPagePath: normalizedPath,
|
|
152
|
-
headings: headings || [], isOfflineMode,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const pageHtml = await generateHtmlPage(pageDataForTemplate, isOfflineMode);
|
|
156
|
-
await fs.ensureDir(path.dirname(finalOutputHtmlPath));
|
|
157
|
-
await fs.writeFile(finalOutputHtmlPath, pageHtml);
|
|
158
|
-
|
|
159
|
-
const sitemapOutputPath = isIndexFile
|
|
160
|
-
? (path.dirname(relativePath) === '.' ? '' : path.dirname(relativePath) + '/')
|
|
161
|
-
: relativePath.replace(/\.md$/, '/');
|
|
162
|
-
|
|
163
|
-
processedPages.push({
|
|
164
|
-
outputPath: sitemapOutputPath.replace(/\\/g, '/'),
|
|
165
|
-
frontmatter: pageFrontmatter
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// Collect Search Data
|
|
169
|
-
if (searchData) {
|
|
170
|
-
let pageUrl = outputHtmlPath.replace(/\\/g, '/');
|
|
171
|
-
if (pageUrl.endsWith('/index.html')) pageUrl = pageUrl.substring(0, pageUrl.length - 10);
|
|
172
|
-
|
|
173
|
-
searchIndexData.push({
|
|
174
|
-
id: pageUrl,
|
|
175
|
-
title: searchData.title,
|
|
176
|
-
text: searchData.content,
|
|
177
|
-
headings: searchData.headings.join(' ')
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error(`โ Error processing ${path.relative(CWD, filePath)}:`, error);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 4. Generate Sitemap
|
|
187
|
-
if (config.plugins?.sitemap !== false) {
|
|
188
|
-
try {
|
|
189
|
-
await generateSitemap(config, processedPages, OUTPUT_DIR, { isDev: options.isDev });
|
|
190
|
-
} catch (error) {
|
|
191
|
-
console.error(`โ Error generating sitemap: ${error.message}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 5. Generate Search Index
|
|
196
|
-
if (config.search !== false) {
|
|
197
|
-
if (!options.isDev) console.log('๐ Generating search index...');
|
|
198
|
-
|
|
199
|
-
// MiniSearch build process (server-side)
|
|
200
|
-
const miniSearch = new MiniSearch({
|
|
201
|
-
fields: ['title', 'headings', 'text'],
|
|
202
|
-
storeFields: ['title', 'id', 'text'],
|
|
203
|
-
searchOptions: { boost: { title: 2, headings: 1.5 }, fuzzy: 0.2 }
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
miniSearch.addAll(searchIndexData);
|
|
207
|
-
|
|
208
|
-
// Save the index JSON
|
|
209
|
-
await fs.writeFile(path.join(OUTPUT_DIR, 'search-index.json'), JSON.stringify(miniSearch.toJSON()));
|
|
210
|
-
|
|
211
|
-
if (!options.isDev) console.log(`โ
Search index generated.`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return { config, processedPages, markdownFiles };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Helper function to find HTML files and sitemap.xml to clean up
|
|
218
|
-
async function findFilesToCleanup(dir) {
|
|
219
|
-
let filesToRemove = [];
|
|
220
|
-
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
221
|
-
|
|
222
|
-
for (const item of items) {
|
|
223
|
-
const fullPath = path.join(dir, item.name);
|
|
224
|
-
|
|
225
|
-
if (item.isDirectory()) {
|
|
226
|
-
if (item.name !== 'assets') {
|
|
227
|
-
const subDirFiles = await findFilesToCleanup(fullPath);
|
|
228
|
-
filesToRemove = filesToRemove.concat(subDirFiles);
|
|
229
|
-
}
|
|
230
|
-
} else if (item.name.endsWith('.html') || item.name === 'sitemap.xml') {
|
|
231
|
-
filesToRemove.push(fullPath);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return filesToRemove;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
module.exports = { buildSite };
|
package/src/commands/dev.js
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
// Source file from the docmd project โ https://github.com/docmd-io/docmd
|
|
2
|
-
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const WebSocket = require('ws');
|
|
5
|
-
const chokidar = require('chokidar');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const fs = require('../core/fs-utils');
|
|
8
|
-
const chalk = require('chalk');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const readline = require('readline');
|
|
11
|
-
const { buildSite } = require('./build');
|
|
12
|
-
const { loadConfig } = require('../core/config-loader');
|
|
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
|
-
|
|
124
|
-
function formatPathForDisplay(absolutePath, cwd) {
|
|
125
|
-
const relativePath = path.relative(cwd, absolutePath);
|
|
126
|
-
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
127
|
-
return `./${relativePath}`;
|
|
128
|
-
}
|
|
129
|
-
return relativePath;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function getNetworkIp() {
|
|
133
|
-
const interfaces = os.networkInterfaces();
|
|
134
|
-
for (const name of Object.keys(interfaces)) {
|
|
135
|
-
for (const iface of interfaces[name]) {
|
|
136
|
-
if (iface.family === 'IPv4' && !iface.internal) {
|
|
137
|
-
return iface.address;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// --- 3. Main Dev Function ---
|
|
145
|
-
|
|
146
|
-
async function startDevServer(configPathOption, options = { preserve: false, port: undefined }) {
|
|
147
|
-
let config = await loadConfig(configPathOption);
|
|
148
|
-
const CWD = process.cwd();
|
|
149
|
-
|
|
150
|
-
// Config Fallback Logic
|
|
151
|
-
let actualConfigPath = path.resolve(CWD, configPathOption);
|
|
152
|
-
if (configPathOption === 'docmd.config.js' && !await fs.pathExists(actualConfigPath)) {
|
|
153
|
-
const legacyPath = path.resolve(CWD, 'config.js');
|
|
154
|
-
if (await fs.pathExists(legacyPath)) {
|
|
155
|
-
actualConfigPath = legacyPath;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const resolveConfigPaths = (currentConfig) => {
|
|
160
|
-
return {
|
|
161
|
-
outputDir: path.resolve(CWD, currentConfig.outputDir),
|
|
162
|
-
srcDirToWatch: path.resolve(CWD, currentConfig.srcDir),
|
|
163
|
-
configFileToWatch: actualConfigPath,
|
|
164
|
-
userAssetsDir: path.resolve(CWD, 'assets'),
|
|
165
|
-
};
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
let paths = resolveConfigPaths(config);
|
|
169
|
-
const DOCMD_ROOT = path.resolve(__dirname, '..');
|
|
170
|
-
|
|
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)
|
|
177
|
-
|
|
178
|
-
function broadcastReload() {
|
|
179
|
-
if (wss) {
|
|
180
|
-
wss.clients.forEach((client) => {
|
|
181
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
182
|
-
client.send('reload');
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// --- Initial Build ---
|
|
189
|
-
console.log(chalk.blue('๐ Performing initial build...'));
|
|
190
|
-
try {
|
|
191
|
-
await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
|
|
192
|
-
} catch (error) {
|
|
193
|
-
console.error(chalk.red('โ Initial build failed:'), error.message);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// --- Watcher Setup ---
|
|
197
|
-
const userAssetsDirExists = await fs.pathExists(paths.userAssetsDir);
|
|
198
|
-
const watchedPaths = [paths.srcDirToWatch, paths.configFileToWatch];
|
|
199
|
-
if (userAssetsDirExists) watchedPaths.push(paths.userAssetsDir);
|
|
200
|
-
|
|
201
|
-
if (process.env.DOCMD_DEV === 'true') {
|
|
202
|
-
watchedPaths.push(
|
|
203
|
-
path.join(DOCMD_ROOT, 'templates'),
|
|
204
|
-
path.join(DOCMD_ROOT, 'assets'),
|
|
205
|
-
path.join(DOCMD_ROOT, 'core'),
|
|
206
|
-
path.join(DOCMD_ROOT, 'plugins')
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
console.log(chalk.dim('\n๐ Watching for changes in:'));
|
|
211
|
-
console.log(chalk.dim(` - Source: ${chalk.cyan(formatPathForDisplay(paths.srcDirToWatch, CWD))}`));
|
|
212
|
-
console.log(chalk.dim(` - Config: ${chalk.cyan(formatPathForDisplay(paths.configFileToWatch, CWD))}`));
|
|
213
|
-
if (userAssetsDirExists) {
|
|
214
|
-
console.log(chalk.dim(` - Assets: ${chalk.cyan(formatPathForDisplay(paths.userAssetsDir, CWD))}`));
|
|
215
|
-
}
|
|
216
|
-
console.log('');
|
|
217
|
-
|
|
218
|
-
const watcher = chokidar.watch(watchedPaths, {
|
|
219
|
-
ignored: /(^|[\/\\])\../,
|
|
220
|
-
persistent: true,
|
|
221
|
-
ignoreInitial: true,
|
|
222
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
watcher.on('all', async (event, filePath) => {
|
|
226
|
-
const relativeFilePath = path.relative(CWD, filePath);
|
|
227
|
-
process.stdout.write(chalk.dim(`โป Change in ${relativeFilePath}... `));
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
if (filePath === paths.configFileToWatch) {
|
|
231
|
-
config = await loadConfig(configPathOption);
|
|
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);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
|
|
238
|
-
broadcastReload();
|
|
239
|
-
process.stdout.write(chalk.green('Done.\n'));
|
|
240
|
-
|
|
241
|
-
} catch (error) {
|
|
242
|
-
console.error(chalk.red('\nโ Rebuild failed:'), error.message);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// --- Server Startup Logic (Port Checking) ---
|
|
247
|
-
const PORT = parseInt(options.port || process.env.PORT || 3000, 10);
|
|
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
|
-
}
|
|
281
|
-
|
|
282
|
-
function tryStartServer(port, attempt = 1) {
|
|
283
|
-
server.listen(port, '0.0.0.0')
|
|
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
|
-
|
|
289
|
-
const indexHtmlPath = path.join(paths.outputDir, 'index.html');
|
|
290
|
-
const networkIp = getNetworkIp();
|
|
291
|
-
|
|
292
|
-
const localUrl = `http://127.0.0.1:${port}`;
|
|
293
|
-
const networkUrl = networkIp ? `http://${networkIp}:${port}` : null;
|
|
294
|
-
|
|
295
|
-
const border = chalk.gray('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
296
|
-
console.log(border);
|
|
297
|
-
console.log(` ${chalk.bold.green('SERVER RUNNING')} ${chalk.dim(`(v${require('../../package.json').version})`)}`);
|
|
298
|
-
console.log('');
|
|
299
|
-
console.log(` ${chalk.bold('Local:')} ${chalk.cyan(localUrl)}`);
|
|
300
|
-
if (networkUrl) {
|
|
301
|
-
console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);
|
|
302
|
-
}
|
|
303
|
-
console.log('');
|
|
304
|
-
console.log(` ${chalk.dim('Serving:')} ${formatPathForDisplay(paths.outputDir, CWD)}`);
|
|
305
|
-
console.log(border);
|
|
306
|
-
console.log('');
|
|
307
|
-
|
|
308
|
-
if (!await fs.pathExists(indexHtmlPath)) {
|
|
309
|
-
console.warn(chalk.yellow(`โ ๏ธ Warning: Root index.html not found.`));
|
|
310
|
-
}
|
|
311
|
-
})
|
|
312
|
-
.on('error', (err) => {
|
|
313
|
-
if (err.code === 'EADDRINUSE') {
|
|
314
|
-
server.close();
|
|
315
|
-
tryStartServer(port + 1);
|
|
316
|
-
} else {
|
|
317
|
-
console.error(chalk.red(`Failed to start server: ${err.message}`));
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
}
|
|
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
|
-
})();
|
|
344
|
-
|
|
345
|
-
process.on('SIGINT', () => {
|
|
346
|
-
console.log(chalk.yellow('\n๐ Shutting down...'));
|
|
347
|
-
watcher.close();
|
|
348
|
-
process.exit(0);
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
module.exports = { startDevServer };
|