@shunyasasaki/mkserve 0.1.1

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/dist/cli.mjs +613 -0
  4. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  5. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  7. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  13. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  19. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  20. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  22. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  23. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  25. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  26. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  28. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  29. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  31. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  32. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  34. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  35. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  37. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  46. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  47. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  49. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  50. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  53. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  55. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  57. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  58. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  60. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  61. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  63. package/dist/client/assets/abnfDiagram-O67JEVCF-BniYQwXa.js +1 -0
  64. package/dist/client/assets/arc-BJA87daU.js +1 -0
  65. package/dist/client/assets/architecture-7GRP2DOG-Brw-cPmm.js +1 -0
  66. package/dist/client/assets/architectureDiagram-NJMV4G6O-BNbYrQOP.js +36 -0
  67. package/dist/client/assets/array-BifhSqXX.js +1 -0
  68. package/dist/client/assets/blockDiagram-BEXU5L5S-DFIHGD-G.js +140 -0
  69. package/dist/client/assets/c4Diagram-YGBWAQC7-BkFaWKQU.js +38 -0
  70. package/dist/client/assets/channel-BrqIJdOD.js +1 -0
  71. package/dist/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  72. package/dist/client/assets/chunk-3FUC2YCW-B4uxGZek.js +2 -0
  73. package/dist/client/assets/chunk-5DYCD2WN-CnbWAgyo.js +1 -0
  74. package/dist/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  75. package/dist/client/assets/chunk-742MDFTN-CSlr64s4.js +1 -0
  76. package/dist/client/assets/chunk-7INBJB4K-CelZsjbp.js +62 -0
  77. package/dist/client/assets/chunk-7M6MHVWA-De6KLJmg.js +213 -0
  78. package/dist/client/assets/chunk-7PRAP22T-CE6VUtLz.js +1 -0
  79. package/dist/client/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  80. package/dist/client/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
  81. package/dist/client/assets/chunk-GTNCS2PH-DfgoKLZ5.js +88 -0
  82. package/dist/client/assets/chunk-GWA4HPMP-CPJ61ePi.js +1 -0
  83. package/dist/client/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  84. package/dist/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  85. package/dist/client/assets/chunk-MBY4JIJT--fcyBAwh.js +72 -0
  86. package/dist/client/assets/chunk-NETBCI7D-Rdu9faBx.js +1 -0
  87. package/dist/client/assets/chunk-O7XYJQB3-Bqi4DWie.js +128 -0
  88. package/dist/client/assets/chunk-UA2S7LBM-BjrJ4RAw.js +1 -0
  89. package/dist/client/assets/chunk-WEXAMYUT-CnyLp8pC.js +1 -0
  90. package/dist/client/assets/chunk-XXDRQBXY-C1_eEj17.js +1 -0
  91. package/dist/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  92. package/dist/client/assets/chunk-Z7XXMR3K-BFUBfhD1.js +10 -0
  93. package/dist/client/assets/chunk-ZIGJFQKS-8PrSySWh.js +2 -0
  94. package/dist/client/assets/classDiagram-v2-NBCMYWYE-KufjDRbB.js +217 -0
  95. package/dist/client/assets/cose-bilkent-JH36ORCC-Bm2Bhb7n.js +1 -0
  96. package/dist/client/assets/cynefin-OW5HDTMX-H13mnOIn.js +1 -0
  97. package/dist/client/assets/cynefinDiagram-VND7K2PF-CI5Kf3Kt.js +62 -0
  98. package/dist/client/assets/cytoscape.esm-Yq6u8L66.js +321 -0
  99. package/dist/client/assets/dagre-6A5THRUB-DtRiYFPQ.js +4 -0
  100. package/dist/client/assets/defaultLocale-BFoDCU3G.js +1 -0
  101. package/dist/client/assets/diagram-22UHCM2B-DqFq9NIL.js +200 -0
  102. package/dist/client/assets/diagram-3UASUU5V-C2qQgCvw.js +24 -0
  103. package/dist/client/assets/diagram-ATOU4E4O-B6wVKZ5z.js +3 -0
  104. package/dist/client/assets/diagram-CDSNMT55-owh05Yfi.js +30 -0
  105. package/dist/client/assets/diagram-MLGK6HIB-C55nJ_CR.js +24 -0
  106. package/dist/client/assets/diagram-MPIPVDR6-DeYo8-yc.js +41 -0
  107. package/dist/client/assets/dist-Bfr8PhAA.js +1 -0
  108. package/dist/client/assets/ebnfDiagram-ZINNZB2B-BoJE7Elt.js +1 -0
  109. package/dist/client/assets/elk-276RUBZZ-F1vDPlT8.js +27 -0
  110. package/dist/client/assets/erDiagram-OPXOYQCR-JFHuQAWq.js +99 -0
  111. package/dist/client/assets/eventmodeling-NTZA5JFV-BDnaXJON.js +1 -0
  112. package/dist/client/assets/flowDiagram-KWPJA3E3-DC2FrXkH.js +1 -0
  113. package/dist/client/assets/ganttDiagram-FUAMR5RP-C5psbznc.js +292 -0
  114. package/dist/client/assets/gitGraph-4MIJSDKK-CUDLWiCq.js +1 -0
  115. package/dist/client/assets/gitGraphDiagram-X574FWY7-Cy-gNPac.js +106 -0
  116. package/dist/client/assets/graphlib-DS17s2tU.js +1 -0
  117. package/dist/client/assets/index-BEY9E2Q9.js +2 -0
  118. package/dist/client/assets/index-FULGulkF.css +2 -0
  119. package/dist/client/assets/info-A6RAGUB7-mIpFX_UH.js +1 -0
  120. package/dist/client/assets/infoDiagram-VRGFBTTK-DnkrYsVs.js +2 -0
  121. package/dist/client/assets/init-C-OQMol4.js +1 -0
  122. package/dist/client/assets/ishikawaDiagram-OU5B5YK6-_u6_xJba.js +70 -0
  123. package/dist/client/assets/journeyDiagram-ZHPQQLJL-CY5HU_eC.js +139 -0
  124. package/dist/client/assets/kanban-definition-PNTS6WVX-DvzRwcD7.js +89 -0
  125. package/dist/client/assets/katex-ZlcWpGUi.js +257 -0
  126. package/dist/client/assets/line-B4MFnJjP.js +1 -0
  127. package/dist/client/assets/linear-ZUqVn2xB.js +1 -0
  128. package/dist/client/assets/mermaid-parser.core-DYtj8_hH.js +7 -0
  129. package/dist/client/assets/mermaid.core-DQoUVKoE.js +44 -0
  130. package/dist/client/assets/mindmap-definition-NLK3R4M7-yqrZgVjd.js +96 -0
  131. package/dist/client/assets/ordinal-BDEzSJ7C.js +1 -0
  132. package/dist/client/assets/packet-AYTQ26CC-B26SBXWV.js +1 -0
  133. package/dist/client/assets/path-fybaL0A-.js +1 -0
  134. package/dist/client/assets/pegDiagram-GJSIUBJH-BfvMLRyD.js +1 -0
  135. package/dist/client/assets/pie-WAS4IAKB-DBtOIS90.js +1 -0
  136. package/dist/client/assets/pieDiagram-5QR66LMP-MLPjQpFM.js +39 -0
  137. package/dist/client/assets/quadrantDiagram-O4NWA36T-CprY7yVN.js +7 -0
  138. package/dist/client/assets/radar-RG4KPBEZ-Ced1CHqa.js +1 -0
  139. package/dist/client/assets/railroad-74A4TZTK-BsND1cV-.js +1 -0
  140. package/dist/client/assets/railroad-abnf-HS5TGJTU-DiXPG6oV.js +1 -0
  141. package/dist/client/assets/railroad-ebnf-LZEXJU2U-DgNTMybz.js +1 -0
  142. package/dist/client/assets/railroad-peg-WCYAUIDC-CvY-xH_Q.js +1 -0
  143. package/dist/client/assets/railroadDiagram-XR7U4H2S-BeHaiLp2.js +1 -0
  144. package/dist/client/assets/requirementDiagram-PLB6GJNP-Ck-7Hpab.js +84 -0
  145. package/dist/client/assets/rough.esm-Dy-Kn_BL.js +1 -0
  146. package/dist/client/assets/sankeyDiagram-IPEJSGJF-DmstfGGj.js +40 -0
  147. package/dist/client/assets/sequenceDiagram-PO4LG4MO-wco_mvTt.js +169 -0
  148. package/dist/client/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  149. package/dist/client/assets/src-D_iIdUkB.js +1 -0
  150. package/dist/client/assets/stateDiagram-v2-GCMORJYK-B1l0Xks5.js +291 -0
  151. package/dist/client/assets/swimlanes-2SLR337P-0YucaaVy.js +1 -0
  152. package/dist/client/assets/swimlanesDiagram-TC7HE7FX-Dx92YAeU.js +8 -0
  153. package/dist/client/assets/timeline-definition-EJHVYXUP-BvXqIXyV.js +120 -0
  154. package/dist/client/assets/treeView-Q6P3EWNA-DrBl-X_2.js +1 -0
  155. package/dist/client/assets/treemap-WGGIJYW6-bl_fzdzs.js +1 -0
  156. package/dist/client/assets/usecaseDiagram-POWQR4AR-D_lJYuuq.js +328 -0
  157. package/dist/client/assets/vennDiagram-UO4OBE2U-BCmaseck.js +34 -0
  158. package/dist/client/assets/wardley-WFR3VGLG-B2g3_G7F.js +1 -0
  159. package/dist/client/assets/wardleyDiagram-VNRHLVJA-CZ-FrOG9.js +78 -0
  160. package/dist/client/assets/xychartDiagram-PMCCYNJV-C3xVeS8n.js +7 -0
  161. package/dist/client/index.html +59 -0
  162. package/package.json +96 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,613 @@
