@takazudo/zudo-doc 0.2.22 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/zudo-doc.mjs +83 -0
- package/dist/eject/index.d.ts +33 -0
- package/dist/eject/index.js +229 -0
- package/dist/plugins/routes.d.ts +2 -0
- package/dist/plugins/routes.js +100 -0
- package/dist/preset.d.ts +47 -1
- package/dist/preset.js +29 -13
- package/dist/routes/404.d.ts +7 -0
- package/dist/routes/404.js +47 -0
- package/dist/routes/_chrome.d.ts +32 -0
- package/dist/routes/_chrome.js +365 -0
- package/dist/routes/_context.d.ts +56 -0
- package/dist/routes/_context.js +222 -0
- package/dist/routes/_docs-helpers.d.ts +43 -0
- package/dist/routes/_docs-helpers.js +173 -0
- package/dist/routes/_virtual.d.js +0 -0
- package/dist/routes/api-ai-chat.d.ts +5 -0
- package/dist/routes/api-ai-chat.js +24 -0
- package/dist/routes/docs-slug.d.ts +21 -0
- package/dist/routes/docs-slug.js +26 -0
- package/dist/routes/docs-tags-index.d.ts +7 -0
- package/dist/routes/docs-tags-index.js +11 -0
- package/dist/routes/docs-tags-tag.d.ts +23 -0
- package/dist/routes/docs-tags-tag.js +19 -0
- package/dist/routes/docs-versions.d.ts +7 -0
- package/dist/routes/docs-versions.js +11 -0
- package/dist/routes/index.d.ts +7 -0
- package/dist/routes/index.js +117 -0
- package/dist/routes/locale-docs-slug.d.ts +27 -0
- package/dist/routes/locale-docs-slug.js +40 -0
- package/dist/routes/locale-docs-tags-index.d.ts +18 -0
- package/dist/routes/locale-docs-tags-index.js +15 -0
- package/dist/routes/locale-docs-tags-tag.d.ts +25 -0
- package/dist/routes/locale-docs-tags-tag.js +22 -0
- package/dist/routes/locale-docs-versions.d.ts +18 -0
- package/dist/routes/locale-docs-versions.js +15 -0
- package/dist/routes/locale-index.d.ts +24 -0
- package/dist/routes/locale-index.js +132 -0
- package/dist/routes/robots.txt.d.ts +3 -0
- package/dist/routes/robots.txt.js +8 -0
- package/dist/routes/sitemap.xml.d.ts +5 -0
- package/dist/routes/sitemap.xml.js +31 -0
- package/dist/routes/v-docs-slug.d.ts +27 -0
- package/dist/routes/v-docs-slug.js +34 -0
- package/dist/routes/v-locale-docs-slug.d.ts +31 -0
- package/dist/routes/v-locale-docs-slug.js +50 -0
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +15 -0
- package/eject/breadcrumb/breadcrumb.tsx +194 -0
- package/eject/breadcrumb/find-path.ts +26 -0
- package/eject/breadcrumb/index.ts +4 -0
- package/eject/breadcrumb/types.ts +32 -0
- package/eject/code-group/index.tsx +78 -0
- package/eject/content-admonition/index.tsx +50 -0
- package/eject/details/details.tsx +34 -0
- package/eject/details/index.ts +4 -0
- package/eject/doc-pager/index.tsx +91 -0
- package/eject/footer/footer.tsx +167 -0
- package/eject/footer/index.ts +19 -0
- package/eject/footer/types.ts +60 -0
- package/eject/header/header.tsx +725 -0
- package/eject/header/index.ts +36 -0
- package/eject/header/nav-active.ts +122 -0
- package/eject/header/nav-overflow-script.ts +302 -0
- package/eject/header/right-items.ts +78 -0
- package/eject/header/types.ts +69 -0
- package/eject/page-loading/index.ts +10 -0
- package/eject/page-loading/page-loading-overlay.tsx +103 -0
- package/eject/sidebar/index.ts +27 -0
- package/eject/sidebar/sidebar.tsx +137 -0
- package/eject/sidebar/types.ts +73 -0
- package/eject/tab-item/index.ts +7 -0
- package/eject/tab-item/tab-item.tsx +70 -0
- package/eject/theme-toggle/color-scheme-sync.ts +73 -0
- package/eject/theme-toggle/index.tsx +120 -0
- package/eject/toc/cx.ts +38 -0
- package/eject/toc/index.ts +11 -0
- package/eject/toc/mobile-toc.tsx +133 -0
- package/eject/toc/toc-title.ts +33 -0
- package/eject/toc/toc.tsx +119 -0
- package/eject/toc/types.ts +14 -0
- package/eject/toc/use-active-heading.ts +162 -0
- package/package.json +87 -7
- package/routes-src/404.tsx +49 -0
- package/routes-src/_chrome.tsx +470 -0
- package/routes-src/_context.ts +314 -0
- package/routes-src/_docs-helpers.ts +284 -0
- package/routes-src/_virtual.d.ts +15 -0
- package/routes-src/api-ai-chat.tsx +46 -0
- package/routes-src/docs-slug.tsx +43 -0
- package/routes-src/docs-tags-index.tsx +14 -0
- package/routes-src/docs-tags-tag.tsx +29 -0
- package/routes-src/docs-versions.tsx +15 -0
- package/routes-src/index.tsx +134 -0
- package/routes-src/locale-docs-slug.tsx +69 -0
- package/routes-src/locale-docs-tags-index.tsx +23 -0
- package/routes-src/locale-docs-tags-tag.tsx +39 -0
- package/routes-src/locale-docs-versions.tsx +23 -0
- package/routes-src/locale-index.tsx +150 -0
- package/routes-src/robots.txt.tsx +13 -0
- package/routes-src/sitemap.xml.tsx +48 -0
- package/routes-src/v-docs-slug.tsx +64 -0
- package/routes-src/v-locale-docs-slug.tsx +85 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// routes/_context — the route-context reconstruction seam (epic Package-First
|
|
2
|
+
// Finale #2356, A1 #2361; ADR docs/adr/route-injection-seam.md, Decision 1).
|
|
3
|
+
//
|
|
4
|
+
// Every package route entrypoint imports `{ routeContext }` from the virtual
|
|
5
|
+
// module `virtual:zudo-doc-route-context` (SERIALIZABLE DATA ONLY: `settings`,
|
|
6
|
+
// `translations`, `tagVocabulary`) and rebuilds the full injected context by
|
|
7
|
+
// calling the ALREADY-EXISTING package factories with importable package
|
|
8
|
+
// subpaths — never raw host functions, never `@/` aliases. This module does
|
|
9
|
+
// that reconstruction ONCE and exposes the wired runtime context so each
|
|
10
|
+
// entrypoint stays a thin `paths()` + default-export shell (mirroring the kept
|
|
11
|
+
// `pages/*.tsx` stubs).
|
|
12
|
+
//
|
|
13
|
+
// The host-dep → package-subpath map this wires (ADR table):
|
|
14
|
+
// buildNavTree/collectAutoIndexNodes/groupSatelliteNodes/isNavVisible/
|
|
15
|
+
// findNode/firstRoutedHref/buildBreadcrumbs ... ./routes/_docs-helpers
|
|
16
|
+
// (the package `docs` helpers, on `@takazudo/zudo-doc/sidebar-tree`)
|
|
17
|
+
// loadCategoryMeta ........................... @takazudo/zudo-doc/sidebar-tree
|
|
18
|
+
// extractHeadings ............................ @takazudo/zudo-doc/extract-headings
|
|
19
|
+
// toRouteSlug/toSlugParams/toTitleCase/toHistorySlug ... @takazudo/zudo-doc/slug
|
|
20
|
+
// resolveTag / collectTags ................... @takazudo/zudo-doc/tag-helpers
|
|
21
|
+
// getNavSectionForSlug/getNavSubtree/getCategoryOrder .. @takazudo/zudo-doc/nav-scope
|
|
22
|
+
// url helpers ................................ @takazudo/zudo-doc/url-helpers
|
|
23
|
+
// mergeLocaleDocs ............................ @takazudo/zudo-doc/locale-merge
|
|
24
|
+
// stableDocs (content bridge) ................ ./routes/_docs-helpers (zfb/content)
|
|
25
|
+
// resolveNavSource/… ......................... @takazudo/zudo-doc/nav-source-docs
|
|
26
|
+
// buildDocRouteEntries ....................... @takazudo/zudo-doc/doc-route-entries
|
|
27
|
+
// route enumeration .......................... @takazudo/zudo-doc/route-enumerators
|
|
28
|
+
// i18n (t/getLocaleLabel/defaultLocale/locales) reconstructed from settings +
|
|
29
|
+
// translations as a FactoryI18n.
|
|
30
|
+
|
|
31
|
+
// Virtual module, resolved by the routes plugin at build via
|
|
32
|
+
// addVirtualModule("virtual:zudo-doc-route-context", …). Not present on disk;
|
|
33
|
+
// the package ships ambient typings for it (routes/_virtual.d.ts).
|
|
34
|
+
import { routeContext } from "virtual:zudo-doc-route-context";
|
|
35
|
+
|
|
36
|
+
import type { Settings } from "@takazudo/zudo-doc/settings";
|
|
37
|
+
import type { FactoryI18n } from "@takazudo/zudo-doc/factory-context";
|
|
38
|
+
import { makeUrlHelpers, type UrlHelpers } from "@takazudo/zudo-doc/url-helpers";
|
|
39
|
+
import {
|
|
40
|
+
loadCategoryMeta,
|
|
41
|
+
type CategoryMeta,
|
|
42
|
+
} from "@takazudo/zudo-doc/sidebar-tree";
|
|
43
|
+
import { extractHeadings as extractHeadingsBase } from "@takazudo/zudo-doc/extract-headings";
|
|
44
|
+
import { toRouteSlug, toSlugParams } from "@takazudo/zudo-doc/slug";
|
|
45
|
+
import { resolveTag, type TagVocabularyEntry } from "@takazudo/zudo-doc/tag-helpers";
|
|
46
|
+
import {
|
|
47
|
+
getCategoryOrder as getCategoryOrderBase,
|
|
48
|
+
getNavSectionForSlug as getNavSectionForSlugBase,
|
|
49
|
+
getNavSubtree as getNavSubtreeBase,
|
|
50
|
+
} from "@takazudo/zudo-doc/nav-scope";
|
|
51
|
+
import { mergeLocaleDocs } from "@takazudo/zudo-doc/locale-merge";
|
|
52
|
+
import {
|
|
53
|
+
createNavSourceDocs,
|
|
54
|
+
type NavSourceDocsAPI,
|
|
55
|
+
} from "@takazudo/zudo-doc/nav-source-docs";
|
|
56
|
+
import {
|
|
57
|
+
createDocRouteEntries,
|
|
58
|
+
type DocRouteEntriesAPI,
|
|
59
|
+
} from "@takazudo/zudo-doc/doc-route-entries";
|
|
60
|
+
import {
|
|
61
|
+
createRouteEnumerators,
|
|
62
|
+
type RouteEnumeratorsAPI,
|
|
63
|
+
type DocsEntryForTags,
|
|
64
|
+
type TagInfoForEnum,
|
|
65
|
+
} from "@takazudo/zudo-doc/route-enumerators";
|
|
66
|
+
import type { DocNavNode, DocPageEntry } from "@takazudo/zudo-doc/doc-page-props";
|
|
67
|
+
import type { HeadingItem } from "@takazudo/zudo-doc/extract-headings";
|
|
68
|
+
import {
|
|
69
|
+
stableDocs,
|
|
70
|
+
isNavVisible,
|
|
71
|
+
buildNavTree,
|
|
72
|
+
buildBreadcrumbs,
|
|
73
|
+
collectAutoIndexNodes,
|
|
74
|
+
groupSatelliteNodes,
|
|
75
|
+
findNode,
|
|
76
|
+
firstRoutedHref,
|
|
77
|
+
} from "./_docs-helpers.js";
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// The serializable route-context payload (re-exported for entrypoints).
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export interface RouteContextPayload {
|
|
84
|
+
settings: Settings;
|
|
85
|
+
translations: Record<string, Record<string, string>>;
|
|
86
|
+
tagVocabulary: readonly TagVocabularyEntry[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The serializable route-context (from the virtual module). */
|
|
90
|
+
export const ctx = routeContext as unknown as RouteContextPayload;
|
|
91
|
+
export const settings: Settings = ctx.settings;
|
|
92
|
+
const translations = ctx.translations;
|
|
93
|
+
const tagVocabulary = ctx.tagVocabulary;
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// i18n — reconstruct a FactoryI18n from settings + translations (Decision 1).
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
export const defaultLocale: string = settings.defaultLocale;
|
|
100
|
+
export const locales: readonly string[] = [
|
|
101
|
+
defaultLocale,
|
|
102
|
+
...Object.keys(settings.locales),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
export function getLocaleConfig(locale: string): { label: string; dir: string } | undefined {
|
|
106
|
+
return (settings.locales as Record<string, { label: string; dir: string } | undefined>)[locale];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getLocaleLabel(locale: string): string {
|
|
110
|
+
if (locale === defaultLocale) return defaultLocale.toUpperCase();
|
|
111
|
+
return getLocaleConfig(locale)?.label ?? locale.toUpperCase();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function t(key: string, locale: string = defaultLocale): string {
|
|
115
|
+
return translations[locale]?.[key] ?? translations[defaultLocale]?.[key] ?? key;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const i18n: FactoryI18n = {
|
|
119
|
+
defaultLocale,
|
|
120
|
+
locales,
|
|
121
|
+
getLocaleLabel,
|
|
122
|
+
t,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// URL helpers (@takazudo/zudo-doc/url-helpers).
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export const urlHelpers: UrlHelpers = makeUrlHelpers(settings, i18n);
|
|
130
|
+
export const {
|
|
131
|
+
withBase,
|
|
132
|
+
stripBase,
|
|
133
|
+
docsUrl,
|
|
134
|
+
versionedDocsUrl,
|
|
135
|
+
navHref,
|
|
136
|
+
isDefaultLocaleOnlyPath,
|
|
137
|
+
absoluteUrl,
|
|
138
|
+
isExternal,
|
|
139
|
+
resolveHref,
|
|
140
|
+
buildLocaleLinks,
|
|
141
|
+
} = urlHelpers;
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// nav-scope (@takazudo/zudo-doc/nav-scope) — parameterized by settings.headerNav.
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
export function getCategoryOrder(): string[] {
|
|
148
|
+
return getCategoryOrderBase(settings.headerNav);
|
|
149
|
+
}
|
|
150
|
+
export function getNavSectionForSlug(slug: string): string | undefined {
|
|
151
|
+
return getNavSectionForSlugBase(slug, settings.headerNav);
|
|
152
|
+
}
|
|
153
|
+
export function getNavSubtree(tree: DocNavNode[], categoryMatch?: string): DocNavNode[] {
|
|
154
|
+
return getNavSubtreeBase(tree, categoryMatch, settings.headerNav);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// extract-headings — bound to settings depth window + id strategy.
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
export function extractHeadings(body: string): HeadingItem[] {
|
|
162
|
+
return extractHeadingsBase(body, {
|
|
163
|
+
tocMinDepth: settings.tocMinDepth,
|
|
164
|
+
tocMaxDepth: settings.tocMaxDepth,
|
|
165
|
+
strategy: settings.headingIdStrategy,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// tag-helpers — collectTags reconstructed package-side (the host version lives
|
|
171
|
+
// in src/utils/tags.ts because it depends on the host DocsEntry; here it is
|
|
172
|
+
// parameterized by the virtual-module tagVocabulary + settings.tagGovernance).
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
export interface TagInfo {
|
|
176
|
+
tag: string;
|
|
177
|
+
count: number;
|
|
178
|
+
docs: { slug: string; title: string; description?: string }[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getVocab(): readonly TagVocabularyEntry[] | false {
|
|
182
|
+
return settings.tagVocabulary ? tagVocabulary : false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function resolveTagBound(raw: string) {
|
|
186
|
+
return resolveTag(raw, getVocab(), settings.tagGovernance);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function collectTags(
|
|
190
|
+
entries: ReadonlyArray<{ id: string; data: { slug?: string; title?: string; description?: string; tags?: string[] } }>,
|
|
191
|
+
slugFn: (id: string, data: { slug?: string }) => string,
|
|
192
|
+
): Map<string, TagInfo> {
|
|
193
|
+
const tagMap = new Map<string, TagInfo>();
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
const rawTags = entry.data.tags ?? [];
|
|
196
|
+
const slug = slugFn(entry.id, entry.data);
|
|
197
|
+
const seen = new Set<string>();
|
|
198
|
+
for (const raw of rawTags) {
|
|
199
|
+
const resolved = resolveTagBound(raw);
|
|
200
|
+
if (resolved.deprecated) continue;
|
|
201
|
+
if (seen.has(resolved.canonical)) continue;
|
|
202
|
+
seen.add(resolved.canonical);
|
|
203
|
+
if (!tagMap.has(resolved.canonical)) {
|
|
204
|
+
tagMap.set(resolved.canonical, { tag: resolved.canonical, count: 0, docs: [] });
|
|
205
|
+
}
|
|
206
|
+
const info = tagMap.get(resolved.canonical)!;
|
|
207
|
+
info.count++;
|
|
208
|
+
info.docs.push({
|
|
209
|
+
slug,
|
|
210
|
+
title: entry.data.title ?? "",
|
|
211
|
+
description: entry.data.description,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return tagMap;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Nav-source resolver (@takazudo/zudo-doc/nav-source-docs).
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
const navSourceDocs: NavSourceDocsAPI = createNavSourceDocs({
|
|
223
|
+
defaultLocale,
|
|
224
|
+
docsDir: settings.docsDir,
|
|
225
|
+
getVersions: () => settings.versions,
|
|
226
|
+
getLocaleConfig,
|
|
227
|
+
loadCategoryMeta: loadCategoryMeta as (dir: string) => Map<string, CategoryMeta>,
|
|
228
|
+
isNavVisible,
|
|
229
|
+
isDefaultLocaleOnlyPath,
|
|
230
|
+
stableDocs,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export const {
|
|
234
|
+
resolveNavSource,
|
|
235
|
+
resolveVersionedLocaleSource,
|
|
236
|
+
loadNavSourceDocs,
|
|
237
|
+
} = navSourceDocs;
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// doc-route-entries (@takazudo/zudo-doc/doc-route-entries).
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
const docRouteEntries: DocRouteEntriesAPI = createDocRouteEntries({
|
|
244
|
+
buildNavTree: (docs: DocPageEntry[], locale: string, categoryMeta: Map<string, unknown>) =>
|
|
245
|
+
buildNavTree(
|
|
246
|
+
docs,
|
|
247
|
+
locale,
|
|
248
|
+
categoryMeta as Map<string, CategoryMeta>,
|
|
249
|
+
(slug, loc) => docsUrl(slug, loc),
|
|
250
|
+
),
|
|
251
|
+
buildBreadcrumbs: (tree: DocNavNode[], slug: string, locale: string, urlFor?: (slug: string) => string) =>
|
|
252
|
+
buildBreadcrumbs(
|
|
253
|
+
tree,
|
|
254
|
+
slug,
|
|
255
|
+
locale === defaultLocale ? withBase("/") : withBase(`/${locale}/`),
|
|
256
|
+
urlFor,
|
|
257
|
+
),
|
|
258
|
+
collectAutoIndexNodes,
|
|
259
|
+
getNavSectionForSlug,
|
|
260
|
+
getNavSubtree,
|
|
261
|
+
toRouteSlug,
|
|
262
|
+
toSlugParams,
|
|
263
|
+
extractHeadings,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
export const { buildDocRouteEntries } = docRouteEntries;
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// route-enumerators (@takazudo/zudo-doc/route-enumerators).
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
const routeEnumerators: RouteEnumeratorsAPI = createRouteEnumerators({
|
|
273
|
+
defaultLocale,
|
|
274
|
+
getLocaleKeys: () => Object.keys(settings.locales),
|
|
275
|
+
getVersions: () => settings.versions,
|
|
276
|
+
getDocTags: () => settings.docTags,
|
|
277
|
+
docsUrl,
|
|
278
|
+
versionedDocsUrl,
|
|
279
|
+
withBase,
|
|
280
|
+
loadDocs: (collectionName: string) => stableDocs(collectionName) as unknown as DocsEntryForTags[],
|
|
281
|
+
isDefaultLocaleOnlyPath,
|
|
282
|
+
collectTags: (docs: DocsEntryForTags[], slugFn): Map<string, TagInfoForEnum> =>
|
|
283
|
+
collectTags(docs as unknown as Array<{ id: string; data: { slug?: string; title?: string; description?: string; tags?: string[] } }>, slugFn) as unknown as Map<string, TagInfoForEnum>,
|
|
284
|
+
toRouteSlug,
|
|
285
|
+
buildNavTree: (docs: DocPageEntry[], locale: string, categoryMeta: Map<string, unknown>) =>
|
|
286
|
+
buildNavTree(docs, locale, categoryMeta as Map<string, CategoryMeta>, (slug, loc) => docsUrl(slug, loc)),
|
|
287
|
+
collectAutoIndexNodes,
|
|
288
|
+
resolveNavSource,
|
|
289
|
+
resolveVersionedLocaleSource,
|
|
290
|
+
mergeLocaleDocs: mergeLocaleDocs as RouteEnumeratorsAPIDeps["mergeLocaleDocs"],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Local alias so the `mergeLocaleDocs` cast above reads clearly.
|
|
294
|
+
type RouteEnumeratorsAPIDeps = Parameters<typeof createRouteEnumerators>[0];
|
|
295
|
+
|
|
296
|
+
export const {
|
|
297
|
+
enumerateDocsRoutes,
|
|
298
|
+
enumerateTagsRoutes,
|
|
299
|
+
enumerateVersionedRoutes,
|
|
300
|
+
enumerateAllRoutes,
|
|
301
|
+
} = routeEnumerators;
|
|
302
|
+
|
|
303
|
+
// Re-export the pure nav-tree helpers entrypoints reach for directly.
|
|
304
|
+
export {
|
|
305
|
+
buildNavTree,
|
|
306
|
+
groupSatelliteNodes,
|
|
307
|
+
findNode,
|
|
308
|
+
firstRoutedHref,
|
|
309
|
+
collectAutoIndexNodes,
|
|
310
|
+
isNavVisible,
|
|
311
|
+
stableDocs,
|
|
312
|
+
toRouteSlug,
|
|
313
|
+
toSlugParams,
|
|
314
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// routes/_docs-helpers — package-side nav-tree helpers for the package-owned
|
|
2
|
+
// route entrypoints (epic Package-First Finale #2356, A1 #2361).
|
|
3
|
+
//
|
|
4
|
+
// The host's `src/utils/docs.ts` `buildNavTree` / `buildBreadcrumbs` /
|
|
5
|
+
// `collectAutoIndexNodes` / `groupSatelliteNodes` / `findNode` /
|
|
6
|
+
// `firstRoutedHref` are host-only wrappers around the package primitive
|
|
7
|
+
// `buildSidebarTree` (`@takazudo/zudo-doc/sidebar-tree`). The package route
|
|
8
|
+
// entrypoints inject these same helpers into the doc-route / nav-source /
|
|
9
|
+
// nav-wrapper factories, so this module reproduces them WITHOUT the host `@/`
|
|
10
|
+
// alias — the "+ package `docs` helpers" note in the ADR's host-dep table.
|
|
11
|
+
//
|
|
12
|
+
// Faithful to the host derivation (root-index node synthesis, NavNode shape,
|
|
13
|
+
// satellite grouping, breadcrumb walk) but without the host-only LRU /
|
|
14
|
+
// identity caches: build-time array-identity stability already comes from the
|
|
15
|
+
// snapshot-anchored `stableDocs` (below) + `memoizeDerived` in the factories.
|
|
16
|
+
//
|
|
17
|
+
// `stableDocs` (the content bridge — Decision 5) imports `@takazudo/zfb/content`
|
|
18
|
+
// directly (NOT the host `zfb/content` tsconfig alias — Decision 2), anchoring
|
|
19
|
+
// the bridged + draft-filtered array on the snapshot so repeat callers within a
|
|
20
|
+
// build get the SAME instance and the nav-tree / doc-route memos short-circuit.
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
getCollection,
|
|
24
|
+
getContentSnapshot,
|
|
25
|
+
type CollectionEntry,
|
|
26
|
+
} from "@takazudo/zfb/content";
|
|
27
|
+
import {
|
|
28
|
+
buildSidebarTree,
|
|
29
|
+
type CategoryMeta,
|
|
30
|
+
type SidebarNode,
|
|
31
|
+
} from "@takazudo/zudo-doc/sidebar-tree";
|
|
32
|
+
import { toRouteSlug } from "@takazudo/zudo-doc/slug";
|
|
33
|
+
import type { DocNavNode, DocPageEntry, DocPageFrontmatter } from "@takazudo/zudo-doc/doc-page-props";
|
|
34
|
+
|
|
35
|
+
export type { CategoryMeta, DocNavNode, DocPageEntry };
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Content bridge — snapshot-anchored stable docs (Decision 5)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/** The stable per-build anchor array for a collection — the raw readonly
|
|
42
|
+
* snapshot array zfb installs once. `undefined` on the fs-fallback path. */
|
|
43
|
+
function snapshotAnchor(name: string): readonly unknown[] | undefined {
|
|
44
|
+
return getContentSnapshot()?.collections[name];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const bridgedByAnchor = new WeakMap<object, DocPageEntry[]>();
|
|
48
|
+
|
|
49
|
+
/** Bridge a raw zfb collection entry onto the `DocPageEntry` shape the factories
|
|
50
|
+
* expect (Astro-compat `id`/`collection`, with `index` suffix stripped via the
|
|
51
|
+
* canonical root-slug rule). */
|
|
52
|
+
function bridgeEntry(
|
|
53
|
+
e: CollectionEntry<DocPageFrontmatter>,
|
|
54
|
+
collectionName: string,
|
|
55
|
+
): DocPageEntry {
|
|
56
|
+
return {
|
|
57
|
+
slug: e.slug,
|
|
58
|
+
id: toRouteSlug(e.slug),
|
|
59
|
+
collection: collectionName,
|
|
60
|
+
data: e.data,
|
|
61
|
+
body: e.body,
|
|
62
|
+
module_specifier: e.module_specifier,
|
|
63
|
+
Content: e.Content,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildBridged(collectionName: string): DocPageEntry[] {
|
|
68
|
+
const raw = getCollection<DocPageFrontmatter>(collectionName);
|
|
69
|
+
return raw.map((e) => bridgeEntry(e, collectionName)).filter((d) => !d.data.draft);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Identity-stable, draft-filtered `DocPageEntry[]` for a collection. Returns the
|
|
74
|
+
* SAME array instance on every call within one build (anchored on the snapshot
|
|
75
|
+
* array); the no-snapshot (fs-fallback / unit-test) path computes fresh and is
|
|
76
|
+
* deliberately unmemoized. Passed as `stableDocs` to `createNavSourceDocs`.
|
|
77
|
+
*/
|
|
78
|
+
export function stableDocs(collectionName: string): DocPageEntry[] {
|
|
79
|
+
const anchor = snapshotAnchor(collectionName);
|
|
80
|
+
if (anchor === undefined) return buildBridged(collectionName);
|
|
81
|
+
const cached = bridgedByAnchor.get(anchor);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
const built = buildBridged(collectionName);
|
|
84
|
+
bridgedByAnchor.set(anchor, built);
|
|
85
|
+
return built;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Nav-tree helpers — ported from src/utils/docs.ts onto buildSidebarTree
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/** Filter predicate: true when a doc should appear in navigation. */
|
|
93
|
+
export function isNavVisible(doc: DocPageEntry): boolean {
|
|
94
|
+
return !doc.data.unlisted && !doc.data.standalone;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A docs-href builder: `(slug, locale) => url`. */
|
|
98
|
+
export type BuildHref = (slug: string, locale: string) => string;
|
|
99
|
+
|
|
100
|
+
export interface BuildNavTreeOptions {
|
|
101
|
+
buildHref?: BuildHref;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toNavNode(node: SidebarNode): DocNavNode {
|
|
105
|
+
return {
|
|
106
|
+
slug: node.id,
|
|
107
|
+
label: node.label,
|
|
108
|
+
description: node.description,
|
|
109
|
+
position: node.sidebar_position ?? 999,
|
|
110
|
+
href: node.href,
|
|
111
|
+
hasPage: node.hasPage,
|
|
112
|
+
children: node.children.map(toNavNode),
|
|
113
|
+
sortOrder: node.sortOrder ?? "asc",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Last entry whose package-derived slug is empty ("") — the entry the shared
|
|
118
|
+
* builder skips. Last one wins (mirrors the host builder's overwrite). */
|
|
119
|
+
function findRootIndexDoc(docs: DocPageEntry[]): DocPageEntry | undefined {
|
|
120
|
+
let found: DocPageEntry | undefined;
|
|
121
|
+
for (const d of docs) {
|
|
122
|
+
const slug = d.data.slug ?? d.id.replace(/\/index$/, "");
|
|
123
|
+
if (slug === "") found = d;
|
|
124
|
+
}
|
|
125
|
+
return found;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toRootNavNode(
|
|
129
|
+
doc: DocPageEntry,
|
|
130
|
+
locale: string,
|
|
131
|
+
buildHref: BuildHref,
|
|
132
|
+
categoryMeta?: Map<string, CategoryMeta>,
|
|
133
|
+
): DocNavNode {
|
|
134
|
+
const meta = categoryMeta?.get("");
|
|
135
|
+
const noPage = doc.data.category_no_page ?? meta?.noPage;
|
|
136
|
+
const sortOrder =
|
|
137
|
+
(doc.data.category_sort_order as "asc" | "desc" | undefined) ?? meta?.sortOrder ?? "asc";
|
|
138
|
+
return {
|
|
139
|
+
slug: "",
|
|
140
|
+
label:
|
|
141
|
+
(doc.data.sidebar_label as string | undefined) ??
|
|
142
|
+
doc.data.title ??
|
|
143
|
+
meta?.label ??
|
|
144
|
+
"",
|
|
145
|
+
description: doc.data.description ?? meta?.description,
|
|
146
|
+
position: (doc.data.sidebar_position as number | undefined) ?? meta?.position ?? 999,
|
|
147
|
+
href: noPage ? undefined : buildHref("", locale),
|
|
148
|
+
hasPage: noPage !== true,
|
|
149
|
+
children: [],
|
|
150
|
+
sortOrder,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a recursive navigation tree from a flat doc collection. Delegates tree
|
|
156
|
+
* construction to the shared `buildSidebarTree`; keeps the host-side concerns
|
|
157
|
+
* (DocNavNode shape, root-index node synthesis). `buildHref` parameterizes the
|
|
158
|
+
* href space (default: the injected `docsUrl`). Unlike the host copy this drops
|
|
159
|
+
* the LRU / identity caches — the snapshot-anchored `stableDocs` already makes
|
|
160
|
+
* the input arrays identity-stable so the factory-level `memoizeDerived` short-
|
|
161
|
+
* circuits per build.
|
|
162
|
+
*/
|
|
163
|
+
export function buildNavTree(
|
|
164
|
+
docs: DocPageEntry[],
|
|
165
|
+
locale: string,
|
|
166
|
+
categoryMeta: Map<string, CategoryMeta> | undefined,
|
|
167
|
+
buildHref: BuildHref,
|
|
168
|
+
options?: BuildNavTreeOptions,
|
|
169
|
+
): DocNavNode[] {
|
|
170
|
+
const href: BuildHref = options?.buildHref ?? buildHref;
|
|
171
|
+
const sidebarTree = buildSidebarTree(
|
|
172
|
+
docs.map((d) => ({ id: d.id, data: d.data })),
|
|
173
|
+
locale,
|
|
174
|
+
{
|
|
175
|
+
categoryMeta,
|
|
176
|
+
buildHref: (slug, loc) => href(slug, loc),
|
|
177
|
+
// Host call sites own visibility; disable the builder's default filter so
|
|
178
|
+
// breadcrumb (unfiltered) and nav (pre-filtered) paths are unchanged.
|
|
179
|
+
isNavVisible: () => true,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
const result = sidebarTree.map(toNavNode);
|
|
183
|
+
|
|
184
|
+
const rootDoc = findRootIndexDoc(docs);
|
|
185
|
+
if (rootDoc) {
|
|
186
|
+
result.push(toRootNavNode(rootDoc, locale, href, categoryMeta));
|
|
187
|
+
result.sort((a, b) => {
|
|
188
|
+
const posCompare = a.position - b.position;
|
|
189
|
+
if (posCompare !== 0) return posCompare;
|
|
190
|
+
return a.slug.localeCompare(b.slug);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Group "satellite" nodes (slug-prefix siblings) under their primary node. */
|
|
197
|
+
export function groupSatelliteNodes(tree: DocNavNode[], prefixes: string[]): DocNavNode[] {
|
|
198
|
+
const result = [...tree];
|
|
199
|
+
for (const prefix of prefixes) {
|
|
200
|
+
const primaryIdx = result.findIndex((n) => n.slug === prefix);
|
|
201
|
+
if (primaryIdx < 0) continue;
|
|
202
|
+
const primary = result[primaryIdx];
|
|
203
|
+
if (!primary) continue;
|
|
204
|
+
const satelliteIdxs: number[] = [];
|
|
205
|
+
for (let i = 0; i < result.length; i++) {
|
|
206
|
+
const node = result[i];
|
|
207
|
+
if (node && i !== primaryIdx && node.slug.startsWith(`${prefix}-`)) {
|
|
208
|
+
satelliteIdxs.push(i);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (satelliteIdxs.length === 0) continue;
|
|
212
|
+
const extraChildren: DocNavNode[] = [];
|
|
213
|
+
for (const idx of satelliteIdxs) {
|
|
214
|
+
const node = result[idx];
|
|
215
|
+
if (node) extraChildren.push(node);
|
|
216
|
+
}
|
|
217
|
+
result[primaryIdx] = { ...primary, children: [...primary.children, ...extraChildren] };
|
|
218
|
+
for (const idx of satelliteIdxs.reverse()) result.splice(idx, 1);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Find a node by slug anywhere in the tree. */
|
|
224
|
+
export function findNode(nodes: DocNavNode[], slug: string): DocNavNode | undefined {
|
|
225
|
+
for (const node of nodes) {
|
|
226
|
+
if (node.slug === slug) return node;
|
|
227
|
+
const found = findNode(node.children, slug);
|
|
228
|
+
if (found) return found;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Href of the first routed descendant, walking children depth-first. */
|
|
234
|
+
export function firstRoutedHref(node: DocNavNode): string | undefined {
|
|
235
|
+
for (const child of node.children) {
|
|
236
|
+
if (child.hasPage && child.href) return child.href;
|
|
237
|
+
const nested = firstRoutedHref(child);
|
|
238
|
+
if (nested) return nested;
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Collect all category nodes that have children but no page (no index.mdx). */
|
|
244
|
+
export function collectAutoIndexNodes(nodes: DocNavNode[]): DocNavNode[] {
|
|
245
|
+
const result: DocNavNode[] = [];
|
|
246
|
+
for (const node of nodes) {
|
|
247
|
+
if (!node.hasPage && node.children.length > 0 && node.href) result.push(node);
|
|
248
|
+
result.push(...collectAutoIndexNodes(node.children));
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface BreadcrumbItem {
|
|
254
|
+
label: string;
|
|
255
|
+
href?: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Build breadcrumb trail by walking the nav tree. `homeHref` is the locale
|
|
259
|
+
* docs root (injected). `hrefFor` optionally remaps intermediate crumbs into
|
|
260
|
+
* a versioned URL space. */
|
|
261
|
+
export function buildBreadcrumbs(
|
|
262
|
+
tree: DocNavNode[],
|
|
263
|
+
slug: string,
|
|
264
|
+
homeHref: string,
|
|
265
|
+
hrefFor?: (slug: string) => string,
|
|
266
|
+
): BreadcrumbItem[] {
|
|
267
|
+
const parts = slug.split("/");
|
|
268
|
+
const crumbs: BreadcrumbItem[] = [{ label: "", href: homeHref }];
|
|
269
|
+
let nodes = tree;
|
|
270
|
+
for (let i = 0; i < parts.length; i++) {
|
|
271
|
+
const partialSlug = parts.slice(0, i + 1).join("/");
|
|
272
|
+
const node = nodes.find((n) => n.slug === partialSlug);
|
|
273
|
+
if (!node) break;
|
|
274
|
+
const isLast = i === parts.length - 1;
|
|
275
|
+
const href = isLast
|
|
276
|
+
? undefined
|
|
277
|
+
: hrefFor && node.href !== undefined
|
|
278
|
+
? hrefFor(node.slug)
|
|
279
|
+
: node.href;
|
|
280
|
+
crumbs.push({ label: node.label, href });
|
|
281
|
+
nodes = node.children;
|
|
282
|
+
}
|
|
283
|
+
return crumbs;
|
|
284
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Ambient typing for the route-context virtual module emitted by the routes
|
|
2
|
+
// plugin (`addVirtualModule("virtual:zudo-doc-route-context", …)`). The module
|
|
3
|
+
// has no on-disk source — it is materialised at build by the zfb plugin runtime
|
|
4
|
+
// — so this declaration gives the package route entrypoints a typed import.
|
|
5
|
+
//
|
|
6
|
+
// The payload is SERIALIZABLE DATA ONLY (ADR Decision 1): `settings`,
|
|
7
|
+
// `translations`, `tagVocabulary`. `_context.ts` narrows it to the concrete
|
|
8
|
+
// `RouteContextPayload` at the seam.
|
|
9
|
+
declare module "virtual:zudo-doc-route-context" {
|
|
10
|
+
export const routeContext: {
|
|
11
|
+
settings: unknown;
|
|
12
|
+
translations: Record<string, Record<string, string>>;
|
|
13
|
+
tagVocabulary: ReadonlyArray<Record<string, unknown>>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Package route entrypoint: /api/ai-chat — package-owned equivalent of
|
|
2
|
+
// pages/api/ai-chat.tsx (A1 #2361). SSR route (prerender = false), injected
|
|
3
|
+
// with `opts: { prerender: false }` by the routes plugin (Decision 3).
|
|
4
|
+
//
|
|
5
|
+
// SCOPE NOTE (A1): the FULL ai-chat server handler (CORS allowlist, prompt-
|
|
6
|
+
// injection screening, KV rate limiting, audit logging, the Anthropic client)
|
|
7
|
+
// lives host-side in `pages/api/_ai-chat-*` and is NOT yet package-shipped — it
|
|
8
|
+
// depends on host-only modules and CF env bindings that the serializable
|
|
9
|
+
// route-context virtual module does not carry. This package entrypoint proves
|
|
10
|
+
// the SSR-route seam (prerender shape + demo-mode short-circuit driven from the
|
|
11
|
+
// virtual-module `settings`) and returns a safe disabled-by-default response;
|
|
12
|
+
// re-homing the full handler into the package is deferred to the render-proof
|
|
13
|
+
// follow-up (A2 #2363 / a later sub-issue). With `packageOwnedRoutes` off this
|
|
14
|
+
// is never evaluated, and even on it is dropped if the user keeps their
|
|
15
|
+
// `pages/api/ai-chat.tsx` stub (Decision 6).
|
|
16
|
+
|
|
17
|
+
import { settings } from "./_context.js";
|
|
18
|
+
|
|
19
|
+
// `frontmatter` is required by zfb's TSX page contract; without it zfb defaults
|
|
20
|
+
// the route to SSG and the `prerender = false` below is ignored.
|
|
21
|
+
export const frontmatter = { title: "AI Chat API" };
|
|
22
|
+
|
|
23
|
+
export const prerender = false;
|
|
24
|
+
|
|
25
|
+
const DISABLED_MESSAGE =
|
|
26
|
+
"This feature is disabled on this demo. Need per project setup to enable this.";
|
|
27
|
+
|
|
28
|
+
function jsonResponse(body: Record<string, unknown>, status: number): Response {
|
|
29
|
+
return new Response(JSON.stringify(body), {
|
|
30
|
+
status,
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function AiChatHandler(): Response {
|
|
36
|
+
// Package default: no real Anthropic-backed handler is wired here (see the
|
|
37
|
+
// scope note). Reply with the stable disabled message regardless of method —
|
|
38
|
+
// the host route (kept as a `pages/` stub) owns the full implementation.
|
|
39
|
+
if (settings.aiChatDemoMode === false && settings.aiAssistant) {
|
|
40
|
+
return jsonResponse(
|
|
41
|
+
{ error: "ai-chat handler not provided by the package route (see A1 #2361)" },
|
|
42
|
+
501,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return jsonResponse({ response: DISABLED_MESSAGE }, 200);
|
|
46
|
+
}
|