seemore 1.0.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.
@@ -0,0 +1,1635 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/node/paths.ts
13
+ var paths_exports = {};
14
+ __export(paths_exports, {
15
+ appRoot: () => appRoot,
16
+ cacheDir: () => cacheDir,
17
+ packageRoot: () => packageRoot,
18
+ resolveContentRoot: () => resolveContentRoot
19
+ });
20
+ import { existsSync as existsSync2, realpathSync } from "fs";
21
+ import { createHash } from "crypto";
22
+ import { tmpdir } from "os";
23
+ import { dirname as dirname3, join as join2, resolve as resolve3 } from "path";
24
+ import { fileURLToPath } from "url";
25
+ function packageRoot() {
26
+ let dir = dirname3(fileURLToPath(import.meta.url));
27
+ for (let depth = 0; depth < 10; depth++) {
28
+ if (existsSync2(join2(dir, "package.json"))) return dir;
29
+ const parent = dirname3(dir);
30
+ if (parent === dir) break;
31
+ dir = parent;
32
+ }
33
+ throw new Error("seemore: could not locate its own package root.");
34
+ }
35
+ function appRoot() {
36
+ return join2(packageRoot(), "src", "app");
37
+ }
38
+ function cacheDir(contentRoot) {
39
+ const key = createHash("sha256").update(resolve3(contentRoot)).digest("hex").slice(0, 12);
40
+ return join2(tmpdir(), "seemore", key);
41
+ }
42
+ function resolveContentRoot(cwd, explicit) {
43
+ if (explicit !== void 0) return canonicalise(resolve3(cwd, explicit));
44
+ for (const candidate of ["docs", "content"]) {
45
+ const dir = resolve3(cwd, candidate);
46
+ if (existsSync2(dir)) return canonicalise(dir);
47
+ }
48
+ return canonicalise(resolve3(cwd));
49
+ }
50
+ function canonicalise(dir) {
51
+ try {
52
+ return realpathSync.native(dir);
53
+ } catch {
54
+ return dir;
55
+ }
56
+ }
57
+ var init_paths = __esm({
58
+ "src/node/paths.ts"() {
59
+ "use strict";
60
+ }
61
+ });
62
+
63
+ // src/cli/index.ts
64
+ import { parseArgs } from "util";
65
+ import pc4 from "picocolors";
66
+
67
+ // src/cli/build.ts
68
+ import { mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
69
+ import { createRequire as createRequire2 } from "module";
70
+ import { tmpdir as tmpdir2 } from "os";
71
+ import { isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve as resolve5 } from "path";
72
+ import pc2 from "picocolors";
73
+ import { build as viteBuild } from "vite";
74
+
75
+ // src/node/config/load.ts
76
+ import { existsSync } from "fs";
77
+ import { dirname, isAbsolute, resolve } from "path";
78
+ import { createJiti } from "jiti";
79
+ import "zod";
80
+
81
+ // src/shared/base.ts
82
+ var EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
83
+ function normaliseBase(base) {
84
+ if (base === void 0 || base === "") return "/";
85
+ if (EXTERNAL.test(base)) {
86
+ throw new Error(
87
+ `Invalid \`base\`: ${JSON.stringify(base)}. \`base\` is a path on the host, not a URL \u2014 use "/${base.replace(/^.*?:\/\/[^/]*/, "").replace(/^\/+/, "")}".`
88
+ );
89
+ }
90
+ const trimmed = base.replace(/^\/+/, "").replace(/\/+$/, "");
91
+ return trimmed === "" ? "/" : `/${trimmed}/`;
92
+ }
93
+ function isExternalHref(href) {
94
+ return EXTERNAL.test(href) || href.startsWith("#") || !href.startsWith("/");
95
+ }
96
+ function withBase(base, href) {
97
+ const b = normaliseBase(base);
98
+ if (b === "/" || isExternalHref(href)) return href;
99
+ if (href === "/") return b;
100
+ if (href === b.slice(0, -1) || href.startsWith(b)) return href;
101
+ return b + href.replace(/^\/+/, "");
102
+ }
103
+
104
+ // src/shared/types.ts
105
+ var FEATURES = [
106
+ "navigation.instant.prefetch",
107
+ "navigation.instant.preview",
108
+ "navigation.footer",
109
+ "navigation.top",
110
+ "navigation.path",
111
+ "navigation.sections",
112
+ "navigation.prune",
113
+ "toc.follow",
114
+ "toc.integrate",
115
+ "content.code.copy",
116
+ "content.action.edit",
117
+ "search.suggest",
118
+ "search.highlight",
119
+ "social.cards"
120
+ ];
121
+
122
+ // src/node/config/features.ts
123
+ var FEATURE_DEFAULTS = {
124
+ "navigation.instant.prefetch": true,
125
+ "navigation.instant.preview": false,
126
+ "navigation.footer": true,
127
+ "navigation.top": true,
128
+ "navigation.path": false,
129
+ "navigation.sections": false,
130
+ "navigation.prune": false,
131
+ "toc.follow": true,
132
+ "toc.integrate": false,
133
+ "content.code.copy": true,
134
+ // Implicitly on when `editLink` is configured; there is nothing to link to otherwise.
135
+ "content.action.edit": false,
136
+ "search.suggest": true,
137
+ "search.highlight": true,
138
+ "social.cards": false
139
+ };
140
+ var RULES = [
141
+ {
142
+ kind: "conflict",
143
+ a: "toc.integrate",
144
+ b: "toc.follow",
145
+ why: "`toc.integrate` merges the table of contents into the sidebar, leaving no separate TOC pane for `toc.follow` to scroll."
146
+ },
147
+ {
148
+ kind: "requires",
149
+ flag: "navigation.instant.preview",
150
+ needs: "navigation.instant.prefetch",
151
+ why: "`navigation.instant.preview` renders the target page in a popover, which is only possible once prefetch has loaded it."
152
+ }
153
+ ];
154
+ function resolveFeatures(input, implicit = {}) {
155
+ const resolved = { ...FEATURE_DEFAULTS, ...implicit };
156
+ for (const flag of input) {
157
+ const off = flag.startsWith("!");
158
+ const name = off ? flag.slice(1) : flag;
159
+ resolved[name] = !off;
160
+ }
161
+ const problems = [];
162
+ for (const rule of RULES) {
163
+ if (rule.kind === "conflict") {
164
+ if (!resolved[rule.a] || !resolved[rule.b]) continue;
165
+ const fix = FEATURE_DEFAULTS[rule.b] ? `Add '!${rule.b}' to \`features\` to switch it off.` : `Remove '${rule.b}' from \`features\`.`;
166
+ problems.push(`\`${rule.a}\` cannot be combined with \`${rule.b}\`. ${rule.why} ${fix}`);
167
+ } else if (resolved[rule.flag] && !resolved[rule.needs]) {
168
+ problems.push(
169
+ `\`${rule.flag}\` requires \`${rule.needs}\`, which is switched off. ${rule.why}`
170
+ );
171
+ }
172
+ }
173
+ if (problems.length > 0) {
174
+ throw new Error(
175
+ `Incompatible \`features\` in seemore config:
176
+ ${problems.map((p) => ` - ${p}`).join("\n")}`
177
+ );
178
+ }
179
+ return resolved;
180
+ }
181
+
182
+ // src/node/config/schema.ts
183
+ import { z } from "zod";
184
+ var THEMES = [
185
+ "neutral",
186
+ "black",
187
+ "catppuccin",
188
+ "dusk",
189
+ "ocean",
190
+ "purple",
191
+ "ruby",
192
+ "solar",
193
+ "aspen",
194
+ "emerald",
195
+ "vitepress",
196
+ "shadcn"
197
+ ];
198
+ var featureFlag = z.enum([...FEATURES, ...FEATURES.map((f) => `!${f}`)]);
199
+ var navItem = z.lazy(
200
+ () => z.object({
201
+ text: z.string(),
202
+ link: z.string().optional(),
203
+ items: z.array(navItem).optional()
204
+ })
205
+ );
206
+ var searchSchema = z.union([
207
+ z.literal("static"),
208
+ z.object({ provider: z.literal("static") }),
209
+ z.object({
210
+ provider: z.literal("orama-cloud"),
211
+ endpoint: z.string(),
212
+ apiKey: z.string()
213
+ }),
214
+ z.object({
215
+ provider: z.literal("algolia"),
216
+ appId: z.string(),
217
+ apiKey: z.string(),
218
+ indexName: z.string()
219
+ })
220
+ ]);
221
+ var configSchema = z.object({
222
+ title: z.string().default("Documentation"),
223
+ description: z.string().optional(),
224
+ favicon: z.string().optional(),
225
+ /** Subpath the site is served from, e.g. `/my-repo/`. Never inferred. */
226
+ base: z.string().optional(),
227
+ theme: z.enum(THEMES).default("neutral"),
228
+ /** A CSS file appended after everything else, so it wins. */
229
+ css: z.string().optional(),
230
+ features: z.array(featureFlag).default([]),
231
+ nav: z.array(navItem).optional(),
232
+ footer: z.object({
233
+ text: z.string().optional(),
234
+ links: z.array(z.object({ text: z.string(), link: z.string() })).optional()
235
+ }).optional(),
236
+ editLink: z.object({
237
+ base: z.string(),
238
+ text: z.string().default("Edit this page")
239
+ }).optional(),
240
+ search: searchSchema.default("static"),
241
+ exclude: z.array(z.string()).default([])
242
+ });
243
+
244
+ // src/node/config/load.ts
245
+ var CONFIG_NAMES = ["seemore.config.ts", "seemore.config.mts", "seemore.config.js", "seemore.config.mjs"];
246
+ function resolveConfig(input, options) {
247
+ const parsed = parseOrThrow(input, options.configFile);
248
+ const search = parsed.search === "static" ? { provider: "static" } : parsed.search;
249
+ const features = resolveFeatures(parsed.features, {
250
+ // Nothing to link to without an edit base, so the flag follows the option.
251
+ "content.action.edit": parsed.editLink !== void 0
252
+ });
253
+ return {
254
+ title: parsed.title,
255
+ description: parsed.description,
256
+ favicon: parsed.favicon,
257
+ base: normaliseBase(parsed.base),
258
+ theme: parsed.theme,
259
+ css: parsed.css === void 0 ? void 0 : resolveFrom(options.root, parsed.css),
260
+ features,
261
+ nav: parsed.nav,
262
+ footer: parsed.footer,
263
+ editLink: parsed.editLink,
264
+ search,
265
+ exclude: parsed.exclude,
266
+ root: options.root,
267
+ configFile: options.configFile
268
+ };
269
+ }
270
+ async function loadConfig(options) {
271
+ const file = findConfigFile(options);
272
+ if (file === void 0) {
273
+ return { config: resolveConfig({}, { root: options.root }) };
274
+ }
275
+ const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false });
276
+ let loaded;
277
+ try {
278
+ loaded = await jiti.import(file, { default: true });
279
+ } catch (error) {
280
+ throw new Error(`Failed to load ${file}:
281
+ ${error instanceof Error ? error.message : String(error)}`, {
282
+ cause: error
283
+ });
284
+ }
285
+ if (loaded === null || typeof loaded !== "object") {
286
+ throw new Error(`${file} must export a config object as its default export, got ${typeof loaded}.`);
287
+ }
288
+ return {
289
+ config: resolveConfig(loaded, { root: dirname(file), configFile: file }),
290
+ file
291
+ };
292
+ }
293
+ function findConfigFile({ root, configPath }) {
294
+ if (configPath !== void 0) {
295
+ const absolute = resolveFrom(root, configPath);
296
+ if (!existsSync(absolute)) {
297
+ throw new Error(`Config file not found: ${absolute}`);
298
+ }
299
+ return absolute;
300
+ }
301
+ for (const name of CONFIG_NAMES) {
302
+ const candidate = resolve(root, name);
303
+ if (existsSync(candidate)) return candidate;
304
+ }
305
+ return void 0;
306
+ }
307
+ function resolveFrom(root, path) {
308
+ return isAbsolute(path) ? path : resolve(root, path);
309
+ }
310
+ function parseOrThrow(input, file) {
311
+ const result = configSchema.safeParse(input);
312
+ if (result.success) return result.data;
313
+ const where = file === void 0 ? "seemore config" : file;
314
+ const issues = result.error.issues.map((issue) => {
315
+ const field = issue.path.length === 0 ? "(root)" : issue.path.join(".");
316
+ return ` - ${field}: ${explain(issue)}`;
317
+ });
318
+ throw new Error(`Invalid ${where}:
319
+ ${issues.join("\n")}`);
320
+ }
321
+ function explain(issue) {
322
+ if (issue.code === "invalid_value" && issue.path.join(".") === "theme") {
323
+ return `unknown theme. Valid themes: ${THEMES.join(", ")}.`;
324
+ }
325
+ return issue.message;
326
+ }
327
+
328
+ // src/node/content/links.ts
329
+ import { slug as slugify2 } from "github-slugger";
330
+
331
+ // src/node/content/slug.ts
332
+ import { slug as slugify } from "github-slugger";
333
+ var INDEX_NAMES = /* @__PURE__ */ new Set(["index", "readme"]);
334
+ var CONTENT_EXT = /\.mdx?$/i;
335
+ function toPosix(file) {
336
+ return file.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
337
+ }
338
+ function slugifySegment(segment) {
339
+ const slugged = slugify(segment);
340
+ if (slugged !== "") return slugged;
341
+ const fallback = segment.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
342
+ return fallback === "" ? "untitled" : fallback;
343
+ }
344
+ function toRoute(file) {
345
+ const posix = toPosix(file);
346
+ const segments = posix.split("/");
347
+ const basename2 = segments.pop() ?? "";
348
+ const stem = basename2.replace(CONTENT_EXT, "");
349
+ const isIndex = INDEX_NAMES.has(stem.toLowerCase());
350
+ const slugs = segments.map(slugifySegment);
351
+ if (!isIndex) slugs.push(slugifySegment(stem));
352
+ return {
353
+ file: posix,
354
+ url: slugs.length === 0 ? "/" : `/${slugs.join("/")}`,
355
+ slugs,
356
+ output: [...slugs, "index.html"].join("/"),
357
+ isIndex
358
+ };
359
+ }
360
+ function resolveRoutes(files) {
361
+ const sorted = [...files].map(toPosix).sort();
362
+ const byUrl = /* @__PURE__ */ new Map();
363
+ const warnings = [];
364
+ const errors = [];
365
+ for (const file of sorted) {
366
+ const route = toRoute(file);
367
+ const bucket = byUrl.get(route.url);
368
+ if (bucket) bucket.push(route);
369
+ else byUrl.set(route.url, [route]);
370
+ }
371
+ const routes = [];
372
+ for (const [url, candidates] of [...byUrl.entries()].sort(([a], [b]) => a < b ? -1 : 1)) {
373
+ if (candidates.length === 1) {
374
+ routes.push(candidates[0]);
375
+ continue;
376
+ }
377
+ const indexes = candidates.filter((c) => c.isIndex);
378
+ if (indexes.length === candidates.length) {
379
+ const winner = indexes.find((c) => c.file.split("/").pop()?.toLowerCase().startsWith("index")) ?? indexes[0];
380
+ const losers = indexes.filter((c) => c !== winner);
381
+ warnings.push(
382
+ `${url} has more than one index file: using ${winner.file}, ignoring ${losers.map((l) => l.file).join(", ")}.`
383
+ );
384
+ routes.push(winner);
385
+ continue;
386
+ }
387
+ errors.push(
388
+ `Duplicate route ${url} produced by ${candidates.length} files:
389
+ ` + candidates.map((c) => ` - ${c.file}`).join("\n") + `
390
+ Rename one of them, or exclude it with \`exclude\` in seemore.config.ts.`
391
+ );
392
+ }
393
+ return { routes, errors, warnings };
394
+ }
395
+
396
+ // src/node/content/links.ts
397
+ var CONTENT_EXT2 = /\.mdx?$/i;
398
+ function createLinkResolver(pages, base) {
399
+ const byPath = /* @__PURE__ */ new Map();
400
+ const byName = /* @__PURE__ */ new Map();
401
+ const add = (map, key, page) => {
402
+ const bucket = map.get(key);
403
+ if (bucket) bucket.push(page);
404
+ else map.set(key, [page]);
405
+ };
406
+ for (const page of pages) {
407
+ const withoutExt = page.file.replace(CONTENT_EXT2, "");
408
+ byPath.set(withoutExt.toLowerCase(), page);
409
+ byPath.set(page.file.toLowerCase(), page);
410
+ if (page.isIndex) {
411
+ const dir = withoutExt.split("/").slice(0, -1).join("/");
412
+ if (dir !== "") byPath.set(dir.toLowerCase(), page);
413
+ }
414
+ const basename2 = withoutExt.split("/").pop() ?? "";
415
+ add(byName, basename2.toLowerCase(), page);
416
+ const slugged = slugifySegment(basename2);
417
+ if (slugged !== basename2.toLowerCase()) add(byName, slugged, page);
418
+ }
419
+ const pick = (candidates) => [...candidates].sort((a, b) => {
420
+ const depth = a.file.split("/").length - b.file.split("/").length;
421
+ return depth !== 0 ? depth : a.file.localeCompare(b.file);
422
+ })[0];
423
+ function lookup(target) {
424
+ const cleaned = toPosix(target).replace(/^\/+/, "").replace(CONTENT_EXT2, "");
425
+ const key = cleaned.toLowerCase();
426
+ const exact = byPath.get(key);
427
+ if (exact) return { page: exact };
428
+ const named = byName.get(key) ?? byName.get(slugifySegment(cleaned));
429
+ if (named === void 0 || named.length === 0) return {};
430
+ if (named.length === 1) return { page: named[0] };
431
+ return { page: pick(named), ambiguous: named };
432
+ }
433
+ function href(page, hash) {
434
+ const url = withBase(base, page.url);
435
+ return hash === void 0 ? url : `${url}#${slugify2(hash)}`;
436
+ }
437
+ return {
438
+ resolveHref(raw, fromFile) {
439
+ if (isExternalHref(raw) && !raw.startsWith(".") && !CONTENT_EXT2.test(raw.split("#")[0] ?? "")) {
440
+ return { href: raw };
441
+ }
442
+ const [pathPart = "", hashPart] = splitHash(raw);
443
+ if (!CONTENT_EXT2.test(pathPart)) return { href: raw };
444
+ const fromDir = pathPart.startsWith("/") ? [] : toPosix(fromFile).split("/").slice(0, -1);
445
+ const resolved = joinPosix(fromDir, pathPart);
446
+ const page = byPath.get(resolved.toLowerCase());
447
+ if (page === void 0) {
448
+ return {
449
+ href: raw,
450
+ warning: `Broken link ${raw} in ${toPosix(fromFile)}: no page at ${resolved}.`
451
+ };
452
+ }
453
+ return { href: href(page, hashPart) };
454
+ },
455
+ resolveWikilink(target, fromFile) {
456
+ const pipe = target.indexOf("|");
457
+ const linkPart = (pipe === -1 ? target : target.slice(0, pipe)).trim();
458
+ const label = (pipe === -1 ? target : target.slice(pipe + 1)).trim();
459
+ const [pathPart = "", hashPart] = splitHash(linkPart);
460
+ const { page, ambiguous } = lookup(pathPart);
461
+ if (page === void 0) {
462
+ return {
463
+ label,
464
+ warning: `Dead wikilink [[${target}]] in ${toPosix(fromFile)}: no page matches "${pathPart}".`
465
+ };
466
+ }
467
+ const warning = ambiguous === void 0 ? void 0 : `Ambiguous wikilink [[${target}]] in ${toPosix(fromFile)}: matches ${ambiguous.map((c) => c.file).sort().join(", ")}. Using ${page.file}.`;
468
+ return { href: href(page, hashPart), label, warning };
469
+ }
470
+ };
471
+ }
472
+ function splitHash(value) {
473
+ const index = value.indexOf("#");
474
+ if (index === -1) return [value, void 0];
475
+ return [value.slice(0, index), value.slice(index + 1)];
476
+ }
477
+ function joinPosix(fromDir, relative3) {
478
+ const segments = [...fromDir];
479
+ for (const part of toPosix(relative3).split("/")) {
480
+ if (part === "" || part === ".") continue;
481
+ if (part === "..") segments.pop();
482
+ else segments.push(part);
483
+ }
484
+ return segments.join("/").replace(CONTENT_EXT2, "");
485
+ }
486
+
487
+ // src/node/content/source.ts
488
+ import { dynamicLoader } from "fumadocs-core/source";
489
+
490
+ // src/node/content/scan.ts
491
+ import { readFileSync } from "fs";
492
+ import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
493
+ import { globSync } from "tinyglobby";
494
+ import { z as z4 } from "zod";
495
+
496
+ // src/node/content/frontmatter.ts
497
+ import matter from "gray-matter";
498
+ import { z as z3 } from "zod";
499
+ var frontmatterSchema = z3.object({
500
+ title: z3.string().optional(),
501
+ description: z3.string().optional(),
502
+ icon: z3.string().optional(),
503
+ /** Sidebar ordering, second only to `meta.json`. */
504
+ order: z3.number().optional(),
505
+ /**
506
+ * Excluded from the build. Dev keeps drafts so they can be written, so a link to one
507
+ * works while you write it and warns as a dead link when you build.
508
+ */
509
+ draft: z3.boolean().optional()
510
+ }).loose();
511
+ function parseFrontmatter(source, file) {
512
+ let parsed;
513
+ try {
514
+ parsed = matter(source);
515
+ } catch (error) {
516
+ throw new Error(
517
+ `Invalid frontmatter in ${file}: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`,
518
+ { cause: error }
519
+ );
520
+ }
521
+ return { data: validateFrontmatter(parsed.data, file), content: parsed.content };
522
+ }
523
+ function validateFrontmatter(data, file) {
524
+ const result = frontmatterSchema.safeParse(data ?? {});
525
+ if (result.success) return result.data;
526
+ const issues = result.error.issues.map((issue) => {
527
+ const field = issue.path.length === 0 ? "(root)" : issue.path.join(".");
528
+ return ` - ${field}: ${issue.message}`;
529
+ });
530
+ throw new Error(`Invalid frontmatter in ${file}:
531
+ ${issues.join("\n")}`);
532
+ }
533
+
534
+ // src/node/content/scan.ts
535
+ var DEFAULT_EXCLUDES = [
536
+ "**/node_modules/**",
537
+ "**/.git/**",
538
+ "**/dist/**",
539
+ "**/.seemore/**",
540
+ "**/.*/**",
541
+ "**/.*",
542
+ "**/CHANGELOG.md"
543
+ ];
544
+ var metaSchema = z4.object({
545
+ title: z4.string().optional(),
546
+ icon: z4.string().optional(),
547
+ root: z4.boolean().optional(),
548
+ pages: z4.array(z4.string()).optional(),
549
+ pagesIndex: z4.string().optional(),
550
+ defaultOpen: z4.boolean().optional(),
551
+ collapsible: z4.boolean().optional(),
552
+ description: z4.string().optional()
553
+ }).loose();
554
+ function scan(options) {
555
+ const contentRoot = resolve2(options.contentRoot);
556
+ const ignore = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
557
+ const contentFiles = globSync(["**/*.md", "**/*.mdx"], {
558
+ cwd: contentRoot,
559
+ ignore,
560
+ dot: false,
561
+ absolute: false
562
+ }).map(toPosix);
563
+ const metaFiles = globSync(["**/meta.json"], { cwd: contentRoot, ignore, dot: false, absolute: false }).map(toPosix);
564
+ const { routes, errors, warnings } = resolveRoutes(contentFiles);
565
+ const pages = [];
566
+ for (const route of routes) {
567
+ const absPath = join(contentRoot, route.file);
568
+ let data;
569
+ try {
570
+ data = parseFrontmatter(readFileSync(absPath, "utf8"), route.file).data;
571
+ } catch (error) {
572
+ errors.push(error instanceof Error ? error.message : String(error));
573
+ continue;
574
+ }
575
+ if (data.draft === true && options.includeDrafts !== true) continue;
576
+ pages.push({ ...route, absPath, data: { ...data, title: titleFor(route, data, options.siteTitle) } });
577
+ }
578
+ const files = pages.map((page) => ({
579
+ type: "page",
580
+ path: page.file,
581
+ absolutePath: page.absPath,
582
+ // Our slugs, not fumadocs' — one algorithm decides URLs, anchors and the index.
583
+ slugs: page.slugs,
584
+ data: page.data
585
+ }));
586
+ const metaDirs = /* @__PURE__ */ new Set();
587
+ for (const file of metaFiles) {
588
+ const absPath = join(contentRoot, file);
589
+ try {
590
+ const parsed = metaSchema.safeParse(JSON.parse(readFileSync(absPath, "utf8")));
591
+ if (!parsed.success) {
592
+ errors.push(
593
+ `Invalid ${file}:
594
+ ${parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n")}`
595
+ );
596
+ continue;
597
+ }
598
+ metaDirs.add(dirname2(file));
599
+ files.push({ type: "meta", path: file, absolutePath: absPath, data: parsed.data });
600
+ } catch (error) {
601
+ errors.push(`Invalid ${file}: ${error instanceof Error ? error.message : String(error)}`);
602
+ }
603
+ }
604
+ files.push(...synthesiseOrderMeta(pages, metaDirs));
605
+ return { files, pages, errors, warnings };
606
+ }
607
+ function synthesiseOrderMeta(pages, metaDirs) {
608
+ const byDir = /* @__PURE__ */ new Map();
609
+ for (const page of pages) {
610
+ const dir = dirname2(page.file);
611
+ const bucket = byDir.get(dir);
612
+ if (bucket) bucket.push(page);
613
+ else byDir.set(dir, [page]);
614
+ }
615
+ const out = [];
616
+ for (const [dir, dirPages] of byDir) {
617
+ if (metaDirs.has(dir)) continue;
618
+ if (!dirPages.some((p) => typeof p.data.order === "number" || p.isIndex)) continue;
619
+ const ordered = [...dirPages].sort(compareForOrder).map((p) => basename(p.file).replace(/\.mdx?$/i, ""));
620
+ out.push({
621
+ type: "meta",
622
+ path: dir === "." ? "meta.json" : `${dir}/meta.json`,
623
+ data: { pages: [...ordered, "..."] }
624
+ });
625
+ }
626
+ return out;
627
+ }
628
+ function compareForOrder(a, b) {
629
+ const ao = orderOf(a);
630
+ const bo = orderOf(b);
631
+ if (ao !== bo) return ao - bo;
632
+ return a.data.title.localeCompare(b.data.title);
633
+ }
634
+ function orderOf(page) {
635
+ if (typeof page.data.order === "number") return page.data.order;
636
+ return page.isIndex ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
637
+ }
638
+ function titleFor(route, data, siteTitle) {
639
+ if (typeof data.title === "string" && data.title !== "") return data.title;
640
+ if (route.url === "/") return siteTitle ?? "Home";
641
+ return humanise(route.slugs[route.slugs.length - 1] ?? "Untitled");
642
+ }
643
+ function humanise(slug) {
644
+ return slug.split("-").filter((word) => word !== "").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
645
+ }
646
+
647
+ // src/node/content/source.ts
648
+ function createSource(options) {
649
+ let cached;
650
+ const read = () => cached ??= scan(options);
651
+ const loader = dynamicLoader(
652
+ {
653
+ cache: "custom",
654
+ files: () => read().files,
655
+ invalidate: () => {
656
+ cached = void 0;
657
+ }
658
+ },
659
+ { baseUrl: "/" }
660
+ );
661
+ return {
662
+ loader,
663
+ current: read,
664
+ pages: () => read().pages,
665
+ refresh() {
666
+ loader.invalidate();
667
+ return read();
668
+ },
669
+ async getPageTree() {
670
+ const output = await loader.get();
671
+ return output.getPageTree();
672
+ },
673
+ async serializeTree() {
674
+ const output = await loader.get();
675
+ return output.serializePageTree(output.getPageTree());
676
+ }
677
+ };
678
+ }
679
+
680
+ // src/node/report.ts
681
+ import pc from "picocolors";
682
+ function createWarningCollector() {
683
+ const seen = /* @__PURE__ */ new Set();
684
+ return {
685
+ add(message) {
686
+ seen.add(message);
687
+ },
688
+ list: () => [...seen],
689
+ clear: () => seen.clear(),
690
+ flush(log = console.warn) {
691
+ const messages = [...seen].sort();
692
+ seen.clear();
693
+ if (messages.length === 0) return 0;
694
+ log("");
695
+ log(pc.yellow(`${messages.length} warning${messages.length === 1 ? "" : "s"}:`));
696
+ for (const message of messages) log(pc.yellow(` - ${message}`));
697
+ log("");
698
+ return messages.length;
699
+ }
700
+ };
701
+ }
702
+
703
+ // src/node/context.ts
704
+ function createContext(options) {
705
+ const { config, contentRoot } = options;
706
+ const source = createSource({
707
+ contentRoot,
708
+ exclude: config.exclude,
709
+ siteTitle: config.title,
710
+ includeDrafts: options.includeDrafts
711
+ });
712
+ let resolver = createLinkResolver(source.pages(), config.base);
713
+ return {
714
+ config,
715
+ contentRoot,
716
+ source,
717
+ warnings: createWarningCollector(),
718
+ pages: () => source.pages(),
719
+ resolver: () => resolver,
720
+ errors: () => source.current().errors,
721
+ refresh() {
722
+ const result = source.refresh();
723
+ resolver = createLinkResolver(result.pages, config.base);
724
+ return result;
725
+ }
726
+ };
727
+ }
728
+
729
+ // src/cli/build.ts
730
+ init_paths();
731
+
732
+ // src/node/prerender/emit.ts
733
+ import { mkdirSync, writeFileSync } from "fs";
734
+ import { dirname as dirname4, join as join3 } from "path";
735
+ function outputPathFor(url) {
736
+ const clean = url.replace(/^\/+|\/+$/g, "");
737
+ return clean === "" ? "index.html" : join3(clean, "index.html");
738
+ }
739
+ function writeHtml(outDir, relativePath, html) {
740
+ const target = join3(outDir, relativePath);
741
+ mkdirSync(dirname4(target), { recursive: true });
742
+ writeFileSync(target, html, "utf8");
743
+ }
744
+ function applyTemplate(template, { html, head }) {
745
+ return template.replace("<!--seemore-head-->", () => head).replace("<!--seemore-app-->", () => html);
746
+ }
747
+
748
+ // src/node/prerender/deploy.ts
749
+ import { writeFileSync as writeFileSync2 } from "fs";
750
+ import { join as join4 } from "path";
751
+ function writeDeployArtifacts(outDir, base, shell) {
752
+ const prefix = base === "/" ? "" : base.replace(/\/+$/, "");
753
+ writeFileSync2(join4(outDir, "_redirects"), `${prefix}/* ${prefix}/index.html 200
754
+ `, "utf8");
755
+ writeFileSync2(join4(outDir, "200.html"), shell, "utf8");
756
+ writeFileSync2(join4(outDir, ".nojekyll"), "", "utf8");
757
+ }
758
+
759
+ // src/node/prerender/render.ts
760
+ import { pathToFileURL } from "url";
761
+ import { join as join6 } from "path";
762
+ import { build } from "vite";
763
+
764
+ // src/node/vite/config.ts
765
+ init_paths();
766
+ import { realpathSync as realpathSync2 } from "fs";
767
+ import { join as join5 } from "path";
768
+ import react from "@vitejs/plugin-react";
769
+ import tailwindcss from "@tailwindcss/vite";
770
+ import mdx from "@mdx-js/rollup";
771
+
772
+ // src/node/vite/mdx.ts
773
+ import remarkFrontmatter from "remark-frontmatter";
774
+ import {
775
+ rehypeCode,
776
+ rehypeToc,
777
+ remarkAdmonition,
778
+ remarkDirectiveAdmonition,
779
+ remarkGfm,
780
+ remarkHeading,
781
+ remarkImage,
782
+ remarkMdxMermaid,
783
+ remarkSteps
784
+ } from "fumadocs-core/mdx-plugins";
785
+
786
+ // src/node/vite/remark.ts
787
+ import { existsSync as existsSync3 } from "fs";
788
+ import { dirname as dirname5, relative, resolve as resolve4 } from "path";
789
+ import { visit } from "unist-util-visit";
790
+ var WIKILINK = /\[\[([^\]\n]+)\]\]/g;
791
+ var ALERTS = {
792
+ NOTE: { type: "info", title: "Note" },
793
+ TIP: { type: "idea", title: "Tip" },
794
+ IMPORTANT: { type: "info", title: "Important" },
795
+ WARNING: { type: "warn", title: "Warning" },
796
+ CAUTION: { type: "error", title: "Caution" }
797
+ };
798
+ var ALERT_MARKER = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/;
799
+ function remarkSeemoreAlerts() {
800
+ return (tree) => {
801
+ visit(tree, "blockquote", (node, index, parent) => {
802
+ if (parent === void 0 || index === void 0) return;
803
+ const first = node.children[0];
804
+ if (first === void 0 || first.type !== "paragraph") return;
805
+ const marker = ALERT_MARKER.exec(textOf(first));
806
+ const alert = marker === null ? void 0 : ALERTS[marker[1] ?? ""];
807
+ if (marker === void 0 || marker === null || alert === void 0) return;
808
+ stripMarker(first, marker[0]);
809
+ parent.children[index] = {
810
+ type: "mdxJsxFlowElement",
811
+ name: "Callout",
812
+ attributes: [
813
+ { type: "mdxJsxAttribute", name: "type", value: alert.type },
814
+ { type: "mdxJsxAttribute", name: "title", value: alert.title }
815
+ ],
816
+ children: node.children
817
+ };
818
+ });
819
+ };
820
+ }
821
+ function textOf(paragraph) {
822
+ const first = paragraph.children[0];
823
+ return first !== void 0 && first.type === "text" ? first.value.trimStart() : "";
824
+ }
825
+ function stripMarker(paragraph, marker) {
826
+ const first = paragraph.children[0];
827
+ if (first === void 0 || first.type !== "text") return;
828
+ first.value = first.value.trimStart().slice(marker.length).replace(/^\n/, "");
829
+ if (first.value === "") paragraph.children.shift();
830
+ if (paragraph.children[0]?.type === "break") paragraph.children.shift();
831
+ }
832
+ function remarkSeemoreWikilinks(options) {
833
+ return (tree, file) => {
834
+ const from = virtualPath(options.contentRoot, file);
835
+ const resolver = options.getResolver();
836
+ visit(tree, "text", (node, index, parent) => {
837
+ if (parent === void 0 || index === void 0) return;
838
+ if (!node.value.includes("[[")) return;
839
+ const replacement = [];
840
+ let cursor = 0;
841
+ WIKILINK.lastIndex = 0;
842
+ for (let match = WIKILINK.exec(node.value); match !== null; match = WIKILINK.exec(node.value)) {
843
+ const target = match[1] ?? "";
844
+ if (match.index > cursor) {
845
+ replacement.push({ type: "text", value: node.value.slice(cursor, match.index) });
846
+ }
847
+ cursor = match.index + match[0].length;
848
+ const resolved = resolver.resolveWikilink(target, from);
849
+ if (resolved.warning !== void 0) options.onWarning(resolved.warning);
850
+ if (resolved.href === void 0) {
851
+ replacement.push({
852
+ type: "mdxJsxTextElement",
853
+ name: "span",
854
+ attributes: [
855
+ { type: "mdxJsxAttribute", name: "className", value: "seemore-broken-wikilink" },
856
+ { type: "mdxJsxAttribute", name: "title", value: "Unresolved link" }
857
+ ],
858
+ children: [{ type: "text", value: resolved.label }]
859
+ });
860
+ } else {
861
+ replacement.push({
862
+ type: "link",
863
+ url: resolved.href,
864
+ children: [{ type: "text", value: resolved.label }]
865
+ });
866
+ }
867
+ }
868
+ if (replacement.length === 0) return;
869
+ if (cursor < node.value.length) replacement.push({ type: "text", value: node.value.slice(cursor) });
870
+ parent.children.splice(index, 1, ...replacement);
871
+ return index + replacement.length;
872
+ });
873
+ };
874
+ }
875
+ function remarkSeemoreAssets(options) {
876
+ return (tree, file) => {
877
+ if (typeof file.path !== "string" || file.path === "") return;
878
+ const dir = dirname5(file.path);
879
+ const from = virtualPath(options.contentRoot, file);
880
+ visit(tree, "image", (node, index, parent) => {
881
+ if (parent === void 0 || index === void 0) return;
882
+ if (isExternal(node.url) || node.url.startsWith("/")) return;
883
+ const target = resolve4(dir, decodeURIComponent(node.url.split(/[?#]/)[0] ?? ""));
884
+ if (existsSync3(target)) return;
885
+ options.onWarning(`Missing asset ${node.url} referenced by ${from}.`);
886
+ parent.children.splice(index, 1, {
887
+ type: "mdxJsxTextElement",
888
+ name: "img",
889
+ attributes: [
890
+ { type: "mdxJsxAttribute", name: "src", value: node.url },
891
+ { type: "mdxJsxAttribute", name: "alt", value: node.alt ?? "" },
892
+ { type: "mdxJsxAttribute", name: "data-seemore-missing", value: "true" }
893
+ ],
894
+ children: []
895
+ });
896
+ });
897
+ };
898
+ }
899
+ function isExternal(url) {
900
+ return /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(url);
901
+ }
902
+ function remarkSeemoreLinks(options) {
903
+ return (tree, file) => {
904
+ const from = virtualPath(options.contentRoot, file);
905
+ const resolver = options.getResolver();
906
+ const rewrite = (node) => {
907
+ const resolved = resolver.resolveHref(node.url, from);
908
+ if (resolved.warning !== void 0) options.onWarning(resolved.warning);
909
+ node.url = resolved.href;
910
+ };
911
+ visit(tree, "link", rewrite);
912
+ visit(tree, "definition", rewrite);
913
+ };
914
+ }
915
+ function virtualPath(contentRoot, file) {
916
+ if (typeof file.path !== "string" || file.path === "") return "";
917
+ return toPosix(relative(contentRoot, file.path));
918
+ }
919
+
920
+ // src/node/vite/mdx.ts
921
+ function createRemarkPlugins(options) {
922
+ return [
923
+ // Strips the `---` block so it never renders. Its data already came from the scan.
924
+ [remarkFrontmatter, ["yaml"]],
925
+ remarkGfm,
926
+ remarkHeading,
927
+ remarkAdmonition,
928
+ remarkDirectiveAdmonition,
929
+ // After the fumadocs admonition plugins, which handle `:::note`, and before anything that
930
+ // rewrites link or text nodes inside the quote.
931
+ remarkSeemoreAlerts,
932
+ remarkSteps,
933
+ // Before `remark-image`: a reference to a file that is not there becomes a warning and a
934
+ // visibly broken image, rather than a failed build.
935
+ () => remarkSeemoreAssets(options),
936
+ [
937
+ remarkImage,
938
+ {
939
+ onError: (error) => {
940
+ options.onWarning(error.message);
941
+ }
942
+ }
943
+ ],
944
+ // Rewrites ```mermaid fences to <Mermaid chart="…" />. We supply the component.
945
+ remarkMdxMermaid,
946
+ () => remarkSeemoreWikilinks(options),
947
+ () => remarkSeemoreLinks(options)
948
+ ];
949
+ }
950
+ function createRehypePlugins() {
951
+ return [rehypeCode, rehypeToc];
952
+ }
953
+
954
+ // src/node/vite/plugin.ts
955
+ import { readFileSync as readFileSync3 } from "fs";
956
+ import { createRequire } from "module";
957
+ import { dirname as dirname6 } from "path";
958
+
959
+ // src/node/search/build.ts
960
+ import { readFileSync as readFileSync2 } from "fs";
961
+ import { gzipSync } from "zlib";
962
+ import { createFromSource } from "fumadocs-core/search/server";
963
+ import { structure } from "fumadocs-core/mdx-plugins";
964
+ var SIZE_WARNING_BYTES = 15e5;
965
+ async function buildSearchIndex(ctx) {
966
+ const loader = await ctx.source.loader.get();
967
+ const bodies = /* @__PURE__ */ new Map();
968
+ for (const page of ctx.pages()) {
969
+ try {
970
+ bodies.set(page.url, parseFrontmatter(readFileSync2(page.absPath, "utf8"), page.file).content);
971
+ } catch {
972
+ }
973
+ }
974
+ const server = createFromSource(loader, {
975
+ buildIndex(page) {
976
+ const body = bodies.get(page.url) ?? "";
977
+ return {
978
+ id: page.url,
979
+ url: withBase(ctx.config.base, page.url),
980
+ title: typeof page.data.title === "string" ? page.data.title : page.url,
981
+ description: typeof page.data.description === "string" ? page.data.description : void 0,
982
+ structuredData: structure(body)
983
+ };
984
+ }
985
+ });
986
+ const response = await server.staticGET();
987
+ return await response.text();
988
+ }
989
+ function measureIndex(json2) {
990
+ const bytes = Buffer.byteLength(json2);
991
+ const gzipped = gzipSync(json2).byteLength;
992
+ if (gzipped <= SIZE_WARNING_BYTES) return { bytes, gzipped };
993
+ return {
994
+ bytes,
995
+ gzipped,
996
+ warning: `The static search index is ${formatBytes(gzipped)} gzipped, which every visitor downloads before their first search. Above ${formatBytes(SIZE_WARNING_BYTES)} consider a hosted index: set \`search: { provider: 'orama-cloud', \u2026 }\` or \`search: { provider: 'algolia', \u2026 }\` in seemore.config.ts.`
997
+ };
998
+ }
999
+ function formatBytes(bytes) {
1000
+ if (bytes < 1024) return `${bytes} B`;
1001
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
1002
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
1003
+ }
1004
+
1005
+ // src/node/vite/plugin.ts
1006
+ var VIRTUAL = {
1007
+ tree: "virtual:seemore/tree",
1008
+ routes: "virtual:seemore/routes",
1009
+ config: "virtual:seemore/config"
1010
+ };
1011
+ var PAGE_PREFIX = "seemore-page:";
1012
+ var resolvedId = (id) => `\0${id}`;
1013
+ var IMPORTS_MARKER = /\/\* seemore:imports[\s\S]*?\*\//;
1014
+ var USER_CSS_MARKER = /\/\* seemore:user-css[\s\S]*?\*\//;
1015
+ var require_ = createRequire(import.meta.url);
1016
+ function styleImports(ctx) {
1017
+ const lines = [`@import 'fumadocs-ui/css/${ctx.config.theme}.css';`];
1018
+ try {
1019
+ lines.push(`@source '${dirname6(require_.resolve("fumadocs-ui/package.json"))}/dist';`);
1020
+ } catch {
1021
+ }
1022
+ return lines.join("\n");
1023
+ }
1024
+ function userCss(ctx) {
1025
+ if (ctx.config.css === void 0) return "";
1026
+ const css = readIfExists(ctx.config.css);
1027
+ if (css === void 0) {
1028
+ ctx.warnings.add(`The stylesheet named by \`css\` was not found: ${ctx.config.css}`);
1029
+ return "";
1030
+ }
1031
+ return `/* ${ctx.config.css} */
1032
+ ${css}`;
1033
+ }
1034
+ function seemorePlugin({ ctx, serveSearch = false }) {
1035
+ let server;
1036
+ return {
1037
+ name: "seemore",
1038
+ enforce: "pre",
1039
+ resolveId(id) {
1040
+ if (id.startsWith(PAGE_PREFIX)) return id.slice(PAGE_PREFIX.length).replace(/\\/g, "/");
1041
+ for (const virtualId of Object.values(VIRTUAL)) {
1042
+ if (id === virtualId) return resolvedId(virtualId);
1043
+ }
1044
+ return void 0;
1045
+ },
1046
+ /**
1047
+ * The theme preset, the paths Tailwind must scan, and the user's own stylesheet are
1048
+ * injected into our root stylesheet rather than imported from it.
1049
+ *
1050
+ * Tailwind v4 only processes the file that contains `@import "tailwindcss"`, and bare
1051
+ * specifiers in a virtual stylesheet have no directory to resolve from — injecting into
1052
+ * the real `globals.css` keeps both working.
1053
+ */
1054
+ transform(code, id) {
1055
+ const path = id.replace(/\\/g, "/").split("?")[0] ?? "";
1056
+ if (!path.endsWith("/src/app/styles/globals.css")) return void 0;
1057
+ const transformed = code.replace(IMPORTS_MARKER, () => styleImports(ctx)).replace(USER_CSS_MARKER, () => userCss(ctx));
1058
+ return { code: transformed, map: null };
1059
+ },
1060
+ async load(id) {
1061
+ if (id === resolvedId(VIRTUAL.tree)) {
1062
+ return hotStoreModule("Tree", json(await ctx.source.serializeTree()));
1063
+ }
1064
+ if (id === resolvedId(VIRTUAL.routes)) {
1065
+ return hotStoreModule("Routes", renderRoutesValue(ctx));
1066
+ }
1067
+ if (id === resolvedId(VIRTUAL.config)) return `export const config = ${json(clientConfig(ctx))};`;
1068
+ return void 0;
1069
+ },
1070
+ configureServer(devServer) {
1071
+ server = devServer;
1072
+ if (!serveSearch) return;
1073
+ devServer.middlewares.use(async (req, res, next) => {
1074
+ const path = (req.url ?? "").split("?")[0] ?? "";
1075
+ if (path !== withBase(ctx.config.base, "/api/search.json") && path !== "/api/search.json") return next();
1076
+ try {
1077
+ const index = await buildSearchIndex(ctx);
1078
+ res.setHeader("Content-Type", "application/json");
1079
+ res.end(index);
1080
+ } catch (error) {
1081
+ next(error);
1082
+ }
1083
+ });
1084
+ },
1085
+ /** Called by the watcher after a rescan. */
1086
+ api: {
1087
+ invalidate() {
1088
+ if (server === void 0) return;
1089
+ for (const virtualId of [VIRTUAL.tree, VIRTUAL.routes]) {
1090
+ const mod = server.moduleGraph.getModuleById(resolvedId(virtualId));
1091
+ if (mod) server.moduleGraph.invalidateModule(mod);
1092
+ }
1093
+ server.ws.send({ type: "update", updates: [] });
1094
+ }
1095
+ }
1096
+ };
1097
+ }
1098
+ function hotStoreModule(suffix, value) {
1099
+ return `const state = import.meta.hot
1100
+ ? (import.meta.hot.data.seemore${suffix} ||= { listeners: new Set() })
1101
+ : { listeners: new Set() };
1102
+
1103
+ state.value = ${value};
1104
+
1105
+ export function get${suffix}() {
1106
+ return state.value;
1107
+ }
1108
+
1109
+ export function subscribe${suffix}(listener) {
1110
+ state.listeners.add(listener);
1111
+ return () => {
1112
+ state.listeners.delete(listener);
1113
+ };
1114
+ }
1115
+
1116
+ if (import.meta.hot) {
1117
+ import.meta.hot.accept();
1118
+ for (const listener of state.listeners) listener();
1119
+ }
1120
+ `;
1121
+ }
1122
+ function renderRoutesValue(ctx) {
1123
+ const entries = ctx.pages().map((page) => {
1124
+ const specifier = `${PAGE_PREFIX}${page.absPath.replace(/\\/g, "/")}`;
1125
+ return [
1126
+ " {",
1127
+ ` url: ${json(page.url)},`,
1128
+ ` file: ${json(page.file)},`,
1129
+ ` absPath: ${json(page.absPath)},`,
1130
+ ` title: ${json(page.data.title)},`,
1131
+ ` description: ${json(page.data.description ?? null)},`,
1132
+ ` load: () => import(${json(specifier)}),`,
1133
+ " },"
1134
+ ].join("\n");
1135
+ });
1136
+ return `[
1137
+ ${entries.join("\n")}
1138
+ ]`;
1139
+ }
1140
+ function clientConfig(ctx) {
1141
+ const { config } = ctx;
1142
+ return {
1143
+ title: config.title,
1144
+ description: config.description,
1145
+ base: config.base,
1146
+ theme: config.theme,
1147
+ features: config.features,
1148
+ nav: config.nav,
1149
+ footer: config.footer,
1150
+ editLink: config.editLink,
1151
+ favicon: config.favicon === void 0 ? void 0 : withBase(config.base, `/${toPosix(config.favicon)}`),
1152
+ search: config.search.provider === "static" ? { provider: "static", from: withBase(config.base, "/api/search.json") } : config.search,
1153
+ contentRoot: config.root
1154
+ };
1155
+ }
1156
+ function json(value) {
1157
+ return JSON.stringify(value ?? null).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1158
+ }
1159
+ function readIfExists(path) {
1160
+ try {
1161
+ return readFileSync3(path, "utf8");
1162
+ } catch {
1163
+ return void 0;
1164
+ }
1165
+ }
1166
+
1167
+ // src/node/vite/watcher.ts
1168
+ import chokidar from "chokidar";
1169
+ var CONTENT_FILE = /\.(?:mdx?|json)$/i;
1170
+ function seemoreWatcherPlugin(ctx) {
1171
+ let watcher;
1172
+ return {
1173
+ name: "seemore:watcher",
1174
+ apply: "serve",
1175
+ configureServer(server) {
1176
+ watcher = chokidar.watch(ctx.contentRoot, {
1177
+ ignoreInitial: true,
1178
+ ignored: (path, stats) => {
1179
+ if (path === ctx.config.configFile) return false;
1180
+ const posix = path.replace(/\\/g, "/");
1181
+ if (posix.includes("/node_modules/") || posix.includes("/.git/")) return true;
1182
+ return stats?.isFile() === true && !CONTENT_FILE.test(posix);
1183
+ }
1184
+ });
1185
+ const onEvent = (event, path) => {
1186
+ void handleContentChange(server, ctx, event, path);
1187
+ };
1188
+ watcher.on("add", (p) => onEvent("add", p));
1189
+ watcher.on("change", (p) => onEvent("change", p));
1190
+ watcher.on("unlink", (p) => onEvent("unlink", p));
1191
+ watcher.on("addDir", (p) => onEvent("addDir", p));
1192
+ watcher.on("unlinkDir", (p) => onEvent("unlinkDir", p));
1193
+ if (ctx.config.configFile !== void 0) {
1194
+ watcher.add(ctx.config.configFile);
1195
+ watcher.on("change", (path) => {
1196
+ if (path !== ctx.config.configFile) return;
1197
+ server.environments.client.hot.send({ type: "full-reload", path: "*" });
1198
+ server.config.logger.info(
1199
+ "seemore config changed \u2014 reloading. Changes to `base` need a restart to take effect."
1200
+ );
1201
+ });
1202
+ }
1203
+ server.httpServer?.once("close", () => void watcher?.close());
1204
+ },
1205
+ async closeBundle() {
1206
+ await watcher?.close();
1207
+ watcher = void 0;
1208
+ }
1209
+ };
1210
+ }
1211
+ async function handleContentChange(server, ctx, event, path) {
1212
+ const scan2 = ctx.refresh();
1213
+ for (const message of [...scan2.errors, ...scan2.warnings]) ctx.warnings.add(message);
1214
+ ctx.warnings.flush((line) => server.config.logger.warn(line));
1215
+ await reloadVirtual(server, VIRTUAL.tree);
1216
+ await reloadVirtual(server, VIRTUAL.routes);
1217
+ if (event === "change" && /\.mdx?$/i.test(path)) {
1218
+ await reloadFile(server, path);
1219
+ }
1220
+ }
1221
+ async function reloadVirtual(server, id) {
1222
+ await reloadById(server, `\0${id}`);
1223
+ }
1224
+ async function reloadFile(server, absolutePath) {
1225
+ await reloadById(server, absolutePath.replace(/\\/g, "/"));
1226
+ }
1227
+ async function reloadById(server, id) {
1228
+ const environments = Object.values(server.environments ?? {});
1229
+ if (environments.length === 0) {
1230
+ const legacy = server.moduleGraph.getModuleById(id);
1231
+ if (legacy) await server.reloadModule(legacy);
1232
+ return;
1233
+ }
1234
+ for (const environment of environments) {
1235
+ const mod = environment.moduleGraph?.getModuleById(id);
1236
+ if (mod === void 0 || mod === null) continue;
1237
+ if (typeof environment.reloadModule === "function") await environment.reloadModule(mod);
1238
+ else environment.moduleGraph.invalidateModule(mod);
1239
+ }
1240
+ }
1241
+
1242
+ // src/node/vite/config.ts
1243
+ function createViteConfig({ ctx, mode, outDir, ssrOutDir }) {
1244
+ const root = appRoot();
1245
+ const isSsr = ssrOutDir !== void 0;
1246
+ const mdxOptions = {
1247
+ // `format` is inferred per file, so a plain `.md` never needs MDX syntax.
1248
+ remarkPlugins: createRemarkPlugins({
1249
+ contentRoot: ctx.contentRoot,
1250
+ getResolver: () => ctx.resolver(),
1251
+ onWarning: (message) => ctx.warnings.add(message)
1252
+ }),
1253
+ rehypePlugins: createRehypePlugins(),
1254
+ // MDX compiles its own JSX. Vite's builtin transform infers a file's language from its
1255
+ // extension and does not know `.md`/`.mdx`, so leaving JSX in the output would fail to
1256
+ // parse. Fast Refresh is unaffected: it is a separate transform, applied to these files
1257
+ // through the React plugin's `include` below, which is what turns a content edit into an
1258
+ // in-place component swap rather than a reload.
1259
+ jsx: false
1260
+ };
1261
+ return {
1262
+ root,
1263
+ base: ctx.config.base,
1264
+ cacheDir: cacheDir(ctx.contentRoot),
1265
+ configFile: false,
1266
+ envDir: false,
1267
+ clearScreen: false,
1268
+ logLevel: mode === "build" ? "warn" : "info",
1269
+ plugins: [
1270
+ // Order matters: MDX first, then React, so JSX from MDX is transformed and refreshed.
1271
+ { ...mdx(mdxOptions), enforce: "pre" },
1272
+ react({ include: /\.(?:mdx?|jsx?|tsx?)$/ }),
1273
+ // Before Tailwind: our plugin injects the theme preset into the root stylesheet, and
1274
+ // Tailwind must see the injected version.
1275
+ seemorePlugin({ ctx, serveSearch: mode === "dev" }),
1276
+ tailwindcss(),
1277
+ ...mode === "dev" ? [seemoreWatcherPlugin(ctx)] : []
1278
+ ],
1279
+ // Vite bundles workers with the browser export condition, but a worker has no `document`.
1280
+ // `decode-named-character-reference` — pulled in through fumadocs' search client, via
1281
+ // remark — calls `document.createElement` at module scope in its browser build, so the
1282
+ // search worker threw on load. The package ships a DOM-free `worker` entry; use it.
1283
+ worker: { plugins: () => [workerConditionPlugin()] },
1284
+ resolve: {
1285
+ // The app is compiled from seemore's own sources, so its dependencies must resolve
1286
+ // from seemore's directory rather than from the user's project.
1287
+ dedupe: ["react", "react-dom", "react-router", "fumadocs-core", "fumadocs-ui"]
1288
+ },
1289
+ server: {
1290
+ fs: {
1291
+ // The content root is normally *outside* the Vite root, and files outside `allow`
1292
+ // 404 silently — the single most likely cause of "the watcher does nothing".
1293
+ allow: withRealPaths([root, packageRoot(), ctx.contentRoot, ctx.config.root, process.cwd()])
1294
+ },
1295
+ watch: {
1296
+ // Only real exclusions here. Vite merges these into chokidar's ignore *list*, where a
1297
+ // leading `!` is a negated matcher that matches everything it is not — so the obvious
1298
+ // `!<contentRoot>/**` "re-include" would silently ignore the entire project instead.
1299
+ // Content outside the Vite root is watched by seemore's own chokidar instance.
1300
+ ignored: ["**/node_modules/**", "**/.git/**"]
1301
+ }
1302
+ },
1303
+ build: isSsr ? {
1304
+ ssr: join5(root, "entry.prerender.tsx"),
1305
+ outDir: ssrOutDir,
1306
+ emptyOutDir: true,
1307
+ copyPublicDir: false,
1308
+ minify: false,
1309
+ rollupOptions: { output: { entryFileNames: "entry.prerender.js" } }
1310
+ } : {
1311
+ outDir,
1312
+ emptyOutDir: true,
1313
+ rollupOptions: { input: join5(root, "index.html") },
1314
+ // The app bundle is seemore's own, not the user's; warning them about a size they
1315
+ // cannot act on is noise.
1316
+ chunkSizeWarningLimit: 2e3
1317
+ },
1318
+ // The prerender bundle is written to a scratch directory outside any `node_modules`, so
1319
+ // it has to be self-contained: an externalised `react` there resolves against the scratch
1320
+ // directory and is simply not found.
1321
+ ssr: isSsr ? { noExternal: true } : void 0
1322
+ };
1323
+ }
1324
+ function withRealPaths(paths) {
1325
+ const out = /* @__PURE__ */ new Set();
1326
+ for (const path of paths) {
1327
+ out.add(path);
1328
+ try {
1329
+ out.add(realpathSync2.native(path));
1330
+ } catch {
1331
+ }
1332
+ }
1333
+ return [...out];
1334
+ }
1335
+ function workerConditionPlugin() {
1336
+ return {
1337
+ name: "seemore:worker-conditions",
1338
+ enforce: "pre",
1339
+ async resolveId(source, importer, options) {
1340
+ if (!WORKER_SAFE_ENTRIES.has(source)) return void 0;
1341
+ const resolved = await this.resolve(source, importer, options);
1342
+ if (resolved === null) return void 0;
1343
+ const domFree = resolved.id.replace(/index\.dom\.js$/, "index.js");
1344
+ return domFree === resolved.id ? resolved : { ...resolved, id: domFree };
1345
+ }
1346
+ };
1347
+ }
1348
+ var WORKER_SAFE_ENTRIES = /* @__PURE__ */ new Set(["decode-named-character-reference"]);
1349
+
1350
+ // src/node/prerender/render.ts
1351
+ async function loadPrerenderModule(ctx, ssrOutDir) {
1352
+ await build(createViteConfig({ ctx, mode: "build", ssrOutDir }));
1353
+ const entry = join6(ssrOutDir, "entry.prerender.js");
1354
+ const loaded = await import(pathToFileURL(entry).href);
1355
+ if (typeof loaded.render !== "function" || typeof loaded.listRoutes !== "function") {
1356
+ throw new Error(`seemore: the prerender build at ${entry} did not export \`render\` and \`listRoutes\`.`);
1357
+ }
1358
+ return { render: loaded.render, listRoutes: loaded.listRoutes };
1359
+ }
1360
+
1361
+ // src/node/social/cards.ts
1362
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1363
+ import { dirname as dirname7, join as join7 } from "path";
1364
+
1365
+ // src/shared/og.ts
1366
+ function ogImagePath(url) {
1367
+ const clean = url.replace(/^\/+|\/+$/g, "");
1368
+ return clean === "" ? "/api/og/card.png" : `/api/og/${clean}/card.png`;
1369
+ }
1370
+
1371
+ // src/node/social/cards.ts
1372
+ async function generateSocialCards(ctx, outDir) {
1373
+ let takumi;
1374
+ try {
1375
+ takumi = await importOptional("takumi-js");
1376
+ } catch {
1377
+ ctx.warnings.add(
1378
+ "`social.cards` is enabled but `takumi-js` is not installed. Run `npm install takumi-js`, or remove the flag."
1379
+ );
1380
+ return 0;
1381
+ }
1382
+ let written = 0;
1383
+ for (const page of ctx.pages()) {
1384
+ const png = await renderCard(takumi, ctx.config.title, page.data.title, page.data.description);
1385
+ if (png === void 0) continue;
1386
+ const target = join7(outDir, ogImagePath(page.url));
1387
+ mkdirSync2(dirname7(target), { recursive: true });
1388
+ writeFileSync3(target, png);
1389
+ written++;
1390
+ }
1391
+ return written;
1392
+ }
1393
+ async function renderCard(takumi, site, title, description) {
1394
+ const renderer = new takumi.Renderer({ fonts: [] });
1395
+ const node = takumi.container(
1396
+ {
1397
+ style: {
1398
+ width: 1200,
1399
+ height: 630,
1400
+ display: "flex",
1401
+ flexDirection: "column",
1402
+ justifyContent: "center",
1403
+ padding: 80,
1404
+ backgroundColor: "#0b0b0b",
1405
+ color: "#ffffff",
1406
+ gap: 24
1407
+ }
1408
+ },
1409
+ [
1410
+ takumi.text(site, { style: { fontSize: 28, opacity: 0.6 } }),
1411
+ takumi.text(title, { style: { fontSize: 64, fontWeight: 700 } }),
1412
+ ...typeof description === "string" ? [takumi.text(description, { style: { fontSize: 30, opacity: 0.8 } })] : []
1413
+ ]
1414
+ );
1415
+ return await renderer.renderAsync(node, { width: 1200, height: 630, format: "png" });
1416
+ }
1417
+ async function importOptional(specifier) {
1418
+ return await import(specifier);
1419
+ }
1420
+
1421
+ // src/cli/build.ts
1422
+ async function runBuild(options) {
1423
+ const contentRoot = resolveContentRoot(options.cwd, options.dir);
1424
+ const loaded = await loadConfig({ root: options.cwd, configPath: options.configPath });
1425
+ const config = { ...loaded.config, base: options.base === void 0 ? loaded.config.base : normaliseBase(options.base) };
1426
+ warnAboutMissingBase(config.base, loaded.file);
1427
+ const ctx = createContext({ config, contentRoot });
1428
+ const outDir = resolve5(options.cwd, options.outDir ?? "dist");
1429
+ assertSafeOutDir(outDir, options.cwd, contentRoot);
1430
+ const scan2 = ctx.source.current();
1431
+ failOnErrors(ctx.errors(), contentRoot);
1432
+ if (scan2.pages.length === 0) {
1433
+ throw new Error(`No Markdown files found under ${contentRoot}. Point seemore at a folder that has some, or check \`exclude\`.`);
1434
+ }
1435
+ for (const warning of scan2.warnings) ctx.warnings.add(warning);
1436
+ console.log(pc2.dim(`seemore ${scan2.pages.length} pages from ${relative2(options.cwd, contentRoot) || "."}`));
1437
+ await viteBuild(createViteConfig({ ctx, mode: "build", outDir }));
1438
+ const template = readFileSync4(join8(outDir, "index.html"), "utf8");
1439
+ const ssrOutDir = mkdtempSync(join8(tmpdir2(), "seemore-ssr-"));
1440
+ try {
1441
+ const prerender = await loadPrerenderModule(ctx, ssrOutDir);
1442
+ const urls = prerender.listRoutes();
1443
+ for (const url of urls) {
1444
+ writeHtml(outDir, outputPathFor(url), applyTemplate(template, await prerender.render(url)));
1445
+ }
1446
+ const notFound = applyTemplate(template, await prerender.render("/__seemore_not_found"));
1447
+ writeHtml(outDir, "404.html", notFound);
1448
+ writeDeployArtifacts(outDir, config.base, notFound);
1449
+ if (config.search.provider === "static") {
1450
+ const index = await buildSearchIndex(ctx);
1451
+ mkdirSync3(join8(outDir, "api"), { recursive: true });
1452
+ writeFileSync4(join8(outDir, "api", "search.json"), index, "utf8");
1453
+ const size = measureIndex(index);
1454
+ console.log(pc2.dim(`seemore search index ${formatBytes(size.gzipped)} gzipped`));
1455
+ if (size.warning !== void 0) ctx.warnings.add(size.warning);
1456
+ }
1457
+ if (config.search.provider !== "static") await warnIfSearchSdkMissing(ctx, config.search.provider);
1458
+ if (config.features["social.cards"]) await generateSocialCards(ctx, outDir);
1459
+ ctx.warnings.flush();
1460
+ console.log(pc2.green(`seemore ${urls.length} pages written to ${relative2(options.cwd, outDir) || outDir}`));
1461
+ return { outDir, routes: urls.length };
1462
+ } finally {
1463
+ rmSync(ssrOutDir, { recursive: true, force: true });
1464
+ }
1465
+ }
1466
+ function assertSafeOutDir(outDir, cwd, contentRoot) {
1467
+ const contains = (parent, child) => {
1468
+ const rel = relative2(parent, child);
1469
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
1470
+ };
1471
+ for (const [name, dir] of [
1472
+ ["the current directory", cwd],
1473
+ ["the content directory", contentRoot]
1474
+ ]) {
1475
+ if (contains(outDir, dir)) {
1476
+ throw new Error(
1477
+ `Refusing to build into ${outDir}: it is, or contains, ${name}, and the build empties its output directory first. Pass --out with a directory of its own.`
1478
+ );
1479
+ }
1480
+ }
1481
+ }
1482
+ async function warnIfSearchSdkMissing(ctx, provider) {
1483
+ const packageName = provider === "algolia" ? "algoliasearch" : "@orama/core";
1484
+ try {
1485
+ createRequire2(join8(ctx.config.root, "noop.js")).resolve(packageName);
1486
+ } catch {
1487
+ ctx.warnings.add(
1488
+ `\`search.provider\` is '${provider}', which needs ${packageName}. Run \`npm install ${packageName}\` or search will find nothing.`
1489
+ );
1490
+ }
1491
+ }
1492
+ function failOnErrors(errors, contentRoot) {
1493
+ if (errors.length === 0) return;
1494
+ throw new Error(`seemore found ${errors.length} problem(s) in ${contentRoot}:
1495
+
1496
+ ${errors.join("\n\n")}`);
1497
+ }
1498
+ function warnAboutMissingBase(base, configFile) {
1499
+ if (base !== "/" || process.env.GITHUB_ACTIONS !== "true") return;
1500
+ const repo = process.env.GITHUB_REPOSITORY?.split("/")[1];
1501
+ console.warn(
1502
+ pc2.yellow(
1503
+ `seemore \`base\` is not set, and GitHub Pages serves project sites from a subpath.
1504
+ Add this to ${configFile ?? "seemore.config.ts"}:
1505
+
1506
+ base: '/${repo ?? "your-repo"}/',
1507
+
1508
+ Or pass --base '/${repo ?? "your-repo"}/'. Ignore this if you deploy to a domain root.`
1509
+ )
1510
+ );
1511
+ }
1512
+
1513
+ // src/cli/dev.ts
1514
+ import { createServer } from "vite";
1515
+ import pc3 from "picocolors";
1516
+ init_paths();
1517
+ var DEFAULT_PORT = 4040;
1518
+ async function runDev(options) {
1519
+ const contentRoot = resolveContentRoot(options.cwd, options.dir);
1520
+ const loaded = await loadConfig({ root: options.cwd, configPath: options.configPath });
1521
+ const config = {
1522
+ ...loaded.config,
1523
+ base: options.base === void 0 ? loaded.config.base : normaliseBase(options.base)
1524
+ };
1525
+ const ctx = createContext({ config, contentRoot, includeDrafts: true });
1526
+ const scan2 = ctx.source.current();
1527
+ for (const message of [...scan2.errors, ...scan2.warnings]) ctx.warnings.add(message);
1528
+ if (scan2.pages.length === 0) {
1529
+ ctx.warnings.add(`No Markdown files found under ${contentRoot}. seemore will serve an empty site until there are.`);
1530
+ }
1531
+ const base = createViteConfig({ ctx, mode: "dev" });
1532
+ const server = await createServer({
1533
+ ...base,
1534
+ server: {
1535
+ ...base.server,
1536
+ port: options.port ?? DEFAULT_PORT,
1537
+ host: options.host,
1538
+ open: options.open === true ? config.base : false
1539
+ }
1540
+ });
1541
+ await server.listen();
1542
+ const resolvedPort = server.config.server.port ?? DEFAULT_PORT;
1543
+ const url = `http://localhost:${resolvedPort}${config.base}`;
1544
+ ctx.warnings.flush();
1545
+ console.log(`
1546
+ ${pc3.green("seemore")} ${pc3.bold(url)}`);
1547
+ console.log(` ${pc3.dim(`${scan2.pages.length} pages from ${contentRoot}`)}
1548
+ `);
1549
+ return {
1550
+ server,
1551
+ ctx,
1552
+ url,
1553
+ close: async () => {
1554
+ await server.close();
1555
+ }
1556
+ };
1557
+ }
1558
+
1559
+ // src/cli/index.ts
1560
+ var USAGE = `
1561
+ ${pc4.bold("seemore")} \u2014 turn a folder of Markdown into a docs site
1562
+
1563
+ seemore [dir] start the dev server
1564
+ seemore build [dir] build a static site into dist/
1565
+
1566
+ Options
1567
+ --port <number> dev server port (default 4040)
1568
+ --host [host] expose the dev server on the network
1569
+ --open / --no-open open a browser on start (default: no)
1570
+ --config <path> path to seemore.config.ts
1571
+ --out <dir> build output directory (default: dist)
1572
+ --base <path> subpath the site is served from, e.g. /my-repo/
1573
+ -h, --help show this message
1574
+ -v, --version show the version
1575
+ `;
1576
+ function normaliseHostFlag(argv) {
1577
+ const index = argv.indexOf("--host");
1578
+ if (index === -1) return argv;
1579
+ const next = argv[index + 1];
1580
+ if (next !== void 0 && !next.startsWith("-")) return argv;
1581
+ return [...argv.slice(0, index), "--host=", ...argv.slice(index + 1)];
1582
+ }
1583
+ async function main(argv = process.argv.slice(2)) {
1584
+ const { values, positionals } = parseArgs({
1585
+ args: normaliseHostFlag(argv),
1586
+ allowPositionals: true,
1587
+ options: {
1588
+ port: { type: "string" },
1589
+ host: { type: "string" },
1590
+ open: { type: "boolean" },
1591
+ "no-open": { type: "boolean" },
1592
+ config: { type: "string" },
1593
+ out: { type: "string" },
1594
+ base: { type: "string" },
1595
+ help: { type: "boolean", short: "h" },
1596
+ version: { type: "boolean", short: "v" }
1597
+ }
1598
+ });
1599
+ if (values.help === true) {
1600
+ console.log(USAGE);
1601
+ return;
1602
+ }
1603
+ if (values.version === true) {
1604
+ const { readFileSync: readFileSync5 } = await import("fs");
1605
+ const { join: join9 } = await import("path");
1606
+ const { packageRoot: packageRoot2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
1607
+ const pkg = JSON.parse(readFileSync5(join9(packageRoot2(), "package.json"), "utf8"));
1608
+ console.log(pkg.version);
1609
+ return;
1610
+ }
1611
+ const [command, ...rest] = positionals;
1612
+ const isBuild = command === "build";
1613
+ const dir = isBuild ? rest[0] : command;
1614
+ const shared = { cwd: process.cwd(), dir, configPath: values.config, base: values.base };
1615
+ if (isBuild) {
1616
+ await runBuild({ ...shared, outDir: values.out });
1617
+ return;
1618
+ }
1619
+ await runDev({
1620
+ ...shared,
1621
+ port: values.port === void 0 ? void 0 : Number(values.port),
1622
+ host: values.host === void 0 ? void 0 : values.host === "" ? true : values.host,
1623
+ open: values.open === true && values["no-open"] !== true
1624
+ });
1625
+ }
1626
+ main().catch((error) => {
1627
+ console.error(`
1628
+ ${pc4.red("seemore")} ${error instanceof Error ? error.message : String(error)}
1629
+ `);
1630
+ process.exitCode = 1;
1631
+ });
1632
+ export {
1633
+ main
1634
+ };
1635
+ //# sourceMappingURL=index.js.map