1
+ #!/usr/bin/env node
2
+ import { basename, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
+ import { defineCommand, runMain } from "citty";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { readFile, readdir, stat } from "node:fs/promises";
6
+ import { fileURLToPath } from "node:url";
7
+ import fastifyStatic from "@fastify/static";
8
+ import { consola } from "consola";
9
+ import Fastify from "fastify";
10
+ import getPort from "get-port";
11
+ import open from "open";
12
+ import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
13
+ import { toString } from "hast-util-to-string";
14
+ import rehypeAutolinkHeadings from "rehype-autolink-headings";
15
+ import rehypeKatex from "rehype-katex";
16
+ import rehypeRaw from "rehype-raw";
17
+ import rehypeSlug from "rehype-slug";
18
+ import rehypeStringify from "rehype-stringify";
19
+ import remarkFrontmatter from "remark-frontmatter";
20
+ import remarkGfm from "remark-gfm";
21
+ import { remarkAlert } from "remark-github-blockquote-alert";
22
+ import remarkMath from "remark-math";
23
+ import remarkParse from "remark-parse";
24
+ import remarkRehype from "remark-rehype";
25
+ import { createHighlighter } from "shiki";
26
+ import { unified } from "unified";
27
+ import { visit } from "unist-util-visit";
28
+ import { watch } from "chokidar";
29
+ import ignore from "ignore";
30
+ //#region package.json
31
+ var version = "0.1.1";
32
+ var description = "Serve a workspace of Markdown files with a live-reloading preview.";
33
+ //#endregion
34
+ //#region src/shared/paths.ts
35
+ /** URL-encodes each segment of a POSIX path, keeping the slashes. */
36
+ function encodePath(path) {
37
+ return path.split("/").map(encodeURIComponent).join("/");
38
+ }
39
+ /** Inverse of {@link encodePath}; returns the input if it is malformed. */
40
+ function decodePath(path) {
41
+ try {
42
+ return decodeURIComponent(path);
43
+ } catch {
44
+ return path;
45
+ }
46
+ }
47
+ //#endregion
48
+ //#region src/shared/types.ts
49
+ /** Extensions treated as Markdown documents. */
50
+ const MARKDOWN_EXTENSIONS = [".md", ".markdown"];
51
+ /** Returns whether `path` names a Markdown document. */
52
+ function isMarkdownPath(path) {
53
+ const lower = path.toLowerCase();
54
+ return MARKDOWN_EXTENSIONS.some((ext) => lower.endsWith(ext));
55
+ }
56
+ //#endregion
57
+ //#region src/server/render.ts
58
+ /** Languages loaded up front; others are loaded on first use. */
59
+ const PRELOADED_LANGUAGES = [
60
+ "bash",
61
+ "css",
62
+ "diff",
63
+ "html",
64
+ "javascript",
65
+ "json",
66
+ "jsx",
67
+ "markdown",
68
+ "python",
69
+ "shell",
70
+ "sql",
71
+ "toml",
72
+ "tsx",
73
+ "typescript",
74
+ "yaml"
75
+ ];
76
+ const THEMES = {
77
+ light: "github-light",
78
+ dark: "github-dark"
79
+ };
80
+ const AUTOLINK_OPTIONS = {
81
+ behavior: "append",
82
+ properties: {
83
+ className: ["heading-anchor"],
84
+ ariaHidden: "true"
85
+ },
86
+ content: {
87
+ type: "text",
88
+ value: "#"
89
+ }
90
+ };
91
+ /** Elements whose URL attributes point at workspace files. */
92
+ const URL_ATTRIBUTES = {
93
+ a: ["href"],
94
+ img: ["src"],
95
+ source: ["src"],
96
+ video: ["src", "poster"],
97
+ audio: ["src"]
98
+ };
99
+ /**
100
+ * Creates the unified pipeline (CommonMark + GFM, math, alerts, Mermaid,
101
+ * Shiki). The Shiki highlighter is created once and shared by all renders.
102
+ */
103
+ async function createRenderer() {
104
+ const highlighter = await createHighlighter({
105
+ themes: Object.values(THEMES),
106
+ langs: PRELOADED_LANGUAGES
107
+ });
108
+ const processor = unified().use(remarkParse).use(remarkFrontmatter).use(remarkGfm).use(remarkMath).use(remarkAlert).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeSlug).use(rehypeAutolinkHeadings, AUTOLINK_OPTIONS).use(rehypeKatex, { strict: "ignore" }).use(rehypeMermaid).use(rehypeShikiFromHighlighter, highlighter, {
109
+ themes: THEMES,
110
+ lazy: true,
111
+ fallbackLanguage: "text"
112
+ }).use(rehypeRewriteUrls).use(rehypeExtractTitle).use(rehypeStringify);
113
+ return { async render(markdown, docPath) {
114
+ const file = await processor.process({
115
+ value: markdown,
116
+ data: { docPath }
117
+ });
118
+ return {
119
+ html: String(file),
120
+ title: file.data.title
121
+ };
122
+ } };
123
+ }
124
+ /**
125
+ * Turns ```` ```mermaid ```` code blocks into `<pre class="mermaid">` so the
126
+ * client can render them (and Shiki leaves them alone).
127
+ */
128
+ function rehypeMermaid() {
129
+ return (tree) => {
130
+ visit(tree, "element", (node) => {
131
+ const code = node.children[0];
132
+ if (node.tagName === "pre" && code?.type === "element" && code.tagName === "code" && classNames(code).includes("language-mermaid")) {
133
+ node.properties = { className: ["mermaid"] };
134
+ node.children = [{
135
+ type: "text",
136
+ value: toString(code)
137
+ }];
138
+ }
139
+ });
140
+ };
141
+ }
142
+ /**
143
+ * Rewrites relative links: Markdown links become SPA routes, other files are
144
+ * served through `/@ws/`, and external links open in a new tab.
145
+ */
146
+ function rehypeRewriteUrls() {
147
+ return (tree, file) => {
148
+ const docDir = posix.dirname(file.data.docPath ?? "");
149
+ visit(tree, "element", (node) => {
150
+ for (const attr of URL_ATTRIBUTES[node.tagName] ?? []) {
151
+ const value = node.properties[attr];
152
+ if (typeof value !== "string") continue;
153
+ if (isExternal(value)) {
154
+ if (node.tagName === "a") {
155
+ node.properties.target = "_blank";
156
+ node.properties.rel = ["noopener", "noreferrer"];
157
+ }
158
+ continue;
159
+ }
160
+ node.properties[attr] = rewriteUrl(value, docDir, node.tagName === "a");
161
+ }
162
+ });
163
+ };
164
+ }
165
+ function rehypeExtractTitle() {
166
+ return (tree, file) => {
167
+ visit(tree, "element", (node) => {
168
+ if (node.tagName === "h1" && file.data.title === void 0) file.data.title = toString(node).replace(/#$/, "").trim();
169
+ });
170
+ };
171
+ }
172
+ function isExternal(url) {
173
+ return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//");
174
+ }
175
+ /** Exported for tests. */
176
+ function rewriteUrl(url, docDir, isLink) {
177
+ if (url === "" || url.startsWith("#")) return url;
178
+ const cut = url.search(/[?#]/);
179
+ const pathPart = cut === -1 ? url : url.slice(0, cut);
180
+ const suffix = cut === -1 ? "" : url.slice(cut);
181
+ let decoded;
182
+ try {
183
+ decoded = decodeURI(pathPart);
184
+ } catch {
185
+ return url;
186
+ }
187
+ const resolved = decoded.startsWith("/") ? posix.normalize(decoded.slice(1)) : posix.join(docDir, decoded);
188
+ if (resolved.startsWith("..")) return url;
189
+ return (isLink && isMarkdownPath(resolved) ? "/" : "/@ws/") + encodePath(resolved) + suffix;
190
+ }
191
+ function classNames(node) {
192
+ const value = node.properties.className;
193
+ return Array.isArray(value) ? value.map(String) : [];
194
+ }
195
+ //#endregion
196
+ //#region src/server/safe_path.ts
197
+ /**
198
+ * Resolves a workspace-relative path to an absolute one, or returns
199
+ * `undefined` when it would escape `root` (including through symlinks).
200
+ */
201
+ function resolveInRoot(root, relPath) {
202
+ if (relPath.includes("\0")) return;
203
+ const abs = resolve(root, relPath.replace(/^\/+/, ""));
204
+ if (!isInside(root, abs)) return;
205
+ try {
206
+ if (!isInside(realpathSync(root), realpathSync(abs))) return;
207
+ } catch {}
208
+ return abs;
209
+ }
210
+ /** Converts an absolute path inside `root` to a POSIX relative path. */
211
+ function toRelPosix(root, abs) {
212
+ return relative(root, abs).split(sep).join("/");
213
+ }
214
+ function isInside(root, abs) {
215
+ const rel = relative(root, abs);
216
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
217
+ }
218
+ //#endregion
219
+ //#region src/server/sse.ts
220
+ const PING_INTERVAL_MS = 3e4;
221
+ /** Registry of connected `EventSource` clients. */
222
+ var EventHub = class {
223
+ clients = /* @__PURE__ */ new Set();
224
+ pingTimer;
225
+ constructor() {
226
+ this.pingTimer = setInterval(() => this.write("ping", {}), PING_INTERVAL_MS);
227
+ this.pingTimer.unref();
228
+ }
229
+ /** Number of connected clients. */
230
+ get size() {
231
+ return this.clients.size;
232
+ }
233
+ /** Starts an SSE stream on `res` and keeps it until the client leaves. */
234
+ add(res) {
235
+ res.writeHead(200, {
236
+ "content-type": "text/event-stream; charset=utf-8",
237
+ "cache-control": "no-cache",
238
+ connection: "keep-alive",
239
+ "x-accel-buffering": "no"
240
+ });
241
+ res.write("retry: 1000\n\n");
242
+ this.clients.add(res);
243
+ res.on("close", () => this.clients.delete(res));
244
+ }
245
+ /** Sends an event to every client. */
246
+ broadcast(event, data) {
247
+ this.write(event, data);
248
+ }
249
+ /** Ends all streams so the HTTP server can shut down. */
250
+ close() {
251
+ clearInterval(this.pingTimer);
252
+ for (const res of this.clients) res.end();
253
+ this.clients.clear();
254
+ }
255
+ write(event, data) {
256
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
257
+ for (const res of this.clients) res.write(message);
258
+ }
259
+ };
260
+ //#endregion
261
+ //#region src/server/tree.ts
262
+ /**
263
+ * Builds the Markdown-only tree of `root`. Directories without Markdown
264
+ * files (recursively) are pruned; ignored and hidden entries are skipped.
265
+ */
266
+ async function buildTree(root, ignore) {
267
+ const children = await readDir(root, "", ignore);
268
+ return {
269
+ name: basename(root),
270
+ path: "",
271
+ type: "dir",
272
+ children
273
+ };
274
+ }
275
+ async function readDir(root, relDir, ignore) {
276
+ let entries;
277
+ try {
278
+ entries = await readdir(join(root, relDir), { withFileTypes: true });
279
+ } catch {
280
+ return [];
281
+ }
282
+ const nodes = [];
283
+ for (const entry of entries) {
284
+ const path = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
285
+ let isDir = entry.isDirectory();
286
+ let isFile = entry.isFile();
287
+ if (entry.isSymbolicLink()) {
288
+ const target = await stat(join(root, path)).catch(() => void 0);
289
+ isDir = false;
290
+ isFile = target?.isFile() ?? false;
291
+ }
292
+ if (!isDir && !isFile || ignore.isIgnored(path, isDir)) continue;
293
+ if (isDir) {
294
+ const children = await readDir(root, path, ignore);
295
+ if (children.length > 0) nodes.push({
296
+ name: entry.name,
297
+ path,
298
+ type: "dir",
299
+ children
300
+ });
301
+ } else if (isMarkdownPath(entry.name)) nodes.push({
302
+ name: entry.name,
303
+ path,
304
+ type: "file"
305
+ });
306
+ }
307
+ return nodes.sort(compareNodes);
308
+ }
309
+ function compareNodes(a, b) {
310
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
311
+ return a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
312
+ }
313
+ //#endregion
314
+ //#region src/server/watcher.ts
315
+ const DEBOUNCE_MS = 50;
316
+ /**
317
+ * Watches the workspace and broadcasts `change` (Markdown edited, added, or
318
+ * removed), `tree` (structure changed), and `asset` (other file edited).
319
+ */
320
+ function startWatcher(options) {
321
+ const { root, ignore, hub } = options;
322
+ const timers = /* @__PURE__ */ new Map();
323
+ function debounced(key, fn) {
324
+ clearTimeout(timers.get(key));
325
+ timers.set(key, setTimeout(() => {
326
+ timers.delete(key);
327
+ fn();
328
+ }, DEBOUNCE_MS));
329
+ }
330
+ function treeChanged() {
331
+ debounced("\0tree", () => {
332
+ options.onTreeChange();
333
+ hub.broadcast("tree", {});
334
+ });
335
+ }
336
+ const watcher = watch(root, {
337
+ ignoreInitial: true,
338
+ awaitWriteFinish: {
339
+ stabilityThreshold: 100,
340
+ pollInterval: 20
341
+ },
342
+ ignored: (absPath, stats) => {
343
+ const rel = toRelPosix(root, absPath);
344
+ if (rel === "") return false;
345
+ if (basename(rel) === ".gitignore") {
346
+ const dir = posix.dirname(rel);
347
+ return dir !== "." && ignore.isIgnored(dir, true);
348
+ }
349
+ return ignore.isIgnored(rel, stats?.isDirectory() ?? false);
350
+ }
351
+ });
352
+ watcher.on("all", (event, absPath) => {
353
+ const path = toRelPosix(root, absPath);
354
+ if (basename(path) === ".gitignore") {
355
+ ignore.invalidate();
356
+ treeChanged();
357
+ return;
358
+ }
359
+ if (event === "addDir" || event === "unlinkDir") {
360
+ treeChanged();
361
+ return;
362
+ }
363
+ if (!isMarkdownPath(path)) {
364
+ debounced(`asset:${path}`, () => hub.broadcast("asset", { path }));
365
+ return;
366
+ }
367
+ if (event === "add" || event === "unlink") treeChanged();
368
+ debounced(`change:${path}`, () => hub.broadcast("change", { path }));
369
+ });
370
+ return watcher;
371
+ }
372
+ //#endregion
373
+ //#region src/server/workspace_ignore.ts
374
+ /** Directory names that are never part of the workspace view. */
375
+ const ALWAYS_IGNORED = /* @__PURE__ */ new Set([".git", "node_modules"]);
376
+ /**
377
+ * Decides whether workspace paths are hidden, honoring `.gitignore` files at
378
+ * every directory level. Rules are loaded lazily and cached until
379
+ * {@link WorkspaceIgnore.invalidate} is called.
380
+ */
381
+ var WorkspaceIgnore = class {
382
+ root;
383
+ rules = /* @__PURE__ */ new Map();
384
+ constructor(root) {
385
+ this.root = root;
386
+ }
387
+ /** Drops cached rules, e.g. after a `.gitignore` changed. */
388
+ invalidate() {
389
+ this.rules.clear();
390
+ }
391
+ /**
392
+ * Returns whether the POSIX relative path is hidden: a dot-entry, an
393
+ * always-ignored directory, or matched by a `.gitignore` (or inside a
394
+ * directory that is).
395
+ */
396
+ isIgnored(relPath, isDir) {
397
+ const segments = relPath.split("/").filter((s) => s !== "" && s !== ".");
398
+ for (let i = 0; i < segments.length; i++) {
399
+ const name = segments[i];
400
+ if (name.startsWith(".") || ALWAYS_IGNORED.has(name)) return true;
401
+ const entryIsDir = i < segments.length - 1 || isDir;
402
+ if (this.matchesGitignore(segments, i, entryIsDir)) return true;
403
+ }
404
+ return false;
405
+ }
406
+ /** Checks segments[0..=index] against every ancestor `.gitignore`. */
407
+ matchesGitignore(segments, index, isDir) {
408
+ for (let base = 0; base <= index; base++) {
409
+ const rules = this.load(segments.slice(0, base).join("/"));
410
+ if (!rules) continue;
411
+ const sub = segments.slice(base, index + 1).join("/");
412
+ if (rules.ignores(isDir ? `${sub}/` : sub)) return true;
413
+ }
414
+ return false;
415
+ }
416
+ load(dir) {
417
+ const cached = this.rules.get(dir);
418
+ if (cached !== void 0) return cached;
419
+ let rules = null;
420
+ try {
421
+ const text = readFileSync(join(this.root, dir, ".gitignore"), "utf8");
422
+ rules = ignore().add(text);
423
+ } catch {}
424
+ this.rules.set(dir, rules);
425
+ return rules;
426
+ }
427
+ };
428
+ //#endregion
429
+ //#region src/server/app.ts
430
+ /** Built client, located next to the bundled `dist/cli.mjs`. */
431
+ const CLIENT_DIR = fileURLToPath(new URL("./client/", import.meta.url));
432
+ const CLIENT_NOT_BUILT_HTML = "<!doctype html><title>mkserve</title><p>The client is not built. Run <code>pnpm build</code>.</p>";
433
+ /** Creates the Fastify app (routes, static files, watcher) without listening. */
434
+ async function buildApp(options) {
435
+ const { root } = options;
436
+ const clientDir = options.clientDir ?? CLIENT_DIR;
437
+ const renderer = options.renderer ?? await createRenderer();
438
+ const ignore = new WorkspaceIgnore(root);
439
+ const hub = new EventHub();
440
+ const app = Fastify({ forceCloseConnections: true });
441
+ let treeCache;
442
+ function getTree() {
443
+ if (!options.watch) return buildTree(root, ignore);
444
+ treeCache ??= buildTree(root, ignore);
445
+ return treeCache;
446
+ }
447
+ if (options.watch) {
448
+ const watcher = startWatcher({
449
+ root,
450
+ ignore,
451
+ hub,
452
+ onTreeChange: () => {
453
+ treeCache = void 0;
454
+ }
455
+ });
456
+ app.addHook("onClose", async () => {
457
+ await watcher.close();
458
+ });
459
+ }
460
+ app.addHook("preClose", async () => hub.close());
461
+ await app.register(fastifyStatic, {
462
+ root,
463
+ serve: false
464
+ });
465
+ const indexHtml = existsSync(join(clientDir, "index.html")) ? readFileSync(join(clientDir, "index.html"), "utf8") : CLIENT_NOT_BUILT_HTML;
466
+ if (existsSync(clientDir)) await app.register(fastifyStatic, {
467
+ root: clientDir,
468
+ prefix: "/",
469
+ index: false,
470
+ wildcard: false,
471
+ decorateReply: false
472
+ });
473
+ app.get("/api/info", async () => {
474
+ return {
475
+ name: basename(root),
476
+ version,
477
+ watch: options.watch
478
+ };
479
+ });
480
+ app.get("/api/tree", () => getTree());
481
+ app.get("/api/render", async (request, reply) => {
482
+ const relPath = request.query.path;
483
+ if (!relPath || !isMarkdownPath(relPath)) return sendError(reply, 400, "A Markdown path is required");
484
+ const abs = resolveInRoot(root, relPath);
485
+ if (abs === void 0) return sendError(reply, 400, "Path is outside the workspace");
486
+ const path = toRelPosix(root, abs);
487
+ if (ignore.isIgnored(path, false)) return sendError(reply, 404, "Not found");
488
+ let source;
489
+ let mtime;
490
+ try {
491
+ [source, mtime] = await Promise.all([readFile(abs, "utf8"), stat(abs).then((s) => s.mtimeMs)]);
492
+ } catch {
493
+ return sendError(reply, 404, "Not found");
494
+ }
495
+ const { html, title } = await renderer.render(source, path);
496
+ return {
497
+ path,
498
+ html,
499
+ title: title ?? basename(path),
500
+ mtime
501
+ };
502
+ });
503
+ app.get("/api/events", (request, reply) => {
504
+ if (!options.watch) return sendError(reply, 404, "Live reload is disabled");
505
+ reply.hijack();
506
+ hub.add(reply.raw);
507
+ return reply;
508
+ });
509
+ app.get("/@ws/*", (request, reply) => {
510
+ const abs = resolveInRoot(root, decodePath(request.params["*"]));
511
+ if (abs === void 0) return sendError(reply, 400, "Path is outside the workspace");
512
+ const path = toRelPosix(root, abs);
513
+ if (path === "" || ignore.isIgnored(path, false)) return sendError(reply, 404, "Not found");
514
+ return reply.header("cache-control", "no-cache").sendFile(path);
515
+ });
516
+ app.setNotFoundHandler((request, reply) => {
517
+ const wantsHtml = request.headers.accept?.includes("text/html") ?? false;
518
+ const isApi = /^\/(api|@ws)\//.test(request.url);
519
+ if (request.method === "GET" && wantsHtml && !isApi) return reply.type("text/html; charset=utf-8").send(indexHtml);
520
+ return sendError(reply, 404, "Not found");
521
+ });
522
+ return app;
523
+ }
524
+ function sendError(reply, statusCode, message) {
525
+ return reply.code(statusCode).send({ error: message });
526
+ }
527
+ /** Builds the app, listens, and wires graceful shutdown. */
528
+ async function startServer(options) {
529
+ if (!existsSync(options.root)) {
530
+ consola.error(`Directory not found: ${options.root}`);
531
+ process.exit(1);
532
+ }
533
+ const app = await buildApp({
534
+ root: options.root,
535
+ watch: options.watch
536
+ });
537
+ const port = options.port ?? await getPort({ port: options.defaultPort });
538
+ try {
539
+ await app.listen({
540
+ port,
541
+ host: options.host
542
+ });
543
+ } catch (err) {
544
+ consola.error(err instanceof Error ? err.message : err);
545
+ process.exit(1);
546
+ }
547
+ const url = `http://${options.host}:${port}`;
548
+ consola.success(`mkserve v${version} → ${url} (root: ${options.root})`);
549
+ if (options.open) await open(url);
550
+ const shutdown = async () => {
551
+ await app.close();
552
+ process.exit(0);
553
+ };
554
+ process.once("SIGINT", shutdown);
555
+ process.once("SIGTERM", shutdown);
556
+ }
557
+ //#endregion
558
+ //#region src/cli.ts
559
+ const DEFAULT_PORT = 3e3;
560
+ const main = defineCommand({
561
+ meta: {
562
+ name: "mkserve",
563
+ version,
564
+ description
565
+ },
566
+ args: {
567
+ dir: {
568
+ type: "positional",
569
+ description: "Workspace root",
570
+ required: false,
571
+ default: "."
572
+ },
573
+ port: {
574
+ type: "string",
575
+ alias: "p",
576
+ description: `Port to listen on (default: ${DEFAULT_PORT})`
577
+ },
578
+ host: {
579
+ type: "string",
580
+ description: "Bind address",
581
+ default: "127.0.0.1"
582
+ },
583
+ open: {
584
+ type: "boolean",
585
+ alias: "o",
586
+ description: "Open the browser after start",
587
+ default: false
588
+ },
589
+ watch: {
590
+ type: "boolean",
591
+ description: "Reload pages on file changes (use --no-watch to disable)",
592
+ default: true
593
+ }
594
+ },
595
+ async run({ args }) {
596
+ await startServer({
597
+ root: resolve(args.dir),
598
+ port: args.port === void 0 ? void 0 : parsePort(args.port),
599
+ defaultPort: DEFAULT_PORT,
600
+ host: args.host,
601
+ open: args.open,
602
+ watch: args.watch
603
+ });
604
+ }
605
+ });
606
+ function parsePort(value) {
607
+ const port = Number(value);
608
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${value}`);
609
+ return port;
610
+ }
611
+ runMain(main);
612
+ //#endregion
613
+ export {};