@docmd/core 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/LICENSE +21 -0
- package/README.md +166 -0
- package/bin/docmd.js +52 -0
- package/package.json +23 -0
- package/src/commands/build.js +262 -0
- package/src/commands/dev.js +353 -0
- package/src/commands/init.js +277 -0
- package/src/commands/live.js +15 -0
- package/src/utils/config-loader.js +38 -0
- package/src/utils/fs-utils.js +40 -0
- package/src/utils/logger.js +21 -0
- package/src/utils/plugin-loader.js +90 -0
- package/src/utils/plugin-runner.js +31 -0
|
@@ -0,0 +1,353 @@
|
|
|
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('../utils/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('../utils/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
|
+
console.log(chalk.yellow(`⚠️ 404 Not Found: ${req.url}`));
|
|
114
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
115
|
+
res.end('<h1>404 Not Found</h1><p>docmd dev server</p>');
|
|
116
|
+
} else {
|
|
117
|
+
res.writeHead(500);
|
|
118
|
+
res.end(`Server Error: ${err.code}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- 2. Helper Utilities ---
|
|
124
|
+
|
|
125
|
+
function formatPathForDisplay(absolutePath, cwd) {
|
|
126
|
+
const relativePath = path.relative(cwd, absolutePath);
|
|
127
|
+
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
128
|
+
return `./${relativePath}`;
|
|
129
|
+
}
|
|
130
|
+
return relativePath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getNetworkIp() {
|
|
134
|
+
const interfaces = os.networkInterfaces();
|
|
135
|
+
for (const name of Object.keys(interfaces)) {
|
|
136
|
+
for (const iface of interfaces[name]) {
|
|
137
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
138
|
+
return iface.address;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- 3. Main Dev Function ---
|
|
146
|
+
|
|
147
|
+
async function startDevServer(configPathOption, options = { preserve: false, port: undefined }) {
|
|
148
|
+
let config = await loadConfig(configPathOption);
|
|
149
|
+
const CWD = process.cwd();
|
|
150
|
+
|
|
151
|
+
// Config Fallback Logic
|
|
152
|
+
let actualConfigPath = path.resolve(CWD, configPathOption);
|
|
153
|
+
if (configPathOption === 'docmd.config.js' && !await fs.pathExists(actualConfigPath)) {
|
|
154
|
+
const legacyPath = path.resolve(CWD, 'config.js');
|
|
155
|
+
if (await fs.pathExists(legacyPath)) {
|
|
156
|
+
actualConfigPath = legacyPath;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const resolveConfigPaths = (currentConfig) => {
|
|
161
|
+
return {
|
|
162
|
+
outputDir: path.resolve(CWD, currentConfig.outputDir),
|
|
163
|
+
srcDirToWatch: path.resolve(CWD, currentConfig.srcDir),
|
|
164
|
+
configFileToWatch: actualConfigPath,
|
|
165
|
+
userAssetsDir: path.resolve(CWD, 'assets'),
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
let paths = resolveConfigPaths(config);
|
|
170
|
+
const DOCMD_ROOT = path.resolve(__dirname, '..');
|
|
171
|
+
|
|
172
|
+
// --- Create Native Server ---
|
|
173
|
+
const server = http.createServer((req, res) => {
|
|
174
|
+
serveStatic(req, res, paths.outputDir);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let wss; // WebSocket instance (initialized later)
|
|
178
|
+
|
|
179
|
+
function broadcastReload() {
|
|
180
|
+
if (wss) {
|
|
181
|
+
wss.clients.forEach((client) => {
|
|
182
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
183
|
+
client.send('reload');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Initial Build ---
|
|
190
|
+
console.log(chalk.blue('🚀 Performing initial build...'));
|
|
191
|
+
try {
|
|
192
|
+
await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(chalk.red('❌ Initial build failed:'), error.message);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- Watcher Setup ---
|
|
198
|
+
const userAssetsDirExists = await fs.pathExists(paths.userAssetsDir);
|
|
199
|
+
const watchedPaths = [paths.srcDirToWatch, paths.configFileToWatch];
|
|
200
|
+
if (userAssetsDirExists) watchedPaths.push(paths.userAssetsDir);
|
|
201
|
+
|
|
202
|
+
if (process.env.DOCMD_DEV === 'true') {
|
|
203
|
+
watchedPaths.push(
|
|
204
|
+
path.join(DOCMD_ROOT, 'templates'),
|
|
205
|
+
path.join(DOCMD_ROOT, 'assets'),
|
|
206
|
+
path.join(DOCMD_ROOT, 'core'),
|
|
207
|
+
path.join(DOCMD_ROOT, 'plugins')
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(chalk.dim('\n👀 Watching for changes in:'));
|
|
212
|
+
console.log(chalk.dim(` - Source: ${chalk.cyan(formatPathForDisplay(paths.srcDirToWatch, CWD))}`));
|
|
213
|
+
console.log(chalk.dim(` - Config: ${chalk.cyan(formatPathForDisplay(paths.configFileToWatch, CWD))}`));
|
|
214
|
+
if (userAssetsDirExists) {
|
|
215
|
+
console.log(chalk.dim(` - Assets: ${chalk.cyan(formatPathForDisplay(paths.userAssetsDir, CWD))}`));
|
|
216
|
+
}
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
const watcher = chokidar.watch(watchedPaths, {
|
|
220
|
+
ignored: /(^|[\/\\])\../,
|
|
221
|
+
persistent: true,
|
|
222
|
+
ignoreInitial: true,
|
|
223
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 100 }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
watcher.on('all', async (event, filePath) => {
|
|
227
|
+
const relativeFilePath = path.relative(CWD, filePath);
|
|
228
|
+
process.stdout.write(chalk.dim(`↻ Change in ${relativeFilePath}... `));
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
if (filePath === paths.configFileToWatch) {
|
|
232
|
+
config = await loadConfig(configPathOption);
|
|
233
|
+
// Note: With native server, we don't need to restart middleware,
|
|
234
|
+
// serveStatic reads from disk dynamically on every request.
|
|
235
|
+
paths = resolveConfigPaths(config);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await buildSite(configPathOption, { isDev: true, preserve: options.preserve, noDoubleProcessing: true });
|
|
239
|
+
broadcastReload();
|
|
240
|
+
process.stdout.write(chalk.green('Done.\n'));
|
|
241
|
+
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(chalk.red('\n❌ Rebuild failed:'), error.message);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// --- Server Startup Logic (Port Checking) ---
|
|
248
|
+
const PORT = parseInt(options.port || process.env.PORT || 3000, 10);
|
|
249
|
+
const MAX_PORT_ATTEMPTS = 10;
|
|
250
|
+
|
|
251
|
+
function checkPortInUse(port) {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
const tester = http.createServer()
|
|
254
|
+
.once('error', (err) => {
|
|
255
|
+
if (err.code === 'EADDRINUSE') resolve(true);
|
|
256
|
+
else resolve(false);
|
|
257
|
+
})
|
|
258
|
+
.once('listening', () => {
|
|
259
|
+
tester.close(() => resolve(false));
|
|
260
|
+
})
|
|
261
|
+
.listen(port, '0.0.0.0');
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function askUserConfirmation() {
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
const rl = readline.createInterface({
|
|
268
|
+
input: process.stdin,
|
|
269
|
+
output: process.stdout
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
console.log(chalk.yellow(`\n⚠️ Port ${PORT} is already in use.`));
|
|
273
|
+
console.log(chalk.yellow(` Another instance of docmd (or another app) might be running.`));
|
|
274
|
+
|
|
275
|
+
rl.question(' Do you want to start another instance on a different port? (Y/n) ', (answer) => {
|
|
276
|
+
rl.close();
|
|
277
|
+
const isYes = answer.trim().toLowerCase() === 'y' || answer.trim() === '';
|
|
278
|
+
resolve(isYes);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function tryStartServer(port, attempt = 1) {
|
|
284
|
+
server.listen(port, '0.0.0.0')
|
|
285
|
+
.on('listening', async () => {
|
|
286
|
+
// Initialize WebSocket Server only AFTER successful listen
|
|
287
|
+
wss = new WebSocket.Server({ server });
|
|
288
|
+
wss.on('error', (e) => console.error('WebSocket Error:', e.message));
|
|
289
|
+
|
|
290
|
+
const indexHtmlPath = path.join(paths.outputDir, 'index.html');
|
|
291
|
+
const networkIp = getNetworkIp();
|
|
292
|
+
|
|
293
|
+
const localUrl = `http://127.0.0.1:${port}`;
|
|
294
|
+
const networkUrl = networkIp ? `http://${networkIp}:${port}` : null;
|
|
295
|
+
|
|
296
|
+
const border = chalk.gray('────────────────────────────────────────');
|
|
297
|
+
console.log(border);
|
|
298
|
+
console.log(` ${chalk.bold.green('SERVER RUNNING')} ${chalk.dim(`(v${require('../../package.json').version})`)}`);
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log(` ${chalk.bold('Local:')} ${chalk.cyan(localUrl)}`);
|
|
301
|
+
if (networkUrl) {
|
|
302
|
+
console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);
|
|
303
|
+
}
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log(` ${chalk.dim('Serving:')} ${formatPathForDisplay(paths.outputDir, CWD)}`);
|
|
306
|
+
console.log(border);
|
|
307
|
+
console.log('');
|
|
308
|
+
|
|
309
|
+
if (!await fs.pathExists(indexHtmlPath)) {
|
|
310
|
+
console.warn(chalk.yellow(`⚠️ Warning: Root index.html not found.`));
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
.on('error', (err) => {
|
|
314
|
+
if (err.code === 'EADDRINUSE') {
|
|
315
|
+
server.close();
|
|
316
|
+
tryStartServer(port + 1);
|
|
317
|
+
} else {
|
|
318
|
+
console.error(chalk.red(`Failed to start server: ${err.message}`));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- Main Execution Flow ---
|
|
325
|
+
(async () => {
|
|
326
|
+
// Skip check if user manually specified port flag
|
|
327
|
+
if (options.port) {
|
|
328
|
+
tryStartServer(PORT);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const isBusy = await checkPortInUse(PORT);
|
|
333
|
+
|
|
334
|
+
if (isBusy) {
|
|
335
|
+
const shouldProceed = await askUserConfirmation();
|
|
336
|
+
if (!shouldProceed) {
|
|
337
|
+
console.log(chalk.dim('Cancelled.'));
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
tryStartServer(PORT + 1);
|
|
341
|
+
} else {
|
|
342
|
+
tryStartServer(PORT);
|
|
343
|
+
}
|
|
344
|
+
})();
|
|
345
|
+
|
|
346
|
+
process.on('SIGINT', () => {
|
|
347
|
+
console.log(chalk.yellow('\n🛑 Shutting down...'));
|
|
348
|
+
watcher.close();
|
|
349
|
+
process.exit(0);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = { startDevServer };
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Source file from the docmd project — https://github.com/docmd-io/docmd
|
|
2
|
+
|
|
3
|
+
const fs = require('../utils/fs-utils');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { version } = require('../../package.json');
|
|
7
|
+
|
|
8
|
+
const defaultConfigContent = `// docmd.config.js
|
|
9
|
+
module.exports = {
|
|
10
|
+
// --- Core Metadata ---
|
|
11
|
+
siteTitle: 'My Documentation',
|
|
12
|
+
siteUrl: '', // e.g. https://mysite.com (Critical for SEO/Sitemap)
|
|
13
|
+
|
|
14
|
+
// --- Branding ---
|
|
15
|
+
logo: {
|
|
16
|
+
light: 'assets/images/docmd-logo-dark.png',
|
|
17
|
+
dark: 'assets/images/docmd-logo-light.png',
|
|
18
|
+
alt: 'Logo',
|
|
19
|
+
href: './',
|
|
20
|
+
},
|
|
21
|
+
favicon: 'assets/favicon.ico',
|
|
22
|
+
|
|
23
|
+
// --- Source & Output ---
|
|
24
|
+
srcDir: 'docs',
|
|
25
|
+
outputDir: 'site',
|
|
26
|
+
|
|
27
|
+
// --- Theme & Layout ---
|
|
28
|
+
theme: {
|
|
29
|
+
name: 'sky', // Options: 'default', 'sky', 'ruby', 'retro'
|
|
30
|
+
defaultMode: 'system', // 'light', 'dark', or 'system'
|
|
31
|
+
enableModeToggle: true, // Show mode toggle button
|
|
32
|
+
positionMode: 'top', // 'top' or 'bottom'
|
|
33
|
+
codeHighlight: true, // Enable Highlight.js
|
|
34
|
+
customCss: [], // e.g. ['assets/css/custom.css']
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// --- Features ---
|
|
38
|
+
search: true, // Built-in offline search
|
|
39
|
+
minify: true, // Minify HTML/CSS/JS in build
|
|
40
|
+
autoTitleFromH1: true, // Auto-generate page title from first H1
|
|
41
|
+
copyCode: true, // Show "copy" button on code blocks
|
|
42
|
+
pageNavigation: true, // Prev/Next buttons at bottom
|
|
43
|
+
|
|
44
|
+
// --- Navigation (Sidebar) ---
|
|
45
|
+
navigation: [
|
|
46
|
+
{ title: 'Introduction', path: '/', icon: 'home' },
|
|
47
|
+
{
|
|
48
|
+
title: 'Guide',
|
|
49
|
+
icon: 'book-open',
|
|
50
|
+
collapsible: true,
|
|
51
|
+
children: [
|
|
52
|
+
{ title: 'Getting Started', path: 'https://docs.docmd.io/getting-started/installation', icon: 'rocket', external: true },
|
|
53
|
+
{ title: 'Configuration', path: 'https://docs.docmd.io/configuration', icon: 'settings', external: true },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{ title: 'Live Editor', path: 'https://live.docmd.io', icon: 'pencil-ruler', external: true },
|
|
57
|
+
{ title: 'GitHub', path: 'https://github.com/docmd-io/docmd', icon: 'github', external: true },
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
// --- Plugins ---
|
|
61
|
+
plugins: {
|
|
62
|
+
seo: {
|
|
63
|
+
defaultDescription: 'Documentation built with docmd.',
|
|
64
|
+
openGraph: {
|
|
65
|
+
defaultImage: '', // e.g. 'assets/images/og-image.png'
|
|
66
|
+
},
|
|
67
|
+
twitter: {
|
|
68
|
+
cardType: 'summary_large_image',
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
analytics: {
|
|
72
|
+
googleV4: {
|
|
73
|
+
measurementId: 'G-X9WTDL262N' // Replace with your Google Analytics Measurement ID
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
sitemap: {
|
|
77
|
+
defaultChangefreq: 'weekly', // e.g. 'daily', 'weekly', 'monthly'
|
|
78
|
+
defaultPriority: 0.8 // Priority between 0.0 and 1.0
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// --- Footer ---
|
|
83
|
+
footer: '© ' + new Date().getFullYear() + ' My Project. Built with [docmd](https://docmd.io).',
|
|
84
|
+
|
|
85
|
+
// --- Edit Link ---
|
|
86
|
+
editLink: {
|
|
87
|
+
enabled: false,
|
|
88
|
+
baseUrl: 'https://github.com/USERNAME/REPO/edit/main/docs',
|
|
89
|
+
text: 'Edit this page'
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
const defaultIndexMdContent = `---
|
|
95
|
+
title: "Welcome"
|
|
96
|
+
description: "Welcome to your new documentation site."
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
# Welcome to Your Docs
|
|
100
|
+
|
|
101
|
+
Congratulations! You have successfully initialized a new **docmd** project.
|
|
102
|
+
|
|
103
|
+
## 🚀 Quick Start
|
|
104
|
+
|
|
105
|
+
You are currently viewing the content of \`docs/index.md\`.
|
|
106
|
+
|
|
107
|
+
\`\`\`bash
|
|
108
|
+
npm start # Start the dev server
|
|
109
|
+
docmd build # Build for production
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
## ✨ Features Demo
|
|
113
|
+
|
|
114
|
+
docmd comes with built-in components to make your documentation beautiful.
|
|
115
|
+
|
|
116
|
+
::: callout tip
|
|
117
|
+
**Try this:** Edit this file and save it. The browser will live reload instantly!
|
|
118
|
+
:::
|
|
119
|
+
|
|
120
|
+
### Container Examples
|
|
121
|
+
|
|
122
|
+
::: card Flexible Structure
|
|
123
|
+
**Organize your way.**
|
|
124
|
+
Create Markdown files in the \`docs/\` folder and map them in \`docmd.config.js\`.
|
|
125
|
+
:::
|
|
126
|
+
|
|
127
|
+
::: tabs
|
|
128
|
+
== tab "Simple"
|
|
129
|
+
This is a simple tab content.
|
|
130
|
+
|
|
131
|
+
== tab "Nested"
|
|
132
|
+
::: callout info
|
|
133
|
+
You can even nest other components inside tabs!
|
|
134
|
+
:::
|
|
135
|
+
:::
|
|
136
|
+
|
|
137
|
+
## 📚 Next Steps
|
|
138
|
+
|
|
139
|
+
* [Check the Official Documentation](https://docs.docmd.io)
|
|
140
|
+
* [Customize your Theme](https://docs.docmd.io/theming)
|
|
141
|
+
* [Deploy to GitHub Pages](https://docs.docmd.io/deployment)
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const defaultPackageJson = {
|
|
145
|
+
name: "my-docs",
|
|
146
|
+
version: "0.0.1",
|
|
147
|
+
private: true,
|
|
148
|
+
scripts: {
|
|
149
|
+
"dev": "docmd dev",
|
|
150
|
+
"build": "docmd build",
|
|
151
|
+
"preview": "npx serve site"
|
|
152
|
+
},
|
|
153
|
+
dependencies: {
|
|
154
|
+
"@docmd/core": `^${version}`
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
async function initProject() {
|
|
159
|
+
const baseDir = process.cwd();
|
|
160
|
+
const packageJsonFile = path.join(baseDir, 'package.json');
|
|
161
|
+
const configFile = path.join(baseDir, 'docmd.config.js');
|
|
162
|
+
const docsDir = path.join(baseDir, 'docs');
|
|
163
|
+
const indexMdFile = path.join(docsDir, 'index.md');
|
|
164
|
+
const assetsDir = path.join(baseDir, 'assets');
|
|
165
|
+
const assetsCssDir = path.join(assetsDir, 'css');
|
|
166
|
+
const assetsJsDir = path.join(assetsDir, 'js');
|
|
167
|
+
const assetsImagesDir = path.join(assetsDir, 'images');
|
|
168
|
+
|
|
169
|
+
const existingFiles = [];
|
|
170
|
+
const dirExists = {
|
|
171
|
+
docs: false,
|
|
172
|
+
assets: false
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Check if package.json exists
|
|
176
|
+
if (!await fs.pathExists(packageJsonFile)) {
|
|
177
|
+
await fs.writeJson(packageJsonFile, defaultPackageJson, { spaces: 2 });
|
|
178
|
+
console.log('📦 Created `package.json` (Deployment Ready)');
|
|
179
|
+
} else {
|
|
180
|
+
console.log('⏭️ Skipped existing `package.json`');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check each file individually
|
|
184
|
+
if (await fs.pathExists(configFile)) {
|
|
185
|
+
existingFiles.push('docmd.config.js');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for the legacy config.js
|
|
189
|
+
const oldConfigFile = path.join(baseDir, 'config.js');
|
|
190
|
+
if (await fs.pathExists(oldConfigFile)) {
|
|
191
|
+
existingFiles.push('config.js');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check if docs directory exists
|
|
195
|
+
if (await fs.pathExists(docsDir)) {
|
|
196
|
+
dirExists.docs = true;
|
|
197
|
+
if (await fs.pathExists(indexMdFile)) {
|
|
198
|
+
existingFiles.push('docs/index.md');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if assets directory exists
|
|
203
|
+
if (await fs.pathExists(assetsDir)) {
|
|
204
|
+
dirExists.assets = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Determine if we should override existing files
|
|
208
|
+
let shouldOverride = false;
|
|
209
|
+
if (existingFiles.length > 0) {
|
|
210
|
+
console.warn('⚠️ The following files already exist:');
|
|
211
|
+
existingFiles.forEach(file => console.warn(` - ${file}`));
|
|
212
|
+
|
|
213
|
+
const rl = readline.createInterface({
|
|
214
|
+
input: process.stdin,
|
|
215
|
+
output: process.stdout
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const answer = await new Promise(resolve => {
|
|
219
|
+
rl.question('Do you want to override these files? (y/N): ', resolve);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
rl.close();
|
|
223
|
+
|
|
224
|
+
shouldOverride = answer.toLowerCase() === 'y';
|
|
225
|
+
|
|
226
|
+
if (!shouldOverride) {
|
|
227
|
+
console.log('⏭️ Skipping existing files. Will only create new files.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Create docs directory if it doesn't exist
|
|
232
|
+
if (!dirExists.docs) {
|
|
233
|
+
await fs.ensureDir(docsDir);
|
|
234
|
+
console.log('📁 Created `docs/` directory');
|
|
235
|
+
} else {
|
|
236
|
+
console.log('📁 Using existing `docs/` directory');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Create assets directory structure if it doesn't exist
|
|
240
|
+
if (!dirExists.assets) {
|
|
241
|
+
await fs.ensureDir(assetsDir);
|
|
242
|
+
await fs.ensureDir(assetsCssDir);
|
|
243
|
+
await fs.ensureDir(assetsJsDir);
|
|
244
|
+
await fs.ensureDir(assetsImagesDir);
|
|
245
|
+
console.log('📁 Created `assets/` directory with css, js, and images subdirectories');
|
|
246
|
+
} else {
|
|
247
|
+
console.log('📁 Using existing `assets/` directory');
|
|
248
|
+
|
|
249
|
+
if (!await fs.pathExists(assetsCssDir)) await fs.ensureDir(assetsCssDir);
|
|
250
|
+
if (!await fs.pathExists(assetsJsDir)) await fs.ensureDir(assetsJsDir);
|
|
251
|
+
if (!await fs.pathExists(assetsImagesDir)) await fs.ensureDir(assetsImagesDir);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Write config file if it doesn't exist or user confirmed override
|
|
255
|
+
if (!await fs.pathExists(configFile) || shouldOverride) {
|
|
256
|
+
await fs.writeFile(configFile, defaultConfigContent, 'utf8');
|
|
257
|
+
console.log(`📄 ${shouldOverride ? 'Updated' : 'Created'} \`docmd.config.js\``);
|
|
258
|
+
} else {
|
|
259
|
+
console.log('⏭️ Skipped existing `docmd.config.js`');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Write index.md file if it doesn't exist or user confirmed override
|
|
263
|
+
if (!await fs.pathExists(indexMdFile) || shouldOverride) {
|
|
264
|
+
await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
|
|
265
|
+
console.log('📄 Created `docs/index.md`');
|
|
266
|
+
} else if (shouldOverride) {
|
|
267
|
+
await fs.writeFile(indexMdFile, defaultIndexMdContent, 'utf8');
|
|
268
|
+
console.log('📄 Updated `docs/index.md`');
|
|
269
|
+
} else {
|
|
270
|
+
console.log('⏭️ Skipped existing `docs/index.md`');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log('✅ Project initialization complete!');
|
|
274
|
+
console.log('👉 Run `npm install` to setup dependencies.');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = { initProject };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
async function buildLive(options = {}) {
|
|
2
|
+
// Delegate to the standalone package
|
|
3
|
+
const livePkg = require('@docmd/live');
|
|
4
|
+
|
|
5
|
+
// If explicitly asked NOT to serve (for testing), just build
|
|
6
|
+
if (options.serve === false) {
|
|
7
|
+
console.log('🔨 Building Live Editor ...');
|
|
8
|
+
await livePkg.build();
|
|
9
|
+
} else {
|
|
10
|
+
// Default behavior: Build + Serve
|
|
11
|
+
await livePkg.start();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { buildLive };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { validateConfig } = require('@docmd/parser');
|
|
4
|
+
|
|
5
|
+
async function loadConfig(configPath) {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
let absoluteConfigPath = path.resolve(cwd, configPath);
|
|
8
|
+
|
|
9
|
+
// Fallback for default filename
|
|
10
|
+
if (!fs.existsSync(absoluteConfigPath) && configPath === 'docmd.config.js') {
|
|
11
|
+
const legacyPath = path.resolve(cwd, 'config.js');
|
|
12
|
+
if (fs.existsSync(legacyPath)) absoluteConfigPath = legacyPath;
|
|
13
|
+
else throw new Error(`Configuration file not found at: ${absoluteConfigPath}\nRun "docmd init" to create one.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
delete require.cache[require.resolve(absoluteConfigPath)];
|
|
18
|
+
const config = require(absoluteConfigPath);
|
|
19
|
+
|
|
20
|
+
// Validate using Parser
|
|
21
|
+
validateConfig(config);
|
|
22
|
+
|
|
23
|
+
// Apply Defaults
|
|
24
|
+
return {
|
|
25
|
+
base: '/',
|
|
26
|
+
srcDir: 'docs',
|
|
27
|
+
outputDir: 'site',
|
|
28
|
+
...config,
|
|
29
|
+
theme: { defaultMode: 'light', ...config.theme },
|
|
30
|
+
navigation: config.navigation || [{ title: 'Home', path: '/' }]
|
|
31
|
+
};
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (e.message === 'Invalid configuration file.') throw e;
|
|
34
|
+
throw new Error(`Error parsing config file: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { loadConfig };
|