@takazudo/zudo-doc 5.15.0 → 5.16.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/CHANGELOG.md +17 -0
- package/dist/asset-index-page/index.js +1 -1
- package/dist/asset-page/index.js +1 -1
- package/dist/config.d.ts +15 -6
- package/dist/config.js +2 -0
- package/dist/doc-content-header/index.js +1 -1
- package/dist/doc-page-shell/index.js +1 -1
- package/dist/home-page/index.js +1 -1
- package/dist/nav-indexing/versions-page-content.js +1 -1
- package/dist/plugins/internal/asset-viewer/asset-pages.d.ts +53 -0
- package/dist/plugins/internal/asset-viewer/asset-pages.js +98 -0
- package/dist/plugins/internal/asset-viewer/scan.d.ts +18 -1
- package/dist/plugins/internal/asset-viewer/scan.js +34 -27
- package/dist/plugins/internal/llms-txt/assets.d.ts +24 -0
- package/dist/plugins/internal/llms-txt/assets.js +65 -0
- package/dist/plugins/internal/llms-txt/dev-middleware.d.ts +5 -0
- package/dist/plugins/internal/llms-txt/dev-middleware.js +9 -1
- package/dist/plugins/internal/llms-txt/emit.js +21 -5
- package/dist/plugins/internal/llms-txt/generate.d.ts +3 -3
- package/dist/plugins/internal/llms-txt/generate.js +55 -2
- package/dist/plugins/internal/llms-txt/index.d.ts +4 -1
- package/dist/plugins/internal/llms-txt/index.js +4 -0
- package/dist/plugins/internal/llms-txt/types.d.ts +24 -0
- package/dist/plugins/internal/llms-txt/types.js +4 -0
- package/dist/plugins/internal/search-index/collect.js +46 -1
- package/dist/plugins/internal/search-index/types.d.ts +6 -1
- package/dist/plugins/llms-txt.js +9 -1
- package/dist/plugins/search-index.js +10 -1
- package/dist/preset.d.ts +5 -1
- package/dist/preset.js +15 -2
- package/dist/route-enumerators/index.d.ts +14 -1
- package/dist/route-enumerators/index.js +38 -0
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +15 -0
- package/dist/tag-pages/index.js +2 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `@takazudo/zudo-doc` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
|
|
6
6
|
|
|
7
|
+
## [5.16.0] - 2026-09-02
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Added opt-in Markdown/MDX transclusion through `transclude`, plus independent `assetViewerIndexing` controls for search, llms.txt, and sitemap output. (`8145aae41`)
|
|
12
|
+
- Generated asset-viewer pages can now be enumerated in sitemaps and indexed in search and llms outputs, with locale, base-path, exclusion, and bounded content-extraction handling. (`5da03612e`, `b48009d7e`, `3de313585`, `76b90bc48`)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- Long unbroken titles now wrap across documentation, home, asset, version, and tag pages. (`f21c6b73d`)
|
|
17
|
+
- Asset search excerpts now use bounded reads for large files. (`6c81a13b3`)
|
|
18
|
+
- Asset links in llms outputs now honor the configured `siteUrl`. (`67482a1d5`)
|
|
19
|
+
|
|
20
|
+
### Other Changes
|
|
21
|
+
|
|
22
|
+
- Raised the optional `@takazudo/zudo-doc-history-server` peer floor to the already-published 5.15.0 release. (`01bd83bc7`)
|
|
23
|
+
|
|
7
24
|
## [5.15.0] - 2026-09-01
|
|
8
25
|
|
|
9
26
|
### Features
|
|
@@ -145,7 +145,7 @@ function createAssetIndexPageView(ctx) {
|
|
|
145
145
|
/* @__PURE__ */ jsx("span", { class: "rounded-full border border-muted px-hsp-sm py-vsp-3xs text-fg", children: title }),
|
|
146
146
|
/* @__PURE__ */ jsx("span", { class: "rounded-full border border-muted px-hsp-sm py-vsp-3xs text-muted", children: t("asset.indexBadge", locale) })
|
|
147
147
|
] }),
|
|
148
|
-
/* @__PURE__ */ jsx("h1", { class: "mb-vsp-xs border-b border-fg pb-vsp-xs font-mono text-heading font-bold leading-tight", children: title }),
|
|
148
|
+
/* @__PURE__ */ jsx("h1", { class: "mb-vsp-xs border-b border-fg pb-vsp-xs font-mono text-heading font-bold leading-tight break-words", children: title }),
|
|
149
149
|
/* @__PURE__ */ jsxs("div", { "data-doc-metainfo": true, class: "mb-vsp-md flex flex-wrap items-center gap-x-hsp-md gap-y-vsp-2xs text-caption text-fg", children: [
|
|
150
150
|
/* @__PURE__ */ jsx("span", { children: countLabel(tree.fileCount, fileCountLabel, fileCountSingleLabel) }),
|
|
151
151
|
/* @__PURE__ */ jsx("span", { children: countLabel(folders, folderCountLabel, folderCountSingleLabel) }),
|
package/dist/asset-page/index.js
CHANGED
|
@@ -42,7 +42,7 @@ function AssetHeader({ asset, locale, badge, updatedLabel, linesLabel }) {
|
|
|
42
42
|
const facet = facetLabel(asset, linesLabel);
|
|
43
43
|
return /* @__PURE__ */ jsxs("header", { children: [
|
|
44
44
|
/* @__PURE__ */ jsx(AssetEyebrow, { asset, badge }),
|
|
45
|
-
/* @__PURE__ */ jsx("h1", { class: "mb-vsp-xs border-b border-fg pb-vsp-xs font-mono text-heading font-bold leading-tight", children: asset.name }),
|
|
45
|
+
/* @__PURE__ */ jsx("h1", { class: "mb-vsp-xs border-b border-fg pb-vsp-xs font-mono text-heading font-bold leading-tight break-words", children: asset.name }),
|
|
46
46
|
/* @__PURE__ */ jsxs("div", { "data-doc-metainfo": true, class: "mb-vsp-md flex flex-wrap items-center gap-x-hsp-md gap-y-vsp-2xs text-caption text-fg", children: [
|
|
47
47
|
asset.dir && /* @__PURE__ */ jsx("span", { children: asset.dir }),
|
|
48
48
|
facet && /* @__PURE__ */ jsx("span", { children: facet }),
|
package/dist/config.d.ts
CHANGED
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
* `zudoDoc()` SHALLOW-merges the user's fields over {@link DEFAULT_SETTINGS} —
|
|
19
19
|
* top-level fields only (`{ ...DEFAULT_SETTINGS, ...user }`). A supplied nested
|
|
20
20
|
* object (e.g. `colorMode`, `metaTags`) REPLACES the default wholesale; it is
|
|
21
|
-
* NOT deep-merged key-by-key.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* NOT deep-merged key-by-key. Nested config types that intentionally expose
|
|
22
|
+
* optional keys (such as `FrontmatterPreviewConfig` and
|
|
23
|
+
* `AssetViewerIndexingConfig`) get their per-key defaults at the read site;
|
|
24
|
+
* this shallow merge never fills nested keys. `FaviconConfig` is also
|
|
25
|
+
* all-optional — wholesale replacement IS its semantics (only the supplied
|
|
26
|
+
* slots emit a `<link rel="icon">`), and its default is `undefined`, so there
|
|
27
|
+
* is nothing for a partial object to clobber.
|
|
26
28
|
* `zudoDoc()` also supplies the Wave-3 package defaults
|
|
27
29
|
* (`buildDocsSchema`/`directiveVocabulary`/`translations`/`colorSchemes`/tag
|
|
28
30
|
* vocabulary) unless overridden, and returns a **complete `ZfbConfig`** — the
|
|
@@ -65,7 +67,7 @@ import type { ZfbConfig, BundleConfig } from "@takazudo/zfb/config";
|
|
|
65
67
|
import type { ZodType } from "zod";
|
|
66
68
|
import type { DirectiveVocabulary, PresetTranslations, PresetTagVocabularyEntry } from "./preset.js";
|
|
67
69
|
import type { ColorScheme } from "./color-scheme-utils.js";
|
|
68
|
-
import type { Settings, ColorModeConfig, LocaleConfig, MetaTagsConfig, SiteHeadConfig, TagPlacement, TagGovernanceMode, VersionConfig, FooterConfig, HeaderNavItem, HeaderRightItem, HomeConfig, FrontmatterPreviewConfig, BodyFootUtilAreaConfig, HtmlPreviewConfig, FaviconConfig } from "./settings.js";
|
|
70
|
+
import type { Settings, ColorModeConfig, LocaleConfig, MetaTagsConfig, SiteHeadConfig, TagPlacement, TagGovernanceMode, VersionConfig, FooterConfig, HeaderNavItem, HeaderRightItem, HomeConfig, AssetViewerIndexingConfig, FrontmatterPreviewConfig, BodyFootUtilAreaConfig, HtmlPreviewConfig, FaviconConfig } from "./settings.js";
|
|
69
71
|
/** The `settings.claudeResources` block (or `false` when disabled). */
|
|
70
72
|
type ClaudeResourcesConfig = {
|
|
71
73
|
claudeDir: string;
|
|
@@ -188,6 +190,11 @@ export interface ZudoDocConfig {
|
|
|
188
190
|
* @default true
|
|
189
191
|
*/
|
|
190
192
|
mermaid?: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Enable transclusion of other Markdown/MDX files via `:::include`.
|
|
195
|
+
* @default false
|
|
196
|
+
*/
|
|
197
|
+
transclude?: boolean;
|
|
191
198
|
/**
|
|
192
199
|
* Add `noindex,nofollow` to every page (for internal docs).
|
|
193
200
|
* @default false
|
|
@@ -377,6 +384,8 @@ export interface ZudoDocConfig {
|
|
|
377
384
|
assetViewerExclude?: string[];
|
|
378
385
|
/** When `true` (and `assetViewer` is `true`), generate a listing page at `/<assetViewerRoutePrefix>/` showing every managed asset as a folder tree. No effect when `assetViewer` is `false`. @default false */
|
|
379
386
|
assetViewerIndex?: boolean;
|
|
387
|
+
/** Per-output opt-in controls for indexing generated asset-viewer pages. Omitted keys are off; `false` disables all asset-page indexing. @default false */
|
|
388
|
+
assetViewerIndexing?: AssetViewerIndexingConfig | false;
|
|
380
389
|
/**
|
|
381
390
|
* Body-foot utility area (doc-history / view-source), or `false` to disable.
|
|
382
391
|
* @default false
|
package/dist/config.js
CHANGED
|
@@ -53,6 +53,7 @@ const DEFAULT_SETTINGS = {
|
|
|
53
53
|
defaultLocale: "en",
|
|
54
54
|
locales: {},
|
|
55
55
|
mermaid: true,
|
|
56
|
+
transclude: false,
|
|
56
57
|
noindex: false,
|
|
57
58
|
editUrl: false,
|
|
58
59
|
githubUrl: false,
|
|
@@ -98,6 +99,7 @@ const DEFAULT_SETTINGS = {
|
|
|
98
99
|
assetViewerRoutePrefix: "files",
|
|
99
100
|
assetViewerExclude: [],
|
|
100
101
|
assetViewerIndex: false,
|
|
102
|
+
assetViewerIndexing: false,
|
|
101
103
|
bodyFootUtilArea: false,
|
|
102
104
|
htmlPreview: void 0,
|
|
103
105
|
versions: false,
|
|
@@ -20,7 +20,7 @@ function createDocContentHeader(ctx) {
|
|
|
20
20
|
version
|
|
21
21
|
}) {
|
|
22
22
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
23
|
-
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs", children: entry.data.title }),
|
|
23
|
+
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs break-words", children: entry.data.title }),
|
|
24
24
|
docContentHeaderExtras?.({
|
|
25
25
|
entry,
|
|
26
26
|
slug,
|
|
@@ -176,7 +176,7 @@ function createDocPageShell(ctx) {
|
|
|
176
176
|
Fragment (not <div>) so children become direct children of
|
|
177
177
|
<article class="zd-content">, picking up the flow-space rule. */
|
|
178
178
|
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
179
|
-
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs", children: autoIndexLabel }),
|
|
179
|
+
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs break-words", children: autoIndexLabel }),
|
|
180
180
|
metainfoSlot,
|
|
181
181
|
description && /* @__PURE__ */ jsx("p", { class: "mb-vsp-lg text-title text-muted", "data-doc-description": true, children: description }),
|
|
182
182
|
/* @__PURE__ */ jsx(NavCardGrid, { children: autoIndexChildren ?? [] })
|
package/dist/home-page/index.js
CHANGED
|
@@ -117,7 +117,7 @@ function createHomePageView(ctx) {
|
|
|
117
117
|
}
|
|
118
118
|
) : null,
|
|
119
119
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
120
|
-
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs", children: settings.siteName }),
|
|
120
|
+
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs break-words", children: settings.siteName }),
|
|
121
121
|
/* @__PURE__ */ jsx("p", { class: "text-muted text-small mb-vsp-sm", children: settings.siteDescription }),
|
|
122
122
|
/* @__PURE__ */ jsx("div", { class: "flex items-center justify-center lg:justify-start gap-hsp-md text-small", children: rowItems.map((item, index) => /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
123
123
|
index > 0 && /* @__PURE__ */ jsx("span", { class: "text-muted", children: "/" }),
|
|
@@ -3,7 +3,7 @@ import { ChevronRight } from "../icons/index.js";
|
|
|
3
3
|
function VersionsPageContent(props) {
|
|
4
4
|
const { latestHref, versions, labels } = props;
|
|
5
5
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6
|
-
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-lg", children: labels.pageTitle }),
|
|
6
|
+
/* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-lg break-words", children: labels.pageTitle }),
|
|
7
7
|
/* @__PURE__ */ jsxs("section", { class: "mb-vsp-xl", children: [
|
|
8
8
|
/* @__PURE__ */ jsx("h2", { class: "text-title font-bold mb-vsp-xs", children: labels.latestTitle }),
|
|
9
9
|
/* @__PURE__ */ jsx("p", { class: "text-small text-muted mb-vsp-sm", children: labels.latestDescription }),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AssetViewerIndexingConfig } from "../../../settings.js";
|
|
2
|
+
/** The serialized asset-viewer settings shared by index consumers. */
|
|
3
|
+
export interface AssetScanProjection {
|
|
4
|
+
assetViewer: boolean;
|
|
5
|
+
assetViewerIndexing: AssetViewerIndexingConfig | false;
|
|
6
|
+
assetViewerDir: string;
|
|
7
|
+
assetViewerRoutePrefix: string;
|
|
8
|
+
assetViewerExclude: readonly string[];
|
|
9
|
+
base: string;
|
|
10
|
+
/** Non-default locales, keyed by locale code. */
|
|
11
|
+
locales: Record<string, {
|
|
12
|
+
dir?: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** Route prefixes whose generated pages exist only in the default locale. */
|
|
15
|
+
defaultLocaleOnlyPrefixes: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/** The asset-page output currently supported by a consumer. */
|
|
18
|
+
export type AssetIndexConsumer = keyof AssetViewerIndexingConfig;
|
|
19
|
+
/** Minimal normalized descriptor consumed by search, llms.txt, and sitemap. */
|
|
20
|
+
export interface AssetPageDescriptor {
|
|
21
|
+
/** NFC-normalized POSIX path relative to `assetViewerDir`. */
|
|
22
|
+
path: string;
|
|
23
|
+
/** Base-prefixed, trailing-slashed generated asset-viewer URL. */
|
|
24
|
+
url: string;
|
|
25
|
+
/** Non-default locale segment; omitted for the default locale. */
|
|
26
|
+
locale?: string;
|
|
27
|
+
/** Whether the asset has a text-like extension understood by the viewer. */
|
|
28
|
+
isText: boolean;
|
|
29
|
+
/** Current byte size of the public asset. */
|
|
30
|
+
size: number;
|
|
31
|
+
}
|
|
32
|
+
export interface AssetPageDescriptorOptions {
|
|
33
|
+
/** Runtime project root injected by the zfb plugin wrapper. */
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
/** Serialized projection from `zudoDocPreset()`. */
|
|
36
|
+
assetScan?: AssetScanProjection;
|
|
37
|
+
/** The output whose explicit indexing gate must be enabled. */
|
|
38
|
+
consumer: AssetIndexConsumer;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Scan the public asset directory and materialize one descriptor per emitted
|
|
42
|
+
* asset page. The default-locale descriptor is emitted first, followed by
|
|
43
|
+
* configured locales in object iteration order. Every descriptor URL is
|
|
44
|
+
* minted by `assetViewerHref()` so encoding, base, and trailing-slash rules
|
|
45
|
+
* cannot diverge between consumers.
|
|
46
|
+
*
|
|
47
|
+
* The descriptor path is intentionally synchronous. Search and llms.txt are
|
|
48
|
+
* synchronous build/dev paths, and `connect-adapter.ts` does not await a
|
|
49
|
+
* middleware's returned promise. Callers must pass the runtime `projectRoot`
|
|
50
|
+
* supplied by the plugin wrapper; it is never part of the serialized preset
|
|
51
|
+
* projection.
|
|
52
|
+
*/
|
|
53
|
+
export declare function collectAssetPageDescriptors(options: AssetPageDescriptorOptions): AssetPageDescriptor[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
|
+
import { assetViewerHref } from "../../../asset-path/index.js";
|
|
4
|
+
import { scanAssetsSync } from "./scan.js";
|
|
5
|
+
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6
|
+
"js",
|
|
7
|
+
"ts",
|
|
8
|
+
"tsx",
|
|
9
|
+
"jsx",
|
|
10
|
+
"mjs",
|
|
11
|
+
"cjs",
|
|
12
|
+
"json",
|
|
13
|
+
"yaml",
|
|
14
|
+
"yml",
|
|
15
|
+
"toml",
|
|
16
|
+
"css",
|
|
17
|
+
"scss",
|
|
18
|
+
"html",
|
|
19
|
+
"md",
|
|
20
|
+
"mdx",
|
|
21
|
+
"sh",
|
|
22
|
+
"bash",
|
|
23
|
+
"py",
|
|
24
|
+
"rb",
|
|
25
|
+
"go",
|
|
26
|
+
"rs",
|
|
27
|
+
"java",
|
|
28
|
+
"kt",
|
|
29
|
+
"swift",
|
|
30
|
+
"c",
|
|
31
|
+
"h",
|
|
32
|
+
"cpp",
|
|
33
|
+
"hpp",
|
|
34
|
+
"cs",
|
|
35
|
+
"php",
|
|
36
|
+
"sql",
|
|
37
|
+
"xml",
|
|
38
|
+
"txt",
|
|
39
|
+
"log",
|
|
40
|
+
"csv",
|
|
41
|
+
"tsv",
|
|
42
|
+
"env",
|
|
43
|
+
"conf",
|
|
44
|
+
"ini"
|
|
45
|
+
]);
|
|
46
|
+
function hasBinaryPrefix(absPath) {
|
|
47
|
+
const fd = openSync(absPath, "r");
|
|
48
|
+
try {
|
|
49
|
+
const sample = Buffer.allocUnsafe(8 * 1024);
|
|
50
|
+
const bytesRead = readSync(fd, sample, 0, sample.length, 0);
|
|
51
|
+
return sample.subarray(0, bytesRead).includes(0);
|
|
52
|
+
} finally {
|
|
53
|
+
closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isTextAsset(absPath, path) {
|
|
57
|
+
const extension = extname(path).slice(1).toLowerCase();
|
|
58
|
+
if (extension.length !== 0 && !TEXT_EXTENSIONS.has(extension)) return false;
|
|
59
|
+
return !hasBinaryPrefix(absPath);
|
|
60
|
+
}
|
|
61
|
+
function isDefaultLocaleOnlyAsset(routePrefix, path, prefixes) {
|
|
62
|
+
const routePath = assetViewerHref({ base: "/", routePrefix, path });
|
|
63
|
+
return prefixes.some((prefix) => routePath.startsWith(prefix));
|
|
64
|
+
}
|
|
65
|
+
function hasConsumerGate(assetScan, consumer) {
|
|
66
|
+
const indexing = assetScan.assetViewerIndexing;
|
|
67
|
+
return assetScan.assetViewer === true && indexing !== false && typeof indexing === "object" && indexing !== null && indexing[consumer] === true;
|
|
68
|
+
}
|
|
69
|
+
function collectAssetPageDescriptors(options) {
|
|
70
|
+
const { projectRoot, assetScan, consumer } = options;
|
|
71
|
+
if (assetScan === void 0 || !hasConsumerGate(assetScan, consumer)) return [];
|
|
72
|
+
const dir = assetScan.assetViewerDir;
|
|
73
|
+
const routePrefix = assetScan.assetViewerRoutePrefix;
|
|
74
|
+
const base = assetScan.base;
|
|
75
|
+
const exclude = assetScan.assetViewerExclude ?? [];
|
|
76
|
+
const paths = scanAssetsSync(projectRoot, dir, exclude);
|
|
77
|
+
const assetRoot = resolve(projectRoot, "public", dir);
|
|
78
|
+
const descriptorFor = (path, locale) => ({
|
|
79
|
+
path,
|
|
80
|
+
url: assetViewerHref({ base, routePrefix, path, ...locale === void 0 ? {} : { locale } }),
|
|
81
|
+
...locale === void 0 ? {} : { locale },
|
|
82
|
+
isText: isTextAsset(resolve(assetRoot, path), path),
|
|
83
|
+
size: statSync(resolve(assetRoot, path)).size
|
|
84
|
+
});
|
|
85
|
+
const descriptors = paths.map((path) => descriptorFor(path));
|
|
86
|
+
for (const locale of Object.keys(assetScan.locales ?? {})) {
|
|
87
|
+
for (const path of paths) {
|
|
88
|
+
if (isDefaultLocaleOnlyAsset(routePrefix, path, assetScan.defaultLocaleOnlyPrefixes ?? [])) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
descriptors.push(descriptorFor(path, locale));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return descriptors;
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
collectAssetPageDescriptors
|
|
98
|
+
};
|
|
@@ -1,2 +1,19 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Recursively enumerate safe, public assets in deterministic path order.
|
|
3
|
+
*
|
|
4
|
+
* This synchronous implementation is the canonical walk. Search and llms.txt
|
|
5
|
+
* build/dev consumers are synchronous, while the routes setup hook already
|
|
6
|
+
* exposes an async `scanAssets()` contract. Keeping one walk here gives both
|
|
7
|
+
* boundaries identical symlink, exclusion, and collision behavior.
|
|
8
|
+
*/
|
|
9
|
+
export declare function scanAssetsSync(projectRoot: string, dir: string, exclude?: readonly string[]): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Async-compatible asset scan used by the routes setup hook.
|
|
12
|
+
*
|
|
13
|
+
* The route plugin's public lifecycle contract is async, but its scan itself
|
|
14
|
+
* is only filesystem enumeration. Delegating to the canonical sync walk keeps
|
|
15
|
+
* it behaviorally identical to `scanAssetsSync()` without introducing a
|
|
16
|
+
* second implementation (or a promise that synchronous middleware cannot
|
|
17
|
+
* consume).
|
|
18
|
+
*/
|
|
2
19
|
export declare function scanAssets(projectRoot: string, dir: string, exclude?: readonly string[]): Promise<string[]>;
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { matchExclude, normalizeAssetPath } from "../../../asset-path/index.js";
|
|
4
4
|
function isWithin(root, candidate) {
|
|
5
5
|
const rel = relative(root, candidate);
|
|
6
6
|
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== "..";
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
function finishScan(paths) {
|
|
9
|
+
paths.sort((a, b) => a.localeCompare(b, "en"));
|
|
10
|
+
const folded = /* @__PURE__ */ new Map();
|
|
11
|
+
for (const path of paths) {
|
|
12
|
+
const key = path.toLocaleLowerCase("en-US");
|
|
13
|
+
const existing = folded.get(key);
|
|
14
|
+
if (existing !== void 0) {
|
|
15
|
+
throw new Error(`[asset-viewer] case-insensitive or Unicode-normalized URL collision: ${existing} and ${path}`);
|
|
16
|
+
}
|
|
17
|
+
folded.set(key, path);
|
|
18
|
+
if (path === "client" || path.startsWith("client/")) {
|
|
19
|
+
console.warn(`[asset-viewer] reserved client/ asset path: ${path}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return paths;
|
|
23
|
+
}
|
|
24
|
+
function scanAssetsSync(projectRoot, dir, exclude = []) {
|
|
9
25
|
const publicRoot = resolve(projectRoot, "public");
|
|
10
26
|
const assetRoot = resolve(publicRoot, dir);
|
|
11
27
|
if (!isWithin(publicRoot, assetRoot)) {
|
|
@@ -13,8 +29,8 @@ async function scanAssets(projectRoot, dir, exclude = []) {
|
|
|
13
29
|
}
|
|
14
30
|
let canonicalRoot;
|
|
15
31
|
try {
|
|
16
|
-
const canonicalPublicRoot =
|
|
17
|
-
canonicalRoot =
|
|
32
|
+
const canonicalPublicRoot = realpathSync(publicRoot);
|
|
33
|
+
canonicalRoot = realpathSync(assetRoot);
|
|
18
34
|
if (!isWithin(canonicalPublicRoot, canonicalRoot)) {
|
|
19
35
|
throw new Error(`[asset-viewer] asset directory symlink escapes public/: ${dir}`);
|
|
20
36
|
}
|
|
@@ -23,29 +39,29 @@ async function scanAssets(projectRoot, dir, exclude = []) {
|
|
|
23
39
|
throw error;
|
|
24
40
|
}
|
|
25
41
|
const paths = [];
|
|
26
|
-
|
|
27
|
-
const canonicalDir =
|
|
42
|
+
function walk(absDir, relDir, canonicalAncestors) {
|
|
43
|
+
const canonicalDir = realpathSync(absDir);
|
|
28
44
|
if (!isWithin(canonicalRoot, canonicalDir)) {
|
|
29
45
|
throw new Error(`[asset-viewer] symlink escapes public/${dir}: ${relDir || basename(absDir)}`);
|
|
30
46
|
}
|
|
31
47
|
if (canonicalAncestors.has(canonicalDir)) return;
|
|
32
48
|
const nextAncestors = new Set(canonicalAncestors).add(canonicalDir);
|
|
33
|
-
const entries =
|
|
49
|
+
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
34
50
|
entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
35
51
|
for (const entry of entries) {
|
|
36
52
|
if (entry.name.startsWith(".")) continue;
|
|
37
53
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
38
54
|
if (entry.name.endsWith(".meta.json")) continue;
|
|
39
55
|
const absPath = resolve(absDir, entry.name);
|
|
40
|
-
const canonical =
|
|
56
|
+
const canonical = realpathSync(absPath);
|
|
41
57
|
if (!isWithin(canonicalRoot, canonical)) {
|
|
42
58
|
throw new Error(`[asset-viewer] symlink escapes public/${dir}: ${relPath}`);
|
|
43
59
|
}
|
|
44
60
|
if (entry.isDirectory()) {
|
|
45
|
-
|
|
61
|
+
walk(absPath, relPath, nextAncestors);
|
|
46
62
|
} else if (entry.isSymbolicLink()) {
|
|
47
|
-
const target =
|
|
48
|
-
if (target.isDirectory())
|
|
63
|
+
const target = statSync(canonical);
|
|
64
|
+
if (target.isDirectory()) walk(absPath, relPath, nextAncestors);
|
|
49
65
|
else {
|
|
50
66
|
const path = normalizeAssetPath(relPath);
|
|
51
67
|
if (!matchExclude(path, exclude)) paths.push(path);
|
|
@@ -56,22 +72,13 @@ async function scanAssets(projectRoot, dir, exclude = []) {
|
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
74
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const existing = folded.get(key);
|
|
65
|
-
if (existing !== void 0) {
|
|
66
|
-
throw new Error(`[asset-viewer] case-insensitive or Unicode-normalized URL collision: ${existing} and ${path}`);
|
|
67
|
-
}
|
|
68
|
-
folded.set(key, path);
|
|
69
|
-
if (path === "client" || path.startsWith("client/")) {
|
|
70
|
-
console.warn(`[asset-viewer] reserved client/ asset path: ${path}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return paths;
|
|
75
|
+
walk(assetRoot, "", /* @__PURE__ */ new Set());
|
|
76
|
+
return finishScan(paths);
|
|
77
|
+
}
|
|
78
|
+
async function scanAssets(projectRoot, dir, exclude = []) {
|
|
79
|
+
return scanAssetsSync(projectRoot, dir, exclude);
|
|
74
80
|
}
|
|
75
81
|
export {
|
|
76
|
-
scanAssets
|
|
82
|
+
scanAssets,
|
|
83
|
+
scanAssetsSync
|
|
77
84
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize the shared asset-viewer descriptors for llms.txt output.
|
|
3
|
+
*
|
|
4
|
+
* The descriptor scanner owns the asset-viewer/indexing gates and URL rules;
|
|
5
|
+
* this adapter only reads the bounded text body needed by llms-full.txt.
|
|
6
|
+
* Keeping it separate from the emitter lets build and dev use precisely the
|
|
7
|
+
* same data preparation path.
|
|
8
|
+
*/
|
|
9
|
+
import { type AssetScanProjection } from "../asset-viewer/asset-pages.js";
|
|
10
|
+
import { type LlmsAssetEntry } from "./types.js";
|
|
11
|
+
export interface LlmsAssetLoadOptions {
|
|
12
|
+
/** Runtime project root supplied by the zfb plugin wrapper. */
|
|
13
|
+
projectRoot?: string;
|
|
14
|
+
/** Shared serialized asset-viewer projection from the preset. */
|
|
15
|
+
assetScan?: AssetScanProjection;
|
|
16
|
+
/** Optional canonical site origin, matching document-entry URL behavior. */
|
|
17
|
+
siteUrl?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Collect and materialize all llms-indexed assets. Missing runtime inputs are
|
|
21
|
+
* treated as the legacy no-assets case so direct unit callers and consumers
|
|
22
|
+
* that do not run through zfb remain byte-compatible.
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadLlmsAssetEntries(options: LlmsAssetLoadOptions): LlmsAssetEntry[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
import {
|
|
5
|
+
collectAssetPageDescriptors
|
|
6
|
+
} from "../asset-viewer/asset-pages.js";
|
|
7
|
+
import {
|
|
8
|
+
LLMS_ASSET_TEXT_CAP_BYTES
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
function readTextPrefix(filePath) {
|
|
11
|
+
const sample = Buffer.alloc(LLMS_ASSET_TEXT_CAP_BYTES + 1);
|
|
12
|
+
const fd = openSync(filePath, "r");
|
|
13
|
+
try {
|
|
14
|
+
let offset = 0;
|
|
15
|
+
while (offset < sample.length) {
|
|
16
|
+
const bytesRead = readSync(fd, sample, offset, sample.length - offset, offset);
|
|
17
|
+
if (bytesRead === 0) break;
|
|
18
|
+
offset += bytesRead;
|
|
19
|
+
}
|
|
20
|
+
const decoder = new StringDecoder("utf8");
|
|
21
|
+
return {
|
|
22
|
+
content: decoder.write(sample.subarray(0, offset)),
|
|
23
|
+
truncated: offset > LLMS_ASSET_TEXT_CAP_BYTES
|
|
24
|
+
};
|
|
25
|
+
} finally {
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function toLlmsAssetEntry(descriptor, projectRoot, assetScan, siteUrl) {
|
|
30
|
+
const url = siteUrl ? `${siteUrl.replace(/\/$/, "")}${descriptor.url}` : descriptor.url;
|
|
31
|
+
if (!descriptor.isText) {
|
|
32
|
+
return {
|
|
33
|
+
path: descriptor.path,
|
|
34
|
+
url,
|
|
35
|
+
...descriptor.locale === void 0 ? {} : { locale: descriptor.locale },
|
|
36
|
+
isText: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const body = readTextPrefix(
|
|
40
|
+
resolve(projectRoot, "public", assetScan.assetViewerDir, descriptor.path)
|
|
41
|
+
);
|
|
42
|
+
return {
|
|
43
|
+
path: descriptor.path,
|
|
44
|
+
url,
|
|
45
|
+
...descriptor.locale === void 0 ? {} : { locale: descriptor.locale },
|
|
46
|
+
isText: true,
|
|
47
|
+
content: body.content,
|
|
48
|
+
truncated: body.truncated
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function loadLlmsAssetEntries(options) {
|
|
52
|
+
const { projectRoot, assetScan, siteUrl } = options;
|
|
53
|
+
if (projectRoot === void 0 || assetScan === void 0) return [];
|
|
54
|
+
const descriptors = collectAssetPageDescriptors({
|
|
55
|
+
projectRoot,
|
|
56
|
+
assetScan,
|
|
57
|
+
consumer: "llmsTxt"
|
|
58
|
+
});
|
|
59
|
+
return descriptors.map(
|
|
60
|
+
(descriptor) => toLlmsAssetEntry(descriptor, projectRoot, assetScan, siteUrl)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
loadLlmsAssetEntries
|
|
65
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { LlmsTxtLocaleConfig, LlmsTxtSiteMeta } from "./types.js";
|
|
3
|
+
import type { AssetScanProjection } from "../asset-viewer/asset-pages.js";
|
|
3
4
|
/** Connect-style middleware signature — works as a Vite plugin middleware. */
|
|
4
5
|
export type LlmsTxtNextFn = (err?: unknown) => void;
|
|
5
6
|
export type LlmsTxtMiddleware = (req: IncomingMessage, res: ServerResponse, next: LlmsTxtNextFn) => void;
|
|
@@ -22,6 +23,10 @@ export interface LlmsTxtDevMiddlewareOptions extends LlmsTxtSiteMeta {
|
|
|
22
23
|
defaultLocaleDir: string;
|
|
23
24
|
/** Additional locales (e.g. `[{ code: "ja", dir: "src/content/docs-ja" }]`). */
|
|
24
25
|
locales?: LlmsTxtLocaleConfig[];
|
|
26
|
+
/** Runtime project root; injected by the zfb plugin wrapper, never serialized in the preset. */
|
|
27
|
+
projectRoot?: string;
|
|
28
|
+
/** Shared asset-viewer projection; consumed by the asset indexing wave. */
|
|
29
|
+
assetScan?: AssetScanProjection;
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* Build a dev-server middleware that responds to GET requests for
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadLlmsAssetEntries } from "./assets.js";
|
|
1
2
|
import { generateLlmsFullTxt, generateLlmsTxt } from "./generate.js";
|
|
2
3
|
import { loadDocEntries } from "./load.js";
|
|
3
4
|
const LLMS_KIND_PATTERN = /^(?:\/(.+?))?\/(llms|llms-full)\.txt$/;
|
|
@@ -39,7 +40,14 @@ function createLlmsTxtDevMiddleware(options, logger) {
|
|
|
39
40
|
base,
|
|
40
41
|
siteUrl
|
|
41
42
|
});
|
|
42
|
-
const
|
|
43
|
+
const assets = loadLlmsAssetEntries({
|
|
44
|
+
projectRoot: options.projectRoot,
|
|
45
|
+
assetScan: options.assetScan,
|
|
46
|
+
siteUrl
|
|
47
|
+
}).filter(
|
|
48
|
+
(asset) => match.locale === null ? asset.locale === void 0 : asset.locale === match.locale
|
|
49
|
+
);
|
|
50
|
+
const body = match.kind === "llms" ? generateLlmsTxt(entries, meta, assets) : generateLlmsFullTxt(entries, meta, assets);
|
|
43
51
|
res.statusCode = 200;
|
|
44
52
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
45
53
|
res.end(body);
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { generateLlmsFullTxt, generateLlmsTxt } from "./generate.js";
|
|
4
|
+
import { loadLlmsAssetEntries } from "./assets.js";
|
|
4
5
|
import { loadDocEntries } from "./load.js";
|
|
6
|
+
function assetsForLocale(assets, locale) {
|
|
7
|
+
return assets.filter(
|
|
8
|
+
(asset) => locale === null ? asset.locale === void 0 : asset.locale === locale
|
|
9
|
+
);
|
|
10
|
+
}
|
|
5
11
|
function emitLlmsTxt(options) {
|
|
6
12
|
const {
|
|
7
13
|
outDir,
|
|
@@ -11,10 +17,13 @@ function emitLlmsTxt(options) {
|
|
|
11
17
|
siteDescription,
|
|
12
18
|
defaultLocaleDir,
|
|
13
19
|
locales = [],
|
|
14
|
-
logger
|
|
20
|
+
logger,
|
|
21
|
+
projectRoot,
|
|
22
|
+
assetScan
|
|
15
23
|
} = options;
|
|
16
24
|
const meta = { siteName, siteDescription };
|
|
17
25
|
const written = [];
|
|
26
|
+
const assetEntries = loadLlmsAssetEntries({ projectRoot, assetScan, siteUrl });
|
|
18
27
|
const defaultEntries = loadDocEntries({
|
|
19
28
|
contentDir: defaultLocaleDir,
|
|
20
29
|
locale: null,
|
|
@@ -24,8 +33,14 @@ function emitLlmsTxt(options) {
|
|
|
24
33
|
mkdirSync(outDir, { recursive: true });
|
|
25
34
|
const defaultIndexPath = join(outDir, "llms.txt");
|
|
26
35
|
const defaultFullPath = join(outDir, "llms-full.txt");
|
|
27
|
-
writeFileSync(
|
|
28
|
-
|
|
36
|
+
writeFileSync(
|
|
37
|
+
defaultIndexPath,
|
|
38
|
+
generateLlmsTxt(defaultEntries, meta, assetsForLocale(assetEntries, null))
|
|
39
|
+
);
|
|
40
|
+
writeFileSync(
|
|
41
|
+
defaultFullPath,
|
|
42
|
+
generateLlmsFullTxt(defaultEntries, meta, assetsForLocale(assetEntries, null))
|
|
43
|
+
);
|
|
29
44
|
written.push(defaultIndexPath, defaultFullPath);
|
|
30
45
|
logger?.info(
|
|
31
46
|
`Generated llms.txt and llms-full.txt (${defaultEntries.length} pages)`
|
|
@@ -41,8 +56,9 @@ function emitLlmsTxt(options) {
|
|
|
41
56
|
mkdirSync(localeDir, { recursive: true });
|
|
42
57
|
const indexPath = join(localeDir, "llms.txt");
|
|
43
58
|
const fullPath = join(localeDir, "llms-full.txt");
|
|
44
|
-
|
|
45
|
-
writeFileSync(
|
|
59
|
+
const localeAssets = assetsForLocale(assetEntries, code);
|
|
60
|
+
writeFileSync(indexPath, generateLlmsTxt(localeEntries, meta, localeAssets));
|
|
61
|
+
writeFileSync(fullPath, generateLlmsFullTxt(localeEntries, meta, localeAssets));
|
|
46
62
|
written.push(indexPath, fullPath);
|
|
47
63
|
logger?.info(
|
|
48
64
|
`Generated ${code}/llms.txt and ${code}/llms-full.txt (${localeEntries.length} pages)`
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
* Output format must remain byte-identical to the legacy Astro emitter
|
|
10
10
|
* — the file shape is part of the project's public surface.
|
|
11
11
|
*/
|
|
12
|
-
import type
|
|
12
|
+
import { type LlmsAssetEntry, type LlmsDocEntry, type LlmsTxtSiteMeta } from "./types.js";
|
|
13
13
|
/**
|
|
14
14
|
* Slim index — site header followed by a markdown bullet per page with
|
|
15
15
|
* `[title](url): description`.
|
|
16
16
|
*/
|
|
17
|
-
export declare function generateLlmsTxt(entries: readonly LlmsDocEntry[], meta: LlmsTxtSiteMeta): string;
|
|
17
|
+
export declare function generateLlmsTxt(entries: readonly LlmsDocEntry[], meta: LlmsTxtSiteMeta, assets?: readonly LlmsAssetEntry[]): string;
|
|
18
18
|
/**
|
|
19
19
|
* Full content concatenation — site header followed by every page's
|
|
20
20
|
* stripped body separated by `---` rules.
|
|
21
21
|
*/
|
|
22
|
-
export declare function generateLlmsFullTxt(entries: readonly LlmsDocEntry[], meta: LlmsTxtSiteMeta): string;
|
|
22
|
+
export declare function generateLlmsFullTxt(entries: readonly LlmsDocEntry[], meta: LlmsTxtSiteMeta, assets?: readonly LlmsAssetEntry[]): string;
|