@pruddiman/mdmirror 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/README.md +1 -0
- package/dist/build/builder.d.ts +41 -0
- package/dist/build/builder.js +108 -0
- package/dist/build/builder.js.map +1 -0
- package/dist/cli/build.d.ts +14 -0
- package/dist/cli/build.js +116 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +104 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/serve.d.ts +15 -0
- package/dist/cli/serve.js +233 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/core/discovery.d.ts +12 -0
- package/dist/core/discovery.js +91 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/navigation.d.ts +12 -0
- package/dist/core/navigation.js +155 -0
- package/dist/core/navigation.js.map +1 -0
- package/dist/core/slug.d.ts +47 -0
- package/dist/core/slug.js +132 -0
- package/dist/core/slug.js.map +1 -0
- package/dist/core/title.d.ts +22 -0
- package/dist/core/title.js +43 -0
- package/dist/core/title.js.map +1 -0
- package/dist/render/highlight.d.ts +23 -0
- package/dist/render/highlight.js +36 -0
- package/dist/render/highlight.js.map +1 -0
- package/dist/render/mermaid.d.ts +13 -0
- package/dist/render/mermaid.js +66 -0
- package/dist/render/mermaid.js.map +1 -0
- package/dist/render/pipeline.d.ts +32 -0
- package/dist/render/pipeline.js +141 -0
- package/dist/render/pipeline.js.map +1 -0
- package/dist/search/index.d.ts +19 -0
- package/dist/search/index.js +43 -0
- package/dist/search/index.js.map +1 -0
- package/dist/server/reload.d.ts +23 -0
- package/dist/server/reload.js +80 -0
- package/dist/server/reload.js.map +1 -0
- package/dist/server/server.d.ts +22 -0
- package/dist/server/server.js +137 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/watcher.d.ts +24 -0
- package/dist/server/watcher.js +62 -0
- package/dist/server/watcher.js.map +1 -0
- package/dist/theme/index.d.ts +8 -0
- package/dist/theme/index.js +19 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/layout.d.ts +37 -0
- package/dist/theme/layout.js +141 -0
- package/dist/theme/layout.js.map +1 -0
- package/dist/theme/scripts.d.ts +29 -0
- package/dist/theme/scripts.js +159 -0
- package/dist/theme/scripts.js.map +1 -0
- package/dist/theme/styles.css +462 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# mdmirror
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static site builder.
|
|
3
|
+
* Writes all rendered HTML pages and assets to an output directory.
|
|
4
|
+
*/
|
|
5
|
+
import type { Document, NavigationTree } from "../types.js";
|
|
6
|
+
export interface BuildOptions {
|
|
7
|
+
/** All processed documents */
|
|
8
|
+
documents: Document[];
|
|
9
|
+
/** Built navigation tree */
|
|
10
|
+
navigationTree: NavigationTree;
|
|
11
|
+
/** CSS stylesheet content */
|
|
12
|
+
stylesheet: string;
|
|
13
|
+
/** Output directory path */
|
|
14
|
+
outputDir: string;
|
|
15
|
+
/** Navigation JS content */
|
|
16
|
+
navigationScript?: string;
|
|
17
|
+
/** URL path to the Mermaid UMD bundle */
|
|
18
|
+
mermaidBundleSrc?: string;
|
|
19
|
+
/** Inline script to initialize Mermaid after the bundle loads */
|
|
20
|
+
mermaidInitScript?: string;
|
|
21
|
+
/** Inline search script (injected per-page) */
|
|
22
|
+
searchScript?: string;
|
|
23
|
+
/** Base URL for canonical links and sitemap (e.g. "https://example.com") */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BuildResult {
|
|
27
|
+
/** Number of pages written */
|
|
28
|
+
pageCount: number;
|
|
29
|
+
/** Total output size in bytes */
|
|
30
|
+
totalSize: number;
|
|
31
|
+
/** Absolute path of the output directory */
|
|
32
|
+
outputPath: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the static site output.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildSite(options: BuildOptions): Promise<BuildResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Format bytes into a human-readable string.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatSize(bytes: number): string;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static site builder.
|
|
3
|
+
* Writes all rendered HTML pages and assets to an output directory.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, writeFile, copyFile, stat as fsStat } from "node:fs/promises";
|
|
6
|
+
import { resolve, dirname, join } from "node:path";
|
|
7
|
+
import { renderLayout } from "../theme/layout.js";
|
|
8
|
+
import { buildSearchIndex } from "../search/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Build the static site output.
|
|
11
|
+
*/
|
|
12
|
+
export async function buildSite(options) {
|
|
13
|
+
const { documents, navigationTree, stylesheet, outputDir, navigationScript, mermaidBundleSrc, mermaidInitScript, searchScript, baseUrl, } = options;
|
|
14
|
+
const outputPath = resolve(outputDir);
|
|
15
|
+
let totalSize = 0;
|
|
16
|
+
// Create output directory
|
|
17
|
+
try {
|
|
18
|
+
await mkdir(outputPath, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new Error(`Cannot create output directory: ${outputPath} (${error instanceof Error ? error.message : "permission denied"})`, { cause: error });
|
|
22
|
+
}
|
|
23
|
+
// Create assets directory
|
|
24
|
+
const assetsDir = join(outputPath, "assets");
|
|
25
|
+
await mkdir(assetsDir, { recursive: true });
|
|
26
|
+
const cssContent = stylesheet;
|
|
27
|
+
// Write search index
|
|
28
|
+
const searchIndex = buildSearchIndex(documents);
|
|
29
|
+
const searchJson = JSON.stringify(searchIndex);
|
|
30
|
+
await writeFile(join(assetsDir, "search.json"), searchJson, "utf-8");
|
|
31
|
+
totalSize += Buffer.byteLength(searchJson, "utf-8");
|
|
32
|
+
// Write navigation script if provided
|
|
33
|
+
if (navigationScript) {
|
|
34
|
+
await writeFile(join(assetsDir, "main.js"), navigationScript, "utf-8");
|
|
35
|
+
totalSize += Buffer.byteLength(navigationScript, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
// Copy Mermaid JS bundle if available
|
|
38
|
+
try {
|
|
39
|
+
const mermaidSrc = resolve("node_modules/mermaid/dist/mermaid.min.js");
|
|
40
|
+
await fsStat(mermaidSrc);
|
|
41
|
+
await copyFile(mermaidSrc, join(assetsDir, "mermaid.min.js"));
|
|
42
|
+
const mermaidSize = (await fsStat(join(assetsDir, "mermaid.min.js"))).size;
|
|
43
|
+
totalSize += mermaidSize;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.warn("Warning: Mermaid bundle not found — diagram pages may not render correctly");
|
|
47
|
+
}
|
|
48
|
+
// Generate sitemap.xml when a base URL is provided
|
|
49
|
+
if (baseUrl) {
|
|
50
|
+
const sitemapBase = baseUrl.replace(/\/$/, "");
|
|
51
|
+
const urls = documents
|
|
52
|
+
.map((doc) => {
|
|
53
|
+
const path = doc.slug === "" ? "/" : `/${doc.slug}/`;
|
|
54
|
+
return ` <url>\n <loc>${sitemapBase}${path}</loc>\n </url>`;
|
|
55
|
+
})
|
|
56
|
+
.join("\n");
|
|
57
|
+
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
58
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n` +
|
|
59
|
+
`${urls}\n` +
|
|
60
|
+
`</urlset>\n`;
|
|
61
|
+
await writeFile(join(outputPath, "sitemap.xml"), sitemapXml, "utf-8");
|
|
62
|
+
totalSize += Buffer.byteLength(sitemapXml, "utf-8");
|
|
63
|
+
}
|
|
64
|
+
// Write HTML pages
|
|
65
|
+
for (const doc of documents) {
|
|
66
|
+
const pagePath = doc.slug === "" ? "/" : `/${doc.slug}/`;
|
|
67
|
+
const canonicalUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${pagePath}` : undefined;
|
|
68
|
+
const html = renderLayout({
|
|
69
|
+
title: doc.title,
|
|
70
|
+
description: doc.description,
|
|
71
|
+
canonicalUrl,
|
|
72
|
+
content: doc.renderedHtml,
|
|
73
|
+
navigation: navigationTree,
|
|
74
|
+
currentSlug: doc.slug,
|
|
75
|
+
stylesheet: cssContent,
|
|
76
|
+
mode: "build",
|
|
77
|
+
mermaidBundleSrc,
|
|
78
|
+
mermaidInitScript,
|
|
79
|
+
navigationScript,
|
|
80
|
+
searchScript,
|
|
81
|
+
});
|
|
82
|
+
// Determine output file path
|
|
83
|
+
const htmlPath = doc.slug === ""
|
|
84
|
+
? join(outputPath, "index.html")
|
|
85
|
+
: join(outputPath, doc.slug, "index.html");
|
|
86
|
+
// Create parent directories
|
|
87
|
+
await mkdir(dirname(htmlPath), { recursive: true });
|
|
88
|
+
// Write the HTML file
|
|
89
|
+
await writeFile(htmlPath, html, "utf-8");
|
|
90
|
+
totalSize += Buffer.byteLength(html, "utf-8");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
pageCount: documents.length,
|
|
94
|
+
totalSize,
|
|
95
|
+
outputPath,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Format bytes into a human-readable string.
|
|
100
|
+
*/
|
|
101
|
+
export function formatSize(bytes) {
|
|
102
|
+
if (bytes < 1024)
|
|
103
|
+
return `${bytes} B`;
|
|
104
|
+
if (bytes < 1024 * 1024)
|
|
105
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
106
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.js","sourceRoot":"","sources":["../../src/build/builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build command handler.
|
|
3
|
+
* Orchestrates the full static build flow: validate, discover, render, write output.
|
|
4
|
+
*/
|
|
5
|
+
export interface BuildCommandOptions {
|
|
6
|
+
path: string;
|
|
7
|
+
output: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Execute the build command.
|
|
12
|
+
* Discovers files, renders them, builds navigation, writes static output.
|
|
13
|
+
*/
|
|
14
|
+
export declare function build(options: BuildCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build command handler.
|
|
3
|
+
* Orchestrates the full static build flow: validate, discover, render, write output.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { discoverFiles } from "../core/discovery.js";
|
|
8
|
+
import { generateSlug, generateSortKey, detectSlugCollisions, disambiguateSlugs, } from "../core/slug.js";
|
|
9
|
+
import { getTitle } from "../core/title.js";
|
|
10
|
+
import { extractFrontMatter } from "../render/pipeline.js";
|
|
11
|
+
import { buildNavigationTree } from "../core/navigation.js";
|
|
12
|
+
import { renderContent } from "../render/pipeline.js";
|
|
13
|
+
import { buildSite, formatSize } from "../build/builder.js";
|
|
14
|
+
import { getStylesheet } from "../theme/index.js";
|
|
15
|
+
import { getMermaidBundleSrc, MERMAID_INIT_SCRIPT, NAVIGATION_SCRIPT, buildSearchScript, } from "../theme/scripts.js";
|
|
16
|
+
/**
|
|
17
|
+
* Execute the build command.
|
|
18
|
+
* Discovers files, renders them, builds navigation, writes static output.
|
|
19
|
+
*/
|
|
20
|
+
export async function build(options) {
|
|
21
|
+
const sourcePath = resolve(options.path);
|
|
22
|
+
// Step 1: Discover files
|
|
23
|
+
const documents = await discoverFiles(sourcePath);
|
|
24
|
+
// Step 2: Read content, generate slugs, extract titles
|
|
25
|
+
await processDocuments(documents);
|
|
26
|
+
// Step 3: Check for slug collisions
|
|
27
|
+
checkSlugCollisions(documents);
|
|
28
|
+
// Step 4: Build navigation tree
|
|
29
|
+
const navigationTree = buildNavigationTree(documents);
|
|
30
|
+
// Step 5: Render all pages to HTML
|
|
31
|
+
await renderDocuments(documents);
|
|
32
|
+
// Step 6: Get theme assets
|
|
33
|
+
const stylesheet = getStylesheet();
|
|
34
|
+
// Step 7: Build static output
|
|
35
|
+
const result = await buildSite({
|
|
36
|
+
documents,
|
|
37
|
+
navigationTree,
|
|
38
|
+
stylesheet,
|
|
39
|
+
outputDir: options.output,
|
|
40
|
+
mermaidBundleSrc: getMermaidBundleSrc("build"),
|
|
41
|
+
mermaidInitScript: MERMAID_INIT_SCRIPT,
|
|
42
|
+
navigationScript: NAVIGATION_SCRIPT,
|
|
43
|
+
searchScript: buildSearchScript("assets/search.json"),
|
|
44
|
+
baseUrl: options.baseUrl,
|
|
45
|
+
});
|
|
46
|
+
// Print summary
|
|
47
|
+
console.log(`\nmdmirror build complete`);
|
|
48
|
+
console.log(` Source: ${sourcePath}`);
|
|
49
|
+
console.log(` Output: ${result.outputPath}`);
|
|
50
|
+
console.log(` Pages: ${result.pageCount}`);
|
|
51
|
+
console.log(` Size: ${formatSize(result.totalSize)}\n`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read file content, generate slugs, extract titles, and set sort keys.
|
|
55
|
+
* Skips files that can't be read (e.g. non-UTF-8 encoding) with a warning.
|
|
56
|
+
*/
|
|
57
|
+
async function processDocuments(documents) {
|
|
58
|
+
const failed = [];
|
|
59
|
+
await Promise.all(documents.map(async (doc) => {
|
|
60
|
+
try {
|
|
61
|
+
doc.content = await readFile(doc.absolutePath, "utf-8");
|
|
62
|
+
// Detect non-UTF-8 content (replacement character indicates encoding issues)
|
|
63
|
+
if (doc.content.includes("\uFFFD")) {
|
|
64
|
+
console.error(`Warning: Skipping ${doc.sourcePath} (appears to be non-UTF-8 encoded)`);
|
|
65
|
+
failed.push(doc);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const { frontMatter } = extractFrontMatter(doc.content);
|
|
69
|
+
doc.slug = generateSlug(doc.sourcePath);
|
|
70
|
+
doc.title =
|
|
71
|
+
frontMatter.title ||
|
|
72
|
+
getTitle(doc.content, doc.sourcePath);
|
|
73
|
+
doc.description = frontMatter.description || "";
|
|
74
|
+
const filename = doc.sourcePath.split(/[/\\]/).pop() || doc.sourcePath;
|
|
75
|
+
doc.sortKey = generateSortKey(filename.replace(/\.[^.]+$/, ""));
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error(`Warning: Skipping ${doc.sourcePath} (${error instanceof Error ? error.message : "read error"})`);
|
|
79
|
+
failed.push(doc);
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
// Remove failed documents from the array
|
|
83
|
+
for (const doc of failed) {
|
|
84
|
+
const index = documents.indexOf(doc);
|
|
85
|
+
if (index !== -1)
|
|
86
|
+
documents.splice(index, 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check for slug collisions, warn on stderr, and disambiguate with suffixes.
|
|
91
|
+
*/
|
|
92
|
+
function checkSlugCollisions(documents) {
|
|
93
|
+
const slugMap = new Map();
|
|
94
|
+
for (const doc of documents) {
|
|
95
|
+
const existing = slugMap.get(doc.slug) || [];
|
|
96
|
+
existing.push(doc.sourcePath);
|
|
97
|
+
slugMap.set(doc.slug, existing);
|
|
98
|
+
}
|
|
99
|
+
const collisions = detectSlugCollisions(slugMap);
|
|
100
|
+
for (const [slug, paths] of collisions) {
|
|
101
|
+
console.error(`Warning: Slug collision detected: "${slug}" maps to ${paths.join(" and ")} — disambiguating with suffixes`);
|
|
102
|
+
}
|
|
103
|
+
// Apply disambiguation suffixes to colliding slugs
|
|
104
|
+
if (collisions.size > 0) {
|
|
105
|
+
disambiguateSlugs(documents);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Render all document content to HTML.
|
|
110
|
+
*/
|
|
111
|
+
async function renderDocuments(documents) {
|
|
112
|
+
await Promise.all(documents.map(async (doc) => {
|
|
113
|
+
doc.renderedHtml = await renderContent(doc.content, doc.fileType);
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for mdmirror.
|
|
4
|
+
* Handles manual subcommand routing since citty runs parent + subcommand.
|
|
5
|
+
*/
|
|
6
|
+
import { defineCommand, runMain } from "citty";
|
|
7
|
+
import { serve } from "./serve.js";
|
|
8
|
+
import { build } from "./build.js";
|
|
9
|
+
// Detect if "build" subcommand is being invoked
|
|
10
|
+
const isBuildCommand = process.argv[2] === "build";
|
|
11
|
+
if (isBuildCommand) {
|
|
12
|
+
// Remove "build" from argv so citty parses the remaining args correctly
|
|
13
|
+
process.argv.splice(2, 1);
|
|
14
|
+
const buildCommand = defineCommand({
|
|
15
|
+
meta: {
|
|
16
|
+
name: "mdmirror build",
|
|
17
|
+
description: "Generate a self-contained static site for deployment",
|
|
18
|
+
},
|
|
19
|
+
args: {
|
|
20
|
+
path: {
|
|
21
|
+
type: "positional",
|
|
22
|
+
description: "Path to the source folder containing documentation",
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
output: {
|
|
26
|
+
type: "string",
|
|
27
|
+
alias: "o",
|
|
28
|
+
description: "Output directory for the generated site",
|
|
29
|
+
default: "./dist",
|
|
30
|
+
},
|
|
31
|
+
"base-url": {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Base URL for canonical links and sitemap (e.g. https://example.com)",
|
|
34
|
+
default: "",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
async run({ args }) {
|
|
38
|
+
try {
|
|
39
|
+
await build({
|
|
40
|
+
path: args.path,
|
|
41
|
+
output: args.output,
|
|
42
|
+
baseUrl: args["base-url"] || undefined,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
console.error(`Error: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.error("An unexpected error occurred");
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
runMain(buildCommand);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const main = defineCommand({
|
|
60
|
+
meta: {
|
|
61
|
+
name: "mdmirror",
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
description: "Zero-config CLI tool that turns any folder of markdown files into a browsable documentation site",
|
|
64
|
+
},
|
|
65
|
+
args: {
|
|
66
|
+
path: {
|
|
67
|
+
type: "positional",
|
|
68
|
+
description: "Path to the source folder containing documentation",
|
|
69
|
+
required: true,
|
|
70
|
+
},
|
|
71
|
+
port: {
|
|
72
|
+
type: "string",
|
|
73
|
+
alias: "p",
|
|
74
|
+
description: "Port to serve on",
|
|
75
|
+
default: process.env["PORT"] || "3000",
|
|
76
|
+
},
|
|
77
|
+
host: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Bind address for the server",
|
|
80
|
+
default: "localhost",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
async run({ args }) {
|
|
84
|
+
try {
|
|
85
|
+
await serve({
|
|
86
|
+
path: args.path,
|
|
87
|
+
port: parseInt(args.port, 10),
|
|
88
|
+
host: args.host,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
console.error(`Error: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error("An unexpected error occurred");
|
|
97
|
+
}
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
runMain(main);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve command handler.
|
|
3
|
+
* Orchestrates the full serve flow: validate, discover, render, serve, watch, live reload.
|
|
4
|
+
*/
|
|
5
|
+
export interface ServeOptions {
|
|
6
|
+
path: string;
|
|
7
|
+
port: number;
|
|
8
|
+
host: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Execute the serve command.
|
|
12
|
+
* Discovers files, renders them, builds navigation, starts the server,
|
|
13
|
+
* then watches for changes and triggers live reload.
|
|
14
|
+
*/
|
|
15
|
+
export declare function serve(options: ServeOptions): Promise<void>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve command handler.
|
|
3
|
+
* Orchestrates the full serve flow: validate, discover, render, serve, watch, live reload.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { resolve, relative } from "node:path";
|
|
7
|
+
import { discoverFiles } from "../core/discovery.js";
|
|
8
|
+
import { generateSlug, generateSortKey, detectSlugCollisions, disambiguateSlugs, } from "../core/slug.js";
|
|
9
|
+
import { getTitle } from "../core/title.js";
|
|
10
|
+
import { extractFrontMatter } from "../render/pipeline.js";
|
|
11
|
+
import { buildNavigationTree } from "../core/navigation.js";
|
|
12
|
+
import { renderContent } from "../render/pipeline.js";
|
|
13
|
+
import { renderLayout } from "../theme/layout.js";
|
|
14
|
+
import { startServer } from "../server/server.js";
|
|
15
|
+
import { createReloadServer, LIVE_RELOAD_CLIENT_SCRIPT } from "../server/reload.js";
|
|
16
|
+
import { startWatcher } from "../server/watcher.js";
|
|
17
|
+
import { getStylesheet } from "../theme/index.js";
|
|
18
|
+
import { getMermaidBundleSrc, MERMAID_INIT_SCRIPT, NAVIGATION_SCRIPT, buildSearchScript, } from "../theme/scripts.js";
|
|
19
|
+
import { buildSearchIndex } from "../search/index.js";
|
|
20
|
+
/**
|
|
21
|
+
* Execute the serve command.
|
|
22
|
+
* Discovers files, renders them, builds navigation, starts the server,
|
|
23
|
+
* then watches for changes and triggers live reload.
|
|
24
|
+
*/
|
|
25
|
+
export async function serve(options) {
|
|
26
|
+
const sourcePath = resolve(options.path);
|
|
27
|
+
// Step 1: Discover files
|
|
28
|
+
let documents = await discoverFiles(sourcePath);
|
|
29
|
+
// Step 2: Read content, generate slugs, extract titles
|
|
30
|
+
await processDocuments(documents);
|
|
31
|
+
// Step 3: Check for slug collisions
|
|
32
|
+
checkSlugCollisions(documents);
|
|
33
|
+
// Step 4: Build navigation tree
|
|
34
|
+
let navigationTree = buildNavigationTree(documents);
|
|
35
|
+
// Step 5: Render all pages to HTML
|
|
36
|
+
await renderDocuments(documents);
|
|
37
|
+
// Step 6: Generate full HTML pages
|
|
38
|
+
const stylesheet = getStylesheet();
|
|
39
|
+
let pages = generatePages(documents, navigationTree, stylesheet, "serve");
|
|
40
|
+
// Step 7: Start the server
|
|
41
|
+
const config = {
|
|
42
|
+
port: options.port,
|
|
43
|
+
host: options.host,
|
|
44
|
+
sourcePath,
|
|
45
|
+
liveReload: true,
|
|
46
|
+
};
|
|
47
|
+
const site = {
|
|
48
|
+
pages,
|
|
49
|
+
sourcePath,
|
|
50
|
+
};
|
|
51
|
+
const { server, updatePages } = await startServer(config, site);
|
|
52
|
+
// Step 8: Set up live reload WebSocket
|
|
53
|
+
const reloadServer = createReloadServer(server);
|
|
54
|
+
// Step 9: Start file watcher
|
|
55
|
+
let rebuilding = false;
|
|
56
|
+
const watcher = startWatcher({
|
|
57
|
+
sourcePath,
|
|
58
|
+
debounceMs: 300,
|
|
59
|
+
onChange: async (events) => {
|
|
60
|
+
if (rebuilding)
|
|
61
|
+
return;
|
|
62
|
+
rebuilding = true;
|
|
63
|
+
try {
|
|
64
|
+
const hasStructuralChange = events.some((e) => e.type === "add" || e.type === "unlink");
|
|
65
|
+
if (hasStructuralChange) {
|
|
66
|
+
// Full rebuild: re-discover, re-process, re-render everything
|
|
67
|
+
documents = await discoverFiles(sourcePath);
|
|
68
|
+
await processDocuments(documents);
|
|
69
|
+
checkSlugCollisions(documents);
|
|
70
|
+
navigationTree = buildNavigationTree(documents);
|
|
71
|
+
await renderDocuments(documents);
|
|
72
|
+
pages = generatePages(documents, navigationTree, stylesheet, "serve");
|
|
73
|
+
updatePages(pages);
|
|
74
|
+
const addedFiles = events.filter((e) => e.type === "add").length;
|
|
75
|
+
const removedFiles = events.filter((e) => e.type === "unlink").length;
|
|
76
|
+
if (addedFiles > 0)
|
|
77
|
+
console.log(` + ${addedFiles} file(s) added`);
|
|
78
|
+
if (removedFiles > 0)
|
|
79
|
+
console.log(` - ${removedFiles} file(s) removed`);
|
|
80
|
+
console.log(` Pages: ${navigationTree.totalPages}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Incremental update: only re-render changed files
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
if (event.type === "change") {
|
|
86
|
+
const relativePath = relative(sourcePath, event.path);
|
|
87
|
+
const doc = documents.find((d) => d.sourcePath === relativePath);
|
|
88
|
+
if (doc) {
|
|
89
|
+
doc.content = await readFile(doc.absolutePath, "utf-8");
|
|
90
|
+
const { frontMatter: fm } = extractFrontMatter(doc.content);
|
|
91
|
+
doc.title =
|
|
92
|
+
fm.title ||
|
|
93
|
+
getTitle(doc.content, doc.sourcePath);
|
|
94
|
+
doc.description = fm.description || "";
|
|
95
|
+
doc.renderedHtml = await renderContent(doc.content, doc.fileType);
|
|
96
|
+
// Re-render the changed page (and update nav if title changed)
|
|
97
|
+
navigationTree = buildNavigationTree(documents);
|
|
98
|
+
pages = generatePages(documents, navigationTree, stylesheet, "serve");
|
|
99
|
+
updatePages(pages);
|
|
100
|
+
console.log(` ~ ${relativePath} updated`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Signal browsers to reload
|
|
106
|
+
reloadServer.reload();
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof Error) {
|
|
110
|
+
console.error(` Error during reload: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
rebuilding = false;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
// Print startup message
|
|
119
|
+
console.log(`\nmdmirror serving at http://${options.host}:${options.port}`);
|
|
120
|
+
console.log(` Source: ${sourcePath}`);
|
|
121
|
+
console.log(` Pages: ${navigationTree.totalPages}`);
|
|
122
|
+
console.log(` Live reload: enabled`);
|
|
123
|
+
console.log(`\n Press Ctrl+C to stop\n`);
|
|
124
|
+
// Handle graceful shutdown
|
|
125
|
+
const shutdown = () => {
|
|
126
|
+
console.log("\nShutting down...");
|
|
127
|
+
reloadServer.close();
|
|
128
|
+
watcher.close();
|
|
129
|
+
server.close(() => process.exit(0));
|
|
130
|
+
// Force exit if server doesn't close promptly
|
|
131
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
132
|
+
};
|
|
133
|
+
process.on("SIGINT", shutdown);
|
|
134
|
+
process.on("SIGTERM", shutdown);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Read file content, generate slugs, extract titles, and set sort keys.
|
|
138
|
+
* Skips files that can't be read (e.g. non-UTF-8 encoding) with a warning.
|
|
139
|
+
*/
|
|
140
|
+
async function processDocuments(documents) {
|
|
141
|
+
const failed = [];
|
|
142
|
+
await Promise.all(documents.map(async (doc) => {
|
|
143
|
+
try {
|
|
144
|
+
// Read content
|
|
145
|
+
doc.content = await readFile(doc.absolutePath, "utf-8");
|
|
146
|
+
// Detect non-UTF-8 content (replacement character indicates encoding issues)
|
|
147
|
+
if (doc.content.includes("\uFFFD")) {
|
|
148
|
+
console.error(`Warning: Skipping ${doc.sourcePath} (appears to be non-UTF-8 encoded)`);
|
|
149
|
+
failed.push(doc);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Extract front matter (title/description overrides)
|
|
153
|
+
const { frontMatter } = extractFrontMatter(doc.content);
|
|
154
|
+
// Generate slug
|
|
155
|
+
doc.slug = generateSlug(doc.sourcePath);
|
|
156
|
+
// Extract title (front matter wins, then heading, then humanized filename)
|
|
157
|
+
doc.title =
|
|
158
|
+
frontMatter.title ||
|
|
159
|
+
getTitle(doc.content, doc.sourcePath);
|
|
160
|
+
doc.description = frontMatter.description || "";
|
|
161
|
+
// Generate sort key from filename
|
|
162
|
+
const filename = doc.sourcePath.split(/[/\\]/).pop() || doc.sourcePath;
|
|
163
|
+
doc.sortKey = generateSortKey(filename.replace(/\.[^.]+$/, ""));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error(`Warning: Skipping ${doc.sourcePath} (${error instanceof Error ? error.message : "read error"})`);
|
|
167
|
+
failed.push(doc);
|
|
168
|
+
}
|
|
169
|
+
}));
|
|
170
|
+
// Remove failed documents from the array
|
|
171
|
+
for (const doc of failed) {
|
|
172
|
+
const index = documents.indexOf(doc);
|
|
173
|
+
if (index !== -1)
|
|
174
|
+
documents.splice(index, 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check for slug collisions, warn on stderr, and disambiguate with suffixes.
|
|
179
|
+
*/
|
|
180
|
+
function checkSlugCollisions(documents) {
|
|
181
|
+
const slugMap = new Map();
|
|
182
|
+
for (const doc of documents) {
|
|
183
|
+
const existing = slugMap.get(doc.slug) || [];
|
|
184
|
+
existing.push(doc.sourcePath);
|
|
185
|
+
slugMap.set(doc.slug, existing);
|
|
186
|
+
}
|
|
187
|
+
const collisions = detectSlugCollisions(slugMap);
|
|
188
|
+
for (const [slug, paths] of collisions) {
|
|
189
|
+
console.error(`Warning: Slug collision detected: "${slug}" maps to ${paths.join(" and ")} — disambiguating with suffixes`);
|
|
190
|
+
}
|
|
191
|
+
// Apply disambiguation suffixes to colliding slugs
|
|
192
|
+
if (collisions.size > 0) {
|
|
193
|
+
disambiguateSlugs(documents);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Render all document content to HTML.
|
|
198
|
+
*/
|
|
199
|
+
async function renderDocuments(documents) {
|
|
200
|
+
await Promise.all(documents.map(async (doc) => {
|
|
201
|
+
doc.renderedHtml = await renderContent(doc.content, doc.fileType);
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Generate full HTML pages for all documents.
|
|
206
|
+
* Returns a map of slug -> full HTML page.
|
|
207
|
+
*/
|
|
208
|
+
function generatePages(documents, navigationTree, stylesheet, mode) {
|
|
209
|
+
const pages = new Map();
|
|
210
|
+
const mermaidBundleSrc = getMermaidBundleSrc(mode);
|
|
211
|
+
for (const doc of documents) {
|
|
212
|
+
const html = renderLayout({
|
|
213
|
+
title: doc.title,
|
|
214
|
+
description: doc.description,
|
|
215
|
+
content: doc.renderedHtml,
|
|
216
|
+
navigation: navigationTree,
|
|
217
|
+
currentSlug: doc.slug,
|
|
218
|
+
stylesheet,
|
|
219
|
+
mode,
|
|
220
|
+
liveReloadScript: mode === "serve" ? LIVE_RELOAD_CLIENT_SCRIPT : undefined,
|
|
221
|
+
mermaidBundleSrc,
|
|
222
|
+
mermaidInitScript: MERMAID_INIT_SCRIPT,
|
|
223
|
+
navigationScript: NAVIGATION_SCRIPT,
|
|
224
|
+
searchScript: buildSearchScript("/assets/search.json"),
|
|
225
|
+
});
|
|
226
|
+
pages.set(doc.slug, html);
|
|
227
|
+
}
|
|
228
|
+
// Also expose the search index at /assets/search.json
|
|
229
|
+
const searchIndex = buildSearchIndex(documents);
|
|
230
|
+
pages.set("assets/search.json", JSON.stringify(searchIndex));
|
|
231
|
+
return pages;
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=serve.js.map
|