yaakdoc 0.1.0 → 0.2.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.
@@ -1,537 +1,23 @@
1
+ import { a as matchRoute, c as VERSION, h as resolveFrom, i as writeSite, m as ENGINE_DIR, p as ENGINE_ANCHOR, r as withBase, s as resolveContext, t as assertSafeOutDir, u as resolveTheme } from "./site-X7q76qYf.js";
1
2
  import { createRequire } from "node:module";
2
- import { builtinSnippets, generateExamples } from "@yaakdoc/snippets";
3
3
  import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
- import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
- import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { absoluteUrl, escapeRegExp, findWorkspace, isUrl, normalize, prepareDocument, sanitizeExport, selectEnvironment, summarize } from "@yaakdoc/core";
4
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
5
+ import { pathToFileURL } from "node:url";
7
6
  import { build, createServer, isRunnableDevEnvironment, searchForWorkspaceRoot } from "vite";
8
7
  import { createHash } from "node:crypto";
9
- import { createReadStream, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
+ import { createReadStream, existsSync, readFileSync, readdirSync } from "node:fs";
10
9
  import { loadConfig } from "@yaakdoc/config/node";
11
- import { detectExport, readExport } from "@yaakdoc/core/node";
12
- import { folderToMarkdown, indexToMarkdown, llmsFullTxt, llmsTxt, requestToMarkdown } from "@yaakdoc/markdown";
13
- import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
14
- import { createJiti } from "jiti";
15
- import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
16
- import rehypeStringify from "rehype-stringify";
17
- import remarkGfm from "remark-gfm";
18
- import remarkParse from "remark-parse";
19
- import remarkRehype from "remark-rehype";
20
- import { bundledLanguages, createHighlighter } from "shiki";
21
- import { unified } from "unified";
10
+ import { escapeRegExp } from "@yaakdoc/core";
11
+ import { detectExport } from "@yaakdoc/core/node";
22
12
  import { tmpdir } from "node:os";
23
13
  import { createServer as createServer$1 } from "node:http";
24
14
  import { consola } from "consola";
25
15
  import colors from "picocolors";
26
16
  import { glob } from "tinyglobby";
27
17
  import sirv from "sirv";
28
- //#region src/node/render.ts
29
- const PRELOADED_LANGUAGES = [
30
- "bash",
31
- "javascript",
32
- "typescript",
33
- "python",
34
- "go",
35
- "json",
36
- "graphql",
37
- "xml",
38
- "html",
39
- "yaml",
40
- "http"
41
- ];
42
- /** Highlights a delimited region as GraphQL (opening captures: prefix tokens, then the delimiter). */
43
- const embeddedGraphql = (begin, end, prefixScopes) => ({
44
- begin,
45
- beginCaptures: Object.fromEntries(prefixScopes.map((name, index) => [index + 1, { name }])),
46
- end,
47
- endCaptures: { 0: { name: prefixScopes.at(-1) } },
48
- contentName: "meta.embedded.block.graphql",
49
- patterns: [{ include: "source.graphql" }]
50
- });
51
- /**
52
- * Injection grammar highlighting GraphQL inside code examples (like VS Code's GraphQL extension):
53
- * `/* GraphQL *\/ \`…\`` and `gql\`…\`` in JavaScript, `query = """…"""` in Python and
54
- * `query := \`…\`` in Go.
55
- */
56
- const GRAPHQL_INJECTION = {
57
- name: "yaakdoc-graphql-injection",
58
- scopeName: "inline.graphql.yaakdoc",
59
- injectTo: [
60
- "source.js",
61
- "source.ts",
62
- "source.python",
63
- "source.go"
64
- ],
65
- injectionSelector: "L:source -comment -string",
66
- embeddedLangs: ["graphql"],
67
- patterns: [
68
- embeddedGraphql("(/\\*\\s*GraphQL\\s*\\*/)\\s*(`)", "`", ["comment.block.js", "punctuation.definition.string.template.begin.js"]),
69
- embeddedGraphql("\\b(gql|graphql)(`)", "`", ["entity.name.function.tagged-template.js", "punctuation.definition.string.template.begin.js"]),
70
- embeddedGraphql("\\b(query)\\s*(=)\\s*(\"\"\")", "\"\"\"", [
71
- "variable.other.python",
72
- "keyword.operator.assignment.python",
73
- "punctuation.definition.string.begin.python"
74
- ]),
75
- embeddedGraphql("\\b(query)\\s*(:=)\\s*(`)", "`", [
76
- "variable.other.go",
77
- "keyword.operator.assignment.go",
78
- "punctuation.definition.string.begin.go"
79
- ])
80
- ]
81
- };
82
- const highlighters = /* @__PURE__ */ new Map();
83
- function getHighlighter(themes) {
84
- const key = `${themes.light}|${themes.dark}`;
85
- let highlighter = highlighters.get(key);
86
- if (!highlighter) {
87
- highlighter = createHighlighter({
88
- themes: [themes.light, themes.dark],
89
- langs: [...PRELOADED_LANGUAGES, GRAPHQL_INJECTION]
90
- });
91
- highlighters.set(key, highlighter);
92
- }
93
- return highlighter;
94
- }
95
- const LANGUAGE_ALIASES = {
96
- shell: "bash",
97
- sh: "bash",
98
- js: "javascript",
99
- ts: "typescript",
100
- text: "text",
101
- plain: "text"
102
- };
103
- async function ensureLanguage(highlighter, language) {
104
- const lang = LANGUAGE_ALIASES[language] ?? language;
105
- if (lang === "text" || highlighter.getLoadedLanguages().includes(lang)) return lang;
106
- if (lang in bundledLanguages) {
107
- await highlighter.loadLanguage(lang);
108
- return lang;
109
- }
110
- return "text";
111
- }
112
- const sanitizeSchema = {
113
- ...defaultSchema,
114
- attributes: {
115
- ...defaultSchema.attributes,
116
- code: [...defaultSchema.attributes?.code ?? [], ["className", /^language-./]]
117
- }
118
- };
119
- /**
120
- * Creates the Markdown/code renderer: GFM, sanitized HTML (descriptions come from exports that may
121
- * be shared by others), and Shiki highlighting with dual light/dark themes via CSS variables.
122
- */
123
- async function createRenderer(themes) {
124
- const highlighter = await getHighlighter(themes);
125
- const shikiOptions = {
126
- themes,
127
- defaultColor: false
128
- };
129
- const processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeSanitize, sanitizeSchema).use(rehypeShikiFromHighlighter, highlighter, {
130
- ...shikiOptions,
131
- fallbackLanguage: "text",
132
- lazy: true
133
- }).use(rehypeStringify);
134
- return {
135
- markdown: async (source) => String(await processor.process(source)),
136
- highlight: async (code, language) => highlighter.codeToHtml(code, {
137
- ...shikiOptions,
138
- lang: await ensureLanguage(highlighter, language)
139
- })
140
- };
141
- }
142
- /** Loads snippet generators by built-in id or module path/package (default export or `snippet`). */
143
- async function loadSnippets(ids, root) {
144
- const jiti = createJiti(`${root}/`, { interopDefault: true });
145
- const snippets = [];
146
- for (const id of ids) {
147
- const builtin = builtinSnippets[id];
148
- if (builtin) {
149
- snippets.push(builtin);
150
- continue;
151
- }
152
- if (!existsSync(id) && !id.includes("/") && !id.startsWith("@")) throw new Error(`Unknown snippet "${id}". Built-in snippets: ${Object.keys(builtinSnippets).join(", ")}.`);
153
- const mod = await jiti.import(id);
154
- const loaded = "default" in mod ? mod.default : mod;
155
- for (const snippet of Array.isArray(loaded) ? loaded : [loaded]) {
156
- if (!snippet || typeof snippet.generate !== "function") throw new Error(`Snippet module "${id}" must export a snippet created with defineSnippet().`);
157
- snippets.push(snippet);
158
- }
159
- }
160
- return snippets;
161
- }
162
- //#endregion
163
- //#region src/node/resolve.ts
164
- /** Directory of the installed `yaakdoc` package. */
165
- const ENGINE_DIR = (() => {
166
- let dir = dirname(fileURLToPath(import.meta.url));
167
- while (!existsSync(join(dir, "package.json"))) dir = dirname(dir);
168
- return dir;
169
- })();
170
- /** A file inside the engine package, used as importer for fallback resolution. */
171
- const ENGINE_ANCHOR = join(ENGINE_DIR, "package.json");
172
- /**
173
- * Resolves an ESM specifier as if imported from `fromFile`. Returns an absolute path or null.
174
- */
175
- function resolveFrom(specifier, fromFile) {
176
- try {
177
- const resolved = createJiti(fromFile).esmResolve(specifier, { try: true });
178
- return resolved ? resolved.startsWith("file:") ? fileURLToPath(resolved) : resolved : null;
179
- } catch {
180
- return null;
181
- }
182
- }
183
- /** Resolves a package directory (the folder containing its package.json). */
184
- function resolvePackageDir(name, fromFile) {
185
- const entry = resolveFrom(`${name}/package.json`, fromFile);
186
- if (entry) return dirname(entry);
187
- const main = resolveFrom(name, fromFile);
188
- if (!main) return null;
189
- let dir = dirname(main);
190
- while (dir !== dirname(dir)) {
191
- if (existsSync(join(dir, "package.json")) && dir.endsWith(name.split("/").at(-1))) return dir;
192
- dir = dirname(dir);
193
- }
194
- return null;
195
- }
196
- //#endregion
197
- //#region src/node/theme.ts
198
- var ThemeError = class extends Error {
199
- name = "ThemeError";
200
- };
201
- const MANIFEST_NAMES = [
202
- "theme.ts",
203
- "theme.mts",
204
- "theme.js",
205
- "theme.mjs",
206
- "index.ts",
207
- "index.js"
208
- ];
209
- /** Candidate package names for a theme spec: `kiseki` → `@yaakdoc/theme-kiseki`, `yaakdoc-theme-kiseki`. */
210
- function themePackageCandidates(spec) {
211
- if (spec.startsWith("@") || spec.includes("/") || spec.startsWith("yaakdoc-theme-")) return [spec];
212
- return [
213
- `@yaakdoc/theme-${spec}`,
214
- `yaakdoc-theme-${spec}`,
215
- spec
216
- ];
217
- }
218
- const isLocal = (spec) => spec.startsWith(".") || isAbsolute(spec);
219
- function findManifest(path) {
220
- if (existsSync(path) && statSync(path).isFile()) return path;
221
- for (const name of MANIFEST_NAMES) {
222
- const candidate = join(path, name);
223
- if (existsSync(candidate)) return candidate;
224
- }
225
- throw new ThemeError(`No theme manifest (${MANIFEST_NAMES.join(", ")}) found in ${path}`);
226
- }
227
- /** Built-in theme package directories, used to resolve dependencies of ejected themes. */
228
- function builtinFallbacks() {
229
- const kiseki = resolvePackageDir("@yaakdoc/theme-kiseki", ENGINE_ANCHOR);
230
- return [ENGINE_ANCHOR, ...kiseki ? [join(kiseki, "package.json")] : []];
231
- }
232
- /** Loads and validates a theme from a short name, package name or local path. */
233
- async function resolveTheme(spec, options) {
234
- const userAnchor = join(options.root, "package.json");
235
- let manifest = null;
236
- let packageName = null;
237
- if (isLocal(spec)) manifest = findManifest(resolve(options.root, spec));
238
- else {
239
- for (const candidate of themePackageCandidates(spec)) {
240
- const resolved = resolveFrom(candidate, userAnchor) ?? resolveFrom(candidate, ENGINE_ANCHOR);
241
- if (resolved) {
242
- manifest = resolved;
243
- packageName = candidate;
244
- break;
245
- }
246
- }
247
- if (!manifest) throw new ThemeError(`Theme "${spec}" not found. Install it (e.g. npm i -D ${themePackageCandidates(spec)[0]}) or use a local path.`);
248
- }
249
- const fallbackImporters = builtinFallbacks();
250
- const alias = buildAliases(manifest, fallbackImporters);
251
- const jiti = createJiti(manifest, {
252
- alias,
253
- moduleCache: false,
254
- interopDefault: true
255
- });
256
- let definition;
257
- try {
258
- definition = await jiti.import(manifest, { default: true });
259
- } catch (error) {
260
- throw new ThemeError(`Failed to load theme ${manifest}: ${error.message}`);
261
- }
262
- if (!definition || typeof definition !== "object" || !definition.entry) throw new ThemeError(`Theme ${manifest} must default-export defineTheme({ name, entry, ... }).`);
263
- const root = dirname(manifest);
264
- const abs = (path) => resolve(root, path);
265
- let themeOptions = options.themeOptions;
266
- if (definition.options) try {
267
- themeOptions = definition.options.parse(options.themeOptions);
268
- } catch (error) {
269
- throw new ThemeError(`Invalid themeOptions for theme "${definition.name}":\n${error.message}`);
270
- }
271
- return {
272
- name: definition.name,
273
- definition,
274
- root,
275
- manifest,
276
- entry: abs(definition.entry),
277
- client: definition.client ? abs(definition.client) : null,
278
- styles: (definition.styles ?? []).map(abs),
279
- options: themeOptions,
280
- packageName,
281
- fallbackImporters,
282
- highlight: definition.highlight ?? {
283
- light: "github-light",
284
- dark: "github-dark"
285
- }
286
- };
287
- }
288
- /**
289
- * jiti aliases so a theme manifest (e.g. an ejected theme in a project without dependencies) can
290
- * import `@yaakdoc/react/theme` and the built-in themes' dependencies.
291
- */
292
- function buildAliases(manifest, fallbacks) {
293
- const specs = /* @__PURE__ */ new Set(["@yaakdoc/react/theme", "@yaakdoc/react"]);
294
- for (const fallback of fallbacks) try {
295
- const pkg = JSON.parse(readFileSync(fallback, "utf8"));
296
- for (const name of Object.keys(pkg.dependencies ?? {})) specs.add(name);
297
- } catch {}
298
- const alias = {};
299
- for (const spec of specs) {
300
- if (resolveFrom(spec, manifest)) continue;
301
- for (const fallback of fallbacks) {
302
- const resolved = resolveFrom(spec, fallback);
303
- if (resolved) {
304
- alias[spec] = resolved;
305
- break;
306
- }
307
- }
308
- }
309
- return alias;
310
- }
311
- //#endregion
312
- //#region src/node/context.ts
313
- const VERSION = JSON.parse(readFileSync(resolve(ENGINE_DIR, "package.json"), "utf8")).version;
314
- /** Loads config, export and theme, and prepares everything needed to render the site. */
315
- async function resolveContext(options = {}) {
316
- const cwd = resolve(options.cwd ?? process.cwd());
317
- const { config, configFile, root } = await loadConfig({
318
- cwd,
319
- ...options.configFile ? { configFile: options.configFile } : {},
320
- ...options.overrides ? { overrides: options.overrides } : {}
321
- });
322
- const exportPath = config.export ?? await detectExport(root);
323
- const rawExport = await readExport(exportPath);
324
- const source = normalize(rawExport);
325
- const workspace = findWorkspace(source, config.workspace);
326
- const environments = config.environments === "public" ? workspace.environments.filter((env) => env.public) : (config.environments ?? []).map((name) => selectEnvironment(workspace, name));
327
- const environment = config.environment === false ? null : config.environment === void 0 && environments[0] ? environments[0] : selectEnvironment(workspace, config.environment);
328
- const theme = await resolveTheme(config.theme, {
329
- root,
330
- themeOptions: config.themeOptions
331
- });
332
- const renderer = await createRenderer(theme.highlight);
333
- const snippets = await loadSnippets(config.snippets, root);
334
- const document = await prepareDocument({
335
- workspace,
336
- environment,
337
- environments,
338
- pages: config.pages,
339
- base: config.base,
340
- variables: config.variables,
341
- filter: config.filter,
342
- renderMarkdown: renderer.markdown,
343
- highlight: renderer.highlight,
344
- examples: (request) => generateExamples(request, snippets),
345
- meta: {
346
- yaakVersion: source.yaakVersion,
347
- exportedAt: source.exportedAt
348
- }
349
- });
350
- const assets = /* @__PURE__ */ new Map();
351
- const asset = (path) => publicAsset(path, config.base, assets);
352
- const site = await createSiteData(config, document, theme, asset, renderer.markdown);
353
- const { routes, notFound } = createRoutes(document, site);
354
- const markdownOptions = {
355
- title: site.title,
356
- description: site.description,
357
- base: config.base,
358
- ...site.url ? { siteUrl: site.url } : {}
359
- };
360
- const textFiles = /* @__PURE__ */ new Map();
361
- if (config.llms.enabled) {
362
- textFiles.set(`${config.base}index.md`, config.pages === "single" ? llmsFullTxt(document, markdownOptions) : indexToMarkdown(document, markdownOptions));
363
- for (const folder of Object.values(document.folders)) textFiles.set(folder.markdownUrl, folderToMarkdown(document, folder, markdownOptions));
364
- for (const request of Object.values(document.requests)) textFiles.set(request.markdownUrl, requestToMarkdown(request, markdownOptions));
365
- textFiles.set(`${config.base}llms.txt`, llmsTxt(document, {
366
- ...markdownOptions,
367
- ...config.llms.full ? { fullTextUrl: `${config.base}llms-full.txt` } : {}
368
- }));
369
- if (config.llms.full) textFiles.set(`${config.base}llms-full.txt`, llmsFullTxt(document, markdownOptions));
370
- }
371
- const externalExport = typeof config.runInYaak === "object" && config.runInYaak.url;
372
- if (site.runInYaak && !externalExport) {
373
- const shared = shareableExport(rawExport, workspace, document, config.variables.hide);
374
- textFiles.set(site.runInYaak.dataUrl, JSON.stringify(shared, null, 2));
375
- }
376
- return {
377
- cwd,
378
- root,
379
- configFile,
380
- config,
381
- exportPath,
382
- source,
383
- theme,
384
- document,
385
- site,
386
- routes,
387
- notFound,
388
- textFiles,
389
- assets,
390
- watchFiles: [
391
- exportPath,
392
- ...configFile ? [configFile] : [],
393
- ...assets.values()
394
- ]
395
- };
396
- }
397
- /** The export restricted to what the docs show, safe to publish for "Run in Yaak". */
398
- function shareableExport(raw, workspace, document, hide) {
399
- const environmentIds = new Set(document.environments.map((env) => env.id));
400
- if (document.environment) environmentIds.add(document.environment.id);
401
- if (workspace.baseEnvironment) environmentIds.add(workspace.baseEnvironment.id);
402
- for (const folderId of Object.keys(document.folders)) for (const env of workspace.folderEnvironments[folderId] ?? []) environmentIds.add(env.id);
403
- return sanitizeExport(raw, {
404
- workspaceId: workspace.id,
405
- folderIds: new Set(Object.keys(document.folders)),
406
- requestIds: new Set(Object.keys(document.requests)),
407
- environmentIds,
408
- hide
409
- });
410
- }
411
- /**
412
- * Maps a local asset (logo, favicon) to a public URL under `<base>_yaakdoc/`. URLs and site-root
413
- * paths that don't exist on disk (e.g. `/logo.svg` served from `public/`) are kept as is.
414
- */
415
- function publicAsset(path, base, assets) {
416
- if (!path) return null;
417
- if (isUrl(path) || !existsSync(path)) return path;
418
- const hash = createHash("sha256").update(path).digest("hex").slice(0, 8);
419
- const url = `${base}_yaakdoc/${basename(path, extname(path))}-${hash}${extname(path)}`;
420
- assets.set(url, path);
421
- return url;
422
- }
423
- async function createSiteData(config, document, theme, asset, markdown) {
424
- const { branding } = config;
425
- const title = branding.title ?? document.workspace.name;
426
- let logo = null;
427
- if (typeof branding.logo === "string") logo = {
428
- light: asset(branding.logo) ?? void 0,
429
- alt: title,
430
- href: config.base
431
- };
432
- else if (branding.logo) logo = {
433
- ...branding.logo.light ? { light: asset(branding.logo.light) } : {},
434
- ...branding.logo.dark ? { dark: asset(branding.logo.dark) } : {},
435
- alt: branding.logo.alt ?? title,
436
- href: branding.logo.href ?? config.base,
437
- ...branding.logo.width ? { width: branding.logo.width } : {},
438
- ...branding.logo.height ? { height: branding.logo.height } : {}
439
- };
440
- return {
441
- title,
442
- description: branding.description ?? summarize(document.workspace.description, 200),
443
- url: branding.url ?? null,
444
- base: config.base,
445
- pages: config.pages,
446
- logo,
447
- favicon: asset(branding.favicon),
448
- colors: branding.colors ?? {},
449
- fonts: branding.fonts ?? {},
450
- links: branding.links,
451
- footerHtml: branding.footer ? await markdown(branding.footer) : null,
452
- defaultColorMode: branding.defaultColorMode,
453
- colorModeToggle: branding.colorModeToggle,
454
- llms: {
455
- ...config.llms,
456
- txtUrl: config.llms.enabled ? `${config.base}llms.txt` : null,
457
- fullTxtUrl: config.llms.enabled && config.llms.full ? `${config.base}llms-full.txt` : null
458
- },
459
- showVariables: config.variables.show,
460
- runInYaak: config.runInYaak ? {
461
- name: typeof config.runInYaak === "object" && config.runInYaak.name || title,
462
- dataUrl: typeof config.runInYaak === "object" && config.runInYaak.url || `${config.base}yaak.json`
463
- } : null,
464
- head: config.head,
465
- themeOptions: theme.options,
466
- generator: `yaakdoc ${VERSION}`
467
- };
468
- }
469
- function createRoutes(document, site) {
470
- const { base } = site;
471
- const llms = site.llms.enabled;
472
- const routes = [{
473
- path: base,
474
- file: "index.html",
475
- page: {
476
- kind: "index",
477
- id: null,
478
- path: base,
479
- title: "",
480
- description: site.description,
481
- markdownUrl: llms ? `${base}index.md` : null
482
- }
483
- }];
484
- if (site.pages === "per-request") {
485
- for (const folder of Object.values(document.folders)) routes.push({
486
- path: folder.href,
487
- file: `${folder.slug}/index.html`,
488
- page: {
489
- kind: "folder",
490
- id: folder.id,
491
- path: folder.href,
492
- title: folder.name,
493
- description: folder.summary,
494
- markdownUrl: llms ? folder.markdownUrl : null
495
- }
496
- });
497
- for (const request of Object.values(document.requests)) routes.push({
498
- path: request.href,
499
- file: `${request.slug}/index.html`,
500
- page: {
501
- kind: "request",
502
- id: request.id,
503
- path: request.href,
504
- title: request.name,
505
- description: request.summary,
506
- markdownUrl: llms ? request.markdownUrl : null
507
- }
508
- });
509
- }
510
- return {
511
- routes,
512
- notFound: {
513
- path: `${base}404.html`,
514
- file: "404.html",
515
- page: {
516
- kind: "not-found",
517
- id: null,
518
- path: `${base}404.html`,
519
- title: "Page not found",
520
- description: "",
521
- markdownUrl: null
522
- }
523
- }
524
- };
525
- }
526
- /** Finds the route for a URL path (ignoring query/hash; `/x` matches `/x/`). */
527
- function matchRoute(context, pathname) {
528
- const path = decodeURIComponent(pathname.split(/[?#]/)[0] ?? "/");
529
- const normalized = path.endsWith("/") || path.endsWith(".html") ? path : `${path}/`;
530
- return context.routes.find((route) => route.path === normalized || `${route.path}index.html` === path) ?? null;
531
- }
532
- //#endregion
533
18
  //#region src/node/plugin.ts
534
19
  const VIRTUAL_SERVER = "virtual:yaakdoc/entry-server";
20
+ /** The SSR entry exports `render` (see `RenderEntry`). */
535
21
  const VIRTUAL_CLIENT = "virtual:yaakdoc/entry-client";
536
22
  const RESOLVED = (id) => `\0${id}`;
537
23
  const isBare = (id) => !id.startsWith(".") && !id.startsWith("/") && !id.startsWith("\0") && !/^[a-z]+:/i.test(id) && !id.startsWith("@/");
@@ -742,8 +228,33 @@ async function build$1(options = {}) {
742
228
  async function buildWithContext(context) {
743
229
  const outDir = context.config.outDir;
744
230
  assertSafeOutDir(outDir, context.root);
231
+ const { assets, ssrEntry } = await bundleTheme(context, outDir);
232
+ const { render } = await import(pathToFileURL(ssrEntry).href);
233
+ const files = await writeSite(context, {
234
+ render,
235
+ assets: withBase(assets, context.config.base)
236
+ });
237
+ await rm(dirname(ssrEntry), {
238
+ recursive: true,
239
+ force: true
240
+ });
241
+ return {
242
+ outDir,
243
+ files,
244
+ context
245
+ };
246
+ }
247
+ /**
248
+ * Builds the theme with Vite: the island runtime and CSS into `outDir` (emptied first), and the SSR
249
+ * entry, fully bundled so it runs from anywhere. Pass `base: "./"` for client assets that work
250
+ * under any base path (used to precompile the theme bundled into the Yaak plugin).
251
+ */
252
+ async function bundleTheme(context, outDir, options = {}) {
745
253
  const getContext = () => context;
746
- const base = await createViteConfig(getContext, "production");
254
+ const base = {
255
+ ...await createViteConfig(getContext, "production"),
256
+ ...options.base ? { base: options.base } : {}
257
+ };
747
258
  await build({
748
259
  ...base,
749
260
  build: {
@@ -782,45 +293,18 @@ async function buildWithContext(context) {
782
293
  }
783
294
  }
784
295
  });
785
- const assets = assetsFromManifest(JSON.parse(await readFile(join(outDir, ".vite", "manifest.json"), "utf8")), context.config.base);
786
- const { render } = await import(pathToFileURL(join(ssrOutDir, "entry-server.mjs")).href);
787
- const files = [];
788
- const write = async (file, content) => {
789
- const target = join(outDir, file);
790
- await mkdir(dirname(target), { recursive: true });
791
- await writeFile(target, content);
792
- files.push(file);
793
- };
794
- for (const route of [...context.routes, context.notFound]) await write(route.file, await render({
795
- document: context.document,
796
- site: context.site,
797
- page: route.page,
798
- assets
799
- }));
800
- for (const [url, content] of context.textFiles) await write(stripBase(url, context.config.base), content);
801
- for (const [url, source] of context.assets) {
802
- const file = stripBase(url, context.config.base);
803
- await mkdir(dirname(join(outDir, file)), { recursive: true });
804
- await copyFile(source, join(outDir, file));
805
- files.push(file);
806
- }
807
- if (context.site.url) await write("sitemap.xml", sitemap(context));
808
- await write(".nojekyll", "");
809
- await rm(ssrOutDir, {
810
- recursive: true,
811
- force: true
812
- });
296
+ const manifest = JSON.parse(await readFile(join(outDir, ".vite", "manifest.json"), "utf8"));
813
297
  await rm(join(outDir, ".vite"), {
814
298
  recursive: true,
815
299
  force: true
816
300
  });
817
301
  return {
818
- outDir,
819
- files,
820
- context
302
+ assets: assetsFromManifest(manifest),
303
+ ssrEntry: join(ssrOutDir, "entry-server.mjs")
821
304
  };
822
305
  }
823
- function assetsFromManifest(manifest, base) {
306
+ /** Client assets of the build, relative to the output directory. */
307
+ function assetsFromManifest(manifest) {
824
308
  const entry = Object.values(manifest).find((chunk) => chunk.isEntry);
825
309
  if (!entry) throw new Error("Client build produced no entry chunk.");
826
310
  const collectImports = (keys = [], seen = /* @__PURE__ */ new Set()) => keys.flatMap((key) => {
@@ -832,21 +316,12 @@ function assetsFromManifest(manifest, base) {
832
316
  const css = /* @__PURE__ */ new Set([...entry.css ?? []]);
833
317
  for (const key of entry.imports ?? []) for (const file of manifest[key]?.css ?? []) css.add(file);
834
318
  return {
835
- styles: [...css].map((file) => base + file),
836
- scripts: [base + entry.file],
319
+ styles: [...css],
320
+ scripts: [entry.file],
837
321
  inlineStyles: [],
838
- modulePreloads: collectImports(entry.imports).map((file) => base + file)
322
+ modulePreloads: collectImports(entry.imports)
839
323
  };
840
324
  }
841
- const stripBase = (url, base) => url.startsWith(base) ? url.slice(base.length) : url.replace(/^\//, "");
842
- function sitemap(context) {
843
- return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${context.routes.map((route) => absoluteUrl(context.site.url, context.config.base, route.path)).map((url) => ` <url><loc>${url}</loc></url>`).join("\n")}\n</urlset>\n`;
844
- }
845
- /** Refuses to empty the project directory (or a parent of it). */
846
- function assertSafeOutDir(outDir, root) {
847
- const rel = relative(resolve(outDir), resolve(root));
848
- if (rel === "" || !rel.startsWith("..") && !rel.startsWith(sep) && !rel.includes(":")) throw new Error(`Refusing to use ${outDir} as outDir: it contains the project directory and would be emptied.`);
849
- }
850
325
  //#endregion
851
326
  //#region src/node/server.ts
852
327
  const CONTENT_TYPES = {
@@ -1181,4 +656,4 @@ async function preview(options = {}) {
1181
656
  };
1182
657
  }
1183
658
  //#endregion
1184
- export { createDevServer as a, VIRTUAL_SERVER as c, matchRoute as d, resolveContext as f, themePackageCandidates as h, schemaUrl as i, yaakdocPlugin as l, resolveTheme as m, eject as n, build$1 as o, ThemeError as p, init as r, VIRTUAL_CLIENT as s, preview as t, VERSION as u };
659
+ export { createDevServer as a, VIRTUAL_CLIENT as c, schemaUrl as i, VIRTUAL_SERVER as l, eject as n, build$1 as o, init as r, bundleTheme as s, preview as t, yaakdocPlugin as u };