@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 docmd (docmd.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ <div align="center">
2
+
3
+ <!-- PROJECT TITLE -->
4
+ <h1>
5
+ <img src="https://github.com/docmd-io/docmd/blob/main/src/assets/images/docmd-logo-dark.png?raw=true" alt="docmd logo" width="200" />
6
+ <!-- docmd -->
7
+ </h1>
8
+
9
+ <!-- ONE LINE SUMMARY -->
10
+ <p>
11
+ <b>The minimalist, zero-config documentation generator.</b>
12
+ </p>
13
+
14
+ <!-- BADGES -->
15
+ <p>
16
+ <a href="https://www.npmjs.com/package/@docmd/core"><img src="https://img.shields.io/npm/v/@docmd/core.svg?style=flat-square&color=d25353" alt="npm version"></a>
17
+ <a href="https://www.npmjs.com/package/@docmd/core?activeTab=versions"><img src="https://img.shields.io/npm/dt/@docmd/core.svg?style=flat-square&color=38bd24" alt="downloads"></a>
18
+ <a href="https://github.com/docmd-io/docmd/stargazers"><img src="https://img.shields.io/github/stars/docmd-io/docmd?style=flat-square&logo=github" alt="stars"></a>
19
+ <a href="https://github.com/docmd-io/docmd/blob/main/LICENSE"><img src="https://img.shields.io/github/license/docmd-io/docmd.svg?style=flat-square&color=blue" alt="license"></a>
20
+ </p>
21
+
22
+ <!-- MENU -->
23
+ <p>
24
+ <h4>
25
+ <a href="https://docmd.io">View Demo</a> •
26
+ <a href="https://docs.docmd.io/getting-started/installation/">Documentation</a> •
27
+ <a href="https://live.docmd.io">Live Editor</a> •
28
+ <a href="https://github.com/docmd-io/docmd/issues">Report Bug</a>
29
+ </h4>
30
+ </p>
31
+
32
+ <!-- PREVIEW -->
33
+ <p>
34
+ <img width="800" alt="docmd preview" src="https://github.com/user-attachments/assets/1a74d6f7-10f9-41fa-be8a-faeee278dbb9" />
35
+ <br/>
36
+ <sup><i>docmd noStyle page preview in light mode</i></sup>
37
+ </p>
38
+
39
+ </div>
40
+
41
+ ## Features
42
+
43
+ - **Zero Config**: Works out of the box with sensible defaults. Just `init` and go.
44
+ - **Blazing Fast**: Generates **pure, static HTML**. No React hydration lag, no heavy bundles.
45
+ - **Smart Search**: Built-in, **offline-capable** full-text search with fuzzy matching. No API keys required.
46
+ - **Isomorphic Core**: Runs anywhere, Node.js CLI, CI/CD pipelines, or **directly in the browser**.
47
+ - **Rich Content**: Built-in support for Callouts, Cards, Tabs, Steps, Changelogs, and Mermaid diagrams.
48
+ - **Theming**: Beautiful light/dark modes and multiple pre-built themes (`sky`, `ruby`, `retro`).
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ npm install -g @docmd/core
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### CLI
59
+
60
+ The Command Line Interface is the primary way to interact with `docmd`.
61
+
62
+ ```bash
63
+ docmd init # Initialize a new project with config and assets
64
+ docmd dev # Start a local development server with hot-reload
65
+ docmd build # Generate a production-ready static site in ./site
66
+ docmd live # Launch the browser-based Live Editor locally
67
+ ```
68
+
69
+ ### API
70
+
71
+ `docmd` exports its core engine, allowing you to build documentation programmatically within your own Node.js scripts or build tools.
72
+
73
+ ```javascript
74
+ const { build, buildLive } = require('@docmd/core');
75
+
76
+ // Trigger a standard documentation build
77
+ await build('./docmd.config.js', {
78
+ isDev: false,
79
+ preserve: true
80
+ });
81
+
82
+ // Trigger a Live Editor bundle build
83
+ await buildLive();
84
+ ```
85
+
86
+ ### Live Editor
87
+
88
+ `docmd` features a modular architecture that allows the core engine to run client-side.
89
+
90
+ Running `docmd live` builds a standalone web application where you can write Markdown and see the preview instantly without any server-side processing. You can embed the generated `docmd-live.js` bundle to add Markdown capabilities to your own applications.
91
+
92
+ ## Project Structure
93
+
94
+ `docmd` keeps it simple. Your content lives in `docs/`, your config in `docmd.config.js`.
95
+
96
+ ```bash
97
+ my-docs/
98
+ ├── docs/ # Your Markdown files
99
+ │ ├── index.md # Homepage
100
+ │ └── guide.md # Content page
101
+ ├── assets/ # Images and custom CSS
102
+ ├── docmd.config.js # Configuration
103
+ └── package.json
104
+ ```
105
+
106
+ ## Configuration
107
+
108
+ Customize your site in seconds via `docmd.config.js`:
109
+
110
+ ```javascript
111
+ module.exports = {
112
+ siteTitle: 'My Project',
113
+ siteUrl: 'https://mysite.com',
114
+ srcDir: 'docs',
115
+ outputDir: 'site',
116
+
117
+ // Theme Settings
118
+ theme: {
119
+ name: 'sky', // 'default', 'sky', 'ruby', 'retro'
120
+ defaultMode: 'system', // 'light', 'dark', or 'system'
121
+ enableModeToggle: true
122
+ },
123
+
124
+ // Sidebar Navigation
125
+ navigation: [
126
+ { title: 'Home', path: '/', icon: 'home' },
127
+ {
128
+ title: 'Guide',
129
+ icon: 'book',
130
+ children: [
131
+ { title: 'Installation', path: '/guide/install' }
132
+ ]
133
+ }
134
+ ],
135
+
136
+ // Plugins
137
+ plugins: {
138
+ seo: { /* ... */ },
139
+ sitemap: { /* ... */ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ ## Comparison
145
+
146
+ | Feature | docmd | Docusaurus | MkDocs | Mintlify |
147
+ | :--- | :--- | :--- | :--- | :--- |
148
+ | **Language** | **Node.js** | React.js | Python | Proprietary |
149
+ | **Output** | **Static HTML** | React SPA | Static HTML | Hosted |
150
+ | **JS Payload** | **Tiny (< 15kb)** | Heavy | Minimal | Medium |
151
+ | **Search** | **Built-in (Offline)** | Algolia (Ext) | Built-in | Built-in |
152
+ | **Setup** | **~1 min** | ~15 mins | ~10 mins | Instant |
153
+ | **Cost** | **Free OSS** | Free OSS | Free OSS | Freemium |
154
+
155
+ ## Community & Support
156
+
157
+ - **Contributing**: We welcome PRs! See [CONTRIBUTING.md](.github/CONTRIBUTING.md).
158
+ - **Support**: If you find `docmd` useful, please consider [sponsoring the project](https://github.com/sponsors/mgks) or giving it a star ⭐.
159
+
160
+ ## License
161
+
162
+ Distributed under the MIT License. See `LICENSE` for more information.
163
+
164
+ > **{ github.com/mgks }**
165
+ >
166
+ > ![Website Badge](https://img.shields.io/badge/Visit-mgks.dev-blue?style=flat&link=https%3A%2F%2Fmgks.dev) ![Sponsor Badge](https://img.shields.io/badge/%20%20Become%20a%20Sponsor%20%20-red?style=flat&logo=github&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fmgks)
package/bin/docmd.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const { version } = require('../package.json');
5
+ const { initProject } = require('../src/commands/init');
6
+ const { buildSite } = require('../src/commands/build');
7
+ const { startDevServer } = require('../src/commands/dev');
8
+ const { buildLive } = require('../src/commands/live');
9
+ const { printBanner } = require('../src/utils/logger');
10
+
11
+ program
12
+ .name('docmd')
13
+ .description('The minimalist, zero-config documentation generator')
14
+ .version(version);
15
+
16
+ program
17
+ .command('init')
18
+ .action(() => {
19
+ printBanner();
20
+ initProject();
21
+ });
22
+
23
+ program
24
+ .command('build')
25
+ .option('-c, --config <path>', 'Path to config', 'docmd.config.js')
26
+ .option('--offline', 'Optimize for file:// viewing')
27
+ .action((opts) => {
28
+ buildSite(opts.config, { isDev: false, offline: opts.offline });
29
+ });
30
+
31
+ program
32
+ .command('dev')
33
+ .option('-c, --config <path>', 'Path to config', 'docmd.config.js')
34
+ .option('-p, --port <number>', 'Port to run server')
35
+ .action((opts) => {
36
+ printBanner();
37
+ startDevServer(opts.config, opts);
38
+ });
39
+
40
+ program
41
+ .command('live')
42
+ .action(async () => {
43
+ try {
44
+ await buildLive();
45
+ // ... (Add the spawn 'serve' logic here from the backup if desired)
46
+ } catch (e) {
47
+ console.error(e);
48
+ process.exit(1);
49
+ }
50
+ });
51
+
52
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@docmd/core",
3
+ "version": "0.4.0",
4
+ "description": "The minimalist, zero-config documentation generator.",
5
+ "bin": {
6
+ "docmd": "./bin/docmd.js"
7
+ },
8
+ "dependencies": {
9
+ "chalk": "^4.1.2",
10
+ "chokidar": "^3.5.3",
11
+ "commander": "^11.0.0",
12
+ "ws": "^8.13.0",
13
+ "@docmd/themes": "0.4.0",
14
+ "@docmd/ui": "0.4.0",
15
+ "@docmd/parser": "0.4.0",
16
+ "@docmd/live": "0.4.0",
17
+ "@docmd/plugin-seo": "0.4.0",
18
+ "@docmd/plugin-sitemap": "0.4.0",
19
+ "@docmd/plugin-search": "0.4.0",
20
+ "@docmd/plugin-analytics": "0.4.0"
21
+ },
22
+ "license": "MIT"
23
+ }
@@ -0,0 +1,262 @@
1
+ const path = require('path');
2
+ const fs = require('../utils/fs-utils');
3
+ const { loadConfig } = require('../utils/config-loader');
4
+ const { loadPlugins } = require('../utils/plugin-loader');
5
+ const parser = require('@docmd/parser');
6
+ const ui = require('@docmd/ui');
7
+ const themes = require('@docmd/themes');
8
+ const { findPageNeighbors } = require('@docmd/parser/src/utils/navigation-helper');
9
+
10
+ async function findMarkdownFilesRecursive(dir) {
11
+ let files = [];
12
+ if (!await fs.exists(dir)) return [];
13
+ const items = await fs.readdir(dir, { withFileTypes: true });
14
+ for (const item of items) {
15
+ const fullPath = path.join(dir, item.name);
16
+ if (item.isDirectory()) {
17
+ files = files.concat(await findMarkdownFilesRecursive(fullPath));
18
+ } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
19
+ files.push(fullPath);
20
+ }
21
+ }
22
+ return files;
23
+ }
24
+
25
+ function generateTag(pathOrUrl, type, attributes = {}) {
26
+ const attrs = Object.entries(attributes).map(([k,v]) => v === true ? k : `${k}="${v}"`).join(' ');
27
+ if (type === 'css') return `<link rel="stylesheet" href="${pathOrUrl}" ${attrs}>`;
28
+ if (type === 'js') return `<script src="${pathOrUrl}" ${attrs}></script>`;
29
+ return '';
30
+ }
31
+
32
+ async function buildSite(configPath, options = { isDev: false, offline: false }) {
33
+ const CWD = process.cwd();
34
+ const config = await loadConfig(configPath);
35
+ const hooks = loadPlugins(config);
36
+ const buildHash = Date.now().toString(36);
37
+
38
+ const srcDir = path.resolve(CWD, config.srcDir);
39
+ const outputDir = path.resolve(CWD, config.outputDir);
40
+
41
+ if (!await fs.exists(srcDir)) throw new Error(`Source directory not found: ${srcDir}`);
42
+ await fs.ensureDir(outputDir);
43
+
44
+ // --- 1. ASSET COPYING (Simplified) ---
45
+
46
+ // A. Copy ALL UI Assets (Deep copy)
47
+ // This handles css, js, images, AND favicon.ico in root of assets
48
+ const uiAssets = ui.getAssetsDir();
49
+ if (await fs.exists(uiAssets)) {
50
+ await fs.copy(uiAssets, path.join(outputDir, 'assets'));
51
+ }
52
+
53
+ // B. Copy Themes
54
+ const themesDir = themes.getThemesDir();
55
+ if (await fs.exists(themesDir)) {
56
+ await fs.copy(themesDir, path.join(outputDir, 'assets/css'));
57
+ }
58
+
59
+ // C. Copy User Assets (Override)
60
+ const userAssets = path.resolve(CWD, 'assets');
61
+ if (await fs.exists(userAssets)) await fs.copy(userAssets, path.join(outputDir, 'assets'));
62
+
63
+ // --- 2. GENERATE TAGS ---
64
+ const assetTags = { head: [], body: [] };
65
+
66
+ if (config.theme && config.theme.name && config.theme.name !== 'default') {
67
+ const themeFileName = `docmd-theme-${config.theme.name}.css`;
68
+ if (await fs.exists(path.join(themes.getThemesDir(), themeFileName))) {
69
+ assetTags.head.push(rel => generateTag(`${rel}assets/css/${themeFileName}?v=${buildHash}`, 'css'));
70
+ } else {
71
+ if (!options.isDev) console.warn(`⚠️ Theme not found: ${themeFileName}`);
72
+ }
73
+ }
74
+
75
+ // Core JS (Keep this, or move to EJS if you want full control, but keeping here is fine)
76
+ assetTags.body.push(rel => generateTag(`${rel}assets/js/docmd-main.js?v=${buildHash}`, 'js'));
77
+
78
+ // Lightbox
79
+ if(await fs.exists(path.join(uiAssets, 'js/docmd-image-lightbox.js'))) {
80
+ assetTags.body.push(rel => generateTag(`${rel}assets/js/docmd-image-lightbox.js?v=${buildHash}`, 'js'));
81
+ }
82
+
83
+ // Plugin Assets
84
+ if (hooks.assets) {
85
+ for (const getAssetsFn of hooks.assets) {
86
+ const assets = getAssetsFn();
87
+ if (Array.isArray(assets)) {
88
+ for (const asset of assets) {
89
+ let tagGen;
90
+ if (asset.src && asset.dest) {
91
+ const destPath = path.join(outputDir, asset.dest);
92
+ if (await fs.exists(asset.src)) {
93
+ await fs.ensureDir(path.dirname(destPath));
94
+ await fs.copy(asset.src, destPath);
95
+ }
96
+ tagGen = (rel) => generateTag(`${rel}${asset.dest}?v=${buildHash}`, asset.type, asset.attributes);
97
+ } else if (asset.url) {
98
+ tagGen = () => generateTag(asset.url, asset.type, asset.attributes);
99
+ }
100
+ if (tagGen) assetTags[asset.location === 'head' ? 'head' : 'body'].push(tagGen);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ // --- 3. PROCESSING ---
107
+ const mdProcessor = parser.createMarkdownProcessor(config, (md) => hooks.markdownSetup.forEach(hook => hook(md)));
108
+ const themeInitPath = path.join(ui.getTemplatesDir(), 'partials', 'theme-init.js');
109
+ let themeInitScript = '';
110
+ if (await fs.exists(themeInitPath)) themeInitScript = `<script>${await fs.readFile(themeInitPath, 'utf8')}</script>`;
111
+ let footerHtml = config.footer ? mdProcessor.renderInline(config.footer) : '';
112
+
113
+ const mdFiles = await findMarkdownFilesRecursive(srcDir);
114
+ const pages = [];
115
+
116
+ for (const filePath of mdFiles) {
117
+ const rawContent = await fs.readFile(filePath, 'utf8');
118
+ const processed = parser.processContent(rawContent, mdProcessor, config);
119
+ if (!processed) continue;
120
+
121
+ const relativePath = path.relative(srcDir, filePath);
122
+ const isIndex = path.basename(relativePath).startsWith('index.');
123
+ const htmlOutputPath = isIndex ? path.join(path.dirname(relativePath), 'index.html') : relativePath.replace(/\.md$/, '/index.html');
124
+ pages.push({ ...processed, sourcePath: filePath, outputPath: htmlOutputPath });
125
+ }
126
+
127
+ // --- 4. RENDER LOOP ---
128
+ for (const page of pages) {
129
+ // 1. Determine Output Location
130
+ const finalPath = path.join(outputDir, page.outputPath);
131
+
132
+ // 2. Calculate Relative Path to Root (CRITICAL FIX)
133
+ // "content/nested/index.html" -> dir "content/nested" -> relative "../.."
134
+ const fileDir = path.dirname(page.outputPath);
135
+ let relativePathToRoot = path.relative(fileDir, '.');
136
+
137
+ if (relativePathToRoot === '') relativePathToRoot = './';
138
+ else relativePathToRoot += '/';
139
+ relativePathToRoot = relativePathToRoot.replace(/\\/g, '/'); // Windows fix
140
+
141
+ // 3. Normalize Nav Path for Matching
142
+ // We use the "clean" URL path for checking active state
143
+ let navPath = '/' + page.outputPath.replace(/\\/g, '/').replace(/\/index\.html$/, '').replace(/^index\.html$/, '');
144
+ if(navPath === '/.') navPath = '/';
145
+
146
+ // 4. Navigation & Neighbors
147
+ const { prevPage, nextPage } = findPageNeighbors(config.navigation, navPath);
148
+
149
+ // Fix Neighbor Links (Prepend relative root)
150
+ if (prevPage && !prevPage.path.startsWith('http')) {
151
+ let p = prevPage.path.replace(/^\//, ''); // Strip leading slash
152
+ if(options.offline && !p.endsWith('.html')) p = p.replace(/\/$/, '') + '/index.html';
153
+ prevPage.url = relativePathToRoot + p;
154
+ }
155
+ if (nextPage && !nextPage.path.startsWith('http')) {
156
+ let p = nextPage.path.replace(/^\//, '');
157
+ if(options.offline && !p.endsWith('.html')) p = p.replace(/\/$/, '') + '/index.html';
158
+ nextPage.url = relativePathToRoot + p;
159
+ }
160
+
161
+ // 5. Asset Injection (Head/Body)
162
+ const assetHeadHtml = assetTags.head.map(gen => gen(relativePathToRoot)).join('\n');
163
+ const assetBodyHtml = assetTags.body.map(gen => gen(relativePathToRoot)).join('\n');
164
+
165
+ const pageContext = { frontmatter: page.frontmatter, outputPath: page.outputPath };
166
+
167
+ const fullHeadHtml = [
168
+ hooks.injectHead.map(fn => fn(config, pageContext, relativePathToRoot)).join('\n'),
169
+ assetHeadHtml
170
+ ].join('\n');
171
+
172
+ const fullBodyHtml = [
173
+ assetBodyHtml,
174
+ hooks.injectBody.map(fn => fn(config, pageContext)).join('\n')
175
+ ].join('\n');
176
+
177
+ // 6. Helpers
178
+ let faviconLinkHtml = '';
179
+ if (config.favicon) {
180
+ const cleanFavicon = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
181
+ const finalFavicon = `${relativePathToRoot}${cleanFavicon}?v=${buildHash}`;
182
+ faviconLinkHtml = `<link rel="icon" href="${finalFavicon}" type="image/x-icon" sizes="any">\n<link rel="shortcut icon" href="${finalFavicon}" type="image/x-icon">`;
183
+ }
184
+
185
+ const isActivePage = page.htmlContent && page.htmlContent.trim().length > 0;
186
+
187
+ let editUrl = null, editLinkText = 'Edit this page';
188
+ if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) {
189
+ const cleanBase = config.editLink.baseUrl.replace(/\/$/, '');
190
+ const cleanPath = page.outputPath.replace(/\/index\.html$/, '.md');
191
+ editUrl = `${cleanBase}/${cleanPath}`;
192
+ if (page.outputPath.endsWith('index.html') && page.outputPath !== 'index.html') editUrl = editUrl.replace('.md', '/index.md');
193
+ if (page.outputPath === 'index.html') editUrl = `${cleanBase}/index.md`;
194
+ editLinkText = config.editLink.text || editLinkText;
195
+ }
196
+
197
+ // 7. Render
198
+ const templateName = page.frontmatter.noStyle ? 'no-style' : 'layout';
199
+ const templatePath = ui.getTemplatePath(templateName);
200
+ const templateString = await fs.readFile(templatePath, 'utf8');
201
+ const navTemplateString = await fs.readFile(ui.getTemplatePath('navigation'), 'utf8');
202
+
203
+ const navigationHtml = parser.renderTemplate(navTemplateString, {
204
+ config,
205
+ navItems: config.navigation,
206
+ currentPagePath: navPath,
207
+ relativePathToRoot,
208
+ isOfflineMode: options.offline
209
+ }, { filename: ui.getTemplatePath('navigation') });
210
+
211
+ const fullHtml = parser.renderTemplate(templateString, {
212
+ content: page.htmlContent,
213
+ frontmatter: page.frontmatter,
214
+ headings: page.headings,
215
+ config,
216
+ buildHash,
217
+ siteTitle: config.siteTitle,
218
+ pageTitle: page.frontmatter.title,
219
+ description: page.frontmatter.description || '',
220
+ defaultMode: config.theme?.defaultMode || 'light',
221
+ relativePathToRoot,
222
+ isOfflineMode: options.offline,
223
+ navigationHtml,
224
+ prevPage,
225
+ nextPage,
226
+ logo: config.logo,
227
+ theme: config.theme,
228
+ sidebarConfig: config.sidebar || {},
229
+ footer: config.footer,
230
+ sponsor: config.sponsor,
231
+ customCssFiles: config.theme.customCss || [],
232
+ customJsFiles: config.customJs || [],
233
+
234
+ pluginHeadScriptsHtml: fullHeadHtml,
235
+ pluginBodyScriptsHtml: fullBodyHtml,
236
+
237
+ faviconLinkHtml,
238
+ themeInitScript,
239
+ footerHtml,
240
+ isActivePage,
241
+ editUrl,
242
+ editLinkText,
243
+ themeCssLinkHtml: '',
244
+ metaTagsHtml: '',
245
+ pluginStylesHtml: ''
246
+ }, { filename: templatePath });
247
+
248
+ // 8. Write File
249
+ await fs.ensureDir(path.dirname(finalPath));
250
+ await fs.writeFile(finalPath, fullHtml);
251
+ }
252
+
253
+ await Promise.all(hooks.onPostBuild.map(fn => fn({
254
+ config,
255
+ pages,
256
+ outputDir,
257
+ log: (msg) => !options.isDev && console.log(msg)
258
+ })));
259
+ if (!options.isDev) console.log(`✅ Build complete. Generated ${pages.length} pages.`);
260
+ }
261
+
262
+ module.exports = { buildSite };