yaakdoc 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -2
- package/bin/yaakdoc.js +4 -0
- package/config.schema.json +446 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +313 -0
- package/dist/index.d.ts +177 -0
- package/dist/index.js +5 -0
- package/dist/open-C4Bh1af9.js +19 -0
- package/dist/preview-2YFamRKo.js +1184 -0
- package/package.json +87 -3
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { builtinSnippets, generateExamples } from "@yaakdoc/snippets";
|
|
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";
|
|
7
|
+
import { build, createServer, isRunnableDevEnvironment, searchForWorkspaceRoot } from "vite";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { createReadStream, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
10
|
+
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";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { createServer as createServer$1 } from "node:http";
|
|
24
|
+
import { consola } from "consola";
|
|
25
|
+
import colors from "picocolors";
|
|
26
|
+
import { glob } from "tinyglobby";
|
|
27
|
+
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
|
+
//#region src/node/plugin.ts
|
|
534
|
+
const VIRTUAL_SERVER = "virtual:yaakdoc/entry-server";
|
|
535
|
+
const VIRTUAL_CLIENT = "virtual:yaakdoc/entry-client";
|
|
536
|
+
const RESOLVED = (id) => `\0${id}`;
|
|
537
|
+
const isBare = (id) => !id.startsWith(".") && !id.startsWith("/") && !id.startsWith("\0") && !/^[a-z]+:/i.test(id) && !id.startsWith("@/");
|
|
538
|
+
const json = (value) => JSON.stringify(value);
|
|
539
|
+
const REACT_RE = /^react(-dom)?(\/|$)/;
|
|
540
|
+
const CJS_PREFIX = "\0yaakdoc-cjs:";
|
|
541
|
+
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
|
|
542
|
+
/** ESM module re-exporting a CommonJS module required from yaakdoc's installation. */
|
|
543
|
+
function cjsWrapper(specifier) {
|
|
544
|
+
const mod = createRequire(ENGINE_ANCHOR)(specifier);
|
|
545
|
+
const names = Object.keys(mod).filter((name) => name !== "default" && IDENTIFIER_RE.test(name));
|
|
546
|
+
return [
|
|
547
|
+
`import { createRequire } from "node:module";`,
|
|
548
|
+
`const mod = createRequire(${json(ENGINE_ANCHOR)})(${json(specifier)});`,
|
|
549
|
+
"export default mod;",
|
|
550
|
+
names.length > 0 ? `export const { ${names.join(", ")} } = mod;` : ""
|
|
551
|
+
].join("\n");
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The yaakdoc Vite plugin: virtual SSR/client entries, theme-scoped `@/` alias, and fallback
|
|
555
|
+
* resolution of theme dependencies from the yaakdoc installation (so ejected themes and `npx` work
|
|
556
|
+
* without installing anything in the project).
|
|
557
|
+
*/
|
|
558
|
+
function yaakdocPlugin(getContext, onSsrUpdate) {
|
|
559
|
+
return {
|
|
560
|
+
name: "yaakdoc",
|
|
561
|
+
enforce: "pre",
|
|
562
|
+
async resolveId(source, importer, options) {
|
|
563
|
+
if (source === "virtual:yaakdoc/entry-server" || source === "virtual:yaakdoc/entry-client") return RESOLVED(source);
|
|
564
|
+
const { theme } = getContext();
|
|
565
|
+
if (source.startsWith("@/") && importer?.startsWith(theme.root + sep)) return this.resolve(join(theme.root, "src", source.slice(2)), importer, {
|
|
566
|
+
...options,
|
|
567
|
+
skipSelf: true
|
|
568
|
+
});
|
|
569
|
+
if (!isBare(source)) return null;
|
|
570
|
+
const server = this.environment.config.consumer === "server";
|
|
571
|
+
const finish = (resolved) => {
|
|
572
|
+
if (!resolved) return null;
|
|
573
|
+
if (server) return REACT_RE.test(source) ? {
|
|
574
|
+
...resolved,
|
|
575
|
+
external: true
|
|
576
|
+
} : resolved;
|
|
577
|
+
return optimized(this.environment, resolved, options.scan);
|
|
578
|
+
};
|
|
579
|
+
if (REACT_RE.test(source) && server && this.environment.mode === "dev") return `${CJS_PREFIX}${source}`;
|
|
580
|
+
if (REACT_RE.test(source)) return finish(await this.resolve(source, ENGINE_ANCHOR, {
|
|
581
|
+
...options,
|
|
582
|
+
skipSelf: true
|
|
583
|
+
}));
|
|
584
|
+
if (!(!importer || importer.startsWith("\0"))) {
|
|
585
|
+
const resolved = await this.resolve(source, importer, {
|
|
586
|
+
...options,
|
|
587
|
+
skipSelf: true
|
|
588
|
+
});
|
|
589
|
+
if (resolved) return finish(resolved);
|
|
590
|
+
}
|
|
591
|
+
for (const fallback of [
|
|
592
|
+
ENGINE_ANCHOR,
|
|
593
|
+
...theme.fallbackImporters,
|
|
594
|
+
join(theme.root, "package.json")
|
|
595
|
+
]) {
|
|
596
|
+
const resolved = await this.resolve(source, fallback, {
|
|
597
|
+
...options,
|
|
598
|
+
skipSelf: true
|
|
599
|
+
});
|
|
600
|
+
if (resolved) return finish(resolved);
|
|
601
|
+
}
|
|
602
|
+
return null;
|
|
603
|
+
},
|
|
604
|
+
load(id) {
|
|
605
|
+
if (id.startsWith(CJS_PREFIX)) return cjsWrapper(id.slice(13));
|
|
606
|
+
const context = getContext();
|
|
607
|
+
if (id === RESOLVED("virtual:yaakdoc/entry-server")) return [
|
|
608
|
+
`import theme from ${json(context.theme.entry)};`,
|
|
609
|
+
`import { renderPage } from "@yaakdoc/react/server";`,
|
|
610
|
+
"export function render(input) {",
|
|
611
|
+
" return renderPage({ theme, ...input });",
|
|
612
|
+
"}"
|
|
613
|
+
].join("\n");
|
|
614
|
+
if (id === RESOLVED("virtual:yaakdoc/entry-client")) return [
|
|
615
|
+
...styleEntries(context).map((style) => `import ${json(style)};`),
|
|
616
|
+
`import { hydrateIslands } from "@yaakdoc/react/client";`,
|
|
617
|
+
context.theme.client ? `import islands from ${json(context.theme.client)};` : `import { builtinIslands as islands } from "@yaakdoc/react/islands";`,
|
|
618
|
+
"hydrateIslands(islands);"
|
|
619
|
+
].join("\n");
|
|
620
|
+
return null;
|
|
621
|
+
},
|
|
622
|
+
hotUpdate({ modules, file }) {
|
|
623
|
+
if (this.environment.name !== "ssr" || modules.length === 0 || /\.css($|\?)/.test(file)) return;
|
|
624
|
+
onSsrUpdate?.();
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
|
|
629
|
+
const JS_RE = /\.(m?js|cjs)$/;
|
|
630
|
+
/**
|
|
631
|
+
* Dev only: maps a dependency in node_modules to its pre-bundled version.
|
|
632
|
+
*
|
|
633
|
+
* Vite skips dependency discovery for imports coming from inside node_modules – which is where
|
|
634
|
+
* yaakdoc and its theme live when run with `npx`. Such dependencies are registered here by absolute
|
|
635
|
+
* path (reusing an existing entry for the same file), so they are served pre-bundled as ESM.
|
|
636
|
+
*/
|
|
637
|
+
function optimized(environment, resolved, scan) {
|
|
638
|
+
const optimizer = environment.mode === "dev" ? environment.depsOptimizer : void 0;
|
|
639
|
+
if (!optimizer || scan || resolved.external) return resolved;
|
|
640
|
+
const file = resolved.id.split("?")[0];
|
|
641
|
+
if (!NODE_MODULES_RE.test(file) || !JS_RE.test(file) || optimizer.isOptimizedDepFile(file)) return resolved;
|
|
642
|
+
const { optimized: done, discovered } = optimizer.metadata;
|
|
643
|
+
const info = done[file] ?? discovered[file] ?? [...Object.values(done), ...Object.values(discovered)].find((dep) => dep.src === file) ?? optimizer.registerMissingImport(file, file);
|
|
644
|
+
return optimizer.getOptimizedDepId(info);
|
|
645
|
+
}
|
|
646
|
+
/** CSS entries loaded on every page: the theme's styles, then `branding.customCss`. */
|
|
647
|
+
function styleEntries(context) {
|
|
648
|
+
const { customCss } = context.config.branding;
|
|
649
|
+
return [...context.theme.styles, ...customCss ? [customCss] : []];
|
|
650
|
+
}
|
|
651
|
+
const nodeModulesRoot = (file) => {
|
|
652
|
+
const index = file.lastIndexOf(`${sep}node_modules${sep}`);
|
|
653
|
+
return index === -1 ? dirname(file) : file.slice(0, index + `${sep}node_modules`.length);
|
|
654
|
+
};
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/node/vite.ts
|
|
657
|
+
/** Base Vite config shared by the dev server and builds. */
|
|
658
|
+
async function createViteConfig(getContext, mode, extra = {}) {
|
|
659
|
+
const context = getContext();
|
|
660
|
+
const themePlugins = await context.theme.definition.vitePlugins?.() ?? [];
|
|
661
|
+
const publicDir = join(context.root, "public");
|
|
662
|
+
const cacheDir = existsSync(join(context.root, "node_modules")) ? join(context.root, "node_modules", ".vite-yaakdoc") : join(tmpdir(), "yaakdoc-vite", createHash("sha256").update(context.root).digest("hex").slice(0, 12));
|
|
663
|
+
return {
|
|
664
|
+
configFile: false,
|
|
665
|
+
envDir: false,
|
|
666
|
+
root: context.root,
|
|
667
|
+
base: context.config.base,
|
|
668
|
+
mode,
|
|
669
|
+
logLevel: extra.logLevel ?? "warn",
|
|
670
|
+
clearScreen: false,
|
|
671
|
+
publicDir: existsSync(publicDir) ? publicDir : false,
|
|
672
|
+
cacheDir,
|
|
673
|
+
plugins: [yaakdocPlugin(getContext, extra.onSsrUpdate), ...themePlugins],
|
|
674
|
+
resolve: { alias: styleAliases(context) },
|
|
675
|
+
oxc: { jsx: {
|
|
676
|
+
runtime: "automatic",
|
|
677
|
+
importSource: "react"
|
|
678
|
+
} },
|
|
679
|
+
ssr: { noExternal: [
|
|
680
|
+
"@yaakdoc/react",
|
|
681
|
+
/^@yaakdoc\/theme-/,
|
|
682
|
+
/^yaakdoc-theme-/
|
|
683
|
+
] },
|
|
684
|
+
optimizeDeps: { entries: [] },
|
|
685
|
+
build: { rolldownOptions: { onLog(level, log, handler) {
|
|
686
|
+
if (log.code === "MODULE_LEVEL_DIRECTIVE") return;
|
|
687
|
+
handler(level, log);
|
|
688
|
+
} } },
|
|
689
|
+
server: { fs: { allow: [
|
|
690
|
+
searchForWorkspaceRoot(context.root),
|
|
691
|
+
cacheDir,
|
|
692
|
+
context.theme.root,
|
|
693
|
+
nodeModulesRoot(ENGINE_DIR),
|
|
694
|
+
...context.theme.fallbackImporters.map(nodeModulesRoot)
|
|
695
|
+
] } }
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* CSS `@import "pkg"` is resolved by Vite's CSS resolver (aliases only, no plugins). For CSS packages
|
|
700
|
+
* an ejected theme doesn't have installed (e.g. `tailwindcss`), alias them to yaakdoc's copy.
|
|
701
|
+
*/
|
|
702
|
+
function styleAliases(context) {
|
|
703
|
+
const themeAnchor = join(context.theme.root, "package.json");
|
|
704
|
+
const aliases = [];
|
|
705
|
+
const seen = /* @__PURE__ */ new Set();
|
|
706
|
+
for (const fallback of context.theme.fallbackImporters) {
|
|
707
|
+
let dependencies = [];
|
|
708
|
+
try {
|
|
709
|
+
dependencies = Object.keys(JSON.parse(readFileSync(fallback, "utf8")).dependencies ?? {});
|
|
710
|
+
} catch {}
|
|
711
|
+
for (const name of dependencies) {
|
|
712
|
+
if (seen.has(name)) continue;
|
|
713
|
+
seen.add(name);
|
|
714
|
+
if (resolveFrom(`${name}/package.json`, themeAnchor)) continue;
|
|
715
|
+
const pkgJson = resolveFrom(`${name}/package.json`, fallback);
|
|
716
|
+
if (!pkgJson) continue;
|
|
717
|
+
const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
|
|
718
|
+
const root = pkg.exports?.["."];
|
|
719
|
+
const style = typeof root === "object" && root && "style" in root ? root.style : pkg.style;
|
|
720
|
+
if (typeof style === "string") aliases.push({
|
|
721
|
+
find: new RegExp(`^${escapeRegExp(name)}$`),
|
|
722
|
+
replacement: join(dirname(pkgJson), style)
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return aliases;
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/node/build.ts
|
|
730
|
+
const SSR_DIR = ".yaakdoc-ssr";
|
|
731
|
+
/** Builds the static site: client assets, prerendered HTML for every route, Markdown and llms.txt. */
|
|
732
|
+
async function build$1(options = {}) {
|
|
733
|
+
const previousEnv = process.env.NODE_ENV;
|
|
734
|
+
process.env.NODE_ENV = "production";
|
|
735
|
+
try {
|
|
736
|
+
return await buildWithContext(await resolveContext(options));
|
|
737
|
+
} finally {
|
|
738
|
+
if (previousEnv === void 0) delete process.env.NODE_ENV;
|
|
739
|
+
else process.env.NODE_ENV = previousEnv;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function buildWithContext(context) {
|
|
743
|
+
const outDir = context.config.outDir;
|
|
744
|
+
assertSafeOutDir(outDir, context.root);
|
|
745
|
+
const getContext = () => context;
|
|
746
|
+
const base = await createViteConfig(getContext, "production");
|
|
747
|
+
await build({
|
|
748
|
+
...base,
|
|
749
|
+
build: {
|
|
750
|
+
outDir,
|
|
751
|
+
emptyOutDir: true,
|
|
752
|
+
manifest: true,
|
|
753
|
+
assetsDir: "_yaakdoc",
|
|
754
|
+
copyPublicDir: true,
|
|
755
|
+
rolldownOptions: {
|
|
756
|
+
...base.build?.rolldownOptions,
|
|
757
|
+
input: { client: VIRTUAL_CLIENT }
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
const ssrOutDir = join(outDir, SSR_DIR);
|
|
762
|
+
await build({
|
|
763
|
+
...base,
|
|
764
|
+
publicDir: false,
|
|
765
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
766
|
+
ssr: {
|
|
767
|
+
...base.ssr,
|
|
768
|
+
noExternal: true
|
|
769
|
+
},
|
|
770
|
+
build: {
|
|
771
|
+
ssr: true,
|
|
772
|
+
outDir: ssrOutDir,
|
|
773
|
+
emptyOutDir: true,
|
|
774
|
+
copyPublicDir: false,
|
|
775
|
+
rolldownOptions: {
|
|
776
|
+
...base.build?.rolldownOptions,
|
|
777
|
+
input: { "entry-server": VIRTUAL_SERVER },
|
|
778
|
+
output: {
|
|
779
|
+
entryFileNames: "[name].mjs",
|
|
780
|
+
format: "esm"
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
});
|
|
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
|
+
});
|
|
813
|
+
await rm(join(outDir, ".vite"), {
|
|
814
|
+
recursive: true,
|
|
815
|
+
force: true
|
|
816
|
+
});
|
|
817
|
+
return {
|
|
818
|
+
outDir,
|
|
819
|
+
files,
|
|
820
|
+
context
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function assetsFromManifest(manifest, base) {
|
|
824
|
+
const entry = Object.values(manifest).find((chunk) => chunk.isEntry);
|
|
825
|
+
if (!entry) throw new Error("Client build produced no entry chunk.");
|
|
826
|
+
const collectImports = (keys = [], seen = /* @__PURE__ */ new Set()) => keys.flatMap((key) => {
|
|
827
|
+
if (seen.has(key)) return [];
|
|
828
|
+
seen.add(key);
|
|
829
|
+
const chunk = manifest[key];
|
|
830
|
+
return chunk ? [chunk.file, ...collectImports(chunk.imports, seen)] : [];
|
|
831
|
+
});
|
|
832
|
+
const css = /* @__PURE__ */ new Set([...entry.css ?? []]);
|
|
833
|
+
for (const key of entry.imports ?? []) for (const file of manifest[key]?.css ?? []) css.add(file);
|
|
834
|
+
return {
|
|
835
|
+
styles: [...css].map((file) => base + file),
|
|
836
|
+
scripts: [base + entry.file],
|
|
837
|
+
inlineStyles: [],
|
|
838
|
+
modulePreloads: collectImports(entry.imports).map((file) => base + file)
|
|
839
|
+
};
|
|
840
|
+
}
|
|
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
|
+
//#endregion
|
|
851
|
+
//#region src/node/server.ts
|
|
852
|
+
const CONTENT_TYPES = {
|
|
853
|
+
".md": "text/markdown; charset=utf-8",
|
|
854
|
+
".txt": "text/plain; charset=utf-8",
|
|
855
|
+
".json": "application/json; charset=utf-8",
|
|
856
|
+
".svg": "image/svg+xml",
|
|
857
|
+
".png": "image/png",
|
|
858
|
+
".jpg": "image/jpeg",
|
|
859
|
+
".jpeg": "image/jpeg",
|
|
860
|
+
".gif": "image/gif",
|
|
861
|
+
".webp": "image/webp",
|
|
862
|
+
".avif": "image/avif",
|
|
863
|
+
".ico": "image/x-icon",
|
|
864
|
+
".css": "text/css; charset=utf-8",
|
|
865
|
+
".woff2": "font/woff2"
|
|
866
|
+
};
|
|
867
|
+
/** Interface to listen on: a given host, all interfaces (`true`), or localhost. */
|
|
868
|
+
const listenHostname = (host) => typeof host === "string" ? host : host ? "0.0.0.0" : "localhost";
|
|
869
|
+
/** URL to print/open for a server (all-interfaces hosts are shown as localhost). */
|
|
870
|
+
function serverUrl(host, port, base) {
|
|
871
|
+
const hostname = listenHostname(host);
|
|
872
|
+
return `http://${hostname === "0.0.0.0" ? "localhost" : hostname}:${port}${base}`;
|
|
873
|
+
}
|
|
874
|
+
/** Listens on `port`, trying the next ports when it is taken. */
|
|
875
|
+
function listen(server, port, host) {
|
|
876
|
+
const hostname = listenHostname(host);
|
|
877
|
+
return new Promise((resolvePort, reject) => {
|
|
878
|
+
let attempt = 0;
|
|
879
|
+
const tryListen = (candidate) => {
|
|
880
|
+
server.once("error", (error) => {
|
|
881
|
+
if (error.code === "EADDRINUSE" && attempt++ < 20 && port !== 0) tryListen(candidate + 1);
|
|
882
|
+
else reject(error);
|
|
883
|
+
});
|
|
884
|
+
server.listen(candidate, hostname, () => resolvePort(server.address().port));
|
|
885
|
+
};
|
|
886
|
+
tryListen(port);
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/node/dev.ts
|
|
891
|
+
/** Starts the development server with SSR, HMR for theme code/CSS and reload on export changes. */
|
|
892
|
+
async function createDevServer(options = {}) {
|
|
893
|
+
let context = await resolveContext(options);
|
|
894
|
+
const getContext = () => context;
|
|
895
|
+
const httpServer = createServer$1();
|
|
896
|
+
let vite;
|
|
897
|
+
const fullReload = () => vite?.ws.send({
|
|
898
|
+
type: "full-reload",
|
|
899
|
+
path: "*"
|
|
900
|
+
});
|
|
901
|
+
const config = await createViteConfig(getContext, "development", { onSsrUpdate: fullReload });
|
|
902
|
+
vite = await createServer({
|
|
903
|
+
...config,
|
|
904
|
+
appType: "custom",
|
|
905
|
+
server: {
|
|
906
|
+
...config.server,
|
|
907
|
+
middlewareMode: true,
|
|
908
|
+
hmr: { server: httpServer }
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
const ssr = vite.environments.ssr;
|
|
912
|
+
if (!ssr || !isRunnableDevEnvironment(ssr)) throw new Error("The Vite SSR environment is not runnable.");
|
|
913
|
+
const render = async (route, url) => {
|
|
914
|
+
const { render } = await ssr.runner.import(VIRTUAL_SERVER);
|
|
915
|
+
const inlineStyles = await Promise.all(styleEntries(context).map(async (file) => {
|
|
916
|
+
return {
|
|
917
|
+
id: file,
|
|
918
|
+
css: (await ssr.runner.import(`${file}?inline`)).default
|
|
919
|
+
};
|
|
920
|
+
}));
|
|
921
|
+
const html = await render({
|
|
922
|
+
document: context.document,
|
|
923
|
+
site: context.site,
|
|
924
|
+
page: route.page,
|
|
925
|
+
assets: {
|
|
926
|
+
styles: [],
|
|
927
|
+
inlineStyles,
|
|
928
|
+
scripts: [`${context.config.base}@id/__x00__${VIRTUAL_CLIENT}`],
|
|
929
|
+
modulePreloads: []
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
return vite.transformIndexHtml(url, html);
|
|
933
|
+
};
|
|
934
|
+
vite.middlewares.use(async (req, res, next) => {
|
|
935
|
+
const url = req.url ?? "/";
|
|
936
|
+
const pathname = decodeURIComponent(url.split(/[?#]/)[0] ?? "/");
|
|
937
|
+
try {
|
|
938
|
+
const assetFile = context.assets.get(pathname);
|
|
939
|
+
if (assetFile && existsSync(assetFile)) {
|
|
940
|
+
res.setHeader("Content-Type", CONTENT_TYPES[extname(assetFile)] ?? "application/octet-stream");
|
|
941
|
+
createReadStream(assetFile).pipe(res);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const route = matchRoute(context, pathname);
|
|
945
|
+
const text = context.textFiles.get(pathname) ?? (route && wantsMarkdown(req) ? context.textFiles.get(route.page.markdownUrl ?? "") : void 0);
|
|
946
|
+
if (text !== void 0) {
|
|
947
|
+
res.setHeader("Content-Type", CONTENT_TYPES[extname(pathname)] ?? CONTENT_TYPES[".md"]);
|
|
948
|
+
res.end(text);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
|
952
|
+
if (route || isHtmlRequest(req, pathname)) {
|
|
953
|
+
const html = await render(route ?? context.notFound, url);
|
|
954
|
+
res.statusCode = route ? 200 : 404;
|
|
955
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
956
|
+
res.end(html);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
next();
|
|
960
|
+
} catch (error) {
|
|
961
|
+
vite.ssrFixStacktrace(error);
|
|
962
|
+
consola.error(error);
|
|
963
|
+
res.statusCode = 500;
|
|
964
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
965
|
+
res.end(errorPage(error));
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
httpServer.on("request", vite.middlewares);
|
|
969
|
+
if (options.watch !== false) {
|
|
970
|
+
const watched = new Set(context.watchFiles.map((file) => resolve(file)));
|
|
971
|
+
vite.watcher.add([...watched]);
|
|
972
|
+
let timer;
|
|
973
|
+
const onChange = (file) => {
|
|
974
|
+
if (!watched.has(resolve(file))) return;
|
|
975
|
+
clearTimeout(timer);
|
|
976
|
+
timer = setTimeout(async () => {
|
|
977
|
+
try {
|
|
978
|
+
const previous = context;
|
|
979
|
+
const next = await resolveContext(options);
|
|
980
|
+
if (next.theme.root !== previous.theme.root || next.config.base !== previous.config.base) consola.warn("The theme or base path changed. Restart yaakdoc to apply it.");
|
|
981
|
+
context = next;
|
|
982
|
+
for (const file of next.watchFiles) if (!watched.has(resolve(file))) {
|
|
983
|
+
watched.add(resolve(file));
|
|
984
|
+
vite.watcher.add(file);
|
|
985
|
+
}
|
|
986
|
+
const mod = ssr.moduleGraph.getModuleById(`\0${VIRTUAL_SERVER}`);
|
|
987
|
+
if (mod) ssr.moduleGraph.invalidateModule(mod);
|
|
988
|
+
consola.success(`Reloaded ${colors.dim(file.replace(`${context.root}/`, ""))}`);
|
|
989
|
+
options.onReload?.(context);
|
|
990
|
+
fullReload();
|
|
991
|
+
} catch (error) {
|
|
992
|
+
consola.error(error.message);
|
|
993
|
+
}
|
|
994
|
+
}, 50);
|
|
995
|
+
};
|
|
996
|
+
vite.watcher.on("change", onChange);
|
|
997
|
+
vite.watcher.on("add", onChange);
|
|
998
|
+
}
|
|
999
|
+
const port = await listen(httpServer, options.port ?? context.config.server.port, options.host ?? context.config.server.host);
|
|
1000
|
+
const url = serverUrl(options.host ?? context.config.server.host, port, context.config.base);
|
|
1001
|
+
if (options.open ?? context.config.server.open) {
|
|
1002
|
+
const { default: openBrowser } = await import("./open-C4Bh1af9.js");
|
|
1003
|
+
openBrowser(url);
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
url,
|
|
1007
|
+
vite,
|
|
1008
|
+
httpServer,
|
|
1009
|
+
getContext,
|
|
1010
|
+
close: async () => {
|
|
1011
|
+
await vite.close();
|
|
1012
|
+
await new Promise((done) => httpServer.close(() => done()));
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function wantsMarkdown(req) {
|
|
1017
|
+
const accept = req.headers.accept ?? "";
|
|
1018
|
+
return accept.includes("text/markdown") && !accept.includes("text/html");
|
|
1019
|
+
}
|
|
1020
|
+
function isHtmlRequest(req, pathname) {
|
|
1021
|
+
return (req.headers.accept ?? "").includes("text/html") && !extname(pathname);
|
|
1022
|
+
}
|
|
1023
|
+
const escapeHtml = (value) => value.replace(/[&<>"]/g, (char) => ({
|
|
1024
|
+
"&": "&",
|
|
1025
|
+
"<": "<",
|
|
1026
|
+
">": ">",
|
|
1027
|
+
"\"": """
|
|
1028
|
+
})[char]);
|
|
1029
|
+
function errorPage(error) {
|
|
1030
|
+
return `<!doctype html><meta charset="utf-8"><title>yaakdoc error</title><style>body{font:14px/1.5 ui-monospace,monospace;padding:2rem;background:#1e1e2e;color:#f5e0dc}pre{white-space:pre-wrap;color:#f38ba8}</style><h1>Render error</h1><pre>${escapeHtml(error.stack ?? error.message)}</pre><script type="module" src="/@vite/client"><\/script>`;
|
|
1031
|
+
}
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region src/node/fs.ts
|
|
1034
|
+
/** Writes pretty-printed JSON with a trailing newline (like editors and formatters do). */
|
|
1035
|
+
function writeJson(file, value) {
|
|
1036
|
+
return writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/node/init.ts
|
|
1040
|
+
/** `$schema` value for yaakdoc.config.json: local copy when installed, otherwise unpkg. */
|
|
1041
|
+
function schemaUrl(cwd) {
|
|
1042
|
+
const local = join(cwd, "node_modules", "yaakdoc", "config.schema.json");
|
|
1043
|
+
return existsSync(local) ? "./node_modules/yaakdoc/config.schema.json" : `https://unpkg.com/yaakdoc@${VERSION}/config.schema.json`;
|
|
1044
|
+
}
|
|
1045
|
+
/** Writes a yaakdoc.config.json (with `$schema` for editor autocomplete). */
|
|
1046
|
+
async function init(options = {}) {
|
|
1047
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
1048
|
+
const file = join(cwd, "yaakdoc.config.json");
|
|
1049
|
+
if (existsSync(file) && !options.force) throw new Error("yaakdoc.config.json already exists.");
|
|
1050
|
+
const config = {
|
|
1051
|
+
$schema: schemaUrl(cwd),
|
|
1052
|
+
...options.config
|
|
1053
|
+
};
|
|
1054
|
+
if (!config.export) try {
|
|
1055
|
+
config.export = `./${relative(cwd, await detectExport(cwd))}`;
|
|
1056
|
+
} catch {}
|
|
1057
|
+
await writeJson(file, config);
|
|
1058
|
+
return {
|
|
1059
|
+
file,
|
|
1060
|
+
config
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
//#endregion
|
|
1064
|
+
//#region src/node/eject.ts
|
|
1065
|
+
const DEFAULT_IGNORE = [
|
|
1066
|
+
"node_modules/**",
|
|
1067
|
+
"dist/**",
|
|
1068
|
+
"package.json",
|
|
1069
|
+
"CHANGELOG.md",
|
|
1070
|
+
"**/*.test.*",
|
|
1071
|
+
"test/**"
|
|
1072
|
+
];
|
|
1073
|
+
/** Copies the current theme's source into the project so it can be customized freely. */
|
|
1074
|
+
async function eject(options = {}) {
|
|
1075
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
1076
|
+
const { config, configFile, root } = await loadConfig({
|
|
1077
|
+
cwd,
|
|
1078
|
+
...options.configFile ? { configFile: options.configFile } : {}
|
|
1079
|
+
});
|
|
1080
|
+
const theme = await resolveTheme(config.theme, {
|
|
1081
|
+
root,
|
|
1082
|
+
themeOptions: config.themeOptions
|
|
1083
|
+
});
|
|
1084
|
+
const out = resolve(cwd, options.out ?? "theme");
|
|
1085
|
+
if (out === theme.root) throw new Error(`The theme is already located at ${out}.`);
|
|
1086
|
+
if (existsSync(out) && readdirSync(out).length > 0 && !options.force) throw new Error(`${relative(cwd, out) || out} already exists and is not empty. Use --force to overwrite.`);
|
|
1087
|
+
const files = await glob(theme.definition.eject?.files ?? ["**/*"], {
|
|
1088
|
+
cwd: theme.root,
|
|
1089
|
+
ignore: DEFAULT_IGNORE,
|
|
1090
|
+
dot: false,
|
|
1091
|
+
onlyFiles: true
|
|
1092
|
+
});
|
|
1093
|
+
if (!files.some((file) => resolve(theme.root, file) === theme.manifest)) files.push(relative(theme.root, theme.manifest));
|
|
1094
|
+
for (const file of files) {
|
|
1095
|
+
const target = join(out, file);
|
|
1096
|
+
await mkdir(dirname(target), { recursive: true });
|
|
1097
|
+
await copyFile(join(theme.root, file), target);
|
|
1098
|
+
}
|
|
1099
|
+
let configResult = null;
|
|
1100
|
+
if (options.updateConfig !== false) {
|
|
1101
|
+
const themePath = `./${relative(configFile ? dirname(configFile) : cwd, out).split("\\").join("/")}`;
|
|
1102
|
+
if (configFile && extname(configFile).startsWith(".json")) {
|
|
1103
|
+
const json = JSON.parse(await readFile(configFile, "utf8"));
|
|
1104
|
+
json.theme = themePath;
|
|
1105
|
+
await writeJson(configFile, json);
|
|
1106
|
+
configResult = {
|
|
1107
|
+
file: configFile,
|
|
1108
|
+
updated: true,
|
|
1109
|
+
created: false
|
|
1110
|
+
};
|
|
1111
|
+
} else if (configFile) configResult = {
|
|
1112
|
+
file: configFile,
|
|
1113
|
+
updated: false,
|
|
1114
|
+
created: false
|
|
1115
|
+
};
|
|
1116
|
+
else {
|
|
1117
|
+
const file = join(cwd, "yaakdoc.config.json");
|
|
1118
|
+
await writeJson(file, {
|
|
1119
|
+
$schema: schemaUrl(cwd),
|
|
1120
|
+
theme: themePath
|
|
1121
|
+
});
|
|
1122
|
+
configResult = {
|
|
1123
|
+
file,
|
|
1124
|
+
updated: true,
|
|
1125
|
+
created: true
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
out,
|
|
1131
|
+
files: files.sort(),
|
|
1132
|
+
theme: theme.name,
|
|
1133
|
+
dependencies: theme.definition.eject?.dependencies ?? [],
|
|
1134
|
+
config: configResult
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region src/node/preview.ts
|
|
1139
|
+
/** Serves a built site (like a static host would, including 404.html and base path). */
|
|
1140
|
+
async function preview(options = {}) {
|
|
1141
|
+
const { config } = await loadConfig({
|
|
1142
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1143
|
+
...options.configFile ? { configFile: options.configFile } : {}
|
|
1144
|
+
});
|
|
1145
|
+
const outDir = options.outDir ? resolve(options.cwd ?? process.cwd(), options.outDir) : config.outDir;
|
|
1146
|
+
if (!existsSync(join(outDir, "index.html"))) throw new Error(`No build found in ${outDir}. Run \`yaakdoc build\` first.`);
|
|
1147
|
+
const serve = sirv(outDir, {
|
|
1148
|
+
dev: true,
|
|
1149
|
+
etag: true,
|
|
1150
|
+
extensions: ["html"],
|
|
1151
|
+
setHeaders: (res, pathname) => {
|
|
1152
|
+
const type = CONTENT_TYPES[extname(pathname)];
|
|
1153
|
+
if (type) res.setHeader("Content-Type", type);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
const notFoundFile = join(outDir, "404.html");
|
|
1157
|
+
const base = config.base;
|
|
1158
|
+
const server = createServer$1((req, res) => {
|
|
1159
|
+
const url = req.url ?? "/";
|
|
1160
|
+
if (!url.startsWith(base) && base !== "/") {
|
|
1161
|
+
res.writeHead(302, { Location: base });
|
|
1162
|
+
res.end();
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
req.url = base === "/" ? url : url.slice(base.length - 1);
|
|
1166
|
+
serve(req, res, () => {
|
|
1167
|
+
res.statusCode = 404;
|
|
1168
|
+
if (existsSync(notFoundFile)) {
|
|
1169
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1170
|
+
res.end(readFileSync(notFoundFile));
|
|
1171
|
+
} else res.end("Not found");
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
const host = options.host ?? config.server.host;
|
|
1175
|
+
const url = serverUrl(host, await listen(server, options.port ?? config.server.port, host), base);
|
|
1176
|
+
if (options.open) (await import("./open-C4Bh1af9.js")).default(url);
|
|
1177
|
+
return {
|
|
1178
|
+
url,
|
|
1179
|
+
server,
|
|
1180
|
+
close: () => new Promise((done) => server.close(() => done()))
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
//#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 };
|