@mgks/docmd 0.1.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/.gitattributes +2 -0
- package/.github/FUNDING.yml +15 -0
- package/.github/workflows/deploy-docmd.yml +45 -0
- package/.github/workflows/publish.yml +84 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bin/docmd.js +63 -0
- package/config.js +137 -0
- package/docs/cli-commands.md +87 -0
- package/docs/configuration.md +166 -0
- package/docs/contributing.md +86 -0
- package/docs/deployment.md +129 -0
- package/docs/getting-started/basic-usage.md +88 -0
- package/docs/getting-started/index.md +21 -0
- package/docs/getting-started/installation.md +75 -0
- package/docs/index.md +56 -0
- package/docs/plugins/analytics.md +76 -0
- package/docs/plugins/index.md +71 -0
- package/docs/plugins/seo.md +79 -0
- package/docs/plugins/sitemap.md +88 -0
- package/docs/theming/available-themes.md +85 -0
- package/docs/theming/custom-css-js.md +84 -0
- package/docs/theming/icons.md +93 -0
- package/docs/theming/index.md +19 -0
- package/docs/theming/light-dark-mode.md +107 -0
- package/docs/writing-content/custom-containers.md +129 -0
- package/docs/writing-content/frontmatter.md +76 -0
- package/docs/writing-content/index.md +17 -0
- package/docs/writing-content/markdown-syntax.md +277 -0
- package/package.json +56 -0
- package/src/assets/css/highlight-dark.css +1 -0
- package/src/assets/css/highlight-light.css +1 -0
- package/src/assets/css/main.css +562 -0
- package/src/assets/css/theme-sky.css +499 -0
- package/src/assets/css/toc.css +76 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/images/docmd-logo.png +0 -0
- package/src/assets/images/docmd-preview.png +0 -0
- package/src/assets/images/logo-dark.png +0 -0
- package/src/assets/images/logo-light.png +0 -0
- package/src/assets/js/theme-toggle.js +59 -0
- package/src/commands/build.js +300 -0
- package/src/commands/dev.js +182 -0
- package/src/commands/init.js +51 -0
- package/src/core/config-loader.js +28 -0
- package/src/core/file-processor.js +376 -0
- package/src/core/html-generator.js +139 -0
- package/src/core/icon-renderer.js +105 -0
- package/src/plugins/analytics.js +44 -0
- package/src/plugins/seo.js +65 -0
- package/src/plugins/sitemap.js +100 -0
- package/src/templates/layout.ejs +174 -0
- package/src/templates/navigation.ejs +107 -0
- package/src/templates/toc.ejs +34 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// src/commands/build.js
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { loadConfig } = require('../core/config-loader');
|
|
5
|
+
const { processMarkdownFile } = require('../core/file-processor');
|
|
6
|
+
const { generateHtmlPage, generateNavigationHtml } = require('../core/html-generator');
|
|
7
|
+
const { renderIcon, clearWarnedIcons } = require('../core/icon-renderer'); // Update import
|
|
8
|
+
const { generateSitemap } = require('../plugins/sitemap'); // Import our sitemap plugin
|
|
9
|
+
|
|
10
|
+
// Debug function to log navigation information
|
|
11
|
+
function logNavigationPaths(pagePath, navPath, normalizedPath) {
|
|
12
|
+
console.log(`\nPage: ${pagePath}`);
|
|
13
|
+
console.log(`Navigation Path: ${navPath}`);
|
|
14
|
+
console.log(`Normalized Path: ${normalizedPath}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Add a global or scoped flag to track if the warning has been shown in the current dev session
|
|
18
|
+
let highlightWarningShown = false;
|
|
19
|
+
|
|
20
|
+
async function buildSite(configPath, options = { isDev: false }) {
|
|
21
|
+
clearWarnedIcons(); // Clear warnings at the start of every build
|
|
22
|
+
|
|
23
|
+
const config = await loadConfig(configPath);
|
|
24
|
+
const CWD = process.cwd();
|
|
25
|
+
const SRC_DIR = path.resolve(CWD, config.srcDir);
|
|
26
|
+
const OUTPUT_DIR = path.resolve(CWD, config.outputDir);
|
|
27
|
+
|
|
28
|
+
if (!await fs.pathExists(SRC_DIR)) {
|
|
29
|
+
throw new Error(`Source directory not found: ${SRC_DIR}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await fs.emptyDir(OUTPUT_DIR);
|
|
33
|
+
if (!options.isDev) {
|
|
34
|
+
console.log(`๐งน Cleaned output directory: ${OUTPUT_DIR}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const assetsSrcDir = path.join(__dirname, '..', 'assets');
|
|
38
|
+
const assetsDestDir = path.join(OUTPUT_DIR, 'assets');
|
|
39
|
+
if (await fs.pathExists(assetsSrcDir)) {
|
|
40
|
+
await fs.copy(assetsSrcDir, assetsDestDir);
|
|
41
|
+
if (!options.isDev) {
|
|
42
|
+
console.log(`๐ Copied assets to ${assetsDestDir}`);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
console.warn(`โ ๏ธ Assets source directory not found: ${assetsSrcDir}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for Highlight.js themes
|
|
49
|
+
const lightThemePath = path.join(__dirname, '..', 'assets', 'css', 'highlight-light.css');
|
|
50
|
+
const darkThemePath = path.join(__dirname, '..', 'assets', 'css', 'highlight-dark.css');
|
|
51
|
+
|
|
52
|
+
const themesMissing = !await fs.pathExists(lightThemePath) || !await fs.pathExists(darkThemePath);
|
|
53
|
+
|
|
54
|
+
if (themesMissing) {
|
|
55
|
+
// For 'docmd build', always show.
|
|
56
|
+
// For 'docmd dev', show only once per session if not already shown.
|
|
57
|
+
if (!options.isDev || (options.isDev && !highlightWarningShown)) {
|
|
58
|
+
console.warn(`โ ๏ธ Highlight.js themes not found in assets. Please ensure these files exist:
|
|
59
|
+
- ${lightThemePath}
|
|
60
|
+
- ${darkThemePath}
|
|
61
|
+
Syntax highlighting may not work correctly.`);
|
|
62
|
+
if (options.isDev) {
|
|
63
|
+
highlightWarningShown = true; // Mark as shown for this dev session
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
const markdownFiles = await findMarkdownFiles(SRC_DIR);
|
|
70
|
+
if (markdownFiles.length === 0) {
|
|
71
|
+
console.warn(`โ ๏ธ No Markdown files found in ${SRC_DIR}. Nothing to build.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!options.isDev) {
|
|
75
|
+
console.log(`๐ Found ${markdownFiles.length} markdown files.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Array to collect information about all processed pages for sitemap
|
|
79
|
+
const processedPages = [];
|
|
80
|
+
|
|
81
|
+
// Extract a flattened navigation array for prev/next links
|
|
82
|
+
const flatNavigation = [];
|
|
83
|
+
|
|
84
|
+
// Helper function to create a normalized path for navigation matching
|
|
85
|
+
function createNormalizedPath(item) {
|
|
86
|
+
if (!item.path) return null;
|
|
87
|
+
return item.path.startsWith('/') ? item.path : '/' + item.path;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractNavigationItems(items, parentPath = '') {
|
|
91
|
+
if (!items || !Array.isArray(items)) return;
|
|
92
|
+
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
if (item.external) continue; // Skip external links
|
|
95
|
+
|
|
96
|
+
// Only include items with paths (not section headers without links)
|
|
97
|
+
if (item.path) {
|
|
98
|
+
// Normalize path - ensure leading slash
|
|
99
|
+
let normalizedPath = createNormalizedPath(item);
|
|
100
|
+
|
|
101
|
+
// For parent items with children, ensure path ends with / (folders)
|
|
102
|
+
// This helps with matching in the navigation template
|
|
103
|
+
if (item.children && item.children.length > 0) {
|
|
104
|
+
// If path from config doesn't end with slash, add it
|
|
105
|
+
if (!item.path.endsWith('/') && !normalizedPath.endsWith('/')) {
|
|
106
|
+
normalizedPath += '/';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
flatNavigation.push({
|
|
111
|
+
title: item.title,
|
|
112
|
+
path: normalizedPath,
|
|
113
|
+
fullPath: item.path, // Original path as defined in config
|
|
114
|
+
isParent: item.children && item.children.length > 0 // Mark if it's a parent with children
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Process children (depth first to maintain document outline order)
|
|
119
|
+
if (item.children && Array.isArray(item.children)) {
|
|
120
|
+
extractNavigationItems(item.children, item.path || parentPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract navigation items into flat array
|
|
126
|
+
extractNavigationItems(config.navigation);
|
|
127
|
+
|
|
128
|
+
for (const mdFilePath of markdownFiles) {
|
|
129
|
+
const relativeMdPath = path.relative(SRC_DIR, mdFilePath);
|
|
130
|
+
|
|
131
|
+
// Pretty URL handling - properly handle index.md files in subfolders
|
|
132
|
+
let outputHtmlPath;
|
|
133
|
+
const fileName = path.basename(relativeMdPath);
|
|
134
|
+
const isIndexFile = fileName === 'index.md';
|
|
135
|
+
|
|
136
|
+
if (isIndexFile) {
|
|
137
|
+
// For any index.md file (in root or subfolder), convert to index.html in the same folder
|
|
138
|
+
const dirPath = path.dirname(relativeMdPath);
|
|
139
|
+
outputHtmlPath = path.join(dirPath, 'index.html');
|
|
140
|
+
} else {
|
|
141
|
+
// For non-index files, create a folder with index.html
|
|
142
|
+
outputHtmlPath = relativeMdPath.replace(/\.md$/, '/index.html');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const finalOutputHtmlPath = path.join(OUTPUT_DIR, outputHtmlPath);
|
|
146
|
+
|
|
147
|
+
const depth = outputHtmlPath.split(path.sep).length - 1;
|
|
148
|
+
const relativePathToRoot = depth > 0 ? '../'.repeat(depth) : './';
|
|
149
|
+
|
|
150
|
+
const { frontmatter, htmlContent, headings } = await processMarkdownFile(mdFilePath);
|
|
151
|
+
|
|
152
|
+
// Get the URL path for navigation
|
|
153
|
+
let currentPagePathForNav;
|
|
154
|
+
let normalizedPath;
|
|
155
|
+
|
|
156
|
+
if (isIndexFile) {
|
|
157
|
+
// For index.md files, the nav path should be the directory itself with trailing slash
|
|
158
|
+
const dirPath = path.dirname(relativeMdPath);
|
|
159
|
+
if (dirPath === '.') {
|
|
160
|
+
// Root index.md
|
|
161
|
+
currentPagePathForNav = 'index.html';
|
|
162
|
+
normalizedPath = '/';
|
|
163
|
+
} else {
|
|
164
|
+
// Subfolder index.md - simple format: directory-name/
|
|
165
|
+
currentPagePathForNav = dirPath + '/';
|
|
166
|
+
normalizedPath = '/' + dirPath;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// For non-index files, the path should be the file name with trailing slash
|
|
170
|
+
const pathWithoutExt = relativeMdPath.replace(/\.md$/, '');
|
|
171
|
+
currentPagePathForNav = pathWithoutExt + '/';
|
|
172
|
+
normalizedPath = '/' + pathWithoutExt;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Convert Windows backslashes to forward slashes for web paths
|
|
176
|
+
currentPagePathForNav = currentPagePathForNav.replace(/\\/g, '/');
|
|
177
|
+
|
|
178
|
+
// Log navigation paths for debugging
|
|
179
|
+
// Uncomment this line when debugging:
|
|
180
|
+
// logNavigationPaths(mdFilePath, currentPagePathForNav, normalizedPath);
|
|
181
|
+
|
|
182
|
+
const navigationHtml = await generateNavigationHtml(
|
|
183
|
+
config.navigation,
|
|
184
|
+
currentPagePathForNav,
|
|
185
|
+
relativePathToRoot,
|
|
186
|
+
config
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Find current page in navigation for prev/next links
|
|
190
|
+
let prevPage = null;
|
|
191
|
+
let nextPage = null;
|
|
192
|
+
let currentPageIndex = -1;
|
|
193
|
+
|
|
194
|
+
// Find the current page in flatNavigation
|
|
195
|
+
currentPageIndex = flatNavigation.findIndex(item => {
|
|
196
|
+
// Direct path match
|
|
197
|
+
if (item.path === normalizedPath) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Special handling for parent folders
|
|
202
|
+
if (isIndexFile && item.path.endsWith('/')) {
|
|
203
|
+
// Remove trailing slash for comparison
|
|
204
|
+
const itemPathWithoutSlash = item.path.slice(0, -1);
|
|
205
|
+
return itemPathWithoutSlash === normalizedPath;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (currentPageIndex >= 0) {
|
|
212
|
+
// Get previous and next pages if they exist
|
|
213
|
+
if (currentPageIndex > 0) {
|
|
214
|
+
prevPage = flatNavigation[currentPageIndex - 1];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (currentPageIndex < flatNavigation.length - 1) {
|
|
218
|
+
nextPage = flatNavigation[currentPageIndex + 1];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Convert page paths to proper URLs for links
|
|
223
|
+
if (prevPage) {
|
|
224
|
+
// Format the previous page URL, avoiding double slashes
|
|
225
|
+
if (prevPage.path === '/') {
|
|
226
|
+
prevPage.url = relativePathToRoot + 'index.html';
|
|
227
|
+
} else {
|
|
228
|
+
// Remove leading slash and ensure clean path
|
|
229
|
+
const cleanPath = prevPage.path.substring(1).replace(/\/+$/, '');
|
|
230
|
+
prevPage.url = relativePathToRoot + cleanPath + '/';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (nextPage) {
|
|
235
|
+
// Format the next page URL, avoiding double slashes
|
|
236
|
+
if (nextPage.path === '/') {
|
|
237
|
+
nextPage.url = relativePathToRoot + 'index.html';
|
|
238
|
+
} else {
|
|
239
|
+
// Remove leading slash and ensure clean path
|
|
240
|
+
const cleanPath = nextPage.path.substring(1).replace(/\/+$/, '');
|
|
241
|
+
nextPage.url = relativePathToRoot + cleanPath + '/';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const pageDataForTemplate = {
|
|
246
|
+
content: htmlContent,
|
|
247
|
+
pageTitle: frontmatter.title || 'Untitled',
|
|
248
|
+
siteTitle: config.siteTitle,
|
|
249
|
+
navigationHtml,
|
|
250
|
+
relativePathToRoot: relativePathToRoot,
|
|
251
|
+
config: config, // Pass full config
|
|
252
|
+
frontmatter: frontmatter,
|
|
253
|
+
outputPath: outputHtmlPath, // Relative path from outputDir root
|
|
254
|
+
prettyUrl: true, // Flag to indicate we're using pretty URLs
|
|
255
|
+
prevPage: prevPage, // Previous page in navigation
|
|
256
|
+
nextPage: nextPage, // Next page in navigation
|
|
257
|
+
currentPagePath: normalizedPath, // Pass the normalized path for active state detection
|
|
258
|
+
headings: headings || [], // Pass headings for TOC
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const pageHtml = await generateHtmlPage(pageDataForTemplate);
|
|
262
|
+
|
|
263
|
+
await fs.ensureDir(path.dirname(finalOutputHtmlPath));
|
|
264
|
+
await fs.writeFile(finalOutputHtmlPath, pageHtml);
|
|
265
|
+
|
|
266
|
+
// Add to processed pages for sitemap
|
|
267
|
+
processedPages.push({
|
|
268
|
+
outputPath: isIndexFile
|
|
269
|
+
? (path.dirname(relativeMdPath) === '.' ? 'index.html' : path.dirname(relativeMdPath) + '/')
|
|
270
|
+
: outputHtmlPath.replace(/\\/g, '/').replace(/\/index\.html$/, '/'),
|
|
271
|
+
frontmatter: frontmatter
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Generate sitemap if enabled in config
|
|
276
|
+
if (config.plugins?.sitemap !== false) {
|
|
277
|
+
try {
|
|
278
|
+
await generateSitemap(config, processedPages, OUTPUT_DIR);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error(`โ Error generating sitemap: ${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// findMarkdownFiles function remains the same
|
|
286
|
+
async function findMarkdownFiles(dir) {
|
|
287
|
+
let files = [];
|
|
288
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
289
|
+
for (const item of items) {
|
|
290
|
+
const fullPath = path.join(dir, item.name);
|
|
291
|
+
if (item.isDirectory()) {
|
|
292
|
+
files = files.concat(await findMarkdownFiles(fullPath));
|
|
293
|
+
} else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
|
|
294
|
+
files.push(fullPath);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return files;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = { buildSite };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/commands/dev.js
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const WebSocket = require('ws');
|
|
5
|
+
const chokidar = require('chokidar');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const { buildSite } = require('./build'); // Re-use the build logic
|
|
9
|
+
const { loadConfig } = require('../core/config-loader');
|
|
10
|
+
|
|
11
|
+
async function startDevServer(configPathOption) {
|
|
12
|
+
let config = await loadConfig(configPathOption); // Load initial config
|
|
13
|
+
const CWD = process.cwd(); // Current Working Directory where user runs `docmd dev`
|
|
14
|
+
|
|
15
|
+
// Function to resolve paths based on current config
|
|
16
|
+
const resolveConfigPaths = (currentConfig) => {
|
|
17
|
+
return {
|
|
18
|
+
outputDir: path.resolve(CWD, currentConfig.outputDir),
|
|
19
|
+
srcDirToWatch: path.resolve(CWD, currentConfig.srcDir),
|
|
20
|
+
configFileToWatch: path.resolve(CWD, configPathOption), // Path to the config file itself
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let paths = resolveConfigPaths(config);
|
|
25
|
+
|
|
26
|
+
// docmd's internal templates and assets (for live dev of docmd itself)
|
|
27
|
+
const DOCMD_TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates');
|
|
28
|
+
const DOCMD_ASSETS_DIR = path.resolve(__dirname, '..', 'assets');
|
|
29
|
+
|
|
30
|
+
const app = express();
|
|
31
|
+
const server = http.createServer(app);
|
|
32
|
+
const wss = new WebSocket.Server({ server });
|
|
33
|
+
|
|
34
|
+
let wsClients = new Set();
|
|
35
|
+
wss.on('connection', (ws) => {
|
|
36
|
+
wsClients.add(ws);
|
|
37
|
+
// console.log('Client connected to WebSocket. Total clients:', wsClients.size);
|
|
38
|
+
ws.on('close', () => {
|
|
39
|
+
wsClients.delete(ws);
|
|
40
|
+
// console.log('Client disconnected. Total clients:', wsClients.size);
|
|
41
|
+
});
|
|
42
|
+
ws.on('error', (error) => {
|
|
43
|
+
console.error('WebSocket error on client:', error);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
wss.on('error', (error) => {
|
|
47
|
+
console.error('WebSocket Server error:', error);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
function broadcastReload() {
|
|
52
|
+
// console.log('Broadcasting reload to', wsClients.size, 'clients');
|
|
53
|
+
wsClients.forEach(client => {
|
|
54
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
55
|
+
client.send('reload');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Inject live reload script into HTML
|
|
61
|
+
app.use((req, res, next) => {
|
|
62
|
+
if (req.path.endsWith('.html')) {
|
|
63
|
+
const originalSend = res.send;
|
|
64
|
+
res.send = function (body) {
|
|
65
|
+
if (typeof body === 'string') {
|
|
66
|
+
const liveReloadScript = `
|
|
67
|
+
<script>
|
|
68
|
+
const socket = new WebSocket(\`ws://\${window.location.host}\`);
|
|
69
|
+
socket.onmessage = function(event) { if (event.data === 'reload') window.location.reload(); };
|
|
70
|
+
socket.onerror = function(error) { console.error('WebSocket Client Error:', error); };
|
|
71
|
+
// socket.onopen = function() { console.log('WebSocket Client Connected'); };
|
|
72
|
+
// socket.onclose = function() { console.log('WebSocket Client Disconnected'); };
|
|
73
|
+
</script>
|
|
74
|
+
`;
|
|
75
|
+
body = body.replace('</body>', `${liveReloadScript}</body>`);
|
|
76
|
+
}
|
|
77
|
+
originalSend.call(this, body);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
next();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Serve static files from the output directory
|
|
84
|
+
// This middleware needs to be dynamic if outputDir changes
|
|
85
|
+
let staticMiddleware = express.static(paths.outputDir);
|
|
86
|
+
app.use((req, res, next) => staticMiddleware(req, res, next));
|
|
87
|
+
|
|
88
|
+
// Initial build
|
|
89
|
+
console.log('๐ Performing initial build for dev server...');
|
|
90
|
+
try {
|
|
91
|
+
await buildSite(configPathOption, { isDev: true }); // Use the original config path option
|
|
92
|
+
console.log('โ
Initial build complete.');
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('โ Initial build failed:', error.message, error.stack);
|
|
95
|
+
// Optionally, don't start server if initial build fails, or serve a specific error page.
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// Watch for changes
|
|
100
|
+
const watchedPaths = [
|
|
101
|
+
paths.srcDirToWatch,
|
|
102
|
+
paths.configFileToWatch,
|
|
103
|
+
DOCMD_TEMPLATES_DIR,
|
|
104
|
+
DOCMD_ASSETS_DIR
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
console.log(`๐ Watching for changes in:
|
|
108
|
+
- Source: ${paths.srcDirToWatch}
|
|
109
|
+
- Config: ${paths.configFileToWatch}
|
|
110
|
+
- docmd Templates: ${DOCMD_TEMPLATES_DIR} (internal)
|
|
111
|
+
- docmd Assets: ${DOCMD_ASSETS_DIR} (internal)
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
const watcher = chokidar.watch(watchedPaths, {
|
|
115
|
+
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
|
116
|
+
persistent: true,
|
|
117
|
+
ignoreInitial: true, // Don't trigger for initial scan
|
|
118
|
+
awaitWriteFinish: { // Helps with rapid saves or large file writes
|
|
119
|
+
stabilityThreshold: 100,
|
|
120
|
+
pollInterval: 100
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
watcher.on('all', async (event, filePath) => {
|
|
125
|
+
const relativeFilePath = path.relative(CWD, filePath);
|
|
126
|
+
console.log(`๐ Detected ${event} in ${relativeFilePath}. Rebuilding...`);
|
|
127
|
+
try {
|
|
128
|
+
if (filePath === paths.configFileToWatch) {
|
|
129
|
+
console.log('Config file changed. Reloading configuration...');
|
|
130
|
+
config = await loadConfig(configPathOption); // Reload config
|
|
131
|
+
const newPaths = resolveConfigPaths(config);
|
|
132
|
+
|
|
133
|
+
// Update watcher if srcDir changed - Chokidar doesn't easily support dynamic path changes after init.
|
|
134
|
+
// For simplicity, we might need to restart the watcher or inform user to restart dev server if srcDir/outputDir change.
|
|
135
|
+
// For now, we'll at least update the static server path.
|
|
136
|
+
if (newPaths.outputDir !== paths.outputDir) {
|
|
137
|
+
console.log(`Output directory changed from ${paths.outputDir} to ${newPaths.outputDir}. Updating static server.`);
|
|
138
|
+
staticMiddleware = express.static(newPaths.outputDir);
|
|
139
|
+
}
|
|
140
|
+
// If srcDirToWatch changes, chokidar won't automatically pick it up.
|
|
141
|
+
// A full dev server restart would be more robust for such config changes.
|
|
142
|
+
// For now, the old srcDir will still be watched.
|
|
143
|
+
paths = newPaths; // Update paths for next build reference
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await buildSite(configPathOption, { isDev: true }); // Re-build using the potentially updated config path
|
|
147
|
+
broadcastReload();
|
|
148
|
+
console.log('โ
Rebuild complete. Browser should refresh.');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('โ Rebuild failed:', error.message, error.stack);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
watcher.on('error', error => console.error(`Watcher error: ${error}`));
|
|
155
|
+
|
|
156
|
+
const PORT = process.env.PORT || 3000;
|
|
157
|
+
server.listen(PORT, async () => {
|
|
158
|
+
// Check if index.html exists after initial build
|
|
159
|
+
const indexHtmlPath = path.join(paths.outputDir, 'index.html');
|
|
160
|
+
if (!await fs.pathExists(indexHtmlPath)) {
|
|
161
|
+
console.warn(`โ ๏ธ Warning: ${indexHtmlPath} not found after initial build.
|
|
162
|
+
The dev server is running, but you might see a 404 for the root page.
|
|
163
|
+
Ensure your '${config.srcDir}' directory contains an 'index.md' or your navigation points to existing files.`);
|
|
164
|
+
}
|
|
165
|
+
console.log(`๐ Dev server started at http://localhost:${PORT}`);
|
|
166
|
+
console.log(`Serving content from: ${paths.outputDir}`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Graceful shutdown
|
|
170
|
+
process.on('SIGINT', () => {
|
|
171
|
+
console.log('\n๐ Shutting down dev server...');
|
|
172
|
+
watcher.close();
|
|
173
|
+
wss.close(() => {
|
|
174
|
+
server.close(() => {
|
|
175
|
+
console.log('Server closed.');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { startDevServer };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const defaultConfigContent = `// config.js
|
|
5
|
+
module.exports = {
|
|
6
|
+
siteTitle: 'My Awesome Project Docs',
|
|
7
|
+
srcDir: 'docs',
|
|
8
|
+
outputDir: 'site',
|
|
9
|
+
theme: {
|
|
10
|
+
defaultMode: 'light', // 'light' or 'dark'
|
|
11
|
+
},
|
|
12
|
+
navigation: [
|
|
13
|
+
{ title: 'Home', path: '/' }, // Corresponds to docs/index.md
|
|
14
|
+
// {
|
|
15
|
+
// title: 'Category',
|
|
16
|
+
// children: [
|
|
17
|
+
// { title: 'Page 1', path: '/category/page1' },
|
|
18
|
+
// ],
|
|
19
|
+
// },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const defaultIndexMdContent = `---
|
|
25
|
+
title: "Welcome"
|
|
26
|
+
description: "Your documentation starts here."
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Hello, docmd!
|
|
30
|
+
|
|
31
|
+
Start writing your Markdown content here.
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
async function initProject() {
|
|
35
|
+
const baseDir = process.cwd();
|
|
36
|
+
const docsDir = path.join(baseDir, 'docs');
|
|
37
|
+
const configFile = path.join(baseDir, 'config.js');
|
|
38
|
+
const indexMdFile = path.join(docsDir, 'index.md');
|
|
39
|
+
|
|
40
|
+
if (await fs.pathExists(configFile) || await fs.pathExists(docsDir)) {
|
|
41
|
+
console.warn('โ ๏ธ `docs/` directory or `config.js` already exists. Skipping creation to avoid overwriting.');
|
|
42
|
+
} else {
|
|
43
|
+
await fs.ensureDir(docsDir);
|
|
44
|
+
await fs.writeFile(configFile, defaultConfigContent, 'utf8');
|
|
45
|
+
await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
|
|
46
|
+
console.log('๐ Created `config.js`');
|
|
47
|
+
console.log('๐ Created `docs/` directory with a sample `index.md`');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { initProject };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
|
|
4
|
+
async function loadConfig(configPath) {
|
|
5
|
+
const absoluteConfigPath = path.resolve(process.cwd(), configPath);
|
|
6
|
+
if (!await fs.pathExists(absoluteConfigPath)) {
|
|
7
|
+
throw new Error(`Configuration file not found: ${absoluteConfigPath}`);
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
// Clear require cache to always get the freshest config
|
|
11
|
+
delete require.cache[require.resolve(absoluteConfigPath)];
|
|
12
|
+
const config = require(absoluteConfigPath);
|
|
13
|
+
|
|
14
|
+
// Basic validation and defaults
|
|
15
|
+
if (!config.siteTitle) throw new Error('`siteTitle` is missing in config.js');
|
|
16
|
+
config.srcDir = config.srcDir || 'docs';
|
|
17
|
+
config.outputDir = config.outputDir || 'site';
|
|
18
|
+
config.theme = config.theme || {};
|
|
19
|
+
config.theme.defaultMode = config.theme.defaultMode || 'light';
|
|
20
|
+
config.navigation = config.navigation || [{ title: 'Home', path: '/' }];
|
|
21
|
+
|
|
22
|
+
return config;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw new Error(`Error loading or parsing config file ${absoluteConfigPath}: ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { loadConfig };
|