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.
- package/README.md +42 -0
- package/dist/cli.js +34 -4
- package/dist/index.d.ts +20 -79
- package/dist/index.js +3 -2
- package/dist/{preview-2YFamRKo.js → preview-Ba1BfVZV.js} +42 -567
- package/dist/site-X7q76qYf.js +609 -0
- package/dist/static.d.ts +2 -0
- package/dist/static.js +1 -0
- package/dist/version-Bwv0ibuY.d.ts +133 -0
- package/package.json +12 -7
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import { builtinSnippets, generateExamples } from "@yaakdoc/snippets";
|
|
2
|
+
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { loadConfig } from "@yaakdoc/config/node";
|
|
8
|
+
import { absoluteUrl, findWorkspace, isUrl, normalize, prepareDocument, sanitizeExport, selectEnvironment, summarize } from "@yaakdoc/core";
|
|
9
|
+
import { detectExport, readExport } from "@yaakdoc/core/node";
|
|
10
|
+
import { folderToMarkdown, indexToMarkdown, llmsFullTxt, llmsTxt, requestToMarkdown } from "@yaakdoc/markdown";
|
|
11
|
+
import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
|
|
12
|
+
import { createJiti } from "jiti";
|
|
13
|
+
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
14
|
+
import rehypeStringify from "rehype-stringify";
|
|
15
|
+
import remarkGfm from "remark-gfm";
|
|
16
|
+
import remarkParse from "remark-parse";
|
|
17
|
+
import remarkRehype from "remark-rehype";
|
|
18
|
+
import { bundledLanguages, createHighlighter } from "shiki";
|
|
19
|
+
import { unified } from "unified";
|
|
20
|
+
//#region src/node/render.ts
|
|
21
|
+
const PRELOADED_LANGUAGES = [
|
|
22
|
+
"bash",
|
|
23
|
+
"javascript",
|
|
24
|
+
"typescript",
|
|
25
|
+
"python",
|
|
26
|
+
"go",
|
|
27
|
+
"json",
|
|
28
|
+
"graphql",
|
|
29
|
+
"xml",
|
|
30
|
+
"html",
|
|
31
|
+
"yaml",
|
|
32
|
+
"http"
|
|
33
|
+
];
|
|
34
|
+
/** Highlights a delimited region as GraphQL (opening captures: prefix tokens, then the delimiter). */
|
|
35
|
+
const embeddedGraphql = (begin, end, prefixScopes) => ({
|
|
36
|
+
begin,
|
|
37
|
+
beginCaptures: Object.fromEntries(prefixScopes.map((name, index) => [index + 1, { name }])),
|
|
38
|
+
end,
|
|
39
|
+
endCaptures: { 0: { name: prefixScopes.at(-1) } },
|
|
40
|
+
contentName: "meta.embedded.block.graphql",
|
|
41
|
+
patterns: [{ include: "source.graphql" }]
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Injection grammar highlighting GraphQL inside code examples (like VS Code's GraphQL extension):
|
|
45
|
+
* `/* GraphQL *\/ \`...\`` and `gql\`...\`` in JavaScript, `query = """..."""` in Python and
|
|
46
|
+
* `query := \`...\`` in Go.
|
|
47
|
+
*/
|
|
48
|
+
const GRAPHQL_INJECTION = {
|
|
49
|
+
name: "yaakdoc-graphql-injection",
|
|
50
|
+
scopeName: "inline.graphql.yaakdoc",
|
|
51
|
+
injectTo: [
|
|
52
|
+
"source.js",
|
|
53
|
+
"source.ts",
|
|
54
|
+
"source.python",
|
|
55
|
+
"source.go"
|
|
56
|
+
],
|
|
57
|
+
injectionSelector: "L:source -comment -string",
|
|
58
|
+
embeddedLangs: ["graphql"],
|
|
59
|
+
patterns: [
|
|
60
|
+
embeddedGraphql("(/\\*\\s*GraphQL\\s*\\*/)\\s*(`)", "`", ["comment.block.js", "punctuation.definition.string.template.begin.js"]),
|
|
61
|
+
embeddedGraphql("\\b(gql|graphql)(`)", "`", ["entity.name.function.tagged-template.js", "punctuation.definition.string.template.begin.js"]),
|
|
62
|
+
embeddedGraphql("\\b(query)\\s*(=)\\s*(\"\"\")", "\"\"\"", [
|
|
63
|
+
"variable.other.python",
|
|
64
|
+
"keyword.operator.assignment.python",
|
|
65
|
+
"punctuation.definition.string.begin.python"
|
|
66
|
+
]),
|
|
67
|
+
embeddedGraphql("\\b(query)\\s*(:=)\\s*(`)", "`", [
|
|
68
|
+
"variable.other.go",
|
|
69
|
+
"keyword.operator.assignment.go",
|
|
70
|
+
"punctuation.definition.string.begin.go"
|
|
71
|
+
])
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
const highlighters = /* @__PURE__ */ new Map();
|
|
75
|
+
function getHighlighter(themes) {
|
|
76
|
+
const key = `${themes.light}|${themes.dark}`;
|
|
77
|
+
let highlighter = highlighters.get(key);
|
|
78
|
+
if (!highlighter) {
|
|
79
|
+
highlighter = createHighlighter({
|
|
80
|
+
themes: [themes.light, themes.dark],
|
|
81
|
+
langs: [...PRELOADED_LANGUAGES, GRAPHQL_INJECTION]
|
|
82
|
+
});
|
|
83
|
+
highlighters.set(key, highlighter);
|
|
84
|
+
}
|
|
85
|
+
return highlighter;
|
|
86
|
+
}
|
|
87
|
+
const LANGUAGE_ALIASES = {
|
|
88
|
+
shell: "bash",
|
|
89
|
+
sh: "bash",
|
|
90
|
+
js: "javascript",
|
|
91
|
+
ts: "typescript",
|
|
92
|
+
text: "text",
|
|
93
|
+
plain: "text"
|
|
94
|
+
};
|
|
95
|
+
async function ensureLanguage(highlighter, language) {
|
|
96
|
+
const lang = LANGUAGE_ALIASES[language] ?? language;
|
|
97
|
+
if (lang === "text" || highlighter.getLoadedLanguages().includes(lang)) return lang;
|
|
98
|
+
if (lang in bundledLanguages) {
|
|
99
|
+
await highlighter.loadLanguage(lang);
|
|
100
|
+
return lang;
|
|
101
|
+
}
|
|
102
|
+
return "text";
|
|
103
|
+
}
|
|
104
|
+
const sanitizeSchema = {
|
|
105
|
+
...defaultSchema,
|
|
106
|
+
attributes: {
|
|
107
|
+
...defaultSchema.attributes,
|
|
108
|
+
code: [...defaultSchema.attributes?.code ?? [], ["className", /^language-./]]
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Creates the Markdown/code renderer: GFM, sanitized HTML (descriptions come from exports that may
|
|
113
|
+
* be shared by others), and Shiki highlighting with dual light/dark themes via CSS variables.
|
|
114
|
+
*/
|
|
115
|
+
async function createRenderer(themes) {
|
|
116
|
+
const highlighter = await getHighlighter(themes);
|
|
117
|
+
const shikiOptions = {
|
|
118
|
+
themes,
|
|
119
|
+
defaultColor: false
|
|
120
|
+
};
|
|
121
|
+
const processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype).use(rehypeSanitize, sanitizeSchema).use(rehypeShikiFromHighlighter, highlighter, {
|
|
122
|
+
...shikiOptions,
|
|
123
|
+
fallbackLanguage: "text",
|
|
124
|
+
lazy: true
|
|
125
|
+
}).use(rehypeStringify);
|
|
126
|
+
return {
|
|
127
|
+
markdown: async (source) => String(await processor.process(source)),
|
|
128
|
+
highlight: async (code, language) => highlighter.codeToHtml(code, {
|
|
129
|
+
...shikiOptions,
|
|
130
|
+
lang: await ensureLanguage(highlighter, language)
|
|
131
|
+
})
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Loads snippet generators by built-in id or module path/package (default export or `snippet`). */
|
|
135
|
+
async function loadSnippets(ids, root) {
|
|
136
|
+
const jiti = createJiti(`${root}/`, { interopDefault: true });
|
|
137
|
+
const snippets = [];
|
|
138
|
+
for (const id of ids) {
|
|
139
|
+
const builtin = builtinSnippets[id];
|
|
140
|
+
if (builtin) {
|
|
141
|
+
snippets.push(builtin);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!existsSync(id) && !id.includes("/") && !id.startsWith("@")) throw new Error(`Unknown snippet "${id}". Built-in snippets: ${Object.keys(builtinSnippets).join(", ")}.`);
|
|
145
|
+
const mod = await jiti.import(id);
|
|
146
|
+
const loaded = "default" in mod ? mod.default : mod;
|
|
147
|
+
for (const snippet of Array.isArray(loaded) ? loaded : [loaded]) {
|
|
148
|
+
if (!snippet || typeof snippet.generate !== "function") throw new Error(`Snippet module "${id}" must export a snippet created with defineSnippet().`);
|
|
149
|
+
snippets.push(snippet);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return snippets;
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/node/resolve.ts
|
|
156
|
+
/**
|
|
157
|
+
* Directory of the installed `yaakdoc` package. When yaakdoc is bundled into CommonJS (the Yaak
|
|
158
|
+
* plugin), `import.meta.url` is not available and the bundle's own location is used.
|
|
159
|
+
*/
|
|
160
|
+
const ENGINE_DIR = (() => {
|
|
161
|
+
const start = typeof __filename === "string" ? dirname(__filename) : dirname(fileURLToPath(import.meta.url));
|
|
162
|
+
let dir = start;
|
|
163
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
164
|
+
if (dirname(dir) === dir) return start;
|
|
165
|
+
dir = dirname(dir);
|
|
166
|
+
}
|
|
167
|
+
return dir;
|
|
168
|
+
})();
|
|
169
|
+
/** A file inside the engine package, used as importer for fallback resolution. */
|
|
170
|
+
const ENGINE_ANCHOR = join(ENGINE_DIR, "package.json");
|
|
171
|
+
/**
|
|
172
|
+
* Resolves an ESM specifier as if imported from `fromFile`. Returns an absolute path or null.
|
|
173
|
+
*/
|
|
174
|
+
function resolveFrom(specifier, fromFile) {
|
|
175
|
+
try {
|
|
176
|
+
const resolved = createJiti(fromFile).esmResolve(specifier, { try: true });
|
|
177
|
+
return resolved ? resolved.startsWith("file:") ? fileURLToPath(resolved) : resolved : null;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Resolves a package directory (the folder containing its package.json). */
|
|
183
|
+
function resolvePackageDir(name, fromFile) {
|
|
184
|
+
const entry = resolveFrom(`${name}/package.json`, fromFile);
|
|
185
|
+
if (entry) return dirname(entry);
|
|
186
|
+
const main = resolveFrom(name, fromFile);
|
|
187
|
+
if (!main) return null;
|
|
188
|
+
let dir = dirname(main);
|
|
189
|
+
while (dir !== dirname(dir)) {
|
|
190
|
+
if (existsSync(join(dir, "package.json")) && dir.endsWith(name.split("/").at(-1))) return dir;
|
|
191
|
+
dir = dirname(dir);
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/node/theme.ts
|
|
197
|
+
var ThemeError = class extends Error {
|
|
198
|
+
name = "ThemeError";
|
|
199
|
+
};
|
|
200
|
+
const MANIFEST_NAMES = [
|
|
201
|
+
"theme.ts",
|
|
202
|
+
"theme.mts",
|
|
203
|
+
"theme.js",
|
|
204
|
+
"theme.mjs",
|
|
205
|
+
"index.ts",
|
|
206
|
+
"index.js"
|
|
207
|
+
];
|
|
208
|
+
/** Candidate package names for a theme spec: `kiseki` → `@yaakdoc/theme-kiseki`, `yaakdoc-theme-kiseki`. */
|
|
209
|
+
function themePackageCandidates(spec) {
|
|
210
|
+
if (spec.startsWith("@") || spec.includes("/") || spec.startsWith("yaakdoc-theme-")) return [spec];
|
|
211
|
+
return [
|
|
212
|
+
`@yaakdoc/theme-${spec}`,
|
|
213
|
+
`yaakdoc-theme-${spec}`,
|
|
214
|
+
spec
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
const isLocal = (spec) => spec.startsWith(".") || isAbsolute(spec);
|
|
218
|
+
function findManifest(path) {
|
|
219
|
+
if (existsSync(path) && statSync(path).isFile()) return path;
|
|
220
|
+
for (const name of MANIFEST_NAMES) {
|
|
221
|
+
const candidate = join(path, name);
|
|
222
|
+
if (existsSync(candidate)) return candidate;
|
|
223
|
+
}
|
|
224
|
+
throw new ThemeError(`No theme manifest (${MANIFEST_NAMES.join(", ")}) found in ${path}`);
|
|
225
|
+
}
|
|
226
|
+
/** Built-in theme package directories, used to resolve dependencies of ejected themes. */
|
|
227
|
+
function builtinFallbacks() {
|
|
228
|
+
const kiseki = resolvePackageDir("@yaakdoc/theme-kiseki", ENGINE_ANCHOR);
|
|
229
|
+
return [ENGINE_ANCHOR, ...kiseki ? [join(kiseki, "package.json")] : []];
|
|
230
|
+
}
|
|
231
|
+
/** Loads and validates a theme from a short name, package name or local path. */
|
|
232
|
+
async function resolveTheme(spec, options) {
|
|
233
|
+
const userAnchor = join(options.root, "package.json");
|
|
234
|
+
let manifest = null;
|
|
235
|
+
let packageName = null;
|
|
236
|
+
if (isLocal(spec)) manifest = findManifest(resolve(options.root, spec));
|
|
237
|
+
else {
|
|
238
|
+
for (const candidate of themePackageCandidates(spec)) {
|
|
239
|
+
const resolved = resolveFrom(candidate, userAnchor) ?? resolveFrom(candidate, ENGINE_ANCHOR);
|
|
240
|
+
if (resolved) {
|
|
241
|
+
manifest = resolved;
|
|
242
|
+
packageName = candidate;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!manifest) throw new ThemeError(`Theme "${spec}" not found. Install it (e.g. npm i -D ${themePackageCandidates(spec)[0]}) or use a local path.`);
|
|
247
|
+
}
|
|
248
|
+
const fallbackImporters = builtinFallbacks();
|
|
249
|
+
const alias = buildAliases(manifest, fallbackImporters);
|
|
250
|
+
const jiti = createJiti(manifest, {
|
|
251
|
+
alias,
|
|
252
|
+
moduleCache: false,
|
|
253
|
+
interopDefault: true
|
|
254
|
+
});
|
|
255
|
+
let definition;
|
|
256
|
+
try {
|
|
257
|
+
definition = await jiti.import(manifest, { default: true });
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new ThemeError(`Failed to load theme ${manifest}: ${error.message}`);
|
|
260
|
+
}
|
|
261
|
+
if (!definition || typeof definition !== "object" || !definition.entry) throw new ThemeError(`Theme ${manifest} must default-export defineTheme({ name, entry, ... }).`);
|
|
262
|
+
return themeFromDefinition(definition, {
|
|
263
|
+
manifest,
|
|
264
|
+
packageName,
|
|
265
|
+
themeOptions: options.themeOptions,
|
|
266
|
+
fallbackImporters
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Validates the options of an already loaded theme definition. `resolveTheme` loads the definition
|
|
271
|
+
* from a manifest; bundled engines (the Yaak plugin) pass a definition they import themselves.
|
|
272
|
+
*/
|
|
273
|
+
function themeFromDefinition(definition, options) {
|
|
274
|
+
const root = dirname(options.manifest);
|
|
275
|
+
const abs = (path) => resolve(root, path);
|
|
276
|
+
let themeOptions = options.themeOptions;
|
|
277
|
+
if (definition.options) try {
|
|
278
|
+
themeOptions = definition.options.parse(options.themeOptions);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new ThemeError(`Invalid themeOptions for theme "${definition.name}":\n${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
name: definition.name,
|
|
284
|
+
definition,
|
|
285
|
+
root,
|
|
286
|
+
manifest: options.manifest,
|
|
287
|
+
entry: abs(definition.entry),
|
|
288
|
+
client: definition.client ? abs(definition.client) : null,
|
|
289
|
+
styles: (definition.styles ?? []).map(abs),
|
|
290
|
+
options: themeOptions,
|
|
291
|
+
packageName: options.packageName ?? null,
|
|
292
|
+
fallbackImporters: options.fallbackImporters ?? [],
|
|
293
|
+
highlight: definition.highlight ?? {
|
|
294
|
+
light: "github-light",
|
|
295
|
+
dark: "github-dark"
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* jiti aliases so a theme manifest (e.g. an ejected theme in a project without dependencies) can
|
|
301
|
+
* import `@yaakdoc/react/theme` and the built-in themes' dependencies.
|
|
302
|
+
*/
|
|
303
|
+
function buildAliases(manifest, fallbacks) {
|
|
304
|
+
const specs = /* @__PURE__ */ new Set(["@yaakdoc/react/theme", "@yaakdoc/react"]);
|
|
305
|
+
for (const fallback of fallbacks) try {
|
|
306
|
+
const pkg = JSON.parse(readFileSync(fallback, "utf8"));
|
|
307
|
+
for (const name of Object.keys(pkg.dependencies ?? {})) specs.add(name);
|
|
308
|
+
} catch {}
|
|
309
|
+
const alias = {};
|
|
310
|
+
for (const spec of specs) {
|
|
311
|
+
if (resolveFrom(spec, manifest)) continue;
|
|
312
|
+
for (const fallback of fallbacks) {
|
|
313
|
+
const resolved = resolveFrom(spec, fallback);
|
|
314
|
+
if (resolved) {
|
|
315
|
+
alias[spec] = resolved;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return alias;
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/node/version.ts
|
|
324
|
+
/** The yaakdoc version: inlined by the build, read from package.json when running from source. */
|
|
325
|
+
const VERSION = "0.2.0";
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/node/context.ts
|
|
328
|
+
/** Loads config, export and theme, and prepares everything needed to render the site. */
|
|
329
|
+
async function resolveContext(options = {}) {
|
|
330
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
331
|
+
const { config, configFile, root } = await loadConfig({
|
|
332
|
+
cwd,
|
|
333
|
+
...options.configFile ? { configFile: options.configFile } : {},
|
|
334
|
+
...options.overrides ? { overrides: options.overrides } : {}
|
|
335
|
+
});
|
|
336
|
+
const exportPath = options.exportData === void 0 ? config.export ?? await detectExport(root) : null;
|
|
337
|
+
const rawExport = exportPath ? await readExport(exportPath) : options.exportData;
|
|
338
|
+
const source = normalize(rawExport);
|
|
339
|
+
const workspace = findWorkspace(source, config.workspace);
|
|
340
|
+
const environments = config.environments === "public" ? workspace.environments.filter((env) => env.public) : (config.environments ?? []).map((name) => selectEnvironment(workspace, name));
|
|
341
|
+
const environment = config.environment === false ? null : config.environment === void 0 && environments[0] ? environments[0] : selectEnvironment(workspace, config.environment);
|
|
342
|
+
const theme = await (options.resolveTheme ?? resolveTheme)(config.theme, {
|
|
343
|
+
root,
|
|
344
|
+
themeOptions: config.themeOptions
|
|
345
|
+
});
|
|
346
|
+
const renderer = await createRenderer(theme.highlight);
|
|
347
|
+
const snippets = await loadSnippets(config.snippets, root);
|
|
348
|
+
const document = await prepareDocument({
|
|
349
|
+
workspace,
|
|
350
|
+
environment,
|
|
351
|
+
environments,
|
|
352
|
+
pages: config.pages,
|
|
353
|
+
base: config.base,
|
|
354
|
+
variables: config.variables,
|
|
355
|
+
filter: config.filter,
|
|
356
|
+
renderMarkdown: renderer.markdown,
|
|
357
|
+
highlight: renderer.highlight,
|
|
358
|
+
examples: (request) => generateExamples(request, snippets),
|
|
359
|
+
meta: {
|
|
360
|
+
yaakVersion: source.yaakVersion,
|
|
361
|
+
exportedAt: source.exportedAt
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
const assets = /* @__PURE__ */ new Map();
|
|
365
|
+
const asset = (path) => publicAsset(path, config.base, assets);
|
|
366
|
+
const site = await createSiteData(config, document, theme, asset, renderer.markdown);
|
|
367
|
+
const { routes, notFound } = createRoutes(document, site);
|
|
368
|
+
const markdownOptions = {
|
|
369
|
+
title: site.title,
|
|
370
|
+
description: site.description,
|
|
371
|
+
base: config.base,
|
|
372
|
+
...site.url ? { siteUrl: site.url } : {}
|
|
373
|
+
};
|
|
374
|
+
const textFiles = /* @__PURE__ */ new Map();
|
|
375
|
+
if (config.llms.enabled) {
|
|
376
|
+
textFiles.set(`${config.base}index.md`, config.pages === "single" ? llmsFullTxt(document, markdownOptions) : indexToMarkdown(document, markdownOptions));
|
|
377
|
+
for (const folder of Object.values(document.folders)) textFiles.set(folder.markdownUrl, folderToMarkdown(document, folder, markdownOptions));
|
|
378
|
+
for (const request of Object.values(document.requests)) textFiles.set(request.markdownUrl, requestToMarkdown(request, markdownOptions));
|
|
379
|
+
textFiles.set(`${config.base}llms.txt`, llmsTxt(document, {
|
|
380
|
+
...markdownOptions,
|
|
381
|
+
...config.llms.full ? { fullTextUrl: `${config.base}llms-full.txt` } : {}
|
|
382
|
+
}));
|
|
383
|
+
if (config.llms.full) textFiles.set(`${config.base}llms-full.txt`, llmsFullTxt(document, markdownOptions));
|
|
384
|
+
}
|
|
385
|
+
const externalExport = typeof config.runInYaak === "object" && config.runInYaak.url;
|
|
386
|
+
if (site.runInYaak && !externalExport) {
|
|
387
|
+
const shared = shareableExport(rawExport, workspace, document, config.variables.hide);
|
|
388
|
+
textFiles.set(site.runInYaak.dataUrl, JSON.stringify(shared, null, 2));
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
cwd,
|
|
392
|
+
root,
|
|
393
|
+
configFile,
|
|
394
|
+
config,
|
|
395
|
+
exportPath,
|
|
396
|
+
source,
|
|
397
|
+
theme,
|
|
398
|
+
document,
|
|
399
|
+
site,
|
|
400
|
+
routes,
|
|
401
|
+
notFound,
|
|
402
|
+
textFiles,
|
|
403
|
+
assets,
|
|
404
|
+
watchFiles: [
|
|
405
|
+
...exportPath ? [exportPath] : [],
|
|
406
|
+
...configFile ? [configFile] : [],
|
|
407
|
+
...assets.values()
|
|
408
|
+
]
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/** The export restricted to what the docs show, safe to publish for "Run in Yaak". */
|
|
412
|
+
function shareableExport(raw, workspace, document, hide) {
|
|
413
|
+
const environmentIds = new Set(document.environments.map((env) => env.id));
|
|
414
|
+
if (document.environment) environmentIds.add(document.environment.id);
|
|
415
|
+
if (workspace.baseEnvironment) environmentIds.add(workspace.baseEnvironment.id);
|
|
416
|
+
for (const folderId of Object.keys(document.folders)) for (const env of workspace.folderEnvironments[folderId] ?? []) environmentIds.add(env.id);
|
|
417
|
+
return sanitizeExport(raw, {
|
|
418
|
+
workspaceId: workspace.id,
|
|
419
|
+
folderIds: new Set(Object.keys(document.folders)),
|
|
420
|
+
requestIds: new Set(Object.keys(document.requests)),
|
|
421
|
+
environmentIds,
|
|
422
|
+
hide
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Maps a local asset (logo, favicon) to a public URL under `<base>_yaakdoc/`, registering it in
|
|
427
|
+
* `assets` so it's copied into the site. URLs and site-root paths that don't exist on disk (e.g.
|
|
428
|
+
* `/logo.svg` served from `public/`) are kept as is.
|
|
429
|
+
*/
|
|
430
|
+
function publicAsset(path, base, assets) {
|
|
431
|
+
if (!path) return null;
|
|
432
|
+
if (isUrl(path) || !existsSync(path)) return path;
|
|
433
|
+
const hash = createHash("sha256").update(path).digest("hex").slice(0, 8);
|
|
434
|
+
const url = `${base}_yaakdoc/${basename(path, extname(path))}-${hash}${extname(path)}`;
|
|
435
|
+
assets.set(url, path);
|
|
436
|
+
return url;
|
|
437
|
+
}
|
|
438
|
+
async function createSiteData(config, document, theme, asset, markdown) {
|
|
439
|
+
const { branding } = config;
|
|
440
|
+
const title = branding.title ?? document.workspace.name;
|
|
441
|
+
let logo = null;
|
|
442
|
+
if (typeof branding.logo === "string") logo = {
|
|
443
|
+
light: asset(branding.logo) ?? void 0,
|
|
444
|
+
alt: title,
|
|
445
|
+
href: config.base
|
|
446
|
+
};
|
|
447
|
+
else if (branding.logo) logo = {
|
|
448
|
+
...branding.logo.light ? { light: asset(branding.logo.light) } : {},
|
|
449
|
+
...branding.logo.dark ? { dark: asset(branding.logo.dark) } : {},
|
|
450
|
+
alt: branding.logo.alt ?? title,
|
|
451
|
+
href: branding.logo.href ?? config.base,
|
|
452
|
+
...branding.logo.width ? { width: branding.logo.width } : {},
|
|
453
|
+
...branding.logo.height ? { height: branding.logo.height } : {}
|
|
454
|
+
};
|
|
455
|
+
return {
|
|
456
|
+
title,
|
|
457
|
+
description: branding.description ?? summarize(document.workspace.description, 200),
|
|
458
|
+
url: branding.url ?? null,
|
|
459
|
+
base: config.base,
|
|
460
|
+
pages: config.pages,
|
|
461
|
+
logo,
|
|
462
|
+
favicon: asset(branding.favicon),
|
|
463
|
+
colors: branding.colors ?? {},
|
|
464
|
+
fonts: branding.fonts ?? {},
|
|
465
|
+
links: branding.links,
|
|
466
|
+
footerHtml: branding.footer ? await markdown(branding.footer) : null,
|
|
467
|
+
defaultColorMode: branding.defaultColorMode,
|
|
468
|
+
colorModeToggle: branding.colorModeToggle,
|
|
469
|
+
llms: {
|
|
470
|
+
...config.llms,
|
|
471
|
+
txtUrl: config.llms.enabled ? `${config.base}llms.txt` : null,
|
|
472
|
+
fullTxtUrl: config.llms.enabled && config.llms.full ? `${config.base}llms-full.txt` : null
|
|
473
|
+
},
|
|
474
|
+
showVariables: config.variables.show,
|
|
475
|
+
runInYaak: config.runInYaak ? {
|
|
476
|
+
name: typeof config.runInYaak === "object" && config.runInYaak.name || title,
|
|
477
|
+
dataUrl: typeof config.runInYaak === "object" && config.runInYaak.url || `${config.base}yaak.json`
|
|
478
|
+
} : null,
|
|
479
|
+
head: config.head,
|
|
480
|
+
themeOptions: theme.options,
|
|
481
|
+
generator: `yaakdoc ${VERSION}`
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function createRoutes(document, site) {
|
|
485
|
+
const { base } = site;
|
|
486
|
+
const llms = site.llms.enabled;
|
|
487
|
+
const routes = [{
|
|
488
|
+
path: base,
|
|
489
|
+
file: "index.html",
|
|
490
|
+
page: {
|
|
491
|
+
kind: "index",
|
|
492
|
+
id: null,
|
|
493
|
+
path: base,
|
|
494
|
+
title: "",
|
|
495
|
+
description: site.description,
|
|
496
|
+
markdownUrl: llms ? `${base}index.md` : null
|
|
497
|
+
}
|
|
498
|
+
}];
|
|
499
|
+
if (site.pages === "per-request") {
|
|
500
|
+
for (const folder of Object.values(document.folders)) routes.push({
|
|
501
|
+
path: folder.href,
|
|
502
|
+
file: `${folder.slug}/index.html`,
|
|
503
|
+
page: {
|
|
504
|
+
kind: "folder",
|
|
505
|
+
id: folder.id,
|
|
506
|
+
path: folder.href,
|
|
507
|
+
title: folder.name,
|
|
508
|
+
description: folder.summary,
|
|
509
|
+
markdownUrl: llms ? folder.markdownUrl : null
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
for (const request of Object.values(document.requests)) routes.push({
|
|
513
|
+
path: request.href,
|
|
514
|
+
file: `${request.slug}/index.html`,
|
|
515
|
+
page: {
|
|
516
|
+
kind: "request",
|
|
517
|
+
id: request.id,
|
|
518
|
+
path: request.href,
|
|
519
|
+
title: request.name,
|
|
520
|
+
description: request.summary,
|
|
521
|
+
markdownUrl: llms ? request.markdownUrl : null
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
routes,
|
|
527
|
+
notFound: {
|
|
528
|
+
path: `${base}404.html`,
|
|
529
|
+
file: "404.html",
|
|
530
|
+
page: {
|
|
531
|
+
kind: "not-found",
|
|
532
|
+
id: null,
|
|
533
|
+
path: `${base}404.html`,
|
|
534
|
+
title: "Page not found",
|
|
535
|
+
description: "",
|
|
536
|
+
markdownUrl: null
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
/** Finds the route for a URL path (ignoring query/hash; `/x` matches `/x/`). */
|
|
542
|
+
function matchRoute(context, pathname) {
|
|
543
|
+
const path = decodeURIComponent(pathname.split(/[?#]/)[0] ?? "/");
|
|
544
|
+
const normalized = path.endsWith("/") || path.endsWith(".html") ? path : `${path}/`;
|
|
545
|
+
return context.routes.find((route) => route.path === normalized || `${route.path}index.html` === path) ?? null;
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/node/site.ts
|
|
549
|
+
/** Prefixes the client asset paths (relative to the output directory) with the site's base. */
|
|
550
|
+
function withBase(assets, base) {
|
|
551
|
+
const prefix = (path) => base + path;
|
|
552
|
+
return {
|
|
553
|
+
styles: assets.styles.map(prefix),
|
|
554
|
+
scripts: assets.scripts.map(prefix),
|
|
555
|
+
inlineStyles: assets.inlineStyles,
|
|
556
|
+
modulePreloads: assets.modulePreloads.map(prefix)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/** Refuses to empty the project directory (or a parent of it). */
|
|
560
|
+
function assertSafeOutDir(outDir, root) {
|
|
561
|
+
const rel = relative(resolve(outDir), resolve(root));
|
|
562
|
+
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.`);
|
|
563
|
+
}
|
|
564
|
+
/** Empties (or creates) the output directory, after checking that it's safe to. */
|
|
565
|
+
async function emptyOutDir(outDir, root) {
|
|
566
|
+
assertSafeOutDir(outDir, root);
|
|
567
|
+
await rm(outDir, {
|
|
568
|
+
recursive: true,
|
|
569
|
+
force: true
|
|
570
|
+
});
|
|
571
|
+
await mkdir(outDir, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Writes the site into `context.config.outDir`, next to the client assets that are already there:
|
|
575
|
+
* every route prerendered with `render`, the Markdown twins and llms.txt, branding assets and the
|
|
576
|
+
* sitemap. Returns the written files, relative to the output directory.
|
|
577
|
+
*/
|
|
578
|
+
async function writeSite(context, { render, assets }) {
|
|
579
|
+
const outDir = context.config.outDir;
|
|
580
|
+
const files = [];
|
|
581
|
+
const write = async (file, content) => {
|
|
582
|
+
const target = join(outDir, file);
|
|
583
|
+
await mkdir(dirname(target), { recursive: true });
|
|
584
|
+
await writeFile(target, content);
|
|
585
|
+
files.push(file);
|
|
586
|
+
};
|
|
587
|
+
for (const route of [...context.routes, context.notFound]) await write(route.file, await render({
|
|
588
|
+
document: context.document,
|
|
589
|
+
site: context.site,
|
|
590
|
+
page: route.page,
|
|
591
|
+
assets
|
|
592
|
+
}));
|
|
593
|
+
for (const [url, content] of context.textFiles) await write(stripBase(url, context.config.base), content);
|
|
594
|
+
for (const [url, source] of context.assets) {
|
|
595
|
+
const file = stripBase(url, context.config.base);
|
|
596
|
+
await mkdir(dirname(join(outDir, file)), { recursive: true });
|
|
597
|
+
await copyFile(source, join(outDir, file));
|
|
598
|
+
files.push(file);
|
|
599
|
+
}
|
|
600
|
+
if (context.site.url) await write("sitemap.xml", sitemap(context));
|
|
601
|
+
await write(".nojekyll", "");
|
|
602
|
+
return files;
|
|
603
|
+
}
|
|
604
|
+
const stripBase = (url, base) => url.startsWith(base) ? url.slice(base.length) : url.replace(/^\//, "");
|
|
605
|
+
function sitemap(context) {
|
|
606
|
+
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`;
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
export { matchRoute as a, VERSION as c, themeFromDefinition as d, themePackageCandidates as f, resolveFrom as h, writeSite as i, ThemeError as l, ENGINE_DIR as m, emptyOutDir as n, publicAsset as o, ENGINE_ANCHOR as p, withBase as r, resolveContext as s, assertSafeOutDir as t, resolveTheme as u };
|
package/dist/static.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as withBase, c as Route, d as publicAsset, f as resolveContext, g as themeFromDefinition, i as emptyOutDir, l as YaakdocOptions, m as ThemeError, n as RenderEntry, o as writeSite, p as ResolvedTheme, r as assertSafeOutDir, s as Context, t as VERSION, u as matchRoute } from "./version-Bwv0ibuY.js";
|
|
2
|
+
export { type Context, type RenderEntry, type ResolvedTheme, type Route, ThemeError, VERSION, type YaakdocOptions, assertSafeOutDir, emptyOutDir, matchRoute, publicAsset, resolveContext, themeFromDefinition, withBase, writeSite };
|
package/dist/static.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { l as ThemeError, c as VERSION, t as assertSafeOutDir, n as emptyOutDir, a as matchRoute, o as publicAsset, s as resolveContext, d as themeFromDefinition, r as withBase, i as writeSite } from "./site-X7q76qYf.js";
|