serve-my-md 1.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.
Files changed (49) hide show
  1. package/README.md +52 -0
  2. package/bin/index.js +487 -0
  3. package/index.html +70 -0
  4. package/package.json +111 -0
  5. package/shared/constants.json +3 -0
  6. package/shared/index.d.ts +34 -0
  7. package/web/.cta.json +12 -0
  8. package/web/.cursorrules +7 -0
  9. package/web/.prettierignore +3 -0
  10. package/web/.vscode/settings.json +11 -0
  11. package/web/README.md +489 -0
  12. package/web/components.json +21 -0
  13. package/web/eslint.config.js +5 -0
  14. package/web/index.html +66 -0
  15. package/web/prettier.config.js +10 -0
  16. package/web/public/og-image.png +0 -0
  17. package/web/src/.generated/output.json +1 -0
  18. package/web/src/.generated/paths.json +1 -0
  19. package/web/src/App.tsx +15 -0
  20. package/web/src/article.css +199 -0
  21. package/web/src/components/Bettercrumb.tsx +86 -0
  22. package/web/src/components/Fonts.tsx +13 -0
  23. package/web/src/components/Header.tsx +10 -0
  24. package/web/src/components/IntentLink.tsx +20 -0
  25. package/web/src/components/Rendrer.tsx +140 -0
  26. package/web/src/components/Search.tsx +275 -0
  27. package/web/src/components/Sidebar.tsx +89 -0
  28. package/web/src/components/ThemeSwitcher.tsx +46 -0
  29. package/web/src/components/ui/breadcrumb.tsx +122 -0
  30. package/web/src/components/ui/button.tsx +60 -0
  31. package/web/src/components/ui/collapsible.tsx +33 -0
  32. package/web/src/components/ui/dropdown-menu.tsx +255 -0
  33. package/web/src/components/ui/input.tsx +21 -0
  34. package/web/src/components/ui/kbd.tsx +28 -0
  35. package/web/src/components/ui/separator.tsx +26 -0
  36. package/web/src/components/ui/sheet.tsx +139 -0
  37. package/web/src/components/ui/sidebar.tsx +727 -0
  38. package/web/src/components/ui/skeleton.tsx +13 -0
  39. package/web/src/components/ui/tooltip.tsx +59 -0
  40. package/web/src/contexts.ts +10 -0
  41. package/web/src/hooks/useMobile.ts +19 -0
  42. package/web/src/lib/utils.tsx +89 -0
  43. package/web/src/main.tsx +100 -0
  44. package/web/src/reportWebVitals.ts +13 -0
  45. package/web/src/styles.css +196 -0
  46. package/web/src/types/index.ts +3 -0
  47. package/web/tsconfig.json +35 -0
  48. package/web/vite.config.ts +31 -0
  49. package/web/vitest.config.ts +16 -0
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # serve-my-md
2
+
3
+ A tiny CLI to generate a static docs website from markdown files.
4
+
5
+ ## Documentation
6
+
7
+ Full docs are at [https://ashishantil.dev/serve-my-md](https://ashishantil.dev/serve-my-md).
8
+
9
+ ## Basic usage
10
+
11
+ ```bash
12
+ serve-my-md --directory .
13
+ ```
14
+
15
+ Run this inside (or pointing to) the folder that contains your markdown docs.
16
+
17
+ ## Commands and options
18
+
19
+ - `serve-my-md`: scans markdown files, builds the static site, and outputs it in the target directory.
20
+ - `-d, --directory <path>`: sets the docs root directory (default: current directory).
21
+ - `-i, --interactive`: asks for directory input interactively.
22
+
23
+ ## Optional customization
24
+
25
+ In your target docs directory, you can optionally create files like `smm.config.json` and `.smmignore` to customize behavior (routing, sorting, ignored paths, etc.).
26
+
27
+ ## Features
28
+
29
+ - **Static site generation** — every page is a standalone `.html` file with pre-rendered content
30
+ - **Groupers** — directory-based sidebar grouping with `(Name)` syntax, excluded from URLs
31
+ - **Full-text search** — site-wide search with `Ctrl+Shift+F`
32
+ - **Keyboard shortcuts** — quick page navigation
33
+ - **Light/dark themes** — automatic or user-toggleable
34
+ - **Responsive** — works on desktop and mobile
35
+ - **Accessibility** — keyboard navigation, ARIA labels, semantic landmarks
36
+ - **Breadcrumbs** — clear navigation context
37
+ - **Custom ordering** — numeric filename prefixes with `trimIndexFromPath` config
38
+ - **Rich markdown** — footnotes, task lists, syntax highlighting
39
+ - **SEO** — per-page OG tags, meta descriptions, pre-rendered content
40
+ - **Font customization** — configurable title/body/mono fonts
41
+ - **Public assets** — copy static files via `publicPath` config
42
+
43
+ ## Future goals
44
+
45
+ - **Search Indexing** — structured search index for smarter results
46
+ - **Config validation** — Zod-based schema validation of `smm.config.json` with JSON Schema export
47
+ - **Sitemap** — automatic `sitemap.xml` generation
48
+ - **Per-page Open Graph** — page-level og tags from frontmatter
49
+ - **Doctor command** — health checks: config validation, route discovery, broken link detection, and more
50
+ - **Link validation** — flag invalid internal links at build-time
51
+ - **Optional RSS** — config-enableable RSS feed for blog/changelog content
52
+ - **SchemaStore upload** — publish JSON Schema to SchemaStore once stable
package/bin/index.js ADDED
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cli/src/lib/logger.ts
4
+ var Logger = class {
5
+ static log(message, type) {
6
+ console.log(`${type ? `[${type.toUpperCase()}] ` : ""}${message}`);
7
+ }
8
+ static error(message) {
9
+ console.error(`[ERROR] ${message}`);
10
+ }
11
+ };
12
+
13
+ // cli/src/shared.ts
14
+ import MarkdownIt from "markdown-it";
15
+ import path2 from "path";
16
+ import Prism from "prismjs";
17
+
18
+ // cli/src/core/index.ts
19
+ import fs2 from "fs/promises";
20
+ import path from "path";
21
+ import { minimatch } from "minimatch";
22
+
23
+ // cli/src/utils/index.ts
24
+ import fs from "fs/promises";
25
+ var indexTokens = "1234567890.";
26
+ function trimIndexFromPath(filePath) {
27
+ return filePath.split("/").map((segment) => {
28
+ let offset = 0;
29
+ let encountered = false;
30
+ while (offset < segment.length && (indexTokens.includes(segment[offset]) || segment[offset] === " " && !encountered))
31
+ if (segment[offset++] !== " ") encountered = true;
32
+ return segment.slice(offset).trim();
33
+ }).join("/");
34
+ }
35
+ function cleanName(filename) {
36
+ return filename === "index.md" ? "" : filename.replace(/\.md$/, "");
37
+ }
38
+ function optional(prop, val) {
39
+ return val ? { [prop]: val } : {};
40
+ }
41
+ function slugify(filepath) {
42
+ return filepath.toLowerCase().split("").map((c) => {
43
+ if (".,;\"'\\:<>`?!".includes(c)) return "";
44
+ if (c === " " || c === "_") return "-";
45
+ return c;
46
+ }).join("");
47
+ }
48
+ async function FileOrDirectoryExists(filepath) {
49
+ try {
50
+ await fs.access(filepath);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+ function makeRoutesOfNestedPaths(nestedPaths, prefix = "/") {
57
+ return nestedPaths.reduce((acc, { pathSegment, children, isGrouper }) => {
58
+ return [
59
+ ...acc,
60
+ ...isGrouper || !children ? [] : [prefix + pathSegment],
61
+ ...children ? makeRoutesOfNestedPaths(
62
+ children,
63
+ prefix + (!isGrouper ? pathSegment + "/" : "")
64
+ ) : isGrouper ? [] : [prefix + pathSegment]
65
+ ];
66
+ }, []);
67
+ }
68
+ function makeRoutesOfNestedPathsRaw(nestedPaths, prefix = "/") {
69
+ return nestedPaths.reduce((acc, { pathSegment, children }) => {
70
+ return [
71
+ ...acc,
72
+ ...children ? makeRoutesOfNestedPathsRaw(
73
+ children,
74
+ prefix + pathSegment + "/"
75
+ ) : [prefix + pathSegment]
76
+ ];
77
+ }, []);
78
+ }
79
+ function ogToHtml(og) {
80
+ const tags = [];
81
+ for (const [key, value] of Object.entries(og)) {
82
+ if (value == null) continue;
83
+ if (["images", "videos", "audios"].includes(key)) continue;
84
+ if (Array.isArray(value)) {
85
+ for (const v of value) {
86
+ tags.push(`<meta property="og:${key}" content="${v}">`);
87
+ }
88
+ } else {
89
+ tags.push(`<meta property="og:${key}" content="${value}">`);
90
+ }
91
+ }
92
+ [...og.images ?? [], ...og.videos ?? [], ...og.audios ?? []].forEach(
93
+ (img) => {
94
+ Object.entries(img).forEach(([k, v]) => {
95
+ if (v == null) return;
96
+ tags.push(`<meta property="og:${k}" content="${v}">`);
97
+ });
98
+ }
99
+ );
100
+ return tags.join("\n");
101
+ }
102
+
103
+ // cli/src/lib/commander.ts
104
+ import * as inquirer from "@inquirer/prompts";
105
+
106
+ // cli/src/config.json
107
+ var config_default = {
108
+ name: "serve-my-md",
109
+ version: "1.0.0",
110
+ description: "A CLI tool to create a ready-to-serve static website from markdown files",
111
+ defaultConfigPath: "./smm.config.json",
112
+ defaultIgnorePath: "./.smmignore"
113
+ };
114
+
115
+ // cli/src/lib/commander.ts
116
+ import { Command } from "commander";
117
+ var program = new Command();
118
+ program.name(config_default.name).description(config_default.description).version(config_default.version);
119
+ program.option("-d, --directory <path>", "Directory to scan for markdown files", ".");
120
+ program.option("-i, --interactive", "Enable interactive mode");
121
+ if (process.env.VITEST) {
122
+ program.option("--skip-build", "Skip the build step");
123
+ }
124
+ program.parse(process.argv);
125
+ var options = program.opts();
126
+ if (options.interactive || options.directory === void 0) {
127
+ const res = await inquirer.input({
128
+ message: `Enter root directory: `,
129
+ default: options.directory || "./"
130
+ });
131
+ options.directory = res.trim();
132
+ }
133
+
134
+ // cli/src/core/index.ts
135
+ import { readdirSync } from "fs";
136
+
137
+ // shared/constants.json
138
+ var constants_default = {
139
+ STATIC_TEMP_CONTENT_PREFIX: "__smm_static_temp_content__"
140
+ };
141
+
142
+ // cli/src/core/index.ts
143
+ var STATIC_TEMP_CONTENT_PREFIX = constants_default.STATIC_TEMP_CONTENT_PREFIX;
144
+ async function readConfig(filepath) {
145
+ try {
146
+ const data = JSON.parse(await fs2.readFile(filepath, "utf-8"));
147
+ return data;
148
+ } catch (err) {
149
+ Logger.log(
150
+ `No config file found at ${filepath}, proceeding with defaults.`,
151
+ "info"
152
+ );
153
+ return {};
154
+ }
155
+ }
156
+ async function parseSmmIgnore(filePath) {
157
+ try {
158
+ let shouldIgnore3 = function(targetPath) {
159
+ const p = targetPath.replace(/\\/g, "/");
160
+ let ignored = false;
161
+ for (const rule of rules) {
162
+ if (minimatch(p, rule.pattern, { dot: true })) {
163
+ ignored = !rule.negated;
164
+ }
165
+ }
166
+ return ignored;
167
+ };
168
+ var shouldIgnore2 = shouldIgnore3;
169
+ const raw = await fs2.readFile(filePath, "utf8");
170
+ const rules = raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#")).map((line) => {
171
+ const negated = line.startsWith("!");
172
+ const pattern = negated ? line.slice(1) : line;
173
+ return { pattern, negated };
174
+ });
175
+ return { rules, shouldIgnore: shouldIgnore3 };
176
+ } catch (err) {
177
+ Logger.log(
178
+ `No .smmignore file found at ${filePath}, proceeding without ignore rules.`,
179
+ "info"
180
+ );
181
+ return {
182
+ rules: [],
183
+ shouldIgnore: (_) => false
184
+ };
185
+ }
186
+ }
187
+ async function getMarkdownFiles(baseUrl, pairChildren) {
188
+ const files = await fs2.readdir(baseUrl, { withFileTypes: true });
189
+ const routeTree = pairChildren || [];
190
+ const promises = [];
191
+ for (const file of files) {
192
+ const filePath = path.join(baseUrl, file.name);
193
+ if (shouldIgnore(filePath.slice(options.directory.length)) || filePath.slice(options.directory.length) === finalConfig.publicPath)
194
+ continue;
195
+ if (file.isDirectory()) {
196
+ const isGrouper = file.name.startsWith("(") && file.name.endsWith(")");
197
+ const dirPair = {
198
+ label: isGrouper ? file.name.slice(1, -1) : file.name,
199
+ children: [],
200
+ pathSegment: file.name,
201
+ isGrouper
202
+ };
203
+ routeTree.push(dirPair);
204
+ promises.push(
205
+ getMarkdownFiles(filePath, dirPair.children)
206
+ );
207
+ } else if (file.name.endsWith(".md")) {
208
+ routeTree.push({
209
+ label: file.name,
210
+ children: null,
211
+ pathSegment: file.name
212
+ });
213
+ promises.push(Promise.resolve([filePath]));
214
+ }
215
+ }
216
+ if (finalConfig.sortRoutes)
217
+ routeTree.sort((a, b) => {
218
+ if (a.label === "index.md") return -1;
219
+ if (b.label === "index.md") return 1;
220
+ if (a.isGrouper && !b.isGrouper) return 1;
221
+ if (b.isGrouper && !a.isGrouper) return -1;
222
+ return a.label.localeCompare(b.label);
223
+ });
224
+ const filess = finalConfig.trimIndexFromPath ? (await Promise.all(promises)).flat().map((val) => trimIndexFromPath(val)) : (await Promise.all(promises)).flat();
225
+ return pairChildren ? filess : { routeTree, files: filess };
226
+ }
227
+ function cleanNestedPaths(routeTree) {
228
+ for (const pair of routeTree) {
229
+ if (finalConfig.trimIndexFromPath) {
230
+ pair.label = trimIndexFromPath(pair.label);
231
+ }
232
+ pair.label = cleanName(pair.label);
233
+ pair.pathSegment = getPath(cleanName(pair.pathSegment)).replaceAll("/", "");
234
+ if (pair.children) {
235
+ cleanNestedPaths(pair.children);
236
+ if (pair.children?.length === 1 && ["", "index.md"].includes(pair.children?.[0]?.label)) {
237
+ pair.children = null;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ function getPath(filepath) {
243
+ let transformedPath = filepath.replace(options.directory, "").replace(/\\/g, "/").replace(/\/index.md$/, "").replace(/\.md$/, "");
244
+ if (finalConfig.trimIndexFromPath) {
245
+ transformedPath = trimIndexFromPath(transformedPath);
246
+ }
247
+ return slugify(transformedPath).split("/").filter((s) => !(s.startsWith("(") && s.endsWith(")"))).join("/") || "/";
248
+ }
249
+ async function parseMD(filepath) {
250
+ const path4 = getPath(filepath);
251
+ return {
252
+ path: path4,
253
+ content: mdParser.render(await fs2.readFile(filepath, "utf-8"))
254
+ };
255
+ }
256
+ async function generateHtml(distDir, routeContent) {
257
+ try {
258
+ let htmlTemplate = await fs2.readFile(
259
+ path.join(import.meta.dirname, "..", "index.html"),
260
+ "utf-8"
261
+ );
262
+ const commentStart = htmlTemplate.indexOf("<!--");
263
+ htmlTemplate = htmlTemplate.replace(
264
+ htmlTemplate.slice(
265
+ commentStart,
266
+ htmlTemplate.indexOf("-->", commentStart) + 3
267
+ ),
268
+ ""
269
+ );
270
+ if (distDir) {
271
+ const files = readdirSync(path.join(distDir, "assets"));
272
+ const cssFile = files.find((file) => file.endsWith(".css"));
273
+ const jsFile = files.find((file) => file.endsWith(".js"));
274
+ const prefix = distDir.slice(path.join(import.meta.dirname, options.directory).length);
275
+ htmlTemplate = htmlTemplate.replace(`<script type="module" src="/src/main.tsx"></script>`, "");
276
+ if (cssFile && jsFile) {
277
+ htmlTemplate = htmlTemplate.replace(
278
+ "{{distAssets}}",
279
+ `<link rel="stylesheet" href="${path.join(
280
+ prefix,
281
+ "assets",
282
+ cssFile
283
+ )}" />
284
+ <script type="module" src="${path.join(
285
+ prefix,
286
+ "assets",
287
+ jsFile
288
+ )}"></script>`
289
+ );
290
+ } else {
291
+ Logger.error(`Could not find CSS and JS files in dist assets.`);
292
+ htmlTemplate = htmlTemplate.replace("{{distAssets}}", "");
293
+ }
294
+ } else {
295
+ htmlTemplate = htmlTemplate.replace("{{distAssets}}", "");
296
+ }
297
+ return htmlTemplate.replace("{{og}}", ogToHtml(finalConfig.og ?? {})).replace("{{title}}", finalConfig.rootTitle ?? "Serve My MD").replace("{{description}}", finalConfig.description ?? "").replace(
298
+ "{{favicon}}",
299
+ finalConfig.favicon ? `<link rel="icon" href="${finalConfig.favicon}" />` : ""
300
+ ).replace(
301
+ "{{fonts}}",
302
+ finalConfig.fonts ? (finalConfig.fonts.title && finalConfig.fonts.title.url ? `<link rel="stylesheet" href="${finalConfig.fonts.title.url}" />` : "") + (finalConfig.fonts.body && finalConfig.fonts.body.url ? `<link rel="stylesheet" href="${finalConfig.fonts.body.url}" />` : "") + (finalConfig.fonts.mono && finalConfig.fonts.mono.url ? `<link rel="stylesheet" href="${finalConfig.fonts.mono.url}" />` : "") : ""
303
+ ).replace("{{content}}", STATIC_TEMP_CONTENT_PREFIX + (routeContent ?? "")).trim();
304
+ } catch (err) {
305
+ throw new Error(`Failed to generate HTML: ${err}`);
306
+ }
307
+ }
308
+ async function buildDistRoutesFromRouteTree(routeTree, groupedRoutes, distPath, prefix = "/") {
309
+ for (const node of routeTree) {
310
+ if (node.children && node.isGrouper) {
311
+ await buildDistRoutesFromRouteTree(
312
+ node.children,
313
+ groupedRoutes,
314
+ distPath,
315
+ prefix
316
+ );
317
+ } else {
318
+ const distRoutePath = path.join(distPath, prefix, node.pathSegment.replace("/", "")) + (node.pathSegment === "" ? "/index.html" : ".html");
319
+ await fs2.mkdir(path.dirname(distRoutePath), { recursive: true });
320
+ const html = await generateHtml(
321
+ distPath,
322
+ groupedRoutes[path.posix.join(prefix, node.pathSegment)]?.[0]?.content
323
+ );
324
+ await fs2.writeFile(distRoutePath, html, "utf-8");
325
+ if (node.children) {
326
+ await buildDistRoutesFromRouteTree(
327
+ node.children,
328
+ groupedRoutes,
329
+ distPath,
330
+ path.join(prefix, node.pathSegment)
331
+ );
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ // cli/src/smm.config.json
338
+ var smm_config_default = {
339
+ rootTitle: "Serve My MD",
340
+ description: "A simple markdown to static site builder.",
341
+ baseRoute: "/",
342
+ defaultTheme: "dark",
343
+ markdownItOptions: {
344
+ html: true,
345
+ xhtmlOut: true,
346
+ breaks: true,
347
+ langPrefix: "language-",
348
+ linkify: true,
349
+ typographer: false
350
+ },
351
+ outDir: "dist",
352
+ favicon: "",
353
+ logo: "",
354
+ name: "Serve My MD",
355
+ showNameWithLogo: true,
356
+ sortRoutes: true,
357
+ trimIndexFromPath: false
358
+ };
359
+
360
+ // cli/src/shared.ts
361
+ import MarkdownItFootNote from "markdown-it-footnote";
362
+ import MarkdownItTasks from "markdown-it-task-lists";
363
+ import loadLanguages from "prismjs/components/index.js";
364
+ var { shouldIgnore } = await parseSmmIgnore(
365
+ path2.join(options.directory, config_default.defaultIgnorePath)
366
+ );
367
+ var finalConfig = {
368
+ ...smm_config_default,
369
+ ...await readConfig(path2.join(options.directory, config_default.defaultConfigPath))
370
+ };
371
+ var md = new MarkdownIt({
372
+ ...finalConfig.markdownItOptions,
373
+ highlight: function(str, lang) {
374
+ if (!Object.hasOwn(Prism.languages, lang)) {
375
+ loadLanguages([lang]);
376
+ }
377
+ const highlighted = Prism.highlight(str, Prism.languages[lang], lang);
378
+ return `<pre class="language-${lang}"><code class="language-${lang}">${highlighted}</code></pre>`;
379
+ }
380
+ }).use(MarkdownItFootNote).use(MarkdownItTasks);
381
+ md.linkify.set({ fuzzyEmail: false });
382
+ var mdParser = md;
383
+
384
+ // cli/src/core/build.ts
385
+ import { cp, rm, writeFile } from "fs/promises";
386
+ import path3, { resolve } from "path";
387
+ import { fileURLToPath } from "url";
388
+ import { build as viteBuild } from "vite";
389
+ import { mkdirSync } from "fs";
390
+ var DIST_DIRNAME = finalConfig.outDir || "dist";
391
+ var WEB_DIRNAME = "web";
392
+ var PUBLIC_DIRNAME = "public";
393
+ async function build(options2) {
394
+ const skipBuild = ("skipBuild" in options2 && options2.skipBuild) ?? false;
395
+ const { routeTree, files: markdownFiles } = await getMarkdownFiles(
396
+ options2.directory
397
+ );
398
+ const parsePromises = [];
399
+ Logger.log("Processing routes...");
400
+ for (const file of makeRoutesOfNestedPathsRaw(routeTree)) {
401
+ parsePromises.push(parseMD(path3.join(options2.directory, file)));
402
+ }
403
+ cleanNestedPaths(routeTree);
404
+ const groupedRoutes = Object.groupBy(
405
+ await Promise.all(parsePromises),
406
+ (route) => route.path
407
+ );
408
+ const routes = makeRoutesOfNestedPaths(routeTree).reduce(
409
+ (acc, pth) => [...acc, ...(groupedRoutes[pth] ?? []).map((r) => ({ ...r, path: path3.join(finalConfig.baseRoute || "/", r.path) }))],
410
+ []
411
+ );
412
+ const out = {
413
+ rootTitle: finalConfig.rootTitle ?? "Documentation",
414
+ description: finalConfig.description ?? "Documentation",
415
+ baseRoute: finalConfig.baseRoute ?? "/",
416
+ defaultTheme: finalConfig.defaultTheme ?? "dark",
417
+ name: finalConfig.name ?? "Serve My MD",
418
+ showNameWithLogo: finalConfig.showNameWithLogo ?? false,
419
+ routes,
420
+ outDir: DIST_DIRNAME,
421
+ fonts: {
422
+ title: finalConfig.fonts?.title?.name || "serif",
423
+ body: finalConfig.fonts?.body?.name || "sans-serif",
424
+ mono: finalConfig.fonts?.mono?.name || "monospace"
425
+ },
426
+ ...optional("favicon", finalConfig.favicon),
427
+ ...optional("version", finalConfig.version)
428
+ };
429
+ routes.forEach((o) => {
430
+ Logger.log(o.path);
431
+ });
432
+ const __dirname = path3.dirname(fileURLToPath(import.meta.url));
433
+ const webDir = path3.join(__dirname, "..", WEB_DIRNAME);
434
+ const distDir = path3.join(webDir, DIST_DIRNAME);
435
+ mkdirSync(path3.join(webDir, "src", ".generated"), { recursive: true });
436
+ await writeFile(
437
+ path3.join(webDir, "src", ".generated", "output.json"),
438
+ JSON.stringify(out)
439
+ );
440
+ await writeFile(
441
+ path3.join(webDir, "src", ".generated", "paths.json"),
442
+ JSON.stringify(routeTree)
443
+ );
444
+ Logger.log("\nParsed MDs");
445
+ await writeFile(path3.join(webDir, "index.html"), await generateHtml());
446
+ Logger.log("Generated HTML from template");
447
+ if (!skipBuild) {
448
+ if (finalConfig.publicPath) {
449
+ if (await FileOrDirectoryExists(
450
+ path3.join(options2.directory, finalConfig.publicPath)
451
+ )) {
452
+ Logger.log(`Copying public assets from ${finalConfig.publicPath}...`);
453
+ await cp(
454
+ path3.join(options2.directory, finalConfig.publicPath),
455
+ path3.join(webDir, PUBLIC_DIRNAME),
456
+ { recursive: true }
457
+ );
458
+ } else {
459
+ Logger.error(`Public path "${finalConfig.publicPath}" does not exist!`);
460
+ }
461
+ }
462
+ Logger.log("Building the app...");
463
+ await viteBuild({
464
+ configFile: resolve(webDir, "vite.config.ts")
465
+ });
466
+ await buildDistRoutesFromRouteTree(routeTree, groupedRoutes, distDir);
467
+ const targetDist = path3.join(options2.directory, DIST_DIRNAME);
468
+ await rm(targetDist, { recursive: true }).catch(() => {
469
+ });
470
+ Logger.log("Built the app, copying results...");
471
+ return cp(distDir, targetDist, { recursive: true }).then(() => {
472
+ Logger.log("Done successfully!");
473
+ return true;
474
+ }).catch((err) => {
475
+ Logger.error("Error copying files: " + err);
476
+ return false;
477
+ });
478
+ }
479
+ return Promise.resolve(true);
480
+ }
481
+
482
+ // cli/src/index.ts
483
+ if (await build(options)) {
484
+ Logger.log("Completed successfully.");
485
+ } else {
486
+ Logger.error("Failed.");
487
+ }
package/index.html ADDED
@@ -0,0 +1,70 @@
1
+ <!-- ? this html file is a template for that final site
2
+
3
+ ? The `content` field is only for SEO purpose, and is obviously overriden on mount by react
4
+ ? `distAssets` is for the css and js files generated by vite. Only used for the sub-routes, not for the home page.
5
+ ? The `loading` class on body is for the loading screen, and is removed on mount by react.
6
+ -->
7
+
8
+ <!doctype html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
13
+ {{favicon}}
14
+ <meta name="theme-color" content="#000000" />
15
+ <meta
16
+ name="description"
17
+ content="{{description}}"
18
+ />
19
+ {{fonts}}
20
+ {{og}}
21
+ <title>{{title}}</title>
22
+ {{distAssets}}
23
+
24
+ <style>
25
+ @keyframes keep-rotating {
26
+ from {
27
+ transform: translate(-50%, -50%) rotate(0deg);
28
+ }
29
+ to {
30
+ transform: translate(-50%, -50%) rotate(360deg);
31
+ }
32
+ }
33
+
34
+ body.loading {
35
+ overflow: hidden;
36
+
37
+ &::before {
38
+ content: '';
39
+ position: fixed;
40
+ inset: 0;
41
+ z-index: 50;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ background-color: rgba(255, 255, 255, 0.8);
46
+ backdrop-filter: blur(4px);
47
+ }
48
+
49
+ &::after {
50
+ content: '';
51
+ position: fixed;
52
+ width: 4rem;
53
+ height: 4rem;
54
+ top: 50%;
55
+ left: 50%;
56
+ transform: translate(-50%, -50%);
57
+ z-index: 51;
58
+ border: 4px solid #3498db;
59
+ border-top-color: transparent;
60
+ border-radius: 50%;
61
+ animation: keep-rotating 1s linear infinite;
62
+ }
63
+ }
64
+ </style>
65
+ </head>
66
+ <body class='loading'>
67
+ <div id="app">{{content}}</div>
68
+ <script type="module" src="/src/main.tsx"></script>
69
+ </body>
70
+ </html>